如何使用 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。

希望对您有所帮助!