From 0c894d793a49f6557f3ca8e5a8ac56b672aaa0b0 Mon Sep 17 00:00:00 2001 From: hutiefang76 <137664623+hutiefang76@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:28:30 +0800 Subject: [PATCH] fix: decode general rule command data only once --- .../handler/ModifyRulesCommandHandler.java | 12 +- .../ModifyRulesCommandHandlerDecodeTest.java | 198 +++++++++++ .../netty/SetRulesCommandDecodeTest.java | 286 +++++++++++++++ .../http/SetRulesCommandDecodeTest.java | 325 ++++++++++++++++++ 4 files changed, 810 insertions(+), 11 deletions(-) create mode 100644 sentinel-transport/sentinel-transport-common/src/test/java/com/alibaba/csp/sentinel/command/handler/ModifyRulesCommandHandlerDecodeTest.java create mode 100644 sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/SetRulesCommandDecodeTest.java create mode 100644 sentinel-transport/sentinel-transport-simple-http/src/test/java/com/alibaba/csp/sentinel/transport/command/http/SetRulesCommandDecodeTest.java diff --git a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/ModifyRulesCommandHandler.java b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/ModifyRulesCommandHandler.java index b600844e14..21276d5a6c 100755 --- a/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/ModifyRulesCommandHandler.java +++ b/sentinel-transport/sentinel-transport-common/src/main/java/com/alibaba/csp/sentinel/command/handler/ModifyRulesCommandHandler.java @@ -15,7 +15,6 @@ */ package com.alibaba.csp.sentinel.command.handler; -import java.net.URLDecoder; import java.util.List; import com.alibaba.csp.sentinel.command.CommandHandler; @@ -24,7 +23,6 @@ import com.alibaba.csp.sentinel.command.annotation.CommandMapping; import com.alibaba.csp.sentinel.datasource.WritableDataSource; import com.alibaba.csp.sentinel.log.RecordLog; -import com.alibaba.csp.sentinel.util.StringUtil; import com.alibaba.csp.sentinel.util.VersionUtil; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager; @@ -57,16 +55,8 @@ public CommandResponse handle(CommandRequest request) { + "\" introduced in application is too old, you need fastjson-1.2.12 at least.")); } String type = request.getParam("type"); - // rule data in get parameter + // Command transports URL-decode request parameters, so "data" is already plain JSON here. String data = request.getParam("data"); - if (StringUtil.isNotEmpty(data)) { - try { - data = URLDecoder.decode(data, "utf-8"); - } catch (Exception e) { - RecordLog.info("Decode rule data error", e); - return CommandResponse.ofFailure(e, "decode rule data error"); - } - } RecordLog.info("Receiving rule change (type: {}): {}", type, data); diff --git a/sentinel-transport/sentinel-transport-common/src/test/java/com/alibaba/csp/sentinel/command/handler/ModifyRulesCommandHandlerDecodeTest.java b/sentinel-transport/sentinel-transport-common/src/test/java/com/alibaba/csp/sentinel/command/handler/ModifyRulesCommandHandlerDecodeTest.java new file mode 100644 index 0000000000..6e603609b9 --- /dev/null +++ b/sentinel-transport/sentinel-transport-common/src/test/java/com/alibaba/csp/sentinel/command/handler/ModifyRulesCommandHandlerDecodeTest.java @@ -0,0 +1,198 @@ +/* + * 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.command.handler; + +import java.util.List; + +import com.alibaba.csp.sentinel.command.CommandRequest; +import com.alibaba.csp.sentinel.command.CommandResponse; +import com.alibaba.csp.sentinel.datasource.WritableDataSource; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Verifies that {@link ModifyRulesCommandHandler} treats the {@code data} parameter as plain + * JSON: the transports URL-decode request parameters before they reach a {@link CommandRequest}, + * so the handler must not decode the value a second time. + */ +public class ModifyRulesCommandHandlerDecodeTest { + + private final ModifyRulesCommandHandler handler = new ModifyRulesCommandHandler(); + + private List previousFlowRules; + private List previousAuthorityRules; + private WritableDataSource> previousFlowDataSource; + private WritableDataSource> previousAuthorityDataSource; + + private RecordingFlowDataSource flowDataSource; + private RecordingAuthorityDataSource authorityDataSource; + + @Before + public void setUp() { + previousFlowRules = FlowRuleManager.getRules(); + previousAuthorityRules = AuthorityRuleManager.getRules(); + previousFlowDataSource = WritableDataSourceRegistry.getFlowDataSource(); + previousAuthorityDataSource = WritableDataSourceRegistry.getAuthorityDataSource(); + + flowDataSource = new RecordingFlowDataSource(); + authorityDataSource = new RecordingAuthorityDataSource(); + WritableDataSourceRegistry.registerFlowDataSource(flowDataSource); + WritableDataSourceRegistry.registerAuthorityDataSource(authorityDataSource); + } + + @After + public void tearDown() { + try { + FlowRuleManager.loadRules(previousFlowRules); + AuthorityRuleManager.loadRules(previousAuthorityRules); + } finally { + WritableDataSourceRegistry.registerFlowDataSource(previousFlowDataSource); + WritableDataSourceRegistry.registerAuthorityDataSource(previousAuthorityDataSource); + } + } + + @Test + public void testFlowOrdinaryAsciiResourceIsKept() { + assertFlowRuleApplied("decodeAsciiResource"); + } + + @Test + public void testFlowUnicodeResourceIsKept() { + assertFlowRuleApplied("限流资源api"); + } + + @Test + public void testFlowRealSpaceResourceIsKept() { + assertFlowRuleApplied("decode space resource"); + } + + @Test + public void testFlowGenuinePlusResourceIsKept() { + assertFlowRuleApplied("decode+plus"); + } + + @Test + public void testFlowLiteralPercent2BResourceIsKept() { + assertFlowRuleApplied("decode%2Bpercent"); + } + + @Test + public void testFlowLonePercentResourceIsAccepted() { + assertFlowRuleApplied("decode50%off"); + } + + @Test + public void testAuthorityOrdinaryAsciiResourceIsKept() { + assertAuthorityRuleApplied("authorityAsciiResource"); + } + + @Test + public void testAuthorityGenuinePlusResourceIsKept() { + assertAuthorityRuleApplied("authority+plus"); + } + + @Test + public void testAuthorityLiteralPercent2BResourceIsKept() { + assertAuthorityRuleApplied("authority%2Bpercent"); + } + + /** + * Sends the {@code data} parameter exactly as the transports hand it over + * (already URL-decoded) and checks the rule manager and the writable data source. + */ + private void assertFlowRuleApplied(String resource) { + String data = "[{\"resource\":\"" + resource + "\",\"limitApp\":\"default\",\"grade\":1," + + "\"count\":10.0,\"strategy\":0,\"controlBehavior\":0}]"; + + CommandResponse response = handler.handle(setRulesRequest("flow", data)); + assertEquals("success", response.getResult()); + + List rules = FlowRuleManager.getRules(); + assertEquals(1, rules.size()); + assertEquals(resource, rules.get(0).getResource()); + + List written = flowDataSource.getLastValue(); + assertEquals(1, written.size()); + assertEquals(resource, written.get(0).getResource()); + } + + private void assertAuthorityRuleApplied(String resource) { + String data = "[{\"resource\":\"" + resource + "\",\"limitApp\":\"appA\",\"strategy\":0}]"; + + CommandResponse response = handler.handle(setRulesRequest("authority", data)); + assertEquals("success", response.getResult()); + + List rules = AuthorityRuleManager.getRules(); + assertEquals(1, rules.size()); + assertEquals(resource, rules.get(0).getResource()); + + List written = authorityDataSource.getLastValue(); + assertEquals(1, written.size()); + assertEquals(resource, written.get(0).getResource()); + } + + private CommandRequest setRulesRequest(String type, String decodedData) { + CommandRequest request = new CommandRequest(); + request.addParam("type", type); + request.addParam("data", decodedData); + return request; + } + + private static class RecordingFlowDataSource implements WritableDataSource> { + + private List lastValue; + + @Override + public void write(List value) { + lastValue = value; + } + + @Override + public void close() { + } + + List getLastValue() { + return lastValue; + } + } + + private static class RecordingAuthorityDataSource implements WritableDataSource> { + + private List lastValue; + + @Override + public void write(List value) { + lastValue = value; + } + + @Override + public void close() { + } + + List getLastValue() { + return lastValue; + } + } +} diff --git a/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/SetRulesCommandDecodeTest.java b/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/SetRulesCommandDecodeTest.java new file mode 100644 index 0000000000..48803d3972 --- /dev/null +++ b/sentinel-transport/sentinel-transport-netty-http/src/test/java/com/alibaba/csp/sentinel/transport/command/netty/SetRulesCommandDecodeTest.java @@ -0,0 +1,286 @@ +/* + * 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.netty; + +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.List; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.handler.ModifyRulesCommandHandler; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.datasource.WritableDataSource; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpRequestDecoder; +import io.netty.handler.codec.http.HttpResponseEncoder; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Verifies that the {@code setRules} command keeps the {@code data} parameter intact when a + * client URL-encodes it exactly once through the netty-http pipeline. + */ +public class SetRulesCommandDecodeTest { + + private static final String CRLF = "\r\n"; + private static final Charset SENTINEL_CHARSET = Charset.forName(SentinelConfig.charset()); + + private static CommandHandler previousSetRulesHandler; + private static boolean setRulesHandlerExisted; + + private EmbeddedChannel embeddedChannel; + + private List previousFlowRules; + private List previousAuthorityRules; + private WritableDataSource> previousFlowDataSource; + private WritableDataSource> previousAuthorityDataSource; + + private RecordingFlowDataSource flowDataSource; + private RecordingAuthorityDataSource authorityDataSource; + + @BeforeClass + public static void beforeClass() { + // Only the "setRules" entry is replaced; other registered commands stay untouched. + setRulesHandlerExisted = HttpServer.handlerMap.containsKey("setRules"); + previousSetRulesHandler = HttpServer.handlerMap.get("setRules"); + HttpServer.handlerMap.put("setRules", new ModifyRulesCommandHandler()); + } + + @AfterClass + public static void afterClass() { + if (setRulesHandlerExisted) { + HttpServer.handlerMap.put("setRules", previousSetRulesHandler); + } else { + HttpServer.handlerMap.remove("setRules"); + } + } + + @Before + public void setUp() { + previousFlowRules = FlowRuleManager.getRules(); + previousAuthorityRules = AuthorityRuleManager.getRules(); + previousFlowDataSource = WritableDataSourceRegistry.getFlowDataSource(); + previousAuthorityDataSource = WritableDataSourceRegistry.getAuthorityDataSource(); + + // The same handlers in order as the pipeline in HttpServerInitializer. + embeddedChannel = new EmbeddedChannel(new HttpRequestDecoder(), new HttpObjectAggregator(1024 * 1024), + new HttpResponseEncoder(), new HttpServerHandler()); + + flowDataSource = new RecordingFlowDataSource(); + authorityDataSource = new RecordingAuthorityDataSource(); + WritableDataSourceRegistry.registerFlowDataSource(flowDataSource); + WritableDataSourceRegistry.registerAuthorityDataSource(authorityDataSource); + } + + @After + public void tearDown() { + try { + embeddedChannel.finishAndReleaseAll(); + } finally { + try { + FlowRuleManager.loadRules(previousFlowRules); + AuthorityRuleManager.loadRules(previousAuthorityRules); + } finally { + WritableDataSourceRegistry.registerFlowDataSource(previousFlowDataSource); + WritableDataSourceRegistry.registerAuthorityDataSource(previousAuthorityDataSource); + } + } + } + + @Test + public void testFlowPostOrdinaryAsciiResource() throws Exception { + assertApplied(process(postSetRules("flow", flowJson("decodeAsciiResource"))), "flow", "decodeAsciiResource"); + } + + @Test + public void testFlowPostRealSpaceResource() throws Exception { + assertApplied(process(postSetRules("flow", flowJson("decode space resource"))), "flow", + "decode space resource"); + } + + @Test + public void testFlowPostUnicodeResource() throws Exception { + assertApplied(process(postSetRules("flow", flowJson("限流资源api"))), "flow", "限流资源api"); + } + + @Test + public void testFlowGetOrdinaryAsciiResource() throws Exception { + assertApplied(process(getSetRules("flow", flowJson("decodeAsciiResource"))), "flow", "decodeAsciiResource"); + } + + @Test + public void testFlowPostGenuinePlusResource() throws Exception { + assertApplied(process(postSetRules("flow", flowJson("decode+plus"))), "flow", "decode+plus"); + } + + @Test + public void testFlowPostLiteralPercent2BResource() throws Exception { + assertApplied(process(postSetRules("flow", flowJson("decode%2Bpercent"))), "flow", "decode%2Bpercent"); + } + + @Test + public void testFlowPostLonePercentResource() throws Exception { + assertApplied(process(postSetRules("flow", flowJson("decode50%off"))), "flow", "decode50%off"); + } + + @Test + public void testFlowGetGenuinePlusResource() throws Exception { + assertApplied(process(getSetRules("flow", flowJson("decode+plus"))), "flow", "decode+plus"); + } + + @Test + public void testAuthorityPostOrdinaryAsciiResource() throws Exception { + assertApplied(process(postSetRules("authority", authorityJson("authorityAsciiResource"))), "authority", + "authorityAsciiResource"); + } + + @Test + public void testAuthorityPostGenuinePlusResource() throws Exception { + assertApplied(process(postSetRules("authority", authorityJson("authority+plus"))), "authority", + "authority+plus"); + } + + @Test + public void testAuthorityPostLiteralPercent2BResource() throws Exception { + assertApplied(process(postSetRules("authority", authorityJson("authority%2Bpercent"))), "authority", + "authority%2Bpercent"); + } + + @Test + public void testAuthorityGetGenuinePlusResource() throws Exception { + assertApplied(process(getSetRules("authority", authorityJson("authority+plus"))), "authority", + "authority+plus"); + } + + private String flowJson(String resource) { + return "[{\"resource\":\"" + resource + "\",\"limitApp\":\"default\",\"grade\":1," + + "\"count\":10.0,\"strategy\":0,\"controlBehavior\":0}]"; + } + + private String authorityJson(String resource) { + return "[{\"resource\":\"" + resource + "\",\"limitApp\":\"appA\",\"strategy\":0}]"; + } + + /** + * Builds a real HTTP request whose {@code data} parameter is URL-encoded exactly once, + * as the dashboard does ({@code UrlEncodedFormEntity}). + */ + private String postSetRules(String type, String data) throws Exception { + String body = "type=" + URLEncoder.encode(type, "UTF-8") + + "&data=" + URLEncoder.encode(data, "UTF-8"); + return "POST /setRules HTTP/1.1" + CRLF + + "Host: localhost:8719" + CRLF + + "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" + CRLF + + "Content-Length: " + body.getBytes(SENTINEL_CHARSET).length + CRLF + + CRLF + + body; + } + + private String getSetRules(String type, String data) throws Exception { + return "GET /setRules?type=" + URLEncoder.encode(type, "UTF-8") + + "&data=" + URLEncoder.encode(data, "UTF-8") + " HTTP/1.1" + CRLF + + "Host: localhost:8719" + CRLF + + CRLF; + } + + /** + * Feeds the raw request through the real netty pipeline and returns the response body. + */ + private String process(String httpRequestStr) { + embeddedChannel.writeInbound(Unpooled.wrappedBuffer(httpRequestStr.getBytes(SENTINEL_CHARSET))); + + StringBuilder sb = new StringBuilder(); + ByteBuf byteBuf; + while ((byteBuf = embeddedChannel.readOutbound()) != null) { + sb.append(byteBuf.toString(SENTINEL_CHARSET)); + byteBuf.release(); + } + + String responseStr = sb.toString(); + int bodyStart = responseStr.indexOf(CRLF + CRLF); + return bodyStart < 0 ? responseStr : responseStr.substring(bodyStart + 4); + } + + private void assertApplied(String responseBody, String type, String resource) { + assertEquals("success", responseBody.trim()); + if ("flow".equals(type)) { + List rules = FlowRuleManager.getRules(); + assertEquals(1, rules.size()); + assertEquals(resource, rules.get(0).getResource()); + List written = flowDataSource.getLastValue(); + assertEquals(1, written.size()); + assertEquals(resource, written.get(0).getResource()); + } else { + List rules = AuthorityRuleManager.getRules(); + assertEquals(1, rules.size()); + assertEquals(resource, rules.get(0).getResource()); + List written = authorityDataSource.getLastValue(); + assertEquals(1, written.size()); + assertEquals(resource, written.get(0).getResource()); + } + } + + private static class RecordingFlowDataSource implements WritableDataSource> { + + private List lastValue; + + @Override + public void write(List value) { + lastValue = value; + } + + @Override + public void close() { + } + + List getLastValue() { + return lastValue; + } + } + + private static class RecordingAuthorityDataSource implements WritableDataSource> { + + private List lastValue; + + @Override + public void write(List value) { + lastValue = value; + } + + @Override + public void close() { + } + + List getLastValue() { + return lastValue; + } + } +} diff --git a/sentinel-transport/sentinel-transport-simple-http/src/test/java/com/alibaba/csp/sentinel/transport/command/http/SetRulesCommandDecodeTest.java b/sentinel-transport/sentinel-transport-simple-http/src/test/java/com/alibaba/csp/sentinel/transport/command/http/SetRulesCommandDecodeTest.java new file mode 100644 index 0000000000..0ca846a2b9 --- /dev/null +++ b/sentinel-transport/sentinel-transport-simple-http/src/test/java/com/alibaba/csp/sentinel/transport/command/http/SetRulesCommandDecodeTest.java @@ -0,0 +1,325 @@ +/* + * 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 java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; + +import com.alibaba.csp.sentinel.command.CommandHandler; +import com.alibaba.csp.sentinel.command.handler.ModifyRulesCommandHandler; +import com.alibaba.csp.sentinel.config.SentinelConfig; +import com.alibaba.csp.sentinel.datasource.WritableDataSource; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; +import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.transport.command.SimpleHttpCommandCenter; +import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Verifies that the {@code setRules} command keeps the {@code data} parameter intact when a + * client URL-encodes it exactly once over the simple-http transport. + */ +public class SetRulesCommandDecodeTest { + + private static final String CRLF = "\r\n"; + private static final Charset SENTINEL_CHARSET = Charset.forName(SentinelConfig.charset()); + private static final int SOCKET_TIMEOUT_MS = 5000; + + private static Map handlerMap; + private static CommandHandler previousSetRulesHandler; + private static boolean setRulesHandlerExisted; + + private List previousFlowRules; + private List previousAuthorityRules; + private WritableDataSource> previousFlowDataSource; + private WritableDataSource> previousAuthorityDataSource; + + private RecordingFlowDataSource flowDataSource; + private RecordingAuthorityDataSource authorityDataSource; + + @SuppressWarnings("unchecked") + @BeforeClass + public static void beforeClass() throws Exception { + // SimpleHttpCommandCenter has no unregister API, so only the "setRules" entry is + // replaced through its private map and put back afterwards. + Field handlerMapField = SimpleHttpCommandCenter.class.getDeclaredField("handlerMap"); + handlerMapField.setAccessible(true); + handlerMap = (Map) handlerMapField.get(null); + setRulesHandlerExisted = handlerMap.containsKey("setRules"); + previousSetRulesHandler = handlerMap.get("setRules"); + handlerMap.put("setRules", new ModifyRulesCommandHandler()); + } + + @AfterClass + public static void afterClass() { + if (setRulesHandlerExisted) { + handlerMap.put("setRules", previousSetRulesHandler); + } else { + handlerMap.remove("setRules"); + } + } + + @Before + public void setUp() { + previousFlowRules = FlowRuleManager.getRules(); + previousAuthorityRules = AuthorityRuleManager.getRules(); + previousFlowDataSource = WritableDataSourceRegistry.getFlowDataSource(); + previousAuthorityDataSource = WritableDataSourceRegistry.getAuthorityDataSource(); + + flowDataSource = new RecordingFlowDataSource(); + authorityDataSource = new RecordingAuthorityDataSource(); + WritableDataSourceRegistry.registerFlowDataSource(flowDataSource); + WritableDataSourceRegistry.registerAuthorityDataSource(authorityDataSource); + } + + @After + public void tearDown() { + try { + FlowRuleManager.loadRules(previousFlowRules); + AuthorityRuleManager.loadRules(previousAuthorityRules); + } finally { + WritableDataSourceRegistry.registerFlowDataSource(previousFlowDataSource); + WritableDataSourceRegistry.registerAuthorityDataSource(previousAuthorityDataSource); + } + } + + @Test + public void testFlowPostOrdinaryAsciiResource() throws Exception { + assertApplied(send(postSetRules("flow", flowJson("decodeAsciiResource"))), "flow", "decodeAsciiResource"); + } + + @Test + public void testFlowPostRealSpaceResource() throws Exception { + assertApplied(send(postSetRules("flow", flowJson("decode space resource"))), "flow", + "decode space resource"); + } + + @Test + public void testFlowPostUnicodeResource() throws Exception { + assertApplied(send(postSetRules("flow", flowJson("限流资源api"))), "flow", "限流资源api"); + } + + @Test + public void testFlowGetOrdinaryAsciiResource() throws Exception { + assertApplied(send(getSetRules("flow", flowJson("decodeAsciiResource"))), "flow", "decodeAsciiResource"); + } + + @Test + public void testFlowPostGenuinePlusResource() throws Exception { + assertApplied(send(postSetRules("flow", flowJson("decode+plus"))), "flow", "decode+plus"); + } + + @Test + public void testFlowPostLiteralPercent2BResource() throws Exception { + assertApplied(send(postSetRules("flow", flowJson("decode%2Bpercent"))), "flow", "decode%2Bpercent"); + } + + @Test + public void testFlowPostLonePercentResource() throws Exception { + assertApplied(send(postSetRules("flow", flowJson("decode50%off"))), "flow", "decode50%off"); + } + + @Test + public void testFlowGetGenuinePlusResource() throws Exception { + assertApplied(send(getSetRules("flow", flowJson("decode+plus"))), "flow", "decode+plus"); + } + + @Test + public void testAuthorityPostOrdinaryAsciiResource() throws Exception { + assertApplied(send(postSetRules("authority", authorityJson("authorityAsciiResource"))), "authority", + "authorityAsciiResource"); + } + + @Test + public void testAuthorityPostGenuinePlusResource() throws Exception { + assertApplied(send(postSetRules("authority", authorityJson("authority+plus"))), "authority", + "authority+plus"); + } + + @Test + public void testAuthorityPostLiteralPercent2BResource() throws Exception { + assertApplied(send(postSetRules("authority", authorityJson("authority%2Bpercent"))), "authority", + "authority%2Bpercent"); + } + + @Test + public void testAuthorityGetGenuinePlusResource() throws Exception { + assertApplied(send(getSetRules("authority", authorityJson("authority+plus"))), "authority", + "authority+plus"); + } + + private String flowJson(String resource) { + return "[{\"resource\":\"" + resource + "\",\"limitApp\":\"default\",\"grade\":1," + + "\"count\":10.0,\"strategy\":0,\"controlBehavior\":0}]"; + } + + private String authorityJson(String resource) { + return "[{\"resource\":\"" + resource + "\",\"limitApp\":\"appA\",\"strategy\":0}]"; + } + + /** + * Builds a real HTTP request whose {@code data} parameter is URL-encoded exactly once, + * as the dashboard does ({@code UrlEncodedFormEntity}). + */ + private String postSetRules(String type, String data) throws Exception { + String body = "type=" + URLEncoder.encode(type, "UTF-8") + + "&data=" + URLEncoder.encode(data, "UTF-8"); + return "POST /setRules HTTP/1.1" + CRLF + + "Host: localhost:8719" + CRLF + + "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" + CRLF + + "Content-Length: " + body.getBytes(SENTINEL_CHARSET).length + CRLF + + CRLF + + body; + } + + private String getSetRules(String type, String data) throws Exception { + return "GET /setRules?type=" + URLEncoder.encode(type, "UTF-8") + + "&data=" + URLEncoder.encode(data, "UTF-8") + " HTTP/1.1" + CRLF + + "Host: localhost:8719" + CRLF + + CRLF; + } + + /** + * Feeds the raw request through a real loopback socket into {@link HttpEventTask} + * and returns the response body. Only loopback and a transient port are used. + */ + private String send(String httpRequestStr) throws IOException { + InetAddress loopback = InetAddress.getLoopbackAddress(); + ServerSocket serverSocket = new ServerSocket(0, 1, loopback); + Socket clientSocket = null; + Socket taskSocket = null; + try { + clientSocket = new Socket(loopback, serverSocket.getLocalPort()); + clientSocket.setSoTimeout(SOCKET_TIMEOUT_MS); + taskSocket = serverSocket.accept(); + taskSocket.setSoTimeout(SOCKET_TIMEOUT_MS); + serverSocket.close(); + + OutputStream out = clientSocket.getOutputStream(); + out.write(httpRequestStr.getBytes(SENTINEL_CHARSET)); + out.flush(); + clientSocket.shutdownOutput(); + + // Real transport parsing + command dispatch (+ handler), then the socket is closed. + new HttpEventTask(taskSocket).run(); + + return responseBody(readAll(clientSocket.getInputStream())); + } finally { + closeQuietly(taskSocket); + closeQuietly(serverSocket); + closeQuietly(clientSocket); + } + } + + private String responseBody(String responseStr) { + int bodyStart = responseStr.indexOf(CRLF + CRLF); + return bodyStart < 0 ? responseStr : responseStr.substring(bodyStart + 4); + } + + private String readAll(InputStream in) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) >= 0) { + bos.write(buf, 0, len); + } + return new String(bos.toByteArray(), SENTINEL_CHARSET); + } + + private void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException ignored) { + } + } + } + + private void assertApplied(String responseBody, String type, String resource) { + assertEquals("success", responseBody.trim()); + if ("flow".equals(type)) { + List rules = FlowRuleManager.getRules(); + assertEquals(1, rules.size()); + assertEquals(resource, rules.get(0).getResource()); + List written = flowDataSource.getLastValue(); + assertEquals(1, written.size()); + assertEquals(resource, written.get(0).getResource()); + } else { + List rules = AuthorityRuleManager.getRules(); + assertEquals(1, rules.size()); + assertEquals(resource, rules.get(0).getResource()); + List written = authorityDataSource.getLastValue(); + assertEquals(1, written.size()); + assertEquals(resource, written.get(0).getResource()); + } + } + + private static class RecordingFlowDataSource implements WritableDataSource> { + + private List lastValue; + + @Override + public void write(List value) { + lastValue = value; + } + + @Override + public void close() { + } + + List getLastValue() { + return lastValue; + } + } + + private static class RecordingAuthorityDataSource implements WritableDataSource> { + + private List lastValue; + + @Override + public void write(List value) { + lastValue = value; + } + + @Override + public void close() { + } + + List getLastValue() { + return lastValue; + } + } +}