diff --git a/src/commands/config/mod.rs b/src/commands/config/mod.rs index fd32333e5..f3d19515f 100644 --- a/src/commands/config/mod.rs +++ b/src/commands/config/mod.rs @@ -46,7 +46,7 @@ pub fn global_config(user: &GlobalUser, verify: bool) -> Result<(), failure::Err // validate_credentials() checks the /user/tokens/verify endpoint (for API token) // or /user endpoint (for global API key) to ensure provided credentials actually work. pub fn validate_credentials(user: &GlobalUser) -> Result<(), failure::Error> { - let client = http::cf_v4_api_client(user, HttpApiClientConfig::default())?; + let client = http::cf_v4_api_client(user, None)?; match user { GlobalUser::TokenAuth { .. } => { diff --git a/src/commands/kv/mod.rs b/src/commands/kv/mod.rs index 8dddad7f8..f1643b869 100644 --- a/src/commands/kv/mod.rs +++ b/src/commands/kv/mod.rs @@ -10,6 +10,7 @@ use crate::settings::global_user::GlobalUser; use crate::settings::toml::Target; use crate::http; +use crate::http::Feature; pub mod bucket; pub mod bulk; @@ -19,15 +20,7 @@ pub mod namespace; // Create a special API client that has a longer timeout than usual, given that KV operations // can be lengthy if payloads are large. fn api_client(user: &GlobalUser) -> Result { - http::cf_v4_api_client( - user, - HttpApiClientConfig { - default_headers: http::headers(None), - // Use 5 minute timeout instead of default 30-second one. - // This is useful for bulk upload operations. - http_timeout: Duration::from_secs(5 * 60), - }, - ) + http::cf_v4_api_client(user, Some(Feature::KV)) } fn format_error(e: ApiFailure) -> String { diff --git a/src/commands/publish/mod.rs b/src/commands/publish/mod.rs index 1a32fa420..e145bf82e 100644 --- a/src/commands/publish/mod.rs +++ b/src/commands/publish/mod.rs @@ -14,6 +14,7 @@ use crate::commands::kv; use crate::commands::kv::bucket::AssetManifest; use crate::commands::subdomain::Subdomain; use crate::http; +use crate::http::Feature; use crate::settings::global_user::GlobalUser; use crate::settings::toml::{DeployConfig, KvNamespace, Site, Target, Zoneless}; use crate::terminal::{emoji, message}; @@ -99,7 +100,7 @@ fn upload_script( ); let client = if target.site.is_some() { - http::auth_client(Some("site"), user) + http::auth_client(Some(Feature::Sites), user) } else { http::auth_client(None, user) }; diff --git a/src/commands/publish/route.rs b/src/commands/publish/route.rs index 99051973b..0a42b9d65 100644 --- a/src/commands/publish/route.rs +++ b/src/commands/publish/route.rs @@ -4,7 +4,6 @@ use serde::Serialize; use cloudflare::endpoints::workers::{CreateRoute, CreateRouteParams, ListRoutes}; use cloudflare::framework::apiclient::ApiClient; -use cloudflare::framework::HttpApiClientConfig; use crate::http::{cf_v4_api_client, format_error}; use crate::settings::global_user::GlobalUser; @@ -29,7 +28,7 @@ pub fn publish_routes( } fn fetch_all(user: &GlobalUser, zone_identifier: &str) -> Result, failure::Error> { - let client = cf_v4_api_client(user, HttpApiClientConfig::default())?; + let client = cf_v4_api_client(user, None)?; let routes: Vec = match client.request(&ListRoutes { zone_identifier }) { Ok(success) => success.result.iter().map(Route::from).collect(), @@ -44,7 +43,7 @@ fn create( zone_identifier: &str, route: &Route, ) -> Result { - let client = cf_v4_api_client(user, HttpApiClientConfig::default())?; + let client = cf_v4_api_client(user, None)?; log::info!("Creating your route {:#?}", &route.pattern,); match client.request(&CreateRoute { diff --git a/src/commands/route/mod.rs b/src/commands/route/mod.rs index 1547304a6..820cceb93 100644 --- a/src/commands/route/mod.rs +++ b/src/commands/route/mod.rs @@ -9,7 +9,7 @@ use crate::settings::global_user::GlobalUser; use crate::terminal::message; pub fn list(zone_identifier: String, user: &GlobalUser) -> Result<(), failure::Error> { - let client = http::cf_v4_api_client(user, HttpApiClientConfig::default())?; + let client = http::cf_v4_api_client(user, None)?; let result = client.request(&ListRoutes { zone_identifier: &zone_identifier, @@ -31,7 +31,7 @@ pub fn delete( user: &GlobalUser, route_id: &str, ) -> Result<(), failure::Error> { - let client = http::cf_v4_api_client(user, HttpApiClientConfig::default())?; + let client = http::cf_v4_api_client(user, None)?; let result = client.request(&DeleteRoute { zone_identifier: &zone_identifier, diff --git a/src/commands/secret/mod.rs b/src/commands/secret/mod.rs index ad80c7ddf..57c8489d2 100644 --- a/src/commands/secret/mod.rs +++ b/src/commands/secret/mod.rs @@ -61,7 +61,7 @@ pub fn create_secret(name: &str, user: &GlobalUser, target: &Target) -> Result<( target.name )); - let client = http::cf_v4_api_client(user, HttpApiClientConfig::default())?; + let client = http::cf_v4_api_client(user, None)?; let params = CreateSecretParams { name: name.to_string(), @@ -103,7 +103,7 @@ pub fn delete_secret(name: &str, user: &GlobalUser, target: &Target) -> Result<( name, target.name )); - let client = http::cf_v4_api_client(user, HttpApiClientConfig::default())?; + let client = http::cf_v4_api_client(user, None)?; let response = client.request(&DeleteSecret { account_identifier: &target.account_id, @@ -121,7 +121,7 @@ pub fn delete_secret(name: &str, user: &GlobalUser, target: &Target) -> Result<( pub fn list_secrets(user: &GlobalUser, target: &Target) -> Result<(), failure::Error> { validate_target(target)?; - let client = http::cf_v4_api_client(user, HttpApiClientConfig::default())?; + let client = http::cf_v4_api_client(user, None)?; let response = client.request(&ListSecrets { account_identifier: &target.account_id, diff --git a/src/http.rs b/src/http.rs deleted file mode 100644 index b96cf84dd..000000000 --- a/src/http.rs +++ /dev/null @@ -1,126 +0,0 @@ -use reqwest::blocking::{Client, ClientBuilder}; -use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; -use reqwest::redirect::Policy; -use std::time::Duration; - -use crate::install; -use crate::settings::global_user::GlobalUser; - -use http::status::StatusCode; - -use cloudflare::framework::auth::Credentials; -use cloudflare::framework::response::ApiFailure; -use cloudflare::framework::{Environment, HttpApiClient, HttpApiClientConfig}; - -use crate::terminal::emoji; -use crate::terminal::message; - -////---------------------------OLD API CLIENT CODE---------------------------//// -// TODO: remove this and replace it entirely with cloudflare-rs -pub fn headers(feature: Option<&str>) -> HeaderMap { - let version = if install::target::DEBUG { - "dev" - } else { - env!("CARGO_PKG_VERSION") - }; - let user_agent = if let Some(feature) = feature { - format!("wrangler/{}/{}", version, feature) - } else { - format!("wrangler/{}", version) - }; - - let mut headers = HeaderMap::new(); - headers.insert(USER_AGENT, HeaderValue::from_str(&user_agent).unwrap()); - headers -} - -fn builder() -> ClientBuilder { - let builder = reqwest::blocking::Client::builder(); - builder - .connect_timeout(Duration::from_secs(10)) - .timeout(Duration::from_secs(30)) -} - -pub fn client(feature: Option<&str>) -> Client { - builder() - .default_headers(headers(feature)) - .build() - .expect("could not create http client") -} - -pub fn auth_client(feature: Option<&str>, user: &GlobalUser) -> Client { - let mut headers = headers(feature); - add_auth_headers(&mut headers, user); - - builder() - .default_headers(headers) - .redirect(Policy::none()) - .build() - .expect("could not create authenticated http client") -} - -fn add_auth_headers<'a>(headers: &'a mut HeaderMap, user: &GlobalUser) { - match user { - GlobalUser::TokenAuth { api_token } => { - headers.insert( - "Authorization", - HeaderValue::from_str(&format!("Bearer {}", &api_token)).unwrap(), - ); - } - GlobalUser::GlobalKeyAuth { email, api_key } => { - headers.insert("X-Auth-Email", HeaderValue::from_str(&email).unwrap()); - headers.insert("X-Auth-Key", HeaderValue::from_str(&api_key).unwrap()); - } - } -} - -////---------------------------NEW API CLIENT CODE---------------------------//// -pub fn cf_v4_api_client( - user: &GlobalUser, - config: HttpApiClientConfig, -) -> Result { - HttpApiClient::new( - Credentials::from(user.to_owned()), - config, - Environment::Production, - ) -} - -// Format errors from the cloudflare-rs cli for printing. -// Optionally takes an argument for providing a function that maps error code numbers to -// helpful additional information about why someone is getting an error message and how to fix it. -pub fn format_error(e: ApiFailure, err_helper: Option<&dyn Fn(u16) -> &'static str>) -> String { - match e { - ApiFailure::Error(status, api_errors) => { - print_status_code_context(status); - let mut complete_err = "".to_string(); - for error in api_errors.errors { - let error_msg = format!("{} Code {}: {}\n", emoji::WARN, error.code, error.message); - - if let Some(annotate_help) = err_helper { - let suggestion_text = annotate_help(error.code); - let help_msg = format!("{} {}\n", emoji::SLEUTH, suggestion_text); - complete_err.push_str(&format!("{}{}", error_msg, help_msg)); - } else { - complete_err.push_str(&error_msg) - } - } - complete_err.trim_end().to_string() // Trimming strings in place for String is apparently not a thing... - } - ApiFailure::Invalid(reqwest_err) => format!("{} Error: {}", emoji::WARN, reqwest_err), - } -} - -// For handling cases where the API gateway returns errors via HTTP status codes -// (no API-specific, more granular error code is given). -fn print_status_code_context(status_code: StatusCode) { - match status_code { - // Folks should never hit PAYLOAD_TOO_LARGE, given that Wrangler ensures that bulk file uploads - // are max ~50 MB in size. This case is handled anyways out of an abundance of caution. - StatusCode::PAYLOAD_TOO_LARGE => - message::warn("Returned status code 413, Payload Too Large. Please make sure your upload is less than 100MB in size"), - StatusCode::GATEWAY_TIMEOUT => - message::warn("Returned status code 504, Gateway Timeout. Please try again in a few seconds"), - _ => (), - } -} diff --git a/src/http/feature.rs b/src/http/feature.rs new file mode 100644 index 000000000..e0081c5da --- /dev/null +++ b/src/http/feature.rs @@ -0,0 +1,36 @@ +use reqwest::header::{HeaderMap, HeaderValue, USER_AGENT}; + +use crate::install; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Feature { + Sites, + KV, +} + +pub(super) fn headers(feature: Option) -> HeaderMap { + let mut headers = HeaderMap::default(); + headers.insert( + USER_AGENT, + HeaderValue::from_str(&get_user_agent(feature)).unwrap(), + ); + headers +} + +fn get_user_agent(feature: Option) -> String { + let version = if install::target::DEBUG { + "dev" + } else { + env!("CARGO_PKG_VERSION") + }; + + let mut agent = format!("wrangler/{}", version); + if let Some(feature) = feature { + agent.push_str("/"); + match feature { + Feature::Sites => agent.push_str("sites"), + Feature::KV => agent.push_str("kv"), + } + } + agent +} diff --git a/src/http/legacy.rs b/src/http/legacy.rs new file mode 100644 index 000000000..ec2ed1481 --- /dev/null +++ b/src/http/legacy.rs @@ -0,0 +1,48 @@ +use reqwest::blocking::{Client, ClientBuilder}; +use reqwest::header::{HeaderMap, HeaderValue}; +use reqwest::redirect::Policy; +use std::time::Duration; + +use crate::http::{feature::headers, Feature}; +use crate::settings::global_user::GlobalUser; + +// TODO: remove this and replace it entirely with cloudflare-rs +pub fn client(feature: Option) -> Client { + builder() + .default_headers(headers(feature)) + .build() + .expect("could not create http client") +} + +pub fn auth_client(feature: Option, user: &GlobalUser) -> Client { + let mut headers = headers(feature); + add_auth_headers(&mut headers, user); + + builder() + .default_headers(headers) + .redirect(Policy::none()) + .build() + .expect("could not create authenticated http client") +} + +fn builder() -> ClientBuilder { + let builder = reqwest::blocking::Client::builder(); + builder + .connect_timeout(Duration::from_secs(10)) + .timeout(Duration::from_secs(30)) +} + +fn add_auth_headers<'a>(headers: &'a mut HeaderMap, user: &GlobalUser) { + match user { + GlobalUser::TokenAuth { api_token } => { + headers.insert( + "Authorization", + HeaderValue::from_str(&format!("Bearer {}", &api_token)).unwrap(), + ); + } + GlobalUser::GlobalKeyAuth { email, api_key } => { + headers.insert("X-Auth-Email", HeaderValue::from_str(&email).unwrap()); + headers.insert("X-Auth-Key", HeaderValue::from_str(&api_key).unwrap()); + } + } +} diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 000000000..0d2913723 --- /dev/null +++ b/src/http/mod.rs @@ -0,0 +1,7 @@ +pub(self) mod feature; +pub(self) mod legacy; +pub(self) mod v4; + +pub use feature::Feature; +pub use legacy::{auth_client, client}; +pub use v4::{cf_v4_api_client, format_error}; diff --git a/src/http/v4.rs b/src/http/v4.rs new file mode 100644 index 000000000..698144f6b --- /dev/null +++ b/src/http/v4.rs @@ -0,0 +1,72 @@ +use std::time::Duration; + +use cloudflare::framework::auth::Credentials; +use cloudflare::framework::response::ApiFailure; +use cloudflare::framework::{Environment, HttpApiClient, HttpApiClientConfig}; +use http::StatusCode; + +use crate::http::{feature::headers, Feature}; +use crate::settings::global_user::GlobalUser; +use crate::terminal::{emoji, message}; + +pub fn cf_v4_api_client( + user: &GlobalUser, + feature: Option, +) -> Result { + let mut http_timeout = Duration::from_secs(30); + if let Some(feature) = feature { + // Use 5 minute timeout instead of default 30-second one. + // This is useful for bulk upload operations. + match feature { + Feature::KV | Feature::Sites => { + http_timeout = Duration::from_secs(5 * 60); + } + } + } + let config = HttpApiClientConfig { + http_timeout, + default_headers: headers(feature), + }; + HttpApiClient::new( + Credentials::from(user.to_owned()), + config, + Environment::Production, + ) +} + +// Format errors from the cloudflare-rs cli for printing. +// Optionally takes an argument for providing a function that maps error code numbers to +// helpful additional information about why someone is getting an error message and how to fix it. +pub fn format_error(e: ApiFailure, err_helper: Option<&dyn Fn(u16) -> &'static str>) -> String { + match e { + ApiFailure::Error(status, api_errors) => { + print_status_code_context(status); + let mut complete_err = "".to_string(); + for error in api_errors.errors { + let error_msg = format!("{} Code {}: {}\n", emoji::WARN, error.code, error.message); + + if let Some(annotate_help) = err_helper { + let suggestion_text = annotate_help(error.code); + let help_msg = format!("{} {}\n", emoji::SLEUTH, suggestion_text); + complete_err.push_str(&format!("{}{}", error_msg, help_msg)); + } else { + complete_err.push_str(&error_msg) + } + } + complete_err.trim_end().to_string() // Trimming strings in place for String is apparently not a thing... + } + ApiFailure::Invalid(reqwest_err) => format!("{} Error: {}", emoji::WARN, reqwest_err), + } +} + +// For handling cases where the API gateway returns errors via HTTP status codes +// (no API-specific, more granular error code is given). +fn print_status_code_context(status_code: StatusCode) { + match status_code { + // Folks should never hit PAYLOAD_TOO_LARGE, given that Wrangler ensures that bulk file uploads + // are max ~50 MB in size. This case is handled anyways out of an abundance of caution. + StatusCode::PAYLOAD_TOO_LARGE => message::warn("Returned status code 413, Payload Too Large. Please make sure your upload is less than 100MB in size"), + StatusCode::GATEWAY_TIMEOUT => message::warn("Returned status code 504, Gateway Timeout. Please try again in a few seconds"), + _ => (), + } +}