| 1 | //! Live Share on hostile networks: through a proxy that asks for a password, through one that |
| 2 | //! refuses WebSockets (the relay is then reached by plain requests), and the troubles named |
| 3 | //! when the relay can't be reached. One test, as the proxy chosen holds for the process. |
| 4 | #![cfg(feature = "live")] |
| 5 | |
| 6 | use notebook::live::{ |
| 7 | Trouble, |
| 8 | proxy::{self, Proxy}, |
| 9 | share::{self, Refusal, Sharing}, |
| 10 | }; |
| 11 | use std::{ |
| 12 | io::{self, Read, Write}, |
| 13 | net::{SocketAddr, TcpListener, TcpStream}, |
| 14 | sync::{ |
| 15 | Arc, |
| 16 | atomic::{AtomicUsize, Ordering}, |
| 17 | }, |
| 18 | thread, |
| 19 | }; |
| 20 | |
| 21 | #[path = "support/live.rs"] |
| 22 | mod live; |
| 23 | use live::*; |
| 24 | |
| 25 | /// What a proxy on this computer did. |
| 26 | #[derive(Default)] |
| 27 | struct Seen { |
| 28 | tunnels: AtomicUsize, |
| 29 | refused: AtomicUsize, |
| 30 | } |
| 31 | |
| 32 | /// An HTTP proxy that tunnels `CONNECT`s, asking for `credentials` where given and, with |
| 33 | /// `block_websockets`, refusing a WebSocket's upgrade inside the tunnel, as a proxy that |
| 34 | /// inspects HTTPS does. |
| 35 | fn proxy(credentials: Option<&'static str>, block_websockets: bool) -> (SocketAddr, Arc<Seen>) { |
| 36 | let listener = TcpListener::bind("127.0.0.1:0").unwrap(); |
| 37 | let address = listener.local_addr().unwrap(); |
| 38 | let seen = Arc::new(Seen::default()); |
| 39 | let counting = Arc::clone(&seen); |
| 40 | thread::spawn(move || { |
| 41 | for client in listener.incoming().flatten() { |
| 42 | let seen = Arc::clone(&counting); |
| 43 | thread::spawn(move || { |
| 44 | let _ = tunnel(client, credentials, block_websockets, &seen); |
| 45 | }); |
| 46 | } |
| 47 | }); |
| 48 | (address, seen) |
| 49 | } |
| 50 | |
| 51 | fn tunnel( |
| 52 | mut client: TcpStream, |
| 53 | credentials: Option<&str>, |
| 54 | block_websockets: bool, |
| 55 | seen: &Seen, |
| 56 | ) -> io::Result<()> { |
| 57 | let head = relay::ws::head(&mut client)?; |
| 58 | let target = head.split(' ').nth(1).unwrap_or_default().to_owned(); |
| 59 | let authorized = credentials |
| 60 | .is_none_or(|expected| relay::ws::header(&head, "Proxy-Authorization") == Some(expected)); |
| 61 | if !head.starts_with("CONNECT ") || !authorized { |
| 62 | client.write_all( |
| 63 | b"HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic\r\n\ |
| 64 | Content-Length: 0\r\n\r\n", |
| 65 | )?; |
| 66 | return Ok(()); |
| 67 | } |
| 68 | let mut upstream = TcpStream::connect(&target)?; |
| 69 | client.write_all(b"HTTP/1.1 200 Connection established\r\n\r\n")?; |
| 70 | seen.tunnels.fetch_add(1, Ordering::Relaxed); |
| 71 | if block_websockets { |
| 72 | let request = relay::ws::head(&mut client)?; |
| 73 | if relay::ws::header(&request, "Upgrade").is_some() { |
| 74 | seen.refused.fetch_add(1, Ordering::Relaxed); |
| 75 | client.write_all(b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n")?; |
| 76 | return Ok(()); |
| 77 | } |
| 78 | upstream.write_all(request.as_bytes())?; |
| 79 | } |
| 80 | let (mut from, mut to) = (client.try_clone()?, upstream.try_clone()?); |
| 81 | thread::spawn(move || { |
| 82 | let _ = io::copy(&mut from, &mut to); |
| 83 | let _ = to.shutdown(std::net::Shutdown::Both); |
| 84 | }); |
| 85 | let mut buffer = [0; 16 << 10]; |
| 86 | loop { |
| 87 | let length = upstream.read(&mut buffer)?; |
| 88 | if length == 0 { |
| 89 | return Ok(()); |
| 90 | } |
| 91 | client.write_all(&buffer[..length])?; |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | fn through(address: SocketAddr, credentials: Option<(&str, &str)>) { |
| 96 | proxy::use_proxy(Some(Some(Proxy { |
| 97 | host: address.ip().to_string(), |
| 98 | port: address.port(), |
| 99 | credentials: credentials.map(|(name, password)| (name.into(), password.into())), |
| 100 | }))); |
| 101 | } |
| 102 | |
| 103 | /// Shares a notebook and has a guest join it and publish an edit, all through `url`. |
| 104 | fn share_and_edit(directory: &std::path::Path, url: &str) { |
| 105 | let folder = notebook(directory); |
| 106 | let host = host( |
| 107 | &folder, |
| 108 | &directory.join("host"), |
| 109 | &Sharing::new("").unwrap(), |
| 110 | url, |
| 111 | ); |
| 112 | let (guest, notebook) = guest("Grace", &code(&host), url, &directory.join("grace")); |
| 113 | let section = open(&notebook, &guest, "Garden.one", None); |
| 114 | let file = folder.join("Garden.one"); |
| 115 | let id = replace(&section, &std::fs::read(&file).unwrap(), 0..8, "Through"); |
| 116 | published(&section, id); |
| 117 | assert_eq!( |
| 118 | server::text(&std::fs::read(&file).unwrap()).2, |
| 119 | "Through text" |
| 120 | ); |
| 121 | } |
| 122 | |
| 123 | fn refusal(code: &str, url: &str) -> Refusal { |
| 124 | share::join(hello("Mallory"), code, "", None, Some(url)).unwrap_err() |
| 125 | } |
| 126 | |
| 127 | #[test] |
| 128 | fn hostile_networks() { |
| 129 | let directory = tempfile::tempdir().unwrap(); |
| 130 | let url = relay(Default::default()); |
| 131 | let code = notebook::live::code::format(412, "4MZ9XR").unwrap(); |
| 132 | |
| 133 | // A proxy that asks for a password: refused without, through with it. |
| 134 | let (address, seen) = proxy(Some("Basic YWRhOnNlY3JldA=="), false); |
| 135 | through(address, None); |
| 136 | assert_eq!( |
| 137 | refusal(&code, &url), |
| 138 | Refusal::Unreachable(Trouble::ProxyAuthentication) |
| 139 | ); |
| 140 | through(address, Some(("ada", "secret"))); |
| 141 | share_and_edit(&directory.path().join("connect"), &url); |
| 142 | assert!( |
| 143 | seen.tunnels.load(Ordering::Relaxed) >= 3, |
| 144 | "host, guest and joiner tunnel" |
| 145 | ); |
| 146 | |
| 147 | // A proxy that refuses WebSockets: the relay is reached by requests instead. |
| 148 | let (address, seen) = proxy(None, true); |
| 149 | through(address, None); |
| 150 | share_and_edit(&directory.path().join("blocked"), &url); |
| 151 | assert!(seen.refused.load(Ordering::Relaxed) >= 1); |
| 152 | |
| 153 | // A proxy that isn't there, and a relay whose name doesn't resolve. |
| 154 | let closed = TcpListener::bind("127.0.0.1:0") |
| 155 | .unwrap() |
| 156 | .local_addr() |
| 157 | .unwrap(); |
| 158 | through(closed, None); |
| 159 | assert_eq!( |
| 160 | refusal(&code, &url), |
| 161 | Refusal::Unreachable(Trouble::ProxyUnreachable) |
| 162 | ); |
| 163 | proxy::use_proxy(Some(None)); |
| 164 | assert_eq!( |
| 165 | refusal(&code, "wss://relay.invalid"), |
| 166 | Refusal::Unreachable(Trouble::Dns) |
| 167 | ); |
| 168 | } |