├── .gitignore ├── LICENSE.md ├── README.md ├── build.gradle ├── settings.gradle ├── src ├── main │ ├── java │ │ └── com │ │ │ └── jwrp │ │ │ └── javawebsocketreverseproxy │ │ │ ├── JavaWebsocketReverseProxyApplication.java │ │ │ ├── NextHop.java │ │ │ ├── ProxyWebSocketConfigurer.java │ │ │ ├── WebSocketProxyClientHandler.java │ │ │ └── WebSocketProxyServerHandler.java │ └── resources │ │ └── application.yml └── test │ └── java │ └── com │ └── jwrp │ └── javawebsocketreverseproxy │ └── JavaWebsocketReverseProxyApplicationTests.java ├── test_websocketserver_direct.sh ├── test_websocketserver_javaproxy.sh ├── test_websocketserver_nodeproxy.sh ├── websocketproxy.js └── websocketserver.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | 24 | .gradle/ 25 | gradle/ 26 | gradlew* 27 | 28 | .idea/ 29 | *.iml 30 | 31 | node_modules 32 | package-lock.json 33 | 34 | build/ 35 | out/ 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rob Barrett 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # java-websocket-reverse-proxy 2 | 3 | Most of the java websocket examples I've found have been based on, or included a messaging 4 | protocol like STOMP. This example is the result of looking at how to proxy any message content, 5 | without worrying about the messaging protocol. 6 | 7 | Java implementation of a websocket reverse proxy. A similar method to the one described in 8 | [https://www.nginx.com/blog/websocket-nginx/](https://www.nginx.com/blog/websocket-nginx/), 9 | but implemented in Java. This could be useful in Java application servers, e.g. Spring Boot. 10 | 11 | There are nodejs scripts at the root level that can be used to mimic functionality 12 | required to verify the proxy server. 13 | 14 | The common one is the [websocketserver.js](./websocketserver.js) script, which listens 15 | on port `9999` and echoes back any input after uppercasing it. To test the echoing behaviour 16 | by establishing a direct connection to a websocket server, run 17 | [test_websocketserver_direct.sh](./test_websocketserver_direct.sh) 18 | 19 | *(requires [node & npm](https://nodejs.org/en/) to be installed)* 20 | 21 | There is also a very simple [websocketproxy.js](./websocketproxy.js) script which will proxy 22 | the websocketserver. It listens on port `8888` and will relay all requests to port `9999`. 23 | It can be tested by running [test_websocketserver_nodeproxy.sh](./test_websocketserver_nodeproxy.sh) 24 | 25 | A simple java implementation to match this proxying behaviour is contained in the classes defined 26 | in the [src](./src) folder. It is based on [Spring Boot](https://projects.spring.io/spring-boot/). 27 | You can build it using [Gradle](https://gradle.org/) and run it up manually, 28 | or use the [test_websocketserver_javaproxy.sh](./test_websocketserver_javaproxy.sh) script. 29 | 30 | The java websocket reverse proxy will listen on port `7777` and relay all requests to port `9999`. 31 | 32 | *(developed on macOS, ymmv on other platforms)* 33 | 34 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.5.4.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | apply plugin: 'java' 14 | apply plugin: 'org.springframework.boot' 15 | 16 | group = 'jwrp' 17 | version = '0.0.1-SNAPSHOT' 18 | sourceCompatibility = 1.8 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | compile('org.springframework.boot:spring-boot-starter-websocket') 26 | compile('org.springframework.security.oauth:spring-security-oauth2') 27 | testCompile('org.springframework.boot:spring-boot-starter-test') 28 | } 29 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'java-websocket-reverse-proxy' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/com/jwrp/javawebsocketreverseproxy/JavaWebsocketReverseProxyApplication.java: -------------------------------------------------------------------------------- 1 | package com.jwrp.javawebsocketreverseproxy; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocket; 6 | 7 | @SpringBootApplication 8 | @EnableWebSocket 9 | public class JavaWebsocketReverseProxyApplication { 10 | 11 | public static void main(String[] args) { 12 | System.out.println("Proxy server started on port 7777"); 13 | System.out.println("Proxy server will forward requests to port 9999"); 14 | SpringApplication.run(JavaWebsocketReverseProxyApplication.class, args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/jwrp/javawebsocketreverseproxy/NextHop.java: -------------------------------------------------------------------------------- 1 | package com.jwrp.javawebsocketreverseproxy; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.security.oauth2.provider.OAuth2Authentication; 7 | import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; 8 | import org.springframework.web.socket.WebSocketHttpHeaders; 9 | import org.springframework.web.socket.WebSocketMessage; 10 | import org.springframework.web.socket.WebSocketSession; 11 | import org.springframework.web.socket.client.standard.StandardWebSocketClient; 12 | 13 | import java.io.IOException; 14 | import java.net.URI; 15 | import java.security.Principal; 16 | import java.util.Collections; 17 | import java.util.concurrent.TimeUnit; 18 | 19 | /** 20 | * Represents a 'hop' in the proxying chain, establishes a 'client' to 21 | * communicate with the next server, with a {@link WebSocketProxyClientHandler} 22 | * to copy data from the 'client' to the supplied 'server' session. 23 | */ 24 | public class NextHop { 25 | 26 | private final WebSocketSession webSocketClientSession; 27 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 28 | 29 | public NextHop(WebSocketSession webSocketServerSession, WebSocketProxyClientHandler.ClientOfflineListener listener) { 30 | webSocketClientSession = createWebSocketClientSession(webSocketServerSession, listener); 31 | } 32 | 33 | private WebSocketHttpHeaders getWebSocketHttpHeaders(final WebSocketSession userAgentSession) { 34 | WebSocketHttpHeaders headers = new WebSocketHttpHeaders(); 35 | Principal principal = userAgentSession.getPrincipal(); 36 | if (principal != null && OAuth2Authentication.class.isAssignableFrom(principal.getClass())) { 37 | OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) principal; 38 | OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) oAuth2Authentication.getDetails(); 39 | String accessToken = details.getTokenValue(); 40 | headers.put(HttpHeaders.AUTHORIZATION, Collections.singletonList("Bearer " + accessToken)); 41 | if(logger.isDebugEnabled()) { 42 | logger.debug("Added Oauth2 bearer token authentication header for user " + 43 | principal.getName() + " to web sockets http headers"); 44 | } 45 | } 46 | else { 47 | if(logger.isDebugEnabled()) { 48 | logger.debug("Skipped adding basic authentication header since user session principal is null"); 49 | } 50 | } 51 | return headers; 52 | } 53 | 54 | private WebSocketSession createWebSocketClientSession(WebSocketSession webSocketServerSession, WebSocketProxyClientHandler.ClientOfflineListener listener) { 55 | try { 56 | WebSocketHttpHeaders headers = getWebSocketHttpHeaders(webSocketServerSession); 57 | return new StandardWebSocketClient() 58 | .doHandshake(new WebSocketProxyClientHandler(webSocketServerSession, listener), headers, new URI("ws://localhost:9999")) 59 | .get(1000, TimeUnit.MILLISECONDS); 60 | } catch (Exception e) { 61 | throw new RuntimeException(e); 62 | } 63 | } 64 | 65 | public void sendMessageToNextHop(WebSocketMessage webSocketMessage) throws IOException { 66 | webSocketClientSession.sendMessage(webSocketMessage); 67 | } 68 | 69 | /** 70 | * Triggering client offline. 71 | */ 72 | public void close() throws IOException { 73 | webSocketClientSession.close(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/jwrp/javawebsocketreverseproxy/ProxyWebSocketConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.jwrp.javawebsocketreverseproxy; 2 | 3 | import org.springframework.stereotype.Component; 4 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 5 | import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; 6 | 7 | @Component 8 | public class ProxyWebSocketConfigurer implements WebSocketConfigurer { 9 | 10 | @Override 11 | public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { 12 | registry.addHandler(new WebSocketProxyServerHandler(), "/"); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/jwrp/javawebsocketreverseproxy/WebSocketProxyClientHandler.java: -------------------------------------------------------------------------------- 1 | package com.jwrp.javawebsocketreverseproxy; 2 | 3 | import org.springframework.web.socket.CloseStatus; 4 | import org.springframework.web.socket.WebSocketMessage; 5 | import org.springframework.web.socket.WebSocketSession; 6 | import org.springframework.web.socket.handler.AbstractWebSocketHandler; 7 | 8 | /** 9 | * Copies data from the client to the server session. 10 | */ 11 | public class WebSocketProxyClientHandler extends AbstractWebSocketHandler { 12 | 13 | private final WebSocketSession webSocketServerSession; 14 | private final ClientOfflineListener listener; 15 | 16 | public WebSocketProxyClientHandler(WebSocketSession webSocketServerSession, ClientOfflineListener listener) { 17 | this.webSocketServerSession = webSocketServerSession; 18 | this.listener = listener; 19 | } 20 | 21 | @Override 22 | public void handleMessage(WebSocketSession session, WebSocketMessage webSocketMessage) throws Exception { 23 | webSocketServerSession.sendMessage(webSocketMessage); 24 | } 25 | 26 | @Override 27 | public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { 28 | super.afterConnectionClosed(session, status); 29 | // notify client has been offline, upstream client should exit. 30 | listener.clientOffline(); 31 | } 32 | 33 | public interface ClientOfflineListener{ 34 | void clientOffline(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/jwrp/javawebsocketreverseproxy/WebSocketProxyServerHandler.java: -------------------------------------------------------------------------------- 1 | package com.jwrp.javawebsocketreverseproxy; 2 | 3 | import org.springframework.stereotype.Component; 4 | import org.springframework.web.socket.CloseStatus; 5 | import org.springframework.web.socket.WebSocketMessage; 6 | import org.springframework.web.socket.WebSocketSession; 7 | import org.springframework.web.socket.handler.AbstractWebSocketHandler; 8 | 9 | import java.util.Map; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | 12 | /** 13 | * Handles establishment and tracking of next 'hop', and 14 | * copies data from the current session to the next hop. 15 | */ 16 | @Component 17 | public class WebSocketProxyServerHandler extends AbstractWebSocketHandler { 18 | 19 | private final Map nextHops = new ConcurrentHashMap<>(); 20 | 21 | @Override 22 | public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage webSocketMessage) throws Exception { 23 | getNextHop(webSocketSession).sendMessageToNextHop(webSocketMessage); 24 | } 25 | 26 | @Override 27 | public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus status) throws Exception { 28 | super.afterConnectionClosed(webSocketSession, status); 29 | NextHop nextHop = nextHops.get(webSocketSession.getId()); 30 | if (nextHop != null) { 31 | nextHop.close(); 32 | } 33 | } 34 | 35 | private NextHop getNextHop(WebSocketSession webSocketSession) { 36 | NextHop nextHop = nextHops.get(webSocketSession.getId()); 37 | if (nextHop == null) { 38 | // registering offline listener to avoid nextHop leaks. 39 | nextHop = new NextHop(webSocketSession, () -> nextHops.remove(webSocketSession.getId())); 40 | nextHops.put(webSocketSession.getId(), nextHop); 41 | } 42 | return nextHop; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 7777 -------------------------------------------------------------------------------- /src/test/java/com/jwrp/javawebsocketreverseproxy/JavaWebsocketReverseProxyApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.jwrp.javawebsocketreverseproxy; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class JavaWebsocketReverseProxyApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /test_websocketserver_direct.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install node modules 4 | npm install ws && npm install wscat && npm install http-proxy 5 | 6 | # start the websocket server 7 | node websocketserver.js & 8 | sleep 1 9 | 10 | # use wscat to issue data 11 | (echo -n; sleep 1; echo orville; sleep 1; echo wilbur; sleep 1; ) | ./node_modules/wscat/bin/wscat --connect ws://localhost:9999 12 | 13 | # kill the server 14 | pkill -f websocketserver.js 15 | 16 | -------------------------------------------------------------------------------- /test_websocketserver_javaproxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install node modules 4 | npm install ws && npm install wscat && npm install http-proxy 5 | 6 | # build java proxy 7 | gradle clean build 8 | 9 | # start the node websocket server and java websocket proxy 10 | node websocketserver.js & 11 | java -jar ./build/libs/java-websocket-reverse-proxy-0.0.1-SNAPSHOT.jar & 12 | sleep 4 13 | 14 | # use wscat to issue data 15 | (echo -n; sleep 1; echo orville; sleep 1; echo wilbur; sleep 1; ) | ./node_modules/wscat/bin/wscat --connect ws://localhost:7777 16 | 17 | # kill the servers 18 | pkill -f websocketserver.js 19 | pkill -f java-websocket-reverse-proxy 20 | -------------------------------------------------------------------------------- /test_websocketserver_nodeproxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # install node modules 4 | npm install ws && npm install wscat && npm install http-proxy 5 | 6 | # start the node websocket server and node websocket proxy 7 | node websocketserver.js & 8 | node websocketproxy.js & 9 | sleep 1 10 | 11 | # use wscat to issue data 12 | (echo -n; sleep 1; echo orville; sleep 1; echo wilbur; sleep 1; ) | ./node_modules/wscat/bin/wscat --connect ws://localhost:8888 13 | 14 | # kill the servers 15 | pkill -f websocketserver.js 16 | pkill -f websocketproxy.js 17 | 18 | -------------------------------------------------------------------------------- /websocketproxy.js: -------------------------------------------------------------------------------- 1 | var httpProxy = require('http-proxy'); 2 | var proxy = new httpProxy.createProxyServer({ 3 | target: { 4 | port: 9999 5 | } 6 | }); 7 | 8 | var http = require('http'); 9 | var server = http.createServer(function (req, res) { 10 | proxy.web(req, res); 11 | }); 12 | 13 | server.on('upgrade', function (req, socket, head) { 14 | proxy.ws(req, socket, head); 15 | }); 16 | 17 | const listenOnPort = 8888; 18 | console.log("Proxy server started on port %d", listenOnPort); 19 | console.log("Proxy server will forward requests to port 9999"); 20 | 21 | server.listen(listenOnPort); -------------------------------------------------------------------------------- /websocketserver.js: -------------------------------------------------------------------------------- 1 | const listenOnPort = 9999; 2 | console.log("Websocket server started on port %d", listenOnPort); 3 | console.log("Websocket server will echo back requests in UPPERCASE"); 4 | 5 | var WebSocketServer = require('ws').Server; 6 | var wss = new WebSocketServer({port: listenOnPort}); 7 | 8 | wss.on('connection', function (ws) { 9 | ws.on('message', function (message) { 10 | console.log('Received from client: %s', message); 11 | ws.send(message.toUpperCase()); 12 | }); 13 | }); --------------------------------------------------------------------------------