Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
michael@0 | 1 | // This file was copied from: |
michael@0 | 2 | // https://github.com/koush/android-websockets/blob/master/src/com/codebutler/android_websockets/WebSocketClient.java |
michael@0 | 3 | |
michael@0 | 4 | package com.codebutler.android_websockets; |
michael@0 | 5 | |
michael@0 | 6 | import android.os.Handler; |
michael@0 | 7 | import android.os.HandlerThread; |
michael@0 | 8 | import android.text.TextUtils; |
michael@0 | 9 | import android.util.Base64; |
michael@0 | 10 | import android.util.Log; |
michael@0 | 11 | import org.apache.http.*; |
michael@0 | 12 | import org.apache.http.client.HttpResponseException; |
michael@0 | 13 | import org.apache.http.message.BasicLineParser; |
michael@0 | 14 | import org.apache.http.message.BasicNameValuePair; |
michael@0 | 15 | |
michael@0 | 16 | import javax.net.SocketFactory; |
michael@0 | 17 | import javax.net.ssl.SSLContext; |
michael@0 | 18 | import javax.net.ssl.SSLException; |
michael@0 | 19 | import javax.net.ssl.SSLSocketFactory; |
michael@0 | 20 | import javax.net.ssl.TrustManager; |
michael@0 | 21 | import java.io.EOFException; |
michael@0 | 22 | import java.io.IOException; |
michael@0 | 23 | import java.io.OutputStream; |
michael@0 | 24 | import java.io.PrintWriter; |
michael@0 | 25 | import java.net.Socket; |
michael@0 | 26 | import java.net.URI; |
michael@0 | 27 | import java.security.KeyManagementException; |
michael@0 | 28 | import java.security.NoSuchAlgorithmException; |
michael@0 | 29 | import java.util.List; |
michael@0 | 30 | |
michael@0 | 31 | public class WebSocketClient { |
michael@0 | 32 | private static final String TAG = "WebSocketClient"; |
michael@0 | 33 | |
michael@0 | 34 | private URI mURI; |
michael@0 | 35 | private Listener mListener; |
michael@0 | 36 | private Socket mSocket; |
michael@0 | 37 | private Thread mThread; |
michael@0 | 38 | private HandlerThread mHandlerThread; |
michael@0 | 39 | private Handler mHandler; |
michael@0 | 40 | private List<BasicNameValuePair> mExtraHeaders; |
michael@0 | 41 | private HybiParser mParser; |
michael@0 | 42 | private boolean mConnected; |
michael@0 | 43 | |
michael@0 | 44 | private final Object mSendLock = new Object(); |
michael@0 | 45 | |
michael@0 | 46 | private static TrustManager[] sTrustManagers; |
michael@0 | 47 | |
michael@0 | 48 | public static void setTrustManagers(TrustManager[] tm) { |
michael@0 | 49 | sTrustManagers = tm; |
michael@0 | 50 | } |
michael@0 | 51 | |
michael@0 | 52 | public WebSocketClient(URI uri, Listener listener, List<BasicNameValuePair> extraHeaders) { |
michael@0 | 53 | mURI = uri; |
michael@0 | 54 | mListener = listener; |
michael@0 | 55 | mExtraHeaders = extraHeaders; |
michael@0 | 56 | mConnected = false; |
michael@0 | 57 | mParser = new HybiParser(this); |
michael@0 | 58 | |
michael@0 | 59 | mHandlerThread = new HandlerThread("websocket-thread"); |
michael@0 | 60 | mHandlerThread.start(); |
michael@0 | 61 | mHandler = new Handler(mHandlerThread.getLooper()); |
michael@0 | 62 | } |
michael@0 | 63 | |
michael@0 | 64 | public Listener getListener() { |
michael@0 | 65 | return mListener; |
michael@0 | 66 | } |
michael@0 | 67 | |
michael@0 | 68 | public void connect() { |
michael@0 | 69 | if (mThread != null && mThread.isAlive()) { |
michael@0 | 70 | return; |
michael@0 | 71 | } |
michael@0 | 72 | |
michael@0 | 73 | mThread = new Thread(new Runnable() { |
michael@0 | 74 | @Override |
michael@0 | 75 | public void run() { |
michael@0 | 76 | try { |
michael@0 | 77 | int port = (mURI.getPort() != -1) ? mURI.getPort() : ((mURI.getScheme().equals("wss") || mURI.getScheme().equals("https")) ? 443 : 80); |
michael@0 | 78 | |
michael@0 | 79 | String path = TextUtils.isEmpty(mURI.getPath()) ? "/" : mURI.getPath(); |
michael@0 | 80 | if (!TextUtils.isEmpty(mURI.getQuery())) { |
michael@0 | 81 | path += "?" + mURI.getQuery(); |
michael@0 | 82 | } |
michael@0 | 83 | |
michael@0 | 84 | String originScheme = mURI.getScheme().equals("wss") ? "https" : "http"; |
michael@0 | 85 | URI origin = new URI(originScheme, "//" + mURI.getHost(), null); |
michael@0 | 86 | |
michael@0 | 87 | SocketFactory factory = (mURI.getScheme().equals("wss") || mURI.getScheme().equals("https")) ? getSSLSocketFactory() : SocketFactory.getDefault(); |
michael@0 | 88 | mSocket = factory.createSocket(mURI.getHost(), port); |
michael@0 | 89 | |
michael@0 | 90 | PrintWriter out = new PrintWriter(mSocket.getOutputStream()); |
michael@0 | 91 | out.print("GET " + path + " HTTP/1.1\r\n"); |
michael@0 | 92 | out.print("Upgrade: websocket\r\n"); |
michael@0 | 93 | out.print("Connection: Upgrade\r\n"); |
michael@0 | 94 | out.print("Host: " + mURI.getHost() + "\r\n"); |
michael@0 | 95 | out.print("Origin: " + origin.toString() + "\r\n"); |
michael@0 | 96 | out.print("Sec-WebSocket-Key: " + createSecret() + "\r\n"); |
michael@0 | 97 | out.print("Sec-WebSocket-Version: 13\r\n"); |
michael@0 | 98 | if (mExtraHeaders != null) { |
michael@0 | 99 | for (NameValuePair pair : mExtraHeaders) { |
michael@0 | 100 | out.print(String.format("%s: %s\r\n", pair.getName(), pair.getValue())); |
michael@0 | 101 | } |
michael@0 | 102 | } |
michael@0 | 103 | out.print("\r\n"); |
michael@0 | 104 | out.flush(); |
michael@0 | 105 | |
michael@0 | 106 | HybiParser.HappyDataInputStream stream = new HybiParser.HappyDataInputStream(mSocket.getInputStream()); |
michael@0 | 107 | |
michael@0 | 108 | // Read HTTP response status line. |
michael@0 | 109 | StatusLine statusLine = parseStatusLine(readLine(stream)); |
michael@0 | 110 | if (statusLine == null) { |
michael@0 | 111 | throw new HttpException("Received no reply from server."); |
michael@0 | 112 | } else if (statusLine.getStatusCode() != HttpStatus.SC_SWITCHING_PROTOCOLS) { |
michael@0 | 113 | throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase()); |
michael@0 | 114 | } |
michael@0 | 115 | |
michael@0 | 116 | // Read HTTP response headers. |
michael@0 | 117 | String line; |
michael@0 | 118 | while (!TextUtils.isEmpty(line = readLine(stream))) { |
michael@0 | 119 | Header header = parseHeader(line); |
michael@0 | 120 | if (header.getName().equals("Sec-WebSocket-Accept")) { |
michael@0 | 121 | // FIXME: Verify the response... |
michael@0 | 122 | } |
michael@0 | 123 | } |
michael@0 | 124 | |
michael@0 | 125 | mListener.onConnect(); |
michael@0 | 126 | |
michael@0 | 127 | mConnected = true; |
michael@0 | 128 | |
michael@0 | 129 | // Now decode websocket frames. |
michael@0 | 130 | mParser.start(stream); |
michael@0 | 131 | |
michael@0 | 132 | } catch (EOFException ex) { |
michael@0 | 133 | Log.d(TAG, "WebSocket EOF!", ex); |
michael@0 | 134 | mListener.onDisconnect(0, "EOF"); |
michael@0 | 135 | mConnected = false; |
michael@0 | 136 | |
michael@0 | 137 | } catch (SSLException ex) { |
michael@0 | 138 | // Connection reset by peer |
michael@0 | 139 | Log.d(TAG, "Websocket SSL error!", ex); |
michael@0 | 140 | mListener.onDisconnect(0, "SSL"); |
michael@0 | 141 | mConnected = false; |
michael@0 | 142 | |
michael@0 | 143 | } catch (Exception ex) { |
michael@0 | 144 | mListener.onError(ex); |
michael@0 | 145 | } |
michael@0 | 146 | } |
michael@0 | 147 | }); |
michael@0 | 148 | mThread.start(); |
michael@0 | 149 | } |
michael@0 | 150 | |
michael@0 | 151 | public void disconnect() { |
michael@0 | 152 | if (mSocket != null) { |
michael@0 | 153 | mHandler.post(new Runnable() { |
michael@0 | 154 | @Override |
michael@0 | 155 | public void run() { |
michael@0 | 156 | if (mSocket != null) { |
michael@0 | 157 | try { |
michael@0 | 158 | mSocket.close(); |
michael@0 | 159 | } catch (IOException ex) { |
michael@0 | 160 | Log.d(TAG, "Error while disconnecting", ex); |
michael@0 | 161 | mListener.onError(ex); |
michael@0 | 162 | } |
michael@0 | 163 | mSocket = null; |
michael@0 | 164 | } |
michael@0 | 165 | mConnected = false; |
michael@0 | 166 | } |
michael@0 | 167 | }); |
michael@0 | 168 | } |
michael@0 | 169 | } |
michael@0 | 170 | |
michael@0 | 171 | public void send(String data) { |
michael@0 | 172 | sendFrame(mParser.frame(data)); |
michael@0 | 173 | } |
michael@0 | 174 | |
michael@0 | 175 | public void send(byte[] data) { |
michael@0 | 176 | sendFrame(mParser.frame(data)); |
michael@0 | 177 | } |
michael@0 | 178 | |
michael@0 | 179 | public boolean isConnected() { |
michael@0 | 180 | return mConnected; |
michael@0 | 181 | } |
michael@0 | 182 | |
michael@0 | 183 | private StatusLine parseStatusLine(String line) { |
michael@0 | 184 | if (TextUtils.isEmpty(line)) { |
michael@0 | 185 | return null; |
michael@0 | 186 | } |
michael@0 | 187 | return BasicLineParser.parseStatusLine(line, new BasicLineParser()); |
michael@0 | 188 | } |
michael@0 | 189 | |
michael@0 | 190 | private Header parseHeader(String line) { |
michael@0 | 191 | return BasicLineParser.parseHeader(line, new BasicLineParser()); |
michael@0 | 192 | } |
michael@0 | 193 | |
michael@0 | 194 | // Can't use BufferedReader because it buffers past the HTTP data. |
michael@0 | 195 | private String readLine(HybiParser.HappyDataInputStream reader) throws IOException { |
michael@0 | 196 | int readChar = reader.read(); |
michael@0 | 197 | if (readChar == -1) { |
michael@0 | 198 | return null; |
michael@0 | 199 | } |
michael@0 | 200 | StringBuilder string = new StringBuilder(""); |
michael@0 | 201 | while (readChar != '\n') { |
michael@0 | 202 | if (readChar != '\r') { |
michael@0 | 203 | string.append((char) readChar); |
michael@0 | 204 | } |
michael@0 | 205 | |
michael@0 | 206 | readChar = reader.read(); |
michael@0 | 207 | if (readChar == -1) { |
michael@0 | 208 | return null; |
michael@0 | 209 | } |
michael@0 | 210 | } |
michael@0 | 211 | return string.toString(); |
michael@0 | 212 | } |
michael@0 | 213 | |
michael@0 | 214 | private String createSecret() { |
michael@0 | 215 | byte[] nonce = new byte[16]; |
michael@0 | 216 | for (int i = 0; i < 16; i++) { |
michael@0 | 217 | nonce[i] = (byte) (Math.random() * 256); |
michael@0 | 218 | } |
michael@0 | 219 | return Base64.encodeToString(nonce, Base64.DEFAULT).trim(); |
michael@0 | 220 | } |
michael@0 | 221 | |
michael@0 | 222 | void sendFrame(final byte[] frame) { |
michael@0 | 223 | mHandler.post(new Runnable() { |
michael@0 | 224 | @Override |
michael@0 | 225 | public void run() { |
michael@0 | 226 | try { |
michael@0 | 227 | synchronized (mSendLock) { |
michael@0 | 228 | OutputStream outputStream = mSocket.getOutputStream(); |
michael@0 | 229 | outputStream.write(frame); |
michael@0 | 230 | outputStream.flush(); |
michael@0 | 231 | } |
michael@0 | 232 | } catch (IOException e) { |
michael@0 | 233 | mListener.onError(e); |
michael@0 | 234 | } |
michael@0 | 235 | } |
michael@0 | 236 | }); |
michael@0 | 237 | } |
michael@0 | 238 | |
michael@0 | 239 | public interface Listener { |
michael@0 | 240 | public void onConnect(); |
michael@0 | 241 | public void onMessage(String message); |
michael@0 | 242 | public void onMessage(byte[] data); |
michael@0 | 243 | public void onDisconnect(int code, String reason); |
michael@0 | 244 | public void onError(Exception error); |
michael@0 | 245 | } |
michael@0 | 246 | |
michael@0 | 247 | private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { |
michael@0 | 248 | SSLContext context = SSLContext.getInstance("TLS"); |
michael@0 | 249 | context.init(null, sTrustManagers, null); |
michael@0 | 250 | return context.getSocketFactory(); |
michael@0 | 251 | } |
michael@0 | 252 | } |