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