mirror of
https://github.com/telemt/telemt.git
synced 2026-10-27 11:07:39 +03:00
823c6cbc7f
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
186 lines
7.5 KiB
Rust
186 lines
7.5 KiB
Rust
use base64::Engine as _;
|
|
|
|
use crate::config::WebCarrierMethod;
|
|
use crate::crypto::SecureRandom;
|
|
|
|
/// Browser security policy for the transient Telegram Desktop bridge page.
|
|
pub(crate) const PERMISSIONS_POLICY: &str = "accelerometer=(), autoplay=(), camera=(), clipboard-read=(), clipboard-write=(), display-capture=(), encrypted-media=(), fullscreen=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), publickey-credentials-create=(), publickey-credentials-get=(), screen-wake-lock=(), serial=(), usb=(), web-share=(), xr-spatial-tracking=()";
|
|
|
|
/// Fully rendered bridge response and its per-response script policy.
|
|
pub(crate) struct BridgePage {
|
|
/// Complete transient HTML document.
|
|
pub(crate) body: String,
|
|
/// Nonce-bound policy that authorizes only the embedded bridge script.
|
|
pub(crate) content_security_policy: String,
|
|
}
|
|
|
|
/// Renders the bounded WEB carrier-negotiation bridge with a fresh CSP nonce.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) fn render(
|
|
host: &str,
|
|
base: &str,
|
|
bootstrap: &str,
|
|
batch_limit: usize,
|
|
queue_limit: usize,
|
|
queue_items: usize,
|
|
max_streams: usize,
|
|
negotiation_enabled: bool,
|
|
candidate_count: usize,
|
|
carrier_deadlines: [u64; 4],
|
|
long_poll_secs: u64,
|
|
bridge_request_secs: u64,
|
|
bridge_retry_secs: u64,
|
|
bridge_recovery_secs: u64,
|
|
websocket_open_secs: u64,
|
|
reconnect_grace_secs: u64,
|
|
carrier_probe_coalesce_ms: u64,
|
|
bridge_diagnostics_enabled: bool,
|
|
carrier_method: WebCarrierMethod,
|
|
rng: &SecureRandom,
|
|
) -> BridgePage {
|
|
let mut nonce = [0u8; 18];
|
|
rng.fill(&mut nonce);
|
|
let nonce = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(nonce);
|
|
let diagnostic_script = if bridge_diagnostics_enabled {
|
|
format!("<script nonce=\"__NONCE__\">\n{DIAGNOSTIC_RUNTIME}\n</script>\n")
|
|
} else {
|
|
String::new()
|
|
};
|
|
let diagnostic_hook = |method: &str| {
|
|
bridge_diagnostics_enabled
|
|
.then(|| {
|
|
format!("if(clientDiagnostics)try{{clientDiagnostics.{method}()}}catch(error){{}}")
|
|
})
|
|
.unwrap_or_default()
|
|
};
|
|
let diagnostic_runtime_started = if bridge_diagnostics_enabled {
|
|
format!("{}\n", diagnostic_hook("runtimeStarted"))
|
|
} else {
|
|
String::new()
|
|
};
|
|
let base_prefix = base.strip_suffix('/').unwrap_or(base);
|
|
let body = DOCUMENT
|
|
.replace("__DIAGNOSTIC_RUNTIME__\n", &diagnostic_script)
|
|
.replace("__RESPONSE_RUNTIME__", RESPONSE_RUNTIME)
|
|
.replace("__REQUEST_RUNTIME__", REQUEST_RUNTIME)
|
|
.replace("__BUFFER_RUNTIME__", BUFFER_RUNTIME)
|
|
.replace("__RECOVERY_RUNTIME__", RECOVERY_RUNTIME)
|
|
.replace("__DOWNLINK_RUNTIME__", DOWNLINK_RUNTIME)
|
|
.replace("__CONVEYOR_RUNTIME__", CONVEYOR_RUNTIME)
|
|
.replace("__RUNTIME__", RUNTIME)
|
|
.replace(
|
|
"__DIAGNOSTIC_BINDING__;\n",
|
|
if bridge_diagnostics_enabled {
|
|
"const clientDiagnostics=globalThis.TelemtBridgeDiagnostics;\n"
|
|
} else {
|
|
""
|
|
},
|
|
)
|
|
.replace(
|
|
"__DIAGNOSTIC_RUNTIME_STARTED__;\n",
|
|
&diagnostic_runtime_started,
|
|
)
|
|
.replace(
|
|
"__DIAGNOSTIC_BOUNDARY_ACTIVATED__;",
|
|
&diagnostic_hook("boundaryActivated"),
|
|
)
|
|
.replace(
|
|
"__STATUS_FUNCTION__",
|
|
if bridge_diagnostics_enabled {
|
|
"state=>{if(port&&!closed){port.postMessage({t:'status',state});if(clientDiagnostics)try{clientDiagnostics.statusPosted()}catch(error){}}}"
|
|
} else {
|
|
"state=>{if(port&&!closed)port.postMessage({t:'status',state})}"
|
|
},
|
|
)
|
|
.replace(
|
|
"__DIAGNOSTIC_HELLO_RECEIVED__;",
|
|
&diagnostic_hook("helloReceived"),
|
|
)
|
|
.replace(
|
|
"__HELLO_TIMEOUT_CALLBACK__",
|
|
if bridge_diagnostics_enabled {
|
|
"()=>{if(clientDiagnostics)try{clientDiagnostics.helloTimeout()}catch(error){}fail('timeout')}"
|
|
} else {
|
|
"()=>fail('timeout')"
|
|
},
|
|
)
|
|
.replace(
|
|
"__DIAGNOSTIC_CLIENT_CLOSE__;",
|
|
&diagnostic_hook("clientCloseBeforeHello"),
|
|
)
|
|
.replace(
|
|
"__PAGEHIDE_CALLBACK__",
|
|
if bridge_diagnostics_enabled {
|
|
"()=>{if(clientDiagnostics)try{clientDiagnostics.documentUnloadedBeforeHello()}catch(error){}fail('navigation')}"
|
|
} else {
|
|
"()=>fail('navigation')"
|
|
},
|
|
)
|
|
.replace(
|
|
"__DIAGNOSTIC_BOOTSTRAP_REPLACED__;",
|
|
if bridge_diagnostics_enabled {
|
|
"if(clientDiagnostics)try{clientDiagnostics.setBootstrap(bootstrap)}catch(error){}"
|
|
} else {
|
|
""
|
|
},
|
|
)
|
|
.replace("__NONCE__", &nonce)
|
|
.replace("__HOST__", host)
|
|
.replace("__BASE_PREFIX__", base_prefix)
|
|
.replace("__BOOTSTRAP__", bootstrap)
|
|
.replace("__CARRIER_METHOD__", carrier_method.as_str())
|
|
.replace("__BATCH_LIMIT__", &batch_limit.to_string())
|
|
.replace("__QUEUE_LIMIT__", &queue_limit.to_string())
|
|
.replace("__QUEUE_ITEMS__", &queue_items.to_string())
|
|
.replace("__MAX_STREAMS__", &max_streams.to_string())
|
|
.replace(
|
|
"__NEGOTIATION_ENABLED__",
|
|
if negotiation_enabled { "true" } else { "false" },
|
|
)
|
|
.replace("__CANDIDATE_COUNT__", &candidate_count.to_string())
|
|
.replace("__LONG_POLL_SECS__", &long_poll_secs.to_string())
|
|
.replace("__BRIDGE_REQUEST_SECS__", &bridge_request_secs.to_string())
|
|
.replace("__BRIDGE_RETRY_SECS__", &bridge_retry_secs.to_string())
|
|
.replace(
|
|
"__BRIDGE_RECOVERY_SECS__",
|
|
&bridge_recovery_secs.to_string(),
|
|
)
|
|
.replace("__WEBSOCKET_OPEN_SECS__", &websocket_open_secs.to_string())
|
|
.replace(
|
|
"__RECONNECT_GRACE_SECS__",
|
|
&reconnect_grace_secs.to_string(),
|
|
)
|
|
.replace(
|
|
"__CARRIER_PROBE_COALESCE_MS__",
|
|
&carrier_probe_coalesce_ms.to_string(),
|
|
)
|
|
.replace(
|
|
"__CARRIER_DEADLINES__",
|
|
&carrier_deadlines
|
|
.iter()
|
|
.map(u64::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join(","),
|
|
);
|
|
BridgePage {
|
|
body,
|
|
content_security_policy: format!(
|
|
"default-src 'none'; base-uri 'none'; child-src 'none'; connect-src 'self' wss://{host}; font-src 'none'; form-action 'none'; frame-ancestors http://127.0.0.1:*; frame-src 'none'; img-src 'none'; manifest-src 'none'; media-src 'none'; object-src 'none'; script-src 'nonce-{nonce}'; style-src 'none'; worker-src 'none'; sandbox allow-same-origin allow-scripts"
|
|
),
|
|
}
|
|
}
|
|
|
|
const DOCUMENT: &str = include_str!("bridge/document.html");
|
|
const DIAGNOSTIC_RUNTIME: &str = include_str!("bridge/diagnostic.js");
|
|
const RESPONSE_RUNTIME: &str = include_str!("bridge/response.js");
|
|
const REQUEST_RUNTIME: &str = include_str!("bridge/request.js");
|
|
const BUFFER_RUNTIME: &str = include_str!("bridge/buffers.js");
|
|
const RECOVERY_RUNTIME: &str = include_str!("bridge/recovery.js");
|
|
const DOWNLINK_RUNTIME: &str = include_str!("bridge/downlink.js");
|
|
const CONVEYOR_RUNTIME: &str = include_str!("bridge/conveyor.js");
|
|
const RUNTIME: &str = include_str!("bridge/runtime.js");
|
|
|
|
// Rendered wire-contract tests remain separate from the embedded document.
|
|
#[cfg(test)]
|
|
mod tests;
|