code: /*
   * Copyright (c) 2019 the Eclipse Milo Authors
   *
   * This program and the accompanying materials are made
   * available under the terms of the Eclipse Public License 2.0
   * which is available at https://www.eclipse.org/legal/epl-2.0/
   *
   * SPDX-License-Identifier: EPL-2.0
   */
  
  package org.eclipse.milo.opcua.stack.server.transport.http;
  
  import java.security.KeyPair;
  import java.security.PrivateKey;
  import java.security.cert.X509Certificate;
  import java.util.Objects;
  
  import io.netty.channel.ChannelHandlerContext;
  import io.netty.channel.ChannelInitializer;
  import io.netty.channel.SimpleChannelInboundHandler;
  import io.netty.channel.socket.SocketChannel;
  import io.netty.handler.codec.http.DefaultFullHttpResponse;
  import io.netty.handler.codec.http.FullHttpRequest;
  import io.netty.handler.codec.http.HttpHeaderNames;
  import io.netty.handler.codec.http.HttpHeaderValues;
  import io.netty.handler.codec.http.HttpMethod;
  import io.netty.handler.codec.http.HttpObjectAggregator;
  import io.netty.handler.codec.http.HttpResponse;
  import io.netty.handler.codec.http.HttpResponseStatus;
  import io.netty.handler.codec.http.HttpServerCodec;
  import io.netty.handler.codec.http.HttpVersion;
  import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
  import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
  import io.netty.handler.logging.LogLevel;
  import io.netty.handler.logging.LoggingHandler;
  import io.netty.handler.ssl.ClientAuth;
  import io.netty.handler.ssl.SslContext;
  import io.netty.handler.ssl.SslContextBuilder;
  import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
  import org.eclipse.milo.opcua.stack.core.Stack;
  import org.eclipse.milo.opcua.stack.core.util.EndpointUtil;
  import org.eclipse.milo.opcua.stack.server.UaStackServer;
  import org.eclipse.milo.opcua.stack.server.transport.RateLimitingHandler;
  import org.eclipse.milo.opcua.stack.server.transport.websocket.OpcServerWebSocketFrameHandler;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  public class OpcServerHttpChannelInitializer extends ChannelInitializer<SocketChannel> {
  
      private SslContext sslContext = null;
  
      private final UaStackServer stackServer;
  
      public OpcServerHttpChannelInitializer(UaStackServer stackServer) {
          this.stackServer = stackServer;
  
          KeyPair keyPair = stackServer.getConfig().getHttpsKeyPair().orElse(null);
          X509Certificate[] httpsCertificate = stackServer.getConfig().getHttpsCertificateChain().orElse(null);
  
          if (keyPair != null && httpsCertificate != null) {
              try {
                  PrivateKey privateKey = keyPair.getPrivate();
  
                  sslContext = SslContextBuilder
                      .forServer(privateKey, httpsCertificate)
                      .clientAuth(ClientAuth.NONE)
                      .trustManager(InsecureTrustManagerFactory.INSTANCE)
                      .build();
              } catch (Exception e) {
                  LoggerFactory.getLogger(OpcServerHttpChannelInitializer.class)
                      .error("Error configuration SslContext: {}", e.getMessage(), e);
              }
          } else {
              LoggerFactory.getLogger(OpcServerHttpChannelInitializer.class)
                  .warn("HTTPS KeyPair and/or Certificate not configured; falling back to plaintext...");
          }
      }
  
  
      @Override
      protected void initChannel(SocketChannel channel) {
          stackServer.registerConnectedChannel(channel);
  
          channel.closeFuture().addListener(future -> stackServer.unregisterConnectedChannel(channel));
  
          channel.pipeline().addLast(RateLimitingHandler.getInstance());
  
          if (sslContext != null) {
              channel.pipeline().addLast(sslContext.newHandler(channel.alloc()));
          }
  
          channel.pipeline().addLast(new LoggingHandler(LogLevel.TRACE));
          channel.pipeline().addLast(new HttpServerCodec());
  
          // TODO configure maxContentLength based on MaxRequestSize?
          channel.pipeline().addLast(new HttpObjectAggregator(Integer.MAX_VALUE));
          channel.pipeline().addLast(new OpcHttpTransportInterceptor(stackServer));
      }
  
      private static class OpcHttpTransportInterceptor extends SimpleChannelInboundHandler<FullHttpRequest> {
  
          private final Logger logger = LoggerFactory.getLogger(getClass());
  
          private final UaStackServer stackServer;
  
          public OpcHttpTransportInterceptor(UaStackServer stackServer) {
              this.stackServer = stackServer;
          }
  
          @Override
          protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest httpRequest) {
              String host = httpRequest.headers().get(HttpHeaderNames.HOST);
              String uri = httpRequest.uri();
  
              logger.debug("host={} uri={}", host, uri);
  
              boolean endpointMatch = stackServer.getEndpointDescriptions()
                  .stream()
                  .anyMatch(endpoint ->
                      Objects.equals(
                          uri,
                          EndpointUtil.getPath(endpoint.getEndpointUrl()))
                  );
  
              if (!endpointMatch) {
                  logger.debug("unrecognized endpoint URL: " + uri);
  
                  HttpResponse response = new DefaultFullHttpResponse(
                      HttpVersion.HTTP_1_1,
                      HttpResponseStatus.NOT_FOUND
                  );
  
                  ctx.channel().writeAndFlush(response)
                      .addListener(future -> ctx.close());
  
                  return;
              }
  
              if (Objects.equals(httpRequest.method(), HttpMethod.GET) &&
                  "websocket".equalsIgnoreCase(httpRequest.headers().get(HttpHeaderValues.UPGRADE))) {
  
                  logger.debug("intercepted WebSocket upgrade");
  
                  ctx.channel().pipeline().remove(this);
  
                  ctx.channel().pipeline().addLast(new WebSocketServerCompressionHandler());
  
                  // TODO configure webSocketPath based on path component of endpoint URL?
                  ctx.channel().pipeline().addLast(new WebSocketServerProtocolHandler(
                      "/ws",
                      String.format("%s, %s", Stack.WSS_PROTOCOL_BINARY, Stack.WSS_PROTOCOL_JSON),
                      true
                  ));
  
                  ctx.channel().pipeline().addLast(new OpcServerWebSocketFrameHandler(stackServer));
  
                  httpRequest.retain();
                  ctx.executor().execute(() -> ctx.fireChannelRead(httpRequest));
              } else if (Objects.equals(httpRequest.method(), HttpMethod.POST)) {
                  logger.debug("intercepted HTTP POST");
  
                  ctx.channel().pipeline().remove(this);
                  ctx.channel().pipeline().addLast(new OpcServerHttpRequestHandler(stackServer));
  
                  httpRequest.retain();
                  ctx.executor().execute(() -> ctx.pipeline().fireChannelRead(httpRequest));
              } else {
                  HttpResponse response = new DefaultFullHttpResponse(
                      HttpVersion.HTTP_1_1,
                      HttpResponseStatus.BAD_REQUEST
                  );
  
                  ctx.channel().writeAndFlush(response)
                      .addListener(future -> ctx.close());
              }
          }
      }
  
  }
  