, 7 min read

Installing and Configuring the H2O Web-Server

1. Task at hand. Install H2O web-server on Arch Linux. H2O is a web-server written by Kazuho Oku et al. It supports:

  1. HTTP/1 and HTTP/1.1,
  2. HTTP/2,
  3. HTTP/3 ("QUIC"),
  4. FastCGI, therefore PHP-FPM,
  5. Reverse proxy,
  6. Builtin mruby, though, that crashes.

In benchmarks it ranks at the top constantly. See Web Framework Benchmarks.

Photo

It works way faster than NGINX or Apache. It shines for static web content.

2. Building. The already existing AUR packages for H2O do not work. I.e., they generate a binary which crashes. Below PKGBUILD produces a H2O binary.

pkgname=h2o-master-git
pkgver=1.0
pkgrel=1
arch=('i686' 'x86_64')
pkgdesc="H2O: the optimized HTTP/1.x, HTTP/2, HTTP/3 server"
provides=(h2o)
url="https://h2o.examp1e.net"
source=("git+https://github.com/h2o/h2o.git?commit=master?signed/" h2o.service)
sha256sums=('SKIP' 734e9d045dd5568665762d48e4077208c3da8c68f87510aaa9559d495dd680fd)


build() {
    cd "$srcdir"/h2o
    cmake -DCMAKE_INSTALL_PREFIX=/usr .
    make
}

package() {
    cd "$srcdir"/h2o
    install -Dm 644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE
    install -Dm 644 README.md "$pkgdir"/usr/share/doc/h2o/README.md
    install -Dm 644 "$srcdir"/h2o.service "$pkgdir"/usr/lib/systemd/system/h2o.service
    install -Dm 644 examples/h2o/h2o.conf "$pkgdir/etc/h2o.conf"
    make DESTDIR="$pkgdir" install
}

Compiling on AMD Ryzen 7 5700G, max clock 4.673 GHz, 64 GB RAM, finishes in less than two minutes.

$ time makepkg -f
...
==> Tidying install...
  -> Removing libtool files...
  -> Purging unwanted files...
  -> Removing static library files...
  -> Copying source files needed for debug symbols...
  -> Compressing man and info pages...
==> Checking for packaging issues...
==> Creating package "h2o-master-git"...
  -> Generating .PKGINFO file...
  -> Generating .BUILDINFO file...
  -> Generating .MTREE file...
  -> Compressing package...
==> Leaving fakeroot environment.
==> Finished making: h2o-master-git 1.0-1 (Fri 12 Apr 2024 09:48:36 PM CEST)
        real 92.42s
        user 447.76s
        sys 0
        swapped 0
        total space 0

3. Configuration. Below is a working configuration in file /etc/h2o.conf. The configuration accomplishes the following:

  1. it serves http and https,
  2. it compresses via gzip and brotli,
  3. it is started user root, then switches to user http,
  4. Log format is similar to the Hiawatha log-format,
  5. PHP files are handled by php-fpm.

The entire configuration file is a YAML file.

listen: 80
listen: &ssl_listen
  port: 443
  ssl:
    certificate-file:    /etc/letsencrypt/live/eklausmeier.goip.de/fullchain.pem
    key-file:  /etc/letsencrypt/live/eklausmeier.goip.de/privkey.pem
    minimum-version: TLSv1.2
    cipher-preference: server
    cipher-suite: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"
    # Oldest compatible clients: Firefox 27, Chrome 30, IE 11 on Windows 7, Edge, Opera 17, Safari 9, Android 5.0, and Java 8
    # see: https://wiki.mozilla.org/Security/Server_Side_TLS

# The following three lines enable HTTP/3
listen:
  <<: *ssl_listen
  type: quic
header.set: "Alt-Svc: h3-25=\":443\""

user: http
#pid-file: /var/run/h2o/h2o.pid
#crash-handler: /usr/local/bin/h2obacktrace
access-log:
  path: /var/log/h2o/access.log
  format: "%h|%{%Y/%m/%d:%T %z}t|%s|%b|%r|%{referer}i|%{user-agent}i|%V:%p|"
