Support for getting/setting remote config variables

This commit is contained in:
Moxie Marlinspike
2019-12-13 14:57:57 -08:00
parent 9d77f8dcd2
commit 08a70664f4
14 changed files with 722 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
package org.whispersystems.textsecuregcm.controllers;
import com.codahale.metrics.annotation.Timed;
import com.google.common.annotations.VisibleForTesting;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfig;
import org.whispersystems.textsecuregcm.entities.UserRemoteConfigList;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.RemoteConfig;
import org.whispersystems.textsecuregcm.storage.RemoteConfigsManager;
import org.whispersystems.textsecuregcm.util.Conversions;
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;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.stream.Collectors;
import io.dropwizard.auth.Auth;
@Path("/v1/config")
public class RemoteConfigController {
private final RemoteConfigsManager remoteConfigsManager;
private final List<String> configAuthTokens;
public RemoteConfigController(RemoteConfigsManager remoteConfigsManager, List<String> configAuthTokens) {
this.remoteConfigsManager = remoteConfigsManager;
this.configAuthTokens = configAuthTokens;
}
@Timed
@GET
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public UserRemoteConfigList getAll(@Auth Account account) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA1");
byte[] number = account.getNumber().getBytes();
return new UserRemoteConfigList(remoteConfigsManager.getAll().stream().map(config -> new UserRemoteConfig(config.getName(),
isInBucket(digest, number,
config.getName().getBytes(),
config.getPercentage())))
.collect(Collectors.toList()));
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
@Timed
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public void set(@HeaderParam("Config-Token") String configToken, @Valid RemoteConfig config) {
if (!isAuthorized(configToken)) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
remoteConfigsManager.set(config);
}
@Timed
@DELETE
@Path("/{name}")
public void delete(@HeaderParam("Config-Token") String configToken, @PathParam("name") String name) {
if (!isAuthorized(configToken)) {
throw new WebApplicationException(Response.Status.UNAUTHORIZED);
}
remoteConfigsManager.delete(name);
}
@VisibleForTesting
public static boolean isInBucket(MessageDigest digest, byte[] user, byte[] configName, int configPercentage) {
digest.update(user);
byte[] hash = digest.digest(configName);
int bucket = (int)(Math.abs(Conversions.byteArrayToLong(hash)) % 100);
return bucket < configPercentage;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean isAuthorized(String configToken) {
return configAuthTokens.stream().anyMatch(authorized -> MessageDigest.isEqual(authorized.getBytes(), configToken.getBytes()));
}
}