Update Harbor client and gateway integration workflows
This commit is contained in:
@@ -84,12 +84,33 @@ interface DomainTrafficSnapshot {
|
||||
}>;
|
||||
}
|
||||
|
||||
interface ActivityEntry {
|
||||
device: string;
|
||||
service: string;
|
||||
upload: bigint;
|
||||
download: bigint;
|
||||
}
|
||||
|
||||
interface ActivitySample {
|
||||
at: number;
|
||||
entries: ActivityEntry[];
|
||||
}
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: {};
|
||||
}
|
||||
|
||||
function publicDeviceLabel(value: unknown) {
|
||||
const device = record(value);
|
||||
for (const candidate of [device.alias, device.hostname]) {
|
||||
const label = typeof candidate === 'string' ? candidate.trim() : '';
|
||||
if (label && label.length <= 64 && !/[\/?#@\\]/.test(label) && !net.isIP(label)) return label;
|
||||
}
|
||||
return 'Устройство';
|
||||
}
|
||||
|
||||
const matchesDomain = (domain: string, suffix: string) => domain === suffix || domain.endsWith(`.${suffix}`);
|
||||
|
||||
export function classifyDomain(value: unknown): { domain: string; service: string } | null {
|
||||
@@ -209,6 +230,10 @@ export function createDomainTrafficService({
|
||||
unsupported_source: 0n,
|
||||
};
|
||||
let refreshPromise: Promise<DomainTrafficSnapshot> | null = null;
|
||||
let activityEnabled = false;
|
||||
let activityStartedAt = 0;
|
||||
let activitySamples: ActivitySample[] = [];
|
||||
let quietSince: string | null = null;
|
||||
let current: DomainTrafficSnapshot = {
|
||||
epoch,
|
||||
observedAt: null,
|
||||
@@ -270,6 +295,7 @@ export function createDomainTrafficService({
|
||||
const response = record(await observe());
|
||||
if (!Array.isArray(response.connections)) throw new Error('Sing-box не вернул connections array');
|
||||
const devicesByIp = new Map<string, string | null>();
|
||||
const deviceLabels = new Map<string, string>();
|
||||
const observedDevices = devices();
|
||||
for (const value of Array.isArray(observedDevices) ? observedDevices : []) {
|
||||
const device = record(value);
|
||||
@@ -277,9 +303,11 @@ export function createDomainTrafficService({
|
||||
const id = typeof device.mac === 'string' ? deviceId(device.mac.toLowerCase()) : null;
|
||||
if (!net.isIPv4(ip) || !id) continue;
|
||||
devicesByIp.set(ip, devicesByIp.has(ip) ? null : id);
|
||||
deviceLabels.set(id, publicDeviceLabel(device));
|
||||
}
|
||||
const connections = response.connections.map((connection) => parseConnection(connection, devicesByIp));
|
||||
const activeConnections = new Map<string, PreviousConnection>();
|
||||
const activityEntries: ActivityEntry[] = [];
|
||||
for (const connection of connections) {
|
||||
const previous = previousConnections.get(connection.id);
|
||||
if (connection.outcome !== 'classified' && previous?.outcome !== connection.outcome) {
|
||||
@@ -302,6 +330,16 @@ export function createDomainTrafficService({
|
||||
tracked.uploadBytes += uploadDelta;
|
||||
tracked.downloadBytes += downloadDelta;
|
||||
trackedTotals.set(trackedKey, tracked);
|
||||
if (activityEnabled && connection.outbound === 'vpn' && uploadDelta + downloadDelta > 0n) {
|
||||
activityEntries.push({
|
||||
device: 'deviceId' in connection
|
||||
? deviceLabels.get(connection.deviceId) || 'Устройство'
|
||||
: 'Неизвестное устройство',
|
||||
service: 'service' in connection ? connection.service : 'Не распознано',
|
||||
upload: uploadDelta,
|
||||
download: downloadDelta,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (connection.outcome === 'unknown_device' || connection.outcome === 'unsupported_source') {
|
||||
activeConnections.set(connection.id, {
|
||||
@@ -372,7 +410,12 @@ export function createDomainTrafficService({
|
||||
});
|
||||
}
|
||||
previousConnections = activeConnections;
|
||||
current = { ...current, observedAt: now().toISOString() };
|
||||
const observed = now();
|
||||
if (activityEnabled) {
|
||||
activitySamples.push({ at: observed.getTime(), entries: activityEntries });
|
||||
activitySamples = activitySamples.filter(({ at }) => at >= observed.getTime() - 10_000);
|
||||
}
|
||||
current = { ...current, observedAt: observed.toISOString() };
|
||||
current = buildSnapshot();
|
||||
return current;
|
||||
} catch (error) {
|
||||
@@ -390,5 +433,64 @@ export function createDomainTrafficService({
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
return { snapshot: () => current, refresh };
|
||||
function enableActivity() {
|
||||
if (activityEnabled) return;
|
||||
activityEnabled = true;
|
||||
activityStartedAt = now().getTime();
|
||||
activitySamples = [];
|
||||
quietSince = null;
|
||||
}
|
||||
|
||||
function disableActivity() {
|
||||
activityEnabled = false;
|
||||
activityStartedAt = 0;
|
||||
activitySamples = [];
|
||||
quietSince = null;
|
||||
}
|
||||
|
||||
function activitySnapshot(thresholdBytesPerSecond: unknown = 0) {
|
||||
if (!activityEnabled || !current.observedAt) return null;
|
||||
const observedAt = Date.parse(current.observedAt);
|
||||
const threshold = Math.max(0, Number(thresholdBytesPerSecond) || 0);
|
||||
const divisorMs = Math.max(1_000, Math.min(10_000, observedAt - activityStartedAt || 1_000));
|
||||
const totals = new Map<string, ActivityEntry>();
|
||||
let bytes = 0n;
|
||||
for (const sample of activitySamples) {
|
||||
for (const entry of sample.entries) {
|
||||
bytes += entry.upload + entry.download;
|
||||
const key = `${entry.device}\0${entry.service}`;
|
||||
const total = totals.get(key) || { ...entry, upload: 0n, download: 0n };
|
||||
total.upload += entry.upload;
|
||||
total.download += entry.download;
|
||||
totals.set(key, total);
|
||||
}
|
||||
}
|
||||
const totalBytesPerSecond = Number(bytes * 1_000n / BigInt(divisorMs));
|
||||
const active = totalBytesPerSecond > threshold;
|
||||
quietSince = active ? null : quietSince || current.observedAt;
|
||||
const latest = activitySamples.at(-1);
|
||||
return {
|
||||
state: active ? 'active' : 'quiet',
|
||||
observedAt: current.observedAt,
|
||||
windowMs: 10_000,
|
||||
thresholdBytesPerSecond: threshold,
|
||||
totalBytesPerSecond,
|
||||
transmittingConnections: latest?.entries.length || 0,
|
||||
quietSince,
|
||||
blockers: [...totals.values()]
|
||||
.map((entry) => ({
|
||||
device: entry.device,
|
||||
service: entry.service,
|
||||
uploadBytesPerSecond: Number(entry.upload * 1_000n / BigInt(divisorMs)),
|
||||
downloadBytesPerSecond: Number(entry.download * 1_000n / BigInt(divisorMs)),
|
||||
}))
|
||||
.sort((left, right) => (
|
||||
right.uploadBytesPerSecond + right.downloadBytesPerSecond
|
||||
- left.uploadBytesPerSecond - left.downloadBytesPerSecond
|
||||
))
|
||||
.slice(0, 3),
|
||||
};
|
||||
}
|
||||
|
||||
return { snapshot: () => current, refresh, enableActivity, disableActivity, activitySnapshot };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user