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.
- package/index.ts +99 -19
- 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 {
|
|
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(/^\[([^\]]+)\](?::(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
437
|
-
|
|
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
|
|
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(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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 =
|
|
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,
|
|
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.
|
|
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.
|
|
51
|
+
"@landstrip/landstrip": "^0.17.31",
|
|
52
52
|
"@opentui/solid": ">=0.3.4",
|
|
53
53
|
"solid-js": "1.9.12"
|
|
54
54
|
},
|