1
0
Quellcode durchsuchen

Import upstream version 15

Sergio Arroutbi vor 1 Jahr
Ursprung
Commit
c7d2cb234e

+ 12 - 7
.github/workflows/build.yml

@@ -1,7 +1,13 @@
 ---
 ---
 name: build
 name: build
 
 
-on: [push, pull_request]
+on:
+  push:
+    paths-ignore:
+      - '**.md'
+  pull_request:
+    paths-ignore:
+      - '**.md'
 
 
 jobs:
 jobs:
   build:
   build:
@@ -11,17 +17,15 @@ jobs:
       matrix:
       matrix:
         os:
         os:
           - fedora:latest
           - fedora:latest
-          - centos:7
           - quay.io/centos/centos:stream8
           - quay.io/centos/centos:stream8
           - quay.io/centos/centos:stream9
           - quay.io/centos/centos:stream9
           - debian:testing
           - debian:testing
           - debian:latest
           - debian:latest
+          - ubuntu:latest
           - ubuntu:rolling
           - ubuntu:rolling
           - ubuntu:lunar
           - ubuntu:lunar
           - ubuntu:jammy
           - ubuntu:jammy
           - ubuntu:focal
           - ubuntu:focal
-          - ubuntu:bionic
-          - ubuntu:kinetic
         stable: [true]
         stable: [true]
         include:
         include:
           - os: quay.io/fedora/fedora:rawhide
           - os: quay.io/fedora/fedora:rawhide
@@ -29,10 +33,11 @@ jobs:
           - os: ubuntu:devel
           - os: ubuntu:devel
             stable: false
             stable: false
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
 
       - name: Show OS information
       - name: Show OS information
-        run: cat /etc/os-release 2>/dev/null || echo /etc/os-release not available
+        run: cat /etc/os-release 2>/dev/null || \
+          echo /etc/os-release not available
 
 
       - name: Install build dependencies
       - name: Install build dependencies
         run: bash .github/workflows/install-dependencies
         run: bash .github/workflows/install-dependencies
@@ -42,7 +47,7 @@ jobs:
           mkdir -p build && cd build
           mkdir -p build && cd build
           export ninja=$(command -v ninja)
           export ninja=$(command -v ninja)
           [ -z "${ninja}" ] && export ninja=$(command -v ninja-build)
           [ -z "${ninja}" ] && export ninja=$(command -v ninja-build)
-          meson .. || cat meson-logs/meson-log.txt >&2
+          meson setup .. || cat meson-logs/meson-log.txt >&2
           ${ninja}
           ${ninja}
 
 
       - name: Run tests
       - name: Run tests

+ 9 - 3
.github/workflows/coverage.yml

@@ -1,7 +1,13 @@
 ---
 ---
 name: coverage
 name: coverage
 
 
-on: [push, pull_request]
+on:
+  push:
+    paths-ignore:
+      - '**.md'
+  pull_request:
+    paths-ignore:
+      - '**.md'
 
 
 jobs:
 jobs:
   build:
   build:
@@ -11,7 +17,7 @@ jobs:
         os:
         os:
           - ubuntu:latest
           - ubuntu:latest
     steps:
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
 
 
       - name: Show OS information
       - name: Show OS information
         run: cat /etc/os-release 2>/dev/null || echo /etc/os-release not available
         run: cat /etc/os-release 2>/dev/null || echo /etc/os-release not available
@@ -47,7 +53,7 @@ jobs:
           [ -z "${ninja}" ] && export ninja=$(command -v ninja-build)
           [ -z "${ninja}" ] && export ninja=$(command -v ninja-build)
           gcovr -r .. -f ../src -f src/ -e ../tests -e tests -x coverage.xml
           gcovr -r .. -f ../src -f src/ -e ../tests -e tests -x coverage.xml
 
 
-      - uses: codecov/codecov-action@v2
+      - uses: codecov/codecov-action@v3
         with:
         with:
           file: build/coverage.xml
           file: build/coverage.xml
           fail_ci_if_error: true # optional (default = false)
           fail_ci_if_error: true # optional (default = false)

+ 5 - 5
.github/workflows/install-dependencies

@@ -6,24 +6,24 @@ debian:*|ubuntu:*)
     apt clean
     apt clean
     apt update
     apt update
     apt -y install gcc meson pkg-config libjose-dev jose libhttp-parser-dev \
     apt -y install gcc meson pkg-config libjose-dev jose libhttp-parser-dev \
-                   systemd gcovr curl socat iproute2
+                   systemd gcovr curl socat iproute2 asciidoc
     ;;
     ;;
 
 
 *fedora:*)
 *fedora:*)
     echo 'max_parallel_downloads=10' >> /etc/dnf/dnf.conf
     echo 'max_parallel_downloads=10' >> /etc/dnf/dnf.conf
     dnf -y clean all
     dnf -y clean all
     dnf -y --setopt=deltarpm=0 update
     dnf -y --setopt=deltarpm=0 update
-    dnf -y install gcc meson pkgconfig libjose-devel jose http-parser-devel \
-                   systemd gcovr curl socat iproute
+    dnf -y install gcc meson pkgconfig libjose-devel jose llhttp-devel \
+                   systemd gcovr curl socat iproute asciidoc
     ;;
     ;;
 
 
-centos:*)
+centos:7)
     yum -y clean all
     yum -y clean all
     yum -y --setopt=deltarpm=0 update
     yum -y --setopt=deltarpm=0 update
     yum install -y yum-utils epel-release
     yum install -y yum-utils epel-release
     yum config-manager -y --set-enabled PowerTools \
     yum config-manager -y --set-enabled PowerTools \
         || yum config-manager -y --set-enabled powertools || :
         || yum config-manager -y --set-enabled powertools || :
