You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hop.apache.org by ha...@apache.org on 2021/12/17 11:05:00 UTC

[incubator-hop] branch master updated: HOP-3555 : Hop server startup error with SSL

This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-hop.git


The following commit(s) were added to refs/heads/master by this push:
     new 490c229  HOP-3555 : Hop server startup error with SSL
     new 4214157  Merge pull request #1227 from mattcasters/master
490c229 is described below

commit 490c229f0716036c3890b3ac389de69daeef7c4e
Author: Matt Casters <ma...@gmail.com>
AuthorDate: Thu Dec 16 16:31:20 2021 +0100

    HOP-3555 : Hop server startup error with SSL
---
 .../modules/ROOT/pages/hop-server/index.adoc       |  33 +-
 .../main/java/org/apache/hop/server/HopServer.java |  26 +-
 .../main/java/org/apache/hop/www/WebServer.java    | 857 +++++++++++----------
 .../java/org/apache/hop/server/HopServerTest.java  |   4 +-
 4 files changed, 495 insertions(+), 425 deletions(-)

diff --git a/docs/hop-user-manual/modules/ROOT/pages/hop-server/index.adoc b/docs/hop-user-manual/modules/ROOT/pages/hop-server/index.adoc
index 0d05ac3..6656a82 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/hop-server/index.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/hop-server/index.adoc
@@ -228,20 +228,45 @@ The 3 main options are:
 * `keyPassword` : the key password.
 If this is the same as the keystore password you can omit this option.
 
-Example:
-[source,xml]
+The HTTP protocol used is version 1.1 or `HTTP/1.1`.
+The type of keystore read is a Java Keystore or type: `JKS`.
+Let's take a look at how we can generate a sample keystore:
 
+[source,bash]
+----
+# Generate a new key
+#
+openssl genrsa -des3 -out hop.key
+
+# Make a new certificate
+#
+openssl req -new -x509 -key hop.key -out hop.crt
+
+# Create a PKCS12 keystore and import it into a JKS keystore
+# The resulting file is: keystore
+#
+keytool -keystore keystore -import -alias hop -file hop.crt -trustcacerts
+openssl req -new -key hop.key -out hop.csr
+openssl pkcs12 -inkey hop.key -in hop.crt -export -out hop.pkcs12
+keytool -importkeystore -srckeystore hop.pkcs12 -srcstoretype PKCS12 -destkeystore keystore
+----
+
+Here is an example of the information to include in your server XML:
+
+[source,xml]
 ----
 <hop-server-config>
   <hop-server>
     ...
 
     <sslConfig>
-      <keyStore>/path/to/keystore.jks</keyStore>
+      <keyStore>/path/to/keystore</keyStore>
       <keyStorePassword>password</keyStorePassword>
-      <keyPassword>anotherPassword</keyPassword>
+      <keyPassword>keyPassword</keyPassword>
     </sslConfig>
 
+    <!-- Add the following line to support querying over https -->
+    <sslMode>Y</sslMode>
   </hop-server>
   ...
 </hop-server-config>
diff --git a/engine/src/main/java/org/apache/hop/server/HopServer.java b/engine/src/main/java/org/apache/hop/server/HopServer.java
index bba279b..b373386 100644
--- a/engine/src/main/java/org/apache/hop/server/HopServer.java
+++ b/engine/src/main/java/org/apache/hop/server/HopServer.java
@@ -44,15 +44,22 @@ import org.apache.http.client.methods.HttpGet;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpUriRequest;
 import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
+import org.apache.http.conn.ssl.TrustStrategy;
 import org.apache.http.entity.ByteArrayEntity;
 import org.apache.http.entity.InputStreamEntity;
 import org.apache.http.impl.auth.BasicScheme;
 import org.apache.http.impl.client.BasicAuthCache;
 import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
 import org.apache.http.message.BasicHeader;
 import org.w3c.dom.Document;
 import org.w3c.dom.Node;
 
+import javax.net.ssl.SSLContext;
 import java.io.*;
 import java.net.URLEncoder;
 import java.text.MessageFormat;
@@ -722,9 +729,22 @@ public class HopServer extends HopMetadataBase implements Cloneable, IXml, IHopM
   }
 
   // Method is defined as package-protected in order to be accessible by unit tests
