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