Compare commits
2 Commits
b23ee8e6d5
...
0adf899437
| Author | SHA1 | Date | |
|---|---|---|---|
| 0adf899437 | |||
|
|
82376fea9d |
@ -1,7 +1,7 @@
|
||||
import { ChangeEvent, ReactNode, useEffect, useMemo, useState } from "react";
|
||||
import { Button } from "../../ui";
|
||||
import { ConnectionAdapter } from "./mockAdapter";
|
||||
import { blankConnection, ConnectionConfig, ConnectionRecord, connectionStates, DiagnosticCategory, DiagnosticResult, maskedSecret, parseImportPreview, toSafeExport, validateConnection } from "./model";
|
||||
import { blankConnection, ConnectionConfig, ConnectionRecord, connectionStates, createConnectionId, DiagnosticCategory, DiagnosticResult, maskedSecret, parseImportPreview, toSafeExport, validateConnection } from "./model";
|
||||
|
||||
const field = (config: ConnectionConfig, onChange: (next: ConnectionConfig) => void) =>
|
||||
<K extends keyof ConnectionConfig>(key: K, value: ConnectionConfig[K]) => onChange({ ...config, [key]: value });
|
||||
@ -103,7 +103,7 @@ export function ConnectionCenter({ adapter, developmentMode = false }: { adapter
|
||||
const importPreview = () => { try { setEditing(parseImportPreview(importText)); setNotice("导入预览已加载;秘密字段已清空,保存前请确认。"); } catch (error) { setNotice(error instanceof Error ? error.message : "导入失败"); } };
|
||||
const exportRecord = (record: ConnectionRecord) => { const safe = JSON.stringify(toSafeExport(record.config), null, 2); setNotice(`安全导出预览(不含 Token、密码和客户端私钥):\n${safe}`); };
|
||||
if (editing) return <ConnectionEditor initial={editing} developmentMode={developmentMode} onCancel={() => setEditing(undefined)} onSave={async (config) => { await adapter.save(config); await refresh(); setSelected(config.id); setEditing(undefined); setNotice("配置已保存到内存 mock adapter;尚未建立网络连接。"); }} />;
|
||||
return <div className="connection-center"><ConnectionList records={records} selectedId={selected} onSelect={setSelected} onCreate={() => setEditing(blankConnection())} onCopy={(record) => setEditing({ ...structuredClone(record.config), id: crypto.randomUUID(), name: `${record.config.name} 副本`, auth: { ...record.config.auth, secret: "" }, proxy: { ...record.config.proxy, password: "" }, tls: { ...record.config.tls, clientKey: "" } })} onExport={exportRecord} onDelete={setDeleting} />
|
||||
return <div className="connection-center"><ConnectionList records={records} selectedId={selected} onSelect={setSelected} onCreate={() => setEditing(blankConnection())} onCopy={(record) => setEditing({ ...structuredClone(record.config), id: createConnectionId(), name: `${record.config.name} 副本`, auth: { ...record.config.auth, secret: "" }, proxy: { ...record.config.proxy, password: "" }, tls: { ...record.config.tls, clientKey: "" } })} onExport={exportRecord} onDelete={setDeleting} />
|
||||
<section className="import-panel"><h3>导入预览</h3><textarea aria-label="导入 JSON" value={importText} onChange={(e: ChangeEvent<HTMLTextAreaElement>) => setImportText(e.target.value)} placeholder="粘贴由本客户端安全导出的 JSON" /><button className="secondary" onClick={importPreview}>验证并预览</button></section>
|
||||
{selectedRecord && <ConnectionDiagnostics record={selectedRecord} adapter={adapter} />}
|
||||
{notice && <pre className="connection-notice" aria-live="polite">{notice}</pre>}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { blankConnection, connectionStates, maskedSecret, parseImportPreview, toSafeExport, validateConnection } from "./model";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { blankConnection, connectionStates, createConnectionId, maskedSecret, parseImportPreview, toSafeExport, validateConnection } from "./model";
|
||||
|
||||
function validConfig() {
|
||||
const config = blankConnection();
|
||||
@ -9,6 +9,11 @@ function validConfig() {
|
||||
}
|
||||
|
||||
describe("连接配置安全边界", () => {
|
||||
it("缺少 crypto.randomUUID 时仍可生成 RFC 4122 格式 ID", () => {
|
||||
vi.stubGlobal("crypto", { getRandomValues: (bytes: Uint8Array) => { bytes.fill(7); return bytes; } });
|
||||
expect(createConnectionId()).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
it("验证地址、URL 凭据及开发模式 TLS 约束", () => {
|
||||
const config = validConfig();
|
||||
config.address = "https://user:password@atlas.example";
|
||||
|
||||
@ -33,8 +33,21 @@ export interface ConnectionRecord { config: ConnectionConfig; state: ConnectionS
|
||||
export type DiagnosticCategory = "network" | "tls" | "authentication" | "version" | "permission" | "rate_limit" | "timeout" | "message_format" | "server";
|
||||
export interface DiagnosticResult { category: DiagnosticCategory; ok: boolean; summary: string; }
|
||||
|
||||
/** 连接配置 ID 不承载安全语义;兼容缺少 randomUUID 的旧 WebView 与非安全 HTTP 上下文。 */
|
||||
export function createConnectionId(): string {
|
||||
const webCrypto = globalThis.crypto;
|
||||
if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
|
||||
const bytes = new Uint8Array(16);
|
||||
if (typeof webCrypto?.getRandomValues === "function") webCrypto.getRandomValues(bytes);
|
||||
else for (let index = 0; index < bytes.length; index += 1) bytes[index] = Math.floor(Math.random() * 256);
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
const value = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
|
||||
}
|
||||
|
||||
export const blankConnection = (): ConnectionConfig => ({
|
||||
id: crypto.randomUUID(), name: "", address: "", transport: "grpc",
|
||||
id: createConnectionId(), name: "", address: "", transport: "grpc",
|
||||
tls: { enabled: true, customCa: "", clientCertificate: "", clientKey: "", fingerprint: "", sni: "", skipVerification: false },
|
||||
auth: { method: "token", username: "", secret: "" },
|
||||
proxy: { enabled: false, url: "", username: "", password: "" },
|
||||
@ -87,7 +100,7 @@ export function parseImportPreview(raw: string): ConnectionConfig {
|
||||
if (input.exportVersion !== 1) throw new Error("不支持的导出版本");
|
||||
const base = blankConnection();
|
||||
const imported: ConnectionConfig = {
|
||||
...base, ...input, id: crypto.randomUUID(),
|
||||
...base, ...input, id: createConnectionId(),
|
||||
tls: { ...base.tls, ...input.tls, clientKey: "" },
|
||||
auth: { ...base.auth, ...input.auth, secret: "" },
|
||||
proxy: { ...base.proxy, ...input.proxy, password: "" },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user