feat: support real browser auth import
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# SmartUp Auth Importer
|
||||
|
||||
用于在本机 Chrome/Edge 真实浏览器中通过 Cloudflare 后,把目标站凭证导入 SmartUp。
|
||||
|
||||
## 安装
|
||||
|
||||
1. 打开 Chrome/Edge 扩展管理页。
|
||||
2. 启用“开发者模式”。
|
||||
3. 选择“加载已解压的扩展程序”。
|
||||
4. 选择本目录:`browser-extension/`。
|
||||
|
||||
## 使用
|
||||
|
||||
1. 在 SmartUp 上游认证提取窗口选择“真实浏览器导入”。
|
||||
2. 点击“生成导入码”,复制 SmartUp 地址和导入码。
|
||||
3. 在本机真实浏览器打开 Meow/New-API 页面并完成登录。
|
||||
4. 点击 SmartUp Auth Importer 扩展图标。
|
||||
5. 粘贴 SmartUp 地址和导入码。
|
||||
6. 点击“采集当前页并回填”。
|
||||
7. 回到 SmartUp,等待候选凭证出现后点击“填入当前表单”。
|
||||
|
||||
扩展会采集当前标签页所属域名的 cookie、localStorage、sessionStorage,以及扩展已捕获到的 Authorization / X-API-Key 请求头。
|
||||
@@ -0,0 +1,46 @@
|
||||
const authHeadersByTab = new Map()
|
||||
const AUTH_HEADER_NAMES = new Set(['authorization', 'x-api-key', 'api-key'])
|
||||
|
||||
function rememberHeader(tabId, entry) {
|
||||
if (tabId < 0) return
|
||||
const rows = authHeadersByTab.get(tabId) || []
|
||||
const dedupKey = `${entry.type}:${entry.value}:${entry.url}`
|
||||
if (!rows.some((item) => `${item.type}:${item.value}:${item.url}` === dedupKey)) {
|
||||
rows.unshift(entry)
|
||||
}
|
||||
authHeadersByTab.set(tabId, rows.slice(0, 50))
|
||||
}
|
||||
|
||||
chrome.webRequest.onBeforeSendHeaders.addListener(
|
||||
(details) => {
|
||||
const headers = details.requestHeaders || []
|
||||
for (const header of headers) {
|
||||
const name = String(header.name || '').toLowerCase()
|
||||
const value = String(header.value || '').trim()
|
||||
if (!AUTH_HEADER_NAMES.has(name) || !value) continue
|
||||
rememberHeader(details.tabId, {
|
||||
type: name === 'authorization' ? 'authorization' : 'api_key',
|
||||
value,
|
||||
url: details.url || '',
|
||||
})
|
||||
}
|
||||
},
|
||||
{ urls: ['<all_urls>'] },
|
||||
['requestHeaders', 'extraHeaders'],
|
||||
)
|
||||
|
||||
chrome.tabs.onRemoved.addListener((tabId) => {
|
||||
authHeadersByTab.delete(tabId)
|
||||
})
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message?.type !== 'get-auth-headers') return false
|
||||
const tabId = Number(message.tabId)
|
||||
const origin = String(message.origin || '')
|
||||
const rows = authHeadersByTab.get(tabId) || []
|
||||
const filtered = origin
|
||||
? rows.filter((item) => String(item.url || '').startsWith(origin))
|
||||
: rows
|
||||
sendResponse({ auth_headers: filtered })
|
||||
return true
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "SmartUp Auth Importer",
|
||||
"version": "0.1.0",
|
||||
"description": "Import cookies, storage, and auth headers from a real browser session into SmartUp.",
|
||||
"permissions": [
|
||||
"cookies",
|
||||
"scripting",
|
||||
"tabs",
|
||||
"storage",
|
||||
"webRequest"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_title": "SmartUp Auth Importer"
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>SmartUp Auth Importer</title>
|
||||
<style>
|
||||
body {
|
||||
width: 360px;
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: #1f2937;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin: 10px 0 4px;
|
||||
font-size: 12px;
|
||||
color: #4b5563;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
padding: 9px 10px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: #2563eb;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #6b7280;
|
||||
}
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
min-height: 18px;
|
||||
font-size: 12px;
|
||||
color: #374151;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.ok { color: #15803d; }
|
||||
.err { color: #b91c1c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SmartUp 凭证导入</h1>
|
||||
<label for="smartup">SmartUp 地址</label>
|
||||
<input id="smartup" placeholder="http://127.0.0.1:8899" />
|
||||
<label for="code">导入码</label>
|
||||
<input id="code" placeholder="session_id:secret" />
|
||||
<div class="hint">
|
||||
先在当前标签页完成目标站登录,再点击采集。扩展会读取当前域 cookie、storage 和已捕获的认证请求头。
|
||||
</div>
|
||||
<button id="submit">采集当前页并回填</button>
|
||||
<div id="status" class="status"></div>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,138 @@
|
||||
const smartupInput = document.getElementById('smartup')
|
||||
const codeInput = document.getElementById('code')
|
||||
const submitButton = document.getElementById('submit')
|
||||
const statusEl = document.getElementById('status')
|
||||
|
||||
function setStatus(text, cls = '') {
|
||||
statusEl.textContent = text
|
||||
statusEl.className = `status ${cls}`.trim()
|
||||
}
|
||||
|
||||
function normalizeOrigin(value) {
|
||||
const text = String(value || '').trim().replace(/\/+$/, '')
|
||||
if (!/^https?:\/\//.test(text)) return ''
|
||||
return text
|
||||
}
|
||||
|
||||
function parseImportCode(value) {
|
||||
const parts = String(value || '').trim().split(':')
|
||||
if (parts.length < 2 || !parts[0] || !parts.slice(1).join(':')) return null
|
||||
return {
|
||||
sessionId: parts.shift(),
|
||||
secret: parts.join(':'),
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSavedConfig() {
|
||||
const saved = await chrome.storage.local.get(['smartupOrigin', 'importCode'])
|
||||
smartupInput.value = saved.smartupOrigin || 'http://127.0.0.1:8899'
|
||||
codeInput.value = saved.importCode || ''
|
||||
}
|
||||
|
||||
async function saveConfig(origin, code) {
|
||||
await chrome.storage.local.set({ smartupOrigin: origin, importCode: code })
|
||||
}
|
||||
|
||||
async function getActiveTab() {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
|
||||
if (!tab?.id || !tab.url || !/^https?:\/\//.test(tab.url)) {
|
||||
throw new Error('请切换到已登录的 http/https 目标页面')
|
||||
}
|
||||
return tab
|
||||
}
|
||||
|
||||
async function readPageStorage(tabId) {
|
||||
const [result] = await chrome.scripting.executeScript({
|
||||
target: { tabId },
|
||||
func: () => {
|
||||
const copyStorage = (storage) => {
|
||||
const out = {}
|
||||
for (let i = 0; i < storage.length; i += 1) {
|
||||
const key = storage.key(i)
|
||||
if (key) out[key] = storage.getItem(key) || ''
|
||||
}
|
||||
return out
|
||||
}
|
||||
return {
|
||||
local_storage: copyStorage(window.localStorage),
|
||||
session_storage: copyStorage(window.sessionStorage),
|
||||
}
|
||||
},
|
||||
})
|
||||
return result?.result || { local_storage: {}, session_storage: {} }
|
||||
}
|
||||
|
||||
async function readCookies(url) {
|
||||
const cookies = await chrome.cookies.getAll({ url })
|
||||
return cookies.map((cookie) => ({
|
||||
name: cookie.name,
|
||||
value: cookie.value,
|
||||
domain: cookie.domain,
|
||||
path: cookie.path,
|
||||
httpOnly: Boolean(cookie.httpOnly),
|
||||
secure: Boolean(cookie.secure),
|
||||
}))
|
||||
}
|
||||
|
||||
function getAuthHeaders(tabId, origin) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage({ type: 'get-auth-headers', tabId, origin }, (response) => {
|
||||
resolve(response?.auth_headers || [])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function submitImport() {
|
||||
const smartupOrigin = normalizeOrigin(smartupInput.value)
|
||||
const importCode = codeInput.value.trim()
|
||||
const parsed = parseImportCode(importCode)
|
||||
if (!smartupOrigin) {
|
||||
setStatus('SmartUp 地址必须以 http:// 或 https:// 开头', 'err')
|
||||
return
|
||||
}
|
||||
if (!parsed) {
|
||||
setStatus('导入码格式应为 session_id:secret', 'err')
|
||||
return
|
||||
}
|
||||
|
||||
submitButton.disabled = true
|
||||
setStatus('正在采集当前页凭证…')
|
||||
try {
|
||||
await saveConfig(smartupOrigin, importCode)
|
||||
const tab = await getActiveTab()
|
||||
const pageUrl = tab.url
|
||||
const pageOrigin = new URL(pageUrl).origin
|
||||
const [cookies, storage, authHeaders] = await Promise.all([
|
||||
readCookies(pageUrl),
|
||||
readPageStorage(tab.id),
|
||||
getAuthHeaders(tab.id, pageOrigin),
|
||||
])
|
||||
const response = await fetch(`${smartupOrigin}/api/auth-capture/import-sessions/${parsed.sessionId}/submit`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
secret: parsed.secret,
|
||||
page_url: pageUrl,
|
||||
cookies,
|
||||
local_storage: storage.local_storage || {},
|
||||
session_storage: storage.session_storage || {},
|
||||
auth_headers: authHeaders,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
throw new Error(text || `提交失败:${response.status}`)
|
||||
}
|
||||
setStatus(`已提交:${cookies.length} 个 cookie,${authHeaders.length} 个认证请求头`, 'ok')
|
||||
} catch (error) {
|
||||
setStatus(error?.message || '提交失败', 'err')
|
||||
} finally {
|
||||
submitButton.disabled = false
|
||||
}
|
||||
}
|
||||
|
||||
submitButton.addEventListener('click', () => {
|
||||
void submitImport()
|
||||
})
|
||||
|
||||
void loadSavedConfig()
|
||||
Reference in New Issue
Block a user