Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reloadable server certificates backport to 1.x #3163

Merged
merged 2 commits into from
Jun 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2017, 2021 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,6 +22,7 @@
import io.helidon.common.http.ContextualRegistry;
import io.helidon.webserver.ServerConfiguration;
import io.helidon.webserver.WebServer;
import io.helidon.webserver.WebServerTls;

/**
* Kind of WebServer mock for tests.
Expand Down Expand Up @@ -72,4 +73,14 @@ public ContextualRegistry context() {
public int port(String name) {
return 0;
}

@Override
public void updateTls(WebServerTls tls) {

}

@Override
public void updateTls(WebServerTls tls, String socketName) {

}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2020 Oracle and/or its affiliates.
* Copyright (c) 2017, 2021 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.
Expand All @@ -17,15 +17,20 @@
package io.helidon.webserver;


import java.security.Principal;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Logger;

import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLPeerUnverifiedException;

import io.helidon.common.http.DataChunk;
import io.helidon.webserver.HelidonConnectionHandler.HelidonHttp2ConnectionHandlerBuilder;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
Expand All @@ -41,18 +46,21 @@
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.AsciiString;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.Future;

/**
* The HttpInitializer.
*/
class HttpInitializer extends ChannelInitializer<SocketChannel> {
private static final Logger LOGGER = Logger.getLogger(HttpInitializer.class.getName());
static final AttributeKey<String> CLIENT_CERTIFICATE_NAME = AttributeKey.valueOf("client_certificate_name");

private final SslContext sslContext;
private final NettyWebServer webServer;
private final SocketConfiguration soConfig;
private final Routing routing;
private final Queue<ReferenceHoldingQueue<DataChunk>> queues = new ConcurrentLinkedQueue<>();
private volatile SslContext sslContext;

HttpInitializer(SocketConfiguration soConfig,
SslContext sslContext,
Expand All @@ -64,6 +72,10 @@ class HttpInitializer extends ChannelInitializer<SocketChannel> {
this.webServer = webServer;
}

SocketConfiguration socketConfiguration() {
return soConfig;
}

private void clearQueues() {
queues.removeIf(ReferenceHoldingQueue::release);
}
Expand All @@ -75,15 +87,24 @@ void queuesShutdown() {
});
}

void updateSslContext(SslContext context) {
if (sslContext == null) {
throw new IllegalStateException("Current TLS context is not set, update not allowed");
}
sslContext = context;
}

