The org.bzdev.ejws package

The org.bzdev.ejws package provides a class for configuring instances of HttpServer or HttpsServer, and several classes that are useful for implementing an embedded web server. The following sections describe how this package is used:

Constructors

The class EmbeddedWebServer will configure an HttpServer or HttpsServer, including setting up a thread pool. Its Constructors use various combinations of the following arguments:

  • addr. The internet address (some constructors do not provide this explicitly, in which case the wildcard address is used).
  • port. The TCP port to use.
  • backlog. The TCP backlog.
  • nthreads. The number of threads the server uses.
  • sslSetup. An object whose type is EmbeddedWebServer.SSLSetup used to configure SSL for HTTPS.
  • certManager. An object whose type is CertManager used to configure SSL for HTTPS.
The certManager and sslSetup arguments are mutually exclusive. They provide the same functionality, except that CertManager can be used to automatically renew SSL certificates if an appropriate provider is available. Examples:
EmbeddedWebServer ews = new EmbeddedWebServer(8080, 48, 8);
and for the SSL case,
EmbeddedWebServer ews = new EmbeddedWebServer(8080, 48, 8,
                          new EmbeddedWebServer.SSLSetup("TLS")
                          .keystore(new FileInputStream("ks.jks"));

Authenticators

Two authenticators are provided by EJWS:

  • EjwsBasicAuthenticator. This is an abstract class with a single method to implement: checkCredentials, which checks that a given password is the one provided for a given user name. The simplest implementations use a Map to look up the password for a user and compares that to the given one.
  • EjwsSecureBasicAuth. This class implements secure basic authentication, which uses public-key encryption to generate a one-time password that is resistant to man in the middle attacks.
Both authenticators allow one to specify three BiConsumer objects whose "accept" methods are called
  • when a login request succeeds.
  • when an an authentication request has succeeded while already logged in.
  • when a logout request is seen.
Each BiConsumer has two type parameter: At most one of these BiConsumer.accept(T, U) methods will be called in each HTTP/HTTPS transaction. Typically, authenticators should be created before prefixes and contexts are added. Examples:
EjwsBasicAuthenticator auth =
     new EjwsBasicAuthenticator("realm");
auth.add("user", "password");
and
EjwsBasicAuthenticator auth =
    new EjwsSecureBasicAuthenticator("realm"
                                     ews.getCertificates());
// These strings can be created using the program sbl.
String user = "user"
String publicKey = [user's public key in PEM format]
String password = "password",
auth.add(user, publicKey, password);

One can also add functions to call at a login, logout, or when authentication is successful:

auth.setLoginFunction((p,t) -> {
    System.out.println(p.getName() + " logged in");
};
auth.setLogoutFunction((p,t) -> {
    System.out.println(p.getName() + " logged out");
};
auth.setAuthorizedFunction((p,t) -> {
    System.out.println(p.getName() + " authorized");
        };

Adding contexts

After a server is created one will create instances of HttpHandler and associate each with with a prefix (the initial portion of a URL's path). These methods are named "add", where

  • the method's first argument is the prefix.
  • the method's second argument is either an HttpHandler, a String naming a subclass of WebMap, or the class of a subclass of WebMap. In the latter two cases, the third argument is an object that will be passed to a WebMap's constructor, and the "add" method will return the WebMap that was just created. The package org.bzdev.ejws.maps contain several subclasses of WebMap covering common cases. These are described below. An HttpHandler will also be created&emdash;an instance of FileHandler.
  • the method's third argument, or forth argument when the third argument is used to initialize a WebMap, is an instance of Authenticator or null if authentication is not needed.
There may be additional arguments as well, depending on the method's signature. Subclasses of WebMap provide operations that map URLs to resources such as files in a file system, entries in a ZIP file, or a light-weight servlet implementation. The class WebMap contains several methods that return their instance so that methods can be chained together for convenience. These are
  • setLoginAlias(String alias, String target, boolean required), setLoginAlias(String alias, URI target, boolean required), and several variants with fewer arguments that provide defaults. The arguments are
    • alias—a relative, non-hierarchical path from the WebMap's context indicating that a login is requested. There may be no corresponding resource.
    • target—a relative path from the WebMap's context to the page that should be visited after a successful login
    • required—true if all logins must start by visiting the page indicated by the alias.
  • setLogoutAlias(String alias, URI target). The arguments are
    • alias—a relative, non-hierarchical path from the WebMap's context indicating that a logout is requested. There may be no corresponding resource.
    • target—a relative path from the WebMap's context to the page that should be visited after a logout.
  • addWelcome(String path)—add a "welcome" page. The argument is a path starting from the base URL for the WebMap to a welcome page. Multiple welcome pages can be added.
  • addMapping(String suffix, String mtype)—associate a media type with a file-name suffix or extension. The first argument is the file-name extension (an initial period will be ignored), and the second argument is the media type (MIME type).
  • addGzipSuffix(String suffix)—indicate that a suffix implies the use of GZIP compression. The argument, suffix is a suffix indicating that a file was compressed using GZIP. if a resource does not exist and adding such a suffix will match a resource that does exist, then that compressed version is sent and the headers are set to indicate that the recipient should automatically decompress the resource. The standard suffix is "gz" but it may be desirable in some cases to add additional ones (e.g., "gzip"). This allows compressed text files to be stored on a server and transferred while still compressed, and then automatically decompressed by a client's HTTP implementation.
For example:
ews.add("/", DirWebMap.class, new File("api"), auth, true, true, true);
    .setLoginAlias("login.html", "index.html", true)
    .setLogoutAlias("logout.html", "http://example.com")
    .addMapping("sbl", "application/vnd.bzdev.sblauncher")
    .addWelcome("index.html")
    .addGZipSuffix("gzip");
The class EmbeddedWebServer also includes partial support for WAR (Web ARchive) files and JSP (Java Server Pages). For WAR files, the WEB-INF/web.xml file is processed to find encodings for pages, media-types (MIME types) for file-name extensions, error pages for particular error codes or exceptions, and a welcome-file list. The use of the web.xml file is describe in the documentation for WebMap. JSP support is really minimal and is restricted to processing error messages. This is also describe in the documentation for WebMap.

Built-in WebMap subclasses

  • DirWebMap. This WebMap associates paths in a URL with files in a directory and its subdirectories. The constructor's argument is either an instance of File for a directory or an instance of DirWebMap.Config, whose constructor's arguments include a File and some strings representing various colors used when directories are displayed.
  • PropertiesWebMap. This WebMap associates paths in a URL with resources stored in a Properties instance. The constructor's argument is either a Properties instance or an instance of PropertiesWebMap.Config, whose constructor's arguments include an instance of Properties and some strings representing various colors used when directories are displayed.
  • RedirectWebMap. This WebMap will redirect its base path to URL representing another base path.
  • ResourceWebMap.This WebMap provides access to resources on an applications class path. The base path will be mapped to the initial portion of a resource's path, and the remainder of the path provided in a URL will be appended. The constructor's argument is either this path prefix or an instance of ResourceWebMap.Config, which provides that path and a series of colors.
  • ServletWebMap. This WebMap configures a light-weight Servlet— an instance of ServletAdapter. The ServletAdapter class is more or less a simplified Servlet implementation, providing the methods needed for the simplest cases. The ServletWebMap constructor's argument is an instance of ServletWebMap.Config, whose arguments include an instance of ServletAdapter, a Map providing initialization parameters, a boolean flag indicating if the ServletAdapter can process HTTP queries, and the HTTP methods the ServletAdapter can process.
  • URLWebMap. This WebMap will obtain a resource by fetching it from another server. The constructor's argument is either a URL or an instance of URLWebMap.Params, which provides a base URL and indicates if HTTPS requests should not be verified. The portion of a path following the context path is appended to the base URL. Then the corresponding object is downloaded by this server and sent to the requester.
  • ZipWebMap. This WebMap associates paths in a URL with entries in a ZIP file. The constructor's argument is either a String naming a ZIP file, a File referencing a ZIP file, or an instance of ZipWebMap.Config, which provides the ZIP file and some colors used when displaying directories.

Certificate Managers and SSLSetup

Both EmbeddedWebServer.SSLSetup and CertManager configure SSL by providing a keystore, truststore, and the corresponding passwords. They can also set a Configurator for applying additional SSL configuration options. If it necessary to modify a server's certificate, the method modifyServerSetup(SSLSetup) should be called before the server is stopped. A similar method will typically be called by a certificate manager automatically.

Instances of SSLSetup are created by calling its constructor:

  • SSLSetup(). This will create an instance of SSLSetup using TLS.
  • SSLSetup(String). This will create an instance of SSLSetup using a secure-socket protocol. The argument should be either "SSL", "TLS", or a protocol listed in the Java API documentation.
Methods used to configure instances of SSLSetup include Additional methods can configure a trust store, if desired. If a server needs a trust store to open connections to a server, use the methods in SSLUtilities.

Certificate managers are created by calling either CertManager.newInstance() or CertManager.newInstance(String), whose argument is a provider name. Each instance will use a keystore that is expected to contain an entry using the alias servercert. There are two default providers

  • default. This provider creates a self-signed certificate, replacing it when the certificate is close to expiring. The keystore entry whose alias is servercert will be created or modified when close to expiring.
  • external. This provider reloads the keystore when its servercert certificate is close to expiring, but does not modify the keystore.

The methods used to configure a CertManager return the certificate manager itself so that the methods can be chained together for programming convenience. The methods used to configure how certificates are renewed and how to find certificates are

  • setTimeOffset(int). This method provides a time offset in seconds from midnight and is used to schedule checks for expired certificates at a time at which the server is not likely to be busy: the server has to be stopped temporarily to install new certificates.
  • setInterval(int). This method provides the interval in days between certificate renewal attempts. A certificate will not be renewed if it is not close to expiring. If set to zero, the value will be treated as one minute, which is useful for testing but not for actual use.
  • setStopDelay(int). This method provides the time in seconds to wait before fully shutting down a server when necessary to renew certificates.
  • setTimeZone(String) or setTimeZone(TimeZone). These methods provide the time zone for the server.
  • CertManager.setProtocol(String). This method provides the HTTPS protocol to use ("SSL" or "TLS").
  • setKeystoreFile(File). This method provides the File the contains the keystore.
  • setKeystorePW(char[]). This method provides the 'store' password used to access entries in a keystore (public keys and certificates). The default is changeit.
  • setKeyPW(char[]). This method provides the password used to access the private keys in a keystore. The default is changeit.
  • setConfigurator(EmbeddedWebServer.Configurator). This method provides a functional interface that has a 'configure' method whose first argument is an instance of SSLContext and whose second argument is an instance of HttpsParameters.

The methods used to create (or renew) certificates are

  • setCertName(String). This method provides an identifier to name a certificate, which is needed by some implementations.
  • setAlias(String). This method provides a user-specified name for an alias. Some certificate managers may not support this method and will throw an exception if it is used.
  • setDomain(String). This method provides the domain name used in a certificate.
  • setEmail(String). This method provides an email address used to contact a user for administrative purposes (e.g., if a certificate is about to expire).
  • setHelper(EmbeddedWebServer). This method provides a 'helper' web server that some certificate managers require to automatically manage certificates. a 'helper' can be created when needed, but one might prefer that the helper is always running, in which case this method should be used. A 'helper' typically runs HTTP instead of HTTPS. The documentation for a certificate manager that needs a helper should describe how to create it.
Occasionally one might want to include a trust store, in which case the following methods can be used:
  • setTrustManagers(TrustManager[]) or setTruststoreFile(File). These methods are mutually exclusive and provide either trust managers to use or the file containing a trust store created using keytool. They are not needed when the server's certificate was signed by a certificate authority listed in Java's cacerts file (e.g., /user/lib/jvm/.../lib/security/cacerts).
  • setTruststorePW(char[]). This method provides the 'store' password used to access a trust store.

Several configuration methods, mostly used for testing or debugging, are not described: alwaysCreate and setMode.

Certificate creation

Creating and signing certificates using the Java program keytool is somewhat of a black art, particularly when generating certificates for testing. The cases shown below use elliptic-curve cryptography to generate public and private keys.

In the following,

  • KS should be replaced with the file name of a keystore, excluding a ".jks" file-name suffix. This file is needed to configure a server.
  • TS should be replaced with the file name of a truststore, excluding a ".jks" file-name suffix. This is needed to configure applications so that they will accept a certificate chain whose root does not appear in the standard Java cacerts file.
  • ALIAS should be replaced with an alias for a server private key and certificate.
  • HOST should replaced with the server's host name. In many caaes, this should be a fully qualified name.
  • ROOT_ALIAS should be replaced with an alias for a root certificate when added to a Java cacerts file.

Stand-alone certificates

The simplest case is a stand-alone certificate. The following example creates one using elliptic curves, and placing it in a keystore named KS.jks with a 'store' and 'key' password "changeit":

keytool -genkey -keyalg EC -groupname secp256r1 \
        -sigalg SHA256withECDSA -keystore KS.jks \
        -keypass changeit -storepass changeit \
        -dname CN=HOST -alias ALIAS -validity 90

HOST should be the host name for a server and ALIAS should be whatever alias is used to name the keystore entry. This certificate can also be copied to a trust store, although in this simple case, one can use the same keystore for both.:

keytool -importkeystore -srckeystore KS.jks \
        -destkeystore TS.jks -srcalias ALIAS -destalias ALIAS \
        -srcstorepass changeit -deststorepass changeit \
        -srckeypass changeit -destkeypass changeit

or

keytool -keystore KS.jks -alias ALIAS -exportcert \
        -storepass changeit -rfc \
  | keytool -importcert -alias ALIAS  -keystore TS.jks \
        -keypass changeit -storepass changeit -noprompt

Two-level certificate chain

The following generates a certificate for a certificate authority:

keytool -genkey -keyalg EC -groupname secp256r1 \
        -sigalg SHA256withECDSA -keystore KS.jks \
        -keypass changeit -storepass changeit \
        -ext bc=ca:true -dname CN=ca -alias ca -validity 365

The argument "-ext bc=ca:true" is a Basic Constraints X509 extension that indicates that this is a CA (Certificate Authority) certificate. It was created in the same keystore as the previous self-signed certificate. If it is in a different keystore, the process will fail: this certificate is being used as a certificate authority but is not in the CA file (e.g., /usr/lib/jvm/java-11-openjdk-amd64/lib/security/cacerts), and if the root certificate is not there, everything must be done in the same keystore.

The next step is to generate a certificate request and process that by generating a certificate:

keytool -keystore KS.jks -alias ALIAS \
        -certreq -storepass changeit -keypass changeit | \
        keytool -keystore KS.jks  -storepass changeit \
         -alias ca -gencert  -ext san=dns:HOST | \
        keytool -keystore KS.jks  -storepass changeit \
          -alias ALIAS -importcert

The extension "san=dns:HOST" indicates that HOST will be a subject alternate name for the signed certificate. There are three programs in the pipeline. The first generates a certificate request, the second processes that request to generate a signed certificate, and the third imports the signed certificate.

Finally, one can move the CA certificate and its private key to a trust store and delete it from the keystore

keytool -importkeystore -srckeystore KS.jks \
        -destkeystore TS.jks -srcalias ca -destalias ca \
        -srcstorepass changeit -deststorepass changeit \
        -srckeypass changeit -destkeypass changeit
keytool -delete -alias ca -keystore KS.jks -storepass changeit

At this point, there is a single entry in the keystore with a certificate chain whose length is 2. A program such as SBL will have to include a trust store and its password, but not the keystore.

Three-level certificate

To create the root certificate authority and its private key, run the commands

keytool -genkey -keyalg EC -groupname secp256r1 \
        -sigalg SHA256withECDSA -keystore root.jks \
        -keypass changeit -storepass changeit \
        -ext bc=ca:true -dname CN=root -alias root -validity 365
keytool -keystore root.jks -alias root -exportcert -rfc \
        -storepass changeit -keypass changeit > root.pem
keytool -keystore TS.jks -alias root -importcert  -file root.pem \
        -storepass changeit -keypass changeit -noprompt

This places the root certificate and its private key in root.jks, and places the root certificate without its private key in TS.jks. It also stores the certificate in a file root.pem for later use. To import the root certificate into the standard Java cacerts file, use

keytool -keystore $JAVA_HOME/jre/lib/security/cacerts -file root.pem \
    -alias ROOT_ALIAS -importcert -storepass changeit -keypass changeit \
    -trustcacerts

where ROOT_ALIAS does not conflict with entries already in cacerts.

To create an intermediate certificate authority, run the commands

keytool -genkey -keyalg EC -groupname secp256r1 \
        -sigalg SHA256withECDSA -keystore ca.jks \
        -keypass changeit -storepass changeit \
        -ext bc=ca:true -dname CN=ca -alias ca -validity 365
keytool -storepass changeit  -keypass changeit -keystore ca.jks \
        -certreq -alias ca |
    keytool -storepass changeit -keypass changeit -keystore root.jks \
    -gencert -alias root -ext BC=0 -rfc > ca.pem
keytool -keystore ca.jks -importcert -alias ca -file ca.pem \
            -keypass changeit -storepass changeit

This creates the intermediate certificate ca, stores that certificate and its private key in ca.jks, and provides a file ca.pem containing this certificate for later use.

To create a server certificate and private key in a keystore KS.jks, run the commands

keytool -genkey -keyalg EC -groupname secp256r1 \
        -sigalg SHA256withECDSA -keystore KS.jks \
        -keypass changeit -storepass changeit \
        -dname CN=HOST -alias servercert -validity 90
keytool -storepass changeit -keypass changeit -keystore server.jks \
        -certreq -alias servercert |
    keytool -storepass changeit -keypass changeit -keystore ca.jks\
            -gencert -alias ca \
    -ext san=dns:wtz-thelio -ext ku:c=dig,kE -rfc > server.pem

This will store the server certificate in the file server.pem.

The final step is to add the server certificate to the keystore. containing the server's private key.

cat root.pem ca.pem server.pem |
    keytool -keystore KS.jks -importcert -alias servercert \
            -storepass changeit -keypass changeit -noprompt
The procedure above is based on an example in the keytool manual page (under "GENERATING CERTIFICATES FOR AN SSL SERVER"), and some examples from Using Keytool to create certificate chain. This code will work if the commands are entered in a terminal window or a script, but the keytool commands with the option -importcert will print an error message and return a non-zero exit code, although the operation actually completed. This will cause a failure if the commands are used in a Makefile. A fix is to add "|| true" to

Coding Example

For a simple coding example based on code in the EPTS program, consider the case of a web server embedded in an application for providing on-line help. The first step is to create an embedded web server:

EmbeddedWebServer ews = new EmbeddedWebServer(8080, 48, 2, false);
The argument 8080 is the TCP port number to use, 48 is the TCP backlog, 2 is the number of threads the server uses, and 'false' indicates that HTTP is used rather than HTTPS. The next step is to map a path to some service.
ews.add("/", ResourceWebMap.class, "org/bzdev/epts/manual/",
        null, true, false, true);

For these arguments, "/" indicates the common portion of the URL paths for the resources being added, ResourceWebMap.class and the following argument specify a mapping class that will find classes whose paths in the application's code base start with org/bzdev.epts/manual/, null indicates that an authenticator is not used, the first true indicates that a web.xml file should not be used , false indicates that the server should not display directories, and the final true indicates that any WEB-INF directory should not be visible.

The sequence of statements

WebMap wmap = epts.getWebMap("/");
wmap.addWelcome("index.html");
webmap.addMapping("html", "text/html; charset=utf-8");

looks up a web map for the context "/", configures the web map so that "index.html" is the 'welcome' page, and configures the web map so that files ending in the suffix "html" will have a media type of "text/html; charset=utf-8".

Finally

ews.start();

will start the web server. The URL http://localhost:8080/ can be typed into a browser to view this web page on the same computer as the server.