Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions rttp_client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<A: AsRef<Auth>>(&mut self, auth: A) -> &mut Self {
self.header(("Authorization", auth.as_ref().header_value().as_str()))
}

/// Add request header
Expand Down
88 changes: 88 additions & 0 deletions rttp_client/src/types/auth.rs
Original file line number Diff line number Diff line change
@@ -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 <base64(user:pass)>`).
Basic { username: String, password: String },
/// Bearer token authentication (`Authorization: Bearer <token>`).
Bearer { token: String },
}

impl Auth {
/// Create a Basic auth credential.
pub fn basic<U: AsRef<str>, P: AsRef<str>>(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<T: AsRef<str>>(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<Auth> 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()
);
}
}
2 changes: 2 additions & 0 deletions rttp_client/src/types/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub use self::auth::Auth;
pub use self::cookie::Cookie;
pub use self::form_data::*;
pub use self::header::*;
Expand All @@ -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;
Expand Down
45 changes: 45 additions & 0 deletions rttp_client/tests/support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
29 changes: 28 additions & 1 deletion rttp_client/tests/test_http_basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
}
Loading