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