Fix the parsing issue in large post request for sentinel-transport-simple-http (#1255)
This commit is contained in:
parent
2c2f60cf58
commit
05e3caf618
|
|
@ -4,6 +4,9 @@
|
||||||
out
|
out
|
||||||
gen
|
gen
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
# Maven
|
# Maven
|
||||||
target/
|
target/
|
||||||
pom.xml.tag
|
pom.xml.tag
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,10 @@
|
||||||
<artifactId>sentinel-transport-common</artifactId>
|
<artifactId>sentinel-transport-common</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,9 +15,22 @@
|
||||||
*/
|
*/
|
||||||
package com.alibaba.csp.sentinel.transport.command.http;
|
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.Closeable;
|
||||||
import java.io.InputStreamReader;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.OutputStreamWriter;
|
import java.io.OutputStreamWriter;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
|
|
@ -25,15 +38,9 @@ import java.io.UnsupportedEncodingException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.nio.charset.Charset;
|
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.
|
* The task handles incoming command request in HTTP protocol.
|
||||||
|
|
@ -42,6 +49,7 @@ import com.alibaba.csp.sentinel.util.StringUtil;
|
||||||
* @author Eric Zhao
|
* @author Eric Zhao
|
||||||
*/
|
*/
|
||||||
public class HttpEventTask implements Runnable {
|
public class HttpEventTask implements Runnable {
|
||||||
|
private static final String SERVER_ERROR_MESSAGE = "Command server error";
|
||||||
|
|
||||||
private final Socket socket;
|
private final Socket socket;
|
||||||
|
|
||||||
|
|
@ -61,86 +69,30 @@ public class HttpEventTask implements Runnable {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BufferedReader in = null;
|
|
||||||
PrintWriter printWriter = null;
|
PrintWriter printWriter = null;
|
||||||
|
InputStream inputStream = null;
|
||||||
try {
|
try {
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), SentinelConfig.charset()));
|
inputStream = new BufferedInputStream(socket.getInputStream());
|
||||||
OutputStream outputStream = socket.getOutputStream();
|
OutputStream outputStream = socket.getOutputStream();
|
||||||
|
|
||||||
printWriter = new PrintWriter(
|
printWriter = new PrintWriter(
|
||||||
new OutputStreamWriter(outputStream, Charset.forName(SentinelConfig.charset())));
|
new OutputStreamWriter(outputStream, Charset.forName(SentinelConfig.charset())));
|
||||||
|
|
||||||
String line = in.readLine();
|
String firstLine = readLine(inputStream);
|
||||||
CommandCenterLog.info("[SimpleHttpCommandCenter] Socket income: " + line
|
CommandCenterLog.info("[SimpleHttpCommandCenter] Socket income: " + firstLine
|
||||||
+ ", addr: " + socket.getInetAddress());
|
+ ", 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
|
// Deal with post method
|
||||||
// Now simple-http only support form-encoded post request.
|
processPostRequest(inputStream, 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the target command.
|
// Validate the target command.
|
||||||
String commandName = HttpCommandUtils.getTarget(request);
|
String commandName = HttpCommandUtils.getTarget(request);
|
||||||
if (StringUtil.isBlank(commandName)) {
|
if (StringUtil.isBlank(commandName)) {
|
||||||
badRequest(printWriter, "Invalid command");
|
writeResponse(printWriter, StatusCode.BAD_REQUEST, "Invalid command");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,23 +100,25 @@ public class HttpEventTask implements Runnable {
|
||||||
CommandHandler<?> commandHandler = SimpleHttpCommandCenter.getHandler(commandName);
|
CommandHandler<?> commandHandler = SimpleHttpCommandCenter.getHandler(commandName);
|
||||||
if (commandHandler != null) {
|
if (commandHandler != null) {
|
||||||
CommandResponse<?> response = commandHandler.handle(request);
|
CommandResponse<?> response = commandHandler.handle(request);
|
||||||
handleResponse(response, printWriter, outputStream);
|
handleResponse(response, printWriter);
|
||||||
} else {
|
} else {
|
||||||
// No matching command handler.
|
// No matching command handler.
|
||||||
badRequest(printWriter, "Unknown command `" + commandName + '`');
|
writeResponse(printWriter, StatusCode.BAD_REQUEST, "Unknown command `" + commandName + '`');
|
||||||
}
|
}
|
||||||
printWriter.flush();
|
|
||||||
|
|
||||||
long cost = System.currentTimeMillis() - start;
|
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");
|
+ ", address: " + socket.getInetAddress() + ", time cost: " + cost + " ms");
|
||||||
|
} catch (RequestException e) {
|
||||||
|
writeResponse(printWriter, e.getStatusCode(), e.getMessage());
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
CommandCenterLog.warn("[SimpleHttpCommandCenter] CommandCenter error", e);
|
CommandCenterLog.warn("[SimpleHttpCommandCenter] CommandCenter error", e);
|
||||||
try {
|
try {
|
||||||
if (printWriter != null) {
|
if (printWriter != null) {
|
||||||
String errorMessage = SERVER_ERROR_MESSAGE;
|
String errorMessage = SERVER_ERROR_MESSAGE;
|
||||||
|
e.printStackTrace();
|
||||||
if (!writtenHead) {
|
if (!writtenHead) {
|
||||||
internalError(printWriter, errorMessage);
|
writeResponse(printWriter, StatusCode.INTERNAL_SERVER_ERROR, errorMessage);
|
||||||
} else {
|
} else {
|
||||||
printWriter.println(errorMessage);
|
printWriter.println(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
@ -174,12 +128,170 @@ public class HttpEventTask implements Runnable {
|
||||||
CommandCenterLog.warn("[SimpleHttpCommandCenter] Close server socket failed", e);
|
CommandCenterLog.warn("[SimpleHttpCommandCenter] Close server socket failed", e);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
closeResource(in);
|
closeResource(inputStream);
|
||||||
closeResource(printWriter);
|
closeResource(printWriter);
|
||||||
closeResource(socket);
|
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<String, String> 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<String, String> parsePostHeaders(InputStream in) throws IOException {
|
||||||
|
Map<String, String> headerMap = new HashMap<String, String>(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) {
|
private void closeResource(Closeable closeable) {
|
||||||
if (closeable != null) {
|
if (closeable != null) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -190,56 +302,31 @@ public class HttpEventTask implements Runnable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleResponse(CommandResponse response, /*@NonNull*/ final PrintWriter printWriter,
|
private <T> void handleResponse(CommandResponse<T> response, final PrintWriter printWriter) throws Exception {
|
||||||
/*@NonNull*/ final OutputStream rawOutputStream) throws Exception {
|
|
||||||
if (response.isSuccess()) {
|
if (response.isSuccess()) {
|
||||||
if (response.getResult() == null) {
|
if (response.getResult() == null) {
|
||||||
writeOkStatusLine(printWriter);
|
writeResponse(printWriter, StatusCode.OK, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Write 200 OK status line.
|
|
||||||
writeOkStatusLine(printWriter);
|
|
||||||
// Here we directly use `toString` to encode the result to plain text.
|
// Here we directly use `toString` to encode the result to plain text.
|
||||||
byte[] buffer = response.getResult().toString().getBytes(SentinelConfig.charset());
|
byte[] buffer = response.getResult().toString().getBytes(SentinelConfig.charset());
|
||||||
rawOutputStream.write(buffer);
|
writeResponse(printWriter, StatusCode.OK, new String(buffer));
|
||||||
rawOutputStream.flush();
|
|
||||||
} else {
|
} else {
|
||||||
String msg = SERVER_ERROR_MESSAGE;
|
String msg = SERVER_ERROR_MESSAGE;
|
||||||
if (response.getException() != null) {
|
if (response.getException() != null) {
|
||||||
msg = response.getException().getMessage();
|
msg = response.getException().getMessage();
|
||||||
}
|
}
|
||||||
badRequest(printWriter, msg);
|
writeResponse(printWriter, StatusCode.BAD_REQUEST, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void writeResponse(PrintWriter out, StatusCode statusCode, String message) {
|
||||||
* Write `400 Bad Request` HTTP response status line and message body, then flush.
|
out.print("HTTP/1.0 " + statusCode.toString() + "\r\n"
|
||||||
*/
|
+ "Content-Length: " + (message == null ? 0 : message.getBytes().length) + "\r\n"
|
||||||
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");
|
+ "Connection: close\r\n\r\n");
|
||||||
|
if (message != null) {
|
||||||
out.print(message);
|
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");
|
|
||||||
out.flush();
|
out.flush();
|
||||||
writtenHead = true;
|
writtenHead = true;
|
||||||
}
|
}
|
||||||
|
|
@ -250,7 +337,7 @@ public class HttpEventTask implements Runnable {
|
||||||
* @param line HTTP request line
|
* @param line HTTP request line
|
||||||
* @return parsed command request
|
* @return parsed command request
|
||||||
*/
|
*/
|
||||||
private CommandRequest parseRequest(String line) {
|
protected static CommandRequest processQueryString(String line) {
|
||||||
CommandRequest request = new CommandRequest();
|
CommandRequest request = new CommandRequest();
|
||||||
if (StringUtil.isBlank(line)) {
|
if (StringUtil.isBlank(line)) {
|
||||||
return request;
|
return request;
|
||||||
|
|
@ -268,27 +355,48 @@ public class HttpEventTask implements Runnable {
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void parseParams(String queryString, CommandRequest request) {
|
/**
|
||||||
for (String parameter : queryString.split("&")) {
|
* Truncate query from "a=1&b=2#mark" to "a=1&b=2"
|
||||||
if (StringUtil.isBlank(parameter)) {
|
*
|
||||||
continue;
|
* @param str
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
protected static String removeAnchor(String str) {
|
||||||
|
if (str == null || str.length() == 0) {
|
||||||
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] keyValue = parameter.split("=");
|
int anchor = str.indexOf('#');
|
||||||
if (keyValue.length != 2) {
|
|
||||||
continue;
|
if (anchor == 0) {
|
||||||
|
return "";
|
||||||
|
} else if (anchor > 0) {
|
||||||
|
return str.substring(0, anchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
String value = StringUtil.trim(keyValue[1]);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
String value = StringUtil.trim(single.substring(index + 1));
|
||||||
|
String key = StringUtil.trim(single.substring(0, index));
|
||||||
try {
|
try {
|
||||||
|
key = URLDecoder.decode(key, SentinelConfig.charset());
|
||||||
value = URLDecoder.decode(value, SentinelConfig.charset());
|
value = URLDecoder.decode(value, SentinelConfig.charset());
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
request.addParam(StringUtil.trim(keyValue[0]), value);
|
request.addParam(key, value);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String SERVER_ERROR_MESSAGE = "Command server error";
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue