440 lines
14 KiB
JavaScript
440 lines
14 KiB
JavaScript
/**
|
|
* Pinduoduo Cookie Sync Background Service Worker (Manifest V3)
|
|
* 拼多多 Cookie 自动嗅探与加密保活后台服务 - 带结构化日志系统
|
|
*/
|
|
|
|
try {
|
|
importScripts('crypto.js')
|
|
} catch (e) {
|
|
console.error('导入加密工具失败:', e)
|
|
}
|
|
|
|
const ALARM_NAME = 'pdd_cookie_sync_alarm'
|
|
const MAX_LOGS_LIMIT = 100
|
|
let debounceTimer = null
|
|
|
|
// 设置扩展图标角标状态
|
|
function setExtensionBadge(text, color) {
|
|
chrome.action.setBadgeText({ text })
|
|
if (color) {
|
|
chrome.action.setBadgeBackgroundColor({ color })
|
|
}
|
|
}
|
|
|
|
// 格式化时间戳
|
|
function getNowFormattedTime() {
|
|
const d = new Date()
|
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
const day = String(d.getDate()).padStart(2, '0')
|
|
const h = String(d.getHours()).padStart(2, '0')
|
|
const min = String(d.getMinutes()).padStart(2, '0')
|
|
const s = String(d.getSeconds()).padStart(2, '0')
|
|
return `${m}-${day} ${h}:${min}:${s}`
|
|
}
|
|
|
|
/**
|
|
* 记录扩展运行日志并持久化至 chrome.storage.local
|
|
* @param {'info' | 'warn' | 'error' | 'success'} level 日志级别
|
|
* @param {string} action 动作模块
|
|
* @param {string} message 简要说明
|
|
* @param {any} [details] 附加诊断数据(脱敏安全显示)
|
|
*/
|
|
async function addExtensionLog(level, action, message, details = null) {
|
|
try {
|
|
const timeStr = getNowFormattedTime()
|
|
const entry = {
|
|
id: 'log_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6),
|
|
time: timeStr,
|
|
timestamp: Date.now(),
|
|
level,
|
|
action,
|
|
message,
|
|
details: details ? (typeof details === 'object' ? JSON.stringify(details) : String(details)) : null,
|
|
}
|
|
|
|
const data = await chrome.storage.local.get(['pdd_extension_logs'])
|
|
const existing = Array.isArray(data.pdd_extension_logs) ? data.pdd_extension_logs : []
|
|
const updated = [entry, ...existing].slice(0, MAX_LOGS_LIMIT)
|
|
await chrome.storage.local.set({ pdd_extension_logs: updated })
|
|
|
|
console.log(`[PDD-EXT] [${level.toUpperCase()}] [${action}] ${message}`, details || '')
|
|
} catch (err) {
|
|
console.error('保存扩展日志异常:', err)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取拼多多全量 Cookie 字符串
|
|
* 针对 mms.pinduoduo.com Host-only Cookie、根域名 Cookie、不同路径 Cookie 进行多维深度嗅探与合并去重
|
|
*/
|
|
async function getPddCookieDetails(customCookie = '') {
|
|
if (customCookie && customCookie.trim().length > 10) {
|
|
const trimmed = customCookie.trim()
|
|
const keys = trimmed
|
|
.split(';')
|
|
.map((p) => p.trim().split('=')[0]?.trim())
|
|
.filter(Boolean)
|
|
return {
|
|
cookieStr: trimmed,
|
|
cookieCount: keys.length,
|
|
keys,
|
|
hasPassId: keys.includes('PASS_ID'),
|
|
hasRckk: keys.includes('rckk'),
|
|
hasNano: keys.some((k) => k.includes('nano')),
|
|
isManual: true,
|
|
}
|
|
}
|
|
|
|
try {
|
|
const cookieMap = new Map()
|
|
|
|
// 1. 深度嗅探 MMS 商家后台关键 URL 上下文 (包含 Host-only、Secure、HttpOnly 令牌)
|
|
const targetUrls = [
|
|
'https://mms.pinduoduo.com/',
|
|
'https://mms.pinduoduo.com/orders/list',
|
|
'https://mms.pinduoduo.com/home/',
|
|
'https://mms.pinduoduo.com/mangkhut/mms/recentOrderList',
|
|
]
|
|
|
|
for (const url of targetUrls) {
|
|
try {
|
|
const cookies = await chrome.cookies.getAll({ url })
|
|
if (Array.isArray(cookies)) {
|
|
for (const c of cookies) {
|
|
if (c.name && c.value) {
|
|
cookieMap.set(c.name, c.value)
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn(`读取 URL [${url}] Cookie 异常:`, err)
|
|
}
|
|
}
|
|
|
|
// 2. 补充查询主域名及各子域 Cookie
|
|
const domains = [
|
|
'mms.pinduoduo.com',
|
|
'.mms.pinduoduo.com',
|
|
'pinduoduo.com',
|
|
'.pinduoduo.com',
|
|
]
|
|
|
|
for (const domain of domains) {
|
|
try {
|
|
const cookies = await chrome.cookies.getAll({ domain })
|
|
if (Array.isArray(cookies)) {
|
|
for (const c of cookies) {
|
|
if (c.name && c.value && !cookieMap.has(c.name)) {
|
|
cookieMap.set(c.name, c.value)
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn(`读取 Domain [${domain}] Cookie 异常:`, err)
|
|
}
|
|
}
|
|
|
|
const keys = Array.from(cookieMap.keys())
|
|
const hasPassId = cookieMap.has('PASS_ID')
|
|
const hasRckk = cookieMap.has('rckk')
|
|
const hasNano = keys.some((k) => k.includes('nano'))
|
|
|
|
if (cookieMap.size === 0) {
|
|
return {
|
|
cookieStr: '',
|
|
cookieCount: 0,
|
|
keys: [],
|
|
hasPassId: false,
|
|
hasRckk: false,
|
|
hasNano: false,
|
|
isManual: false,
|
|
}
|
|
}
|
|
|
|
// 拼接成标准 Cookie 请求头字符串
|
|
const cookieStr = Array.from(cookieMap.entries())
|
|
.map(([name, val]) => `${name}=${val}`)
|
|
.join('; ')
|
|
|
|
return {
|
|
cookieStr,
|
|
cookieCount: cookieMap.size,
|
|
keys,
|
|
hasPassId,
|
|
hasRckk,
|
|
hasNano,
|
|
isManual: false,
|
|
}
|
|
} catch (err) {
|
|
console.error('读取拼多多 Cookie 异常:', err)
|
|
return {
|
|
cookieStr: '',
|
|
cookieCount: 0,
|
|
keys: [],
|
|
hasPassId: false,
|
|
hasRckk: false,
|
|
hasNano: false,
|
|
isManual: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
// 核心同步逻辑
|
|
async function syncCookieToServer(isManual = false) {
|
|
const config = await chrome.storage.local.get([
|
|
'serverUrl',
|
|
'shopId',
|
|
'syncToken',
|
|
'manualCookie',
|
|
'lastCookieHash',
|
|
'lastSyncAt',
|
|
])
|
|
|
|
const serverUrl = String(config.serverUrl || '').trim().replace(/\/+$/, '')
|
|
const shopId = String(config.shopId || '').trim()
|
|
const syncToken = String(config.syncToken || '').trim()
|
|
const manualCookie = String(config.manualCookie || '').trim()
|
|
|
|
if (!serverUrl || !shopId || !syncToken) {
|
|
setExtensionBadge('CFG', '#faad14') // 待配置
|
|
await addExtensionLog('warn', '配置检查', '未完成基础配置,缺少服务端地址、店铺标识或配对密钥')
|
|
return {
|
|
success: false,
|
|
message: '请先打开插件弹窗完成服务器地址与店铺配对密钥配置',
|
|
}
|
|
}
|
|
|
|
await addExtensionLog(
|
|
'info',
|
|
'发起同步',
|
|
`${isManual ? '手动点击立即同步' : '后台自动/保活同步'} [店铺: ${shopId}]`,
|
|
{ serverUrl, shopId, isManual },
|
|
)
|
|
|
|
// 1. 读取拼多多 Cookie
|
|
const cookieDetails = await getPddCookieDetails(manualCookie)
|
|
const cookieStr = cookieDetails.cookieStr
|
|
|
|
if (!cookieStr) {
|
|
setExtensionBadge('OFF', '#8c8c8c') // 未登录
|
|
await chrome.storage.local.set({
|
|
lastStatus: 'offline',
|
|
lastMessage: '未在浏览器中检测到拼多多商家后台登录 Cookie',
|
|
})
|
|
await addExtensionLog(
|
|
'error',
|
|
'Cookie嗅探',
|
|
'未在浏览器检测到任何拼多多 Cookie,请先在当前浏览器打开并登录 mms.pinduoduo.com',
|
|
)
|
|
return {
|
|
success: false,
|
|
message: '未检测到拼多多登录 Cookie,请先在当前浏览器中登录 mms.pinduoduo.com 商家后台',
|
|
}
|
|
}
|
|
|
|
await addExtensionLog(
|
|
cookieDetails.hasPassId ? 'info' : 'warn',
|
|
'Cookie嗅探',
|
|
`成功提取 ${cookieDetails.cookieCount} 项 Cookie (含: ${cookieDetails.keys.slice(0, 8).join(', ')}${cookieDetails.keys.length > 8 ? '...' : ''})`,
|
|
{
|
|
totalKeys: cookieDetails.cookieCount,
|
|
hasPassId: cookieDetails.hasPassId,
|
|
hasRckk: cookieDetails.hasRckk,
|
|
hasNano: cookieDetails.hasNano,
|
|
isManual: cookieDetails.isManual,
|
|
},
|
|
)
|
|
|
|
if (!cookieDetails.hasPassId) {
|
|
await addExtensionLog(
|
|
'warn',
|
|
'会话预警',
|
|
'⚠️ 未检测到拼多多核心会话标识 PASS_ID,可能未登录或会话已过期。请在浏览器登录 mms.pinduoduo.com 并刷新后台页面',
|
|
)
|
|
}
|
|
|
|
// 2. 计算 Cookie 哈希并进行指纹去重
|
|
const currentHash = await computeSha256(cookieStr)
|
|
const now = Date.now()
|
|
const lastSyncTime = Number(config.lastSyncAt || 0)
|
|
|
|
// 若非手动强制触发,且 Cookie 未变动,且距离上次同步不足 1 小时,跳过重复上报
|
|
if (!isManual && config.lastCookieHash === currentHash && now - lastSyncTime < 3600_000) {
|
|
await addExtensionLog('info', '指纹比对', 'Cookie 无变动且未到周期,跳过重复网络上报')
|
|
return {
|
|
success: true,
|
|
skipped: true,
|
|
message: 'Cookie 无实质变动,无需重复上报',
|
|
}
|
|
}
|
|
|
|
try {
|
|
// 3. 请求服务端公钥
|
|
const pkUrl = `${serverUrl}/api/v1/pdd/extension/public-key`
|
|
await addExtensionLog('info', '获取公钥', `请求服务端公钥: ${pkUrl}`)
|
|
|
|
const pkResp = await fetch(pkUrl, {
|
|
method: 'GET',
|
|
headers: { Accept: 'application/json' },
|
|
})
|
|
|
|
if (!pkResp.ok) {
|
|
const statusText = await pkResp.text().catch(() => '')
|
|
throw new Error(`获取公钥失败 HTTP ${pkResp.status}: ${statusText || pkResp.statusText}`)
|
|
}
|
|
|
|
const pkJson = await pkResp.json()
|
|
const serverPublicKeySpki = pkJson?.data?.publicKeySpki
|
|
if (!serverPublicKeySpki) {
|
|
throw new Error('服务端返回的 RSA 公钥格式异常 (缺少 publicKeySpki 字段)')
|
|
}
|
|
|
|
// 4. 构建明文载荷
|
|
const payload = {
|
|
shopId,
|
|
syncToken,
|
|
cookie: cookieStr,
|
|
userAgent: navigator.userAgent,
|
|
timestamp: Date.now(),
|
|
nonce: 'ext_' + Math.random().toString(36).slice(2) + Date.now().toString(36),
|
|
}
|
|
|
|
// 5. 执行端到端混合信封加密 (RSA-OAEP + AES-256-GCM)
|
|
await addExtensionLog('info', '信封加密', '使用 RSA-OAEP + AES-256-GCM 封装高强度密文包')
|
|
const envelope = await encryptPayloadWithEnvelope(payload, serverPublicKeySpki)
|
|
|
|
// 6. 安全回传至服务端
|
|
const syncUrl = `${serverUrl}/api/v1/pdd/extension/sync-cookie`
|
|
await addExtensionLog('info', '上传Cookie', `密文已发送至网关: ${syncUrl},等待服务端实测校验...`)
|
|
|
|
const syncResp = await fetch(syncUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Shop-Token': syncToken,
|
|
},
|
|
body: JSON.stringify({ envelope }),
|
|
})
|
|
|
|
const syncJson = await syncResp.json().catch(() => null)
|
|
|
|
if (!syncResp.ok || !syncJson || syncJson.code !== 0) {
|
|
const errMsg = syncJson?.message || `服务端返回错误 HTTP ${syncResp.status}`
|
|
await addExtensionLog('error', '上传失败', `服务端返回校验未通过: ${errMsg}`, {
|
|
httpStatus: syncResp.status,
|
|
code: syncJson?.code,
|
|
detail: syncJson?.data?.detail,
|
|
})
|
|
throw new Error(errMsg)
|
|
}
|
|
|
|
// 同步成功,更新持久化状态
|
|
await chrome.storage.local.set({
|
|
lastCookieHash: currentHash,
|
|
lastSyncAt: now,
|
|
lastStatus: 'ok',
|
|
lastMessage: syncJson.message || '同步成功并已生效',
|
|
totalOrders: syncJson.data?.totalOrders,
|
|
shopName: syncJson.data?.shopName,
|
|
})
|
|
|
|
setExtensionBadge('OK', '#52c41a') // 绿色成功
|
|
await addExtensionLog('success', '同步成功', `Cookie 已通过拼多多网关实测校验并生效 (在售/历史订单数: ${syncJson.data?.totalOrders ?? 0})`, {
|
|
shopName: syncJson.data?.shopName,
|
|
totalOrders: syncJson.data?.totalOrders,
|
|
})
|
|
|
|
return {
|
|
success: true,
|
|
message: syncJson.message || 'Cookie 同步成功!服务端已完成在线校验并持久化生效',
|
|
totalOrders: syncJson.data?.totalOrders,
|
|
shopName: syncJson.data?.shopName,
|
|
}
|
|
} catch (err) {
|
|
const errMsg = err instanceof Error ? err.message : String(err)
|
|
console.error('Cookie 自动同步失败:', errMsg)
|
|
|
|
await chrome.storage.local.set({
|
|
lastStatus: 'error',
|
|
lastMessage: errMsg,
|
|
})
|
|
|
|
setExtensionBadge('ERR', '#ff4d4f') // 红色错误
|
|
return {
|
|
success: false,
|
|
message: errMsg,
|
|
}
|
|
}
|
|
}
|
|
|
|
// 监听拼多多 Cookie 实时变动事件(带防抖延时 3 秒)
|
|
chrome.cookies.onChanged.addListener((changeInfo) => {
|
|
const domain = changeInfo.cookie?.domain || ''
|
|
if (domain.includes('pinduoduo.com')) {
|
|
if (debounceTimer) {
|
|
clearTimeout(debounceTimer)
|
|
}
|
|
debounceTimer = setTimeout(() => {
|
|
syncCookieToServer(false).catch(() => null)
|
|
}, 3000)
|
|
}
|
|
})
|
|
|
|
// 定时任务:每隔 30 分钟进行一次轻量巡检与必要保活上报
|
|
chrome.alarms.onAlarm.addListener((alarm) => {
|
|
if (alarm.name === ALARM_NAME) {
|
|
syncCookieToServer(false).catch(() => null)
|
|
}
|
|
})
|
|
|
|
// 扩展安装或更新时注册定时闹钟
|
|
chrome.runtime.onInstalled.addListener(() => {
|
|
chrome.alarms.create(ALARM_NAME, {
|
|
periodInMinutes: 30,
|
|
})
|
|
addExtensionLog('info', '扩展安装/更新', '拼多多 Cookie 保活扩展 v1.1.0 后台服务启动')
|
|
// 首次运行尝试同步一次
|
|
syncCookieToServer(false).catch(() => null)
|
|
})
|
|
|
|
// 响应来自 popup.js 的前端交互消息
|
|
chrome.runtime.onMessage.addListener((req, _sender, sendResponse) => {
|
|
if (req.action === 'trigger_sync') {
|
|
syncCookieToServer(true).then(sendResponse).catch((err) => {
|
|
sendResponse({ success: false, message: String(err) })
|
|
})
|
|
return true // 保持异步通信通道
|
|
}
|
|
|
|
if (req.action === 'get_status') {
|
|
chrome.storage.local.get(null).then(async (data) => {
|
|
const details = await getPddCookieDetails(data.manualCookie)
|
|
sendResponse({
|
|
hasCookie: Boolean(details.cookieStr && details.cookieStr.length > 20),
|
|
cookieLength: details.cookieStr ? details.cookieStr.length : 0,
|
|
cookieCount: details.cookieCount,
|
|
hasPassId: details.hasPassId,
|
|
hasRckk: details.hasRckk,
|
|
hasNano: details.hasNano,
|
|
storage: data,
|
|
})
|
|
})
|
|
return true
|
|
}
|
|
|
|
if (req.action === 'get_logs') {
|
|
chrome.storage.local.get(['pdd_extension_logs']).then((res) => {
|
|
sendResponse({ logs: Array.isArray(res.pdd_extension_logs) ? res.pdd_extension_logs : [] })
|
|
})
|
|
return true
|
|
}
|
|
|
|
if (req.action === 'clear_logs') {
|
|
chrome.storage.local.set({ pdd_extension_logs: [] }).then(async () => {
|
|
await addExtensionLog('info', '清空日志', '用户已在插件控制台清空历史运行日志')
|
|
sendResponse({ success: true })
|
|
})
|
|
return true
|
|
}
|
|
})
|