Tomcat tricks

Content:

  1. Introduction
  2. Structure
  3. Configuration
  4. JSP compilation
  5. Database connection pool
  6. Container managed security
  7. HTTPS
  8. Cache-Control
  9. Apache httpd in front

Introduction:

The article Modern Java EE describe Java EE at the high level and the article Java EE tricks describe many details with a focus on web frontend. Both are focusing on the Java EE standard and not on specific servers.

This article focuses on how to configure and setup Tomcat showing the server specific features.

Java EE is a rather well defined standard that allows code to be written server indepdendent. But even well abstracted Java EE features need to be configured in a specific context.

Structure

The main directories of Tomcat are:

Directory Content
$CATALINA_HOME/bin Various scripts for startup, shutdown etc.
$CATALINA_HOME/conf Configuration files
$CATALINA_HOME/lib All jar files used by Tomcat itself (note: these are also available for web applications, but web applications jar files are not available for Tomcat itself)
$CATALINA_HOME/logs Log files
$CATALINA_HOME/temp Temporary files
$CATALINA_HOME/wepapps The web applications
$CATALINA_HOME/work See JSP compilation

Configuation:

Server wide configuration:

The main config files covering the entire server are:

Config file Content
$CATALINA_HOME/conf/server.xml The main configuation file - it defines connectors (HTTP, HTTPS, AJP)
$CATALINA_HOME/conf/web.xml Defaults for web applications WEB-INF/web.xml - it enables JSP support, define MIME types and define welcome files
$CATALINA_HOME/conf/logging.properties Control logging

Application specific configuration:

There are 3 possible locations to define application specific configuration:

For production servers use the second. For development servers use the third. Never use the first.

JSP compilation:

JSP pages are not interpreted. JSP pages are compiled. In fact JSP pages are tripple compiled:

  1. The jspc compiler compiles .jsp files to .java files
  2. The javac compiler compiles .java files to .class files
  3. The JVM JIT compiler compiles the Java byte code to native code

One interesting aspect is that it is possible to see thee Java code generated by the jspc compiler.

$CATALINA_HOME/webapps/foobar/abc.jsp get compiled to $CATALINA_HOME/work/Catalina/localhost/foobar/org/apache/jsp/abc_jsp.java and even though the generated Java code is not that readable for humans then sometimes it can help troubleshoot weird error messages.

Database connection pool:

Why?

Establish a conection to the database before usage and close it again after usage can add significant overhead to database operations.

How much overhead depends on the database. Embedded databased has very little overhead. MySQL has low overhead. But some other database servers have huge overhead.

And when the transport to a non-embedded database is changed from plain socket to SSL socket, then the connection overhead explode due to the SSL handshake.

The solution is to use a database connection pool.

A database connection pool create N connections to the database and keep them open permanently. When applications need a connection then instead of creating a new connection it just borrows am existing connection from the pool and return it after usage.

Many performance problems have been solved by simply switching to using a database connection pool.

.NET ADO.NET providers comes with builtin connection pool, but Java JDBC drivers does not.

Luckily Java EE servers including Tomcat comes with a connection pool that just need to be enabled.

Config:

Configuring a connection pool is rather simple. Just supply the normal database connection information and specify pool size.

Example with MySQL pool size 30-100:

<Context>
    ...
    <Resource name="jdbc/MySqlTest"
              type="javax.sql.DataSource"
              driverClassName="com.mysql.jdbc.Driver"
              url="jdbc:mysql://localhost/Test"
              username="root"
              password=""
              maxIdle="30"
              maxTotal="100"/>
    ...
</Context>

Direct usage:

Code snippet without connection pool:

...
import java.sql.Connection;
import java.sql.DriverManager;
...

public class JDBC {
    ...
    public int test() {
        ...
        Class.forName("com.mysql.jdbc.Driver"); // not necessary in Java 6 and newer
        Connection con = DriverManager.getConnection("jdbc:mysql://localhost/Test", "root", "");
        ... // using connection
        con.close(); // should be done with try with resources in Java 7 and newer
        ...
    }
    ...
}

Code snippet with connection pool:

...
import java.sql.Connection;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
...

