diff --git a/netty/src/main/java/com/baeldung/http/server/CustomHttpServerHandler.java b/netty/src/main/java/com/baeldung/http/server/CustomHttpServerHandler.java new file mode 100644 index 0000000000..038f559329 --- /dev/null +++ b/netty/src/main/java/com/baeldung/http/server/CustomHttpServerHandler.java @@ -0,0 +1,116 @@ +package com.baeldung.http.server; + +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; +import static io.netty.handler.codec.http.HttpResponseStatus.OK; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +import java.util.Set; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; +import io.netty.handler.codec.http.cookie.ServerCookieEncoder; +import io.netty.util.CharsetUtil; + +public class CustomHttpServerHandler extends SimpleChannelInboundHandler { + + private HttpRequest request; + StringBuilder responseData = new StringBuilder(); + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof HttpRequest) { + HttpRequest request = this.request = (HttpRequest) msg; + + if (HttpUtil.is100ContinueExpected(request)) { + writeResponse(ctx); + } + + responseData.setLength(0); + responseData.append(ResponseBuilder.addRequestAttributes(request)); + responseData.append(ResponseBuilder.addHeaders(request)); + responseData.append(ResponseBuilder.addParams(request)); + } + + responseData.append(ResponseBuilder.addDecoderResult(request)); + + if (msg instanceof HttpContent) { + HttpContent httpContent = (HttpContent) msg; + + responseData.append(ResponseBuilder.addBody(httpContent)); + responseData.append(ResponseBuilder.addDecoderResult(request)); + + if (msg instanceof LastHttpContent) { + LastHttpContent trailer = (LastHttpContent) msg; + responseData.append(ResponseBuilder.addLastResponse(request, trailer)); + writeResponse(ctx, trailer, responseData); + } + } + } + + private void writeResponse(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER); + ctx.write(response); + } + + private void writeResponse(ChannelHandlerContext ctx, LastHttpContent trailer, StringBuilder responseData) { + boolean keepAlive = HttpUtil.isKeepAlive(request); + + FullHttpResponse httpResponse = new DefaultFullHttpResponse(HTTP_1_1, ((HttpObject) trailer).decoderResult() + .isSuccess() ? OK : BAD_REQUEST, Unpooled.copiedBuffer(responseData.toString(), CharsetUtil.UTF_8)); + + httpResponse.headers() + .set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); + + if (keepAlive) { + httpResponse.headers() + .setInt(HttpHeaderNames.CONTENT_LENGTH, httpResponse.content() + .readableBytes()); + httpResponse.headers() + .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + + String cookieString = request.headers() + .get(HttpHeaderNames.COOKIE); + if (cookieString != null) { + Set cookies = ServerCookieDecoder.STRICT.decode(cookieString); + if (!cookies.isEmpty()) { + for (Cookie cookie : cookies) { + httpResponse.headers() + .add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie)); + } + } + } + + ctx.write(httpResponse); + + if (!keepAlive) { + ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) + .addListener(ChannelFutureListener.CLOSE); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } +} diff --git a/netty/src/main/java/com/baeldung/http/server/HttpServer.java b/netty/src/main/java/com/baeldung/http/server/HttpServer.java new file mode 100644 index 0000000000..529d14f0fd --- /dev/null +++ b/netty/src/main/java/com/baeldung/http/server/HttpServer.java @@ -0,0 +1,64 @@ +package com.baeldung.http.server; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +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.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; + +public class HttpServer { + + private int port; + static Logger logger = LoggerFactory.getLogger(HttpServer.class); + + public HttpServer(int port) { + this.port = port; + } + + public static void main(String[] args) throws Exception { + + int port = args.length > 0 ? Integer.parseInt(args[0]) : 8080; + + new HttpServer(port).run(); + } + + public void run() throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(1); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .handler(new LoggingHandler(LogLevel.INFO)) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(new HttpRequestDecoder()); + p.addLast(new HttpResponseEncoder()); + p.addLast(new CustomHttpServerHandler()); + } + }); + + ChannelFuture f = b.bind(port) + .sync(); + f.channel() + .closeFuture() + .sync(); + + } finally { + bossGroup.shutdownGracefully(); + workerGroup.shutdownGracefully(); + } + } +} diff --git a/netty/src/main/java/com/baeldung/http/server/ResponseBuilder.java b/netty/src/main/java/com/baeldung/http/server/ResponseBuilder.java new file mode 100644 index 0000000000..6d4e7845da --- /dev/null +++ b/netty/src/main/java/com/baeldung/http/server/ResponseBuilder.java @@ -0,0 +1,120 @@ +package com.baeldung.http.server; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.DecoderResult; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.QueryStringDecoder; +import io.netty.util.CharsetUtil; + +class ResponseBuilder { + + static StringBuilder addRequestAttributes(HttpRequest request) { + StringBuilder responseData = new StringBuilder(); + responseData.append("Version: ") + .append(request.protocolVersion()) + .append("\r\n"); + responseData.append("Host: ") + .append(request.headers() + .get(HttpHeaderNames.HOST, "unknown")) + .append("\r\n"); + responseData.append("URI: ") + .append(request.uri()) + .append("\r\n\r\n"); + return responseData; + } + + static StringBuilder addParams(HttpRequest request) { + StringBuilder responseData = new StringBuilder(); + QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri()); + Map> params = queryStringDecoder.parameters(); + if (!params.isEmpty()) { + for (Entry> p : params.entrySet()) { + String key = p.getKey(); + List vals = p.getValue(); + for (String val : vals) { + responseData.append("Parameter: ") + .append(key) + .append(" = ") + .append(val) + .append("\r\n"); + } + } + responseData.append("\r\n"); + } + return responseData; + } + + static StringBuilder addHeaders(HttpRequest request) { + StringBuilder responseData = new StringBuilder(); + HttpHeaders headers = request.headers(); + if (!headers.isEmpty()) { + for (Map.Entry header : headers) { + CharSequence key = header.getKey(); + CharSequence value = header.getValue(); + responseData.append(key) + .append(" = ") + .append(value) + .append("\r\n"); + } + responseData.append("\r\n"); + } + return responseData; + } + + static StringBuilder addBody(HttpContent httpContent) { + StringBuilder responseData = new StringBuilder(); + ByteBuf content = httpContent.content(); + if (content.isReadable()) { + responseData.append(content.toString(CharsetUtil.UTF_8) + .toUpperCase()); + responseData.append("\r\n"); + } + return responseData; + } + + static StringBuilder addDecoderResult(HttpObject o) { + StringBuilder responseData = new StringBuilder(); + DecoderResult result = o.decoderResult(); + + if (!result.isSuccess()) { + responseData.append("..Decoder Failure: "); + responseData.append(result.cause()); + responseData.append("\r\n"); + } + + return responseData; + } + + static StringBuilder addLastResponse(HttpRequest request, LastHttpContent trailer) { + StringBuilder responseData = new StringBuilder(); + responseData.append("Good Bye!\r\n"); + + if (!trailer.trailingHeaders() + .isEmpty()) { + responseData.append("\r\n"); + for (CharSequence name : trailer.trailingHeaders() + .names()) { + for (CharSequence value : trailer.trailingHeaders() + .getAll(name)) { + responseData.append("P.S. Trailing Header: "); + responseData.append(name) + .append(" = ") + .append(value) + .append("\r\n"); + } + } + responseData.append("\r\n"); + } + return responseData; + } + +} diff --git a/netty/src/test/java/com/baeldung/http/server/HttpServerLiveTest.java b/netty/src/test/java/com/baeldung/http/server/HttpServerLiveTest.java new file mode 100644 index 0000000000..7a0f884724 --- /dev/null +++ b/netty/src/test/java/com/baeldung/http/server/HttpServerLiveTest.java @@ -0,0 +1,180 @@ +package com.baeldung.http.server; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +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.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.DefaultCookie; +import io.netty.util.CharsetUtil; + +//Ensure the server class - HttpServer.java is already started before running this test +public class HttpServerLiveTest { + + private static final String HOST = "127.0.0.1"; + private static final int PORT = 8080; + private Channel channel; + private EventLoopGroup group = new NioEventLoopGroup(); + ResponseAggregator response = new ResponseAggregator(); + + @Before + public void setup() throws Exception { + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + + @Override + protected void initChannel(SocketChannel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast(new HttpClientCodec()); + p.addLast(new HttpContentDecompressor()); + p.addLast(new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception { + response = prepareResponse(ctx, msg, response); + } + }); + } + }); + + channel = b.connect(HOST, PORT) + .sync() + .channel(); + } + + @Test + public void whenPostSent_thenContentReceivedInUppercase() throws Exception { + String body = "Hello World!"; + + DefaultFullHttpRequest request = createRequest(body); + + channel.writeAndFlush(request); + Thread.sleep(200); + + assertEquals(200, response.getStatus()); + assertEquals("HTTP/1.1", response.getVersion()); + + assertTrue(response.getContent() + .contains(body.toUpperCase())); + } + + @Test + public void whenGetSent_thenCookieReceivedInResponse() throws Exception { + DefaultFullHttpRequest request = createRequest(null); + + channel.writeAndFlush(request); + Thread.sleep(200); + + assertEquals(200, response.getStatus()); + assertEquals("HTTP/1.1", response.getVersion()); + + Map headers = response.getHeaders(); + String cookies = headers.get("set-cookie"); + assertTrue(cookies.contains("my-cookie")); + } + + @After + public void cleanup() throws InterruptedException { + channel.closeFuture() + .sync(); + group.shutdownGracefully(); + } + + private static DefaultFullHttpRequest createRequest(final CharSequence body) throws Exception { + DefaultFullHttpRequest request; + if (body != null) { + request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/"); + request.content() + .writeBytes(body.toString() + .getBytes(CharsetUtil.UTF_8.name())); + request.headers() + .set(HttpHeaderNames.CONTENT_TYPE, "application/json"); + request.headers() + .set(HttpHeaderNames.CONTENT_LENGTH, request.content() + .readableBytes()); + } else { + request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/", Unpooled.EMPTY_BUFFER); + request.headers() + .set(HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(new DefaultCookie("my-cookie", "foo"))); + } + + request.headers() + .set(HttpHeaderNames.HOST, HOST); + request.headers() + .set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + + return request; + } + + private static ResponseAggregator prepareResponse(ChannelHandlerContext ctx, HttpObject msg, ResponseAggregator responseAgg) { + + if (msg instanceof HttpResponse) { + HttpResponse response = (HttpResponse) msg; + + responseAgg.setStatus(response.status() + .code()); + + responseAgg.setVersion(response.protocolVersion() + .text()); + + if (!response.headers() + .isEmpty()) { + Map headers = new HashMap(); + for (CharSequence name : response.headers() + .names()) { + for (CharSequence value : response.headers() + .getAll(name)) { + headers.put(name.toString(), value.toString()); + } + } + responseAgg.setHeaders(headers); + } + if (HttpUtil.isTransferEncodingChunked(response)) { + responseAgg.setChunked(true); + } else { + responseAgg.setChunked(false); + } + } + if (msg instanceof HttpContent) { + HttpContent content = (HttpContent) msg; + String responseData = content.content() + .toString(CharsetUtil.UTF_8); + + if (content instanceof LastHttpContent) { + responseAgg.setContent(responseData + "} End Of Content"); + ctx.close(); + } + } + return responseAgg; + } +} diff --git a/netty/src/test/java/com/baeldung/http/server/ResponseAggregator.java b/netty/src/test/java/com/baeldung/http/server/ResponseAggregator.java new file mode 100644 index 0000000000..9f6e5e9823 --- /dev/null +++ b/netty/src/test/java/com/baeldung/http/server/ResponseAggregator.java @@ -0,0 +1,52 @@ +package com.baeldung.http.server; + +import java.util.Map; + +public class ResponseAggregator { + int status; + String version; + Map headers; + boolean isChunked; + String content; + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public boolean isChunked() { + return isChunked; + } + + public void setChunked(boolean isChunked) { + this.isChunked = isChunked; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + +}