@mseep/anything-analyzer 3.6.50

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.
Files changed (172) hide show
  1. package/.codeartsdoer/.codebaseignore +0 -0
  2. package/.codeartsdoer/AGENTS.md +12 -0
  3. package/.github/workflows/build.yml +146 -0
  4. package/README.en.md +264 -0
  5. package/README.md +276 -0
  6. package/RELEASE_NOTES.md +16 -0
  7. package/USAGE.md +490 -0
  8. package/color-preview-r3.html +414 -0
  9. package/color-preview.html +414 -0
  10. package/dev-app-update.yml +3 -0
  11. package/electron-builder.yml +36 -0
  12. package/electron.vite.config.ts +40 -0
  13. package/package.json +53 -0
  14. package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
  15. package/resources/doloffer-logo.png +0 -0
  16. package/resources/entitlements.mac.plist +12 -0
  17. package/resources/icon.ico +0 -0
  18. package/resources/icon.png +0 -0
  19. package/src/main/ai/ai-analyzer.ts +517 -0
  20. package/src/main/ai/crypto-script-extractor.ts +206 -0
  21. package/src/main/ai/data-assembler.ts +205 -0
  22. package/src/main/ai/llm-router.ts +1120 -0
  23. package/src/main/ai/prompt-builder.ts +349 -0
  24. package/src/main/ai/scene-detector.ts +302 -0
  25. package/src/main/capture/capture-engine.ts +130 -0
  26. package/src/main/capture/interaction-recorder.ts +171 -0
  27. package/src/main/capture/js-injector.ts +57 -0
  28. package/src/main/capture/replay-engine.ts +256 -0
  29. package/src/main/capture/storage-collector.ts +76 -0
  30. package/src/main/cdp/cdp-manager.ts +233 -0
  31. package/src/main/db/database.ts +41 -0
  32. package/src/main/db/migrations.ts +235 -0
  33. package/src/main/db/repositories.ts +574 -0
  34. package/src/main/fingerprint/http-spoofing.ts +48 -0
  35. package/src/main/fingerprint/presets.ts +173 -0
  36. package/src/main/fingerprint/profile-generator.ts +115 -0
  37. package/src/main/fingerprint/profile-store.ts +52 -0
  38. package/src/main/index.ts +260 -0
  39. package/src/main/ipc.ts +856 -0
  40. package/src/main/logger.ts +42 -0
  41. package/src/main/mcp/mcp-config.ts +66 -0
  42. package/src/main/mcp/mcp-manager.ts +155 -0
  43. package/src/main/mcp/mcp-server.ts +1038 -0
  44. package/src/main/prompt-templates.ts +170 -0
  45. package/src/main/proxy/ca-manager.ts +204 -0
  46. package/src/main/proxy/cert-download-page.ts +171 -0
  47. package/src/main/proxy/cert-installer.ts +242 -0
  48. package/src/main/proxy/mitm-proxy-config.ts +37 -0
  49. package/src/main/proxy/mitm-proxy-server.ts +1085 -0
  50. package/src/main/proxy/system-proxy.ts +248 -0
  51. package/src/main/session/session-manager.ts +724 -0
  52. package/src/main/tab-manager.ts +582 -0
  53. package/src/main/updater.ts +111 -0
  54. package/src/main/window.ts +235 -0
  55. package/src/preload/hook-script.ts +270 -0
  56. package/src/preload/index.ts +211 -0
  57. package/src/preload/interaction-hook.ts +286 -0
  58. package/src/preload/stealth-script.ts +302 -0
  59. package/src/preload/target-preload.ts +15 -0
  60. package/src/renderer/App.tsx +656 -0
  61. package/src/renderer/components/AiLogDetail.tsx +173 -0
  62. package/src/renderer/components/AiLogList.tsx +101 -0
  63. package/src/renderer/components/AiLogView.module.css +364 -0
  64. package/src/renderer/components/AiLogView.tsx +86 -0
  65. package/src/renderer/components/AnalyzeBar.module.css +79 -0
  66. package/src/renderer/components/AnalyzeBar.tsx +104 -0
  67. package/src/renderer/components/BrowserPanel.module.css +67 -0
  68. package/src/renderer/components/BrowserPanel.tsx +90 -0
  69. package/src/renderer/components/ControlBar.module.css +47 -0
  70. package/src/renderer/components/ControlBar.tsx +205 -0
  71. package/src/renderer/components/HookLog.tsx +132 -0
  72. package/src/renderer/components/InteractionLog.tsx +183 -0
  73. package/src/renderer/components/MCPServerModal.tsx +427 -0
  74. package/src/renderer/components/PromptTemplateModal.tsx +254 -0
  75. package/src/renderer/components/ReportView.module.css +413 -0
  76. package/src/renderer/components/ReportView.tsx +429 -0
  77. package/src/renderer/components/RequestDetail.module.css +191 -0
  78. package/src/renderer/components/RequestDetail.tsx +202 -0
  79. package/src/renderer/components/RequestLog.module.css +69 -0
  80. package/src/renderer/components/RequestLog.tsx +208 -0
  81. package/src/renderer/components/SessionList.module.css +245 -0
  82. package/src/renderer/components/SessionList.tsx +247 -0
  83. package/src/renderer/components/SettingsModal.tsx +100 -0
  84. package/src/renderer/components/StatusBar.module.css +44 -0
  85. package/src/renderer/components/StatusBar.tsx +102 -0
  86. package/src/renderer/components/StorageView.module.css +41 -0
  87. package/src/renderer/components/StorageView.tsx +178 -0
  88. package/src/renderer/components/TabBar.module.css +88 -0
  89. package/src/renderer/components/TabBar.tsx +70 -0
  90. package/src/renderer/components/Titlebar.module.css +254 -0
  91. package/src/renderer/components/Titlebar.tsx +169 -0
  92. package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
  93. package/src/renderer/components/settings/GeneralSection.tsx +164 -0
  94. package/src/renderer/components/settings/LLMSection.tsx +148 -0
  95. package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
  96. package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
  97. package/src/renderer/components/settings/ProxySection.tsx +110 -0
  98. package/src/renderer/css-modules.d.ts +4 -0
  99. package/src/renderer/hooks/useCapture.ts +383 -0
  100. package/src/renderer/hooks/useConfirm.tsx +91 -0
  101. package/src/renderer/hooks/useSession.ts +136 -0
  102. package/src/renderer/hooks/useTabs.ts +103 -0
  103. package/src/renderer/i18n/en.ts +167 -0
  104. package/src/renderer/i18n/index.ts +47 -0
  105. package/src/renderer/i18n/zh.ts +170 -0
  106. package/src/renderer/index.html +12 -0
  107. package/src/renderer/main.tsx +15 -0
  108. package/src/renderer/styles/global.css +144 -0
  109. package/src/renderer/styles/themes/ayu-dark.css +59 -0
  110. package/src/renderer/styles/themes/catppuccin.css +59 -0
  111. package/src/renderer/styles/themes/discord.css +59 -0
  112. package/src/renderer/styles/themes/dracula.css +59 -0
  113. package/src/renderer/styles/themes/github-dark.css +59 -0
  114. package/src/renderer/styles/themes/gruvbox.css +59 -0
  115. package/src/renderer/styles/themes/index.css +11 -0
  116. package/src/renderer/styles/themes/light.css +59 -0
  117. package/src/renderer/styles/themes/nord.css +59 -0
  118. package/src/renderer/styles/themes/one-dark.css +59 -0
  119. package/src/renderer/styles/themes/tokyo-night.css +59 -0
  120. package/src/renderer/styles/tokens.css +137 -0
  121. package/src/renderer/theme.ts +31 -0
  122. package/src/renderer/ui/Badge.module.css +38 -0
  123. package/src/renderer/ui/Badge.tsx +36 -0
  124. package/src/renderer/ui/Button.module.css +142 -0
  125. package/src/renderer/ui/Button.tsx +46 -0
  126. package/src/renderer/ui/Collapse.module.css +49 -0
  127. package/src/renderer/ui/Collapse.tsx +57 -0
  128. package/src/renderer/ui/CopyableBlock.module.css +56 -0
  129. package/src/renderer/ui/CopyableBlock.tsx +42 -0
  130. package/src/renderer/ui/Empty.module.css +19 -0
  131. package/src/renderer/ui/Empty.tsx +34 -0
  132. package/src/renderer/ui/Icons.tsx +346 -0
  133. package/src/renderer/ui/Input.module.css +103 -0
  134. package/src/renderer/ui/Input.tsx +94 -0
  135. package/src/renderer/ui/InputNumber.module.css +68 -0
  136. package/src/renderer/ui/InputNumber.tsx +104 -0
  137. package/src/renderer/ui/Modal.module.css +83 -0
  138. package/src/renderer/ui/Modal.tsx +67 -0
  139. package/src/renderer/ui/Popconfirm.module.css +73 -0
  140. package/src/renderer/ui/Popconfirm.tsx +74 -0
  141. package/src/renderer/ui/Progress.module.css +35 -0
  142. package/src/renderer/ui/Progress.tsx +30 -0
  143. package/src/renderer/ui/Select.module.css +91 -0
  144. package/src/renderer/ui/Select.tsx +100 -0
  145. package/src/renderer/ui/Spinner.module.css +44 -0
  146. package/src/renderer/ui/Spinner.tsx +27 -0
  147. package/src/renderer/ui/Switch.module.css +39 -0
  148. package/src/renderer/ui/Switch.tsx +43 -0
  149. package/src/renderer/ui/Tabs.module.css +76 -0
  150. package/src/renderer/ui/Tabs.tsx +53 -0
  151. package/src/renderer/ui/Tag.module.css +66 -0
  152. package/src/renderer/ui/Tag.tsx +47 -0
  153. package/src/renderer/ui/Timeline.module.css +42 -0
  154. package/src/renderer/ui/Timeline.tsx +29 -0
  155. package/src/renderer/ui/Toast.module.css +99 -0
  156. package/src/renderer/ui/Toast.tsx +90 -0
  157. package/src/renderer/ui/Tooltip.module.css +26 -0
  158. package/src/renderer/ui/Tooltip.tsx +23 -0
  159. package/src/renderer/ui/VirtualTable.module.css +230 -0
  160. package/src/renderer/ui/VirtualTable.tsx +416 -0
  161. package/src/renderer/ui/index.ts +55 -0
  162. package/src/shared/types.ts +695 -0
  163. package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
  164. package/tests/main/ai/llm-router.test.ts +1537 -0
  165. package/tests/main/ai/prompt-builder.test.ts +178 -0
  166. package/tests/main/ai/scene-detector.test.ts +212 -0
  167. package/tests/main/db/migrations.test.ts +134 -0
  168. package/tests/main/release-workflow.test.ts +59 -0
  169. package/tsconfig.json +7 -0
  170. package/tsconfig.node.json +23 -0
  171. package/tsconfig.web.json +24 -0
  172. package/vitest.config.ts +13 -0
