我正在使用嵌入式 Jetty 构建沙箱 RESTful API。我的概念验证设计:一个简单的嵌入式 jetty 服务器,(1) 接受 SSL 端口上的连接,(2) 使用 ContextHandlerCollection 根据 URI 前缀调用正确的处理程序。
我最初的测试使用简单的非 SSL 连接,似乎工作得很好(注意,附录中的导入代码和助手 HelloHandler 类)。
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();
}
然而,在测试时,我没有注意到当我省略尾部正斜杠时,浏览器重定向正在发生,所以http://localhost:12000/test1
被重定向到http://localhost:12000/test1/
。 (FWIW,这种现场服务后来会转化为 4 个多小时的故障排除)。
当我切换到 HTTPS SSL 连接时,一切都出了问题。代码如下:
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();
}
症状:
尝试使用https://localhost:12000/test1
(没有尾部斜杠)将导致浏览器报告服务器已重置连接。另外,以及我所做的not最初,URI 被重定向到http://localhost:12000/test1/
(不是 https!)。有趣的是(以一种施虐幽默感的方式),有几次,我会更改代码中一些无关紧要的内容,然后无意中进行测试https://localhost:12000/test1/
它会起作用的。言语无法充分表达此类误报所造成的挫败感。
除了浏览器重定向和报告连接重置错误之外,我还会在服务器日志中收到以下异常:
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
不幸的是,当关键线索在浏览器重定向的 URL 中时,我花了所有时间尝试直接解决此异常。事实证明,ContextHandler 代码有一个默认行为,当不存在尾部正斜杠时,它会导致它仅重定向到具有尾部斜杠的 URI。不幸的是,此重定向是到 HTTP URI - 因此 HTTPS 方案被丢弃,这导致服务器抱怨纯文本。
解决方法:
一旦我清楚了这种重定向行为,对实际问题的快速 Google 搜索就引导我找到了 ContextHandler.setAllowNullPathInfo(true) 方法 - 该方法会关闭此重定向行为。在我上面的代码中,这是通过两行完成的:
test1Context.setAllowNullPathInfo(true);
test2Context.setAllowNullPathInfo(true);
这篇文章的要点:
我花了 3 - 4 个小时尝试解决“javax.net.ssl.SSLException:无法识别的 SSL 消息,纯文本连接?”异常,并且我在网络上没有找到与上面的解决方案/解决方法链接的异常。如果我能将一名其他开发人员从我所经历的挫败感中拯救出来,那么任务就完成了。
挥之不去的问题(发布此内容的其他原因):
好吧,我让这段代码工作了,但必须承认:我非常不安,因为我的极其简单的概念验证测试,做了一些我认为很常见的事情,遇到了这种似乎完全前所未有的情况。这告诉我,我可能正在做一些超出“最佳实践”范围的事情;或者,更糟糕的是,我的设计方式完全错误。那么,问题:
1)我做错了吗?
2) 为什么 ContextHandler 的默认行为是重定向缺少尾随空格的 URI?使用 setAllowNullPathInfo(true) 覆盖默认行为会带来什么风险?
附录(辅助类和导入的代码)
进口:
导入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;
助手类:
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>");
}
}