Embedded Jetty: In secure https server, ContextHan

2019-05-10 06:55发布

问题:

I'm building a sandbox RESTful API using embedded Jetty. My Proof-of-Concept design: a simple embedded jetty server that (1) accepts connections on an SSL port and (2) uses a ContextHandlerCollection to invoke the proper Handler based on the URI prefix.

My original test, using a simple non-SSL connection, seemed to work perfectly (note, code for imports and helper HelloHandler class in APPENDIX).

public static void main(String[] args) throws Exception {
    Server server = new Server(12000); 

    ContextHandler test1Context = new ContextHandler();
    test1Context.setContextPath("/test1");
    test1Context.setHandler(new HelloHandler("Hello1"));

    ContextHandler test2Context = new ContextHandler();
    test2Context.setContextPath("/test2");
    test2Context.setHandler(new HelloHandler("Hello2"));

    ContextHandlerCollection contextHandlers = new ContextHandlerCollection();
    contextHandlers.setHandlers(new Handler[] { test1Context, test2Context });

    server.setHandler(contextHandlers);
    server.start();
    server.join();
}

However, while testing this, it escaped my attention that browser redirects were happening when I omitted a trailing forward slash, so http://localhost:12000/test1 was getting redirected to http://localhost:12000/test1/. (FWIW, that oversite would later translate to 4+ hours of troubleshooting).

When I switched to an HTTPS SSL connection, everything went wrong. Code below:

public static void main(String[] args) throws Exception {
    Server server = new Server(); 

    // Setups
    SslContextFactory sslContextFactory = new SslContextFactory();
    sslContextFactory.setKeyStorePath("C:/keystore.jks");
    sslContextFactory.setKeyStorePassword("password");
    sslContextFactory.setKeyManagerPassword("password");

    ContextHandler test1Context = new ContextHandler();
    test1Context.setContextPath("/test1");
    test1Context.setHandler(new HelloHandler("Hello1"));

    ContextHandler test2Context = new ContextHandler();
    test2Context.setContextPath("/test2");
    test2Context.setHandler(new HelloHandler("Hello2"));

    ContextHandlerCollection contextHandlers = new ContextHandlerCollection();
    contextHandlers.setHandlers(new Handler[] { test1Context, test2Context });

    ServerConnector serverConnector = new ServerConnector(server,
            new SslConnectionFactory(sslContextFactory, "http/1.1"),
            new HttpConnectionFactory());

    serverConnector.setPort(12000);
    server.setConnectors(new Connector[] { serverConnector });
    server.setHandler(contextHandlers);

    server.start();
    server.join();
}

SYMPTOMS:

Attempting to use https://localhost:12000/test1 (no trailing slash) would cause browser to report that the server had reset the connection. Additionally, and what I did not spot initially, the URI was being redirected to http://localhost:12000/test1/ (not https!). Amusingly (in a sadistic-sense-of-humor sort of way), on a couple occasions, I'd change something inconsequential in the code and then inadvertently test with https://localhost:12000/test1/ and it would work. Words don't do justice to the frustration such false-positives caused.

In addition to the browser redirecting and reporting a connection-reset error, I would get the following exception in my server logs:

2013-11-23 13:57:48 DEBUG org.eclipse.jetty.server.HttpConnection:282 - 
org.eclipse.jetty.io.EofException
    at org.eclipse.jetty.io.ssl.SslConnection$DecryptedEndPoint.fill(SslConnection.java:653)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:240)
    at org.eclipse.jetty.io.AbstractConnection$ReadCallback.run(AbstractConnection.java:358)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:601)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:532)
    at java.lang.Thread.run(Thread.java:722)
Caused by: javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?
    at sun.security.ssl.EngineInputRecord.bytesInCompletePacket(EngineInputRecord.java:171)
    at sun.security.ssl.SSLEngineImpl.readNetRecord(SSLEngineImpl.java:845)
    at sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:758)
    at javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:624)
    at org.eclipse.jetty.io.ssl.SslConnection$DecryptedEndPoint.fill(SslConnection.java:518)
    ... 5 more

