Java tutorial
/** * 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.libaxolotl.ecc.ECPublicKey; import org.whispersystems.libaxolotl.state.PreKeyBundle; import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import org.whispersystems.textsecure.push.exceptions.AuthorizationFailedException; import org.whispersystems.textsecure.push.exceptions.ExpectationFailedException; import org.whispersystems.textsecure.push.exceptions.MismatchedDevicesException; import org.whispersystems.textsecure.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.textsecure.push.exceptions.NotFoundException; import org.whispersystems.textsecure.push.exceptions.PushNetworkException; import org.whispersystems.textsecure.push.exceptions.RateLimitException; import org.whispersystems.textsecure.push.exceptions.StaleDevicesException; 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 = "/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 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 RECEIPT_PATH = "/v1/receipt/%s/%d"; 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 sendReceipt(String destination, long messageId, String relay) throws IOException { String path = String.format(RECEIPT_PATH, destination, messageId); if (!Util.isEmpty(relay)) { path += "?relay=" + relay; } makeRequest(path, "PUT", null); } 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, SignedPreKeyRecord signedPreKey, List<PreKeyRecord> records) throws IOException { List<PreKeyEntity> entities = new LinkedList<>(); for (PreKeyRecord record : records) { PreKeyEntity entity = new PreKeyEntity(record.getId(), record.getKeyPair().getPublicKey()); entities.add(entity); } PreKeyEntity lastResortEntity = new PreKeyEntity(lastResortKey.getId(), lastResortKey.getKeyPair().getPublicKey()); SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(), signedPreKey.getKeyPair().getPublicKey(), signedPreKey.getSignature()); makeRequest(String.format(PREKEY_PATH, ""), "PUT", PreKeyState.toJson(new PreKeyState(entities, lastResortEntity, signedPreKeyEntity, identityKey))); } 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<PreKeyBundle> 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); PreKeyResponse response = PreKeyResponse.fromJson(responseText); List<PreKeyBundle> 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 (JsonParseException e) { throw new IOException(e); } catch (NotFoundException nfe) { throw new UnregisteredUserException(destination.getNumber(), nfe); } } public PreKeyBundle 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); PreKeyResponse response = PreKeyResponse.fromJson(responseText); 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 (JsonParseException e) { throw new IOException(e); } catch (NotFoundException nfe) { throw new UnregisteredUserException(destination.getNumber(), nfe); } } public SignedPreKeyEntity getCurrentSignedPreKey() throws IOException { try { String responseText = makeRequest(SIGNED_PREKEY_PATH, "GET", null); return SignedPreKeyEntity.fromJson(responseText); } catch (NotFoundException e) { Log.w("PushServiceSocket", e); return null; } } public void setCurrentSignedPreKey(SignedPreKeyRecord signedPreKey) throws IOException { SignedPreKeyEntity signedPreKeyEntity = new SignedPreKeyEntity(signedPreKey.getId(), signedPreKey.getKeyPair().getPublicKey(), signedPreKey.getSignature()); makeRequest(SIGNED_PREKEY_PATH, "PUT", SignedPreKeyEntity.toJson(signedPreKeyEntity)); } 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 NonSuccessfulResponseCodeException, PushNetworkException { HttpURLConnection connection = makeBaseRequest(urlFragment, method, body); try { String response = Util.readFully(connection.getInputStream()); connection.disconnect(); return response; } catch (IOException ioe) { throw new PushNetworkException(ioe); } } private HttpURLConnection makeBaseRequest(String urlFragment, String method, String body) throws NonSuccessfulResponseCodeException, PushNetworkException { HttpURLConnection connection = getConnection(urlFragment, method, body); int responseCode; String responseMessage; String response; try { responseCode = connection.getResponseCode(); responseMessage = connection.getResponseMessage(); } catch (IOException ioe) { throw new PushNetworkException(ioe); } switch (responseCode) { case 413: connection.disconnect(); throw new RateLimitException("Rate limit exceeded: " + responseCode); case 401: case 403: connection.disconnect(); throw new AuthorizationFailedException("Authorization failed!"); case 404: connection.disconnect(); throw new NotFoundException("Not found"); case 409: try { response = Util.readFully(connection.getErrorStream()); } catch (IOException e) { throw new PushNetworkException(e); } throw new MismatchedDevicesException(new Gson().fromJson(response, MismatchedDevices.class)); case 410: try { response = Util.readFully(connection.getErrorStream()); } catch (IOException e) { throw new PushNetworkException(e); } throw new StaleDevicesException(new Gson().fromJson(response, StaleDevices.class)); case 417: throw new ExpectationFailedException(); } if (responseCode != 200 && responseCode != 204) { throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); } return connection; } private HttpURLConnection getConnection(String urlFragment, String method, String body) throws PushNetworkException { 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()); } 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(); } return connection; } catch (IOException e) { throw new PushNetworkException(e); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } catch (KeyManagementException 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(); } }