printobserver_sdk/
transport.rs1use std::io::{Read as _, Write as _};
14use std::net::TcpStream;
15
16use crate::error::ClientError;
17
18pub const MEDIA_TYPE: &str = "application/json";
20
21pub const CREDENTIAL_HEADER: &str = "Authorization";
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Answered {
27 pub status: u16,
29 pub body: String,
31}
32
33pub fn send(
42 address: &str,
43 credential: Option<&str>,
44 method: &str,
45 target: &str,
46 body: Option<&str>,
47) -> Result<Answered, ClientError> {
48 let failing = |detail: String| ClientError::Unreachable {
49 address: address.to_owned(),
50 detail,
51 };
52 let mut stream =
53 TcpStream::connect(address).map_err(|error| failing(format!("{:?}", error.kind())))?;
54
55 let authenticating = credential.map_or_else(String::new, |credential| {
56 format!("{CREDENTIAL_HEADER}: Bearer {credential}\r\n")
57 });
58 let carried = body.map_or_else(
59 || "\r\n".to_owned(),
60 |document| {
61 format!(
62 "Content-Type: {MEDIA_TYPE}\r\nContent-Length: {}\r\n\r\n{document}",
63 document.len()
64 )
65 },
66 );
67 let request = format!(
68 "{method} {target} HTTP/1.1\r\nHost: {address}\r\nAccept: {MEDIA_TYPE}\r\n\
69 Connection: close\r\n{authenticating}{carried}"
70 );
71 stream.write_all(request.as_bytes()).map_err(|error| {
72 failing(format!(
73 "the request could not be sent ({:?})",
74 error.kind()
75 ))
76 })?;
77
78 let mut answer = Vec::new();
79 stream
80 .read_to_end(&mut answer)
81 .map_err(|error| failing(format!("the answer could not be read ({:?})", error.kind())))?;
82 read_answer(&answer).map_err(failing)
83}
84
85fn read_answer(answer: &[u8]) -> Result<Answered, String> {
87 let separator = answer
88 .windows(4)
89 .position(|window| window == b"\r\n\r\n")
90 .ok_or_else(|| "the supervisor answered something this client cannot read".to_owned())?;
91 let head = String::from_utf8_lossy(&answer[..separator]).into_owned();
92 let body = &answer[separator + 4..];
93
94 let status = head
95 .split_whitespace()
96 .nth(1)
97 .filter(|_| head.starts_with("HTTP/1.1 "))
98 .and_then(|code| code.parse::<u16>().ok())
99 .ok_or_else(|| format!("the supervisor answered no status: {head}"))?;
100 let declared = head
101 .lines()
102 .filter_map(|line| line.split_once(':'))
103 .find(|(name, _)| name.eq_ignore_ascii_case("content-length"))
104 .and_then(|(_, value)| value.trim().parse::<usize>().ok())
105 .ok_or_else(|| format!("the supervisor declared no length for what it answered: {head}"))?;
106 if body.len() != declared {
107 return Err(format!(
108 "the supervisor declared {declared} bytes and sent {}",
109 body.len()
110 ));
111 }
112 Ok(Answered {
113 status,
114 body: String::from_utf8_lossy(body).into_owned(),
115 })
116}
117
118#[cfg(test)]
119mod tests {
120 use super::{Answered, read_answer, send};
121 use crate::error::ClientError;
122
123 #[test]
125 fn a_whole_answer_is_read_as_its_status_and_its_body() {
126 let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\n{}";
127 assert_eq!(
128 read_answer(raw).expect("a whole answer"),
129 Answered {
130 status: 200,
131 body: "{}".to_owned(),
132 }
133 );
134 }
135
136 #[test]
138 fn an_answer_that_declared_more_than_it_sent_is_refused() {
139 let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 9\r\n\r\n{}";
140 let refusal = read_answer(raw).expect_err("a truncated answer is refused");
141 assert!(refusal.contains("declared 9 bytes and sent 2"), "{refusal}");
142 }
143
144 #[test]
146 fn an_answer_with_no_status_and_one_with_no_length_are_refused() {
147 assert!(read_answer(b"nothing at all").is_err());
148 assert!(read_answer(b"ICY 200 OK\r\nContent-Length: 0\r\n\r\n").is_err());
149 assert!(read_answer(b"HTTP/1.1 200 OK\r\n\r\n").is_err());
150 }
151
152 #[test]
154 fn an_address_nothing_is_listening_on_names_that_address() {
155 let refusal = send("127.0.0.1:0", None, "GET", "/v1/status", None)
158 .expect_err("nothing answers on port zero");
159 assert!(
160 matches!(&refusal, ClientError::Unreachable { address, .. } if address == "127.0.0.1:0"),
161 "{refusal}"
162 );
163 }
164}