summary refs log tree commit diff
path: root/src/config
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/config
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/config')
-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
12 files changed, 1274 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 ;
+}