Servlet Engine

Download from http://www.nyangau.org/se/se.zip.

What is it

This is an implementation of a very small subset of the Servlet standards. It does not claim to comply with any particular level of specification, but rather it implements those calls used (so far) in my trivially simple servlets.

It is sufficient to host small web sites including simple servlets.

There is a class of servlet which don't require full blown application servers (such as WebSphere) to host them. They might not require EJBs, might need to consume very little memory or CPU, and perhaps might only utililise just a few of the Servlet APIs. A large number of vendor products now provide simple web interfaces to their products, and often the functionality is basic, perhaps being restricted to read-only status viewing (eg: the Tivoli monitor agent, Best/1 performance capacity/trend graphing review, etc.).

This Servlet Engine is designed to be extremely lightweight. In fact, at this time, the .jar file is <35KB. To host a Servlet, you need the Servlet itself, a "Server" which you write (which basically instantiates an instance of the Servlet Engine, and registers the Servlet into it), and the .jar file.

It is also designed to allow the construction 'tightly packaged' servers. When looking for properties files (or content via ServletContext.getResourceAsStream) or static web content, it uses the classloader first, and then the filesystem. This allows the bundling of Servlets, "Server", properties files and static web content into a single .jar file.

eg: You could have hello.jar containing :-

HelloServlet.class
HelloServlet.props
HelloServer.class
HelloServer.props

and you'd run it via :-

java -cp nyangau-se-<version>.jar:hello.jar HelloServer

Functionality provided

The Servlet Engine compiles up to form a .jar file, containing classes in the jakarta.servlet. namespace and below. Thus servlets compiled up to run in WebSphere, JRun, WebLogic, Tomcat etc., should require zero modification to run in this environment (providing they only use the subset of APIs supported by this Servlet Engine).

public class ServletException extends Exception
  {
  public ServletException();
  public ServletException(String s);
  }  

public abstract class ServletOutputStream extends OutputStream
  {
  public abstract void print(String s)
    throws IOException;
  public abstract void println()
    throws IOException;
  public abstract void println(String s)
    throws IOException;
  }

public interface ServletContext
  {
  public abstract String getInitParameter(String n);
  public abstract Enumeration<String> getInitParameterNames();
  public abstract String getMimeType(String fn);
  public abstract ServletContext getContext(String uripath);
  public abstract InputStream getResourceAsStream(String path);
  }

public interface ServletConfig
  {
  public abstract ServletContext getServletContext();
  public abstract String getInitParameter(String n);
  public abstract Enumeration<String> getInitParameterNames();
  }

public interface ServletRequest
  {
  public abstract String getScheme();
  public abstract int getServerPort();
  public abstract String getProtocol();
  public abstract String getParameter(String p);
  public abstract Enumeration<String> getParameterNames();
  public abstract String[] getParameterValues(String p);
  }

public interface ServletResponse
  {
  public abstract boolean isCommitted();
  public abstract void setContentType(String ct);
  public abstract void setContentLength(int length);
  public abstract ServletOutputStream getOutputStream()
    throws IOException;
  public abstract PrintWriter getWriter()
    throws IOException;
  }

public interface Servlet
  {
  public abstract void init(ServletConfig sc)
    throws ServletException;
  public abstract ServletConfig getServletConfig();
  public abstract void service(ServletRequest req, ServletResponse resp)
    throws ServletException, IOException;
  }

public abstract class GenericServlet implements Servlet
  {
  public void init()
    throws ServletException;
  public void init(ServletConfig sc)
    throws ServletException;
  public ServletConfig getServletConfig()
  public abstract void service(ServletRequest req, ServletResponse resp)
    throws ServletException, IOException;
  }

public interface HttpServletRequest extends ServletRequest
  {
  public static final String BASIC_AUTH = "BASIC";
  public static final String FORM_AUTH = "FORM";
  public static final String CLIENT_CERT_AUTH = "CLIENT_CERT";
  public static final String DIGEST_AUTH = "DIGEST";
  public static final String PRE_AUTH = "PRE"; // Non-standard
  public abstract String getAuthType();
  public abstract String getMethod();
  public abstract String getRequestURI();
  public abstract String getQueryString();
  public abstract int getIntHeader(String h)
    throws NumberFormatException;
  public abstract Enumeration<String> getHeaders(String h);
  public abstract Enumeration<String> getHeaderNames();
  public abstract String getRemoteUser();
  public abstract boolean isUserInRole(String role);
  }

