From 2a2e4b7f15f1ee17faefc3b4212f19c04f98d874 Mon Sep 17 00:00:00 2001 From: matcap Date: Mon, 10 May 2021 21:20:11 +0200 Subject: [PATCH 1/2] Whitelist function to exclude IPs/networks from JS challenge --- ngx_http_js_challenge.c | 157 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 155 insertions(+), 2 deletions(-) diff --git a/ngx_http_js_challenge.c b/ngx_http_js_challenge.c index ac93c80..b0f375c 100644 --- a/ngx_http_js_challenge.c +++ b/ngx_http_js_challenge.c @@ -27,6 +27,14 @@ #define DEFAULT_TITLE "Verifying your browser..." +#define WHITELIST_MAX_LENGTH 256 + + +typedef struct { + uint32_t addr; + uint32_t mask; +} network_info_t; + typedef struct { ngx_flag_t enabled; ngx_uint_t bucket_duration; @@ -34,6 +42,9 @@ typedef struct { ngx_str_t html_path; ngx_str_t title; char *html; + ngx_str_t whitelist_path; + size_t whitelist_len; + network_info_t* whitelist; } ngx_http_js_challenge_loc_conf_t; static ngx_int_t ngx_http_js_challenge(ngx_conf_t *cf); @@ -88,6 +99,14 @@ static ngx_command_t ngx_http_js_challenge_commands[] = { offsetof(ngx_http_js_challenge_loc_conf_t, title), NULL }, + { + ngx_string("js_challenge_whitelist"), + 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, whitelist_path), + NULL + }, ngx_null_command }; @@ -133,11 +152,83 @@ static void *ngx_http_js_challenge_create_loc_conf(ngx_conf_t *cf) { conf->secret = (ngx_str_t) {0, NULL}; conf->bucket_duration = NGX_CONF_UNSET_UINT; conf->enabled = NGX_CONF_UNSET; + conf->whitelist_len = 0; + conf->whitelist = NULL; return conf; } +static size_t load_whitelist( const char* file, network_info_t* list, size_t maxlen) +{ + FILE* fp = NULL; + char* linebuf = NULL; + char* token; + uint32_t octets[4]; + uint32_t maskbits; + size_t len = 0; + + if ((fp = fopen(file, "r")) == NULL) { + // Cannot open file + return 0; + } + + // Allocate 1KB for line + linebuf = malloc(1024); + if (!linebuf) { + return 0; + } + + // Read line + while (fgets(linebuf, 1024, fp) != NULL) { + // If line is empty or begins with # (comment) + // skip altogether + if (strlen(linebuf) == 0 || linebuf[0] == '#') { + continue; + } + + // Split by ; + token = strtok(linebuf, ";"); + + do + { + int res = sscanf(token, "%u.%u.%u.%u/%u", + &octets[0], + &octets[1], + &octets[2], + &octets[3], + &maskbits); + + // IP address found + if (res == 4 || res == 5) { + list[len].addr = ((octets[0] << 24) & 0xFF000000) + | ((octets[1] << 16) & 0x00FF0000) + | ((octets[2] << 8) & 0x0000FF00) + | (octets[3] & 0x000000FF); + + if (res == 5) { + // IPv4 network address + list[len].mask = ~((1u << (32 - maskbits)) - 1); + } + else { + // IPv4 host address + list[len].mask = 0xFFFFFFFF; + } + + ++len; + } + + + } while ((token = strtok(NULL, ";")) != NULL); + } + + free(linebuf); + fclose(fp); + return len; +} + + + 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; @@ -147,6 +238,8 @@ static char *ngx_http_js_challenge_merge_loc_conf(ngx_conf_t *cf, void *parent, 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) + ngx_conf_merge_str_value(conf->whitelist_path, prev->whitelist_path, NULL) + if (conf->bucket_duration < 1) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "bucket_duration must be equal or more than 1"); @@ -181,6 +274,32 @@ static char *ngx_http_js_challenge_merge_loc_conf(ngx_conf_t *cf, void *parent, } } + if (conf->whitelist_path.data == NULL) { + conf->whitelist_len = 0; + } else if (conf->enabled) { + + // Allocate necessary memory + conf->whitelist = ngx_palloc(cf->pool, + WHITELIST_MAX_LENGTH * sizeof(network_info_t)); + + + if (prev->whitelist != NULL) { + // Copy whitelist from previous config + // without need to reload from file + conf->whitelist_len = prev->whitelist_len; + for (size_t i=0; iwhitelist_len; ++i) { + conf->whitelist[i] = prev->whitelist[i]; + } + } else { + // Read whitelist file + char path[PATH_MAX]; + memcpy(path, conf->whitelist_path.data, conf->whitelist_path.len); + *(path + conf->whitelist_path.len) = '\0'; + + conf->whitelist_len = load_whitelist(path, conf->whitelist, WHITELIST_MAX_LENGTH); + } + } + return NGX_CONF_OK; } @@ -324,13 +443,47 @@ int get_cookie(ngx_http_request_t *r, ngx_str_t *name, ngx_str_t *value) { return -1; } -static ngx_int_t ngx_http_js_challenge_handler(ngx_http_request_t *r) { +static ngx_int_t ip_whitelist_hit (ngx_http_request_t *r, ngx_http_js_challenge_loc_conf_t* conf) { + char ipstr[16]; + uint32_t ip; + //unsigned char* ipa, *ipb; + // Convert ngx_str_t to C string + memset(ipstr, 0, sizeof(ipstr)); + ngx_snprintf((u_char*)ipstr, sizeof(ipstr), "%V", &r->connection->addr_text); + + // Convert C string to uint32_t + if ( !inet_pton(AF_INET, ipstr, &ip) ) { + return 0; + } + // Network to host order + ip = ntohl(ip); + + // Iterate through whitelist + for (size_t i = 0; i < conf->whitelist_len; ++i) { + /* + ipa = (unsigned char*) &ip; + ipb = (unsigned char*) &conf->whitelist[i].addr; + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "TESTING %ud.%ud.%ud.%ud <> %ud.%ud.%ud.%ud", + ipa[0],ipa[1],ipa[2],ipa[3],ipb[0],ipb[1],ipb[2],ipb[3]); + */ + if ((ip & conf->whitelist[i].mask) == (conf->whitelist[i].addr & conf->whitelist[i].mask)) { + // Whitelist hit + return 1; + } + } + + + return 0; +} + +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) { + if ( !conf->enabled || (conf->enabled && ip_whitelist_hit(r, conf))) { return NGX_DECLINED; } + unsigned long bucket = r->start_sec - (r->start_sec % conf->bucket_duration); ngx_str_t addr = r->connection->addr_text; From c05d000c00818a2d42050ddbd76dcb43a95f3b5f Mon Sep 17 00:00:00 2001 From: matcap Date: Tue, 11 May 2021 22:58:34 +0200 Subject: [PATCH 2/2] Increased whitelist to 1024 entries. Bound checking on loading. Updated README with new config --- README.md | 2 ++ ngx_http_js_challenge.c | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c54472b..09f36b6 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ server { js_challenge_html /path/to/body.html; js_challenge_bucket_duration 3600; js_challenge_title "Verifying your browser..."; + js_challenge_whitelist /path/to/whitelist/file.conf; location /static { js_challenge off; @@ -57,6 +58,7 @@ server { * `js_challenge_html "/path/to/file.html"` Path to html file to be inserted in the `` tag of the interstitial page * `js_challenge_title "title"` Will be inserted in the `` tag of the interstitial page. DEFAULT: "Verifying your browser..." * `js_challenge_bucket_duration time` Interval to prompt js challenge, in seconds. DEFAULT: 3600 +* `js_challenge_whitelist "/path/to/whitelist/file.conf"` Path to conf file containing whitelisted hosts and networks ### Installation diff --git a/ngx_http_js_challenge.c b/ngx_http_js_challenge.c index b0f375c..a7e9de4 100644 --- a/ngx_http_js_challenge.c +++ b/ngx_http_js_challenge.c @@ -27,7 +27,7 @@ #define DEFAULT_TITLE "Verifying your browser..." -#define WHITELIST_MAX_LENGTH 256 +#define WHITELIST_MAX_LENGTH 1024 typedef struct { @@ -219,7 +219,7 @@ static size_t load_whitelist( const char* file, network_info_t* list, size_t max } - } while ((token = strtok(NULL, ";")) != NULL); + } while (len < WHITELIST_MAX_LENGTH && ((token = strtok(NULL, ";")) != NULL)); } free(linebuf);