diff --git a/README.md b/README.md index c54472b..ffa6a4a 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,64 @@ +# ngx_http_js_challenge_module -## ngx_http_js_challenge_module - -![GitHub](https://img.shields.io/github/license/simon987/ngx_http_js_challenge_module.svg) +[![GitHub License](https://img.shields.io/github/license/simon987/ngx_http_js_challenge_module.svg)](LICENSE) [![CodeFactor](https://www.codefactor.io/repository/github/simon987/ngx_http_js_challenge_module/badge)](https://www.codefactor.io/repository/github/simon987/ngx_http_js_challenge_module) +[![Demo Website](https://img.shields.io/badge/demo-website-blue.svg)](https://ngx-js-demo.simon987.net/) +Simple JavaScript proof-of-work based access control for Nginx, designed to provide security with minimal overhead. -[Demo website](https://ngx-js-demo.simon987.net/) +## Features -Simple javascript proof-of-work based access for Nginx with virtually no overhead. +- **Lightweight Integration:** Easy to integrate with existing Nginx installations. +- **Configurable Security:** Flexible settings to adjust security strength and client experience. +- **Minimal Performance Impact:** Designed to operate with virtually no additional server load. -Easy installation: just add `load_module /path/to/ngx_http_js_challenge_module.so;` to your -`nginx.conf` file and follow the [configuration instructions](#configuration). +## Quick Start -

- -

