An HTTP server in Java, part 3

In my previous two posts, I walked through the development of a functional mini-HTTP server in Java that I use to mock out external calls when I want to emulate specific hard-to-reproduce events like server failures. Although it covers most of the functionality that I need right now (while still falling far short of being a specification compliant HTTP server), there are at least two more features that are useful for testing specific scenarios: cookie handling and persistent connections.

Cookies

Cookies allow HTTP servers to exhibit stateful behavior (with respect to a particular client) while not actually storing any state, allowing each network request to be serviced independently. Conceptually, cookies are simple - the server sends the client (i.e. a browser) a piece of information to "remember" and the client is responsible for sending that piece of data back on each subsequent request.

If you've even dabbled in HTTP, it should come as no surprise that this is implemented in HTTP headers: the server should send a response header called Set-Cookie and the client should parse it and then respond with its own request header Cookie. The value itself is sort of arcane; the value of the Set-Cookie is a comma-delimited list of key-value pairs which themselves each include metadata about the applicability of the cookie.

Strictly speaking, then, since the implementation of my mini-HTTP server provides handlers access to request and response headers, the server implementation doesn't actually have to change to support cookies. I could just push all the responsibility down to the individual handlers, but since Cookie behavior follows some fairly standard patterns, it's worth just doing it once. Basic cookie support could be added by including a line in the form:

response.addHeader("Set-Cookie", "name=value");
In the response handler and then including code such as:
request.getHeader("Cookie").split("=")[1];
Assuming that only one cookie was in effect. Whereas the responder can include as many Set-Cookie headers as it cares to, the requester will generally only include one corresponding Cookie header in the request with all of the cookie values concatenated together into one. So, if the response included the following headers:
Set-Cookie: abc=123
Set-Cookie: def=456
Set-Cookie: ghi=789
The next request will have the consolidated header:
Cookie: abc=123; def=456; ghi=789
One limitation of the server I've developed so far, though, is that there can only be one instance of each header value per response, which is a problem for cookies because multiple cookies require multiple headers each named Set-Cookie. In listing 1 below, then, I'll expand the Response class to handle multiple repeated headers.

public class Response {
  private OutputStream out;
  private int statusCode;
  private String statusMessage;
  private Map<String, List<String>> headers = new HashMap<String, List<String>>();
  private String body;

  public Response(OutputStream out) {
    this.out = out;
  }

  public void setResponseCode(int statusCode, String statusMessage) {
    this.statusCode = statusCode;
    this.statusMessage = statusMessage;
  }

  public void addHeader(String headerName, String headerValue)  {
    List<String> headerValues = this.headers.get(headerName);
    if (headerValues == null) {
      headerValues = new ArrayList<String>();
      this.headers.put(headerName, headerValues);
    }

    headerValues.add(headerValue);
  }

  public void addBody(String body)  {
    addHeader("Content-Length", Integer.toString(body.length()));
    this.body = body;
  }

  public void send() throws IOException {
    addHeader("Connection", "Close");
    out.write(("HTTP/1.1 " + statusCode + " " + statusMessage + "\r\n").getBytes());
    for (String headerName : headers.keySet())  {
      Iterator<String> headerValues = headers.get(headerName).iterator();
      while (headerValues.hasNext())  {
        out.write((headerName + ": " + headerValues.next() + "\r\n").getBytes());
      }
    }
    out.write("\r\n".getBytes());
    if (body != null) {
      out.write(body.getBytes());
    }
  }
}

Listing 1: multiple header values

Here I've changed the Map<String, String> into a Map<String, List<String>> — singular header values are just lists of length one now. With this change, I can append as many cookie values as I care to.

However, the Set-Cookie header is still more complex than a simple name/value pair. There are seven pieces of optional metadata that can be included with each cookie. To include them, the sender should append a semicolon after the cookie specification and then include the metadata element. Of the seven, all but two accept additional parameters. To save some headache for the handler writer, then, it makes sense to create a dedicated class to specify the cookie. This is illustrated in listing 2.

public class Cookie     {
  private String name;
  private String value;
  private Date expires;
  private Integer maxAge;
  private String domain;
  private String path;
  private boolean secure;
  private boolean httpOnly;
  private String sameSite;

