diff --git a/.github/workflows/nginx-module-build.yml b/.github/workflows/nginx-module-build.yml
new file mode 100644
index 0000000..e7e0aff
--- /dev/null
+++ b/.github/workflows/nginx-module-build.yml
@@ -0,0 +1,52 @@
+name: Build NGINX Module
+
+on:
+ push:
+ branches:
+ - master # Ensure this matches your main branch name
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y nginx gcc make
+
+ - name: Download and extract NGINX source
+ run: |
+ wget https://nginx.org/download/nginx-1.25.4.tar.gz
+ tar -xvzf nginx-1.25.4.tar.gz
+ echo "NGINX_PATH=$(pwd)/nginx-1.25.4" >> $GITHUB_ENV
+
+ - name: Run build script
+ run: |
+ chmod +x build.sh
+ ./build.sh
+
+ - name: List modules
+ run: |
+ ls ${NGINX_PATH}/objs
+
+ - name: Package module
+ run: |
+ find ${NGINX_PATH}/objs -name "*.so" -exec tar -czvf ngx_http_js_challenge_module.tar.gz -C ${NGINX_PATH}/objs {} +
+
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: ngx_http_js_challenge_module
+ path: ngx_http_js_challenge_module.tar.gz
+
+ - name: Check module output
+ run: |
+ ls ${NGINX_PATH}/objs/*.so
+
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
-
-
+[](LICENSE)
[](https://www.codefactor.io/repository/github/simon987/ngx_http_js_challenge_module)
+[](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 `` 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
### 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
+
+
+
\ 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
#include
+#include
+
#define DEFAULT_SECRET "changeme"
#define SHA1_MD_LEN 20
#define SHA1_STR_LEN 40
@@ -17,15 +19,51 @@
"" \
"" \
"" \
"%s" \
"" \
""
-#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 = "Set the js_challenge_html /path/to/body.html;
directive to change this page.
";
+ html = "Your connection is being verified. Please wait...
";
}
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 *)"Cookies Required
Please enable cookies in your browser to continue.
";
+ 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