Skip to main content

bobview_server/
pvws.rs

1//! A PV Web Socket server in the dialect of <https://github.com/ornl-epics/pvws>.
2//!
3//! Clients send `subscribe`, `clear`, `write`, `list` and `echo` messages and
4//! get one `update` per changed PV: full metadata first, then only what
5//! changed. Like PVWS, an update with neither `value` nor `text` means the PV
6//! is not connected.
7
8use std::collections::{BTreeMap, HashMap, HashSet};
9use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::sync::{Arc, Mutex, OnceLock};
12use std::time::{Duration, Instant, UNIX_EPOCH};
13
14use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
15use axum::extract::{ConnectInfo, State};
16use axum::http::header::{HOST, ORIGIN};
17use axum::http::{HeaderMap, StatusCode};
18use axum::response::{IntoResponse, Response};
19use base64::Engine;
20use pv::{Listener, Pv, PvManager, PvState, Scheme, Severity, Value};
21use serde_json::{Map, Value as Json, json};
22use tokio::sync::Notify;
23
24pub struct Config {
25    /// Accept `write` messages.
26    pub write: bool,
27    /// The token every `write` message must carry.
28    pub write_token: String,
29    /// Origins other than the server's own whose pages may connect,
30    /// e.g. `https://host.example.com`.
31    pub allow_origins: Vec<String>,
32    /// Names browsers may use for this server, besides IP addresses,
33    /// `localhost`, this machine's name and the hosts of `allow_origins`;
34    /// checked against `Host` to stop DNS rebinding.
35    pub hosts: Vec<String>,
36    /// Least time between two rounds of updates to one client; a PV that
37    /// changes faster sends only its latest state.
38    pub throttle: Duration,
39    /// Most clients connected at once from one address.
40    pub max_per_ip: usize,
41    /// How often to ping a client; one that has said nothing for two
42    /// intervals is dropped.
43    pub ping: Duration,
44}
45
46/// What every client connection shares.
47pub struct Hub {
48    manager: PvManager,
49    config: Config,
50    started: String,
51    /// Clients connected now, by address (see [`client_key`]).
52    clients: Mutex<HashMap<IpAddr, usize>>,
53    /// PVs subscribed to by all clients together.
54    subscriptions: Arc<AtomicUsize>,
55}
56
57impl Hub {
58    pub fn new(manager: PvManager, config: Config) -> Self {
59        let started = UNIX_EPOCH
60            .elapsed()
61            .unwrap_or_default()
62            .as_secs()
63            .to_string();
64        Self {
65            manager,
66            config,
67            started,
68            clients: Mutex::new(HashMap::new()),
69            subscriptions: Arc::new(AtomicUsize::new(0)),
70        }
71    }
72}
73
74/// Counts a connected client until dropped.
75struct ClientSlot(Arc<Hub>, IpAddr);
76
77impl ClientSlot {
78    /// A slot for a client at `ip`, or why there is none.
79    fn take(hub: &Arc<Hub>, ip: IpAddr) -> Result<Self, String> {
80        let ip = client_key(ip);
81        let mut clients = hub.clients.lock().unwrap();
82        if clients.values().sum::<usize>() >= MAX_CLIENTS {
83            return Err(format!("{MAX_CLIENTS} clients already connected"));
84        }
85        let n = clients.entry(ip).or_default();
86        if *n >= hub.config.max_per_ip {
87            return Err(format!(
88                "{} clients already connected from {ip}",
89                hub.config.max_per_ip
90            ));
91        }
92        *n += 1;
93        Ok(Self(hub.clone(), ip))
94    }
95}
96
97impl Drop for ClientSlot {
98    fn drop(&mut self) {
99        let mut clients = self.0.clients.lock().unwrap();
100        if let Some(n) = clients.get_mut(&self.1) {
101            *n -= 1;
102            if *n == 0 {
103                clients.remove(&self.1);
104            }
105        }
106    }
107}
108
109/// The address clients are counted by: an IPv4 address (also when mapped
110/// into IPv6), or the /64 network of an IPv6 one, as one machine may hold
111/// all of a /64.
112fn client_key(ip: IpAddr) -> IpAddr {
113    match ip.to_canonical() {
114        IpAddr::V6(v6) => {
115            let net = u128::from(v6) & !((1u128 << 64) - 1);
116            IpAddr::V6(Ipv6Addr::from(net))
117        }
118        v4 => v4,
119    }
120}
121
122/// The largest message a client may send.
123const MAX_MESSAGE: usize = 1 << 20;
124/// The most PVs one client may subscribe to.
125const MAX_PVS: usize = 10_000;
126/// The most clients connected at once.
127const MAX_CLIENTS: usize = 100;
128/// The most PVs all clients together may subscribe to.
129const MAX_TOTAL_PVS: usize = 50_000;
130/// The longest PV name accepted.
131const MAX_NAME: usize = 256;
132/// The longest one message to a client may take to send before the
133/// client is dropped.
134const SEND_TIMEOUT: Duration = Duration::from_secs(10);
135/// New PVs one client may subscribe to at once ...
136const SUBSCRIBE_BURST: f64 = MAX_PVS as f64;
137/// ... and each second after that: more are delayed, not refused.
138const SUBSCRIBE_RATE: f64 = 1000.0;
139
140pub async fn socket(
141    State(hub): State<Arc<Hub>>,
142    ConnectInfo(peer): ConnectInfo<SocketAddr>,
143    headers: HeaderMap,
144    ws: WebSocketUpgrade,
145) -> Response {
146    // Browsers let any page open a websocket to any server, so only pages
147    // from here (or allowed origins) may. A page can also reach here by a
148    // name of its own that resolves to this server (DNS rebinding), so the
149    // name it used must be one of ours too.
150    if !browser_allowed(&headers, &hub.config) {
151        let origin = headers.get(ORIGIN).and_then(|o| o.to_str().ok());
152        let host = headers.get(HOST).and_then(|h| h.to_str().ok());
153        log::warn!(
154            "refused a PVWS client {peer} from origin {origin:?} to host {host:?} \
155             (see --allow-origin and --host)"
156        );
157        return StatusCode::FORBIDDEN.into_response();
158    }
159    let slot = match ClientSlot::take(&hub, peer.ip()) {
160        Ok(slot) => slot,
161        Err(why) => {
162            log::warn!("refused PVWS client {peer}: {why}");
163            return StatusCode::SERVICE_UNAVAILABLE.into_response();
164        }
165    };
166    ws.max_message_size(MAX_MESSAGE)
167        .on_upgrade(move |socket| async move {
168            serve(&hub, socket, peer).await;
169            drop(slot);
170        })
171}
172
173/// Whether a request comes from a page of this server or an allowed
174/// origin, by a name of this server; a request without an `Origin` is not
175/// from a browser page.
176fn browser_allowed(headers: &HeaderMap, config: &Config) -> bool {
177    let Some(origin) = headers.get(ORIGIN) else {
178        return true;
179    };
180    let Ok(origin) = origin.to_str() else {
181        return false;
182    };
183    let Some(host) = headers.get(HOST).and_then(|h| h.to_str().ok()) else {
184        return false;
185    };
186    let host = host.to_ascii_lowercase();
187    // `name.` is the fully qualified `name`.
188    let name = host_name(&host);
189    if !host_allowed(name.strip_suffix('.').unwrap_or(name), config) {
190        return false;
191    }
192    let origin = normalise_origin(origin);
193    if config
194        .allow_origins
195        .iter()
196        .any(|a| normalise_origin(a) == origin)
197    {
198        return true;
199    }
200    // The page's own server, with any default port left out of both.
201    let (scheme, origin_host) = origin.split_once("://").unwrap_or_default();
202    origin_host == strip_default_port(scheme, &host)
203}
204
205/// Whether browsers may call this server `host` (lowercase, no port).
206fn host_allowed(host: &str, config: &Config) -> bool {
207    if host.parse::<Ipv4Addr>().is_ok()
208        || host
209            .strip_prefix('[')
210            .and_then(|h| h.strip_suffix(']'))
211            .is_some_and(|h| h.parse::<Ipv6Addr>().is_ok())
212        || host == "localhost"
213    {
214        return true;
215    }
216    machine_names().iter().any(|name| name == host)
217        || config.hosts.iter().any(|h| h.eq_ignore_ascii_case(host))
218        || config.allow_origins.iter().any(|a| {
219            let origin = normalise_origin(a);
220            let host_port = origin.split_once("://").map_or(&*origin, |(_, h)| h);
221            host_name(host_port) == host
222        })
223}
224
225/// The host of a `host[:port]`, keeping the brackets of an IPv6 address.
226fn host_name(host_port: &str) -> &str {
227    match host_port.find(']') {
228        Some(end) if host_port.starts_with('[') => &host_port[..=end],
229        _ => host_port.split(':').next().unwrap_or_default(),
230    }
231}
232
233/// An origin in one spelling: lowercase, no trailing `/`, no default port.
234fn normalise_origin(origin: &str) -> String {
235    let origin = origin.trim_end_matches('/').to_ascii_lowercase();
236    match origin.split_once("://") {
237        Some((scheme, host)) => format!("{scheme}://{}", strip_default_port(scheme, host)),
238        None => origin,
239    }
240}
241
242fn strip_default_port<'a>(scheme: &str, host: &'a str) -> &'a str {
243    let port = match scheme {
244        "http" | "ws" => ":80",
245        "https" | "wss" => ":443",
246        _ => return host,
247    };
248    host.strip_suffix(port).unwrap_or(host)
249}
250
251/// This machine's names: as the kernel and /etc/hostname have them, each
252/// without its domain, and in each domain of /etc/resolv.conf.
253fn machine_names() -> &'static [String] {
254    static NAMES: OnceLock<Vec<String>> = OnceLock::new();
255    NAMES.get_or_init(|| {
256        let read = |f: &str| std::fs::read_to_string(f).unwrap_or_default();
257        let hostnames = [read("/proc/sys/kernel/hostname"), read("/etc/hostname")];
258        names_of(&hostnames, &resolv_domains(&read("/etc/resolv.conf")))
259    })
260}
261
262/// Every name of a machine called `hostnames` in `domains`: exact names
263/// only, so `ws01.evil.com` is not `ws01.site.org`.
264fn names_of(hostnames: &[String], domains: &[String]) -> Vec<String> {
265    let mut names = Vec::new();
266    for name in hostnames {
267        let name = name.trim().trim_end_matches('.').to_ascii_lowercase();
268        if name.is_empty() {
269            continue;
270        }
271        let short = name.split('.').next().unwrap_or_default().to_string();
272        names.extend(domains.iter().map(|d| format!("{short}.{d}")));
273        names.push(short);
274        names.push(name);
275    }
276    names.sort();
277    names.dedup();
278    names
279}
280
281/// The domains of a resolv.conf: its `domain` and `search` lines.
282fn resolv_domains(conf: &str) -> Vec<String> {
283    conf.lines()
284        .filter_map(|line| {
285            let mut words = line.split_whitespace();
286            matches!(words.next(), Some("domain" | "search")).then_some(words)
287        })
288        .flatten()
289        .map(|d| d.trim_end_matches('.').to_ascii_lowercase())
290        .filter(|d| !d.is_empty())
291        .collect()
292}
293
294pub async fn info(State(hub): State<Arc<Hub>>) -> axum::Json<Json> {
295    axum::Json(json!({
296        "server": format!("bobview-server {}", env!("CARGO_PKG_VERSION")),
297        "start_time": hub.started,
298        "write": hub.config.write,
299    }))
300}
301
302struct Sub {
303    pv: Pv,
304    /// The state the client last heard about.
305    sent: Option<PvState>,
306    /// Marks the PV changed for this client, until dropped.
307    _listener: Listener,
308    /// Gives this subscription back to the server-wide count when dropped.
309    _slot: SubSlot,
310}
311
312/// The PVs that changed since a client last heard, and a wake-up for it.
313#[derive(Default)]
314struct Changed {
315    names: Mutex<HashSet<String>>,
316    wake: Notify,
317}
318
319impl Changed {
320    fn mark(&self, name: &str) {
321        let mut names = self.names.lock().unwrap();
322        if !names.contains(name) {
323            names.insert(name.to_string());
324        }
325        drop(names);
326        self.wake.notify_one();
327    }
328
329    fn take(&self) -> HashSet<String> {
330        std::mem::take(&mut *self.names.lock().unwrap())
331    }
332}
333
334/// One client's subscriptions.
335#[derive(Default)]
336struct Subs {
337    map: BTreeMap<String, Sub>,
338    changed: Arc<Changed>,
339}
340
341impl Subs {
342    /// Subscribe to `pv` as `name`, which the client hears about now and
343    /// whenever it changes.
344    fn add(&mut self, name: &str, pv: Pv, slot: SubSlot) {
345        let changed = Arc::downgrade(&self.changed);
346        let key = name.to_string();
347        let listener: Listener = Arc::new(move |_: &PvState| {
348            if let Some(changed) = changed.upgrade() {
349                changed.mark(&key);
350            }
351        });
352        // Marks it changed now, so the client hears of it.
353        pv.listen(&listener);
354        let sub = Sub {
355            pv,
356            sent: None,
357            _listener: listener,
358            _slot: slot,
359        };
360        self.map.insert(name.to_string(), sub);
361    }
362}
363
364/// Paces a client's new subscriptions: each spends a token, and tokens
365/// come back at [`SUBSCRIBE_RATE`] a second up to [`SUBSCRIBE_BURST`].
366struct Pace {
367    tokens: f64,
368    at: Instant,
369}
370
371impl Pace {
372    fn new() -> Self {
373        Self {
374            tokens: SUBSCRIBE_BURST,
375            at: Instant::now(),
376        }
377    }
378
379    /// Spend `n` tokens at `now`, returning how long to wait first.
380    fn spend(&mut self, n: usize, now: Instant) -> Duration {
381        let earned = now.saturating_duration_since(self.at).as_secs_f64() * SUBSCRIBE_RATE;
382        self.tokens = (self.tokens + earned).min(SUBSCRIBE_BURST) - n as f64;
383        self.at = now;
384        if self.tokens >= 0.0 {
385            Duration::ZERO
386        } else {
387            Duration::from_secs_f64(-self.tokens / SUBSCRIBE_RATE)
388        }
389    }
390}
391
392struct SubSlot(Arc<AtomicUsize>);
393
394impl SubSlot {
395    fn take(total: &Arc<AtomicUsize>) -> Option<Self> {
396        let n = total.fetch_add(1, Ordering::SeqCst);
397        let slot = Self(total.clone());
398        (n < MAX_TOTAL_PVS).then_some(slot)
399    }
400}
401
402impl Drop for SubSlot {
403    fn drop(&mut self) {
404        self.0.fetch_sub(1, Ordering::SeqCst);
405    }
406}
407
408/// Whether `name` is a plausible PV name: not too long, and only printable
409/// ASCII with no spaces.
410fn valid_name(name: &str) -> bool {
411    !name.is_empty() && name.len() <= MAX_NAME && name.bytes().all(|b| b.is_ascii_graphic())
412}
413
414/// Whether a client may have `name` from this server: not `loc://` PVs,
415/// which would live here for ever and be shared by every client, nor
416/// `sim://` ones, which cost the server for nothing, nor formulas, which
417/// could read the server's `loc://` PVs. A page makes all three itself.
418fn served_scheme(name: &str) -> bool {
419    !matches!(
420        pv::PvName::parse(name).scheme,
421        Scheme::Loc | Scheme::Sim | Scheme::Formula
422    )
423}
424
425/// Whether `given` is the write token, without stopping at the first
426/// difference.
427fn token_ok(given: &str, token: &str) -> bool {
428    let (a, b) = (given.as_bytes(), token.as_bytes());
429    a.len() == b.len() && a.iter().zip(b).fold(0, |d, (x, y)| d | (x ^ y)) == 0
430}
431
432async fn serve(hub: &Hub, mut ws: WebSocket, peer: SocketAddr) {
433    let mut subs = Subs::default();
434    let changed = subs.changed.clone();
435    let mut pace = Pace::new();
436    let mut last_round = Instant::now();
437    // Without pings a client that has vanished (or stopped reading) would
438    // keep its slot for ever.
439    let mut ping = tokio::time::interval(hub.config.ping);
440    let mut heard = Instant::now();
441    loop {
442        tokio::select! {
443            msg = ws.recv() => {
444                heard = Instant::now();
445                match msg {
446                    Some(Ok(Message::Text(text))) => {
447                        // Subscribing costs the control system a search per
448                        // PV, so a client churning through them is slowed.
449                        let wait = pace.spend(new_names(&subs, &text), Instant::now());
450                        if !wait.is_zero() {
451                            log::debug!("pacing PVWS client {peer}'s subscriptions: {wait:?}");
452                            tokio::time::sleep(wait).await;
453                        }
454                        let replies = handle(hub, &mut subs, &text, peer);
455                        // New subscriptions may already hold a value.
456                        if !send_all(&mut ws, replies).await
457                            || !send_updates(hub, &mut ws, &mut subs).await
458                        {
459                            break;
460                        }
461                    }
462                    Some(Ok(Message::Close(_))) | Some(Err(_)) | None => break,
463                    Some(Ok(_)) => {}
464                }
465            }
466            _ = ping.tick() => {
467                if heard.elapsed() > hub.config.ping * 2 {
468                    log::info!("dropping PVWS client {peer}: no answer to pings");
469                    break;
470                }
471                if !send(&mut ws, Message::Ping(Default::default())).await {
472                    break;
473                }
474            }
475            () = changed.wake.notified() => {
476                tokio::time::sleep_until((last_round + hub.config.throttle).into()).await;
477                last_round = Instant::now();
478                if !send_updates(hub, &mut ws, &mut subs).await {
479                    break;
480                }
481            }
482        }
483    }
484    log::debug!("PVWS client {peer} left with {} PVs", subs.map.len());
485}
486
487/// How many PVs a message subscribes to that the client does not have.
488fn new_names(subs: &Subs, text: &str) -> usize {
489    let Ok(Json::Object(msg)) = serde_json::from_str::<Json>(text) else {
490        return 0;
491    };
492    if !matches!(
493        msg.get("type").and_then(Json::as_str),
494        Some("subscribe" | "monitor")
495    ) {
496        return 0;
497    }
498    let new: HashSet<&str> = msg
499        .get("pvs")
500        .and_then(Json::as_array)
501        .into_iter()
502        .flatten()
503        .filter_map(Json::as_str)
504        .filter(|name| valid_name(name) && served_scheme(name) && !subs.map.contains_key(*name))
505        .collect();
506    // Only those `handle` will take.
507    new.len().min(MAX_PVS.saturating_sub(subs.map.len()))
508}
509
510/// Send one message, giving up on a client that takes too long to take it.
511async fn send(ws: &mut WebSocket, msg: Message) -> bool {
512    matches!(
513        tokio::time::timeout(SEND_TIMEOUT, ws.send(msg)).await,
514        Ok(Ok(()))
515    )
516}
517
518/// Act on one client message, returning any replies.
519fn handle(hub: &Hub, subs: &mut Subs, text: &str, peer: SocketAddr) -> Vec<String> {
520    let Ok(Json::Object(msg)) = serde_json::from_str::<Json>(text) else {
521        return vec![error(format!("cannot parse {text:.100}"))];
522    };
523    let names = || {
524        msg.get("pvs")
525            .and_then(Json::as_array)
526            .into_iter()
527            .flatten()
528            .filter_map(Json::as_str)
529    };
530    match msg.get("type").and_then(Json::as_str) {
531        Some("subscribe" | "monitor") => {
532            let mut refused = 0;
533            let mut refused_total = 0;
534            let mut invalid = 0;
535            let mut local = 0;
536            for name in names() {
537                // A repeat subscription gets the full state again.
538                if let Some(sub) = subs.map.get_mut(name) {
539                    sub.sent = None;
540                    subs.changed.mark(name);
541                } else if !valid_name(name) {
542                    invalid += 1;
543                } else if !served_scheme(name) {
544                    local += 1;
545                } else if subs.map.len() >= MAX_PVS {
546                    refused += 1;
547                } else if let Some(slot) = SubSlot::take(&hub.subscriptions) {
548                    subs.add(name, hub.manager.subscribe(name), slot);
549                } else {
550                    refused_total += 1;
551                }
552            }
553            if invalid > 0 {
554                log::warn!("PVWS client {peer} asked for {invalid} invalid PV names");
555                return vec![error(format!(
556                    "Cannot subscribe to {invalid} PVs: names are at most {MAX_NAME} \
557                     printable characters with no spaces"
558                ))];
559            }
560            if local > 0 {
561                log::warn!("PVWS client {peer} asked for {local} loc://, sim:// or formula PVs");
562                return vec![error(format!(
563                    "Cannot subscribe to {local} PVs: loc:// and sim:// PVs and formulas \
564                     are made by the page, not this server"
565                ))];
566            }
567            if refused > 0 {
568                return vec![error(format!(
569                    "Cannot subscribe to {refused} more PVs: at most {MAX_PVS} per client"
570                ))];
571            }
572            if refused_total > 0 {
573                return vec![error(format!(
574                    "Cannot subscribe to {refused_total} more PVs: the server is at its \
575                     limit of {MAX_TOTAL_PVS} PVs for all clients"
576                ))];
577            }
578            vec![]
579        }
580        Some("clear") => {
581            for name in names() {
582                subs.map.remove(name);
583            }
584            vec![]
585        }
586        Some("list") => {
587            vec![json!({"type": "list", "pvs": subs.map.keys().collect::<Vec<_>>()}).to_string()]
588        }
589        Some("write") => {
590            let name = msg.get("pv").and_then(Json::as_str).unwrap_or_default();
591            let Some(sub) = subs.map.get(name) else {
592                return vec![error(format!("Cannot write unknown PV {name}"))];
593            };
594            if !hub.config.write {
595                return vec![write_error(
596                    name,
597                    "writes are disabled on this server (--write)",
598                )];
599            }
600            let given = msg.get("token").and_then(Json::as_str).unwrap_or_default();
601            if !token_ok(given, &hub.config.write_token) {
602                log::warn!("refused a write to {name} from {peer}: bad token");
603                return vec![write_error(name, "bad or missing token")];
604            }
605            let Some(value) = msg.get("value").and_then(write_text) else {
606                return vec![write_error(name, "no value")];
607            };
608            // Said to the client, not the log, which would fill with them.
609            // A PV that has yet to connect takes the write into its queue.
610            if sub.pv.dropped() {
611                return vec![write_error(name, "not connected")];
612            }
613            log::info!("write {name} = {value} from {peer}");
614            // bobview's own mark for a string-format write (see
615            // `pv::Pv::write_long_string`).
616            if msg.get("long_string").and_then(Json::as_bool) == Some(true) {
617                sub.pv.write_long_string(value);
618            } else {
619                sub.pv.write(value);
620            }
621            vec![]
622        }
623        Some("echo") => vec![text.to_string()],
624        Some("ping") => vec![],
625        other => vec![error(format!("unknown message type {other:?}"))],
626    }
627}
628
629fn error(message: String) -> String {
630    json!({"type": "error", "message": message}).to_string()
631}
632
633/// An error naming the PV a write was refused for, so the page can show
634/// it by the PV as well as log it.
635fn write_error(name: &str, why: &str) -> String {
636    json!({
637        "type": "error",
638        "pv": name,
639        "message": format!("Cannot write {name}: {why}"),
640        "write_error": why,
641    })
642    .to_string()
643}
644
645/// A written value as the text `Pv::write` takes; whole numbers lose their
646/// `.0` so integer PVs accept them.
647fn write_text(value: &Json) -> Option<String> {
648    match value {
649        Json::String(s) => Some(s.clone()),
650        Json::Number(n) => Some(match n.as_f64() {
651            Some(x) if x.fract() == 0.0 && x.abs() < 1e15 => (x as i64).to_string(),
652            _ => n.to_string(),
653        }),
654        Json::Bool(b) => Some(u8::from(*b).to_string()),
655        _ => None,
656    }
657}
658
659async fn send_all(ws: &mut WebSocket, messages: Vec<String>) -> bool {
660    for msg in messages {
661        if !send(ws, Message::Text(msg.into())).await {
662            return false;
663        }
664    }
665    true
666}
667
668/// Tell the client about every PV that changed since it last heard.
669async fn send_updates(hub: &Hub, ws: &mut WebSocket, subs: &mut Subs) -> bool {
670    send_all(ws, updates(hub, subs)).await
671}
672
673/// The updates for the PVs that changed since the client last heard.
674fn updates(hub: &Hub, subs: &mut Subs) -> Vec<String> {
675    let mut messages = Vec::new();
676    for name in subs.changed.take() {
677        let Some(sub) = subs.map.get_mut(&name) else {
678            continue;
679        };
680        let state = sub.pv.state();
681        if let Some(msg) = update(&name, &state, sub.sent.as_ref(), hub.config.write) {
682            messages.push(msg);
683        }
684        sub.sent = Some(state);
685    }
686    messages
687}
688
689/// The `update` message taking a client from `last` to `s`, if anything
690/// it would see changed.
691fn update(name: &str, s: &PvState, last: Option<&PvState>, write: bool) -> Option<String> {
692    let mut m = Map::new();
693    let readonly = !(write && s.writable);
694    // Connected without a value yet is still disconnected to a client, so
695    // the metadata goes with the first value.
696    let live = |s: &PvState| s.connected && s.value != Value::None;
697    let was_connected = last.is_some_and(live);
698    if last.is_none_or(|l| readonly != !(write && l.writable)) {
699        m.insert("readonly".into(), json!(readonly));
700    }
701    if !live(s) {
702        if was_connected {
703            // No value or text: disconnected.
704            m.insert("severity".into(), json!("UNDEFINED"));
705        }
706    } else {
707        let first = !was_connected;
708        let changed = |f: &dyn Fn(&PvState) -> bool| first || last.is_some_and(f);
709        if first {
710            m.insert("vtype".into(), json!(vtype(&s.value)));
711        }
712        if changed(&|l| l.units != s.units) && !matches!(s.value, Value::Enum(_)) {
713            m.insert("units".into(), json!(s.units));
714        }
715        if let Some(p) = s
716            .precision
717            .filter(|_| changed(&|l| l.precision != s.precision))
718        {
719            m.insert("precision".into(), json!(p));
720        }
721        if let Some((lo, hi)) = s
722            .display_range
723            .filter(|_| changed(&|l| l.display_range != s.display_range))
724        {
725            m.insert("min".into(), number(lo));
726            m.insert("max".into(), number(hi));
727        }
728        // Alarm and warning limits as PVWS sends them, and control limits,
729        // which it does not: meters and sliders take their ranges from them.
730        let limits = [
731            (s.warning_range, "warn_low", "warn_high"),
732            (s.alarm_range, "alarm_low", "alarm_high"),
733            (s.control_range, "ctrl_low", "ctrl_high"),
734        ];
735        let last_limits = last.map(|l| [l.warning_range, l.alarm_range, l.control_range]);
736        for (i, (range, lo_key, hi_key)) in limits.into_iter().enumerate() {
737            let Some((lo, hi)) = range else { continue };
738            if first || last_limits.is_some_and(|l| !same_range(l[i], range)) {
739                m.insert(lo_key.into(), number(lo));
740                m.insert(hi_key.into(), number(hi));
741            }
742        }
743        if matches!(s.value, Value::Enum(_)) && changed(&|l| l.labels != s.labels) {
744            m.insert("labels".into(), json!(s.labels));
745        }
746        if changed(&|l| l.severity != s.severity) {
747            m.insert("severity".into(), json!(severity(s.severity)));
748        }
749        if changed(&|l| !same_value(&l.value, &s.value)) {
750            encode_value(&mut m, s);
751        }
752        if let Some(t) = s
753            .timestamp
754            .filter(|_| changed(&|l| l.timestamp != s.timestamp))
755        {
756            let t = t.duration_since(UNIX_EPOCH).unwrap_or_default();
757            m.insert("seconds".into(), json!(t.as_secs()));
758            m.insert("nanos".into(), json!(t.subsec_nanos()));
759        }
760    }
761    if m.is_empty() {
762        return None;
763    }
764    let mut msg = Map::new();
765    msg.insert("type".into(), json!("update"));
766    msg.insert("pv".into(), json!(name));
767    msg.extend(m);
768    Some(Json::Object(msg).to_string())
769}
770
771/// Whether two limit pairs are equal, with NaN equal to NaN (unset alarm
772/// limits are NaN, which would otherwise be sent on every update).
773fn same_range(a: Option<(f64, f64)>, b: Option<(f64, f64)>) -> bool {
774    let same = |x: f64, y: f64| x == y || (x.is_nan() && y.is_nan());
775    match (a, b) {
776        (Some(a), Some(b)) => same(a.0, b.0) && same(a.1, b.1),
777        (None, None) => true,
778        _ => false,
779    }
780}
781
782/// Whether two values are equal, without comparing arrays that are the
783/// same allocation element by element, and with NaN equal to NaN.
784fn same_value(a: &Value, b: &Value) -> bool {
785    match (a, b) {
786        (Value::Double(x), Value::Double(y)) => x == y || (x.is_nan() && y.is_nan()),
787        (Value::DoubleArray(x), Value::DoubleArray(y)) => Arc::ptr_eq(x, y) || x == y,
788        (Value::LongArray(x), Value::LongArray(y)) => Arc::ptr_eq(x, y) || x == y,
789        (Value::StrArray(x), Value::StrArray(y)) => Arc::ptr_eq(x, y) || x == y,
790        (Value::Image(x), Value::Image(y)) => Arc::ptr_eq(x, y) || x == y,
791        _ => a == b,
792    }
793}
794
795fn vtype(value: &Value) -> &'static str {
796    match value {
797        Value::Double(_) | Value::None => "VDouble",
798        Value::Long(_) => "VLong",
799        Value::Str(_) => "VString",
800        Value::Enum(_) => "VEnum",
801        Value::DoubleArray(_) => "VDoubleArray",
802        Value::LongArray(a) if fits_i32(a) => "VIntArray",
803        Value::LongArray(_) => "VDoubleArray",
804        Value::StrArray(_) => "VStringArray",
805        Value::Image(_) => "VImage",
806    }
807}
808
809fn fits_i32(a: &[i64]) -> bool {
810    a.iter().all(|&v| i32::try_from(v).is_ok())
811}
812
813fn severity(s: Severity) -> &'static str {
814    match s {
815        Severity::NoAlarm => "NONE",
816        Severity::Minor => "MINOR",
817        Severity::Major => "MAJOR",
818        Severity::Invalid => "INVALID",
819    }
820}
821
822/// A double as PVWS sends it: non-finite values as text.
823fn number(x: f64) -> Json {
824    if x.is_finite() {
825        json!(x)
826    } else {
827        json!("NaN")
828    }
829}
830
831fn encode_value(m: &mut Map<String, Json>, s: &PvState) {
832    let b64 = |bytes: Vec<u8>| json!(base64::engine::general_purpose::STANDARD.encode(bytes));
833    match &s.value {
834        Value::None => {}
835        Value::Double(v) => {
836            m.insert("value".into(), number(*v));
837        }
838        Value::Long(v) => {
839            m.insert("value".into(), json!(v));
840        }
841        Value::Str(t) => {
842            m.insert("text".into(), json!(t));
843        }
844        Value::Enum(i) => {
845            m.insert("value".into(), json!(i));
846            let label = s
847                .labels
848                .get(*i as usize)
849                .cloned()
850                .unwrap_or_else(|| i.to_string());
851            m.insert("text".into(), json!(label));
852        }
853        Value::DoubleArray(a) => {
854            m.insert(
855                "b64dbl".into(),
856                b64(a.iter().flat_map(|v| v.to_le_bytes()).collect()),
857            );
858        }
859        Value::LongArray(a) if fits_i32(a) => {
860            let bytes = a.iter().flat_map(|&v| (v as i32).to_le_bytes()).collect();
861            m.insert("b64int".into(), b64(bytes));
862        }
863        Value::LongArray(a) => {
864            let bytes = a.iter().flat_map(|&v| (v as f64).to_le_bytes()).collect();
865            m.insert("b64dbl".into(), b64(bytes));
866        }
867        Value::StrArray(a) => {
868            // The text, as PVWS sends it, for any PVWS client; the items
869            // for bobview under a key of its own, so that a client reading
870            // `value` as a number (cs-web-lib) never sees an array.
871            m.insert("text".into(), json!(a.join(", ")));
872            m.insert("strings".into(), json!(a.as_ref()));
873        }
874        Value::Image(img) => {
875            // Images need a video stream, not JSON (bobview#20).
876            m.insert(
877                "text".into(),
878                json!(format!("image {}x{}", img.width, img.height)),
879            );
880        }
881    }
882}
883
884#[cfg(test)]
885mod tests {
886    use super::*;
887
888    fn parse(msg: Option<String>) -> Map<String, Json> {
889        match serde_json::from_str(&msg.expect("an update")).unwrap() {
890            Json::Object(m) => m,
891            _ => panic!("not an object"),
892        }
893    }
894
895    fn double(v: f64) -> PvState {
896        PvState {
897            connected: true,
898            value: Value::Double(v),
899            units: "mA".into(),
900            precision: Some(3),
901            writable: true,
902            ..Default::default()
903        }
904    }
905
906    #[test]
907    fn first_update_has_metadata_then_only_changes() {
908        let a = double(1.5);
909        let m = parse(update("X", &a, None, true));
910        assert_eq!(m["vtype"], "VDouble");
911        assert_eq!(m["units"], "mA");
912        assert_eq!(m["severity"], "NONE");
913        assert_eq!(m["value"], 1.5);
914        assert_eq!(m["readonly"], false);
915
916        let b = double(2.5);
917        let m = parse(update("X", &b, Some(&a), true));
918        let mut keys: Vec<_> = m.keys().collect();
919        keys.sort();
920        assert_eq!(keys, ["pv", "type", "value"]);
921        assert!(update("X", &b, Some(&b), true).is_none());
922    }
923
924    #[test]
925    fn unreachable_pv_has_no_value() {
926        let m = parse(update("X", &PvState::default(), None, true));
927        assert_eq!(m["readonly"], true);
928        assert!(!m.contains_key("value") && !m.contains_key("text"));
929        // ... and a lost connection says so once.
930        let a = double(1.0);
931        let gone = PvState {
932            writable: true,
933            ..Default::default()
934        };
935        let m = parse(update("X", &gone, Some(&a), true));
936        assert_eq!(m["severity"], "UNDEFINED");
937        assert!(update("X", &gone, Some(&gone), true).is_none());
938    }
939
940    #[test]
941    fn metadata_comes_with_the_first_value() {
942        // Connected, but the first value has yet to arrive.
943        let empty = PvState {
944            connected: true,
945            writable: true,
946            ..Default::default()
947        };
948        let m = parse(update("X", &empty, None, true));
949        assert!(!m.contains_key("vtype") && !m.contains_key("severity"));
950        assert!(update("X", &empty, Some(&empty), true).is_none());
951
952        let m = parse(update("X", &double(1.0), Some(&empty), true));
953        assert_eq!(m["vtype"], "VDouble");
954        assert_eq!(m["units"], "mA");
955        assert_eq!(m["value"], 1.0);
956    }
957
958    #[test]
959    fn no_change_no_update() {
960        let states = [
961            PvState::default(),
962            PvState {
963                connected: true,
964                ..Default::default()
965            },
966            double(1.0),
967            double(f64::NAN),
968            PvState {
969                connected: true,
970                value: Value::DoubleArray(vec![f64::NAN, 1.0].into()),
971                ..Default::default()
972            },
973        ];
974        for s in &states {
975            for write in [false, true] {
976                assert!(update("X", s, Some(s), write).is_none(), "{s:?}");
977            }
978        }
979    }
980
981    #[test]
982    fn limits_and_string_arrays() {
983        let mut a = PvState {
984            alarm_range: Some((f64::NAN, f64::NAN)),
985            warning_range: Some((1.0, 9.0)),
986            control_range: Some((0.0, 10.0)),
987            ..double(5.0)
988        };
989        let m = parse(update("X", &a, None, false));
990        assert_eq!(m["warn_low"], 1.0);
991        assert_eq!(m["warn_high"], 9.0);
992        assert_eq!(m["alarm_low"], "NaN");
993        assert_eq!(m["ctrl_high"], 10.0);
994        // Unchanged NaN limits are not sent again; a changed limit is.
995        assert!(update("X", &a.clone(), Some(&a), false).is_none());
996        let before = a.clone();
997        a.control_range = Some((0.0, 20.0));
998        let m = parse(update("X", &a, Some(&before), false));
999        assert_eq!(m["ctrl_high"], 20.0);
1000        assert!(!m.contains_key("warn_low"));
1001
1002        let s = PvState {
1003            connected: true,
1004            value: Value::StrArray(vec!["a, b".into(), "c".into()].into()),
1005            ..Default::default()
1006        };
1007        let m = parse(update("X", &s, None, false));
1008        assert_eq!(m["vtype"], "VStringArray");
1009        assert_eq!(m["strings"], json!(["a, b", "c"]));
1010        assert_eq!(m["text"], "a, b, c");
1011        assert!(!m.contains_key("value"), "PVWS sends no value for these");
1012    }
1013
1014    #[test]
1015    fn writes_off_means_readonly() {
1016        let m = parse(update("X", &double(1.0), None, false));
1017        assert_eq!(m["readonly"], true);
1018    }
1019
1020    #[test]
1021    fn enums_and_arrays() {
1022        let e = PvState {
1023            connected: true,
1024            value: Value::Enum(1),
1025            labels: vec!["Off".into(), "On".into()],
1026            ..Default::default()
1027        };
1028        let m = parse(update("X", &e, None, false));
1029        assert_eq!(m["vtype"], "VEnum");
1030        assert_eq!(m["value"], 1);
1031        assert_eq!(m["text"], "On");
1032        assert_eq!(m["labels"], json!(["Off", "On"]));
1033
1034        let a = PvState {
1035            connected: true,
1036            value: Value::LongArray(vec![1, -2].into()),
1037            ..Default::default()
1038        };
1039        let m = parse(update("X", &a, None, false));
1040        let bytes = base64::engine::general_purpose::STANDARD
1041            .decode(m["b64int"].as_str().unwrap())
1042            .unwrap();
1043        assert_eq!(bytes, [1, 0, 0, 0, 0xfe, 0xff, 0xff, 0xff]);
1044    }
1045
1046    fn config(allow_origins: &[&str], hosts: &[&str]) -> Config {
1047        Config {
1048            write: true,
1049            write_token: "secret".into(),
1050            allow_origins: allow_origins.iter().map(|s| s.to_string()).collect(),
1051            hosts: hosts.iter().map(|s| s.to_string()).collect(),
1052            throttle: Duration::ZERO,
1053            max_per_ip: 10,
1054            ping: Duration::from_secs(30),
1055        }
1056    }
1057
1058    fn headers(host: &str, origin: Option<&str>) -> HeaderMap {
1059        let mut h = HeaderMap::new();
1060        h.insert(HOST, host.parse().unwrap());
1061        if let Some(o) = origin {
1062            h.insert(ORIGIN, o.parse().unwrap());
1063        }
1064        h
1065    }
1066
1067    #[test]
1068    fn origins() {
1069        let none = config(&[], &["server"]);
1070        let allowed = config(&["https://Other.example.com:443/"], &["server"]);
1071        let ok = |host, origin, config: &Config| browser_allowed(&headers(host, origin), config);
1072        assert!(ok("server:8080", None, &none));
1073        assert!(ok("server:8080", Some("http://server:8080"), &none));
1074        assert!(ok("Server:8080", Some("http://server:8080"), &none));
1075        assert!(!ok("server:8080", Some("http://evil:8080"), &none));
1076        assert!(!ok("server:8080", Some("http://server"), &none));
1077        assert!(ok("server", Some("http://server"), &none));
1078        assert!(ok("server:80", Some("http://server"), &none));
1079        assert!(!ok("server:8080", Some("https://other.example.com"), &none));
1080        assert!(ok(
1081            "server:8080",
1082            Some("https://other.example.com"),
1083            &allowed
1084        ));
1085        // DNS rebinding: the attacker's name for this server.
1086        assert!(!ok(
1087            "evil.example:8080",
1088            Some("http://evil.example:8080"),
1089            &none
1090        ));
1091        // But no Origin is not a browser.
1092        assert!(ok("evil.example:8080", None, &none));
1093        assert!(ok("127.0.0.1:8080", Some("http://127.0.0.1:8080"), &none));
1094        assert!(ok("[::1]:8080", Some("http://[::1]:8080"), &none));
1095        assert!(ok("localhost:8080", Some("http://localhost:8080"), &none));
1096        // A trailing dot is the same name, fully qualified.
1097        assert!(ok("localhost.:8080", Some("http://localhost.:8080"), &none));
1098        assert!(ok("server.", Some("http://server."), &none));
1099        assert!(!ok(
1100            "evil.example.:8080",
1101            Some("http://evil.example.:8080"),
1102            &none
1103        ));
1104        assert!(!ok("server..", Some("http://server.."), &none));
1105        assert!(!ok("[evil]:8080", Some("http://[evil]:8080"), &none));
1106        // The host of an allowed origin is one of ours.
1107        assert!(ok(
1108            "other.example.com",
1109            Some("http://other.example.com"),
1110            &allowed
1111        ));
1112    }
1113
1114    #[test]
1115    fn machine_names_are_hosts() {
1116        let config = config(&[], &[]);
1117        for name in machine_names() {
1118            assert!(host_allowed(name, &config), "{name}");
1119            let short = name.split('.').next().unwrap();
1120            // Any other domain is someone else's name for this machine.
1121            assert!(!host_allowed(&format!("{short}.evil.com"), &config));
1122            assert!(!host_allowed(&format!("{short}x"), &config));
1123        }
1124    }
1125
1126    #[test]
1127    fn only_exact_machine_names() {
1128        let names = names_of(
1129            &["WS01.site.org\n".into(), "ws01".into(), String::new()],
1130            &resolv_domains("# a comment\ndomain site.org\nsearch Other.org. site.org\n"),
1131        );
1132        assert_eq!(
1133            names,
1134            ["ws01", "ws01.other.org", "ws01.site.org"].map(String::from)
1135        );
1136        let names = names_of(&["ws01".into()], &[]);
1137        assert_eq!(names, ["ws01"]);
1138        assert_eq!(
1139            resolv_domains("nameserver 10.0.0.1\n"),
1140            Vec::<String>::new()
1141        );
1142    }
1143
1144    #[test]
1145    fn clients_are_counted_by_v4_address_or_v6_network() {
1146        let ip = |s: &str| client_key(s.parse().unwrap());
1147        assert_eq!(ip("192.0.2.1"), ip("::ffff:192.0.2.1"));
1148        assert_ne!(ip("192.0.2.1"), ip("192.0.2.2"));
1149        assert_eq!(ip("2001:db8:1:2::1"), ip("2001:db8:1:2:ffff::9"));
1150        assert_eq!(ip("2001:db8:1:2::1"), ip("2001:db8:1:2::"));
1151        assert_ne!(ip("2001:db8:1:2::1"), ip("2001:db8:1:3::1"));
1152    }
1153
1154    #[test]
1155    fn one_v6_network_shares_a_cap() {
1156        let hub = Arc::new(Hub::new(PvManager::new(), config(&[], &[])));
1157        let slots: Vec<_> = (0..10u16)
1158            .map(|n| {
1159                let ip = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, n));
1160                ClientSlot::take(&hub, ip).expect("under the cap")
1161            })
1162            .collect();
1163        let next = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 9, 9, 9, 9));
1164        assert!(ClientSlot::take(&hub, next).is_err());
1165        let other = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 2, 0, 0, 0, 1));
1166        assert!(ClientSlot::take(&hub, other).is_ok());
1167        drop(slots);
1168    }
1169
1170    #[test]
1171    fn subscriptions_are_paced() {
1172        let t0 = Instant::now();
1173        let mut pace = Pace::new();
1174        assert_eq!(pace.spend(MAX_PVS, t0), Duration::ZERO);
1175        // The bucket is empty: 500 more wait half a second.
1176        let wait = pace.spend(500, t0);
1177        assert!((wait.as_secs_f64() - 0.5).abs() < 1e-9, "{wait:?}");
1178        // Ten seconds later it is full again, but no fuller.
1179        let later = t0 + Duration::from_secs(20);
1180        assert_eq!(pace.spend(MAX_PVS, later), Duration::ZERO);
1181        assert!(pace.spend(1, later) > Duration::ZERO);
1182    }
1183
1184    #[test]
1185    fn pacing_counts_only_pvs_that_would_be_subscribed() {
1186        let mut subs = Subs::default();
1187        let total = Arc::new(AtomicUsize::new(0));
1188        let pvs = PvManager::new();
1189        subs.add(
1190            "HAVE",
1191            pvs.subscribe("loc://pace_have"),
1192            SubSlot::take(&total).unwrap(),
1193        );
1194        let msg = |names: Vec<&str>| json!({"type": "subscribe", "pvs": names}).to_string();
1195        let mut names = vec!["DUP:X"; 30_000];
1196        names.extend([
1197            "HAVE",
1198            "bad name",
1199            "loc://x",
1200            "sim://sine",
1201            "NEW",
1202            "pva://NEW",
1203        ]);
1204        assert_eq!(new_names(&subs, &msg(names)), 3);
1205        // No more than the client may still have.
1206        let many: Vec<String> = (0..MAX_PVS + 5).map(|i| format!("PV{i}")).collect();
1207        let many = msg(many.iter().map(String::as_str).collect());
1208        assert_eq!(new_names(&subs, &many), MAX_PVS - 1);
1209        assert_eq!(new_names(&subs, r#"{"type": "clear", "pvs": ["A"]}"#), 0);
1210    }
1211
1212    #[test]
1213    fn local_and_simulated_pvs_are_refused() {
1214        let hub = Hub::new(PvManager::new(), config(&[], &[]));
1215        let peer: SocketAddr = "192.0.2.1:5000".parse().unwrap();
1216        let mut subs = Subs::default();
1217        for name in ["loc://x(1)", "sim://sine", "loc://y", "=pv('loc://x')"] {
1218            let msg = json!({"type": "subscribe", "pvs": [name]}).to_string();
1219            let replies = handle(&hub, &mut subs, &msg, peer);
1220            assert!(replies[0].contains("loc:// and sim://"), "{replies:?}");
1221        }
1222        assert!(subs.map.is_empty());
1223        assert_eq!(hub.subscriptions.load(Ordering::SeqCst), 0);
1224        assert!(served_scheme("ca://X") && served_scheme("pva://X") && served_scheme("X"));
1225    }
1226
1227    #[test]
1228    fn only_changed_pvs_are_sent() {
1229        let hub = Hub::new(PvManager::new(), config(&[], &[]));
1230        let mut subs = Subs::default();
1231        let total = Arc::new(AtomicUsize::new(0));
1232        let a = hub.manager.subscribe("loc://changed_a(1)");
1233        let b = hub.manager.subscribe("loc://changed_b(1)");
1234        subs.add("a", a.clone(), SubSlot::take(&total).unwrap());
1235        subs.add("b", b.clone(), SubSlot::take(&total).unwrap());
1236        // Both are new.
1237        assert_eq!(updates(&hub, &mut subs).len(), 2);
1238        assert!(updates(&hub, &mut subs).is_empty());
1239        a.write("2");
1240        let sent = updates(&hub, &mut subs);
1241        assert_eq!(sent.len(), 1);
1242        assert!(sent[0].contains("\"pv\":\"a\""), "{sent:?}");
1243        // A cleared PV marks nothing.
1244        subs.map.remove("b");
1245        b.write("3");
1246        assert!(subs.changed.take().is_empty());
1247        assert_eq!(total.load(Ordering::SeqCst), 1);
1248    }
1249
1250    #[test]
1251    fn normalised_origins() {
1252        assert_eq!(
1253            normalise_origin("HTTPS://Host.Example:443/"),
1254            "https://host.example"
1255        );
1256        assert_eq!(normalise_origin("http://host:80"), "http://host");
1257        assert_eq!(normalise_origin("http://host:8080"), "http://host:8080");
1258        assert_eq!(normalise_origin("https://host:80"), "https://host:80");
1259    }
1260
1261    /// A server on a free port with `config`, and its hub.
1262    fn start(config: Config) -> (Arc<Hub>, SocketAddr, PvManager) {
1263        let manager = PvManager::new();
1264        let hub = Arc::new(Hub::new(manager.clone(), config));
1265        let app = axum::Router::new()
1266            .route("/pvws/pv", axum::routing::get(socket))
1267            .with_state(hub.clone())
1268            .into_make_service_with_connect_info::<SocketAddr>();
1269        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1270        listener.set_nonblocking(true).unwrap();
1271        let addr = listener.local_addr().unwrap();
1272        manager.runtime().spawn(async move {
1273            let listener = tokio::net::TcpListener::from_std(listener).unwrap();
1274            axum::serve(listener, app).await.unwrap();
1275        });
1276        (hub, addr, manager)
1277    }
1278
1279    /// Open a websocket by hand; returns the stream and the status line.
1280    async fn upgrade(addr: SocketAddr, origin: Option<&str>) -> (tokio::net::TcpStream, String) {
1281        use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
1282        let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
1283        let origin = origin.map_or(String::new(), |o| format!("Origin: {o}\r\n"));
1284        let request = format!(
1285            "GET /pvws/pv HTTP/1.1\r\nHost: {addr}\r\n{origin}Connection: Upgrade\r\n\
1286             Upgrade: websocket\r\nSec-WebSocket-Version: 13\r\n\
1287             Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n"
1288        );
1289        stream.write_all(request.as_bytes()).await.unwrap();
1290        let mut status = String::new();
1291        BufReader::new(&mut stream)
1292            .read_line(&mut status)
1293            .await
1294            .unwrap();
1295        (stream, status)
1296    }
1297
1298    #[test]
1299    fn origin_is_checked_without_writes() {
1300        let mut read_only = config(&[], &[]);
1301        read_only.write = false;
1302        let (_hub, addr, manager) = start(read_only);
1303        manager.runtime().block_on(async {
1304            let (_s, status) = upgrade(addr, Some("http://evil.example")).await;
1305            assert!(status.contains("403"), "{status}");
1306            let (_s, status) = upgrade(addr, Some(&format!("http://{addr}"))).await;
1307            assert!(status.contains("101"), "{status}");
1308        });
1309    }
1310
1311    #[test]
1312    fn silent_clients_are_dropped() {
1313        let mut quick = config(&[], &[]);
1314        quick.ping = Duration::from_millis(50);
1315        let (hub, addr, manager) = start(quick);
1316        manager.runtime().block_on(async {
1317            // Connects, then neither reads nor answers pings.
1318            let (stream, status) = upgrade(addr, None).await;
1319            assert!(status.contains("101"), "{status}");
1320            assert_eq!(hub.clients.lock().unwrap().len(), 1);
1321            let mut waited = 0;
1322            while !hub.clients.lock().unwrap().is_empty() {
1323                assert!(waited < 100, "the silent client kept its slot");
1324                tokio::time::sleep(Duration::from_millis(50)).await;
1325                waited += 1;
1326            }
1327            drop(stream);
1328        });
1329    }
1330
1331    #[test]
1332    fn pv_names() {
1333        assert!(valid_name("SR-DI-DCCT-01:SIGNAL"));
1334        assert!(valid_name("pva://a.b[1]{x}"));
1335        assert!(!valid_name(""));
1336        assert!(!valid_name("has space"));
1337        assert!(!valid_name("new\nline"));
1338        assert!(!valid_name("caf\u{e9}"));
1339        assert!(valid_name(&"x".repeat(MAX_NAME)));
1340        assert!(!valid_name(&"x".repeat(MAX_NAME + 1)));
1341    }
1342
1343    #[test]
1344    fn write_tokens() {
1345        assert!(token_ok("secret", "secret"));
1346        assert!(!token_ok("", "secret"));
1347        assert!(!token_ok("secreT", "secret"));
1348        assert!(!token_ok("secret1", "secret"));
1349    }
1350
1351    #[test]
1352    fn client_and_subscription_caps() {
1353        let hub = Arc::new(Hub::new(PvManager::new(), config(&[], &[])));
1354        let ip = |n: usize| IpAddr::from([10, 0, (n / 250) as u8, (n % 250) as u8]);
1355        let slots: Vec<_> = (0..MAX_CLIENTS)
1356            .map(|n| ClientSlot::take(&hub, ip(n)).expect("under the cap"))
1357            .collect();
1358        assert!(ClientSlot::take(&hub, ip(MAX_CLIENTS)).is_err());
1359        drop(slots);
1360        assert!(hub.clients.lock().unwrap().is_empty());
1361        assert!(ClientSlot::take(&hub, ip(0)).is_ok());
1362
1363        // One address gets only its share.
1364        let mine: Vec<_> = (0..10)
1365            .map(|_| ClientSlot::take(&hub, ip(1)).expect("under the per-address cap"))
1366            .collect();
1367        assert!(ClientSlot::take(&hub, ip(1)).is_err());
1368        assert!(ClientSlot::take(&hub, ip(2)).is_ok());
1369        drop(mine);
1370        assert!(ClientSlot::take(&hub, ip(1)).is_ok());
1371
1372        let total = Arc::new(AtomicUsize::new(MAX_TOTAL_PVS - 1));
1373        let last = SubSlot::take(&total).expect("one left");
1374        assert!(SubSlot::take(&total).is_none());
1375        drop(last);
1376        assert_eq!(total.load(Ordering::SeqCst), MAX_TOTAL_PVS - 1);
1377    }
1378
1379    #[test]
1380    fn writes_need_the_token() {
1381        let hub = Hub::new(PvManager::new(), config(&[], &[]));
1382        let peer: SocketAddr = "192.0.2.1:5000".parse().unwrap();
1383        let mut subs = Subs::default();
1384        let sub = |subs: &mut Subs, name: &str| {
1385            let msg = json!({"type": "subscribe", "pvs": [name]}).to_string();
1386            handle(&hub, subs, &msg, peer)
1387        };
1388        assert!(sub(&mut subs, "TEST:WRITE").is_empty());
1389        assert_eq!(hub.subscriptions.load(Ordering::SeqCst), 1);
1390        let write = |subs: &mut Subs, token: Option<&str>| {
1391            let mut msg = json!({"type": "write", "pv": "TEST:WRITE", "value": 2});
1392            if let Some(t) = token {
1393                msg["token"] = json!(t);
1394            }
1395            handle(&hub, subs, &msg.to_string(), peer)
1396        };
1397        assert!(write(&mut subs, None)[0].contains("token"));
1398        assert!(write(&mut subs, Some("wrong"))[0].contains("token"));
1399        // The right token: TEST:WRITE has yet to connect, so the write is
1400        // queued for it.
1401        assert!(write(&mut subs, Some("secret")).is_empty());
1402        // Once it has connected and dropped, the client hears so at once.
1403        let pv = &subs.map["TEST:WRITE"].pv;
1404        pv.set_connected_for_test(true);
1405        pv.set_connected_for_test(false);
1406        let refused = write(&mut subs, Some("secret"));
1407        let m: Map<String, Json> = serde_json::from_str(&refused[0]).unwrap();
1408        assert_eq!(m["pv"], "TEST:WRITE");
1409        assert_eq!(m["write_error"], "not connected");
1410        // Bad names are refused and not counted.
1411        assert!(sub(&mut subs, "bad name")[0].contains("Cannot subscribe"));
1412        assert_eq!(subs.map.len(), 1);
1413        subs.map.clear();
1414        assert_eq!(hub.subscriptions.load(Ordering::SeqCst), 0);
1415    }
1416
1417    #[test]
1418    fn long_string_writes_are_marked() {
1419        let hub = Hub::new(PvManager::new(), config(&[], &[]));
1420        let peer: SocketAddr = "192.0.2.1:5000".parse().unwrap();
1421        let mut subs = Subs::default();
1422        // Clients may not subscribe to a local PV, but one shows the value
1423        // written.
1424        let name = "loc://srv_long_string<VLongArray>(0,0)";
1425        let slot = SubSlot::take(&hub.subscriptions).unwrap();
1426        subs.add(name, hub.manager.subscribe(name), slot);
1427        let write = |subs: &mut Subs, value: &str, long_string: bool| {
1428            let mut msg = json!({"type": "write", "pv": name, "value": value, "token": "secret"});
1429            if long_string {
1430                msg["long_string"] = json!(true);
1431            }
1432            assert!(handle(&hub, subs, &msg.to_string(), peer).is_empty());
1433            subs.map[name].pv.state().value
1434        };
1435        assert_eq!(
1436            write(&mut subs, "ab", true),
1437            Value::LongArray(Arc::from(vec![97, 98, 0]))
1438        );
1439        assert_eq!(
1440            write(&mut subs, "[1, 2]", false),
1441            Value::LongArray(Arc::from(vec![1, 2]))
1442        );
1443    }
1444
1445    #[test]
1446    fn written_numbers() {
1447        assert_eq!(write_text(&json!(5.0)).unwrap(), "5");
1448        assert_eq!(write_text(&json!(2.5)).unwrap(), "2.5");
1449        assert_eq!(write_text(&json!("abc")).unwrap(), "abc");
1450    }
1451}