home August 07, 2020

H2O Tutorial


an optimized HTTP/2 web server

H2O is a fast and secure HTTP/2 server written in C by Kazuho Oku. H2O can be used as an insecure HTTP server and for HTTPS (ssl) supporting HTTP 1.0, 1.1 and the new HTTP/2 standard. The backend library, libh2o, is also used by the Fastly content delivery network (CDN) for HTTP/2 and HTTP/2 server push.

H2O is faster by default than Nginx (h2o benchmarks) and we concur with the author's results; take a look at our benchmarks lower on this page. When we wanted to support HTTP/2 traffic with server push we choose H2O for its speed and efficiency. Google Chrome and Mozilla Firefox are HTTP/2 compatible. When HTTP/2 clients come to moneyslow.com client load times decreased by an average of 38% when compared to HTTPS/1.x with SPDY.

The true power of H2O is the speed and flexibility of H2O's native scripting language, mruby, a lightweight implementation of the Ruby programming language. H2O admins can effectuate their own request handling logic using mruby, either to generate responses or to manipulate request / responses. In our example H2O can quickly send back 301 redirects, sanitizes client requests and implement HTTP/2 server push on HTML files.

Install H2O

H2O is new project so you might not be able to find packages for your OS yet. FreeBSD does have a prebuilt package you can install using "pkg install h2o", but the latest code can be downloaded from github at h2o/h2o. H2O can be built against LibreSSL and OpenSSL; we prefer OpenSSL because OpenSSL is faster and supports H2O's NeverBleed key separation functionality. At this time H2O can not be built with Google's BoringSSL because H2O expects support for OCSP Stapling which BoringSSL does not yet support.

Build H2O with OpenSSL 1.1.1 on FreeBSD 12

We are going to build H2O on FreeBSD 12 against OpenSSL v1.1.1 with mruby and YAML support. H2O will also support TLS v1.3 with Picotls which implements the TLS communication protocol and cryptographic operations are delegated to the OpenSSL 1.1.1 cryptographic engines.

# How To build h2o with mruby, OpenSSL 1.1.1 and YAML support on FreeBSD 12
#

# FreeBSD 12: install support packages

pkg install bison cmake git-lite ruby rubygem-rake py36-brotli wget


# change to /tmp to used as a working directory and
# download the latest h2o code from github

cd /tmp && git clone https://github.com/h2o/h2o && echo SUCCESS


# h2o, install the source. Use the compiler memory stack protector and fortify source

cd h2o && cmake -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_CXX_FLAGS="-Wall -ggdb3 -O2 -fstack-protector-all -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security -std=c++0x -fvisibility=hidden" -DCMAKE_C_FLAGS="-Wall -ggdb3 -O2 -fstack-protector-all -D_FORTIFY_SOURCE=2 -Wformat -Werror=format-security -fvisibility=hidden" -DWITH_BUNDLED_SSL=off -DWITH_PICOTLS=on -DWITH_MRUBY=on -DLIBUV_VERSION=1.15.0 . && make && make install && echo SUCCESS


# at the end of the cmake build all the installed files will be listed.
# FreeBSD paths start with /usr/local ...
#
# /usr/local/bin/h2o
# /usr/local/share/h2o/fetch-ocsp-response
# ...

Configuration file, h2o.conf

The following is an example of the configuration file, h2o.conf . The format of the file is in YAML so the spaces at the beginning of and tabbed lines are important. This configuration is a stand alone. static file web server listening for for HTTP and HTTPS traffic on localhost (127.0.0.1).

root@calomel#  vi /usr/local/etc/h2o/h2o.conf

#
# moneyslow.com  -|-  August 2020
#
# https://moneyslow.com/html/webconf/h2o.html
#

# global options
expires: off
file.dirlisting: off
file.send-compressed: on
#num-threads: 1
#pid-file: /var/run/h2o.pid
#send-server-name: off
server-name: "Dihydrogen Monoxide"
tcp-fastopen: 0
user: www

