/* * Copyright 2014-2016 CyberVision, Inc. * * Licensed 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 org.netty.http.server; import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import io.netty.bootstrap.ServerBootstrap; 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.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; 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.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpRequestDecoder; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseEncoder; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.multipart.Attribute; import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; import io.netty.handler.codec.http.multipart.HttpDataFactory; import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; import io.netty.handler.codec.http.multipart.InterfaceHttpData; import io.netty.util.CharsetUtil; import io.netty.util.concurrent.DefaultEventExecutorGroup; import io.netty.util.concurrent.EventExecutorGroup; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.kaaproject.kaa.server.common.server.BadRequestException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.IOException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.nio.ByteBuffer; import java.util.List; import java.util.Random; import java.util.Vector; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * Integration Test which demonstrates Netty framework error in case when parses Multipart-mixed * POST HTTP request. In case when multipart entity ends with odd number of '0x0D' bytes, Netty * framework append one more '0x0D' byte. * * To extract multipart entities used flowing code: * * HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); * HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(factory, request); InterfaceHttpData * data = decoder.getBodyHttpData(HTTP_TEST_ATTRIBUTE); Attribute attribute = (Attribute) data; * requestData = attribute.get(); * * Test initialize Netty server and produce Http request with POST multipart entity. * * @author Andrey Panasenko */ public class NettyHttpTestIT { /** * Multipart entity Name in POST request */ public static final String HTTP_TEST_ATTRIBUTE = "Test-attribute"; public static final String HTTP_RESPONSE_CONTENT_TYPE = "x-application/kaaproject"; /** * The Constant LOG. */ private static final Logger LOG = LoggerFactory.getLogger(NettyHttpTestIT.class); /** * Netty bind port */ private static final int HTTP_BIND_PORT = 9878; private static final int DEFAULT_REQUEST_MAX_SIZE = 2048; private static String[] hex = new String[]{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"}; /** * Thread executor, used for HttpClient operation and Netty starter operation */ private static ExecutorService executor = null; private NettyStarter netty = null; /** * Byte array filled out with sending bytes in multipart entity */ private byte[] sendingArray; /** * Initialize Thread executor. */ @BeforeClass public static void before() throws InterruptedException { executor = Executors.newFixedThreadPool(5); } /** * Shutdown Thread executor. */ @AfterClass public static void after() throws InterruptedException { executor.shutdown(); executor.awaitTermination(1, TimeUnit.MINUTES); } /** * Return hex String of bytes. */ public static String bytesToString(byte[] bytes, String delim) { if (delim == null) { delim = ":"; } StringBuffer sb = new StringBuffer(); for (int i = 0; i < bytes.length; i++) { int f = (bytes[i] & 0xF0) >> 4; int s = bytes[i] & 0x0F; sb.append(hex[f]); sb.append(hex[s]); sb.append(delim); } return sb.toString(); } /** * Start Netty server. */ @Before public void beforeTest() { try { netty = new NettyStarter(); } catch (InterruptedException e) { LOG.error("Netty start Error", e); fail(e.toString()); } executor.execute(netty); } /** * Stop Netty server. */ @After public void afterTest() { netty.shutdown(); netty = null; } /** * Test which demonstrates Netty parsing bug. * In case of multipart POST entity ends with CR with odd number, * Netty return one more CR byte in multipart entity body. */ @Test public void testOddCr() { LOG.info("Starting HTTP test"); assertNotNull(netty); String host = "localhost"; Random rnd = new Random(); int arrayLength = 16; sendingArray = new byte[arrayLength]; rnd.nextBytes(sendingArray); //sendingArray[arrayLength-5] = 0x0d; //sendingArray[arrayLength-4] = 0x0d; sendingArray[arrayLength - 3] = 0x0d; sendingArray[arrayLength - 2] = 0x0d; sendingArray[arrayLength - 1] = 0x0d; try { LOG.info("Starting HTTP test to {}:{} ", host, HTTP_BIND_PORT); HttpTestClient test = new HttpTestClient(host, HTTP_BIND_PORT, sendingArray); executor.submit(test); byte[] response = test.getResponseBody(); assertEquals(ByteBuffer.wrap(sendingArray), ByteBuffer.wrap(response)); } catch (IOException e) { LOG.error("Error: ", e); fail(e.toString()); } LOG.info("Test complete"); } /** * Netty starter class. * Initialize netty framework. * Start Netty. * Shutdown netty. * * @author Andrey Panasenko */ public class NettyStarter implements Runnable { private EventLoopGroup bossGroup; private EventLoopGroup workerGroup; private ServerBootstrap bServer; private EventExecutorGroup eventExecutor; private Channel bindChannel; public NettyStarter() throws InterruptedException { LOG.info("NettyHttpServer Initializing..."); bossGroup = new NioEventLoopGroup(); LOG.trace("NettyHttpServer bossGroup created."); workerGroup = new NioEventLoopGroup(); LOG.trace("NettyHttpServer workGroup created."); bServer = new ServerBootstrap(); LOG.trace("NettyHttpServer ServerBootstrap created."); eventExecutor = new DefaultEventExecutorGroup(1); LOG.trace("NettyHttpServer Task Executor created."); DefaultServerInitializer sInit = new DefaultServerInitializer(eventExecutor); LOG.trace("NettyHttpServer InitClass instance created."); LOG.trace("NettyHttpServer InitClass instance Init()."); bServer.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class).childHandler(sInit) .option(ChannelOption.SO_REUSEADDR, true); LOG.trace("NettyHttpServer ServerBootstrap group initialized."); bindChannel = bServer.bind(HTTP_BIND_PORT).sync().channel(); } public void shutdown() { LOG.info("NettyHttpServer stopping..."); if (bossGroup != null) { try { Future<? extends Object> f = bossGroup.shutdownGracefully(); f.await(); } catch (InterruptedException e) { LOG.trace("NettyHttpServer stopping: bossGroup error", e); } finally { bossGroup = null; LOG.trace("NettyHttpServer stopping: bossGroup stoped"); } } if (workerGroup != null) { try { Future<? extends Object> f = workerGroup.shutdownGracefully(); f.await(); } catch (InterruptedException e) { LOG.trace("NettyHttpServer stopping: workerGroup error", e); } finally { workerGroup = null; LOG.trace("NettyHttpServer stopping: workerGroup stopped"); } } if (eventExecutor != null) { try { Future<? extends Object> f = eventExecutor.shutdownGracefully(); f.await(); } catch (InterruptedException e) { LOG.trace("NettyHttpServer stopping: task executor error", e); } finally { eventExecutor = null; LOG.trace("NettyHttpServer stopping: task executor stopped."); } } } /* (non-Javadoc) * @see java.lang.Runnable#run() */ @Override public void run() { LOG.info("NettyHttpServer starting..."); try { bindChannel.closeFuture().sync(); } catch (InterruptedException e) { LOG.error("NettyHttpServer error", e); } finally { shutdown(); LOG.info("NettyHttpServer shut down"); } } } /** * Netty channel initializer. * * @author Andrey Panasenko */ public class DefaultServerInitializer extends ChannelInitializer<SocketChannel> { private EventExecutorGroup eventExecutor; public DefaultServerInitializer(EventExecutorGroup eventExecutor) { this.eventExecutor = eventExecutor; } /* (non-Javadoc) * @see io.netty.channel.ChannelInitializer#initChannel(io.netty.channel.Channel) */ @Override protected void initChannel(SocketChannel ch) throws Exception { final ChannelPipeline p = ch.pipeline(); LOG.info("New connection from {}", ch.remoteAddress().toString()); p.addLast("httpDecoder", new HttpRequestDecoder()); p.addLast("httpAggregator", new HttpObjectAggregator(DEFAULT_REQUEST_MAX_SIZE)); p.addLast("httpEncoder", new HttpResponseEncoder()); p.addLast("handler", new DefaultHandler(eventExecutor)); p.addLast("httpExceptionHandler", new DefaultExceptionHandler()); } } /** * HTTP Request handler. * * @author Andrey Panasenko */ public class DefaultHandler extends SimpleChannelInboundHandler<HttpRequest> { private EventExecutorGroup eventExecutor; public DefaultHandler(EventExecutorGroup eventExecutor) { this.eventExecutor = eventExecutor; } /* (non-Javadoc) * @see io.netty.channel.SimpleChannelInboundHandler#channelRead0(io.netty.channel.ChannelHandlerContext, java.lang.Object) */ @Override protected void channelRead0(final ChannelHandlerContext ctx, HttpRequest msg) throws Exception { HttpHandler handler = new HttpHandler(); handler.setHttpRequest(msg); final Future<HttpHandler> future = (Future<HttpHandler>) eventExecutor.submit(handler); future.addListener(new GenericFutureListener<Future<HttpHandler>>() { @Override public void operationComplete(Future<HttpHandler> future) throws Exception { LOG.trace("HttpHandler().operationComplete..."); if (future.isSuccess()) { HttpResponse response = future.get().getHttpResponse(); if (response != null) { ctx.writeAndFlush(response); } else { ctx.fireExceptionCaught(new Exception("Error creating response")); } } else { ctx.fireExceptionCaught(future.cause()); } } }); } } /** * HTTP Request handler. * Parse HTTP Request. * Check received bytes[] in multipart entity with sending bytes[] * Produce response if check correct. * * @author Andrey Panasenko */ public class HttpHandler implements Callable<HttpHandler> { private FullHttpResponse response; private byte[] requestData; /* (non-Javadoc) * @see java.util.concurrent.Callable#call() */ @Override public HttpHandler call() throws Exception { if (requestData.length <= 0) { LOG.error("HttpRequest not received byte[]"); throw new BadRequestException("HttpRequest not received byte[]"); } LOG.info(bytesToString(sendingArray, ":")); LOG.info(bytesToString(requestData, ":")); assertEquals(ByteBuffer.wrap(sendingArray), ByteBuffer.wrap(requestData)); LOG.info("Received array equalse sent array"); response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.copiedBuffer(requestData)); response.headers().set(CONTENT_TYPE, HTTP_RESPONSE_CONTENT_TYPE); response.headers().set(CONTENT_LENGTH, response.content().readableBytes()); response.headers().set(CONNECTION, HttpHeaders.Values.CLOSE); return this; } public void setHttpRequest(HttpRequest request) throws BadRequestException, IOException { if (request == null) { LOG.error("HttpRequest not initialized"); throw new BadRequestException("HttpRequest not initialized"); } if (!request.getMethod().equals(HttpMethod.POST)) { LOG.error("Got invalid HTTP method: expecting only POST"); throw new BadRequestException("Incorrect method " + request.getMethod().toString() + ", expected POST"); } HttpDataFactory factory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE); HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(factory, request); InterfaceHttpData data = decoder.getBodyHttpData(HTTP_TEST_ATTRIBUTE); if (data == null) { LOG.error("HTTP Resolve request inccorect, {} attribute not found", HTTP_TEST_ATTRIBUTE); throw new BadRequestException("HTTP Resolve request inccorect, " + HTTP_TEST_ATTRIBUTE + " attribute not found"); } Attribute attribute = (Attribute) data; requestData = attribute.get(); LOG.trace("Name {}, type {} found, data size {}", data.getName(), data.getHttpDataType().name(), requestData.length); } public HttpResponse getHttpResponse() { return response; } } /** * Netty exception handler. * * @author Andrey Panasenko */ public class DefaultExceptionHandler extends ChannelInboundHandlerAdapter { @Override public final void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception { LOG.error("Exception caught", cause); HttpResponseStatus status; if (cause instanceof BadRequestException) { status = BAD_REQUEST; } else { status = INTERNAL_SERVER_ERROR; } String content = cause.getMessage(); FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.copiedBuffer(content, CharsetUtil.UTF_8)); response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8"); response.headers().set(CONTENT_LENGTH, response.content().readableBytes()); response.headers().set(CONNECTION, HttpHeaders.Values.CLOSE); ChannelFuture future = ctx.writeAndFlush(response); future.addListener(ChannelFutureListener.CLOSE); ctx.close(); } } /** * Test client. * Generate HTTP request. * Open URLConnection to Netty. * Return bytes[] from HTTP response body. * * @author Andrey Panasenko */ public class HttpTestClient implements Runnable { /** * boundary size */ public static final int BOUNDARY_LENGTH = 35; /** * ContentType constant string */ public static final String CONTENT_TYPE_CONST = "multipart/form-data; boundary="; /** * ContentDisposition constant string */ public static final String CONTENT_DISPOSITION = "Content-Disposition: form-data; "; /** * Content name filed */ public static final String CONTENT_NAME = "name="; /** * CRLF */ public static final String crlf = "\r\n"; /** * The pool of ASCII chars to be used for generating a multipart boundary. */ private final char[] MULTIPART_CHARS = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" .toCharArray(); /** * Generated boundary */ private String boundary; /** * Random number generator */ private Random rnd = new Random(); private HttpURLConnection connection; private byte[] object; private byte[] response; private boolean responseComplete = false; private Object sync = new Object(); public HttpTestClient(String host, int port, byte[] object) throws MalformedURLException, IOException { String url = "http://" + host + ":" + port + "/test"; connection = (HttpURLConnection) new URL(url).openConnection(); this.object = object; boundary = getRandomString(BOUNDARY_LENGTH); } /* (non-Javadoc) * @see java.lang.Runnable#run() */ @Override public void run() { LOG.info("Run Http test to {}", connection.getURL().toString()); List<Byte> bodyArray = new Vector<>(); try { connection.setRequestMethod("POST"); connection.setDoOutput(true); connection.setRequestProperty("Content-Type", CONTENT_TYPE_CONST + boundary); DataOutputStream out = new DataOutputStream(connection.getOutputStream()); dumbObject(HTTP_TEST_ATTRIBUTE, object, out); out.flush(); out.close(); DataInputStream r = new DataInputStream(connection.getInputStream()); while (true) { bodyArray.add(new Byte(r.readByte())); } } catch (EOFException eof) { response = new byte[bodyArray.size()]; for (int i = 0; i < response.length; i++) { response[i] = bodyArray.get(i); } } catch (IOException e) { LOG.error("Error request HTTP to {}", connection.getURL().toString()); } finally { connection.disconnect(); synchronized (sync) { responseComplete = true; sync.notify(); } } } /** * Return HTTP response body. * Method blocks, until body received. * * @return byte[] */ public byte[] getResponseBody() { synchronized (sync) { if (!responseComplete) { try { sync.wait(); } catch (InterruptedException e) { LOG.error("Error wait ", e); } } } return response; } /** * generate random String. * * @param int size of String * @return random String */ public String getRandomString(int size) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < size; i++) { int j = rnd.nextInt(MULTIPART_CHARS.length); sb.append(MULTIPART_CHARS[j]); } return sb.toString(); } /** * Write multipart entity * * @param name multipart entity name * @param bytes multipart entity body * @param out DataOutputStream connection stream * @throws IOException throws in case ot write error. */ public void dumbObject(String name, byte[] bytes, DataOutputStream out) throws IOException { out.writeBytes("--" + boundary + crlf); out.writeBytes(CONTENT_DISPOSITION + CONTENT_NAME + "\"" + name + "\"" + crlf); out.writeBytes("Content-Type: application/octet-stream" + crlf); out.writeBytes(crlf); out.write(bytes); out.writeBytes(crlf); out.writeBytes("--" + boundary + "--" + crlf); } } }