Specialize HTTPClient 84/113684/4
authorRobert Varga <robert.varga@pantheon.tech>
Wed, 25 Sep 2024 19:13:36 +0000 (21:13 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Thu, 26 Sep 2024 11:50:36 +0000 (13:50 +0200)
We know statically whether a particular client is TLS-enabled, so use
two subclasses, each specialized for the task.

JIRA: NETCONF-1379
Change-Id: Ib46a3c779dd2f3bdf060561a754180f099fdc997
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/HTTPClient.java
transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/PlainHTTPClient.java [new file with mode: 0644]
transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/TlsHTTPClient.java [new file with mode: 0644]

index 5a6565bea6829b167b446a8c920371aae8eba009..3ec654932149e4dcd9d582af9a8c7ad90f4bd4ca 100644 (file)
@@ -12,26 +12,16 @@ import static java.util.Objects.requireNonNull;
 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;
@@ -45,13 +35,12 @@ import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.client.
 /**
  * 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;
@@ -103,32 +92,25 @@ public final class HTTPClient extends HTTPTransportStack {
             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));
@@ -136,7 +118,7 @@ public final class HTTPClient extends HTTPTransportStack {
         }
     }
 
-    private void configureEndOfPipeline(final TransportChannel underlayChannel, final ChannelPipeline pipeline) {
+    final void configureEndOfPipeline(final TransportChannel underlayChannel, final ChannelPipeline pipeline) {
         if (authProvider != null) {
             pipeline.addLast(authProvider);
         }
@@ -148,45 +130,6 @@ public final class HTTPClient extends HTTPTransportStack {
         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);
 }
diff --git a/transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/PlainHTTPClient.java b/transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/PlainHTTPClient.java
new file mode 100644 (file)
index 0000000..1144ff2
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * 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"));
+                }
+            }
+        });
+    }
+}
diff --git a/transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/TlsHTTPClient.java b/transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/TlsHTTPClient.java
new file mode 100644 (file)
index 0000000..767ca9a
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * 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);
+            }
+        });
+    }
+}