Pb running server as non-root service

The objective is to run a local LT server as a systemd service that starts automatically when booting and runs in the background. In part this works, but breaks when running the service as a non-root user without home directory.

Here’s a recipe:

  1. Download LanguageTool to a directory of your choice:

    $ wget https://languagetool.org/download/LanguageTool-stable.zip
    
  2. Install LT into directory /opt/ and create a soft link /opt/LanguageTool so we don’t have to deal with the version number in paths:

    $ sudo unzip -d /opt/ LanguageTool-stable.zip
    $ sudo ln -s /opt/LanguageTool-<version>/ /opt/LanguageTool
    $ ls -l /opt/
    lrwxrwxrwx 1 root root   22 Mär  8 11:05 LanguageTool -> /opt/LanguageTool-4.8/
    drwxr-xr-x 6 root root 4096 Mär  8 11:06 LanguageTool-4.8
    
  3. Create a service unit file languagetool.service in a user writable directory (we need to modify this file later):

    [Unit]
    Description=LanguageTool server
    After=network.target
    
    [Service]
    Type=simple
    ExecStart=/usr/bin/java -cp /opt/LanguageTool/languagetool-server.jar org.languagetool.server.HTTPServer --port 8081 --allow-origin "*"
    
    [Install]
    WantedBy=multi-user.target
    
  4. Copy the unit file to a directory where systemd searches system units:

    $ sudo cp languagetool.service /etc/systemd/system/
    
  5. Open a new terminal to watch service activity:

    $ journalctl -f -u languagetool.service
    
  6. Start the service:

    $ sudo systemctl enable languagetool.service
    Created symlink /etc/systemd/system/multi-user.target.wants/languagetool.service → /etc/systemd/system/languagetool.service.
    $ sudo systemctl start languagetool.service
    
  7. Test the server:

    $ curl --data "language=en-US&text=a simple test" http://localhost:8081/v2/check
    {"software":{"name":"LanguageTool","version":"4.8","buildDate":"2019-12-27 13:18","apiVersion":1,"premium":false,"premiumHint":"You might be missing errors only the Premium version can find. Contact us at support<at>languagetoolplus.com.","status":""},"warnings":{"incompleteResults":false},"language":{"name":"English (US)","code":"en-US","detectedLanguage":{"name":"French","code":"fr","confidence":0.815771}},"matches":[{"message":"This sentence does not start with an uppercase letter","shortMessage":"","replacements":[{"value":"A"}],"offset":0,"length":1,"context":{"text":"a simple test","offset":0,"length":1},"sentence":"a simple test","type":{"typeName":"Other"},"rule":{"id":"UPPERCASE_SENTENCE_START","description":"Checks that a sentence starts with an uppercase letter","issueType":"typographical","category":{"id":"CASING","name":"Capitalization"}},"ignoreForIncompleteSentence":true,"contextForSureMatch":-1}]}
    

This works just fine. Except that the log in the other terminal contains a big warning about the server running as root.

systemd[1]: Started LanguageTool server.
java[28357]: 2020-03-07 16:59:25 +0100 INFO  org.languagetool.server.DatabaseAccess Not setting up database access, dbDriver is not configured
java[28357]: 2020-03-07 15:59:25 +0000 ****************************************************************************************************
java[28357]: 2020-03-07 15:59:25 +0000 *** WARNING: this process is running as root - please do not run it as root for security reasons ***
java[28357]: 2020-03-07 15:59:25 +0000 ****************************************************************************************************
java[28357]: 2020-03-07 15:59:25 +0000 WARNING: running in HTTP mode, consider using org.languagetool.server.HTTPSServer for encrypted connections
java[28357]: 2020-03-07 15:59:27 +0000 Setting up thread pool with 10 threads
java[28357]: 2020-03-07 15:59:27 +0000 Starting LanguageTool 4.8 (build date: 2019-12-27 13:18, 72d565f) server on http://localhost:8081...
java[28357]: 2020-03-07 15:59:27 +0000 Server started

That warning makes certainly sense. A hacker exploiting a bug in LT or Java could do harm on our machine.

To run as non-root, we create a new system user (without home directory). As far as we need configuration files later, we can store them either in /opt/LanguageTool/ or somewhere below /etc/. The following command creates a system user that cannot log in. (A system user has no privileges, it just has no home directory and UID is below 1000):

$ sudo useradd --system --shell /usr/sbin/nologin langtool

Now we need to direct systemd to run the service unit as user langtool.

Disable the running service:

$ sudo systemctl disable languagetool.service
Removed /etc/systemd/system/multi-user.target.wants/languagetool.service.
$ sudo systemctl stop languagetool.service

Add the line

User=langtool

to the service unit file inserting it above the line containing the ExecStart= directive.

