1

Question:

Is there a way to support CONNECT HTTP Requests with the Oracle HTTPServer class?

Note: If you do not want to deal with this, Jetty seems to not have this problem with their HTTPServer class (even when not using their ProxyHTTP implementation ConnectHandler). Grizzly however seemed to have the same issue as the default Java implementation, not surprising since they were both made by Oracle.

Explanation and Difficulty:

Note: I include this to provided a detailed explanation of what I believe to be the issue.

I've recently been trying to make a HTTP proxy for secure (HTTPS) connections on Firefox. The way this is accomplished is the CONNECT method (IETF RFC 2817). An example of the CONNECT request from Firefox is the following (which occurred when requesting the Google homepage).

CONNECT www.google.com:443 HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:58.0) Gecko/20100101 Firefox/58.0
Proxy-Connection: keep-alive
Connection: keep-alive
Host: www.google.com:443

this differs from a normal GET request.

GET /index HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:58.0) Gecko/20100101 Firefox/58.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1

The notable difference is the so called 'request target' (see this Mozilla article for details ). Specifically the normal GET request is a 'absolute path' whereas the CONNECT request target is the 'authority form'.

Using the demo server in this StackOverflow answer (simple Java HTTPServer implementation) we can see the way to register a HTTPHandler is server.createContext("/test", new MyHandler()), which according to the Oracle documentation MUST begin with a '/'. However because the 'authority form' doesn't contain a '/' at the start the handle is never triggered

Note: This is still somewhat speculation as Oracle's code is closed source, but it was confirmed by sending the same TCP request to the server with the same CONNECT request above, but one time with a leading '/' (so '/www.google.com' instead of 'www.google.com'). With the leading '/' the handler was triggered.

With all this said my question is this,

Is there a way around this, while still using the included libraries in the Oracle version of Java?

Note: Other than writing an new HTTP Server from scratch, or using another library. In my testing Jetty did trigger the HTTPHandler. Grizzly did not (and, in my opinion, has some code quality issues).

Zimm3r
  • 3,369
  • 5
  • 35
  • 53
  • 1
    JDK's `HttpServer`, while not part of the officially supported API, is not closed source, see [here](https://github.com/dmlloyd/openjdk/tree/jdk8u/jdk8u/jdk/src/share/classes/com/sun/net/httpserver) and [there](https://github.com/dmlloyd/openjdk/tree/jdk8u/jdk8u/jdk/src/share/classes/sun/net/httpserver), perhaps exploring the source code can help with your endeavor. – Hugues M. Feb 18 '18 at 15:06
  • @HuguesM. Thanks, I did think it was closed source. My mistake. – Zimm3r Feb 18 '18 at 18:55

2 Answers2

1

You can view the source code at below link

https://github.com/akashche/java-1.8.0-openjdk-1.8.0.151-2.b12.fc28.ppc64le

So below are the few files in play here

  • /sun/net/httpserver/ServerImpl.java
  • /sun/net/httpserver/ContextList.java

When the http server tries to get the path for a CONNECT request

ctx = contexts.findContext (protocol, uri.getPath());

It has uri as www.google.com:443 and uri.getPath() returns null as shown in below screenshot

Context path null

The ContextList class has below methods of context

public synchronized HttpContextImpl createContext (String path, HttpHandler handler) {
    if (handler == null || path == null) {
        throw new NullPointerException ("null handler, or path parameter");
    }
    HttpContextImpl context = new HttpContextImpl (protocol, path, handler, this);
    contexts.add (context);
    logger.config ("context created: " + path);
    return context;
}

public synchronized HttpContextImpl createContext (String path) {
    if (path == null) {
        throw new NullPointerException ("null path parameter");
    }
    HttpContextImpl context = new HttpContextImpl (protocol, path, null, this);
    contexts.add (context);
    logger.config ("context created: " + path);
    return context;
}

Both have check for path null and doesn't allow you to set a context with null path. This means we can never get the context to work in this case. For a CONNECT request to work we need to be able to register a handler with path as null or some kind of default catch all handler. This is not supported in the current code as I have checked. And all these class are not marked public, so you can't inherit from these and get it working.

But since you have the source code, if you really want to make it work, you will have to copy the two files and make some modifications and get it working.

So short answer is that it is NOT possible to do this with the original com.sun.net.httpserver.HttpServer from Oracle. I have checked JDK 1.8 and not 1.9, but I doubt there would be much change to these classes in 1.9

