├── .gitignore ├── src ├── main │ └── java │ │ └── com │ │ └── netiq │ │ └── websockify │ │ ├── FlashPolicyRequest.java │ │ ├── IProxyTargetResolver.java │ │ ├── StaticTargetResolver.java │ │ ├── OutboundWebsocketHandler.java │ │ ├── WebsockifyProxyPipelineFactory.java │ │ ├── OutboundHandler.java │ │ ├── FlashPolicyHandler.java │ │ ├── WebsockifyServer.java │ │ ├── WebsockifySslContext.java │ │ ├── Websockify.java │ │ ├── DirectProxyHandler.java │ │ ├── PortUnificationHandler.java │ │ └── WebsockifyProxyHandler.java └── test │ └── java │ └── com │ └── netiq │ └── websockify │ ├── PortUnificationHandlerTest.java │ └── FlashPolicyHandlerTest.java ├── LICENSE ├── README.markdown ├── logging.properties └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /.settings 3 | /target 4 | /.classpath 5 | /.project 6 | /keystore.jks 7 | /testweb 8 | 9 | /ncmKeystore.jks -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/FlashPolicyRequest.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | public class FlashPolicyRequest { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/IProxyTargetResolver.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | import org.jboss.netty.channel.Channel; 6 | 7 | public interface IProxyTargetResolver { 8 | 9 | public InetSocketAddress resolveTarget ( Channel channel ); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/StaticTargetResolver.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import java.net.InetSocketAddress; 4 | 5 | import org.jboss.netty.channel.Channel; 6 | 7 | public class StaticTargetResolver implements IProxyTargetResolver { 8 | 9 | private InetSocketAddress targetAddress; 10 | 11 | public StaticTargetResolver ( String targetHost, int targetPort ) 12 | { 13 | targetAddress = new InetSocketAddress ( targetHost, targetPort ); 14 | } 15 | 16 | public InetSocketAddress resolveTarget(Channel channel) { 17 | return targetAddress; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/OutboundWebsocketHandler.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import org.jboss.netty.buffer.ChannelBuffer; 4 | import org.jboss.netty.channel.Channel; 5 | import org.jboss.netty.handler.codec.base64.Base64; 6 | import org.jboss.netty.handler.codec.http.websocketx.TextWebSocketFrame; 7 | 8 | public class OutboundWebsocketHandler extends OutboundHandler { 9 | 10 | OutboundWebsocketHandler(Channel inboundChannel, Object trafficLock) { 11 | super ( inboundChannel, trafficLock ); 12 | } 13 | 14 | @Override 15 | protected Object processMessage ( ChannelBuffer buffer ) { 16 | // Encode the message to base64 17 | ChannelBuffer base64Msg = Base64.encode(buffer, false); 18 | return new TextWebSocketFrame(base64Msg); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 NetIQ 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/test/java/com/netiq/websockify/PortUnificationHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import static org.jboss.netty.buffer.ChannelBuffers.wrappedBuffer; 4 | import static org.junit.Assert.*; 5 | import static org.mockito.Mockito.mock; 6 | 7 | import org.jboss.netty.buffer.ChannelBuffer; 8 | import org.jboss.netty.channel.ChannelPipeline; 9 | import org.jboss.netty.channel.socket.ClientSocketChannelFactory; 10 | import org.jboss.netty.handler.codec.embedder.DecoderEmbedder; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | 14 | import com.netiq.websockify.WebsockifyServer.SSLSetting; 15 | 16 | 17 | public class PortUnificationHandlerTest { 18 | 19 | private DecoderEmbedder embedder; 20 | private ClientSocketChannelFactory cf = null; 21 | private IProxyTargetResolver resolver = null; 22 | 23 | @Before 24 | public void setUp() { 25 | cf = mock ( ClientSocketChannelFactory.class ); 26 | resolver = mock ( IProxyTargetResolver.class ); 27 | embedder = new DecoderEmbedder(new PortUnificationHandler(cf, resolver, SSLSetting.OFF, null, null, null, null)); 28 | } 29 | 30 | @Test 31 | public void testFlashRequest() { 32 | String request = ""; 33 | byte[] b = request.getBytes(); 34 | ChannelBuffer buf = wrappedBuffer(b); 35 | embedder.offer(buf); 36 | ChannelPipeline pipeline = embedder.getPipeline(); 37 | assertNotNull(pipeline.get("flash")); 38 | assertTrue(pipeline.get("flash") instanceof FlashPolicyHandler); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/WebsockifyProxyPipelineFactory.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import static org.jboss.netty.channel.Channels.pipeline; 4 | 5 | import org.jboss.netty.channel.ChannelPipeline; 6 | import org.jboss.netty.channel.ChannelPipelineFactory; 7 | import org.jboss.netty.channel.socket.ClientSocketChannelFactory; 8 | 9 | import com.netiq.websockify.WebsockifyServer.SSLSetting; 10 | 11 | public class WebsockifyProxyPipelineFactory implements ChannelPipelineFactory { 12 | 13 | private final ClientSocketChannelFactory cf; 14 | private final IProxyTargetResolver resolver; 15 | private final SSLSetting sslSetting; 16 | private final String keystore; 17 | private final String keystorePassword; 18 | private final String keystoreKeyPassword; 19 | private final String webDirectory; 20 | 21 | public WebsockifyProxyPipelineFactory(ClientSocketChannelFactory cf, IProxyTargetResolver resolver, SSLSetting sslSetting, String keystore, String keystorePassword, String keystoreKeyPassword, String webDirectory) { 22 | this.cf = cf; 23 | this.resolver = resolver; 24 | this.sslSetting = sslSetting; 25 | this.keystore = keystore; 26 | this.keystorePassword = keystorePassword; 27 | this.keystoreKeyPassword = keystoreKeyPassword; 28 | this.webDirectory = webDirectory; 29 | } 30 | 31 | public ChannelPipeline getPipeline() throws Exception { 32 | ChannelPipeline p = pipeline(); // Note the static import. 33 | 34 | p.addLast("unification", new PortUnificationHandler(cf, resolver, sslSetting, keystore, keystorePassword, keystoreKeyPassword, webDirectory)); 35 | return p; 36 | 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Java WebSockify 2 | =============== 3 | 4 | This repository contains a basic Websockify server implementation 5 | written using [Netty](http://netty.io). It's designed to work with the websockify 6 | client found at https://github.com/kanaka/websockify. I specifically am using it as 7 | a server to run [noVNC](http://kanaka.github.com/noVNC/) client. 8 | 9 | Compiling and Running 10 | --------------------- 11 | 12 | This project uses [Maven] (http://maven.apache.org/) for building. Clone the repo, then compile 13 | the project from the command line with: 14 | 15 | mvn compile 16 | 17 | You can run the code from the command line with: 18 | 19 | mvn exec:java -Dexec.mainClass="com.netiq.websockify.Websockify" -Dexec.args=" " 20 | 21 | where you replace `` with the port number you want websockify to listen on and `` and `` 22 | with the host and port that you want to websockify to proxy to. For example: 23 | 24 | mvn exec:java -Dexec.mainClass="com.netiq.websockify.Websockify" -Dexec.args="10900 localhost 5900" 25 | 26 | 27 | Using SSL Encryption 28 | -------------------- 29 | Websockify can encrypt the websocket side of the traffic. To use SSL encryption you first 30 | need a keystore for your server key. To create a self signed key run this on the command line: 31 | 32 | keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -storepass password -validity 360 -keysize 2048 33 | 34 | When it asks "What is your first and last name?" give it the host name that you will be using, for example `localhost`. 35 | 36 | Run Websockify with `encrypt` as your 4th parameter, and specifying the keystore and password as JVM args, for example: 37 | 38 | mvn exec:java -Dexec.mainClass="com.netiq.websockify.Websockify" -Dexec.args="10900 localhost 5900 encrypt" -Dkeystore.file.path=keystore.jks -Dkeystore.file.password=password 39 | 40 | License 41 | ------- 42 | 43 | Everything found in this repo is licensed under an MIT license. See 44 | the `LICENSE` file for specifics. 45 | -------------------------------------------------------------------------------- /logging.properties: -------------------------------------------------------------------------------- 1 | ############################################################ 2 | # Default Logging Configuration File 3 | # 4 | # You can use a different file by specifying a filename 5 | # with the java.util.logging.config.file system property. 6 | # For example java -Djava.util.logging.config.file=myfile 7 | ############################################################ 8 | 9 | ############################################################ 10 | # Global properties 11 | ############################################################ 12 | 13 | # "handlers" specifies a comma separated list of log Handler 14 | # classes. These handlers will be installed during VM startup. 15 | # Note that these classes must be on the system classpath. 16 | # By default we only configure a ConsoleHandler, which will only 17 | # show messages at the INFO and above levels. 18 | handlers= java.util.logging.ConsoleHandler 19 | 20 | # To also add the FileHandler, use the following line instead. 21 | #handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler 22 | 23 | # Default global logging level. 24 | # This specifies which kinds of events are logged across 25 | # all loggers. For any given facility this global level 26 | # can be overriden by a facility specific level 27 | # Note that the ConsoleHandler also has a separate level 28 | # setting to limit messages printed to the console. 29 | .level= INFO 30 | 31 | ############################################################ 32 | # Handler specific properties. 33 | # Describes specific configuration info for Handlers. 34 | ############################################################ 35 | 36 | # default file output is in user's home directory. 37 | java.util.logging.FileHandler.pattern = %h/java%u.log 38 | java.util.logging.FileHandler.limit = 50000 39 | java.util.logging.FileHandler.count = 1 40 | java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter 41 | 42 | # Limit the message that are printed on the console to INFO and above. 43 | java.util.logging.ConsoleHandler.level = FINEST 44 | java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter 45 | 46 | 47 | ############################################################ 48 | # Facility specific properties. 49 | # Provides extra control for each logger. 50 | ############################################################ 51 | 52 | # For example, set the com.xyz.foo logger to only log SEVERE 53 | # messages: 54 | com.netiq.websockify.level = FINEST 55 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | oss-parent 4 | org.sonatype.oss 5 | 7 6 | 7 | 4.0.0 8 | com.netiq 9 | websockify 10 | jar 11 | 1.6-SNAPSHOT 12 | Java Websockify 13 | A websockify server written in Java using Netty. 14 | https://github.com/jribble/Java-Websockify 15 | 16 | 17 | The MIT License (MIT) 18 | http://www.opensource.org/licenses/mit-license.php 19 | repo 20 | 21 | 22 | 23 | scm:git:git@github.com:jribble/Java-Websockify.git 24 | scm:git:git@github.com:jribble/Java-Websockify.git 25 | scm:git:git@github.com:jribble/Java-Websockify.git 26 | 27 | 28 | 29 | jribble 30 | Jarrod Ribble 31 | jribble@netiq.com 32 | NetIQ 33 | http://www.netiq.com 34 | 35 | 36 | 37 | 38 | junit 39 | junit 40 | 4.10 41 | test 42 | 43 | 44 | mockito-all 45 | org.mockito 46 | 1.8.5 47 | test 48 | 49 | 50 | io.netty 51 | netty 52 | 3.3.0.Final 53 | 54 | 55 | args4j 56 | args4j 57 | 2.0.16 58 | 59 | 60 | 61 | 62 | 63 | org.apache.maven.plugins 64 | maven-release-plugin 65 | 2.0-beta-9 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-compiler-plugin 70 | 2.5.1 71 | 72 | 5 73 | 5 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/OutboundHandler.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import java.util.logging.Logger; 4 | 5 | import org.jboss.netty.buffer.ChannelBuffer; 6 | import org.jboss.netty.channel.Channel; 7 | import org.jboss.netty.channel.ChannelHandlerContext; 8 | import org.jboss.netty.channel.ChannelStateEvent; 9 | import org.jboss.netty.channel.ExceptionEvent; 10 | import org.jboss.netty.channel.MessageEvent; 11 | import org.jboss.netty.channel.SimpleChannelUpstreamHandler; 12 | 13 | public class OutboundHandler extends SimpleChannelUpstreamHandler { 14 | 15 | private final Channel inboundChannel; 16 | private final Object trafficLock; 17 | 18 | OutboundHandler(Channel inboundChannel, Object trafficLock) { 19 | this.inboundChannel = inboundChannel; 20 | this.trafficLock = trafficLock; 21 | } 22 | 23 | protected Object processMessage ( ChannelBuffer buffer ) { 24 | return buffer; 25 | } 26 | 27 | @Override 28 | public void messageReceived(ChannelHandlerContext ctx, final MessageEvent e) 29 | throws Exception { 30 | ChannelBuffer msg = (ChannelBuffer) e.getMessage(); 31 | Object outMsg = processMessage ( msg ); 32 | synchronized (trafficLock) { 33 | inboundChannel.write(outMsg); 34 | // If inboundChannel is saturated, do not read until notified in 35 | // HexDumpProxyInboundHandler.channelInterestChanged(). 36 | if (!inboundChannel.isWritable()) { 37 | e.getChannel().setReadable(false); 38 | } 39 | } 40 | } 41 | 42 | @Override 43 | public void channelInterestChanged(ChannelHandlerContext ctx, 44 | ChannelStateEvent e) throws Exception { 45 | // If outboundChannel is not saturated anymore, continue accepting 46 | // the incoming traffic from the inboundChannel. 47 | synchronized (trafficLock) { 48 | if (e.getChannel().isWritable()) { 49 | inboundChannel.setReadable(true); 50 | } 51 | } 52 | } 53 | 54 | @Override 55 | public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) 56 | throws Exception { 57 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).info("Outbound proxy connection to " + ctx.getChannel().getRemoteAddress() + " closed."); 58 | WebsockifyProxyHandler.closeOnFlush(inboundChannel); 59 | } 60 | 61 | @Override 62 | public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) 63 | throws Exception { 64 | e.getCause().printStackTrace(); 65 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).severe("Exception on outbound proxy connection to " + e.getChannel().getRemoteAddress() + ": " + e.getCause().getMessage()); 66 | WebsockifyProxyHandler.closeOnFlush(e.getChannel()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/FlashPolicyHandler.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import org.jboss.netty.channel.ChannelFutureListener; 4 | 5 | import org.jboss.netty.handler.codec.frame.FrameDecoder; 6 | import org.jboss.netty.channel.ChannelHandlerContext; 7 | import org.jboss.netty.channel.Channel; 8 | import org.jboss.netty.buffer.ChannelBuffer; 9 | import org.jboss.netty.buffer.ChannelBuffers; 10 | import org.jboss.netty.util.CharsetUtil; 11 | 12 | /** 13 | * A Flash policy file handler 14 | * Will detect connection attempts made by Adobe Flash clients and return a policy file response 15 | * 16 | * After the policy has been sent, it will instantly close the connection. 17 | * If the first bytes sent are not a policy file request the handler will simply remove itself 18 | * from the pipeline. 19 | * 20 | * Read more at http://www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html 21 | * 22 | * Example usage: 23 | * 24 | * ChannelPipeline pipeline = Channels.pipeline(); 25 | * pipeline.addLast("flashPolicy", new FlashPolicyHandler()); 26 | * pipeline.addLast("decoder", new MyProtocolDecoder()); 27 | * pipeline.addLast("encoder", new MyProtocolEncoder()); 28 | * pipeline.addLast("handler", new MyBusinessLogicHandler()); 29 | * 30 | */ 31 | public class FlashPolicyHandler extends FrameDecoder { 32 | /*package*/ static final String XML = ""; 33 | private ChannelBuffer policyResponse = ChannelBuffers.copiedBuffer(XML, CharsetUtil.UTF_8); 34 | 35 | /** 36 | * Creates a handler allowing access from any domain and any port 37 | */ 38 | public FlashPolicyHandler() { 39 | super(); 40 | } 41 | 42 | /** 43 | * Create a handler with a custom XML response. Useful for defining your own domains and ports. 44 | * @param policyResponse Response XML to be passed back to a connecting client 45 | */ 46 | public FlashPolicyHandler(ChannelBuffer policyResponse) { 47 | super(); 48 | this.policyResponse = policyResponse; 49 | } 50 | 51 | protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception { 52 | if (buffer.readableBytes() < 2) { 53 | return null; 54 | } 55 | 56 | final int magic1 = buffer.getUnsignedByte(buffer.readerIndex()); 57 | final int magic2 = buffer.getUnsignedByte(buffer.readerIndex() + 1); 58 | boolean isFlashPolicyRequest = (magic1 == '<' && magic2 == 'p'); 59 | 60 | if (isFlashPolicyRequest) { 61 | buffer.skipBytes(buffer.readableBytes()); // Discard everything 62 | channel.write(policyResponse).addListener(ChannelFutureListener.CLOSE); 63 | return new FlashPolicyRequest(); 64 | } 65 | 66 | return null; 67 | } 68 | } -------------------------------------------------------------------------------- /src/test/java/com/netiq/websockify/FlashPolicyHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import java.util.Random; 4 | 5 | import static org.hamcrest.core.Is.is; 6 | import static org.jboss.netty.buffer.ChannelBuffers.wrappedBuffer; 7 | import static org.junit.Assert.*; 8 | 9 | import org.jboss.netty.buffer.ChannelBuffer; 10 | import org.jboss.netty.buffer.ChannelBuffers; 11 | import org.jboss.netty.handler.codec.embedder.DecoderEmbedder; 12 | import org.jboss.netty.util.CharsetUtil; 13 | import org.junit.Before; 14 | import org.junit.Test; 15 | 16 | 17 | public class FlashPolicyHandlerTest { 18 | 19 | private DecoderEmbedder embedder; 20 | 21 | @Before 22 | public void setUp() { 23 | embedder = new DecoderEmbedder(new FlashPolicyHandler()); 24 | } 25 | 26 | @Test 27 | public void testDecode() { 28 | String request = ""; 29 | byte[] b = request.getBytes(); 30 | ChannelBuffer buf = wrappedBuffer(b); 31 | embedder.offer(buf); 32 | // the first object on the list is not the FlashPolicyRequest object 33 | Object first = embedder.poll(); 34 | ChannelBuffer response = (ChannelBuffer) first; 35 | String resp = new String(response.array(), 0, response.readableBytes()); 36 | assertTrue(FlashPolicyHandler.XML.equals(resp)); 37 | FlashPolicyRequest fpr = embedder.poll(); 38 | assertTrue( fpr instanceof FlashPolicyRequest); 39 | } 40 | 41 | @Test 42 | public void testDecodeCustomResponse() { 43 | String XML = ""; 44 | ChannelBuffer policyResponse = ChannelBuffers.copiedBuffer(XML, CharsetUtil.UTF_8); 45 | embedder = new DecoderEmbedder(new FlashPolicyHandler(policyResponse)); 46 | 47 | String request = ""; 48 | byte[] b = request.getBytes(); 49 | ChannelBuffer buf = wrappedBuffer(b); 50 | embedder.offer(buf); 51 | // the first object on the list is not the FlashPolicyRequest object 52 | Object first = embedder.poll(); 53 | ChannelBuffer response = (ChannelBuffer) first; 54 | String resp = new String(response.array(), 0, response.readableBytes()); 55 | assertTrue(XML.equals(resp)); 56 | FlashPolicyRequest fpr = embedder.poll(); 57 | assertTrue( fpr instanceof FlashPolicyRequest); 58 | } 59 | 60 | @Test 61 | public void testDecodeEmpty() { 62 | byte[] b = new byte[2048]; 63 | new Random().nextBytes(b); 64 | ChannelBuffer buf = wrappedBuffer(b); 65 | embedder.offer(buf); 66 | assertTrue(embedder.pollAll().length <= 0); 67 | } 68 | 69 | @Test 70 | public void testDecodeOtherType() { 71 | String str = "Meep!"; 72 | embedder.offer(str); 73 | assertThat(embedder.poll(), is((Object) str)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/WebsockifyServer.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import org.jboss.netty.bootstrap.ServerBootstrap; 4 | import org.jboss.netty.channel.Channel; 5 | import org.jboss.netty.channel.socket.ClientSocketChannelFactory; 6 | import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; 7 | import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; 8 | 9 | import java.io.IOException; 10 | import java.net.InetSocketAddress; 11 | import java.security.KeyManagementException; 12 | import java.security.KeyStoreException; 13 | import java.security.NoSuchAlgorithmException; 14 | import java.security.UnrecoverableKeyException; 15 | import java.security.cert.CertificateException; 16 | import java.util.concurrent.Executor; 17 | import java.util.concurrent.Executors; 18 | 19 | public class WebsockifyServer { 20 | private Executor executor; 21 | private ServerBootstrap sb; 22 | private ClientSocketChannelFactory cf; 23 | private Channel serverChannel = null; 24 | 25 | public enum SSLSetting { OFF, ON, REQUIRED }; 26 | 27 | public WebsockifyServer ( ) 28 | { 29 | // Configure the bootstrap. 30 | executor = Executors.newCachedThreadPool(); 31 | sb = new ServerBootstrap(new NioServerSocketChannelFactory(executor, executor)); 32 | 33 | // Set up the event pipeline factory. 34 | cf = new NioClientSocketChannelFactory(executor, executor); 35 | } 36 | 37 | public void connect ( int localPort, String remoteHost, int remotePort ) 38 | { 39 | connect ( localPort, remoteHost, remotePort, null ); 40 | } 41 | 42 | public void connect ( int localPort, String remoteHost, int remotePort, String webDirectory ) 43 | { 44 | connect ( localPort, remoteHost, remotePort, SSLSetting.OFF, null, null, null, webDirectory ); 45 | } 46 | 47 | public void connect ( int localPort, String remoteHost, int remotePort, SSLSetting sslSetting, String keystore, String keystorePassword, String keystoreKeyPassword, String webDirectory ) 48 | { 49 | connect ( localPort, new StaticTargetResolver ( remoteHost, remotePort ), sslSetting, keystore, keystorePassword, keystoreKeyPassword, webDirectory ); 50 | } 51 | 52 | public void connect ( int localPort, IProxyTargetResolver resolver ) 53 | { 54 | connect ( localPort, resolver, null ); 55 | } 56 | 57 | public void connect ( int localPort, IProxyTargetResolver resolver, String webDirectory ) 58 | { 59 | connect ( localPort, resolver, SSLSetting.OFF, null, null, null, webDirectory ); 60 | } 61 | 62 | public void connect ( int localPort, IProxyTargetResolver resolver, SSLSetting sslSetting, String keystore, String keystorePassword, String keystoreKeyPassword, String webDirectory ) 63 | { 64 | if ( serverChannel != null ) 65 | { 66 | close ( ); 67 | } 68 | 69 | sb.setPipelineFactory(new WebsockifyProxyPipelineFactory(cf, resolver, sslSetting, keystore, keystorePassword, keystoreKeyPassword, webDirectory)); 70 | 71 | // Start up the server. 72 | serverChannel = sb.bind(new InetSocketAddress(localPort)); 73 | 74 | } 75 | 76 | public void close ( ) 77 | { 78 | if ( serverChannel != null && serverChannel.isBound() ) 79 | { 80 | serverChannel.close(); 81 | serverChannel = null; 82 | } 83 | } 84 | 85 | public Channel getChannel ( ) 86 | { 87 | return serverChannel; 88 | } 89 | 90 | /** 91 | * Validates that a keystore with the given parameters exists and can be used for an SSL context. 92 | * @param keystore - path to the keystore file 93 | * @param password - password to the keystore file 94 | * @param keyPassword - password to the private key in the keystore file 95 | * @return null if valid, otherwise a string describing the error. 96 | */ 97 | public void validateKeystore ( String keystore, String password, String keyPassword ) 98 | throws KeyManagementException, UnrecoverableKeyException, IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException 99 | { 100 | WebsockifySslContext.validateKeystore(keystore, password, keyPassword); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/WebsockifySslContext.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import java.io.FileInputStream; 4 | import java.io.FileNotFoundException; 5 | import java.io.IOException; 6 | import java.security.KeyManagementException; 7 | import java.security.KeyStore; 8 | import java.security.KeyStoreException; 9 | import java.security.NoSuchAlgorithmException; 10 | import java.security.Security; 11 | import java.security.UnrecoverableKeyException; 12 | import java.security.cert.CertificateException; 13 | import java.util.HashMap; 14 | import java.util.logging.Logger; 15 | 16 | import javax.net.ssl.KeyManagerFactory; 17 | import javax.net.ssl.SSLContext; 18 | 19 | import org.jboss.netty.logging.InternalLogger; 20 | import org.jboss.netty.logging.InternalLoggerFactory; 21 | 22 | public class WebsockifySslContext { 23 | 24 | private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebsockifySslContext.class); 25 | private static final String PROTOCOL = "TLS"; 26 | private SSLContext _serverContext; 27 | 28 | /** 29 | * Returns the singleton instance for this class 30 | */ 31 | public static WebsockifySslContext getInstance(String keystore, String password, String keyPassword) { 32 | WebsockifySslContext context = SingletonHolder.INSTANCE_MAP.get(keystore); 33 | if ( context == null ) 34 | { 35 | context = new WebsockifySslContext ( keystore, password, keyPassword ); 36 | SingletonHolder.INSTANCE_MAP.put(keystore, context); 37 | } 38 | return context; 39 | } 40 | 41 | /** 42 | * SingletonHolder is loaded on the first execution of Singleton.getInstance() or the first access to 43 | * SingletonHolder.INSTANCE, not before. 44 | * 45 | * See http://en.wikipedia.org/wiki/Singleton_pattern 46 | */ 47 | private static class SingletonHolder { 48 | public static final HashMap INSTANCE_MAP = new HashMap(); 49 | } 50 | /** 51 | * Constructor for singleton 52 | */ 53 | private WebsockifySslContext(String keystore, String password) { 54 | this ( keystore, password, password ); 55 | } 56 | 57 | /** 58 | * Constructor for singleton 59 | */ 60 | private WebsockifySslContext(String keystore, String password, String keyPassword) { 61 | try { 62 | SSLContext serverContext = null; 63 | try { 64 | serverContext = getSSLContext(keystore, password, keyPassword); 65 | } catch (Exception e) { 66 | Logger.getLogger(WebsockifySslContext.class.getName()).severe("Error creating SSL context for keystore " + keystore + ": " + e.getMessage()); 67 | throw new Error("Failed to initialize the server-side SSLContext", e); 68 | } 69 | _serverContext = serverContext; 70 | } catch (Exception ex) { 71 | logger.error("Error initializing SslContextManager. " + ex.getMessage(), ex); 72 | System.exit(1); 73 | 74 | } 75 | } 76 | 77 | /** 78 | * Returns the server context with server side key store 79 | */ 80 | public SSLContext getServerContext() { 81 | return _serverContext; 82 | } 83 | 84 | private static SSLContext getSSLContext ( String keyStoreFilePath, String password, String keyPassword ) 85 | throws KeyStoreException, FileNotFoundException, CertificateException, NoSuchAlgorithmException, 86 | IOException, UnrecoverableKeyException, KeyManagementException 87 | { 88 | // Key store (Server side certificate) 89 | String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm"); 90 | if (algorithm == null) { 91 | algorithm = "SunX509"; 92 | } 93 | 94 | KeyStore ks = KeyStore.getInstance("JKS"); 95 | FileInputStream fin = new FileInputStream(keyStoreFilePath); 96 | ks.load(fin, password.toCharArray()); 97 | 98 | // Set up key manager factory to use our key store 99 | // Assume key password is the same as the key store file 100 | // password 101 | KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); 102 | kmf.init(ks, keyPassword.toCharArray()); 103 | 104 | // Initialise the SSLContext to work with our key managers. 105 | SSLContext context = SSLContext.getInstance(PROTOCOL); 106 | context.init(kmf.getKeyManagers(), null, null); 107 | return context; 108 | } 109 | 110 | /** 111 | * Validates that a keystore with the given parameters exists and can be used for an SSL context. 112 | * @param keystore - path to the keystore file 113 | * @param password - password to the keystore file 114 | * @param keyPassword - password to the private key in the keystore file 115 | * @return null if valid, otherwise a string describing the error. 116 | */ 117 | public static void validateKeystore ( String keystore, String password, String keyPassword ) 118 | throws KeyManagementException, UnrecoverableKeyException, IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException 119 | { 120 | 121 | getSSLContext(keystore, password, keyPassword); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/Websockify.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import java.io.PrintStream; 4 | 5 | import org.kohsuke.args4j.Argument; 6 | import org.kohsuke.args4j.CmdLineException; 7 | import org.kohsuke.args4j.CmdLineParser; 8 | import org.kohsuke.args4j.Option; 9 | 10 | import com.netiq.websockify.WebsockifyServer.SSLSetting; 11 | 12 | public class Websockify { 13 | 14 | @Option(name="--help",usage="show this help message and quit") 15 | private boolean showHelp = false; 16 | 17 | @Option(name="--enable-ssl",usage="enable SSL") 18 | private boolean enableSSL = false; 19 | 20 | @Option(name="--ssl-only",usage="disallow non-encrypted connections") 21 | private boolean requireSSL = false; 22 | 23 | @Option(name="--dir",usage="run webserver on same port. Serve files from specified directory.") 24 | private String webDirectory = null; 25 | 26 | @Option(name="--keystore",usage="path to a java keystore file. Required for SSL.") 27 | private String keystore = null; 28 | 29 | @Option(name="--keystore-password",usage="password to the java keystore file. Required for SSL.") 30 | private String keystorePassword = null; 31 | 32 | @Option(name="--keystore-key-password",usage="password to the private key in the java keystore file. If not specified the keystore-password value will be used.") 33 | private String keystoreKeyPassword = null; 34 | 35 | @Option(name="--direct-proxy-timeout",usage="connection timeout before a direct proxy connection is established in milliseconds. Default is 5000 (5 seconds). With the VNC protocol the server sends the first message. This means that a client that wants a direct proxy connection will connect and not send a message. Websockify will wait the specified number of milliseconds for an incoming connection to send a message. If no message is recieved it initiates a direct proxy connection. Setting this value too low will cause connection attempts that aren't direct proxy connections to fail. Set this to 0 to disable direct proxy connections.") 36 | private int directProxyTimeout = 5000; 37 | 38 | @Argument(index=0,metaVar="source_port",usage="(required) local port the websockify server will listen on",required=true) 39 | private int sourcePort; 40 | 41 | @Argument(index=1,metaVar="target_host",usage="(required) host the websockify server will proxy to",required=true) 42 | private String targetHost; 43 | 44 | @Argument(index=2,metaVar="target_port",usage="(required) port the websockify server will proxy to",required=true) 45 | private int targetPort; 46 | 47 | private CmdLineParser parser; 48 | 49 | public Websockify ( ) { 50 | parser = new CmdLineParser(this); 51 | } 52 | 53 | public void printUsage ( PrintStream out ) { 54 | out.println("Usage:"); 55 | out.println(" java -jar websockify.jar [options] source_port target_addr target_port"); 56 | out.println(); 57 | out.println("Options:"); 58 | parser.printUsage(out); 59 | out.println(); 60 | out.println("Example:"); 61 | out.println(" java -jar websockify.jar 5900 server.example.net 5900"); 62 | } 63 | 64 | public static void main(String[] args) throws Exception { 65 | new Websockify().doMain(args); 66 | } 67 | 68 | public void doMain(String[] args) throws Exception { 69 | parser.setUsageWidth(80); 70 | 71 | try { 72 | parser.parseArgument(args); 73 | } 74 | catch (CmdLineException e) { 75 | System.err.println(e.getMessage()); 76 | printUsage(System.err); 77 | return; 78 | } 79 | 80 | if ( showHelp ) { 81 | printUsage(System.out); 82 | return; 83 | } 84 | 85 | SSLSetting sslSetting = SSLSetting.OFF; 86 | if ( requireSSL ) sslSetting = SSLSetting.REQUIRED; 87 | else if ( enableSSL ) sslSetting = SSLSetting.ON; 88 | 89 | if ( sslSetting != SSLSetting.OFF ) { 90 | if (keystore == null || keystore.isEmpty()) { 91 | System.err.println("No keystore specified."); 92 | printUsage(System.err); 93 | System.exit(1); 94 | } 95 | 96 | if (keystorePassword == null || keystorePassword.isEmpty()) { 97 | System.err.println("No keystore password specified."); 98 | printUsage(System.err); 99 | System.exit(1); 100 | } 101 | 102 | if (keystoreKeyPassword == null || keystoreKeyPassword.isEmpty()) { 103 | keystoreKeyPassword = keystorePassword; 104 | } 105 | 106 | try 107 | { 108 | WebsockifySslContext.validateKeystore(keystore, keystorePassword, keystoreKeyPassword); 109 | } 110 | catch ( Exception e ) 111 | { 112 | System.err.println("Error validating keystore: " + e.getMessage() ); 113 | printUsage(System.err); 114 | System.exit(2); 115 | } 116 | } 117 | 118 | System.out.println( 119 | "Websockify Proxying *:" + sourcePort + " to " + 120 | targetHost + ':' + targetPort + " ..."); 121 | if(sslSetting != SSLSetting.OFF) System.out.println("SSL is " + (sslSetting == SSLSetting.REQUIRED ? "required." : "enabled.")); 122 | 123 | PortUnificationHandler.setConnectionToFirstMessageTimeout(directProxyTimeout); 124 | 125 | WebsockifyServer wss = new WebsockifyServer ( ); 126 | wss.connect ( sourcePort, targetHost, targetPort, sslSetting, keystore, keystorePassword, keystoreKeyPassword, webDirectory ); 127 | 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/DirectProxyHandler.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.util.logging.Logger; 5 | 6 | import org.jboss.netty.bootstrap.ClientBootstrap; 7 | import org.jboss.netty.buffer.ChannelBuffer; 8 | import org.jboss.netty.buffer.ChannelBuffers; 9 | import org.jboss.netty.channel.Channel; 10 | import org.jboss.netty.channel.ChannelFuture; 11 | import org.jboss.netty.channel.ChannelFutureListener; 12 | import org.jboss.netty.channel.ChannelHandlerContext; 13 | import org.jboss.netty.channel.ChannelStateEvent; 14 | import org.jboss.netty.channel.ExceptionEvent; 15 | import org.jboss.netty.channel.MessageEvent; 16 | import org.jboss.netty.channel.SimpleChannelUpstreamHandler; 17 | import org.jboss.netty.channel.socket.ClientSocketChannelFactory; 18 | 19 | public class DirectProxyHandler extends SimpleChannelUpstreamHandler { 20 | 21 | private final ClientSocketChannelFactory cf; 22 | private final IProxyTargetResolver resolver; 23 | 24 | // This lock guards against the race condition that overrides the 25 | // OP_READ flag incorrectly. 26 | // See the related discussion: http://markmail.org/message/x7jc6mqx6ripynqf 27 | final Object trafficLock = new Object(); 28 | 29 | private volatile Channel outboundChannel; 30 | 31 | public DirectProxyHandler(final Channel inboundChannel, ClientSocketChannelFactory cf, IProxyTargetResolver resolver) { 32 | this.cf = cf; 33 | this.resolver = resolver; 34 | this.outboundChannel = null; 35 | 36 | ensureTargetConnection ( inboundChannel, false, null ); 37 | } 38 | 39 | private void ensureTargetConnection(final Channel inboundChannel, boolean websocket, final Object sendMsg) { 40 | if(outboundChannel == null) { 41 | // Suspend incoming traffic until connected to the remote host. 42 | inboundChannel.setReadable(false); 43 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).info("Inbound proxy connection from " + inboundChannel.getRemoteAddress() + "."); 44 | 45 | // resolve the target 46 | final InetSocketAddress target = resolver.resolveTarget(inboundChannel); 47 | if ( target == null ) 48 | { 49 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).severe("Connection from " + inboundChannel.getRemoteAddress() + " failed to resolve target."); 50 | // there is no target 51 | inboundChannel.close(); 52 | return; 53 | } 54 | 55 | // Start the connection attempt. 56 | ClientBootstrap cb = new ClientBootstrap(cf); 57 | if ( websocket ) { 58 | cb.getPipeline().addLast("handler", new OutboundWebsocketHandler(inboundChannel, trafficLock)); 59 | } 60 | else { 61 | cb.getPipeline().addLast("handler", new OutboundHandler(inboundChannel, trafficLock)); 62 | } 63 | ChannelFuture f = cb.connect(target); 64 | 65 | outboundChannel = f.getChannel(); 66 | if ( sendMsg != null ) outboundChannel.write(sendMsg); 67 | f.addListener(new ChannelFutureListener() { 68 | public void operationComplete(ChannelFuture future) throws Exception { 69 | if (future.isSuccess()) { 70 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).info("Created outbound connection to " + target + "."); 71 | // Connection attempt succeeded: 72 | // Begin to accept incoming traffic. 73 | inboundChannel.setReadable(true); 74 | } else { 75 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).severe("Failed to create outbound connection to " + target + "."); 76 | // Close the connection if the connection attempt has failed. 77 | inboundChannel.close(); 78 | } 79 | } 80 | }); 81 | } else { 82 | if ( sendMsg != null ) outboundChannel.write(sendMsg); 83 | } 84 | } 85 | 86 | // In cases where there will be a direct VNC proxy connection 87 | // The client won't send any message so connect directly on channel open 88 | @Override 89 | public void channelOpen(final ChannelHandlerContext ctx, final ChannelStateEvent e) 90 | throws Exception { 91 | try { 92 | // make the proxy connection 93 | ensureTargetConnection ( e.getChannel(), false, null ); 94 | } catch (Exception ex) { 95 | // target connection failed, so close the client connection 96 | e.getChannel().close(); 97 | ex.printStackTrace(); 98 | } 99 | } 100 | 101 | @Override 102 | public void messageReceived(ChannelHandlerContext ctx, final MessageEvent e) 103 | throws Exception { 104 | Object msg = e.getMessage(); 105 | handleVncDirect(ctx, (ChannelBuffer) msg, e); 106 | } 107 | 108 | private void handleVncDirect(ChannelHandlerContext ctx, ChannelBuffer buffer, final MessageEvent e) throws Exception { 109 | // ensure the target connection is open and send the data 110 | ensureTargetConnection(e.getChannel(), false, buffer); 111 | } 112 | 113 | @Override 114 | public void channelInterestChanged(ChannelHandlerContext ctx, 115 | ChannelStateEvent e) throws Exception { 116 | // If inboundChannel is not saturated anymore, continue accepting 117 | // the incoming traffic from the outboundChannel. 118 | synchronized (trafficLock) { 119 | if (e.getChannel().isWritable() && outboundChannel != null) { 120 | outboundChannel.setReadable(true); 121 | } 122 | } 123 | } 124 | 125 | @Override 126 | public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) 127 | throws Exception { 128 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).info("Inbound proxy connection from " + ctx.getChannel().getRemoteAddress() + " closed."); 129 | if (outboundChannel != null) { 130 | closeOnFlush(outboundChannel); 131 | } 132 | } 133 | 134 | @Override 135 | public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) 136 | throws Exception { 137 | e.getCause().printStackTrace(); 138 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).severe("Exception on inbound proxy connection from " + e.getChannel().getRemoteAddress() + ": " + e.getCause().getMessage()); 139 | closeOnFlush(e.getChannel()); 140 | } 141 | 142 | /** 143 | * Closes the specified channel after all queued write requests are flushed. 144 | */ 145 | static void closeOnFlush(Channel ch) { 146 | if (ch.isConnected()) { 147 | ch.write(ChannelBuffers.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/PortUnificationHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package com.netiq.websockify; 17 | 18 | import java.util.Timer; 19 | import java.util.TimerTask; 20 | import java.util.logging.Logger; 21 | 22 | import javax.net.ssl.SSLEngine; 23 | 24 | import org.jboss.netty.buffer.ChannelBuffer; 25 | import org.jboss.netty.channel.Channel; 26 | import org.jboss.netty.channel.ChannelHandlerContext; 27 | import org.jboss.netty.channel.ChannelPipeline; 28 | import org.jboss.netty.channel.ChannelStateEvent; 29 | import org.jboss.netty.channel.ExceptionEvent; 30 | import org.jboss.netty.channel.socket.ClientSocketChannelFactory; 31 | import org.jboss.netty.handler.codec.frame.FrameDecoder; 32 | import org.jboss.netty.handler.codec.http.HttpChunkAggregator; 33 | import org.jboss.netty.handler.codec.http.HttpRequestDecoder; 34 | import org.jboss.netty.handler.codec.http.HttpResponseEncoder; 35 | import org.jboss.netty.handler.ssl.SslHandler; 36 | import org.jboss.netty.handler.stream.ChunkedWriteHandler; 37 | 38 | import com.netiq.websockify.WebsockifyServer.SSLSetting; 39 | 40 | /** 41 | * Manipulates the current pipeline dynamically to switch protocols or enable 42 | * SSL or GZIP. 43 | */ 44 | public class PortUnificationHandler extends FrameDecoder { 45 | protected static long connectionToFirstMessageTimeout = 5000; 46 | 47 | private final ClientSocketChannelFactory cf; 48 | private final IProxyTargetResolver resolver; 49 | private final SSLSetting sslSetting; 50 | private final String keystore; 51 | private final String keystorePassword; 52 | private final String keystoreKeyPassword; 53 | private final String webDirectory; 54 | private Timer msgTimer = null; 55 | private long directConnectTimerStart = 0; 56 | 57 | private PortUnificationHandler(ClientSocketChannelFactory cf, IProxyTargetResolver resolver, SSLSetting sslSetting, String keystore, String keystorePassword, String keystoreKeyPassword, String webDirectory, final ChannelHandlerContext ctx) { 58 | this ( cf, resolver, sslSetting, keystore, keystorePassword, keystoreKeyPassword, webDirectory); 59 | startDirectConnectionTimer(ctx); 60 | } 61 | 62 | public PortUnificationHandler(ClientSocketChannelFactory cf, IProxyTargetResolver resolver, SSLSetting sslSetting, String keystore, String keystorePassword, String keystoreKeyPassword, String webDirectory) { 63 | this.cf = cf; 64 | this.resolver = resolver; 65 | this.sslSetting = sslSetting; 66 | this.keystore = keystore; 67 | this.keystorePassword = keystorePassword; 68 | this.keystoreKeyPassword = keystoreKeyPassword; 69 | this.webDirectory = webDirectory; 70 | } 71 | 72 | public static long getConnectionToFirstMessageTimeout() { 73 | return connectionToFirstMessageTimeout; 74 | } 75 | 76 | public static void setConnectionToFirstMessageTimeout(long connectionToFirstMessageTimeout) { 77 | PortUnificationHandler.connectionToFirstMessageTimeout = connectionToFirstMessageTimeout; 78 | } 79 | 80 | // In cases where there will be a direct VNC proxy connection 81 | // The client won't send any message because VNC servers talk first 82 | // So we'll set a timer on the connection - if there's no message by the time 83 | // the timer fires we'll create the proxy connection to the target 84 | @Override 85 | public void channelOpen(final ChannelHandlerContext ctx, final ChannelStateEvent e) 86 | throws Exception { 87 | startDirectConnectionTimer( ctx ); 88 | } 89 | 90 | private void startDirectConnectionTimer ( final ChannelHandlerContext ctx ) 91 | { 92 | // cancel any outstanding timer 93 | cancelDirectConnectionTimer ( ); 94 | 95 | // direct proxy connection disabled 96 | if ( connectionToFirstMessageTimeout <= 0 ) return; 97 | 98 | directConnectTimerStart = System.currentTimeMillis(); 99 | 100 | // cancelling a timer makes it unusable again, so we have to create another one 101 | msgTimer = new Timer(); 102 | msgTimer.schedule(new TimerTask ( ) { 103 | 104 | @Override 105 | public void run() { 106 | switchToDirectProxy(ctx); 107 | } 108 | 109 | }, connectionToFirstMessageTimeout); 110 | 111 | } 112 | 113 | private void cancelDirectConnectionTimer ( ) 114 | { 115 | if ( directConnectTimerStart > 0 ) { 116 | long directConnectTimerCancel = System.currentTimeMillis(); 117 | Logger.getLogger(PortUnificationHandler.class.getName()).finer("Direct connection timer canceled after " + (directConnectTimerCancel - directConnectTimerStart) + " milliseconds."); 118 | } 119 | 120 | if ( msgTimer != null ) { 121 | msgTimer.cancel(); 122 | msgTimer = null; 123 | } 124 | 125 | } 126 | 127 | @Override 128 | protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) throws Exception { 129 | // Will use the first two bytes to detect a protocol. 130 | if (buffer.readableBytes() < 2) { 131 | return null; 132 | } 133 | 134 | cancelDirectConnectionTimer ( ); 135 | 136 | final int magic1 = buffer.getUnsignedByte(buffer.readerIndex()); 137 | final int magic2 = buffer.getUnsignedByte(buffer.readerIndex() + 1); 138 | 139 | if (isSsl(magic1)) { 140 | enableSsl(ctx); 141 | } else if ( isFlashPolicy ( magic1, magic2 ) ) { 142 | switchToFlashPolicy(ctx); 143 | } else { 144 | switchToWebsocketProxy(ctx); 145 | } 146 | 147 | // Forward the current read buffer as is to the new handlers. 148 | return buffer.readBytes(buffer.readableBytes()); 149 | } 150 | 151 | private boolean isSsl(int magic1) { 152 | if (sslSetting != SSLSetting.OFF) { 153 | switch (magic1) { 154 | case 20: case 21: case 22: case 23: case 255: 155 | return true; 156 | default: 157 | return magic1 >= 128; 158 | } 159 | } 160 | return false; 161 | } 162 | 163 | private boolean isFlashPolicy(int magic1, int magic2 ) { 164 | return (magic1 == '<' && magic2 == 'p'); 165 | } 166 | 167 | private void enableSsl(ChannelHandlerContext ctx) { 168 | ChannelPipeline p = ctx.getPipeline(); 169 | 170 | Logger.getLogger(PortUnificationHandler.class.getName()).fine("SSL request from " + ctx.getChannel().getRemoteAddress() + "."); 171 | 172 | SSLEngine engine = WebsockifySslContext.getInstance(keystore, keystorePassword, keystoreKeyPassword).getServerContext().createSSLEngine(); 173 | engine.setUseClientMode(false); 174 | 175 | p.addLast("ssl", new SslHandler(engine)); 176 | p.addLast("unificationA", new PortUnificationHandler(cf, resolver, SSLSetting.OFF, keystore, keystorePassword, keystoreKeyPassword, webDirectory, ctx)); 177 | p.remove(this); 178 | } 179 | 180 | private void switchToWebsocketProxy(ChannelHandlerContext ctx) { 181 | ChannelPipeline p = ctx.getPipeline(); 182 | 183 | Logger.getLogger(PortUnificationHandler.class.getName()).fine("Websocket proxy request from " + ctx.getChannel().getRemoteAddress() + "."); 184 | 185 | p.addLast("decoder", new HttpRequestDecoder()); 186 | p.addLast("aggregator", new HttpChunkAggregator(65536)); 187 | p.addLast("encoder", new HttpResponseEncoder()); 188 | p.addLast("chunkedWriter", new ChunkedWriteHandler()); 189 | p.addLast("handler", new WebsockifyProxyHandler(cf, resolver, webDirectory)); 190 | p.remove(this); 191 | } 192 | 193 | private void switchToFlashPolicy(ChannelHandlerContext ctx) { 194 | ChannelPipeline p = ctx.getPipeline(); 195 | 196 | Logger.getLogger(PortUnificationHandler.class.getName()).fine("Flash policy request from " + ctx.getChannel().getRemoteAddress() + "."); 197 | 198 | p.addLast("flash", new FlashPolicyHandler()); 199 | 200 | p.remove(this); 201 | } 202 | 203 | private void switchToDirectProxy(ChannelHandlerContext ctx) { 204 | ChannelPipeline p = ctx.getPipeline(); 205 | 206 | Logger.getLogger(PortUnificationHandler.class.getName()).fine("Direct proxy request from " + ctx.getChannel().getRemoteAddress() + "."); 207 | 208 | p.addLast("proxy", new DirectProxyHandler( ctx.getChannel(), cf, resolver )); 209 | 210 | p.remove(this); 211 | } 212 | 213 | // cancel the timer if channel is closed - prevents useless stack traces 214 | @Override 215 | public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) 216 | throws Exception { 217 | cancelDirectConnectionTimer ( ); 218 | } 219 | 220 | // cancel the timer if exception is caught - prevents useless stack traces 221 | @Override 222 | public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) 223 | throws Exception { 224 | cancelDirectConnectionTimer ( ); 225 | Logger.getLogger(PortUnificationHandler.class.getName()).severe("Exception on connection to " + ctx.getChannel().getRemoteAddress() + ": " + e.getCause().getMessage() ); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/main/java/com/netiq/websockify/WebsockifyProxyHandler.java: -------------------------------------------------------------------------------- 1 | package com.netiq.websockify; 2 | 3 | import static org.jboss.netty.handler.codec.http.HttpHeaders.isKeepAlive; 4 | import static org.jboss.netty.handler.codec.http.HttpHeaders.setContentLength; 5 | import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; 6 | import static org.jboss.netty.handler.codec.http.HttpMethod.GET; 7 | import static org.jboss.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN; 8 | import static org.jboss.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED; 9 | import static org.jboss.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; 10 | import static org.jboss.netty.handler.codec.http.HttpResponseStatus.OK; 11 | import static org.jboss.netty.handler.codec.http.HttpResponseStatus.TEMPORARY_REDIRECT; 12 | import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; 13 | 14 | import java.io.File; 15 | import java.io.FileNotFoundException; 16 | import java.io.RandomAccessFile; 17 | import java.io.UnsupportedEncodingException; 18 | import java.net.InetSocketAddress; 19 | import java.net.MalformedURLException; 20 | import java.net.URI; 21 | import java.net.URISyntaxException; 22 | import java.net.URLDecoder; 23 | import java.text.SimpleDateFormat; 24 | import java.util.Calendar; 25 | import java.util.Date; 26 | import java.util.GregorianCalendar; 27 | import java.util.HashMap; 28 | import java.util.Locale; 29 | import java.util.Map; 30 | import java.util.TimeZone; 31 | import java.util.logging.Logger; 32 | 33 | import javax.activation.MimetypesFileTypeMap; 34 | 35 | import org.jboss.netty.bootstrap.ClientBootstrap; 36 | import org.jboss.netty.buffer.ChannelBuffer; 37 | import org.jboss.netty.buffer.ChannelBuffers; 38 | import org.jboss.netty.channel.Channel; 39 | import org.jboss.netty.channel.ChannelEvent; 40 | import org.jboss.netty.channel.ChannelFuture; 41 | import org.jboss.netty.channel.ChannelFutureListener; 42 | import org.jboss.netty.channel.ChannelFutureProgressListener; 43 | import org.jboss.netty.channel.ChannelHandlerContext; 44 | import org.jboss.netty.channel.ChannelStateEvent; 45 | import org.jboss.netty.channel.DefaultFileRegion; 46 | import org.jboss.netty.channel.ExceptionEvent; 47 | import org.jboss.netty.channel.FileRegion; 48 | import org.jboss.netty.channel.MessageEvent; 49 | import org.jboss.netty.channel.SimpleChannelUpstreamHandler; 50 | import org.jboss.netty.channel.socket.ClientSocketChannelFactory; 51 | import org.jboss.netty.handler.codec.base64.Base64; 52 | import org.jboss.netty.handler.codec.http.DefaultHttpResponse; 53 | import org.jboss.netty.handler.codec.http.HttpHeaders; 54 | import org.jboss.netty.handler.codec.http.HttpRequest; 55 | import org.jboss.netty.handler.codec.http.HttpResponse; 56 | import org.jboss.netty.handler.codec.http.HttpResponseStatus; 57 | import org.jboss.netty.handler.codec.http.websocketx.CloseWebSocketFrame; 58 | import org.jboss.netty.handler.codec.http.websocketx.PingWebSocketFrame; 59 | import org.jboss.netty.handler.codec.http.websocketx.PongWebSocketFrame; 60 | import org.jboss.netty.handler.codec.http.websocketx.TextWebSocketFrame; 61 | import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame; 62 | import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; 63 | import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; 64 | import org.jboss.netty.handler.ssl.SslHandler; 65 | import org.jboss.netty.handler.stream.ChunkedFile; 66 | import org.jboss.netty.util.CharsetUtil; 67 | 68 | public class WebsockifyProxyHandler extends SimpleChannelUpstreamHandler { 69 | 70 | private static final String URL_PARAMETER = "url"; 71 | public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz"; 72 | public static final String HTTP_DATE_GMT_TIMEZONE = "GMT"; 73 | public static final int HTTP_CACHE_SECONDS = 60; 74 | public static final String REDIRECT_PATH = "/redirect"; 75 | 76 | private final ClientSocketChannelFactory cf; 77 | private final IProxyTargetResolver resolver; 78 | 79 | private WebSocketServerHandshaker handshaker = null; 80 | private String webDirectory; 81 | 82 | // This lock guards against the race condition that overrides the 83 | // OP_READ flag incorrectly. 84 | // See the related discussion: http://markmail.org/message/x7jc6mqx6ripynqf 85 | final Object trafficLock = new Object(); 86 | 87 | private volatile Channel outboundChannel; 88 | 89 | public WebsockifyProxyHandler(ClientSocketChannelFactory cf, IProxyTargetResolver resolver, String webDirectory) { 90 | this.cf = cf; 91 | this.resolver = resolver; 92 | this.outboundChannel = null; 93 | this.webDirectory = webDirectory; 94 | } 95 | 96 | private void ensureTargetConnection(ChannelEvent e, boolean websocket, final Object sendMsg) 97 | throws Exception { 98 | if(outboundChannel == null) { 99 | // Suspend incoming traffic until connected to the remote host. 100 | final Channel inboundChannel = e.getChannel(); 101 | inboundChannel.setReadable(false); 102 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).info("Inbound proxy connection from " + inboundChannel.getRemoteAddress() + "."); 103 | 104 | // resolve the target 105 | final InetSocketAddress target = resolver.resolveTarget(inboundChannel); 106 | if ( target == null ) 107 | { 108 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).severe("Connection from " + inboundChannel.getRemoteAddress() + " failed to resolve target."); 109 | // there is no target 110 | inboundChannel.close(); 111 | return; 112 | } 113 | 114 | // Start the connection attempt. 115 | ClientBootstrap cb = new ClientBootstrap(cf); 116 | if ( websocket ) { 117 | cb.getPipeline().addLast("handler", new OutboundWebsocketHandler(e.getChannel(), trafficLock)); 118 | } 119 | else { 120 | cb.getPipeline().addLast("handler", new OutboundHandler(e.getChannel(), trafficLock)); 121 | } 122 | ChannelFuture f = cb.connect(target); 123 | 124 | outboundChannel = f.getChannel(); 125 | if ( sendMsg != null ) outboundChannel.write(sendMsg); 126 | f.addListener(new ChannelFutureListener() { 127 | public void operationComplete(ChannelFuture future) throws Exception { 128 | if (future.isSuccess()) { 129 | // Connection attempt succeeded: 130 | // Begin to accept incoming traffic. 131 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).info("Created outbound connection to " + target + "."); 132 | inboundChannel.setReadable(true); 133 | } else { 134 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).severe("Failed to create outbound connection to " + target + "."); 135 | // Close the connection if the connection attempt has failed. 136 | inboundChannel.close(); 137 | } 138 | } 139 | }); 140 | } else { 141 | if ( sendMsg != null ) outboundChannel.write(sendMsg); 142 | } 143 | } 144 | 145 | @Override 146 | public void messageReceived(ChannelHandlerContext ctx, final MessageEvent e) 147 | throws Exception { 148 | Object msg = e.getMessage(); 149 | // An HttpRequest means either an initial websocket connection 150 | // or a web server request 151 | if (msg instanceof HttpRequest) { 152 | handleHttpRequest(ctx, (HttpRequest) msg, e); 153 | // A WebSocketFrame means a continuation of an established websocket connection 154 | } else if (msg instanceof WebSocketFrame) { 155 | handleWebSocketFrame(ctx, (WebSocketFrame) msg, e); 156 | // A channel buffer we treat as a VNC protocol request 157 | } else if (msg instanceof ChannelBuffer) { 158 | handleVncDirect(ctx, (ChannelBuffer) msg, e); 159 | } 160 | } 161 | 162 | private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req, final MessageEvent e) throws Exception { 163 | // Allow only GET methods. 164 | if (req.getMethod() != GET) { 165 | sendHttpResponse(ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN)); 166 | return; 167 | } 168 | 169 | String upgradeHeader = req.getHeader("Upgrade"); 170 | if(upgradeHeader != null && upgradeHeader.toUpperCase().equals("WEBSOCKET")){ 171 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).fine("Websocket request from " + e.getRemoteAddress() + "."); 172 | // Handshake 173 | WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( 174 | this.getWebSocketLocation(req), "base64", false); 175 | this.handshaker = wsFactory.newHandshaker(req); 176 | if (this.handshaker == null) { 177 | wsFactory.sendUnsupportedWebSocketVersionResponse(ctx.getChannel()); 178 | } else { 179 | // deal with a bug in the flash websocket emulation 180 | // it specifies WebSocket-Protocol when it seems it should specify Sec-WebSocket-Protocol 181 | String protocol = req.getHeader("WebSocket-Protocol"); 182 | String secProtocol = req.getHeader("Sec-WebSocket-Protocol"); 183 | if(protocol != null && secProtocol == null ) 184 | { 185 | req.addHeader("Sec-WebSocket-Protocol", protocol); 186 | } 187 | this.handshaker.handshake(ctx.getChannel(), req); 188 | } 189 | ensureTargetConnection (e, true, null); 190 | } 191 | else { 192 | HttpRequest request = (HttpRequest) e.getMessage(); 193 | String redirectUrl = isRedirect(request.getUri()); 194 | if ( redirectUrl != null) { 195 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).fine("Redirecting to " + redirectUrl + "."); 196 | HttpResponse response = new DefaultHttpResponse(HTTP_1_1, TEMPORARY_REDIRECT); 197 | response.setHeader(HttpHeaders.Names.LOCATION, redirectUrl); 198 | sendHttpResponse(ctx, req, response); 199 | return; 200 | } 201 | else if ( webDirectory != null )/* not a websocket connection attempt */{ 202 | handleWebRequest ( ctx, e ); 203 | } 204 | } 205 | } 206 | 207 | private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame, final MessageEvent e) { 208 | 209 | // Check for closing frame 210 | if (frame instanceof CloseWebSocketFrame) { 211 | this.handshaker.close(ctx.getChannel(), (CloseWebSocketFrame) frame); 212 | return; 213 | } else if (frame instanceof PingWebSocketFrame) { 214 | ctx.getChannel().write(new PongWebSocketFrame(frame.getBinaryData())); 215 | return; 216 | } else if (!(frame instanceof TextWebSocketFrame)) { 217 | throw new UnsupportedOperationException(String.format("%s frame types not supported", frame.getClass() 218 | .getName())); 219 | } 220 | 221 | ChannelBuffer msg = ((TextWebSocketFrame) frame).getBinaryData(); 222 | ChannelBuffer decodedMsg = Base64.decode(msg); 223 | synchronized (trafficLock) { 224 | outboundChannel.write(decodedMsg); 225 | // If outboundChannel is saturated, do not read until notified in 226 | // OutboundHandler.channelInterestChanged(). 227 | if (!outboundChannel.isWritable()) { 228 | e.getChannel().setReadable(false); 229 | } 230 | } 231 | } 232 | 233 | private void handleVncDirect(ChannelHandlerContext ctx, ChannelBuffer buffer, final MessageEvent e) throws Exception { 234 | // ensure the target connection is open and send the data 235 | ensureTargetConnection(e, false, buffer); 236 | } 237 | 238 | private void handleWebRequest(ChannelHandlerContext ctx, final MessageEvent e) throws Exception { 239 | 240 | HttpRequest request = (HttpRequest) e.getMessage(); 241 | if (request.getMethod() != GET) { 242 | sendError(ctx, METHOD_NOT_ALLOWED); 243 | return; 244 | } 245 | 246 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).info("Web request from " + e.getRemoteAddress() + " for " + request.getUri() + "."); 247 | 248 | final String path = sanitizeUri(request.getUri()); 249 | if (path == null) { 250 | sendError(ctx, FORBIDDEN); 251 | return; 252 | } 253 | 254 | File file = new File(path); 255 | if (file.isHidden() || !file.exists()) { 256 | sendError(ctx, NOT_FOUND); 257 | return; 258 | } 259 | if (!file.isFile()) { 260 | sendError(ctx, FORBIDDEN); 261 | return; 262 | } 263 | 264 | // Cache Validation 265 | String ifModifiedSince = request.getHeader(HttpHeaders.Names.IF_MODIFIED_SINCE); 266 | if (ifModifiedSince != null && !ifModifiedSince.equals("")) { 267 | SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); 268 | Date ifModifiedSinceDate = dateFormatter.parse(ifModifiedSince); 269 | 270 | // Only compare up to the second because the datetime format we send to the client does not have milliseconds 271 | long ifModifiedSinceDateSeconds = ifModifiedSinceDate.getTime() / 1000; 272 | long fileLastModifiedSeconds = file.lastModified() / 1000; 273 | if (ifModifiedSinceDateSeconds == fileLastModifiedSeconds) { 274 | sendNotModified(ctx); 275 | return; 276 | } 277 | } 278 | 279 | RandomAccessFile raf; 280 | try { 281 | raf = new RandomAccessFile(file, "r"); 282 | } catch (FileNotFoundException fnfe) { 283 | sendError(ctx, NOT_FOUND); 284 | return; 285 | } 286 | long fileLength = raf.length(); 287 | 288 | HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); 289 | setContentLength(response, fileLength); 290 | setContentTypeHeader(response, file); 291 | setDateAndCacheHeaders(response, file); 292 | 293 | Channel ch = e.getChannel(); 294 | 295 | // Write the initial line and the header. 296 | ch.write(response); 297 | 298 | // Write the content. 299 | ChannelFuture writeFuture; 300 | if (ch.getPipeline().get(SslHandler.class) != null) { 301 | // Cannot use zero-copy with HTTPS. 302 | writeFuture = ch.write(new ChunkedFile(raf, 0, fileLength, 8192)); 303 | } else { 304 | // No encryption - use zero-copy. 305 | final FileRegion region = 306 | new DefaultFileRegion(raf.getChannel(), 0, fileLength); 307 | writeFuture = ch.write(region); 308 | writeFuture.addListener(new ChannelFutureProgressListener() { 309 | public void operationComplete(ChannelFuture future) { 310 | region.releaseExternalResources(); 311 | } 312 | 313 | public void operationProgressed( 314 | ChannelFuture future, long amount, long current, long total) { 315 | System.out.printf("%s: %d / %d (+%d)%n", path, current, total, amount); 316 | } 317 | }); 318 | } 319 | 320 | // Decide whether to close the connection or not. 321 | if (!isKeepAlive(request)) { 322 | // Close the connection when the whole content is written out. 323 | writeFuture.addListener(ChannelFutureListener.CLOSE); 324 | } 325 | } 326 | 327 | private static Map getQueryMap(String query) 328 | { 329 | String[] params = query.split("&"); 330 | Map map = new HashMap(); 331 | for (String param : params) 332 | { 333 | String name = param.split("=")[0]; 334 | String value = param.split("=")[1]; 335 | map.put(name, value); 336 | } 337 | return map; 338 | } 339 | 340 | // checks to see if the uri is a redirect request 341 | // if it is, it returns the url parameter 342 | private String isRedirect(String uri) throws URISyntaxException, MalformedURLException { 343 | // Decode the path. 344 | URI url = new URI (uri); 345 | 346 | if ( REDIRECT_PATH.equals(url.getPath()) ) { 347 | String query = url.getRawQuery(); 348 | Map params = getQueryMap(query); 349 | 350 | String urlParam = params.get(URL_PARAMETER); 351 | if ( urlParam == null ) return null; 352 | 353 | try { 354 | return URLDecoder.decode(urlParam, "UTF-8"); 355 | } catch (UnsupportedEncodingException e) { 356 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).severe(e.getMessage()); 357 | } 358 | } 359 | 360 | return null; 361 | } 362 | 363 | private String sanitizeUri(String uri) throws URISyntaxException { 364 | // Decode the path. 365 | URI url = new URI (uri); 366 | uri = url.getPath(); 367 | 368 | // Convert file separators. 369 | uri = uri.replace('/', File.separatorChar); 370 | 371 | // Simplistic dumb security check. 372 | // You will have to do something serious in the production environment. 373 | if (uri.contains(File.separator + ".") || 374 | uri.contains("." + File.separator) || 375 | uri.startsWith(".") || uri.endsWith(".")) { 376 | return null; 377 | } 378 | 379 | // Convert to absolute path. 380 | return webDirectory + File.separator + uri; 381 | } 382 | 383 | private void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) { 384 | // Generate an error page if response status code is not OK (200). 385 | if (res.getStatus().getCode() != 200) { 386 | res.setContent(ChannelBuffers.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8)); 387 | setContentLength(res, res.getContent().readableBytes()); 388 | } 389 | 390 | // Send the response and close the connection if necessary. 391 | ChannelFuture f = ctx.getChannel().write(res); 392 | if (!isKeepAlive(req) || res.getStatus().getCode() != 200) { 393 | f.addListener(ChannelFutureListener.CLOSE); 394 | } 395 | } 396 | 397 | private void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { 398 | HttpResponse response = new DefaultHttpResponse(HTTP_1_1, status); 399 | response.setHeader(CONTENT_TYPE, "text/plain; charset=UTF-8"); 400 | response.setContent(ChannelBuffers.copiedBuffer( 401 | "Failure: " + status.toString() + "\r\n", 402 | CharsetUtil.UTF_8)); 403 | 404 | // Close the connection as soon as the error message is sent. 405 | ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE); 406 | } 407 | 408 | /** 409 | * When file timestamp is the same as what the browser is sending up, send a "304 Not Modified" 410 | * 411 | * @param ctx 412 | * Context 413 | */ 414 | private void sendNotModified(ChannelHandlerContext ctx) { 415 | HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.NOT_MODIFIED); 416 | setDateHeader(response); 417 | 418 | // Close the connection as soon as the error message is sent. 419 | ctx.getChannel().write(response).addListener(ChannelFutureListener.CLOSE); 420 | } 421 | 422 | /** 423 | * Sets the Date header for the HTTP response 424 | * 425 | * @param response 426 | * HTTP response 427 | */ 428 | private void setDateHeader(HttpResponse response) { 429 | SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); 430 | dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); 431 | 432 | Calendar time = new GregorianCalendar(); 433 | response.setHeader(HttpHeaders.Names.DATE, dateFormatter.format(time.getTime())); 434 | } 435 | 436 | /** 437 | * Sets the Date and Cache headers for the HTTP Response 438 | * 439 | * @param response 440 | * HTTP response 441 | * @param fileToCache 442 | * file to extract content type 443 | */ 444 | private void setDateAndCacheHeaders(HttpResponse response, File fileToCache) { 445 | SimpleDateFormat dateFormatter = new SimpleDateFormat(HTTP_DATE_FORMAT, Locale.US); 446 | dateFormatter.setTimeZone(TimeZone.getTimeZone(HTTP_DATE_GMT_TIMEZONE)); 447 | 448 | // Date header 449 | Calendar time = new GregorianCalendar(); 450 | response.setHeader(HttpHeaders.Names.DATE, dateFormatter.format(time.getTime())); 451 | 452 | // Add cache headers 453 | time.add(Calendar.SECOND, HTTP_CACHE_SECONDS); 454 | response.setHeader(HttpHeaders.Names.EXPIRES, dateFormatter.format(time.getTime())); 455 | response.setHeader(HttpHeaders.Names.CACHE_CONTROL, "private, max-age=" + HTTP_CACHE_SECONDS); 456 | response.setHeader(HttpHeaders.Names.LAST_MODIFIED, dateFormatter.format(new Date(fileToCache.lastModified()))); 457 | } 458 | 459 | /** 460 | * Sets the content type header for the HTTP Response 461 | * 462 | * @param response 463 | * HTTP response 464 | * @param file 465 | * file to extract content type 466 | */ 467 | private void setContentTypeHeader(HttpResponse response, File file) { 468 | MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap(); 469 | response.setHeader(HttpHeaders.Names.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath())); 470 | } 471 | 472 | private String getWebSocketLocation(HttpRequest req) { 473 | String prefix = "ws"; 474 | String origin = req.getHeader(HttpHeaders.Names.ORIGIN).toLowerCase(); 475 | if(origin.contains("https")){ 476 | prefix = "wss"; 477 | } 478 | return prefix + "://" + req.getHeader(HttpHeaders.Names.HOST) + req.getUri(); 479 | } 480 | 481 | @Override 482 | public void channelInterestChanged(ChannelHandlerContext ctx, 483 | ChannelStateEvent e) throws Exception { 484 | // If inboundChannel is not saturated anymore, continue accepting 485 | // the incoming traffic from the outboundChannel. 486 | synchronized (trafficLock) { 487 | if (e.getChannel().isWritable() && outboundChannel != null) { 488 | outboundChannel.setReadable(true); 489 | } 490 | } 491 | } 492 | 493 | @Override 494 | public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) 495 | throws Exception { 496 | 497 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).info("Inbound proxy connection from " + ctx.getChannel().getRemoteAddress() + " closed."); 498 | if (outboundChannel != null) { 499 | closeOnFlush(outboundChannel); 500 | } 501 | } 502 | 503 | @Override 504 | public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) 505 | throws Exception { 506 | e.getCause().printStackTrace(); 507 | Logger.getLogger(WebsockifyProxyHandler.class.getName()).severe("Exception on inbound proxy connection from " + e.getChannel().getRemoteAddress() + ": " + e.getCause().getMessage()); 508 | closeOnFlush(e.getChannel()); 509 | } 510 | 511 | /** 512 | * Closes the specified channel after all queued write requests are flushed. 513 | */ 514 | static void closeOnFlush(Channel ch) { 515 | if (ch.isConnected()) { 516 | ch.write(ChannelBuffers.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); 517 | } 518 | } 519 | } 520 | --------------------------------------------------------------------------------