com.mindprotectionkit.freephone.signaling.SignalingSocket.java Source code

Java tutorial

Introduction

Here is the source code for com.mindprotectionkit.freephone.signaling.SignalingSocket.java

Source

/*
 * 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 com.mindprotectionkit.freephone.signaling;

import android.content.Context;
import android.content.res.AssetManager;
import android.preference.PreferenceManager;
import android.util.Log;

import com.google.thoughtcrimegson.Gson;

import org.apache.http.conn.ssl.SSLSocketFactory;
import com.mindprotectionkit.freephone.Constants;
import com.mindprotectionkit.freephone.Release;
import com.mindprotectionkit.freephone.network.LowLatencySocketConnector;
import com.mindprotectionkit.freephone.signaling.signals.BusySignal;
import com.mindprotectionkit.freephone.signaling.signals.C2DMRegistrationSignal;
import com.mindprotectionkit.freephone.signaling.signals.C2DMUnregistrationSignal;
import com.mindprotectionkit.freephone.signaling.signals.DirectoryRequestSignal;
import com.mindprotectionkit.freephone.signaling.signals.GCMRegistrationSignal;
import com.mindprotectionkit.freephone.signaling.signals.GCMUnregistrationSignal;
import com.mindprotectionkit.freephone.signaling.signals.HangupSignal;
import com.mindprotectionkit.freephone.signaling.signals.InitiateSignal;
import com.mindprotectionkit.freephone.signaling.signals.RingingSignal;
import com.mindprotectionkit.freephone.signaling.signals.ServerSignal;
import com.mindprotectionkit.freephone.signaling.signals.Signal;
import com.mindprotectionkit.freephone.signaling.signals.SignalPreferenceSignal;
import com.mindprotectionkit.freephone.util.LineReader;
import com.mindprotectionkit.freephone.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;

/**
 * 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, Release.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(context, 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 {
            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);

            if (Release.SSL) {
                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);
        } catch (IOException ioe) {
            throw new SignalingException(ioe);
        } catch (NoSuchAlgorithmException 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 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();

        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 gson.fromJson(new String(response.getBody()), SessionDescriptor.class);
        default:
            throw new SignalingException("Unknown response: " + response.getStatusCode());
        }
    }

    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(500);
            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);
                }
            }
        }
    }
}