Skip to content

Commit c1104c9

Browse files
feat: Make on connect script toggle only + disable remote dash by default (#2621)
1 parent 0b448f1 commit c1104c9

File tree

6 files changed

+181
-23
lines changed

6 files changed

+181
-23
lines changed

alvr/dashboard/src/data_sources.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ use std::{
1212
thread::{self, JoinHandle},
1313
time::{Duration, Instant},
1414
};
15-
use tungstenite::http::Uri;
15+
use tungstenite::{
16+
client::IntoClientRequest,
17+
http::{HeaderValue, Uri},
18+
};
1619

1720
const REQUEST_TIMEOUT: Duration = Duration::from_millis(200);
1821

@@ -190,7 +193,11 @@ impl DataSources {
190193
}
191194
}
192195
} else {
193-
request_agent.get(&uri).send_json(&request).ok();
196+
request_agent
197+
.get(&uri)
198+
.set("X-ALVR", "true")
199+
.send_json(&request)
200+
.ok();
194201
}
195202
}
196203

@@ -224,7 +231,11 @@ impl DataSources {
224231
continue;
225232
};
226233

227-
let mut ws = if let Ok((ws, _)) = tungstenite::client(uri, socket) {
234+
let mut req = uri.into_client_request().unwrap();
235+
req.headers_mut()
236+
.insert("X-ALVR", HeaderValue::from_str("true").unwrap());
237+
238+
let mut ws = if let Ok((ws, _)) = tungstenite::client(req, socket) {
228239
ws
229240
} else {
230241
thread::sleep(Duration::from_millis(500));
@@ -281,6 +292,7 @@ impl DataSources {
281292
loop {
282293
let maybe_server_version = request_agent
283294
.get(&uri)
295+
.set("X-ALVR", "true")
284296
.call()
285297
.ok()
286298
.and_then(|r| Version::from_str(&r.into_string().ok()?).ok());

alvr/dashboard/src/data_sources_wasm.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ impl DataSources {
2121
let context = self.context.clone();
2222
wasm_bindgen_futures::spawn_local(async move {
2323
Request::post("/api/dashboard-request")
24+
.header("X-ALVR", "true")
2425
.body(serde_json::to_string(&request).unwrap())
2526
.send()
2627
.await
@@ -33,6 +34,9 @@ impl DataSources {
3334
pub fn poll_event(&mut self) -> Option<Event> {
3435
if self.ws_receiver.is_none() {
3536
let host = web_sys::window().unwrap().location().host().unwrap();
37+
// TODO: Set X-ALVR
38+
//let mut options = ewebsock::Options::default();
39+
//options.additional_headers = vec!(("X-ALVR", "true"));
3640
let Ok((_, receiver)) = ewebsock::connect(format!("ws://{host}/api/events")) else {
3741
return None;
3842
};

alvr/filesystem/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,22 @@ impl Layout {
223223
}
224224
}
225225

226+
pub fn connect_script(&self) -> PathBuf {
227+
self.config_dir.join(if cfg!(windows) {
228+
"on_connect.bat"
229+
} else {
230+
"on_connect.sh"
231+
})
232+
}
233+
234+
pub fn disconnect_script(&self) -> PathBuf {
235+
self.config_dir.join(if cfg!(windows) {
236+
"on_disconnect.bat"
237+
} else {
238+
"on_disconnect.sh"
239+
})
240+
}
241+
226242
pub fn crash_log(&self) -> PathBuf {
227243
self.log_dir.join("crash_log.txt")
228244
}

alvr/server_core/src/connection.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,10 +1361,12 @@ fn connection_pipeline(
13611361
});
13621362

13631363
{
1364-
let on_connect_script = initial_settings.connection.on_connect_script;
1365-
1366-
if !on_connect_script.is_empty() {
1367-
info!("Running on connect script (connect): {on_connect_script}");
1364+
if initial_settings.connection.enable_on_connect_script {
1365+
let on_connect_script = FILESYSTEM_LAYOUT.get().map(|l| l.connect_script()).unwrap();
1366+
info!(
1367+
"Running on connect script (connect): {}",
1368+
on_connect_script.display()
1369+
);
13681370
if let Err(e) = Command::new(&on_connect_script)
13691371
.env("ACTION", "connect")
13701372
.spawn()
@@ -1403,13 +1405,19 @@ fn connection_pipeline(
14031405
ClientListAction::SetConnectionState(ConnectionState::Disconnecting),
14041406
);
14051407

1406-
let on_disconnect_script = session_manager_lock
1408+
let enable_on_disconnect_script = session_manager_lock
14071409
.settings()
14081410
.connection
1409-
.on_disconnect_script
1410-
.clone();
1411-
if !on_disconnect_script.is_empty() {
1412-
info!("Running on disconnect script (disconnect): {on_disconnect_script}");
1411+
.enable_on_disconnect_script;
1412+
if enable_on_disconnect_script {
1413+
let on_disconnect_script = FILESYSTEM_LAYOUT
1414+
.get()
1415+
.map(|l| l.disconnect_script())
1416+
.unwrap();
1417+
info!(
1418+
"Running on disconnect script (disconnect): {}",
1419+
on_disconnect_script.display()
1420+
);
14131421
if let Err(e) = Command::new(&on_disconnect_script)
14141422
.env("ACTION", "disconnect")
14151423
.spawn()

alvr/server_core/src/web_server.rs

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@ use alvr_events::{ButtonEvent, EventType};
1010
use alvr_packets::{ButtonEntry, ClientListAction, ServerRequest};
1111
use bytes::Buf;
1212
use futures::SinkExt;
13-
use headers::HeaderMapExt;
13+
use headers::{
14+
AccessControlAllowHeaders, AccessControlAllowMethods, AccessControlRequestHeaders,
15+
AccessControlRequestMethod, HeaderMapExt,
16+
};
1417
use hyper::{
15-
header::{self, HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL},
16-
service, Body, Request, Response, StatusCode,
18+
header::{
19+
self, HeaderName, HeaderValue, ACCESS_CONTROL_ALLOW_ORIGIN, CACHE_CONTROL, CONTENT_TYPE,
20+
},
21+
service, Body, Method, Request, Response, StatusCode,
1722
};
1823
use serde::de::DeserializeOwned;
1924
use serde_json as json;
@@ -88,6 +93,93 @@ async fn http_api(
8893
connection_context: &ConnectionContext,
8994
request: Request<Body>,
9095
) -> Result<Response<Body>> {
96+
let allow_untrusted_http = SESSION_MANAGER
97+
.read()
98+
.session()
99+
.session_settings
100+
.connection
101+
.allow_untrusted_http;
102+
103+
const X_ALVR: &str = "X-ALVR";
104+
105+
// A browser is asking for CORS info
106+
if request.method() == Method::OPTIONS {
107+
let bad_request: Response<Body> = Response::builder()
108+
.status(StatusCode::FORBIDDEN)
109+
.body("".into())?;
110+
111+
if !allow_untrusted_http {
112+
return Ok(bad_request);
113+
}
114+
115+
if let Some(requested_method) = request.headers().typed_get::<AccessControlRequestMethod>()
116+
{
117+
if requested_method != Method::GET.into() && requested_method != Method::POST.into() {
118+
return Ok(bad_request);
119+
}
120+
} else {
121+
return Ok(bad_request);
122+
}
123+
124+
if let Some(requested_headers) =
125+
request.headers().typed_get::<AccessControlRequestHeaders>()
126+
{
127+
let mut found_x_alvr = false;
128+
for header in requested_headers.iter() {
129+
if header == HeaderName::from_static(X_ALVR) {
130+
found_x_alvr = true;
131+
} else if header != CONTENT_TYPE {
132+
return Ok(bad_request);
133+
}
134+
}
135+
136+
// Ensure it actually requested the X-ALVR header, because we don't want to allow it
137+
// if it never got asked for
138+
if !found_x_alvr {
139+
return Ok(bad_request);
140+
}
141+
} else {
142+
return Ok(bad_request);
143+
}
144+
145+
let allowed_methods = [Method::GET, Method::POST, Method::OPTIONS]
146+
.into_iter()
147+
.collect::<AccessControlAllowMethods>();
148+
let allowed_headers = [CONTENT_TYPE, HeaderName::from_static(X_ALVR)]
149+
.into_iter()
150+
.collect::<AccessControlAllowHeaders>();
151+
152+
let mut response: Response<Body> = Response::builder()
153+
.status(StatusCode::OK)
154+
.header(CACHE_CONTROL, "no-cache, no-store, must-revalidate")
155+
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
156+
.body("".into())?;
157+
158+
let headers = response.headers_mut();
159+
headers.typed_insert(allowed_methods);
160+
headers.typed_insert(allowed_headers);
161+
162+
return Ok(response);
163+
}
164+
165+
if request.method() != Method::POST && request.method() != Method::GET {
166+
return Ok(Response::builder()
167+
.status(StatusCode::BAD_REQUEST)
168+
.body("invalid method".into())?);
169+
}
170+
171+
// This is the actual core part of cors
172+
// We require the X-ALVR header, but the browser forces a cors preflight
173+
// if the site tries to send a request with it set since it's not-whitelisted
174+
//
175+
// The dashboard can just set the header and be allowed through without the preflight
176+
// thus not getting blocked by allow_untrusted_http being disabled
177+
if request.headers().get(X_ALVR) != Some(&HeaderValue::from_static("true")) {
178+
return Ok(Response::builder()
179+
.status(StatusCode::BAD_REQUEST)
180+
.body("missing X-ALVR header".into())?);
181+
}
182+
91183
let mut response = match request.uri().path() {
92184
// New unified requests
93185
"/api/dashboard-request" => {

alvr/session/src/settings.rs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,6 +1116,7 @@ pub enum SocketProtocol {
11161116

11171117
#[derive(SettingsSchema, Serialize, Deserialize, Clone)]
11181118
pub struct DiscoveryConfig {
1119+
#[cfg_attr(target_os = "linux", schema(flag = "hidden"))]
11191120
#[schema(strings(
11201121
help = "Allow untrusted clients to connect without confirmation. This is not recommended for security reasons."
11211122
))]
@@ -1149,16 +1150,40 @@ TCP: Slower than UDP, but more stable. Pick this if you experience video or audi
11491150
))]
11501151
pub wired_client_autolaunch: bool,
11511152

1152-
#[schema(strings(
1153-
help = "This script will be ran when the headset connects. Env var ACTION will be set to `connect`."
1154-
))]
1155-
pub on_connect_script: String,
1153+
#[cfg_attr(
1154+
windows,
1155+
schema(strings(
1156+
help = "If on_connect.bat exists alongside session.json, it will be run on headset connect. Env var ACTION will be set to `connect`."
1157+
))
1158+
)]
1159+
#[cfg_attr(
1160+
not(windows),
1161+
schema(strings(
1162+
help = "If on_connect.sh exists alongside session.json, it will be run on headset connect. Env var ACTION will be set to `connect`."
1163+
))
1164+
)]
1165+
pub enable_on_connect_script: bool,
1166+
1167+
#[cfg_attr(
1168+
windows,
1169+
schema(strings(
1170+
help = "If on_disconnect.bat exists alongside session.json, it will be run on headset disconnect. Env var ACTION will be set to `disconnect`."
1171+
))
1172+
)]
1173+
#[cfg_attr(
1174+
not(windows),
1175+
schema(strings(
1176+
help = "If on_disconnect.sh exists alongside session.json, it will be run on headset disconnect. Env var ACTION will be set to `disconnect`."
1177+
))
1178+
)]
1179+
#[schema(flag = "real-time")]
1180+
pub enable_on_disconnect_script: bool,
11561181

11571182
#[schema(strings(
1158-
help = "This script will be ran when the headset disconnects, or when SteamVR shuts down. Env var ACTION will be set to `disconnect`."
1183+
help = "Allow cross-origin browser requests to control ALVR settings remotely."
11591184
))]
11601185
#[schema(flag = "real-time")]
1161-
pub on_disconnect_script: String,
1186+
pub allow_untrusted_http: bool,
11621187

11631188
#[schema(strings(
11641189
help = r#"If the client, server or the network discarded one packet, discard packets until a IDR packet is found.
@@ -1834,8 +1859,9 @@ pub fn session_settings_default() -> SettingsDefault {
18341859
max_queued_server_video_frames: 1024,
18351860
avoid_video_glitching: false,
18361861
minimum_idr_interval_ms: 100,
1837-
on_connect_script: "".into(),
1838-
on_disconnect_script: "".into(),
1862+
enable_on_connect_script: false,
1863+
enable_on_disconnect_script: false,
1864+
allow_untrusted_http: false,
18391865
packet_size: 1400,
18401866
statistics_history_size: 256,
18411867
},

0 commit comments

Comments
 (0)