-  HttpClient getHttpClient() {
-    ServerConnectionManager connectionManager = ServerConnectionManager.getInstance();
-    return connectionManager.createHttpClient();
+  HttpClient getHttpClient() throws HopException {
+    try {
+      if (sslConfig != null) {
+        TrustStrategy acceptingTrustStrategy = new TrustSelfSignedStrategy();
+        SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
+
+        SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
+        CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(csf).build();
+        return httpClient;
+      } else {
+        ServerConnectionManager connectionManager = ServerConnectionManager.getInstance();
+        return connectionManager.createHttpClient();
+      }
+    } catch (Exception e) {
+      throw new HopException("Error creating new HTTP client", e);
+    }
   }
 
   public HopServerStatus getStatus(IVariables variables) throws Exception {
diff --git a/engine/src/main/java/org/apache/hop/www/WebServer.java b/engine/src/main/java/org/apache/hop/www/WebServer.java
index a74afa9..ffdcf84 100644
--- a/engine/src/main/java/org/apache/hop/www/WebServer.java
+++ b/engine/src/main/java/org/apache/hop/www/WebServer.java
@@ -20,6 +20,7 @@ package org.apache.hop.www;
 import com.sun.jersey.spi.container.servlet.ServletContainer;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.encryption.Encr;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.extension.ExtensionPointHandler;
 import org.apache.hop.core.extension.HopExtensionPoint;
@@ -32,6 +33,7 @@ import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.variables.Variables;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.server.HopServer;
+import org.eclipse.jetty.http.HttpVersion;
 import org.eclipse.jetty.jaas.JAASLoginService;
 import org.eclipse.jetty.security.*;
 import org.eclipse.jetty.server.*;
@@ -53,442 +55,465 @@ import java.util.List;
 
 public class WebServer {
 
-  private static final int DEFAULT_DETECTION_TIMER = 20000;
-  private static final Class<?> PKG = WebServer.class; // For Translator
-
-  private ILogChannel log;
-  private IVariables variables;
-  public static final int PORT = 80;
-
-  private Server server;
-
-  private PipelineMap pipelineMap;
-  private WorkflowMap workflowMap;
-
-  private String hostname;
-  private int port;
-
-  private String passwordFile;
-  private WebServerShutdownHook webServerShutdownHook;
-  private IWebServerShutdownHandler webServerShutdownHandler =
-      new DefaultWebServerShutdownHandler();
-
-  private SslConfiguration sslConfig;
-
-  public WebServer(
-      ILogChannel log,
-      PipelineMap pipelineMap,
-      WorkflowMap workflowMap,
-      String hostname,
-      int port,
-      boolean join,
-      String passwordFile)
-      throws Exception {
-    this(log, pipelineMap, workflowMap, hostname, port, join, passwordFile, null);
-  }
-
-  public WebServer(
-      ILogChannel log,
-      PipelineMap pipelineMap,
-      WorkflowMap workflowMap,
-      String hostname,
-      int port,
-      boolean join,
-      String passwordFile,
-      SslConfiguration sslConfig)
-      throws Exception {
-    this.log = log;
-    this.pipelineMap = pipelineMap;
-    this.workflowMap = workflowMap;
-    if (pipelineMap != null) {
-      variables = pipelineMap.getHopServerConfig().getVariables();
-    } else if (workflowMap != null) {
-      variables = workflowMap.getHopServerConfig().getVariables();
-    } else {
-      variables = Variables.getADefaultVariableSpace();
+    private static final int DEFAULT_DETECTION_TIMER = 20000;
+    private static final Class<?> PKG = WebServer.class; // For Translator
+
+    private ILogChannel log;
+    private IVariables variables;
+    public static final int PORT = 80;
+
+    private Server server;
+
+    private PipelineMap pipelineMap;
+    private WorkflowMap workflowMap;
+
+    private String hostname;
+    private int port;
+
+    private String passwordFile;
+    private WebServerShutdownHook webServerShutdownHook;
+    private IWebServerShutdownHandler webServerShutdownHandler =
+            new DefaultWebServerShutdownHandler();
+
+    private SslConfiguration sslConfig;
+
+    public WebServer(
+            ILogChannel log,
+            PipelineMap pipelineMap,
+            WorkflowMap workflowMap,
+            String hostname,
+            int port,
+            boolean join,
+            String passwordFile)
+            throws Exception {
+        this(log, pipelineMap, workflowMap, hostname, port, join, passwordFile, null);
     }
-    this.hostname = hostname;
-    this.port = port;
-    this.passwordFile = passwordFile;
-    this.sslConfig = sslConfig;
-
-    startServer();
-
-    webServerShutdownHook = new WebServerShutdownHook(this);
-    Runtime.getRuntime().addShutdownHook(webServerShutdownHook);
-
-    try {
-      ExtensionPointHandler.callExtensionPoint(
-          log, variables, HopExtensionPoint.HopServerStartup.id, this);
-    } catch (HopException e) {
-      // Log error but continue regular operations to make sure HopServer continues to run properly
-      //
-      log.logError("Error calling extension point HopServerStartup", e);
+
+    public WebServer(
+            ILogChannel log,
+            PipelineMap pipelineMap,
+            WorkflowMap workflowMap,
+            String hostname,
+            int port,
+            boolean join,
+            String passwordFile,
+            SslConfiguration sslConfig)
+            throws Exception {
+        this.log = log;
+        this.pipelineMap = pipelineMap;
+        this.workflowMap = workflowMap;
+        if (pipelineMap != null) {
+            variables = pipelineMap.getHopServerConfig().getVariables();
+        } else if (workflowMap != null) {
+            variables = workflowMap.getHopServerConfig().getVariables();
+        } else {
+            variables = Variables.getADefaultVariableSpace();
+        }
+        this.hostname = hostname;
+        this.port = port;
+        this.passwordFile = passwordFile;
+        this.sslConfig = sslConfig;
+
+        startServer();
+
+        webServerShutdownHook = new WebServerShutdownHook(this);
+        Runtime.getRuntime().addShutdownHook(webServerShutdownHook);
+
+        try {
+            ExtensionPointHandler.callExtensionPoint(
+                    log, variables, HopExtensionPoint.HopServerStartup.id, this);
+        } catch (HopException e) {
+            // Log error but continue regular operations to make sure HopServer continues to run properly
+            //
+            log.logError("Error calling extension point HopServerStartup", e);
+        }
+
+        if (join) {
+            server.join();
+        }
     }
 
-    if (join) {
-      server.join();
+    public WebServer(
+            ILogChannel log, PipelineMap pipelineMap, WorkflowMap workflowMap, String hostname, int port)
+            throws Exception {
+        this(log, pipelineMap, workflowMap, hostname, port, true);
     }
-  }
-
-  public WebServer(
-      ILogChannel log, PipelineMap pipelineMap, WorkflowMap workflowMap, String hostname, int port)
-      throws Exception {
-    this(log, pipelineMap, workflowMap, hostname, port, true);
-  }
-
-  public WebServer(
-      ILogChannel log,
-      PipelineMap pipelineMap,
-      WorkflowMap workflowMap,
-      String hostname,
-      int port,
-      boolean join)
-      throws Exception {
-    this(log, pipelineMap, workflowMap, hostname, port, join, null, null);
-  }
-
-  public Server getServer() {
-    return server;
-  }
-
-  public void startServer() throws Exception {
-    server = new Server();
-
-    List<String> roles = new ArrayList<>();
-    roles.add(Constraint.ANY_ROLE);
-
-    // Set up the security handler, optionally with JAAS
-    //
-    ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
-
-    if (System.getProperty("loginmodulename") != null
-        && System.getProperty("java.security.auth.login.config") != null) {
-      JAASLoginService jaasLoginService = new JAASLoginService("Hop");
-      jaasLoginService.setLoginModuleName(System.getProperty("loginmodulename"));
-      securityHandler.setLoginService(jaasLoginService);
-    } else {
-      roles.add("default");
-      HashLoginService hashLoginService;
-      HopServer hopServer = pipelineMap.getHopServerConfig().getHopServer();
-      if (!Utils.isEmpty(hopServer.getPassword())) {
-        hashLoginService = new HashLoginService("Hop");
-        UserStore userStore = new UserStore();
-        userStore.addUser(
-            hopServer.getUsername(),
-            new Password(hopServer.getPassword()),
-            new String[] {"default"});
-        hashLoginService.setUserStore(userStore);
-      } else {
-        // See if there is a hop.pwd file in the HOP_HOME directory:
-        if (Utils.isEmpty(passwordFile)) {
-          passwordFile = Const.getHopLocalServerPasswordFile();
+
+    public WebServer(
+            ILogChannel log,
+            PipelineMap pipelineMap,
+            WorkflowMap workflowMap,
+            String hostname,
+            int port,
+            boolean join)
+            throws Exception {
+        this(log, pipelineMap, workflowMap, hostname, port, join, null, null);
+    }
+
+    public Server getServer() {
+        return server;
+    }
+
+    public void startServer() throws Exception {
+        server = new Server();
+
+        List<String> roles = new ArrayList<>();
+        roles.add(Constraint.ANY_ROLE);
+
+        // Set up the security handler, optionally with JAAS
+        //
+        ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
+
+        if (System.getProperty("loginmodulename") != null
+                && System.getProperty("java.security.auth.login.config") != null) {
+            JAASLoginService jaasLoginService = new JAASLoginService("Hop");
+            jaasLoginService.setLoginModuleName(System.getProperty("loginmodulename"));
+            securityHandler.setLoginService(jaasLoginService);
+        } else {
+            roles.add("default");
+            HashLoginService hashLoginService;
+            HopServer hopServer = pipelineMap.getHopServerConfig().getHopServer();
+            if (!Utils.isEmpty(hopServer.getPassword())) {
+                hashLoginService = new HashLoginService("Hop");
+                UserStore userStore = new UserStore();
+                userStore.addUser(
+                        hopServer.getUsername(),
+                        new Password(hopServer.getPassword()),
+                        new String[]{"default"});
+                hashLoginService.setUserStore(userStore);
+            } else {
+                // See if there is a hop.pwd file in the HOP_HOME directory:
+                if (Utils.isEmpty(passwordFile)) {
+                    passwordFile = Const.getHopLocalServerPasswordFile();
+                }
+                hashLoginService = new HashLoginService("Hop");
+                PropertyUserStore userStore = new PropertyUserStore();
+                userStore.setConfig(passwordFile);
+                hashLoginService.setUserStore(userStore);
+            }
+            securityHandler.setLoginService(hashLoginService);
+        }
+
+        Constraint constraint = new Constraint();
+        constraint.setName(Constraint.__BASIC_AUTH);
+        constraint.setRoles(roles.toArray(new String[roles.size()]));
+        constraint.setAuthenticate(true);
+
+        ConstraintMapping constraintMapping = new ConstraintMapping();
+        constraintMapping.setConstraint(constraint);
+        constraintMapping.setPathSpec("/*");
+
+        securityHandler.setConstraintMappings(new ConstraintMapping[]{constraintMapping});
+
+        // Add all the servlets defined in hop-servlets.xml ...
+        //
+        ContextHandlerCollection contexts = new ContextHandlerCollection();
+
+        // Root
+        //
+        ServletContextHandler root =
+                new ServletContextHandler(
+                        contexts, GetRootServlet.CONTEXT_PATH, ServletContextHandler.SESSIONS);
+        GetRootServlet rootServlet = new GetRootServlet();
+        rootServlet.setJettyMode(true);
+        root.addServlet(new ServletHolder(rootServlet), "/*");
+
+        PluginRegistry pluginRegistry = PluginRegistry.getInstance();
+        List<IPlugin> plugins = pluginRegistry.getPlugins(HopServerPluginType.class);
+        for (IPlugin plugin : plugins) {
+
+            IHopServerPlugin servlet = pluginRegistry.loadClass(plugin, IHopServerPlugin.class);
+            servlet.setup(pipelineMap, workflowMap);
+            servlet.setJettyMode(true);
+
+            ServletContextHandler servletContext =
+                    new ServletContextHandler(
+                            contexts, getContextPath(servlet), ServletContextHandler.SESSIONS);
+            ServletHolder servletHolder = new ServletHolder((Servlet) servlet);
+            servletContext.addServlet(servletHolder, "/*");
         }
-        hashLoginService = new HashLoginService("Hop");
-        PropertyUserStore userStore = new PropertyUserStore();
-        userStore.setConfig(passwordFile);
-        hashLoginService.setUserStore(userStore);
-      }
-      securityHandler.setLoginService(hashLoginService);
+
+        // setup jersey (REST)
+        ServletHolder jerseyServletHolder = new ServletHolder(ServletContainer.class);
+        jerseyServletHolder.setInitParameter(
+                "com.sun.jersey.config.property.resourceConfigClass",
+                "com.sun.jersey.api.core.PackagesResourceConfig");
+        jerseyServletHolder.setInitParameter(
+                "com.sun.jersey.config.property.packages", "org.apache.hop.www.jaxrs");
+        root.addServlet(jerseyServletHolder, "/api/*");
+
+        // setup static resource serving
+        // ResourceHandler mobileResourceHandler = new ResourceHandler();
+        // mobileResourceHandler.setWelcomeFiles(new String[]{"index.html"});
+        // mobileResourceHandler.setResourceBase(getClass().getClassLoader().
+        // getResource("org.apache.hop/www/mobile").toExternalForm());
+        // Context mobileContext = new Context(contexts, "/mobile", Context.SESSIONS);
+        // mobileContext.setHandler(mobileResourceHandler);
+
+        // Allow png files to be shown for pipelines and workflows...
+        //
+        ResourceHandler resourceHandler = new ResourceHandler();
+        resourceHandler.setResourceBase("temp");
+        // add all handlers/contexts to server
+
+        // set up static servlet
+        ServletHolder staticHolder = new ServletHolder("static", DefaultServlet.class);
+        // resourceBase maps to the path relative to where carte is started
+        staticHolder.setInitParameter("resourceBase", "./static/");
+        staticHolder.setInitParameter("dirAllowed", "true");
+        staticHolder.setInitParameter("pathInfoOnly", "true");
+        root.addServlet(staticHolder, "/static/*");
+
+        HandlerList handlers = new HandlerList();
+        handlers.setHandlers(new Handler[]{resourceHandler, contexts});
+        securityHandler.setHandler(handlers);
+
+        server.setHandler(securityHandler);
+
+        // Start execution
+        createListeners();
+
+        server.start();
     }
 
-    Constraint constraint = new Constraint();
-    constraint.setName(Constraint.__BASIC_AUTH);
-    constraint.setRoles(roles.toArray(new String[roles.size()]));
-    constraint.setAuthenticate(true);
-
-    ConstraintMapping constraintMapping = new ConstraintMapping();
-    constraintMapping.setConstraint(constraint);
-    constraintMapping.setPathSpec("/*");
-
-    securityHandler.setConstraintMappings(new ConstraintMapping[] {constraintMapping});
-
-    // Add all the servlets defined in hop-servlets.xml ...
-    //
-    ContextHandlerCollection contexts = new ContextHandlerCollection();
-
-    // Root
-    //
-    ServletContextHandler root =
-        new ServletContextHandler(
-            contexts, GetRootServlet.CONTEXT_PATH, ServletContextHandler.SESSIONS);
-    GetRootServlet rootServlet = new GetRootServlet();
-    rootServlet.setJettyMode(true);
-    root.addServlet(new ServletHolder(rootServlet), "/*");
-
-    PluginRegistry pluginRegistry = PluginRegistry.getInstance();
-    List<IPlugin> plugins = pluginRegistry.getPlugins(HopServerPluginType.class);
-    for (IPlugin plugin : plugins) {
-
-      IHopServerPlugin servlet = pluginRegistry.loadClass(plugin, IHopServerPlugin.class);
-      servlet.setup(pipelineMap, workflowMap);
-      servlet.setJettyMode(true);
-
-      ServletContextHandler servletContext =
-          new ServletContextHandler(
-              contexts, getContextPath(servlet), ServletContextHandler.SESSIONS);
-      ServletHolder servletHolder = new ServletHolder((Servlet) servlet);
-      servletContext.addServlet(servletHolder, "/*");
+    public String getContextPath(IHopServerPlugin servlet) {
+        String contextPath = servlet.getContextPath();
+        return contextPath;
     }
 
-    // setup jersey (REST)
-    ServletHolder jerseyServletHolder = new ServletHolder(ServletContainer.class);
-    jerseyServletHolder.setInitParameter(
-        "com.sun.jersey.config.property.resourceConfigClass",
-        "com.sun.jersey.api.core.PackagesResourceConfig");
-    jerseyServletHolder.setInitParameter(
-        "com.sun.jersey.config.property.packages", "org.apache.hop.www.jaxrs");
-    root.addServlet(jerseyServletHolder, "/api/*");
-
-    // setup static resource serving
-    // ResourceHandler mobileResourceHandler = new ResourceHandler();
-    // mobileResourceHandler.setWelcomeFiles(new String[]{"index.html"});
-    // mobileResourceHandler.setResourceBase(getClass().getClassLoader().
-    // getResource("org.apache.hop/www/mobile").toExternalForm());
-    // Context mobileContext = new Context(contexts, "/mobile", Context.SESSIONS);
-    // mobileContext.setHandler(mobileResourceHandler);
-
-    // Allow png files to be shown for pipelines and workflows...
-    //
-    ResourceHandler resourceHandler = new ResourceHandler();
-    resourceHandler.setResourceBase("temp");
-    // add all handlers/contexts to server
-
-    // set up static servlet
-    ServletHolder staticHolder = new ServletHolder("static", DefaultServlet.class);
-    // resourceBase maps to the path relative to where carte is started
-    staticHolder.setInitParameter("resourceBase", "./static/");
-    staticHolder.setInitParameter("dirAllowed", "true");
-    staticHolder.setInitParameter("pathInfoOnly", "true");
-    root.addServlet(staticHolder, "/static/*");
-
-    HandlerList handlers = new HandlerList();
-    handlers.setHandlers(new Handler[] {resourceHandler, contexts});
-    securityHandler.setHandler(handlers);
-
-    server.setHandler(securityHandler);
-
-    // Start execution
-    createListeners();
-
-    server.start();
-  }
-
-  public String getContextPath(IHopServerPlugin servlet) {
-    String contextPath = servlet.getContextPath();
-    return contextPath;
-  }
-
-  public void join() throws InterruptedException {
-    server.join();
-  }
-
-  public void stopServer() {
-
-    webServerShutdownHook.setShuttingDown(true);
-
-    try {
-      ExtensionPointHandler.callExtensionPoint(
-          log, variables, HopExtensionPoint.HopServerShutdown.id, this);
-    } catch (HopException e) {
-      // Log error but continue regular operations to make sure HopServer can be shut down properly.
-      //
-      log.logError("Error calling extension point HopServerStartup", e);
+    public void join() throws InterruptedException {
+        server.join();
     }
 
-    try {
-      if (server != null) {
+    public void stopServer() {
 
-        // Stop the server...
-        //
-        server.stop();
-        HopEnvironment.shutdown();
-        if (webServerShutdownHandler != null) {
-          webServerShutdownHandler.shutdownWebServer();
+        webServerShutdownHook.setShuttingDown(true);
+
+        try {
+            ExtensionPointHandler.callExtensionPoint(
+                    log, variables, HopExtensionPoint.HopServerShutdown.id, this);
+        } catch (HopException e) {
+            // Log error but continue regular operations to make sure HopServer can be shut down properly.
+            //
+            log.logError("Error calling extension point HopServerStartup", e);
+        }
+
+        try {
+            if (server != null) {
+
+                // Stop the server...
+                //
+                server.stop();
+                HopEnvironment.shutdown();
+                if (webServerShutdownHandler != null) {
+                    webServerShutdownHandler.shutdownWebServer();
+                }
+            }
+        } catch (Exception e) {
+            log.logError(
+                    BaseMessages.getString(PKG, "WebServer.Error.FailedToStop.Title"),
+                    BaseMessages.getString(PKG, "WebServer.Error.FailedToStop.Msg", "" + e));
         }
-      }
-    } catch (Exception e) {
-      log.logError(
-          BaseMessages.getString(PKG, "WebServer.Error.FailedToStop.Title"),
-          BaseMessages.getString(PKG, "WebServer.Error.FailedToStop.Msg", "" + e));
     }
-  }
-
-  private void createListeners() {
-
-    ServerConnector connector = getConnector();
-
-    setupJettyOptions(connector);
-    connector.setPort(port);
-    connector.setHost(hostname);
-    connector.setName(BaseMessages.getString(PKG, "WebServer.Log.HopHTTPListener", hostname));
-    log.logBasic(BaseMessages.getString(PKG, "WebServer.Log.CreateListener", hostname, "" + port));
-
-    server.setConnectors(new Connector[] {connector});
-  }
-
-  private ServerConnector getConnector() {
-    if (sslConfig != null) {
-      log.logBasic(BaseMessages.getString(PKG, "WebServer.Log.SslModeUsing"));
-      SslConnectionFactory connector = new SslConnectionFactory();
-
-      SslContextFactory contextFactory = new SslContextFactory();
-      contextFactory.setKeyStoreResource(new PathResource(new File(sslConfig.getKeyStore())));
-      contextFactory.setKeyStorePassword(sslConfig.getKeyStorePassword());
-      contextFactory.setKeyManagerPassword(sslConfig.getKeyPassword());
-      contextFactory.setKeyStoreType(sslConfig.getKeyStoreType());
-      return new ServerConnector(server, connector);
-    } else {
-      return new ServerConnector(server);
+
+    private void createListeners() {
+
+        ServerConnector connector = getConnector();
+
+        setupJettyOptions(connector);
+        connector.setPort(port);
+        connector.setHost(hostname);
+        connector.setName(BaseMessages.getString(PKG, "WebServer.Log.HopHTTPListener", hostname));
+        log.logBasic(BaseMessages.getString(PKG, "WebServer.Log.CreateListener", hostname, "" + port));
+
+        server.setConnectors(new Connector[]{connector});
     }
-  }
-
-  /**
-   * Set up jetty options to the connector
-   *
-   * @param connector
-   */
-  protected void setupJettyOptions(ServerConnector connector) {
-    LowResourceMonitor lowResourceMonitor = new LowResourceMonitor(server);
-    if (validProperty(Const.HOP_SERVER_JETTY_ACCEPTORS)) {
-      server.addBean(
-          new ConnectionLimit(
-              Integer.parseInt(System.getProperty(Const.HOP_SERVER_JETTY_ACCEPTORS))));
-      log.logBasic(
-          BaseMessages.getString(
-              PKG, "WebServer.Log.ConfigOptions", "acceptors", connector.getAcceptors()));
+
+    private ServerConnector getConnector() {
+        if (sslConfig != null) {
+            log.logBasic(BaseMessages.getString(PKG, "WebServer.Log.SslModeUsing"));
+
+            HttpConfiguration httpConfig = new HttpConfiguration();
+            httpConfig.setSecureScheme("https");
+            httpConfig.setSecurePort(port);
+
+            String keyStorePassword = Encr.decryptPasswordOptionallyEncrypted(sslConfig.getKeyStorePassword());
+            String keyPassword = Encr.decryptPasswordOptionallyEncrypted(sslConfig.getKeyPassword());
+
+            SslContextFactory.Client factory = new SslContextFactory.Client();
+            factory.setKeyStoreResource(new PathResource(new File(sslConfig.getKeyStore())));
+            factory.setKeyStorePassword(keyStorePassword);
+            factory.setKeyManagerPassword(keyPassword);
+            factory.setKeyStoreType(sslConfig.getKeyStoreType());
+            factory.setTrustStoreResource(new PathResource(new File(sslConfig.getKeyStore())));
+            factory.setTrustStorePassword(keyStorePassword);
+
+            HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
+            httpsConfig.addCustomizer(new SecureRequestCustomizer());
+
+            ServerConnector sslConnector = new ServerConnector(server,
+                    new SslConnectionFactory(factory, HttpVersion.HTTP_1_1.asString()),
+                    new HttpConnectionFactory(httpsConfig));
+            sslConnector.setPort(port);
+
+            return sslConnector;
+        } else {
+            return new ServerConnector(server);
+        }
     }
 
-    if (validProperty(Const.HOP_SERVER_JETTY_ACCEPT_QUEUE_SIZE)) {
-      connector.setAcceptQueueSize(
-          Integer.parseInt(System.getProperty(Const.HOP_SERVER_JETTY_ACCEPT_QUEUE_SIZE)));
-      log.logBasic(
-          BaseMessages.getString(
-              PKG,
-              "WebServer.Log.ConfigOptions",
-              "acceptQueueSize",
-              connector.getAcceptQueueSize()));
+    /**
+     * Set up jetty options to the connector
+     *
+     * @param connector
+     */
+    protected void setupJettyOptions(ServerConnector connector) {
+        LowResourceMonitor lowResourceMonitor = new LowResourceMonitor(server);
+        if (validProperty(Const.HOP_SERVER_JETTY_ACCEPTORS)) {
+            server.addBean(
+                    new ConnectionLimit(
+                            Integer.parseInt(System.getProperty(Const.HOP_SERVER_JETTY_ACCEPTORS))));
+            log.logBasic(
+                    BaseMessages.getString(
+                            PKG, "WebServer.Log.ConfigOptions", "acceptors", connector.getAcceptors()));
+        }
+
+        if (validProperty(Const.HOP_SERVER_JETTY_ACCEPT_QUEUE_SIZE)) {
+            connector.setAcceptQueueSize(
+                    Integer.parseInt(System.getProperty(Const.HOP_SERVER_JETTY_ACCEPT_QUEUE_SIZE)));
+            log.logBasic(
+                    BaseMessages.getString(
+                            PKG,
+                            "WebServer.Log.ConfigOptions",
+                            "acceptQueueSize",
+                            connector.getAcceptQueueSize()));
+        }
+
+        if (validProperty(Const.HOP_SERVER_JETTY_RES_MAX_IDLE_TIME)) {
+            connector.setIdleTimeout(
+                    Integer.parseInt(System.getProperty(Const.HOP_SERVER_JETTY_RES_MAX_IDLE_TIME)));
+            log.logBasic(
+                    BaseMessages.getString(
+                            PKG,
+                            "WebServer.Log.ConfigOptions",
+                            "lowResourcesMaxIdleTime",
+                            connector.getIdleTimeout()));
+        }
     }
 
-    if (validProperty(Const.HOP_SERVER_JETTY_RES_MAX_IDLE_TIME)) {
-      connector.setIdleTimeout(
-          Integer.parseInt(System.getProperty(Const.HOP_SERVER_JETTY_RES_MAX_IDLE_TIME)));
-      log.logBasic(
-          BaseMessages.getString(
-              PKG,
-              "WebServer.Log.ConfigOptions",
-              "lowResourcesMaxIdleTime",
-              connector.getIdleTimeout()));
+    /**
+     * Checks if the property is not null or not empty String that can be parseable as int and returns
+     * true if it is, otherwise false
+     *
+     * @param property the property to check
+     * @return true if the property is not null or not empty String that can be parseable as int,
+     * false otherwise
+     */
+    private boolean validProperty(String property) {
+        boolean isValid = false;
+        if (System.getProperty(property) != null && System.getProperty(property).length() > 0) {
+            try {
+                Integer.parseInt(System.getProperty(property));
+                isValid = true;
+            } catch (NumberFormatException nmbfExc) {
+                log.logBasic(
+                        BaseMessages.getString(
+                                PKG, "WebServer.Log.ConfigOptionsInvalid", property, System.getProperty(property)));
+            }
+        }
+        return isValid;
     }
-  }
-
-  /**
-   * Checks if the property is not null or not empty String that can be parseable as int and returns
-   * true if it is, otherwise false
-   *
-   * @param property the property to check
-   * @return true if the property is not null or not empty String that can be parseable as int,
-   *     false otherwise
-   */
-  private boolean validProperty(String property) {
-    boolean isValid = false;
-    if (System.getProperty(property) != null && System.getProperty(property).length() > 0) {
-      try {
-        Integer.parseInt(System.getProperty(property));
-        isValid = true;
-      } catch (NumberFormatException nmbfExc) {
-        log.logBasic(
-            BaseMessages.getString(
-                PKG, "WebServer.Log.ConfigOptionsInvalid", property, System.getProperty(property)));
-      }
+
+    /**
+     * @return the hostname
+     */
+    public String getHostname() {
+        return hostname;
     }
-    return isValid;
-  }
-
-  /** @return the hostname */
-  public String getHostname() {
-    return hostname;
-  }
-
-  /** @param hostname the hostname to set */
-  public void setHostname(String hostname) {
-    this.hostname = hostname;
-  }
-
-  public String getPasswordFile() {
-    return passwordFile;
-  }
-
-  public void setPasswordFile(String passwordFile) {
-    this.passwordFile = passwordFile;
-  }
-
-  public ILogChannel getLog() {
-    return log;
-  }
-
-  public void setLog(ILogChannel log) {
-    this.log = log;
-  }
-
-  public PipelineMap getPipelineMap() {
-    return pipelineMap;
-  }
-
-  public void setPipelineMap(PipelineMap pipelineMap) {
-    this.pipelineMap = pipelineMap;
-  }
-
-  public WorkflowMap getWorkflowMap() {
-    return workflowMap;
-  }
-
-  public void setWorkflowMap(WorkflowMap workflowMap) {
-    this.workflowMap = workflowMap;
-  }
-
-  public int getPort() {
-    return port;
-  }
-
-  public void setPort(int port) {
-    this.port = port;
-  }
-
-  public void setServer(Server server) {
-    this.server = server;
-  }
-
-  /**
-   * Gets variables
-   *
-   * @return value of variables
-   */
-  public IVariables getVariables() {
-    return variables;
-  }
-
-  /** @param variables The variables to set */
-  public void setVariables(IVariables variables) {
-    this.variables = variables;
-  }
-
-  /**
-   * Can be used to override the default shutdown behavior of performing a System.exit
-   *
-   * @param webServerShutdownHandler
-   */
-  public void setWebServerShutdownHandler(IWebServerShutdownHandler webServerShutdownHandler) {
-    this.webServerShutdownHandler = webServerShutdownHandler;
-  }
-
-  public int defaultDetectionTimer() {
-    String sDetectionTimer = System.getProperty(Const.HOP_SERVER_DETECTION_TIMER);
-
-    if (sDetectionTimer != null) {
-      return Integer.parseInt(sDetectionTimer);
-    } else {
-      return DEFAULT_DETECTION_TIMER;
+
+    /**
+     * @param hostname the hostname to set
+     */
+    public void setHostname(String hostname) {
+        this.hostname = hostname;
+    }
+
+    public String getPasswordFile() {
+        return passwordFile;
+    }
+
+    public void setPasswordFile(String passwordFile) {
+        this.passwordFile = passwordFile;
+    }
+
+    public ILogChannel getLog() {
+        return log;
+    }
+
+    public void setLog(ILogChannel log) {
+        this.log = log;
+    }
+
+    public PipelineMap getPipelineMap() {
+        return pipelineMap;
+    }
+
+    public void setPipelineMap(PipelineMap pipelineMap) {
+        this.pipelineMap = pipelineMap;
+    }
+
+    public WorkflowMap getWorkflowMap() {
+        return workflowMap;
+    }
+
+    public void setWorkflowMap(WorkflowMap workflowMap) {
+        this.workflowMap = workflowMap;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    public void setServer(Server server) {
+        this.server = server;
+    }
+
+    /**
+     * Gets variables
+     *
+     * @return value of variables
+     */
+    public IVariables getVariables() {
+        return variables;
+    }
+
+    /**
+     * @param variables The variables to set
+     */
+    public void setVariables(IVariables variables) {
+        this.variables = variables;
+    }
+
+    /**
+     * Can be used to override the default shutdown behavior of performing a System.exit
+     *
+     * @param webServerShutdownHandler
+     */
+    public void setWebServerShutdownHandler(IWebServerShutdownHandler webServerShutdownHandler) {
+        this.webServerShutdownHandler = webServerShutdownHandler;
+    }
+
+    public int defaultDetectionTimer() {
+        String sDetectionTimer = System.getProperty(Const.HOP_SERVER_DETECTION_TIMER);
+
+        if (sDetectionTimer != null) {
+            return Integer.parseInt(sDetectionTimer);
+        } else {
+            return DEFAULT_DETECTION_TIMER;
+        }
     }
-  }
 }
diff --git a/engine/src/test/java/org/apache/hop/server/HopServerTest.java b/engine/src/test/java/org/apache/hop/server/HopServerTest.java
index c6a90f9..d5e0a79 100644
--- a/engine/src/test/java/org/apache/hop/server/HopServerTest.java
+++ b/engine/src/test/java/org/apache/hop/server/HopServerTest.java
@@ -6,7 +6,7 @@
  * (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
+ *       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,
@@ -80,7 +80,7 @@ public class HopServerTest {
   }
 
   @Before
-  public void init() throws IOException {
+  public void init() throws Exception {
     ServerConnectionManager connectionManager = ServerConnectionManager.getInstance();
     HttpClient httpClient = spy(connectionManager.createHttpClient());