IP Whitelists and Blacklists
- You can specify IP ranges to whitelist or blacklist from accessing the proxy. Note that changes here are not
- persisted across server restarts. If you want to make changes permanent, you can copy the values to your deployment
- configuration.
-
-
- Entries can be specified as single addresses or
+ You can specify IP ranges to whitelist or blacklist from accessing the proxy. Entries can be specified as single
+ addresses or
CIDR notation. IPv6 is
supported but not recommended for use with the current version of the proxy.
+
+ Note: Changes here are not persisted across server restarts. If you want to make changes permanent,
+ you can copy the values to your deployment configuration.
+
<% for (let i = 0; i < whitelists.length; i++) { %>
<%- include("partials/admin-cidr-widget", { list: whitelists[i] }) %>
<% } %>
@@ -99,10 +104,25 @@
diff --git a/src/user/web/pow-captcha.ts b/src/user/web/pow-captcha.ts
index 9d5ff8c..43b28be 100644
--- a/src/user/web/pow-captcha.ts
+++ b/src/user/web/pow-captcha.ts
@@ -187,7 +187,7 @@ function verifyTokenRefreshable(token: string, req: express.Request) {
}
}
- req.log.info({ token }, "Allowing token refresh");
+ req.log.info({ token: `...${token.slice(-5)}` }, "Allowing token refresh");
return true;
}
@@ -227,51 +227,57 @@ router.post("/verify", async (req, res) => {
const ip = req.ip;
req.log.info("Got verification request");
if (recentAttempts.has(ip)) {
- res
- .status(429)
- .json({ error: "Rate limited; wait a minute before trying again" });
+ const error = "Rate limited; wait a minute before trying again";
+ req.log.info({ error }, "Verification rejected");
+ res.status(429).json({ error });
return;
}
const result = verifySchema.safeParse(req.body);
if (!result.success) {
- res
- .status(400)
- .json({ error: "Invalid verify request", details: result.error });
+ const error = "Invalid verify request";
+ req.log.info({ error, result }, "Verification rejected");
+ res.status(400).json({ error, details: result.error });
return;
}
const { challenge, signature, solution } = result.data;
if (signMessage(challenge, powKeySalt) !== signature) {
- res.status(400).json({
- error:
- "Invalid signature; server may have restarted since challenge was issued. Please request a new challenge.",
- });
+ const error =
+ "Invalid signature; server may have restarted since challenge was issued. Please request a new challenge.";
+ req.log.info({ error }, "Verification rejected");
+ res.status(400).json({ error });
return;
}
if (config.proxyKey && result.data.proxyKey !== config.proxyKey) {
- res.status(401).json({ error: "Invalid proxy password" });
+ const error = "Invalid proxy password";
+ req.log.info({ error }, "Verification rejected");
+ res.status(401).json({ error, password: result.data.proxyKey });
return;
}
if (challenge.ip && challenge.ip !== ip) {
- req.log.warn("Attempt to verify from different IP address");
- res.status(400).json({
- error: "Solution must be verified from original IP address",
- });
+ const error = "Solution must be verified from original IP address";
+ req.log.info(
+ { error, challengeIp: challenge.ip, clientIp: ip },
+ "Verification rejected"
+ );
+ res.status(400).json({ error });
return;
}
if (solves.has(signature)) {
- req.log.warn("Attempt to reuse signature");
- res.status(400).json({ error: "Reused signature" });
+ const error = "Reused signature";
+ req.log.info({ error }, "Verification rejected");
+ res.status(400).json({ error });
return;
}
if (Date.now() > challenge.e) {
- req.log.warn("Verification took too long");
- res.status(400).json({ error: "Verification took too long" });
+ const error = "Verification took too long";
+ req.log.info({ error }, "Verification rejected");
+ res.status(400).json({ error });
return;
}
@@ -285,7 +291,7 @@ router.post("/verify", async (req, res) => {
const success = await verifySolution(challenge, solution, req.log);
if (!success) {
recentAttempts.set(ip, Date.now() + 1000 * 60 * 60 * 6);
- req.log.warn("Solution failed verification");
+ req.log.warn("Bogus solution, client blocked");
res.status(400).json({ error: "Solution failed verification" });
return;
}
@@ -299,10 +305,12 @@ router.post("/verify", async (req, res) => {
if (challenge.token) {
const user = getUser(challenge.token);
if (user) {
- user.expiresAt = Date.now() + config.powTokenHours * 60 * 60 * 1000;
- user.disabledAt = undefined;
- user.disabledReason = undefined;
- upsertUser(user);
+ upsertUser({
+ token: challenge.token,
+ expiresAt: Date.now() + config.powTokenHours * 60 * 60 * 1000,
+ disabledAt: null,
+ disabledReason: null,
+ });
req.log.info(
{ token: `...${challenge.token.slice(-5)}` },
"Token refreshed"
diff --git a/src/user/web/views/partials/user_challenge_widget.ejs b/src/user/web/views/partials/user_challenge_widget.ejs
index 4e5d579..ab8df3e 100644
--- a/src/user/web/views/partials/user_challenge_widget.ejs
+++ b/src/user/web/views/partials/user_challenge_widget.ejs
@@ -5,8 +5,8 @@
- Your device needs to perform a verification task before you can receive a token. This might take anywhere from a few
- seconds to a few minutes, depending on your device and the proxy's security settings.
+ Your device needs to be verified before you can receive a token. This might take anywhere from a few seconds to a
+ few minutes, depending on your device and the proxy's security settings.
-
Click the button below to start.
What is this?
@@ -107,18 +116,6 @@
faster than the first one.
-
- What is the "Workers" setting?
-
- This controls how many CPU cores will be used to solve the verification task. If your device gets too hot or slows
- down too much during verification, reduce the number of workers.
-
-
- For fastest verification, set this to the number of physical CPU cores in your device. Setting more workers than
- you have actual cores will generally only slow down verification.
-
- If you don't understand what this means, leave it at the default setting.
-
Other important information
+
+ Settings
+
+
+
+
+
+ This controls how many CPU cores will be used to solve the verification task. If your device gets too hot or slows
+ down too much during verification, reduce the number of workers.
+
+
+ For fastest verification, set this to the number of physical CPU cores in your device. Setting more workers than
+ you have actual cores will generally only slow down verification.
+
+ If you don't understand what this means, leave it at the default setting.
+
@@ -185,6 +194,9 @@
function handleWorkerMessage(e) {
switch (e.data.type) {
case "progress":
+ if (solution) {
+ return;
+ }
totalHashes += e.data.hashes;
reports++;
break;
@@ -206,13 +218,13 @@
}
workers.forEach((w, i) => {
w.postMessage({ type: "stop" });
- setTimeout(() => w.terminate(), 1000 + i * 100)
+ setTimeout(() => w.terminate(), 1000 + i * 100);
});
workers = [];
active = false;
solution = e.data.nonce;
document.getElementById("captcha-result").textContent =
- "Verification completed. Submitting solution for verification...";
+ "Solution found. Verifying with server...";
document.getElementById("captcha-control").style.display = "none";
submitVerification();
break;
@@ -233,6 +245,21 @@
estimateProgress();
}
+ function copyToClipboard(text) {
+ if (!navigator.clipboard) {
+ const textArea = document.createElement("textarea");
+ textArea.value = text;
+ document.body.appendChild(textArea);
+ textArea.focus();
+ textArea.select();
+ document.execCommand("copy");
+ textArea.remove();
+ } else {
+ navigator.clipboard.writeText(text);
+ }
+ alert("Copied to clipboard.");
+ }
+
function loadNewChallenge(c, s) {
const btn = document.getElementById("worker-control");
btn.textContent = "Start verification";
@@ -248,6 +275,7 @@
startTime = 0;
lastUpdateTime = 0;
elapsedTime = 0;
+ totalHashes = 0;
const targetValue = challenge.d.slice(0, -1);
const hashLength = challenge.hl;
workFactor = Number(BigInt(2) ** BigInt(8 * hashLength) / BigInt(targetValue));
@@ -329,52 +357,53 @@
document.getElementById("captcha-progress").style.display = "none";
document.getElementById("captcha-result").innerHTML = `
Verification complete
-
Your user token is: ${data.token}
+
Your user token is: ${data.token}
Valid until: ${new Date(Date.now() + lifetime * 3600 * 1000).toLocaleString()}
`;
}
});
}
+ function formatTime(time) {
+ if (time < 60) {
+ return time.toFixed(1) + "s";
+ } else if (time < 3600) {
+ const minutes = Math.floor(time / 60);
+ const seconds = Math.floor(time % 60);
+ return minutes + "m " + seconds + "s";
+ } else {
+ const hours = Math.floor(time / 3600);
+ const minutes = Math.floor((time % 3600) / 60);
+ return hours + "h " + minutes + "m";
+ }
+ }
+
function estimateProgress() {
- if (reports % workers.length !== 0) {
+ // if (reports % workers.length !== 0) {
+ // return;
+ // }
+ if (Date.now() - lastUpdateTime < 500) {
return;
}
elapsedTime += (Date.now() - lastUpdateTime) / 1000;
lastUpdateTime = Date.now();
const hashRate = totalHashes / elapsedTime;
const timeRemaining = (workFactor - totalHashes) / hashRate;
- const progress = 100 * (1 - Math.exp(-totalHashes / workFactor));
-
- const formatTime = (time) => {
- if (time < 60) {
- return time.toFixed(1) + "s";
- } else if (time < 3600) {
- const minutes = Math.floor(time / 60);
- const seconds = Math.floor(time % 60);
- return minutes + "m " + seconds + "s";
- } else {
- const hours = Math.floor(time / 3600);
- const minutes = Math.floor((time % 3600) / 60);
- return hours + "h " + minutes + "m";
- }
- };
-
+ // const progress = 100 * (1 - Math.exp(-totalHashes / workFactor));
const p = 1 / workFactor;
- const odds = ((1 - p) ** totalHashes * 100).toFixed(2);
+ const odds = ((1 - p) ** totalHashes * 100).toFixed(3);
+ const progress = 100 - odds;
- let note = "";
- if (odds < 33) {
- note = " (" + odds + "% odds of no solution yet)";
- }
+ // let note = " (" + odds + "% odds of no solution yet)";
- document.getElementById("captcha-progress").style.width = Math.min(progress, 100) + "%";
+ document.querySelector("#captcha-progress>.progress").style.width = Math.min(progress, 100) + "%";
document.getElementById("captcha-progress-text").value = `
Solution probability: 1 in ${workFactor.toLocaleString()} hashes
-Hashes computed: ${totalHashes.toLocaleString()}${note}
+Hashes computed: ${totalHashes.toLocaleString()}
+Luckiness: ${odds}%
Elapsed time: ${formatTime(elapsedTime)}
Hash rate: ${hashRate.toFixed(2)} H/s
Workers: ${workers.length}${isMobileWebkit ? " (iOS/iPadOS detected)" : ""}
-${active ? `Average time remaining: ${formatTime(timeRemaining)}` : "Verification task stopped"}`.trim();
+${active ? `Average time remaining: ${formatTime(timeRemaining)}` : "Verification stopped"}`.trim();
}
diff --git a/src/user/web/views/user_request_token.ejs b/src/user/web/views/user_request_token.ejs
index 1fd86b1..33c2b24 100644
--- a/src/user/web/views/user_request_token.ejs
+++ b/src/user/web/views/user_request_token.ejs
@@ -1,20 +1,27 @@
<%- include("partials/shared_header", { title: "Request User Token" }) %>
Request User Token
@@ -28,37 +35,70 @@
<% } %>
-
-
It looks like you might have an older temporary user token. If it has expired, you can try to refresh it.
-
Existing token:
+
+
+
+
Existing token:
-