ManagerServlet.java

/*
 * 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.catalina.manager;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.naming.Binding;
import javax.naming.NamingEnumeration;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.UnavailableException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.apache.catalina.Container;
import org.apache.catalina.ContainerServlet;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Host;
import org.apache.catalina.Manager;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.Session;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.startup.ExpandWar;
import org.apache.catalina.util.ContextName;
import org.apache.catalina.util.IOTools;
import org.apache.catalina.util.ServerInfo;
import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.apache.tomcat.util.Diagnostics;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.IntrospectionUtils;
import org.apache.tomcat.util.buf.StringUtils;
import org.apache.tomcat.util.modeler.Registry;
import org.apache.tomcat.util.net.SSLContext;
import org.apache.tomcat.util.net.SSLHostConfig;
import org.apache.tomcat.util.net.SSLHostConfigCertificate;
import org.apache.tomcat.util.net.SSLUtilBase;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.util.security.Escape;


/**
 * Servlet that enables remote management of the web applications installed within the same virtual host as this web
 * application is. Normally, this functionality will be protected by a security constraint in the web application
 * deployment descriptor. However, this requirement can be relaxed during testing.
 * <p>
 * This servlet examines the value returned by <code>getPathInfo()</code> and related query parameters to determine what
 * action is being requested. The following actions and parameters (starting after the servlet path) are supported:
 * <ul>
 * <li><b>/deploy?config={config-url}</b> - Install and start a new web application, based on the contents of the
 * context configuration file found at the specified URL. The <code>docBase</code> attribute of the context
 * configuration file is used to locate the actual WAR or directory containing the application.</li>
 * <li><b>/deploy?config={config-url}&amp;war={war-url}/</b> - Install and start a new web application, based on the
 * contents of the context configuration file found at <code>{config-url}</code>, overriding the <code>docBase</code>
 * attribute with the contents of the web application archive found at <code>{war-url}</code>.</li>
 * <li><b>/deploy?path=/xxx&amp;war={war-url}</b> - Install and start a new web application attached to context path
 * <code>/xxx</code>, based on the contents of the web application archive found at the specified URL.</li>
 * <li><b>/list</b> - List the context paths of all currently installed web applications for this virtual host. Each
 * context will be listed with the following format <code>path:status:sessions</code>. Where path is the context path.
 * Status is either running or stopped. Sessions is the number of active Sessions.</li>
 * <li><b>/reload?path=/xxx</b> - Reload the Java classes and resources for the application at the specified path.</li>
 * <li><b>/resources?type=xxxx</b> - Enumerate the available global JNDI resources, optionally limited to those of the
 * specified type (fully qualified Java class name), if available.</li>
 * <li><b>/serverinfo</b> - Display system OS and JVM properties.
 * <li><b>/sessions</b> - Deprecated. Use expire.
 * <li><b>/expire?path=/xxx</b> - List session idle time information about the web application attached to context path
 * <code>/xxx</code> for this virtual host.</li>
 * <li><b>/expire?path=/xxx&amp;idle=mm</b> - Expire sessions for the context path <code>/xxx</code> which were idle for
 * at least mm minutes.</li>
 * <li><b>/sslConnectorCiphers</b> - Display diagnostic info on SSL/TLS ciphers that are currently configured for each
 * connector.
 * <li><b>/start?path=/xxx</b> - Start the web application attached to context path <code>/xxx</code> for this virtual
 * host.</li>
 * <li><b>/stop?path=/xxx</b> - Stop the web application attached to context path <code>/xxx</code> for this virtual
 * host.</li>
 * <li><b>/threaddump</b> - Write a JVM thread dump.</li>
 * <li><b>/undeploy?path=/xxx</b> - Shutdown and remove the web application attached to context path <code>/xxx</code>
 * for this virtual host, and remove the underlying WAR file or document base directory. (<em>NOTE</em> - This is only
 * allowed if the WAR file or document base is stored in the <code>appBase</code> directory of this host, typically as a
 * result of being placed there via the <code>/deploy</code> command.</li>
 * <li><b>/vminfo</b> - Write some VM info.</li>
 * <li><b>/save</b> - Save the current server configuration to server.xml</li>
 * <li><b>/save?path=/xxx</b> - Save the context configuration for the web application deployed with path
 * <code>/xxx</code> to an appropriately named context.xml file in the <code>xmlBase</code> for the associated
 * Host.</li>
 * </ul>
 * <p>
 * Use <code>path=/</code> for the ROOT context.
 * </p>
 * <p>
 * The syntax of the URL for a web application archive must conform to one of the following patterns to be successfully
 * deployed:
 * </p>
 * <ul>
 * <li><b>file:/absolute/path/to/a/directory</b> - You can specify the absolute path of a directory that contains the
 * unpacked version of a web application. This directory will be attached to the context path you specify without any
 * changes.</li>
 * </ul>
 * <p>
 * <b>NOTE</b> - Attempting to reload or remove the application containing this servlet itself will not succeed.
 * Therefore, this servlet should generally be deployed as a separate web application within the virtual host to be
 * managed.
 * <p>
 * The following servlet initialization parameters are recognized:
 * <ul>
 * <li><b>debug</b> - The debugging detail level that controls the amount of information that is logged by this servlet.
 * Default is zero.
 * </ul>
 *
 * @author Craig R. McClanahan
 * @author Remy Maucherat
 */
public class ManagerServlet extends HttpServlet implements ContainerServlet {

    private static final long serialVersionUID = 1L;

    // ----------------------------------------------------- Instance Variables


    /**
     * Path where context descriptors should be deployed.
     */
    protected File configBase = null;


    /**
     * The Context container associated with our web application.
     */
    protected transient Context context = null;


    /**
     * The debugging detail level for this servlet.
     */
    protected int debug = 1;


    /**
     * Path used to store revisions of webapps.
     */
    protected File versioned = null;


    /**
     * The associated host.
     */
    protected transient Host host = null;


    /**
     * MBean server.
     */
    protected transient MBeanServer mBeanServer = null;


    /**
     * The associated deployer ObjectName.
     */
    protected ObjectName oname = null;


    /**
     * The global JNDI <code>NamingContext</code> for this server, if available.
     */
    protected transient javax.naming.Context global = null;


    /**
     * The string manager for this package.
     */
    protected static final StringManager sm = StringManager.getManager(Constants.Package);


    /**
     * The Wrapper container associated with this servlet.
     */
    protected transient Wrapper wrapper = null;


