From ddf481467ddcbb77c27537f11c175eb2a326a7f3 Mon Sep 17 00:00:00 2001 From: ducaale Date: Sat, 11 Sep 2021 22:44:03 +0300 Subject: [PATCH 01/21] implement middleware system this is inspired by https://truelayer.com/blog/adding-middleware-support-to-rust-reqwest --- src/middleware.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/middleware.rs diff --git a/src/middleware.rs b/src/middleware.rs new file mode 100644 index 00000000..94bc2e5e --- /dev/null +++ b/src/middleware.rs @@ -0,0 +1,51 @@ +use anyhow::Result; +use reqwest::blocking::{Client, Request, Response}; + +pub struct Next<'a, 'b> { + client: &'a Client, + middlewares: &'a mut [Box], +} + +impl<'a, 'b> Next<'a, 'b> { + fn new(client: &'a Client, middlewares: &'a mut [Box]) -> Self { + Next { + client, + middlewares, + } + } + + pub fn run(&mut self, request: Request) -> Result { + match self.middlewares { + [] => Ok(self.client.execute(request)?), + [ref mut head, tail @ ..] => head.handle(request, Next::new(self.client, tail)), + } + } +} + +pub trait Middleware { + fn handle(&mut self, request: Request, next: Next) -> Result; +} + +pub struct ClientWithMiddleware<'a> { + client: &'a Client, + middlewares: Vec>, +} + +impl<'a> ClientWithMiddleware<'a> { + pub fn new(client: &'a Client) -> Self { + ClientWithMiddleware { + client, + middlewares: vec![], + } + } + + pub fn with(mut self, middleware: impl Middleware + 'a) -> Self { + self.middlewares.push(Box::new(middleware)); + self + } + + pub fn execute(&mut self, request: Request) -> Result { + let mut next = Next::new(self.client, &mut self.middlewares[..]); + next.run(request) + } +} From 2dd3cd675365ff49bd6f70dcca9c9374cbe4ebe0 Mon Sep 17 00:00:00 2001 From: ducaale Date: Sat, 11 Sep 2021 22:44:47 +0300 Subject: [PATCH 02/21] convert RedirectFollower to a middleware --- src/cli.rs | 2 +- src/main.rs | 61 ++++++++++++++++++++++++++++++------------------- src/printer.rs | 38 ++++-------------------------- src/redirect.rs | 22 ++++++++++-------- 4 files changed, 56 insertions(+), 67 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 403461a9..4df78718 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -724,7 +724,7 @@ impl Theme { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Print { pub request_headers: bool, pub request_body: bool, diff --git a/src/main.rs b/src/main.rs index 4f344455..68b472c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod buffer; mod cli; mod download; mod formatting; +mod middleware; mod printer; mod redirect; mod request_items; @@ -20,6 +21,7 @@ use std::sync::Arc; use anyhow::{anyhow, Context, Result}; use atty::Stream; +use redirect::RedirectFollower; use reqwest::blocking::Client; use reqwest::header::{ HeaderValue, ACCEPT, ACCEPT_ENCODING, AUTHORIZATION, CONNECTION, CONTENT_TYPE, COOKIE, RANGE, @@ -30,6 +32,7 @@ use crate::auth::{auth_from_netrc, parse_auth, read_netrc}; use crate::buffer::Buffer; use crate::cli::{BodyType, Cli, Print, Proxy, Verify}; use crate::download::{download_file, get_file_size}; +use crate::middleware::ClientWithMiddleware; use crate::printer::Printer; use crate::request_items::{Body, FORM_CONTENT_TYPE, JSON_ACCEPT, JSON_CONTENT_TYPE}; use crate::session::Session; @@ -373,31 +376,42 @@ fn run(args: Cli) -> Result { ), }; let pretty = args.pretty.unwrap_or_else(|| buffer.guess_pretty()); - let mut printer = Printer::new(print.clone(), pretty, args.style, args.stream, buffer); + let mut printer = Printer::new(pretty, args.style, args.stream, buffer); - printer.print_request_headers(&request, &*cookie_jar)?; - printer.print_request_body(&mut request)?; + if print.request_headers { + printer.print_request_headers(&request, &*cookie_jar)?; + } + if print.request_body { + printer.print_request_body(&mut request)?; + } if !args.offline { - let response = if args.follow { - let mut client = - redirect::RedirectFollower::new(&client, args.max_redirects.unwrap_or(10)); - if let Some(history_print) = args.history_print { - printer.print = history_print; - } - if args.all { - client.on_redirect(|prev_response, next_request| { - printer.print_response_headers(&prev_response)?; - printer.print_response_body(prev_response)?; - printer.print_separator()?; - printer.print_request_headers(next_request, &*cookie_jar)?; - printer.print_request_body(next_request)?; - Ok(()) - }); + let response = { + let history_print = args.history_print.unwrap_or(print); + let mut client = ClientWithMiddleware::new(&client); + if args.follow { + let mut redirect_follower = RedirectFollower::new(args.max_redirects.unwrap_or(10)); + if args.all { + redirect_follower.on_redirect(|prev_response, next_request| { + if history_print.response_headers { + printer.print_response_headers(&prev_response)?; + } + if history_print.response_body { + printer.print_response_body(prev_response)?; + printer.print_separator()?; + } + if history_print.request_headers { + printer.print_request_headers(next_request, &*cookie_jar)?; + } + if history_print.request_body { + printer.print_request_body(next_request)?; + } + Ok(()) + }); + } + client = client.with(redirect_follower); } client.execute(request)? - } else { - client.execute(request)? }; let status = response.status(); @@ -413,8 +427,9 @@ fn run(args: Cli) -> Result { warn(&format!("HTTP {}", status)); } - printer.print = print; - printer.print_response_headers(&response)?; + if print.response_headers { + printer.print_response_headers(&response)?; + } if args.download { if exit_code == 0 { download_file( @@ -426,7 +441,7 @@ fn run(args: Cli) -> Result { args.quiet, )?; } - } else { + } else if print.response_body { printer.print_response_body(response)?; } } diff --git a/src/printer.rs b/src/printer.rs index 25e02c6c..835b01be 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -13,7 +13,7 @@ use termcolor::WriteColor; use crate::{ buffer::Buffer, - cli::{Pretty, Print, Theme}, + cli::{Pretty, Theme}, formatting::{get_json_formatter, Highlighter}, utils::{copy_largebuf, test_mode, BUFFER_SIZE}, }; @@ -66,7 +66,6 @@ impl<'a, T: Read> BinaryGuard<'a, T> { } pub struct Printer { - pub print: Print, indent_json: bool, color: bool, theme: Theme, @@ -76,17 +75,10 @@ pub struct Printer { } impl Printer { - pub fn new( - print: Print, - pretty: Pretty, - theme: Option, - stream: bool, - buffer: Buffer, - ) -> Self { + pub fn new(pretty: Pretty, theme: Option, stream: bool, buffer: Buffer) -> Self { let theme = theme.unwrap_or(Theme::auto); Printer { - print, indent_json: pretty.format(), sort_headers: pretty.format(), color: pretty.color() && (cfg!(test) || buffer.supports_color()), @@ -291,13 +283,8 @@ impl Printer { header_string } - // Each of the print_* functions adds an extra line separator at the end - // except for print_response_body. We are using this function when we have - // something to print after the response body. pub fn print_separator(&mut self) -> io::Result<()> { - if self.print.response_body { - self.buffer.print("\n")?; - } + self.buffer.print("\n")?; Ok(()) } @@ -305,10 +292,6 @@ impl Printer { where T: CookieStore, { - if !self.print.request_headers { - return Ok(()); - } - let method = request.method(); let url = request.url(); let query_string = url.query().map_or(String::from(""), |q| ["?", q].concat()); @@ -357,10 +340,6 @@ impl Printer { } pub fn print_response_headers(&mut self, response: &Response) -> io::Result<()> { - if !self.print.response_headers { - return Ok(()); - } - let version = response.version(); let status = response.status(); let headers = response.headers(); @@ -374,10 +353,6 @@ impl Printer { } pub fn print_request_body(&mut self, request: &mut Request) -> anyhow::Result<()> { - if !self.print.request_body { - return Ok(()); - } - let content_type = get_content_type(request.headers()); if let Some(body) = request.body_mut() { let body = body.buffer()?; @@ -394,10 +369,6 @@ impl Printer { } pub fn print_response_body(&mut self, mut response: Response) -> anyhow::Result<()> { - if !self.print.response_body { - return Ok(()); - } - let content_type = get_content_type(response.headers()); if !self.buffer.is_terminal() { if (self.color || self.indent_json) && content_type.is_text() { @@ -552,7 +523,7 @@ mod tests { let buffer = Buffer::new(args.download, args.output.as_deref(), is_stdout_tty, None).unwrap(); let pretty = args.pretty.unwrap_or_else(|| buffer.guess_pretty()); - Printer::new("hHbB".parse().unwrap(), pretty, args.style, false, buffer) + Printer::new(pretty, args.style, false, buffer) } fn temp_path() -> String { @@ -634,7 +605,6 @@ mod tests { #[test] fn test_header_casing() { let p = Printer { - print: "hHbB".parse().unwrap(), indent_json: false, color: false, theme: Theme::auto, diff --git a/src/redirect.rs b/src/redirect.rs index c17d442a..d154d7d8 100644 --- a/src/redirect.rs +++ b/src/redirect.rs @@ -1,5 +1,4 @@ use anyhow::{anyhow, Result}; -use reqwest::blocking::Client; use reqwest::blocking::{Request, Response}; use reqwest::header::{ HeaderMap, AUTHORIZATION, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE, COOKIE, LOCATION, @@ -7,22 +6,22 @@ use reqwest::header::{ }; use reqwest::{Method, StatusCode, Url}; -pub struct RedirectFollower<'a, T> +use crate::middleware::{Middleware, Next}; + +pub struct RedirectFollower where T: FnMut(Response, &mut Request) -> Result<()>, { - client: &'a Client, max_redirects: usize, callback: Option, } -impl<'a, T> RedirectFollower<'a, T> +impl RedirectFollower where T: FnMut(Response, &mut Request) -> Result<()>, { - pub fn new(client: &'a Client, max_redirects: usize) -> Self { + pub fn new(max_redirects: usize) -> Self { RedirectFollower { - client, max_redirects, callback: None, } @@ -31,12 +30,17 @@ where pub fn on_redirect(&mut self, callback: T) { self.callback = Some(callback); } +} - pub fn execute(&mut self, mut first_request: Request) -> Result { +impl Middleware for RedirectFollower +where + T: FnMut(Response, &mut Request) -> Result<()>, +{ + fn handle(&mut self, mut first_request: Request, mut next: Next) -> Result { // This buffers the body in case we need it again later // reqwest does *not* do this, it ignores 307/308 with a streaming body let mut request = clone_request(&mut first_request)?; - let mut response = self.client.execute(first_request)?; + let mut response = next.run(first_request)?; let mut remaining_redirects = self.max_redirects - 1; while let Some(mut next_request) = get_next_request(request, &response) { @@ -52,7 +56,7 @@ where callback(response, &mut next_request)?; } request = clone_request(&mut next_request)?; - response = self.client.execute(next_request)?; + response = next.run(next_request)?; } Ok(response) From 8674769a98a41e9e36509f348bc43ddf9de987f7 Mon Sep 17 00:00:00 2001 From: ducaale Date: Sun, 12 Sep 2021 22:54:10 +0300 Subject: [PATCH 03/21] implement digest-auth middleware --- Cargo.lock | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 1 + src/auth.rs | 53 +++++++++++++++++++++++++++-- src/main.rs | 6 +++- 4 files changed, 152 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08980949..24690ea9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -349,6 +349,15 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "blocking" version = "1.0.2" @@ -563,6 +572,15 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.2.1" @@ -641,6 +659,28 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest_auth" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa30657988b2ced88f68fe490889e739bf98d342916c33ed3100af1d6f1cbc9c" +dependencies = [ + "digest", + "hex", + "md-5", + "rand", + "sha2", +] + [[package]] name = "dirs" version = "1.0.5" @@ -876,6 +916,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getopts" version = "0.2.21" @@ -969,6 +1019,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "0.2.4" @@ -1266,9 +1322,9 @@ checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" [[package]] name = "libc" -version = "0.2.95" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" +checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" [[package]] name = "libnghttp2-sys" @@ -1323,6 +1379,17 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + [[package]] name = "memchr" version = "2.4.0" @@ -1491,6 +1558,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openssl" version = "0.10.34" @@ -2144,6 +2217,19 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +[[package]] +name = "sha2" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + [[package]] name = "shell-escape" version = "0.1.5" @@ -2625,6 +2711,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "typenum" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" + [[package]] name = "unicase" version = "2.6.0" @@ -2928,6 +3020,7 @@ dependencies = [ "cookie 0.15.0", "cookie_store 0.15.0", "curl", + "digest_auth", "dirs 3.0.2", "encoding_rs", "encoding_rs_io", diff --git a/Cargo.toml b/Cargo.toml index c486c08b..15c0026c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ base64 = "0.13" cookie_crate = { version = "0.15", package = "cookie" } cookie_store = { version = "0.15.0" } reqwest_cookie_store = { version = "0.2.0" } +digest_auth = "0.3.0" dirs = "3.0.1" encoding_rs = "0.8.28" encoding_rs_io = "0.1.7" diff --git a/src/auth.rs b/src/auth.rs index bfe50593..79390699 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,11 +1,27 @@ use std::env; +use std::fs; use std::io; use std::path::PathBuf; -use crate::regex; +use anyhow::Result; use dirs::home_dir; use netrc_rs::Netrc; -use std::fs; +use reqwest::blocking::{Request, Response}; +use reqwest::header::{HeaderValue, AUTHORIZATION, WWW_AUTHENTICATE}; +use reqwest::StatusCode; + +use crate::middleware::{Middleware, Next}; +use crate::regex; + +// TODO: move this to utils.rs +fn clone_request(request: &mut Request) -> Result { + if let Some(b) = request.body_mut().as_mut() { + b.buffer()?; + } + // This doesn't copy the contents of the buffer, cloning requests is cheap + // https://docs.rs/bytes/1.0.1/bytes/struct.Bytes.html + Ok(request.try_clone().unwrap()) // guaranteed to not fail if body is already buffered +} pub fn parse_auth(auth: String, host: &str) -> io::Result<(String, Option)> { if let Some(cap) = regex!(r"^([^:]*):$").captures(&auth) { @@ -82,6 +98,39 @@ pub fn auth_from_netrc(machine: &str, netrc: &str) -> Option<(String, Option { + username: &'a str, + password: &'a str, +} + +impl<'a> DigestAuth<'a> { + pub fn new(username: &'a str, password: &'a str) -> Self { + DigestAuth { username, password } + } +} + +impl<'a> Middleware for DigestAuth<'a> { + fn handle(&mut self, mut request: Request, mut next: Next) -> Result { + let response = next.run(clone_request(&mut request)?)?; + match response.headers().get(WWW_AUTHENTICATE) { + Some(wwwauth) if response.status() == StatusCode::UNAUTHORIZED => { + let context = digest_auth::AuthContext::new( + self.username, + self.password, + request.url().path(), + ); + let mut prompt = digest_auth::parse(wwwauth.to_str()?)?; + let answer = prompt.respond(&context)?.to_header_string(); + request + .headers_mut() + .insert(AUTHORIZATION, HeaderValue::from_str(&answer)?); + Ok(next.run(request)?) + } + _ => Ok(response), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/main.rs b/src/main.rs index 68b472c4..cfcd06bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,7 @@ use reqwest::header::{ USER_AGENT, }; -use crate::auth::{auth_from_netrc, parse_auth, read_netrc}; +use crate::auth::{auth_from_netrc, parse_auth, read_netrc, DigestAuth}; use crate::buffer::Buffer; use crate::cli::{BodyType, Cli, Print, Proxy, Verify}; use crate::download::{download_file, get_file_size}; @@ -411,6 +411,10 @@ fn run(args: Cli) -> Result { } client = client.with(redirect_follower); } + // TODO: + // 1. get username and password from user + // 2. print intermediate intermediary requests/responses if --all flag is used + client = client.with(DigestAuth::new("username", "password")); client.execute(request)? }; From e7ea268190be8adfe769b18f659bcfc462b9e53b Mon Sep 17 00:00:00 2001 From: ducaale Date: Sun, 12 Sep 2021 23:04:33 +0300 Subject: [PATCH 04/21] add some todos --- src/main.rs | 1 + src/session.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main.rs b/src/main.rs index cfcd06bf..d44fcf4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -320,6 +320,7 @@ fn run(args: Cli) -> Result { } } + // TODO: handle digest auth if let Some(auth) = args.auth { let (username, password) = parse_auth(auth, args.url.host_str().unwrap_or(""))?; if let Some(ref mut s) = session { diff --git a/src/session.rs b/src/session.rs index 7dfe7e77..fb7c52b1 100644 --- a/src/session.rs +++ b/src/session.rs @@ -115,6 +115,7 @@ impl Session { Ok(()) } + // TODO: handle digest auth pub fn auth(&self) -> Result> { if let Auth { auth_type: Some(ref auth_type), From dec3eaf1fdca4b5bba994008984cdcf1b3c68ee2 Mon Sep 17 00:00:00 2001 From: ducaale Date: Tue, 14 Sep 2021 22:31:53 +0300 Subject: [PATCH 05/21] get digest credentials from users also handle digest auth in session + a little bit of refactoring for auth.rs --- src/auth.rs | 136 ++++++++++++++++++++++++------------------------ src/cli.rs | 56 +++++++++++--------- src/main.rs | 55 ++++++++++---------- src/redirect.rs | 10 +--- src/session.rs | 67 +++++++++++++++--------- src/to_curl.rs | 31 +++++++---- src/utils.rs | 28 ++++++++-- 7 files changed, 211 insertions(+), 172 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 79390699..fcf724d1 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -4,26 +4,62 @@ use std::io; use std::path::PathBuf; use anyhow::Result; -use dirs::home_dir; use netrc_rs::Netrc; use reqwest::blocking::{Request, Response}; use reqwest::header::{HeaderValue, AUTHORIZATION, WWW_AUTHENTICATE}; use reqwest::StatusCode; +use crate::cli::AuthType; use crate::middleware::{Middleware, Next}; use crate::regex; +use crate::utils::{clone_request, get_home_dir}; -// TODO: move this to utils.rs -fn clone_request(request: &mut Request) -> Result { - if let Some(b) = request.body_mut().as_mut() { - b.buffer()?; +#[derive(Debug, PartialEq, Eq)] +pub enum Auth { + Bearer(String), + Basic(String, Option), + Digest(String, String), +} + +impl Auth { + pub fn from_str(auth: &str, auth_type: AuthType, host: &str) -> Result { + match auth_type { + AuthType::basic => { + let (username, password) = parse_auth(auth, host)?; + Ok(Auth::Basic(username, password)) + } + AuthType::digest => { + let (username, password) = parse_auth(auth, host)?; + Ok(Auth::Digest(username, password.unwrap_or("".to_string()))) + } + AuthType::bearer => Ok(Auth::Bearer(auth.to_string())), + } + } + + pub fn from_netrc(netrc: &str, auth_type: AuthType, host: &str) -> Option { + Netrc::parse_borrow(&netrc, false) + .ok()? + .machines + .into_iter() + .filter_map(|machine| match machine.name { + Some(name) if name == host => { + let username = machine.login.unwrap_or_else(|| "".to_string()); + let password = machine.password; + match auth_type { + AuthType::basic => Some(Auth::Basic(username, password)), + AuthType::digest => { + Some(Auth::Digest(username, password.unwrap_or("".to_string()))) + } + AuthType::bearer => None, + } + } + _ => None, + }) + .last() } - // This doesn't copy the contents of the buffer, cloning requests is cheap - // https://docs.rs/bytes/1.0.1/bytes/struct.Bytes.html - Ok(request.try_clone().unwrap()) // guaranteed to not fail if body is already buffered } -pub fn parse_auth(auth: String, host: &str) -> io::Result<(String, Option)> { +pub fn parse_auth(auth: &str, host: &str) -> io::Result<(String, Option)> { if let Some(cap) = regex!(r"^([^:]*):$").captures(&auth) { Ok((cap[1].to_string(), None)) } else if let Some(cap) = regex!(r"^(.+?):(.+)$").captures(&auth) { @@ -31,85 +67,47 @@ pub fn parse_auth(auth: String, host: &str) -> io::Result<(String, Option Option { - #[cfg(target_os = "windows")] - if let Some(path) = env::var_os("XH_TEST_MODE_WIN_HOME_DIR") { - return Some(PathBuf::from(path)); - } - - home_dir() -} - -fn netrc_path() -> Option { - match env::var_os("NETRC") { +pub fn read_netrc() -> Option { + let netrc_path = match env::var_os("NETRC") { Some(path) => { - let pth = PathBuf::from(path); - if pth.exists() { - Some(pth) + let path = PathBuf::from(path); + if path.exists() { + Some(path) } else { None } } None => { - if let Some(hd_path) = get_home_dir() { - [".netrc", "_netrc"] - .iter() - .map(|f| hd_path.join(f)) - .find(|p| p.exists()) - } else { - None - } + let home_dir = get_home_dir()?; + [".netrc", "_netrc"] + .iter() + .map(|f| home_dir.join(f)) + .find(|p| p.exists()) } - } -} - -pub fn read_netrc() -> Option { - if let Some(netrc_path) = netrc_path() { - if let Ok(result) = fs::read_to_string(netrc_path) { - return Some(result); - } - }; - - None -} - -pub fn auth_from_netrc(machine: &str, netrc: &str) -> Option<(String, Option)> { - if let Ok(netrc) = Netrc::parse_borrow(&netrc, false) { - return netrc - .machines - .into_iter() - .filter_map(|mach| match mach.name { - Some(name) if name == machine => { - let user = mach.login.unwrap_or_else(|| "".to_string()); - Some((user, mach.password)) - } - _ => None, - }) - .last(); - } + }?; - None + fs::read_to_string(netrc_path).ok() } -pub struct DigestAuth<'a> { +pub struct DigestAuthMiddleware<'a> { username: &'a str, password: &'a str, } -impl<'a> DigestAuth<'a> { +impl<'a> DigestAuthMiddleware<'a> { pub fn new(username: &'a str, password: &'a str) -> Self { - DigestAuth { username, password } + DigestAuthMiddleware { username, password } } } -impl<'a> Middleware for DigestAuth<'a> { +impl<'a> Middleware for DigestAuthMiddleware<'a> { fn handle(&mut self, mut request: Request, mut next: Next) -> Result { let response = next.run(clone_request(&mut request)?)?; match response.headers().get(WWW_AUTHENTICATE) { @@ -144,7 +142,7 @@ mod tests { (":", ("", None)), ]; for (input, output) in expected { - let (user, pass) = parse_auth(input.to_string(), "").unwrap(); + let (user, pass) = parse_auth(input, "").unwrap(); assert_eq!(output, (user.as_str(), pass.as_deref())); } } @@ -160,24 +158,24 @@ mod tests { ( "example.com", good_netrc, - Some(("user".to_string(), Some("pass".to_string()))), + Some(Auth::Basic("user".to_string(), Some("pass".to_string()))), ), ("example.org", good_netrc, None), ("example.com", malformed_netrc, None), ( "example.com", missing_login, - Some(("".to_string(), Some("pass".to_string()))), + Some(Auth::Basic("".to_string(), Some("pass".to_string()))), ), ( "example.com", missing_pass, - Some(("user".to_string(), None)), + Some(Auth::Basic("user".to_string(), None)), ), ]; for (machine, netrc, output) in expected { - assert_eq!(output, auth_from_netrc(machine, netrc)); + assert_eq!(output, Auth::from_netrc(netrc, AuthType::basic, machine)); } } } diff --git a/src/cli.rs b/src/cli.rs index 4df78718..86dee47d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -137,9 +137,8 @@ pub struct Cli { // Currently deprecated in favor of --bearer, un-hide if new auth types are introduced /// Specify the auth mechanism. - #[structopt(short = "A", long, possible_values = &AuthType::variants(), - default_value = "basic", case_insensitive = true, hidden = true)] - pub auth_type: AuthType, + #[structopt(short = "A", long, possible_values = &AuthType::variants(), case_insensitive = true)] + pub auth_type: Option, /// Authenticate as USER with PASS. PASS will be prompted if missing. /// @@ -498,8 +497,9 @@ impl Cli { if self.https { self.default_scheme = Some("https".to_string()); } - if self.auth_type == AuthType::bearer && self.auth.is_some() { - self.bearer = self.auth.take(); + if self.bearer.is_some() { + self.auth_type = Some(AuthType::bearer); + self.auth = self.bearer.take(); } self.check_status = match (self.check_status_raw, matches.is_present("no-check-status")) { (true, true) => unreachable!(), @@ -683,7 +683,13 @@ arg_enum! { #[allow(non_camel_case_types)] #[derive(Debug, PartialEq)] pub enum AuthType { - basic, bearer + basic, bearer, digest + } +} + +impl Default for AuthType { + fn default() -> Self { + AuthType::basic } } @@ -1017,28 +1023,28 @@ mod tests { ); } - #[test] - fn auth() { - let cli = parse(&["--auth=user:pass", ":"]).unwrap(); - assert_eq!(cli.auth.as_deref(), Some("user:pass")); - assert_eq!(cli.bearer, None); + // #[test] + // fn auth() { + // let cli = parse(&["--auth=user:pass", ":"]).unwrap(); + // assert_eq!(cli.auth.as_deref(), Some("user:pass")); + // assert_eq!(cli.bearer, None); - let cli = parse(&["--auth=user:pass", "--auth-type=basic", ":"]).unwrap(); - assert_eq!(cli.auth.as_deref(), Some("user:pass")); - assert_eq!(cli.bearer, None); + // let cli = parse(&["--auth=user:pass", "--auth-type=basic", ":"]).unwrap(); + // assert_eq!(cli.auth.as_deref(), Some("user:pass")); + // assert_eq!(cli.bearer, None); - let cli = parse(&["--auth=token", "--auth-type=bearer", ":"]).unwrap(); - assert_eq!(cli.auth, None); - assert_eq!(cli.bearer.as_deref(), Some("token")); + // let cli = parse(&["--auth=token", "--auth-type=bearer", ":"]).unwrap(); + // assert_eq!(cli.auth, None); + // assert_eq!(cli.bearer.as_deref(), Some("token")); - let cli = parse(&["--bearer=token", "--auth-type=bearer", ":"]).unwrap(); - assert_eq!(cli.auth, None); - assert_eq!(cli.bearer.as_deref(), Some("token")); + // let cli = parse(&["--bearer=token", "--auth-type=bearer", ":"]).unwrap(); + // assert_eq!(cli.auth, None); + // assert_eq!(cli.bearer.as_deref(), Some("token")); - let cli = parse(&["--auth-type=bearer", ":"]).unwrap(); - assert_eq!(cli.auth, None); - assert_eq!(cli.bearer, None); - } + // let cli = parse(&["--auth-type=bearer", ":"]).unwrap(); + // assert_eq!(cli.auth, None); + // assert_eq!(cli.bearer, None); + // } #[test] fn request_type_overrides() { @@ -1237,7 +1243,7 @@ mod tests { ]) .unwrap(); assert_eq!(cli.bearer, None); - assert_eq!(cli.auth_type, AuthType::basic); + assert_eq!(cli.auth_type, None); } #[test] diff --git a/src/main.rs b/src/main.rs index d44fcf4d..d3812ef4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,11 +24,10 @@ use atty::Stream; use redirect::RedirectFollower; use reqwest::blocking::Client; use reqwest::header::{ - HeaderValue, ACCEPT, ACCEPT_ENCODING, AUTHORIZATION, CONNECTION, CONTENT_TYPE, COOKIE, RANGE, - USER_AGENT, + HeaderValue, ACCEPT, ACCEPT_ENCODING, CONNECTION, CONTENT_TYPE, COOKIE, RANGE, USER_AGENT, }; -use crate::auth::{auth_from_netrc, parse_auth, read_netrc, DigestAuth}; +use crate::auth::{read_netrc, Auth, DigestAuthMiddleware}; use crate::buffer::Buffer; use crate::cli::{BodyType, Cli, Print, Proxy, Verify}; use crate::download::{download_file, get_file_size}; @@ -138,6 +137,7 @@ fn run(args: Cli) -> Result { let mut exit_code: i32 = 0; let mut resume: Option = None; + let mut auth = None; if args.url.scheme() == "https" { let verify = args.verify.unwrap_or_else(|| { @@ -235,14 +235,10 @@ fn run(args: Cli) -> Result { }; if let Some(ref mut s) = session { + auth = s.auth()?; for (key, value) in s.headers()?.iter() { headers.entry(key).or_insert_with(|| value.clone()); } - if let Some(auth) = s.auth()? { - headers - .entry(AUTHORIZATION) - .or_insert(HeaderValue::from_str(&auth)?); - } s.save_headers(&headers)?; let mut cookie_jar = cookie_jar.lock().unwrap(); @@ -320,27 +316,30 @@ fn run(args: Cli) -> Result { } } - // TODO: handle digest auth - if let Some(auth) = args.auth { - let (username, password) = parse_auth(auth, args.url.host_str().unwrap_or(""))?; - if let Some(ref mut s) = session { - s.save_basic_auth(username.clone(), password.clone()); - } - request_builder = request_builder.basic_auth(username, password); + let auth_type = args.auth_type.unwrap_or_default(); + if let Some(auth_from_arg) = args.auth { + auth = Some(Auth::from_str( + &auth_from_arg, + auth_type, + args.url.host_str().unwrap_or(""), + )?); } else if !args.ignore_netrc { - if let Some(host) = args.url.host_str() { - if let Some(netrc) = read_netrc() { - if let Some((username, password)) = auth_from_netrc(host, &netrc) { - request_builder = request_builder.basic_auth(username, password); - } - } + if let (Some(host), Some(netrc)) = (args.url.host_str(), read_netrc()) { + auth = Auth::from_netrc(&netrc, auth_type, host); } } - if let Some(token) = args.bearer { + + if let Some(auth) = &auth { if let Some(ref mut s) = session { - s.save_bearer_auth(token.clone()) + s.save_auth(auth); + } + request_builder = match auth { + Auth::Basic(username, password) => { + request_builder.basic_auth(username, password.as_ref()) + } + Auth::Bearer(token) => request_builder.bearer_auth(token), + Auth::Digest(..) => request_builder, } - request_builder = request_builder.bearer_auth(token); } let mut request = request_builder.headers(headers).build()?; @@ -412,10 +411,10 @@ fn run(args: Cli) -> Result { } client = client.with(redirect_follower); } - // TODO: - // 1. get username and password from user - // 2. print intermediate intermediary requests/responses if --all flag is used - client = client.with(DigestAuth::new("username", "password")); + if let Some(Auth::Digest(username, password)) = &auth { + // TODO: print intermediate intermediary requests/responses if --all flag is used + client = client.with(DigestAuthMiddleware::new(username, password)); + } client.execute(request)? }; diff --git a/src/redirect.rs b/src/redirect.rs index d154d7d8..a6a2375c 100644 --- a/src/redirect.rs +++ b/src/redirect.rs @@ -7,6 +7,7 @@ use reqwest::header::{ use reqwest::{Method, StatusCode, Url}; use crate::middleware::{Middleware, Next}; +use crate::utils::clone_request; pub struct RedirectFollower where @@ -63,15 +64,6 @@ where } } -fn clone_request(request: &mut Request) -> Result { - if let Some(b) = request.body_mut().as_mut() { - b.buffer()?; - } - // This doesn't copy the contents of the buffer, cloning requests is cheap - // https://docs.rs/bytes/1.0.1/bytes/struct.Bytes.html - Ok(request.try_clone().unwrap()) // guaranteed to not fail if body is already buffered -} - // See https://github.com/seanmonstar/reqwest/blob/bbeb1ede4e8098481c3de6f2cafb8ecca1db4ede/src/async_impl/client.rs#L1500-L1607 fn get_next_request(mut request: Request, response: &Response) -> Option { let get_next_url = |request: &Request| { diff --git a/src/session.rs b/src/session.rs index fb7c52b1..d7502bd2 100644 --- a/src/session.rs +++ b/src/session.rs @@ -10,6 +10,7 @@ use reqwest::header::HeaderMap; use reqwest::Url; use serde::{Deserialize, Serialize}; +use crate::auth; use crate::utils::{config_dir, test_mode}; #[derive(Debug, Serialize, Deserialize)] @@ -115,37 +116,53 @@ impl Session { Ok(()) } - // TODO: handle digest auth - pub fn auth(&self) -> Result> { + pub fn auth(&self) -> Result> { if let Auth { - auth_type: Some(ref auth_type), - raw_auth: Some(ref raw_auth), - } = self.content.auth + auth_type: Some(auth_type), + raw_auth: Some(raw_auth), + } = &self.content.auth { - if auth_type.as_str() == "basic" { - return Ok(Some(format!("Basic {}", base64::encode(raw_auth)))); - } else if auth_type.as_str() == "bearer" { - return Ok(Some(format!("Bearer {}", raw_auth))); - } else { - return Err(anyhow!("Unknown auth type {}", raw_auth)); + match auth_type.as_str() { + "basic" => { + let (username, password) = auth::parse_auth(raw_auth, "")?; + Ok(Some(auth::Auth::Basic(username, password))) + } + "digest" => { + let (username, password) = auth::parse_auth(raw_auth, "")?; + Ok(Some(auth::Auth::Digest( + username, + password.unwrap_or("".into()), + ))) + } + "bearer" => Ok(Some(auth::Auth::Bearer(raw_auth.into()))), + _ => Err(anyhow!("Unknown auth type {}", raw_auth)), } - } - - Ok(None) - } - - pub fn save_bearer_auth(&mut self, token: String) { - self.content.auth = Auth { - auth_type: Some("bearer".into()), - raw_auth: Some(token), + } else { + Ok(None) } } - pub fn save_basic_auth(&mut self, username: String, password: Option) { - let password = password.unwrap_or_else(|| "".into()); - self.content.auth = Auth { - auth_type: Some("basic".into()), - raw_auth: Some(format!("{}:{}", username, password)), + pub fn save_auth(&mut self, auth: &auth::Auth) { + match auth { + auth::Auth::Basic(username, password) => { + let password = password.as_deref().unwrap_or_else(|| ""); + self.content.auth = Auth { + auth_type: Some("basic".into()), + raw_auth: Some(format!("{}:{}", username, password)), + } + } + auth::Auth::Digest(username, password) => { + self.content.auth = Auth { + auth_type: Some("digest".into()), + raw_auth: Some(format!("{}:{}", username, password)), + } + } + auth::Auth::Bearer(token) => { + self.content.auth = Auth { + auth_type: Some("bearer".into()), + raw_auth: Some(token.into()), + } + } } } diff --git a/src/to_curl.rs b/src/to_curl.rs index 05864e69..53b01fdd 100644 --- a/src/to_curl.rs +++ b/src/to_curl.rs @@ -3,10 +3,8 @@ use std::io::{stderr, stdout, Write}; use anyhow::{anyhow, Result}; use reqwest::Method; -use crate::{ - cli::{Cli, Verify}, - request_items::{Body, RequestItem, FORM_CONTENT_TYPE, JSON_ACCEPT, JSON_CONTENT_TYPE}, -}; +use crate::cli::{AuthType, Cli, Verify}; +use crate::request_items::{Body, RequestItem, FORM_CONTENT_TYPE, JSON_ACCEPT, JSON_CONTENT_TYPE}; pub fn print_curl_translation(args: Cli) -> Result<()> { let cmd = translate(args)?; @@ -236,13 +234,24 @@ pub fn translate(args: Cli) -> Result { cmd.push(format!("{}:", header)); } if let Some(auth) = args.auth { - // curl implements this flag the same way, including password prompt - cmd.flag("-u", "--user"); - cmd.push(auth); - } - if let Some(token) = args.bearer { - cmd.push("--oauth2-bearer"); - cmd.push(token); + match args.auth_type.unwrap_or_default() { + AuthType::basic => { + cmd.push("--basic"); + // curl implements this flag the same way, including password prompt + cmd.flag("-u", "--user"); + cmd.push(auth); + } + AuthType::digest => { + cmd.push("--digest"); + // curl implements this flag the same way, including password prompt + cmd.flag("-u", "--user"); + cmd.push(auth); + } + AuthType::bearer => { + cmd.push("--oauth2-bearer"); + cmd.push(auth); + } + } } if args.request_items.is_multipart() { diff --git a/src/utils.rs b/src/utils.rs index 31af6b01..a29aaf57 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,11 +1,20 @@ -use std::{ - env::var_os, - io::{self, Write}, - path::PathBuf, -}; +use std::env::var_os; +use std::io::{self, Write}; +use std::path::PathBuf; +use anyhow::Result; +use reqwest::blocking::Request; use url::{Host, Url}; +pub fn clone_request(request: &mut Request) -> Result { + if let Some(b) = request.body_mut().as_mut() { + b.buffer()?; + } + // This doesn't copy the contents of the buffer, cloning requests is cheap + // https://docs.rs/bytes/1.0.1/bytes/struct.Bytes.html + Ok(request.try_clone().unwrap()) // guaranteed to not fail if body is already buffered +} + /// Whether to make some things more deterministic for the benefit of tests pub fn test_mode() -> bool { // In integration tests the binary isn't compiled with cfg(test), so we @@ -41,6 +50,15 @@ pub fn config_dir() -> Option { } } +pub fn get_home_dir() -> Option { + #[cfg(target_os = "windows")] + if let Some(path) = env::var_os("XH_TEST_MODE_WIN_HOME_DIR") { + return Some(PathBuf::from(path)); + } + + dirs::home_dir() +} + // https://stackoverflow.com/a/45145246/5915221 #[macro_export] macro_rules! vec_of_strings { From 86f5149c01150646e1e49fd5a4c06976887e7f2e Mon Sep 17 00:00:00 2001 From: ducaale Date: Tue, 14 Sep 2021 22:34:15 +0300 Subject: [PATCH 06/21] remove base64 crate --- Cargo.lock | 1 - Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24690ea9..3433d9be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3016,7 +3016,6 @@ dependencies = [ "assert_cmd", "assert_matches", "atty", - "base64", "cookie 0.15.0", "cookie_store 0.15.0", "curl", diff --git a/Cargo.toml b/Cargo.toml index 15c0026c..ea0c9a19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ readme = "README.md" [dependencies] anyhow = "1.0.38" atty = "0.2" -base64 = "0.13" cookie_crate = { version = "0.15", package = "cookie" } cookie_store = { version = "0.15.0" } reqwest_cookie_store = { version = "0.2.0" } From 5ad07a364c4cddf93fc598cebdf8cd48ca0d559d Mon Sep 17 00:00:00 2001 From: ducaale Date: Tue, 14 Sep 2021 22:45:01 +0300 Subject: [PATCH 07/21] fix clippy warnings --- src/auth.rs | 20 ++++++++++++-------- src/session.rs | 4 ++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index fcf724d1..516c4bd2 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -30,9 +30,12 @@ impl Auth { } AuthType::digest => { let (username, password) = parse_auth(auth, host)?; - Ok(Auth::Digest(username, password.unwrap_or("".to_string()))) + Ok(Auth::Digest( + username, + password.unwrap_or_else(|| "".into()), + )) } - AuthType::bearer => Ok(Auth::Bearer(auth.to_string())), + AuthType::bearer => Ok(Auth::Bearer(auth.into())), } } @@ -43,13 +46,14 @@ impl Auth { .into_iter() .filter_map(|machine| match machine.name { Some(name) if name == host => { - let username = machine.login.unwrap_or_else(|| "".to_string()); + let username = machine.login.unwrap_or_else(|| "".into()); let password = machine.password; match auth_type { AuthType::basic => Some(Auth::Basic(username, password)), - AuthType::digest => { - Some(Auth::Digest(username, password.unwrap_or("".to_string()))) - } + AuthType::digest => Some(Auth::Digest( + username, + password.unwrap_or_else(|| "".into()), + )), AuthType::bearer => None, } } @@ -60,9 +64,9 @@ impl Auth { } pub fn parse_auth(auth: &str, host: &str) -> io::Result<(String, Option)> { - if let Some(cap) = regex!(r"^([^:]*):$").captures(&auth) { + if let Some(cap) = regex!(r"^([^:]*):$").captures(auth) { Ok((cap[1].to_string(), None)) - } else if let Some(cap) = regex!(r"^(.+?):(.+)$").captures(&auth) { + } else if let Some(cap) = regex!(r"^(.+?):(.+)$").captures(auth) { let username = cap[1].to_string(); let password = cap[2].to_string(); Ok((username, Some(password))) diff --git a/src/session.rs b/src/session.rs index d7502bd2..fb4e7a10 100644 --- a/src/session.rs +++ b/src/session.rs @@ -131,7 +131,7 @@ impl Session { let (username, password) = auth::parse_auth(raw_auth, "")?; Ok(Some(auth::Auth::Digest( username, - password.unwrap_or("".into()), + password.unwrap_or_else(|| "".into()), ))) } "bearer" => Ok(Some(auth::Auth::Bearer(raw_auth.into()))), @@ -145,7 +145,7 @@ impl Session { pub fn save_auth(&mut self, auth: &auth::Auth) { match auth { auth::Auth::Basic(username, password) => { - let password = password.as_deref().unwrap_or_else(|| ""); + let password = password.as_deref().unwrap_or(""); self.content.auth = Auth { auth_type: Some("basic".into()), raw_auth: Some(format!("{}:{}", username, password)), From f7197547f6ec083ddaadb0ada73248bdbe91310b Mon Sep 17 00:00:00 2001 From: ducaale Date: Tue, 14 Sep 2021 22:47:28 +0300 Subject: [PATCH 08/21] modify some comments --- src/cli.rs | 1 - src/main.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 86dee47d..88911978 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -135,7 +135,6 @@ pub struct Cli { #[structopt(skip)] pub is_session_read_only: bool, - // Currently deprecated in favor of --bearer, un-hide if new auth types are introduced /// Specify the auth mechanism. #[structopt(short = "A", long, possible_values = &AuthType::variants(), case_insensitive = true)] pub auth_type: Option, diff --git a/src/main.rs b/src/main.rs index d3812ef4..c978dd3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -412,7 +412,7 @@ fn run(args: Cli) -> Result { client = client.with(redirect_follower); } if let Some(Auth::Digest(username, password)) = &auth { - // TODO: print intermediate intermediary requests/responses if --all flag is used + // TODO: print intermediary requests/responses if --all flag is used client = client.with(DigestAuthMiddleware::new(username, password)); } client.execute(request)? From 35e3ed7a08b40b1cc4d9c5ae61992132a6302676 Mon Sep 17 00:00:00 2001 From: ducaale Date: Tue, 14 Sep 2021 22:58:58 +0300 Subject: [PATCH 09/21] use qualified namespace for env --- src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.rs b/src/utils.rs index a29aaf57..a0a817e9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -52,7 +52,7 @@ pub fn config_dir() -> Option { pub fn get_home_dir() -> Option { #[cfg(target_os = "windows")] - if let Some(path) = env::var_os("XH_TEST_MODE_WIN_HOME_DIR") { + if let Some(path) = std::env::var_os("XH_TEST_MODE_WIN_HOME_DIR") { return Some(PathBuf::from(path)); } From a355d957f294357d0747964aa936f24bbb778fa6 Mon Sep 17 00:00:00 2001 From: ducaale Date: Tue, 28 Sep 2021 10:35:37 +0300 Subject: [PATCH 10/21] print intermediary requests/responses for digest --- src/auth.rs | 3 +++ src/main.rs | 40 +++++++++++++++++++--------------------- src/middleware.rs | 40 +++++++++++++++++++++++++++++++++++----- src/redirect.rs | 29 ++++++----------------------- 4 files changed, 63 insertions(+), 49 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 516c4bd2..7b58ecdc 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -126,6 +126,9 @@ impl<'a> Middleware for DigestAuthMiddleware<'a> { request .headers_mut() .insert(AUTHORIZATION, HeaderValue::from_str(&answer)?); + if let Some(ref mut printer) = next.printer { + printer(response, &mut request)?; + } Ok(next.run(request)?) } _ => Ok(response), diff --git a/src/main.rs b/src/main.rs index c978dd3a..54e78623 100644 --- a/src/main.rs +++ b/src/main.rs @@ -389,30 +389,28 @@ fn run(args: Cli) -> Result { let response = { let history_print = args.history_print.unwrap_or(print); let mut client = ClientWithMiddleware::new(&client); + if args.all { + client = client.with_printer(|prev_response, next_request| { + if history_print.response_headers { + printer.print_response_headers(&prev_response)?; + } + if history_print.response_body { + printer.print_response_body(prev_response)?; + printer.print_separator()?; + } + if history_print.request_headers { + printer.print_request_headers(next_request, &*cookie_jar)?; + } + if history_print.request_body { + printer.print_request_body(next_request)?; + } + Ok(()) + }); + } if args.follow { - let mut redirect_follower = RedirectFollower::new(args.max_redirects.unwrap_or(10)); - if args.all { - redirect_follower.on_redirect(|prev_response, next_request| { - if history_print.response_headers { - printer.print_response_headers(&prev_response)?; - } - if history_print.response_body { - printer.print_response_body(prev_response)?; - printer.print_separator()?; - } - if history_print.request_headers { - printer.print_request_headers(next_request, &*cookie_jar)?; - } - if history_print.request_body { - printer.print_request_body(next_request)?; - } - Ok(()) - }); - } - client = client.with(redirect_follower); + client = client.with(RedirectFollower::new(args.max_redirects.unwrap_or(10))); } if let Some(Auth::Digest(username, password)) = &auth { - // TODO: print intermediary requests/responses if --all flag is used client = client.with(DigestAuthMiddleware::new(username, password)); } client.execute(request)? diff --git a/src/middleware.rs b/src/middleware.rs index 94bc2e5e..7fe4eb7a 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -1,15 +1,23 @@ use anyhow::Result; use reqwest::blocking::{Client, Request, Response}; +// TODO: come up with a more suitable name than "Next" +// maybe "Handler"?? pub struct Next<'a, 'b> { client: &'a Client, + pub printer: Option<&'a mut (dyn FnMut(Response, &mut Request) -> Result<()> + 'b)>, middlewares: &'a mut [Box], } impl<'a, 'b> Next<'a, 'b> { - fn new(client: &'a Client, middlewares: &'a mut [Box]) -> Self { + fn new( + client: &'a Client, + printer: Option<&'a mut (dyn FnMut(Response, &mut Request) -> Result<()> + 'b)>, + middlewares: &'a mut [Box], + ) -> Self { Next { client, + printer, middlewares, } } @@ -17,7 +25,10 @@ impl<'a, 'b> Next<'a, 'b> { pub fn run(&mut self, request: Request) -> Result { match self.middlewares { [] => Ok(self.client.execute(request)?), - [ref mut head, tail @ ..] => head.handle(request, Next::new(self.client, tail)), + [ref mut head, tail @ ..] => head.handle( + request, + Next::new(self.client, self.printer.as_deref_mut(), tail), + ), } } } @@ -26,26 +37,45 @@ pub trait Middleware { fn handle(&mut self, request: Request, next: Next) -> Result; } -pub struct ClientWithMiddleware<'a> { +pub struct ClientWithMiddleware<'a, T> +where + T: FnMut(Response, &mut Request) -> Result<()>, +{ client: &'a Client, + printer: Option, middlewares: Vec>, } -impl<'a> ClientWithMiddleware<'a> { +impl<'a, T: 'a> ClientWithMiddleware<'a, T> +where + T: FnMut(Response, &mut Request) -> Result<()>, +{ pub fn new(client: &'a Client) -> Self { ClientWithMiddleware { client, + printer: None, middlewares: vec![], } } + pub fn with_printer(mut self, printer: T) -> Self { + self.printer = Some(printer); + self + } + pub fn with(mut self, middleware: impl Middleware + 'a) -> Self { self.middlewares.push(Box::new(middleware)); self } pub fn execute(&mut self, request: Request) -> Result { - let mut next = Next::new(self.client, &mut self.middlewares[..]); + let mut next = Next::new( + self.client, + self.printer + .as_mut() + .map(|p| p as &mut dyn FnMut(Response, &mut Request) -> Result<()>), + &mut self.middlewares[..], + ); next.run(request) } } diff --git a/src/redirect.rs b/src/redirect.rs index a6a2375c..09e189ce 100644 --- a/src/redirect.rs +++ b/src/redirect.rs @@ -9,34 +9,17 @@ use reqwest::{Method, StatusCode, Url}; use crate::middleware::{Middleware, Next}; use crate::utils::clone_request; -pub struct RedirectFollower -where - T: FnMut(Response, &mut Request) -> Result<()>, -{ +pub struct RedirectFollower { max_redirects: usize, - callback: Option, } -impl RedirectFollower -where - T: FnMut(Response, &mut Request) -> Result<()>, -{ +impl RedirectFollower { pub fn new(max_redirects: usize) -> Self { - RedirectFollower { - max_redirects, - callback: None, - } - } - - pub fn on_redirect(&mut self, callback: T) { - self.callback = Some(callback); + RedirectFollower { max_redirects } } } -impl Middleware for RedirectFollower -where - T: FnMut(Response, &mut Request) -> Result<()>, -{ +impl Middleware for RedirectFollower { fn handle(&mut self, mut first_request: Request, mut next: Next) -> Result { // This buffers the body in case we need it again later // reqwest does *not* do this, it ignores 307/308 with a streaming body @@ -53,8 +36,8 @@ where self.max_redirects )); } - if let Some(ref mut callback) = self.callback { - callback(response, &mut next_request)?; + if let Some(ref mut printer) = next.printer { + printer(response, &mut next_request)?; } request = clone_request(&mut next_request)?; response = next.run(next_request)?; From d95eb3bed6c86629461c81b4ceba48f846094dda Mon Sep 17 00:00:00 2001 From: ducaale Date: Tue, 28 Sep 2021 10:42:00 +0300 Subject: [PATCH 11/21] use alternative syntax for bounding T lifetime --- src/middleware.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/middleware.rs b/src/middleware.rs index 7fe4eb7a..c0ac8915 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -46,9 +46,9 @@ where middlewares: Vec>, } -impl<'a, T: 'a> ClientWithMiddleware<'a, T> +impl<'a, T> ClientWithMiddleware<'a, T> where - T: FnMut(Response, &mut Request) -> Result<()>, + T: FnMut(Response, &mut Request) -> Result<()> + 'a, { pub fn new(client: &'a Client) -> Self { ClientWithMiddleware { From 4c113af4854f18daee9e087cd35b7bc7829c0829 Mon Sep 17 00:00:00 2001 From: ducaale Date: Tue, 28 Sep 2021 12:24:57 +0300 Subject: [PATCH 12/21] deleted commented out test --- src/cli.rs | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 88911978..df83bea6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1022,29 +1022,6 @@ mod tests { ); } - // #[test] - // fn auth() { - // let cli = parse(&["--auth=user:pass", ":"]).unwrap(); - // assert_eq!(cli.auth.as_deref(), Some("user:pass")); - // assert_eq!(cli.bearer, None); - - // let cli = parse(&["--auth=user:pass", "--auth-type=basic", ":"]).unwrap(); - // assert_eq!(cli.auth.as_deref(), Some("user:pass")); - // assert_eq!(cli.bearer, None); - - // let cli = parse(&["--auth=token", "--auth-type=bearer", ":"]).unwrap(); - // assert_eq!(cli.auth, None); - // assert_eq!(cli.bearer.as_deref(), Some("token")); - - // let cli = parse(&["--bearer=token", "--auth-type=bearer", ":"]).unwrap(); - // assert_eq!(cli.auth, None); - // assert_eq!(cli.bearer.as_deref(), Some("token")); - - // let cli = parse(&["--auth-type=bearer", ":"]).unwrap(); - // assert_eq!(cli.auth, None); - // assert_eq!(cli.bearer, None); - // } - #[test] fn request_type_overrides() { let cli = parse(&["--form", "--json", ":"]).unwrap(); From e483beee412e3d1095292b64509b76af48ef7594 Mon Sep 17 00:00:00 2001 From: ducaale Date: Sun, 3 Oct 2021 18:53:21 +0300 Subject: [PATCH 13/21] add tests for auth-digest --- src/auth.rs | 8 +++- tests/cli.rs | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/auth.rs b/src/auth.rs index 7b58ecdc..20d78fd9 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -116,16 +116,22 @@ impl<'a> Middleware for DigestAuthMiddleware<'a> { let response = next.run(clone_request(&mut request)?)?; match response.headers().get(WWW_AUTHENTICATE) { Some(wwwauth) if response.status() == StatusCode::UNAUTHORIZED => { - let context = digest_auth::AuthContext::new( + let mut context = digest_auth::AuthContext::new( self.username, self.password, request.url().path(), ); + if let Some(cnonc) = std::env::var_os("XH_TEST_DIGEST_AUTH_CNONCE") { + context.set_custom_cnonce(cnonc.to_string_lossy().to_string()); + } let mut prompt = digest_auth::parse(wwwauth.to_str()?)?; let answer = prompt.respond(&context)?.to_header_string(); request .headers_mut() .insert(AUTHORIZATION, HeaderValue::from_str(&answer)?); + if let Some(url) = std::env::var_os("XH_TEST_DIGEST_AUTH_URL") { + *request.url_mut() = reqwest::Url::parse(&url.to_string_lossy())?; + } if let Some(ref mut printer) = next.printer { printer(response, &mut request)?; } diff --git a/tests/cli.rs b/tests/cli.rs index a2c34ef8..2f5b332b 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -2196,3 +2196,119 @@ fn warns_if_config_is_invalid() { .stderr(contains("Unable to parse config file")) .success(); } + +#[test] +fn digest_auth() { + let server1 = MockServer::start(); + let server2 = MockServer::start(); + let mock1 = server1.mock(|when, then| { + when.matches(|req: &HttpMockRequest| { + !req.headers + .as_ref() + .unwrap() + .iter() + .any(|(key, _)| key == "Authorization") + }); + then.status(401).header("WWW-Authenticate", r#"Digest realm="me@xh.com", nonce="e5051361f053723a807674177fc7022f", qop="auth, auth-int", opaque="9dcf562038f1ec1c8d02f218ef0e7a4b", algorithm=MD5, stale=FALSE"#); + }); + let mock2 = server2.mock(|when, then| { + when.header_exists("Authorization"); + then.body("authenticated"); + }); + + get_command() + .env("XH_TEST_DIGEST_AUTH_URL", server2.base_url()) + .arg("--auth-type=digest") + .arg("--auth=ahmed:12345") + .arg(server1.base_url()) + .assert() + .stdout(contains("HTTP/1.1 200 OK")); + + mock1.assert(); + mock2.assert(); +} + +#[test] +fn digest_auth_with_redirection() { + let server1 = MockServer::start(); + let server2 = MockServer::start(); + let server3 = MockServer::start(); + let mock1 = server1.mock(|when, then| { + when.matches(|req: &HttpMockRequest| { + !req.headers + .as_ref() + .unwrap() + .iter() + .any(|(key, _)| key == "Authorization") + }); + then.status(401) + .header("WWW-Authenticate", r#"Digest realm="me@xh.com", nonce="e5051361f053723a807674177fc7022f", qop="auth, auth-int", opaque="9dcf562038f1ec1c8d02f218ef0e7a4b", algorithm=MD5, stale=FALSE"#) + .header("date", "N/A"); + }); + let mock2 = server2.mock(|when, then| { + when.header_exists("Authorization"); + then.status(302) + .header("location", &server3.base_url()) + .header("date", "N/A") + .body("authentication successful, redirecting..."); + }); + server3.mock(|_, then| { + then.header("date", "N/A").body("final destination"); + }); + + get_command() + .env("XH_TEST_DIGEST_AUTH_URL", server2.base_url()) + .env("XH_TEST_DIGEST_AUTH_CNONCE", "f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ") + .arg("--auth-type=digest") + .arg("--auth=ahmed:12345") + .arg("--follow") + .arg("--verbose") + .arg(server1.base_url()) + .assert() + .stdout(formatdoc! {r#" + GET / HTTP/1.1 + Accept: */* + Accept-Encoding: gzip, deflate, br + Connection: keep-alive + Host: http.mock + User-Agent: xh/0.0.0 (test mode) + + HTTP/1.1 401 Unauthorized + Content-Length: 0 + Date: N/A + Www-Authenticate: Digest realm="me@xh.com", nonce="e5051361f053723a807674177fc7022f", qop="auth, auth-int", opaque="9dcf562038f1ec1c8d02f218ef0e7a4b", algorithm=MD5, stale=FALSE + + + + GET / HTTP/1.1 + Accept: */* + Accept-Encoding: gzip, deflate, br + Authorization: Digest username="ahmed", realm="me@xh.com", nonce="e5051361f053723a807674177fc7022f", uri="/", qop=auth, nc=00000001, cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ", response="1e96c9808de24d5dd36e9e4865ffca7d", opaque="9dcf562038f1ec1c8d02f218ef0e7a4b", algorithm=MD5 + Connection: keep-alive + Host: http.mock + User-Agent: xh/0.0.0 (test mode) + + HTTP/1.1 302 Found + Content-Length: 41 + Date: N/A + Location: {redirect_url} + + authentication successful, redirecting... + + GET / HTTP/1.1 + Accept: */* + Accept-Encoding: gzip, deflate, br + Connection: keep-alive + Host: http.mock + User-Agent: xh/0.0.0 (test mode) + + HTTP/1.1 200 OK + Content-Length: 17 + Date: N/A + + final destination + "#, redirect_url = server3.base_url()}); + + mock1.assert(); + mock2.assert(); +} From 291aab090da7d828aa4c65d8d0a8b1071c295289 Mon Sep 17 00:00:00 2001 From: ducaale Date: Mon, 11 Oct 2021 20:13:50 +0300 Subject: [PATCH 14/21] don't save netrc auth in session --- src/main.rs | 6 +++++- tests/cli.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 54e78623..4becfaa0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -138,6 +138,7 @@ fn run(args: Cli) -> Result { let mut exit_code: i32 = 0; let mut resume: Option = None; let mut auth = None; + let mut save_auth_in_session = true; if args.url.scheme() == "https" { let verify = args.verify.unwrap_or_else(|| { @@ -326,12 +327,15 @@ fn run(args: Cli) -> Result { } else if !args.ignore_netrc { if let (Some(host), Some(netrc)) = (args.url.host_str(), read_netrc()) { auth = Auth::from_netrc(&netrc, auth_type, host); + save_auth_in_session = false; } } if let Some(auth) = &auth { if let Some(ref mut s) = session { - s.save_auth(auth); + if save_auth_in_session { + s.save_auth(auth); + } } request_builder = match auth { Auth::Basic(username, password) => { diff --git a/tests/cli.rs b/tests/cli.rs index 2f5b332b..3626ccd3 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1908,6 +1908,53 @@ fn bearer_auth_from_session_is_used() { mock.assert(); } +#[test] +fn auth_netrc_is_not_persisted_in_session() { + let server = MockServer::start(); + let mock = server.mock(|when, _| { + when.header("Authorization", "Basic dXNlcjpwYXNz"); + }); + + let mut path_to_session = std::env::temp_dir(); + let file_name = random_string(); + path_to_session.push(file_name); + assert_eq!(path_to_session.exists(), false); + + let mut netrc = tempfile::NamedTempFile::new().unwrap(); + writeln!( + netrc, + "machine {}\nlogin user\npassword pass", + server.host() + ) + .unwrap(); + + get_command() + .env("NETRC", netrc.path()) + .arg(server.base_url()) + .arg("hello:world") + .arg(format!("--session={}", path_to_session.to_string_lossy())) + .assert() + .success(); + + mock.assert(); + + let session_content = read_to_string(path_to_session).unwrap(); + assert_eq!( + serde_json::from_str::(&session_content).unwrap(), + serde_json::json!({ + "__meta__": { + "about": "xh session file", + "xh": "0.0.0" + }, + "auth": { "type": null, "raw_auth": null }, + "cookies": {}, + "headers": { + "hello": "world" + } + }) + ); +} + #[test] fn print_intermediate_requests_and_responses() { let server1 = MockServer::start(); From a635aa6e09bb5814655ec1d7bb97d41a48cdc96a Mon Sep 17 00:00:00 2001 From: ducaale Date: Mon, 11 Oct 2021 19:17:59 +0300 Subject: [PATCH 15/21] disable tests that call self-signed.badssl.com --- tests/cli.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/cli.rs b/tests/cli.rs index 3626ccd3..e45f4320 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -800,6 +800,7 @@ fn bearer_auth() { // This intersects with both #41 and #59 #[test] +#[ignore = "https://self-signed.badssl.com cert has expired"] fn verify_default_yes() { get_command() .arg("-v") @@ -813,6 +814,7 @@ fn verify_default_yes() { } #[test] +#[ignore = "https://self-signed.badssl.com cert has expired"] fn verify_explicit_yes() { get_command() .arg("-v") @@ -841,6 +843,7 @@ fn verify_no() { } #[test] +#[ignore = "https://self-signed.badssl.com cert has expired"] fn verify_valid_file() { get_command() .arg("-v") From bad4c80e2ff3c7a7898cf7ed7d0cbe5196daac26 Mon Sep 17 00:00:00 2001 From: ducaale Date: Tue, 12 Oct 2021 21:38:50 +0300 Subject: [PATCH 16/21] Revert "disable tests that call self-signed.badssl.com" This reverts commit a635aa6e09bb5814655ec1d7bb97d41a48cdc96a. --- tests/cli.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/cli.rs b/tests/cli.rs index e45f4320..3626ccd3 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -800,7 +800,6 @@ fn bearer_auth() { // This intersects with both #41 and #59 #[test] -#[ignore = "https://self-signed.badssl.com cert has expired"] fn verify_default_yes() { get_command() .arg("-v") @@ -814,7 +813,6 @@ fn verify_default_yes() { } #[test] -#[ignore = "https://self-signed.badssl.com cert has expired"] fn verify_explicit_yes() { get_command() .arg("-v") @@ -843,7 +841,6 @@ fn verify_no() { } #[test] -#[ignore = "https://self-signed.badssl.com cert has expired"] fn verify_valid_file() { get_command() .arg("-v") From 57f731dda1dffc6f3c63cda7f4544e8b7af92c63 Mon Sep 17 00:00:00 2001 From: ducaale Date: Fri, 29 Oct 2021 09:29:50 +0300 Subject: [PATCH 17/21] rename Next to Context also changed Context to be a black box --- src/auth.rs | 12 +++++------- src/middleware.rs | 32 +++++++++++++++++++++----------- src/redirect.rs | 12 +++++------- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index 20d78fd9..d66390ec 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -10,7 +10,7 @@ use reqwest::header::{HeaderValue, AUTHORIZATION, WWW_AUTHENTICATE}; use reqwest::StatusCode; use crate::cli::AuthType; -use crate::middleware::{Middleware, Next}; +use crate::middleware::{Context, Middleware}; use crate::regex; use crate::utils::{clone_request, get_home_dir}; @@ -112,8 +112,8 @@ impl<'a> DigestAuthMiddleware<'a> { } impl<'a> Middleware for DigestAuthMiddleware<'a> { - fn handle(&mut self, mut request: Request, mut next: Next) -> Result { - let response = next.run(clone_request(&mut request)?)?; + fn handle(&mut self, mut ctx: Context, mut request: Request) -> Result { + let response = self.next(&mut ctx, clone_request(&mut request)?)?; match response.headers().get(WWW_AUTHENTICATE) { Some(wwwauth) if response.status() == StatusCode::UNAUTHORIZED => { let mut context = digest_auth::AuthContext::new( @@ -132,10 +132,8 @@ impl<'a> Middleware for DigestAuthMiddleware<'a> { if let Some(url) = std::env::var_os("XH_TEST_DIGEST_AUTH_URL") { *request.url_mut() = reqwest::Url::parse(&url.to_string_lossy())?; } - if let Some(ref mut printer) = next.printer { - printer(response, &mut request)?; - } - Ok(next.run(request)?) + self.print(&mut ctx, response, &mut request)?; + Ok(self.next(&mut ctx, request)?) } _ => Ok(response), } diff --git a/src/middleware.rs b/src/middleware.rs index c0ac8915..8c9d9588 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -1,40 +1,50 @@ use anyhow::Result; use reqwest::blocking::{Client, Request, Response}; -// TODO: come up with a more suitable name than "Next" -// maybe "Handler"?? -pub struct Next<'a, 'b> { +pub struct Context<'a, 'b> { client: &'a Client, - pub printer: Option<&'a mut (dyn FnMut(Response, &mut Request) -> Result<()> + 'b)>, + printer: Option<&'a mut (dyn FnMut(Response, &mut Request) -> Result<()> + 'b)>, middlewares: &'a mut [Box], } -impl<'a, 'b> Next<'a, 'b> { +impl<'a, 'b> Context<'a, 'b> { fn new( client: &'a Client, printer: Option<&'a mut (dyn FnMut(Response, &mut Request) -> Result<()> + 'b)>, middlewares: &'a mut [Box], ) -> Self { - Next { + Context { client, printer, middlewares, } } - pub fn run(&mut self, request: Request) -> Result { + fn execute(&mut self, request: Request) -> Result { match self.middlewares { [] => Ok(self.client.execute(request)?), [ref mut head, tail @ ..] => head.handle( + Context::new(self.client, self.printer.as_deref_mut(), tail), request, - Next::new(self.client, self.printer.as_deref_mut(), tail), ), } } } pub trait Middleware { - fn handle(&mut self, request: Request, next: Next) -> Result; + fn handle(&mut self, ctx: Context, request: Request) -> Result; + + fn next(&self, ctx: &mut Context, request: Request) -> Result { + ctx.execute(request) + } + + fn print(&self, ctx: &mut Context, response: Response, request: &mut Request) -> Result<()> { + if let Some(ref mut printer) = ctx.printer { + printer(response, request)? + } + + Ok(()) + } } pub struct ClientWithMiddleware<'a, T> @@ -69,13 +79,13 @@ where } pub fn execute(&mut self, request: Request) -> Result { - let mut next = Next::new( + let mut ctx = Context::new( self.client, self.printer .as_mut() .map(|p| p as &mut dyn FnMut(Response, &mut Request) -> Result<()>), &mut self.middlewares[..], ); - next.run(request) + ctx.execute(request) } } diff --git a/src/redirect.rs b/src/redirect.rs index 09e189ce..70c8dc98 100644 --- a/src/redirect.rs +++ b/src/redirect.rs @@ -6,7 +6,7 @@ use reqwest::header::{ }; use reqwest::{Method, StatusCode, Url}; -use crate::middleware::{Middleware, Next}; +use crate::middleware::{Context, Middleware}; use crate::utils::clone_request; pub struct RedirectFollower { @@ -20,11 +20,11 @@ impl RedirectFollower { } impl Middleware for RedirectFollower { - fn handle(&mut self, mut first_request: Request, mut next: Next) -> Result { + fn handle(&mut self, mut ctx: Context, mut first_request: Request) -> Result { // This buffers the body in case we need it again later // reqwest does *not* do this, it ignores 307/308 with a streaming body let mut request = clone_request(&mut first_request)?; - let mut response = next.run(first_request)?; + let mut response = self.next(&mut ctx, first_request)?; let mut remaining_redirects = self.max_redirects - 1; while let Some(mut next_request) = get_next_request(request, &response) { @@ -36,11 +36,9 @@ impl Middleware for RedirectFollower { self.max_redirects )); } - if let Some(ref mut printer) = next.printer { - printer(response, &mut next_request)?; - } + self.print(&mut ctx, response, &mut next_request)?; request = clone_request(&mut next_request)?; - response = next.run(next_request)?; + response = self.next(&mut ctx, next_request)?; } Ok(response) From c8a6e99c467f7ee55271579605f5e595a3d402f7 Mon Sep 17 00:00:00 2001 From: Mohamed Daahir Date: Sun, 14 Nov 2021 19:20:23 +0200 Subject: [PATCH 18/21] simplify the code for converting printer to dyn pointer Co-authored-by: Jan Verbeek --- src/middleware.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/middleware.rs b/src/middleware.rs index 8c9d9588..14b647c6 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -81,9 +81,7 @@ where pub fn execute(&mut self, request: Request) -> Result { let mut ctx = Context::new( self.client, - self.printer - .as_mut() - .map(|p| p as &mut dyn FnMut(Response, &mut Request) -> Result<()>), + self.printer.as_mut().map(|p| p as _), &mut self.middlewares[..], ); ctx.execute(request) From 6bad5ab4b14f4464901405b8d583fa2ddf193d5b Mon Sep 17 00:00:00 2001 From: ducaale Date: Sun, 14 Nov 2021 19:22:24 +0200 Subject: [PATCH 19/21] hide the bearer flag --- src/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index 9e4f4e29..72ed80c1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -147,7 +147,7 @@ pub struct Cli { pub auth: Option, /// Authenticate with a bearer token. - #[structopt(long, value_name = "TOKEN")] + #[structopt(long, value_name = "TOKEN", hidden = true)] pub bearer: Option, /// Do not use credentials from .netrc From 23ab96af2e560aa15bce13f47f9ad85e34e81ce0 Mon Sep 17 00:00:00 2001 From: ducaale Date: Sun, 14 Nov 2021 19:30:58 +0200 Subject: [PATCH 20/21] test digest auth using a httpbin.org --- tests/cli.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/cli.rs b/tests/cli.rs index 7e70ef75..f11f7296 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -2275,6 +2275,26 @@ fn digest_auth() { mock2.assert(); } +#[test] +fn successful_digest_auth() { + get_command() + .arg("--auth-type=digest") + .arg("--auth=ahmed:12345") + .arg("httpbin.org/digest-auth/5/ahmed/12345") + .assert() + .stdout(contains("HTTP/1.1 200 OK")); +} + +#[test] +fn unsuccessful_digest_auth() { + get_command() + .arg("--auth-type=digest") + .arg("--auth=ahmed:wrongpass") + .arg("httpbin.org/digest-auth/5/ahmed/12345") + .assert() + .stdout(contains("HTTP/1.1 401 Unauthorized")); +} + #[test] fn digest_auth_with_redirection() { let server1 = MockServer::start(); From 5614dff9cf26f2153e29c7e660bb38529580b43c Mon Sep 17 00:00:00 2001 From: ducaale Date: Sun, 14 Nov 2021 20:59:30 +0200 Subject: [PATCH 21/21] update the docs for --auth flag --- src/cli.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index bacd4947..cbcaa67f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -156,11 +156,14 @@ pub struct Cli { #[structopt(short = "A", long, possible_values = &AuthType::variants(), case_insensitive = true)] pub auth_type: Option, - /// Authenticate as USER with PASS. PASS will be prompted if missing. + /// Authenticate as USER with PASS or with token. /// - /// Use a trailing colon (i.e. `USER:`) to authenticate with just a username. + /// PASS will be prompted if missing. Use a trailing colon (i.e. `USER:`) + /// to authenticate with just a username. + /// + /// if --auth-type=bearer then --auth expects a token /// {n}{n}{n} - #[structopt(short = "a", long, value_name = "USER[:PASS]")] + #[structopt(short = "a", long, value_name = "USER[:PASS] | token")] pub auth: Option, /// Authenticate with a bearer token.