Skip to main content

urma_node/
service.rs

1//! Small authenticated loopback control surface. The archive engine remains
2//! usable directly; HTTP is not a prerequisite for embedded applications.
3use crate::{
4    Error,
5    engine::Engine,
6    model::{CacheReceipt, ServiceStatus},
7    peer::SyncPeers,
8    peer_discovery::Discovery,
9    peer_sync::Scheduler,
10    recorded::{RecordedError, read},
11    retrieval::RecordSource,
12    source::{CommitSource, CommitSources},
13};
14
15mod overlay;
16mod worker;
17use rand::RngCore;
18use serde_json::json;
19use std::{
20    fs::OpenOptions,
21    io::{Read, Write},
22    net::{SocketAddr, TcpListener, TcpStream},
23    path::{Path, PathBuf},
24    sync::{
25        Arc, Mutex,
26        atomic::{AtomicBool, Ordering},
27    },
28    time::Duration,
29};
30
31#[derive(Default, Clone)]
32struct CacheStatus {
33    receipt: Option<CacheReceipt>,
34    error: Option<RecordedError>,
35}
36
37type ErrorSlot = Arc<Mutex<Option<RecordedError>>>;
38
39/// Cooperative stop request shared with the owner of a running service.
40/// Requesting shutdown does not wait for completion or interrupt entered I/O
41/// or SQL. The owner must wait for `Service::run` to return before reopening
42/// its archive. Embedded services do not install process signal handlers.
43#[derive(Clone)]
44pub struct ShutdownHandle {
45    stop: Arc<AtomicBool>,
46}
47
48impl ShutdownHandle {
49    pub fn request(&self) {
50        self.stop.store(true, Ordering::Release);
51    }
52}
53
54pub struct Service {
55    listener: TcpListener,
56    token: String,
57    engine: Arc<Engine>,
58    stop: Arc<AtomicBool>,
59    sync_error: ErrorSlot,
60    names_error: ErrorSlot,
61    retention_error: ErrorSlot,
62    cache_status: Arc<Mutex<CacheStatus>>,
63    cache_bytes: u64,
64    retention: Option<(PathBuf, Duration)>,
65    sources: Vec<Arc<dyn RecordSource + Send + Sync>>,
66    record_peers: Option<SyncPeers>,
67    commit_sources: Option<CommitSources>,
68    dependency_error: ErrorSlot,
69    peer_sync: Option<Scheduler>,
70    inventory_error: ErrorSlot,
71    discovery: Option<Discovery>,
72    discovery_error: ErrorSlot,
73}
74
75impl Service {
76    const CAPABILITIES: [&str; 9] = [
77        "status",
78        "header-sync",
79        "full-block-scan",
80        "record-read",
81        "object-status",
82        "object-pin",
83        "cache-prune",
84        "wire-index",
85        "names-index",
86    ];
87
88    pub fn bind(address: SocketAddr, token: String, engine: Arc<Engine>) -> Result<Self, Error> {
89        if !address.ip().is_loopback()
90            || token.len() != 64
91            || !token.bytes().all(|c| c.is_ascii_hexdigit())
92        {
93            return Err(Error::Invalid(
94                "service requires loopback and a 32-byte hexadecimal bearer token".into(),
95            ));
96        }
97        let listener = TcpListener::bind(address)?;
98        listener.set_nonblocking(true)?;
99        Ok(Self {
100            listener,
101            token,
102            engine,
103            stop: Arc::new(AtomicBool::new(false)),
104            sync_error: Arc::new(Mutex::new(None)),
105            names_error: Arc::new(Mutex::new(None)),
106            retention_error: Arc::new(Mutex::new(None)),
107            cache_status: Arc::new(Mutex::new(CacheStatus::default())),
108            cache_bytes: crate::cache::DEFAULT_CACHE_BYTES,
109            retention: None,
110            sources: Vec::new(),
111            record_peers: None,
112            commit_sources: None,
113            dependency_error: Arc::new(Mutex::new(None)),
114            peer_sync: None,
115            inventory_error: Arc::new(Mutex::new(None)),
116            discovery: None,
117            discovery_error: Arc::new(Mutex::new(None)),
118        })
119    }
120
121    pub fn address(&self) -> Result<SocketAddr, Error> {
122        Ok(self.listener.local_addr()?)
123    }
124
125    pub fn shutdown_handle(&self) -> ShutdownHandle {
126        ShutdownHandle {
127            stop: Arc::clone(&self.stop),
128        }
129    }
130
131    /// Explicit optional candidates for background object recovery. Merely
132    /// configuring handles does not dial, fetch, change headers or scan coverage.
133    pub fn with_record_sources(
134        mut self,
135        sources: Vec<Arc<dyn RecordSource + Send + Sync>>,
136    ) -> Result<Self, Error> {
137        if sources.len() > 1024 {
138            return Err(Error::Invalid("too many configured record sources".into()));
139        }
140        self.sources = sources;
141        Ok(self)
142    }
143
144    pub fn with_commit_sources(mut self, sources: CommitSources) -> Result<Self, Error> {
145        if sources.is_empty() {
146            return Err(Error::Invalid("commit source set is empty".into()));
147        }
148        let expected = self.engine.status()?.chain_genesis;
149        let mut chains = sources.source_chains();
150        chains.push(sources.chain());
151        for chain in chains {
152            let genesis = chain
153                .genesis()
154                .map_err(|error| Error::Invalid(error.to_string()))?
155                .0;
156            if genesis != expected {
157                return Err(Error::Invalid(
158                    "commit source belongs to another chain".into(),
159                ));
160            }
161        }
162        self.commit_sources = Some(sources);
163        Ok(self)
164    }
165
166    /// Operational soft target, not persisted archive identity or a disk quota.
167    /// Only run(true) schedules enforcement; run(false) remains worker-free.
168    /// Zero requests eventual drainage of unprotected cache over bounded steps.
169    pub fn with_cache_limit(mut self, bytes: u64) -> Result<Self, Error> {
170        if bytes > i64::MAX as u64 {
171            return Err(Error::Invalid("cache limit exceeds storage range".into()));
172        }
173        self.cache_bytes = bytes;
174        Ok(self)
175    }
176
177    /// Configure background acquisition of every object, bounded only by the
178    /// protocol geometry. Each round spends at most `slice` on one object; an
179    /// unfinished object resumes from its stored records in a later round. The
180    /// caller supplies an owned scratch directory. Only run(true) schedules it;
181    /// pruned archives perform no automatic content acquisition.
182    pub fn with_retention(mut self, scratch: PathBuf, slice: Duration) -> Result<Self, Error> {
183        if slice.is_zero() {
184            return Err(Error::Invalid(
185                "object retention slice must be nonzero".into(),
186            ));
187        }
188        self.retention = Some((scratch, slice));
189        Ok(self)
190    }
191
192    /// Run foreground. Network acquisition is opt-in; status works offline.
193    /// run(true) adds the sync and maintenance threads (see `worker`); stop
194    /// joins both before this returns.
195    pub fn run(self, synchronize: bool) -> Result<(), Error> {
196        std::thread::scope(|scope| {
197            if synchronize {
198                scope.spawn(|| self.sync_loop());
199                scope.spawn(|| self.maintenance_loop());
200            }
201            let outcome = self.accept_loop();
202            self.stop.store(true, Ordering::Release);
203            outcome
204        })
205    }
206
207    fn status(&self) -> Result<ServiceStatus, Error> {
208        let (last_sync_error, last_sync_error_at) =
209            RecordedError::split(read(&self.sync_error, "sync")?);
210        let (last_retention_error, last_retention_error_at) =
211            RecordedError::split(read(&self.retention_error, "retention")?);
212        let (last_names_error, last_names_error_at) =
213            RecordedError::split(read(&self.names_error, "Names")?);
214        let (last_dependency_error, last_dependency_error_at) =
215            RecordedError::split(read(&self.dependency_error, "dependency")?);
216        let (last_inventory_error, last_inventory_error_at) =
217            RecordedError::split(read(&self.inventory_error, "inventory")?);
218        let (last_discovery_error, last_discovery_error_at) =
219            RecordedError::split(read(&self.discovery_error, "discovery")?);
220        let cache = self
221            .cache_status
222            .lock()
223            .map_err(|_| Error::Invalid("cache status poisoned".into()))?
224            .clone();
225        let (last_cache_error, last_cache_error_at) = RecordedError::split(cache.error);
226        let overlay = self.overlay_status()?;
227        let mut commit_sources = Vec::new();
228        for sources in self.commit_sources.iter() {
229            commit_sources.extend(sources.status()?);
230        }
231        let mut status = ServiceStatus::default();
232        status.archive = self.engine.status()?;
233        status.last_sync_error = last_sync_error;
234        status.last_sync_error_at = last_sync_error_at;
235        status.last_retention_error = last_retention_error;
236        status.last_retention_error_at = last_retention_error_at;
237        status.last_names_error = last_names_error;
238        status.last_names_error_at = last_names_error_at;
239        status.last_dependency_error = last_dependency_error;
240        status.last_dependency_error_at = last_dependency_error_at;
241        status.last_inventory_error = last_inventory_error;
242        status.last_inventory_error_at = last_inventory_error_at;
243        status.last_discovery_error = last_discovery_error;
244        status.last_discovery_error_at = last_discovery_error_at;
245        status.commit_sources = commit_sources;
246        status.cache_limit_bytes = self.cache_bytes;
247        status.last_cache_error = last_cache_error;
248        status.last_cache_error_at = last_cache_error_at;
249        status.last_cache_maintenance = cache.receipt;
250        status.overlay = overlay;
251        status.chain_peers = self.engine.chain_peers()?;
252        status.chain_target = self.engine.chain_target();
253        status.retention_configured = self.retention.is_some();
254        status.record_sources_configured = u64::try_from(self.record_source_count())
255            .map_err(|error| Error::Invalid(format!("record source count: {error}")))?;
256        status.capabilities = Self::CAPABILITIES.map(String::from).to_vec();
257        Ok(status)
258    }
259
260    fn accept_loop(&self) -> Result<(), Error> {
261        while !self.stop.load(Ordering::Acquire) {
262            match self.listener.accept() {
263                Ok((mut stream, peer)) => {
264                    if !peer.ip().is_loopback() {
265                        continue;
266                    }
267                    stream.set_read_timeout(Some(Duration::from_secs(2)))?;
268                    stream.set_write_timeout(Some(Duration::from_secs(2)))?;
269                    // A malformed or disconnected client never stops the service.
270                    let _ = self.request(&mut stream);
271                }
272                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
273                    std::thread::sleep(Duration::from_millis(50))
274                }
275                Err(e) => return Err(e.into()),
276            }
277        }
278        Ok(())
279    }
280
281    fn request(&self, stream: &mut TcpStream) -> Result<(), Error> {
282        let mut raw = Vec::new();
283        let began = std::time::Instant::now();
284        loop {
285            if raw.len() == 8192 || began.elapsed() > Duration::from_secs(3) {
286                return respond(stream, 400, json!({"error":"request header limit"}));
287            }
288            let mut byte = [0];
289            if stream.read(&mut byte)? == 0 {
290                return Ok(());
291            }
292            raw.push(byte[0]);
293            if began.elapsed() > Duration::from_secs(3) {
294                return respond(stream, 400, json!({"error":"request header deadline"}));
295            }
296            if raw.ends_with(b"\r\n\r\n") {
297                break;
298            }
299        }
300        let Ok(request) = std::str::from_utf8(&raw) else {
301            return respond(stream, 400, json!({"error":"invalid header encoding"}));
302        };
303        let mut lines = request.split("\r\n");
304        let first = lines.next().unwrap_or("");
305        let mut authorized = false;
306        let mut authorization_seen = false;
307        for line in lines.filter(|line| !line.is_empty()) {
308            let Some((name, value)) = line.split_once(':') else {
309                return respond(stream, 400, json!({"error":"malformed header"}));
310            };
311            if name.eq_ignore_ascii_case("authorization") {
312                if authorization_seen {
313                    return respond(stream, 400, json!({"error":"duplicate authorization"}));
314                }
315                authorization_seen = true;
316                authorized = value
317                    .trim()
318                    .strip_prefix("Bearer ")
319                    .is_some_and(|value| equal_token(value, &self.token));
320            }
321            if name.eq_ignore_ascii_case("transfer-encoding")
322                || (name.eq_ignore_ascii_case("content-length") && value.trim() != "0")
323            {
324                return respond(
325                    stream,
326                    400,
327                    json!({"error":"this control endpoint accepts no request body"}),
328                );
329            }
330        }
331        if !authorized {
332            return respond(stream, 401, json!({"error":"authentication required"}));
333        }
334        match first {
335            line if (line.starts_with("GET /v1/names/") || line.starts_with("POST /v1/names/"))
336                && line.ends_with(" HTTP/1.1") =>
337            {
338                let (method, path) = line[..line.len() - " HTTP/1.1".len()]
339                    .split_once(' ')
340                    .expect("matched HTTP method");
341                let request = match names_request(method, path) {
342                    Ok(Some(request)) => request,
343                    Ok(None) => {
344                        return respond(stream, 404, json!({"error":"unknown endpoint"}));
345                    }
346                    Err(_) => {
347                        return respond(stream, 400, json!({"error":"invalid Names request"}));
348                    }
349                };
350                // Reads never register or advance a registry. Mutations are
351                // explicit one-shot commands under the Engine archive mutex.
352                let result = match request {
353                    NamesRequest::Status(genesis) => match self.engine.names_status(genesis) {
354                        Ok(Some(status)) => serde_json::to_value(status).map_err(Error::from),
355                        Ok(None) => {
356                            return respond(
357                                stream,
358                                404,
359                                json!({"error":"Names registry is not registered locally"}),
360                            );
361                        }
362                        Err(error) => Err(error),
363                    },
364                    NamesRequest::Resolve(genesis, name) => self
365                        .engine
366                        .names_resolve(genesis, &name)
367                        .and_then(|value| serde_json::to_value(value).map_err(Error::from)),
368                    NamesRequest::Register(genesis, point) => self
369                        .engine
370                        .register_names(genesis, point)
371                        .and_then(|value| serde_json::to_value(value).map_err(Error::from)),
372                    NamesRequest::Step(genesis) => self
373                        .engine
374                        .names_step(genesis)
375                        .and_then(|value| serde_json::to_value(value).map_err(Error::from)),
376                    NamesRequest::View(genesis, limit, cursor) => self
377                        .engine
378                        .names_view(genesis, cursor.as_ref(), limit)
379                        .and_then(|value| serde_json::to_value(value).map_err(Error::from)),
380                };
381                match result {
382                    Ok(value) => respond(stream, 200, value),
383                    Err(error) => control_error(stream, error),
384                }
385            }
386            line if line.starts_with("GET /v1/wire?") && line.ends_with(" HTTP/1.1") => {
387                let query = &line["GET /v1/wire?".len()..line.len() - " HTTP/1.1".len()];
388                let (limit, cursor) = match wire_query(query) {
389                    Ok(value) => value,
390                    Err(_) => {
391                        return respond(stream, 400, json!({"error":"invalid Wire page query"}));
392                    }
393                };
394                match self.engine.wire_page(cursor.as_ref(), limit) {
395                    Ok(page) => respond(stream, 200, serde_json::to_value(page)?),
396                    Err(error) => control_error(stream, error),
397                }
398            }
399            "POST /v1/cache/prune HTTP/1.1" => match self.engine.prune_cache() {
400                Ok(records_removed) => respond(
401                    stream,
402                    200,
403                    serde_json::to_value(crate::model::PruneReceipt { records_removed })?,
404                ),
405                Err(error) => control_error(stream, error),
406            },
407            line if line.starts_with("POST /v1/objects/") && line.ends_with(" HTTP/1.1") => {
408                let path = &line["POST /v1/objects/".len()..line.len() - " HTTP/1.1".len()];
409                let (id, pin) = match path.rsplit_once('/') {
410                    Some((id, "pin")) => (id, true),
411                    Some((id, "unpin")) => (id, false),
412                    _ => return respond(stream, 404, json!({"error":"unknown endpoint"})),
413                };
414                if id.len() != 64 || !id.bytes().all(|b| b.is_ascii_hexdigit()) {
415                    return respond(stream, 400, json!({"error":"invalid root transaction ID"}));
416                }
417                let root = id
418                    .parse()
419                    .map_err(|e| Error::Invalid(format!("TXID: {e}")))?;
420                // Engine serializes validation and mutation with pruning/reorg.
421                // The acknowledgement describes this operation's commit point;
422                // a lost response must not be treated as proof of no mutation.
423                match self.engine.set_pin(root, pin) {
424                    Ok(()) => respond(
425                        stream,
426                        200,
427                        serde_json::to_value(crate::model::PinReceipt { root, pinned: pin })?,
428                    ),
429                    Err(error) => control_error(stream, error),
430                }
431            }
432            line if line.starts_with("GET /v1/objects/") && line.ends_with(" HTTP/1.1") => {
433                let id = &line["GET /v1/objects/".len()..line.len() - " HTTP/1.1".len()];
434                if id.len() != 64 || !id.bytes().all(|b| b.is_ascii_hexdigit()) {
435                    return respond(stream, 400, json!({"error":"invalid root transaction ID"}));
436                }
437                let root = id
438                    .parse()
439                    .map_err(|e| Error::Invalid(format!("TXID: {e}")))?;
440                match self.engine.object_status(root)? {
441                    Some(status) => respond(stream, 200, serde_json::to_value(status)?),
442                    None => respond(
443                        stream,
444                        404,
445                        json!({"error":"no completed object recorded locally"}),
446                    ),
447                }
448            }
449            line if line.starts_with("GET /v1/records/") && line.ends_with(" HTTP/1.1") => {
450                let id = &line["GET /v1/records/".len()..line.len() - " HTTP/1.1".len()];
451                if id.len() != 64 || !id.bytes().all(|b| b.is_ascii_hexdigit()) {
452                    return respond(stream, 400, json!({"error":"invalid transaction ID"}));
453                }
454                let txid = id
455                    .parse()
456                    .map_err(|e| Error::Invalid(format!("TXID: {e}")))?;
457                match self.engine.record(txid)? {
458                    Some(record) => respond(stream, 200, serde_json::to_value(record)?),
459                    None => respond(
460                        stream,
461                        404,
462                        json!({"error":"record is not locally available in the selected view"}),
463                    ),
464                }
465            }
466            "GET /v1/status HTTP/1.1" => {
467                respond(stream, 200, serde_json::to_value(self.status()?)?)
468            }
469            // Ascending by height; only the newest (highest) 1024 rows are returned.
470            "GET /v1/overlay/omissions HTTP/1.1" => match self.engine.peer_omissions_latest(1024) {
471                Ok(omissions) => respond(stream, 200, serde_json::to_value(omissions)?),
472                Err(error) => control_error(stream, error),
473            },
474            "GET /v1/overlay/book HTTP/1.1" => match self.overlay_book() {
475                Ok(Some(book)) => respond(stream, 200, serde_json::to_value(book)?),
476                Ok(None) => respond(stream, 404, json!({"error":"overlay disabled"})),
477                Err(error) => control_error(stream, error),
478            },
479            "POST /v1/shutdown HTTP/1.1" => {
480                respond(stream, 200, json!({"stopping":true}))?;
481                self.stop.store(true, Ordering::Release);
482                Ok(())
483            }
484            _ => respond(stream, 404, json!({"error":"unknown endpoint"})),
485        }
486    }
487}
488
489enum NamesRequest {
490    Status(bitcoin::Txid),
491    Resolve(bitcoin::Txid, urma_names::name::Name),
492    Register(bitcoin::Txid, crate::model::ChainPoint),
493    Step(bitcoin::Txid),
494    View(bitcoin::Txid, u32, Option<crate::names::NamesCursor>),
495}
496
497fn names_request(method: &str, path: &str) -> Result<Option<NamesRequest>, Error> {
498    let invalid = || Error::Invalid("invalid Names request".into());
499    let path = path.strip_prefix("/v1/names/").ok_or_else(invalid)?;
500    let (id, operation) = path.split_once('/').unwrap_or((path, ""));
501    if id.len() != 64 || !id.bytes().all(|b| b.is_ascii_hexdigit()) {
502        return Err(invalid());
503    }
504    let genesis = id.parse().map_err(|_| invalid())?;
505    let request = match (method, operation) {
506        ("GET", "") => NamesRequest::Status(genesis),
507        ("GET", operation) if operation.starts_with("view?") => {
508            let (limit, cursor) = names_view_query(&operation["view?".len()..])?;
509            if cursor.as_ref().is_some_and(|c| c.genesis != genesis) {
510                return Err(invalid());
511            }
512            NamesRequest::View(genesis, limit, cursor)
513        }
514        ("GET", operation) if operation.starts_with("resolve/") => {
515            // Canonical Names V0 uses only ASCII letters, digits and hyphens;
516            // do not normalize, percent-decode or accept alternate spellings.
517            let name = urma_names::name::Name::parse(&operation["resolve/".len()..])
518                .map_err(|_| invalid())?;
519            NamesRequest::Resolve(genesis, name)
520        }
521        ("POST", "step") => NamesRequest::Step(genesis),
522        ("POST", operation) if operation.starts_with("register?") => {
523            let mut height = None;
524            let mut hash = None;
525            for field in operation["register?".len()..].split('&') {
526                match field.split_once('=') {
527                    Some(("height", value)) if height.is_none() => {
528                        if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
529                            return Err(invalid());
530                        }
531                        let value: u64 = value.parse().map_err(|_| invalid())?;
532                        if value > i64::MAX as u64 {
533                            return Err(invalid());
534                        }
535                        height = Some(value);
536                    }
537                    Some(("hash", value)) if hash.is_none() => {
538                        if value.len() != 64 || !value.bytes().all(|b| b.is_ascii_hexdigit()) {
539                            return Err(invalid());
540                        }
541                        hash = Some(value.parse().map_err(|_| invalid())?);
542                    }
543                    _ => return Err(invalid()),
544                }
545            }
546            NamesRequest::Register(
547                genesis,
548                crate::model::ChainPoint {
549                    height: height.ok_or_else(invalid)?,
550                    hash: hash.ok_or_else(invalid)?,
551                },
552            )
553        }
554        _ => return Ok(None),
555    };
556    Ok(Some(request))
557}
558
559fn names_view_query(query: &str) -> Result<(u32, Option<crate::names::NamesCursor>), Error> {
560    let invalid = || Error::Invalid("invalid Names view query".into());
561    let mut limit = None;
562    let mut cursor = None;
563    for field in query.split('&') {
564        match field.split_once('=') {
565            Some(("limit", value)) if limit.is_none() => {
566                if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
567                    return Err(invalid());
568                }
569                let value: u32 = value.parse().map_err(|_| invalid())?;
570                if !(1..=crate::names::MAX_NAMES_PAGE).contains(&value) {
571                    return Err(invalid());
572                }
573                limit = Some(value);
574            }
575            Some(("cursor", value)) if cursor.is_none() => {
576                if value.len() > 1024 {
577                    return Err(invalid());
578                }
579                let bytes = hex::decode(value).map_err(|_| invalid())?;
580                let decoded: crate::names::NamesCursor = serde_json::from_slice(&bytes)?;
581                if decoded.snapshot.instance.len() != 32
582                    || !decoded
583                        .snapshot
584                        .instance
585                        .bytes()
586                        .all(|b| b.is_ascii_hexdigit())
587                {
588                    return Err(invalid());
589                }
590                cursor = Some(decoded);
591            }
592            _ => return Err(invalid()),
593        }
594    }
595    Ok((limit.ok_or_else(invalid)?, cursor))
596}
597
598fn wire_query(query: &str) -> Result<(u32, Option<crate::projection::WireCursor>), Error> {
599    let invalid = || Error::Invalid("invalid Wire page query".into());
600    let mut limit = None;
601    let mut cursor = None;
602    for field in query.split('&') {
603        match field.split_once('=') {
604            Some(("limit", value)) if limit.is_none() => {
605                if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
606                    return Err(invalid());
607                }
608                let value: u32 = value.parse().map_err(|_| invalid())?;
609                if !(1..=crate::projection::MAX_WIRE_PAGE).contains(&value) {
610                    return Err(invalid());
611                }
612                limit = Some(value);
613            }
614            Some(("cursor", value)) if cursor.is_none() => {
615                if value.len() > 1024 {
616                    return Err(invalid());
617                }
618                let bytes = hex::decode(value).map_err(|_| invalid())?;
619                let decoded: crate::projection::WireCursor = serde_json::from_slice(&bytes)?;
620                if decoded.snapshot.instance.len() != 32
621                    || !decoded
622                        .snapshot
623                        .instance
624                        .bytes()
625                        .all(|b| b.is_ascii_hexdigit())
626                    || decoded.height > i64::MAX as u64
627                {
628                    return Err(invalid());
629                }
630                cursor = Some(decoded);
631            }
632            _ => return Err(invalid()),
633        }
634    }
635    Ok((limit.ok_or_else(invalid)?, cursor))
636}
637
638fn control_error(stream: &mut TcpStream, error: Error) -> Result<(), Error> {
639    match error {
640        Error::Invalid(message) => respond(stream, 422, json!({"error":message})),
641        error @ (Error::Busy(_)
642        | Error::PeerBusy
643        | Error::Transport(_)
644        | Error::DialDenied(_)
645        | Error::Stale) => respond(stream, 409, json!({"error":error.to_string()})),
646        // Do not expose database internals or local filesystem paths over HTTP.
647        _ => respond(
648            stream,
649            500,
650            json!({"error":"local archive operation failed"}),
651        ),
652    }
653}
654
655fn equal_token(left: &str, right: &str) -> bool {
656    if left.len() != right.len() {
657        return false;
658    }
659    left.bytes()
660        .zip(right.bytes())
661        .fold(0u8, |different, (a, b)| different | (a ^ b))
662        == 0
663}
664
665fn respond(stream: &mut TcpStream, code: u16, body: serde_json::Value) -> Result<(), Error> {
666    let body = serde_json::to_vec(&body)?;
667    if body.len() > 32 * 1024 * 1024 {
668        return respond(
669            stream,
670            413,
671            json!({"error":"record exceeds local API response limit"}),
672        );
673    }
674    let reason = match code {
675        200 => "OK",
676        400 => "Bad Request",
677        401 => "Unauthorized",
678        409 => "Conflict",
679        413 => "Content Too Large",
680        422 => "Unprocessable Content",
681        500 => "Internal Server Error",
682        _ => "Not Found",
683    };
684    write!(
685        stream,
686        "HTTP/1.1 {code} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n",
687        body.len()
688    )?;
689    stream.write_all(&body)?;
690    Ok(())
691}
692
693/// New service token, never a wallet/signing/decryption key. Existing files
694/// are not overwritten. Platform-specific private ACL support is required.
695pub fn create_token(path: &Path) -> Result<(), Error> {
696    #[cfg(not(unix))]
697    return Err(Error::Invalid(
698        "private token ACL creation is not implemented for this platform".into(),
699    ));
700    #[cfg(unix)]
701    {
702        use std::os::unix::fs::OpenOptionsExt;
703        let mut bytes = [0u8; 32];
704        rand::rngs::OsRng.fill_bytes(&mut bytes);
705        let mut file = OpenOptions::new()
706            .write(true)
707            .create_new(true)
708            .mode(0o600)
709            .open(path)?;
710        file.write_all(hex::encode(bytes).as_bytes())?;
711        file.sync_all()?;
712        Ok(())
713    }
714}