Skip to content

Commit e430e50

Browse files
committed
add cross-platform proxy crate with macOS backend and docs
1 parent 87ea5c1 commit e430e50

File tree

9 files changed

+820
-0
lines changed

9 files changed

+820
-0
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/proxy/Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "robius-proxy"
3+
version.workspace = true
4+
edition.workspace = true
5+
authors = [
6+
"Tyrese Luo <tyreseluo@outlook.com>",
7+
"Project Robius Maintainers",
8+
]
9+
homepage.workspace = true
10+
categories.workspace = true
11+
license.workspace = true
12+
readme = "README.md"
13+
repository.workspace = true
14+
15+
[dependencies]
16+
cfg-if.workspace = true
17+
log = { workspace = true, optional = true }
18+
19+
[target.'cfg(target_os = "macos")'.dependencies]
20+
objc2-app-kit = { workspace = true, features = ["NSWorkspace"] }
21+
objc2-foundation = { workspace = true, features = ["NSString", "NSURL"] }

crates/proxy/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# `robius-proxy`
2+
3+
This crate provides easy Rust interfaces to get and apply proxy across multiple platforms, including:
4+
5+
- Modes: direct, manual (HTTP/HTTPS/SOCKS), PAC URL, WPAD.
6+
- Platforms:
7+
1. **macOS** : via `networksetup`/`scutil`; others return
8+
2. Others(Planned): `Error::Unsupported` until added.
9+
10+
## Usage
11+
12+
```rust
13+
use robius_proxy::{BypassList, ProxyEndpoint, ProxyManager, ProxyMode, ProxySettings, ProxyState};
14+
15+
let manager = ProxyManager::new()?;
16+
let current = manager.current()?;
17+
18+
println!("Current mode: {:?}", current.mode);
19+
20+
let state = ProxyState::manual(ProxySettings {
21+
http: Some(ProxyEndpoint::new("proxy.local", 8080)),
22+
https: None,
23+
socks: None,
24+
bypass: BypassList::new(vec!["localhost".into(), "127.0.0.1".into()]),
25+
});
26+
27+
manager.apply(state)?;
28+
```
29+
30+
## Examples
31+
32+
- Show current configuration: `cargo run -p robius-proxy --example show_current`
33+
- Set manual proxies (macOS): `cargo run -p robius-proxy --example set_manual`
34+
35+
Examples invoke `networksetup` under the hood, so they may prompt for system
36+
permissions depending on your macOS configuration.
37+
38+
## Platform support
39+
40+
- macOS: implemented via `networksetup` and `scutil`.
41+
- Linux (planned): reserve hooks for NetworkManager, GNOME proxy settings, and
42+
system-wide environment exports.
43+
- Windows (planned): reserve hooks for WinHTTP/WinINET and per-connection
44+
proxy configuration.
45+
- Android (planned): reserve hooks for global HTTP proxy settings available to
46+
device owner apps.
47+
- iOS (planned): reserve hooks for CFNetwork proxy dictionaries.
48+
- OpenHarmony (planned): reserve hooks for system proxy APIs when exposed.
49+
50+
Contributions adding new platforms or improving parsing/validation are welcome.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use robius_proxy::{ProxyManager, ProxyMode, ProxyState};
2+
3+
fn main() -> Result<(), Box<dyn std::error::Error>> {
4+
let manager = ProxyManager::new()?;
5+
let state = manager.current()?;
6+
7+
println!("Current proxy configuration:");
8+
print_state(&state);
9+
10+
Ok(())
11+
}
12+
13+
fn print_state(state: &ProxyState) {
14+
match &state.mode {
15+
ProxyMode::Direct => println!(" mode: Direct (no proxy)"),
16+
ProxyMode::Manual(settings) => {
17+
println!(" mode: Manual");
18+
if let Some(http) = &settings.http {
19+
println!(" http: {}:{}", http.host, http.port);
20+
}
21+
if let Some(https) = &settings.https {
22+
println!(" https: {}:{}", https.host, https.port);
23+
}
24+
if let Some(socks) = &settings.socks {
25+
println!(" socks: {}:{}", socks.host, socks.port);
26+
}
27+
if settings.bypass.entries.is_empty() {
28+
println!(" bypass: (none)");
29+
} else {
30+
println!(" bypass:");
31+
for entry in &settings.bypass.entries {
32+
println!(" - {entry}");
33+
}
34+
}
35+
}
36+
ProxyMode::AutoConfigUrl { url, bypass } => {
37+
println!(" mode: PAC url");
38+
println!(" url: {url}");
39+
if bypass.entries.is_empty() {
40+
println!(" bypass: (none)");
41+
} else {
42+
println!(" bypass:");
43+
for entry in &bypass.entries {
44+
println!(" - {entry}");
45+
}
46+
}
47+
}
48+
ProxyMode::AutoDiscovery { bypass } => {
49+
println!(" mode: Auto-discovery (WPAD)");
50+
if bypass.entries.is_empty() {
51+
println!(" bypass: (none)");
52+
} else {
53+
println!(" bypass:");
54+
for entry in &bypass.entries {
55+
println!(" - {entry}");
56+
}
57+
}
58+
}
59+
}
60+
}

