@jobshimo/browser-link 0.5.1 → 0.5.2
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.
|
@@ -3,16 +3,32 @@ export interface PeerProcess {
|
|
|
3
3
|
binaryName: string;
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Identify which OS process owns the
|
|
7
|
-
*
|
|
6
|
+
* Identify which OS process owns the LOCAL end of a TCP connection at
|
|
7
|
+
* `host:port`. Returns null when the lookup cannot resolve a single owner
|
|
8
8
|
* (no result, ambiguous result, command not available, timeout).
|
|
9
9
|
*
|
|
10
10
|
* The caller decides how to treat null: this module's only job is to ask the
|
|
11
11
|
* kernel honestly. Concretely the server treats null as "reject", so users
|
|
12
12
|
* on systems without lsof / netstat fail closed.
|
|
13
|
+
*
|
|
14
|
+
* Defense in depth: even if the OS lookup somehow points at our own process,
|
|
15
|
+
* we return null. The auth path must never identify the server as its own
|
|
16
|
+
* peer — that would short-circuit the allowlist check.
|
|
13
17
|
*/
|
|
14
18
|
export declare function lookupPeerProcess(host: string, port: number): Promise<PeerProcess | null>;
|
|
15
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Parse `lsof -F pcn` output and return the unique process whose LOCAL
|
|
21
|
+
* endpoint equals `host:port`. Returns null on:
|
|
22
|
+
* - empty output
|
|
23
|
+
* - no entry matching the local endpoint
|
|
24
|
+
* - two or more distinct PIDs claiming the same local endpoint (fail closed)
|
|
25
|
+
* - malformed records (missing pid/command/name)
|
|
26
|
+
*
|
|
27
|
+
* Output shape: every process group starts with `p<pid>`, followed by
|
|
28
|
+
* `c<command>` (one), then one or more `n<local>-><remote>` lines (one per
|
|
29
|
+
* matching socket). Names can contain spaces; lsof escapes them as `\xHH`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseLsofOutput(out: string, host: string, port: number): PeerProcess | null;
|
|
16
32
|
/** lsof escapes spaces and tabs in command names as \xHH. Reverse that. */
|
|
17
33
|
export declare function decodeLsofString(s: string): string;
|
|
18
34
|
export declare function parseNetstatForLocal(out: string, host: string, port: number): number | null;
|
|
@@ -4,53 +4,111 @@ import { promisify } from 'node:util';
|
|
|
4
4
|
const execAsync = promisify(exec);
|
|
5
5
|
const LOOKUP_TIMEOUT_MS = 1500;
|
|
6
6
|
/**
|
|
7
|
-
* Identify which OS process owns the
|
|
8
|
-
*
|
|
7
|
+
* Identify which OS process owns the LOCAL end of a TCP connection at
|
|
8
|
+
* `host:port`. Returns null when the lookup cannot resolve a single owner
|
|
9
9
|
* (no result, ambiguous result, command not available, timeout).
|
|
10
10
|
*
|
|
11
11
|
* The caller decides how to treat null: this module's only job is to ask the
|
|
12
12
|
* kernel honestly. Concretely the server treats null as "reject", so users
|
|
13
13
|
* on systems without lsof / netstat fail closed.
|
|
14
|
+
*
|
|
15
|
+
* Defense in depth: even if the OS lookup somehow points at our own process,
|
|
16
|
+
* we return null. The auth path must never identify the server as its own
|
|
17
|
+
* peer — that would short-circuit the allowlist check.
|
|
14
18
|
*/
|
|
15
19
|
export async function lookupPeerProcess(host, port) {
|
|
16
20
|
const os = platform();
|
|
21
|
+
let result;
|
|
17
22
|
try {
|
|
18
23
|
if (os === 'darwin' || os === 'linux') {
|
|
19
|
-
|
|
24
|
+
result = await lookupUnix(host, port);
|
|
20
25
|
}
|
|
21
|
-
if (os === 'win32') {
|
|
22
|
-
|
|
26
|
+
else if (os === 'win32') {
|
|
27
|
+
result = await lookupWindows(host, port);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
return null;
|
|
23
31
|
}
|
|
24
|
-
return null;
|
|
25
32
|
}
|
|
26
33
|
catch {
|
|
27
34
|
return null;
|
|
28
35
|
}
|
|
36
|
+
if (result && result.pid === process.pid)
|
|
37
|
+
return null;
|
|
38
|
+
return result;
|
|
29
39
|
}
|
|
30
40
|
/**
|
|
31
|
-
* macOS / Linux path: `lsof` is the broadest tool available.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
41
|
+
* macOS / Linux path: `lsof` is the broadest tool available.
|
|
42
|
+
*
|
|
43
|
+
* The `-F pcn` flag asks for a stable, field-tagged output (one field per
|
|
44
|
+
* line, prefixed with the field letter): p=PID, c=command, n=NAME (which for
|
|
45
|
+
* TCP sockets is "local->remote"). We need the NAME because `-i @host:port`
|
|
46
|
+
* matches BOTH ends of any socket that touches host:port — and on loopback
|
|
47
|
+
* (peer and server both on 127.0.0.1) that means lsof returns both ends of
|
|
48
|
+
* the same connection. The parser then keeps only entries whose LOCAL side
|
|
49
|
+
* is host:port — the unambiguous peer.
|
|
50
|
+
*
|
|
51
|
+
* The Windows path (`parseNetstatForLocal`) applies the same local-endpoint
|
|
52
|
+
* filter; keep these two branches in sync.
|
|
35
53
|
*/
|
|
36
54
|
async function lookupUnix(host, port) {
|
|
37
|
-
const cmd = `lsof -nP -F
|
|
55
|
+
const cmd = `lsof -nP -F pcn -iTCP@${host}:${port} -sTCP:ESTABLISHED`;
|
|
38
56
|
const { stdout } = await execAsync(cmd, { timeout: LOOKUP_TIMEOUT_MS });
|
|
39
|
-
return parseLsofOutput(stdout);
|
|
57
|
+
return parseLsofOutput(stdout, host, port);
|
|
40
58
|
}
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Parse `lsof -F pcn` output and return the unique process whose LOCAL
|
|
61
|
+
* endpoint equals `host:port`. Returns null on:
|
|
62
|
+
* - empty output
|
|
63
|
+
* - no entry matching the local endpoint
|
|
64
|
+
* - two or more distinct PIDs claiming the same local endpoint (fail closed)
|
|
65
|
+
* - malformed records (missing pid/command/name)
|
|
66
|
+
*
|
|
67
|
+
* Output shape: every process group starts with `p<pid>`, followed by
|
|
68
|
+
* `c<command>` (one), then one or more `n<local>-><remote>` lines (one per
|
|
69
|
+
* matching socket). Names can contain spaces; lsof escapes them as `\xHH`.
|
|
70
|
+
*/
|
|
71
|
+
export function parseLsofOutput(out, host, port) {
|
|
72
|
+
const target = `${host}:${port}`;
|
|
73
|
+
const owners = new Map();
|
|
74
|
+
let curPid = null;
|
|
75
|
+
let curName = null;
|
|
43
76
|
for (const line of out.split('\n')) {
|
|
44
|
-
if (line.
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
if (line.length === 0)
|
|
78
|
+
continue;
|
|
79
|
+
const tag = line[0];
|
|
80
|
+
const rest = line.slice(1);
|
|
81
|
+
if (tag === 'p') {
|
|
82
|
+
const n = Number.parseInt(rest, 10);
|
|
83
|
+
curPid = Number.isFinite(n) ? n : null;
|
|
84
|
+
curName = null;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (tag === 'c') {
|
|
88
|
+
curName = decodeLsofString(rest);
|
|
47
89
|
continue;
|
|
48
90
|
}
|
|
49
|
-
if (
|
|
50
|
-
|
|
91
|
+
if (tag === 'n' && curPid !== null && curName !== null) {
|
|
92
|
+
const arrowIdx = rest.indexOf('->');
|
|
93
|
+
if (arrowIdx < 0)
|
|
94
|
+
continue;
|
|
95
|
+
const local = rest.slice(0, arrowIdx);
|
|
96
|
+
if (local !== target)
|
|
97
|
+
continue;
|
|
98
|
+
const existing = owners.get(curPid);
|
|
99
|
+
if (existing === undefined)
|
|
100
|
+
owners.set(curPid, curName);
|
|
101
|
+
else if (existing !== curName) {
|
|
102
|
+
// Same pid reporting two different names within one dump is
|
|
103
|
+
// contradictory — bail rather than guess.
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
51
106
|
}
|
|
52
107
|
}
|
|
53
|
-
|
|
108
|
+
if (owners.size !== 1)
|
|
109
|
+
return null;
|
|
110
|
+
const [[pid, binaryName]] = owners.entries();
|
|
111
|
+
return { pid, binaryName };
|
|
54
112
|
}
|
|
55
113
|
/** lsof escapes spaces and tabs in command names as \xHH. Reverse that. */
|
|
56
114
|
export function decodeLsofString(s) {
|
|
@@ -60,6 +118,9 @@ export function decodeLsofString(s) {
|
|
|
60
118
|
* Windows path: `netstat -ano` lists every TCP connection with its owning
|
|
61
119
|
* PID. We then ask `tasklist` for the image name of that PID. Both ship by
|
|
62
120
|
* default with Windows; no extra tooling needed.
|
|
121
|
+
*
|
|
122
|
+
* Like the UNIX path, this filters by LOCAL endpoint match — see
|
|
123
|
+
* `parseNetstatForLocal`. Keep the two branches in sync.
|
|
63
124
|
*/
|
|
64
125
|
async function lookupWindows(host, port) {
|
|
65
126
|
const { stdout: netstatOut } = await execAsync('netstat -ano -p TCP', {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-identity.js","sourceRoot":"","sources":["../../src/auth/process-identity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;AAClC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAO/B
|
|
1
|
+
{"version":3,"file":"process-identity.js","sourceRoot":"","sources":["../../src/auth/process-identity.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jobshimo/browser-link",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "MCP server that bridges Claude Code, OpenCode, GitHub Copilot CLI and other MCP clients to a Chrome tab. Per-tool permissions, multi-agent mode (multiple MCP clients sharing one bridge), persistent UI map across sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|