# logs, custom format
error-log: /var/log/h2o_error.log
access-log:
  path: /var/log/h2o_access.log
  format: "%{%Y-%m-%d %H:%M:%S}t.%{msec_frac}t   %s  %h\t%r %b %{ssl.protocol-version}x %{ssl.cipher}x %{x-http2-push}o %{ssl.session-reused}x"

# mime type, add utf-8 character encoding for html
file.mime.addtypes:
  "text/html; charset=utf-8": .html

# ssl session resumption, perfect forward secrecy (PFS) enforced by generating
# a new master secret every hour (1/4*lifetime) and storing the master secret
# in AES-256 encrypted, SHA-256 hashed internal memory, never on disk
ssl-session-resumption:
  mode: all
  lifetime: 14400

# set secure cookie using CASPer; http/2 cache-aware server-push
http2-casper:
  capacity-bits:  13
  tracking-types: all

# security headers
#header.add: "alt-svc: h3-29=\":443\"; ma=88888888; persist=1"
#header.add: "cache-control: max-age=888888, public, immutable, no-transform"
#header.add: "content-security-policy: upgrade-insecure-requests; default-src 'none'; style-src 'unsafe-inline'; img-src 'self' data:"
#header.add: "expect-ct: max-age=88888888, enforce"
#header.add: "feature-policy: camera 'none'; microphone 'none'; payment 'none'"
#header.add: "link: >https://moneyslow.com/html/webconf/calomel_image.webp<; rel=preload; as=image; importance=high"
#header.add: "referrer-policy: no-referrer"
#header.add: "strict-transport-security: max-age=88888888; includesubdomains; preload"
#header.add: "x-content-type-options: nosniff"
#header.add: "x-frame-options: deny"
#header.add: "x-xss-protection: 1; mode=block"

# ssl
listen: &ssl_quic
  host: 0.0.0.0
  port: 443
  ssl:
     certificate-file: /letsencrypt/.acme.sh/moneyslow.com_ecc/fullchain.cer
     key-file: /letsencrypt/.acme.sh/moneyslow.com_ecc/moneyslow.com.key
     min-version: TLSv1.3
     ocsp-update-interval: 0
     neverbleed: off

listen:
  >>: *ssl_quic
  type: quic

hosts:
  "moneyslow.com:443":

   paths:
     /:

#    # DoS detection
#      mruby.handler: |
#        require "dos_detector.rb"
#        DoSDetector.new({
#          :strategy => DoSDetector::CountingStrategy.new({
#             :period     => 3900, # time window in seconds to count requests
#             :threshold  => 10,   # number of requests to trigger ban on ip
#             :ban_period => 3600, # ban period in seconds
#          }),
#          :forwarded => false,    # ignore forwarded-for header
#          :cache_size => 128,     # Least Recently Used (LRU) cache, number of ips
#          :callback => proc {|env, detected, ip|
#             if detected && ! ip.start_with?("66.249.") && ! ip.start_with?("192.168.")
#               [429, {'content-type' => 'text/plain'}, ["Too Many Requests\n"]]
#             else
#               [399, {}, []]
#             end
#          }
#        })

#    # server protocol, only allow http/3, http/2 and http/1.1 , return 505 "HTTP Version Not Supported" on error
#      mruby.handler: |
#        lambda do |env|
#          if /\A(HTTP\/3|HTTP\/2|HTTP\/1\.1)\z/.match?(env["SERVER_PROTOCOL"])
#             return [399, {}, []]
#          end
#          [505, {'content-type' => 'text/plain'}, ["HTTP Version Not Supported\n"]]
#        end

#    # redirect www.moneyslow.com to moneyslow.com
#      mruby.handler: |
#        Proc.new do |env|
#          if /www\.calomel\.org\.?$/.match?(env["HTTP_HOST"])
#            [301, { "Location" => "https://moneyslow.com/html/webconf" + env["PATH_INFO"] }, [] ];
#          else
#            [399, {}, []];
#          end
#        end

