about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--doc/tipidee-config.html2
-rw-r--r--doc/tipidee.conf.html45
-rw-r--r--src/config/lexparse.c39
-rw-r--r--src/libtipidee/tipidee_conf_get.c2
-rw-r--r--src/tipideed/responses.c25
-rw-r--r--src/tipideed/tipideed.c19
6 files changed, 111 insertions, 21 deletions
diff --git a/doc/tipidee-config.html b/doc/tipidee-config.html
index c56c248..4e3433d 100644
--- a/doc/tipidee-config.html
+++ b/doc/tipidee-config.html
@@ -108,7 +108,7 @@ static — and that means incompressible RAM. By contrast, a cdb file
 is mapped read-only, so its pages are <em>shared clean</em>, which means it's
 essentially free. </li>
    <li> <a href="tipideed.html">tipideed</a> is exposed to the network. You
-want to its attack surface to be as small as possible. Taking the parsing code
+want its attack surface to be as small as possible. Taking the parsing code
 out of it goes a long way &mdash; admittedly, having to parse HTTP in the
 first place is more attack surface than a simple config file can ever hope
 to be, but every little bit helps. </li>
diff --git a/doc/tipidee.conf.html b/doc/tipidee.conf.html
index 483d9d0..4af3b8b 100644
--- a/doc/tipidee.conf.html
+++ b/doc/tipidee.conf.html
@@ -447,7 +447,7 @@ to clients with the <tt>Content-Type: <em>type</em></tt> header.
  <li> Example: <tt>content-type text/html .html .htm</tt> means that files
 ending in <tt>.html</tt> or <tt>.htm</tt> should be served as <tt>text/html</tt>.
  <li> tipidee already comes with a
-<a href="https://git.skarnet.org/cgi-bin/cgit.cgi/tipidee/tree/src/config/defaults.c#n26">large
+<a href="https://git.skarnet.org/cgi-bin/cgit.cgi/tipidee/tree/src/config/defaults.c#n19">large
 list</a> of default Content-Type mappings; this directive should only be necessary if you're
 serving files with uncommon extensions or have specific needs. </li>
 </ul>
@@ -705,5 +705,48 @@ requests received on port 443). But if you declare a redirection under the
 <tt>example.com</tt> domain, it will apply to requests received on <em>any</em> port. </li>
 </ul>
 
+<div id="custom-response">
+<h4> <tt>custom-response</tt> </h4>
+</div>
+
+<p>
+ <code> custom-response <em>status</em> <em>file</em> </code>
+</p>
+
+<ul>
+ <li> <tt>custom-response</tt> allows you to customize the contents of HTTP error
+responses for the current domain. </li>
+ <li> <em>status</em> is the 3-digit HTTP response code you want to specify a custom response for. </li>
+ <li> <em>file</em> is the file containing the body of the HTTP response that will be
+sent to the client if <a href="tipideed.html">tipideed</a> finds that it must answer
+with a <em>status</em> code.
+  <ul>
+   <li> <em>file</em> is used relative to tipideed's working directory (even if
+it is given absolute). It does not have to be under the current domain's document root:
+it is <strong>not a resource</strong>, and is not handled as such.
+However, <em>file</em> cannot go up the filesystem hierarchy: it will always
+be under tipideed's working directory. </li>
+   <li> The Content-Type for <em>file</em> is determined by its extension,
+and mappings you add via <tt>content-type</tt> directives will work. However,
+since <em>file</em> is not a resource, <tt>file-type</tt>
+directives will not work, even if <em>file</em> is under the current virtual
+domain's document root. Don't try to be smart with this. Just name your
+custom files <tt>e404.html</tt> or something. </li>
+  </ul> </li>
+ <li> The instruction is only valid for <em>status</em> responses to requests
+targetting the current domain. You can specify another <tt>custom-response</tt>,
+or even the same one, after the next <tt>domain</tt> directive. </li>
+ <li> An example: <tt>custom-response 404 /errors/404.html</tt> under
+a <tt>domain example.com</tt> line will mean that the <tt>errors/404.html</tt>
+file will be served as the text/html body of any "404 Not Found" response
+tipideed may send to requests addressed to <tt>example.com</tt>. The
+<tt>errors</tt> subdirectory is at the same level as the <tt>example.com</tt>
+subdirectory. </li>
+ <li> If tipideed cannot open <em>file</em>, it will log a warning message
+and answer the client with a basic builtin response. </li>
+ <li> Not every HTTP status code is used by tipideed. Nothing will happen
+if you define custom responses for codes that aren't used. </li>
+</ul>
+
 </body>
 </html>
