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