agim-cli 1.2.142 → 1.2.144
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/CHANGELOG.md +142 -0
- package/dist/cli-ui/tui/app.d.ts +1 -0
- package/dist/cli-ui/tui/app.d.ts.map +1 -1
- package/dist/cli-ui/tui/app.js +24 -9
- package/dist/cli-ui/tui/app.js.map +1 -1
- package/dist/cli-ui/tui/markdown.d.ts.map +1 -1
- package/dist/cli-ui/tui/markdown.js +12 -3
- package/dist/cli-ui/tui/markdown.js.map +1 -1
- package/dist/cli.js +23 -3
- package/dist/cli.js.map +1 -1
- package/dist/core/access-token.d.ts.map +1 -1
- package/dist/core/access-token.js +4 -2
- package/dist/core/access-token.js.map +1 -1
- package/dist/plugins/agents/codex/index.d.ts +19 -0
- package/dist/plugins/agents/codex/index.d.ts.map +1 -1
- package/dist/plugins/agents/codex/index.js +50 -0
- package/dist/plugins/agents/codex/index.js.map +1 -1
- package/dist/web/server.d.ts +23 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +103 -20
- package/dist/web/server.js.map +1 -1
- package/package.json +3 -2
package/dist/web/server.d.ts
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
import { type IncomingMessage } from 'node:http';
|
|
2
|
+
declare function isTrustedLoopbackPeer(req: IncomingMessage): boolean;
|
|
3
|
+
/** Resolve whether the request's actor has admin role. Used to gate
|
|
4
|
+
* mutation + privileged-read endpoints so a stolen viewer-role token
|
|
5
|
+
* can't elevate to control plane (R13 A1).
|
|
6
|
+
*
|
|
7
|
+
* Trust order:
|
|
8
|
+
* 1. IMHUB_WEB_AUTH=off → admin (operator explicitly disabled auth)
|
|
9
|
+
* 2. Trusted loopback → admin (operator on the host)
|
|
10
|
+
* 3. Bearer token → token.role === 'admin'
|
|
11
|
+
* 4. Otherwise → not admin
|
|
12
|
+
*
|
|
13
|
+
* Note: when no token has been created yet (pre-bootstrap), the
|
|
14
|
+
* trusted-loopback branch still grants admin so the CLI bootstrap flow
|
|
15
|
+
* works. Disable it with IMHUB_TRUST_LOOPBACK=off. Reverse-proxied
|
|
16
|
+
* requests with Forwarded / X-Forwarded-* peer headers never qualify. */
|
|
17
|
+
declare function isRequestAdmin(req: IncomingMessage): boolean;
|
|
18
|
+
export declare const __webAuthForTesting: {
|
|
19
|
+
isTrustedLoopbackPeer: typeof isTrustedLoopbackPeer;
|
|
20
|
+
isRequestAdmin: typeof isRequestAdmin;
|
|
21
|
+
setTokenModule(mod: typeof import("../core/access-token.js") | null): void;
|
|
22
|
+
};
|
|
1
23
|
export declare function createSerialQueue(): (fn: () => Promise<void>) => void;
|
|
2
24
|
/**
|
|
3
25
|
* Start the web chat server
|
|
@@ -9,4 +31,5 @@ export declare function startWebServer(options: {
|
|
|
9
31
|
close: () => void;
|
|
10
32
|
port: number;
|
|
11
33
|
}>;
|
|
34
|
+
export {};
|
|
12
35
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/web/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAAgB,KAAK,eAAe,EAAuB,MAAM,WAAW,CAAA;AA4GnF,iBAAS,qBAAqB,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAQ5D;AAyID;;;;;;;;;;;;;0EAa0E;AAC1E,iBAAS,cAAc,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAcrD;AAWD,eAAO,MAAM,mBAAmB;;;wBAGV,cAAc,yBAAyB,CAAC,GAAG,IAAI,GAAG,IAAI;CAG3E,CAAA;AAED,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAkvC/C"}
|
package/dist/web/server.js
CHANGED
|
@@ -75,6 +75,29 @@ function isLoopbackPeer(req) {
|
|
|
75
75
|
const ip = (req.socket.remoteAddress || '').replace(/^::ffff:/, '');
|
|
76
76
|
return ip === '127.0.0.1' || ip === '::1';
|
|
77
77
|
}
|
|
78
|
+
function isEnvOff(name) {
|
|
79
|
+
const v = (process.env[name] || '').trim().toLowerCase();
|
|
80
|
+
return v === 'off' || v === '0' || v === 'false' || v === 'no';
|
|
81
|
+
}
|
|
82
|
+
function hasForwardedPeerHeaders(req) {
|
|
83
|
+
return req.headers.forwarded !== undefined
|
|
84
|
+
|| req.headers['x-forwarded-for'] !== undefined
|
|
85
|
+
|| req.headers['x-forwarded-host'] !== undefined
|
|
86
|
+
|| req.headers['x-real-ip'] !== undefined
|
|
87
|
+
|| req.headers['cf-connecting-ip'] !== undefined;
|
|
88
|
+
}
|
|
89
|
+
function isTrustedLoopbackPeer(req) {
|
|
90
|
+
if (!isLoopbackPeer(req))
|
|
91
|
+
return false;
|
|
92
|
+
if (isEnvOff('IMHUB_TRUST_LOOPBACK'))
|
|
93
|
+
return false;
|
|
94
|
+
// A reverse proxy on the same host makes remote users appear as
|
|
95
|
+
// 127.0.0.1. Treat forwarded requests as network traffic and require
|
|
96
|
+
// the normal token path instead of granting the local bootstrap bypass.
|
|
97
|
+
if (hasForwardedPeerHeaders(req))
|
|
98
|
+
return false;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
78
101
|
/** R13 A5 — track once-per-process whether the deprecated `?token=`
|
|
79
102
|
* URL fallback has been used, so we warn at most once per service
|
|
80
103
|
* lifetime instead of spamming the journal. Cleared by tests via
|
|
@@ -142,8 +165,8 @@ function checkAuth(req, res, url) {
|
|
|
142
165
|
// 1. Disabled by env → pass through.
|
|
143
166
|
if ((process.env.IMHUB_WEB_AUTH || '').toLowerCase() === 'off')
|
|
144
167
|
return true;
|
|
145
|
-
// 2.
|
|
146
|
-
if (
|
|
168
|
+
// 2. Trusted loopback → pass through (local CLI / browser-on-same-host).
|
|
169
|
+
if (isTrustedLoopbackPeer(req))
|
|
147
170
|
return true;
|
|
148
171
|
// 3. Public-by-design path → pass through.
|
|
149
172
|
if (isPublicPath(url.pathname, req.method || 'GET'))
|
|
@@ -201,7 +224,7 @@ function verifyTokenSync(raw) {
|
|
|
201
224
|
function getRequestActor(req) {
|
|
202
225
|
if ((process.env.IMHUB_WEB_AUTH || '').toLowerCase() === 'off')
|
|
203
226
|
return 'web:auth-off';
|
|
204
|
-
if (
|
|
227
|
+
if (isTrustedLoopbackPeer(req))
|
|
205
228
|
return 'web:loopback';
|
|
206
229
|
let url;
|
|
207
230
|
try {
|
|
@@ -224,17 +247,18 @@ function getRequestActor(req) {
|
|
|
224
247
|
*
|
|
225
248
|
* Trust order:
|
|
226
249
|
* 1. IMHUB_WEB_AUTH=off → admin (operator explicitly disabled auth)
|
|
227
|
-
* 2.
|
|
250
|
+
* 2. Trusted loopback → admin (operator on the host)
|
|
228
251
|
* 3. Bearer token → token.role === 'admin'
|
|
229
252
|
* 4. Otherwise → not admin
|
|
230
253
|
*
|
|
231
254
|
* Note: when no token has been created yet (pre-bootstrap), the
|
|
232
|
-
* loopback
|
|
233
|
-
* works.
|
|
255
|
+
* trusted-loopback branch still grants admin so the CLI bootstrap flow
|
|
256
|
+
* works. Disable it with IMHUB_TRUST_LOOPBACK=off. Reverse-proxied
|
|
257
|
+
* requests with Forwarded / X-Forwarded-* peer headers never qualify. */
|
|
234
258
|
function isRequestAdmin(req) {
|
|
235
259
|
if ((process.env.IMHUB_WEB_AUTH || '').toLowerCase() === 'off')
|
|
236
260
|
return true;
|
|
237
|
-
if (
|
|
261
|
+
if (isTrustedLoopbackPeer(req))
|
|
238
262
|
return true;
|
|
239
263
|
let url;
|
|
240
264
|
try {
|
|
@@ -265,6 +289,13 @@ function requireAdmin(req, res) {
|
|
|
265
289
|
res.end(JSON.stringify({ error: 'forbidden', message: 'admin role required' }));
|
|
266
290
|
return false;
|
|
267
291
|
}
|
|
292
|
+
export const __webAuthForTesting = {
|
|
293
|
+
isTrustedLoopbackPeer,
|
|
294
|
+
isRequestAdmin,
|
|
295
|
+
setTokenModule(mod) {
|
|
296
|
+
_tokenModule = mod;
|
|
297
|
+
},
|
|
298
|
+
};
|
|
268
299
|
export function createSerialQueue() {
|
|
269
300
|
let queue = Promise.resolve();
|
|
270
301
|
return (fn) => {
|
|
@@ -357,6 +388,7 @@ export async function startWebServer(options) {
|
|
|
357
388
|
event: 'web.auth_mode',
|
|
358
389
|
bind: bindHost,
|
|
359
390
|
enabled: isAuthEnabled(),
|
|
391
|
+
trustLoopback: !isEnvOff('IMHUB_TRUST_LOOPBACK'),
|
|
360
392
|
}, `Web console auth: ${isAuthEnabled() ? 'token-gated' : 'disabled (IMHUB_WEB_AUTH=off)'}`);
|
|
361
393
|
// HTTP request handler — static files + REST API
|
|
362
394
|
const httpServer = createServer(async (req, res) => {
|
|
@@ -643,21 +675,31 @@ export async function startWebServer(options) {
|
|
|
643
675
|
}
|
|
644
676
|
// Jobs
|
|
645
677
|
if (url.pathname === '/api/jobs' && req.method === 'GET') {
|
|
678
|
+
if (!requireAdmin(req, res))
|
|
679
|
+
return;
|
|
646
680
|
return handleListJobs(req, res, url);
|
|
647
681
|
}
|
|
648
682
|
const jobIdMatch = url.pathname.match(/^\/api\/jobs\/(\d+)$/);
|
|
649
683
|
if (jobIdMatch && req.method === 'GET') {
|
|
684
|
+
if (!requireAdmin(req, res))
|
|
685
|
+
return;
|
|
650
686
|
return handleGetJob(req, res, parseInt(jobIdMatch[1], 10));
|
|
651
687
|
}
|
|
652
688
|
const jobCancelMatch = url.pathname.match(/^\/api\/jobs\/(\d+)\/cancel$/);
|
|
653
689
|
if (jobCancelMatch && req.method === 'POST') {
|
|
690
|
+
if (!requireAdmin(req, res))
|
|
691
|
+
return;
|
|
654
692
|
return handleCancelJob(req, res, parseInt(jobCancelMatch[1], 10));
|
|
655
693
|
}
|
|
656
694
|
const jobRunMatch = url.pathname.match(/^\/api\/jobs\/(\d+)\/run$/);
|
|
657
695
|
if (jobRunMatch && req.method === 'POST') {
|
|
696
|
+
if (!requireAdmin(req, res))
|
|
697
|
+
return;
|
|
658
698
|
return handleRunJob(req, res, parseInt(jobRunMatch[1], 10));
|
|
659
699
|
}
|
|
660
700
|
if (url.pathname === '/api/jobs' && req.method === 'POST') {
|
|
701
|
+
if (!requireAdmin(req, res))
|
|
702
|
+
return;
|
|
661
703
|
return handleCreateJob(req, res);
|
|
662
704
|
}
|
|
663
705
|
// bgjobs (read-only view of ~/.claude/bgjobs, ~/.config/opencode/bgjobs, ~/.codex/bgjobs)
|
|
@@ -677,34 +719,46 @@ export async function startWebServer(options) {
|
|
|
677
719
|
return handleListSchedules(req, res, url);
|
|
678
720
|
}
|
|
679
721
|
// Reminders — list / cancel / snooze. Web-only path (the IM-side path
|
|
680
|
-
// is /remind slash command).
|
|
681
|
-
//
|
|
682
|
-
// single-operator deployments only.
|
|
722
|
+
// is /remind slash command). This exposes global reminders, so it is
|
|
723
|
+
// admin-only until a per-user mobile scope exists.
|
|
683
724
|
if (url.pathname === '/api/reminders' && req.method === 'GET') {
|
|
725
|
+
if (!requireAdmin(req, res))
|
|
726
|
+
return;
|
|
684
727
|
return handleListReminders(req, res, url);
|
|
685
728
|
}
|
|
686
729
|
const reminderCancelMatch = url.pathname.match(/^\/api\/reminders\/(\d+)\/cancel$/);
|
|
687
730
|
if (reminderCancelMatch && req.method === 'POST') {
|
|
731
|
+
if (!requireAdmin(req, res))
|
|
732
|
+
return;
|
|
688
733
|
return handleCancelReminderApi(req, res, Number.parseInt(reminderCancelMatch[1], 10));
|
|
689
734
|
}
|
|
690
735
|
const reminderSnoozeMatch = url.pathname.match(/^\/api\/reminders\/(\d+)\/snooze$/);
|
|
691
736
|
if (reminderSnoozeMatch && req.method === 'POST') {
|
|
737
|
+
if (!requireAdmin(req, res))
|
|
738
|
+
return;
|
|
692
739
|
return handleSnoozeReminderApi(req, res, Number.parseInt(reminderSnoozeMatch[1], 10));
|
|
693
740
|
}
|
|
694
741
|
// /api/memos — search / list / delete. List uses the same searchMemos
|
|
695
742
|
// function the MCP tool exposes; query/who/what/has_location/limit
|
|
696
743
|
// come through as URL params.
|
|
697
744
|
if (url.pathname === '/api/memos' && req.method === 'GET') {
|
|
745
|
+
if (!requireAdmin(req, res))
|
|
746
|
+
return;
|
|
698
747
|
return handleListMemos(req, res, url);
|
|
699
748
|
}
|
|
700
749
|
const memoIdMatch = url.pathname.match(/^\/api\/memos\/(\d+)$/);
|
|
701
750
|
if (memoIdMatch && req.method === 'DELETE') {
|
|
751
|
+
if (!requireAdmin(req, res))
|
|
752
|
+
return;
|
|
702
753
|
return handleDeleteMemo(req, res, Number.parseInt(memoIdMatch[1], 10));
|
|
703
754
|
}
|
|
704
755
|
// /api/env — read/write SMTP + Baidu AK + IMHUB_WEB_BIND. Values
|
|
705
756
|
// sensitive enough that GET returns them masked (only the last 4 chars
|
|
706
|
-
// visible) unless an explicit ?reveal=1 is passed
|
|
757
|
+
// visible) unless an explicit ?reveal=1 is passed. Keep the settings
|
|
758
|
+
// surface admin-only: even masked values disclose configured providers.
|
|
707
759
|
if (url.pathname === '/api/env' && req.method === 'GET') {
|
|
760
|
+
if (!requireAdmin(req, res))
|
|
761
|
+
return;
|
|
708
762
|
return handleGetEnv(req, res, url);
|
|
709
763
|
}
|
|
710
764
|
if (url.pathname === '/api/env' && req.method === 'PUT') {
|
|
@@ -866,9 +920,13 @@ export async function startWebServer(options) {
|
|
|
866
920
|
// v1.5 — Memory admin: enumerate users, list / delete facts, view /
|
|
867
921
|
// edit / delete persona, export. Backs the Memory tab in /tasks.
|
|
868
922
|
if (url.pathname === '/api/memory/users' && req.method === 'GET') {
|
|
923
|
+
if (!requireAdmin(req, res))
|
|
924
|
+
return;
|
|
869
925
|
return handleMemoryUsers(req, res);
|
|
870
926
|
}
|
|
871
927
|
if (url.pathname === '/api/memory/facts' && req.method === 'GET') {
|
|
928
|
+
if (!requireAdmin(req, res))
|
|
929
|
+
return;
|
|
872
930
|
return handleMemoryFacts(req, res, url);
|
|
873
931
|
}
|
|
874
932
|
if (url.pathname === '/api/memory/facts' && req.method === 'DELETE') {
|
|
@@ -878,9 +936,13 @@ export async function startWebServer(options) {
|
|
|
878
936
|
}
|
|
879
937
|
const memFactIdMatch = url.pathname.match(/^\/api\/memory\/facts\/(\d+)$/);
|
|
880
938
|
if (memFactIdMatch && req.method === 'DELETE') {
|
|
939
|
+
if (!requireAdmin(req, res))
|
|
940
|
+
return;
|
|
881
941
|
return handleMemoryDeleteOne(req, res, url, parseInt(memFactIdMatch[1], 10));
|
|
882
942
|
}
|
|
883
943
|
if (url.pathname === '/api/memory/persona' && req.method === 'GET') {
|
|
944
|
+
if (!requireAdmin(req, res))
|
|
945
|
+
return;
|
|
884
946
|
return handleMemoryPersona(req, res, url);
|
|
885
947
|
}
|
|
886
948
|
if (url.pathname === '/api/memory/persona' && req.method === 'PUT') {
|
|
@@ -894,6 +956,8 @@ export async function startWebServer(options) {
|
|
|
894
956
|
return handleMemoryPersonaDelete(req, res, url);
|
|
895
957
|
}
|
|
896
958
|
if (url.pathname === '/api/memory/export' && req.method === 'GET') {
|
|
959
|
+
if (!requireAdmin(req, res))
|
|
960
|
+
return;
|
|
897
961
|
return handleMemoryExport(req, res, url);
|
|
898
962
|
}
|
|
899
963
|
// v1.6 — vector backend control + index ops.
|
|
@@ -928,6 +992,8 @@ export async function startWebServer(options) {
|
|
|
928
992
|
return handleMemoryConsolidate(req, res);
|
|
929
993
|
}
|
|
930
994
|
if (url.pathname === '/api/memory/consolidate/status' && req.method === 'GET') {
|
|
995
|
+
if (!requireAdmin(req, res))
|
|
996
|
+
return;
|
|
931
997
|
return handleMemoryConsolidateStatus(req, res);
|
|
932
998
|
}
|
|
933
999
|
// v1.2.3 — Skills browser. Lists locally-installed claude/opencode
|
|
@@ -944,10 +1010,14 @@ export async function startWebServer(options) {
|
|
|
944
1010
|
}
|
|
945
1011
|
// PR-B: HITL approvals — global pending list + per-reqId resolve.
|
|
946
1012
|
if (url.pathname === '/api/approvals' && req.method === 'GET') {
|
|
1013
|
+
if (!requireAdmin(req, res))
|
|
1014
|
+
return;
|
|
947
1015
|
return handleListApprovals(req, res);
|
|
948
1016
|
}
|
|
949
1017
|
const approvalResolveMatch = url.pathname.match(/^\/api\/approvals\/([^/]+)\/resolve$/);
|
|
950
1018
|
if (approvalResolveMatch && req.method === 'POST') {
|
|
1019
|
+
if (!requireAdmin(req, res))
|
|
1020
|
+
return;
|
|
951
1021
|
return handleResolveApproval(req, res, approvalResolveMatch[1]);
|
|
952
1022
|
}
|
|
953
1023
|
// PR-D: Agent workspace file browser. Read-only inspection of
|
|
@@ -955,6 +1025,8 @@ export async function startWebServer(options) {
|
|
|
955
1025
|
// text files. PUT path supports inline editing (annotate CLAUDE.md,
|
|
956
1026
|
// AGENTS.md, etc.) — same traversal/size guards as GET.
|
|
957
1027
|
if (url.pathname === '/api/workspace-files' && req.method === 'GET') {
|
|
1028
|
+
if (!requireAdmin(req, res))
|
|
1029
|
+
return;
|
|
958
1030
|
return handleWorkspaceFiles(req, res, url);
|
|
959
1031
|
}
|
|
960
1032
|
if (url.pathname === '/api/workspace-files' && req.method === 'PUT') {
|
|
@@ -976,9 +1048,12 @@ export async function startWebServer(options) {
|
|
|
976
1048
|
return handleBatchJob(req, res, 'run', getDefaultAgent(options.defaultAgent));
|
|
977
1049
|
}
|
|
978
1050
|
// PR-C: SSE event stream — audit / approval / job / metrics events
|
|
979
|
-
// pushed real-time so the dashboard stops polling.
|
|
980
|
-
//
|
|
1051
|
+
// pushed real-time so the dashboard stops polling. Global control-plane
|
|
1052
|
+
// telemetry is admin-only; user/mobile-scoped streams should use a
|
|
1053
|
+
// separate endpoint when added.
|
|
981
1054
|
if (url.pathname === '/events' && req.method === 'GET') {
|
|
1055
|
+
if (!requireAdmin(req, res))
|
|
1056
|
+
return;
|
|
982
1057
|
return handleEventsSSE(req, res);
|
|
983
1058
|
}
|
|
984
1059
|
if (url.pathname === '/api/notify' && req.method === 'POST') {
|
|
@@ -1110,11 +1185,11 @@ export async function startWebServer(options) {
|
|
|
1110
1185
|
}, 'WS upgrade refused (per-IP rate limit)');
|
|
1111
1186
|
return cb(false, 429, 'rate limited');
|
|
1112
1187
|
}
|
|
1113
|
-
// Auth-off / loopback bypass — mirror checkAuth's two short-circuits
|
|
1188
|
+
// Auth-off / trusted-loopback bypass — mirror checkAuth's two short-circuits
|
|
1114
1189
|
// so dev / local CLI sessions still work without a token.
|
|
1115
1190
|
if ((process.env.IMHUB_WEB_AUTH || '').toLowerCase() === 'off')
|
|
1116
1191
|
return cb(true);
|
|
1117
|
-
if (
|
|
1192
|
+
if (isTrustedLoopbackPeer(info.req))
|
|
1118
1193
|
return cb(true);
|
|
1119
1194
|
// Origin check: cookie SameSite=Lax handles most of the CSWSH
|
|
1120
1195
|
// surface, but defence-in-depth — reject when Origin's host
|
|
@@ -1168,8 +1243,9 @@ export async function startWebServer(options) {
|
|
|
1168
1243
|
// IMHUB_WS_MAX_PER_IP active connections per IP (default 20)
|
|
1169
1244
|
// IMHUB_WS_MAX_NEW_PER_IP_PER_MIN new connections per IP per minute (default 30)
|
|
1170
1245
|
//
|
|
1171
|
-
//
|
|
1172
|
-
// short connections legitimately.
|
|
1246
|
+
// Trusted loopback bypasses both — local dev / CLI tooling makes many
|
|
1247
|
+
// short connections legitimately. Reverse-proxied loopback traffic is
|
|
1248
|
+
// still counted as network traffic.
|
|
1173
1249
|
const wsMaxPerIp = (() => {
|
|
1174
1250
|
const raw = process.env.IMHUB_WS_MAX_PER_IP;
|
|
1175
1251
|
if (raw) {
|
|
@@ -1198,7 +1274,7 @@ export async function startWebServer(options) {
|
|
|
1198
1274
|
}
|
|
1199
1275
|
/** Returns {ok:true} when the IP may open a new WS, else {ok:false, reason}. */
|
|
1200
1276
|
function checkWsIpRateLimit(req) {
|
|
1201
|
-
if (
|
|
1277
|
+
if (isTrustedLoopbackPeer(req))
|
|
1202
1278
|
return { ok: true };
|
|
1203
1279
|
const ip = peerIp(req);
|
|
1204
1280
|
if (!ip)
|
|
@@ -1216,7 +1292,7 @@ export async function startWebServer(options) {
|
|
|
1216
1292
|
return { ok: true };
|
|
1217
1293
|
}
|
|
1218
1294
|
function recordWsIpOpen(req) {
|
|
1219
|
-
if (
|
|
1295
|
+
if (isTrustedLoopbackPeer(req))
|
|
1220
1296
|
return;
|
|
1221
1297
|
const ip = peerIp(req);
|
|
1222
1298
|
if (!ip)
|
|
@@ -1227,7 +1303,7 @@ export async function startWebServer(options) {
|
|
|
1227
1303
|
wsPerIp.set(ip, slot);
|
|
1228
1304
|
}
|
|
1229
1305
|
function recordWsIpClose(req) {
|
|
1230
|
-
if (
|
|
1306
|
+
if (isTrustedLoopbackPeer(req))
|
|
1231
1307
|
return;
|
|
1232
1308
|
const ip = peerIp(req);
|
|
1233
1309
|
if (!ip)
|
|
@@ -4655,6 +4731,13 @@ function readBody(req, res) {
|
|
|
4655
4731
|
aborted = true;
|
|
4656
4732
|
if (res && !res.headersSent) {
|
|
4657
4733
|
sendJson(res, 413, { error: 'Request body too large' });
|
|
4734
|
+
res.once('finish', () => {
|
|
4735
|
+
if (!req.destroyed)
|
|
4736
|
+
req.destroy();
|
|
4737
|
+
});
|
|
4738
|
+
}
|
|
4739
|
+
else if (!req.destroyed) {
|
|
4740
|
+
req.destroy();
|
|
4658
4741
|
}
|
|
4659
4742
|
const err = new Error('Request body too large');
|
|
4660
4743
|
err.statusCode = 413;
|