-    yum -y install meson socat iproute
+    yum -y install meson socat iproute asciidoc
     yum-builddep -y tang
     yum-builddep -y tang
     ;;
     ;;
 
 

+ 51 - 27
README.md

@@ -60,11 +60,15 @@ identifying information from the client.
 ## Getting Started
 ## Getting Started
 ### Dependencies
 ### Dependencies
 
 
-Tang requires a few other software libraries:
+Tang requires two other software libraries:
 
 
-1. http-parser >= 2.8.0 - https://github.com/nodejs/http-parser
-2. systemd - https://github.com/systemd/systemd
-3. jose >= 8 - https://github.com/latchset/jose
+1. jose >= 8 - https://github.com/latchset/jose
+2. Either:
+   - llhttp - https://github.com/nodejs/llhttp
+   - http_parser >= 2.8 - https://github.com/nodejs/http-parser
+
+http_parser is unmaintained, but llhttp is not availalbe in all
+distributions - notably Debian and CentOS.
 
 
 #### Fedora
 #### Fedora
 
 
@@ -76,44 +80,49 @@ additional settings (such as SETGID directories) out of the box. To install it:
 If you really want to build from source on Fedora, you will need the following
 If you really want to build from source on Fedora, you will need the following
 packages:
 packages:
 
 
-1. http-parser - ``http-parser-devel``
-2. systemd - ``systemd``
+1. llhttp - ``llhttp-devel``
+2. systemd - ``systemd`` (desirable but not strictly required)
 3. jose - ``jose``, ``libjose-devel``
 3. jose - ``jose``, ``libjose-devel``
