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 || "/";
|
window.location=location.search.slice(1)+location.hash || "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
function finishPow(combined, answer) {
|
function postResponse(powResponse, captchaResponse) {
|
||||||
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 body = {
|
||||||
const hasCaptchaForm = document.querySelector('form');
|
'pow_response': powResponse,
|
||||||
if (!hasCaptchaForm) {
|
};
|
||||||
finishRedirect();
|
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) => {
|
const powFinished = new Promise((resolve, reject) => {
|
||||||
window.addEventListener('DOMContentLoaded', (event) => {
|
window.addEventListener('DOMContentLoaded', (event) => {
|
||||||
const combined = document.querySelector('[data-pow]').dataset.pow;
|
const combined = document.querySelector('[data-pow]').dataset.pow;
|
||||||
const [_userkey, challenge, _signature] = combined.split("#");
|
const [userkey, challenge, signature] = combined.split("#");
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
if (window.Worker && crypto.subtle) {
|
if (window.Worker && crypto.subtle) {
|
||||||
const threads = Math.min(2,Math.ceil(window.navigator.hardwareConcurrency/2));
|
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');
|
console.log('Worker', workerId, 'returned answer', answer, 'in', Date.now()-start+'ms');
|
||||||
const dummyTime = 5000 - (Date.now()-start);
|
const dummyTime = 5000 - (Date.now()-start);
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
finishPow(combined, answer);
|
resolve(`${combined}#${answer}`);
|
||||||
resolve();
|
|
||||||
}, dummyTime);
|
}, dummyTime);
|
||||||
}
|
}
|
||||||
const workers = [];
|
const workers = [];
|
||||||
@ -53,31 +64,24 @@ const powFinished = new Promise((resolve, reject) => {
|
|||||||
}
|
}
|
||||||
const dummyTime = 5000 - (Date.now()-start);
|
const dummyTime = 5000 - (Date.now()-start);
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
finishPow(combined, i);
|
resolve(`${combined}#${i}`);
|
||||||
resolve();
|
|
||||||
}, dummyTime);
|
}, 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]');
|
const captchaElem = document.querySelector('[data-sitekey]');
|
||||||
captchaElem.insertAdjacentHTML('afterend', `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`);
|
captchaElem.insertAdjacentHTML('afterend', `<div class="lds-ring"><div></div><div></div><div></div><div></div></div>`);
|
||||||
captchaElem.remove();
|
captchaElem.remove();
|
||||||
powFinished.then(() => {
|
powFinished.then((powResponse) => {
|
||||||
fetch('/bot-check', {
|
postResponse(powResponse, captchaResponse);
|
||||||
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();
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,8 +116,11 @@ local noscript_extra_template = [[
|
|||||||
<code style="word-break: break-all;">
|
<code style="word-break: break-all;">
|
||||||
echo "Q0g9IiQyIjtCPSIwMDQxIjtJPTA7RElGRj0kKCgxNiMke0NIOjA6MX0gKiAyKSk7d2hpbGUgdHJ1ZTsgZG8gSD0kKGVjaG8gLW4gJENIJEkgfCBzaGEyNTZzdW0pO0U9JHtIOiRESUZGOjR9O1tbICRFID09ICRCIF1dICYmIGVjaG8gJDEjJDIjJDMjJEkgJiYgZXhpdCAwOygoSSsrKSk7ZG9uZTs=" | base64 -d | bash -s %s %s %s
|
echo "Q0g9IiQyIjtCPSIwMDQxIjtJPTA7RElGRj0kKCgxNiMke0NIOjA6MX0gKiAyKSk7d2hpbGUgdHJ1ZTsgZG8gSD0kKGVjaG8gLW4gJENIJEkgfCBzaGEyNTZzdW0pO0U9JHtIOiRESUZGOjR9O1tbICRFID09ICRCIF1dICYmIGVjaG8gJDEjJDIjJDMjJEkgJiYgZXhpdCAwOygoSSsrKSk7ZG9uZTs=" | base64 -d | bash -s %s %s %s
|
||||||
</code>
|
</code>
|
||||||
<li>Set a cookie named <code>z_ddos_pow</code> with the value as the script output, and path <code>/</code>.
|
<li>Paste the output from the script into the box and submit:
|
||||||
<li>Remove <code>/bot-check?</code> from the url, and reload the page.
|
<form method="POST">
|
||||||
|
<textarea type="text" name="pow_response"></textarea>
|
||||||
|
<input type="submit" value="submit" />
|
||||||
|
</form>
|
||||||
</ol>
|
</ol>
|
||||||
</details>
|
</details>
|
||||||
]]
|
]]
|
||||||
@ -145,25 +148,26 @@ local captcha_section_template = [[
|
|||||||
<h3>
|
<h3>
|
||||||
Please solve the captcha to continue.
|
Please solve the captcha to continue.
|
||||||
</h3>
|
</h3>
|
||||||
<form class="jsonly" method="POST">
|
<div id="captcha" class="jsonly">
|
||||||
<div class="%s" data-sitekey="%s" data-callback="onCaptchaSubmit"></div>
|
<div class="%s" data-sitekey="%s" data-callback="onCaptchaSubmit"></div>
|
||||||
<script src="%s" async defer></script>
|
<script src="%s" async defer></script>
|
||||||
</form>
|
</div>
|
||||||
]]
|
]]
|
||||||
|
|
||||||
function _M.view(applet)
|
function _M.view(applet)
|
||||||
|
|
||||||
|
-- set response body and declare status code
|
||||||
local response_body = ""
|
local response_body = ""
|
||||||
local response_status_code
|
local response_status_code
|
||||||
|
|
||||||
|
-- if request is GET, serve the challenge page
|
||||||
if applet.method == "GET" then
|
if applet.method == "GET" then
|
||||||
|
|
||||||
-- get the user_key#challenge#sig
|
-- get the user_key#challenge#sig
|
||||||
local user_key = sha.bin_to_hex(randbytes(16))
|
local user_key = sha.bin_to_hex(randbytes(16))
|
||||||
local challenge_hash = utils.generate_secret(applet, pow_cookie_secret, user_key, true)
|
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 signature = sha.hmac(sha.sha256, hmac_cookie_secret, user_key .. challenge_hash)
|
||||||
local combined_challenge = user_key .. "#" .. challenge_hash .. "#" .. signature
|
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
|
-- define body sections
|
||||||
local site_name_body = ""
|
local site_name_body = ""
|
||||||
@ -194,19 +198,34 @@ function _M.view(applet)
|
|||||||
-- sub in the body sections
|
-- 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_body = string.format(body_template, combined_challenge, site_name_body, pow_body, captcha_body, noscript_extra_body, ray_id)
|
||||||
response_status_code = 403
|
response_status_code = 403
|
||||||
|
|
||||||
|
-- if request is POST, check the answer to the pow/cookie
|
||||||
elseif applet.method == "POST" then
|
elseif applet.method == "POST" then
|
||||||
|
|
||||||
|
-- parsed POST body
|
||||||
local parsed_body = url.parseQuery(applet.receive(applet))
|
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"]
|
local user_captcha_response = parsed_body["h-captcha-response"] or parsed_body["g-recaptcha-response"]
|
||||||
if user_captcha_response then
|
if user_captcha_response then
|
||||||
|
-- format the url for verifying the captcha response
|
||||||
local captcha_url = string.format(
|
local captcha_url = string.format(
|
||||||
"https://%s%s",
|
"https://%s%s",
|
||||||
core.backends[captcha_backend_name].servers[captcha_backend_name]:get_addr(),
|
core.backends[captcha_backend_name].servers[captcha_backend_name]:get_addr(),
|
||||||
captcha_siteverify_path
|
captcha_siteverify_path
|
||||||
)
|
)
|
||||||
|
-- construct the captcha body to send to the captcha url
|
||||||
local captcha_body = url.buildQuery({
|
local captcha_body = url.buildQuery({
|
||||||
secret=captcha_secret,
|
secret=captcha_secret,
|
||||||
response=user_captcha_response
|
response=user_captcha_response
|
||||||
})
|
})
|
||||||
|
-- instantiate an http client and make the request
|
||||||
local httpclient = core.httpclient()
|
local httpclient = core.httpclient()
|
||||||
local res = httpclient:post{
|
local res = httpclient:post{
|
||||||
url=captcha_url,
|
url=captcha_url,
|
||||||
@ -216,42 +235,90 @@ function _M.view(applet)
|
|||||||
[ "content-type" ] = { "application/x-www-form-urlencoded" }
|
[ "content-type" ] = { "application/x-www-form-urlencoded" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
-- try parsing the response as json
|
||||||
local status, api_response = pcall(json.decode, res.body)
|
local status, api_response = pcall(json.decode, res.body)
|
||||||
if not status then
|
if not status then
|
||||||
api_response = {}
|
api_response = {}
|
||||||
end
|
end
|
||||||
|
-- the response was good i.e the captcha provider says they passed, give them a cookie
|
||||||
if api_response.success == true then
|
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_key = sha.bin_to_hex(randbytes(16))
|
||||||
local user_hash = utils.generate_secret(applet, captcha_cookie_secret, user_key, true)
|
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 signature = sha.hmac(sha.sha256, hmac_cookie_secret, user_key .. user_hash)
|
||||||
local combined_cookie = user_key .. "#" .. user_hash .. "#" .. signature
|
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(
|
applet:add_header(
|
||||||
"set-cookie",
|
"set-cookie",
|
||||||
string.format(
|
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,
|
combined_cookie,
|
||||||
secure_cookie_flag
|
secure_cookie_flag
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
end
|
end
|
||||||
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
|
response_status_code = 302
|
||||||
applet:add_header("location", applet.qs)
|
applet:add_header("location", applet.qs)
|
||||||
|
|
||||||
|
-- else if its another http method, just 403 them
|
||||||
else
|
else
|
||||||
-- other methods
|
|
||||||
response_status_code = 403
|
response_status_code = 403
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- finish sending the response
|
||||||
applet:set_status(response_status_code)
|
applet:set_status(response_status_code)
|
||||||
applet:add_header("content-type", "text/html; charset=utf-8")
|
applet:add_header("content-type", "text/html; charset=utf-8")
|
||||||
applet:add_header("content-length", string.len(response_body))
|
applet:add_header("content-length", string.len(response_body))
|
||||||
applet:start_response()
|
applet:start_response()
|
||||||
applet:send(response_body)
|
applet:send(response_body)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- check if captcha is enabled, path+domain priority, then just domain, and 0 otherwise
|
-- 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)
|
function _M.check_captcha_status(txn)
|
||||||
local parsed_request_cookies = cookie.get_cookie_table(txn.sf:hdr("Cookie"))
|
local parsed_request_cookies = cookie.get_cookie_table(txn.sf:hdr("Cookie"))
|
||||||
local received_captcha_cookie = parsed_request_cookies["z_ddos_captcha"] or ""
|
local received_captcha_cookie = parsed_request_cookies["z_ddos_captcha"] or ""
|
||||||
|
-- split the cookie up
|
||||||
local split_cookie = utils.split(received_captcha_cookie, "#")
|
local split_cookie = utils.split(received_captcha_cookie, "#")
|
||||||
if #split_cookie ~= 3 then
|
if #split_cookie ~= 3 then
|
||||||
return
|
return
|
||||||
@ -303,22 +371,16 @@ function _M.check_pow_status(txn)
|
|||||||
end
|
end
|
||||||
local given_user_key = split_cookie[1]
|
local given_user_key = split_cookie[1]
|
||||||
local given_challenge_hash = split_cookie[2]
|
local given_challenge_hash = split_cookie[2]
|
||||||
local given_signature = split_cookie[3]
|
local given_answer = split_cookie[3]
|
||||||
local given_nonce = split_cookie[4]
|
local given_signature = split_cookie[4]
|
||||||
-- regenerate the challenge and compare it
|
-- regenerate the challenge and compare it
|
||||||
local generated_challenge_hash = utils.generate_secret(txn, pow_cookie_secret, given_user_key, false)
|
local generated_challenge_hash = utils.generate_secret(txn, pow_cookie_secret, given_user_key, false)
|
||||||
if given_challenge_hash ~= generated_challenge_hash then
|
if given_challenge_hash ~= generated_challenge_hash then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- regenerate the signature and compare it
|
-- regenerate the signature and compare it
|
||||||
local generated_signature = sha.hmac(sha.sha256, hmac_cookie_secret, given_user_key .. given_challenge_hash)
|
local generated_signature = sha.hmac(sha.sha256, hmac_cookie_secret, given_user_key .. given_challenge_hash .. given_answer)
|
||||||
if given_signature ~= generated_signature then
|
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 :^)
|
|
||||||
return txn:set_var("txn.pow_passed", true)
|
return txn:set_var("txn.pow_passed", true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Reference in New Issue
Block a user