Skip to content

Commit 87375fe

Browse files
committed
ServerHttpRequest exposes SSL certificates
Issue: SPR-15964
1 parent 9a894ab commit 87375fe

File tree

11 files changed

+271
-13
lines changed

11 files changed

+271
-13
lines changed

spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.http.HttpRange;
3939
import org.springframework.http.MediaType;
4040
import org.springframework.http.server.reactive.AbstractServerHttpRequest;
41+
import org.springframework.http.server.reactive.SslInfo;
4142
import org.springframework.lang.Nullable;
4243
import org.springframework.util.LinkedMultiValueMap;
4344
import org.springframework.util.MimeType;
@@ -60,17 +61,22 @@ public class MockServerHttpRequest extends AbstractServerHttpRequest {
6061
@Nullable
6162
private final InetSocketAddress remoteAddress;
6263

64+
@Nullable
65+
private final SslInfo sslInfo;
66+
6367
private final Flux<DataBuffer> body;
6468

6569

6670
private MockServerHttpRequest(HttpMethod httpMethod, URI uri, @Nullable String contextPath,
6771
HttpHeaders headers, MultiValueMap<String, HttpCookie> cookies,
68-
@Nullable InetSocketAddress remoteAddress, Publisher<? extends DataBuffer> body) {
72+
@Nullable InetSocketAddress remoteAddress, @Nullable SslInfo sslInfo,
73+
Publisher<? extends DataBuffer> body) {
6974

7075
super(uri, contextPath, headers);
7176
this.httpMethod = httpMethod;
7277
this.cookies = cookies;
7378
this.remoteAddress = remoteAddress;
79+
this.sslInfo = sslInfo;
7480
this.body = Flux.from(body);
7581
}
7682

@@ -91,6 +97,12 @@ public InetSocketAddress getRemoteAddress() {
9197
return this.remoteAddress;
9298
}
9399

100+
@Nullable
101+
@Override
102+
protected SslInfo initSslInfo() {
103+
return this.sslInfo;
104+
}
105+
94106
@Override
95107
public Flux<DataBuffer> getBody() {
96108
return this.body;
@@ -218,6 +230,11 @@ public interface BaseBuilder<B extends BaseBuilder<B>> {
218230
*/
219231
B remoteAddress(InetSocketAddress remoteAddress);
220232

233+
/**
234+
* Set SSL session information and certificates.
235+
*/
236+
void sslInfo(SslInfo sslInfo);
237+
221238
/**
222239
* Add one or more cookies.
223240
*/
@@ -365,6 +382,9 @@ private static class DefaultBodyBuilder implements BodyBuilder {
365382
@Nullable
366383
private InetSocketAddress remoteAddress;
367384

385+
@Nullable
386+
private SslInfo sslInfo;
387+
368388

369389
public DefaultBodyBuilder(HttpMethod method, URI url) {
370390
this.method = method;
@@ -383,6 +403,11 @@ public BodyBuilder remoteAddress(InetSocketAddress remoteAddress) {
383403
return this;
384404
}
385405

406+
@Override
407+
public void sslInfo(SslInfo sslInfo) {
408+
this.sslInfo = sslInfo;
409+
}
410+
386411
@Override
387412
public BodyBuilder cookie(HttpCookie... cookies) {
388413
Arrays.stream(cookies).forEach(cookie -> this.cookies.add(cookie.getName(), cookie));
@@ -482,7 +507,7 @@ private Charset getCharset() {
482507
public MockServerHttpRequest body(Publisher<? extends DataBuffer> body) {
483508
applyCookiesIfNecessary();
484509
return new MockServerHttpRequest(this.method, this.url, this.contextPath,
485-
this.headers, this.cookies, this.remoteAddress, body);
510+
this.headers, this.cookies, this.remoteAddress, this.sslInfo, body);
486511
}
487512

488513
private void applyCookiesIfNecessary() {

spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest {
5959
@Nullable
6060
private MultiValueMap<String, HttpCookie> cookies;
6161

62+
@Nullable
63+
private SslInfo sslInfo;
64+
6265

6366
/**
6467
* Constructor with the URI and headers for the request.
@@ -152,6 +155,23 @@ public MultiValueMap<String, HttpCookie> getCookies() {
152155
*/
153156
protected abstract MultiValueMap<String, HttpCookie> initCookies();
154157

158+
@Nullable
159+
@Override
160+
public SslInfo getSslInfo() {
161+
if (this.sslInfo == null) {
162+
this.sslInfo = initSslInfo();
163+
}
164+
return this.sslInfo;
165+
}
166+
167+
/**
168+
* Obtain SSL session information from the underlying "native" request.
169+
* @return the SSL information or {@code null} if not available
170+
* @since 5.0.2
171+
*/
172+
@Nullable
173+
protected abstract SslInfo initSslInfo();
174+
155175
/**
156176
* Return the underlying server response.
157177
* <p><strong>Note:</strong> This is exposed mainly for internal framework

spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder {
5252

5353
private final MultiValueMap<String, HttpCookie> cookies;
5454

55-
@Nullable
56-
private final InetSocketAddress remoteAddress;
57-
5855
@Nullable
5956
private String uriPath;
6057

@@ -71,7 +68,6 @@ public DefaultServerHttpRequestBuilder(ServerHttpRequest original) {
7168

7269
this.uri = original.getURI();
7370
this.httpMethodValue = original.getMethodValue();
74-
this.remoteAddress = original.getRemoteAddress();
7571
this.body = original.getBody();
7672

7773
this.httpHeaders = new HttpHeaders();
@@ -135,8 +131,7 @@ public ServerHttpRequest.Builder headers(Consumer<HttpHeaders> headersConsumer)
135131
public ServerHttpRequest build() {
136132
URI uriToUse = getUriToUse();
137133
return new DefaultServerHttpRequest(uriToUse, this.contextPath, this.httpHeaders,
138-
this.httpMethodValue, this.cookies, this.remoteAddress, this.body,
139-
this.originalRequest);
134+
this.httpMethodValue, this.cookies, this.body, this.originalRequest);
140135

141136
}
142137

@@ -162,20 +157,23 @@ private static class DefaultServerHttpRequest extends AbstractServerHttpRequest
162157
@Nullable
163158
private final InetSocketAddress remoteAddress;
164159

160+
@Nullable
161+
private final SslInfo sslInfo;
162+
165163
private final Flux<DataBuffer> body;
166164

167165
private final ServerHttpRequest originalRequest;
168166

169167

170168
public DefaultServerHttpRequest(URI uri, @Nullable String contextPath,
171169
HttpHeaders headers, String methodValue, MultiValueMap<String, HttpCookie> cookies,
172-
@Nullable InetSocketAddress remoteAddress,
173170
Flux<DataBuffer> body, ServerHttpRequest originalRequest) {
174171

175172
super(uri, contextPath, headers);
176173
this.methodValue = methodValue;
177174
this.cookies = cookies;
178-
this.remoteAddress = remoteAddress;
175+
this.remoteAddress = originalRequest.getRemoteAddress();
176+
this.sslInfo = originalRequest.getSslInfo();
179177
this.body = body;
180178
this.originalRequest = originalRequest;
181179
}
@@ -197,6 +195,12 @@ public InetSocketAddress getRemoteAddress() {
197195
return this.remoteAddress;
198196
}
199197

198+
@Nullable
199+
@Override
200+
protected SslInfo initSslInfo() {
201+
return this.sslInfo;
202+
}
203+
200204
@Override
201205
public Flux<DataBuffer> getBody() {
202206
return this.body;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2002-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.http.server.reactive;
17+
18+
import java.security.cert.Certificate;
19+
import java.security.cert.X509Certificate;
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import javax.net.ssl.SSLSession;
24+
25+
import org.springframework.lang.Nullable;
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
*
30+
* @author Rossen Stoyanchev
31+
* @since 5.0
32+
*/
33+
final class DefaultSslInfo implements SslInfo {
34+
35+
@Nullable
36+
private final String sessionId;
37+
38+
private final X509Certificate[] peerCertificates;
39+
40+
41+
DefaultSslInfo(String sessionId, X509Certificate[] peerCertificates) {
42+
Assert.notNull(peerCertificates, "No SSL certificates");
43+
this.sessionId = sessionId;
44+
this.peerCertificates = peerCertificates;
45+
}
46+
47+
DefaultSslInfo(SSLSession session) {
48+
Assert.notNull(session, "SSLSession is required");
49+
this.sessionId = initSessionId(session);
50+
this.peerCertificates = initCertificates(session);
51+
}
52+
53+
@Nullable
54+
private static String initSessionId(SSLSession session) {
55+
byte [] bytes = session.getId();
56+
if (bytes == null) {
57+
return null;
58+
}
59+
StringBuilder sb = new StringBuilder();
60+
for (byte b : bytes) {
61+
String digit = Integer.toHexString(b);
62+
if (digit.length() < 2) {
63+
sb.append('0');
64+
}
65+
if (digit.length() > 2) {
66+
digit = digit.substring(digit.length() - 2);
67+
}
68+
sb.append(digit);
69+
}
70+
return sb.toString();
71+
}
72+
73+
private static X509Certificate[] initCertificates(SSLSession session) {
74+
Certificate[] certificates;
75+
try {
76+
certificates = session.getPeerCertificates();
77+
}
78+
catch (Throwable ex) {
79+
throw new IllegalStateException("Failed to get SSL certificates", ex);
80+
}
81+
List<X509Certificate> result = new ArrayList<>(certificates.length);
82+
for (Certificate certificate : certificates) {
83+
if (certificate instanceof X509Certificate) {
84+
result.add((X509Certificate) certificate);
85+
}
86+
}
87+
return result.toArray(new X509Certificate[result.size()]);
88+
}
89+
90+
91+
@Override
92+
@Nullable
93+
public String getSessionId() {
94+
return this.sessionId;
95+
}
96+
97+
@Override
98+
public X509Certificate[] getPeerCertificates() {
99+
return this.peerCertificates;
100+
}
101+
102+
}

spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.net.InetSocketAddress;
2020
import java.net.URI;
2121
import java.net.URISyntaxException;
22+
import javax.net.ssl.SSLSession;
2223

2324
import io.netty.channel.ChannelPipeline;
2425
import io.netty.handler.codec.http.HttpHeaderNames;
@@ -31,6 +32,7 @@
3132
import org.springframework.core.io.buffer.NettyDataBufferFactory;
3233
import org.springframework.http.HttpCookie;
3334
import org.springframework.http.HttpHeaders;
35+
import org.springframework.lang.Nullable;
3436
import org.springframework.util.Assert;
3537
import org.springframework.util.LinkedMultiValueMap;
3638
import org.springframework.util.MultiValueMap;
@@ -108,6 +110,7 @@ private static HttpHeaders initHeaders(HttpServerRequest channel) {
108110
return headers;
109111
}
110112

113+
111114
@Override
112115
public String getMethodValue() {
113116
return this.request.method().name();
@@ -130,6 +133,16 @@ public InetSocketAddress getRemoteAddress() {
130133
return this.request.remoteAddress();
131134
}
132135

136+
@Nullable
137+
protected SslInfo initSslInfo() {
138+
SslHandler sslHandler = this.request.context().channel().pipeline().get(SslHandler.class);
139+
if (sslHandler != null) {
140+
SSLSession session = sslHandler.engine().getSession();
141+
return new DefaultSslInfo(session);
142+
}
143+
return null;
144+
}
145+
133146
@Override
134147
public Flux<DataBuffer> getBody() {
135148
return this.request.receive().retain().map(this.bufferFactory::wrap);

spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage
6161
@Nullable
6262
InetSocketAddress getRemoteAddress();
6363

64+
/**
65+
* Return the SSL session information if the request has been transmitted
66+
* over a secure protocol including SSL certificates, if available.
67+
* @return the session information or {@code null}
68+
* @since 5.0.2
69+
*/
70+
@Nullable
71+
SslInfo getSslInfo();
6472

6573
/**
6674
* Return a builder to mutate properties of this request by wrapping it

spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequestDecorator.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ public InetSocketAddress getRemoteAddress() {
9696
return getDelegate().getRemoteAddress();
9797
}
9898

99+
@Nullable
100+
@Override
101+
public SslInfo getSslInfo() {
102+
return getDelegate().getSslInfo();
103+
}
104+
99105
@Override
100106
public Flux<DataBuffer> getBody() {
101107
return getDelegate().getBody();

spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.net.URI;
2222
import java.net.URISyntaxException;
2323
import java.nio.charset.Charset;
24+
import java.security.cert.X509Certificate;
2425
import java.util.Enumeration;
2526
import java.util.Map;
2627
import javax.servlet.AsyncContext;
@@ -89,7 +90,6 @@ public ServletServerHttpRequest(HttpServletRequest request, AsyncContext asyncCo
8990
this.bodyPublisher.registerReadListener();
9091
}
9192

92-
9393
private static URI initUri(HttpServletRequest request) {
9494
Assert.notNull(request, "'request' must not be null");
9595
try {
@@ -172,6 +172,16 @@ public InetSocketAddress getRemoteAddress() {
172172
return new InetSocketAddress(this.request.getRemoteHost(), this.request.getRemotePort());
173173
}
174174

175+
@Nullable
176+
protected SslInfo initSslInfo() {
177+
if (!this.request.isSecure()) {
178+
return null;
179+
}
180+
return new DefaultSslInfo(
181+
(String) request.getAttribute("javax.servlet.request.ssl_session_id"),
182+
(X509Certificate[]) request.getAttribute("java.security.cert.X509Certificate"));
183+
}
184+
175185
@Override
176186
public Flux<DataBuffer> getBody() {
177187
return Flux.from(this.bodyPublisher);

0 commit comments

Comments
 (0)