Now go to item 4. of the recipe.

After testing the server, this time the log contains these error messages:

systemd[1]: Started LanguageTool server.
java[21759]: 2020-03-08 12:30:31 +0100 INFO  org.languagetool.server.DatabaseAccess Not setting up database access, dbDriver is not configured
java[21759]: 2020-03-08 11:30:31 +0000 WARNING: running in HTTP mode, consider using org.languagetool.server.HTTPSServer for encrypted connections
java[21759]: 2020-03-08 11:30:33 +0000 Setting up thread pool with 10 threads
java[21759]: 2020-03-08 11:30:33 +0000 Starting LanguageTool 4.8 (build date: 2019-12-27 13:18, 72d565f) server on http://localhost:8081...
java[21759]: 2020-03-08 11:30:33 +0000 Server started
java[21759]: 2020-03-08 12:30:40 +0100 ERROR org.languagetool.server.LanguageToolHttpHandler An error has occurred: 'java.lang.IllegalArgumentException: Cannot open file .languagetool.cfg in directory /home/langtool, detected: fr', sending HTTP code 400. Access from 127.0.0.1, HTTP user agent: curl/7.65.3, User agent param: null, Referrer: null, language: en-US, h: 1, r: 1, time: 2376text length: 13, m: ALL, Stacktrace follows:java.lang.RuntimeException: java.lang.IllegalArgumentException: Cannot open file .languagetool.cfg in directory /home/langtool, detected: fr
java[21759]:         at org.languagetool.server.TextChecker.checkText(TextChecker.java:345)
java[21759]:         at org.languagetool.server.ApiV2.handleCheckRequest(ApiV2.java:141)
java[21759]:         at org.languagetool.server.ApiV2.handleRequest(ApiV2.java:71)
java[21759]:         at org.languagetool.server.LanguageToolHttpHandler.handle(LanguageToolHttpHandler.java:170)
java[21759]:         at jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:77)
java[21759]:         at jdk.httpserver/sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:82)
java[21759]:         at jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:80)
java[21759]:         at jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:692)
java[21759]:         at jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:77)
java[21759]:         at jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:664)
java[21759]:         at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
java[21759]:         at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
java[21759]:         at java.base/java.lang.Thread.run(Thread.java:834)
java[21759]: Caused by: java.util.concurrent.ExecutionException: java.lang.IllegalArgumentException: Cannot open file .languagetool.cfg in directory /home/langtool
java[21759]:         at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
java[21759]:         at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
java[21759]:         at org.languagetool.server.TextChecker.checkText(TextChecker.java:326)
java[21759]:         ... 12 more
java[21759]: Caused by: java.lang.IllegalArgumentException: Cannot open file .languagetool.cfg in directory /home/langtool
java[21759]:         at org.languagetool.gui.Configuration.<init>(Configuration.java:188)
java[21759]:         at org.languagetool.gui.Configuration.<init>(Configuration.java:183)
java[21759]:         at org.languagetool.gui.Configuration.<init>(Configuration.java:179)
java[21759]:         at org.languagetool.gui.Configuration.<init>(Configuration.java:171)
java[21759]:         at org.languagetool.server.PipelinePool.configureFromGUI(PipelinePool.java:234)
java[21759]:         at org.languagetool.server.PipelinePool.createPipeline(PipelinePool.java:207)
java[21759]:         at org.languagetool.server.PipelinePool.getPipeline(PipelinePool.java:175)
java[21759]:         at org.languagetool.server.TextChecker.getRuleMatches(TextChecker.java:509)
java[21759]:         at org.languagetool.server.TextChecker.access$000(TextChecker.java:54)
java[21759]:         at org.languagetool.server.TextChecker$1.call(TextChecker.java:319)
java[21759]:         at org.languagetool.server.TextChecker$1.call(TextChecker.java:312)
java[21759]:         at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
java[21759]:         ... 3 more

The error seems to happen when reading a file .languagetool.cfg in the user’s home directory. Since that file doesn’t exist for root as well and no error happened in the first attempt, I think the error occurs because the home directory doesn’t even exist. LT not catching that case looks like a bug to me.

Am I missing something?

Hi,
thanks for reporting this; it’s definitely a bug.
I made a PR with a fix, that should resolve the issue.

Thank you!

I can confirm that with the LT 4.9 snapshot of 2020-03-09 running the server as a system user works. I had lots of “broken pipe” messages in the log with the unit file mentioned before. Those could be silenced by adding, e.g.,

WorkingDirectory=/opt/LanguageTool

to the unit file (in section [service]). (Not that user langtool had write access in that directory, but it seems to work anyway.)

When starting the server the first time with this setting, there were still some “broken pipe” messages, but they seem to be gone now. I’ll keep an eye on that.