use base64::Engine as _; use crate::config::WebCarrier; 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 selected HTTPS WEB carrier bridge with a fresh CSP nonce. pub(crate) fn render( host: &str, bootstrap: &str, batch_limit: usize, queue_limit: usize, queue_items: usize, carrier: WebCarrier, 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 body = DOCUMENT .replace("__NONCE__", &nonce) .replace("__HOST__", host) .replace("__BOOTSTRAP__", bootstrap) .replace("__BATCH_LIMIT__", &batch_limit.to_string()) .replace("__QUEUE_LIMIT__", &queue_limit.to_string()) .replace("__QUEUE_ITEMS__", &queue_items.to_string()) .replace("__CARRIER__", carrier.as_str()); 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 = r##" Connection "##; #[cfg(test)] mod tests { use super::*; #[test] fn rendered_page_contains_no_template_markers_or_capability() { let page = render( "proxy.example.com", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", 2 * 1024 * 1024, 32 * 1024 * 1024, 16 * 1024, WebCarrier::HttpsLanes, &SecureRandom::new(), ); assert!(!page.body.contains("__")); assert!(!page.body.contains("bridge=")); assert!(page.body.contains("X-Up-Seq")); assert!(page.body.contains("carrier='https-lanes'")); assert!(page.body.contains("X-Lane-ID")); assert!(page.body.contains("const when=Date.parse(header)")); assert!(page.body.contains("},{once:false});")); assert!( page.body .contains("if(!sessionToken)throw new Error('missing session token')") ); assert!(!page.body.contains("welcomeBytes")); assert!( page.body .contains("for(const value of splitFrames(data))if(value.id!==lane.id)") ); assert!( page.body .contains("let frames;try{frames=splitFrames(value)}catch(error){fail();return}") ); assert!( page.content_security_policy .contains("frame-ancestors http://127.0.0.1:*") ); } #[test] fn rendered_page_is_parseable_by_ios_native_carrier() { let bootstrap = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; let page = render( "proxy.example.com", bootstrap, 2 * 1024 * 1024, 32 * 1024 * 1024, 16 * 1024, WebCarrier::Https, &SecureRandom::new(), ); let accepted_shapes = [ format!("const bootstrap=\"{bootstrap}\""), format!("const bootstrap='{bootstrap}'"), format!("bootstrap=\"{bootstrap}\""), ]; assert!( accepted_shapes .iter() .any(|shape| page.body.contains(shape)), "the iOS native carrier cannot parse a comma-declared single-quoted bootstrap" ); } #[test] fn rendered_page_advertises_exact_websocket_carriers() { let websocket = render( "proxy.example.com", "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", 2 * 1024 * 1024, 32 * 1024 * 1024, 16 * 1024, WebCarrier::Websocket, &SecureRandom::new(), ); assert!(websocket.body.contains("carrier='websocket'")); assert!( websocket .body .contains("new WebSocket(socketURL(),'tproxy-v1.'+sessionToken)") ); assert!( websocket .content_security_policy .contains("connect-src 'self' wss://proxy.example.com") ); let lanes = render( "proxy.example.com", "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", 2 * 1024 * 1024, 32 * 1024 * 1024, 16 * 1024, WebCarrier::WebsocketLanes, &SecureRandom::new(), ); assert!(lanes.body.contains("carrier='websocket-lanes'")); assert!( lanes .body .contains("'tproxy-lane-v1.'+sessionToken+'.'+String(lane.id)") ); assert!(!lanes.body.contains("__")); } }