opencode-landstrip 0.18.11 → 0.18.13
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.
- package/index.ts +61 -14
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import type { Hooks, Plugin, PluginInput, PluginOptions } from '@opencode-ai/plugin';
|
|
5
5
|
|
|
6
|
+
import { randomBytes } from 'node:crypto';
|
|
6
7
|
import { spawnSync } from 'node:child_process';
|
|
7
8
|
import { lookup } from 'node:dns/promises';
|
|
8
9
|
import { existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
@@ -51,6 +52,7 @@ interface BashSandboxState {
|
|
|
51
52
|
wrappedCommand: string;
|
|
52
53
|
policyDir: string;
|
|
53
54
|
port: number | null;
|
|
55
|
+
proxyToken: string | null;
|
|
54
56
|
stop: (() => Promise<void>) | null;
|
|
55
57
|
trapServer: ReturnType<typeof createServer> | null;
|
|
56
58
|
trapServerPort: number | null;
|
|
@@ -566,9 +568,23 @@ function writePolicyFile(
|
|
|
566
568
|
return { dir, path };
|
|
567
569
|
}
|
|
568
570
|
|
|
569
|
-
function startProxy(
|
|
571
|
+
function startProxy(
|
|
572
|
+
config: SandboxConfig,
|
|
573
|
+
authorization?: string,
|
|
574
|
+
): Promise<{ port: number; stop: () => Promise<void> }> {
|
|
570
575
|
const sockets = new Set<Socket>();
|
|
571
576
|
|
|
577
|
+
// Track the upstream socket so stop() tears it down, and abandon a still-
|
|
578
|
+
// connecting upstream when its client goes away — otherwise a connect to a
|
|
579
|
+
// black-holed host lingers in SYN-retry (~2 min), leaking an fd per request.
|
|
580
|
+
function trackUpstream(upstream: Socket, client: Socket, settled: () => boolean): void {
|
|
581
|
+
sockets.add(upstream);
|
|
582
|
+
upstream.once('close', () => sockets.delete(upstream));
|
|
583
|
+
client.once('close', () => {
|
|
584
|
+
if (!settled()) upstream.destroy();
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
572
588
|
async function handleConnect(client: Socket, target: string, rest: Buffer): Promise<void> {
|
|
573
589
|
const endpoint = splitHostPort(target, 443);
|
|
574
590
|
if (!endpoint) {
|
|
@@ -582,17 +598,20 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
|
|
|
582
598
|
}
|
|
583
599
|
|
|
584
600
|
const resolved = await resolveProxyEndpoint(endpoint.host);
|
|
585
|
-
let
|
|
601
|
+
let settled = false;
|
|
586
602
|
const upstream = connectNet(
|
|
587
603
|
{ host: resolved.address, port: endpoint.port, family: resolved.family },
|
|
588
604
|
() => {
|
|
589
|
-
|
|
605
|
+
settled = true;
|
|
590
606
|
client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
591
607
|
pipeSockets(client, upstream, rest);
|
|
592
608
|
},
|
|
593
609
|
);
|
|
594
|
-
upstream
|
|
595
|
-
|
|
610
|
+
trackUpstream(upstream, client, () => settled);
|
|
611
|
+
upstream.once('error', () => {
|
|
612
|
+
if (settled) return;
|
|
613
|
+
settled = true;
|
|
614
|
+
denyProxyRequest(client, '502 Bad Gateway');
|
|
596
615
|
});
|
|
597
616
|
}
|
|
598
617
|
|
|
@@ -641,17 +660,24 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
|
|
|
641
660
|
lines[0] = `${method} ${path} ${version}`;
|
|
642
661
|
|
|
643
662
|
const rewrittenHeader = lines
|
|
644
|
-
.filter(
|
|
663
|
+
.filter(
|
|
664
|
+
(line) =>
|
|
665
|
+
!line.toLowerCase().startsWith('proxy-connection:') &&
|
|
666
|
+
!line.toLowerCase().startsWith('proxy-authorization:'),
|
|
667
|
+
)
|
|
645
668
|
.join('\r\n');
|
|
646
669
|
const resolved = await resolveProxyEndpoint(url.hostname);
|
|
647
|
-
let
|
|
670
|
+
let settled = false;
|
|
648
671
|
const upstream = connectNet({ host: resolved.address, port, family: resolved.family }, () => {
|
|
649
|
-
|
|
672
|
+
settled = true;
|
|
650
673
|
upstream.write(`${rewrittenHeader}\r\n\r\n`);
|
|
651
674
|
pipeSockets(client, upstream, rest);
|
|
652
675
|
});
|
|
653
|
-
upstream
|
|
654
|
-
|
|
676
|
+
trackUpstream(upstream, client, () => settled);
|
|
677
|
+
upstream.once('error', () => {
|
|
678
|
+
if (settled) return;
|
|
679
|
+
settled = true;
|
|
680
|
+
denyProxyRequest(client, '502 Bad Gateway');
|
|
655
681
|
});
|
|
656
682
|
}
|
|
657
683
|
|
|
@@ -679,6 +705,17 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
|
|
|
679
705
|
|
|
680
706
|
const header = buffered.subarray(0, headerEnd).toString('utf-8');
|
|
681
707
|
const rest = buffered.subarray(headerEnd + 4);
|
|
708
|
+
if (authorization) {
|
|
709
|
+
const supplied = header
|
|
710
|
+
.split(/\r?\n/)
|
|
711
|
+
.find((line) => line.toLowerCase().startsWith('proxy-authorization:'))
|
|
712
|
+
?.slice('proxy-authorization:'.length)
|
|
713
|
+
.trim();
|
|
714
|
+
if (supplied !== authorization) {
|
|
715
|
+
denyProxyRequest(client, '407 Proxy Authentication Required');
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
682
719
|
const firstLine = header.split(/\r?\n/, 1)[0];
|
|
683
720
|
const [method, target] = (firstLine ?? '').split(' ');
|
|
684
721
|
|
|
@@ -716,9 +753,13 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
|
|
|
716
753
|
});
|
|
717
754
|
}
|
|
718
755
|
|
|
719
|
-
function proxyEnv(
|
|
756
|
+
function proxyEnv(
|
|
757
|
+
port: number | null,
|
|
758
|
+
proxyToken?: string | null,
|
|
759
|
+
): Record<string, string> | undefined {
|
|
720
760
|
if (port === null) return undefined;
|
|
721
|
-
const
|
|
761
|
+
const credentials = proxyToken ? `landstrip:${proxyToken}@` : '';
|
|
762
|
+
const url = `http://${credentials}127.0.0.1:${port}`;
|
|
722
763
|
|
|
723
764
|
return {
|
|
724
765
|
HTTP_PROXY: url,
|
|
@@ -1305,7 +1346,12 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1305
1346
|
};
|
|
1306
1347
|
}
|
|
1307
1348
|
|
|
1308
|
-
const
|
|
1349
|
+
const proxyToken = allowNetwork ? null : randomBytes(32).toString('base64url');
|
|
1350
|
+
const proxyAuthorization =
|
|
1351
|
+
proxyToken === null
|
|
1352
|
+
? undefined
|
|
1353
|
+
: `Basic ${Buffer.from(`landstrip:${proxyToken}`).toString('base64')}`;
|
|
1354
|
+
const proxy = allowNetwork ? null : await startProxy(effectiveConfig, proxyAuthorization);
|
|
1309
1355
|
const proxyPort = proxy ? proxy.port : null;
|
|
1310
1356
|
let policy: { dir: string; path: string };
|
|
1311
1357
|
|
|
@@ -1348,6 +1394,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1348
1394
|
wrappedCommand,
|
|
1349
1395
|
policyDir: policy.dir,
|
|
1350
1396
|
port: proxyPort,
|
|
1397
|
+
proxyToken,
|
|
1351
1398
|
stop: proxy ? proxy.stop : null,
|
|
1352
1399
|
trapServer: trapServer?.server ?? null,
|
|
1353
1400
|
trapServerPort: trapPort,
|
|
@@ -1437,7 +1484,7 @@ const plugin: Plugin = async ({ client, directory }: PluginInput, options?: Plug
|
|
|
1437
1484
|
const state = activeBash.get(input.callID);
|
|
1438
1485
|
if (!state) return;
|
|
1439
1486
|
|
|
1440
|
-
const envVars = proxyEnv(state.port);
|
|
1487
|
+
const envVars = proxyEnv(state.port, state.proxyToken);
|
|
1441
1488
|
if (envVars) Object.assign(output.env, envVars);
|
|
1442
1489
|
},
|
|
1443
1490
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-landstrip",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.13",
|
|
4
4
|
"description": "OS-level AI command sandboxing for OpenCode with Landstrip",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"landlock",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"ci:test": "npm test"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@landstrip/landstrip": "^0.18.
|
|
51
|
+
"@landstrip/landstrip": "^0.18.13",
|
|
52
52
|
"@opentui/solid": ">=0.3.4",
|
|
53
53
|
"solid-js": "1.9.12"
|
|
54
54
|
},
|