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>");
}
}