[mod_accesslog] %{ratio}n logs compression ratio (fixes #2133)
[lighttpd.git] / src / mod_secdownload.c
blobff752546a005aaf00f53e8806992fdfbdaa303d6
1 #include "first.h"
3 #include "base.h"
4 #include "log.h"
5 #include "buffer.h"
6 #include "base64.h"
8 #include "plugin.h"
10 #include <ctype.h>
11 #include <stdlib.h>
12 #include <string.h>
14 #if defined(USE_OPENSSL)
15 #include <openssl/evp.h>
16 #include <openssl/hmac.h>
17 #endif
19 #include "md5.h"
21 #define HASHLEN 16
22 typedef unsigned char HASH[HASHLEN];
23 #define HASHHEXLEN 32
24 typedef char HASHHEX[HASHHEXLEN+1];
27 * mod_secdownload verifies a checksum associated with a timestamp
28 * and a path.
30 * It takes an URL of the form:
31 * securl := <uri-prefix> <mac> <protected-path>
32 * uri-prefix := '/' any* # whatever was configured: must start with a '/')
33 * mac := [a-zA-Z0-9_-]{mac_len} # mac length depends on selected algorithm
34 * protected-path := '/' <timestamp> <rel-path>
35 * timestamp := [a-f0-9]{8} # timestamp when the checksum was calculated
36 * # to prevent access after timeout (active requests
37 * # will finish successfully even after the timeout)
38 * rel-path := '/' any* # the protected path; changing the path breaks the
39 * # checksum
41 * The timestamp is the `epoch` timestamp in hex, i.e. time in seconds
42 * since 00:00:00 UTC on 1 January 1970.
44 * mod_secdownload supports various MAC algorithms:
46 * # md5
47 * mac_len := 32 (and hex only)
48 * mac := md5-hex(<secrect><rel-path><timestamp>) # lowercase hex
49 * perl example:
50 use Digest::MD5 qw(md5_hex);
51 my $secret = "verysecret";
52 my $rel_path = "/index.html"
53 my $xtime = sprintf("%08x", time);
54 my $url = '/'. md5_hex($secret . $rel_path . $xtime) . '/' . $xtime . $rel_path;
56 * # hmac-sha1
57 * mac_len := 27 (no base64 padding)
58 * mac := base64-url(hmac-sha1(<secret>, <protected-path>))
59 * perl example:
60 use Digest::SHA qw(hmac_sha1);
61 use MIME::Base64 qw(encode_base64url);
62 my $secret = "verysecret";
63 my $rel_path = "/index.html"
64 my $protected_path = '/' . sprintf("%08x", time) . $rel_path;
65 my $url = '/'. encode_base64url(hmac_sha1($protected_path, $secret)) . $protected_path;
67 * # hmac-256
68 * mac_len := 43 (no base64 padding)
69 * mac := base64-url(hmac-256(<secret>, <protected-path>))
70 use Digest::SHA qw(hmac_sha256);
71 use MIME::Base64 qw(encode_base64url);
72 my $secret = "verysecret";
73 my $rel_path = "/index.html"
74 my $protected_path = '/' . sprintf("%08x", time) . $rel_path;
75 my $url = '/'. encode_base64url(hmac_sha256($protected_path, $secret)) . $protected_path;
79 /* plugin config for all request/connections */
81 typedef enum {
82 SECDL_INVALID = 0,
83 SECDL_MD5 = 1,
84 SECDL_HMAC_SHA1 = 2,
85 SECDL_HMAC_SHA256 = 3,
86 } secdl_algorithm;
88 typedef struct {
89 buffer *doc_root;
90 buffer *secret;
91 buffer *uri_prefix;
92 secdl_algorithm algorithm;
94 unsigned int timeout;
95 } plugin_config;
97 typedef struct {
98 PLUGIN_DATA;
100 plugin_config **config_storage;
102 plugin_config conf;
103 } plugin_data;
105 static int const_time_memeq(const char *a, const char *b, size_t len) {
106 /* constant time memory compare, unless the compiler figures it out */
107 char diff = 0;
108 size_t i;
109 for (i = 0; i < len; ++i) {
110 diff |= (a[i] ^ b[i]);
112 return 0 == diff;
115 static const char* secdl_algorithm_names[] = {
116 "invalid",
117 "md5",
118 "hmac-sha1",
119 "hmac-sha256",
122 static secdl_algorithm algorithm_from_string(buffer *name) {
123 size_t ndx;
125 if (buffer_string_is_empty(name)) return SECDL_INVALID;
127 for (ndx = 1; ndx < sizeof(secdl_algorithm_names)/sizeof(secdl_algorithm_names[0]); ++ndx) {
128 if (0 == strcmp(secdl_algorithm_names[ndx], name->ptr)) return (secdl_algorithm)ndx;
131 return SECDL_INVALID;
134 static size_t secdl_algorithm_mac_length(secdl_algorithm alg) {
135 switch (alg) {
136 case SECDL_INVALID:
137 break;
138 case SECDL_MD5:
139 return 32;
140 case SECDL_HMAC_SHA1:
141 return 27;
142 case SECDL_HMAC_SHA256:
143 return 43;
145 return 0;
148 static int secdl_verify_mac(server *srv, plugin_config *config, const char* protected_path, const char* mac, size_t maclen) {
149 UNUSED(srv);
150 if (0 == maclen || secdl_algorithm_mac_length(config->algorithm) != maclen) return 0;
152 switch (config->algorithm) {
153 case SECDL_INVALID:
154 break;
155 case SECDL_MD5:
157 li_MD5_CTX Md5Ctx;
158 HASH HA1;
159 char hexmd5[33];
160 const char *ts_str;
161 const char *rel_uri;
163 /* legacy message:
164 * protected_path := '/' <timestamp-hex> <rel-path>
165 * timestamp-hex := [0-9a-f]{8}
166 * rel-path := '/' any*
167 * (the protected path was already verified)
168 * message = <secret><rel-path><timestamp-hex>
170 ts_str = protected_path + 1;
171 rel_uri = ts_str + 8;
173 li_MD5_Init(&Md5Ctx);
174 li_MD5_Update(&Md5Ctx, CONST_BUF_LEN(config->secret));
175 li_MD5_Update(&Md5Ctx, rel_uri, strlen(rel_uri));
176 li_MD5_Update(&Md5Ctx, ts_str, 8);
177 li_MD5_Final(HA1, &Md5Ctx);
179 li_tohex(hexmd5, sizeof(hexmd5), (const char *)HA1, 16);
181 return (32 == maclen) && const_time_memeq(mac, hexmd5, 32);
183 case SECDL_HMAC_SHA1:
184 #if defined(USE_OPENSSL)
186 unsigned char digest[20];
187 char base64_digest[27];
189 if (NULL == HMAC(
190 EVP_sha1(),
191 (unsigned char const*) CONST_BUF_LEN(config->secret),
192 (unsigned char const*) protected_path, strlen(protected_path),
193 digest, NULL)) {
194 log_error_write(srv, __FILE__, __LINE__, "s",
195 "hmac-sha1: HMAC() failed");
196 return 0;
199 li_to_base64_no_padding(base64_digest, 27, digest, 20, BASE64_URL);
201 return (27 == maclen) && const_time_memeq(mac, base64_digest, 27);
203 #endif
204 break;
205 case SECDL_HMAC_SHA256:
206 #if defined(USE_OPENSSL)
208 unsigned char digest[32];
209 char base64_digest[43];
211 if (NULL == HMAC(
212 EVP_sha256(),
213 (unsigned char const*) CONST_BUF_LEN(config->secret),
214 (unsigned char const*) protected_path, strlen(protected_path),
215 digest, NULL)) {
216 log_error_write(srv, __FILE__, __LINE__, "s",
217 "hmac-sha256: HMAC() failed");
218 return 0;
221 li_to_base64_no_padding(base64_digest, 43, digest, 32, BASE64_URL);
223 return (43 == maclen) && const_time_memeq(mac, base64_digest, 43);
225 #endif
226 break;
229 return 0;
232 /* init the plugin data */
233 INIT_FUNC(mod_secdownload_init) {
234 plugin_data *p;
236 p = calloc(1, sizeof(*p));
238 return p;
241 /* detroy the plugin data */
242 FREE_FUNC(mod_secdownload_free) {
243 plugin_data *p = p_d;
244 UNUSED(srv);
246 if (!p) return HANDLER_GO_ON;
248 if (p->config_storage) {
249 size_t i;
250 for (i = 0; i < srv->config_context->used; i++) {
251 plugin_config *s = p->config_storage[i];
253 if (NULL == s) continue;
255 buffer_free(s->secret);
256 buffer_free(s->doc_root);
257 buffer_free(s->uri_prefix);
259 free(s);
261 free(p->config_storage);
264 free(p);
266 return HANDLER_GO_ON;
269 /* handle plugin config and check values */
271 SETDEFAULTS_FUNC(mod_secdownload_set_defaults) {
272 plugin_data *p = p_d;
273 size_t i = 0;
275 config_values_t cv[] = {
276 { "secdownload.secret", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 0 */
277 { "secdownload.document-root", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 1 */
278 { "secdownload.uri-prefix", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 2 */
279 { "secdownload.timeout", NULL, T_CONFIG_INT, T_CONFIG_SCOPE_CONNECTION }, /* 3 */
280 { "secdownload.algorithm", NULL, T_CONFIG_STRING, T_CONFIG_SCOPE_CONNECTION }, /* 4 */
281 { NULL, NULL, T_CONFIG_UNSET, T_CONFIG_SCOPE_UNSET }
284 if (!p) return HANDLER_ERROR;
286 p->config_storage = calloc(1, srv->config_context->used * sizeof(plugin_config *));
288 for (i = 0; i < srv->config_context->used; i++) {
289 data_config const* config = (data_config const*)srv->config_context->data[i];
290 plugin_config *s;
291 buffer *algorithm = buffer_init();
293 s = calloc(1, sizeof(plugin_config));
294 s->secret = buffer_init();
295 s->doc_root = buffer_init();
296 s->uri_prefix = buffer_init();
297 s->timeout = 60;
299 cv[0].destination = s->secret;
300 cv[1].destination = s->doc_root;
301 cv[2].destination = s->uri_prefix;
302 cv[3].destination = &(s->timeout);
303 cv[4].destination = algorithm;
305 p->config_storage[i] = s;
307 if (0 != config_insert_values_global(srv, config->value, cv, i == 0 ? T_CONFIG_SCOPE_SERVER : T_CONFIG_SCOPE_CONNECTION)) {
308 buffer_free(algorithm);
309 return HANDLER_ERROR;
312 if (!buffer_is_empty(algorithm)) {
313 s->algorithm = algorithm_from_string(algorithm);
314 switch (s->algorithm) {
315 case SECDL_INVALID:
316 log_error_write(srv, __FILE__, __LINE__, "sb",
317 "invalid secdownload.algorithm:",
318 algorithm);
319 buffer_free(algorithm);
320 return HANDLER_ERROR;
321 #if !defined(USE_OPENSSL)
322 case SECDL_HMAC_SHA1:
323 case SECDL_HMAC_SHA256:
324 log_error_write(srv, __FILE__, __LINE__, "sb",
325 "unsupported secdownload.algorithm:",
326 algorithm);
327 buffer_free(algorithm);
328 return HANDLER_ERROR;
329 #endif
330 default:
331 break;
335 buffer_free(algorithm);
338 return HANDLER_GO_ON;
342 * checks if the supplied string is a hex string
344 * @param str a possible hex string
345 * @return if the supplied string is a valid hex string 1 is returned otherwise 0
348 static int is_hex_len(const char *str, size_t len) {
349 size_t i;
351 if (NULL == str) return 0;
353 for (i = 0; i < len && *str; i++, str++) {
354 /* illegal characters */
355 if (!((*str >= '0' && *str <= '9') ||
356 (*str >= 'a' && *str <= 'f') ||
357 (*str >= 'A' && *str <= 'F'))
359 return 0;
363 return i == len;
367 * checks if the supplied string is a base64 (modified URL) string
369 * @param str a possible base64 (modified URL) string
370 * @return if the supplied string is a valid base64 (modified URL) string 1 is returned otherwise 0
373 static int is_base64_len(const char *str, size_t len) {
374 size_t i;
376 if (NULL == str) return 0;
378 for (i = 0; i < len && *str; i++, str++) {
379 /* illegal characters */
380 if (!((*str >= '0' && *str <= '9') ||
381 (*str >= 'a' && *str <= 'z') ||
382 (*str >= 'A' && *str <= 'Z') ||
383 (*str == '-') || (*str == '_'))
385 return 0;
389 return i == len;
392 #define PATCH(x) \
393 p->conf.x = s->x;
394 static int mod_secdownload_patch_connection(server *srv, connection *con, plugin_data *p) {
395 size_t i, j;
396 plugin_config *s = p->config_storage[0];
398 PATCH(secret);
399 PATCH(doc_root);
400 PATCH(uri_prefix);
401 PATCH(timeout);
402 PATCH(algorithm);
404 /* skip the first, the global context */
405 for (i = 1; i < srv->config_context->used; i++) {
406 data_config *dc = (data_config *)srv->config_context->data[i];
407 s = p->config_storage[i];
409 /* condition didn't match */
410 if (!config_check_cond(srv, con, dc)) continue;
412 /* merge config */
413 for (j = 0; j < dc->value->used; j++) {
414 data_unset *du = dc->value->data[j];
416 if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.secret"))) {
417 PATCH(secret);
418 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.document-root"))) {
419 PATCH(doc_root);
420 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.uri-prefix"))) {
421 PATCH(uri_prefix);
422 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.timeout"))) {
423 PATCH(timeout);
424 } else if (buffer_is_equal_string(du->key, CONST_STR_LEN("secdownload.algorithm"))) {
425 PATCH(algorithm);
430 return 0;
432 #undef PATCH
435 URIHANDLER_FUNC(mod_secdownload_uri_handler) {
436 plugin_data *p = p_d;
437 const char *rel_uri, *ts_str, *mac_str, *protected_path;
438 time_t ts = 0;
439 size_t i, mac_len;
441 if (con->mode != DIRECT) return HANDLER_GO_ON;
443 if (buffer_is_empty(con->uri.path)) return HANDLER_GO_ON;
445 mod_secdownload_patch_connection(srv, con, p);
447 if (buffer_string_is_empty(p->conf.uri_prefix)) return HANDLER_GO_ON;
449 if (buffer_string_is_empty(p->conf.secret)) {
450 log_error_write(srv, __FILE__, __LINE__, "s",
451 "secdownload.secret has to be set");
452 con->http_status = 500;
453 return HANDLER_FINISHED;
456 if (buffer_string_is_empty(p->conf.doc_root)) {
457 log_error_write(srv, __FILE__, __LINE__, "s",
458 "secdownload.document-root has to be set");
459 con->http_status = 500;
460 return HANDLER_FINISHED;
463 if (SECDL_INVALID == p->conf.algorithm) {
464 log_error_write(srv, __FILE__, __LINE__, "s",
465 "secdownload.algorithm has to be set");
466 con->http_status = 500;
467 return HANDLER_FINISHED;
470 mac_len = secdl_algorithm_mac_length(p->conf.algorithm);
472 if (0 != strncmp(con->uri.path->ptr, p->conf.uri_prefix->ptr, buffer_string_length(p->conf.uri_prefix))) return HANDLER_GO_ON;
474 mac_str = con->uri.path->ptr + buffer_string_length(p->conf.uri_prefix);
476 if (!is_base64_len(mac_str, mac_len)) return HANDLER_GO_ON;
478 protected_path = mac_str + mac_len;
479 if (*protected_path != '/') return HANDLER_GO_ON;
481 ts_str = protected_path + 1;
482 if (!is_hex_len(ts_str, 8)) return HANDLER_GO_ON;
483 if (*(ts_str + 8) != '/') return HANDLER_GO_ON;
485 for (i = 0; i < 8; i++) {
486 ts = (ts << 4) + hex2int(ts_str[i]);
489 /* timed-out */
490 if ( (srv->cur_ts > ts && (unsigned int) (srv->cur_ts - ts) > p->conf.timeout) ||
491 (srv->cur_ts < ts && (unsigned int) (ts - srv->cur_ts) > p->conf.timeout) ) {
492 /* "Gone" as the url will never be valid again instead of "408 - Timeout" where the request may be repeated */
493 con->http_status = 410;
495 return HANDLER_FINISHED;
498 rel_uri = ts_str + 8;
500 if (!secdl_verify_mac(srv, &p->conf, protected_path, mac_str, mac_len)) {
501 con->http_status = 403;
503 if (con->conf.log_request_handling) {
504 log_error_write(srv, __FILE__, __LINE__, "sb",
505 "mac invalid:",
506 con->uri.path);
509 return HANDLER_FINISHED;
512 /* starting with the last / we should have relative-path to the docroot
515 buffer_copy_buffer(con->physical.doc_root, p->conf.doc_root);
516 buffer_copy_buffer(con->physical.basedir, p->conf.doc_root);
517 buffer_copy_string(con->physical.rel_path, rel_uri);
518 buffer_copy_buffer(con->physical.path, con->physical.doc_root);
519 buffer_append_string_buffer(con->physical.path, con->physical.rel_path);
521 return HANDLER_GO_ON;
524 /* this function is called at dlopen() time and inits the callbacks */
526 int mod_secdownload_plugin_init(plugin *p);
527 int mod_secdownload_plugin_init(plugin *p) {
528 p->version = LIGHTTPD_VERSION_ID;
529 p->name = buffer_init_string("secdownload");
531 p->init = mod_secdownload_init;
532 p->handle_physical = mod_secdownload_uri_handler;
533 p->set_defaults = mod_secdownload_set_defaults;
534 p->cleanup = mod_secdownload_free;
536 p->data = NULL;
538 return 0;