Split up HTTPServer 29/113429/1
authorRobert Varga <robert.varga@pantheon.tech>
Wed, 4 Sep 2024 17:27:48 +0000 (19:27 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 4 Sep 2024 17:27:48 +0000 (19:27 +0200)
HTTPServer is either backed by TCP or TLS, each of which has a different
flow setup. Specialize the two so we do not need to muck around with
SslHandler.

Change-Id: I91563aad1c3c924a5cb63a47c76df8e706e11a1c
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/HTTPServer.java
transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/PlainHTTPServer.java [new file with mode: 0644]
transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/TlsHTTPServer.java [new file with mode: 0644]

index 64bb65ed0312cb3100aec497d78f95a00f8f5950..6ed984790e99105e0fa08f3db60add3b2c2af1be 100644 (file)
@@ -19,27 +19,13 @@ import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.ListenableFuture;
 import io.netty.bootstrap.ServerBootstrap;
 import io.netty.buffer.Unpooled;
-import io.netty.channel.ChannelHandler;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.channel.ChannelPipeline;
 import io.netty.channel.SimpleChannelInboundHandler;
 import io.netty.handler.codec.http.DefaultFullHttpResponse;
 import io.netty.handler.codec.http.FullHttpRequest;
 import io.netty.handler.codec.http.FullHttpResponse;
-import io.netty.handler.codec.http.HttpMessage;
-import io.netty.handler.codec.http.HttpObjectAggregator;
-import io.netty.handler.codec.http.HttpServerCodec;
-import io.netty.handler.codec.http.HttpServerKeepAliveHandler;
-import io.netty.handler.codec.http.HttpServerUpgradeHandler;
-import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler;
-import io.netty.handler.codec.http2.Http2CodecUtil;
 import io.netty.handler.codec.http2.Http2ConnectionHandler;
-import io.netty.handler.codec.http2.Http2ServerUpgradeCodec;
-import io.netty.handler.ssl.ApplicationProtocolNames;
-import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
-import io.netty.handler.ssl.SslHandler;
-import io.netty.util.AsciiString;
-import io.netty.util.ReferenceCountUtil;
 import java.nio.charset.StandardCharsets;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
@@ -48,23 +34,20 @@ import org.opendaylight.netconf.transport.api.TransportChannelListener;
 import org.opendaylight.netconf.transport.api.UnsupportedConfigurationException;
 import org.opendaylight.netconf.transport.tcp.TCPServer;
 import org.opendaylight.netconf.transport.tls.TLSServer;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.HttpServerGrouping;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.HttpServerStackGrouping;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.http.server.stack.grouping.transport.Tcp;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.http.server.rev240208.http.server.stack.grouping.transport.Tls;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tcp.server.rev240208.TcpServerGrouping;
-import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.tls.server.rev240208.TlsServerGrouping;
 
 /**
  * A {@link HTTPTransportStack} acting as a server.
  */
-public final class HTTPServer extends HTTPTransportStack {
+public abstract sealed class HTTPServer extends HTTPTransportStack permits PlainHTTPServer, TlsHTTPServer {
     static final String REQUEST_DISPATCHER_HANDLER_NAME = "request-dispatcher";
 
     private final AuthHandlerFactory authHandlerFactory;
     private final @NonNull RequestDispatcher dispatcher;
 
-    private HTTPServer(final TransportChannelListener listener, final RequestDispatcher dispatcher,
+    HTTPServer(final TransportChannelListener listener, final RequestDispatcher dispatcher,
             final AuthHandlerFactory authHandlerFactory) {
         super(listener);
         this.dispatcher = requireNonNull(dispatcher);
@@ -82,7 +65,7 @@ public final class HTTPServer extends HTTPTransportStack {
      * @throws UnsupportedConfigurationException when {@code listenParams} contains an unsupported options
      * @throws NullPointerException if any argument is {@code null}
      */
-    public static @NonNull ListenableFuture<HTTPServer> listen(final TransportChannelListener listener,
+    public static final @NonNull ListenableFuture<HTTPServer> listen(final TransportChannelListener listener,
             final ServerBootstrap bootstrap, final HttpServerStackGrouping listenParams,
             final RequestDispatcher dispatcher) throws UnsupportedConfigurationException {
         return listen(listener, bootstrap, listenParams, dispatcher, null);
@@ -101,128 +84,54 @@ public final class HTTPServer extends HTTPTransportStack {
      * @throws UnsupportedConfigurationException when {@code listenParams} contains an unsupported options
      * @throws NullPointerException if any argument is {@code null}
      */
-    public static @NonNull ListenableFuture<HTTPServer> listen(final TransportChannelListener listener,
+    public static final @NonNull ListenableFuture<HTTPServer> listen(final TransportChannelListener listener,
             final ServerBootstrap bootstrap, final HttpServerStackGrouping listenParams,
             final RequestDispatcher dispatcher, final @Nullable AuthHandlerFactory authHandlerFactory)
             throws UnsupportedConfigurationException {
-        final HttpServerGrouping httpParams;
-        final TcpServerGrouping tcpParams;
-        final TlsServerGrouping tlsParams;
         final var transport = requireNonNull(listenParams).getTransport();
-        switch (transport) {
-            case Tcp tcpCase -> {
-                final var tcp = tcpCase.getTcp();
-                httpParams = tcp.getHttpServerParameters();
-                tcpParams = tcp.nonnullTcpServerParameters();
-                tlsParams = null;
-            }
-            case Tls tlsCase -> {
-                final var tls = tlsCase.getTls();
-                httpParams = tls.getHttpServerParameters();
-                tcpParams = tls.nonnullTcpServerParameters();
-                tlsParams = tls.nonnullTlsServerParameters();
-            }
+        return switch (transport) {
+            case Tcp tcpCase -> listen(listener, bootstrap, tcpCase, dispatcher, authHandlerFactory);
+            case Tls tlsCase -> listen(listener, bootstrap, tlsCase, dispatcher, authHandlerFactory);
             default -> throw new UnsupportedConfigurationException("Unsupported transport: " + transport);
-        }
+        };
+    }
 
-        final var server = new HTTPServer(listener, dispatcher,
-            authHandlerFactory != null ? authHandlerFactory : BasicAuthHandlerFactory.ofNullable(httpParams));
-        final var underlay = tlsParams == null
-            ? TCPServer.listen(server.asListener(), bootstrap, tcpParams)
-            : TLSServer.listen(server.asListener(), bootstrap, tcpParams, new HttpSslHandlerFactory(tlsParams));
-        return transformUnderlay(server, underlay);
+    private static @NonNull ListenableFuture<HTTPServer> listen(final TransportChannelListener listener,
+            final ServerBootstrap bootstrap, final Tcp tcpCase, final RequestDispatcher dispatcher,
+            final @Nullable AuthHandlerFactory authHandlerFactory) throws UnsupportedConfigurationException {
+        final var tcp = tcpCase.getTcp();
+        final var server = new PlainHTTPServer(listener, dispatcher, authHandlerFactory != null ? authHandlerFactory
+            : BasicAuthHandlerFactory.ofNullable(tcp.getHttpServerParameters()));
+        return transformUnderlay(server,
+            TCPServer.listen(server.asListener(), bootstrap, tcp.nonnullTcpServerParameters()));
     }
 
-    @Override
-    protected void onUnderlayChannelEstablished(final TransportChannel underlayChannel) {
-        final var pipeline = underlayChannel.channel().pipeline();
-        final var ssl = pipeline.get(SslHandler.class) != null;
+    private static @NonNull ListenableFuture<HTTPServer> listen(final TransportChannelListener listener,
+            final ServerBootstrap bootstrap, final Tls tlsCase, final RequestDispatcher dispatcher,
+            final @Nullable AuthHandlerFactory authHandlerFactory) throws UnsupportedConfigurationException {
+        final var tls = tlsCase.getTls();
+        final var server = new TlsHTTPServer(listener, dispatcher, authHandlerFactory != null ? authHandlerFactory
+            : BasicAuthHandlerFactory.ofNullable(tls.getHttpServerParameters()));
+        return transformUnderlay(server,
+            TLSServer.listen(server.asListener(), bootstrap, tls.nonnullTcpServerParameters(),
+                new HttpSslHandlerFactory(tls.nonnullTlsServerParameters())));
+    }
 
+    @Override
+    protected final void onUnderlayChannelEstablished(final TransportChannel underlayChannel) {
         // External HTTP 2 to internal HTTP 1.1 adapter handler
-        final var connectionHandler = Http2Utils.connectionHandler(true, MAX_HTTP_CONTENT_LENGTH);
-        if (ssl) {
-            // Application protocol negotiator over TLS
-            pipeline.addLast(apnHandler(connectionHandler));
-        } else {
-            // Cleartext upgrade flow
-            final var sourceCodec = new HttpServerCodec();
-            final var upgradeHandler =
-                new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory(connectionHandler));
-            pipeline.addLast(new CleartextHttp2ServerUpgradeHandler(sourceCodec, upgradeHandler, connectionHandler),
-                upgradeResultHandler());
-        }
-
+        initializePipeline(underlayChannel.channel().pipeline(),
+            Http2Utils.connectionHandler(true, MAX_HTTP_CONTENT_LENGTH));
         addTransportChannel(new HTTPTransportChannel(underlayChannel));
     }
 
-    private ApplicationProtocolNegotiationHandler apnHandler(final Http2ConnectionHandler connectionHandler) {
-        return new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) {
-            @Override
-            protected void configurePipeline(final ChannelHandlerContext ctx, final String protocol) {
-                final var pipeline = ctx.pipeline();
-
-                switch (protocol) {
-                    case null -> throw new NullPointerException();
-                    case ApplicationProtocolNames.HTTP_1_1 -> {
-                        pipeline.addLast(new HttpServerCodec(),
-                            new HttpServerKeepAliveHandler(),
-                            new HttpObjectAggregator(MAX_HTTP_CONTENT_LENGTH));
-                    }
-                    case ApplicationProtocolNames.HTTP_2 -> {
-                        pipeline.addLast(connectionHandler);
-                    }
-                    default -> throw new IllegalStateException("unknown protocol: " + protocol);
-                }
-
-                configureEndOfPipeline(pipeline);
-            }
-        };
-    }
+    abstract void initializePipeline(ChannelPipeline pipeline, Http2ConnectionHandler connectionHandler);
 
-    private void configureEndOfPipeline(final ChannelPipeline pipeline) {
+    final void configureEndOfPipeline(final ChannelPipeline pipeline) {
         if (authHandlerFactory != null) {
             pipeline.addLast(authHandlerFactory.create());
         }
-        pipeline.addLast(REQUEST_DISPATCHER_HANDLER_NAME, serverHandler(dispatcher));
-    }
-
-    private ChannelHandler upgradeResultHandler() {
-        // the handler processes cleartext upgrade result
-
-        return new SimpleChannelInboundHandler<HttpMessage>() {
-            @Override
-            protected void channelRead0(final ChannelHandlerContext ctx, final HttpMessage request) throws Exception {
-                // if there was no upgrade to HTTP/2 the incoming message is accepted via channel read;
-                // configure HTTP 1.1 flow, pass the message further the pipeline, remove self as no longer required
-                final var pipeline = ctx.pipeline();
-                pipeline.addLast(new HttpServerKeepAliveHandler(), new HttpObjectAggregator(MAX_HTTP_CONTENT_LENGTH));
-                configureEndOfPipeline(pipeline);
-                ctx.fireChannelRead(ReferenceCountUtil.retain(request));
-                pipeline.remove(this);
-            }
-
-            @Override
-            public void userEventTriggered(final ChannelHandlerContext ctx, final Object event) throws Exception {
-                // if there was upgrade to HTTP/2 the upgrade event is fired further the pipeline;
-                // on event occurrence it's only required to complete the configuration for future requests,
-                // then remove self as no longer required
-                if (event instanceof HttpServerUpgradeHandler.UpgradeEvent) {
-                    final var pipeline = ctx.pipeline();
-                    configureEndOfPipeline(pipeline);
-                    pipeline.remove(this);
-                }
-            }
-        };
-    }
-
-    private static HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory(
-            final Http2ConnectionHandler connectionHandler) {
-        return protocol -> AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)
-            ? new Http2ServerUpgradeCodec(connectionHandler) : null;
-    }
-
-    private static ChannelHandler serverHandler(final RequestDispatcher dispatcher) {
-        return new SimpleChannelInboundHandler<FullHttpRequest>() {
+        pipeline.addLast(REQUEST_DISPATCHER_HANDLER_NAME, new SimpleChannelInboundHandler<FullHttpRequest>() {
             @Override
             protected void channelRead0(final ChannelHandlerContext ctx, final FullHttpRequest request) {
                 dispatcher.dispatch(request.retain(), new FutureCallback<>() {
@@ -247,6 +156,6 @@ public final class HTTPServer extends HTTPTransportStack {
                     }
                 });
             }
-        };
+        });
     }
 }
diff --git a/transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/PlainHTTPServer.java b/transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/PlainHTTPServer.java
new file mode 100644 (file)
index 0000000..5ee9277
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ * 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.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.HttpMessage;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpServerKeepAliveHandler;
+import io.netty.handler.codec.http.HttpServerUpgradeHandler;
+import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler;
+import io.netty.handler.codec.http2.Http2CodecUtil;
+import io.netty.handler.codec.http2.Http2ConnectionHandler;
+import io.netty.handler.codec.http2.Http2ServerUpgradeCodec;
+import io.netty.util.AsciiString;
+import io.netty.util.ReferenceCountUtil;
+import org.opendaylight.netconf.transport.api.TransportChannelListener;
+
+/**
+ * An {@link HTTPServer} operating over plain TCP.
+ */
+final class PlainHTTPServer extends HTTPServer {
+    PlainHTTPServer(final TransportChannelListener listener, final RequestDispatcher dispatcher,
+            final AuthHandlerFactory authHandlerFactory) {
+        super(listener, dispatcher, authHandlerFactory);
+    }
+
+    @Override
+    void initializePipeline(final ChannelPipeline pipeline, final Http2ConnectionHandler connectionHandler) {
+        // Cleartext upgrade flow
+        final var sourceCodec = new HttpServerCodec();
+        pipeline.addLast(new CleartextHttp2ServerUpgradeHandler(sourceCodec,
+            new HttpServerUpgradeHandler(sourceCodec,
+                protocol -> AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)
+                    ? new Http2ServerUpgradeCodec(connectionHandler) : null), connectionHandler),
+            upgradeResultHandler());
+    }
+
+    private ChannelHandler upgradeResultHandler() {
+        // the handler processes cleartext upgrade result
+        return new SimpleChannelInboundHandler<HttpMessage>() {
+            @Override
+            protected void channelRead0(final ChannelHandlerContext ctx, final HttpMessage request) {
+                // if there was no upgrade to HTTP/2 the incoming message is accepted via channel read;
+                // configure HTTP 1.1 flow, pass the message further the pipeline, remove self as no longer required
+                final var pipeline = ctx.pipeline();
+                pipeline.addLast(new HttpServerKeepAliveHandler(), new HttpObjectAggregator(MAX_HTTP_CONTENT_LENGTH));
+                configureEndOfPipeline(pipeline);
+                ctx.fireChannelRead(ReferenceCountUtil.retain(request));
+                pipeline.remove(this);
+            }
+
+            @Override
+            public void userEventTriggered(final ChannelHandlerContext ctx, final Object event) {
+                // if there was upgrade to HTTP/2 the upgrade event is fired further the pipeline;
+                // on event occurrence it's only required to complete the configuration for future requests,
+                // then remove self as no longer required
+                if (event instanceof HttpServerUpgradeHandler.UpgradeEvent) {
+                    final var pipeline = ctx.pipeline();
+                    configureEndOfPipeline(pipeline);
+                    pipeline.remove(this);
+                }
+            }
+        };
+    }
+}
diff --git a/transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/TlsHTTPServer.java b/transport/transport-http/src/main/java/org/opendaylight/netconf/transport/http/TlsHTTPServer.java
new file mode 100644 (file)
index 0000000..55a36da
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.HttpServerKeepAliveHandler;
+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.TransportChannelListener;
+
+/**
+ * An {@link HTTPServer} operating over TLS.
+ */
+final class TlsHTTPServer extends HTTPServer {
+    TlsHTTPServer(final TransportChannelListener listener, final RequestDispatcher dispatcher,
+            final AuthHandlerFactory authHandlerFactory) {
+        super(listener, dispatcher, authHandlerFactory);
+    }
+
+    @Override
+    void initializePipeline(final ChannelPipeline pipeline, final Http2ConnectionHandler connectionHandler) {
+        // Application protocol negotiator over TLS
+        pipeline.addLast(new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) {
+            @Override
+            protected void configurePipeline(final ChannelHandlerContext ctx, final String protocol) {
+                final var pipeline = ctx.pipeline();
+
+                switch (protocol) {
+                    case null -> throw new NullPointerException();
+                    case ApplicationProtocolNames.HTTP_1_1 -> {
+                        pipeline.addLast(new HttpServerCodec(),
+                            new HttpServerKeepAliveHandler(),
+                            new HttpObjectAggregator(MAX_HTTP_CONTENT_LENGTH));
+                    }
+                    case ApplicationProtocolNames.HTTP_2 -> {
+                        pipeline.addLast(connectionHandler);
+                    }
+                    default -> throw new IllegalStateException("unknown protocol: " + protocol);
+                }
+
+                configureEndOfPipeline(pipeline);
+            }
+        });
+    }
+}