org.mitre.svmp.apprtc.AppRTCClient.java Source code

Java tutorial

Introduction

Here is the source code for org.mitre.svmp.apprtc.AppRTCClient.java

Source

/*
 * Copyright (c) 2014 The MITRE Corporation, All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this work except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.mitre.svmp.apprtc;

import android.os.AsyncTask;
import android.os.Binder;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;

import com.google.protobuf.InvalidProtocolBufferException;

import de.tavendo.autobahn.WebSocket;
import de.tavendo.autobahn.WebSocketConnection;
import de.tavendo.autobahn.WebSocketException;
import de.tavendo.autobahn.WebSocketOptions;

import org.apache.http.*;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;
import org.json.JSONException;
import org.json.JSONObject;
import org.mitre.svmp.common.SessionInfo;
import org.mitre.svmp.net.SSLConfig;
import org.mitre.svmp.performance.PerformanceTimer;
import org.mitre.svmp.services.SessionService;
import org.mitre.svmp.activities.AppRTCActivity;
import org.mitre.svmp.auth.AuthData;
import com.citicrowd.oval.R;
import org.mitre.svmp.common.*;
import org.mitre.svmp.protocol.SVMPProtocol.*;
import org.mitre.svmp.common.StateMachine.STATE;

import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocket;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.URI;
import java.util.Date;
import java.util.HashMap;

/**
 * @author Joe Portner
 *
 * Negotiates signaling for chatting with apprtc.appspot.com "rooms".
 * Uses the client<->server specifics of the apprtc AppEngine webapp.
 *
 * Now extended to act as a Binder object between a Service and an Activity.
 *
 * To use: create an instance of this object (registering a message handler) and
 * call connectToRoom().  Once that's done call sendMessage() and wait for the
 * registered handler to be called with received messages.
 */
public class AppRTCClient extends Binder implements Constants {
    private static final String TAG = AppRTCClient.class.getName();

    // service and activity objects
    private StateMachine machine;
    private SessionService service = null;
    private AppRTCActivity activity = null;

    // common variables
    private ConnectionInfo connectionInfo;
    private SessionInfo sessionInfo;
    private DatabaseHandler dbHandler;
    private boolean init = false; // switched to 'true' when activity first binds
    private boolean proxying = false; // switched to 'true' upon state machine change

    // performance instrumentation
    private PerformanceTimer performance;

    // variables for networking
    private boolean useSSL;
    private SSLConfig sslConfig;
    private Socket socket;
    private SocketHandlerThread socketHandlerThread;
    private WebSocketConnection webSocket;

    // STEP 0: NEW -> STARTED
    public AppRTCClient(SessionService service, StateMachine machine, ConnectionInfo connectionInfo) {
        this.service = service;
        this.machine = machine;
        machine.addObserver(service);
        this.connectionInfo = connectionInfo;

        this.dbHandler = new DatabaseHandler(service);
        this.performance = new PerformanceTimer(service, this, connectionInfo.getConnectionID());

        machine.setState(STATE.STARTED, 0);
    }

    // called from activity
    public void connectToRoom(AppRTCActivity activity) {
        this.activity = activity;
        machine.addObserver(activity);

        // we don't initialize the SocketConnector until the activity first binds; mitigates concurrency issues
        if (!init) {
            init = true;

            int error = 0;
            // determine whether we should use SSL from the EncryptionType integer
            useSSL = connectionInfo.getEncryptionType() == Constants.ENCRYPTION_SSLTLS;

            if (useSSL) {
                sslConfig = new SSLConfig(connectionInfo, activity);
                error = sslConfig.configure();
            }

            if (error == 0)
                login();
            else
                machine.setState(STATE.ERROR, error);
        }
        // if the state is already running, we are reconnecting
        else if (machine.getState() == STATE.RUNNING) {
            activity.onOpen();
        }
    }

