2015-09-24

Building a two way https web service server using Java


I have the following requirements:
  • to build a multi-thread http server serving inbound web service requests via POST
  • the request is not conventional SOAP but is still XML-based
  • both client and server authentication (certificate based) is needed (riding not https)

 After some googling I found that Java provides a convenient framework to deliver this capability (as in Meisch's article "Java Webservice using HTTPS part 2")

The major classes used is:
  1. com.sun.net.httpserver.HttpsServer (abstract class); and
  2. com.sun.net.httpserver.HttpHandler (interface)
As HttpsServer is an abstract class, it cannot be instantiated (creating an object via new operator).  My example class java_https_server is therefore to implement the HttpHandler interface and have a method CreateHttpServer to create an HttpsServer object via the static HttpsServer.create() method.

The logic of the program is as follows:
  1. inside the main method, after creating a dummy java_https_server object, invoke the CreateHttpServer method to create a HttpsServer object
  2. inside CreateHttpServer method, open a Java Key Store containing the web server's own certificate and the Trust Store containing the trusted client certificate (the latter is needed because I use two-way SSL).  An SSLContext object instance is linked to these two key stores.  Finally, a context (which I find useless because I cannot still associate different handler with different context) is created for the HttpsServer object.
  3. the main method continue to set up a shutdown hook to graceful termination handling
  4. since the java_https_server implements the HttpHandler interface, a handle() method is defined  This is the core running logic of the whole web service server. I have added many println statements to display the attributes of the http connection in my codes.
The source listing is as follows:

import java.io.FileInputStream;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpsServer;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpsExchange;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsParameters;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.SSLContext;
import java.security.KeyStore;
import java.net.URI;
import java.net.InetSocketAddress;

public class java_https_server implements HttpHandler {
private static final int HTTP_OK_STATUS = 200;
// ----------class property --------------
private String context = "/";
private int port = 8000;
private String keystorePasswordString = "password";
private String keystoreFile = "/full_path/keystore.jks";
private String truststorePasswordString = "password";
private String truststoreFile = "/full_path/truststore.jks";
// --------- Constructor -------------------
public java_https_server () {
  }
// --------------------------------------------
public HttpsServer CreateHttpServer(int port, String context) {
  HttpsServer httpServer;
  try {
    httpServer = HttpsServer.create(new InetSocketAddress(port), 0);
    SSLContext sslContext = SSLContext.getInstance("TLS");
    // server keystore
    char[] keystorePassword = keystorePasswordString.toCharArray();
    KeyStore ks = KeyStore.getInstance("JKS");
    ks.load (new FileInputStream(keystoreFile), keystorePassword);
    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    kmf.init (ks, keystorePassword);
    // server truststore
    TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
    char[] truststorePassword = truststorePasswordString.toCharArray();
    ks.load (new FileInputStream(truststoreFile), truststorePassword);
    tmf.init (ks);
    sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
    // create an anonymous inner class HttpsConfigurator to require client certificate
    HttpsConfigurator configurator = new HttpsConfigurator(sslContext) {
      public void configure (HttpsParameters params) {
        SSLParameters sslParams = getSSLContext().getDefaultSSLParameters();
        sslParams.setNeedClientAuth(true);
        params.setSSLParameters(sslParams);
        }
      };
    httpServer.setHttpsConfigurator(configurator);
    //Create a new context for the given context and handler
    httpServer.createContext(context, this);
    //Create a default executor
    httpServer.setExecutor(null);
    }
  catch (Exception e) {
    e.printStackTrace();
    return null;
    }
  return httpServer;
  } // method CreateHttpServer
// --------------------------------------------
public void handle(HttpExchange t) throws IOException {
  URI uri = t.getRequestURI();
  String response;
  System.out.println ("LocalAddress: " + t.getLocalAddress().toString());
  System.out.println ("RemoteAddress: " + t.getRemoteAddress().toString());
  System.out.println ("URL is: " + uri.toString());
  System.out.println ("Method: " + t.getRequestMethod());
  System.out.println ("Client Certficate: " +
    ((HttpsExchange)t).getSSLSession().getPeerCertificateChain()[0].getSubjectDN());
  if (t.getRequestMethod().equals ("POST")) {
    InputStream is = t.getRequestBody();
    byte[] data = new byte[100000];
    int length = is.read(data);
    if (length == 100000)
      System.out.println ("Warning: the input buffer is FULL!");
    System.out.println ("Request Length: " + length);
    data = java.util.Arrays.copyOf(data, length); // trim the array to the correct size
    System.out.println ("Request Body:[" + new String(data) + "]");
    is.close();
    response = "Give your response here";
    }
  else {
    response = "Error";
    }
  //Set the response header status and length
  t.sendResponseHeaders(HTTP_OK_STATUS, response.getBytes().length);
  //Write the response string
  OutputStream os = t.getResponseBody();
  os.write(response.getBytes());
  os.close();
}
// --------------------------------------------
public static void main(String[] args) throws Exception {
  java_https_server server = new java_https_server();
  System.out.println("Use Ctrl-C (foreground) or \"kill -15 (background)\" to stop me");
  final HttpsServer httpServer = server.CreateHttpServer(server.port, server.context);
  Runtime.getRuntime().addShutdownHook(new Thread() {
    @Override
    public void run() {
      System.out.println("Stopping Server..");
      httpServer.stop(0);
      System.out.println("Server stopped");
      } // run()
    });
  httpServer.start(); // Start the server
  System.out.println("Server is started and listening on port "+ server.port);
  } // method main
} // class java_https_server

Some example runtime output is as follows:
$ java -cp . java_https_server
Use Ctrl-C (foreground) or "kill -15 (background)" to stop me
Server is started and listening on port 8000
LocalAddress: /127.0.0.1:8000
RemoteAddress: localhost/127.0.0.1:52204
URL is: /app
Method: POST
Client Certficate: CN=ccm010, OU=xxx, O=xxx, L=xxx, ST=xxx, C=HK
Request Length: 385
Request Body:[<?xml version='1.0'?><Envelope xmlns='http://schemas.xmlsoap.org/soap/envelope/' xmlns:op='http://schemas.xyz.com/svc' xmlns:ems='http://schemas.xyz.com/ems'><Header>Header_Text</Header><Body><op:AddPbs><ems:ChannelAcctId><ems:ChannelId>06</ems:ChannelId><ems:AcctId>1234567</ems:AcctId></ems:ChannelAcctId></op:AddPbs></Body></Envelope>]
Stopping Server..
Server stopped

I will cover how to test using openssl with client certificate in the next post.

Using openssl to test two way SSL connectivity