mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-29 04:55:15 +00:00
d83a3d71bc
Merge in RedPhone // FREEBIE
437 lines
16 KiB
Java
437 lines
16 KiB
Java
/*
|
|
* 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.content.res.AssetManager;
|
|
import android.preference.PreferenceManager;
|
|
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.C2DMRegistrationSignal;
|
|
import org.thoughtcrime.redphone.signaling.signals.C2DMUnregistrationSignal;
|
|
import org.thoughtcrime.redphone.signaling.signals.DirectoryRequestSignal;
|
|
import org.thoughtcrime.redphone.signaling.signals.GCMRegistrationSignal;
|
|
import org.thoughtcrime.redphone.signaling.signals.GCMUnregistrationSignal;
|
|
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.signaling.signals.SignalPreferenceSignal;
|
|
import org.thoughtcrime.redphone.util.LineReader;
|
|
import org.whispersystems.textsecure.api.push.TrustStore;
|
|
import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
|
|
|
|
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.UnrecoverableKeyException;
|
|
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 {
|
|
protected static final int PROTOCOL_VERSION = 1;
|
|
|
|
private final Context context;
|
|
private final Socket socket;
|
|
private final String signalingHost;
|
|
private final int signalingPort;
|
|
|
|
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) throws SignalingException {
|
|
// this(context,
|
|
// BuildConfig.RE.MASTER_SERVER_HOST,
|
|
// Release.SERVER_PORT,
|
|
// PreferenceManager.getDefaultSharedPreferences(context).getString(Constants.NUMBER_PREFERENCE, "NO_SAVED_NUMBER!"),
|
|
// PreferenceManager.getDefaultSharedPreferences(context).getString(Constants.PASSWORD_PREFERENCE, "NO_SAVED_PASSWORD!"),
|
|
// null);
|
|
// }
|
|
|
|
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.signalingHost = host;
|
|
this.signalingPort = port;
|
|
this.socket = constructSSLSocket(context, signalingHost, signalingPort);
|
|
this.outputStream = this.socket.getOutputStream();
|
|
this.lineReader = new LineReader(socket.getInputStream());
|
|
// this.localNumber = PhoneNumberFormatter.formatNumber(localNumber);
|
|
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);
|
|
|
|
|
|
//
|
|
// AssetManager assetManager = context.getAssets();
|
|
// InputStream keyStoreInputStream = assetManager.open("whisper.store");
|
|
// KeyStore trustStore = KeyStore.getInstance("BKS");
|
|
//
|
|
// trustStore.load(keyStoreInputStream, "whisper".toCharArray());
|
|
//
|
|
// SSLSocketFactory sslSocketFactory = new SSLSocketFactory(trustStore);
|
|
// sslSocketFactory.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
|
|
// } else {
|
|
// Log.w("SignalingSocket", "Disabling hostname verification...");
|
|
// sslSocketFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
|
|
// }
|
|
|
|
// return timeoutHackConnect(sslSocketFactory, host, port);
|
|
return timeoutHackConnect(sslContext.getSocketFactory(), host, port);
|
|
} catch (IOException ioe) {
|
|
throw new SignalingException(ioe);
|
|
} catch (NoSuchAlgorithmException | KeyManagementException e) {
|
|
throw new IllegalArgumentException(e);
|
|
// } catch (KeyStoreException e) {
|
|
// throw new IllegalArgumentException(e);
|
|
// } catch (CertificateException e) {
|
|
// throw new IllegalArgumentException(e);
|
|
// } catch (KeyManagementException e) {
|
|
// throw new IllegalArgumentException(e);
|
|
// } catch (UnrecoverableKeyException 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("SignalingSocket", "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();
|
|
|
|
// Gson gson = new Gson();
|
|
|
|
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 new ObjectMapper().readValue(response.getBody(), SessionDescriptor.class);
|
|
// .gson.fromJson(new String(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 registerSignalingPreference(String preference) throws SignalingException {
|
|
sendSignal(new SignalPreferenceSignal(localNumber, password, preference));
|
|
SignalResponse response = readSignalResponse();
|
|
|
|
switch (response.getStatusCode()) {
|
|
case 200: return;
|
|
default: throw new SignalingException("Received error from server: " +
|
|
new String(response.getBody()));
|
|
}
|
|
}
|
|
|
|
public void registerGcm(String registrationId) throws SignalingException {
|
|
sendSignal(new GCMRegistrationSignal(localNumber, password, registrationId));
|
|
SignalResponse response = readSignalResponse();
|
|
|
|
switch (response.getStatusCode()) {
|
|
case 200: return;
|
|
default: throw new SignalingException("Received error from server: " +
|
|
new String(response.getBody()));
|
|
}
|
|
}
|
|
|
|
public void unregisterGcm(String registrationId) throws SignalingException {
|
|
sendSignal(new GCMUnregistrationSignal(localNumber, password, registrationId));
|
|
SignalResponse response = readSignalResponse();
|
|
|
|
switch (response.getStatusCode()) {
|
|
case 200: return;
|
|
default: throw new SignalingException("Received error from server: " +
|
|
new String(response.getBody()));
|
|
}
|
|
}
|
|
|
|
public void registerC2dm(String registrationId) throws SignalingException {
|
|
sendSignal(new C2DMRegistrationSignal(localNumber, password, registrationId));
|
|
SignalResponse response = readSignalResponse();
|
|
|
|
switch (response.getStatusCode()) {
|
|
case 200: return;
|
|
default: throw new SignalingException("Received error from server: " +
|
|
new String(response.getBody()));
|
|
}
|
|
}
|
|
|
|
public void unregisterC2dm() throws SignalingException {
|
|
sendSignal(new C2DMUnregistrationSignal(localNumber, password));
|
|
SignalResponse response = readSignalResponse();
|
|
|
|
switch (response.getStatusCode()) {
|
|
case 200: return;
|
|
default: throw new SignalingException("Received error from server: " +
|
|
new String(response.getBody()));
|
|
}
|
|
}
|
|
|
|
public DirectoryResponse getNumberFilter() throws SignalingException {
|
|
sendSignal(new DirectoryRequestSignal(localNumber, password));
|
|
SignalResponse response = readSignalResponse();
|
|
|
|
switch (response.getStatusCode()) {
|
|
case 200:
|
|
try {
|
|
if (!response.getHeaders().containsKey("X-Hash-Count"))
|
|
break;
|
|
|
|
int hashCount = Integer.parseInt(response.getHeaders().get("X-Hash-Count"));
|
|
|
|
Log.w("SignalingSocket", "Got directory response: " + hashCount +
|
|
" , " + response.getBody());
|
|
|
|
return new DirectoryResponse(hashCount, response.getBody());
|
|
} catch (NumberFormatException nfe) {
|
|
Log.w("SignalingSocket", nfe);
|
|
break;
|
|
}
|
|
default:
|
|
Log.w("SignalingSocket", "Unknown response from directory request: " +
|
|
response.getStatusCode());
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
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 {
|
|
Log.d("SignalingSocket", "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) {
|
|
Log.w("SignalingSocket", ioe);
|
|
} catch (InterruptedException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |