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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,9 @@ stream carries on. A crash of the browser itself fails every browser source.
Either shows at once, since the browser's processes dump no core. On Linux,
pages run without Chromium's sandbox, as in OBS, so a page that exploits a
browser bug can do anything you can: add only pages you trust.
The browser requests only what its pages load, never asking Google for
updates, the time, or autofill suggestions on its own, so pages are not
spellchecked, which would need a dictionary from Google.
**Reload** in Sources restarts a page, failed or not. A page's dropdown
lists, such as a `<select>`'s, draw over it and take clicks under
**Interact**, but it opens no other windows (`window.open` and new-tab links
Expand Down
1 change: 0 additions & 1 deletion quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ repository.
- [Web studio](/quest/m0/web.md) - the full studio in wasm on WebGPU and WebCodecs, served from this repository
- [Virtual camera](/quest/m0/vcam/README.md) - the program or a published scene exposed as a system camera on Linux, Windows, and macOS
- [Browser sources on the GPU under Mesa](/quest/m0/cef-headless.md) - shared-texture pages paint on Intel and AMD under Wayland, and a page that cannot paint fails loudly
- [Browser sources make no outside requests](/quest/m0/browser-offline.md) - no component updates, spellcheck downloads, or network time from the helper
- [A media file's sound plays with its pictures](/quest/m0/media-av-sync.md) - media sources write a bus latency ahead, as broadcast sources do
- [Catalogs finish when the studio quits](/quest/m0/catalog-finish.md) - the program's and the recording's catalogs end with `finish()`, not a drop
- [The UI keeps its frame rate while the program renders](/quest/m0/ui-frame-rate.md) - the editor draws without waiting on the program's render
Expand Down
24 changes: 0 additions & 24 deletions quest/m0/browser-offline.md

This file was deleted.

36 changes: 36 additions & 0 deletions rs/moq-scene/src/browser/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ fn browse(shared: &Arc<Shared>, start: Start, reader: Channel, painted_tx: mpsc:
});
}

if let Err(err) = no_spellcheck() {
shutdown();
return Err(err);
}

// Pages asked for meanwhile open now.
inbox.up.store(true, Ordering::Release);
inbox.post(shared);
Expand All @@ -205,6 +210,21 @@ fn browse(shared: &Arc<Shared>, start: Start, reader: Channel, painted_tx: mpsc:
Ok(())
}

/// Spellcheck in no language. Chromium otherwise fetches the dictionary for
/// the profile's language from Google, which no switch stops.
fn no_spellcheck() -> Result<(), String> {
let context = request_context_get_global_context().ok_or("CEF has no profile")?;
let mut none = value_create().ok_or("CEF cannot create a value")?;
none.set_list(list_value_create().as_mut());
// CEF refuses to set anything without somewhere to say why it failed.
let mut error = CefString::from("unknown");
let name = CefString::from("spellcheck.dictionaries");
match context.set_preference(Some(&name), Some(&mut none), Some(&mut error)) {
1 => Ok(()),
_ => Err(format!("cannot turn off spellcheck: {error}")),
}
}

