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

HTTP/2 - Enable H2 protocol #490

Merged
merged 7 commits into from
Jan 15, 2025
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
Expand Up @@ -27,9 +27,7 @@
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import lombok.Data;
import org.carapaceproxy.core.EndpointKey;
import org.carapaceproxy.core.HttpProxyServer;
import org.carapaceproxy.server.config.NetworkListenerConfiguration;

/**
* Access to listeners
Expand Down Expand Up @@ -58,23 +56,22 @@ public static final class ListenerBean {

@GET
public Map<String, ListenerBean> getAllListeners() {
HttpProxyServer server = (HttpProxyServer) context.getAttribute("server");

return server.getListeners().getListeningChannels().entrySet().stream().map(listener -> {
NetworkListenerConfiguration config = listener.getValue().getConfig();
int port = listener.getKey().port();
ListenerBean bean = new ListenerBean(
config.getHost(),
port,
config.isSsl(),
config.getSslCiphers(),
config.getSslProtocols(),
config.getDefaultCertificate(),
(int) listener.getValue().getTotalRequests().get()
);
EndpointKey endpointKey = EndpointKey.make(config.getHost(), port);
return Map.entry(endpointKey.toString(), bean);
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
final HttpProxyServer server = (HttpProxyServer) context.getAttribute("server");
return server.getListeners()
.getListeningChannels()
.values()
.stream()
.collect(Collectors.toMap(
channel -> channel.getHostPort().toString(),
channel -> new ListenerBean(
channel.getHostPort().host(),
channel.getHostPort().port(),
channel.getConfig().ssl(),
channel.getConfig().sslCiphers(),
channel.getConfig().sslProtocols(),
channel.getConfig().defaultCertificate(),
channel.getTotalRequests()
)));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
*/
package org.carapaceproxy.configstore;

import static org.carapaceproxy.utils.CertificatesUtils.createKeystore;
import static org.carapaceproxy.utils.CertificatesUtils.readChainFromKeystore;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
Expand All @@ -28,8 +30,6 @@
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import static org.carapaceproxy.utils.CertificatesUtils.createKeystore;
import static org.carapaceproxy.utils.CertificatesUtils.readChainFromKeystore;

