diff --git a/src/api.rs b/src/api.rs index aaf5595..aab64e2 100644 --- a/src/api.rs +++ b/src/api.rs @@ -4,10 +4,11 @@ use std::io::Read; use std::time::Duration; -use reqwest::blocking::Client; use serde::Deserialize; use thiserror::Error; +use crate::http; + const MAX_RESPONSE_BYTES: u64 = 64 * 1024; #[derive(Debug, Deserialize)] @@ -37,7 +38,7 @@ pub enum ApiError { /// GET /v1/auth/me — returns the authenticated user's identity. pub fn me(api_base: &str, access_token: &str) -> Result { let endpoint = format!("{}/v1/auth/me", api_base.trim_end_matches('/')); - let http = Client::builder().timeout(Duration::from_secs(15)).build()?; + let http = http::client(Duration::from_secs(15))?; let resp = http .get(&endpoint) diff --git a/src/commands/login.rs b/src/commands/login.rs index eb814b3..fff5a0e 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -11,11 +11,11 @@ use std::time::{Duration, SystemTime}; use anyhow::{Context, Result, anyhow}; use clap::Args; -use reqwest::blocking::Client; use crate::auth::{self, AuthError}; use crate::browser; use crate::credentials::{self, Credentials}; +use crate::http; use super::{DEFAULT_API_BASE, DEFAULT_WEB_BASE, ENV_API_URL, ENV_WEB_URL}; @@ -44,10 +44,7 @@ pub fn run(_args: LoginArgs) -> Result<()> { return Err(anyhow!("login: no code entered")); } - let http = Client::builder() - .timeout(Duration::from_secs(30)) - .build() - .context("login: build HTTP client")?; + let http = http::client(Duration::from_secs(30)).context("login: build HTTP client")?; let tokens = match auth::exchange_code(&http, &api_base, code, &pkce) { Ok(t) => t, diff --git a/src/http.rs b/src/http.rs new file mode 100644 index 0000000..6c6effd --- /dev/null +++ b/src/http.rs @@ -0,0 +1,45 @@ +//! Shared reqwest Client builder. Centralizes the User-Agent string so +//! every outbound request labels itself as the CLI — the platform's +//! session list (and any audit log) can then render "MirrorStack CLI" +//! instead of falling through to a generic browser-UA parser. + +use std::time::Duration; + +use reqwest::blocking::{Client, ClientBuilder}; + +/// User-Agent shape: `mirrorstack-cli/ (; )`. +/// Mirrors the convention of curl, gh CLI, brew, etc. +fn user_agent() -> String { + format!( + "mirrorstack-cli/{} ({}; {})", + env!("CARGO_PKG_VERSION"), + std::env::consts::OS, + std::env::consts::ARCH, + ) +} + +/// Construct a reqwest blocking Client with the CLI's User-Agent and a +/// caller-supplied timeout. Use this everywhere we make HTTP calls so +/// the UA stays consistent. +pub fn client(timeout: Duration) -> reqwest::Result { + builder(timeout).build() +} + +/// Same as [`client`] but returns the builder so callers can layer on +/// additional config before `.build()`. Currently unused — exposed so +/// the seam is obvious for future commands. +pub fn builder(timeout: Duration) -> ClientBuilder { + Client::builder().timeout(timeout).user_agent(user_agent()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn user_agent_matches_expected_shape() { + let ua = user_agent(); + assert!(ua.starts_with("mirrorstack-cli/"), "got {ua}"); + assert!(ua.contains('('), "expected (os; arch) in {ua}"); + } +} diff --git a/src/main.rs b/src/main.rs index 2ab2b2f..17b9e30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod auth; mod browser; mod commands; mod credentials; +mod http; fn main() -> ExitCode { // Load .env from CWD if present. Process env vars still take precedence