@Override
public void initChannel(SocketChannel ch) {
final ChannelPipeline p = ch.pipeline();

SSLEngine sslEngine = null;
if (sslContext != null) {
SslHandler sslHandler = sslContext.newHandler(ch.alloc());
SslContext context = sslContext;
if (context != null) {
SslHandler sslHandler = context.newHandler(ch.alloc());
sslEngine = sslHandler.engine();
p.addLast(sslHandler);
sslHandler.handshakeFuture().addListener(future -> obtainClientCN(future, ch, sslHandler));
}

// Set up HTTP/2 pipeline if feature is enabled
Expand Down Expand Up @@ -125,6 +146,40 @@ public void initChannel(SocketChannel ch) {
ch.eventLoop().execute(this::clearQueues);
}

/**
* Sets {@code CERTIFICATE_NAME} in socket channel.
*
* @param future future passed to listener
* @param ch the socket channel
* @param sslHandler the SSL handler
*/
private void obtainClientCN(Future<? super Channel> future, SocketChannel ch, SslHandler sslHandler) {
if (future.cause() == null) {
try {
Certificate[] peerCertificates = sslHandler.engine().getSession().getPeerCertificates();
if (peerCertificates.length >= 1) {
Certificate certificate = peerCertificates[0];
X509Certificate cert = (X509Certificate) certificate;
Principal principal = cert.getSubjectDN();

int start = principal.getName().indexOf("CN=");
String tmpName = "Unknown CN";
if (start >= 0) {
tmpName = principal.getName().substring(start + 3);
int end = tmpName.indexOf(",");
if (end > 0) {
tmpName = tmpName.substring(0, end);
}
}
ch.attr(CLIENT_CERTIFICATE_NAME).set(tmpName);
}
} catch (SSLPeerUnverifiedException ignored) {
//User not authenticated. Client authentication probably set to OPTIONAL or NONE
}

}
}

private static final class HelidonEventLogger extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
Expand All @@ -34,6 +35,8 @@
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ssl.SSLContext;

import io.helidon.common.Version;
import io.helidon.common.context.Context;
import io.helidon.common.http.ContextualRegistry;
Expand All @@ -51,6 +54,7 @@
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.IdentityCipherSuiteFilter;
import io.netty.handler.ssl.JdkSslContext;
import io.netty.handler.ssl.SslContext;
import io.netty.util.concurrent.Future;

/**
Expand All @@ -74,7 +78,7 @@ class NettyWebServer implements WebServer {
private final CompletableFuture<WebServer> threadGroupsShutdownFuture = new CompletableFuture<>();
private final ContextualRegistry contextualRegistry;
private final ConcurrentMap<String, Channel> channels = new ConcurrentHashMap<>();
private final List<HttpInitializer> initializers = new LinkedList<>();
private final Map<String, HttpInitializer> initializers = new LinkedHashMap<>();

private volatile boolean started;
private final AtomicBoolean shutdownThreadGroupsInitiated = new AtomicBoolean(false);
Expand Down Expand Up @@ -110,35 +114,8 @@ class NettyWebServer implements WebServer {
String name = entry.getKey();
SocketConfiguration soConfig = entry.getValue();
ServerBootstrap bootstrap = new ServerBootstrap();
// Transform java SSLContext into Netty SslContext
JdkSslContext sslContext = null;
if (soConfig.ssl() != null) {
// TODO configuration support for CLIENT AUTH (btw, ClientAuth.REQUIRE doesn't seem to work with curl nor with
// Chrome)
String[] protocols;
if (soConfig.enabledSslProtocols().isEmpty()) {
protocols = null;
} else {
protocols = soConfig.enabledSslProtocols().toArray(new String[0]);
}

// Enable ALPN for application protocol negotiation with HTTP/2
// Needs JDK >= 9 or Jetty’s ALPN boot library
ApplicationProtocolConfig appProtocolConfig = null;
if (configuration.isHttp2Enabled()) {
appProtocolConfig = new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN,
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1);
}

sslContext = new JdkSslContext(
soConfig.ssl(), false, null,
IdentityCipherSuiteFilter.INSTANCE, appProtocolConfig,
soConfig.clientAuth().nettyClientAuth(), protocols, false);
}
SslContext sslContext = createSslContext(soConfig.ssl(), soConfig.enabledSslProtocols(), soConfig.clientAuth());

if (soConfig.backlog() > 0) {
bootstrap.option(ChannelOption.SO_BACKLOG, soConfig.backlog());
Expand All @@ -154,7 +131,7 @@ class NettyWebServer implements WebServer {
sslContext,
namedRoutings.getOrDefault(name, routing),
this);
initializers.add(childHandler);
initializers.put(name, childHandler);
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.DEBUG))
Expand All @@ -164,6 +141,36 @@ class NettyWebServer implements WebServer {
}
}

private SslContext createSslContext(SSLContext context, Set<String> enabledProtocols, ClientAuthentication clientAuth) {
// Transform java SSLContext into Netty SslContext
if (context != null) {
String[] protocols;
if (enabledProtocols.isEmpty()) {
protocols = null;
} else {
protocols = enabledProtocols.toArray(new String[0]);
}

// Enable ALPN for application protocol negotiation with HTTP/2
// Needs JDK >= 9 or Jetty’s ALPN boot library
ApplicationProtocolConfig appProtocolConfig = null;
if (configuration.isHttp2Enabled()) {
appProtocolConfig = new ApplicationProtocolConfig(
ApplicationProtocolConfig.Protocol.ALPN,
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1);
}

return new JdkSslContext(
context, false, null,
IdentityCipherSuiteFilter.INSTANCE, appProtocolConfig,
clientAuth.nettyClientAuth(), protocols, false);
}
return null;
}

@Override
public ServerConfiguration configuration() {
return configuration;
Expand Down Expand Up @@ -339,7 +346,7 @@ private CompletionStage<WebServer> shutdownThreadGroups() {
}

private void forceQueuesRelease() {
initializers.removeIf(httpInitializer -> {
initializers.values().removeIf(httpInitializer -> {
httpInitializer.queuesShutdown();
return true;
});
Expand Down Expand Up @@ -383,4 +390,27 @@ public int port(String name) {
SocketAddress address = channel.localAddress();
return address instanceof InetSocketAddress ? ((InetSocketAddress) address).getPort() : -1;
}

@Override
public void updateTls(WebServerTls tls) {
updateTls(tls, ServerConfiguration.DEFAULT_SOCKET_NAME);
}

@Override
public void updateTls(WebServerTls tls, String socketName) {
Objects.requireNonNull(tls, "Tls could not be updated. WebServerTls is required to be non-null");
HttpInitializer httpInitializer = initializers.get(socketName);
if (httpInitializer == null) {
throw new IllegalStateException("Unknown socket name: " + socketName);
} else {
if (tls.sslContext() == null) {
throw new IllegalStateException("Tls could not be updated. SSLContext is required to be set");
}
SslContext context = createSslContext(tls.sslContext(),
new HashSet<>(httpInitializer.socketConfiguration().enabledSslProtocols()),
tls.clientAuth());
httpInitializer.updateSslContext(context);
}
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2017, 2021 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.
Expand Down Expand Up @@ -103,6 +103,26 @@ default int port() {
*/
int port(String socketName);

/**
* Update the TLS configuration of the default socket {@link ServerConfiguration#DEFAULT_SOCKET_NAME}.
*
* @param tls new TLS configuration
* @throws IllegalStateException if {@link WebServerTls#sslContext()} returns {@code null} or
* if {@link SocketConfiguration#ssl()} returns {@code null}
*/
void updateTls(WebServerTls tls);

/**
* Update the TLS configuration of the named socket.
*
* @param tls new TLS configuration
* @param socketName specific named socket name
* @throws IllegalStateException if {@link WebServerTls#sslContext()} returns {@code null} or
* if {@link SocketConfiguration#ssl()} returns {@code null}
*/
void updateTls(WebServerTls tls, String socketName);


/**
* Creates a new instance from a provided configuration and a routing.
*
Expand Down
Loading