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