diff --git a/common/tls/src/main/java/io/helidon/common/tls/TlsUtils.java b/common/tls/src/main/java/io/helidon/common/tls/TlsUtils.java new file mode 100644 index 00000000000..dc18a41dd2e --- /dev/null +++ b/common/tls/src/main/java/io/helidon/common/tls/TlsUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * 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 io.helidon.common.tls; + +import java.security.Principal; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Optional; + +/** + * Utility class for TLS. + */ +public class TlsUtils { + + private TlsUtils() { + } + + /** + * Parse the Common Name (CN) from the first certificate if present. + * + * @param certificates certificates + * @return Common Name value + */ + public static Optional parseCn(Certificate[] certificates) { + if (certificates.length >= 1) { + Certificate certificate = certificates[0]; + X509Certificate cert = (X509Certificate) certificate; + Principal principal = cert.getSubjectX500Principal(); + int i = 0; + String[] segments = principal.getName().split("=|,"); + while (i + 1 < segments.length) { + if ("CN".equals(segments[i])) { + return Optional.of(segments[i + 1]); + } + i += 2; + } + return Optional.of("Unknown CN"); + } + return Optional.empty(); + } +} diff --git a/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/CertificateHelper.java b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/CertificateHelper.java deleted file mode 100644 index c326cff9497..00000000000 --- a/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/CertificateHelper.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * 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 io.helidon.examples.webserver.mtls; - -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -class CertificateHelper { - - private static final Pattern CN_PATTERN = Pattern.compile("(.*)CN=(.*?)(,.*)?"); - - private CertificateHelper() { - } - - static Optional clientCertificateName(String name) { - Matcher matcher = CN_PATTERN.matcher(name); - if (matcher.matches()) { - String cn = matcher.group(2); - if (!cn.isBlank()) { - return Optional.of(cn); - } - } - return Optional.empty(); - } -} diff --git a/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/SecureService.java b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/SecureService.java index 298601d103a..232186f6ba8 100644 --- a/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/SecureService.java +++ b/examples/webserver/mutual-tls/src/main/java/io/helidon/examples/webserver/mtls/SecureService.java @@ -15,25 +15,19 @@ */ package io.helidon.examples.webserver.mtls; -import java.security.Principal; - import io.helidon.http.Http; import io.helidon.webserver.http.HttpRules; import io.helidon.webserver.http.HttpService; +import static io.helidon.http.Http.HeaderNames.X_HELIDON_CN; + class SecureService implements HttpService { @Override public void routing(HttpRules rules) { rules.any((req, res) -> { - String cn = req.remotePeer() - .tlsPrincipal() - .map(Principal::getName) - .flatMap(CertificateHelper::clientCertificateName) - .orElse("Unknown CN"); - // close to avoid re-using cached connections on the client side res.header(Http.Headers.CONNECTION_CLOSE); - res.send("Hello " + cn + "!"); + res.send("Hello " + req.headers().get(X_HELIDON_CN).value() + "!"); }); } } diff --git a/http/http2/src/main/java/io/helidon/http/http2/Http2Headers.java b/http/http2/src/main/java/io/helidon/http/http2/Http2Headers.java index 40333a7bebb..2846fe648f3 100644 --- a/http/http2/src/main/java/io/helidon/http/http2/Http2Headers.java +++ b/http/http2/src/main/java/io/helidon/http/http2/Http2Headers.java @@ -106,6 +106,7 @@ private Http2Headers(Headers httpHeaders, PseudoHeaders pseudoHeaders) { * @param stream stream that owns these headers * @param table dynamic table for this connection * @param huffman huffman decoder + * @param headers http2 headers * @param frames frames of the headers * @return new headers parsed from the frames * @throws Http2Exception in case of protocol errors @@ -113,6 +114,7 @@ private Http2Headers(Headers httpHeaders, PseudoHeaders pseudoHeaders) { public static Http2Headers create(Http2Stream stream, DynamicTable table, Http2HuffmanDecoder huffman, + Http2Headers headers, Http2FrameData... frames) { if (frames.length == 0) { @@ -135,7 +137,7 @@ public static Http2Headers create(Http2Stream stream, stream.priority(priority); } - WritableHeaders headers = WritableHeaders.create(); + WritableHeaders writableHeaders = WritableHeaders.create(headers.httpHeaders()); BufferData[] buffers = new BufferData[frames.length]; for (int i = 0; i < frames.length; i++) { @@ -151,17 +153,34 @@ public static Http2Headers create(Http2Stream stream, if (padLength > 0) { data.skip(padLength); } - return create(ServerRequestHeaders.create(headers), pseudoHeaders); + return create(ServerRequestHeaders.create(writableHeaders), pseudoHeaders); } if (data.available() == 0) { throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Expecting more header bytes"); } - lastIsPseudoHeader = readHeader(headers, pseudoHeaders, table, huffman, data, lastIsPseudoHeader); + lastIsPseudoHeader = readHeader(writableHeaders, pseudoHeaders, table, huffman, data, lastIsPseudoHeader); } } + /** + * Create headers from HTTP request. + * + * @param stream stream that owns these headers + * @param table dynamic table for this connection + * @param huffman huffman decoder + * @param frames frames of the headers + * @return new headers parsed from the frames + * @throws Http2Exception in case of protocol errors + */ + public static Http2Headers create(Http2Stream stream, + DynamicTable table, + Http2HuffmanDecoder huffman, + Http2FrameData... frames) { + return create(stream, table, huffman, Http2Headers.create(WritableHeaders.create()), frames); + } + /** * Create HTTP/2 headers from HTTP headers. * diff --git a/webclient/tests/webclient/src/test/java/io/helidon/webclient/tests/CertificateHelper.java b/webclient/tests/webclient/src/test/java/io/helidon/webclient/tests/CertificateHelper.java deleted file mode 100644 index 1de62d353b9..00000000000 --- a/webclient/tests/webclient/src/test/java/io/helidon/webclient/tests/CertificateHelper.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * 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 io.helidon.webclient.tests; - -import java.util.Optional; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -class CertificateHelper { - - private static final Pattern CN_PATTERN = Pattern.compile("(.*)CN=(.*?)(,.*)?"); - - private CertificateHelper() { - } - - static Optional clientCertificateName(String name) { - Matcher matcher = CN_PATTERN.matcher(name); - if (matcher.matches()) { - String cn = matcher.group(2); - if (!cn.isBlank()) { - return Optional.of(cn); - } - } - return Optional.empty(); - } -} diff --git a/webclient/tests/webclient/src/test/java/io/helidon/webclient/tests/MutualTlsTest.java b/webclient/tests/webclient/src/test/java/io/helidon/webclient/tests/MutualTlsTest.java index c039298bcb8..42aa3fcf164 100644 --- a/webclient/tests/webclient/src/test/java/io/helidon/webclient/tests/MutualTlsTest.java +++ b/webclient/tests/webclient/src/test/java/io/helidon/webclient/tests/MutualTlsTest.java @@ -16,7 +16,6 @@ package io.helidon.webclient.tests; import java.io.UncheckedIOException; -import java.security.Principal; import java.util.concurrent.atomic.AtomicBoolean; import io.helidon.http.Http; @@ -32,6 +31,8 @@ import org.junit.jupiter.api.Test; +import static io.helidon.http.Http.HeaderNames.X_HELIDON_CN; + import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.endsWith; import static org.hamcrest.CoreMatchers.is; @@ -181,15 +182,10 @@ private static void plainRouting(HttpRouting.Builder routing) { private static void mTlsRouting(HttpRouting.Builder routing) { routing.get("/", (req, res) -> { - String cn = req.remotePeer() - .tlsPrincipal() - .map(Principal::getName) - .flatMap(CertificateHelper::clientCertificateName) - .orElse("Unknown CN"); // close to avoid re-using cached connections on the client side res.header(Http.Headers.CONNECTION_CLOSE); - res.send("Hello " + cn + "!"); + res.send("Hello " + req.headers().value(X_HELIDON_CN).orElse("Unknown CN") + "!"); }); } diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java index 3a1525a707e..c8bc9183c04 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java @@ -29,9 +29,11 @@ import io.helidon.common.buffers.BufferData; import io.helidon.common.buffers.DataReader; import io.helidon.common.task.InterruptableTask; +import io.helidon.common.tls.TlsUtils; import io.helidon.http.Http; import io.helidon.http.Http.HeaderNames; import io.helidon.http.HttpPrologue; +import io.helidon.http.WritableHeaders; import io.helidon.http.http2.ConnectionFlowControl; import io.helidon.http.http2.Http2ConnectionWriter; import io.helidon.http.http2.Http2ErrorCode; @@ -61,6 +63,7 @@ import io.helidon.webserver.http2.spi.Http2SubProtocolSelector; import io.helidon.webserver.spi.ServerConnection; +import static io.helidon.http.Http.HeaderNames.X_HELIDON_CN; import static io.helidon.http.http2.Http2Util.PREFACE_LENGTH; import static java.lang.System.Logger.Level.DEBUG; import static java.lang.System.Logger.Level.TRACE; @@ -92,6 +95,7 @@ public class Http2Connection implements ServerConnection, InterruptableTask connectionHeaders; // initial client settings, until we receive real ones private Http2Settings clientSettings = Http2Settings.builder() @@ -106,6 +110,7 @@ public class Http2Connection implements ServerConnection, InterruptableTask connectionHeaders.add(X_HELIDON_CN, cn)); + initConnectionHeaders = false; + } if (frameHeader.type() == Http2FrameType.CONTINUATION) { // end of continuations with header frames headers = Http2Headers.create(stream, requestDynamicTable, requestHuffman, + Http2Headers.create(connectionHeaders), streamContext.contData()); endOfStream = streamContext.contHeader().flags(Http2FrameTypes.HEADERS).endOfStream(); streamContext.clearContinuations(); @@ -603,6 +617,7 @@ private void doHeaders(Semaphore requestSemaphore) { headers = Http2Headers.create(stream, requestDynamicTable, requestHuffman, + Http2Headers.create(connectionHeaders), new Http2FrameData(frameHeader, inProgressFrame())); } diff --git a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java index 9ce9c65f18e..de51e64e044 100644 --- a/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java +++ b/webserver/webserver/src/main/java/io/helidon/webserver/http1/Http1Connection.java @@ -29,6 +29,7 @@ import io.helidon.common.buffers.DataWriter; import io.helidon.common.mapper.MapperException; import io.helidon.common.task.InterruptableTask; +import io.helidon.common.tls.TlsUtils; import io.helidon.http.BadRequestException; import io.helidon.http.DirectHandler; import io.helidon.http.DirectHandler.EventType; @@ -49,6 +50,7 @@ import io.helidon.webserver.http1.spi.Http1Upgrader; import io.helidon.webserver.spi.ServerConnection; +import static io.helidon.http.Http.HeaderNames.X_HELIDON_CN; import static java.lang.System.Logger.Level.TRACE; import static java.lang.System.Logger.Level.WARNING; @@ -136,6 +138,9 @@ public void handle(Semaphore requestSemaphore) throws InterruptedException { currentEntitySizeRead = 0; WritableHeaders headers = http1headers.readHeaders(prologue); + ctx.remotePeer().tlsCertificates() + .flatMap(TlsUtils::parseCn) + .ifPresent(name -> headers.set(X_HELIDON_CN, name)); recvListener.headers(ctx, headers); if (canUpgrade) {