Skip to main content

rich_web/
server.rs

1//! The server: [`serve`], [`serve_command`] and [`Server`], behind the
2//! crate's `server` feature (on by default). Re-exported at the crate root.
3
4/// What a program runs on: the PTY host trait and the local implementation,
5/// from rs-rich-embed.
6pub use rich_embed::{Command, ExitStatus, LocalPty, PtyHost};
7
8use std::io::{self, Write};
9use std::net::{
10    IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs,
11};
12use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
14use std::thread::{self, JoinHandle};
15use std::time::{Duration, Instant};
16
17use intuituive::App;
18use tungstenite::WebSocket;
19use tungstenite::protocol::{Role, WebSocketConfig};
20
21use crate::http::{self, Head};
22use crate::{isolate, pty, session};
23
24pub use crate::isolate::{Isolation, SESSION_ENV};
25
26/// The version of xterm.js the page uses, vendored in this crate.
27pub const XTERM_VERSION: &str = "6.0.0";
28
29/// The sessions a server runs at once unless told otherwise.
30pub const DEFAULT_MAX_SESSIONS: usize = 8;
31
32/// The output a program's session holds for its page unless told
33/// otherwise ([`Server::max_buffered`]): 1 MiB.
34pub const DEFAULT_MAX_BUFFERED: usize = 1 << 20;
35
36/// Connections still sending their request at once; more are answered
37/// `503`.
38const MAX_PENDING: usize = 64;
39/// How long a connection may take to send its request, all of it.
40const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
41/// How long a write to a browser may take before the session ends.
42const WRITE_TIMEOUT: Duration = Duration::from_secs(30);
43/// The largest message a page may send (a big paste).
44pub(crate) const MAX_MESSAGE: usize = 1 << 20;
45
46const PAGE: &str = include_str!("../assets/index.html");
47const PAGE_JS: &str = include_str!("../assets/app.js");
48const DOM_PAGE: &str = include_str!("../assets/dom.html");
49const DOM_JS: &str = include_str!("../assets/dom.js");
50const XTERM_JS: &str = include_str!("../assets/xterm/xterm.js");
51const XTERM_CSS: &str = include_str!("../assets/xterm/xterm.css");
52const FIT_JS: &str = include_str!("../assets/xterm/addon-fit.js");
53
54/// Serve `app` at `addr`: print the URL to open (with its token), then run
55/// one app, made by `app`, per browser tab that opens it, until the process
56/// ends. [`Server::bind`] and [`Server::run`] with the defaults.
57///
58/// `app` is called in each session's own process (or, with
59/// [`Isolation::Threads`], on its own thread), so the [`App`] itself never
60/// crosses threads.
61pub fn serve<A, F>(addr: A, app: F) -> io::Result<()>
62where
63    A: ToSocketAddrs,
64    F: Fn() -> App + Send + Sync + 'static,
65{
66    Server::bind(addr, app)?.run()
67}
68
69/// Serve `command` at `addr`: print the URL to open (with its token), then
70/// run the program on a pseudo-terminal of its own for each browser tab
71/// that opens it, until the process ends. [`Server::bind_command`] and
72/// [`Server::run`] with the defaults.
73///
74/// A string is a program name; an array or a vector is the program and its
75/// arguments; a [`Command`] also sets its environment and directory.
76pub fn serve_command<A>(addr: A, command: impl Into<Command>) -> io::Result<()>
77where
78    A: ToSocketAddrs,
79{
80    Server::bind_command(addr, command)?.run()
81}
82
83/// How an app's page draws it.
84#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
85#[non_exhaustive]
86pub enum Renderer {
87    /// A terminal: xterm.js, fed the app's frames as terminal output.
88    #[default]
89    Xterm,
90    /// The page's own DOM: a grid of styled spans, with the app's
91    /// accessibility tree as ARIA for a browser's screen reader. Apps only:
92    /// a program on a PTY has no tree, and is always drawn by xterm.js.
93    Dom,
94}
95
96impl Renderer {
97    /// `"xterm"` or `"dom"`, as the page's address names it.
98    pub fn name(self) -> &'static str {
99        match self {
100            Renderer::Xterm => "xterm",
101            Renderer::Dom => "dom",
102        }
103    }
104
105    /// The renderer called `name` (`"xterm"`, `"dom"`).
106    pub fn parse(name: &str) -> Option<Renderer> {
107        match name {
108            "xterm" => Some(Renderer::Xterm),
109            "dom" => Some(Renderer::Dom),
110            _ => None,
111        }
112    }
113}
114
115/// What each session runs.
116enum Mode {
117    App(Box<dyn Fn() -> App + Send + Sync>),
118    /// A program on a [`LocalPty`], held back by the page's pace.
119    Command(Command),
120    /// A program on another host.
121    Host(Box<dyn Fn() -> Box<dyn PtyHost> + Send + Sync>),
122}
123
124/// A server that is bound and ready to [`run`](Self::run) or
125/// [`spawn`](Self::spawn), with its options.
126pub struct Server {
127    listener: TcpListener,
128    shared: Shared,
129}
130
131/// What every connection's thread reads.
132struct Shared {
133    mode: Mode,
134    token: String,
135    max_sessions: usize,
136    max_buffered: usize,
137    renderer: Renderer,
138    /// Compress frames from this size, for pages that ask.
139    compress: Option<usize>,
140    origins: Vec<String>,
141    title: String,
142    /// The `Host` values the server answers to, besides each connection's
143    /// own address (`http::allowed_hosts`, then `allow_host`'s).
144    hosts: Vec<String>,
145    /// Sessions running.
146    sessions: AtomicUsize,
147    /// Connections still sending their request.
148    pending: AtomicUsize,
149    /// Programs (and sessions' processes) started and not yet ended.
150    programs: AtomicUsize,
151    stopping: AtomicBool,
152    /// Where an app's sessions run, and how their processes start.
153    isolation: Isolation,
154    spawn: isolate::Spawn,
155    /// This server's number among the app servers this program binds, which
156    /// a session's process uses to find the same one.
157    bind: usize,
158}
159
160impl Server {
161    /// Listen on `addr` (`"127.0.0.1:0"` picks a free port) with a fresh
162    /// random token, to serve the apps `app` makes.
163    ///
164    /// In a session's own process ([`Isolation::Processes`], the default),
165    /// this is where the session runs: the process this program started
166    /// for a browser tab reaches this same call, runs the app for that tab,
167    /// and exits here, without listening.
168    pub fn bind<A, F>(addr: A, app: F) -> io::Result<Server>
169    where
170        A: ToSocketAddrs,
171        F: Fn() -> App + Send + Sync + 'static,
172    {
173        let bind = isolate::next_bind();
174        if let Some(spec) = isolate::session_for(bind) {
175            std::process::exit(isolate::run_child(spec, app));
176        }
177        let mode = Mode::App(Box::new(app));
178        let mut server = if isolate::is_session_process() {
179            // Another server of this program: a session's process does not
180            // take its port.
181            Server::with_mode(("127.0.0.1", 0), mode, "intuiTUIve".into())?
182        } else {
183            Server::with_mode(addr, mode, "intuiTUIve".into())?
184        };
185        server.shared.bind = bind;
186        Ok(server)
187    }
188
189    /// Listen on `addr` with a fresh random token, to run `command` on a
190    /// pseudo-terminal of its own for each tab (through [`LocalPty`]). The
191    /// page's title is the program's name unless set.
192    pub fn bind_command<A>(addr: A, command: impl Into<Command>) -> io::Result<Server>
193    where
194        A: ToSocketAddrs,
195    {
196        let command = command.into();
197        let title = command.program().to_string_lossy().into_owned();
198        Server::with_mode(addr, Mode::Command(command), title)
199    }
200
201    /// Listen on `addr` with a fresh random token, to run a program on the
202    /// [`PtyHost`] `host` makes for each tab: an SSH session, a container's
203    /// exec stream, or a test host. It is made, started and dropped on the
204    /// session's own thread. [`max_buffered`](Self::max_buffered) bounds
205    /// only what the session holds: a host that reads ahead buffers on its
206    /// own terms.
207    pub fn bind_host<A, F>(addr: A, host: F) -> io::Result<Server>
208    where
209        A: ToSocketAddrs,
210        F: Fn() -> Box<dyn PtyHost> + Send + Sync + 'static,
211    {
212        Server::with_mode(addr, Mode::Host(Box::new(host)), "terminal".into())
213    }
214
215    fn with_mode<A: ToSocketAddrs>(addr: A, mode: Mode, title: String) -> io::Result<Server> {
216        let listener = TcpListener::bind(addr)?;
217        let hosts = http::allowed_hosts(listener.local_addr()?);
218        Ok(Server {
219            listener,
220            shared: Shared {
221                mode,
222                token: random_token()?,
223                max_sessions: DEFAULT_MAX_SESSIONS,
224                max_buffered: DEFAULT_MAX_BUFFERED,
225                renderer: Renderer::Xterm,
226                compress: None,
227                origins: Vec::new(),
228                title,
229                hosts,
230                sessions: AtomicUsize::new(0),
231                pending: AtomicUsize::new(0),
232                programs: AtomicUsize::new(0),
233                stopping: AtomicBool::new(false),
234                isolation: Isolation::default(),
235                spawn: isolate::Spawn::default(),
236                bind: 0,
237            },
238        })
239    }
240
241    /// Where an app's sessions run: each in a process of its own
242    /// ([`Isolation::Processes`], the default), or each on a thread of the
243    /// server ([`Isolation::Threads`]). A program's sessions (a command, a
244    /// host) are processes already, and this does not change them.
245    #[must_use]
246    pub fn isolation(mut self, isolation: Isolation) -> Server {
247        self.shared.isolation = isolation;
248        self
249    }
250
251    /// Start each session's process as `program` with `args`, rather than
252    /// as this program with the arguments it was given. It must reach the
253    /// same `serve` or `Server::bind` call: an interpreter running the same
254    /// script, say (Python's `rs_rich.tui.serve` passes its own).
255    #[must_use]
256    pub fn session_command<P, I, S>(mut self, program: P, args: I) -> Server
257    where
258        P: Into<std::path::PathBuf>,
259        I: IntoIterator<Item = S>,
260        S: Into<std::ffi::OsString>,
261    {
262        let args = args.into_iter().map(Into::into).collect();
263        self.shared.spawn.command = Some((program.into(), args));
264        self
265    }
266
267    /// Cap each session's process at `bytes` of memory (its address
268    /// space): an allocation past it fails, and the process ends, alone.
269    /// On Linux it holds from the moment the process starts; on other Unix
270    /// systems from when it reaches `serve`; macOS has no such limit, and
271    /// elsewhere (Windows) it is not applied.
272    #[must_use]
273    pub fn session_memory(mut self, bytes: u64) -> Server {
274        self.shared.spawn.memory = Some(bytes);
275        self
276    }
277
278    /// Cap each session's process at `time` of CPU (whole seconds, at least
279    /// one): past it the process is killed, alone. A session that waits for
280    /// input uses none. On Linux it holds from the moment the process
281    /// starts; on other Unix systems from when it reaches `serve`;
282    /// elsewhere (Windows) it is not applied.
283    #[must_use]
284    pub fn session_cpu(mut self, time: Duration) -> Server {
285        self.shared.spawn.cpu = Some(time);
286        self
287    }
288
289    /// Run at most `n` sessions at once (8 by default); a browser that
290    /// opens one more is refused until one ends.
291    #[must_use]
292    pub fn max_sessions(mut self, n: usize) -> Server {
293        self.shared.max_sessions = n;
294        self
295    }
296
297    /// For a program: hold at most about `bytes` of its output for a page
298    /// that has not shown it yet (1 MiB by default). Half is output sent
299    /// and not yet acknowledged by the page, half is output read from the
300    /// program and not yet sent; past that, the program's writes wait until
301    /// the page catches up, as on a slow terminal, so nothing is dropped
302    /// and memory stays bounded however fast it writes, at any size (a
303    /// [`LocalPty`] with backpressure never drops output, even past its
304    /// own 16 MiB limit without it). Apps send whole frames and are not
305    /// affected.
306    #[must_use]
307    pub fn max_buffered(mut self, bytes: usize) -> Server {
308        self.shared.max_buffered = bytes.max(2);
309        self
310    }
311
312    /// Compress frames of `from` bytes or more with deflate, for pages
313    /// that can undo it (they have `DecompressionStream`, and ask with
314    /// `z=1`): an app's frames, the DOM renderer's messages and a
315    /// program's output. Off (`None`) by default. Worth it over a slow
316    /// link: measured on a few scenes, frames shrink to 5 to 30% of their
317    /// size for 15 to 60 µs each (level 1). On a local machine it only
318    /// adds work. 1024 is a fair threshold.
319    #[must_use]
320    pub fn compress(mut self, from: Option<usize>) -> Server {
321        self.shared.compress = from;
322        self
323    }
324
325    /// How an app's page draws it: [`Renderer::Xterm`] (the default) or
326    /// [`Renderer::Dom`]. A page's address can ask for either with
327    /// `&renderer=xterm` or `&renderer=dom`. A program is always drawn by
328    /// xterm.js.
329    #[must_use]
330    pub fn renderer(mut self, renderer: Renderer) -> Server {
331        self.shared.renderer = renderer;
332        self
333    }
334
335    /// Use `token` instead of a random one, for an address that stays the
336    /// same between runs. Keep it as secret, and as long: anyone with it
337    /// can use the app.
338    ///
339    /// # Panics
340    /// When `token` is empty or has characters other than ASCII letters,
341    /// digits, `-`, `_`, `.` and `~` (it goes in a URL as it is).
342    #[must_use]
343    pub fn token(mut self, token: impl Into<String>) -> Server {
344        let token = token.into();
345        assert!(
346            !token.is_empty()
347                && token
348                    .bytes()
349                    .all(|b| b.is_ascii_alphanumeric() || b"-_.~".contains(&b)),
350            "a token is ASCII letters, digits, '-', '_', '.' and '~'"
351        );
352        self.shared.token = token;
353        self
354    }
355
356    /// Also answer to `host` (say `"box.lan"`, or `"box.lan:9000"`): a name
357    /// this machine is reached by that the server does not know itself. The
358    /// server's port is added unless `host` names one. The `Host` of a
359    /// WebSocket upgrade must be one the server answers to: its bound
360    /// address (with `localhost` for a loopback one), or, bound to all
361    /// interfaces, the loopback addresses, `localhost`, this machine's own
362    /// name and the address the connection arrived on; plus these. That is
363    /// what stops a page whose name was pointed here (DNS rebinding). The
364    /// token is still required.
365    #[must_use]
366    pub fn allow_host(mut self, host: impl AsRef<str>) -> Server {
367        let port = self.local_addr().port();
368        self.shared
369            .hosts
370            .extend(http::host_entries(host.as_ref(), port));
371        self
372    }
373
374    /// Also accept WebSocket upgrades from pages served at `origin` (say
375    /// `"https://term.example.com"`): the address a reverse proxy serves
376    /// this server's page from. The token is still required.
377    #[must_use]
378    pub fn allow_origin(mut self, origin: impl Into<String>) -> Server {
379        self.shared.origins.push(origin.into());
380        self
381    }
382
383    /// The page's title (`intuiTUIve` for an app, the program's name for a
384    /// command, by default).
385    #[must_use]
386    pub fn title(mut self, title: impl Into<String>) -> Server {
387        self.shared.title = title.into();
388        self
389    }
390
391    /// The address the server listens on.
392    pub fn local_addr(&self) -> SocketAddr {
393        self.listener
394            .local_addr()
395            .expect("a bound listener has an address")
396    }
397
398    /// The token browsers must give.
399    pub fn access_token(&self) -> &str {
400        &self.shared.token
401    }
402
403    /// The address to open in a browser, with the token.
404    pub fn url(&self) -> String {
405        url_for(self.local_addr(), &self.shared.token)
406    }
407
408    /// Print the URL (and a warning when the address is not loopback),
409    /// then serve until the process ends.
410    pub fn run(self) -> io::Result<()> {
411        self.announce()?;
412        accept(self.listener, Arc::new(self.shared));
413        Ok(())
414    }
415
416    /// Print the URL as [`run`](Self::run) does, then serve until `until`
417    /// returns, and stop as [`Handle::stop`] does: every session ends, and
418    /// this returns once their programs have. `until` runs on this thread:
419    /// a wait for a signal, say, as `rich serve` waits for Ctrl+C, so that
420    /// no program outlives the server.
421    pub fn run_until(self, until: impl FnOnce()) -> io::Result<()> {
422        self.announce()?;
423        let handle = self.spawn()?;
424        until();
425        handle.stop();
426        Ok(())
427    }
428
429    /// Print the URL, and a warning when the address is not loopback.
430    fn announce(&self) -> io::Result<()> {
431        let local = self.local_addr();
432        let mut out = io::stdout().lock();
433        writeln!(out, "Serving {} at {}", self.shared.title, self.url())?;
434        out.flush()?;
435        drop(out);
436        if !local.ip().is_loopback() {
437            let what = match self.shared.mode {
438                Mode::App(_) => "the app",
439                Mode::Command(_) | Mode::Host(_) => "the program",
440            };
441            eprintln!(
442                "warning: listening on {local}, beyond this computer. Anyone who can reach it \
443                 with the token can use {what}: there is no other authentication and no TLS. \
444                 Put it behind a reverse proxy that has both."
445            );
446        }
447        Ok(())
448    }
449
450    /// Serve on a thread of its own, printing nothing, until the
451    /// [`Handle`] is stopped or dropped.
452    pub fn spawn(self) -> io::Result<Handle> {
453        let local = self.local_addr();
454        let url = self.url();
455        let shared = Arc::new(self.shared);
456        let listener = self.listener;
457        let thread = {
458            let shared = shared.clone();
459            thread::Builder::new()
460                .name("rich-web accept".into())
461                .spawn(move || accept(listener, shared))?
462        };
463        Ok(Handle {
464            local,
465            url,
466            shared,
467            thread: Some(thread),
468        })
469    }
470}
471
472/// A server running in the background, from [`Server::spawn`]. Stopping it
473/// (or dropping it) stops accepting connections and ends every session.
474pub struct Handle {
475    local: SocketAddr,
476    url: String,
477    shared: Arc<Shared>,
478    thread: Option<JoinHandle<()>>,
479}
480
481impl Handle {
482    /// The address the server listens on.
483    pub fn local_addr(&self) -> SocketAddr {
484        self.local
485    }
486
487    /// The address to open in a browser, with the token.
488    pub fn url(&self) -> &str {
489        &self.url
490    }
491
492    /// The sessions running now.
493    pub fn sessions(&self) -> usize {
494        self.shared.sessions.load(Ordering::SeqCst)
495    }
496
497    /// Stop: no more connections, and every session ends (within one turn
498    /// of its loop, at most 50 ms). A program's session ends its program:
499    /// it is hung up on, and a [`LocalPty`]'s is killed, with its process
500    /// group, if it is still running a second later. This returns once
501    /// every program has ended; a connection still sending its request is
502    /// not served.
503    pub fn stop(mut self) {
504        self.shutdown();
505    }
506
507    fn shutdown(&mut self) {
508        self.shared.stopping.store(true, Ordering::SeqCst);
509        if let Some(accepting) = self.thread.take() {
510            // Wake the accept loop, which then sees `stopping`.
511            let _ = TcpStream::connect_timeout(&reachable(self.local), Duration::from_secs(1));
512            let _ = accepting.join();
513            // A session counts its program before it looks at `stopping`.
514            while self.shared.programs.load(Ordering::SeqCst) > 0 {
515                thread::sleep(Duration::from_millis(10));
516            }
517        }
518    }
519}
520
521impl Drop for Handle {
522    fn drop(&mut self) {
523        self.shutdown();
524    }
525}
526
527/// The address to connect to for `local`: loopback when it is every
528/// interface.
529fn reachable(local: SocketAddr) -> SocketAddr {
530    let ip = match local.ip() {
531        IpAddr::V4(v4) if v4.is_unspecified() => IpAddr::V4(Ipv4Addr::LOCALHOST),
532        IpAddr::V6(v6) if v6.is_unspecified() => IpAddr::V6(Ipv6Addr::LOCALHOST),
533        ip => ip,
534    };
535    SocketAddr::new(ip, local.port())
536}
537
538fn url_for(local: SocketAddr, token: &str) -> String {
539    format!("http://{}/?token={token}", reachable(local))
540}
541
542/// 128 bits from the operating system's random source, as hex.
543pub(crate) fn random_token() -> io::Result<String> {
544    let mut bytes = [0u8; 16];
545    getrandom::fill(&mut bytes).map_err(|e| io::Error::other(format!("no random token: {e}")))?;
546    Ok(bytes.iter().map(|b| format!("{b:02x}")).collect())
547}
548
549fn accept(listener: TcpListener, shared: Arc<Shared>) {
550    for stream in listener.incoming() {
551        if shared.stopping.load(Ordering::SeqCst) {
552            break;
553        }
554        let Ok(stream) = stream else {
555            // Out of file descriptors, say: let some go before the next.
556            thread::sleep(Duration::from_millis(10));
557            continue;
558        };
559        if shared.pending.fetch_add(1, Ordering::SeqCst) >= MAX_PENDING {
560            shared.pending.fetch_sub(1, Ordering::SeqCst);
561            busy(stream);
562            continue;
563        }
564        let pending = Pending(shared.clone());
565        let spawned = thread::Builder::new()
566            .name("rich-web connection".into())
567            .spawn(move || connection(stream, pending));
568        if spawned.is_err() {
569            // The closure, and the `Pending` in it, were dropped.
570            continue;
571        }
572    }
573}
574
575/// Answer a connection past [`MAX_PENDING`] with `503`, without waiting
576/// for it: the answer is small enough for the socket's buffer.
577fn busy(mut stream: TcpStream) {
578    if stream.set_nonblocking(true).is_err() {
579        return;
580    }
581    // What the request has sent so far, so closing does not reset the
582    // connection before the answer is read.
583    let _ = io::Read::read(&mut stream, &mut [0u8; 4096]);
584    let _ = http::refuse(
585        &mut stream,
586        "503 Service Unavailable",
587        "Too many connections at once: try again in a moment.",
588    );
589    let _ = stream.shutdown(Shutdown::Write);
590}
591
592/// A connection still sending its request: counted until dropped.
593struct Pending(Arc<Shared>);
594
595impl Drop for Pending {
596    fn drop(&mut self) {
597        self.0.pending.fetch_sub(1, Ordering::SeqCst);
598    }
599}
600
601/// A running session: counted until dropped.
602struct Slot(Arc<Shared>);
603
604impl Slot {
605    fn acquire(shared: &Arc<Shared>) -> Option<Slot> {
606        let mut n = shared.sessions.load(Ordering::SeqCst);
607        loop {
608            if n >= shared.max_sessions {
609                return None;
610            }
611            match shared
612                .sessions
613                .compare_exchange(n, n + 1, Ordering::SeqCst, Ordering::SeqCst)
614            {
615                Ok(_) => return Some(Slot(shared.clone())),
616                Err(now) => n = now,
617            }
618        }
619    }
620}
621
622impl Drop for Slot {
623    fn drop(&mut self) {
624        self.0.sessions.fetch_sub(1, Ordering::SeqCst);
625    }
626}
627
628/// Serve one connection: the page, an asset, or a session.
629fn connection(mut stream: TcpStream, pending: Pending) {
630    let _ = stream.set_nodelay(true);
631    if stream.set_write_timeout(Some(WRITE_TIMEOUT)).is_err() {
632        return;
633    }
634    // The whole head within the timeout, however slowly it comes.
635    let until = Instant::now() + HANDSHAKE_TIMEOUT;
636    let Ok(bytes) = http::read_head(&mut http::Deadline::new(&stream, until)) else {
637        return;
638    };
639    // The server stopped while the request came in.
640    if pending.0.stopping.load(Ordering::SeqCst) {
641        return;
642    }
643    let Some(head) = Head::parse(&bytes) else {
644        let _ = http::refuse(&mut stream, "400 Bad Request", "Bad request");
645        return;
646    };
647    let shared = pending.0.clone();
648    if head.method != "GET" {
649        let _ = http::refuse(&mut stream, "405 Method Not Allowed", "Only GET");
650        return;
651    }
652    let js = "text/javascript; charset=utf-8";
653    let asset = match head.path.as_str() {
654        "/ws" => {
655            websocket(stream, &head, &shared, pending);
656            return;
657        }
658        "/" | "/index.html" => {
659            page(&mut stream, &head, &shared);
660            return;
661        }
662        "/app.js" => (js, PAGE_JS),
663        "/dom.js" => (js, DOM_JS),
664        "/xterm.js" => (js, XTERM_JS),
665        "/addon-fit.js" => (js, FIT_JS),
666        "/xterm.css" => ("text/css; charset=utf-8", XTERM_CSS),
667        _ => {
668            let _ = http::refuse(&mut stream, "404 Not Found", "Not found");
669            return;
670        }
671    };
672    let _ = http::respond(&mut stream, "200 OK", asset.0, &[], asset.1.as_bytes());
673}
674
675/// The renderer a request asks for: its `renderer` parameter, else the
676/// server's. A program is always drawn by xterm.js.
677fn renderer_for(head: &Head, shared: &Shared) -> Renderer {
678    match shared.mode {
679        Mode::App(_) => head
680            .param("renderer")
681            .and_then(Renderer::parse)
682            .unwrap_or(shared.renderer),
683        Mode::Command(_) | Mode::Host(_) => Renderer::Xterm,
684    }
685}
686
687/// The page, for a request with the token.
688fn page(stream: &mut TcpStream, head: &Head, shared: &Shared) {
689    if !head
690        .param("token")
691        .is_some_and(|t| http::same_token(t, &shared.token))
692    {
693        let _ = http::refuse(
694            stream,
695            "403 Forbidden",
696            "Open the address the server printed when it started, with its token.",
697        );
698        return;
699    }
700    // The page loads only its own scripts and styles, opens a WebSocket
701    // only to this server, and is never framed by another site.
702    let mut connect = "'self'".to_string();
703    if let Some(host) = head.header("host").filter(|h| {
704        h.bytes()
705            .all(|b| b.is_ascii_alphanumeric() || b".-:[]".contains(&b))
706    }) {
707        connect.push_str(&format!(" ws://{host} wss://{host}"));
708    }
709    let policy = format!(
710        "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; \
711         img-src 'self' data:; font-src 'self' data:; connect-src {connect}; \
712         frame-ancestors 'none'; base-uri 'none'; form-action 'none'"
713    );
714    let template = match renderer_for(head, shared) {
715        Renderer::Xterm => PAGE,
716        Renderer::Dom => DOM_PAGE,
717    };
718    let mode = match shared.mode {
719        Mode::App(_) => "app",
720        Mode::Command(_) | Mode::Host(_) => "program",
721    };
722    let body = template
723        .replace("{{title}}", &http::escape_html(&shared.title))
724        .replace("{{mode}}", mode);
725    let _ = http::respond(
726        stream,
727        "200 OK",
728        "text/html; charset=utf-8",
729        &[
730            ("Content-Security-Policy", policy),
731            ("X-Frame-Options", "DENY".to_string()),
732        ],
733        body.as_bytes(),
734    );
735}
736
737/// Why an upgrade is refused, as a status and a message; `None` when it
738/// may go ahead.
739fn check_upgrade(
740    head: &Head,
741    shared: &Shared,
742    local: Option<SocketAddr>,
743) -> Option<(&'static str, &'static str)> {
744    if http::websocket_key(head).is_none() {
745        return Some(("400 Bad Request", "Expected a WebSocket upgrade."));
746    }
747    if !head
748        .param("token")
749        .is_some_and(|t| http::same_token(t, &shared.token))
750    {
751        return Some(("403 Forbidden", "Wrong or missing token."));
752    }
753    let mut hosts = shared.hosts.clone();
754    hosts.extend(local.map(http::connection_hosts).unwrap_or_default());
755    if !http::origin_allowed(
756        head.header("origin"),
757        head.header("host"),
758        &hosts,
759        &shared.origins,
760    ) {
761        return Some(("403 Forbidden", "This origin may not open a session."));
762    }
763    None
764}
765
766/// Upgrade to a WebSocket and run a session on it, or refuse.
767fn websocket(mut stream: TcpStream, head: &Head, shared: &Arc<Shared>, pending: Pending) {
768    if let Some((status, why)) = check_upgrade(head, shared, stream.local_addr().ok()) {
769        let _ = http::refuse(&mut stream, status, why);
770        return;
771    }
772    let Some(_slot) = Slot::acquire(shared) else {
773        let _ = http::refuse(
774            &mut stream,
775            "503 Service Unavailable",
776            "Too many sessions: try again when one has ended.",
777        );
778        return;
779    };
780    let key = http::websocket_key(head).expect("checked");
781    let accept = tungstenite::handshake::derive_accept_key(key.as_bytes());
782    let answer = format!(
783        "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\
784         Sec-WebSocket-Accept: {accept}\r\n\r\n"
785    );
786    if stream.write_all(answer.as_bytes()).is_err() || stream.flush().is_err() {
787        return;
788    }
789    drop(pending);
790    let size = |name, default| {
791        head.param(name)
792            .and_then(|n| n.parse::<u16>().ok())
793            .filter(|n| (1..=session::MAX_CELLS).contains(n))
794            .unwrap_or(default)
795    };
796    let (columns, rows) = (size("cols", 80), size("rows", 24));
797    // Framed when the page can undo compression, compressed only when the
798    // server allows it too.
799    let z = session::Compress::negotiate(shared.compress, head.param("z") == Some("1"));
800    if let (Mode::App(_), Isolation::Processes) = (&shared.mode, shared.isolation) {
801        let session = isolate::Session {
802            spawn: &shared.spawn,
803            bind: shared.bind,
804            columns,
805            rows,
806            renderer: renderer_for(head, shared),
807            compress: z.0,
808            stopping: &shared.stopping,
809            running: &shared.programs,
810        };
811        isolate::run_parent(stream, session);
812        return;
813    }
814    let config = WebSocketConfig::default()
815        .max_message_size(Some(MAX_MESSAGE))
816        .max_frame_size(Some(MAX_MESSAGE));
817    let mut socket = WebSocket::from_raw_socket(stream, Role::Server, Some(config));
818    let (stopping, programs) = (&shared.stopping, &shared.programs);
819    let _ = match &shared.mode {
820        Mode::App(app) => {
821            let renderer = renderer_for(head, shared);
822            session::run(&mut socket, app(), columns, rows, stopping, renderer, z)
823        }
824        Mode::Command(command) => {
825            let (window, held) = pty::split(shared.max_buffered);
826            let host = LocalPty::new(command.clone()).backpressure(held);
827            let host = Box::new(host);
828            pty::run(
829                &mut socket,
830                host,
831                (columns, rows),
832                stopping,
833                programs,
834                window,
835                z,
836            )
837        }
838        Mode::Host(host) => {
839            let (window, _) = pty::split(shared.max_buffered);
840            let host = host();
841            pty::run(
842                &mut socket,
843                host,
844                (columns, rows),
845                stopping,
846                programs,
847                window,
848                z,
849            )
850        }
851    };
852}
853
854#[cfg(test)]
855mod tests {
856    use super::*;
857
858    #[test]
859    fn tokens_are_random_hex() {
860        let a = random_token().unwrap();
861        let b = random_token().unwrap();
862        assert_eq!(a.len(), 32);
863        assert!(a.bytes().all(|c| c.is_ascii_hexdigit()));
864        assert_ne!(a, b);
865    }
866
867    #[test]
868    fn urls_point_somewhere_reachable() {
869        let token = "t0k";
870        assert_eq!(
871            url_for("127.0.0.1:8080".parse().unwrap(), token),
872            "http://127.0.0.1:8080/?token=t0k"
873        );
874        assert_eq!(
875            url_for("0.0.0.0:80".parse().unwrap(), token),
876            "http://127.0.0.1:80/?token=t0k"
877        );
878        assert_eq!(
879            url_for("[::]:80".parse().unwrap(), token),
880            "http://[::1]:80/?token=t0k"
881        );
882    }
883
884    #[test]
885    #[should_panic(expected = "a token is")]
886    fn tokens_must_be_url_safe() {
887        let _ = Server::bind("127.0.0.1:0", || App::new(|| intuituive::label("x")))
888            .unwrap()
889            .token("has space");
890    }
891
892    #[test]
893    fn renderers_have_names() {
894        for renderer in [Renderer::Xterm, Renderer::Dom] {
895            assert_eq!(Renderer::parse(renderer.name()), Some(renderer));
896        }
897        assert_eq!(Renderer::parse("canvas"), None);
898        assert_eq!(Renderer::default(), Renderer::Xterm);
899    }
900
901    #[test]
902    fn connections_past_the_pending_cap_are_answered() {
903        use std::io::Read;
904
905        let handle = Server::bind("127.0.0.1:0", || App::new(|| intuituive::label("x")))
906            .unwrap()
907            .spawn()
908            .unwrap();
909        let addr = handle.local_addr();
910        // Connections that send nothing hold every place for a request.
911        let idle: Vec<TcpStream> = (0..MAX_PENDING)
912            .map(|_| TcpStream::connect(addr).unwrap())
913            .collect();
914        let start = Instant::now();
915        while handle.shared.pending.load(Ordering::SeqCst) < MAX_PENDING {
916            assert!(
917                start.elapsed() < Duration::from_secs(5),
918                "the idle connections"
919            );
920            thread::sleep(Duration::from_millis(10));
921        }
922        // One more is told so, not closed without a word.
923        let mut late = TcpStream::connect(addr).unwrap();
924        late.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
925        let mut answer = String::new();
926        let _ = late.read_to_string(&mut answer);
927        assert!(answer.starts_with("HTTP/1.1 503 "), "{answer:?}");
928        drop(idle);
929    }
930
931    #[test]
932    fn a_command_is_titled_by_its_program() {
933        let server = Server::bind_command("127.0.0.1:0", ["htop", "-d", "10"]).unwrap();
934        assert_eq!(server.shared.title, "htop");
935        assert_eq!(server.shared.max_buffered, DEFAULT_MAX_BUFFERED);
936    }
937}