@kitman-sh/cli 0.0.0-stage → 0.6.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/dist/chunk-3j7sewn0.js +4618 -0
- package/dist/chunk-6h8ghah0.js +14 -0
- package/dist/chunk-brp7cekg.js +13 -0
- package/dist/chunk-dsefng1v.js +1085 -0
- package/dist/chunk-fc0v325q.js +75 -0
- package/dist/chunk-h8ctzn91.js +241 -0
- package/dist/chunk-hrvcz0m4.js +211 -0
- package/dist/chunk-hw7qkap1.js +1517 -0
- package/dist/chunk-n32jhswz.js +964 -0
- package/dist/chunk-tfr74ays.js +7060 -0
- package/dist/chunk-txyq8r41.js +25537 -0
- package/dist/chunk-xdk488qp.js +364 -0
- package/dist/egress-proxy.ts +107 -0
- package/dist/index.js +5738 -0
- package/package.json +16 -4
- package/README.md +0 -3
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
build2,
|
|
4
|
+
provide2,
|
|
5
|
+
gen,
|
|
6
|
+
flatMap,
|
|
7
|
+
provideContext,
|
|
8
|
+
runForEach,
|
|
9
|
+
optionalKey,
|
|
10
|
+
optional,
|
|
11
|
+
Literal,
|
|
12
|
+
Null,
|
|
13
|
+
String2,
|
|
14
|
+
Boolean2,
|
|
15
|
+
Void,
|
|
16
|
+
Struct,
|
|
17
|
+
Record,
|
|
18
|
+
ArraySchema,
|
|
19
|
+
Union,
|
|
20
|
+
Literals,
|
|
21
|
+
UndefinedOr,
|
|
22
|
+
TaggedStruct,
|
|
23
|
+
Finite,
|
|
24
|
+
Int,
|
|
25
|
+
Uint8ArrayFromBase64,
|
|
26
|
+
TaggedError2,
|
|
27
|
+
Json,
|
|
28
|
+
WorkUnitRepoBinding,
|
|
29
|
+
WorkUnitClaimResult,
|
|
30
|
+
WorkUnitClaimInput,
|
|
31
|
+
WorkUnitHeartbeatInput,
|
|
32
|
+
WorkUnitHeartbeatResult,
|
|
33
|
+
WorkUnitReleaseInput,
|
|
34
|
+
WorkUnitCompletion
|
|
35
|
+
} from "./chunk-txyq8r41.js";
|
|
36
|
+
import {
|
|
37
|
+
layerWebSocketConstructorGlobal,
|
|
38
|
+
layerWebSocket,
|
|
39
|
+
make5,
|
|
40
|
+
make6,
|
|
41
|
+
layerJson,
|
|
42
|
+
make7,
|
|
43
|
+
withHeaders,
|
|
44
|
+
layerProtocolSocket,
|
|
45
|
+
Service
|
|
46
|
+
} from "./chunk-hw7qkap1.js";
|
|
47
|
+
|
|
48
|
+
// packages/live/src/rpc/runner-group.ts
|
|
49
|
+
var RunnerHeaders = { runnerId: "x-kitman-runner-id", protocolVersion: "x-kitman-rpc-protocol" };
|
|
50
|
+
var RunnerRpcProtocolVersion = 1;
|
|
51
|
+
|
|
52
|
+
class RunnerRpcError extends TaggedError2()("RunnerRpcError", {
|
|
53
|
+
code: String2,
|
|
54
|
+
message: String2,
|
|
55
|
+
status: Finite
|
|
56
|
+
}) {
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class RunnerAuthentication extends Service()("RunnerAuthentication", {
|
|
60
|
+
error: RunnerRpcError
|
|
61
|
+
}) {
|
|
62
|
+
}
|
|
63
|
+
var EventFilterValue = Union([String2, Finite, Boolean2, Null]);
|
|
64
|
+
var WorkAvailable = TaggedStruct("WorkAvailable", {});
|
|
65
|
+
var LeaseRevoked = TaggedStruct("LeaseRevoked", { workUnitId: String2 });
|
|
66
|
+
var Settled = TaggedStruct("Settled", { workUnitId: String2 });
|
|
67
|
+
var Canceled = TaggedStruct("Canceled", { workUnitId: String2 });
|
|
68
|
+
var RunSettled = TaggedStruct("RunSettled", { runId: String2 });
|
|
69
|
+
var Steer = TaggedStruct("Steer", {
|
|
70
|
+
workUnitId: String2,
|
|
71
|
+
messageId: String2,
|
|
72
|
+
text: String2
|
|
73
|
+
});
|
|
74
|
+
var SessionUpdated = TaggedStruct("SessionUpdated", { workUnitId: String2 });
|
|
75
|
+
var RunnerSignal = Union([
|
|
76
|
+
WorkAvailable,
|
|
77
|
+
LeaseRevoked,
|
|
78
|
+
Settled,
|
|
79
|
+
Canceled,
|
|
80
|
+
RunSettled,
|
|
81
|
+
Steer,
|
|
82
|
+
SessionUpdated
|
|
83
|
+
]);
|
|
84
|
+
var ProgramLease = Struct({
|
|
85
|
+
workUnitId: String2,
|
|
86
|
+
leaseOwnerId: String2,
|
|
87
|
+
runnerReplicaId: String2,
|
|
88
|
+
leaseId: String2,
|
|
89
|
+
attempt: Int,
|
|
90
|
+
fencingToken: String2,
|
|
91
|
+
capacitySlot: String2
|
|
92
|
+
});
|
|
93
|
+
var CheckpointSession = Struct({
|
|
94
|
+
harness: String2,
|
|
95
|
+
id: String2,
|
|
96
|
+
name: String2,
|
|
97
|
+
content: Uint8ArrayFromBase64
|
|
98
|
+
});
|
|
99
|
+
var StepCheckpoint = Struct({
|
|
100
|
+
ref: optionalKey(String2),
|
|
101
|
+
revision: optionalKey(String2),
|
|
102
|
+
session: optionalKey(CheckpointSession)
|
|
103
|
+
});
|
|
104
|
+
var SuggestedVerification = Struct({
|
|
105
|
+
name: String2,
|
|
106
|
+
command: String2,
|
|
107
|
+
workingDirectory: optional(String2)
|
|
108
|
+
});
|
|
109
|
+
var ProgramOutcome = Union([
|
|
110
|
+
Struct({ status: Literal("completed"), output: Json }),
|
|
111
|
+
Struct({ status: Literal("failed"), failureSummary: String2 })
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
class RunnerRpcs extends make6(make5("Claim", {
|
|
115
|
+
payload: WorkUnitClaimInput,
|
|
116
|
+
success: WorkUnitClaimResult,
|
|
117
|
+
error: RunnerRpcError
|
|
118
|
+
}), make5("Heartbeat", {
|
|
119
|
+
payload: WorkUnitHeartbeatInput,
|
|
120
|
+
success: WorkUnitHeartbeatResult,
|
|
121
|
+
error: RunnerRpcError
|
|
122
|
+
}), make5("Complete", {
|
|
123
|
+
payload: WorkUnitCompletion,
|
|
124
|
+
error: RunnerRpcError
|
|
125
|
+
}), make5("Release", {
|
|
126
|
+
payload: WorkUnitReleaseInput,
|
|
127
|
+
error: RunnerRpcError
|
|
128
|
+
}), make5("RecordActivity", {
|
|
129
|
+
payload: { lease: ProgramLease, name: String2, attempt: Int, exit: Json },
|
|
130
|
+
error: RunnerRpcError
|
|
131
|
+
}), make5("DeferredDone", {
|
|
132
|
+
payload: { lease: ProgramLease, name: String2, exit: Json },
|
|
133
|
+
error: RunnerRpcError
|
|
134
|
+
}), make5("ScheduleClock", {
|
|
135
|
+
payload: { lease: ProgramLease, name: String2, deferredName: String2, durationMillis: Int },
|
|
136
|
+
error: RunnerRpcError
|
|
137
|
+
}), make5("PutQuestion", {
|
|
138
|
+
payload: {
|
|
139
|
+
lease: ProgramLease,
|
|
140
|
+
step: String2,
|
|
141
|
+
prompt: String2,
|
|
142
|
+
options: optional(ArraySchema(String2)),
|
|
143
|
+
verification: optional(SuggestedVerification),
|
|
144
|
+
deferredName: String2
|
|
145
|
+
},
|
|
146
|
+
success: Struct({ questionId: String2 }),
|
|
147
|
+
error: RunnerRpcError
|
|
148
|
+
}), make5("StartAgentStep", {
|
|
149
|
+
payload: {
|
|
150
|
+
lease: ProgramLease,
|
|
151
|
+
step: String2,
|
|
152
|
+
deferredName: String2,
|
|
153
|
+
role: String2,
|
|
154
|
+
agentId: String2,
|
|
155
|
+
prompt: String2,
|
|
156
|
+
outputSchema: Json,
|
|
157
|
+
secrets: optionalKey(ArraySchema(String2))
|
|
158
|
+
},
|
|
159
|
+
success: Struct({ stepAttemptId: String2 }),
|
|
160
|
+
error: RunnerRpcError
|
|
161
|
+
}), make5("StartVerification", {
|
|
162
|
+
payload: {
|
|
163
|
+
lease: ProgramLease,
|
|
164
|
+
step: String2,
|
|
165
|
+
deferredName: String2,
|
|
166
|
+
revision: optionalKey(String2),
|
|
167
|
+
checks: optional(ArraySchema(String2))
|
|
168
|
+
},
|
|
169
|
+
success: Struct({ verificationId: String2 }),
|
|
170
|
+
error: RunnerRpcError
|
|
171
|
+
}), make5("KnownEvidence", {
|
|
172
|
+
payload: { lease: ProgramLease, trees: ArraySchema(String2) },
|
|
173
|
+
success: ArraySchema(Struct({ tree: String2, name: String2, command: String2 })),
|
|
174
|
+
error: RunnerRpcError
|
|
175
|
+
}), make5("AwaitEvent", {
|
|
176
|
+
payload: {
|
|
177
|
+
lease: ProgramLease,
|
|
178
|
+
step: String2,
|
|
179
|
+
deferredName: String2,
|
|
180
|
+
event: String2,
|
|
181
|
+
filter: optional(Record(String2, Union([EventFilterValue, ArraySchema(EventFilterValue)]))),
|
|
182
|
+
timeoutMillis: Int
|
|
183
|
+
},
|
|
184
|
+
error: RunnerRpcError
|
|
185
|
+
}), make5("AdmitChild", {
|
|
186
|
+
payload: {
|
|
187
|
+
lease: ProgramLease,
|
|
188
|
+
step: String2,
|
|
189
|
+
loop: String2,
|
|
190
|
+
input: Json,
|
|
191
|
+
deferredName: String2
|
|
192
|
+
},
|
|
193
|
+
success: Struct({ runId: String2 }),
|
|
194
|
+
error: RunnerRpcError
|
|
195
|
+
}), make5("StoreArtifact", {
|
|
196
|
+
payload: {
|
|
197
|
+
lease: ProgramLease,
|
|
198
|
+
step: String2,
|
|
199
|
+
kind: String2,
|
|
200
|
+
title: String2,
|
|
201
|
+
text: String2,
|
|
202
|
+
data: optional(Json)
|
|
203
|
+
},
|
|
204
|
+
success: Struct({ artifactId: String2 }),
|
|
205
|
+
error: RunnerRpcError
|
|
206
|
+
}), make5("RecordNote", {
|
|
207
|
+
payload: {
|
|
208
|
+
lease: ProgramLease,
|
|
209
|
+
step: String2,
|
|
210
|
+
text: String2,
|
|
211
|
+
artifactId: optional(String2)
|
|
212
|
+
},
|
|
213
|
+
error: RunnerRpcError
|
|
214
|
+
}), make5("PerformHostEffect", {
|
|
215
|
+
payload: {
|
|
216
|
+
lease: ProgramLease,
|
|
217
|
+
step: String2,
|
|
218
|
+
effect: Union([
|
|
219
|
+
Struct({ kind: Literal("post_comment"), changeRequestId: String2, body: String2 }),
|
|
220
|
+
Struct({
|
|
221
|
+
kind: Literal("change_request"),
|
|
222
|
+
title: String2,
|
|
223
|
+
body: String2,
|
|
224
|
+
integration: optionalKey(String2)
|
|
225
|
+
}),
|
|
226
|
+
Struct({
|
|
227
|
+
kind: Literal("write_back"),
|
|
228
|
+
milestone: optionalKey(Literals(["started", "change_request_opened", "merged", "failed"])),
|
|
229
|
+
changeRequestUrl: optionalKey(String2)
|
|
230
|
+
})
|
|
231
|
+
])
|
|
232
|
+
},
|
|
233
|
+
success: UndefinedOr(Struct({ id: String2, number: String2, url: String2 })),
|
|
234
|
+
error: RunnerRpcError
|
|
235
|
+
}), make5("CreatePlanTasks", {
|
|
236
|
+
payload: {
|
|
237
|
+
lease: ProgramLease,
|
|
238
|
+
tickets: ArraySchema(Struct({
|
|
239
|
+
key: String2,
|
|
240
|
+
title: String2,
|
|
241
|
+
body: String2,
|
|
242
|
+
acceptanceCriteria: ArraySchema(String2),
|
|
243
|
+
blockedBy: ArraySchema(String2),
|
|
244
|
+
outcome: optionalKey(Literals(["feature", "bug", "refactor", "investigation", "docs", "migration"]))
|
|
245
|
+
}))
|
|
246
|
+
},
|
|
247
|
+
success: ArraySchema(Struct({ key: String2, taskId: String2 })),
|
|
248
|
+
error: RunnerRpcError
|
|
249
|
+
}), make5("IntegrationBranch", {
|
|
250
|
+
payload: { lease: ProgramLease, repoBindingId: optionalKey(String2) },
|
|
251
|
+
success: Struct({ repoBindingId: String2, branch: String2, revision: String2 }),
|
|
252
|
+
error: RunnerRpcError
|
|
253
|
+
}), make5("MergeChangeRequest", {
|
|
254
|
+
payload: { lease: ProgramLease, step: String2, changeRequestId: String2 },
|
|
255
|
+
success: Union([
|
|
256
|
+
Struct({ status: Literal("merged"), revision: String2 }),
|
|
257
|
+
Struct({ status: Literal("conflict") })
|
|
258
|
+
]),
|
|
259
|
+
error: RunnerRpcError
|
|
260
|
+
}), make5("PublishRelease", {
|
|
261
|
+
payload: {
|
|
262
|
+
lease: ProgramLease,
|
|
263
|
+
step: String2,
|
|
264
|
+
tag: String2,
|
|
265
|
+
revision: optionalKey(String2),
|
|
266
|
+
title: String2,
|
|
267
|
+
notes: String2
|
|
268
|
+
},
|
|
269
|
+
success: Struct({ tag: String2, revision: String2, url: String2 }),
|
|
270
|
+
error: RunnerRpcError
|
|
271
|
+
}), make5("ReportSteer", {
|
|
272
|
+
payload: { workUnitId: String2, messageId: String2, delivered: Boolean2 },
|
|
273
|
+
success: Void,
|
|
274
|
+
error: RunnerRpcError
|
|
275
|
+
}), make5("SaveCheckpoint", {
|
|
276
|
+
payload: { lease: ProgramLease, checkpoint: StepCheckpoint },
|
|
277
|
+
success: Void,
|
|
278
|
+
error: RunnerRpcError
|
|
279
|
+
}), make5("ResumeCheckpoint", {
|
|
280
|
+
payload: { lease: ProgramLease },
|
|
281
|
+
success: UndefinedOr(StepCheckpoint),
|
|
282
|
+
error: RunnerRpcError
|
|
283
|
+
}), make5("SettledCheckpoints", {
|
|
284
|
+
success: ArraySchema(Struct({
|
|
285
|
+
stepAttemptId: String2,
|
|
286
|
+
ref: optionalKey(String2),
|
|
287
|
+
repoBinding: optionalKey(WorkUnitRepoBinding)
|
|
288
|
+
})),
|
|
289
|
+
error: RunnerRpcError
|
|
290
|
+
}), make5("ForgetCheckpoints", {
|
|
291
|
+
payload: { stepAttemptIds: ArraySchema(String2) },
|
|
292
|
+
success: Void,
|
|
293
|
+
error: RunnerRpcError
|
|
294
|
+
}), make5("Bundle", {
|
|
295
|
+
payload: { digest: String2 },
|
|
296
|
+
success: Struct({ bundle: String2 }),
|
|
297
|
+
error: RunnerRpcError
|
|
298
|
+
}), make5("SettledRuns", {
|
|
299
|
+
payload: { runIds: ArraySchema(String2) },
|
|
300
|
+
success: ArraySchema(String2),
|
|
301
|
+
error: RunnerRpcError
|
|
302
|
+
}), make5("ProgramSecrets", {
|
|
303
|
+
payload: { lease: ProgramLease },
|
|
304
|
+
success: Record(String2, String2),
|
|
305
|
+
error: RunnerRpcError
|
|
306
|
+
}), make5("Suspend", { payload: { lease: ProgramLease }, error: RunnerRpcError }), make5("FinishProgram", { payload: { lease: ProgramLease, outcome: ProgramOutcome }, error: RunnerRpcError }), make5("Signals", {
|
|
307
|
+
payload: { runnerReplicaId: String2 },
|
|
308
|
+
success: RunnerSignal,
|
|
309
|
+
error: RunnerRpcError,
|
|
310
|
+
stream: true
|
|
311
|
+
})).middleware(RunnerAuthentication) {
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// packages/live/src/rpc/runner-client.ts
|
|
315
|
+
var runnerWebSocketProtocol2 = (url) => layerProtocolSocket({ retryTransientErrors: true }).pipe(provide2(layerWebSocket(url)), provide2(layerWebSocketConstructorGlobal), provide2(layerJson));
|
|
316
|
+
function runnerRpcUrl2(apiUrl) {
|
|
317
|
+
const url = new URL("/v1/runners/rpc", apiUrl);
|
|
318
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
319
|
+
return url.toString();
|
|
320
|
+
}
|
|
321
|
+
var makeRunnerRpcClient2 = (options) => gen(function* () {
|
|
322
|
+
const protocol = yield* build2(options.protocol);
|
|
323
|
+
const client = yield* make7(RunnerRpcs).pipe(provideContext(protocol));
|
|
324
|
+
const authorized = (call) => flatMap(options.bearer, (token) => withHeaders(call, {
|
|
325
|
+
authorization: `Bearer ${token}`,
|
|
326
|
+
[RunnerHeaders.runnerId]: options.runnerId,
|
|
327
|
+
[RunnerHeaders.protocolVersion]: String(RunnerRpcProtocolVersion)
|
|
328
|
+
}));
|
|
329
|
+
return {
|
|
330
|
+
claim: (input) => authorized(client.Claim(input)),
|
|
331
|
+
heartbeat: (input) => authorized(client.Heartbeat(input)),
|
|
332
|
+
complete: (input) => authorized(client.Complete(input)),
|
|
333
|
+
release: (input) => authorized(client.Release(input)),
|
|
334
|
+
recordActivity: (input) => authorized(client.RecordActivity(input)),
|
|
335
|
+
deferredDone: (input) => authorized(client.DeferredDone(input)),
|
|
336
|
+
scheduleClock: (input) => authorized(client.ScheduleClock(input)),
|
|
337
|
+
putQuestion: (input) => authorized(client.PutQuestion(input)),
|
|
338
|
+
programSecrets: (input) => authorized(client.ProgramSecrets(input)),
|
|
339
|
+
suspend: (input) => authorized(client.Suspend(input)),
|
|
340
|
+
finishProgram: (input) => authorized(client.FinishProgram(input)),
|
|
341
|
+
startAgentStep: (input) => authorized(client.StartAgentStep(input)),
|
|
342
|
+
startVerification: (input) => authorized(client.StartVerification(input)),
|
|
343
|
+
knownEvidence: (input) => authorized(client.KnownEvidence(input)),
|
|
344
|
+
awaitEvent: (input) => authorized(client.AwaitEvent(input)),
|
|
345
|
+
admitChild: (input) => authorized(client.AdmitChild(input)),
|
|
346
|
+
storeArtifact: (input) => authorized(client.StoreArtifact(input)),
|
|
347
|
+
recordNote: (input) => authorized(client.RecordNote(input)),
|
|
348
|
+
performHostEffect: (input) => authorized(client.PerformHostEffect(input)),
|
|
349
|
+
createPlanTasks: (input) => authorized(client.CreatePlanTasks(input)),
|
|
350
|
+
integrationBranch: (input) => authorized(client.IntegrationBranch(input)),
|
|
351
|
+
mergeChangeRequest: (input) => authorized(client.MergeChangeRequest(input)),
|
|
352
|
+
publishRelease: (input) => authorized(client.PublishRelease(input)),
|
|
353
|
+
bundle: (digest) => authorized(client.Bundle({ digest })),
|
|
354
|
+
settledRuns: (runIds) => authorized(client.SettledRuns({ runIds })),
|
|
355
|
+
reportSteer: (input) => authorized(client.ReportSteer(input)),
|
|
356
|
+
saveCheckpoint: (input) => authorized(client.SaveCheckpoint(input)),
|
|
357
|
+
resumeCheckpoint: (input) => authorized(client.ResumeCheckpoint(input)),
|
|
358
|
+
settledCheckpoints: authorized(client.SettledCheckpoints()),
|
|
359
|
+
forgetCheckpoints: (stepAttemptIds) => authorized(client.ForgetCheckpoints({ stepAttemptIds })),
|
|
360
|
+
listen: (runnerReplicaId, onSignal) => authorized(runForEach(client.Signals({ runnerReplicaId }), onSignal))
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
export { RunnerRpcProtocolVersion, RunnerRpcError, RunSettled, runnerWebSocketProtocol2, runnerRpcUrl2, makeRunnerRpcClient2 };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Runs alone inside the egress proxy container, so it imports nothing outside Node.
|
|
2
|
+
import { once } from 'node:events'
|
|
3
|
+
import { connect, createServer, type Server, type Socket } from 'node:net'
|
|
4
|
+
|
|
5
|
+
/** Whether the allowlist takes `host:port`: an entry `host` takes any port, `host:port` one, `*.domain` subdomains. */
|
|
6
|
+
export const egressAllowed = (hosts: readonly string[], host: string, port: number) => {
|
|
7
|
+
const target = host.toLowerCase()
|
|
8
|
+
|
|
9
|
+
return hosts.some(entry => {
|
|
10
|
+
const [name = '', entryPort] = entry.toLowerCase().split(':')
|
|
11
|
+
|
|
12
|
+
if (entryPort !== undefined && Number(entryPort) !== port) return false
|
|
13
|
+
|
|
14
|
+
return name.startsWith('*.') ? target.endsWith(name.slice(1)) : target === name
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const connectTarget = (authority: string) => {
|
|
19
|
+
const separator = authority.lastIndexOf(':')
|
|
20
|
+
const port = Number(authority.slice(separator + 1))
|
|
21
|
+
|
|
22
|
+
return separator === -1 || !Number.isInteger(port)
|
|
23
|
+
? undefined
|
|
24
|
+
: { host: authority.slice(0, separator).replace(/^\[|\]$/g, ''), port }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const httpTarget = (absolute: string) => {
|
|
28
|
+
if (!URL.canParse(absolute)) return undefined
|
|
29
|
+
const url = new URL(absolute)
|
|
30
|
+
|
|
31
|
+
return url.protocol !== 'http:'
|
|
32
|
+
? undefined
|
|
33
|
+
: {
|
|
34
|
+
host: url.hostname.replace(/^\[|\]$/g, ''),
|
|
35
|
+
port: url.port === '' ? 80 : Number(url.port),
|
|
36
|
+
path: `${url.pathname}${url.search}`
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const answer = (socket: Socket, status: string) => socket.end(`HTTP/1.1 ${status}\r\nContent-Length: 0\r\n\r\n`)
|
|
41
|
+
|
|
42
|
+
const handle = (hosts: readonly string[]) => (client: Socket) => {
|
|
43
|
+
let head = Buffer.alloc(0)
|
|
44
|
+
|
|
45
|
+
const onData = (chunk: Buffer) => {
|
|
46
|
+
head = Buffer.concat([head, chunk])
|
|
47
|
+
const end = head.indexOf('\r\n\r\n')
|
|
48
|
+
|
|
49
|
+
if (end === -1) return head.length > 16_384 ? answer(client, '431 Request Header Fields Too Large') : undefined
|
|
50
|
+
client.off('data', onData)
|
|
51
|
+
|
|
52
|
+
const [requestLine = '', ...headers] = head.subarray(0, end).toString().split('\r\n')
|
|
53
|
+
const [method = '', target = '', version = 'HTTP/1.1'] = requestLine.split(' ')
|
|
54
|
+
const destination = method === 'CONNECT' ? connectTarget(target) : httpTarget(target)
|
|
55
|
+
|
|
56
|
+
if (destination === undefined) return answer(client, '405 Method Not Allowed')
|
|
57
|
+
|
|
58
|
+
if (!egressAllowed(hosts, destination.host, destination.port)) return answer(client, '403 Forbidden')
|
|
59
|
+
|
|
60
|
+
const upstream = connect(destination.port, destination.host, () => {
|
|
61
|
+
if (!('path' in destination)) {
|
|
62
|
+
client.write('HTTP/1.1 200 Connection Established\r\n\r\n')
|
|
63
|
+
upstream.write(head.subarray(end + 4))
|
|
64
|
+
} else {
|
|
65
|
+
// One request per connection: a reused one could carry a request for another host to this one.
|
|
66
|
+
const kept = headers.filter(line => !/^(proxy-)?connection:/i.test(line))
|
|
67
|
+
|
|
68
|
+
upstream.write(
|
|
69
|
+
Buffer.concat([
|
|
70
|
+
Buffer.from(
|
|
71
|
+
[[method, destination.path, version].join(' '), ...kept, 'Connection: close', '', ''].join('\r\n')
|
|
72
|
+
),
|
|
73
|
+
head.subarray(end + 4)
|
|
74
|
+
])
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
client.pipe(upstream)
|
|
79
|
+
upstream.pipe(client)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
upstream.on('error', () => (client.writable ? answer(client, '502 Bad Gateway') : client.destroy()))
|
|
83
|
+
client.on('error', () => upstream.destroy())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
client.on('data', onData)
|
|
87
|
+
client.on('error', () => client.destroy())
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** An HTTP proxy that tunnels CONNECT and forwards plain HTTP only to the allowlist. */
|
|
91
|
+
export const startEgressProxy = (input: {
|
|
92
|
+
readonly hosts: readonly string[]
|
|
93
|
+
readonly port: number
|
|
94
|
+
readonly hostname: string
|
|
95
|
+
}): Promise<Server> => {
|
|
96
|
+
const server = createServer(handle(input.hosts)).listen(input.port, input.hostname)
|
|
97
|
+
|
|
98
|
+
return once(server, 'listening').then(() => server)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// `bun egress-proxy.ts <comma-separated allowlist>`
|
|
102
|
+
if (import.meta.main)
|
|
103
|
+
void startEgressProxy({
|
|
104
|
+
hosts: (Bun.argv[2] ?? '').split(',').filter(host => host.length > 0),
|
|
105
|
+
port: 3128,
|
|
106
|
+
hostname: '0.0.0.0'
|
|
107
|
+
})
|