Merge branch 'master' into kikeflare

This commit is contained in:
Thomas Lynch
2022-10-02 06:18:26 +00:00
7 changed files with 133 additions and 59 deletions

View File

@ -39,6 +39,7 @@ NOTE: Use either HCAPTCHA_ or RECAPTHCA_, not both.
- POW_TIME - argon2 iterations - POW_TIME - argon2 iterations
- POW_KB - argon2 memory usage in KB - POW_KB - argon2 memory usage in KB
- POW_DIFFICULTY - pow "difficulty" (you should use all 3 POW_ parameters to tune the difficulty) - POW_DIFFICULTY - pow "difficulty" (you should use all 3 POW_ parameters to tune the difficulty)
- TOR_CONTROL_PORT_PASSWORD - the control port password for tor daemon
#### Run in docker (for testing/development) #### Run in docker (for testing/development)
@ -69,6 +70,22 @@ sudo luarocks install argon2
If you have problems, read the error messages before opening an issue that is simply a bad configuration. If you have problems, read the error messages before opening an issue that is simply a bad configuration.
### Tor
- Check the `bind` line comments. Switch to the one with `accept-proxy` and `option forwardfor`
- To generate a tor control port password:
```
$ tor --hash-password example
16:0175C41DDD88C5EA605582C858BC08FA29014215F233479A99FE78EDED
```
- Set `TOR_CONTROL_PORT_PASSWORD` env var to the same password (NOT the output hash)
- Add to your torrc (where xxxx is the output of `tor --hash-password`):
```
ControlPort 9051
HashedControlPassword xxxxxxxxxxxxxxxxx
```
- Don't forget to restart tor
#### Screenshots #### Screenshots
![nocaptcha](img/nocaptcha.png "no captcha mode") ![nocaptcha](img/nocaptcha.png "no captcha mode")

View File

@ -38,6 +38,7 @@ services:
- POW_TIME=2 - POW_TIME=2
- POW_KB=512 - POW_KB=512
- POW_DIFFICULTY=25 - POW_DIFFICULTY=25
- TOR_CONTROL_PORT_PASSWORD=changeme
nginx: nginx:
ports: ports:

View File

@ -10,10 +10,13 @@ global
tune.bufsize 51200 tune.bufsize 51200
defaults defaults
log global
mode http mode http
option httplog
timeout connect 5000ms timeout connect 5000ms
timeout client 50000ms timeout client 50000ms
timeout server 50000ms timeout server 50000ms
timeout tarpit 5000ms
#frontend stats-frontend #frontend stats-frontend
# bind *:2000 # bind *:2000
@ -47,6 +50,11 @@ frontend http-in
acl blocked_ip_or_subnet src,map_ip(/etc/haproxy/blocked.map) -m found 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 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 lua.kill-tor-circuit if { sc_http_req_rate(0) gt 1 }
http-request tarpit if { sc_http_req_rate(0) gt 1 }
# acl for lua check whitelisted IPs/subnets and some excluded paths # acl for lua check whitelisted IPs/subnets and some excluded paths
acl is_excluded src,map_ip(/etc/haproxy/whitelist.map) -m found acl is_excluded src,map_ip(/etc/haproxy/whitelist.map) -m found
acl is_excluded path /favicon.ico #add more acl is_excluded path /favicon.ico #add more
@ -74,9 +82,10 @@ frontend http-in
# check pow/captcha and show page if necessary # check pow/captcha and show page if necessary
acl on_captcha_url path /bot-check acl on_captcha_url path /bot-check
http-request use-service lua.hcaptcha-view if on_captcha_url !is_excluded http-request use-service lua.hcaptcha-view if on_captcha_url !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.decide-checks-necessary if !is_excluded !on_captcha_url ddos_mode_enabled
# global override enabled pow-check only, uncomment the OR to also do hcaptcha-check http-request lua.hcaptcha-check if !is_excluded !on_captcha_url validate_captcha
http-request lua.hcaptcha-check if !is_excluded !on_captcha_url validate_captcha #OR !is_excluded !on_captcha_url ddos_mode_enabled_override
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 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 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
@ -104,6 +113,9 @@ backend servers
# use server based on hostname # use server based on hostname
use-server %[req.hdr(host),lower,map(/etc/haproxy/backends.map)] if TRUE use-server %[req.hdr(host),lower,map(/etc/haproxy/backends.map)] if TRUE
backend bot_check_post_throttle
stick-table type ipv6 size 100k expire 60s store http_req_rate(60s)
backend hcaptcha backend hcaptcha
mode http mode http
server hcaptcha hcaptcha.com:443 server hcaptcha hcaptcha.com:443

