blob: 1a55b903c60a722bc31b614ecaa968bf0f1eedcb [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::{BoxedFuture, Error, MaybeSend, Result};
use bytes::Bytes;
use std::collections::HashMap;
use std::fmt::Debug;
use std::future::Future;
use std::ops::Deref;
use std::path::PathBuf;
use std::sync::Arc;
/// Context provides the context for the request signing.
///
/// ## Important
///
/// reqsign provides NO default implementations. Users MAY configure components they need.
/// Any unconfigured component will use a no-op implementation that returns errors or empty values when called.
///
/// ## Example
///
/// ```
/// use reqsign_core::{Context, OsEnv};
///
/// // Create a context with explicit implementations
/// let ctx = Context::new()
/// .with_env(OsEnv); // Optionally configure environment implementation
/// ```
#[derive(Clone)]
pub struct Context {
fs: Arc<dyn FileReadDyn>,
http: Arc<dyn HttpSendDyn>,
env: Arc<dyn Env>,
cmd: Arc<dyn CommandExecuteDyn>,
}
impl Debug for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Context")
.field("fs", &self.fs)
.field("http", &self.http)
.field("env", &self.env)
.field("cmd", &self.cmd)
.finish()
}
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
impl Context {
/// Create a new Context with no-op implementations.
///
/// All components use no-op implementations by default.
/// Use the `with_*` methods to configure the components you need.
///
/// ```
/// use reqsign_core::Context;
///
/// let ctx = Context::new();
/// // All components use no-op implementations by default
/// // You can configure specific components as needed:
/// // ctx.with_file_read(my_file_reader)
/// // .with_http_send(my_http_client)
/// // .with_env(my_env_provider);
/// ```
pub fn new() -> Self {
Self {
fs: Arc::new(NoopFileRead),
http: Arc::new(NoopHttpSend),
env: Arc::new(NoopEnv),
cmd: Arc::new(NoopCommandExecute),
}
}
/// Replace the file reader implementation.
pub fn with_file_read(mut self, fs: impl FileRead) -> Self {
self.fs = Arc::new(fs);
self
}
/// Replace the HTTP client implementation.
pub fn with_http_send(mut self, http: impl HttpSend) -> Self {
self.http = Arc::new(http);
self
}
/// Replace the environment implementation.
pub fn with_env(mut self, env: impl Env) -> Self {
self.env = Arc::new(env);
self
}
/// Replace the command executor implementation.
pub fn with_command_execute(mut self, cmd: impl CommandExecute) -> Self {
self.cmd = Arc::new(cmd);
self
}
/// Read the file content entirely in `Vec<u8>`.
#[inline]
pub async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
self.fs.file_read_dyn(path).await
}
/// Read the file content entirely in `String`.
pub async fn file_read_as_string(&self, path: &str) -> Result<String> {
let bytes = self.file_read(path).await?;
Ok(String::from_utf8_lossy(&bytes).to_string())
}
/// Send http request and return the response.
#[inline]
pub async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
self.http.http_send_dyn(req).await
}
/// Send http request and return the response as string.
pub async fn http_send_as_string(
&self,
req: http::Request<Bytes>,
) -> Result<http::Response<String>> {
let (parts, body) = self.http.http_send_dyn(req).await?.into_parts();
let body = String::from_utf8_lossy(&body).to_string();
Ok(http::Response::from_parts(parts, body))
}
/// Get the home directory of the current user.
#[inline]
pub fn home_dir(&self) -> Option<PathBuf> {
self.env.home_dir()
}
/// Expand `~` in input path.
///
/// - If path not starts with `~/` or `~\\`, returns `Some(path)` directly.
/// - Otherwise, replace `~` with home dir instead.
/// - If home_dir is not found, returns `None`.
pub fn expand_home_dir(&self, path: &str) -> Option<String> {
if !path.starts_with("~/") && !path.starts_with("~\\") {
Some(path.to_string())
} else {
self.home_dir()
.map(|home| path.replace('~', &home.to_string_lossy()))
}
}
/// Get the environment variable.
///
/// - Returns `Some(v)` if the environment variable is found and is valid utf-8.
/// - Returns `None` if the environment variable is not found or value is invalid.
#[inline]
pub fn env_var(&self, key: &str) -> Option<String> {
self.env.var(key)
}
/// Returns a hashmap of (variable, value) pairs of strings, for all the
/// environment variables of the current process.
#[inline]
pub fn env_vars(&self) -> HashMap<String, String> {
self.env.vars()
}
/// Execute an external command with the given program and arguments.
///
/// Returns the command output including exit status, stdout, and stderr.
pub async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
self.cmd.command_execute_dyn(program, args).await
}
}
/// FileRead is used to read the file content entirely in `Vec<u8>`.
///
/// This could be used by `Load` to load the credential from the file.
pub trait FileRead: Debug + Send + Sync + 'static {
/// Read the file content entirely in `Vec<u8>`.
fn file_read(&self, path: &str) -> impl Future<Output = Result<Vec<u8>>> + MaybeSend;
}
/// FileReadDyn is the dyn version of [`FileRead`].
pub trait FileReadDyn: Debug + Send + Sync + 'static {
/// Dyn version of [`FileRead::file_read`].
fn file_read_dyn<'a>(&'a self, path: &'a str) -> BoxedFuture<'a, Result<Vec<u8>>>;
}
impl<T: FileRead + ?Sized> FileReadDyn for T {
fn file_read_dyn<'a>(&'a self, path: &'a str) -> BoxedFuture<'a, Result<Vec<u8>>> {
Box::pin(self.file_read(path))
}
}
impl<T: FileReadDyn + ?Sized> FileRead for Arc<T> {
async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
self.deref().file_read_dyn(path).await
}
}
/// HttpSend is used to send http request during the signing process.
///
/// For example, fetch IMDS token from AWS or OAuth2 refresh token. This trait is designed
/// especially for the signer, please don't use it as a general http client.
pub trait HttpSend: Debug + Send + Sync + 'static {
/// Send http request and return the response.
fn http_send(
&self,
req: http::Request<Bytes>,
) -> impl Future<Output = Result<http::Response<Bytes>>> + MaybeSend;
}
/// HttpSendDyn is the dyn version of [`HttpSend`].
pub trait HttpSendDyn: Debug + Send + Sync + 'static {
/// Dyn version of [`HttpSend::http_send`].
fn http_send_dyn(
&self,
req: http::Request<Bytes>,
) -> BoxedFuture<'_, Result<http::Response<Bytes>>>;
}
impl<T: HttpSend + ?Sized> HttpSendDyn for T {
fn http_send_dyn(
&self,
req: http::Request<Bytes>,
) -> BoxedFuture<'_, Result<http::Response<Bytes>>> {
Box::pin(self.http_send(req))
}
}
impl<T: HttpSendDyn + ?Sized> HttpSend for Arc<T> {
async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
self.deref().http_send_dyn(req).await
}
}
/// Permits parameterizing the home functions via the _from variants
pub trait Env: Debug + Send + Sync + 'static {
/// Get an environment variable.
///
/// - Returns `Some(v)` if the environment variable is found and is valid utf-8.
/// - Returns `None` if the environment variable is not found or value is invalid.
fn var(&self, key: &str) -> Option<String>;
/// Returns a hashmap of (variable, value) pairs of strings, for all the
/// environment variables of the current process.
fn vars(&self) -> HashMap<String, String>;
/// Return the path to the users home dir, returns `None` if any error occurs.
fn home_dir(&self) -> Option<PathBuf>;
}
/// Implements Env for the OS context, both Unix style and Windows.
#[derive(Debug, Copy, Clone)]
pub struct OsEnv;
impl Env for OsEnv {
fn var(&self, key: &str) -> Option<String> {
std::env::var_os(key)?.into_string().ok()
}
fn vars(&self) -> HashMap<String, String> {
std::env::vars().collect()
}
#[cfg(any(unix, target_os = "redox"))]
fn home_dir(&self) -> Option<PathBuf> {
#[allow(deprecated)]
std::env::home_dir()
}
#[cfg(windows)]
fn home_dir(&self) -> Option<PathBuf> {
windows::home_dir_inner()
}
#[cfg(target_arch = "wasm32")]
fn home_dir(&self) -> Option<PathBuf> {
None
}
}
/// StaticEnv provides a static env environment.
///
/// This is useful for testing or for providing a fixed environment.
#[derive(Debug, Clone, Default)]
pub struct StaticEnv {
/// The home directory to use.
pub home_dir: Option<PathBuf>,
/// The environment variables to use.
pub envs: HashMap<String, String>,
}
impl Env for StaticEnv {
fn var(&self, key: &str) -> Option<String> {
self.envs.get(key).cloned()
}
fn vars(&self) -> HashMap<String, String> {
self.envs.clone()
}
fn home_dir(&self) -> Option<PathBuf> {
self.home_dir.clone()
}
}
/// CommandOutput represents the output of a command execution.
#[derive(Debug, Clone)]
pub struct CommandOutput {
/// Exit status code (0 for success)
pub status: i32,
/// Standard output as bytes
pub stdout: Vec<u8>,
/// Standard error as bytes
pub stderr: Vec<u8>,
}
impl CommandOutput {
/// Check if the command exited successfully.
pub fn success(&self) -> bool {
self.status == 0
}
}
/// CommandExecute is used to execute external commands for credential retrieval.
///
/// This trait abstracts command execution to support different runtime environments:
/// - Tokio-based async execution
/// - Blocking execution for non-async contexts
/// - WebAssembly environments (returning errors)
/// - Mock implementations for testing
pub trait CommandExecute: Debug + Send + Sync + 'static {
/// Execute a command with the given program and arguments.
fn command_execute<'a>(
&'a self,
program: &'a str,
args: &'a [&'a str],
) -> impl Future<Output = Result<CommandOutput>> + MaybeSend + 'a;
}
/// CommandExecuteDyn is the dyn version of [`CommandExecute`].
pub trait CommandExecuteDyn: Debug + Send + Sync + 'static {
/// Dyn version of [`CommandExecute::command_execute`].
fn command_execute_dyn<'a>(
&'a self,
program: &'a str,
args: &'a [&'a str],
) -> BoxedFuture<'a, Result<CommandOutput>>;
}
impl<T: CommandExecute + ?Sized> CommandExecuteDyn for T {
fn command_execute_dyn<'a>(
&'a self,
program: &'a str,
args: &'a [&'a str],
) -> BoxedFuture<'a, Result<CommandOutput>> {
Box::pin(self.command_execute(program, args))
}
}
impl<T: CommandExecuteDyn + ?Sized> CommandExecute for Arc<T> {
async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
self.deref().command_execute_dyn(program, args).await
}
}
/// NoopFileRead is a no-op implementation that always returns an error.
///
/// This is used when no file reader is configured.
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopFileRead;
impl FileRead for NoopFileRead {
async fn file_read(&self, _path: &str) -> Result<Vec<u8>> {
Err(Error::unexpected(
"file reading not supported: no file reader configured",
))
}
}
/// NoopHttpSend is a no-op implementation that always returns an error.
///
/// This is used when no HTTP client is configured.
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopHttpSend;
impl HttpSend for NoopHttpSend {
async fn http_send(&self, _req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
Err(Error::unexpected(
"HTTP sending not supported: no HTTP client configured",
))
}
}
/// NoopEnv is a no-op implementation that always returns None/empty.
///
/// This is used when no environment is configured.
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopEnv;
impl Env for NoopEnv {
fn var(&self, _key: &str) -> Option<String> {
None
}
fn vars(&self) -> HashMap<String, String> {
HashMap::new()
}
fn home_dir(&self) -> Option<PathBuf> {
None
}
}
/// NoopCommandExecute is a no-op implementation that always returns an error.
///
/// This is used when no command executor is configured.
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopCommandExecute;
impl CommandExecute for NoopCommandExecute {
async fn command_execute(&self, _program: &str, _args: &[&str]) -> Result<CommandOutput> {
Err(Error::unexpected(
"command execution not supported: no command executor configured",
))
}
}
#[cfg(target_os = "windows")]
mod windows {
use std::env;
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use std::path::PathBuf;
use std::ptr;
use std::slice;
use windows_sys::Win32::Foundation::S_OK;
use windows_sys::Win32::System::Com::CoTaskMemFree;
use windows_sys::Win32::UI::Shell::{
FOLDERID_Profile, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
};
pub fn home_dir_inner() -> Option<PathBuf> {
env::var_os("USERPROFILE")
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.or_else(home_dir_crt)
}
#[cfg(not(target_vendor = "uwp"))]
fn home_dir_crt() -> Option<PathBuf> {
unsafe {
let mut path = ptr::null_mut();
match SHGetKnownFolderPath(
&FOLDERID_Profile,
KF_FLAG_DONT_VERIFY as u32,
std::ptr::null_mut(),
&mut path,
) {
S_OK => {
let path_slice = slice::from_raw_parts(path, wcslen(path));
let s = OsString::from_wide(&path_slice);
CoTaskMemFree(path.cast());
Some(PathBuf::from(s))
}
_ => {
// Free any allocated memory even on failure. A null ptr is a no-op for `CoTaskMemFree`.
CoTaskMemFree(path.cast());
None
}
}
}
}
#[cfg(target_vendor = "uwp")]
fn home_dir_crt() -> Option<PathBuf> {
None
}
unsafe extern "C" {
unsafe fn wcslen(buf: *const u16) -> usize;
}
#[cfg(not(target_vendor = "uwp"))]
#[cfg(test)]
mod tests {
use super::home_dir_inner;
use std::env;
use std::ops::Deref;
use std::path::{Path, PathBuf};
#[test]
fn test_with_without() {
let olduserprofile = env::var_os("USERPROFILE").unwrap();
unsafe {
env::remove_var("HOME");
env::remove_var("USERPROFILE");
}
assert_eq!(home_dir_inner(), Some(PathBuf::from(olduserprofile)));
let home = Path::new(r"C:\Users\foo tar baz");
unsafe {
env::set_var("HOME", home.as_os_str());
env::set_var("USERPROFILE", home.as_os_str());
}
assert_ne!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
assert_eq!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
}
}
}