Signal-Android/library/src/org/whispersystems/textsecure/push/PushServiceSocket.java

471 wiersze
16 KiB
Java

/**
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.whispersystems.textsecure.push;
import android.content.Context;
import android.util.Log;
import com.google.thoughtcrimegson.Gson;
import com.google.thoughtcrimegson.JsonParseException;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.textsecure.storage.PreKeyRecord;
import org.whispersystems.textsecure.util.Base64;
import org.whispersystems.textsecure.util.BlacklistingTrustManager;
import org.whispersystems.textsecure.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.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
/**
*
* Network interface to the TextSecure server API.
*
* @author Moxie Marlinspike
*/
public class PushServiceSocket {
private static final String CREATE_ACCOUNT_SMS_PATH = "/v1/accounts/sms/code/%s";
private static final String CREATE_ACCOUNT_VOICE_PATH = "/v1/accounts/voice/code/%s";
private static final String VERIFY_ACCOUNT_PATH = "/v1/accounts/code/%s";
private static final String REGISTER_GCM_PATH = "/v1/accounts/gcm/";
private static final String PREKEY_METADATA_PATH = "/v1/keys/";
private static final String PREKEY_PATH = "/v1/keys/%s";
private static final String PREKEY_DEVICE_PATH = "/v1/keys/%s/%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 MESSAGE_PATH = "/v1/messages/%s";
private static final String ATTACHMENT_PATH = "/v1/attachments/%s";
private static final boolean ENFORCE_SSL = true;
private final Context context;
private final String serviceUrl;
private final String localNumber;
private final String password;
private final TrustManager[] trustManagers;
public PushServiceSocket(Context context, String serviceUrl, TrustStore trustStore,
String localNumber, String password)
{
this.context = context.getApplicationContext();
this.serviceUrl = serviceUrl;
this.localNumber = localNumber;
this.password = password;
this.trustManagers = initializeTrustManager(trustStore);
}
public void createAccount(boolean voice) throws IOException {
String path = voice ? CREATE_ACCOUNT_VOICE_PATH : CREATE_ACCOUNT_SMS_PATH;
makeRequest(String.format(path, localNumber), "GET", null);
}
public void verifyAccount(String verificationCode, String signalingKey,
boolean supportsSms, int registrationId)
throws IOException
{
AccountAttributes signalingKeyEntity = new AccountAttributes(signalingKey, supportsSms, registrationId);
makeRequest(String.format(VERIFY_ACCOUNT_PATH, verificationCode),
"PUT", new Gson().toJson(signalingKeyEntity));
}
public void registerGcmId(String gcmRegistrationId) throws IOException {
GcmRegistrationId registration = new GcmRegistrationId(gcmRegistrationId);
makeRequest(REGISTER_GCM_PATH, "PUT", new Gson().toJson(registration));
}
public void unregisterGcmId() throws IOException {
makeRequest(REGISTER_GCM_PATH, "DELETE", null);
}
public void sendMessage(OutgoingPushMessageList bundle)
throws IOException
{
try {
makeRequest(String.format(MESSAGE_PATH, bundle.getDestination()), "PUT", new Gson().toJson(bundle));
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(bundle.getDestination(), nfe);
}
}
public void registerPreKeys(IdentityKey identityKey,
PreKeyRecord lastResortKey,
List<PreKeyRecord> records)
throws IOException
{
List<PreKeyEntity> entities = new LinkedList<PreKeyEntity>();
for (PreKeyRecord record : records) {
PreKeyEntity entity = new PreKeyEntity(record.getId(),
record.getKeyPair().getPublicKey(),
identityKey);
entities.add(entity);
}
PreKeyEntity lastResortEntity = new PreKeyEntity(lastResortKey.getId(),
lastResortKey.getKeyPair().getPublicKey(),
identityKey);
makeRequest(String.format(PREKEY_PATH, ""), "PUT",
PreKeyList.toJson(new PreKeyList(lastResortEntity, entities)));
}
public int getAvailablePreKeys() throws IOException {
String responseText = makeRequest(PREKEY_METADATA_PATH, "GET", null);
PreKeyStatus preKeyStatus = new Gson().fromJson(responseText, PreKeyStatus.class);
return preKeyStatus.getCount();
}
public List<PreKeyEntity> getPreKeys(PushAddress destination) throws IOException {
try {
String deviceId = String.valueOf(destination.getDeviceId());
if (deviceId.equals("1"))
deviceId = "*";
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(), deviceId);
if (!Util.isEmpty(destination.getRelay())) {
path = path + "?relay=" + destination.getRelay();
}
String responseText = makeRequest(path, "GET", null);
PreKeyList response = PreKeyList.fromJson(responseText);
return response.getKeys();
} catch (JsonParseException e) {
throw new IOException(e);
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(destination.getNumber(), nfe);
}
}
public PreKeyEntity getPreKey(PushAddress destination) throws IOException {
try {
String path = String.format(PREKEY_DEVICE_PATH, destination.getNumber(),
String.valueOf(destination.getDeviceId()));
if (!Util.isEmpty(destination.getRelay())) {
path = path + "?relay=" + destination.getRelay();
}
String responseText = makeRequest(path, "GET", null);
PreKeyList response = PreKeyList.fromJson(responseText);
if (response.getKeys() == null || response.getKeys().size() < 1)
throw new IOException("Empty prekey list");
return response.getKeys().get(0);
} catch (JsonParseException e) {
throw new IOException(e);
} catch (NotFoundException nfe) {
throw new UnregisteredUserException(destination.getNumber(), nfe);
}
}
public long sendAttachment(PushAttachmentData attachment) throws IOException {
String response = makeRequest(String.format(ATTACHMENT_PATH, ""), "GET", null);
AttachmentDescriptor attachmentKey = new Gson().fromJson(response, AttachmentDescriptor.class);
if (attachmentKey == null || attachmentKey.getLocation() == null) {
throw new IOException("Server failed to allocate an attachment key!");
}
Log.w("PushServiceSocket", "Got attachment content location: " + attachmentKey.getLocation());
uploadExternalFile("PUT", attachmentKey.getLocation(), attachment.getData());
return attachmentKey.getId();
}
public File retrieveAttachment(String relay, long attachmentId) throws IOException {
String path = String.format(ATTACHMENT_PATH, String.valueOf(attachmentId));
if (!Util.isEmpty(relay)) {
path = path + "?relay=" + relay;
}
String response = makeRequest(path, "GET", null);
AttachmentDescriptor descriptor = new Gson().fromJson(response, AttachmentDescriptor.class);
Log.w("PushServiceSocket", "Attachment: " + attachmentId + " is at: " + descriptor.getLocation());
File attachment = File.createTempFile("attachment", ".tmp", context.getFilesDir());
attachment.deleteOnExit();
downloadExternalFile(descriptor.getLocation(), attachment);
return attachment;
}
public List<ContactTokenDetails> retrieveDirectory(Set<String> contactTokens) {
try {
ContactTokenList contactTokenList = new ContactTokenList(new LinkedList<String>(contactTokens));
String response = makeRequest(DIRECTORY_TOKENS_PATH, "PUT", new Gson().toJson(contactTokenList));
ContactTokenDetailsList activeTokens = new Gson().fromJson(response, ContactTokenDetailsList.class);
return activeTokens.getContacts();
} catch (IOException ioe) {
Log.w("PushServiceSocket", ioe);
return null;
}
}
public ContactTokenDetails getContactTokenDetails(String contactToken) throws IOException {
try {
String response = makeRequest(String.format(DIRECTORY_VERIFY_PATH, contactToken), "GET", null);
return new Gson().fromJson(response, ContactTokenDetails.class);
} catch (NotFoundException nfe) {
return null;
}
}
private void downloadExternalFile(String url, File localDestination)
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 IOException("Bad response: " + connection.getResponseCode());
}
OutputStream output = new FileOutputStream(localDestination);
InputStream input = connection.getInputStream();
byte[] buffer = new byte[4096];
int read;
while ((read = input.read(buffer)) != -1) {
output.write(buffer, 0, read);
}
output.close();
Log.w("PushServiceSocket", "Downloaded: " + url + " to: " + localDestination.getAbsolutePath());
} finally {
connection.disconnect();
}
}
private void uploadExternalFile(String method, String url, byte[] data)
throws IOException
{
URL uploadUrl = new URL(url);
HttpsURLConnection connection = (HttpsURLConnection) uploadUrl.openConnection();
connection.setDoOutput(true);
connection.setRequestMethod(method);
connection.setRequestProperty("Content-Type", "application/octet-stream");
connection.connect();
try {
OutputStream out = connection.getOutputStream();
out.write(data);
out.close();
if (connection.getResponseCode() != 200) {
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
}
} finally {
connection.disconnect();
}
}
private String makeRequest(String urlFragment, String method, String body)
throws IOException
{
HttpURLConnection connection = makeBaseRequest(urlFragment, method, body);
String response = Util.readFully(connection.getInputStream());
connection.disconnect();
return response;
}
private HttpURLConnection makeBaseRequest(String urlFragment, String method, String body)
throws IOException
{
HttpURLConnection connection = getConnection(urlFragment, method);
if (body != null) {
connection.setDoOutput(true);
}
connection.connect();
if (body != null) {
Log.w("PushServiceSocket", method + " -- " + body);
OutputStream out = connection.getOutputStream();
out.write(body.getBytes());
out.close();
}
if (connection.getResponseCode() == 413) {
connection.disconnect();
throw new RateLimitException("Rate limit exceeded: " + connection.getResponseCode());
}
if (connection.getResponseCode() == 401 || connection.getResponseCode() == 403) {
connection.disconnect();
throw new AuthorizationFailedException("Authorization failed!");
}
if (connection.getResponseCode() == 404) {
connection.disconnect();
throw new NotFoundException("Not found");
}
if (connection.getResponseCode() == 409) {
String response = Util.readFully(connection.getErrorStream());
throw new MismatchedDevicesException(new Gson().fromJson(response, MismatchedDevices.class));
}
if (connection.getResponseCode() == 410) {
String response = Util.readFully(connection.getErrorStream());
throw new StaleDevicesException(new Gson().fromJson(response, StaleDevices.class));
}
if (connection.getResponseCode() == 417) {
throw new ExpectationFailedException();
}
if (connection.getResponseCode() != 200 && connection.getResponseCode() != 204) {
throw new IOException("Bad response: " + connection.getResponseCode() + " " + connection.getResponseMessage());
}
return connection;
}
private HttpURLConnection getConnection(String urlFragment, String method) throws IOException {
try {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trustManagers, null);
URL url = new URL(String.format("%s%s", serviceUrl, urlFragment));
Log.w("PushServiceSocket", "Push service URL: " + serviceUrl);
Log.w("PushServiceSocket", "Opening URL: " + url);
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
if (ENFORCE_SSL) {
((HttpsURLConnection)connection).setSSLSocketFactory(context.getSocketFactory());
((HttpsURLConnection)connection).setHostnameVerifier(new StrictHostnameVerifier());
}
connection.setRequestMethod(method);
connection.setRequestProperty("Content-Type", "application/json");
if (password != null) {
connection.setRequestProperty("Authorization", getAuthorizationHeader());
}
return connection;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (KeyManagementException e) {
throw new AssertionError(e);
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
}
private String getAuthorizationHeader() {
try {
return "Basic " + Base64.encodeBytes((localNumber + ":" + password).getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
private TrustManager[] initializeTrustManager(TrustStore trustStore) {
try {
InputStream keyStoreInputStream = trustStore.getKeyStoreInputStream();
KeyStore keyStore = KeyStore.getInstance("BKS");
keyStore.load(keyStoreInputStream, trustStore.getKeyStorePassword().toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("X509");
trustManagerFactory.init(keyStore);
return BlacklistingTrustManager.createFor(trustManagerFactory.getTrustManagers());
} catch (KeyStoreException kse) {
throw new AssertionError(kse);
} catch (CertificateException e) {
throw new AssertionError(e);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
} catch (IOException ioe) {
throw new AssertionError(ioe);
}
}
private static class GcmRegistrationId {
private String gcmRegistrationId;
public GcmRegistrationId() {}
public GcmRegistrationId(String gcmRegistrationId) {
this.gcmRegistrationId = gcmRegistrationId;
}
}
private static class AttachmentDescriptor {
private long id;
private String location;
public long getId() {
return id;
}
public String getLocation() {
return location;
}
}
public interface TrustStore {
public InputStream getKeyStoreInputStream();
public String getKeyStorePassword();
}
}