Success: Nextcloud runs with jFastCGI on Tomcat 9

I just want to report, that I managed to run Nextcloud 12 on Tomcat 9.0.1 beta with this patch

to jFastCGI.
I use the SSO & SAML module. So the http basic authentication prompt is created by Tomcat. That means non logged in users don’t get in touch with PHP.

web.xml:

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
    version="2.4"> 

    <servlet> 
      <servlet-name>php-fpm</servlet-name>
      <description>
        Forward php to php7.0-fpm on port 9000.
      </description>
      <servlet-class>org.jfastcgi.servlet.FastCGIServlet</servlet-class>
      <init-param>
        <param-name>server-address</param-name>
        <param-value>127.0.0.1:9000</param-value>
      </init-param>
      <!-- init-param> ... somehow not used ??? :-(
        <param-name>connection-factory</param-name>
        <param-value>org.jfastcgi.client.SingleConnectionFactory</param-value>
      </init-param -->
  </servlet>


   <servlet-mapping> 
      <servlet-name>php-fpm</servlet-name> 
      <url-pattern>/index.php</url-pattern> 
   </servlet-mapping> 

   <servlet-mapping> 
      <servlet-name>php-fpm</servlet-name> 
      <url-pattern>/index.php/*</url-pattern> 
   </servlet-mapping> 

   <servlet-mapping> 
      <servlet-name>php-fpm</servlet-name> 
      <url-pattern>/ocs/*</url-pattern> 
   </servlet-mapping> 

   <servlet-mapping> 
      <servlet-name>php-fpm</servlet-name> 
      <url-pattern>/remote.php/*</url-pattern> 
   </servlet-mapping> 

   <servlet-mapping> 
      <servlet-name>php-fpm</servlet-name> 
      <url-pattern>/updater/index.php</url-pattern> 
   </servlet-mapping> 

    <welcome-file-list>
        <welcome-file>index.php</welcome-file>
    </welcome-file-list>

   <security-constraint>
      <display-name>Disable access to everything</display-name>
      <web-resource-collection>
         <web-resource-name>disable-everything</web-resource-name>
         <url-pattern>/*</url-pattern>
      </web-resource-collection>
      <auth-constraint />
   </security-constraint>

    <security-constraint>
      <display-name>Require login for all access</display-name>
      <web-resource-collection>
         <web-resource-name>protected</web-resource-name>
         <url-pattern>/index.php</url-pattern>
         <url-pattern>/index.php/*</url-pattern>
         <url-pattern>/ocs/*</url-pattern>
         <url-pattern>/remote.php/*</url-pattern>
         <url-pattern>/core/*</url-pattern>
         <url-pattern>/apps/*</url-pattern>
         <url-pattern>/settings/*</url-pattern>
         <url-pattern>/updater/index.php</url-pattern>
      </web-resource-collection>
      <auth-constraint>
         <role-name>validUser</role-name>
      </auth-constraint>
      <!-- user-data-constraint>
         <transport-guarantee>CONFIDENTIAL</transport-guarantee>
      </user-data-constraint -->
   </security-constraint>

   <security-role>
      <role-name>validUser</role-name>
   </security-role>

   <login-config>
      <auth-method>BASIC</auth-method>
      <realm-name>Nextcloud Login</realm-name>
   </login-config>

</web-app>

context.xml:

<?xml version="1.0" encoding="UTF-8"?>

<Context docBase="/var/www/default/nextcloud##120003" privileged="true">

   <!-- Realm className="org.apache.catalina.realm.MemoryRealm" pathname="conf/git_protected-users.xml" -->

   <Realm className="org.apache.catalina.realm.JDBCRealm"
       connectionName="nextcloud" connectionPassword="xxx"
       connectionURL="jdbc:postgresql://xxx:5432/nextcloud?ssl=true"
       driverName="org.postgresql.Driver"
       userTable="oc_users" userNameCol="uid" userCredCol="password"
       userRoleTable="oc_group_user" roleNameCol="gid"
       allRolesMode="authOnly">
       <CredentialHandler className="de.greinerinformatik.NextcloudCredentialHandler" />
   </Realm>

</Context>

NextcloudCredentialHandler.java

package de.greinerinformatik;

import org.apache.catalina.CredentialHandler;
import org.mindrot.jbcrypt.BCrypt;

public class NextcloudCredentialHandler implements CredentialHandler {

    @Override
    public boolean matches(String inputCredentials, String storedCredentials) {
        // substring(5) = "1|$2y...". 2y is used by PHP, because there was a bug in
        // the 2a implementation of BCRYPT
        return BCrypt.checkpw(inputCredentials, "$2a" + storedCredentials.substring(5));
    }

    @Override
    public String mutate(String inputCredentials) {
         String s = BCrypt.hashpw(inputCredentials, BCrypt.gensalt(10).substring(3));
         if (!s.startsWith("$2a$10$")) {
             throw new RuntimeException("Unexpected result of BCrypt.gensalt: '" + s + "'.");
         }
         return "1|$2y" + s.substring(3);
    }
    
}

can you please help, I have a apache tomcat 8.5.24 running in raspberry pi, and I have a java web application running in it and what I want is next cloud should also run in the same server. I have installed php 7.0 by using apt-get php7.0 and I have mysql server and client installed in the raspberry pi. And I made tomcat to run port 80.

The next step would probably be to install jFastCGI…
One needs to have some knowledge about Java Webapp related things, to get all this up and running…

Here is an update for Nextcloud version 24.

web.xml does not need to be changed, but I did some changes anyway (do not require authentication for plugin ressources - .css, .svg and .js files):

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
    version="2.4"> 

    <servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

   <servlet> 
      <servlet-name>php-cgi</servlet-name>
      <description>
        Forward php to php8.1-fpm on port 9000.
      </description>
      <servlet-class>org.jfastcgi.servlet.FastCGIServlet</servlet-class>
      <load-on-startup>1</load-on-startup>
      <init-param>
        <param-name>allowed-headers-regex</param-name>
        <param-value>ACCEPT[-0-9A-Z]{0,100}|AUTHORIZATION|CACHE-CONTROL|COOKIE|DEPTH|HOST|IF-[-0-9A-Z]{2,100}|OCS-APIREQUEST|REFERER|REQUESTTOKEN|USER-AGENT|X-FORWARDED-FOR|X-UPDATER-AUTH|DESTINATION|OVERWRITE|X-REQUESTED-WITH</param-value>
      </init-param>
      <init-param>
        <param-name>server-address</param-name>
        <param-value>[::1]:9000</param-value>
      </init-param>
      <init-param>
        <param-name>keep-alive</param-name>
        <param-value>true</param-value>
      </init-param>
      <init-param> <!--  ... somehow not used ??? :-( -->
        <param-name>connection-factory</param-name>
        <!-- param-value>org.jfastcgi.client.SingleConnectionFactory</param-value -->
        <param-value>org.jfastcgi.client.PooledConnectionFactory</param-value>
      </init-param>
     <init-param> <!--  ... somehow not used ??? :-( -->
        <param-name>connection-factory</param-name>
        <!-- param-value>org.jfastcgi.client.SingleConnectionFactory</param-value -->
        <param-value>org.jfastcgi.client.PooledConnectionFactory</param-value>
      </init-param>
  </servlet>

   <servlet-mapping> 
      <servlet-name>default</servlet-name> 
      <url-pattern>/</url-pattern> 
      <!-- url-pattern>*.svg</url-pattern --> 
   </servlet-mapping> 

   <servlet-mapping> 
      <servlet-name>php-cgi</servlet-name> 
      <url-pattern>/index.php</url-pattern> 
      <url-pattern>/phpinfo.php</url-pattern> 
      <url-pattern>/index.php/*</url-pattern> 
      <url-pattern>/ocs/*</url-pattern> 
      <url-pattern>/remote.php/*</url-pattern> 
      <url-pattern>/updater/index.php</url-pattern> 
   </servlet-mapping> 

    <welcome-file-list>
        <welcome-file>index.php</welcome-file>
    </welcome-file-list>

   <security-constraint>
      <display-name>Disable access to everything</display-name>
      <web-resource-collection>
         <web-resource-name>disable-everything</web-resource-name>
         <url-pattern>/*</url-pattern>
      </web-resource-collection>
      <auth-constraint />
   </security-constraint>
    <security-constraint>
      <display-name>Require login for all access</display-name>
      <web-resource-collection>
         <web-resource-name>protected</web-resource-name>
         <url-pattern>/index.php</url-pattern>
         <url-pattern>/phpinfo.php</url-pattern>
         <url-pattern>/index.php/*</url-pattern>
         <url-pattern>/ocs/*</url-pattern>
         <url-pattern>/remote.php/*</url-pattern>
         <url-pattern>/core/*</url-pattern>
         <url-pattern>/dist/*</url-pattern>
         <url-pattern>/apps/*</url-pattern>
         <url-pattern>/settings/*</url-pattern>
         <url-pattern>/updater/index.php</url-pattern>
      </web-resource-collection>
      <auth-constraint>
         <role-name>validUser</role-name>
      </auth-constraint>
      <!-- user-data-constraint>
         <transport-guarantee>CONFIDENTIAL</transport-guarantee>
      </user-data-constraint -->
   </security-constraint>

   <security-constraint>
      <display-name>Allow static files</display-name>
      <web-resource-collection>
         <web-resource-name>notProtected</web-resource-name>
         <url-pattern>/core/css/*</url-pattern>
         <url-pattern>/core/doc/*</url-pattern>
         <url-pattern>/core/fonts/*</url-pattern>
         <url-pattern>/core/js/*</url-pattern>
         <url-pattern>/core/img/*</url-pattern>
         <url-pattern>/core/l10n/*</url-pattern>
         <url-pattern>/dist/*</url-pattern>
         <url-pattern>/apps/activity/img/*</url-pattern>
         <url-pattern>/apps/accessibility/l10n/*</url-pattern>
         <url-pattern>/apps/calendar/css/*</url-pattern>
         <url-pattern>/apps/calendar/img/*</url-pattern>
          <url-pattern>/apps/calendar/l10n/*</url-pattern>
         <url-pattern>/apps/contacts/css/*</url-pattern>
         <url-pattern>/apps/contacts/img/*</url-pattern>
         <url-pattern>/apps/contacts/l10n/*</url-pattern>
         <url-pattern>/apps/dashboard/css/*</url-pattern>
         <url-pattern>/apps/dashboard/img/*</url-pattern>
         <url-pattern>/apps/dashboard/l10n/*</url-pattern>
         <url-pattern>/apps/files/css/*</url-pattern>
         <url-pattern>/apps/files/img/*</url-pattern>
         <url-pattern>/apps/files/l10n/*</url-pattern>
         <url-pattern>/apps/files_rightclick/css/*</url-pattern>
         <url-pattern>/apps/files_rightclick/l10n/*</url-pattern>
         <url-pattern>/apps/files_sharing/l10n/*</url-pattern>
         <url-pattern>/apps/files_pdfviewer/js/*</url-pattern>
         <url-pattern>/apps/files_versions/l10n/*</url-pattern>
         <url-pattern>/apps/firstrunwizard/img/*</url-pattern>
         <url-pattern>/apps/firstrunwizard/js/*</url-pattern>
         <url-pattern>/apps/images/css/*</url-pattern>
         <url-pattern>/apps/images/img/*</url-pattern>
         <url-pattern>/apps/images/l10n/*</url-pattern>
         <url-pattern>/apps/notifications/css/*</url-pattern>
         <url-pattern>/apps/notifications/img/*</url-pattern>
         <url-pattern>/apps/notifications/js/*</url-pattern>
         <url-pattern>/apps/notifications/l10n/*</url-pattern>
         <url-pattern>/apps/photos/img/*</url-pattern>
         <url-pattern>/apps/settings/img/*</url-pattern>
         <url-pattern>/apps/tasks/img/tasks.svg</url-pattern>
         <url-pattern>/apps/theming/css/*</url-pattern>
         <url-pattern>/apps/theming/img/*</url-pattern>
         <url-pattern>/apps/theming/js/*</url-pattern>
         <url-pattern>/apps/theming/l10n/*</url-pattern>
         <url-pattern>/apps/text/js/*</url-pattern>
         <url-pattern>/apps/text/l10n/*</url-pattern>
         <url-pattern>/apps/user_status/css/*</url-pattern>
         <url-pattern>/apps/user_status/img/*</url-pattern>
        <url-pattern>/apps/user_status/l10n/*</url-pattern>
         <url-pattern>/apps/viewer/l10n/*</url-pattern>
         <url-pattern>/apps/viewer/js/*</url-pattern>
      </web-resource-collection>
   </security-constraint>

   <security-role>
      <role-name>validUser</role-name>
   </security-role>

   <login-config>
      <auth-method>BASIC</auth-method>
      <realm-name>Nextcloud Login</realm-name>
   </login-config>

   <resource-ref>
      <description>Nextcloud DB Connection</description>
      <res-ref-name>jdbc/nextcloudlogin</res-ref-name>
      <res-type>javax.sql.DataSource</res-type>
      <res-auth>Container</res-auth>
   </resource-ref>

</web-app>

JDBCRealm will become deprecated, so we change to DataSourceRealm with a sql DataSource - context.xml:

<?xml version="1.0" encoding="UTF-8"?>

<Context docBase="/var/www/nextcloud##240001" privileged="true">

   <Resources allowLinking="true"/>

   <Resource name="jdbc/nextcloudlogin" auth="Container" type="javax.sql.DataSource"
           maxTotal="150" maxIdle="50" maxWaitMillis="15000"
           username="XXX" password="YYY" driverClassName="org.postgresql.Driver"
           url="jdbc:postgresql://[zzz]:5432/nextcloud?sslmode=require"/>

   <Realm className="org.apache.catalina.realm.DataSourceRealm"
        localDataSource="true"
        dataSourceName="jdbc/nextcloudlogin"
        userTable="oc_users" userNameCol="uid" userCredCol="password"
        userRoleTable="oc_group_user" roleNameCol="gid">
        <CredentialHandler className="de.greinerinformatik.NextcloudCredentialHandler" />
   </Realm>

</Context>

Nextcloud 24 uses Argon2 as hash function for passwords - NextcloudCredentialHandler.java:

package de.greinerinformatik;

import org.apache.catalina.CredentialHandler;
import de.mkammerer.argon2.Argon2;
import de.mkammerer.argon2.Argon2Factory;
//import java.util.logging.Logger;
import java.util.Hashtable;


public class NextcloudCredentialHandler implements CredentialHandler {

    // Argon2i is quite slow (on purpose!) so do some caching of already confirmed logins:
    private static Hashtable<String, Long> successfulLoginCache = new Hashtable<String, Long>();

    @Override
    public boolean matches(String inputCredentials, String storedCredentials) {
        Argon2 argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id);
        int firstIndexOfPipe = storedCredentials.indexOf('|');
        if (firstIndexOfPipe < 0 || firstIndexOfPipe > 3) {
           throw new RuntimeException("Unknown format of storedCredentials.");
        }
        String inputCredentialsNoTab = inputCredentials.replace("\\", "\\\\").replace("\t", "\\t");
        String storedCredentialsNoTab = storedCredentials.replace("\\", "\\\\").replace("\t", "\\t");
        String hashTableKey = inputCredentialsNoTab + "\t" + storedCredentialsNoTab;
        Long lastCheckTime = successfulLoginCache.get(hashTableKey);
        if (lastCheckTime != null && lastCheckTime > System.currentTimeMillis() - 180000) { // less than three minutes ago
           return true;
        }
        boolean loginOk = argon2.verify(storedCredentials.substring(firstIndexOfPipe + 1), inputCredentials.toCharArray());
        if (loginOk) {
           successfulLoginCache.put(hashTableKey, System.currentTimeMillis()); 
           return true;
        }
        else {
           return false;
        }
    }

    @Override
    public String mutate(String inputCredentials) {
         Argon2 argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id);
         int iterations = 4;
         int memoryInKB = 65536;
         int parallelism = 1; // Number of threads and compute lanes
         return "3|" + argon2.hash(iterations, memoryInKB, parallelism, inputCredentials.toCharArray());
    }
    
}