about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorLaurent Bercot <ska-skaware@skarnet.org>2023-08-05 11:51:25 +0000
committerLaurent Bercot <ska@appnovation.com>2023-08-05 11:51:25 +0000
commit17c382d1c9d7236c101418060758d2296cc5e17e (patch)
treefd00e58df0d9d3c70ddd1accfec9e819249c672a /src
downloadtipidee-17c382d1c9d7236c101418060758d2296cc5e17e.tar.gz
tipidee-17c382d1c9d7236c101418060758d2296cc5e17e.tar.xz
tipidee-17c382d1c9d7236c101418060758d2296cc5e17e.zip
Initial commit
Signed-off-by: Laurent Bercot <ska@appnovation.com>
Diffstat (limited to 'src')
-rw-r--r--src/config/PARSING-config.txt56
-rw-r--r--src/config/PARSING-preprocess.txt38
-rw-r--r--src/config/PROTOCOL.txt43
-rw-r--r--src/config/confnode.c35
-rw-r--r--src/config/conftree.c82
-rw-r--r--src/config/defaults.c106
-rw-r--r--src/config/deps-exe/tipidee-config6
-rw-r--r--src/config/deps-exe/tipidee-config-preprocess1
-rw-r--r--src/config/lexparse.c443
-rw-r--r--src/config/tipidee-config-internal.h59
-rw-r--r--src/config/tipidee-config-preprocess.c270
-rw-r--r--src/config/tipidee-config.c135
-rw-r--r--src/include/tipidee/body.h25
-rw-r--r--src/include/tipidee/conf.h44
-rw-r--r--src/include/tipidee/headers.h38
-rw-r--r--src/include/tipidee/method.h32
-rw-r--r--src/include/tipidee/response.h36
-rw-r--r--src/include/tipidee/rql.h31
-rw-r--r--src/include/tipidee/tipidee.h15
-rw-r--r--src/include/tipidee/uri.h31
-rw-r--r--src/libtipidee/deps-lib/tipidee24
-rw-r--r--src/libtipidee/tipidee_chunked_read.c41
-rw-r--r--src/libtipidee/tipidee_conf_free.c10
-rw-r--r--src/libtipidee/tipidee_conf_get.c22
-rw-r--r--src/libtipidee/tipidee_conf_get_argv.c32
-rw-r--r--src/libtipidee/tipidee_conf_get_content_type.c22
-rw-r--r--src/libtipidee/tipidee_conf_get_redirection.c35
-rw-r--r--src/libtipidee/tipidee_conf_get_string.c17
-rw-r--r--src/libtipidee/tipidee_conf_get_uint32.c19
-rw-r--r--src/libtipidee/tipidee_conf_init.c10
-rw-r--r--src/libtipidee/tipidee_headers_get_content_length.c17
-rw-r--r--src/libtipidee/tipidee_headers_init.c29
-rw-r--r--src/libtipidee/tipidee_headers_parse.c210
-rw-r--r--src/libtipidee/tipidee_headers_search.c13
-rw-r--r--src/libtipidee/tipidee_method_conv_table.c19
-rw-r--r--src/libtipidee/tipidee_method_tonum.c12
-rw-r--r--src/libtipidee/tipidee_method_tostr.c8
-rw-r--r--src/libtipidee/tipidee_response_error.c41
-rw-r--r--src/libtipidee/tipidee_response_header_builtin.c40
-rw-r--r--src/libtipidee/tipidee_response_header_common_put.c23
-rw-r--r--src/libtipidee/tipidee_response_header_date_fmt.c24
-rw-r--r--src/libtipidee/tipidee_response_status.c27
-rw-r--r--src/libtipidee/tipidee_rql_read.c85
-rw-r--r--src/libtipidee/tipidee_uri_parse.c184
-rw-r--r--src/tipideed/cgi.c381
-rw-r--r--src/tipideed/deps-exe/tipideed11
-rw-r--r--src/tipideed/harden.c50
-rw-r--r--src/tipideed/log.c59
-rw-r--r--src/tipideed/options.c25
-rw-r--r--src/tipideed/regular.c48
-rw-r--r--src/tipideed/responses.c64
-rw-r--r--src/tipideed/send_file.c123
-rw-r--r--src/tipideed/tipideed-internal.h147
-rw-r--r--src/tipideed/tipideed.c514
-rw-r--r--src/tipideed/trace.c67
55 files changed, 3979 insertions, 0 deletions
diff --git a/src/config/PARSING-config.txt b/src/config/PARSING-config.txt
new file mode 100644
index 0000000..b1be2df
--- /dev/null
+++ b/src/config/PARSING-config.txt
@@ -0,0 +1,56 @@
+global verbosity 1
+global read_timeout 60000
+global index index.cgi index.html
+
+content-type application/pdf .pdf
+content-type text/plain .c .h .txt
+
+
+domain example.com
+
+nph-prefix nph-
+
+redirect rickroll.html 307 https://www.youtube.com/watch?v=dQw4w9WgXcQ
+redirect community/ 308 https://example.org/
+
+cgi /cgi-bin/
+nph /cgi-bin/nph/
+
+cgi /some/script
+nph /some/otherscript
+
+noncgi /cgi-bin/file.html
+noncgi /cgi-bin/directory
+
+file-type /source/ text/plain
+file-type /source/file.html text/html
+
+basic-auth /protected.html
+basic-auth /protected/
+
+
+class	|	0	1	2	3	4
+st\ev	|	\0	space	#	\n	other
+
+START	|				P	np
+00	|	END	SPACE	COMMENT	START	WORD
+
+COMMENT	|				P
+01	|	END	COMMENT	COMMENT	START	COMMENT
+
+SPACE	|	P			P	np
+02	|	END	SPACE	COMMENT	START	WORD
+
+WORD	|	0P	0	p	0P	p
+03	|	END	SPACE	WORD	START	WORD
+
+END: 04
+X: 05
+
+states: 3 bits
+actions: 4 bits
+
+0x10	n	new word
+0x20	p	push cur
+0x40	0	push \0
+0x80	P	process line
diff --git a/src/config/PARSING-preprocess.txt b/src/config/PARSING-preprocess.txt
new file mode 100644
index 0000000..e81c86e
--- /dev/null
+++ b/src/config/PARSING-preprocess.txt
@@ -0,0 +1,38 @@
+
+ Automaton for the preprocessor:
+
+
+class	|	0	1	2	3	4
+st\ev	|	\0	\n	!	space	other
+
+START	|		print		print	print
+0	|	END	START	CMD	NORMAL	NORMAL
+
+NORMAL	|		print	print	print	print
+1	|	END	START	NORMAL	NORMAL	NORMAL
+
+CMD	|					add
+2	|	END	START	IGNORE	CMD1	CMD2
+
+IGNORE	|
+3	|	END	START	IGNORE	IGNORE	IGNORE
+
+CMD1	|					add
+4	|	X	X	X	CMD1	CMD2
+
+CMD2	|				idcmd   add
+5	|	X	X	X	ARG	CMD2
+
+ARG	|					add
+6	|	X	X	ARG1	ARG	ARG1
+
+ARG1	|	proc	proc    add     add     add
+7	|	END	START	ARG1	ARG1	ARG1
+
+states: 0-7 plus END and X -> 4 bits
+actions: 4. -> 8 bits total, fits in a char.
+
+print 0x10 copies the character to stdout
+add   0x20 adds the character to the processing string
+idcmd 0x40 ids the processing string for an !include cmd
+proc  0x80 gets the filename and procs the include
diff --git a/src/config/PROTOCOL.txt b/src/config/PROTOCOL.txt
new file mode 100644
index 0000000..ffeb72f
--- /dev/null
+++ b/src/config/PROTOCOL.txt
@@ -0,0 +1,43 @@
+* Globals
+
+G:verbosity -> 4 bytes
+G:read_timeout -> 4 bytes	exit if waiting on client input for read_timeout ms
+G:write_timeout -> 4 bytes	die if waiting on flush to client for write_timeout ms
+G:cgi_timeout -> 4 bytes	504 if CGI hasn't finished answering / die if NPH hasn't finished reading
+G:max_request_body_length -> 4 bytes
+G:max_cgi_body_length -> 4 bytes
+G:index_file -> file1\0file2\0file3\0  list of files to attempt when URL is a directory
+
+They all need to exist, tipidee-config creates defaults.
+
+
+* Content-Type
+
+T:extension -> string		T:pdf -> application/pdf
+or individual Content-Types in resource attributes
+
+tipidee-config hardcodes a number of default content-types, they
+can be overridden.
+
+
+* Individual redirection
+
+R:vres -> Xurl			R:skarnet.org/rickroll.html -> Xhttps://www.youtube.com/watch?v=dQw4w9WgXcQ
+				X = '@' | redir_type (307=0, 308=1, 302=2, 301=3)
+
+
+* Prefix redirection
+
+r:prefix -> Xurlprefix		r:s6.org -> Xhttps://skarnet.org/software/s6
+				X = '@' | redir_type (307=0, 308=1, 302=2, 301=3)
+
+* Resource attributes
+
+A:file -> Xstring	X = '@' | 1 (iscgi)
+			if string nonempty: it's the content-type for the resource. If empty, default ctype
+
+a:prefix -> Xstring	same, but for prefixes
+
+* NPH prefixes
+
+N:domain -> nphprefix		N:skarnet.org -> nph-	any CGI under skarnet.org starting with nph
diff --git a/src/config/confnode.c b/src/config/confnode.c
new file mode 100644
index 0000000..758e79d
--- /dev/null
+++ b/src/config/confnode.c
@@ -0,0 +1,35 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <string.h>
+
+#include <skalibs/stralloc.h>
+#include <skalibs/strerr.h>
+
+#include "tipidee-config-internal.h"
+
+#define dienomem() strerr_diefu1sys(111, "stralloc_catb")
+#define diestorage() strerr_diefu2x(100, "add node to configuration tree", ": too much data")
+#define diefilepos() strerr_diefu2x(100, "add node to configuration tree", ": file too large")
+
+void confnode_start (confnode *node, char const *key, size_t filepos, uint32_t line)
+{
+  size_t l = strlen(key) ;
+  size_t k = g.storage.len ;
+  if (!stralloc_catb(&g.storage, key, l + 1)) dienomem() ;
+  if (g.storage.len >= UINT32_MAX) diestorage() ;
+  if (filepos > UINT32_MAX) diefilepos() ;
+  node->key = k ;
+  node->keylen = l ;
+  node->data = g.storage.len ;
+  node->datalen = 0 ;
+  node->filepos = filepos ;
+  node->line = line ;
+}
+
+void confnode_add (confnode *node, char const *s, size_t len)
+{
+  if (!stralloc_catb(&g.storage, s, len)) dienomem() ;
+  if (g.storage.len >= UINT32_MAX) strerr_diefu1x(100, "add node to configuration tree: too much data") ;
+  node->datalen += len ;
+}
diff --git a/src/config/conftree.c b/src/config/conftree.c
new file mode 100644
index 0000000..fc0b5bc
--- /dev/null
+++ b/src/config/conftree.c
@@ -0,0 +1,82 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <string.h>
+#include <errno.h>
+
+#include <skalibs/gensetdyn.h>
+#include <skalibs/avltree.h>
+#include <skalibs/cdbmake.h>
+#include <skalibs/strerr.h>
+
+#include "tipidee-config-internal.h"
+
+#define dienomem() strerr_diefu1sys(111, "stralloc_catb")
+
+static void *confnode_dtok (uint32_t d, void *data)
+{
+  return g.storage.s + GENSETDYN_P(confnode, (gensetdyn *)data, d)->key ;
+}
+
+static int confnode_cmp (void const *a, void const *b, void *data)
+{
+  (void)data ;
+  return strcmp((char const *)a, (char const *)b) ;
+}
+
+struct nodestore_s
+{
+  gensetdyn set ;
+  avltree tree ;
+} ;
+
+static struct nodestore_s nodestore = \
+{ \
+  .set = GENSETDYN_INIT(confnode, 8, 3, 8), \
+  .tree = AVLTREE_INIT(8, 3, 8, &confnode_dtok, &confnode_cmp, &nodestore.set) \
+} ;
+
+
+confnode const *conftree_search (char const *key)
+{
+  uint32_t i ;
+  return avltree_search(&nodestore.tree, key, &i) ? GENSETDYN_P(confnode const, &nodestore.set, i) : 0 ;
+}
+
+void conftree_add (confnode const *node)
+{
+  uint32_t i ;
+  if (!gensetdyn_new(&nodestore.set, &i)) dienomem() ;
+  *GENSETDYN_P(confnode, &nodestore.set, i) = *node ;
+  if (!avltree_insert(&nodestore.tree, i)) dienomem() ;
+}
+
+void conftree_update (confnode const *node)
+{
+  uint32_t i ;
+  if (avltree_search(&nodestore.tree, g.storage.s + node->key, &i))
+  {
+    if (!avltree_delete(&nodestore.tree, g.storage.s + node->key)) dienomem() ;
+    *GENSETDYN_P(confnode, &nodestore.set, i) = *node ;
+    if (!avltree_insert(&nodestore.tree, i)) dienomem() ;
+  }
+  else return conftree_add(node) ;
+}
+
+static int confnode_write (uint32_t d, unsigned int h, void *data)
+{
+  confnode *node = GENSETDYN_P(confnode, &nodestore.set, d) ;
+  cdbmaker *cm = data ;
+  (void)h ;
+  if ((g.storage.s[node->key] & ~0x20) == 'A')
+  {
+    g.storage.s[++node->data] |= '@' ;
+    node->datalen-- ;
+  }
+  return cdbmake_add(cm, g.storage.s + node->key, node->keylen, g.storage.s + node->data, node->datalen) ;
+}
+
+int conftree_write (cdbmaker *cm)
+{
+  return avltree_iter(&nodestore.tree, &confnode_write, cm) ;
+}
diff --git a/src/config/defaults.c b/src/config/defaults.c
new file mode 100644
index 0000000..5913796
--- /dev/null
+++ b/src/config/defaults.c
@@ -0,0 +1,106 @@
+/* ISC license. */
+
+#include <stddef.h>
+
+#include "tipidee-config-internal.h"
+
+struct defaults_s
+{
+  char const *key ;
+  char const *value ;
+  size_t vlen ;
+} ;
+
+#define RECB(k, v, n) { .key = k, .value = v, .vlen = n }
+#define REC(k, v) RECB(k, v, sizeof(v))
+
+struct defaults_s const defaults[] =
+{
+  RECB("G:verbosity", "\0\0\0\001", 4),
+  RECB("G:read_timeout", "\0\0\0", 4),
+  RECB("G:write_timeout", "\0\0\0", 4),
+  RECB("G:cgi_timeout", "\0\0\0", 4),
+  RECB("G:max_request_body_length", "\0\0 ", 4),
+  RECB("G:max_cgi_body_length", "\0@\0", 4),
+  REC("G:index_file", "index.html"),
+
+  REC("T:html", "text/html"),
+  REC("T:htm", "text/html"),
+  REC("T:txt", "text/plain"),
+  REC("T:h", "text/plain"),
+  REC("T:c", "text/plain"),
+  REC("T:cc", "text/plain"),
+  REC("T:cpp", "text/plain"),
+  REC("T:java", "text/plain"),
+  REC("T:mjs", "text/javascript"),
+  REC("T:css", "text/css"),
+  REC("T:csv", "text/csv"),
+  REC("T:doc", "application/msword"),
+  REC("T:docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
+  REC("T:js", "application/javascript"),
+  REC("T:jpg", "image/jpeg"),
+  REC("T:jpeg", "image/jpeg"),
+  REC("T:gif", "image/gif"),
+  REC("T:png", "image/png"),
+  REC("T:bmp", "image/bmp"),
+  REC("T:svg", "image/svg+xml"),
+  REC("T:tif", "image/tiff"),
+  REC("T:tiff", "image/tiff"),
+  REC("T:ico", "image/vnd.microsoft.icon"),
+  REC("T:au", "audio/basic"),
+  REC("T:aac", "audio/aac"),
+  REC("T:wav", "audio/wav"),
+  REC("T:mid", "audio/midi"),
+  REC("T:midi", "audio/midi"),
+  REC("T:mp3", "audio/mpeg"),
+  REC("T:ogg", "audio/ogg"),
+  REC("T:oga", "audio/ogg"),
+  REC("T:ogv", "video/ogg"),
+  REC("T:avi", "video/x-msvideo"),
+  REC("T:wmv", "video/x-ms-wmv"),
+  REC("T:qt", "video/quicktime"),
+  REC("T:mov", "video/quicktime"),
+  REC("T:mpe", "video/mpeg"),
+  REC("T:mpeg", "video/mpeg"),
+  REC("T:mp4", "video/mp4"),
+  REC("T:otf", "font/otf"),
+  REC("T:ttf", "font/ttf"),
+  REC("T:epub", "application/epub+zip"),
+  REC("T:jar", "application/java-archive"),
+  REC("T:json", "application/json"),
+  REC("T:jsonld", "application/ld+json"),
+  REC("T:pdf", "application/pdf"),
+  REC("T:ppt", "application/vnd.ms-powerpoint"),
+  REC("T:pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"),
+  REC("T:odp", "application/vnd.oasis.opendocument.presentation"),
+  REC("T:ods", "application/vnd.oasis.opendocument.spreadsheet"),
+  REC("T:odt", "application/vnd.oasis.opendocument.text"),
+  REC("T:oggx", "application/ogg"),
+  REC("T:rar", "application/vnd.rar"),
+  REC("T:rtf", "application/rtf"),
+  REC("T:sh", "application/x-sh"),
+  REC("T:tar", "application/x-tar"),
+  REC("T:xhtml", "application/xhtml+xml"),
+  REC("T:xls", "application/vnd.ms-excel"),
+  REC("T:xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
+  REC("T:xml", "application/xml"),
+  REC("T:xul", "application/vnd.mozilla.xul+xml"),
+  REC("T:zip", "application/zip"),
+  REC("T:7z", "application/x-7z-compressed"),
+
+  RECB(0, 0, 0)
+} ;
+
+void conf_defaults (void)
+{
+  for (struct defaults_s const *p = defaults ; p->key ; p++)
+  {
+    if (!conftree_search(p->key))
+    {
+      confnode node ;
+      confnode_start(&node, p->key, 0, 0) ;
+      confnode_add(&node, p->value, p->vlen) ;
+      conftree_add(&node) ;
+    }
+  }
+}
diff --git a/src/config/deps-exe/tipidee-config b/src/config/deps-exe/tipidee-config
new file mode 100644
index 0000000..85a9645
--- /dev/null
+++ b/src/config/deps-exe/tipidee-config
@@ -0,0 +1,6 @@
+confnode.o
+conftree.o
+defaults.o
+lexparse.o
+-lskarnet
+${SPAWN_LIB}
diff --git a/src/config/deps-exe/tipidee-config-preprocess b/src/config/deps-exe/tipidee-config-preprocess
new file mode 100644
index 0000000..e7187fe
--- /dev/null
+++ b/src/config/deps-exe/tipidee-config-preprocess
@@ -0,0 +1 @@
+-lskarnet
diff --git a/src/config/lexparse.c b/src/config/lexparse.c
new file mode 100644
index 0000000..f843f97
--- /dev/null
+++ b/src/config/lexparse.c
@@ -0,0 +1,443 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#include <skalibs/uint32.h>
+#include <skalibs/buffer.h>
+#include <skalibs/strerr.h>
+#include <skalibs/stralloc.h>
+#include <skalibs/genalloc.h>
+#include <skalibs/skamisc.h>
+
+#include <tipidee/config.h>
+#include "tipidee-config-internal.h"
+
+#define dienomem() strerr_diefu1sys(111, "stralloc_catb")
+#define dietoobig() strerr_diefu1sys(100, "read configuration")
+
+typedef struct mdt_s mdt, *mdt_ref ;
+struct mdt_s
+{
+  size_t filepos ;
+  uint32_t line ;
+  char linefmt[UINT32_FMT] ;
+} ;
+#define MDT_ZERO { .filepos = 0, .line = 0, .linefmt = "0" }
+
+struct globalkey_s
+{
+  char const *name ;
+  char const *key ;
+  uint32_t type ;
+} ;
+
+static int globalkey_cmp (void const *a, void const *b)
+{
+  return strcmp((char const *)a, ((struct globalkey_s const *)b)->name) ;
+}
+
+enum token_e
+{
+  T_BANG,
+  T_GLOBAL,
+  T_CONTENTTYPE,
+  T_DOMAIN,
+  T_NPHPREFIX,
+  T_REDIRECT,
+  T_CGI,
+  T_NONCGI,
+  T_NPH,
+  T_NONNPH,
+  T_BASICAUTH,
+  T_NOAUTH,
+  T_FILETYPE
+} ;
+
+struct directive_s
+{
+  char const *s ;
+  enum token_e token ;
+} ;
+
+static int directive_cmp (void const *a, void const *b)
+{
+  return strcmp((char const *)a, ((struct directive_s const *)b)->s) ;
+}
+
+static void check_unique (char const *key, mdt const *md)
+{
+  confnode const *node = conftree_search(key) ;
+  if (node)
+  {
+    char fmt[UINT32_FMT] ;
+    fmt[uint32_fmt(fmt, node->line)] = 0 ;
+    strerr_diefn(1, 11, "duplicate key ", key, " in file ", g.storage.s + md->filepos, " line ", md->linefmt, ", previously defined", " in file ", g.storage.s + node->filepos, " line ", fmt) ;
+  }
+}
+
+static void add_unique (char const *key, char const *value, size_t valuelen, mdt const *md)
+{
+  confnode node ;
+  check_unique(key, md) ;
+  confnode_start(&node, key, md->filepos, md->line) ;
+  confnode_add(&node, value, valuelen) ;
+  conftree_add(&node) ;
+}
+
+static inline void parse_global (char const *s, size_t const *word, size_t n, mdt const *md)
+{
+  static struct globalkey_s const globalkeys[] =
+  {
+    { .name = "cgi_timeout", .key = "G:cgi_timeout", .type = 0 },
+    { .name = "index_file", .key = "G:index_file", .type = 1 },
+    { .name = "max_cgi_body_length", .key = "G:max_cgi_body_length", .type = 0 },
+    { .name = "max_request_body_length", .key = "G:max_request_body_length", .type = 0 },
+    { .name = "read_timeout", .key = "G:read_timeout", .type = 0 },
+    { .name = "verbosity", .key = "G:verbosity", .type = 0 },
+    { .name = "write_timeout", .key = "G:write_timeout", .type = 0 }
+  } ;
+  struct globalkey_s *gl ;
+  if (n != 2)
+    strerr_dief8x(1, "too ", n > 2 ? "many" : "few", " arguments to directive ", "global", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  gl = bsearch(s + word[0], globalkeys, sizeof(globalkeys)/sizeof(struct globalkey_s), sizeof(struct globalkey_s), &globalkey_cmp) ;
+  if (!gl) strerr_dief6x(1, "unrecognized global setting ", s + word[0], " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  switch (gl->type)
+  {
+    case 0 : /* 4 bytes */
+    {
+      char pack[4] ;
+      uint32_t u ;
+      if (n != 2) strerr_dief7x(1, "too many", " arguments to global setting ", s + word[0], " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+      if (!uint320_scan(s + word[1], &u))
+        strerr_dief6x(1, "invalid (non-numeric) value for global setting ", s + word[0], " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+      uint32_pack_big(pack, u) ;
+      add_unique(gl->key, pack, 4, md) ;
+      break ;
+    }
+    case 1 : /* argv */
+    {
+      confnode node ;
+      check_unique(gl->key, md) ;
+      confnode_start(&node, gl->key, md->filepos, md->line) ;
+      for (size_t i = 1 ; i < n ; i++)
+        confnode_add(&node, s + word[i], strlen(s + word[i]) + 1) ;
+      conftree_add(&node) ;
+      break ;
+    }
+  }
+}
+
+static inline void parse_contenttype (char const *s, size_t const *word, size_t n, mdt const *md)
+{
+  char const *ct ;
+  if (n != 3)
+    strerr_dief8x(1, "too ", n > 3 ? "many" : "few", " arguments to directive ", "redirect", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  ct = s + *word++ ;
+  if (!strchr(ct, '/'))
+    strerr_dief6x(1, "Content-Type must include a slash, ", "check directive", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  n-- ;
+  for (size_t i = 0 ; i < n ; i++)
+  {
+    size_t len = strlen(s + word[i]) ;
+    char key[len + 2] ;
+    if (s[word[i]] != '.')
+      strerr_dief6x(1, "file extensions must start with a dot, ", "check directive", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+    key[0] = 'T' ;
+    key[1] = ':' ;
+    memcpy(key + 2, s + word[i] + 1, len - 1) ;
+    key[len + 1] = 0 ;
+    add_unique(key, ct, strlen(ct) + 1, md) ;
+  }
+}
+
+static inline void parse_redirect (char const *s, size_t const *word, size_t n, char const *domain, size_t domainlen, mdt const *md)
+{
+  static uint32_t const codes[4] = { 307, 308, 302, 301 } ;
+  uint32_t code ;
+  uint32_t i = 0 ;
+  if (n != 3)
+    strerr_dief8x(1, "too ", n > 3 ? "many" : "few", " arguments to directive ", "redirect", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  if (!domain)
+    strerr_dief6x(1, "redirection", " without a domain directive", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  if (s[word[0]] != '/')
+    strerr_dief5x(1, "redirected resource must start with /", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  if (!uint320_scan(s + word[1], &code))
+    strerr_dief6x(1, "invalid redirection code ", s + word[1], " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  for (; i < 4 ; i++) if (code == codes[i]) break ;
+  if (i >= 4)
+    strerr_dief6x(1, "redirection code ", "must be 301, 302, 307 or 308", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  if (strncmp(s + word[2], "http://", 7) && strncmp(s + word[2], "https://", 8))
+    strerr_dief5x(1, "redirection target must be a full http:// or https:// target", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  {
+    confnode node ;
+    size_t urlen = strlen(s + word[0]) ;
+    char key[3 + domainlen + urlen] ;
+    if (s[word[0] + urlen - 1] == '/') { key[0] = 'r' ; urlen-- ; } else key[0] = 'R' ;
+    key[1] = ':' ;
+    memcpy(key + 2, domain, domainlen) ;
+    memcpy(key + 2 + domainlen, s + word[0], urlen) ;
+    key[2 + domainlen + urlen] = 0 ;
+    check_unique(key, md) ;
+    confnode_start(&node, key, md->filepos, md->line) ;
+    key[0] = '@' | i ;
+    confnode_add(&node, &key[0], 1) ;
+    confnode_add(&node, s + word[2], strlen(s + word[2]) + 1) ;
+    conftree_add(&node) ;
+  }
+}
+
+static void parse_bitattr (char const *s, size_t const *word, size_t n, char const *domain, size_t domainlen, mdt const *md, unsigned int bit, int set)
+{
+  static char const *attr[3][2] = { { "noncgi", "cgi" }, { "nonnph", "nph", }, { "noauth", "basic-auth" } } ;
+  uint8_t mask = (uint8_t)0x01 << bit ;
+  if (n != 1)
+    strerr_dief8x(1, "too ", n > 1 ? "many" : "few", " arguments to directive ", attr[bit][set], " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  if (!domain)
+    strerr_dief7x(1, "resource attribute ", "definition", " without a domain directive", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  if (s[*word] != '/')
+    strerr_dief5x(1, "resource must start with /", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  {
+    confnode const *oldnode ;
+    size_t arglen = strlen(s + *word) ;
+    char key[3 + domainlen + arglen] ;
+    if (s[*word + arglen - 1] == '/') { key[0] = 'a' ; arglen-- ; } else key[0] = 'A' ;
+    key[1] = ':' ;
+    memcpy(key + 2, domain, domainlen) ;
+    memcpy(key + 2 + domainlen, s + *word, arglen) ;
+    key[2 + domainlen + arglen] = 0 ;
+    oldnode = conftree_search(key) ;
+    if (oldnode)
+      if (g.storage.s[oldnode->data] & mask)
+      {
+        char fmt[UINT32_FMT] ;
+        fmt[uint32_fmt(fmt, oldnode->line)] = 0 ;
+        strerr_diefn(1, 13, "resource attribute ", attr[bit][set], " redefinition", " in file ", g.storage.s + md->filepos, " line ", md->linefmt, "; previous definition", " in file ", g.storage.s + oldnode->filepos, " line ", fmt, " or later") ;
+      }
+      else
+      {
+        g.storage.s[oldnode->data] |= mask ;
+        if (set) g.storage.s[oldnode->data + 1] |= mask ;
+        else g.storage.s[oldnode->data + 1] &= ~mask ;
+      }
+    else
+    {
+      confnode node ;
+      char val[3] = { mask, set ? mask : 0, 0 } ;
+      confnode_start(&node, key, md->filepos, md->line) ;
+      confnode_add(&node, val, 3) ;
+      conftree_add(&node) ;
+    }
+  }
+}
+
+static inline void parse_filetype (char const *s, size_t const *word, size_t n, char const *domain, size_t domainlen, mdt const *md)
+{
+  if (n != 2)
+    strerr_dief8x(1, "too ", n > 2 ? "many" : "few", " arguments to directive ", "file-type", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  if (!domain)
+    strerr_dief7x(1, "file-type", " definition", " without a domain directive", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  if (s[word[0]] != '/')
+    strerr_dief5x(1, "resource must start with /", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  {
+    confnode const *oldnode ;
+    size_t arglen = strlen(s + word[0]) ;
+    char key[3 + domainlen + arglen] ;
+    if (s[word[0] + arglen - 1] == '/') { key[0] = 'a' ; arglen-- ; } else key[0] = 'A' ;
+    key[1] = ':' ;
+    memcpy(key + 2, domain, domainlen) ;
+    memcpy(key + 2 + domainlen, s + *word, arglen) ;
+    key[2 + domainlen + arglen] = 0 ;
+    oldnode = conftree_search(key) ;
+    if (oldnode)
+    {
+      if (g.storage.s[oldnode->data] & 0x80)
+      {
+        char fmt[UINT32_FMT] ;
+        fmt[uint32_fmt(fmt, oldnode->line)] = 0 ;
+        strerr_diefn(1, 12, "file-type", " redefinition", " in file ", g.storage.s + md->filepos, " line ", md->linefmt, "; previous definition", " in file ", g.storage.s + oldnode->filepos, " line ", fmt, " or later") ;
+      }
+      
+      else
+      {
+        confnode node ;
+        char val[2] = { g.storage.s[oldnode->data] | 0x80, g.storage.s[oldnode->data + 1] } ;
+        confnode_start(&node, key, md->filepos, md->line) ;
+        confnode_add(&node, val, 2) ;
+        confnode_add(&node, s + word[1], strlen(s + word[1]) + 1) ;
+        conftree_update(&node) ;
+      }
+    }
+    else
+    {
+      confnode node ;
+      char val[2] = { 0x80, 0x00 } ;
+      confnode_start(&node, key, md->filepos, md->line) ;
+      confnode_add(&node, val, 2) ;
+      confnode_add(&node, s + word[1], strlen(s + word[1]) + 1) ;
+      conftree_add(&node) ;
+    }
+  }
+}
+
+static inline void process_line (char const *s, size_t const *word, size_t n, stralloc *domain, mdt *md)
+{
+  static struct directive_s const directives[] =
+  {
+    { .s = "!", .token = T_BANG },
+    { .s = "basic-auth", .token = T_BASICAUTH },
+    { .s = "cgi", .token = T_CGI },
+    { .s = "content-type", .token = T_CONTENTTYPE },
+    { .s = "domain", .token = T_DOMAIN },
+    { .s = "file-type", .token = T_FILETYPE },
+    { .s = "global", .token = T_GLOBAL },
+    { .s = "no-auth", .token = T_NOAUTH },
+    { .s = "noncgi", .token = T_NONCGI },
+    { .s = "nonnph", .token = T_NONNPH },
+    { .s = "nph", .token = T_NPH },
+    { .s = "nph-prefix", .token = T_NPHPREFIX },
+    { .s = "redirect", .token = T_REDIRECT },
+  } ;
+  struct directive_s const *directive ;
+  char const *word0 ;
+  if (!n--) return ;
+  word0 = s + *word++ ;
+  directive = bsearch(word0, directives, sizeof(directives)/sizeof(struct directive_s), sizeof(struct directive_s), &directive_cmp) ;
+  if (!directive)
+    strerr_dief6x(1, "unrecognized word ", word0, " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  switch (directive->token)
+  {
+    case T_BANG :
+    {
+      size_t len, r, w ;
+      if (n != 2 || !uint320_scan(s + word[0], &md->line))
+        strerr_dief5x(101, "can't happen: invalid ! control directive", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+      len = strlen(s + word[1]) ;
+      if (!stralloc_readyplus(&g.storage, len + 1)) dienomem() ;
+      if (!string_unquote(g.storage.s + g.storage.len, &w, s + word[1], len, &r))
+        strerr_dief7sys(101, "can't happen: unable to unquote ", s + word[1], " in file ", g.storage.s + md->filepos, " line ", md->linefmt, ", error reported is") ;
+      g.storage.s[g.storage.len + w++] = 0 ;
+      md->filepos = g.storage.len ;
+      g.storage.len += w ;
+      break ;
+    }
+    case T_GLOBAL :
+      parse_global(s, word, n, md) ;
+      break ;
+    case T_CONTENTTYPE :
+      parse_contenttype(s, word, n, md) ;
+      break ;
+    case T_DOMAIN :
+      if (n != 1)
+        strerr_dief8x(1, "too", n > 1 ? "many" : "few", " arguments to directive ", "domain", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+      domain->len = 0 ;
+      if (!stralloc_cats(domain, s + *word)) dienomem() ;
+      while (domain->len && (domain->s[domain->len - 1] == '/' || domain->s[domain->len - 1] == '.')) domain->len-- ;
+      break ;
+    case T_NPHPREFIX :
+      if (n != 1)
+        strerr_dief8x(1, "too ", n > 1 ? "many" : "few", " arguments to directive ", "nph-prefix", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+      if (!domain->s)
+        strerr_dief6x(1, "nph prefix definition", "without a domain directive", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+      if (strchr(s + *word, '/')) strerr_dief5x(1, "invalid / in nph-prefix argument", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+      {
+        char key[3 + domain->len] ;
+        key[0] = 'N' ;
+        key[1] = ':' ;
+        memcpy(key + 2, domain->s, domain->len) ;
+        key[2 + domain->len] = 0 ;
+        add_unique(key, s + *word, strlen(s + *word) + 1, md) ;
+      }
+      break ;
+    case T_REDIRECT :
+      parse_redirect(s, word, n, domain->s, domain->len, md) ;
+      break ;
+    case T_CGI :
+      parse_bitattr(s, word, n, domain->s, domain->len, md, 0, 1) ;
+      break ;
+    case T_NONCGI :
+      parse_bitattr(s, word, n, domain->s, domain->len, md, 0, 0) ;
+      break ;
+    case T_NPH :
+      parse_bitattr(s, word, n, domain->s, domain->len, md, 1, 1) ;
+      break ;
+    case T_NONNPH :
+      parse_bitattr(s, word, n, domain->s, domain->len, md, 1, 0) ;
+      break ;
+    case T_BASICAUTH :
+      strerr_warnw5x("file ", g.storage.s + md->filepos, " line ", md->linefmt, ": directive basic-auth not implemented in tipidee-" TIPIDEE_VERSION) ;
+      parse_bitattr(s, word, n, domain->s, domain->len, md, 2, 1) ;
+      break ;
+    case T_NOAUTH :
+      strerr_warnw5x("file ", g.storage.s + md->filepos, " line ", md->linefmt, ": directive basic-auth not implemented in tipidee-" TIPIDEE_VERSION) ;
+      parse_bitattr(s, word, n, domain->s, domain->len, md, 2, 0) ;
+      break ;
+    case T_FILETYPE :
+      parse_filetype(s, word, n, domain->s, domain->len, md) ;
+      break ;
+  }
+}
+
+static inline uint8_t cclass (char c)
+{
+  switch (c)
+  {
+    case 0 : return 0 ;
+    case ' ' :
+    case '\t' :
+    case '\f' :
+    case '\r' : return 1 ;
+    case '#' : return 2 ;
+    case '\n' : return 3 ;
+    default : return 4 ;
+  }
+}
+
+static inline char next (buffer *b, mdt const *md)
+{
+  char c ;
+  ssize_t r = buffer_get(b, &c, 1) ;
+  if (r == -1) strerr_diefu1sys(111, "read from preprocessor") ;
+  if (!r) return 0 ;
+  if (!c) strerr_dief5x(1, "null character", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  return c ;
+}
+
+void conf_lexparse (buffer *b, char const *ifile)
+{
+  static uint8_t const table[4][5] =  /* see PARSING.txt */
+  {
+    { 0x04, 0x02, 0x01, 0x80, 0x33 },
+    { 0x04, 0x01, 0x01, 0x80, 0x01 },
+    { 0x84, 0x02, 0x01, 0x80, 0x33 },
+    { 0xc4, 0x42, 0x23, 0xc0, 0x23 }
+  } ;
+  stralloc sa = STRALLOC_ZERO ;
+  genalloc words = GENALLOC_ZERO ; /* size_t */
+  stralloc domain = STRALLOC_ZERO ;
+  mdt md = MDT_ZERO ;
+  uint8_t state = 0 ;
+  if (!stralloc_catb(&g.storage, ifile, strlen(ifile) + 1)) dienomem() ;
+  while (state < 0x04)
+  {
+    char c = next(b, &md) ;
+    uint8_t what = table[state][cclass(c)] ;
+    state = what & 0x07 ;
+    if (what & 0x10) if (!genalloc_catb(size_t, &words, &sa.len, 1)) dienomem() ;
+    if (what & 0x20) if (!stralloc_catb(&sa, &c, 1)) dienomem() ; 
+    if (what & 0x40) if (!stralloc_0(&sa)) dienomem() ;
+    if (what & 0x80)
+    {
+      process_line(sa.s, genalloc_s(size_t, &words), genalloc_len(size_t, &words), &domain, &md) ;
+      genalloc_setlen(size_t, &words, 0) ;
+      sa.len = 0 ;
+      md.line++ ;
+      md.linefmt[uint32_fmt(md.linefmt, md.line)] = 0 ;
+    }
+  }
+  stralloc_free(&domain) ;
+  genalloc_free(size_t, &words) ;
+  stralloc_free(&sa) ;
+}
diff --git a/src/config/tipidee-config-internal.h b/src/config/tipidee-config-internal.h
new file mode 100644
index 0000000..e274f94
--- /dev/null
+++ b/src/config/tipidee-config-internal.h
@@ -0,0 +1,59 @@
+/* ISC license. */
+
+#ifndef TIPIDEE_CONFIG_INTERNAL_H
+#define TIPIDEE_CONFIG_INTERNAL_H
+
+#include <stdint.h>
+#include <string.h>
+
+#include <skalibs/buffer.h>
+#include <skalibs/stralloc.h>
+#include <skalibs/cdbmake.h>
+
+typedef struct confnode_s confnode, *confnode_ref ;
+struct confnode_s
+{
+  uint32_t key ;
+  uint32_t keylen ;
+  uint32_t data ;
+  uint32_t datalen ;
+  uint32_t filepos ;
+  uint32_t line ;
+} ;
+#define CONFNODE_ZERO { .key = 0, .keylen = 0, .data = 0, .datalen = 0 }
+
+struct global_s
+{
+  stralloc storage ;
+} ;
+#define GLOBAL_ZERO { .storage = STRALLOC_ZERO }
+
+extern struct global_s g ;
+
+
+ /* confnode */
+
+extern void confnode_start (confnode *, char const *, size_t, uint32_t) ;
+extern void confnode_add (confnode *, char const *, size_t) ;
+#define confnode_adds(node, s) confnode_add(node, (s), strlen(s))
+#define confnode_add0(node) confnode_add((node), "", 1)
+
+
+ /* conftree */
+
+extern confnode const *conftree_search (char const *) ;
+extern void conftree_add (confnode const *) ;
+extern void conftree_update (confnode const *) ;
+extern int conftree_write (cdbmaker *) ;
+
+
+ /* lexparse */
+
+extern void conf_lexparse (buffer *, char const *) ;
+
+
+ /* defaults */
+
+extern void conf_defaults (void) ;
+
+#endif
diff --git a/src/config/tipidee-config-preprocess.c b/src/config/tipidee-config-preprocess.c
new file mode 100644
index 0000000..6ac4812
--- /dev/null
+++ b/src/config/tipidee-config-preprocess.c
@@ -0,0 +1,270 @@
+/* ISC license. */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+
+#include <skalibs/uint32.h>
+#include <skalibs/sgetopt.h>
+#include <skalibs/buffer.h>
+#include <skalibs/strerr.h>
+#include <skalibs/stralloc.h>
+#include <skalibs/genalloc.h>
+#include <skalibs/direntry.h>
+#include <skalibs/djbunix.h>
+#include <skalibs/skamisc.h>
+#include <skalibs/avltree.h>
+
+#define USAGE "tipidee-config-preprocess file"
+#define dieusage() strerr_dieusage(100, USAGE)
+#define dienomem() strerr_diefu1sys(111, "stralloc_catb") ;
+
+static stralloc sa = STRALLOC_ZERO ;
+static genalloc ga = GENALLOC_ZERO ;  /* size_t */
+
+
+ /* Name storage */
+
+static stralloc namesa = STRALLOC_ZERO ;
+
+static void *name_dtok (uint32_t pos, void *aux)
+{
+  return ((stralloc *)aux)->s + pos + 1 ;
+}
+
+static int name_cmp (void const *a, void const *b, void *aux)
+{
+  (void)aux ;
+  return strcmp((char const *)a, (char const *)b) ;
+}
+
+static avltree namemap = AVLTREE_INIT(8, 3, 8, &name_dtok, &name_cmp, &namesa) ;
+
+
+ /* Directory sorting */
+
+static char const *dname_cmp_base ;
+static int dname_cmp (void const *a, void const *b)
+{
+  return strcmp(dname_cmp_base + *(size_t *)a, dname_cmp_base + *(size_t *)b) ;
+}
+
+
+ /* Recursive inclusion functions */
+
+static void includefromhere (char const *) ;
+
+static inline void includecwd (void)
+{
+  DIR *dir ;
+  size_t sabase = sa.len ;
+  size_t gabase = genalloc_len(size_t, &ga) ;
+  if (sagetcwd(&sa) < 0 || !stralloc_0(&sa)) dienomem() ;
+  dir = opendir(".") ;
+  if (!dir) strerr_diefu2sys(111, "opendir ", sa.s + sabase) ;
+
+  for (;;)
+  {
+    direntry *d ;
+    errno = 0 ;
+    d = readdir(dir) ;
+    if (!d) break ;
+    if (d->d_name[0] == '.') continue ;
+    if (!genalloc_catb(size_t, &ga, &sa.len, 1)) dienomem() ;
+    if (!stralloc_catb(&sa, d->d_name, strlen(d->d_name)+1)) dienomem() ;
+  }
+  dir_close(dir) ;
+  if (errno) strerr_diefu2sys(111, "readdir ", sa.s + sabase) ;
+
+  dname_cmp_base = sa.s ;
+  qsort(genalloc_s(size_t, &ga) + gabase, genalloc_len(size_t, &ga) - gabase, sizeof(size_t), &dname_cmp) ;
+
+  for (size_t i = 0 ; i < genalloc_len(size_t, &ga) ; i++)
+    includefromhere(sa.s + genalloc_s(size_t, &ga)[gabase + i]) ;
+
+  genalloc_setlen(size_t, &ga, gabase) ;
+  sa.len = sabase ;
+}
+
+static void include (char const *file)
+{
+  size_t sabase = sa.len ;
+  size_t filelen = strlen(file) ;
+  if (!sadirname(&sa, file, filelen) || !stralloc_0(&sa)) dienomem() ;
+  if (chdir(sa.s + sabase) < 0) strerr_diefu2sys(111, "chdir to ", sa.s + sabase) ;
+  sa.len = sabase ;
+  if (!sabasename(&sa, file, filelen)) dienomem() ;
+  {
+    char fn[sa.len + 1 - sabase] ;
+    memcpy(fn, sa.s + sabase, sa.len - sabase) ;
+    fn[sa.len - sabase] = 0 ;
+    sa.len = sabase ;
+    includefromhere(fn) ;
+  }
+}
+
+static inline int idcmd (char const *s)
+{
+  static char const *commands[] =
+  {
+    "include",
+    "includedir",
+    "included:",
+    0
+  } ;
+  for (char const **p = commands ; *p ; p++)
+    if (!strcmp(s, *p)) return p - commands ;
+  return -1 ;
+}
+
+static inline unsigned char cclass (char c)
+{
+  static unsigned char const classtable[34] = "0444444443144344444444444444444432" ;
+  return (unsigned char)c < 34 ? classtable[(unsigned char)c] - '0' : 4 ;
+}
+
+static void includefromhere (char const *file)
+{
+  static unsigned char const table[8][5] =
+  {
+    { 0x08, 0x10, 0x02, 0x11, 0x11 },
+    { 0x08, 0x10, 0x11, 0x11, 0x11 },
+    { 0x08, 0x00, 0x03, 0x04, 0x25 },
+    { 0x08, 0x00, 0x03, 0x03, 0x03 },
+    { 0x09, 0x09, 0x09, 0x04, 0x25 },
+    { 0x09, 0x09, 0x09, 0x46, 0x25 },
+    { 0x0a, 0x0a, 0x07, 0x06, 0x27 },
+    { 0x88, 0x80, 0x27, 0x27, 0x27 }
+  } ;
+  size_t sabase = sa.len ;
+  size_t namesabase = namesa.len ;
+  size_t sastart ;
+  int cmd = -1 ;
+  int fd ;
+  buffer b ;
+  uint32_t d ;
+  uint32_t line = 1 ;
+  char buf[4096] ;
+  char linefmt[UINT32_FMT] = "1" ;
+  unsigned char state = 0 ;
+
+  if (!stralloc_catb(&namesa, "\004", 1) || sarealpath(&namesa, file) < 0 || !stralloc_0(&namesa)) dienomem() ;
+  if (avltree_search(&namemap, namesa.s + namesabase + 1, &d))
+  {
+    if (namesa.s[d] & 0x04)
+      strerr_dief3x(3, "file ", namesa.s + namesabase + 1, " is included in a cycle") ;
+    if (!(namesa.s[d] & 0x02))
+      strerr_dief3x(3, "file ", namesa.s + namesabase + 1, " is included twice but does not declare !included: unique or !included: multiple") ;
+    namesa.len = namesabase ;
+    if (namesa.s[d] & 0x01) return ;
+  }
+  else
+  {
+    if (namesabase > UINT32_MAX)
+      strerr_dief3x(3, "in ", namesa.s + d + 1, ": too many, too long filenames") ;
+    d = namesabase ;
+    if (!avltree_insert(&namemap, d)) dienomem() ;
+  }
+
+  if (!string_quote(&sa, namesa.s + d + 1, strlen(namesa.s + d + 1))) dienomem() ;
+  if (!stralloc_0(&sa)) dienomem() ;
+
+  sastart = sa.len ;
+  fd = open_readb(file) ;
+  if (fd < 0) strerr_diefu2sys(111, "open ", namesa.s + d + 1) ;
+  buffer_init(&b, &buffer_read, fd, buf, 4096) ;
+
+  if (buffer_put(buffer_1, "! 0 ", 4) < 4
+   || buffer_puts(buffer_1, sa.s + sabase) < 0
+   || buffer_put(buffer_1, "\n", 1) < 1)
+    strerr_diefu1sys(111, "write to stdout") ;
+
+  while (state < 8)
+  {
+    uint16_t what ;
+    char c = 0 ;
+    if (buffer_get(&b, &c, 1) < 0) strerr_diefu2sys(111, "read from ", namesa.s + d + 1) ;
+    what = table[state][cclass((unsigned char)c)] ;
+    state = what & 0x000f ;
+    if (what & 0x0010) if (buffer_put(buffer_1, &c, 1) < 1) strerr_diefu1sys(111, "write to stdout") ;
+    if (what & 0x0020) if (!stralloc_catb(&sa, &c, 1)) dienomem() ;
+    if (what & 0x0040)
+    {
+      if (!stralloc_0(&sa)) dienomem() ;
+      cmd = idcmd(sa.s + sastart) ;
+      if (cmd == -1)
+        strerr_dief6x(2, "in ", namesa.s + d + 1, " line ", linefmt, ": unrecognized directive: ", sa.s + sastart) ;
+      sa.len = sastart ;
+    }
+    if (what & 0x0080)
+    {
+      if (!stralloc_0(&sa)) dienomem() ;
+      switch (cmd)
+      {
+        case 2 :
+          if (!strcmp(sa.s + sastart, "unique")) namesa.s[d] |= 3 ;
+          else if (!strcmp(sa.s + sastart, "multiple")) namesa.s[d] |= 2 ;
+          else strerr_dief6x(3, "in ", namesa.s + d + 1, " line ", linefmt, "invalid !included: argument: ", sa.s + sastart) ;
+          break ;
+        case 1 :
+        case 0 :
+        {
+          int fdhere = open2(".", O_RDONLY | O_DIRECTORY) ;
+          if (fdhere == -1)
+            strerr_dief3sys(111, "in ", namesa.s + d + 1, ": unable to open base directory: ") ;
+          if (cmd & 1)
+          {
+            if (chdir(sa.s + sastart) == -1)
+              strerr_dief6sys(111, "in ", namesa.s + d + 1, " line ", linefmt, ": unable to chdir to ", sa.s + sastart) ;
+            includecwd() ;
+          }
+          else include(sa.s + sastart) ;
+          if (fchdir(fdhere) == -1)
+            strerr_dief4sys(111, "in ", namesa.s + d + 1, ": unable to fchdir back after including ", sa.s + sastart) ;
+          fd_close(fdhere) ;
+          if (buffer_put(buffer_1, "! ", 2) < 2
+           || buffer_puts(buffer_1, linefmt) < 0
+           || buffer_put(buffer_1, " ", 1) < 1
+           || buffer_puts(buffer_1, sa.s + sabase) < 0
+           || buffer_put(buffer_1, "\n", 1) < 1)
+            strerr_diefu1sys(111, "write to stdout") ;
+          break ;
+        }
+      }
+      sa.len = sastart ;
+    }
+    if (c == '\n' && state <= 8) linefmt[uint32_fmt(linefmt, ++line)] = 0 ;
+  }
+  if (state > 8) strerr_dief5x(2, "in ", namesa.s + d + 1, " line ", linefmt, ": syntax error: invalid ! line") ;
+  fd_close(fd) ;
+  sa.len = sabase ;
+  namesa.s[d] &= ~0x04 ;
+}
+
+int main (int argc, char const *const *argv, char const *const *envp)
+{
+  PROG = "tipidee-config-preprocess" ;
+  {
+    subgetopt l = SUBGETOPT_ZERO ;
+    for (;;)
+    {
+      int opt = subgetopt_r(argc, argv, "", &l) ;
+      if (opt == -1) break ;
+      switch (opt)
+      {
+        default : dieusage() ;
+      }
+    }
+    argc -= l.ind ; argv += l.ind ;
+  }
+  if (!argc) dieusage() ;
+
+  include(argv[0]) ;
+  if (!buffer_flush(buffer_1))
+    strerr_diefu1sys(111, "write to stdout") ;
+  return 0 ;
+}
diff --git a/src/config/tipidee-config.c b/src/config/tipidee-config.c
new file mode 100644
index 0000000..be13e39
--- /dev/null
+++ b/src/config/tipidee-config.c
@@ -0,0 +1,135 @@
+/* ISC license. */
+
+#include <string.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <stdio.h>  /* rename() */
+#include <errno.h>
+#include <signal.h>
+
+#include <skalibs/posixplz.h>
+#include <skalibs/types.h>
+#include <skalibs/sgetopt.h>
+#include <skalibs/buffer.h>
+#include <skalibs/strerr.h>
+#include <skalibs/sig.h>
+#include <skalibs/djbunix.h>
+
+#include <tipidee/config.h>
+#include "tipidee-config-internal.h"
+
+#define USAGE "tipidee-config [ -i textfile ] [ -o cdbfile ] [ -m mode ]"
+#define dieusage() strerr_dieusage(100, USAGE)
+
+struct global_s g = GLOBAL_ZERO ;
+
+static pid_t pid = 0 ;
+
+static void sigchld_handler (int sig)
+{
+  (void)sig ;
+  for (;;)
+  {
+    int wstat ;
+    pid_t r = wait_nohang(&wstat) ;
+    if (r == -1 && errno != ECHILD) strerr_diefu1sys(111, "wait") ;
+    else if (r <= 0) break ;
+    else if (r == pid)
+    {
+      if (WIFEXITED(wstat) && !WEXITSTATUS(wstat)) pid = 0 ;
+      else _exit(wait_estatus(wstat)) ;
+    }
+  }
+}
+
+static inline void conf_output (char const *ofile, unsigned int omode)
+{
+  int fdw ;
+  cdbmaker cm = CDBMAKER_ZERO ;
+  size_t olen = strlen(ofile) ;
+  char otmp[olen + 8] ;
+  memcpy(otmp, ofile, olen) ;
+  memcpy(otmp + olen, ":XXXXXX", 8) ;
+  fdw = mkstemp(otmp) ;
+  if (fdw == -1) strerr_diefu3sys(111, "open ", otmp, " for writing") ;
+  if (coe(fdw) == -1)
+  {
+    unlink_void(otmp) ;
+    strerr_diefu2sys(111, "coe ", otmp) ;
+  }
+  if (!cdbmake_start(&cm, fdw))
+  {
+    unlink_void(otmp) ;
+    strerr_diefu2sys(111, "cdmake_start ", otmp) ;
+  }
+  if (!conftree_write(&cm))
+  {
+    unlink_void(otmp) ;
+    strerr_diefu2sys(111, "write config tree into ", otmp) ;
+  }
+  if (!cdbmake_finish(&cm))
+  {
+    unlink_void(otmp) ;
+    strerr_diefu2sys(111, "cdbmake_finish ", otmp) ;
+  }
+  if (fsync(fdw) == -1)
+  {
+    unlink_void(otmp) ;
+    strerr_diefu2sys(111, "fsync ", otmp) ;
+  }
+  if (fchmod(fdw, omode & 0755) == -1)
+  {
+    unlink_void(otmp) ;
+    strerr_diefu2sys(111, "fchmod ", otmp) ;
+  }
+  if (rename(otmp, ofile) == -1)
+  {
+    unlink_void(otmp) ;
+    strerr_diefu4sys(111, "rename ", otmp, " to ", ofile) ;
+  }
+}
+
+int main (int argc, char const *const *argv, char const *const *envp)
+{
+  char const *ifile = "/etc/tipidee.conf" ;
+  char const *ofile = "/etc/tipidee.conf.cdb" ;
+  unsigned int omode = 0644 ;
+
+  PROG = "tipidee-config" ;
+  {
+    subgetopt l = SUBGETOPT_ZERO ;
+    for (;;)
+    {
+      int opt = subgetopt_r(argc, argv, "i:o:m:", &l) ;
+      if (opt == -1) break ;
+      switch (opt)
+      {
+        case 'i' : ifile = l.arg ; break ;
+        case 'o' : ofile = l.arg ; break ;
+        case 'm' : if (!uint0_oscan(l.arg, &omode)) dieusage() ; break ;
+        default : strerr_dieusage(100, USAGE) ;
+      }
+    }
+    argc -= l.ind ; argv += l.ind ;
+  }
+
+  {
+    int fdr ;
+    buffer b ;
+    char buf[4096] ;
+    sig_block(SIGCHLD) ;
+    if (!sig_catch(SIGCHLD, &sigchld_handler))
+      strerr_diefu1sys(111, "install SIGCHLD handler") ;
+    {
+      char const *ppargv[3] = { TIPIDEE_LIBEXECPREFIX "tipidee-config-preprocess", ifile, 0 } ;
+      pid = child_spawn1_pipe(ppargv[0], ppargv, envp, &fdr, 1) ;
+      if (!pid) strerr_diefu2sys(errno == ENOENT ? 127 : 126, "spawn ", ppargv[0]) ;
+    }
+    sig_unblock(SIGCHLD) ;
+    buffer_init(&b, &buffer_read, fdr, buf, 4096) ;
+    conf_lexparse(&b, ifile) ;
+  }
+  conf_defaults() ;
+  conf_output(ofile, omode) ;
+  return 0 ;
+}
diff --git a/src/include/tipidee/body.h b/src/include/tipidee/body.h
new file mode 100644
index 0000000..fc8bd9a
--- /dev/null
+++ b/src/include/tipidee/body.h
@@ -0,0 +1,25 @@
+/* ISC license. */
+
+#ifndef TIPIDEE_BODY_H
+#define TIPIDEE_BODY_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <skalibs/buffer.h>
+#include <skalibs/tai.h>
+#include <skalibs/stralloc.h>
+
+typedef enum tipidee_transfercoding_e tipidee_transfercoding, *tipidee_transfercoding_ref ;
+enum tipidee_transfercoding_e
+{
+  TIPIDEE_TRANSFERCODING_NONE = 0,
+  TIPIDEE_TRANSFERCODING_FIXED,
+  TIPIDEE_TRANSFERCODING_CHUNKED,
+  TIPIDEE_TRANSFERCODING_UNKNOWN
+} ;
+
+extern int tipidee_chunked_read (buffer *, stralloc *, size_t, tain const *, tain *) ;
+#define tipidee_chunked_read_g(b, sa, max, deadline) tipidee_chunked_read(b, sa, max, (deadline), &STAMP)
+
+#endif
diff --git a/src/include/tipidee/conf.h b/src/include/tipidee/conf.h
new file mode 100644
index 0000000..bc66d76
--- /dev/null
+++ b/src/include/tipidee/conf.h
@@ -0,0 +1,44 @@
+/* ISC license. */
+
+#ifndef TIPIDEE_CONF_H
+#define TIPIDEE_CONF_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <skalibs/uint16.h>
+#include <skalibs/cdb.h>
+
+#include <tipidee/uri.h>
+
+#define TIPIDEE_CONF_KEY_MAXLEN 0x1000U
+
+typedef struct tipidee_conf_s tipidee_conf, *tipidee_conf_ref ;
+struct tipidee_conf_s
+{
+  cdb c ;
+} ;
+#define TIPIDEE_CONF_ZERO { .c = CDB_ZERO }
+
+typedef struct tipidee_redirection_s tipidee_redirection, *tipidee_redirection_ref ;
+struct tipidee_redirection_s
+{
+  char const *location ;
+  char const *sub ;
+  uint32_t type : 2 ;
+} ;
+#define TIPIDEE_REDIRECTION_ZERO { .location = 0, .sub = 0, .type = 0 }
+
+extern void tipidee_conf_free (tipidee_conf *) ;
+extern int tipidee_conf_init (tipidee_conf *, char const *) ;
+
+extern int tipidee_conf_get (tipidee_conf const *, char const *, cdb_data *) ;
+extern char const *tipidee_conf_get_string (tipidee_conf const *, char const *) ;
+extern int tipidee_conf_get_uint32 (tipidee_conf const *, char const *, uint32_t *) ;
+extern unsigned int tipidee_conf_get_argv (tipidee_conf const *, char const *, char const **, unsigned int, size_t *) ;
+
+extern char const *tipidee_conf_get_docroot (tipidee_conf const *, tipidee_uri const *, uint16_t) ;
+extern int tipidee_conf_get_redirection (tipidee_conf const *, char const *, size_t, tipidee_redirection *) ;
+extern char const *tipidee_conf_get_content_type (tipidee_conf const *, char const *) ;
+
+#endif
diff --git a/src/include/tipidee/headers.h b/src/include/tipidee/headers.h
new file mode 100644
index 0000000..420b4c9
--- /dev/null
+++ b/src/include/tipidee/headers.h
@@ -0,0 +1,38 @@
+/* ISC license. */
+
+#ifndef TIPIDEE_HEADERS_H
+#define TIPIDEE_HEADERS_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <skalibs/disize.h>
+#include <skalibs/buffer.h>
+#include <skalibs/tai.h>
+#include <skalibs/avltreen.h>
+
+#define TIPIDEE_HEADERS_MAX 32
+
+typedef struct tipidee_headers_s tipidee_headers, *tipidee_headers_ref ;
+struct tipidee_headers_s
+{
+  char *buf ;
+  size_t max ;
+  size_t len ;
+  disize list[TIPIDEE_HEADERS_MAX] ;
+  avltreen map ;
+  uint32_t map_freelist[TIPIDEE_HEADERS_MAX] ;
+  avlnode map_storage[TIPIDEE_HEADERS_MAX] ;
+  uint32_t n ;
+} ;
+
+extern void tipidee_headers_init (tipidee_headers *, char *, size_t) ;
+
+extern int tipidee_headers_parse_nb (buffer *, tipidee_headers *, disize *, uint32_t *) ;
+extern int tipidee_headers_timed_parse (buffer *, tipidee_headers *, tain const *, tain *) ;
+#define tipidee_headers_timed_parse_g(b, hdr, deadline) tipidee_headers_timed_parse(b, hdr, (deadline), &STAMP)
+
+extern char const *tipidee_headers_search (tipidee_headers const *, char const *) ;
+extern ssize_t tipidee_headers_get_content_length (tipidee_headers const *) ;
+
+#endif
diff --git a/src/include/tipidee/method.h b/src/include/tipidee/method.h
new file mode 100644
index 0000000..05123e8
--- /dev/null
+++ b/src/include/tipidee/method.h
@@ -0,0 +1,32 @@
+/* ISC license. */
+
+#ifndef TIPIDEE_METHOD_H
+#define TIPIDEE_METHOD_H
+
+typedef enum tipidee_method_e tipidee_method, *tipidee_method_ref ;
+enum tipidee_method_e
+{
+  TIPIDEE_METHOD_GET = 0,
+  TIPIDEE_METHOD_HEAD,
+  TIPIDEE_METHOD_OPTIONS,
+  TIPIDEE_METHOD_POST,
+  TIPIDEE_METHOD_PUT,
+  TIPIDEE_METHOD_DELETE,
+  TIPIDEE_METHOD_TRACE,
+  TIPIDEE_METHOD_CONNECT,
+  TIPIDEE_METHOD_PRI,
+  TIPIDEE_METHOD_UNKNOWN
+} ;
+
+typedef struct tipidee_method_conv_s tipidee_method_conv, *tipidee_method_conv_ref ;
+struct tipidee_method_conv_s
+{
+  tipidee_method num ;
+  char const *str ;
+} ;
+
+extern tipidee_method_conv const *tipidee_method_conv_table ;
+extern char const *tipidee_method_tostr (tipidee_method) ;
+extern tipidee_method tipidee_method_tonum (char const *) ;
+
+#endif
diff --git a/src/include/tipidee/response.h b/src/include/tipidee/response.h
new file mode 100644
index 0000000..e0a6177
--- /dev/null
+++ b/src/include/tipidee/response.h
@@ -0,0 +1,36 @@
+/* ISC license. */
+
+#ifndef TIPIDEE_RESPONSE_H
+#define TIPIDEE_RESPONSE_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <skalibs/gccattributes.h>
+#include <skalibs/buffer.h>
+#include <skalibs/strerr.h>
+#include <skalibs/tai.h>
+
+#include <tipidee/rql.h>
+
+typedef struct tipidee_response_header_builtin_s tipidee_response_header_builtin, *tipidee_response_header_builtin_ref ;
+struct tipidee_response_header_builtin_s
+{
+  char const *key ;
+  char const *value ;
+} ;
+
+extern size_t tipidee_response_status (buffer *, tipidee_rql const *, unsigned int, char const *) ;
+#define tipidee_response_status_line(b, rql, line) tipidee_response_status(b, rql, 0, (line))
+
+extern size_t tipidee_response_header_date_fmt (char *, size_t, tain const *) ;
+#define tipidee_response_header_date_fmt_g(buf, max) tipidee_response_header_date_fmt(buf, (max), &STAMP)
+extern size_t tipidee_response_header_common_put (buffer *, uint32_t, tain const *) ;
+#define tipidee_response_header_common_put_g(b, options) tipidee_response_header_common_put(b, (options), &STAMP)
+
+extern size_t tipidee_response_error (buffer *, tipidee_rql const *, char const *, char const *, uint32_t) ;
+
+extern tipidee_response_header_builtin const *tipidee_response_header_builtin_table ;
+extern char const *tipidee_response_header_builtin_search (char const *) ;
+
+#endif
diff --git a/src/include/tipidee/rql.h b/src/include/tipidee/rql.h
new file mode 100644
index 0000000..471c4ad
--- /dev/null
+++ b/src/include/tipidee/rql.h
@@ -0,0 +1,31 @@
+/* ISC license. */
+
+#ifndef TIPIDEE_RQL_H
+#define TIPIDEE_RQL_H
+
+#include <skalibs/buffer.h>
+#include <skalibs/tai.h>
+
+#include <tipidee/method.h>
+#include <tipidee/uri.h>
+
+typedef struct tipidee_rql_s tipidee_rql, *tipidee_rql_ref ;
+struct tipidee_rql_s
+{
+  tipidee_method m ;
+  unsigned int http_major ;
+  unsigned int http_minor ;
+  tipidee_uri uri ;
+} ;
+#define TIPIDEE_RQL_ZERO \
+{ \
+  .m = TIPIDEE_METHOD_UNKNOWN, \
+  .http_major = 0, \
+  .http_minor = 0, \
+  .uri = TIPIDEE_URI_ZERO \
+}
+
+extern int tipidee_rql_read (buffer *, char *, size_t, size_t *, tipidee_rql *, tain const *, tain *) ;
+#define tipidee_rql_read_g(b, buf, max, w, rql, deadline) tipidee_rql_read(b, buf, max, w, rql, (deadline), &STAMP)
+
+#endif
diff --git a/src/include/tipidee/tipidee.h b/src/include/tipidee/tipidee.h
new file mode 100644
index 0000000..ddd1348
--- /dev/null
+++ b/src/include/tipidee/tipidee.h
@@ -0,0 +1,15 @@
+/* ISC license. */
+
+#ifndef TIPIDEE_H
+#define TIPIDEE_H
+
+#include <tipidee/body.h>
+#include <tipidee/config.h>
+#include <tipidee/conf.h>
+#include <tipidee/headers.h>
+#include <tipidee/method.h>
+#include <tipidee/response.h>
+#include <tipidee/rql.h>
+#include <tipidee/uri.h>
+
+#endif
diff --git a/src/include/tipidee/uri.h b/src/include/tipidee/uri.h
new file mode 100644
index 0000000..6e5148b
--- /dev/null
+++ b/src/include/tipidee/uri.h
@@ -0,0 +1,31 @@
+/* ISC license. */
+
+#ifndef TIPIDEE_URI_H
+#define TIPIDEE_URI_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+typedef struct tipidee_uri_s tipidee_uri, *tipidee_uri_ref ;
+struct tipidee_uri_s
+{
+  char const *host ;
+  char const *path ;
+  char const *query ;
+  size_t lastslash ;
+  uint16_t port ;
+  uint8_t https : 1 ;
+} ;
+#define TIPIDEE_URI_ZERO \
+{ \
+  .host = 0, \
+  .path = 0, \
+  .query = 0, \
+  .lastslash = 0, \
+  .port = 0, \
+  .https = 0 \
+}
+
+extern size_t tipidee_uri_parse (char *, size_t, char const *, tipidee_uri *) ;
+
+#endif
diff --git a/src/libtipidee/deps-lib/tipidee b/src/libtipidee/deps-lib/tipidee
new file mode 100644
index 0000000..d218af1
--- /dev/null
+++ b/src/libtipidee/deps-lib/tipidee
@@ -0,0 +1,24 @@
+tipidee_chunked_read.o
+tipidee_conf_free.o
+tipidee_conf_get.o
+tipidee_conf_get_argv.o
+tipidee_conf_get_content_type.o
+tipidee_conf_get_redirection.o
+tipidee_conf_get_string.o
+tipidee_conf_get_uint32.o
+tipidee_conf_init.o
+tipidee_headers_get_content_length.o
+tipidee_headers_init.o
+tipidee_headers_parse.o
+tipidee_headers_search.o
+tipidee_method_conv_table.o
+tipidee_method_tonum.o
+tipidee_method_tostr.o
+tipidee_response_error.o
+tipidee_response_header_builtin.o
+tipidee_response_header_common_put.o
+tipidee_response_header_date_fmt.o
+tipidee_response_status.o
+tipidee_rql_read.o
+tipidee_uri_parse.o
+-lskarnet
diff --git a/src/libtipidee/tipidee_chunked_read.c b/src/libtipidee/tipidee_chunked_read.c
new file mode 100644
index 0000000..66d5d80
--- /dev/null
+++ b/src/libtipidee/tipidee_chunked_read.c
@@ -0,0 +1,41 @@
+/* ISC license. */
+
+#include <string.h>
+#include <errno.h>
+
+#include <skalibs/types.h>
+#include <skalibs/stralloc.h>
+#include <skalibs/unix-timed.h>
+
+#include <tipidee/body.h>
+
+#include <skalibs/posixishard.h>
+
+int tipidee_chunked_read (buffer *b, stralloc *sa, size_t maxlen, tain const *deadline, tain *stamp)
+{
+  char line[512] ;
+  for (;;)
+  {
+    size_t chunklen, pos, w = 0 ;
+    ssize_t r = timed_getlnmax(b, line, 512, &w, '\n', deadline, stamp) ;
+    if (r < 0) return 0 ;
+    if (!r) return (errno = EPIPE, 0) ;
+    pos = size_scan(line, &chunklen) ;
+    if (!pos) return (errno = EPROTO, 0) ;
+    if (!memchr("\r\n; \t", line[pos], 5)) return (errno = EPROTO, 0) ;
+    if (!chunklen) break ;
+    if (sa->len + chunklen > maxlen) return (errno = EMSGSIZE, 0) ;
+    if (!stralloc_readyplus(sa, chunklen)) return 0 ;
+    if (buffer_timed_get(b, sa->s + sa->len, chunklen, deadline, stamp) < chunklen) return 0 ;
+    sa->len += chunklen ;
+  }
+  for (;;)
+  {
+    size_t w = 0 ;
+    ssize_t r = timed_getlnmax(b, line, 512, &w, '\n', deadline, stamp) ;
+    if (r < 0) return 0 ;
+    if (!r) return (errno = EPIPE, 0) ;
+    if (w == 1 || (w == 2 && line[0] == '\r')) break ;
+  }
+  return 1 ;
+}
diff --git a/src/libtipidee/tipidee_conf_free.c b/src/libtipidee/tipidee_conf_free.c
new file mode 100644
index 0000000..e708d89
--- /dev/null
+++ b/src/libtipidee/tipidee_conf_free.c
@@ -0,0 +1,10 @@
+/* ISC license. */
+
+#include <skalibs/cdb.h>
+
+#include <tipidee/conf.h>
+
+void tipidee_conf_free (tipidee_conf *conf)
+{
+  cdb_free(&conf->c) ;
+}
diff --git a/src/libtipidee/tipidee_conf_get.c b/src/libtipidee/tipidee_conf_get.c
new file mode 100644
index 0000000..866ec99
--- /dev/null
+++ b/src/libtipidee/tipidee_conf_get.c
@@ -0,0 +1,22 @@
+/* ISC license. */
+
+#include <errno.h>
+#include <string.h>
+
+#include <skalibs/cdb.h>
+#include <skalibs/lolstdio.h>
+
+#include <tipidee/conf.h>
+
+int tipidee_conf_get (tipidee_conf const *conf, char const *key, cdb_data *data)
+{
+  size_t keylen = strlen(key) ;
+  if (keylen > TIPIDEE_CONF_KEY_MAXLEN) return (errno = EINVAL, 0) ;
+  LOLDEBUG("tipidee_conf_get: looking up %s", key) ;
+  switch (cdb_find(&conf->c, data, key, keylen))
+  {
+    case -1 : return (errno = EILSEQ, 0) ;
+    case 0 : return (errno = ENOENT, 0) ;
+    default : return 1 ;
+  }
+}
diff --git a/src/libtipidee/tipidee_conf_get_argv.c b/src/libtipidee/tipidee_conf_get_argv.c
new file mode 100644
index 0000000..0dd016e
--- /dev/null
+++ b/src/libtipidee/tipidee_conf_get_argv.c
@@ -0,0 +1,32 @@
+/* ISC license. */
+
+#include <errno.h>
+#include <string.h>
+
+#include <skalibs/cdb.h>
+
+#include <tipidee/conf.h>
+
+#include <skalibs/posixishard.h>
+
+unsigned int tipidee_conf_get_argv (tipidee_conf const *conf, char const *key, char const **argv, unsigned int max, size_t *maxlen)
+{
+  cdb_data data ;
+  size_t curlen = 0 ;
+  unsigned int n = 0, pos = 0 ;
+  if (!tipidee_conf_get(conf, key, &data)) return 0 ;
+  if (data.s[data.len-1]) return (errno = EPROTO, 0) ;
+  while (pos < data.len)
+  {
+    size_t len ;
+    if (n >= max) return (errno = E2BIG, 0) ;
+    argv[n++] = data.s + pos ;
+    len = strlen(data.s + pos) ;
+    if (len > curlen) curlen = len ;
+    pos += len + 1 ;
+  }
+  if (n >= max) return (errno = E2BIG, 0) ;
+  argv[n++] = 0 ;
+  if (maxlen) *maxlen = curlen ;
+  return n ;
+}
diff --git a/src/libtipidee/tipidee_conf_get_content_type.c b/src/libtipidee/tipidee_conf_get_content_type.c
new file mode 100644
index 0000000..7ee8866
--- /dev/null
+++ b/src/libtipidee/tipidee_conf_get_content_type.c
@@ -0,0 +1,22 @@
+/* ISC license. */
+
+#include <errno.h>
+#include <string.h>
+
+#include <tipidee/conf.h>
+
+char const *tipidee_conf_get_content_type (tipidee_conf const *conf, char const *res)
+{
+  char const *ext = strrchr(res, '.') ;
+  if (ext && !strchr(ext, '/'))
+  {
+    char const *value = 0 ;
+    size_t extlen = strlen(ext+1) ;
+    char key[3 + extlen] ;
+    key[0] = 'T' ; key[1] = ':' ;
+    memcpy(key + 2, ext + 1, extlen + 1) ;
+    value = tipidee_conf_get_string(conf, key) ;
+    if (value || errno != ENOENT) return value ;
+  }
+  return "application/octet-stream" ;
+}
diff --git a/src/libtipidee/tipidee_conf_get_redirection.c b/src/libtipidee/tipidee_conf_get_redirection.c
new file mode 100644
index 0000000..62ada34
--- /dev/null
+++ b/src/libtipidee/tipidee_conf_get_redirection.c
@@ -0,0 +1,35 @@
+/* ISC license. */
+
+#include <errno.h>
+#include <string.h>
+
+#include <tipidee/conf.h>
+
+#include <skalibs/posixishard.h>
+
+int tipidee_conf_get_redirection (tipidee_conf const *conf, char const *res, size_t docrootlen, tipidee_redirection *r)
+{
+  size_t reslen = strlen(res) ;
+  size_t l = 2 + reslen ;
+  char const *v = 0 ;
+  char key[3 + reslen] ;
+  key[0] = 'R' ; key[1] = ':' ;
+  memcpy(key + 2, res, reslen) ;
+  key[2 + reslen] = '/' ;
+  errno = ENOENT ;
+  while (!v)
+  {
+    if (errno != ENOENT) return -1 ;
+    while (l > 2  + docrootlen && key[l] != '/') l-- ;
+    if (l <= 2 + docrootlen) break ;
+    key[l--] = 0 ;
+    key[0] = 'r' ;
+    v = tipidee_conf_get_string(conf, key) ;
+  }
+  if (!v) return 0 ;
+  if (v[0] < '@' || v[0] > 'C') return (errno = EPROTO, -1) ;
+  r->type = v[0] & ~'@' ;
+  r->location = v+1 ;
+  r->sub = res + l - 2 ;
+  return 1 ;
+}
diff --git a/src/libtipidee/tipidee_conf_get_string.c b/src/libtipidee/tipidee_conf_get_string.c
new file mode 100644
index 0000000..0ea93cd
--- /dev/null
+++ b/src/libtipidee/tipidee_conf_get_string.c
@@ -0,0 +1,17 @@
+/* ISC license. */
+
+#include <errno.h>
+
+#include <skalibs/cdb.h>
+
+#include <tipidee/conf.h>
+
+#include <skalibs/posixishard.h>
+
+char const *tipidee_conf_get_string (tipidee_conf const *conf, char const *key)
+{
+  cdb_data data ;
+  if (!tipidee_conf_get(conf, key, &data)) return 0 ;
+  if (data.s[data.len-1]) { errno = EPROTO ; return 0 ; }
+  return data.s ;
+}
diff --git a/src/libtipidee/tipidee_conf_get_uint32.c b/src/libtipidee/tipidee_conf_get_uint32.c
new file mode 100644
index 0000000..ad8b21b
--- /dev/null
+++ b/src/libtipidee/tipidee_conf_get_uint32.c
@@ -0,0 +1,19 @@
+/* ISC license. */
+
+#include <errno.h>
+
+#include <skalibs/uint32.h>
+#include <skalibs/cdb.h>
+
+#include <tipidee/conf.h>
+
+#include <skalibs/posixishard.h>
+
+int tipidee_conf_get_uint32 (tipidee_conf const *conf, char const *key, uint32_t *value)
+{
+  cdb_data data ;
+  if (!tipidee_conf_get(conf, key, &data)) return 0 ;
+  if (data.len != 4) return (errno = EPROTO, 0) ;
+  uint32_unpack_big(data.s, value) ;
+  return 1 ;
+}
diff --git a/src/libtipidee/tipidee_conf_init.c b/src/libtipidee/tipidee_conf_init.c
new file mode 100644
index 0000000..3d2e119
--- /dev/null
+++ b/src/libtipidee/tipidee_conf_init.c
@@ -0,0 +1,10 @@
+/* ISC license. */
+
+#include <skalibs/cdb.h>
+
+#include <tipidee/conf.h>
+
+int tipidee_conf_init (tipidee_conf *conf, char const *file)
+{
+  return cdb_init(&conf->c, file) ;
+}
diff --git a/src/libtipidee/tipidee_headers_get_content_length.c b/src/libtipidee/tipidee_headers_get_content_length.c
new file mode 100644
index 0000000..0b29ece
--- /dev/null
+++ b/src/libtipidee/tipidee_headers_get_content_length.c
@@ -0,0 +1,17 @@
+/* ISC license. */
+
+#include <stddef.h>
+#include <limits.h>
+
+#include <skalibs/types.h>
+
+#include <tipidee/headers.h>
+
+ssize_t tipidee_headers_get_content_length (tipidee_headers const *hdr)
+{
+  size_t n ;
+  char const *x = tipidee_headers_search(hdr, "Content-Length") ;
+  if (!x) return 0 ;
+  if (!size0_scan(x, &n) || n > SSIZE_MAX) return -1 ;
+  return n ;
+}
diff --git a/src/libtipidee/tipidee_headers_init.c b/src/libtipidee/tipidee_headers_init.c
new file mode 100644
index 0000000..6c2d336
--- /dev/null
+++ b/src/libtipidee/tipidee_headers_init.c
@@ -0,0 +1,29 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <strings.h>
+
+#include <skalibs/avltreen.h>
+
+#include <tipidee/headers.h>
+
+static void *tipidee_headers_dtok (uint32_t d, void *data)
+{
+  tipidee_headers *hdr = data ;
+  return hdr->buf + hdr->list[d].left ;
+}
+
+static int tipidee_headers_cmp (void const *a, void const *b, void *data)
+{
+  (void)data ;
+  return strcasecmp(a, b) ;
+}
+
+void tipidee_headers_init (tipidee_headers *hdr, char *buf, size_t max)
+{
+  hdr->buf = buf ;
+  hdr->max = max ;
+  hdr->len = 0 ;
+  hdr->n = 0 ;
+  avltreen_init(&hdr->map, hdr->map_storage, hdr->map_freelist, TIPIDEE_HEADERS_MAX, &tipidee_headers_dtok, &tipidee_headers_cmp, hdr) ;
+}
diff --git a/src/libtipidee/tipidee_headers_parse.c b/src/libtipidee/tipidee_headers_parse.c
new file mode 100644
index 0000000..57108bb
--- /dev/null
+++ b/src/libtipidee/tipidee_headers_parse.c
@@ -0,0 +1,210 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <string.h>
+#include <strings.h>
+#include <errno.h>
+
+#include <skalibs/bytestr.h>
+#include <skalibs/buffer.h>
+#include <skalibs/error.h>
+#include <skalibs/avltreen.h>
+#include <skalibs/unix-timed.h>
+// #include <skalibs/lolstdio.h>
+
+#include <tipidee/headers.h>
+
+/*
+
+Reads header lines, separates into \0-terminated keys+values.
+key is at hdr->buf + hdr->list[i].left
+value is at hdr->buf + hdr->list[i].right 
+Compresses linear whitespace.
+Does not unquote strings/comments in values. 
+
+
+st\ev	0	1	2	3	4	5	6	7
+	CTL	CR	LF	LWS	:	special normal	8bit
+
+00							kp
+START	X	END?	END	X	X	X	K	X
+
+01
+END?	X	X	END	X	X	X	X	X
+
+02					zv		p
+K	X	X	X	X	V1	X	K	X
+
+03					p	p	p	p
+V1	X	V1??	V1?	V1	V	V	V	V
+
+04
+V1??	X	X	V1?	X	X	X	X	X
+
+05		zn	zn				znkp
+V1?	X	END?	END	V1	X	X	K	X
+
+06		s	s	s	p	p	p	p
+V	X	V2??	V2?	V2	V	V	V	V
+
+07					p	p	p	p
+V2	X	V2??	V2?	V2	V	V	V	V
+
+08
+V2??	X	X	V2?	X	X	X	X	X
+
+09		mzn	mzn				mznkp
+V2?	X	END?	END	V2	X	X	K	X
+
+END = 0a, X = 0b
+
+0x4000 s: write space
+0x2000 m: go back one char
+0x1000 z: write \0
+0x0800 n: cut key/value pair, prepare next
+0x0400 k: start of key
+0x0200 v: start of value
+0x0100 p: write current char
+
+states: 4 bits, actions: 7 bits
+
+*/
+
+
+struct tainp_s
+{
+  tain const *deadline ;
+  tain *stamp ;
+} ;
+
+typedef int get1_func (buffer *, char *, struct tainp_s *) ;
+typedef get1_func *get1_func_ref ;
+
+static int get1_timed (buffer *b, char *c, struct tainp_s *d)
+{
+  return buffer_timed_get(b, c, 1, d->deadline, d->stamp) ;
+}
+
+static int get1_notimed (buffer *b, char *c, struct tainp_s *data)
+{
+  (void)data ;
+  return buffer_get(b, c, 1) == 1 ;
+}
+
+static uint8_t cclass (char c)
+{
+  static uint8_t const ctable[128] = "00000000032001000000000000000000365666665566566566666666664565655666666666666666666666666665556666666666666666666666666666656560" ;
+  return c & 0x80 ? 7 : ctable[(uint8_t)c] - '0' ;
+}
+
+static int needs_processing (char const *s)
+{
+  if (!strcasecmp(s, "Set-Cookie")) return 0 ;
+  if (str_start(s, "X-")) return 0 ;
+  return 1 ;
+}
+
+static int tipidee_headers_parse_with (buffer *b, tipidee_headers *hdr, get1_func_ref next, struct tainp_s *data, disize *header, uint32_t *state)
+{
+  static uint16_t const table[10][8] =
+  {
+    { 0x000b, 0x0001, 0x000a, 0x000b, 0x000b, 0x000b, 0x0502, 0x000b },
+    { 0x000b, 0x000b, 0x000a, 0x000b, 0x000b, 0x000b, 0x000b, 0x000b },
+    { 0x000b, 0x000b, 0x000b, 0x000b, 0x1203, 0x000b, 0x0102, 0x000b },
+    { 0x000b, 0x0004, 0x0005, 0x0003, 0x0106, 0x0106, 0x0106, 0x0106 },
+    { 0x000b, 0x000b, 0x0005, 0x000b, 0x000b, 0x000b, 0x000b, 0x000b },
+    { 0x000b, 0x1801, 0x180a, 0x0003, 0x000b, 0x000b, 0x1d02, 0x000b },
+    { 0x000b, 0x4008, 0x4009, 0x4007, 0x0106, 0x0106, 0x0106, 0x0106 },
+    { 0x000b, 0x0008, 0x0009, 0x0007, 0x0106, 0x0106, 0x0106, 0x0106 },
+    { 0x000b, 0x000b, 0x0009, 0x000b, 0x000b, 0x000b, 0x000b, 0x000b },
+    { 0x000b, 0x3801, 0x380a, 0x0007, 0x000b, 0x000b, 0x3d02, 0x000b },
+  } ;
+  while (*state < 0x0a)
+  {
+    uint16_t c ;
+    char cur ;
+    if (!(*next)(b, &cur, data))
+      return errno == ETIMEDOUT ? 408 : error_isagain(errno) ? -2 : -1 ;
+    c = table[*state][cclass(cur)] ;
+/*
+   {
+
+      char s[2] = { cur, 0 } ;
+      LOLDEBUG("tipidee_headers_parse_with: state %hhu, event %s, newstate %hhu, actions %s%s%s%s%s%s%s",
+        *state,
+        cur == '\r' ? "\\r" : cur == '\n' ? "\\n" : s,
+        c & 0x0f,
+        c & 0x4000 ? "s" : "",
+        c & 0x2000 ? "m" : "",
+        c & 0x1000 ? "z" : "",
+        c & 0x0800 ? "n" : "",
+        c & 0x0400 ? "k" : "",
+        c & 0x0200 ? "v" : "",
+        c & 0x0100 ? "p" : ""
+      ) ;
+    }
+*/
+    *state = c & 0x0f ;
+    if (c & 0x4000) { if (hdr->len >= hdr->max) return 413 ; hdr->buf[hdr->len++] = ' ' ; }
+    if (c & 0x2000) hdr->len-- ;
+    if (c & 0x1000) { if (hdr->len >= hdr->max) return 413 ; hdr->buf[hdr->len++] = 0 ; }
+    if (c & 0x0800)
+    {
+      uint32_t prev ;
+      if (hdr->n >= TIPIDEE_HEADERS_MAX) return 413 ;
+      hdr->list[hdr->n] = *header ;
+      if (needs_processing(hdr->buf + header->left))
+      {
+//      LOLDEBUG("tipidee_headers_parse_with: n: adding header %u - key %zu (%s), value %zu (%s)", hdr->n, header->left, hdr->buf + header->left, header->right, hdr->buf + header->right) ;
+        if (avltreen_search(&hdr->map, hdr->buf + header->left, &prev))
+        {
+          size_t start = hdr->list[prev+1].left ;
+          if (prev+1 == hdr->n)
+          {
+            hdr->buf[start - 1] = ',' ;
+            hdr->buf[start] = ' ' ;
+            memcpy(hdr->buf + start + 1, hdr->buf + header->right, hdr->len - header->right) ;
+          }
+          else
+          {
+            size_t len = header->left - start ;
+            size_t offset = hdr->len - header->right + 1 ;
+            char tmp[len] ;
+            memcpy(tmp, hdr->buf + start, len) ;
+            hdr->buf[start - 1] = ',' ;
+            hdr->buf[start] = ' ' ;
+            memcpy(hdr->buf + start + 1, hdr->buf + header->right, hdr->len - header->right) ;
+            memcpy(hdr->buf + start + offset, tmp, len) ;
+            for (uint32_t i = prev + 1 ; i < hdr->n ; i++)
+            {
+              hdr->list[i].left += offset ;
+              hdr->list[i].right += offset ;
+            }
+          }
+          hdr->len -= header->right - header->left - 1 ;
+          hdr->n-- ;
+        }
+        else if (!avltreen_insert(&hdr->map, hdr->n)) return 500 ;
+      }
+      hdr->n++ ;
+    }
+    if (c & 0x0400) header->left = hdr->len ;
+    if (c & 0x0200) header->right = hdr->len ;
+    if (c & 0x0100) { if (hdr->len >= hdr->max) return 413 ; hdr->buf[hdr->len++] = cur ; }
+  }
+  if (*state > 0x0a) return 400 ;
+  return 0 ;
+}
+
+int tipidee_headers_timed_parse (buffer *b, tipidee_headers *hdr, tain const *deadline, tain *stamp)
+{
+  struct tainp_s d = { .deadline = deadline, .stamp = stamp } ;
+  disize header = DISIZE_ZERO ;
+  uint32_t state = 0 ;
+  return tipidee_headers_parse_with(b, hdr, &get1_timed, &d, &header, &state) ;
+}
+
+int tipidee_headers_parse_nb (buffer *b, tipidee_headers *hdr, disize *header, uint32_t *state)
+{
+  return tipidee_headers_parse_with(b, hdr, &get1_notimed, 0, header, state) ;
+}
diff --git a/src/libtipidee/tipidee_headers_search.c b/src/libtipidee/tipidee_headers_search.c
new file mode 100644
index 0000000..553ef65
--- /dev/null
+++ b/src/libtipidee/tipidee_headers_search.c
@@ -0,0 +1,13 @@
+/* ISC license. */
+
+#include <stdint.h>
+
+#include <skalibs/avltreen.h>
+
+#include <tipidee/headers.h>
+
+char const *tipidee_headers_search (tipidee_headers const *hdr, char const *key)
+{
+  uint32_t i ;
+  return avltreen_search(&hdr->map, key, &i) ? hdr->buf + hdr->list[i].right : 0 ;
+}
diff --git a/src/libtipidee/tipidee_method_conv_table.c b/src/libtipidee/tipidee_method_conv_table.c
new file mode 100644
index 0000000..892e5b5
--- /dev/null
+++ b/src/libtipidee/tipidee_method_conv_table.c
@@ -0,0 +1,19 @@
+/* ISC license. */
+
+#include <tipidee/method.h>
+
+static tipidee_method_conv const table[] =
+{
+  { .num = TIPIDEE_METHOD_GET, .str = "GET" },
+  { .num = TIPIDEE_METHOD_HEAD, .str = "HEAD" },
+  { .num = TIPIDEE_METHOD_OPTIONS, .str = "OPTIONS" },
+  { .num = TIPIDEE_METHOD_POST, .str = "POST" },
+  { .num = TIPIDEE_METHOD_PUT, .str = "PUT" },
+  { .num = TIPIDEE_METHOD_DELETE, .str = "DELETE" },
+  { .num = TIPIDEE_METHOD_TRACE, .str = "TRACE" },
+  { .num = TIPIDEE_METHOD_CONNECT, .str = "CONNECT" },
+  { .num = TIPIDEE_METHOD_PRI, .str = "PRI" },
+  { .num = TIPIDEE_METHOD_UNKNOWN, .str = 0 }
+} ;
+
+tipidee_method_conv const *tipidee_method_conv_table = table ;
diff --git a/src/libtipidee/tipidee_method_tonum.c b/src/libtipidee/tipidee_method_tonum.c
new file mode 100644
index 0000000..7c88e45
--- /dev/null
+++ b/src/libtipidee/tipidee_method_tonum.c
@@ -0,0 +1,12 @@
+/* ISC license. */
+
+#include <string.h>
+
+#include <tipidee/method.h>
+
+tipidee_method tipidee_method_tonum (char const *s)
+{
+  tipidee_method_conv const *p = tipidee_method_conv_table ;
+  for (; p->str ; p++) if (!strcmp(s, p->str)) break ;
+  return p->num ;
+}
diff --git a/src/libtipidee/tipidee_method_tostr.c b/src/libtipidee/tipidee_method_tostr.c
new file mode 100644
index 0000000..8a3831d
--- /dev/null
+++ b/src/libtipidee/tipidee_method_tostr.c
@@ -0,0 +1,8 @@
+/* ISC license. */
+
+#include <tipidee/method.h>
+
+char const *tipidee_method_tostr (tipidee_method m)
+{
+  return m < TIPIDEE_METHOD_UNKNOWN ? tipidee_method_conv_table[m].str : 0 ;
+}
diff --git a/src/libtipidee/tipidee_response_error.c b/src/libtipidee/tipidee_response_error.c
new file mode 100644
index 0000000..e2687a4
--- /dev/null
+++ b/src/libtipidee/tipidee_response_error.c
@@ -0,0 +1,41 @@
+/* ISC license. */
+
+#include <stddef.h>
+
+#include <skalibs/types.h>
+#include <skalibs/buffer.h>
+
+#include <tipidee/method.h>
+#include <tipidee/rql.h>
+#include <tipidee/response.h>
+
+size_t tipidee_response_error (buffer *b, tipidee_rql const *rql, char const *rsl, char const *text, uint32_t options)
+{
+  size_t n = 0 ;
+  static char const txt1[] = "<html>\n<head><title>" ;
+  static char const txt2[] = "</title></head>\n<body>\n<h1> " ;
+  static char const txt3[] = " </h1>\n<p>\n" ;
+  static char const txt4[] = "\n</p>\n</body>\n</html>\n" ;
+  n += tipidee_response_status_line(b, rql, rsl) ;
+  n += tipidee_response_header_common_put_g(buffer_1, options) ;
+  if (!(options & 2))
+  {
+    char fmt[SIZE_FMT] ;
+    n += buffer_putsnoflush(buffer_1, "Content-Type: text/html; charset=UTF-8\r\n") ;
+    n += buffer_putsnoflush(buffer_1, "Content-Length: ") ;
+    n += buffer_putnoflush(buffer_1, fmt, size_fmt(fmt, sizeof(txt1) + sizeof(txt2) + sizeof(txt3) + sizeof(txt4) - 4 + 2 * strlen(rsl) + strlen(text))) ;
+    n += buffer_putnoflush(buffer_1, "\r\n", 2) ;
+  }
+  n += buffer_putnoflush(buffer_1, "\r\n", 2) ;
+  if (rql->m != TIPIDEE_METHOD_HEAD)
+  {
+    n += buffer_putsnoflush(buffer_1, txt1) ;
+    n += buffer_putsnoflush(buffer_1, rsl) ;
+    n += buffer_putsnoflush(buffer_1, txt2) ;
+    n += buffer_putsnoflush(buffer_1, rsl) ;
+    n += buffer_putsnoflush(buffer_1, txt3) ;
+    n += buffer_putsnoflush(buffer_1, text) ;
+    n += buffer_putsnoflush(buffer_1, txt4) ;
+  }
+  return n ;
+}
diff --git a/src/libtipidee/tipidee_response_header_builtin.c b/src/libtipidee/tipidee_response_header_builtin.c
new file mode 100644
index 0000000..0125cb8
--- /dev/null
+++ b/src/libtipidee/tipidee_response_header_builtin.c
@@ -0,0 +1,40 @@
+/* ISC license. */
+
+#include <string.h>
+#include <stdlib.h>
+
+#include <tipidee/config.h>
+#include <tipidee/response.h>
+
+static tipidee_response_header_builtin const tipidee_response_header_builtin_table_[] =
+{
+  { .key = "Accept-Ranges", .value = "none" },
+  { .key = "Cache-Control", .value = "private" },
+  { .key = "Content-Security-Policy", .value = "default-src 'self'; style-src 'self' 'unsafe-inline';" },
+  { .key = "Referrer-Policy", .value = "no-referrer-when-downgrade" },
+  { .key = "Server", .value = "tipidee/" TIPIDEE_VERSION },
+  { .key = "Vary", .value = "Accept-Encoding" },
+  { .key = "X-Content-Type-Options", .value = "nosniff" },
+  { .key = "X-Frame-Options", .value = "DENY" },
+  { .key = "X-XSS-Protection", .value = "1; mode=block" },
+  { .key = 0, .value = 0 },
+} ;
+
+tipidee_response_header_builtin const *tipidee_response_header_builtin_table = tipidee_response_header_builtin_table_ ;
+
+static int tipidee_response_header_builtin_cmp (void const *a, void const *b)
+{
+  return strcmp((char const *)a, ((tipidee_response_header_builtin const *)b)->key) ;
+}
+
+char const *tipidee_response_header_builtin_search (char const *key)
+{
+  tipidee_response_header_builtin const *p = bsearch(
+    key,
+    tipidee_response_header_builtin_table_,
+    sizeof(tipidee_response_header_builtin_table_) / sizeof(tipidee_response_header_builtin) - 1,
+    sizeof(tipidee_response_header_builtin),
+    &tipidee_response_header_builtin_cmp) ;
+  return p ? p->value : 0 ;
+}
+
diff --git a/src/libtipidee/tipidee_response_header_common_put.c b/src/libtipidee/tipidee_response_header_common_put.c
new file mode 100644
index 0000000..8352ba9
--- /dev/null
+++ b/src/libtipidee/tipidee_response_header_common_put.c
@@ -0,0 +1,23 @@
+/* ISC license. */
+
+#include <stdint.h>
+
+#include <skalibs/buffer.h>
+
+#include <tipidee/config.h>
+#include <tipidee/response.h>
+
+size_t tipidee_response_header_common_put (buffer *b, uint32_t options, tain const *stamp)
+{
+  char fmt[128] ;
+  size_t m = buffer_putnoflush(b, fmt, tipidee_response_header_date_fmt(fmt, 128, stamp)) ;
+  for (tipidee_response_header_builtin const *p = tipidee_response_header_builtin_table ; p->key ; p++)
+  {
+    m += buffer_putsnoflush(b, p->key) ;
+    m += buffer_putnoflush(b, ": ", 2) ;
+    m += buffer_putsnoflush(b, p->value) ;
+    m += buffer_putnoflush(b, "\r\n", 2) ;
+  }
+  if (options & 1) m += buffer_putsnoflush(b, "Connection: close\r\n") ;
+  return m ;
+}
diff --git a/src/libtipidee/tipidee_response_header_date_fmt.c b/src/libtipidee/tipidee_response_header_date_fmt.c
new file mode 100644
index 0000000..df19673
--- /dev/null
+++ b/src/libtipidee/tipidee_response_header_date_fmt.c
@@ -0,0 +1,24 @@
+/* ISC license. */
+
+#include <string.h>
+#include <time.h>
+
+#include <skalibs/tai.h>
+#include <skalibs/djbtime.h>
+
+#include <tipidee/response.h>
+
+size_t tipidee_response_header_date_fmt (char *s, size_t max, tain const *stamp)
+{
+  size_t m = 0, l ;
+  struct tm tm ;
+  if (m + 6 > max) return 0 ;
+  if (!localtm_from_tai(&tm, tain_secp(stamp), 0)) return 0 ;
+  memcpy(s, "Date: ", 6) ; m += 6 ;
+  l = strftime(s + m, max - m, "%a, %d %b %Y %T GMT", &tm) ;
+  if (!l) return 0 ;
+  m += l ;
+  if (m + 2 > max) return 0 ;
+  s[m++] = '\r' ; s[m++] = '\n' ;
+  return m ;
+}
diff --git a/src/libtipidee/tipidee_response_status.c b/src/libtipidee/tipidee_response_status.c
new file mode 100644
index 0000000..aedec39
--- /dev/null
+++ b/src/libtipidee/tipidee_response_status.c
@@ -0,0 +1,27 @@
+/* ISC license. */
+
+#include <skalibs/types.h>
+#include <skalibs/buffer.h>
+
+#include <tipidee/response.h>
+
+size_t tipidee_response_status (buffer *b, tipidee_rql const *rql, unsigned int status, char const *line)
+{
+  size_t n = 0 ;
+  char fmt[UINT_FMT] ;
+  n += buffer_putnoflush(b, "HTTP/", 5) ;
+  n += buffer_putnoflush(b, fmt, uint_fmt(fmt, rql->http_major ? rql->http_major : 1)) ;
+  n += buffer_putnoflush(b, ".", 1) ;
+  n += buffer_putnoflush(b, fmt, uint_fmt(fmt, rql->http_major ? rql->http_minor : 1)) ;
+  n += buffer_putnoflush(b, " ", 1) ;
+  if (status)
+  {
+    char fmt[UINT_FMT] ;
+    size_t m = uint_fmt(fmt, status) ;
+    n += buffer_putnoflush(b, fmt, m) ;
+    n += buffer_putnoflush(b, " ", 1) ;
+  }
+  n += buffer_putsnoflush(b, line) ;
+  n += buffer_putnoflush(b, "\r\n", 2) ;
+  return n ;
+}
diff --git a/src/libtipidee/tipidee_rql_read.c b/src/libtipidee/tipidee_rql_read.c
new file mode 100644
index 0000000..f3508cf
--- /dev/null
+++ b/src/libtipidee/tipidee_rql_read.c
@@ -0,0 +1,85 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <string.h>
+#include <strings.h>
+
+#include <skalibs/types.h>
+#include <skalibs/bytestr.h>
+#include <skalibs/buffer.h>
+#include <skalibs/unix-timed.h>
+// #include <skalibs/lolstdio.h>
+
+#include <tipidee/method.h>
+#include <tipidee/uri.h>
+#include <tipidee/rql.h>
+
+static inline uint8_t tokenize_cclass (char c)
+{
+  switch (c)
+  {
+    case '\0' : return 0 ;
+    case ' ' :
+    case '\t' : return 1 ;
+    default : return 2 ;
+  }
+}
+
+static inline int rql_tokenize (char *s, size_t *tab)
+{
+  uint8_t const table[2][3] =
+  {
+    { 0x02, 0x00, 0x11 },
+    { 0x02, 0x20, 0x01 }
+  } ;
+  size_t i = 0 ;
+  unsigned int tokens = 0 ;
+  uint8_t state = 0 ;
+  for (; state < 2 ; i++)
+  {
+    uint8_t c = table[state][tokenize_cclass(s[i])] ;
+    state = c & 3 ;
+    if (c & 0x10)
+    {
+      if (tokens >= 3) goto err ;
+      tab[tokens++] = i ;
+    }
+    if (c & 0x20) s[i] = 0 ;
+  }
+  return 1 ;
+ err:
+  return 0 ;
+}
+
+static inline int get_version (char const *in, tipidee_rql *rql)
+{
+  size_t l ;
+  if (strncmp(in, "HTTP/", 5)) return 0 ;
+  in += 5 ;
+  l = uint_scan(in, &rql->http_major) ;
+  if (!l) return 0 ;
+  in += l ;
+  if (*in++ != '.') return 0 ;
+  return !!uint0_scan(in, &rql->http_minor) ;
+}
+
+int tipidee_rql_read (buffer *b, char *buf, size_t max, size_t *w, tipidee_rql *rql, tain const *deadline, tain *stamp)
+{
+  size_t pos[3] = { 0 } ;
+  if (timed_getlnmax(b, buf, max, &pos[0], '\n', deadline, stamp) <= 0) return -1 ;
+  buf[--pos[0]] = 0 ;
+  if (buf[pos[0] - 1] == '\r') buf[--pos[0]] = 0 ;
+//  LOLDEBUG("tipidee_rql_read: timed_getlnmax: len is %zu, line is %s", pos[0], buf) ;
+  if (!rql_tokenize(buf, pos)) return 400 ;
+//  LOLDEBUG("tipidee_rql_read: method: %s, version: %s, uri to parse: %s", buf + pos[0], buf + pos[2], buf + pos[1]) ;
+  rql->m = tipidee_method_tonum(buf + pos[0]) ;
+  if (rql->m == TIPIDEE_METHOD_UNKNOWN) return 400 ;
+  if (!get_version(buf + pos[2], rql)) return 400 ;
+  if (rql->m != TIPIDEE_METHOD_OPTIONS || strcmp(buf + pos[1], "*"))
+  {
+    size_t l = tipidee_uri_parse(buf, max, buf + pos[1], &rql->uri) ;
+    if (!l) return 400 ;
+    *w = l ;
+  }
+  return 0 ;
+}
diff --git a/src/libtipidee/tipidee_uri_parse.c b/src/libtipidee/tipidee_uri_parse.c
new file mode 100644
index 0000000..10b0f91
--- /dev/null
+++ b/src/libtipidee/tipidee_uri_parse.c
@@ -0,0 +1,184 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <string.h>
+
+#include <skalibs/uint16.h>
+#include <skalibs/bytestr.h>
+#include <skalibs/fmtscan.h>
+#include <skalibs/lolstdio.h>
+
+#include <tipidee/uri.h>
+
+
+/*
+
+ Decodes an URI.
+ Accepts absolute (http and https) and local, decodes %-encoding up to ? query.
+
+st\ev	0	1	2	3	4	5	6	7	8	9	a	b	c	d	e
+	\0	invalid	%	?	/	:	@	h	t	p	s	0-9	a-f	other	delim
+
+00					Pp
+START	X	X	X	X	PATH?	X	X	H	X	X	X	X	X	X	X
+
+01
+H	X	X	X	X	X	X	X	X	HT	X	X	X	X	X	X
+
+02
+HT	X	X	X	X	X	X	X	X	HTT	X	X	X	X	X	X
+
+03
+HTT	X	X	X	X	X	X	X	X	X	HTTP	X	X	X	X	X
+
+04						
+HTTP	X	X	X	X	X	AUTH	X	X	X	X	HTTPS	X	X	X	X
+
+05						s
+HTTPS	X	X	X	X	X	AUTH	X	X	X	X	X	X	X	X	X
+
+06
+AUTH	X	X	X	X	AUTH/	X	X	X	X	X	X	X	X	X	X
+
+07
+AUTH/	X	X	X	X	HOST	X	X	X	X	X	X	X	X	X	X
+
+08			H	Hp				Hp	Hp	Hp	Hp	Hp	Hp	Hp
+HOST	X	X	HQ	H1	X	X	X	H1	H1	H1	H1	H1	H1	H1	X
+
+09			p									a	a
+HQ	X	X	H1	X	X	X	X	X	X	X	X	HQ1	HQ1	X	X
+
+0a												ab	ab
+HQ1	X	X	X	X	X	X	X	X	X	X	X	H1	H1	X	X
+
+0b	p				zPp	zm		p	p	p	p	p	p	p
+H1	END	X	HQ	H1	PATH	PORT	X	H1	H1	H1	H1	H1	H1	H1	X
+
+0c					Pp							p
+PORT	X	X	X	X	PATH	X	X	X	X	X	X	PORT1	X	X	X
+
+0d	zc				zcPp							p
+PORT1	END	X	X	X	PATH	X	X	X	X	X	X	PORT1	X	X	X
+
+0e	p			zQ	p	p	p	p	p	p	p	p	p	p
+PATH	END	X	Q	QUERY	PATH	PATH	PATH	PATH	PATH	PATH	PATH	PATH	PATH	PATH	X
+
+0f			p									a	a
+Q	X	X	PATH	X	X	X	X	X	X	X	X	Q1	Q1	X	X
+
+10												ab	ab
+Q1	X	X	X	X	X	X	X	X	X	X	X	PATH	PATH	X	X
+
+11	p		p	p	p	p	p	p	p	p	p	p	p	p	p
+QUERY	END	X	QUERY	QUERY	QUERY	QUERY	QUERY	QUERY	QUERY	QUERY	QUERY	QUERY	QUERY	QUERY	QUERY
+
+12	p			zQ		p	p	p	p	p	p	p	p	p
+PATH?	END	X	Q	QUERY	X	PATH	PATH	PATH	PATH	PATH	PATH	PATH	PATH	PATH	X
+
+st\ev	0	1	2	3	4	5	6	7	8	9	a	b	c	d	e
+	\0	invalid	%	?	/	:	@	h	t	p	s	0-9	a-f	other	delim
+
+END = 13, X = 14
+
+0x8000	s	ssl
+0x4000	H	start host
+0x2000	z	print \0
+0x1000	m	mark
+0x0800	c	scan port from mark, reset to mark
+0x0400	P	start path
+0x0200	p	print cur
+0x0100	Q	start query
+0x0080	a	push num
+0x0040	b	decode num, print num, reinit
+
+*/
+
+static inline uint8_t uridecode_cclass (char c)
+{
+  static uint8_t const table[128] = "01111111111111111111111111111111161162>>>>>=>==4;;;;;;;;;;5>>=>36<<<<<<====================>1>==1<<<<<<=7=======9==:8======111=1" ;
+  return c < 0 ? 1 : table[(uint8_t)c] - '0' ;
+}
+
+size_t tipidee_uri_parse (char *out, size_t max, char const *in, tipidee_uri *uri)
+{
+  static uint16_t const table[19][15] =
+  {
+    { 0x0014, 0x0014, 0x0014, 0x0014, 0x0612, 0x0014, 0x0014, 0x0001, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014 },
+    { 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0002, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014 },
+    { 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0003, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014 },
+    { 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0004, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014 },
+    { 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0006, 0x0014, 0x0014, 0x0014, 0x0014, 0x0005, 0x0014, 0x0014, 0x0014, 0x0014 },
+    { 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x8006, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014 },
+    { 0x0014, 0x0014, 0x0014, 0x0014, 0x0007, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014 },
+    { 0x0014, 0x0014, 0x0014, 0x0014, 0x0008, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014 },
+    { 0x0014, 0x0014, 0x4009, 0x420b, 0x0014, 0x0014, 0x0014, 0x420b, 0x420b, 0x420b, 0x420b, 0x420b, 0x420b, 0x420b, 0x0014 },
+    { 0x0014, 0x0014, 0x020b, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x008a, 0x008a, 0x0014, 0x0014 },
+    { 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x00cb, 0x00cb, 0x0014, 0x0014 },
+    { 0x0213, 0x0014, 0x0009, 0x000b, 0x260e, 0x300c, 0x0014, 0x020b, 0x020b, 0x020b, 0x020b, 0x020b, 0x020b, 0x020b, 0x0014 },
+    { 0x0014, 0x0014, 0x0014, 0x0014, 0x060e, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x020d, 0x0014, 0x0014, 0x0014 },
+    { 0x2813, 0x0014, 0x0014, 0x0014, 0x2e0e, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x020d, 0x0014, 0x0014, 0x0014 },
+    { 0x0213, 0x0014, 0x000f, 0x2111, 0x020e, 0x020e, 0x020e, 0x020e, 0x020e, 0x020e, 0x020e, 0x020e, 0x020e, 0x020e, 0x0014 },
+    { 0x0014, 0x0014, 0x020e, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0090, 0x0090, 0x0014, 0x0014 },
+    { 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x00ce, 0x00ce, 0x0014, 0x0014 },
+    { 0x0213, 0x0014, 0x0211, 0x0211, 0x0211, 0x0211, 0x0211, 0x0211, 0x0211, 0x0211, 0x0211, 0x0211, 0x0211, 0x0211, 0x0211 },
+    { 0x0213, 0x0014, 0x000f, 0x2111, 0x0014, 0x020e, 0x020e, 0x020e, 0x020e, 0x020e, 0x020e, 0x020e, 0x020e, 0x020e, 0x0014 }
+  } ;
+  size_t w = 0, lastslash = 0, mark = 0 ;
+  char const *host = 0 ;
+  char const *path = 0 ;
+  char const *query = 0 ;
+  uint16_t port = 0 ;
+  uint16_t state = 0 ;
+  unsigned char decoded = 0 ;
+  uint8_t ssl = 0 ;
+  for (; state < 0x13 ; in++)
+  {
+    uint16_t c = table[state][uridecode_cclass(*in)] ;
+/*
+    LOLDEBUG("tipidee_uri_parse: state %hu, event %c, newstate %hu, actions %s%s%s%s%s%s%s%s%s%s", state, *in, c & 0x1f,
+      c & 0x8000 ? "s" : "",
+      c & 0x4000 ? "H" : "",
+      c & 0x2000 ? "z" : "",
+      c & 0x1000 ? "m" : "",
+      c & 0x0800 ? "c" : "",
+      c & 0x0400 ? "P" : "",
+      c & 0x0200 ? "p" : "",
+      c & 0x0100 ? "Q" : "",
+      c & 0x0080 ? "a" : "",
+      c & 0x0040 ? "b" : ""
+    ) ;
+*/
+    state = c & 0x1f ;
+    if (c & 0x8000) ssl = 1 ;
+    if (c & 0x4000) host = out + w ;
+    if (c & 0x2000) { if (w >= max) return 0 ; out[w++] = 0 ; }
+    if (c & 0x1000) mark = w ;
+    if (c & 0x0800) { if (!uint160_scan(out + mark, &port)) return 0 ; w = mark ; }
+    if (c & 0x0400) path = out + w ;
+    if (c & 0x0200) { if (w >= max) return 0 ; out[w++] = *in ; }
+    if (c & 0x0100) query = out + w ;
+    if (c & 0x0080) decoded = (decoded << 4) | fmtscan_num(*in, 16) ;
+    if (c & 0x0040)
+    {
+      if (w >= max) return 0 ;
+      if (decoded == '/') lastslash = w ;
+      out[w++] = decoded ;
+      decoded = 0 ;
+    }
+  }
+  if (state > 0x13) return 0 ;
+  if (path)
+  {
+    size_t len = strlen(path) ;
+    if (len >= 3 && !memcmp(path + len - 3, "/..", 3)) return 0 ;
+    if (strstr(path, "/../")) return 0 ;
+  }
+  uri->host = host ;
+  uri->port = port ;
+  uri->path = path ? path : "/" ;
+  uri->query = query ;
+  uri->lastslash = path ? lastslash - (path - out) : 0 ;
+  uri->https = ssl ;
+  return w ;
+}
diff --git a/src/tipideed/cgi.c b/src/tipideed/cgi.c
new file mode 100644
index 0000000..7a3ca03
--- /dev/null
+++ b/src/tipideed/cgi.c
@@ -0,0 +1,381 @@
+/* ISC license. */
+
+#include <unistd.h>
+#include <string.h>
+#include <strings.h>
+#include <errno.h>
+#include <signal.h>
+
+#include <skalibs/gccattributes.h>
+#include <skalibs/posixplz.h>
+#include <skalibs/types.h>
+#include <skalibs/bytestr.h>
+#include <skalibs/buffer.h>
+#include <skalibs/error.h>
+#include <skalibs/allreadwrite.h>
+#include <skalibs/strerr.h>
+#include <skalibs/stralloc.h>
+#include <skalibs/djbunix.h>
+#include <skalibs/iopause.h>
+#include <skalibs/env.h>
+#include <skalibs/exec.h>
+#include <skalibs/unix-timed.h>
+#include <skalibs/lolstdio.h>
+
+#include <tipidee/method.h>
+#include <tipidee/headers.h>
+#include <tipidee/response.h>
+#include <tipidee/uri.h>
+#include "tipideed-internal.h"
+
+static void addenv_ (tipidee_rql const *rql, char const *k, char const *v, int slash)
+{
+  if (!stralloc_cats(&g.sa, k)
+   || !stralloc_catb(&g.sa, "=/", 1 + !!slash)
+   || !stralloc_cats(&g.sa, v)
+   || !stralloc_0(&g.sa))
+    die500sys(rql, 111, "stralloc_catb") ;
+}
+
+#define addenv(rql, k, v) addenv_(rql, k, (v), 0)
+#define addenvslash(rql, k, v) addenv_(rql, k, (v), 1)
+
+static void delenv (tipidee_rql const *rql, char const *k)
+{
+  if (!stralloc_cats(&g.sa, k)
+   || !stralloc_0(&g.sa))
+    die500sys(rql, 111, "stralloc_catb") ;
+}
+
+static inline void modify_env (tipidee_rql const *rql, tipidee_headers const *hdr, size_t cl, char const *script, char const *infopath)
+{
+  uint32_t got = 0 ;
+  addenv(rql, "REQUEST_METHOD", tipidee_method_tostr(rql->m)) ;
+  if (cl)
+  {
+    char fmt[SIZE_FMT] ;
+    fmt[size_fmt(fmt, cl)] = 0 ;
+    addenv(rql, "CONTENT_LENGTH", fmt) ;
+  }
+  else delenv(rql, "CONTENT_LENGTH") ;
+
+  if (infopath) addenvslash(rql, "PATH_INFO", infopath) ;
+  else delenv(rql, "PATH_INFO") ;
+  if (rql->uri.query) addenv(rql, "QUERY_STRING", rql->uri.query) ;
+  else delenv(rql, "QUERY_STRING") ;
+  addenv(rql, "SCRIPT_NAME", script) ;
+  
+  for (size_t i = 0 ; i < hdr->n ; i++)
+  {
+    char const *key = hdr->buf + hdr->list[i].left ;
+    char const *val = hdr->buf + hdr->list[i].right ;
+    if (!strcasecmp(key, "Authorization"))
+    {
+      size_t n = str_chr(val, ' ') ;
+      if (n)
+      {
+        char scheme[n] ;
+        memcpy(scheme, val, n-1) ;
+        scheme[n-1] = 0 ;
+        addenv(rql, "AUTH_TYPE", scheme) ;
+        got |= 1 ;
+      }
+    }
+    else if (!strcasecmp(key, "Content-Type")) { addenv(rql, "CONTENT_TYPE", val) ; got |= 2 ; }
+    else if (!strcasecmp(key, "Content-Length") || !strcasecmp(key, "Connection")) ;
+    else
+    {
+      size_t len = strlen(key), pos = g.sa.len + 5 ;
+      if (!stralloc_catb(&g.sa, "HTTP_", 5)) die500sys(rql, 111, "stralloc_catb") ;
+      addenv(rql, key, val) ;
+      for (char *s = g.sa.s + pos ; len-- ; s++)
+        if (*s == '-') *s = '_' ;
+        else if (*s >= 'a' && *s <= 'z') *s -= 32 ;
+    }
+  }
+  if (!(got & 1)) delenv(rql, "AUTH_TYPE") ;
+  if (!(got & 2)) delenv(rql, "CONTENT_TYPE") ;
+}
+
+static inline int do_nph (tipidee_rql const *rql, char const *const *argv, char const *const *envp, char const *body, size_t bodylen) gccattr_noreturn ;
+static inline int do_nph (tipidee_rql const *rql, char const *const *argv, char const *const *envp, char const *body, size_t bodylen)
+{
+  int p[2] ;
+  log_nph(argv, envp) ;
+  if (pipe(p) == -1) die500sys(rql, 111, "pipe") ;
+  if (bodylen)
+  {
+    switch (fork())
+    {
+      case -1 : die500sys(rql, 111, "fork") ;
+      case 0 :      
+      {
+        tain deadline ;
+        char buf[4096] ;
+        buffer b = BUFFER_INIT(&buffer_write, p[1], buf, 4096) ;
+        PROG = "tipidee (nph helper child)" ;
+        tain_add_g(&deadline, &g.cgitto) ;
+        close(p[0]) ;
+        if (ndelay_on(p[1]) == -1) strerr_diefu1sys(111, "set fd nonblocking") ;
+        if (buffer_timed_put_g(&b, body, bodylen, &deadline) < bodylen
+         || !buffer_timed_flush_g(&b, &deadline))
+          strerr_diefu2sys(111, "write request body to nph ", argv[0]) ;
+        _exit(0) ;
+      }
+      default : break ;
+    }
+  }
+  close(p[1]) ;
+  if (fd_move(0, p[0]) == -1) die500sys(rql, 111, "fd_move") ;
+  exec_e(argv, envp) ;
+  die500sys(rql, errno == ENOENT ? 127 : 126, "exec nph ", argv[0]) ;
+}
+
+static inline int run_cgi (tipidee_rql const *rql, char const *const *argv, char const *const *envp, char const *body, size_t bodylen, tipidee_headers *hdr, stralloc *sa)
+{
+  iopause_fd x[2] = { { .events = IOPAUSE_READ }, { .events = IOPAUSE_WRITE } } ;
+  size_t bodyw = 0 ;
+  unsigned int rstate = 0 ;
+  tain deadline ;
+  pid_t pid ;
+  disize curheader = DISIZE_ZERO ;
+  uint32_t parserstate = 0 ;
+  buffer b ;
+  char buf[4096] ;
+  log_cgi(argv, envp) ;
+  {
+    int fd[2] = { 0, 1 } ;
+    pid = child_spawn2(argv[0], argv, envp, fd) ;
+    if (!pid) die500sys(rql, 111, "spawn ", argv[0]) ;
+    x[0].fd = fd[0] ; x[1].fd = fd[1] ;
+  }
+  if (!bodylen)
+  {
+    close(x[1].fd) ;
+    x[1].fd = -1 ;
+    LOLDEBUG("run_cgi: no request body, closing writing pipe to cgi") ;
+  }
+  buffer_init(&b, &buffer_read, x[0].fd, buf, 4096) ;
+  tain_add_g(&deadline, &g.cgitto) ;
+  while (x[0].fd >= 0)
+  {
+    int r = iopause_g(x, 1 + (x[1].fd >= 0), &deadline) ;
+    if (r == -1) die500sys(rql, 111, "iopause") ;
+    if (!r)
+    {
+      kill(pid, SIGTERM) ;
+      respond_504(rql) ;
+      break ;
+    }
+    if (x[1].fd >= 0 && x[1].revents & (IOPAUSE_WRITE | IOPAUSE_EXCEPT))
+    {
+      size_t len = allwrite(x[1].fd, body + bodyw, bodylen - bodyw) ;
+      if (!len)
+      {
+        if (g.verbosity) strerr_warnwu2sys("write request body to cgi ", argv[0]) ;
+        bodyw = bodylen ;
+      }
+      else bodyw += len ;
+      if (bodyw >= bodylen)
+      {
+        close(x[1].fd) ;
+        x[1].fd = -1 ;
+        LOLDEBUG("run_cgi: finished writing body") ;
+      }
+    }
+    if (x[0].fd >= 0 && x[0].revents & (IOPAUSE_READ | IOPAUSE_EXCEPT))
+    {
+      switch (rstate)
+      {
+        case 0 :
+        {
+          r = tipidee_headers_parse_nb(&b, hdr, &curheader, &parserstate) ;
+          switch (r)
+          {
+            case -2 : break ;
+            case -1 : die500sys(rql, 111, "read from cgi ", argv[0]) ;
+            case 0 :
+            {
+              size_t n = buffer_len(&b) ;
+              if (!stralloc_readyplus(sa, n)) die500sys(rql, 111, "stralloc_readyplus") ;
+              buffer_getnofill(&b, sa->s + sa->len, n) ;
+              sa->len += n ;
+              rstate = 1 ;
+              break ;
+            }
+            case 400 : die502x(rql, 1, "invalid headers", " from cgi ", argv[0]) ;
+            case 413 : die502x(rql, 1, hdr->n >= TIPIDEE_HEADERS_MAX ? "Too many headers" : "Too much header data", " from cgi ", argv[0]) ;
+            case 500 : die500x(rql, 101, "can't happen: ", "avltreen_insert failed", " in do_cgi") ;
+            default : die500x(rql, 101, "can't happen: ", "unknown tipidee_headers_parse return code", " in do_cgi") ;
+          }
+          if (!rstate) break ;
+        }
+        case 1 :
+        {
+          if (!slurpn(x[0].fd, sa, g.maxcgibody))
+          {
+            if (error_isagain(errno)) break ;
+            else if (errno == ENOBUFS) die502x(rql, 1, "Too fat body", " from cgi ", argv[0]) ;
+            else die500sys(rql, 111, "read body", " from cgi ", argv[0]) ;
+          }
+          close(x[0].fd) ;
+          x[0].fd = -1 ;
+          rstate = 2 ;
+          LOLDEBUG("run_cgi: rstate = 2") ;
+        }
+      }
+    }
+  }
+  if (x[1].fd >= 0) close(x[1].fd) ;
+  if (x[0].fd >= 0) close(x[0].fd) ;
+  return rstate == 2 ;
+}
+
+static inline int local_redirect (tipidee_rql *rql, char const *loc, char *uribuf, char const *cginame)
+{
+  size_t n ;
+  size_t hostlen = strlen(rql->uri.host) ;
+  uint16_t port = rql->uri.port ;
+  uint8_t ishttps = rql->uri.https ;
+  char hosttmp[hostlen + 1] ;
+  memcpy(hosttmp, rql->uri.host, hostlen + 1) ;
+  n = tipidee_uri_parse(uribuf, URI_BUFSIZE, loc, &rql->uri) ;
+  if (!n || n + hostlen + 1 > URI_BUFSIZE)
+    die502x(rql, 1, "cgi ", cginame, " returned an invalid ", "Location", " value", " for local redirection") ;
+  memcpy(uribuf + n, hosttmp, hostlen + 1) ;
+  rql->uri.host = uribuf + n ;
+  rql->uri.port = port ;
+  rql->uri.https = ishttps ;
+  return 1 ;
+}
+
+static inline void print_cgi_headers (tipidee_headers const *hdr, size_t rbodylen)
+{
+  static char const *const nope_table[] =
+  {
+    "Connection",
+    "Date",
+    "Status",
+    "Content-Length",
+    0
+  } ;
+  for (size_t i = 0 ; i < hdr->n ; i++)
+  {
+    char const *key = hdr->buf + hdr->list[i].left ;
+    char const *const *p = nope_table ;
+    if (tipidee_response_header_builtin_search(key)) continue ;
+    if (str_start(key, "X-CGI-")) continue ;
+    for (; *p ; p++) if (!strcasecmp(key, *p)) break ;
+    if (*p) continue ;
+    buffer_putsnoflush(buffer_1, key) ;
+    buffer_putnoflush(buffer_1, ": ", 2) ;
+    buffer_putsnoflush(buffer_1, hdr->buf + hdr->list[i].right) ;
+    buffer_putnoflush(buffer_1, "\r\n", 2) ;
+  }
+  if (rbodylen)
+  {
+    char fmt[SIZE_FMT] ;
+    fmt[size_fmt(fmt, rbodylen)] = 0 ;
+    buffer_putsnoflush(buffer_1, "Content-Length: ") ;
+    buffer_putsnoflush(buffer_1, fmt) ;
+    buffer_putnoflush(buffer_1, "\r\n", 2) ;
+  }
+}
+
+static inline int process_cgi_output (tipidee_rql *rql, tipidee_headers const *hdr, char const *rbody, size_t rbodylen, char *uribuf, char const *cginame)
+{
+  char const *location = tipidee_headers_search(hdr, "Location") ;
+  char const *x = tipidee_headers_search(hdr, "Status") ;
+  char const *reason_phrase = "OK" ;
+  unsigned int status = 0 ;
+  tain deadline ;
+  tain_add_g(&deadline, &g.writetto) ;
+  if (x)
+  {
+    size_t m = uint_scan(x, &status) ;
+    if (!m || x[m] != ' ')
+      die502x(rql, 1, "cgi ", cginame, " returned an invalid ", "Status", " header") ;
+    reason_phrase = x + m + 1 ;
+    if (status >= 300 && status < 399 && !location)
+      die502x(rql, 1, "cgi ", cginame, " returned a 3xx status code without a ", "Location", " header") ;
+    if (status < 100 || status > 999)
+      die502x(rql, 1, "cgi ", cginame, " returned an invalid ", "Status", " value") ;
+  }
+  if (location)
+  {
+    if (!location[0]) die502x(rql, 1, "cgi ", cginame, " returned an invalid ", "Location", " header") ;
+    if (location[0] == '/' && location[1] != '/') return local_redirect(rql, location, uribuf, cginame) ;
+    if (rbodylen)
+    {
+      if (!status)
+        die502x(rql, 1, "cgi ", cginame, " didn't output a ", "Status", " header", " for a client redirect response with document") ;
+      if (status < 300 || status > 399)
+        die502x(rql, 1, "cgi ", cginame, " returned an invalid ", "Status", " value", " for a client redirect response with document") ;
+    }
+    else
+    {
+      for (size_t i = 0 ; i < hdr->n ; i++)
+      {
+        char const *key = hdr->buf + hdr->list[i].left ;
+        if (!strcasecmp(key, "Location")) continue ;
+        if (str_start(key, "X-CGI-")) continue ;
+        die502x(rql, 1, "cgi ", cginame, "returned extra headers", " for a client redirect response without document") ;
+      }
+      status = 302 ;
+      reason_phrase = "Found" ;
+    }
+  }
+  else
+  {
+    if (!status) status = 200 ;
+    if (!tipidee_headers_search(hdr, "Content-Type"))
+      die502x(rql, 1, "cgi ", cginame, " didn't output a ", "Content-Type", " header") ;
+  }
+  x = tipidee_headers_search(hdr, "Content-Length") ;
+  if (x)
+  {
+    size_t cln ;
+    if (!size0_scan(x, &cln))
+      die502x(rql, 1, "cgi ", cginame, " returned an invalid ", "Content-Length", " header") ;
+    if (cln != rbodylen)
+      die502x(rql, 1, "cgi ", cginame, " returned a mismatching ", "Content-Length", " header") ;
+  }
+
+  tipidee_response_status(buffer_1, rql, status, reason_phrase) ;
+  tipidee_response_header_common_put_g(buffer_1, !g.cont) ;
+  print_cgi_headers(hdr, rbodylen) ;
+  if (buffer_timed_put_g(buffer_1, "\r\n", 2, &deadline) < 2)
+    strerr_diefu1sys(111, "write to stdout") ;
+  if (rbodylen)
+  {
+    if (buffer_timed_put_g(buffer_1, rbody, rbodylen, &deadline) < rbodylen)
+      strerr_diefu1sys(111, "write to stdout") ;
+  }
+  if (!buffer_timed_flush_g(buffer_1, &deadline))
+    strerr_diefu1sys(111, "write to stdout") ;
+  return 0 ;
+}
+
+static inline int do_cgi (tipidee_rql *rql, char const *const *argv, char const *const *envp, char const *body, size_t bodylen, char *uribuf)
+{
+  static stralloc sa = STRALLOC_ZERO ;
+  tipidee_headers hdr ;
+  char hdrbuf[2048] ;
+  sa.len = 0 ;
+  tipidee_headers_init(&hdr, hdrbuf, 2048) ;
+  if (!run_cgi(rql, argv, envp, body, bodylen, &hdr, &sa)) return 0 ;
+  return process_cgi_output(rql, &hdr, sa.s, sa.len, uribuf, argv[0]) ;
+}
+
+int respond_cgi (tipidee_rql *rql, char const *fn, size_t docrootlen, char const *infopath, char *uribuf, tipidee_headers const *hdr, tipidee_resattr const *ra, char const *body, size_t bodylen)
+{
+  size_t sabase = g.sa.len ;
+  size_t envmax = g.envlen + 16 + TIPIDEE_HEADERS_MAX ;
+  char const *argv[2] = { fn, 0 } ;
+  char const *envp[envmax] ;
+  modify_env(rql, hdr, bodylen, fn + docrootlen, infopath) ;
+  env_merge(envp, envmax, (char const *const *)environ, g.envlen, g.sa.s + g.cwdlen + 1, g.sa.len - (g.cwdlen+1)) ;
+  g.sa.len = sabase ;
+  return ra->isnph ? do_nph(rql, argv, envp, body, bodylen) :
+                     do_cgi(rql, argv, envp, body, bodylen, uribuf) ;
+}
diff --git a/src/tipideed/deps-exe/tipideed b/src/tipideed/deps-exe/tipideed
new file mode 100644
index 0000000..aad1417
--- /dev/null
+++ b/src/tipideed/deps-exe/tipideed
@@ -0,0 +1,11 @@
+cgi.o
+harden.o
+log.o
+options.o
+regular.o
+responses.o
+send_file.o
+tipideed.o
+trace.o
+libtipidee.a.xyzzy
+-lskarnet
diff --git a/src/tipideed/harden.c b/src/tipideed/harden.c
new file mode 100644
index 0000000..5c925f2
--- /dev/null
+++ b/src/tipideed/harden.c
@@ -0,0 +1,50 @@
+/* ISC license. */
+
+#include <skalibs/sysdeps.h>
+#include <skalibs/nonposix.h>
+
+#include <unistd.h>
+#include <errno.h>
+#include <stdlib.h>
+
+#include <skalibs/types.h>
+#include <skalibs/strerr.h>
+
+#include "tipideed-internal.h"
+
+static inline void tipideed_chroot (void)
+{
+#ifdef SKALIBS_HASCHROOT
+  if (chroot(".") == -1) strerr_diefu1sys(111, "chroot") ;
+#else
+  errno = ENOSYS ;
+  strerr_warnwu1sys("chroot") ; 
+#endif
+}
+
+static inline void tipideed_dropuidgid (void)
+{
+  uid_t uid = 0 ;
+  gid_t gid = 0 ;
+  char const *gidfmt = getenv("GID") ;
+  char const *uidfmt = getenv("UID") ;
+  if (!uidfmt) strerr_dienotset(100, "UID") ;
+  if (!uid0_scan(uidfmt, &uid)) strerr_dieinvalid(100, "UID") ;
+  if (!gidfmt) strerr_dienotset(100, "GID") ;
+  if (!gid0_scan(gidfmt, &gid)) strerr_dieinvalid(100, "GID") ;
+  if (gid)
+  {
+#ifdef SKALIBS_HASSETGROUPS
+    if (setgroups(1, &gid) == -1) strerr_diefu2sys(111, "setgroups to ", gidfmt) ;
+#endif
+    if (setgid(gid) == -1) strerr_diefu2sys(111, "setgid to ", gidfmt) ;
+  }
+  if (uid)
+    if (setuid(uid) == -1) strerr_diefu2sys(111, "setuid to ", uidfmt) ;
+}
+
+void tipideed_harden (unsigned int h)
+{
+  if (h & 2) tipideed_chroot() ;
+  if (h & 1) tipideed_dropuidgid() ;
+}
diff --git a/src/tipideed/log.c b/src/tipideed/log.c
new file mode 100644
index 0000000..a257ff5
--- /dev/null
+++ b/src/tipideed/log.c
@@ -0,0 +1,59 @@
+/* ISC license. */
+
+#include <unistd.h>
+
+#include <skalibs/uint16.h>
+#include <skalibs/types.h>
+#include <skalibs/strerr.h>
+
+#include <tipidee/method.h>
+#include "tipideed-internal.h"
+
+void log_start (void)
+{
+  if (g.verbosity >= 4)
+    strerr_warni7x("new connection", " from ip ", g.sa.s + g.remoteip, " (", g.sa.s + g.remotehost, ") port ", g.sa.s + g.remoteport) ;
+  else if (g.verbosity >= 3)
+    strerr_warni1x("new connection") ;
+}
+
+void log_and_exit (int e)
+{
+  if (g.verbosity >= 3)
+  {
+    char fmt[INT_FMT] ;
+    fmt[int_fmt(fmt, e)] = 0 ;
+    strerr_warni2x("exiting ", fmt) ;
+  }
+  _exit(e) ;
+}
+
+void log_request (tipidee_rql const *rql)
+{
+  if (g.verbosity >= 2)
+  {
+    char fmt[UINT16_FMT] ;
+    if (rql->uri.port) fmt[uint16_fmt(fmt, rql->uri.port)] = 0 ;
+    strerr_warnin(11, "request ", tipidee_method_tostr(rql->m), " for", rql->uri.host ? " host " : "", rql->uri.host ? rql->uri.host : "", rql->uri.port ? " port " : "", rql->uri.port ? fmt : "", " path ", rql->uri.path, rql->uri.query ? " query " : "", rql->uri.query ? rql->uri.query : "") ;
+  }
+}
+
+void log_regular (char const *fn, char const *sizefmt, int ishead, char const *ct)
+{
+  if (g.verbosity >= 2)
+    strerr_warni8x("sending ", ishead ? "headers for " : "", "regular file ", fn, " (", sizefmt, " bytes) with type ", ct) ;
+}
+
+void log_nph (char const *const *argv, char const *const *envp)
+{
+  if (g.verbosity >= 2)
+    strerr_warni3x("running ", "nph ", argv[0]) ;
+  (void)envp ;
+}
+
+void log_cgi (char const *const *argv, char const *const *envp)
+{
+  if (g.verbosity >= 2)
+    strerr_warni3x("running ", "cgi ", argv[0]) ;
+  (void)envp ;
+}
diff --git a/src/tipideed/options.c b/src/tipideed/options.c
new file mode 100644
index 0000000..d425943
--- /dev/null
+++ b/src/tipideed/options.c
@@ -0,0 +1,25 @@
+/* ISC license. */
+
+#include <string.h>
+
+#include <skalibs/buffer.h>
+#include <skalibs/strerr.h>
+#include <skalibs/tai.h>
+#include <skalibs/unix-timed.h>
+
+#include <tipidee/response.h>
+#include "tipideed-internal.h"
+
+int respond_options (tipidee_rql const *rql, uint32_t flags)
+{
+  tain deadline ;
+  tipidee_response_status_line(buffer_1, rql, "200 OK") ;
+  tipidee_response_header_common_put_g(buffer_1, 0) ;
+  buffer_putsnoflush(buffer_1, "Content-Length: 0\r\nAllow: GET, HEAD") ;
+  if (flags & 1) buffer_putsnoflush(buffer_1, ", POST") ;
+  buffer_putnoflush(buffer_1, "\r\n\r\n", 4) ;
+  tain_add_g(&deadline, &g.writetto) ;
+  if (!buffer_timed_flush_g(buffer_1, &deadline))
+    strerr_diefu1sys(111, "write to stdout") ;
+  return 0 ;
+}
diff --git a/src/tipideed/regular.c b/src/tipideed/regular.c
new file mode 100644
index 0000000..1ac1095
--- /dev/null
+++ b/src/tipideed/regular.c
@@ -0,0 +1,48 @@
+/* ISC license. */
+
+#include <skalibs/uint64.h>
+#include <skalibs/types.h>
+#include <skalibs/buffer.h>
+#include <skalibs/djbunix.h>
+#include <skalibs/strerr.h>
+#include <skalibs/tai.h>
+#include <skalibs/unix-timed.h>
+
+#include <tipidee/method.h>
+#include <tipidee/response.h>
+#include "tipideed-internal.h"
+
+int respond_regular (tipidee_rql const *rql, char const *fn, uint64_t size, tipidee_resattr const *ra)
+{
+  tain deadline ;
+  size_t n = tipidee_response_status_line(buffer_1, rql, "200 OK") ;
+  n += tipidee_response_header_common_put_g(buffer_1, !g.cont) ;
+  n += buffer_putsnoflush(buffer_1, "Content-Type: ") ;
+  n += buffer_putsnoflush(buffer_1, ra->content_type) ;
+  n += buffer_putsnoflush(buffer_1, "\r\nContent-Length: ") ;
+  {
+    char fmt[UINT64_FMT] ;
+    fmt[uint64_fmt(fmt, size)] = 0 ;
+    n += buffer_putsnoflush(buffer_1, fmt) ;
+    log_regular(fn, fmt, rql->m == TIPIDEE_METHOD_HEAD, ra->content_type) ;
+  }
+  n += buffer_putnoflush(buffer_1, "\r\n\r\n", 4) ;
+  if (rql->m == TIPIDEE_METHOD_HEAD)
+  {
+    tain_add_g(&deadline, &g.writetto) ;
+    if (!buffer_timed_flush_g(buffer_1, &deadline))
+      strerr_diefu1sys(111, "write to stdout") ;
+  }
+  else
+  {
+    int fd = open_read(fn) ;
+    if (fd == -1)
+    {
+      buffer_unput(buffer_1, n) ;
+      die500sys(rql, 111, "open ", fn) ;
+    }
+    send_file(fd, size, fn) ;
+    fd_close(fd) ;
+  }
+  return 0 ;
+}
diff --git a/src/tipideed/responses.c b/src/tipideed/responses.c
new file mode 100644
index 0000000..02109b3
--- /dev/null
+++ b/src/tipideed/responses.c
@@ -0,0 +1,64 @@
+/* ISC license. */
+
+#include <unistd.h>
+
+#include <skalibs/buffer.h>
+#include <skalibs/strerr.h>
+#include <skalibs/tai.h>
+#include <skalibs/unix-timed.h>
+
+#include <tipidee/rql.h>
+#include <tipidee/response.h>
+
+#include "tipideed-internal.h"
+
+void response_error (tipidee_rql const *rql, char const *rsl, char const *text, int doclose)
+{
+  tain deadline ;
+  tipidee_response_error(buffer_1, rql, rsl, text, doclose || !g.cont) ;
+  tain_add_g(&deadline, &g.writetto) ;
+  if (!buffer_timed_flush_g(buffer_1, &deadline))
+    strerr_diefu1sys(111, "write to stdout") ;
+}
+
+void response_error_and_exit (tipidee_rql const *rql, char const *rsl, char const *text)
+{
+  response_error(rql, rsl, text, 1) ;
+  log_and_exit(0) ;
+}
+
+void response_error_and_die (tipidee_rql const *rql, int e, char const *rsl, char const *text, char const *const *v, unsigned int n, int dosys)
+{
+  response_error(rql, rsl, text, 1) ;
+  if (dosys) strerr_dievsys(e, v, n) ;
+  else strerr_diev(e, v, n) ;
+}
+
+void exit_405 (tipidee_rql const *rql, uint32_t options)
+{
+  tain deadline ;
+  tipidee_response_status_line(buffer_1, rql, "405 Method Not Allowed") ;
+  tipidee_response_header_common_put_g(buffer_1, 1) ;
+  buffer_putsnoflush(buffer_1, "Allow: GET, HEAD") ;
+  if (options & 1) buffer_putsnoflush(buffer_1, ", POST") ;
+  buffer_putnoflush(buffer_1, "\r\n\r\n", 4) ;
+  tain_add_g(&deadline, &g.writetto) ;
+  if (!buffer_timed_flush_g(buffer_1, &deadline))
+    strerr_diefu1sys(111, "write to stdout") ;
+  log_and_exit(0) ;
+}
+
+void respond_30x (tipidee_rql const *rql, tipidee_redirection const *rd)
+{
+  static char const *rsl[4] = { "307 Temporary Redirect", "308 Permanent Redirect", "302 Found", "301 Moved Permanently" } ;
+  tain deadline ;
+  tipidee_response_status_line(buffer_1, rql, rsl[rd->type]) ;
+  tipidee_response_header_common_put_g(buffer_1, 0) ;
+  buffer_putsnoflush(buffer_1, "Location: ") ;
+  buffer_putsnoflush(buffer_1, rd->location) ;
+  if (rd->sub) buffer_putsnoflush(buffer_1, rd->sub) ;
+  buffer_putnoflush(buffer_1, "\r\n\r\n", 4) ;
+  tain_add_g(&deadline, &g.writetto) ;
+  if (!buffer_timed_flush_g(buffer_1, &deadline))
+    strerr_diefu1sys(111, "write to stdout") ;
+}
diff --git a/src/tipideed/send_file.c b/src/tipideed/send_file.c
new file mode 100644
index 0000000..77b49dd
--- /dev/null
+++ b/src/tipideed/send_file.c
@@ -0,0 +1,123 @@
+/* ISC license. */
+
+#include <skalibs/sysdeps.h>
+
+#ifdef SKALIBS_HASSPLICE
+
+#include <skalibs/nonposix.h>
+
+#include <fcntl.h>
+#include <stdint.h>
+#include <unistd.h>
+
+#include <skalibs/strerr.h>
+#include <skalibs/djbunix.h>
+#include <skalibs/unix-timed.h>
+
+#include "tipideed-internal.h"
+
+void init_splice_pipe (void)
+{
+  if (pipenbcoe(g.p) == -1)
+    strerr_diefu1sys(111, "pipe2") ;
+}
+
+struct spliceinfo_s
+{
+  ssize_t n ;
+  uint32_t last : 1 ;
+} ;
+
+static int getfd (void *b)
+{
+  (void)b ;
+  return 1 ;
+}
+
+static int isnonempty (void *b)
+{
+  struct spliceinfo_s *si = b ;
+  return !!si->n ;
+}
+
+static int flush (void *b)
+{
+  struct spliceinfo_s *si = b ;
+  while (si->n)
+  {
+    ssize_t r = splice(g.p[0], 0, 1, 0, si->n, SPLICE_F_NONBLOCK | (si->last ? 0 : SPLICE_F_MORE)) ;
+    if (r == -1) return 0 ;
+    if (!r) return 1 ;
+    si->n -= r ;
+  }
+  return 1 ;
+}
+
+void send_file (int fd, uint64_t n, char const *fn)
+{
+  tain deadline ;
+  struct spliceinfo_s si = { .last = 0 } ;
+  tain_add_g(&deadline, &g.writetto) ;
+  if (!buffer_timed_flush_g(buffer_1, &deadline))
+    strerr_diefu2sys(111, "write", " to stdout") ;
+  while (n)
+  {
+    si.n = splice(fd, 0, g.p[1], 0, n, 0) ;
+    if (si.n == -1) strerr_diefu2sys(111, "read from ", fn) ;
+    else if (!si.n) strerr_diefu3x(111, "serve ", fn, ": file was truncated") ;
+    else if (si.n > n)
+    {
+      si.n = n ;
+      if (g.verbosity >= 2)
+        strerr_warnw2x("serving elongated file: ", fn) ;
+    }
+    n -= si.n ;
+    if (!n) si.last = 1 ;
+    tain_add_g(&deadline, &g.writetto) ;
+    if (!timed_flush_g(&si, &getfd, &isnonempty, &flush, &deadline))
+      strerr_diefu2sys(111, "splice", " to stdout") ;
+  }
+}
+
+#else
+
+#include <sys/uio.h>
+
+#include <skalibs/allreadwrite.h>
+#include <skalibs/buffer.h>
+#include <skalibs/strerr.h>
+#include <skalibs/tai.h>
+
+#include "tipideed-internal.h"
+
+void init_splice_pipe (void)
+{
+}
+
+void send_file (int fd, uint64_t n, char const *fn)
+{
+  tain deadline ;
+  struct iovec v[2] ;
+  while (n)
+  {
+    ssize_t r ;
+    buffer_rpeek(buffer_1, v) ;
+    r = allreadv(fd, v, 2) ;
+    if (r > n)
+    if (r == -1) strerr_diefu2sys(111, "read from ", fn) ;
+    if (!r) strerr_diefu3x(111, "serve ", fn, ": file was truncated") ;
+    if (r > n)
+    {
+      r = n ;
+      if (g.verbosity >= 2)
+        strerr_warnw2x("serving elongated file: ", fn)
+    }
+    buffer_rseek(b, r) ;
+    tain_add_g(&deadline, g.writetto) ;
+    if (!buffer_timed_flush_g(buffer_1, &deadline))
+      strerr_diefu1sys(111, "write to stdout") ;
+    n -= r ;
+  }
+}
+
+#endif
diff --git a/src/tipideed/tipideed-internal.h b/src/tipideed/tipideed-internal.h
new file mode 100644
index 0000000..c4ff928
--- /dev/null
+++ b/src/tipideed/tipideed-internal.h
@@ -0,0 +1,147 @@
+/* ISC license. */
+
+#ifndef TIPIDEED_INTERNAL_H
+#define TIPIDEED_INTERNAL_H
+
+#include <sys/types.h>
+#include <stdint.h>
+
+#include <skalibs/gccattributes.h>
+#include <skalibs/uint64.h>
+#include <skalibs/stralloc.h>
+#include <skalibs/strerr.h>
+#include <skalibs/tai.h>
+
+#include <tipidee/tipidee.h>
+
+#define URI_BUFSIZE 4096
+#define HDR_BUFSIZE 8192
+
+typedef struct tipidee_resattr_s tipidee_resattr, *tipidee_resattr_ref ;
+struct tipidee_resattr_s
+{
+  char const *content_type ;
+  uint32_t iscgi : 1 ;
+  uint32_t isnph : 1 ;
+} ;
+#define TIPIDEE_RESATTR_ZERO { .content_type = 0, .iscgi = 0, .isnph = 0 }
+
+struct global_s
+{
+  tipidee_conf conf ;
+  stralloc sa ;
+  size_t envlen ;
+  size_t localip ;
+  size_t localhost ;
+  size_t localport ;
+  size_t localportlen ;
+  size_t remoteip ;
+  size_t remotehost ;
+  size_t remoteport ;
+  size_t cwdlen ;
+  size_t indexlen ;
+  tain readtto ;
+  tain writetto ;
+  tain cgitto ;
+  char const *indexnames[16] ;
+  int p[2] ;
+  uint32_t maxrqbody ;
+  uint32_t maxcgibody ;
+  uint16_t indexn : 4 ;
+  uint16_t verbosity : 3 ;
+  uint16_t cont : 2 ;
+} ;
+#define GLOBAL_ZERO \
+{ \
+  .conf = TIPIDEE_CONF_ZERO, \
+  .sa = STRALLOC_ZERO, \
+  .envlen = 0, \
+  .localip = 0, \
+  .localhost = 0, \
+  .localport = 0, \
+  .localportlen = 0, \
+  .remoteip = 0, \
+  .remotehost = 0, \
+  .remoteport = 0, \
+  .cwdlen = 1, \
+  .indexlen = 0, \
+  .readtto = TAIN_ZERO, \
+  .writetto = TAIN_ZERO, \
+  .cgitto = TAIN_ZERO, \
+  .indexnames = { 0 }, \
+  .p = { -1, -1 }, \
+  .maxrqbody = 0, \
+  .maxcgibody = 0, \
+  .indexn = 0, \
+  .verbosity = 1, \
+  .cont = 1 \
+}
+
+extern struct global_s g ;
+
+
+ /* uid/gid and chroot */
+
+extern void tipideed_harden (unsigned int) ;
+
+
+ /* Responses */
+
+extern void response_error (tipidee_rql const *, char const *, char const *, int) ;
+extern void response_error_and_exit (tipidee_rql const *, char const *, char const *) gccattr_noreturn ;
+extern void response_error_and_die (tipidee_rql const *, int e, char const *, char const *, char const *const *, unsigned int, int) gccattr_noreturn ;
+
+#define exit_400(r, s) response_error_and_exit(r, "400 Bad Request", s)
+extern void exit_405 (tipidee_rql const *, uint32_t) gccattr_noreturn ;
+#define exit_408(r) response_error_and_exit(r, "408 Request Timeout", "")
+#define exit_413(r, s) response_error_and_exit(r, "413 Request Entity Too Large", s)
+#define exit_501(r, s) response_error_and_exit(r, "501 Not Implemented", s)
+
+#define respond_403(r) response_error(r, "403 Forbidden", "Missing credentials to access the URI.", 0)
+#define respond_404(r) response_error(r, "404 Not Found", "The request URI was not found.", 0)
+#define respond_414(r) response_error(r, "414 URI Too Long", "The request URI had an oversized component.", 0)
+extern void respond_30x (tipidee_rql const *, tipidee_redirection const *) ;
+#define respond_504(r) response_error(r, "504 Gateway Timeout", "The CGI script took too long to answer.", 0)
+
+#define diefx(r, e, rsl, text, ...) response_error_and_die(r, e, rsl, text, strerr_array(PROG, ": fatal: ", __VA_ARGS__), sizeof(strerr_array(__VA_ARGS__))/sizeof(char const *)+2, 0)
+#define diefusys(r, e, rsl, text, ...) response_error_and_die(r, e, rsl, text, strerr_array(PROG, ": fatal: ", "unable to ", __VA_ARGS__), sizeof(strerr_array(__VA_ARGS__))/sizeof(char const *)+3, 1)
+#define die500x(r, e, ...) diefx(r, e, "500 Internal Server Error", "Bad server configuration.", __VA_ARGS__)
+#define die500sys(r, e, ...) diefusys(r, e, "500 Internal Server Error", "System error.", __VA_ARGS__)
+#define die502x(r, e, ...) diefx(r, e, "502 Bad Gateway", "Bad CGI script.", __VA_ARGS__)
+
+ /* Trace */
+
+extern int respond_trace (char const *, tipidee_rql const *, tipidee_headers const *) ;
+
+
+ /* Options */
+
+extern int respond_options (tipidee_rql const *, uint32_t) ;
+
+
+ /* send_file */
+
+extern void init_splice_pipe (void) ;
+extern void send_file (int, uint64_t, char const *) ;
+
+
+ /* regular */
+
+extern int respond_regular (tipidee_rql const *, char const *, uint64_t, tipidee_resattr const *) ;
+
+
+ /* cgi */
+
+extern int respond_cgi (tipidee_rql *, char const *, size_t, char const *, char *, tipidee_headers const *, tipidee_resattr const *, char const *, size_t) ;
+
+
+ /* log */
+
+extern void log_start (void) ;
+extern void log_and_exit (int) gccattr_noreturn ;
+extern void log_request (tipidee_rql const *) ;
+extern void log_regular (char const *, char const *, int, char const *) ;
+extern void log_nph (char const *const *, char const *const *) ;
+extern void log_cgi (char const *const *, char const *const *) ;
+
+#endif
diff --git a/src/tipideed/tipideed.c b/src/tipideed/tipideed.c
new file mode 100644
index 0000000..42e65a6
--- /dev/null
+++ b/src/tipideed/tipideed.c
@@ -0,0 +1,514 @@
+/* ISC license. */
+
+#include <unistd.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <sys/stat.h>
+
+#include <skalibs/env.h>
+#include <skalibs/uint16.h>
+#include <skalibs/types.h>
+#include <skalibs/bytestr.h>
+#include <skalibs/sgetopt.h>
+#include <skalibs/buffer.h>
+#include <skalibs/error.h>
+#include <skalibs/strerr.h>
+#include <skalibs/tai.h>
+#include <skalibs/ip46.h>
+#include <skalibs/sig.h>
+#include <skalibs/stralloc.h>
+#include <skalibs/djbunix.h>
+#include <skalibs/avltreen.h>
+#include <skalibs/unix-timed.h>
+#include <skalibs/lolstdio.h>
+
+#include <tipidee/tipidee.h>
+#include "tipideed-internal.h"
+
+#define USAGE "tipideed [ -v verbosity ] [ -f conffile ] [ -R chroot ] [ -U ]"
+#define dieusage() strerr_dieusage(100, USAGE)
+#define dienomem() strerr_diefu1sys(111, "stralloc_catb")
+
+#define ARGV_MAX 128
+
+struct global_s g = GLOBAL_ZERO ;
+
+static void sigchld_handler (int sig)
+{
+  (void)sig ;
+  wait_reap() ;
+}
+
+static inline void prep_env (void)
+{
+  static char const basevars[] = "PROTO\0GATEWAY_INTERFACE=CGI/1.1\0SERVER_PROTOCOL=HTTP/1.1\0SERVER_SOFTWARE=tipidee/" TIPIDEE_VERSION ;
+  static char const sslvars[] = "SSL_PROTOCOL\0SSL_CIPHER\0SSL_TLS_SNI_SERVERNAME\0SSL_PEER_CERT_HASH\0SSL_PEER_CERT_SUBJECT\0HTTPS=on" ;
+  char const *x = getenv("SSL_PROTOCOL") ;
+  if (!stralloc_readyplus(&g.sa, 320)) dienomem() ;
+  if (sagetcwd(&g.sa) == -1) strerr_diefu1sys(111, "getcwd") ;
+  if (g.sa.len == 1) g.sa.len = 0 ;
+  g.cwdlen = g.sa.len ;
+  if (g.cwdlen && !stralloc_0(&g.sa)) dienomem() ;
+  if (!stralloc_catb(&g.sa, basevars, sizeof(basevars))) dienomem() ;
+  if (x && !stralloc_catb(&g.sa, sslvars, sizeof(sslvars))) dienomem() ;
+  x = getenv(basevars) ;
+  if (!x) strerr_dienotset(100, "PROTO") ;
+  {
+    size_t protolen = strlen(x) ;
+    size_t m ;
+    ip46 ip ;
+    uint16_t port ;
+    char fmt[IP46_FMT] ;
+    char var[protolen + 11] ;
+    memcpy(var, x, protolen) ;
+
+    memcpy(var + protolen, "LOCALIP", 8) ;
+    x = getenv(var) ;
+    if (!x) strerr_dienotset(100, var) ;
+    if (!ip46_scan(x, &ip)) strerr_dieinvalid(100, var) ;
+    if (!stralloc_catb(&g.sa, var, protolen + 8)
+     || !stralloc_catb(&g.sa, "SERVER_ADDR=", 12)) dienomem() ;
+    g.localip = g.sa.len ;
+    m = ip46_fmt(fmt, &ip) ; fmt[m++] = 0 ;
+    if (!stralloc_catb(&g.sa, fmt, m)) dienomem() ;
+
+    memcpy(var + protolen, "LOCALHOST", 10) ;
+    x = getenv(var) ;
+    if (!x) strerr_dienotset(100, var) ;
+    if (!stralloc_catb(&g.sa, var, protolen + 10)
+     || !stralloc_catb(&g.sa, "SERVER_NAME=", 12)) dienomem() ;
+    g.localhost = g.sa.len ;
+    if (!stralloc_cats(&g.sa, x) || !stralloc_0(&g.sa)) dienomem() ;
+
+    memcpy(var + protolen, "LOCALPORT", 10) ;
+    x = getenv(var) ;
+    if (!x) strerr_dienotset(100, var) ;
+    if (!uint160_scan(x, &port)) strerr_dieinvalid(100, var) ;
+    if (!stralloc_catb(&g.sa, var, protolen + 10)
+     || !stralloc_catb(&g.sa, "SERVER_PORT=", 12)) dienomem() ;
+    g.localport = g.sa.len ;
+    g.localportlen = uint16_fmt(fmt, port) ; fmt[g.localportlen] = 0 ;
+    if (!stralloc_catb(&g.sa, fmt, g.localportlen + 1)) dienomem() ;
+
+    memcpy(var + protolen, "REMOTEIP", 9) ;
+    x = getenv(var) ;
+    if (!x) strerr_dienotset(100, var) ;
+    if (!ip46_scan(x, &ip)) strerr_dieinvalid(100, var) ;
+    if (!stralloc_catb(&g.sa, var, protolen + 9)
+     || !stralloc_catb(&g.sa, "REMOTE_ADDR=", 12)) dienomem() ;
+    g.remoteip = g.sa.len ;
+    m = ip46_fmt(fmt, &ip) ; fmt[m++] = 0 ;
+    if (!stralloc_catb(&g.sa, fmt, m)) dienomem() ;
+
+    memcpy(var + protolen, "REMOTEHOST", 11) ;
+    x = getenv(var) ;
+    if ((x && !stralloc_catb(&g.sa, var, protolen + 11))
+     || !stralloc_catb(&g.sa, "REMOTE_HOST=", 12)) dienomem() ;
+    g.remotehost = g.sa.len ;
+    if (!stralloc_cats(&g.sa, x ? x : fmt)
+     || !stralloc_0(&g.sa)) dienomem() ;
+
+    memcpy(var + protolen, "REMOTEPORT", 11) ;
+    x = getenv(var) ;
+    if (!x) strerr_dienotset(100, var) ;
+    if (!uint160_scan(x, &port)) strerr_dieinvalid(100, var) ;
+    if (!stralloc_catb(&g.sa, var, protolen + 11)
+     || !stralloc_catb(&g.sa, "REMOTE_PORT=", 12)) dienomem() ;
+    g.remoteport = g.sa.len ;
+    m = uint16_fmt(fmt, port) ; fmt[m++] = 0 ;
+    if (!stralloc_catb(&g.sa, fmt, m)) dienomem() ;
+
+    memcpy(var + protolen, "REMOTEINFO", 11) ;
+    x = getenv(var) ;
+    if (x)
+      if (!stralloc_catb(&g.sa, var, protolen + 11)
+       || !stralloc_catb(&g.sa, "REMOTE_IDENT=", 13)
+       || !stralloc_cats(&g.sa, x) || !stralloc_0(&g.sa)) dienomem() ;
+  }
+}
+
+static uint32_t get_uint32 (char const *key)
+{
+  uint32_t n ;
+  if (!tipidee_conf_get_uint32(&g.conf, key, &n))
+    strerr_diefu2sys(100, "read config value for ", key) ;
+  return n ;
+}
+
+static inline unsigned int indexify (tipidee_rql const *rql, char *s, struct stat *st)
+{
+  size_t len = strlen(s) ;
+  unsigned int i = 0 ;
+  if (s[len - 1] != '/') s[len++] = '/' ;
+  for (; i < g.indexn ; i++)
+  {
+    strcpy(s + len, g.indexnames[i]) ;
+    if (stat(s, st) == 0) break ;
+    switch (errno)
+    {
+      case EACCES : return 403 ;
+      case ENAMETOOLONG : return 414 ;
+      case ENOTDIR : return 404 ;
+      case ENOENT : continue ;
+      default : die500sys(rql, 111, "stat ", s) ;
+    }
+  }
+  if (i >= g.indexn) return 404 ;
+  if (S_ISDIR(st->st_mode)) die500x(rql, 103, "bad document hierarchy: ", s, " is a directory") ;
+  return 0 ;
+}
+
+static inline void get_resattr (tipidee_rql const *rql, char const *res, tipidee_resattr *ra)
+{
+  static stralloc sa = STRALLOC_ZERO ;
+  sa.len = 0 ;
+  if (sarealpath(&sa, res) == -1 || !stralloc_0(&sa)) die500sys(rql, 111, "realpath ", res) ;
+  if (strncmp(sa.s, g.sa.s, g.cwdlen) || sa.s[g.cwdlen] != '/')
+    die500x(rql, 102, "resource ", res, " points outside of the server's root") ;
+
+  {
+    char const *attr = 0 ;
+    size_t len = sa.len - g.cwdlen + 1 ;
+    char key[len + 1] ;
+    key[0] = 'A' ; key[1] = ':' ;
+    memcpy(key + 2, sa.s + 1 + g.cwdlen, sa.len - 1 - g.cwdlen) ;
+    key[len] = '/' ;
+    errno = ENOENT ;
+    while (!attr)
+    {
+      if (errno != ENOENT) die500x(rql, 102, "invalid configuration data for ", key) ;
+      while (len > 2 && key[len] != '/') len-- ;
+      if (len <= 2) break ;
+      key[len--] = 0 ;
+      attr = tipidee_conf_get_string(&g.conf, key) ;
+      key[0] = 'a' ;
+    }
+    if (attr)
+    {
+      if (*attr < '@' || *attr > 'G') die500x(rql, 102, "invalid configuration data for ", key) ;
+      ra->iscgi = *attr & ~'@' & 1 ;
+      if (attr[1]) ra->content_type = attr + 1 ;
+      if (ra->iscgi)
+      {
+        char const *nphprefix ;
+        char *p ;
+        key[0] = 'N' ;
+        p = strchr(key+2, '/') ;
+        if (p) *p = 0 ;
+        nphprefix = tipidee_conf_get_string(&g.conf, key) ;
+        if (nphprefix)
+        {
+          char const *base = strrchr(sa.s + g.cwdlen, '/') ;
+          if (str_start(base + 1, nphprefix)) ra->isnph = 1 ;
+        }
+      }
+    }
+  }
+
+  if (!ra->iscgi && !ra->content_type)
+  {
+    ra->content_type = tipidee_conf_get_content_type(&g.conf, sa.s + g.cwdlen) ;
+    if (!ra->content_type) die500sys(rql, 111, "get content type for ", sa.s + g.cwdlen) ;
+  }
+}
+
+static inline int serve (tipidee_rql *rql, char const *docroot, size_t docrootlen, char *uribuf, tipidee_headers const *hdr, char const *body, size_t bodylen)
+{
+  tipidee_resattr ra = TIPIDEE_RESATTR_ZERO ;
+  size_t pathlen = strlen(rql->uri.path) ;
+  char const *infopath = 0 ;
+  struct stat st ;
+  char fn[docrootlen + pathlen + 2 + g.indexlen] ;
+  memcpy(fn, docroot, docrootlen) ;
+  memcpy(fn + docrootlen, rql->uri.path, pathlen) ;
+  fn[docrootlen + pathlen] = 0 ;
+
+ /* Redirection */
+
+  if (rql->m != TIPIDEE_METHOD_OPTIONS)
+  {
+    tipidee_redirection rd = TIPIDEE_REDIRECTION_ZERO ;
+    int e = tipidee_conf_get_redirection(&g.conf, fn, docrootlen, &rd) ;
+    if (e == -1) die500sys(rql, 111, "get redirection data for ", fn) ;
+    if (e)
+    {
+      respond_30x(rql, &rd) ;
+      return 0 ;
+    }
+  }
+
+ /* Resource in the filesystem */
+
+  if (stat(fn, &st) == -1)
+  {
+    size_t pos = docrootlen + pathlen - 1 ;
+    for (;;)
+    {
+      while (fn[pos] != '/') pos-- ;
+      if (pos <= docrootlen) { respond_404(rql) ; return 0 ; }
+      fn[pos] = 0 ;
+      if (stat(fn, &st) == 0) break ;
+      switch (errno)
+      {
+        case ENOTDIR :
+        case ENOENT : fn[pos--] = '/' ; break ;
+        case EACCES : respond_403(rql) ; return 0 ;
+        case ENAMETOOLONG : respond_414(rql) ; return 0 ;
+        default : die500sys(rql, 111, "stat ", fn) ;
+      }
+    }
+    infopath = fn + pos + 1 ;
+  }
+  if (S_ISDIR(st.st_mode))
+  {
+    if (infopath) { respond_404(rql) ; return 0 ; }
+    switch (indexify(rql, fn, &st))
+    {
+      case 403 : respond_403(rql) ; return 0 ;
+      case 404 : respond_404(rql) ; return 0 ;
+      case 414 : respond_414(rql) ; return 0 ;
+      case 0 : break ;
+    }
+  }
+  LOLDEBUG("serve: %s with %s %s, docroot %s", fn, infopath ? "infopath" : "no", infopath ? infopath : "infopath", docroot) ;
+
+  get_resattr(rql, fn, &ra) ;
+
+  if (!ra.iscgi)
+  {
+    if (infopath) { respond_404(rql) ; return 0 ; }
+    if (rql->m == TIPIDEE_METHOD_POST) exit_405(rql, 0) ;
+  }
+
+  if (rql->m == TIPIDEE_METHOD_OPTIONS)
+    return respond_options(rql, ra.iscgi) ;
+  else if (ra.iscgi)
+    return respond_cgi(rql, fn, docrootlen, infopath, uribuf, hdr, &ra, body, bodylen) ;
+  else
+    return respond_regular(rql, fn, st.st_size, &ra) ;
+}
+
+int main (int argc, char const *const *argv, char const *const *envp)
+{
+  stralloc bodysa = STRALLOC_ZERO ;
+  char progstr[14 + PID_FMT] = "tipideed: pid " ;
+  progstr[14 + pid_fmt(progstr + 14, getpid())] = 0 ;
+  PROG = progstr ;
+
+  {
+    char const *conffile = "/etc/tipidee.conf.cdb" ;
+    char const *newroot = 0 ;
+    unsigned int h = 0 ;
+    int gotv = 0 ;
+    subgetopt l = SUBGETOPT_ZERO ;
+
+    for (;;)
+    {
+      int opt = subgetopt_r(argc, argv, "v:f:d:RU", &l) ;
+      if (opt == -1) break ;
+      switch (opt)
+      {
+        case 'v' :
+        {
+          unsigned int n ;
+          if (!uint0_scan(l.arg, &n)) dieusage() ;
+          if (n > 7) n = 7 ;
+          g.verbosity = n ;
+          gotv = 1 ;
+          break ;
+        }
+        case 'f' : conffile = l.arg ; break ;
+        case 'd' : newroot = l.arg ; break ;
+        case 'R' : h |= 3 ; break ;
+        case 'U' : h |= 1 ; break ;
+        default : dieusage() ;
+      }
+    }
+    argc -= l.ind ; argv += l.ind ;
+
+    g.envlen = env_len(envp) ;
+    if (!tipidee_conf_init(&g.conf, conffile))
+      strerr_diefu2sys(111, "find configuration in ", conffile) ;
+    if (newroot && chdir(newroot) == -1)
+      strerr_diefu2sys(111, "chdir to ", newroot) ;
+    tipideed_harden(h) ;
+    if (!gotv) g.verbosity = get_uint32("G:verbosity") ;
+  }
+
+  prep_env() ;
+  tain_from_millisecs(&g.readtto, get_uint32("G:read_timeout")) ;
+  tain_from_millisecs(&g.writetto, get_uint32("G:write_timeout")) ;
+  tain_from_millisecs(&g.cgitto, get_uint32("G:cgi_timeout")) ;
+  g.maxrqbody = get_uint32("G:max_request_body_length") ;
+  g.maxcgibody = get_uint32("G:max_cgi_body_length") ;
+  {
+    unsigned int n = tipidee_conf_get_argv(&g.conf, "G:index_file", g.indexnames, 16, &g.indexlen) ;
+    if (!n) strerr_dief3x(100, "bad", " config value for ", "G:index_file") ;
+    g.indexn = n-1 ;
+  }
+
+  if (ndelay_on(0) == -1 || ndelay_on(1) == -1)
+    strerr_diefu1sys(111, "set I/O nonblocking") ;
+  init_splice_pipe() ;
+  if (!sig_catch(SIGCHLD, &sigchld_handler))
+    strerr_diefu1sys(111, "set SIGCHLD handler") ;
+  if (!tain_now_set_stopwatch_g())
+    strerr_diefu1sys(111, "initialize clock") ;
+
+  log_start() ;
+
+
+ /* Main loop */
+
+  while (g.cont)
+  {
+    tain deadline ;
+    tipidee_rql rql = TIPIDEE_RQL_ZERO ;
+    tipidee_headers hdr ;
+    int e ;
+    char const *x ;
+    size_t content_length ;
+    tipidee_transfercoding tcoding = TIPIDEE_TRANSFERCODING_UNKNOWN ;
+    char uribuf[URI_BUFSIZE] ;
+    char hdrbuf[HDR_BUFSIZE] ;
+
+    tain_add_g(&deadline, &g.readtto) ;
+    bodysa.len = 0 ;
+
+    e = tipidee_rql_read_g(buffer_0, uribuf, URI_BUFSIZE, &content_length, &rql, &deadline) ;
+    switch (e)
+    {
+      case -1 : log_and_exit(1) ;  /* Timeout, malicious client, or shitty client */
+      case 0 : break ;
+      case 400 : exit_400(&rql, "Syntax error in request line") ;
+      default : strerr_dief2x(101, "can't happen: ", "unknown tipidee_rql_read return code") ;
+    }
+    if (rql.http_major != 1) log_and_exit(1) ;
+    if (rql.http_minor > 2) exit_400(&rql, "Bad HTTP version") ;
+
+    content_length = 0 ;
+    tipidee_headers_init(&hdr, hdrbuf, HDR_BUFSIZE) ;
+    e = tipidee_headers_timed_parse_g(buffer_0, &hdr, &deadline) ;
+    switch (e)
+    {
+      case -1 : log_and_exit(1) ;  /* connection issue, client timeout, etc. */
+      case 0 : break ;
+      case 400 : exit_400(&rql, "Syntax error in headers") ;
+      case 408 : exit_408(&rql) ;  /* timeout */
+      case 413 : exit_413(&rql, hdr.n >= TIPIDEE_HEADERS_MAX ? "Too many headers" : "Too much header data") ;
+      case 500 : die500x(&rql, 101, "can't happen: ", "avltreen_insert failed") ;
+      default : die500x(&rql, 101, "can't happen: ", "unknown tipidee_headers_parse return code") ;
+    }
+
+    if (rql.http_minor == 0) g.cont = 0 ;
+    else
+    {
+      x = tipidee_headers_search(&hdr, "Connection") ;
+      if (x)
+      {
+        if (strstr(x, "close")) g.cont = 0 ;
+        else if (strstr(x, "keep-alive")) g.cont = 2 ;
+      }
+    }
+
+    x = tipidee_headers_search(&hdr, "Transfer-Encoding") ;
+    if (x)
+    {
+      if (strcmp(x, "chunked")) exit_400(&rql, "unsupported Transfer-Encoding") ;
+      else tcoding = TIPIDEE_TRANSFERCODING_CHUNKED ;
+    }
+    else
+    {
+      x = tipidee_headers_search(&hdr, "Content-Length") ;
+      if (x)
+      {
+        if (!size_scan(x, &content_length)) exit_400(&rql, "Invalid Content-Length") ;
+        else if (content_length) tcoding = TIPIDEE_TRANSFERCODING_FIXED ;
+        else tcoding = TIPIDEE_TRANSFERCODING_NONE ;
+      }
+      else tcoding = TIPIDEE_TRANSFERCODING_NONE ;
+    }
+
+    if (tcoding != TIPIDEE_TRANSFERCODING_NONE && rql.m != TIPIDEE_METHOD_POST)
+      exit_400(&rql, "only POST requests can have an entity body") ;
+
+    switch (rql.m)
+    {
+      case TIPIDEE_METHOD_GET :
+      case TIPIDEE_METHOD_HEAD :
+      case TIPIDEE_METHOD_POST : break ;
+      case TIPIDEE_METHOD_OPTIONS :
+        if (!rql.uri.path) { respond_options(&rql, 1) ; continue ; }
+        break ;
+      case TIPIDEE_METHOD_PUT :
+      case TIPIDEE_METHOD_DELETE : exit_405(&rql, 1) ;
+      case TIPIDEE_METHOD_TRACE : respond_trace(hdrbuf, &rql, &hdr) ; continue ;
+      case TIPIDEE_METHOD_CONNECT : exit_501(&rql, "CONNECT method unsupported") ;
+      case TIPIDEE_METHOD_PRI : exit_501(&rql, "PRI method attempted with HTTP/1.1") ;
+      default : die500x(&rql, 101, "can't happen: unknown HTTP method") ;
+    }
+
+    if (!rql.uri.host)
+    {
+      x = tipidee_headers_search(&hdr, "Host") ;
+      if (x)
+      {
+        char *p = strchr(x, ':') ;
+        if (p)
+        {
+          if (!uint160_scan(p+1, &rql.uri.port)) exit_400(&rql, "Invalid Host header") ;
+          *p = 0 ;
+        }
+        if (!*x || *x == '.') exit_400(&rql, "Invalid Host header") ;
+        rql.uri.host = x ;
+      }
+      else if (!rql.http_minor) rql.uri.host = "@" ;
+      else exit_400(&rql, "Missing Host header") ;
+    }
+
+    {
+      size_t hostlen = strlen(rql.uri.host) ;
+      char docroot[hostlen + g.localportlen + 2] ;
+      if (rql.uri.host[hostlen - 1] == '.') hostlen-- ;
+      memcpy(docroot, rql.uri.host, hostlen) ;
+      docroot[hostlen] = ':' ;
+      memcpy(docroot + hostlen + 1, g.sa.s + g.localport, g.localportlen + 1) ;
+
+     /* All good. Read the body if any */
+
+      switch (tcoding)
+      {
+        case TIPIDEE_TRANSFERCODING_FIXED :
+        {
+          if (content_length > g.maxrqbody) exit_413(&rql, "Request body too large") ;
+          if (!stralloc_ready(&bodysa, content_length)) die500sys(&rql, 111, "stralloc_ready") ;
+          if (buffer_timed_get_g(buffer_0, bodysa.s, content_length, &deadline) < content_length)
+          {
+            if (errno == ETIMEDOUT) exit_408(&rql) ;
+            else exit_400(&rql, "Request body does not match Content-Length") ;
+          }
+          bodysa.len = content_length ;
+        }
+        case TIPIDEE_TRANSFERCODING_CHUNKED :
+        {
+          if (!tipidee_chunked_read_g(buffer_0, &bodysa, g.maxrqbody, &deadline))
+          {
+            if (error_temp(errno)) die500sys(&rql, 111, "decode chunked body") ;
+            else if (errno == EMSGSIZE) exit_413(&rql, "Request body too large") ;
+            else exit_400(&rql, "Invalid chunked body") ;
+          }
+        }
+        default : break ;
+      }
+
+      log_request(&rql) ;
+
+
+     /* And serve the resource. The loop is in case of CGI local-redirection. */
+
+      while (serve(&rql, docroot, hostlen + 1 + g.localportlen, uribuf, &hdr, bodysa.s, bodysa.len)) ;
+    }
+  }
+  log_and_exit(0) ;
+}
diff --git a/src/tipideed/trace.c b/src/tipideed/trace.c
new file mode 100644
index 0000000..4761ea5
--- /dev/null
+++ b/src/tipideed/trace.c
@@ -0,0 +1,67 @@
+/* ISC license. */
+
+#include <string.h>
+
+#include <skalibs/types.h>
+#include <skalibs/buffer.h>
+#include <skalibs/strerr.h>
+#include <skalibs/tai.h>
+#include <skalibs/unix-timed.h>
+
+#include <tipidee/method.h>
+#include <tipidee/response.h>
+#include "tipideed-internal.h"
+
+int respond_trace (char const *buf, tipidee_rql const *rql, tipidee_headers const *hdr)
+{
+  tain deadline ;
+  size_t cl = 0 ;
+  char fmt[SIZE_FMT] ;
+  tipidee_response_status_line(buffer_1, rql, "200 OK") ;
+  tipidee_response_header_common_put_g(buffer_1, 0) ;
+  buffer_putsnoflush(buffer_1, "Content-Type: message/http\r\nContent-Length: ") ;
+  cl += strlen(tipidee_method_tostr(rql->m)) + 1;
+  if (rql->uri.host) cl += 7 + rql->uri.https + strlen(rql->uri.host) ;
+  cl += strlen(rql->uri.path) + (rql->uri.query ? 1 + strlen(rql->uri.query) : 0) ;
+  cl += 6 + uint_fmt(0, rql->http_major) + 1 + uint_fmt(0, rql->http_minor) + 2 ;
+  for (size_t i = 0 ; i < hdr->n ; i++)
+    cl += strlen(buf + hdr->list[i].left) + 2 + strlen(buf + hdr->list[i].right) + 2 ;
+  cl += 2 ;
+  buffer_putnoflush(buffer_1, fmt, size_fmt(fmt, cl)) ;
+  buffer_putsnoflush(buffer_1, "\r\n\r\n") ;
+
+  buffer_putsnoflush(buffer_1, tipidee_method_tostr(rql->m)) ;
+  buffer_putnoflush(buffer_1, " ", 1) ;
+  if (rql->uri.host)
+  {
+    buffer_putsnoflush(buffer_1, rql->uri.https ? "https://" : "http://") ;
+    buffer_putsnoflush(buffer_1, rql->uri.host) ;
+  }
+  buffer_putsnoflush(buffer_1, rql->uri.path) ;
+  if (rql->uri.query)
+  {
+    buffer_putnoflush(buffer_1, "?", 1) ;
+    buffer_putsnoflush(buffer_1, rql->uri.query) ;
+  }
+  buffer_putsnoflush(buffer_1, " HTTP/") ;
+  buffer_putnoflush(buffer_1, fmt, uint_fmt(fmt, rql->http_major)) ;
+  buffer_putnoflush(buffer_1, ".", 1) ;
+  buffer_putnoflush(buffer_1, fmt, uint_fmt(fmt, rql->http_minor)) ;
+  buffer_putsnoflush(buffer_1, "\r\n") ;
+  for (size_t i = 0 ; i < hdr->n ; i++)
+  {
+    size_t len = strlen(buf + hdr->list[i].left) ;
+    tain_add_g(&deadline, &g.writetto) ;
+    if (buffer_timed_put_g(buffer_1, buf + hdr->list[i].left, len, &deadline) < len) goto err ;
+    if (buffer_timed_put_g(buffer_1, ": ", 2, &deadline) < 2) goto err ;
+    len = strlen(buf + hdr->list[i].right) ;
+    if (buffer_timed_put_g(buffer_1, buf + hdr->list[i].right, len, &deadline) < len) goto err ;
+    if (buffer_timed_put_g(buffer_1, "\r\n", 2, &deadline) < 2) goto err ;
+  }
+  if (buffer_timed_put_g(buffer_1, "\r\n", 2, &deadline) < 2
+   || !buffer_timed_flush_g(buffer_1, &deadline)) goto err ;
+  return 0 ;
+
+ err:
+  strerr_diefu1sys(111, "write to stdout") ;
+}