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