    // ----------------------------------------------- ContainerServlet Methods


    @Override
    public Wrapper getWrapper() {
        return this.wrapper;
    }


    @Override
    public void setWrapper(Wrapper wrapper) {

        this.wrapper = wrapper;
        if (wrapper == null) {
            context = null;
            host = null;
            oname = null;
        } else {
            context = (Context) wrapper.getParent();
            host = (Host) context.getParent();
            Engine engine = (Engine) host.getParent();
            String name = engine.getName() + ":type=Deployer,host=" + host.getName();
            try {
                oname = new ObjectName(name);
            } catch (Exception e) {
                log(sm.getString("managerServlet.objectNameFail", name), e);
            }
        }

        // Retrieve the MBean server
        mBeanServer = Registry.getRegistry(null, null).getMBeanServer();

    }


    // --------------------------------------------------------- Public Methods


    @Override
    public void destroy() {

        // No actions necessary

    }


    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {

        StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales());

        // Identify the request parameters that we need
        String command = request.getPathInfo();
        if (command == null) {
            command = request.getServletPath();
        }

        String path = request.getParameter("path");
        String war = request.getParameter("war");
        String config = request.getParameter("config");
        ContextName cn = null;
        if (path != null) {
            cn = new ContextName(path, request.getParameter("version"));
        } else if (config != null) {
            cn = ContextName.extractFromPath(config);
        } else if (war != null) {
            cn = ContextName.extractFromPath(war);
        }

        String type = request.getParameter("type");
        String tag = request.getParameter("tag");
        boolean update = false;
        if (request.getParameter("update") != null && request.getParameter("update").equals("true")) {
            update = true;
        }
        String tlsHostName = request.getParameter("tlsHostName");

        boolean statusLine = false;
        if ("true".equals(request.getParameter("statusLine"))) {
            statusLine = true;
        }

        // Prepare our output writer to generate the response message
        response.setContentType("text/plain; charset=" + Constants.CHARSET);
        // Stop older versions of IE thinking they know best. We set text/plain
        // in the line above for a reason. IE's behaviour is unwanted at best
        // and dangerous at worst.
        response.setHeader("X-Content-Type-Options", "nosniff");
        PrintWriter writer = response.getWriter();

        // Process the requested command
        if (command == null) {
            writer.println(smClient.getString("managerServlet.noCommand"));
        } else if (command.equals("/deploy")) {
            if (war != null || config != null) {
                deploy(writer, config, cn, war, update, smClient);
            } else if (tag != null) {
                deploy(writer, cn, tag, smClient);
            } else {
                writer.println(smClient.getString("managerServlet.invalidCommand", command));
            }
        } else if (command.equals("/list")) {
            list(writer, smClient);
        } else if (command.equals("/reload")) {
            reload(writer, cn, smClient);
        } else if (command.equals("/resources")) {
            resources(writer, type, smClient);
        } else if (command.equals("/save")) {
            save(writer, path, smClient);
        } else if (command.equals("/serverinfo")) {
            serverinfo(writer, smClient);
        } else if (command.equals("/sessions")) {
            expireSessions(writer, cn, request, smClient);
        } else if (command.equals("/expire")) {
            expireSessions(writer, cn, request, smClient);
        } else if (command.equals("/start")) {
            start(writer, cn, smClient);
        } else if (command.equals("/stop")) {
            stop(writer, cn, smClient);
        } else if (command.equals("/undeploy")) {
            undeploy(writer, cn, smClient);
        } else if (command.equals("/findleaks")) {
            findleaks(statusLine, writer, smClient);
        } else if (command.equals("/vminfo")) {
            vmInfo(writer, smClient, request.getLocales());
        } else if (command.equals("/threaddump")) {
            threadDump(writer, smClient, request.getLocales());
        } else if (command.equals("/sslConnectorCiphers")) {
            sslConnectorCiphers(writer, smClient);
        } else if (command.equals("/sslConnectorCerts")) {
            sslConnectorCerts(writer, smClient);
        } else if (command.equals("/sslConnectorTrustedCerts")) {
            sslConnectorTrustedCerts(writer, smClient);
        } else if (command.equals("/sslReload")) {
            sslReload(writer, tlsHostName, smClient);
        } else {
            writer.println(smClient.getString("managerServlet.unknownCommand", command));
        }

