mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-15 21:03:47 +00:00
Merge commit from fork
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { statsLogger } from "./logger.js";
|
import { statsLogger } from "./logger.js";
|
||||||
|
import { safeOutboundFetch } from "./safe-outbound-fetch.js";
|
||||||
|
|
||||||
export interface AlertPayload {
|
export interface AlertPayload {
|
||||||
hostName: string;
|
hostName: string;
|
||||||
@@ -36,7 +37,7 @@ async function fetchWithRetry(
|
|||||||
options: RequestInit,
|
options: RequestInit,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const attempt = async () => {
|
const attempt = async () => {
|
||||||
const res = await fetch(url, options);
|
const res = await safeOutboundFetch(url, options);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { lookup } from "dns";
|
||||||
|
import { BlockList, isIP } from "net";
|
||||||
|
import { Agent } from "undici";
|
||||||
|
|
||||||
|
const blockedAddresses = new BlockList();
|
||||||
|
|
||||||
|
for (const [network, prefix] of [
|
||||||
|
["0.0.0.0", 8],
|
||||||
|
["10.0.0.0", 8],
|
||||||
|
["100.64.0.0", 10],
|
||||||
|
["127.0.0.0", 8],
|
||||||
|
["169.254.0.0", 16],
|
||||||
|
["172.16.0.0", 12],
|
||||||
|
["192.168.0.0", 16],
|
||||||
|
["198.18.0.0", 15],
|
||||||
|
["224.0.0.0", 4],
|
||||||
|
["240.0.0.0", 4],
|
||||||
|
] as const) {
|
||||||
|
blockedAddresses.addSubnet(network, prefix, "ipv4");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [network, prefix] of [
|
||||||
|
["::", 128],
|
||||||
|
["::1", 128],
|
||||||
|
["::ffff:0:0", 96],
|
||||||
|
["fc00::", 7],
|
||||||
|
["fe80::", 10],
|
||||||
|
["ff00::", 8],
|
||||||
|
] as const) {
|
||||||
|
blockedAddresses.addSubnet(network, prefix, "ipv6");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBlockedAddress(address: string): boolean {
|
||||||
|
const family = isIP(address);
|
||||||
|
return (
|
||||||
|
family === 0 ||
|
||||||
|
blockedAddresses.check(address, family === 4 ? "ipv4" : "ipv6")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function safeOutboundFetch(
|
||||||
|
rawUrl: string,
|
||||||
|
options: RequestInit,
|
||||||
|
): Promise<Response> {
|
||||||
|
const url = new URL(rawUrl);
|
||||||
|
if (
|
||||||
|
!["http:", "https:"].includes(url.protocol) ||
|
||||||
|
url.username ||
|
||||||
|
url.password
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid outbound URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
||||||
|
if (isIP(hostname) && isBlockedAddress(hostname)) {
|
||||||
|
throw new Error("Private destinations are not allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatcher = new Agent({
|
||||||
|
connect: {
|
||||||
|
lookup(host, lookupOptions, callback) {
|
||||||
|
lookup(
|
||||||
|
host,
|
||||||
|
{ ...lookupOptions, all: true, verbatim: true },
|
||||||
|
(error, addresses) => {
|
||||||
|
if (error) return callback(error, "", 0);
|
||||||
|
if (
|
||||||
|
!addresses.length ||
|
||||||
|
addresses.some(({ address }) => isBlockedAddress(address))
|
||||||
|
) {
|
||||||
|
return callback(
|
||||||
|
new Error("Private destinations are not allowed"),
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const selected = addresses[0];
|
||||||
|
callback(null, selected.address, selected.family);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fetch(url, {
|
||||||
|
...options,
|
||||||
|
redirect: "error",
|
||||||
|
dispatcher,
|
||||||
|
} as RequestInit & { dispatcher: Agent });
|
||||||
|
} finally {
|
||||||
|
await dispatcher.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user