diff --git a/build.gradle b/build.gradle index 802d7fc..dc5f1f3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,36 @@ -def android_jar = new File(System.getenv('ANDROID_HOME'), '/platforms/android-17/android.jar') +apply plugin: 'com.android.library' -apply plugin: 'java' +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.2.3' + } +} -sourceCompatibility = 1.5 +android { + compileSdkVersion 22 + buildToolsVersion "22.0.1" + defaultConfig { + minSdkVersion 9 + targetSdkVersion 19 + versionCode 1 + versionName "1.0" + } + lintOptions { + abortOnError false + } + +} +repositories { + jcenter() +} dependencies { - compile files(android_jar) + repositories { + mavenCentral() + } + compile 'com.squareup.okhttp:okhttp:2.5.0' + } diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 0000000..1ab3b62 --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/src/main/java/com/codebutler/android_websockets/WebSocketClient.java b/src/main/java/com/codebutler/android_websockets/WebSocketClient.java index d5e1b73..765968a 100644 --- a/src/main/java/com/codebutler/android_websockets/WebSocketClient.java +++ b/src/main/java/com/codebutler/android_websockets/WebSocketClient.java @@ -5,16 +5,10 @@ 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 com.squareup.okhttp.Headers; +import com.squareup.okhttp.internal.http.StatusLine; + import java.io.EOFException; import java.io.IOException; import java.io.OutputStream; @@ -24,58 +18,82 @@ import java.security.KeyManagementException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.List; +import java.util.Map; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; -public class WebSocketClient { +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 mExtraHeaders; - private HybiParser mParser; + private static final int SWITCHING_PROTOCOLS_CODE = 101; + + private URI mURI; + + private Listener mListener; + + private Socket mSocket; + + private Thread mThread; + + private HandlerThread mHandlerThread; + + private Handler mHandler; + + private Map mExtraHeaders; + + private HybiParser mParser; private final Object mSendLock = new Object(); private static TrustManager[] sTrustManagers; - public static void setTrustManagers(TrustManager[] tm) { + public static void setTrustManagers(TrustManager[] tm) + { sTrustManagers = tm; } - public WebSocketClient(URI uri, Listener listener, List extraHeaders) { - mURI = uri; + public WebSocketClient(URI uri, Listener listener, Map extraHeaders) + { + mURI = uri; mListener = listener; mExtraHeaders = extraHeaders; - mParser = new HybiParser(this); + mParser = new HybiParser(this); mHandlerThread = new HandlerThread("websocket-thread"); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); } - public Listener getListener() { + public Listener getListener() + { return mListener; } - public void connect() { - if (mThread != null && mThread.isAlive()) { + public void connect() + { + if(mThread != null && mThread.isAlive()) + { return; } - mThread = new Thread(new Runnable() { + mThread = new Thread(new Runnable() + { @Override - public void run() { - try { + 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())) { + if(!TextUtils.isEmpty(mURI.getQuery())) + { path += "?" + mURI.getQuery(); } @@ -93,9 +111,13 @@ public void run() { 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())); + + + if(mExtraHeaders != null) + { + for(String key : mExtraHeaders.keySet()) + { + out.print(String.format("%s: %s\r\n", key, mExtraHeaders.get(key))); } } out.print("\r\n"); @@ -103,67 +125,94 @@ public void run() { HybiParser.HappyDataInputStream stream = new HybiParser.HappyDataInputStream(mSocket.getInputStream()); + String statusLineString = readLine(stream); // 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()); + StatusLine statusLine = StatusLine.parse(statusLineString); + if(statusLine == null) + { + throw new Exception("Received no reply from server."); + } + else if(statusLine.code != SWITCHING_PROTOCOLS_CODE) + { + throw new Exception("Wrong status code: " + statusLine.code); } // 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")) { + while(!TextUtils.isEmpty(line = readLine(stream))) + { + Headers header = parseHeader(line); + if(header.name(0).equals("Sec-WebSocket-Accept")) + { String expected = createSecretValidation(secret); - String actual = header.getValue().trim(); + String actual = header.value(0).trim(); - if (!expected.equals(actual)) { - throw new HttpException("Bad Sec-WebSocket-Accept header value."); + if(!expected.equals(actual)) + { + throw new Exception("Bad Sec-WebSocket-Accept header value."); } validated = true; } } - if (!validated) { - throw new HttpException("No Sec-WebSocket-Accept header."); + if(!validated) + { + throw new Exception("No Sec-WebSocket-Accept header."); } mListener.onConnect(); // Now decode websocket frames. mParser.start(stream); - - } catch (EOFException ex) { + } + catch(EOFException ex) + { Log.d(TAG, "WebSocket EOF!", ex); mListener.onDisconnect(0, "EOF"); - - } catch (SSLException ex) { + } + catch(SSLException ex) + { // Connection reset by peer Log.d(TAG, "Websocket SSL error!", ex); mListener.onDisconnect(0, "SSL"); - - } catch (Exception ex) { + } + catch(Exception ex) + { mListener.onError(ex); } } }); mThread.start(); } + public Headers parseHeader(String line) + { + int index = line.indexOf(":"); + if(index == -1) + { + throw new IllegalArgumentException("Unexpected header: " + line); + } + return new Headers.Builder().add(line.substring(0, index).trim(), line.substring(index + 1)).build(); + } - public void disconnect() { - if (mSocket != null) { - mHandler.post(new Runnable() { + public void disconnect() + { + if(mSocket != null) + { + mHandler.post(new Runnable() + { @Override - public void run() { - try { + public void run() + { + try + { mSocket.close(); mSocket = null; - } catch (IOException ex) { + } + catch(IOException ex) + { Log.d(TAG, "Error while disconnecting", ex); mListener.onError(ex); } @@ -172,92 +221,109 @@ public void run() { } } - public void send(String data) { + public void send(String data) + { sendFrame(mParser.frame(data)); } - public void send(byte[] 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 { + private String readLine(HybiParser.HappyDataInputStream reader) throws IOException + { int readChar = reader.read(); - if (readChar == -1) { + if(readChar == -1) + { return null; } StringBuilder string = new StringBuilder(""); - while (readChar != '\n') { - if (readChar != '\r') { + while(readChar != '\n') + { + if(readChar != '\r') + { string.append((char) readChar); } readChar = reader.read(); - if (readChar == -1) { + if(readChar == -1) + { return null; } } return string.toString(); } - private String createSecret() { + private String createSecret() + { byte[] nonce = new byte[16]; - for (int i = 0; i < 16; i++) { + 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 { + 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) { + } + catch(NoSuchAlgorithmException e) + { throw new RuntimeException(e); } } - void sendFrame(final byte[] frame) { - mHandler.post(new Runnable() { + void sendFrame(final byte[] frame) + { + mHandler.post(new Runnable() + { @Override - public void run() { - try { - synchronized (mSendLock) { - if (mSocket == null) { + 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) { + } + 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); + public 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 { + private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException + { SSLContext context = SSLContext.getInstance("TLS"); context.init(null, sTrustManagers, null); return context.getSocketFactory();