@@ -39,12 +39,58 @@ const PRIVATE_NETWORK_RE =
3939/** Loopback addresses (127.x, localhost). */
4040const LOOPBACK_RE = / ^ ( 1 2 7 \. | l o c a l h o s t ) / i;
4141
42+ /** sslip.io-style hostname under local.eca.dev with an embedded IP (e.g. 192-168-1-1.local.eca.dev). */
43+ const SSLIP_LOCAL_RE =
44+ / ^ ( \d { 1 , 3 } ) - ( \d { 1 , 3 } ) - ( \d { 1 , 3 } ) - ( \d { 1 , 3 } ) \. l o c a l \. e c a \. d e v $ / i;
45+
46+ const SSLIP_DOMAIN = 'local.eca.dev' ;
47+
48+ /**
49+ * Extract the embedded IP from an sslip.io-style hostname.
50+ * E.g. "192-168-15-17.local.eca.dev" → "192.168.15.17"
51+ * Returns undefined if the hostname doesn't match.
52+ */
53+ export function extractSslipIp ( host : string ) : string | undefined {
54+ const match = SSLIP_LOCAL_RE . exec ( host ) ;
55+ if ( ! match ) return undefined ;
56+ return `${ match [ 1 ] } .${ match [ 2 ] } .${ match [ 3 ] } .${ match [ 4 ] } ` ;
57+ }
58+
59+ /**
60+ * Convert a raw IP to its sslip.io-style hostname under local.eca.dev.
61+ * E.g. "192.168.15.17" → "192-168-15-17.local.eca.dev"
62+ *
63+ * If `hostWithPort` contains a port (e.g. "192.168.1.42:7777"), the port
64+ * is preserved: "192-168-1-42.local.eca.dev:7777".
65+ *
66+ * Returns the input unchanged if it's not a raw IPv4 address.
67+ */
68+ export function ipToSslipHostname ( hostWithPort : string ) : string {
69+ const [ hostPart , port ] = hostWithPort . split ( ':' ) ;
70+ if ( ! / ^ \d { 1 , 3 } \. \d { 1 , 3 } \. \d { 1 , 3 } \. \d { 1 , 3 } $ / . test ( hostPart ) ) return hostWithPort ;
71+ const sslipHost = `${ hostPart . replace ( / \. / g, '-' ) } .${ SSLIP_DOMAIN } ` ;
72+ return port ? `${ sslipHost } :${ port } ` : sslipHost ;
73+ }
74+
75+ /**
76+ * True when `host` is a raw private/loopback IPv4 (not already an sslip hostname).
77+ * Useful to decide whether to transform a user-entered host to sslip form.
78+ */
79+ export function isRawPrivateIp ( host : string ) : boolean {
80+ const hostPart = host . split ( ':' ) [ 0 ] ;
81+ return ( PRIVATE_NETWORK_RE . test ( hostPart ) || LOOPBACK_RE . test ( hostPart ) )
82+ && ! SSLIP_LOCAL_RE . test ( hostPart ) ;
83+ }
84+
4285/**
4386 * Returns true when `host` targets a private/local network address.
87+ * Also recognises sslip.io-style hostnames under *.local.eca.dev
88+ * that embed a private IP (e.g. 192-168-15-17.local.eca.dev).
4489 * Used for protocol defaults and Chrome Local Network Access hints.
4590 */
4691export function isLocalNetworkHost ( host : string ) : boolean {
47- return PRIVATE_NETWORK_RE . test ( host ) || LOOPBACK_RE . test ( host ) ;
92+ const resolved = extractSslipIp ( host ) ?? host ;
93+ return PRIVATE_NETWORK_RE . test ( resolved ) || LOOPBACK_RE . test ( resolved ) ;
4894}
4995
5096/**
@@ -55,30 +101,42 @@ export function isLocalNetworkHost(host: string): boolean {
55101 * - `"private"` — RFC 1918 (10.x, 172.16-31.x, 192.168.x)
56102 * - `undefined` — public / not applicable
57103 *
104+ * Also handles sslip.io-style *.local.eca.dev hostnames by extracting
105+ * the embedded IP before classification.
106+ *
58107 * Chrome validates that the resolved IP matches the declared space;
59108 * a mismatch causes the request to fail.
60109 */
61110export function targetAddressSpace ( host : string ) : string | undefined {
62- if ( LOOPBACK_RE . test ( host ) ) return 'local' ;
63- if ( PRIVATE_NETWORK_RE . test ( host ) ) return 'private' ;
111+ const resolved = extractSslipIp ( host ) ?? host ;
112+ if ( LOOPBACK_RE . test ( resolved ) ) return 'local' ;
113+ if ( PRIVATE_NETWORK_RE . test ( resolved ) ) return 'private' ;
64114 return undefined ;
65115}
66116
67117/**
68118 * Resolve the HTTP protocol for a given host string.
69119 * When an explicit protocol is provided it is used as-is;
70- * otherwise private/loopback addresses → http, everything else → https.
120+ * otherwise defaults to HTTPS (ECA servers support TLS for private IPs
121+ * via *.local.eca.dev wildcard certs).
71122 */
72123export function resolveProtocol ( host : string , protocol ?: Protocol ) : Protocol {
73124 if ( protocol ) return protocol ;
74- return isLocalNetworkHost ( host ) ? 'http' : 'https' ;
125+ return 'https' ;
75126}
76127
77128/**
78129 * Build the base API URL for a host, e.g. "https://myhost:7888/api/v1".
130+ *
131+ * When connecting over HTTPS to a raw private IP, automatically rewrites
132+ * the host to its sslip.io hostname so the TLS certificate matches.
79133 */
80134export function resolveBaseUrl ( host : string , protocol ?: Protocol ) : string {
81- return `${ resolveProtocol ( host , protocol ) } ://${ host } /api/v1` ;
135+ const proto = resolveProtocol ( host , protocol ) ;
136+ const effectiveHost = proto === 'https' && isRawPrivateIp ( host )
137+ ? ipToSslipHostname ( host )
138+ : host ;
139+ return `${ proto } ://${ effectiveHost } /api/v1` ;
82140}
83141
84142/**
0 commit comments