-4. curl - curl (only needed for running tests)
+4. curl - ``curl`` (only needed for running tests)
+5. socat - ``socat`` (only needed for running tests)
 
 
 #### OpenWrt
 #### OpenWrt
 
 
 Tang is also capable of running on devices without systemd even for example
 Tang is also capable of running on devices without systemd even for example
 OpenWrt (see: [this PR](https://github.com/openwrt/packages/pull/5447)).
 OpenWrt (see: [this PR](https://github.com/openwrt/packages/pull/5447)).
 Instead of using systemd for socket activation you can use another daemon for
 Instead of using systemd for socket activation you can use another daemon for
-spawning services like xinetd.
+spawning services like xinetd. As of version 12 tang can also be run as a
+standalone server without a separate socket listener.
 
 
 An example of configuration file for Tang using xinetd can be found in the
 An example of configuration file for Tang using xinetd can be found in the
 `units/` directory as 'tangdx'.  Using that will also require installing the
 `units/` directory as 'tangdx'.  Using that will also require installing the
 wrapper from the 'units/' directroy 'tangdw' in '/usr/libexec/tangdw'.
 wrapper from the 'units/' directroy 'tangdw' in '/usr/libexec/tangdw'.
 
 
-#### FreeBSD, HardenedBSD and OPNsense
+#### FreeBSD
 
 
-Tang is also capable of running on FreeBSD Unix variants. The build is simple
-and differs only sligtly from the general instructions.
+Tang is also capable of running on FreeBSD Unix variants. It is available in
+the ports tree and package system.  As root you can install it with:
 
 
-    (as root) # pkg install jose git meson pkgconf jansson openssl asciidoc http-parser socat
-    $ mkdir build && cd build
-    $ meson .. --prefix=/usr/local --localstatedir=/usr/local/var
-    $ ninja
-    (as root) # ninja install
-    (as root) # mkdir -m 0700 /usr/local/var/db/tang
-    (as root) service tangd enable
-    (as root) service tangd start
+    # pkg install tang
+    # service tangd enable
+    # service tangd start
 
 
-Once built it does not require the many packages above, but still requires
-jose, socat and http_parser. 
+#### OPNsense
+
+Tang can be installed on OPNsense by enabling the FreeBSD package repositories
+and then installing. There are some extra steps to minimize the installation.
 
 
-FreeBSD, HardendedBSD, and OPNsense use inetd rather than systemd or
-xinetd. To limit the need to manage inetd configuration which has a shared
-config file, tangd is instead packaged to depend on `socat`.  Of course,
-if desired it may be configured to run instead from inetd.conf in which case
-the socat package will no longer be required.
+As root enable the FreeBSD repository, download tang, jose, and llhttp.
+Then disable the FreeBSD repository to prevent installing extraneous
+dependencies not needed by tang. And finally install the downloaded packages
+and start the server:
+
+    # vi /usr/local/etc/pkg/repos/FreeBSD.conf (set enabled to yes)
+    # pkg download tang jose llhttp
+    # vi /usr/local/etc/pkg/repos/FreeBSD.conf (set enabled back to no)
+    # pkg install /var/cache/pkg/tang-*.pkg /var/cache/pkg/jose-*.pkg /var/cache/pkg/llhttp-*.pkg
+    # service tangd enable
+    # service tangd start
 
 
 #### Docker Container
 #### Docker Container
 
 
@@ -129,7 +138,7 @@ protect.
 Building Tang is fairly straightforward:
 Building Tang is fairly straightforward:
 
 
     $ mkdir build && cd build
     $ mkdir build && cd build
-    $ meson .. --prefix=/usr
+    $ meson setup .. --prefix=/usr
     $ ninja
     $ ninja
     $ sudo ninja install
     $ sudo ninja install
 
 
@@ -137,6 +146,21 @@ You can even run the tests if you'd like:
 
 
     $ meson test
     $ meson test
 
 
+#### FreeBSD
+
+The build is simple and differs only sligtly from the general instructions.
+
+    (as root) # pkg install jose git meson pkgconf jansson asciidoc llhttp socat
+    $ mkdir build && cd build
+    $ meson setup .. --prefix=/usr/local
+    $ ninja
+    $ meson test # if you want to run the tests
+    (as root) # ninja install
+    (as root) # mkdir -m 0700 /var/db/tang
+
+Once built it does not require the many packages above, but still requires
+jose and llhttp. 
+
 ### Server Enablement
 ### Server Enablement
 
 
 Once installed, starting a Tang server is simple:
 Once installed, starting a Tang server is simple:

+ 21 - 0
doc/tang.8.adoc

@@ -71,6 +71,27 @@ be changed with the *-p* option.
 
 
     tang -l -p 9090
     tang -l -p 9090
 
 
+== ENDPOINT
+
+The Tang server can be provided an endpoint. This endpoint will act as a prefix
+for the URL to be accessed by the client. This endpoint can be specified with
+the *-e* option.
+
+    tang -l -p 9090 -e this/is/an/endpoint
+
+When endpoint is specified, the endpoint will be prepended to the normal adv/rec
+URL. If no endpoint is provided, and assuming port 9090 is used, Tang server
+will listen on next URLs:
+
+    http://localhost:9090/adv (GET)
+    http://localhost:9090/rec (POST)
+
+If endpoint is provided, and assuming endpoint is /this/is/an/endpoint/, and
+assuming also port 9090 is used, Tang server will listen on next URLs:
+
+    http://localhost:9090/this/is/an/endpoint/adv (GET)
+    http://localhost:9090/this/is/an/endpoint/rec (POST)
+
 == KEY ROTATION
 == KEY ROTATION
 
 
 In order to preserve the security of the system over the long run, you need to
 In order to preserve the security of the system over the long run, you need to

+ 14 - 5
meson.build

@@ -1,5 +1,5 @@
 project('tang', 'c',
 project('tang', 'c',
-  version: '14',
+  version: '15',
   license: 'GPL3+',
   license: 'GPL3+',
   default_options: [
   default_options: [
     'c_std=c99',
     'c_std=c99',
@@ -55,13 +55,22 @@ add_project_arguments('-DVERSION="'+meson.project_version() + '"', language : 'c
 jose = dependency('jose', version: '>=8')
 jose = dependency('jose', version: '>=8')
 a2x = find_program('a2x', required: false)
 a2x = find_program('a2x', required: false)
 compiler = meson.get_compiler('c')
 compiler = meson.get_compiler('c')
-if not compiler.has_header('http_parser.h',args : '-I/usr/local/include')
-  error('http-parser devel files not found.')
+
+http_lib = []
+if compiler.has_header('llhttp.h', args: '-I/usr/local/include')
+  http_lib = 'llhttp'
+  add_project_arguments('-DUSE_LLHTTP', language: 'c')
+else
+  if not compiler.has_header('http_parser.h', args: '-I/usr/local/include')
+    error('neither llhttp nor http-parser devel files found.')
+  endif
+  http_lib = 'http_parser'
 endif
 endif
+
 if host_machine.system() == 'freebsd'
 if host_machine.system() == 'freebsd'
-  http_parser = compiler.find_library('http_parser',dirs : '/usr/local/lib')
+  http_parser = compiler.find_library(http_lib, dirs : '/usr/local/lib')
 else
 else
-  http_parser = compiler.find_library('http_parser')
+  http_parser = compiler.find_library(http_lib)
 endif
 endif
 
 
 licenses = ['COPYING']
 licenses = ['COPYING']

+ 5 - 5
src/http.c

@@ -36,7 +36,7 @@ HTTP_METHOD_MAP(XX)
 };
 };
 
 
 static int
 static int
-on_url(http_parser *parser, const char *at, size_t length)
+on_url(http_parser_t *parser, const char *at, size_t length)
 {
 {
     struct http_state *state = parser->data;
     struct http_state *state = parser->data;
 
 
@@ -51,7 +51,7 @@ on_url(http_parser *parser, const char *at, size_t length)
 }
 }
 
 
 static int
 static int
-on_body(http_parser *parser, const char *at, size_t length)
+on_body(http_parser_t *parser, const char *at, size_t length)
 {
 {
     struct http_state *state = parser->data;
     struct http_state *state = parser->data;
 
 
@@ -66,7 +66,7 @@ on_body(http_parser *parser, const char *at, size_t length)
 }
 }
 
 
 static int
 static int
-on_message_complete(http_parser *parser)
+on_message_complete(http_parser_t *parser)
 {
 {
     struct http_state *state = parser->data;
     struct http_state *state = parser->data;
     const char *addr = NULL;
     const char *addr = NULL;
@@ -132,7 +132,7 @@ egress:
     return 0;
     return 0;
 }
 }
 
 
-const http_parser_settings http_settings = {
+const http_settings_t http_settings = {
     .on_url = on_url,
     .on_url = on_url,
     .on_body = on_body,
     .on_body = on_body,
     .on_message_complete = on_message_complete,
     .on_message_complete = on_message_complete,
@@ -140,7 +140,7 @@ const http_parser_settings http_settings = {
 
 
 int
 int
 http_reply(const char *file, int line,
 http_reply(const char *file, int line,
-           enum http_status code, const char *fmt, ...)
+           http_status_t code, const char *fmt, ...)
 {
 {
     const char *msg = NULL;
     const char *msg = NULL;
     va_list ap;
     va_list ap;

+ 29 - 4
src/http.h

@@ -19,12 +19,37 @@
 
 
 #pragma once
 #pragma once
 
 
-#include <http_parser.h>
 #include <sys/types.h>
 #include <sys/types.h>
 #include <regex.h>
 #include <regex.h>
 
 
+#ifdef USE_LLHTTP
+#include <llhttp.h>
+
+typedef llhttp_method_t http_method_t;
+typedef llhttp_status_t http_status_t;
+typedef llhttp_settings_t http_settings_t;
+typedef llhttp_t http_parser_t;
+#define tang_http_parser_init(parser, settings) llhttp_init(parser, HTTP_REQUEST, settings)
+#define tang_http_parser_errno(parser) parser.error
+#define tang_http_errno_description(parser, errno) llhttp_get_error_reason(parser)
+#define tang_http_parser_resume(parser) llhttp_resume(parser)
+#else
+/* Legacy http-parser. */
+#include <http_parser.h>
+
+typedef enum http_method http_method_t;
+typedef enum http_status http_status_t;
+typedef http_parser_settings http_settings_t;
+typedef struct http_parser http_parser_t;
+
+#define tang_http_parser_init(parser, settings) http_parser_init(parser, HTTP_REQUEST)
+#define tang_http_parser_errno(parser) parser.http_errno
+#define tang_http_errno_description(parser, errno) http_errno_description(errno)
+#define tang_http_parser_resume(parser) http_parser_pause(parser, 0)
+#endif /* USE_LLHTTP */
+
 struct http_dispatch {
 struct http_dispatch {
-    int (*func)(enum http_method method, const char *path,
+    int (*func)(http_method_t method, const char *path,
                 const char *body, regmatch_t matches[], void *misc);
                 const char *body, regmatch_t matches[], void *misc);
     uint64_t methods;
     uint64_t methods;
     size_t nmatches;
     size_t nmatches;
@@ -43,11 +68,11 @@ struct http_state {
     void *misc;
     void *misc;
 };
 };
 
 
-extern const http_parser_settings http_settings;
+extern const http_settings_t http_settings;
 
 
 int __attribute__ ((format(printf, 4, 5)))
 int __attribute__ ((format(printf, 4, 5)))
 http_reply(const char *file, int line,
 http_reply(const char *file, int line,
-           enum http_status code, const char *fmt, ...);
+           http_status_t code, const char *fmt, ...);
 
 
 #define http_reply(code, ...) \
 #define http_reply(code, ...) \
     http_reply(__FILE__, __LINE__, code, __VA_ARGS__)
     http_reply(__FILE__, __LINE__, code, __VA_ARGS__)

+ 1 - 2
src/socket.c

@@ -178,10 +178,9 @@ static void spawn_process(int fd, const char *jwkdir,
 
 
 static void handle_child(int sig)
 static void handle_child(int sig)
 {
 {
-	pid_t pid;
 	int status;
 	int status;
 
 
-	while ((pid = waitpid(-1, &status, WNOHANG)) > 0);
+	while ((waitpid(-1, &status, WNOHANG)) > 0);
 }
 }
 
 
 int run_service(const char *jwkdir, int port, process_request_func pfunc)
 int run_service(const char *jwkdir, int port, process_request_func pfunc)

+ 12 - 3
src/tang-show-keys

@@ -20,14 +20,23 @@
 
 
 set -e
 set -e
 
 
-if [ $# -gt 1 ]; then
-    echo "Usage: $0 [<port>]" >&2
+if [ $# -gt 2 ]; then
+    echo "Usage: $0 [<port>] [<endpoint>]" >&2
     exit 1
     exit 1
 fi
 fi
 
 
 port=${1-80}
 port=${1-80}
 
 
-adv=$(curl -sSf "localhost:$port/adv")
+if test -n "$2"; then
+  first_letter=$(printf %.1s "$2")
+  if [ "${first_letter}" = "/" ]; then
+      adv=$(curl -sSf "localhost:$port$2/adv")
+  else
+      adv=$(curl -sSf "localhost:$port/$2/adv")
+  fi
+else
+  adv=$(curl -sSf "localhost:$port/adv")
+fi
 
 
 THP_DEFAULT_HASH=S256    # SHA-256.
 THP_DEFAULT_HASH=S256    # SHA-256.
 jose fmt --json "${adv}" -g payload -y -o- \
 jose fmt --json "${adv}" -g payload -y -o- \

+ 80 - 13
src/tangd.c

@@ -32,8 +32,11 @@
 #include "keys.h"
 #include "keys.h"
 #include "socket.h"
 #include "socket.h"
 
 
+#define MAX_URL 256
+
 static const struct option long_options[] = {
 static const struct option long_options[] = {
 	{"port", 1, 0, 'p'},
 	{"port", 1, 0, 'p'},
+	{"endpoint", 1, 0, 'e'},
 	{"listen", 0, 0, 'l'},
 	{"listen", 0, 0, 'l'},
 	{"version", 0, 0, 'v'},
 	{"version", 0, 0, 'v'},
 	{"help", 0, 0, 'h'},
 	{"help", 0, 0, 'h'},
@@ -45,6 +48,7 @@ print_help(const char *name)
 {
 {
 	fprintf(stderr, "Usage: %s [OPTIONS] <jwkdir>\n", name);
 	fprintf(stderr, "Usage: %s [OPTIONS] <jwkdir>\n", name);
 	fprintf(stderr, "  -p, --port=PORT                 Specify the port to listen (default 9090)\n");
 	fprintf(stderr, "  -p, --port=PORT                 Specify the port to listen (default 9090)\n");
+	fprintf(stderr, "  -e, --endpoint=ENDPOINT         Specify endpoint to listen (empty by default)\n");
 	fprintf(stderr, "  -l, --listen                    Run as a service and wait for connections\n");
 	fprintf(stderr, "  -l, --listen                    Run as a service and wait for connections\n");
 	fprintf(stderr, "  -v, --version                   Display program version\n");
 	fprintf(stderr, "  -v, --version                   Display program version\n");
 	fprintf(stderr, "  -h, --help                      Show this help message\n");
 	fprintf(stderr, "  -h, --help                      Show this help message\n");
@@ -64,7 +68,7 @@ str_cleanup(char **str)
 }
 }
 
 
 static int
 static int
-adv(enum http_method method, const char *path, const char *body,
+adv(http_method_t method, const char *path, const char *body,
     regmatch_t matches[], void *misc)
     regmatch_t matches[], void *misc)
 {
 {
     __attribute__((cleanup(str_cleanup))) char *adv = NULL;
     __attribute__((cleanup(str_cleanup))) char *adv = NULL;
@@ -101,7 +105,7 @@ adv(enum http_method method, const char *path, const char *body,
 }
 }
 
 
 static int
 static int
-rec(enum http_method method, const char *path, const char *body,
+rec(http_method_t method, const char *path, const char *body,
     regmatch_t matches[], void *misc)
     regmatch_t matches[], void *misc)
 {
 {
     __attribute__((cleanup(str_cleanup))) char *enc = NULL;
     __attribute__((cleanup(str_cleanup))) char *enc = NULL;
@@ -184,7 +188,7 @@ rec(enum http_method method, const char *path, const char *body,
                       "\r\n%s", strlen(enc), enc);
                       "\r\n%s", strlen(enc), enc);
 }
 }
 
 
-static struct http_dispatch dispatch[] = {
+static struct http_dispatch s_dispatch[] = {
     { adv, 1 << HTTP_GET,  2, "^/+adv/+([0-9A-Za-z_-]+)$" },
     { adv, 1 << HTTP_GET,  2, "^/+adv/+([0-9A-Za-z_-]+)$" },
     { adv, 1 << HTTP_GET,  2, "^/+adv/*$" },
     { adv, 1 << HTTP_GET,  2, "^/+adv/*$" },
     { rec, 1 << HTTP_POST, 2, "^/+rec/+([0-9A-Za-z_-]+)$" },
     { rec, 1 << HTTP_POST, 2, "^/+rec/+([0-9A-Za-z_-]+)$" },
@@ -193,17 +197,56 @@ static struct http_dispatch dispatch[] = {
 
 
 #define DEFAULT_PORT 9090
 #define DEFAULT_PORT 9090
 
 
+static size_t
+tang_http_parser_execute(http_parser_t *parser, const char* data, size_t len)
+{
+#ifdef USE_LLHTTP
+    llhttp_errno_t error;
+    size_t parsed_len;
+
+    /*
+     * Unlike http_parser, which returns the number of parsed
+     * bytes in the _execute() call, llhttp returns an error
+     * code.
+     */
+
+    if (data == NULL || len == 0) {
+        error = llhttp_finish(parser);
+    } else {
+        error = llhttp_execute(parser, data, len);
+    }
+
+    parsed_len = len;
+    /*
+     * Adjust number of parsed bytes in case of error.
+     */
+    if (error != HPE_OK) {
+        parsed_len = llhttp_get_error_pos(parser) - data;
+
+        /* This isn't a real pause, just a way to stop parsing early. */
+        if (error == HPE_PAUSED_UPGRADE) {
+            llhttp_resume_after_upgrade(parser);
+        }
+    }
+
+    return parsed_len;
+#else
+    return http_parser_execute(parser, &http_settings, data, len);
+#endif
+}
+
 static int
 static int
 process_request(const char *jwkdir, int in_fileno)
 process_request(const char *jwkdir, int in_fileno)
 {
 {
-    struct http_state state = { .dispatch = dispatch, .misc = (char*)jwkdir };
-    struct http_parser parser = { .data = &state };
+    struct http_state state = { .dispatch = s_dispatch, .misc = (char*)jwkdir };
+    http_parser_t parser;
     struct stat st = {};
     struct stat st = {};
     char req[4096] = {};
     char req[4096] = {};
     size_t rcvd = 0;
     size_t rcvd = 0;
     int r = 0;
     int r = 0;
 
 
-    http_parser_init(&parser, HTTP_REQUEST);
+    tang_http_parser_init(&parser, &http_settings);
+    parser.data = &state;
 
 
     if (stat(jwkdir, &st) != 0) {
     if (stat(jwkdir, &st) != 0) {
         fprintf(stderr, "Error calling stat() on path: %s: %m\n", jwkdir);
         fprintf(stderr, "Error calling stat() on path: %s: %m\n", jwkdir);
@@ -224,17 +267,22 @@ process_request(const char *jwkdir, int in_fileno)
 
 
         rcvd += r;
         rcvd += r;
 
 
-        r = http_parser_execute(&parser, &http_settings, req, rcvd);
-        if (parser.http_errno != 0) {
+        r = tang_http_parser_execute(&parser, req, rcvd);
+        switch (tang_http_parser_errno(parser)) {
+        case HPE_OK:
+            break;
+        case HPE_PAUSED:
+            tang_http_parser_resume(&parser);
+            break;
+        default:
             fprintf(stderr, "HTTP Parsing Error: %s\n",
             fprintf(stderr, "HTTP Parsing Error: %s\n",
-                    http_errno_description(parser.http_errno));
+                    tang_http_errno_description(&parser, tang_http_parser_errno(parser)));
             return EXIT_SUCCESS;
             return EXIT_SUCCESS;
         }
         }
 
 
         memmove(req, &req[r], rcvd - r);
         memmove(req, &req[r], rcvd - r);
         rcvd -= r;
         rcvd -= r;
     }
     }
-
     return EXIT_SUCCESS;
     return EXIT_SUCCESS;
 }
 }
 
 
@@ -244,9 +292,10 @@ main(int argc, char *argv[])
     int listen = 0;
     int listen = 0;
     int port = DEFAULT_PORT;
     int port = DEFAULT_PORT;
     const char *jwkdir = NULL;
     const char *jwkdir = NULL;
+    const char *endpoint = NULL;
 
 
     while (1) {
     while (1) {
-	int c = getopt_long(argc, argv, "lp:vh", long_options, NULL);
+	int c = getopt_long(argc, argv, "lp:e:vh", long_options, NULL);
 	if (c == -1)
 	if (c == -1)
             break;
             break;
 
 
@@ -260,6 +309,9 @@ main(int argc, char *argv[])
 	    case 'p':
 	    case 'p':
 		port = atoi(optarg);
 		port = atoi(optarg);
 		break;
 		break;
+	    case 'e':
+		endpoint = optarg;
+		break;
 	    case 'l':
 	    case 'l':
 		listen = 1;
 		listen = 1;
 		break;
 		break;
@@ -272,9 +324,24 @@ main(int argc, char *argv[])
     }
     }
     jwkdir = argv[optind++];
     jwkdir = argv[optind++];
 
 
+    char adv_thp_endpoint[MAX_URL] = {};
+    char adv_endpoint[MAX_URL] = {};
+    char rec_endpoint[MAX_URL] = {};
+    if (endpoint != NULL) {
+        char *endpoint_ptr = (char*)endpoint;
+        while (*endpoint_ptr == '/') {
+            endpoint_ptr++;
+        }
+        snprintf(adv_thp_endpoint, MAX_URL, "^/%s/+adv/+([0-9A-Za-z_-]+)$", endpoint_ptr);
+        snprintf(adv_endpoint, MAX_URL, "^/%s/+adv/*$", endpoint_ptr);
+        snprintf(rec_endpoint, MAX_URL, "^/%s/+rec/+([0-9A-Za-z_-]+)$", endpoint_ptr);
+        s_dispatch[0].re = adv_thp_endpoint;
+        s_dispatch[1].re = adv_endpoint;
+        s_dispatch[2].re = rec_endpoint;
+    }
     if (listen == 0) { /* process one-shot query from stdin */
     if (listen == 0) { /* process one-shot query from stdin */
-	return process_request(jwkdir, STDIN_FILENO);
+        return process_request(jwkdir, STDIN_FILENO);
     } else { /* listen and process all incoming connections */
     } else { /* listen and process all incoming connections */
-	return run_service(jwkdir, port, process_request);
+        return run_service(jwkdir, port, process_request);
     }
     }
 }
 }

+ 23 - 23
tests/adv

@@ -36,47 +36,47 @@ adv_startup () {
 
 
 adv_second_phase () {
 adv_second_phase () {
     # Make sure requests on the root fail
     # Make sure requests on the root fail
-    fetch / && expected_fail
+    fetch "${ENDPOINT}"/ && expected_fail
 
 
     # The request should fail (404) for non-signature key IDs
     # The request should fail (404) for non-signature key IDs
-    fetch /adv/`jose jwk thp -i $TMP/db/exc.jwk` && expected_fail
-    fetch /adv/`jose jwk thp -a S512 -i $TMP/db/exc.jwk` && expected_fail
+    fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/exc.jwk` && expected_fail
+    fetch "${ENDPOINT}"/adv/`jose jwk thp -a S512 -i $TMP/db/exc.jwk` && expected_fail
 
 
     # The default advertisement fetch should succeed and pass verification
     # The default advertisement fetch should succeed and pass verification
-    fetch /adv
-    fetch /adv | ver $TMP/db/sig.jwk
-    fetch /adv/ | ver $TMP/db/sig.jwk
+    fetch "${ENDPOINT}"/adv
+    fetch "${ENDPOINT}"/adv | ver $TMP/db/sig.jwk
+    fetch "${ENDPOINT}"/adv/ | ver $TMP/db/sig.jwk
 
 
     # Fetching by any thumbprint should work
     # Fetching by any thumbprint should work
-    fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk
-    fetch /adv/`jose jwk thp -a S512 -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk
+    fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk
+    fetch "${ENDPOINT}"/adv/`jose jwk thp -a S512 -i $TMP/db/sig.jwk` | ver $TMP/db/sig.jwk
 
 
     # Requesting an adv by an advertised key ID should't be signed by hidden keys
     # Requesting an adv by an advertised key ID should't be signed by hidden keys
-    fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.sig.jwk && expected_fail
-    fetch /adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail
+    fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.sig.jwk && expected_fail
+    fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail
 
 
     # Verify that the default advertisement is not signed with hidden signature keys
     # Verify that the default advertisement is not signed with hidden signature keys
-    fetch /adv/ | ver $TMP/db/.oth.jwk && expected_fail
-    fetch /adv/ | ver $TMP/db/.sig.jwk && expected_fail
+    fetch "${ENDPOINT}"/adv/ | ver $TMP/db/.oth.jwk && expected_fail
+    fetch "${ENDPOINT}"/adv/ | ver $TMP/db/.sig.jwk && expected_fail
 
 
     # A private key advertisement is signed by all advertised keys and the requested private key
     # A private key advertisement is signed by all advertised keys and the requested private key
-    fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/sig.jwk
-    fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.sig.jwk
-    fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail
+    fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/sig.jwk
+    fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.sig.jwk
+    fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/.sig.jwk` | ver $TMP/db/.oth.jwk && expected_fail
 
 
     # Verify that the advertisements contain the cty parameter
     # Verify that the advertisements contain the cty parameter
-    fetch /adv | jose fmt -j- -Og protected -SyOg cty -Sq "jwk-set+json" -E
-    fetch /adv/`jose jwk thp -i $TMP/db/.sig.jwk` \
+    fetch "${ENDPOINT}"/adv | jose fmt -j- -Og protected -SyOg cty -Sq "jwk-set+json" -E
+    fetch "${ENDPOINT}"/adv/`jose jwk thp -i $TMP/db/.sig.jwk` \
         | jose fmt -j- -Og signatures -A \
         | jose fmt -j- -Og signatures -A \
                -g 0 -Og protected -SyOg cty -Sq "jwk-set+json" -EUUUUU \
                -g 0 -Og protected -SyOg cty -Sq "jwk-set+json" -EUUUUU \
                -g 1 -Og protected -SyOg cty -Sq "jwk-set+json" -EUUUUU
                -g 1 -Og protected -SyOg cty -Sq "jwk-set+json" -EUUUUU
 
 
     THP_DEFAULT_HASH=S256     # SHA-256.
     THP_DEFAULT_HASH=S256     # SHA-256.
-    test "$(tang-show-keys $PORT)" = "$(jose jwk thp -a "${THP_DEFAULT_HASH}" -i $TMP/db/sig.jwk)"
+    test "$(tang-show-keys $PORT $ENDPOINT)" = "$(jose jwk thp -a "${THP_DEFAULT_HASH}" -i $TMP/db/sig.jwk)"
 
 
     # Check that new keys will be created if none exist.
     # Check that new keys will be created if none exist.
     rm -rf "${TMP}/db" && mkdir -p "${TMP}/db"
     rm -rf "${TMP}/db" && mkdir -p "${TMP}/db"
-    fetch /adv
+    fetch "${ENDPOINT}"/adv
 
 
     # Now let's make sure the new keys were named using our default thumbprint
     # Now let's make sure the new keys were named using our default thumbprint
     # hash and then rotate them and check if we still create new keys.
     # hash and then rotate them and check if we still create new keys.
@@ -88,7 +88,7 @@ adv_second_phase () {
         mv -f -- "${k}" ".${k}"
         mv -f -- "${k}" ".${k}"
     done
     done
     cd -
     cd -
-    fetch /adv
+    fetch "${ENDPOINT}"/adv
 
 
     # Lets's now test with multiple pairs of keys.
     # Lets's now test with multiple pairs of keys.
     for i in 1 2 3 4 5 6 7 8 9; do
     for i in 1 2 3 4 5 6 7 8 9; do
@@ -103,12 +103,12 @@ adv_second_phase () {
     done
     done
 
 
     # Verify the advertisement is correct.
     # Verify the advertisement is correct.
-    validate "$(fetch /adv)"
+    validate "$(fetch "${ENDPOINT}"/adv)"
 
 
     # And make sure we can fetch an adv by its thumbprint.
     # And make sure we can fetch an adv by its thumbprint.
     for jwk in "${TMP}"/db/other-sig-*.jwk; do
     for jwk in "${TMP}"/db/other-sig-*.jwk; do
 	for alg in $(jose alg -k hash); do
 	for alg in $(jose alg -k hash); do
-		fetch /adv/"$(jose jwk thp -a "${alg}" -i "${jwk}")" | ver "${jwk}"
+		fetch "${ENDPOINT}"/adv/"$(jose jwk thp -a "${alg}" -i "${jwk}")" | ver "${jwk}"
 	done
 	done
     done
     done
 
 
@@ -130,5 +130,5 @@ adv_second_phase () {
 	valid_key_perm "${jwk}"
 	valid_key_perm "${jwk}"
     done
     done
     [ -z "${thp}" ] && die "There should be valid keys after rotation"
     [ -z "${thp}" ] && die "There should be valid keys after rotation"
-    test "$(tang-show-keys $PORT)" = "${thp}"
+    test "$(tang-show-keys $PORT $ENDPOINT)" = "${thp}"
 }
 }

+ 33 - 0
tests/adv-socat-endpoint

@@ -0,0 +1,33 @@
+#!/bin/sh -ex
+#
+# Copyright (c) 2023 Red Hat, Inc.
+# Author: Sergio Arroutbi <sarroutb@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+. adv
+
+sanity_check
+
+adv_startup
+
+port=$(random_port)
+export PORT=$((port+3))
+export ENDPOINT="/api/dee-hms"
+start_server_endpoint "${PORT}" "${ENDPOINT}"
+export PID=$!
+wait_for_port ${PORT}
+
+adv_second_phase

+ 31 - 0
tests/adv-standalone-endpoint

@@ -0,0 +1,31 @@
+#!/bin/sh -ex
+#
+# Copyright (c) 2023 Red Hat, Inc.
+# Author: Sergio Arroutbi <sarroutb@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+. adv
+
+adv_startup
+
+port=$(random_port)
+export PORT=$((port+1))
+export ENDPOINT="/api/dee-hms"
+start_standalone_server_endpoint "${PORT}" "${ENDPOINT}"
+export PID=$!
+wait_for_port ${PORT}
+
+adv_second_phase

+ 14 - 1
tests/helpers

@@ -30,7 +30,12 @@ random_port() {
     if [ -n "${TANG_BSD}" ]; then
     if [ -n "${TANG_BSD}" ]; then
         jot -r 1 1024 65536
         jot -r 1 1024 65536
     else
     else
-        shuf -i 1024-65536 -n 1
+        if test -f /dev/urandom;
+        then
+            shuf -i 1024-65535 -n 1 --random-file=/dev/urandom
+        else
+            shuf -i 1024-65535 -n 1
+        fi
     fi
     fi
 }
 }
 
 
@@ -62,10 +67,18 @@ start_server() {
     "${SOCAT}" TCP-LISTEN:"${1}",bind=127.0.0.1,fork SYSTEM:"${VALGRIND} tangd ${TMP}/db" &
     "${SOCAT}" TCP-LISTEN:"${1}",bind=127.0.0.1,fork SYSTEM:"${VALGRIND} tangd ${TMP}/db" &
 }
 }
 
 
+start_server_endpoint() {
+    "${SOCAT}" TCP-LISTEN:"${1}",bind=127.0.0.1,fork SYSTEM:"${VALGRIND} tangd ${TMP}/db -e ${ENDPOINT}" &
+}
+
 start_standalone_server() {
 start_standalone_server() {
     ${VALGRIND} tangd -p ${1} -l ${TMP}/db &
     ${VALGRIND} tangd -p ${1} -l ${TMP}/db &
 }
 }
 
 
+start_standalone_server_endpoint() {
+    ${VALGRIND} tangd -p ${1} -l ${TMP}/db -e ${2} &
+}
+
 on_exit() {
 on_exit() {
     if [ "${PID}" ]; then
     if [ "${PID}" ]; then
         kill "${PID}" || true
         kill "${PID}" || true

+ 9 - 5
tests/meson.build

@@ -40,10 +40,14 @@ if socat.found()
   env.set('SOCAT', socat.path())
   env.set('SOCAT', socat.path())
 endif
 endif
 
 
-test('adv-standalone', find_program('adv-standalone'), env: env, timeout: 60)
-test('adv-socat', find_program('adv-socat'), env: env, timeout: 60)
-test('rec-standalone', find_program('rec-standalone'), env: env, timeout: 60)
-test('rec-socat', find_program('rec-socat'), env: env)
-test('test-keys', test_keys, env: env, timeout: 60)
+test('adv-standalone', find_program('adv-standalone'), env: env, timeout: 360)
+test('adv-standalone-endpoint', find_program('adv-standalone-endpoint'), env: env, timeout: 360)
+test('adv-socat', find_program('adv-socat'), env: env, timeout: 360)
+test('adv-socat-endpoint', find_program('adv-socat-endpoint'), env: env, timeout: 360)
+test('rec-standalone', find_program('rec-standalone'), env: env, timeout: 360)
+test('rec-standalone-endpoint', find_program('rec-standalone-endpoint'), env: env, timeout: 360)
+test('rec-socat', find_program('rec-socat'), env: env, timeout: 360)
+test('rec-socat-endpoint', find_program('rec-socat-endpoint'), env: env, timeout: 360)
+test('test-keys', test_keys, env: env, timeout: 360)
 
 
 # vim:set ts=2 sw=2 et:
 # vim:set ts=2 sw=2 et:

+ 14 - 0
tests/rec

@@ -51,3 +51,17 @@ rec_second_phase () {
            http://127.0.0.1:$PORT/rec/${exc_kid} < $TMP/exc.pub.jwk`
            http://127.0.0.1:$PORT/rec/${exc_kid} < $TMP/exc.pub.jwk`
     [ "$good" = "$test" ]
     [ "$good" = "$test" ]
 }
 }
+
+rec_second_phase_endpoint () {
+    # Make sure that GET fails
+    curl -sf http://127.0.0.1:$PORT/$ENDPOINT/rec && expected_fail
+    curl -sf http://127.0.0.1:$PORT/$ENDPOINT/rec/ && expected_fail
+
+    # Make a recovery request (NOTE: this is insecure! Don't do this in real code!)
+    good=`jose jwk exc -i '{"alg":"ECMR","key_ops":["deriveKey"]}' -l $TMP/exc.jwk -r $TMP/db/exc.jwk`
+    test=`curl -sf -X POST \
+           -H "Content-Type: application/jwk+json" \
+           --data-binary @- \
+           http://127.0.0.1:$PORT/$ENDPOINT/rec/${exc_kid} < $TMP/exc.pub.jwk`
+    [ "$good" = "$test" ]
+}

+ 34 - 0
tests/rec-socat-endpoint

@@ -0,0 +1,34 @@
+#!/bin/sh -ex
+#
+# Copyright (c) 2023 Red Hat, Inc.
+# Author: Sergio Arroutbi <sarroutb@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+. rec
+
+sanity_check
+
+rec_startup
+
+# Start the server
+port=$(random_port)
+export PORT=$((port+4))
+export ENDPOINT="api/dee-hms"
+start_server_endpoint "${PORT}" "${ENDPOINT}"
+export PID=$!
+wait_for_port ${PORT}
+
+rec_second_phase_endpoint

+ 34 - 0
tests/rec-standalone-endpoint

@@ -0,0 +1,34 @@
+#!/bin/sh -ex
+#
+# Copyright (c) 2023 Red Hat, Inc.
+# Author: Sergio Arroutbi <sarroutb@redhat.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+. rec
+
+sanity_check
+
+rec_startup
+
+# Start the server
+port=$(random_port)
+export PORT=$((port+2))
+export ENDPOINT="api/dee-hms"
+start_standalone_server_endpoint "${PORT}" "${ENDPOINT}"
+export PID=$!
+wait_for_port ${PORT}
+
+rec_second_phase_endpoint