1use crate::{
9 Error,
10 model::{ArchivedRecord, ChainPoint, ObjectStatus, PinReceipt, PruneReceipt, ServiceStatus},
11 names::{
12 MAX_NAMES_PAGE, MAX_NAMES_PAGE_BYTES, MAX_NAMES_STATE_BYTES, NamesCursor, NamesEntry,
13 NamesPage, NamesResolution, NamesStatus, NamesStep,
14 },
15 projection::{MAX_WIRE_PAGE, WireCursor, WirePage},
16};
17use bitcoin::Txid;
18use std::{
19 io::{self, Read, Write},
20 net::{SocketAddr, TcpStream},
21 time::{Duration, Instant},
22};
23use urma_names::name::Name;
24mod overlay;
25
26const CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
27const OPERATION_TIMEOUT: Duration = Duration::from_secs(5);
28const MAX_HEADER: usize = 8192;
29const MAX_BODY: usize = 32 * 1024 * 1024;
30
31pub struct Client {
34 address: SocketAddr,
35 token: String,
36}
37
38impl Client {
39 pub fn new(address: SocketAddr, token: String) -> Result<Self, Error> {
40 if !address.ip().is_loopback()
41 || token.len() != 64
42 || !token.bytes().all(|byte| byte.is_ascii_hexdigit())
43 {
44 return Err(invalid(
45 "client requires loopback and a 64-hex bearer token",
46 ));
47 }
48 Ok(Self { address, token })
49 }
50
51 pub fn status(&self) -> Result<ServiceStatus, Error> {
54 Ok(serde_json::from_value(self.status_json()?)?)
55 }
56
57 pub fn status_json(&self) -> Result<serde_json::Value, Error> {
59 let deadline = Instant::now() + OPERATION_TIMEOUT;
60 let (code, body) = self.request("GET", "/v1/status", deadline)?;
61 if code != 200 {
62 return Err(invalid(format!("local status HTTP {code}")));
63 }
64 let value = serde_json::from_slice(&body)?;
65 remaining(deadline)?;
66 Ok(value)
67 }
68
69 pub fn record(&self, txid: Txid) -> Result<Option<ArchivedRecord>, Error> {
77 let deadline = Instant::now() + OPERATION_TIMEOUT;
78 let (code, body) = self.request("GET", &format!("/v1/records/{txid}"), deadline)?;
79 match code {
80 200 => {
81 let record: ArchivedRecord = serde_json::from_slice(&body)?;
82 if record.receipt.txid != txid {
83 return Err(invalid("local record receipt TXID differs from request"));
84 }
85 remaining(deadline)?;
86 Ok(Some(record))
87 }
88 404 => {
89 let _: serde_json::Value = serde_json::from_slice(&body)?;
91 remaining(deadline)?;
92 Ok(None)
93 }
94 _ => Err(invalid(format!("local record HTTP {code}"))),
95 }
96 }
97
98 pub fn object_status(&self, root: Txid) -> Result<Option<ObjectStatus>, Error> {
105 let deadline = Instant::now() + OPERATION_TIMEOUT;
106 let (code, body) = self.request("GET", &format!("/v1/objects/{root}"), deadline)?;
107 match code {
108 200 => {
109 let status: ObjectStatus = serde_json::from_slice(&body)?;
110 remaining(deadline)?;
111 if status.root != root {
112 return Err(invalid("local object status root differs from request"));
113 }
114 if status.members_total == 0
115 || status.members_local > status.members_total
116 || status.members_selected > status.members_total
117 {
118 return Err(invalid("local object status has impossible member counts"));
119 }
120 Ok(Some(status))
121 }
122 404 => {
123 let _: serde_json::Value = serde_json::from_slice(&body)?;
124 remaining(deadline)?;
125 Ok(None)
126 }
127 _ => Err(invalid(format!("local object status HTTP {code}"))),
128 }
129 }
130
131 pub fn set_pin(&self, root: Txid, pin: bool) -> Result<PinReceipt, Error> {
137 let deadline = Instant::now() + OPERATION_TIMEOUT;
138 let action = if pin { "pin" } else { "unpin" };
139 let (code, body) =
140 self.request("POST", &format!("/v1/objects/{root}/{action}"), deadline)?;
141 if code != 200 {
142 return Err(invalid(format!("local {action} HTTP {code}")));
143 }
144 let receipt: PinReceipt = serde_json::from_slice(&body)?;
145 remaining(deadline)?;
146 if receipt.root != root || receipt.pinned != pin {
147 return Err(invalid("local pin receipt differs from request"));
148 }
149 Ok(receipt)
150 }
151
152 pub fn prune_cache(&self) -> Result<PruneReceipt, Error> {
157 let deadline = Instant::now() + OPERATION_TIMEOUT;
158 let (code, body) = self.request("POST", "/v1/cache/prune", deadline)?;
159 if code != 200 {
160 return Err(invalid(format!("local cache prune HTTP {code}")));
161 }
162 let receipt = serde_json::from_slice(&body)?;
163 remaining(deadline)?;
164 Ok(receipt)
165 }
166
167 pub fn wire_page(&self, cursor: Option<&WireCursor>, limit: u32) -> Result<WirePage, Error> {
174 let deadline = Instant::now() + OPERATION_TIMEOUT;
175 if !(1..=MAX_WIRE_PAGE).contains(&limit) {
176 return Err(invalid("local Wire limit must be 1..128"));
177 }
178 let mut path = format!("/v1/wire?limit={limit}");
179 if let Some(cursor) = cursor {
180 validate_instance(&cursor.snapshot.instance)?;
181 if cursor.height > i64::MAX as u64 {
182 return Err(invalid("local Wire cursor height is out of range"));
183 }
184 let encoded = serde_json::to_vec(cursor)?;
185 if encoded.len() > 512 {
186 return Err(invalid("local Wire cursor exceeds 1024 hex characters"));
187 }
188 path.push_str("&cursor=");
189 path.push_str(&hex::encode(encoded));
190 }
191 let (code, body) = self.request("GET", &path, deadline)?;
192 if code == 409 {
193 return Err(Error::Busy(
194 "local Wire snapshot changed; discard traversal and restart".into(),
195 ));
196 }
197 if code != 200 {
198 return Err(invalid(format!("local Wire HTTP {code}")));
199 }
200 let page: WirePage = serde_json::from_slice(&body)?;
201 remaining(deadline)?;
202 validate_wire_page(&page, cursor, limit, deadline)?;
203 remaining(deadline)?;
204 Ok(page)
205 }
206
207 pub fn register_names(&self, genesis: Txid, block: ChainPoint) -> Result<NamesStatus, Error> {
213 let deadline = Instant::now() + OPERATION_TIMEOUT;
214 if block.height > i64::MAX as u64 {
215 return Err(invalid("local Names registration height is out of range"));
216 }
217 let path = format!(
218 "/v1/names/{genesis}/register?height={}&hash={}",
219 block.height, block.hash
220 );
221 let (code, body) = self.names_request("POST", &path, deadline)?;
222 if code != 200 {
223 return Err(invalid(format!("local Names registration HTTP {code}")));
224 }
225 let status: NamesStatus = serde_json::from_slice(&body)?;
226 remaining(deadline)?;
227 validate_names_status(&status, genesis)?;
228 if status.genesis_block != block {
229 return Err(invalid(
230 "local Names genesis block differs from registration",
231 ));
232 }
233 remaining(deadline)?;
234 Ok(status)
235 }
236
237 pub fn names_step(&self, genesis: Txid) -> Result<NamesStep, Error> {
242 let deadline = Instant::now() + OPERATION_TIMEOUT;
243 let (code, body) =
244 self.names_request("POST", &format!("/v1/names/{genesis}/step"), deadline)?;
245 if code != 200 {
246 return Err(invalid(format!("local Names step HTTP {code}")));
247 }
248 let step: NamesStep = serde_json::from_slice(&body)?;
249 remaining(deadline)?;
250 validate_names_status(&step.status, genesis)?;
251 if step
252 .applied
253 .is_some_and(|point| point != step.status.projected)
254 {
255 return Err(invalid("local Names applied block differs from projection"));
256 }
257 remaining(deadline)?;
258 Ok(step)
259 }
260
261 pub fn names_status(&self, genesis: Txid) -> Result<Option<NamesStatus>, Error> {
266 let deadline = Instant::now() + OPERATION_TIMEOUT;
267 let (code, body) = self.names_request("GET", &format!("/v1/names/{genesis}"), deadline)?;
268 match code {
269 200 => {
270 let status: NamesStatus = serde_json::from_slice(&body)?;
271 remaining(deadline)?;
272 validate_names_status(&status, genesis)?;
273 remaining(deadline)?;
274 Ok(Some(status))
275 }
276 404 => {
277 let _: serde_json::Value = serde_json::from_slice(&body)?;
278 remaining(deadline)?;
279 Ok(None)
280 }
281 _ => Err(invalid(format!("local Names status HTTP {code}"))),
282 }
283 }
284
285 pub fn names_resolve(&self, genesis: Txid, name: &Name) -> Result<NamesResolution, Error> {
290 let deadline = Instant::now() + OPERATION_TIMEOUT;
291 let path = format!("/v1/names/{genesis}/resolve/{}", name.as_str());
292 let (code, body) = self.names_request("GET", &path, deadline)?;
293 if code != 200 {
294 return Err(invalid(format!("local Names resolution HTTP {code}")));
295 }
296 let resolution: NamesResolution = serde_json::from_slice(&body)?;
297 remaining(deadline)?;
298 validate_names_status(&resolution.status, genesis)?;
299 remaining(deadline)?;
300 Ok(resolution)
301 }
302
303 pub fn names_view(
310 &self,
311 genesis: Txid,
312 cursor: Option<&NamesCursor>,
313 limit: u32,
314 ) -> Result<NamesPage, Error> {
315 let deadline = Instant::now() + OPERATION_TIMEOUT;
316 if !(1..=MAX_NAMES_PAGE).contains(&limit) {
317 return Err(invalid("local Names view limit must be 1..128"));
318 }
319 let mut path = format!("/v1/names/{genesis}/view?limit={limit}");
320 if let Some(cursor) = cursor {
321 if cursor.genesis != genesis {
322 return Err(invalid("local Names cursor genesis differs from request"));
323 }
324 validate_names_instance(&cursor.snapshot.instance)?;
325 let encoded = serde_json::to_vec(cursor)?;
326 if encoded.len() > 512 {
327 return Err(invalid("local Names cursor exceeds 1024 hex characters"));
328 }
329 path.push_str("&cursor=");
330 path.push_str(&hex::encode(encoded));
331 }
332 let (code, body) = self.names_request("GET", &path, deadline)?;
333 if code != 200 {
334 return Err(invalid(format!("local Names view HTTP {code}")));
335 }
336 if body.len() > MAX_NAMES_PAGE_BYTES {
337 return Err(invalid("local Names page exceeds 16 MiB"));
338 }
339 let page: NamesPage = serde_json::from_slice(&body)?;
340 remaining(deadline)?;
341 validate_names_page(&page, genesis, cursor, limit, deadline)?;
342 remaining(deadline)?;
343 Ok(page)
344 }
345
346 fn names_request(
347 &self,
348 method: &str,
349 path: &str,
350 deadline: Instant,
351 ) -> Result<(u16, Vec<u8>), Error> {
352 let response = self.request(method, path, deadline)?;
353 if response.0 == 409 {
354 return Err(Error::Busy(
355 "local Names operation busy or selected state changed".into(),
356 ));
357 }
358 Ok(response)
359 }
360
361 fn request(
362 &self,
363 method: &str,
364 path: &str,
365 deadline: Instant,
366 ) -> Result<(u16, Vec<u8>), Error> {
367 let mut stream =
368 TcpStream::connect_timeout(&self.address, CONNECT_TIMEOUT.min(remaining(deadline)?))?;
369 let body_length = if method == "POST" {
370 "Content-Length: 0\r\n"
371 } else {
372 ""
373 };
374 let request = format!(
375 "{method} {path} HTTP/1.1\r\nHost: {}\r\nAuthorization: Bearer {}\r\nAccept: application/json\r\nAccept-Encoding: identity\r\n{body_length}Connection: close\r\n\r\n",
376 self.address, self.token
377 );
378 let mut pending = request.as_bytes();
379 while !pending.is_empty() {
380 stream.set_write_timeout(Some(remaining(deadline)?))?;
381 let count = match stream.write(pending) {
382 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
383 result => result?,
384 };
385 remaining(deadline)?;
386 if count == 0 {
387 return Err(io::Error::from(io::ErrorKind::WriteZero).into());
388 }
389 pending = &pending[count..];
390 }
391 let mut header = Vec::new();
392 while !header.ends_with(b"\r\n\r\n") {
393 if header.len() == MAX_HEADER {
394 return Err(invalid("local HTTP header exceeds 8192 bytes"));
395 }
396 let mut byte = [0];
397 read(&mut stream, &mut byte, deadline)?;
398 header.push(byte[0]);
399 }
400 let (code, length) = parse_header(&header)?;
401 let mut body = vec![0; length];
402 let mut received = 0;
403 while received < length {
404 received += read(&mut stream, &mut body[received..], deadline)?;
405 }
406 remaining(deadline)?;
407 Ok((code, body))
408 }
409}
410
411fn invalid(message: impl Into<String>) -> Error {
412 Error::Invalid(message.into())
413}
414
415fn validate_names_status(status: &NamesStatus, genesis: Txid) -> Result<(), Error> {
416 if status.genesis != genesis {
417 return Err(invalid("local Names genesis differs from request"));
418 }
419 let tip = status
420 .selected_tip
421 .ok_or_else(|| invalid("local Names projection has no selected tip"))?;
422 if status.projected.height < status.genesis_block.height || tip.height < status.projected.height
423 {
424 return Err(invalid("local Names projection heights are inconsistent"));
425 }
426 for (left, right) in [
427 (status.genesis_block, status.projected),
428 (status.projected, tip),
429 (status.genesis_block, tip),
430 ] {
431 if left.height == right.height && left.hash != right.hash {
432 return Err(invalid("local Names equal-height block hashes differ"));
433 }
434 }
435 if status.complete_to_tip != (status.projected == tip) {
436 return Err(invalid(
437 "local Names completion differs from projection and tip",
438 ));
439 }
440 Ok(())
441}
442
443fn validate_names_instance(instance: &str) -> Result<(), Error> {
444 if instance.len() != 32 || !instance.bytes().all(|byte| byte.is_ascii_hexdigit()) {
445 return Err(invalid(
446 "local Names snapshot instance must be 32 hex characters",
447 ));
448 }
449 Ok(())
450}
451
452fn names_json_bound(value: &impl serde::Serialize, max: usize, message: &str) -> Result<(), Error> {
455 struct Counter {
456 count: usize,
457 max: usize,
458 exceeded: bool,
459 }
460 impl Write for Counter {
461 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
462 if bytes.len() > self.max - self.count {
463 self.exceeded = true;
464 return Err(io::Error::other("serialized Names byte bound"));
465 }
466 self.count += bytes.len();
467 Ok(bytes.len())
468 }
469 fn flush(&mut self) -> io::Result<()> {
470 Ok(())
471 }
472 }
473 let mut counter = Counter {
474 count: 0,
475 max,
476 exceeded: false,
477 };
478 serde_json::to_writer(&mut counter, value).map_err(|error| {
479 if counter.exceeded {
480 invalid(message)
481 } else {
482 Error::Json(error)
483 }
484 })
485}
486
487fn validate_names_page(
488 page: &NamesPage,
489 genesis: Txid,
490 cursor: Option<&NamesCursor>,
491 limit: u32,
492 deadline: Instant,
493) -> Result<(), Error> {
494 validate_names_instance(&page.snapshot.instance)?;
495 validate_names_status(&page.status, genesis)?;
496 if page.entries.len() > limit as usize {
497 return Err(invalid(
498 "local Names page entry count exceeds requested limit",
499 ));
500 }
501 if cursor.is_some_and(|cursor| cursor.snapshot != page.snapshot) {
502 return Err(invalid("local Names page snapshot differs from cursor"));
503 }
504 names_json_bound(
505 &page.rules,
506 MAX_NAMES_STATE_BYTES,
507 "local Names rules exceed 64 KiB",
508 )?;
509 remaining(deadline)?;
510 page.rules
511 .validate()
512 .map_err(|_| invalid("local Names rules are invalid"))?;
513 let mut previous = cursor.map(|cursor| cursor.after.order_key());
514 for entry in &page.entries {
515 remaining(deadline)?;
516 let key = entry.key().order_key();
517 if previous.as_ref().is_some_and(|previous| &key <= previous) {
518 return Err(invalid(
519 "local Names keys are not strictly after cursor/in order",
520 ));
521 }
522 match entry {
523 NamesEntry::Name { state, .. } => names_json_bound(
524 state,
525 MAX_NAMES_STATE_BYTES,
526 "local Names state exceeds 64 KiB",
527 )?,
528 NamesEntry::Pending { record, .. } => names_json_bound(
529 record,
530 MAX_NAMES_STATE_BYTES,
531 "local Names pending state exceeds 64 KiB",
532 )?,
533 }
534 previous = Some(key);
535 }
536 if let Some(next) = &page.next {
537 let last = page
538 .entries
539 .last()
540 .ok_or_else(|| invalid("empty local Names page has a continuation"))?;
541 if next.genesis != genesis || next.snapshot != page.snapshot || next.after != last.key() {
542 return Err(invalid(
543 "local Names continuation differs from last key/genesis/snapshot",
544 ));
545 }
546 }
547 remaining(deadline)?;
548 names_json_bound(
549 page,
550 MAX_NAMES_PAGE_BYTES,
551 "local Names page exceeds 16 MiB",
552 )?;
553 remaining(deadline)?;
554 Ok(())
555}
556
557fn validate_instance(instance: &str) -> Result<(), Error> {
558 if instance.len() != 32 || !instance.bytes().all(|byte| byte.is_ascii_hexdigit()) {
559 return Err(invalid(
560 "local Wire snapshot instance must be 32 hex characters",
561 ));
562 }
563 Ok(())
564}
565
566fn validate_wire_page(
567 page: &WirePage,
568 cursor: Option<&WireCursor>,
569 limit: u32,
570 deadline: Instant,
571) -> Result<(), Error> {
572 validate_instance(&page.snapshot.instance)?;
573 if page.archive.api_version != crate::model::API_VERSION || page.records.len() > limit as usize
574 {
575 return Err(invalid(
576 "local Wire page version or record count is invalid",
577 ));
578 }
579 if cursor.is_some_and(|cursor| cursor.snapshot != page.snapshot) {
580 return Err(invalid("local Wire page snapshot differs from cursor"));
581 }
582 let mut previous_coordinate = cursor.map(|cursor| (cursor.height, cursor.position));
583 let mut previous_block = None;
584 for record in &page.records {
585 remaining(deadline)?;
586 let entry = &record.entry;
587 let coordinate = (entry.height, entry.position);
588 if previous_coordinate.is_some_and(|previous| coordinate <= previous) {
589 return Err(invalid(
590 "local Wire entries are not strictly after the cursor/in order",
591 ));
592 }
593 let start = page.archive.application_start;
594 let tip = page
595 .archive
596 .selected_tip
597 .ok_or_else(|| invalid("local Wire records require a selected tip"))?;
598 if entry.height < start.height || entry.height > tip.height {
599 return Err(invalid(
600 "local Wire entry lies outside the application/selected range",
601 ));
602 }
603 if (entry.height == start.height && record.block_hash != start.hash)
604 || (entry.height == tip.height && record.block_hash != tip.hash)
605 || previous_block
606 .is_some_and(|(height, hash)| height == entry.height && hash != record.block_hash)
607 {
608 return Err(invalid(
609 "local Wire block hashes disagree at the same height",
610 ));
611 }
612 let txid = entry
613 .txid
614 .parse::<Txid>()
615 .map_err(|_| invalid("local Wire entry has an invalid TXID"))?;
616 let author = entry
617 .author
618 .parse::<bitcoin::XOnlyPublicKey>()
619 .map_err(|_| invalid("local Wire entry has an invalid author"))?;
620 if entry.record.len() > urma_core::format::Urma::MAX_PUBLIC_BYTES * 2 {
621 return Err(invalid("local Wire record exceeds the public byte bound"));
622 }
623 let raw = hex::decode(&entry.record)
624 .map_err(|_| invalid("local Wire entry has invalid record hex"))?;
625 let parsed = urma_wire::index::entry_from_record(
626 &raw,
627 txid,
628 author,
629 None,
630 entry.height,
631 entry.position,
632 )
633 .map_err(|error| invalid(format!("invalid local Wire record: {error}")))?;
634 if parsed.is_none() {
635 return Err(invalid("local Wire entry is not a Wire record kind"));
636 }
637 previous_coordinate = Some(coordinate);
638 previous_block = Some((entry.height, record.block_hash));
639 }
640 if let Some(next) = &page.next {
641 let last = page
642 .records
643 .last()
644 .ok_or_else(|| invalid("empty local Wire page has a continuation"))?;
645 if next.snapshot != page.snapshot
646 || (next.height, next.position) != (last.entry.height, last.entry.position)
647 {
648 return Err(invalid(
649 "local Wire continuation differs from the last entry/snapshot",
650 ));
651 }
652 }
653 Ok(())
654}
655
656fn remaining(deadline: Instant) -> Result<Duration, Error> {
657 deadline
658 .checked_duration_since(Instant::now())
659 .filter(|duration| !duration.is_zero())
660 .ok_or_else(|| {
661 io::Error::new(io::ErrorKind::TimedOut, "local HTTP operation deadline").into()
662 })
663}
664
665fn read(stream: &mut TcpStream, bytes: &mut [u8], deadline: Instant) -> Result<usize, Error> {
666 loop {
667 stream.set_read_timeout(Some(remaining(deadline)?))?;
668 let count = match stream.read(bytes) {
669 Err(error) if error.kind() == io::ErrorKind::Interrupted => continue,
670 result => result?,
671 };
672 remaining(deadline)?;
673 if count == 0 {
674 return Err(io::Error::new(
675 io::ErrorKind::UnexpectedEof,
676 "truncated local HTTP response",
677 )
678 .into());
679 }
680 return Ok(count);
681 }
682}
683
684fn is_token(byte: u8) -> bool {
685 byte.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&byte)
686}
687
688fn json_content_type(value: &str) -> bool {
689 let mut parts = value.split(';');
690 if !parts
691 .next()
692 .unwrap()
693 .trim()
694 .eq_ignore_ascii_case("application/json")
695 {
696 return false;
697 }
698 if let Some(parameter) = parts.next() {
699 let Some((name, charset)) = parameter.trim().split_once('=') else {
700 return false;
701 };
702 let charset = charset.trim();
703 let charset = charset
704 .strip_prefix('"')
705 .and_then(|s| s.strip_suffix('"'))
706 .unwrap_or(charset);
707 if !name.trim().eq_ignore_ascii_case("charset")
708 || charset.is_empty()
709 || !charset.bytes().all(is_token)
710 {
711 return false;
712 }
713 }
714 parts.next().is_none()
715}
716
717fn parse_header(bytes: &[u8]) -> Result<(u16, usize), Error> {
718 let text =
719 std::str::from_utf8(bytes).map_err(|_| invalid("invalid local HTTP header encoding"))?;
720 let mut lines = text[..text.len() - 4].split("\r\n");
721 let mut status = lines.next().unwrap().splitn(3, ' ');
722 if status.next() != Some("HTTP/1.1") {
723 return Err(invalid("local response requires HTTP/1.1"));
724 }
725 let code = status.next().unwrap_or("");
726 if code.len() != 3 || !code.bytes().all(|byte| byte.is_ascii_digit()) {
727 return Err(invalid("malformed local HTTP status"));
728 }
729 let reason = status
730 .next()
731 .ok_or_else(|| invalid("malformed local HTTP status"))?;
732 if !reason
733 .bytes()
734 .all(|byte| byte == b'\t' || (32..=126).contains(&byte))
735 {
736 return Err(invalid("malformed local HTTP status"));
737 }
738 let code: u16 = code
739 .parse()
740 .map_err(|_| invalid("malformed local HTTP status"))?;
741 if !(100..=599).contains(&code) {
742 return Err(invalid("malformed local HTTP status"));
743 }
744 let mut length = None;
745 let mut content_type = None;
746 for line in lines {
747 let (name, value) = line
748 .split_once(':')
749 .ok_or_else(|| invalid("malformed local HTTP header"))?;
750 if name.is_empty()
751 || !name.bytes().all(is_token)
752 || !value
753 .bytes()
754 .all(|byte| byte == b'\t' || (32..=126).contains(&byte))
755 {
756 return Err(invalid("malformed local HTTP header"));
757 }
758 let value = value.trim_matches([' ', '\t']);
759 if name.eq_ignore_ascii_case("transfer-encoding") {
760 return Err(invalid("local HTTP transfer encoding is unsupported"));
761 }
762 if name.eq_ignore_ascii_case("content-encoding") && !value.eq_ignore_ascii_case("identity")
763 {
764 return Err(invalid("local HTTP content encoding is unsupported"));
765 }
766 if name.eq_ignore_ascii_case("content-length") {
767 if length.is_some()
768 || value.is_empty()
769 || !value.bytes().all(|byte| byte.is_ascii_digit())
770 {
771 return Err(invalid(
772 "local HTTP requires a single decimal Content-Length",
773 ));
774 }
775 let parsed: usize = value
776 .parse()
777 .map_err(|_| invalid("invalid local HTTP Content-Length"))?;
778 if parsed > MAX_BODY {
779 return Err(invalid("local HTTP body exceeds 32 MiB"));
780 }
781 length = Some(parsed);
782 }
783 if name.eq_ignore_ascii_case("content-type") {
784 if content_type.is_some() || !json_content_type(value) {
785 return Err(invalid(
786 "local HTTP requires application/json with optional charset",
787 ));
788 }
789 content_type = Some(());
790 }
791 }
792 if content_type.is_none() {
793 return Err(invalid("local HTTP Content-Type is missing"));
794 }
795 Ok((
796 code,
797 length.ok_or_else(|| invalid("local HTTP Content-Length is missing"))?,
798 ))
799}