public class DS {
    private DataSource ds;
    public DS() throws NamingException {
        Context initctx = new InitialContext();
        Context envctx  = (Context)initctx.lookup("java:/comp/env");
        ds = (DataSource)envctx.lookup("jdbc/MySqlTest");
    }
    ...
    public int test() {
        ...
        Connection con = ds.getConnection();
        ... // using connection
        con.close(); // should be done with try with resources in Java 7 and newer
        ...
    }
    ...
}

In many cases where JSF or CDI are present the above code can be made a little more elegant with injection:

...
import java.sql.Connection;
import javax.annotation.ManagedBean;
import javax.annotation.Resource;
import javax.sql.DataSource;
...

@ManagedBean // only needed if not a servlet and not an EJB
public class DS {
    @Resource(name="jdbc/MySqlTest")
    private DataSource ds;
    ...
    public int test() {
        ...
        Connection con = ds.getConnection();
        ... // using connection
        con.close(); // should be done with try with resources in Java 7 and newer
        ...
    }
    ...
}

Usage with JPA:

Usually database access is done via JPA or another ORM.

The article Modern Java EE has an example on how to use data source with JPA here (scroll down a few pages to find persistence.xml example).

Container managed security:

Java EE has a standard for how to protect parts or the entire web application. Protect means require login and user to be in a specific role.

But the actual authentication mechanism must be defined in the server.

Tomcat supports several such mechanisms called realms:

JDBCRealm
Authentication against database (without connection pool)
DataSource Realm
Authentication against database (with connection pool)
JNDIRealm
Authentication against LDAP server
MemoryRealm
Authentication against simpel XML file
JAASRealm
Authentication against another system supporting JAAS (Java Authentication & Authorization Service)

We will look at MemoryRealm that is good for developer test and the two database realms that can be used for production usage.

Realms are always defined in Context config section (see above for where it can be located).

MemoryRealm:

Example with MemoryRealm:

<Context>
    ...
    <Realm className="org.apache.catalina.realm.MemoryRealm"
           pathname="webapps/test/WEB-INF/tomcat-users.xml"/>
    ...
</Context>

It assumes a WEB-INF/tomcat-users.xml with:

<tomcat-users>
    <role rolename="admin"/>
    <role rolename="user"/>
    <user username="arne" password="secret" roles="admin,user"/>
</tomcat-users>

Do not use MemoryReal for production usage. Having passwords in clear text in a file is a security risk. And there is no supported way of updating users programmatically.

JDNBCRealm and DataSourceRealm:

Example with JDBCRealm:

<Context>
    ...
    <Realm className="org.apache.catalina.realm.JDBCRealm"
           driverName="com.mysql.jdbc.Driver"
           connectionURL="jdbc:mysql://localhost/Test"
           connectionName="root"
           connectionPassword=""
           userTable="webusers"
           userRoleTable="webroles"
           userNameCol="usr"
           userCredCol="pwd"
           roleNameCol="role">
        <CredentialHandler className="org.apache.catalina.realm.MessageDigestCredentialHandler"
                           algorithm="sha-512"
                           iterations="1000"
                           saltLength="32"/>
    </Realm>
    ...
</Context>

Example with DataSourceRealm:

<Context>
    ...
    <Realm className="org.apache.catalina.realm.DataSourceRealm"
           dataSourceName="jdbc/MySqlTest"
           userTable="webusers"
           userRoleTable="webroles"
           userNameCol="usr"
           userCredCol="pwd"
           roleNameCol="role">
        <CredentialHandler className="org.apache.catalina.realm.MessageDigestCredentialHandler"
                           algorithm="sha-512"
                           iterations="1000"
                           saltLength="32"/>
    </Realm>
    ...
</Context>

Both assume a database setup like the following:

CREATE TABLE webusers (
    usr VARCHAR(20) NOT NULL,
    pwd VARCHAR(255)  NOT NULL,
    PRIMARY KEY(usr)
);
CREATE TABLE webroles (
    usr VARCHAR(20) NOT NULL,
    role VARCHAR(20)  NOT NULL,
    PRIMARY KEY(usr,role)
);
INSERT INTO webusers VALUES('arne', '4356f3c7e5e4591f3aa6a984cf1e48a7ab00af6412588b0305b8d4c493b9a501$1000$bc17f265f840b1c10c0f80e29617debc41f0aeca01f50b763204266e7ec7fbf4119987fb88e4b7be193007e58f4d219af9dc942a218503f6aa360eee67c97f83');
INSERT INTO webroles VALUES('arne', 'user');
INSERT INTO webroles VALUES('arne', 'admin');

