Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/IPC/WindServiceHandlers/NativeHost/
OpenExternal.rs

1//! Wire method: `native:openExternal`, `nativeHost:openExternal`.
2//! Opens an http/https URL in the platform default browser.
3
4use std::sync::Arc;
5
6use serde_json::Value;
7
8use crate::{RunTime::ApplicationRunTime::ApplicationRunTime, dev_log};
9
10pub async fn Fn(RunTime:Arc<ApplicationRunTime>, Arguments:Vec<Value>) -> Result<Value, String> {
11	// Accept both a plain URI string and the object shape
12	// `{ uri: "..." }` that some VS Code callers emit.
13	let url_str = match Arguments.first() {
14		Some(Value::String(S)) => S.as_str(),
15
16		Some(Value::Object(Obj)) => Obj.get("uri").or_else(|| Obj.get("url")).and_then(|V| V.as_str()).unwrap_or(""),
17
18		_ => return Ok(Value::Bool(false)),
19	};
20
21	if url_str.is_empty() {
22		return Ok(Value::Bool(false));
23	}
24
25	dev_log!("lifecycle", "openExternal: {}", url_str);
26
27	// Allowlist of safe protocols. Block `file://` (arbitrary filesystem
28	// access) and bare shell commands. Everything else that parses as a
29	// valid URI scheme is forwarded to the OS default handler.
30	let Scheme = url_str.splitn(2, ':').next().unwrap_or("").to_lowercase();
31
32	let AllowedSchemes = [
33		"http",
34		"https",
35		"mailto",
36		"ftp",
37		"vscode",
38		"fiddee",
39		"ssh",
40		"git",
41		"x-github-client",
42		"github-windows",
43		"slack",
44		"teams",
45		"zoommtg",
46		"tel",
47		"callto",
48	];
49
50	if Scheme == "file" || Scheme.is_empty() || !url_str.contains(':') {
51		dev_log!(
52			"lifecycle",
53			"warn: [OpenExternal] blocked scheme '{}' for uri '{}'",
54			Scheme,
55			url_str
56		);
57
58		return Ok(Value::Bool(false));
59	}
60
61	let IsKnownScheme = AllowedSchemes.contains(&Scheme.as_str());
62
63	if !IsKnownScheme {
64		dev_log!(
65			"lifecycle",
66			"[OpenExternal] unknown scheme '{}' - forwarding to OS anyway",
67			Scheme
68		);
69	}
70
71	#[cfg(target_os = "macos")]
72	{
73		use std::process::Command;
74
75		let result = Command::new("open")
76			.arg(url_str)
77			.output()
78			.map_err(|Error| format!("Failed to execute open command: {}", Error))?;
79
80		if !result.status.success() {
81			return Err(format!("Failed to open URL: {}", String::from_utf8_lossy(&result.stderr)));
82		}
83	}
84
85	#[cfg(target_os = "windows")]
86	{
87		use std::process::Command;
88
89		let result = Command::new("cmd")
90			.arg("/c")
91			.arg("start")
92			.arg(url_str)
93			.output()
94			.map_err(|Error| format!("Failed to execute start command: {}", Error))?;
95
96		if !result.status.success() {
97			return Err(format!("Failed to open URL: {}", String::from_utf8_lossy(&result.stderr)));
98		}
99	}
100
101	#[cfg(target_os = "linux")]
102	{
103		use std::process::Command;
104
105		let handlers = ["xdg-open", "gnome-open", "kde-open", "x-www-browser"];
106
107		let mut last_error = String::new();
108
109		for handler in handlers.iter() {
110			let result = Command::new(handler).arg(url_str).output();
111
112			match result {
113				Ok(output) if output.status.success() => {
114					dev_log!("lifecycle", "opened with {}", handler);
115
116					break;
117				},
118
119				Err(e) => {
120					last_error = e.to_string();
121
122					continue;
123				},
124
125				_ => continue,
126			}
127		}
128
129		if !last_error.is_empty() {
130			return Err(format!("Failed to open URL with any handler: {}", last_error));
131		}
132	}
133
134	dev_log!("lifecycle", "opened URL: {}", url_str);
135
136	Ok(Value::Bool(true))
137}