diff --git a/src/config/lexparse.c b/src/config/lexparse.c
index d7d9e1b..88a9157 100644
--- a/src/config/lexparse.c
+++ b/src/config/lexparse.c
@@ -55,7 +55,8 @@ enum token_e
   T_NONNPH,
   T_BASICAUTH,
   T_NOAUTH,
-  T_FILETYPE
+  T_FILETYPE,
+  T_CUSTOMRESPONSE
 } ;
 
 struct directive_s
@@ -200,7 +201,7 @@ static inline void parse_redirect (char const *s, size_t const *word, size_t n,
   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) ;
+    strerr_dief6x(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 ;
@@ -237,7 +238,7 @@ static void parse_bitattr (char const *s, size_t const *word, size_t n, char con
   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) ;
+    strerr_dief6x(1, "resource", " must start with /", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
   {
     confnode const *oldnode ;
     size_t arglen = strlen(s + *word) ;
@@ -279,7 +280,7 @@ static inline void parse_filetype (char const *s, size_t const *word, size_t n,
   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) ;
+    strerr_dief6x(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]) ;
@@ -321,6 +322,32 @@ static inline void parse_filetype (char const *s, size_t const *word, size_t n,
   }
 }
 
+static inline void parse_customresponse (char const *s, size_t const *word, size_t n, char const *domain, size_t domainlen, mdt const *md)
+{
+  uint32_t status ;
+  size_t word1, len ;
+  char key[7 + domainlen] ;
+
+  if (n != 2)
+    strerr_dief8x(1, "too ", n > 2 ? "many" : "few", " arguments to directive ", "custom-response", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  if (!domain)
+    strerr_dief7x(1, "custom-response", " definition", " without a domain directive", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  if (!uint320_scan(s + word[0], &status) || status < 100 || status > 999)
+    strerr_dief6x(1, "invalid status", " for custom-response", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  word1 = word[1] ;
+  while (s[word1] == '/') word1++ ;
+  len = strlen(s + word1) ;
+  if (!len || !strncmp(s + word1, "../", 3) || strstr(s + word1, "/../") || (len >= 3 && !strcmp(s + word1 + len - 3, "/..")))
+    strerr_dief6x(1, "invalid file", " for custom-response", " in file ", g.storage.s + md->filepos, " line ", md->linefmt) ;
+  key[0] = 'E' ;
+  key[1] = ':' ;
+  uint32_fmt(key + 2, status) ;
+  key[5] = ':' ;
+  memcpy(key + 6, domain, domainlen) ;
+  key[6 + domainlen] = 0 ;
+  add_unique(key, s + word1, len + 1, md) ;
+}
+
 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[] =
@@ -329,6 +356,7 @@ static inline void process_line (char const *s, size_t const *word, size_t n, st
     { .s = "basic-auth", .token = T_BASICAUTH },
     { .s = "cgi", .token = T_CGI },
     { .s = "content-type", .token = T_CONTENTTYPE },
+    { .s = "custom-response", .token = T_CUSTOMRESPONSE },
     { .s = "domain", .token = T_DOMAIN },
     { .s = "file-type", .token = T_FILETYPE },
     { .s = "global", .token = T_GLOBAL },
@@ -420,6 +448,9 @@ static inline void process_line (char const *s, size_t const *word, size_t n, st
     case T_FILETYPE :
       parse_filetype(s, word, n, domain->s, domain->len, md) ;
       break ;
+    case T_CUSTOMRESPONSE :
+      parse_customresponse(s, word, n, domain->s, domain->len, md) ;
+      break ;
   }
 }
 
diff --git a/src/libtipidee/tipidee_conf_get.c b/src/libtipidee/tipidee_conf_get.c
index 2c96d2b..b056fd6 100644
--- a/src/libtipidee/tipidee_conf_get.c
+++ b/src/libtipidee/tipidee_conf_get.c
@@ -4,12 +4,14 @@
 #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) ;
+  LOLDEBUG("tipidee_conf_get: looking up %s", key) ;
   if (keylen > TIPIDEE_CONF_KEY_MAXLEN) return (errno = EINVAL, 0) ;
   switch (cdb_find(&conf->c, data, key, keylen))
   {
diff --git a/src/tipideed/responses.c b/src/tipideed/responses.c
index 0d0840c..70dcddf 100644
--- a/src/tipideed/responses.c
+++ b/src/tipideed/responses.c
@@ -8,6 +8,7 @@
 #include <skalibs/types.h>
 #include <skalibs/buffer.h>
 #include <skalibs/strerr.h>
+#include <skalibs/stralloc.h>
 #include <skalibs/tai.h>
 #include <skalibs/djbunix.h>
 #include <skalibs/unix-timed.h>
@@ -37,7 +38,14 @@ void response_error (tipidee_rql const *rql, char const *docroot, unsigned int s
 {
   tain deadline ;
   tipidee_defaulttext dt ;
-  char const *file = tipidee_conf_get_errorfile(&g.conf, docroot, status) ;
+  char const *file ;
+  size_t salen = g.sa.len ;
+  if (sarealpath(&g.sa, docroot) == -1 || !stralloc_0(&g.sa))
+      die500sys(rql, 111, docroot, "realpath ", docroot) ;
+  if (strncmp(g.sa.s + salen, g.sa.s, g.cwdlen) || g.sa.s[salen + g.cwdlen] != '/')
+    die500x(rql, 102, docroot, "docroot ", docroot, " points outside of the server's root") ;
+  file = tipidee_conf_get_errorfile(&g.conf, g.sa.s + salen + g.cwdlen + 1, status) ;
+  g.sa.len = salen ;
   if (!tipidee_util_defaulttext(status, &dt))
   {
     char fmt[UINT_FMT] ;
@@ -47,20 +55,27 @@ void response_error (tipidee_rql const *rql, char const *docroot, unsigned int s
 
   if (file)
   {
-    int fd = open_read(file) ;
-    if (fd == -1) strerr_warnwu3sys("open ", "custom error file ", file) ;
+    int fd ;
+    if (file[0] == '/')
+    {
+      char fmt[UINT_FMT] ;
+      fmt[uint_fmt(fmt, status)] = 0 ;
+      strerr_dief4x(102, "bad configuration: absolute path for custom ", fmt, " file: ", file) ;
+    }
+    fd = open_read(file) ;
+    if (fd == -1) strerr_warnwu3sys("open ", "custom response file ", file) ;
     else
     {
       struct stat st ;
       if (fstat(fd, &st) == -1)
       {
         fd_close(fd) ;
-        strerr_warnwu3sys("stat ", "custom error file ", file) ;
+        strerr_warnwu3sys("stat ", "custom response file ", file) ;
       }
       else if (!S_ISREG(st.st_mode))
       {
         fd_close(fd) ;
-        strerr_warnw3x("custom error file ", file, " is not a regular file") ;
+        strerr_warnw3x("custom response file ", file, " is not a regular file") ;
       }
       else
       {
diff --git a/src/tipideed/tipideed.c b/src/tipideed/tipideed.c
index b004782..b499b2a 100644
--- a/src/tipideed/tipideed.c
+++ b/src/tipideed/tipideed.c
@@ -195,18 +195,17 @@ static inline unsigned int indexify (tipidee_rql const *rql, char const *docroot
 
 static inline void get_resattr (tipidee_rql const *rql, char const *docroot, 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, docroot, "realpath ", res) ;
-  if (strncmp(sa.s, g.sa.s, g.cwdlen) || sa.s[g.cwdlen] != '/')
+  size_t pos = g.sa.len ;
+  if (sarealpath(&g.sa, res) == -1 || !stralloc_0(&g.sa)) die500sys(rql, 111, docroot, "realpath ", res) ;
+  if (strncmp(g.sa.s + pos, g.sa.s, g.cwdlen) || g.sa.s[pos + g.cwdlen] != '/')
     die500x(rql, 102, docroot, "resource ", res, " points outside of the server's root") ;
 
   {
     char const *attr = 0 ;
-    size_t len = sa.len - g.cwdlen + 1 ;
+    size_t len = g.sa.len - pos - 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) ;
+    memcpy(key + 2, g.sa.s + pos + 1 + g.cwdlen, len - 2) ;
     key[len] = '/' ;
     errno = ENOENT ;
     while (!attr)
@@ -233,18 +232,18 @@ static inline void get_resattr (tipidee_rql const *rql, char const *docroot, cha
         nphprefix = tipidee_conf_get_string(&g.conf, key) ;
         if (nphprefix)
         {
-          char const *base = strrchr(sa.s + g.cwdlen, '/') ;
+          char const *base = strrchr(g.sa.s + pos + 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, docroot, "get content type for ", sa.s + g.cwdlen) ;
+    ra->content_type = tipidee_conf_get_content_type(&g.conf, g.sa.s + pos + g.cwdlen) ;
+    if (!ra->content_type) die500sys(rql, 111, docroot, "get content type for ", g.sa.s + pos + g.cwdlen) ;
   }
+  g.sa.len = pos ;
 }
 
 static inline void force_redirect (tipidee_rql const *rql, char const *fn)