reqsign_core/
context.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::{Error, Result};
19use bytes::Bytes;
20use std::collections::HashMap;
21use std::fmt::Debug;
22use std::path::PathBuf;
23use std::sync::Arc;
24
25/// Context provides the context for the request signing.
26///
27/// ## Important
28///
29/// reqsign provides NO default implementations. Users MAY configure components they need.
30/// Any unconfigured component will use a no-op implementation that returns errors or empty values when called.
31///
32/// ## Example
33///
34/// ```
35/// use reqsign_core::{Context, OsEnv};
36///
37/// // Create a context with explicit implementations
38/// let ctx = Context::new()
39///     .with_env(OsEnv);  // Optionally configure environment implementation
40/// ```
41#[derive(Clone)]
42pub struct Context {
43    fs: Arc<dyn FileRead>,
44    http: Arc<dyn HttpSend>,
45    env: Arc<dyn Env>,
46    cmd: Arc<dyn CommandExecute>,
47}
48
49impl Debug for Context {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.debug_struct("Context")
52            .field("fs", &self.fs)
53            .field("http", &self.http)
54            .field("env", &self.env)
55            .field("cmd", &self.cmd)
56            .finish()
57    }
58}
59
60impl Default for Context {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl Context {
67    /// Create a new Context with no-op implementations.
68    ///
69    /// All components use no-op implementations by default.
70    /// Use the `with_*` methods to configure the components you need.
71    ///
72    /// ```
73    /// use reqsign_core::Context;
74    ///
75    /// let ctx = Context::new();
76    /// // All components use no-op implementations by default
77    /// // You can configure specific components as needed:
78    /// // ctx.with_file_read(my_file_reader)
79    /// //    .with_http_send(my_http_client)
80    /// //    .with_env(my_env_provider);
81    /// ```
82    pub fn new() -> Self {
83        Self {
84            fs: Arc::new(NoopFileRead),
85            http: Arc::new(NoopHttpSend),
86            env: Arc::new(NoopEnv),
87            cmd: Arc::new(NoopCommandExecute),
88        }
89    }
90
91    /// Replace the file reader implementation.
92    pub fn with_file_read(mut self, fs: impl FileRead) -> Self {
93        self.fs = Arc::new(fs);
94        self
95    }
96
97    /// Replace the HTTP client implementation.
98    pub fn with_http_send(mut self, http: impl HttpSend) -> Self {
99        self.http = Arc::new(http);
100        self
101    }
102
103    /// Replace the environment implementation.
104    pub fn with_env(mut self, env: impl Env) -> Self {
105        self.env = Arc::new(env);
106        self
107    }
108
109    /// Replace the command executor implementation.
110    pub fn with_command_execute(mut self, cmd: impl CommandExecute) -> Self {
111        self.cmd = Arc::new(cmd);
112        self
113    }
114
115    /// Read the file content entirely in `Vec<u8>`.
116    #[inline]
117    pub async fn file_read(&self, path: &str) -> Result<Vec<u8>> {
118        self.fs.file_read(path).await
119    }
120
121    /// Read the file content entirely in `String`.
122    pub async fn file_read_as_string(&self, path: &str) -> Result<String> {
123        let bytes = self.file_read(path).await?;
124        Ok(String::from_utf8_lossy(&bytes).to_string())
125    }
126
127    /// Send http request and return the response.
128    #[inline]
129    pub async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
130        self.http.http_send(req).await
131    }
132
133    /// Send http request and return the response as string.
134    pub async fn http_send_as_string(
135        &self,
136        req: http::Request<Bytes>,
137    ) -> Result<http::Response<String>> {
138        let (parts, body) = self.http.http_send(req).await?.into_parts();
139        let body = String::from_utf8_lossy(&body).to_string();
140        Ok(http::Response::from_parts(parts, body))
141    }
142
143    /// Get the home directory of the current user.
144    #[inline]
145    pub fn home_dir(&self) -> Option<PathBuf> {
146        self.env.home_dir()
147    }
148
149    /// Expand `~` in input path.
150    ///
151    /// - If path not starts with `~/` or `~\\`, returns `Some(path)` directly.
152    /// - Otherwise, replace `~` with home dir instead.
153    /// - If home_dir is not found, returns `None`.
154    pub fn expand_home_dir(&self, path: &str) -> Option<String> {
155        if !path.starts_with("~/") && !path.starts_with("~\\") {
156            Some(path.to_string())
157        } else {
158            self.home_dir()
159                .map(|home| path.replace('~', &home.to_string_lossy()))
160        }
161    }
162
163    /// Get the environment variable.
164    ///
165    /// - Returns `Some(v)` if the environment variable is found and is valid utf-8.
166    /// - Returns `None` if the environment variable is not found or value is invalid.
167    #[inline]
168    pub fn env_var(&self, key: &str) -> Option<String> {
169        self.env.var(key)
170    }
171
172    /// Returns a hashmap of (variable, value) pairs of strings, for all the
173    /// environment variables of the current process.
174    #[inline]
175    pub fn env_vars(&self) -> HashMap<String, String> {
176        self.env.vars()
177    }
178
179    /// Execute an external command with the given program and arguments.
180    ///
181    /// Returns the command output including exit status, stdout, and stderr.
182    pub async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
183        self.cmd.command_execute(program, args).await
184    }
185}
186
187/// FileRead is used to read the file content entirely in `Vec<u8>`.
188///
189/// This could be used by `Load` to load the credential from the file.
190#[async_trait::async_trait]
191pub trait FileRead: Debug + Send + Sync + 'static {
192    /// Read the file content entirely in `Vec<u8>`.
193    async fn file_read(&self, path: &str) -> Result<Vec<u8>>;
194}
195
196/// HttpSend is used to send http request during the signing process.
197///
198/// For example, fetch IMDS token from AWS or OAuth2 refresh token. This trait is designed
199/// especially for the signer, please don't use it as a general http client.
200#[async_trait::async_trait]
201pub trait HttpSend: Debug + Send + Sync + 'static {
202    /// Send http request and return the response.
203    async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>>;
204}
205
206/// Permits parameterizing the home functions via the _from variants
207pub trait Env: Debug + Send + Sync + 'static {
208    /// Get an environment variable.
209    ///
210    /// - Returns `Some(v)` if the environment variable is found and is valid utf-8.
211    /// - Returns `None` if the environment variable is not found or value is invalid.
212    fn var(&self, key: &str) -> Option<String>;
213
214    /// Returns a hashmap of (variable, value) pairs of strings, for all the
215    /// environment variables of the current process.
216    fn vars(&self) -> HashMap<String, String>;
217
218    /// Return the path to the users home dir, returns `None` if any error occurs.
219    fn home_dir(&self) -> Option<PathBuf>;
220}
221
222/// Implements Env for the OS context, both Unix style and Windows.
223#[derive(Debug, Copy, Clone)]
224pub struct OsEnv;
225
226impl Env for OsEnv {
227    fn var(&self, key: &str) -> Option<String> {
228        std::env::var_os(key)?.into_string().ok()
229    }
230
231    fn vars(&self) -> HashMap<String, String> {
232        std::env::vars().collect()
233    }
234
235    #[cfg(any(unix, target_os = "redox"))]
236    fn home_dir(&self) -> Option<PathBuf> {
237        #[allow(deprecated)]
238        std::env::home_dir()
239    }
240
241    #[cfg(windows)]
242    fn home_dir(&self) -> Option<PathBuf> {
243        windows::home_dir_inner()
244    }
245
246    #[cfg(target_arch = "wasm32")]
247    fn home_dir(&self) -> Option<PathBuf> {
248        None
249    }
250}
251
252/// StaticEnv provides a static env environment.
253///
254/// This is useful for testing or for providing a fixed environment.
255#[derive(Debug, Clone, Default)]
256pub struct StaticEnv {
257    /// The home directory to use.
258    pub home_dir: Option<PathBuf>,
259    /// The environment variables to use.
260    pub envs: HashMap<String, String>,
261}
262
263impl Env for StaticEnv {
264    fn var(&self, key: &str) -> Option<String> {
265        self.envs.get(key).cloned()
266    }
267
268    fn vars(&self) -> HashMap<String, String> {
269        self.envs.clone()
270    }
271
272    fn home_dir(&self) -> Option<PathBuf> {
273        self.home_dir.clone()
274    }
275}
276
277/// CommandOutput represents the output of a command execution.
278#[derive(Debug, Clone)]
279pub struct CommandOutput {
280    /// Exit status code (0 for success)
281    pub status: i32,
282    /// Standard output as bytes
283    pub stdout: Vec<u8>,
284    /// Standard error as bytes
285    pub stderr: Vec<u8>,
286}
287
288impl CommandOutput {
289    /// Check if the command exited successfully.
290    pub fn success(&self) -> bool {
291        self.status == 0
292    }
293}
294
295/// CommandExecute is used to execute external commands for credential retrieval.
296///
297/// This trait abstracts command execution to support different runtime environments:
298/// - Tokio-based async execution
299/// - Blocking execution for non-async contexts
300/// - WebAssembly environments (returning errors)
301/// - Mock implementations for testing
302#[async_trait::async_trait]
303pub trait CommandExecute: Debug + Send + Sync + 'static {
304    /// Execute a command with the given program and arguments.
305    async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput>;
306}
307
308/// NoopFileRead is a no-op implementation that always returns an error.
309///
310/// This is used when no file reader is configured.
311#[derive(Debug, Clone, Copy, Default)]
312pub struct NoopFileRead;
313
314#[async_trait::async_trait]
315impl FileRead for NoopFileRead {
316    async fn file_read(&self, _path: &str) -> Result<Vec<u8>> {
317        Err(Error::unexpected(
318            "file reading not supported: no file reader configured",
319        ))
320    }
321}
322
323/// NoopHttpSend is a no-op implementation that always returns an error.
324///
325/// This is used when no HTTP client is configured.
326#[derive(Debug, Clone, Copy, Default)]
327pub struct NoopHttpSend;
328
329#[async_trait::async_trait]
330impl HttpSend for NoopHttpSend {
331    async fn http_send(&self, _req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
332        Err(Error::unexpected(
333            "HTTP sending not supported: no HTTP client configured",
334        ))
335    }
336}
337
338/// NoopEnv is a no-op implementation that always returns None/empty.
339///
340/// This is used when no environment is configured.
341#[derive(Debug, Clone, Copy, Default)]
342pub struct NoopEnv;
343
344impl Env for NoopEnv {
345    fn var(&self, _key: &str) -> Option<String> {
346        None
347    }
348
349    fn vars(&self) -> HashMap<String, String> {
350        HashMap::new()
351    }
352
353    fn home_dir(&self) -> Option<PathBuf> {
354        None
355    }
356}
357
358/// NoopCommandExecute is a no-op implementation that always returns an error.
359///
360/// This is used when no command executor is configured.
361#[derive(Debug, Clone, Copy, Default)]
362pub struct NoopCommandExecute;
363
364#[async_trait::async_trait]
365impl CommandExecute for NoopCommandExecute {
366    async fn command_execute(&self, _program: &str, _args: &[&str]) -> Result<CommandOutput> {
367        Err(Error::unexpected(
368            "command execution not supported: no command executor configured",
369        ))
370    }
371}
372
373#[cfg(target_os = "windows")]
374mod windows {
375    use std::env;
376    use std::ffi::OsString;
377    use std::os::windows::ffi::OsStringExt;
378    use std::path::PathBuf;
379    use std::ptr;
380    use std::slice;
381
382    use windows_sys::Win32::Foundation::S_OK;
383    use windows_sys::Win32::System::Com::CoTaskMemFree;
384    use windows_sys::Win32::UI::Shell::{
385        FOLDERID_Profile, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
386    };
387
388    pub fn home_dir_inner() -> Option<PathBuf> {
389        env::var_os("USERPROFILE")
390            .filter(|s| !s.is_empty())
391            .map(PathBuf::from)
392            .or_else(home_dir_crt)
393    }
394
395    #[cfg(not(target_vendor = "uwp"))]
396    fn home_dir_crt() -> Option<PathBuf> {
397        unsafe {
398            let mut path = ptr::null_mut();
399            match SHGetKnownFolderPath(
400                &FOLDERID_Profile,
401                KF_FLAG_DONT_VERIFY as u32,
402                std::ptr::null_mut(),
403                &mut path,
404            ) {
405                S_OK => {
406                    let path_slice = slice::from_raw_parts(path, wcslen(path));
407                    let s = OsString::from_wide(&path_slice);
408                    CoTaskMemFree(path.cast());
409                    Some(PathBuf::from(s))
410                }
411                _ => {
412                    // Free any allocated memory even on failure. A null ptr is a no-op for `CoTaskMemFree`.
413                    CoTaskMemFree(path.cast());
414                    None
415                }
416            }
417        }
418    }
419
420    #[cfg(target_vendor = "uwp")]
421    fn home_dir_crt() -> Option<PathBuf> {
422        None
423    }
424
425    unsafe extern "C" {
426        unsafe fn wcslen(buf: *const u16) -> usize;
427    }
428
429    #[cfg(not(target_vendor = "uwp"))]
430    #[cfg(test)]
431    mod tests {
432        use super::home_dir_inner;
433        use std::env;
434        use std::ops::Deref;
435        use std::path::{Path, PathBuf};
436
437        #[test]
438        fn test_with_without() {
439            let olduserprofile = env::var_os("USERPROFILE").unwrap();
440            unsafe {
441                env::remove_var("HOME");
442                env::remove_var("USERPROFILE");
443            }
444            assert_eq!(home_dir_inner(), Some(PathBuf::from(olduserprofile)));
445
446            let home = Path::new(r"C:\Users\foo tar baz");
447            unsafe {
448                env::set_var("HOME", home.as_os_str());
449                env::set_var("USERPROFILE", home.as_os_str());
450            }
451            assert_ne!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
452            assert_eq!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
453        }
454    }
455}