Skip to main content

rich_web/
lib.rs

1//! Serve intuiTUIve apps, and any terminal program, to a web browser.
2//!
3//! [`serve`] listens on an address and runs one [`App`](intuituive::App) per browser tab,
4//! each in a process of its own (see [Isolation](#isolation)). The page is
5//! [xterm.js](https://xtermjs.org),
6//! vendored in this crate and served by it; the app's frames go to it as
7//! terminal output over a WebSocket, and its keys, mouse and resizes come
8//! back as the same [`Event`](intuituive::interact::Event)s a terminal
9//! gives. No async runtime, and nothing fetched from a CDN.
10//!
11//! The framework is re-exported as [`rich_web::intuituive`](intuituive), so
12//! an app served this way needs no other dependency.
13//!
14//! ```no_run
15//! use rich_web::intuituive::prelude::*;
16//!
17//! fn counter() -> App {
18//!     App::new(|| {
19//!         let count = signal(0);
20//!         text!("[b]Count:[/] {count} (+ adds one)")
21//!             .on_key("+", move |_| count.update(|c| *c += 1))
22//!     })
23//! }
24//!
25//! fn main() -> std::io::Result<()> {
26//!     // Prints http://127.0.0.1:8080/?token=… to open in a browser.
27//!     rich_web::serve("127.0.0.1:8080", counter)
28//! }
29//! ```
30//!
31//! [`Server`] is the same with options: a session cap, a fixed token, the
32//! page's title, origins a proxy serves the page from, and
33//! [`spawn`](Server::spawn) to run in the background.
34//!
35//! # Isolation
36//!
37//! Each app session runs in a process of its own ([`Isolation::Processes`],
38//! the default): the server starts this program again with
39//! [`SESSION_ENV`] set, and when that process reaches the same [`serve`] or
40//! [`Server::bind`] call, it runs the tab's app there, connected back to
41//! the server over loopback, and exits. A crash, a hang or a runaway
42//! allocation then ends one tab; the server kills a session's process
43//! with what it started (on Unix its process group, on Windows a job
44//! object) when the tab closes, the server stops or the server dies, and
45//! on Unix can cap its memory and CPU time
46//! ([`Server::session_memory`], [`Server::session_cpu`]).
47//! [`Server::session_command`] starts it some other way (an interpreter
48//! and its script), and [`Isolation::Threads`] runs every session on a
49//! thread of the server instead, as 0.0.18 did: for tests, and programs
50//! that cannot start themselves again.
51//!
52//! # Any terminal program
53//!
54//! [`serve_command`] runs a program on a pseudo-terminal per tab instead
55//! (a PTY on Unix, ConPTY on Windows, through rs-rich-embed's
56//! [`LocalPty`]) and streams it to the same page: keys, pastes and resizes
57//! go to the program, its output to the page, and its exit ends the session
58//! with its status shown. [`Server::bind_host`] takes any other
59//! [`PtyHost`] (an SSH session, a container, a test host).
60//!
61//! ```no_run
62//! fn main() -> std::io::Result<()> {
63//!     rich_web::serve_command("127.0.0.1:8080", ["htop"])
64//! }
65//! ```
66//!
67//! A page that falls behind holds the program back rather than the server
68//! buffering without end: at most [`Server::max_buffered`] bytes of output
69//! wait for the page, and past that the program's writes wait, as they do
70//! on a slow terminal. Input is bounded too: with 1 MiB that the program
71//! has not read held for it, the server stops reading the page until it
72//! reads some, so a flood of pastes waits in the network.
73//!
74//! # A DOM renderer
75//!
76//! [`Renderer::Dom`] (or `&renderer=dom` in the page's address) draws an
77//! app as a grid of styled spans instead of xterm.js, and carries its
78//! [accessibility tree](intuituive::Driver::accessibility) as ARIA: roles,
79//! names and states, the focused node focused in the page, and
80//! announcements in a live region, so a browser's screen reader reads the
81//! app. Its messages are described in [`dom`].
82//!
83//! # Security
84//!
85//! Anyone who can open a session can do whatever the app (or the program)
86//! lets them, so the server is closed by default:
87//!
88//! - **It listens where it is told.** Give it a loopback address
89//!   (`127.0.0.1`, `[::1]`, `localhost`) to keep it on this computer, as
90//!   the examples do. Any other address prints a warning.
91//! - **A random token** (128 bits from the operating system) is part of the
92//!   URL printed at start; the page and the WebSocket both refuse a request
93//!   without it.
94//! - **The `Origin` header is checked**: browsers let any web page open a
95//!   WebSocket to a localhost port, so the upgrade must come from this
96//!   server's own page (its address, which must also be the `Host` asked
97//!   for, against DNS rebinding) or from an origin allowed with
98//!   [`Server::allow_origin`]. The `Host` must be one the server answers
99//!   to even when it is bound to every interface: the loopback addresses,
100//!   `localhost`, this machine's name, the address the connection arrived
101//!   on, and any [`Server::allow_host`] names.
102//! - **Sessions are capped** ([`DEFAULT_MAX_SESSIONS`] unless set with
103//!   [`Server::max_sessions`]); one more is refused with `503`.
104//! - **Requests are timed**: a connection has 10 seconds to send its whole
105//!   request, however slowly it trickles in, and with 64 still sending,
106//!   one more is answered `503`.
107//!
108//! There is no other authentication and no TLS. To reach the server from
109//! another machine, put it behind a reverse proxy that has both, and allow
110//! the proxy's origin.
111//!
112//! # Features
113//!
114//! - `server` (default): everything above. Without it the crate is
115//!   [`input`] (the page's key and mouse decoder) and [`dom`] (the DOM
116//!   renderer's messages) only, with no networking or PTYs, so it builds
117//!   for `wasm32-unknown-unknown`: the docs playground decodes xterm.js's
118//!   input with it, in the browser.
119//! - `syntax` (default): intuiTUIve's syntax highlighting (syntect).
120
121// A builder method that returns `Self` is `#[must_use]`: dropping its
122// result drops the value (CONTRIBUTING.md, "API conventions").
123#![warn(clippy::return_self_not_must_use)]
124
125// The framework, under the name its own crate gives it (the workspace's
126// dependency key makes it `rich_intuituive`), and re-exported, so an app needs
127// only this crate.
128pub extern crate rich_intuituive as intuituive;
129
130pub mod dom;
131pub mod input;
132
133#[cfg(feature = "server")]
134mod http;
135#[cfg(feature = "server")]
136mod isolate;
137#[cfg(feature = "server")]
138mod pty;
139#[cfg(feature = "server")]
140mod server;
141#[cfg(feature = "server")]
142mod session;
143
144#[cfg(feature = "server")]
145pub use server::*;
146#[cfg(feature = "server")]
147pub(crate) use server::{MAX_MESSAGE, random_token};