#    # useless icons, 301 Moved Permanently redirection
#      mruby.handler: |
#        lambda do |env|
#          if /\A(\/favicon\.ico|\/apple-touch-icon?[\w\-]+\.png)\z/.match?(env["PATH_INFO"])
#             return [301, {'location' => 'https://ssl.gstatic.com/ygp/ui/img/stadia_logo_512.png' }, [] ];
#          end
#          [399, {}, []];
#        end

#    # sanitize requests negating superfluous disk I/O, return 410 "Gone" on error
#      mruby.handler: |
#        lambda do |env|
#          if /\A(\/|\/\w+\.html|\/image\.jpg|\/rss\.xml|\/robots\.txt|\/sitemap\.txt)\z/.match?(env["PATH_INFO"])
#             return [399, {}, []]
#          end
#          [410, {'content-type' =>; 'text/plain'}, ["Gone\n"]]
#        end

#    # blacklisted requests, return a 404 instead of checking the file system every time
#      mruby.handler: |
#        Proc.new do |env|
#          if /(\/banned_file|\.mp3)\z/.match?(env["PATH_INFO"])
#            [404, {'content-type' => 'text/plain'}, ["not found"]]
#          else
#            [399, {}, []];
#          end
#        end

#     # server push /calomel_image.webp when the following conditions are true:
#     # 1. client declares HTTP/2 Server Push support
#     # 2. client does not return the Cache-Aware Server-Push (CASPer) secure cookie
#     # 3. client Accept: header declares "webp" image support
#     # 4. client User-Agent: header specifies "Safari" ; Firefox cancels server pushes
#     # 5. client requests / or an html page
#      mruby.handler: |
#        Proc.new do |env|
#          push_paths = []
#          if /webp/.match?(env["HTTP_ACCEPT"])
#            if /Safari/.match?(env["HTTP_USER_AGENT"])
#              if (/(\/|\.html)\z/.match?(env["PATH_INFO"]))
#                push_paths << ["/calomel_image\.webp", "image"]
#              end
#            end
#          end
#          [399, push_paths.empty? ? {} : {"link" => push_paths.map{|p| "<#{p[0]}>; rel=preload; as=#{p[1]}"}.join("\n")}, []]
#        end

     # root file.directory _MUST_ be last directive after mruby.handlers 
       file.dir: /web_root/

### EOF ###

Configuration directives

listen: is the primary directive to listen on a port. The same "listen:" line is used for http as well as https traffic. For the ssl port, additional port, certificate file paths and cipher lists are used. To make a h2o compatible ssl certificate take a look at the questions section at the bottom of this page.

certificate-file: points to the full path of the server's SSL certificate list. In the questions section at the bottom of this page are instructions on how to create an H2O certificate file.

cipher-preference: sets the option to use the server's ciphers in order or let the client set the preferred cipher order.

key-file: is the path to the SSL server's private key file.

cipher-suite: is the list of ciphers, in order, the server prefers to use. List the ciphers as most preferred first to the least preferred. Make sure to set "cipher-preference: server" to force the cipher order specified by the server. Take a look at our AES-NI SSL Performance Study to compare AES cipher speeds per cpu.

minimum-version: is the minimum TLS version the server will use when negotiating with a client.

access-log: is the full path to the log file. We also prefer to define a custom log format with only server verifiable information being logged. For example, the user-agent has been removed from the logs because the client can easily spoofed the user-agent information and the user-agent bloats the size of the log file. The following shows some example logs lines from our server...


# Custom log format from the h2o.conf above

format: "%{%Y-%m-%d %H:%M:%S}t.%{msec_frac}t   %s  %h\t%r %b %{ssl.protocol-version}x %{ssl.cipher}x %{x-http2-push}o %{ssl.session-reused}x"


# Example 1: an HTTP/2 compatible client who has never connected before 
# requests an HTML page and gets HTTP/2 server pushed our image.jpg . Notice the
# "pushed" log value at the end of the image.jpg line. 

