HTTPS in a Production-Grade Web Server

The HttpsPublisher is simple enough in structure to illustrate the basics of wire-level security. Among the several reasons for going with a production-grade web server such as Tomcat or Jetty is that these servers provide such good support for HTTPS, at the application and at the administrative level. Although these web servers provide first-rate HTTPS support, they do require setup comparable to that illustrated with the HttpsPublisher. This section focuses on Tomcat.

Tomcat does not ship with a keystore of digital certificates and, accordingly, does not enable HTTPS by default. The service must be turned on by editing the configuration file TOMCAT_HOME/conf/server.xml, with details provided shortly. The same keystore file used in the HttpsPublisher example, test.keystore, could be re-used for Tomcat. A modern browser connecting over HTTPS to Tomcat should complain that the digital certificate in test.keystore is self-signed and, therefore, worthless as a security credential. In production, a keystore with commercial-grade keystore digital certificates would be needed. Yet the point of immediate interest is that Tomcat does require the programmer to jump through a few hoops in order to switch from an HTTP-accessible to an HTTPS-accessible service. There are only three such hoops:

The first two changes are covered in Setting Up Tomcat for HTTPS Support.

A website or a web service can instruct Tomcat to enforce HTTPS access to either the entire resource (for instance, all of the HTML pages in the website and all of the operations in the web service) or only parts thereof (for instance, to administrative HTML pages in the site or to selected operations in the service). The instructions to Tomcat occur in a security-constraint section of the web.xml deployment file. To illustrate, the RESTful predictions2 service of Chapter 2, originally deployed with HTTP access only, can be redeployed with HTTPS access only. This requires no change whatsoever in the code. The revised web.xml is Example 6-7.

The changes are limited to the security-constraint section (line 1). In this example, the security constraint is enforced on the entire resource because the url-pattern (line 2) has /* as its value. The deployed WAR file predictions2.war (created, as usual, with the Ant script) could be partitioned in subdirectories, for example:

/admin    ;; contains administrative operations
/public   ;; contains publicly accessible operations

Under this partition, the service operations in the /admin directory might require HTTPS but not the ones in the /public directory. To enforce this policy, the url-pattern in line 2 would change to /admin/*. The transport-guarantee element, with a value of CONFIDENTIAL (line 3), instructs Tomcat to enforce HTTPS access on the specified resource, in this example on the entire predictions2 WAR file. If a client tried to access the predictions2 service under HTTP, Tomcat would respond with an HTTP status code of 302 and the appropriate https URL, thereby signaling to the client that a new request with an HTTPS connection should be attempted.

Within the web-resource-collection element of web.xml, access constraints can be specified that depend on the HTTP verb of the client request. For example, the web.xml segment:

<web-resource-collection>
  <url-pattern>/*</url-pattern>
  <http-method>POST</http-method> 1
  <http-method>PUT</http-method>  2
</web-resource-collection>

specifies that access to the resource, in this case the entire predictions2 service, is constrained only on POST and PUT requests (lines 1 and 2). If no specific HTTP verbs are specified, then the constraint covers them all.

Example 6-8 shows the HttpsPredictionsClient against the predictions2 service.

Example 6-8. The HttpsPredictionsClient against the predictions2 service

import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import java.security.KeyStore;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import java.security.cert.X509Certificate;
import java.security.SecureRandom;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;

public class PredictionsHttpsClient {
    private static final String endpoint = "https://localhost:8443/predictions2";
    private static final String truststore = "test.keystore";

    public static void main(String[ ] args) {
        new PredictionsHttpsClient().runTests();
    }
    private void runTests() {
        try {
            SSLContext sslCtx = SSLContext.getInstance("TLS");
            char[ ] password = "qubits".toCharArray();
            KeyStore ks = KeyStore.getInstance("JKS");
            FileInputStream fis = new FileInputStream(truststore);
            ks.load(fis, password);
            TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
            tmf.init(ks); // same as keystore
            sslCtx.init(null,                   // not needed, not challenged
                        tmf.getTrustManagers(),
                        new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sslCtx.getSocketFactory());
            getTest();
            postTest();
            getTestAll();     // confirm POST test
            deleteTest("31");
            getTestAll();     // confirm DELETE test
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private HttpsURLConnection getConnection(URL url, String verb) {
        try {
            HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
            conn.setDoInput(true);
            conn.setDoOutput(true);
            conn.setRequestMethod(verb);
            conn.setHostnameVerifier(new HostnameVerifier() {
                    public boolean verify(String host, SSLSession session) {
                        return host.equals("localhost"); // for development
                    }
                });
            return conn;
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void getTest() {
        getTestAll();
        getTestOne("31");
    }
    private void getTestAll() {
        try {
            URL url = new URL(endpoint);
            HttpsURLConnection conn = getConnection(url, "GET");
            conn.connect();
            readResponse("GET all request:\n", conn);
            conn.disconnect();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void getTestOne(String id) {
        try {
            URL url = new URL(endpoint + "?id=" + id);
            HttpsURLConnection conn = getConnection(url, "GET");
            conn.connect();
            readResponse("GET request for " + id + ":\n", conn);
            conn.disconnect();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void postTest() {
        try {
            URL url = new URL(endpoint);
            HttpsURLConnection conn = getConnection(url, "POST");
            conn.connect();
            writeBody(conn);
            readResponse("POST request:\n", conn);
            conn.disconnect();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void deleteTest(String id) {
        try {
            URL url = new URL(endpoint + "?id=" + id);
            HttpsURLConnection conn = getConnection(url, "DELETE");
            conn.connect();
            readResponse("DELETE request:\n", conn);
            conn.disconnect();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void writeBody(HttpsURLConnection conn) {
        try {
            String pairs = "who=Freddy&what=Avoid Friday nights if possible.";
            OutputStream out = conn.getOutputStream();
            out.write(pairs.getBytes());
            out.flush();
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
    private void readResponse(String msg, HttpsURLConnection conn) {
        try {
            byte[ ] buffer = new byte[4096];
            InputStream in = conn.getInputStream();
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int n = 0;
            while ((n = in.read(buffer)) != -1) out.write(buffer, 0, n);
            in.close();
            System.out.println(new String(out.toByteArray())); // stringify and print
        }
        catch(Exception e) { throw new RuntimeException(e); }
    }
}

The PredictionsHttpsClient (see Example 6-8) is a test client against the HTTPS-deployed version of the predictions2 service. This client is roughly similar to the HttpsClient (Example 6-6) but methods such as readResponse and writeBody now are beefed up in order to make realistic CRUD requests against the service. For example, the postTest adds new Prediction to the collection, which requires that writeBody insert the key/value pairs for the key who (the predictor) and the key what (the prediction); the getTestAll must read all of the bytes returned from the service in order to display the Prediction list.

Tomcat’s approach to HTTPS exemplifies the separation-of-concerns principle. A web service (or a website) need not be changed at the code level to move from HTTP to HTTPS access. It bears repeating that no code in the original predictions2 service had to be changed; instead, only the deployment descriptor web.xml needed to change, and then only a little. Tomcat also assumes responsibility for enforcing HTTPS access in accordance with the policy given in the web.xml document: a client that now tries to hit the predictions2 service with an HTTP-based request is signaled that an HTTPS-based request should be used instead.