Tarun Lalwani
  • 142,312
  • 9
  • 204
  • 265
  • 1
    Nice analysis. :-) @Zimm3r: I could maybe help you fix the null path and put something else there instead via AspectJ, but you seem not to want any third-party libraries on your class path. In this case you would need the AspectJ weaver. Tell me if you are interested anyway. But it would probably be easier for you to just use a 3rd party lib for your server. – kriegaex Feb 19 '18 at 01:12
  • @kriegaex No, I was just hoping for no 3rd party libraries, but if I'm going to go the 3rd party library wrote I might as well use another HTTP library. The other options I thought of was patching the private variables context list like so https://stackoverflow.com/questions/32716952/set-private-field-value-with-reflection – Zimm3r Feb 19 '18 at 03:51
  • 1
    Another option is to copy the OpenJDK source code of `ContextList` (incl. package name!) and change `findContext` so as to return a registered context with a magic name like "/SOCKS" if the path is null. This way the handler gets called and can do whatever you like. **But** the ugly thing is that in order to override the JRE class, you need to prepend the classes directory or JAR containing the manipulated class to the Java boot classpath via `-Xbootclasspath/p:/my/path`. It works nicely, but in a way is worse than using a 3rd party lib. Let me know if you want an answer for this. – kriegaex Feb 19 '18 at 12:20
  • @kriegaex Ya these get more and more ugly. Not to say that is bad if needed, but it is probably easier to go the third party route or write a basic http server. But certainly good options if those aren't available. – Zimm3r Feb 19 '18 at 22:51
1

For what it is worth - let us just call it a finger exercise - here is the little hack which enables you to get your handler triggered for CONNECT calls.

1. Modify class ContextList from OpenJDK

Copy classContextList from JDK 8 (or look for 9) into a new project. The raw file is here.

Now modify the class by inserting this little code snippet marked by "snip/snap":

synchronized HttpContextImpl findContext (String protocol, String path, boolean exact) {
  protocol = protocol.toLowerCase();
  String longest = "";
  HttpContextImpl lc = null;
  for (HttpContextImpl ctx: list) {
    // --- snip -----------------------------
    if (path == null) {
      if (ctx.getPath().equals("/_CONNECT_")) {
        System.out.println("Null path detected, using CONNECT handler");
        return ctx;
      }
      continue;
    }
    // --- snap -----------------------------
    if (!ctx.getProtocol().equals(protocol)) {
      continue;
    }
    // (...)
  return lc;
}

I added log output in order to more easily determine whether the new code is executed in case of an empty URL path. You can remove it if you do not need it.

Compile the project into a JAR such as my.jar.

2. Use modification for your own project

Change the demo class for this answer in order to register a new dummy context /_CONNECT_ (or whatever you called it in the JDK modification) meant to be used for CONNECT calls.

package de.scrum_master.app;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

public class Application {

  public static void main(String[] args) throws Exception {
    HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
    server.createContext("/test", new MyHandler());
    server.createContext("/_CONNECT_", new MyHandler());
    server.setExecutor(null); // creates a default executor
    server.start();
  }

  static class MyHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange t) throws IOException {
      String response = "This is the response";
      t.sendResponseHeaders(200, response.length());
      OutputStream os = t.getResponseBody();
      os.write(response.getBytes());
      os.close();
    }
  }

}

3. Start JVM with modified class on boot classpath

Prepend the JAR to the boot classpath via JVM argument (e.g. in your IDE run configuration for the project using this modified class) in order to let the JRE find the modified class before the original one:

java -Xbootclasspath/p:/path/to/my.jar ...

4. Test your server

$ curl http://localhost:8000/test
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    20  100    20    0     0     20      0  0:00:01 --:--:--  0:00:01   645This is the response

$ curl -p -x http://localhost:8000 https://scrum-master.de
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
curl: (35) error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol

You can see that both the /test context and the /_CONNECT_ context work. Of course, the latter is not doing the right thing, but that is up to you. The goal was to get it triggered and it is, as you can see on the server's console which should print Null path detected, using CONNECT handler for the second request.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • That's one way to do it. One of the reasons I've never been a fan of private variables. Still pretty amazing java has this facility. – Zimm3r Feb 21 '18 at 01:09
  • I do not understand that comment. I am not using private variables. But maybe you are relating to the private variables in the JDK code you want to hack. The problem are not private variables here - not having them would be bad IMO and kill class encapsulation - but just that you cannot inject dependencies (e.g. an `HttpContext` implementation of your own) the way you need because the classes were not designed to be extended or overridden that way. – kriegaex Feb 21 '18 at 01:56
  • Yes I was referring to the HttpContext, specifically patching the paths it has that are compared. – Zimm3r Feb 21 '18 at 04:29
  • So what is your experience with my solution? Does it help you implement your proxy server? Did you play with it and make progress or have you already decided to use another tool? I put quite some time into my little exercise because I was curious, so I am curious to know about the outcome. – kriegaex Feb 21 '18 at 10:44
  • It did work, however I decided on implementing my own http server from scratch with sockets and threads do to a number of constraints, mainly that I intended for it to be as easily runnable as possible, and loading a new class path is difficult. Also the OpenJDK code is GPL'd and so there were license worries. – Zimm3r Feb 24 '18 at 03:36