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.
219pub trait HttpSend: Debug + Send + Sync + 'static {
220    /// Send http request and return the response.
221    fn http_send(
222        &self,
223        req: http::Request<Bytes>,
224    ) -> impl Future<Output = Result<http::Response<Bytes>>> + MaybeSend;
225}
226
227/// HttpSendDyn is the dyn version of [`HttpSend`].
228pub trait HttpSendDyn: Debug + Send + Sync + 'static {
229    /// Dyn version of [`HttpSend::http_send`].
230    fn http_send_dyn(
231        &self,
232        req: http::Request<Bytes>,
233    ) -> BoxedFuture<'_, Result<http::Response<Bytes>>>;
234}
235
236impl<T: HttpSend + ?Sized> HttpSendDyn for T {
237    fn http_send_dyn(
238        &self,
239        req: http::Request<Bytes>,
240    ) -> BoxedFuture<'_, Result<http::Response<Bytes>>> {
241        Box::pin(self.http_send(req))
242    }
243}
244
245impl<T: HttpSendDyn + ?Sized> HttpSend for Arc<T> {
246    async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
247        self.deref().http_send_dyn(req).await
248    }
249}
250
251/// Permits parameterizing the home functions via the _from variants
252pub trait Env: Debug + Send + Sync + 'static {
253    /// Get an environment variable.
254    ///
255    /// - Returns `Some(v)` if the environment variable is found and is valid utf-8.
256    /// - Returns `None` if the environment variable is not found or value is invalid.
257    fn var(&self, key: &str) -> Option<String>;
258
259    /// Returns a hashmap of (variable, value) pairs of strings, for all the
260    /// environment variables of the current process.
261    fn vars(&self) -> HashMap<String, String>;
262
263    /// Return the path to the users home dir, returns `None` if any error occurs.
264    fn home_dir(&self) -> Option<PathBuf>;
265}
266
267/// Implements Env for the OS context, both Unix style and Windows.
268#[derive(Debug, Copy, Clone)]
269pub struct OsEnv;
270
271impl Env for OsEnv {
272    fn var(&self, key: &str) -> Option<String> {
273        std::env::var_os(key)?.into_string().ok()
274    }
275
276    fn vars(&self) -> HashMap<String, String> {
277        std::env::vars().collect()
278    }
279
280    #[cfg(any(unix, target_os = "redox"))]
281    fn home_dir(&self) -> Option<PathBuf> {
282        #[allow(deprecated)]
283        std::env::home_dir()
284    }
285
286    #[cfg(windows)]
287    fn home_dir(&self) -> Option<PathBuf> {
288        windows::home_dir_inner()
289    }
290
291    #[cfg(target_arch = "wasm32")]
292    fn home_dir(&self) -> Option<PathBuf> {
293        None
294    }
295}
296
297/// StaticEnv provides a static env environment.
298///
299/// This is useful for testing or for providing a fixed environment.
300#[derive(Debug, Clone, Default)]
301pub struct StaticEnv {
302    /// The home directory to use.
303    pub home_dir: Option<PathBuf>,
304    /// The environment variables to use.
305    pub envs: HashMap<String, String>,
306}
307
308impl Env for StaticEnv {
309    fn var(&self, key: &str) -> Option<String> {
310        self.envs.get(key).cloned()
311    }
312
313    fn vars(&self) -> HashMap<String, String> {
314        self.envs.clone()
315    }
316
317    fn home_dir(&self) -> Option<PathBuf> {
318        self.home_dir.clone()
319    }
320}
321
322/// CommandOutput represents the output of a command execution.
323#[derive(Debug, Clone)]
324pub struct CommandOutput {
325    /// Exit status code (0 for success)
326    pub status: i32,
327    /// Standard output as bytes
328    pub stdout: Vec<u8>,
329    /// Standard error as bytes
330    pub stderr: Vec<u8>,
331}
332
333impl CommandOutput {
334    /// Check if the command exited successfully.
335    pub fn success(&self) -> bool {
336        self.status == 0
337    }
338}
339
340/// CommandExecute is used to execute external commands for credential retrieval.
341///
342/// This trait abstracts command execution to support different runtime environments:
343/// - Tokio-based async execution
344/// - Blocking execution for non-async contexts
345/// - WebAssembly environments (returning errors)
346/// - Mock implementations for testing
347pub trait CommandExecute: Debug + Send + Sync + 'static {
348    /// Execute a command with the given program and arguments.
349    fn command_execute<'a>(
350        &'a self,
351        program: &'a str,
352        args: &'a [&'a str],
353    ) -> impl Future<Output = Result<CommandOutput>> + MaybeSend + 'a;
354}
355
356/// CommandExecuteDyn is the dyn version of [`CommandExecute`].
357pub trait CommandExecuteDyn: Debug + Send + Sync + 'static {
358    /// Dyn version of [`CommandExecute::command_execute`].
359    fn command_execute_dyn<'a>(
360        &'a self,
361        program: &'a str,
362        args: &'a [&'a str],
363    ) -> BoxedFuture<'a, Result<CommandOutput>>;
364}
365
366impl<T: CommandExecute + ?Sized> CommandExecuteDyn for T {
367    fn command_execute_dyn<'a>(
368        &'a self,
369        program: &'a str,
370        args: &'a [&'a str],
371    ) -> BoxedFuture<'a, Result<CommandOutput>> {
372        Box::pin(self.command_execute(program, args))
373    }
374}
375
376impl<T: CommandExecuteDyn + ?Sized> CommandExecute for Arc<T> {
377    async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
378        self.deref().command_execute_dyn(program, args).await
379    }
380}
381
382/// NoopFileRead is a no-op implementation that always returns an error.
383///
384/// This is used when no file reader is configured.
385#[derive(Debug, Clone, Copy, Default)]
386pub struct NoopFileRead;
387
388impl FileRead for NoopFileRead {
389    async fn file_read(&self, _path: &str) -> Result<Vec<u8>> {
390        Err(Error::unexpected(
391            "file reading not supported: no file reader configured",
392        ))
393    }
394}
395
396/// NoopHttpSend is a no-op implementation that always returns an error.
397///
398/// This is used when no HTTP client is configured.
399#[derive(Debug, Clone, Copy, Default)]
400pub struct NoopHttpSend;
401
402impl HttpSend for NoopHttpSend {
403    async fn http_send(&self, _req: http::Request<Bytes>) -> Result<http::Response<Bytes>> {
404        Err(Error::unexpected(
405            "HTTP sending not supported: no HTTP client configured",
406        ))
407    }
408}
409
410/// NoopEnv is a no-op implementation that always returns None/empty.
411///
412/// This is used when no environment is configured.
413#[derive(Debug, Clone, Copy, Default)]
414pub struct NoopEnv;
415
416impl Env for NoopEnv {
417    fn var(&self, _key: &str) -> Option<String> {
418        None
419    }
420
421    fn vars(&self) -> HashMap<String, String> {
422        HashMap::new()
423    }
424
425    fn home_dir(&self) -> Option<PathBuf> {
426        None
427    }
428}
429
430/// NoopCommandExecute is a no-op implementation that always returns an error.
431///
432/// This is used when no command executor is configured.
433#[derive(Debug, Clone, Copy, Default)]
434pub struct NoopCommandExecute;
435
436impl CommandExecute for NoopCommandExecute {
437    async fn command_execute(&self, _program: &str, _args: &[&str]) -> Result<CommandOutput> {
438        Err(Error::unexpected(
439            "command execution not supported: no command executor configured",
440        ))
441    }
442}
443
444#[cfg(target_os = "windows")]
445mod windows {
446    use std::env;
447    use std::ffi::OsString;
448    use std::os::windows::ffi::OsStringExt;
449    use std::path::PathBuf;
450    use std::ptr;
451    use std::slice;
452
453    use windows_sys::Win32::Foundation::S_OK;
454    use windows_sys::Win32::System::Com::CoTaskMemFree;
455    use windows_sys::Win32::UI::Shell::{
456        FOLDERID_Profile, KF_FLAG_DONT_VERIFY, SHGetKnownFolderPath,
457    };
458
459    pub fn home_dir_inner() -> Option<PathBuf> {
460        env::var_os("USERPROFILE")
461            .filter(|s| !s.is_empty())
462            .map(PathBuf::from)
463            .or_else(home_dir_crt)
464    }
465
466    #[cfg(not(target_vendor = "uwp"))]
467    fn home_dir_crt() -> Option<PathBuf> {
468        unsafe {
469            let mut path = ptr::null_mut();
470            match SHGetKnownFolderPath(
471                &FOLDERID_Profile,
472                KF_FLAG_DONT_VERIFY as u32,
473                std::ptr::null_mut(),
474                &mut path,
475            ) {
476                S_OK => {
477                    let path_slice = slice::from_raw_parts(path, wcslen(path));
478                    let s = OsString::from_wide(&path_slice);
479                    CoTaskMemFree(path.cast());
480                    Some(PathBuf::from(s))
481                }
482                _ => {
483                    // Free any allocated memory even on failure. A null ptr is a no-op for `CoTaskMemFree`.
484                    CoTaskMemFree(path.cast());
485                    None
486                }
487            }
488        }
489    }
490
491    #[cfg(target_vendor = "uwp")]
492    fn home_dir_crt() -> Option<PathBuf> {
493        None
494    }
495
496    unsafe extern "C" {
497        unsafe fn wcslen(buf: *const u16) -> usize;
498    }
499
500    #[cfg(not(target_vendor = "uwp"))]
501    #[cfg(test)]
502    mod tests {
503        use super::home_dir_inner;
504        use std::env;
505        use std::ops::Deref;
506        use std::path::{Path, PathBuf};
507
508        #[test]
509        fn test_with_without() {
510            let olduserprofile = env::var_os("USERPROFILE").unwrap();
511            unsafe {
512                env::remove_var("HOME");
513                env::remove_var("USERPROFILE");
514            }
515            assert_eq!(home_dir_inner(), Some(PathBuf::from(olduserprofile)));
516
517            let home = Path::new(r"C:\Users\foo tar baz");
518            unsafe {
519                env::set_var("HOME", home.as_os_str());
520                env::set_var("USERPROFILE", home.as_os_str());
521            }
522            assert_ne!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
523            assert_eq!(home_dir_inner().as_ref().map(Deref::deref), Some(home));
524        }
525    }
526}