#include #include "ngx_http.c" #define DEFAULT_SECRET "changeme" #define SHA1_MD_LEN 20 #define SHA1_STR_LEN 40 #define JS_SOLVER_TEMPLATE \ "" \ "" \ "" \ "" \ "%s" \ "" \ "" \ "" \ "" \ "%s" \ "" \ "" #define DEFAULT_TITLE "Verifying your browser..." typedef struct { ngx_flag_t enabled; ngx_uint_t bucket_duration; ngx_str_t secret; ngx_str_t html_path; ngx_str_t title; char *html; } ngx_http_js_challenge_loc_conf_t; static ngx_int_t ngx_http_js_challenge(ngx_conf_t *cf); static void *ngx_http_js_challenge_create_loc_conf(ngx_conf_t *cf); static char *ngx_http_js_challenge_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child); static ngx_int_t ngx_http_js_challenge_handler(ngx_http_request_t *r); static ngx_command_t ngx_http_js_challenge_commands[] = { { ngx_string("js_challenge"), NGX_HTTP_LOC_CONF | NGX_HTTP_SRV_CONF | NGX_CONF_FLAG, ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_js_challenge_loc_conf_t, enabled), NULL }, { ngx_string("js_challenge_bucket_duration"), NGX_HTTP_LOC_CONF | NGX_HTTP_SRV_CONF | NGX_CONF_TAKE1, ngx_conf_set_num_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_js_challenge_loc_conf_t, bucket_duration), NULL }, { ngx_string("js_challenge_secret"), NGX_HTTP_LOC_CONF | NGX_HTTP_SRV_CONF | NGX_CONF_TAKE1, ngx_conf_set_str_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_js_challenge_loc_conf_t, secret), NULL }, { ngx_string("js_challenge_secret"), NGX_HTTP_LOC_CONF | NGX_HTTP_SRV_CONF | NGX_CONF_TAKE1, ngx_conf_set_str_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_js_challenge_loc_conf_t, secret), NULL }, { ngx_string("js_challenge_html"), NGX_HTTP_LOC_CONF | NGX_HTTP_SRV_CONF | NGX_CONF_TAKE1, ngx_conf_set_str_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_js_challenge_loc_conf_t, html_path), NULL }, { ngx_string("js_challenge_title"), NGX_HTTP_LOC_CONF | NGX_HTTP_SRV_CONF | NGX_CONF_TAKE1, ngx_conf_set_str_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_js_challenge_loc_conf_t, title), NULL }, ngx_null_command }; /** * Module context */ static ngx_http_module_t ngx_http_js_challenge_module_ctx = { NULL, /* preconfiguration */ ngx_http_js_challenge, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_js_challenge_create_loc_conf, ngx_http_js_challenge_merge_loc_conf }; /* Module definition. */ ngx_module_t ngx_http_js_challenge_module = { NGX_MODULE_V1, &ngx_http_js_challenge_module_ctx, /* module context */ ngx_http_js_challenge_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; static void *ngx_http_js_challenge_create_loc_conf(ngx_conf_t *cf) { ngx_http_js_challenge_loc_conf_t *conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_js_challenge_loc_conf_t)); if (conf == NULL) { return NGX_CONF_ERROR; } conf->secret = (ngx_str_t) {0, NULL}; conf->bucket_duration = NGX_CONF_UNSET_UINT; conf->enabled = NGX_CONF_UNSET; return conf; } static char *ngx_http_js_challenge_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_js_challenge_loc_conf_t *prev = parent; ngx_http_js_challenge_loc_conf_t *conf = child; ngx_conf_merge_uint_value(conf->bucket_duration, prev->bucket_duration, 3600) ngx_conf_merge_value(conf->enabled, prev->enabled, 0) ngx_conf_merge_str_value(conf->secret, prev->secret, DEFAULT_SECRET) ngx_conf_merge_str_value(conf->html_path, prev->html_path, NULL) ngx_conf_merge_str_value(conf->title, prev->title, DEFAULT_TITLE) if (conf->bucket_duration < 1) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "bucket_duration must be equal or more than 1"); return NGX_CONF_ERROR; } if (conf->html_path.data == NULL) { conf->html = NULL; } else if (conf->enabled) { // Read file in memory char path[PATH_MAX]; memcpy(path, conf->html_path.data, conf->html_path.len); *(path + conf->html_path.len) = '\0'; struct stat info; stat(path, &info); int fd = open(path, O_RDONLY, 0); if (fd < 0) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "js_challenge_html: Could not open file '%s': %s", path, strerror(errno)); return NGX_CONF_ERROR; } conf->html = ngx_palloc(cf->pool, info.st_size); int ret = read(fd, conf->html, info.st_size); if (ret < 0) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "js_challenge_html: Could not read file '%s': %s", path, strerror(errno)); return NGX_CONF_ERROR; } } return NGX_CONF_OK; } __always_inline static void buf2hex(const unsigned char *buf, size_t buflen, char *hex_string) { static const char hexdig[] = "0123456789ABCDEF"; const unsigned char *p; size_t i; char *s = hex_string; for (i = 0, p = buf; i < buflen; i++, p++) { *s++ = hexdig[(*p >> 4) & 0x0f]; *s++ = hexdig[*p & 0x0f]; } } int serve_challenge(ngx_http_request_t *r, const char *challenge, const char *html, ngx_str_t title) { ngx_buf_t *b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); ngx_chain_t out; char challenge_c_str[SHA1_STR_LEN + 1]; memcpy(challenge_c_str, challenge, SHA1_STR_LEN); *(challenge_c_str + SHA1_STR_LEN) = '\0'; char title_c_str[4096]; memcpy(title_c_str, title.data, title.len); *(title_c_str + title.len) = '\0'; unsigned char buf[32768]; if (html == NULL) { html = "

