Skip to content

Commit b30984f

Browse files
committed
Support private ips
1 parent c310bd2 commit b30984f

3 files changed

Lines changed: 50 additions & 8 deletions

File tree

src/bridge/api.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {
1515
SessionResponse,
1616
} from './types';
1717
import type { Protocol } from './utils';
18-
import { resolveBaseUrl } from './utils';
18+
import { localNetworkFetchOptions, resolveBaseUrl } from './utils';
1919

2020
export class EcaRemoteApi {
2121
private baseUrl: string;
@@ -57,7 +57,9 @@ export class EcaRemoteApi {
5757
const { method = 'GET', body, auth = true, allowStatus = [] } = options;
5858
const hasBody = body !== undefined;
5959

60-
const res = await fetch(`${this.baseUrl}${path}`, {
60+
const url = `${this.baseUrl}${path}`;
61+
const res = await fetch(url, {
62+
...localNetworkFetchOptions(url),
6163
method,
6264
headers: auth ? this.headers(hasBody) : (hasBody ? { 'Content-Type': 'application/json' } : undefined),
6365
...(hasBody ? { body: JSON.stringify(body) } : {}),

src/bridge/sse.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
* the client assumes the connection is dead and triggers onDisconnect.
1010
*/
1111

12+
import { localNetworkFetchOptions } from './utils';
13+
1214
export interface SSEEvent {
1315
event: string;
1416
data: string;
@@ -65,6 +67,7 @@ export class SSEClient {
6567
this.abortController = new AbortController();
6668

6769
const response = await fetch(this.url, {
70+
...localNetworkFetchOptions(this.url),
6871
headers: { 'Authorization': `Bearer ${this.password}` },
6972
signal: this.abortController.signal,
7073
});

src/bridge/utils.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,26 @@
88

99
export type Protocol = 'http' | 'https';
1010

11+
/** RFC 1918 + loopback regex — matches private/local network hosts. */
12+
const LOCAL_NETWORK_RE =
13+
/^(192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.|127\.|localhost)/i;
14+
15+
/**
16+
* Returns true when `host` targets a private/local network address.
17+
* Used for protocol defaults and Chrome Local Network Access hints.
18+
*/
19+
export function isLocalNetworkHost(host: string): boolean {
20+
return LOCAL_NETWORK_RE.test(host);
21+
}
22+
1123
/**
1224
* Resolve the HTTP protocol for a given host string.
1325
* When an explicit protocol is provided it is used as-is;
14-
* otherwise localhost / 127.0.0.1 → http, everything else → https.
26+
* otherwise private/loopback addresses → http, everything else → https.
1527
*/
1628
export function resolveProtocol(host: string, protocol?: Protocol): Protocol {
1729
if (protocol) return protocol;
18-
if (host.startsWith('localhost') || host.startsWith('127.0.0.1')) {
19-
return 'http';
20-
}
21-
return 'https';
30+
return isLocalNetworkHost(host) ? 'http' : 'https';
2231
}
2332

2433
/**
@@ -28,12 +37,36 @@ export function resolveBaseUrl(host: string, protocol?: Protocol): string {
2837
return `${resolveProtocol(host, protocol)}://${host}/api/v1`;
2938
}
3039

40+
/**
41+
* Build extra fetch options for Chrome Local Network Access (LNA).
42+
*
43+
* When the target URL points to a private/local address, returns
44+
* `{ targetAddressSpace: "local" }` so Chrome surfaces its LNA
45+
* permission prompt instead of silently blocking the request.
46+
*
47+
* @see https://developer.chrome.com/blog/local-network-access
48+
*/
49+
export function localNetworkFetchOptions(url: string): RequestInit {
50+
try {
51+
const host = new URL(url).hostname;
52+
if (isLocalNetworkHost(host)) {
53+
return { targetAddressSpace: 'local' } as RequestInit;
54+
}
55+
} catch {
56+
// invalid URL — ignore
57+
}
58+
return {};
59+
}
60+
3161
/**
3262
* Fetch with an abort-based timeout.
3363
*
3464
* Wraps the standard `fetch` and aborts the request if it exceeds
3565
* `timeoutMs` milliseconds. The AbortError can be caught upstream
3666
* to show a user-friendly timeout message.
67+
*
68+
* Automatically adds `targetAddressSpace: "local"` for private-network
69+
* URLs to cooperate with Chrome's Local Network Access restrictions.
3770
*/
3871
export async function fetchWithTimeout(
3972
url: string,
@@ -43,7 +76,11 @@ export async function fetchWithTimeout(
4376
const controller = new AbortController();
4477
const timer = setTimeout(() => controller.abort(), timeoutMs);
4578
try {
46-
return await fetch(url, { ...init, signal: controller.signal });
79+
return await fetch(url, {
80+
...localNetworkFetchOptions(url),
81+
...init,
82+
signal: controller.signal,
83+
});
4784
} finally {
4885
clearTimeout(timer);
4986
}

0 commit comments

Comments
 (0)