Unfortunately, I spent all of my time trying to troubleshoot this exception directly, when the key clue was in the browser redirected URL. It turns out that the ContextHandler code has a default behavior which, when no trailing forward slash exists, causes it to just redirect to a URI that has the trailing slash. Unfortunately, this redirect is to an HTTP URI - so the HTTPS scheme is dropped, which caused the server to complain about the plain-text.

WORKAROUND:

Once this redirect behavior became clear to me, a quick Google search of that actual issue led me to the ContextHandler.setAllowNullPathInfo(true) method - which turns off this redirect behavior. In my code above, this is accomplished with 2 lines:

test1Context.setAllowNullPathInfo(true);
test2Context.setAllowNullPathInfo(true);

MAIN POINT OF THIS POST:

I spent 3 - 4 hours trying to troubleshoot the "javax.net.ssl.SSLException: Unrecognized SSL message, plaintext connection?" exception, and nowhere on the web did I find that exception linked to the solution/workaround above. If I save even one other developer from the frustration I experienced, then mission accomplished.

LINGERING QUESTIONS (OTHER REASON FOR POSTING THIS): Okay, so I got this code working, but have to confess: I'm incredibly uneasy about the fact that my incredibly simple proof-of-concept test, doing something that I assume would be quite common, encountered this situation which seems utterly unprecedented. That tells me, I'm probably doing something beyond the realm of "best practice"; or, worse, doing something utterly wrong in how I'm designing this. So, questions:

1) AM I doing this wrong?

2) Why is the default behavior of ContextHandler to redirect URI's which lack trailing space? What risk(s) am I incurring by over-riding that default behavior with setAllowNullPathInfo(true)?

APPENDIX (code of helper class and imports) Imports: import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.util.ssl.SslContextFactory;

Helper Class:

static class HelloHandler extends AbstractHandler {
    final String _greeting;

    public HelloHandler(String greeting) {
        _greeting = greeting;
    }

    public void handle(String target, Request baseRequest,
            HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        response.setContentType("text/html;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        baseRequest.setHandled(true);
        response.getWriter().println("<h1>" + _greeting + "</h1>");
    }
}

回答1:

You are missing the HttpConfiguration setup.

Here ...

package jetty.examples;

import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.SecureRequestCustomizer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.util.ssl.SslContextFactory;

public class SecureContexts
{
    public static void main(String[] args) throws Exception
    {
        Server server = new Server();
        int port = 12000;

        // Setup SSL
        SslContextFactory sslContextFactory = new SslContextFactory();
        sslContextFactory.setKeyStorePath(System.getProperty("jetty.keystore.path","C:/keystore.jks"));
        sslContextFactory.setKeyStorePassword(System.getProperty("jetty.keystore.password","password"));
        sslContextFactory.setKeyManagerPassword(System.getProperty("jetty.keymanager.password","password"));

        // Setup HTTP Configuration
        HttpConfiguration httpConf = new HttpConfiguration();
        httpConf.setSecurePort(port);
        httpConf.setSecureScheme("https");
        httpConf.addCustomizer(new SecureRequestCustomizer());

        ContextHandler test1Context = new ContextHandler();
        test1Context.setContextPath("/test1");
        test1Context.setHandler(new HelloHandler("Hello1"));

        ContextHandler test2Context = new ContextHandler();
        test2Context.setContextPath("/test2");
        test2Context.setHandler(new HelloHandler("Hello2"));

        ContextHandlerCollection contextHandlers = new ContextHandlerCollection();
        contextHandlers.setHandlers(new Handler[]
        { test1Context, test2Context });

        ServerConnector serverConnector = new ServerConnector(server,
            new SslConnectionFactory(sslContextFactory,"http/1.1"),
            new HttpConnectionFactory(httpConf)); // <-- use it!
        serverConnector.setPort(port);

        server.setConnectors(new Connector[]
        { serverConnector });
        server.setHandler(contextHandlers);

        server.start();
        server.join();
    }
}