diff --git a/.changelog/unreleased/breaking-changes/1362-rpc-by-reqwest.md b/.changelog/unreleased/breaking-changes/1362-rpc-by-reqwest.md new file mode 100644 index 000000000..f9920f46b --- /dev/null +++ b/.changelog/unreleased/breaking-changes/1362-rpc-by-reqwest.md @@ -0,0 +1,10 @@ +- `[tendermint-rpc]` Changed `ErrorDetail` variants + ([\#1362](https://github.com/informalsystems/tendermint-rs/pull/1362)): + * Removed the `Hyper` and `InvalidUri` variants. + * The `Http` variant now has `Error` from `reqwest` as the source. + * Added the `InvalidProxy` variant. + * The `tungstenite` dependency exposed through its `Error` type in + WebSocket-related variants has been updated to version 0.20.x. +- `[tendermint-rpc]` Removed a `TryFrom` conversion for + `hyper::Uri` as hyper is no longer a direct dependency + ([\#1362](https://github.com/informalsystems/tendermint-rs/pull/1362)). diff --git a/.changelog/unreleased/security/1342-rpc-by-reqwest.md b/.changelog/unreleased/security/1342-rpc-by-reqwest.md new file mode 100644 index 000000000..b34eee927 --- /dev/null +++ b/.changelog/unreleased/security/1342-rpc-by-reqwest.md @@ -0,0 +1,3 @@ +- `[tendermint-rpc]` Address the RUSTSEC-2023-0052 vulnerability by dropping + dependency on `hyper-proxy` and changing the HTTP client to use `reqwest` + ([\#1342](https://github.com/informalsystems/tendermint-rs/issues/1342)). diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index f90dcac10..fd1f339e1 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -38,11 +38,7 @@ cli = [ ] http-client = [ "futures", - "http", - "hyper", - "hyper-proxy", - "hyper-rustls", - "tokio/fs", + "reqwest", "tokio/macros", "tracing" ] @@ -50,7 +46,6 @@ secp256k1 = [ "tendermint/secp256k1" ] websocket-client = [ "async-tungstenite", "futures", - "http", "tokio/rt-multi-thread", "tokio/fs", "tokio/macros", @@ -83,17 +78,15 @@ subtle = { version = "2", default-features = false } semver = { version = "1.0", default-features = false } # Optional dependencies -async-tungstenite = { version = "0.20", default-features = false, features = ["tokio-runtime", "tokio-rustls-native-certs"], optional = true } +async-tungstenite = { version = "0.23", default-features = false, features = ["tokio-runtime", "tokio-rustls-native-certs"], optional = true } futures = { version = "0.3", optional = true, default-features = false } -http = { version = "0.2", optional = true, default-features = false } -hyper = { version = "0.14", optional = true, default-features = false, features = ["client", "http1", "http2"] } -hyper-proxy = { version = "0.9.1", optional = true, default-features = false, features = ["rustls"] } -hyper-rustls = { version = "0.22.1", optional = true, default-features = false, features = ["rustls-native-certs", "webpki-roots", "tokio-runtime"] } +reqwest = { version = "0.11.20", optional = true, default-features = false, features = ["rustls-tls-native-roots"] } structopt = { version = "0.3", optional = true, default-features = false } tokio = { version = "1.0", optional = true, default-features = false, features = ["rt-multi-thread"] } tracing = { version = "0.1", optional = true, default-features = false } tracing-subscriber = { version = "0.2", optional = true, default-features = false, features = ["fmt"] } [dev-dependencies] +http = { version = "0.2", default-features = false } lazy_static = { version = "1.4.0", default-features = false } tokio-test = { version = "0.4", default-features = false } diff --git a/rpc/src/client/transport.rs b/rpc/src/client/transport.rs index f8859696b..3eb72ee5f 100644 --- a/rpc/src/client/transport.rs +++ b/rpc/src/client/transport.rs @@ -8,8 +8,16 @@ macro_rules! perform_with_compat { ($self:expr, $request:expr) => {{ let request = $request; match $self.compat { - CompatMode::V0_37 => $self.perform(request).await, - CompatMode::V0_34 => $self.perform_v0_34(request).await, + CompatMode::V0_37 => { + $self + .perform_with_dialect(request, crate::dialect::v0_37::Dialect) + .await + }, + CompatMode::V0_34 => { + $self + .perform_with_dialect(request, crate::dialect::v0_34::Dialect) + .await + }, } }}; } diff --git a/rpc/src/client/transport/auth.rs b/rpc/src/client/transport/auth.rs index 4d6734ff5..28176a561 100644 --- a/rpc/src/client/transport/auth.rs +++ b/rpc/src/client/transport/auth.rs @@ -5,8 +5,8 @@ use alloc::string::{String, ToString}; use core::fmt; -use http::Uri; use subtle_encoding::base64; +use url::Url; /// An HTTP authorization. /// @@ -28,10 +28,10 @@ impl fmt::Display for Authorization { /// /// This authorization can then be supplied to the RPC server via /// the `Authorization` HTTP header. -pub fn authorize(uri: &Uri) -> Option { - let authority = uri.authority()?; +pub fn authorize(url: &Url) -> Option { + let authority = url.authority(); - if let Some((userpass, _)) = authority.as_str().split_once('@') { + if let Some((userpass, _)) = authority.split_once('@') { let bytes = base64::encode(userpass); let credentials = String::from_utf8_lossy(bytes.as_slice()); Some(Authorization::Basic(credentials.to_string())) @@ -42,28 +42,24 @@ pub fn authorize(uri: &Uri) -> Option { #[cfg(test)] mod tests { - use core::str::FromStr; - - use http::Uri; - use super::*; #[test] fn extract_auth_absent() { - let uri = Uri::from_str("http://example.com").unwrap(); + let uri = "http://example.com".parse().unwrap(); assert_eq!(authorize(&uri), None); } #[test] fn extract_auth_username_only() { - let uri = Uri::from_str("http://toto@example.com").unwrap(); + let uri = "http://toto@example.com".parse().unwrap(); let base64 = "dG90bw==".to_string(); assert_eq!(authorize(&uri), Some(Authorization::Basic(base64))); } #[test] fn extract_auth_username_password() { - let uri = Uri::from_str("http://toto:tata@example.com").unwrap(); + let uri = "http://toto:tata@example.com".parse().unwrap(); let base64 = "dG90bzp0YXRh".to_string(); assert_eq!(authorize(&uri), Some(Authorization::Basic(base64))); } diff --git a/rpc/src/client/transport/http.rs b/rpc/src/client/transport/http.rs index ceaa427cd..6bb8f092c 100644 --- a/rpc/src/client/transport/http.rs +++ b/rpc/src/client/transport/http.rs @@ -6,18 +6,25 @@ use core::{ }; use async_trait::async_trait; +use reqwest::{header, Proxy}; use tendermint::{block::Height, evidence::Evidence, Hash}; use tendermint_config::net; +use super::auth; use crate::prelude::*; use crate::{ client::{Client, CompatMode}, - dialect, endpoint, + dialect::{v0_34, Dialect, LatestDialect}, + endpoint, query::Query, + request::RequestMessage, + response::Response, Error, Order, Scheme, SimpleRequest, Url, }; +const USER_AGENT: &str = concat!("tendermint.rs/", env!("CARGO_PKG_VERSION")); + /// A JSON-RPC/HTTP Tendermint RPC client (implements [`crate::Client`]). /// /// Supports both HTTP and HTTPS connections to Tendermint RPC endpoints, and @@ -46,7 +53,8 @@ use crate::{ /// ``` #[derive(Debug, Clone)] pub struct HttpClient { - inner: sealed::HttpClient, + inner: reqwest::Client, + url: reqwest::Url, compat: CompatMode, } @@ -79,27 +87,23 @@ impl Builder { /// Try to create a client with the options specified for this builder. pub fn build(self) -> Result { - match self.proxy_url { - None => Ok(HttpClient { - inner: if self.url.0.is_secure() { - sealed::HttpClient::new_https(self.url.try_into()?) - } else { - sealed::HttpClient::new_http(self.url.try_into()?) - }, - compat: self.compat, - }), - Some(proxy_url) => Ok(HttpClient { - inner: if proxy_url.0.is_secure() { - sealed::HttpClient::new_https_proxy( - self.url.try_into()?, - proxy_url.try_into()?, - )? + let builder = reqwest::ClientBuilder::new().user_agent(USER_AGENT); + let inner = match self.proxy_url { + None => builder.build().map_err(Error::http)?, + Some(proxy_url) => { + let proxy = if self.url.0.is_secure() { + Proxy::https(reqwest::Url::from(proxy_url.0)).map_err(Error::invalid_proxy)? } else { - sealed::HttpClient::new_http_proxy(self.url.try_into()?, proxy_url.try_into()?)? - }, - compat: self.compat, - }), - } + Proxy::http(reqwest::Url::from(proxy_url.0)).map_err(Error::invalid_proxy)? + }; + builder.proxy(proxy).build().map_err(Error::http)? + }, + }; + Ok(HttpClient { + inner, + url: self.url.into(), + compat: self.compat, + }) } } @@ -111,14 +115,7 @@ impl HttpClient { U: TryInto, { let url = url.try_into()?; - Ok(Self { - inner: if url.0.is_secure() { - sealed::HttpClient::new_https(url.try_into()?) - } else { - sealed::HttpClient::new_http(url.try_into()?) - }, - compat: Default::default(), - }) + Self::builder(url).build() } /// Construct a new Tendermint RPC HTTP/S client connecting to the given @@ -157,11 +154,40 @@ impl HttpClient { self.compat = compat; } - async fn perform_v0_34(&self, request: R) -> Result + fn build_request(&self, request: R) -> Result + where + R: RequestMessage, + { + let request_body = request.into_json(); + + tracing::trace!("outgoing request: {}", request_body); + + let mut builder = self + .inner + .post(self.url.clone()) + .header(header::CONTENT_TYPE, "application/json") + .body(request_body.into_bytes()); + + if let Some(auth) = auth::authorize(&self.url) { + builder = builder.header(header::AUTHORIZATION, auth.to_string()); + } + + builder.build().map_err(Error::http) + } + + async fn perform_with_dialect(&self, request: R, _dialect: S) -> Result where - R: SimpleRequest, + R: SimpleRequest, + S: Dialect, { - self.inner.perform(request).await + let request = self.build_request(request)?; + let response = self.inner.execute(request).await.map_err(Error::http)?; + let response_body = response.bytes().await.map_err(Error::http)?; + tracing::trace!( + "incoming response: {}", + String::from_utf8_lossy(&response_body) + ); + R::Response::from_string(&response_body).map(Into::into) } } @@ -171,7 +197,7 @@ impl Client for HttpClient { where R: SimpleRequest, { - self.inner.perform(request).await + self.perform_with_dialect(request, LatestDialect).await } async fn block_results(&self, height: H) -> Result @@ -196,7 +222,7 @@ impl Client for HttpClient { // Back-fill with a request to /block endpoint and // taking just the header from the response. let resp = self - .perform_v0_34(endpoint::block::Request::new(height)) + .perform_with_dialect(endpoint::block::Request::new(height), v0_34::Dialect) .await?; Ok(resp.into()) }, @@ -216,7 +242,10 @@ impl Client for HttpClient { // Back-fill with a request to /block_by_hash endpoint and // taking just the header from the response. let resp = self - .perform_v0_34(endpoint::block_by_hash::Request::new(hash)) + .perform_with_dialect( + endpoint::block_by_hash::Request::new(hash), + v0_34::Dialect, + ) .await?; Ok(resp.into()) }, @@ -228,7 +257,7 @@ impl Client for HttpClient { match self.compat { CompatMode::V0_37 => self.perform(endpoint::evidence::Request::new(e)).await, CompatMode::V0_34 => { - self.perform_v0_34(endpoint::evidence::Request::new(e)) + self.perform_with_dialect(endpoint::evidence::Request::new(e), v0_34::Dialect) .await }, } @@ -318,168 +347,9 @@ impl From for Url { } } -impl TryFrom for hyper::Uri { - type Error = Error; - - fn try_from(value: HttpClientUrl) -> Result { - value.0.to_string().parse().map_err(Error::invalid_uri) - } -} - -mod sealed { - use std::io::Read; - - use http::header::AUTHORIZATION; - use hyper::{ - body::Buf, - client::{connect::Connect, HttpConnector}, - header, Uri, - }; - use hyper_proxy::{Intercept, Proxy, ProxyConnector}; - use hyper_rustls::HttpsConnector; - - use crate::prelude::*; - use crate::{ - client::transport::auth::authorize, dialect::Dialect, Error, Response, SimpleRequest, - }; - - /// A wrapper for a `hyper`-based client, generic over the connector type. - #[derive(Debug, Clone)] - pub struct HyperClient { - uri: Uri, - inner: hyper::Client, - } - - impl HyperClient { - pub fn new(uri: Uri, inner: hyper::Client) -> Self { - Self { uri, inner } - } - } - - impl HyperClient - where - C: Connect + Clone + Send + Sync + 'static, - { - pub async fn perform(&self, request: R) -> Result - where - R: SimpleRequest, - S: Dialect, - { - let request = self.build_request(request)?; - let response = self.inner.request(request).await.map_err(Error::hyper)?; - let response_body = response_to_string(response).await?; - tracing::debug!("Incoming response: {}", response_body); - R::Response::from_string(&response_body).map(Into::into) - } - } - - impl HyperClient { - /// Build a request using the given Tendermint RPC request. - pub fn build_request(&self, request: R) -> Result, Error> - where - R: SimpleRequest, - S: Dialect, - { - let request_body = request.into_json(); - - tracing::debug!("Outgoing request: {}", request_body); - - let mut request = hyper::Request::builder() - .method("POST") - .uri(&self.uri) - .body(hyper::Body::from(request_body.into_bytes())) - .map_err(Error::http)?; - - { - let headers = request.headers_mut(); - headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap()); - headers.insert( - header::USER_AGENT, - format!("tendermint.rs/{}", env!("CARGO_PKG_VERSION")) - .parse() - .unwrap(), - ); - - if let Some(auth) = authorize(&self.uri) { - headers.insert(AUTHORIZATION, auth.to_string().parse().unwrap()); - } - } - - Ok(request) - } - } - - /// We offer several variations of `hyper`-based client. - /// - /// Here we erase the type signature of the underlying `hyper`-based - /// client, allowing the higher-level HTTP client to operate via HTTP or - /// HTTPS, and with or without a proxy. - #[derive(Debug, Clone)] - pub enum HttpClient { - Http(HyperClient), - Https(HyperClient>), - HttpProxy(HyperClient>), - HttpsProxy(HyperClient>>), - } - - impl HttpClient { - pub fn new_http(uri: Uri) -> Self { - Self::Http(HyperClient::new(uri, hyper::Client::new())) - } - - pub fn new_https(uri: Uri) -> Self { - Self::Https(HyperClient::new( - uri, - hyper::Client::builder().build(HttpsConnector::with_native_roots()), - )) - } - - pub fn new_http_proxy(uri: Uri, proxy_uri: Uri) -> Result { - let proxy = Proxy::new(Intercept::All, proxy_uri); - let proxy_connector = - ProxyConnector::from_proxy(HttpConnector::new(), proxy).map_err(Error::io)?; - Ok(Self::HttpProxy(HyperClient::new( - uri, - hyper::Client::builder().build(proxy_connector), - ))) - } - - pub fn new_https_proxy(uri: Uri, proxy_uri: Uri) -> Result { - let proxy = Proxy::new(Intercept::All, proxy_uri); - let proxy_connector = - ProxyConnector::from_proxy(HttpsConnector::with_native_roots(), proxy) - .map_err(Error::io)?; - - Ok(Self::HttpsProxy(HyperClient::new( - uri, - hyper::Client::builder().build(proxy_connector), - ))) - } - - pub async fn perform(&self, request: R) -> Result - where - R: SimpleRequest, - S: Dialect, - { - match self { - HttpClient::Http(c) => c.perform(request).await, - HttpClient::Https(c) => c.perform(request).await, - HttpClient::HttpProxy(c) => c.perform(request).await, - HttpClient::HttpsProxy(c) => c.perform(request).await, - } - } - } - - async fn response_to_string(response: hyper::Response) -> Result { - let mut response_body = String::new(); - hyper::body::aggregate(response.into_body()) - .await - .map_err(Error::hyper)? - .reader() - .read_to_string(&mut response_body) - .map_err(Error::io)?; - - Ok(response_body) +impl From for url::Url { + fn from(url: HttpClientUrl) -> Self { + url.0.into() } } @@ -487,14 +357,13 @@ mod sealed { mod tests { use core::str::FromStr; - use http::{header::AUTHORIZATION, Request, Uri}; - use hyper::Body; + use reqwest::{header::AUTHORIZATION, Request}; - use super::sealed::HyperClient; - use crate::dialect::LatestDialect; + use super::HttpClient; use crate::endpoint::abci_info; + use crate::Url; - fn authorization(req: &Request) -> Option<&str> { + fn authorization(req: &Request) -> Option<&str> { req.headers() .get(AUTHORIZATION) .map(|h| h.to_str().unwrap()) @@ -502,22 +371,18 @@ mod tests { #[test] fn without_basic_auth() { - let uri = Uri::from_str("http://example.com").unwrap(); - let inner = hyper::Client::new(); - let client = HyperClient::new(uri, inner); - let req = - HyperClient::build_request::<_, LatestDialect>(&client, abci_info::Request).unwrap(); + let url = Url::from_str("http://example.com").unwrap(); + let client = HttpClient::new(url).unwrap(); + let req = HttpClient::build_request(&client, abci_info::Request).unwrap(); assert_eq!(authorization(&req), None); } #[test] fn with_basic_auth() { - let uri = Uri::from_str("http://toto:tata@example.com").unwrap(); - let inner = hyper::Client::new(); - let client = HyperClient::new(uri, inner); - let req = - HyperClient::build_request::<_, LatestDialect>(&client, abci_info::Request).unwrap(); + let url = Url::from_str("http://toto:tata@example.com").unwrap(); + let client = HttpClient::new(url).unwrap(); + let req = HttpClient::build_request(&client, abci_info::Request).unwrap(); assert_eq!(authorization(&req), Some("Basic dG90bzp0YXRh")); } diff --git a/rpc/src/client/transport/websocket.rs b/rpc/src/client/transport/websocket.rs index 01ec8950d..ce3738f63 100644 --- a/rpc/src/client/transport/websocket.rs +++ b/rpc/src/client/transport/websocket.rs @@ -25,7 +25,6 @@ use tendermint::{block::Height, Hash}; use tendermint_config::net; use super::router::{SubscriptionId, SubscriptionIdRef}; -use crate::dialect::v0_34; use crate::{ client::{ subscription::SubscriptionTx, @@ -33,6 +32,7 @@ use crate::{ transport::router::{PublishResult, SubscriptionRouter}, Client, CompatMode, }, + dialect::{v0_34, Dialect, LatestDialect}, endpoint::{self, subscribe, unsubscribe}, error::Error, event::{self, Event}, @@ -220,11 +220,12 @@ impl WebSocketClient { } } - async fn perform_v0_34(&self, request: R) -> Result + async fn perform_with_dialect(&self, request: R, dialect: S) -> Result where - R: SimpleRequest, + R: SimpleRequest, + S: Dialect, { - self.inner.perform(request).await + self.inner.perform(request, dialect).await } } @@ -234,7 +235,7 @@ impl Client for WebSocketClient { where R: SimpleRequest, { - self.inner.perform(request).await + self.perform_with_dialect(request, LatestDialect).await } async fn block_results(&self, height: H) -> Result @@ -259,7 +260,7 @@ impl Client for WebSocketClient { // Back-fill with a request to /block endpoint and // taking just the header from the response. let resp = self - .perform_v0_34(endpoint::block::Request::new(height)) + .perform_with_dialect(endpoint::block::Request::new(height), v0_34::Dialect) .await?; Ok(resp.into()) }, @@ -279,7 +280,10 @@ impl Client for WebSocketClient { // Back-fill with a request to /block_by_hash endpoint and // taking just the header from the response. let resp = self - .perform_v0_34(endpoint::block_by_hash::Request::new(hash)) + .perform_with_dialect( + endpoint::block_by_hash::Request::new(hash), + v0_34::Dialect, + ) .await?; Ok(resp.into()) }, @@ -618,7 +622,7 @@ mod sealed { } impl WebSocketClient { - pub async fn perform(&self, request: R) -> Result + pub async fn perform(&self, request: R, _dialect: S) -> Result where R: SimpleRequest, S: Dialect, @@ -650,8 +654,6 @@ mod sealed { fn into_client_request( self, ) -> tungstenite::Result { - let uri = self.to_string().parse::().unwrap(); - let builder = tungstenite::handshake::client::Request::builder() .method("GET") .header("Host", self.host()) @@ -663,14 +665,14 @@ mod sealed { tungstenite::handshake::client::generate_key(), ); - let builder = if let Some(auth) = authorize(&uri) { + let builder = if let Some(auth) = authorize(self.as_ref()) { builder.header("Authorization", auth.to_string()) } else { builder }; builder - .uri(uri) + .uri(self.to_string()) .body(()) .map_err(tungstenite::error::Error::HttpFormat) } diff --git a/rpc/src/dialect.rs b/rpc/src/dialect.rs index 176fe59b9..4d33a409e 100644 --- a/rpc/src/dialect.rs +++ b/rpc/src/dialect.rs @@ -3,6 +3,7 @@ pub mod v0_34; pub mod v0_37; +pub use v0_37::Dialect as LatestDialect; mod begin_block; mod check_tx; @@ -23,8 +24,6 @@ pub trait Dialect: sealed::Sealed + Default + Clone + Send + Sync { type Evidence: From + Serialize + DeserializeOwned + Send; } -pub type LatestDialect = v0_37::Dialect; - mod sealed { pub trait Sealed {} diff --git a/rpc/src/error.rs b/rpc/src/error.rs index c1119708b..84f411c57 100644 --- a/rpc/src/error.rs +++ b/rpc/src/error.rs @@ -6,23 +6,11 @@ use flex_error::{define_error, DefaultTracer, DisplayError, DisplayOnly, ErrorMe use crate::{prelude::*, response_error::ResponseError, rpc_url::Url}; -#[cfg(feature = "http")] -type HttpError = flex_error::DisplayOnly; +#[cfg(feature = "reqwest")] +type ReqwestError = flex_error::DisplayOnly; -#[cfg(not(feature = "http"))] -type HttpError = flex_error::NoSource; - -#[cfg(feature = "http")] -type InvalidUriError = flex_error::DisplayOnly; - -#[cfg(not(feature = "http"))] -type InvalidUriError = flex_error::NoSource; - -#[cfg(feature = "hyper")] -type HyperError = flex_error::DisplayOnly; - -#[cfg(not(feature = "hyper"))] -type HyperError = flex_error::NoSource; +#[cfg(not(feature = "reqwest"))] +type ReqwestError = flex_error::NoSource; #[cfg(feature = "tokio")] type JoinError = flex_error::DisplayOnly; @@ -48,12 +36,12 @@ define_error! { | _ | { "I/O error" }, Http - [ HttpError ] + [ ReqwestError ] | _ | { "HTTP error" }, - Hyper - [ HyperError ] - | _ | { "HTTP error" }, + InvalidProxy + [ ReqwestError ] + | _ | { "Invalid proxy configuration" }, InvalidParams { @@ -136,10 +124,6 @@ define_error! { ) }, - InvalidUri - [ InvalidUriError ] - | _ | { "invalid URI" }, - Tendermint [ tendermint::Error ] | _ | { "tendermint error" }, diff --git a/rpc/src/rpc_url.rs b/rpc/src/rpc_url.rs index 135f123d2..52ee3e0c5 100644 --- a/rpc/src/rpc_url.rs +++ b/rpc/src/rpc_url.rs @@ -49,33 +49,14 @@ impl FromStr for Scheme { pub struct Url { inner: url::Url, scheme: Scheme, - host: String, - port: u16, } impl FromStr for Url { type Err = Error; fn from_str(s: &str) -> Result { - let inner: url::Url = s.parse().map_err(Error::parse_url)?; - - let scheme: Scheme = inner.scheme().parse()?; - - let host = inner - .host_str() - .ok_or_else(|| Error::invalid_params(format!("URL is missing its host: {s}")))? - .to_owned(); - - let port = inner.port_or_known_default().ok_or_else(|| { - Error::invalid_params(format!("cannot determine appropriate port for URL: {s}")) - })?; - - Ok(Self { - inner, - scheme, - host, - port, - }) + let url: url::Url = s.parse().map_err(Error::parse_url)?; + url.try_into() } } @@ -115,12 +96,12 @@ impl Url { /// Get the host associated with this URL. pub fn host(&self) -> &str { - &self.host + self.inner.host_str().unwrap() } /// Get the port associated with this URL. pub fn port(&self) -> u16 { - self.port + self.inner.port_or_known_default().unwrap() } /// Get this URL's path. @@ -135,11 +116,37 @@ impl fmt::Display for Url { } } +impl AsRef for Url { + fn as_ref(&self) -> &url::Url { + &self.inner + } +} + +impl From for url::Url { + fn from(value: Url) -> Self { + value.inner + } +} + impl TryFrom for Url { type Error = crate::Error; - fn try_from(value: url::Url) -> Result { - value.to_string().parse() + fn try_from(url: url::Url) -> Result { + let scheme: Scheme = url.scheme().parse()?; + + if url.host_str().is_none() { + return Err(Error::invalid_params(format!( + "URL is missing its host: {url}" + ))); + } + + if url.port_or_known_default().is_none() { + return Err(Error::invalid_params(format!( + "cannot determine appropriate port for URL: {url}" + ))); + } + + Ok(Self { inner: url, scheme }) } }