public interface HttpServletResponse extends ServletResponse
  {
  static final int SC_OK                    = 200;
  static final int SC_MULTIPLE_CHOICES      = 300;
  static final int SC_MOVED_PERMANENTLY     = 301;
  static final int SC_MOVED_TEMPORARILY     = 302;
  static final int SC_BAD_REQUEST           = 400;
  static final int SC_UNAUTHORIZED          = 401;
  static final int SC_FORBIDDEN             = 403;
  static final int SC_NOT_FOUND             = 404;
  static final int SC_METHOD_NOT_ALLOWED    = 405;
  static final int SC_GONE                  = 410;
  static final int SC_INTERNAL_SERVER_ERROR = 500;
  static final int SC_NOT_IMPLEMENTED       = 501;
  static final int SC_SERVICE_UNAVAILABLE   = 503;
  public abstract boolean containsHeader(String h);
  public abstract void addHeader(String h, String v);
  public abstract void setHeader(String h, String v);
  public abstract void addIntHeader(String h, int v);
  public abstract void setIntHeader(String h, int v);
  public abstract void addDateHeader(String h, long v);
  public abstract void setDateHeader(String h, long v);
  public abstract void setStatus(int sc);
  public abstract void sendError(int sc)
    throws IOException;
  public abstract void sendError(int sc, String message)
    throws IOException;
  public void sendRedirect(String url)
    throws IOException;
  public String encodeURL(String url);
  public String encodeRedirectURL(String url);
  }

