如何使用Java HTTPS服务器管理HTTP请求?

时间:2015-12-31 15:05:56

标签: java redirect ssl https httpserver

我已经使用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会很棒。

我怎样才能做到这一点?

1 个答案:

答案 0 :(得分:1)

经过几个小时的尝试和哭泣后,我想出来了。

首先,我将HTTP服务器lib从官方更改为org.jboss.com.sun.net.httpserver。 (download source code here

然后我查看了该lib的源代码,发现类org.jboss.sun.net.httpserver.ServerImpl负责所有HTTP(S)流量处理。当您查看静态可运行子类run中的方法Exchange时,您可以看到以下代码:

/* 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类决定连接类型(HTTP或HTTPS)。该决定仅基于https布尔对象的值(true / false) - 它声明您是否使用HttpsServer或HttpServer类作为您的服务器。 这就是问题 - 您希望根据访问者选择的协议显示您网页的访问者内容,而不是默认情况下服务器使用的协议。

接下来重要的是,当您使用HTTP协议访问HttpsServer时,服务器会尝试与您打开SSL安全连接,然后无法解码未加密的数据(即代码new Request(rawin, rawout);,新请求开始从输入流rawin读取字节并失败)。

然后,课程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 Moved Permanently),并将位置地址重定向到。

只需将 www.example.com 更改为您的主机名即可。端口自动添加。唯一的问题是尝试自动获取主机名。只有您可以提取此信息的位置来自输入流(来自请求标头),但此流是在抛出不再可用的异常之后。

现在,您网页的访问者将从HTTP协议重定向到HTTPS。

希望这有帮助!