1use 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#[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 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 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 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 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 self.discover_peers();
220 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 if !self.stop.load(Ordering::Acquire) {
237 let result = self.engine.maintain_next_names(&self.stop);
238 record(&self.names_error, &result);
239 }
240 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 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 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 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 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 "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 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 _ => 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
775pub 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}