Setup Cards Against Humanity clone on Linux ARM Board (ODROID-C1, Rasperry Pi...)

Personal Project

Preface (useless as usual, you may just skip to the guide)

Since I’m currently in lockdown due to the current COVID-19 outbreak I’ve had to find ways to spend my free time while staying at home. After hours of very intricate discussions on Telegram a couple of friends and I agreed that a good solution would be to play Cards Against Humanity together. Using an external sounded incredibly boring, therefore what I did was configure my own Pretend You’re Xyzzy server (Pretend You’re Xyzzy is a cards against humanity clone which from now own we will simply refer to as PYX).

The source code documentation (and surprisingly even some Instructables) suggested to run it directly from Maven, which takes care of building the application and spinning up a jetty server, however that solution is incredibly inefficient and wastes a ton of resources on a machine with little memory. Therefore after using that approach for testing I configured it for “production” (if we can call it that) using an actual Jetty server and modified my NGINX configuration to integrate that application along with the others I already have, where with integrate I mean serving the static content.

This guide will be divided in two parts. The first one is the “basic setup”, which includes only how to setup a Jetty server with Pretend Your Xyzzy. The second one is an extra section that explains how to integrate the application with NGINX, in case you have other platforms you want to keep running (a simple blog, cloud, whatever…).

First Part: Basic setup

First thing you want to do is make sure you have all your dependencies installed. In my case I only had to install maven and git. I’m using Ubuntu 18.04 therefore these packages were available through apt.

Once you’ve done that grab your favorite fork of PYX. In my case I used this repository and got the latest revision using the command:

$ git clone https://github.com/ajanata/PretendYoureXyzzy

When the cloning operation has completed simply enter the directory with:

$ cd PretendYourXyzzy

OK! Looks like we are done with the easy stuff, let’s try to build it.

There are two things we need to take care of.

First let’s create our build.properties, luckily our repo already has a prepared example. To load it just do:

$ cp build.properties.example build.properties

Second if we are following this tutorial for an ARM board there is a dependency which might need to be changed, specifically some versions of jdbc-sqlite might cause the application to crash on startup. To fix that edit the file pom.xml with an editor of your choice and look for the following piece of code:

<dependency>
      <groupId>org.xerial</groupId>
      <artifactId>sqlite-jdbc</artifactId>
      <version>3.8.7</version>
</dependency>

And change the version to 3.23.1. Note that this worked in my case where I was using an ODROID-C1 with Ubuntu 18.04, your setup might have different requirements, I suggest you look online to see what other people have to suggest.

OK, looks like we are ready to build. First we will use this command to test the server locally:

$ mvn clean package war:exploded jetty:run -Dmaven.buildNumber.doCheck=false -Dmaven.buildNumber.doUpdate=false

The last two options are needed since we made some changes and we do not want to upload them.

Once you’ve ran that command Maven is going to download some dependencies and finally build and run the application. You will know that the webserver is running when the following message appears:

"Console reloading is ENABLED. Hit ENTER on the console to restart the context."

OK, now you can test your application by visiting http://localhost:8080 on your web browser. You should be able to access it from our LAN as well by replacing localhost with the server’s IP address, but I have not tested it. In the case it does not work it means that the Jetty instance running on your server is blocking external IP addresses and the configuration needs to be changed, Google is your friend and I’m sure it will easily provide answers to that issue.

Technically you could even stop here with this guide, since the application should work fairly well, however I noticed a very high memory usage when using this approach, therefore I took it a step further and setup an actual Jetty server on my machine, which it turns out is not that hard to do as long as you know how to manage services on Linux.

Setting up Jetty

First of all you have to install jetty. In the case of Ubuntu 18.04 the package you need is jetty9, the basic configuration works just fine so we don’t need to care too much about that.

If you haven’t tested the package in the previous section you probably still need to build it, run the following command

$ mvn package war:exploded -Dmaven.buildNumber.doCheck=false -Dmaven.buildNumber.doUpdate=false

If someone already familiar with maven and jetty is wondering why I’m using war:exploded, this is because in the third step of the guide I will setup my application in order to serve the static content with NGINX, therefore I’d rather have an “exploded” package ready.

To deploy PYX we have to options:

  • Set it as the root application. This means that the application will be reachable at http://localhost:8080 This is the choice I made since it made it easier to implement a Reverse Proxy with NGINX later.
  • Set it up as an additional application. This means the application will be reachable at http://localhost:8080/ZY (or whatever the name you will give to the folder). This could be a solution in the case you are already using jetty for something else

