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.
Files changed (2) hide show
  1. package/index.ts +61 -14
  2. 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(config: SandboxConfig): Promise<{ port: number; stop: () => Promise<void> }> {
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 connected = false;
601
+ let settled = false;
586
602
  const upstream = connectNet(
587
603
  { host: resolved.address, port: endpoint.port, family: resolved.family },
588
604
  () => {
589
- connected = true;
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.on('error', () => {
595
- if (!connected) denyProxyRequest(client, '502 Bad Gateway');
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((line) => !line.toLowerCase().startsWith('proxy-connection:'))
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 connected = false;
670
+ let settled = false;
648
671
  const upstream = connectNet({ host: resolved.address, port, family: resolved.family }, () => {
649
- connected = true;
672
+ settled = true;
650
673
  upstream.write(`${rewrittenHeader}\r\n\r\n`);
651
674
  pipeSockets(client, upstream, rest);
652
675
  });
653
- upstream.on('error', () => {
654
- if (!connected) denyProxyRequest(client, '502 Bad Gateway');
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(port: number | null): Record<string, string> | undefined {
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 url = `http://127.0.0.1:${port}`;
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 proxy = allowNetwork ? null : await startProxy(effectiveConfig);
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.11",
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.11",
51
+ "@landstrip/landstrip": "^0.18.13",
52
52
  "@opentui/solid": ">=0.3.4",
53
53
  "solid-js": "1.9.12"
54
54
  },