content: /*
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License.  You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package org.apache.camel.component.cometd;
  
  import java.net.URL;
  import java.util.ArrayList;
  import java.util.EnumSet;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.locks.Lock;
  import java.util.concurrent.locks.ReentrantLock;
  
  import jakarta.servlet.DispatcherType;
  
  import org.apache.camel.Endpoint;
  import org.apache.camel.SSLContextParametersAware;
  import org.apache.camel.spi.Metadata;
  import org.apache.camel.spi.annotations.Component;
  import org.apache.camel.support.DefaultComponent;
  import org.apache.camel.support.jsse.SSLContextParameters;
  import org.cometd.bayeux.server.BayeuxServer;
  import org.cometd.bayeux.server.SecurityPolicy;
  import org.cometd.server.BayeuxServerImpl;
  import org.cometd.server.http.jakarta.CometDServlet;
  import org.eclipse.jetty.ee10.servlet.FilterHolder;
  import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
  import org.eclipse.jetty.ee10.servlet.ServletHolder;
  import org.eclipse.jetty.ee10.servlet.SessionHandler;
  import org.eclipse.jetty.ee10.servlets.CrossOriginFilter;
  import org.eclipse.jetty.server.Connector;
  import org.eclipse.jetty.server.Server;
  import org.eclipse.jetty.server.ServerConnector;
  import org.eclipse.jetty.server.handler.ContextHandlerCollection;
  import org.eclipse.jetty.util.resource.MountedPathResourceFactory;
  import org.eclipse.jetty.util.ssl.SslContextFactory;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * Component for Jetty Cometd
   */
  @Component("cometd,cometds")
  public class CometdComponent extends DefaultComponent implements SSLContextParametersAware {
      private static final Logger LOG = LoggerFactory.getLogger(CometdComponent.class);
  
      private final Lock connectorsLock = new ReentrantLock();
      private final Map<String, ConnectorRef> connectors = new LinkedHashMap<>();
  
      private List<BayeuxServer.BayeuxServerListener> serverListeners;
  
      @Metadata(label = "security", secret = true)
      private String sslKeyPassword;
      @Metadata(label = "security", secret = true)
      private String sslPassword;
      @Metadata(label = "security")
      private String sslKeystore;
      @Metadata(label = "security")
      private SecurityPolicy securityPolicy;
      @Metadata(label = "advanced")
      private List<BayeuxServer.Extension> extensions;
      @Metadata(label = "security")
      private SSLContextParameters sslContextParameters;
      @Metadata(label = "security", defaultValue = "false")
      private boolean useGlobalSslContextParameters;
  
      static class ConnectorRef {
          Connector connector;
          CometDServlet servlet;
          Server server;
          int refCount;
  
          ConnectorRef(Connector connector, CometDServlet servlet, Server server) {
              this.connector = connector;
              this.servlet = servlet;
              this.server = server;
              increment();
          }
  
          public int increment() {
              return ++refCount;
          }
  
          public int decrement() {
              return --refCount;
          }
      }
  
      public CometdComponent() {
      }
  
      @Override
      protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
          CometdEndpoint endpoint = new CometdEndpoint(this, uri, remaining);
          setProperties(endpoint, parameters);
          return endpoint;
      }
  
      /**
       * Connects the URL specified on the endpoint to the specified processor.
       */
      public void connect(CometdProducerConsumer prodcon) throws Exception {
          Server server = null;
          // Make sure that there is a connector for the requested endpoint.
          CometdEndpoint endpoint = prodcon.getEndpoint();
          String connectorKey = endpoint.getProtocol() + ":" + endpoint.getUri().getHost() + ":" + endpoint.getPort();
  
          connectorsLock.lock();
          try {
              ConnectorRef connectorRef = connectors.get(connectorKey);
              if (connectorRef == null) {
                  ServerConnector connector;
                  server = createServer();
                  if ("cometds".equals(endpoint.getProtocol())) {
                      connector = getSslSocketConnector(server);
                  } else {
                      connector = new ServerConnector(server);
                  }
                  connector.setPort(endpoint.getPort());
                  connector.setHost(endpoint.getUri().getHost());
                  if ("localhost".equalsIgnoreCase(endpoint.getUri().getHost())) {
                      LOG.warn("You use localhost interface! It means that no external connections will be available."
                               + " Don't you want to use 0.0.0.0 instead (all network interfaces)?");
                  }
  
                  server.addConnector(connector);
  
                  CometDServlet servlet = createServletForConnector(server, endpoint);
                  connectorRef = new ConnectorRef(connector, servlet, server);
                  server.start();
  
                  connectors.put(connectorKey, connectorRef);
              } else {
                  connectorRef.increment();
              }
  
              BayeuxServerImpl bayeux = (BayeuxServerImpl) connectorRef.servlet.getBayeuxServer();
  
              if (securityPolicy != null) {
                  bayeux.setSecurityPolicy(securityPolicy);
              }
              if (extensions != null) {
                  for (BayeuxServer.Extension extension : extensions) {
                      bayeux.addExtension(extension);
                  }
              }
              if (serverListeners != null) {
                  for (BayeuxServer.BayeuxServerListener serverListener : serverListeners) {
                      bayeux.addListener(serverListener);
                  }
              }
              prodcon.setBayeux(bayeux);
          } finally {
              connectorsLock.unlock();
          }
      }
  
      /**
       * Disconnects the URL specified on the endpoint from the specified processor.
       */
      public void disconnect(CometdProducerConsumer prodcon) throws Exception {
          CometdEndpoint endpoint = prodcon.getEndpoint();
  
          String connectorKey = endpoint.getProtocol() + ":" + endpoint.getUri().getHost() + ":" + endpoint.getPort();
  
          connectorsLock.lock();
          try {
              ConnectorRef connectorRef = connectors.get(connectorKey);
              if (connectorRef != null && connectorRef.decrement() == 0) {
                  connectorRef.server.removeConnector(connectorRef.connector);
                  connectorRef.connector.stop();
                  connectorRef.server.stop();
                  connectors.remove(connectorKey);
              }
          } finally {
              connectorsLock.unlock();
          }
      }
  
      protected CometDServlet createServletForConnector(Server server, CometdEndpoint endpoint)
              throws Exception {
          CometDServlet servlet = new CometDServlet();
  
          ServletContextHandler context
                  = new ServletContextHandler("/", false, false);
  
          server.setHandler(context);
  
          ServletHolder holder = new ServletHolder();
          holder.setServlet(servlet);
          holder.setAsyncSupported(true);
  
          // Use baseResource to pass as a parameter the url
          // pointing to by example classpath:webapp
          if (endpoint.getBaseResource() != null) {
              String[] resources = endpoint.getBaseResource().split(":");
              LOG.debug(">>> Protocol found: {}, and resource: {}", resources[0], resources[1]);
  
              if (resources[0].equals("file")) {
                  context.setBaseResource(new MountedPathResourceFactory().newResource(resources[1]));
              } else if (resources[0].equals("classpath")) {
                  // Create a URL handler using classpath protocol
                  URL url = this.getCamelContext().getClassResolver().loadResourceAsURL(resources[1]);
                  context.setBaseResource(new MountedPathResourceFactory().newResource(url));
              }
          }
  
          applyCrossOriginFiltering(endpoint, context);
  
          context.addServlet(holder, "/cometd/*");
          context.addServlet("org.eclipse.jetty.ee10.servlet.DefaultServlet", "/");
          context.setSessionHandler(new SessionHandler());
  
          holder.setInitParameter("timeout", Integer.toString(endpoint.getTimeout()));
          holder.setInitParameter("interval", Integer.toString(endpoint.getInterval()));
          holder.setInitParameter("maxInterval", Integer.toString(endpoint.getMaxInterval()));
          holder.setInitParameter("multiFrameInterval", Integer.toString(endpoint.getMultiFrameInterval()));
          holder.setInitParameter("JSONCommented", Boolean.toString(endpoint.isJsonCommented()));
          holder.setInitParameter("logLevel", Integer.toString(endpoint.getLogLevel()));
  
          return servlet;
      }
  
      protected ServerConnector getSslSocketConnector(Server server) throws Exception {
          ServerConnector sslSocketConnector = null;
          SSLContextParameters sslParams = this.sslContextParameters;
          if (sslParams == null) {
              sslParams = retrieveGlobalSslContextParameters();
          }
  
          SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
          sslContextFactory.setEndpointIdentificationAlgorithm(null);
          if (sslParams != null) {
              sslContextFactory.setSslContext(sslParams.createSSLContext(getCamelContext()));
          } else {
              sslContextFactory.setKeyStorePassword(sslKeyPassword);
              sslContextFactory.setKeyManagerPassword(sslPassword);
              if (sslKeystore != null) {
                  sslContextFactory.setKeyStorePath(sslKeystore);
              }
          }
          sslSocketConnector = new ServerConnector(server, sslContextFactory);
          return sslSocketConnector;
      }
  
      public String getSslKeyPassword() {
          return sslKeyPassword;
      }
  
      public String getSslPassword() {
          return sslPassword;
      }
  
      public String getSslKeystore() {
          return sslKeystore;
      }
  
      /**
       * The password for the keystore when using SSL.
       */
      public void setSslKeyPassword(String sslKeyPassword) {
          this.sslKeyPassword = sslKeyPassword;
      }
  
      /**
       * The password when using SSL.
       */
      public void setSslPassword(String sslPassword) {
          this.sslPassword = sslPassword;
      }
  
      /**
       * The path to the keystore.
       */
      public void setSslKeystore(String sslKeystore) {
          this.sslKeystore = sslKeystore;
      }
  
      /**
       * To use a custom configured SecurityPolicy to control authorization
       */
      public void setSecurityPolicy(SecurityPolicy securityPolicy) {
          this.securityPolicy = securityPolicy;
      }
  
      public SecurityPolicy getSecurityPolicy() {
          return securityPolicy;
      }
  
      public List<BayeuxServer.Extension> getExtensions() {
          return extensions;
      }
  
      /**
       * To use a list of custom BayeuxServer.Extension that allows modifying incoming and outgoing requests.
       */
      public void setExtensions(List<BayeuxServer.Extension> extensions) {
          this.extensions = extensions;
      }
  
      public void addExtension(BayeuxServer.Extension extension) {
          if (extensions == null) {
              extensions = new ArrayList<>();
          }
          extensions.add(extension);
      }
  
      public void addServerListener(BayeuxServer.BayeuxServerListener serverListener) {
          if (serverListeners == null) {
              serverListeners = new ArrayList<>();
          }
          serverListeners.add(serverListener);
      }
  
      public SSLContextParameters getSslContextParameters() {
          return sslContextParameters;
      }
  
      /**
       * To configure security using SSLContextParameters
       */
      public void setSslContextParameters(SSLContextParameters sslContextParameters) {
          this.sslContextParameters = sslContextParameters;
      }
  
      @Override
      public boolean isUseGlobalSslContextParameters() {
          return this.useGlobalSslContextParameters;
      }
  
      /**
       * Enable usage of global SSL context parameters.
       */
      @Override
      public void setUseGlobalSslContextParameters(boolean useGlobalSslContextParameters) {
          this.useGlobalSslContextParameters = useGlobalSslContextParameters;
      }
  
      protected Server createServer() {
          Server server = new Server();
          ContextHandlerCollection collection = new ContextHandlerCollection();
          server.setHandler(collection);
          return server;
      }
  
      @Override
      protected void doStop() throws Exception {
          connectorsLock.lock();
          try {
              for (ConnectorRef connectorRef : connectors.values()) {
                  connectorRef.connector.stop();
              }
              connectors.clear();
          } finally {
              connectorsLock.unlock();
          }
  
          super.doStop();
      }
  
      private void applyCrossOriginFiltering(CometdEndpoint endpoint, ServletContextHandler context) {
          if (endpoint.isCrossOriginFilterOn()) {
              FilterHolder filterHolder = new FilterHolder();
              CrossOriginFilter filter = new CrossOriginFilter();
              filterHolder.setFilter(filter);
              filterHolder.setInitParameter("allowedOrigins", endpoint.getAllowedOrigins());
              context.addFilter(filterHolder, endpoint.getFilterPath(), EnumSet.allOf(DispatcherType.class));
          }
      }
  
  }
  