/// The studio's commands the UI thread has yet to apply.
///
/// They wait coalesced, as in the studio, with one task at a time posted to
Expand Down Expand Up @@ -555,6 +575,22 @@ wrap_app! {
// which a user build lacks. Without either, the browser aborts
// rather than run unsandboxed, so the sandbox is off, as in OBS.
command_line.append_switch(Some(&"no-sandbox".into()));
// The browser requests only what its pages load. Otherwise it asks
// Google, unprompted, for the network time, for autofill's guesses
// at each form a page shows, and for whether AI Mode is offered.
// CEF disables features of its own, which ours join.
let disable = CefString::from("disable-features");
let theirs = CefString::from(&command_line.switch_value(Some(&disable))).to_string();
let ours = "NetworkTimeServiceQuerying,AutofillServerCommunication,AimEnabled";
let features = match theirs.is_empty() {
true => ours.to_string(),
false => format!("{theirs},{ours}"),
};
command_line.append_switch_with_value(Some(&disable), Some(&features.as_str().into()));
// Nor does it install components from Google: the updater's server is
// nowhere. `--disable-component-update` would leave the installers that
// ask for themselves, such as the on-device AI model's.
command_line.append_switch_with_value(Some(&"component-updater".into()), Some(&"url-source=data:".into()));
match &self.raster {
Raster::Gpu { render_node, display } => {
let platform = match display {
Expand Down
63 changes: 62 additions & 1 deletion rs/moq-studio/tests/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#![cfg(all(target_os = "linux", feature = "cef"))]

use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use kio::clock::Instant;
Expand Down Expand Up @@ -44,6 +45,12 @@ impl Studio {

/// A studio whose pages keep their cookies in `profile`.
async fn on(profile: &Path, sources: &[(&str, Source)]) -> Self {
Self::start(Path::new(env!("CARGO_BIN_EXE_moq-studio")), profile, sources).await
}

/// A studio whose helper is `program`, with its pages' cookies in
/// `profile`.
async fn start(program: &Path, profile: &Path, sources: &[(&str, Source)]) -> Self {
let instance = wgpu::Instance::default();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions::default())
Expand Down Expand Up @@ -73,7 +80,7 @@ impl Studio {
let (origin, driver) = moq_net::origin::Producer::new(Default::default());
tokio::spawn(moq_net::time::run(driver));
let (bus, mixer) = moq_audio::bus::Bus::new(Default::default()).unwrap();
let helper = Helper::new(env!("CARGO_BIN_EXE_moq-studio"), profile, &adapter, &device, &queue);
let helper = Helper::new(program, profile, &adapter, &device, &queue);
let mut sources = Sources::new(Context {
bus,
origin: origin.consume(),
Expand Down Expand Up @@ -699,6 +706,60 @@ async fn serve(html: &'static str) -> String {
url
}

/// The helper requests only what its pages load: nothing of its own, such as
/// component updates, a spellcheck dictionary, the network time, or
/// autofill's guesses at a page's text field.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "needs CEF and a Vulkan device; run by just render-sw"]
async fn the_helper_requests_only_its_pages() {
use std::os::unix::fs::PermissionsExt;
use tokio::io::AsyncBufReadExt;

// Everything off loopback goes through this proxy, which answers nothing
// and keeps each request's first line: `CONNECT host:443` for HTTPS, or
// the method and URL. Chromium sends loopback past a proxy, so the page
// still loads.
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let proxy = listener.local_addr().unwrap();
let requests = Arc::new(Mutex::new(Vec::new()));
tokio::spawn({
let requests = requests.clone();
async move {
while let Ok((stream, _)) = listener.accept().await {
let mut line = String::new();
if tokio::io::BufReader::new(stream).read_line(&mut line).await.is_ok() {
requests.lock().unwrap().push(line.trim_end().to_string());
}
}
}
});
// The helper, told to use the proxy.
let dir = tempfile::tempdir().unwrap();
let program = dir.path().join("helper");
let script = format!(
"#!/bin/sh\nexec '{}' \"$@\" --proxy-server=http://{proxy}\n",
env!("CARGO_BIN_EXE_moq-studio")
);
std::fs::write(&program, script).unwrap();
std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap();

local(async {
let url = serve("<body style='margin:0;background:lime'><textarea autofocus></textarea></body>").await;
let page = Source::Browser {
url,
width: 320,
height: 240,
};
let mut studio = Studio::start(&program, &dir.path().join("profile"), &[("page", page)]).await;
studio.until_center(GREEN, &[]).await;
// The browser asks for what it wants as it starts and as the page
// loads, so every request seen had begun by the time the page drew.
kio::clock::sleep(Duration::from_secs(2)).await;
})
.await;
assert_eq!(*requests.lock().unwrap(), Vec::<String>::new());
}

/// Where [`a_studio_that_dies`] keeps its pages' profile, which also tells it
/// to run.
const DYING: &str = "MOQ_STUDIO_TEST_DYING_PROFILE";
Expand Down