package org.sagemath.droid.websocket; /** * @author Eric Butler */ import android.os.Handler; import android.os.HandlerThread; import android.text.TextUtils; import android.util.Base64; import android.util.Log; import org.apache.http.*; import org.apache.http.client.HttpResponseException; import org.apache.http.message.BasicLineParser; import org.apache.http.message.BasicNameValuePair; import javax.net.SocketFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import java.io.EOFException; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.net.URI; import java.security.KeyManagementException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.List; public class WebSocketClient { private static final String TAG = "WebSocketClient"; private URI mURI; private Listener mListener; private Socket mSocket; private Thread mThread; private HandlerThread mHandlerThread; private Handler mHandler; private List<BasicNameValuePair> mExtraHeaders; private HybiParser mParser; private final Object mSendLock = new Object(); private static TrustManager[] sTrustManagers; public static void setTrustManagers(TrustManager[] tm) { sTrustManagers = tm; } public WebSocketClient(URI uri, Listener listener, List<BasicNameValuePair> extraHeaders) { mURI = uri; mListener = listener; mExtraHeaders = extraHeaders; mParser = new HybiParser(this); mHandlerThread = new HandlerThread("websocket-thread"); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); } public Listener getListener() { return mListener; } public void connect() { if (mThread != null && mThread.isAlive()) { return; } mThread = new Thread(new Runnable() { @Override public void run() { try { String secret = createSecret(); int port = (mURI.getPort() != -1) ? mURI.getPort() : (mURI.getScheme().equals("wss") ? 443 : 80); String path = TextUtils.isEmpty(mURI.getPath()) ? "/" : mURI.getPath(); if (!TextUtils.isEmpty(mURI.getQuery())) { path += "?" + mURI.getQuery(); } String originScheme = mURI.getScheme().equals("wss") ? "https" : "http"; URI origin = new URI(originScheme, "//" + mURI.getHost(), null); SocketFactory factory = mURI.getScheme().equals("wss") ? getSSLSocketFactory() : SocketFactory.getDefault(); mSocket = factory.createSocket(mURI.getHost(), port); PrintWriter out = new PrintWriter(mSocket.getOutputStream()); out.print("GET " + path + " HTTP/1.1\r\n"); out.print("Upgrade: websocket\r\n"); out.print("Connection: Upgrade\r\n"); out.print("Host: " + mURI.getHost() + "\r\n"); out.print("Origin: " + origin.toString() + "\r\n"); out.print("Sec-WebSocket-Key: " + secret + "\r\n"); out.print("Sec-WebSocket-Version: 13\r\n"); if (mExtraHeaders != null) { for (NameValuePair pair : mExtraHeaders) { out.print(String.format("%s: %s\r\n", pair.getName(), pair.getValue())); } } out.print("\r\n"); out.flush(); HybiParser.HappyDataInputStream stream = new HybiParser.HappyDataInputStream(mSocket.getInputStream()); // Read HTTP response status line. StatusLine statusLine = parseStatusLine(readLine(stream)); if (statusLine == null) { throw new HttpException("Received no reply from server."); } else if (statusLine.getStatusCode() != HttpStatus.SC_SWITCHING_PROTOCOLS) { throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase()); } // Read HTTP response headers. String line; boolean validated = false; while (!TextUtils.isEmpty(line = readLine(stream))) { Header header = parseHeader(line); if (header.getName().equals("Sec-WebSocket-Accept")) { String expected = createSecretValidation(secret); String actual = header.getValue().trim(); if (!expected.equals(actual)) { throw new HttpException("Bad Sec-WebSocket-Accept header value."); } validated = true; } } if (!validated) { throw new HttpException("No Sec-WebSocket-Accept header."); } mListener.onConnect(); // Now decode websocket frames. mParser.start(stream); } catch (EOFException ex) { Log.d(TAG, "WebSocket EOF!", ex); mListener.onDisconnect(0, "EOF"); } catch (SSLException ex) { // Connection reset by peer Log.d(TAG, "Websocket SSL error!", ex); mListener.onDisconnect(0, "SSL"); } catch (Exception ex) { mListener.onError(ex); } } }); mThread.start(); } public void disconnect() { if (mSocket != null) { mHandler.post(new Runnable() { @Override public void run() { try { mSocket.close(); mSocket = null; } catch (IOException ex) { Log.d(TAG, "Error while disconnecting", ex); mListener.onError(ex); } } }); } } public void send(String data) { sendFrame(mParser.frame(data)); } public void send(byte[] data) { sendFrame(mParser.frame(data)); } private StatusLine parseStatusLine(String line) { if (TextUtils.isEmpty(line)) { return null; } return BasicLineParser.parseStatusLine(line, new BasicLineParser()); } private Header parseHeader(String line) { return BasicLineParser.parseHeader(line, new BasicLineParser()); } // Can't use BufferedReader because it buffers past the HTTP data. private String readLine(HybiParser.HappyDataInputStream reader) throws IOException { int readChar = reader.read(); if (readChar == -1) { return null; } StringBuilder string = new StringBuilder(""); while (readChar != '\n') { if (readChar != '\r') { string.append((char) readChar); } readChar = reader.read(); if (readChar == -1) { return null; } } return string.toString(); } private String createSecret() { byte[] nonce = new byte[16]; for (int i = 0; i < 16; i++) { nonce[i] = (byte) (Math.random() * 256); } return Base64.encodeToString(nonce, Base64.DEFAULT).trim(); } private String createSecretValidation(String secret) { try { MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update((secret + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").getBytes()); return Base64.encodeToString(md.digest(), Base64.DEFAULT).trim(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } void sendFrame(final byte[] frame) { mHandler.post(new Runnable() { @Override public void run() { try { synchronized (mSendLock) { if (mSocket == null) { throw new IllegalStateException("Socket not connected"); } OutputStream outputStream = mSocket.getOutputStream(); outputStream.write(frame); outputStream.flush(); } } catch (IOException e) { mListener.onError(e); } } }); } public interface Listener { public void onConnect(); public void onMessage(String message); public void onMessage(byte[] data); public void onDisconnect(int code, String reason); public void onError(Exception error); } private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { SSLContext context = SSLContext.getInstance("TLS"); context.init(null, sTrustManagers, null); return context.getSocketFactory(); } }