~/home of geeks

Tomcat Portdetection

· 946 Wörter · 5 Minute(n) Lesedauer

“Auf welchem Port läuft der HTTPS-Connector der laufenden Tomcat?” ist eine wichtige Frage, wenn man z. B. einen HTTP -> HTTPS redirect machen möchte und die man mit Tomcat Portdetection beantworten kann.

Als laufende Webanwendung gibt es nur einen sinnvollen Weg, auf die Laufzeitinformationen der Tomcat zuzugreifen, nämlich via MXBeans.

Die MXBeans bieten aber sehr viele Informationen und es ist nicht immer einfach, die Richtigen herauszufischen. So liefert ein ungefilterter Aufruf auf die Informationen der Tomcat mehrere dutzend MBeans über verschiedenste Laufzeitinformationen. Für die Ports sind die Connectoren relevant, die üblicherweise in der server.xml konfiguriert werden. Ein Beispiel für die Informationen zur MXBean eines Connectors, hier den HTTP-Port 8080, in gekürzter Form, sieht wie folgt aus:

MBean:  Catalina:type=Connector,port=8080
        modelerType     java.lang.String    null
        maxPostSize     int                 2097152
        scheme          java.lang.String    http
        className       java.lang.String    null
        acceptCount     int                 100
        secure          boolean             false
        ...
        protocol        java.lang.String    HTTP/1.1
        port            int                 8080

Und eines für den HTTPS-Port 8443

MBean:  Catalina:type=Connector,port=8443
        modelerType     java.lang.String    null
        maxPostSize     int                 2097152
        scheme          java.lang.String    https
        secure          boolean             true
        protocol    java.lang.String    org.apache.coyote.http11.Http11AprProtocol
        ...
     port    int    8443

Die MBean-API erlaubt einen rudimentären Querybuilder, mit dem die MBeans entsprechend gefiltert werden können.

Der erste Filter nach "scheme=http" or "scheme=https" liefert leider auch den AJP-Connector, der über das HTTP-Schema arbeitet. Beim Protokol gibt es die Möglichkeit, zwischen AJP und normalem HTTP zu unterscheiden:

Catalina:port=8080,type=Connector
protocol=HTTP/1.1

Catalina:port=8029,type=Connector
protocol=AJP/1.3

Catalina:port=8443,type=Connector
protocol=HTTP/1.1

Die Erweiterung der Query hilft, lediglich die für HTTP und HTTPS relevanten Ports zu filtern. Leider verwenden unterschiedliche Tomcatversionen verschiedene Werte bei “protocol”. So steht in der aktuellen Tomcat8 Version stets “HTTP/1.1”, in der Tomcat7 steht hier bei HTTPS “org.apache.coyote.http11.Http11AprProtocol”. Entsprechend muss die MBean-Query auch hierzu angepasst werden.

Die vorliegende Klasse ermittelt alle diese Informationen und stellt diese zur Verfügung.

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.management.*;
import javax.servlet.http.HttpServletRequest;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.*;

/**
 * Utility for working with Tomcat.
 *
 * @author Serhat Cinar
 */
public final class TomcatUtil2 {
    private static final Logger LOG = LogManager.getLogger(TomcatUtil2.class);
    private static TomcatUtil2 INSTANCE;
    private Map<String, Integer> portsByScheme = null;

    private TomcatUtil2() {
    }

