openclaw-groupme 0.0.4 → 0.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/LICENSE +21 -0
- package/README.md +348 -70
- package/package.json +36 -11
- package/src/accounts.ts +51 -23
- package/src/channel.ts +154 -16
- package/src/config-schema.ts +73 -3
- package/src/groupme-api.ts +98 -0
- package/src/history.ts +54 -0
- package/src/inbound.ts +128 -23
- package/src/monitor.ts +275 -33
- package/src/normalize.ts +1 -9
- package/src/onboarding.ts +136 -36
- package/src/parse.ts +32 -33
- package/src/policy.ts +5 -2
- package/src/rate-limit.ts +128 -0
- package/src/replay-cache.ts +71 -0
- package/src/security.ts +460 -0
- package/src/send.ts +237 -51
- package/src/types.ts +98 -1
- package/.github/workflows/publish-npm.yml +0 -30
- package/openclaw.plugin.json +0 -9
- package/src/monitor.test.ts +0 -186
- package/src/normalize.test.ts +0 -43
- package/src/parse.test.ts +0 -162
- package/src/policy.test.ts +0 -23
- package/src/send.test.ts +0 -153
package/src/security.ts
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import type { IncomingHttpHeaders } from "node:http";
|
|
3
|
+
import { BlockList, isIP } from "node:net";
|
|
4
|
+
import { readTrimmed } from "./accounts.js";
|
|
5
|
+
import type {
|
|
6
|
+
CallbackAuthResult,
|
|
7
|
+
GroupMeAccountConfig,
|
|
8
|
+
GroupMeSecurityConfig,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
type ProxyRule = {
|
|
12
|
+
kind: "cidr" | "ip";
|
|
13
|
+
value: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const IPV4_MAX_CIDR_PREFIX = 32;
|
|
17
|
+
const IPV6_MAX_CIDR_PREFIX = 128;
|
|
18
|
+
|
|
19
|
+
export type ResolvedGroupMeSecurity = {
|
|
20
|
+
callbackToken: string;
|
|
21
|
+
callbackRejectStatus: 404;
|
|
22
|
+
groupId: string;
|
|
23
|
+
replay: {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
ttlSeconds: number;
|
|
26
|
+
maxEntries: number;
|
|
27
|
+
};
|
|
28
|
+
rateLimit: {
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
windowMs: number;
|
|
31
|
+
maxRequestsPerIp: number;
|
|
32
|
+
maxRequestsPerSender: number;
|
|
33
|
+
maxConcurrent: number;
|
|
34
|
+
};
|
|
35
|
+
media: {
|
|
36
|
+
allowPrivateNetworks: boolean;
|
|
37
|
+
maxDownloadBytes: number;
|
|
38
|
+
requestTimeoutMs: number;
|
|
39
|
+
allowedMimePrefixes: string[];
|
|
40
|
+
};
|
|
41
|
+
logging: {
|
|
42
|
+
redactSecrets: boolean;
|
|
43
|
+
logRejectedRequests: boolean;
|
|
44
|
+
};
|
|
45
|
+
commandBypass: {
|
|
46
|
+
requireAllowFrom: boolean;
|
|
47
|
+
requireMentionForCommands: boolean;
|
|
48
|
+
};
|
|
49
|
+
proxy: {
|
|
50
|
+
enabled: boolean;
|
|
51
|
+
trustedProxyCidrs: string[];
|
|
52
|
+
allowedPublicHosts: string[];
|
|
53
|
+
requireHttpsProto: boolean;
|
|
54
|
+
rejectStatus: 400 | 403 | 404;
|
|
55
|
+
isTrustedProxy: (ip: string) => boolean;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type GroupMeWebhookRequestContext = {
|
|
60
|
+
remoteIp: string;
|
|
61
|
+
clientIp: string;
|
|
62
|
+
host: string;
|
|
63
|
+
proto: "http" | "https";
|
|
64
|
+
fromTrustedProxy: boolean;
|
|
65
|
+
usingForwardedHeaders: boolean;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type GroupMeProxyValidation =
|
|
69
|
+
| { ok: true; context: GroupMeWebhookRequestContext }
|
|
70
|
+
| {
|
|
71
|
+
ok: false;
|
|
72
|
+
reason: "missing_host" | "host_not_allowed" | "proto_not_https";
|
|
73
|
+
status: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function positiveIntOrDefault(value: unknown, fallback: number): number {
|
|
77
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
78
|
+
return Math.floor(value);
|
|
79
|
+
}
|
|
80
|
+
return fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeIpCandidate(raw: string): string {
|
|
84
|
+
let value = raw.trim();
|
|
85
|
+
if (!value) {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
if (value.includes(",")) {
|
|
89
|
+
value = value.split(",")[0]?.trim() ?? "";
|
|
90
|
+
}
|
|
91
|
+
if (value.startsWith("[")) {
|
|
92
|
+
const endIndex = value.indexOf("]");
|
|
93
|
+
if (endIndex > 0) {
|
|
94
|
+
value = value.slice(1, endIndex);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const zoneIndex = value.indexOf("%");
|
|
98
|
+
if (zoneIndex > 0) {
|
|
99
|
+
value = value.slice(0, zoneIndex);
|
|
100
|
+
}
|
|
101
|
+
if (value.startsWith("::ffff:")) {
|
|
102
|
+
const mapped = value.slice("::ffff:".length);
|
|
103
|
+
if (isIP(mapped) === 4) {
|
|
104
|
+
value = mapped;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (isIP(value) === 0) {
|
|
108
|
+
const maybeWithPort = value.split(":");
|
|
109
|
+
if (
|
|
110
|
+
maybeWithPort.length === 2 &&
|
|
111
|
+
/^\d+$/.test(maybeWithPort[1] ?? "") &&
|
|
112
|
+
isIP(maybeWithPort[0] ?? "") === 4
|
|
113
|
+
) {
|
|
114
|
+
value = maybeWithPort[0] ?? "";
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return isIP(value) === 0 ? "" : value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getHeaderValue(headers: IncomingHttpHeaders, key: string): string {
|
|
121
|
+
const raw = headers[key];
|
|
122
|
+
if (typeof raw === "string") {
|
|
123
|
+
return raw.trim();
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(raw)) {
|
|
126
|
+
return raw[0]?.trim() ?? "";
|
|
127
|
+
}
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeHost(value: string): string {
|
|
132
|
+
let host = value.trim().toLowerCase();
|
|
133
|
+
if (!host) {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
if (host.includes(",")) {
|
|
137
|
+
host = host.split(",")[0]?.trim() ?? "";
|
|
138
|
+
}
|
|
139
|
+
if (!host) {
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
if (host.startsWith("[")) {
|
|
143
|
+
const endBracket = host.indexOf("]");
|
|
144
|
+
if (endBracket <= 0) {
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
147
|
+
return host.slice(1, endBracket);
|
|
148
|
+
}
|
|
149
|
+
if (host.includes("@")) {
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
const maybeWithoutPort = host.split(":");
|
|
153
|
+
if (
|
|
154
|
+
maybeWithoutPort.length === 2 &&
|
|
155
|
+
/^\d+$/.test(maybeWithoutPort[1] ?? "")
|
|
156
|
+
) {
|
|
157
|
+
host = maybeWithoutPort[0] ?? "";
|
|
158
|
+
}
|
|
159
|
+
return host.trim();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseProxyRules(entries: string[]): ProxyRule[] {
|
|
163
|
+
const rules: ProxyRule[] = [];
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
const raw = entry.trim();
|
|
166
|
+
if (!raw) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (raw.includes("/")) {
|
|
170
|
+
const [network, prefixRaw] = raw.split("/");
|
|
171
|
+
const normalizedNetwork = normalizeIpCandidate(network ?? "");
|
|
172
|
+
const ipVersion = isIP(normalizedNetwork);
|
|
173
|
+
if (!ipVersion) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const prefix = Number(prefixRaw);
|
|
177
|
+
const maxPrefix =
|
|
178
|
+
ipVersion === 4 ? IPV4_MAX_CIDR_PREFIX : IPV6_MAX_CIDR_PREFIX;
|
|
179
|
+
if (!Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
rules.push({ kind: "cidr", value: `${normalizedNetwork}/${prefix}` });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const normalizedIp = normalizeIpCandidate(raw);
|
|
186
|
+
if (normalizedIp) {
|
|
187
|
+
rules.push({ kind: "ip", value: normalizedIp });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return rules;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function createTrustedProxyMatcher(entries: string[]): (ip: string) => boolean {
|
|
194
|
+
const rules = parseProxyRules(entries);
|
|
195
|
+
if (rules.length === 0) {
|
|
196
|
+
return () => false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const blockList = new BlockList();
|
|
200
|
+
for (const rule of rules) {
|
|
201
|
+
if (rule.kind === "ip") {
|
|
202
|
+
const version = isIP(rule.value);
|
|
203
|
+
if (version === 4) {
|
|
204
|
+
blockList.addAddress(rule.value, "ipv4");
|
|
205
|
+
} else if (version === 6) {
|
|
206
|
+
blockList.addAddress(rule.value, "ipv6");
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const [network, prefixRaw] = rule.value.split("/");
|
|
211
|
+
const prefix = Number(prefixRaw);
|
|
212
|
+
const version = isIP(network);
|
|
213
|
+
if (version === 4) {
|
|
214
|
+
blockList.addSubnet(network, prefix, "ipv4");
|
|
215
|
+
} else if (version === 6) {
|
|
216
|
+
blockList.addSubnet(network, prefix, "ipv6");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return (ip: string) => {
|
|
221
|
+
const normalized = normalizeIpCandidate(ip);
|
|
222
|
+
if (!normalized) {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (isIP(normalized) === 4) {
|
|
226
|
+
return blockList.check(normalized, "ipv4");
|
|
227
|
+
}
|
|
228
|
+
return blockList.check(normalized, "ipv6");
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function extractCallbackToken(callbackUrl: string | undefined): string {
|
|
233
|
+
const raw = callbackUrl?.trim() ?? "";
|
|
234
|
+
if (!raw) {
|
|
235
|
+
return "";
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const parsed = new URL(raw, "http://localhost");
|
|
239
|
+
return parsed.searchParams.get("k")?.trim() ?? "";
|
|
240
|
+
} catch {
|
|
241
|
+
throw new Error(`Invalid callbackUrl: unable to parse "${raw}"`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function resolveGroupMeSecurity(
|
|
246
|
+
accountConfig: GroupMeAccountConfig,
|
|
247
|
+
): ResolvedGroupMeSecurity {
|
|
248
|
+
const security = (accountConfig.security ?? {}) as GroupMeSecurityConfig;
|
|
249
|
+
const callbackToken = extractCallbackToken(accountConfig.callbackUrl);
|
|
250
|
+
const groupId = readTrimmed(accountConfig.groupId) ?? "";
|
|
251
|
+
|
|
252
|
+
const allowedMimePrefixes = Array.isArray(security.media?.allowedMimePrefixes)
|
|
253
|
+
? security.media.allowedMimePrefixes
|
|
254
|
+
.map((prefix) => readTrimmed(prefix))
|
|
255
|
+
.filter((entry): entry is string => Boolean(entry))
|
|
256
|
+
: ["image/"];
|
|
257
|
+
const trustedProxyCidrs = Array.isArray(security.proxy?.trustedProxyCidrs)
|
|
258
|
+
? security.proxy.trustedProxyCidrs
|
|
259
|
+
.map((entry) => readTrimmed(entry))
|
|
260
|
+
.filter((entry): entry is string => Boolean(entry))
|
|
261
|
+
: [];
|
|
262
|
+
const allowedPublicHosts = Array.isArray(security.proxy?.allowedPublicHosts)
|
|
263
|
+
? security.proxy.allowedPublicHosts
|
|
264
|
+
.map((entry) => normalizeHost(readTrimmed(entry) ?? ""))
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
: [];
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
callbackToken,
|
|
270
|
+
callbackRejectStatus: 404,
|
|
271
|
+
groupId,
|
|
272
|
+
replay: {
|
|
273
|
+
enabled: true,
|
|
274
|
+
ttlSeconds: positiveIntOrDefault(security.replay?.ttlSeconds, 600),
|
|
275
|
+
maxEntries: positiveIntOrDefault(security.replay?.maxEntries, 10_000),
|
|
276
|
+
},
|
|
277
|
+
rateLimit: {
|
|
278
|
+
enabled: true,
|
|
279
|
+
windowMs: positiveIntOrDefault(security.rateLimit?.windowMs, 60_000),
|
|
280
|
+
maxRequestsPerIp: positiveIntOrDefault(security.rateLimit?.maxRequestsPerIp, 120),
|
|
281
|
+
maxRequestsPerSender: positiveIntOrDefault(security.rateLimit?.maxRequestsPerSender, 60),
|
|
282
|
+
maxConcurrent: positiveIntOrDefault(security.rateLimit?.maxConcurrent, 8),
|
|
283
|
+
},
|
|
284
|
+
media: {
|
|
285
|
+
allowPrivateNetworks: security.media?.allowPrivateNetworks === true,
|
|
286
|
+
maxDownloadBytes: positiveIntOrDefault(security.media?.maxDownloadBytes, 15 * 1024 * 1024),
|
|
287
|
+
requestTimeoutMs: positiveIntOrDefault(security.media?.requestTimeoutMs, 10_000),
|
|
288
|
+
allowedMimePrefixes:
|
|
289
|
+
allowedMimePrefixes.length > 0 ? allowedMimePrefixes : ["image/"],
|
|
290
|
+
},
|
|
291
|
+
logging: {
|
|
292
|
+
redactSecrets: security.logging?.redactSecrets !== false,
|
|
293
|
+
logRejectedRequests: security.logging?.logRejectedRequests !== false,
|
|
294
|
+
},
|
|
295
|
+
commandBypass: {
|
|
296
|
+
requireAllowFrom: security.commandBypass?.requireAllowFrom !== false,
|
|
297
|
+
requireMentionForCommands:
|
|
298
|
+
security.commandBypass?.requireMentionForCommands === true,
|
|
299
|
+
},
|
|
300
|
+
proxy: {
|
|
301
|
+
enabled: security.proxy != null,
|
|
302
|
+
trustedProxyCidrs,
|
|
303
|
+
allowedPublicHosts,
|
|
304
|
+
requireHttpsProto: security.proxy?.requireHttpsProto === true,
|
|
305
|
+
rejectStatus: security.proxy?.rejectStatus ?? 403,
|
|
306
|
+
isTrustedProxy: createTrustedProxyMatcher(trustedProxyCidrs),
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function safeEqualToken(left: string, right: string): boolean {
|
|
312
|
+
const leftBuffer = Buffer.from(left);
|
|
313
|
+
const rightBuffer = Buffer.from(right);
|
|
314
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function verifyCallbackAuth(params: {
|
|
321
|
+
url: URL;
|
|
322
|
+
security: ResolvedGroupMeSecurity;
|
|
323
|
+
}): CallbackAuthResult {
|
|
324
|
+
const expectedToken = params.security.callbackToken;
|
|
325
|
+
if (!expectedToken) {
|
|
326
|
+
return { ok: false, reason: "disabled" };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const inboundToken = params.url.searchParams.get("k")?.trim() ?? "";
|
|
330
|
+
if (!inboundToken) {
|
|
331
|
+
return { ok: false, reason: "missing" };
|
|
332
|
+
}
|
|
333
|
+
if (safeEqualToken(inboundToken, expectedToken)) {
|
|
334
|
+
return { ok: true, tokenId: "active" };
|
|
335
|
+
}
|
|
336
|
+
return { ok: false, reason: "mismatch" };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function checkGroupBinding(params: {
|
|
340
|
+
groupId: string;
|
|
341
|
+
inboundGroupId: string;
|
|
342
|
+
}): { ok: true } | { ok: false; reason: "mismatch" } {
|
|
343
|
+
if (params.groupId !== params.inboundGroupId) {
|
|
344
|
+
return { ok: false, reason: "mismatch" };
|
|
345
|
+
}
|
|
346
|
+
return { ok: true };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function redactCallbackUrl(
|
|
350
|
+
raw: string,
|
|
351
|
+
security: ResolvedGroupMeSecurity,
|
|
352
|
+
): string {
|
|
353
|
+
if (!security.callbackToken) {
|
|
354
|
+
return raw;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const parsed = new URL(raw, "http://localhost");
|
|
359
|
+
if (parsed.searchParams.has("k")) {
|
|
360
|
+
parsed.searchParams.set("k", "[redacted]");
|
|
361
|
+
}
|
|
362
|
+
const serialized = `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
363
|
+
return (serialized || raw).replaceAll("%5Bredacted%5D", "[redacted]");
|
|
364
|
+
} catch {
|
|
365
|
+
return raw.replaceAll(security.callbackToken, "[redacted]");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function validateProxyRequest(params: {
|
|
370
|
+
headers: IncomingHttpHeaders;
|
|
371
|
+
remoteAddress: string;
|
|
372
|
+
socketEncrypted: boolean;
|
|
373
|
+
security: ResolvedGroupMeSecurity;
|
|
374
|
+
}): GroupMeProxyValidation {
|
|
375
|
+
const remoteIp = normalizeIpCandidate(params.remoteAddress) || "unknown";
|
|
376
|
+
const proxyConfig = params.security.proxy;
|
|
377
|
+
const defaultProto: "http" | "https" = params.socketEncrypted
|
|
378
|
+
? "https"
|
|
379
|
+
: "http";
|
|
380
|
+
const hostHeader = normalizeHost(getHeaderValue(params.headers, "host"));
|
|
381
|
+
|
|
382
|
+
if (!proxyConfig.enabled) {
|
|
383
|
+
return {
|
|
384
|
+
ok: true,
|
|
385
|
+
context: {
|
|
386
|
+
remoteIp,
|
|
387
|
+
clientIp: remoteIp,
|
|
388
|
+
host: hostHeader,
|
|
389
|
+
proto: defaultProto,
|
|
390
|
+
fromTrustedProxy: false,
|
|
391
|
+
usingForwardedHeaders: false,
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const fromTrustedProxy =
|
|
397
|
+
proxyConfig.trustedProxyCidrs.length > 0 &&
|
|
398
|
+
proxyConfig.isTrustedProxy(remoteIp);
|
|
399
|
+
|
|
400
|
+
const forwardedFor = normalizeIpCandidate(
|
|
401
|
+
getHeaderValue(params.headers, "x-forwarded-for"),
|
|
402
|
+
);
|
|
403
|
+
const forwardedHost = normalizeHost(
|
|
404
|
+
getHeaderValue(params.headers, "x-forwarded-host"),
|
|
405
|
+
);
|
|
406
|
+
const forwardedProtoRaw = getHeaderValue(params.headers, "x-forwarded-proto")
|
|
407
|
+
.split(",")[0]
|
|
408
|
+
?.trim()
|
|
409
|
+
.toLowerCase();
|
|
410
|
+
const forwardedProto: "http" | "https" | null =
|
|
411
|
+
forwardedProtoRaw === "http" || forwardedProtoRaw === "https"
|
|
412
|
+
? forwardedProtoRaw
|
|
413
|
+
: null;
|
|
414
|
+
|
|
415
|
+
const usingForwardedHeaders =
|
|
416
|
+
fromTrustedProxy && Boolean(forwardedFor || forwardedHost || forwardedProto);
|
|
417
|
+
const effectiveClientIp =
|
|
418
|
+
usingForwardedHeaders && forwardedFor ? forwardedFor : remoteIp;
|
|
419
|
+
const effectiveHost =
|
|
420
|
+
usingForwardedHeaders && forwardedHost ? forwardedHost : hostHeader;
|
|
421
|
+
const effectiveProto =
|
|
422
|
+
usingForwardedHeaders && forwardedProto ? forwardedProto : defaultProto;
|
|
423
|
+
|
|
424
|
+
if (!effectiveHost) {
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
reason: "missing_host",
|
|
428
|
+
status: proxyConfig.rejectStatus,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
if (
|
|
432
|
+
proxyConfig.allowedPublicHosts.length > 0 &&
|
|
433
|
+
!proxyConfig.allowedPublicHosts.includes(effectiveHost)
|
|
434
|
+
) {
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
reason: "host_not_allowed",
|
|
438
|
+
status: proxyConfig.rejectStatus,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (proxyConfig.requireHttpsProto && effectiveProto !== "https") {
|
|
442
|
+
return {
|
|
443
|
+
ok: false,
|
|
444
|
+
reason: "proto_not_https",
|
|
445
|
+
status: proxyConfig.rejectStatus,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
ok: true,
|
|
451
|
+
context: {
|
|
452
|
+
remoteIp,
|
|
453
|
+
clientIp: effectiveClientIp || "unknown",
|
|
454
|
+
host: effectiveHost,
|
|
455
|
+
proto: effectiveProto,
|
|
456
|
+
fromTrustedProxy,
|
|
457
|
+
usingForwardedHeaders,
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|