import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import io.netty.bootstrap.Bootstrap;
-import io.netty.channel.ChannelHandler;
-import io.netty.channel.ChannelHandlerContext;
-import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
-import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpClientCodec;
-import io.netty.handler.codec.http.HttpClientUpgradeHandler;
-import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObjectAggregator;
-import io.netty.handler.codec.http.HttpVersion;
-import io.netty.handler.codec.http2.Http2ClientUpgradeCodec;
import io.netty.handler.codec.http2.Http2ConnectionHandler;
-import io.netty.handler.ssl.ApplicationProtocolNames;
-import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
-import io.netty.handler.ssl.SslHandler;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.netconf.transport.api.TransportChannel;
import org.opendaylight.netconf.transport.api.TransportChannelListener;
+import org.opendaylight.netconf.transport.api.TransportStack;
import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
import org.opendaylight.netconf.transport.tcp.TCPClient;
import org.opendaylight.netconf.transport.tls.TLSClient;
/**
* A {@link HTTPTransportStack} acting as a client.
*/
-public final class HTTPClient extends HTTPTransportStack {
+public abstract sealed class HTTPClient extends HTTPTransportStack permits PlainHTTPClient, TlsHTTPClient {
private final ClientRequestDispatcher dispatcher;
private final ClientAuthProvider authProvider;
private final boolean http2;
- private HTTPClient(final TransportChannelListener listener, final ClientAuthProvider authProvider,
- final boolean http2) {
+ HTTPClient(final TransportChannelListener listener, final ClientAuthProvider authProvider, final boolean http2) {
super(listener);
this.authProvider = authProvider;
this.http2 = http2;
default -> throw new UnsupportedConfigurationException("Unsupported transport: " + transport);
}
- final var client = new HTTPClient(listener, ClientAuthProvider.ofNullable(httpParams), http2);
- final var underlay = tlsParams == null
- ? TCPClient.connect(client.asListener(), bootstrap, tcpParams)
- : TLSClient.connect(client.asListener(), bootstrap, tcpParams, new HttpSslHandlerFactory(tlsParams, http2));
+ final HTTPClient client;
+ final ListenableFuture<? extends TransportStack> underlay;
+ if (tlsParams != null) {
+ client = new TlsHTTPClient(listener, ClientAuthProvider.ofNullable(httpParams), http2);
+ underlay = TLSClient.connect(client.asListener(), bootstrap, tcpParams,
+ new HttpSslHandlerFactory(tlsParams, http2));
+ } else {
+ client = new PlainHTTPClient(listener, ClientAuthProvider.ofNullable(httpParams), http2);
+ underlay = TCPClient.connect(client.asListener(), bootstrap, tcpParams);
+ }
return transformUnderlay(client, underlay);
}
@Override
protected void onUnderlayChannelEstablished(final TransportChannel underlayChannel) {
final var pipeline = underlayChannel.channel().pipeline();
- final boolean ssl = pipeline.get(SslHandler.class) != null;
-
if (http2) {
// External HTTP 2 to internal HTTP 1.1 adapter handler
- final var connectionHandler = Http2Utils.connectionHandler(false, MAX_HTTP_CONTENT_LENGTH);
- if (ssl) {
- // Application protocol negotiator over TLS
- pipeline.addLast(apnHandler(underlayChannel, connectionHandler));
- } else {
- // Cleartext upgrade flow
- final var sourceCodec = new HttpClientCodec();
- final var upgradeHandler = new HttpClientUpgradeHandler(sourceCodec,
- new Http2ClientUpgradeCodec(connectionHandler), MAX_HTTP_CONTENT_LENGTH);
- pipeline.addLast(sourceCodec, upgradeHandler, upgradeRequestHandler(underlayChannel));
- }
-
+ initializePipeline(underlayChannel, pipeline, Http2Utils.connectionHandler(false, MAX_HTTP_CONTENT_LENGTH));
} else {
// HTTP 1.1
pipeline.addLast(new HttpClientCodec(), new HttpObjectAggregator(MAX_HTTP_CONTENT_LENGTH));
}
}
- private void configureEndOfPipeline(final TransportChannel underlayChannel, final ChannelPipeline pipeline) {
+ final void configureEndOfPipeline(final TransportChannel underlayChannel, final ChannelPipeline pipeline) {
if (authProvider != null) {
pipeline.addLast(authProvider);
}
addTransportChannel(new HTTPTransportChannel(underlayChannel));
}
- private ApplicationProtocolNegotiationHandler apnHandler(final TransportChannel underlayChannel,
- final Http2ConnectionHandler connectionHandler) {
- return new ApplicationProtocolNegotiationHandler("") {
- @Override
- protected void configurePipeline(final ChannelHandlerContext ctx, final String protocol) {
- if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
- final var pipeline = ctx.pipeline();
- pipeline.addLast(connectionHandler);
- configureEndOfPipeline(underlayChannel, pipeline);
- return;
- }
- ctx.close();
- throw new IllegalStateException("unknown protocol: " + protocol);
- }
- };
- }
-
- private ChannelHandler upgradeRequestHandler(final TransportChannel underlayChannel) {
- return new ChannelInboundHandlerAdapter() {
- @Override
- public void channelActive(final ChannelHandlerContext ctx) throws Exception {
- // Trigger upgrade with an OPTIONS request targetting the server itself, as per
- // https://www.rfc-editor.org/rfc/rfc7231#section-4.3.7
- // required headers and flow will be handled by HttpClientUpgradeHandler
- ctx.writeAndFlush(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "*"));
- ctx.fireChannelActive();
- }
-
- @Override
- public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) {
- // process upgrade result
- if (evt == HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_SUCCESSFUL) {
- final var pipeline = ctx.pipeline();
- configureEndOfPipeline(underlayChannel, pipeline);
- pipeline.remove(this);
- } else if (evt == HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_REJECTED) {
- notifyTransportChannelFailed(new IllegalStateException("Server rejected HTTP/2 upgrade request"));
- }
- }
- };
- }
+ abstract void initializePipeline(TransportChannel underlayChannel, ChannelPipeline pipeline,
+ Http2ConnectionHandler connectionHandler);
}
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.http;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.channel.ChannelPipeline;
+import io.netty.handler.codec.http.DefaultFullHttpRequest;
+import io.netty.handler.codec.http.HttpClientCodec;
+import io.netty.handler.codec.http.HttpClientUpgradeHandler;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http2.Http2ClientUpgradeCodec;
+import io.netty.handler.codec.http2.Http2ConnectionHandler;
+import org.opendaylight.netconf.transport.api.TransportChannel;
+import org.opendaylight.netconf.transport.api.TransportChannelListener;
+
+/**
+ * An {@link HTTPClient} operating over plain TCP.
+ */
+final class PlainHTTPClient extends HTTPClient {
+ PlainHTTPClient(final TransportChannelListener listener, final ClientAuthProvider authProvider,
+ final boolean http2) {
+ super(listener, authProvider, http2);
+ }
+
+ @Override
+ void initializePipeline(final TransportChannel underlayChannel, final ChannelPipeline pipeline,
+ final Http2ConnectionHandler connectionHandler) {
+ // Cleartext upgrade flow
+ final var sourceCodec = new HttpClientCodec();
+ final var upgradeHandler = new HttpClientUpgradeHandler(sourceCodec,
+ new Http2ClientUpgradeCodec(connectionHandler), MAX_HTTP_CONTENT_LENGTH);
+ pipeline.addLast(sourceCodec, upgradeHandler, new ChannelInboundHandlerAdapter() {
+ @Override
+ public void channelActive(final ChannelHandlerContext ctx) throws Exception {
+ // Trigger upgrade with an OPTIONS request targetting the server itself, as per
+ // https://www.rfc-editor.org/rfc/rfc7231#section-4.3.7
+ // required headers and flow will be handled by HttpClientUpgradeHandler
+ ctx.writeAndFlush(new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.OPTIONS, "*"));
+ ctx.fireChannelActive();
+ }
+
+ @Override
+ public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) {
+ // process upgrade result
+ if (evt == HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_SUCCESSFUL) {
+ final var pipeline = ctx.pipeline();
+ configureEndOfPipeline(underlayChannel, pipeline);
+ pipeline.remove(this);
+ } else if (evt == HttpClientUpgradeHandler.UpgradeEvent.UPGRADE_REJECTED) {
+ notifyTransportChannelFailed(new IllegalStateException("Server rejected HTTP/2 upgrade request"));
+ }
+ }
+ });
+ }
+}
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.transport.http;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPipeline;
+import io.netty.handler.codec.http2.Http2ConnectionHandler;
+import io.netty.handler.ssl.ApplicationProtocolNames;
+import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
+import org.opendaylight.netconf.transport.api.TransportChannel;
+import org.opendaylight.netconf.transport.api.TransportChannelListener;
+
+/**
+ * An {@link HTTPClient} operating over TLS.
+ */
+final class TlsHTTPClient extends HTTPClient {
+ TlsHTTPClient(final TransportChannelListener listener, final ClientAuthProvider authProvider, final boolean http2) {
+ super(listener, authProvider, http2);
+ }
+
+ @Override
+ void initializePipeline(final TransportChannel underlayChannel, final ChannelPipeline pipeline,
+ final Http2ConnectionHandler connectionHandler) {
+ // Application protocol negotiator over TLS
+ pipeline.addLast(new ApplicationProtocolNegotiationHandler("") {
+ @Override
+ protected void configurePipeline(final ChannelHandlerContext ctx, final String protocol) {
+ if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
+ final var pipeline = ctx.pipeline();
+ pipeline.addLast(connectionHandler);
+ configureEndOfPipeline(underlayChannel, pipeline);
+ return;
+ }
+ ctx.close();
+ throw new IllegalStateException("unknown protocol: " + protocol);
+ }
+ });
+ }
+}