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.net.InetSocketAddress;
  import java.security.KeyPair;
  import java.security.cert.X509Certificate;
  import java.util.Objects;
  import java.util.Optional;
  
  import io.netty.buffer.ByteBuf;
  import io.netty.channel.ChannelHandlerContext;
  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.HttpHeaderNames;
  import io.netty.handler.codec.http.HttpHeaderValues;
  import io.netty.handler.codec.http.HttpResponseStatus;
  import io.netty.handler.codec.http.HttpVersion;
  import org.eclipse.milo.opcua.stack.core.StatusCodes;
  import org.eclipse.milo.opcua.stack.core.UaException;
  import org.eclipse.milo.opcua.stack.core.channel.ServerSecureChannel;
  import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
  import org.eclipse.milo.opcua.stack.core.serialization.OpcUaBinaryStreamDecoder;
  import org.eclipse.milo.opcua.stack.core.serialization.OpcUaBinaryStreamEncoder;
  import org.eclipse.milo.opcua.stack.core.serialization.UaRequestMessage;
  import org.eclipse.milo.opcua.stack.core.serialization.UaResponseMessage;
  import org.eclipse.milo.opcua.stack.core.transport.TransportProfile;
  import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString;
  import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime;
  import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
  import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
  import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode;
  import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
  import org.eclipse.milo.opcua.stack.core.types.structured.ResponseHeader;
  import org.eclipse.milo.opcua.stack.core.types.structured.ServiceFault;
  import org.eclipse.milo.opcua.stack.core.util.BufferUtil;
  import org.eclipse.milo.opcua.stack.core.util.DigestUtil;
  import org.eclipse.milo.opcua.stack.core.util.EndpointUtil;
  import org.eclipse.milo.opcua.stack.server.UaStackServer;
  import org.eclipse.milo.opcua.stack.server.services.ServiceRequest;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  public class OpcServerHttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
  
      private static final String UABINARY_CONTENT_TYPE =
          HttpHeaderValues.APPLICATION_OCTET_STREAM.toString();
  
      private final Logger logger = LoggerFactory.getLogger(getClass());
  
      private final UaStackServer stackServer;
  
      OpcServerHttpRequestHandler(UaStackServer stackServer) {
          this.stackServer = stackServer;
      }
  
      @Override
      protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest httpRequest) throws Exception {
          String host = httpRequest.headers().get(HttpHeaderNames.HOST);
          String uri = httpRequest.uri();
          String contentType = httpRequest.headers().get(HttpHeaderNames.CONTENT_TYPE);
          String securityPolicyUri = httpRequest.headers().get("OPCUA-SecurityPolicy");
  
          logger.debug("host={} uri={} contentType={} securityPolicy={}", host, uri, contentType, securityPolicyUri);
  
          SecurityPolicy securityPolicy = securityPolicyUri != null ?
              SecurityPolicy.fromUri(securityPolicyUri) :
              SecurityPolicy.None;
  
          MessageSecurityMode securityMode = securityPolicy == SecurityPolicy.None ?
              MessageSecurityMode.None :
              MessageSecurityMode.Sign;
  
          EndpointDescription endpoint = stackServer.getEndpointDescriptions()
              .stream()
              .filter(e -> {
                  // TODO use contentType to determine which TransportProfile to match
                  boolean transportMatch = Objects.equals(
                      e.getTransportProfileUri(),
                      TransportProfile.HTTPS_UABINARY.getUri()
                  );
  
                  boolean pathMatch = Objects.equals(
                      EndpointUtil.getPath(e.getEndpointUrl()),
                      uri
                  );
  
                  boolean securityPolicyMatch = Objects.equals(
                      e.getSecurityPolicyUri(),
                      securityPolicy.getUri()
                  );
  
                  boolean securityModeMatch = Objects.equals(
                      e.getSecurityMode(),
                      securityMode
                  );
  
                  return transportMatch && pathMatch && securityPolicyMatch && securityModeMatch;
              })
              .findFirst()
              .orElseThrow(() ->
                  new UaException(
                      StatusCodes.Bad_TcpEndpointUrlInvalid,
                      "unrecognized endpoint uri: " + uri));
  
  
          ServerSecureChannel secureChannel = new ServerSecureChannel();
          secureChannel.setChannelId(0L); // TODO shared id per endpoint URL / path?
          secureChannel.setSecurityPolicy(securityPolicy);
          secureChannel.setMessageSecurityMode(securityMode);
  
          ByteString thumbprint = ByteString.of(DigestUtil.sha1(endpoint.getServerCertificate().bytesOrEmpty()));
  
          Optional<X509Certificate[]> certificateChain = stackServer.getConfig()
              .getCertificateManager()
              .getCertificateChain(thumbprint);
  
          Optional<KeyPair> keyPair = stackServer.getConfig()
              .getCertificateManager()
              .getKeyPair(thumbprint);
  
          certificateChain.ifPresent(chain -> {
              secureChannel.setLocalCertificateChain(chain);
              secureChannel.setLocalCertificate(chain[0]);
          });
  
          keyPair.ifPresent(secureChannel::setKeyPair);
  
          OpcUaBinaryStreamDecoder decoder = new OpcUaBinaryStreamDecoder(stackServer.getSerializationContext());
          decoder.setBuffer(httpRequest.content());
  
          try {
              UaRequestMessage request = (UaRequestMessage) decoder.readMessage(null);
              UInteger requestHandle = request.getRequestHeader().getRequestHandle();
  
              InetSocketAddress remoteSocketAddress =
                  (InetSocketAddress) ctx.channel().remoteAddress();
  
              ServiceRequest serviceRequest = new ServiceRequest(
                  stackServer,
                  request,
                  endpoint,
                  secureChannel.getChannelId(),
                  remoteSocketAddress.getAddress(),
                  null
              );
  
              serviceRequest.getFuture().whenComplete((response, fault) -> {
                  if (response != null) {
                      sendServiceResponse(ctx, request, response);
                  } else {
                      sendServiceFault(ctx, requestHandle, fault);
                  }
              });
  
              stackServer.onServiceRequest(uri, serviceRequest);
          } catch (Throwable t) {
              logger.error("Error decoding UaRequestMessage", t);
  
              sendServiceFault(ctx, null, t);
          }
      }
  
      private void sendServiceResponse(
          ChannelHandlerContext ctx,
          UaRequestMessage request,
          UaResponseMessage response) {
  
          ByteBuf contentBuffer = BufferUtil.pooledBuffer();
  
          // TODO switch on transport profile for binary vs xml encoding
          OpcUaBinaryStreamEncoder binaryEncoder = new OpcUaBinaryStreamEncoder(stackServer.getSerializationContext());
          binaryEncoder.setBuffer(contentBuffer);
          binaryEncoder.writeMessage(null, response);
  
          FullHttpResponse httpResponse = new DefaultFullHttpResponse(
              HttpVersion.HTTP_1_1,
              HttpResponseStatus.OK,
              contentBuffer
          );
  
          httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
          httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, UABINARY_CONTENT_TYPE);
          httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, contentBuffer.readableBytes());
  
          ctx.writeAndFlush(httpResponse);
      }
  
      private void sendServiceFault(
          ChannelHandlerContext ctx,
          UInteger requestHandle,
          Throwable fault) {
  
          StatusCode statusCode = UaException.extract(fault)
              .map(UaException::getStatusCode)
              .orElse(StatusCode.BAD);
  
          ServiceFault serviceFault = new ServiceFault(
              new ResponseHeader(
                  DateTime.now(),
                  requestHandle,
                  statusCode,
                  null,
                  null,
                  null
              )
          );
  
          ByteBuf contentBuffer = BufferUtil.pooledBuffer();
  
          // TODO switch on transport profile for binary vs xml encoding
          OpcUaBinaryStreamEncoder binaryEncoder = new OpcUaBinaryStreamEncoder(stackServer.getSerializationContext());
          binaryEncoder.setBuffer(contentBuffer);
          binaryEncoder.writeMessage(null, serviceFault);
  
          FullHttpResponse httpResponse = new DefaultFullHttpResponse(
              HttpVersion.HTTP_1_1,
              HttpResponseStatus.OK,
              contentBuffer
          );
  
          httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
          httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, UABINARY_CONTENT_TYPE);
          httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, contentBuffer.readableBytes());
  
          ctx.writeAndFlush(httpResponse);
      }
  
  }
  