  public Cookie(String name,
                String value,
                Date expires,
                Integer maxAge,
                String domain,
                String path,
                boolean secure,
                boolean httpOnly,
                String sameSite)        {
    this.name = name;
    this.value = value;
    this.expires = expires;
    this.maxAge = maxAge;
    this.domain = domain;
    this.path = path;
    this.secure = secure;
    this.httpOnly = httpOnly;
    this.sameSite = sameSite;
  }

  public String toString()        {
    StringBuffer s = new StringBuffer();

    s.append(name + "=" + value);

    if (expires != null)    {
      SimpleDateFormat fmt = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss");
      s.append("; Expires=" + fmt.format(expires) + " GMT");
    }

    if (maxAge != null)     {
      s.append("; Max-Age=" + maxAge);
    }

    if (domain != null)     {
      s.append("; Domain=" + domain);
    }

    if (path != null)       {
      s.append("; Path=" + path);
    }

    if (secure)     {
      s.append("; Secure");
    }

    if (httpOnly)   {
      s.append("; HttpOnly");
    }

    if (sameSite != null)   {
      s.append("; SameSite=" + sameSite);
    }

    return s.toString();
  }
}

Listing 2: Cookie class

In short - the Expires and Max-Age parameters specify how long the cookie should be remembered; if omitted, the cookie should not be remembered past the current browser session. The domain and path indicate which subset of the current server the cookie should be returned to: if omitted, the cookie should be returned by the client on every request to the domain that the cookie came from. Of course, for security reasons, no compliant browser will allow a server to set cookies outside its domain, but the server can restrict the cookie to subdomains. Secure and HttpOnly indicate that the cookie should only be returned when the connection is an HTTPS connection or when the request is made by the browser (as opposed to a JavaScript XMLHttpRequest), respectively, and SameSite can be set to Strict or Lax to indicate whether or not the cookie should only be sent if the request originated from the cookie's own site. These last three are security options to guard against XSS and CSRF attacks.

With this class, the handler can instantiate a Cookie instance and invoke addHeader with Set-Cookie and the string value of the cookie itself, but the following convenience method in Response makes this clearer:

public void addCookie(Cookie cookie)  {
  addHeader("Set-Cookie", cookie.toString());
}

As I pointed out before, the client is responsible for passing back cookie values — the specification requires that all cookies be transmitted in a single Cookie header. The cookie header value is a semicolon (;) delimited list of name-value pairs, each of which is a cookie that was sent back by the server in some prior request. The metadata is not returned by the client; all of the metadata values indicate to the client how and when to return the cookie so are not meaningful to the server. Although I have a Cookie class, there's no good reason to parse the request cookies into instances of it since they're just name/value pairs; however, I should go ahead and parse the name/value pairs themselves and make them easily accessible. I'll modify the header parsing routine of Request.parse to handle the Cookie header specially as shown in listing 3.

  String name = headerLine.substring(0, separator);
  String value = headerLine.substring(separator + 2);
  headers.put(name, value);

  if ("Cookie".equals(name))  {
    parseCookies(value);
  }

Listing 3: request parser changes

Assuming conforming input, the cookie parsing is pretty simple:

public class Request  {
  ...
  private Map<String, String> cookies = new HashMap<String, String>();

  private void parseCookies(String cookieString)  {
    String[] cookiePairs = cookieString.split("; ");
    for (int i = 0; i < cookiePairs.length; i++)  {
      String[] cookieValue = cookiePairs[i].split("=");
      cookies.put(cookieValue[0], cookieValue[1]);
    }
  }

