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());
    }
    
}