    // called from activity
    public void disconnectFromRoom() {
        machine.removeObserver(activity);
        this.activity = null;
    }

    public boolean isBound() {
        return this.activity != null;
    }

    public PerformanceTimer getPerformance() {
        return performance;
    }

    public AppRTCSignalingParameters getSignalingParams() {
        return sessionInfo.getSignalingParams();
    }

    // called from AppRTCActivity
    public void changeToErrorState() {
        machine.setState(STATE.ERROR, R.string.appRTC_toast_connection_finish);
    }

    public void disconnect() {
        proxying = false;

        // we're disconnecting, update the database record with the current timestamp
        dbHandler.close();

        performance.cancel(); // stop taking performance measurements

        // shut down the WebSocket if it's open
        if (webSocket != null && webSocket.isConnected())
            webSocket.disconnect();
        if (socketHandlerThread != null)
            socketHandlerThread.quitSafely();
    }

    public synchronized void sendMessage(Request msg) {
        if (proxying) {
            //webSocket.sendBinaryMessage(msg.toByteArray());
            // VM is expecting a message delimiter (varint prefix) so write a delimited message instead
            try {
                ByteArrayOutputStream stream = new ByteArrayOutputStream();
                msg.writeDelimitedTo(stream);
                webSocket.sendBinaryMessage(stream.toByteArray());
            } catch (IOException e) {
                Log.e(TAG, "Error writing delimited byte output:", e);
            }
        }
    }

    // STEP 1: STARTED -> AUTH, Authenticate with the SVMP login REST service
    private class SVMPAuthenticator extends AsyncTask<JSONObject, Void, Integer> {
        private boolean passwordChange;

        @Override
        protected Integer doInBackground(JSONObject... jsonObjects) {
            int returnVal = R.string.appRTC_toast_socketConnector_fail; // generic error message
            JSONObject jsonRequest = jsonObjects[0];

            passwordChange = jsonRequest.has("newPassword");
            int rPort = connectionInfo.getPort();
            String proto = useSSL ? "https" : "http", rHost = connectionInfo.getHost(),
                    // if we're changing our password, use a different API
                    api = passwordChange ? "changePassword" : "login",
                    uri = String.format("%s://%s:%d/%s", proto, rHost, rPort, api);

            // set up HttpParams
            HttpParams params = new BasicHttpParams();
            HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
            HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);

            // set up ConnectionManager
            SchemeRegistry registry = new SchemeRegistry();
            registry.register(new Scheme(proto,
                    useSSL ? sslConfig.getSocketFactory() : PlainSocketFactory.getSocketFactory(), rPort));
            ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);

            // create HttpClient
            DefaultHttpClient httpclient = new DefaultHttpClient(ccm, params);
            HttpPost post = new HttpPost(uri);
            post.setHeader(HTTP.CONTENT_TYPE, "application/json");

