This commit is contained in:
hongqiaowei
2022-07-26 15:16:08 +08:00
parent 13220d35b5
commit ebaee5b62b
14 changed files with 629 additions and 92 deletions

View File

@@ -17,34 +17,35 @@
package we.plugin.grayrelease;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import org.apache.commons.lang3.StringUtils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.core.type.TypeReference;
import inet.ipaddr.AddressStringException;
import inet.ipaddr.IPAddress;
import inet.ipaddr.IPAddressSeqRange;
import inet.ipaddr.IPAddressString;
import ognl.Ognl;
import ognl.OgnlException;
import org.apache.logging.log4j.ThreadContext;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.NettyDataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.*;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import we.config.SystemConfig;
import we.dedicated_line.DedicatedLineService;
import we.plugin.FizzPluginFilterChain;
import we.plugin.auth.ApiConfig;
import we.plugin.requestbody.RequestBodyPlugin;
import we.spring.http.server.reactive.ext.FizzServerHttpRequestDecorator;
import we.spring.http.server.reactive.ext.FizzServerHttpResponseDecorator;
import we.util.Consts;
import we.util.NettyDataBufferUtils;
import we.util.WebUtils;
import we.proxy.Route;
import we.spring.web.server.ext.FizzServerWebExchangeDecorator;
import we.util.*;
import javax.annotation.Resource;
import java.util.Map;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author hongqiaowei
@@ -53,91 +54,438 @@ import java.util.Map;
@Component(GrayReleasePlugin.GRAY_RELEASE_PLUGIN)
public class GrayReleasePlugin extends RequestBodyPlugin {
private static final Logger log = LoggerFactory.getLogger(GrayReleasePlugin.class);
private static final Logger LOGGER = LoggerFactory.getLogger(GrayReleasePlugin.class);
public static final String GRAY_RELEASE_PLUGIN = "GrayReleasePlugin";
public static final String GRAY_RELEASE_PLUGIN = "GrayReleasePlugin";
@Resource
private SystemConfig systemConfig;
private static final String triggerCondition = "triggerCondition";
private static final String routeType = "routeType";
private static final String routeConfig = "routeConfig";
private static final String routeConfigMap = "routeConfigMap";
private static final String method = "method";
private static final String path = "path";
private static final String contentType = "contentType";
private static final String body = "body";
private static final String form = "form";
private static final String cookie = "cookie";
private static final String header = "header";
private static final String query = "query";
private static final String client = "client";
private static final String ip = "ip";
@Resource
private DedicatedLineService dedicatedLineService;
private static class OgnlRoot extends HashMap<String, Object> {
public double random() {
return Math.random();
}
public boolean exist(String key) {
String[] keys = StringUtils.split(key, Consts.S.COMMA);
Map<String, Object> m = this;
int keyLen = keys.length;
for (int i = 0; i < keyLen; i++) {
String k = keys[i];
if (m.containsKey(k)) {
Object obj = m.get(k);
if (obj instanceof Map) {
m = (Map<String, Object>) obj;
} else if (i + 1 != keyLen) {
return false;
}
} else {
return false;
}
}
return true;
}
public boolean matches(String key, String regex) throws OgnlException {
String value = (String) Ognl.getValue(key, this);
if (value == null) {
return false;
}
return value.matches(regex);
}
public String jwtClaim(String name) {
Map<String, Object> headerMap = (Map<String, Object>) get(GrayReleasePlugin.header);
if (headerMap == null) {
return null;
} else {
String token = (String) headerMap.get(HttpHeaders.AUTHORIZATION.toLowerCase());
if (StringUtils.isBlank(token)) {
return null;
} else if (token.length() > 7 && token.substring(0, 7).equalsIgnoreCase("Bearer ")) {
token = token.substring(7);
}
DecodedJWT jwt = JWT.decode(token);
Claim claim = jwt.getClaim(name);
if (claim == null) {
return null;
}
return claim.asString();
}
}
public boolean clientIpInRange(String range) throws AddressStringException {
Map<String, Object> cli = (Map<String, Object>) get(client);
if (cli == null) {
return false;
} else {
String pi = (String) cli.get(ip);
if (pi == null) {
return false;
} else {
return ipInRange(pi, range);
}
}
}
public boolean ipInRange(String ip, String range) throws AddressStringException {
IPAddress ipAddress = new IPAddressString(ip).toAddress();
IPAddress rangeAddress = new IPAddressString(range).getAddress();
return rangeAddress.contains(ipAddress);
}
public boolean ipInRange(String ip, String rangeStartIp, String rangeEndIp) throws AddressStringException {
IPAddress startIPAddress = new IPAddressString(rangeStartIp).getAddress();
IPAddress endIPAddress = new IPAddressString(rangeEndIp).getAddress();
IPAddressSeqRange ipRange = startIPAddress.spanWithRange(endIPAddress);
IPAddress ipAddress = new IPAddressString(ip).toAddress();
return ipRange.contains(ipAddress);
}
public String toString() {
return JacksonUtils.writeValueAsString(this);
}
}
@Override
public Mono<Void> doFilter(ServerWebExchange exchange, Map<String, Object> config) {
String traceId = WebUtils.getTraceId(exchange);
ThreadContext.put(Consts.TRACE_ID, traceId);
String tc = (String) config.get(triggerCondition);
Object ognlRoot = request2ognlContext(exchange);
Boolean conditionMatch = false;
try {
// LogService.setBizId(traceId);
String dedicatedLineId = WebUtils.getDedicatedLineId(exchange);
String cryptoKey = dedicatedLineService.getRequestCryptoKey(dedicatedLineId);
conditionMatch = (Boolean) Ognl.getValue(tc, ognlRoot);
} catch (OgnlException e) {
LOGGER.error("calc condition expression {} with context {}", tc, ognlRoot, e);
throw new RuntimeException(e);
}
if (conditionMatch) {
Route route = WebUtils.getRoute(exchange);
changeRoute(exchange, route, config);
if (route.type == ApiConfig.Type.DIRECT_RESPONSE) {
HttpHeaders hdrs = new HttpHeaders();
hdrs.setContentType(route.contentType);
return WebUtils.response(exchange, HttpStatus.OK, hdrs, route.body);
} else {
exchange.getAttributes().put(WebUtils.IGNORE_PLUGIN, Consts.S.EMPTY);
}
}
return FizzPluginFilterChain.next(exchange);
}
FizzServerHttpRequestDecorator request = (FizzServerHttpRequestDecorator) exchange.getRequest();
return request.getBody().defaultIfEmpty(NettyDataBufferUtils.EMPTY_DATA_BUFFER).single().flatMap(body -> {
if (body != NettyDataBufferUtils.EMPTY_DATA_BUFFER && systemConfig.fizzDedicatedLineClientRequestCrypto()) {
byte[] bodyBytes = request.getBodyBytes();
request.setBody(decrypt(bodyBytes, cryptoKey));
request.getHeaders().remove(HttpHeaders.CONTENT_LENGTH);
}
private Object request2ognlContext(ServerWebExchange exchange) {
OgnlRoot ognlRoot = new OgnlRoot();
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse original = exchange.getResponse();
FizzServerHttpResponseDecorator fizzServerHttpResponseDecorator = new FizzServerHttpResponseDecorator(original) {
@Override
public Publisher<? extends DataBuffer> writeWith(DataBuffer remoteResponseBody) {
if (remoteResponseBody == null || remoteResponseBody == NettyDataBufferUtils.EMPTY_DATA_BUFFER) {
return Mono.empty();
} else {
if (StringUtils.isNotBlank(cryptoKey)) {
getDelegate().getHeaders().remove(HttpHeaders.CONTENT_LENGTH);
byte[] bytes = remoteResponseBody.asByteBuffer().array();
NettyDataBuffer from = NettyDataBufferUtils.from(encrypt(bytes, cryptoKey));
return Mono.just(from);
} else {
return Mono.just(remoteResponseBody);
ognlRoot.put(method, request.getMethodValue().toLowerCase());
ognlRoot.put(path, WebUtils.getClientReqPath(exchange));
MultiValueMap<String, String> queryParams = request.getQueryParams();
if (!queryParams.isEmpty()) {
Map<String, Object> queryMap = new HashMap<>();
queryParams.forEach(
(name, values) -> {
if (CollectionUtils.isEmpty(values)) {
queryMap.put(name, null);
} else if (values.size() > 1) {
queryMap.put(name, values);
} else {
queryMap.put(name, values.get(0));
}
}
);
ognlRoot.put(query, queryMap);
}
HttpHeaders headers = request.getHeaders();
if (!headers.isEmpty()) {
Map<String, Object> headerMap = new HashMap<>();
headers.forEach(
(nm, values) -> {
String name = nm.toLowerCase();
if (CollectionUtils.isEmpty(values)) {
headerMap.put(name, null);
} else if (values.size() > 1) {
headerMap.put(name, values);
} else {
headerMap.put(name, values.get(0));
}
}
);
ognlRoot.put(header, headerMap);
}
MultiValueMap<String, HttpCookie> cookies = request.getCookies();
if (!CollectionUtils.isEmpty(cookies)) {
Map<String, Object> cookieMap = new HashMap<>();
cookies.forEach(
(name, values) -> {
if (CollectionUtils.isEmpty(values)) {
cookieMap.put(name, null);
} else if (values.size() > 1) {
List<String> lst = new ArrayList<>(values.size());
for (HttpCookie value : values) {
lst.add(value.getValue());
}
cookieMap.put(name, lst);
} else {
cookieMap.put(name, values.get(0).getValue());
}
}
);
ognlRoot.put(cookie, cookieMap);
}
MediaType reqContentType = request.getHeaders().getContentType();
if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(reqContentType)) {
exchange.getFormData()
.map(
formData -> {
if (formData == FizzServerWebExchangeDecorator.EMPTY_FORM_DATA) {
return null;
} else {
Map<String, Object> formMap = new HashMap<>();
formData.forEach(
(name, values) -> {
if (CollectionUtils.isEmpty(values)) {
formMap.put(name, null);
} else if (values.size() > 1) {
formMap.put(name, values);
} else {
formMap.put(name, values.get(0));
}
}
);
ognlRoot.put(form, formMap);
return formMap;
}
}
}
}
};
ServerWebExchange build = exchange.mutate().response(fizzServerHttpResponseDecorator).build();
return FizzPluginFilterChain.next(build);
});
)
.subscribe();
} else if (MediaType.APPLICATION_JSON.isCompatibleWith(reqContentType)) {
request.getBody()
.single()
.map(
bodyDataBuffer -> {
if (bodyDataBuffer == NettyDataBufferUtils.EMPTY_DATA_BUFFER) {
return null;
} else {
String json = bodyDataBuffer.toString(StandardCharsets.UTF_8).trim();
if (json.charAt(0) == Consts.S.LEFT_SQUARE_BRACKET) {
List<Object> bodyMap = JacksonUtils.readValue(json, new TypeReference<List<Object>>(){});
ognlRoot.put(body, bodyMap);
} else {
Map<String, Object> bodyMap = JacksonUtils.readValue(json, new TypeReference<Map<String, Object>>(){});
ognlRoot.put(body, bodyMap);
}
return null;
}
}
)
.subscribe();
}
} catch (Exception e) {
// log.error("{} {} Exception", traceId, DEDICATED_LINE_CODEC_PLUGIN_FILTER, LogService.BIZ_ID, traceId, e);
log.error("{} {} Exception", traceId, GRAY_RELEASE_PLUGIN, e);
String respJson = WebUtils.jsonRespBody(HttpStatus.INTERNAL_SERVER_ERROR.value(),
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), traceId);
return WebUtils.response(exchange, HttpStatus.INTERNAL_SERVER_ERROR, null, respJson);
String originIp = WebUtils.getOriginIp(exchange);
if (originIp != null) {
Map<String, Object> clientMap = new HashMap<>();
clientMap.put(ip, originIp);
ognlRoot.put(client, clientMap);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("request {} ognl root: {}", request.getId(), ognlRoot);
}
return ognlRoot;
}
private void changeRoute(ServerWebExchange exchange, Route route, Map<String, Object> pluginConfig) {
byte rt = ((Integer) pluginConfig.get(routeType)).byteValue();
route.type = rt;
Map<String, String> newRouteConfig = (Map<String, String>) pluginConfig.get(routeConfigMap);
if (newRouteConfig == null) {
newRouteConfig = routeConfig2map((String) pluginConfig.get(routeConfig));
pluginConfig.put(routeConfigMap, newRouteConfig);
pluginConfig.remove(routeConfig);
}
if (rt == ApiConfig.Type.SERVICE_DISCOVERY) {
changeServiceDiscoveryRoute(exchange, route, newRouteConfig);
} else if (rt == ApiConfig.Type.REVERSE_PROXY) {
changeReverseProxyRoute(exchange, pluginConfig, route, newRouteConfig);
} else if (rt == ApiConfig.Type.SERVICE_AGGREGATE) {
changeAggregateRoute(exchange, route, newRouteConfig);
} else {
String ct = (String) pluginConfig.get(contentType);
String b = (String) pluginConfig.get(body);
route.contentType(MediaType.valueOf(ct))
.body(b);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("route is changed: {}", route);
}
}
/*public String encrypt(String data, String secretKey) {
if (StringUtils.isBlank(data)) {
return data;
private void changeServiceDiscoveryRoute(ServerWebExchange exchange, Route route, Map<String, String> newRouteConfig) {
String type = newRouteConfig.get("type");
String service = newRouteConfig.get("serviceName");
if (StringUtils.isNotBlank(service)) {
route.backendService = service;
}
String timeout = newRouteConfig.get("timeout");
if (StringUtils.isNotBlank(timeout)) {
route.timeout(Long.parseLong(timeout));
}
if (type.equals("http")) {
String registry = newRouteConfig.get("registry");
if (StringUtils.isNotBlank(registry)) {
route.registryCenter = registry;
}
String method = newRouteConfig.get("methodName");
if (StringUtils.isNotBlank(method)) {
route.method(HttpMethod.resolve(method));
}
String path = newRouteConfig.get("path");
if (StringUtils.isNotBlank(path)) {
route.backendPath = UrlTransformUtils.transform(route.path, path, WebUtils.getClientReqPath(exchange));
}
String qry = newRouteConfig.get("query");
if (StringUtils.isNotBlank(qry)) {
route.query = qry;
}
String retryCount = newRouteConfig.get("retryCount");
if (StringUtils.isNotBlank(retryCount)) {
route.retryCount(Integer.parseInt(retryCount));
}
String retryInterval = newRouteConfig.get("retryInterval");
if (StringUtils.isNotBlank(retryInterval)) {
route.retryInterval(Long.parseLong(retryInterval));
}
} else {
route.type = ApiConfig.Type.DUBBO;
String method = newRouteConfig.get("methodName");
if (StringUtils.isNotBlank(method)) {
route.rpcMethod(method);
}
String version = newRouteConfig.get("version");
if (StringUtils.isNotBlank(version)) {
route.rpcVersion(version);
}
String group = newRouteConfig.get("group");
if (StringUtils.isNotBlank(group)) {
route.rpcGroup(group);
}
String paramTypes = newRouteConfig.get("paramTypes");
if (StringUtils.isNotBlank(paramTypes)) {
route.rpcParamTypes(paramTypes);
}
}
byte[] key = SecureUtil.decode(secretKey);
SymmetricCrypto symmetric = new SymmetricCrypto(SymmetricAlgorithm.AES, key);
return symmetric.encryptBase64(data);
}*/
public byte[] encrypt(byte[] data, String secretKey) {
byte[] key = SecureUtil.decode(secretKey);
SymmetricCrypto symmetric = new SymmetricCrypto(SymmetricAlgorithm.AES, key);
return symmetric.encrypt(data);
}
/*public String decrypt(String data, String secretKey) {
if (StringUtils.isBlank(data)) {
return data;
private void changeReverseProxyRoute(ServerWebExchange exchange, Map<String, Object> pluginConfig, Route route, Map<String, String> newRouteConfig) {
List<String> httpHostPorts = (List<String>) pluginConfig.get("httpHostPorts");
if (httpHostPorts == null) {
String httpHostPortStr = newRouteConfig.get("serviceName");
if (StringUtils.isBlank(httpHostPortStr)) {
httpHostPorts = WebUtils.getApiConfig(exchange).httpHostPorts;
} else {
httpHostPorts = Arrays.asList(StringUtils.split(httpHostPortStr, Consts.S.COMMA));
}
pluginConfig.put("httpHostPorts", httpHostPorts);
newRouteConfig.remove("serviceName");
}
byte[] key = SecureUtil.decode(secretKey);
SymmetricCrypto symmetric = new SymmetricCrypto(SymmetricAlgorithm.AES, key);
return symmetric.decryptStr(data);
}*/
int counter = (int) pluginConfig.getOrDefault("counter", 0);
counter++;
if (counter < 0) {
counter = Math.abs(counter);
}
String hostPort = httpHostPorts.get(
counter % httpHostPorts.size()
);
route.nextHttpHostPort(hostPort);
pluginConfig.put("counter", counter);
public byte[] decrypt(byte[] data, String secretKey) {
byte[] key = SecureUtil.decode(secretKey);
SymmetricCrypto symmetric = new SymmetricCrypto(SymmetricAlgorithm.AES, key);
return symmetric.decrypt(data);
String method = newRouteConfig.get("methodName");
if (StringUtils.isNotBlank(method)) {
route.method(HttpMethod.resolve(method));
}
String path = newRouteConfig.get("path");
if (StringUtils.isNotBlank(path)) {
route.backendPath = UrlTransformUtils.transform(route.path, path, WebUtils.getClientReqPath(exchange));
}
String qry = newRouteConfig.get("query");
if (StringUtils.isNotBlank(qry)) {
route.query = qry;
}
String timeout = newRouteConfig.get("timeout");
if (StringUtils.isNotBlank(timeout)) {
route.timeout(Long.parseLong(timeout));
}
String retryCount = newRouteConfig.get("retryCount");
if (StringUtils.isNotBlank(retryCount)) {
route.retryCount(Integer.parseInt(retryCount));
}
String retryInterval = newRouteConfig.get("retryInterval");
if (StringUtils.isNotBlank(retryInterval)) {
route.retryInterval(Long.parseLong(retryInterval));
}
}
private void changeAggregateRoute(ServerWebExchange exchange, Route route, Map<String, String> newRouteConfig) {
String service = newRouteConfig.get("serviceName");
if (StringUtils.isNotBlank(service)) {
route.backendService = service;
WebUtils.setBackendService(exchange, route.backendService);
}
String path = newRouteConfig.get("path");
if (StringUtils.isNotBlank(path)) {
route.backendPath = UrlTransformUtils.transform(route.path, path, WebUtils.getClientReqPath(exchange));
WebUtils.setBackendPath(exchange, route.backendPath);
}
}
private Map<String, String> routeConfig2map(String config) {
Map<String, String> result = new HashMap<>();
String[] lines = StringUtils.split(config, Consts.S.LF);
for (String line : lines) {
/*int colonIdx = line.indexOf(Consts.S.COLON);
int start = 0, end = 0;
for (int i = 0; i < line.length(); i++) {
if (line.charAt(i) != Consts.S.SPACE) {
start = i;
break;
}
}
for (int i = line.length() - 1; i > -1; i--) {
if (line.charAt(i) != Consts.S.SPACE) {
end = i;
break;
}
}
String name = line.substring(start, colonIdx);
String value = line.substring(colonIdx + 1, end + 1);
result.put(name, value);*/
String[] nameValue = StringUtils.split(line, Consts.S.COLON);
result.put(nameValue[0].trim(), nameValue[1].trim());
}
return result;
}
}

