Only allow linking desktop clients if they support the third-generation GV2 capability.
This commit is contained in:
parent
656e6db846
commit
e6d4620af1
|
@ -58,6 +58,10 @@ import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import io.dropwizard.auth.Auth;
|
import io.dropwizard.auth.Auth;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ua.ClientPlatform;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ua.UnrecognizedUserAgentException;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ua.UserAgent;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ua.UserAgentUtil;
|
||||||
|
|
||||||
@Path("/v1/devices")
|
@Path("/v1/devices")
|
||||||
public class DeviceController {
|
public class DeviceController {
|
||||||
|
@ -156,6 +160,7 @@ public class DeviceController {
|
||||||
@Path("/{verification_code}")
|
@Path("/{verification_code}")
|
||||||
public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode,
|
public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode,
|
||||||
@HeaderParam("Authorization") String authorizationHeader,
|
@HeaderParam("Authorization") String authorizationHeader,
|
||||||
|
@HeaderParam("User-Agent") String userAgent,
|
||||||
@Valid AccountAttributes accountAttributes)
|
@Valid AccountAttributes accountAttributes)
|
||||||
throws RateLimitExceededException, DeviceLimitExceededException
|
throws RateLimitExceededException, DeviceLimitExceededException
|
||||||
{
|
{
|
||||||
|
@ -191,7 +196,7 @@ public class DeviceController {
|
||||||
}
|
}
|
||||||
|
|
||||||
final DeviceCapabilities capabilities = accountAttributes.getCapabilities();
|
final DeviceCapabilities capabilities = accountAttributes.getCapabilities();
|
||||||
if (capabilities != null && isCapabilityDowngrade(account.get(), capabilities)) {
|
if (capabilities != null && isCapabilityDowngrade(account.get(), capabilities, userAgent)) {
|
||||||
throw new WebApplicationException(Response.status(409).build());
|
throw new WebApplicationException(Response.status(409).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,9 +246,42 @@ public class DeviceController {
|
||||||
return new VerificationCode(randomInt);
|
return new VerificationCode(randomInt);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities) {
|
private boolean isCapabilityDowngrade(Account account, DeviceCapabilities capabilities, String userAgent) {
|
||||||
// Only iOS and desktop clients can be linked devices right now, and both require the second-gen GV2 capability to
|
boolean isDowngrade = false;
|
||||||
// support GV2.
|
|
||||||
return (!capabilities.isGv2_2() && account.isGroupsV2Supported());
|
if (account.isGroupsV2Supported()) {
|
||||||
|
try {
|
||||||
|
switch (UserAgentUtil.parseUserAgentString(userAgent).getPlatform()) {
|
||||||
|
case ANDROID: {
|
||||||
|
if (!capabilities.isGv2() && !capabilities.isGv2_2() && !capabilities.isGv2_3()) {
|
||||||
|
isDowngrade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case IOS: {
|
||||||
|
if (!capabilities.isGv2_2() && !capabilities.isGv2_3()) {
|
||||||
|
isDowngrade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case DESKTOP: {
|
||||||
|
if (!capabilities.isGv2_3()) {
|
||||||
|
isDowngrade = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (final UnrecognizedUserAgentException e) {
|
||||||
|
// If we can't parse the UA string, the client is for sure too old to support groups V2
|
||||||
|
isDowngrade = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isDowngrade;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,13 @@
|
||||||
package org.whispersystems.textsecuregcm.tests.controllers;
|
package org.whispersystems.textsecuregcm.tests.controllers;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import junitparams.JUnitParamsRunner;
|
||||||
|
import junitparams.Parameters;
|
||||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
|
||||||
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
import org.whispersystems.textsecuregcm.auth.StoredVerificationCode;
|
||||||
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
import org.whispersystems.textsecuregcm.controllers.DeviceController;
|
||||||
|
@ -53,6 +56,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@RunWith(JUnitParamsRunner.class)
|
||||||
public class DeviceControllerTest {
|
public class DeviceControllerTest {
|
||||||
@Path("/v1/devices")
|
@Path("/v1/devices")
|
||||||
static class DumbVerificationDeviceController extends DeviceController {
|
static class DumbVerificationDeviceController extends DeviceController {
|
||||||
|
@ -222,17 +226,42 @@ public class DeviceControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void deviceDowngradeCapabilitiesTest() throws Exception {
|
@Parameters(method = "argumentsForDeviceDowngradeCapabilitiesTest")
|
||||||
Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(false, false, false, true, false);
|
public void deviceDowngradeCapabilitiesTest(final String userAgent, final boolean gv2, final boolean gv2_2, final boolean gv2_3, final int expectedStatus) throws Exception {
|
||||||
|
Device.DeviceCapabilities deviceCapabilities = new Device.DeviceCapabilities(gv2, gv2_2, gv2_3, true, false);
|
||||||
AccountAttributes accountAttributes = new AccountAttributes("keykeykeykey", false, 1234, null, null, null, null, true, deviceCapabilities);
|
AccountAttributes accountAttributes = new AccountAttributes("keykeykeykey", false, 1234, null, null, null, null, true, deviceCapabilities);
|
||||||
Response response = resources.getJerseyTest()
|
Response response = resources.getJerseyTest()
|
||||||
.target("/v1/devices/5678901")
|
.target("/v1/devices/5678901")
|
||||||
.request()
|
.request()
|
||||||
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, "password1"))
|
||||||
|
.header("User-Agent", userAgent)
|
||||||
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
.put(Entity.entity(accountAttributes, MediaType.APPLICATION_JSON_TYPE));
|
||||||
|
|
||||||
assertThat(response.getStatus()).isEqualTo(409);
|
assertThat(response.getStatus()).isEqualTo(expectedStatus);
|
||||||
|
|
||||||
|
if (expectedStatus >= 300) {
|
||||||
verifyNoMoreInteractions(messagesManager);
|
verifyNoMoreInteractions(messagesManager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Object argumentsForDeviceDowngradeCapabilitiesTest() {
|
||||||
|
return new Object[] {
|
||||||
|
new Object[] { "Signal-Android/4.68.3 Android/25", false, false, false, 409 },
|
||||||
|
new Object[] { "Signal-Android/4.68.3 Android/25", true, false, false, 200 },
|
||||||
|
new Object[] { "Signal-Android/4.68.3 Android/25", false, true, false, 200 },
|
||||||
|
new Object[] { "Signal-Android/4.68.3 Android/25", false, false, true, 200 },
|
||||||
|
new Object[] { "Signal-iOS/3.9.0", false, false, false, 409 },
|
||||||
|
new Object[] { "Signal-iOS/3.9.0", true, false, false, 409 },
|
||||||
|
new Object[] { "Signal-iOS/3.9.0", false, true, false, 200 },
|
||||||
|
new Object[] { "Signal-iOS/3.9.0", false, false, true, 200 },
|
||||||
|
new Object[] { "Signal-Desktop/1.32.0-beta.3", false, false, false, 409 },
|
||||||
|
new Object[] { "Signal-Desktop/1.32.0-beta.3", true, false, false, 409 },
|
||||||
|
new Object[] { "Signal-Desktop/1.32.0-beta.3", false, true, false, 409 },
|
||||||
|
new Object[] { "Signal-Desktop/1.32.0-beta.3", false, false, true, 200 },
|
||||||
|
new Object[] { "Old client with unparsable UA", false, false, false, 409 },
|
||||||
|
new Object[] { "Old client with unparsable UA", true, false, false, 409 },
|
||||||
|
new Object[] { "Old client with unparsable UA", false, true, false, 409 },
|
||||||
|
new Object[] { "Old client with unparsable UA", false, false, true, 409 }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue