feat(browser): unify site credentials with browser profiles (BR-2)
- Inject the site account Cookie (manual + auto-login session, session wins)
into the camofox profile before the first tab opens; password accounts
without a session run refresh-login first; API Key sites are skipped.
- Cookie expired after injection -> credential.status=expired; cookie
accounts go to waiting/login_required ("log in once, applies site-wide").
- Write back site-domain cookies from the browser on handover, session close,
every 10 min of activity, and from refresh-login when the stored cookie is
invalid (verified before saving). user_info.cookieOrigin records
source=browser; source shown as manual/login/browser in the origin view.
- New camofox plugin umt-cookies (GET/POST /umt/sessions/:userId/cookies
under the access key); profile dir 0700/files 0600 in the desktop image.
- Single entry: sources-identity/service/credentials.ts.
- Real check: HHClub cookie injected into a fresh profile -> home page
logged in (logout link present), 5 cookies read back.
Requires rebuilding umt-browser-desktop (docker compose build camofox &&
docker compose up -d camofox) to take effect.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
901b01d1a1
commit
28974d1e1a
58 files changed
+2843
-1112
No files matched your search
@@ -18,6 +18,7 @@
|
||||
import type {
|
||||
BrowserCapabilities,
|
||||
BrowserControlLease,
|
||||
BrowserCookie,
|
||||
BrowserHealth,
|
||||
BrowserSession,
|
||||
BrowserSnapshot,
|
||||
@@ -150,6 +151,8 @@ export class CamofoxBrowserProvider implements BrowserProvider {
|
||||
private readonly sink: DownloadSink;
|
||||
private readonly now: () => Date;
|
||||
private readonly newId: () => string;
|
||||
/** Cookie 接口:plugin = umt-cookies 插件(读写);upstream = 上游导入端点(只写,要求 CAMOFOX_API_KEY 与访问凭据相同);none = 都没有 */
|
||||
private cookieApi: "plugin" | "upstream" | "none" | null = null;
|
||||
|
||||
constructor(private readonly cfg: CamofoxConfig) {
|
||||
this.base = cfg.baseUrl.replace(/\/+$/, "");
|
||||
@@ -368,6 +371,16 @@ export class CamofoxBrowserProvider implements BrowserProvider {
|
||||
if (s.session.profileKey === profileKey && s.session.controller !== "closed")
|
||||
throw new BrowserError("control_conflict", "该站点/账号已有未关闭的浏览器会话");
|
||||
const sessionId = this.newId();
|
||||
let cookies: OpenSessionResult["cookies"];
|
||||
if (req.cookies?.length) {
|
||||
try {
|
||||
cookies = { status: "injected", count: (await this.putCookies(profileKey, req.cookies)).count };
|
||||
} catch (e) {
|
||||
if (e instanceof BrowserError && e.kind === "not_implemented") cookies = { status: "unsupported", count: 0 };
|
||||
else if (e instanceof BrowserError && e.kind !== "auth") cookies = { status: "failed", count: 0 };
|
||||
else throw e;
|
||||
}
|
||||
}
|
||||
const res = await this.request("POST", "/tabs", {
|
||||
body: { userId: profileKey, sessionKey: sessionId, ...(req.initialUrl ? { url: req.initialUrl } : {}) },
|
||||
timeoutMs: req.budget.timeoutMs,
|
||||
@@ -425,7 +438,7 @@ export class CamofoxBrowserProvider implements BrowserProvider {
|
||||
};
|
||||
this.assertLanded(rawUrl, req.budget);
|
||||
}
|
||||
return { session: { ...session }, tab: { ...tab }, navigation };
|
||||
return { session: { ...session }, tab: { ...tab }, navigation, ...(cookies ? { cookies } : {}) };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -442,6 +455,91 @@ export class CamofoxBrowserProvider implements BrowserProvider {
|
||||
throw new BrowserError("domain_blocked", `导航被重定向到白名单外:${redactBrowserUrl(new URL(rawUrl).origin)}`);
|
||||
}
|
||||
|
||||
// ---------- Cookie(BR-2 凭据统一) ----------
|
||||
|
||||
/**
|
||||
* 写 Cookie:先走 umt-cookies 插件 `POST /umt/sessions/{userId}/cookies`(docker/browser-desktop 自带,访问凭据即可);
|
||||
* 插件不在(404)再试上游 `POST /sessions/{userId}/cookies`(1.17.0 要求 CAMOFOX_API_KEY,与访问凭据相同时可用);
|
||||
* 都不行报 not_implemented。两条路上游都会立即把 profile 落盘。
|
||||
*/
|
||||
setCookies(profileKey: string, cookies: BrowserCookie[]): Promise<{ count: number }> {
|
||||
return this.withProfile(profileKey, () => this.putCookies(profileKey, cookies));
|
||||
}
|
||||
|
||||
private async putCookies(profileKey: string, cookies: BrowserCookie[]): Promise<{ count: number }> {
|
||||
const list = cookies.slice(0, 500);
|
||||
const body = { cookies: list } as unknown as Json;
|
||||
const path = `/sessions/${encodeURIComponent(profileKey)}/cookies`;
|
||||
if (this.cookieApi === null || this.cookieApi === "plugin") {
|
||||
try {
|
||||
const res = await this.json(
|
||||
await this.request("POST", `/umt${path}`, { body }),
|
||||
"/umt/sessions/{userId}/cookies",
|
||||
);
|
||||
this.cookieApi = "plugin";
|
||||
return { count: int(res.count) ?? list.length };
|
||||
} catch (e) {
|
||||
if (!(e instanceof BrowserError && e.httpStatus === 404)) throw e;
|
||||
this.cookieApi = null;
|
||||
}
|
||||
}
|
||||
if (this.cookieApi === null || this.cookieApi === "upstream") {
|
||||
try {
|
||||
const res = await this.json(await this.request("POST", path, { body }), "/sessions/{userId}/cookies");
|
||||
this.cookieApi = "upstream";
|
||||
return { count: int(res.count) ?? list.length };
|
||||
} catch (e) {
|
||||
// 403:上游导入口令(CAMOFOX_API_KEY)未设置或与访问凭据不同
|
||||
if (!(e instanceof BrowserError && (e.httpStatus === 403 || e.httpStatus === 404))) throw e;
|
||||
this.cookieApi = "none";
|
||||
}
|
||||
}
|
||||
throw new BrowserError("not_implemented", "浏览器服务没有 Cookie 接口(镜像未带 umt-cookies 插件)");
|
||||
}
|
||||
|
||||
/** 读 Cookie:只有 umt-cookies 插件提供(上游 1.17.0 没有读取端点);返回作用于这些地址的 Cookie */
|
||||
async getCookies(profileKey: string, urls: string[]): Promise<BrowserCookie[]> {
|
||||
if (this.cookieApi === "upstream" || this.cookieApi === "none")
|
||||
throw new BrowserError("not_implemented", "浏览器服务不支持读回 Cookie(镜像未带 umt-cookies 插件)");
|
||||
let res: Json;
|
||||
try {
|
||||
res = await this.json(
|
||||
await this.request("GET", `/umt/sessions/${encodeURIComponent(profileKey)}/cookies`, {
|
||||
query: { urls: urls.join(",") },
|
||||
}),
|
||||
"/umt/sessions/{userId}/cookies",
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof BrowserError && e.httpStatus === 404) {
|
||||
throw new BrowserError("not_implemented", "浏览器服务不支持读回 Cookie(镜像未带 umt-cookies 插件)");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
this.cookieApi = "plugin";
|
||||
const list = Array.isArray(res.cookies) ? res.cookies : [];
|
||||
const out: BrowserCookie[] = [];
|
||||
for (const c of list) {
|
||||
if (!c || typeof c !== "object") continue;
|
||||
const o = c as Json;
|
||||
const name = str(o.name);
|
||||
const domain = str(o.domain);
|
||||
const value = str(o.value);
|
||||
if (!name || !domain || value === undefined) continue;
|
||||
const sameSite = str(o.sameSite);
|
||||
out.push({
|
||||
name,
|
||||
value,
|
||||
domain,
|
||||
path: str(o.path) ?? "/",
|
||||
expires: typeof o.expires === "number" ? o.expires : -1,
|
||||
httpOnly: o.httpOnly === true,
|
||||
secure: o.secure === true,
|
||||
...(sameSite === "Strict" || sameSite === "Lax" || sameSite === "None" ? { sameSite } : {}),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** DELETE /sessions/{userId}:关闭该 profile 的全部上游标签页;上游 404(已被回收)视为已关闭 */
|
||||
async closeSession(sessionId: string, reason = "closed"): Promise<void> {
|
||||
const s = this.state(sessionId);
|
||||
@@ -567,6 +665,18 @@ export class CamofoxBrowserProvider implements BrowserProvider {
|
||||
}
|
||||
if (lost.length) s.refsValid = false;
|
||||
if (s.tabs.get(s.currentTabId)?.status !== "open") s.currentTabId = alive[0] ?? "";
|
||||
// 桌面模式下人在画面里导航不经本适配器:按上游报告的当前地址同步(登录页判定、回写登录态前要用最新地址)
|
||||
const cur = list.find((t) => (str(t.tabId) ?? str(t.targetId)) === s.currentTabId);
|
||||
const curUrl = cur ? str(cur.url) : undefined;
|
||||
if (curUrl && curUrl !== s.rawUrl && /^https?:/.test(curUrl)) {
|
||||
s.refsValid = false;
|
||||
this.touch(s, curUrl);
|
||||
const tab = s.tabs.get(s.currentTabId);
|
||||
if (tab) {
|
||||
tab.url = s.session.currentUrl;
|
||||
tab.updatedAt = at;
|
||||
}
|
||||
}
|
||||
return { alive, lost };
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user