@ai-ecoverse/slicc-kernel 1.1.0 → 1.3.0
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/README.md +42 -3
- package/dist/index.js +151 -2
- package/dist/index.js.map +3 -3
- package/dist/kernel-worker.js +2842 -463
- package/dist/kernel-worker.js.map +4 -4
- package/dist/process-worker.js +282 -29
- package/dist/process-worker.js.map +4 -4
- package/package.json +7 -6
package/dist/kernel-worker.js
CHANGED
|
@@ -1,3 +1,152 @@
|
|
|
1
|
+
// src/fs/meta.ts
|
|
2
|
+
var META_DB = "slicc-kernel";
|
|
3
|
+
var META_VERSION = 1;
|
|
4
|
+
var META_STORE = "entries";
|
|
5
|
+
function parentOf(path) {
|
|
6
|
+
return path.slice(0, path.lastIndexOf("/")) || "/";
|
|
7
|
+
}
|
|
8
|
+
function moved(entry, path) {
|
|
9
|
+
return entry.link === void 0 ? { ...entry, path } : { ...entry, path, dir: parentOf(path) };
|
|
10
|
+
}
|
|
11
|
+
function under(path, root) {
|
|
12
|
+
return path === root || path.startsWith(`${root}/`);
|
|
13
|
+
}
|
|
14
|
+
var MemoryMeta = class {
|
|
15
|
+
entries = /* @__PURE__ */ new Map();
|
|
16
|
+
async get(paths) {
|
|
17
|
+
return paths.map((path) => this.entries.get(path));
|
|
18
|
+
}
|
|
19
|
+
async update(path, change) {
|
|
20
|
+
this.set(path, change(this.entries.get(path)));
|
|
21
|
+
}
|
|
22
|
+
set(path, entry) {
|
|
23
|
+
if (entry) this.entries.set(path, entry);
|
|
24
|
+
else this.entries.delete(path);
|
|
25
|
+
}
|
|
26
|
+
async move(from, to, root) {
|
|
27
|
+
await this.remove([to]);
|
|
28
|
+
const carried = [...this.entries.values()].filter((entry) => under(entry.path, from));
|
|
29
|
+
for (const entry of carried) this.entries.delete(entry.path);
|
|
30
|
+
for (const entry of carried) {
|
|
31
|
+
const path = to + entry.path.slice(from.length);
|
|
32
|
+
if (path !== to) this.entries.set(path, moved(entry, path));
|
|
33
|
+
}
|
|
34
|
+
this.entries.set(to, moved(root(carried.find((entry) => entry.path === from)), to));
|
|
35
|
+
}
|
|
36
|
+
async remove(paths) {
|
|
37
|
+
for (const key of [...this.entries.keys()]) {
|
|
38
|
+
if (paths.some((path) => under(key, path))) this.entries.delete(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async links(dir) {
|
|
42
|
+
return [...this.entries.values()].filter((entry) => entry.dir === dir);
|
|
43
|
+
}
|
|
44
|
+
async under(path) {
|
|
45
|
+
return [...this.entries.values()].filter((entry) => under(entry.path, path));
|
|
46
|
+
}
|
|
47
|
+
async all() {
|
|
48
|
+
return [...this.entries.values()];
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function descendants(path) {
|
|
52
|
+
return IDBKeyRange.bound(`${path}/`, `${path}0`, false, true);
|
|
53
|
+
}
|
|
54
|
+
function settle(transaction, value) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
transaction.oncomplete = () => resolve(value());
|
|
57
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("metadata transaction aborted"));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
var IndexedDbMeta = class {
|
|
61
|
+
db;
|
|
62
|
+
constructor(db) {
|
|
63
|
+
this.db = db;
|
|
64
|
+
db.onversionchange = () => db.close();
|
|
65
|
+
}
|
|
66
|
+
transaction(mode) {
|
|
67
|
+
const transaction = this.db.transaction(META_STORE, mode);
|
|
68
|
+
return [transaction, transaction.objectStore(META_STORE)];
|
|
69
|
+
}
|
|
70
|
+
async get(paths) {
|
|
71
|
+
const [transaction, store] = this.transaction("readonly");
|
|
72
|
+
const found = paths.map(() => void 0);
|
|
73
|
+
paths.forEach((path, i) => {
|
|
74
|
+
const request2 = store.get(path);
|
|
75
|
+
request2.onsuccess = () => {
|
|
76
|
+
found[i] = request2.result;
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
return settle(transaction, () => found);
|
|
80
|
+
}
|
|
81
|
+
async update(path, change) {
|
|
82
|
+
const [transaction, store] = this.transaction("readwrite");
|
|
83
|
+
const request2 = store.get(path);
|
|
84
|
+
request2.onsuccess = () => {
|
|
85
|
+
const next = change(request2.result);
|
|
86
|
+
if (next) store.put(next);
|
|
87
|
+
else store.delete(path);
|
|
88
|
+
};
|
|
89
|
+
return settle(transaction, () => void 0);
|
|
90
|
+
}
|
|
91
|
+
async move(from, to, root) {
|
|
92
|
+
const [transaction, store] = this.transaction("readwrite");
|
|
93
|
+
store.delete(to);
|
|
94
|
+
store.delete(descendants(to));
|
|
95
|
+
const source = store.get(from);
|
|
96
|
+
const below = store.getAll(descendants(from));
|
|
97
|
+
below.onsuccess = () => {
|
|
98
|
+
store.delete(from);
|
|
99
|
+
store.delete(descendants(from));
|
|
100
|
+
for (const entry of below.result) {
|
|
101
|
+
store.put(moved(entry, to + entry.path.slice(from.length)));
|
|
102
|
+
}
|
|
103
|
+
store.put(moved(root(source.result), to));
|
|
104
|
+
};
|
|
105
|
+
return settle(transaction, () => void 0);
|
|
106
|
+
}
|
|
107
|
+
async remove(paths) {
|
|
108
|
+
const [transaction, store] = this.transaction("readwrite");
|
|
109
|
+
for (const path of paths) {
|
|
110
|
+
store.delete(path);
|
|
111
|
+
store.delete(descendants(path));
|
|
112
|
+
}
|
|
113
|
+
return settle(transaction, () => void 0);
|
|
114
|
+
}
|
|
115
|
+
async links(dir) {
|
|
116
|
+
const [transaction, store] = this.transaction("readonly");
|
|
117
|
+
const request2 = store.index("dir").getAll(dir);
|
|
118
|
+
return settle(transaction, () => request2.result);
|
|
119
|
+
}
|
|
120
|
+
async under(path) {
|
|
121
|
+
const [transaction, store] = this.transaction("readonly");
|
|
122
|
+
const self = store.get(path);
|
|
123
|
+
const below = store.getAll(descendants(path));
|
|
124
|
+
return settle(
|
|
125
|
+
transaction,
|
|
126
|
+
() => [self.result, ...below.result].filter(
|
|
127
|
+
(entry) => entry !== void 0
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
async all() {
|
|
132
|
+
const [transaction, store] = this.transaction("readonly");
|
|
133
|
+
const request2 = store.getAll();
|
|
134
|
+
return settle(transaction, () => request2.result);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
function openMeta(name2, factory) {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const request2 = factory.open(name2, META_VERSION);
|
|
140
|
+
request2.onupgradeneeded = (event) => {
|
|
141
|
+
if (event.oldVersion < 1) {
|
|
142
|
+
request2.result.createObjectStore(META_STORE, { keyPath: "path" }).createIndex("dir", "dir");
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
request2.onsuccess = () => resolve(new IndexedDbMeta(request2.result));
|
|
146
|
+
request2.onerror = () => reject(request2.error);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
1
150
|
// src/fs/types.ts
|
|
2
151
|
function fsError(code, path) {
|
|
3
152
|
return Object.assign(new Error(`${code}: ${path}`), { code });
|
|
@@ -14,12 +163,26 @@ function normalizePath(path) {
|
|
|
14
163
|
function resolvePath(base, path) {
|
|
15
164
|
return normalizePath(path.startsWith("/") ? path : `${base}/${path}`);
|
|
16
165
|
}
|
|
166
|
+
function inodeOf(path) {
|
|
167
|
+
let h1 = 3735928559;
|
|
168
|
+
let h2 = 1103547991;
|
|
169
|
+
for (let i = 0; i < path.length; i++) {
|
|
170
|
+
const c = path.charCodeAt(i);
|
|
171
|
+
h1 = Math.imul(h1 ^ c, 2654435761);
|
|
172
|
+
h2 = Math.imul(h2 ^ c, 1597334677);
|
|
173
|
+
}
|
|
174
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
175
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
176
|
+
return 4294967296 * (2097151 & h2) + (h1 >>> 0) || 1;
|
|
177
|
+
}
|
|
17
178
|
|
|
18
179
|
// src/fs/opfs.ts
|
|
19
180
|
var S_IFDIR = 16384;
|
|
20
181
|
var S_IFREG = 32768;
|
|
182
|
+
var S_IFLNK = 40960;
|
|
21
183
|
var PERM_MASK = 4095;
|
|
22
184
|
var READ_ATTEMPTS = 3;
|
|
185
|
+
var MAX_LINKS = 40;
|
|
23
186
|
var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
|
|
24
187
|
var ERRNO_BY_DOM_ERROR = {
|
|
25
188
|
NotFoundError: "ENOENT",
|
|
@@ -30,29 +193,46 @@ var ERRNO_BY_DOM_ERROR = {
|
|
|
30
193
|
};
|
|
31
194
|
function translate(err, path) {
|
|
32
195
|
if (typeof err?.code === "string") return err;
|
|
33
|
-
const
|
|
34
|
-
return fsError(typeof
|
|
196
|
+
const name2 = err?.name;
|
|
197
|
+
return fsError(typeof name2 === "string" && ERRNO_BY_DOM_ERROR[name2] || "EIO", path);
|
|
198
|
+
}
|
|
199
|
+
function missing(err) {
|
|
200
|
+
const code = err?.code;
|
|
201
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
35
202
|
}
|
|
36
203
|
function split(path) {
|
|
37
204
|
return path.split("/").filter(Boolean);
|
|
38
205
|
}
|
|
206
|
+
function parentOf2(path) {
|
|
207
|
+
return path.slice(0, path.lastIndexOf("/")) || "/";
|
|
208
|
+
}
|
|
209
|
+
function baseOf(path) {
|
|
210
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
211
|
+
}
|
|
212
|
+
function prefixes(path) {
|
|
213
|
+
const parts = split(path);
|
|
214
|
+
return parts.map((_, i) => `/${parts.slice(0, i + 1).join("/")}`);
|
|
215
|
+
}
|
|
39
216
|
function within(path, root) {
|
|
40
217
|
return path === root || path.startsWith(`${root}/`);
|
|
41
218
|
}
|
|
219
|
+
function isHandle(child) {
|
|
220
|
+
return "kind" in child;
|
|
221
|
+
}
|
|
42
222
|
var OpfsFs = class {
|
|
43
|
-
modes = /* @__PURE__ */ new Map();
|
|
44
|
-
times = /* @__PURE__ */ new Map();
|
|
45
223
|
root;
|
|
46
|
-
|
|
224
|
+
meta;
|
|
225
|
+
constructor(root, meta = new MemoryMeta()) {
|
|
47
226
|
this.root = root;
|
|
227
|
+
this.meta = meta;
|
|
48
228
|
}
|
|
49
229
|
resolvePath(base, path) {
|
|
50
230
|
return resolvePath(base, path);
|
|
51
231
|
}
|
|
52
|
-
async dir(path
|
|
232
|
+
async dir(path) {
|
|
53
233
|
let dir = this.root;
|
|
54
234
|
try {
|
|
55
|
-
for (const part of split(path)) dir = await dir.getDirectoryHandle(part
|
|
235
|
+
for (const part of split(path)) dir = await dir.getDirectoryHandle(part);
|
|
56
236
|
} catch (err) {
|
|
57
237
|
throw translate(err, path);
|
|
58
238
|
}
|
|
@@ -60,48 +240,103 @@ var OpfsFs = class {
|
|
|
60
240
|
}
|
|
61
241
|
async parent(path) {
|
|
62
242
|
const parts = split(path);
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
return [await this.dir(`/${parts.join("/")}`),
|
|
243
|
+
const name2 = parts.pop();
|
|
244
|
+
if (name2 === void 0) throw fsError("EBUSY", path);
|
|
245
|
+
return [await this.dir(`/${parts.join("/")}`), name2];
|
|
66
246
|
}
|
|
67
247
|
async handle(path) {
|
|
68
248
|
if (split(path).length === 0) return this.root;
|
|
69
|
-
const [dir,
|
|
249
|
+
const [dir, name2] = await this.parent(path);
|
|
70
250
|
try {
|
|
71
|
-
return await dir.getFileHandle(
|
|
251
|
+
return await dir.getFileHandle(name2);
|
|
72
252
|
} catch (err) {
|
|
73
253
|
if (err?.name !== "TypeMismatchError") throw translate(err, path);
|
|
74
254
|
}
|
|
75
|
-
return dir.getDirectoryHandle(
|
|
255
|
+
return dir.getDirectoryHandle(name2);
|
|
256
|
+
}
|
|
257
|
+
real(path) {
|
|
258
|
+
return this.handle(path).then(
|
|
259
|
+
() => true,
|
|
260
|
+
() => false
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
async linkAlong(path) {
|
|
264
|
+
for (const entry of await this.meta.get(prefixes(path))) {
|
|
265
|
+
if (entry?.link === void 0) continue;
|
|
266
|
+
if (!await this.real(entry.path) && await this.real(parentOf2(entry.path))) return entry;
|
|
267
|
+
}
|
|
268
|
+
return void 0;
|
|
269
|
+
}
|
|
270
|
+
async locate(path, follow) {
|
|
271
|
+
let current = path;
|
|
272
|
+
for (let hops = 0; hops <= MAX_LINKS; hops++) {
|
|
273
|
+
try {
|
|
274
|
+
return { path: current, handle: await this.handle(current) };
|
|
275
|
+
} catch (err) {
|
|
276
|
+
if (!missing(err)) throw err;
|
|
277
|
+
const link2 = await this.linkAlong(current);
|
|
278
|
+
if (!link2) throw err;
|
|
279
|
+
if (link2.path === current && !follow) return { path: current, link: link2 };
|
|
280
|
+
const rest = current.slice(link2.path.length);
|
|
281
|
+
current = resolvePath(parentOf2(link2.path), `${link2.link}${rest}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
throw fsError("ELOOP", path);
|
|
76
285
|
}
|
|
77
|
-
mode(path, directory) {
|
|
286
|
+
mode(path, directory, meta) {
|
|
78
287
|
const fallback = directory || EXECUTABLE.test(path) ? 493 : 420;
|
|
79
|
-
return (directory ? S_IFDIR : S_IFREG) | (
|
|
288
|
+
return (directory ? S_IFDIR : S_IFREG) | (meta?.mode ?? fallback);
|
|
80
289
|
}
|
|
81
|
-
async
|
|
82
|
-
const handle = await this.handle(path);
|
|
290
|
+
async statOf(path, handle, meta) {
|
|
83
291
|
const directory = handle.kind === "directory";
|
|
84
292
|
const file = directory ? void 0 : await handle.getFile();
|
|
293
|
+
const modified = file?.lastModified ?? 0;
|
|
294
|
+
const mtime = meta?.mtimeMs !== void 0 && meta.mtimeFor === modified ? meta.mtimeMs : modified;
|
|
85
295
|
return {
|
|
86
296
|
isFile: !directory,
|
|
87
297
|
isDirectory: directory,
|
|
88
298
|
isSymbolicLink: false,
|
|
89
299
|
size: file?.size ?? 0,
|
|
90
|
-
mode: this.mode(path, directory),
|
|
91
|
-
mtime: new Date(
|
|
300
|
+
mode: this.mode(path, directory, meta),
|
|
301
|
+
mtime: new Date(mtime),
|
|
302
|
+
atime: new Date(meta?.atimeMs ?? mtime),
|
|
303
|
+
ctime: new Date(Math.max(meta?.ctimeMs ?? mtime, modified)),
|
|
304
|
+
ino: meta?.ino ?? inodeOf(path)
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
linkStat(entry) {
|
|
308
|
+
const time2 = new Date(entry.mtimeMs ?? 0);
|
|
309
|
+
return {
|
|
310
|
+
isFile: false,
|
|
311
|
+
isDirectory: false,
|
|
312
|
+
isSymbolicLink: true,
|
|
313
|
+
size: new TextEncoder().encode(entry.link).length,
|
|
314
|
+
mode: S_IFLNK | 511,
|
|
315
|
+
mtime: time2,
|
|
316
|
+
atime: time2,
|
|
317
|
+
ctime: new Date(entry.ctimeMs ?? 0),
|
|
318
|
+
ino: entry.ino ?? inodeOf(entry.path)
|
|
92
319
|
};
|
|
93
320
|
}
|
|
94
|
-
|
|
95
|
-
return this.
|
|
321
|
+
async describe(found) {
|
|
322
|
+
if (found.link) return this.linkStat(found.link);
|
|
323
|
+
const [meta] = await this.meta.get([found.path]);
|
|
324
|
+
return this.statOf(found.path, found.handle, meta);
|
|
325
|
+
}
|
|
326
|
+
async stat(path) {
|
|
327
|
+
return this.describe(await this.locate(path, true));
|
|
328
|
+
}
|
|
329
|
+
async lstat(path) {
|
|
330
|
+
return this.describe(await this.locate(path, false));
|
|
96
331
|
}
|
|
97
332
|
async exists(path) {
|
|
98
|
-
return this.
|
|
333
|
+
return this.locate(path, true).then(
|
|
99
334
|
() => true,
|
|
100
335
|
() => false
|
|
101
336
|
);
|
|
102
337
|
}
|
|
103
338
|
async readFileBuffer(path) {
|
|
104
|
-
const handle = await this.
|
|
339
|
+
const handle = (await this.locate(path, true)).handle;
|
|
105
340
|
if (handle.kind === "directory") throw fsError("EISDIR", path);
|
|
106
341
|
for (let attempt = 1; ; attempt++) {
|
|
107
342
|
try {
|
|
@@ -117,11 +352,22 @@ var OpfsFs = class {
|
|
|
117
352
|
async readFile(path) {
|
|
118
353
|
return new TextDecoder().decode(await this.readFileBuffer(path));
|
|
119
354
|
}
|
|
355
|
+
async writeTarget(path) {
|
|
356
|
+
try {
|
|
357
|
+
return (await this.locate(path, true)).path;
|
|
358
|
+
} catch (err) {
|
|
359
|
+
if (!missing(err)) throw err;
|
|
360
|
+
}
|
|
361
|
+
const [entry] = await this.meta.get([path]);
|
|
362
|
+
const wanted = entry?.link === void 0 ? path : resolvePath(parentOf2(path), entry.link);
|
|
363
|
+
return resolvePath((await this.locate(parentOf2(wanted), true)).path, baseOf(wanted));
|
|
364
|
+
}
|
|
120
365
|
async writeFile(path, content) {
|
|
121
|
-
const
|
|
366
|
+
const target = await this.writeTarget(path);
|
|
367
|
+
const [dir, name2] = await this.parent(target);
|
|
122
368
|
let file;
|
|
123
369
|
try {
|
|
124
|
-
file = await dir.getFileHandle(
|
|
370
|
+
file = await dir.getFileHandle(name2, { create: true });
|
|
125
371
|
} catch (err) {
|
|
126
372
|
const code = err?.name === "TypeMismatchError" ? "EISDIR" : null;
|
|
127
373
|
throw code ? fsError(code, path) : translate(err, path);
|
|
@@ -129,357 +375,256 @@ var OpfsFs = class {
|
|
|
129
375
|
const writable = await file.createWritable();
|
|
130
376
|
await writable.write(content);
|
|
131
377
|
await writable.close();
|
|
132
|
-
this.times.delete(path);
|
|
133
378
|
}
|
|
134
|
-
async
|
|
135
|
-
const
|
|
379
|
+
async listing(path) {
|
|
380
|
+
const found = await this.locate(path, true);
|
|
381
|
+
const handle = found.handle;
|
|
136
382
|
if (handle.kind !== "directory") throw fsError("ENOTDIR", path);
|
|
137
|
-
const
|
|
138
|
-
for await (const
|
|
139
|
-
|
|
383
|
+
const children2 = /* @__PURE__ */ new Map();
|
|
384
|
+
for await (const child of handle.values()) children2.set(child.name, child);
|
|
385
|
+
for (const link2 of await this.meta.links(found.path)) {
|
|
386
|
+
const name2 = baseOf(link2.path);
|
|
387
|
+
if (!children2.has(name2)) children2.set(name2, link2);
|
|
388
|
+
}
|
|
389
|
+
return [found.path, [...children2].sort(([a], [b]) => a < b ? -1 : 1)];
|
|
390
|
+
}
|
|
391
|
+
async readdir(path) {
|
|
392
|
+
return (await this.listing(path))[1].map(([name2]) => name2);
|
|
393
|
+
}
|
|
394
|
+
async readdirStat(path) {
|
|
395
|
+
const [dir, children2] = await this.listing(path);
|
|
396
|
+
const base = dir === "/" ? "" : dir;
|
|
397
|
+
const metas = await this.meta.get(children2.map(([name2]) => `${base}/${name2}`));
|
|
398
|
+
return Promise.all(
|
|
399
|
+
children2.map(async ([name2, child], i) => {
|
|
400
|
+
if (!isHandle(child)) return [name2, this.linkStat(child)];
|
|
401
|
+
const stat = await this.statOf(`${base}/${name2}`, child, metas[i]).catch(() => null);
|
|
402
|
+
return [name2, stat];
|
|
403
|
+
})
|
|
404
|
+
);
|
|
140
405
|
}
|
|
141
406
|
async mkdir(path, options = {}) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
407
|
+
let found;
|
|
408
|
+
try {
|
|
409
|
+
found = await this.locate(path, true);
|
|
410
|
+
} catch (err) {
|
|
411
|
+
if (!missing(err)) throw err;
|
|
412
|
+
}
|
|
413
|
+
if (found) {
|
|
414
|
+
const directory = found.handle?.kind === "directory";
|
|
415
|
+
if (options.recursive && directory) return;
|
|
416
|
+
throw fsError(options.recursive ? "ENOTDIR" : "EEXIST", path);
|
|
145
417
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
await
|
|
418
|
+
const [entry] = await this.meta.get([path]);
|
|
419
|
+
if (entry?.link !== void 0) throw fsError("EEXIST", path);
|
|
420
|
+
if (options.recursive) await this.mkdir(parentOf2(path), options);
|
|
421
|
+
const parent = await this.locate(parentOf2(path), true);
|
|
422
|
+
if (parent.handle?.kind !== "directory") throw fsError("ENOTDIR", path);
|
|
423
|
+
await parent.handle.getDirectoryHandle(baseOf(path), { create: true });
|
|
149
424
|
}
|
|
150
425
|
async rm(path, options = {}) {
|
|
426
|
+
let found;
|
|
151
427
|
try {
|
|
152
|
-
|
|
153
|
-
await dir.removeEntry(name, { recursive: options.recursive === true });
|
|
428
|
+
found = await this.locate(path, false);
|
|
154
429
|
} catch (err) {
|
|
155
430
|
const error = translate(err, path);
|
|
156
431
|
if (options.force && error.code === "ENOENT") return;
|
|
157
432
|
throw error;
|
|
158
433
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
carry(from, to) {
|
|
167
|
-
for (const map of [this.modes, this.times]) {
|
|
168
|
-
for (const [key, value] of [...map]) {
|
|
169
|
-
if (!within(key, from)) continue;
|
|
170
|
-
map.delete(key);
|
|
171
|
-
map.set(to + key.slice(from.length), value);
|
|
434
|
+
if (found.handle) {
|
|
435
|
+
const [dir, name2] = await this.parent(found.path);
|
|
436
|
+
try {
|
|
437
|
+
await dir.removeEntry(name2, { recursive: options.recursive === true });
|
|
438
|
+
} catch (err) {
|
|
439
|
+
throw translate(err, path);
|
|
172
440
|
}
|
|
173
441
|
}
|
|
442
|
+
await this.meta.remove([found.path]);
|
|
174
443
|
}
|
|
175
444
|
async rename(from, to) {
|
|
176
445
|
if (from === to) return;
|
|
177
|
-
const source = await this.
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
446
|
+
const source = await this.locate(from, false);
|
|
447
|
+
const parent = await this.locate(parentOf2(to), true);
|
|
448
|
+
const destination = resolvePath(parent.path, baseOf(to));
|
|
449
|
+
const sourceStat = await this.describe(source);
|
|
450
|
+
if (sourceStat.isDirectory && within(destination, source.path)) throw fsError("EINVAL", to);
|
|
451
|
+
const target = await this.locate(destination, false).catch((err) => {
|
|
452
|
+
if (missing(err)) return null;
|
|
453
|
+
throw err;
|
|
454
|
+
});
|
|
455
|
+
const targetStat = target && await this.describe(target);
|
|
456
|
+
if (targetStat?.isDirectory && !sourceStat.isDirectory) throw fsError("EISDIR", to);
|
|
457
|
+
if (targetStat && !targetStat.isDirectory && sourceStat.isDirectory) {
|
|
458
|
+
throw fsError("ENOTDIR", to);
|
|
459
|
+
}
|
|
460
|
+
if (targetStat?.isDirectory && (await this.readdir(destination)).length > 0) {
|
|
183
461
|
throw fsError("ENOTEMPTY", to);
|
|
184
462
|
}
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
const carry = (entry) => ({
|
|
465
|
+
ino: inodeOf(source.path),
|
|
466
|
+
...entry,
|
|
467
|
+
path: destination,
|
|
468
|
+
ctimeMs: now
|
|
469
|
+
});
|
|
470
|
+
if (source.link) {
|
|
471
|
+
if (target?.handle) await this.rm(destination);
|
|
472
|
+
await this.meta.move(source.path, destination, carry);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const dir = parent.handle;
|
|
476
|
+
const name2 = baseOf(destination);
|
|
477
|
+
const swap = targetStat?.isDirectory === true;
|
|
478
|
+
const staging = swap ? `.${name2}.${crypto.randomUUID()}` : name2;
|
|
479
|
+
const timed = await this.explicitTimes(source.path);
|
|
480
|
+
const moved2 = await this.place(source.handle, dir, staging);
|
|
481
|
+
if (swap) await this.rm(destination, { recursive: true });
|
|
482
|
+
await this.meta.move(source.path, destination, carry);
|
|
483
|
+
if (!moved2) await this.rm(source.path, { recursive: true });
|
|
484
|
+
let copied = !moved2;
|
|
485
|
+
if (swap) {
|
|
486
|
+
const staged = await dir.getDirectoryHandle(staging);
|
|
487
|
+
const placed = await this.place(staged, dir, name2);
|
|
488
|
+
if (!placed) await dir.removeEntry(staging, { recursive: true });
|
|
489
|
+
copied ||= !placed;
|
|
490
|
+
}
|
|
491
|
+
if (copied) await this.restamp(destination, timed);
|
|
492
|
+
}
|
|
493
|
+
async modified(path) {
|
|
494
|
+
const handle = await this.handle(path);
|
|
495
|
+
return handle.kind === "file" ? (await handle.getFile()).lastModified : 0;
|
|
496
|
+
}
|
|
497
|
+
async explicitTimes(path) {
|
|
498
|
+
const timed = [];
|
|
499
|
+
for (const entry of await this.meta.under(path)) {
|
|
500
|
+
if (entry.mtimeFor === void 0 || entry.link !== void 0) continue;
|
|
501
|
+
const current = await this.modified(entry.path).catch(() => void 0);
|
|
502
|
+
if (current === entry.mtimeFor) timed.push(entry.path.slice(path.length));
|
|
503
|
+
}
|
|
504
|
+
return timed;
|
|
505
|
+
}
|
|
506
|
+
async restamp(path, timed) {
|
|
507
|
+
for (const rest of timed) {
|
|
508
|
+
const mtimeFor = await this.modified(path + rest);
|
|
509
|
+
await this.meta.update(path + rest, (entry) => entry && { ...entry, mtimeFor });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async place(handle, dir, name2) {
|
|
513
|
+
const moved2 = await handle.move?.(dir, name2).then(
|
|
199
514
|
() => true,
|
|
200
515
|
() => false
|
|
201
516
|
);
|
|
202
|
-
if (
|
|
517
|
+
if (moved2) return true;
|
|
203
518
|
try {
|
|
204
|
-
await this.copy(handle, dir,
|
|
519
|
+
await this.copy(handle, dir, name2);
|
|
205
520
|
} catch (err) {
|
|
206
|
-
if (handle.kind === "directory") await dir.removeEntry(
|
|
207
|
-
throw translate(err,
|
|
521
|
+
if (handle.kind === "directory") await dir.removeEntry(name2, { recursive: true });
|
|
522
|
+
throw translate(err, name2);
|
|
208
523
|
}
|
|
209
524
|
return false;
|
|
210
525
|
}
|
|
211
|
-
async copy(handle, dir,
|
|
526
|
+
async copy(handle, dir, name2) {
|
|
212
527
|
if (handle.kind === "file") {
|
|
213
|
-
const writable = await (await dir.getFileHandle(
|
|
528
|
+
const writable = await (await dir.getFileHandle(name2, { create: true })).createWritable();
|
|
214
529
|
await writable.write(await handle.getFile());
|
|
215
530
|
await writable.close();
|
|
216
531
|
return;
|
|
217
532
|
}
|
|
218
|
-
const target = await dir.getDirectoryHandle(
|
|
533
|
+
const target = await dir.getDirectoryHandle(name2, { create: true });
|
|
219
534
|
for await (const child of handle.values()) await this.copy(child, target, child.name);
|
|
220
535
|
}
|
|
221
|
-
async symlink(
|
|
222
|
-
|
|
536
|
+
async symlink(target, path) {
|
|
537
|
+
const parent = await this.locate(parentOf2(path), true);
|
|
538
|
+
if (parent.handle?.kind !== "directory") throw fsError("ENOTDIR", path);
|
|
539
|
+
const at = resolvePath(parent.path, baseOf(path));
|
|
540
|
+
if (await this.locate(at, false).then(
|
|
541
|
+
() => true,
|
|
542
|
+
(err) => !missing(err)
|
|
543
|
+
)) {
|
|
544
|
+
throw fsError("EEXIST", path);
|
|
545
|
+
}
|
|
546
|
+
const now = Date.now();
|
|
547
|
+
await this.meta.update(at, () => ({
|
|
548
|
+
path: at,
|
|
549
|
+
link: target,
|
|
550
|
+
dir: parent.path,
|
|
551
|
+
ino: inodeOf(at),
|
|
552
|
+
mtimeMs: now,
|
|
553
|
+
ctimeMs: now
|
|
554
|
+
}));
|
|
223
555
|
}
|
|
224
556
|
async readlink(path) {
|
|
225
|
-
await this.
|
|
557
|
+
const found = await this.locate(path, false);
|
|
558
|
+
if (found.link) return found.link.link;
|
|
226
559
|
throw fsError("EINVAL", path);
|
|
227
560
|
}
|
|
228
561
|
async chmod(path, mode) {
|
|
229
|
-
await this.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
562
|
+
const found = await this.locate(path, true);
|
|
563
|
+
const ctimeMs = Date.now();
|
|
564
|
+
await this.meta.update(found.path, (entry) => ({
|
|
565
|
+
...entry,
|
|
566
|
+
path: found.path,
|
|
567
|
+
mode: mode & PERM_MASK,
|
|
568
|
+
ctimeMs
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
async utimes(path, atime, mtime) {
|
|
572
|
+
const found = await this.locate(path, true);
|
|
573
|
+
const handle = found.handle;
|
|
574
|
+
const file = handle.kind === "file" ? await handle.getFile() : void 0;
|
|
575
|
+
const ctimeMs = Date.now();
|
|
576
|
+
await this.meta.update(found.path, (entry) => ({
|
|
577
|
+
...entry,
|
|
578
|
+
path: found.path,
|
|
579
|
+
atimeMs: atime.getTime(),
|
|
580
|
+
mtimeMs: mtime.getTime(),
|
|
581
|
+
mtimeFor: file?.lastModified ?? 0,
|
|
582
|
+
ctimeMs
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
async reconcile() {
|
|
586
|
+
let removed = 0;
|
|
587
|
+
for (const entry of await this.meta.all()) {
|
|
588
|
+
const real = await this.real(entry.path);
|
|
589
|
+
const stale = entry.link === void 0 ? !real : real || !await this.real(parentOf2(entry.path));
|
|
590
|
+
if (!stale) continue;
|
|
591
|
+
await this.meta.update(entry.path, () => void 0);
|
|
592
|
+
removed++;
|
|
593
|
+
}
|
|
594
|
+
return removed;
|
|
235
595
|
}
|
|
236
596
|
};
|
|
237
597
|
|
|
238
|
-
// src/kernel/
|
|
239
|
-
var
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
BUS: 7,
|
|
247
|
-
FPE: 8,
|
|
248
|
-
KILL: 9,
|
|
249
|
-
USR1: 10,
|
|
250
|
-
SEGV: 11,
|
|
251
|
-
USR2: 12,
|
|
252
|
-
PIPE: 13,
|
|
253
|
-
ALRM: 14,
|
|
254
|
-
TERM: 15,
|
|
255
|
-
STKFLT: 16,
|
|
256
|
-
CHLD: 17,
|
|
257
|
-
CONT: 18,
|
|
258
|
-
STOP: 19,
|
|
259
|
-
TSTP: 20,
|
|
260
|
-
TTIN: 21,
|
|
261
|
-
TTOU: 22,
|
|
262
|
-
URG: 23,
|
|
263
|
-
XCPU: 24,
|
|
264
|
-
XFSZ: 25,
|
|
265
|
-
VTALRM: 26,
|
|
266
|
-
PROF: 27,
|
|
267
|
-
WINCH: 28,
|
|
268
|
-
POLL: 29,
|
|
269
|
-
PWR: 30,
|
|
270
|
-
SYS: 31
|
|
271
|
-
};
|
|
272
|
-
var IGNORED_BY_DEFAULT = /* @__PURE__ */ new Set([SIG.CHLD, SIG.URG, SIG.WINCH]);
|
|
273
|
-
var STOPPING = /* @__PURE__ */ new Set([SIG.STOP, SIG.TSTP, SIG.TTIN, SIG.TTOU]);
|
|
274
|
-
function defaultAction(sig) {
|
|
275
|
-
if (sig === SIG.CONT) return "continue";
|
|
276
|
-
if (STOPPING.has(sig)) return "stop";
|
|
277
|
-
return IGNORED_BY_DEFAULT.has(sig) ? "ignore" : "terminate";
|
|
278
|
-
}
|
|
279
|
-
function isSignal(sig) {
|
|
280
|
-
return Number.isInteger(sig) && sig >= 1 && sig <= 31;
|
|
281
|
-
}
|
|
282
|
-
function sigbit(sig) {
|
|
283
|
-
return 1 << sig;
|
|
284
|
-
}
|
|
285
|
-
var SIGNAL_BY_NAME = {
|
|
286
|
-
SIGINT: SIG.INT,
|
|
287
|
-
SIGTERM: SIG.TERM,
|
|
288
|
-
SIGKILL: SIG.KILL,
|
|
289
|
-
SIGSTOP: SIG.STOP,
|
|
290
|
-
SIGCONT: SIG.CONT
|
|
598
|
+
// src/kernel/pipe.ts
|
|
599
|
+
var PIPE_CAPACITY = 64 * 1024;
|
|
600
|
+
var PipeError = class extends Error {
|
|
601
|
+
code;
|
|
602
|
+
constructor(code) {
|
|
603
|
+
super(code);
|
|
604
|
+
this.code = code;
|
|
605
|
+
}
|
|
291
606
|
};
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return `${pkg}/${clean}`;
|
|
302
|
-
}
|
|
303
|
-
function envOf(pkg, raw) {
|
|
304
|
-
const env = {};
|
|
305
|
-
if (!raw || typeof raw !== "object") return env;
|
|
306
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
307
|
-
if (ENV_KEY.test(key) && typeof value === "string") env[key] = value.replace(PACKAGE, pkg);
|
|
607
|
+
var KernelPipe = class {
|
|
608
|
+
chunks = [];
|
|
609
|
+
size = 0;
|
|
610
|
+
readers = 0;
|
|
611
|
+
writers = 0;
|
|
612
|
+
waiters = [];
|
|
613
|
+
capacity;
|
|
614
|
+
constructor(capacity = PIPE_CAPACITY) {
|
|
615
|
+
this.capacity = capacity;
|
|
308
616
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
function argsOf(pkg, raw) {
|
|
312
|
-
if (!Array.isArray(raw) || raw.length === 0) return void 0;
|
|
313
|
-
if (!raw.every((arg) => typeof arg === "string")) return void 0;
|
|
314
|
-
return raw.map((arg) => arg.replace(PACKAGE, pkg));
|
|
315
|
-
}
|
|
316
|
-
function commandsOf(pkg, manifest) {
|
|
317
|
-
const slicc = manifest.slicc;
|
|
318
|
-
if (!slicc || typeof slicc !== "object" || (slicc.abi ?? "emscripten") !== "emscripten")
|
|
319
|
-
return [];
|
|
320
|
-
const entries = slicc.commands;
|
|
321
|
-
if (!entries || typeof entries !== "object") return [];
|
|
322
|
-
const shared = envOf(pkg, slicc.env);
|
|
323
|
-
const out = [];
|
|
324
|
-
for (const [name, raw] of Object.entries(entries)) {
|
|
325
|
-
if (!NAME.test(name) || name === "." || name === ".." || !raw || typeof raw !== "object") {
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
const glue = inside(pkg, raw.glue);
|
|
329
|
-
const wasm = inside(pkg, raw.wasm);
|
|
330
|
-
if ((raw.abi ?? "emscripten") !== "emscripten" || !glue || !wasm) continue;
|
|
331
|
-
const argv0 = typeof raw.argv0 === "string" && raw.argv0 ? raw.argv0 : name;
|
|
332
|
-
const args = argsOf(pkg, raw.args);
|
|
333
|
-
const env = { ...shared, ...envOf(pkg, raw.env) };
|
|
334
|
-
out.push({
|
|
335
|
-
name,
|
|
336
|
-
glue,
|
|
337
|
-
wasm,
|
|
338
|
-
argv0,
|
|
339
|
-
...args ? { args } : {},
|
|
340
|
-
...Object.keys(env).length > 0 ? { env } : {}
|
|
341
|
-
});
|
|
617
|
+
get buffered() {
|
|
618
|
+
return this.size;
|
|
342
619
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
async function packages(fs, modules) {
|
|
346
|
-
const names = await fs.readdir(modules).catch(() => []);
|
|
347
|
-
const dirs = [];
|
|
348
|
-
for (const name of names) {
|
|
349
|
-
if (name.startsWith(".")) continue;
|
|
350
|
-
if (!name.startsWith("@")) {
|
|
351
|
-
dirs.push(`${modules}/${name}`);
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
for (const inner of await fs.readdir(`${modules}/${name}`).catch(() => [])) {
|
|
355
|
-
dirs.push(`${modules}/${name}/${inner}`);
|
|
356
|
-
}
|
|
620
|
+
get readReady() {
|
|
621
|
+
return this.size > 0 || this.writers === 0;
|
|
357
622
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
async function scanCommands(fs, modules) {
|
|
361
|
-
const found = /* @__PURE__ */ new Map();
|
|
362
|
-
for (const pkg of await packages(fs, modules)) {
|
|
363
|
-
let manifest;
|
|
364
|
-
try {
|
|
365
|
-
manifest = JSON.parse(await fs.readFile(`${pkg}/package.json`));
|
|
366
|
-
} catch {
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
for (const command of commandsOf(pkg, manifest)) {
|
|
370
|
-
if (!found.has(command.name)) found.set(command.name, command);
|
|
371
|
-
}
|
|
623
|
+
get writeReady() {
|
|
624
|
+
return this.size < this.capacity || this.readers === 0;
|
|
372
625
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
return found;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// src/fs/commands.ts
|
|
380
|
-
var S_IFDIR2 = 16384;
|
|
381
|
-
var S_IFREG2 = 32768;
|
|
382
|
-
var COMMAND = /^\/(?:usr\/)?bin\/([^/]+)$/;
|
|
383
|
-
function missing(err) {
|
|
384
|
-
return err?.code === "ENOENT";
|
|
385
|
-
}
|
|
386
|
-
function synthetic(directory) {
|
|
387
|
-
return {
|
|
388
|
-
isFile: !directory,
|
|
389
|
-
isDirectory: directory,
|
|
390
|
-
isSymbolicLink: false,
|
|
391
|
-
size: 0,
|
|
392
|
-
mode: (directory ? S_IFDIR2 : S_IFREG2) | 493,
|
|
393
|
-
mtime: /* @__PURE__ */ new Date(0)
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
function withCommandDirs(fs, names) {
|
|
397
|
-
async function children(path) {
|
|
398
|
-
if (path === "/") return ["bin", "usr"];
|
|
399
|
-
if (path === "/usr") return ["bin"];
|
|
400
|
-
if (path === "/bin" || path === "/usr/bin") return [...await names()];
|
|
401
|
-
return void 0;
|
|
402
|
-
}
|
|
403
|
-
async function virtual(path) {
|
|
404
|
-
if (path === "/usr" || path === "/bin" || path === "/usr/bin") return synthetic(true);
|
|
405
|
-
const name = COMMAND.exec(path)?.[1];
|
|
406
|
-
return name !== void 0 && (await names()).has(name) ? synthetic(false) : void 0;
|
|
407
|
-
}
|
|
408
|
-
async function stat(path, real) {
|
|
409
|
-
try {
|
|
410
|
-
return await real(path);
|
|
411
|
-
} catch (err) {
|
|
412
|
-
const found = missing(err) ? await virtual(path) : void 0;
|
|
413
|
-
if (found) return found;
|
|
414
|
-
throw err;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
return {
|
|
418
|
-
resolvePath: (base, path) => fs.resolvePath(base, path),
|
|
419
|
-
readFile: (path) => fs.readFile(path),
|
|
420
|
-
async readFileBuffer(path) {
|
|
421
|
-
try {
|
|
422
|
-
return await fs.readFileBuffer(path);
|
|
423
|
-
} catch (err) {
|
|
424
|
-
if (missing(err) && (await virtual(path))?.isFile) return new Uint8Array(0);
|
|
425
|
-
throw err;
|
|
426
|
-
}
|
|
427
|
-
},
|
|
428
|
-
writeFile: (path, content) => fs.writeFile(path, content),
|
|
429
|
-
async exists(path) {
|
|
430
|
-
return await fs.exists(path) || await virtual(path) !== void 0;
|
|
431
|
-
},
|
|
432
|
-
stat: (path) => stat(path, (p) => fs.stat(p)),
|
|
433
|
-
lstat: (path) => stat(path, (p) => fs.lstat(p)),
|
|
434
|
-
async readdir(path) {
|
|
435
|
-
const extra = await children(path);
|
|
436
|
-
if (!extra) return fs.readdir(path);
|
|
437
|
-
const real = await fs.readdir(path).catch((err) => {
|
|
438
|
-
if (missing(err)) return [];
|
|
439
|
-
throw err;
|
|
440
|
-
});
|
|
441
|
-
return [.../* @__PURE__ */ new Set([...real, ...extra])].sort();
|
|
442
|
-
},
|
|
443
|
-
mkdir: (path, options) => fs.mkdir(path, options),
|
|
444
|
-
rm: (path, options) => fs.rm(path, options),
|
|
445
|
-
rename: (from, to) => fs.rename(from, to),
|
|
446
|
-
symlink: (target, path) => fs.symlink(target, path),
|
|
447
|
-
readlink: (path) => fs.readlink(path),
|
|
448
|
-
chmod: (path, mode) => fs.chmod(path, mode),
|
|
449
|
-
utimes: (path, atime, mtime) => fs.utimes(path, atime, mtime)
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// src/kernel/pipe.ts
|
|
454
|
-
var PIPE_CAPACITY = 64 * 1024;
|
|
455
|
-
var PipeError = class extends Error {
|
|
456
|
-
code;
|
|
457
|
-
constructor(code) {
|
|
458
|
-
super(code);
|
|
459
|
-
this.code = code;
|
|
460
|
-
}
|
|
461
|
-
};
|
|
462
|
-
var KernelPipe = class {
|
|
463
|
-
chunks = [];
|
|
464
|
-
size = 0;
|
|
465
|
-
readers = 0;
|
|
466
|
-
writers = 0;
|
|
467
|
-
waiters = [];
|
|
468
|
-
capacity;
|
|
469
|
-
constructor(capacity = PIPE_CAPACITY) {
|
|
470
|
-
this.capacity = capacity;
|
|
471
|
-
}
|
|
472
|
-
get buffered() {
|
|
473
|
-
return this.size;
|
|
474
|
-
}
|
|
475
|
-
get readReady() {
|
|
476
|
-
return this.size > 0 || this.writers === 0;
|
|
477
|
-
}
|
|
478
|
-
get writeReady() {
|
|
479
|
-
return this.size < this.capacity || this.readers === 0;
|
|
480
|
-
}
|
|
481
|
-
get writersGone() {
|
|
482
|
-
return this.writers === 0;
|
|
626
|
+
get writersGone() {
|
|
627
|
+
return this.writers === 0;
|
|
483
628
|
}
|
|
484
629
|
get readersGone() {
|
|
485
630
|
return this.readers === 0;
|
|
@@ -518,10 +663,10 @@ var KernelPipe = class {
|
|
|
518
663
|
}
|
|
519
664
|
const out = new Uint8Array(Math.min(max, this.size));
|
|
520
665
|
let filled = 0;
|
|
521
|
-
for (const
|
|
666
|
+
for (const chunk2 of this.chunks) {
|
|
522
667
|
if (filled === out.length) break;
|
|
523
|
-
const count = Math.min(
|
|
524
|
-
out.set(
|
|
668
|
+
const count = Math.min(chunk2.length, out.length - filled);
|
|
669
|
+
out.set(chunk2.subarray(0, count), filled);
|
|
525
670
|
filled += count;
|
|
526
671
|
}
|
|
527
672
|
return out;
|
|
@@ -742,57 +887,1752 @@ var FdTable = class _FdTable {
|
|
|
742
887
|
statusFlags(fd) {
|
|
743
888
|
return this.status.get(fd);
|
|
744
889
|
}
|
|
745
|
-
install(file, min = 0) {
|
|
746
|
-
for (let fd = min; fd < _FdTable.MAX_FDS; fd++) {
|
|
747
|
-
if (!this.fds.has(fd)) {
|
|
748
|
-
this.fds.set(fd, file);
|
|
749
|
-
return fd;
|
|
750
|
-
}
|
|
890
|
+
install(file, min = 0) {
|
|
891
|
+
for (let fd = min; fd < _FdTable.MAX_FDS; fd++) {
|
|
892
|
+
if (!this.fds.has(fd)) {
|
|
893
|
+
this.fds.set(fd, file);
|
|
894
|
+
return fd;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
void Promise.resolve(file.release()).catch(() => void 0);
|
|
898
|
+
throw new KernelError("EMFILE");
|
|
899
|
+
}
|
|
900
|
+
installAt(fd, file) {
|
|
901
|
+
if (fd < 0 || fd >= _FdTable.MAX_FDS) {
|
|
902
|
+
void Promise.resolve(file.release()).catch(() => void 0);
|
|
903
|
+
throw new KernelError("EBADF");
|
|
904
|
+
}
|
|
905
|
+
const previous = this.fds.get(fd);
|
|
906
|
+
this.fds.set(fd, file);
|
|
907
|
+
this.cloexec.delete(fd);
|
|
908
|
+
this.status.delete(fd);
|
|
909
|
+
void Promise.resolve(previous?.release()).catch(() => void 0);
|
|
910
|
+
}
|
|
911
|
+
dup(fd, min = 0) {
|
|
912
|
+
return this.install(this.get(fd).retain(), min);
|
|
913
|
+
}
|
|
914
|
+
dup2(oldFd, newFd) {
|
|
915
|
+
const file = this.get(oldFd);
|
|
916
|
+
if (oldFd !== newFd) this.installAt(newFd, file.retain());
|
|
917
|
+
return newFd;
|
|
918
|
+
}
|
|
919
|
+
close(fd) {
|
|
920
|
+
const file = this.get(fd);
|
|
921
|
+
this.fds.delete(fd);
|
|
922
|
+
this.cloexec.delete(fd);
|
|
923
|
+
this.status.delete(fd);
|
|
924
|
+
return file.release();
|
|
925
|
+
}
|
|
926
|
+
fork() {
|
|
927
|
+
const child = new _FdTable();
|
|
928
|
+
for (const [fd, file] of this.fds) child.fds.set(fd, file.retain());
|
|
929
|
+
for (const fd of this.cloexec) child.cloexec.add(fd);
|
|
930
|
+
for (const [fd, flags] of this.status) child.status.set(fd, flags);
|
|
931
|
+
return child;
|
|
932
|
+
}
|
|
933
|
+
async closeAll() {
|
|
934
|
+
const files = [...this.fds.values()];
|
|
935
|
+
this.fds.clear();
|
|
936
|
+
this.cloexec.clear();
|
|
937
|
+
this.status.clear();
|
|
938
|
+
await Promise.allSettled(files.map((file) => Promise.resolve().then(() => file.release())));
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// src/kernel/net/http1.ts
|
|
943
|
+
var HttpError = class extends Error {
|
|
944
|
+
status;
|
|
945
|
+
constructor(status, message2) {
|
|
946
|
+
super(message2);
|
|
947
|
+
this.status = status;
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
var CR = 13;
|
|
951
|
+
var LF = 10;
|
|
952
|
+
var READ_SIZE = 64 * 1024;
|
|
953
|
+
var TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
954
|
+
var REASON = {
|
|
955
|
+
100: "Continue",
|
|
956
|
+
200: "OK",
|
|
957
|
+
400: "Bad Request",
|
|
958
|
+
403: "Forbidden",
|
|
959
|
+
408: "Request Timeout",
|
|
960
|
+
411: "Length Required",
|
|
961
|
+
413: "Content Too Large",
|
|
962
|
+
417: "Expectation Failed",
|
|
963
|
+
421: "Misdirected Request",
|
|
964
|
+
431: "Request Header Fields Too Large",
|
|
965
|
+
501: "Not Implemented",
|
|
966
|
+
502: "Bad Gateway",
|
|
967
|
+
503: "Service Unavailable",
|
|
968
|
+
505: "HTTP Version Not Supported"
|
|
969
|
+
};
|
|
970
|
+
var Incoming = class {
|
|
971
|
+
source;
|
|
972
|
+
buf = new Uint8Array(0);
|
|
973
|
+
eof = false;
|
|
974
|
+
constructor(source) {
|
|
975
|
+
this.source = source;
|
|
976
|
+
}
|
|
977
|
+
get buffered() {
|
|
978
|
+
return this.buf.length;
|
|
979
|
+
}
|
|
980
|
+
async fill(signal) {
|
|
981
|
+
if (this.eof) return false;
|
|
982
|
+
const chunk2 = await this.source.read(READ_SIZE, signal);
|
|
983
|
+
if (chunk2.length === 0) {
|
|
984
|
+
this.eof = true;
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
const next = new Uint8Array(this.buf.length + chunk2.length);
|
|
988
|
+
next.set(this.buf);
|
|
989
|
+
next.set(chunk2, this.buf.length);
|
|
990
|
+
this.buf = next;
|
|
991
|
+
return true;
|
|
992
|
+
}
|
|
993
|
+
take(n) {
|
|
994
|
+
const out = this.buf.slice(0, n);
|
|
995
|
+
this.buf = this.buf.subarray(n);
|
|
996
|
+
return out;
|
|
997
|
+
}
|
|
998
|
+
async some(max, signal) {
|
|
999
|
+
if (this.buf.length > 0) return this.take(Math.min(max, this.buf.length));
|
|
1000
|
+
if (this.eof) return new Uint8Array(0);
|
|
1001
|
+
const chunk2 = await this.source.read(max, signal);
|
|
1002
|
+
if (chunk2.length === 0) this.eof = true;
|
|
1003
|
+
return chunk2;
|
|
1004
|
+
}
|
|
1005
|
+
async head(limit, signal) {
|
|
1006
|
+
let from = 0;
|
|
1007
|
+
for (; ; ) {
|
|
1008
|
+
const end = headEnd(this.buf, from);
|
|
1009
|
+
if (end > 0) {
|
|
1010
|
+
if (end > limit) throw new HttpError(431, "request head too large");
|
|
1011
|
+
return this.take(end);
|
|
1012
|
+
}
|
|
1013
|
+
if (this.buf.length > limit) throw new HttpError(431, "request head too large");
|
|
1014
|
+
from = Math.max(0, this.buf.length - 3);
|
|
1015
|
+
if (!await this.fill(signal)) {
|
|
1016
|
+
if (this.buf.length === 0) return null;
|
|
1017
|
+
throw new HttpError(400, "connection closed inside a request head");
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
async line(limit, signal) {
|
|
1022
|
+
let from = 0;
|
|
1023
|
+
for (; ; ) {
|
|
1024
|
+
const at = this.buf.indexOf(LF, from);
|
|
1025
|
+
if (at >= 0) {
|
|
1026
|
+
const line = this.take(at + 1);
|
|
1027
|
+
const text = latin1(line.subarray(0, line[at - 1] === CR ? at - 1 : at));
|
|
1028
|
+
return text;
|
|
1029
|
+
}
|
|
1030
|
+
if (this.buf.length > limit) throw new HttpError(400, "line too long");
|
|
1031
|
+
from = this.buf.length;
|
|
1032
|
+
if (!await this.fill(signal)) throw new HttpError(400, "connection closed inside a body");
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
async exactly(n, signal) {
|
|
1036
|
+
while (this.buf.length < n) {
|
|
1037
|
+
if (!await this.fill(signal)) throw new HttpError(400, "connection closed inside a body");
|
|
1038
|
+
}
|
|
1039
|
+
return this.take(n);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
function headEnd(buf, from) {
|
|
1043
|
+
for (let i = from; i < buf.length; i++) {
|
|
1044
|
+
if (buf[i] !== LF) continue;
|
|
1045
|
+
if (buf[i + 1] === LF) return i + 2;
|
|
1046
|
+
if (buf[i + 1] === CR && buf[i + 2] === LF) return i + 3;
|
|
1047
|
+
}
|
|
1048
|
+
return 0;
|
|
1049
|
+
}
|
|
1050
|
+
function latin1(bytes) {
|
|
1051
|
+
let out = "";
|
|
1052
|
+
for (let i = 0; i < bytes.length; i += 32768) {
|
|
1053
|
+
out += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
1054
|
+
}
|
|
1055
|
+
return out;
|
|
1056
|
+
}
|
|
1057
|
+
function latin1Bytes(text) {
|
|
1058
|
+
const out = new Uint8Array(text.length);
|
|
1059
|
+
for (let i = 0; i < text.length; i++) out[i] = text.charCodeAt(i) & 255;
|
|
1060
|
+
return out;
|
|
1061
|
+
}
|
|
1062
|
+
function parseRequestHead(bytes) {
|
|
1063
|
+
const lines = latin1(bytes).split(/\r?\n/);
|
|
1064
|
+
while (lines.length > 0 && lines[0] === "") lines.shift();
|
|
1065
|
+
const match = /^([!#$%&'*+\-.^_`|~0-9A-Za-z]+) (\S+) HTTP\/(\d)\.(\d)$/.exec(lines.shift() ?? "");
|
|
1066
|
+
if (!match) throw new HttpError(400, "malformed request line");
|
|
1067
|
+
if (match[3] !== "1") throw new HttpError(505, `HTTP/${match[3]}.${match[4]} is not supported`);
|
|
1068
|
+
return {
|
|
1069
|
+
method: match[1],
|
|
1070
|
+
target: match[2],
|
|
1071
|
+
minor: Number(match[4]),
|
|
1072
|
+
headers: parseFields(lines)
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
function parseFields(lines) {
|
|
1076
|
+
const headers = [];
|
|
1077
|
+
for (const line of lines) {
|
|
1078
|
+
if (line === "") continue;
|
|
1079
|
+
if (line.startsWith(" ") || line.startsWith(" ")) {
|
|
1080
|
+
throw new HttpError(400, "obsolete line folding");
|
|
1081
|
+
}
|
|
1082
|
+
const colon = line.indexOf(":");
|
|
1083
|
+
const name2 = colon > 0 ? line.slice(0, colon) : "";
|
|
1084
|
+
if (!TOKEN.test(name2)) throw new HttpError(400, "malformed header field");
|
|
1085
|
+
headers.push([name2, line.slice(colon + 1).trim()]);
|
|
1086
|
+
}
|
|
1087
|
+
return headers;
|
|
1088
|
+
}
|
|
1089
|
+
function fieldValues(headers, name2) {
|
|
1090
|
+
const lower = name2.toLowerCase();
|
|
1091
|
+
return headers.filter(([n]) => n.toLowerCase() === lower).map(([, v]) => v);
|
|
1092
|
+
}
|
|
1093
|
+
function fieldTokens(headers, name2) {
|
|
1094
|
+
return fieldValues(headers, name2).flatMap((v) => v.split(",")).map((t) => t.trim().toLowerCase()).filter((t) => t !== "");
|
|
1095
|
+
}
|
|
1096
|
+
function requestFraming(headers) {
|
|
1097
|
+
const codings = fieldTokens(headers, "transfer-encoding");
|
|
1098
|
+
const lengths = fieldValues(headers, "content-length");
|
|
1099
|
+
if (codings.length > 0) {
|
|
1100
|
+
if (lengths.length > 0) throw new HttpError(400, "both Transfer-Encoding and Content-Length");
|
|
1101
|
+
if (codings.length !== 1 || codings[0] !== "chunked") {
|
|
1102
|
+
throw new HttpError(501, `unsupported Transfer-Encoding: ${codings.join(", ")}`);
|
|
1103
|
+
}
|
|
1104
|
+
return { kind: "chunked" };
|
|
1105
|
+
}
|
|
1106
|
+
if (lengths.length === 0) return { kind: "none" };
|
|
1107
|
+
const distinct = new Set(lengths.flatMap((v) => v.split(",")).map((v) => v.trim()));
|
|
1108
|
+
const [only] = distinct;
|
|
1109
|
+
if (distinct.size !== 1 || !/^\d{1,15}$/.test(only)) {
|
|
1110
|
+
throw new HttpError(400, "invalid Content-Length");
|
|
1111
|
+
}
|
|
1112
|
+
const length2 = Number(only);
|
|
1113
|
+
return length2 === 0 ? { kind: "none" } : { kind: "length", length: length2 };
|
|
1114
|
+
}
|
|
1115
|
+
var MAX_CHUNK_LINE = 4096;
|
|
1116
|
+
var MAX_TRAILERS = 64 * 1024;
|
|
1117
|
+
async function readBody(incoming, framing, cap, signal) {
|
|
1118
|
+
if (framing.kind === "none") return void 0;
|
|
1119
|
+
if (framing.kind === "length") {
|
|
1120
|
+
if (framing.length > cap) throw new HttpError(413, `request body over ${cap} bytes`);
|
|
1121
|
+
return incoming.exactly(framing.length, signal);
|
|
1122
|
+
}
|
|
1123
|
+
const chunks = [];
|
|
1124
|
+
let total = 0;
|
|
1125
|
+
for (; ; ) {
|
|
1126
|
+
const sizeLine = await incoming.line(MAX_CHUNK_LINE, signal);
|
|
1127
|
+
const size = /^([0-9A-Fa-f]{1,12})[ \t]*(;.*)?$/.exec(sizeLine);
|
|
1128
|
+
if (!size) throw new HttpError(400, "malformed chunk size");
|
|
1129
|
+
const n = Number.parseInt(size[1], 16);
|
|
1130
|
+
if (n === 0) break;
|
|
1131
|
+
total += n;
|
|
1132
|
+
if (total > cap) throw new HttpError(413, `request body over ${cap} bytes`);
|
|
1133
|
+
chunks.push(await incoming.exactly(n, signal));
|
|
1134
|
+
if (await incoming.line(2, signal) !== "") throw new HttpError(400, "malformed chunk");
|
|
1135
|
+
}
|
|
1136
|
+
let trailers = 0;
|
|
1137
|
+
for (let line = await incoming.line(MAX_TRAILERS, signal); line !== ""; ) {
|
|
1138
|
+
trailers += line.length;
|
|
1139
|
+
if (trailers > MAX_TRAILERS) throw new HttpError(431, "trailers too large");
|
|
1140
|
+
line = await incoming.line(MAX_TRAILERS, signal);
|
|
1141
|
+
}
|
|
1142
|
+
return concat(chunks, total);
|
|
1143
|
+
}
|
|
1144
|
+
function concat(chunks, total = chunks.reduce((n, c) => n + c.length, 0)) {
|
|
1145
|
+
const out = new Uint8Array(total);
|
|
1146
|
+
let at = 0;
|
|
1147
|
+
for (const chunk2 of chunks) {
|
|
1148
|
+
out.set(chunk2, at);
|
|
1149
|
+
at += chunk2.length;
|
|
1150
|
+
}
|
|
1151
|
+
return out;
|
|
1152
|
+
}
|
|
1153
|
+
function safeValue(value) {
|
|
1154
|
+
return !/[\r\n\0]/.test(value);
|
|
1155
|
+
}
|
|
1156
|
+
function responseHead(status, reason, headers) {
|
|
1157
|
+
const phrase = /^[\t\x20-\x7e\x80-\xff]*$/.test(reason) ? reason : "";
|
|
1158
|
+
let head = `HTTP/1.1 ${status} ${phrase}\r
|
|
1159
|
+
`;
|
|
1160
|
+
for (const [name2, value] of headers) {
|
|
1161
|
+
if (TOKEN.test(name2) && safeValue(value)) head += `${name2}: ${value}\r
|
|
1162
|
+
`;
|
|
1163
|
+
}
|
|
1164
|
+
return latin1Bytes(`${head}\r
|
|
1165
|
+
`);
|
|
1166
|
+
}
|
|
1167
|
+
function chunk(bytes) {
|
|
1168
|
+
const size = latin1Bytes(`${bytes.length.toString(16)}\r
|
|
1169
|
+
`);
|
|
1170
|
+
const out = new Uint8Array(size.length + bytes.length + 2);
|
|
1171
|
+
out.set(size);
|
|
1172
|
+
out.set(bytes, size.length);
|
|
1173
|
+
out[out.length - 2] = CR;
|
|
1174
|
+
out[out.length - 1] = LF;
|
|
1175
|
+
return out;
|
|
1176
|
+
}
|
|
1177
|
+
var LAST_CHUNK = latin1Bytes("0\r\n\r\n");
|
|
1178
|
+
|
|
1179
|
+
// src/kernel/net/proxy-service.ts
|
|
1180
|
+
var REALM_PROXY_PORT = 3128;
|
|
1181
|
+
var DEFAULT_LIMITS = {
|
|
1182
|
+
maxConnections: 64,
|
|
1183
|
+
maxHead: 64 * 1024,
|
|
1184
|
+
idleMs: 12e4,
|
|
1185
|
+
bodyBudget: 128 * 1024 * 1024
|
|
1186
|
+
};
|
|
1187
|
+
var HOP_BY_HOP = /* @__PURE__ */ new Set([
|
|
1188
|
+
"connection",
|
|
1189
|
+
"keep-alive",
|
|
1190
|
+
"proxy-connection",
|
|
1191
|
+
"proxy-authenticate",
|
|
1192
|
+
"proxy-authorization",
|
|
1193
|
+
"te",
|
|
1194
|
+
"trailer",
|
|
1195
|
+
"transfer-encoding",
|
|
1196
|
+
"upgrade"
|
|
1197
|
+
]);
|
|
1198
|
+
var REQUEST_OWN = /* @__PURE__ */ new Set(["host", "content-length", "expect"]);
|
|
1199
|
+
var Budget = class {
|
|
1200
|
+
free;
|
|
1201
|
+
waiters = [];
|
|
1202
|
+
constructor(free) {
|
|
1203
|
+
this.free = free;
|
|
1204
|
+
}
|
|
1205
|
+
async acquire(n, signal) {
|
|
1206
|
+
while (this.free < n) {
|
|
1207
|
+
signal.throwIfAborted();
|
|
1208
|
+
await new Promise((resolve) => {
|
|
1209
|
+
const done3 = () => {
|
|
1210
|
+
signal.removeEventListener("abort", done3);
|
|
1211
|
+
resolve();
|
|
1212
|
+
};
|
|
1213
|
+
this.waiters.push(done3);
|
|
1214
|
+
signal.addEventListener("abort", done3, { once: true });
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
this.free -= n;
|
|
1218
|
+
}
|
|
1219
|
+
release(n) {
|
|
1220
|
+
this.free += n;
|
|
1221
|
+
const waiters = this.waiters;
|
|
1222
|
+
this.waiters = [];
|
|
1223
|
+
for (const wake of waiters) wake();
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
function canonicalHost(host) {
|
|
1227
|
+
const bare = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
1228
|
+
try {
|
|
1229
|
+
return new URL(`http://${bare}/`).hostname;
|
|
1230
|
+
} catch {
|
|
1231
|
+
return void 0;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
function localV4(a, b) {
|
|
1235
|
+
return a === 127 || a === 0 || a === 169 && b === 254;
|
|
1236
|
+
}
|
|
1237
|
+
function hextets(address) {
|
|
1238
|
+
const [head, tail, extra] = address.split("::");
|
|
1239
|
+
if (extra !== void 0) return void 0;
|
|
1240
|
+
const part = (text) => text ? text.split(":").map((h) => Number.parseInt(h, 16)) : [];
|
|
1241
|
+
const front = part(head);
|
|
1242
|
+
const back = part(tail);
|
|
1243
|
+
const groups = tail === void 0 ? front : [...front, ...new Array(8 - front.length - back.length).fill(0), ...back];
|
|
1244
|
+
return groups.length === 8 && groups.every((g) => g >= 0 && g <= 65535) ? groups : void 0;
|
|
1245
|
+
}
|
|
1246
|
+
function localV6(address) {
|
|
1247
|
+
const g = hextets(address);
|
|
1248
|
+
if (!g) return true;
|
|
1249
|
+
if ((g[0] & 65472) === 65152) return true;
|
|
1250
|
+
const zeroPrefix = g.slice(0, 5).every((x) => x === 0);
|
|
1251
|
+
if (!zeroPrefix) return false;
|
|
1252
|
+
if (g[5] === 65535 || g[5] === 0 && g[6] !== 0) return localV4(g[6] >> 8, g[6] & 255);
|
|
1253
|
+
return g[5] === 0 && g[6] === 0;
|
|
1254
|
+
}
|
|
1255
|
+
function isLoopbackHost(hostname) {
|
|
1256
|
+
const canonical = canonicalHost(hostname);
|
|
1257
|
+
if (canonical === void 0) return false;
|
|
1258
|
+
const host = canonical.replace(/\.+$/, "");
|
|
1259
|
+
if (host === "localhost" || host.endsWith(".localhost")) return true;
|
|
1260
|
+
if (host.startsWith("[")) return localV6(host.slice(1, -1));
|
|
1261
|
+
const v4 = /^(\d+)\.(\d+)\.\d+\.\d+$/.exec(host);
|
|
1262
|
+
return v4 ? localV4(Number(v4[1]), Number(v4[2])) : false;
|
|
1263
|
+
}
|
|
1264
|
+
function dropped(headers) {
|
|
1265
|
+
return /* @__PURE__ */ new Set([...HOP_BY_HOP, ...fieldTokens(headers, "connection")]);
|
|
1266
|
+
}
|
|
1267
|
+
function forwardRequestHeaders(headers) {
|
|
1268
|
+
const drop = dropped(headers);
|
|
1269
|
+
return headers.filter(([name2]) => {
|
|
1270
|
+
const lower = name2.toLowerCase();
|
|
1271
|
+
return !drop.has(lower) && !REQUEST_OWN.has(lower);
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
function forwardResponseHeaders(headers, opts) {
|
|
1275
|
+
const drop = dropped(headers);
|
|
1276
|
+
if (!opts.encodedBodies) drop.add("content-encoding");
|
|
1277
|
+
if (!(opts.encodedBodies && opts.bodiless)) drop.add("content-length");
|
|
1278
|
+
return headers.filter(([name2]) => !drop.has(name2.toLowerCase()));
|
|
1279
|
+
}
|
|
1280
|
+
function keepsAlive(req) {
|
|
1281
|
+
const tokens = fieldTokens(req.headers, "connection");
|
|
1282
|
+
if (tokens.includes("close")) return false;
|
|
1283
|
+
return req.minor >= 1 || tokens.includes("keep-alive");
|
|
1284
|
+
}
|
|
1285
|
+
function tunnelRequestUrl(req, origin) {
|
|
1286
|
+
const base = new URL(origin);
|
|
1287
|
+
let url;
|
|
1288
|
+
try {
|
|
1289
|
+
url = new URL(req.target, req.target.startsWith("/") ? base : void 0);
|
|
1290
|
+
} catch {
|
|
1291
|
+
throw new HttpError(400, "malformed request target");
|
|
1292
|
+
}
|
|
1293
|
+
const hosts = fieldValues(req.headers, "host");
|
|
1294
|
+
const hostOk = hosts.every(
|
|
1295
|
+
(h) => URL.canParse(`https://${h}`) && new URL(`https://${h}`).host === base.host
|
|
1296
|
+
);
|
|
1297
|
+
if (url.origin !== base.origin || !hostOk) {
|
|
1298
|
+
throw new HttpError(421, `this tunnel is for ${base.host}`);
|
|
1299
|
+
}
|
|
1300
|
+
return url.href;
|
|
1301
|
+
}
|
|
1302
|
+
function requestUrl(req) {
|
|
1303
|
+
if (req.target.startsWith("/")) {
|
|
1304
|
+
throw new HttpError(400, "this is a proxy: send the absolute URL (GET http://host/path)");
|
|
1305
|
+
}
|
|
1306
|
+
let url;
|
|
1307
|
+
try {
|
|
1308
|
+
url = new URL(req.target);
|
|
1309
|
+
} catch {
|
|
1310
|
+
throw new HttpError(400, "malformed request target");
|
|
1311
|
+
}
|
|
1312
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
1313
|
+
throw new HttpError(400, `unsupported scheme ${url.protocol}`);
|
|
1314
|
+
}
|
|
1315
|
+
if (isLoopbackHost(url.hostname)) {
|
|
1316
|
+
throw new HttpError(
|
|
1317
|
+
403,
|
|
1318
|
+
`${url.hostname} is the realm's own loopback, which the proxy does not reach: list it in no_proxy`
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
return url.href;
|
|
1322
|
+
}
|
|
1323
|
+
function tunnelTarget(req) {
|
|
1324
|
+
const match = /^([A-Za-z0-9.-]+|\[[0-9A-Fa-f:.]+\]):(\d{1,5})$/.exec(req.target);
|
|
1325
|
+
const port = Number(match?.[2]);
|
|
1326
|
+
if (!match || port < 1 || port > 65535) {
|
|
1327
|
+
throw new HttpError(400, "CONNECT needs host:port");
|
|
1328
|
+
}
|
|
1329
|
+
const host = canonicalHost(match[1]);
|
|
1330
|
+
if (host === void 0) throw new HttpError(400, "CONNECT needs host:port");
|
|
1331
|
+
if (isLoopbackHost(host)) {
|
|
1332
|
+
throw new HttpError(
|
|
1333
|
+
403,
|
|
1334
|
+
`${host} is the realm's own loopback, which the proxy does not reach: list it in no_proxy`
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
return { host, port };
|
|
1338
|
+
}
|
|
1339
|
+
async function watchHangup(conn, abort, until) {
|
|
1340
|
+
while (!until.aborted && !abort.signal.aborted) {
|
|
1341
|
+
if (conn.poll().hangup) {
|
|
1342
|
+
abort.abort(new KernelError("EPIPE"));
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
try {
|
|
1346
|
+
await conn.changed(until);
|
|
1347
|
+
} catch {
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
function untilAborted(response, signal) {
|
|
1353
|
+
return new Promise((resolve, reject) => {
|
|
1354
|
+
const onAbort = () => {
|
|
1355
|
+
reject(signal.reason ?? new KernelError("EINTR"));
|
|
1356
|
+
response.then((late) => late.cancel()).catch(() => void 0);
|
|
1357
|
+
};
|
|
1358
|
+
if (signal.aborted) return onAbort();
|
|
1359
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
1360
|
+
response.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
function plainResponse(status, message2) {
|
|
1364
|
+
const body = latin1Bytes(`slicc realm proxy: ${message2}
|
|
1365
|
+
`);
|
|
1366
|
+
return {
|
|
1367
|
+
status,
|
|
1368
|
+
statusText: REASON[status] ?? "",
|
|
1369
|
+
headers: [["Content-Type", "text/plain; charset=utf-8"]],
|
|
1370
|
+
body: (async function* () {
|
|
1371
|
+
yield body;
|
|
1372
|
+
})(),
|
|
1373
|
+
cancel: async () => void 0
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
function timedSource(conn, idleMs, stop) {
|
|
1377
|
+
return {
|
|
1378
|
+
read: (max) => conn.read(max, AbortSignal.any([stop, AbortSignal.timeout(idleMs)]))
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
var RealmProxy = class {
|
|
1382
|
+
options;
|
|
1383
|
+
listener;
|
|
1384
|
+
stop = new AbortController();
|
|
1385
|
+
limits;
|
|
1386
|
+
slots;
|
|
1387
|
+
bodies;
|
|
1388
|
+
connections = /* @__PURE__ */ new Set();
|
|
1389
|
+
closed;
|
|
1390
|
+
port;
|
|
1391
|
+
constructor(options) {
|
|
1392
|
+
this.options = options;
|
|
1393
|
+
this.limits = { ...DEFAULT_LIMITS, ...options.limits };
|
|
1394
|
+
this.slots = new Budget(this.limits.maxConnections);
|
|
1395
|
+
this.bodies = new Budget(this.limits.bodyBudget);
|
|
1396
|
+
this.listener = options.net.listen({
|
|
1397
|
+
family: "inet",
|
|
1398
|
+
host: options.host ?? "127.0.0.1",
|
|
1399
|
+
port: options.port ?? REALM_PROXY_PORT
|
|
1400
|
+
});
|
|
1401
|
+
this.port = this.listener.local?.family === "inet" ? this.listener.local.port : 0;
|
|
1402
|
+
this.closed = this.acceptLoop();
|
|
1403
|
+
}
|
|
1404
|
+
get stopped() {
|
|
1405
|
+
return this.stop.signal.aborted;
|
|
1406
|
+
}
|
|
1407
|
+
close() {
|
|
1408
|
+
if (this.stop.signal.aborted) return;
|
|
1409
|
+
this.stop.abort();
|
|
1410
|
+
this.listener.close();
|
|
1411
|
+
for (const conn of this.connections) conn.close();
|
|
1412
|
+
}
|
|
1413
|
+
async acceptLoop() {
|
|
1414
|
+
const served = /* @__PURE__ */ new Set();
|
|
1415
|
+
try {
|
|
1416
|
+
for (; ; ) {
|
|
1417
|
+
await this.slots.acquire(1, this.stop.signal);
|
|
1418
|
+
let conn;
|
|
1419
|
+
try {
|
|
1420
|
+
conn = await this.listener.accept(this.stop.signal);
|
|
1421
|
+
} catch {
|
|
1422
|
+
this.slots.release(1);
|
|
1423
|
+
break;
|
|
1424
|
+
}
|
|
1425
|
+
const done3 = this.serve(conn).finally(() => {
|
|
1426
|
+
this.slots.release(1);
|
|
1427
|
+
served.delete(done3);
|
|
1428
|
+
});
|
|
1429
|
+
served.add(done3);
|
|
1430
|
+
}
|
|
1431
|
+
} catch {
|
|
1432
|
+
}
|
|
1433
|
+
await Promise.allSettled([...served]);
|
|
1434
|
+
}
|
|
1435
|
+
async serve(conn) {
|
|
1436
|
+
this.connections.add(conn);
|
|
1437
|
+
const incoming = new Incoming(timedSource(conn, this.limits.idleMs, this.stop.signal));
|
|
1438
|
+
try {
|
|
1439
|
+
await this.requests({ sink: conn, incoming, conn, socket: conn });
|
|
1440
|
+
} finally {
|
|
1441
|
+
this.connections.delete(conn);
|
|
1442
|
+
conn.close();
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
async requests(ctx) {
|
|
1446
|
+
try {
|
|
1447
|
+
for (; ; ) {
|
|
1448
|
+
const head = await ctx.incoming.head(this.limits.maxHead);
|
|
1449
|
+
if (!head || !await this.exchange(ctx, head)) break;
|
|
1450
|
+
}
|
|
1451
|
+
} catch (e) {
|
|
1452
|
+
if (e instanceof HttpError) await this.refuse(ctx.sink, e);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
async refuse(sink, error) {
|
|
1456
|
+
try {
|
|
1457
|
+
await this.relay(sink, "GET", 1, plainResponse(error.status, error.message), false);
|
|
1458
|
+
} catch {
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
async exchange(ctx, head) {
|
|
1462
|
+
const { sink: conn, incoming } = ctx;
|
|
1463
|
+
const req = parseRequestHead(head);
|
|
1464
|
+
if (req.method === "CONNECT") {
|
|
1465
|
+
if (!ctx.conn) throw new HttpError(400, "CONNECT inside a tunnel");
|
|
1466
|
+
await this.connect(ctx.conn, incoming, req);
|
|
1467
|
+
return false;
|
|
1468
|
+
}
|
|
1469
|
+
const url = ctx.origin ? tunnelRequestUrl(req, ctx.origin) : requestUrl(req);
|
|
1470
|
+
const keep = keepsAlive(req);
|
|
1471
|
+
const framing = requestFraming(req.headers);
|
|
1472
|
+
const reserve = framing.kind === "length" ? framing.length : framing.kind === "chunked" ? this.cap() : 0;
|
|
1473
|
+
if (reserve > this.cap()) throw new HttpError(413, `request body over ${this.cap()} bytes`);
|
|
1474
|
+
await this.expectContinue(conn, req, framing.kind !== "none");
|
|
1475
|
+
await this.bodies.acquire(reserve, this.stop.signal);
|
|
1476
|
+
let response;
|
|
1477
|
+
const abort = new AbortController();
|
|
1478
|
+
const stopExchange = () => abort.abort();
|
|
1479
|
+
this.stop.signal.addEventListener("abort", stopExchange, { once: true });
|
|
1480
|
+
try {
|
|
1481
|
+
try {
|
|
1482
|
+
const body = await readBody(incoming, framing, this.cap());
|
|
1483
|
+
const waiting = new AbortController();
|
|
1484
|
+
void watchHangup(ctx.socket, abort, waiting.signal);
|
|
1485
|
+
try {
|
|
1486
|
+
response = await this.upstream(req, url, body, abort.signal);
|
|
1487
|
+
} finally {
|
|
1488
|
+
waiting.abort();
|
|
1489
|
+
}
|
|
1490
|
+
} finally {
|
|
1491
|
+
this.bodies.release(reserve);
|
|
1492
|
+
}
|
|
1493
|
+
return await this.relay(conn, req.method, req.minor, response, keep);
|
|
1494
|
+
} finally {
|
|
1495
|
+
this.stop.signal.removeEventListener("abort", stopExchange);
|
|
1496
|
+
abort.abort();
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
cap() {
|
|
1500
|
+
return Math.min(this.options.transport.traits.maxRequestBody, this.limits.bodyBudget);
|
|
1501
|
+
}
|
|
1502
|
+
async expectContinue(conn, req, hasBody) {
|
|
1503
|
+
const expect = fieldTokens(req.headers, "expect");
|
|
1504
|
+
if (expect.length === 0) return;
|
|
1505
|
+
if (expect.length !== 1 || expect[0] !== "100-continue") {
|
|
1506
|
+
throw new HttpError(417, `unsupported expectation: ${expect.join(", ")}`);
|
|
1507
|
+
}
|
|
1508
|
+
if (hasBody && req.minor >= 1) {
|
|
1509
|
+
await conn.write(latin1Bytes("HTTP/1.1 100 Continue\r\n\r\n"), this.stop.signal);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
async upstream(req, url, body, signal) {
|
|
1513
|
+
try {
|
|
1514
|
+
const fetching = this.options.transport.fetch({
|
|
1515
|
+
url,
|
|
1516
|
+
method: req.method.toUpperCase(),
|
|
1517
|
+
headers: forwardRequestHeaders(req.headers),
|
|
1518
|
+
body: req.method === "GET" || req.method === "HEAD" ? void 0 : body,
|
|
1519
|
+
signal
|
|
1520
|
+
});
|
|
1521
|
+
return await untilAborted(fetching, signal);
|
|
1522
|
+
} catch (e) {
|
|
1523
|
+
if (signal.aborted) throw e;
|
|
1524
|
+
const message2 = e instanceof Error ? e.message : String(e);
|
|
1525
|
+
const status = e?.status;
|
|
1526
|
+
const own = typeof status === "number" && status >= 400 && status <= 599 ? status : 502;
|
|
1527
|
+
return plainResponse(own, message2 || "upstream request failed");
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
async relay(conn, method, minor, response, keepAlive) {
|
|
1531
|
+
const status = response.status;
|
|
1532
|
+
const bodiless = method === "HEAD" || status === 204 || status === 304 || status < 200;
|
|
1533
|
+
const headers = forwardResponseHeaders(response.headers, {
|
|
1534
|
+
encodedBodies: this.options.transport.traits.encodedBodies,
|
|
1535
|
+
bodiless
|
|
1536
|
+
});
|
|
1537
|
+
const chunked = !bodiless && minor >= 1;
|
|
1538
|
+
const keep = keepAlive && (bodiless || chunked);
|
|
1539
|
+
headers.push(["Connection", keep ? "keep-alive" : "close"]);
|
|
1540
|
+
if (chunked) headers.push(["Transfer-Encoding", "chunked"]);
|
|
1541
|
+
const signal = this.stop.signal;
|
|
1542
|
+
await conn.write(
|
|
1543
|
+
responseHead(status, response.statusText || REASON[status] || "", headers),
|
|
1544
|
+
signal
|
|
1545
|
+
);
|
|
1546
|
+
if (bodiless) {
|
|
1547
|
+
await response.cancel();
|
|
1548
|
+
return keep;
|
|
1549
|
+
}
|
|
1550
|
+
try {
|
|
1551
|
+
for await (const piece of response.body) {
|
|
1552
|
+
if (piece.length > 0) await conn.write(chunked ? chunk(piece) : piece, signal);
|
|
1553
|
+
}
|
|
1554
|
+
} catch {
|
|
1555
|
+
await response.cancel().catch(() => void 0);
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1558
|
+
if (chunked) await conn.write(LAST_CHUNK, signal);
|
|
1559
|
+
return keep;
|
|
1560
|
+
}
|
|
1561
|
+
async connect(conn, incoming, req) {
|
|
1562
|
+
const target = tunnelTarget(req);
|
|
1563
|
+
const tunnel = this.options.tunnel;
|
|
1564
|
+
if (!tunnel) {
|
|
1565
|
+
throw new HttpError(
|
|
1566
|
+
501,
|
|
1567
|
+
`CONNECT ${target.host}:${target.port}: no tunnels through this proxy`
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
await this.options.tunnelReady?.().catch((e) => {
|
|
1571
|
+
throw new HttpError(
|
|
1572
|
+
501,
|
|
1573
|
+
`CONNECT ${target.host}:${target.port}: ${e instanceof Error ? e.message : String(e)}`
|
|
1574
|
+
);
|
|
1575
|
+
});
|
|
1576
|
+
await conn.write(latin1Bytes("HTTP/1.1 200 Connection Established\r\n\r\n"), this.stop.signal);
|
|
1577
|
+
try {
|
|
1578
|
+
await tunnel(
|
|
1579
|
+
conn,
|
|
1580
|
+
incoming,
|
|
1581
|
+
target,
|
|
1582
|
+
this.stop.signal,
|
|
1583
|
+
(source, sink, origin) => this.requests({ sink, incoming: new Incoming(source), origin, socket: conn })
|
|
1584
|
+
);
|
|
1585
|
+
} catch {
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
|
|
1590
|
+
// src/kernel/net/x509.ts
|
|
1591
|
+
function concat2(parts) {
|
|
1592
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
1593
|
+
let at = 0;
|
|
1594
|
+
for (const p of parts) {
|
|
1595
|
+
out.set(p, at);
|
|
1596
|
+
at += p.length;
|
|
1597
|
+
}
|
|
1598
|
+
return out;
|
|
1599
|
+
}
|
|
1600
|
+
function length(n) {
|
|
1601
|
+
if (n < 128) return Uint8Array.of(n);
|
|
1602
|
+
const bytes = [];
|
|
1603
|
+
for (let v = n; v > 0; v = Math.floor(v / 256)) bytes.unshift(v & 255);
|
|
1604
|
+
return Uint8Array.of(128 | bytes.length, ...bytes);
|
|
1605
|
+
}
|
|
1606
|
+
function tlv(tag, ...contents2) {
|
|
1607
|
+
const body = concat2(contents2);
|
|
1608
|
+
return concat2([Uint8Array.of(tag), length(body.length), body]);
|
|
1609
|
+
}
|
|
1610
|
+
var seq = (...parts) => tlv(48, ...parts);
|
|
1611
|
+
var set = (...parts) => tlv(49, ...parts);
|
|
1612
|
+
var octets = (bytes) => tlv(4, bytes);
|
|
1613
|
+
var bits = (bytes, unused = 0) => tlv(3, Uint8Array.of(unused), bytes);
|
|
1614
|
+
var TRUE = tlv(1, Uint8Array.of(255));
|
|
1615
|
+
function integer(magnitude) {
|
|
1616
|
+
let start = 0;
|
|
1617
|
+
while (start < magnitude.length - 1 && magnitude[start] === 0) start++;
|
|
1618
|
+
const trimmed = magnitude.subarray(start);
|
|
1619
|
+
return tlv(2, trimmed[0] & 128 ? concat2([Uint8Array.of(0), trimmed]) : trimmed);
|
|
1620
|
+
}
|
|
1621
|
+
function oid(dotted) {
|
|
1622
|
+
const [a, b, ...rest] = dotted.split(".").map(Number);
|
|
1623
|
+
const out = [40 * a + b];
|
|
1624
|
+
for (const arc of rest) {
|
|
1625
|
+
const groups = [];
|
|
1626
|
+
for (let v = arc; ; v = Math.floor(v / 128)) {
|
|
1627
|
+
groups.unshift(v & 127);
|
|
1628
|
+
if (v < 128) break;
|
|
1629
|
+
}
|
|
1630
|
+
out.push(...groups.map((g, i) => i < groups.length - 1 ? g | 128 : g));
|
|
1631
|
+
}
|
|
1632
|
+
return tlv(6, Uint8Array.from(out));
|
|
1633
|
+
}
|
|
1634
|
+
var utf8 = (text) => tlv(12, new TextEncoder().encode(text));
|
|
1635
|
+
var ascii = (text) => Uint8Array.from(text, (c) => c.charCodeAt(0) & 127);
|
|
1636
|
+
function time(date) {
|
|
1637
|
+
const iso = date.toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
1638
|
+
const year = date.getUTCFullYear();
|
|
1639
|
+
return year < 2050 ? tlv(23, ascii(`${iso.slice(2)}Z`)) : tlv(24, ascii(`${iso}Z`));
|
|
1640
|
+
}
|
|
1641
|
+
var ECDSA_SHA256 = seq(oid("1.2.840.10045.4.3.2"));
|
|
1642
|
+
function name(dn) {
|
|
1643
|
+
const rdns = [];
|
|
1644
|
+
if (dn.organization) rdns.push(set(seq(oid("2.5.4.10"), utf8(dn.organization))));
|
|
1645
|
+
rdns.push(set(seq(oid("2.5.4.3"), utf8(dn.commonName))));
|
|
1646
|
+
return seq(...rdns);
|
|
1647
|
+
}
|
|
1648
|
+
function extension(id, critical, value) {
|
|
1649
|
+
return critical ? seq(oid(id), TRUE, octets(value)) : seq(oid(id), octets(value));
|
|
1650
|
+
}
|
|
1651
|
+
function children(der) {
|
|
1652
|
+
const out = [];
|
|
1653
|
+
const [, bodyStart, bodyEnd] = header(der, 0);
|
|
1654
|
+
for (let at = bodyStart; at < bodyEnd; ) {
|
|
1655
|
+
const [, , end] = header(der, at);
|
|
1656
|
+
out.push(der.subarray(at, end));
|
|
1657
|
+
at = end;
|
|
1658
|
+
}
|
|
1659
|
+
return out;
|
|
1660
|
+
}
|
|
1661
|
+
function header(der, at) {
|
|
1662
|
+
const tag = der[at];
|
|
1663
|
+
let len = der[at + 1];
|
|
1664
|
+
let start = at + 2;
|
|
1665
|
+
if (len & 128) {
|
|
1666
|
+
const count = len & 127;
|
|
1667
|
+
len = 0;
|
|
1668
|
+
for (let i = 0; i < count; i++) len = len * 256 + der[start + i];
|
|
1669
|
+
start += count;
|
|
1670
|
+
}
|
|
1671
|
+
if (start + len > der.length) throw new Error("truncated DER");
|
|
1672
|
+
return [tag, start, start + len];
|
|
1673
|
+
}
|
|
1674
|
+
function contents(element) {
|
|
1675
|
+
const [, start, end] = header(element, 0);
|
|
1676
|
+
return element.subarray(start, end);
|
|
1677
|
+
}
|
|
1678
|
+
function publicKeyBits(spki) {
|
|
1679
|
+
const key = children(spki)[1];
|
|
1680
|
+
return contents(key).subarray(1);
|
|
1681
|
+
}
|
|
1682
|
+
async function keyId(spki) {
|
|
1683
|
+
return new Uint8Array(
|
|
1684
|
+
await crypto.subtle.digest("SHA-1", publicKeyBits(spki))
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
function ecdsaDer(raw) {
|
|
1688
|
+
const half = raw.length / 2;
|
|
1689
|
+
return seq(integer(raw.subarray(0, half)), integer(raw.subarray(half)));
|
|
1690
|
+
}
|
|
1691
|
+
function randomSerial() {
|
|
1692
|
+
const serial = crypto.getRandomValues(new Uint8Array(16));
|
|
1693
|
+
serial[0] = serial[0] & 127 | 1;
|
|
1694
|
+
return serial;
|
|
1695
|
+
}
|
|
1696
|
+
function ipv4(host) {
|
|
1697
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
1698
|
+
if (!m) return void 0;
|
|
1699
|
+
const parts = m.slice(1).map(Number);
|
|
1700
|
+
return parts.every((p) => p <= 255) ? Uint8Array.from(parts) : void 0;
|
|
1701
|
+
}
|
|
1702
|
+
function ipv6(host) {
|
|
1703
|
+
if (!host.includes(":") || !/^[0-9A-Fa-f:.]+$/.test(host)) return void 0;
|
|
1704
|
+
const v4 = /(\d{1,3}(?:\.\d{1,3}){3})$/.exec(host);
|
|
1705
|
+
let text = host;
|
|
1706
|
+
const tail = [];
|
|
1707
|
+
if (v4) {
|
|
1708
|
+
const bytes = ipv4(v4[1]);
|
|
1709
|
+
if (!bytes) return void 0;
|
|
1710
|
+
tail.push(bytes[0] << 8 | bytes[1], bytes[2] << 8 | bytes[3]);
|
|
1711
|
+
text = host.slice(0, -v4[1].length).replace(/:$/, host.endsWith(`::${v4[1]}`) ? ":" : "");
|
|
1712
|
+
}
|
|
1713
|
+
const halves = text.split("::");
|
|
1714
|
+
if (halves.length > 2) return void 0;
|
|
1715
|
+
const groups = (part) => part === "" ? [] : part.split(":");
|
|
1716
|
+
const head = groups(halves[0]);
|
|
1717
|
+
const back = halves.length === 2 ? groups(halves[1]) : [];
|
|
1718
|
+
if (![...head, ...back].every((g) => /^[0-9A-Fa-f]{1,4}$/.test(g))) return void 0;
|
|
1719
|
+
const words = [...head, ...back].map((g) => Number.parseInt(g, 16));
|
|
1720
|
+
const missing3 = 8 - words.length - tail.length;
|
|
1721
|
+
if (halves.length === 2 ? missing3 < 1 : missing3 !== 0) return void 0;
|
|
1722
|
+
const all = [
|
|
1723
|
+
...head.map((g) => Number.parseInt(g, 16)),
|
|
1724
|
+
...new Array(halves.length === 2 ? missing3 : 0).fill(0),
|
|
1725
|
+
...back.map((g) => Number.parseInt(g, 16)),
|
|
1726
|
+
...tail
|
|
1727
|
+
];
|
|
1728
|
+
return Uint8Array.from(all.flatMap((w) => [w >> 8, w & 255]));
|
|
1729
|
+
}
|
|
1730
|
+
async function extensions(req) {
|
|
1731
|
+
const list = [extension("2.5.29.14", false, octets(await keyId(req.spki)))];
|
|
1732
|
+
if (req.kind.ca) {
|
|
1733
|
+
list.push(extension("2.5.29.19", true, seq(TRUE, integer(Uint8Array.of(0)))));
|
|
1734
|
+
list.push(extension("2.5.29.15", true, bits(Uint8Array.of(134), 1)));
|
|
1735
|
+
} else {
|
|
1736
|
+
list.push(extension("2.5.29.35", false, seq(tlv(128, await keyId(req.issuerSpki)))));
|
|
1737
|
+
list.push(extension("2.5.29.19", true, seq()));
|
|
1738
|
+
list.push(extension("2.5.29.15", true, bits(Uint8Array.of(128), 7)));
|
|
1739
|
+
list.push(extension("2.5.29.37", false, seq(oid("1.3.6.1.5.5.7.3.1"))));
|
|
1740
|
+
const ip = ipv4(req.kind.host) ?? ipv6(req.kind.host);
|
|
1741
|
+
const altName = ip ? tlv(135, ip) : tlv(130, ascii(req.kind.host));
|
|
1742
|
+
list.push(extension("2.5.29.17", false, seq(altName)));
|
|
1743
|
+
}
|
|
1744
|
+
return tlv(163, seq(...list));
|
|
1745
|
+
}
|
|
1746
|
+
async function certificate(req) {
|
|
1747
|
+
const tbs = seq(
|
|
1748
|
+
tlv(160, integer(Uint8Array.of(2))),
|
|
1749
|
+
integer(req.serial),
|
|
1750
|
+
ECDSA_SHA256,
|
|
1751
|
+
name(req.issuer),
|
|
1752
|
+
seq(time(req.notBefore), time(req.notAfter)),
|
|
1753
|
+
name(req.subject),
|
|
1754
|
+
req.spki,
|
|
1755
|
+
await extensions(req)
|
|
1756
|
+
);
|
|
1757
|
+
const signature = ecdsaDer(await req.sign(tbs));
|
|
1758
|
+
return seq(tbs, ECDSA_SHA256, bits(signature));
|
|
1759
|
+
}
|
|
1760
|
+
function pem(der) {
|
|
1761
|
+
let b64 = "";
|
|
1762
|
+
for (let i = 0; i < der.length; i += 32768) {
|
|
1763
|
+
b64 += String.fromCharCode(...der.subarray(i, i + 32768));
|
|
1764
|
+
}
|
|
1765
|
+
const lines = btoa(b64).match(/.{1,64}/g) ?? [];
|
|
1766
|
+
return `-----BEGIN CERTIFICATE-----
|
|
1767
|
+
${lines.join("\n")}
|
|
1768
|
+
-----END CERTIFICATE-----
|
|
1769
|
+
`;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// src/kernel/net/realm-ca.ts
|
|
1773
|
+
function sameCert(a, b) {
|
|
1774
|
+
if (!a || !b) return a === b;
|
|
1775
|
+
return a.cert.length === b.cert.length && a.cert.every((byte, i) => byte === b.cert[i]);
|
|
1776
|
+
}
|
|
1777
|
+
function winner(current, expected, record) {
|
|
1778
|
+
return sameCert(current, expected) ? record : current;
|
|
1779
|
+
}
|
|
1780
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
1781
|
+
var CA_LIFETIME = 3650 * DAY;
|
|
1782
|
+
var CA_RENEW_BEFORE = 30 * DAY;
|
|
1783
|
+
var LEAF_LIFETIME = 7 * DAY;
|
|
1784
|
+
var ECDSA = { name: "ECDSA", namedCurve: "P-256" };
|
|
1785
|
+
var SIGN = { name: "ECDSA", hash: "SHA-256" };
|
|
1786
|
+
function request(req) {
|
|
1787
|
+
return new Promise((resolve, reject) => {
|
|
1788
|
+
req.onsuccess = () => resolve(req.result);
|
|
1789
|
+
req.onerror = () => reject(req.error);
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
function indexedDbCaStore(dbName = "slicc-realm-ca") {
|
|
1793
|
+
let db;
|
|
1794
|
+
const open = () => {
|
|
1795
|
+
db ??= new Promise((resolve, reject) => {
|
|
1796
|
+
const req = indexedDB.open(dbName, 1);
|
|
1797
|
+
req.onupgradeneeded = () => req.result.createObjectStore("ca");
|
|
1798
|
+
req.onsuccess = () => resolve(req.result);
|
|
1799
|
+
req.onerror = () => reject(req.error);
|
|
1800
|
+
});
|
|
1801
|
+
db.catch(() => {
|
|
1802
|
+
db = void 0;
|
|
1803
|
+
});
|
|
1804
|
+
return db;
|
|
1805
|
+
};
|
|
1806
|
+
return {
|
|
1807
|
+
async get(owner) {
|
|
1808
|
+
const store = (await open()).transaction("ca").objectStore("ca");
|
|
1809
|
+
return await request(store.get(owner));
|
|
1810
|
+
},
|
|
1811
|
+
async swap(owner, expected, record) {
|
|
1812
|
+
const tx = (await open()).transaction("ca", "readwrite");
|
|
1813
|
+
const store = tx.objectStore("ca");
|
|
1814
|
+
let stored = record;
|
|
1815
|
+
const read = store.get(owner);
|
|
1816
|
+
read.onsuccess = () => {
|
|
1817
|
+
const current = read.result;
|
|
1818
|
+
const kept = winner(current, expected, record);
|
|
1819
|
+
if (kept === record) store.put(record, owner);
|
|
1820
|
+
else stored = kept;
|
|
1821
|
+
};
|
|
1822
|
+
await new Promise((resolve, reject) => {
|
|
1823
|
+
tx.oncomplete = () => resolve();
|
|
1824
|
+
tx.onerror = () => reject(tx.error);
|
|
1825
|
+
});
|
|
1826
|
+
return stored;
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
}
|
|
1830
|
+
var LeafNameError = class extends Error {
|
|
1831
|
+
};
|
|
1832
|
+
function validLeafName(host) {
|
|
1833
|
+
if (ipv6(host)) return true;
|
|
1834
|
+
if (host.length === 0 || host.length > 253) return false;
|
|
1835
|
+
return host.split(".").every((label) => /^(?!-)[a-z0-9-]{1,63}(?<!-)$/i.test(label));
|
|
1836
|
+
}
|
|
1837
|
+
var RealmCa = class _RealmCa {
|
|
1838
|
+
record;
|
|
1839
|
+
constructor(record) {
|
|
1840
|
+
this.record = record;
|
|
1841
|
+
}
|
|
1842
|
+
static async open(owner, store, now = Date.now()) {
|
|
1843
|
+
const stored = await store.get(owner).catch(() => void 0);
|
|
1844
|
+
if (stored && stored.notAfter - now > CA_RENEW_BEFORE) return new _RealmCa(stored);
|
|
1845
|
+
const record = await _RealmCa.create(owner, now);
|
|
1846
|
+
return new _RealmCa(await store.swap(owner, stored, record));
|
|
1847
|
+
}
|
|
1848
|
+
static async create(owner, now) {
|
|
1849
|
+
const pair = await crypto.subtle.generateKey(ECDSA, false, ["sign", "verify"]);
|
|
1850
|
+
const spki = new Uint8Array(await crypto.subtle.exportKey("spki", pair.publicKey));
|
|
1851
|
+
const id = Array.from(
|
|
1852
|
+
crypto.getRandomValues(new Uint8Array(4)),
|
|
1853
|
+
(b) => b.toString(16).padStart(2, "0")
|
|
1854
|
+
).join("");
|
|
1855
|
+
const name2 = { organization: "SLICC", commonName: `SLICC realm CA ${owner} ${id}` };
|
|
1856
|
+
const notAfter = now + CA_LIFETIME;
|
|
1857
|
+
const cert = await certificate({
|
|
1858
|
+
serial: randomSerial(),
|
|
1859
|
+
issuer: name2,
|
|
1860
|
+
subject: name2,
|
|
1861
|
+
notBefore: new Date(now - DAY),
|
|
1862
|
+
notAfter: new Date(notAfter),
|
|
1863
|
+
spki,
|
|
1864
|
+
issuerSpki: spki,
|
|
1865
|
+
kind: { ca: true },
|
|
1866
|
+
sign: signer(pair.privateKey)
|
|
1867
|
+
});
|
|
1868
|
+
return { key: pair.privateKey, cert, spki, notAfter, name: name2 };
|
|
1869
|
+
}
|
|
1870
|
+
get cert() {
|
|
1871
|
+
return this.record.cert;
|
|
1872
|
+
}
|
|
1873
|
+
get pem() {
|
|
1874
|
+
return pem(this.record.cert);
|
|
1875
|
+
}
|
|
1876
|
+
async issue(host, spki, now = Date.now()) {
|
|
1877
|
+
const name2 = host.toLowerCase();
|
|
1878
|
+
if (!validLeafName(name2)) throw new LeafNameError(`no certificate for ${host}`);
|
|
1879
|
+
return certificate({
|
|
1880
|
+
serial: randomSerial(),
|
|
1881
|
+
issuer: this.record.name,
|
|
1882
|
+
subject: { commonName: name2.length <= 64 ? name2 : "SLICC realm leaf" },
|
|
1883
|
+
notBefore: new Date(now - 60 * 60 * 1e3),
|
|
1884
|
+
notAfter: new Date(now + LEAF_LIFETIME),
|
|
1885
|
+
spki,
|
|
1886
|
+
issuerSpki: this.record.spki,
|
|
1887
|
+
kind: { ca: false, host: name2 },
|
|
1888
|
+
sign: signer(this.record.key)
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
};
|
|
1892
|
+
function signer(key) {
|
|
1893
|
+
return async (tbs) => new Uint8Array(await crypto.subtle.sign(SIGN, key, tbs));
|
|
1894
|
+
}
|
|
1895
|
+
var opened = /* @__PURE__ */ new WeakMap();
|
|
1896
|
+
function realmCa(owner, store) {
|
|
1897
|
+
const cas = opened.get(store) ?? /* @__PURE__ */ new Map();
|
|
1898
|
+
opened.set(store, cas);
|
|
1899
|
+
let ca = cas.get(owner);
|
|
1900
|
+
if (!ca) {
|
|
1901
|
+
const opening = RealmCa.open(owner, store);
|
|
1902
|
+
ca = opening;
|
|
1903
|
+
cas.set(owner, opening);
|
|
1904
|
+
opening.catch(() => {
|
|
1905
|
+
if (cas.get(owner) === opening) cas.delete(owner);
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
return ca;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// src/kernel/net/tls-engine.ts
|
|
1912
|
+
var SCRATCH = 64 * 1024;
|
|
1913
|
+
var EOF = -1;
|
|
1914
|
+
var TlsError = class extends Error {
|
|
1915
|
+
code;
|
|
1916
|
+
constructor(code, message2) {
|
|
1917
|
+
super(message2);
|
|
1918
|
+
this.code = code;
|
|
1919
|
+
}
|
|
1920
|
+
};
|
|
1921
|
+
var TlsEngine = class {
|
|
1922
|
+
m;
|
|
1923
|
+
scratch;
|
|
1924
|
+
constructor(m) {
|
|
1925
|
+
this.m = m;
|
|
1926
|
+
this.scratch = m._malloc(SCRATCH);
|
|
1927
|
+
if (!this.scratch) throw new Error("tls engine: out of memory");
|
|
1928
|
+
}
|
|
1929
|
+
put(bytes) {
|
|
1930
|
+
const ptr = this.m._malloc(bytes.length + 1);
|
|
1931
|
+
if (!ptr) throw new Error("tls engine: out of memory");
|
|
1932
|
+
this.m.HEAPU8.set(bytes, ptr);
|
|
1933
|
+
this.m.HEAPU8[ptr + bytes.length] = 0;
|
|
1934
|
+
return ptr;
|
|
1935
|
+
}
|
|
1936
|
+
taken(n) {
|
|
1937
|
+
return this.m.HEAPU8.slice(this.scratch, this.scratch + n);
|
|
1938
|
+
}
|
|
1939
|
+
error(code, what) {
|
|
1940
|
+
this.m._tls_strerror(code, this.scratch, 256);
|
|
1941
|
+
const end = this.m.HEAPU8.indexOf(0, this.scratch);
|
|
1942
|
+
const text = new TextDecoder().decode(this.m.HEAPU8.subarray(this.scratch, end));
|
|
1943
|
+
return new TlsError(code, `${what}: ${text || code}`);
|
|
1944
|
+
}
|
|
1945
|
+
leaf() {
|
|
1946
|
+
const ptr = this.m._tls_leaf_new();
|
|
1947
|
+
if (!ptr) throw this.error(this.m._tls_last_error(), "leaf key");
|
|
1948
|
+
return new TlsLeaf(this, this.m, ptr);
|
|
1949
|
+
}
|
|
1950
|
+
session(leaf, host) {
|
|
1951
|
+
const name2 = this.put(new TextEncoder().encode(host));
|
|
1952
|
+
try {
|
|
1953
|
+
const ptr = this.m._tls_session_new(leaf.ptr, name2);
|
|
1954
|
+
if (!ptr) throw this.error(this.m._tls_last_error(), "tls session");
|
|
1955
|
+
return new TlsSession(this, this.m, ptr);
|
|
1956
|
+
} finally {
|
|
1957
|
+
this.m._free(name2);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
withBytes(bytes, fn) {
|
|
1961
|
+
const ptr = this.put(bytes);
|
|
1962
|
+
try {
|
|
1963
|
+
return fn(ptr);
|
|
1964
|
+
} finally {
|
|
1965
|
+
this.m._free(ptr);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
intoScratch(fn) {
|
|
1969
|
+
const n = fn(this.scratch, SCRATCH);
|
|
1970
|
+
return n > 0 ? this.taken(n) : n;
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
var TlsLeaf = class {
|
|
1974
|
+
engine;
|
|
1975
|
+
m;
|
|
1976
|
+
ptr;
|
|
1977
|
+
constructor(engine, m, ptr) {
|
|
1978
|
+
this.engine = engine;
|
|
1979
|
+
this.m = m;
|
|
1980
|
+
this.ptr = ptr;
|
|
1981
|
+
}
|
|
1982
|
+
spki() {
|
|
1983
|
+
const out = this.engine.intoScratch((ptr, max) => this.m._tls_leaf_spki(this.ptr, ptr, max));
|
|
1984
|
+
if (typeof out === "number") throw this.engine.error(out, "leaf public key");
|
|
1985
|
+
return out;
|
|
1986
|
+
}
|
|
1987
|
+
addCert(der) {
|
|
1988
|
+
const ret = this.engine.withBytes(
|
|
1989
|
+
der,
|
|
1990
|
+
(ptr) => this.m._tls_leaf_add_cert(this.ptr, ptr, der.length)
|
|
1991
|
+
);
|
|
1992
|
+
if (ret !== 0) throw this.engine.error(ret, "leaf certificate");
|
|
1993
|
+
}
|
|
1994
|
+
release() {
|
|
1995
|
+
this.m._tls_leaf_free(this.ptr);
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
var TlsSession = class {
|
|
1999
|
+
engine;
|
|
2000
|
+
m;
|
|
2001
|
+
ptr;
|
|
2002
|
+
constructor(engine, m, ptr) {
|
|
2003
|
+
this.engine = engine;
|
|
2004
|
+
this.m = m;
|
|
2005
|
+
this.ptr = ptr;
|
|
2006
|
+
}
|
|
2007
|
+
feed(bytes) {
|
|
2008
|
+
return this.engine.withBytes(bytes, (ptr) => this.m._tls_feed(this.ptr, ptr, bytes.length));
|
|
2009
|
+
}
|
|
2010
|
+
feedEnd() {
|
|
2011
|
+
this.m._tls_feed_eof(this.ptr);
|
|
2012
|
+
}
|
|
2013
|
+
read() {
|
|
2014
|
+
const out = this.engine.intoScratch((ptr, max) => this.m._tls_read(this.ptr, ptr, max));
|
|
2015
|
+
if (out instanceof Uint8Array) return out;
|
|
2016
|
+
if (out === 0) return "more";
|
|
2017
|
+
if (out === EOF) return "eof";
|
|
2018
|
+
throw this.engine.error(out, "tls");
|
|
2019
|
+
}
|
|
2020
|
+
write(bytes) {
|
|
2021
|
+
const n = this.engine.withBytes(bytes, (ptr) => this.m._tls_write(this.ptr, ptr, bytes.length));
|
|
2022
|
+
if (n < 0) throw this.engine.error(n, "tls");
|
|
2023
|
+
return n;
|
|
2024
|
+
}
|
|
2025
|
+
take() {
|
|
2026
|
+
if (this.m._tls_out_size(this.ptr) === 0) return new Uint8Array(0);
|
|
2027
|
+
const out = this.engine.intoScratch((ptr, max) => this.m._tls_out_take(this.ptr, ptr, max));
|
|
2028
|
+
return out instanceof Uint8Array ? out : new Uint8Array(0);
|
|
2029
|
+
}
|
|
2030
|
+
close() {
|
|
2031
|
+
this.m._tls_close(this.ptr);
|
|
2032
|
+
}
|
|
2033
|
+
free() {
|
|
2034
|
+
this.m._tls_session_free(this.ptr);
|
|
2035
|
+
}
|
|
2036
|
+
};
|
|
2037
|
+
var loading = /* @__PURE__ */ new WeakMap();
|
|
2038
|
+
function loadTlsEngine(load) {
|
|
2039
|
+
let engine = loading.get(load);
|
|
2040
|
+
if (!engine) {
|
|
2041
|
+
const started = load().then((m) => new TlsEngine(m));
|
|
2042
|
+
engine = started;
|
|
2043
|
+
loading.set(load, started);
|
|
2044
|
+
started.catch(() => {
|
|
2045
|
+
if (loading.get(load) === started) loading.delete(load);
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
return engine;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
// src/kernel/net/tls-tunnel.ts
|
|
2052
|
+
var LEAF_REUSE = LEAF_LIFETIME - 24 * 60 * 60 * 1e3;
|
|
2053
|
+
var LEAF_CACHE = 256;
|
|
2054
|
+
var CIPHER_READ = 64 * 1024;
|
|
2055
|
+
var TlsStream = class {
|
|
2056
|
+
session;
|
|
2057
|
+
cipherIn;
|
|
2058
|
+
cipherOut;
|
|
2059
|
+
pending = new Uint8Array(0);
|
|
2060
|
+
ended = false;
|
|
2061
|
+
constructor(session, cipherIn, cipherOut) {
|
|
2062
|
+
this.session = session;
|
|
2063
|
+
this.cipherIn = cipherIn;
|
|
2064
|
+
this.cipherOut = cipherOut;
|
|
2065
|
+
}
|
|
2066
|
+
async flush(signal) {
|
|
2067
|
+
for (let out = this.session.take(); out.length > 0; out = this.session.take()) {
|
|
2068
|
+
await this.cipherOut.write(out, signal);
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
feedPending() {
|
|
2072
|
+
if (this.pending.length === 0) return;
|
|
2073
|
+
const n = this.session.feed(this.pending);
|
|
2074
|
+
this.pending = this.pending.subarray(n);
|
|
2075
|
+
}
|
|
2076
|
+
async read(_max, signal) {
|
|
2077
|
+
for (; ; ) {
|
|
2078
|
+
this.feedPending();
|
|
2079
|
+
const got = this.session.read();
|
|
2080
|
+
await this.flush(signal);
|
|
2081
|
+
if (got instanceof Uint8Array) return got;
|
|
2082
|
+
if (got === "eof") return new Uint8Array(0);
|
|
2083
|
+
if (this.pending.length > 0) continue;
|
|
2084
|
+
if (this.ended) {
|
|
2085
|
+
this.session.feedEnd();
|
|
2086
|
+
const last = this.session.read();
|
|
2087
|
+
await this.flush(signal);
|
|
2088
|
+
return last instanceof Uint8Array ? last : new Uint8Array(0);
|
|
2089
|
+
}
|
|
2090
|
+
const cipher = await this.cipherIn.read(CIPHER_READ, signal);
|
|
2091
|
+
if (cipher.length === 0) this.ended = true;
|
|
2092
|
+
else this.pending = cipher;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
async write(bytes, signal) {
|
|
2096
|
+
for (let at = 0; at < bytes.length; ) {
|
|
2097
|
+
const n = this.session.write(bytes.subarray(at, at + 16 * 1024));
|
|
2098
|
+
await this.flush(signal);
|
|
2099
|
+
at += n;
|
|
2100
|
+
}
|
|
2101
|
+
return bytes.length;
|
|
2102
|
+
}
|
|
2103
|
+
async close(signal) {
|
|
2104
|
+
this.session.close();
|
|
2105
|
+
await this.flush(signal);
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
var TlsTerminator = class {
|
|
2109
|
+
options;
|
|
2110
|
+
leaves = /* @__PURE__ */ new Map();
|
|
2111
|
+
now;
|
|
2112
|
+
constructor(options) {
|
|
2113
|
+
this.options = options;
|
|
2114
|
+
this.now = options.now ?? Date.now;
|
|
2115
|
+
}
|
|
2116
|
+
leafFor(engine, host) {
|
|
2117
|
+
const now = this.now();
|
|
2118
|
+
const cached = this.leaves.get(host);
|
|
2119
|
+
if (cached && cached.expires > now) {
|
|
2120
|
+
this.leaves.delete(host);
|
|
2121
|
+
this.leaves.set(host, cached);
|
|
2122
|
+
return cached.leaf;
|
|
2123
|
+
}
|
|
2124
|
+
if (cached) void this.drop(host, cached);
|
|
2125
|
+
const leaf = this.issue(engine, host, now);
|
|
2126
|
+
const entry = { leaf, expires: now + LEAF_REUSE };
|
|
2127
|
+
this.leaves.set(host, entry);
|
|
2128
|
+
leaf.catch(() => {
|
|
2129
|
+
if (this.leaves.get(host) === entry) this.leaves.delete(host);
|
|
2130
|
+
});
|
|
2131
|
+
for (const [oldest, old] of this.leaves) {
|
|
2132
|
+
if (this.leaves.size <= LEAF_CACHE) break;
|
|
2133
|
+
void this.drop(oldest, old);
|
|
2134
|
+
}
|
|
2135
|
+
return leaf;
|
|
2136
|
+
}
|
|
2137
|
+
async drop(host, entry) {
|
|
2138
|
+
if (this.leaves.get(host) === entry) this.leaves.delete(host);
|
|
2139
|
+
(await entry.leaf.catch(() => void 0))?.release();
|
|
2140
|
+
}
|
|
2141
|
+
async issue(engine, host, now) {
|
|
2142
|
+
const ca = await this.options.ca();
|
|
2143
|
+
const leaf = engine.leaf();
|
|
2144
|
+
try {
|
|
2145
|
+
leaf.addCert(await ca.issue(host, leaf.spki(), now));
|
|
2146
|
+
leaf.addCert(ca.cert);
|
|
2147
|
+
return leaf;
|
|
2148
|
+
} catch (e) {
|
|
2149
|
+
leaf.release();
|
|
2150
|
+
throw e;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
async close() {
|
|
2154
|
+
await Promise.all([...this.leaves].map(([host, entry]) => this.drop(host, entry)));
|
|
2155
|
+
}
|
|
2156
|
+
handler = async (conn, incoming, target, signal, serveHttp) => {
|
|
2157
|
+
const engine = await this.options.engine();
|
|
2158
|
+
const host = target.host.replace(/^\[|\]$/g, "");
|
|
2159
|
+
const leaf = await this.leafFor(engine, host);
|
|
2160
|
+
const session = engine.session(leaf, host);
|
|
2161
|
+
try {
|
|
2162
|
+
const stream = new TlsStream(session, cipherSource(incoming), conn);
|
|
2163
|
+
const origin = `https://${authority(target)}`;
|
|
2164
|
+
await serveHttp(stream, stream, origin);
|
|
2165
|
+
await stream.close(signal).catch(() => void 0);
|
|
2166
|
+
} finally {
|
|
2167
|
+
session.free();
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
};
|
|
2171
|
+
function authority(target) {
|
|
2172
|
+
return target.port === 443 ? target.host : `${target.host}:${target.port}`;
|
|
2173
|
+
}
|
|
2174
|
+
function cipherSource(incoming) {
|
|
2175
|
+
return { read: (max, signal) => incoming.some(max, signal) };
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// src/kernel/net/network.ts
|
|
2179
|
+
var NO_TRANSPORT = "slicc-kernel: no network transport (createKernel({ network: { transport } }))";
|
|
2180
|
+
var TLS_ENGINE_PACKAGE = "@ai-ecoverse/wasm-tls-engine";
|
|
2181
|
+
var CA_PATH = "/etc/ssl/certs/slicc-kernel-ca.pem";
|
|
2182
|
+
var PROXY_URL = `http://127.0.0.1:${REALM_PROXY_PORT}`;
|
|
2183
|
+
var NO_PROXY = "localhost,.localhost,127.0.0.1,127.0.0.0/8";
|
|
2184
|
+
var CA_OWNER = "slicc-kernel";
|
|
2185
|
+
function networkEnv() {
|
|
2186
|
+
return {
|
|
2187
|
+
http_proxy: PROXY_URL,
|
|
2188
|
+
https_proxy: PROXY_URL,
|
|
2189
|
+
HTTP_PROXY: PROXY_URL,
|
|
2190
|
+
HTTPS_PROXY: PROXY_URL,
|
|
2191
|
+
no_proxy: NO_PROXY,
|
|
2192
|
+
NO_PROXY,
|
|
2193
|
+
SSL_CERT_FILE: CA_PATH,
|
|
2194
|
+
CURL_CA_BUNDLE: CA_PATH,
|
|
2195
|
+
GIT_SSL_CAINFO: CA_PATH
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
function missingTransport() {
|
|
2199
|
+
return {
|
|
2200
|
+
traits: { manualRedirects: true, encodedBodies: true, maxRequestBody: 0 },
|
|
2201
|
+
fetch: () => Promise.reject(Object.assign(new Error(NO_TRANSPORT), { status: 502 }))
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
function memoryCaStore() {
|
|
2205
|
+
const records = /* @__PURE__ */ new Map();
|
|
2206
|
+
return {
|
|
2207
|
+
get: async (owner) => records.get(owner),
|
|
2208
|
+
swap: async (owner, expected, record) => {
|
|
2209
|
+
const kept = winner(records.get(owner), expected, record);
|
|
2210
|
+
records.set(owner, kept);
|
|
2211
|
+
return kept;
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
function caStore(metadata) {
|
|
2216
|
+
return metadata === false ? memoryCaStore() : indexedDbCaStore(`${metadata}-ca`);
|
|
2217
|
+
}
|
|
2218
|
+
function base64(bytes) {
|
|
2219
|
+
let text = "";
|
|
2220
|
+
for (let i = 0; i < bytes.length; i += 32768) {
|
|
2221
|
+
text += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
2222
|
+
}
|
|
2223
|
+
return btoa(text);
|
|
2224
|
+
}
|
|
2225
|
+
function packageTlsEngine(fs, modules) {
|
|
2226
|
+
const dir = `${modules.replace(/\/+$/, "")}/${TLS_ENGINE_PACKAGE}/dist`;
|
|
2227
|
+
return async () => {
|
|
2228
|
+
let glue;
|
|
2229
|
+
let wasmBinary;
|
|
2230
|
+
try {
|
|
2231
|
+
[glue, wasmBinary] = await Promise.all([
|
|
2232
|
+
fs.readFileBuffer(`${dir}/slicc-tls-engine.mjs`),
|
|
2233
|
+
fs.readFileBuffer(`${dir}/slicc-tls-engine.wasm`)
|
|
2234
|
+
]);
|
|
2235
|
+
} catch {
|
|
2236
|
+
throw new Error(`TLS needs ${TLS_ENGINE_PACKAGE} installed in ${modules}`);
|
|
2237
|
+
}
|
|
2238
|
+
const url = `data:text/javascript;base64,${base64(glue)}`;
|
|
2239
|
+
const factory = await import(url);
|
|
2240
|
+
return factory.default({ wasmBinary, locateFile: (path) => path });
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
function kernelCa(store) {
|
|
2244
|
+
return () => realmCa(CA_OWNER, store);
|
|
2245
|
+
}
|
|
2246
|
+
function kernelTlsEngine(load) {
|
|
2247
|
+
return () => loadTlsEngine(load);
|
|
2248
|
+
}
|
|
2249
|
+
async function writeCaFile(fs, ca) {
|
|
2250
|
+
const { pem: pem2 } = await ca();
|
|
2251
|
+
const current = await fs.exists(CA_PATH) ? await fs.readFile(CA_PATH) : void 0;
|
|
2252
|
+
if (current === pem2) return;
|
|
2253
|
+
await fs.mkdir(CA_PATH.slice(0, CA_PATH.lastIndexOf("/")), { recursive: true });
|
|
2254
|
+
await fs.writeFile(CA_PATH, pem2);
|
|
2255
|
+
}
|
|
2256
|
+
function enableNetwork(net, options) {
|
|
2257
|
+
let proxy;
|
|
2258
|
+
const transport = options.transport ?? missingTransport();
|
|
2259
|
+
net.activate({ family: "inet", host: "127.0.0.1", port: REALM_PROXY_PORT }, () => {
|
|
2260
|
+
const tls = new TlsTerminator({
|
|
2261
|
+
ca: options.ca,
|
|
2262
|
+
engine: options.engine,
|
|
2263
|
+
...options.now ? { now: options.now } : {}
|
|
2264
|
+
});
|
|
2265
|
+
const started = new RealmProxy({
|
|
2266
|
+
net,
|
|
2267
|
+
transport,
|
|
2268
|
+
tunnel: tls.handler,
|
|
2269
|
+
tunnelReady: async () => {
|
|
2270
|
+
await options.engine();
|
|
2271
|
+
}
|
|
2272
|
+
});
|
|
2273
|
+
proxy = started;
|
|
2274
|
+
void started.closed.then(async () => {
|
|
2275
|
+
if (proxy === started) proxy = void 0;
|
|
2276
|
+
await tls.close();
|
|
2277
|
+
});
|
|
2278
|
+
});
|
|
2279
|
+
return () => proxy;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// src/kernel/net/remote-transport.ts
|
|
2283
|
+
var TransportError = class extends Error {
|
|
2284
|
+
status;
|
|
2285
|
+
constructor(message2, status) {
|
|
2286
|
+
super(message2);
|
|
2287
|
+
this.status = status;
|
|
2288
|
+
}
|
|
2289
|
+
};
|
|
2290
|
+
var RemoteTransport = class {
|
|
2291
|
+
traits;
|
|
2292
|
+
port;
|
|
2293
|
+
waiting = /* @__PURE__ */ new Map();
|
|
2294
|
+
nextId = 0;
|
|
2295
|
+
constructor(port, traits) {
|
|
2296
|
+
this.port = port;
|
|
2297
|
+
this.traits = traits;
|
|
2298
|
+
}
|
|
2299
|
+
receive(reply) {
|
|
2300
|
+
const waiter = this.waiting.get(reply.nid);
|
|
2301
|
+
if (!waiter) return;
|
|
2302
|
+
this.waiting.delete(reply.nid);
|
|
2303
|
+
if (reply.net === "error") waiter.reject(new TransportError(reply.message, reply.status));
|
|
2304
|
+
else waiter.resolve(reply);
|
|
2305
|
+
}
|
|
2306
|
+
ask(call) {
|
|
2307
|
+
return new Promise((resolve, reject) => {
|
|
2308
|
+
this.waiting.set(call.nid, { resolve, reject });
|
|
2309
|
+
this.port.postMessage(call);
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
cancel(nid) {
|
|
2313
|
+
const waiter = this.waiting.get(nid);
|
|
2314
|
+
this.waiting.delete(nid);
|
|
2315
|
+
waiter?.reject(new TransportError("the request was cancelled"));
|
|
2316
|
+
this.port.postMessage({ net: "cancel", nid });
|
|
2317
|
+
}
|
|
2318
|
+
async fetch(request2) {
|
|
2319
|
+
const nid = ++this.nextId;
|
|
2320
|
+
const { signal } = request2;
|
|
2321
|
+
signal.throwIfAborted();
|
|
2322
|
+
let finished = false;
|
|
2323
|
+
const abort = () => {
|
|
2324
|
+
finished = true;
|
|
2325
|
+
this.cancel(nid);
|
|
2326
|
+
};
|
|
2327
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
2328
|
+
const finish = () => {
|
|
2329
|
+
if (finished) return;
|
|
2330
|
+
finished = true;
|
|
2331
|
+
signal.removeEventListener("abort", abort);
|
|
2332
|
+
};
|
|
2333
|
+
let head;
|
|
2334
|
+
try {
|
|
2335
|
+
head = await this.ask({
|
|
2336
|
+
net: "fetch",
|
|
2337
|
+
nid,
|
|
2338
|
+
url: request2.url,
|
|
2339
|
+
method: request2.method,
|
|
2340
|
+
headers: request2.headers,
|
|
2341
|
+
...request2.body ? { body: request2.body } : {}
|
|
2342
|
+
});
|
|
2343
|
+
} catch (e) {
|
|
2344
|
+
finish();
|
|
2345
|
+
throw e;
|
|
2346
|
+
}
|
|
2347
|
+
const { status, statusText, headers } = head;
|
|
2348
|
+
const read = async () => {
|
|
2349
|
+
if (finished) return void 0;
|
|
2350
|
+
const reply = await this.ask({ net: "read", nid }).catch((e) => {
|
|
2351
|
+
finish();
|
|
2352
|
+
throw e;
|
|
2353
|
+
});
|
|
2354
|
+
if (reply.net === "chunk") return reply.bytes;
|
|
2355
|
+
finish();
|
|
2356
|
+
return void 0;
|
|
2357
|
+
};
|
|
2358
|
+
const cancel = async () => {
|
|
2359
|
+
if (finished) return;
|
|
2360
|
+
finish();
|
|
2361
|
+
this.cancel(nid);
|
|
2362
|
+
};
|
|
2363
|
+
return {
|
|
2364
|
+
status,
|
|
2365
|
+
statusText,
|
|
2366
|
+
headers,
|
|
2367
|
+
cancel,
|
|
2368
|
+
body: {
|
|
2369
|
+
[Symbol.asyncIterator]: () => ({
|
|
2370
|
+
next: async () => {
|
|
2371
|
+
const bytes = await read();
|
|
2372
|
+
return bytes ? { value: bytes, done: false } : { value: void 0, done: true };
|
|
2373
|
+
},
|
|
2374
|
+
return: async () => {
|
|
2375
|
+
await cancel();
|
|
2376
|
+
return { value: void 0, done: true };
|
|
2377
|
+
}
|
|
2378
|
+
})
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
|
|
2384
|
+
// src/kernel/signals.ts
|
|
2385
|
+
var SIG = {
|
|
2386
|
+
HUP: 1,
|
|
2387
|
+
INT: 2,
|
|
2388
|
+
QUIT: 3,
|
|
2389
|
+
ILL: 4,
|
|
2390
|
+
TRAP: 5,
|
|
2391
|
+
ABRT: 6,
|
|
2392
|
+
BUS: 7,
|
|
2393
|
+
FPE: 8,
|
|
2394
|
+
KILL: 9,
|
|
2395
|
+
USR1: 10,
|
|
2396
|
+
SEGV: 11,
|
|
2397
|
+
USR2: 12,
|
|
2398
|
+
PIPE: 13,
|
|
2399
|
+
ALRM: 14,
|
|
2400
|
+
TERM: 15,
|
|
2401
|
+
STKFLT: 16,
|
|
2402
|
+
CHLD: 17,
|
|
2403
|
+
CONT: 18,
|
|
2404
|
+
STOP: 19,
|
|
2405
|
+
TSTP: 20,
|
|
2406
|
+
TTIN: 21,
|
|
2407
|
+
TTOU: 22,
|
|
2408
|
+
URG: 23,
|
|
2409
|
+
XCPU: 24,
|
|
2410
|
+
XFSZ: 25,
|
|
2411
|
+
VTALRM: 26,
|
|
2412
|
+
PROF: 27,
|
|
2413
|
+
WINCH: 28,
|
|
2414
|
+
POLL: 29,
|
|
2415
|
+
PWR: 30,
|
|
2416
|
+
SYS: 31
|
|
2417
|
+
};
|
|
2418
|
+
var IGNORED_BY_DEFAULT = /* @__PURE__ */ new Set([SIG.CHLD, SIG.URG, SIG.WINCH]);
|
|
2419
|
+
var STOPPING = /* @__PURE__ */ new Set([SIG.STOP, SIG.TSTP, SIG.TTIN, SIG.TTOU]);
|
|
2420
|
+
function defaultAction(sig) {
|
|
2421
|
+
if (sig === SIG.CONT) return "continue";
|
|
2422
|
+
if (STOPPING.has(sig)) return "stop";
|
|
2423
|
+
return IGNORED_BY_DEFAULT.has(sig) ? "ignore" : "terminate";
|
|
2424
|
+
}
|
|
2425
|
+
function isSignal(sig) {
|
|
2426
|
+
return Number.isInteger(sig) && sig >= 1 && sig <= 31;
|
|
2427
|
+
}
|
|
2428
|
+
function sigbit(sig) {
|
|
2429
|
+
return 1 << sig;
|
|
2430
|
+
}
|
|
2431
|
+
var SIGNAL_BY_NAME = {
|
|
2432
|
+
SIGINT: SIG.INT,
|
|
2433
|
+
SIGTERM: SIG.TERM,
|
|
2434
|
+
SIGKILL: SIG.KILL,
|
|
2435
|
+
SIGSTOP: SIG.STOP,
|
|
2436
|
+
SIGCONT: SIG.CONT
|
|
2437
|
+
};
|
|
2438
|
+
|
|
2439
|
+
// src/commands.ts
|
|
2440
|
+
var NAME = /^[A-Za-z0-9._+-]+$/;
|
|
2441
|
+
var ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2442
|
+
var PACKAGE = /\$\{package\}/g;
|
|
2443
|
+
function inside(pkg, rel) {
|
|
2444
|
+
if (typeof rel !== "string" || rel === "") return void 0;
|
|
2445
|
+
const clean = rel.replace(/^\.\//, "");
|
|
2446
|
+
if (clean.startsWith("/") || clean.split("/").includes("..")) return void 0;
|
|
2447
|
+
return `${pkg}/${clean}`;
|
|
2448
|
+
}
|
|
2449
|
+
function envOf(pkg, raw) {
|
|
2450
|
+
const env = {};
|
|
2451
|
+
if (!raw || typeof raw !== "object") return env;
|
|
2452
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
2453
|
+
if (ENV_KEY.test(key) && typeof value === "string") env[key] = value.replace(PACKAGE, pkg);
|
|
2454
|
+
}
|
|
2455
|
+
return env;
|
|
2456
|
+
}
|
|
2457
|
+
function argsOf(pkg, raw) {
|
|
2458
|
+
if (!Array.isArray(raw) || raw.length === 0) return void 0;
|
|
2459
|
+
if (!raw.every((arg) => typeof arg === "string")) return void 0;
|
|
2460
|
+
return raw.map((arg) => arg.replace(PACKAGE, pkg));
|
|
2461
|
+
}
|
|
2462
|
+
function commandsOf(pkg, manifest) {
|
|
2463
|
+
const slicc = manifest.slicc;
|
|
2464
|
+
if (!slicc || typeof slicc !== "object" || (slicc.abi ?? "emscripten") !== "emscripten")
|
|
2465
|
+
return [];
|
|
2466
|
+
const entries = slicc.commands;
|
|
2467
|
+
if (!entries || typeof entries !== "object") return [];
|
|
2468
|
+
const shared = envOf(pkg, slicc.env);
|
|
2469
|
+
const out = [];
|
|
2470
|
+
for (const [name2, raw] of Object.entries(entries)) {
|
|
2471
|
+
if (!NAME.test(name2) || name2 === "." || name2 === ".." || !raw || typeof raw !== "object") {
|
|
2472
|
+
continue;
|
|
2473
|
+
}
|
|
2474
|
+
const glue = inside(pkg, raw.glue);
|
|
2475
|
+
const wasm = inside(pkg, raw.wasm);
|
|
2476
|
+
if ((raw.abi ?? "emscripten") !== "emscripten" || !glue || !wasm) continue;
|
|
2477
|
+
const argv0 = typeof raw.argv0 === "string" && raw.argv0 ? raw.argv0 : name2;
|
|
2478
|
+
const args = argsOf(pkg, raw.args);
|
|
2479
|
+
const env = { ...shared, ...envOf(pkg, raw.env) };
|
|
2480
|
+
out.push({
|
|
2481
|
+
name: name2,
|
|
2482
|
+
glue,
|
|
2483
|
+
wasm,
|
|
2484
|
+
argv0,
|
|
2485
|
+
...args ? { args } : {},
|
|
2486
|
+
...Object.keys(env).length > 0 ? { env } : {}
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
return out;
|
|
2490
|
+
}
|
|
2491
|
+
async function packages(fs, modules) {
|
|
2492
|
+
const names = await fs.readdir(modules).catch(() => []);
|
|
2493
|
+
const dirs = [];
|
|
2494
|
+
for (const name2 of names) {
|
|
2495
|
+
if (name2.startsWith(".")) continue;
|
|
2496
|
+
if (!name2.startsWith("@")) {
|
|
2497
|
+
dirs.push(`${modules}/${name2}`);
|
|
2498
|
+
continue;
|
|
2499
|
+
}
|
|
2500
|
+
for (const inner of await fs.readdir(`${modules}/${name2}`).catch(() => [])) {
|
|
2501
|
+
dirs.push(`${modules}/${name2}/${inner}`);
|
|
751
2502
|
}
|
|
752
|
-
void Promise.resolve(file.release()).catch(() => void 0);
|
|
753
|
-
throw new KernelError("EMFILE");
|
|
754
2503
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
2504
|
+
return dirs;
|
|
2505
|
+
}
|
|
2506
|
+
async function scanCommands(fs, modules) {
|
|
2507
|
+
const found = /* @__PURE__ */ new Map();
|
|
2508
|
+
for (const pkg of await packages(fs, modules)) {
|
|
2509
|
+
let manifest;
|
|
2510
|
+
try {
|
|
2511
|
+
manifest = JSON.parse(await fs.readFile(`${pkg}/package.json`));
|
|
2512
|
+
} catch {
|
|
2513
|
+
continue;
|
|
2514
|
+
}
|
|
2515
|
+
for (const command of commandsOf(pkg, manifest)) {
|
|
2516
|
+
if (!found.has(command.name)) found.set(command.name, command);
|
|
759
2517
|
}
|
|
760
|
-
const previous = this.fds.get(fd);
|
|
761
|
-
this.fds.set(fd, file);
|
|
762
|
-
this.cloexec.delete(fd);
|
|
763
|
-
this.status.delete(fd);
|
|
764
|
-
void Promise.resolve(previous?.release()).catch(() => void 0);
|
|
765
2518
|
}
|
|
766
|
-
|
|
767
|
-
|
|
2519
|
+
if (!found.has("sh") && found.has("bash")) {
|
|
2520
|
+
found.set("sh", { ...found.get("bash"), name: "sh", argv0: "sh" });
|
|
768
2521
|
}
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
2522
|
+
return found;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// src/fs/commands.ts
|
|
2526
|
+
var S_IFDIR2 = 16384;
|
|
2527
|
+
var S_IFREG2 = 32768;
|
|
2528
|
+
var COMMAND = /^\/(?:usr\/)?bin\/([^/]+)$/;
|
|
2529
|
+
var MAX_LINKS2 = 40;
|
|
2530
|
+
function missing2(err) {
|
|
2531
|
+
return err?.code === "ENOENT";
|
|
2532
|
+
}
|
|
2533
|
+
function synthetic(directory, path) {
|
|
2534
|
+
const epoch = /* @__PURE__ */ new Date(0);
|
|
2535
|
+
return {
|
|
2536
|
+
isFile: !directory,
|
|
2537
|
+
isDirectory: directory,
|
|
2538
|
+
isSymbolicLink: false,
|
|
2539
|
+
size: 0,
|
|
2540
|
+
mode: (directory ? S_IFDIR2 : S_IFREG2) | 493,
|
|
2541
|
+
mtime: epoch,
|
|
2542
|
+
atime: epoch,
|
|
2543
|
+
ctime: epoch,
|
|
2544
|
+
ino: inodeOf(path)
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
async function followLinks(fs, path) {
|
|
2548
|
+
let current = path;
|
|
2549
|
+
for (let hops = 0; hops < MAX_LINKS2; hops++) {
|
|
2550
|
+
const st = await fs.lstat(current).catch(() => void 0);
|
|
2551
|
+
if (!st?.isSymbolicLink) return current;
|
|
2552
|
+
const parent = current.slice(0, current.lastIndexOf("/")) || "/";
|
|
2553
|
+
current = fs.resolvePath(parent, await fs.readlink(current));
|
|
2554
|
+
}
|
|
2555
|
+
return current;
|
|
2556
|
+
}
|
|
2557
|
+
function withCommandDirs(fs, names) {
|
|
2558
|
+
async function children2(path) {
|
|
2559
|
+
if (path === "/") return ["bin", "usr"];
|
|
2560
|
+
if (path === "/usr") return ["bin"];
|
|
2561
|
+
if (path === "/bin" || path === "/usr/bin") return [...await names()];
|
|
2562
|
+
return void 0;
|
|
773
2563
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
this.status.delete(fd);
|
|
779
|
-
return file.release();
|
|
2564
|
+
async function virtual(path) {
|
|
2565
|
+
if (path === "/usr" || path === "/bin" || path === "/usr/bin") return synthetic(true, path);
|
|
2566
|
+
const name2 = COMMAND.exec(path)?.[1];
|
|
2567
|
+
return name2 !== void 0 && (await names()).has(name2) ? synthetic(false, path) : void 0;
|
|
780
2568
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
for (const [fd, file] of this.fds) child.fds.set(fd, file.retain());
|
|
784
|
-
for (const fd of this.cloexec) child.cloexec.add(fd);
|
|
785
|
-
for (const [fd, flags] of this.status) child.status.set(fd, flags);
|
|
786
|
-
return child;
|
|
2569
|
+
async function linked(path) {
|
|
2570
|
+
return await virtual(path) ?? virtual(await followLinks(fs, path));
|
|
787
2571
|
}
|
|
788
|
-
async
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
2572
|
+
async function stat(path, real) {
|
|
2573
|
+
try {
|
|
2574
|
+
return await real(path);
|
|
2575
|
+
} catch (err) {
|
|
2576
|
+
const found = missing2(err) ? await linked(path) : void 0;
|
|
2577
|
+
if (found) return found;
|
|
2578
|
+
throw err;
|
|
2579
|
+
}
|
|
794
2580
|
}
|
|
795
|
-
|
|
2581
|
+
return {
|
|
2582
|
+
resolvePath: (base, path) => fs.resolvePath(base, path),
|
|
2583
|
+
readFile: (path) => fs.readFile(path),
|
|
2584
|
+
async readFileBuffer(path) {
|
|
2585
|
+
try {
|
|
2586
|
+
return await fs.readFileBuffer(path);
|
|
2587
|
+
} catch (err) {
|
|
2588
|
+
if (missing2(err) && (await linked(path))?.isFile) return new Uint8Array(0);
|
|
2589
|
+
throw err;
|
|
2590
|
+
}
|
|
2591
|
+
},
|
|
2592
|
+
writeFile: (path, content) => fs.writeFile(path, content),
|
|
2593
|
+
async exists(path) {
|
|
2594
|
+
return await fs.exists(path) || await linked(path) !== void 0;
|
|
2595
|
+
},
|
|
2596
|
+
stat: (path) => stat(path, (p) => fs.stat(p)),
|
|
2597
|
+
lstat: (path) => stat(path, (p) => fs.lstat(p)),
|
|
2598
|
+
async readdir(path) {
|
|
2599
|
+
const extra = await children2(path);
|
|
2600
|
+
if (!extra) return fs.readdir(path);
|
|
2601
|
+
const real = await fs.readdir(path).catch((err) => {
|
|
2602
|
+
if (missing2(err)) return [];
|
|
2603
|
+
throw err;
|
|
2604
|
+
});
|
|
2605
|
+
return [.../* @__PURE__ */ new Set([...real, ...extra])].sort();
|
|
2606
|
+
},
|
|
2607
|
+
async readdirStat(path) {
|
|
2608
|
+
const extra = await children2(path);
|
|
2609
|
+
if (!extra) return fs.readdirStat(path);
|
|
2610
|
+
const real = await fs.readdirStat(path).catch((err) => {
|
|
2611
|
+
if (missing2(err)) return [];
|
|
2612
|
+
throw err;
|
|
2613
|
+
});
|
|
2614
|
+
const seen = new Set(real.map(([name2]) => name2));
|
|
2615
|
+
const base = path === "/" ? "" : path;
|
|
2616
|
+
const added = extra.filter((name2) => !seen.has(name2));
|
|
2617
|
+
const virtuals = await Promise.all(
|
|
2618
|
+
added.map(
|
|
2619
|
+
async (name2) => [
|
|
2620
|
+
name2,
|
|
2621
|
+
await virtual(`${base}/${name2}`)
|
|
2622
|
+
]
|
|
2623
|
+
)
|
|
2624
|
+
);
|
|
2625
|
+
return [...real, ...virtuals].sort(([a], [b]) => a < b ? -1 : 1);
|
|
2626
|
+
},
|
|
2627
|
+
mkdir: (path, options) => fs.mkdir(path, options),
|
|
2628
|
+
rm: (path, options) => fs.rm(path, options),
|
|
2629
|
+
rename: (from, to) => fs.rename(from, to),
|
|
2630
|
+
symlink: (target, path) => fs.symlink(target, path),
|
|
2631
|
+
readlink: (path) => fs.readlink(path),
|
|
2632
|
+
chmod: (path, mode) => fs.chmod(path, mode),
|
|
2633
|
+
utimes: (path, atime, mtime) => fs.utimes(path, atime, mtime)
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
796
2636
|
|
|
797
2637
|
// src/kernel/children.ts
|
|
798
2638
|
var SpawnError = class extends Error {
|
|
@@ -816,12 +2656,12 @@ var CONTINUED_STATUS = 65535;
|
|
|
816
2656
|
function waitStatus(code, termsig) {
|
|
817
2657
|
return termsig ? termsig & 127 : (code & 255) << 8;
|
|
818
2658
|
}
|
|
819
|
-
function
|
|
2659
|
+
function concat3(chunks) {
|
|
820
2660
|
const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
|
|
821
2661
|
let offset = 0;
|
|
822
|
-
for (const
|
|
823
|
-
out.set(
|
|
824
|
-
offset +=
|
|
2662
|
+
for (const chunk2 of chunks) {
|
|
2663
|
+
out.set(chunk2, offset);
|
|
2664
|
+
offset += chunk2.length;
|
|
825
2665
|
}
|
|
826
2666
|
return out;
|
|
827
2667
|
}
|
|
@@ -973,7 +2813,7 @@ var ChildTable = class {
|
|
|
973
2813
|
if (!slots || !chunks) return new Uint8Array(0);
|
|
974
2814
|
slots.delete(slot);
|
|
975
2815
|
if (slots.size === 0) this.leftovers.delete(pid);
|
|
976
|
-
return
|
|
2816
|
+
return concat3(chunks);
|
|
977
2817
|
}
|
|
978
2818
|
};
|
|
979
2819
|
|
|
@@ -999,12 +2839,12 @@ function syncError(code, label) {
|
|
|
999
2839
|
|
|
1000
2840
|
// src/realm/sync-fs-dispatch.ts
|
|
1001
2841
|
function toErrno(err) {
|
|
1002
|
-
const
|
|
2842
|
+
const message2 = err instanceof Error ? err.message : String(err);
|
|
1003
2843
|
const code = err?.code;
|
|
1004
2844
|
if (typeof code === "string" && /^E[A-Z]+$/.test(code)) {
|
|
1005
|
-
return { ok: false, errno: code, message };
|
|
2845
|
+
return { ok: false, errno: code, message: message2 };
|
|
1006
2846
|
}
|
|
1007
|
-
return { ok: false, errno: "EIO", message };
|
|
2847
|
+
return { ok: false, errno: "EIO", message: message2 };
|
|
1008
2848
|
}
|
|
1009
2849
|
function statJson(s) {
|
|
1010
2850
|
return {
|
|
@@ -1013,21 +2853,14 @@ function statJson(s) {
|
|
|
1013
2853
|
isSymbolicLink: s.isSymbolicLink,
|
|
1014
2854
|
size: s.size,
|
|
1015
2855
|
mode: s.mode,
|
|
1016
|
-
mtimeMs: s.mtime.getTime()
|
|
2856
|
+
mtimeMs: s.mtime.getTime(),
|
|
2857
|
+
atimeMs: s.atime.getTime(),
|
|
2858
|
+
ctimeMs: s.ctime.getTime(),
|
|
2859
|
+
ino: s.ino
|
|
1017
2860
|
};
|
|
1018
2861
|
}
|
|
1019
2862
|
async function readdirStat(fs, dir) {
|
|
1020
|
-
|
|
1021
|
-
const base = dir === "/" ? "" : dir;
|
|
1022
|
-
return Promise.all(
|
|
1023
|
-
names.map(async (name) => {
|
|
1024
|
-
try {
|
|
1025
|
-
return [name, statJson(await fs.lstat(`${base}/${name}`))];
|
|
1026
|
-
} catch {
|
|
1027
|
-
return [name, null];
|
|
1028
|
-
}
|
|
1029
|
-
})
|
|
1030
|
-
);
|
|
2863
|
+
return (await fs.readdirStat(dir)).map(([name2, stat]) => [name2, stat && statJson(stat)]);
|
|
1031
2864
|
}
|
|
1032
2865
|
var done = { ok: true, kind: "void" };
|
|
1033
2866
|
function json(value) {
|
|
@@ -1146,7 +2979,7 @@ function sabViews(sab) {
|
|
|
1146
2979
|
var PENDING_TTL_MS = SYNC_FS_REQUEST_TIMEOUT_MS + 5e3;
|
|
1147
2980
|
function attachSyncSabResponder(port, sab, token, opts) {
|
|
1148
2981
|
const views = sabViews(sab);
|
|
1149
|
-
const { header, window } = views;
|
|
2982
|
+
const { header: header2, window } = views;
|
|
1150
2983
|
const pending = /* @__PURE__ */ new Map();
|
|
1151
2984
|
let disposed = false;
|
|
1152
2985
|
const { dispatch } = opts;
|
|
@@ -1157,18 +2990,18 @@ function attachSyncSabResponder(port, sab, token, opts) {
|
|
|
1157
2990
|
pending.delete(id);
|
|
1158
2991
|
}
|
|
1159
2992
|
function publish(id, entry, offset) {
|
|
1160
|
-
const
|
|
1161
|
-
if (
|
|
1162
|
-
Atomics.store(
|
|
1163
|
-
Atomics.store(
|
|
1164
|
-
Atomics.store(
|
|
1165
|
-
Atomics.store(
|
|
1166
|
-
Atomics.store(
|
|
1167
|
-
Atomics.store(
|
|
1168
|
-
Atomics.notify(
|
|
1169
|
-
if (offset +
|
|
1170
|
-
}
|
|
1171
|
-
function
|
|
2993
|
+
const chunk2 = Math.min(window.byteLength, entry.payload.byteLength - offset);
|
|
2994
|
+
if (chunk2 > 0) window.set(entry.payload.subarray(offset, offset + chunk2));
|
|
2995
|
+
Atomics.store(header2, SAB_I_STATUS, entry.status);
|
|
2996
|
+
Atomics.store(header2, SAB_I_TOTAL, entry.payload.byteLength);
|
|
2997
|
+
Atomics.store(header2, SAB_I_CHUNK, chunk2);
|
|
2998
|
+
Atomics.store(header2, SAB_I_OFFSET, offset);
|
|
2999
|
+
Atomics.store(header2, SAB_I_SEQ, id);
|
|
3000
|
+
Atomics.store(header2, SAB_I_STATE, SAB_STATE_READY);
|
|
3001
|
+
Atomics.notify(header2, SAB_I_STATE);
|
|
3002
|
+
if (offset + chunk2 >= entry.payload.byteLength) drop(id);
|
|
3003
|
+
}
|
|
3004
|
+
function settle2(id, result) {
|
|
1172
3005
|
if (disposed) return;
|
|
1173
3006
|
const { status, payload } = encodeSabResult(result);
|
|
1174
3007
|
drop(id);
|
|
@@ -1187,7 +3020,7 @@ function attachSyncSabResponder(port, sab, token, opts) {
|
|
|
1187
3020
|
const entry = pending.get(data.id);
|
|
1188
3021
|
const offset = data.offset;
|
|
1189
3022
|
if (!entry || typeof offset !== "number" || !Number.isInteger(offset) || offset < 0 || offset > entry.payload.byteLength) {
|
|
1190
|
-
|
|
3023
|
+
settle2(data.id, { ok: false, errno: "EIO", message: "sync-sab: bad continuation" });
|
|
1191
3024
|
return;
|
|
1192
3025
|
}
|
|
1193
3026
|
publish(data.id, entry, offset);
|
|
@@ -1201,7 +3034,7 @@ function attachSyncSabResponder(port, sab, token, opts) {
|
|
|
1201
3034
|
try {
|
|
1202
3035
|
dispatched = dispatch(req);
|
|
1203
3036
|
} catch (err) {
|
|
1204
|
-
|
|
3037
|
+
settle2(id, {
|
|
1205
3038
|
ok: false,
|
|
1206
3039
|
errno: "EIO",
|
|
1207
3040
|
message: err instanceof Error ? err.message : String(err)
|
|
@@ -1209,8 +3042,8 @@ function attachSyncSabResponder(port, sab, token, opts) {
|
|
|
1209
3042
|
return;
|
|
1210
3043
|
}
|
|
1211
3044
|
void dispatched.then(
|
|
1212
|
-
(result) =>
|
|
1213
|
-
(err) =>
|
|
3045
|
+
(result) => settle2(id, result),
|
|
3046
|
+
(err) => settle2(id, {
|
|
1214
3047
|
ok: false,
|
|
1215
3048
|
errno: "EIO",
|
|
1216
3049
|
message: err instanceof Error ? err.message : String(err)
|
|
@@ -1228,6 +3061,112 @@ function attachSyncSabResponder(port, sab, token, opts) {
|
|
|
1228
3061
|
};
|
|
1229
3062
|
}
|
|
1230
3063
|
|
|
3064
|
+
// src/kernel/net/http-syscalls.ts
|
|
3065
|
+
var HTTP_OPS = ["net-request", "net-read", "net-close"];
|
|
3066
|
+
var EMPTY = new Uint8Array(0);
|
|
3067
|
+
function message(e) {
|
|
3068
|
+
return e instanceof Error ? e.message : String(e);
|
|
3069
|
+
}
|
|
3070
|
+
async function* once(bytes) {
|
|
3071
|
+
yield bytes;
|
|
3072
|
+
}
|
|
3073
|
+
var HttpHandles = class {
|
|
3074
|
+
transport;
|
|
3075
|
+
open = /* @__PURE__ */ new Map();
|
|
3076
|
+
nextHandle = 0;
|
|
3077
|
+
constructor(transport) {
|
|
3078
|
+
this.transport = transport;
|
|
3079
|
+
}
|
|
3080
|
+
syscall(req) {
|
|
3081
|
+
if (req.op === "net-request") return this.request(req);
|
|
3082
|
+
if (req.op === "net-read") return this.read(req.handle, req.max);
|
|
3083
|
+
return this.close(req.handle);
|
|
3084
|
+
}
|
|
3085
|
+
add(abort, body, cancel) {
|
|
3086
|
+
const handle = ++this.nextHandle;
|
|
3087
|
+
this.open.set(handle, {
|
|
3088
|
+
abort,
|
|
3089
|
+
body: body[Symbol.asyncIterator](),
|
|
3090
|
+
left: EMPTY,
|
|
3091
|
+
done: false,
|
|
3092
|
+
reading: Promise.resolve(),
|
|
3093
|
+
cancel
|
|
3094
|
+
});
|
|
3095
|
+
return handle;
|
|
3096
|
+
}
|
|
3097
|
+
async request(req) {
|
|
3098
|
+
const abort = new AbortController();
|
|
3099
|
+
try {
|
|
3100
|
+
const response = await this.transport.fetch({
|
|
3101
|
+
url: req.url,
|
|
3102
|
+
method: req.method,
|
|
3103
|
+
headers: req.headers,
|
|
3104
|
+
...req.body ? { body: req.body } : {},
|
|
3105
|
+
signal: abort.signal
|
|
3106
|
+
});
|
|
3107
|
+
const handle = this.add(abort, response.body, () => response.cancel());
|
|
3108
|
+
const head = {
|
|
3109
|
+
handle,
|
|
3110
|
+
status: response.status,
|
|
3111
|
+
statusText: response.statusText,
|
|
3112
|
+
url: req.url,
|
|
3113
|
+
headers: response.headers
|
|
3114
|
+
};
|
|
3115
|
+
return { ok: true, kind: "json", json: head };
|
|
3116
|
+
} catch (e) {
|
|
3117
|
+
const text = message(e);
|
|
3118
|
+
if (text === NO_TRANSPORT) return { ok: false, errno: "ENETUNREACH", message: text };
|
|
3119
|
+
const status = e.status;
|
|
3120
|
+
if (typeof status !== "number" || status === 502) {
|
|
3121
|
+
return { ok: false, errno: "ECONNREFUSED", message: text };
|
|
3122
|
+
}
|
|
3123
|
+
const body = new TextEncoder().encode(text);
|
|
3124
|
+
const handle = this.add(abort, once(body), async () => void 0);
|
|
3125
|
+
const head = {
|
|
3126
|
+
handle,
|
|
3127
|
+
status,
|
|
3128
|
+
statusText: "",
|
|
3129
|
+
url: req.url,
|
|
3130
|
+
headers: [["content-type", "text/plain; charset=utf-8"]]
|
|
3131
|
+
};
|
|
3132
|
+
return { ok: true, kind: "json", json: head };
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
async read(handle, max) {
|
|
3136
|
+
const entry = this.open.get(handle);
|
|
3137
|
+
if (!entry) return { ok: false, errno: "EBADF", message: `no request ${handle}` };
|
|
3138
|
+
const turn = entry.reading.then(() => this.take(entry, max));
|
|
3139
|
+
entry.reading = turn.then(() => void 0);
|
|
3140
|
+
return turn;
|
|
3141
|
+
}
|
|
3142
|
+
async take(entry, max) {
|
|
3143
|
+
if (entry.left.length === 0 && !entry.done) {
|
|
3144
|
+
try {
|
|
3145
|
+
const next = await entry.body.next();
|
|
3146
|
+
if (next.done) entry.done = true;
|
|
3147
|
+
else entry.left = next.value;
|
|
3148
|
+
} catch (e) {
|
|
3149
|
+
entry.done = true;
|
|
3150
|
+
return { ok: false, errno: "EIO", message: message(e) };
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
const bytes = entry.left.subarray(0, Math.max(0, max));
|
|
3154
|
+
entry.left = entry.left.subarray(bytes.length);
|
|
3155
|
+
return { ok: true, kind: "bytes", bytes };
|
|
3156
|
+
}
|
|
3157
|
+
async close(handle) {
|
|
3158
|
+
const entry = this.open.get(handle);
|
|
3159
|
+
if (!entry) return { ok: false, errno: "EBADF", message: `no request ${handle}` };
|
|
3160
|
+
this.open.delete(handle);
|
|
3161
|
+
entry.abort.abort();
|
|
3162
|
+
await entry.cancel().catch(() => void 0);
|
|
3163
|
+
return { ok: true, kind: "void" };
|
|
3164
|
+
}
|
|
3165
|
+
async closeAll() {
|
|
3166
|
+
await Promise.all([...this.open.keys()].map((handle) => this.close(handle)));
|
|
3167
|
+
}
|
|
3168
|
+
};
|
|
3169
|
+
|
|
1231
3170
|
// src/kernel/tty.ts
|
|
1232
3171
|
var ICRNL = 256;
|
|
1233
3172
|
var IUTF8 = 16384;
|
|
@@ -1396,9 +3335,9 @@ var KernelTty = class {
|
|
|
1396
3335
|
return n;
|
|
1397
3336
|
}
|
|
1398
3337
|
rubOut(n) {
|
|
1399
|
-
const
|
|
1400
|
-
const
|
|
1401
|
-
const cells =
|
|
3338
|
+
const dropped2 = this.line.splice(this.line.length - n, n);
|
|
3339
|
+
const utf82 = (this.termios.c_iflag & IUTF8) !== 0;
|
|
3340
|
+
const cells = utf82 ? dropped2.filter((b) => !isContinuation(b)).length : dropped2.length;
|
|
1402
3341
|
if (cells > 0 && this.termios.c_lflag & ECHO) {
|
|
1403
3342
|
this.output(encoder2.encode("\b \b".repeat(cells)), false);
|
|
1404
3343
|
}
|
|
@@ -1448,9 +3387,9 @@ var KernelTty = class {
|
|
|
1448
3387
|
}
|
|
1449
3388
|
const out = new Uint8Array(n);
|
|
1450
3389
|
let at = 0;
|
|
1451
|
-
for (const
|
|
1452
|
-
out.set(
|
|
1453
|
-
at +=
|
|
3390
|
+
for (const chunk2 of chunks) {
|
|
3391
|
+
out.set(chunk2, at);
|
|
3392
|
+
at += chunk2.length;
|
|
1454
3393
|
}
|
|
1455
3394
|
return out;
|
|
1456
3395
|
}
|
|
@@ -1547,9 +3486,9 @@ var PtyPair = class {
|
|
|
1547
3486
|
}
|
|
1548
3487
|
const out = new Uint8Array(status + n);
|
|
1549
3488
|
let at = status;
|
|
1550
|
-
for (const
|
|
1551
|
-
out.set(
|
|
1552
|
-
at +=
|
|
3489
|
+
for (const chunk2 of chunks) {
|
|
3490
|
+
out.set(chunk2, at);
|
|
3491
|
+
at += chunk2.length;
|
|
1553
3492
|
}
|
|
1554
3493
|
return out;
|
|
1555
3494
|
}
|
|
@@ -1720,6 +3659,391 @@ async function waitReady(fds, read, write, deadline, interrupted2) {
|
|
|
1720
3659
|
}
|
|
1721
3660
|
}
|
|
1722
3661
|
|
|
3662
|
+
// src/kernel/socket.ts
|
|
3663
|
+
var SOMAXCONN = 4096;
|
|
3664
|
+
var EPHEMERAL_FIRST = 32768;
|
|
3665
|
+
var EPHEMERAL_LAST = 60999;
|
|
3666
|
+
var SOL_SOCKET = 1;
|
|
3667
|
+
var SO_TYPE = 3;
|
|
3668
|
+
var SO_ERROR = 4;
|
|
3669
|
+
var SO_SNDBUF = 7;
|
|
3670
|
+
var SO_RCVBUF = 8;
|
|
3671
|
+
var SO_ACCEPTCONN = 30;
|
|
3672
|
+
var SO_DOMAIN = 39;
|
|
3673
|
+
var SOCK_STREAM = 1;
|
|
3674
|
+
var AF_UNIX = 1;
|
|
3675
|
+
var AF_INET = 2;
|
|
3676
|
+
var SHUT_RD = 0;
|
|
3677
|
+
var SHUT_WR = 1;
|
|
3678
|
+
var SHUT_RDWR = 2;
|
|
3679
|
+
function isLoopback(host) {
|
|
3680
|
+
return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
3681
|
+
}
|
|
3682
|
+
async function pipeCall(op) {
|
|
3683
|
+
try {
|
|
3684
|
+
return await op();
|
|
3685
|
+
} catch (e) {
|
|
3686
|
+
if (e instanceof PipeError) throw new KernelError(e.code);
|
|
3687
|
+
throw e;
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
var Changes = class {
|
|
3691
|
+
waiters = [];
|
|
3692
|
+
wait(signal) {
|
|
3693
|
+
if (signal?.aborted) return Promise.reject(new KernelError("EINTR"));
|
|
3694
|
+
return new Promise((resolve, reject) => {
|
|
3695
|
+
const onAbort = () => {
|
|
3696
|
+
this.waiters = this.waiters.filter((w) => w !== waiter);
|
|
3697
|
+
reject(new KernelError("EINTR"));
|
|
3698
|
+
};
|
|
3699
|
+
const waiter = () => {
|
|
3700
|
+
signal?.removeEventListener("abort", onAbort);
|
|
3701
|
+
resolve();
|
|
3702
|
+
};
|
|
3703
|
+
this.waiters.push(waiter);
|
|
3704
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
3705
|
+
});
|
|
3706
|
+
}
|
|
3707
|
+
wake() {
|
|
3708
|
+
const waiters = this.waiters;
|
|
3709
|
+
this.waiters = [];
|
|
3710
|
+
for (const resolve of waiters) resolve();
|
|
3711
|
+
}
|
|
3712
|
+
};
|
|
3713
|
+
function link(a, b) {
|
|
3714
|
+
const ab = new KernelPipe();
|
|
3715
|
+
const ba = new KernelPipe();
|
|
3716
|
+
for (const pipe of [ab, ba]) {
|
|
3717
|
+
pipe.openRead();
|
|
3718
|
+
pipe.openWrite();
|
|
3719
|
+
}
|
|
3720
|
+
a.attach({ rx: ba, tx: ab });
|
|
3721
|
+
b.attach({ rx: ab, tx: ba });
|
|
3722
|
+
}
|
|
3723
|
+
var KernelSocket = class _KernelSocket {
|
|
3724
|
+
net;
|
|
3725
|
+
domain;
|
|
3726
|
+
state = "open";
|
|
3727
|
+
local;
|
|
3728
|
+
peer;
|
|
3729
|
+
link;
|
|
3730
|
+
readShut = false;
|
|
3731
|
+
writeShut = false;
|
|
3732
|
+
queue = [];
|
|
3733
|
+
backlog = 0;
|
|
3734
|
+
bound = false;
|
|
3735
|
+
changes = new Changes();
|
|
3736
|
+
options = /* @__PURE__ */ new Map();
|
|
3737
|
+
constructor(net, domain) {
|
|
3738
|
+
this.net = net;
|
|
3739
|
+
this.domain = domain;
|
|
3740
|
+
}
|
|
3741
|
+
attach(link2) {
|
|
3742
|
+
this.link = link2;
|
|
3743
|
+
this.state = "connected";
|
|
3744
|
+
this.changes.wake();
|
|
3745
|
+
}
|
|
3746
|
+
get listening() {
|
|
3747
|
+
return this.state === "listening";
|
|
3748
|
+
}
|
|
3749
|
+
connection() {
|
|
3750
|
+
if (!this.link || this.state !== "connected") throw new KernelError("ENOTCONN");
|
|
3751
|
+
return this.link;
|
|
3752
|
+
}
|
|
3753
|
+
async read(max, signal) {
|
|
3754
|
+
const { rx } = this.connection();
|
|
3755
|
+
if (this.readShut || max <= 0) return new Uint8Array(0);
|
|
3756
|
+
return pipeCall(() => rx.read(max, signal));
|
|
3757
|
+
}
|
|
3758
|
+
async peek(max, signal) {
|
|
3759
|
+
const { rx } = this.connection();
|
|
3760
|
+
if (this.readShut || max <= 0) return new Uint8Array(0);
|
|
3761
|
+
return pipeCall(() => rx.peek(max, signal));
|
|
3762
|
+
}
|
|
3763
|
+
async write(bytes, signal) {
|
|
3764
|
+
const { tx } = this.connection();
|
|
3765
|
+
if (this.writeShut) throw new KernelError("EPIPE");
|
|
3766
|
+
return pipeCall(() => tx.write(bytes, signal));
|
|
3767
|
+
}
|
|
3768
|
+
poll() {
|
|
3769
|
+
if (this.state === "listening") {
|
|
3770
|
+
return { readable: this.queue.length > 0, writable: false, hangup: false };
|
|
3771
|
+
}
|
|
3772
|
+
if (this.state !== "connected" || !this.link) {
|
|
3773
|
+
return { readable: false, writable: false, hangup: true };
|
|
3774
|
+
}
|
|
3775
|
+
const { rx, tx } = this.link;
|
|
3776
|
+
return {
|
|
3777
|
+
readable: this.readShut || rx.readReady,
|
|
3778
|
+
writable: this.writeShut || tx.writeReady,
|
|
3779
|
+
hangup: rx.writersGone && tx.readersGone
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
changed(signal) {
|
|
3783
|
+
const own = this.changes.wait(signal);
|
|
3784
|
+
if (this.state !== "connected" || !this.link) return own;
|
|
3785
|
+
const waits = [own, this.link.rx.changed(signal), this.link.tx.changed(signal)];
|
|
3786
|
+
for (const wait of waits) wait.catch(() => void 0);
|
|
3787
|
+
return pipeCall(() => Promise.race(waits));
|
|
3788
|
+
}
|
|
3789
|
+
bind(addr) {
|
|
3790
|
+
if (this.state !== "open" || this.local) throw new KernelError("EINVAL");
|
|
3791
|
+
if (addr.family !== this.domain) throw new KernelError("EAFNOSUPPORT");
|
|
3792
|
+
this.local = this.net.bind(this, addr);
|
|
3793
|
+
this.bound = true;
|
|
3794
|
+
}
|
|
3795
|
+
listen(backlog) {
|
|
3796
|
+
if (this.state === "connected") throw new KernelError("EISCONN");
|
|
3797
|
+
if (this.state !== "open" && this.state !== "listening") throw new KernelError("EINVAL");
|
|
3798
|
+
if (!this.local) {
|
|
3799
|
+
if (this.domain === "unix") throw new KernelError("EINVAL");
|
|
3800
|
+
this.bind({ family: "inet", host: "0.0.0.0", port: 0 });
|
|
3801
|
+
}
|
|
3802
|
+
this.backlog = Math.min(Math.max(backlog, 1), SOMAXCONN);
|
|
3803
|
+
this.state = "listening";
|
|
3804
|
+
}
|
|
3805
|
+
async accept(signal, nonblock = false) {
|
|
3806
|
+
for (; ; ) {
|
|
3807
|
+
if (this.state !== "listening") throw new KernelError("EINVAL");
|
|
3808
|
+
const next = this.queue.shift();
|
|
3809
|
+
if (next) return next;
|
|
3810
|
+
if (nonblock) throw new KernelError("EAGAIN");
|
|
3811
|
+
await this.changes.wait(signal);
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
offer(server) {
|
|
3815
|
+
if (this.state !== "listening" || this.queue.length >= this.backlog) return false;
|
|
3816
|
+
this.queue.push(server);
|
|
3817
|
+
this.changes.wake();
|
|
3818
|
+
return true;
|
|
3819
|
+
}
|
|
3820
|
+
connect(addr) {
|
|
3821
|
+
if (this.state === "connected") throw new KernelError("EISCONN");
|
|
3822
|
+
if (this.state !== "open") throw new KernelError("EINVAL");
|
|
3823
|
+
if (addr.family !== this.domain) throw new KernelError("EAFNOSUPPORT");
|
|
3824
|
+
const target = this.net.target(addr);
|
|
3825
|
+
if (!this.local) {
|
|
3826
|
+
if (addr.family === "inet") {
|
|
3827
|
+
this.local = this.net.bind(this, { family: "inet", host: "127.0.0.1", port: 0 });
|
|
3828
|
+
this.bound = true;
|
|
3829
|
+
} else this.local = { family: "unix", path: "" };
|
|
3830
|
+
}
|
|
3831
|
+
const server = new _KernelSocket(this.net, this.domain);
|
|
3832
|
+
server.local = addr.family === "inet" ? { ...addr, host: canonicalHost2(addr.host) } : addr;
|
|
3833
|
+
server.peer = this.local;
|
|
3834
|
+
this.peer = server.local;
|
|
3835
|
+
link(this, server);
|
|
3836
|
+
if (!target.offer(server)) {
|
|
3837
|
+
this.unlink();
|
|
3838
|
+
server.close();
|
|
3839
|
+
throw new KernelError("ECONNREFUSED");
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
unlink() {
|
|
3843
|
+
const link2 = this.link;
|
|
3844
|
+
if (link2) {
|
|
3845
|
+
link2.rx.closeRead();
|
|
3846
|
+
link2.tx.closeWrite();
|
|
3847
|
+
}
|
|
3848
|
+
this.link = void 0;
|
|
3849
|
+
this.peer = void 0;
|
|
3850
|
+
this.state = "open";
|
|
3851
|
+
}
|
|
3852
|
+
shutdown(how) {
|
|
3853
|
+
const { rx, tx } = this.connection();
|
|
3854
|
+
if (how !== SHUT_RD && how !== SHUT_WR && how !== SHUT_RDWR) throw new KernelError("EINVAL");
|
|
3855
|
+
if (how !== SHUT_WR && !this.readShut) {
|
|
3856
|
+
this.readShut = true;
|
|
3857
|
+
rx.closeRead();
|
|
3858
|
+
}
|
|
3859
|
+
if (how !== SHUT_RD && !this.writeShut) {
|
|
3860
|
+
this.writeShut = true;
|
|
3861
|
+
tx.closeWrite();
|
|
3862
|
+
}
|
|
3863
|
+
this.changes.wake();
|
|
3864
|
+
}
|
|
3865
|
+
getOption(level, name2) {
|
|
3866
|
+
if (level === SOL_SOCKET) {
|
|
3867
|
+
switch (name2) {
|
|
3868
|
+
case SO_TYPE:
|
|
3869
|
+
return SOCK_STREAM;
|
|
3870
|
+
case SO_ERROR:
|
|
3871
|
+
return 0;
|
|
3872
|
+
case SO_ACCEPTCONN:
|
|
3873
|
+
return this.state === "listening" ? 1 : 0;
|
|
3874
|
+
case SO_DOMAIN:
|
|
3875
|
+
return this.domain === "inet" ? AF_INET : AF_UNIX;
|
|
3876
|
+
case SO_SNDBUF:
|
|
3877
|
+
case SO_RCVBUF:
|
|
3878
|
+
return this.options.get(`${level}:${name2}`) ?? PIPE_CAPACITY;
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
return this.options.get(`${level}:${name2}`) ?? 0;
|
|
3882
|
+
}
|
|
3883
|
+
setOption(level, name2, value) {
|
|
3884
|
+
this.options.set(`${level}:${name2}`, value);
|
|
3885
|
+
}
|
|
3886
|
+
close() {
|
|
3887
|
+
const was = this.state;
|
|
3888
|
+
this.state = "closed";
|
|
3889
|
+
if (was === "listening") {
|
|
3890
|
+
for (const pending of this.queue.splice(0)) pending.close();
|
|
3891
|
+
}
|
|
3892
|
+
if (this.link) {
|
|
3893
|
+
if (!this.readShut) this.link.rx.closeRead();
|
|
3894
|
+
if (!this.writeShut) this.link.tx.closeWrite();
|
|
3895
|
+
this.readShut = true;
|
|
3896
|
+
this.writeShut = true;
|
|
3897
|
+
}
|
|
3898
|
+
if (this.bound && this.local) this.net.release(this.local, this);
|
|
3899
|
+
this.bound = false;
|
|
3900
|
+
this.changes.wake();
|
|
3901
|
+
}
|
|
3902
|
+
static pair(net, domain) {
|
|
3903
|
+
const a = new _KernelSocket(net, domain);
|
|
3904
|
+
const b = new _KernelSocket(net, domain);
|
|
3905
|
+
const name2 = domain === "inet" ? { family: "inet", host: "127.0.0.1", port: 0 } : { family: "unix", path: "" };
|
|
3906
|
+
a.local = b.local = a.peer = b.peer = name2;
|
|
3907
|
+
link(a, b);
|
|
3908
|
+
return [a, b];
|
|
3909
|
+
}
|
|
3910
|
+
};
|
|
3911
|
+
function canonicalHost2(host) {
|
|
3912
|
+
return host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
3913
|
+
}
|
|
3914
|
+
function keyOf(addr) {
|
|
3915
|
+
return addr.family === "inet" ? `inet:${addr.port}` : `unix:${addr.path}`;
|
|
3916
|
+
}
|
|
3917
|
+
var LoopbackNet = class {
|
|
3918
|
+
bound = /* @__PURE__ */ new Map();
|
|
3919
|
+
activators = /* @__PURE__ */ new Map();
|
|
3920
|
+
nextEphemeral = EPHEMERAL_FIRST;
|
|
3921
|
+
activate(addr, start) {
|
|
3922
|
+
this.activators.set(keyOf(addr), start);
|
|
3923
|
+
}
|
|
3924
|
+
socket(domain) {
|
|
3925
|
+
return new KernelSocket(this, domain);
|
|
3926
|
+
}
|
|
3927
|
+
listen(addr, backlog = 128) {
|
|
3928
|
+
const socket = this.socket(addr.family);
|
|
3929
|
+
socket.bind(addr);
|
|
3930
|
+
socket.listen(backlog);
|
|
3931
|
+
return socket;
|
|
3932
|
+
}
|
|
3933
|
+
connect(addr) {
|
|
3934
|
+
const socket = this.socket(addr.family);
|
|
3935
|
+
socket.connect(addr);
|
|
3936
|
+
return socket;
|
|
3937
|
+
}
|
|
3938
|
+
bind(socket, addr) {
|
|
3939
|
+
if (addr.family === "unix") {
|
|
3940
|
+
if (addr.path === "") throw new KernelError("EINVAL");
|
|
3941
|
+
} else if (addr.host !== "0.0.0.0" && !isLoopback(addr.host)) {
|
|
3942
|
+
throw new KernelError("EADDRNOTAVAIL");
|
|
3943
|
+
}
|
|
3944
|
+
const named = addr.family === "inet" && addr.port === 0 ? { ...addr, port: this.ephemeral() } : addr;
|
|
3945
|
+
const key = keyOf(named);
|
|
3946
|
+
if (this.bound.has(key)) throw new KernelError("EADDRINUSE");
|
|
3947
|
+
this.bound.set(key, socket);
|
|
3948
|
+
return named;
|
|
3949
|
+
}
|
|
3950
|
+
release(addr, socket) {
|
|
3951
|
+
const key = keyOf(addr);
|
|
3952
|
+
if (this.bound.get(key) === socket) this.bound.delete(key);
|
|
3953
|
+
}
|
|
3954
|
+
target(addr) {
|
|
3955
|
+
if (addr.family === "inet" && !isLoopback(canonicalHost2(addr.host))) {
|
|
3956
|
+
throw new KernelError("ENETUNREACH");
|
|
3957
|
+
}
|
|
3958
|
+
const key = keyOf(addr);
|
|
3959
|
+
if (!this.bound.has(key)) this.activators.get(key)?.();
|
|
3960
|
+
const socket = this.bound.get(key);
|
|
3961
|
+
if (!socket?.listening) throw new KernelError("ECONNREFUSED");
|
|
3962
|
+
return socket;
|
|
3963
|
+
}
|
|
3964
|
+
ephemeral() {
|
|
3965
|
+
const span = EPHEMERAL_LAST - EPHEMERAL_FIRST + 1;
|
|
3966
|
+
for (let i = 0; i < span; i++) {
|
|
3967
|
+
const port = this.nextEphemeral;
|
|
3968
|
+
this.nextEphemeral = port === EPHEMERAL_LAST ? EPHEMERAL_FIRST : port + 1;
|
|
3969
|
+
if (!this.bound.has(`inet:${port}`)) return port;
|
|
3970
|
+
}
|
|
3971
|
+
throw new KernelError("EADDRINUSE");
|
|
3972
|
+
}
|
|
3973
|
+
};
|
|
3974
|
+
|
|
3975
|
+
// src/kernel/socket-syscalls.ts
|
|
3976
|
+
var SOCKET_OPS = [
|
|
3977
|
+
"sock-open",
|
|
3978
|
+
"sock-pair",
|
|
3979
|
+
"sock-bind",
|
|
3980
|
+
"sock-listen",
|
|
3981
|
+
"sock-accept",
|
|
3982
|
+
"sock-connect",
|
|
3983
|
+
"sock-shutdown",
|
|
3984
|
+
"sock-name",
|
|
3985
|
+
"sock-getopt",
|
|
3986
|
+
"sock-setopt"
|
|
3987
|
+
];
|
|
3988
|
+
function socketAt(fds, fd) {
|
|
3989
|
+
const file = fds.get(fd).file;
|
|
3990
|
+
if (!(file instanceof KernelSocket)) throw new KernelError("ENOTSOCK");
|
|
3991
|
+
return file;
|
|
3992
|
+
}
|
|
3993
|
+
var ok = (json3) => json3 === void 0 ? { ok: true, kind: "void" } : { ok: true, kind: "json", json: json3 };
|
|
3994
|
+
async function socketSyscall(req, proc) {
|
|
3995
|
+
const { fds, net } = proc;
|
|
3996
|
+
switch (req.op) {
|
|
3997
|
+
case "sock-open":
|
|
3998
|
+
return ok(fds.install(new OpenFile(net.socket(req.domain)), 3));
|
|
3999
|
+
case "sock-pair": {
|
|
4000
|
+
const [a, b] = KernelSocket.pair(net, req.domain);
|
|
4001
|
+
const first = fds.install(new OpenFile(a), 3);
|
|
4002
|
+
try {
|
|
4003
|
+
return ok([first, fds.install(new OpenFile(b), 3)]);
|
|
4004
|
+
} catch (e) {
|
|
4005
|
+
await Promise.resolve(fds.close(first));
|
|
4006
|
+
throw e;
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
case "sock-bind":
|
|
4010
|
+
socketAt(fds, req.fd).bind(req.addr);
|
|
4011
|
+
return ok();
|
|
4012
|
+
case "sock-listen":
|
|
4013
|
+
socketAt(fds, req.fd).listen(req.backlog);
|
|
4014
|
+
return ok();
|
|
4015
|
+
case "sock-accept":
|
|
4016
|
+
return ok(await accept(proc, req.fd, req.nonblock));
|
|
4017
|
+
case "sock-connect":
|
|
4018
|
+
socketAt(fds, req.fd).connect(req.addr);
|
|
4019
|
+
if (req.nonblock) throw new KernelError("EINPROGRESS");
|
|
4020
|
+
return ok();
|
|
4021
|
+
case "sock-shutdown":
|
|
4022
|
+
socketAt(fds, req.fd).shutdown(req.how);
|
|
4023
|
+
return ok();
|
|
4024
|
+
case "sock-name": {
|
|
4025
|
+
const socket = socketAt(fds, req.fd);
|
|
4026
|
+
const addr = req.peer ? socket.peer : socket.local;
|
|
4027
|
+
if (req.peer && !addr) throw new KernelError("ENOTCONN");
|
|
4028
|
+
return ok(addr ?? unnamed(socket.domain));
|
|
4029
|
+
}
|
|
4030
|
+
case "sock-getopt":
|
|
4031
|
+
return ok(socketAt(fds, req.fd).getOption(req.level, req.name));
|
|
4032
|
+
case "sock-setopt":
|
|
4033
|
+
socketAt(fds, req.fd).setOption(req.level, req.name, req.value);
|
|
4034
|
+
return ok();
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
function unnamed(domain) {
|
|
4038
|
+
return domain === "inet" ? { family: "inet", host: "0.0.0.0", port: 0 } : { family: "unix", path: "" };
|
|
4039
|
+
}
|
|
4040
|
+
async function accept(proc, fd, nonblock) {
|
|
4041
|
+
const listener = socketAt(proc.fds, fd);
|
|
4042
|
+
const waits = !nonblock && !pollFile(listener).readable;
|
|
4043
|
+
const socket = await listener.accept(waits ? proc.blocking() : void 0, nonblock);
|
|
4044
|
+
return { fd: proc.fds.install(new OpenFile(socket), 3), peer: socket.peer };
|
|
4045
|
+
}
|
|
4046
|
+
|
|
1723
4047
|
// src/kernel/vfs-file.ts
|
|
1724
4048
|
var O_ACCMODE = 3;
|
|
1725
4049
|
var O_WRONLY = 1;
|
|
@@ -1749,14 +4073,14 @@ var VfsNode = class {
|
|
|
1749
4073
|
fs;
|
|
1750
4074
|
path;
|
|
1751
4075
|
orphaned;
|
|
1752
|
-
constructor(fs, path,
|
|
4076
|
+
constructor(fs, path, contents2, orphaned = false, dirty = false) {
|
|
1753
4077
|
this.fs = fs;
|
|
1754
4078
|
this.path = path;
|
|
1755
4079
|
this.orphaned = orphaned;
|
|
1756
|
-
if (
|
|
4080
|
+
if (contents2 !== void 0) this.data = new Uint8Array(contents2);
|
|
1757
4081
|
else if (orphaned) this.data = new Uint8Array(0);
|
|
1758
4082
|
this.length = this.data?.length ?? 0;
|
|
1759
|
-
if (dirty &&
|
|
4083
|
+
if (dirty && contents2 !== void 0) this.markDirty();
|
|
1760
4084
|
}
|
|
1761
4085
|
serial(op) {
|
|
1762
4086
|
const next = this.queue.then(op);
|
|
@@ -1776,8 +4100,8 @@ var VfsNode = class {
|
|
|
1776
4100
|
}
|
|
1777
4101
|
return this.data;
|
|
1778
4102
|
}
|
|
1779
|
-
replace(
|
|
1780
|
-
this.data = new Uint8Array(
|
|
4103
|
+
replace(contents2) {
|
|
4104
|
+
this.data = new Uint8Array(contents2);
|
|
1781
4105
|
this.length = this.data.length;
|
|
1782
4106
|
this.missing = false;
|
|
1783
4107
|
this.markDirty();
|
|
@@ -1858,13 +4182,13 @@ var VfsNodes = class {
|
|
|
1858
4182
|
node.opens++;
|
|
1859
4183
|
return node;
|
|
1860
4184
|
}
|
|
1861
|
-
adopt(path,
|
|
4185
|
+
adopt(path, contents2, dirty) {
|
|
1862
4186
|
let node = this.byPath.get(path);
|
|
1863
4187
|
if (!node) {
|
|
1864
|
-
node = new VfsNode(this.fs, path,
|
|
4188
|
+
node = new VfsNode(this.fs, path, contents2, false, dirty);
|
|
1865
4189
|
this.byPath.set(path, node);
|
|
1866
4190
|
} else if (dirty) {
|
|
1867
|
-
node.replace(
|
|
4191
|
+
node.replace(contents2);
|
|
1868
4192
|
}
|
|
1869
4193
|
node.opens++;
|
|
1870
4194
|
return node;
|
|
@@ -1888,15 +4212,15 @@ var VfsNodes = class {
|
|
|
1888
4212
|
}
|
|
1889
4213
|
renamed(from, to) {
|
|
1890
4214
|
if (from === to) return;
|
|
1891
|
-
const
|
|
4215
|
+
const moved2 = [];
|
|
1892
4216
|
for (const [p, node] of this.byPath) {
|
|
1893
|
-
if (within2(p, from))
|
|
4217
|
+
if (within2(p, from)) moved2.push(node);
|
|
1894
4218
|
else if (within2(p, to)) {
|
|
1895
4219
|
node.orphaned = true;
|
|
1896
4220
|
this.byPath.delete(p);
|
|
1897
4221
|
}
|
|
1898
4222
|
}
|
|
1899
|
-
for (const node of
|
|
4223
|
+
for (const node of moved2) {
|
|
1900
4224
|
this.byPath.delete(node.path);
|
|
1901
4225
|
node.path = to + node.path.slice(from.length);
|
|
1902
4226
|
this.byPath.set(node.path, node);
|
|
@@ -1986,6 +4310,12 @@ var JOB_OPS = /* @__PURE__ */ new Set([
|
|
|
1986
4310
|
function isJobSyscall(req) {
|
|
1987
4311
|
return JOB_OPS.has(req.op);
|
|
1988
4312
|
}
|
|
4313
|
+
function isHttpSyscall(req) {
|
|
4314
|
+
return req.op.startsWith("net-");
|
|
4315
|
+
}
|
|
4316
|
+
function isSocketSyscall(req) {
|
|
4317
|
+
return req.op.startsWith("sock-");
|
|
4318
|
+
}
|
|
1989
4319
|
function isPtySyscall(req) {
|
|
1990
4320
|
return req.op.startsWith("pty-");
|
|
1991
4321
|
}
|
|
@@ -2044,6 +4374,8 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
|
|
|
2044
4374
|
"tty-pgrp-set",
|
|
2045
4375
|
"sig-mask",
|
|
2046
4376
|
"sig-pause",
|
|
4377
|
+
...SOCKET_OPS,
|
|
4378
|
+
...HTTP_OPS,
|
|
2047
4379
|
...PTY_OPS
|
|
2048
4380
|
]);
|
|
2049
4381
|
function isWasmSyscall(req) {
|
|
@@ -2064,6 +4396,7 @@ var WasmProcess = class {
|
|
|
2064
4396
|
resumed = Promise.resolve();
|
|
2065
4397
|
wake;
|
|
2066
4398
|
stateListeners = [];
|
|
4399
|
+
net;
|
|
2067
4400
|
nodes;
|
|
2068
4401
|
pid;
|
|
2069
4402
|
fds;
|
|
@@ -2133,6 +4466,8 @@ var WasmProcess = class {
|
|
|
2133
4466
|
if (isFdSyscall(req)) return await this.fdSyscall(req);
|
|
2134
4467
|
if (isTtySyscall(req)) return this.ttySyscall(req);
|
|
2135
4468
|
if (isJobSyscall(req)) return this.jobSyscall(req);
|
|
4469
|
+
if (isSocketSyscall(req)) return await this.socketSyscall(req);
|
|
4470
|
+
if (isHttpSyscall(req)) return await this.httpSyscall(req);
|
|
2136
4471
|
if (isPtySyscall(req)) {
|
|
2137
4472
|
const { ptys, jobs } = this.options;
|
|
2138
4473
|
return ptySyscall(req, { pid: this.pid, fds: this.fds, ptys, jobs });
|
|
@@ -2330,7 +4665,7 @@ var WasmProcess = class {
|
|
|
2330
4665
|
const flags = this.fds.statusFlags(fd);
|
|
2331
4666
|
return {
|
|
2332
4667
|
tty: file.tty !== void 0,
|
|
2333
|
-
kind: kernelFdKind(file),
|
|
4668
|
+
kind: file instanceof KernelSocket ? "socket" : kernelFdKind(file),
|
|
2334
4669
|
...file.heldMeta ? { meta: file.heldMeta } : {},
|
|
2335
4670
|
...flags !== void 0 ? { flags } : {},
|
|
2336
4671
|
...this.fds.closesOnExec(fd) ? { cloexec: true } : {},
|
|
@@ -2403,6 +4738,19 @@ var WasmProcess = class {
|
|
|
2403
4738
|
return { ok: true, kind: "json", json: jobs.setsid(this.pid) };
|
|
2404
4739
|
}
|
|
2405
4740
|
}
|
|
4741
|
+
httpSyscall(req) {
|
|
4742
|
+
const http = this.options.http;
|
|
4743
|
+
if (!http) return Promise.resolve({ ok: false, errno: "ENETUNREACH", message: NO_TRANSPORT });
|
|
4744
|
+
return http.syscall(req);
|
|
4745
|
+
}
|
|
4746
|
+
socketSyscall(req) {
|
|
4747
|
+
this.net ??= this.options.net ?? new LoopbackNet();
|
|
4748
|
+
return socketSyscall(req, {
|
|
4749
|
+
fds: this.fds,
|
|
4750
|
+
net: this.net,
|
|
4751
|
+
blocking: () => this.blockingSignal()
|
|
4752
|
+
});
|
|
4753
|
+
}
|
|
2406
4754
|
tty(fd) {
|
|
2407
4755
|
const file = this.fds.get(fd).file;
|
|
2408
4756
|
const tty = file.tty ?? file.pty?.slave;
|
|
@@ -2501,6 +4849,7 @@ var WasmProcess = class {
|
|
|
2501
4849
|
this.clearAlarm();
|
|
2502
4850
|
for (const pid of this.children.pids()) this.options.onReap?.(pid);
|
|
2503
4851
|
await this.fds.closeAll();
|
|
4852
|
+
await this.options.http?.closeAll();
|
|
2504
4853
|
}
|
|
2505
4854
|
};
|
|
2506
4855
|
|
|
@@ -2522,7 +4871,7 @@ function inheritedFds(fds) {
|
|
|
2522
4871
|
return fds.numbers().filter((fd) => fd > 2 && !fds.get(fd).file.held).map((fd) => {
|
|
2523
4872
|
const open = fds.get(fd);
|
|
2524
4873
|
const flags = fds.statusFlags(fd);
|
|
2525
|
-
const kind = kernelFdKind(open.file);
|
|
4874
|
+
const kind = open.file instanceof KernelSocket ? "socket" : kernelFdKind(open.file);
|
|
2526
4875
|
const meta = open.file.heldMeta;
|
|
2527
4876
|
return {
|
|
2528
4877
|
fd,
|
|
@@ -2536,7 +4885,7 @@ function inheritedFds(fds) {
|
|
|
2536
4885
|
}
|
|
2537
4886
|
function spawnWasmProcess(opts) {
|
|
2538
4887
|
const sab = new SharedArrayBuffer(SAB_HEADER_BYTES + SAB_DEFAULT_WINDOW_BYTES);
|
|
2539
|
-
const
|
|
4888
|
+
const header2 = new Int32Array(sab, 0, SAB_HEADER_I32);
|
|
2540
4889
|
const process = new WasmProcess(opts.pid, opts.fds, {
|
|
2541
4890
|
spawner: opts.spawner,
|
|
2542
4891
|
forker: opts.forker,
|
|
@@ -2544,10 +4893,12 @@ function spawnWasmProcess(opts) {
|
|
|
2544
4893
|
kill: opts.kill,
|
|
2545
4894
|
jobs: opts.jobs,
|
|
2546
4895
|
ptys: opts.ptys,
|
|
4896
|
+
net: opts.net,
|
|
4897
|
+
http: opts.http,
|
|
2547
4898
|
onReap: opts.onReap,
|
|
2548
|
-
onPending: (sig) => void Atomics.or(
|
|
2549
|
-
onTimer: (which) => void Atomics.or(
|
|
2550
|
-
hasPending: () => Atomics.load(
|
|
4899
|
+
onPending: (sig) => void Atomics.or(header2, SAB_I_SIGNALS, sigbit(sig)),
|
|
4900
|
+
onTimer: (which) => void Atomics.or(header2, SAB_I_TIMERS, 1 << which),
|
|
4901
|
+
hasPending: () => Atomics.load(header2, SAB_I_SIGNALS) !== 0 || Atomics.load(header2, SAB_I_TIMERS) !== 0,
|
|
2551
4902
|
raise: (sig) => signal(sig)
|
|
2552
4903
|
});
|
|
2553
4904
|
const token = mintSyncFsToken({ fs: opts.fs, cwd: opts.cwd });
|
|
@@ -2557,8 +4908,8 @@ function spawnWasmProcess(opts) {
|
|
|
2557
4908
|
return dispatchSyncFs(req);
|
|
2558
4909
|
};
|
|
2559
4910
|
const responder = attachSyncSabResponder(worker, sab, token, { dispatch });
|
|
2560
|
-
let
|
|
2561
|
-
const exited = new Promise((resolve) =>
|
|
4911
|
+
let settle2;
|
|
4912
|
+
const exited = new Promise((resolve) => settle2 = resolve);
|
|
2562
4913
|
let done3 = false;
|
|
2563
4914
|
let endedBy;
|
|
2564
4915
|
const finish = (code, sig) => {
|
|
@@ -2571,8 +4922,8 @@ function spawnWasmProcess(opts) {
|
|
|
2571
4922
|
revokeSyncFsToken(token);
|
|
2572
4923
|
worker.terminate();
|
|
2573
4924
|
void process.exit().then(
|
|
2574
|
-
() =>
|
|
2575
|
-
() =>
|
|
4925
|
+
() => settle2(code),
|
|
4926
|
+
() => settle2(code)
|
|
2576
4927
|
);
|
|
2577
4928
|
};
|
|
2578
4929
|
const onMessage = (event) => {
|
|
@@ -2682,9 +5033,9 @@ var JobTable = class {
|
|
|
2682
5033
|
this.terminals.delete(pid);
|
|
2683
5034
|
return pid;
|
|
2684
5035
|
}
|
|
2685
|
-
terminalNamed(
|
|
5036
|
+
terminalNamed(name2) {
|
|
2686
5037
|
const known = /* @__PURE__ */ new Set([...this.terminals.values(), ...this.foreground.keys()]);
|
|
2687
|
-
return [...known].find((tty) => tty.name ===
|
|
5038
|
+
return [...known].find((tty) => tty.name === name2);
|
|
2688
5039
|
}
|
|
2689
5040
|
controllingTerminal(pid) {
|
|
2690
5041
|
const member = this.members.get(pid);
|
|
@@ -2754,12 +5105,12 @@ function childHandle(handle) {
|
|
|
2754
5105
|
onState: (listener) => handle.onState(listener)
|
|
2755
5106
|
};
|
|
2756
5107
|
}
|
|
2757
|
-
function
|
|
2758
|
-
const out = new Uint8Array(chunks.reduce((n,
|
|
5108
|
+
function concat4(chunks) {
|
|
5109
|
+
const out = new Uint8Array(chunks.reduce((n, chunk2) => n + chunk2.length, 0));
|
|
2759
5110
|
let offset = 0;
|
|
2760
|
-
for (const
|
|
2761
|
-
out.set(
|
|
2762
|
-
offset +=
|
|
5111
|
+
for (const chunk2 of chunks) {
|
|
5112
|
+
out.set(chunk2, offset);
|
|
5113
|
+
offset += chunk2.length;
|
|
2763
5114
|
}
|
|
2764
5115
|
return out;
|
|
2765
5116
|
}
|
|
@@ -2776,13 +5127,23 @@ var Launcher = class {
|
|
|
2776
5127
|
orphans = /* @__PURE__ */ new Set();
|
|
2777
5128
|
jobs = new JobTable();
|
|
2778
5129
|
ptys = new PtyTable((tty, sig) => this.jobs.signalOwnedForeground(tty, sig));
|
|
5130
|
+
net = new LoopbackNet();
|
|
5131
|
+
ca;
|
|
5132
|
+
transport;
|
|
2779
5133
|
nextPid = 1e3;
|
|
2780
5134
|
terminals = 0;
|
|
2781
5135
|
constructor(options) {
|
|
2782
5136
|
this.base = options.fs;
|
|
2783
5137
|
this.createWorker = options.createWorker;
|
|
2784
5138
|
this.modulesDir = options.modules ?? "/node_modules";
|
|
2785
|
-
this.env = options.env
|
|
5139
|
+
this.env = { ...networkEnv(), ...options.env };
|
|
5140
|
+
this.ca = kernelCa(options.caStore ?? memoryCaStore());
|
|
5141
|
+
this.transport = options.transport ?? missingTransport();
|
|
5142
|
+
enableNetwork(this.net, {
|
|
5143
|
+
transport: this.transport,
|
|
5144
|
+
engine: kernelTlsEngine(packageTlsEngine(options.fs, this.modulesDir)),
|
|
5145
|
+
ca: this.ca
|
|
5146
|
+
});
|
|
2786
5147
|
this.fs = withCommandDirs(options.fs, async () => new Set((await this.commands()).keys()));
|
|
2787
5148
|
}
|
|
2788
5149
|
commands() {
|
|
@@ -2790,12 +5151,14 @@ var Launcher = class {
|
|
|
2790
5151
|
return this.catalog;
|
|
2791
5152
|
}
|
|
2792
5153
|
async resolve(file, argv0, cwd) {
|
|
2793
|
-
const
|
|
2794
|
-
if (
|
|
2795
|
-
const command = (await this.commands()).get(
|
|
5154
|
+
const name2 = COMMAND2.exec(file)?.[1] ?? (file.includes("/") ? void 0 : file);
|
|
5155
|
+
if (name2 !== void 0) {
|
|
5156
|
+
const command = (await this.commands()).get(name2);
|
|
2796
5157
|
return command && targetOf(command);
|
|
2797
5158
|
}
|
|
2798
|
-
const glue = this.fs.resolvePath(cwd, file);
|
|
5159
|
+
const glue = await followLinks(this.base, this.fs.resolvePath(cwd, file));
|
|
5160
|
+
const linked = COMMAND2.exec(glue)?.[1];
|
|
5161
|
+
if (linked !== void 0) return this.resolve(`/bin/${linked}`, argv0, cwd);
|
|
2799
5162
|
const wasm = modulePath(glue);
|
|
2800
5163
|
if (!await this.base.exists(glue) || !await this.base.exists(wasm)) return void 0;
|
|
2801
5164
|
return { glue, wasm, argv0: baseName(argv0 || file) };
|
|
@@ -2883,6 +5246,8 @@ var Launcher = class {
|
|
|
2883
5246
|
kill: (target, sig) => this.kill(target, sig),
|
|
2884
5247
|
jobs: this.jobs,
|
|
2885
5248
|
ptys: this.ptys,
|
|
5249
|
+
net: this.net,
|
|
5250
|
+
http: new HttpHandles(this.transport),
|
|
2886
5251
|
onReap: (child) => this.reaped(child),
|
|
2887
5252
|
...req.fork ? { fork: req.fork } : {},
|
|
2888
5253
|
...req.ppid !== void 0 ? { ppid: req.ppid } : {}
|
|
@@ -2923,6 +5288,7 @@ var Launcher = class {
|
|
|
2923
5288
|
}
|
|
2924
5289
|
async prepare() {
|
|
2925
5290
|
for (const dir of SHARED_DIRS) await this.base.mkdir(dir, { recursive: true });
|
|
5291
|
+
await writeCaFile(this.base, this.ca).catch(() => void 0);
|
|
2926
5292
|
}
|
|
2927
5293
|
environment(cwd, extra) {
|
|
2928
5294
|
return { PATH: "/usr/bin:/bin", HOME: "/home", ...this.env, PWD: cwd, ...extra };
|
|
@@ -2954,7 +5320,7 @@ var Launcher = class {
|
|
|
2954
5320
|
COLORTERM: "truecolor",
|
|
2955
5321
|
...options.env
|
|
2956
5322
|
});
|
|
2957
|
-
const report = (
|
|
5323
|
+
const report = (message2) => options.onData(encoder3.encode(`${message2}\r
|
|
2958
5324
|
`));
|
|
2959
5325
|
const handle = await this.launch(planned, { env, cwd, fds, report });
|
|
2960
5326
|
leader = handle.pid;
|
|
@@ -2982,11 +5348,11 @@ var Launcher = class {
|
|
|
2982
5348
|
};
|
|
2983
5349
|
const stdout = collect(out, options.onStdout);
|
|
2984
5350
|
const stderr = collect(err, options.onStderr);
|
|
2985
|
-
const report = (
|
|
5351
|
+
const report = (message2) => stderr(encoder3.encode(`${message2}
|
|
2986
5352
|
`));
|
|
2987
5353
|
if (!planned) {
|
|
2988
5354
|
report(`${file}: command not found`);
|
|
2989
|
-
return { status: NOT_FOUND, stdout:
|
|
5355
|
+
return { status: NOT_FOUND, stdout: concat4(out), stderr: concat4(err) };
|
|
2990
5356
|
}
|
|
2991
5357
|
const fds = new FdTable();
|
|
2992
5358
|
fds.installAt(0, options.stdin ? bytesSource(options.stdin) : nullFile());
|
|
@@ -2994,15 +5360,15 @@ var Launcher = class {
|
|
|
2994
5360
|
fds.installAt(2, sinkFile(stderr));
|
|
2995
5361
|
const handle = await this.launch(planned, { env, cwd, fds, report });
|
|
2996
5362
|
const status = await handle.exited;
|
|
2997
|
-
return { status, stdout:
|
|
5363
|
+
return { status, stdout: concat4(out), stderr: concat4(err) };
|
|
2998
5364
|
}
|
|
2999
5365
|
};
|
|
3000
5366
|
|
|
3001
5367
|
// src/serve.ts
|
|
3002
|
-
function signalNumber(
|
|
3003
|
-
const key =
|
|
3004
|
-
if (!
|
|
3005
|
-
throw new Error(`unknown signal ${
|
|
5368
|
+
function signalNumber(name2) {
|
|
5369
|
+
const key = name2.slice(3);
|
|
5370
|
+
if (!name2.startsWith("SIG") || !Object.hasOwn(SIG, key))
|
|
5371
|
+
throw new Error(`unknown signal ${name2}`);
|
|
3006
5372
|
return SIG[key];
|
|
3007
5373
|
}
|
|
3008
5374
|
function act(session, req) {
|
|
@@ -3013,6 +5379,7 @@ function act(session, req) {
|
|
|
3013
5379
|
}
|
|
3014
5380
|
function serveKernel(port, deps) {
|
|
3015
5381
|
let launcher;
|
|
5382
|
+
let remote;
|
|
3016
5383
|
const terminals = /* @__PURE__ */ new Map();
|
|
3017
5384
|
const reply = (id, body) => port.postMessage({ id, ...body });
|
|
3018
5385
|
async function ready2() {
|
|
@@ -3033,11 +5400,18 @@ function serveKernel(port, deps) {
|
|
|
3033
5400
|
async function handle(req) {
|
|
3034
5401
|
if (req.op === "init") {
|
|
3035
5402
|
launcher = (req.root ? Promise.resolve(req.root) : deps.storage()).then(async (root) => {
|
|
5403
|
+
const name2 = req.metadata ?? META_DB;
|
|
5404
|
+
const meta = name2 === false ? void 0 : await deps.metadata?.(name2);
|
|
5405
|
+
remote = req.transport ? new RemoteTransport(port, req.transport) : void 0;
|
|
5406
|
+
const fs = new OpfsFs(root, meta);
|
|
5407
|
+
await fs.reconcile();
|
|
3036
5408
|
const started = new Launcher({
|
|
3037
|
-
fs
|
|
5409
|
+
fs,
|
|
3038
5410
|
createWorker: deps.createWorker,
|
|
3039
5411
|
...req.modules ? { modules: req.modules } : {},
|
|
3040
|
-
...req.env ? { env: req.env } : {}
|
|
5412
|
+
...req.env ? { env: req.env } : {},
|
|
5413
|
+
...remote ? { transport: remote } : {},
|
|
5414
|
+
caStore: caStore(deps.metadata ? name2 : false)
|
|
3041
5415
|
});
|
|
3042
5416
|
await started.prepare();
|
|
3043
5417
|
return started;
|
|
@@ -3059,6 +5433,10 @@ function serveKernel(port, deps) {
|
|
|
3059
5433
|
});
|
|
3060
5434
|
}
|
|
3061
5435
|
port.addEventListener("message", (event) => {
|
|
5436
|
+
if (event.data.net !== void 0) {
|
|
5437
|
+
remote?.receive(event.data);
|
|
5438
|
+
return;
|
|
5439
|
+
}
|
|
3062
5440
|
const req = event.data;
|
|
3063
5441
|
handle(req).then(
|
|
3064
5442
|
(result) => reply(req.id, { result }),
|
|
@@ -3071,6 +5449,7 @@ function serveKernel(port, deps) {
|
|
|
3071
5449
|
var processWorker = new URL("./process-worker.js", import.meta.url);
|
|
3072
5450
|
serveKernel(globalThis, {
|
|
3073
5451
|
storage: () => navigator.storage.getDirectory(),
|
|
3074
|
-
createWorker: () => new Worker(processWorker, { type: "module" })
|
|
5452
|
+
createWorker: () => new Worker(processWorker, { type: "module" }),
|
|
5453
|
+
metadata: (name2) => openMeta(name2, indexedDB)
|
|
3075
5454
|
});
|
|
3076
5455
|
//# sourceMappingURL=kernel-worker.js.map
|