session-android/src/org/thoughtcrime/redphone/signaling/SignalingSocket.java

307 lines
11 KiB
Java
Raw Normal View History

/*
* 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 <http://www.gnu.org/licenses/>.
*/
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 {
2015-09-21 19:07:57 +00:00
private static final String TAG = SignalingSocket.class.getSimpleName();
protected static final int PROTOCOL_VERSION = 1;
private final Context context;
private final Socket socket;
2015-09-21 19:07:57 +00:00
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;
2015-09-21 19:07:57 +00:00
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);
2015-09-21 19:07:57 +00:00
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<String, String> 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 {
2015-09-21 19:07:57 +00:00
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<String, String> 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) {
2015-09-21 19:07:57 +00:00
Log.w(TAG, ioe);
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
}
}
}