@@ -0,0 +1,170 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { app } from "electron";
4
+ import type { PromptTemplate } from "@shared/types";
5
+
6
+ // Default system prompt shared by all built-in templates
7
+ const DEFAULT_SYSTEM_PROMPT = `你是一位网站协议分析专家。你的任务是分析用户在网站上的操作过程中产生的HTTP请求、JS调用和存储变化,识别其业务场景,并生成结构化的协议分析报告。Be precise and technical. Output in Chinese (Simplified).`;
8
+
9
+ /**
10
+ * Built-in template definitions — used as defaults and for reset.
11
+ */
12
+ function getDefaultTemplates(): PromptTemplate[] {
13
+ return [
14
+ {
15
+ id: "auto",
16
+ name: "自动识别",
17
+ description: "默认 — AI 自动检测场景并生成通用分析",
18
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
19
+ requirements: `1. 场景识别:判断用户执行了什么操作(注册、登录、AI对话、支付等)
20
+ 2. 交互流程概述:按时间顺序描述完整交互链路
21
+ 3. API端点清单:列出所有关键API,标注方法、路径、用途
22
+ 4. 鉴权机制分析:认证方式、凭据获取流程、凭据传递方式
23
+ 5. 流式通信分析(如检测到SSE/WebSocket):协议类型、端点、请求/响应格式
24
+ 6. 存储使用分析:Cookie/localStorage/sessionStorage 的关键变化
25
+ 7. 关键依赖关系:请求之间的依赖和时序关系
26
+ 8. 复现建议:用代码伪逻辑描述如何复现整个流程`,
27
+ isBuiltin: true,
28
+ isModified: false,
29
+ },
30
+ {
31
+ id: "reverse-api",
32
+ name: "逆向 API 协议",
33
+ description: "聚焦 API 端点、请求/响应模式、鉴权流程、数据模型、复现代码",
34
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
35
+ requirements: `1. 完整 API 端点清单:列出所有 API 的方法、路径、请求参数、响应 JSON 结构
36
+ 2. 鉴权流程:Token/Cookie 获取、刷新、传递机制的完整链路
37
+ 3. 请求依赖链:哪些请求的响应是后续请求的必要输入
38
+ 4. 数据模型推断:从 API 响应结构推断后端数据模型
39
+ 5. 复现代码:用 Python requests 库写出可直接运行的完整 API 调用流程`,
40
+ isBuiltin: true,
41
+ isModified: false,
42
+ },
43
+ {
44
+ id: "security-audit",
45
+ name: "安全审计",
46
+ description: "聚焦认证安全、敏感数据暴露、CSRF/XSS 风险、权限控制",
47
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
48
+ requirements: `1. 认证安全:分析认证方式的安全性,是否存在弱口令、明文传输、Token 泄露风险
49
+ 2. 敏感数据暴露:检查响应中是否包含不必要的敏感信息(密码、密钥、PII)
50
+ 3. CSRF/XSS 风险:分析请求是否缺少 CSRF Token,响应头是否缺少安全头(CSP, X-Frame-Options 等)
51
+ 4. 权限控制:分析是否存在越权访问的可能(水平/垂直越权)
52
+ 5. 安全建议:针对发现的问题给出具体修复建议`,
53
+ isBuiltin: true,
54
+ isModified: false,
55
+ },
56
+ {
57
+ id: "performance",
58
+ name: "性能分析",
59
+ description: "聚焦请求时序、冗余请求、资源加载、缓存策略",
60
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
61
+ requirements: `1. 请求时序分析:分析请求的串行/并行关系,识别阻塞链路
62
+ 2. 冗余请求:识别重复或不必要的请求
63
+ 3. 资源优化:分析资源加载顺序,识别可优化的静态资源
64
+ 4. 缓存策略:分析 Cache-Control、ETag 等缓存头的使用情况
65
+ 5. 性能建议:给出具体的性能优化建议和预期收益`,
66
+ isBuiltin: true,
67
+ isModified: false,
68
+ },
69
+ {
70
+ id: "crypto-reverse",
71
+ name: "JS加密逆向",
72
+ description: "聚焦JS加密算法识别、加密流程还原、密钥分析、Python复现代码",
73
+ systemPrompt: DEFAULT_SYSTEM_PROMPT,
74
+ requirements: `1. 加密算法识别:识别所有使用的加密/签名/哈希算法(AES、RSA、SHA、HMAC、SM2/3/4 等),标注具体库和方法名
75
+ 2. 加密流程还原:完整描述每个请求参数的加密 pipeline(明文 → 各步骤 → 密文),画出数据流转图
76
+ 3. 密钥管理分析:密钥来源(硬编码/动态/协商)、密钥格式(Hex/Base64/PEM)、密钥长度
77
+ 4. 签名/校验机制:请求签名的生成算法、参与签名的参数排序规则、时间戳/nonce 机制
78
+ 5. 复现代码:用 Python 写出完整的加密/签名/请求复现代码,确保可直接运行,包含所有必要的密钥和参数`,
79
+ isBuiltin: true,
80
+ isModified: false,
81
+ },
82
+ ];
83
+ }
84
+
85
+ function getTemplatesPath(): string {
86
+ return join(app.getPath("userData"), "prompt-templates.json");
87
+ }
88
+
89
+ /**
90
+ * Load templates from disk. Initializes with defaults if file does not exist.
91
+ */
92
+ export function loadTemplates(): PromptTemplate[] {
93
+ const path = getTemplatesPath();
94
+ if (!existsSync(path)) {
95
+ const defaults = getDefaultTemplates();
96
+ writeFileSync(path, JSON.stringify(defaults, null, 2), "utf-8");
97
+ return defaults;
98
+ }
99
+ try {
100
+ return JSON.parse(readFileSync(path, "utf-8")) as PromptTemplate[];
101
+ } catch {
102
+ const defaults = getDefaultTemplates();
103
+ writeFileSync(path, JSON.stringify(defaults, null, 2), "utf-8");
104
+ return defaults;
105
+ }
106
+ }
107
+
108
+ function persistTemplates(templates: PromptTemplate[]): void {
109
+ writeFileSync(getTemplatesPath(), JSON.stringify(templates, null, 2), "utf-8");
110
+ }
111
+
112
+ /**
113
+ * Save (create or update) a template.
114
+ */
115
+ export function saveTemplate(template: PromptTemplate): void {
116
+ const templates = loadTemplates();
117
+ const idx = templates.findIndex((t) => t.id === template.id);
118
+ if (idx >= 0) {
119
+ // Update existing — mark builtin as modified
120
+ if (templates[idx].isBuiltin) {
121
+ template.isBuiltin = true;
122
+ template.isModified = true;
123
+ }
124
+ templates[idx] = template;
125
+ } else {
126
+ // New custom template
127
+ template.isBuiltin = false;
128
+ template.isModified = false;
129
+ templates.push(template);
130
+ }
131
+ persistTemplates(templates);
132
+ }
133
+
134
+ /**
135
+ * Delete a custom template. Builtin templates cannot be deleted.
136
+ */
137
+ export function deleteTemplate(id: string): void {
138
+ const templates = loadTemplates();
139
+ const target = templates.find((t) => t.id === id);
140
+ if (target?.isBuiltin) {
141
+ throw new Error("Cannot delete builtin template");
142
+ }
143
+ persistTemplates(templates.filter((t) => t.id !== id));
144
+ }
145
+
146
+ /**
147
+ * Reset a builtin template to its default values.
148
+ */
149
+ export function resetTemplate(id: string): void {
150
+ const templates = loadTemplates();
151
+ const defaults = getDefaultTemplates();
152
+ const defaultTemplate = defaults.find((t) => t.id === id);
153
+ if (!defaultTemplate) {
154
+ throw new Error(`No builtin default for template: ${id}`);
155
+ }
156
+ const idx = templates.findIndex((t) => t.id === id);
157
+ if (idx >= 0) {
158
+ templates[idx] = { ...defaultTemplate };
159
+ } else {
160
+ templates.push({ ...defaultTemplate });
161
+ }
162
+ persistTemplates(templates);
163
+ }
164
+
165
+ /**
166
+ * Find a template by ID. Returns undefined if not found.
167
+ */
168
+ export function findTemplate(id: string): PromptTemplate | undefined {
169
+ return loadTemplates().find((t) => t.id === id);
170
+ }
@@ -0,0 +1,204 @@
1
+ import * as forge from "node-forge";
2
+ import * as tls from "tls";
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
4
+ import { join } from "path";
5
+
6
+ const CA_KEY_FILE = "ca-key.pem";
7
+ const CA_CERT_FILE = "ca-cert.pem";
8
+ const CA_VALIDITY_YEARS = 10;
9
+ const LEAF_VALIDITY_DAYS = 825; // Apple max
10
+ const CACHE_MAX_SIZE = 500;
11
+
12
+ /**
13
+ * CaManager — Generates and caches a root CA certificate,
14
+ * then issues per-host leaf certificates on demand for MITM TLS interception.
15
+ */
16
+ export class CaManager {
17
+ private caKey: forge.pki.rsa.KeyPair | null = null;
18
+ private caCert: forge.pki.Certificate | null = null;
19
+ /** LRU-ish cache: hostname → tls.SecureContext */
20
+ private contextCache = new Map<string, tls.SecureContext>();
21
+
22
+ constructor(private certsDir: string) {}
23
+
24
+ /**
25
+ * Load existing CA from disk, or generate a new one.
26
+ */
27
+ async init(): Promise<void> {
28
+ if (!existsSync(this.certsDir)) {
29
+ mkdirSync(this.certsDir, { recursive: true });
30
+ }
31
+
32
+ const keyPath = join(this.certsDir, CA_KEY_FILE);
33
+ const certPath = join(this.certsDir, CA_CERT_FILE);
34
+
35
+ if (existsSync(keyPath) && existsSync(certPath)) {
36
+ const keyPem = readFileSync(keyPath, "utf-8");
37
+ const certPem = readFileSync(certPath, "utf-8");
38
+ const privateKey = forge.pki.privateKeyFromPem(keyPem);
39
+ this.caKey = {
40
+ privateKey,
41
+ publicKey: forge.pki.setRsaPublicKey(privateKey.n, privateKey.e),
42
+ } as forge.pki.rsa.KeyPair;
43
+ this.caCert = forge.pki.certificateFromPem(certPem);
44
+ } else {
45
+ await this.generate();
46
+ }
47
+ }
48
+
49
+ isInitialized(): boolean {
50
+ return this.caCert !== null && this.caKey !== null;
51
+ }
52
+
53
+ getCaCertPath(): string {
54
+ return join(this.certsDir, CA_CERT_FILE);
55
+ }
56
+
57
+ /**
58
+ * Get the CA certificate in DER (binary) format for mobile download.
59
+ */
60
+ getCaCertDer(): Buffer {
61
+ if (!this.caCert) throw new Error("CA not initialized");
62
+ const asn1 = forge.pki.certificateToAsn1(this.caCert);
63
+ const der = forge.asn1.toDer(asn1);
64
+ return Buffer.from(der.getBytes(), "binary");
65
+ }
66
+
67
+ /**
68
+ * Get (or create) a TLS SecureContext for the given hostname.
69
+ */
70
+ getSecureContextForHost(hostname: string): tls.SecureContext {
71
+ const cached = this.contextCache.get(hostname);
72
+ if (cached) return cached;
73
+
74
+ // Evict oldest if cache full
75
+ if (this.contextCache.size >= CACHE_MAX_SIZE) {
76
+ const oldest = this.contextCache.keys().next().value!;
77
+ this.contextCache.delete(oldest);
78
+ }
79
+
80
+ const { key, cert } = this.issueLeafCert(hostname);
81
+ const ctx = tls.createSecureContext({
82
+ key,
83
+ cert,
84
+ ca: forge.pki.certificateToPem(this.caCert!),
85
+ });
86
+ this.contextCache.set(hostname, ctx);
87
+ return ctx;
88
+ }
89
+
90
+ /**
91
+ * Delete existing CA, generate a new one, clear cache.
92
+ */
93
+ async regenerate(): Promise<void> {
94
+ this.contextCache.clear();
95
+ await this.generate();
96
+ }
97
+
98
+ // ---- Private ----
99
+
100
+ private async generate(): Promise<void> {
101
+ const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 });
102
+ this.caKey = keys;
103
+
104
+ const cert = forge.pki.createCertificate();
105
+ cert.publicKey = keys.publicKey;
106
+ cert.serialNumber = this.randomSerial();
107
+
108
+ const now = new Date();
109
+ cert.validity.notBefore = now;
110
+ cert.validity.notAfter = new Date(
111
+ now.getFullYear() + CA_VALIDITY_YEARS,
112
+ now.getMonth(),
113
+ now.getDate(),
114
+ );
115
+
116
+ const attrs: forge.pki.CertificateField[] = [
117
+ { shortName: "CN", value: "Anything Analyzer CA" },
118
+ { shortName: "O", value: "Anything Analyzer" },
119
+ ];
120
+ cert.setSubject(attrs);
121
+ cert.setIssuer(attrs);
122
+
123
+ cert.setExtensions([
124
+ { name: "basicConstraints", cA: true, critical: true },
125
+ {
126
+ name: "keyUsage",
127
+ keyCertSign: true,
128
+ cRLSign: true,
129
+ critical: true,
130
+ },
131
+ {
132
+ name: "subjectKeyIdentifier",
133
+ },
134
+ ]);
135
+
136
+ cert.sign(keys.privateKey, forge.md.sha256.create());
137
+ this.caCert = cert;
138
+
139
+ // Persist
140
+ const keyPem = forge.pki.privateKeyToPem(keys.privateKey);
141
+ const certPem = forge.pki.certificateToPem(cert);
142
+ writeFileSync(join(this.certsDir, CA_KEY_FILE), keyPem, "utf-8");
143
+ writeFileSync(join(this.certsDir, CA_CERT_FILE), certPem, "utf-8");
144
+ }
145
+
146
+ private issueLeafCert(hostname: string): { key: string; cert: string } {
147
+ if (!this.caKey || !this.caCert) {
148
+ throw new Error("CA not initialized");
149
+ }
150
+
151
+ const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 });
152
+ const cert = forge.pki.createCertificate();
153
+
154
+ cert.publicKey = keys.publicKey;
155
+ cert.serialNumber = this.randomSerial();
156
+
157
+ const now = new Date();
158
+ cert.validity.notBefore = now;
159
+ cert.validity.notAfter = new Date(
160
+ now.getFullYear(),
161
+ now.getMonth(),
162
+ now.getDate() + LEAF_VALIDITY_DAYS,
163
+ );
164
+
165
+ cert.setSubject([{ shortName: "CN", value: hostname }]);
166
+ cert.setIssuer(this.caCert.subject.attributes);
167
+
168
+ // SAN: support both DNS name and IP address
169
+ const isIP = /^[\d.]+$/.test(hostname) || hostname.includes(":");
170
+ const altNames: { type: number; value?: string; ip?: string }[] = isIP
171
+ ? [{ type: 7, ip: hostname }]
172
+ : [{ type: 2, value: hostname }];
173
+
174
+ cert.setExtensions([
175
+ { name: "basicConstraints", cA: false },
176
+ {
177
+ name: "keyUsage",
178
+ digitalSignature: true,
179
+ keyEncipherment: true,
180
+ },
181
+ { name: "extKeyUsage", serverAuth: true },
182
+ { name: "subjectAltName", altNames },
183
+ { name: "subjectKeyIdentifier" },
184
+ {
185
+ name: "authorityKeyIdentifier",
186
+ keyIdentifier: true,
187
+ },
188
+ ]);
189
+
190
+ cert.sign(this.caKey.privateKey, forge.md.sha256.create());
191
+
192
+ // Include CA cert in chain so mobile clients can verify
193
+ const leafPem = forge.pki.certificateToPem(cert);
194
+ const caPem = forge.pki.certificateToPem(this.caCert!);
195
+ return {
196
+ key: forge.pki.privateKeyToPem(keys.privateKey),
197
+ cert: leafPem + caPem,
198
+ };
199
+ }
200
+
201
+ private randomSerial(): string {
202
+ return forge.util.bytesToHex(forge.random.getBytesSync(16));
203
+ }
204
+ }
@@ -0,0 +1,171 @@
1
+ import { readFileSync } from "fs";
2
+ import type { CaManager } from "./ca-manager";
3
+
4
+ /** Magic hostname that triggers the cert download page */
5
+ export const CERT_DOWNLOAD_HOST = "cert.anything.test";
6
+ export const CERT_DOWNLOAD_FALLBACK_HOST = "cert.anything.local";
7
+
8
+ export function isCertDownloadHost(host: string): boolean {
9
+ const normalized = host.trim().toLowerCase().replace(/\.$/, "");
10
+ return normalized === CERT_DOWNLOAD_HOST || normalized === CERT_DOWNLOAD_FALLBACK_HOST;
11
+ }
12
+
13
+ /**
14
+ * Detect platform from User-Agent string.
15
+ */
16
+ function detectPlatform(ua: string): "ios" | "android" | "desktop" {
17
+ if (/iPhone|iPad|iPod/i.test(ua)) return "ios";
18
+ if (/Android/i.test(ua)) return "android";
19
+ return "desktop";
20
+ }
21
+
22
+ /**
23
+ * Generate the HTML certificate download page.
24
+ * @param ua User-Agent string for platform detection
25
+ * @param overrideHost Optional host (ip:port or hostname:port) to use for the download link.
26
+ * When a LAN device accesses the proxy directly by IP, this ensures the
27
+ * download link points back to the same accessible address.
28
+ */
29
+ export function generateCertPage(ua: string, overrideHost?: string): string {
30
+ const platform = detectPlatform(ua);
31
+ const defaultHost = platform === "ios" ? CERT_DOWNLOAD_HOST : CERT_DOWNLOAD_FALLBACK_HOST;
32
+ const downloadHost = overrideHost || defaultHost;
33
+ return `<!DOCTYPE html>
34
+ <html lang="zh-CN">
35
+ <head>
36
+ <meta charset="UTF-8">
37
+ <meta name="viewport" content="width=device-width,initial-scale=1">
38
+ <title>Anything Analyzer - 安装 CA 证书</title>
39
+ <style>
40
+ *{margin:0;padding:0;box-sizing:border-box}
41
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;background:#0f172a;color:#e2e8f0;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}
42
+ .card{background:#1e293b;border-radius:16px;padding:32px 28px;max-width:420px;width:100%;box-shadow:0 25px 50px rgba(0,0,0,.4)}
43
+ .logo{text-align:center;margin-bottom:24px}
44
+ .logo svg{width:48px;height:48px}
45
+ h1{font-size:20px;text-align:center;margin-bottom:8px;color:#f8fafc}
46
+ .subtitle{text-align:center;font-size:14px;color:#94a3b8;margin-bottom:28px}
47
+ .download-btn{display:block;width:100%;padding:14px;border:none;border-radius:12px;font-size:16px;font-weight:600;cursor:pointer;text-align:center;text-decoration:none;transition:all .2s;margin-bottom:16px}
48
+ .download-btn.primary{background:linear-gradient(135deg,#3b82f6,#6366f1);color:#fff}
49
+ .download-btn.primary:active{transform:scale(.98);opacity:.9}
50
+ .platform-tag{display:inline-block;background:#334155;color:#94a3b8;padding:3px 10px;border-radius:20px;font-size:12px;margin-bottom:20px}
51
+ .steps{background:#0f172a;border-radius:12px;padding:20px;margin-top:4px}
52
+ .steps h2{font-size:15px;margin-bottom:14px;color:#f1f5f9}
53
+ .step{display:flex;gap:12px;margin-bottom:14px;font-size:13px;line-height:1.6;color:#cbd5e1}
54
+ .step:last-child{margin-bottom:0}
55
+ .step-num{flex-shrink:0;width:22px;height:22px;border-radius:50%;background:#334155;color:#60a5fa;font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;margin-top:1px}
56
+ .note{margin-top:20px;padding:14px;background:rgba(234,179,8,.08);border:1px solid rgba(234,179,8,.2);border-radius:10px;font-size:12px;color:#fbbf24;line-height:1.6}
57
+ .note strong{color:#fcd34d}
58
+ .tabs{display:flex;gap:8px;margin-bottom:16px}
59
+ .tab{flex:1;padding:8px;border:1px solid #334155;border-radius:8px;background:transparent;color:#94a3b8;font-size:13px;cursor:pointer;text-align:center;transition:all .2s}
60
+ .tab.active{background:#334155;color:#f1f5f9;border-color:#475569}
61
+ .tab-content{display:none}
62
+ .tab-content.active{display:block}
63
+ </style>
64
+ </head>
65
+ <body>
66
+ <div class="card">
67
+ <div class="logo">
68
+ <svg viewBox="0 0 48 48" fill="none"><circle cx="24" cy="24" r="22" stroke="#3b82f6" stroke-width="2"/><path d="M24 12v10l7 7" stroke="#60a5fa" stroke-width="2.5" stroke-linecap="round"/><circle cx="24" cy="24" r="4" fill="#3b82f6"/></svg>
69
+ </div>
70
+ <h1>安装 CA 证书</h1>
71
+ <p class="subtitle">Anything Analyzer 需要安装根证书以解密 HTTPS 流量</p>
72
+ <div style="text-align:center"><span class="platform-tag" id="platformTag">${platform === "ios" ? "🍎 iOS" : platform === "android" ? "🤖 Android" : "💻 桌面端"}</span></div>
73
+
74
+ <a class="download-btn primary" href="http://${downloadHost}/cert.cer" id="downloadBtn">⬇ 下载证书</a>
75
+
76
+ <div class="tabs">
77
+ <button class="tab${platform === "ios" ? " active" : ""}" onclick="showTab('ios', this)">iOS</button>
78
+ <button class="tab${platform === "android" ? " active" : ""}" onclick="showTab('android', this)">Android</button>
79
+ <button class="tab${platform === "desktop" ? " active" : ""}" onclick="showTab('desktop', this)">桌面端</button>
80
+ </div>
81
+
82
+ <div id="tab-ios" class="tab-content${platform === "ios" ? " active" : ""}">
83
+ <div class="steps">
84
+ <h2>iOS 安装步骤</h2>
85
+ <div class="step"><span class="step-num">1</span><span>确保 iPhone/iPad 与电脑连接在<strong>同一局域网</strong>下</span></div>
86
+ <div class="step"><span class="step-num">2</span><span>打开 iPhone 的「设置」→「Wi-Fi」→ 点击当前连接的 Wi-Fi 名称右侧的 <strong>ⓘ</strong> 按钮</span></div>
87
+ <div class="step"><span class="step-num">3</span><span>滚动到底部,点击「配置代理」→ 选择「手动」→ 填写服务器为<strong>电脑的 IP 地址</strong>,端口为 <strong>代理端口号</strong>(默认 8888)→ 点击「存储」</span></div>
88
+ <div class="step"><span class="step-num">4</span><span>打开 Safari 浏览器(<strong>必须用 Safari</strong>,其他浏览器无法触发描述文件安装),访问本页面并点击上方「下载证书」按钮</span></div>
89
+ <div class="step"><span class="step-num">5</span><span>弹出「此网站正尝试下载一个配置描述文件,要允许吗?」→ 点击「允许」</span></div>
90
+ <div class="step"><span class="step-num">6</span><span>提示「已下载描述文件」后,打开「设置」→「通用」→「VPN 与设备管理」(或「描述文件与设备管理」)</span></div>
91
+ <div class="step"><span class="step-num">7</span><span>找到「Anything Analyzer CA」描述文件,点击进入 → 点击右上角「安装」→ 输入<strong>锁屏密码</strong> → 再次点击「安装」确认</span></div>
92
+ <div class="step"><span class="step-num">8</span><span><strong>⚠ 关键步骤:</strong>前往「设置」→「通用」→「关于本机」→ 滚动到最底部 → 点击「证书信任设置」</span></div>
93
+ <div class="step"><span class="step-num">9</span><span>在「针对根证书启用完全信任」列表中,找到「Anything Analyzer CA」→ <strong>打开右侧开关</strong> → 弹出警告框点击「继续」</span></div>
94
+ <div class="step"><span class="step-num">10</span><span>✅ 安装完成!打开 Safari 访问任意 HTTPS 网站,验证地址栏无证书警告即为成功</span></div>
95
+ </div>
96
+ <div class="note"><strong>⚠ 常见问题:</strong><br>
97
+ • <strong>找不到「VPN 与设备管理」?</strong> → 不同 iOS 版本名称不同,可能叫「描述文件」或「描述文件与设备管理」<br>
98
+ • <strong>安装后仍报证书错误?</strong> → 99% 是因为没有执行第 8-9 步的「证书信任设置」,这是 iOS 的硬性要求<br>
99
+ • <strong>非 Safari 浏览器下载无反应?</strong> → iOS 仅允许 Safari 安装描述文件,请切换到 Safari 重新下载<br>
100
+ • <strong>使用完毕后</strong> → 记得回到 Wi-Fi 设置将代理改回「关闭」或「自动」</div>
101
+ </div>
102
+
103
+ <div id="tab-android" class="tab-content${platform === "android" ? " active" : ""}">
104
+ <div class="steps">
105
+ <h2>Android 安装步骤</h2>
106
+ <div class="step"><span class="step-num">1</span><span>确保手机与电脑连接在<strong>同一局域网</strong>下</span></div>
107
+ <div class="step"><span class="step-num">2</span><span>打开「设置」→「WLAN」→ <strong>长按</strong>当前连接的 Wi-Fi(或点击齿轮图标)→ 选择「修改网络」</span></div>
108
+ <div class="step"><span class="step-num">3</span><span>展开「高级选项」→ 代理选择「手动」→ 主机名填<strong>电脑的 IP 地址</strong>,端口填 <strong>代理端口号</strong>(默认 8888)→ 保存</span></div>
109
+ <div class="step"><span class="step-num">4</span><span>打开任意浏览器,访问本页面并点击上方「下载证书」按钮,将 .cer 文件保存到手机</span></div>
110
+ <div class="step"><span class="step-num">5</span><span><strong>方式一(推荐):</strong>打开「设置」→「安全」(或「安全和隐私」)→「更多安全设置」→「加密与凭据」→「安装证书」→ 选择「<strong>CA 证书</strong>」</span></div>
111
+ <div class="step"><span class="step-num">6</span><span>系统会弹出安全警告「安装 CA 证书可能允许第三方监控流量...」→ 点击「<strong>仍然安装</strong>」→ 验证指纹/密码/PIN</span></div>
112
+ <div class="step"><span class="step-num">7</span><span>在文件选择器中找到刚下载的 <code>anything-analyzer-ca.cer</code> 文件(通常在 Download 文件夹)→ 点击选择</span></div>
113
+ <div class="step"><span class="step-num">8</span><span>提示「已安装 CA 证书」即为成功</span></div>
114
+ </div>
115
+ <div class="steps" style="margin-top:12px">
116
+ <h2>⚡ 验证证书是否安装成功</h2>
117
+ <div class="step"><span class="step-num">1</span><span>前往「设置」→「安全」→「加密与凭据」→「信任的凭据」→ 切换到「用户」选项卡</span></div>
118
+ <div class="step"><span class="step-num">2</span><span>应能看到「Anything Analyzer CA」→ 点击可查看证书详情和有效期</span></div>
119
+ </div>
120
+ <div class="steps" style="margin-top:12px">
121
+ <h2>🔧 不同 Android 品牌的设置路径</h2>
122
+ <div class="step"><span class="step-num">•</span><span><strong>小米/Redmi (MIUI/HyperOS):</strong>设置 → 密码与安全 → 系统安全 → 加密与凭据 → 安装证书 → CA 证书</span></div>
123
+ <div class="step"><span class="step-num">•</span><span><strong>华为/荣耀 (HarmonyOS):</strong>设置 → 安全 → 更多安全设置 → 加密和凭据 → 从存储设备安装 → CA 证书</span></div>
124
+ <div class="step"><span class="step-num">•</span><span><strong>OPPO/realme (ColorOS):</strong>设置 → 密码与安全 → 系统安全 → 安装证书 → CA 证书</span></div>
125
+ <div class="step"><span class="step-num">•</span><span><strong>vivo (OriginOS/FuntouchOS):</strong>设置 → 安全与隐私 → 更多安全设置 → 加密与凭据 → 安装证书</span></div>
126
+ <div class="step"><span class="step-num">•</span><span><strong>三星 (One UI):</strong>设置 → 生物识别和安全 → 其他安全设置 → 安装证书 → CA 证书</span></div>
127
+ <div class="step"><span class="step-num">•</span><span><strong>Google Pixel (原生):</strong>设置 → 安全 → 加密与凭据 → 安装证书 → CA 证书</span></div>
128
+ <div class="step"><span class="step-num">•</span><span><strong>找不到入口?</strong>在设置中搜索「<strong>证书</strong>」或「<strong>凭据</strong>」关键词即可快速定位</span></div>
129
+ </div>
130
+ <div class="note"><strong>⚠ Android 特别说明:</strong><br>
131
+ • <strong>Android 7.0+ 限制:</strong>用户安装的 CA 证书默认只对<strong>浏览器</strong>有效,大部分 App 不信任用户 CA。这是 Google 的安全策略,属于正常现象<br>
132
+ • <strong>如需让所有 App 信任:</strong>需要 Root 权限 + 将证书移入系统证书目录(/system/etc/security/cacerts/),或使用 Magisk + MoveUserCertificates 模块<br>
133
+ • <strong>安装时要求设置锁屏?</strong>这是 Android 安全要求,安装 CA 证书必须设置 PIN/密码/图案锁屏<br>
134
+ • <strong>使用完毕后</strong> → 记得回到 Wi-Fi 设置将代理改回「无」</div>
135
+ </div>
136
+
137
+ <div id="tab-desktop" class="tab-content${platform === "desktop" ? " active" : ""}">
138
+ <div class="steps">
139
+ <h2>桌面端安装步骤</h2>
140
+ <div class="step"><span class="step-num">1</span><span>推荐直接在 Anything Analyzer 应用内点击「安装证书」按钮一键安装</span></div>
141
+ <div class="step"><span class="step-num">2</span><span>或下载证书后手动双击安装到系统信任存储中(Windows 选择「受信任的根证书颁发机构」)</span></div>
142
+ <div class="step"><span class="step-num">3</span><span>macOS 用户需在「钥匙串访问」中找到证书 → 双击 → 展开「信任」→ 设为「始终信任」</span></div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ <script>
147
+ function showTab(name, el){
148
+ document.querySelectorAll('.tab-content').forEach(e=>e.classList.remove('active'));
149
+ document.querySelectorAll('.tab').forEach(e=>e.classList.remove('active'));
150
+ document.getElementById('tab-'+name).classList.add('active');
151
+ if (el) el.classList.add('active');
152
+ }
153
+ </script>
154
+ </body>
155
+ </html>`;
156
+ }
157
+
158
+ /**
159
+ * Get the CA certificate content as PEM-encoded Buffer for download.
160
+ */
161
+ export function getCertFileContent(caManager: CaManager): Buffer {
162
+ const certPath = caManager.getCaCertPath();
163
+ return readFileSync(certPath);
164
+ }
165
+
166
+ /**
167
+ * Get the CA certificate content as DER-encoded Buffer for mobile download (.cer).
168
+ */
169
+ export function getCertDerContent(caManager: CaManager): Buffer {
170
+ return caManager.getCaCertDer();
171
+ }