<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">From d3e4e8ad1493679392966f3adbbf7f7c7054cd0a Mon Sep 17 00:00:00 2001
From: Glenn Strauss &lt;gstrauss@gluelogic.com&gt;
Date: Thu, 13 Apr 2023 16:14:48 -0400
Subject: [PATCH] Revert "[multiple] remove deprecated modules"

This reverts commit fcf0dc3e336a5d62c58036cdb8fc9f4c099b178e.
---
 src/CMakeLists.txt       |   8 +
 src/Makefile.am          |  25 ++
 src/SConscript           |   4 +
 src/meson.build          |   4 +
 src/mod_evasive.c        | 194 +++++++++++++++
 src/mod_secdownload.c    | 502 +++++++++++++++++++++++++++++++++++++++
 src/mod_uploadprogress.c | 334 ++++++++++++++++++++++++++
 src/mod_usertrack.c      | 242 +++++++++++++++++++
 src/t/test_mod.c         |   2 +
 src/t/test_mod_evasive.c |  72 ++++++
 tests/lighttpd.conf      |  27 +++
 tests/request.t          | 192 ++++++++++++++-
 12 files changed, 1605 insertions(+), 1 deletion(-)
 create mode 100644 src/mod_evasive.c
 create mode 100644 src/mod_secdownload.c
 create mode 100644 src/mod_uploadprogress.c
 create mode 100644 src/mod_usertrack.c
 create mode 100644 src/t/test_mod_evasive.c

--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -953,14 +953,18 @@ add_and_install_library(mod_authn_file "
 add_and_install_library(mod_cgi mod_cgi.c)
 add_and_install_library(mod_deflate mod_deflate.c)
 add_and_install_library(mod_dirlisting mod_dirlisting.c)
+add_and_install_library(mod_evasive mod_evasive.c)
 add_and_install_library(mod_extforward mod_extforward.c)
 add_and_install_library(mod_h2 "h2.c;ls-hpack/lshpack.c;algo_xxhash.c")
 add_and_install_library(mod_proxy mod_proxy.c)
 add_and_install_library(mod_rrdtool mod_rrdtool.c)
+add_and_install_library(mod_secdownload "mod_secdownload.c;algo_hmac.c")
 add_and_install_library(mod_sockproxy mod_sockproxy.c)
 add_and_install_library(mod_ssi mod_ssi.c)
 add_and_install_library(mod_status mod_status.c)
+add_and_install_library(mod_uploadprogress mod_uploadprogress.c)
 add_and_install_library(mod_userdir mod_userdir.c)
+add_and_install_library(mod_usertrack mod_usertrack.c)
 if(WIN32 AND NOT BUILD_STATIC)
 add_and_install_library(mod_vhostdb "mod_vhostdb.c")
 else()
@@ -988,6 +992,7 @@ add_executable(test_mod
 	t/test_mod.c
 	t/test_mod_access.c
 	t/test_mod_alias.c
+	t/test_mod_evasive.c
 	t/test_mod_evhost.c
 	t/test_mod_indexfile.c
 	t/test_mod_simple_vhost.c
@@ -1188,6 +1193,8 @@ if(NOT ${CRYPTO_LIBRARY} EQUAL "")
 	target_link_libraries(mod_auth ${CRYPTO_LIBRARY})
 	set(L_MOD_AUTHN_FILE ${L_MOD_AUTHN_FILE} ${CRYPTO_LIBRARY})
 	target_link_libraries(mod_authn_file ${L_MOD_AUTHN_FILE})
+	target_link_libraries(mod_secdownload ${CRYPTO_LIBRARY})
+	target_link_libraries(mod_usertrack ${CRYPTO_LIBRARY})
 	target_link_libraries(mod_wstunnel ${CRYPTO_LIBRARY})
 	target_link_libraries(test_mod ${CRYPTO_LIBRARY})
 endif()
@@ -1212,6 +1219,7 @@ if(HAVE_LIBMBEDTLS AND HAVE_LIBMEDCRYPTO
 	add_and_install_library(mod_mbedtls "mod_mbedtls.c")
 	set(L_MOD_MBEDTLS ${L_MOD_MBEDTLS} mbedtls mbedcrypto mbedx509)
 	target_link_libraries(mod_mbedtls ${L_MOD_MBEDTLS})
+	# not doing "cross module" linkage yet (e.g. mod_authn, secdownload)
 endif()
 
 if(HAVE_LIBSSL3 AND HAVE_LIBSMIME3 AND HAVE_LIBNSS3 AND HAVE_LIBNSSUTIL3)
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -134,6 +134,11 @@ mod_maxminddb_la_LDFLAGS = $(common_modu
 mod_maxminddb_la_LIBADD = $(common_libadd) $(MAXMINDDB_LIB)
 endif
 
+lib_LTLIBRARIES += mod_evasive.la
+mod_evasive_la_SOURCES = mod_evasive.c
+mod_evasive_la_LDFLAGS = $(common_module_ldflags)
+mod_evasive_la_LIBADD = $(common_libadd)
+
 lib_LTLIBRARIES += mod_webdav.la
 mod_webdav_la_SOURCES = mod_webdav.c
 mod_webdav_la_CFLAGS = $(AM_CFLAGS) $(XML_CFLAGS) $(SQLITE_CFLAGS) 
@@ -207,6 +212,11 @@ mod_rrdtool_la_SOURCES = mod_rrdtool.c
 mod_rrdtool_la_LDFLAGS = $(common_module_ldflags)
 mod_rrdtool_la_LIBADD = $(common_libadd)
 
+lib_LTLIBRARIES += mod_usertrack.la
+mod_usertrack_la_SOURCES = mod_usertrack.c
+mod_usertrack_la_LDFLAGS = $(common_module_ldflags)
+mod_usertrack_la_LIBADD = $(common_libadd) $(CRYPTO_LIB)
+
 lib_LTLIBRARIES += mod_proxy.la
 mod_proxy_la_SOURCES = mod_proxy.c
 mod_proxy_la_LDFLAGS = $(common_module_ldflags)
@@ -222,6 +232,11 @@ mod_ssi_la_SOURCES = mod_ssi.c
 mod_ssi_la_LDFLAGS = $(common_module_ldflags)
 mod_ssi_la_LIBADD = $(common_libadd)
 
+lib_LTLIBRARIES += mod_secdownload.la
+mod_secdownload_la_SOURCES = mod_secdownload.c algo_hmac.c
+mod_secdownload_la_LDFLAGS = $(common_module_ldflags)
+mod_secdownload_la_LIBADD = $(common_libadd) $(CRYPTO_LIB)
+
 lib_LTLIBRARIES += mod_ajp13.la
 mod_ajp13_la_SOURCES = mod_ajp13.c
 mod_ajp13_la_LDFLAGS = $(common_module_ldflags)
@@ -336,6 +351,11 @@ mod_accesslog_la_SOURCES = mod_accesslog
 mod_accesslog_la_LDFLAGS = $(common_module_ldflags)
 mod_accesslog_la_LIBADD = $(common_libadd)
 
+lib_LTLIBRARIES += mod_uploadprogress.la
+mod_uploadprogress_la_SOURCES = mod_uploadprogress.c
+mod_uploadprogress_la_LDFLAGS = $(common_module_ldflags)
+mod_uploadprogress_la_LIBADD = $(common_libadd)
+
 lib_LTLIBRARIES += mod_wstunnel.la
 mod_wstunnel_la_SOURCES = mod_wstunnel.c
 mod_wstunnel_la_LDFLAGS = $(common_module_ldflags)
@@ -394,6 +414,7 @@ lighttpd_SOURCES = \
   mod_deflate.c \
   mod_dirlisting.c \
   mod_evhost.c \
+  mod_evasive.c \
   mod_expire.c \
   mod_extforward.c \
   mod_fastcgi.c \
@@ -403,13 +424,16 @@ lighttpd_SOURCES = \
   mod_rewrite.c \
   mod_rrdtool.c \
   mod_scgi.c \
+  mod_secdownload.c algo_hmac.c \
   mod_setenv.c \
   mod_simple_vhost.c \
   mod_sockproxy.c \
   mod_ssi.c \
   mod_staticfile.c \
   mod_status.c \
+  mod_uploadprogress.c \
   mod_userdir.c \
+  mod_usertrack.c \
   mod_vhostdb.c \
   mod_vhostdb_api.c \
   mod_webdav.c
@@ -520,6 +544,7 @@ t_test_configfile_LDADD = $(PCRE_LIB) $(
 t_test_mod_SOURCES = $(common_src) t/test_mod.c \
                      t/test_mod_access.c \
                      t/test_mod_alias.c \
+                     t/test_mod_evasive.c \
                      t/test_mod_evhost.c \
                      t/test_mod_indexfile.c \
                      t/test_mod_simple_vhost.c \
--- a/src/SConscript
+++ b/src/SConscript
@@ -116,14 +116,18 @@ modules = {
 	'mod_cgi' : { 'src' : [ 'mod_cgi.c' ] },
 	'mod_deflate' : { 'src' : [ 'mod_deflate.c' ], 'lib' : [ env['LIBZ'], env['LIBZSTD'], env['LIBBZ2'], env['LIBBROTLI'], env['LIBDEFLATE'], 'm' ] },
 	'mod_dirlisting' : { 'src' : [ 'mod_dirlisting.c' ] },
+	'mod_evasive' : { 'src' : [ 'mod_evasive.c' ] },
 	'mod_extforward' : { 'src' : [ 'mod_extforward.c' ] },
 	'mod_h2' : { 'src' : [ 'h2.c', 'ls-hpack/lshpack.c', 'algo_xxhash.c' ], 'lib' : [ env['LIBXXHASH'] ] },
 	'mod_proxy' : { 'src' : [ 'mod_proxy.c' ] },
 	'mod_rrdtool' : { 'src' : [ 'mod_rrdtool.c' ] },
+	'mod_secdownload' : { 'src' : [ 'mod_secdownload.c', 'algo_hmac.c' ], 'lib' : [ env['LIBCRYPTO'] ] },
 	'mod_sockproxy' : { 'src' : [ 'mod_sockproxy.c' ] },
 	'mod_ssi' : { 'src' : [ 'mod_ssi.c' ] },
 	'mod_status' : { 'src' : [ 'mod_status.c' ] },
+	'mod_uploadprogress' : { 'src' : [ 'mod_uploadprogress.c' ] },
 	'mod_userdir' : { 'src' : [ 'mod_userdir.c' ] },
+	'mod_usertrack' : { 'src' : [ 'mod_usertrack.c' ], 'lib' : [ env['LIBCRYPTO'] ] },
 	'mod_vhostdb' : { 'src' : [ 'mod_vhostdb.c', 'mod_vhostdb_api.c' ] },
 	'mod_webdav' : { 'src' : [ 'mod_webdav.c' ], 'lib' : [ env['LIBXML2'], env['LIBSQLITE3'], env['LIBUUID'] ] },
 	'mod_wstunnel' : { 'src' : [ 'mod_wstunnel.c' ], 'lib' : [ env['LIBCRYPTO'] ] },
--- a/src/meson.build
+++ b/src/meson.build
@@ -776,14 +776,18 @@ modules = [
 	[ 'mod_cgi', [ 'mod_cgi.c' ] ],
 	[ 'mod_deflate', [ 'mod_deflate.c' ], [ libbz2, libz, libzstd, libbrotli, libdeflate ] ],
 	[ 'mod_dirlisting', [ 'mod_dirlisting.c' ] ],
+	[ 'mod_evasive', [ 'mod_evasive.c' ] ],
 	[ 'mod_extforward', [ 'mod_extforward.c' ] ],
 	[ 'mod_h2', [ 'h2.c', 'ls-hpack/lshpack.c', 'algo_xxhash.c' ], [ libxxhash ] ],
 	[ 'mod_proxy', [ 'mod_proxy.c' ], socket_libs ],
 	[ 'mod_rrdtool', [ 'mod_rrdtool.c' ] ],
+	[ 'mod_secdownload', [ 'mod_secdownload.c', 'algo_hmac.c' ], libcrypto ],
 	[ 'mod_sockproxy', [ 'mod_sockproxy.c' ] ],
 	[ 'mod_ssi', [ 'mod_ssi.c' ], socket_libs ],
 	[ 'mod_status', [ 'mod_status.c' ] ],
+	[ 'mod_uploadprogress', [ 'mod_uploadprogress.c' ] ],
 	[ 'mod_userdir', [ 'mod_userdir.c' ] ],
+	[ 'mod_usertrack', [ 'mod_usertrack.c' ], libcrypto ],
 	[ 'mod_vhostdb', [ 'mod_vhostdb.c', 'mod_vhostdb_api.c' ] ],
 	[ 'mod_webdav', [ 'mod_webdav.c' ], [ libsqlite3, libuuid, libxml2, libelftc ] ],
 	[ 'mod_wstunnel', [ 'mod_wstunnel.c' ], libcrypto ],
--- /dev/null
+++ b/src/mod_evasive.c
@@ -0,0 +1,194 @@
+#include "first.h"
+
+#include "base.h"
+#include "log.h"
+#include "buffer.h"
+#include "http_header.h"
+#include "sock_addr.h"
+
+#include "plugin.h"
+
+#include &lt;stdlib.h&gt;
+#include &lt;string.h&gt;
+
+/**
+ * mod_evasive
+ *
+ * A combination of lighttpd modules provides similar features
+ * to those in (old) Apache mod_evasive
+ *
+ * - limit of connections per IP
+ *     ==&gt; mod_evasive
+ * - provide a list of block-listed ip/networks (no access)
+ *     ==&gt; block at firewall
+ *     ==&gt; block using lighttpd.conf conditionals and mod_access
+ *     ==&gt; block using mod_magnet and an external (updatable) constant database
+ *         https://wiki.lighttpd.net/AbsoLUAtion#Fight-DDoS
+ * - provide a white-list of ips/network which is not affected by the limit
+ *     ==&gt; allow using lighttpd.conf conditionals
+ *         and configure evasive.max-conns-per-ip = 0 for whitelist
+ * - provide a bandwidth limiter per IP
+ *     ==&gt; set using lighttpd.conf conditionals
+ *         and configure connection.kbytes-per-second
+ * - enforce additional policy using mod_magnet and libmodsecurity
+ *     ==&gt; https://wiki.lighttpd.net/AbsoLUAtion#Mod_Security
+ *
+ * started by:
+ * - w1zzard@techpowerup.com
+ */
+
+typedef struct {
+    unsigned short max_conns;
+    unsigned short silent;
+    const buffer *location;
+} plugin_config;
+
+typedef struct {
+    PLUGIN_DATA;
+    plugin_config defaults;
+    plugin_config conf;
+} plugin_data;
+
+INIT_FUNC(mod_evasive_init) {
+    return calloc(1, sizeof(plugin_data));
+}
+
+static void mod_evasive_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
+    switch (cpv-&gt;k_id) { /* index into static config_plugin_keys_t cpk[] */
+      case 0: /* evasive.max-conns-per-ip */
+        pconf-&gt;max_conns = cpv-&gt;v.shrt;
+        break;
+      case 1: /* evasive.silent */
+        pconf-&gt;silent = (0 != cpv-&gt;v.u);
+        break;
+      case 2: /* evasive.location */
+        pconf-&gt;location = cpv-&gt;v.b;
+        break;
+      default:/* should not happen */
+        return;
+    }
+}
+
+static void mod_evasive_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
+    do {
+        mod_evasive_merge_config_cpv(pconf, cpv);
+    } while ((++cpv)-&gt;k_id != -1);
+}
+
+static void mod_evasive_patch_config(request_st * const r, plugin_data * const p) {
+    p-&gt;conf = p-&gt;defaults; /* copy small struct instead of memcpy() */
+    /*memcpy(&amp;p-&gt;conf, &amp;p-&gt;defaults, sizeof(plugin_config));*/
+    for (int i = 1, used = p-&gt;nconfig; i &lt; used; ++i) {
+        if (config_check_cond(r, (uint32_t)p-&gt;cvlist[i].k_id))
+            mod_evasive_merge_config(&amp;p-&gt;conf,p-&gt;cvlist + p-&gt;cvlist[i].v.u2[0]);
+    }
+}
+
+SETDEFAULTS_FUNC(mod_evasive_set_defaults) {
+    static const config_plugin_keys_t cpk[] = {
+      { CONST_STR_LEN("evasive.max-conns-per-ip"),
+        T_CONFIG_SHORT,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ CONST_STR_LEN("evasive.silent"),
+        T_CONFIG_BOOL,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ CONST_STR_LEN("evasive.location"),
+        T_CONFIG_STRING,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ NULL, 0,
+        T_CONFIG_UNSET,
+        T_CONFIG_SCOPE_UNSET }
+    };
+
+    plugin_data * const p = p_d;
+    if (!config_plugin_values_init(srv, p, cpk, "mod_evasive"))
+        return HANDLER_ERROR;
+
+    /* process and validate config directives
+     * (init i to 0 if global context; to 1 to skip empty global context) */
+    for (int i = !p-&gt;cvlist[0].v.u2[1]; i &lt; p-&gt;nconfig; ++i) {
+        config_plugin_value_t *cpv = p-&gt;cvlist + p-&gt;cvlist[i].v.u2[0];
+        for (; -1 != cpv-&gt;k_id; ++cpv) {
+            switch (cpv-&gt;k_id) {
+              case 0: /* evasive.max-conns-per-ip */
+              case 1: /* evasive.silent */
+                break;
+              case 2: /* evasive.location */
+                if (buffer_is_blank(cpv-&gt;v.b))
+                    cpv-&gt;v.b = NULL;
+                break;
+              default:/* should not happen */
+                break;
+            }
+        }
+    }
+
+    /* initialize p-&gt;defaults from global config context */
+    if (p-&gt;nconfig &gt; 0 &amp;&amp; p-&gt;cvlist-&gt;v.u2[1]) {
+        const config_plugin_value_t *cpv = p-&gt;cvlist + p-&gt;cvlist-&gt;v.u2[0];
+        if (-1 != cpv-&gt;k_id)
+            mod_evasive_merge_config(&amp;p-&gt;defaults, cpv);
+    }
+
+    return HANDLER_GO_ON;
+}
+
+__attribute_cold__
+__attribute_noinline__
+static handler_t
+mod_evasive_reached_per_ip_limit (request_st * const r, const plugin_data * const p)
+{
+			if (!p-&gt;conf.silent) {
+				log_error(r-&gt;conf.errh, __FILE__, __LINE__,
+				  "%s turned away. Too many connections.",
+				  r-&gt;con-&gt;dst_addr_buf.ptr);
+			}
+
+			if (p-&gt;conf.location) {
+				http_header_response_set(r, HTTP_HEADER_LOCATION,
+				                         CONST_STR_LEN("Location"),
+				                         BUF_PTR_LEN(p-&gt;conf.location));
+				r-&gt;http_status = 302;
+				r-&gt;resp_body_finished = 1;
+			} else {
+				r-&gt;http_status = 403;
+			}
+			r-&gt;handler_module = NULL;
+			return HANDLER_FINISHED;
+}
+
+static handler_t
+mod_evasive_check_per_ip_limit (request_st * const r, const plugin_data * const p, const connection *c)
+{
+    const sock_addr * const dst_addr = &amp;r-&gt;con-&gt;dst_addr;
+    for (uint_fast32_t conns_by_ip = 0; c; c = c-&gt;next) {
+        /* count connections already actively serving data for the same IP
+         * (only count connections already behind the 'read request' state) */
+        if (c-&gt;request.state &gt; CON_STATE_REQUEST_END
+            &amp;&amp; sock_addr_is_addr_eq(&amp;c-&gt;dst_addr, dst_addr)
+            &amp;&amp; ++conns_by_ip &gt; p-&gt;conf.max_conns)
+            return mod_evasive_reached_per_ip_limit(r, p);/* HANDLER_FINISHED */
+    }
+    return HANDLER_GO_ON;
+}
+
+URIHANDLER_FUNC(mod_evasive_uri_handler) {
+    plugin_data * const p = p_d;
+    mod_evasive_patch_config(r, p);
+    return (p-&gt;conf.max_conns == 0) /* no limit set, nothing to block */
+      ? HANDLER_GO_ON
+      : mod_evasive_check_per_ip_limit(r, p, r-&gt;con-&gt;srv-&gt;conns);
+}
+
+
+int mod_evasive_plugin_init(plugin *p);
+int mod_evasive_plugin_init(plugin *p) {
+	p-&gt;version     = LIGHTTPD_VERSION_ID;
+	p-&gt;name        = "evasive";
+
+	p-&gt;init        = mod_evasive_init;
+	p-&gt;set_defaults = mod_evasive_set_defaults;
+	p-&gt;handle_uri_clean  = mod_evasive_uri_handler;
+
+	return 0;
+}
--- /dev/null
+++ b/src/mod_secdownload.c
@@ -0,0 +1,502 @@
+#include "first.h"
+
+#include "base.h"
+#include "log.h"
+#include "buffer.h"
+#include "base64.h"
+#include "ck.h"
+
+#include "plugin.h"
+
+#include &lt;stdlib.h&gt;
+#include &lt;string.h&gt;
+
+#include "sys-crypto-md.h"
+#include "algo_hmac.h"
+
+/*
+ * mod_secdownload verifies a checksum associated with a timestamp
+ * and a path.
+ *
+ * It takes an URL of the form:
+ *   securl := &lt;uri-prefix&gt; &lt;mac&gt; &lt;protected-path&gt;
+ *   uri-prefix := '/' any*         # whatever was configured: must start with a '/')
+ *   mac := [a-zA-Z0-9_-]{mac_len}  # mac length depends on selected algorithm
+ *   protected-path := '/' &lt;timestamp&gt; &lt;rel-path&gt;
+ *   timestamp := [a-f0-9]{1,16}    # timestamp when the checksum was calculated
+ *                                  # to prevent access after timeout (active requests
+ *                                  # will finish successfully even after the timeout)
+ *   rel-path := '/' any*           # the protected path; changing the path breaks the
+ *                                  # checksum
+ *
+ * The timestamp is the `epoch` timestamp in hex, i.e. time in seconds
+ * since 00:00:00 UTC on 1 January 1970.
+ *
+ * mod_secdownload supports various MAC algorithms:
+ *
+ * # md5
+ * mac_len := 32 (and hex only)
+ * mac := md5-hex(&lt;secrect&gt;&lt;rel-path&gt;&lt;timestamp&gt;)   # lowercase hex
+ * perl example:
+    use Digest::MD5 qw(md5_hex);
+    my $secret = "verysecret";
+    my $rel_path = "/index.html"
+    my $xtime = sprintf("%x", time);
+    my $url = '/'. md5_hex($secret . $rel_path . $xtime) . '/' . $xtime . $rel_path;
+ *
+ * # hmac-sha1
+ * mac_len := 27  (no base64 padding)
+ * mac := base64-url(hmac-sha1(&lt;secret&gt;, &lt;protected-path&gt;))
+ * perl example:
+    use Digest::SHA qw(hmac_sha1);
+    use MIME::Base64 qw(encode_base64url);
+    my $secret = "verysecret";
+    my $rel_path = "/index.html"
+    my $protected_path = '/' . sprintf("%x", time) . $rel_path;
+    my $url = '/'. encode_base64url(hmac_sha1($protected_path, $secret)) . $protected_path;
+ *
+ * # hmac-sha256
+ * mac_len := 43  (no base64 padding)
+ * mac := base64-url(hmac-sha256(&lt;secret&gt;, &lt;protected-path&gt;))
+    use Digest::SHA qw(hmac_sha256);
+    use MIME::Base64 qw(encode_base64url);
+    my $secret = "verysecret";
+    my $rel_path = "/index.html"
+    my $protected_path = '/' . sprintf("%x", time) . $rel_path;
+    my $url = '/'. encode_base64url(hmac_sha256($protected_path, $secret)) . $protected_path;
+ *
+ */
+
+/* plugin config for all request/connections */
+
+typedef enum {
+	SECDL_INVALID = 0,
+	SECDL_MD5 = 1,
+	SECDL_HMAC_SHA1 = 2,
+	SECDL_HMAC_SHA256 = 3,
+} secdl_algorithm;
+
+typedef struct {
+    const buffer *doc_root;
+    const buffer *secret;
+    const buffer *uri_prefix;
+    secdl_algorithm algorithm;
+
+    unsigned int timeout;
+    unsigned short path_segments;
+    unsigned short hash_querystr;
+} plugin_config;
+
+typedef struct {
+    PLUGIN_DATA;
+    plugin_config defaults;
+    plugin_config conf;
+} plugin_data;
+
+static const char* secdl_algorithm_names[] = {
+	"invalid",
+	"md5",
+	"hmac-sha1",
+	"hmac-sha256",
+};
+
+static secdl_algorithm algorithm_from_string(const buffer *name) {
+	size_t ndx;
+
+	if (buffer_is_blank(name)) return SECDL_INVALID;
+
+	for (ndx = 1; ndx &lt; sizeof(secdl_algorithm_names)/sizeof(secdl_algorithm_names[0]); ++ndx) {
+		if (0 == strcmp(secdl_algorithm_names[ndx], name-&gt;ptr)) return (secdl_algorithm)ndx;
+	}
+
+	return SECDL_INVALID;
+}
+
+static size_t secdl_algorithm_mac_length(secdl_algorithm alg) {
+	switch (alg) {
+	case SECDL_INVALID:
+		break;
+	case SECDL_MD5:
+		return 32;
+	case SECDL_HMAC_SHA1:
+		return 27;
+	case SECDL_HMAC_SHA256:
+		return 43;
+	}
+	return 0;
+}
+
+static int secdl_verify_mac(plugin_config *config, const char* protected_path, const char* mac, size_t maclen, log_error_st *errh) {
+	UNUSED(errh);
+	if (0 == maclen || secdl_algorithm_mac_length(config-&gt;algorithm) != maclen) return 0;
+
+	switch (config-&gt;algorithm) {
+	case SECDL_INVALID:
+		break;
+	case SECDL_MD5:
+		{
+			const char *ts_str;
+			const char *rel_uri;
+			unsigned char HA1[MD5_DIGEST_LENGTH];
+			unsigned char md5bin[MD5_DIGEST_LENGTH];
+
+			if (0 != li_hex2bin(md5bin, sizeof(md5bin), mac, maclen)) return 0;
+
+			/* legacy message:
+			 *   protected_path := '/' &lt;timestamp-hex&gt; &lt;rel-path&gt;
+			 *   timestamp-hex := [0-9a-f]{1,16}
+			 *   rel-path := '/' any*
+			 *   (the protected path was already verified)
+			 * message = &lt;secret&gt;&lt;rel-path&gt;&lt;timestamp-hex&gt;
+			 */
+			ts_str = protected_path + 1;
+			rel_uri = ts_str;
+			do { ++rel_uri; } while (*rel_uri != '/');
+
+			struct const_iovec iov[] = {
+			  { BUF_PTR_LEN(config-&gt;secret) }
+			 ,{ rel_uri, strlen(rel_uri) }
+			 ,{ ts_str, (size_t)(rel_uri - ts_str) }
+			};
+			MD5_iov(HA1, iov, sizeof(iov)/sizeof(*iov));
+
+			return ck_memeq_const_time_fixed_len((char *)HA1,
+							     (char *)md5bin,sizeof(md5bin));
+		}
+     #ifdef USE_LIB_CRYPTO
+	case SECDL_HMAC_SHA1:
+		{
+			unsigned char digest[20];
+			char base64_digest[28];
+
+                        if (!li_hmac_sha1(digest, BUF_PTR_LEN(config-&gt;secret),
+			                  (const unsigned char *)protected_path,
+			                  strlen(protected_path))) {
+				log_error(errh, __FILE__, __LINE__,
+				  "hmac-sha1: HMAC() failed");
+				return 0;
+			}
+
+			li_to_base64_no_padding(base64_digest, 28, digest, 20, BASE64_URL);
+
+			return (27 == maclen)
+			    &amp;&amp; ck_memeq_const_time_fixed_len(mac, base64_digest, 27);
+		}
+		break;
+	case SECDL_HMAC_SHA256:
+		{
+			unsigned char digest[32];
+			char base64_digest[44];
+
+                        if (!li_hmac_sha256(digest, BUF_PTR_LEN(config-&gt;secret),
+			                    (const unsigned char *)protected_path,
+			                    strlen(protected_path))) {
+				log_error(errh, __FILE__, __LINE__,
+				  "hmac-sha256: HMAC() failed");
+				return 0;
+			}
+
+			li_to_base64_no_padding(base64_digest, 44, digest, 32, BASE64_URL);
+
+			return (43 == maclen)
+			    &amp;&amp; ck_memeq_const_time_fixed_len(mac, base64_digest, 43);
+		}
+		break;
+     #endif
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+INIT_FUNC(mod_secdownload_init) {
+    return calloc(1, sizeof(plugin_data));
+}
+
+static int mod_secdownload_parse_algorithm(config_plugin_value_t * const cpv, log_error_st * const errh) {
+    secdl_algorithm algorithm = algorithm_from_string(cpv-&gt;v.b);
+    switch (algorithm) {
+      case SECDL_INVALID:
+        log_error(errh, __FILE__, __LINE__,
+          "invalid secdownload.algorithm: %s", cpv-&gt;v.b-&gt;ptr);
+        return 0;
+     #ifndef USE_LIB_CRYPTO
+      case SECDL_HMAC_SHA1:
+      case SECDL_HMAC_SHA256:
+        log_error(errh, __FILE__, __LINE__,
+          "unsupported secdownload.algorithm: %s", cpv-&gt;v.b-&gt;ptr);
+        /*return 0;*/
+        /* proceed to allow config to load for other tests */
+        /* (use of unsupported algorithm will result in failure at runtime) */
+        break;
+     #endif
+      default:
+        break;
+    }
+
+    cpv-&gt;vtype = T_CONFIG_INT;
+    cpv-&gt;v.u = algorithm;
+    return 1;
+}
+
+static void mod_secdownload_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
+    switch (cpv-&gt;k_id) { /* index into static config_plugin_keys_t cpk[] */
+      case 0: /* secdownload.secret */
+        pconf-&gt;secret = cpv-&gt;v.b;
+        break;
+      case 1: /* secdownload.document-root */
+        pconf-&gt;doc_root = cpv-&gt;v.b;
+        break;
+      case 2: /* secdownload.uri-prefix */
+        pconf-&gt;uri_prefix = cpv-&gt;v.b;
+        break;
+      case 3: /* secdownload.timeout */
+        pconf-&gt;timeout = cpv-&gt;v.u;
+        break;
+      case 4: /* secdownload.algorithm */
+        pconf-&gt;algorithm = cpv-&gt;v.u; /* mod_secdownload_parse_algorithm() */
+        break;
+      case 5: /* secdownload.path-segments */
+        pconf-&gt;path_segments = cpv-&gt;v.shrt;
+        break;
+      case 6: /* secdownload.hash-querystr */
+        pconf-&gt;hash_querystr = cpv-&gt;v.u;
+        break;
+      default:/* should not happen */
+        return;
+    }
+}
+
+static void mod_secdownload_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
+    do {
+        mod_secdownload_merge_config_cpv(pconf, cpv);
+    } while ((++cpv)-&gt;k_id != -1);
+}
+
+static void mod_secdownload_patch_config(request_st * const r, plugin_data * const p) {
+    memcpy(&amp;p-&gt;conf, &amp;p-&gt;defaults, sizeof(plugin_config));
+    for (int i = 1, used = p-&gt;nconfig; i &lt; used; ++i) {
+        if (config_check_cond(r, (uint32_t)p-&gt;cvlist[i].k_id))
+            mod_secdownload_merge_config(&amp;p-&gt;conf, p-&gt;cvlist + p-&gt;cvlist[i].v.u2[0]);
+    }
+}
+
+SETDEFAULTS_FUNC(mod_secdownload_set_defaults) {
+    static const config_plugin_keys_t cpk[] = {
+      { CONST_STR_LEN("secdownload.secret"),
+        T_CONFIG_STRING,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ CONST_STR_LEN("secdownload.document-root"),
+        T_CONFIG_STRING,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ CONST_STR_LEN("secdownload.uri-prefix"),
+        T_CONFIG_STRING,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ CONST_STR_LEN("secdownload.timeout"),
+        T_CONFIG_INT,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ CONST_STR_LEN("secdownload.algorithm"),
+        T_CONFIG_STRING,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ CONST_STR_LEN("secdownload.path-segments"),
+        T_CONFIG_SHORT,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ CONST_STR_LEN("secdownload.hash-querystr"),
+        T_CONFIG_BOOL,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ NULL, 0,
+        T_CONFIG_UNSET,
+        T_CONFIG_SCOPE_UNSET }
+    };
+
+    plugin_data * const p = p_d;
+    if (!config_plugin_values_init(srv, p, cpk, "mod_secdownload"))
+        return HANDLER_ERROR;
+
+    /* process and validate config directives
+     * (init i to 0 if global context; to 1 to skip empty global context) */
+    for (int i = !p-&gt;cvlist[0].v.u2[1]; i &lt; p-&gt;nconfig; ++i) {
+        config_plugin_value_t *cpv = p-&gt;cvlist + p-&gt;cvlist[i].v.u2[0];
+        for (; -1 != cpv-&gt;k_id; ++cpv) {
+            switch (cpv-&gt;k_id) {
+              case 0: /* secdownload.secret */
+              case 1: /* secdownload.document-root */
+              case 2: /* secdownload.uri-prefix */
+                if (buffer_is_blank(cpv-&gt;v.b))
+                    cpv-&gt;v.b = NULL;
+                break;
+              case 3: /* secdownload.timeout */
+                break;
+              case 4: /* secdownload.algorithm */
+                if (!mod_secdownload_parse_algorithm(cpv, srv-&gt;errh))
+                    return HANDLER_ERROR;
+                break;
+              case 5: /* secdownload.path-segments */
+              case 6: /* secdownload.hash-querystr */
+                break;
+              default:/* should not happen */
+                break;
+            }
+        }
+    }
+
+    p-&gt;defaults.timeout = 60;
+
+    /* initialize p-&gt;defaults from global config context */
+    if (p-&gt;nconfig &gt; 0 &amp;&amp; p-&gt;cvlist-&gt;v.u2[1]) {
+        const config_plugin_value_t *cpv = p-&gt;cvlist + p-&gt;cvlist-&gt;v.u2[0];
+        if (-1 != cpv-&gt;k_id)
+            mod_secdownload_merge_config(&amp;p-&gt;defaults, cpv);
+    }
+
+    return HANDLER_GO_ON;
+}
+
+/**
+ * checks if the supplied string is a base64 (modified URL) string
+ *
+ * @param str a possible base64 (modified URL) string
+ * @return if the supplied string is a valid base64 (modified URL) string 1 is returned otherwise 0
+ */
+
+static int is_base64_len(const char *str, size_t len) {
+	size_t i;
+
+	if (NULL == str) return 0;
+
+	for (i = 0; i &lt; len &amp;&amp; *str; i++, str++) {
+		/* illegal characters */
+		if (!(light_isalnum(*str) || *str == '-' || *str == '_'))
+			return 0;
+	}
+
+	return i == len;
+}
+
+URIHANDLER_FUNC(mod_secdownload_uri_handler) {
+	plugin_data *p = p_d;
+	const char *rel_uri, *ts_str, *mac_str, *protected_path;
+	size_t i, mac_len;
+
+	if (NULL != r-&gt;handler_module) return HANDLER_GO_ON;
+
+  #ifdef __COVERITY__
+	if (buffer_is_blank(&amp;r-&gt;uri.path)) return HANDLER_GO_ON;
+  #endif
+
+	mod_secdownload_patch_config(r, p);
+
+	if (!p-&gt;conf.uri_prefix) return HANDLER_GO_ON;
+
+	if (!p-&gt;conf.secret) {
+		log_error(r-&gt;conf.errh, __FILE__, __LINE__,
+		  "secdownload.secret has to be set");
+		r-&gt;http_status = 500;
+		return HANDLER_FINISHED;
+	}
+
+	if (!p-&gt;conf.doc_root) {
+		log_error(r-&gt;conf.errh, __FILE__, __LINE__,
+		  "secdownload.document-root has to be set");
+		r-&gt;http_status = 500;
+		return HANDLER_FINISHED;
+	}
+
+	if (SECDL_INVALID == p-&gt;conf.algorithm) {
+		log_error(r-&gt;conf.errh, __FILE__, __LINE__,
+		  "secdownload.algorithm has to be set");
+		r-&gt;http_status = 500;
+		return HANDLER_FINISHED;
+	}
+
+	mac_len = secdl_algorithm_mac_length(p-&gt;conf.algorithm);
+
+	if (0 != strncmp(r-&gt;uri.path.ptr, p-&gt;conf.uri_prefix-&gt;ptr, buffer_clen(p-&gt;conf.uri_prefix))) return HANDLER_GO_ON;
+
+	mac_str = r-&gt;uri.path.ptr + buffer_clen(p-&gt;conf.uri_prefix);
+
+	if (!is_base64_len(mac_str, mac_len)) return HANDLER_GO_ON;
+
+	protected_path = mac_str + mac_len;
+	if (*protected_path != '/') return HANDLER_GO_ON;
+
+	ts_str = protected_path + 1;
+	uint64_t ts = 0;
+	for (i = 0; i &lt; 16 &amp;&amp; light_isxdigit(ts_str[i]); ++i) {
+		ts = (ts &lt;&lt; 4) | hex2int(ts_str[i]);
+	}
+	rel_uri = ts_str + i;
+	if (i == 0 || *rel_uri != '/') return HANDLER_GO_ON;
+
+	/* timed-out */
+	const uint64_t cur_ts = (uint64_t)log_epoch_secs;
+	if ((cur_ts &gt; ts ? cur_ts - ts : ts - cur_ts) &gt; p-&gt;conf.timeout) {
+		/* "Gone" as the url will never be valid again instead of "408 - Timeout" where the request may be repeated */
+		r-&gt;http_status = 410;
+
+		return HANDLER_FINISHED;
+	}
+
+	buffer * const tb = r-&gt;tmp_buf;
+
+	if (p-&gt;conf.path_segments) {
+		const char *rel_uri_end = rel_uri;
+		unsigned int count = p-&gt;conf.path_segments;
+		do {
+			rel_uri_end = strchr(rel_uri_end+1, '/');
+		} while (rel_uri_end &amp;&amp; --count);
+		if (rel_uri_end) {
+			buffer_copy_string_len(tb, protected_path,
+					       rel_uri_end - protected_path);
+			protected_path = tb-&gt;ptr;
+		}
+	}
+
+	if (p-&gt;conf.hash_querystr &amp;&amp; !buffer_is_blank(&amp;r-&gt;uri.query)) {
+		if (protected_path != tb-&gt;ptr) {
+			buffer_copy_string(tb, protected_path);
+		}
+		buffer_append_str2(tb, CONST_STR_LEN("?"),
+		                       BUF_PTR_LEN(&amp;r-&gt;uri.query));
+		/* assign last in case tb-&gt;ptr is reallocated */
+		protected_path = tb-&gt;ptr;
+	}
+
+	if (!secdl_verify_mac(&amp;p-&gt;conf, protected_path, mac_str, mac_len,
+	                      r-&gt;conf.errh)) {
+		r-&gt;http_status = 403;
+
+		if (r-&gt;conf.log_request_handling) {
+			log_error(r-&gt;conf.errh, __FILE__, __LINE__,
+			  "mac invalid: %s", r-&gt;uri.path.ptr);
+		}
+
+		return HANDLER_FINISHED;
+	}
+
+	/* starting with the last / we should have relative-path to the docroot
+	 */
+
+	buffer_copy_buffer(&amp;r-&gt;physical.doc_root, p-&gt;conf.doc_root);
+	buffer_copy_buffer(&amp;r-&gt;physical.basedir, p-&gt;conf.doc_root);
+	buffer_copy_string(&amp;r-&gt;physical.rel_path, rel_uri);
+	buffer_copy_path_len2(&amp;r-&gt;physical.path,
+	                      BUF_PTR_LEN(&amp;r-&gt;physical.doc_root),
+	                      BUF_PTR_LEN(&amp;r-&gt;physical.rel_path));
+
+	return HANDLER_GO_ON;
+}
+
+
+int mod_secdownload_plugin_init(plugin *p);
+int mod_secdownload_plugin_init(plugin *p) {
+	p-&gt;version     = LIGHTTPD_VERSION_ID;
+	p-&gt;name        = "secdownload";
+
+	p-&gt;init        = mod_secdownload_init;
+	p-&gt;handle_physical  = mod_secdownload_uri_handler;
+	p-&gt;set_defaults  = mod_secdownload_set_defaults;
+
+	return 0;
+}
--- /dev/null
+++ b/src/mod_uploadprogress.c
@@ -0,0 +1,334 @@
+#include "first.h"
+
+#include "algo_splaytree.h"
+#include "log.h"
+#include "buffer.h"
+#include "request.h"
+#include "http_header.h"
+
+#include "plugin.h"
+
+#include &lt;stdlib.h&gt;
+#include &lt;string.h&gt;
+
+/**
+ * this is a uploadprogress for a lighttpd plugin
+ *
+ */
+
+typedef struct {
+    buffer r_id;
+    request_st *r;
+    int ndx;
+} request_map_entry;
+
+typedef struct {
+    const buffer *progress_url;
+} plugin_config;
+
+typedef struct {
+    PLUGIN_DATA;
+    plugin_config defaults;
+    plugin_config conf;
+
+    splay_tree *request_map;
+} plugin_data;
+
+/**
+ *
+ * request maps
+ *
+ */
+
+static request_map_entry *
+request_map_entry_init (request_st * const r, const char *r_id, size_t idlen)
+{
+    request_map_entry * const rme = calloc(1, sizeof(request_map_entry));
+    force_assert(rme);
+    rme-&gt;r = r;
+    rme-&gt;ndx = splaytree_djbhash(r_id, idlen);
+    buffer_copy_string_len(&amp;rme-&gt;r_id, r_id, idlen);
+    return rme;
+}
+
+static void
+request_map_entry_free (request_map_entry *rme)
+{
+    free(rme-&gt;r_id.ptr);
+    free(rme);
+}
+
+static void
+request_map_remove (plugin_data * const p, request_map_entry * const rme)
+{
+    splay_tree ** const sptree = &amp;p-&gt;request_map;
+    *sptree = splaytree_splay(*sptree, rme-&gt;ndx);
+    if (NULL != *sptree &amp;&amp; (*sptree)-&gt;key == rme-&gt;ndx) {
+        request_map_entry_free((*sptree)-&gt;data);
+        *sptree = splaytree_delete(*sptree, (*sptree)-&gt;key);
+    }
+}
+
+static request_map_entry *
+request_map_insert (plugin_data * const p, request_map_entry * const rme)
+{
+    splay_tree ** const sptree = &amp;p-&gt;request_map;
+    *sptree = splaytree_splay(*sptree, rme-&gt;ndx);
+    if (NULL == *sptree || (*sptree)-&gt;key != rme-&gt;ndx) {
+        *sptree = splaytree_insert(*sptree, rme-&gt;ndx, rme);
+        return rme;
+    }
+    else { /* collision (not expected); leave old entry and forget new */
+        /*(old entry is referenced elsewhere, so new entry is freed here)*/
+        request_map_entry_free(rme);
+        return NULL;
+    }
+}
+
+__attribute_pure__
+static request_st *
+request_map_get_request (plugin_data * const p, const char * const r_id,  const size_t idlen)
+{
+    splay_tree ** const sptree = &amp;p-&gt;request_map;
+    int ndx = splaytree_djbhash(r_id, idlen);
+    *sptree = splaytree_splay(*sptree, ndx);
+    if (NULL != *sptree &amp;&amp; (*sptree)-&gt;key == ndx) {
+        request_map_entry * const rme = (*sptree)-&gt;data;
+        if (buffer_eq_slen(&amp;rme-&gt;r_id, r_id, idlen))
+            return rme-&gt;r;
+    }
+    return NULL;
+}
+
+static void
+request_map_free (plugin_data * const p)
+{
+    splay_tree *sptree = p-&gt;request_map;
+    p-&gt;request_map = NULL;
+    while (sptree) {
+        request_map_entry_free(sptree-&gt;data);
+        sptree = splaytree_delete(sptree, sptree-&gt;key);
+    }
+}
+
+INIT_FUNC(mod_uploadprogress_init) {
+    return calloc(1, sizeof(plugin_data));
+}
+
+FREE_FUNC(mod_uploadprogress_free) {
+    request_map_free((plugin_data *)p_d);
+}
+
+static void mod_uploadprogress_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
+    switch (cpv-&gt;k_id) { /* index into static config_plugin_keys_t cpk[] */
+      case 0: /* upload-progress.progress-url */
+        pconf-&gt;progress_url = cpv-&gt;v.b;
+        break;
+      default:/* should not happen */
+        return;
+    }
+}
+
+static void mod_uploadprogress_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
+    do {
+        mod_uploadprogress_merge_config_cpv(pconf, cpv);
+    } while ((++cpv)-&gt;k_id != -1);
+}
+
+static void mod_uploadprogress_patch_config(request_st * const r, plugin_data * const p) {
+    p-&gt;conf = p-&gt;defaults; /* copy small struct instead of memcpy() */
+    /*memcpy(&amp;p-&gt;conf, &amp;p-&gt;defaults, sizeof(plugin_config));*/
+    for (int i = 1, used = p-&gt;nconfig; i &lt; used; ++i) {
+        if (config_check_cond(r, (uint32_t)p-&gt;cvlist[i].k_id))
+            mod_uploadprogress_merge_config(&amp;p-&gt;conf,
+                                            p-&gt;cvlist + p-&gt;cvlist[i].v.u2[0]);
+    }
+}
+
+SETDEFAULTS_FUNC(mod_uploadprogress_set_defaults) {
+    static const config_plugin_keys_t cpk[] = {
+      { CONST_STR_LEN("upload-progress.progress-url"),
+        T_CONFIG_STRING,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ NULL, 0,
+        T_CONFIG_UNSET,
+        T_CONFIG_SCOPE_UNSET }
+    };
+
+    plugin_data * const p = p_d;
+    if (!config_plugin_values_init(srv, p, cpk, "mod_uploadprogress"))
+        return HANDLER_ERROR;
+
+    /* process and validate config directives
+     * (init i to 0 if global context; to 1 to skip empty global context) */
+    for (int i = !p-&gt;cvlist[0].v.u2[1]; i &lt; p-&gt;nconfig; ++i) {
+        config_plugin_value_t *cpv = p-&gt;cvlist + p-&gt;cvlist[i].v.u2[0];
+        for (; -1 != cpv-&gt;k_id; ++cpv) {
+            switch (cpv-&gt;k_id) {
+              case 0: /* upload-progress.progress-url */
+                if (buffer_is_blank(cpv-&gt;v.b))
+                    cpv-&gt;v.b = NULL;
+                break;
+              default:/* should not happen */
+                break;
+            }
+        }
+    }
+
+    /* initialize p-&gt;defaults from global config context */
+    if (p-&gt;nconfig &gt; 0 &amp;&amp; p-&gt;cvlist-&gt;v.u2[1]) {
+        const config_plugin_value_t *cpv = p-&gt;cvlist + p-&gt;cvlist-&gt;v.u2[0];
+        if (-1 != cpv-&gt;k_id)
+            mod_uploadprogress_merge_config(&amp;p-&gt;defaults, cpv);
+    }
+
+    return HANDLER_GO_ON;
+}
+
+#define REQID_LEN 32
+
+static const char * mod_uploadprogress_get_reqid (request_st * const r) {
+    const char *idstr;
+    uint32_t len;
+    int pathinfo = 0;
+    const buffer *h = http_header_request_get(r, HTTP_HEADER_OTHER,
+                                              CONST_STR_LEN("X-Progress-ID"));
+    if (NULL != h)
+        idstr = h-&gt;ptr;
+    else if (!buffer_is_blank(&amp;r-&gt;uri.query)
+             &amp;&amp; (idstr = strstr(r-&gt;uri.query.ptr, "X-Progress-ID=")))
+        idstr += sizeof("X-Progress-ID=")-1;
+    else { /*(path-info is not known at this point in request)*/
+        idstr = r-&gt;uri.path.ptr;
+        len = buffer_clen(&amp;r-&gt;uri.path);
+        if (len &gt; REQID_LEN &amp;&amp; idstr[len-REQID_LEN-1] == '/') {
+            pathinfo = 1;
+            idstr += len - REQID_LEN;
+        }
+        else
+            return NULL;
+    }
+
+    /* request must contain ID of REQID_LEN bytes */
+    for (len = 0; light_isxdigit(idstr[len]); ++len) ;
+    if (len != REQID_LEN) {
+        if (!pathinfo) { /*(reduce false positive noise in error log)*/
+            log_error(r-&gt;conf.errh, __FILE__, __LINE__,
+              "invalid progress-id; non-xdigit or len != %d: %s",
+              REQID_LEN, idstr);
+        }
+        return NULL;
+    }
+
+    return idstr;
+}
+
+/**
+ *
+ * the idea:
+ *
+ * for the first request we check if it is a post-request
+ *
+ * if no, move out, don't care about them
+ *
+ * if yes, take the connection structure and register it locally
+ * in the progress-struct together with an session-id (md5 ... )
+ *
+ * if the connections closes, cleanup the entry in the progress-struct
+ *
+ * a second request can now get the info about the size of the upload,
+ * the received bytes
+ *
+ */
+
+URIHANDLER_FUNC(mod_uploadprogress_uri_handler) {
+	plugin_data *p = p_d;
+
+	switch(r-&gt;http_method) {
+	case HTTP_METHOD_GET:
+	case HTTP_METHOD_POST: break;
+	default:               return HANDLER_GO_ON;
+	}
+
+	mod_uploadprogress_patch_config(r, p);
+	if (!p-&gt;conf.progress_url) return HANDLER_GO_ON;
+
+	if (r-&gt;http_method == HTTP_METHOD_GET
+	    &amp;&amp; !buffer_is_equal(&amp;r-&gt;uri.path, p-&gt;conf.progress_url))
+		return HANDLER_GO_ON;
+
+	const char * const idstr = mod_uploadprogress_get_reqid(r);
+	if (NULL == idstr) return HANDLER_GO_ON;
+
+	if (r-&gt;http_method == HTTP_METHOD_POST) {
+		r-&gt;plugin_ctx[p-&gt;id] =
+		  request_map_insert(p, request_map_entry_init(r, idstr, REQID_LEN));
+		return HANDLER_GO_ON;
+	} /* else r-&gt;http_method == HTTP_METHOD_GET */
+
+
+		r-&gt;resp_body_started = 1;
+		r-&gt;resp_body_finished = 1;
+
+		r-&gt;http_status = 200;
+		r-&gt;handler_module = NULL;
+
+		/* get the connection */
+		request_st * const post_r = request_map_get_request(p,idstr,REQID_LEN);
+		if (NULL == post_r) {
+			log_error(r-&gt;conf.errh, __FILE__, __LINE__, "ID not known: %.*s", REQID_LEN, idstr);
+			/* XXX: why is this not an XML response, too?
+			 * (At least Content-Type is not set to text/xml) */
+			chunkqueue_append_mem(&amp;r-&gt;write_queue, CONST_STR_LEN("not in progress"));
+			return HANDLER_FINISHED;
+		}
+
+		http_header_response_set(r, HTTP_HEADER_CONTENT_TYPE, CONST_STR_LEN("Content-Type"), CONST_STR_LEN("text/xml"));
+
+		/* just an attempt the force the IE/proxies to NOT cache the request ... doesn't help :( */
+		http_header_response_set(r, HTTP_HEADER_PRAGMA, CONST_STR_LEN("Pragma"), CONST_STR_LEN("no-cache"));
+		http_header_response_set(r, HTTP_HEADER_EXPIRES, CONST_STR_LEN("Expires"), CONST_STR_LEN("Thu, 19 Nov 1981 08:52:00 GMT"));
+		http_header_response_set(r, HTTP_HEADER_CACHE_CONTROL, CONST_STR_LEN("Cache-Control"), CONST_STR_LEN("no-store, no-cache, must-revalidate, post-check=0, pre-check=0"));
+
+		/* prepare XML */
+		buffer * const b = chunkqueue_append_buffer_open(&amp;r-&gt;write_queue);
+		buffer_copy_string_len(b, CONST_STR_LEN(
+			"&lt;?xml version=\"1.0\" encoding=\"iso-8859-1\"?&gt;"
+			"&lt;upload&gt;"
+			"&lt;size&gt;"));
+		buffer_append_int(b, post_r-&gt;reqbody_length);
+		buffer_append_string_len(b, CONST_STR_LEN(
+			"&lt;/size&gt;"
+			"&lt;received&gt;"));
+		buffer_append_int(b, post_r-&gt;reqbody_queue.bytes_in);
+		buffer_append_string_len(b, CONST_STR_LEN(
+			"&lt;/received&gt;"
+			"&lt;/upload&gt;"));
+		chunkqueue_append_buffer_commit(&amp;r-&gt;write_queue);
+		return HANDLER_FINISHED;
+}
+
+REQUESTDONE_FUNC(mod_uploadprogress_request_done) {
+	plugin_data *p = p_d;
+	request_map_entry * const rme = r-&gt;plugin_ctx[p-&gt;id];
+	if (rme) {
+		r-&gt;plugin_ctx[p-&gt;id] = NULL;
+		request_map_remove(p, rme);
+	}
+	return HANDLER_GO_ON;
+}
+
+
+int mod_uploadprogress_plugin_init(plugin *p);
+int mod_uploadprogress_plugin_init(plugin *p) {
+	p-&gt;version     = LIGHTTPD_VERSION_ID;
+	p-&gt;name        = "uploadprogress";
+
+	p-&gt;init        = mod_uploadprogress_init;
+	p-&gt;handle_uri_clean  = mod_uploadprogress_uri_handler;
+	p-&gt;handle_request_reset = mod_uploadprogress_request_done;
+	p-&gt;set_defaults  = mod_uploadprogress_set_defaults;
+	p-&gt;cleanup     = mod_uploadprogress_free;
+
+	return 0;
+}
--- /dev/null
+++ b/src/mod_usertrack.c
@@ -0,0 +1,242 @@
+#include "first.h"
+
+#include "base.h"
+#include "log.h"
+#include "buffer.h"
+#include "rand.h"
+#include "http_header.h"
+
+#include "plugin.h"
+
+#include "sys-crypto-md.h"
+
+#include &lt;stdlib.h&gt;
+#include &lt;string.h&gt;
+
+typedef struct {
+	const buffer *cookie_name;
+	const buffer *cookie_attrs;
+	const buffer *cookie_domain;
+	unsigned int cookie_max_age;
+} plugin_config;
+
+typedef struct {
+    PLUGIN_DATA;
+    plugin_config defaults;
+    plugin_config conf;
+} plugin_data;
+
+INIT_FUNC(mod_usertrack_init) {
+    return calloc(1, sizeof(plugin_data));
+}
+
+static void mod_usertrack_merge_config_cpv(plugin_config * const pconf, const config_plugin_value_t * const cpv) {
+    switch (cpv-&gt;k_id) { /* index into static config_plugin_keys_t cpk[] */
+      case 0: /* usertrack.cookie-name */
+        pconf-&gt;cookie_name = cpv-&gt;v.b;
+        break;
+      case 1: /* usertrack.cookie-max-age */
+        pconf-&gt;cookie_max_age = cpv-&gt;v.u;
+        break;
+      case 2: /* usertrack.cookie-domain */
+        pconf-&gt;cookie_domain = cpv-&gt;v.b;
+        break;
+      case 3: /* usertrack.cookie-attrs */
+        pconf-&gt;cookie_attrs = cpv-&gt;v.b;
+        break;
+      default:/* should not happen */
+        return;
+    }
+}
+
+static void mod_usertrack_merge_config(plugin_config * const pconf, const config_plugin_value_t *cpv) {
+    do {
+        mod_usertrack_merge_config_cpv(pconf, cpv);
+    } while ((++cpv)-&gt;k_id != -1);
+}
+
+static void mod_usertrack_patch_config(request_st * const r, plugin_data * const p) {
+    p-&gt;conf = p-&gt;defaults; /* copy small struct instead of memcpy() */
+    /*memcpy(&amp;p-&gt;conf, &amp;p-&gt;defaults, sizeof(plugin_config));*/
+    for (int i = 1, used = p-&gt;nconfig; i &lt; used; ++i) {
+        if (config_check_cond(r, (uint32_t)p-&gt;cvlist[i].k_id))
+            mod_usertrack_merge_config(&amp;p-&gt;conf, p-&gt;cvlist + p-&gt;cvlist[i].v.u2[0]);
+    }
+}
+
+SETDEFAULTS_FUNC(mod_usertrack_set_defaults) {
+    static const config_plugin_keys_t cpk[] = {
+      { CONST_STR_LEN("usertrack.cookie-name"),
+        T_CONFIG_STRING,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ CONST_STR_LEN("usertrack.cookie-max-age"),
+        T_CONFIG_INT,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ CONST_STR_LEN("usertrack.cookie-domain"),
+        T_CONFIG_STRING,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ CONST_STR_LEN("usertrack.cookie-attrs"),
+        T_CONFIG_STRING,
+        T_CONFIG_SCOPE_CONNECTION }
+     ,{ NULL, 0,
+        T_CONFIG_UNSET,
+        T_CONFIG_SCOPE_UNSET }
+    };
+
+    plugin_data * const p = p_d;
+    if (!config_plugin_values_init(srv, p, cpk, "mod_usertrack"))
+        return HANDLER_ERROR;
+
+    /* process and validate config directives
+     * (init i to 0 if global context; to 1 to skip empty global context) */
+    for (int i = !p-&gt;cvlist[0].v.u2[1]; i &lt; p-&gt;nconfig; ++i) {
+        config_plugin_value_t *cpv = p-&gt;cvlist + p-&gt;cvlist[i].v.u2[0];
+        for (; -1 != cpv-&gt;k_id; ++cpv) {
+            switch (cpv-&gt;k_id) {
+              case 0: /* usertrack.cookie-name */
+                if (!buffer_is_blank(cpv-&gt;v.b)) {
+                    const char * const ptr = cpv-&gt;v.b-&gt;ptr;
+                    const size_t len = buffer_clen(cpv-&gt;v.b);
+                    for (size_t j = 0; j &lt; len; ++j) {
+                        if (!light_isalpha(ptr[j])) {
+                            log_error(srv-&gt;errh, __FILE__, __LINE__,
+                              "invalid character in %s: %s",
+                               cpk[cpv-&gt;k_id].k, ptr);
+                            return HANDLER_ERROR;
+                        }
+                    }
+                }
+                else
+                    cpv-&gt;v.b = NULL;
+                break;
+              case 1: /* usertrack.cookie-max-age */
+                break;
+              case 2: /* usertrack.cookie-domain */
+                if (!buffer_is_blank(cpv-&gt;v.b)) {
+                    const char * const ptr = cpv-&gt;v.b-&gt;ptr;
+                    const size_t len = buffer_clen(cpv-&gt;v.b);
+                    for (size_t j = 0; j &lt; len; ++j) {
+                        const char c = ptr[j];
+                        if (c &lt;= 32 || c &gt;= 127 || c == '"' || c == '\\') {
+                            log_error(srv-&gt;errh, __FILE__, __LINE__,
+                              "invalid character in %s: %s",
+                               cpk[cpv-&gt;k_id].k, ptr);
+                            return HANDLER_ERROR;
+                        }
+                    }
+                }
+                else
+                    cpv-&gt;v.b = NULL;
+                break;
+              case 3: /* usertrack.cookie-attrs */
+                if (buffer_is_blank(cpv-&gt;v.b))
+                    cpv-&gt;v.b = NULL;
+                break;
+              default:/* should not happen */
+                break;
+            }
+        }
+    }
+
+    /* initialize p-&gt;defaults from global config context */
+    if (p-&gt;nconfig &gt; 0 &amp;&amp; p-&gt;cvlist-&gt;v.u2[1]) {
+        const config_plugin_value_t *cpv = p-&gt;cvlist + p-&gt;cvlist-&gt;v.u2[0];
+        if (-1 != cpv-&gt;k_id)
+            mod_usertrack_merge_config(&amp;p-&gt;defaults, cpv);
+    }
+    if (NULL == p-&gt;defaults.cookie_name) {
+        static const struct { const char *ptr; uint32_t used; uint32_t size; }
+          default_cookie_name = { "TRACKID", sizeof("TRACKID"), 0 };
+        *((const buffer **)&amp;p-&gt;defaults.cookie_name) =
+          (const buffer *)&amp;default_cookie_name;
+    }
+
+    return HANDLER_GO_ON;
+}
+
+__attribute_noinline__
+static handler_t mod_usertrack_set_cookie(request_st * const r, plugin_data * const p) {
+
+	/* generate shared-secret */
+	/* (reference mod_auth.c) */
+	int rnd = li_rand_pseudo();
+	struct const_iovec iov[] = {
+	  { BUF_PTR_LEN(&amp;r-&gt;uri.path) }
+	 ,{ "+", 1 }
+	 ,{ &amp;log_epoch_secs, sizeof(log_epoch_secs) }
+	 ,{ &amp;rnd, sizeof(rnd) }
+	};
+	unsigned char h[MD5_DIGEST_LENGTH];
+	MD5_iov(h, iov, sizeof(iov)/sizeof(*iov));
+
+	/* set a cookie */
+	buffer * const cookie = r-&gt;tmp_buf;
+	buffer_clear(cookie);
+	buffer_append_str2(cookie, BUF_PTR_LEN(p-&gt;conf.cookie_name),
+                                   CONST_STR_LEN("="));
+	buffer_append_string_encoded_hex_lc(cookie, (char *)h, sizeof(h));
+
+	/* usertrack.cookie-attrs, if set, replaces all other attrs */
+	if (p-&gt;conf.cookie_attrs) {
+		buffer_append_string_buffer(cookie, p-&gt;conf.cookie_attrs);
+		http_header_response_insert(r, HTTP_HEADER_SET_COOKIE, CONST_STR_LEN("Set-Cookie"), BUF_PTR_LEN(cookie));
+		return HANDLER_GO_ON;
+	}
+
+	buffer_append_string_len(cookie, CONST_STR_LEN("; Path=/; Version=1"));
+
+	if (p-&gt;conf.cookie_domain) {
+		buffer_append_string_len(cookie, CONST_STR_LEN("; Domain="));
+		buffer_append_string_encoded(cookie, BUF_PTR_LEN(p-&gt;conf.cookie_domain), ENCODING_REL_URI);
+	}
+
+	if (p-&gt;conf.cookie_max_age) {
+		buffer_append_string_len(cookie, CONST_STR_LEN("; max-age="));
+		buffer_append_int(cookie, p-&gt;conf.cookie_max_age);
+	}
+
+	http_header_response_insert(r, HTTP_HEADER_SET_COOKIE, CONST_STR_LEN("Set-Cookie"), BUF_PTR_LEN(cookie));
+
+	return HANDLER_GO_ON;
+}
+
+URIHANDLER_FUNC(mod_usertrack_uri_handler) {
+    plugin_data * const p = p_d;
+
+    mod_usertrack_patch_config(r, p);
+    if (!p-&gt;conf.cookie_name) return HANDLER_GO_ON;
+
+    const buffer * const b =
+      http_header_request_get(r, HTTP_HEADER_COOKIE, CONST_STR_LEN("Cookie"));
+    if (NULL != b) {
+        /* parse the cookie (fuzzy; not precise using strstr() below)
+         * check for cookiename + (WS | '=')
+         */
+        const char * const g = strstr(b-&gt;ptr, p-&gt;conf.cookie_name-&gt;ptr);
+        if (NULL != g) {
+            const char *nc = g+buffer_clen(p-&gt;conf.cookie_name);
+            while (*nc == ' ' || *nc == '\t') ++nc; /* skip WS */
+            if (*nc == '=') { /* ok, found the key of our own cookie */
+                if (strlen(nc) &gt; 32) {
+                    /* i'm lazy */
+                    return HANDLER_GO_ON;
+                }
+            }
+        }
+    }
+
+    return mod_usertrack_set_cookie(r, p);
+}
+
+
+int mod_usertrack_plugin_init(plugin *p);
+int mod_usertrack_plugin_init(plugin *p) {
+	p-&gt;version     = LIGHTTPD_VERSION_ID;
+	p-&gt;name        = "usertrack";
+
+	p-&gt;init        = mod_usertrack_init;
+	p-&gt;handle_uri_clean  = mod_usertrack_uri_handler;
+	p-&gt;set_defaults  = mod_usertrack_set_defaults;
+
+	return 0;
+}
--- a/src/t/test_mod.c
+++ b/src/t/test_mod.c
@@ -5,6 +5,7 @@
 
 void test_mod_access (void);
 void test_mod_alias (void);
+void test_mod_evasive (void);
 void test_mod_evhost (void);
 void test_mod_indexfile (void);
 void test_mod_simple_vhost (void);
@@ -15,6 +16,7 @@ void test_mod_userdir (void);
 int main(void) {
     test_mod_access();
     test_mod_alias();
+    test_mod_evasive();
     test_mod_evhost();
     test_mod_indexfile();
     test_mod_simple_vhost();
--- /dev/null
+++ b/src/t/test_mod_evasive.c
@@ -0,0 +1,72 @@
+#include "first.h"
+
+#undef NDEBUG
+#include &lt;assert.h&gt;
+#include &lt;stdlib.h&gt;
+
+#include "mod_evasive.c"
+
+static void test_mod_evasive_check(void) {
+    connection c[4];
+    memset(&amp;c, 0, sizeof(c));
+    c[0].next = &amp;c[1];
+    c[1].prev = &amp;c[0];
+    c[1].next = &amp;c[2];
+    c[2].prev = &amp;c[1];
+    c[2].next = &amp;c[3];
+    c[3].prev = &amp;c[2];
+    sock_addr_inet_pton(&amp;c[0].dst_addr, "10.0.0.1", AF_INET, 80);
+    buffer_copy_string_len(&amp;c[0].dst_addr_buf, CONST_STR_LEN("10.0.0.1"));
+    sock_addr_inet_pton(&amp;c[1].dst_addr, "10.0.0.2", AF_INET, 80);
+    buffer_copy_string_len(&amp;c[1].dst_addr_buf, CONST_STR_LEN("10.0.0.2"));
+    sock_addr_inet_pton(&amp;c[2].dst_addr, "10.0.0.3", AF_INET, 80);
+    buffer_copy_string_len(&amp;c[2].dst_addr_buf, CONST_STR_LEN("10.0.0.3"));
+    sock_addr_inet_pton(&amp;c[3].dst_addr, "10.0.0.4", AF_INET, 80);
+    buffer_copy_string_len(&amp;c[3].dst_addr_buf, CONST_STR_LEN("10.0.0.4"));
+
+    c[0].request.state = CON_STATE_HANDLE_REQUEST;
+    c[1].request.state = CON_STATE_HANDLE_REQUEST;
+    c[2].request.state = CON_STATE_HANDLE_REQUEST;
+    c[3].request.state = CON_STATE_HANDLE_REQUEST;
+
+    request_st *r = &amp;c[0].request;
+    r-&gt;con = &amp;c[0];
+    r-&gt;tmp_buf                = buffer_init();
+    r-&gt;conf.errh              = fdlog_init(NULL, -1, FDLOG_FD);
+    r-&gt;conf.errh-&gt;fd          = -1; /* (disable) */
+
+    plugin_data p;
+    memset(&amp;p, 0, sizeof(plugin_data));
+    p.conf.silent = 1;
+
+    p.conf.max_conns = 1;
+    assert(HANDLER_GO_ON == mod_evasive_check_per_ip_limit(r, &amp;p, c));
+
+    p.conf.max_conns = 2;
+    assert(HANDLER_GO_ON == mod_evasive_check_per_ip_limit(r, &amp;p, c));
+
+    sock_addr_inet_pton(&amp;c[1].dst_addr, "10.0.0.1", AF_INET, 80);
+    buffer_copy_string_len(&amp;c[1].dst_addr_buf, CONST_STR_LEN("10.0.0.1"));
+    assert(HANDLER_GO_ON == mod_evasive_check_per_ip_limit(r, &amp;p, c));
+
+    c[2].request.state = CON_STATE_READ;
+    sock_addr_inet_pton(&amp;c[1].dst_addr, "10.0.0.1", AF_INET, 80);
+    buffer_copy_string_len(&amp;c[1].dst_addr_buf, CONST_STR_LEN("10.0.0.1"));
+    assert(HANDLER_GO_ON == mod_evasive_check_per_ip_limit(r, &amp;p, c));
+
+    c[2].request.state = CON_STATE_HANDLE_REQUEST;
+    sock_addr_inet_pton(&amp;c[2].dst_addr, "10.0.0.1", AF_INET, 80);
+    buffer_copy_string_len(&amp;c[2].dst_addr_buf, CONST_STR_LEN("10.0.0.1"));
+    assert(HANDLER_FINISHED == mod_evasive_check_per_ip_limit(r, &amp;p, c));
+
+    for (uint32_t i = 0; i &lt; sizeof(c)/sizeof(*c); ++i)
+        buffer_free_ptr(&amp;c[i].dst_addr_buf);
+    fdlog_free(r-&gt;conf.errh);
+    buffer_free(r-&gt;tmp_buf);
+}
+
+void test_mod_evasive (void);
+void test_mod_evasive (void)
+{
+    test_mod_evasive_check();
+}
--- a/tests/lighttpd.conf
+++ b/tests/lighttpd.conf
@@ -30,6 +30,7 @@ server.modules += (
 	"mod_simple_vhost",
 	"mod_cgi",
 	"mod_status",
+	"mod_secdownload",
 	"mod_deflate",
 	"mod_accesslog",
 )
@@ -252,3 +253,29 @@ $HTTP["host"] =~ "^auth-" {
 	status.status-url = "/server-status"
 	status.config-url = "/server-config"
 }
+
+$HTTP["host"] == "vvv.example.org" {
+	server.document-root = env.SRCDIR + "/tmp/lighttpd/servers/www.example.org/pages/"
+	secdownload.secret          = "verysecret"
+	secdownload.document-root   = env.SRCDIR + "/tmp/lighttpd/servers/www.example.org/pages/"
+	secdownload.uri-prefix      = "/sec/"
+	secdownload.timeout         = 120
+	secdownload.algorithm       = "md5"
+}
+$HTTP["host"] == "vvv-sha1.example.org" {
+	server.document-root = env.SRCDIR + "/tmp/lighttpd/servers/www.example.org/pages/"
+	secdownload.secret          = "verysecret"
+	secdownload.document-root   = env.SRCDIR + "/tmp/lighttpd/servers/www.example.org/pages/"
+	secdownload.uri-prefix      = "/sec/"
+	secdownload.timeout         = 120
+	secdownload.algorithm       = "hmac-sha1"
+}
+$HTTP["host"] == "vvv-sha256.example.org" {
+	server.document-root = env.SRCDIR + "/tmp/lighttpd/servers/www.example.org/pages/"
+	secdownload.secret          = "verysecret"
+	secdownload.document-root   = env.SRCDIR + "/tmp/lighttpd/servers/www.example.org/pages/"
+	secdownload.uri-prefix      = "/sec/"
+	secdownload.timeout         = 120
+	secdownload.algorithm       = "hmac-sha256"
+	secdownload.hash-querystr   = "enable"
+}
--- a/tests/request.t
+++ b/tests/request.t
@@ -8,7 +8,7 @@ BEGIN {
 
 use strict;
 use IO::Socket;
-use Test::More tests =&gt; 164;
+use Test::More tests =&gt; 178;
 use LightyTest;
 
 my $tf = LightyTest-&gt;new();
@@ -1593,6 +1593,196 @@ ok($tf_proxy-&gt;stop_proc == 0, "Stopping
 } while (0);
 
 
+## mod_secdownload
+
+use Digest::MD5 qw(md5_hex);
+use Digest::SHA qw(hmac_sha1 hmac_sha256);
+use MIME::Base64 qw(encode_base64url);
+
+my $secret = "verysecret";
+my ($f, $thex, $m);
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /index.html HTTP/1.0
+Host: www.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 200 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'skipping secdownload - direct access');
+
+## MD5
+$f = "/index.html";
+$thex = sprintf("%08x", time);
+$m = md5_hex($secret.$f.$thex);
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec/$m/$thex$f HTTP/1.0
+Host: vvv.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 200 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload (md5)');
+
+$thex = sprintf("%08x", time - 1800);
+$m = md5_hex($secret.$f.$thex);
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec/$m/$thex$f HTTP/1.0
+Host: vvv.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 410 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload - gone (timeout) (md5)');
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec$f HTTP/1.0
+Host: vvv.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 404 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload - direct access (md5)');
+
+$f = "/noexists";
+$thex = sprintf("%08x", time);
+$m = md5_hex($secret.$f.$thex);
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec/$m/$thex$f HTTP/1.0
+Host: vvv.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 404 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload - timeout (md5)');
+
+
+if (!$tf-&gt;has_crypto()) {
+
+    for (1..4) { ok(1, "secdownload (hmac-sha1) (skipped) - (missing SSL support)"); }
+    for (1..5) { ok(1, "secdownload (hmac-sha256) (skipped) - (missing SSL support)"); }
+
+}
+else {
+
+## HMAC-SHA1
+$f = "/index.html";
+$thex = sprintf("%08x", time);
+$m = encode_base64url(hmac_sha1("/$thex$f", $secret));
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec/$m/$thex$f HTTP/1.0
+Host: vvv-sha1.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 200 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload (hmac-sha1)');
+
+$thex = sprintf("%08x", time - 1800);
+$m = encode_base64url(hmac_sha1("/$thex$f", $secret));
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec/$m/$thex$f HTTP/1.0
+Host: vvv-sha1.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 410 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload - gone (timeout) (hmac-sha1)');
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec$f HTTP/1.0
+Host: vvv-sha1.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 404 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload - direct access (hmac-sha1)');
+
+
+$f = "/noexists";
+$thex = sprintf("%08x", time);
+$m = encode_base64url(hmac_sha1("/$thex$f", $secret));
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec/$m/$thex$f HTTP/1.0
+Host: vvv-sha1.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 404 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload - timeout (hmac-sha1)');
+
+## HMAC-SHA256
+$f = "/index.html";
+$thex = sprintf("%08x", time);
+$m = encode_base64url(hmac_sha256("/$thex$f", $secret));
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec/$m/$thex$f HTTP/1.0
+Host: vvv-sha256.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 200 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload (hmac-sha256)');
+
+## HMAC-SHA256
+$f = "/index.html?qs=1";
+$thex = sprintf("%08x", time);
+$m = encode_base64url(hmac_sha256("/$thex$f", $secret));
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec/$m/$thex$f HTTP/1.0
+Host: vvv-sha256.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 200 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload (hmac-sha256) with hash-querystr');
+
+$thex = sprintf("%08x", time - 1800);
+$m = encode_base64url(hmac_sha256("/$thex$f", $secret));
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec/$m/$thex$f HTTP/1.0
+Host: vvv-sha256.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 410 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload - gone (timeout) (hmac-sha256)');
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec$f HTTP/1.0
+Host: vvv-sha256.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 404 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload - direct access (hmac-sha256)');
+
+
+$f = "/noexists";
+$thex = sprintf("%08x", time);
+$m = encode_base64url(hmac_sha256("/$thex$f", $secret));
+
+$t-&gt;{REQUEST}  = ( &lt;&lt;EOF
+GET /sec/$m/$thex$f HTTP/1.0
+Host: vvv-sha256.example.org
+EOF
+ );
+$t-&gt;{RESPONSE} = [ { 'HTTP-Protocol' =&gt; 'HTTP/1.0', 'HTTP-Status' =&gt; 404 } ];
+
+ok($tf-&gt;handle_http($t) == 0, 'secdownload - timeout (hmac-sha256)');
+
+} # SKIP if lighttpd built without crypto algorithms (e.g. without openssl)
+
+
 ## mod_setenv
 
 $t-&gt;{REQUEST} = ( &lt;&lt;EOF
</pre></body></html>