org.whispersystems.signalservice.internal.push.PushServiceSocket Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of signal-service-java Show documentation
Show all versions of signal-service-java Show documentation
Signal Service communication library for Java
/*
* Copyright (C) 2014-2017 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.state.PreKeyBundle;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.DigestingOutputStream;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.push.exceptions.RateLimitException;
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.configuration.SignalUrl;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest;
import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest;
import org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse;
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
import org.whispersystems.signalservice.internal.push.http.OutputStreamFactory;
import org.whispersystems.signalservice.internal.util.Base64;
import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager;
import org.whispersystems.signalservice.internal.util.JsonUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import okhttp3.Call;
import okhttp3.ConnectionSpec;
import okhttp3.Credentials;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* @author Moxie Marlinspike
*/
public class PushServiceSocket {
private static final String TAG = PushServiceSocket.class.getSimpleName();
private static final String CREATE_ACCOUNT_SMS_PATH = "/v1/accounts/sms/code/%s?client=%s";
private static final String CREATE_ACCOUNT_VOICE_PATH = "/v1/accounts/voice/code/%s";
private static final String VERIFY_ACCOUNT_CODE_PATH = "/v1/accounts/code/%s";
private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/";
private static final String TURN_SERVER_INFO = "/v1/accounts/turn";
private static final String SET_ACCOUNT_ATTRIBUTES = "/v1/accounts/attributes/";
private static final String PIN_PATH = "/v1/accounts/pin/";
private static final String PREKEY_METADATA_PATH = "/v2/keys/";
private static final String PREKEY_PATH = "/v2/keys/%s";
private static final String PREKEY_DEVICE_PATH = "/v2/keys/%s/%s";
private static final String SIGNED_PREKEY_PATH = "/v2/keys/signed";
private static final String PROVISIONING_CODE_PATH = "/v1/devices/provisioning/code";
private static final String PROVISIONING_MESSAGE_PATH = "/v1/provisioning/%s";
private static final String DEVICE_PATH = "/v1/devices/%s";
private static final String DIRECTORY_TOKENS_PATH = "/v1/directory/tokens";
private static final String DIRECTORY_VERIFY_PATH = "/v1/directory/%s";
private static final String DIRECTORY_AUTH_PATH = "/v1/directory/auth";
private static final String MESSAGE_PATH = "/v1/messages/%s";
private static final String SENDER_ACK_MESSAGE_PATH = "/v1/messages/%s/%d";
private static final String UUID_ACK_MESSAGE_PATH = "/v1/messages/uuid/%s";
private static final String ATTACHMENT_PATH = "/v1/attachments/%s";
private static final String PROFILE_PATH = "/v1/profile/%s";
private static final String SENDER_CERTIFICATE_PATH = "/v1/certificate/delivery";
private static final Map NO_HEADERS = Collections.emptyMap();
private long soTimeoutMillis = TimeUnit.SECONDS.toMillis(30);
private final Set connections = new HashSet<>();
private final ServiceConnectionHolder[] serviceClients;
private final ConnectionHolder[] cdnClients;
private final ConnectionHolder[] contactDiscoveryClients;
private final CredentialsProvider credentialsProvider;
private final String userAgent;
private final SecureRandom random;
public PushServiceSocket(SignalServiceConfiguration signalServiceConfiguration, CredentialsProvider credentialsProvider, String userAgent) {
this.credentialsProvider = credentialsProvider;
this.userAgent = userAgent;
this.serviceClients = createServiceConnectionHolders(signalServiceConfiguration.getSignalServiceUrls());
this.cdnClients = createConnectionHolders(signalServiceConfiguration.getSignalCdnUrls());
this.contactDiscoveryClients = createConnectionHolders(signalServiceConfiguration.getSignalContactDiscoveryUrls());
this.random = new SecureRandom();
}
public void requestSmsVerificationCode(boolean androidSmsRetriever) throws IOException {
makeServiceRequest(String.format(CREATE_ACCOUNT_SMS_PATH, credentialsProvider.getUser(), androidSmsRetriever ? "android-ng" : "android"), "GET", null, NO_HEADERS);
}
public void requestVoiceVerificationCode(Locale locale) throws IOException {
Map headers = locale != null ? Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry()) : NO_HEADERS;
makeServiceRequest(String.format(CREATE_ACCOUNT_VOICE_PATH, credentialsProvider.getUser()), "GET", null, headers);
}
public void verifyAccountCode(String verificationCode, String signalingKey, int registrationId, boolean fetchesMessages, String pin,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin,
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
makeServiceRequest(String.format(VERIFY_ACCOUNT_CODE_PATH, verificationCode),
"PUT", JsonUtil.toJson(signalingKeyEntity));
}
public void setAccountAttributes(String signalingKey, int registrationId, boolean fetchesMessages, String pin,
byte[] unidentifiedAccessKey, boolean unrestrictedUnidentifiedAccess)
throws IOException
{
AccountAttributes accountAttributes = new AccountAttributes(signalingKey, registrationId, fetchesMessages, pin,
unidentifiedAccessKey, unrestrictedUnidentifiedAccess);
makeServiceRequest(SET_ACCOUNT_ATTRIBUTES, "PUT", JsonUtil.toJson(accountAttributes));
}
public String getNewDeviceVerificationCode() throws IOException {
String responseText = makeServiceRequest(PROVISIONING_CODE_PATH, "GET", null);
return JsonUtil.fromJson(responseText, DeviceCode.class).getVerificationCode();
}
public List getDevices() throws IOException {
String responseText = makeServiceRequest(String.format(DEVICE_PATH, ""), "GET", null);
return JsonUtil.fromJson(responseText, DeviceInfoList.class).getDevices();
}
public void removeDevice(long deviceId) throws IOException {
makeServiceRequest(String.format(DEVICE_PATH, String.valueOf(deviceId)), "DELETE", null);
}
public void sendProvisioningMessage(String destination, byte[] body) throws IOException {
makeServiceRequest(String.format(PROVISIONING_MESSAGE_PATH, destination), "PUT",
JsonUtil.toJson(new ProvisioningMessage(Base64.encodeBytes(body))));
}
public void registerGcmId(String gcmRegistrationId) throws IOException {
GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId, true);
makeServiceRequest(REGISTER_GCM_PATH, "PUT", JsonUtil.toJson(registration));
}
public void unregisterGcmId() throws IOException {
makeServiceRequest(REGISTER_GCM_PATH, "DELETE", null);
}
public void setPin(String pin) throws IOException {
RegistrationLock accountLock = new RegistrationLock(pin);
makeServiceRequest(PIN_PATH, "PUT", JsonUtil.toJson(accountLock));
}
public void removePin() throws IOException {
makeServiceRequest(PIN_PATH, "DELETE", null);
}
public byte[] getSenderCertificate() throws IOException {
String responseText = makeServiceRequest(SENDER_CERTIFICATE_PATH, "GET", null);
return JsonUtil.fromJson(responseText, SenderCertificate.class).getCertificate();
}
public SendMessageResponse sendMessage(OutgoingPushMessageList bundle, Optional unidentifiedAccess)
throws IOException
{
try {
String responseText = makeServiceRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", JsonUtil.toJson(bundle), NO_HEADERS, unidentifiedAccess);
if (responseText == null) return new SendMessageResponse(false);
else return JsonUtil.fromJson(responseText, SendMessageResponse.class);
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(bundle.getDestination(), nfe);
}
}
public List getMessages() throws IOException {
String responseText = makeServiceRequest(String.format(MESSAGE_PATH, ""), "GET", null);
return JsonUtil.fromJson(responseText, SignalServiceEnvelopeEntityList.class).getMessages();
}
public void acknowledgeMessage(String sender, long timestamp) throws IOException {
makeServiceRequest(String.format(SENDER_ACK_MESSAGE_PATH, sender, timestamp), "DELETE", null);
}
public void acknowledgeMessage(String uuid) throws IOException {
makeServiceRequest(String.format(UUID_ACK_MESSAGE_PATH, uuid), "DELETE", null);
}
public void registerPreKeys(IdentityKey identityKey,
SignedPreKeyRecord signedPreKey,
List records)
throws IOException
{
List entities = new LinkedList<>();
for (PreKeyRecord record : records) {
PreKeyEntity entity = new PreKeyEntity(record.getId(),
record.getKeyPair().getPublicKey());
entities.add(entity);
}
SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(),
signedPreKey.getKeyPair().getPublicKey(),
signedPreKey.getSignature());
makeServiceRequest(String.format(PREKEY_PATH, ""), "PUT",
JsonUtil.toJson(new PreKeyState(entities, signedPreKeyEntity, identityKey)));
}
public int getAvailablePreKeys() throws IOException {
String responseText = makeServiceRequest(PREKEY_METADATA_PATH, "GET", null);
PreKeyStatus preKeyStatus = JsonUtil.fromJson(responseText, PreKeyStatus.class);
return preKeyStatus.getCount();
}
public List getPreKeys(SignalServiceAddress destination,
Optional unidentifiedAccess,
int deviceIdInteger)
throws IOException
{
try {
String deviceId = String.valueOf(deviceIdInteger);
if (deviceId.equals("1"))
deviceId = "*";
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(), deviceId);
if (destination.getRelay().isPresent()) {
path = path + "?relay=" + destination.getRelay().get();
}
String responseText = makeServiceRequest(path, "GET", null, NO_HEADERS, unidentifiedAccess);
PreKeyResponse response = JsonUtil.fromJson(responseText, PreKeyResponse.class);
List bundles = new LinkedList<>();
for (PreKeyResponseItem device : response.getDevices()) {
ECPublicKey preKey = null;
ECPublicKey signedPreKey = null;
byte[] signedPreKeySignature = null;
int preKeyId = -1;
int signedPreKeyId = -1;
if (device.getSignedPreKey() != null) {
signedPreKey = device.getSignedPreKey().getPublicKey();
signedPreKeyId = device.getSignedPreKey().getKeyId();
signedPreKeySignature = device.getSignedPreKey().getSignature();
}
if (device.getPreKey() != null) {
preKeyId = device.getPreKey().getKeyId();
preKey = device.getPreKey().getPublicKey();
}
bundles.add(new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId,
preKey, signedPreKeyId, signedPreKey, signedPreKeySignature,
response.getIdentityKey()));
}
return bundles;
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(destination.getNumber(), nfe);
}
}
public PreKeyBundle getPreKey(SignalServiceAddress destination, int deviceId) throws IOException {
try {
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(),
String.valueOf(deviceId));
if (destination.getRelay().isPresent()) {
path = path + "?relay=" + destination.getRelay().get();
}
String responseText = makeServiceRequest(path, "GET", null);
PreKeyResponse response = JsonUtil.fromJson(responseText, PreKeyResponse.class);
if (response.getDevices() == null || response.getDevices().size() < 1)
throw new IOException("Empty prekey list");
PreKeyResponseItem device = response.getDevices().get(0);
ECPublicKey preKey = null;
ECPublicKey signedPreKey = null;
byte[] signedPreKeySignature = null;
int preKeyId = -1;
int signedPreKeyId = -1;
if (device.getPreKey() != null) {
preKeyId = device.getPreKey().getKeyId();
preKey = device.getPreKey().getPublicKey();
}
if (device.getSignedPreKey() != null) {
signedPreKeyId = device.getSignedPreKey().getKeyId();
signedPreKey = device.getSignedPreKey().getPublicKey();
signedPreKeySignature = device.getSignedPreKey().getSignature();
}
return new PreKeyBundle(device.getRegistrationId(), device.getDeviceId(), preKeyId, preKey,
signedPreKeyId, signedPreKey, signedPreKeySignature, response.getIdentityKey());
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(destination.getNumber(), nfe);
}
}
public SignedPreKeyEntity getCurrentSignedPreKey() throws IOException {
try {
String responseText = makeServiceRequest(SIGNED_PREKEY_PATH, "GET", null);
return JsonUtil.fromJson(responseText, SignedPreKeyEntity.class);
} catch (NotFoundException e) {
Log.w(TAG, e);
return null;
}
}
public void setCurrentSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException {
SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(),
signedPreKey.getKeyPair().getPublicKey(),
signedPreKey.getSignature());
makeServiceRequest(SIGNED_PREKEY_PATH, "PUT", JsonUtil.toJson(signedPreKeyEntity));
}
public Pair sendAttachment(PushAttachmentData attachment) throws IOException {
String response = makeServiceRequest(String.format(ATTACHMENT_PATH, ""), "GET", null);
AttachmentDescriptor attachmentKey = JsonUtil.fromJson(response, AttachmentDescriptor.class);
if (attachmentKey == null || attachmentKey.getLocation() == null) {
throw new IOException("Server failed to allocate an attachment key!");
}
Log.w(TAG, "Got attachment content location: " + attachmentKey.getLocation());
byte[] digest = uploadAttachment("PUT", attachmentKey.getLocation(), attachment.getData(),
attachment.getDataSize(), attachment.getOutputStreamFactory(), attachment.getListener());
return new Pair<>(attachmentKey.getId(), digest);
}
public void retrieveAttachment(long attachmentId, File destination, int maxSizeBytes, ProgressListener listener) throws IOException {
String path = String.format(ATTACHMENT_PATH, String.valueOf(attachmentId));
String response = makeServiceRequest(path, "GET", null);
AttachmentDescriptor descriptor = JsonUtil.fromJson(response, AttachmentDescriptor.class);
Log.w(TAG, "Attachment: " + attachmentId + " is at: " + descriptor.getLocation());
downloadAttachment(descriptor.getLocation(), destination, maxSizeBytes, listener);
}
public SignalServiceProfile retrieveProfile(SignalServiceAddress target, Optional unidentifiedAccess)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
try {
String response = makeServiceRequest(String.format(PROFILE_PATH, target.getNumber()), "GET", null, NO_HEADERS, unidentifiedAccess);
return JsonUtil.fromJson(response, SignalServiceProfile.class);
} catch (IOException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
}
}
public void retrieveProfileAvatar(String path, File destination, int maxSizeBytes)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
downloadFromCdn(destination, path, maxSizeBytes);
}
public void setProfileName(String name) throws NonSuccessfulResponseCodeException, PushNetworkException {
makeServiceRequest(String.format(PROFILE_PATH, "name/" + (name == null ? "" : URLEncoder.encode(name))), "PUT", "");
}
public void setProfileAvatar(ProfileAvatarData profileAvatar)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
String response = makeServiceRequest(String.format(PROFILE_PATH, "form/avatar"), "GET", null);
ProfileAvatarUploadAttributes formAttributes;
try {
formAttributes = JsonUtil.fromJson(response, ProfileAvatarUploadAttributes.class);
} catch (IOException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
}
if (profileAvatar != null) {
uploadToCdn(formAttributes.getAcl(), formAttributes.getKey(),
formAttributes.getPolicy(), formAttributes.getAlgorithm(),
formAttributes.getCredential(), formAttributes.getDate(),
formAttributes.getSignature(), profileAvatar.getData(),
profileAvatar.getContentType(), profileAvatar.getDataLength(),
profileAvatar.getOutputStreamFactory());
}
}
public List retrieveDirectory(Set contactTokens)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
try {
ContactTokenList contactTokenList = new ContactTokenList(new LinkedList<>(contactTokens));
String response = makeServiceRequest(DIRECTORY_TOKENS_PATH, "PUT", JsonUtil.toJson(contactTokenList));
ContactTokenDetailsList activeTokens = JsonUtil.fromJson(response, ContactTokenDetailsList.class);
return activeTokens.getContacts();
} catch (IOException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
}
}
public ContactTokenDetails getContactTokenDetails(String contactToken) throws IOException {
try {
String response = makeServiceRequest(String.format(DIRECTORY_VERIFY_PATH, contactToken), "GET", null);
return JsonUtil.fromJson(response, ContactTokenDetails.class);
} catch (NotFoundException nfe) {
return null;
}
}
public String getContactDiscoveryAuthorization() throws IOException {
String response = makeServiceRequest(DIRECTORY_AUTH_PATH, "GET", null);
ContactDiscoveryCredentials token = JsonUtil.fromJson(response, ContactDiscoveryCredentials.class);
return Credentials.basic(token.getUsername(), token.getPassword());
}
public Pair> getContactDiscoveryRemoteAttestation(String authorization, RemoteAttestationRequest request, String mrenclave)
throws IOException
{
Response response = makeContactDiscoveryRequest(authorization, new LinkedList(), "/v1/attestation/" + mrenclave, "PUT", JsonUtil.toJson(request));
ResponseBody body = response.body();
List rawCookies = response.headers("Set-Cookie");
List cookies = new LinkedList<>();
for (String cookie : rawCookies) {
cookies.add(cookie.split(";")[0]);
}
if (body != null) {
return new Pair<>(JsonUtil.fromJson(body.string(), RemoteAttestationResponse.class), cookies);
} else {
throw new NonSuccessfulResponseCodeException("Empty response!");
}
}
public DiscoveryResponse getContactDiscoveryRegisteredUsers(String authorizationToken, DiscoveryRequest request, List cookies, String mrenclave)
throws IOException
{
ResponseBody body = makeContactDiscoveryRequest(authorizationToken, cookies, "/v1/discovery/" + mrenclave, "PUT", JsonUtil.toJson(request)).body();
if (body != null) {
return JsonUtil.fromJson(body.string(), DiscoveryResponse.class);
} else {
throw new NonSuccessfulResponseCodeException("Empty response!");
}
}
public void reportContactDiscoveryServiceMatch() throws IOException {
makeServiceRequest("/v1/directory/feedback/ok", "PUT", "");
}
public void reportContactDiscoveryServiceMismatch() throws IOException {
makeServiceRequest("/v1/directory/feedback/mismatch", "PUT", "");
}
public void reportContactDiscoveryServiceServerError() throws IOException {
makeServiceRequest("/v1/directory/feedback/server-error", "PUT", "");
}
public void reportContactDiscoveryServiceClientError() throws IOException {
makeServiceRequest("/v1/directory/feedback/client-error", "PUT", "");
}
public void reportContactDiscoveryServiceAttestationError() throws IOException {
makeServiceRequest("/v1/directory/feedback/attestation-error", "PUT", "");
}
public void reportContactDiscoveryServiceUnexpectedError() throws IOException {
makeServiceRequest("/v1/directory/feedback/unexpected-error", "PUT", "");
}
public TurnServerInfo getTurnServerInfo() throws IOException {
String response = makeServiceRequest(TURN_SERVER_INFO, "GET", null);
return JsonUtil.fromJson(response, TurnServerInfo.class);
}
public void setSoTimeoutMillis(long soTimeoutMillis) {
this.soTimeoutMillis = soTimeoutMillis;
}
public void cancelInFlightRequests() {
synchronized (connections) {
Log.w(TAG, "Canceling: " + connections.size());
for (Call connection : connections) {
Log.w(TAG, "Canceling: " + connection);
connection.cancel();
}
}
}
private void downloadAttachment(String url, File localDestination, int maxSizeBytes, ProgressListener listener)
throws IOException
{
URL downloadUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) downloadUrl.openConnection();
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.setRequestMethod("GET");
connection.setDoInput(true);
try {
if (connection.getResponseCode() != 200) {
throw new NonSuccessfulResponseCodeException("Bad response: " + connection.getResponseCode());
}
OutputStream output = new FileOutputStream(localDestination);
InputStream input = connection.getInputStream();
byte[] buffer = new byte[32768];
int contentLength = connection.getContentLength();
int read,totalRead = 0;
if (contentLength > maxSizeBytes) {
throw new NonSuccessfulResponseCodeException("File exceeds maximum size.");
}
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
totalRead += read;
if (totalRead > maxSizeBytes) {
localDestination.delete();
throw new NonSuccessfulResponseCodeException("File exceeds maximum size.");
}
if (listener != null) {
listener.onAttachmentProgress(contentLength, totalRead);
}
}
output.close();
Log.w(TAG, "Downloaded: " + url + " to: " + localDestination.getAbsolutePath());
} catch (IOException ioe) {
throw new PushNetworkException(ioe);
} finally {
connection.disconnect();
}
}
private byte[] uploadAttachment(String method, String url, InputStream data,
long dataSize, OutputStreamFactory outputStreamFactory, ProgressListener listener)
throws IOException
{
URL uploadUrl = new URL(url);
HttpsURLConnection connection = (HttpsURLConnection) uploadUrl.openConnection();
connection.setDoOutput(true);
if (dataSize > 0) {
connection.setFixedLengthStreamingMode(Util.toIntExact(dataSize));
} else {
connection.setChunkedStreamingMode(0);
}
connection.setRequestMethod(method);
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.setRequestProperty("Connection", "close");
connection.connect();
try {
DigestingOutputStream out = outputStreamFactory.createFor(connection.getOutputStream());
byte[] buffer = new byte[32768];
int read, written = 0;
while ((read = data.read(buffer)) != -1) {
out.write(buffer, 0, read);
written += read;
if (listener != null) {
listener.onAttachmentProgress(dataSize, written);
}
}
out.flush();
data.close();
out.close();
if (connection.getResponseCode() != 200) {
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
}
return out.getTransmittedDigest();
} finally {
connection.disconnect();
}
}
private void downloadFromCdn(File destination, String path, int maxSizeBytes)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
ConnectionHolder connectionHolder = getRandom(cdnClients, random);
OkHttpClient okHttpClient = connectionHolder.getClient()
.newBuilder()
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + "/" + path).get();
if (connectionHolder.getHostHeader().isPresent()) {
request.addHeader("Host", connectionHolder.getHostHeader().get());
}
Call call = okHttpClient.newCall(request.build());
synchronized (connections) {
connections.add(call);
}
Response response;
try {
response = call.execute();
if (response.isSuccessful()) {
ResponseBody body = response.body();
if (body == null) throw new PushNetworkException("No response body!");
if (body.contentLength() > maxSizeBytes) throw new PushNetworkException("Response exceeds max size!");
InputStream in = body.byteStream();
OutputStream out = new FileOutputStream(destination);
byte[] buffer = new byte[32768];
int read, totalRead = 0;
while ((read = in.read(buffer, 0, buffer.length)) != -1) {
out.write(buffer, 0, read);
if ((totalRead += read) > maxSizeBytes) throw new PushNetworkException("Response exceeded max size!");
}
return;
}
} catch (IOException e) {
throw new PushNetworkException(e);
} finally {
synchronized (connections) {
connections.remove(call);
}
}
throw new NonSuccessfulResponseCodeException("Response: " + response);
}
private byte[] uploadToCdn(String acl, String key, String policy, String algorithm,
String credential, String date, String signature,
InputStream data, String contentType, long length,
OutputStreamFactory outputStreamFactory)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
ConnectionHolder connectionHolder = getRandom(cdnClients, random);
OkHttpClient okHttpClient = connectionHolder.getClient()
.newBuilder()
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
DigestingRequestBody file = new DigestingRequestBody(data, outputStreamFactory, contentType, length);
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("acl", acl)
.addFormDataPart("key", key)
.addFormDataPart("policy", policy)
.addFormDataPart("Content-Type", contentType)
.addFormDataPart("x-amz-algorithm", algorithm)
.addFormDataPart("x-amz-credential", credential)
.addFormDataPart("x-amz-date", date)
.addFormDataPart("x-amz-signature", signature)
.addFormDataPart("file", "file", file)
.build();
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl()).post(requestBody);
if (connectionHolder.getHostHeader().isPresent()) {
request.addHeader("Host", connectionHolder.getHostHeader().get());
}
Call call = okHttpClient.newCall(request.build());
synchronized (connections) {
connections.add(call);
}
try {
Response response;
try {
response = call.execute();
} catch (IOException e) {
throw new PushNetworkException(e);
}
if (response.isSuccessful()) return file.getTransmittedDigest();
else throw new NonSuccessfulResponseCodeException("Response: " + response);
} finally {
synchronized (connections) {
connections.remove(call);
}
}
}
private String makeServiceRequest(String urlFragment, String method, String body)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
return makeServiceRequest(urlFragment, method, body, NO_HEADERS, Optional.absent());
}
private String makeServiceRequest(String urlFragment, String method, String body, Map headers)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
return makeServiceRequest(urlFragment, method, body, headers, Optional.absent());
}
private String makeServiceRequest(String urlFragment, String method, String body, Map headers, Optional unidentifiedAccessKey)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
Response response = getServiceConnection(urlFragment, method, body, headers, unidentifiedAccessKey);
int responseCode;
String responseMessage;
String responseBody;
try {
responseCode = response.code();
responseMessage = response.message();
responseBody = response.body().string();
} catch (IOException ioe) {
throw new PushNetworkException(ioe);
}
switch (responseCode) {
case 413:
throw new RateLimitException("Rate limit exceeded: " + responseCode);
case 401:
case 403:
throw new AuthorizationFailedException("Authorization failed!");
case 404:
throw new NotFoundException("Not found");
case 409:
MismatchedDevices mismatchedDevices;
try {
mismatchedDevices = JsonUtil.fromJson(responseBody, MismatchedDevices.class);
} catch (JsonProcessingException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
} catch (IOException e) {
throw new PushNetworkException(e);
}
throw new MismatchedDevicesException(mismatchedDevices);
case 410:
StaleDevices staleDevices;
try {
staleDevices = JsonUtil.fromJson(responseBody, StaleDevices.class);
} catch (JsonProcessingException e) {
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
} catch (IOException e) {
throw new PushNetworkException(e);
}
throw new StaleDevicesException(staleDevices);
case 411:
DeviceLimit deviceLimit;
try {
deviceLimit = JsonUtil.fromJson(responseBody, DeviceLimit.class);
} catch (JsonProcessingException e) {
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
} catch (IOException e) {
throw new PushNetworkException(e);
}
throw new DeviceLimitExceededException(deviceLimit);
case 417:
throw new ExpectationFailedException();
case 423:
RegistrationLockFailure accountLockFailure;
try {
accountLockFailure = JsonUtil.fromJson(responseBody, RegistrationLockFailure.class);
} catch (JsonProcessingException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
} catch (IOException e) {
throw new PushNetworkException(e);
}
throw new LockedException(accountLockFailure.length, accountLockFailure.timeRemaining);
}
if (responseCode != 200 && responseCode != 204) {
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " +
responseMessage);
}
return responseBody;
}
private Response getServiceConnection(String urlFragment, String method, String body, Map headers, Optional unidentifiedAccess)
throws PushNetworkException
{
try {
ServiceConnectionHolder connectionHolder = (ServiceConnectionHolder) getRandom(serviceClients, random);
OkHttpClient baseClient = unidentifiedAccess.isPresent() ? connectionHolder.getUnidentifiedClient() : connectionHolder.getClient();
OkHttpClient okHttpClient = baseClient.newBuilder()
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
Log.w(TAG, "Push service URL: " + connectionHolder.getUrl());
Log.w(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), urlFragment));
Request.Builder request = new Request.Builder();
request.url(String.format("%s%s", connectionHolder.getUrl(), urlFragment));
if (body != null) {
request.method(method, RequestBody.create(MediaType.parse("application/json"), body));
} else {
request.method(method, null);
}
for (Map.Entry header : headers.entrySet()) {
request.addHeader(header.getKey(), header.getValue());
}
if (unidentifiedAccess.isPresent()) {
request.addHeader("Unidentified-Access-Key", Base64.encodeBytes(unidentifiedAccess.get().getUnidentifiedAccessKey()));
} else if (credentialsProvider.getPassword() != null) {
request.addHeader("Authorization", getAuthorizationHeader(credentialsProvider));
}
if (userAgent != null) {
request.addHeader("X-Signal-Agent", userAgent);
}
if (connectionHolder.getHostHeader().isPresent()) {
request.addHeader("Host", connectionHolder.getHostHeader().get());
}
Call call = okHttpClient.newCall(request.build());
synchronized (connections) {
connections.add(call);
}
try {
return call.execute();
} finally {
synchronized (connections) {
connections.remove(call);
}
}
} catch (IOException e) {
throw new PushNetworkException(e);
}
}
private Response makeContactDiscoveryRequest(String authorization, List cookies, String path, String method, String body)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
ConnectionHolder connectionHolder = getRandom(contactDiscoveryClients, random);
OkHttpClient okHttpClient = connectionHolder.getClient()
.newBuilder()
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
if (body != null) {
request.method(method, RequestBody.create(MediaType.parse("application/json"), body));
} else {
request.method(method, null);
}
if (connectionHolder.getHostHeader().isPresent()) {
request.addHeader("Host", connectionHolder.getHostHeader().get());
}
if (authorization != null) {
request.addHeader("Authorization", authorization);
}
if (cookies != null && !cookies.isEmpty()) {
request.addHeader("Cookie", Util.join(cookies, "; "));
}
Call call = okHttpClient.newCall(request.build());
synchronized (connections) {
connections.add(call);
}
Response response;
try {
response = call.execute();
if (response.isSuccessful()) {
return response;
}
} catch (IOException e) {
throw new PushNetworkException(e);
} finally {
synchronized (connections) {
connections.remove(call);
}
}
throw new NonSuccessfulResponseCodeException("Response: " + response);
}
private ServiceConnectionHolder[] createServiceConnectionHolders(SignalUrl[] urls) {
List serviceConnectionHolders = new LinkedList<>();
for (SignalUrl url : urls) {
serviceConnectionHolders.add(new ServiceConnectionHolder(createConnectionClient(url),
createConnectionClient(url),
url.getUrl(), url.getHostHeader()));
}
return serviceConnectionHolders.toArray(new ServiceConnectionHolder[0]);
}
private ConnectionHolder[] createConnectionHolders(SignalUrl[] urls) {
List connectionHolders = new LinkedList<>();
for (SignalUrl url : urls) {
connectionHolders.add(new ConnectionHolder(createConnectionClient(url), url.getUrl(), url.getHostHeader()));
}
return connectionHolders.toArray(new ConnectionHolder[0]);
}
private OkHttpClient createConnectionClient(SignalUrl url) {
try {
TrustManager[] trustManagers = BlacklistingTrustManager.createFor(url.getTrustStore());
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trustManagers, null);
return new OkHttpClient.Builder()
.sslSocketFactory(context.getSocketFactory(), (X509TrustManager)trustManagers[0])
.connectionSpecs(url.getConnectionSpecs().or(Util.immutableList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS)))
.build();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new AssertionError(e);
}
}
private String getAuthorizationHeader(CredentialsProvider credentialsProvider) {
try {
return "Basic " + Base64.encodeBytes((credentialsProvider.getUser() + ":" + credentialsProvider.getPassword()).getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
private ConnectionHolder getRandom(ConnectionHolder[] connections, SecureRandom random) {
return connections[random.nextInt(connections.length)];
}
private static class GcmRegistrationId {
@JsonProperty
private String gcmRegistrationId;
@JsonProperty
private boolean webSocketChannel;
public GcmRegistrationId() {}
public GcmRegistrationId(String gcmRegistrationId, boolean webSocketChannel) {
this.gcmRegistrationId = gcmRegistrationId;
this.webSocketChannel = webSocketChannel;
}
}
private static class RegistrationLock {
@JsonProperty
private String pin;
public RegistrationLock() {}
public RegistrationLock(String pin) {
this.pin = pin;
}
}
private static class RegistrationLockFailure {
@JsonProperty
private int length;
@JsonProperty
private long timeRemaining;
}
private static class AttachmentDescriptor {
@JsonProperty
private long id;
@JsonProperty
private String location;
public long getId() {
return id;
}
public String getLocation() {
return location;
}
}
private static class ConnectionHolder {
private final OkHttpClient client;
private final String url;
private final Optional hostHeader;
private ConnectionHolder(OkHttpClient client, String url, Optional hostHeader) {
this.client = client;
this.url = url;
this.hostHeader = hostHeader;
}
OkHttpClient getClient() {
return client;
}
public String getUrl() {
return url;
}
Optional getHostHeader() {
return hostHeader;
}
}
private static class ServiceConnectionHolder extends ConnectionHolder {
private final OkHttpClient unidentifiedClient;
private ServiceConnectionHolder(OkHttpClient identifiedClient, OkHttpClient unidentifiedClient, String url, Optional hostHeader) {
super(identifiedClient, url, hostHeader);
this.unidentifiedClient = unidentifiedClient;
}
OkHttpClient getUnidentifiedClient() {
return unidentifiedClient;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy