diff --git a/haproxy/haproxy.cfg b/haproxy/haproxy.cfg index 7f32697..4b92d59 100644 --- a/haproxy/haproxy.cfg +++ b/haproxy/haproxy.cfg @@ -70,9 +70,10 @@ frontend http-in acl ddos_mode_enabled base,map(/etc/haproxy/map/ddos.map) -m bool # serve challenge page scripts directly from haproxy - http-request return file /etc/haproxy/js/argon2.min.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if { path /.basedflare/js/argon2.min.js } - http-request return file /etc/haproxy/js/challenge.min.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if { path /.basedflare/js/challenge.min.js } - http-request return file /etc/haproxy/js/worker.min.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=300" if { path /.basedflare/js/worker.min.js } + http-request return file /etc/haproxy/js/auto.min.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=3600" if { path /.basedflare/js/auto.min.js } + http-request return file /etc/haproxy/js/argon2.min.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=3600" if { path /.basedflare/js/argon2.min.js } + http-request return file /etc/haproxy/js/challenge.min.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=3600" if { path /.basedflare/js/challenge.min.js } + http-request return file /etc/haproxy/js/worker.min.js status 200 content-type "application/javascript; charset=utf-8" hdr "cache-control" "public, max-age=3600" if { path /.basedflare/js/worker.min.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/map/maintenance.map) -m found diff --git a/src/js/auto.js b/src/js/auto.js new file mode 100644 index 0000000..a4adf90 --- /dev/null +++ b/src/js/auto.js @@ -0,0 +1,98 @@ +(() => { + const doBotCheck = async () => { + try { + const json = await fetch("/.basedflare/bot-check", { headers: { "accept": "application/json" }}) + .then(res => res.json()); + if (json && json.ch) { + if (json.ca) { + // TODO: captcha popup + } else { + const [ userkey, challenge, _expiry, _signature ] = json.ch.split("#"); + const [ mode, diff, argon_time, argon_kb ] = json.pow.split("#"); + if (mode === "argon2") { + if (!argon2) { + await new Promise((res) => { + const script = document.createElement("script"); + script.onload = () => res(); + script.src = something; + document.head.appendChild(script); + }) + } + } + console.log(json) + const diffString = "0".repeat(diff); + const cpuThreads = window.navigator.hardwareConcurrency; + const isTor = location.hostname.endsWith(".onion"); + const workerThreads = (isTor || cpuThreads === 2) ? cpuThreads : Math.max(Math.ceil(cpuThreads / 2), cpuThreads - 1); + const workers = []; + let finished = false; + const messageHandler = (e) => { + if (e.data.length === 1) { + return console.log(e.data[0]); + } + if (finished) return; + finished = true; + workers.forEach((w) => w.terminate()); + const [_workerId, answer] = e.data; + fetch("/.basedflare/bot-check", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams({ + "pow_response": `${json.ch}#${answer}`, + }), + redirect: "manual", + }).then((res) => { + if (res.status >= 400) { + console.error("basedflare post status >= 400", res); + } + }).catch((e) => { + console.error(e) + }); + }; + for (let i = 0; i < workerThreads; i++) { + const powWorker = new Worker("/.basedflare/js/worker.min.js"); + powWorker.onmessage = messageHandler; + workers.push(powWorker); + powWorker.postMessage([ + userkey, + challenge, + diff, + diffString, + { + time: argon_time, + mem: argon_kb, + hashLen: 32, + parallelism: 1, + type: argon2 ? argon2.ArgonType.Argon2id : null, + mode: mode, + }, + i, + workerThreads, + ]); + } + } + } + } catch(e) { + console.error(e); + } + }; + const cookieMinLife = 600; + const checkCookie = () => { + const powCookie = document.cookie + .split("; ") + .find((row) => row.startsWith("_basedflare_pow=")); + if (powCookie) { + powCookieValue = powCookie.split("=")[1]; + const expiry = powCookieValue.split("#")[2]; + const remainingSecs = ((expiry*1000) - Date.now()) / 1000; + console.log('Basedflare cookie check, valid for', remainingSecs, 'seconds'); + if (remainingSecs < cookieMinLife) { + return doBotCheck(); + } + setTimeout(checkCookie, Math.floor(((remainingSecs-cookieMinLife-(Math.random()*300))*1000))); + } + } + checkCookie(); +})(); diff --git a/src/js/auto.min.js b/src/js/auto.min.js new file mode 100644 index 0000000..c0ec107 --- /dev/null +++ b/src/js/auto.min.js @@ -0,0 +1 @@ +(()=>{const doBotCheck=async()=>{try{const json=await fetch("/.basedflare/bot-check",{headers:{"accept":"application/json"}}).then(res=>res.json());if(json&&json.ch){if(json.ca){}else{const[userkey,challenge,_expiry,_signature]=json.ch.split("#");const[mode,diff,argon_time,argon_kb]=json.pow.split("#");if(mode==="argon2"){if(!argon2){await new Promise((res)=>{const script=document.createElement("script");script.onload=()=>res();script.src=something;document.head.appendChild(script)})}}console.log(json);const diffString="0".repeat(diff);const cpuThreads=window.navigator.hardwareConcurrency;const isTor=location.hostname.endsWith(".onion");const workerThreads=(isTor||cpuThreads===2)?cpuThreads:Math.max(Math.ceil(cpuThreads/2),cpuThreads-1);const workers=[];let finished=false;const messageHandler=(e)=>{if(e.data.length===1){return console.log(e.data[0])}if(finished){return}finished=true;workers.forEach((w)=>w.terminate());const[_workerId,answer]=e.data;fetch("/.basedflare/bot-check",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({"pow_response":`${json.ch }#${ answer }`}),redirect:"manual"}).then((res)=>{if(res.status>=400){console.error("basedflare post status >= 400",res)}}).catch((e)=>{console.error(e)})};for(let i=0;i{const powCookie=document.cookie.split("; ").find((row)=>row.startsWith("_basedflare_pow="));if(powCookie){powCookieValue=powCookie.split("=")[1];const expiry=powCookieValue.split("#")[2];const remainingSecs=((expiry*1000)-Date.now())/1000;console.log('Basedflare cookie check, valid for',remainingSecs,'seconds');if(remainingSecsError: ${str}

`, @@ -51,6 +50,25 @@ const wasmSupported = (() => { return false; })(); +// const registerServiceWorker = async () => { + // if ("serviceWorker" in navigator) { + // try { + // const registration = await navigator.serviceWorker.register("/.basedflare/js/serviceworker.min.js", { + // scope: "/", + // }); + // if (registration.installing) { + // console.log("BasedFlare service worker installing"); + // } else if (registration.waiting) { + // console.log("BasedFlare service worker installed"); + // } else if (registration.active) { + // console.log("BasedFlare service worker active"); + // } + // } catch (error) { + // console.error(`BasedFlare worker registration failed: ${error}`); + // } + // } +// }; + function postResponse(powResponse, captchaResponse) { const body = { "pow_response": powResponse, @@ -107,6 +125,7 @@ const powFinished = new Promise((resolve) => { }; window.addEventListener("DOMContentLoaded", async () => { + // registerServiceWorker(); const { time, kb, diff --git a/src/js/challenge.min.js b/src/js/challenge.min.js index 56e7a9c..02549c5 100644 --- a/src/js/challenge.min.js +++ b/src/js/challenge.min.js @@ -1 +1 @@ -function updateElem(selector,text,color){const updateElem=document.querySelector(selector);if(updateElem){updateElem.innerText=text;if(color){updateElem.style.color=color}}}function insertError(str){const loader=document.querySelector("#loader");const captcha=document.querySelector("#captcha");console.log(loader,captcha);(captcha||loader).insertAdjacentHTML("afterend",`

Error: ${ str }

`,);loader&&loader.remove();captcha&&captcha.remove();updateElem(".powstatus","")}function finishRedirect(){window.location=location.search.slice(1)+location.hash||"/"}function makeLoaderGreen(){const dots=document.querySelectorAll(".b");if(dots&&dots.length>0){dots.forEach((dot)=>dot.classList.add("green"))}}const wasmSupported=(()=>{try{if(typeof WebAssembly==="object"&&typeof WebAssembly.instantiate==="function"){const module=new WebAssembly.Module(Uint8Array.of(0x0,0x61,0x73,0x6d,0x01,0x00,0x00,0x00),);if(module instanceof WebAssembly.Module){return new WebAssembly.Instance(module)instanceof WebAssembly.Instance}}}catch(e){console.error(e)}return false})();function postResponse(powResponse,captchaResponse){const body={"pow_response":powResponse};if(captchaResponse){body["h-captcha-response"]=captchaResponse;body["g-recaptcha-response"]=captchaResponse}fetch("/.basedflare/bot-check",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams(body),redirect:"manual"}).then((res)=>{const s=res.status;if(s>=400&&s<500){return insertError("Server rejected your submission.")}else if(s>=500){return insertError("Server encountered an error.")}window.localStorage.setItem("basedflare-redirect",Math.random());finishRedirect()}).catch(()=>{insertError("Failed to send request to server.")})}const powFinished=new Promise((resolve)=>{let start=Date.now();const workers=[];let finished=false;const stopPow=()=>{finished=true;const hasCaptcha=document.getElementById("captcha");if(hasCaptcha){updateElem(".powstatus","Waiting for captcha.","#31cc31")}else{updateElem(".powstatus","Submitting...","#31cc31");makeLoaderGreen()}workers.forEach((w)=>w.terminate())};const submitPow=(answer)=>{window.localStorage.setItem("basedflare-pow-response",answer);stopPow();const dummyTime=3500-(Date.now()-start);window.setTimeout(()=>{resolve({answer})},dummyTime)};window.addEventListener("DOMContentLoaded",async()=>{const{time,kb,pow,diff,mode}=document.querySelector("[data-pow]").dataset;window.addEventListener("storage",(event)=>{if(event.key==="basedflare-pow-response"&&!finished){console.log("Got answer",event.newValue,"from storage event");stopPow();resolve({answer:event.newValue,localStorage:true})}else if(event.key==="basedflare-redirect"){console.log("Redirecting, solved in another tab");finishRedirect()}});if(!wasmSupported){return insertError("Browser does not support WebAssembly.")}const powOpts={time:time,mem:kb,hashLen:32,parallelism:1,type:argon2?argon2.ArgonType.Argon2id:null,mode:mode};console.log("Got pow",pow,"with difficulty",diff);const eHashes=Math.pow(16,Math.floor(diff/8))*(((diff%8)*2)||1);const diffString="0".repeat(Math.floor(diff/8));const[userkey,challenge]=pow.split("#");if(window.Worker){const cpuThreads=window.navigator.hardwareConcurrency;const isTor=location.hostname.endsWith(".onion");const workerThreads=(isTor||cpuThreads===2)?cpuThreads:Math.max(Math.ceil(cpuThreads/2),cpuThreads-1);const messageHandler=(e)=>{if(e.data.length===1){const totalHashes=e.data[0];const elapsedSec=Math.floor((Date.now()-start)/1000);const hps=Math.floor(totalHashes/elapsedSec);const requiredSec=Math.floor(eHashes/hps)*1.5;const remainingSec=Math.max(0,Math.floor(requiredSec-elapsedSec),);console.log(`${ hps }H/s, ≈${ remainingSec }s remaining`);return updateElem(".powstatus",`Working, ≈${ remainingSec }s remaining`,)}if(finished){return}const[workerId,answer]=e.data;console.log("Worker",workerId,"returned answer",answer,"in",Date.now()-start+"ms",);submitPow(`${ pow }#${ answer }`)};for(let i=0;isetTimeout(res,10));workers[i].postMessage([userkey,challenge,diff,diffString,powOpts,i,workerThreads])}}else{return insertError("Browser does not support Web Workers.")}})}).then((powResponse)=>{const hasCaptchaForm=document.getElementById("captcha");if(!hasCaptchaForm&&!powResponse.localStorage){postResponse(powResponse.answer)}return powResponse.answer}).catch((e)=>{console.error(e)});function onCaptchaSubmit(captchaResponse){const captchaElem=document.querySelector("[data-sitekey]");captchaElem.insertAdjacentHTML("afterend",`
`,);captchaElem.remove();powFinished.then((powResponse)=>{updateElem(".powstatus","Submitting...","#31cc31");makeLoaderGreen();postResponse(powResponse,captchaResponse)})} +function updateElem(selector,text,color){const updateElem=document.querySelector(selector);if(updateElem){updateElem.innerText=text;if(color){updateElem.style.color=color}}}function insertError(str){const loader=document.querySelector("#loader");const captcha=document.querySelector("#captcha");(captcha||loader).insertAdjacentHTML("afterend",`

Error: ${ str }

`,);loader&&loader.remove();captcha&&captcha.remove();updateElem(".powstatus","")}function finishRedirect(){window.location=location.search.slice(1)+location.hash||"/"}function makeLoaderGreen(){const dots=document.querySelectorAll(".b");if(dots&&dots.length>0){dots.forEach((dot)=>dot.classList.add("green"))}}const wasmSupported=(()=>{try{if(typeof WebAssembly==="object"&&typeof WebAssembly.instantiate==="function"){const module=new WebAssembly.Module(Uint8Array.of(0x0,0x61,0x73,0x6d,0x01,0x00,0x00,0x00),);if(module instanceof WebAssembly.Module){return new WebAssembly.Instance(module)instanceof WebAssembly.Instance}}}catch(e){console.error(e)}return false})();function postResponse(powResponse,captchaResponse){const body={"pow_response":powResponse};if(captchaResponse){body["h-captcha-response"]=captchaResponse;body["g-recaptcha-response"]=captchaResponse}fetch("/.basedflare/bot-check",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams(body),redirect:"manual"}).then((res)=>{const s=res.status;if(s>=400&&s<500){return insertError("Server rejected your submission.")}else if(s>=500){return insertError("Server encountered an error.")}window.localStorage.setItem("basedflare-redirect",Math.random());finishRedirect()}).catch(()=>{insertError("Failed to send request to server.")})}const powFinished=new Promise((resolve)=>{let start=Date.now();const workers=[];let finished=false;const stopPow=()=>{finished=true;const hasCaptcha=document.getElementById("captcha");if(hasCaptcha){updateElem(".powstatus","Waiting for captcha.","#31cc31")}else{updateElem(".powstatus","Submitting...","#31cc31");makeLoaderGreen()}workers.forEach((w)=>w.terminate())};const submitPow=(answer)=>{window.localStorage.setItem("basedflare-pow-response",answer);stopPow();const dummyTime=3500-(Date.now()-start);window.setTimeout(()=>{resolve({answer})},dummyTime)};window.addEventListener("DOMContentLoaded",async()=>{const{time,kb,pow,diff,mode}=document.querySelector("[data-pow]").dataset;window.addEventListener("storage",(event)=>{if(event.key==="basedflare-pow-response"&&!finished){console.log("Got answer",event.newValue,"from storage event");stopPow();resolve({answer:event.newValue,localStorage:true})}else if(event.key==="basedflare-redirect"){console.log("Redirecting, solved in another tab");finishRedirect()}});if(!wasmSupported){return insertError("Browser does not support WebAssembly.")}const powOpts={time:time,mem:kb,hashLen:32,parallelism:1,type:argon2?argon2.ArgonType.Argon2id:null,mode:mode};console.log("Got pow",pow,"with difficulty",diff);const eHashes=Math.pow(16,Math.floor(diff/8))*(((diff%8)*2)||1);const diffString="0".repeat(Math.floor(diff/8));const[userkey,challenge]=pow.split("#");if(window.Worker){const cpuThreads=window.navigator.hardwareConcurrency;const isTor=location.hostname.endsWith(".onion");const workerThreads=(isTor||cpuThreads===2)?cpuThreads:Math.max(Math.ceil(cpuThreads/2),cpuThreads-1);const messageHandler=(e)=>{if(e.data.length===1){const totalHashes=e.data[0];const elapsedSec=Math.floor((Date.now()-start)/1000);const hps=Math.floor(totalHashes/elapsedSec);const requiredSec=Math.floor(eHashes/hps)*1.5;const remainingSec=Math.max(0,Math.floor(requiredSec-elapsedSec),);console.log(`${ hps }H/s, ≈${ remainingSec }s remaining`);return updateElem(".powstatus",`Working, ≈${ remainingSec }s remaining`,)}if(finished){return}const[workerId,answer]=e.data;console.log("Worker",workerId,"returned answer",answer,"in",Date.now()-start+"ms",);submitPow(`${ pow }#${ answer }`)};for(let i=0;isetTimeout(res,10));workers[i].postMessage([userkey,challenge,diff,diffString,powOpts,i,workerThreads])}}else{return insertError("Browser does not support Web Workers.")}})}).then((powResponse)=>{const hasCaptchaForm=document.getElementById("captcha");if(!hasCaptchaForm&&!powResponse.localStorage){postResponse(powResponse.answer)}return powResponse.answer}).catch((e)=>{console.error(e)});function onCaptchaSubmit(captchaResponse){const captchaElem=document.querySelector("[data-sitekey]");captchaElem.insertAdjacentHTML("afterend",`
`,);captchaElem.remove();powFinished.then((powResponse)=>{updateElem(".powstatus","Submitting...","#31cc31");makeLoaderGreen();postResponse(powResponse,captchaResponse)})} diff --git a/src/lua/scripts/bot-check.lua b/src/lua/scripts/bot-check.lua index 38f26a7..d6690af 100644 --- a/src/lua/scripts/bot-check.lua +++ b/src/lua/scripts/bot-check.lua @@ -106,6 +106,19 @@ function _M.view(applet) captcha_enabled = true end + -- return simple json if they send accept: application/json header + local accept_header = applet.headers['accept'] + if accept_header ~= nil and accept_header[0] == 'application/json' then + local_pow_combined = string.format('%s#%d#%s#%s', pow_type, math.ceil(pow_difficulty/8), argon_time, argon_kb) + response_body = "{\"ch\":\""..combined_challenge.."\",\"ca\":"..(captcha_enabled and "true" or "false")..",\"pow\":\""..local_pow_combined.."\"}" + applet:set_status(403) + applet:add_header("content-type", "application/json; charset=utf-8") + applet:add_header("content-length", string.len(response_body)) + applet:start_response() + applet:send(response_body) + return + end + -- pow at least is always enabled when reaching bot-check page site_name_body = string.format(templates.site_name_section, host) if captcha_enabled then @@ -192,7 +205,8 @@ function _M.view(applet) applet:add_header( "set-cookie", string.format( - "_basedflare_pow=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; Domain=.%s; SameSite=Strict; HttpOnly;%s", + --"_basedflare_pow=%s; Expires=Thu, 31-Dec-37 23:55:55 GMT; Path=/; Domain=.%s; SameSite=Strict; HttpOnly;%s", + "_basedflare_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