#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine as _;
use futures::{SinkExt, StreamExt};
use serde_json::Value;
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::ClientRequestBuilder;
use tokio_tungstenite::tungstenite::Message;
use omni_dev::browser::{self, BridgeConfig};
static START_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
async fn start_bridge(allow_origin: Option<String>, timeout: Duration) -> (u16, u16, String) {
start_bridge_with(allow_origin, timeout, 1024 * 1024).await
}
async fn start_bridge_with(
allow_origin: Option<String>,
timeout: Duration,
max_body_bytes: usize,
) -> (u16, u16, String) {
let token = "test-token-abcdef".to_string();
for _ in 0..20 {
let _guard = START_LOCK.lock().await;
let c = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let w = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
let control_port = c.local_addr().unwrap().port();
let ws_port = w.local_addr().unwrap().port();
drop(c);
drop(w);
let config = BridgeConfig {
ws_port,
control_port,
request_timeout: timeout,
allow_origin: allow_origin.clone(),
max_body_bytes,
max_concurrent: 64,
};
let token_clone = token.clone();
let mut handle = tokio::spawn(async move { browser::run(config, token_clone).await });
let up = tokio::select! {
ok = both_listening(control_port, ws_port) => ok,
_ = &mut handle => false,
};
if up {
return (control_port, ws_port, token);
}
}
panic!("could not start a bridge on free ports after 20 attempts");
}
async fn both_listening(control_port: u16, ws_port: u16) -> bool {
wait_until_listening(control_port).await && wait_until_listening(ws_port).await
}
async fn wait_until_listening(port: u16) -> bool {
for _ in 0..100 {
if tokio::net::TcpStream::connect(("127.0.0.1", port))
.await
.is_ok()
{
return true;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
false
}
struct FakeBrowser {
handle: tokio::task::JoinHandle<()>,
}
impl FakeBrowser {
async fn connect(ws_port: u16, token: &str, reply: bool) -> Self {
let uri = format!("ws://127.0.0.1:{ws_port}/").parse().unwrap();
let request = ClientRequestBuilder::new(uri).with_sub_protocol(token);
let (ws, _resp) = tokio_tungstenite::connect_async(request).await.unwrap();
let (mut sink, mut stream) = ws.split();
let handle = tokio::spawn(async move {
while let Some(Ok(msg)) = stream.next().await {
if let Message::Text(txt) = msg {
let cmd: Value = serde_json::from_str(&txt).unwrap();
let id = cmd["id"].as_u64().unwrap();
if reply {
let body = format!("echo:{}", cmd["url"].as_str().unwrap_or(""));
let resp = serde_json::json!({
"id": id,
"status": 200,
"headers": {"content-type": "text/plain"},
"body": body,
});
sink.send(Message::Text(resp.to_string().into()))
.await
.unwrap();
}
}
}
});
Self { handle }
}
async fn connect_tagged(ws_port: u16, token: &str, origin: &str, tag: &str) -> Self {
let uri = format!("ws://127.0.0.1:{ws_port}/").parse().unwrap();
let request = ClientRequestBuilder::new(uri)
.with_sub_protocol(token)
.with_header("Origin", origin);
let (ws, _resp) = tokio_tungstenite::connect_async(request).await.unwrap();
let (mut sink, mut stream) = ws.split();
let tag = tag.to_string();
let handle = tokio::spawn(async move {
while let Some(Ok(msg)) = stream.next().await {
if let Message::Text(txt) = msg {
let cmd: Value = serde_json::from_str(&txt).unwrap();
let id = cmd["id"].as_u64().unwrap();
let body = format!("tab:{tag}:{}", cmd["url"].as_str().unwrap_or(""));
let resp = serde_json::json!({
"id": id,
"status": 200,
"headers": {"content-type": "text/plain"},
"body": body,
});
sink.send(Message::Text(resp.to_string().into()))
.await
.unwrap();
}
}
});
Self { handle }
}
}
impl Drop for FakeBrowser {
fn drop(&mut self) {
self.handle.abort();
}
}
fn client(control_port: u16, token: &str) -> (reqwest::Client, String, String) {
(
reqwest::Client::new(),
format!("http://127.0.0.1:{control_port}"),
token.to_string(),
)
}
#[tokio::test]
async fn status_reflects_connection() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.get(format!("{base}/__bridge/status"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
let body: Value = resp.json().await.unwrap();
assert_eq!(body["connected"], Value::Bool(false));
let _browser = FakeBrowser::connect(ws_port, &token, true).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let resp = http
.get(format!("{base}/__bridge/status"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
let body: Value = resp.json().await.unwrap();
assert_eq!(body["connected"], Value::Bool(true));
}
#[tokio::test]
async fn bridge_request_round_trips() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = FakeBrowser::connect(ws_port, &token, true).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/loki/api/v1/labels", "method": "GET"}))
.send()
.await
.unwrap();
assert!(resp.status().is_success());
let env: Value = resp.json().await.unwrap();
assert_eq!(env["status"], 200);
assert_eq!(env["body"], "echo:/loki/api/v1/labels");
}
#[tokio::test]
async fn transparent_proxy_round_trips() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = FakeBrowser::connect(ws_port, &token, true).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.get(format!("{base}/api/datasources"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.text().await.unwrap(), "echo:/api/datasources");
}
#[tokio::test]
async fn concurrent_requests_correlate_by_id() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = FakeBrowser::connect(ws_port, &token, true).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let mut handles = Vec::new();
for i in 0..20 {
let http = http.clone();
let base = base.clone();
let tok = tok.clone();
handles.push(tokio::spawn(async move {
let url = format!("/path/{i}");
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": url, "method": "GET"}))
.send()
.await
.unwrap();
let env: Value = resp.json().await.unwrap();
assert_eq!(env["body"], format!("echo:/path/{i}"));
}));
}
for h in handles {
h.await.unwrap();
}
}
#[tokio::test]
async fn no_reply_times_out_with_504() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_millis(300)).await;
let _browser = FakeBrowser::connect(ws_port, &token, false).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/slow", "method": "GET"}))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 504);
}
#[tokio::test]
async fn no_browser_returns_503() {
let (control_port, _ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/x", "method": "GET"}))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 503);
}
#[tokio::test]
async fn missing_token_is_rejected() {
let (control_port, _ws_port, _token) = start_bridge(None, Duration::from_secs(5)).await;
let http = reqwest::Client::new();
let resp = http
.get(format!("http://127.0.0.1:{control_port}/__bridge/status"))
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 401);
}
#[tokio::test]
async fn missing_bridge_header_is_rejected() {
let (control_port, _ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let http = reqwest::Client::new();
let resp = http
.get(format!("http://127.0.0.1:{control_port}/__bridge/status"))
.bearer_auth(&token)
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 403);
}
#[tokio::test]
async fn browser_originated_request_is_rejected() {
let (control_port, _ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let http = reqwest::Client::new();
let resp = http
.get(format!("http://127.0.0.1:{control_port}/__bridge/status"))
.bearer_auth(&token)
.header("x-omni-bridge", "1")
.header("origin", "https://evil.test")
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 403);
}
#[tokio::test]
async fn cross_origin_url_rejected_without_allow_origin() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = FakeBrowser::connect(ws_port, &token, true).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "https://evil.test/x", "method": "GET"}))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 403);
}
#[tokio::test]
async fn unauthenticated_ws_peer_cannot_connect() {
let (_control_port, ws_port, _token) = start_bridge(None, Duration::from_secs(5)).await;
let uri = format!("ws://127.0.0.1:{ws_port}/").parse().unwrap();
let request = ClientRequestBuilder::new(uri).with_sub_protocol("wrong-token");
let result = tokio_tungstenite::connect_async(request).await;
assert!(result.is_err(), "unauthenticated peer must be rejected");
}
#[tokio::test]
async fn authenticated_browser_is_not_evicted_by_unauthenticated_peer() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = FakeBrowser::connect(ws_port, &token, true).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let uri = format!("ws://127.0.0.1:{ws_port}/").parse().unwrap();
let bad = ClientRequestBuilder::new(uri).with_sub_protocol("wrong-token");
let _ = tokio_tungstenite::connect_async(bad).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/still-here", "method": "GET"}))
.send()
.await
.unwrap();
let env: Value = resp.json().await.unwrap();
assert_eq!(env["body"], "echo:/still-here");
}
#[tokio::test]
async fn proxy_forwards_method_and_headers() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let token2 = token.clone();
let uri = format!("ws://127.0.0.1:{ws_port}/").parse().unwrap();
let request = ClientRequestBuilder::new(uri).with_sub_protocol(&token2);
let (ws, _r) = tokio_tungstenite::connect_async(request).await.unwrap();
let (mut sink, mut stream) = ws.split();
let _h = tokio::spawn(async move {
while let Some(Ok(Message::Text(txt))) = stream.next().await {
let cmd: Value = serde_json::from_str(&txt).unwrap();
let headers: BTreeMap<String, String> =
serde_json::from_value(cmd["headers"].clone()).unwrap();
let resp = serde_json::json!({
"id": cmd["id"].as_u64().unwrap(),
"status": 200,
"headers": {},
"body": format!("{} accept={}", cmd["method"].as_str().unwrap(),
headers.get("accept").cloned().unwrap_or_default()),
});
sink.send(Message::Text(resp.to_string().into()))
.await
.unwrap();
}
});
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/api/thing"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.header("accept", "application/json")
.send()
.await
.unwrap();
let text = resp.text().await.unwrap();
assert_eq!(text, "POST accept=application/json");
}
const BINARY_BYTES: &[u8] = &[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
const BINARY_B64: &str = "iVBORw0KGgo=";
fn spawn_reply_browser(
ws_port: u16,
token: String,
body: String,
encoding: Option<&'static str>,
) -> tokio::task::JoinHandle<()> {
tokio::spawn(async move {
let uri = format!("ws://127.0.0.1:{ws_port}/").parse().unwrap();
let request = ClientRequestBuilder::new(uri).with_sub_protocol(&token);
let (ws, _r) = tokio_tungstenite::connect_async(request).await.unwrap();
let (mut sink, mut stream) = ws.split();
while let Some(Ok(Message::Text(txt))) = stream.next().await {
let cmd: Value = serde_json::from_str(&txt).unwrap();
let mut resp = serde_json::json!({
"id": cmd["id"].as_u64().unwrap(),
"status": 200,
"headers": {"content-type": "image/png"},
"body": body,
});
if let Some(enc) = encoding {
resp["encoding"] = Value::String(enc.to_string());
}
sink.send(Message::Text(resp.to_string().into()))
.await
.unwrap();
}
})
}
fn spawn_binary_browser(ws_port: u16, token: String) -> tokio::task::JoinHandle<()> {
spawn_reply_browser(ws_port, token, BINARY_B64.to_string(), Some("base64"))
}
#[tokio::test]
async fn transparent_proxy_decodes_base64_to_raw_bytes() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = spawn_binary_browser(ws_port, token.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.get(format!("{base}/render/panel.png"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"image/png",
"content-type is forwarded for binary bodies"
);
let bytes = resp.bytes().await.unwrap();
assert_eq!(
bytes.as_ref(),
BINARY_BYTES,
"proxy must hand curl the decoded raw bytes"
);
}
#[tokio::test]
async fn bridge_request_returns_base64_envelope() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = spawn_binary_browser(ws_port, token.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/render/panel.png", "method": "GET"}))
.send()
.await
.unwrap();
assert!(resp.status().is_success());
let env: Value = resp.json().await.unwrap();
assert_eq!(env["status"], 200);
assert_eq!(env["encoding"], "base64");
assert_eq!(env["body"], BINARY_B64);
}
#[tokio::test]
async fn oversized_decoded_binary_body_is_rejected() {
let (control_port, ws_port, token) = start_bridge_with(None, Duration::from_secs(5), 200).await;
let _browser = spawn_reply_browser(ws_port, token.clone(), "iVBO".repeat(100), Some("base64"));
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/x.png", "method": "GET"}))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 502);
let body = resp.text().await.unwrap();
assert!(body.contains("--max-body-bytes"));
assert!(body.contains("200")); assert!(body.contains("300")); assert!(body.contains("page the request"));
}
#[tokio::test]
async fn invalid_base64_body_is_rejected() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = spawn_reply_browser(
ws_port,
token.clone(),
"@@@ not base64 @@@".into(),
Some("base64"),
);
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/x.png", "method": "GET"}))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 502);
}
#[tokio::test]
async fn unsupported_encoding_is_rejected() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = spawn_reply_browser(ws_port, token.clone(), "anything".into(), Some("gzip"));
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/x.png", "method": "GET"}))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 502);
}
async fn spawn_stream_browser(
ws_port: u16,
token: String,
chunks: Vec<Vec<u8>>,
delay: Duration,
cancelled: Arc<AtomicBool>,
) -> tokio::task::JoinHandle<()> {
let uri = format!("ws://127.0.0.1:{ws_port}/").parse().unwrap();
let request = ClientRequestBuilder::new(uri).with_sub_protocol(&token);
let (ws, _r) = tokio_tungstenite::connect_async(request).await.unwrap();
tokio::spawn(async move {
let (sink, mut stream) = ws.split();
let sink = Arc::new(tokio::sync::Mutex::new(sink));
while let Some(Ok(Message::Text(txt))) = stream.next().await {
let cmd: Value = serde_json::from_str(&txt).unwrap();
if cmd["cancel"].as_bool() == Some(true) {
cancelled.store(true, Ordering::SeqCst);
continue;
}
let id = cmd["id"].as_u64().unwrap();
let sink = sink.clone();
let chunks = chunks.clone();
let cancelled = cancelled.clone();
tokio::spawn(async move {
let head = serde_json::json!({
"id": id, "status": 200,
"headers": {"content-type": "text/event-stream"}, "stream": true,
});
sink.lock()
.await
.send(Message::Text(head.to_string().into()))
.await
.ok();
for (seq, chunk) in chunks.iter().enumerate() {
if cancelled.load(Ordering::SeqCst) {
return;
}
let frame =
serde_json::json!({"id": id, "seq": seq, "chunk": BASE64.encode(chunk)});
if sink
.lock()
.await
.send(Message::Text(frame.to_string().into()))
.await
.is_err()
{
return;
}
tokio::time::sleep(delay).await;
}
let done = serde_json::json!({"id": id, "done": true});
sink.lock()
.await
.send(Message::Text(done.to_string().into()))
.await
.ok();
});
}
})
}
#[tokio::test]
async fn transparent_proxy_streams_chunks_as_raw_body() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let cancelled = Arc::new(AtomicBool::new(false));
let _browser = spawn_stream_browser(
ws_port,
token.clone(),
vec![b"data: 1\n\n".to_vec(), b"data: 2\n\n".to_vec()],
Duration::from_millis(10),
cancelled,
)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.get(format!("{base}/events?__stream=1"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(
resp.headers().get("content-type").unwrap(),
"text/event-stream"
);
let body = resp.bytes().await.unwrap();
assert_eq!(
body.as_ref(),
b"data: 1\n\ndata: 2\n\n",
"proxy must reassemble decoded chunks into the raw body"
);
}
#[tokio::test]
async fn bridge_request_streams_ndjson() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let cancelled = Arc::new(AtomicBool::new(false));
let _browser = spawn_stream_browser(
ws_port,
token.clone(),
vec![b"hello ".to_vec(), b"world".to_vec()],
Duration::from_millis(10),
cancelled,
)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/events", "method": "GET", "stream": true}))
.send()
.await
.unwrap();
assert!(resp.status().is_success());
assert_eq!(
resp.headers().get("content-type").unwrap(),
"application/x-ndjson"
);
let text = resp.text().await.unwrap();
let lines: Vec<Value> = text
.lines()
.filter(|l| !l.is_empty())
.map(|l| serde_json::from_str(l).unwrap())
.collect();
assert_eq!(lines[0]["status"], 200);
assert_eq!(lines[1]["seq"], 0);
let mut reassembled = Vec::new();
for line in lines.iter().filter(|l| l.get("chunk").is_some()) {
reassembled.extend(BASE64.decode(line["chunk"].as_str().unwrap()).unwrap());
}
assert_eq!(reassembled, b"hello world");
assert_eq!(lines.last().unwrap()["done"], true);
}
#[tokio::test]
async fn streaming_cumulative_cap_aborts_and_cancels() {
let (control_port, ws_port, token) = start_bridge_with(None, Duration::from_secs(5), 300).await;
let cancelled = Arc::new(AtomicBool::new(false));
let _browser = spawn_stream_browser(
ws_port,
token.clone(),
vec![vec![b'a'; 200], vec![b'b'; 200], vec![b'c'; 200]],
Duration::from_millis(50),
cancelled.clone(),
)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.get(format!("{base}/big?__stream=1"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
let body = resp.bytes().await.unwrap();
assert_eq!(body.len(), 200, "stream truncated at the cumulative cap");
for _ in 0..50 {
if cancelled.load(Ordering::SeqCst) {
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(
cancelled.load(Ordering::SeqCst),
"browser must receive a cancel frame when the cap aborts the stream"
);
}
#[tokio::test]
async fn streaming_idle_timeout_ends_stream() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_millis(250)).await;
let cancelled = Arc::new(AtomicBool::new(false));
let _browser = spawn_stream_browser(
ws_port,
token.clone(),
vec![b"first".to_vec(), b"second".to_vec()],
Duration::from_secs(1),
cancelled.clone(),
)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.get(format!("{base}/slow?__stream=1"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
let body = resp.bytes().await.unwrap();
assert_eq!(body.as_ref(), b"first", "stream ends at the idle timeout");
for _ in 0..50 {
if cancelled.load(Ordering::SeqCst) {
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(cancelled.load(Ordering::SeqCst), "idle timeout must cancel");
}
#[tokio::test]
async fn cli_request_client_streams() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let cancelled = Arc::new(AtomicBool::new(false));
let _browser = spawn_stream_browser(
ws_port,
token.clone(),
vec![b"chunk-one ".to_vec(), b"chunk-two".to_vec()],
Duration::from_millis(10),
cancelled,
)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
let bin = env!("CARGO_BIN_EXE_omni-dev");
let out = tokio::process::Command::new(bin)
.args([
"browser",
"bridge",
"request",
"--stream",
"--control-port",
&control_port.to_string(),
"--url",
"/events",
])
.env("OMNI_BRIDGE_TOKEN", &token)
.output()
.await
.unwrap();
assert!(
out.status.success(),
"client exited non-zero: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(out.stdout, b"chunk-one chunk-two");
}
async fn spawn_templated_browser(
ws_port: u16,
token: String,
frames: Vec<Value>,
) -> tokio::task::JoinHandle<()> {
let uri = format!("ws://127.0.0.1:{ws_port}/").parse().unwrap();
let request = ClientRequestBuilder::new(uri).with_sub_protocol(&token);
let (ws, _r) = tokio_tungstenite::connect_async(request).await.unwrap();
tokio::spawn(async move {
let (mut sink, mut stream) = ws.split();
while let Some(Ok(Message::Text(txt))) = stream.next().await {
let cmd: Value = serde_json::from_str(&txt).unwrap();
if cmd["cancel"].as_bool() == Some(true) {
continue;
}
let id = cmd["id"].as_u64().unwrap();
for frame in &frames {
let mut frame = frame.clone();
frame["id"] = Value::from(id);
sink.send(Message::Text(frame.to_string().into()))
.await
.ok();
}
}
})
}
#[tokio::test]
async fn streaming_proxy_no_browser_returns_503() {
let (control_port, _ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.get(format!("{base}/events?__stream=1"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 503);
}
#[tokio::test]
async fn streaming_request_cross_origin_url_rejected() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = FakeBrowser::connect(ws_port, &token, false).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "https://evil.test/x", "method": "GET", "stream": true}))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 403);
}
#[tokio::test]
async fn streaming_request_invalid_header_rejected() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = FakeBrowser::connect(ws_port, &token, false).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({
"url": "/events", "method": "GET", "stream": true,
"headers": {"X-Bad": "a\r\nInjected: 1"}
}))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 400);
}
#[tokio::test]
async fn streaming_browser_error_head_returns_502() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = spawn_templated_browser(
ws_port,
token.clone(),
vec![serde_json::json!({"error": "fetch blew up"})],
)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.get(format!("{base}/events?__stream=1"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 502);
}
#[tokio::test]
async fn streaming_chunk_before_head_returns_502() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = spawn_templated_browser(
ws_port,
token.clone(),
vec![serde_json::json!({"seq": 0, "chunk": "aGk="})],
)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.get(format!("{base}/events?__stream=1"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 502);
}
#[tokio::test]
async fn streaming_start_idle_timeout_returns_504() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_millis(250)).await;
let _browser = FakeBrowser::connect(ws_port, &token, false).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.get(format!("{base}/events?__stream=1"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 504);
}
#[tokio::test]
async fn streaming_invalid_base64_chunk_truncates_body() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = spawn_templated_browser(
ws_port,
token.clone(),
vec![
serde_json::json!({"status": 200, "headers": {}, "stream": true}),
serde_json::json!({"seq": 0, "chunk": "@@@ not base64 @@@"}),
serde_json::json!({"done": true}),
],
)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.get(format!("{base}/events?__stream=1"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 200);
assert!(
resp.bytes().await.unwrap().is_empty(),
"an undecodable chunk aborts the stream before any body bytes"
);
}
#[tokio::test]
async fn streaming_stray_head_after_first_is_ignored() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = spawn_templated_browser(
ws_port,
token.clone(),
vec![
serde_json::json!({"status": 200, "headers": {}, "stream": true}),
serde_json::json!({"status": 200, "headers": {}, "stream": true}),
serde_json::json!({"seq": 0, "chunk": BASE64.encode("payload")}),
serde_json::json!({"done": true}),
],
)
.await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.get(format!("{base}/events?__stream=1"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap();
assert_eq!(resp.bytes().await.unwrap().as_ref(), b"payload");
}
#[tokio::test]
async fn bridge_survives_unparseable_browser_frame() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let uri = format!("ws://127.0.0.1:{ws_port}/").parse().unwrap();
let request = ClientRequestBuilder::new(uri).with_sub_protocol(token.as_str());
let (ws, _r) = tokio_tungstenite::connect_async(request).await.unwrap();
tokio::spawn(async move {
let (mut sink, mut stream) = ws.split();
sink.send(Message::Text("not json at all".into()))
.await
.ok();
while let Some(Ok(Message::Text(txt))) = stream.next().await {
let cmd: Value = serde_json::from_str(&txt).unwrap();
let resp = serde_json::json!({
"id": cmd["id"].as_u64().unwrap(), "status": 200,
"headers": {"content-type": "text/plain"}, "body": "ok",
});
sink.send(Message::Text(resp.to_string().into())).await.ok();
}
});
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/x", "method": "GET"}))
.send()
.await
.unwrap();
assert!(resp.status().is_success());
let env: Value = resp.json().await.unwrap();
assert_eq!(env["body"], "ok");
}
#[tokio::test]
async fn cli_request_client_stream_reports_error() {
let (control_port, _ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let bin = env!("CARGO_BIN_EXE_omni-dev");
let out = tokio::process::Command::new(bin)
.args([
"browser",
"bridge",
"request",
"--stream",
"--control-port",
&control_port.to_string(),
"--url",
"/events",
])
.env("OMNI_BRIDGE_TOKEN", &token)
.output()
.await
.unwrap();
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("503"), "stderr was: {stderr}");
}
#[tokio::test]
async fn cli_request_client_round_trips() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _browser = FakeBrowser::connect(ws_port, &token, true).await;
tokio::time::sleep(Duration::from_millis(100)).await;
let bin = env!("CARGO_BIN_EXE_omni-dev");
let out = tokio::process::Command::new(bin)
.args([
"browser",
"bridge",
"request",
"--control-port",
&control_port.to_string(),
"--url",
"/api/labels",
"--header",
"Accept: application/json",
])
.env("OMNI_BRIDGE_TOKEN", &token)
.output()
.await
.unwrap();
assert!(
out.status.success(),
"client exited non-zero: {}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let env: Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(env["status"], 200);
assert_eq!(env["body"], "echo:/api/labels");
let dir = tempfile::tempdir().unwrap();
let payload = dir.path().join("payload.json");
std::fs::write(&payload, r#"{"q":"x"}"#).unwrap();
let out = tokio::process::Command::new(bin)
.args([
"browser",
"bridge",
"request",
"--control-port",
&control_port.to_string(),
"--url",
"/api/ds/query",
"--method",
"POST",
"--body",
&format!("@{}", payload.display()),
])
.env("OMNI_BRIDGE_TOKEN", &token)
.output()
.await
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let env: Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(env["body"], "echo:/api/ds/query");
}
#[tokio::test]
async fn cli_request_client_without_token_errors() {
let bin = env!("CARGO_BIN_EXE_omni-dev");
let out = tokio::process::Command::new(bin)
.args(["browser", "bridge", "request", "--url", "/x"])
.env_remove("OMNI_BRIDGE_TOKEN")
.output()
.await
.unwrap();
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("OMNI_BRIDGE_TOKEN"), "stderr was: {stderr}");
}
#[tokio::test]
async fn cli_request_client_reports_bridge_error() {
let (control_port, _ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let bin = env!("CARGO_BIN_EXE_omni-dev");
let out = tokio::process::Command::new(bin)
.args([
"browser",
"bridge",
"request",
"--control-port",
&control_port.to_string(),
"--url",
"/x",
])
.env("OMNI_BRIDGE_TOKEN", &token)
.output()
.await
.unwrap();
assert!(!out.status.success());
}
async fn fetch_status(http: &reqwest::Client, base: &str, tok: &str) -> Value {
http.get(format!("{base}/__bridge/status"))
.bearer_auth(tok)
.header("x-omni-bridge", "1")
.send()
.await
.unwrap()
.json()
.await
.unwrap()
}
#[tokio::test]
async fn two_tabs_coexist_in_status() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _a = FakeBrowser::connect_tagged(ws_port, &token, "https://a.test", "A").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let _b = FakeBrowser::connect_tagged(ws_port, &token, "https://b.test", "B").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let body = fetch_status(&http, &base, &tok).await;
assert_eq!(body["connected"], Value::Bool(true));
let tabs = body["tabs"].as_array().unwrap();
assert_eq!(tabs.len(), 2);
let origins: Vec<&str> = tabs.iter().map(|t| t["origin"].as_str().unwrap()).collect();
assert!(origins.contains(&"https://a.test") && origins.contains(&"https://b.test"));
assert!(body.get("browser_origin").is_none() || body["browser_origin"].is_null());
}
#[tokio::test]
async fn two_tabs_no_target_is_409() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _a = FakeBrowser::connect_tagged(ws_port, &token, "https://a.test", "A").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let _b = FakeBrowser::connect_tagged(ws_port, &token, "https://b.test", "B").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/x", "method": "GET"}))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 409);
}
#[tokio::test]
async fn route_by_origin_selects_the_right_tab() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _a = FakeBrowser::connect_tagged(ws_port, &token, "https://a.test", "A").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let _b = FakeBrowser::connect_tagged(ws_port, &token, "https://b.test", "B").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/p", "method": "GET", "target": "https://b.test"}))
.send()
.await
.unwrap();
let env: Value = resp.json().await.unwrap();
assert_eq!(env["body"], "tab:B:/p");
}
#[tokio::test]
async fn route_by_id_header_via_proxy() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _a = FakeBrowser::connect_tagged(ws_port, &token, "https://a.test", "A").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let _b = FakeBrowser::connect_tagged(ws_port, &token, "https://b.test", "B").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let status = fetch_status(&http, &base, &tok).await;
let id_a = status["tabs"]
.as_array()
.unwrap()
.iter()
.find(|t| t["origin"] == "https://a.test")
.unwrap()["id"]
.as_u64()
.unwrap();
let resp = http
.get(format!("{base}/proxied"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.header("x-omni-bridge-target", id_a.to_string())
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.text().await.unwrap(), "tab:A:/proxied");
}
#[tokio::test]
async fn unknown_target_id_is_404() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _a = FakeBrowser::connect_tagged(ws_port, &token, "https://a.test", "A").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/x", "method": "GET", "target": "999"}))
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 404);
}
#[tokio::test]
async fn single_tab_back_compat() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _a = FakeBrowser::connect_tagged(ws_port, &token, "https://only.test", "A").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let status = fetch_status(&http, &base, &tok).await;
assert_eq!(status["browser_origin"], "https://only.test");
assert_eq!(status["tabs"].as_array().unwrap().len(), 1);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.json(&serde_json::json!({"url": "/x", "method": "GET"}))
.send()
.await
.unwrap();
let env: Value = resp.json().await.unwrap();
assert_eq!(env["body"], "tab:A:/x");
}
#[tokio::test]
async fn request_invalid_json_body_is_400() {
let (control_port, _ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.header("content-type", "application/json")
.body("not valid json {")
.send()
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 400);
}
#[tokio::test]
async fn request_target_header_overrides_body_field() {
let (control_port, ws_port, token) = start_bridge(None, Duration::from_secs(5)).await;
let _a = FakeBrowser::connect_tagged(ws_port, &token, "https://a.test", "A").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let _b = FakeBrowser::connect_tagged(ws_port, &token, "https://b.test", "B").await;
tokio::time::sleep(Duration::from_millis(100)).await;
let (http, base, tok) = client(control_port, &token);
let resp = http
.post(format!("{base}/__bridge/request"))
.bearer_auth(&tok)
.header("x-omni-bridge", "1")
.header("x-omni-bridge-target", "https://b.test")
.json(&serde_json::json!({"url": "/p", "method": "GET", "target": "https://a.test"}))
.send()
.await
.unwrap();
let env: Value = resp.json().await.unwrap();
assert_eq!(env["body"], "tab:B:/p");
}