diff --git a/rttp_client/src/client.rs b/rttp_client/src/client.rs index eebbba2..60f6b1c 100644 --- a/rttp_client/src/client.rs +++ b/rttp_client/src/client.rs @@ -3,7 +3,7 @@ use crate::connection::AsyncConnection; use crate::connection::BlockConnection; use crate::request::{RawRequest, Request}; use crate::response::Response; -use crate::types::{Header, IntoHeader, IntoPara, Proxy, ToFormData, ToRoUrl}; +use crate::types::{Auth, Header, IntoHeader, IntoPara, Proxy, ToFormData, ToRoUrl}; use crate::{error, Config}; #[derive(Debug)] @@ -120,9 +120,20 @@ impl HttpClient { self } - /// Not support now - pub fn auth(&mut self) -> &mut Self { - unimplemented!() + /// Set HTTP authentication. Supports Basic Auth and Bearer Token. + /// + /// # Examples + /// + /// ```rust + /// use rttp_client::HttpClient; + /// use rttp_client::types::Auth; + /// + /// let mut client = HttpClient::new(); + /// client.auth(Auth::basic("user", "secret")); + /// client.auth(Auth::bearer("my-token")); + /// ``` + pub fn auth>(&mut self, auth: A) -> &mut Self { + self.header(("Authorization", auth.as_ref().header_value().as_str())) } /// Add request header diff --git a/rttp_client/src/types/auth.rs b/rttp_client/src/types/auth.rs new file mode 100644 index 0000000..76d5548 --- /dev/null +++ b/rttp_client/src/types/auth.rs @@ -0,0 +1,88 @@ +use base64::engine::general_purpose::STANDARD; +use base64::Engine; + +/// HTTP authentication type for use with [`rttp_client::HttpClient::auth`]. +/// +/// # Examples +/// +/// ```rust +/// use rttp_client::types::Auth; +/// +/// let basic = Auth::basic("user", "password"); +/// let bearer = Auth::bearer("my-token"); +/// ``` +#[derive(Clone, Debug)] +pub enum Auth { + /// HTTP Basic authentication (`Authorization: Basic `). + Basic { username: String, password: String }, + /// Bearer token authentication (`Authorization: Bearer `). + Bearer { token: String }, +} + +impl Auth { + /// Create a Basic auth credential. + pub fn basic, P: AsRef>(username: U, password: P) -> Self { + Auth::Basic { + username: username.as_ref().to_string(), + password: password.as_ref().to_string(), + } + } + + /// Create a Bearer token credential. + pub fn bearer>(token: T) -> Self { + Auth::Bearer { + token: token.as_ref().to_string(), + } + } + + /// Return the `Authorization` header value for this credential. + pub fn header_value(&self) -> String { + match self { + Auth::Basic { username, password } => { + let encoded = STANDARD.encode(format!("{}:{}", username, password)); + format!("Basic {}", encoded) + } + Auth::Bearer { token } => format!("Bearer {}", token), + } + } +} + +impl AsRef for Auth { + fn as_ref(&self) -> &Auth { + self + } +} + +#[cfg(test)] +mod tests { + use super::Auth; + + #[test] + fn test_basic_auth_header_value() { + let auth = Auth::basic("user", "secret"); + // base64("user:secret") = "dXNlcjpzZWNyZXQ=" + assert_eq!("Basic dXNlcjpzZWNyZXQ=", auth.header_value()); + } + + #[test] + fn test_basic_auth_empty_password() { + let auth = Auth::basic("admin", ""); + // base64("admin:") = "YWRtaW46" + assert_eq!("Basic YWRtaW46", auth.header_value()); + } + + #[test] + fn test_bearer_auth_header_value() { + let auth = Auth::bearer("my-token-123"); + assert_eq!("Bearer my-token-123", auth.header_value()); + } + + #[test] + fn test_bearer_auth_complex_token() { + let auth = Auth::bearer("eyJhbGciOiJIUzI1NiJ9.payload.signature"); + assert_eq!( + "Bearer eyJhbGciOiJIUzI1NiJ9.payload.signature", + auth.header_value() + ); + } +} diff --git a/rttp_client/src/types/mod.rs b/rttp_client/src/types/mod.rs index ea96072..0050d22 100644 --- a/rttp_client/src/types/mod.rs +++ b/rttp_client/src/types/mod.rs @@ -1,3 +1,4 @@ +pub use self::auth::Auth; pub use self::cookie::Cookie; pub use self::form_data::*; pub use self::header::*; @@ -6,6 +7,7 @@ pub use self::proxy::*; pub use self::status::*; pub use self::url::*; +mod auth; mod cookie; mod form_data; mod header; diff --git a/rttp_client/tests/support/mod.rs b/rttp_client/tests/support/mod.rs index 81258b3..2caedce 100644 --- a/rttp_client/tests/support/mod.rs +++ b/rttp_client/tests/support/mod.rs @@ -108,6 +108,51 @@ pub fn spawn_redirect_server() -> (SocketAddr, JoinHandle<()>) { (addr, handle) } +pub fn spawn_auth_echo_server() -> (SocketAddr, JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind auth echo server"); + let addr = listener.local_addr().expect("auth echo addr"); + let handle = thread::spawn(move || { + if let Ok((mut stream, _)) = listener.accept() { + let mut request = Vec::new(); + let mut buf = [0u8; 1024]; + loop { + let Ok(read) = stream.read(&mut buf) else { + break; + }; + if read == 0 { + break; + } + request.extend_from_slice(&buf[..read]); + if request.windows(4).any(|w| w == b"\r\n\r\n") { + break; + } + } + + let req_str = String::from_utf8_lossy(&request); + let auth_value = req_str + .lines() + .find_map(|line| { + let (name, value) = line.split_once(':')?; + if name.eq_ignore_ascii_case("authorization") { + Some(value.trim().to_string()) + } else { + None + } + }) + .unwrap_or_default(); + + let body = auth_value.as_bytes(); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + let _ = stream.write_all(response.as_bytes()); + let _ = stream.write_all(body); + } + }); + (addr, handle) +} + #[cfg(feature = "tls-rustls")] pub fn spawn_tls_server() -> (SocketAddr, JoinHandle<()>) { use rcgen::generate_simple_self_signed; diff --git a/rttp_client/tests/test_http_basic.rs b/rttp_client/tests/test_http_basic.rs index 1fae144..212245d 100644 --- a/rttp_client/tests/test_http_basic.rs +++ b/rttp_client/tests/test_http_basic.rs @@ -2,7 +2,7 @@ mod support; use std::collections::HashMap; -use rttp_client::types::{Para, Proxy, RoUrl}; +use rttp_client::types::{Auth, Para, Proxy, RoUrl}; use rttp_client::{Config, HttpClient}; fn client() -> HttpClient { @@ -202,3 +202,30 @@ fn test_connection_closed() { .emit(); assert!(resp4.is_ok()); } + +#[test] +fn test_basic_auth() { + let (addr, _handle) = support::spawn_auth_echo_server(); + let response = client() + .get() + .url(format!("http://{}/", addr)) + .auth(Auth::basic("user", "secret")) + .emit(); + assert!(response.is_ok()); + let response = response.unwrap(); + // base64("user:secret") = "dXNlcjpzZWNyZXQ=" + assert_eq!("Basic dXNlcjpzZWNyZXQ=", response.body().string().unwrap()); +} + +#[test] +fn test_bearer_auth() { + let (addr, _handle) = support::spawn_auth_echo_server(); + let response = client() + .get() + .url(format!("http://{}/", addr)) + .auth(Auth::bearer("my-token-abc")) + .emit(); + assert!(response.is_ok()); + let response = response.unwrap(); + assert_eq!("Bearer my-token-abc", response.body().string().unwrap()); +}