Skip to main content

haematite_cli/
client.rs

1//! `haematite call`: ask a running service anything its protocol offers, from the
2//! command line; `haematite write` is in [`write`] and `haematite query` in [`query`].
3//!
4//! The client says hello first and takes the service's `max_frame` as the
5//! bound on what it sends and what it reads. Params go out as the text given
6//! and a result comes back as the bytes the service sent: no number is parsed
7//! into a float and written again. Four outcomes are told apart by exit code:
8//! answered (0), could not ask, so nothing was sent (1), refused by the service
9//! (2), and outcome not known to the caller (3).
10
11use crate::serve::protocol::{HelloAgreement, PROTOCOL, RECEIPT_CAPABILITY};
12use crate::serve::receipt_fragments::{Assembler, VerifiedReceipt, minimum_cap};
13use std::io::{self, Read, Write};
14use std::num::{NonZeroU32, NonZeroU64};
15use std::os::unix::net::UnixStream;
16use std::path::{Path, PathBuf};
17use std::process::ExitCode;
18use std::sync::{Arc, mpsc};
19use std::time::Duration;
20
21use serde::{Deserialize, Deserializer, Serialize};
22use serde_json::value::RawValue;
23
24use crate::serve::{cli::Config, frame};
25
26pub mod network;
27pub mod query;
28pub mod write;
29
30pub use network::Address;
31use network::{Hangup, Stream};
32
33const USAGE: &str = "\
34usage: haematite call (--socket <path> | --config <serve.json> | --endpoint <host:port> --server-name <name>) [--pass-file <path>] [--timeout-ms N] <method> [<params> | -]
35<params> is one JSON value; - reads it from standard input; absent, the operation has no params.
36--timeout-ms bounds connecting, writing and reading; the default is 30000.
37Exit 0: answered, the result on standard output. Exit 1: could not ask; nothing was sent.
38Exit 2: refused by the service, its code and message on standard error. Exit 3: the outcome
39is not known to the caller; the service may have acted.";
40
41const DEFAULT_TIMEOUT_MS: NonZeroU64 = NonZeroU64::MIN.saturating_add(29_999);
42/// Hello's answer is read before the service has said its frame limit.
43const HELLO_BOUND: NonZeroU32 = NonZeroU32::MIN.saturating_add(65_535);
44const ID: &str = "haem-call";
45
46/// Codes which leave the mutation's outcome uncertain.
47///
48/// Unrecognised codes also retain uncertainty unless the shared refusal table
49/// proves otherwise.
50pub const MAY_HAVE_APPLIED: &[&str] = &[
51    // The commit failed after the branch reference moved: storage/mutations.rs
52    // 196 and 476 (a fork); a discard, storage/discards.rs 226, 296, 332, 418
53    // and 424; a receipt's confirmation, storage/mutations.rs 71.
54    "durability_unknown",
55    // The change is durable and the catalogue could not show it:
56    // storage/catalogue.rs 37, by publish_record after every commit.
57    "committed_unpublished",
58    // The reference vanished after its commit: storage.rs 275, the same
59    // publish_record; it is also raised before commits, which cannot be told
60    // apart from here.
61    "reference_missing",
62    // A refused discard commit whose intent could not be withdrawn:
63    // storage/discards.rs 253.
64    "discard_abort_failed",
65    // The catalogue could not hide a branch whose discard intent was
66    // installed: storage/catalogue.rs 49, called from storage/discards.rs 221
67    // and after the commit at 416, 425 and 429.
68    "catalogue_unavailable",
69    // Raised by Failure::storage everywhere, including after the change, as
70    // when the receipt cannot be serialized (serve/service.rs 123-124).
71    "storage_unavailable",
72    // The answer did not fit after the change was made: serve/service.rs 108
73    // and serve/transport.rs 177, which says to reconcile by request id.
74    "response_limit",
75];
76
77/// The service's shared proof that a refusal applied no mutation.
78pub use crate::serve::protocol::NOT_APPLIED_REFUSALS as KNOWN_REFUSALS;
79
80/// Why an exchange gave neither an answer nor a refusal.
81#[derive(Debug)]
82pub enum Ended {
83    /// The command line was not one the command takes; the usage text follows.
84    Usage(String),
85    /// Nothing the service could act on was sent.
86    NotAsked(String),
87    /// The request was sent, or may have been; whether the service acted on
88    /// it is not known.
89    Unknown(String),
90    /// The service sent a complete, verified receipt, and whether the change
91    /// was applied is still not known: the receipt carried no refusal, or its
92    /// code is one that may have applied or that this client does not know.
93    /// The receipt is kept as the exact bytes sent, to reconcile by.
94    Received {
95        why: String,
96        receipt: Arc<VerifiedReceipt>,
97    },
98}
99
100impl Ended {
101    /// The cause, without the words for which kind of end it was.
102    #[must_use]
103    pub fn why(&self) -> &str {
104        match self {
105            Self::Usage(why)
106            | Self::NotAsked(why)
107            | Self::Unknown(why)
108            | Self::Received { why, .. } => why,
109        }
110    }
111}
112
113impl std::fmt::Display for Ended {
114    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        let kind = match self {
116            Self::Usage(_) => "usage",
117            Self::NotAsked(_) => "not asked",
118            Self::Unknown(_) => "outcome unknown",
119            Self::Received { .. } => "complete receipt received",
120        };
121        write!(formatter, "{kind}: {}", self.why())
122    }
123}
124
125impl std::error::Error for Ended {}
126
127/// What the service said to one request.
128#[derive(Debug)]
129pub enum Reply {
130    /// The bytes the service sent for `result`.
131    Answered(Box<RawValue>),
132    Refused(Refusal),
133}
134
135/// A refusal by name, as the service sent it.
136#[derive(Debug, Deserialize)]
137pub struct Refusal {
138    pub code: String,
139    pub message: String,
140    #[serde(default)]
141    pub path: Option<String>,
142    #[serde(skip)]
143    pub verified: Option<Arc<VerifiedReceipt>>,
144}
145
146/// One connection to a running service, past its hello.
147pub struct Client {
148    stream: Stream,
149    hello: Box<RawValue>,
150    max_frame: NonZeroU32,
151    max_page: NonZeroU32,
152}
153
154#[derive(Deserialize)]
155struct Limits {
156    protocol: String,
157    capabilities: Vec<String>,
158    max_frame: NonZeroU32,
159    max_page: NonZeroU32,
160}
161
162impl Client {
163    /// Connect to `socket` and say hello, each step bounded by `timeout`.
164    ///
165    /// # Errors
166    ///
167    /// `Ended::NotAsked` naming the cause: hello changes nothing, so any
168    /// failure up to its answer leaves nothing asked.
169    pub fn connect(socket: &Path, timeout: Duration) -> Result<Self, Ended> {
170        Self::over(Stream::Unix(connect(socket, timeout)?), timeout)
171    }
172
173    /// Reach the store at `address`, its socket or its network endpoint
174    /// over verified TLS (HAEMNET-003 R1), and say hello, each step bounded
175    /// by `timeout`.
176    ///
177    /// # Errors
178    ///
179    /// `Ended::NotAsked` naming the cause, as [`Client::connect`]: a network
180    /// store not reached, or not over verified TLS, was asked nothing.
181    pub fn reach(address: &Address, timeout: Duration) -> Result<Self, Ended> {
182        match address {
183            Address::Socket(socket) => Self::connect(socket, timeout),
184            Address::Network(remote) => Self::over(
185                network::open(remote, timeout)
186                    .map_err(|refused| Ended::NotAsked(refused.why().to_owned()))?,
187                timeout,
188            ),
189        }
190    }
191
192    /// Say hello over `stream`, each step bounded by `timeout`.
193    fn over(mut stream: Stream, timeout: Duration) -> Result<Self, Ended> {
194        let unset =
195            |error: io::Error| Ended::NotAsked(format!("the timeout could not be set: {error}"));
196        stream.set_read_timeout(Some(timeout)).map_err(unset)?;
197        stream.set_write_timeout(Some(timeout)).map_err(unset)?;
198        let agreement = serde_json::value::to_raw_value(&HelloAgreement::client())
199            .map_err(|error| Ended::NotAsked(format!("receipt_hello_encode_failed: {error}")))?;
200        let hello = match exchange(&mut stream, HELLO_BOUND, "hello", Some(&agreement)) {
201            Ok(Reply::Answered(hello)) => hello,
202            Ok(Reply::Refused(refusal)) => {
203                return Err(Ended::NotAsked(format!(
204                    "the service refused hello: {}: {}",
205                    refusal.code, refusal.message
206                )));
207            }
208            Err(Ended::Received { why, receipt }) => {
209                return Err(Ended::Received {
210                    why: format!("hello failed: {why}"),
211                    receipt,
212                });
213            }
214            Err(Ended::Unknown(why) | Ended::NotAsked(why) | Ended::Usage(why)) => {
215                return Err(Ended::NotAsked(format!("hello failed: {why}")));
216            }
217        };
218        let limits: Limits = serde_json::from_str(hello.get()).map_err(|error| {
219            Ended::NotAsked(format!(
220                "the service's hello did not give its limits: {error}"
221            ))
222        })?;
223        if limits.protocol != PROTOCOL {
224            return Err(Ended::NotAsked(format!(
225                "receipt_protocol_mismatch: expected {PROTOCOL}, received {}",
226                limits.protocol
227            )));
228        }
229        if !limits
230            .capabilities
231            .iter()
232            .any(|capability| capability == RECEIPT_CAPABILITY)
233        {
234            return Err(Ended::NotAsked(format!(
235                "typed_receipt_capability_missing: required {RECEIPT_CAPABILITY}"
236            )));
237        }
238        let minimum = minimum_cap().map_err(|error| Ended::NotAsked(error.to_string()))?;
239        if u64::from(limits.max_frame.get())
240            < u64::try_from(minimum).map_err(|error| Ended::NotAsked(error.to_string()))?
241        {
242            return Err(Ended::NotAsked(format!(
243                "receipt_frame_too_small: offered {}, minimum {minimum}",
244                limits.max_frame
245            )));
246        }
247        Ok(Self {
248            stream,
249            hello,
250            max_frame: limits.max_frame,
251            max_page: limits.max_page,
252        })
253    }
254
255    /// The bytes the service sent for hello's `result`.
256    #[must_use]
257    pub fn hello(&self) -> &RawValue {
258        &self.hello
259    }
260
261    /// The largest frame the service reads or sends.
262    #[must_use]
263    pub const fn max_frame(&self) -> NonZeroU32 {
264        self.max_frame
265    }
266
267    /// The most rows the service puts on one page.
268    #[must_use]
269    pub const fn max_page(&self) -> NonZeroU32 {
270        self.max_page
271    }
272
273    /// Send one request and read its reply.
274    ///
275    /// # Errors
276    ///
277    /// `Ended::NotAsked` for a request larger than the service reads, refused
278    /// before sending; `Ended::Unknown` for any failure from sending onward,
279    /// and for a `mutation.apply` error that is not one of
280    /// [`KNOWN_REFUSALS`]: those the service sends when the change may already
281    /// have been applied ([`MAY_HAVE_APPLIED`]), and any code this client does
282    /// not know.
283    pub fn call(&mut self, method: &str, params: Option<&RawValue>) -> Result<Reply, Ended> {
284        let reply = exchange(&mut self.stream, self.max_frame, method, params)?;
285        let Reply::Refused(refusal) = reply else {
286            return Ok(reply);
287        };
288        // A read changes nothing, so a refusal of a read is a refusal.
289        if method != "mutation.apply" || KNOWN_REFUSALS.contains(&refusal.code.as_str()) {
290            return Ok(Reply::Refused(refusal));
291        }
292        let why = if MAY_HAVE_APPLIED.contains(&refusal.code.as_str()) {
293            "the change may have been applied"
294        } else {
295            "this client does not know that code, so whether the change was applied is not known"
296        };
297        let why = format!(
298            "the service answered {}: {}; {why}",
299            refusal.code, refusal.message
300        );
301        match refusal.verified {
302            Some(receipt) => Err(Ended::Received { why, receipt }),
303            None => Err(Ended::Unknown(why)),
304        }
305    }
306
307    /// Read the next frame of an answer that is a stream, such as
308    /// `head.follow`'s, sent under the same id as its first. A read changes
309    /// nothing, so a refusal is a refusal.
310    ///
311    /// # Errors
312    ///
313    /// `Ended::Unknown` when the frame could not be read: the connection
314    /// closed, failed or timed out.
315    pub fn next_reply(&mut self) -> Result<Reply, Ended> {
316        receive(&mut self.stream, self.max_frame)
317    }
318
319    /// An owned handle used only to cancel a disconnected browser's follow.
320    pub(crate) fn follow_socket(&self) -> io::Result<Hangup> {
321        self.stream.hangup()
322    }
323
324    /// Bound each later read by `timeout`, or by nothing with `None`.
325    ///
326    /// # Errors
327    ///
328    /// `Ended::Unknown` when the bound could not be set.
329    pub fn read_within(&self, timeout: Option<Duration>) -> Result<(), Ended> {
330        self.stream
331            .set_read_timeout(timeout)
332            .map_err(|error| Ended::Unknown(format!("the read bound could not be set: {error}")))
333    }
334}
335
336/// What asking a network store hello came to (HAEMNET-003 R1).
337pub enum Reached {
338    /// Reached over verified TLS, and the store answered hello.
339    Answered,
340    /// Reached over verified TLS; hello was not answered, for the reason.
341    Unanswered(String),
342    /// Not reached over verified TLS.
343    Refused(network::Refused),
344}
345
346/// Reach `remote` over verified TLS and ask it hello, which changes
347/// nothing, each step bounded by `timeout`.
348#[must_use]
349pub fn probe(remote: &network::Remote, timeout: Duration) -> Reached {
350    match network::open(remote, timeout) {
351        Err(refused) => Reached::Refused(refused),
352        Ok(stream) => match Client::over(stream, timeout) {
353            Ok(_) => Reached::Answered,
354            Err(ended) => Reached::Unanswered(ended.why().to_owned()),
355        },
356    }
357}
358
359/// `haematite call`, ending in its exit code.
360#[must_use]
361pub fn run(args: impl Iterator<Item = String>) -> ExitCode {
362    finish(call(args))
363}
364
365/// Print what an exchange came to and give its exit code: 0 answered, 1 could
366/// not ask, 2 refused, 3 outcome not known to the caller.
367fn finish(outcome: Result<Reply, Ended>) -> ExitCode {
368    match outcome {
369        Ok(Reply::Answered(result)) => {
370            let mut output = io::stdout().lock();
371            match writeln!(output, "{}", result.get()).and_then(|()| output.flush()) {
372                Ok(()) => ExitCode::SUCCESS,
373                Err(error) => {
374                    // The service acted and the caller did not get its answer.
375                    eprintln!(
376                        "haematite: answer received, but the result could not be written to stdout: {error}"
377                    );
378                    ExitCode::from(3)
379                }
380            }
381        }
382        Ok(Reply::Refused(refusal)) => {
383            if let Err(error) = print_refusal(&refusal) {
384                eprintln!("haematite: receipt_output_failed: {error}");
385            }
386            ExitCode::from(2)
387        }
388        Err(ended) => {
389            if let Err(error) = print_ended(&ended) {
390                eprintln!("haematite: receipt_output_failed: {error}");
391            }
392            ExitCode::from(exit_code(&ended))
393        }
394    }
395}
396
397/// 1 when nothing was sent, so asking again is safe; 3 when it may have been.
398#[must_use]
399pub const fn exit_code(ended: &Ended) -> u8 {
400    match ended {
401        Ended::Usage(_) | Ended::NotAsked(_) => 1,
402        Ended::Unknown(_) | Ended::Received { .. } => 3,
403    }
404}
405
406/// The standard error line for a refusal.
407#[must_use]
408pub fn refused(refusal: &Refusal) -> String {
409    let at = refusal
410        .path
411        .as_deref()
412        .map_or_else(String::new, |path| format!(" at {path}"));
413    let (code, message) = (&refusal.code, &refusal.message);
414    format!("haematite: refused {code}: {message}{at}")
415}
416
417/// The standard error line for an exchange that gave no reply.
418#[must_use]
419pub fn words(ended: &Ended) -> String {
420    match ended {
421        Ended::Usage(why) => format!("haematite: {why}"),
422        Ended::NotAsked(why) => format!("haematite: could not ask: {why}"),
423        Ended::Unknown(why) => format!("haematite: the outcome is not known: {why}"),
424        Ended::Received { why, .. } => format!("haematite: complete receipt received: {why}"),
425    }
426}
427
428pub(super) fn print_refusal(refusal: &Refusal) -> io::Result<()> {
429    let mut output = io::stderr().lock();
430    writeln!(output, "{}", refused(refusal))?;
431    if let Some(receipt) = &refusal.verified {
432        output.write_all(receipt.bytes())?;
433        output.write_all(b"\n")?;
434    }
435    output.flush()
436}
437pub(super) fn print_ended(ended: &Ended) -> io::Result<()> {
438    let mut output = io::stderr().lock();
439    writeln!(output, "{}", words(ended))?;
440    if let Ended::Received { receipt, .. } = ended {
441        output.write_all(receipt.bytes())?;
442        output.write_all(b"\n")?;
443    }
444    output.flush()
445}
446
447enum Place {
448    Socket(PathBuf),
449    Config(PathBuf),
450    /// A store's HAEMNET-001 network endpoint, over TLS verified against
451    /// this machine's roots, as the name its certificate carries
452    /// (HAEMNET-004 R1).
453    Endpoint {
454        endpoint: String,
455        server_name: String,
456    },
457}
458
459/// Where a client command's store is, and the file holding the pass it
460/// presents there: required on the network, which admits only a Lys pass
461/// (HAEMNET-001 R2), and presented on the socket when given.
462struct Target {
463    place: Place,
464    pass: Option<PathBuf>,
465}
466
467impl Target {
468    /// Reach the store and say hello, then present the pass, when there is
469    /// one, with `session.pass`.
470    fn open(&self, timeout: Duration) -> Result<Client, Ended> {
471        let address = match &self.place {
472            Place::Socket(socket) => network::Address::Socket(socket.clone()),
473            Place::Config(_) => network::Address::Socket(socket_of(&self.place)?),
474            Place::Endpoint {
475                endpoint,
476                server_name,
477            } => {
478                let trust = network::Trust::system().map_err(Ended::NotAsked)?;
479                network::Address::Network(
480                    network::Remote::new(endpoint, server_name, &trust).map_err(Ended::NotAsked)?,
481                )
482            }
483        };
484        let mut client = Client::reach(&address, timeout)?;
485        if let Some(file) = &self.pass {
486            let named = file.display();
487            let pass = std::fs::read_to_string(file).map_err(|error| {
488                Ended::NotAsked(format!("--pass-file {named}: {}", error.kind()))
489            })?;
490            let pass = pass.trim();
491            if pass.is_empty() {
492                return Err(Ended::NotAsked(format!("--pass-file {named}: empty")));
493            }
494            let params = serde_json::value::to_raw_value(&serde_json::json!({ "pass": pass }))
495                .map_err(|_| Ended::NotAsked("the pass could not be written".to_owned()))?;
496            if let Reply::Refused(refusal) = client.call("session.pass", Some(&params))? {
497                return Err(Ended::NotAsked(format!(
498                    "the store refused the pass: {}: {}",
499                    refusal.code, refusal.message
500                )));
501            }
502        }
503        Ok(client)
504    }
505}
506
507const ONE_PLACE: &str = "give exactly one of --socket, --config and --endpoint";
508
509/// The flags every client command takes: where the service is, the pass it
510/// is given, and how long to wait for it.
511#[derive(Default)]
512struct Common {
513    place: Option<Place>,
514    endpoint: Option<String>,
515    server_name: Option<String>,
516    pass: Option<PathBuf>,
517    timeout: Option<NonZeroU64>,
518}
519
520impl Common {
521    /// Take `arg`, and its value from `args`, when it is a shared flag.
522    fn take(&mut self, arg: &str, args: &mut impl Iterator<Item = String>) -> Result<bool, String> {
523        match arg {
524            "--socket" | "--config" if self.place.is_none() => {
525                let path = PathBuf::from(args.next().ok_or_else(|| format!("{arg} needs a path"))?);
526                self.place = Some(if arg == "--socket" {
527                    Place::Socket(path)
528                } else {
529                    Place::Config(path)
530                });
531                Ok(true)
532            }
533            "--endpoint" if self.endpoint.is_none() => {
534                self.endpoint = Some(args.next().ok_or("--endpoint needs <host>:<port>")?);
535                Ok(true)
536            }
537            "--server-name" if self.server_name.is_none() => {
538                self.server_name = Some(args.next().ok_or("--server-name needs a name")?);
539                Ok(true)
540            }
541            "--pass-file" if self.pass.is_none() => {
542                self.pass = Some(PathBuf::from(
543                    args.next().ok_or("--pass-file needs a path")?,
544                ));
545                Ok(true)
546            }
547            "--socket" | "--config" => Err(ONE_PLACE.to_owned()),
548            "--timeout-ms" if self.timeout.is_none() => {
549                let text = args
550                    .next()
551                    .ok_or("--timeout-ms needs a number of milliseconds")?;
552                let millis = text
553                    .parse::<NonZeroU64>()
554                    .map_err(|error| format!("--timeout-ms {text:?}: {error}"))?;
555                self.timeout = Some(millis);
556                Ok(true)
557            }
558            _ => Ok(false),
559        }
560    }
561
562    /// Where the service is and how long to wait, once every flag is read.
563    fn finish(self) -> Result<(Target, Duration), String> {
564        let place = match (self.place, self.endpoint, self.server_name) {
565            (Some(place), None, None) => place,
566            (None, Some(endpoint), Some(server_name)) => {
567                if self.pass.is_none() {
568                    return Err(
569                        "--endpoint needs --pass-file: the network admits only a pass".to_owned(),
570                    );
571                }
572                Place::Endpoint {
573                    endpoint,
574                    server_name,
575                }
576            }
577            (None, Some(_), None) => return Err("--endpoint needs --server-name".to_owned()),
578            (_, None, Some(_)) => {
579                return Err("--server-name is given only with --endpoint".to_owned());
580            }
581            (None, None, None) | (Some(_), Some(_), _) => return Err(ONE_PLACE.to_owned()),
582        };
583        let millis = self.timeout.unwrap_or(DEFAULT_TIMEOUT_MS);
584        Ok((
585            Target {
586                place,
587                pass: self.pass,
588            },
589            Duration::from_millis(millis.get()),
590        ))
591    }
592}
593
594struct Call {
595    target: Target,
596    timeout: Duration,
597    method: String,
598    /// `None` when the operation has no params.
599    params: Option<Text>,
600}
601
602/// One JSON value as given on the command line, or `-` for standard input.
603enum Text {
604    Given(String),
605    Stdin,
606}
607
608impl Text {
609    fn of(text: String) -> Self {
610        if text == "-" {
611            Self::Stdin
612        } else {
613            Self::Given(text)
614        }
615    }
616}
617
618fn call(args: impl Iterator<Item = String>) -> Result<Reply, Ended> {
619    let asked = parse(args).map_err(|why| Ended::Usage(format!("{why}\n{USAGE}")))?;
620    let params = asked
621        .params
622        .map(|text| one_value(text, "params"))
623        .transpose()?;
624    let mut client = asked.target.open(asked.timeout)?;
625    if asked.method == "hello" && params.is_none() {
626        return Ok(Reply::Answered(client.hello().to_owned()));
627    }
628    client.call(&asked.method, params.as_deref())
629}
630
631fn parse(mut args: impl Iterator<Item = String>) -> Result<Call, String> {
632    let mut common = Common::default();
633    let mut rest = Vec::new();
634    while let Some(arg) = args.next() {
635        if common.take(&arg, &mut args)? {
636            continue;
637        }
638        if arg.starts_with("--") {
639            return Err(format!("unknown or repeated flag {arg:?}"));
640        }
641        rest.push(arg);
642    }
643    let (target, timeout) = common.finish()?;
644    let mut rest = rest.into_iter();
645    let method = rest.next().ok_or("name the method to call")?;
646    let params = rest.next().map(Text::of);
647    if let Some(extra) = rest.next() {
648        return Err(format!("unexpected argument {extra:?}"));
649    }
650    Ok(Call {
651        target,
652        timeout,
653        method,
654        params,
655    })
656}
657
658/// `given` as one JSON value, kept as the text given; `what` names it.
659fn one_value(given: Text, what: &str) -> Result<Box<RawValue>, Ended> {
660    let text = match given {
661        Text::Given(text) => text,
662        Text::Stdin => {
663            let mut text = String::new();
664            io::stdin().read_to_string(&mut text).map_err(|error| {
665                Ended::NotAsked(format!(
666                    "the {what} could not be read from standard input: {error}"
667                ))
668            })?;
669            text
670        }
671    };
672    serde_json::from_str(&text)
673        .map_err(|error| Ended::NotAsked(format!("the {what} is not one JSON value: {error}")))
674}
675
676/// The socket named, or the one a service config names, read by the
677/// service's own config parser.
678fn socket_of(place: &Place) -> Result<PathBuf, Ended> {
679    match place {
680        Place::Socket(path) => Ok(path.clone()),
681        Place::Endpoint { endpoint, .. } => Err(Ended::NotAsked(format!(
682            "{endpoint} is a network endpoint, not a socket"
683        ))),
684        Place::Config(path) => {
685            let named = path.display();
686            let bytes = std::fs::read(path).map_err(|error| {
687                Ended::NotAsked(format!(
688                    "the service config {named} could not be read: {error}"
689                ))
690            })?;
691            let config: Config = serde_json::from_slice(&bytes)
692                .map_err(|error| Ended::NotAsked(format!("the service config {named}: {error}")))?;
693            config
694                .endpoint(false)
695                .map(|endpoint| endpoint.socket)
696                .map_err(|error| Ended::NotAsked(format!("the service config {named}: {error}")))
697        }
698    }
699}
700
701/// `UnixStream` has no connect timeout, so the connect runs on its own thread
702/// and is waited for at most `timeout`.
703fn connect(socket: &Path, timeout: Duration) -> Result<UnixStream, Ended> {
704    let (sender, receiver) = mpsc::channel();
705    let path = socket.to_path_buf();
706    std::thread::Builder::new()
707        .name("haem-call-connect".to_owned())
708        .spawn(move || {
709            // The receiver is gone only when the connect outlived the timeout.
710            let _unwaited = sender.send(UnixStream::connect(path));
711        })
712        .map_err(|error| Ended::NotAsked(format!("the connect could not start: {error}")))?;
713    match receiver.recv_timeout(timeout) {
714        Ok(Ok(stream)) => Ok(stream),
715        Ok(Err(error)) => Err(Ended::NotAsked(format!(
716            "could not connect to {}: {error}",
717            socket.display()
718        ))),
719        Err(_) => Err(Ended::NotAsked(format!(
720            "could not connect to {} within {} ms",
721            socket.display(),
722            timeout.as_millis()
723        ))),
724    }
725}
726
727#[derive(Serialize)]
728struct Outgoing<'a> {
729    id: &'a str,
730    operation: Operation<'a>,
731}
732
733#[derive(Serialize)]
734struct Operation<'a> {
735    method: &'a str,
736    #[serde(skip_serializing_if = "Option::is_none")]
737    params: Option<&'a RawValue>,
738}
739
740#[derive(Deserialize)]
741struct Incoming {
742    id: String,
743    /// Present-and-null is an answer of null, so `null` is kept as its bytes.
744    #[serde(default, deserialize_with = "present")]
745    result: Option<Box<RawValue>>,
746    #[serde(default)]
747    error: Option<Refusal>,
748}
749
750fn present<'de, D: Deserializer<'de>>(input: D) -> Result<Option<Box<RawValue>>, D::Error> {
751    Box::<RawValue>::deserialize(input).map(Some)
752}
753
754/// The first answer frame read only for whether it opens a sealed receipt.
755#[derive(Deserialize)]
756struct Probe {
757    #[serde(default)]
758    receipt: Option<Box<RawValue>>,
759}
760
761fn exchange(
762    stream: &mut (impl Read + Write),
763    limit: NonZeroU32,
764    method: &str,
765    params: Option<&RawValue>,
766) -> Result<Reply, Ended> {
767    let operation = Operation { method, params };
768    let request = serde_json::to_vec(&Outgoing { id: ID, operation })
769        .map_err(|error| Ended::NotAsked(format!("the request could not be written: {error}")))?;
770    if !u32::try_from(request.len()).is_ok_and(|length| length <= limit.get()) {
771        return Err(Ended::NotAsked(format!(
772            "request_too_large: the request is {} bytes and the service reads at most {limit}",
773            request.len()
774        )));
775    }
776    frame::write(stream, &request, limit).map_err(|error| lost("sending the request", &error))?;
777    receive(stream, limit)
778}
779
780/// Read one answer frame, and the rest of a sealed receipt it opens.
781fn receive(stream: &mut impl Read, limit: NonZeroU32) -> Result<Reply, Ended> {
782    let bytes = match frame::read(stream, limit) {
783        Ok(Some(bytes)) => bytes,
784        Ok(None) => {
785            return Err(Ended::Unknown(
786                "the service closed the connection before answering".to_owned(),
787            ));
788        }
789        Err(error) => return Err(lost("waiting for the answer", &error)),
790    };
791    let probe: Probe = serde_json::from_slice(&bytes)
792        .map_err(|error| Ended::Unknown(format!("receipt_frame_decode_failed: {error}")))?;
793    if probe.receipt.is_none() {
794        return reply(&bytes);
795    }
796    let mut assembler =
797        Assembler::new(ID, limit.get()).map_err(|error| Ended::Unknown(error.to_string()))?;
798    let mut body = bytes;
799    loop {
800        if let Some(complete) = assembler
801            .accept(&body)
802            .map_err(|error| Ended::Unknown(error.to_string()))?
803        {
804            let complete = Arc::new(complete);
805            let refusal = complete.refusal().ok_or_else(|| Ended::Received {
806                why: "receipt_refusal_missing".to_owned(),
807                receipt: Arc::clone(&complete),
808            })?;
809            return Ok(Reply::Refused(Refusal {
810                code: refusal.code.clone(),
811                message: refusal.message.clone(),
812                path: refusal.path.clone(),
813                verified: Some(complete),
814            }));
815        }
816        body = match frame::read(stream, limit) {
817            Ok(Some(body)) => body,
818            Ok(None) => return Err(Ended::Unknown("receipt_connection_closed".to_owned())),
819            Err(error) => {
820                return Err(Ended::Unknown(format!(
821                    "receipt_connection_failed: {error}"
822                )));
823            }
824        };
825    }
826}
827
828fn lost(doing: &str, error: &io::Error) -> Ended {
829    match error.kind() {
830        io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut => {
831            Ended::Unknown(format!("timed out {doing}: {error}"))
832        }
833        _ => Ended::Unknown(format!("the connection failed {doing}: {error}")),
834    }
835}
836
837fn reply(bytes: &[u8]) -> Result<Reply, Ended> {
838    let unreadable = |why: String| Ended::Unknown(format!("the answer could not be read: {why}"));
839    let incoming: Incoming =
840        serde_json::from_slice(bytes).map_err(|error| unreadable(error.to_string()))?;
841    if incoming.id != ID {
842        return Err(unreadable(format!(
843            "it answers {:?}, not {ID:?}",
844            incoming.id
845        )));
846    }
847    match (incoming.result, incoming.error) {
848        (Some(result), None) => Ok(Reply::Answered(result)),
849        (None, Some(refusal)) => Ok(Reply::Refused(refusal)),
850        _ => Err(unreadable(
851            "it carries neither or both of result and error".to_owned(),
852        )),
853    }
854}
855
856#[cfg(test)]
857mod nonzero_tests {
858    use super::{Common, DEFAULT_TIMEOUT_MS, NonZeroU64, PathBuf, Place};
859
860    #[test]
861    fn nonzero_timeout_readback() -> Result<(), Box<dyn std::error::Error>> {
862        let cases = [
863            (None, DEFAULT_TIMEOUT_MS.get()),
864            (Some(1), 1),
865            (Some(37), 37),
866            (Some(u64::MAX), u64::MAX),
867        ];
868        for (given, expected) in cases {
869            let timeout = given
870                .map(|value| NonZeroU64::new(value).ok_or("zero timeout"))
871                .transpose()?;
872            let common = Common {
873                place: Some(Place::Socket(PathBuf::from("unused.sock"))),
874                timeout: std::hint::black_box(timeout),
875                ..Common::default()
876            };
877            let (_, actual) = common.finish()?;
878            assert_eq!(actual.as_millis(), u128::from(expected));
879        }
880        Ok(())
881    }
882}
883
884#[cfg(test)]
885mod json_text_refusal_tests {
886    use super::{KNOWN_REFUSALS, Refusal, Reply, finish, refused};
887    use std::process::ExitCode;
888
889    #[test]
890    fn json_text_invalid_is_a_named_client_refusal() {
891        let refusal = Refusal {
892            code: "json_text_invalid".to_owned(),
893            message: "value is not JSON".to_owned(),
894            path: Some("/payload".to_owned()),
895            verified: None,
896        };
897        assert!(KNOWN_REFUSALS.contains(&refusal.code.as_str()));
898        assert_eq!(
899            refused(&refusal),
900            "haematite: refused json_text_invalid: value is not JSON at /payload"
901        );
902        assert_eq!(finish(Ok(Reply::Refused(refusal))), ExitCode::from(2));
903    }
904}
905
906#[cfg(test)]
907mod access_refusal_tests {
908    use super::KNOWN_REFUSALS;
909
910    /// A change a Lys-served service refused before it ran is a refusal, not
911    /// an unknown outcome: the client says it was not applied.
912    #[test]
913    fn every_access_refusal_is_known_not_to_have_applied() {
914        for code in [
915            "route_unclassified",
916            "permission_refused",
917            "held_for_approval",
918            "pass_missing",
919            "lys_could_not_be_asked",
920            "signature_refused",
921            "pass_expired",
922        ] {
923            assert!(KNOWN_REFUSALS.contains(&code), "{code} is not known");
924        }
925    }
926}