2030-01-02 11:24:08.226   200  66.249.xx.xx   GET /h2o.html          HTTP/2 12149 TLSv1.3 AES128-GCM - 0
2030-01-02 11:24:08.226   200  66.249.xx.xx   GET /calomel_image.webp HTTP/2 29855 TLSv1.3 AES128-GCM pushed 0


# Example 2: The same client returns an hour later for a different HTML page.
# Notice that since the client returns the secure CASPer cookie we "cancelled" the
# server push of the image.jpg because the client already had a copy. Also, The
# "1" at the end of the log line signifies that the client used ssl session
# resumption when connection which saves the client CPU processing time and at
# least one(1) round trip (RTT) of latency.

2030-01-02 12:14:02.323   200  66.249.xx.xx   GET /calomel_image.webp HTTP/2 29855 TLSv1.3 AES128-GCM cancelled 1
2030-01-02 12:14:02.323   200  66.249.xx.xx   GET /unbound_dns.html  HTTP/2 38940 TLSv1.3 AES128-GCM - 1


# Example 3: an HTTP/2 compatible RSS client returns every hour checking if the
# RSS file has changed (304) and uses ssl session resumption signified by the
# "1" at the end of the log line.


error-log: is the full path to the log file. The error log is standard error out for the daemon. You can also use the path "/dev/stdout" if you want to log directly to the console instead of a log file. /dev/stdout is good to use for initial testing.

expires: is the expires header sent to the client to tell them how long the resource is good for. An example would be the all the object on the moneyslow.com site are cache-able for at least one(1) year.

file.dirlisting: is similar to auto indexing which Apache has. If file.dirlisting is on then the H2o daemon will make a page listing out the files in each directory of the web path.

file.send-gzip: tell h2o to look for gzip pre-compressed files in the web page with the extension ".gz". If those files are found the pre-compressed version of the files will be sent out instead of the uncompressed versions. H2o does not have real compression as the operation would be too slow. Instead, you can pre-compress your HTML files and make then 60% smaller using gzip. The command "gzip -1 index.html" would compress the index.html file with level one(1) compression.

The following script will publish pre-compressed gzip and brotli files from a given html file. When you are done editing the html file execute this script to make a compressed copy ready for distribution. As soon as it is in place, h2o will serve the compressed copy out to the client. The script using gzip level nine(9) which has the best compression ratio to save network bandwidth.

#!/bin/sh
set -euf
#
# moneyslow.com  publish_html2gz.sh
# usage: ./publish_html2gz.sh index.html
#

# Make a tmp copy of the original HTML file
cp $1 $1_l1.tmp

# Remove the old gz if there is one
rm -rf $1.gz

# brotli compressed
/usr/local/bin/brotli-3.6 -f -i $1_l3.tmp -o $1.br

# gzip tmp HTML copy. Use compression and do
# not store dates or file names in the gzip header.
#
# Use level 9 for lower client latency. Decompression
# times are the same 0.003s for all levels.
gzip -9 -n $1_l3.tmp
mv $1_l3.tmp.gz $1.gz

# Clean up tmp files
rm -rf $1_l1.tmp $1_l2.tmp

#printf "\nVerify files\n"
ls -al $1 $1.gz $1.br

limit-request-body: will limit the amount of the request sent from the client to the size listed in bytes. If the value is 1024 then if the client sends more then 1024 bytes total in the request and headers then the server will send the client a HTTP Error 414, Request URI too long.

num-threads: is how many treads h2o will start off off the single h2o daemon process. If the directive is commented out then h2o will start the same amount of threads as the machine has CPU cores, which is the recommended setting. If you have many blocking operations like slow loading of data off a spinning hard dish then you can set the threads higher. Only testing will tell how many threads would be good for your setup. In an ideal situation with perfectly non-blocking I/O, a working set that fits entirely in L1 cache, and no other processes in the physical system, each thread will use the entire resources of a processor core. In such a situation, the ideal number of threads is one thread per logical core which is the default number of threads for h2o.

