mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 06:09:56 -05:00
fix: handle malformed tokens, add refresh concurrency control, and improve list URL validation
- Wrap decodeURIComponent in try-catch to handle malformed percent-encoding - Add inFlight promise to prevent concurrent callers from reusing single-use refresh tokens - Require list parameter to be "likes" or "bookmarks" for Snapmaker list URLs - Update documentation to clarify Snapmaker sign-in renewal and token transmission
This commit is contained in:
1 parent
ac5bf253c7
commit
44f4bf2635
6 files changed
+25
-5
No files matched your search
@@ -32,7 +32,13 @@ export function normalizeSnapmakerToken(raw: string | null | undefined): Snapmak
|
||||
.split(";")
|
||||
.map((part) => part.trim())
|
||||
.find((part) => part.startsWith(`${ACCESS_COOKIE}=`));
|
||||
if (pair) text = decodeURIComponent(pair.slice(ACCESS_COOKIE.length + 1));
|
||||
if (pair) {
|
||||
try {
|
||||
text = decodeURIComponent(pair.slice(ACCESS_COOKIE.length + 1));
|
||||
} catch {
|
||||
return null; // Malformed percent-encoding: not a token.
|
||||
}
|
||||
}
|
||||
if (!/^[\w-]+\.[\w-]+\.[\w-]+$/.test(text)) return null;
|
||||
const payload = jwtPayload(text);
|
||||
if (!payload) return null;
|
||||
|
||||
@@ -220,5 +220,6 @@ describe("Snapmaker sign-in", () => {
|
||||
expect(normalizeSnapmakerToken("")).toBeNull();
|
||||
expect(normalizeSnapmakerToken("not a token")).toBeNull();
|
||||
expect(normalizeSnapmakerToken("a.b.c")).toBeNull();
|
||||
expect(normalizeSnapmakerToken("login_access_token=%E0%A4%A")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -37,7 +37,9 @@ the list of recent imports, which is synced between your browsers as described b
|
||||
## What the extension sends, and where
|
||||
|
||||
The extension only communicates with **your Thingport instance** (the address you entered) and with
|
||||
the **provider site you're on**.
|
||||
the **provider site you're on**. The one exception: once you've allowed Snapmaker Space, it also renews your
|
||||
Snapmaker sign-in at id.snapmaker.com every few hours while the browser is open, even with no Snapmaker page open
|
||||
(see below).
|
||||
|
||||
**To your Thingport instance:**
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ HOW TO TEST
|
||||
|
||||
PERMISSIONS
|
||||
- storage: the server address, sign-in and recent imports.
|
||||
- cookies: reads the MakerWorld session cookie, on makerworld.com only, which MakerWorld requires for downloads, and the MyMiniFactory session cookie, on myminifactory.com only, which downloads the user's bought models from their own library, and reads and renews the Snapmaker Space sign-in cookies, on snapmaker.com only, which Snapmaker requires for any download. All are sent only to the user's own server.
|
||||
- cookies: reads the MakerWorld session cookie, on makerworld.com only, which MakerWorld requires for downloads, and the MyMiniFactory session cookie, on myminifactory.com only, which downloads the user's bought models from their own library, and reads and renews the Snapmaker Space sign-in cookies, on snapmaker.com only, which Snapmaker requires for any download. The MakerWorld and MyMiniFactory cookies and the Snapmaker access token are sent only to the user's own server; the Snapmaker refresh token is sent only to Snapmaker's own sign-in service (id.snapmaker.com), to renew the sign-in.
|
||||
- downloads: reads the link from a download MakerWorld's page starts, then cancels it.
|
||||
- alarms: renews the Snapmaker Space sign-in every few hours, as it lasts a day; only once the user has turned Snapmaker Space on.
|
||||
- scripting: registers the content script on MyMiniFactory and Snapmaker Space, and on model sites added in later versions, once the user allows them.
|
||||
|
||||
@@ -83,9 +83,19 @@ async function renew(): Promise<string | null> {
|
||||
return access;
|
||||
}
|
||||
|
||||
// Shared by concurrent callers, so two of them can't both spend the same single-use refresh token.
|
||||
let inFlight: Promise<{ userId: string | null }> | null = null;
|
||||
|
||||
/** Sends Thingport a token that lasts, renewing the browser's when it, or the one Thingport has, is running out.
|
||||
* Best-effort: never throws. Returns the signed-in Snapmaker user's id, or null when there's none. */
|
||||
export async function syncSnapmakerSession(): Promise<{ userId: string | null }> {
|
||||
export function syncSnapmakerSession(): Promise<{ userId: string | null }> {
|
||||
inFlight ??= syncOnce().finally(() => {
|
||||
inFlight = null;
|
||||
});
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
async function syncOnce(): Promise<{ userId: string | null }> {
|
||||
const site = OPTIONAL_SITES.find((s) => s.id === "snapmaker");
|
||||
if (!site || !(await siteEnabled(site).catch(() => false))) return { userId: null };
|
||||
try {
|
||||
|
||||
@@ -110,7 +110,8 @@ export function isSnapmakerListUrl(url: string): boolean {
|
||||
if (!parsed || parsed.hostname.toLowerCase() !== "space.snapmaker.com") return false;
|
||||
return (
|
||||
/^\/(?:[a-z]{2}(?:-[a-z]{2})?\/)?creator\/\d+\/?$/i.test(parsed.pathname) &&
|
||||
parsed.searchParams.get("tab") === "interactions"
|
||||
parsed.searchParams.get("tab") === "interactions" &&
|
||||
["likes", "bookmarks"].includes(parsed.searchParams.get("list") ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user