    public static synchronized TomcatUtil2 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new TomcatUtil2();
            try {
                INSTANCE.init();
            } catch (MalformedObjectNameException | AttributeNotFoundException |
                    InstanceNotFoundException | MBeanException |
                    ReflectionException e) {
                throw new RuntimeException(e);
            }
        }
        return INSTANCE;
    }

    /**
     * Finds all scheme / port definitiona via MBeans for Catalina.
     *
     */
    private void init() throws MalformedObjectNameException, AttributeNotFoundException,
            InstanceNotFoundException, MBeanException, ReflectionException{
        portsByScheme = new HashMap();

        final MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
        findPortsByScheme(portsByScheme, mBeanServer);
        logInfo();
    }

    private void logInfo(){
        LOG.info("Initializing scheme -> port mappings: " + portsByScheme);

        if (portsByScheme.isEmpty() || portsByScheme.get("http") == null || portsByScheme.get("https") == null) {
            final StringBuilder warning = new StringBuilder();
            if (portsByScheme.isEmpty()) {
                warning.append("No port- or schema-informations found.");
            } else {
                if (portsByScheme.get("http") == null) {
                    warning.append("No port- or schema-information found for schema HTTP.");
                }
                if (portsByScheme.get("https") == null) {
                    warning.append("No port- or schema-information found for schema HTTPS.");
                }
            }
            LOG.warn(warning.toString());
        }

    }

    /**
     * Returns a map with key = scheme (http, https) and value = port.
     *
     * @return
     */
    public Map<String, Integer> getPortsByScheme() {
        return Collections.unmodifiableMap(portsByScheme);
    }

    private static void findPortsByScheme(Map<String, Integer> portsByScheme, MBeanServer mBeanServer)
            throws MalformedObjectNameException, AttributeNotFoundException, InstanceNotFoundException, MBeanException,
            ReflectionException {

        // Filtering by scheme and protocol.
        // Protocol String contained sometimes "HTTP/1.1" and sometimes
        // the name of the class, like "org.apache.coyote.http11.Http11AprProtocol" (depending on the tomcat Version)
        final Set<ObjectName> objectNames = mBeanServer.queryNames(new ObjectName("*:type=Connector,*"),
                Query.and(
                    Query.or(
                       Query.match(Query.attr("scheme"), Query.value("http")),
                       Query.match(Query.attr("scheme"), Query.value("https"))
                    ),
                    Query.or(
                            Query.anySubString(Query.attr("protocol"), Query.value("HTTP")),
                            Query.or(
                                    Query.anySubString(Query.attr("protocol"), Query.value("Http")),
                                    Query.anySubString(Query.attr("protocol"), Query.value("http"))
                            )
                    )
                )
        );

        for (final ObjectName objectName : objectNames) {
            final String scheme = String.valueOf(mBeanServer.getAttribute(objectName, "scheme"));
            final String port = objectName.getKeyProperty("port");
            portsByScheme.put(scheme, Integer.valueOf(port));
        }
    }

    /**
     * Returns the portnumber for the given scheme (http or https).
     * 
     * @param scheme "http" or "https"
     * @return
     */
    public Integer getPortByScheme(final String scheme) {
        return getPortsByScheme().get(scheme);
    }

    /**
     * Returns a HTTP-URL for the given httpRequest, whether it's HTTP or HTTPS.<br/>
     * {@code TomcatUtil.getInstance().generateHttpUrl( myHttpRequest, "page.html") }
     * will generate "http://localhost:8080/myApp/page.html" on most default configurations,
     * when the httpRequest belongs to "myApp" and the page is set to "page.html".
     *
     * @param httpRequest
     * @param page
     * @return
     * @throws MalformedURLException
     */
    public String generateHttpUrl(final HttpServletRequest httpRequest, final String page) throws MalformedURLException {
        return generateUrl(httpRequest, page, "http");
    }

    /**
     * Returns a HTTPS-URL for the given httpRequest, whether it's HTTP or HTTPS.<br/>
     * {@code TomcatUtil.getInstance().generateHttpsUrl( myHttpRequest, "page.html") }
     * will generate "https://localhost:8443/myApp/page.html" on most default configurations,
     * when the httpRequest belongs to "myApp" and the page is set to "page.html".
     *
     * @param httpRequest
     * @param page
     * @return
     * @throws MalformedURLException
     */
    public String generateHttpsUrl(final HttpServletRequest httpRequest, final String page) throws MalformedURLException {
        return generateUrl(httpRequest, page, "https");
    }

    /**
     * Returns a HTTP/HTTPS-URL for the given httpRequest, whether it's HTTP or HTTPS.
     *
     * @param httpRequest
     * @param page
     * @return
     * @throws MalformedURLException
     * @see #generateHttpsUrl(HttpServletRequest, String)
     * @see #generateHttpUrl(HttpServletRequest, String)
     */
    public String generateUrl(final HttpServletRequest httpRequest, final String page, final String scheme) throws MalformedURLException {
        String targetPage = page;
        if (targetPage != null && !targetPage.startsWith("/")) {
            targetPage = "/" + targetPage;
        }
        final String requestUrlStr = httpRequest.getRequestURL().toString();
        final URL requestUrl = new URL(requestUrlStr);
        final URL secureUrl = new URL(scheme, requestUrl.getHost(), getPortByScheme(scheme), httpRequest.getContextPath() + targetPage);
        return secureUrl.toString();
    }

    /**
     * See {@link #generateHttpUrl(HttpServletRequest, String)}.<br/>
     * Here page will be set to {@code httpRequest.getServletPath()}.
     * 
     * @param httpRequest
     * @return
     * @throws MalformedURLException
     * @see #generateHttpUrl(HttpServletRequest, String)
     */
    public String generateHttpUrl(final HttpServletRequest httpRequest) throws MalformedURLException {
        return generateHttpUrl(httpRequest, httpRequest.getServletPath());
    }

    /**
     * See {@link #generateHttpsUrl(HttpServletRequest, String)}.<br/>
     * Here page will be set to {@code httpRequest.getServletPath()}.
     *
     * @param httpRequest
     * @return
     * @throws MalformedURLException
     * @see #generateHttpUrls(HttpServletRequest, String)
     */
    public String generateHttpsUrl(final HttpServletRequest httpRequest) throws MalformedURLException {
        return generateHttpsUrl(httpRequest, httpRequest.getServletPath());
    }
}