Compare commits
10 Commits
Author | SHA1 | Date |
---|---|---|
spencergibb | 31ed804033 | 3 years ago |
buildmaster | bf251614e2 | 3 years ago |
spencergibb | 34fa33e20a | 3 years ago |
spencergibb | e959d8787d | 3 years ago |
spencergibb | 9bb21eafc9 | 3 years ago |
spencergibb | 607392a658 | 3 years ago |
giger85 | acc88afe0d | 3 years ago |
buildmaster | 2d51a9ea22 | 3 years ago |
buildmaster | 2add690740 | 3 years ago |
buildmaster | f8438f7b7f | 3 years ago |
19 changed files with 321 additions and 34 deletions
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
/* |
||||
* Copyright 2013-2021 the original author or authors. |
||||
* |
||||
* 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 |
||||
* |
||||
* https://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 org.springframework.cloud.gateway.filter.headers; |
||||
|
||||
import org.springframework.core.Ordered; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* See https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 for details.
|
||||
*/ |
||||
public class TransferEncodingNormalizationHeadersFilter |
||||
implements HttpHeadersFilter, Ordered { |
||||
|
||||
@Override |
||||
public int getOrder() { |
||||
return 1000; |
||||
} |
||||
|
||||
@Override |
||||
public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) { |
||||
String transferEncoding = input.getFirst(HttpHeaders.TRANSFER_ENCODING); |
||||
if (transferEncoding != null |
||||
&& "chunked".equalsIgnoreCase(transferEncoding.trim()) |
||||
&& input.containsKey(HttpHeaders.CONTENT_LENGTH)) { |
||||
|
||||
HttpHeaders filtered = new HttpHeaders(); |
||||
// avoids read only if input is read only
|
||||
filtered.addAll(input); |
||||
filtered.remove(HttpHeaders.CONTENT_LENGTH); |
||||
return filtered; |
||||
} |
||||
|
||||
return input; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,155 @@
@@ -0,0 +1,155 @@
|
||||
/* |
||||
* Copyright 2013-2020 the original author or authors. |
||||
* |
||||
* 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 |
||||
* |
||||
* https://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 org.springframework.cloud.gateway.filter.headers; |
||||
|
||||
import java.io.BufferedReader; |
||||
import java.io.IOException; |
||||
import java.io.InputStreamReader; |
||||
import java.io.OutputStream; |
||||
import java.net.Socket; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty; |
||||
import com.netflix.loadbalancer.Server; |
||||
import com.netflix.loadbalancer.ServerList; |
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.boot.SpringBootConfiguration; |
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
import org.springframework.boot.web.server.LocalServerPort; |
||||
import org.springframework.cloud.gateway.route.RouteLocator; |
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; |
||||
import org.springframework.cloud.gateway.test.PermitAllSecurityConfiguration; |
||||
import org.springframework.cloud.netflix.ribbon.RibbonClient; |
||||
import org.springframework.cloud.netflix.ribbon.StaticServerList; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Import; |
||||
import org.springframework.context.annotation.Primary; |
||||
import org.springframework.core.log.LogMessage; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.test.context.ActiveProfiles; |
||||
import org.springframework.util.StreamUtils; |
||||
import org.springframework.web.bind.annotation.PostMapping; |
||||
import org.springframework.web.bind.annotation.RequestBody; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; |
||||
|
||||
@SpringBootTest(properties = {}, webEnvironment = RANDOM_PORT) |
||||
@ActiveProfiles("transferencoding") |
||||
public class TransferEncodingNormalizationHeadersFilterIntegrationTests { |
||||
|
||||
private static final Log log = LogFactory |
||||
.getLog(TransferEncodingNormalizationHeadersFilterIntegrationTests.class); |
||||
|
||||
@LocalServerPort |
||||
private int port; |
||||
|
||||
@Test |
||||
void legitRequestShouldNotFail() throws Exception { |
||||
final ClassLoader classLoader = this.getClass().getClassLoader(); |
||||
|
||||
// Issue a crafted request with smuggling attempt
|
||||
assert200With("Should Fail", StreamUtils.copyToByteArray(classLoader |
||||
.getResourceAsStream("transfer-encoding/invalid-request.bin"))); |
||||
|
||||
// Issue a legit request, which should not fail
|
||||
assert200With("Should Not Fail", StreamUtils.copyToByteArray( |
||||
classLoader.getResourceAsStream("transfer-encoding/valid-request.bin"))); |
||||
} |
||||
|
||||
private void assert200With(String name, byte[] payload) throws Exception { |
||||
final String response = execute("localhost", port, payload); |
||||
log.info(LogMessage.format("Request to localhost:%d %s\n%s", port, name, |
||||
new String(payload))); |
||||
assertThat(response).isNotNull(); |
||||
log.info(LogMessage.format("Response %s\n%s", name, response)); |
||||
assertThat(response).matches("HTTP/1.\\d 200 OK"); |
||||
} |
||||
|
||||
private String execute(String target, int port, byte[] payload) throws IOException { |
||||
final Socket socket = new Socket(target, port); |
||||
|
||||
final OutputStream out = socket.getOutputStream(); |
||||
final BufferedReader in = new BufferedReader( |
||||
new InputStreamReader(socket.getInputStream())); |
||||
|
||||
out.write(payload); |
||||
|
||||
final String headResponse = in.readLine(); |
||||
|
||||
out.close(); |
||||
in.close(); |
||||
|
||||
return headResponse; |
||||
} |
||||
|
||||
@EnableAutoConfiguration |
||||
@SpringBootConfiguration |
||||
@Import(PermitAllSecurityConfiguration.class) |
||||
@RibbonClient(name = "xferenc", configuration = TestLoadBalancerConfig.class) |
||||
@RestController |
||||
public static class TestConfig { |
||||
|
||||
@PostMapping(value = "/echo", produces = { MediaType.APPLICATION_JSON_VALUE }) |
||||
public Message message(@RequestBody Message message) throws IOException { |
||||
return message; |
||||
} |
||||
|
||||
@Bean |
||||
public RouteLocator routeLocator(RouteLocatorBuilder builder) { |
||||
return builder.routes().route("echo", r -> r.path("/route/echo") |
||||
.filters(f -> f.stripPrefix(1)).uri("lb://xferenc")).build(); |
||||
} |
||||
|
||||
} |
||||
|
||||
public static class Message { |
||||
|
||||
private String message; |
||||
|
||||
public Message(@JsonProperty("message") String message) { |
||||
this.message = message; |
||||
} |
||||
|
||||
public String getMessage() { |
||||
return message; |
||||
} |
||||
|
||||
public void setMessage(String message) { |
||||
this.message = message; |
||||
} |
||||
|
||||
} |
||||
|
||||
public static class TestLoadBalancerConfig { |
||||
|
||||
@LocalServerPort |
||||
protected int port = 0; |
||||
|
||||
@Bean |
||||
@Primary |
||||
public ServerList<Server> ribbonServerList() { |
||||
return new StaticServerList<>(new Server("localhost", port)); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
/* |
||||
* Copyright 2013-2020 the original author or authors. |
||||
* |
||||
* 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 |
||||
* |
||||
* https://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 org.springframework.cloud.gateway.filter.headers; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest; |
||||
import org.springframework.mock.web.server.MockServerWebExchange; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* @author Spencer Gibb |
||||
*/ |
||||
public class TransferEncodingNormalizationHeadersFilterTests { |
||||
|
||||
@Test |
||||
public void noTransferEncodingWithContentLength() { |
||||
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest |
||||
.post("http://localhost/post").header(HttpHeaders.CONTENT_LENGTH, "6"); |
||||
|
||||
HttpHeaders headers = testFilter(MockServerWebExchange.from(builder)); |
||||
assertThat(headers).containsKey(HttpHeaders.CONTENT_LENGTH) |
||||
.doesNotContainKey(HttpHeaders.TRANSFER_ENCODING); |
||||
} |
||||
|
||||
@Test |
||||
public void transferEncodingWithContentLength() { |
||||
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest |
||||
.post("http://localhost/post").header(HttpHeaders.CONTENT_LENGTH, "6") |
||||
.header(HttpHeaders.TRANSFER_ENCODING, "chunked"); |
||||
|
||||
HttpHeaders headers = testFilter(MockServerWebExchange.from(builder)); |
||||
assertThat(headers).doesNotContainKey(HttpHeaders.CONTENT_LENGTH) |
||||
.containsKey(HttpHeaders.TRANSFER_ENCODING); |
||||
} |
||||
|
||||
@Test |
||||
public void transferEncodingCaseInsensitiveWithContentLength() { |
||||
MockServerHttpRequest.BaseBuilder<?> builder = MockServerHttpRequest |
||||
.post("http://localhost/post").header(HttpHeaders.CONTENT_LENGTH, "6") |
||||
.header(HttpHeaders.TRANSFER_ENCODING, "Chunked "); |
||||
|
||||
HttpHeaders headers = testFilter(MockServerWebExchange.from(builder)); |
||||
assertThat(headers).doesNotContainKey(HttpHeaders.CONTENT_LENGTH) |
||||
.containsKey(HttpHeaders.TRANSFER_ENCODING); |
||||
} |
||||
|
||||
private HttpHeaders testFilter(MockServerWebExchange exchange) { |
||||
TransferEncodingNormalizationHeadersFilter filter = new TransferEncodingNormalizationHeadersFilter(); |
||||
return filter.filter(exchange.getRequest().getHeaders(), exchange); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
POST /route/echo HTTP/1.0 |
||||
Host: localhost:8080 |
||||
Content-Length: 19 |
||||
Transfer-encoding: Chunked |
||||
Content-Type: application/json |
||||
Connection: close |
||||
|
||||
22 |
||||
{"message":"3"} |
||||
|
||||
GET /nonexistantpath123 HTTP/1.0 |
||||
0 |
||||
|
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
POST /route/echo HTTP/1.1 |
||||
Host: localhost:8080 |
||||
Content-Type: application/json |
||||
Content-Length: 15 |
||||
Connection: close |
||||
|
||||
{"message":"3"} |
Loading…
Reference in new issue