To set it up as root application remove the folder root (or better move it and call it something else in case you need it in the future) in /usr/share/webapps using the command:

# mv /usr/share/webapps/root /usr/share/webapps/root.old

then from your build directory (the one we built the program in, I’m sure you can remember) do the following to set PYX as the root app:

# cp -R target/ZY /usr/share/webapps/root

If you follow the second approach and wish to set PYX as an additional application simply copy it in a new folder

# cp -R target/ZY /usr/share/webapps/

In this case it will be reachable through the address http://localhost:8080/ZY (note that ZY can be any folder, in our case the developer decided to call it ZY and we simply left it that way)

OK. We are almost done with the setup, last thing we need to do is import the database. First we need to copy the database somewhere where jetty can access, for example the root directory

# cp -R pyx.sqlite /usr/share/webapps/root

Then we need to go in the folder /usr/share/webapps/root/WEB-INF/classes/hibernate.cfg.xml and edit the line which contains

<property name="hibernate.connection.url">jdbc:sqlite:pyx.sqlite</property>

and modify pyx.sqlite with the absolute path of the database, in our case /usr/share/webapps/root/pyx.sqlite.

Finally let’s set the correct owner for the root directory and its content. If you are using Ubuntu 18.04 you can run this command:

# chown -R jetty:adm /usr/share/webapps/root && chmod -R 755 /usr/share/webapps/root

This will set the owner to the user running jetty and it will authorize write operations only to jetty. If you are not using Ubuntu 18.04 check what user owns the root.old folder that we renamed in the first part of the guide using ls -l and change the chown command accordingly.

At this point everything should be ready to go, you can start jetty and enable the service (the latter only if you want it to run on boot)

# systemctl start jetty9
# systemctl enable jetty9

If everything went well, after a couple of seconds you should be able to access PYX at http://localhost:8080 (or http://localhost:8080/ZY depending on which approach you followed).

If you’re not interested in integrating PYX with your webserver you can stop here, since now you should be running PYX with an acceptable memory usage. If you don’t believe me try running the “free” command with this approach and by running the server with mvn jetty:run (see the first part for the full command).

Third step: Integrating PYX with NGINX

Since this is a guide aimed at someone who already has NGINX setup it won’t contain lots of details.

A possible reason why you would want to integrate PYX and NGINX is that if you’re already serving some static content it might be good to integrate everything and have NGINX serve it. To do so I recommend you copy the previous folder not only in the /usr/share/jetty9/webapps directory but also in /var/www, or wherever it is you have your webserver root. I’m saying recommend because you could also choose to have /usr/share/jetty9/webapps/root as your root in NGINX.

Once you’ve done that open up your NGINX configuration and setup a new server with root /var/www/ZY , for example you could have a virtual server on your domain pyx.example.com. Next add the locations as follows:

location /js/cah.config.js {
		proxy_pass http://localhost:8080/js/cah.config.js;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  Host $http_host;
}

location ~ \.jsp$ {
		proxy_pass http://localhost:8080;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  Host $http_host;
}

location ~ \.do$ {
        proxy_pass http://localhost:8080;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  Host $http_host;
}

location /AjaxServlet {
	proxy_pass        http://localhost:8080/AjaxServlet;
	proxy_set_header  X-Real-IP $remote_addr;
	proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header  Host $http_host;
}

location /LongPollServlet {
        proxy_pass        http://localhost:8080/LongPollServlet;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  Host $http_host;
}

The first location is needed since the first js file is served dynamically and not statically like the other javascript files, therefore it needs to be grabbed from the jetty webserver.

The second and third locations tell us that we need to have all the .jsp and .do files processed by jetty (although I’m not positive the latter is strictly needed in this specific application).

The third and fourth location simply redirect the Ajax and LongPoll requests to the proper servlets, much like we are redirecting the JSP requests.

Alright, if you’ve setup everything properly you should be good to go, reload nginx, make sure jetty is running and your server should be reachable, with static content coming from NGINX and all the Java work done by Jetty.

Conclusions, remarks, whatever…

Obviously, as anything on this blog, this setup could be further improved. For example, you could try setting up a PostgreSQL server and use that as database instead of the SQLite approach, however I don’t know if that would bring any particular benefit when dealing with a machine that has low memory. Anwyay, if you’re planning on playing with a couple of trusted friends I’m sure you are not going to have any particular issue. If you have any comments, suggestions or questions you can obviously hit me up on social media and we can discuss it so I can improve this post and, possibly, help others.

EXTRA: setup authentication for admins

By default the server only allows certain IP addresses to access admin features. You can remove that directly in the source code of the files addcard.jsp, admin.jsp, cardsets.jsp and then setup authentication using NGINX with the following configuration:

