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