public abstract class HttpServlet extends GenericServlet
  {
  public void service(ServletRequest req, ServletResponse resp)
    throws ServletException, IOException;
  protected void service(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException;
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException;
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException;
  protected void doHead(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException;
  }

Access to the web application can be over HTTP, or over HTTPS, or both.

Access to the web application can be unauthenticated, or authenticated against a small set of userids configured in the properties file. Basic authentication can be used, so best to combine this with the use of HTTPS, so that the base64 encoded userid:password cannot be snooped in-flight.

Userids can be associated with roles.

GET and POST methods are supported. According to the standards, the GET method is only supposed to work for ASCII characters in the parameter names and values. The POST method is supposed to work for any ISO10464 character, although this only works for ASCII characters too. The POST file-upload mechanism isn't implemented.

How to use

A simple Servlet

Lets assume we have a simple servlet called HelloServlet, which we wish to host.

//
// HelloServlet.java
//

import java.io.*;
import jakarta.servlet.*;
import jakarta.servlet.http.*;

public class HelloServlet extends HttpServlet
  {
  public void doGet(HttpServletRequest req, HttpServletResponse resp)
    throws ServletException, IOException
    {
    resp.setContentType("text/html");
    ServletOutputStream os = resp.getOutputStream();
    os.println("<H1>Hello</H1>");
    }
  }

A Servlet Engine to host it

We need to construct a Servlet Engine to run it in :-

//
// HelloServer.java - Hello Server
//

import java.net.*;
import java.io.*;
import java.util.*;
import nyangau.se.*;

public class HelloServer
  {
  public static void main(String args[])
    {
    try
      {
      ServletContext sctx = new ServletContext("HelloServer.props", "docroot");
      ServletEngine  seng = new ServletEngine(sctx);
      ServletConfig  scfg = new ServletConfig("HelloServlet.props");
      seng.registerServlet("/", new HelloServlet(), scfg);
      seng.serve();
      }
    catch ( javax.servlet.ServletException |
            IOException                    e )
      {
      System.err.println("HelloServer: "+e.toString());
      }
    }
  }

You can see where a new instance of HelloServlet is registered into the URI space (at / in the example).

The ServletEngine.serve method is where the server begins listening for inbound web requests and responds to them.

Just for completeness, we display any exceptions which occur during initialisation. Exceptions which occur during the serving of individual pages will be logged to stderr.

ServletContext

HelloServer.props is a Java properties file containing servlet-context wide properties. As there is only one servlet context (ie: one web application) in this servlet engine, these properties are effectively global to the entire server.

The server properties file looks like this :-

# Uncomment to conceal technology used
# server=

# HTTP listen port
port=8080

# HTTPS listen port
port_ssl=8443
keystore=mykeystore.jks
storepass=mykeystore-password

Normally the Server: HTTP response header returned is nyangau.se V.V where V.V is the version. By setting the server property, you can lie about what web server technology is being used or by setting to nothing, prevent the header from being returned at all. Web security folk like this as it reveals less to a potential hacker.

If there is no port, then it doesn't listen for HTTP requests.

If there is no port_ssl, then it doesn't listen for HTTPS requests. If keystore is omitted, then the default is se.jks. If storepass is omitted, then the default is password. Obviously, when using SSL, you need to have a keystore, with the indicated filename and password.

If neither port or port_ssl parameters are given, the ServletEngine.serve() method returns immediately, which normally results in the process terminating.

This file can also contain MIME mappings in the form mime.extension=mimetype, eg:

mime.png=image/png

The Servlet Engine will use these when serving static pages. If the file extension in question is not listed here, the Servlet Engine will fall back on its internal hard coded list of around 70 common MIME types.

Note also that where the ServletContext is set, a document root is chosen. This is for any static web content to be included.

Authentication

Users can be authenticated against a small repository of users. The Servlet Engine uses HTTP Basic Authentication to do this. eg:

realm=Famous fictional figures website
userids=tom,dick
password.tom=toms-password
password.dick=dicks-password

You can also configure a web server (eg: Apache HTTPD) to authenticate users before proxying the request to the Servlet Engine. It would need to pass the authenticated userid in a request header, and the Servlet Engine will trust this rather than try to authenticate the user itself. However, it will only trust the web server if the web server provides a secret value in another request header, eg:

preauth.secret.header.name=PreAuthSecret
preauth.secret.header.value=web-servers-password
preauth.userid.header.name=PreAuthUserid

If you are using Apache HTTPD to do the pre-authentication, your httpd.conf might look a bit like this :-

<Location "/">
    AuthType Basic
    AuthName "Restricted Resource"
    AuthBasicProvider file
    AuthUserFile "/etc/httpd/conf/httpd.users"
    # chcon -t httpd_sys_content_t /etc/httpd/conf/httpd.users
    Require valid-user
    RequestHeader set PreAuthSecret "web-servers-password"
#   RequestHeader set PreAuthUserid "%{REMOTE_USER}e"
    RewriteEngine On
    RewriteCond %{IS_SUBREQ} =false
    RewriteCond %{LA-U:REMOTE_USER} (.+)
    RewriteRule . - [E=RU:%1]
    RequestHeader set PreAuthUserid "%{RU}e" env=RU
</Location>

ProxyPass "/" "http://host:port/"
ProxyPassReverse "/" "http://host:port/"
# /usr/sbin/setsebool httpd_can_network_connect 1

Observe the contrived way in which we get access to the authenticated userid, in order to be able to pass it on in a request header. The commented-out line doesn't work because Apache HTTPD does URI mapping before it has determined the REMOTE_USER. %{LA-U:REMOTE_USER} returns the REMOTE_USER from the evaluation of a second internal look-ahead request, which is discarded. The look-ahead feature is something understood by mod_rewrite.so, not environment variable references in general. The %{IS_SUBREQ} =false test doesn't appear in Internet examples, but appears necessary to prevent AH00125: Request exceeded the limit of 10 subrequest nesting levels in the error log.

If the pre-authentication mechanism is used, then note that HttpServletRequest.getAuthType() returns the non-standard value HttpServletRequest.PRE_AUTH.

With either of these approaches, configure the Servlet Engine to only listen over SSL to avoid passwords being transmitted unencrypted (ie: don't use http: like in the example above).

Authorisation

Users can be assigned roles, eg:

roles=appuser
roles.tom=appuser,write
roles.dick=appuser,investigate,write

By default, if you don't assign roles to a user then they have no roles. However, you can grant a default set of roles, eg:

roles.default=readonly

Normally all content is visible to all users. You can declare that certain URI patterns are not accessible by default. Roles can be granted access to certain URI patterns. eg:

secureURIs=/*
secureURIs.appuser=/*

The roles property needs to include all role names that grant access to a secureURI. In other words all roles R where property secureURIs.R exists. Other roles granted to a user needn't be listed.

ServletConfig

HelloServlet.props is a Java properties file containing initialisation time parameters for the servlet. ie: the values returned from the getInitParameter method of the jakarta.servlet.ServletConfig class.

Compiling and running

Java 11 or later is required.

Assuming $n is the path to find nyangau-se-<version>.jar :-

When compiling with javac, be sure to -extdirs nyangau-se-<version>.jar.

javac -extdirs $n HelloServlet.java
javac -extdirs $n HelloServer.java
jar -cf hello.jar *.class *.props

When running your servlet engine with java, be sure to include nyangau-se.jar on the classpath.

java -cp $n/nyangau-se-<version>.jar:hello.jar HelloServer

Monitoring

Servlet Engine exposes the following MBeans :-

ObjectNameAttributeMeaning
nyangau.se:type=ConnectionServer,scheme=http RequestsHow many requests over HTTP
nyangau.se:type=ConnectionServer,scheme=https RequestsHow many requests over HTTPS

You might only see one of the above, depending on the ports you've enabled.

Revision history

VersionDateComments
1.0  First public release.
1.1  Slight tidy up.
1.22010-06-16Now possible to construct ServletContext and ServletConfig from Properties.
Add JMX MBeans (had to refactor inner classes).
1.32010-07-18getRequestURI should still include + and %hh non-URLDecoded
1.42014-10-01Use generic types
1.52014-12-12Correctly URL decode request parameters
1.62014-12-12Support for basic authentication
1.72014-12-14POST application/x-www-form-urlencoded support (ASCII only)
1.82014-12-28Ability to secure some URIs and not others. Support for Date response headers.
1.9  Silently tolerate empty connections with no request.
2.02016-03-25server= property, to reveal less to attackers.
2.12017-05-28Pre-authenticated feature.
2.22021-12-31Mavenized. Use Java 8.
2.32023-08-05Use Java 11.
3.02024-02-17Move to jakarta. packages
future...If I write servlets which need APIs not implemented so far, I may well add them.

Copying

Feel free to copy, its public domain. Caveat Emptor.


The documentation is written and maintained by Andy Key
andy.z.key@googlemail.com