mirror of
https://gitgud.io/fatchan/haproxy-protection.git
synced 2025-05-09 02:05:37 +00:00
Move POW proof checking to POST and sign a cookie there, kinda like captcha flow, so we can do a more intensive one without it happening on every request. We just check the hmac now.
Still TODO actually converting it to argon, but should be straightforward from this point. Another advantage of making POW check use POST is a better noscript experience. We now provide a box and "submit" button, so they don't have to mess with setting a cookie.
This commit is contained in:
@ -2,18 +2,30 @@ function finishRedirect() {
|
||||
window.location=location.search.slice(1)+location.hash || "/";
|
||||
}
|
||||
|
||||
function finishPow(combined, answer) {
|
||||
document.cookie='z_ddos_pow='+combined+'#'+answer+';expires=Thu, 31-Dec-37 23:55:55 GMT; path=/; SameSite=Strict; '+(location.protocol==='https:'?'Secure=true; ':'');
|
||||
const hasCaptchaForm = document.querySelector('form');
|
||||
if (!hasCaptchaForm) {
|
||||
finishRedirect();
|
||||
function postResponse(powResponse, captchaResponse) {
|
||||
const body = {
|
||||
'pow_response': powResponse,
|
||||
};
|
||||
if (captchaResponse) {
|
||||
body['h-captcha-response'] = captchaResponse;
|
||||
body['g-recaptcha-response'] = captchaResponse;
|
||||
}
|
||||
fetch('/bot-check', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams(body),
|
||||
redirect: 'manual',
|
||||
}).then(res => {
|
||||
finishRedirect();
|
||||
})
|
||||
}
|
||||
|
||||
const powFinished = new Promise((resolve, reject) => {
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
const combined = document.querySelector('[data-pow]').dataset.pow;
|
||||
const [_userkey, challenge, _signature] = combined.split("#");
|
||||
const [userkey, challenge, signature] = combined.split("#");
|
||||
const start = Date.now();
|
||||
if (window.Worker && crypto.subtle) {
|
||||
const threads = Math.min(2,Math.ceil(window.navigator.hardwareConcurrency/2));
|
||||
@ -26,8 +38,7 @@ const powFinished = new Promise((resolve, reject) => {
|
||||
console.log('Worker', workerId, 'returned answer', answer, 'in', Date.now()-start+'ms');
|
||||
const dummyTime = 5000 - (Date.now()-start);
|
||||
window.setTimeout(() => {
|
||||
finishPow(combined, answer);
|
||||
resolve();
|
||||
resolve(`${combined}#${answer}`);
|
||||
}, dummyTime);
|
||||
}
|
||||
const workers = [];
|
||||
@ -53,31 +64,24 @@ const powFinished = new Promise((resolve, reject) => {
|
||||
}
|
||||
const dummyTime = 5000 - (Date.now()-start);
|
||||
window.setTimeout(() => {
|
||||
finishPow(combined, i);
|
||||
resolve();
|
||||
resolve(`${combined}#${i}`);
|
||||
}, dummyTime);
|
||||
}
|
||||
});
|
||||
}).then((powResponse) => {
|
||||
const hasCaptchaForm = document.getElementById('captcha');
|
||||
if (!hasCaptchaForm) {
|
||||
postResponse(powResponse);
|
||||
}
|
||||
return powResponse;
|
||||
});
|
||||
|
||||
function onCaptchaSubmit(callback) {
|
||||
function onCaptchaSubmit(captchaResponse) {
|
||||
const captchaElem = document.querySelector('[data-sitekey]');
|
||||
captchaElem.insertAdjacentHTML('afterend', `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`);
|
||||
captchaElem.remove();
|
||||
powFinished.then(() => {
|
||||
fetch('/bot-check', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
'h-captcha-response': callback,
|
||||
'g-recaptcha-response': callback,
|
||||
}),
|
||||
redirect: 'manual',
|
||||
}).then(res => {
|
||||
finishRedirect();
|
||||
})
|
||||
powFinished.then((powResponse) => {
|
||||
postResponse(powResponse, captchaResponse);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -116,8 +116,11 @@ local noscript_extra_template = [[
|
||||
<code style="word-break: break-all;">
|
||||
echo "Q0g9IiQyIjtCPSIwMDQxIjtJPTA7RElGRj0kKCgxNiMke0NIOjA6MX0gKiAyKSk7d2hpbGUgdHJ1ZTsgZG8gSD0kKGVjaG8gLW4gJENIJEkgfCBzaGEyNTZzdW0pO0U9JHtIOiRESUZGOjR9O1tbICRFID09ICRCIF1dICYmIGVjaG8gJDEjJDIjJDMjJEkgJiYgZXhpdCAwOygoSSsrKSk7ZG9uZTs=" | base64 -d | bash -s %s %s %s
|
||||
</code>
|
||||
<li>Set a cookie named <code>z_ddos_pow</code> with the value as the script output, and path <code>/</code>.
|
||||
<li>Remove <code>/bot-check?</code> from the url, and reload the page.
|
||||
<li>Paste the output from the script into the box and submit:
|
||||
<form method="POST">
|
||||
<textarea type="text" name="pow_response"></textarea>
|
||||
<input type="submit" value="submit" />
|
||||
</form>
|
||||
</ol>
|
||||
</details>
|
||||
]]
|
||||
@ -145,25 +148,26 @@ local captcha_section_template = [[
|
||||
<h3>
|
||||
Please solve the captcha to continue.
|
||||
</h3>
|
||||
<form class="jsonly" method="POST">
|
||||
<div id="captcha" class="jsonly">
|
||||
<div class="%s" data-sitekey="%s" data-callback="onCaptchaSubmit"></div>
|
||||
<script src="%s" async defer></script>
|
||||
</form>
|
||||
</div>
|
||||
]]
|
||||
|
||||
function _M.view(applet)
|
||||
|
||||
-- set response body and declare status code
|
||||
local response_body = ""
|
||||
local response_status_code
|
||||
|
||||
-- if request is GET, serve the challenge page
|
||||
if applet.method == "GET" then
|
||||
|
||||
-- get the user_key#challenge#sig
|
||||
local user_key = sha.bin_to_hex(randbytes(16))
|
||||
local challenge_hash = utils.generate_secret(applet, pow_cookie_secret, user_key, true)
|
||||
local signature = sha.hmac(sha.sha256, hmac_cookie_secret, user_key .. challenge_hash)
|
||||
local combined_challenge = user_key .. "#" .. challenge_hash .. "#" .. signature
|
||||
-- print_r(user_key)
|
||||
-- print_r(challenge_hash)
|
||||
-- print_r(signature)
|
||||
-- print_r(combined_challenge)
|
||||
|
||||
-- define body sections
|
||||
local site_name_body = ""
|
||||
@ -194,19 +198,34 @@ function _M.view(applet)
|
||||
-- sub in the body sections
|
||||
response_body = string.format(body_template, combined_challenge, site_name_body, pow_body, captcha_body, noscript_extra_body, ray_id)
|
||||
response_status_code = 403
|
||||
|
||||
-- if request is POST, check the answer to the pow/cookie
|
||||
elseif applet.method == "POST" then
|
||||
|
||||
-- parsed POST body
|
||||
local parsed_body = url.parseQuery(applet.receive(applet))
|
||||
|
||||
-- whether to set cookies sent as secure or not
|
||||
local secure_cookie_flag = " Secure=true;"
|
||||
if applet.sf:ssl_fc() == "0" then
|
||||
secure_cookie_flag = ""
|
||||
end
|
||||
|
||||
-- handle setting the captcha cookie
|
||||
local user_captcha_response = parsed_body["h-captcha-response"] or parsed_body["g-recaptcha-response"]
|
||||
if user_captcha_response then
|
||||
-- format the url for verifying the captcha response
|
||||
local captcha_url = string.format(
|
||||
"https://%s%s",
|
||||
core.backends[captcha_backend_name].servers[captcha_backend_name]:get_addr(),
|
||||
captcha_siteverify_path
|
||||
)
|
||||
-- construct the captcha body to send to the captcha url
|
||||
local captcha_body = url.buildQuery({
|
||||
secret=captcha_secret,
|
||||
response=user_captcha_response
|
||||
})
|
||||
-- instantiate an http client and make the request
|
||||
local httpclient = core.httpclient()
|
||||
local res = httpclient:post{
|
||||
url=captcha_url,
|
||||
@ -216,42 +235,90 @@ function _M.view(applet)
|
||||
[ "content-type" ] = { "application/x-www-form-urlencoded" }
|
||||
}
|
||||
}
|
||||
-- try parsing the response as json
|
||||
local status, api_response = pcall(json.decode, res.body)
|
||||
if not status then
|
||||
api_response = {}
|
||||
end
|
||||
-- the response was good i.e the captcha provider says they passed, give them a cookie
|
||||
if api_response.success == true then
|
||||
-- for captcha, they dont need to solve a POW but we check the user_hash and sig later
|
||||
|
||||
local user_key = sha.bin_to_hex(randbytes(16))
|
||||
local user_hash = utils.generate_secret(applet, captcha_cookie_secret, user_key, true)
|
||||
local signature = sha.hmac(sha.sha256, hmac_cookie_secret, user_key .. user_hash)
|
||||
local combined_cookie = user_key .. "#" .. user_hash .. "#" .. signature
|
||||
local secure_cookie_flag = " Secure=true;"
|
||||
if applet.sf:ssl_fc() == "0" then
|
||||
secure_cookie_flag = ""
|
||||
end
|
||||
applet:add_header(
|
||||
"set-cookie",
|
||||
string.format(
|
||||
"z_ddos_captcha=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; SameSite=Strict;",
|
||||
"z_ddos_captcha=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; SameSite=Strict;%s",
|
||||
combined_cookie,
|
||||
secure_cookie_flag
|
||||
)
|
||||
)
|
||||
|
||||
end
|
||||
end
|
||||
-- if failed captcha, will just get sent back here so 302 is fine
|
||||
|
||||
-- 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.sha256, hmac_cookie_secret, given_user_key .. given_challenge_hash)
|
||||
if given_signature == generated_signature then
|
||||
|
||||
-- do the work with their given answer
|
||||
local completed_work = sha.sha256(generated_challenge_hash .. given_answer) -- (TODO: replace this bit with argon2)
|
||||
|
||||
-- check the output is correct
|
||||
local challenge_offset = tonumber(generated_challenge_hash:sub(1,1),16) * 2
|
||||
if completed_work:sub(challenge_offset+1, challenge_offset+4) == '0041' then
|
||||
|
||||
-- the answer was good, give them a cookie
|
||||
local signature = sha.hmac(sha.sha256, 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=/; SameSite=Strict;%s",
|
||||
combined_cookie,
|
||||
secure_cookie_flag
|
||||
)
|
||||
)
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- redirect them to their desired page in applet.qs (query string)
|
||||
-- if they didn't get the appropriate cookies they will be sent back to the challenge page
|
||||
response_status_code = 302
|
||||
applet:add_header("location", applet.qs)
|
||||
|
||||
-- else if its another http method, just 403 them
|
||||
else
|
||||
-- other methods
|
||||
response_status_code = 403
|
||||
end
|
||||
|
||||
-- finish sending the response
|
||||
applet:set_status(response_status_code)
|
||||
applet:add_header("content-type", "text/html; charset=utf-8")
|
||||
applet:add_header("content-length", string.len(response_body))
|
||||
applet:start_response()
|
||||
applet:send(response_body)
|
||||
|
||||
end
|
||||
|
||||
-- check if captcha is enabled, path+domain priority, then just domain, and 0 otherwise
|
||||
@ -273,6 +340,7 @@ end
|
||||
function _M.check_captcha_status(txn)
|
||||
local parsed_request_cookies = cookie.get_cookie_table(txn.sf:hdr("Cookie"))
|
||||
local received_captcha_cookie = parsed_request_cookies["z_ddos_captcha"] or ""
|
||||
-- split the cookie up
|
||||
local split_cookie = utils.split(received_captcha_cookie, "#")
|
||||
if #split_cookie ~= 3 then
|
||||
return
|
||||
@ -303,22 +371,16 @@ function _M.check_pow_status(txn)
|
||||
end
|
||||
local given_user_key = split_cookie[1]
|
||||
local given_challenge_hash = split_cookie[2]
|
||||
local given_signature = split_cookie[3]
|
||||
local given_nonce = split_cookie[4]
|
||||
local given_answer = split_cookie[3]
|
||||
local given_signature = split_cookie[4]
|
||||
-- regenerate the challenge and compare it
|
||||
local generated_challenge_hash = utils.generate_secret(txn, pow_cookie_secret, given_user_key, false)
|
||||
if given_challenge_hash ~= generated_challenge_hash then
|
||||
return
|
||||
end
|
||||
-- regenerate the signature and compare it
|
||||
local generated_signature = sha.hmac(sha.sha256, hmac_cookie_secret, given_user_key .. given_challenge_hash)
|
||||
if given_signature ~= generated_signature then
|
||||
return
|
||||
end
|
||||
-- check the work
|
||||
local completed_work = sha.sha256(generated_challenge_hash .. given_nonce)
|
||||
local challenge_offset = tonumber(generated_challenge_hash:sub(1,1),16) * 2
|
||||
if completed_work:sub(challenge_offset+1, challenge_offset+4) == '0041' then -- i dont know lua properly :^)
|
||||
local generated_signature = sha.hmac(sha.sha256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_answer)
|
||||
if given_signature == generated_signature then
|
||||
return txn:set_var("txn.pow_passed", true)
|
||||
end
|
||||
end
|
||||
|
Reference in New Issue
Block a user