/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #![allow(unknown_lints)] #![warn(rust_2018_idioms)] use url::Url; #[macro_use] mod headers; mod backend; pub mod error; pub mod settings; pub use error::*; pub use backend::{note_backend, set_backend, Backend}; pub use headers::{consts as header_names, Header, HeaderName, Headers, InvalidHeaderName}; pub use settings::GLOBAL_SETTINGS; #[allow(clippy::derive_partial_eq_without_eq)] pub(crate) mod msg_types { include!("mozilla.appservices.httpconfig.protobuf.rs"); } /// HTTP Methods. /// /// The supported methods are the limited to what's supported by android-components. #[derive(Clone, Debug, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] #[repr(u8)] pub enum Method { Get, Head, Post, Put, Delete, Connect, Options, Trace, Patch, } impl Method { pub fn as_str(self) -> &'static str { match self { Method::Get => "GET", Method::Head => "HEAD", Method::Post => "POST", Method::Put => "PUT", Method::Delete => "DELETE", Method::Connect => "CONNECT", Method::Options => "OPTIONS", Method::Trace => "TRACE", Method::Patch => "PATCH", } } } impl std::fmt::Display for Method { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_str()) } } #[must_use = "`Request`'s \"builder\" functions take by move, not by `&mut self`"] #[derive(Clone, Debug)] pub struct Request { pub method: Method, pub url: Url, pub headers: Headers, pub body: Option>, } impl Request { /// Construct a new request to the given `url` using the given `method`. /// Note that the request is not made until `send()` is called. pub fn new(method: Method, url: Url) -> Self { Self { method, url, headers: Headers::new(), body: None, } } pub fn send(self) -> Result { crate::backend::send(self) } /// Alias for `Request::new(Method::Get, url)`, for convenience. pub fn get(url: Url) -> Self { Self::new(Method::Get, url) } /// Alias for `Request::new(Method::Patch, url)`, for convenience. pub fn patch(url: Url) -> Self { Self::new(Method::Patch, url) } /// Alias for `Request::new(Method::Post, url)`, for convenience. pub fn post(url: Url) -> Self { Self::new(Method::Post, url) } /// Alias for `Request::new(Method::Put, url)`, for convenience. pub fn put(url: Url) -> Self { Self::new(Method::Put, url) } /// Alias for `Request::new(Method::Delete, url)`, for convenience. pub fn delete(url: Url) -> Self { Self::new(Method::Delete, url) } /// Append the provided query parameters to the URL /// /// ## Example /// ``` /// # use viaduct::{Request, header_names}; /// # use url::Url; /// let some_url = url::Url::parse("https://www.example.com/xyz").unwrap(); /// /// let req = Request::post(some_url).query(&[("a", "1234"), ("b", "qwerty")]); /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz?a=1234&b=qwerty"); /// /// // This appends to the query query instead of replacing `a`. /// let req = req.query(&[("a", "5678")]); /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz?a=1234&b=qwerty&a=5678"); /// ``` pub fn query(mut self, pairs: &[(&str, &str)]) -> Self { let mut append_to = self.url.query_pairs_mut(); for (k, v) in pairs { append_to.append_pair(k, v); } drop(append_to); self } /// Set the query string of the URL. Note that `req.set_query(None)` will /// clear the query. /// /// See also `Request::query` which appends a slice of query pairs, which is /// typically more ergonomic when usable. /// /// ## Example /// ``` /// # use viaduct::{Request, header_names}; /// # use url::Url; /// let some_url = url::Url::parse("https://www.example.com/xyz").unwrap(); /// /// let req = Request::post(some_url).set_query("a=b&c=d"); /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz?a=b&c=d"); /// /// let req = req.set_query(None); /// assert_eq!(req.url.as_str(), "https://www.example.com/xyz"); /// ``` pub fn set_query<'a, Q: Into>>(mut self, query: Q) -> Self { self.url.set_query(query.into()); self } /// Add all the provided headers to the list of headers to send with this /// request. pub fn headers(mut self, to_add: I) -> Self where I: IntoIterator, { self.headers.extend(to_add); self } /// Add the provided header to the list of headers to send with this request. /// /// This returns `Err` if `val` contains characters that may not appear in /// the body of a header. /// /// ## Example /// ``` /// # use viaduct::{Request, header_names}; /// # use url::Url; /// # fn main() -> Result<(), viaduct::Error> { /// # let some_url = url::Url::parse("https://www.example.com").unwrap(); /// Request::post(some_url) /// .header(header_names::CONTENT_TYPE, "application/json")? /// .header("My-Header", "Some special value")?; /// // ... /// # Ok(()) /// # } /// ``` pub fn header(mut self, name: Name, val: Val) -> Result where Name: Into + PartialEq, Val: Into + AsRef, { self.headers.insert(name, val)?; Ok(self) } /// Set this request's body. pub fn body(mut self, body: impl Into>) -> Self { self.body = Some(body.into()); self } /// Set body to the result of serializing `val`, and, unless it has already /// been set, set the Content-Type header to "application/json". /// /// Note: This panics if serde_json::to_vec fails. This can only happen /// in a couple cases: /// /// 1. Trying to serialize a map with non-string keys. /// 2. We wrote a custom serializer that fails. /// /// Neither of these are things we do. If they happen, it seems better for /// this to fail hard with an easy to track down panic, than for e.g. `sync` /// to fail with a JSON parse error (which we'd probably attribute to /// corrupt data on the server, or something). pub fn json(mut self, val: &T) -> Self { self.body = Some(serde_json::to_vec(val).expect("Rust component bug: serde_json::to_vec failure")); self.headers .insert_if_missing(header_names::CONTENT_TYPE, "application/json") .unwrap(); // We know this has to be valid. self } } /// A response from the server. #[derive(Clone, Debug)] pub struct Response { /// The method used to request this response. pub request_method: Method, /// The URL of this response. pub url: Url, /// The HTTP Status code of this response. pub status: u16, /// The headers returned with this response. pub headers: Headers, /// The body of the response. Note that responses with binary bodies are /// currently unsupported. pub body: Vec, } impl Response { /// Parse the body as JSON. pub fn json<'a, T>(&'a self) -> Result where T: serde::Deserialize<'a>, { serde_json::from_slice(&self.body) } /// Get the body as a string. Assumes UTF-8 encoding. Any non-utf8 bytes /// are replaced with the replacement character. pub fn text(&self) -> std::borrow::Cow<'_, str> { String::from_utf8_lossy(&self.body) } /// Returns true if the status code is in the interval `[200, 300)`. #[inline] pub fn is_success(&self) -> bool { status_codes::is_success_code(self.status) } /// Returns true if the status code is in the interval `[500, 600)`. #[inline] pub fn is_server_error(&self) -> bool { status_codes::is_server_error_code(self.status) } /// Returns true if the status code is in the interval `[400, 500)`. #[inline] pub fn is_client_error(&self) -> bool { status_codes::is_client_error_code(self.status) } /// Returns an [`UnexpectedStatus`] error if `self.is_success()` is false, /// otherwise returns `Ok(self)`. #[inline] pub fn require_success(self) -> Result { if self.is_success() { Ok(self) } else { Err(UnexpectedStatus { method: self.request_method, // XXX We probably should try and sanitize this. Replace the user id // if it's a sync token server URL, for example. url: self.url, status: self.status, }) } } } /// A module containing constants for all HTTP status codes. pub mod status_codes { /// Is it a 2xx status? #[inline] pub fn is_success_code(c: u16) -> bool { (200..300).contains(&c) } /// Is it a 4xx error? #[inline] pub fn is_client_error_code(c: u16) -> bool { (400..500).contains(&c) } /// Is it a 5xx error? #[inline] pub fn is_server_error_code(c: u16) -> bool { (500..600).contains(&c) } macro_rules! define_status_codes { ($(($val:expr, $NAME:ident)),* $(,)?) => { $(pub const $NAME: u16 = $val;)* }; } // From https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html define_status_codes![ (100, CONTINUE), (101, SWITCHING_PROTOCOLS), // 2xx (200, OK), (201, CREATED), (202, ACCEPTED), (203, NONAUTHORITATIVE_INFORMATION), (204, NO_CONTENT), (205, RESET_CONTENT), (206, PARTIAL_CONTENT), // 3xx (300, MULTIPLE_CHOICES), (301, MOVED_PERMANENTLY), (302, FOUND), (303, SEE_OTHER), (304, NOT_MODIFIED), (305, USE_PROXY), // no 306 (307, TEMPORARY_REDIRECT), // 4xx (400, BAD_REQUEST), (401, UNAUTHORIZED), (402, PAYMENT_REQUIRED), (403, FORBIDDEN), (404, NOT_FOUND), (405, METHOD_NOT_ALLOWED), (406, NOT_ACCEPTABLE), (407, PROXY_AUTHENTICATION_REQUIRED), (408, REQUEST_TIMEOUT), (409, CONFLICT), (410, GONE), (411, LENGTH_REQUIRED), (412, PRECONDITION_FAILED), (413, REQUEST_ENTITY_TOO_LARGE), (414, REQUEST_URI_TOO_LONG), (415, UNSUPPORTED_MEDIA_TYPE), (416, REQUESTED_RANGE_NOT_SATISFIABLE), (417, EXPECTATION_FAILED), (429, TOO_MANY_REQUESTS), // 5xx (500, INTERNAL_SERVER_ERROR), (501, NOT_IMPLEMENTED), (502, BAD_GATEWAY), (503, SERVICE_UNAVAILABLE), (504, GATEWAY_TIMEOUT), (505, HTTP_VERSION_NOT_SUPPORTED), ]; }