Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ resolution.

| | config source | PAC + WPAD | change signal |
|---|---|---|---|
| **Windows** | `WinHttpGetIEProxyConfigForCurrentUser` | selected embedded backend + DNS WPAD; WinHTTP fallback when backend-less | registry change notification |
| **Windows** | `WinHttpGetIEProxyConfigForCurrentUser` | selected embedded backend + DHCP/DNS WPAD; WinHTTP fallback when backend-less | registry change notification |
| **macOS** | `SCDynamicStoreCopyProxies` | built-in [QuickJS] PAC engine + DNS WPAD | `SCDynamicStore` callback |
| **Linux** | GNOME `org.gnome.system.proxy` via `gsettings` | built-in [QuickJS] PAC engine + DNS WPAD | `dconf watch` / `gsettings monitor` |

Expand Down Expand Up @@ -95,9 +95,9 @@ backend-less Windows build is valid and uses WinHTTP for PAC and WPAD
resolution. The PAC helper functions are first-party JavaScript implemented
from the public PAC specification.

Non-goals: DHCP-based WPAD (option 252) when using an embedded backend or on
macOS/Linux, KDE proxy settings, proxy authentication credentials. A
backend-less Windows build gets DHCP WPAD through WinHTTP.
Non-goals: DHCP-based WPAD (option 252) on macOS/Linux, KDE proxy settings,
proxy authentication credentials. Windows probes DHCP before DNS for both
embedded and WinHTTP-backed resolution.

## The PAC cage