pid-file: is the process identification file the processes id is stored in. For FreeBSD the default pid path is /var/run/ .

user: is the unprivileged user h2o will switch to when starting up. Make sure the htdocs path is readable by this user as well has the ssl certificate files.

file.mime.addtypes: are additional mime types h2o does not have listed. For example, we have an rss feed which is a ".xml" file and some compressed ".zip" files. H2o did not have the mime type specified for these files. Mime types are sent by the server to the client to inform the client what type of data the file is. The client can then make a decision about what to do with the file, for example, to save the zip file instead of trying to play a zip file in a media player or open the zip in a text editor.

headers.set: allow h2o to insert the user defined headers into the response to the client. H2O provides following directives: header.add, header.append, header.merge, header.set, header.setifempty and header.unset which work just like the Apache Header Directives. The three(3) headers in the example h2o.conf are for our HTTPS security profile. On moneyslow.com we define the rules for HTTP Strict Transport Security, the Content Security Policy and X-Frame-Options.

hosts: is the section to specify the paths to the `web files, like htdocs, of to point to a proxied backend server.

redirect: can be used to send an HTTP status code (e.g. 301, 401, ect.) and redirection URL to the client. In the example above we use redirect to send code 301 and our URL with the https designation back to the client since moneyslow.com is a https only site.

file.dir: is the full path to the htdocs file we tree for h2o to serve to the public.

proxy.reverse.url: is the pointer to a backed server you with h2o to proxy for. The backend server can be listening on the same machine or an a different host.

Starting the H2O daemon

Manual start: Running h2o as a daemon requires executing the binary and passing the configuration file. For example, if the configuration file is in /etc we would execute the following:

/usr/local/bin/h2o -c /usr/local/etc/h2o/h2o.conf

Start using rc.d: The easiest method to start h2o on reboot and manual restart is using a FreeBSD rc.d script. The following script will start h2o in "worker" mode and set the proper pid file location. In your /etc/rc.conf set h2o_enable="YES" to start the deamon on boot. Once the script is in place h2o can be started, stopped and restarted; service h2o (start | stop | restart).

vi /usr/local/etc/rc.d/h2o


#!/bin/sh
#
# PROVIDE: h2o
# REQUIRE: LOGIN DAEMON NETWORKING
# KEYWORD: shutdown

. /etc/rc.subr

name="h2o"
rcvar="h2o_enable"
desc="h2o web server"
h2o_command="/usr/local/bin/${name} -m worker -c /usr/local/etc/${name}/${name}.conf"
pidfile="/var/run/${name}.pid"
command="/usr/sbin/daemon"
command_args="-P ${pidfile} -r -f ${h2o_command}"

load_rc_config $name
: ${h2o_enable:=no}

run_rc_command "$1"

mRuby regular expression match performance

With the introduction of Ruby v2.4, a new more efficient regular expression method was introduced which is almost twice as fast as any other ruby based regex method. H2O v2.2.5 was released with the latest mruby-onig-regexp gem which includes support for the faster regular expression format, "re.match? str".

Using a simple Ruby script we can benchmark each of the Ruby regex to string methods. The script will loop four(4) million times for each method and output the time to completion in seconds. "re.match? str" finished in the shortest amount of time at 0.53 seconds.


Performance of each regular expression method, looped 4 million times

                  user       system     total      real
str.match re      2.320312   0.007812   2.328125   2.314778
re.match str      2.187500   0.000000   2.187500   2.192056
str[re]           1.593750   0.000000   1.593750   1.591088
str =~ re         1.117188   0.000000   1.117188   1.117763
re =~ str         1.109375   0.000000   1.109375   1.111592
re.match? str     0.531250   0.000000   0.531250   0.531187
                                                   ^^^^

################

root@calomel#  cat str_match_test.rb 

#!/usr/bin/env ruby
require 'benchmark'

str = "test.html"
re = Regexp.new('\.html').freeze

N = 4_000_000

Benchmark.bm do |b|
    b.report("str.match re\t") { N.times { str.match re } }
    b.report("re.match str\t") { N.times { re.match str } }
    b.report("str[re]  \t")    { N.times { str[re] } }
    b.report("str =~ re\t")    { N.times { str =~ re } }
    b.report("re =~ str\t")    { N.times { re =~ str } }
    if re.respond_to?(:match?)
        b.report("re.match? str\t") { N.times { re.match? str } }
    end
end

H2O Performance Benchmarks

How fast is the H2O web server? Very fast. Up to 337 thousand https requests per second depending on the cipher used.

A pair of FreeBSD 10 servers are installed to ZFS root on an SSD and networked with Myricom 10 gigabit optical interfaces through an Arista 7050S class switch. Both machines are the same; 6 core AMD FX-6100 CPU, 16 gigabytes of DDR3-1600 ram and AES-NI enabled for the suite of TLS v1.2 ciphers. The client machine runs h2load - HTTP/2 bench-marking tool while the second server runs H2O using the same config file at the top of this page, but with http2 server push disabled. The test file size is 1,280 bytes which fits into one TCP packet (1500 MTU) and contains sudo random data from /dev/random . Each cipher is tested five(5) times, the results are ordered and the median test score reported.


HTTP/2 TLSv1.2 Cipher Performance Summery

  ECDHE-RSA-AES256-GCM-SHA384   325,458 requests per second 
  ECDHE-RSA-AES128-GCM-SHA256   337,751 requests per second
  ECDHE-RSA-AES256-SHA384       195,207 requests per second
  ECDHE-RSA-AES128-SHA256       193,338 requests per second
  ECDHE-RSA-AES256-SHA          328,669 requests per second
  ECDHE-RSA-AES128-SHA          330,985 requests per second  


Testing Notes:

  concurrency : 200 simultaneous clients, 16 HTTP/2 streams per client
  file size   : 1,280 bytes (test.txt)
  network     : 10 gigabit fiber though a switch
  protocol    : TLS v1.2 though HTTP/2
  requests    : 10 million total requests
  threads     : 6 threads to use all 6 cpu cores on the client and server


The raw h2load test results:


./h2load -t 6 -m 16 -c 200 -n 10000000 -v https://192.168.10.10/test.txt
starting benchmark...
Protocol: TLSv1.2
Cipher: ECDHE-RSA-AES256-GCM-SHA384
finished in 30.73s, 325458 req/s, 405.05MB/s
requests: 10000000 total, 10000000 started, 10000000 done, 10000000 succeeded, 0 failed, 0 errored
status codes: 10000000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 13050151643 bytes total, 70144443 bytes headers, 12800000000 bytes data
                     min         max         mean         sd        +/- sd
time for request:      284us     43.54ms      8.44ms      3.56ms    69.09%


./h2load -t 6 -m 16 -c 200 -n 10000000 -v https://192.168.10.10/test.txt
starting benchmark...
Protocol: TLSv1.2
Cipher: ECDHE-RSA-AES128-GCM-SHA256
finished in 29.61s, 337751 req/s, 420.35MB/s
requests: 10000000 total, 10000000 started, 10000000 done, 10000000 succeeded, 0 failed, 0 errored
status codes: 10000000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 13050150404 bytes total, 70143204 bytes headers, 12800000000 bytes data
                     min         max         mean         sd        +/- sd
time for request:      181us     43.27ms      8.24ms      4.01ms    68.45%


./h2load -t 6 -m 16 -c 200 -n 10000000 -v https://192.168.10.10/test.txt
starting benchmark...
Protocol: TLSv1.2
Cipher: ECDHE-RSA-AES256-SHA384
finished in 51.23s, 195207 req/s, 242.95MB/s
requests: 10000000 total, 10000000 started, 10000000 done, 10000000 succeeded, 0 failed, 0 errored
status codes: 10000000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 13050277896 bytes total, 70270696 bytes headers, 12800000000 bytes data
                     min         max         mean         sd        +/- sd
time for request:      533us     70.00ms     16.03ms      1.47ms    79.86%


./h2load -t 6 -m 16 -c 200 -n 10000000 -v https://192.168.10.10/test.txt
starting benchmark...
Protocol: TLSv1.2
Cipher: ECDHE-RSA-AES128-SHA256
finished in 51.72s, 193338 req/s, 240.62MB/s
requests: 10000000 total, 10000000 started, 10000000 done, 10000000 succeeded, 0 failed, 0 errored
status codes: 10000000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 13050277176 bytes total, 70269976 bytes headers, 12800000000 bytes data
                     min         max         mean         sd        +/- sd
time for request:      469us     38.75ms     16.03ms      1.32ms    78.20%


./h2load -t 6 -m 16 -c 200 -n 10000000 -v https://192.168.10.10/test.txt
starting benchmark...
Protocol: TLSv1.2
Cipher: ECDHE-RSA-AES256-SHA
finished in 30.43s, 328669 req/s, 409.05MB/s
requests: 10000000 total, 10000000 started, 10000000 done, 10000000 succeeded, 0 failed, 0 errored
status codes: 10000000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 13050177049 bytes total, 70169849 bytes headers, 12800000000 bytes data
                     min         max         mean         sd        +/- sd
time for request:      382us     37.87ms      9.33ms      1.71ms    76.56%


./h2load -t 6 -m 16 -c 200 -n 10000000 -v https://192.168.10.10/test.txt
starting benchmark...
Protocol: TLSv1.2
Cipher: ECDHE-RSA-AES128-SHA
finished in 30.21s, 330985 req/s, 411.93MB/s
requests: 10000000 total, 10000000 started, 10000000 done, 10000000 succeeded, 0 failed, 0 errored
status codes: 10000000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 13050174786 bytes total, 70167586 bytes headers, 12800000000 bytes data
                     min         max         mean         sd        +/- sd
time for request:      247us     93.57ms      9.32ms      1.42ms    78.88%

Our web site, moneyslow.com, is an SSL only domain which redirects all clear text HTTP requests to the encrypted HTTPS server. The following is h2load performance testing the 301 redirection response speed of the H2O server.

h2load - 301 redirect performance benchmark 

  301 redirect - 1,107,264 requests per second

./h2load -t 6 -m 16 -c 200 -n 10000000 -v http://192.168.10.10/test.txt
starting benchmark...
finished in 9.03s, 1107264 req/s, 27.46MB/s
requests: 10000000 total, 10000000 started, 10000000 done, 10000000 succeeded, 0 failed, 0 errored
status codes: 0 2xx, 10000000 3xx, 0 4xx, 0 5xx
traffic: 260054618 bytes total, 80047418 bytes headers, 0 bytes data
                     min         max         mean         sd        +/- sd
time for request:      113us     24.25ms      2.36ms      1.33ms    77.51%

Questions?

How to make an ECDSA or RSA SSL certificate for https access ?

The steps to create H2O certificate and key files are exactly the same as for an Nginx web server. We have a complete tutorial on our Nginx page. Search for the section titled, "Setting up an ECDSA or RSA SSL certificate for https access" in our Nginx tutorial.

How can I test server ciphers ?

Use the OpenSSL or LibreSSL s_client client. Remember that both the client and server need to have the cipher you want to test. If you are looking to test the CHACHA20-POLY1305 cipher you need to have OpenSSL 1.0.2 or LibreSSL on both the server and client.

# check your version of openssl
$ openssl version
  LibreSSL 2.1.4

# test moneyslow.com for the most preferred cipher
$ echo -n | openssl s_client -connect moneyslow.com:443
  ...
  Cipher    : ECDHE-ECDSA-AES256-GCM-SHA384

# test moneyslow.com for the CHACHA20-POLY1305 cipher
$ echo -n | openssl s_client -cipher ECDHE-ECDSA-CHACHA20-POLY1305 -connect moneyslow.com:443
  ...
  Cipher    : ECDHE-ECDSA-CHACHA20-POLY1305