fix: add shell-free proxy process manager
This commit is contained in:
@@ -0,0 +1,211 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const nodeFs = require('node:fs');
|
||||||
|
const nodePath = require('node:path');
|
||||||
|
const { spawn: nodeSpawn } = require('node:child_process');
|
||||||
|
|
||||||
|
const HELPER_FILENAME = 'aware-cam-proxy.exe';
|
||||||
|
const MAX_COOKIE_LENGTH = 4096;
|
||||||
|
const MAX_HOST_LENGTH = 253;
|
||||||
|
const DEVICE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
|
const DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||||
|
const ALTA_SUFFIXES = ['.avasecurity.com', '.avigilon.com'];
|
||||||
|
|
||||||
|
class ProxyLaunchError extends Error {
|
||||||
|
constructor(message, code) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ProxyLaunchError';
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDeploymentHost(value) {
|
||||||
|
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_HOST_LENGTH) {
|
||||||
|
throw new ProxyLaunchError('Deployment host is invalid.', 'INVALID_DEPLOYMENT_HOST');
|
||||||
|
}
|
||||||
|
|
||||||
|
const host = value.toLowerCase();
|
||||||
|
const labels = host.split('.');
|
||||||
|
const hasAltaSuffix = ALTA_SUFFIXES.some((suffix) => host.endsWith(suffix));
|
||||||
|
if (!hasAltaSuffix || labels.some((label) => !DNS_LABEL_PATTERN.test(label))) {
|
||||||
|
throw new ProxyLaunchError('Deployment host is invalid.', 'INVALID_DEPLOYMENT_HOST');
|
||||||
|
}
|
||||||
|
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateDeviceId(value) {
|
||||||
|
if (typeof value !== 'string' || value.length !== 36 || !DEVICE_ID_PATTERN.test(value)) {
|
||||||
|
throw new ProxyLaunchError('Device identifier is invalid.', 'INVALID_DEVICE_ID');
|
||||||
|
}
|
||||||
|
return value.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCookie(value) {
|
||||||
|
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_COOKIE_LENGTH) {
|
||||||
|
throw new ProxyLaunchError('Cookie is invalid.', 'INVALID_COOKIE');
|
||||||
|
}
|
||||||
|
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
||||||
|
throw new ProxyLaunchError('Cookie must not contain control characters.', 'INVALID_COOKIE');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function redactMessage(error, secret) {
|
||||||
|
const source = error && typeof error.message === 'string' ? error.message : 'Unknown process error';
|
||||||
|
const withoutSecret = secret ? source.split(secret).join('[REDACTED]') : source;
|
||||||
|
return withoutSecret.replace(/[\u0000-\u001f\u007f]/g, ' ').slice(0, 512);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeMetadata(entry, status = entry.status) {
|
||||||
|
return {
|
||||||
|
processId: entry.processId,
|
||||||
|
deviceId: entry.deviceId,
|
||||||
|
startedAt: entry.startedAt,
|
||||||
|
status
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProxyManager({
|
||||||
|
appDirectory,
|
||||||
|
fs = nodeFs,
|
||||||
|
spawn = nodeSpawn,
|
||||||
|
platform = process.platform,
|
||||||
|
now = Date.now
|
||||||
|
} = {}) {
|
||||||
|
if (platform !== 'win32') {
|
||||||
|
throw new ProxyLaunchError('Proxy helper is supported only on the Windows platform.', 'UNSUPPORTED_PLATFORM');
|
||||||
|
}
|
||||||
|
if (typeof appDirectory !== 'string' || !nodePath.win32.isAbsolute(appDirectory)) {
|
||||||
|
throw new ProxyLaunchError('Approved application directory must be an absolute path.', 'INVALID_APP_DIRECTORY');
|
||||||
|
}
|
||||||
|
if (typeof fs.existsSync !== 'function' || typeof spawn !== 'function' || typeof now !== 'function') {
|
||||||
|
throw new TypeError('Invalid proxy manager dependency.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const helperPath = nodePath.win32.join(appDirectory, HELPER_FILENAME);
|
||||||
|
const trackedChildren = new Map();
|
||||||
|
|
||||||
|
function removeIfOwned(processId, child) {
|
||||||
|
const current = trackedChildren.get(processId);
|
||||||
|
if (current && current.child === child) trackedChildren.delete(processId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchProxy(request) {
|
||||||
|
if (!request || typeof request !== 'object' || Array.isArray(request)) {
|
||||||
|
throw new ProxyLaunchError('Proxy launch request is invalid.', 'INVALID_REQUEST');
|
||||||
|
}
|
||||||
|
|
||||||
|
const deploymentHost = validateDeploymentHost(request.deploymentHost);
|
||||||
|
const deviceId = validateDeviceId(request.deviceId);
|
||||||
|
const cookie = validateCookie(request.cookie);
|
||||||
|
|
||||||
|
if (!fs.existsSync(helperPath)) {
|
||||||
|
throw new ProxyLaunchError('Proxy helper was not found in the approved application directory.', 'HELPER_NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
let child;
|
||||||
|
try {
|
||||||
|
child = spawn(
|
||||||
|
helperPath,
|
||||||
|
['-a', deploymentHost, '-d', deviceId, '-k', cookie],
|
||||||
|
{
|
||||||
|
shell: false,
|
||||||
|
detached: false,
|
||||||
|
stdio: 'ignore',
|
||||||
|
windowsHide: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new ProxyLaunchError(
|
||||||
|
`Failed to launch proxy helper: ${redactMessage(error, cookie)}`,
|
||||||
|
'SPAWN_FAILED'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!child || !Number.isSafeInteger(child.pid) || child.pid <= 0 || typeof child.kill !== 'function') {
|
||||||
|
throw new ProxyLaunchError('Proxy helper did not return a valid child process.', 'INVALID_CHILD_PROCESS');
|
||||||
|
}
|
||||||
|
if (trackedChildren.has(child.pid)) {
|
||||||
|
try {
|
||||||
|
child.kill('SIGTERM');
|
||||||
|
} catch {
|
||||||
|
// The new child is deliberately not tracked when its PID collides.
|
||||||
|
}
|
||||||
|
throw new ProxyLaunchError('Proxy helper returned a process identifier already in use.', 'DUPLICATE_PROCESS_ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
child,
|
||||||
|
processId: child.pid,
|
||||||
|
deviceId,
|
||||||
|
startedAt: now(),
|
||||||
|
status: 'running'
|
||||||
|
};
|
||||||
|
trackedChildren.set(entry.processId, entry);
|
||||||
|
|
||||||
|
if (typeof child.once === 'function') {
|
||||||
|
child.once('exit', () => removeIfOwned(entry.processId, child));
|
||||||
|
child.once('error', () => removeIfOwned(entry.processId, child));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, ...safeMetadata(entry) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopProxy(processId) {
|
||||||
|
if (!Number.isSafeInteger(processId) || processId <= 0) {
|
||||||
|
throw new ProxyLaunchError('Process identifier is invalid.', 'INVALID_PROCESS_ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = trackedChildren.get(processId);
|
||||||
|
if (!entry) return { success: false, processId, status: 'not-tracked' };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const requested = entry.child.kill('SIGTERM');
|
||||||
|
if (!requested) {
|
||||||
|
removeIfOwned(processId, entry.child);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
processId,
|
||||||
|
deviceId: entry.deviceId,
|
||||||
|
status: 'already-exited'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
removeIfOwned(processId, entry.child);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
processId,
|
||||||
|
deviceId: entry.deviceId,
|
||||||
|
status: 'stop-requested'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const permissionDenied = error && (error.code === 'EPERM' || error.code === 'EACCES');
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
processId,
|
||||||
|
deviceId: entry.deviceId,
|
||||||
|
status: permissionDenied ? 'permission-denied' : 'stop-failed',
|
||||||
|
message: permissionDenied
|
||||||
|
? 'Unable to stop the tracked proxy process: permission denied.'
|
||||||
|
: 'Unable to stop the tracked proxy process.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listTrackedProxies() {
|
||||||
|
return Array.from(trackedChildren.values(), (entry) => safeMetadata(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.freeze({ launchProxy, stopProxy, listTrackedProxies });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
HELPER_FILENAME,
|
||||||
|
MAX_COOKIE_LENGTH,
|
||||||
|
ProxyLaunchError,
|
||||||
|
createProxyManager,
|
||||||
|
validateCookie,
|
||||||
|
validateDeploymentHost,
|
||||||
|
validateDeviceId
|
||||||
|
};
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { EventEmitter } = require('node:events');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
|
||||||
|
const MODULE_PATH = '../src/proxy-launch';
|
||||||
|
const APPROVED_DIRECTORY = 'C:\\Program Files\\Alta Proxy Tool';
|
||||||
|
const APPROVED_HELPER = 'C:\\Program Files\\Alta Proxy Tool\\aware-cam-proxy.exe';
|
||||||
|
const VALID_HOST = 'tenant.avasecurity.com';
|
||||||
|
const VALID_DEVICE_ID = '123e4567-e89b-42d3-a456-426614174000';
|
||||||
|
const SYNTHETIC_COOKIE = 'va=HERMES_SENTINEL_SECRET';
|
||||||
|
|
||||||
|
function loadModule() {
|
||||||
|
return require(MODULE_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeChild extends EventEmitter {
|
||||||
|
constructor(pid, killResult = true) {
|
||||||
|
super();
|
||||||
|
this.pid = pid;
|
||||||
|
this.killResult = killResult;
|
||||||
|
this.killCalls = [];
|
||||||
|
this.killError = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
kill(signal) {
|
||||||
|
this.killCalls.push(signal);
|
||||||
|
if (this.killError) throw this.killError;
|
||||||
|
return this.killResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHarness({ helperExists = true, children = [new FakeChild(4101)] } = {}) {
|
||||||
|
const calls = [];
|
||||||
|
let childIndex = 0;
|
||||||
|
const spawn = (...args) => {
|
||||||
|
calls.push(args);
|
||||||
|
return children[childIndex++];
|
||||||
|
};
|
||||||
|
const fsStub = { existsSync: (candidate) => helperExists && candidate === APPROVED_HELPER };
|
||||||
|
const { createProxyManager } = loadModule();
|
||||||
|
const manager = createProxyManager({
|
||||||
|
appDirectory: APPROVED_DIRECTORY,
|
||||||
|
fs: fsStub,
|
||||||
|
spawn,
|
||||||
|
platform: 'win32',
|
||||||
|
now: () => 1_777_777_777_777
|
||||||
|
});
|
||||||
|
return { manager, calls, children };
|
||||||
|
}
|
||||||
|
|
||||||
|
function validRequest(overrides = {}) {
|
||||||
|
return {
|
||||||
|
deploymentHost: VALID_HOST,
|
||||||
|
deviceId: VALID_DEVICE_ID,
|
||||||
|
cookie: SYNTHETIC_COOKIE,
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('exports the proxy manager module', () => {
|
||||||
|
assert.doesNotThrow(() => loadModule());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('launches the approved helper directly with exact argv and shell disabled', () => {
|
||||||
|
const { manager, calls } = createHarness();
|
||||||
|
|
||||||
|
const result = manager.launchProxy(validRequest());
|
||||||
|
|
||||||
|
assert.deepEqual(calls, [[
|
||||||
|
APPROVED_HELPER,
|
||||||
|
['-a', VALID_HOST, '-d', VALID_DEVICE_ID, '-k', SYNTHETIC_COOKIE],
|
||||||
|
{
|
||||||
|
shell: false,
|
||||||
|
detached: false,
|
||||||
|
stdio: 'ignore',
|
||||||
|
windowsHide: false
|
||||||
|
}
|
||||||
|
]]);
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
success: true,
|
||||||
|
processId: 4101,
|
||||||
|
deviceId: VALID_DEVICE_ID,
|
||||||
|
startedAt: 1_777_777_777_777,
|
||||||
|
status: 'running'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes cookie metacharacters literally in argv without invoking a shell', () => {
|
||||||
|
const cookie = 'va=abc&whoami|calc.exe;<>()^%!"`$';
|
||||||
|
const { manager, calls } = createHarness();
|
||||||
|
|
||||||
|
manager.launchProxy(validRequest({ cookie }));
|
||||||
|
|
||||||
|
assert.equal(calls[0][1][5], cookie);
|
||||||
|
assert.equal(calls[0][2].shell, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects the CRLF calc.exe reproducer in every structured input', () => {
|
||||||
|
const { manager, calls } = createHarness();
|
||||||
|
const attacks = [
|
||||||
|
{ deploymentHost: `${VALID_HOST}\r\ncalc.exe` },
|
||||||
|
{ deviceId: `${VALID_DEVICE_ID}\r\ncalc.exe` },
|
||||||
|
{ cookie: `${SYNTHETIC_COOKIE}\r\ncalc.exe` },
|
||||||
|
{ cookie: `${SYNTHETIC_COOKIE}\0calc.exe` }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const attack of attacks) {
|
||||||
|
assert.throws(() => manager.launchProxy(validRequest(attack)), /invalid|must not contain/i);
|
||||||
|
}
|
||||||
|
assert.equal(calls.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects shell metacharacters in deployment hosts and device identifiers', () => {
|
||||||
|
const { manager, calls } = createHarness();
|
||||||
|
for (const deploymentHost of [
|
||||||
|
'tenant.avasecurity.com&calc.exe',
|
||||||
|
'tenant.avasecurity.com|calc.exe',
|
||||||
|
'https://tenant.avasecurity.com',
|
||||||
|
'tenant.avasecurity.com/path',
|
||||||
|
'tenant.avasecurity.com:443'
|
||||||
|
]) {
|
||||||
|
assert.throws(() => manager.launchProxy(validRequest({ deploymentHost })), /deployment host/i);
|
||||||
|
}
|
||||||
|
for (const deviceId of [
|
||||||
|
`${VALID_DEVICE_ID}&calc.exe`,
|
||||||
|
`${VALID_DEVICE_ID}|calc.exe`,
|
||||||
|
'$(calc.exe)'
|
||||||
|
]) {
|
||||||
|
assert.throws(() => manager.launchProxy(validRequest({ deviceId })), /device identifier/i);
|
||||||
|
}
|
||||||
|
assert.equal(calls.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts only strict canonical UUID device identifiers', () => {
|
||||||
|
const invalidIds = [
|
||||||
|
'',
|
||||||
|
'123e4567-e89b-12d3-a456-42661417400',
|
||||||
|
'123e4567e89b42d3a456426614174000',
|
||||||
|
'123e4567-e89b-02d3-a456-426614174000',
|
||||||
|
'123e4567-e89b-42d3-c456-426614174000',
|
||||||
|
'g23e4567-e89b-42d3-a456-426614174000',
|
||||||
|
'a'.repeat(37)
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const deviceId of invalidIds) {
|
||||||
|
const { manager } = createHarness();
|
||||||
|
assert.throws(() => manager.launchProxy(validRequest({ deviceId })), /device identifier/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('accepts only bounded Alta subdomain hostnames from trusted session state', () => {
|
||||||
|
const invalidHosts = [
|
||||||
|
'',
|
||||||
|
'avasecurity.com',
|
||||||
|
'avigilon.com',
|
||||||
|
'evil.example',
|
||||||
|
'tenant.avasecurity.com.evil.example',
|
||||||
|
'.avasecurity.com',
|
||||||
|
'-tenant.avasecurity.com',
|
||||||
|
'tenant..avasecurity.com',
|
||||||
|
'tenant_avasecurity.com',
|
||||||
|
`tenant.${'a'.repeat(240)}.avasecurity.com`
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const deploymentHost of invalidHosts) {
|
||||||
|
const { manager } = createHarness();
|
||||||
|
assert.throws(() => manager.launchProxy(validRequest({ deploymentHost })), /deployment host/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('normalizes a valid Alta hostname to lowercase', () => {
|
||||||
|
const { manager, calls } = createHarness();
|
||||||
|
manager.launchProxy(validRequest({ deploymentHost: 'Tenant.AVIGILON.com' }));
|
||||||
|
assert.equal(calls[0][1][1], 'tenant.avigilon.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('rejects empty, oversized, and control-character cookies', () => {
|
||||||
|
for (const cookie of ['', 'x'.repeat(4097), 'va=abc\nxyz', 'va=abc\rxyz', 'va=abc\0xyz']) {
|
||||||
|
const { manager } = createHarness();
|
||||||
|
assert.throws(() => manager.launchProxy(validRequest({ cookie })), /cookie/i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fails closed when the fixed helper is missing and never spawns', () => {
|
||||||
|
const { manager, calls } = createHarness({ helperExists: false });
|
||||||
|
assert.throws(() => manager.launchProxy(validRequest()), /helper.*not found/i);
|
||||||
|
assert.equal(calls.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('requires an absolute approved application directory and Windows platform', () => {
|
||||||
|
const { createProxyManager } = loadModule();
|
||||||
|
const dependencies = { fs: { existsSync: () => true }, spawn: () => new FakeChild(1) };
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => createProxyManager({ appDirectory: '..\\untrusted', platform: 'win32', ...dependencies }),
|
||||||
|
/absolute/i
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => createProxyManager({ appDirectory: '/opt/apt', platform: 'linux', ...dependencies }),
|
||||||
|
/platform/i
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('redacts the cookie if spawn throws an error containing it', () => {
|
||||||
|
const { createProxyManager } = loadModule();
|
||||||
|
const manager = createProxyManager({
|
||||||
|
appDirectory: APPROVED_DIRECTORY,
|
||||||
|
fs: { existsSync: () => true },
|
||||||
|
platform: 'win32',
|
||||||
|
spawn: () => { throw new Error(`spawn failed for ${SYNTHETIC_COOKIE}`); }
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => manager.launchProxy(validRequest()),
|
||||||
|
(error) => {
|
||||||
|
assert.match(error.message, /spawn failed/);
|
||||||
|
assert.match(error.message, /\[REDACTED\]/);
|
||||||
|
assert.doesNotMatch(error.message, /HERMES_SENTINEL_SECRET/);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tracks only safe process metadata and never exposes or persists cookies', () => {
|
||||||
|
const { manager } = createHarness();
|
||||||
|
manager.launchProxy(validRequest());
|
||||||
|
|
||||||
|
const tracked = manager.listTrackedProxies();
|
||||||
|
assert.deepEqual(tracked, [{
|
||||||
|
processId: 4101,
|
||||||
|
deviceId: VALID_DEVICE_ID,
|
||||||
|
startedAt: 1_777_777_777_777,
|
||||||
|
status: 'running'
|
||||||
|
}]);
|
||||||
|
assert.doesNotMatch(JSON.stringify(tracked), /HERMES_SENTINEL_SECRET/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stopping one tracked process leaves the other tracked process alive', () => {
|
||||||
|
const first = new FakeChild(4101);
|
||||||
|
const second = new FakeChild(4102);
|
||||||
|
const { manager } = createHarness({ children: [first, second] });
|
||||||
|
manager.launchProxy(validRequest());
|
||||||
|
manager.launchProxy(validRequest({ deviceId: '123e4567-e89b-42d3-a456-426614174001' }));
|
||||||
|
|
||||||
|
const result = manager.stopProxy(4101);
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
success: true,
|
||||||
|
processId: 4101,
|
||||||
|
deviceId: VALID_DEVICE_ID,
|
||||||
|
status: 'stop-requested'
|
||||||
|
});
|
||||||
|
assert.deepEqual(first.killCalls, ['SIGTERM']);
|
||||||
|
assert.deepEqual(second.killCalls, []);
|
||||||
|
assert.deepEqual(manager.listTrackedProxies().map((item) => item.processId), [4102]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('never stops an untracked PID', () => {
|
||||||
|
const child = new FakeChild(4101);
|
||||||
|
const { manager } = createHarness({ children: [child] });
|
||||||
|
manager.launchProxy(validRequest());
|
||||||
|
|
||||||
|
assert.deepEqual(manager.stopProxy(9999), {
|
||||||
|
success: false,
|
||||||
|
processId: 9999,
|
||||||
|
status: 'not-tracked'
|
||||||
|
});
|
||||||
|
assert.deepEqual(child.killCalls, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reports already-exited and permission-denied states honestly', () => {
|
||||||
|
const exited = new FakeChild(4101, false);
|
||||||
|
const denied = new FakeChild(4102);
|
||||||
|
denied.killError = Object.assign(new Error('operation not permitted'), { code: 'EPERM' });
|
||||||
|
const { manager } = createHarness({ children: [exited, denied] });
|
||||||
|
manager.launchProxy(validRequest());
|
||||||
|
manager.launchProxy(validRequest({ deviceId: '123e4567-e89b-42d3-a456-426614174001' }));
|
||||||
|
|
||||||
|
assert.deepEqual(manager.stopProxy(4101), {
|
||||||
|
success: true,
|
||||||
|
processId: 4101,
|
||||||
|
deviceId: VALID_DEVICE_ID,
|
||||||
|
status: 'already-exited'
|
||||||
|
});
|
||||||
|
assert.deepEqual(manager.stopProxy(4102), {
|
||||||
|
success: false,
|
||||||
|
processId: 4102,
|
||||||
|
deviceId: '123e4567-e89b-42d3-a456-426614174001',
|
||||||
|
status: 'permission-denied',
|
||||||
|
message: 'Unable to stop the tracked proxy process: permission denied.'
|
||||||
|
});
|
||||||
|
assert.deepEqual(manager.listTrackedProxies().map((item) => item.processId), [4102]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('exit events remove only the matching owned child', () => {
|
||||||
|
const original = new FakeChild(4101);
|
||||||
|
const replacement = new FakeChild(4101);
|
||||||
|
const { manager } = createHarness({ children: [original, replacement] });
|
||||||
|
manager.launchProxy(validRequest());
|
||||||
|
original.emit('exit', 0);
|
||||||
|
manager.launchProxy(validRequest({ deviceId: '123e4567-e89b-42d3-a456-426614174001' }));
|
||||||
|
|
||||||
|
original.emit('exit', 0);
|
||||||
|
|
||||||
|
assert.deepEqual(manager.listTrackedProxies().map((item) => item.processId), [4101]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('source contains no shell launchers, broad process killers, or persistence APIs', () => {
|
||||||
|
const source = fs.readFileSync(path.join(__dirname, '..', 'src', 'proxy-launch.js'), 'utf8');
|
||||||
|
assert.doesNotMatch(source, /\b(?:cmd(?:\.exe)?|powershell|taskkill|pkill|wmic)\b/i);
|
||||||
|
assert.doesNotMatch(source, /\.(?:bat|command)\b/i);
|
||||||
|
assert.doesNotMatch(source, /(?:writeFile|appendFile|mkdtemp|tmpdir)/);
|
||||||
|
assert.match(source, /shell:\s*false/);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user