11import type { LookupFunction } from 'node:net'
2- import { runtime } from 'std-env'
2+ import { createNetworkDispatcher } from '#nuxt-scripts/network-dispatcher'
3+ import { isPublicNetworkHostname } from './network-hostname'
34
4- const LOCAL_HOST_SUFFIXES = [
5- 'home' ,
6- 'internal' ,
7- 'lan' ,
8- 'local' ,
9- 'localdomain' ,
10- 'localhost' ,
11- ]
5+ export { isPublicNetworkHostname } from './network-hostname'
126
13- interface NetworkAddress {
7+ export interface NetworkAddress {
148address : string
159family : 4 | 6
1610}
1711
18- type ResolveNetworkHostname = (
12+ export type ResolveNetworkHostname = (
1913hostname : string ,
2014callback : ( error : Error | null , addresses : NetworkAddress [ ] ) => void ,
2115) => void
@@ -31,6 +25,11 @@ export interface PublicNetworkDispatcher {
3125close : ( ) => Promise < void >
3226}
3327
28+ export type CreateNetworkDispatcher = (
29+ createLookup : ( resolveHostname : ResolveNetworkHostname ) => LookupFunction ,
30+ resolveHostnameOverride ?: ResolveNetworkHostname ,
31+ ) => Promise < PublicNetworkDispatcher >
32+
3433/** Close a dispatcher without replacing the request or stream error already in flight. */
3534export async function closePublicNetworkDispatcher (
3635dispatcher : PublicNetworkDispatcher | undefined ,
@@ -48,68 +47,6 @@ export async function closePublicNetworkDispatcher(
4847} )
4948}
5049
51- function parseIPv4 ( hostname : string ) : [ number , number , number , number ] | undefined {
52- const parts = hostname . split ( '.' )
53- if ( parts . length !== 4 || parts . some ( part => ! / ^ \d { 1 , 3 } $ / . test ( part ) ) )
54- return
55- const octets = parts . map ( Number )
56- return octets . every ( octet => octet >= 0 && octet <= 255 )
57- ? octets as [ number , number , number , number ]
58- : undefined
59- }
60-
61- function isPublicIPv4 ( [ a , b , c ] : [ number , number , number , number ] ) : boolean {
62- return a !== 0
63- && a !== 10
64- && a !== 127
65- && ! ( a === 100 && b >= 64 && b <= 127 )
66- && ! ( a === 169 && b === 254 )
67- && ! ( a === 172 && b >= 16 && b <= 31 )
68- && ! ( a === 192 && b === 0 && c === 0 )
69- && ! ( a === 192 && b === 0 && c === 2 )
70- && ! ( a === 192 && b === 88 && c === 99 )
71- && ! ( a === 192 && b === 168 )
72- && ! ( a === 198 && ( b === 18 || b === 19 ) )
73- && ! ( a === 198 && b === 51 && c === 100 )
74- && ! ( a === 203 && b === 0 && c === 113 )
75- && a < 224
76- }
77-
78- function isPublicIPv6 ( hostname : string ) : boolean {
79- const groups = hostname . split ( ':' )
80- const firstGroup = Number . parseInt ( groups [ 0 ] || '0' , 16 )
81- if ( ! Number . isInteger ( firstGroup ) || firstGroup < 0x2000 || firstGroup > 0x3FFF )
82- return false
83- // Documentation, Teredo, and 6to4 ranges can encode or route to non-public targets.
84- const secondGroup = Number . parseInt ( groups [ 1 ] || '0' , 16 )
85- return ! ( firstGroup === 0x2001 && ( secondGroup === 0 || secondGroup === 0xDB8 ) )
86- && firstGroup !== 0x2002
87- }
88-
89- /** Reject hostnames that directly address local, private, link-local, or reserved networks. */
90- export function isPublicNetworkHostname ( input : string ) : boolean {
91- const hostname = input
92- . trim ( )
93- . toLowerCase ( )
94- . replace ( / ^ \[ | \] $ / g, '' )
95- . split ( '%' , 1 ) [ 0 ] !
96- . replace ( / \. $ / , '' )
97- if ( ! hostname )
98- return false
99-
100- const ipv4 = parseIPv4 ( hostname )
101- if ( ipv4 )
102- return isPublicIPv4 ( ipv4 )
103- if ( hostname . includes ( ':' ) )
104- return isPublicIPv6 ( hostname )
105-
106- const labels = hostname . split ( '.' )
107- if ( labels . length < 2 )
108- return false
109- const suffix = labels . at ( - 1 ) !
110- return ! LOCAL_HOST_SUFFIXES . includes ( suffix )
111- }
112-
11350/** Resolve once inside the socket connection, reject mixed/private answers, then pin the selected address. */
11451export function createPublicNetworkLookup ( resolveHostname : ResolveNetworkHostname ) : LookupFunction {
11552const networkLookup : NetworkLookup = ( hostname , options , callback ) => {
@@ -142,33 +79,9 @@ export function createPublicNetworkLookup(resolveHostname: ResolveNetworkHostnam
14279return networkLookup as LookupFunction
14380}
14481
145- /** Create a Node fetch dispatcher whose socket lookup validates and pins every DNS answer. */
82+ /** Create a host fetch implementation that validates and pins every DNS answer when Node permits it . */
14683export async function createPublicNetworkDispatcher ( resolveHostnameOverride ?: ResolveNetworkHostname ) : Promise < PublicNetworkDispatcher > {
147- if ( runtime !== 'node' )
148- return { fetch : globalThis . fetch , close : async ( ) => { } }
149-
150- // Loaded only on Node. Other runtimes keep their platform fetch behavior;
151- // call sites still reject direct non-public hostnames before fetching.
152- const { Agent, fetch } = await import ( 'undici' )
153- let resolveHostname = resolveHostnameOverride
154- if ( ! resolveHostname ) {
155- const { lookup } = await import ( 'node:dns' )
156- resolveHostname = ( hostname , callback ) => lookup ( hostname , { all : true , verbatim : true } , ( error , addresses ) => {
157- callback ( error , addresses as NetworkAddress [ ] )
158- } )
159- }
160- const dispatcher = new Agent ( {
161- connect : {
162- lookup : createPublicNetworkLookup ( resolveHostname ) ,
163- } ,
164- } )
165- return {
166- fetch : ( ( input , init ) => fetch ( input as string | URL , {
167- ...( init as unknown as NonNullable < Parameters < typeof fetch > [ 1 ] > ) ,
168- dispatcher,
169- } ) as unknown as Promise < Response > ) as typeof globalThis . fetch ,
170- close : ( ) => dispatcher . close ( ) ,
171- }
84+ return createNetworkDispatcher ( createPublicNetworkLookup , resolveHostnameOverride )
17285}
17386
17487/** Detect the tagged DNS policy error through fetch/ofetch cause wrappers. */
0 commit comments