error-log: /var/log/h2o/error.log
compress: [ br, gzip ]
#file.dirlisting: ON

file.custom-handler:
  extension: .php
  fastcgi.connect:
    port: /run/php-fpm/php-fpm.sock
    type: unix

hosts:
  0:
    paths:
      /jpilot/favicon.ico:
        file.file: /home/klm/php/saaze-jpilot/public/favicon.ico
      /jpilot/img:
        file.dir: /home/klm/php/saaze-jpilot/public/img
      /jpilot/jpilot.css:
        file.file: /home/klm/php/saaze-jpilot/public/jpilot.css
      /koehntopp/assets:
        file.dir: /home/klm/php/saaze-koehntopp/public/assets
      /koehntopp/jscss:
        file.dir: /home/klm/php/saaze-koehntopp/public/jscss
      /lemire/jscss:
        file.dir: /home/klm/php/saaze-lemire/public/jscss
      /mobility/img:
        file.dir: /home/klm/php/saaze-mobility/public/img
      /nukeklaus/img:
        file.dir: /home/klm/php/saaze-nukeklaus/public/img
      /nukeklaus/jscss:
        file.dir: /home/klm/php/saaze-nukeklaus/public/jscss
      /panorama/img:
        file.dir: /home/klm/php/saaze-panorama/public/img
      /paternoster/paternoster.css:
        file.file: /home/klm/php/saaze-paternoster/public/paternoster.css
      /saaze-example/blogklm.css:
        file.file: /home/klm/php/saaze-example/public/blogklm.css
      /vonhoff/img:
        file.dir: /home/klm/php/saaze-vonhoff/public/img
      /wendt/pagefind:
        file.dir: /home/klm/php/saaze-wendt/public/pagefind
      /:
        file.dir: /srv/http
        redirect:
          status: 301
          internal: YES
          url: /index.php?
      /p:
        mruby.handler: |
          Proc.new do |env|
            [200, {'content-type' => 'text/plain'}, ["Hello world"]]
          end

As already mentioned at the top: mruby doesn't work. Once you access /p the entire web-server crashes.

H2O does not offer URL rewriting out of the box. The above path-configurations operate on a prefix match schema. I.e., if the URL in question starts with the string provided, this is considered a match. The string after the match is appended to the part in file.dir.

4. Discussion. While alternatives to Apache and NGINX are highly welcome, the current state of H2O leaves many questions unanswered.

  1. The builtin brotli compression is "stone old": it is seven years behind the official Google Brotli repository, which contains a number of serious fixes.
  2. The builtin mruby software is two years behind, offering mruby version 3.1 instead of 3.3.
  3. mruby crashes once called.
  4. In the hosts part the hostname seems to have no effect.

I tried to replace the old mruby dependency with the current 3.3 version. The build of H2O then failed.

While embodying software packages directly into the H2O GitHub repo makes building the software easier, it risks that the included software rots. That's exactly what is happening here.

Fun fact: I noticed H2O when reading about the LWAN web-server written by L. Pereira. Both, Kazuho Oku and L. Pereira, work at Fastly.

Also see H2O Tutorial.

In case someone wants to analyze why mruby crashes, here is the result of where in gdb:

