mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-25 03:38:04 +01:00
Enhance device management API.
1. Put a limit on the number of registered devices per account. 2. Support removing devices. 3. Support device names and created dates. 4. Support enumerating devices. // FREEBIE
This commit is contained in:
@@ -46,7 +46,9 @@ import org.whispersystems.textsecuregcm.federation.FederatedClientManager;
|
||||
import org.whispersystems.textsecuregcm.federation.FederatedPeer;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
|
||||
import org.whispersystems.textsecuregcm.mappers.DeviceLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.IOExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.InvalidWebsocketAddressExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.metrics.CpuUsageGauge;
|
||||
import org.whispersystems.textsecuregcm.metrics.FreeMemoryGauge;
|
||||
@@ -246,6 +248,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
|
||||
environment.jersey().register(new IOExceptionMapper());
|
||||
environment.jersey().register(new RateLimitExceededExceptionMapper());
|
||||
environment.jersey().register(new InvalidWebsocketAddressExceptionMapper());
|
||||
environment.jersey().register(new DeviceLimitExceededExceptionMapper());
|
||||
|
||||
environment.metrics().register(name(CpuUsageGauge.class, "cpu"), new CpuUsageGauge());
|
||||
environment.metrics().register(name(FreeMemoryGauge.class, "free_memory"), new FreeMemoryGauge());
|
||||
|
||||
@@ -282,6 +282,9 @@ public class AccountController {
|
||||
device.setSignalingKey(accountAttributes.getSignalingKey());
|
||||
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||
device.setRegistrationId(accountAttributes.getRegistrationId());
|
||||
device.setName(accountAttributes.getName());
|
||||
device.setCreated(System.currentTimeMillis());
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
|
||||
Account account = new Account();
|
||||
account.setNumber(number);
|
||||
|
||||
@@ -25,6 +25,8 @@ import org.whispersystems.textsecuregcm.auth.AuthenticationCredentials;
|
||||
import org.whispersystems.textsecuregcm.auth.AuthorizationHeader;
|
||||
import org.whispersystems.textsecuregcm.auth.InvalidAuthorizationHeaderException;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceInfo;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceInfoList;
|
||||
import org.whispersystems.textsecuregcm.entities.DeviceResponse;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
@@ -36,6 +38,7 @@ import org.whispersystems.textsecuregcm.util.VerificationCode;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.HeaderParam;
|
||||
import javax.ws.rs.PUT;
|
||||
@@ -48,6 +51,8 @@ import javax.ws.rs.core.Response;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
|
||||
@@ -56,6 +61,8 @@ public class DeviceController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DeviceController.class);
|
||||
|
||||
private static final int MAX_DEVICES = 3;
|
||||
|
||||
private final PendingDevicesManager pendingDevices;
|
||||
private final AccountsManager accounts;
|
||||
private final RateLimiters rateLimiters;
|
||||
@@ -69,15 +76,41 @@ public class DeviceController {
|
||||
this.rateLimiters = rateLimiters;
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public DeviceInfoList getDevices(@Auth Account account) {
|
||||
List<DeviceInfo> devices = new LinkedList<>();
|
||||
|
||||
for (Device device : account.getDevices()) {
|
||||
devices.add(new DeviceInfo(device.getId(), device.getName(),
|
||||
device.getLastSeen(), device.getCreated()));
|
||||
}
|
||||
|
||||
return new DeviceInfoList(devices);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@DELETE
|
||||
@Path("/{device_id}")
|
||||
public void removeDevice(@Auth Account account, @PathParam("device_id") long deviceId) {
|
||||
account.removeDevice(deviceId);
|
||||
accounts.update(account);
|
||||
}
|
||||
|
||||
@Timed
|
||||
@GET
|
||||
@Path("/provisioning/code")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public VerificationCode createDeviceToken(@Auth Account account)
|
||||
throws RateLimitExceededException
|
||||
throws RateLimitExceededException, DeviceLimitExceededException
|
||||
{
|
||||
rateLimiters.getAllocateDeviceLimiter().validate(account.getNumber());
|
||||
|
||||
if (account.getActiveDeviceCount() >= MAX_DEVICES) {
|
||||
throw new DeviceLimitExceededException(account.getDevices().size(), MAX_DEVICES);
|
||||
}
|
||||
|
||||
VerificationCode verificationCode = generateVerificationCode();
|
||||
pendingDevices.store(account.getNumber(), verificationCode.getVerificationCode());
|
||||
|
||||
@@ -92,7 +125,7 @@ public class DeviceController {
|
||||
public DeviceResponse verifyDeviceToken(@PathParam("verification_code") String verificationCode,
|
||||
@HeaderParam("Authorization") String authorizationHeader,
|
||||
@Valid AccountAttributes accountAttributes)
|
||||
throws RateLimitExceededException
|
||||
throws RateLimitExceededException, DeviceLimitExceededException
|
||||
{
|
||||
try {
|
||||
AuthorizationHeader header = AuthorizationHeader.fromFullHeader(authorizationHeader);
|
||||
@@ -115,13 +148,19 @@ public class DeviceController {
|
||||
throw new WebApplicationException(Response.status(403).build());
|
||||
}
|
||||
|
||||
if (account.get().getActiveDeviceCount() >= MAX_DEVICES) {
|
||||
throw new DeviceLimitExceededException(account.get().getDevices().size(), MAX_DEVICES);
|
||||
}
|
||||
|
||||
Device device = new Device();
|
||||
device.setName(accountAttributes.getName());
|
||||
device.setAuthenticationCredentials(new AuthenticationCredentials(password));
|
||||
device.setSignalingKey(accountAttributes.getSignalingKey());
|
||||
device.setFetchesMessages(accountAttributes.getFetchesMessages());
|
||||
device.setId(account.get().getNextDeviceId());
|
||||
device.setRegistrationId(accountAttributes.getRegistrationId());
|
||||
device.setLastSeen(Util.todayInMillis());
|
||||
device.setCreated(System.currentTimeMillis());
|
||||
|
||||
account.get().addDevice(device);
|
||||
accounts.update(account.get());
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
|
||||
public class DeviceLimitExceededException extends Exception {
|
||||
|
||||
private final int currentDevices;
|
||||
private final int maxDevices;
|
||||
|
||||
public DeviceLimitExceededException(int currentDevices, int maxDevices) {
|
||||
this.currentDevices = currentDevices;
|
||||
this.maxDevices = maxDevices;
|
||||
}
|
||||
|
||||
public int getCurrentDevices() {
|
||||
return currentDevices;
|
||||
}
|
||||
|
||||
public int getMaxDevices() {
|
||||
return maxDevices;
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,12 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
|
||||
public class AccountAttributes {
|
||||
|
||||
@JsonProperty
|
||||
@@ -31,12 +35,23 @@ public class AccountAttributes {
|
||||
@JsonProperty
|
||||
private int registrationId;
|
||||
|
||||
@JsonProperty
|
||||
@Length(max = 50, message = "This field must be less than 50 characters")
|
||||
private String name;
|
||||
|
||||
public AccountAttributes() {}
|
||||
|
||||
@VisibleForTesting
|
||||
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId) {
|
||||
this(signalingKey, fetchesMessages, registrationId, null);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public AccountAttributes(String signalingKey, boolean fetchesMessages, int registrationId, String name) {
|
||||
this.signalingKey = signalingKey;
|
||||
this.fetchesMessages = fetchesMessages;
|
||||
this.registrationId = registrationId;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getSignalingKey() {
|
||||
@@ -50,4 +65,8 @@ public class AccountAttributes {
|
||||
public int getRegistrationId() {
|
||||
return registrationId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class DeviceInfo {
|
||||
@JsonProperty
|
||||
private long id;
|
||||
|
||||
@JsonProperty
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private long lastSeen;
|
||||
|
||||
@JsonProperty
|
||||
private long created;
|
||||
|
||||
public DeviceInfo(long id, String name, long lastSeen, long created) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.lastSeen = lastSeen;
|
||||
this.created = created;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DeviceInfoList {
|
||||
|
||||
@JsonProperty
|
||||
private List<DeviceInfo> devices;
|
||||
|
||||
public DeviceInfoList(List<DeviceInfo> devices) {
|
||||
this.devices = devices;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,6 @@ public class NonLimitedAccount extends Account {
|
||||
|
||||
@Override
|
||||
public Optional<Device> getAuthenticatedDevice() {
|
||||
return Optional.of(new Device(deviceId, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis()));
|
||||
return Optional.of(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, System.currentTimeMillis(), System.currentTimeMillis()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.whispersystems.textsecuregcm.mappers;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import org.whispersystems.textsecuregcm.controllers.DeviceLimitExceededException;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
@Provider
|
||||
public class DeviceLimitExceededExceptionMapper implements ExceptionMapper<DeviceLimitExceededException> {
|
||||
@Override
|
||||
public Response toResponse(DeviceLimitExceededException exception) {
|
||||
return Response.status(411)
|
||||
.entity(new DeviceLimitExceededDetails(exception.getCurrentDevices(),
|
||||
exception.getMaxDevices()))
|
||||
.build();
|
||||
}
|
||||
|
||||
private static class DeviceLimitExceededDetails {
|
||||
@JsonProperty
|
||||
private int current;
|
||||
@JsonProperty
|
||||
private int max;
|
||||
|
||||
public DeviceLimitExceededDetails(int current, int max) {
|
||||
this.current = current;
|
||||
this.max = max;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,10 @@ public class Account {
|
||||
this.devices.add(device);
|
||||
}
|
||||
|
||||
public void removeDevice(long deviceId) {
|
||||
this.devices.remove(new Device(deviceId, null, null, null, null, null, null, null, false, 0, null, 0, 0));
|
||||
}
|
||||
|
||||
public Set<Device> getDevices() {
|
||||
return devices;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ public class Device {
|
||||
@JsonProperty
|
||||
private long id;
|
||||
|
||||
@JsonProperty
|
||||
private String name;
|
||||
|
||||
@JsonProperty
|
||||
private String authToken;
|
||||
|
||||
@@ -64,14 +67,19 @@ public class Device {
|
||||
@JsonProperty
|
||||
private long lastSeen;
|
||||
|
||||
@JsonProperty
|
||||
private long created;
|
||||
|
||||
public Device() {}
|
||||
|
||||
public Device(long id, String authToken, String salt,
|
||||
public Device(long id, String name, String authToken, String salt,
|
||||
String signalingKey, String gcmId, String apnId,
|
||||
String voipApnId, boolean fetchesMessages,
|
||||
int registrationId, SignedPreKey signedPreKey, long lastSeen)
|
||||
int registrationId, SignedPreKey signedPreKey,
|
||||
long lastSeen, long created)
|
||||
{
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.authToken = authToken;
|
||||
this.salt = salt;
|
||||
this.signalingKey = signalingKey;
|
||||
@@ -82,6 +90,7 @@ public class Device {
|
||||
this.registrationId = registrationId;
|
||||
this.signedPreKey = signedPreKey;
|
||||
this.lastSeen = lastSeen;
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public String getApnId() {
|
||||
@@ -112,6 +121,14 @@ public class Device {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
public void setCreated(long created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public long getCreated() {
|
||||
return this.created;
|
||||
}
|
||||
|
||||
public String getGcmId() {
|
||||
return gcmId;
|
||||
}
|
||||
@@ -132,6 +149,14 @@ public class Device {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public void setAuthenticationCredentials(AuthenticationCredentials credentials) {
|
||||
this.authToken = credentials.getHashedAuthenticationToken();
|
||||
this.salt = credentials.getSalt();
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class SystemMapper {
|
||||
@@ -11,6 +12,7 @@ public class SystemMapper {
|
||||
static {
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
|
||||
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
public static ObjectMapper getMapper() {
|
||||
|
||||
Reference in New Issue
Block a user