From 87375fe6f81cb70f9ee3e44c4800ef8edce655a0 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 10 Nov 2017 16:43:12 -0500 Subject: [PATCH] ServerHttpRequest exposes SSL certificates Issue: SPR-15964 --- .../reactive/MockServerHttpRequest.java | 29 ++++- .../reactive/AbstractServerHttpRequest.java | 20 ++++ .../DefaultServerHttpRequestBuilder.java | 20 ++-- .../http/server/reactive/DefaultSslInfo.java | 102 ++++++++++++++++++ .../reactive/ReactorServerHttpRequest.java | 13 +++ .../server/reactive/ServerHttpRequest.java | 8 ++ .../reactive/ServerHttpRequestDecorator.java | 6 ++ .../reactive/ServletServerHttpRequest.java | 12 ++- .../http/server/reactive/SslInfo.java | 34 ++++++ .../reactive/UndertowServerHttpRequest.java | 11 ++ .../reactive/test/MockServerHttpRequest.java | 29 ++++- 11 files changed, 271 insertions(+), 13 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/DefaultSslInfo.java create mode 100644 spring-web/src/main/java/org/springframework/http/server/reactive/SslInfo.java diff --git a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java index d51116d2de..25889ec448 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java @@ -38,6 +38,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpRange; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.AbstractServerHttpRequest; +import org.springframework.http.server.reactive.SslInfo; import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MimeType; @@ -60,17 +61,22 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest { @Nullable private final InetSocketAddress remoteAddress; + @Nullable + private final SslInfo sslInfo; + private final Flux body; private MockServerHttpRequest(HttpMethod httpMethod, URI uri, @Nullable String contextPath, HttpHeaders headers, MultiValueMap cookies, - @Nullable InetSocketAddress remoteAddress, Publisher body) { + @Nullable InetSocketAddress remoteAddress, @Nullable SslInfo sslInfo, + Publisher body) { super(uri, contextPath, headers); this.httpMethod = httpMethod; this.cookies = cookies; this.remoteAddress = remoteAddress; + this.sslInfo = sslInfo; this.body = Flux.from(body); } @@ -91,6 +97,12 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest { return this.remoteAddress; } + @Nullable + @Override + protected SslInfo initSslInfo() { + return this.sslInfo; + } + @Override public Flux getBody() { return this.body; @@ -218,6 +230,11 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest { */ B remoteAddress(InetSocketAddress remoteAddress); + /** + * Set SSL session information and certificates. + */ + void sslInfo(SslInfo sslInfo); + /** * Add one or more cookies. */ @@ -365,6 +382,9 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest { @Nullable private InetSocketAddress remoteAddress; + @Nullable + private SslInfo sslInfo; + public DefaultBodyBuilder(HttpMethod method, URI url) { this.method = method; @@ -383,6 +403,11 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest { return this; } + @Override + public void sslInfo(SslInfo sslInfo) { + this.sslInfo = sslInfo; + } + @Override public BodyBuilder cookie(HttpCookie... cookies) { Arrays.stream(cookies).forEach(cookie -> this.cookies.add(cookie.getName(), cookie)); @@ -482,7 +507,7 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest { public MockServerHttpRequest body(Publisher body) { applyCookiesIfNecessary(); return new MockServerHttpRequest(this.method, this.url, this.contextPath, - this.headers, this.cookies, this.remoteAddress, body); + this.headers, this.cookies, this.remoteAddress, this.sslInfo, body); } private void applyCookiesIfNecessary() { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index 163d260f1c..a7fd365918 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -59,6 +59,9 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { @Nullable private MultiValueMap cookies; + @Nullable + private SslInfo sslInfo; + /** * Constructor with the URI and headers for the request. @@ -152,6 +155,23 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { */ protected abstract MultiValueMap initCookies(); + @Nullable + @Override + public SslInfo getSslInfo() { + if (this.sslInfo == null) { + this.sslInfo = initSslInfo(); + } + return this.sslInfo; + } + + /** + * Obtain SSL session information from the underlying "native" request. + * @return the SSL information or {@code null} if not available + * @since 5.0.2 + */ + @Nullable + protected abstract SslInfo initSslInfo(); + /** * Return the underlying server response. *

Note: This is exposed mainly for internal framework diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java index 8de8d44300..5166a7b4a5 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java @@ -52,9 +52,6 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { private final MultiValueMap cookies; - @Nullable - private final InetSocketAddress remoteAddress; - @Nullable private String uriPath; @@ -71,7 +68,6 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { this.uri = original.getURI(); this.httpMethodValue = original.getMethodValue(); - this.remoteAddress = original.getRemoteAddress(); this.body = original.getBody(); this.httpHeaders = new HttpHeaders(); @@ -135,8 +131,7 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { public ServerHttpRequest build() { URI uriToUse = getUriToUse(); return new DefaultServerHttpRequest(uriToUse, this.contextPath, this.httpHeaders, - this.httpMethodValue, this.cookies, this.remoteAddress, this.body, - this.originalRequest); + this.httpMethodValue, this.cookies, this.body, this.originalRequest); } @@ -162,6 +157,9 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { @Nullable private final InetSocketAddress remoteAddress; + @Nullable + private final SslInfo sslInfo; + private final Flux body; private final ServerHttpRequest originalRequest; @@ -169,13 +167,13 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { public DefaultServerHttpRequest(URI uri, @Nullable String contextPath, HttpHeaders headers, String methodValue, MultiValueMap cookies, - @Nullable InetSocketAddress remoteAddress, Flux body, ServerHttpRequest originalRequest) { super(uri, contextPath, headers); this.methodValue = methodValue; this.cookies = cookies; - this.remoteAddress = remoteAddress; + this.remoteAddress = originalRequest.getRemoteAddress(); + this.sslInfo = originalRequest.getSslInfo(); this.body = body; this.originalRequest = originalRequest; } @@ -197,6 +195,12 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { return this.remoteAddress; } + @Nullable + @Override + protected SslInfo initSslInfo() { + return this.sslInfo; + } + @Override public Flux getBody() { return this.body; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultSslInfo.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultSslInfo.java new file mode 100644 index 0000000000..02b34ef93c --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultSslInfo.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2017 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.http.server.reactive; + +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +import javax.net.ssl.SSLSession; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +final class DefaultSslInfo implements SslInfo { + + @Nullable + private final String sessionId; + + private final X509Certificate[] peerCertificates; + + + DefaultSslInfo(String sessionId, X509Certificate[] peerCertificates) { + Assert.notNull(peerCertificates, "No SSL certificates"); + this.sessionId = sessionId; + this.peerCertificates = peerCertificates; + } + + DefaultSslInfo(SSLSession session) { + Assert.notNull(session, "SSLSession is required"); + this.sessionId = initSessionId(session); + this.peerCertificates = initCertificates(session); + } + + @Nullable + private static String initSessionId(SSLSession session) { + byte [] bytes = session.getId(); + if (bytes == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + String digit = Integer.toHexString(b); + if (digit.length() < 2) { + sb.append('0'); + } + if (digit.length() > 2) { + digit = digit.substring(digit.length() - 2); + } + sb.append(digit); + } + return sb.toString(); + } + + private static X509Certificate[] initCertificates(SSLSession session) { + Certificate[] certificates; + try { + certificates = session.getPeerCertificates(); + } + catch (Throwable ex) { + throw new IllegalStateException("Failed to get SSL certificates", ex); + } + List result = new ArrayList<>(certificates.length); + for (Certificate certificate : certificates) { + if (certificate instanceof X509Certificate) { + result.add((X509Certificate) certificate); + } + } + return result.toArray(new X509Certificate[result.size()]); + } + + + @Override + @Nullable + public String getSessionId() { + return this.sessionId; + } + + @Override + public X509Certificate[] getPeerCertificates() { + return this.peerCertificates; + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 9fe5813a14..dc0ed6e9a1 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -19,6 +19,7 @@ package org.springframework.http.server.reactive; import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; +import javax.net.ssl.SSLSession; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.http.HttpHeaderNames; @@ -31,6 +32,7 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -108,6 +110,7 @@ class ReactorServerHttpRequest extends AbstractServerHttpRequest { return headers; } + @Override public String getMethodValue() { return this.request.method().name(); @@ -130,6 +133,16 @@ class ReactorServerHttpRequest extends AbstractServerHttpRequest { return this.request.remoteAddress(); } + @Nullable + protected SslInfo initSslInfo() { + SslHandler sslHandler = this.request.context().channel().pipeline().get(SslHandler.class); + if (sslHandler != null) { + SSLSession session = sslHandler.engine().getSession(); + return new DefaultSslInfo(session); + } + return null; + } + @Override public Flux getBody() { return this.request.receive().retain().map(this.bufferFactory::wrap); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 7a5ec8d228..85370a960f 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -61,6 +61,14 @@ public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage @Nullable InetSocketAddress getRemoteAddress(); + /** + * Return the SSL session information if the request has been transmitted + * over a secure protocol including SSL certificates, if available. + * @return the session information or {@code null} + * @since 5.0.2 + */ + @Nullable + SslInfo getSslInfo(); /** * Return a builder to mutate properties of this request by wrapping it diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java index 116aac326b..9ba1cd1fc2 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java @@ -96,6 +96,12 @@ public class ServerHttpRequestDecorator implements ServerHttpRequest { return getDelegate().getRemoteAddress(); } + @Nullable + @Override + public SslInfo getSslInfo() { + return getDelegate().getSslInfo(); + } + @Override public Flux getBody() { return getDelegate().getBody(); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index 4932b72e9d..89418a0ecc 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -21,6 +21,7 @@ import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.security.cert.X509Certificate; import java.util.Enumeration; import java.util.Map; import javax.servlet.AsyncContext; @@ -89,7 +90,6 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest { this.bodyPublisher.registerReadListener(); } - private static URI initUri(HttpServletRequest request) { Assert.notNull(request, "'request' must not be null"); try { @@ -172,6 +172,16 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest { return new InetSocketAddress(this.request.getRemoteHost(), this.request.getRemotePort()); } + @Nullable + protected SslInfo initSslInfo() { + if (!this.request.isSecure()) { + return null; + } + return new DefaultSslInfo( + (String) request.getAttribute("javax.servlet.request.ssl_session_id"), + (X509Certificate[]) request.getAttribute("java.security.cert.X509Certificate")); + } + @Override public Flux getBody() { return Flux.from(this.bodyPublisher); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/SslInfo.java b/spring-web/src/main/java/org/springframework/http/server/reactive/SslInfo.java new file mode 100644 index 0000000000..58eeee2ab5 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/SslInfo.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2017 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.http.server.reactive; + +import java.security.cert.X509Certificate; + +import org.springframework.lang.Nullable; + +/** + * + * @author Rossen Stoyanchev + * @since 5.0.2 + */ +public interface SslInfo { + + @Nullable + String getSessionId(); + + X509Certificate[] getPeerCertificates(); + +} diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 4766e0824f..586e3c8453 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -23,6 +23,7 @@ import java.net.InetSocketAddress; import java.net.URI; import java.nio.ByteBuffer; import java.util.function.IntPredicate; +import javax.net.ssl.SSLSession; import io.undertow.connector.ByteBufferPool; import io.undertow.connector.PooledByteBuffer; @@ -101,6 +102,16 @@ class UndertowServerHttpRequest extends AbstractServerHttpRequest { return this.exchange.getSourceAddress(); } + @Nullable + @Override + protected SslInfo initSslInfo() { + SSLSession session = this.exchange.getConnection().getSslSession(); + if (session != null) { + return new DefaultSslInfo(session); + } + return null; + } + @Override public Flux getBody() { return Flux.from(this.body); diff --git a/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpRequest.java b/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpRequest.java index 00ae919b52..ffe448ec23 100644 --- a/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpRequest.java +++ b/spring-web/src/test/java/org/springframework/mock/http/server/reactive/test/MockServerHttpRequest.java @@ -38,6 +38,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpRange; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.AbstractServerHttpRequest; +import org.springframework.http.server.reactive.SslInfo; import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MimeType; @@ -60,17 +61,22 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest { @Nullable private final InetSocketAddress remoteAddress; + @Nullable + private final SslInfo sslInfo; + private final Flux body; private MockServerHttpRequest(HttpMethod httpMethod, URI uri, @Nullable String contextPath, HttpHeaders headers, MultiValueMap cookies, - @Nullable InetSocketAddress remoteAddress, Publisher body) { + @Nullable InetSocketAddress remoteAddress, @Nullable SslInfo sslInfo, + Publisher body) { super(uri, contextPath, headers); this.httpMethod = httpMethod; this.cookies = cookies; this.remoteAddress = remoteAddress; + this.sslInfo = sslInfo; this.body = Flux.from(body); } @@ -91,6 +97,12 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest { return this.remoteAddress; } + @Nullable + @Override + protected SslInfo initSslInfo() { + return this.sslInfo; + } + @Override public Flux getBody() { return this.body; @@ -218,6 +230,11 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest { */ B remoteAddress(InetSocketAddress remoteAddress); + /** + * Set SSL session information and certificates. + */ + void sslInfo(SslInfo sslInfo); + /** * Add one or more cookies. */ @@ -365,6 +382,9 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest { @Nullable private InetSocketAddress remoteAddress; + @Nullable + private SslInfo sslInfo; + public DefaultBodyBuilder(HttpMethod method, URI url) { this.method = method; @@ -383,6 +403,11 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest { return this; } + @Override + public void sslInfo(SslInfo sslInfo) { + this.sslInfo = sslInfo; + } + @Override public BodyBuilder cookie(HttpCookie... cookies) { Arrays.stream(cookies).forEach(cookie -> this.cookies.add(cookie.getName(), cookie)); @@ -482,7 +507,7 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest { public MockServerHttpRequest body(Publisher body) { applyCookiesIfNecessary(); return new MockServerHttpRequest(this.method, this.url, this.contextPath, - this.headers, this.cookies, this.remoteAddress, body); + this.headers, this.cookies, this.remoteAddress, this.sslInfo, body); } private void applyCookiesIfNecessary() {