Skip to main content

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