crates/proxy/src/error.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use std::{fmt, io};
2+
3+
/// Result alias for the proxy crate.
4+
pub type Result<T> = std::result::Result<T, Error>;
5+
6+
/// Errors that can arise while interacting with system proxy settings.
7+
#[derive(Debug)]
8+
pub enum Error {
9+
/// The operation is not supported on this platform.
10+
Unsupported,
11+
/// Input data was invalid for the target platform.
12+
InvalidInput(&'static str),
13+
/// A command failed to execute successfully.
14+
CommandFailed {
15+
command: String,
16+
stderr: String,
17+
},
18+
/// Parsing system proxy data failed.
19+
Parse(String),
20+
/// I/O failure while invoking platform facilities.
21+
Io(io::Error),
22+
}
23+
24+
impl From<io::Error> for Error {
25+
fn from(value: io::Error) -> Self {
26+
Self::Io(value)
27+
}
28+
}
29+
30+
impl fmt::Display for Error {
31+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32+
match self {
33+
Self::Unsupported => write!(f, "operation not supported on this platform"),
34+
Self::InvalidInput(reason) => write!(f, "invalid proxy input: {reason}"),
35+
Self::CommandFailed { command, stderr } => {
36+
write!(f, "command `{command}` failed: {stderr}")
37+
}
38+
Self::Parse(reason) => write!(f, "failed to parse proxy data: {reason}"),
39+
Self::Io(e) => write!(f, "io error: {e}"),
40+
}
41+
}
42+
}
43+
44+
impl std::error::Error for Error {}

