Spencer Gibb
6 years ago
10 changed files with 405 additions and 3 deletions
@ -0,0 +1,154 @@
@@ -0,0 +1,154 @@
|
||||
/* |
||||
* Copyright 2013-2019 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 |
||||
* |
||||
* 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 org.springframework.cloud.gateway.filter.factory; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.List; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter; |
||||
import org.springframework.http.HttpHeaders; |
||||
|
||||
/* |
||||
Use case: Both your legacy backend and your API gateway add CORS header values. So, your consumer ends up with |
||||
Access-Control-Allow-Credentials: true, true |
||||
Access-Control-Allow-Origin: https://musk.mars, https://musk.mars
|
||||
(The one from the gateway will be the first of the two.) To fix, add |
||||
DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin |
||||
|
||||
Configuration parameters: |
||||
- name |
||||
String representing response header names, space separated. Required. |
||||
- strategy |
||||
RETAIN_FIRST - Default. Retain the first value only. |
||||
RETAIN_LAST - Retain the last value only. |
||||
RETAIN_UNIQUE - Retain all unique values in the order of their first encounter. |
||||
|
||||
Example 1 |
||||
default-filters: |
||||
- DedupeResponseHeader=Access-Control-Allow-Credentials |
||||
|
||||
Response header Access-Control-Allow-Credentials: true, false |
||||
Modified response header Access-Control-Allow-Credentials: true |
||||
|
||||
Example 2 |
||||
default-filters: |
||||
- DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_LAST |
||||
|
||||
Response header Access-Control-Allow-Credentials: true, false |
||||
Modified response header Access-Control-Allow-Credentials: false |
||||
|
||||
Example 3 |
||||
default-filters: |
||||
- DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_UNIQUE |
||||
|
||||
Response header Access-Control-Allow-Credentials: true, true |
||||
Modified response header Access-Control-Allow-Credentials: true |
||||
*/ |
||||
|
||||
/** |
||||
* @author Vitaliy Pavlyuk |
||||
*/ |
||||
public class DedupeResponseHeaderGatewayFilterFactory extends |
||||
AbstractGatewayFilterFactory<DedupeResponseHeaderGatewayFilterFactory.Config> { |
||||
|
||||
private static final String STRATEGY_KEY = "strategy"; |
||||
|
||||
public DedupeResponseHeaderGatewayFilterFactory() { |
||||
super(Config.class); |
||||
} |
||||
|
||||
@Override |
||||
public List<String> shortcutFieldOrder() { |
||||
return Arrays.asList(NAME_KEY, STRATEGY_KEY); |
||||
} |
||||
|
||||
@Override |
||||
public GatewayFilter apply(Config config) { |
||||
return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> { |
||||
dedupe(exchange.getResponse().getHeaders(), config); |
||||
})); |
||||
} |
||||
|
||||
public enum Strategy { |
||||
|
||||
/** |
||||
* Default: Retain the first value only. |
||||
*/ |
||||
RETAIN_FIRST, |
||||
|
||||
/** |
||||
* Retain the last value only. |
||||
*/ |
||||
RETAIN_LAST, |
||||
|
||||
/** |
||||
* Retain all unique values in the order of their first encounter. |
||||
*/ |
||||
RETAIN_UNIQUE |
||||
|
||||
} |
||||
|
||||
void dedupe(HttpHeaders headers, Config config) { |
||||
String names = config.getName(); |
||||
Strategy strategy = config.getStrategy(); |
||||
if (headers == null || names == null || strategy == null) { |
||||
return; |
||||
} |
||||
for (String name : names.split(" ")) { |
||||
dedupe(headers, name.trim(), strategy); |
||||
} |
||||
} |
||||
|
||||
private void dedupe(HttpHeaders headers, String name, Strategy strategy) { |
||||
List<String> values = headers.get(name); |
||||
if (values == null || values.size() <= 1) { |
||||
return; |
||||
} |
||||
switch (strategy) { |
||||
case RETAIN_FIRST: |
||||
headers.set(name, values.get(0)); |
||||
break; |
||||
case RETAIN_LAST: |
||||
headers.set(name, values.get(values.size() - 1)); |
||||
break; |
||||
case RETAIN_UNIQUE: |
||||
headers.put(name, values.stream().distinct().collect(Collectors.toList())); |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
} |
||||
|
||||
public static class Config extends AbstractGatewayFilterFactory.NameConfig { |
||||
|
||||
private Strategy strategy = Strategy.RETAIN_FIRST; |
||||
|
||||
public Strategy getStrategy() { |
||||
return strategy; |
||||
} |
||||
|
||||
public Config setStrategy(Strategy strategy) { |
||||
this.strategy = strategy; |
||||
return this; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
/* |
||||
* Copyright 2013-2018 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 |
||||
* |
||||
* 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 org.springframework.cloud.gateway.filter.factory; |
||||
|
||||
import org.junit.Test; |
||||
import org.junit.runner.RunWith; |
||||
|
||||
import org.springframework.boot.SpringBootConfiguration; |
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||
import org.springframework.boot.test.context.SpringBootTest; |
||||
import org.springframework.cloud.gateway.test.BaseWebClientTests; |
||||
import org.springframework.context.annotation.Import; |
||||
import org.springframework.test.annotation.DirtiesContext; |
||||
import org.springframework.test.context.junit4.SpringRunner; |
||||
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; |
||||
|
||||
@RunWith(SpringRunner.class) |
||||
@SpringBootTest(webEnvironment = RANDOM_PORT) |
||||
@DirtiesContext |
||||
public class DedupeResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTests { |
||||
|
||||
@Test |
||||
public void dedupeResponseHeaderFilterWorks() { |
||||
testClient.get().uri("/headers").header("Host", "www.deduperesponseheader.org") |
||||
.exchange().expectStatus().isOk().expectHeader() |
||||
.valueEquals("Access-Control-Allow-Credentials", "true").expectHeader() |
||||
.valueEquals("Access-Control-Allow-Origin", "https://musk.mars") |
||||
.expectHeader().valueEquals("Scout-Cookie", "S'mores").expectHeader() |
||||
.valueEquals("Next-Week-Lottery-Numbers", "4", "2", "42"); |
||||
} |
||||
|
||||
@EnableAutoConfiguration |
||||
@SpringBootConfiguration |
||||
@Import(DefaultTestConfig.class) |
||||
public static class TestConfig { |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
/* |
||||
* Copyright 2013-2019 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 |
||||
* |
||||
* 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 org.springframework.cloud.gateway.filter.factory; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import org.mockito.Mockito; |
||||
|
||||
import org.springframework.http.HttpHeaders; |
||||
|
||||
public class DedupeResponseHeaderGatewayFilterFactoryUnitTests { |
||||
|
||||
private static final String NAME_1 = HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; |
||||
|
||||
private static final String NAME_2 = HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS; |
||||
|
||||
private HttpHeaders headers; |
||||
|
||||
private DedupeResponseHeaderGatewayFilterFactory.Config config; |
||||
|
||||
private DedupeResponseHeaderGatewayFilterFactory filter; |
||||
|
||||
@Before |
||||
public void setUp() { |
||||
headers = Mockito.mock(HttpHeaders.class); |
||||
config = new DedupeResponseHeaderGatewayFilterFactory.Config(); |
||||
filter = new DedupeResponseHeaderGatewayFilterFactory(); |
||||
} |
||||
|
||||
@Test |
||||
public void dedupNullName() { |
||||
filter.dedupe(headers, config); |
||||
Mockito.verify(headers, Mockito.never()).get(Mockito.anyString()); |
||||
Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(), |
||||
Mockito.anyString()); |
||||
} |
||||
|
||||
@Test |
||||
public void dedupNullValues() { |
||||
config.setName(NAME_1); |
||||
Mockito.when(headers.get(NAME_1)).thenReturn(null); |
||||
filter.dedupe(headers, config); |
||||
Mockito.verify(headers).get(NAME_1); |
||||
Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(), |
||||
Mockito.anyString()); |
||||
} |
||||
|
||||
@Test |
||||
public void dedupEmptyValues() { |
||||
config.setName(NAME_1); |
||||
Mockito.when(headers.get(NAME_1)).thenReturn(new ArrayList<>()); |
||||
filter.dedupe(headers, config); |
||||
Mockito.verify(headers).get(NAME_1); |
||||
Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(), |
||||
Mockito.anyString()); |
||||
} |
||||
|
||||
@Test |
||||
public void dedupSingleValue() { |
||||
config.setName(NAME_1); |
||||
Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("1")); |
||||
filter.dedupe(headers, config); |
||||
Mockito.verify(headers).get(NAME_1); |
||||
Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(), |
||||
Mockito.anyString()); |
||||
} |
||||
|
||||
@Test |
||||
public void dedupMultipleValuesRetainFirst() { |
||||
config.setName(NAME_1 + " " + NAME_2); |
||||
Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("2", "3", "3", "4")); |
||||
Mockito.when(headers.get(NAME_2)).thenReturn(Arrays.asList("true", "false")); |
||||
filter.dedupe(headers, config); |
||||
Mockito.verify(headers).get(NAME_1); |
||||
Mockito.verify(headers).set(NAME_1, "2"); |
||||
Mockito.verify(headers).get(NAME_2); |
||||
Mockito.verify(headers).set(NAME_2, "true"); |
||||
Mockito.verify(headers, Mockito.times(2)).set(Mockito.anyString(), |
||||
Mockito.anyString()); |
||||
} |
||||
|
||||
@Test |
||||
public void dedupMultipleValuesRetainLast() { |
||||
config.setName(NAME_1); |
||||
config.setStrategy(DedupeResponseHeaderGatewayFilterFactory.Strategy.RETAIN_LAST); |
||||
Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("2", "3", "3", "4")); |
||||
filter.dedupe(headers, config); |
||||
Mockito.verify(headers).get(NAME_1); |
||||
Mockito.verify(headers).set(NAME_1, "4"); |
||||
Mockito.verify(headers).set(Mockito.anyString(), Mockito.anyString()); |
||||
} |
||||
|
||||
@Test |
||||
public void dedupMultipleValuesRetainUnique() { |
||||
config.setName(NAME_1); |
||||
config.setStrategy( |
||||
DedupeResponseHeaderGatewayFilterFactory.Strategy.RETAIN_UNIQUE); |
||||
Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("2", "3", "3", "4")); |
||||
filter.dedupe(headers, config); |
||||
Mockito.verify(headers).get(NAME_1); |
||||
Mockito.verify(headers).put(Mockito.eq(NAME_1), |
||||
Mockito.eq(Arrays.asList("2", "3", "4"))); |
||||
Mockito.verify(headers).put(Mockito.anyString(), Mockito.anyList()); |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue