/* * Copyright (C) 2011 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 . */ package org.thoughtcrime.redphone.signaling; import android.content.Context; import android.util.Log; import com.fasterxml.jackson.databind.ObjectMapper; import org.thoughtcrime.redphone.network.LowLatencySocketConnector; import org.thoughtcrime.redphone.signaling.signals.BusySignal; import org.thoughtcrime.redphone.signaling.signals.HangupSignal; import org.thoughtcrime.redphone.signaling.signals.InitiateSignal; import org.thoughtcrime.redphone.signaling.signals.RingingSignal; import org.thoughtcrime.redphone.signaling.signals.ServerSignal; import org.thoughtcrime.redphone.signaling.signals.Signal; import org.thoughtcrime.redphone.util.LineReader; import org.thoughtcrime.securesms.util.JsonUtils; import org.whispersystems.textsecure.api.push.TrustStore; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; import java.net.SocketException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.util.Map; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; /** * A socket that speaks the signaling protocol with a whisperswitch. * * The signaling protocol is very similar to a RESTful HTTP API, where every * request yields a corresponding response, and authorization is done through * an Authorization header. * * Like SIP, however, both endpoints are simultaneously server and client, issuing * requests and responses to each-other. * * Connections are persistent, and the signaling connection * for any ongoing call must remain open, otherwise the call will drop. * * @author Moxie Marlinspike * */ public class SignalingSocket { private static final String TAG = SignalingSocket.class.getSimpleName(); protected static final int PROTOCOL_VERSION = 1; private final Context context; private final Socket socket; protected final LineReader lineReader; protected final OutputStream outputStream; protected final String localNumber; protected final String password; protected final OtpCounterProvider counterProvider; private boolean connectionAttemptComplete; public SignalingSocket(Context context, String host, int port, String localNumber, String password, OtpCounterProvider counterProvider) throws SignalingException { try { this.context = context.getApplicationContext(); this.connectionAttemptComplete = false; this.socket = constructSSLSocket(context, host, port); this.outputStream = this.socket.getOutputStream(); this.lineReader = new LineReader(socket.getInputStream()); this.localNumber = localNumber; this.password = password; this.counterProvider = counterProvider; } catch (IOException ioe) { throw new SignalingException(ioe); } } private Socket constructSSLSocket(Context context, String host, int port) throws SignalingException { try { TrustManager[] trustManagers = getTrustManager(new RedPhoneTrustStore(context)); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustManagers, null); return timeoutHackConnect(sslContext.getSocketFactory(), host, port); } catch (IOException ioe) { throw new SignalingException(ioe); } catch (NoSuchAlgorithmException | KeyManagementException e) { throw new IllegalArgumentException(e); } } private Socket timeoutHackConnect(SSLSocketFactory sslSocketFactory, String host, int port) throws IOException { InetAddress[] addresses = InetAddress.getAllByName(host); Socket stagedSocket = LowLatencySocketConnector.connect(addresses, port); Log.w(TAG, "Connected to: " + stagedSocket.getInetAddress().getHostAddress()); SocketConnectMonitor monitor = new SocketConnectMonitor(stagedSocket); monitor.start(); Socket result = sslSocketFactory.createSocket(stagedSocket, host, port, true); synchronized (this) { this.connectionAttemptComplete = true; notify(); if (result.isConnected()) return result; else throw new IOException("Socket timed out before " + "connection completed."); } } public TrustManager[] getTrustManager(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 trustManagerFactory.getTrustManagers(); } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) { throw new AssertionError(e); } } public void close() { try { this.outputStream.close(); this.socket.getInputStream().close(); this.socket.close(); } catch (IOException ioe) {} } public SessionDescriptor initiateConnection(String remoteNumber) throws ServerMessageException, SignalingException, NoSuchUserException, LoginFailedException { sendSignal(new InitiateSignal(localNumber, password, counterProvider.getOtpCounter(context), remoteNumber)); SignalResponse response = readSignalResponse(); try { switch (response.getStatusCode()) { case 404: throw new NoSuchUserException("No such redphone user."); case 402: throw new ServerMessageException(new String(response.getBody())); case 401: throw new LoginFailedException("Initiate threw 401"); case 200: return JsonUtils.fromJson(response.getBody(), SessionDescriptor.class); default: throw new SignalingException("Unknown response: " + response.getStatusCode()); } } catch (IOException e) { throw new SignalingException(e); } } public void setRinging(long sessionId) throws SignalingException, SessionStaleException, LoginFailedException { sendSignal(new RingingSignal(localNumber, password, counterProvider.getOtpCounter(context), sessionId)); SignalResponse response = readSignalResponse(); switch (response.getStatusCode()) { case 404: throw new SessionStaleException("No such session: " + sessionId); case 401: throw new LoginFailedException("Ringing threw 401"); case 200: return; default: throw new SignalingException("Unknown response: " + response.getStatusCode()); } } public void setHangup(long sessionId) { try { sendSignal(new HangupSignal(localNumber, password, counterProvider.getOtpCounter(context), sessionId)); readSignalResponse(); } catch (SignalingException se) {} } public void setBusy(long sessionId) throws SignalingException { sendSignal(new BusySignal(localNumber, password, counterProvider.getOtpCounter(context), sessionId)); readSignalResponse(); } public void sendOkResponse() throws SignalingException { try { this.outputStream.write("HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n".getBytes()); } catch (IOException ioe) { throw new SignalingException(ioe); } } public boolean waitForSignal() throws SignalingException { try { socket.setSoTimeout(1500); return lineReader.waitForAvailable(); } catch (IOException ioe) { throw new SignalingException(ioe); } finally { try { socket.setSoTimeout(0); } catch (SocketException e) { Log.w("SignalingSocket", e); } } } public ServerSignal readSignal() throws SignalingException { try { SignalReader signalReader = new SignalReader(lineReader); String[] request = signalReader.readSignalRequest(); Map headers = signalReader.readSignalHeaders(); byte[] body = signalReader.readSignalBody(headers); return new ServerSignal(request[0].trim(), request[1].trim(), body); } catch (IOException ioe) { throw new SignalingException(ioe); } } protected void sendSignal(Signal signal) throws SignalingException { try { Log.d(TAG, "Sending signal..."); this.outputStream.write(signal.serialize().getBytes()); } catch (IOException ioe) { throw new SignalingException(ioe); } } protected SignalResponse readSignalResponse() throws SignalingException { try { SignalResponseReader responseReader = new SignalResponseReader(lineReader); int responseCode = responseReader.readSignalResponseCode(); Map headers = responseReader.readSignalHeaders(); byte[] body = responseReader.readSignalBody(headers); return new SignalResponse(responseCode, headers, body); } catch (IOException ioe) { throw new SignalingException(ioe); } } private class SocketConnectMonitor extends Thread { private final Socket socket; public SocketConnectMonitor(Socket socket) { this.socket = socket; } @Override public void run() { synchronized (SignalingSocket.this) { try { if (!SignalingSocket.this.connectionAttemptComplete) SignalingSocket.this.wait(10000); if (!SignalingSocket.this.connectionAttemptComplete) this.socket.close(); } catch (IOException ioe) { Log.w(TAG, ioe); } catch (InterruptedException e) { throw new AssertionError(e); } } } } }