如何使用 Java HTTPS 服务器管理 HTTP 请求?
How to manage HTTP request with Java HTTPS server?
我已经用 com.sun.net.httpserver.HttpsServer
库设置了自己的 https 服务器。
代码如下所示:
int PORT = 12345;
File KEYSTORE = new File("path/to/my.keystore");
String PASS = "mykeystorepass";
HttpsServer server = HttpsServer.create(new InetSocketAddress(PORT), 0);
SSLContext sslContext = SSLContext.getInstance("TLS");
char[] password = PASS.toCharArray();
KeyStore ks = KeyStore.getInstance("JKS");
FileInputStream fis = new FileInputStream(KEYSTORE);
ks.load(fis, password);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, password);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ks);
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
server.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
public void configure(HttpsParameters params) {
try {
SSLContext c = SSLContext.getDefault();
SSLEngine engine = c.createSSLEngine();
params.setNeedClientAuth(false);
params.setCipherSuites(engine.getEnabledCipherSuites());
params.setProtocols(engine.getEnabledProtocols());
SSLParameters defaultSSLParameters = c.getDefaultSSLParameters();
params.setSSLParameters(defaultSSLParameters);
} catch(Exception ex) {
ex.printStackTrace();
}
}
});
server.createContext("/", new Handler());
server.setExecutor(null);
server.start();
一切正常,但现在我正在尝试管理在同一端口上到达此 HTTPS 服务器的 HTTP 请求。当我使用 HTTP 协议打开网页(由我的 HttpHandler 处理)时,它显示错误 ERR_EMPTY_RESPONSE
,只有 HTTPS 协议有效。
我想自动将所有传入连接从 HTTP 重定向到 HTTPS。或者为两种协议使用不同的 HttpHandler 会很棒。
我怎样才能做到这一点?
经过几个小时的尝试和哭泣,我想通了。
首先,我将 HTTP 服务器库从官方更改为 org.jboss.com.sun.net.httpserver
。 (download source code here)
然后我查看了该库的源代码,发现 class org.jboss.sun.net.httpserver.ServerImpl
负责所有 HTTP(S) 流量处理。当你查看static runnable subclassExchange
中的方法run
时,你可以看到这段代码:
/* context will be null for new connections */
context = connection.getHttpContext();
boolean newconnection;
SSLEngine engine = null;
String requestLine = null;
SSLStreams sslStreams = null;
try {
if(context != null) {
this.rawin = connection.getInputStream();
this.rawout = connection.getRawOutputStream();
newconnection = false;
} else {
/* figure out what kind of connection this is */
newconnection = true;
if(https) {
if(sslContext == null) {
logger.warning("SSL connection received. No https contxt created");
throw new HttpError("No SSL context established");
}
sslStreams = new SSLStreams(ServerImpl.this, sslContext, chan);
rawin = sslStreams.getInputStream();
rawout = sslStreams.getOutputStream();
engine = sslStreams.getSSLEngine();
connection.sslStreams = sslStreams;
} else {
rawin = new BufferedInputStream(new Request.ReadStream(ServerImpl.this, chan));
rawout = new Request.WriteStream(ServerImpl.this, chan);
}
connection.raw = rawin;
connection.rawout = rawout;
}
Request req = new Request(rawin, rawout);
requestLine = req.requestLine();
[...]
这个 runnable class 决定连接类型是什么(HTTP 或 HTTPS)。该决定仅基于 https
布尔值 object (true/false) - 它声明您是使用 HttpsServer 还是 HttpServer class 作为您的服务器。
这就是问题所在 - 您希望根据访问者选择的协议向您的网页访问者显示内容,而不是根据服务器默认使用的协议。
下一个重要的事情是,当您使用 HTTP 协议访问 HttpsServer 时,服务器会尝试打开与您的 SSL 安全连接,然后无法解码未加密的数据(即代码 new Request(rawin, rawout);
,新请求开始从输入流 rawin
读取字节并失败)。
然后 class Request
抛出 IOException
,特别是 javax.net.ssl.SSLException
.
的实例
IOException 在此方法底部处理:
} catch(IOException e1) {
logger.log(Level.FINER, "ServerImpl.Exchange (1)", e1);
closeConnection(this.connection);
} catch(NumberFormatException e3) {
reject(Code.HTTP_BAD_REQUEST, requestLine, "NumberFormatException thrown");
} catch(URISyntaxException e) {
reject(Code.HTTP_BAD_REQUEST, requestLine, "URISyntaxException thrown");
} catch(Exception e4) {
logger.log(Level.FINER, "ServerImpl.Exchange (2)", e4);
closeConnection(connection);
}
所以你可以看到它在捕获到异常时默认关闭连接。
(因此 Chrome 显示错误 ERR_EMPTY_RESPONSE
)
解决方案很简单 - 将代码替换为:
} catch(IOException e1) {
logger.log(Level.FINER, "ServerImpl.Exchange (1)", e1);
if(e1 instanceof SSLException) {
try {
rawout = new Request.WriteStream(ServerImpl.this, chan);
StringBuilder builder = new StringBuilder(512);
int code = Code.HTTP_MOVED_PERM;
builder.append("HTTP/1.1 ").append(code).append(Code.msg(code)).append("\r\n");
builder.append("Location: https://www.example.com:"+ wrapper.getAddress().getPort() +"\r\n");
builder.append("Content-Length: 0\r\n");
String s = builder.toString();
byte[] b = s.getBytes("ISO8859_1");
rawout.write(b);
rawout.flush();
} catch(IOException e) {
e.printStackTrace();
}
}
closeConnection(connection);
} catch [...]
在关闭连接之前,它会为未加密的输出流设置正确的输出流rawout
,然后发送重定向代码(301 永久移动)以及要重定向到的位置地址。
只需将 www.example.com 更改为您的主机名。端口是自动添加的。唯一的问题是也尝试自动获取主机名。唯一可以提取此信息的地方是来自输入流(来自请求headers),但此流在抛出异常后不再可用。
现在您网页的访问者从 HTTP 协议重定向到 HTTPS。
希望对您有所帮助!
我已经用 com.sun.net.httpserver.HttpsServer
库设置了自己的 https 服务器。
代码如下所示:
int PORT = 12345;
File KEYSTORE = new File("path/to/my.keystore");
String PASS = "mykeystorepass";
HttpsServer server = HttpsServer.create(new InetSocketAddress(PORT), 0);
SSLContext sslContext = SSLContext.getInstance("TLS");
char[] password = PASS.toCharArray();
KeyStore ks = KeyStore.getInstance("JKS");
FileInputStream fis = new FileInputStream(KEYSTORE);
ks.load(fis, password);
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, password);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
tmf.init(ks);
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
server.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
public void configure(HttpsParameters params) {
try {
SSLContext c = SSLContext.getDefault();
SSLEngine engine = c.createSSLEngine();
params.setNeedClientAuth(false);
params.setCipherSuites(engine.getEnabledCipherSuites());
params.setProtocols(engine.getEnabledProtocols());
SSLParameters defaultSSLParameters = c.getDefaultSSLParameters();
params.setSSLParameters(defaultSSLParameters);
} catch(Exception ex) {
ex.printStackTrace();
}
}
});
server.createContext("/", new Handler());
server.setExecutor(null);
server.start();
一切正常,但现在我正在尝试管理在同一端口上到达此 HTTPS 服务器的 HTTP 请求。当我使用 HTTP 协议打开网页(由我的 HttpHandler 处理)时,它显示错误 ERR_EMPTY_RESPONSE
,只有 HTTPS 协议有效。
我想自动将所有传入连接从 HTTP 重定向到 HTTPS。或者为两种协议使用不同的 HttpHandler 会很棒。
我怎样才能做到这一点?
经过几个小时的尝试和哭泣,我想通了。
首先,我将 HTTP 服务器库从官方更改为 org.jboss.com.sun.net.httpserver
。 (download source code here)
然后我查看了该库的源代码,发现 class org.jboss.sun.net.httpserver.ServerImpl
负责所有 HTTP(S) 流量处理。当你查看static runnable subclassExchange
中的方法run
时,你可以看到这段代码:
/* context will be null for new connections */
context = connection.getHttpContext();
boolean newconnection;
SSLEngine engine = null;
String requestLine = null;
SSLStreams sslStreams = null;
try {
if(context != null) {
this.rawin = connection.getInputStream();
this.rawout = connection.getRawOutputStream();
newconnection = false;
} else {
/* figure out what kind of connection this is */
newconnection = true;
if(https) {
if(sslContext == null) {
logger.warning("SSL connection received. No https contxt created");
throw new HttpError("No SSL context established");
}
sslStreams = new SSLStreams(ServerImpl.this, sslContext, chan);
rawin = sslStreams.getInputStream();
rawout = sslStreams.getOutputStream();
engine = sslStreams.getSSLEngine();
connection.sslStreams = sslStreams;
} else {
rawin = new BufferedInputStream(new Request.ReadStream(ServerImpl.this, chan));
rawout = new Request.WriteStream(ServerImpl.this, chan);
}
connection.raw = rawin;
connection.rawout = rawout;
}
Request req = new Request(rawin, rawout);
requestLine = req.requestLine();
[...]
这个 runnable class 决定连接类型是什么(HTTP 或 HTTPS)。该决定仅基于 https
布尔值 object (true/false) - 它声明您是使用 HttpsServer 还是 HttpServer class 作为您的服务器。
这就是问题所在 - 您希望根据访问者选择的协议向您的网页访问者显示内容,而不是根据服务器默认使用的协议。
下一个重要的事情是,当您使用 HTTP 协议访问 HttpsServer 时,服务器会尝试打开与您的 SSL 安全连接,然后无法解码未加密的数据(即代码 new Request(rawin, rawout);
,新请求开始从输入流 rawin
读取字节并失败)。
然后 class Request
抛出 IOException
,特别是 javax.net.ssl.SSLException
.
IOException 在此方法底部处理:
} catch(IOException e1) {
logger.log(Level.FINER, "ServerImpl.Exchange (1)", e1);
closeConnection(this.connection);
} catch(NumberFormatException e3) {
reject(Code.HTTP_BAD_REQUEST, requestLine, "NumberFormatException thrown");
} catch(URISyntaxException e) {
reject(Code.HTTP_BAD_REQUEST, requestLine, "URISyntaxException thrown");
} catch(Exception e4) {
logger.log(Level.FINER, "ServerImpl.Exchange (2)", e4);
closeConnection(connection);
}
所以你可以看到它在捕获到异常时默认关闭连接。
(因此 Chrome 显示错误 ERR_EMPTY_RESPONSE
)
解决方案很简单 - 将代码替换为:
} catch(IOException e1) {
logger.log(Level.FINER, "ServerImpl.Exchange (1)", e1);
if(e1 instanceof SSLException) {
try {
rawout = new Request.WriteStream(ServerImpl.this, chan);
StringBuilder builder = new StringBuilder(512);
int code = Code.HTTP_MOVED_PERM;
builder.append("HTTP/1.1 ").append(code).append(Code.msg(code)).append("\r\n");
builder.append("Location: https://www.example.com:"+ wrapper.getAddress().getPort() +"\r\n");
builder.append("Content-Length: 0\r\n");
String s = builder.toString();
byte[] b = s.getBytes("ISO8859_1");
rawout.write(b);
rawout.flush();
} catch(IOException e) {
e.printStackTrace();
}
}
closeConnection(connection);
} catch [...]
在关闭连接之前,它会为未加密的输出流设置正确的输出流rawout
,然后发送重定向代码(301 永久移动)以及要重定向到的位置地址。
只需将 www.example.com 更改为您的主机名。端口是自动添加的。唯一的问题是也尝试自动获取主机名。唯一可以提取此信息的地方是来自输入流(来自请求headers),但此流在抛出异常后不再可用。
现在您网页的访问者从 HTTP 协议重定向到 HTTPS。
希望对您有所帮助!