Core was generated by `h2o -c h2o.conf'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x000062085dc9ae9b in mrb_str_hash (mrb=<optimized out>, str=...) at /usr/src/debug/h2o-master-git/h2o/deps/mruby/src/string.c:1673
1673        hval ^= (uint32_t)*bp++;
[Current thread is 1 (Thread 0x7002156006c0 (LWP 18088))]
(gdb) where
#0  0x000062085dc9ae9b in mrb_str_hash (mrb=<optimized out>, str=...) at /usr/src/debug/h2o-master-git/h2o/deps/mruby/src/string.c:1673
#1  0x000062085dc8cb6c in obj_hash_code (h=0x7001d0028660, key=..., mrb=0x1a0) at /usr/src/debug/h2o-master-git/h2o/deps/mruby/src/hash.c:325
#2  ib_it_init (mrb=mrb@entry=0x7001d00015a0, it=it@entry=0x7002155fe550, h=h@entry=0x7001d0028660, key=...) at /usr/src/debug/h2o-master-git/h2o/deps/mruby/src/hash.c:645
#3  0x000062085dc8cd3a in ib_init (ib_byte_size=<optimized out>, ib_bit=<optimized out>, h=0x7001d0028660, mrb=<optimized out>) at /usr/src/debug/h2o-master-git/h2o/deps/mruby/src/hash.c:151
#4  ht_init (mrb=mrb@entry=0x7001d00015a0, h=h@entry=0x7001d0028660, size=size@entry=17, ea=0x7001d0047700, ea_capa=ea_capa@entry=25, ht=ht@entry=0x0, ib_bit=<optimized out>) at /usr/src/debug/h2o-master-git/h2o/deps/mruby/src/hash.c:793
#5  0x000062085dc8d11a in ar_set (mrb=0x7001d00015a0, h=0x7001d0028660, key=..., val=...) at /usr/src/debug/h2o-master-git/h2o/deps/mruby/src/hash.c:536
#6  0x000062085dc8c2e6 in h_set (val=..., key=..., h=0x7001d0028660, mrb=0x7001d00015a0) at /usr/src/debug/h2o-master-git/h2o/deps/mruby/src/hash.c:169
#7  mrb_hash_set (mrb=0x7001d00015a0, hash=..., key=..., val=...) at /usr/src/debug/h2o-master-git/h2o/deps/mruby/src/hash.c:1245
#8  0x000062085dc67938 in iterate_headers_callback (shared_ctx=shared_ctx@entry=0x7001d0001540, pool=pool@entry=0x7001d0076958, header=header@entry=0x7002155fe8d0, cb_data=cb_data@entry=0x7001d0028660) at /usr/src/debug/h2o-master-git/h2o/lib/handler/mruby.c:748
#9  0x000062085dc67e4c in h2o_mruby_iterate_native_headers (shared_ctx=shared_ctx@entry=0x7001d0001540, pool=<optimized out>, headers=<optimized out>, cb=cb@entry=0x62085dc678a0 <iterate_headers_callback>, cb_data=cb_data@entry=0x7001d0028660)
    at /usr/src/debug/h2o-master-git/h2o/lib/handler/mruby.c:727
#10 0x000062085dc6a76e in build_env (generator=0x7001d006cbe0) at /usr/src/debug/h2o-master-git/h2o/lib/handler/mruby.c:836
#11 on_req (_handler=<optimized out>, req=<optimized out>) at /usr/src/debug/h2o-master-git/h2o/lib/handler/mruby.c:974
#12 0x000062085dbc603a in call_handlers (req=0x7001d00765d8, handler=0x62085f2d5ef0) at /usr/src/debug/h2o-master-git/h2o/lib/core/request.c:165
#13 0x000062085dbeeb89 in handle_incoming_request (conn=<optimized out>) at /usr/src/debug/h2o-master-git/h2o/lib/http1.c:714
#14 0x000062085dba6293 in run_socket (sock=0x7001d009b660) at /usr/src/debug/h2o-master-git/h2o/lib/common/socket/evloop.c.h:834
#15 run_pending (loop=loop@entry=0x7001d0000b70) at /usr/src/debug/h2o-master-git/h2o/lib/common/socket/evloop.c.h:876
#16 0x000062085dba6300 in h2o_evloop_run (loop=0x7001d0000b70, max_wait=<optimized out>) at /usr/src/debug/h2o-master-git/h2o/lib/common/socket/evloop.c.h:925
#17 0x000062085dc5da1b in run_loop (_thread_index=<optimized out>) at /usr/src/debug/h2o-master-git/h2o/src/main.c:4210
#18 0x000070022b8a955a in ?? () from /usr/lib/libc.so.6
#19 0x000070022b926a3c in ?? () from /usr/lib/libc.so.6