feat: add privacy-safe server diagnostics

This commit is contained in:
inman committed 2026-08-30 16:01:42 +08:00
1 parent 2360506607
commit a3963ad0f6
27 files changed
+1305 -112

No files matched your search

+88 -20
View File
@@ -3,6 +3,7 @@ import { request as httpsRequest } from 'node:https';
import { isIP } from 'node:net';
import { basename } from 'node:path';
import { sha256Bytes } from './crypto.js';
import { diagnosticDurationMs } from './diagnostics.js';
const SHA256_PATTERN = /^[a-f0-9]{64}$/i;
const BASE64_PATTERN = /^[A-Za-z0-9+/_-]*={0,2}$/;
@@ -35,6 +36,11 @@ export interface AgentBusInputAttachmentReference {
url: string;
}
export type InputAttachmentDiagnostic = (
event: string,
metadata: Record<string, unknown>
) => void;
export class InputAttachmentError extends Error {
constructor(
public readonly code: string,
@@ -46,6 +52,20 @@ export class InputAttachmentError extends Error {
}
}
function emitAttachmentDiagnostic(
diagnostic: InputAttachmentDiagnostic | undefined,
event: string,
metadata: Record<string, unknown>
): void {
if (!diagnostic) return;
try {
diagnostic(event, metadata);
} catch {
// Diagnostics are deliberately non-authoritative and cannot interrupt
// validation, DNS pinning, downloading, or byte verification.
}
}
function extensionOf(fileName: string): string {
const lower = fileName.toLowerCase();
if (lower.endsWith('.xlsx')) return '.xlsx';
@@ -282,28 +302,76 @@ async function downloadPinnedHttps(
export async function downloadAgentBusInputAttachment(
reference: AgentBusInputAttachmentReference,
maxBytes: number
maxBytes: number,
diagnostic?: InputAttachmentDiagnostic
): Promise<TaskInputAttachmentInput> {
let url = validateAgentBusAttachmentUrl(reference.url);
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects += 1) {
const addresses = await publicAddresses(url.hostname.replace(/^\[|\]$/g, ''));
const response = await downloadPinnedHttps(url, addresses[0], maxBytes);
if (response.statusCode >= 300 && response.statusCode < 400) {
if (!response.location || redirects === MAX_REDIRECTS) {
throw new InputAttachmentError('roster_attachment_redirect_invalid', '名单附件重定向无效或次数过多。');
const startedAt = process.hrtime.bigint();
let redirectCount = 0;
emitAttachmentDiagnostic(diagnostic, 'download_started', {
file_extension: extensionOf(reference.name),
declared_size: reference.size ?? null,
declared_sha256_present: Boolean(reference.sha256),
max_bytes: maxBytes
});
try {
let url = validateAgentBusAttachmentUrl(reference.url);
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects += 1) {
redirectCount = redirects;
const dnsStartedAt = process.hrtime.bigint();
emitAttachmentDiagnostic(diagnostic, 'dns_started', { redirect_count: redirects });
const addresses = await publicAddresses(url.hostname.replace(/^\[|\]$/g, ''));
emitAttachmentDiagnostic(diagnostic, 'dns_validated', {
redirect_count: redirects,
address_count: addresses.length,
address_families: [...new Set(addresses.map((address) => address.family))].sort(),
selected_address_family: addresses[0]?.family || null,
duration_ms: diagnosticDurationMs(dnsStartedAt)
});
const requestStartedAt = process.hrtime.bigint();
const response = await downloadPinnedHttps(url, addresses[0], maxBytes);
emitAttachmentDiagnostic(diagnostic, 'http_response', {
redirect_count: redirects,
status_code: response.statusCode,
response_bytes: response.content.byteLength,
duration_ms: diagnosticDurationMs(requestStartedAt)
});
if (response.statusCode >= 300 && response.statusCode < 400) {
if (!response.location || redirects === MAX_REDIRECTS) {
throw new InputAttachmentError('roster_attachment_redirect_invalid', '名单附件重定向无效或次数过多。');
}
emitAttachmentDiagnostic(diagnostic, 'redirect_followed', {
redirect_count: redirects + 1,
status_code: response.statusCode
});
url = validateAgentBusAttachmentUrl(new URL(response.location, url).toString());
continue;
}
url = validateAgentBusAttachmentUrl(new URL(response.location, url).toString());
continue;
validateAttachmentBytes(response.content, maxBytes, reference.size, reference.sha256);
emitAttachmentDiagnostic(diagnostic, 'download_completed', {
redirect_count: redirects,
byte_size: response.content.byteLength,
declared_size_match: reference.size === undefined || reference.size === response.content.byteLength,
declared_sha256_verified: Boolean(reference.sha256),
duration_ms: diagnosticDurationMs(startedAt)
});
return {
fileName: reference.name,
contentType: response.contentType === 'application/octet-stream' ? reference.contentType : response.contentType,
content: response.content,
declaredSize: reference.size,
declaredSha256: reference.sha256,
source: 'agentbus'
};
}
validateAttachmentBytes(response.content, maxBytes, reference.size, reference.sha256);
return {
fileName: reference.name,
contentType: response.contentType === 'application/octet-stream' ? reference.contentType : response.contentType,
content: response.content,
declaredSize: reference.size,
declaredSha256: reference.sha256,
source: 'agentbus'
};
throw new InputAttachmentError('roster_attachment_download_failed', '名单附件下载失败。');
} catch (error) {
emitAttachmentDiagnostic(diagnostic, 'download_failed', {
redirect_count: redirectCount,
duration_ms: diagnosticDurationMs(startedAt),
error_code: error instanceof InputAttachmentError
? error.code
: 'roster_attachment_download_exception'
});
throw error;
}
throw new InputAttachmentError('roster_attachment_download_failed', '名单附件下载失败。');
}