Embedded Jetty: In secure https server, ContextHandler redirects to http URI
embedded-jetty, http-redirect, jetty, rest, ssl
Solution
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();
}
}
Problem
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>"); } } ```