diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac6594650..9e8ae2361 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,7 +66,7 @@ jobs: if: matrix.build == 'linux' run: | rustup target add ${{ env.LINUX_TARGET }} - cargo build --release --target ${{ env.LINUX_TARGET }} --features vendored-openssl + cargo build --release --target ${{ env.LINUX_TARGET }} - name: Build (MacOS) if: matrix.build == 'macos' diff --git a/Cargo.lock b/Cargo.lock index 4c5041dcf..35de9cce9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -615,6 +615,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "eventual" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9bda6d089b434ca50f3d6feb5fca421309b8bac97b8be9af51cff879fa3f54b" +dependencies = [ + "log 0.3.9", + "syncbox", + "time", +] + [[package]] name = "exitfailure" version = "0.5.1" @@ -2134,6 +2145,16 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syncbox" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05bc2b72659ac27a2d0e7c4166c8596578197c4c41f767deab12c81f523b85c7" +dependencies = [ + "log 0.3.9", + "time", +] + [[package]] name = "synstructure" version = "0.12.4" @@ -2754,6 +2775,7 @@ dependencies = [ "console 0.11.3", "dirs 3.0.1", "env_logger", + "eventual", "exitfailure", "failure", "flate2", diff --git a/Cargo.toml b/Cargo.toml index 690f59813..e711b91a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ config = "0.10.1" console = "0.11.3" dirs = "3.0.1" env_logger = "0.7.1" +eventual = "0.1.7" exitfailure = "0.5.1" failure = "0.1.8" flate2 = "1.0.16" @@ -37,7 +38,7 @@ lazy_static = "1.4.0" log = "0.4.11" notify = "4.0.15" number_prefix = "0.4.0" -openssl = { version = '0.10.29', optional = true } +openssl = { version = "0.10.29", optional = true } percent-encoding = "2.1.0" predicates = "1.0.5" prettytable-rs = "0.8.0" @@ -67,10 +68,12 @@ fs_extra = "1.1.0" predicates = "1.0.5" [features] -# We build Linux binaries with vendored openssl -# to avoid SSL issues +# OpenSSL is vendored by default, can use system OpenSSL through feature flag. +default = ['openssl/vendored'] +# Keeping feature for users already using this feature flag vendored-openssl = ['openssl/vendored'] +sys-openssl = ['openssl'] # Treat compiler warnings as a build error. # This only runs in CI by default -strict = [] +strict = ['openssl/vendored'] diff --git a/README.md b/README.md index 7a58e5fc6..e09d1de8f 100644 --- a/README.md +++ b/README.md @@ -88,9 +88,14 @@ $ wrangler publish Additionally, you can configure different [environments](https://developers.cloudflare.com/workers/tooling/wrangler/configuration/environments). + +### 🔓 `login` + + Authenticate Wrangler with your Cloudflare login. This will prompt you with a Cloudflare account login page and is the alternative to `wrangler config`. + ### 🔧 `config` - Configure your global Cloudflare user. This is an interactive command that will prompt you for your API token: + Authenticate Wrangler with a Cloudflare API Token. This is an interactive command that will prompt you for your API token: ```bash wrangler config diff --git a/src/commands/login.rs b/src/commands/login.rs new file mode 100644 index 000000000..92a44e8f0 --- /dev/null +++ b/src/commands/login.rs @@ -0,0 +1,5 @@ +use crate::login; + +pub fn run() -> Result<(), failure::Error> { + login::run() +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index b76bb3c3f..04f455593 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -6,6 +6,7 @@ pub mod dev; pub mod generate; pub mod init; pub mod kv; +pub mod login; mod preview; pub mod publish; pub mod route; diff --git a/src/lib.rs b/src/lib.rs index d7e7c1af8..95f219bf8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ pub mod http; pub mod install; pub mod installer; pub mod kv; +pub mod login; pub mod settings; pub mod sites; pub mod tail; diff --git a/src/login/mod.rs b/src/login/mod.rs new file mode 100644 index 000000000..06e61485e --- /dev/null +++ b/src/login/mod.rs @@ -0,0 +1,103 @@ +use eventual::Timer; +use indicatif::{ProgressBar, ProgressStyle}; +use openssl::base64; +use openssl::rsa::{Padding, Rsa}; +use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; +use serde::Deserialize; +use std::collections::HashMap; +use std::str; + +use crate::commands::config::global_config; +use crate::settings::global_user::GlobalUser; +use crate::terminal::{interactive, open_browser}; + +pub fn run() -> Result<(), failure::Error> { + let rsa = Rsa::generate(1024)?; + let pubkey = rsa.public_key_to_pem_pkcs1()?; + + // Convert key to string and remove header and footer + let pubkey_str = str::from_utf8(&pubkey)?; + let pubkey_filtered = pubkey_str + .lines() + .filter(|line| !line.starts_with("---")) + .fold(String::new(), |mut data, line| { + data.push_str(&line); + data + }); + let pubkey_encoded = percent_encode(pubkey_filtered.as_bytes(), NON_ALPHANUMERIC).to_string(); + + let browser_permission = + interactive::confirm("Allow Wrangler to open a page in your browser?")?; + if !browser_permission { + failure::bail!("In order to log in you must allow Wrangler to open your browser. If you don't want to do this consider using `wrangler config`"); + } + + open_browser(&format!( + "https://dash.cloudflare.com/wrangler?key={0}", + pubkey_encoded + ))?; + + let encrypted_token_str = poll_token(pubkey_filtered)?; + let encrypted_token = base64::decode_block(encrypted_token_str.as_str())?; + let mut token_bytes: [u8; 128] = [0; 128]; + + rsa.private_decrypt(&encrypted_token, &mut token_bytes, Padding::PKCS1)?; + let token = str::from_utf8(&token_bytes)?.trim_matches(char::from(0)); + + let user = GlobalUser::TokenAuth { + api_token: token.to_string(), + }; + global_config(&user, true)?; + + Ok(()) +} + +#[derive(Deserialize)] +struct TokenResponse { + result: String, +} + +/// Poll for token, bail after 500 seconds. +fn poll_token(token_id: String) -> Result { + let mut request_params = HashMap::new(); + request_params.insert("token-id", token_id); + + let client = reqwest::blocking::Client::new(); + let timer = Timer::new().interval_ms(1000).iter(); + + let style = ProgressStyle::default_spinner().template("{spinner} {msg}"); + let spinner = ProgressBar::new_spinner().with_style(style); + spinner.set_message("Waiting for API token..."); + spinner.enable_steady_tick(20); + + for (seconds, _) in timer.enumerate() { + let res = client + .get("https://api.cloudflare.com/client/v4/workers/token") + .json(&request_params) + .send()?; + + if res.status().is_success() { + let body: TokenResponse = res.json()?; + return Ok(body.result); + } + + if seconds >= 500 { + break; + } + } + + failure::bail!( + "Timed out while waiting for API token. Try using `wrangler config` if login fails to work." + ); +} + +#[cfg(test)] +mod tests { + use openssl::rsa::Rsa; + + #[test] + fn test_rsa() { + let rsa = Rsa::generate(1024).unwrap(); + rsa.public_key_to_pem_pkcs1().unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs index ea505c4f8..709bce87e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -550,7 +550,7 @@ fn run() -> Result<(), failure::Error> { .subcommand( SubCommand::with_name("config") .about(&*format!( - "{} Set up wrangler with your Cloudflare account", + "{} Authenticate Wrangler with a Cloudflare API Token or Global API Key", emoji::SLEUTH )) .arg( @@ -615,6 +615,9 @@ fn run() -> Result<(), failure::Error> { ) .arg(verbose_arg.clone()) ) + .subcommand( + SubCommand::with_name("login") + .about(&*format!("{} Authenticate Wrangler with your Cloudflare username and password", emoji::UNLOCKED))) .get_matches(); let mut is_preview = false; @@ -1069,6 +1072,8 @@ fn run() -> Result<(), failure::Error> { let verbose = matches.is_present("verbose"); commands::tail::start(&target, &user, tunnel_port, metrics_port, verbose)?; + } else if matches.subcommand_matches("login").is_some() { + commands::login::run()?; } Ok(()) } diff --git a/src/preview/mod.rs b/src/preview/mod.rs index 8597c383c..afe8ab278 100644 --- a/src/preview/mod.rs +++ b/src/preview/mod.rs @@ -10,7 +10,6 @@ pub use request_payload::RequestPayload; mod upload; pub use upload::upload; -use std::process::Command; use std::sync::mpsc::channel; use std::thread; @@ -22,7 +21,7 @@ use crate::build; use crate::http; use crate::settings::global_user::GlobalUser; use crate::settings::toml::Target; -use crate::terminal::message; +use crate::terminal::{message, open_browser}; use crate::watch::watch_and_build; pub fn preview( @@ -93,22 +92,6 @@ pub struct PreviewOpt { pub headless: bool, } -fn open_browser(url: &str) -> Result<(), failure::Error> { - let _output = if cfg!(target_os = "windows") { - let url_escaped = url.replace("&", "^&"); - let windows_cmd = format!("start {}", url_escaped); - Command::new("cmd").args(&["/C", &windows_cmd]).output()? - } else if cfg!(target_os = "linux") { - let linux_cmd = format!(r#"xdg-open "{}""#, url); - Command::new("sh").arg("-c").arg(&linux_cmd).output()? - } else { - let mac_cmd = format!(r#"open "{}""#, url); - Command::new("sh").arg("-c").arg(&mac_cmd).output()? - }; - - Ok(()) -} - fn client_request(payload: &RequestPayload, script_id: &str, sites_preview: bool) { let client = http::client(); diff --git a/src/terminal/browser.rs b/src/terminal/browser.rs new file mode 100644 index 000000000..a8ce5bbba --- /dev/null +++ b/src/terminal/browser.rs @@ -0,0 +1,17 @@ +use std::process::Command; + +pub fn open_browser(url: &str) -> Result<(), failure::Error> { + let _output = if cfg!(target_os = "windows") { + let url_escaped = url.replace("&", "^&"); + let windows_cmd = format!("start {}", url_escaped); + Command::new("cmd").args(&["/C", &windows_cmd]).output()? + } else if cfg!(target_os = "linux") { + let linux_cmd = format!(r#"xdg-open "{}""#, url); + Command::new("sh").arg("-c").arg(&linux_cmd).output()? + } else { + let mac_cmd = format!(r#"open "{}""#, url); + Command::new("sh").arg("-c").arg(&mac_cmd).output()? + }; + + Ok(()) +} diff --git a/src/terminal/emoji.rs b/src/terminal/emoji.rs index 95fbbf8f0..ed6bbe166 100644 --- a/src/terminal/emoji.rs +++ b/src/terminal/emoji.rs @@ -36,3 +36,4 @@ pub static UP: Emoji = Emoji("🆙 ", ""); pub static WARN: Emoji = Emoji("⚠️ ", ""); pub static WAVING: Emoji = Emoji("👋 ", ""); pub static WORKER: Emoji = Emoji("👷 ", ""); +pub static UNLOCKED: Emoji = Emoji("🔓", ""); diff --git a/src/terminal/mod.rs b/src/terminal/mod.rs index 73dff93cc..a4187af9e 100644 --- a/src/terminal/mod.rs +++ b/src/terminal/mod.rs @@ -1,4 +1,6 @@ +mod browser; pub mod emoji; pub mod interactive; pub mod message; pub mod styles; +pub use browser::open_browser;