dsh-mobile-web 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-APACHE +202 -202
- package/NOTICE +26 -26
- package/README.md +17 -0
- package/assets/mobile.bundle.js +1259 -326
- package/assets/theme.css +3492 -3439
- package/client.js +468 -141
- package/mobile-src/UPSTREAM.md +58 -50
- package/package.json +1 -1
- package/src/host/assets.js +9 -9
- package/src/host/cleanup.js +380 -0
- package/src/host/config.js +6 -0
- package/src/host/core.js +445 -18
- package/src/host/devices.js +226 -226
- package/src/host/http.js +8 -8
- package/src/host/pages.js +514 -514
- package/src/host/rpc/index.js +32 -32
- package/src/host/rpc/push.js +58 -58
- package/src/host/rpc/settings.js +88 -88
- package/src/host/rpc/subagent.js +27 -27
- package/src/host/state.js +30 -30
package/src/host/devices.js
CHANGED
|
@@ -1,226 +1,226 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 设备表与配对相关:持久化(devices.json)、公网基址、受保护会话清单。
|
|
3
|
-
* 纯文件/JSON 读写(从原 index.js 顶层原样搬出);内存中的设备 Map 与配对码
|
|
4
|
-
* 状态仍由 core 的 createHost 闭包持有,等 RPC 注册表那轮再显式化。
|
|
5
|
-
*/
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
7
|
-
import { randomBytes, randomInt } from 'node:crypto'
|
|
8
|
-
import { homedir } from 'node:os'
|
|
9
|
-
import { join } from 'node:path'
|
|
10
|
-
import { MAX_DEVICES, TOKEN_TTL_MS, COOKIE_NAME } from './config.js'
|
|
11
|
-
export function normalizePublicBase(value) {
|
|
12
|
-
const raw = typeof value === 'string' ? value.trim() : ''
|
|
13
|
-
if (raw === '') return ''
|
|
14
|
-
try {
|
|
15
|
-
const url = new URL(raw)
|
|
16
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
|
|
17
|
-
if (url.hostname === '') return null
|
|
18
|
-
url.hash = ''
|
|
19
|
-
url.search = ''
|
|
20
|
-
return url.toString().replace(/\/+$/, '')
|
|
21
|
-
} catch {
|
|
22
|
-
return null
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function loadPublicBase(file) {
|
|
27
|
-
try {
|
|
28
|
-
if (!existsSync(file)) return ''
|
|
29
|
-
const saved = JSON.parse(readFileSync(file, 'utf8'))
|
|
30
|
-
const raw = saved && typeof saved === 'object' ? saved.publicBaseUrl : saved
|
|
31
|
-
const normalized = normalizePublicBase(raw)
|
|
32
|
-
return normalized === null ? '' : normalized
|
|
33
|
-
} catch {
|
|
34
|
-
return ''
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function savePublicBase(file, value) {
|
|
39
|
-
try {
|
|
40
|
-
const dir = join(file, '..')
|
|
41
|
-
if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
42
|
-
writeFileSync(file, JSON.stringify({ publicBaseUrl: value }, null, 2))
|
|
43
|
-
} catch (err) {
|
|
44
|
-
console.warn('[dsh-mobile] failed to save public domain:', err && err.message)
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function loadDevices(file) {
|
|
49
|
-
const out = new Map()
|
|
50
|
-
try {
|
|
51
|
-
if (!existsSync(file)) return out
|
|
52
|
-
const saved = JSON.parse(readFileSync(file, 'utf8'))
|
|
53
|
-
if (saved && typeof saved === 'object') {
|
|
54
|
-
for (const [id, d] of Object.entries(saved)) {
|
|
55
|
-
if (d && typeof d === 'object' && typeof d.token === 'string') out.set(id, d)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
} catch {}
|
|
59
|
-
return out
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function persistDevices(file, devices) {
|
|
63
|
-
try {
|
|
64
|
-
const dir = join(file, '..')
|
|
65
|
-
if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
66
|
-
const obj = {}
|
|
67
|
-
for (const [id, d] of devices) obj[id] = d
|
|
68
|
-
writeFileSync(file, JSON.stringify(obj, null, 2))
|
|
69
|
-
} catch (err) {
|
|
70
|
-
console.error('[dsh-mobile] failed to persist device store:', err && err.message)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function protectedSessionsFile() {
|
|
75
|
-
return join(homedir(), '.dsh', 'dsh-mobile-protected.json')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function loadProtectedSessionIds() {
|
|
79
|
-
try {
|
|
80
|
-
const raw = readFileSync(protectedSessionsFile(), 'utf8')
|
|
81
|
-
const parsed = JSON.parse(raw)
|
|
82
|
-
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed.sessionIds) ? parsed.sessionIds : []
|
|
83
|
-
return new Set(list.filter((id) => typeof id === 'string' && id !== ''))
|
|
84
|
-
} catch {
|
|
85
|
-
return new Set()
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* 有状态的设备/配对运行时:设备表与配对码都在 state bag 上,持久化复用本模块的 persistDevices。
|
|
92
|
-
* core 创建一次后解构使用(纯搬迁,行为不变)。
|
|
93
|
-
*/
|
|
94
|
-
export function createDeviceAuth({ state, cfg }) {
|
|
95
|
-
const devices = state.devices
|
|
96
|
-
const cookieName = COOKIE_NAME
|
|
97
|
-
function deviceTokensOf(req) {
|
|
98
|
-
const header = req.headers.cookie
|
|
99
|
-
if (!header) return []
|
|
100
|
-
const out = []
|
|
101
|
-
for (const part of String(header).split(';')) {
|
|
102
|
-
const i = part.indexOf('=')
|
|
103
|
-
if (i < 0) continue
|
|
104
|
-
if (part.slice(0, i).trim() !== cookieName) continue
|
|
105
|
-
out.push(decodeURIComponent(part.slice(i + 1).trim()))
|
|
106
|
-
}
|
|
107
|
-
return out
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function authenticatedDevice(req) {
|
|
111
|
-
// 容忍历史遗留:Path=/ 与 Path=/m 可能同时存在同名 cookie,任一有效即通过。
|
|
112
|
-
for (const token of deviceTokensOf(req)) {
|
|
113
|
-
for (const [deviceId, d] of devices) {
|
|
114
|
-
if (d.token === token) {
|
|
115
|
-
d.lastSeenAt = Date.now()
|
|
116
|
-
return { deviceId, ...d }
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return null
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function describeDeviceUA(userAgent) {
|
|
124
|
-
const ua = String(userAgent || '')
|
|
125
|
-
const grab = (re) => {
|
|
126
|
-
const hit = re.exec(ua)
|
|
127
|
-
return hit && typeof hit[1] === 'string' ? hit[1] : ''
|
|
128
|
-
}
|
|
129
|
-
let platform = ''
|
|
130
|
-
if (/OpenHarmony|HarmonyOS/i.test(ua)) {
|
|
131
|
-
const v = grab(/OpenHarmony\s+([\d.]+)/i) || grab(/HarmonyOS\s+([\d.]+)/i)
|
|
132
|
-
platform = v === '' ? 'HarmonyOS' : 'HarmonyOS ' + v
|
|
133
|
-
} else if (/Android/i.test(ua)) {
|
|
134
|
-
const v = grab(/Android\s+([\d.]+)/i)
|
|
135
|
-
platform = v === '' ? 'Android' : 'Android ' + v
|
|
136
|
-
} else if (/iPhone|iPad|iPod/i.test(ua)) {
|
|
137
|
-
platform = 'iOS'
|
|
138
|
-
} else if (/Windows NT 10/i.test(ua)) {
|
|
139
|
-
platform = 'Windows'
|
|
140
|
-
} else if (/Macintosh|Mac OS X/i.test(ua)) {
|
|
141
|
-
platform = 'macOS'
|
|
142
|
-
} else if (/Linux/i.test(ua)) {
|
|
143
|
-
platform = 'Linux'
|
|
144
|
-
}
|
|
145
|
-
let browser = ''
|
|
146
|
-
const browsers = [
|
|
147
|
-
[/HuaweiBrowser\/([\d.]+)/i, '华为浏览器'],
|
|
148
|
-
[/Quark\/([\d.]+)/i, '夸克'],
|
|
149
|
-
[/MicroMessenger\/([\d.]+)/i, '微信'],
|
|
150
|
-
[/Edg\/([\d.]+)/i, 'Edge'],
|
|
151
|
-
[/Firefox\/([\d.]+)/i, 'Firefox'],
|
|
152
|
-
[/Chrome\/([\d.]+)/i, 'Chrome'],
|
|
153
|
-
[/Version\/([\d.]+)[\s\S]*Safari/i, 'Safari'],
|
|
154
|
-
]
|
|
155
|
-
for (const [re, name] of browsers) {
|
|
156
|
-
const v = grab(re)
|
|
157
|
-
if (v !== '') {
|
|
158
|
-
browser = name + ' ' + v.split('.')[0]
|
|
159
|
-
break
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
let model = ''
|
|
163
|
-
if (!/OpenHarmony|HarmonyOS/i.test(ua)) {
|
|
164
|
-
if (/iPhone/i.test(ua)) model = 'iPhone'
|
|
165
|
-
else if (/iPad/i.test(ua)) model = 'iPad'
|
|
166
|
-
else if (/Android/i.test(ua)) {
|
|
167
|
-
const hit = /Android[^;)]*;\s*(?:[A-Za-z]{2}-[A-Za-z]{2};\s*)?([^;)]+?)\s*(?:Build\/|\))/i.exec(ua)
|
|
168
|
-
const token = hit && typeof hit[1] === 'string' ? hit[1].replace(/^U;\s*/i, '').trim() : ''
|
|
169
|
-
if (token.length >= 2 && !/^wv$/i.test(token) && !/^Android$/i.test(token)) model = token.slice(0, 40)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
const label = [platform, browser].filter((s) => s !== '').join(' · ')
|
|
173
|
-
return { model, platform, browser, label }
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function createDevice(userAgent) {
|
|
177
|
-
if (devices.size >= MAX_DEVICES) return null
|
|
178
|
-
const now = Date.now()
|
|
179
|
-
const token = randomBytes(24).toString('hex')
|
|
180
|
-
const deviceId = randomBytes(8).toString('hex')
|
|
181
|
-
devices.set(deviceId, {
|
|
182
|
-
token,
|
|
183
|
-
name: '手机设备',
|
|
184
|
-
createdAt: now,
|
|
185
|
-
lastSeenAt: now,
|
|
186
|
-
userAgent: String(userAgent || '').slice(0, 180),
|
|
187
|
-
})
|
|
188
|
-
persistDevices(cfg.devicesFile, devices)
|
|
189
|
-
return { deviceId, token }
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function fixedPairCode() {
|
|
193
|
-
return String(cfg.pairCode == null ? '' : cfg.pairCode).trim()
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function issuePairToken() {
|
|
197
|
-
const fixed = fixedPairCode()
|
|
198
|
-
if (fixed !== '') {
|
|
199
|
-
// 固定码:永不过期,随时可配(不再随机、不再 5 分钟失效)。
|
|
200
|
-
state.pairToken = { code: fixed, expiresAt: Number.MAX_SAFE_INTEGER, fixed: true }
|
|
201
|
-
return state.pairToken
|
|
202
|
-
}
|
|
203
|
-
state.pairToken = { code: String(randomInt(0, 1e6)).padStart(6, '0'), expiresAt: Date.now() + TOKEN_TTL_MS, fixed: false }
|
|
204
|
-
return state.pairToken
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function acceptPairCode(code, userAgent) {
|
|
208
|
-
const input = String(code == null ? '' : code).trim()
|
|
209
|
-
const fixed = fixedPairCode()
|
|
210
|
-
if (fixed !== '') {
|
|
211
|
-
if (input !== fixed) return null
|
|
212
|
-
return createDevice(userAgent)
|
|
213
|
-
}
|
|
214
|
-
const now = Date.now()
|
|
215
|
-
if (!state.pairToken || state.pairToken.code !== input || now > state.pairToken.expiresAt) {
|
|
216
|
-
if (state.pairToken && now > state.pairToken.expiresAt) state.pairToken = null
|
|
217
|
-
return null
|
|
218
|
-
}
|
|
219
|
-
const created = createDevice(userAgent)
|
|
220
|
-
if (created) state.pairToken = null
|
|
221
|
-
return created
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return { deviceTokensOf, authenticatedDevice, describeDeviceUA, createDevice,
|
|
225
|
-
fixedPairCode, issuePairToken, acceptPairCode }
|
|
226
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 设备表与配对相关:持久化(devices.json)、公网基址、受保护会话清单。
|
|
3
|
+
* 纯文件/JSON 读写(从原 index.js 顶层原样搬出);内存中的设备 Map 与配对码
|
|
4
|
+
* 状态仍由 core 的 createHost 闭包持有,等 RPC 注册表那轮再显式化。
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { randomBytes, randomInt } from 'node:crypto'
|
|
8
|
+
import { homedir } from 'node:os'
|
|
9
|
+
import { join } from 'node:path'
|
|
10
|
+
import { MAX_DEVICES, TOKEN_TTL_MS, COOKIE_NAME } from './config.js'
|
|
11
|
+
export function normalizePublicBase(value) {
|
|
12
|
+
const raw = typeof value === 'string' ? value.trim() : ''
|
|
13
|
+
if (raw === '') return ''
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(raw)
|
|
16
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
|
|
17
|
+
if (url.hostname === '') return null
|
|
18
|
+
url.hash = ''
|
|
19
|
+
url.search = ''
|
|
20
|
+
return url.toString().replace(/\/+$/, '')
|
|
21
|
+
} catch {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadPublicBase(file) {
|
|
27
|
+
try {
|
|
28
|
+
if (!existsSync(file)) return ''
|
|
29
|
+
const saved = JSON.parse(readFileSync(file, 'utf8'))
|
|
30
|
+
const raw = saved && typeof saved === 'object' ? saved.publicBaseUrl : saved
|
|
31
|
+
const normalized = normalizePublicBase(raw)
|
|
32
|
+
return normalized === null ? '' : normalized
|
|
33
|
+
} catch {
|
|
34
|
+
return ''
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function savePublicBase(file, value) {
|
|
39
|
+
try {
|
|
40
|
+
const dir = join(file, '..')
|
|
41
|
+
if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
42
|
+
writeFileSync(file, JSON.stringify({ publicBaseUrl: value }, null, 2))
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.warn('[dsh-mobile] failed to save public domain:', err && err.message)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function loadDevices(file) {
|
|
49
|
+
const out = new Map()
|
|
50
|
+
try {
|
|
51
|
+
if (!existsSync(file)) return out
|
|
52
|
+
const saved = JSON.parse(readFileSync(file, 'utf8'))
|
|
53
|
+
if (saved && typeof saved === 'object') {
|
|
54
|
+
for (const [id, d] of Object.entries(saved)) {
|
|
55
|
+
if (d && typeof d === 'object' && typeof d.token === 'string') out.set(id, d)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {}
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function persistDevices(file, devices) {
|
|
63
|
+
try {
|
|
64
|
+
const dir = join(file, '..')
|
|
65
|
+
if (dir && !existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
66
|
+
const obj = {}
|
|
67
|
+
for (const [id, d] of devices) obj[id] = d
|
|
68
|
+
writeFileSync(file, JSON.stringify(obj, null, 2))
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error('[dsh-mobile] failed to persist device store:', err && err.message)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function protectedSessionsFile() {
|
|
75
|
+
return join(homedir(), '.dsh', 'dsh-mobile-protected.json')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function loadProtectedSessionIds() {
|
|
79
|
+
try {
|
|
80
|
+
const raw = readFileSync(protectedSessionsFile(), 'utf8')
|
|
81
|
+
const parsed = JSON.parse(raw)
|
|
82
|
+
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed.sessionIds) ? parsed.sessionIds : []
|
|
83
|
+
return new Set(list.filter((id) => typeof id === 'string' && id !== ''))
|
|
84
|
+
} catch {
|
|
85
|
+
return new Set()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 有状态的设备/配对运行时:设备表与配对码都在 state bag 上,持久化复用本模块的 persistDevices。
|
|
92
|
+
* core 创建一次后解构使用(纯搬迁,行为不变)。
|
|
93
|
+
*/
|
|
94
|
+
export function createDeviceAuth({ state, cfg }) {
|
|
95
|
+
const devices = state.devices
|
|
96
|
+
const cookieName = COOKIE_NAME
|
|
97
|
+
function deviceTokensOf(req) {
|
|
98
|
+
const header = req.headers.cookie
|
|
99
|
+
if (!header) return []
|
|
100
|
+
const out = []
|
|
101
|
+
for (const part of String(header).split(';')) {
|
|
102
|
+
const i = part.indexOf('=')
|
|
103
|
+
if (i < 0) continue
|
|
104
|
+
if (part.slice(0, i).trim() !== cookieName) continue
|
|
105
|
+
out.push(decodeURIComponent(part.slice(i + 1).trim()))
|
|
106
|
+
}
|
|
107
|
+
return out
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function authenticatedDevice(req) {
|
|
111
|
+
// 容忍历史遗留:Path=/ 与 Path=/m 可能同时存在同名 cookie,任一有效即通过。
|
|
112
|
+
for (const token of deviceTokensOf(req)) {
|
|
113
|
+
for (const [deviceId, d] of devices) {
|
|
114
|
+
if (d.token === token) {
|
|
115
|
+
d.lastSeenAt = Date.now()
|
|
116
|
+
return { deviceId, ...d }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function describeDeviceUA(userAgent) {
|
|
124
|
+
const ua = String(userAgent || '')
|
|
125
|
+
const grab = (re) => {
|
|
126
|
+
const hit = re.exec(ua)
|
|
127
|
+
return hit && typeof hit[1] === 'string' ? hit[1] : ''
|
|
128
|
+
}
|
|
129
|
+
let platform = ''
|
|
130
|
+
if (/OpenHarmony|HarmonyOS/i.test(ua)) {
|
|
131
|
+
const v = grab(/OpenHarmony\s+([\d.]+)/i) || grab(/HarmonyOS\s+([\d.]+)/i)
|
|
132
|
+
platform = v === '' ? 'HarmonyOS' : 'HarmonyOS ' + v
|
|
133
|
+
} else if (/Android/i.test(ua)) {
|
|
134
|
+
const v = grab(/Android\s+([\d.]+)/i)
|
|
135
|
+
platform = v === '' ? 'Android' : 'Android ' + v
|
|
136
|
+
} else if (/iPhone|iPad|iPod/i.test(ua)) {
|
|
137
|
+
platform = 'iOS'
|
|
138
|
+
} else if (/Windows NT 10/i.test(ua)) {
|
|
139
|
+
platform = 'Windows'
|
|
140
|
+
} else if (/Macintosh|Mac OS X/i.test(ua)) {
|
|
141
|
+
platform = 'macOS'
|
|
142
|
+
} else if (/Linux/i.test(ua)) {
|
|
143
|
+
platform = 'Linux'
|
|
144
|
+
}
|
|
145
|
+
let browser = ''
|
|
146
|
+
const browsers = [
|
|
147
|
+
[/HuaweiBrowser\/([\d.]+)/i, '华为浏览器'],
|
|
148
|
+
[/Quark\/([\d.]+)/i, '夸克'],
|
|
149
|
+
[/MicroMessenger\/([\d.]+)/i, '微信'],
|
|
150
|
+
[/Edg\/([\d.]+)/i, 'Edge'],
|
|
151
|
+
[/Firefox\/([\d.]+)/i, 'Firefox'],
|
|
152
|
+
[/Chrome\/([\d.]+)/i, 'Chrome'],
|
|
153
|
+
[/Version\/([\d.]+)[\s\S]*Safari/i, 'Safari'],
|
|
154
|
+
]
|
|
155
|
+
for (const [re, name] of browsers) {
|
|
156
|
+
const v = grab(re)
|
|
157
|
+
if (v !== '') {
|
|
158
|
+
browser = name + ' ' + v.split('.')[0]
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
let model = ''
|
|
163
|
+
if (!/OpenHarmony|HarmonyOS/i.test(ua)) {
|
|
164
|
+
if (/iPhone/i.test(ua)) model = 'iPhone'
|
|
165
|
+
else if (/iPad/i.test(ua)) model = 'iPad'
|
|
166
|
+
else if (/Android/i.test(ua)) {
|
|
167
|
+
const hit = /Android[^;)]*;\s*(?:[A-Za-z]{2}-[A-Za-z]{2};\s*)?([^;)]+?)\s*(?:Build\/|\))/i.exec(ua)
|
|
168
|
+
const token = hit && typeof hit[1] === 'string' ? hit[1].replace(/^U;\s*/i, '').trim() : ''
|
|
169
|
+
if (token.length >= 2 && !/^wv$/i.test(token) && !/^Android$/i.test(token)) model = token.slice(0, 40)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const label = [platform, browser].filter((s) => s !== '').join(' · ')
|
|
173
|
+
return { model, platform, browser, label }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function createDevice(userAgent) {
|
|
177
|
+
if (devices.size >= MAX_DEVICES) return null
|
|
178
|
+
const now = Date.now()
|
|
179
|
+
const token = randomBytes(24).toString('hex')
|
|
180
|
+
const deviceId = randomBytes(8).toString('hex')
|
|
181
|
+
devices.set(deviceId, {
|
|
182
|
+
token,
|
|
183
|
+
name: '手机设备',
|
|
184
|
+
createdAt: now,
|
|
185
|
+
lastSeenAt: now,
|
|
186
|
+
userAgent: String(userAgent || '').slice(0, 180),
|
|
187
|
+
})
|
|
188
|
+
persistDevices(cfg.devicesFile, devices)
|
|
189
|
+
return { deviceId, token }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function fixedPairCode() {
|
|
193
|
+
return String(cfg.pairCode == null ? '' : cfg.pairCode).trim()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function issuePairToken() {
|
|
197
|
+
const fixed = fixedPairCode()
|
|
198
|
+
if (fixed !== '') {
|
|
199
|
+
// 固定码:永不过期,随时可配(不再随机、不再 5 分钟失效)。
|
|
200
|
+
state.pairToken = { code: fixed, expiresAt: Number.MAX_SAFE_INTEGER, fixed: true }
|
|
201
|
+
return state.pairToken
|
|
202
|
+
}
|
|
203
|
+
state.pairToken = { code: String(randomInt(0, 1e6)).padStart(6, '0'), expiresAt: Date.now() + TOKEN_TTL_MS, fixed: false }
|
|
204
|
+
return state.pairToken
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function acceptPairCode(code, userAgent) {
|
|
208
|
+
const input = String(code == null ? '' : code).trim()
|
|
209
|
+
const fixed = fixedPairCode()
|
|
210
|
+
if (fixed !== '') {
|
|
211
|
+
if (input !== fixed) return null
|
|
212
|
+
return createDevice(userAgent)
|
|
213
|
+
}
|
|
214
|
+
const now = Date.now()
|
|
215
|
+
if (!state.pairToken || state.pairToken.code !== input || now > state.pairToken.expiresAt) {
|
|
216
|
+
if (state.pairToken && now > state.pairToken.expiresAt) state.pairToken = null
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
const created = createDevice(userAgent)
|
|
220
|
+
if (created) state.pairToken = null
|
|
221
|
+
return created
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { deviceTokensOf, authenticatedDevice, describeDeviceUA, createDevice,
|
|
225
|
+
fixedPairCode, issuePairToken, acceptPairCode }
|
|
226
|
+
}
|
package/src/host/http.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HTTP 基础:响应写入、gzip/brotli、请求体读取、Cookie、回环/局域网地址。
|
|
3
|
-
* 无闭包依赖(从原 index.js 顶层原样搬出)。
|
|
4
|
-
*/
|
|
5
|
-
import { networkInterfaces } from 'node:os'
|
|
6
|
-
import { gzipSync, brotliCompressSync, constants as zlibConstants } from 'node:zlib'
|
|
7
|
-
import { COMPRESSIBLE_TYPE, MIN_COMPRESS_BYTES, SMALL_BODY } from './config.js'
|
|
8
|
-
|
|
1
|
+
/**
|
|
2
|
+
* HTTP 基础:响应写入、gzip/brotli、请求体读取、Cookie、回环/局域网地址。
|
|
3
|
+
* 无闭包依赖(从原 index.js 顶层原样搬出)。
|
|
4
|
+
*/
|
|
5
|
+
import { networkInterfaces } from 'node:os'
|
|
6
|
+
import { gzipSync, brotliCompressSync, constants as zlibConstants } from 'node:zlib'
|
|
7
|
+
import { COMPRESSIBLE_TYPE, MIN_COMPRESS_BYTES, SMALL_BODY } from './config.js'
|
|
8
|
+
|
|
9
9
|
export const compressCache = new WeakMap()
|
|
10
10
|
|
|
11
11
|
export function compressFor(req, headers, body) {
|