viaduct/lib.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5#![allow(unknown_lints)]
6#![warn(rust_2018_idioms)]
7
8use url::Url;
9#[macro_use]
10mod headers;
11
12mod backend;
13pub mod error;
14pub mod settings;
15pub use error::*;
16// reexport logging helpers.
17pub use error_support::{debug, error, info, trace, warn};
18
19pub use backend::{note_backend, set_backend, Backend};
20pub use headers::{consts as header_names, Header, HeaderName, Headers, InvalidHeaderName};
21pub use settings::GLOBAL_SETTINGS;
22
23#[allow(clippy::derive_partial_eq_without_eq)]
24pub(crate) mod msg_types {
25 include!("mozilla.appservices.httpconfig.protobuf.rs");
26}
27
28/// HTTP Methods.
29///
30/// The supported methods are the limited to what's supported by android-components.
31#[derive(Clone, Debug, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
32#[repr(u8)]
33pub enum Method {
34 Get,
35 Head,
36 Post,
37 Put,
38 Delete,
39 Connect,
40 Options,
41 Trace,
42 Patch,
43}
44
45impl Method {
46 pub fn as_str(self) -> &'static str {
47 match self {
48 Method::Get => "GET",
49 Method::Head => "HEAD",
50 Method::Post => "POST",
51 Method::Put => "PUT",
52 Method::Delete => "DELETE",
53 Method::Connect => "CONNECT",
54 Method::Options => "OPTIONS",
55 Method::Trace => "TRACE",
56 Method::Patch => "PATCH",
57 }
58 }
59}
60
61impl std::fmt::Display for Method {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 f.write_str(self.as_str())
64 }
65}
66
67#[must_use = "`Request`'s \"builder\" functions take by move, not by `&mut self`"]
68#[derive(Clone, Debug)]
69pub struct Request {
70 pub method: Method,
71 pub url: Url,
72 pub headers: Headers,
73 pub body: Option<Vec<u8>>,
74}
75
76impl Request {
77 /// Construct a new request to the given `url` using the given `method`.
78 /// Note that the request is not made until `send()` is called.
79 pub fn new(method: Method, url: Url) -> Self {
80 Self {
81 method,
82 url,
83 headers: Headers::new(),
84 body: None,
85 }
86 }
87
88 pub fn send(self) -> Result<Response, Error> {
89 crate::backend::send(self)
90 }
91
92 /// Alias for `Request::new(Method::Get, url)`, for convenience.
93 pub fn get(url: Url) -> Self {
94 Self::new(Method::Get, url)
95 }
96
97 /// Alias for `Request::new(Method::Patch, url)`, for convenience.
98 pub fn patch(url: Url) -> Self {
99 Self::new(Method::Patch, url)
100 }
101
102 /// Alias for `Request::new(Method::Post, url)`, for convenience.
103 pub fn post(url: Url) -> Self {
104 Self::new(Method::Post, url)
105 }
106
107 /// Alias for `Request::new(Method::Put, url)`, for convenience.
108 pub fn put(url: Url) -> Self {
109 Self::new(Method::Put, url)
110 }
111
112 /// Alias for `Request::new(Method::Delete, url)`, for convenience.
113 pub fn delete(url: Url) -> Self {
114 Self::new(Method::Delete, url)
115 }
116
117 /// Append the provided query parameters to the URL
118 ///
119 /// ## Example
120 /// ```
121 /// # use viaduct::{Request, header_names};
122 /// # use url::Url;
123 /// let some_url = url::Url::parse("https://www.example.com/xyz").unwrap();
124 ///
125 /// let req = Request::post(some_url).query(&[("a", "1234"), ("b", "qwerty")]);
126 /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz?a=1234&b=qwerty");
127 ///
128 /// // This appends to the query query instead of replacing `a`.
129 /// let req = req.query(&[("a", "5678")]);
130 /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz?a=1234&b=qwerty&a=5678");
131 /// ```
132 pub fn query(mut self, pairs: &[(&str, &str)]) -> Self {
133 let mut append_to = self.url.query_pairs_mut();
134 for (k, v) in pairs {
135 append_to.append_pair(k, v);
136 }
137 drop(append_to);
138 self
139 }
140
141 /// Set the query string of the URL. Note that `req.set_query(None)` will
142 /// clear the query.
143 ///
144 /// See also `Request::query` which appends a slice of query pairs, which is
145 /// typically more ergonomic when usable.
146 ///
147 /// ## Example
148 /// ```
149 /// # use viaduct::{Request, header_names};
150 /// # use url::Url;
151 /// let some_url = url::Url::parse("https://www.example.com/xyz").unwrap();
152 ///
153 /// let req = Request::post(some_url).set_query("a=b&c=d");
154 /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz?a=b&c=d");
155 ///
156 /// let req = req.set_query(None);
157 /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz");
158 /// ```
159 pub fn set_query<'a, Q: Into<Option<&'a str>>>(mut self, query: Q) -> Self {
160 self.url.set_query(query.into());
161 self
162 }
163
164 /// Add all the provided headers to the list of headers to send with this
165 /// request.
166 pub fn headers<I>(mut self, to_add: I) -> Self
167 where
168 I: IntoIterator<Item = Header>,
169 {
170 self.headers.extend(to_add);
171 self
172 }
173
174 /// Add the provided header to the list of headers to send with this request.
175 ///
176 /// This returns `Err` if `val` contains characters that may not appear in
177 /// the body of a header.
178 ///
179 /// ## Example
180 /// ```
181 /// # use viaduct::{Request, header_names};
182 /// # use url::Url;
183 /// # fn main() -> Result<(), viaduct::Error> {
184 /// # let some_url = url::Url::parse("https://www.example.com").unwrap();
185 /// Request::post(some_url)
186 /// .header(header_names::CONTENT_TYPE, "application/json")?
187 /// .header("My-Header", "Some special value")?;
188 /// // ...
189 /// # Ok(())
190 /// # }
191 /// ```
192 pub fn header<Name, Val>(mut self, name: Name, val: Val) -> Result<Self, crate::Error>
193 where
194 Name: Into<HeaderName> + PartialEq<HeaderName>,
195 Val: Into<String> + AsRef<str>,
196 {
197 self.headers.insert(name, val)?;
198 Ok(self)
199 }
200
201 /// Set this request's body.
202 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
203 self.body = Some(body.into());
204 self
205 }
206
207 /// Set body to the result of serializing `val`, and, unless it has already
208 /// been set, set the Content-Type header to "application/json".
209 ///
210 /// Note: This panics if serde_json::to_vec fails. This can only happen
211 /// in a couple cases:
212 ///
213 /// 1. Trying to serialize a map with non-string keys.
214 /// 2. We wrote a custom serializer that fails.
215 ///
216 /// Neither of these are things we do. If they happen, it seems better for
217 /// this to fail hard with an easy to track down panic, than for e.g. `sync`
218 /// to fail with a JSON parse error (which we'd probably attribute to
219 /// corrupt data on the server, or something).
220 pub fn json<T: ?Sized + serde::Serialize>(mut self, val: &T) -> Self {
221 self.body =
222 Some(serde_json::to_vec(val).expect("Rust component bug: serde_json::to_vec failure"));
223 self.headers
224 .insert_if_missing(header_names::CONTENT_TYPE, "application/json")
225 .unwrap(); // We know this has to be valid.
226 self
227 }
228}
229
230/// A response from the server.
231#[derive(Clone, Debug)]
232pub struct Response {
233 /// The method used to request this response.
234 pub request_method: Method,
235 /// The URL of this response.
236 pub url: Url,
237 /// The HTTP Status code of this response.
238 pub status: u16,
239 /// The headers returned with this response.
240 pub headers: Headers,
241 /// The body of the response.
242 pub body: Vec<u8>,
243}
244
245impl Response {
246 /// Parse the body as JSON.
247 pub fn json<'a, T>(&'a self) -> Result<T, serde_json::Error>
248 where
249 T: serde::Deserialize<'a>,
250 {
251 serde_json::from_slice(&self.body)
252 }
253
254 /// Get the body as a string. Assumes UTF-8 encoding. Any non-utf8 bytes
255 /// are replaced with the replacement character.
256 pub fn text(&self) -> std::borrow::Cow<'_, str> {
257 String::from_utf8_lossy(&self.body)
258 }
259
260 /// Returns true if the status code is in the interval `[200, 300)`.
261 #[inline]
262 pub fn is_success(&self) -> bool {
263 status_codes::is_success_code(self.status)
264 }
265
266 /// Returns true if the status code is in the interval `[500, 600)`.
267 #[inline]
268 pub fn is_server_error(&self) -> bool {
269 status_codes::is_server_error_code(self.status)
270 }
271
272 /// Returns true if the status code is in the interval `[400, 500)`.
273 #[inline]
274 pub fn is_client_error(&self) -> bool {
275 status_codes::is_client_error_code(self.status)
276 }
277
278 /// Returns an [`UnexpectedStatus`] error if `self.is_success()` is false,
279 /// otherwise returns `Ok(self)`.
280 #[inline]
281 pub fn require_success(self) -> Result<Self, UnexpectedStatus> {
282 if self.is_success() {
283 Ok(self)
284 } else {
285 Err(UnexpectedStatus {
286 method: self.request_method,
287 // XXX We probably should try and sanitize this. Replace the user id
288 // if it's a sync token server URL, for example.
289 url: self.url,
290 status: self.status,
291 })
292 }
293 }
294}
295
296/// A module containing constants for all HTTP status codes.
297pub mod status_codes {
298
299 /// Is it a 2xx status?
300 #[inline]
301 pub fn is_success_code(c: u16) -> bool {
302 (200..300).contains(&c)
303 }
304
305 /// Is it a 4xx error?
306 #[inline]
307 pub fn is_client_error_code(c: u16) -> bool {
308 (400..500).contains(&c)
309 }
310
311 /// Is it a 5xx error?
312 #[inline]
313 pub fn is_server_error_code(c: u16) -> bool {
314 (500..600).contains(&c)
315 }
316
317 macro_rules! define_status_codes {
318 ($(($val:expr, $NAME:ident)),* $(,)?) => {
319 $(pub const $NAME: u16 = $val;)*
320 };
321 }
322 // From https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
323 define_status_codes![
324 (100, CONTINUE),
325 (101, SWITCHING_PROTOCOLS),
326 // 2xx
327 (200, OK),
328 (201, CREATED),
329 (202, ACCEPTED),
330 (203, NONAUTHORITATIVE_INFORMATION),
331 (204, NO_CONTENT),
332 (205, RESET_CONTENT),
333 (206, PARTIAL_CONTENT),
334 // 3xx
335 (300, MULTIPLE_CHOICES),
336 (301, MOVED_PERMANENTLY),
337 (302, FOUND),
338 (303, SEE_OTHER),
339 (304, NOT_MODIFIED),
340 (305, USE_PROXY),
341 // no 306
342 (307, TEMPORARY_REDIRECT),
343 // 4xx
344 (400, BAD_REQUEST),
345 (401, UNAUTHORIZED),
346 (402, PAYMENT_REQUIRED),
347 (403, FORBIDDEN),
348 (404, NOT_FOUND),
349 (405, METHOD_NOT_ALLOWED),
350 (406, NOT_ACCEPTABLE),
351 (407, PROXY_AUTHENTICATION_REQUIRED),
352 (408, REQUEST_TIMEOUT),
353 (409, CONFLICT),
354 (410, GONE),
355 (411, LENGTH_REQUIRED),
356 (412, PRECONDITION_FAILED),
357 (413, REQUEST_ENTITY_TOO_LARGE),
358 (414, REQUEST_URI_TOO_LONG),
359 (415, UNSUPPORTED_MEDIA_TYPE),
360 (416, REQUESTED_RANGE_NOT_SATISFIABLE),
361 (417, EXPECTATION_FAILED),
362 (429, TOO_MANY_REQUESTS),
363 // 5xx
364 (500, INTERNAL_SERVER_ERROR),
365 (501, NOT_IMPLEMENTED),
366 (502, BAD_GATEWAY),
367 (503, SERVICE_UNAVAILABLE),
368 (504, GATEWAY_TIMEOUT),
369 (505, HTTP_VERSION_NOT_SUPPORTED),
370 ];
371}