standard_plugin/daemon/
http.rs1#![doc = include_str!("../../examples/daemon_http.rs")]
14use alloc::string::String;
17use alloc::vec::Vec;
18
19use crate::error::{Error, Result};
20
21pub const DEFAULT_TIMEOUT_MS: u32 = 30_000;
24pub const DEFAULT_MAX_BODY: usize = 4 * 1024 * 1024;
26
27#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct Request {
31 pub method: String,
32 pub url: String,
33 pub headers: Vec<(String, String)>,
34 pub body: Vec<u8>,
35 pub timeout_ms: u32,
37 pub max_body: usize,
40}
41
42#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct Response {
45 pub status: u16,
46 pub headers: Vec<(String, String)>,
47 pub body: Vec<u8>,
48}
49
50impl Response {
51 pub fn new(status: u16) -> Self {
55 Self {
56 status,
57 headers: Vec::new(),
58 body: Vec::new(),
59 }
60 }
61
62 pub fn with_header(mut self, name: &str, value: &str) -> Self {
64 self.headers
65 .push((name.to_ascii_lowercase(), String::from(value)));
66 self
67 }
68
69 pub fn with_body(mut self, body: impl Into<Vec<u8>>) -> Self {
70 self.body = body.into();
71 self
72 }
73
74 pub fn with_json<T: serde::Serialize + ?Sized>(self, value: &T) -> Result<Self> {
76 let body = serde_json::to_vec(value).map_err(Error::json)?;
77 Ok(self
78 .with_header("content-type", "application/json")
79 .with_body(body))
80 }
81
82 pub fn header(&self, name: &str) -> Option<&str> {
84 self.headers
85 .iter()
86 .find(|(key, _)| key.eq_ignore_ascii_case(name))
87 .map(|(_, value)| value.as_str())
88 }
89
90 pub fn ok(&self) -> bool {
92 (200..300).contains(&self.status)
93 }
94
95 pub fn text(&self) -> Result<&str> {
96 core::str::from_utf8(&self.body).map_err(|error| Error::Invalid(alloc::format!("{error}")))
97 }
98
99 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T> {
100 serde_json::from_slice(&self.body).map_err(Error::json)
101 }
102}
103
104impl Request {
105 pub fn new(method: &str, url: &str) -> Self {
107 Self {
108 method: String::from(method),
109 url: String::from(url),
110 headers: Vec::new(),
111 body: Vec::new(),
112 timeout_ms: DEFAULT_TIMEOUT_MS,
113 max_body: DEFAULT_MAX_BODY,
114 }
115 }
116
117 pub fn get(url: &str) -> Self {
118 Self::new("GET", url)
119 }
120
121 pub fn post(url: &str) -> Self {
122 Self::new("POST", url)
123 }
124
125 pub fn header(mut self, name: &str, value: &str) -> Self {
127 self.headers.push((String::from(name), String::from(value)));
128 self
129 }
130
131 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
132 self.body = body.into();
133 self
134 }
135
136 pub fn json<T: serde::Serialize + ?Sized>(self, value: &T) -> Result<Self> {
138 let body = serde_json::to_vec(value).map_err(Error::json)?;
139 Ok(self.header("content-type", "application/json").body(body))
140 }
141
142 pub fn timeout_ms(mut self, timeout_ms: u32) -> Self {
145 self.timeout_ms = timeout_ms.max(1);
146 self
147 }
148
149 pub fn max_body(mut self, bytes: usize) -> Self {
152 self.max_body = bytes;
153 self
154 }
155
156 pub async fn send(self) -> Result<Response> {
158 let target = split_url(&self.url)?;
159 #[cfg(target_arch = "wasm32")]
160 let answer = super::http_wasi::send(&self, &target).await;
161 #[cfg(not(target_arch = "wasm32"))]
162 let answer = crate::testing::http_send(&self, &target);
163 answer
164 }
165}
166
167pub async fn get(url: &str) -> Result<Response> {
169 Request::get(url).send().await
170}
171
172#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
173pub(crate) fn timeout() -> Error {
174 Error::Unavailable("timeout".into())
175}
176
177pub(crate) fn too_long(max: usize) -> Error {
178 Error::Invalid(alloc::format!("the response body is over {max} bytes"))
179}
180
181pub(crate) struct Target<'a> {
183 pub scheme: &'static str,
185 pub authority: &'a str,
187 #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
189 pub path: &'a str,
190}
191
192impl Target<'_> {
193 pub fn host(&self) -> String {
195 let host = match self.authority.rsplit_once(':') {
196 Some((host, port)) if port.bytes().all(|byte| byte.is_ascii_digit()) => host,
197 _ => self.authority,
198 };
199 host.to_ascii_lowercase()
200 }
201
202 #[cfg_attr(target_arch = "wasm32", allow(dead_code))]
203 pub fn port(&self) -> Option<u16> {
204 self.authority
205 .rsplit_once(':')
206 .and_then(|(_, port)| port.parse().ok())
207 }
208
209 pub fn denied(&self) -> Error {
211 Error::GrantDenied {
212 grant: alloc::format!("fetch:{}", self.host()),
213 }
214 }
215}
216
217pub(crate) fn split_url(url: &str) -> Result<Target<'_>> {
218 let (scheme, rest) = if let Some(rest) = url.strip_prefix("https://") {
219 ("https", rest)
220 } else if let Some(rest) = url.strip_prefix("http://") {
221 ("http", rest)
222 } else {
223 return Err(Error::Invalid(alloc::format!("not an http(s) url: {url}")));
224 };
225 let (authority, path) = match rest.find(['/', '?']) {
226 Some(index) if rest.as_bytes()[index] == b'/' => (&rest[..index], &rest[index..]),
227 Some(index) => (&rest[..index], &rest[index..]),
228 None => (rest, "/"),
229 };
230 if authority.is_empty() {
231 return Err(Error::Invalid(alloc::format!("no host in {url}")));
232 }
233 Ok(Target {
234 scheme,
235 authority,
236 path,
237 })
238}