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
6use notebook::live::{
7 Trouble,
8 proxy::{self, Proxy},
9 share::{self, Refusal, Sharing},
10};
11use 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"]
22mod live;
23use live::*;
24
25/// What a proxy on this computer did.
26#[derive(Default)]
27struct 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.
35fn 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
51fn 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
95fn 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`.
104fn 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
123fn refusal(code: &str, url: &str) -> Refusal {
124 share::join(hello("Mallory"), code, "", None, Some(url)).unwrap_err()
125}
126
127#[test]
128fn 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}