crates/proxy/src/lib.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//! Cross-platform access to system proxy settings.
2+
//!
3+
//! The API is intentionally small and capability-oriented: you describe the
4+
//! desired [`ProxyState`] and the crate applies it using the native facilities
5+
//! of the underlying platform. Only macOS is implemented right now; other
6+
//! platforms fall back to [`Error::Unsupported`].
7+
//!
8+
//! The abstractions are designed to be forward-compatible with additional
9+
//! platforms such as Linux, Windows, Android, iOS, and OpenHarmony.
10+
11+
mod error;
12+
mod sys;
13+
14+
pub use crate::error::{Error, Result};
15+
16+
/// Uniform representation of a proxy endpoint.
17+
#[derive(Clone, Debug, PartialEq, Eq)]
18+
pub struct ProxyEndpoint {
19+
/// Hostname or IP of the proxy.
20+
pub host: String,
21+
/// TCP port on which the proxy listens.
22+
pub port: u16,
23+
/// Optional credentials when the proxy requires authentication.
24+
pub credentials: Option<Credentials>,
25+
}
26+
27+
impl ProxyEndpoint {
28+
pub fn new(host: impl Into<String>, port: u16) -> Self {
29+
Self {
30+
host: host.into(),
31+
port,
32+
credentials: None,
33+
}
34+
}
35+
36+
pub fn with_credentials(
37+
host: impl Into<String>,
38+
port: u16,
39+
username: impl Into<String>,
40+
password: Option<String>,
41+
) -> Self {
42+
Self {
43+
host: host.into(),
44+
port,
45+
credentials: Some(Credentials {
46+
username: username.into(),
47+
password,
48+
}),
49+
}
50+
}
51+
}
52+
53+
/// Credentials for proxies that require authentication.
54+
#[derive(Clone, Debug, PartialEq, Eq)]
55+
pub struct Credentials {
56+
pub username: String,
57+
pub password: Option<String>,
58+
}
59+
60+
/// List of hosts, domains, or CIDRs that should bypass the proxy.
61+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
62+
pub struct BypassList {
63+
pub entries: Vec<String>,
64+
}
65+
66+
impl BypassList {
67+
pub fn new(entries: impl Into<Vec<String>>) -> Self {
68+
Self {
69+
entries: entries.into(),
70+
}
71+
}
72+
}
73+
74+
/// Proxy configuration for manual modes.
75+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
76+
pub struct ProxySettings {
77+
pub http: Option<ProxyEndpoint>,
78+
pub https: Option<ProxyEndpoint>,
79+
pub socks: Option<ProxyEndpoint>,
80+
pub bypass: BypassList,
81+
}
82+
83+
/// Modes supported by the abstraction layer.
84+
#[derive(Clone, Debug, PartialEq, Eq)]
85+
pub enum ProxyMode {
86+
/// No proxy usage.
87+
Direct,
88+
/// User-specified endpoints.
89+
Manual(ProxySettings),
90+
/// Proxy auto-configuration (PAC) script URL.
91+
AutoConfigUrl {
92+
url: String,
93+
bypass: BypassList,
94+
},
95+
/// WPAD / automatic discovery.
96+
AutoDiscovery {
97+
bypass: BypassList,
98+
},
99+
}
100+
101+
/// High-level proxy state exposed to callers.
102+
#[derive(Clone, Debug, PartialEq, Eq)]
103+
pub struct ProxyState {
104+
pub mode: ProxyMode,
105+
}
106+
107+
impl ProxyState {
108+
pub fn direct() -> Self {
109+
Self {
110+
mode: ProxyMode::Direct,
111+
}
112+
}
113+
114+
pub fn manual(settings: ProxySettings) -> Self {
115+
Self {
116+
mode: ProxyMode::Manual(settings),
117+
}
118+
}
119+
120+
pub fn pac(url: impl Into<String>, bypass: BypassList) -> Self {
121+
Self {
122+
mode: ProxyMode::AutoConfigUrl {
123+
url: url.into(),
124+
bypass,
125+
},
126+
}
127+
}
128+
129+
pub fn auto_discovery(bypass: BypassList) -> Self {
130+
Self {
131+
mode: ProxyMode::AutoDiscovery { bypass },
132+
}
133+
}
134+
}
135+
136+
/// Entry point for interacting with system proxy settings.
137+
pub struct ProxyManager {
138+
inner: sys::Manager,
139+
}
140+
141+
impl ProxyManager {
142+
/// Constructs a manager bound to the current platform.
143+
pub fn new() -> Result<Self> {
144+
Ok(Self {
145+
inner: sys::Manager::new()?,
146+
})
147+
}
148+
149+
/// Reads the current proxy state from the system.
150+
pub fn current(&self) -> Result<ProxyState> {
151+
self.inner.current()
152+
}
153+
154+
/// Applies the given proxy state to the system.
155+
pub fn apply(&self, state: ProxyState) -> Result<()> {
156+
self.inner.apply(state)
157+
}
158+
159+
/// Convenience helper to disable all proxies.
160+
pub fn disable(&self) -> Result<()> {
161+
self.apply(ProxyState::direct())
162+
}
163+
}

crates/proxy/src/sys.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
cfg_if::cfg_if! {
2+
if #[cfg(target_os = "macos")] {
3+
mod macos;
4+
pub(crate) use macos::Manager;
5+
} else {
6+
mod unsupported;
7+
pub(crate) use unsupported::Manager;
8+
}
9+
}

0 commit comments

Comments
 (0)