Skip to content

Commit

Permalink
feat(server): cleanup SSL SNI mapper
Browse files Browse the repository at this point in the history
  • Loading branch information
NiccoMlt committed Sep 2, 2024
1 parent 100dbc3 commit def579f
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 210 deletions.
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
package org.carapaceproxy.core;

import static org.carapaceproxy.core.Listeners.OCSP_CERTIFICATE_CHAIN;
import static reactor.netty.ConnectionObserver.State.CONNECTED;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelOption;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollChannelOption;
import io.netty.channel.socket.nio.NioChannelOption;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.ReferenceCountedOpenSslEngine;
import io.netty.handler.ssl.SniHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.IdleStateHandler;
import java.io.File;
import io.netty.util.AttributeKey;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.cert.Certificate;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import jdk.net.ExtendedSocketOptions;
import org.carapaceproxy.core.ssl.SslContextConfigurator;
import org.carapaceproxy.core.ssl.SniMapper;
import org.carapaceproxy.core.stats.ListenerStats;
import org.carapaceproxy.server.config.HostPort;
import org.carapaceproxy.server.config.NetworkListenerConfiguration;
Expand All @@ -37,54 +45,50 @@ public class DisposableChannelListener {

private final HttpProxyServer parent;
private final HostPort hostPort;
private final NetworkListenerConfiguration configuration;
private final RuntimeServerConfiguration runtimeConfiguration;
private final NetworkListenerConfiguration listenerConfiguration;
private final ListenerStats stats;
private final ConcurrentMap<String, SslContext> sslContextsCache;
private ListenerStats.StatCounter totalRequests;
private DisposableChannel listeningChannel;

public DisposableChannelListener(final HostPort hostPort, final HttpProxyServer parent, final NetworkListenerConfiguration configuration, final ListenerStats stats, final ConcurrentMap<String, SslContext> sslContextsCache) {
public DisposableChannelListener(final HostPort hostPort, final HttpProxyServer parent, final RuntimeServerConfiguration runtimeConfiguration, final NetworkListenerConfiguration listenerConfiguration, final ListenerStats stats, final ConcurrentMap<String, SslContext> sslContextsCache) {
this.parent = parent;
this.hostPort = hostPort;
this.configuration = configuration;
// todo I think we need to address this at some point
// this.configuration = getCurrentConfiguration().getListener(hostPort);
// requireNonNull(this.configuration, "Parent server configuration doesn't define any listener for " + hostPort);
this.runtimeConfiguration = runtimeConfiguration;
this.listenerConfiguration = listenerConfiguration;
this.sslContextsCache = sslContextsCache;
this.listeningChannel = null;
this.stats = stats;
this.totalRequests = null;
}

private RuntimeServerConfiguration getCurrentConfiguration() {
return parent.getCurrentConfiguration();
}

public NetworkListenerConfiguration getConfig() {
return configuration;
return listenerConfiguration;
}

public HostPort getHostPort() {
return hostPort;
}

private File getBasePath() {
return parent.getBasePath();
}

public void start() {
final var hostPort = this.hostPort.offsetPort(parent.getListenersOffsetPort());
final var config = this.configuration;
final var currentConfiguration = getCurrentConfiguration();
final var config = this.listenerConfiguration;
final var clients = stats.clients();
totalRequests = stats.requests(hostPort);
LOG.info("Starting listener at {} ssl:{}", hostPort, config.isSsl());
var httpServer = HttpServer.create()
.host(hostPort.host())
.port(hostPort.port())
.protocol(config.getProtocols().toArray(HttpProtocol[]::new));
final SniMapper sslProviderBuilder;
if (config.isSsl()) {
httpServer = httpServer.secure(new SslContextConfigurator(parent, config, hostPort, sslContextsCache));
sslProviderBuilder = new SniMapper(
parent, runtimeConfiguration, config, hostPort, sslContextsCache
);
httpServer = httpServer.secure(sslProviderBuilder.sslContextSpecConsumer());
} else {
sslProviderBuilder = null;
}
httpServer = httpServer
.metrics(true, Function.identity())
Expand All @@ -103,15 +107,15 @@ public void start() {
: NioChannelOption.of(ExtendedSocketOptions.TCP_KEEPCOUNT), config.getKeepAliveCount())
.maxKeepAliveRequests(config.getMaxKeepAliveRequests())
.doOnChannelInit((observer, channel, remoteAddress) -> {
final var handler = new IdleStateHandler(0, 0, currentConfiguration.getClientsIdleTimeoutSeconds());
final var handler = new IdleStateHandler(0, 0, this.runtimeConfiguration.getClientsIdleTimeoutSeconds());
channel.pipeline().addFirst("idleStateHandler", handler);
/* // todo
if (config.isSsl()) {
SniHandler sni = new SniHandler(listeningChannel) {
assert sslProviderBuilder != null;
SniHandler sni = new SniHandler(sslProviderBuilder.sslContextAsyncMapping()) {
@Override
protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocator) {
SslHandler handler = super.newSslHandler(context, allocator);
if (currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported()) {
if (runtimeConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported()) {
Certificate cert = (Certificate) context.attributes().attr(AttributeKey.valueOf(OCSP_CERTIFICATE_CHAIN)).get();
if (cert != null) {
try {
Expand All @@ -128,7 +132,7 @@ protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocato
}
};
channel.pipeline().addFirst(sni);
} */
}
})
.doOnConnection(conn -> {
clients.increment();
Expand All @@ -140,7 +144,7 @@ protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocato
UriCleanerHandler.INSTANCE.addToPipeline(connection.channel());
}
})
.httpRequestDecoder(option -> option.maxHeaderSize(currentConfiguration.getMaxHeaderSize()))
.httpRequestDecoder(option -> option.maxHeaderSize(this.runtimeConfiguration.getMaxHeaderSize()))
.handle((request, response) -> {
if (CarapaceLogger.isLoggingDebugEnabled()) {
CarapaceLogger.debug("Receive request " + request.uri()
Expand All @@ -151,11 +155,11 @@ protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocato
ProxyRequest proxyRequest = new ProxyRequest(request, response, hostPort);
return parent.getProxyRequestsManager().processRequest(proxyRequest);
});
if (currentConfiguration.getResponseCompressionThreshold() >= 0) {
if (this.runtimeConfiguration.getResponseCompressionThreshold() >= 0) {
CarapaceLogger.debug("Response compression enabled with min size = {0} bytes for listener {1}",
currentConfiguration.getResponseCompressionThreshold(), hostPort
this.runtimeConfiguration.getResponseCompressionThreshold(), hostPort
);
httpServer = httpServer.compress(currentConfiguration.getResponseCompressionThreshold());
httpServer = httpServer.compress(this.runtimeConfiguration.getResponseCompressionThreshold());
} else {
CarapaceLogger.debug("Response compression disabled for listener {0}", hostPort);
}
Expand All @@ -173,7 +177,7 @@ protected SslHandler newSslHandler(SslContext context, ByteBufAllocator allocato
public void stop() throws InterruptedException {
if (this.isStarted()) {
this.listeningChannel.disposeNow(Duration.ofSeconds(10));
FutureMono.from(this.configuration.getGroup().close()).block(Duration.ofSeconds(10));
FutureMono.from(this.listenerConfiguration.getGroup().close()).block(Duration.ofSeconds(10));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ void reloadConfiguration(final RuntimeServerConfiguration newConfiguration) thro

private void bootListener(final HostPort hostPort, final NetworkListenerConfiguration configuration) {
final var newListener = new DisposableChannelListener(
hostPort, this.parent, configuration, PrometheusListenerStats.INSTANCE, sslContexts
hostPort, parent, currentConfiguration, configuration, PrometheusListenerStats.INSTANCE, sslContexts
);
listeningChannels.put(hostPort, newListener);
newListener.start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,31 @@
*/
package org.carapaceproxy.core.ssl;

import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.*;
import java.security.*;
import io.netty.handler.ssl.SslContext;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import org.carapaceproxy.server.config.HostPort;

/**
* Utilitis for Certificates storing as Keystores
Expand All @@ -47,7 +62,13 @@ public final class CertificatesUtils {
* @param chain to store into a keystore
* @param key private key for the chain
* @return keystore data
* @throws GeneralSecurityException
* @throws KeyStoreException if no provider supports a {@code KeyStoreSpi} implementation for the specified type,
* if the keystore has not been initialized,
* the given key cannot be protected,
* or this operation fails for some other reason
* @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found
* @throws CertificateException if any of the certificates in the keystore could not be loaded
* @throws GeneralSecurityException if something else goes wrong, i.e., because of a {@link IOException}
*/
public static byte[] createKeystore(Certificate[] chain, PrivateKey key) throws GeneralSecurityException {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
Expand All @@ -66,7 +87,11 @@ public static byte[] createKeystore(Certificate[] chain, PrivateKey key) throws
*
* @param data keystore data.
* @return certificate chain contained into the keystore.
* @throws GeneralSecurityException
* @throws KeyStoreException if no provider supports a {@code KeyStoreSpi} implementation for the specified type,
* or if the keystore has not been initialized
* @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found
* @throws CertificateException if any of the certificates in the keystore could not be loaded
* @throws GeneralSecurityException if something else goes wrong, i.e., because of a {@link IOException}
*/
public static Certificate[] readChainFromKeystore(byte[] data) throws GeneralSecurityException {
try (ByteArrayInputStream is = new ByteArrayInputStream(data)) {
Expand All @@ -84,7 +109,10 @@ public static Certificate[] readChainFromKeystore(byte[] data) throws GeneralSec
*
* @param keystore keystore.
* @return certificate chain contained into the keystore.
* @throws GeneralSecurityException
* @throws KeyStoreException if no provider supports a {@code KeyStoreSpi} implementation for the specified type,
* or if the keystore has not been initialized
* @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found
* @throws CertificateException if any of the certificates in the keystore could not be loaded
*/
public static Certificate[] readChainFromKeystore(KeyStore keystore) throws GeneralSecurityException {
Iterator<String> iter = keystore.aliases().asIterator();
Expand All @@ -100,7 +128,11 @@ public static Certificate[] readChainFromKeystore(KeyStore keystore) throws Gene

/**
* @param data keystore data.
* @return whether a valid keystore can be retrived from data.
* @return whether a valid keystore can be retrieved from data.
* @throws KeyStoreException if no provider supports a {@code KeyStoreSpi} implementation for the specified type,
* or if the keystore has not been initialized
* @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found
* @throws CertificateException if any of the certificates in the keystore could not be loaded
*/
public static boolean validateKeystore(byte[] data) throws GeneralSecurityException {
try (ByteArrayInputStream is = new ByteArrayInputStream(data)) {
Expand Down Expand Up @@ -160,17 +192,20 @@ public static boolean isCertificateExpired(Date expiringDate, int daysBeforeRene

/**
* Extract certificate private key
* @param data Certificate data.
*
* @param data Certificate data.
* @param password Private key password.
* @return PrivateKey.
* @throws Exception
* @throws GeneralSecurityException if something goes wrong with the keystore
* @throws IOException if there is an I/O or format problem with the keystore data,
* if a password is required but not given,
* or if the given password was incorrect.
*/
public static PrivateKey loadPrivateKey(byte[] data, String password) throws Exception {
public static PrivateKey loadPrivateKey(byte[] data, String password) throws GeneralSecurityException, IOException {
KeyStore ks = KeyStore.getInstance(KEYSTORE_FORMAT);
ks.load(new ByteArrayInputStream(data), password.trim().toCharArray());
String alias = ks.aliases().nextElement();
PrivateKey key = (PrivateKey) ks.getKey(alias, password.trim().toCharArray());
return key;
return (PrivateKey) ks.getKey(alias, password.trim().toCharArray());
}

/**
Expand All @@ -184,6 +219,9 @@ public static boolean compareChains(Certificate[] c1, Certificate[] c2) {
if (c1 == null && c2 != null || c2 == null && c1 != null) {
return false;
}
if (c1 == null /* && c2 == null */) {
return true;
}
if (c1.length != c2.length) {
return false;
}
Expand Down Expand Up @@ -211,4 +249,15 @@ public static String addWildcard(String name) {
return WILDCARD_PREFIX + Objects.requireNonNull(name);
}

/**
* {@link SslContext}s are cached at listener level.
* This method computes the key from hostname, port, and SNI.
*
* @param hostPort
* @param sniHostname the Server Name Indication (SNI) indication
* @return the cache key
*/
public static String computeKey(final HostPort hostPort, final String sniHostname) {
return hostPort.host() + ":" + hostPort.port() + "+" + sniHostname;
}
}
Loading

0 comments on commit def579f

Please sign in to comment.