So what is the story about the weird password?

It is very bad security to store a real password in the database so it can be retrieved. Instead one should store a hash of the password.

To help against dictionary attacks the hash should be of a random unique salt combined with the password.

The above weird looking string contains salt$iteration$hash.

And the hash is 1000 iterations using SHA-512 algorithm.

Such strings can be set administrative by using the digest tool in $CATALINA_HOME/bin dir:

C:\Apache\apache-tomcat-8.5.12\bin>digest -a sha-512 -i 1000 -s 32 secret
secret:4356f3c7e5e4591f3aa6a984cf1e48a7ab00af6412588b0305b8d4c493b9a501$1000$bc17f265f840b1c10c0f80e29617debc41f0aeca01f50b763204266e7ec7fbf4119987fb88e4b7be193007e58f4d219af9dc942a218503f6aa360eee67c97f83

Or generated by Java code to be used from a user creation module within the web application:

package test;

import java.security.NoSuchAlgorithmException;

import org.apache.catalina.realm.MessageDigestCredentialHandler;

public class PwdGen {
    public static String gen(String algorithm, int iterations, int saltlen, String pwd) throws NoSuchAlgorithmException {
        MessageDigestCredentialHandler md = new MessageDigestCredentialHandler();
        md.setAlgorithm(algorithm);
        md.setIterations(iterations);
        md.setSaltLength(saltlen);
        return md.mutate(pwd);
    }
    public static String gen(String pwd) throws NoSuchAlgorithmException {
        return gen("sha-512", 1000, 32, pwd);
    }
}

Custom realm:

If none of the predefined realms fit into a specific context, then it is possible to write a custom realm.

Here is a simple demo:

<Realm className="test.MyRealm" extra="test"/>

MyRealm.java:

package test;

import java.security.Principal;
import java.util.Arrays;

import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.realm.RealmBase;

public class MyRealm extends RealmBase {
    private String extra;
    public String getExtra() {
        return extra;
    }
    public void setExtra(String extra) {
        this.extra = extra;
    }
    @Override
    protected String getName() {
        return "MyRealm";
    }
    @Override
    protected String getPassword(String username) {
        GenericPrincipal p = (GenericPrincipal)getPrincipal(username);
        if(p != null) {
            return p.getPassword();
        } else {
            return null;
        }
    }
    @Override
    protected Principal getPrincipal(String username) {
        if(username.equals("arne")) {
            return new GenericPrincipal("arne", "secret", Arrays.asList("user", "admin", extra));
        } else {
            return null;
        }
    }
}

Note that the jar file with the code need to be in $CATALINA_HOME/lib dir.

Account lockout:

To prevent password guessing accounts can be locked out for a number of seconds after a number of failed attempts.

This is done with the LockoutRealm, which is a nested realm that goes around the real realm.

Example fragment:

    <Realm className="org.apache.catalina.realm.LockOutRealm" failureCount="3" lockOutTime="180">
        <Realm .../>
    </Realm>

HTTPS:

This section assumes Tomcat 8.5 or newer. Older versions are different - see documentation for those.

Self signed certficate:

Getting started with a self signed certificate is easy:

  1. Create keystore with certficate
  2. Define connector in server.xml

Create keystore with certficate example:

C:\Apache\apache-tomcat-8.5.12\conf>keytool -genkey -alias tomcat -keyalg RSA -keystore .\test.jks
Enter keystore password:
Re-enter new password:
What is your first and last name?
  [Unknown]:  Arne Vajhoej
What is the name of your organizational unit?
  [Unknown]:  Article writing
What is the name of your organization?
  [Unknown]:  Personal
What is the name of your City or Locality?
  [Unknown]:  Coventry
What is the name of your State or Province?
  [Unknown]:  RI
What is the two-letter country code for this unit?
  [Unknown]:  US
Is CN=Arne Vajhoej, OU=Article writing, O=Personal, L=Coventry, ST=RI, C=US corr
ect?
  [no]:  yes

Enter key password for <tomcat>
        (RETURN if same as keystore password):

server.xml fragment:

    <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true">
        <SSLHostConfig>
            <Certificate certificateKeystoreFile="conf/test.jks" certificateKeystorePassword="bigsecret"/>
        </SSLHostConfig>
    </Connector>

