Search code examples
javahttp-redirectsslhttpshttpserver

How to manage HTTP request with Java HTTPS server?


I have set my own https server with com.sun.net.httpserver.HttpsServer library.

The code looks like this:

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();

Everything is working, but now I am trying to manage HTTP requests coming to this HTTPS server on the same port. When I open the webpage (which is processed by the my HttpHandler) with HTTP protocol, it says error ERR_EMPTY_RESPONSE, only HTTPS protocol works.

I would like to redirect all incoming connections from HTTP to HTTPS automatically. Or it would be great to use different HttpHandlers for both protocols.

How can I achieve this?


Solution

  • I figured it out after many hours of trying and crying.

    First I changed HTTP server lib from the official to org.jboss.com.sun.net.httpserver. (download source code here)

    Then I looked into the source code of that lib and found out that class org.jboss.sun.net.httpserver.ServerImpl is responsible for all HTTP(S) traffic handling. When you look at method run in static runnable subclass Exchange, you can see this code:

    /* 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();
    [...]
    

    This runnable class decides what the connection type is (HTTP or HTTPS). The decision is based only on value of https boolean object (true/false) - it declares whether you use HttpsServer or HttpServer class as your server. That's the problem - you want to show to your webpage's visitor content based on the protocol which the visitor chooses, but not on the protocol which server uses by default.

    Next important thing is that when you visit your HttpsServer with HTTP protocol, then the server tries to open SSL secured connection with you and then it fails to decode unencrypted data (namely it is the code new Request(rawin, rawout);, new request starts reading bytes from input stream rawin and fails).

    Then the class Request throws IOException, specifically instance of javax.net.ssl.SSLException.

    IOException is handled in the bottom of this method:

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

    So you can see that it by default closes connection when exception is caught. (So Chrome shows error ERR_EMPTY_RESPONSE)

    The solution is simple - replacing the code with this:

    } 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 [...]
    

    Before closing the connection it sets correct outputstream rawout for unencrypted output stream and then it sends redirection code (301 Moved Permanently) with location address to be redirected to.

    Just change www.example.com to your hostname. Port is added automatically. Only problem would be trying to get the hostname automatically too. Only place where you could extract this information is from the input stream (from the request headers), but this stream is after throwing the exception no longer available.

    Now the visitors of your webpage are redirected from the HTTP protocol to HTTPS.

    Hope this helped!