Set the js_challenge_html /path/to/body.html; directive to change this page.

"; } size_t size = snprintf((char *) buf, sizeof(buf), JS_SOLVER_TEMPLATE, title_c_str, challenge_c_str, html); out.buf = b; out.next = NULL; // TODO: is that stack buffer gonna cause problems? b->pos = buf; b->last = buf + size; b->memory = 1; b->last_buf = 1; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = size; ngx_http_send_header(r); return ngx_http_output_filter(r, &out); } /** * @param out 40 bytes long string! */ void get_challenge_string(int32_t bucket, ngx_str_t addr, ngx_str_t secret, char *out) { char buf[4096]; unsigned char md[SHA1_MD_LEN]; char * p = (char*)&bucket; /* * Challenge= hex( SHA1( concat(bucket, addr, secret) ) ) */ memcpy(buf, p, sizeof(bucket)); memcpy((buf + sizeof(int32_t)), addr.data, addr.len); memcpy((buf + sizeof(int32_t) + addr.len), secret.data, secret.len); SHA1((unsigned char *) buf, (size_t) (sizeof(int32_t) + addr.len + secret.len), md); buf2hex(md, SHA1_MD_LEN, out); } int verify_response(ngx_str_t response, char *challenge) { /* * Response is valid if it starts by the challenge, and * its SHA1 hash contains the digits 0xB00B at the offset * of the first digit * * e.g. * challenge = "CC003677C91D53E29F7095FF90C670C69C7C46E7" * response = "CC003677C91D53E29F7095FF90C670C69C7C46E7635919" * SHA1(response) = "CCAE6E414FA62F9C2DFC2742B00B5C94A549BAE6" * ^ offset 24 */ //todo also check if the response is too large if (response.len <= SHA1_STR_LEN) { return -1; } if (strncmp(challenge, (char *) response.data, SHA1_STR_LEN) != 0) { return -1; } unsigned char md[SHA1_MD_LEN]; SHA1((unsigned char *) response.data, response.len, md); unsigned int nibble1; if (challenge[0] <= '9') { nibble1 = challenge[0] - '0'; } else { nibble1 = challenge[0] - 'A' + 10; } return md[nibble1] == 0xB0 && md[nibble1 + 1] == 0x0B ? 0 : -1; } int get_cookie(ngx_http_request_t *r, ngx_str_t *name, ngx_str_t *value) { ngx_table_elt_t **h; h = r->headers_in.cookies.elts; for (ngx_uint_t i = 0; i < r->headers_in.cookies.nelts; i++) { u_char *start = h[i]->value.data; u_char *end = h[i]->value.data + h[i]->value.len; while (start < end) { while (start < end && *start == ' ') { start++; } if (ngx_strncmp(start, name->data, name->len) == 0) { u_char *last; for (last = start; last < end && *last != ';'; last++) {} while (*start++ != '=' && start < last) {} value->data = start; value->len = (last - start); return 0; } while (*start++ != ';' && start < end) {} } } return -1; } static ngx_int_t ngx_http_js_challenge_handler(ngx_http_request_t *r) { ngx_http_js_challenge_loc_conf_t *conf = ngx_http_get_module_loc_conf(r, ngx_http_js_challenge_module); if (!conf->enabled) { return NGX_DECLINED; } unsigned long bucket = r->start_sec - (r->start_sec % conf->bucket_duration); ngx_str_t addr = r->connection->addr_text; char challenge[SHA1_STR_LEN]; get_challenge_string(bucket, addr, conf->secret, challenge); ngx_str_t response; int ret = get_cookie(r, &((ngx_str_t) ngx_string("res")), &response); if (ret < 0) { return serve_challenge(r, challenge, conf->html, conf->title); } get_challenge_string(bucket, addr, conf->secret, challenge); if (verify_response(response, challenge) != 0) { return serve_challenge(r, challenge, conf->html, conf->title); } // Fallthrough next handler return NGX_DECLINED; } /** * post configuration */ static ngx_int_t ngx_http_js_challenge(ngx_conf_t *cf) { ngx_http_handler_pt *h; ngx_http_core_main_conf_t *main_conf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); h = ngx_array_push(&main_conf->phases[NGX_HTTP_PRECONTENT_PHASE].handlers); if (h == NULL) { ngx_log_error(NGX_LOG_ERR, cf->log, 0, "null"); return NGX_ERROR; } *h = ngx_http_js_challenge_handler; return NGX_OK; }