Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b18ddf4ca |
@@ -2174,9 +2174,10 @@ request made through `debugFetch` in `src/shared/log.js` (the explorer, the
|
|||||||
price feed, the RPC calls a site makes, and the endpoint checks in settings) and
|
price feed, the RPC calls a site makes, and the endpoint checks in settings) and
|
||||||
for its response. A request is logged by its HTTP method, the origin of its URL
|
for its response. A request is logged by its HTTP method, the origin of its URL
|
||||||
(scheme, host and port) and, for a JSON-RPC call, the method name; the balance
|
(scheme, host and port) and, for a JSON-RPC call, the method name; the balance
|
||||||
refresh and token lookup name the RPC endpoint by its origin too. The URL's path
|
refresh, the token lookup and a failed endpoint check in settings name the
|
||||||
and query string, where RPC providers put API keys, and the request body are
|
endpoint by its origin too. The URL's path and query string, where RPC providers
|
||||||
never logged.
|
put API keys, any user name and password in it, and the request body are never
|
||||||
|
logged.
|
||||||
|
|
||||||
### Key Decisions
|
### Key Decisions
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,11 @@ but the review is broader than any of them.
|
|||||||
method, the URL's origin and, for a JSON-RPC call, the method name. The
|
method, the URL's origin and, for a JSON-RPC call, the method name. The
|
||||||
balance refresh and token lookup log the RPC endpoint by its origin too. A
|
balance refresh and token lookup log the RPC endpoint by its origin too. A
|
||||||
failed RPC call's error line prints the error's short message, which names the
|
failed RPC call's error line prints the error's short message, which names the
|
||||||
HTTP status, not its full message, which carries the request URL. The README's
|
HTTP status, not its full message, which carries the request URL. A failed
|
||||||
DEBUG Mode Policy says what debug mode logs.
|
endpoint check in settings names the endpoint by its origin, not the `fetch`
|
||||||
|
error's message, which carries the whole URL, password included, for a URL
|
||||||
|
with a user name and password. The README's DEBUG Mode Policy says what debug
|
||||||
|
mode logs.
|
||||||
|
|
||||||
- 2026-10-04: A site has at most one connection prompt and one signature prompt
|
- 2026-10-04: A site has at most one connection prompt and one signature prompt
|
||||||
open at a time ([#405](https://git.eeqj.de/sneak/AutistMask/issues/405)). Each
|
open at a time ([#405](https://git.eeqj.de/sneak/AutistMask/issues/405)). Each
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ const {
|
|||||||
} = require("../dustThreshold");
|
} = require("../dustThreshold");
|
||||||
const { state, saveState, currentNetwork } = require("../../shared/state");
|
const { state, saveState, currentNetwork } = require("../../shared/state");
|
||||||
const { onChainSwitch } = require("../../shared/chainSwitch");
|
const { onChainSwitch } = require("../../shared/chainSwitch");
|
||||||
const { log, debugFetch, setRuntimeDebug } = require("../../shared/log");
|
const {
|
||||||
|
log,
|
||||||
|
debugFetch,
|
||||||
|
urlOrigin,
|
||||||
|
setRuntimeDebug,
|
||||||
|
} = require("../../shared/log");
|
||||||
const deleteWallet = require("./deleteWallet");
|
const deleteWallet = require("./deleteWallet");
|
||||||
const showPhrase = require("./showPhrase");
|
const showPhrase = require("./showPhrase");
|
||||||
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
const { walletHasRecoveryPhrase } = require("../../shared/wallet");
|
||||||
@@ -272,8 +277,11 @@ function init(ctx) {
|
|||||||
showFlash("Wrong network: expected " + net.name + ".");
|
showFlash("Wrong network: expected " + net.name + ".");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
log.errorf("RPC validation fetch failed:", e.message);
|
// Not the error's message: fetch puts the whole URL, password and
|
||||||
|
// key included, in the message of the error it throws for a URL
|
||||||
|
// with a user name and password or one it cannot parse.
|
||||||
|
log.errorf("RPC validation fetch failed:", urlOrigin(url));
|
||||||
showFlash("Could not reach endpoint.");
|
showFlash("Could not reach endpoint.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -295,8 +303,9 @@ function init(ctx) {
|
|||||||
showFlash("Endpoint returned HTTP " + resp.status + ".");
|
showFlash("Endpoint returned HTTP " + resp.status + ".");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
log.errorf("Blockscout validation failed:", e.message);
|
// Not the error's message, as for the RPC check above.
|
||||||
|
log.errorf("Blockscout validation failed:", urlOrigin(url));
|
||||||
showFlash("Could not reach endpoint.");
|
showFlash("Could not reach endpoint.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -43,7 +43,8 @@ const log = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The origin (scheme, host and port) of a URL, for logging in place of the
|
// The origin (scheme, host and port) of a URL, for logging in place of the
|
||||||
// URL: RPC providers put API keys in the path or the query string. A URL that
|
// URL: RPC providers put API keys in the path or the query string, and a URL
|
||||||
|
// can carry a user name and password, which the origin leaves out. A URL that
|
||||||
// does not parse gives "", so logging never stops a request.
|
// does not parse gives "", so logging never stops a request.
|
||||||
function urlOrigin(url) {
|
function urlOrigin(url) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// HTTP method, the URL's origin and the JSON-RPC method, and nothing else of
|
// HTTP method, the URL's origin and the JSON-RPC method, and nothing else of
|
||||||
// the request.
|
// the request.
|
||||||
|
|
||||||
const { debugFetch, setRuntimeDebug } = require("../src/shared/log");
|
const { debugFetch, urlOrigin, setRuntimeDebug } = require("../src/shared/log");
|
||||||
|
|
||||||
const realFetch = globalThis.fetch;
|
const realFetch = globalThis.fetch;
|
||||||
|
|
||||||
@@ -42,3 +42,12 @@ test("logs the origin and JSON-RPC method, not the key in the URL", async () =>
|
|||||||
expect(logged).toContain("https://rpc.example.invalid");
|
expect(logged).toContain("https://rpc.example.invalid");
|
||||||
expect(logged).toContain("eth_chainId");
|
expect(logged).toContain("eth_chainId");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("the origin leaves out a user name and password in the URL", () => {
|
||||||
|
expect(
|
||||||
|
urlOrigin("https://user:SECRETPASS@rpc.example.invalid/v3/KEY"),
|
||||||
|
).toBe("https://rpc.example.invalid");
|
||||||
|
expect(urlOrigin("wss://user:SECRETPASS@rpc.example.invalid:8546/")).toBe(
|
||||||
|
"wss://rpc.example.invalid:8546",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,7 +12,11 @@
|
|||||||
// every log level is printed.
|
// every log level is printed.
|
||||||
|
|
||||||
const { FetchRequest } = require("ethers");
|
const { FetchRequest } = require("ethers");
|
||||||
const { getProvider, refreshBalances } = require("../src/shared/balances");
|
const {
|
||||||
|
getProvider,
|
||||||
|
lookupTokenInfo,
|
||||||
|
refreshBalances,
|
||||||
|
} = require("../src/shared/balances");
|
||||||
const { getFullWarnings } = require("../src/shared/addressWarnings");
|
const { getFullWarnings } = require("../src/shared/addressWarnings");
|
||||||
const { resolveEnsName } = require("../src/shared/ens");
|
const { resolveEnsName } = require("../src/shared/ens");
|
||||||
const { setRuntimeDebug } = require("../src/shared/log");
|
const { setRuntimeDebug } = require("../src/shared/log");
|
||||||
@@ -90,3 +94,12 @@ test("the balance refresh", async () => {
|
|||||||
expectFailureLoggedWithoutKey("ETH balance failed");
|
expectFailureLoggedWithoutKey("ETH balance failed");
|
||||||
expectFailureLoggedWithoutKey("ENS reverse failed");
|
expectFailureLoggedWithoutKey("ENS reverse failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The lookup's first line, at debug level, names the RPC endpoint; the check
|
||||||
|
// of everything printed covers it too.
|
||||||
|
test("the token lookup", async () => {
|
||||||
|
await expect(lookupTokenInfo(ADDRESS, RPC_URL, "mainnet")).rejects.toThrow(
|
||||||
|
"Not a valid ERC-20 token",
|
||||||
|
);
|
||||||
|
expectFailureLoggedWithoutKey("symbol() failed:");
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
// What reaches the console when an endpoint check in Settings fails.
|
||||||
|
//
|
||||||
|
// fetch refuses a URL with a user name and password in it, or one it cannot
|
||||||
|
// parse, with an error whose message carries the whole URL: the password, and
|
||||||
|
// any API key in the path or query string. The checks behind the RPC and
|
||||||
|
// Blockscout Save buttons printed that message
|
||||||
|
// (https://git.eeqj.de/sneak/AutistMask/issues/410); they now name the
|
||||||
|
// endpoint by its origin.
|
||||||
|
//
|
||||||
|
// The real fetch runs; it throws before making any request. Debug mode is on,
|
||||||
|
// so every log level is printed.
|
||||||
|
|
||||||
|
const SECRETS = ["SECRETPASS789", "PATHKEY123", "QUERYTOKEN456"];
|
||||||
|
const RPC_WITH_PASSWORD =
|
||||||
|
"https://user:SECRETPASS789@rpc.example.invalid/v3/PATHKEY123?token=QUERYTOKEN456";
|
||||||
|
// Port 99999 is out of range, so the URL does not parse.
|
||||||
|
const RPC_UNPARSEABLE =
|
||||||
|
"https://rpc.example.invalid:99999/v3/PATHKEY123?token=QUERYTOKEN456";
|
||||||
|
const BLOCKSCOUT_WITH_PASSWORD =
|
||||||
|
"https://user:SECRETPASS789@explorer.example.invalid/PATHKEY123/api/v2";
|
||||||
|
|
||||||
|
const SAVED_RPC = "https://saved-rpc.example.invalid";
|
||||||
|
const SAVED_BLOCKSCOUT = "https://saved-explorer.example.invalid/api/v2";
|
||||||
|
|
||||||
|
let elements;
|
||||||
|
let flashes;
|
||||||
|
let printed;
|
||||||
|
let state;
|
||||||
|
|
||||||
|
// A stand-in for one DOM node: enough of an element for init() to set
|
||||||
|
// properties on it and hang listeners off it.
|
||||||
|
function fakeElement() {
|
||||||
|
return {
|
||||||
|
value: "",
|
||||||
|
checked: false,
|
||||||
|
textContent: "",
|
||||||
|
href: "",
|
||||||
|
style: {},
|
||||||
|
dataset: {},
|
||||||
|
classList: { add() {}, remove() {} },
|
||||||
|
listeners: {},
|
||||||
|
addEventListener(event, handler) {
|
||||||
|
this.listeners[event] = handler;
|
||||||
|
},
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function element(id) {
|
||||||
|
return (elements[id] ||= fakeElement());
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettingsView() {
|
||||||
|
elements = {};
|
||||||
|
flashes = [];
|
||||||
|
|
||||||
|
jest.resetModules();
|
||||||
|
|
||||||
|
jest.doMock("../src/popup/views/helpers", () => ({
|
||||||
|
$: element,
|
||||||
|
showView: () => {},
|
||||||
|
updateDebugBanner: () => {},
|
||||||
|
showFlash: (msg) => flashes.push(msg),
|
||||||
|
escapeHtml: (s) => s,
|
||||||
|
flashCopyFeedback: () => {},
|
||||||
|
goBack: () => {},
|
||||||
|
pushCurrentView: () => {},
|
||||||
|
onViewLeave: () => {},
|
||||||
|
VIEWS: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
state = require("../src/shared/state").state;
|
||||||
|
state.rpcUrl = SAVED_RPC;
|
||||||
|
state.blockscoutUrl = SAVED_BLOCKSCOUT;
|
||||||
|
require("../src/shared/log").setRuntimeDebug(true);
|
||||||
|
|
||||||
|
require("../src/popup/views/settings").init({});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function save(fieldId, buttonId, typed) {
|
||||||
|
element(fieldId).value = typed;
|
||||||
|
await element(buttonId).listeners.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The check failed, nothing was saved, and nothing printed carries the
|
||||||
|
// password or the key.
|
||||||
|
function expectFailedWithoutSecrets(label) {
|
||||||
|
expect(flashes).toContain("Could not reach endpoint.");
|
||||||
|
expect(state.rpcUrl).toBe(SAVED_RPC);
|
||||||
|
expect(state.blockscoutUrl).toBe(SAVED_BLOCKSCOUT);
|
||||||
|
const line = printed.find((text) => text.includes(label));
|
||||||
|
expect(line).toBeDefined();
|
||||||
|
const all = printed.join("\n");
|
||||||
|
for (const secret of SECRETS) {
|
||||||
|
expect(all).not.toContain(secret);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
printed = [];
|
||||||
|
for (const method of ["log", "warn", "error"]) {
|
||||||
|
jest.spyOn(console, method).mockImplementation((...args) => {
|
||||||
|
printed.push(args.map(String).join(" "));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
globalThis.chrome = {
|
||||||
|
runtime: { sendMessage: () => {} },
|
||||||
|
storage: { local: { get: async () => ({}), set: async () => {} } },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.dontMock("../src/popup/views/helpers");
|
||||||
|
delete globalThis.chrome;
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fetch puts the whole URL in the error it throws for such a URL", async () => {
|
||||||
|
for (const url of [RPC_WITH_PASSWORD, RPC_UNPARSEABLE]) {
|
||||||
|
const error = await fetch(url).catch((e) => e);
|
||||||
|
expect(error.message).toContain("PATHKEY123");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the RPC check of a URL with a user name and password", async () => {
|
||||||
|
loadSettingsView();
|
||||||
|
await save("settings-rpc", "btn-save-rpc", RPC_WITH_PASSWORD);
|
||||||
|
const line = expectFailedWithoutSecrets("RPC validation fetch failed");
|
||||||
|
expect(line).toContain("https://rpc.example.invalid");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the RPC check of a URL that does not parse", async () => {
|
||||||
|
loadSettingsView();
|
||||||
|
await save("settings-rpc", "btn-save-rpc", RPC_UNPARSEABLE);
|
||||||
|
expectFailedWithoutSecrets("RPC validation fetch failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the Blockscout check of a URL with a user name and password", async () => {
|
||||||
|
loadSettingsView();
|
||||||
|
await save(
|
||||||
|
"settings-blockscout",
|
||||||
|
"btn-save-blockscout",
|
||||||
|
BLOCKSCOUT_WITH_PASSWORD,
|
||||||
|
);
|
||||||
|
const line = expectFailedWithoutSecrets("Blockscout validation failed");
|
||||||
|
expect(line).toContain("https://explorer.example.invalid");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user