|
|
@@ -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<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) { |
|
|
|
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 <T> void handleResponse(CommandResponse<T> 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); |
|
|
|
} |
|
|
|
|
|
|
|
} |