From 2034dbb6c88967b3d459475e1dc9c0c80bc01642 Mon Sep 17 00:00:00 2001 From: Gary Russell Date: Sun, 14 Oct 2012 13:50:43 -0400 Subject: [PATCH] INTEXT-24 Lightweight WebSocket Server * Add Lightweight WebSocket Server Support * Run WebSocketServerTests and open ws.html in a browser. - Sending 'start' begins sending an incrementing # once per second. - 'stop' stops the stream (leaving the socket open), 'start' resumes again. - Test terminates after 60 seconds. * Fixes and Improvements for WebSocket Server - fix masking - fix bytes sent - add error handling to remove dead sockets - create new web page instead of using vert.x example - add status box - automatically update message to send to start/stop appropriately - update SI to 2.2.0.RELEASE - change test to a main() * Use Interceptor for Handshake * Move handshake to an interceptor instead of doing it in the SI flow. * Add close button to web page. * Add code to remove state from deserializer on close. * Implement Orderly Close Per RFC6455 * Rename Packages - org.springframework.integration.x.* * Change Version to 0.1.0. * Autbahn Test Suite - All Tests Pass - 1.1.x tests from the Autobahn Test Suite. - Autobahn 1.2.x Tests (Binary) - Autobahn Test 2.* (Ping/Pong) - Autobahn Tests 3.* (Reserved Bits) - Autobahn Tests 4.* (Invalid Opcodes) - Autobahn 5.* (Fragmentation) - Autobahn 6.* (UTF-8 Handling) - Autobahn Tests 7.* (Close Handling) * "non-strict" results - fast fail on bad UTF-8. We currently don't detect the bad UTF-8 until all fragments are received. * Run AutobahnTests.java and add the following to fuzzingclient.json... {"agent": "SIServer", "url": "ws://localhost:18080", "options": {"version": 18}} * Remove sysout * Replace with logger.debug(). * Remove SockJS Dependencies - Project started as a SockJS client; WebSocket classes were incorrectly dependent on some SockJS code. * Extracted the WebSocket code to its own class hierarchy. * Test Autobahn with SSL (wss://...) - Add trust store and key store. - Add config to listen for wss: as well as ws: connections * To test Autobahn for both ws and wss - In the wstest config file use: "servers": [ {"agent": "SIServer", "url": "ws://localhost:18080", "options": {"version": 18}}, {"agent": "SIServerSSL", "url": "wss://localhost:28080", "options": {"version": 18}} ], * Polishing + Resequence when Using NIO - Improve debug logging - Clear fragments - Add ResequencingMessageHandler to resequence messages when using NIO (frames can arrive on different threads, out of order; this causes issues with the Autobahn tests. * Reject Non WS Connections - only apply Resequencer if NIO Connection factory is configured with apply-sequence. - check protocol version - only 13 is supported. --- spring-integration-ip-extensions/README.md | 73 +++ spring-integration-ip-extensions/build.gradle | 223 ++++++++ .../gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 46670 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + spring-integration-ip-extensions/gradlew | 164 ++++++ spring-integration-ip-extensions/gradlew.bat | 90 ++++ .../publish-maven.gradle | 61 +++ .../src/api/overview.html | 13 + .../src/dist/license.txt | 201 +++++++ .../src/dist/notice.txt | 21 + .../integration/x/ip/package-info.java | 4 + .../AbstractByteArraySerializer.java | 85 +++ .../AbstractHttpSwitchingDeserializer.java | 146 +++++ .../serializer/ByteArrayCrLfSerializer.java | 87 +++ .../x/ip/serializer/DataFrame.java | 65 +++ .../x/ip/serializer/StatefulDeserializer.java | 28 + .../x/ip/websocket/WebSocketFrame.java | 98 ++++ .../x/ip/websocket/WebSocketSerializer.java | 509 ++++++++++++++++++ ...SocketTcpConnectionInterceptorFactory.java | 282 ++++++++++ .../websocket/WebSocketUpgradeException.java | 42 ++ .../x/ip/sockjs/Autobahn-context.xml | 101 ++++ .../x/ip/sockjs/AutobahnTests.java | 34 ++ .../sockjs/WebSocketServerTests-context.xml | 65 +++ .../x/ip/sockjs/WebSocketServerTests.java | 97 ++++ .../integration/x/ip/sockjs/ws.html | 83 +++ .../src/test/resources/key.store | Bin 0 -> 1383 bytes .../src/test/resources/log4j.xml | 32 ++ .../src/test/resources/trust.store | Bin 0 -> 32 bytes 29 files changed, 2611 insertions(+) create mode 100644 spring-integration-ip-extensions/README.md create mode 100644 spring-integration-ip-extensions/build.gradle create mode 100644 spring-integration-ip-extensions/gradle.properties create mode 100644 spring-integration-ip-extensions/gradle/wrapper/gradle-wrapper.jar create mode 100644 spring-integration-ip-extensions/gradle/wrapper/gradle-wrapper.properties create mode 100755 spring-integration-ip-extensions/gradlew create mode 100644 spring-integration-ip-extensions/gradlew.bat create mode 100644 spring-integration-ip-extensions/publish-maven.gradle create mode 100644 spring-integration-ip-extensions/src/api/overview.html create mode 100644 spring-integration-ip-extensions/src/dist/license.txt create mode 100644 spring-integration-ip-extensions/src/dist/notice.txt create mode 100644 spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/package-info.java create mode 100644 spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/AbstractByteArraySerializer.java create mode 100644 spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/AbstractHttpSwitchingDeserializer.java create mode 100644 spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/ByteArrayCrLfSerializer.java create mode 100644 spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/DataFrame.java create mode 100644 spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/StatefulDeserializer.java create mode 100644 spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketFrame.java create mode 100644 spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketSerializer.java create mode 100644 spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketTcpConnectionInterceptorFactory.java create mode 100644 spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketUpgradeException.java create mode 100644 spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/Autobahn-context.xml create mode 100644 spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/AutobahnTests.java create mode 100644 spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/WebSocketServerTests-context.xml create mode 100644 spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/WebSocketServerTests.java create mode 100644 spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/ws.html create mode 100644 spring-integration-ip-extensions/src/test/resources/key.store create mode 100644 spring-integration-ip-extensions/src/test/resources/log4j.xml create mode 100644 spring-integration-ip-extensions/src/test/resources/trust.store diff --git a/spring-integration-ip-extensions/README.md b/spring-integration-ip-extensions/README.md new file mode 100644 index 0000000..46958b4 --- /dev/null +++ b/spring-integration-ip-extensions/README.md @@ -0,0 +1,73 @@ +Spring Integration IP Extensions +================================================= + +Welcome to the Spring Integration IP Extensions project. It is intended to supplement the spring-integration-ip module with, for example, custom serializers/deserializers. + + +# Building + +If you encounter out of memory errors during the build, increase available heap and permgen for Gradle: + + GRADLE_OPTS='-XX:MaxPermSize=1024m -Xmx1024m' + +To build and install jars into your local Maven cache: + + ./gradlew install + +To build api Javadoc (results will be in `build/api`): + + ./gradlew api + +To build reference documentation (results will be in `build/reference`): + + ./gradlew reference + +To build complete distribution including `-dist`, `-docs`, and `-schema` zip files (results will be in `build/distributions`) + + ./gradlew dist + +# Using SpringSource Tool Suite + + Gradle projects can be directly imported into STS + +# Using PLain Eclipse + +To generate Eclipse metadata (.classpath and .project files), do the following: + + ./gradlew eclipse + +Once complete, you may then import the projects into Eclipse as usual: + + *File -> Import -> Existing projects into workspace* + +Browse to the *'spring-integration'* root directory. All projects should import +free of errors. + +# Using IntelliJ IDEA + +To generate IDEA metadata (.iml and .ipr files), do the following: + + ./gradlew idea + +For more information, please visit the Spring Integration website at: +[http://www.springsource.org/spring-integration](http://www.springsource.org/spring-integration) + + +WebSocket Server Demo +===================== + +This demonstrates how to use the TCP adapters to provide a very lightweight websocket server. + +Run WebSocketServerTests as a Java Application (main) and open + +file:///.../spring-integration-extensions/spring-integration-ip-extensions/src/test/java/org/springframework/integration/ip/extensions/sockjs/ws.html + +in a browser. + +Opening the page opens the WebSocket. + +Sending 'start' begins sending an incrementing # once per second. +'stop' stops the stream (leaving the socket open), 'start' resumes +again. Multiple browser instances get their own sequence #. + + diff --git a/spring-integration-ip-extensions/build.gradle b/spring-integration-ip-extensions/build.gradle new file mode 100644 index 0000000..45864a7 --- /dev/null +++ b/spring-integration-ip-extensions/build.gradle @@ -0,0 +1,223 @@ +description = 'Spring Integration IP Extensions' + +buildscript { + repositories { + maven { url 'https://repo.springsource.org/plugins-snapshot' } + } + dependencies { + classpath 'org.springframework.build.gradle:docbook-reference-plugin:0.1.5' + } +} + +apply plugin: 'java' +apply from: "${rootProject.projectDir}/publish-maven.gradle" +apply plugin: 'eclipse' +apply plugin: 'idea' + +group = 'org.springintegration.ip.extensions' + +repositories { + maven { url 'http://repo.springsource.org/libs-milestone' } + maven { url 'http://repo.springsource.org/plugins-release' } // for bundlor +} + +sourceCompatibility=1.6 +targetCompatibility=1.6 + +ext { + aspectjVersion = '1.6.8' + cglibVersion = '2.2' + commonsNetVersion = '3.0.1' + javaxActivationVersion = '1.1.1' + junitVersion = '4.10' + log4jVersion = '1.2.12' + mockitoVersion = '1.9.0' + springVersion = '3.1.3.RELEASE' + springIntegrationVersion = '2.2.0.RELEASE' +} + +eclipse { + project { + natures += 'org.springframework.ide.eclipse.core.springnature' + } +} + +sourceSets { + test { + resources { + srcDirs = ['src/test/resources', 'src/test/java'] + } + } +} + +// See http://www.gradle.org/docs/current/userguide/dependency_management.html#sub:configurations +// and http://www.gradle.org/docs/current/dsl/org.gradle.api.artifacts.ConfigurationContainer.html +configurations { + jacoco //Configuration Group used by Sonar to provide Code Coverage using JaCoCo +} + +dependencies { + compile "org.springframework.integration:spring-integration-ip:$springIntegrationVersion" + compile "commons-codec:commons-codec:1.5" + testCompile "org.springframework.integration:spring-integration-test:$springIntegrationVersion" + testCompile "junit:junit-dep:$junitVersion" + testCompile "log4j:log4j:$log4jVersion" + testCompile "org.mockito:mockito-all:$mockitoVersion" + testCompile "org.springframework:spring-test:$springVersion" + jacoco group: "org.jacoco", name: "org.jacoco.agent", version: "0.5.6.201201232323", classifier: "runtime" +} + +// enable all compiler warnings; individual projects may customize further +ext.xLintArg = '-Xlint:all' +[compileJava, compileTestJava]*.options*.compilerArgs = [xLintArg] + +test { + // suppress all console output during testing unless running `gradle -i` + logging.captureStandardOutput(LogLevel.INFO) + jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=*" +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allJava +} + +task javadocJar(type: Jar) { + classifier = 'javadoc' + from javadoc +} + +artifacts { + archives sourcesJar + archives javadocJar +} + +apply plugin: 'sonar' + +sonar { + + if (rootProject.hasProperty('sonarHostUrl')) { + server.url = rootProject.sonarHostUrl + } + + database { + if (rootProject.hasProperty('sonarJdbcUrl')) { + url = rootProject.sonarJdbcUrl + } + if (rootProject.hasProperty('sonarJdbcDriver')) { + driverClassName = rootProject.sonarJdbcDriver + } + if (rootProject.hasProperty('sonarJdbcUsername')) { + username = rootProject.sonarJdbcUsername + } + if (rootProject.hasProperty('sonarJdbcPassword')) { + password = rootProject.sonarJdbcPassword + } + } + + project { + dynamicAnalysis = "reuseReports" + withProjectProperties { props -> + props["sonar.core.codeCoveragePlugin"] = "jacoco" + props["sonar.jacoco.reportPath"] = "${buildDir.name}/jacoco.exec" + } + } + + logger.info("Sonar parameters used: server.url='${server.url}'; database.url='${database.url}'; database.driverClassName='${database.driverClassName}'; database.username='${database.username}'") +} + +task api(type: Javadoc) { + group = 'Documentation' + description = 'Generates aggregated Javadoc API documentation.' + title = "${rootProject.description} ${version} API" + options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED + options.author = true + options.header = rootProject.description + options.overview = 'src/api/overview.html' + + source = sourceSets.main.allJava + classpath = project.sourceSets.main.compileClasspath + destinationDir = new File(buildDir, "api") +} + +task docsZip(type: Zip) { + group = 'Distribution' + classifier = 'docs' + description = "Builds -${classifier} archive containing api and reference " + + "for deployment at static.springframework.org/spring-integration/docs." + + from (api) { + into 'api' + } + +} + +task distZip(type: Zip, dependsOn: [docsZip]) { + group = 'Distribution' + classifier = 'dist' + description = "Builds -${classifier} archive, containing all jars and docs, " + + "suitable for community download page." + + ext.baseDir = "${project.name}-${project.version}"; + + from('src/dist') { + include 'readme.txt' + include 'license.txt' + include 'notice.txt' + into "${baseDir}" + } + + from(zipTree(docsZip.archivePath)) { + into "${baseDir}/docs" + } + + into ("${baseDir}/libs") { + from project.jar + from project.sourcesJar + from project.javadocJar + } +} + +// Create an optional "with dependencies" distribution. +// Not published by default; only for use when building from source. +task depsZip(type: Zip, dependsOn: distZip) { zipTask -> + group = 'Distribution' + classifier = 'dist-with-deps' + description = "Builds -${classifier} archive, containing everything " + + "in the -${distZip.classifier} archive plus all dependencies." + + from zipTree(distZip.archivePath) + + gradle.taskGraph.whenReady { taskGraph -> + if (taskGraph.hasTask(":${zipTask.name}")) { + def projectName = rootProject.name + def artifacts = new HashSet() + + rootProject.configurations.runtime.resolvedConfiguration.resolvedArtifacts.each { artifact -> + def dependency = artifact.moduleVersion.id + if (!projectName.equals(dependency.name)) { + artifacts << artifact.file + } + } + + zipTask.from(artifacts) { + into "${distZip.baseDir}/deps" + } + } + } +} + +artifacts { + archives distZip + archives docsZip +} + +task dist(dependsOn: assemble) { + group = 'Distribution' + description = 'Builds -dist, -docs and -schema distribution archives.' +} + +task wrapper(type: Wrapper) { + description = 'Generates gradlew[.bat] scripts' + gradleVersion = '1.3' +} diff --git a/spring-integration-ip-extensions/gradle.properties b/spring-integration-ip-extensions/gradle.properties new file mode 100644 index 0000000..99f6735 --- /dev/null +++ b/spring-integration-ip-extensions/gradle.properties @@ -0,0 +1 @@ +version=0.1.0.BUILD-SNAPSHOT diff --git a/spring-integration-ip-extensions/gradle/wrapper/gradle-wrapper.jar b/spring-integration-ip-extensions/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7b359d719bf96879170b6887c0bbb2e02b00ac34 GIT binary patch literal 46670 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 literal 0 HcmV?d00001 diff --git a/spring-integration-ip-extensions/gradle/wrapper/gradle-wrapper.properties b/spring-integration-ip-extensions/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b2ebed1 --- /dev/null +++ b/spring-integration-ip-extensions/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Jan 07 15:58:18 EST 2013 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-1.3-bin.zip diff --git a/spring-integration-ip-extensions/gradlew b/spring-integration-ip-extensions/gradlew new file mode 100755 index 0000000..3851082 --- /dev/null +++ b/spring-integration-ip-extensions/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env 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 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 + GRADLE_OPTS="$GRADLE_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/spring-integration-ip-extensions/gradlew.bat b/spring-integration-ip-extensions/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/spring-integration-ip-extensions/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/spring-integration-ip-extensions/publish-maven.gradle b/spring-integration-ip-extensions/publish-maven.gradle new file mode 100644 index 0000000..b2374b3 --- /dev/null +++ b/spring-integration-ip-extensions/publish-maven.gradle @@ -0,0 +1,61 @@ +apply plugin: 'maven' + +ext.optionalDeps = [] +ext.providedDeps = [] + +ext.optional = { optionalDeps << it } +ext.provided = { providedDeps << it } + +install { + repositories.mavenInstaller { + customizePom(pom, project) + } +} + +def customizePom(pom, gradleProject) { + pom.whenConfigured { generatedPom -> + // respect 'optional' and 'provided' dependencies + gradleProject.optionalDeps.each { dep -> + generatedPom.dependencies.find { it.artifactId == dep.name }?.optional = true + } + gradleProject.providedDeps.each { dep -> + generatedPom.dependencies.find { it.artifactId == dep.name }?.scope = 'provided' + } + + // eliminate test-scoped dependencies (no need in maven central poms) + generatedPom.dependencies.removeAll { dep -> + dep.scope == 'test' + } + + // add all items necessary for maven central publication + generatedPom.project { + name = gradleProject.description + description = gradleProject.description + url = 'https://github.com/SpringSource/spring-integration' + organization { + name = 'SpringSource' + url = 'http://springsource.org' + } + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + scm { + url = 'https://github.com/SpringSource/spring-integration' + connection = 'scm:git:git://github.com/SpringSource/spring-integration' + developerConnection = 'scm:git:git://github.com/SpringSource/spring-integration' + } + + developers { + developer { + id = 'garyrussell' + name = 'Gary Russell' + email = 'grussell@vmware.com' + } + } + } + } +} diff --git a/spring-integration-ip-extensions/src/api/overview.html b/spring-integration-ip-extensions/src/api/overview.html new file mode 100644 index 0000000..314cf20 --- /dev/null +++ b/spring-integration-ip-extensions/src/api/overview.html @@ -0,0 +1,13 @@ + + + This document is the API specification for Spring Integration +
+
+