Expand Down Expand Up @@ -183,16 +183,18 @@ if let Some(pac) = config.pac {
}
```

The snapshot includes normalized static HTTP/HTTPS/SOCKS rules and, when the
The snapshot includes raw values for configured `http_proxy`, `https_proxy`,
`all_proxy`, and `no_proxy` variables (unset variables are omitted), normalized
static HTTP/HTTPS/SOCKS rules, and, when the
native source was available, source-specific settings from WinHTTP,
SystemConfiguration, or GNOME GSettings. Proxy environment variables are not
included. If auto-detection is enabled, the call performs DNS WPAD discovery
first; if no usable `wpad.dat` is found, it loads the configured PAC URL. PAC
loading is best-effort and synchronous, using the timeouts in `ResolverOptions`.
The returned script includes its configured or discovered URL and is never
evaluated. Windows DNS WPAD uses adapter DNS suffixes. DHCP option 252 is only
available for URL resolution in a backend-less Windows build through WinHTTP;
it is not queried by this inspection API.
SystemConfiguration, or GNOME GSettings. DHCP WPAD, DNS WPAD, and the configured
PAC URL are all inspected;
their `disabled`, `unsupported`, `unconfigured`, `not-found`, `available`,
`error-discovery`, or `error-download` status is retained with URL/error
details. PAC loading is best-effort and synchronous, using the timeouts in
`ResolverOptions`. The selected script is the first available by precedence
(DHCP, DNS, configured), reports `WpadDhcp`, `WpadDns`, or `Configured`, and is
never evaluated. Windows DNS WPAD uses adapter DNS suffixes.

## Bad-proxy feedback

Expand Down
66 changes: 59 additions & 7 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,60 @@ export interface Proxy {
}

/** How a PAC script was selected. */
export type PacScriptSource = 'wpad' | 'configured' | 'unknown';
export type PacScriptSource = 'wpad-dns' | 'wpad-dhcp' | 'configured' | 'unknown';

/** A PAC script loaded from an OS setting or DNS WPAD, but not evaluated. */
/** A PAC script loaded from an OS setting or WPAD, but not evaluated. */
export interface PacScript {
/** The configured or discovered URL from which {@link content} was loaded. */
url: string;
/** The PAC JavaScript source. */
content: string;
/** Whether the script came from DNS WPAD or an explicit OS setting. */
/** Whether the script came from DNS/DHCP WPAD or an explicit OS setting. */
source: PacScriptSource;
}

/** Result of inspecting one possible PAC source. */
export type PacSourceState =
| 'disabled'
| 'unsupported'
| 'unconfigured'
| 'not-found'
| 'available'
| 'error-discovery'
| 'error-download'
| 'unknown';

/** Diagnostics for one possible PAC source. */
export interface PacSourceStatus {
state: PacSourceState;
/** Discovered or configured URL, when known. */
url?: string;
/** Discovery or download error detail. May contain platform/network data. */
error?: string;
}

/** Diagnostics for one effective proxy environment variable. */
export interface EnvironmentVariableStatus {
/** Effective spelling, for example `https_proxy` or `HTTPS_PROXY`. */
variable: string;
/** Raw environment value. May contain credentials. */
value: string;
/** Present when the raw value cannot be used as a proxy setting. */
error?: string;
}

/**
* Supported proxy environment variables captured when the resolver was
* constructed. Unset variables are omitted. Windows matches names
* case-insensitively; Unix prefers lowercase names over uppercase aliases.
*/
export interface EnvironmentProxyConfig {
httpProxy?: EnvironmentVariableStatus;
httpsProxy?: EnvironmentVariableStatus;
allProxy?: EnvironmentVariableStatus;
noProxy?: EnvironmentVariableStatus;
}

/** Normalized static proxy settings read from the operating system. */
export interface StaticProxyRules {
/** Proxy for HTTP and WebSocket requests. */
Expand Down Expand Up @@ -76,12 +118,20 @@ export type PlatformProxyConfig = WindowsProxyConfig | MacosProxyConfig | LinuxP

/** A snapshot of the current operating-system proxy configuration. */
export interface ProxyConfig {
/** Captured `http_proxy`, `https_proxy`, `all_proxy`, and `no_proxy` settings. */
environment: EnvironmentProxyConfig;
/** Whether the operating system requested automatic proxy discovery. */
autoDetect: boolean;
/** The configured PAC URL, even if the script could not be loaded. */
pacUrl?: string;
/** The first PAC script available by resolution precedence. */
pac?: PacScript;
/** DHCP WPAD status. Unsupported on non-Windows platforms. */
wpadDhcp: PacSourceStatus;
/** DNS WPAD status. */
wpadDns: PacSourceStatus;
/** Explicitly configured PAC status. */
configuredPac: PacSourceStatus;
/** Normalized static proxy settings, if configured. */
staticRules?: StaticProxyRules;
/** Raw source-specific settings, if the native source was available. */
Expand Down Expand Up @@ -114,8 +164,10 @@ export declare class ProxyResolver {
/**
* Reads the operating-system proxy configuration without evaluating PAC.
*
* Proxy environment variables are not included. If auto-detection is
* enabled, DNS WPAD discovery runs before the configured PAC URL is loaded.
* Includes proxy environment variables captured when this resolver was
* constructed. DHCP WPAD, DNS WPAD, and the configured PAC URL are inspected
* independently. {@link ProxyConfig.pac} contains the first available script
* by precedence (DHCP before DNS on Windows, then configured PAC).
* Potentially blocking OS, DNS, and network work runs outside the JavaScript
* event loop.
*/
Expand Down Expand Up @@ -167,7 +219,7 @@ export declare class ProxyResolver {
export declare function resolveProxy(url: string): Promise<Proxy[]>;

/**
* Reads the operating-system proxy configuration using a process-wide
* {@link ProxyResolver}. PAC scripts are loaded but never evaluated.
* Reads proxy environment and operating-system configuration using a
* process-wide {@link ProxyResolver}. PAC scripts are loaded but never evaluated.
*/
export declare function readProxyConfig(): Promise<ProxyConfig>;
77 changes: 75 additions & 2 deletions npm/native/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
use napi::{Env, Error, JsFunction, JsUnknown, Result, Status};
use napi_derive::napi;
use os_proxy_resolver::{
PacScriptSource, PlatformProxyConfig, ProxyKind, StaticProxyRules, Subscription,
EnvironmentProxyConfig, EnvironmentVariableStatus, PacScriptSource, PacSourceState,
PacSourceStatus, PlatformProxyConfig, ProxyKind, StaticProxyRules, Subscription,
};

#[napi(object)]
Expand Down Expand Up @@ -71,6 +72,69 @@ pub struct NodePacScript {
pub source: String,
}

#[napi(object)]
pub struct NodePacSourceStatus {
pub state: String,
pub url: Option<String>,
pub error: Option<String>,
}

impl From<PacSourceStatus> for NodePacSourceStatus {
fn from(status: PacSourceStatus) -> Self {
Self {
state: match status.state {
PacSourceState::Disabled => "disabled",
PacSourceState::Unsupported => "unsupported",
PacSourceState::Unconfigured => "unconfigured",
PacSourceState::NotFound => "not-found",
PacSourceState::Available => "available",
PacSourceState::ErrorDiscovery => "error-discovery",
PacSourceState::ErrorDownload => "error-download",
_ => "unknown",
}
.into(),
url: status.url,
error: status.error,
}
}
}

#[napi(object)]
pub struct NodeEnvironmentVariableStatus {
pub variable: String,
pub value: String,
pub error: Option<String>,
}

impl From<EnvironmentVariableStatus> for NodeEnvironmentVariableStatus {
fn from(status: EnvironmentVariableStatus) -> Self {
Self {
variable: status.variable,
value: status.value,
error: status.error,
}
}
}

#[napi(object)]
pub struct NodeEnvironmentProxyConfig {
pub http_proxy: Option<NodeEnvironmentVariableStatus>,
pub https_proxy: Option<NodeEnvironmentVariableStatus>,
pub all_proxy: Option<NodeEnvironmentVariableStatus>,
pub no_proxy: Option<NodeEnvironmentVariableStatus>,
}

impl From<EnvironmentProxyConfig> for NodeEnvironmentProxyConfig {
fn from(config: EnvironmentProxyConfig) -> Self {
Self {
http_proxy: config.http_proxy.map(Into::into),
https_proxy: config.https_proxy.map(Into::into),
all_proxy: config.all_proxy.map(Into::into),
no_proxy: config.no_proxy.map(Into::into),
}
}
}

#[napi(object)]
pub struct NodeStaticProxyRules {
pub http: Option<Proxy>,
Expand Down Expand Up @@ -144,28 +208,37 @@ impl From<PlatformProxyConfig> for NodePlatformProxyConfig {

#[napi(object)]
pub struct NodeProxyConfig {
pub environment: NodeEnvironmentProxyConfig,
pub auto_detect: bool,
pub pac_url: Option<String>,
pub pac: Option<NodePacScript>,
pub wpad_dhcp: NodePacSourceStatus,
pub wpad_dns: NodePacSourceStatus,
pub configured_pac: NodePacSourceStatus,
pub static_rules: Option<NodeStaticProxyRules>,
pub platform: Option<NodePlatformProxyConfig>,
}

impl From<os_proxy_resolver::ProxyConfig> for NodeProxyConfig {
fn from(config: os_proxy_resolver::ProxyConfig) -> Self {
Self {
environment: config.environment.into(),
auto_detect: config.auto_detect,
pac_url: config.pac_url,
pac: config.pac.map(|pac| NodePacScript {
url: pac.url,
content: pac.content,
source: match pac.source {
PacScriptSource::Wpad => "wpad",
PacScriptSource::WpadDns => "wpad-dns",
PacScriptSource::WpadDhcp => "wpad-dhcp",
PacScriptSource::Configured => "configured",
_ => "unknown",
}
.into(),
}),
wpad_dhcp: config.wpad_dhcp.into(),
wpad_dns: config.wpad_dns.into(),
configured_pac: config.configured_pac.into(),
static_rules: config.static_rules.map(NodeStaticProxyRules::from),
platform: config.platform.map(NodePlatformProxyConfig::from),
}
Expand Down
13 changes: 12 additions & 1 deletion npm/test/smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,22 @@ async function main() {
assert.strictEqual(typeof resolver.readProxyConfig, 'function');
assert.strictEqual(typeof resolver.configGeneration, 'number');
const config = await resolver.readProxyConfig();
for (const status of Object.values(config.environment)) {
assert.strictEqual(typeof status.variable, 'string');
assert.strictEqual(typeof status.value, 'string');
}
assert.strictEqual(typeof config.autoDetect, 'boolean');
for (const status of [config.wpadDhcp, config.wpadDns, config.configuredPac]) {
assert.strictEqual(typeof status.state, 'string');
assert.ok([
'disabled', 'unsupported', 'unconfigured', 'not-found', 'available',
'error-discovery', 'error-download', 'unknown',
].includes(status.state));
}
if (config.pac) {
assert.strictEqual(typeof config.pac.url, 'string');
assert.strictEqual(typeof config.pac.content, 'string');
assert.ok(['wpad', 'configured', 'unknown'].includes(config.pac.source));
assert.ok(['wpad-dns', 'wpad-dhcp', 'configured', 'unknown'].includes(config.pac.source));
}
if (config.platform) {
assert.ok(['windows', 'macos', 'linux', 'unknown'].includes(config.platform.kind));
Expand Down
Loading
Loading