diff --git a/.gitignore b/.gitignore index 3f7974706..d7e41aae5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,13 @@ *.class +.git +.gradle +build *.svn *~ target +*.ipr +*.iml +*.iws .settings .project .classpath diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..067c53e42 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: java +jdk: + - oraclejdk7 + +script: "./gradlew test" diff --git a/README.markdown b/README.markdown index 414ec32b6..0aaab7a2f 100644 --- a/README.markdown +++ b/README.markdown @@ -1,3 +1,4 @@ +[![Build Status](https://travis-ci.org/ck1125/Java-WebSocket.png?branch=master)](https://travis-ci.org/ck1125/Java-WebSocket) Java WebSockets =============== @@ -32,8 +33,14 @@ The ant targets are: ```compile```, ```jar```, ```doc``` and ```clean``` ###Maven -Maven is supported. More documentation in that is yet to come... - +To use maven just add this dependency to your pom.xml: +```xml + + org.java-websocket + Java-WebSocket + 1.3.0 + +``` Running the Examples ------------------- @@ -90,7 +97,7 @@ To see how to use wss please take a look at the examples.
If you do not have a valid **certificate** in place then you will have to create a self signed one. Browsers will simply refuse the connection in case of a bad certificate and will not ask the user to accept it. So the first step will be to make a browser to accept your self signed certificate. ( https://bugzilla.mozilla.org/show_bug.cgi?id=594502 ).
-If the websocket server url is `wss://localhost:8000` visit the url `htts://localhost:8000` with your browser. The browser will recognize the handshake and allow you to accept the certificate. +If the websocket server url is `wss://localhost:8000` visit the url `https://localhost:8000` with your browser. The browser will recognize the handshake and allow you to accept the certificate. This technique is also demonstrated in this [video](http://www.youtube.com/watch?v=F8lBdfAZPkU). The vm option `-Djavax.net.debug=all` can help to find out if there is a problem with the certificate. diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..a87d144f7 --- /dev/null +++ b/build.gradle @@ -0,0 +1,44 @@ +apply plugin: 'java' +apply plugin: 'idea' +apply plugin: 'maven' + + +repositories { + mavenLocal() + mavenCentral() +} + +group = 'org.java_websocket' +version = '1.2.1-SNAPSHOT' +sourceCompatibility = 1.6 +targetCompatibility = 1.6 + +configurations { + deployerJars +} + +configure(install.repositories.mavenInstaller) { + pom.version = project.version + pom.groupId = "org.java_websocket" + pom.artifactId = 'Java-WebSocket' +} + +dependencies { + deployerJars "org.apache.maven.wagon:wagon-webdav:1.0-beta-2" +} + + +//deploy to maven repository +//uploadArchives { +// repositories.mavenDeployer { +// configuration = configurations.deployerJars +// repository(url: repositoryUrl) { +// authentication(userName: repositoryUsername, password: repositoryPassword) +// } +// } +//} + +task wrapper(type: Wrapper) { + gradleVersion = '1.4' +} + diff --git a/dist/java_websocket.jar b/dist/java_websocket.jar index bb5caeb4e..7134f4c11 100644 Binary files a/dist/java_websocket.jar and b/dist/java_websocket.jar differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..cf8ca39e9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..3d698f898 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Feb 13 00:13:35 GMT 2013 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-1.4-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..ae91ed902 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/bin/bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" +APP_HOME="`pwd -P`" +cd "$SAVED" + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + JAVA_OPTS="$JAVA_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..8a0b282aa --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pom.xml b/pom.xml index 2d4f42b32..9189fba8d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,13 +1,23 @@ - + + + org.sonatype.oss + oss-parent + 7 + + + scm:git:git@github.com:TooTallNate/Java-WebSocket.git + scm:git:git@github.com:TooTallNate/Java-WebSocket.git + git@github.com:TooTallNate/Java-WebSocket.git + 4.0.0 - org.java_websocket + org.java-websocket Java-WebSocket - 1.0.0-SNAPSHOT + 1.3.1-SNAPSHOT jar Java WebSocket http://java-websocket.org/ + A barebones WebSocket client and server implementation written in 100% Java UTF-8 1.6 @@ -23,6 +33,87 @@ ${java.version} + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9 + + + attach-javadocs + + jar + + + + - \ No newline at end of file + + + TooTallNate + Nathan Rajlich + nathan@tootallnate.net + https://github.com/TooTallNate + + founder + + + + Davidiusdadi + David Rohmer + rohmer.david@gmail.com + https://github.com/Davidiusdadi + + maintainer + + + + + + MIT License + http://github.com/TooTallNate/Java-WebSocket/blob/master/LICENSE + + + + + release-sign-artifacts + + performReleasetrue + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.1 + + + sign-artifacts + verify + + sign + + + + + rohmer.david@gmail.com + + + + + + + diff --git a/src/main/example/ChatClient.java b/src/main/example/ChatClient.java index ee2bdbe24..96a19100d 100644 --- a/src/main/example/ChatClient.java +++ b/src/main/example/ChatClient.java @@ -13,7 +13,7 @@ import javax.swing.JTextArea; import javax.swing.JTextField; -import org.java_websocket.WebSocket; +import org.java_websocket.WebSocketImpl; import org.java_websocket.client.WebSocketClient; import org.java_websocket.drafts.Draft; import org.java_websocket.drafts.Draft_10; @@ -148,7 +148,7 @@ public void onError( Exception ex ) { } public static void main( String[] args ) { - WebSocket.DEBUG = true; + WebSocketImpl.DEBUG = true; String location; if( args.length != 0 ) { location = args[ 0 ]; diff --git a/src/main/example/ChatServer.java b/src/main/example/ChatServer.java index efe6dd2d8..a27cb0ee0 100644 --- a/src/main/example/ChatServer.java +++ b/src/main/example/ChatServer.java @@ -5,8 +5,9 @@ import java.net.UnknownHostException; import java.util.Collection; -import org.java_websocket.IWebSocket; import org.java_websocket.WebSocket; +import org.java_websocket.WebSocketImpl; +import org.java_websocket.framing.Framedata; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; @@ -24,25 +25,30 @@ public ChatServer( InetSocketAddress address ) { } @Override - public void onOpen( IWebSocket conn, ClientHandshake handshake ) { + public void onOpen( WebSocket conn, ClientHandshake handshake ) { this.sendToAll( "new connection: " + handshake.getResourceDescriptor() ); System.out.println( conn.getRemoteSocketAddress().getAddress().getHostAddress() + " entered the room!" ); } @Override - public void onClose( IWebSocket conn, int code, String reason, boolean remote ) { + public void onClose( WebSocket conn, int code, String reason, boolean remote ) { this.sendToAll( conn + " has left the room!" ); System.out.println( conn + " has left the room!" ); } @Override - public void onMessage( IWebSocket conn, String message ) { + public void onMessage( WebSocket conn, String message ) { this.sendToAll( message ); System.out.println( conn + ": " + message ); } + @Override + public void onFragment( WebSocket conn, Framedata fragment ) { + System.out.println( "received fragment: " + fragment ); + } + public static void main( String[] args ) throws InterruptedException , IOException { - WebSocket.DEBUG = true; + WebSocketImpl.DEBUG = true; int port = 8887; // 843 flash policy port try { port = Integer.parseInt( args[ 0 ] ); @@ -56,11 +62,18 @@ public static void main( String[] args ) throws InterruptedException , IOExcepti while ( true ) { String in = sysin.readLine(); s.sendToAll( in ); + if( in.equals( "exit" ) ) { + s.stop(); + break; + } else if( in.equals( "restart" ) ) { + s.stop(); + s.start(); + break; + } } } - @Override - public void onError( IWebSocket conn, Exception ex ) { + public void onError( WebSocket conn, Exception ex ) { ex.printStackTrace(); if( conn != null ) { // some errors like port binding failed may not be assignable to a specific websocket @@ -76,9 +89,9 @@ public void onError( IWebSocket conn, Exception ex ) { * When socket related I/O errors occur. */ public void sendToAll( String text ) { - Collection con = connections(); + Collection con = connections(); synchronized ( con ) { - for( IWebSocket c : con ) { + for( WebSocket c : con ) { c.send( text ); } } diff --git a/src/main/example/ExampleClient.java b/src/main/example/ExampleClient.java index 531784528..3cf68377e 100644 --- a/src/main/example/ExampleClient.java +++ b/src/main/example/ExampleClient.java @@ -4,6 +4,7 @@ import org.java_websocket.client.WebSocketClient; import org.java_websocket.drafts.Draft; import org.java_websocket.drafts.Draft_10; +import org.java_websocket.framing.Framedata; import org.java_websocket.handshake.ServerHandshake; /** This example demonstrates how to create a websocket connection to a server. Only the most important callbacks are overloaded. */ @@ -20,13 +21,17 @@ public ExampleClient( URI serverURI ) { @Override public void onOpen( ServerHandshake handshakedata ) { System.out.println( "opened connection" ); - // if you pan to refuse connection based on ip or httpfields overload: onWebsocketHandshakeReceivedAsClient + // if you plan to refuse connection based on ip or httpfields overload: onWebsocketHandshakeReceivedAsClient } @Override public void onMessage( String message ) { System.out.println( "received: " + message ); - // send( "you said: " + message ); + } + + @Override + public void onFragment( Framedata fragment ) { + System.out.println( "received fragment: " + new String( fragment.getPayloadData().array() ) ); } @Override diff --git a/src/main/example/FragmentedFramesExample.java b/src/main/example/FragmentedFramesExample.java new file mode 100644 index 000000000..b5a03e1aa --- /dev/null +++ b/src/main/example/FragmentedFramesExample.java @@ -0,0 +1,57 @@ +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; + +import org.java_websocket.WebSocket; +import org.java_websocket.client.WebSocketClient; +import org.java_websocket.drafts.Draft_17; +import org.java_websocket.framing.Framedata.Opcode; + +/** + * This example shows how to send fragmented frames.
+ * For information on when to used fragmented frames see http://tools.ietf.org/html/rfc6455#section-5.4
+ * Fragmented and normal messages can not be mixed. + * One is however allowed to mix them with control messages like ping/pong. + * + * @see WebSocket#sendFragmentedFrame(Opcode, ByteBuffer, boolean) + **/ +public class FragmentedFramesExample { + public static void main( String[] args ) throws URISyntaxException , IOException , InterruptedException { + // WebSocketImpl.DEBUG = true; // will give extra output + + WebSocketClient websocket = new ExampleClient( new URI( "ws://localhost:8887" ), new Draft_17() ); // Draft_17 is implementation of rfc6455 + if( !websocket.connectBlocking() ) { + System.err.println( "Could not connect to the server." ); + return; + } + + System.out.println( "This example shows how to send fragmented(continuous) messages." ); + + BufferedReader stdin = new BufferedReader( new InputStreamReader( System.in ) ); + while ( websocket.isOpen() ) { + System.out.println( "Please type in a loooooong line(which then will be send in 2 byte fragments):" ); + String longline = stdin.readLine(); + ByteBuffer longelinebuffer = ByteBuffer.wrap( longline.getBytes() ); + longelinebuffer.rewind(); + + for( int position = 2 ; ; position += 2 ) { + if( position < longelinebuffer.capacity() ) { + longelinebuffer.limit( position ); + websocket.sendFragmentedFrame( Opcode.TEXT, longelinebuffer, false );// when sending binary data one should use Opcode.BINARY + assert ( longelinebuffer.remaining() == 0 ); + // after calling sendFragmentedFrame one may reuse the buffer given to the method immediately + } else { + longelinebuffer.limit( longelinebuffer.capacity() ); + websocket.sendFragmentedFrame( Opcode.TEXT, longelinebuffer, true );// sending the last frame + break; + } + + } + System.out.println( "You can not type in the next long message or press Ctr-C to exit." ); + } + System.out.println( "FragmentedFramesExample terminated" ); + } +} diff --git a/src/main/example/ProxyClientExample.java b/src/main/example/ProxyClientExample.java new file mode 100644 index 000000000..ddff0ea0d --- /dev/null +++ b/src/main/example/ProxyClientExample.java @@ -0,0 +1,12 @@ +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URI; +import java.net.URISyntaxException; + +public class ProxyClientExample { + public static void main( String[] args ) throws URISyntaxException { + ExampleClient c = new ExampleClient( new URI( "ws://echo.websocket.org" ) ); + c.setProxy( new Proxy( Proxy.Type.HTTP, new InetSocketAddress( "proxyaddress", 80 ) ) ); + c.connect(); + } +} diff --git a/src/main/example/SSLClientExample.java b/src/main/example/SSLClientExample.java index 80ec555fa..e740c9c54 100644 --- a/src/main/example/SSLClientExample.java +++ b/src/main/example/SSLClientExample.java @@ -7,10 +7,10 @@ import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManagerFactory; -import org.java_websocket.WebSocket; -import org.java_websocket.client.DefaultSSLWebSocketClientFactory; +import org.java_websocket.WebSocketImpl; import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; @@ -55,7 +55,7 @@ public class SSLClientExample { *keytool -genkey -validity 3650 -keystore "keystore.jks" -storepass "storepassword" -keypass "keypassword" -alias "default" -dname "CN=127.0.0.1, OU=MyOrgUnit, O=MyOrg, L=MyCity, S=MyRegion, C=MyCountry" */ public static void main( String[] args ) throws Exception { - WebSocket.DEBUG = true; + WebSocketImpl.DEBUG = true; WebSocketChatClient chatclient = new WebSocketChatClient( new URI( "wss://localhost:8887" ) ); @@ -79,7 +79,9 @@ public static void main( String[] args ) throws Exception { sslContext.init( kmf.getKeyManagers(), tmf.getTrustManagers(), null ); // sslContext.init( null, null, null ); // will use java's default key and trust store which is sufficient unless you deal with self-signed certificates - chatclient.setWebSocketFactory( new DefaultSSLWebSocketClientFactory( sslContext ) ); + SSLSocketFactory factory = sslContext.getSocketFactory();// (SSLSocketFactory) SSLSocketFactory.getDefault(); + + chatclient.setSocket( factory.createSocket() ); chatclient.connectBlocking(); diff --git a/src/main/example/SSLServerExample.java b/src/main/example/SSLServerExample.java index 9ffb6ce21..563395403 100644 --- a/src/main/example/SSLServerExample.java +++ b/src/main/example/SSLServerExample.java @@ -8,7 +8,7 @@ import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; -import org.java_websocket.WebSocket; +import org.java_websocket.WebSocketImpl; import org.java_websocket.server.DefaultSSLWebSocketServerFactory; public class SSLServerExample { @@ -19,7 +19,7 @@ public class SSLServerExample { *keytool -genkey -validity 3650 -keystore "keystore.jks" -storepass "storepassword" -keypass "keypassword" -alias "default" -dname "CN=127.0.0.1, OU=MyOrgUnit, O=MyOrg, L=MyCity, S=MyRegion, C=MyCountry" */ public static void main( String[] args ) throws Exception { - WebSocket.DEBUG = true; + WebSocketImpl.DEBUG = true; ChatServer chatserver = new ChatServer( 8887 ); // Firefox does allow multible ssl connection only via port 443 //tested on FF16 diff --git a/src/main/java/org/java_websocket/AbstractWrappedByteChannel.java b/src/main/java/org/java_websocket/AbstractWrappedByteChannel.java new file mode 100644 index 000000000..0481a6d4a --- /dev/null +++ b/src/main/java/org/java_websocket/AbstractWrappedByteChannel.java @@ -0,0 +1,75 @@ +package org.java_websocket; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.SocketChannel; + +import javax.net.ssl.SSLException; + + +public class AbstractWrappedByteChannel implements WrappedByteChannel { + + private final ByteChannel channel; + + public AbstractWrappedByteChannel( ByteChannel towrap ) { + this.channel = towrap; + } + + public AbstractWrappedByteChannel( WrappedByteChannel towrap ) { + this.channel = towrap; + } + + @Override + public int read( ByteBuffer dst ) throws IOException { + return channel.read( dst ); + } + + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + @Override + public void close() throws IOException { + channel.close(); + } + + @Override + public int write( ByteBuffer src ) throws IOException { + return channel.write( src ); + } + + @Override + public boolean isNeedWrite() { + return channel instanceof WrappedByteChannel ? ( (WrappedByteChannel) channel ).isNeedWrite() : false; + } + + @Override + public void writeMore() throws IOException { + if( channel instanceof WrappedByteChannel ) + ( (WrappedByteChannel) channel ).writeMore(); + + } + + @Override + public boolean isNeedRead() { + return channel instanceof WrappedByteChannel ? ( (WrappedByteChannel) channel ).isNeedRead() : false; + + } + + @Override + public int readMore( ByteBuffer dst ) throws SSLException { + return channel instanceof WrappedByteChannel ? ( (WrappedByteChannel) channel ).readMore( dst ) : 0; + } + + @Override + public boolean isBlocking() { + if( channel instanceof SocketChannel ) + return ( (SocketChannel) channel ).isBlocking(); + else if( channel instanceof WrappedByteChannel ) + return ( (WrappedByteChannel) channel ).isBlocking(); + return false; + } + +} diff --git a/src/main/java/org/java_websocket/IWebSocket.java b/src/main/java/org/java_websocket/IWebSocket.java deleted file mode 100644 index ffceacc14..000000000 --- a/src/main/java/org/java_websocket/IWebSocket.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.java_websocket; - -import java.net.InetSocketAddress; -import java.nio.ByteBuffer; -import java.nio.channels.NotYetConnectedException; - -import org.java_websocket.drafts.Draft; -import org.java_websocket.exceptions.InvalidDataException; -import org.java_websocket.exceptions.InvalidHandshakeException; -import org.java_websocket.framing.Framedata; -import org.java_websocket.handshake.ClientHandshakeBuilder; - -public interface IWebSocket { - - enum Role { - CLIENT, - SERVER - } - - enum READYSTATE { - NOT_YET_CONNECTED, - CONNECTING, - OPEN, - CLOSING, - CLOSED; - } - - static int RCVBUF = 65536; - - /** - * The default port of WebSockets, as defined in the spec. If the nullary - * constructor is used, DEFAULT_PORT will be the port the WebSocketServer - * is binded to. Note that ports under 1024 usually require root permissions. - */ - int DEFAULT_PORT = 80; - int DEFAULT_WSS_PORT = 443; - - /** - * sends the closing handshake. - * may be send in response to an other handshake. - */ - abstract void close(int code, String message); - - void close(int code); - - /** - * This will close the connection immediately without a proper close handshake. - * The code and the message therefore won't be transfered over the wire also they will be forwarded to onClose/onWebsocketClose. - **/ - void closeConnection(int code, String message); - - void close(InvalidDataException e); - - /** - * Send Text data to the other end. - * - * @throws IllegalArgumentException - * @throws NotYetConnectedException - */ - void send(String text) throws NotYetConnectedException; - - /** - * Send Binary data (plain bytes) to the other end. - * - * @throws IllegalArgumentException - * @throws NotYetConnectedException - */ - void send(ByteBuffer bytes) throws IllegalArgumentException, NotYetConnectedException; - - void send(byte[] bytes) throws IllegalArgumentException, NotYetConnectedException; - - void sendFrame(Framedata framedata); - - boolean hasBufferedData(); - - void startHandshake(ClientHandshakeBuilder handshakedata) throws InvalidHandshakeException; - - InetSocketAddress getRemoteSocketAddress(); - - InetSocketAddress getLocalSocketAddress(); - - boolean isConnecting(); - - boolean isOpen(); - - boolean isClosing(); - - boolean isClosed(); - - Draft getDraft(); - - /** - * Retrieve the WebSocket 'readyState'. - * This represents the state of the connection. - * It returns a numerical value, as per W3C WebSockets specs. - * - * @return Returns '0 = CONNECTING', '1 = OPEN', '2 = CLOSING' or '3 = CLOSED' - */ - READYSTATE getReadyState(); - -} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/SSLSocketChannel2.java b/src/main/java/org/java_websocket/SSLSocketChannel2.java index 4cb3d2712..608cec4d6 100644 --- a/src/main/java/org/java_websocket/SSLSocketChannel2.java +++ b/src/main/java/org/java_websocket/SSLSocketChannel2.java @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -30,6 +31,9 @@ * Implements the relevant portions of the SocketChannel interface with the SSLEngine wrapper. */ public class SSLSocketChannel2 implements ByteChannel, WrappedByteChannel { + /** + * This object is used to feed the {@link SSLEngine}'s wrap and unwrap methods during the handshake phase. + **/ protected static ByteBuffer emptybuffer = ByteBuffer.allocate( 0 ); protected ExecutorService exec; @@ -43,31 +47,67 @@ public class SSLSocketChannel2 implements ByteChannel, WrappedByteChannel { /** encrypted data incoming */ protected ByteBuffer inCrypt; - protected SocketChannel sc; - protected SelectionKey key; + /** the underlying channel */ + protected SocketChannel socketChannel; + /** used to set interestOP SelectionKey.OP_WRITE for the underlying channel */ + protected SelectionKey selectionKey; - protected SSLEngineResult res; protected SSLEngine sslEngine; + protected SSLEngineResult readEngineResult; + protected SSLEngineResult writeEngineResult; - public SSLSocketChannel2( SelectionKey key , SSLEngine sslEngine , ExecutorService exec ) throws IOException { - this.sc = (SocketChannel) key.channel(); - this.key = key; - this.sslEngine = sslEngine; - this.exec = exec; + /** + * Should be used to count the buffer allocations. + * But because of #190 where HandshakeStatus.FINISHED is not properly returned by nio wrap/unwrap this variable is used to check whether {@link #createBuffers(SSLSession)} needs to be called. + **/ + protected int bufferallocations = 0; - tasks = new ArrayList>( 3 ); + public SSLSocketChannel2( SocketChannel channel , SSLEngine sslEngine , ExecutorService exec , SelectionKey key ) throws IOException { + if( channel == null || sslEngine == null || exec == null ) + throw new IllegalArgumentException( "parameter must not be null" ); - this.key.interestOps( key.interestOps() | SelectionKey.OP_WRITE ); + this.socketChannel = channel; + this.sslEngine = sslEngine; + this.exec = exec; - sslEngine.setEnableSessionCreation( true ); - SSLSession session = sslEngine.getSession(); - createBuffers( session ); + readEngineResult = writeEngineResult = new SSLEngineResult( Status.BUFFER_UNDERFLOW, sslEngine.getHandshakeStatus(), 0, 0 ); // init to prevent NPEs - sc.write( wrap( emptybuffer ) );// initializes res + tasks = new ArrayList>( 3 ); + if( key != null ) { + key.interestOps( key.interestOps() | SelectionKey.OP_WRITE ); + this.selectionKey = key; + } + createBuffers( sslEngine.getSession() ); + // kick off handshake + socketChannel.write( wrap( emptybuffer ) );// initializes res processHandshake(); } - private void processHandshake() throws IOException { + private void consumeFutureUninterruptible( Future f ) { + try { + boolean interrupted = false; + while ( true ) { + try { + f.get(); + break; + } catch ( InterruptedException e ) { + interrupted = true; + } + } + if( interrupted ) + Thread.currentThread().interrupt(); + } catch ( ExecutionException e ) { + throw new RuntimeException( e ); + } + } + + /** + * This method will do whatever necessary to process the sslengine handshake. + * Thats why it's called both from the {@link #read(ByteBuffer)} and {@link #write(ByteBuffer)} + **/ + private synchronized void processHandshake() throws IOException { + if( sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING ) + return; // since this may be called either from a reading or a writing thread and because this method is synchronized it is necessary to double check if we are still handshaking. if( !tasks.isEmpty() ) { Iterator> it = tasks.iterator(); while ( it.hasNext() ) { @@ -75,42 +115,57 @@ private void processHandshake() throws IOException { if( f.isDone() ) { it.remove(); } else { + if( isBlocking() ) + consumeFutureUninterruptible( f ); return; } } } - if( res.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP ) { - inCrypt.compact(); - int read = sc.read( inCrypt ); - if( read == -1 ) { - throw new IOException( "connection closed unexpectedly by peer" ); + if( sslEngine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP ) { + if( !isBlocking() || readEngineResult.getStatus() == Status.BUFFER_UNDERFLOW ) { + inCrypt.compact(); + int read = socketChannel.read( inCrypt ); + if( read == -1 ) { + throw new IOException( "connection closed unexpectedly by peer" ); + } + inCrypt.flip(); } - inCrypt.flip(); inData.compact(); unwrap(); + if( readEngineResult.getHandshakeStatus() == HandshakeStatus.FINISHED ) { + createBuffers( sslEngine.getSession() ); + return; + } } consumeDelegatedTasks(); - if( tasks.isEmpty() || res.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP ) { - sc.write( wrap( emptybuffer ) ); + if( tasks.isEmpty() || sslEngine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_WRAP ) { + socketChannel.write( wrap( emptybuffer ) ); + if( writeEngineResult.getHandshakeStatus() == HandshakeStatus.FINISHED ) { + createBuffers( sslEngine.getSession() ); + return; + } } + assert ( sslEngine.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING );// this function could only leave NOT_HANDSHAKING after createBuffers was called unless #190 occurs which means that nio wrap/unwrap never return HandshakeStatus.FINISHED + bufferallocations = 1; // look at variable declaration why this line exists and #190. Without this line buffers would not be be recreated when #190 AND a rehandshake occur. } - private synchronized ByteBuffer wrap( ByteBuffer b ) throws SSLException { outCrypt.compact(); - res = sslEngine.wrap( b, outCrypt ); + writeEngineResult = sslEngine.wrap( b, outCrypt ); outCrypt.flip(); return outCrypt; } + /** + * performs the unwrap operation by unwrapping from {@link #inCrypt} to {@link #inData} + **/ private synchronized ByteBuffer unwrap() throws SSLException { int rem; - do{ + do { rem = inData.remaining(); - res = sslEngine.unwrap( inCrypt, inData ); - } while ( rem != inData.remaining() ); - + readEngineResult = sslEngine.unwrap( inCrypt, inData ); + } while ( readEngineResult.getStatus() == SSLEngineResult.Status.OK && ( rem != inData.remaining() || sslEngine.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP ) ); inData.flip(); return inData; } @@ -118,7 +173,12 @@ private synchronized ByteBuffer unwrap() throws SSLException { protected void consumeDelegatedTasks() { Runnable task; while ( ( task = sslEngine.getDelegatedTask() ) != null ) { - tasks.add( exec.submit( task ) ); + // JBW - 19FEB14: Sometimes we get tasks after the exec is shutdown. Causes ScheduledThreadPoolExecutor (and NPE other places). + if ((!exec.isShutdown()) && (!exec.isTerminated())) { + tasks.add( exec.submit( task ) ); + } else { + System.out.println("Exec shutdown in consumerDelegatedTasks"); + } // task.run(); } } @@ -127,12 +187,25 @@ protected void createBuffers( SSLSession session ) { int appBufferMax = session.getApplicationBufferSize(); int netBufferMax = session.getPacketBufferSize(); - inData = ByteBuffer.allocate( appBufferMax ); - outCrypt = ByteBuffer.allocate( netBufferMax ); - inCrypt = ByteBuffer.allocate( netBufferMax ); + if( inData == null ) { + inData = ByteBuffer.allocate( appBufferMax ); + outCrypt = ByteBuffer.allocate( netBufferMax ); + inCrypt = ByteBuffer.allocate( netBufferMax ); + } else { + if( inData.capacity() != appBufferMax ) + inData = ByteBuffer.allocate( appBufferMax ); + if( outCrypt.capacity() != netBufferMax ) + outCrypt = ByteBuffer.allocate( netBufferMax ); + if( inCrypt.capacity() != netBufferMax ) + inCrypt = ByteBuffer.allocate( netBufferMax ); + } + inData.rewind(); inData.flip(); + inCrypt.rewind(); inCrypt.flip(); + outCrypt.rewind(); outCrypt.flip(); + bufferallocations++; } public int write( ByteBuffer src ) throws IOException { @@ -140,21 +213,50 @@ public int write( ByteBuffer src ) throws IOException { processHandshake(); return 0; } - int num = sc.write( wrap( src ) ); + // assert ( bufferallocations > 1 ); //see #190 + if( bufferallocations <= 1 ) { + createBuffers( sslEngine.getSession() ); + } + int num = socketChannel.write( wrap( src ) ); return num; } + /** + * Blocks when in blocking mode until at least one byte has been decoded.
+ * When not in blocking mode 0 may be returned. + * + * @return the number of bytes read. + **/ public int read( ByteBuffer dst ) throws IOException { - if( !isHandShakeComplete() ) { - processHandshake(); + if( !dst.hasRemaining() ) return 0; + if( !isHandShakeComplete() ) { + if( isBlocking() ) { + while ( !isHandShakeComplete() ) { + processHandshake(); + } + } else { + processHandshake(); + if( !isHandShakeComplete() ) { + return 0; + } + } } - + // assert ( bufferallocations > 1 ); //see #190 + if( bufferallocations <= 1 ) { + createBuffers( sslEngine.getSession() ); + } + /* 1. When "dst" is smaller than "inData" readRemaining will fill "dst" with data decoded in a previous read call. + * 2. When "inCrypt" contains more data than "inData" has remaining space, unwrap has to be called on more time(readRemaining) + */ int purged = readRemaining( dst ); if( purged != 0 ) return purged; + /* We only continue when we really need more data from the network. + * Thats the case if inData is empty or inCrypt holds to less data than necessary for decryption + */ assert ( inData.position() == 0 ); inData.clear(); @@ -163,23 +265,28 @@ public int read( ByteBuffer dst ) throws IOException { else inCrypt.compact(); - if( sc.read( inCrypt ) == -1 ) { - return -1; - } + if( isBlocking() || readEngineResult.getStatus() == Status.BUFFER_UNDERFLOW ) + if( socketChannel.read( inCrypt ) == -1 ) { + return -1; + } inCrypt.flip(); unwrap(); - return transfereTo( inData, dst ); + int transfered = transfereTo( inData, dst ); + if( transfered == 0 && isBlocking() ) { + return read( dst ); // "transfered" may be 0 when not enough bytes were received or during rehandshaking + } + return transfered; } - + /** + * {@link #read(ByteBuffer)} may not be to leave all buffers(inData, inCrypt) + **/ private int readRemaining( ByteBuffer dst ) throws SSLException { - assert ( dst.hasRemaining() ); - if( inData.hasRemaining() ) { return transfereTo( inData, dst ); } - assert ( !inData.hasRemaining() ); - inData.clear(); + if( !inData.hasRemaining() ) + inData.clear(); // test if some bytes left from last read (e.g. BUFFER_UNDERFLOW) if( inCrypt.hasRemaining() ) { unwrap(); @@ -191,36 +298,37 @@ private int readRemaining( ByteBuffer dst ) throws SSLException { } public boolean isConnected() { - return sc.isConnected(); + return socketChannel.isConnected(); } public void close() throws IOException { sslEngine.closeOutbound(); sslEngine.getSession().invalidate(); - if( sc.isOpen() ) - sc.write( wrap( emptybuffer ) );// FIXME what if not all bytes can be written - sc.close(); + if( socketChannel.isOpen() ) + socketChannel.write( wrap( emptybuffer ) );// FIXME what if not all bytes can be written + socketChannel.close(); + exec.shutdownNow(); } private boolean isHandShakeComplete() { - HandshakeStatus status = res.getHandshakeStatus(); + HandshakeStatus status = sslEngine.getHandshakeStatus(); return status == SSLEngineResult.HandshakeStatus.FINISHED || status == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING; } public SelectableChannel configureBlocking( boolean b ) throws IOException { - return sc.configureBlocking( b ); + return socketChannel.configureBlocking( b ); } public boolean connect( SocketAddress remote ) throws IOException { - return sc.connect( remote ); + return socketChannel.connect( remote ); } public boolean finishConnect() throws IOException { - return sc.finishConnect(); + return socketChannel.finishConnect(); } public Socket socket() { - return sc.socket(); + return socketChannel.socket(); } public boolean isInboundDone() { @@ -229,12 +337,12 @@ public boolean isInboundDone() { @Override public boolean isOpen() { - return sc.isOpen(); + return socketChannel.isOpen(); } @Override public boolean isNeedWrite() { - return outCrypt.hasRemaining() || !isHandShakeComplete(); + return outCrypt.hasRemaining() || !isHandShakeComplete(); // FIXME this condition can cause high cpu load during handshaking when network is slow } @Override @@ -244,7 +352,7 @@ public void writeMore() throws IOException { @Override public boolean isNeedRead() { - return inData.hasRemaining() || ( inCrypt.hasRemaining() && res.getStatus() != Status.BUFFER_UNDERFLOW ); + return inData.hasRemaining() || ( inCrypt.hasRemaining() && readEngineResult.getStatus() != Status.BUFFER_UNDERFLOW && readEngineResult.getStatus() != Status.CLOSED ); } @Override @@ -269,4 +377,9 @@ private int transfereTo( ByteBuffer from, ByteBuffer to ) { } + @Override + public boolean isBlocking() { + return socketChannel.isBlocking(); + } + } \ No newline at end of file diff --git a/src/main/java/org/java_websocket/SocketChannelIOHelper.java b/src/main/java/org/java_websocket/SocketChannelIOHelper.java index a8dfc8696..5dcb93c5e 100644 --- a/src/main/java/org/java_websocket/SocketChannelIOHelper.java +++ b/src/main/java/org/java_websocket/SocketChannelIOHelper.java @@ -3,6 +3,9 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.ByteChannel; +import java.nio.channels.spi.AbstractSelectableChannel; + +import org.java_websocket.WebSocket.Role; public class SocketChannelIOHelper { @@ -18,6 +21,10 @@ public static boolean read( final ByteBuffer buf, WebSocketImpl ws, ByteChannel return read != 0; } + /** + * @see WrappedByteChannel#readMore(ByteBuffer) + * @return returns whether there is more data left which can be obtained via {@link #readMore(ByteBuffer, WebSocketImpl, WrappedByteChannel)} + **/ public static boolean readMore( final ByteBuffer buf, WebSocketImpl ws, WrappedByteChannel channel ) throws IOException { buf.clear(); int read = channel.readMore( buf ); @@ -35,10 +42,11 @@ public static boolean readMore( final ByteBuffer buf, WebSocketImpl ws, WrappedB /** Returns whether the whole outQueue has been flushed */ public static boolean batch( WebSocketImpl ws, ByteChannel sockchannel ) throws IOException { ByteBuffer buffer = ws.outQueue.peek(); + WrappedByteChannel c = null; if( buffer == null ) { if( sockchannel instanceof WrappedByteChannel ) { - WrappedByteChannel c = (WrappedByteChannel) sockchannel; + c = (WrappedByteChannel) sockchannel; if( c.isNeedWrite() ) { c.writeMore(); } @@ -55,12 +63,11 @@ public static boolean batch( WebSocketImpl ws, ByteChannel sockchannel ) throws } while ( buffer != null ); } - if( ws.outQueue.isEmpty() && ws.isFlushAndClose() ) { + if( ws.outQueue.isEmpty() && ws.isFlushAndClose() && ws.getDraft().getRole() == WebSocket.Role.SERVER ) {// synchronized ( ws ) { ws.closeConnection(); } } - return sockchannel instanceof WrappedByteChannel == true ? !( (WrappedByteChannel) sockchannel ).isNeedWrite() : true; + return c != null ? !( (WrappedByteChannel) sockchannel ).isNeedWrite() : true; } - } diff --git a/src/main/java/org/java_websocket/WebSocket.java b/src/main/java/org/java_websocket/WebSocket.java index ebd67a1f2..a661eddbf 100644 --- a/src/main/java/org/java_websocket/WebSocket.java +++ b/src/main/java/org/java_websocket/WebSocket.java @@ -1,17 +1,21 @@ package org.java_websocket; import java.net.InetSocketAddress; -import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.NotYetConnectedException; import org.java_websocket.drafts.Draft; -import org.java_websocket.exceptions.InvalidDataException; import org.java_websocket.framing.Framedata; +import org.java_websocket.framing.Framedata.Opcode; -public abstract class WebSocket implements IWebSocket { +public interface WebSocket { + public enum Role { + CLIENT, SERVER + } - public static/*final*/boolean DEBUG = false; // must be final in the future in order to take advantage of VM optimization + public enum READYSTATE { + NOT_YET_CONNECTED, CONNECTING, OPEN, CLOSING, CLOSED; + } /** * The default port of WebSockets, as defined in the spec. If the nullary @@ -26,9 +30,12 @@ public abstract class WebSocket implements IWebSocket { * sends the closing handshake. * may be send in response to an other handshake. */ - public abstract void close( int code, String message ); + public void close( int code, String message ); - public abstract void close( int code ); + public void close( int code ); + + /** Convenience function which behaves like close(CloseFrame.NORMAL) */ + public void close(); /** * This will close the connection immediately without a proper close handshake. @@ -36,8 +43,6 @@ public abstract class WebSocket implements IWebSocket { **/ public abstract void closeConnection( int code, String message ); - public abstract void close( InvalidDataException e ); - /** * Send Text data to the other end. * @@ -58,17 +63,30 @@ public abstract class WebSocket implements IWebSocket { public abstract void sendFrame( Framedata framedata ); + /** + * Allows to send continuous/fragmented frames conveniently.
+ * For more into on this frame type see http://tools.ietf.org/html/rfc6455#section-5.4
+ * + * If the first frame you send is also the last then it is not a fragmented frame and will received via onMessage instead of onFragmented even though it was send by this method. + * + * @param op + * This is only important for the first frame in the sequence. Opcode.TEXT, Opcode.BINARY are allowed. + * @param buffer + * The buffer which contains the payload. It may have no bytes remaining. + * @param fin + * true means the current frame is the last in the sequence. + **/ + public abstract void sendFragmentedFrame( Opcode op, ByteBuffer buffer, boolean fin ); + public abstract boolean hasBufferedData(); /** - * @returns null when connections is closed - * @see Socket#getRemoteSocketAddress() + * @returns never returns null */ public abstract InetSocketAddress getRemoteSocketAddress(); /** - * @returns null when connections is closed - * @see Socket#getLocalSocketAddress() + * @returns never returns null */ public abstract InetSocketAddress getLocalSocketAddress(); @@ -97,4 +115,10 @@ public abstract class WebSocket implements IWebSocket { * @return Returns '0 = CONNECTING', '1 = OPEN', '2 = CLOSING' or '3 = CLOSED' */ public abstract READYSTATE getReadyState(); + + /** + * Returns the HTTP Request-URI as defined by http://tools.ietf.org/html/rfc2616#section-5.1.2
+ * If the opening handshake has not yet happened it will return null. + **/ + public abstract String getResourceDescriptor(); } \ No newline at end of file diff --git a/src/main/java/org/java_websocket/WebSocketAdapter.java b/src/main/java/org/java_websocket/WebSocketAdapter.java index 948598c7a..290e1049a 100644 --- a/src/main/java/org/java_websocket/WebSocketAdapter.java +++ b/src/main/java/org/java_websocket/WebSocketAdapter.java @@ -1,7 +1,10 @@ package org.java_websocket; +import java.net.InetSocketAddress; + import org.java_websocket.drafts.Draft; import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.exceptions.InvalidHandshakeException; import org.java_websocket.framing.Framedata; import org.java_websocket.framing.Framedata.Opcode; import org.java_websocket.framing.FramedataImpl1; @@ -21,12 +24,12 @@ public abstract class WebSocketAdapter implements WebSocketListener { * @see org.java_websocket.WebSocketListener#onWebsocketHandshakeReceivedAsServer(WebSocket, Draft, ClientHandshake) */ @Override - public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer( IWebSocket conn, Draft draft, ClientHandshake request ) throws InvalidDataException { + public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer( WebSocket conn, Draft draft, ClientHandshake request ) throws InvalidDataException { return new HandshakeImpl1Server(); } @Override - public void onWebsocketHandshakeReceivedAsClient( IWebSocket conn, ClientHandshake request, ServerHandshake response ) throws InvalidDataException { + public void onWebsocketHandshakeReceivedAsClient( WebSocket conn, ClientHandshake request, ServerHandshake response ) throws InvalidDataException { } /** @@ -35,7 +38,7 @@ public void onWebsocketHandshakeReceivedAsClient( IWebSocket conn, ClientHandsha * @see org.java_websocket.WebSocketListener#onWebsocketHandshakeSentAsClient(WebSocket, ClientHandshake) */ @Override - public void onWebsocketHandshakeSentAsClient( IWebSocket conn, ClientHandshake request ) throws InvalidDataException { + public void onWebsocketHandshakeSentAsClient( WebSocket conn, ClientHandshake request ) throws InvalidDataException { } /** @@ -44,7 +47,7 @@ public void onWebsocketHandshakeSentAsClient( IWebSocket conn, ClientHandshake r * @see org.java_websocket.WebSocketListener#onWebsocketMessageFragment(WebSocket, Framedata) */ @Override - public void onWebsocketMessageFragment( IWebSocket conn, Framedata frame ) { + public void onWebsocketMessageFragment( WebSocket conn, Framedata frame ) { } /** @@ -54,7 +57,7 @@ public void onWebsocketMessageFragment( IWebSocket conn, Framedata frame ) { * @see org.java_websocket.WebSocketListener#onWebsocketPing(WebSocket, Framedata) */ @Override - public void onWebsocketPing( IWebSocket conn, Framedata f ) { + public void onWebsocketPing( WebSocket conn, Framedata f ) { FramedataImpl1 resp = new FramedataImpl1( f ); resp.setOptcode( Opcode.PONG ); conn.sendFrame( resp ); @@ -66,7 +69,7 @@ public void onWebsocketPing( IWebSocket conn, Framedata f ) { * @see @see org.java_websocket.WebSocketListener#onWebsocketPong(WebSocket, Framedata) */ @Override - public void onWebsocketPong( IWebSocket conn, Framedata f ) { + public void onWebsocketPong( WebSocket conn, Framedata f ) { } /** @@ -79,12 +82,23 @@ public void onWebsocketPong( IWebSocket conn, Framedata f ) { * This is specifically implemented for gitime's WebSocket client for Flash: * http://github.com/gimite/web-socket-js * - * @return An XML String that comforms to Flash's security policy. You MUST + * @return An XML String that comforts to Flash's security policy. You MUST * not include the null char at the end, it is appended automatically. + * @throws InvalidDataException thrown when some data that is required to generate the flash-policy like the websocket local port could not be obtained e.g because the websocket is not connected. */ @Override - public String getFlashPolicy( IWebSocket conn ) { - return "\0"; + public String getFlashPolicy( WebSocket conn ) throws InvalidDataException { + InetSocketAddress adr = conn.getLocalSocketAddress(); + if(null == adr){ + throw new InvalidHandshakeException( "socket not bound" ); + } + + StringBuffer sb = new StringBuffer( 90 ); + sb.append( "\0" ); + + return sb.toString(); } } diff --git a/src/main/java/org/java_websocket/WebSocketFactory.java b/src/main/java/org/java_websocket/WebSocketFactory.java index a412a9988..651e97430 100644 --- a/src/main/java/org/java_websocket/WebSocketFactory.java +++ b/src/main/java/org/java_websocket/WebSocketFactory.java @@ -6,7 +6,7 @@ import org.java_websocket.drafts.Draft; public interface WebSocketFactory { - public IWebSocket createWebSocket( WebSocketAdapter a, Draft d, Socket s ); - public IWebSocket createWebSocket( WebSocketAdapter a, List drafts, Socket s ); + public WebSocket createWebSocket( WebSocketAdapter a, Draft d, Socket s ); + public WebSocket createWebSocket( WebSocketAdapter a, List drafts, Socket s ); } diff --git a/src/main/java/org/java_websocket/WebSocketImpl.java b/src/main/java/org/java_websocket/WebSocketImpl.java index 44dabccdd..b7adea100 100644 --- a/src/main/java/org/java_websocket/WebSocketImpl.java +++ b/src/main/java/org/java_websocket/WebSocketImpl.java @@ -42,7 +42,11 @@ * text frames, and receiving frames through an event-based model. * */ -public class WebSocketImpl extends WebSocket { +public class WebSocketImpl implements WebSocket { + + public static int RCVBUF = 16384; + + public static/*final*/boolean DEBUG = false; // must be final in the future in order to take advantage of VM optimization public static final List defaultdraftlist = new ArrayList( 4 ); static { @@ -54,8 +58,6 @@ public class WebSocketImpl extends WebSocket { public SelectionKey key; - /* only used to obtain the socket addresses*/ - public final Socket socket; /** the possibly wrapped channel object whose selection is controlled by {@link #key} */ public ByteChannel channel; /** @@ -91,7 +93,7 @@ public class WebSocketImpl extends WebSocket { private Opcode current_continuous_frame_opcode = null; /** the bytes of an incomplete received handshake */ - private ByteBuffer tmpHandshakeBytes; + private ByteBuffer tmpHandshakeBytes = ByteBuffer.allocate( 0 ); /** stores the handshake sent by this websocket ( Role.CLIENT only ) */ private ClientHandshake handshakerequest = null; @@ -99,12 +101,14 @@ public class WebSocketImpl extends WebSocket { private String closemessage = null; private Integer closecode = null; private Boolean closedremotely = null; + + private String resourceDescriptor = null; /** * crates a websocket with server role */ - public WebSocketImpl( WebSocketListener listener , List drafts , Socket sock ) { - this( listener, (Draft) null, sock ); + public WebSocketImpl( WebSocketListener listener , List drafts ) { + this( listener, (Draft) null ); this.role = Role.SERVER; // draft.copyInstance will be called when the draft is first needed if( drafts == null || drafts.isEmpty() ) { @@ -116,44 +120,62 @@ public WebSocketImpl( WebSocketListener listener , List drafts , Socket s /** * crates a websocket with client role + * + * @param socket + * may be unbound */ - public WebSocketImpl( WebSocketListener listener , Draft draft , Socket sock ) { + public WebSocketImpl( WebSocketListener listener , Draft draft ) { + if( listener == null || ( draft == null && role == Role.SERVER ) )// socket can be null because we want do be able to create the object without already having a bound channel + throw new IllegalArgumentException( "parameters must not be null" ); this.outQueue = new LinkedBlockingQueue(); inQueue = new LinkedBlockingQueue(); this.wsl = listener; this.role = Role.CLIENT; if( draft != null ) this.draft = draft.copyInstance(); - this.socket = sock; + } + + @Deprecated + public WebSocketImpl( WebSocketListener listener , Draft draft , Socket socket ) { + this( listener, draft ); + } + + @Deprecated + public WebSocketImpl( WebSocketListener listener , List drafts , Socket socket ) { + this( listener, drafts ); } /** * */ public void decode( ByteBuffer socketBuffer ) { - if( !socketBuffer.hasRemaining() || flushandclosestate ) - return; + assert ( socketBuffer.hasRemaining() ); if( DEBUG ) System.out.println( "process(" + socketBuffer.remaining() + "): {" + ( socketBuffer.remaining() > 1000 ? "too big to display" : new String( socketBuffer.array(), socketBuffer.position(), socketBuffer.remaining() ) ) + "}" ); - if( readystate == READYSTATE.OPEN ) { - decodeFrames( socketBuffer ); + if( readystate != READYSTATE.NOT_YET_CONNECTED ) { + decodeFrames( socketBuffer );; } else { if( decodeHandshake( socketBuffer ) ) { - decodeFrames( socketBuffer ); + assert ( tmpHandshakeBytes.hasRemaining() != socketBuffer.hasRemaining() || !socketBuffer.hasRemaining() ); // the buffers will never have remaining bytes at the same time + + if( socketBuffer.hasRemaining() ) { + decodeFrames( socketBuffer ); + } else if( tmpHandshakeBytes.hasRemaining() ) { + decodeFrames( tmpHandshakeBytes ); + } } } assert ( isClosing() || isFlushAndClose() || !socketBuffer.hasRemaining() ); } - /** * Returns whether the handshake phase has is completed. * In case of a broken handshake this will be never the case. **/ private boolean decodeHandshake( ByteBuffer socketBufferNew ) { ByteBuffer socketBuffer; - if( tmpHandshakeBytes == null ) { + if( tmpHandshakeBytes.capacity() == 0 ) { socketBuffer = socketBufferNew; } else { if( tmpHandshakeBytes.remaining() < socketBufferNew.remaining() ) { @@ -172,8 +194,12 @@ private boolean decodeHandshake( ByteBuffer socketBufferNew ) { if( draft == null ) { HandshakeState isflashedgecase = isFlashEdgeCase( socketBuffer ); if( isflashedgecase == HandshakeState.MATCHED ) { - write( ByteBuffer.wrap( Charsetfunctions.utf8Bytes( wsl.getFlashPolicy( this ) ) ) ); - close( CloseFrame.FLASHPOLICY, "" ); + try { + write( ByteBuffer.wrap( Charsetfunctions.utf8Bytes( wsl.getFlashPolicy( this ) ) ) ); + close( CloseFrame.FLASHPOLICY, "" ); + } catch ( InvalidDataException e ) { + close( CloseFrame.ABNORMAL_CLOSE, "remote peer closed connection before flashpolicy could be transmitted", true ); + } return false; } } @@ -195,6 +221,7 @@ private boolean decodeHandshake( ByteBuffer socketBufferNew ) { ClientHandshake handshake = (ClientHandshake) tmphandshake; handshakestate = d.acceptHandshakeAsServer( handshake ); if( handshakestate == HandshakeState.MATCHED ) { + resourceDescriptor = handshake.getResourceDescriptor(); ServerHandshakeBuilder response; try { response = wsl.onWebsocketHandshakeReceivedAsServer( this, d, handshake ); @@ -241,7 +268,7 @@ private boolean decodeHandshake( ByteBuffer socketBufferNew ) { draft.setParseMode( role ); Handshakedata tmphandshake = draft.translateHandshake( socketBuffer ); if( tmphandshake instanceof ServerHandshake == false ) { - flushAndClose( CloseFrame.PROTOCOL_ERROR, "Wwrong http function", false ); + flushAndClose( CloseFrame.PROTOCOL_ERROR, "wrong http function", false ); return false; } ServerHandshake handshake = (ServerHandshake) tmphandshake; @@ -267,7 +294,7 @@ private boolean decodeHandshake( ByteBuffer socketBufferNew ) { close( e ); } } catch ( IncompleteHandshakeException e ) { - if( tmpHandshakeBytes == null ) { + if( tmpHandshakeBytes.capacity() == 0 ) { socketBuffer.reset(); int newsize = e.getPreferedSize(); if( newsize == 0 ) { @@ -288,8 +315,6 @@ private boolean decodeHandshake( ByteBuffer socketBufferNew ) { } private void decodeFrames( ByteBuffer socketBuffer ) { - if( flushandclosestate ) - return; List frames; try { @@ -297,8 +322,6 @@ private void decodeFrames( ByteBuffer socketBuffer ) { for( Framedata f : frames ) { if( DEBUG ) System.out.println( "matched frame: " + f ); - if( flushandclosestate ) - return; Opcode curop = f.getOpcode(); boolean fin = f.isFin(); @@ -371,6 +394,8 @@ private void decodeFrames( ByteBuffer socketBuffer ) { } private void close( int code, String message, boolean remote ) { + if( DEBUG ) + System.out.println( "close: code: " + code + " message: " + message); if( readystate != READYSTATE.CLOSING && readystate != READYSTATE.CLOSED ) { if( readystate == READYSTATE.OPEN ) { if( code == CloseFrame.ABNORMAL_CLOSE ) { @@ -411,6 +436,8 @@ private void close( int code, String message, boolean remote ) { @Override public void close( int code, String message ) { + if( DEBUG ) + System.out.println( "close: code: " + code + " message: " + message); close( code, message, false ); } @@ -424,6 +451,8 @@ public void close( int code, String message ) { **/ protected synchronized void closeConnection( int code, String message, boolean remote ) { + if( DEBUG ) + System.out.println( "close connection: code: " + code + " message: " + message + " remote: " + remote); if( readystate == READYSTATE.CLOSED ) { return; } @@ -431,6 +460,8 @@ protected synchronized void closeConnection( int code, String message, boolean r if( key != null ) { // key.attach( null ); //see issue #114 key.cancel(); + } + if( channel != null ) { try { channel.close(); } catch ( IOException e ) { @@ -447,7 +478,7 @@ protected synchronized void closeConnection( int code, String message, boolean r handshakerequest = null; readystate = READYSTATE.CLOSED; - + this.outQueue.clear(); } protected void closeConnection( int code, boolean remote ) { @@ -489,9 +520,8 @@ protected synchronized void flushAndClose( int code, String message, boolean rem public void eot() { if( getReadyState() == READYSTATE.NOT_YET_CONNECTED ) { closeConnection( CloseFrame.NEVER_CONNECTED, true ); - } - if( draft == null ) { - closeConnection( CloseFrame.ABNORMAL_CLOSE, true ); + } else if( flushandclosestate ) { + closeConnection( closecode, closemessage, closedremotely ); } else if( draft.getCloseHandshakeType() == CloseHandshakeType.NONE ) { closeConnection( CloseFrame.NORMAL, true ); } else if( draft.getCloseHandshakeType() == CloseHandshakeType.ONEWAY ) { @@ -509,7 +539,6 @@ public void close( int code ) { close( code, "", false ); } - @Override public void close( InvalidDataException e ) { close( e.getCloseCode(), e.getMessage(), false ); } @@ -553,6 +582,11 @@ private void send( Collection frames ) { } } + @Override + public void sendFragmentedFrame( Opcode op, ByteBuffer buffer, boolean fin ) { + send( draft.continuousFrame( op, buffer, fin ) ); + } + @Override public void sendFrame( Framedata framedata ) { if( DEBUG ) @@ -589,6 +623,9 @@ public void startHandshake( ClientHandshakeBuilder handshakedata ) throws Invali // Store the Handshake Request we are about to send this.handshakerequest = draft.postProcessHandshakeRequestAsClient( handshakedata ); + resourceDescriptor = handshakedata.getResourceDescriptor(); + assert( resourceDescriptor != null ); + // Notify Listener try { wsl.onWebsocketHandshakeSentAsClient( this, this.handshakerequest ); @@ -680,12 +717,12 @@ public String toString() { @Override public InetSocketAddress getRemoteSocketAddress() { - return (InetSocketAddress) socket.getRemoteSocketAddress(); + return wsl.getRemoteSocketAddress( this ); } @Override public InetSocketAddress getLocalSocketAddress() { - return (InetSocketAddress) socket.getLocalSocketAddress(); + return wsl.getLocalSocketAddress( this ); } @Override @@ -693,4 +730,16 @@ public Draft getDraft() { return draft; } + @Override + public void close() { + if( DEBUG ) + System.out.println( "close: CloseFrame.NORMAL" ); + close( CloseFrame.NORMAL ); + } + + @Override + public String getResourceDescriptor() { + return resourceDescriptor; + } + } diff --git a/src/main/java/org/java_websocket/WebSocketListener.java b/src/main/java/org/java_websocket/WebSocketListener.java index 91dafa448..93478d940 100644 --- a/src/main/java/org/java_websocket/WebSocketListener.java +++ b/src/main/java/org/java_websocket/WebSocketListener.java @@ -1,5 +1,6 @@ package org.java_websocket; +import java.net.InetSocketAddress; import java.nio.ByteBuffer; import org.java_websocket.drafts.Draft; @@ -32,7 +33,7 @@ public interface WebSocketListener { * @throws InvalidDataException * Throwing this exception will cause this handshake to be rejected */ - public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer( IWebSocket conn, Draft draft, ClientHandshake request ) throws InvalidDataException; + public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer( WebSocket conn, Draft draft, ClientHandshake request ) throws InvalidDataException; /** * Called on the client side when the socket connection is first established, and the WebSocketImpl @@ -47,7 +48,7 @@ public interface WebSocketListener { * @throws InvalidDataException * Allows the client to reject the connection with the server in respect of its handshake response. */ - public void onWebsocketHandshakeReceivedAsClient( IWebSocket conn, ClientHandshake request, ServerHandshake response ) throws InvalidDataException; + public void onWebsocketHandshakeReceivedAsClient( WebSocket conn, ClientHandshake request, ServerHandshake response ) throws InvalidDataException; /** * Called on the client side when the socket connection is first established, and the WebSocketImpl @@ -60,7 +61,7 @@ public interface WebSocketListener { * @throws InvalidDataException * Allows the client to stop the connection from progressing */ - public void onWebsocketHandshakeSentAsClient( IWebSocket conn, ClientHandshake request ) throws InvalidDataException; + public void onWebsocketHandshakeSentAsClient( WebSocket conn, ClientHandshake request ) throws InvalidDataException; /** * Called when an entire text frame has been received. Do whatever you want @@ -71,7 +72,7 @@ public interface WebSocketListener { * @param message * The UTF-8 decoded message that was received. */ - public void onWebsocketMessage( IWebSocket conn, String message ); + public void onWebsocketMessage( WebSocket conn, String message ); /** * Called when an entire binary frame has been received. Do whatever you want @@ -82,9 +83,9 @@ public interface WebSocketListener { * @param blob * The binary message that was received. */ - public void onWebsocketMessage( IWebSocket conn, ByteBuffer blob ); + public void onWebsocketMessage( WebSocket conn, ByteBuffer blob ); - public void onWebsocketMessageFragment( IWebSocket conn, Framedata frame ); + public void onWebsocketMessageFragment( WebSocket conn, Framedata frame ); /** * Called after onHandshakeReceived returns true. @@ -94,7 +95,7 @@ public interface WebSocketListener { * @param conn * The WebSocket instance this event is occuring on. */ - public void onWebsocketOpen( IWebSocket conn, Handshakedata d ); + public void onWebsocketOpen( WebSocket conn, Handshakedata d ); /** * Called after WebSocket#close is explicity called, or when the @@ -103,13 +104,13 @@ public interface WebSocketListener { * @param conn * The WebSocket instance this event is occuring on. */ - public void onWebsocketClose( IWebSocket ws, int code, String reason, boolean remote ); + public void onWebsocketClose( WebSocket ws, int code, String reason, boolean remote ); /** called as soon as no further frames are accepted */ - public void onWebsocketClosing( IWebSocket ws, int code, String reason, boolean remote ); + public void onWebsocketClosing( WebSocket ws, int code, String reason, boolean remote ); /** send when this peer sends a close handshake */ - public void onWebsocketCloseInitiated( IWebSocket ws, int code, String reason ); + public void onWebsocketCloseInitiated( WebSocket ws, int code, String reason ); /** * Called if an exception worth noting occurred. @@ -119,7 +120,7 @@ public interface WebSocketListener { * The exception that occurred.
* Might be null if the exception is not related to any specific connection. For example if the server port could not be bound. */ - public void onWebsocketError( IWebSocket conn, Exception ex ); + public void onWebsocketError( WebSocket conn, Exception ex ); /** * Called a ping frame has been received. @@ -128,19 +129,23 @@ public interface WebSocketListener { * @param f * The ping frame. Control frames may contain payload. */ - public void onWebsocketPing( IWebSocket conn, Framedata f ); + public void onWebsocketPing( WebSocket conn, Framedata f ); /** * Called when a pong frame is received. **/ - public void onWebsocketPong( IWebSocket conn, Framedata f ); + public void onWebsocketPong( WebSocket conn, Framedata f ); /** * Gets the XML string that should be returned if a client requests a Flash * security policy. + * @throws InvalidDataException thrown when some data that is required to generate the flash-policy like the websocket local port could not be obtained. */ - public String getFlashPolicy( IWebSocket conn ); + public String getFlashPolicy( WebSocket conn ) throws InvalidDataException; /** This method is used to inform the selector thread that there is data queued to be written to the socket. */ - public void onWriteDemand( IWebSocket conn ); + public void onWriteDemand( WebSocket conn ); + + public InetSocketAddress getLocalSocketAddress( WebSocket conn ); + public InetSocketAddress getRemoteSocketAddress( WebSocket conn ); } diff --git a/src/main/java/org/java_websocket/WrappedByteChannel.java b/src/main/java/org/java_websocket/WrappedByteChannel.java index b7e110fa8..83a3290b3 100644 --- a/src/main/java/org/java_websocket/WrappedByteChannel.java +++ b/src/main/java/org/java_websocket/WrappedByteChannel.java @@ -10,6 +10,17 @@ public interface WrappedByteChannel extends ByteChannel { public boolean isNeedWrite(); public void writeMore() throws IOException; + /** + * returns whether readMore should be called to fetch data which has been decoded but not yet been returned. + * + * @see #read(ByteBuffer) + * @see #readMore(ByteBuffer) + **/ public boolean isNeedRead(); + /** + * This function does not read data from the underlying channel at all. It is just a way to fetch data which has already be received or decoded but was but was not yet returned to the user. + * This could be the case when the decoded data did not fit into the buffer the user passed to {@link #read(ByteBuffer)}. + **/ public int readMore( ByteBuffer dst ) throws SSLException; + public boolean isBlocking(); } diff --git a/src/main/java/org/java_websocket/client/AbstractClientProxyChannel.java b/src/main/java/org/java_websocket/client/AbstractClientProxyChannel.java new file mode 100644 index 000000000..bbac67258 --- /dev/null +++ b/src/main/java/org/java_websocket/client/AbstractClientProxyChannel.java @@ -0,0 +1,38 @@ +package org.java_websocket.client; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; + +import org.java_websocket.AbstractWrappedByteChannel; + +public abstract class AbstractClientProxyChannel extends AbstractWrappedByteChannel { + protected final ByteBuffer proxyHandshake; + + + /** + * @param towrap + * The channel to the proxy server + **/ + public AbstractClientProxyChannel( ByteChannel towrap ) { + super( towrap ); + try { + proxyHandshake = ByteBuffer.wrap( buildHandShake().getBytes( "ASCII" ) ); + } catch ( UnsupportedEncodingException e ) { + throw new RuntimeException( e ); + } + } + + @Override + public int write( ByteBuffer src ) throws IOException { + if( !proxyHandshake.hasRemaining() ) { + return super.write( src ); + } else { + return super.write( proxyHandshake ); + } + } + + public abstract String buildHandShake(); + +} diff --git a/src/main/java/org/java_websocket/client/DefaultSSLWebSocketClientFactory.java b/src/main/java/org/java_websocket/client/DefaultSSLWebSocketClientFactory.java deleted file mode 100644 index 011ce655e..000000000 --- a/src/main/java/org/java_websocket/client/DefaultSSLWebSocketClientFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.java_websocket.client; -import java.io.IOException; -import java.net.Socket; -import java.nio.channels.ByteChannel; -import java.nio.channels.SelectionKey; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLEngine; - -import org.java_websocket.SSLSocketChannel2; -import org.java_websocket.WebSocketAdapter; -import org.java_websocket.WebSocketImpl; -import org.java_websocket.client.WebSocketClient.WebSocketClientFactory; -import org.java_websocket.drafts.Draft; - - -public class DefaultSSLWebSocketClientFactory implements WebSocketClientFactory { - protected SSLContext sslcontext; - protected ExecutorService exec; - - public DefaultSSLWebSocketClientFactory( SSLContext sslContext ) { - this( sslContext, Executors.newSingleThreadScheduledExecutor() ); - } - - public DefaultSSLWebSocketClientFactory( SSLContext sslContext , ExecutorService exec ) { - if( sslContext == null || exec == null ) - throw new IllegalArgumentException(); - this.sslcontext = sslContext; - this.exec = exec; - } - - @Override - public ByteChannel wrapChannel( SelectionKey c, String host, int port ) throws IOException { - SSLEngine e = sslcontext.createSSLEngine( host, port ); - e.setUseClientMode( true ); - return new SSLSocketChannel2( c, e, exec ); - } - - @Override - public WebSocketImpl createWebSocket( WebSocketAdapter a, Draft d, Socket c ) { - return new WebSocketImpl( a, d, c ); - } - - @Override - public WebSocketImpl createWebSocket( WebSocketAdapter a, List d, Socket s ) { - return new WebSocketImpl( a, d, s ); - } -} \ No newline at end of file diff --git a/src/main/java/org/java_websocket/client/WebSocketClient.java b/src/main/java/org/java_websocket/client/WebSocketClient.java index dda308849..2475ffece 100644 --- a/src/main/java/org/java_websocket/client/WebSocketClient.java +++ b/src/main/java/org/java_websocket/client/WebSocketClient.java @@ -1,71 +1,57 @@ package org.java_websocket.client; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.InetSocketAddress; +import java.net.Proxy; import java.net.Socket; import java.net.URI; import java.nio.ByteBuffer; import java.nio.channels.ByteChannel; -import java.nio.channels.CancelledKeyException; -import java.nio.channels.ClosedByInterruptException; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SelectionKey; -import java.nio.channels.Selector; import java.nio.channels.SocketChannel; -import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.CountDownLatch; -import org.java_websocket.IWebSocket; -import org.java_websocket.IWebSocket.READYSTATE; -import org.java_websocket.SocketChannelIOHelper; import org.java_websocket.WebSocket; import org.java_websocket.WebSocketAdapter; import org.java_websocket.WebSocketFactory; import org.java_websocket.WebSocketImpl; -import org.java_websocket.WrappedByteChannel; import org.java_websocket.drafts.Draft; -import org.java_websocket.drafts.Draft_10; +import org.java_websocket.drafts.Draft_17; import org.java_websocket.exceptions.InvalidHandshakeException; import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.Framedata; +import org.java_websocket.framing.Framedata.Opcode; import org.java_websocket.handshake.HandshakeImpl1Client; import org.java_websocket.handshake.Handshakedata; import org.java_websocket.handshake.ServerHandshake; /** - * The WebSocketClient is an abstract class that expects a valid - * "ws://" URI to connect to. When connected, an instance recieves important - * events related to the life of the connection. A subclass must implement - * onOpen, onClose, and onMessage to be - * useful. An instance can send messages to it's connected server via the - * send method. - * - * @author Nathan Rajlich + * A subclass must implement at least onOpen, onClose, and onMessage to be + * useful. At runtime the user is expected to establish a connection via {@link #connect()}, then receive events like {@link #onMessage(String)} via the overloaded methods and to {@link #send(String)} data to the server. */ -public abstract class WebSocketClient extends WebSocketAdapter implements Runnable { +public abstract class WebSocketClient extends WebSocketAdapter implements Runnable, WebSocket { /** * The URI this channel is supposed to connect to. */ - private URI uri = null; + protected URI uri = null; - private WebSocketImpl conn = null; - /** - * The SocketChannel instance this channel uses. - */ - private SocketChannel channel = null; + private WebSocketImpl engine = null; - private ByteChannel wrappedchannel = null; + private Socket socket = null; - private SelectionKey key = null; - /** - * The 'Selector' used to get event keys from the underlying socket. - */ - private Selector selector = null; + private InputStream istream; - private Thread thread; + private OutputStream ostream; + + private Proxy proxy = Proxy.NO_PROXY; + + private Thread writeThread; private Draft draft; @@ -77,88 +63,77 @@ public abstract class WebSocketClient extends WebSocketAdapter implements Runnab private int timeout = 0; - WebSocketClientFactory wf = new WebSocketClientFactory() { - @Override - public IWebSocket createWebSocket( WebSocketAdapter a, Draft d, Socket s ) { - return new WebSocketImpl( WebSocketClient.this, d, s ); - } - - @Override - public IWebSocket createWebSocket( WebSocketAdapter a, List d, Socket s ) { - return new WebSocketImpl( WebSocketClient.this, d, s ); - } - - @Override - public ByteChannel wrapChannel( SelectionKey c, String host, int port ) { - return (ByteChannel) c.channel(); - } - }; + private int connectTimeout = 0; + /** This open a websocket connection as specified by rfc6455 */ public WebSocketClient( URI serverURI ) { - this( serverURI, new Draft_10() ); + this( serverURI, new Draft_17() ); } /** * Constructs a WebSocketClient instance and sets it to the connect to the - * specified URI. The channel does not attampt to connect automatically. You - * must call connect first to initiate the socket connection. + * specified URI. The channel does not attampt to connect automatically. The connection + * will be established once you call connect. */ public WebSocketClient( URI serverUri , Draft draft ) { this( serverUri, draft, null, 0 ); } - public WebSocketClient( URI serverUri , Draft draft , Map headers , int connecttimeout ) { + public WebSocketClient( URI serverUri , Draft protocolDraft , Map httpHeaders , int connectTimeout ) { if( serverUri == null ) { throw new IllegalArgumentException(); - } - if( draft == null ) { + } else if( protocolDraft == null ) { throw new IllegalArgumentException( "null as draft is permitted for `WebSocketServer` only!" ); } this.uri = serverUri; - this.draft = draft; - this.headers = headers; - this.timeout = connecttimeout; + this.draft = protocolDraft; + this.headers = httpHeaders; + this.connectTimeout = connectTimeout; + this.engine = new WebSocketImpl( this, protocolDraft ); } /** - * Gets the URI that this WebSocketClient is connected to. - * - * @return The URI for this WebSocketClient. + * Returns the URI that this WebSocketClient is connected to. */ public URI getURI() { return uri; } - /** Returns the protocol version this channel uses. */ + /** + * Returns the protocol version this channel uses.
+ * For more infos see https://github.com/TooTallNate/Java-WebSocket/wiki/Drafts + */ public Draft getDraft() { return draft; } /** - * Starts a background thread that attempts and maintains a WebSocket - * connection to the URI specified in the constructor or via setURI. - * setURI. + * Initiates the websocket connection. This method does not block. */ public void connect() { - if( thread != null ) + if( writeThread != null ) throw new IllegalStateException( "WebSocketClient objects are not reuseable" ); - thread = new Thread( this ); - thread.start(); + writeThread = new Thread( this ); + writeThread.start(); } /** - * Same as connect but blocks until the websocket connected or failed to do so.
+ * Same as connect but blocks until the websocket connected or failed to do so.
* Returns whether it succeeded or not. **/ public boolean connectBlocking() throws InterruptedException { connect(); connectLatch.await(); - return conn.isOpen(); + return engine.isOpen(); } + /** + * Initiates the websocket close handshake. This method does not block
+ * In oder to make sure the connection is closed use closeBlocking + */ public void close() { - if( thread != null && conn != null ) { - conn.close( CloseFrame.NORMAL ); + if( writeThread != null ) { + engine.close( CloseFrame.NORMAL ); } } @@ -168,134 +143,74 @@ public void closeBlocking() throws InterruptedException { } /** - * Sends text to the connected WebSocket server. + * Sends text to the connected websocket server. * * @param text - * The String to send to the WebSocket server. + * The string which will be transmitted. */ + @Override public void send( String text ) throws NotYetConnectedException { - if( conn != null ) { - conn.send( text ); - } + engine.send( text ); } /** - * Sends data to the connected WebSocket server. + * Sends binary data to the connected webSocket server. * * @param data - * The Byte-Array of data to send to the WebSocket server. + * The byte-Array of data to send to the WebSocket server. */ + @Override public void send( byte[] data ) throws NotYetConnectedException { - if( conn != null ) { - conn.send( data ); - } - } - - private void tryToConnect( InetSocketAddress remote ) throws IOException { - channel = SocketChannel.open(); - channel.configureBlocking( false ); - channel.connect( remote ); - selector = Selector.open(); - key = channel.register( selector, SelectionKey.OP_CONNECT ); + engine.send( data ); } - // Runnable IMPLEMENTATION ///////////////////////////////////////////////// public void run() { - if( thread == null ) - thread = Thread.currentThread(); - interruptableRun(); - - assert ( !channel.isOpen() ); - try { - if( selector != null ) // if the initialization in tryToConnect fails, it could be null - selector.close(); - } catch ( IOException e ) { - onError( e ); + if( socket == null ) { + socket = new Socket( proxy ); + } else if( socket.isClosed() ) { + throw new IOException(); + } + if( !socket.isBound() ) + socket.connect( new InetSocketAddress( uri.getHost(), getPort() ), connectTimeout ); + istream = socket.getInputStream(); + ostream = socket.getOutputStream(); + + sendHandshake(); + } catch ( /*IOException | SecurityException | UnresolvedAddressException | InvalidHandshakeException | ClosedByInterruptException | SocketTimeoutException */Exception e ) { + onWebsocketError( engine, e ); + engine.closeConnection( CloseFrame.NEVER_CONNECTED, e.getMessage() ); + return; } - } + writeThread = new Thread( new WebsocketWriteThread() ); + writeThread.start(); + + byte[] rawbuffer = new byte[ WebSocketImpl.RCVBUF ]; + int readBytes; - private final void interruptableRun() { try { - tryToConnect( new InetSocketAddress( uri.getHost(), getPort() ) ); - } catch ( ClosedByInterruptException e ) { - onWebsocketError( null, e ); - return; - } catch ( /*IOException | SecurityException | UnresolvedAddressException*/Exception e ) {// - onWebsocketError( conn, e ); - conn.closeConnection( CloseFrame.NEVER_CONNECTED, e.getMessage() ); - return; - } - conn = (WebSocketImpl) wf.createWebSocket( this, draft, channel.socket() ); - ByteBuffer buff = ByteBuffer.allocate( WebSocket.RCVBUF ); - try/*IO*/{ - while ( channel.isOpen() ) { - SelectionKey key = null; - selector.select( timeout ); - Set keys = selector.selectedKeys(); - Iterator i = keys.iterator(); - if( conn.getReadyState() == READYSTATE.NOT_YET_CONNECTED && !i.hasNext() ) { - // Hack for issue #140: - // Android does simply return form select without closing the channel if address is not reachable(which seems to be a bug in the android nio proivder) - // TODO provide a way to fix this problem which does not require this hack - throw new IOException( "Host is not reachable(Android Hack)" ); - } - while ( i.hasNext() ) { - key = i.next(); - i.remove(); - if( !key.isValid() ) { - conn.eot(); - continue; - } - if( key.isReadable() && SocketChannelIOHelper.read( buff, this.conn, wrappedchannel ) ) { - conn.decode( buff ); - } - if( key.isConnectable() ) { - try { - finishConnect( key ); - } catch ( InvalidHandshakeException e ) { - conn.close( e ); // http error - } - } - if( key.isWritable() ) { - if( SocketChannelIOHelper.batch( conn, wrappedchannel ) ) { - if( key.isValid() ) - key.interestOps( SelectionKey.OP_READ ); - } else { - key.interestOps( SelectionKey.OP_READ | SelectionKey.OP_WRITE ); - } - } - } - if( wrappedchannel instanceof WrappedByteChannel ) { - WrappedByteChannel w = (WrappedByteChannel) wrappedchannel; - if( w.isNeedRead() ) { - while ( SocketChannelIOHelper.read( buff, conn, w ) ) { - conn.decode( buff ); - } - } - } + while ( !isClosed() && ( readBytes = istream.read( rawbuffer ) ) != -1 ) { + engine.decode( ByteBuffer.wrap( rawbuffer, 0, readBytes ) ); } - - } catch ( CancelledKeyException e ) { - conn.eot(); + engine.eot(); } catch ( IOException e ) { - conn.eot(); + engine.eot(); } catch ( RuntimeException e ) { // this catch case covers internal errors only and indicates a bug in this websocket implementation onError( e ); - conn.closeConnection( CloseFrame.ABNORMAL_CLOSE, e.getMessage() ); + engine.closeConnection( CloseFrame.ABNORMAL_CLOSE, e.getMessage() ); } + assert ( socket.isClosed() ); } - private int getPort() { int port = uri.getPort(); if( port == -1 ) { String scheme = uri.getScheme(); if( scheme.equals( "wss" ) ) { - return IWebSocket.DEFAULT_WSS_PORT; + return WebSocket.DEFAULT_WSS_PORT; } else if( scheme.equals( "ws" ) ) { - return IWebSocket.DEFAULT_PORT; + return WebSocket.DEFAULT_PORT; } else { throw new RuntimeException( "unkonow scheme" + scheme ); } @@ -303,17 +218,6 @@ private int getPort() { return port; } - private void finishConnect( SelectionKey key ) throws IOException , InvalidHandshakeException { - if( !channel.finishConnect() ) - return; - // Now that we're connected, re-register for only 'READ' keys. - conn.key = key.interestOps( SelectionKey.OP_READ | SelectionKey.OP_WRITE ); - - conn.channel = wrappedchannel = wf.wrapChannel( key, uri.getHost(), getPort() ); - timeout = 0; // since connect is over - sendHandshake(); - } - private void sendHandshake() throws InvalidHandshakeException { String path; String part1 = uri.getPath(); @@ -325,7 +229,7 @@ private void sendHandshake() throws InvalidHandshakeException { if( part2 != null ) path += "?" + part2; int port = getPort(); - String host = uri.getHost() + ( port != IWebSocket.DEFAULT_PORT ? ":" + port : "" ); + String host = uri.getHost() + ( port != WebSocket.DEFAULT_PORT ? ":" + port : "" ); HandshakeImpl1Client handshake = new HandshakeImpl1Client(); handshake.setResourceDescriptor( path ); @@ -335,86 +239,82 @@ private void sendHandshake() throws InvalidHandshakeException { handshake.put( kv.getKey(), kv.getValue() ); } } - conn.startHandshake( handshake ); + engine.startHandshake( handshake ); } /** * This represents the state of the connection. - * You can use this method instead of */ + @Override public READYSTATE getReadyState() { - if( conn == null ) { - return READYSTATE.NOT_YET_CONNECTED; - } - return conn.getReadyState(); + return engine.getReadyState(); } /** * Calls subclass' implementation of onMessage. - * - * @param conn - * @param message */ @Override - public final void onWebsocketMessage( IWebSocket conn, String message ) { + public final void onWebsocketMessage( WebSocket conn, String message ) { onMessage( message ); } @Override - public final void onWebsocketMessage( IWebSocket conn, ByteBuffer blob ) { + public final void onWebsocketMessage( WebSocket conn, ByteBuffer blob ) { onMessage( blob ); } + @Override + public void onWebsocketMessageFragment( WebSocket conn, Framedata frame ) { + onFragment( frame ); + } + /** * Calls subclass' implementation of onOpen. - * - * @param conn */ @Override - public final void onWebsocketOpen( IWebSocket conn, Handshakedata handshake ) { + public final void onWebsocketOpen( WebSocket conn, Handshakedata handshake ) { connectLatch.countDown(); onOpen( (ServerHandshake) handshake ); } /** * Calls subclass' implementation of onClose. - * - * @param conn */ @Override - public final void onWebsocketClose( IWebSocket conn, int code, String reason, boolean remote ) { + public final void onWebsocketClose( WebSocket conn, int code, String reason, boolean remote ) { connectLatch.countDown(); closeLatch.countDown(); + if( writeThread != null ) + writeThread.interrupt(); + try { + if( socket != null ) + socket.close(); + } catch ( IOException e ) { + onWebsocketError( this, e ); + } onClose( code, reason, remote ); } /** * Calls subclass' implementation of onIOError. - * - * @param conn */ @Override - public final void onWebsocketError( IWebSocket conn, Exception ex ) { + public final void onWebsocketError( WebSocket conn, Exception ex ) { onError( ex ); } @Override - public final void onWriteDemand( IWebSocket conn ) { - try { - key.interestOps( SelectionKey.OP_READ | SelectionKey.OP_WRITE ); - selector.wakeup(); - } catch ( CancelledKeyException e ) { - // since such an exception/event will also occur on the selector there is no need to do anything herec - } + public final void onWriteDemand( WebSocket conn ) { + // nothing to do } @Override - public void onWebsocketCloseInitiated( IWebSocket conn, int code, String reason ) { + public void onWebsocketCloseInitiated( WebSocket conn, int code, String reason ) { onCloseInitiated( code, reason ); } @Override - public void onWebsocketClosing( IWebSocket conn, int code, String reason, boolean remote ) { + public void onWebsocketClosing( WebSocket conn, int code, String reason, boolean remote ) { onClosing( code, reason, remote ); } @@ -424,16 +324,22 @@ public void onCloseInitiated( int code, String reason ) { public void onClosing( int code, String reason, boolean remote ) { } - public IWebSocket getConnection() { - return conn; + public WebSocket getConnection() { + return engine; } - public final void setWebSocketFactory( WebSocketClientFactory wsf ) { - this.wf = wsf; + @Override + public InetSocketAddress getLocalSocketAddress( WebSocket conn ) { + if( socket != null ) + return (InetSocketAddress) socket.getLocalSocketAddress(); + return null; } - public final WebSocketFactory getWebSocketFactory() { - return wf; + @Override + public InetSocketAddress getRemoteSocketAddress( WebSocket conn ) { + if( socket != null ) + return (InetSocketAddress) socket.getRemoteSocketAddress(); + return null; } // ABTRACT METHODS ///////////////////////////////////////////////////////// @@ -442,9 +348,117 @@ public final WebSocketFactory getWebSocketFactory() { public abstract void onClose( int code, String reason, boolean remote ); public abstract void onError( Exception ex ); public void onMessage( ByteBuffer bytes ) { - }; + } + public void onFragment( Framedata frame ) { + } + + private class WebsocketWriteThread implements Runnable { + @Override + public void run() { + Thread.currentThread().setName( "WebsocketWriteThread" ); + try { + while ( !Thread.interrupted() ) { + ByteBuffer buffer = engine.outQueue.take(); + ostream.write( buffer.array(), 0, buffer.limit() ); + ostream.flush(); + } + } catch ( IOException e ) { + engine.eot(); + } catch ( InterruptedException e ) { + // this thread is regularly terminated via an interrupt + } + } + } + + public void setProxy( Proxy proxy ) { + if( proxy == null ) + throw new IllegalArgumentException(); + this.proxy = proxy; + } + + /** + * Accepts bound and unbound sockets.
+ * This method must be called before connect. + * If the given socket is not yet bound it will be bound to the uri specified in the constructor. + **/ + public void setSocket( Socket socket ) { + if( this.socket != null ) { + throw new IllegalStateException( "socket has already been set" ); + } + this.socket = socket; + } + + @Override + public void sendFragmentedFrame( Opcode op, ByteBuffer buffer, boolean fin ) { + engine.sendFragmentedFrame( op, buffer, fin ); + } + + @Override + public boolean isOpen() { + return engine.isOpen(); + } + + @Override + public boolean isFlushAndClose() { + return engine.isFlushAndClose(); + } + + @Override + public boolean isClosed() { + return engine.isClosed(); + } - public interface WebSocketClientFactory extends WebSocketFactory { - public ByteChannel wrapChannel( SelectionKey key, String host, int port ) throws IOException; + @Override + public boolean isClosing() { + return engine.isClosing(); + } + + @Override + public boolean isConnecting() { + return engine.isConnecting(); + } + + @Override + public boolean hasBufferedData() { + return engine.hasBufferedData(); + } + + @Override + public void close( int code ) { + engine.close(); + } + + @Override + public void close( int code, String message ) { + engine.close( code, message ); + } + + @Override + public void closeConnection( int code, String message ) { + engine.closeConnection( code, message ); + } + + @Override + public void send( ByteBuffer bytes ) throws IllegalArgumentException , NotYetConnectedException { + engine.send( bytes ); + } + + @Override + public void sendFrame( Framedata framedata ) { + engine.sendFrame( framedata ); + } + + @Override + public InetSocketAddress getLocalSocketAddress() { + return engine.getLocalSocketAddress(); + } + @Override + public InetSocketAddress getRemoteSocketAddress() { + return engine.getRemoteSocketAddress(); + } + + @Override + public String getResourceDescriptor() { + return uri.getPath(); } } diff --git a/src/main/java/org/java_websocket/drafts/Draft.java b/src/main/java/org/java_websocket/drafts/Draft.java index 8beab07d5..65b34de8f 100644 --- a/src/main/java/org/java_websocket/drafts/Draft.java +++ b/src/main/java/org/java_websocket/drafts/Draft.java @@ -6,13 +6,16 @@ import java.util.List; import java.util.Locale; -import org.java_websocket.IWebSocket.Role; +import org.java_websocket.WebSocket.Role; import org.java_websocket.exceptions.IncompleteHandshakeException; import org.java_websocket.exceptions.InvalidDataException; import org.java_websocket.exceptions.InvalidHandshakeException; import org.java_websocket.exceptions.LimitExedeedException; import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.FrameBuilder; import org.java_websocket.framing.Framedata; +import org.java_websocket.framing.Framedata.Opcode; +import org.java_websocket.framing.FramedataImpl1; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.handshake.ClientHandshakeBuilder; import org.java_websocket.handshake.HandshakeBuilder; @@ -46,6 +49,8 @@ public enum CloseHandshakeType { /** In some cases the handshake will be parsed different depending on whether */ protected Role role = null; + protected Opcode continuousFrameType = null; + public static ByteBuffer readLine( ByteBuffer buf ) { ByteBuffer sbuf = ByteBuffer.allocate( buf.remaining() ); byte prev = '0'; @@ -123,6 +128,32 @@ protected boolean basicAccept( Handshakedata handshakedata ) { public abstract List createFrames( String text, boolean mask ); + public List continuousFrame( Opcode op, ByteBuffer buffer, boolean fin ) { + if( op != Opcode.BINARY && op != Opcode.TEXT && op != Opcode.TEXT ) { + throw new IllegalArgumentException( "Only Opcode.BINARY or Opcode.TEXT are allowed" ); + } + + if( continuousFrameType != null ) { + continuousFrameType = Opcode.CONTINUOUS; + } else { + continuousFrameType = op; + } + + FrameBuilder bui = new FramedataImpl1( continuousFrameType ); + try { + bui.setPayload( buffer ); + } catch ( InvalidDataException e ) { + throw new RuntimeException( e ); // can only happen when one builds close frames(Opcode.Close) + } + bui.setFin( fin ); + if( fin ) { + continuousFrameType = null; + } else { + continuousFrameType = op; + } + return Collections.singletonList( (Framedata) bui ); + } + public abstract void reset(); public List createHandshake( Handshakedata handshakedata, Role ownrole ) { @@ -189,5 +220,9 @@ public int checkAlloc( int bytecount ) throws LimitExedeedException , InvalidDat public void setParseMode( Role role ) { this.role = role; } + + public Role getRole() { + return role; + } } diff --git a/src/main/java/org/java_websocket/drafts/Draft_10.java b/src/main/java/org/java_websocket/drafts/Draft_10.java index 6ed86f749..305460a52 100644 --- a/src/main/java/org/java_websocket/drafts/Draft_10.java +++ b/src/main/java/org/java_websocket/drafts/Draft_10.java @@ -8,7 +8,7 @@ import java.util.List; import java.util.Random; -import org.java_websocket.IWebSocket.Role; +import org.java_websocket.WebSocket.Role; import org.java_websocket.exceptions.InvalidDataException; import org.java_websocket.exceptions.InvalidFrameException; import org.java_websocket.exceptions.InvalidHandshakeException; @@ -31,13 +31,12 @@ public class Draft_10 extends Draft { private class IncompleteException extends Throwable { - + /** * It's Serializable. */ private static final long serialVersionUID = 7330519489840500997L; - - + private int preferedsize; public IncompleteException( int preferedsize ) { this.preferedsize = preferedsize; @@ -63,7 +62,7 @@ public static int readVersion( Handshakedata handshakedata ) { private ByteBuffer incompleteframe; private Framedata fragmentedframe = null; - + private final Random reuseableRandom = new Random(); @Override @@ -117,7 +116,7 @@ public ByteBuffer createBinaryFrame( Framedata framedata ) { ByteBuffer maskkey = ByteBuffer.allocate( 4 ); maskkey.putInt( reuseableRandom.nextInt() ); buf.put( maskkey.array() ); - for( int i = 0 ; i < mes.limit() ; i++ ) { + for( int i = 0 ; mes.hasRemaining() ; i++ ) { buf.put( (byte) ( mes.get() ^ maskkey.get( i % 4 ) ) ); } } else diff --git a/src/main/java/org/java_websocket/drafts/Draft_75.java b/src/main/java/org/java_websocket/drafts/Draft_75.java index f6e67f8ab..947a35ece 100644 --- a/src/main/java/org/java_websocket/drafts/Draft_75.java +++ b/src/main/java/org/java_websocket/drafts/Draft_75.java @@ -7,7 +7,9 @@ import java.util.Random; import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.exceptions.InvalidFrameException; import org.java_websocket.exceptions.InvalidHandshakeException; +import org.java_websocket.exceptions.LimitExedeedException; import org.java_websocket.exceptions.NotSendableException; import org.java_websocket.framing.CloseFrame; import org.java_websocket.framing.FrameBuilder; @@ -40,14 +42,13 @@ public class Draft_75 extends Draft { */ public static final byte END_OF_FRAME = (byte) 0xFF; + /** Is only used to detect protocol violations */ protected boolean readingState = false; - private boolean inframe = false; + protected List readyframes = new LinkedList(); protected ByteBuffer currentFrame; - - + private final Random reuseableRandom = new Random(); - @Override public HandshakeState acceptHandshakeAsClient( ClientHandshake request, ServerHandshake response ) { @@ -122,15 +123,16 @@ public HandshakeBuilder postProcessHandshakeResponseAsServer( ClientHandshake re } protected List translateRegularFrame( ByteBuffer buffer ) throws InvalidDataException { + while ( buffer.hasRemaining() ) { byte newestByte = buffer.get(); if( newestByte == START_OF_FRAME ) { // Beginning of Frame if( readingState ) - return null; + throw new InvalidFrameException( "unexpected START_OF_FRAME" ); readingState = true; } else if( newestByte == END_OF_FRAME ) { // End of Frame if( !readingState ) - return null; + throw new InvalidFrameException( "unexpected END_OF_FRAME" ); // currentFrame will be null if END_OF_FRAME was send directly after // START_OF_FRAME, thus we will send 'null' as the sent message. if( this.currentFrame != null ) { @@ -138,13 +140,12 @@ protected List translateRegularFrame( ByteBuffer buffer ) throws Inva FramedataImpl1 curframe = new FramedataImpl1(); curframe.setPayload( currentFrame ); curframe.setFin( true ); - curframe.setOptcode( inframe ? Opcode.CONTINUOUS : Opcode.TEXT ); + curframe.setOptcode( Opcode.TEXT ); readyframes.add( curframe ); this.currentFrame = null; buffer.mark(); } readingState = false; - inframe = false; } else if( readingState ) { // Regular frame data, add to current frame buffer //TODO This code is very expensive and slow if( currentFrame == null ) { currentFrame = createBuffer(); @@ -156,19 +157,14 @@ protected List translateRegularFrame( ByteBuffer buffer ) throws Inva return null; } } - if( readingState ) { - FramedataImpl1 curframe = new FramedataImpl1(); - currentFrame.flip(); - curframe.setPayload( currentFrame ); - curframe.setFin( false ); - curframe.setOptcode( inframe ? Opcode.CONTINUOUS : Opcode.TEXT ); - inframe = true; - readyframes.add( curframe ); - } + + // if no error occurred this block will be reached + /*if( readingState ) { + checkAlloc(currentFrame.position()+1); + }*/ List frames = readyframes; readyframes = new LinkedList(); - this.currentFrame = null; return frames; } @@ -196,9 +192,9 @@ public ByteBuffer createBuffer() { return ByteBuffer.allocate( INITIAL_FAMESIZE ); } - public ByteBuffer increaseBuffer( ByteBuffer full ) { + public ByteBuffer increaseBuffer( ByteBuffer full ) throws LimitExedeedException , InvalidDataException { full.flip(); - ByteBuffer newbuffer = ByteBuffer.allocate( full.capacity() * 2 ); + ByteBuffer newbuffer = ByteBuffer.allocate( checkAlloc( full.capacity() * 2 ) ); newbuffer.put( full ); return newbuffer; } diff --git a/src/main/java/org/java_websocket/drafts/Draft_76.java b/src/main/java/org/java_websocket/drafts/Draft_76.java index 6d9097573..26f23531e 100644 --- a/src/main/java/org/java_websocket/drafts/Draft_76.java +++ b/src/main/java/org/java_websocket/drafts/Draft_76.java @@ -9,7 +9,7 @@ import java.util.List; import java.util.Random; -import org.java_websocket.IWebSocket.Role; +import org.java_websocket.WebSocket.Role; import org.java_websocket.exceptions.IncompleteHandshakeException; import org.java_websocket.exceptions.InvalidDataException; import org.java_websocket.exceptions.InvalidFrameException; diff --git a/src/main/java/org/java_websocket/framing/CloseFrameBuilder.java b/src/main/java/org/java_websocket/framing/CloseFrameBuilder.java index 3922cabfa..fee1b540d 100644 --- a/src/main/java/org/java_websocket/framing/CloseFrameBuilder.java +++ b/src/main/java/org/java_websocket/framing/CloseFrameBuilder.java @@ -40,7 +40,7 @@ private void setCodeAndMessage( int code, String m ) throws InvalidDataException m = ""; } if( code == CloseFrame.NOCODE ) { - if( !m.isEmpty() ) { + if( 0 < m.length() ) { throw new InvalidDataException( PROTOCOL_ERROR, "A close frame must have a closecode if it has a reason" ); } return;// empty payload diff --git a/src/main/java/org/java_websocket/framing/FramedataImpl1.java b/src/main/java/org/java_websocket/framing/FramedataImpl1.java index 5a1a5574f..5fba075b4 100644 --- a/src/main/java/org/java_websocket/framing/FramedataImpl1.java +++ b/src/main/java/org/java_websocket/framing/FramedataImpl1.java @@ -104,7 +104,7 @@ public void append( Framedata nextframe ) throws InvalidFrameException { @Override public String toString() { - return "Framedata{ optcode:" + getOpcode() + ", fin:" + isFin() + ", payloadlength:" + unmaskedpayload.limit() + ", payload:" + Arrays.toString( Charsetfunctions.utf8Bytes( new String( unmaskedpayload.array() ) ) ) + "}"; + return "Framedata{ optcode:" + getOpcode() + ", fin:" + isFin() + ", payloadlength:[pos:" + unmaskedpayload.position() + ", len:" + unmaskedpayload.remaining() + "], payload:" + Arrays.toString( Charsetfunctions.utf8Bytes( new String( unmaskedpayload.array() ) ) ) + "}"; } } diff --git a/src/main/java/org/java_websocket/handshake/ClientHandshake.java b/src/main/java/org/java_websocket/handshake/ClientHandshake.java index 4ab28475e..918d22184 100644 --- a/src/main/java/org/java_websocket/handshake/ClientHandshake.java +++ b/src/main/java/org/java_websocket/handshake/ClientHandshake.java @@ -1,5 +1,6 @@ package org.java_websocket.handshake; public interface ClientHandshake extends Handshakedata { + /**returns the HTTP Request-URI as defined by http://tools.ietf.org/html/rfc2616#section-5.1.2*/ public String getResourceDescriptor(); } diff --git a/src/main/java/org/java_websocket/handshake/ClientHandshakeBuilder.java b/src/main/java/org/java_websocket/handshake/ClientHandshakeBuilder.java index 07d17309b..88ac4f27b 100644 --- a/src/main/java/org/java_websocket/handshake/ClientHandshakeBuilder.java +++ b/src/main/java/org/java_websocket/handshake/ClientHandshakeBuilder.java @@ -1,5 +1,5 @@ package org.java_websocket.handshake; public interface ClientHandshakeBuilder extends HandshakeBuilder, ClientHandshake { - public void setResourceDescriptor( String resourcedescriptor ); + public void setResourceDescriptor( String resourceDescriptor ); } diff --git a/src/main/java/org/java_websocket/handshake/HandshakeImpl1Client.java b/src/main/java/org/java_websocket/handshake/HandshakeImpl1Client.java index 789cb087a..15715e37e 100644 --- a/src/main/java/org/java_websocket/handshake/HandshakeImpl1Client.java +++ b/src/main/java/org/java_websocket/handshake/HandshakeImpl1Client.java @@ -1,16 +1,18 @@ package org.java_websocket.handshake; public class HandshakeImpl1Client extends HandshakedataImpl1 implements ClientHandshakeBuilder { - private String resourcedescriptor; + private String resourceDescriptor = "*"; public HandshakeImpl1Client() { } - public void setResourceDescriptor( String resourcedescriptor ) throws IllegalArgumentException { - this.resourcedescriptor = resourcedescriptor; + public void setResourceDescriptor( String resourceDescriptor ) throws IllegalArgumentException { + if(resourceDescriptor==null) + throw new IllegalArgumentException( "http resource descriptor must not be null" ); + this.resourceDescriptor = resourceDescriptor; } public String getResourceDescriptor() { - return resourcedescriptor; + return resourceDescriptor; } } diff --git a/src/main/java/org/java_websocket/handshake/HandshakedataImpl1.java b/src/main/java/org/java_websocket/handshake/HandshakedataImpl1.java index 39e7d4c9c..d4d9555c6 100644 --- a/src/main/java/org/java_websocket/handshake/HandshakedataImpl1.java +++ b/src/main/java/org/java_websocket/handshake/HandshakedataImpl1.java @@ -2,15 +2,14 @@ import java.util.Collections; import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Locale; +import java.util.TreeMap; public class HandshakedataImpl1 implements HandshakeBuilder { private byte[] content; - private LinkedHashMap map; + private TreeMap map; public HandshakedataImpl1() { - map = new LinkedHashMap(); + map = new TreeMap( String.CASE_INSENSITIVE_ORDER ); } /*public HandshakedataImpl1( Handshakedata h ) { @@ -32,8 +31,8 @@ public Iterator iterateHttpFields() { @Override public String getFieldValue( String name ) { - String s = map.get( name.toLowerCase( Locale.ENGLISH ) ); - if( s == null ) { + String s = map.get( name ); + if ( s == null ) { return ""; } return s; @@ -51,11 +50,11 @@ public void setContent( byte[] content ) { @Override public void put( String name, String value ) { - map.put( name.toLowerCase( Locale.ENGLISH ), value ); + map.put( name, value ); } @Override public boolean hasFieldValue( String name ) { - return map.containsKey( name.toLowerCase( Locale.ENGLISH ) ); + return map.containsKey( name ); } } diff --git a/src/main/java/org/java_websocket/server/DefaultSSLWebSocketServerFactory.java b/src/main/java/org/java_websocket/server/DefaultSSLWebSocketServerFactory.java index 6841151fc..b871260f8 100644 --- a/src/main/java/org/java_websocket/server/DefaultSSLWebSocketServerFactory.java +++ b/src/main/java/org/java_websocket/server/DefaultSSLWebSocketServerFactory.java @@ -3,6 +3,7 @@ import java.net.Socket; import java.nio.channels.ByteChannel; import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -32,19 +33,19 @@ public DefaultSSLWebSocketServerFactory( SSLContext sslContext , ExecutorService } @Override - public ByteChannel wrapChannel( SelectionKey c ) throws IOException { + public ByteChannel wrapChannel( SocketChannel channel, SelectionKey key ) throws IOException { SSLEngine e = sslcontext.createSSLEngine(); e.setUseClientMode( false ); - return new SSLSocketChannel2( c, e, exec ); + return new SSLSocketChannel2( channel, e, exec, key ); } @Override public WebSocketImpl createWebSocket( WebSocketAdapter a, Draft d, Socket c ) { - return new WebSocketImpl( a, d, c ); + return new WebSocketImpl( a, d ); } @Override public WebSocketImpl createWebSocket( WebSocketAdapter a, List d, Socket s ) { - return new WebSocketImpl( a, d, s ); + return new WebSocketImpl( a, d ); } } \ No newline at end of file diff --git a/src/main/java/org/java_websocket/server/DefaultWebSocketServerFactory.java b/src/main/java/org/java_websocket/server/DefaultWebSocketServerFactory.java index 70df6bc73..3b89cdc2f 100644 --- a/src/main/java/org/java_websocket/server/DefaultWebSocketServerFactory.java +++ b/src/main/java/org/java_websocket/server/DefaultWebSocketServerFactory.java @@ -13,14 +13,14 @@ public class DefaultWebSocketServerFactory implements WebSocketServerFactory { @Override public WebSocketImpl createWebSocket( WebSocketAdapter a, Draft d, Socket s ) { - return new WebSocketImpl( a, d, s ); + return new WebSocketImpl( a, d ); } @Override public WebSocketImpl createWebSocket( WebSocketAdapter a, List d, Socket s ) { - return new WebSocketImpl( a, d, s ); + return new WebSocketImpl( a, d ); } @Override - public SocketChannel wrapChannel( SelectionKey c ) { - return (SocketChannel) c.channel(); + public SocketChannel wrapChannel( SocketChannel channel, SelectionKey key ) { + return (SocketChannel) channel; } } \ No newline at end of file diff --git a/src/main/java/org/java_websocket/server/WebSocketServer.java b/src/main/java/org/java_websocket/server/WebSocketServer.java index f9ed780bf..2ad5e4169 100644 --- a/src/main/java/org/java_websocket/server/WebSocketServer.java +++ b/src/main/java/org/java_websocket/server/WebSocketServer.java @@ -8,6 +8,8 @@ import java.nio.ByteBuffer; import java.nio.channels.ByteChannel; import java.nio.channels.CancelledKeyException; +import java.nio.channels.ClosedByInterruptException; +import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; @@ -26,7 +28,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import org.java_websocket.IWebSocket; +import org.java_websocket.WebSocket; import org.java_websocket.SocketChannelIOHelper; import org.java_websocket.WebSocket; import org.java_websocket.WebSocketAdapter; @@ -34,9 +36,12 @@ import org.java_websocket.WebSocketImpl; import org.java_websocket.WrappedByteChannel; import org.java_websocket.drafts.Draft; +import org.java_websocket.exceptions.InvalidDataException; import org.java_websocket.framing.CloseFrame; +import org.java_websocket.framing.Framedata; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.handshake.Handshakedata; +import org.java_websocket.handshake.ServerHandshakeBuilder; /** * WebSocketServer is an abstract class that only takes care of the @@ -52,7 +57,7 @@ public abstract class WebSocketServer extends WebSocketAdapter implements Runnab * Holds the list of active WebSocket connections. "Active" means WebSocket * handshake is complete and socket can be written to, or read from. */ - private final Collection connections; + private final Collection connections; /** * The port number that this WebSocket server should listen on. Default is * WebSocket.DEFAULT_PORT. @@ -77,7 +82,6 @@ public abstract class WebSocketServer extends WebSocketAdapter implements Runnab private List decoders; - private BlockingQueue oqueue; private List iqueue; private BlockingQueue buffers; private int queueinvokes = 0; @@ -92,7 +96,7 @@ public abstract class WebSocketServer extends WebSocketAdapter implements Runnab * @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here */ public WebSocketServer() throws UnknownHostException { - this( new InetSocketAddress( IWebSocket.DEFAULT_PORT ), DECODERS, null ); + this( new InetSocketAddress( WebSocket.DEFAULT_PORT ), DECODERS, null ); } /** @@ -122,7 +126,7 @@ public WebSocketServer( InetSocketAddress address , List drafts ) { * @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here */ public WebSocketServer( InetSocketAddress address , int decodercount , List drafts ) { - this( address, decodercount, drafts, new HashSet() ); + this( address, decodercount, drafts, new HashSet() ); } /** @@ -145,7 +149,7 @@ public WebSocketServer( InetSocketAddress address , int decodercount , List more about drafts */ - public WebSocketServer( InetSocketAddress address , int decodercount , List drafts , Collection connectionscontainer ) { + public WebSocketServer( InetSocketAddress address , int decodercount , List drafts , Collection connectionscontainer ) { if( address == null || decodercount < 1 || connectionscontainer == null ) { throw new IllegalArgumentException( "address and connectionscontainer must not be null and you need at least 1 decoder" ); } @@ -158,7 +162,6 @@ public WebSocketServer( InetSocketAddress address , int decodercount , List(); iqueue = new LinkedList(); decoders = new ArrayList( decodercount ); @@ -193,41 +196,42 @@ public void start() { * If this method is called before the server is started it will never start. * * @param timeout - * Specifies how many milliseconds shall pass between initiating the close handshakes with the connected clients and closing the servers socket channel. + * Specifies how many milliseconds the overall close handshaking may take altogether before the connections are closed without proper close handshaking.
* * @throws IOException * When {@link ServerSocketChannel}.close throws an IOException * @throws InterruptedException */ - public void stop( int timeout ) throws IOException , InterruptedException { - if( !isclosed.compareAndSet( false, true ) ) { + public void stop( int timeout ) throws InterruptedException { + if( !isclosed.compareAndSet( false, true ) ) { // this also makes sure that no further connections will be added to this.connections return; } + List socketsToClose = null; + + // copy the connections in a list (prevent callback deadlocks) synchronized ( connections ) { - for( IWebSocket ws : connections ) { - ws.close( CloseFrame.GOING_AWAY ); - } + socketsToClose = new ArrayList( connections ); + } + + for( WebSocket ws : socketsToClose ) { + ws.close( CloseFrame.GOING_AWAY ); } + synchronized ( this ) { if( selectorthread != null ) { if( Thread.currentThread() != selectorthread ) { } - selectorthread.interrupt(); - selectorthread.join(); - } - if( decoders != null ) { - for( WebSocketWorker w : decoders ) { - w.interrupt(); + if( selectorthread != Thread.currentThread() ) { + if( socketsToClose.size() > 0 ) + selectorthread.join( timeout );// isclosed will tell the selectorthread to go down after the last connection was closed + selectorthread.interrupt();// in case the selectorthread did not terminate in time we send the interrupt + selectorthread.join(); } } - if( server != null ) { - server.close(); - } } } - public void stop() throws IOException , InterruptedException { stop( 0 ); } @@ -239,7 +243,7 @@ public void stop() throws IOException , InterruptedException { * * @return The currently connected clients. */ - public Collection connections() { + public Collection connections() { return this.connections; } @@ -279,12 +283,12 @@ public void run() { server = ServerSocketChannel.open(); server.configureBlocking( false ); ServerSocket socket = server.socket(); - socket.setReceiveBufferSize( WebSocket.RCVBUF ); + socket.setReceiveBufferSize( WebSocketImpl.RCVBUF ); socket.bind( address ); selector = Selector.open(); server.register( selector, server.validOps() ); } catch ( IOException ex ) { - onWebsocketError( null, ex ); + handleFatal( null, ex ); return; } try { @@ -293,8 +297,6 @@ public void run() { WebSocketImpl conn = null; try { selector.select(); - registerWrite(); - Set keys = selector.selectedKeys(); Iterator i = keys.iterator(); @@ -316,7 +318,7 @@ public void run() { channel.configureBlocking( false ); WebSocketImpl w = wsf.createWebSocket( this, drafts, channel.socket() ); w.key = channel.register( selector, SelectionKey.OP_READ, w ); - w.channel = wsf.wrapChannel( w.key ); + w.channel = wsf.wrapChannel( channel, w.key ); i.remove(); allocateBuffers( w ); continue; @@ -326,29 +328,29 @@ public void run() { conn = (WebSocketImpl) key.attachment(); ByteBuffer buf = takeBuffer(); try { - if( SocketChannelIOHelper.read( buf, conn, (ByteChannel) conn.channel ) ) { - conn.inQueue.put( buf ); - queue( conn ); - i.remove(); - if( conn.channel instanceof WrappedByteChannel ) { - if( ( (WrappedByteChannel) conn.channel ).isNeedRead() ) { - iqueue.add( conn ); + if( SocketChannelIOHelper.read( buf, conn, conn.channel ) ) { + if( buf.hasRemaining() ) { + conn.inQueue.put( buf ); + queue( conn ); + i.remove(); + if( conn.channel instanceof WrappedByteChannel ) { + if( ( (WrappedByteChannel) conn.channel ).isNeedRead() ) { + iqueue.add( conn ); + } } - } + } else + pushBuffer( buf ); } else { pushBuffer( buf ); } } catch ( IOException e ) { pushBuffer( buf ); throw e; - } catch ( RuntimeException e ) { - pushBuffer( buf ); - throw e; } } if( key.isWritable() ) { conn = (WebSocketImpl) key.attachment(); - if( SocketChannelIOHelper.batch( conn, (ByteChannel) conn.channel ) ) { + if( SocketChannelIOHelper.batch( conn, conn.channel ) ) { if( key.isValid() ) key.interestOps( SelectionKey.OP_READ ); } @@ -358,38 +360,53 @@ public void run() { conn = iqueue.remove( 0 ); WrappedByteChannel c = ( (WrappedByteChannel) conn.channel ); ByteBuffer buf = takeBuffer(); - // JBW/GW - 23DEC12: We get IOExceptions from SSL/TLS and then the buffer pool runs dry since we don't push back. try { if( SocketChannelIOHelper.readMore( buf, conn, c ) ) iqueue.add( conn ); - conn.inQueue.put( buf ); - queue( conn ); - } catch (IOException e) { - pushBuffer(buf); - throw e; - } catch (RuntimeException e) { - pushBuffer(buf); + if( buf.hasRemaining() ) { + conn.inQueue.put( buf ); + queue( conn ); + } else { + pushBuffer( buf ); + } + } catch ( IOException e ) { + pushBuffer( buf ); throw e; } + } } catch ( CancelledKeyException e ) { // an other thread may cancel the key + } catch ( ClosedByInterruptException e ) { + return; // do the same stuff as when InterruptedException is thrown } catch ( IOException ex ) { if( key != null ) key.cancel(); - handleIOException( conn, ex ); + handleIOException( key, conn, ex ); } catch ( InterruptedException e ) { - return;// FIXME controlled shutdown + return;// FIXME controlled shutdown (e.g. take care of buffermanagement) } } } catch ( RuntimeException e ) { // should hopefully never occur handleFatal( null, e ); + } finally { + if( decoders != null ) { + for( WebSocketWorker w : decoders ) { + w.interrupt(); + } + } + if( server != null ) { + try { + server.close(); + } catch ( IOException e ) { + onError( null, e ); + } + } } } - - protected void allocateBuffers( IWebSocket c ) throws InterruptedException { + protected void allocateBuffers( WebSocket c ) throws InterruptedException { if( queuesize.get() >= 2 * decoders.size() + 1 ) { return; } @@ -397,13 +414,13 @@ protected void allocateBuffers( IWebSocket c ) throws InterruptedException { buffers.put( createBuffer() ); } - protected void releaseBuffers( IWebSocket c ) throws InterruptedException { + protected void releaseBuffers( WebSocket c ) throws InterruptedException { // queuesize.decrementAndGet(); // takeBuffer(); } public ByteBuffer createBuffer() { - return ByteBuffer.allocate( WebSocket.RCVBUF ); + return ByteBuffer.allocate( WebSocketImpl.RCVBUF ); } private void queue( WebSocketImpl ws ) throws InterruptedException { @@ -424,22 +441,25 @@ private void pushBuffer( ByteBuffer buf ) throws InterruptedException { buffers.put( buf ); } - private void registerWrite() throws CancelledKeyException { - int size = oqueue.size(); - for( int i = 0 ; i < size ; i++ ) { - WebSocketImpl conn = oqueue.remove(); - conn.key.interestOps( SelectionKey.OP_READ | SelectionKey.OP_WRITE ); - } - } - - private void handleIOException( IWebSocket conn, IOException ex ) { - onWebsocketError( conn, ex );// conn may be null here + private void handleIOException( SelectionKey key, WebSocket conn, IOException ex ) { + // onWebsocketError( conn, ex );// conn may be null here if( conn != null ) { conn.closeConnection( CloseFrame.ABNORMAL_CLOSE, ex.getMessage() ); + } else if( key != null ) { + SelectableChannel channel = key.channel(); + if( channel != null && channel.isOpen() ) { // this could be the case if the IOException ex is a SSLException + try { + channel.close(); + } catch ( IOException e ) { + // there is nothing that must be done here + } + if( WebSocketImpl.DEBUG ) + System.out.println( "Connection closed because of" + ex ); + } } } - private void handleFatal( IWebSocket conn, RuntimeException e ) { + private void handleFatal( WebSocket conn, Exception e ) { onError( conn, e ); try { stop(); @@ -469,25 +489,30 @@ protected String getFlashSecurityPolicy() { } @Override - public final void onWebsocketMessage( IWebSocket conn, String message ) { + public final void onWebsocketMessage( WebSocket conn, String message ) { onMessage( conn, message ); } @Override - public final void onWebsocketMessage( IWebSocket conn, ByteBuffer blob ) { + @Deprecated + public/*final*/void onWebsocketMessageFragment( WebSocket conn, Framedata frame ) {// onFragment should be overloaded instead + onFragment( conn, frame ); + } + + @Override + public final void onWebsocketMessage( WebSocket conn, ByteBuffer blob ) { onMessage( conn, blob ); } @Override - public final void onWebsocketOpen( IWebSocket conn, Handshakedata handshake ) { + public final void onWebsocketOpen( WebSocket conn, Handshakedata handshake ) { if( addConnection( conn ) ) { onOpen( conn, (ClientHandshake) handshake ); } } @Override - public final void onWebsocketClose( IWebSocket conn, int code, String reason, boolean remote ) { - oqueue.add( (WebSocketImpl) conn );// because the ostream will close the channel + public final void onWebsocketClose( WebSocket conn, int code, String reason, boolean remote ) { selector.wakeup(); try { if( removeConnection( conn ) ) { @@ -509,50 +534,72 @@ public final void onWebsocketClose( IWebSocket conn, int code, String reason, bo * {@link #WebSocketServer(InetSocketAddress, int, List, Collection)} allows to specify a collection which will be used to store current connections in.
* Depending on the type on the connection, modifications of that collection may have to be synchronized. **/ - protected boolean removeConnection( IWebSocket ws ) { + protected boolean removeConnection( WebSocket ws ) { + boolean removed; synchronized ( connections ) { - return this.connections.remove( ws ); + removed = this.connections.remove( ws ); + assert ( removed ); + } + if( isclosed.get() && connections.size() == 0 ) { + selectorthread.interrupt(); } + return removed; + } + @Override + public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer( WebSocket conn, Draft draft, ClientHandshake request ) throws InvalidDataException { + return super.onWebsocketHandshakeReceivedAsServer( conn, draft, request ); } /** @see #removeConnection(WebSocket) */ - protected boolean addConnection( IWebSocket ws ) { - synchronized ( connections ) { - return this.connections.add( ws ); + protected boolean addConnection( WebSocket ws ) { + if( !isclosed.get() ) { + synchronized ( connections ) { + boolean succ = this.connections.add( ws ); + assert ( succ ); + return succ; + } + } else { + // This case will happen when a new connection gets ready while the server is already stopping. + ws.close( CloseFrame.GOING_AWAY ); + return true;// for consistency sake we will make sure that both onOpen will be called } } - /** * @param conn * may be null if the error does not belong to a single connection */ @Override - public final void onWebsocketError( IWebSocket conn, Exception ex ) { + public final void onWebsocketError( WebSocket conn, Exception ex ) { onError( conn, ex ); } @Override - public final void onWriteDemand( IWebSocket w ) { + public final void onWriteDemand( WebSocket w ) { WebSocketImpl conn = (WebSocketImpl) w; - oqueue.add( conn ); + try { + conn.key.interestOps( SelectionKey.OP_READ | SelectionKey.OP_WRITE ); + } catch ( CancelledKeyException e ) { + // the thread which cancels key is responsible for possible cleanup + conn.outQueue.clear(); + } selector.wakeup(); } @Override - public void onWebsocketCloseInitiated( IWebSocket conn, int code, String reason ) { + public void onWebsocketCloseInitiated( WebSocket conn, int code, String reason ) { onCloseInitiated( conn, code, reason ); } @Override - public void onWebsocketClosing( IWebSocket conn, int code, String reason, boolean remote ) { + public void onWebsocketClosing( WebSocket conn, int code, String reason, boolean remote ) { onClosing( conn, code, reason, remote ); } - public void onCloseInitiated( IWebSocket conn, int code, String reason ) { + public void onCloseInitiated( WebSocket conn, int code, String reason ) { } - public void onClosing( IWebSocket conn, int code, String reason, boolean remote ) { + public void onClosing( WebSocket conn, int code, String reason, boolean remote ) { } @@ -574,8 +621,23 @@ protected boolean onConnect( SelectionKey key ) { return true; } + private Socket getSocket( WebSocket conn ) { + WebSocketImpl impl = (WebSocketImpl) conn; + return ( (SocketChannel) impl.key.channel() ).socket(); + } + + @Override + public InetSocketAddress getLocalSocketAddress( WebSocket conn ) { + return (InetSocketAddress) getSocket( conn ).getLocalSocketAddress(); + } + + @Override + public InetSocketAddress getRemoteSocketAddress( WebSocket conn ) { + return (InetSocketAddress) getSocket( conn ).getRemoteSocketAddress(); + } + /** Called after an opening handshake has been performed and the given websocket is ready to be written on. */ - public abstract void onOpen( IWebSocket conn, ClientHandshake handshake ); + public abstract void onOpen( WebSocket conn, ClientHandshake handshake ); /** * Called after the websocket connection has been closed. * @@ -586,13 +648,13 @@ protected boolean onConnect( SelectionKey key ) { * @param remote * Returns whether or not the closing of the connection was initiated by the remote host. **/ - public abstract void onClose( IWebSocket conn, int code, String reason, boolean remote ); + public abstract void onClose( WebSocket conn, int code, String reason, boolean remote ); /** * Callback for string messages received from the remote host * * @see #onMessage(WebSocket, ByteBuffer) **/ - public abstract void onMessage( IWebSocket conn, String message ); + public abstract void onMessage( WebSocket conn, String message ); /** * Called when errors occurs. If an error causes the websocket connection to fail {@link #onClose(WebSocket, int, String, boolean)} will be called additionally.
* This method will be called primarily because of IO or protocol errors.
@@ -601,14 +663,20 @@ protected boolean onConnect( SelectionKey key ) { * @param con * Can be null if there error does not belong to one specific websocket. For example if the servers port could not be bound. **/ - public abstract void onError( IWebSocket conn, Exception ex ); + public abstract void onError( WebSocket conn, Exception ex ); /** * Callback for binary messages received from the remote host * * @see #onMessage(WebSocket, String) **/ - public void onMessage( IWebSocket conn, ByteBuffer message ) { - }; + public void onMessage( WebSocket conn, ByteBuffer message ) { + } + + /** + * @see WebSocket#sendFragmentedFrame(org.java_websocket.framing.Framedata.Opcode, ByteBuffer, boolean) + */ + public void onFragment( WebSocket conn, Framedata fragment ) { + } public class WebSocketWorker extends Thread { @@ -664,6 +732,6 @@ public interface WebSocketServerFactory extends WebSocketFactory { * a SelectionKey of an open SocketChannel. * @return The channel on which the read and write operations will be performed.
*/ - public ByteChannel wrapChannel( SelectionKey key ) throws IOException; + public ByteChannel wrapChannel( SocketChannel channel, SelectionKey key ) throws IOException; } } diff --git a/src/test/java/AutobahnClientTest.java b/src/test/java/AutobahnClientTest.java index bd345186a..a567fd1b4 100644 --- a/src/test/java/AutobahnClientTest.java +++ b/src/test/java/AutobahnClientTest.java @@ -5,6 +5,7 @@ import java.nio.ByteBuffer; import org.java_websocket.WebSocket; +import org.java_websocket.WebSocketImpl; import org.java_websocket.client.WebSocketClient; import org.java_websocket.drafts.Draft; import org.java_websocket.drafts.Draft_17; @@ -55,11 +56,11 @@ public static void main( String[] args ) { if( nextline != null ) { line = nextline; nextline = null; - WebSocket.DEBUG = false; + WebSocketImpl.DEBUG = false; } else { System.out.print( ">" ); line = sysin.readLine(); - WebSocket.DEBUG = true; + WebSocketImpl.DEBUG = true; } if( line.equals( "l" ) ) { line = perviousline; @@ -84,7 +85,7 @@ public static void main( String[] args ) { uri = URI.create( serverlocation + "/runCase?case=" + spl[ 1 ] + "&agent=" + clientname ); } else if( line.startsWith( "u" ) ) { - WebSocket.DEBUG = false; + WebSocketImpl.DEBUG = false; uri = URI.create( serverlocation + "/updateReports?agent=" + clientname ); } else if( line.startsWith( "d" ) ) { try { diff --git a/src/test/java/AutobahnServerTest.java b/src/test/java/AutobahnServerTest.java index dcf707b4c..ed7d05c47 100644 --- a/src/test/java/AutobahnServerTest.java +++ b/src/test/java/AutobahnServerTest.java @@ -4,6 +4,7 @@ import java.util.Collections; import org.java_websocket.WebSocket; +import org.java_websocket.WebSocketImpl; import org.java_websocket.drafts.Draft; import org.java_websocket.drafts.Draft_17; import org.java_websocket.framing.FrameBuilder; @@ -57,7 +58,7 @@ public void onWebsocketMessageFragment( WebSocket conn, Framedata frame ) { } public static void main( String[] args ) throws UnknownHostException { - WebSocket.DEBUG = false; + WebSocketImpl.DEBUG = false; int port; try { port = new Integer( args[ 0 ] );