diff --git a/pom.xml b/pom.xml
index 1a8cf2b..e785286 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,6 +56,11 @@
spring-boot-starter
provided
+
+ org.springframework
+ spring-webflux
+ provided
+
org.springframework.boot
spring-boot-starter-test
diff --git a/src/main/java/org/cloudfoundry/router/CertificateLoader.java b/src/main/java/org/cloudfoundry/router/CertificateLoader.java
new file mode 100644
index 0000000..a8c84a5
--- /dev/null
+++ b/src/main/java/org/cloudfoundry/router/CertificateLoader.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2017-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
+ *
+ * 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.cloudfoundry.router;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+class CertificateLoader {
+
+ public static final String HEADER_NAME = "X-Forwarded-Client-Cert";
+ private final CertificateFactory certificateFactory;
+
+ public CertificateLoader(CertificateFactory certificateFactory) {
+ this.certificateFactory = certificateFactory;
+ }
+
+ public List getCertificates(Iterable headerValues) throws CertificateException, IOException {
+ List certificates = new ArrayList<>();
+
+ for (String rawCertificate : getRawCertificates(headerValues)) {
+ try (InputStream in = new ByteArrayInputStream(decodeHeader(rawCertificate))) {
+ certificates.add((X509Certificate) this.certificateFactory.generateCertificate(in));
+ }
+ }
+
+ return certificates;
+ }
+
+ private byte[] decodeHeader(String rawCertificate) {
+ try {
+ return Base64.getDecoder().decode(rawCertificate);
+ } catch (IllegalArgumentException e1) {
+ try {
+ return URLDecoder.decode(rawCertificate, "utf-8").getBytes();
+ } catch (UnsupportedEncodingException e2) {
+ throw new IllegalArgumentException("Header contains value that is neither base64 nor url encoded");
+ }
+ }
+ }
+
+ private List getRawCertificates(Iterable headerValues) {
+ if (headerValues == null) {
+ return Collections.emptyList();
+ }
+
+ Iterator candidates = headerValues.iterator();
+ List rawCertificates = new ArrayList<>();
+ while (candidates.hasNext()) {
+ String candidate = candidates.next();
+
+ if (hasMultipleCertificates(candidate)) {
+ rawCertificates.addAll(Arrays.asList(candidate.split(",")));
+ } else {
+ rawCertificates.add(candidate);
+ }
+ }
+
+ return rawCertificates;
+ }
+
+ private boolean hasMultipleCertificates(String candidate) {
+ return candidate.indexOf(',') != -1;
+ }
+
+}
diff --git a/src/main/java/org/cloudfoundry/router/ClientCertificateMapper.java b/src/main/java/org/cloudfoundry/router/ClientCertificateMapper.java
index 226ddda..2c38c7b 100644
--- a/src/main/java/org/cloudfoundry/router/ClientCertificateMapper.java
+++ b/src/main/java/org/cloudfoundry/router/ClientCertificateMapper.java
@@ -47,14 +47,12 @@ final class ClientCertificateMapper implements Filter {
static final String ATTRIBUTE = "javax.servlet.request.X509Certificate";
- static final String HEADER = "X-Forwarded-Client-Cert";
-
private final Logger logger = Logger.getLogger(this.getClass().getName());
- private final CertificateFactory certificateFactory;
+ private final CertificateLoader certificateLoader;
ClientCertificateMapper() throws CertificateException {
- this.certificateFactory = CertificateFactory.getInstance("X.509");
+ this.certificateLoader = new CertificateLoader(CertificateFactory.getInstance("X.509"));
}
@Override
@@ -66,7 +64,8 @@ public void destroy() {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
try {
- List certificates = getCertificates((HttpServletRequest) request);
+ Enumeration header = ((HttpServletRequest) request).getHeaders(CertificateLoader.HEADER_NAME);
+ List certificates = this.certificateLoader.getCertificates(Collections.list(header));
if (!certificates.isEmpty()) {
request.setAttribute(ATTRIBUTE, certificates.toArray(new X509Certificate[0]));
@@ -83,54 +82,4 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
public void init(FilterConfig filterConfig) {
}
-
- private byte[] decodeHeader(String rawCertificate) {
- try {
- return Base64.getDecoder().decode(rawCertificate);
- } catch (IllegalArgumentException e1) {
- try {
- return URLDecoder.decode(rawCertificate, "utf-8").getBytes();
- } catch (UnsupportedEncodingException e2) {
- throw new IllegalArgumentException("Header contains value that is neither base64 nor url encoded");
- }
- }
- }
-
- private List getCertificates(HttpServletRequest request) throws CertificateException, IOException {
- List certificates = new ArrayList<>();
-
- for (String rawCertificate : getRawCertificates(request)) {
- try (InputStream in = new ByteArrayInputStream(decodeHeader(rawCertificate))) {
- certificates.add((X509Certificate) this.certificateFactory.generateCertificate(in));
- }
- }
-
- return certificates;
- }
-
- private List getRawCertificates(HttpServletRequest request) {
- Enumeration candidates = request.getHeaders(HEADER);
-
- if (candidates == null) {
- return Collections.emptyList();
- }
-
- List rawCertificates = new ArrayList<>();
- while (candidates.hasMoreElements()) {
- String candidate = candidates.nextElement();
-
- if (hasMultipleCertificates(candidate)) {
- rawCertificates.addAll(Arrays.asList(candidate.split(",")));
- } else {
- rawCertificates.add(candidate);
- }
- }
-
- return rawCertificates;
- }
-
- private boolean hasMultipleCertificates(String candidate) {
- return candidate.indexOf(',') != -1;
- }
-
}
diff --git a/src/main/java/org/cloudfoundry/router/ReactiveClientCertificateMapper.java b/src/main/java/org/cloudfoundry/router/ReactiveClientCertificateMapper.java
new file mode 100644
index 0000000..918f127
--- /dev/null
+++ b/src/main/java/org/cloudfoundry/router/ReactiveClientCertificateMapper.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017-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
+ *
+ * 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.cloudfoundry.router;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+import reactor.core.publisher.Mono;
+
+import java.io.IOException;
+import java.security.cert.CertificateException;
+
+public class ReactiveClientCertificateMapper implements WebFilter {
+
+ private final Logger logger = LoggerFactory.getLogger(ReactiveClientCertificateMapper.class);
+
+ private final CertificateLoader certificateLoader;
+
+ public ReactiveClientCertificateMapper(CertificateLoader certificateLoader) {
+ this.certificateLoader = certificateLoader;
+ }
+
+ @Override
+ public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
+ try {
+ final SslInfoRequestDecorator requestDecorator = new SslInfoRequestDecorator(this.certificateLoader, exchange.getRequest());
+ final ServerWebExchange exchangeWithSslInfo = exchange.mutate().request(requestDecorator).build();
+ return chain.filter(exchangeWithSslInfo);
+ } catch (CertificateException e) {
+ this.logger.warn("Unable to parse certificates in X-Forwarded-Client-Cert");
+ } catch (IOException e) {
+ return Mono.error(e);
+ }
+
+ return chain.filter(exchange);
+ }
+}
diff --git a/src/main/java/org/cloudfoundry/router/ReactiveClientCertificateMapperAutoConfiguration.java b/src/main/java/org/cloudfoundry/router/ReactiveClientCertificateMapperAutoConfiguration.java
new file mode 100644
index 0000000..74a3dbc
--- /dev/null
+++ b/src/main/java/org/cloudfoundry/router/ReactiveClientCertificateMapperAutoConfiguration.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017-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
+ *
+ * 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.cloudfoundry.router;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
+import org.springframework.boot.cloud.CloudPlatform;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.server.WebFilter;
+
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+
+@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
+@ConditionalOnCloudPlatform(CloudPlatform.CLOUD_FOUNDRY)
+@Configuration
+public class ReactiveClientCertificateMapperAutoConfiguration {
+
+ @Bean
+ WebFilter reactiveCertificateMapper() throws CertificateException {
+ CertificateLoader certificateLoader = new CertificateLoader(CertificateFactory.getInstance("X.509"));
+ return new ReactiveClientCertificateMapper(certificateLoader);
+ }
+}
diff --git a/src/main/java/org/cloudfoundry/router/SimpleSslInfoHolder.java b/src/main/java/org/cloudfoundry/router/SimpleSslInfoHolder.java
new file mode 100644
index 0000000..1a94c68
--- /dev/null
+++ b/src/main/java/org/cloudfoundry/router/SimpleSslInfoHolder.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017-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
+ *
+ * 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.cloudfoundry.router;
+
+import org.springframework.http.server.reactive.SslInfo;
+
+import java.security.cert.X509Certificate;
+
+public class SimpleSslInfoHolder implements SslInfo {
+
+ private final String sessionId;
+ private final X509Certificate[] peerCertificates;
+
+ public SimpleSslInfoHolder(String sessionId, X509Certificate[] peerCertificates) {
+ this.sessionId = sessionId;
+ this.peerCertificates = peerCertificates;
+ }
+
+ @Override
+ public String getSessionId() {
+ return this.sessionId;
+ }
+
+ @Override
+ public X509Certificate[] getPeerCertificates() {
+ return this.peerCertificates;
+ }
+}
diff --git a/src/main/java/org/cloudfoundry/router/SslInfoRequestDecorator.java b/src/main/java/org/cloudfoundry/router/SslInfoRequestDecorator.java
new file mode 100644
index 0000000..8d2f26c
--- /dev/null
+++ b/src/main/java/org/cloudfoundry/router/SslInfoRequestDecorator.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017-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
+ *
+ * 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.cloudfoundry.router;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
+import org.springframework.http.server.reactive.SslInfo;
+
+import java.io.IOException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Enumeration;
+import java.util.List;
+
+public class SslInfoRequestDecorator extends ServerHttpRequestDecorator {
+
+ private final Logger logger = LoggerFactory.getLogger(SslInfoRequestDecorator.class);
+
+ private final CertificateLoader certificateLoader;
+
+ private final SslInfo sslInfo;
+
+ public SslInfoRequestDecorator(CertificateLoader certificateLoader, ServerHttpRequest delegate) throws CertificateException, IOException {
+ super(delegate);
+
+ this.certificateLoader = certificateLoader;
+ final List headers = delegate.getHeaders().get(CertificateLoader.HEADER_NAME);
+
+ if (delegate.getSslInfo() != null || headers == null || headers.isEmpty()) {
+ logger.debug("Original request contains SslInfo, skipping certificate loading from header");
+ this.sslInfo = delegate.getSslInfo();
+ }
+ else {
+ logger.debug("Original request does not contain SslInfo, loading certificates from header");
+ this.sslInfo = new SimpleSslInfoHolder(null, certificateLoader.getCertificates(headers).toArray(new X509Certificate[0]));
+ }
+ }
+
+ @Override
+ public SslInfo getSslInfo() {
+ return this.sslInfo;
+ }
+}
diff --git a/src/main/resources/META-INF/spring.factories b/src/main/resources/META-INF/spring.factories
index ae9f5ff..7460590 100644
--- a/src/main/resources/META-INF/spring.factories
+++ b/src/main/resources/META-INF/spring.factories
@@ -1 +1 @@
-org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.cloudfoundry.router.ClientCertificateMapperAutoConfiguration
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.cloudfoundry.router.ClientCertificateMapperAutoConfiguration,org.cloudfoundry.router.ReactiveClientCertificateMapperAutoConfiguration
diff --git a/src/test/java/org/cloudfoundry/router/ClientCertificateMapperTest.java b/src/test/java/org/cloudfoundry/router/ClientCertificateMapperTest.java
index 58842ed..cdd12a6 100644
--- a/src/test/java/org/cloudfoundry/router/ClientCertificateMapperTest.java
+++ b/src/test/java/org/cloudfoundry/router/ClientCertificateMapperTest.java
@@ -107,7 +107,7 @@ public ClientCertificateMapperTest() throws CertificateException {
@Test
public void emptyHeader() throws IOException, ServletException {
- this.request.addHeader(ClientCertificateMapper.HEADER, "");
+ this.request.addHeader(CertificateLoader.HEADER_NAME, "");
this.mapper.doFilter(this.request, this.response, this.filterChain);
@@ -117,7 +117,7 @@ public void emptyHeader() throws IOException, ServletException {
@Test
public void invalidHeader() throws IOException, ServletException {
- this.request.addHeader(ClientCertificateMapper.HEADER, "Invalid Header Value");
+ this.request.addHeader(CertificateLoader.HEADER_NAME, "Invalid Header Value");
this.mapper.doFilter(this.request, this.response, this.filterChain);
@@ -127,8 +127,8 @@ public void invalidHeader() throws IOException, ServletException {
@Test
public void invalidMultipleHeaders() throws IOException, ServletException {
- this.request.addHeader(ClientCertificateMapper.HEADER, CERTIFICATE_1);
- this.request.addHeader(ClientCertificateMapper.HEADER, "Invalid Header Value");
+ this.request.addHeader(CertificateLoader.HEADER_NAME, CERTIFICATE_1);
+ this.request.addHeader(CertificateLoader.HEADER_NAME, "Invalid Header Value");
this.mapper.doFilter(this.request, this.response, this.filterChain);
@@ -138,7 +138,7 @@ public void invalidMultipleHeaders() throws IOException, ServletException {
@Test
public void invalidMultipleInOneHeader() throws IOException, ServletException {
- this.request.addHeader(ClientCertificateMapper.HEADER, String.format("%s,Invalid Header Value", CERTIFICATE_1));
+ this.request.addHeader(CertificateLoader.HEADER_NAME, String.format("%s,Invalid Header Value", CERTIFICATE_1));
this.mapper.doFilter(this.request, this.response, this.filterChain);
@@ -148,8 +148,8 @@ public void invalidMultipleInOneHeader() throws IOException, ServletException {
@Test
public void multipleHeaders() throws IOException, ServletException {
- this.request.addHeader(ClientCertificateMapper.HEADER, CERTIFICATE_1);
- this.request.addHeader(ClientCertificateMapper.HEADER, CERTIFICATE_2);
+ this.request.addHeader(CertificateLoader.HEADER_NAME, CERTIFICATE_1);
+ this.request.addHeader(CertificateLoader.HEADER_NAME, CERTIFICATE_2);
this.mapper.doFilter(this.request, this.response, this.filterChain);
@@ -159,7 +159,7 @@ public void multipleHeaders() throws IOException, ServletException {
@Test
public void multipleInOneHeader() throws IOException, ServletException {
- this.request.addHeader(ClientCertificateMapper.HEADER, String.format("%s,%s", CERTIFICATE_1, CERTIFICATE_2));
+ this.request.addHeader(CertificateLoader.HEADER_NAME, String.format("%s,%s", CERTIFICATE_1, CERTIFICATE_2));
this.mapper.doFilter(this.request, this.response, this.filterChain);
@@ -170,7 +170,7 @@ public void multipleInOneHeader() throws IOException, ServletException {
@Test
public void nginxHeader() throws IOException, ServletException {
- this.request.addHeader(ClientCertificateMapper.HEADER, String.format("%s", NGINX_ESCAPED_CERT));
+ this.request.addHeader(CertificateLoader.HEADER_NAME, String.format("%s", NGINX_ESCAPED_CERT));
this.mapper.doFilter(this.request, this.response, this.filterChain);
@@ -186,4 +186,4 @@ public void noHeader() throws IOException, ServletException {
assertThat(this.request.getAttribute(ClientCertificateMapper.ATTRIBUTE)).isNull();
}
-}
\ No newline at end of file
+}
diff --git a/src/test/java/org/cloudfoundry/router/ReactiveClientCertificateMapperTest.java b/src/test/java/org/cloudfoundry/router/ReactiveClientCertificateMapperTest.java
new file mode 100644
index 0000000..a5eed85
--- /dev/null
+++ b/src/test/java/org/cloudfoundry/router/ReactiveClientCertificateMapperTest.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2017-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
+ *
+ * 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.cloudfoundry.router;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ReactiveClientCertificateMapperTest {
+
+ private static final String CERTIFICATE_1 = "" +
+ "MIIDLTCCAhWgAwIBAgIkMDg3ZjVmZGMtOThkNy00MGMwLTY0ZDMtZmQ5NWFmODMx" +
+ "OThkMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNVBAMMD2NyZWRodWJDbGllbnRDQTAe" +
+ "Fw0xNzA1MDIwMDQ5MzFaFw0xNzA1MDMwMDQ5MzFaMGIxMTAvBgNVBAsTKGFwcDoy" +
+ "MzI4MmZkMS0zNWI0LTQ1ZGQtYTYwMi04Zjc2ZjRhNjBkMTExLTArBgNVBAMTJDA4" +
+ "N2Y1ZmRjLTk4ZDctNDBjMC02NGQzLWZkOTVhZjgzMTk4ZDCCASIwDQYJKoZIhvcN" +
+ "AQEBBQADggEPADCCAQoCggEBAPhcSn56pIVWI0RpwrkC3WcvumLw+3i/oj3YBbEx" +
+ "AUAFJMFl/yt1zpAghLvYOOiiUS/W04SKp8Z9FHlmNabJOzV40RIciSbYCW0tBeFG" +
+ "KNkgolTGamvRLZkkHUJdywEQkvnMG7+2XczDBoCZ7fdBepg6gieSqGhQwl/sO7x/" +
+ "TouvQnujKwJLiXOKQq00TkT+MVEzOZyOMlqFh9r2XjUGuh1HnRM0IAj6buR5663t" +
+ "4lAQqOluTAVNCKWSrAMIKb0G4QPTQ4pKRTeMEnTijFErtKlpzc64HYrBpufj1K/q" +
+ "TxYIy3EgeT3UVSclSub14M4/r/mOmWotYP81BR1Ko7pxV28CAwEAAaMTMBEwDwYD" +
+ "VR0RBAgwBocECv4AAjANBgkqhkiG9w0BAQsFAAOCAQEAuG8A33+Un2rvXA+qAf40" +
+ "gBponN2mjx0drasw/MqBnclUL1MYvOepqcGxxNB/1Ok/bKKDMr03ugVaxzAdoknA" +
+ "NwIyY/ghL6xHs/JrmuSGDs9BeNF0y8TOpQmmjh1EDFtR9YFuTRP1OZ6XBf5fbd80" +
+ "Q684k/Wu8ELywZJd53FKcTPJRQ/Yjn4QFJORtcNFlvMFWTmJLLiMDbI8JBcqMLZH" +
+ "sgdyBtV7kJdZU3nszgFEPspYzFfxQZmq6V+pJb+dmG2jYWrX/R21J9x1dJHBCoPp" +
+ "XcqQm8pYsDxi+HTGS6an78sHqrvU5uQJq2MW8o6iBJR80bFgWSl7GTqK3Xz5iTxU" +
+ "Ew==";
+
+ private static final String CERTIFICATE_2 = "" +
+ "MIIC1TCCAb2gAwIBAgIUL3dmX9jNj2XqQaXv9noNfU84VoowDQYJKoZIhvcNAQEL" +
+ "BQAwGjEYMBYGA1UEAwwPY3JlZGh1YkNsaWVudENBMB4XDTE3MDMzMDE5MTg0NVoX" +
+ "DTE4MDMzMDE5MTg0NVowGjEYMBYGA1UEAwwPY3JlZGh1YkNsaWVudENBMIIBIjAN" +
+ "BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz1bJ1NkS+uDl3xMo8fvPFRsXdZUW" +
+ "Un4N9nOfX/bfTWHrDKgW6+qrkkDBW4NLw0IHfgV99HwAygmiMC5La2HJg3JzcRMn" +
+ "dq9MosrNjv5wVtkQAReLVCcZ+EMb4f+0tlbespsfMQpKYfksovXHTSv+zbvbE+pX" +
+ "ObSUYpbZ09LtvbVL9s6hO5E9P9uXuV+ZSOZTISqtEIF6sXOKjx6WTCanG6jqf4+4" +
+ "Lyasffen18NcMld6f7cfEgExUO7OVN86J28+LcILICAOB2m8ug4KnDkigaJp25ou" +
+ "bPl/YnJtMh75buBjiOLI5p9j/n2mliUTKC5fJ54fb6MoMKXgXPAC7bcz5wIDAQAB" +
+ "oxMwETAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBXc7cDaA8D" +
+ "Iuoxnt5SVAk9R664OiMxOiQJ7oavdcU1S2hS22MOzAM1gMAwur1C8fmjcHthma4a" +
+ "IFzzyvWlT3cfKmr+e1CVU0fOr1f4kFFval4kSa9uFbqaqQlj6dovoO34W9eadTyN" +
+ "mACol2RdG0tjYWzbUaHdA21PdcezhiVw+PnXbzfKSnjWoxv0id1JTTPnVqfghTjG" +
+ "pEqerOIo3+YRhkUsUEhJ9SFa58dtlKRPtKQjSuMTeBgQiU7WCpueFfPqRM1Ab7bP" +
+ "OeiChAJVyknz/Mu1KmQxoZ43JfCyUIdtT5oE7CWIJt3qVwJYLgykuYV8vXEnIALB" +
+ "p/ob7SaWTJJO";
+
+ private static final String NGINX_ESCAPED_CERT = "" +
+ "%2D%2D%2D%2D%2DBEGIN%20CERTIFICATE%2D%2D%2D%2D%2D%0D%0AMIIDLTCCA" +
+ "hWgAwIBAgIkMDg3ZjVmZGMtOThkNy00MGMwLTY0ZDMtZmQ5NWFmODMx%0D%0AOTh" +
+ "kMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNVBAMMD2NyZWRodWJDbGllbnRDQTAe%0D" +
+ "%0AFw0xNzA1MDIwMDQ5MzFaFw0xNzA1MDMwMDQ5MzFaMGIxMTAvBgNVBAsTKGFwc" +
+ "Doy%0D%0AMzI4MmZkMS0zNWI0LTQ1ZGQtYTYwMi04Zjc2ZjRhNjBkMTExLTArBgN" +
+ "VBAMTJDA4%0D%0AN2Y1ZmRjLTk4ZDctNDBjMC02NGQzLWZkOTVhZjgzMTk4ZDCCA" +
+ "SIwDQYJKoZIhvcN%0D%0AAQEBBQADggEPADCCAQoCggEBAPhcSn56pIVWI0Rpwrk" +
+ "C3WcvumLw%2B3i%2Foj3YBbEx%0D%0AAUAFJMFl%2Fyt1zpAghLvYOOiiUS%2FW0" +
+ "4SKp8Z9FHlmNabJOzV40RIciSbYCW0tBeFG%0D%0AKNkgolTGamvRLZkkHUJdywE" +
+ "QkvnMG7%2B2XczDBoCZ7fdBepg6gieSqGhQwl%2FsO7x%2F%0D%0ATouvQnujKwJ" +
+ "LiXOKQq00TkT%2BMVEzOZyOMlqFh9r2XjUGuh1HnRM0IAj6buR5663t%0D%0A4lA" +
+ "QqOluTAVNCKWSrAMIKb0G4QPTQ4pKRTeMEnTijFErtKlpzc64HYrBpufj1K%2Fq%" +
+ "0D%0ATxYIy3EgeT3UVSclSub14M4%2Fr%2FmOmWotYP81BR1Ko7pxV28CAwEAAaM" +
+ "TMBEwDwYD%0D%0AVR0RBAgwBocECv4AAjANBgkqhkiG9w0BAQsFAAOCAQEAuG8A3" +
+ "3%2BUn2rvXA%2BqAf40%0D%0AgBponN2mjx0drasw%2FMqBnclUL1MYvOepqcGxx" +
+ "NB%2F1Ok%2FbKKDMr03ugVaxzAdoknA%0D%0ANwIyY%2FghL6xHs%2FJrmuSGDs9" +
+ "BeNF0y8TOpQmmjh1EDFtR9YFuTRP1OZ6XBf5fbd80%0D%0AQ684k%2FWu8ELywZJ" +
+ "d53FKcTPJRQ%2FYjn4QFJORtcNFlvMFWTmJLLiMDbI8JBcqMLZH%0D%0AsgdyBtV" +
+ "7kJdZU3nszgFEPspYzFfxQZmq6V%2BpJb%2BdmG2jYWrX%2FR21J9x1dJHBCoPp%" +
+ "0D%0AXcqQm8pYsDxi%2BHTGS6an78sHqrvU5uQJq2MW8o6iBJR80bFgWSl7GTqK3" +
+ "Xz5iTxU%0D%0AEw%3D%3D%0D%0A%2D%2D%2D%2D%2DEND%20CERTIFICATE%2D%2" +
+ "D%2D%2D%2D%0D%0A";
+
+
+ private ReactiveClientCertificateMapper mapper;
+ private ServerWebExchange actualExchange;
+
+ @Before
+ public void setUp() throws CertificateException {
+ this.mapper = new ReactiveClientCertificateMapper(new CertificateLoader(CertificateFactory.getInstance("X.509")));
+ }
+
+ private ServerWebExchange doFilter(MockServerHttpRequest request) {
+ MockServerWebExchange serverWebExchange = MockServerWebExchange.from(request);
+
+ this.mapper.filter(serverWebExchange, exchange -> {
+ actualExchange = exchange;
+ return Mono.empty();
+ }).block();
+
+ return actualExchange;
+ }
+
+ @Test
+ public void emptyHeader() {
+ MockServerHttpRequest request = MockServerHttpRequest.get("/").header(CertificateLoader.HEADER_NAME, "").build();
+
+ ServerWebExchange exchange = doFilter(request);
+
+ assertThat(exchange.getRequest().getSslInfo()).isNull();
+ }
+
+ @Test
+ public void invalidHeader() {
+ MockServerHttpRequest request = MockServerHttpRequest.get("/").header(CertificateLoader.HEADER_NAME, "Invalid Header Value").build();
+
+ ServerWebExchange exchange = doFilter(request);
+
+ assertThat(exchange.getRequest().getSslInfo()).isNull();
+ }
+
+ @Test
+ public void invalidMultipleHeaders() {
+ MockServerHttpRequest request = MockServerHttpRequest.get("/").header(CertificateLoader.HEADER_NAME, CERTIFICATE_1, "Invalid Header Value").build();
+
+ ServerWebExchange exchange = doFilter(request);
+
+ assertThat(exchange.getRequest().getSslInfo()).isNull();
+ }
+
+ @Test
+ public void invalidMultipleInOneHeader() {
+ MockServerHttpRequest request = MockServerHttpRequest.get("/").header(CertificateLoader.HEADER_NAME, String.format("%s,Invalid Header Value", CERTIFICATE_1)).build();
+
+ ServerWebExchange exchange = doFilter(request);
+
+ assertThat(exchange.getRequest().getSslInfo()).isNull();
+ }
+
+ @Test
+ public void multipleHeaders() {
+ MockServerHttpRequest request = MockServerHttpRequest.get("/").header(CertificateLoader.HEADER_NAME, CERTIFICATE_1, CERTIFICATE_2).build();
+
+ ServerWebExchange exchange = doFilter(request);
+
+ assertThat(exchange.getRequest().getSslInfo().getPeerCertificates()).hasSize(2);
+ }
+
+ @Test
+ public void multipleInOneHeader() {
+ MockServerHttpRequest request = MockServerHttpRequest.get("/").header(CertificateLoader.HEADER_NAME, String.format("%s,%s", CERTIFICATE_1, CERTIFICATE_2)).build();
+
+ ServerWebExchange exchange = doFilter(request);
+
+ assertThat(exchange.getRequest().getSslInfo().getPeerCertificates()).hasSize(2);
+ }
+
+ @Test
+ public void nginxHeader() {
+ MockServerHttpRequest request = MockServerHttpRequest.get("/").header(CertificateLoader.HEADER_NAME, String.format("%s", NGINX_ESCAPED_CERT)).build();
+
+ ServerWebExchange exchange = doFilter(request);
+
+ assertThat(exchange.getRequest().getSslInfo().getPeerCertificates()).hasSize(1);
+ }
+
+ @Test
+ public void noHeader() {
+ MockServerHttpRequest request = MockServerHttpRequest.get("/").build();
+
+ ServerWebExchange exchange = doFilter(request);
+
+ assertThat(exchange.getRequest().getSslInfo()).isNull();
+ }
+}