Java tutorial
// Copyright (c) 2009-2012 James Coglan // Copyright (c) 2012 Eric Butler // // Permission is hereby granted, free of charge, to any person obtaining a copy of // this software and associated documentation files (the 'Software'), to deal in // the Software without restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the // Software, and to permit persons to whom the Software is furnished to do so, // subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // // Source: https://github.com/codebutler/android-websockets/blob/master/src/main/java/com/codebutler/android_websockets/WebSocketClient.java package com.leanplum.internal; import android.os.Handler; import android.os.HandlerThread; import android.text.TextUtils; import android.util.Base64; import org.apache.http.Header; import org.apache.http.HttpException; import org.apache.http.HttpStatus; import org.apache.http.NameValuePair; import org.apache.http.StatusLine; import org.apache.http.client.HttpResponseException; import org.apache.http.message.BasicLineParser; import org.apache.http.message.BasicNameValuePair; import java.io.EOFException; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.net.URI; import java.net.URISyntaxException; import java.security.GeneralSecurityException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.List; import javax.net.SocketFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; // Suppressing deprecated apache dependency. @SuppressWarnings("deprecation") class WebSocketClient { private static final String TAG = "WebSocketClient"; private URI mURI; private Listener mListener; private java.net.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 { 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 = null; try { origin = new URI(originScheme, "//" + mURI.getHost(), null); } catch (URISyntaxException e) { Util.handleException(e); } SocketFactory factory; try { factory = mURI.getScheme().equals("wss") ? getSSLSocketFactory() : SocketFactory.getDefault(); } catch (GeneralSecurityException e) { Util.handleException(e); return; } try { mSocket = factory.createSocket(mURI.getHost(), port); } catch (IOException e) { Util.handleException(e); } 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 != null ? origin.toString() : "unknown") + "\r\n"); out.print("Sec-WebSocket-Key: " + createSecret() + "\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; while (!TextUtils.isEmpty(line = readLine(stream))) { Header header = parseHeader(line); if (header.getName().equals("Sec-WebSocket-Accept")) { // FIXME: Verify the response... } } mListener.onConnect(); // Now decode websocket frames. mParser.start(stream); } catch (EOFException ex) { Log.d("WebSocket EOF!", ex); mListener.onDisconnect(0, "EOF"); } catch (SSLException ex) { // Connection reset by peer Log.d("Websocket SSL error!", ex); mListener.onDisconnect(0, "SSL"); } catch (Exception e) { mListener.onError(e); } } }); mThread.start(); } public void disconnect() { if (mSocket != null) { mHandler.post(new Runnable() { @Override public void run() { try { if (mSocket != null) { mSocket.close(); mSocket = null; } } catch (IOException e) { Log.d("Error while disconnecting", e); mListener.onError(e); } } }); } } 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(); } void sendFrame(final byte[] frame) { mHandler.post(new Runnable() { @Override public void run() { try { synchronized (mSendLock) { if (mSocket != null) { OutputStream outputStream = mSocket.getOutputStream(); outputStream.write(frame); outputStream.flush(); } } } catch (IOException e) { mListener.onError(e); } } }); } interface Listener { void onConnect(); void onMessage(String message); void onMessage(byte[] data); void onDisconnect(int code, String reason); void onError(Exception error); } private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { SSLContext context = SSLContext.getInstance("TLS"); context.init(null, sTrustManagers, null); return context.getSocketFactory(); } }