Skip to main content

ljos_cli/
pack_link.rs

1//! The only speaker that may carry this seat's packsetd token.
2//!
3//! The token stays in this module. Each request is one byte stream to the
4//! socket address resolved for the writer's URL, with no proxy and no
5//! redirect. The proof runs before the token is written on that stream. A
6//! stream that closes is not remembered: the next call proves again before
7//! the token goes out. packsetd answers one request and closes, so the two
8//! refusals and the call that carries the token are three sockets in a row;
9//! the proof is not kept for a later call.
10
11use std::collections::HashMap;
12use std::io::{Read, Write};
13use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
14use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
15use std::sync::{Arc, Mutex, OnceLock};
16use std::time::{Duration, Instant};
17
18use packset_client::{Error, Hit};
19use serde::de::DeserializeOwned;
20use serde_json::Value;
21use url::Url;
22
23/// The seat's pack writer. The token field is private to this module.
24pub struct PacksetClient {
25    base: String,
26    origin: Url,
27    addr: Option<SocketAddr>,
28    workspace: Option<String>,
29    token: Mutex<Option<String>>,
30}
31
32impl Clone for PacksetClient {
33    fn clone(&self) -> Self {
34        Self {
35            base: self.base.clone(),
36            origin: self.origin.clone(),
37            addr: self.addr,
38            workspace: self.workspace.clone(),
39            token: Mutex::new(self.token.lock().unwrap_or_else(|p| p.into_inner()).clone()),
40        }
41    }
42}
43
44/// One answer. `trusted` means the token was on this answer, after the
45/// proof held.
46#[derive(Debug, Clone)]
47pub(crate) struct Answer {
48    pub status: u16,
49    pub body: String,
50    pub trusted: bool,
51}
52
53/// Answers a hook process already has for a search or a status read, by
54/// request, for [`ANSWER_MEMO`]. A prompt hook asks the same search
55/// twice and the status twice; the second asks are answered here. Only a
56/// hook keeps one, and any write through the client drops it.
57type KeptAnswer = (String, Instant, Answer);
58static KEPT_ANSWERS: Mutex<Vec<KeptAnswer>> = Mutex::new(Vec::new());
59const ANSWER_MEMO: Duration = Duration::from_secs(2);
60
61fn answer_memo_key(
62    method: &str,
63    path: &str,
64    query: &[(&str, &str)],
65    bearer: bool,
66) -> Option<String> {
67    if method != "GET" || !matches!(path, "/v1/search" | "/v1/status") {
68        return None;
69    }
70    std::env::var_os("LJOS_IN_HOOK")?;
71    let mut key = format!("{path}|{bearer}");
72    for (k, v) in query {
73        key.push('|');
74        key.push_str(k);
75        key.push('=');
76        key.push_str(v);
77    }
78    Some(key)
79}
80
81fn kept_answer(key: &str) -> Option<Answer> {
82    let kept = KEPT_ANSWERS.lock().unwrap_or_else(|p| p.into_inner());
83    kept.iter()
84        .find(|(k, at, _)| k == key && at.elapsed() < ANSWER_MEMO)
85        .map(|(_, _, answer)| answer.clone())
86}
87
88fn keep_answer(key: String, answer: &Answer) {
89    let mut kept = KEPT_ANSWERS.lock().unwrap_or_else(|p| p.into_inner());
90    kept.retain(|(k, _, _)| k != &key);
91    kept.push((key, Instant::now(), answer.clone()));
92}
93
94/// Drop every kept answer. Called on any write through the client.
95pub(crate) fn forget_kept_answers() {
96    KEPT_ANSWERS
97        .lock()
98        .unwrap_or_else(|p| p.into_inner())
99        .clear();
100}
101
102struct Slot {
103    stream: Option<TcpStream>,
104    /// The proof held on `stream` and the stream is still the one it held on.
105    proven: bool,
106    token_tag: u64,
107    /// The last answer on this socket carried the token.
108    carried: bool,
109}
110
111fn slots() -> &'static Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>> {
112    static SLOTS: OnceLock<Mutex<HashMap<SocketAddr, Arc<Mutex<Slot>>>>> = OnceLock::new();
113    SLOTS.get_or_init(|| Mutex::new(HashMap::new()))
114}
115
116fn slot_for(addr: SocketAddr) -> Arc<Mutex<Slot>> {
117    let mut map = slots().lock().unwrap_or_else(|p| p.into_inner());
118    map.entry(addr)
119        .or_insert_with(|| {
120            Arc::new(Mutex::new(Slot {
121                stream: None,
122                proven: false,
123                token_tag: 0,
124                carried: false,
125            }))
126        })
127        .clone()
128}
129
130fn load_token() -> Option<String> {
131    packset_client::token()
132}
133
134fn token_tag(token: Option<&str>) -> u64 {
135    let Some(token) = token else {
136        return 0;
137    };
138    let mut hash = 0xcbf29ce484222325u64;
139    for byte in token.as_bytes() {
140        hash ^= u64::from(*byte);
141        hash = hash.wrapping_mul(0x100000001b3);
142    }
143    hash
144}
145
146fn nonce() -> Option<String> {
147    use std::io::Read as _;
148    let mut raw = [0u8; 32];
149    std::fs::File::open("/dev/urandom")
150        .and_then(|mut file| file.read_exact(&mut raw))
151        .ok()
152        .map(|()| hex::encode(raw))
153}
154
155fn pin(url: &Url) -> Option<SocketAddr> {
156    let host = url.host_str()?;
157    let port = url.port_or_known_default()?;
158    (host, port).to_socket_addrs().ok()?.next()
159}
160
161fn host_header(url: &Url) -> String {
162    match (url.host_str(), url.port()) {
163        (Some(host), Some(port)) if host.contains(':') => format!("[{host}]:{port}"),
164        (Some(host), Some(port)) => format!("{host}:{port}"),
165        (Some(host), None) => host.to_string(),
166        _ => "localhost".to_string(),
167    }
168}
169
170fn budget() -> Duration {
171    std::env::var("PACKSET_TIMEOUT_MS")
172        .ok()
173        .and_then(|v| v.trim().parse::<u64>().ok())
174        .filter(|ms| *ms > 0)
175        .map_or(Duration::from_secs(30), Duration::from_millis)
176}
177
178/// Whether every socket listening on the loopback port of `url` belongs to
179/// another user. Unknown is not foreign.
180fn foreign(url: &str) -> bool {
181    // Answered once per process for each url: the socket tables are read
182    // in full each time, and a hook connects several times.
183    static KNOWN: std::sync::LazyLock<Mutex<Vec<(String, bool)>>> =
184        std::sync::LazyLock::new(|| Mutex::new(Vec::new()));
185    if let Some((_, known)) = KNOWN
186        .lock()
187        .unwrap_or_else(|p| p.into_inner())
188        .iter()
189        .find(|(u, _)| u == url)
190    {
191        return *known;
192    }
193    let answer = foreign_read(url);
194    KNOWN
195        .lock()
196        .unwrap_or_else(|p| p.into_inner())
197        .push((url.to_string(), answer));
198    answer
199}
200
201fn foreign_read(url: &str) -> bool {
202    #[cfg(target_os = "linux")]
203    {
204        use std::os::unix::fs::MetadataExt;
205        let Some(port) = loopback_port(url) else {
206            return false;
207        };
208        let Ok(me) = std::fs::metadata("/proc/self").map(|m| m.uid()) else {
209            return false;
210        };
211        let owners: Vec<u32> = ["/proc/net/tcp", "/proc/net/tcp6"]
212            .iter()
213            .filter_map(|table| std::fs::read_to_string(table).ok())
214            .flat_map(|text| listen_uids(&text, port))
215            .collect();
216        !owners.is_empty() && owners.iter().all(|uid| *uid != me)
217    }
218    #[cfg(not(target_os = "linux"))]
219    {
220        let _ = url;
221        false
222    }
223}
224
225#[cfg(target_os = "linux")]
226fn loopback_port(url: &str) -> Option<u16> {
227    let rest = url
228        .strip_prefix("http://")
229        .or_else(|| url.strip_prefix("https://"))?;
230    let authority = rest.split(['/', '?', '#']).next()?;
231    let (host, port) = match authority.rsplit_once(':') {
232        Some((host, port)) if !port.contains(']') => (host, port.parse().ok()?),
233        _ => (authority, 80),
234    };
235    matches!(host, "127.0.0.1" | "localhost" | "[::1]").then_some(port)
236}
237
238#[cfg(target_os = "linux")]
239fn listen_uids(table: &str, port: u16) -> Vec<u32> {
240    table
241        .lines()
242        .skip(1)
243        .filter_map(|line| {
244            let fields: Vec<&str> = line.split_whitespace().collect();
245            if fields.get(3) != Some(&"0A") {
246                return None;
247            }
248            let hex = fields.get(1)?.rsplit(':').next()?;
249            if u16::from_str_radix(hex, 16).ok()? != port {
250                return None;
251            }
252            fields.get(7)?.parse().ok()
253        })
254        .collect()
255}
256
257fn target(path: &str, query: &[(&str, &str)]) -> String {
258    if query.is_empty() {
259        return path.to_string();
260    }
261    let mut ser = url::form_urlencoded::Serializer::new(String::new());
262    for (key, value) in query {
263        ser.append_pair(key, value);
264    }
265    format!("{path}?{}", ser.finish())
266}
267
268struct Raw {
269    status: u16,
270    body: String,
271    keep: bool,
272}
273
274/// The pack accepted a connection and then stayed silent, or the connect
275/// itself never finished. While a [`QuietPack`] guard is alive, the first
276/// such read still waits out its budget and a later read returns before it
277/// opens another socket. A consensus reading asks several times. Without
278/// the stop, each ask waited the whole budget and the command sat for the
279/// sum. Dropping the guard clears it, so a later call can try the pack again.
280static PACK_QUIET: AtomicBool = AtomicBool::new(false);
281static QUIET_HELD: AtomicUsize = AtomicUsize::new(0);
282
283/// Remember one quiet pack for the reads this guard covers.
284pub struct QuietPack;
285
286impl QuietPack {
287    /// Start remembering a silent pack. The first guard of a pile drops a
288    /// quiet read already seen, so the pile starts with a fresh try.
289    #[must_use]
290    pub fn enter() -> Self {
291        if QUIET_HELD.fetch_add(1, Ordering::AcqRel) == 0 {
292            PACK_QUIET.store(false, Ordering::Relaxed);
293        }
294        Self
295    }
296}
297
298impl Drop for QuietPack {
299    fn drop(&mut self) {
300        if QUIET_HELD.fetch_sub(1, Ordering::AcqRel) == 1 {
301            PACK_QUIET.store(false, Ordering::Relaxed);
302        }
303    }
304}
305
306fn holding_quiet() -> bool {
307    QUIET_HELD.load(Ordering::Relaxed) > 0
308}
309
310fn pack_quiet() -> bool {
311    holding_quiet() && PACK_QUIET.load(Ordering::Relaxed)
312}
313
314fn mark_pack_quiet() {
315    if holding_quiet() {
316        PACK_QUIET.store(true, Ordering::Relaxed);
317    }
318}
319
320fn quiet_io() -> std::io::Error {
321    std::io::Error::new(std::io::ErrorKind::TimedOut, "the pack did not answer")
322}
323
324/// A read or a connect that ended because the socket stayed silent.
325/// A refused connect is not this: the writer may still be starting.
326fn io_is_quiet(err: &std::io::Error) -> bool {
327    matches!(
328        err.kind(),
329        std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
330    )
331}
332
333fn connect(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
334    if pack_quiet() {
335        return Err(quiet_io());
336    }
337    if side_budget_spent() {
338        return Err(budget_spent());
339    }
340    let started = Instant::now();
341    let result = connect_once(addr, timeout);
342    // A connect that returns a socket is the pack accepting. Time spent
343    // there is not a quiet wait. A connect that fails spends the budget.
344    // A connect that never finishes is remembered, so the next read does
345    // not wait the budget out again.
346    if let Err(err) = &result {
347        if io_is_quiet(err) {
348            mark_pack_quiet();
349        }
350        super::charge_side_wait(started.elapsed());
351    }
352    result
353}
354
355/// A side event's pack budget is gone. Opening a socket for a one
356/// millisecond timer still blocks, and a prompt makes several of those
357/// reads, so the call returns before the connect.
358fn side_budget_spent() -> bool {
359    super::side_remaining_ms() == Some(0)
360}
361
362fn budget_spent() -> std::io::Error {
363    std::io::Error::new(
364        std::io::ErrorKind::TimedOut,
365        "the side event's pack budget is spent",
366    )
367}
368
369fn connect_once(addr: SocketAddr, timeout: Duration) -> std::io::Result<TcpStream> {
370    let wait = capped_wait(timeout);
371    let stream = TcpStream::connect_timeout(&addr, wait)?;
372    stream.set_read_timeout(Some(wait))?;
373    stream.set_write_timeout(Some(wait))?;
374    let _ = stream.set_nodelay(true);
375    Ok(stream)
376}
377
378/// A side-event bound shrinks this wait. A zero bound is one millisecond,
379/// so a later read fails at once instead of taking the default.
380fn capped_wait(timeout: Duration) -> Duration {
381    let wait = super::cap_pack_wait(timeout);
382    if wait.is_zero() {
383        Duration::from_millis(1)
384    } else {
385        wait
386    }
387}
388
389fn write_all(stream: &mut TcpStream, bytes: &[u8]) -> std::io::Result<()> {
390    let mut sent = 0;
391    while sent < bytes.len() {
392        match stream.write(&bytes[sent..]) {
393            Ok(0) => {
394                return Err(std::io::Error::new(
395                    std::io::ErrorKind::WriteZero,
396                    "the pack closed the socket",
397                ))
398            }
399            Ok(n) => sent += n,
400            Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
401            Err(err) => return Err(err),
402        }
403    }
404    stream.flush()
405}
406
407fn read_response(stream: &mut TcpStream, deadline: Instant) -> std::io::Result<Raw> {
408    let mut buf = Vec::new();
409    let mut tmp = [0u8; 4096];
410    let head_end = loop {
411        if buf.len() > 64 * 1024 {
412            return Err(std::io::Error::new(
413                std::io::ErrorKind::InvalidData,
414                "the pack's headers are too long",
415            ));
416        }
417        arm_read(stream, deadline)?;
418        let n = read_some(stream, &mut tmp)?;
419        buf.extend_from_slice(&tmp[..n]);
420        if let Some(at) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
421            break at;
422        }
423    };
424    let header = String::from_utf8_lossy(&buf[..head_end]).into_owned();
425    let status = header
426        .lines()
427        .next()
428        .and_then(|line| line.split_whitespace().nth(1))
429        .and_then(|code| code.parse().ok())
430        .ok_or_else(|| {
431            std::io::Error::new(
432                std::io::ErrorKind::InvalidData,
433                "the pack's status line is not HTTP",
434            )
435        })?;
436    let mut length = None;
437    let mut close = header.starts_with("HTTP/1.0");
438    for line in header.lines().skip(1) {
439        let Some((name, value)) = line.split_once(':') else {
440            continue;
441        };
442        if name.eq_ignore_ascii_case("content-length") {
443            length = value.trim().parse().ok();
444        } else if name.eq_ignore_ascii_case("connection")
445            && value.trim().eq_ignore_ascii_case("close")
446        {
447            close = true;
448        }
449    }
450    let mut body = buf.split_off(head_end + 4);
451    if let Some(length) = length {
452        while body.len() < length {
453            arm_read(stream, deadline)?;
454            let n = read_some(stream, &mut tmp)?;
455            body.extend_from_slice(&tmp[..n]);
456            if body.len() > length.saturating_add(64 * 1024) {
457                break;
458            }
459        }
460        body.truncate(length);
461    } else {
462        loop {
463            if deadline.saturating_duration_since(Instant::now()).is_zero() {
464                break;
465            }
466            let _ = arm_read(stream, deadline);
467            match stream.read(&mut tmp) {
468                Ok(0) => break,
469                Ok(n) => body.extend_from_slice(&tmp[..n]),
470                Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
471                Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
472                Err(err) if err.kind() == std::io::ErrorKind::TimedOut => break,
473                Err(_) => break,
474            }
475        }
476        close = true;
477    }
478    // A redirect is the answer. The token must not follow it.
479    Ok(Raw {
480        status,
481        body: String::from_utf8_lossy(&body).into_owned(),
482        keep: !close && !(300..400).contains(&status),
483    })
484}
485
486fn arm_read(stream: &TcpStream, deadline: Instant) -> std::io::Result<()> {
487    let remain = deadline.saturating_duration_since(Instant::now());
488    if remain.is_zero() {
489        return Err(std::io::Error::new(
490            std::io::ErrorKind::TimedOut,
491            "the pack did not answer",
492        ));
493    }
494    stream.set_read_timeout(Some(remain))?;
495    Ok(())
496}
497
498fn read_some(stream: &mut TcpStream, tmp: &mut [u8]) -> std::io::Result<usize> {
499    loop {
500        match stream.read(tmp) {
501            Ok(0) => {
502                return Err(std::io::Error::new(
503                    std::io::ErrorKind::UnexpectedEof,
504                    "the pack closed the socket",
505                ))
506            }
507            Ok(n) => return Ok(n),
508            Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
509            Err(err) => return Err(err),
510        }
511    }
512}
513
514fn render(
515    method: &str,
516    host: &str,
517    path: &str,
518    token: Option<&str>,
519    body: Option<&[u8]>,
520) -> Vec<u8> {
521    let mut head =
522        format!("{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: keep-alive\r\n");
523    if let Some(token) = token {
524        head.push_str("Authorization: Bearer ");
525        head.push_str(token);
526        head.push_str("\r\n");
527    }
528    let mut out = Vec::new();
529    if let Some(body) = body {
530        head.push_str("Content-Type: application/json\r\n");
531        head.push_str(&format!("Content-Length: {}\r\n\r\n", body.len()));
532        out.extend(head.into_bytes());
533        out.extend_from_slice(body);
534    } else {
535        head.push_str("\r\n");
536        out.extend(head.into_bytes());
537    }
538    out
539}
540
541struct Wire<'a> {
542    method: &'a str,
543    path: &'a str,
544    query: &'a [(&'a str, &'a str)],
545    body: Option<&'a [u8]>,
546    token: Option<&'a str>,
547}
548
549enum Gate {
550    /// The proof held. The call may carry the token.
551    Send,
552    /// The listener did not ask for the token.
553    Bare,
554    /// The writer is busy. The call is not sent.
555    Busy(Raw),
556}
557
558impl PacksetClient {
559    pub fn new(base: impl Into<String>) -> Self {
560        let mut base = base.into();
561        while base.ends_with('/') {
562            base.pop();
563        }
564        let origin = Url::parse(&base).unwrap_or_else(|_| Url::parse("http://127.0.0.1/").unwrap());
565        let addr = Url::parse(&base).ok().as_ref().and_then(pin);
566        Self {
567            base,
568            origin,
569            addr,
570            workspace: None,
571            token: Mutex::new(load_token()),
572        }
573    }
574
575    /// The writer the seat talks to. `PACKSET_URL=off` is no pack.
576    pub fn from_env() -> Result<Self, Error> {
577        packset_client::load_seat_env();
578        let url = std::env::var("PACKSET_URL")
579            .or_else(|_| std::env::var("INSIDE_MEMORY_URL"))
580            .ok()
581            .filter(|url| !url.is_empty());
582        match url {
583            Some(url) if url == "off" => Err(Error::NoUrl),
584            Some(url) => Ok(Self::new(url)),
585            None => Ok(Self::new(format!(
586                "http://127.0.0.1:{}",
587                packset_client::default_port()
588            ))),
589        }
590    }
591
592    #[must_use]
593    pub fn with_workspace(mut self, workspace: impl Into<String>) -> Self {
594        let workspace = workspace.into();
595        self.workspace = (!workspace.is_empty()).then_some(workspace);
596        self
597    }
598
599    pub fn base(&self) -> &str {
600        &self.base
601    }
602
603    /// Whether this seat has a token to show. The token itself stays here.
604    #[must_use]
605    pub(crate) fn has_token(&self) -> bool {
606        load_token().is_some()
607    }
608
609    /// Whether a call may carry the token: the seat has one, and another
610    /// user does not hold the loopback port.
611    #[must_use]
612    pub(crate) fn may_carry(&self) -> bool {
613        load_token().is_some() && !foreign(&self.base)
614    }
615
616    /// Whether the last answer on this socket carried the token. A closed
617    /// socket does not keep this for the next call: that call proves again.
618    #[must_use]
619    pub(crate) fn carried(&self) -> bool {
620        let Some(addr) = self.addr else {
621            return false;
622        };
623        let map = slots().lock().unwrap_or_else(|p| p.into_inner());
624        map.get(&addr)
625            .is_some_and(|slot| slot.lock().unwrap_or_else(|p| p.into_inner()).carried)
626    }
627
628    pub fn workspace(&self) -> String {
629        if let Some(workspace) = &self.workspace {
630            return workspace.clone();
631        }
632        if let Ok(workspace) = std::env::var("PACKSET_WORKSPACE") {
633            if !workspace.is_empty() {
634                return workspace;
635            }
636        }
637        let cwd = std::env::var("GROKOS_WORKSPACE")
638            .ok()
639            .map(|s| s.trim().to_string())
640            .filter(|s| !s.is_empty())
641            .map(std::path::PathBuf::from)
642            .or_else(|| std::env::current_dir().ok())
643            .unwrap_or_else(|| std::path::PathBuf::from("."));
644        let abs = cwd.canonicalize().unwrap_or(cwd);
645        let shown = abs.to_string_lossy().into_owned();
646        if let Ok(answer) = self.roundtrip(
647            "GET",
648            "/v1/identity",
649            &[("cwd", shown.as_str())],
650            None,
651            true,
652            budget(),
653        ) {
654            if answer.status == 200 {
655                if let Ok(val) = serde_json::from_str::<Value>(&answer.body) {
656                    if let Some(ws) = val.get("workspace").and_then(|v| v.as_str()) {
657                        if !ws.is_empty() {
658                            return ws.to_string();
659                        }
660                    }
661                }
662            }
663        }
664        format!("dir:{}", abs.display())
665    }
666
667    /// One request. `bearer` asks to carry the token; it is left off when
668    /// the proof does not hold or another user owns the port. A 3xx is
669    /// returned as the answer and not followed. HTTP 503 is one answer:
670    /// the caller decides whether to ask again.
671    pub(crate) fn roundtrip(
672        &self,
673        method: &str,
674        path: &str,
675        query: &[(&str, &str)],
676        body: Option<&[u8]>,
677        bearer: bool,
678        timeout: Duration,
679    ) -> Result<Answer, Error> {
680        // A write changes what a listing would say: the memo this process
681        // keeps and the gate's freshness mark both go.
682        if method != "GET" {
683            crate::forget_atom_listings();
684            crate::forget_gate_fresh();
685            forget_kept_answers();
686        }
687        let memo_key = answer_memo_key(method, path, query, bearer);
688        if let Some(answer) = memo_key.as_deref().and_then(kept_answer) {
689            return Ok(answer);
690        }
691        let Some(addr) = self.addr else {
692            return Err(Error::Bad(format!(
693                "{} is not a socket this seat can pin",
694                self.base
695            )));
696        };
697        let token = load_token();
698        *self.token.lock().unwrap_or_else(|p| p.into_inner()) = token.clone();
699        let allow = bearer && token.is_some() && !foreign(&self.base);
700        let answer = self.exchange(
701            addr,
702            &Wire {
703                method,
704                path,
705                query,
706                body,
707                token: token.as_deref(),
708            },
709            allow,
710            timeout,
711        )?;
712        if let Some(key) = memo_key {
713            if (200..300).contains(&answer.status) {
714                keep_answer(key, &answer);
715            }
716        }
717        Ok(answer)
718    }
719
720    fn exchange(
721        &self,
722        addr: SocketAddr,
723        call: &Wire<'_>,
724        allow: bool,
725        timeout: Duration,
726    ) -> Result<Answer, Error> {
727        let slot = slot_for(addr);
728        let mut live = slot.lock().unwrap_or_else(|p| p.into_inner());
729        let tag = token_tag(call.token);
730        if live.token_tag != tag {
731            live.stream = None;
732            live.proven = false;
733            live.token_tag = tag;
734        }
735        let carried = if allow {
736            match self.gate(&mut live, addr, timeout)? {
737                Gate::Bare => None,
738                Gate::Send => call.token,
739                Gate::Busy(raw) => {
740                    live.carried = false;
741                    live.proven = false;
742                    return Ok(Answer {
743                        status: raw.status,
744                        body: raw.body,
745                        trusted: false,
746                    });
747                }
748            }
749        } else {
750            None
751        };
752        if live.stream.is_none() {
753            match connect(addr, timeout) {
754                Ok(stream) => live.stream = Some(stream),
755                Err(err) => {
756                    live.proven = false;
757                    live.carried = false;
758                    return Err(Error::Io(err));
759                }
760            }
761            // The probes ran on sockets the writer closed. This socket is
762            // the one the token rides, the third step of the same proof,
763            // not a later call.
764            live.proven = false;
765        }
766        let raw = match send(
767            &mut live,
768            &self.origin,
769            &Wire {
770                token: carried,
771                ..*call
772            },
773            timeout,
774        ) {
775            Ok(raw) => raw,
776            Err(err) => {
777                live.stream = None;
778                live.proven = false;
779                live.carried = false;
780                return Err(Error::Io(err));
781            }
782        };
783        let trusted = carried.is_some();
784        live.carried = trusted;
785        if trusted && live.stream.is_some() {
786            live.proven = true;
787        } else if live.stream.is_none() {
788            live.proven = false;
789        }
790        Ok(Answer {
791            status: raw.status,
792            body: raw.body,
793            trusted,
794        })
795    }
796
797    /// Two refusals on this socket, then the call may carry the token.
798    /// A busy writer (503) is returned as that answer; the call is not sent.
799    fn gate(&self, live: &mut Slot, addr: SocketAddr, timeout: Duration) -> Result<Gate, Error> {
800        if live.proven && live.stream.is_some() {
801            return Ok(Gate::Send);
802        }
803        live.proven = false;
804        let (Some(id), Some(bogus)) = (nonce(), nonce()) else {
805            return Err(Error::Io(std::io::Error::other(
806                "no randomness for the pack proof",
807            )));
808        };
809        let workspace = self.proof_workspace();
810        let path = format!("/v1/atoms/{}", &id[..32]);
811        let query = [("workspace", workspace.as_str())];
812        let mut stage = 0u8;
813        let mut restarts = 0u8;
814        loop {
815            if live.stream.is_none() {
816                live.stream = Some(connect(addr, timeout).map_err(Error::Io)?);
817                live.proven = false;
818            }
819            let shown = if stage == 0 {
820                None
821            } else {
822                Some(bogus.as_str())
823            };
824            let raw = match send(
825                live,
826                &self.origin,
827                &Wire {
828                    method: "GET",
829                    path: &path,
830                    query: &query,
831                    body: None,
832                    token: shown,
833                },
834                timeout,
835            ) {
836                Ok(raw) => raw,
837                Err(err) => {
838                    live.stream = None;
839                    live.proven = false;
840                    restarts += 1;
841                    if restarts > 2 {
842                        return Err(Error::Io(err));
843                    }
844                    stage = 0;
845                    continue;
846                }
847            };
848            if raw.status == 503 {
849                return Ok(Gate::Busy(raw));
850            }
851            if raw.status != 401 {
852                return Ok(Gate::Bare);
853            }
854            if stage == 0 {
855                stage = 1;
856                continue;
857            }
858            return Ok(Gate::Send);
859        }
860    }
861
862    fn proof_workspace(&self) -> String {
863        if let Some(workspace) = &self.workspace {
864            return workspace.clone();
865        }
866        std::env::var("PACKSET_WORKSPACE")
867            .ok()
868            .filter(|w| !w.is_empty())
869            .unwrap_or_else(|| "seat".to_string())
870    }
871
872    /// [`Self::roundtrip`] again while the writer answers busy, inside `timeout`.
873    fn tried(
874        &self,
875        method: &str,
876        path: &str,
877        query: &[(&str, &str)],
878        body: Option<&[u8]>,
879        timeout: Duration,
880    ) -> Result<Answer, Error> {
881        let started = Instant::now();
882        let mut wait = Duration::from_millis(50);
883        for n in 0..4 {
884            let answer = self.roundtrip(method, path, query, body, true, timeout)?;
885            if answer.status == 503 && n < 3 && started.elapsed() + wait < timeout {
886                std::thread::sleep(wait);
887                wait *= 3;
888                continue;
889            }
890            return Ok(answer);
891        }
892        Err(Error::Bad(format!(
893            "{}{path}: 503: packsetd is busy",
894            self.base
895        )))
896    }
897
898    fn json_ok<T: DeserializeOwned>(&self, path: &str, answer: Answer) -> Result<T, Error> {
899        if !(200..300).contains(&answer.status) {
900            return Err(refused(
901                &format!("{}{path}", self.base),
902                answer.status,
903                &answer.body,
904            ));
905        }
906        serde_json::from_str(&answer.body).map_err(Error::from)
907    }
908
909    pub fn health(&self) -> Result<String, Error> {
910        let answer = self.tried("GET", "/health", &[], None, budget())?;
911        if !(200..300).contains(&answer.status) {
912            return Err(refused(
913                &format!("{}/health", self.base),
914                answer.status,
915                &answer.body,
916            ));
917        }
918        Ok(answer.body)
919    }
920
921    pub fn status(&self, workspace: Option<&str>) -> Result<Value, Error> {
922        let query: Vec<(&str, &str)> = workspace
923            .map(|workspace| vec![("workspace", workspace)])
924            .unwrap_or_default();
925        let answer = self.tried("GET", "/v1/status", &query, None, budget())?;
926        self.json_ok("/v1/status", answer)
927    }
928
929    pub fn post_atom(&self, atom: &Value) -> Result<Value, Error> {
930        let bytes = serde_json::to_vec(atom)?;
931        let answer = self.tried("POST", "/v1/atoms", &[], Some(&bytes), budget())?;
932        self.json_ok("/v1/atoms", answer)
933    }
934
935    pub fn delete_atom(
936        &self,
937        workspace: &str,
938        id: &str,
939        why: Option<&str>,
940    ) -> Result<Value, Error> {
941        let mut body = serde_json::json!({"workspace": workspace, "id": id});
942        if let Some(accession) = why {
943            body["why"] = Value::String(accession.to_string());
944        }
945        let bytes = serde_json::to_vec(&body)?;
946        let answer = self.tried("POST", "/v1/atoms/delete", &[], Some(&bytes), budget())?;
947        if answer.status == 404 {
948            return Err(Error::Bad(format!("no atom {id}")));
949        }
950        self.json_ok("/v1/atoms/delete", answer)
951    }
952
953    pub fn atoms_of_kind(&self, workspace: &str, kind: &str) -> Result<Vec<Value>, Error> {
954        let answer = self.tried(
955            "GET",
956            "/v1/atoms",
957            &[("workspace", workspace), ("kind", kind)],
958            None,
959            budget(),
960        )?;
961        let body: Value = self.json_ok("/v1/atoms", answer)?;
962        Ok(body
963            .get("atoms")
964            .and_then(Value::as_array)
965            .cloned()
966            .unwrap_or_default())
967    }
968
969    pub fn atoms_in_set(&self, workspace: &str, set: &str) -> Result<Vec<Value>, Error> {
970        let answer = self.tried(
971            "GET",
972            "/v1/pack",
973            &[("workspace", workspace), ("set", set)],
974            None,
975            budget(),
976        )?;
977        let body: Value = self.json_ok("/v1/pack", answer)?;
978        Ok(body
979            .get("atoms")
980            .and_then(Value::as_array)
981            .cloned()
982            .unwrap_or_default())
983    }
984
985    pub fn sweep(&self, workspace: &str) -> Result<Value, Error> {
986        let bytes = serde_json::to_vec(&serde_json::json!({"workspace": workspace}))?;
987        let answer = self.tried("POST", "/v1/sweep", &[], Some(&bytes), budget())?;
988        self.json_ok("/v1/sweep", answer)
989    }
990
991    pub fn grade(&self, workspace: &str, id: &str, recalled: bool) -> Result<Value, Error> {
992        let bytes = serde_json::to_vec(&serde_json::json!({
993            "workspace": workspace,
994            "id": id,
995            "recalled": recalled,
996        }))?;
997        let answer = self.tried("POST", "/v1/grade", &[], Some(&bytes), budget())?;
998        self.json_ok("/v1/grade", answer)
999    }
1000
1001    pub fn activate_as(
1002        &self,
1003        workspace: &str,
1004        q: &str,
1005        limit: u32,
1006        fire: bool,
1007        lens: Option<&str>,
1008    ) -> Result<Value, Error> {
1009        let limit = limit.to_string();
1010        let fire = if fire { "1" } else { "0" };
1011        let mut query = vec![
1012            ("workspace", workspace),
1013            ("q", q),
1014            ("limit", limit.as_str()),
1015            ("fire", fire),
1016        ];
1017        if let Some(name) = lens.filter(|n| !n.trim().is_empty()) {
1018            query.push(("as", name));
1019        }
1020        let answer = self.tried("GET", "/v1/activate", &query, None, budget())?;
1021        self.json_ok("/v1/activate", answer)
1022    }
1023
1024    pub fn hubs(&self, workspace: &str, limit: usize) -> Result<Value, Error> {
1025        let limit = limit.to_string();
1026        let answer = self.tried(
1027            "GET",
1028            "/v1/hubs",
1029            &[("workspace", workspace), ("limit", limit.as_str())],
1030            None,
1031            budget(),
1032        )?;
1033        self.json_ok("/v1/hubs", answer)
1034    }
1035
1036    pub fn consolidate(&self, workspace: &str, apply: bool) -> Result<Value, Error> {
1037        let bytes =
1038            serde_json::to_vec(&serde_json::json!({"workspace": workspace, "apply": apply}))?;
1039        let answer = self.tried("POST", "/v1/consolidate", &[], Some(&bytes), budget())?;
1040        self.json_ok("/v1/consolidate", answer)
1041    }
1042
1043    pub fn search_opts(
1044        &self,
1045        workspace: &str,
1046        q: &str,
1047        limit: u32,
1048        as_of: Option<&str>,
1049        rerank: bool,
1050    ) -> Result<Vec<Hit>, Error> {
1051        Ok(self.search_fetched(workspace, q, limit, as_of, rerank)?.0)
1052    }
1053
1054    pub(crate) fn search_fetched(
1055        &self,
1056        workspace: &str,
1057        q: &str,
1058        limit: u32,
1059        as_of: Option<&str>,
1060        rerank: bool,
1061    ) -> Result<(Vec<Hit>, bool), Error> {
1062        let limit_s = limit.to_string();
1063        let mut query = vec![
1064            ("workspace", workspace),
1065            ("q", q),
1066            ("limit", limit_s.as_str()),
1067        ];
1068        if let Some(at) = as_of {
1069            query.push(("as_of", at));
1070        }
1071        if rerank {
1072            query.push(("rerank", "1"));
1073        }
1074        let timeout = if rerank {
1075            Duration::from_secs(60).max(budget())
1076        } else {
1077            budget()
1078        };
1079        let answer = self.tried("GET", "/v1/search", &query, None, timeout)?;
1080        let trusted = answer.trusted;
1081        let body: Value = self.json_ok("/v1/search", answer)?;
1082        let hits = body.get("hits").cloned().unwrap_or(Value::Array(vec![]));
1083        Ok((serde_json::from_value(hits)?, trusted))
1084    }
1085
1086    pub fn fire(&self, workspace: &str, ids: &[String]) -> Result<Value, Error> {
1087        let bytes = serde_json::to_vec(&serde_json::json!({
1088            "workspace": workspace,
1089            "ids": ids,
1090            "as": "",
1091        }))?;
1092        let answer = self.tried("POST", "/v1/fire", &[], Some(&bytes), budget())?;
1093        self.json_ok("/v1/fire", answer)
1094    }
1095}
1096
1097fn refused(url: &str, code: u16, body: &str) -> Error {
1098    let reason = serde_json::from_str::<Value>(body)
1099        .ok()
1100        .and_then(|v| v.get("error").and_then(|r| r.as_str()).map(str::to_string))
1101        .unwrap_or_else(|| body.to_string());
1102    let reason = reason.trim();
1103    if code == 401 {
1104        return Error::Bad(format!(
1105            "{url}: 401: {reason}; this seat's token does not match the writer's, \
1106             so it is another user's writer or PACKSET_HOME names another pack"
1107        ));
1108    }
1109    if reason.is_empty() {
1110        Error::Bad(format!("{url}: status code {code}"))
1111    } else {
1112        Error::Bad(format!("{url}: {code}: {reason}"))
1113    }
1114}
1115
1116fn send(live: &mut Slot, origin: &Url, call: &Wire<'_>, timeout: Duration) -> std::io::Result<Raw> {
1117    if pack_quiet() {
1118        return Err(quiet_io());
1119    }
1120    if side_budget_spent() {
1121        return Err(budget_spent());
1122    }
1123    let started = Instant::now();
1124    let result = send_once(live, origin, call, timeout);
1125    // An answer is not charged. A read that ends without one is a quiet
1126    // socket and spends the budget, so the next read does not open another.
1127    // Outside a hook the side budget is not armed. The quiet flag is that
1128    // same stop for a command, which asks the pack more than once.
1129    if let Err(err) = &result {
1130        if io_is_quiet(err) {
1131            mark_pack_quiet();
1132        }
1133        super::charge_side_wait(started.elapsed());
1134    }
1135    result
1136}
1137
1138fn send_once(
1139    live: &mut Slot,
1140    origin: &Url,
1141    call: &Wire<'_>,
1142    timeout: Duration,
1143) -> std::io::Result<Raw> {
1144    let stream = live
1145        .stream
1146        .as_mut()
1147        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotConnected, "no pack socket"))?;
1148    let wait = capped_wait(timeout);
1149    let deadline = Instant::now() + wait;
1150    stream.set_read_timeout(Some(wait))?;
1151    stream.set_write_timeout(Some(wait))?;
1152    let bytes = render(
1153        call.method,
1154        &host_header(origin),
1155        &target(call.path, call.query),
1156        call.token,
1157        call.body,
1158    );
1159    if let Err(err) = write_all(stream, &bytes) {
1160        live.stream = None;
1161        live.proven = false;
1162        return Err(err);
1163    }
1164    match read_response(stream, deadline) {
1165        Ok(raw) => {
1166            if !raw.keep {
1167                live.stream = None;
1168                live.proven = false;
1169            }
1170            Ok(raw)
1171        }
1172        Err(err) => {
1173            live.stream = None;
1174            live.proven = false;
1175            Err(err)
1176        }
1177    }
1178}