diff --git a/betamax-netty/build.gradle b/betamax-netty/build.gradle new file mode 100644 index 00000000..3c2606fc --- /dev/null +++ b/betamax-netty/build.gradle @@ -0,0 +1,11 @@ +dependencies { + compile project(':betamax-core') + compile nettyDependency +} + +modifyPom { + project { + name 'Betamax Netty' + description 'The base Netty support classes for Betamax.' + } +} \ No newline at end of file diff --git a/betamax-netty/src/main/java/co/freeside/betamax/proxy/netty/HttpChannelInitializer.java b/betamax-netty/src/main/java/co/freeside/betamax/proxy/netty/HttpChannelInitializer.java new file mode 100644 index 00000000..9ddb7fdf --- /dev/null +++ b/betamax-netty/src/main/java/co/freeside/betamax/proxy/netty/HttpChannelInitializer.java @@ -0,0 +1,30 @@ +package co.freeside.betamax.proxy.netty; + +import io.netty.channel.*; +import io.netty.channel.socket.*; +import io.netty.handler.codec.http.*; +import io.netty.handler.stream.*; + +/** + * Configures up a channel to handle HTTP requests and responses. + */ +public class HttpChannelInitializer extends ChannelInitializer { + + public static final int MAX_CONTENT_LENGTH = 65536; + + private final ChannelHandler handler; + + public HttpChannelInitializer(ChannelHandler handler) { + this.handler = handler; + } + + @Override + public void initChannel(SocketChannel channel) throws Exception { + channel.pipeline() + .addLast(new HttpRequestDecoder()) + .addLast(new HttpObjectAggregator(MAX_CONTENT_LENGTH)) + .addLast(new HttpResponseEncoder()) + .addLast(new ChunkedWriteHandler()) + .addLast(handler); + } +} diff --git a/betamax-netty/src/main/java/co/freeside/betamax/proxy/netty/NettyBetamaxServer.java b/betamax-netty/src/main/java/co/freeside/betamax/proxy/netty/NettyBetamaxServer.java new file mode 100644 index 00000000..df54ea60 --- /dev/null +++ b/betamax-netty/src/main/java/co/freeside/betamax/proxy/netty/NettyBetamaxServer.java @@ -0,0 +1,42 @@ +package co.freeside.betamax.proxy.netty; + +import java.net.*; +import io.netty.bootstrap.*; +import io.netty.channel.*; +import io.netty.channel.nio.*; +import io.netty.channel.socket.nio.*; + +/** + * A Netty-based implementation of the Betamax proxy server. + */ +public class NettyBetamaxServer { + + private final int port; + private final ChannelHandler handler; + private EventLoopGroup group; + private Channel channel; + + public NettyBetamaxServer(int port, ChannelHandler handler) { + this.port = port; + this.handler = handler; + } + + public InetSocketAddress run() throws Exception { + group = new NioEventLoopGroup(); + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(group) + .channel(NioServerSocketChannel.class) + .childHandler(new HttpChannelInitializer(handler)) + .option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + + channel = bootstrap.bind(port).sync().channel(); + return (InetSocketAddress) channel.localAddress(); + } + + public void shutdown() throws InterruptedException { + if (channel != null) channel.close().sync(); + if (group != null) group.shutdownGracefully(); + } + +} \ No newline at end of file diff --git a/betamax-netty/src/test/groovy/co/freeside/betamax/proxy/netty/EchoServerHandler.java b/betamax-netty/src/test/groovy/co/freeside/betamax/proxy/netty/EchoServerHandler.java new file mode 100644 index 00000000..eb91fb71 --- /dev/null +++ b/betamax-netty/src/test/groovy/co/freeside/betamax/proxy/netty/EchoServerHandler.java @@ -0,0 +1,37 @@ +package co.freeside.betamax.proxy.netty; + +import io.netty.buffer.*; +import io.netty.channel.*; +import io.netty.handler.codec.http.*; +import io.netty.util.*; +import static io.netty.handler.codec.http.HttpHeaders.Names.*; +import static io.netty.handler.codec.http.HttpResponseStatus.*; +import static io.netty.handler.codec.http.HttpVersion.*; + +@ChannelHandler.Sharable +public class EchoServerHandler extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + FullHttpRequest request = (FullHttpRequest) msg; + FullHttpResponse response = new DefaultFullHttpResponse( + HTTP_1_1, + OK, + request.content() + ); + response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + FullHttpResponse response = new DefaultFullHttpResponse( + HTTP_1_1, + OK, + Unpooled.copiedBuffer(cause.getClass().getSimpleName() + ": " + cause.getMessage(), CharsetUtil.UTF_8) + ); + response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } +} diff --git a/betamax-netty/src/test/groovy/co/freeside/betamax/proxy/netty/NettyServerSpec.groovy b/betamax-netty/src/test/groovy/co/freeside/betamax/proxy/netty/NettyServerSpec.groovy new file mode 100644 index 00000000..20fbd636 --- /dev/null +++ b/betamax-netty/src/test/groovy/co/freeside/betamax/proxy/netty/NettyServerSpec.groovy @@ -0,0 +1,33 @@ +package co.freeside.betamax.proxy.netty + +import spock.lang.Specification + +class NettyServerSpec extends Specification { + + void "can serve HTTP responses with Netty"() { + given: + def server = new NettyBetamaxServer(port, new EchoServerHandler()) + server.run() + + when: + HttpURLConnection connection = new URL("http://localhost:$port/").openConnection() + connection.requestMethod = "POST" + connection.doInput = true + connection.doOutput = true + connection.outputStream.withWriter("UTF-8") { writer -> + writer << message + } + connection.connect() + + then: + connection.inputStream.getText("UTF-8") == message + + cleanup: + server.shutdown() + + where: + port = 5000 + message = "O HAI" + } + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index b62afc26..7d5dbb1a 100644 --- a/build.gradle +++ b/build.gradle @@ -18,14 +18,17 @@ allprojects { junitVersion = '4.10' spockVersion = '0.7-groovy-1.8' jettyVersion = '7.3.1.v20110307' + nettyVersion = '4.0.4.Final' groovyDependency = "org.codehaus.groovy:groovy-all:$groovyVersion" httpClientDependency = "org.apache.httpcomponents:httpclient:$httpClientVersion" junitDependency = "junit:junit:$junitVersion" spockDependency = "org.spockframework:spock-core:$spockVersion" jettyDependency = "org.eclipse.jetty:jetty-server:$jettyVersion" + // TODO: not sure we need netty-all + nettyDependency = "io.netty:netty-all:$nettyVersion" - publishedModules = [':betamax-core', ':betamax-proxy', ':betamax-httpclient', ':betamax-jetty'] + publishedModules = [':betamax-core', ':betamax-proxy', ':betamax-httpclient', ':betamax-jetty', ':betamax-netty'] } } diff --git a/settings.gradle b/settings.gradle index c6c77e73..f5a0199c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,6 +2,7 @@ include 'betamax-core', 'betamax-proxy', 'betamax-httpclient', 'betamax-jetty', + 'betamax-netty', 'betamax-test-support' rootProject.name = 'betamax' \ No newline at end of file