+1. **Installation** + Add the following line to your `nginx.conf`: + ``` + load_module /path/to/ngx_http_js_challenge_module.so; + ``` -### Configuration +2. **Configuration** + Use the simple or advanced configurations provided below to customize the module to your needs. -**Simple configuration** -```nginx +## Installation + +To install the ngx_http_js_challenge_module, follow these steps: + +1. Add the module loading directive to your Nginx configuration file (`/etc/nginx/nginx.conf`): + ``` + load_module /path/to/ngx_http_js_challenge_module.so; + ``` + +2. Apply the changes by reloading Nginx: + ``` + nginx -s reload + ``` + +## Configuration + +### Basic Configuration + +For basic setup, update your server block as follows: + +``` server { js_challenge on; - js_challenge_secret "change me!"; - - # ... + js_challenge_secret "change me!"; # Ensure to replace this with a strong secret in production } ``` +### Advanced Configuration -**Advanced configuration** -```nginx +For more complex setups, including exemptions for specific paths: + +``` server { js_challenge on; js_challenge_secret "change me!"; - js_challenge_html /path/to/body.html; + js_challenge_html "/path/to/body.html"; js_challenge_bucket_duration 3600; js_challenge_title "Verifying your browser..."; @@ -45,49 +69,51 @@ server { location /sensitive { js_challenge_bucket_duration 600; - #... + # Add further customization here } - - #... } ``` -* `js_challenge on|off` Toggle javascript challenges for this config block -* `js_challenge_secret "secret"` Secret for generating the challenges. DEFAULT: "changeme" -* `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 +### Parameters + +- **js_challenge on|off** Toggle javascript challenges for this config block +- **js_challenge_secret "secret"** Secret for generating the challenges. DEFAULT: "changeme" +- **js_challenge_html "/path/to/file.html"** Path to html file to be inserted in the `<body>` tag of the interstitial page +- **js_challenge_title "title"** Will be inserted in the `<title>` tag of the interstitial page. DEFAULT: "Verifying your browser..." +- **js_challenge_bucket_duration time** Interval to prompt js challenge, in seconds. DEFAULT: 3600 ### Installation 1. Add `load_module ngx_http_js_challenge_module.so;` to `/etc/nginx/nginx.conf` -1. Reload `nginx -s reload` +2. Reload `nginx -s reload` ### Build from source These steps have to be performed on machine with compatible configuration (same nginx, glibc, openssl version etc.) 1. Install dependencies - ```bash + ``` apt install libperl-dev libgeoip-dev libgd-dev libxslt1-dev libpcre3-dev ``` 2. Download nginx tarball corresponding to your current version (Check with `nginx -v`) - ```bash - wget https://nginx.org/download/nginx-1.16.1.tar.gz - tar -xzf nginx-1.16.1.tar.gz - export NGINX_PATH=$(pwd)/nginx-1.16.1/ + ``` + wget https://nginx.org/download/nginx-1.25.4.tar.gz + tar -xzf nginx-1.25.4.tar.gz + export NGINX_PATH=$(pwd)/nginx-1.25.4/ ``` 3. Compile the module - ```bash + ``` git clone https://github.com/simon987/ngx_http_js_challenge_module cd ngx_http_js_challenge_module ./build.sh ``` 4. The dynamic module can be found at `${NGINX_PATH}/objs/ngx_http_js_challenge_module.so` +### Known limitations (To Do) +* None -### Known limitations / TODO - -* Users with cookies disabled will be stuck in an infinite refresh loop (TODO: redirect with a known query param, if no cookie is specified but the query arg is set, display an error page) -* If nginx is behind a reverse proxy/load balancer, the same challenge will be sent to different users and/or the response cookie will be invalidated when the user is re-routed to another server. (TODO: use the x-real-ip header when available) +### Throughput +<p align="center"> + <img width="600px" src="throughput.png"/> +</p> \ No newline at end of file diff --git a/ngx_http_js_challenge.c b/ngx_http_js_challenge.c index 5adf14d..8e47f66 100644 --- a/ngx_http_js_challenge.c +++ b/ngx_http_js_challenge.c @@ -4,6 +4,8 @@ #include <stdint.h> #include <string.h> +#include <arpa/inet.h> + #define DEFAULT_SECRET "changeme" #define SHA1_MD_LEN 20 #define SHA1_STR_LEN 40 @@ -17,15 +19,51 @@ "</head>" \ "<body>" \ "<script>" \ - "window.onload=function(){!function(){function t(t){t?(f[0]=f[16]=f[1]=f[2]=f[3]=f[4]=f[5]=f[6]=f[7]=f[8]=f[9]=f[10]=f[11]=f[12]=f[13]=f[14]=f[15]=0,this.blocks=f):this.blocks=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],this.h0=1732584193,this.h1=4023233417,this.h2=2562383102,this.h3=271733878,this.h4=3285377520,this.block=this.start=this.bytes=this.hBytes=0,this.finalized=this.hashed=!1,this.first=!0}var h=\"object\"==typeof window?window:{},s=!h.JS_SHA1_NO_NODE_JS&&\"object\"==typeof process&&process.versions&&process.versions.node;s&&(h=global);var i=!h.JS_SHA1_NO_COMMON_JS&&\"object\"==typeof module&&module.exports,e=\"function\"==typeof define&&define.amd,r=\"0123456789abcdef\".split(\"\"),o=[-2147483648,8388608,32768,128],n=[24,16,8,0],a=[\"hex\",\"array\",\"digest\",\"arrayBuffer\"],f=[],u=function(h){return function(s){return new t(!0).update(s)[h]()}},c=function(){var h=u(\"hex\");s&&(h=p(h)),h.create=function(){return new t},h.update=function(t){return h.create().update(t)};for(var i=0;i<a.length;++i){var e=a[i];h[e]=u(e)}return h},p=function(t){var h=eval(\"require('crypto')\"),s=eval(\"require('buffer').Buffer\"),i=function(i){if(\"string\"==typeof i)return h.createHash(\"s1\").update(i,\"utf8\").digest(\"hex\");if(i.constructor===ArrayBuffer)i=new Uint8Array(i);else if(void 0===i.length)return t(i);return h.createHash(\"s1\").update(new s(i)).digest(\"hex\")};return i};t.prototype.update=function(t){if(!this.finalized){var s=\"string\"!=typeof t;s&&t.constructor===h.ArrayBuffer&&(t=new Uint8Array(t));for(var i,e,r=0,o=t.length||0,a=this.blocks;r<o;){if(this.hashed&&(this.hashed=!1,a[0]=this.block,a[16]=a[1]=a[2]=a[3]=a[4]=a[5]=a[6]=a[7]=a[8]=a[9]=a[10]=a[11]=a[12]=a[13]=a[14]=a[15]=0),s)for(e=this.start;r<o&&e<64;++r)a[e>>2]|=t[r]<<n[3&e++];else for(e=this.start;r<o&&e<64;++r)(i=t.charCodeAt(r))<128?a[e>>2]|=i<<n[3&e++]:i<2048?(a[e>>2]|=(192|i>>6)<<n[3&e++],a[e>>2]|=(128|63&i)<<n[3&e++]):i<55296||i>=57344?(a[e>>2]|=(224|i>>12)<<n[3&e++],a[e>>2]|=(128|i>>6&63)<<n[3&e++],a[e>>2]|=(128|63&i)<<n[3&e++]):(i=65536+((1023&i)<<10|1023&t.charCodeAt(++r)),a[e>>2]|=(240|i>>18)<<n[3&e++],a[e>>2]|=(128|i>>12&63)<<n[3&e++],a[e>>2]|=(128|i>>6&63)<<n[3&e++],a[e>>2]|=(128|63&i)<<n[3&e++]);this.lastByteIndex=e,this.bytes+=e-this.start,e>=64?(this.block=a[16],this.start=e-64,this.hash(),this.hashed=!0):this.start=e}return this.bytes>4294967295&&(this.hBytes+=this.bytes/4294967296<<0,this.bytes=this.bytes%%4294967296),this}},t.prototype.finalize=function(){if(!this.finalized){this.finalized=!0;var t=this.blocks,h=this.lastByteIndex;t[16]=this.block,t[h>>2]|=o[3&h],this.block=t[16],h>=56&&(this.hashed||this.hash(),t[0]=this.block,t[16]=t[1]=t[2]=t[3]=t[4]=t[5]=t[6]=t[7]=t[8]=t[9]=t[10]=t[11]=t[12]=t[13]=t[14]=t[15]=0),t[14]=this.hBytes<<3|this.bytes>>>29,t[15]=this.bytes<<3,this.hash()}},t.prototype.hash=function(){var t,h,s=this.h0,i=this.h1,e=this.h2,r=this.h3,o=this.h4,n=this.blocks;for(t=16;t<80;++t)h=n[t-3]^n[t-8]^n[t-14]^n[t-16],n[t]=h<<1|h>>>31;for(t=0;t<20;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i&e|~i&r)+o+1518500249+n[t]<<0)<<5|o>>>27)+(s&(i=i<<30|i>>>2)|~s&e)+r+1518500249+n[t+1]<<0)<<5|r>>>27)+(o&(s=s<<30|s>>>2)|~o&i)+e+1518500249+n[t+2]<<0)<<5|e>>>27)+(r&(o=o<<30|o>>>2)|~r&s)+i+1518500249+n[t+3]<<0)<<5|i>>>27)+(e&(r=r<<30|r>>>2)|~e&o)+s+1518500249+n[t+4]<<0,e=e<<30|e>>>2;for(;t<40;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i^e^r)+o+1859775393+n[t]<<0)<<5|o>>>27)+(s^(i=i<<30|i>>>2)^e)+r+1859775393+n[t+1]<<0)<<5|r>>>27)+(o^(s=s<<30|s>>>2)^i)+e+1859775393+n[t+2]<<0)<<5|e>>>27)+(r^(o=o<<30|o>>>2)^s)+i+1859775393+n[t+3]<<0)<<5|i>>>27)+(e^(r=r<<30|r>>>2)^o)+s+1859775393+n[t+4]<<0,e=e<<30|e>>>2;for(;t<60;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i&e|i&r|e&r)+o-1894007588+n[t]<<0)<<5|o>>>27)+(s&(i=i<<30|i>>>2)|s&e|i&e)+r-1894007588+n[t+1]<<0)<<5|r>>>27)+(o&(s=s<<30|s>>>2)|o&i|s&i)+e-1894007588+n[t+2]<<0)<<5|e>>>27)+(r&(o=o<<30|o>>>2)|r&s|o&s)+i-1894007588+n[t+3]<<0)<<5|i>>>27)+(e&(r=r<<30|r>>>2)|e&o|r&o)+s-1894007588+n[t+4]<<0,e=e<<30|e>>>2;for(;t<80;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i^e^r)+o-899497514+n[t]<<0)<<5|o>>>27)+(s^(i=i<<30|i>>>2)^e)+r-899497514+n[t+1]<<0)<<5|r>>>27)+(o^(s=s<<30|s>>>2)^i)+e-899497514+n[t+2]<<0)<<5|e>>>27)+(r^(o=o<<30|o>>>2)^s)+i-899497514+n[t+3]<<0)<<5|i>>>27)+(e^(r=r<<30|r>>>2)^o)+s-899497514+n[t+4]<<0,e=e<<30|e>>>2;this.h0=this.h0+s<<0,this.h1=this.h1+i<<0,this.h2=this.h2+e<<0,this.h3=this.h3+r<<0,this.h4=this.h4+o<<0},t.prototype.hex=function(){this.finalize();var t=this.h0,h=this.h1,s=this.h2,i=this.h3,e=this.h4;return r[t>>28&15]+r[t>>24&15]+r[t>>20&15]+r[t>>16&15]+r[t>>12&15]+r[t>>8&15]+r[t>>4&15]+r[15&t]+r[h>>28&15]+r[h>>24&15]+r[h>>20&15]+r[h>>16&15]+r[h>>12&15]+r[h>>8&15]+r[h>>4&15]+r[15&h]+r[s>>28&15]+r[s>>24&15]+r[s>>20&15]+r[s>>16&15]+r[s>>12&15]+r[s>>8&15]+r[s>>4&15]+r[15&s]+r[i>>28&15]+r[i>>24&15]+r[i>>20&15]+r[i>>16&15]+r[i>>12&15]+r[i>>8&15]+r[i>>4&15]+r[15&i]+r[e>>28&15]+r[e>>24&15]+r[e>>20&15]+r[e>>16&15]+r[e>>12&15]+r[e>>8&15]+r[e>>4&15]+r[15&e]},t.prototype.toString=t.prototype.hex,t.prototype.digest=function(){this.finalize();var t=this.h0,h=this.h1,s=this.h2,i=this.h3,e=this.h4;return[t>>24&255,t>>16&255,t>>8&255,255&t,h>>24&255,h>>16&255,h>>8&255,255&h,s>>24&255,s>>16&255,s>>8&255,255&s,i>>24&255,i>>16&255,i>>8&255,255&i,e>>24&255,e>>16&255,e>>8&255,255&e]},t.prototype.array=t.prototype.digest,t.prototype.arrayBuffer=function(){this.finalize();var t=new ArrayBuffer(20),h=new DataView(t);return h.setUint32(0,this.h0),h.setUint32(4,this.h1),h.setUint32(8,this.h2),h.setUint32(12,this.h3),h.setUint32(16,this.h4),t};var y=c();i?module.exports=y:(h.s1=y,e&&define(function(){return y}))}();" \ - "const a0_0x2a54=['%s','res=','array'];(function(_0x41abf3,_0x2a548e){const _0x4457dc=function(_0x804ad2){while(--_0x804ad2){_0x41abf3['push'](_0x41abf3['shift']());}};_0x4457dc(++_0x2a548e);}(a0_0x2a54,0x178));const a0_0x4457=function(_0x41abf3,_0x2a548e){_0x41abf3=_0x41abf3-0x0;let _0x4457dc=a0_0x2a54[_0x41abf3];return _0x4457dc;};let c=a0_0x4457('0x2');let i=0x0;let n1=parseInt('0x'+c[0x0]);while(!![]){let s=s1[a0_0x4457('0x1')](c+i);if(s[n1]===0xb0&&s[n1+0x1]===0xb){document['cookie']=a0_0x4457('0x0')+c+i+'; path=/';break;}i++;};" \ - ";window.setTimeout(function(){window.location.reload()}, 3000)}" \ + "window.onload = function() {" \ + " if (!navigator.cookieEnabled) {" \ + " if (!window.location.search.includes('no_cookie=true')) {" \ + " window.location.search += (window.location.search ? '&' : '?') + 'no_cookie=true';" \ + " } else {" \ + " document.body.innerHTML = '<h1>Cookies are required to access this content.</h1><p>Please enable cookies in your browser settings and try again.</p>';" \ + " }" \ + " } else {" \ + " !function(){function t(t){t?(f[0]=f[16]=f[1]=f[2]=f[3]=f[4]=f[5]=f[6]=f[7]=f[8]=f[9]=f[10]=f[11]=f[12]=f[13]=f[14]=f[15]=0,this.blocks=f):this.blocks=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],this.h0=1732584193,this.h1=4023233417,this.h2=2562383102,this.h3=271733878,this.h4=3285377520,this.block=this.start=this.bytes=this.hBytes=0,this.finalized=this.hashed=!1,this.first=!0}var h=\"object\"==typeof window?window:{},s=!h.JS_SHA1_NO_NODE_JS&&\"object\"==typeof process&&process.versions&&process.versions.node;s&&(h=global);var i=!h.JS_SHA1_NO_COMMON_JS&&\"object\"==typeof module&&module.exports,e=\"function\"==typeof define&&define.amd,r=\"0123456789abcdef\".split(\"\"),o=[-2147483648,8388608,32768,128],n=[24,16,8,0],a=[\"hex\",\"array\",\"digest\",\"arrayBuffer\"],f=[],u=function(h){return function(s){return new t(!0).update(s)[h]()}},c=function(){var h=u(\"hex\");s&&(h=p(h)),h.create=function(){return new t},h.update=function(t){return h.create().update(t)};for(var i=0;i<a.length;++i){var e=a[i];h[e]=u(e)}return h},p=function(t){var h=eval(\"require('crypto')\"),s=eval(\"require('buffer').Buffer\"),i=function(i){if(\"string\"==typeof i)return h.createHash(\"s1\").update(i,\"utf8\").digest(\"hex\");if(i.constructor===ArrayBuffer)i=new Uint8Array(i);else if(void 0===i.length)return t(i);return h.createHash(\"s1\").update(new s(i)).digest(\"hex\")};return i};t.prototype.update=function(t){if(!this.finalized){var s=\"string\"!=typeof t;s&&t.constructor===h.ArrayBuffer&&(t=new Uint8Array(t));for(var i,e,r=0,o=t.length||0,a=this.blocks;r<o;){if(this.hashed&&(this.hashed=!1,a[0]=this.block,a[16]=a[1]=a[2]=a[3]=a[4]=a[5]=a[6]=a[7]=a[8]=a[9]=a[10]=a[11]=a[12]=a[13]=a[14]=a[15]=0),s)for(e=this.start;r<o&&e<64;++r)a[e>>2]|=t[r]<<n[3&e++];else for(e=this.start;r<o&&e<64;++r)(i=t.charCodeAt(r))<128?a[e>>2]|=i<<n[3&e++]:i<2048?(a[e>>2]|=(192|i>>6)<<n[3&e++],a[e>>2]|=(128|63&i)<<n[3&e++]):i<55296||i>=57344?(a[e>>2]|=(224|i>>12)<<n[3&e++],a[e>>2]|=(128|i>>6&63)<<n[3&e++],a[e>>2]|=(128|63&i)<<n[3&e++]):(i=65536+((1023&i)<<10|1023&t.charCodeAt(++r)),a[e>>2]|=(240|i>>18)<<n[3&e++],a[e>>2]|=(128|i>>12&63)<<n[3&e++],a[e>>2]|=(128|i>>6&63)<<n[3&e++],a[e>>2]|=(128|63&i)<<n[3&e++]);this.lastByteIndex=e,this.bytes+=e-this.start,e>=64?(this.block=a[16],this.start=e-64,this.hash(),this.hashed=!0):this.start=e}return this.bytes>4294967295&&(this.hBytes+=this.bytes/4294967296<<0,this.bytes=this.bytes%%4294967296),this}},t.prototype.finalize=function(){if(!this.finalized){this.finalized=!0;var t=this.blocks,h=this.lastByteIndex;t[16]=this.block,t[h>>2]|=o[3&h],this.block=t[16],h>=56&&(this.hashed||this.hash(),t[0]=this.block,t[16]=t[1]=t[2]=t[3]=t[4]=t[5]=t[6]=t[7]=t[8]=t[9]=t[10]=t[11]=t[12]=t[13]=t[14]=t[15]=0),t[14]=this.hBytes<<3|this.bytes>>>29,t[15]=this.bytes<<3,this.hash()}},t.prototype.hash=function(){var t,h,s=this.h0,i=this.h1,e=this.h2,r=this.h3,o=this.h4,n=this.blocks;for(t=16;t<80;++t)h=n[t-3]^n[t-8]^n[t-14]^n[t-16],n[t]=h<<1|h>>>31;for(t=0;t<20;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i&e|~i&r)+o+1518500249+n[t]<<0)<<5|o>>>27)+(s&(i=i<<30|i>>>2)|~s&e)+r+1518500249+n[t+1]<<0)<<5|r>>>27)+(o&(s=s<<30|s>>>2)|~o&i)+e+1518500249+n[t+2]<<0)<<5|e>>>27)+(r&(o=o<<30|o>>>2)|~r&s)+i+1518500249+n[t+3]<<0)<<5|i>>>27)+(e&(r=r<<30|r>>>2)|~e&o)+s+1518500249+n[t+4]<<0,e=e<<30|e>>>2;for(;t<40;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i^e^r)+o+1859775393+n[t]<<0)<<5|o>>>27)+(s^(i=i<<30|i>>>2)^e)+r+1859775393+n[t+1]<<0)<<5|r>>>27)+(o^(s=s<<30|s>>>2)^i)+e+1859775393+n[t+2]<<0)<<5|e>>>27)+(r^(o=o<<30|o>>>2)^s)+i+1859775393+n[t+3]<<0)<<5|i>>>27)+(e^(r=r<<30|r>>>2)^o)+s+1859775393+n[t+4]<<0,e=e<<30|e>>>2;for(;t<60;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i&e|i&r|e&r)+o-1894007588+n[t]<<0)<<5|o>>>27)+(s&(i=i<<30|i>>>2)|s&e|i&e)+r-1894007588+n[t+1]<<0)<<5|r>>>27)+(o&(s=s<<30|s>>>2)|o&i|s&i)+e-1894007588+n[t+2]<<0)<<5|e>>>27)+(r&(o=o<<30|o>>>2)|r&s|o&s)+i-1894007588+n[t+3]<<0)<<5|i>>>27)+(e&(r=r<<30|r>>>2)|e&o|r&o)+s-1894007588+n[t+4]<<0,e=e<<30|e>>>2;for(;t<80;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i^e^r)+o-899497514+n[t]<<0)<<5|o>>>27)+(s^(i=i<<30|i>>>2)^e)+r-899497514+n[t+1]<<0)<<5|r>>>27)+(o^(s=s<<30|s>>>2)^i)+e-899497514+n[t+2]<<0)<<5|e>>>27)+(r^(o=o<<30|o>>>2)^s)+i-899497514+n[t+3]<<0)<<5|i>>>27)+(e^(r=r<<30|r>>>2)^o)+s-899497514+n[t+4]<<0,e=e<<30|e>>>2;this.h0=this.h0+s<<0,this.h1=this.h1+i<<0,this.h2=this.h2+e<<0,this.h3=this.h3+r<<0,this.h4=this.h4+o<<0},t.prototype.hex=function(){this.finalize();var t=this.h0,h=this.h1,s=this.h2,i=this.h3,e=this.h4;return r[t>>28&15]+r[t>>24&15]+r[t>>20&15]+r[t>>16&15]+r[t>>12&15]+r[t>>8&15]+r[t>>4&15]+r[15&t]+r[h>>28&15]+r[h>>24&15]+r[h>>20&15]+r[h>>16&15]+r[h>>12&15]+r[h>>8&15]+r[h>>4&15]+r[15&h]+r[s>>28&15]+r[s>>24&15]+r[s>>20&15]+r[s>>16&15]+r[s>>12&15]+r[s>>8&15]+r[s>>4&15]+r[15&s]+r[i>>28&15]+r[i>>24&15]+r[i>>20&15]+r[i>>16&15]+r[i>>12&15]+r[i>>8&15]+r[i>>4&15]+r[15&i]+r[e>>28&15]+r[e>>24&15]+r[e>>20&15]+r[e>>16&15]+r[e>>12&15]+r[e>>8&15]+r[e>>4&15]+r[15&e]},t.prototype.toString=t.prototype.hex,t.prototype.digest=function(){this.finalize();var t=this.h0,h=this.h1,s=this.h2,i=this.h3,e=this.h4;return[t>>24&255,t>>16&255,t>>8&255,255&t,h>>24&255,h>>16&255,h>>8&255,255&h,s>>24&255,s>>16&255,s>>8&255,255&s,i>>24&255,i>>16&255,i>>8&255,255&i,e>>24&255,e>>16&255,e>>8&255,255&e]},t.prototype.array=t.prototype.digest,t.prototype.arrayBuffer=function(){this.finalize();var t=new ArrayBuffer(20),h=new DataView(t);return h.setUint32(0,this.h0),h.setUint32(4,this.h1),h.setUint32(8,this.h2),h.setUint32(12,this.h3),h.setUint32(16,this.h4),t};var y=c();i?module.exports=y:(h.s1=y,e&&define(function(){return y}))}();" \ + "const a0_0x2a54=['%s','challenge_token=','array'];(function(_0x41abf3,_0x2a548e){const _0x4457dc=function(_0x804ad2){while(--_0x804ad2){_0x41abf3['push'](_0x41abf3['shift']());}};_0x4457dc(++_0x2a548e);}(a0_0x2a54,0x178));const a0_0x4457=function(_0x41abf3,_0x2a548e){_0x41abf3=_0x41abf3-0x0;let _0x4457dc=a0_0x2a54[_0x41abf3];return _0x4457dc;};let c=a0_0x4457('0x2');let i=0x0;let n1=parseInt('0x'+c[0x0]);while(!![]){let s=s1[a0_0x4457('0x1')](c+i);if(s[n1]===0xb0&&s[n1+0x1]===0xb){document['cookie']=a0_0x4457('0x0')+c+i+'; path=/';break;}i++;};" \ + ";window.setTimeout(function(){window.location.reload()}, 3000)}}" \ "</script>" \ "%s" \ "</body>" \ "</html>" -#define DEFAULT_TITLE "Verifying your browser..." + +#define DEFAULT_TITLE "Browser Verification" + +static int is_private_ip(const char *ip) { + struct in_addr addr; + if (inet_pton(AF_INET, ip, &addr) != 1) { + return 0; // Not a valid IP address + } + + uint32_t host_addr = ntohl(addr.s_addr); + + // 10.0.0.0/8 + if ((host_addr & 0xFF000000) == 0x0A000000) { + return 1; + } + + // 172.16.0.0/12 + if ((host_addr & 0xFFF00000) == 0xAC100000) { + return 1; + } + + // 192.168.0.0/16 + if ((host_addr & 0xFFFF0000) == 0xC0A80000) { + return 1; + } + + return 0; // IP is not within the private ranges +} + typedef struct { ngx_flag_t enabled; @@ -220,7 +258,7 @@ int serve_challenge(ngx_http_request_t *r, const char *challenge, const char *ht static const ngx_str_t content_type = ngx_string("text/html;charset=utf-8"); if (html == NULL) { - html = "<h2>Set the <code>js_challenge_html /path/to/body.html;</code> directive to change this page.</h2>"; + html = "<h2>Your connection is being verified. Please wait...</h2>"; } size_t size = snprintf((char *) buf, sizeof(buf), JS_SOLVER_TEMPLATE, title_c_str, challenge_c_str, html); @@ -248,22 +286,28 @@ int serve_challenge(ngx_http_request_t *r, const char *challenge, const char *ht /** * @param out 40 bytes long string! */ -void get_challenge_string(int32_t bucket, ngx_str_t addr, ngx_str_t secret, char *out) { +void get_challenge_string(int32_t bucket, ngx_str_t addr, ngx_str_t user_agent, 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) ) ) + * Challenge = hex( SHA1( concat(bucket, addr, user_agent, 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); + int offset = sizeof(int32_t); + memcpy(buf, p, sizeof(bucket)); // Copy the bucket + memcpy(buf + offset, addr.data, addr.len); // Copy the IP address + offset += addr.len; + memcpy(buf + offset, user_agent.data, user_agent.len); // Copy the User-Agent + offset += user_agent.len; + memcpy(buf + offset, secret.data, secret.len); // Copy the secret - __sha1((unsigned char *) buf, (size_t) (sizeof(int32_t) + addr.len + secret.len), md); - buf2hex(md, SHA1_MD_LEN, out); + // Calculate SHA1 hash of the concatenated data + __sha1((unsigned char *) buf, (size_t) (offset + secret.len), md); + buf2hex(md, SHA1_MD_LEN, out); // Convert the hash to a hexadecimal string } + int verify_response(ngx_str_t response, char *challenge) { /* @@ -335,32 +379,94 @@ int get_cookie(ngx_http_request_t *r, ngx_str_t *name, ngx_str_t *value) { } 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; + // Check if 'no_cookie' parameter is present in the query string + ngx_uint_t no_cookie_present = 0; + ngx_str_t no_cookie_arg = ngx_string("no_cookie"); + ngx_str_t value; + if (ngx_http_arg(r, no_cookie_arg.data, no_cookie_arg.len, &value) == NGX_OK) { + no_cookie_present = 1; + } + // Handle the no_cookie case by showing a static error message + if (no_cookie_present) { + ngx_buf_t *b = ngx_create_temp_buf(r->pool, 1024); + if (b == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; + } + ngx_chain_t out; + out.buf = b; + out.next = NULL; + + b->pos = (u_char *)"<html><body><h1>Cookies Required</h1><p>Please enable cookies in your browser to continue.</p></body></html>"; + b->last = b->pos + strlen((char *)b->pos); + b->memory = 1; // memory of the buffer is readonly + b->last_buf = 1; // this is the last buffer in the buffer chain + + r->headers_out.status = NGX_HTTP_FORBIDDEN; + r->headers_out.content_type_len = sizeof("text/html") - 1; + r->headers_out.content_type.len = sizeof("text/html") - 1; + r->headers_out.content_type.data = (u_char *)"text/html"; + r->headers_out.content_length_n = b->last - b->pos; + + ngx_http_send_header(r); + ngx_http_output_filter(r, &out); + ngx_http_finalize_request(r, NGX_HTTP_FORBIDDEN); + return NGX_HTTP_FORBIDDEN; + } + + // Check for X-REAL-IP header and fallback to connection IP if not present + ngx_str_t addr = r->connection->addr_text; // Default IP + ngx_list_part_t *part = &r->headers_in.headers.part; + ngx_table_elt_t *header = part->elts; + + for (ngx_uint_t i = 0; i < part->nelts; i++) { + if ((ngx_strncasecmp(header[i].key.data, (u_char *)"X-REAL-IP", header[i].key.len) == 0 || + ngx_strncasecmp(header[i].key.data, (u_char *)"X-FORWARDED-FOR", header[i].key.len) == 0) && + header[i].value.len > 0 && header[i].value.len <= 39) { + // Convert ngx_str_t to NULL-terminated string for is_private_ip + char ip_str[40]; + ngx_cpystrn((u_char *)ip_str, addr.data, addr.len + 1); + if (is_private_ip(ip_str)) { + addr = header[i].value; + break; + } + } + if (i == part->nelts - 1 && part->next != NULL) { + part = part->next; + header = part->elts; + i = -1; + } + } + + // Extract User-Agent header + ngx_str_t user_agent = {0, NULL}; // Initialize ngx_str_t with default values. + if (r && r->headers_in.user_agent) { + user_agent = r->headers_in.user_agent->value; + } + + unsigned long bucket = r->start_sec - (r->start_sec % conf->bucket_duration); char challenge[SHA1_STR_LEN]; - get_challenge_string(bucket, addr, conf->secret, challenge); + get_challenge_string(bucket, addr, user_agent, conf->secret, challenge); // Updated to include User-Agent ngx_str_t response; - ngx_str_t cookie_name = ngx_string("res"); + ngx_str_t cookie_name = ngx_string("challenge_token"); int ret = get_cookie(r, &cookie_name, &response); if (ret < 0) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "[ js challenge log ] sending challenge... "); - return serve_challenge(r, challenge, conf->html, conf->title); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "[ js challenge log ] sending challenge... "); + return serve_challenge(r, challenge, conf->html, conf->title); } - get_challenge_string(bucket, addr, conf->secret, challenge); + get_challenge_string(bucket, addr, user_agent, conf->secret, challenge); // Re-hash with the latest data if (verify_response(response, challenge) != 0) { - ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "[ js challenge log ] wrong/expired cookie (res=%s), sending challenge...", response.data); + ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "[ js challenge log ] wrong/expired cookie (challenge_token=%s), sending challenge...", response.data); return serve_challenge(r, challenge, conf->html, conf->title); } @@ -628,4 +734,4 @@ unsigned char *__sha1(const unsigned char *d, size_t n, unsigned char *md) { __SHA1_Update(&c, d, n); __SHA1_Final(md, &c); return md; -} +} \ No newline at end of file