use anyhow::Result;
use reqwest::{
Method,
blocking::{Client, Response},
};
use serde_json::Value;
use std::io::Write;
use termcolor::{ColorChoice, StandardStream, WriteColor};
use super::hmac::{AuthMode, generate_hmac_auth_header};
use super::token::{get_token, get_token_path};
pub fn make_authenticated_request(
method: Method,
url: &str,
body: Option<Value>,
) -> Result<Response> {
match try_authenticated_request(method.clone(), url, body.clone(), false) {
Ok(response) => {
let status = response.status();
if status == 401 || status == 403 {
handle_auth_failure_and_retry(method, url, body)
} else {
Ok(response)
}
}
Err(e) => Err(e),
}
}
fn try_authenticated_request(
method: Method,
url: &str,
body: Option<Value>,
force_refresh: bool,
) -> Result<Response> {
let token = if force_refresh {
let token_path = get_token_path()?;
if token_path.exists() {
std::fs::remove_file(&token_path)?;
}
get_token()?
} else {
get_token()?
};
let client = Client::new();
let mut request = client
.request(method, url)
.header("Authorization", format!("Bearer {}", token))
.header("Accept", "application/json")
.header("User-Agent", user_agent());
if let Some(json_body) = body {
request = request.json(&json_body);
}
Ok(request.send()?)
}
fn user_agent() -> String {
format!("forklaunch-cli/{}", env!("CARGO_PKG_VERSION"))
}
fn handle_auth_failure_and_retry(
method: Method,
url: &str,
body: Option<Value>,
) -> Result<Response> {
let mut stdout = StandardStream::stdout(ColorChoice::Always);
let token_path = get_token_path()?;
if token_path.exists() {
std::fs::remove_file(&token_path)?;
}
match get_token() {
Ok(_) => {
try_authenticated_request(method, url, body, false)
}
Err(_) => {
log_warn!(stdout, "\nAuthentication expired. Please log in again.");
crate::login::login()?;
try_authenticated_request(method, url, body, false)
}
}
}
pub fn get(url: &str) -> Result<Response> {
make_authenticated_request(Method::GET, url, None)
}
pub fn post(url: &str, body: Value) -> Result<Response> {
make_authenticated_request(Method::POST, url, Some(body))
}
pub fn put(url: &str, body: Value) -> Result<Response> {
make_authenticated_request(Method::PUT, url, Some(body))
}
pub fn patch(url: &str, body: Value) -> Result<Response> {
make_authenticated_request(Method::PATCH, url, Some(body))
}
pub fn delete(url: &str) -> Result<Response> {
make_authenticated_request(Method::DELETE, url, None)
}
pub fn post_unauthenticated(url: &str, body: Value) -> Result<Response> {
let client = Client::new();
Ok(client
.post(url)
.header("Accept", "application/json")
.header("User-Agent", user_agent())
.json(&body)
.send()?)
}
fn extract_url_path(url: &str) -> Result<String> {
let after_scheme = url
.find("://")
.map(|i| i + 3)
.ok_or_else(|| anyhow::anyhow!("Invalid URL: no scheme found in '{}'", url))?;
let path_start = url[after_scheme..]
.find('/')
.map(|i| after_scheme + i)
.unwrap_or(url.len());
if path_start >= url.len() {
return Ok("/".to_string());
}
Ok(url[path_start..].to_string())
}
fn make_hmac_request(
secret_key: &str,
method: Method,
url: &str,
body: Option<Value>,
) -> Result<Response> {
let path = extract_url_path(url)?;
make_hmac_request_with_sign_path(secret_key, method, url, &path, body)
}
fn make_hmac_request_with_sign_path(
secret_key: &str,
method: Method,
url: &str,
sign_path: &str,
body: Option<Value>,
) -> Result<Response> {
let auth_header = generate_hmac_auth_header(
secret_key,
method.as_str(),
sign_path,
body.as_ref(),
)?;
let client = Client::new();
let mut request = client
.request(method, url)
.header("Authorization", auth_header)
.header("Accept", "application/json")
.header("User-Agent", user_agent());
if let Some(json_body) = body {
request = request.json(&json_body);
}
Ok(request.send()?)
}
pub fn post_with_auth(auth_mode: &AuthMode, url: &str, body: Value) -> Result<Response> {
match auth_mode {
AuthMode::Jwt => post(url, body),
AuthMode::Hmac { secret_key } => {
make_hmac_request(secret_key, Method::POST, url, Some(body))
}
}
}
pub fn post_with_auth_and_sign_path(
auth_mode: &AuthMode,
url: &str,
sign_path: &str,
body: Value,
) -> Result<Response> {
match auth_mode {
AuthMode::Jwt => post(url, body),
AuthMode::Hmac { secret_key } => make_hmac_request_with_sign_path(
secret_key,
Method::POST,
url,
sign_path,
Some(body),
),
}
}
pub fn get_with_auth(auth_mode: &AuthMode, url: &str) -> Result<Response> {
match auth_mode {
AuthMode::Jwt => get(url),
AuthMode::Hmac { secret_key } => make_hmac_request(secret_key, Method::GET, url, None),
}
}
pub fn put_with_auth(auth_mode: &AuthMode, url: &str, body: Value) -> Result<Response> {
match auth_mode {
AuthMode::Jwt => put(url, body),
AuthMode::Hmac { secret_key } => {
make_hmac_request(secret_key, Method::PUT, url, Some(body))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_url_path_basic() {
assert_eq!(
extract_url_path("https://api.example.com/releases").unwrap(),
"/releases"
);
}
#[test]
fn test_extract_url_path_with_port() {
assert_eq!(
extract_url_path("https://api.example.com:8080/releases/internal").unwrap(),
"/releases/internal"
);
}
#[test]
fn test_extract_url_path_with_query() {
assert_eq!(
extract_url_path("https://api.example.com/deployments/123?status=true").unwrap(),
"/deployments/123?status=true"
);
}
#[test]
fn test_extract_url_path_no_path() {
assert_eq!(
extract_url_path("https://api.example.com").unwrap(),
"/"
);
}
#[test]
fn test_extract_url_path_root() {
assert_eq!(
extract_url_path("https://api.example.com/").unwrap(),
"/"
);
}
}