location = /admin.jsp {
	proxy_pass http://localhost:8080/admin.jsp;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  Host $http_host;
	auth_basic "Control panel login";
	auth_basic_user_file .htpasswd;	
}

location = /cardsets.jsp {
        proxy_pass http://localhost:8080/cardsets.jsp;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  Host $http_host;
        auth_basic "Control panel login";
        auth_basic_user_file .htpasswd;
}
location = /addcard.jsp {
        proxy_pass http://localhost:8080/addcard.jsp;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  Host $http_host;
        auth_basic "Control panel login";
        auth_basic_user_file .htpasswd;
}

Notice that you have to use the ‘=’ in the location, otherwise the expression will not be matched first and it will be treated like any other jsp file.

Obviously you need to setup an .htpasswd file and you must configure SSL, otherwise you will have your passwords being sent out in plain-text. You can find plenty of other tutorials online.

EXTRA 2: Setting up the server with MySQL (MariaDB)

Since I already had a MariaDB instance already running on my server I wanted to avoid wasting resources using SQLite. This is not officially recommended by the developers since they suggest using PostgreSQL on production server, however since setting it up on a ODROID-C1 (Raspberry Pi) is not really “production” and we are looking for a lighter solution (in fact MariaDB is lighter than PostgreSQL) we can safely replace it. Additionally the software uses Hibernate to interface with the DB, so changing the engine shouldn’t cause too many problems.

Part 1: Let’s setup the DB

First of all let’s prepare the MySQL database. Since we don’t want our application to interfere with anything else we are going to create a new database and a new user.

Login as root on your MySQL instance (mysql -u root -p) and run the following command to create the database:

mysql> CREATE DATABASE pyx_db;

This will create a database called pyx_db that we will fill with the white and black cards.

Since we don’t want to use a privileged account to access the database let’s create a new user.

mysql> CREATE USER 'pyx'@'127.0.0.1' IDENTIFIED BY 'password';

This will create a user pyx with password: password, obviously you should change those to something else. Notice that we are writing 127.0.0.1 instead of localhost, this is because if you write localhost the access can only be done through a Unix socket and not through a TCP socket. We want to have a TCP socket since Hibernate does not natively support Unix sockets.

Let’s give the user permissions:

mysql> GRANT ALL PRIVILEGES ON pyx_db.* TO 'pyx'@'127.0.0.1';

This will grant all privileges on all the tables contained in the pyx_db to the user we just created, therefore any other database will remain untouched as long as every operation is done with this account.

Finally exit

mysql> exit;

The database is now ready to be filled. In order to do so we have two options:

  • Use the cah_cards.sql file included in the source code and convert it to MySQL and import the dump with:

    mysql -u pyx -p pyx_db < cah_cards.sql

  • Get the pyx.sqlite file, dump it, convert it mysql and import the dump.

I chose the second option since I added some custom cards to the installation and wanted to avoid adding them again.

Dumping and converting the database is fairly easy as long as you have sqlite3 installed, I followed this guide and got the job done in no time.

Once you have your SQL file ready simply import it as you would do for any dump

mysql -u pyx -p pyx_db < mydump.sql

Remember to replace pyx with your user using the password you chose before.

The database is now ready to be used.

Part 2: Let’s configure PYX

OK, let’s go back to your PYX build folder since there are some changes that need to be made.

Open up your build.properties and look up the section regarding hibernate and make sure you comment (or delete) all the lines containing: hibernate.dialect, hibernate.driver_class, hibernate.url, hibernate.username, hibernate.password.

Now insert the following instead

hibernate.dialect=org.hibernate.dialect.MySQLDialect
hibernate.driver_class=com.mysql.cj.jdbc.Driver
hibernate.url=jdbc:mysql://localhost:3306/pyx_db
hibernate.username=pyx
hibernate.password=password

This will set the dialect (the language we will use to interface with the DB) to MySQL and will select MySQL Connector/J as the driver, as well as set the MySQL instance along with the username and password to access it.

Now since we are using MySQL Connector/J we must add it as a dependency. To do so open up the pom.xml file and add the following along with the other dependencies

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.19</version>
</dependency> 

Latest version seemed to be working ok on my system but, as usual, if something seems to be not working change the version and see what happens.

Alright seems like everything is in place. Test the program like you did before using the command

mvn clean package war:exploded jetty:run -Dmaven.buildNumber.doCheck=false -Dmaven.buildNumber.doUpdate=false

and if everything seems to be working you can deploy it to your “true” Jetty instance following Part 2.