        // Finish up the response
        writer.flush();
        writer.close();

    }


    @Override
    public void doPut(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {

        StringManager smClient = StringManager.getManager(Constants.Package, request.getLocales());

        // Identify the request parameters that we need
        String command = request.getPathInfo();
        if (command == null) {
            command = request.getServletPath();
        }
        String path = request.getParameter("path");
        ContextName cn = null;
        if (path != null) {
            cn = new ContextName(path, request.getParameter("version"));
        }
        String config = request.getParameter("config");
        String tag = request.getParameter("tag");
        boolean update = false;
        if (request.getParameter("update") != null && request.getParameter("update").equals("true")) {
            update = true;
        }

        // Prepare our output writer to generate the response message
        response.setContentType("text/plain;charset=" + Constants.CHARSET);
        // Stop older versions of IE thinking they know best. We set text/plain
        // in the line above for a reason. IE's behaviour is unwanted at best
        // and dangerous at worst.
        response.setHeader("X-Content-Type-Options", "nosniff");
        PrintWriter writer = response.getWriter();

        // Process the requested command
        if (command == null) {
            writer.println(smClient.getString("managerServlet.noCommand"));
        } else if (command.equals("/deploy")) {
            deploy(writer, config, cn, tag, update, request, smClient);
        } else {
            writer.println(smClient.getString("managerServlet.unknownCommand", command));
        }

        // Finish up the response
        writer.flush();
        writer.close();

    }


    @Override
    public void init() throws ServletException {

        // Ensure that our ContainerServlet properties have been set
        if (wrapper == null || context == null) {
            throw new UnavailableException(sm.getString("managerServlet.noWrapper"));
        }

        // Set our properties from the initialization parameters
        String value = null;
        try {
            value = getServletConfig().getInitParameter("debug");
            debug = Integer.parseInt(value);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
        }

        // Acquire global JNDI resources if available
        Server server = ((Engine) host.getParent()).getService().getServer();
        if (server != null) {
            global = server.getGlobalNamingContext();
        }

        // Calculate the directory into which we will be deploying applications
        versioned = (File) getServletContext().getAttribute(ServletContext.TEMPDIR);

        configBase = new File(context.getCatalinaBase(), "conf");
        Container container = context;
        Container host = null;
        Container engine = null;
        while (container != null) {
            if (container instanceof Host) {
                host = container;
            }
            if (container instanceof Engine) {
                engine = container;
            }
            container = container.getParent();
        }
        if (engine != null) {
            configBase = new File(configBase, engine.getName());
        }
        if (host != null) {
            configBase = new File(configBase, host.getName());
        }
        // Note: The directory must exist for this to work.

        // Log debugging messages as necessary
        if (debug >= 1) {
            log("init: Associated with Deployer '" + oname + "'");
            if (global != null) {
                log("init: Global resources are available");
            }
        }

    }


    // -------------------------------------------------------- Private Methods


    /**
     * Find potential memory leaks caused by web application reload.
     *
     * @param statusLine Print a status line
     * @param writer     The output writer
     * @param smClient   StringManager for the client's locale
     */
    protected void findleaks(boolean statusLine, PrintWriter writer, StringManager smClient) {

        if (!(host instanceof StandardHost)) {
            writer.println(smClient.getString("managerServlet.findleaksFail"));
            return;
        }

        String[] results = ((StandardHost) host).findReloadedContextMemoryLeaks();

        if (results.length > 0) {
            if (statusLine) {
                writer.println(smClient.getString("managerServlet.findleaksList"));
            }
            for (String result : results) {
                if (result.isEmpty()) {
                    result = "/";
                }
                writer.println(result);
            }
        } else if (statusLine) {
            writer.println(smClient.getString("managerServlet.findleaksNone"));
        }
    }


    protected void sslReload(PrintWriter writer, String tlsHostName, StringManager smClient) {
        Connector connectors[] = getConnectors();
        boolean found = false;
        for (Connector connector : connectors) {
            if (Boolean.TRUE.equals(connector.getProperty("SSLEnabled"))) {
                ProtocolHandler protocol = connector.getProtocolHandler();
                if (protocol instanceof AbstractHttp11Protocol<?>) {
                    AbstractHttp11Protocol<?> http11Protoocol = (AbstractHttp11Protocol<?>) protocol;
                    if (tlsHostName == null || tlsHostName.length() == 0) {
                        found = true;
                        http11Protoocol.reloadSslHostConfigs();
                    } else {
                        SSLHostConfig[] sslHostConfigs = http11Protoocol.findSslHostConfigs();
                        for (SSLHostConfig sslHostConfig : sslHostConfigs) {
                            // tlsHostName is as provided by the user so use a case insensitive
                            // comparison as host names are case insensitive.
                            if (sslHostConfig.getHostName().equalsIgnoreCase(tlsHostName)) {
                                found = true;
                                http11Protoocol.reloadSslHostConfig(tlsHostName);
                            }
                        }
                    }
                }
            }
        }
        if (found) {
            if (tlsHostName == null || tlsHostName.length() == 0) {
                writer.println(smClient.getString("managerServlet.sslReloadAll"));
            } else {
                writer.println(smClient.getString("managerServlet.sslReload", tlsHostName));
            }
        } else {
            writer.println(smClient.getString("managerServlet.sslReloadFail"));
        }
    }


    /**
     * Write some VM info.
     *
     * @param writer           The output writer
     * @param smClient         StringManager for the client's locale
     * @param requestedLocales the client's locales
     */
    protected void vmInfo(PrintWriter writer, StringManager smClient, Enumeration<Locale> requestedLocales) {
        writer.println(smClient.getString("managerServlet.vminfo"));
        writer.print(Diagnostics.getVMInfo(requestedLocales));
    }

    /**
     * Write a JVM thread dump.
     *
     * @param writer           The output writer
     * @param smClient         StringManager for the client's locale
     * @param requestedLocales the client's locales
     */
    protected void threadDump(PrintWriter writer, StringManager smClient, Enumeration<Locale> requestedLocales) {
        writer.println(smClient.getString("managerServlet.threaddump"));
        writer.print(Diagnostics.getThreadDump(requestedLocales));
    }


    protected void sslConnectorCiphers(PrintWriter writer, StringManager smClient) {
        writer.println(smClient.getString("managerServlet.sslConnectorCiphers"));
        Map<String,List<String>> connectorCiphers = getConnectorCiphers(smClient);
        for (Map.Entry<String,List<String>> entry : connectorCiphers.entrySet()) {
            writer.println(entry.getKey());
            for (String cipher : entry.getValue()) {
                writer.print("  ");
                writer.println(cipher);
            }
        }
    }


    private void sslConnectorCerts(PrintWriter writer, StringManager smClient) {
        writer.println(smClient.getString("managerServlet.sslConnectorCerts"));
        Map<String,List<String>> connectorCerts = getConnectorCerts(smClient);
        for (Map.Entry<String,List<String>> entry : connectorCerts.entrySet()) {
            writer.println(entry.getKey());
            for (String cert : entry.getValue()) {
                writer.println(cert);
            }
        }
    }


    private void sslConnectorTrustedCerts(PrintWriter writer, StringManager smClient) {
        writer.println(smClient.getString("managerServlet.sslConnectorTrustedCerts"));
        Map<String,List<String>> connectorTrustedCerts = getConnectorTrustedCerts(smClient);
        for (Map.Entry<String,List<String>> entry : connectorTrustedCerts.entrySet()) {
            writer.println(entry.getKey());
            for (String cert : entry.getValue()) {
                writer.println(cert);
            }
        }
    }


    /**
     * Store server configuration.
     *
     * @param writer   Destination for any user message(s) during this operation
     * @param path     Optional context path to save
     * @param smClient i18n support for current client's locale
     */
    protected synchronized void save(PrintWriter writer, String path, StringManager smClient) {

        ObjectName storeConfigOname;
        try {
            // Note: Hard-coded domain used since this object is per Server/JVM
            storeConfigOname = new ObjectName("Catalina:type=StoreConfig");
        } catch (MalformedObjectNameException e) {
            // Should never happen. The name above is valid.
            log(sm.getString("managerServlet.exception"), e);
            writer.println(smClient.getString("managerServlet.exception", e.toString()));
            return;
        }

        if (!mBeanServer.isRegistered(storeConfigOname)) {
            writer.println(smClient.getString("managerServlet.storeConfig.noMBean", storeConfigOname));
            return;
        }

        if (path == null || path.length() == 0 || !path.startsWith("/")) {
            try {
                mBeanServer.invoke(storeConfigOname, "storeConfig", null, null);
                writer.println(smClient.getString("managerServlet.saved"));
            } catch (Exception e) {
                log(sm.getString("managerServlet.error.storeConfig"), e);
                writer.println(smClient.getString("managerServlet.exception", e.toString()));
            }
        } else {
            String contextPath = path;
            if (path.equals("/")) {
                contextPath = "";
            }
            Context context = (Context) host.findChild(contextPath);
            if (context == null) {
                writer.println(smClient.getString("managerServlet.noContext", path));
                return;
            }
            try {
                Boolean result = (Boolean) mBeanServer.invoke(storeConfigOname, "store", new Object[] { context },
                        new String[] { "org.apache.catalina.Context" });
                if (result.booleanValue()) {
                    writer.println(smClient.getString("managerServlet.savedContext", path));
                } else {
                    writer.println(smClient.getString("managerServlet.savedContextFail", path));
                }
            } catch (Exception e) {
                log(sm.getString("managerServlet.error.storeContextConfig", path), e);
                writer.println(smClient.getString("managerServlet.exception", e.toString()));
            }
        }
    }


    /**
     * Deploy a web application archive (included in the current request) at the specified context path.
     *
     * @param writer   Writer to render results to
     * @param config   URL of the context configuration file to be installed
     * @param cn       Name of the application to be installed
     * @param tag      Tag to be associated with the webapp
     * @param update   Flag that indicates that any existing app should be replaced
     * @param request  Servlet request we are processing
     * @param smClient i18n messages using the locale of the client
     */
    protected void deploy(PrintWriter writer, String config, ContextName cn, String tag, boolean update,
            HttpServletRequest request, StringManager smClient) {

        if (config != null && config.length() == 0) {
            config = null;
        }

        if (debug >= 1) {
            if (config == null) {
                log("deploy: Deploying web application '" + cn + "'");
            } else {
                log("deploy: Deploying web application '" + cn + "' " + "with context configuration at '" + config +
                        "'");
            }
        }

        // Validate the requested context path
        if (!validateContextName(cn, writer, smClient)) {
            return;
        }
        String name = cn.getName();
        String baseName = cn.getBaseName();
        String displayPath = cn.getDisplayName();

        // If app exists deployment can only proceed if update is true
        // Note existing WAR will be deleted and then replaced
        Context context = (Context) host.findChild(name);
        if (context != null && !update) {
            writer.println(smClient.getString("managerServlet.alreadyContext", displayPath));
            return;
        }

        if (config != null && config.startsWith("file:")) {
            config = config.substring("file:".length());
        }

        File deployedWar = new File(host.getAppBaseFile(), baseName + ".war");

        // Determine full path for uploaded WAR
        File uploadedWar;
        if (tag == null) {
            if (update) {
                // Append ".tmp" to the file name so it won't get deployed if auto
                // deployment is enabled. It also means the old war won't get
                // deleted if the upload fails
                uploadedWar = new File(deployedWar.getAbsolutePath() + ".tmp");
                if (uploadedWar.exists() && !uploadedWar.delete()) {
                    writer.println(smClient.getString("managerServlet.deleteFail", uploadedWar));
                }
            } else {
                uploadedWar = deployedWar;
            }
        } else {
            File uploadPath = new File(versioned, tag);
            if (!uploadPath.mkdirs() && !uploadPath.isDirectory()) {
                writer.println(smClient.getString("managerServlet.mkdirFail", uploadPath));
                return;
            }
            uploadedWar = new File(uploadPath, baseName + ".war");
        }
        if (debug >= 2) {
            log("Uploading WAR file to " + uploadedWar);
        }

        try {
            if (tryAddServiced(name)) {
                try {
                    if (config != null) {
                        if (!configBase.mkdirs() && !configBase.isDirectory()) {
                            writer.println(smClient.getString("managerServlet.mkdirFail", configBase));
                            return;
                        }
                        if (ExpandWar.copy(new File(config), new File(configBase, baseName + ".xml")) == false) {
                            throw new Exception(sm.getString("managerServlet.copyError", config));
                        }
                    }
                    // Upload WAR
                    uploadWar(writer, request, uploadedWar, smClient);
                    if (update && tag == null) {
                        if (deployedWar.exists() && !deployedWar.delete()) {
                            writer.println(smClient.getString("managerServlet.deleteFail", deployedWar));
                            return;
                        }
                        // Rename uploaded WAR file
                        if (!uploadedWar.renameTo(deployedWar)) {
                            writer.println(smClient.getString("managerServlet.renameFail", uploadedWar, deployedWar));
                            return;
                        }
                    }
                    if (tag != null) {
                        // Copy WAR to the host's appBase
                        ExpandWar.copy(uploadedWar, deployedWar);
                    }
                } finally {
                    removeServiced(name);
                }
                // Perform new deployment
                check(name);
            } else {
                writer.println(smClient.getString("managerServlet.inService", displayPath));
            }
        } catch (Exception e) {
            log(sm.getString("managerServlet.error.deploy", displayPath), e);
            writer.println(smClient.getString("managerServlet.exception", e.toString()));
            return;
        }

        writeDeployResult(writer, smClient, name, displayPath);
    }


    /**
     * Install an application for the specified path from the specified web application archive.
     *
     * @param writer   Writer to render results to
     * @param tag      Revision tag to deploy from
     * @param cn       Name of the application to be installed
     * @param smClient i18n messages using the locale of the client
     */
    protected void deploy(PrintWriter writer, ContextName cn, String tag, StringManager smClient) {

        // NOTE: It is assumed that update is always true in this method.

        // Validate the requested context path
        if (!validateContextName(cn, writer, smClient)) {
            return;
        }

        String baseName = cn.getBaseName();
        String name = cn.getName();
        String displayPath = cn.getDisplayName();

        // Find the local WAR file
        File localWar = new File(new File(versioned, tag), baseName + ".war");

        File deployedWar = new File(host.getAppBaseFile(), baseName + ".war");

        // Copy WAR to appBase
        try {
            if (tryAddServiced(name)) {
                try {
                    if (!deployedWar.delete()) {
                        writer.println(smClient.getString("managerServlet.deleteFail", deployedWar));
                        return;
                    }
                    ExpandWar.copy(localWar, deployedWar);
                } finally {
                    removeServiced(name);
                }
                // Perform new deployment
                check(name);
            } else {
                writer.println(smClient.getString("managerServlet.inService", displayPath));
            }
        } catch (Exception e) {
            log(sm.getString("managerServlet.error.deploy", displayPath), e);
            writer.println(smClient.getString("managerServlet.exception", e.toString()));
            return;
        }

        writeDeployResult(writer, smClient, name, displayPath);
    }


    /**
     * Install an application for the specified path from the specified web application archive.
     *
     * @param writer   Writer to render results to
     * @param config   URL of the context configuration file to be installed
     * @param cn       Name of the application to be installed
     * @param war      URL of the web application archive to be installed
     * @param update   true to override any existing webapp on the path
     * @param smClient i18n messages using the locale of the client
     */
    protected void deploy(PrintWriter writer, String config, ContextName cn, String war, boolean update,
            StringManager smClient) {

        if (config != null && config.length() == 0) {
            config = null;
        }
        if (war != null && war.length() == 0) {
            war = null;
        }

        if (debug >= 1) {
            if (config != null) {
                if (war != null) {
                    log("install: Installing context configuration at '" + config + "' from '" + war + "'");
                } else {
                    log("install: Installing context configuration at '" + config + "'");
                }
            } else {
                if (cn != null) {
                    log("install: Installing web application '" + cn + "' from '" + war + "'");
                } else {
                    log("install: Installing web application from '" + war + "'");
                }
            }
        }

        if (!validateContextName(cn, writer, smClient)) {
            return;
        }
        @SuppressWarnings("null") // checked in call above
        String name = cn.getName();
        String baseName = cn.getBaseName();
        String displayPath = cn.getDisplayName();

        // If app exists deployment can only proceed if update is true
        // Note existing files will be deleted and then replaced
        Context context = (Context) host.findChild(name);
        if (context != null && !update) {
            writer.println(smClient.getString("managerServlet.alreadyContext", displayPath));
            return;
        }

        if (config != null && config.startsWith("file:")) {
            config = config.substring("file:".length());
        }
        if (war != null && war.startsWith("file:")) {
            war = war.substring("file:".length());
        }

        try {
            if (tryAddServiced(name)) {
                try {
                    if (config != null) {
                        if (!configBase.mkdirs() && !configBase.isDirectory()) {
                            writer.println(smClient.getString("managerServlet.mkdirFail", configBase));
                            return;
                        }
                        File localConfigFile = new File(configBase, baseName + ".xml");
                        File configFile = new File(config);
                        // Skip delete and copy if source == destination
                        if (!configFile.getCanonicalPath().equals(localConfigFile.getCanonicalPath())) {
                            if (localConfigFile.isFile() && !localConfigFile.delete()) {
                                writer.println(smClient.getString("managerServlet.deleteFail", localConfigFile));
                                return;
                            }
                            ExpandWar.copy(configFile, localConfigFile);
                        }
                    }
                    if (war != null) {
                        File localWarFile;
                        if (war.endsWith(".war")) {
                            localWarFile = new File(host.getAppBaseFile(), baseName + ".war");
                        } else {
                            localWarFile = new File(host.getAppBaseFile(), baseName);
                        }
                        File warFile = new File(war);
                        if (!warFile.isAbsolute()) {
                            warFile = new File(host.getAppBaseFile(), war);
                        }
                        // Skip delete and copy if source == destination
                        if (!warFile.getCanonicalPath().equals(localWarFile.getCanonicalPath())) {
                            if (localWarFile.exists() && !ExpandWar.delete(localWarFile)) {
                                writer.println(smClient.getString("managerServlet.deleteFail", localWarFile));
                                return;
                            }
                            ExpandWar.copy(warFile, localWarFile);
                        }
                    }
                } finally {
                    removeServiced(name);
                }
                // Perform new deployment
                check(name);
            } else {
                writer.println(smClient.getString("managerServlet.inService", displayPath));
            }
            writeDeployResult(writer, smClient, name, displayPath);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log(sm.getString("managerServlet.error.deploy", displayPath), t);
            writer.println(smClient.getString("managerServlet.exception", t.toString()));
        }

    }


    private void writeDeployResult(PrintWriter writer, StringManager smClient, String name, String displayPath) {
        Context deployed = (Context) host.findChild(name);
        if (deployed != null && deployed.getConfigured() && deployed.getState().isAvailable()) {
            writer.println(smClient.getString("managerServlet.deployed", displayPath));
        } else if (deployed != null && !deployed.getState().isAvailable()) {
            writer.println(smClient.getString("managerServlet.deployedButNotStarted", displayPath));
        } else {
            // Something failed
            writer.println(smClient.getString("managerServlet.deployFailed", displayPath));
        }
    }


    /**
     * Render a list of the currently active Contexts in our virtual host.
     *
     * @param writer   Writer to render to
     * @param smClient i18n support for current client's locale
     */
    protected void list(PrintWriter writer, StringManager smClient) {

        if (debug >= 1) {
            log("list: Listing contexts for virtual host '" + host.getName() + "'");
        }

        writer.println(smClient.getString("managerServlet.listed", host.getName()));
        Container[] contexts = host.findChildren();
        for (Container container : contexts) {
            Context context = (Context) container;
            if (context != null) {
                String displayPath = context.getPath();
                if (displayPath.equals("")) {
                    displayPath = "/";
                }
                List<String> parts = null;
                if (context.getState().isAvailable()) {
                    parts = Arrays.asList(displayPath, "running", "" + context.getManager().findSessions().length,
                            context.getDocBase());
                } else {
                    parts = Arrays.asList(displayPath, "stopped", "0", context.getDocBase());
                }
                writer.println(StringUtils.join(parts, ':'));
            }
        }
    }


    /**
     * Reload the web application at the specified context path.
     *
     * @param writer   Writer to render to
     * @param cn       Name of the application to be restarted
     * @param smClient i18n support for current client's locale
     */
    protected void reload(PrintWriter writer, ContextName cn, StringManager smClient) {

        if (debug >= 1) {
            log("restart: Reloading web application '" + cn + "'");
        }

        if (!validateContextName(cn, writer, smClient)) {
            return;
        }

        try {
            Context context = (Context) host.findChild(cn.getName());
            if (context == null) {
                writer.println(
                        smClient.getString("managerServlet.noContext", Escape.htmlElementContent(cn.getDisplayName())));
                return;
            }
            // It isn't possible for the manager to reload itself
            if (context.getName().equals(this.context.getName())) {
                writer.println(smClient.getString("managerServlet.noSelf"));
                return;
            }
            context.reload();
            writer.println(smClient.getString("managerServlet.reloaded", cn.getDisplayName()));
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log(sm.getString("managerServlet.error.reload", cn.getDisplayName()), t);
            writer.println(smClient.getString("managerServlet.exception", t.toString()));
        }

    }


    /**
     * Render a list of available global JNDI resources.
     *
     * @param writer   Writer to render to
     * @param type     Fully qualified class name of the resource type of interest, or <code>null</code> to list
     *                     resources of all types
     * @param smClient i18n support for current client's locale
     */
    protected void resources(PrintWriter writer, String type, StringManager smClient) {

        if (debug >= 1) {
            if (type != null) {
                log("resources:  Listing resources of type " + type);
            } else {
                log("resources:  Listing resources of all types");
            }
        }

        // Is the global JNDI resources context available?
        if (global == null) {
            writer.println(smClient.getString("managerServlet.noGlobal"));
            return;
        }

        // Enumerate the global JNDI resources of the requested type
        if (type != null) {
            writer.println(smClient.getString("managerServlet.resourcesType", type));
        } else {
            writer.println(smClient.getString("managerServlet.resourcesAll"));
        }

        printResources(writer, "", global, type, smClient);

    }


    /**
     * List the resources of the given context.
     *
     * @param writer        Writer to render to
     * @param prefix        Path for recursion
     * @param namingContext The naming context for lookups
     * @param type          Fully qualified class name of the resource type of interest, or <code>null</code> to list
     *                          resources of all types
     * @param smClient      i18n support for current client's locale
     */
    protected void printResources(PrintWriter writer, String prefix, javax.naming.Context namingContext, String type,
            StringManager smClient) {
        try {
            NamingEnumeration<Binding> items = namingContext.listBindings("");
            while (items.hasMore()) {
                Binding item = items.next();
                Object obj = item.getObject();
                if (obj instanceof javax.naming.Context) {
                    printResources(writer, prefix + item.getName() + "/", (javax.naming.Context) obj, type, smClient);
                } else {
                    if (type != null && (obj == null || !IntrospectionUtils.isInstance(obj.getClass(), type))) {
                        continue;
                    }
                    writer.print(prefix + item.getName());
                    writer.print(':');
                    writer.print(item.getClassName());
                    // Do we want a description if available?
                    writer.println();
                }
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log(sm.getString("managerServlet.error.resources", type), t);
            writer.println(smClient.getString("managerServlet.exception", t.toString()));
        }
    }


    /**
     * Writes System OS and JVM properties.
     *
     * @param writer   Writer to render to
     * @param smClient i18n support for current client's locale
     */
    protected void serverinfo(PrintWriter writer, StringManager smClient) {
        if (debug >= 1) {
            log("serverinfo");
        }
        try {
            writer.println(smClient.getString("managerServlet.serverInfo", ServerInfo.getServerInfo(),
                    System.getProperty("os.name"), System.getProperty("os.version"), System.getProperty("os.arch"),
                    System.getProperty("java.runtime.version"), System.getProperty("java.vm.vendor")));
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log(sm.getString("managerServlet.error.serverInfo"), t);
            writer.println(smClient.getString("managerServlet.exception", t.toString()));
        }
    }

    /**
     * Session information for the web application at the specified context path. Displays a profile of session
     * thisAccessedTime listing number of sessions for each 10 minute interval up to 10 hours.
     *
     * @param writer   Writer to render to
     * @param cn       Name of the application to list session information for
     * @param idle     Expire all sessions with idle time &gt; idle for this context
     * @param smClient i18n support for current client's locale
     */
    protected void sessions(PrintWriter writer, ContextName cn, int idle, StringManager smClient) {

        if (debug >= 1) {
            log("sessions: Session information for web application '" + cn + "'");
            if (idle >= 0) {
                log("sessions: Session expiration for " + idle + " minutes '" + cn + "'");
            }
        }

        if (!validateContextName(cn, writer, smClient)) {
            return;
        }

        String displayPath = cn.getDisplayName();

        try {
            Context context = (Context) host.findChild(cn.getName());
            if (context == null) {
                writer.println(smClient.getString("managerServlet.noContext", Escape.htmlElementContent(displayPath)));
                return;
            }
            Manager manager = context.getManager();
            if (manager == null) {
                writer.println(smClient.getString("managerServlet.noManager", Escape.htmlElementContent(displayPath)));
                return;
            }
            int maxCount = 60;
            int histoInterval = 1;
            int maxInactiveInterval = context.getSessionTimeout();
            if (maxInactiveInterval > 0) {
                histoInterval = maxInactiveInterval / maxCount;
                if (histoInterval * maxCount < maxInactiveInterval) {
                    histoInterval++;
                }
                if (0 == histoInterval) {
                    histoInterval = 1;
                }
                maxCount = maxInactiveInterval / histoInterval;
                if (histoInterval * maxCount < maxInactiveInterval) {
                    maxCount++;
                }
            }

            writer.println(smClient.getString("managerServlet.sessions", displayPath));
            writer.println(smClient.getString("managerServlet.sessiondefaultmax", "" + maxInactiveInterval));
            Session[] sessions = manager.findSessions();
            int[] timeout = new int[maxCount + 1];
            int notimeout = 0;
            int expired = 0;
            for (Session session : sessions) {
                int time = (int) (session.getIdleTimeInternal() / 1000L);
                if (idle >= 0 && time >= idle * 60) {
                    session.expire();
                    expired++;
                }
                time = time / 60 / histoInterval;
                if (time < 0) {
                    notimeout++;
                } else if (time >= maxCount) {
                    timeout[maxCount]++;
                } else {
                    timeout[time]++;
                }
            }
            if (timeout[0] > 0) {
                writer.println(
                        smClient.getString("managerServlet.sessiontimeout", "<" + histoInterval, "" + timeout[0]));
            }
            for (int i = 1; i < maxCount; i++) {
                if (timeout[i] > 0) {
                    writer.println(smClient.getString("managerServlet.sessiontimeout",
                            "" + i * histoInterval + " - <" + (i + 1) * histoInterval, "" + timeout[i]));
                }
            }
            if (timeout[maxCount] > 0) {
                writer.println(smClient.getString("managerServlet.sessiontimeout", ">=" + maxCount * histoInterval,
                        "" + timeout[maxCount]));
            }
            if (notimeout > 0) {
                writer.println(smClient.getString("managerServlet.sessiontimeout.unlimited", "" + notimeout));
            }
            if (idle >= 0) {
                writer.println(smClient.getString("managerServlet.sessiontimeout.expired", ">" + idle, "" + expired));
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log(sm.getString("managerServlet.error.sessions", displayPath), t);
            writer.println(smClient.getString("managerServlet.exception", t.toString()));
        }

    }


    /**
     * Extract the expiration request parameter
     *
     * @param writer   Writer to render to
     * @param cn       Name of the application to list session information for
     * @param req      The Servlet request
     * @param smClient i18n support for current client's locale
     */
    protected void expireSessions(PrintWriter writer, ContextName cn, HttpServletRequest req, StringManager smClient) {
        int idle = -1;
        String idleParam = req.getParameter("idle");
        if (idleParam != null) {
            try {
                idle = Integer.parseInt(idleParam);
            } catch (NumberFormatException e) {
                log(sm.getString("managerServlet.error.idleParam", idleParam));
            }
        }
        sessions(writer, cn, idle, smClient);
    }

    /**
     * Start the web application at the specified context path.
     *
     * @param writer   Writer to render to
     * @param cn       Name of the application to be started
     * @param smClient i18n support for current client's locale
     */
    protected void start(PrintWriter writer, ContextName cn, StringManager smClient) {

        if (debug >= 1) {
            log("start: Starting web application '" + cn + "'");
        }

        if (!validateContextName(cn, writer, smClient)) {
            return;
        }

        String displayPath = cn.getDisplayName();

        try {
            Context context = (Context) host.findChild(cn.getName());
            if (context == null) {
                writer.println(smClient.getString("managerServlet.noContext", Escape.htmlElementContent(displayPath)));
                return;
            }
            context.start();
            if (context.getState().isAvailable()) {
                writer.println(smClient.getString("managerServlet.started", displayPath));
            } else {
                writer.println(smClient.getString("managerServlet.startFailed", displayPath));
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log(sm.getString("managerServlet.error.start", displayPath), t);
            writer.println(smClient.getString("managerServlet.startFailed", displayPath));
            writer.println(smClient.getString("managerServlet.exception", t.toString()));
        }

    }


    /**
     * Stop the web application at the specified context path.
     *
     * @param writer   Writer to render to
     * @param cn       Name of the application to be stopped
     * @param smClient i18n support for current client's locale
     */
    protected void stop(PrintWriter writer, ContextName cn, StringManager smClient) {

        if (debug >= 1) {
            log("stop: Stopping web application '" + cn + "'");
        }

        if (!validateContextName(cn, writer, smClient)) {
            return;
        }

        String displayPath = cn.getDisplayName();

        try {
            Context context = (Context) host.findChild(cn.getName());
            if (context == null) {
                writer.println(smClient.getString("managerServlet.noContext", Escape.htmlElementContent(displayPath)));
                return;
            }
            // It isn't possible for the manager to stop itself
            if (context.getName().equals(this.context.getName())) {
                writer.println(smClient.getString("managerServlet.noSelf"));
                return;
            }
            context.stop();
            writer.println(smClient.getString("managerServlet.stopped", displayPath));
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log(sm.getString("managerServlet.error.stop", displayPath), t);
            writer.println(smClient.getString("managerServlet.exception", t.toString()));
        }

    }


    /**
     * Undeploy the web application at the specified context path.
     *
     * @param writer   Writer to render to
     * @param cn       Name of the application to be removed
     * @param smClient i18n support for current client's locale
     */
    protected void undeploy(PrintWriter writer, ContextName cn, StringManager smClient) {

        if (debug >= 1) {
            log("undeploy: Undeploying web application at '" + cn + "'");
        }

        if (!validateContextName(cn, writer, smClient)) {
            return;
        }

        String name = cn.getName();
        String baseName = cn.getBaseName();
        String displayPath = cn.getDisplayName();

        try {

            // Validate the Context of the specified application
            Context context = (Context) host.findChild(name);
            if (context == null) {
                writer.println(smClient.getString("managerServlet.noContext", Escape.htmlElementContent(displayPath)));
                return;
            }

            if (!isDeployed(name)) {
                writer.println(
                        smClient.getString("managerServlet.notDeployed", Escape.htmlElementContent(displayPath)));
                return;
            }

            if (tryAddServiced(name)) {
                try {
                    // Try to stop the context first to be nicer
                    context.stop();
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                }
                try {
                    File war = new File(host.getAppBaseFile(), baseName + ".war");
                    File dir = new File(host.getAppBaseFile(), baseName);
                    File xml = new File(configBase, baseName + ".xml");
                    if (war.exists() && !war.delete()) {
                        writer.println(smClient.getString("managerServlet.deleteFail", war));
                        return;
                    } else if (dir.exists() && !ExpandWar.delete(dir, false)) {
                        writer.println(smClient.getString("managerServlet.deleteFail", dir));
                        return;
                    } else if (xml.exists() && !xml.delete()) {
                        writer.println(smClient.getString("managerServlet.deleteFail", xml));
                        return;
                    }
                } finally {
                    removeServiced(name);
                }
                // Perform new deployment
                check(name);
            } else {
                writer.println(smClient.getString("managerServlet.inService", displayPath));
            }
            writer.println(smClient.getString("managerServlet.undeployed", displayPath));
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log(sm.getString("managerServlet.error.undeploy", displayPath), t);
            writer.println(smClient.getString("managerServlet.exception", t.toString()));
        }

    }


    // -------------------------------------------------------- Support Methods


    /**
     * Invoke the isDeployed method on the deployer.
     *
     * @param name The webapp name
     *
     * @return <code>true</code> if a webapp with that name is deployed
     *
     * @throws Exception Propagate JMX invocation error
     */
    protected boolean isDeployed(String name) throws Exception {
        String[] params = { name };
        String[] signature = { "java.lang.String" };
        Boolean result = (Boolean) mBeanServer.invoke(oname, "isDeployed", params, signature);
        return result.booleanValue();
    }


    /**
     * Invoke the check method on the deployer.
     *
     * @param name The webapp name
     *
     * @throws Exception Propagate JMX invocation error
     */
    protected void check(String name) throws Exception {
        String[] params = { name };
        String[] signature = { "java.lang.String" };
        mBeanServer.invoke(oname, "check", params, signature);
    }


    /**
     * Attempt to mark a context as being serviced
     *
     * @param name The context name
     *
     * @return {@code true} if the application was marked as being serviced and {@code false} if the application was
     *             already marked as being serviced
     *
     * @throws Exception Error invoking the deployer
     */
    protected boolean tryAddServiced(String name) throws Exception {
        String[] params = { name };
        String[] signature = { "java.lang.String" };
        Boolean result = (Boolean) mBeanServer.invoke(oname, "tryAddServiced", params, signature);
        return result.booleanValue();
    }


    /**
     * Invoke the removeServiced method on the deployer.
     *
     * @param name The webapp name
     *
     * @throws Exception Propagate JMX invocation error
     */
    protected void removeServiced(String name) throws Exception {
        String[] params = { name };
        String[] signature = { "java.lang.String" };
        mBeanServer.invoke(oname, "removeServiced", params, signature);
    }


    /**
     * Upload the WAR file included in this request, and store it at the specified file location.
     *
     * @param writer   Writer to render to
     * @param request  The servlet request we are processing
     * @param war      The file into which we should store the uploaded WAR
     * @param smClient The StringManager used to construct i18n messages based on the Locale of the client
     *
     * @exception IOException if an I/O error occurs during processing
     */
    protected void uploadWar(PrintWriter writer, HttpServletRequest request, File war, StringManager smClient)
            throws IOException {

        if (war.exists() && !war.delete()) {
            String msg = smClient.getString("managerServlet.deleteFail", war);
            throw new IOException(msg);
        }

        try (ServletInputStream istream = request.getInputStream(); OutputStream ostream = new FileOutputStream(war)) {
            IOTools.flow(istream, ostream);
        } catch (IOException e) {
            if (war.exists() && !war.delete()) {
                writer.println(smClient.getString("managerServlet.deleteFail", war));
            }
            throw e;
        }
    }


    protected static boolean validateContextName(ContextName cn, PrintWriter writer, StringManager smClient) {

        // ContextName should be non-null with a path that is empty or starts
        // with /
        if (cn != null && (cn.getPath().startsWith("/") || cn.getPath().equals(""))) {
            return true;
        }

        String path = null;
        if (cn != null) {
            path = Escape.htmlElementContent(cn.getPath());
        }
        writer.println(smClient.getString("managerServlet.invalidPath", path));
        return false;
    }

    protected Map<String,List<String>> getConnectorCiphers(StringManager smClient) {
        Map<String,List<String>> result = new HashMap<>();

        Connector connectors[] = getConnectors();
        for (Connector connector : connectors) {
            if (Boolean.TRUE.equals(connector.getProperty("SSLEnabled"))) {
                SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs();
                for (SSLHostConfig sslHostConfig : sslHostConfigs) {
                    String name = connector.toString() + "-" + sslHostConfig.getHostName();
                    /* Add cipher list, keep order but remove duplicates */
                    result.put(name,
                            new ArrayList<>(new LinkedHashSet<>(Arrays.asList(sslHostConfig.getEnabledCiphers()))));
                }
            } else {
                ArrayList<String> cipherList = new ArrayList<>(1);
                cipherList.add(smClient.getString("managerServlet.notSslConnector"));
                result.put(connector.toString(), cipherList);
            }
        }
        return result;
    }


    protected Map<String,List<String>> getConnectorCerts(StringManager smClient) {
        Map<String,List<String>> result = new HashMap<>();

        Connector connectors[] = getConnectors();
        for (Connector connector : connectors) {
            if (Boolean.TRUE.equals(connector.getProperty("SSLEnabled"))) {
                SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs();
                for (SSLHostConfig sslHostConfig : sslHostConfigs) {
                    if (sslHostConfig.getOpenSslContext().longValue() == 0) {
                        // Not set. Must be JSSE based.
                        Set<SSLHostConfigCertificate> sslHostConfigCerts = sslHostConfig.getCertificates();
                        for (SSLHostConfigCertificate sslHostConfigCert : sslHostConfigCerts) {
                            String name = connector.toString() + "-" + sslHostConfig.getHostName() + "-" +
                                    sslHostConfigCert.getType();
                            List<String> certList = new ArrayList<>();
                            SSLContext sslContext = sslHostConfigCert.getSslContext();
                            String alias = sslHostConfigCert.getCertificateKeyAlias();
                            if (alias == null) {
                                alias = SSLUtilBase.DEFAULT_KEY_ALIAS;
                            }
                            X509Certificate[] certs = sslContext.getCertificateChain(alias);
                            if (certs == null) {
                                certList.add(smClient.getString("managerServlet.certsNotAvailable"));
                            } else {
                                for (Certificate cert : certs) {
                                    certList.add(cert.toString());
                                }
                            }
                            result.put(name, certList);
                        }
                    } else {
                        List<String> certList = new ArrayList<>();
                        certList.add(smClient.getString("managerServlet.certsNotAvailable"));
                        String name = connector.toString() + "-" + sslHostConfig.getHostName();
                        result.put(name, certList);
                    }
                }
            } else {
                List<String> certList = new ArrayList<>(1);
                certList.add(smClient.getString("managerServlet.notSslConnector"));
                result.put(connector.toString(), certList);
            }
        }

        return result;
    }


    protected Map<String,List<String>> getConnectorTrustedCerts(StringManager smClient) {
        Map<String,List<String>> result = new HashMap<>();

        Connector connectors[] = getConnectors();
        for (Connector connector : connectors) {
            if (Boolean.TRUE.equals(connector.getProperty("SSLEnabled"))) {
                SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs();
                for (SSLHostConfig sslHostConfig : sslHostConfigs) {
                    String name = connector.toString() + "-" + sslHostConfig.getHostName();
                    List<String> certList = new ArrayList<>();
                    if (sslHostConfig.getOpenSslContext().longValue() == 0) {
                        // Not set. Must be JSSE based.
                        SSLContext sslContext = sslHostConfig.getCertificates().iterator().next().getSslContext();
                        X509Certificate[] certs = sslContext.getAcceptedIssuers();
                        if (certs == null) {
                            certList.add(smClient.getString("managerServlet.certsNotAvailable"));
                        } else if (certs.length == 0) {
                            certList.add(smClient.getString("managerServlet.trustedCertsNotConfigured"));
                        } else {
                            for (Certificate cert : certs) {
                                certList.add(cert.toString());
                            }
                        }
                    } else {
                        certList.add(smClient.getString("managerServlet.certsNotAvailable"));
                    }
                    result.put(name, certList);
                }
            } else {
                List<String> certList = new ArrayList<>(1);
                certList.add(smClient.getString("managerServlet.notSslConnector"));
                result.put(connector.toString(), certList);
            }
        }

        return result;
    }


    private Connector[] getConnectors() {
        Engine e = (Engine) host.getParent();
        Service s = e.getService();
        return s.findConnectors();
    }
}