@modelcontextprotocol/inspector 1.0.0 → 1.0.2
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 +11 -1
- package/cli/build/cli.js +13 -0
- package/client/bin/start.js +5 -0
- package/client/dist/assets/{OAuthCallback-D56s1eS_.js → OAuthCallback-at8T3Iwi.js} +1 -1
- package/client/dist/assets/{OAuthDebugCallback-CdGJkhXe.js → OAuthDebugCallback-ZJ9hxfpr.js} +1 -1
- package/client/dist/assets/{index-BlUSNvoM.css → index-DXy0twoH.css} +48 -0
- package/client/dist/assets/{index-DjSLdS7f.js → index-DdyO6kfY.js} +2580 -1499
- package/client/dist/index.html +2 -2
- package/package.json +20 -8
- package/server/build/index.js +17 -76
- package/server/build/proxy-security.js +154 -0
package/client/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/mcp.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>MCP Inspector</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DdyO6kfY.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DXy0twoH.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root" class="w-full"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelcontextprotocol/inspector",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Model Context Protocol inspector",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Model Context Protocol a Series of LF Projects, LLC.",
|
|
@@ -42,19 +42,20 @@
|
|
|
42
42
|
"start-client": "cd client && npm run preview",
|
|
43
43
|
"test": "npm run prettier-check && cd client && npm test",
|
|
44
44
|
"test-cli": "cd cli && npm run test",
|
|
45
|
+
"test-server": "cd server && npm run test",
|
|
45
46
|
"test:e2e": "MCP_AUTO_OPEN_ENABLED=false npm run test:e2e --workspace=client",
|
|
46
47
|
"prettier-fix": "prettier --write .",
|
|
47
48
|
"prettier-check": "prettier --check .",
|
|
48
49
|
"lint": "prettier --check . && cd client && npm run lint",
|
|
49
50
|
"prepare": "husky && npm run build",
|
|
50
|
-
"publish-all": "npm publish --workspaces --access public && npm publish --access public",
|
|
51
|
+
"publish-all": "npm publish --workspaces --access public --tag v1-latest && npm publish --access public --tag v1-latest",
|
|
51
52
|
"update-version": "node scripts/update-version.js",
|
|
52
53
|
"check-version": "node scripts/check-version-consistency.js"
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@modelcontextprotocol/inspector-cli": "^1.0.
|
|
56
|
-
"@modelcontextprotocol/inspector-client": "^1.0.
|
|
57
|
-
"@modelcontextprotocol/inspector-server": "^1.0.
|
|
56
|
+
"@modelcontextprotocol/inspector-cli": "^1.0.2",
|
|
57
|
+
"@modelcontextprotocol/inspector-client": "^1.0.2",
|
|
58
|
+
"@modelcontextprotocol/inspector-server": "^1.0.2",
|
|
58
59
|
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
59
60
|
"concurrently": "^9.2.0",
|
|
60
61
|
"node-fetch": "^3.3.2",
|
|
@@ -80,13 +81,20 @@
|
|
|
80
81
|
"overrides": {
|
|
81
82
|
"get-intrinsic": "1.3.0",
|
|
82
83
|
"shell-quote": "^1.8.4",
|
|
83
|
-
"hono": "^4.
|
|
84
|
+
"hono": "^4.13.3",
|
|
84
85
|
"form-data": "^4.0.6",
|
|
85
86
|
"@babel/core@<=7.29.0": "^7.29.6",
|
|
86
87
|
"esbuild@<0.28.1": "^0.28.1",
|
|
87
88
|
"minimatch@<3.1.4": "3.1.5",
|
|
88
|
-
"js-yaml@<4": "3.15.
|
|
89
|
-
"js-yaml@>=4": "4.
|
|
89
|
+
"js-yaml@<4": "3.15.1",
|
|
90
|
+
"js-yaml@>=4": "4.3.1",
|
|
91
|
+
"@hono/node-server@<1.19.15": "^1.19.15",
|
|
92
|
+
"ip-address@<=10.3.0": "^10.5.0",
|
|
93
|
+
"fast-uri@<3.1.5": "^3.1.5",
|
|
94
|
+
"brace-expansion@<2": "^1.1.18",
|
|
95
|
+
"brace-expansion@>=4": "^5.0.9",
|
|
96
|
+
"nanoid@<3.3.18": "^3.3.18",
|
|
97
|
+
"postcss@<=8.5.22": "^8.5.26"
|
|
90
98
|
},
|
|
91
99
|
"engines": {
|
|
92
100
|
"node": ">=22.7.5"
|
|
@@ -95,5 +103,9 @@
|
|
|
95
103
|
"**/*.{js,ts,jsx,tsx,json,md}": [
|
|
96
104
|
"prettier --write"
|
|
97
105
|
]
|
|
106
|
+
},
|
|
107
|
+
"allowScripts": {
|
|
108
|
+
"esbuild": false,
|
|
109
|
+
"fsevents": false
|
|
98
110
|
}
|
|
99
111
|
}
|
package/server/build/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import cors from "cors";
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { parse as shellParseArgs } from "shell-quote";
|
|
5
5
|
import nodeFetch, { Headers as NodeHeaders } from "node-fetch";
|
|
6
|
+
import { assertSafeProxyTarget, createPinnedAgent, ProxyTargetError, } from "./proxy-security.js";
|
|
6
7
|
// Type-compatible wrappers for node-fetch to work with browser-style types
|
|
7
8
|
const fetch = nodeFetch;
|
|
8
9
|
const Headers = NodeHeaders;
|
|
@@ -16,8 +17,6 @@ import rateLimit from "express-rate-limit";
|
|
|
16
17
|
import { findActualExecutable } from "spawn-rx";
|
|
17
18
|
import mcpProxy from "./mcpProxy.js";
|
|
18
19
|
import { randomUUID, randomBytes, timingSafeEqual } from "node:crypto";
|
|
19
|
-
import { lookup as dnsLookup } from "node:dns/promises";
|
|
20
|
-
import { isIP } from "node:net";
|
|
21
20
|
import { fileURLToPath } from "url";
|
|
22
21
|
import { dirname, join } from "path";
|
|
23
22
|
import { readFileSync } from "fs";
|
|
@@ -675,83 +674,16 @@ app.get("/health", (req, res) => {
|
|
|
675
674
|
status: "ok",
|
|
676
675
|
});
|
|
677
676
|
});
|
|
678
|
-
// Raised when the /fetch proxy is asked to reach a disallowed target, so the
|
|
679
|
-
// route can distinguish an SSRF rejection (403) from an upstream failure (500).
|
|
680
|
-
class ProxyTargetError extends Error {
|
|
681
|
-
}
|
|
682
677
|
const MAX_PROXY_REDIRECTS = 5;
|
|
683
|
-
// The /fetch proxy is used by the client to reach OAuth discovery/token
|
|
684
|
-
// endpoints on the connected MCP server (bypassing browser CORS). Connecting
|
|
685
|
-
// to loopback and RFC1918/private LAN hosts is a core use case (testing local
|
|
686
|
-
// or self-hosted MCP servers), so those stay reachable. We only block
|
|
687
|
-
// link-local addresses — 169.254.0.0/16 (the AWS/GCP/Azure/etc. cloud metadata
|
|
688
|
-
// endpoint 169.254.169.254 lives here) and IPv6 link-local / the AWS IPv6 IMDS
|
|
689
|
-
// address — which are never legitimate proxy targets and are the real SSRF
|
|
690
|
-
// exfiltration vector.
|
|
691
|
-
function isBlockedProxyAddress(ip) {
|
|
692
|
-
const kind = isIP(ip);
|
|
693
|
-
if (kind === 4) {
|
|
694
|
-
const parts = ip.split(".").map(Number);
|
|
695
|
-
// 169.254.0.0/16 — IPv4 link-local (incl. cloud metadata 169.254.169.254)
|
|
696
|
-
return parts[0] === 169 && parts[1] === 254;
|
|
697
|
-
}
|
|
698
|
-
if (kind === 6) {
|
|
699
|
-
const addr = ip.toLowerCase();
|
|
700
|
-
// IPv4-mapped IPv6 (::ffff:x). The WHATWG URL parser serializes the embedded
|
|
701
|
-
// IPv4 in hex (::ffff:a9fe:a9fe for 169.254.169.254), while DNS and other
|
|
702
|
-
// sources may use the dotted form (::ffff:169.254.169.254) — handle both by
|
|
703
|
-
// extracting the IPv4 and re-checking it.
|
|
704
|
-
const mapped = addr.match(/^::ffff:(.+)$/);
|
|
705
|
-
if (mapped) {
|
|
706
|
-
const tail = mapped[1];
|
|
707
|
-
if (isIP(tail) === 4) {
|
|
708
|
-
return isBlockedProxyAddress(tail);
|
|
709
|
-
}
|
|
710
|
-
const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
711
|
-
if (hex) {
|
|
712
|
-
const high = parseInt(hex[1], 16);
|
|
713
|
-
const low = parseInt(hex[2], 16);
|
|
714
|
-
return isBlockedProxyAddress(`${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
// fe80::/10 — IPv6 link-local
|
|
718
|
-
if (/^fe[89ab]/.test(addr)) {
|
|
719
|
-
return true;
|
|
720
|
-
}
|
|
721
|
-
// fd00:ec2::254 — AWS IPv6 instance metadata endpoint
|
|
722
|
-
return addr === "fd00:ec2::254";
|
|
723
|
-
}
|
|
724
|
-
return false;
|
|
725
|
-
}
|
|
726
|
-
// Resolves the target host and rejects it if any resolved address is a blocked
|
|
727
|
-
// (link-local/metadata) address, mitigating SSRF including the DNS case where a
|
|
728
|
-
// hostname resolves to the cloud metadata IP.
|
|
729
|
-
async function assertSafeProxyTarget(parsedUrl) {
|
|
730
|
-
const host = parsedUrl.hostname.replace(/^\[|\]$/g, ""); // strip IPv6 brackets
|
|
731
|
-
let addresses;
|
|
732
|
-
if (isIP(host)) {
|
|
733
|
-
addresses = [host];
|
|
734
|
-
}
|
|
735
|
-
else {
|
|
736
|
-
try {
|
|
737
|
-
addresses = (await dnsLookup(host, { all: true })).map((r) => r.address);
|
|
738
|
-
}
|
|
739
|
-
catch {
|
|
740
|
-
throw new ProxyTargetError(`Could not resolve host: ${host}`);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
if (addresses.length === 0) {
|
|
744
|
-
throw new ProxyTargetError(`Could not resolve host: ${host}`);
|
|
745
|
-
}
|
|
746
|
-
for (const address of addresses) {
|
|
747
|
-
if (isBlockedProxyAddress(address)) {
|
|
748
|
-
throw new ProxyTargetError(`Refusing to proxy request to blocked address (${address})`);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
678
|
// Fetch that follows redirects manually so every hop's target is re-validated
|
|
753
679
|
// against assertSafeProxyTarget — otherwise an allowed host could redirect the
|
|
754
680
|
// proxy into a blocked internal/metadata address.
|
|
681
|
+
//
|
|
682
|
+
// On each hop we call assertSafeProxyTarget (which resolves DNS and validates
|
|
683
|
+
// every returned IP) and then pass a createPinnedAgent to node-fetch so it
|
|
684
|
+
// connects to the IP we just validated instead of re-resolving the hostname.
|
|
685
|
+
// This eliminates the TOCTOU window between the block-list check and the
|
|
686
|
+
// actual TCP connection that a DNS-rebinding attack would exploit.
|
|
755
687
|
async function safeProxyFetch(initialUrl, init) {
|
|
756
688
|
let currentUrl = new URL(initialUrl);
|
|
757
689
|
let method = init?.method ?? "GET";
|
|
@@ -761,12 +693,18 @@ async function safeProxyFetch(initialUrl, init) {
|
|
|
761
693
|
if (!["http:", "https:"].includes(currentUrl.protocol)) {
|
|
762
694
|
throw new ProxyTargetError("Only http/https URLs are allowed");
|
|
763
695
|
}
|
|
764
|
-
|
|
696
|
+
// Resolve DNS once, validate every address, and pin the connection to that
|
|
697
|
+
// validated set so node-fetch never does a second DNS lookup. All of them
|
|
698
|
+
// are passed through, so Node's address-family fallback still works while
|
|
699
|
+
// the reachable set stays limited to what was just validated.
|
|
700
|
+
const validatedAddresses = await assertSafeProxyTarget(currentUrl);
|
|
701
|
+
const agent = createPinnedAgent(currentUrl.protocol, validatedAddresses);
|
|
765
702
|
const response = await fetch(currentUrl.toString(), {
|
|
766
703
|
method,
|
|
767
704
|
headers,
|
|
768
705
|
body,
|
|
769
706
|
redirect: "manual",
|
|
707
|
+
agent,
|
|
770
708
|
});
|
|
771
709
|
if (response.status >= 300 && response.status < 400) {
|
|
772
710
|
const location = response.headers.get("location");
|
|
@@ -866,6 +804,9 @@ const PORT = parseInt(process.env.SERVER_PORT || DEFAULT_MCP_PROXY_LISTEN_PORT,
|
|
|
866
804
|
const HOST = process.env.HOST || "localhost";
|
|
867
805
|
const server = app.listen(PORT, HOST);
|
|
868
806
|
server.on("listening", () => {
|
|
807
|
+
console.log(`\n⚠️ @modelcontextprotocol/inspector v1 is deprecated.\n` +
|
|
808
|
+
` Upgrade: npx @modelcontextprotocol/inspector@latest\n` +
|
|
809
|
+
` v1 receives security fixes only.\n`);
|
|
869
810
|
console.log(`⚙️ Proxy server listening on ${HOST}:${PORT}`);
|
|
870
811
|
if (!authDisabled) {
|
|
871
812
|
console.log(`🔑 Session token: ${sessionToken}\n ` +
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF-prevention helpers for the /fetch reverse proxy.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts so they can be unit-tested independently.
|
|
5
|
+
*
|
|
6
|
+
* ## DNS-rebinding / TOCTOU threat model
|
|
7
|
+
*
|
|
8
|
+
* `assertSafeProxyTarget` resolves the target hostname and validates every
|
|
9
|
+
* returned IP against the block-list. The resolved IPs are returned to the
|
|
10
|
+
* caller so they can be passed to `createPinnedAgent`, which builds an
|
|
11
|
+
* http/https.Agent whose `lookup` callback unconditionally returns the
|
|
12
|
+
* pre-validated IP. The agent is then passed to `node-fetch`, which calls the
|
|
13
|
+
* lookup hook instead of doing its own DNS query.
|
|
14
|
+
*
|
|
15
|
+
* Without this pinning there is a TOCTOU race: an attacker who controls the
|
|
16
|
+
* target domain can flip its DNS record to 169.254.169.254 (AWS/GCP/Azure
|
|
17
|
+
* instance-metadata) in the window between `assertSafeProxyTarget` returning
|
|
18
|
+
* and `fetch` performing its own resolution. Pinning eliminates that window.
|
|
19
|
+
*/
|
|
20
|
+
import http from "node:http";
|
|
21
|
+
import https from "node:https";
|
|
22
|
+
import { isIP } from "node:net";
|
|
23
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Block-list
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/**
|
|
28
|
+
* Returns true if `ip` is a link-local or cloud-metadata address that must
|
|
29
|
+
* never be reachable through the proxy.
|
|
30
|
+
*
|
|
31
|
+
* Blocked ranges:
|
|
32
|
+
* - 169.254.0.0/16 (IPv4 link-local; includes 169.254.169.254 IMDS)
|
|
33
|
+
* - ::ffff:169.254.x.x (IPv4-mapped IPv6 variants of the above)
|
|
34
|
+
* - fe80::/10 (IPv6 link-local)
|
|
35
|
+
* - fd00:ec2::254 (AWS IPv6 instance-metadata)
|
|
36
|
+
*/
|
|
37
|
+
export function isBlockedProxyAddress(ip) {
|
|
38
|
+
const kind = isIP(ip);
|
|
39
|
+
if (kind === 4) {
|
|
40
|
+
const parts = ip.split(".").map(Number);
|
|
41
|
+
return parts[0] === 169 && parts[1] === 254;
|
|
42
|
+
}
|
|
43
|
+
if (kind === 6) {
|
|
44
|
+
const addr = ip.toLowerCase();
|
|
45
|
+
// IPv4-mapped IPv6 — two serialization forms:
|
|
46
|
+
// dotted ::ffff:169.254.169.254
|
|
47
|
+
// hex ::ffff:a9fe:a9fe
|
|
48
|
+
const mapped = addr.match(/^::ffff:(.+)$/);
|
|
49
|
+
if (mapped) {
|
|
50
|
+
const tail = mapped[1];
|
|
51
|
+
if (isIP(tail) === 4) {
|
|
52
|
+
return isBlockedProxyAddress(tail);
|
|
53
|
+
}
|
|
54
|
+
const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
55
|
+
if (hex) {
|
|
56
|
+
const high = parseInt(hex[1], 16);
|
|
57
|
+
const low = parseInt(hex[2], 16);
|
|
58
|
+
return isBlockedProxyAddress(`${high >> 8}.${high & 0xff}.${low >> 8}.${low & 0xff}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (/^fe[89ab]/.test(addr))
|
|
62
|
+
return true; // fe80::/10
|
|
63
|
+
return addr === "fd00:ec2::254"; // AWS IPv6 IMDS
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// DNS validation
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
/** Raised when the /fetch proxy is asked to reach a disallowed target. */
|
|
71
|
+
export class ProxyTargetError extends Error {
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolves `parsedUrl`'s hostname and asserts that every returned IP is
|
|
75
|
+
* outside the block-list.
|
|
76
|
+
*
|
|
77
|
+
* Returns the list of validated IP strings so the caller can pin one of them
|
|
78
|
+
* into an HTTP agent (see `createPinnedAgent`), eliminating the TOCTOU window
|
|
79
|
+
* between this check and the actual `fetch()` call.
|
|
80
|
+
*
|
|
81
|
+
* @throws {ProxyTargetError} if the host cannot be resolved or any resolved
|
|
82
|
+
* address falls inside the block-list.
|
|
83
|
+
*/
|
|
84
|
+
export async function assertSafeProxyTarget(parsedUrl) {
|
|
85
|
+
const host = parsedUrl.hostname.replace(/^\[|\]$/g, ""); // strip IPv6 brackets
|
|
86
|
+
let addresses;
|
|
87
|
+
if (isIP(host)) {
|
|
88
|
+
addresses = [host];
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
try {
|
|
92
|
+
addresses = (await dnsLookup(host, { all: true })).map((r) => r.address);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new ProxyTargetError(`Could not resolve host: ${host}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (addresses.length === 0) {
|
|
99
|
+
throw new ProxyTargetError(`Could not resolve host: ${host}`);
|
|
100
|
+
}
|
|
101
|
+
for (const address of addresses) {
|
|
102
|
+
if (isBlockedProxyAddress(address)) {
|
|
103
|
+
throw new ProxyTargetError(`Refusing to proxy request to blocked address (${address})`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return addresses;
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// IP-pinned agent factory
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
/**
|
|
112
|
+
* Creates an `http.Agent` or `https.Agent` whose internal `lookup` hook always
|
|
113
|
+
* returns `pinnedIps`, bypassing the OS resolver entirely.
|
|
114
|
+
*
|
|
115
|
+
* Pass this agent to `node-fetch` on every hop of `safeProxyFetch` so the TCP
|
|
116
|
+
* connection always goes to an IP that was validated by `assertSafeProxyTarget`
|
|
117
|
+
* — even if the domain's DNS record changes mid-flight.
|
|
118
|
+
*
|
|
119
|
+
* Every address `assertSafeProxyTarget` returned is handed to the hook, not
|
|
120
|
+
* just the first, so Node keeps its normal address-family fallback: a
|
|
121
|
+
* dual-stack host whose first address is unreachable (IPv6 on an IPv4-only
|
|
122
|
+
* network, say) still connects via a later one. The set it may choose from is
|
|
123
|
+
* exactly the prevalidated set, so the TOCTOU guarantee is unchanged.
|
|
124
|
+
*
|
|
125
|
+
* @param protocol - `"http:"` or `"https:"` (from `URL.protocol`)
|
|
126
|
+
* @param pinnedIps - Validated IPv4/IPv6 address strings, in preference order
|
|
127
|
+
*/
|
|
128
|
+
export function createPinnedAgent(protocol, pinnedIps) {
|
|
129
|
+
if (pinnedIps.length === 0) {
|
|
130
|
+
throw new ProxyTargetError("No validated address to connect to");
|
|
131
|
+
}
|
|
132
|
+
const resolved = pinnedIps.map((address) => ({
|
|
133
|
+
address,
|
|
134
|
+
family: isIP(address) === 6 ? 6 : 4,
|
|
135
|
+
}));
|
|
136
|
+
// Node.js lookup signature: (hostname, options, callback).
|
|
137
|
+
//
|
|
138
|
+
// The callback shape depends on `options.all`. Node's `net.connect` runs with
|
|
139
|
+
// `autoSelectFamily` enabled by default (Node >= 20), so it calls the hook
|
|
140
|
+
// with `{ all: true }` and expects an ARRAY of `{ address, family }` — handing
|
|
141
|
+
// it the scalar form there fails the connection with ERR_INVALID_IP_ADDRESS.
|
|
142
|
+
// Both shapes are supported so the agent works on either call path.
|
|
143
|
+
const lookup = (_hostname, opts, callback) => {
|
|
144
|
+
if (opts?.all) {
|
|
145
|
+
callback(null, resolved);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
callback(null, resolved[0].address, resolved[0].family);
|
|
149
|
+
};
|
|
150
|
+
if (protocol === "https:") {
|
|
151
|
+
return new https.Agent({ lookup });
|
|
152
|
+
}
|
|
153
|
+
return new http.Agent({ lookup });
|
|
154
|
+
}
|