diff --git a/haproxy/haproxy.cfg b/haproxy/haproxy.cfg index ceaea01..807e2bd 100644 --- a/haproxy/haproxy.cfg +++ b/haproxy/haproxy.cfg @@ -43,15 +43,15 @@ frontend http-in acl is_existing_vhost hdr(host),lower,map_str(/etc/haproxy/hosts.map) -m found http-request silent-drop unless is_existing_vhost - # debug only, /cdn-cgi/trace - #http-request return status 200 content-type "text/plain; charset=utf-8" lf-file /etc/haproxy/trace.txt if { path /cdn-cgi/trace } + # debug information at /.basedflare/cgi/trace + http-request return status 200 content-type "text/plain; charset=utf-8" lf-file /etc/haproxy/trace.txt if { path /.basedflare/cgi/trace } # acl for blocked IPs/subnets acl blocked_ip_or_subnet src,map_ip(/etc/haproxy/blocked.map) -m found http-request deny deny_status 403 if blocked_ip_or_subnet # ratelimit (and for tor, kill circuit) on POST bot-check. legitimate users shouldn't hit this. - http-request track-sc0 src table bot_check_post_throttle if { path /bot-check } { method POST } + http-request track-sc0 src table bot_check_post_throttle if { path /.basedflare/bot-check } { method POST } http-request lua.kill-tor-circuit if { sc_http_req_rate(0) gt 1 } http-request tarpit if { sc_http_req_rate(0) gt 1 } @@ -65,13 +65,13 @@ frontend http-in acl ddos_mode_enabled base,map(/etc/haproxy/ddos.map) -m bool # serve challenge page scripts directly from haproxy - http-request return file /var/www/js/argon2.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if { path /js/argon2.js } - http-request return file /var/www/js/challenge.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if { path /js/challenge.js } - http-request return file /var/www/js/worker.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if { path /js/worker.js } + http-request return file /var/www/js/argon2.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if { path /.basedflare/js/argon2.js } + http-request return file /var/www/js/challenge.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if { path /.basedflare/js/challenge.js } + http-request return file /var/www/js/worker.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if { path /.basedflare/js/worker.js } # acl for domains in maintenance mode to return maintenance page (after challenge page htp-request return rules, for the footerlogo) acl maintenance_mode hdr(host),lower,map_str(/etc/haproxy/maintenance.map) -m found - http-request return file /var/www/html/maintenance.html status 200 content-type "text/html; charset=utf-8" hdr "cache-control" "private, max-age=30" if maintenance_mode + http-request return lf-file /var/www/html/maintenance.html status 200 content-type "text/html; charset=utf-8" hdr "cache-control" "private, max-age=30" if maintenance_mode # create acl for bools updated by lua acl captcha_passed var(txn.captcha_passed) -m bool @@ -80,14 +80,14 @@ frontend http-in acl validate_pow var(txn.validate_pow) -m bool # check pow/captcha and show page if necessary - acl on_captcha_url path /bot-check - http-request use-service lua.hcaptcha-view if on_captcha_url !is_excluded + acl on_bot_check path /.basedflare/bot-check + http-request use-service lua.bot-check if on_bot_check !is_excluded # challenge decisions, checking, and redirecting to /bot-check - http-request lua.decide-checks-necessary if !is_excluded !on_captcha_url ddos_mode_enabled - http-request lua.hcaptcha-check if !is_excluded !on_captcha_url validate_captcha - http-request lua.pow-check if !is_excluded !on_captcha_url validate_pow OR !is_excluded !on_captcha_url ddos_mode_enabled_override - http-request redirect location /bot-check?%[capture.req.uri] code 302 if validate_captcha !captcha_passed !on_captcha_url ddos_mode_enabled !is_excluded OR validate_pow !pow_passed !on_captcha_url ddos_mode_enabled !is_excluded OR !pow_passed ddos_mode_enabled_override !on_captcha_url !is_excluded + http-request lua.decide-checks-necessary if !is_excluded !on_bot_check ddos_mode_enabled + http-request lua.captcha-check if !is_excluded !on_bot_check validate_captcha + http-request lua.pow-check if !is_excluded !on_bot_check validate_pow OR !is_excluded !on_bot_check ddos_mode_enabled_override + http-request redirect location /.basedflare/bot-check?%[capture.req.uri] code 302 if validate_captcha !captcha_passed !on_bot_check ddos_mode_enabled !is_excluded OR validate_pow !pow_passed !on_bot_check ddos_mode_enabled !is_excluded OR !pow_passed ddos_mode_enabled_override !on_bot_check !is_excluded # X-Cache-Status header (may be sent in some non-cache responses because NOSRV can happen for other reasons, but should always be present in responses served by cache-use) http-response set-header X-Cache-Status HIT if !{ srv_id -m found } diff --git a/haproxy/js/challenge.js b/haproxy/js/challenge.js index ef22c6b..36b6ac7 100644 --- a/haproxy/js/challenge.js +++ b/haproxy/js/challenge.js @@ -25,6 +25,7 @@ const wasmSupported = (() => { return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } catch (e) { + console.error(e); } return false; })(); @@ -37,7 +38,7 @@ function postResponse(powResponse, captchaResponse) { body['h-captcha-response'] = captchaResponse; body['g-recaptcha-response'] = captchaResponse; } - fetch('/bot-check', { + fetch('/.basedflare/bot-check', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -52,7 +53,7 @@ function postResponse(powResponse, captchaResponse) { return insertError('server responded with error.'); } finishRedirect(); - }).catch(err => { + }).catch(() => { insertError('failed to send challenge response.'); }); } @@ -74,7 +75,7 @@ const powFinished = new Promise((resolve, reject) => { const eHashes = Math.pow(16, Math.floor(diff/8)) * ((diff%8)*2); const diffString = '0'.repeat(Math.floor(diff/8)); const combined = pow; - const [userkey, challenge, signature] = combined.split("#"); + const [userkey, challenge] = combined.split("#"); const start = Date.now(); if (window.Worker) { const cpuThreads = window.navigator.hardwareConcurrency; @@ -117,7 +118,7 @@ const powFinished = new Promise((resolve, reject) => { } else { console.warn('No webworker support, running in main/UI thread!'); let i = 0; - let start = Date.now(); + const start = Date.now(); while(true) { const hash = await argon2.hash({ pass: challenge + i.toString(), diff --git a/haproxy/js/worker.js b/haproxy/js/worker.js index 6fd4617..1cb163c 100644 --- a/haproxy/js/worker.js +++ b/haproxy/js/worker.js @@ -16,7 +16,7 @@ onmessage = async function(e) { ...argonOpts, }); // This throttle seems to really help some browsers not stop the workers abruptly - i % 10 === 0 && await new Promise(res => setTimeout(res, 10)); + i % 10 === 0 && await new Promise(res => setTimeout(res, 1)); if (hash.hashHex.startsWith(diffString) && ((parseInt(hash.hashHex[diffString.length],16) & 0xff >> (((diffString.length+1)*8)-diff)) === 0)) { diff --git a/src/libs/utils.lua b/src/libs/utils.lua index d1ef1f9..b596f88 100644 --- a/src/libs/utils.lua +++ b/src/libs/utils.lua @@ -66,4 +66,3 @@ function _M.send_tor_control_port(circuit_identifier) end return _M - diff --git a/src/scripts/hcaptcha.lua b/src/scripts/bot-check.lua similarity index 73% rename from src/scripts/hcaptcha.lua rename to src/scripts/bot-check.lua index 657b94f..6f2197a 100644 --- a/src/scripts/hcaptcha.lua +++ b/src/scripts/bot-check.lua @@ -1,11 +1,19 @@ _M = {} +-- Testing only +-- require("socket") +-- require("print_r") + +-- main libs local url = require("url") local utils = require("utils") local cookie = require("cookie") local json = require("json") local sha = require("sha") local randbytes = require("randbytes") +local templates = require("templates") + +-- argon2 POW local argon2 = require("argon2") local pow_difficulty = tonumber(os.getenv("POW_DIFFICULTY") or 18) local pow_kb = tonumber(os.getenv("POW_KB") or 6000) @@ -16,10 +24,7 @@ argon2.parallelism(1) argon2.hash_len(32) argon2.variant(argon2.variants.argon2_id) --- Testing only --- require("socket") --- require("print_r") - +-- environment variables local captcha_secret = os.getenv("HCAPTCHA_SECRET") or os.getenv("RECAPTCHA_SECRET") local captcha_sitekey = os.getenv("HCAPTCHA_SITEKEY") or os.getenv("RECAPTCHA_SITEKEY") local captcha_cookie_secret = os.getenv("CAPTCHA_COOKIE_SECRET") @@ -27,6 +32,7 @@ local pow_cookie_secret = os.getenv("POW_COOKIE_SECRET") local hmac_cookie_secret = os.getenv("HMAC_COOKIE_SECRET") local ray_id = os.getenv("RAY_ID") +-- load captcha map and set hcaptcha/recaptch based off env vars local captcha_map = Map.new("/etc/haproxy/ddos.map", Map._str); local captcha_provider_domain = "" local captcha_classname = "" @@ -47,6 +53,7 @@ else captcha_backend_name = "recaptcha" end +-- setup initial server backends based on hosts.map into backends.map function _M.setup_servers() if pow_difficulty < 8 then error("POW_DIFFICULTY must be > 8. Around 16-32 is better") @@ -75,103 +82,6 @@ function _M.setup_servers() handle:close() end --- main page template -local body_template = [[ - - - - - Hold on... - - - - - - - %s - %s - %s - -
- - - -]] - -local noscript_extra_template = [[ -
- No JavaScript? -
    -
  1. -

    Run this in a linux terminal (requires argon2 package installed):

    - - echo "Q0g9IiQyIjtCPSQocHJpbnRmICcwJS4wcycgJChzZXEgMSAkNCkpO2VjaG8gIldvcmtpbmcuLi4iO0k9MDt3aGlsZSB0cnVlOyBkbyBIPSQoZWNobyAtbiAkQ0gkSSB8IGFyZ29uMiAkMSAtaWQgLXQgJDUgLWsgJDYgLXAgMSAtbCAzMiAtcik7RT0ke0g6MDokNH07W1sgJEUgPT0gJEIgXV0gJiYgZWNobyAiT3V0cHV0OiIgJiYgZWNobyAkMSMkMiMkMyMkSSAmJiBleGl0IDA7KChJKyspKTtkb25lOwo=" | base64 -d | bash -s %s %s %s %s %s %s - -
  2. Paste the script output into the box and submit: -
    - -
    -
    -
-
-]] - --- title with favicon and hostname -local site_name_section_template = [[ -

- icon - %s -

-]] - --- spinner animation for proof of work -local pow_section_template = [[ -

- Checking your browser for robots 🤖 -

-
-
-
-]] - --- message, captcha form and submit button -local captcha_section_template = [[ -

- Please solve the captcha to continue. -

-
-
- -
-]] - -- kill a tor circuit function _M.kill_tor_circuit(txn) local ip = txn.sf:src() @@ -214,7 +124,7 @@ function _M.view(applet) -- check if captcha is enabled, path+domain priority, then just domain, and 0 otherwise local captcha_enabled = false local host = applet.headers['host'][0] - local path = applet.qs; --because on /bot-check?/whatever, .qs (query string) holds the "path" + local path = applet.qs; --because on /.basedflare/bot-check?/whatever, .qs (query string) holds the "path" local captcha_map_lookup = captcha_map:lookup(host..path) or captcha_map:lookup(host) or 0 captcha_map_lookup = tonumber(captcha_map_lookup) @@ -223,18 +133,18 @@ function _M.view(applet) end -- pow at least is always enabled when reaching bot-check page - site_name_body = string.format(site_name_section_template, host) + site_name_body = string.format(templates.site_name_section, host) if captcha_enabled then - captcha_body = string.format(captcha_section_template, captcha_classname, + captcha_body = string.format(templates.captcha_section, captcha_classname, captcha_sitekey, captcha_script_src) else - pow_body = pow_section_template - noscript_extra_body = string.format(noscript_extra_template, user_key, challenge_hash, signature, + pow_body = templates.pow_section + noscript_extra_body = string.format(templates.noscript_extra, user_key, challenge_hash, signature, math.ceil(pow_difficulty/8), pow_time, pow_kb) end -- sub in the body sections - response_body = string.format(body_template, combined_challenge, + response_body = string.format(templates.body, combined_challenge, pow_difficulty, pow_time, pow_kb, site_name_body, pow_body, captcha_body, noscript_extra_body, ray_id) response_status_code = 403 diff --git a/src/scripts/register.lua b/src/scripts/register.lua index ec408ee..6ba84ef 100644 --- a/src/scripts/register.lua +++ b/src/scripts/register.lua @@ -1,10 +1,10 @@ package.path = package.path .. "./?.lua;/etc/haproxy/scripts/?.lua;/etc/haproxy/libs/?.lua" -local hcaptcha = require("hcaptcha") +local bot_check = require("bot-check") -core.register_service("hcaptcha-view", "http", hcaptcha.view) -core.register_action("hcaptcha-check", { 'http-req', }, hcaptcha.check_captcha_status) -core.register_action("pow-check", { 'http-req', }, hcaptcha.check_pow_status) -core.register_action("decide-checks-necessary", { 'http-req', }, hcaptcha.decide_checks_necessary) -core.register_action("kill-tor-circuit", { 'http-req', }, hcaptcha.kill_tor_circuit) -core.register_init(hcaptcha.setup_servers) +core.register_service("bot-check", "http", bot_check.view) +core.register_action("captcha-check", { 'http-req', }, bot_check.check_captcha_status) +core.register_action("pow-check", { 'http-req', }, bot_check.check_pow_status) +core.register_action("decide-checks-necessary", { 'http-req', }, bot_check.decide_checks_necessary) +core.register_action("kill-tor-circuit", { 'http-req', }, bot_check.kill_tor_circuit) +core.register_init(bot_check.setup_servers) diff --git a/src/scripts/templates.lua b/src/scripts/templates.lua new file mode 100644 index 0000000..e223f27 --- /dev/null +++ b/src/scripts/templates.lua @@ -0,0 +1,100 @@ +local _M = {} + +-- main page template +_M.body = [[ + + + + + Hold on... + + + + + + + %s + %s + %s + +
+ + + +]] + +_M.noscript_extra = [[ +
+ No JavaScript? +
    +
  1. +

    Run this in a linux terminal (requires argon2 package installed):

    + + echo "Q0g9IiQyIjtCPSQocHJpbnRmICcwJS4wcycgJChzZXEgMSAkNCkpO2VjaG8gIldvcmtpbmcuLi4iO0k9MDt3aGlsZSB0cnVlOyBkbyBIPSQoZWNobyAtbiAkQ0gkSSB8IGFyZ29uMiAkMSAtaWQgLXQgJDUgLWsgJDYgLXAgMSAtbCAzMiAtcik7RT0ke0g6MDokNH07W1sgJEUgPT0gJEIgXV0gJiYgZWNobyAiT3V0cHV0OiIgJiYgZWNobyAkMSMkMiMkMyMkSSAmJiBleGl0IDA7KChJKyspKTtkb25lOwo=" | base64 -d | bash -s %s %s %s %s %s %s + +
  2. Paste the script output into the box and submit: +
    + +
    +
    +
+
+]] + +-- title with favicon and hostname +_M.site_name_section = [[ +

+ icon + %s +

+]] + +-- spinner animation for proof of work +_M.pow_section = [[ +

+ Checking your browser for robots 🤖 +

+
+
+
+]] + +-- message, captcha form and submit button +_M.captcha_section = [[ +

+ Please solve the captcha to continue. +

+
+
+ +
+]] + +return _M