diff --git a/.gitignore b/.gitignore index 84c94f69..44f72053 100755 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ out gen +# Visual Studio Code +.history/ + # Maven target/ pom.xml.tag diff --git a/sentinel-transport/sentinel-transport-simple-http/pom.xml b/sentinel-transport/sentinel-transport-simple-http/pom.xml index a588e490..b7121aba 100755 --- a/sentinel-transport/sentinel-transport-simple-http/pom.xml +++ b/sentinel-transport/sentinel-transport-simple-http/pom.xml @@ -17,5 +17,10 @@ sentinel-transport-common ${project.version} + + junit + junit + test + \ No newline at end of file diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/exception/RequestException.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/exception/RequestException.java new file mode 100644 index 00000000..3b778090 --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/exception/RequestException.java @@ -0,0 +1,43 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.transport.command.exception; + +import com.alibaba.csp.sentinel.transport.command.http.StatusCode; + +/** + * Represent exception with status code processing a request + * + * @author jason + * + */ +public class RequestException extends Exception { + private static final long serialVersionUID = 1L; + + private StatusCode statusCode = StatusCode.BAD_REQUEST; + + public RequestException() { + super(); + } + + public RequestException(StatusCode statusCode, String msg) { + super(msg); + this.statusCode = statusCode; + } + + public StatusCode getStatusCode() { + return statusCode; + } +} diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTask.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTask.java index 7658955b..e9fb9e2b 100755 --- a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTask.java +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTask.java @@ -15,9 +15,22 @@ */ package com.alibaba.csp.sentinel.transport.command.http; -import java.io.BufferedReader; +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.log.CommandCenterLog; +import com.alibaba.csp.sentinel.log.RecordLog; +import com.alibaba.csp.sentinel.transport.command.SimpleHttpCommandCenter; +import com.alibaba.csp.sentinel.transport.command.exception.RequestException; +import com.alibaba.csp.sentinel.transport.util.HttpCommandUtils; +import com.alibaba.csp.sentinel.util.StringUtil; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; import java.io.Closeable; -import java.io.InputStreamReader; +import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; @@ -25,15 +38,9 @@ import java.io.UnsupportedEncodingException; import java.net.Socket; import java.net.URLDecoder; import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; -import com.alibaba.csp.sentinel.command.CommandHandler; -import com.alibaba.csp.sentinel.command.CommandRequest; -import com.alibaba.csp.sentinel.command.CommandResponse; -import com.alibaba.csp.sentinel.config.SentinelConfig; -import com.alibaba.csp.sentinel.log.CommandCenterLog; -import com.alibaba.csp.sentinel.transport.command.SimpleHttpCommandCenter; -import com.alibaba.csp.sentinel.transport.util.HttpCommandUtils; -import com.alibaba.csp.sentinel.util.StringUtil; /*** * The task handles incoming command request in HTTP protocol. @@ -42,6 +49,7 @@ import com.alibaba.csp.sentinel.util.StringUtil; * @author Eric Zhao */ public class HttpEventTask implements Runnable { + private static final String SERVER_ERROR_MESSAGE = "Command server error"; private final Socket socket; @@ -61,86 +69,30 @@ public class HttpEventTask implements Runnable { return; } - BufferedReader in = null; PrintWriter printWriter = null; + InputStream inputStream = null; try { long start = System.currentTimeMillis(); - in = new BufferedReader(new InputStreamReader(socket.getInputStream(), SentinelConfig.charset())); + inputStream = new BufferedInputStream(socket.getInputStream()); OutputStream outputStream = socket.getOutputStream(); printWriter = new PrintWriter( new OutputStreamWriter(outputStream, Charset.forName(SentinelConfig.charset()))); - String line = in.readLine(); - CommandCenterLog.info("[SimpleHttpCommandCenter] Socket income: " + line + String firstLine = readLine(inputStream); + CommandCenterLog.info("[SimpleHttpCommandCenter] Socket income: " + firstLine + ", addr: " + socket.getInetAddress()); - CommandRequest request = parseRequest(line); + CommandRequest request = processQueryString(firstLine); - if (line.length() > 4 && StringUtil.equalsIgnoreCase("POST", line.substring(0, 4))) { + if (firstLine.length() > 4 && StringUtil.equalsIgnoreCase("POST", firstLine.substring(0, 4))) { // Deal with post method - // Now simple-http only support form-encoded post request. - String bodyLine = null; - boolean bodyNext = false; - boolean supported = false; - int maxLength = 8192; - while (true) { - // Body processing - if (bodyNext) { - if (!supported) { - break; - } - char[] bodyBytes = new char[maxLength]; - int read = in.read(bodyBytes); - String postData = new String(bodyBytes, 0, read); - parseParams(postData, request); - break; - } - - bodyLine = in.readLine(); - if (bodyLine == null) { - break; - } - // Body separator - if (StringUtil.isEmpty(bodyLine)) { - bodyNext = true; - continue; - } - // Header processing - int index = bodyLine.indexOf(":"); - if (index < 1) { - continue; - } - String headerName = bodyLine.substring(0, index); - String header = bodyLine.substring(index + 1).trim(); - if (StringUtil.equalsIgnoreCase("content-type", headerName)) { - int idx = header.indexOf(";"); - if (idx > 0) { - header = header.substring(0, idx).trim(); - } - if (StringUtil.equals("application/x-www-form-urlencoded", header)) { - supported = true; - } else { - CommandCenterLog.warn("Content-Type not supported: " + header); - // not support request - break; - } - } else if (StringUtil.equalsIgnoreCase("content-length", headerName)) { - try { - int len = Integer.parseInt(header); - if (len > 0) { - maxLength = len; - } - } catch (Exception e) { - CommandCenterLog.warn("Malformed content-length header value: " + header); - } - } - } + processPostRequest(inputStream, request); } // Validate the target command. String commandName = HttpCommandUtils.getTarget(request); if (StringUtil.isBlank(commandName)) { - badRequest(printWriter, "Invalid command"); + writeResponse(printWriter, StatusCode.BAD_REQUEST, "Invalid command"); return; } @@ -148,23 +100,25 @@ public class HttpEventTask implements Runnable { CommandHandler commandHandler = SimpleHttpCommandCenter.getHandler(commandName); if (commandHandler != null) { CommandResponse response = commandHandler.handle(request); - handleResponse(response, printWriter, outputStream); + handleResponse(response, printWriter); } else { // No matching command handler. - badRequest(printWriter, "Unknown command `" + commandName + '`'); + writeResponse(printWriter, StatusCode.BAD_REQUEST, "Unknown command `" + commandName + '`'); } - printWriter.flush(); long cost = System.currentTimeMillis() - start; - CommandCenterLog.info("[SimpleHttpCommandCenter] Deal a socket task: " + line + CommandCenterLog.info("[SimpleHttpCommandCenter] Deal a socket task: " + firstLine + ", address: " + socket.getInetAddress() + ", time cost: " + cost + " ms"); + } catch (RequestException e) { + writeResponse(printWriter, e.getStatusCode(), e.getMessage()); } catch (Throwable e) { CommandCenterLog.warn("[SimpleHttpCommandCenter] CommandCenter error", e); try { if (printWriter != null) { String errorMessage = SERVER_ERROR_MESSAGE; + e.printStackTrace(); if (!writtenHead) { - internalError(printWriter, errorMessage); + writeResponse(printWriter, StatusCode.INTERNAL_SERVER_ERROR, errorMessage); } else { printWriter.println(errorMessage); } @@ -174,11 +128,169 @@ public class HttpEventTask implements Runnable { CommandCenterLog.warn("[SimpleHttpCommandCenter] Close server socket failed", e); } } finally { - closeResource(in); + closeResource(inputStream); closeResource(printWriter); closeResource(socket); } } + + private static String readLine(InputStream in) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(64); + int data; + while (true) { + data = in.read(); + if (data < 0) { + break; + } + if (data == '\n') { + break; + } + bos.write(data); + } + byte[] arr = bos.toByteArray(); + if (arr.length > 0 && arr[arr.length - 1] == '\r') { + return new String(arr, 0, arr.length - 1, SentinelConfig.charset()); + } + return new String(arr, SentinelConfig.charset()); + } + + /** + * Try to process the body of POST request additionally. + * + * @param in + * @param request + * @throws RequestException + * @throws IOException + */ + protected static void processPostRequest(InputStream in, CommandRequest request) + throws RequestException, IOException { + Map headerMap = parsePostHeaders(in); + + if (headerMap == null) { + // illegal request + RecordLog.warn("Illegal request read"); + throw new RequestException(StatusCode.BAD_REQUEST, ""); + } + + if (headerMap.containsKey("content-type") && !checkSupport(headerMap.get("content-type"))) { + // not support Content-type + RecordLog.warn("Not supported Content-Type: {}", headerMap.get("content-type")); + throw new RequestException(StatusCode.UNSUPPORTED_MEDIA_TYPE, + "Only form-encoded post request is supported"); + } + + int bodyLength = 0; + try { + bodyLength = Integer.parseInt(headerMap.get("content-length")); + } catch (Exception e) { + } + if (bodyLength < 1) { + // illegal request without Content-length header + RecordLog.warn("No available Content-Length in headers"); + throw new RequestException(StatusCode.LENGTH_REQUIRED, "No legal Content-Length"); + } + + parseParams(readBody(in, bodyLength), request); + } + + /** + * Process header line in request + * + * @param in + * @return return headers in a Map, null for illegal request + * @throws IOException + */ + protected static Map parsePostHeaders(InputStream in) throws IOException { + Map headerMap = new HashMap(4); + String line; + while (true) { + line = readLine(in); + if (line == null || line.length() == 0) { + // empty line + return headerMap; + } + int index = line.indexOf(":"); + if (index < 1) { + // empty value, abandon + continue; + } + String headerName = line.substring(0, index).trim().toLowerCase(); + String headerValue = line.substring(index + 1).trim(); + if (headerValue.length() > 0) { + headerMap.put(headerName, headerValue); + } + } + } + + private static boolean checkSupport(String contentType) { + int idx = contentType.indexOf(";"); + String type; + if (idx > 0) { + type = contentType.substring(0, idx).toLowerCase().trim(); + } else { + type = contentType.toLowerCase(); + } + // Actually in RFC "x-*" shouldn't have any properties like "type/subtype; key=val" + // But some library do add it. So we will be compatible with that but force to + // encoding specified in configuration as legacy processing will do. + if (!type.contains("application/x-www-form-urlencoded")) { + CommandCenterLog.warn("Content-Type not supported: " + contentType); + // Not supported request type + // Now simple-http only support form-encoded post request. + return false; + } + return true; + } + + private static String readBody(InputStream in, int bodyLength) + throws IOException, RequestException { + byte[] buf = new byte[bodyLength]; + int pos = 0; + while (pos < bodyLength) { + int l = in.read(buf, pos, Math.min(512, bodyLength - pos)); + if (l < 0) { + break; + } + if (l == 0) { + continue; + } + pos += l; + } + // Only allow partial + return new String(buf, 0, pos, SentinelConfig.charset()); + } + + /** + * Consume all the body submitted and parse params into {@link CommandRequest} + * + * @param queryString + * @param request + */ + protected static void parseParams(String queryString, CommandRequest request) { + if (queryString == null || queryString.length() < 1) { + return; + } + + int offset = 0, pos = -1; + + // check anchor + queryString = removeAnchor(queryString); + + while (true) { + offset = pos + 1; + pos = queryString.indexOf('&', offset); + if (offset == pos) { + // empty + continue; + } + parseSingleParam(queryString.substring(offset, pos == -1 ? queryString.length() : pos), request); + + if (pos < 0) { + // reach the end + break; + } + } + } private void closeResource(Closeable closeable) { if (closeable != null) { @@ -190,56 +302,31 @@ public class HttpEventTask implements Runnable { } } - private void handleResponse(CommandResponse response, /*@NonNull*/ final PrintWriter printWriter, - /*@NonNull*/ final OutputStream rawOutputStream) throws Exception { + private void handleResponse(CommandResponse response, final PrintWriter printWriter) throws Exception { if (response.isSuccess()) { if (response.getResult() == null) { - writeOkStatusLine(printWriter); + writeResponse(printWriter, StatusCode.OK, null); return; } - // Write 200 OK status line. - writeOkStatusLine(printWriter); // Here we directly use `toString` to encode the result to plain text. byte[] buffer = response.getResult().toString().getBytes(SentinelConfig.charset()); - rawOutputStream.write(buffer); - rawOutputStream.flush(); + writeResponse(printWriter, StatusCode.OK, new String(buffer)); } else { String msg = SERVER_ERROR_MESSAGE; if (response.getException() != null) { msg = response.getException().getMessage(); } - badRequest(printWriter, msg); + writeResponse(printWriter, StatusCode.BAD_REQUEST, msg); } } - - /** - * Write `400 Bad Request` HTTP response status line and message body, then flush. - */ - private void badRequest(/*@NonNull*/ final PrintWriter out, String message) { - out.print("HTTP/1.1 400 Bad Request\r\n" - + "Connection: close\r\n\r\n"); - out.print(message); - out.flush(); - writtenHead = true; - } - - /** - * Write `500 Internal Server Error` HTTP response status line and message body, then flush. - */ - private void internalError(/*@NonNull*/ final PrintWriter out, String message) { - out.print("HTTP/1.1 500 Internal Server Error\r\n" - + "Connection: close\r\n\r\n"); - out.print(message); - out.flush(); - writtenHead = true; - } - - /** - * Write `200 OK` HTTP response status line and flush. - */ - private void writeOkStatusLine(/*@NonNull*/ final PrintWriter out) { - out.print("HTTP/1.1 200 OK\r\n" - + "Connection: close\r\n\r\n"); + + private void writeResponse(PrintWriter out, StatusCode statusCode, String message) { + out.print("HTTP/1.0 " + statusCode.toString() + "\r\n" + + "Content-Length: " + (message == null ? 0 : message.getBytes().length) + "\r\n" + + "Connection: close\r\n\r\n"); + if (message != null) { + out.print(message); + } out.flush(); writtenHead = true; } @@ -250,7 +337,7 @@ public class HttpEventTask implements Runnable { * @param line HTTP request line * @return parsed command request */ - private CommandRequest parseRequest(String line) { + protected static CommandRequest processQueryString(String line) { CommandRequest request = new CommandRequest(); if (StringUtil.isBlank(line)) { return request; @@ -267,28 +354,49 @@ public class HttpEventTask implements Runnable { parseParams(parameterStr, request); return request; } - - private void parseParams(String queryString, CommandRequest request) { - for (String parameter : queryString.split("&")) { - if (StringUtil.isBlank(parameter)) { - continue; - } - - String[] keyValue = parameter.split("="); - if (keyValue.length != 2) { - continue; - } - - String value = StringUtil.trim(keyValue[1]); - try { - value = URLDecoder.decode(value, SentinelConfig.charset()); - } catch (UnsupportedEncodingException e) { - } - - request.addParam(StringUtil.trim(keyValue[0]), value); + + /** + * Truncate query from "a=1&b=2#mark" to "a=1&b=2" + * + * @param str + * @return + */ + protected static String removeAnchor(String str) { + if (str == null || str.length() == 0) { + return str; } + + int anchor = str.indexOf('#'); + + if (anchor == 0) { + return ""; + } else if (anchor > 0) { + return str.substring(0, anchor); + } + + return str; } + + protected static void parseSingleParam(String single, CommandRequest request) { + if (single == null || single.length() < 3) { + return; + } + + int index = single.indexOf('='); + if (index <= 0 || index >= single.length() - 1) { + // empty key/val or nothing found + return; + } - private static final String SERVER_ERROR_MESSAGE = "Command server error"; + String value = StringUtil.trim(single.substring(index + 1)); + String key = StringUtil.trim(single.substring(0, index)); + try { + key = URLDecoder.decode(key, SentinelConfig.charset()); + value = URLDecoder.decode(value, SentinelConfig.charset()); + } catch (UnsupportedEncodingException e) { + } + + request.addParam(key, value); + } } diff --git a/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/StatusCode.java b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/StatusCode.java new file mode 100644 index 00000000..c8eada37 --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/main/java/com/alibaba/csp/sentinel/transport/command/http/StatusCode.java @@ -0,0 +1,48 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.transport.command.http; + +public enum StatusCode { + OK(200, "OK"), + BAD_REQUEST(400, "Bad Request"), + REQUEST_TIMEOUT(408, "Request Timeout"), + LENGTH_REQUIRED(411, "Length Required"), + UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"), + INTERNAL_SERVER_ERROR(500, "Internal Server Error"); + + private int code; + private String desc; + private String representation; + + private StatusCode(int code, String desc) { + this.code = code; + this.desc = desc; + this.representation = code + " " + desc; + } + + public int getCode() { + return code; + } + + public String getDesc() { + return desc; + } + + @Override + public String toString() { + return representation; + } +} diff --git a/sentinel-transport/sentinel-transport-simple-http/src/test/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTaskTest.java b/sentinel-transport/sentinel-transport-simple-http/src/test/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTaskTest.java new file mode 100644 index 00000000..3b669223 --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/test/java/com/alibaba/csp/sentinel/transport/command/http/HttpEventTaskTest.java @@ -0,0 +1,246 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.transport.command.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.util.Arrays; +import java.util.Map; + +import org.junit.Test; + +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.transport.command.exception.RequestException; + +public class HttpEventTaskTest { + + @Test + public void processQueryString() { + CommandRequest request; + + request = HttpEventTask.processQueryString(null); + assertNotNull(request); + + request = HttpEventTask.processQueryString(null); + assertNotNull(request); + + request = HttpEventTask.processQueryString("get /?a=1&b=2&c=3#mark HTTP/1.0"); + assertNotNull(request); + assertEquals("1", request.getParam("a")); + assertEquals("2", request.getParam("b")); + assertEquals("3", request.getParam("c")); + + request = HttpEventTask.processQueryString("post /test?a=3&b=4&c=3#mark HTTP/1.0"); + assertNotNull(request); + assertEquals("3", request.getParam("a")); + assertEquals("4", request.getParam("b")); + assertEquals("3", request.getParam("c")); + } + + @Test + public void removeAnchor() { + assertNull(HttpEventTask.removeAnchor(null)); + assertEquals("", HttpEventTask.removeAnchor("")); + assertEquals("", HttpEventTask.removeAnchor("#mark")); + assertEquals("a", HttpEventTask.removeAnchor("a#mark")); + } + + @Test + public void parseSingleParam() { + CommandRequest request; + + request = new CommandRequest(); + HttpEventTask.parseSingleParam(null, request); + assertEquals(0, request.getParameters().size()); + + request = new CommandRequest(); + HttpEventTask.parseSingleParam("", request); + assertEquals(0, request.getParameters().size()); + + request = new CommandRequest(); + HttpEventTask.parseSingleParam("a", request); + assertEquals(0, request.getParameters().size()); + + request = new CommandRequest(); + HttpEventTask.parseSingleParam("=", request); + assertEquals(0, request.getParameters().size()); + + request = new CommandRequest(); + HttpEventTask.parseSingleParam("a=", request); + assertEquals(0, request.getParameters().size()); + + request = new CommandRequest(); + HttpEventTask.parseSingleParam("=a", request); + assertEquals(0, request.getParameters().size()); + + request = new CommandRequest(); + HttpEventTask.parseSingleParam("test=", request); + assertEquals(0, request.getParameters().size()); + + request = new CommandRequest(); + HttpEventTask.parseSingleParam("=test", request); + assertEquals(0, request.getParameters().size()); + + request = new CommandRequest(); + HttpEventTask.parseSingleParam("a=1", request); + assertEquals(1, request.getParameters().size()); + assertEquals("1", request.getParam("a")); + + request = new CommandRequest(); + HttpEventTask.parseSingleParam("a_+=1+", request); + assertEquals(1, request.getParameters().size()); + assertEquals("1 ", request.getParam("a_ ")); + } + + @Test + public void parseParams() { + CommandRequest request; + + // mixed + request = new CommandRequest(); + HttpEventTask.parseParams("a=1&&b&=3&&c=4&a_+1=3_3%20&%E7%9A%84=test%E7%9A%84#mark", request); + assertEquals(4, request.getParameters().size()); + assertEquals("1", request.getParam("a")); + assertNull(request.getParam("b")); + assertEquals("4", request.getParam("c")); + assertEquals("3_3 ", request.getParam("a_ 1")); + assertEquals("test的", request.getParam("的")); + + request = new CommandRequest(); + HttpEventTask.parseParams(null, request); + assertEquals(0, request.getParameters().size()); + + request = new CommandRequest(); + HttpEventTask.parseParams("", request); + assertEquals(0, request.getParameters().size()); + + request = new CommandRequest(); + HttpEventTask.parseParams("&&b&=3&", request); + assertEquals(0, request.getParameters().size()); + } + + @Test + public void parsePostHeaders() throws IOException { + Map map; + + map = HttpEventTask.parsePostHeaders(new ByteArrayInputStream("".getBytes())); + assertTrue(map.size() == 0); + + map = HttpEventTask.parsePostHeaders(new ByteArrayInputStream("Content-type: test \r\n\r\nbody".getBytes())); + assertEquals("test", map.get("content-type")); + + map = HttpEventTask.parsePostHeaders(new ByteArrayInputStream("Content-Encoding: utf-8\r\n\r\nbody".getBytes())); + assertEquals("utf-8", map.get("content-encoding")); + } + + @Test + public void processPostRequest() throws IOException { + CommandRequest request; + + request = new CommandRequest(); + request.addParam("a", "1"); + + // illegal(empty) request + try { + HttpEventTask.processPostRequest(new ByteArrayInputStream("".getBytes()), request); + assertFalse(true); // should not reach here + } catch (Exception e) { + assertTrue(e instanceof RequestException); + } + assertEquals("1", request.getParam("a")); + + // normal request + try { + HttpEventTask.processPostRequest(new ByteArrayInputStream(("Host: demo.com\r\n" + + "Accept: */*\r\n" + + "Accept-Language: en-us\r\n" + + "Accept-Encoding: gzip, deflate\r\n" + + "Content-Type: application/x-www-form-urlencoded; charset=UTF-8\r\n" + + "Connection: keep-alive\r\n" + + "Content-Length: 10\r\n" + + "\r\n" + + "a=3&b=5的").getBytes()), request); + assertEquals("3", request.getParam("a")); + assertEquals("5的", request.getParam("b")); + } catch (Exception e) { + assertTrue(false); // should not reach here + } + + // not supported request + try { + HttpEventTask.processPostRequest(new ByteArrayInputStream(("Host: demo.com\r\n" + + "Accept: */*\r\n" + + "Accept-Language: en-us\r\n" + + "Accept-Encoding: gzip, deflate\r\n" + + "Content-Type: application/json\r\n" + + "Connection: keep-alive\r\n" + + "Content-Length: 7\r\n" + + "\r\n" + + "a=1&b=2").getBytes()), request); + assertTrue(false); // should not reach here + } catch (RequestException e) { + assertTrue(e.getStatusCode() == StatusCode.UNSUPPORTED_MEDIA_TYPE); + } + + // Capacity test + char[] buf = new char[1024 * 1024]; + Arrays.fill(buf, '&'); + String padding = new String(buf); + try { + request = new CommandRequest(); + HttpEventTask.processPostRequest(new ByteArrayInputStream(("Host: demo.com\r\n" + + "Accept: */*\r\n" + + "Accept-Language: en-us\r\n" + + "Accept-Encoding: gzip, deflate\r\n" + + "Content-Type: application/x-www-form-urlencoded\r\n" + + "Connection: keep-alive\r\n" + + "Content-Length: 7\r\n" + + "\r\n" + + padding + + "a=1&b=2").getBytes()), request); + assertEquals(0, request.getParameters().size()); + } catch (Exception e) { + assertTrue(false); + } + try { + String querystring = "a+=+&b=%E7%9A%84的"; + request = new CommandRequest(); + HttpEventTask.processPostRequest(new ByteArrayInputStream(("Host: demo.com\r\n" + + "Accept: */*\r\n" + + "Accept-Language: en-us\r\n" + + "Accept-Encoding: gzip, deflate\r\n" + + "Content-Type: application/x-www-form-urlencoded\r\n" + + "Connection: keep-alive\r\n" + + "Content-Length: " + (padding.length() + querystring.getBytes().length) + "\r\n" + + "\r\n" + + padding + + querystring).getBytes()), request); + assertEquals(2, request.getParameters().size()); + assertEquals(" ", request.getParam("a ")); + assertEquals("的的", request.getParam("b")); + } catch (Exception e) { + assertTrue(false); + } + } +}