View File

@ -1,7 +1,9 @@
function insertError(str) { function insertError(str) {
const ring = document.querySelector('.lds-ring'); const ring = document.querySelector('.lds-ring');
ring.insertAdjacentHTML('afterend', `<p class="red">Error: ${str}</p>`); const captcha = document.querySelector('#captcha');
ring.remove(); (ring || captcha).insertAdjacentHTML('afterend', `<p class="red">Error: ${str}</p>`);
ring && ring.remove();
captcha && captcha.remove();
} }
function finishRedirect() { function finishRedirect() {

View File

@ -3,7 +3,9 @@ local _M = {}
local sha = require("sha") local sha = require("sha")
local secret_bucket_duration = tonumber(os.getenv("BUCKET_DURATION")) local secret_bucket_duration = tonumber(os.getenv("BUCKET_DURATION"))
local challenge_includes_ip = os.getenv("CHALLENGE_INCLUDES_IP") local challenge_includes_ip = os.getenv("CHALLENGE_INCLUDES_IP")
local tor_control_port_password = os.getenv("TOR_CONTROL_PORT_PASSWORD")
-- generate the challenge hash/user hash
function _M.generate_secret(context, salt, user_key, is_applet) function _M.generate_secret(context, salt, user_key, is_applet)
-- time bucket for expiry -- time bucket for expiry
@ -30,14 +32,16 @@ function _M.generate_secret(context, salt, user_key, is_applet)
end end
-- split string by delimiter
function _M.split(inputstr, sep) function _M.split(inputstr, sep)
local t = {} local t = {}
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do for str in string.gmatch(inputstr, "([^"..sep.."]*)") do
table.insert(t, str) table.insert(t, str)
end end
return t return t
end end
-- return true if hash passes difficulty
function _M.checkdiff(hash, diff) function _M.checkdiff(hash, diff)
local i = 1 local i = 1
for j = 0, (diff-8), 8 do for j = 0, (diff-8), 8 do
@ -51,5 +55,15 @@ function _M.checkdiff(hash, diff)
return (lnm & msk) == 0 return (lnm & msk) == 0
end end
-- connect to the tor control port and instruct it to close a circuit
function _M.send_tor_control_port(circuit_identifier)
local tcp = core.tcp();
tcp:settimeout(1);
tcp:connect("127.0.0.1", 9051);
-- not buffered, so we are better off sending it all at once
tcp:send('AUTHENTICATE "' .. tor_control_port_password .. '"\nCLOSECIRCUIT ' .. circuit_identifier ..'\n')
tcp:close()
end
return _M return _M

View File

@ -133,10 +133,10 @@ local noscript_extra_template = [[
<code style="word-break: break-all;"> <code style="word-break: break-all;">
echo "Q0g9IiQyIjtCPSQocHJpbnRmICcwJS4wcycgJChzZXEgMSAkNCkpO2VjaG8gIldvcmtpbmcuLi4iO0k9MDt3aGlsZSB0cnVlOyBkbyBIPSQoZWNobyAtbiAkQ0gkSSB8IGFyZ29uMiAkMSAtaWQgLXQgJDUgLWsgJDYgLXAgMSAtbCAzMiAtcik7RT0ke0g6MDokNH07W1sgJEUgPT0gJEIgXV0gJiYgZWNobyAiT3V0cHV0OiIgJiYgZWNobyAkMSMkMiMkMyMkSSAmJiBleGl0IDA7KChJKyspKTtkb25lOwo=" | base64 -d | bash -s %s %s %s %s %s %s echo "Q0g9IiQyIjtCPSQocHJpbnRmICcwJS4wcycgJChzZXEgMSAkNCkpO2VjaG8gIldvcmtpbmcuLi4iO0k9MDt3aGlsZSB0cnVlOyBkbyBIPSQoZWNobyAtbiAkQ0gkSSB8IGFyZ29uMiAkMSAtaWQgLXQgJDUgLWsgJDYgLXAgMSAtbCAzMiAtcik7RT0ke0g6MDokNH07W1sgJEUgPT0gJEIgXV0gJiYgZWNobyAiT3V0cHV0OiIgJiYgZWNobyAkMSMkMiMkMyMkSSAmJiBleGl0IDA7KChJKyspKTtkb25lOwo=" | base64 -d | bash -s %s %s %s %s %s %s
</code> </code>
<li>Paste the output from the script into the box and submit: <li>Paste the script output into the box and submit:
<form method="POST"> <form method="post">
<textarea type="text" name="pow_response"></textarea> <textarea name="pow_response" placeholder="script output" required></textarea>
<input type="submit" value="submit" /> <div><input type="submit" value="submit" /></div>
</form> </form>
</ol> </ol>
</details> </details>
@ -145,7 +145,7 @@ local noscript_extra_template = [[
-- title with favicon and hostname -- title with favicon and hostname
local site_name_section_template = [[ local site_name_section_template = [[
<h3 class="pt"> <h3 class="pt">
<img src="/favicon.ico" width="32" height="32"> <img src="/favicon.ico" width="32" height="32" alt="favicon">
%s %s
</h3> </h3>
]] ]]
@ -171,6 +171,24 @@ local captcha_section_template = [[
</div> </div>
]] ]]
-- kill a tor circuit
function _M.kill_tor_circuit(txn)
local ip = txn.sf:src()
if ip:sub(1,19) ~= "fc00:dead:beef:4dad" then
return -- not a tor circuit id/ip. we shouldn't get here, but just in case.
end
-- split the IP, take the last 2 sections
local split_ip = utils.split(ip, ":")
local aa_bb = split_ip[5] or "0000"
local cc_dd = split_ip[6] or "0000"
aa_bb = string.rep("0", 4 - #aa_bb) .. aa_bb
cc_dd = string.rep("0", 4 - #cc_dd) .. cc_dd
-- convert the last 2 sections to a number from hex, which makes the circuit ID
local circuit_identifier = tonumber(aa_bb..cc_dd, 16)
print('Closing Tor circuit ID: '..circuit_identifier..', "IP": '..ip)
utils.send_tor_control_port(circuit_identifier)
end
function _M.view(applet) function _M.view(applet)
-- set response body and declare status code -- set response body and declare status code
@ -223,6 +241,9 @@ function _M.view(applet)
-- if request is POST, check the answer to the pow/cookie -- if request is POST, check the answer to the pow/cookie
elseif applet.method == "POST" then elseif applet.method == "POST" then
-- if they fail, set a var for use in ACLs later
local valid_submission = false
-- parsed POST body -- parsed POST body
local parsed_body = url.parseQuery(applet.receive(applet)) local parsed_body = url.parseQuery(applet.receive(applet))
@ -232,9 +253,58 @@ function _M.view(applet)
secure_cookie_flag = "" secure_cookie_flag = ""
end end
-- handle setting the POW cookie
local user_pow_response = parsed_body["pow_response"]
if user_pow_response then
-- split the response up (makes the nojs submission easier because it can be a single field)
local split_response = utils.split(user_pow_response, "#")
if #split_response == 4 then
local given_user_key = split_response[1]
local given_challenge_hash = split_response[2]
local given_signature = split_response[3]
local given_answer = split_response[4]
-- regenerate the challenge and compare it
local generated_challenge_hash = utils.generate_secret(applet, pow_cookie_secret, given_user_key, true)
if given_challenge_hash == generated_challenge_hash then
-- regenerate the signature and compare it
local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash)
if given_signature == generated_signature then
-- do the work with their given answer
local full_hash = argon2.hash_encoded(given_challenge_hash .. given_answer, given_user_key)
-- check the output is correct
local hash_output = utils.split(full_hash, '$')[6]:sub(0, 43) -- https://github.com/thibaultcha/lua-argon2/issues/37
local hex_hash_output = sha.bin_to_hex(sha.base64_to_bin(hash_output));
if utils.checkdiff(hex_hash_output, pow_difficulty) then
-- the answer was good, give them a cookie
local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_answer)
local combined_cookie = given_user_key .. "#" .. given_challenge_hash .. "#" .. given_answer .. "#" .. signature
applet:add_header(
"set-cookie",
string.format(
"z_ddos_pow=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; Domain=.%s; SameSite=Strict;%s",
combined_cookie,
applet.headers['host'][0],
secure_cookie_flag
)
)
valid_submission = true
end
end
end
end
end
-- handle setting the captcha cookie -- handle setting the captcha cookie
local user_captcha_response = parsed_body["h-captcha-response"] or parsed_body["g-recaptcha-response"] local user_captcha_response = parsed_body["h-captcha-response"] or parsed_body["g-recaptcha-response"]
if user_captcha_response then if valid_submission and user_captcha_response then -- only check captcha if POW is already correct
-- format the url for verifying the captcha response -- format the url for verifying the captcha response
local captcha_url = string.format( local captcha_url = string.format(
"https://%s%s", "https://%s%s",
@ -277,56 +347,13 @@ function _M.view(applet)
secure_cookie_flag secure_cookie_flag
) )
) )
valid_submission = valid_submission and true
end end
end end
-- handle setting the POW cookie if not valid_submission then
local user_pow_response = parsed_body["pow_response"] _M.kill_tor_circuit(applet)
if user_pow_response then
-- split the response up (makes the nojs submission easier because it can be a single field)
local split_response = utils.split(user_pow_response, "#")
if #split_response == 4 then
local given_user_key = split_response[1]
local given_challenge_hash = split_response[2]
local given_signature = split_response[3]
local given_answer = split_response[4]
-- regenerate the challenge and compare it
local generated_challenge_hash = utils.generate_secret(applet, pow_cookie_secret, given_user_key, true)
if given_challenge_hash == generated_challenge_hash then
-- regenerate the signature and compare it
local generated_signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash)
if given_signature == generated_signature then
-- do the work with their given answer
local full_hash = argon2.hash_encoded(given_challenge_hash .. given_answer, given_user_key)
-- check the output is correct
local hash_output = utils.split(full_hash, '$')[5]:sub(0, 43) -- https://github.com/thibaultcha/lua-argon2/issues/37
local hex_hash_output = sha.bin_to_hex(sha.base64_to_bin(hash_output));
if utils.checkdiff(hex_hash_output, pow_difficulty) then
-- the answer was good, give them a cookie
local signature = sha.hmac(sha.sha3_256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_answer)
local combined_cookie = given_user_key .. "#" .. given_challenge_hash .. "#" .. given_answer .. "#" .. signature
applet:add_header(
"set-cookie",
string.format(
"z_ddos_pow=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; Domain=.%s; SameSite=Strict;%s",
combined_cookie,
applet.headers['host'][0],
secure_cookie_flag
)
)
end
end
end
end
end end
-- redirect them to their desired page in applet.qs (query string) -- redirect them to their desired page in applet.qs (query string)

View File

@ -6,4 +6,5 @@ core.register_service("hcaptcha-view", "http", hcaptcha.view)
core.register_action("hcaptcha-check", { 'http-req', }, hcaptcha.check_captcha_status) 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("pow-check", { 'http-req', }, hcaptcha.check_pow_status)
core.register_action("decide-checks-necessary", { 'http-req', }, hcaptcha.decide_checks_necessary) 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_init(hcaptcha.setup_servers)