+ Spring Integration IP Extensions project is intended to supplement + the spring-integration-ip module with, for example, custom + serializers/deserializers. +

+
+ + diff --git a/spring-integration-ip-extensions/src/dist/license.txt b/spring-integration-ip-extensions/src/dist/license.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/spring-integration-ip-extensions/src/dist/license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/spring-integration-ip-extensions/src/dist/notice.txt b/spring-integration-ip-extensions/src/dist/notice.txt new file mode 100644 index 0000000..f62045a --- /dev/null +++ b/spring-integration-ip-extensions/src/dist/notice.txt @@ -0,0 +1,21 @@ + ======================================================================== + == NOTICE file corresponding to section 4 d of the Apache License, == + == Version 2.0, in this case for the Spring Integration distribution. == + ======================================================================== + + This product includes software developed by + the Apache Software Foundation (http://www.apache.org). + + The end-user documentation included with a redistribution, if any, + must include the following acknowledgement: + + "This product includes software developed by the Spring Framework + Project (http://www.springframework.org)." + + Alternatively, this acknowledgement may appear in the software itself, + if and wherever such third-party acknowledgements normally appear. + + The names "Spring", "Spring Framework", and "Spring Integration" must + not be used to endorse or promote products derived from this software + without prior written permission. For written permission, please contact + enquiries@springsource.com. diff --git a/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/package-info.java b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/package-info.java new file mode 100644 index 0000000..aba45f0 --- /dev/null +++ b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/package-info.java @@ -0,0 +1,4 @@ +/** + * Root package of the IP Extensions. + */ +package org.springframework.integration.x.ip; diff --git a/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/AbstractByteArraySerializer.java b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/AbstractByteArraySerializer.java new file mode 100644 index 0000000..21a9c84 --- /dev/null +++ b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/AbstractByteArraySerializer.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.x.ip.serializer; + +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.serializer.Deserializer; +import org.springframework.core.serializer.Serializer; + +/** + * Base class for (de)serializers that provide a mechanism to + * reconstruct a byte array from an arbitrary stream. + * + * TODO: Enhanced version of standard class - will be merged in 3.0. + * + * @author Gary Russell + * @since 2.0 + * + */ +public abstract class AbstractByteArraySerializer implements + Serializer, + Deserializer { + + protected int maxMessageSize = 2048; + + protected final Log logger = LogFactory.getLog(this.getClass()); + + /** + * The maximum supported message size for this serializer. + * Default 2048. + * @return The max message size. + */ + public int getMaxMessageSize() { + return maxMessageSize; + } + + /** + * The maximum supported message size for this serializer. + * Default 2048. + * @param maxMessageSize The max message size. + */ + public void setMaxMessageSize(int maxMessageSize) { + this.maxMessageSize = maxMessageSize; + } + + protected void checkClosure(int bite) throws IOException { + if (bite < 0) { + logger.debug("Socket closed during message assembly"); + throw new IOException("Socket closed during message assembly"); + } + } + + /** + * Copy size bytes to a new buffer exactly size bytes long. + * @param buffer The buffer containing the data. + * @param size The number of bytes to copy. + * @return The new buffer, or the buffer parameter if it is + * already the correct size. + */ + protected byte[] copyToSizedArray(byte[] buffer, int size) { + if (size == buffer.length) { + return buffer; + } + byte[] assembledData = new byte[size]; + System.arraycopy(buffer, 0, assembledData, 0, size); + return assembledData; + } + +} diff --git a/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/AbstractHttpSwitchingDeserializer.java b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/AbstractHttpSwitchingDeserializer.java new file mode 100644 index 0000000..ddd40cc --- /dev/null +++ b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/AbstractHttpSwitchingDeserializer.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.x.ip.serializer; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Base class for (de)Serializers that start with an HTTP-like protocol then + * switch to some other protocol. + * + * @author Gary Russell + * @since 3.0 + * + */ +public abstract class AbstractHttpSwitchingDeserializer implements StatefulDeserializer { + + protected final Log logger = LogFactory.getLog(this.getClass()); + + protected volatile int maxMessageSize = 2048; + + private final Map streamState = new ConcurrentHashMap(); + + protected final ByteArrayCrLfSerializer crlfDeserializer = new ByteArrayCrLfSerializer(); + + public void setMaxMessageSize(int maxMessageSize) { + this.maxMessageSize = maxMessageSize; + } + + protected ByteArrayCrLfSerializer getCrlfDeserializer() { + return crlfDeserializer; + } + + public abstract DataFrame deserialize(InputStream inputStream) throws IOException; + + protected BasicState getStreamState(InputStream inputStream) { + return streamState.get(inputStream); + } + + /** + * Returns null if we've switched from HTTP-like protocol; headers otherwise. + * @param inputStream + * @return null or list of DataFrame, where the first frame contains the headers. + * Implementations may add additional frames. + * @throws IOException + */ + protected List checkStreaming(InputStream inputStream) throws IOException { + BasicState isStreaming = this.streamState.get(inputStream); + if (isStreaming == null) { //Consume the headers - TODO - check status + StringBuilder headersBuilder = new StringBuilder(); + byte[] headers = new byte[this.maxMessageSize]; + int headersLength; + do { + headersLength = this.crlfDeserializer.fillToCrLf(inputStream, headers); + String header = new String(headers, 0, headersLength, "UTF-8"); + headersBuilder.append(header).append("\r\n"); + } + while (headersLength > 0); + BasicState basicState = createState(); + List dataList = new ArrayList(); + List decodedHeaders = decodeHeaders(headersBuilder.toString(), basicState, dataList); + this.streamState.put(inputStream, basicState); + return decodedHeaders; + } + return null; + } + + protected BasicState createState() { + return new BasicState(); + } + + protected void checkClosure(int bite) throws IOException { + if (bite < 0) { + logger.debug("Socket closed during message assembly"); + throw new IOException("Socket closed during message assembly"); + } + } + + protected List decodeHeaders(String frameData, BasicState state, List dataList) { + // TODO: Full header separation - mvc utils? + if (logger.isDebugEnabled()) { + logger.debug("Received:Headers\r\n" + frameData); + } + dataList.add(createDataFrame(DataFrame.TYPE_HEADERS, frameData)); + return dataList; + } + + protected DataFrame createDataFrame(int type, String frameData) { + return new DataFrame(type, frameData); + } + + public void removeState(Object key) { + this.streamState.remove(key); + } + + public BasicState getState(Object key) { + return this.streamState.get(key); + } + + public static class BasicState { + + private volatile DataFrame pendingFrame; + + private final List fragments = new ArrayList(); + + public DataFrame getPendingFrame() { + return pendingFrame; + } + + public void setPendingFrame(DataFrame pendingFrame) { + this.pendingFrame = pendingFrame; + } + + public List getFragments() { + return fragments; + } + + @Override + public String toString() { + return "BasicState [pendingFrame=" + pendingFrame + ", fragments.size()=" + fragments.size() + "]"; + } + + + } + +} \ No newline at end of file diff --git a/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/ByteArrayCrLfSerializer.java b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/ByteArrayCrLfSerializer.java new file mode 100644 index 0000000..8bb831e --- /dev/null +++ b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/ByteArrayCrLfSerializer.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.integration.x.ip.serializer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.springframework.integration.ip.tcp.serializer.SoftEndOfStreamException; + +/** + * Reads data in an InputStream to a byte[]; data must be terminated by \r\n + * (not included in resulting byte[]). + * Writes a byte[] to an OutputStream and adds \r\n. + * + * TODO: Enhanced version of standard class - will be merged in 3.0. + * + * @author Gary Russell + * @since 2.0 + */ +public class ByteArrayCrLfSerializer extends AbstractByteArraySerializer { + + private static final byte[] CRLF = "\r\n".getBytes(); + + /** + * Reads the data in the inputstream to a byte[]. Data must be terminated + * by CRLF (\r\n). Throws a {@link SoftEndOfStreamException} if the stream + * is closed immediately after the \r\n (i.e. no data is in the process of + * being read). + */ + public byte[] deserialize(InputStream inputStream) throws IOException { + byte[] buffer = new byte[this.maxMessageSize]; + int n = this.fillToCrLf(inputStream, buffer); + byte[] assembledData = this.copyToSizedArray(buffer, n); + return assembledData; + } + + public int fillToCrLf(InputStream inputStream, byte[] buffer) + throws IOException, SoftEndOfStreamException { + int n = 0; + int bite; + if (logger.isDebugEnabled()) { + logger.debug("Available to read:" + inputStream.available()); + } + while (true) { + bite = inputStream.read(); +// logger.debug("Read:" + (char) bite); + if (bite < 0 && n == 0) { + throw new SoftEndOfStreamException("Stream closed between payloads"); + } + checkClosure(bite); + if (n > 0 && bite == '\n' && buffer[n-1] == '\r') { + break; + } + buffer[n++] = (byte) bite; + if (n >= this.maxMessageSize) { + throw new IOException("CRLF not found before max message length: " + + this.maxMessageSize); + } + }; + return n-1; // trim \r + } + + /** + * Writes the byte[] to the stream and appends \r\n. + */ + public void serialize(byte[] bytes, OutputStream outputStream) throws IOException { + outputStream.write(bytes); + outputStream.write(CRLF); + outputStream.flush(); + } + +} diff --git a/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/DataFrame.java b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/DataFrame.java new file mode 100644 index 0000000..da1499a --- /dev/null +++ b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/DataFrame.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.x.ip.serializer; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +public class DataFrame { + + public static final int TYPE_INVALID = 0; + + public static final int TYPE_HEADERS = 1; + + public static final int TYPE_DATA = 4; + + public static final int TYPE_DATA_BINARY = 260; + + protected final int type; + + protected final String payload; + + protected final byte[] binary; + + public DataFrame(int type, String payload) { + this(type, payload, null); + } + + public DataFrame(int type, byte[] binary) { + this(type, null, binary); + } + + public DataFrame(int type, String payload, byte[] binary) { + this.type = type; + this.payload = payload; + this.binary = binary; + } + + public int getType() { + return this.type; + } + + public String getPayload() { + return this.payload; + } + + public byte[] getBinary() { + return binary; + } + +} \ No newline at end of file diff --git a/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/StatefulDeserializer.java b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/StatefulDeserializer.java new file mode 100644 index 0000000..c97f79f --- /dev/null +++ b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/serializer/StatefulDeserializer.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.x.ip.serializer; + +import org.springframework.core.serializer.Deserializer; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +public interface StatefulDeserializer extends Deserializer { + + void removeState(Object key); +} diff --git a/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketFrame.java b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketFrame.java new file mode 100644 index 0000000..75d1175 --- /dev/null +++ b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketFrame.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.x.ip.websocket; + +import org.springframework.integration.x.ip.serializer.DataFrame; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +public class WebSocketFrame extends DataFrame { + + public static final int TYPE_FRAGMENTED_CONTROL = 256; + + public static final int TYPE_INVALID_UTF8 = 512; + + public static final int TYPE_PING = 5; + + public static final int TYPE_PONG = 6; + + public static final int TYPE_OPEN = 7; + + public static final int TYPE_CLOSE = 8; + + private static final String[] typeToString = new String[] { + "Invalid", "Headers", "**", "**", "Data", "Ping", "Pong", "Open", "Close" + }; + + private volatile short status = -1; + + private volatile int rsv; + + public WebSocketFrame(int type, String payload) { + super(type, payload); + } + + public WebSocketFrame(int type, byte[] binary) { + super(type, binary); + } + + public WebSocketFrame(int type, String payload, byte[] binary) { + super(type, payload, binary); + } + + public short getStatus() { + return status; + } + + public void setStatus(short status) { + this.status = status; + } + + public void setRsv(int rsv) { + this.rsv = rsv; + } + + public int getRsv() { + return rsv; + } + + @Override + public String toString() { + int len = 0; + boolean trunc = false; + if (this.payload!= null) { + len = Math.min(100, payload.length()); + trunc = len < payload.length(); + } + String typeAsString; + if ((type & 0xff) < typeToString.length) { + typeAsString = typeToString[type & 0xff]; + } + else { + typeAsString = Integer.toString(type); + } + return "WebSocketFrame [type=" + typeAsString + "(" + + type + ")"+ (payload == null ? "" : ", payload=" + payload.substring(0, len) + + (trunc ? "..." : "")) + + ", binary=" + binary + + (binary != null ? ", binary.length=" + binary.length : "") + + ", status=" + status + ", rsv=" + rsv + "]"; + } + +} diff --git a/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketSerializer.java b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketSerializer.java new file mode 100644 index 0000000..365d3d7 --- /dev/null +++ b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketSerializer.java @@ -0,0 +1,509 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.x.ip.websocket; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.codec.binary.Base64; +import org.springframework.core.serializer.Serializer; +import org.springframework.integration.MessagingException; +import org.springframework.integration.ip.tcp.serializer.SoftEndOfStreamException; +import org.springframework.integration.x.ip.serializer.AbstractHttpSwitchingDeserializer; +import org.springframework.integration.x.ip.serializer.DataFrame; +import org.springframework.util.Assert; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +public class WebSocketSerializer extends AbstractHttpSwitchingDeserializer implements Serializer { + + private static final String HTTP_1_1_101_WEB_SOCKET_PROTOCOL_HANDSHAKE_SPRING_INTEGRATION = + "HTTP/1.1 101 Web Socket Protocol Handshake - Spring Integration\r\n"; + + private static final Set INVALID_STATUS = new HashSet( + Arrays.asList((short) 1004, (short) 1005, (short) 1006, (short) 1012, (short) 1013, (short) 1014, (short) 1015)); + + private volatile boolean server; + + private boolean validateUtf8; + + public void setServer(boolean server) { + this.server = server; + } + + /** + * Validate UTF-8 (required for Autobahn tests). + * @param validateUtf8 + */ + public void setValidateUtf8(boolean validateUtf8) { + this.validateUtf8 = validateUtf8; + } + + @Override + protected DataFrame createDataFrame(int type, String frameData) { + return new WebSocketFrame(type, frameData); + } + + @Override + protected BasicState createState() { + return new WebSocketState(); + } + + public void serialize(final Object frame, OutputStream outputStream) + throws IOException { + String data = ""; + WebSocketFrame theFrame = null; + if (frame instanceof String) { + data = (String) frame; + theFrame = new WebSocketFrame(WebSocketFrame.TYPE_DATA, data); + } + else if (frame instanceof WebSocketFrame) { + theFrame = (WebSocketFrame) frame; + data = theFrame.getPayload(); + } + if (data != null && data.startsWith("HTTP/1.1")) { + outputStream.write(data.getBytes()); + return; + } + int lenBytes; + int payloadLen = this.server ? 0 : 0x80; //masked + boolean close = theFrame.getType() == WebSocketFrame.TYPE_CLOSE; + boolean pong = theFrame.getType() == WebSocketFrame.TYPE_PONG; + byte[] bytes = theFrame.getBinary() != null ? theFrame.getBinary() : data.getBytes("UTF-8"); + + int length = bytes.length; + if (close) { + length += 2; + } + if (length >= Math.pow(2, 16)) { + lenBytes = 8; + payloadLen |= 127; + } + else if (length > 125) { + lenBytes = 2; + payloadLen |= 126; + } + else { + lenBytes = 0; + payloadLen |= length; + } + int mask = (int) System.currentTimeMillis(); + ByteBuffer buffer = ByteBuffer.allocate(length + 6 + lenBytes); + if (pong) { + buffer.put((byte) 0x8a); + } + else if (close) { + buffer.put((byte) 0x88); + } + else if (theFrame.getType() == WebSocketFrame.TYPE_DATA_BINARY) { + buffer.put((byte) 0x82); + } + else { + // Final fragment; text + buffer.put((byte) 0x81); + } + buffer.put((byte) payloadLen); + if (lenBytes == 2) { + buffer.putShort((short) length); + } + else if (lenBytes == 8) { + buffer.putLong(length); + } + + byte[] maskBytes = new byte[4]; + if (!server) { + buffer.putInt(mask); + buffer.position(buffer.position() - 4); + buffer.get(maskBytes); + } + if (close) { + buffer.putShort(theFrame.getStatus()); + // TODO: mask status when client + } + for (int i = 0; i < bytes.length; i++) { + if (server) { + buffer.put(bytes[i]); + } + else { + buffer.put((byte) (bytes[i] ^ maskBytes[i % 4])); + } + } + outputStream.write(buffer.array(), 0, buffer.position()); + } + + @Override + public DataFrame deserialize(InputStream inputStream) throws IOException { + DataFrame frame = null; + BasicState state = this.getState(inputStream); + if (state != null) { + frame = state.getPendingFrame(); + } + while (frame == null || (frame.getPayload() == null && frame.getBinary() == null)) { + frame = doDeserialize(inputStream, frame); + if (frame.getPayload() == null && frame.getBinary() == null) { + state.setPendingFrame(frame); + } + } + return frame; + } + + private DataFrame doDeserialize(InputStream inputStream, DataFrame protoFrame) throws IOException { + List headers = checkStreaming(inputStream); + if (headers != null) { + return headers.get(0); + } + int bite; + if (logger.isDebugEnabled()) { + logger.debug("Available to read:" + inputStream.available()); + } + boolean done = false; + int len = 0; + int n = 0; + int dataInx = 0; + byte[] buffer = null; + boolean fin = false; + boolean ping = false; + boolean pong = false; + boolean close = false; + boolean binary = false; + boolean invalid = false; + String invalidText = null; + boolean fragmentedControl = false; + int lenBytes = 0; + byte[] mask = new byte[4]; + int maskInx = 0; + int rsv = 0; + while (!done ) { + bite = inputStream.read(); +// logger.debug("Read:" + Integer.toHexString(bite)); + if (bite < 0 && n == 0) { + throw new SoftEndOfStreamException("Stream closed between payloads"); + } + checkClosure(bite); + switch (n++) { + case 0: + fin = (bite & 0x80) > 0; + rsv = (bite & 0x70) >> 4; + bite &= 0x0f; + switch (bite) { + case 0x00: + logger.debug("Continuation, fin=" + fin); + if (protoFrame == null) { + invalid = true; + invalidText = "Unexpected continuation frame"; + } + else { + binary = protoFrame.getType() == WebSocketFrame.TYPE_DATA_BINARY; + } + this.getState(inputStream).setPendingFrame(null); + break; + case 0x01: + logger.debug("Text, fin=" + fin); + if (protoFrame != null) { + invalid = true; + invalidText = "Expected continuation frame"; + } + break; + case 0x02: + logger.debug("Binary, fin=" + fin); + if (protoFrame != null) { + invalid = true; + invalidText = "Expected continuation frame"; + } + binary = true; + break; + case 0x08: + logger.debug("Close, fin=" + fin); + fragmentedControl = !fin; + close = true; + break; + case 0x09: + ping = true; + binary = true; + fragmentedControl = !fin; + logger.debug("Ping, fin=" + fin); + break; + case 0x0a: + pong = true; + fragmentedControl = !fin; + logger.debug("Pong, fin=" + fin); + break; + case 0x03: + case 0x04: + case 0x05: + case 0x06: + case 0x07: + case 0x0b: + case 0x0c: + case 0x0d: + case 0x0e: + case 0x0f: + invalid = true; + invalidText = "Reserved opcode " + Integer.toHexString(bite); + break; + default: + throw new IOException("Unexpected opcode " + Integer.toHexString(bite)); + } + break; + case 1: + if (this.server) { + if ((bite & 0x80) == 0) { + throw new IOException("Illegal: Expected masked data from client"); + } + bite &= 0x7f; + } + if ((bite & 0x80) > 0) { + throw new IOException("Illegal: Received masked data from server"); + } + if (bite < 126) { + len = bite; + buffer = new byte[len]; + } + else if (bite == 126) { + lenBytes = 2; + } + else { + lenBytes = 8; + } + break; + case 2: + case 3: + case 4: + case 5: + if (lenBytes > 4 && bite != 0) { + throw new IOException("Max supported length exceeded"); + } + case 6: + if (lenBytes > 3 && (bite & 0x80) > 0) { + throw new IOException("Max supported length exceeded"); + } + case 7: + case 8: + case 9: + if (lenBytes-- > 0) { + len = len << 8 | (bite & 0xff); + if (lenBytes == 0) { + buffer = new byte[len]; + } + break; + } + default: + if (this.server && maskInx < 4) { + mask[maskInx++] = (byte) bite; + } + else { + if (this.server) { + bite ^= mask[dataInx % 4]; + } + buffer[dataInx++] = (byte) bite; + } + done = (server ? maskInx == 4 : true) && dataInx >= len; + } + }; + + WebSocketFrame frame; + + if (fragmentedControl) { + frame = new WebSocketFrame(WebSocketFrame.TYPE_FRAGMENTED_CONTROL, "Fragmented control frame", buffer); + } + else if (invalid) { + frame = new WebSocketFrame(WebSocketFrame.TYPE_INVALID, invalidText, buffer); + } + else if (!fin) { + List fragments = this.getState(inputStream).getFragments(); + fragments.add(buffer); + logger.debug("Fragment"); + return new WebSocketFrame(binary ? WebSocketFrame.TYPE_DATA_BINARY : WebSocketFrame.TYPE_DATA, (String) null); + } + else if (ping) { + frame = new WebSocketFrame(WebSocketFrame.TYPE_PING, buffer); + } + else if (pong) { + String data = new String(buffer, "UTF-8"); + frame = new WebSocketFrame(WebSocketFrame.TYPE_PONG, data); + } + else if (close) { + String data = new String(buffer, "UTF-8"); + if (data.length() >= 2) { + data = data.substring(2); + } + WebSocketFrame closeFrame = new WebSocketFrame(WebSocketFrame.TYPE_CLOSE, data); + short status = 1000; + if (buffer.length >= 2) { + status = (short) ((buffer[0] << 8) | (buffer[1] & 0xff)); + closeFrame.setStatus(status); + } + if (buffer.length == 1 || buffer.length > 125 || + (buffer.length > 2 && !validateUtf8IfNecessary(buffer, 2, data)) || + status < 1000 || INVALID_STATUS.contains(status) || (status >= 1016 && status < 3000) || status >= 5000) { + // Simply close in this case; no close reply + ((WebSocketState) this.getState(inputStream)).setCloseInitiated(true); + } + frame = closeFrame; + } + else { + List fragments = this.getState(inputStream).getFragments(); + if (fragments.size() == 0) { + if (binary) { + frame = new WebSocketFrame(WebSocketFrame.TYPE_DATA_BINARY, buffer); + } + else { + String data = new String(buffer, "UTF-8"); + if (!validateUtf8IfNecessary(buffer, 0, data)) { + frame = new WebSocketFrame(WebSocketFrame.TYPE_INVALID_UTF8, "Invalid UTF-8", buffer); + } + else { + frame = new WebSocketFrame(WebSocketFrame.TYPE_DATA, data); + } + } + } + else { + fragments.add(buffer); + int utf8Len = 0; + for (byte[] fragment : fragments) { + utf8Len += fragment.length; + } + byte[] reconstructed = new byte[utf8Len]; + int utf8Pos = 0; + for (byte[] fragment : fragments) { + System.arraycopy(fragment, 0, reconstructed, utf8Pos, fragment.length); + utf8Pos += fragment.length; + } + fragments.clear(); + if (binary) { + frame = new WebSocketFrame(WebSocketFrame.TYPE_DATA_BINARY, reconstructed); + } + else { + String data = new String(reconstructed, "UTF-8"); + if (!validateUtf8IfNecessary(reconstructed, 0, data)) { + frame = new WebSocketFrame(WebSocketFrame.TYPE_INVALID_UTF8, "Invalid UTF-8", reconstructed); + } + else { + frame = new WebSocketFrame(WebSocketFrame.TYPE_DATA, data); + } + } + } + } + if (rsv > 0) { + frame.setRsv(rsv); + } + return frame; + } + + private boolean validateUtf8IfNecessary(byte[] buffer, int offset, String data) { + if (this.validateUtf8) { + try { + byte[] bytes = data.getBytes("UTF-8"); + if (bytes.length != buffer.length - offset) { + return false; + } + for (int i = 0; i < bytes.length; i++) { + if (buffer[i + offset] != bytes[i]) { + return false; + } + } + } + catch (UnsupportedEncodingException e) { + throw new MessagingException("UTF-8 Conversion error"); + } + } + return true; + } + + @Override + protected void checkClosure(int bite) throws IOException { + if (bite < 0) { + logger.debug("Socket closed during message assembly"); + throw new IOException("Socket closed during message assembly"); + } + } + + @Override + public void removeState(Object inputStream) { + super.removeState(inputStream); + } + + public WebSocketFrame generateHandshake(WebSocketFrame frame) throws Exception { + Assert.isTrue(frame.getType() == WebSocketFrame.TYPE_HEADERS, "Expected headers:" + frame); + String[] headers = frame.getPayload().split("\\r\\n"); + String key = null; + String version = null; + for (String header : headers) { + if (header.toLowerCase().startsWith("sec-websocket-key")) { + key = header.split(":")[1].trim(); + } + else if (header.toLowerCase().startsWith("sec-websocket-version")) { + version = header.split(":")[1].trim(); + } + } + if (key == null) { + throw new WebSocketUpgradeException("400 Bad Request: No sec-websocket-key header detected"); + } + else if (!"13".equals(version)) { + throw new WebSocketUpgradeException("426 Upgrade Required", "sec-websocket-version: 13\r\n"); + } + String handshake = HTTP_1_1_101_WEB_SOCKET_PROTOCOL_HANDSHAKE_SPRING_INTEGRATION + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Accept: " + this.generateWebSocketAccept(key) + "\r\n\r\n"; + return new WebSocketFrame(WebSocketFrame.TYPE_DATA, handshake); + } + + private String generateWebSocketAccept(String key) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + String toDigest = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + byte[] acceptStringBytes = md.digest(toDigest.getBytes()); + acceptStringBytes = Base64.encodeBase64(acceptStringBytes); + String acceptString = new String(acceptStringBytes); + return acceptString; + } + + public static class WebSocketState extends BasicState { + + private volatile boolean closeInitiated; + + private volatile boolean expectingPong; + + public boolean isCloseInitiated() { + return this.closeInitiated; + } + + public void setCloseInitiated(boolean closeInitiated) { + this.closeInitiated = closeInitiated; + } + + public boolean isExpectingPong() { + return this.expectingPong; + } + + public void setExpectingPong(boolean expectingPong) { + this.expectingPong = expectingPong; + } + + } +} diff --git a/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketTcpConnectionInterceptorFactory.java b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketTcpConnectionInterceptorFactory.java new file mode 100644 index 0000000..1d4f1ff --- /dev/null +++ b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketTcpConnectionInterceptorFactory.java @@ -0,0 +1,282 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.x.ip.websocket; + +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.core.serializer.Deserializer; +import org.springframework.integration.Message; +import org.springframework.integration.MessageHandlingException; +import org.springframework.integration.MessageHeaders; +import org.springframework.integration.MessagingException; +import org.springframework.integration.aggregator.ResequencingMessageGroupProcessor; +import org.springframework.integration.aggregator.ResequencingMessageHandler; +import org.springframework.integration.channel.DirectChannel; +import org.springframework.integration.core.MessageHandler; +import org.springframework.integration.endpoint.EventDrivenConsumer; +import org.springframework.integration.ip.tcp.connection.AbstractTcpConnectionInterceptor; +import org.springframework.integration.ip.tcp.connection.TcpConnection; +import org.springframework.integration.ip.tcp.connection.TcpConnectionInterceptor; +import org.springframework.integration.ip.tcp.connection.TcpConnectionInterceptorFactory; +import org.springframework.integration.ip.tcp.connection.TcpNetConnection; +import org.springframework.integration.ip.tcp.connection.TcpNioConnection; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.x.ip.websocket.WebSocketSerializer.WebSocketState; +import org.springframework.util.Assert; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +public class WebSocketTcpConnectionInterceptorFactory implements TcpConnectionInterceptorFactory { + + private static final Log logger = LogFactory.getLog(WebSocketTcpConnectionInterceptor.class); + + @Override + public TcpConnectionInterceptor getInterceptor() { + return new WebSocketTcpConnectionInterceptor(); + } + + private class WebSocketTcpConnectionInterceptor extends AbstractTcpConnectionInterceptor { + + private volatile boolean shook; + + private volatile InputStream theInputStream; + + private final DirectChannel resequenceChannel = new DirectChannel(); + + private final EventDrivenConsumer resequencer; + + public WebSocketTcpConnectionInterceptor() { + super(); + ResequencingMessageHandler handler = new ResequencingMessageHandler(new ResequencingMessageGroupProcessor()); + handler.setReleasePartialSequences(true); + DirectChannel resequenced = new DirectChannel(); + resequenced.setBeanName("resequencedWSFrames"); + handler.setOutputChannel(resequenced); + this.resequencer = new EventDrivenConsumer(this.resequenceChannel, handler); + resequenced.subscribe(new MessageHandler() { + + @Override + public void handleMessage(Message message) throws MessagingException { + doOnMessage(message); + } + }); + this.resequencer.afterPropertiesSet(); + this.resequencer.start(); + } + + /** + * When using NIO, we have to resequence the messages because frames may + * arrive out of order. This is particularly an issue for some of the + * Autobahn tests where, for example, many pings are sent and the test + * expects the pongs to come back in the same order. + */ + @Override + public boolean onMessage(Message message) { + if (this.getTheConnection() instanceof TcpNioConnection && message.getHeaders().getCorrelationId() != null) { + resequenceChannel.send(message); + return true; + } + else { + return this.doOnMessage(message); + } + } + + public boolean doOnMessage(Message message) { + Assert.isInstanceOf(WebSocketFrame.class, message.getPayload()); + WebSocketFrame payload = (WebSocketFrame) message.getPayload(); + InputStream inputStream = null; + try { + inputStream = this.getTheInputStream(); + } + catch (IOException e1) { + this.protocolViolation(message); + } + + WebSocketState state = (WebSocketState) this.getRequiredDeserializer().getState(inputStream); + Assert.notNull(state, "State must not be null:" + message); + if (logger.isDebugEnabled()) { + logger.debug(state); + } + if (payload.getRsv() > 0) { + if (logger.isDebugEnabled()) { + logger.debug("Reserved bits:" + payload.getRsv()); + } + this.protocolViolation(message); + } + else if (payload.getType() == WebSocketFrame.TYPE_CLOSE) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Close, status:" + payload.getStatus()); + } + // If we initiated the close, just close. + if (!state.isCloseInitiated()) { + if (payload.getStatus() < 0) { + payload.setStatus((short) 1000); + } + this.send(message); + } + this.close(); + } + catch (Exception e) { + throw new MessageHandlingException(message, "Send failed", e); + } + } + else if (state == null || state.isCloseInitiated()) { + if (logger.isWarnEnabled()) { + logger.warn("Message dropped - close initiated:" + message); + } + } + else if ((payload.getType() & 0xff) == WebSocketFrame.TYPE_INVALID) { + if (logger.isDebugEnabled()) { + logger.debug("Invalid:" + payload.getPayload()); + } + this.protocolViolation(message); + } + else if (payload.getType() == WebSocketFrame.TYPE_FRAGMENTED_CONTROL) { + if (logger.isDebugEnabled()) { + logger.debug("Fragmented Control Op"); + } + this.protocolViolation(message); + } + else if (payload.getType() == WebSocketFrame.TYPE_PING) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Ping:" + new String(payload.getBinary(), "UTF-8")); + } + if (payload.getBinary().length > 125) { + this.protocolViolation(message); + } + else { + WebSocketFrame pong = new WebSocketFrame(WebSocketFrame.TYPE_PONG, payload.getBinary()); + this.send(MessageBuilder.withPayload(pong) + .copyHeaders(message.getHeaders()) + .build()); + } + } + catch (Exception e) { + throw new MessageHandlingException(message, "Send failed", e); + } + } + else if (payload.getType() == WebSocketFrame.TYPE_PONG) { + if (logger.isDebugEnabled()) { + logger.debug("Pong"); + } + } + else if (this.shook) { + return super.onMessage(message); + } + else { + try { + doHandshake(payload, message.getHeaders()); + this.shook = true; + } + catch (Exception e) { + throw new MessageHandlingException(message, "Handshake failed", e); + } + } + return true; + } + + private void protocolViolation(Message message) { + if (logger.isDebugEnabled()) { + logger.debug("Protocol violation - closing; " + message); + } + WebSocketFrame frame = (WebSocketFrame) message.getPayload(); + String error = "Protocol Error" + frame.getPayload() == null ? "" : (":" + frame.getPayload()); + WebSocketFrame close = new WebSocketFrame(WebSocketFrame.TYPE_CLOSE, error); + close.setStatus(frame.getType() == WebSocketFrame.TYPE_INVALID_UTF8 ? (short) 1007 : (short) 1002); + try { + ((WebSocketState) this.getRequiredDeserializer().getState(this.getTheInputStream())).setCloseInitiated(true); + this.send(MessageBuilder.withPayload(close) + .copyHeaders(message.getHeaders()) + .build()); + } + catch (Exception e) { + throw new MessageHandlingException(message, "Send failed", e); } + } + + @Override + public void close() { + try { + InputStream inputStream = getTheInputStream(); + if (inputStream != null) { + this.getRequiredDeserializer().removeState(inputStream); + } + } + catch (IOException e) { + } + super.close(); + } + + /** + * Hack - need to add getInputStream() to TcpConnection. + * @return + * @throws IOException + */ + private InputStream getTheInputStream() throws IOException { + if (this.theInputStream != null) { + return this.theInputStream; + } + TcpConnection theConnection = this.getTheConnection(); + InputStream inputStream = null; + if (theConnection instanceof TcpNioConnection) { + inputStream = (InputStream) new DirectFieldAccessor(theConnection).getPropertyValue("pipedInputStream"); + } + else if (theConnection instanceof TcpNetConnection) { + Socket socket = (Socket) new DirectFieldAccessor(theConnection).getPropertyValue("socket"); + if (socket != null) { + inputStream = socket.getInputStream(); + } + } + this.theInputStream = inputStream; + return inputStream; + } + + private void doHandshake(WebSocketFrame frame, MessageHeaders messageHeaders) throws Exception { + try { + WebSocketFrame handshake = this.getRequiredDeserializer().generateHandshake(frame); + this.send(MessageBuilder.withPayload(handshake) + .copyHeaders(messageHeaders) + .build()); + } + catch (WebSocketUpgradeException e) { + this.send(MessageBuilder + .withPayload( + new WebSocketFrame(WebSocketFrame.TYPE_DATA, "HTTP/1.1 " + + e.getMessage() + e.getHeaders())) + .copyHeaders(messageHeaders) + .build()); + this.close(); + } + } + + private WebSocketSerializer getRequiredDeserializer() { + Deserializer deserializer = this.getDeserializer(); + Assert.state(deserializer instanceof WebSocketSerializer, + "Deserializer must be a WebSocketSerializer"); + return (WebSocketSerializer) deserializer; + } + } + +} diff --git a/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketUpgradeException.java b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketUpgradeException.java new file mode 100644 index 0000000..b5b1691 --- /dev/null +++ b/spring-integration-ip-extensions/src/main/java/org/springframework/integration/x/ip/websocket/WebSocketUpgradeException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.x.ip.websocket; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +@SuppressWarnings("serial") +public class WebSocketUpgradeException extends RuntimeException { + + private final String headers; + + public WebSocketUpgradeException(String message) { + super(message + "\r\n"); + this.headers = "\r\n"; + } + + public WebSocketUpgradeException(String message, String headers) { + super(message + "\r\n"); + this.headers = headers + "\r\n"; + } + + protected String getHeaders() { + return headers; + } + +} diff --git a/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/Autobahn-context.xml b/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/Autobahn-context.xml new file mode 100644 index 0000000..b3b8f06 --- /dev/null +++ b/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/Autobahn-context.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/AutobahnTests.java b/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/AutobahnTests.java new file mode 100644 index 0000000..51f3aaa --- /dev/null +++ b/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/AutobahnTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.x.ip.sockjs; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +public class AutobahnTests { + + public static void main(String[] args) throws Exception { + new ClassPathXmlApplicationContext("Autobahn-context.xml", AutobahnTests.class); + System.out.println("Hit Enter To Terminate..."); + System.in.read(); + System.exit(0); + } + +} diff --git a/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/WebSocketServerTests-context.xml b/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/WebSocketServerTests-context.xml new file mode 100644 index 0000000..70155bc --- /dev/null +++ b/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/WebSocketServerTests-context.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/WebSocketServerTests.java b/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/WebSocketServerTests.java new file mode 100644 index 0000000..ab1a2ef --- /dev/null +++ b/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/WebSocketServerTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.integration.x.ip.sockjs; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.integration.Message; +import org.springframework.integration.annotation.Header; +import org.springframework.integration.ip.IpHeaders; +import org.springframework.integration.support.MessageBuilder; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +public class WebSocketServerTests { + + public static void main(String[] args) throws Exception { + new ClassPathXmlApplicationContext(WebSocketServerTests.class.getSimpleName() + "-context.xml", WebSocketServerTests.class); + System.out.println("Hit Enter To Terminate..."); + System.in.read(); + System.exit(0); + } + + public static class DemoService { + + private static final Log logger = LogFactory.getLog(DemoService.class); + + private final Map clients = new HashMap(); + + private final Map paused = new HashMap(); + + public void startStop(String command, @Header(IpHeaders.CONNECTION_ID) String connectionId) { + if ("stop".equalsIgnoreCase(command)) { + AtomicInteger clientInt = clients.remove(connectionId); + if (clientInt != null) { + paused.put(connectionId, clientInt); + } + logger.info("Connection " + connectionId + " stopped"); + } + else if ("start".equalsIgnoreCase(command)) { + AtomicInteger clientInt = paused.remove(connectionId); + clientInt = clientInt == null ? new AtomicInteger() : clientInt; + clients.put(connectionId, clientInt); + logger.info("Connection " + connectionId + " (re)started"); + } + else { + logger.info("Unexpected command: " + command); + } + } + + public List> getNext() { + List> messages = new ArrayList>(); + for (Entry entry : clients.entrySet()) { + Message message = MessageBuilder.withPayload(Integer.toString(entry.getValue().incrementAndGet())) + .setHeader(IpHeaders.CONNECTION_ID, entry.getKey()) + .build(); + messages.add(message); + logger.warn("Sending " + message.getPayload() + " to connection " + entry.getKey()); + } + if (messages.size() == 0) { + return null; + } + else { + return messages; + } + } + + public void remove(String connetionId) { + logger.warn("Error on write; removing " + connetionId); + clients.remove(connetionId); + } + } + +} diff --git a/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/ws.html b/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/ws.html new file mode 100644 index 0000000..80904e2 --- /dev/null +++ b/spring-integration-ip-extensions/src/test/java/org/springframework/integration/x/ip/sockjs/ws.html @@ -0,0 +1,83 @@ + + + +Spring Integration Web Socket Test + + + +
+
+

+ +

+ +

+ +
+
+ +

+ +
+ + diff --git a/spring-integration-ip-extensions/src/test/resources/key.store b/spring-integration-ip-extensions/src/test/resources/key.store new file mode 100644 index 0000000000000000000000000000000000000000..cbabe547f276d24e4401be3d11a72140e4826c07 GIT binary patch literal 1383 zcmezO_TO6u1_mY|W&~r-^5W#wqLS>?N}!1KT^SY^pzJP#CZ=r$d~96WY>X_7T1itinpYbDubuuvT5HxEk`XUv|S9 zw!OPI&rWOi|}rUwfG=<@+bNM{*rrSfrrxh)GQRVDEOPjMJx8 z7p2VCh^}W`+wE+l>@%hQ*|XKI8)qi4pW3rH_)P4xGi?G#-fw*%uFC#JVnVKZQuTQo z#ok+Qcjeu=u0M6kQufyg{M(pMuI@D762pB^Vy#*G*2`O^)Pz)aX;<@kUtc$s@4(K7 z+W)QnUS|I7S(W)eI_~0Q|G;EdK+t+ZwWcs!n z;#-{e^qFRxjuZM|`J zS4`Q__4nCJJrZo*1gr=at~eK{%CRe{{N~x`{Lbw*^;-5Nu3uf=Iw{%5E!|jb&flxB zyJ!D7$GZ#ON3~4LmX)opsHsan{OeNswdMP7zc%bG6f4)uwtO;`Nx!F8p3O1iazlAv z$sEIlpR?L0%KjJNf4ZS}oAk4?kZUE&9dqj^aq7nkWafDb7)bDLm{IJaf28AvzKI_z zZ#R?e@+u3*lXY9>-;fXzycKc(<;TOu$?am(o0rb6jeor}e_`1yw|Up~7C)aR=j`}g znZMDhKfLbA?gcN8PI)KvB>&_R>p#c0$9>JU%c-!k`pqk4etz})kKjbj8lh)uU^Z!B5h*7^^OWp0H8y-J7I^x7*R9(duPe_> zjtaURe$GPYdC%mnPXvp9Z;1|kx7B3%Y~~bl&mE1$zz8kkcdmU)txz?&srFyOT^oeclcaFUCf1dng z=~fOC;ow@yo7mfp!rx84(rR4%H{;3dAGy+vB@6mipZ5_7d)i_@>C3HiC#)|W X4|0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-integration-ip-extensions/src/test/resources/trust.store b/spring-integration-ip-extensions/src/test/resources/trust.store new file mode 100644 index 0000000000000000000000000000000000000000..4e5e1399aee491f1765c8ff8a833c7c1103380ae GIT binary patch literal 32 mcmezO_TO6u1_mY|W@tFi`n=R{PU>QDACY&KEuvWkKVksZ3=H4^ literal 0 HcmV?d00001