View File

@@ -0,0 +1,60 @@
package we.plugin.grayrelease;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Mono;
import we.plugin.FizzPluginFilterChain;
import we.proxy.Route;
import we.util.Consts;
import we.util.WebUtils;
import java.util.HashMap;
import java.util.Map;
public class GrayReleasePluginTests {
@Test
public void simpleTest() {
WebTestClient client = WebTestClient.bindToWebHandler(
exchange -> {
ServerHttpResponse r = exchange.getResponse();
r.setStatusCode(HttpStatus.OK);
r.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
return r.writeWith(Mono.just(r.bufferFactory().wrap("this is web handler response".getBytes())));
}
)
.webFilter(
(exchange, chain) -> {
GrayReleasePlugin grayReleasePlugin = new GrayReleasePlugin();
Map<String, Object> config = new HashMap<>();
config.put("triggerCondition", "method == 'get'");
config.put("routeType", 2);
config.put("routeConfig", "type: http \n serviceName: bservice");
// exchange.getAttributes().put("pcsit@", Collections.emptyIterator());
exchange.getAttributes().put(WebUtils.ROUTE, new Route());
exchange.getAttributes().put(WebUtils.IGNORE_PLUGIN, Consts.S.EMPTY);
exchange.getAttributes().put(FizzPluginFilterChain.WEB_FILTER_CHAIN, chain);
exchange.getAttributes().put("oi@", "11.238.145.181");
return grayReleasePlugin.filter(exchange, config);
}
)
.build();
client.get()
.uri("/proxy/aservice/apath")
//.header("h1", "v1")
.exchange()
.expectBody(String.class).value(
v -> {
System.err.println("body:\n" + v);
}
)
;
}
}

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="info">
<properties>
<property name="APP_NAME">fizz-plugin</property>
</properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="warn">
<AppenderRef ref="Console"/>
</Root>
<Logger name="we" level="debug"/>
</Loggers>
</Configuration>