And now URL's https://localhost:8443/test/whatever work.

Well - they do not really work. All modern browsers will produce severe warnings about the self signed certficate.

Real certficate:

To get a real certficate you need to:

  1. generate a CSR
  2. send CSR, some information and some money to a CA that return a real certificate
  3. import real certificate

I have never tried that process, so I can not give any insights.

To generate CSR you can use:

keytool -genkey -alias tomcat -keyalg RSA -keystore .\real.jks
keytool -certreq -keyalg RSA -alias tomcat -file certreq.csr -keystore .\real.jks

To import real certficate you can use:

keytool -import -alias root -keystore .\real.jks -trustcacerts -file <chain certificate>
keytool -import -alias tomcat -keystore .\real.jks -file <real certificate>

(and change server.xml to use real.jks instead of test.jks)

HTTP 2.0:

Tomcat supports HTTP 2.0 but only for HTTPS.

To enable it use the following server.xml fragment:

    <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true">
        <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol"/>
        <SSLHostConfig>
            <Certificate certificateKeystoreFile="conf/test.jks" certificateKeystorePassword="bigsecret"/>
        </SSLHostConfig>
    </Connector>

Cache-Control:

Tomcat provides a neat way to control cache-control by allowing to set max-age for diffent content types via a filter.

Example WEB-INF/web.xml fragment:

   <filter>
       <filter-name>ExpiresFilter</filter-name>
       <filter-class>org.apache.catalina.filters.ExpiresFilter</filter-class>
       <init-param>
          <param-name>ExpiresByType image</param-name>
          <param-value>access plus 10 minutes</param-value>
       </init-param>
       <init-param>
          <param-name>ExpiresByType text/css</param-name>
          <param-value>access plus 10 minutes</param-value>
       </init-param>
       <init-param>
          <param-name>ExpiresByType application/javascript</param-name>
          <param-value>access plus 10 minutes</param-value>
       </init-param>
    </filter>
    <filter-mapping>
       <filter-name>ExpiresFilter</filter-name>
       <url-pattern>*</url-pattern>
       <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

Apache httpd in front:

Sometimes it is preferred to put Apache httpd in front of Tomcat.

Possible reasons being:

  1. Security (see below)
  2. Mixing multiple web technologies (Java EE, PHP, ASP.NET etc.) but keep uniform URL's
  3. Serving static content (HTML, CSS, JS, graphics, video etc.) from Apache httpd instead of Tomcat but keep uniform URL's
  4. SSL offloading (having Apache perform SSL handshake instead of Tomcat)

The extra security is achieved by having an extra firewall.

From:

-firewall-Tomcat

To:

-firewall-httpd-firewall-Tomcat

The two firewalls create a DMZ where the Apache httpd reside and provide an extra layer of security. /

And mod_security can be used to block many forms of attacks.

(there may also be a firewall between Tomcat and database, but that is out of scope for this article)

Note that Apache httpd uses a special protocol AJP (Apache Jserv Protocol) to communicate with Tomcat.

There are 2 supported ways to translate from HTTP(S) to AJP:

mod_jk is the traditional module and comes with a lot of configurability. But setup can be a bit tricky for beginners.

mod_proxy_ajp is only available in newer Apache httpd and has much less configurability. But it is very easy to setup.

mod_jk:

httpd.conf fragment:

# load module
LoadModule jk_module "C:/DivNative/32bit/mod_jk-1.2.42/mod_jk.so"
# define workers
JkWorkersFile conf/workers.properties
# define logging
JkLogFile logs/mod_jk.log
JkLogLevel info
# forward in all virtual hosts
JkMountCopy All
# forward requests matching /test/*
JkMount /test/* testworker

workers.properties:

worker.list=testworker,jkstatus
#
worker.testworker.type=ajp13
worker.testworker.host=localhost
worker.testworker.port=8009 
worker.testworker.connection_pool_size=100
#
worker.jkstatus.type=status

mod_proxy_ajp:

httpd.conf fragments:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
...
ProxyPass "/test" "ajp://localhost:8009/test"

Article history:

Version Date Description
1.0 September 7th 2017 Initial version
1.1 October 14th 2017 Add description of cache filter and HTTP 2.0 support

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj