opencode-landstrip 0.17.29 → 0.17.31

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 +99 -19
  2. package/package.json +2 -2
package/index.ts CHANGED
@@ -4,8 +4,16 @@
4
4
  import type { Hooks, Plugin, PluginInput, PluginOptions } from '@opencode-ai/plugin';
5
5
 
6
6
  import { spawnSync } from 'node:child_process';
7
+ import { lookup } from 'node:dns/promises';
7
8
  import { existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
8
- import { type AddressInfo, connect as connectNet, createServer, type Socket } from 'node:net';
9
+ import {
10
+ type AddressInfo,
11
+ BlockList,
12
+ connect as connectNet,
13
+ createServer,
14
+ isIP,
15
+ type Socket,
16
+ } from 'node:net';
9
17
  import { homedir, tmpdir } from 'node:os';
10
18
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
11
19
  import { URL } from 'node:url';
@@ -64,6 +72,33 @@ const LANDSTRIP_VERSION = [0, 17, 0] as const;
64
72
  const REQUIRED_LANDSTRIP_VERSION = LANDSTRIP_VERSION.join('.');
65
73
  const SUPPORTED_PLATFORMS = new Set<NodeJS.Platform>(['linux', 'darwin', 'win32']);
66
74
 
75
+ const prohibitedProxyAddresses = new BlockList();
76
+
77
+ for (const [network, prefix] of [
78
+ ['0.0.0.0', 8],
79
+ ['10.0.0.0', 8],
80
+ ['100.64.0.0', 10],
81
+ ['127.0.0.0', 8],
82
+ ['169.254.0.0', 16],
83
+ ['172.16.0.0', 12],
84
+ ['192.0.0.0', 24],
85
+ ['192.168.0.0', 16],
86
+ ['198.18.0.0', 15],
87
+ ['224.0.0.0', 4],
88
+ ['240.0.0.0', 4],
89
+ ] as const) {
90
+ prohibitedProxyAddresses.addSubnet(network, prefix, 'ipv4');
91
+ }
92
+ for (const [network, prefix] of [
93
+ ['::', 128],
94
+ ['::1', 128],
95
+ ['fc00::', 7],
96
+ ['fe80::', 10],
97
+ ['ff00::', 8],
98
+ ] as const) {
99
+ prohibitedProxyAddresses.addSubnet(network, prefix, 'ipv6');
100
+ }
101
+
67
102
  function expandPath(filePath: string, baseDirectory: string): string {
68
103
  const expanded = filePath.replace(/^~(?=$|[/])/, homedir());
69
104
  return resolve(isAbsolute(expanded) ? expanded : join(baseDirectory, expanded));
@@ -422,21 +457,26 @@ function hasMinimumVersion(version: string, minimum: readonly [number, number, n
422
457
  return true;
423
458
  }
424
459
 
460
+ function parseProxyPort(value: string | undefined, defaultPort: number): number | null {
461
+ const rawPort = value ?? String(defaultPort);
462
+ if (!/^\d+$/.test(rawPort)) return null;
463
+
464
+ const port = Number(rawPort);
465
+ return port >= 1 && port <= 65535 ? port : null;
466
+ }
467
+
425
468
  function splitHostPort(target: string, defaultPort: number): { host: string; port: number } | null {
426
- const bracketMatch = target.match(/^\[([^\]]+)\](?::(\d+))?$/);
427
- if (bracketMatch?.[1]) {
428
- return {
429
- host: bracketMatch[1],
430
- port: bracketMatch[2] ? Number(bracketMatch[2]) : defaultPort,
431
- };
469
+ const bracketMatch = target.match(/^\[([^\]]+)\](?::(.*))?$/);
470
+ const host = bracketMatch?.[1];
471
+ if (host) {
472
+ const port = parseProxyPort(bracketMatch?.[2], defaultPort);
473
+ return port === null ? null : { host, port };
432
474
  }
433
475
 
434
476
  const lastColon = target.lastIndexOf(':');
435
477
  if (lastColon > -1 && target.indexOf(':') === lastColon) {
436
- return {
437
- host: target.slice(0, lastColon),
438
- port: Number(target.slice(lastColon + 1)),
439
- };
478
+ const port = parseProxyPort(target.slice(lastColon + 1), defaultPort);
479
+ return port === null ? null : { host: target.slice(0, lastColon), port };
440
480
  }
441
481
 
442
482
  return { host: target, port: defaultPort };
@@ -447,6 +487,36 @@ function denyProxyRequest(client: Socket, status = '403 Forbidden'): void {
447
487
  client.end();
448
488
  }
449
489
 
490
+ function isPublicProxyAddress(address: string, family = isIP(address)): boolean {
491
+ if (family === 4) return !prohibitedProxyAddresses.check(address, 'ipv4');
492
+ if (family === 6) return !prohibitedProxyAddresses.check(address, 'ipv6');
493
+ return false;
494
+ }
495
+
496
+ async function resolveProxyEndpoint(host: string): Promise<{ address: string; family: 4 | 6 }> {
497
+ const literalFamily = isIP(host);
498
+ if (literalFamily === 4 || literalFamily === 6) {
499
+ if (!isPublicProxyAddress(host, literalFamily)) {
500
+ throw new Error(`Proxy destination is not public: ${host}`);
501
+ }
502
+ return { address: host, family: literalFamily };
503
+ }
504
+
505
+ const addresses = await lookup(host, { all: true, verbatim: true });
506
+ if (
507
+ addresses.length === 0 ||
508
+ addresses.some(({ address, family }) => !isPublicProxyAddress(address, family))
509
+ ) {
510
+ throw new Error(`Proxy destination resolves to a non-public address: ${host}`);
511
+ }
512
+
513
+ const endpoint = addresses[0];
514
+ if (!endpoint || (endpoint.family !== 4 && endpoint.family !== 6)) {
515
+ throw new Error(`Proxy could not resolve destination: ${host}`);
516
+ }
517
+ return { address: endpoint.address, family: endpoint.family };
518
+ }
519
+
450
520
  function pipeSockets(client: Socket, upstream: Socket, initialData?: Buffer): void {
451
521
  upstream.on('error', () => client.destroy());
452
522
  client.on('error', () => upstream.destroy());
@@ -494,7 +564,7 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
494
564
 
495
565
  async function handleConnect(client: Socket, target: string, rest: Buffer): Promise<void> {
496
566
  const endpoint = splitHostPort(target, 443);
497
- if (!endpoint || !Number.isFinite(endpoint.port)) {
567
+ if (!endpoint) {
498
568
  denyProxyRequest(client, '400 Bad Request');
499
569
  return;
500
570
  }
@@ -504,12 +574,16 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
504
574
  return;
505
575
  }
506
576
 
577
+ const resolved = await resolveProxyEndpoint(endpoint.host);
507
578
  let connected = false;
508
- const upstream = connectNet(endpoint.port, endpoint.host, () => {
509
- connected = true;
510
- client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
511
- pipeSockets(client, upstream, rest);
512
- });
579
+ const upstream = connectNet(
580
+ { host: resolved.address, port: endpoint.port, family: resolved.family },
581
+ () => {
582
+ connected = true;
583
+ client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
584
+ pipeSockets(client, upstream, rest);
585
+ },
586
+ );
513
587
  upstream.on('error', () => {
514
588
  if (!connected) denyProxyRequest(client, '502 Bad Gateway');
515
589
  });
@@ -550,15 +624,21 @@ function startProxy(config: SandboxConfig): Promise<{ port: number; stop: () =>
550
624
  return;
551
625
  }
552
626
 
553
- const port = Number(url.port || (url.protocol === 'https:' ? 443 : 80));
627
+ const port = parseProxyPort(url.port || undefined, url.protocol === 'https:' ? 443 : 80);
628
+ if (port === null) {
629
+ denyProxyRequest(client, '400 Bad Request');
630
+ return;
631
+ }
632
+
554
633
  const path = `${url.pathname}${url.search}` || '/';
555
634
  lines[0] = `${method} ${path} ${version}`;
556
635
 
557
636
  const rewrittenHeader = lines
558
637
  .filter((line) => !line.toLowerCase().startsWith('proxy-connection:'))
559
638
  .join('\r\n');
639
+ const resolved = await resolveProxyEndpoint(url.hostname);
560
640
  let connected = false;
561
- const upstream = connectNet(port, url.hostname, () => {
641
+ const upstream = connectNet({ host: resolved.address, port, family: resolved.family }, () => {
562
642
  connected = true;
563
643
  upstream.write(`${rewrittenHeader}\r\n\r\n`);
564
644
  pipeSockets(client, upstream, rest);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-landstrip",
3
- "version": "0.17.29",
3
+ "version": "0.17.31",
4
4
  "description": "Landlock-based 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.17.29",
51
+ "@landstrip/landstrip": "^0.17.31",
52
52
  "@opentui/solid": ">=0.3.4",
53
53
  "solid-js": "1.9.12"
54
54
  },