  public String getCookie(String cookieName)  {
    return cookies.get(cookieName);
  }

Listing 4: Cookie parser

Connection: Keep-Alive

One last change. I've actually been lying this whole time about this server: I keep saying it's HTTP/1.1, but the way it's coded, it isn't. The biggest omission here is that I'm not honoring the Connection: keep-alive header. In the first revision of HTTP (0.9 if you're counting, but continuing on to 1.0), the actual socket connection was closed by the client once the request was sent and the response was received. Since almost every useful HTTP interchange involves several back-to-back requests to the same origin server, HTTP/1.1 introduced the Connection: Keep-Alive header that the client could set to indicate that it wanted to be able to send another HTTP request right after the last one had been sent (this is why Content-Length and chunked transfer-encoding are so important - there's no other way for the two parties to tell when the transmission is complete otherwise). For example, if the browser downloads a page with embedded img tags, it can immediately begin requesting those images over the same connection while it's still parsing the response — in fact, it can request the next image before the first one has been fully downloaded. The client indicates that it is willing and able to do this by sending a Connection: keep-alive header and the server responds with either a corresponding keep-alive header or a Connection: close header indicating that it can't do that. So far, that's what my server does, and the browsers I tested against degrade gracefully and dutifully open new connections. It's not too much of a stretch to honor keep alives, though.

The socket that handles the connection is opened and processed in HttpServer.run. I can add a loop around the request and response handler there and wait until the client indicates that the connection should be closed. The HTTP/1.1 specification actually mandates that, if the client advertises that it understands HTTP/1.1 but omits the Connection header, the connection should default to keep alive. You might expect that the client would send a flurry of requests followed by a final request with Connection: Close, but in actual practice most clients just leave the connection open and leave it to the server to time it out after a period of inactivity, so it's important for the implementation to take this into account and terminate the connection after a pause in communication.

class SocketHandler implements Runnable  {
  ...
  public void run()  {
    BufferedReader in = null;
    OutputStream out = null;

    try  {
      socket.setSoTimeout(10000);
      in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
      out = socket.getOutputStream();
      boolean done = false;

      while (!done)  {
        Request request = new Request(in);
        try  {
          if (!request.parse())  {
            response(500, "Unable to parse request", out);
            return;
          }
        } catch (SocketTimeoutException e)  {
          break;
        }

        if ("close".equalsIgnoreCase(request.getHeader("connection")))  {
          done = true;
        }
        ...
        response.send(request.getHeader("connection"));

Listing 5: Persistent socket connections

Now, as coded, I read the request, send the response in its entirety, and then loop back around and look for a subsequent request. To properly support HTTP "pipelining", I actually ought to look for the next request concurrently while sending the response. I haven't done this here, but it's easy enough to wrap the last half of the run method in a Runnable and spawn another thread to do so — out and request have to be declared final (in Java versions prior to 1.8) and a bit of error handling has to be shuffled around, but otherwise it's easy to support. I tried it and didn't see much functional or performance difference, but it's something to be aware of if you're looking at HTTP implementations.

Add a comment:

Completely off-topic or spam comments will be removed at the discretion of the moderator.

You may preserve formatting (e.g. a code sample) by indenting with four spaces preceding the formatted line(s)

Name: Name is required
Email (will not be displayed publicly):
Comment:
Comment is required
banzai, 2021-04-17
Hi!

This article (and previous articles 077 and 076) is great! Thanks for your effort.

There's a minor typo in Listing 3:

```
public void addCookie(Cooke cookie) {
addHeader("Set-Cookie", cookie.toString());
}
```
Cooke should be Cookie.


banzai, 2021-04-17


    public void addCookie(Cooke cookie) {
        addHeader("Set-Cookie", cookie.toString());
    }
Josh, 2021-04-26
I can't believe I missed that! Fixed, thanks. Sorry, I usually do a better job of proofreading these.
f.dum, 2021-12-25
Another really nice series. Thanks!

I like that you tackle stuff where most serious "software engineers" automatically shout something about re-inventing something and then point you to libraries weighing hundreds of megabytes.

In fact, I have written my own mini-HTTP server -- which BTW is called exactly that, because my own libraries are called "mini" to remind myself to stay minimal). I will upgrade with some of the features you added (keep-alive and maybe cookie handling).

I created that server mainly to be able to wrap code that relies on heavy libraries in the server, to reduce JAR dependencies of my application code.

For example, I have a library that extracts metadata from a bunch of files. It relies on dozens of fat libraries for decoding PDF, Microsoft Office, various image and video formats, etc. -- definitively stuff where I don't want to reinvent the heptadecagon. I just POST my files, get the metadata as JSON, and it works like a charm. Maybe a little slower than without localhost connections, but my application code is lean and mean and clean, and to me, that weighs up small performance benefits.
My Book

I'm the author of the book "Implementing SSL/TLS Using Cryptography and PKI". Like the title says, this is a from-the-ground-up examination of the SSL protocol that provides security, integrity and privacy to most application-level internet protocols, most notably HTTP. I include the source code to a complete working SSL implementation, including the most popular cryptographic algorithms (DES, 3DES, RC4, AES, RSA, DSA, Diffie-Hellman, HMAC, MD5, SHA-1, SHA-256, and ECC), and show how they all fit together to provide transport-layer security.

My Picture

Joshua Davies

Past Posts