public final class ConfigurationStoreUtils {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,8 @@ public static EndpointKey make(String hostAndPort) {
public String toString() {
return host + ":" + port;
}

public EndpointKey offsetPort(final int offsetPort) {
return make(host(), port() + offsetPort);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ public static HttpProxyServer buildForTests(
) throws ConfigurationNotValidException {
final HttpProxyServer server = new HttpProxyServer(mapperFactory, baseDir.getAbsoluteFile());
final EndpointMapper mapper = server.getMapper();
server.currentConfiguration.addListener(new NetworkListenerConfiguration(host, port));
server.currentConfiguration.addListener(NetworkListenerConfiguration.withDefault(host, port));
server.proxyRequestsManager.reloadConfiguration(server.currentConfiguration, mapper.getBackends().values());
return server;
}
Expand Down
395 changes: 96 additions & 299 deletions carapace-server/src/main/java/org/carapaceproxy/core/Listeners.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package org.carapaceproxy.core;

import static org.carapaceproxy.utils.CertificatesUtils.loadKeyStoreData;
import static org.carapaceproxy.utils.CertificatesUtils.loadKeyStoreFromFile;
import static org.carapaceproxy.utils.CertificatesUtils.readChainFromKeystore;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.OpenSslCachingX509KeyManagerFactory;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import io.prometheus.client.Counter;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import javax.net.ssl.KeyManagerFactory;
import org.carapaceproxy.server.config.ConfigurationNotValidException;
import org.carapaceproxy.server.config.NetworkListenerConfiguration;
import org.carapaceproxy.server.config.SSLCertificateConfiguration;
import org.carapaceproxy.utils.PrometheusUtils;
import org.carapaceproxy.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.netty.DisposableServer;
import reactor.netty.FutureMono;

public class ListeningChannel {

private static final Logger LOG = LoggerFactory.getLogger(ListeningChannel.class);

private static final Counter TOTAL_REQUESTS_PER_LISTENER_COUNTER = PrometheusUtils.createCounter(
"listeners", "requests_total", "total requests", "listener"
).register();

private final int localPort;
private final NetworkListenerConfiguration config;
private final Counter.Child totalRequests;
private final Map<String, SslContext> sslContexts;
private final File basePath;
private final RuntimeServerConfiguration currentConfiguration;
private final HttpProxyServer parent;
private DisposableServer channel;

public ListeningChannel(
final File basePath,
final RuntimeServerConfiguration currentConfiguration,
final HttpProxyServer parent,
final ConcurrentMap<String, SslContext> cachedSslContexts,
final NetworkListenerConfiguration config
) throws ConfigurationNotValidException {
this.localPort = config.port() + parent.getListenersOffsetPort();
this.config = config;
this.totalRequests = TOTAL_REQUESTS_PER_LISTENER_COUNTER.labels(config.host() + "_" + this.localPort);
this.basePath = basePath;
this.currentConfiguration = currentConfiguration;
this.parent = parent;
this.sslContexts = new HashMap<>(currentConfiguration.getCertificates().size());
for (final SSLCertificateConfiguration certificate : currentConfiguration.getCertificates().values()) {
final String certificateId = certificate.getId();
final SslContext sslContext = cachedSslContexts.containsKey(certificateId)
? cachedSslContexts.get(certificateId)
: bootSslContext(config, certificate);
if (sslContext == null) {
// certificate configuration has some problem, should fallback to default certificate (legacy behavior)
continue;
}
sslContexts.put(certificateId, sslContext);
}
}

private static KeyManagerFactory loadKeyFactory(final SSLCertificateConfiguration certificate, final KeyStore keystore) throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException {
final KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
final KeyManagerFactory wrapperFactory = new OpenSslCachingX509KeyManagerFactory(keyFactory);
wrapperFactory.init(keystore, certificate.getPassword().toCharArray());
LOG.debug("Initialized KeyManagerFactory with algorithm: {}", wrapperFactory.getAlgorithm());
return wrapperFactory;
}

public void disposeChannel() {
this.channel.disposeNow(Duration.ofSeconds(10));
FutureMono.from(this.config.group().close()).block(Duration.ofSeconds(10));
}

public void incRequests() {
totalRequests.inc();
}

public void clear() {
this.sslContexts.clear();
}

private SslContext bootSslContext(final NetworkListenerConfiguration listener, final SSLCertificateConfiguration certificate) throws ConfigurationNotValidException {
try {
final EndpointKey hostPort = new EndpointKey(listener.host(), listener.port()).offsetPort(parent.getListenersOffsetPort());
final KeyStore keystore = loadKeyStore(certificate, hostPort);
if (keystore == null) {
// certificate configuration has some problem, should fallback to default certificate (legacy behavior)
return null;
}
final KeyManagerFactory keyFactory = loadKeyFactory(certificate, keystore);
final SslContextBuilder sslContextBuilder = SslContextBuilder
.forServer(keyFactory)
.enableOcsp(currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported())
.trustManager(parent.getTrustStoreManager().getTrustManagerFactory())
.sslProvider(SslProvider.OPENSSL)
.protocols(listener.sslProtocols());
final String sslCiphers = listener.sslCiphers();
if (sslCiphers != null && !sslCiphers.isEmpty()) {
LOG.debug("required sslCiphers {}", sslCiphers);
final List<String> ciphers = Arrays.asList(sslCiphers.split(","));
sslContextBuilder.ciphers(ciphers);
}
final SslContext sslContext = sslContextBuilder.build();
final Certificate[] chain = readChainFromKeystore(keystore);
if (currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported() && chain.length > 0) {
parent.getOcspStaplingManager().addCertificateForStapling(chain);
Attribute<Object> attr = sslContext.attributes().attr(AttributeKey.valueOf(Listeners.OCSP_CERTIFICATE_CHAIN));
attr.set(chain[0]);
}
return sslContext;
} catch (IOException | GeneralSecurityException err) {
LOG.error("ERROR booting listener", err);
throw new ConfigurationNotValidException(err);
}
}

private KeyStore loadKeyStore(final SSLCertificateConfiguration certificate, final EndpointKey hostPort) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
try {
// Try to find certificate data on db
final byte[] keystoreContent = parent.getDynamicCertificatesManager().getCertificateForDomain(certificate.getId());
final KeyStore keystore;
if (keystoreContent == null) {
if (StringUtils.isBlank(certificate.getFile())) {
LOG.warn("No certificate file or dynamic certificate data for certificate id {}", certificate.getId());
return null;
}
LOG.debug("Start SSL with certificate id {}, on listener {}:{} file={}", certificate.getId(), hostPort.host(), hostPort.port(), certificate.getFile());
keystore = loadKeyStoreFromFile(certificate.getFile(), certificate.getPassword(), basePath);
} else {
LOG.debug("Start SSL with dynamic certificate id {}, on listener {}:{}", certificate.getId(), hostPort.host(), hostPort.port());
keystore = loadKeyStoreData(keystoreContent, certificate.getPassword());
}
LOG.debug("Loaded keystore with type: {}, size: {}, aliases: {}", keystore.getType(), keystore.size(), Collections.list(keystore.aliases()));
return keystore;
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
LOG.error(
"ERROR loading keystore for certificate {id {}, hostname {}, file {}, mode {}}",
certificate.getId(),
certificate.getHostname(),
certificate.getFile(),
certificate.getMode()
);
throw e;
}
}

public NetworkListenerConfiguration getConfig() {
return this.config;
}

public int getTotalRequests() {
return (int) this.totalRequests.get();
}

public void setChannel(DisposableServer channel) {
this.channel = channel;
}

public EndpointKey getHostPort() {
return new EndpointKey(this.config.host(), this.localPort);
}

public int getChannelPort() {
if (this.channel != null) {
if (this.channel.address() instanceof InetSocketAddress address) {
return address.getPort();
}
LOG.warn("Unexpected channel address {}", this.channel.address());
}
return -1;
}

public SslContext getDefaultSslContext() {
return this.sslContexts.get(config.defaultCertificate());
}

public reactor.netty.tcp.SslProvider.Builder apply(reactor.netty.tcp.SslProvider.Builder parentBuilder) {
for (final Map.Entry<String, SslContext> entry : this.sslContexts.entrySet()) {
if ("*".equals(entry.getKey())) {
continue;
}
final String key = entry.getKey();
final SslContext sslContext = entry.getValue();
parentBuilder.addSniMapping(key, spec -> {
final reactor.netty.tcp.SslProvider.Builder builder = spec.sslContext(sslContext);
if (isOcspEnabled()) {
builder.handlerConfigurator(new OcspSslHandler(sslContext, parent.getOcspStaplingManager()));
}
});
}
return parentBuilder;
}

public boolean isOcspEnabled() {
return currentConfiguration.isOcspEnabled() && OpenSsl.isOcspSupported();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.carapaceproxy.core;

import io.netty.handler.ssl.ReferenceCountedOpenSslEngine;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.AttributeKey;
import java.io.IOException;
import java.security.cert.Certificate;
import java.util.function.Consumer;
import org.carapaceproxy.server.certificates.ocsp.OcspStaplingManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OcspSslHandler implements Consumer<SslHandler> {
private static final Logger LOG = LoggerFactory.getLogger(OcspSslHandler.class);
private static final AttributeKey<Certificate> ATTRIBUTE = AttributeKey.valueOf(Listeners.OCSP_CERTIFICATE_CHAIN);

private final SslContext sslContext;
private final OcspStaplingManager ocspStaplingManager;

public OcspSslHandler(final SslContext sslContext, final OcspStaplingManager ocspStaplingManager1) {
this.sslContext = sslContext;
this.ocspStaplingManager = ocspStaplingManager1;
}

@Override
public void accept(final SslHandler sslHandler) {
final Certificate cert = sslContext.attributes().attr(ATTRIBUTE).get();
if (cert == null) {
LOG.error("Cannot set OCSP response without the certificate");
return;
}
if (!(sslHandler.engine() instanceof ReferenceCountedOpenSslEngine engine)) {
LOG.error("Unexpected SSL handler type: {}", sslHandler.engine());
return;
}
try {
final byte[] ocspResponse = ocspStaplingManager.getOcspResponseForCertificate(cert);
if (ocspResponse == null) {
LOG.error("No OCSP response for certificate: {}", cert);
return;
}
engine.setOcspResponse(ocspResponse);
} catch (IOException ex) {
LOG.error("Error setting OCSP response.", ex);
}
}
}
Loading
Loading