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