            try {
                StringEntity entity = new StringEntity(jsonRequest.toString());
                post.setEntity(entity);
                HttpResponse response = httpclient.execute(post);
                int responseCode = response.getStatusLine().getStatusCode();

                if (responseCode == 200) { // "OK", code for AUTH_OK
                    // get JSON object
                    ByteArrayOutputStream out = new ByteArrayOutputStream();
                    response.getEntity().writeTo(out);
                    out.close();
                    JSONObject jsonResponse = new JSONObject(out.toString());

                    // get session info
                    String token = jsonResponse.getJSONObject("sessionInfo").getString("token");
                    long expires = new Date().getTime()
                            + (1000 * jsonResponse.getJSONObject("sessionInfo").getInt("maxLength"));
                    String host = jsonResponse.getJSONObject("server").getString("host");
                    String port = jsonResponse.getJSONObject("server").getString("port");
                    JSONObject webrtc = jsonResponse.getJSONObject("webrtc");
                    sessionInfo = new SessionInfo(token, expires, host, port, webrtc);

                    if (sessionInfo.getSignalingParams() != null)
                        returnVal = 0; // success
                } else if (responseCode == 403) { // "Forbidden", code for NEED_PASSWORD_CHANGE
                    returnVal = R.string.svmpActivity_toast_needPasswordChange;
                } else if ((responseCode == 400 || responseCode == 401) && !passwordChange) { // "Unauthorized", code for AUTH_FAIL
                    returnVal = R.string.appRTC_toast_svmpAuthenticator_fail;
                } else if (responseCode == 400 || responseCode == 401) { // "Unauthorized", code for PASSWORD_CHANGE_FAIL
                    returnVal = R.string.appRTC_toast_svmpAuthenticator_passwordChangeFail;
                } else if (responseCode == 404) { // "Not Found"
                    returnVal = R.string.appRTC_toast_socketConnector_404;
                }
            } catch (JSONException e) {
                Log.e(TAG, "Failed to parse JSON response:", e);
            } catch (SSLHandshakeException e) {
                String msg = e.getMessage();
                if (msg.contains("java.security.cert.CertPathValidatorException")) {
                    // the server's certificate isn't in our trust store
                    Log.e(TAG, "Untrusted server certificate!");
                    returnVal = R.string.appRTC_toast_socketConnector_failUntrustedServer;
                } else {
                    Log.e(TAG, "Error during SSL handshake: " + e.getMessage());
                    returnVal = R.string.appRTC_toast_socketConnector_failSSLHandshake;
                }
            } catch (SSLException e) {
                if ("Connection closed by peer".equals(e.getMessage())) {
                    // connection failed, we tried to connect using SSL but REST API's SSL is turned off
                    Log.e(TAG, "Client encryption is on but server encryption is off:", e);
                    returnVal = R.string.appRTC_toast_socketConnector_failSSL;
                } else {
                    Log.e(TAG, "SSL error:", e);
                }
            } catch (NoHttpResponseException e) {
                if ("The target server failed to respond".equals(e.getMessage())) {
                    // connection failed, we tried to connect without using SSL but REST API's SSL is turned on
                    Log.e(TAG, "Client encryption is off but server encryption is on:", e);
                    returnVal = R.string.appRTC_toast_socketConnector_failSSL;
                } else {
                    Log.e(TAG, "HTTP request failed:", e);
                }
            } catch (IOException e) {
                Log.e(TAG, "HTTP request failed:", e);
            }
            return returnVal;
        }

        @Override
        protected void onPostExecute(Integer result) {
            if (result == 0) { // success, start the next phase and connect to the SVMP proxy server
                dbHandler.updateSessionInfo(connectionInfo, sessionInfo);
                machine.setState(STATE.AUTH, R.string.appRTC_toast_svmpAuthenticator_success); // STARTED -> AUTH
                connect();
            } else {
                // authentication failed, handle appropriately
                machine.setState(STATE.ERROR, result); // STARTED -> ERROR
            }
        }
    }

    public void login() {
        // attempt to get any existing auth data JSONObject that's in memory (e.g. made of user input such as password)
        JSONObject jsonObject = AuthData.getJSON(connectionInfo);
        if (jsonObject != null) {
            // execute async HTTP request to the REST auth API
            (new SVMPAuthenticator()).execute(jsonObject);
        } else {
            sessionInfo = dbHandler.getSessionInfo(connectionInfo);
            if (sessionInfo != null) {
                // we've already authenticated, we can connect directly to the proxy
                machine.setState(STATE.AUTH, R.string.appRTC_toast_svmpAuthenticator_bypassed); // STARTED -> AUTH
                connect();
            } else {
                Log.e(TAG, "login failed: no auth data or session info found");
                machine.setState(STATE.ERROR, R.string.appRTC_toast_connection_finish);
            }
        }
    }

    // STEP 2: AUTH -> CONNECTED, Connect to the SVMP proxy service
    public void connect() {
        new SocketConnector().execute();
    }

    private class SocketConnector extends AsyncTask<Void, Void, Integer> {
        @Override
        protected Integer doInBackground(Void... params) {
            int returnVal = R.string.appRTC_toast_socketConnector_fail; // resID for return message

            try {
                // create the socket for the WebSocketConnection to use
                // we do this here because the Looping and Handling that takes place in the WebSocket code causes
                // the app to freeze when any other processes are launched (such as KeyChain or MemorizingTrustManager)
                javax.net.SocketFactory factory;
                if (useSSL) {
                    factory = sslConfig.getSSLContext().getSocketFactory();
                } else {
                    factory = javax.net.SocketFactory.getDefault();
                }
                socket = factory.createSocket(sessionInfo.getHost(), Integer.parseInt(sessionInfo.getPort()));
                if (useSSL) {
                    SSLSocket sslSocket = (SSLSocket) socket;
                    sslSocket.setEnabledProtocols(ENABLED_PROTOCOLS);
                    sslSocket.setEnabledCipherSuites(ENABLED_CIPHERS);
                    sslSocket.startHandshake(); // starts the handshake to verify the cert before continuing
                }
                //socket.setTcpNoDelay(true);

                // if we made it to this point, return a success message
                returnVal = 0;
            } catch (SSLHandshakeException e) {
                String msg = e.getMessage();
                if (msg.contains("java.security.cert.CertPathValidatorException")) {
                    // the server's certificate isn't in our trust store
                    Log.e(TAG, "Untrusted server certificate!");
                    returnVal = R.string.appRTC_toast_socketConnector_failUntrustedServer;
                } else {
                    Log.e(TAG, "Error during SSL handshake: " + e.getMessage());
                    returnVal = R.string.appRTC_toast_socketConnector_failSSLHandshake;
                }
            } catch (SSLException e) {
                if ("Connection closed by peer".equals(e.getMessage())) {
                    // connection failed, we tried to connect using SSL but REST API's SSL is turned off
                    Log.e(TAG, "Client encryption is on but server encryption is off:", e);
                    returnVal = R.string.appRTC_toast_socketConnector_failSSL;
                } else {
                    Log.e(TAG, "SSL error:", e);
                }
            } catch (Exception e) {
                Log.e(TAG, "Exception: " + e.getMessage());
                e.printStackTrace();
            }
            return returnVal;
        }

        @Override
        protected void onPostExecute(Integer result) {
            if (result != 0) {
                machine.setState(STATE.ERROR, result); // STARTED -> ERROR
            } else {
                // we have to run the WebSocket connection in a HandlerThread to ensure that Looper is prepared
                // properly and that the MemorizingTrustManager doesn't execute on the main UI thread
                socketHandlerThread = new SocketHandlerThread("svmp-websocket-" + new Date().getTime());
                socketHandlerThread.start();
            }
        }
    }

    private class SocketHandlerThread extends HandlerThread {

        public SocketHandlerThread(String name) {
            super(name);
        }

        @Override
        protected void onLooperPrepared() {
            // set up the WebSocket URI for the svmp-server
            String proto = useSSL ? "wss" : "ws";
            URI uri = URI.create(String.format("%s://%s:%s", proto, sessionInfo.getHost(), sessionInfo.getPort()));
            Log.d(TAG, "Socket connecting to " + uri.toString());

            // set up the WebSocket options for the svmp-server
            WebSocketOptions options = new WebSocketOptions();
            options.setMaxFramePayloadSize(8 * 128 * 1024); // increase max frame size to handle high-res icons
            HashMap<String, String> headers = new HashMap<String, String>();
            // HACK: JavaScript WebSocket API doesn't allow for custom headers, so we repurpose this header instead
            // We set it here instead of the constructor because this doesn't append a comma suffix
            headers.put("Sec-WebSocket-Protocol", sessionInfo.getToken());
            options.setHeaders(headers);

            // we have the socket and the SSL handshake has completed
            // now establish a WebSocketConnection
            try {
                webSocket = new WebSocketConnection();
                webSocket.connect(socket, uri, null, observer, options);
            } catch (WebSocketException e) {
                Log.e(TAG, "Failed to connect to SVMP proxy:", e);
                machine.setState(STATE.ERROR, R.string.appRTC_toast_socketConnector_fail);
            }
        }
    }

    WebSocket.WebSocketConnectionObserver observer = new WebSocket.WebSocketConnectionObserver() {
        private boolean hasVMREADY;

        @Override
        public void onOpen() {
            Log.i(TAG, "WebSocket connected.");
            machine.setState(STATE.CONNECTED, R.string.appRTC_toast_socketConnector_success); // AUTH -> CONNECTED
            // wait for VMREADY
        }

        @Override
        public void onClose(WebSocketCloseNotification code, String reason) {
            if (proxying || machine.getState() == STATE.AUTH || machine.getState() == STATE.CONNECTED) {
                // either we were disconnected unexpectedly, or the connection was never successfully established
                // we haven't called disconnect(), this was an error; log this as an Error message and change state
                changeToErrorState();
                Log.e(TAG, "WebSocket disconnected: " + code.toString() + ", " + reason);
            } else // we called disconnect(), this was intentional; log this as an Info message
                Log.i(TAG, "WebSocket disconnected.");
        }

        @Override
        public void onTextMessage(String payload) {
        }

        @Override
        public void onRawTextMessage(byte[] payload) {
        }

        @Override
        public void onBinaryMessage(byte[] payload) {
            try {
                Response data = Response.parseFrom(payload);
                Log.d(TAG, "Received incoming message object of type " + data.getType().name());
                onResponse(data);
            } catch (InvalidProtocolBufferException e) {
                Log.e(TAG, "Unable to parse protobuf:", e);
                changeToErrorState();
            }
        }

        private void onResponse(Response data) {
            if (data.getType() == Response.ResponseType.ERROR) {
                Log.e(TAG, "Received ERROR message");
                int error = hasVMREADY ? R.string.appRTC_toast_connection_finish
                        : R.string.appRTC_toast_svmpReadyWait_fail;
                machine.setState(STATE.ERROR, error);
            } else if (!hasVMREADY) // we are in the CONNECTED state, waiting for VMREADY
                onResponseCONNECTED(data);
            else // we are in the RUNNING state
                onResponseRUNNING(data);
        }

        // STEP 3: CONNECTED -> RUNNING, Receive VMREADY message
        private void onResponseCONNECTED(Response data) {
            int error = R.string.appRTC_toast_connection_finish; // generic error message

            // generate a status code
            Response.ResponseType type = data.getType();
            if (type == Response.ResponseType.VMREADY)
                error = 0;
            else if (type == Response.ResponseType.AUTH
                    && data.getAuthResponse().getType() == AuthResponse.AuthResponseType.AUTH_FAIL)
                error = R.string.appRTC_toast_svmpAuthenticator_fail;
            // any other message type throws us into an error state

            // act on the status code
            if (error == 0) { // success
                hasVMREADY = true;
                machine.setState(STATE.RUNNING, R.string.appRTC_toast_svmpReadyWait_success); // CONNECTED -> RUNNING
                proxying = true;

                // callbacks to the service and activity to let them know the connection has started
                service.onOpen();
                if (isBound())
                    activity.onOpen();

                // start taking performance measurements
                performance.start();
            } else // fail with the appropriate error message
                machine.setState(STATE.ERROR, error);
        }

        // STEP 4: RUNNING
        private void onResponseRUNNING(final Response data) {
            boolean consumed = service.onMessage(data);
            if (!consumed && isBound()) {
                activity.runOnUiThread(new Runnable() {
                    public void run() {
                        activity.onMessage(data);
                    }
                });
            }
        }
    };
}