/* * Copyright 2014 Matthias Einwag * * The jawampa authors license this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package ws.wamp.jawampa.transport.netty; import java.net.URI; import java.util.List; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import ws.wamp.jawampa.ApplicationError; import ws.wamp.jawampa.WampRouter; import ws.wamp.jawampa.WampSerialization; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoopGroup; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.util.SelfSignedCertificate; import io.netty.util.CharsetUtil; import static io.netty.handler.codec.http.HttpHeaders.Names.*; import static io.netty.handler.codec.http.HttpMethod.*; import static io.netty.handler.codec.http.HttpResponseStatus.*; import static io.netty.handler.codec.http.HttpVersion.*; /** * A simple default implementation for the websocket adapter for a WAMP router.<br> * It provides listening capabilities for a WAMP router on a given websocket address. */ public class SimpleWampWebsocketListener { enum State { Intialized, Started, Closed } State state = State.Intialized; final EventLoopGroup bossGroup; final EventLoopGroup clientGroup; final WampRouter router; final URI uri; SslContext sslCtx; List<WampSerialization> serializations; Channel channel; boolean started = false; public SimpleWampWebsocketListener(WampRouter router, URI uri, SslContext sslContext) throws ApplicationError { this(router, uri, sslContext, WampSerialization.defaultSerializations()); } public SimpleWampWebsocketListener(WampRouter router, URI uri, SslContext sslContext, List<WampSerialization> serializations) throws ApplicationError { this.router = router; this.uri = uri; this.serializations = serializations; if (serializations == null || serializations.size() == 0 || serializations.contains(WampSerialization.Invalid)) throw new ApplicationError(ApplicationError.INVALID_SERIALIZATIONS); this.bossGroup = new NioEventLoopGroup(1, new ThreadFactory(){ @Override public Thread newThread(Runnable r){ return new Thread(r, "WampRouterBossLoop"); } }); this.clientGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors(), new ThreadFactory(){ private AtomicInteger counter = new AtomicInteger(); @Override public Thread newThread(Runnable r){ return new Thread(r, "WampRouterClientLoop-"+counter.incrementAndGet()); } }); // Copy the ssl context only when we really want ssl if (uri.getScheme().equalsIgnoreCase("wss")) { this.sslCtx = sslContext; } } public void start() { if (state != State.Intialized) return; try { // Initialize SSL when required if (uri.getScheme().equalsIgnoreCase("wss") && sslCtx == null) { // Use a self signed certificate when we got none provided through the constructor SelfSignedCertificate ssc = new SelfSignedCertificate(); sslCtx = SslContext.newServerContext(ssc.certificate(), ssc.privateKey()); } // Use well-known ports if not explicitly specified final int port; if (uri.getPort() == -1) { if (sslCtx != null) port = 443; else port = 80; } else port = uri.getPort(); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, clientGroup) .channel(NioServerSocketChannel.class) .childHandler(new WebSocketServerInitializer(uri, sslCtx)); channel = b.bind(uri.getHost(), port).sync().channel(); } catch(Exception e) { throw new RuntimeException(e); } } public void stop() { if (state == State.Closed) return; if (channel != null) { try { channel.close().sync(); } catch (InterruptedException e) { } channel = null; } bossGroup.shutdownGracefully(); clientGroup.shutdownGracefully(); state = State.Closed; } private class WebSocketServerInitializer extends ChannelInitializer<SocketChannel> { private final URI uri; private final SslContext sslCtx; public WebSocketServerInitializer(URI uri, SslContext sslCtx) { this.uri = uri; this.sslCtx = sslCtx; } @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); if (sslCtx != null) { pipeline.addLast(sslCtx.newHandler(ch.alloc())); } pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new HttpObjectAggregator(65536)); pipeline.addLast(new WampServerWebsocketHandler(uri.getPath().length()==0 ? "/" : uri.getPath(), router, serializations)); pipeline.addLast(new WebSocketServerHandler(uri)); } } /** * Handles handshakes and messages */ public static class WebSocketServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> { private final URI uri; WebSocketServerHandler(URI uri) { this.uri = uri; } @Override public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) { handleHttpRequest(ctx, (FullHttpRequest) msg); } private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest req) { // Handle a bad request. if (!req.getDecoderResult().isSuccess()) { sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST)); return; } // Allow only GET methods. if (req.getMethod() != GET) { sendHttpResponse(ctx, req, new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN)); return; } // Send the demo page and favicon.ico if ("/".equals(req.getUri())) { ByteBuf content = Unpooled.copiedBuffer( "<html><head><title>Wamp Router</title></head><body>" + "<h1>This server provides a wamp router on path " + uri.getPath() + "</h1>" + "</body></html>" , CharsetUtil.UTF_8); FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, OK, content); res.headers().set(CONTENT_TYPE, "text/html; charset=UTF-8"); HttpHeaders.setContentLength(res, content.readableBytes()); sendHttpResponse(ctx, req, res); return; } if ("/favicon.ico".equals(req.getUri())) { FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND); sendHttpResponse(ctx, req, res); return; } FullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND); sendHttpResponse(ctx, req, res); } private static void sendHttpResponse( ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) { // Generate an error page if response getStatus code is not OK (200). if (res.getStatus().code() != 200) { ByteBuf buf = Unpooled.copiedBuffer(res.getStatus().toString(), CharsetUtil.UTF_8); res.content().writeBytes(buf); buf.release(); HttpHeaders.setContentLength(res, res.content().readableBytes()); } // Send the response and close the connection if necessary. ChannelFuture f = ctx.channel().writeAndFlush(res); if (!HttpHeaders.isKeepAlive(req) || res.getStatus().code() != 200) { f.addListener(ChannelFutureListener.CLOSE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } } }