Skip to main content

hightower_node/
context.rs

1use std::borrow::Cow;
2use std::env::VarError;
3use std::error::Error;
4use std::fmt;
5use std::path::Path;
6use std::sync::Arc;
7
8mod kv {
9    use hightower_kv::{
10        AuthService, Error as KvError, KvEngine, SingleNodeEngine, StoreConfig,
11        command::Command,
12        crypto::{AesGcmEncryptor, Argon2SecretHasher},
13    };
14    use rand::RngCore;
15    use std::error::Error;
16    use std::fmt;
17    use std::fs;
18    use std::path::{Path, PathBuf};
19    use std::sync::Arc;
20    use std::time::{SystemTime, UNIX_EPOCH};
21    use tempfile::{Builder as TempDirBuilder, TempDir};
22    use tracing::{debug, info, warn};
23
24    const AUTH_MASTER_KEY: &[u8] = b"secrets/auth_master_key";
25
26    type SharedEngine = Arc<SingleNodeEngine>;
27    type SharedAuthService = AuthService<SharedEngine, Argon2SecretHasher, AesGcmEncryptor>;
28
29    pub type GatewayAuthService = SharedAuthService;
30
31    #[derive(Debug)]
32    pub enum KvInitError {
33        TempDir(std::io::Error),
34        CreateDir(std::io::Error),
35        Store(KvError),
36    }
37
38    impl fmt::Display for KvInitError {
39        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40            match self {
41                KvInitError::TempDir(err) => {
42                    write!(f, "failed to create temporary directory: {}", err)
43                }
44                KvInitError::CreateDir(err) => {
45                    write!(f, "failed to create data directory: {}", err)
46                }
47                KvInitError::Store(err) => {
48                    write!(f, "failed to start key-value engine: {}", err)
49                }
50            }
51        }
52    }
53
54    impl Error for KvInitError {
55        fn source(&self) -> Option<&(dyn Error + 'static)> {
56            match self {
57                KvInitError::TempDir(err) | KvInitError::CreateDir(err) => Some(err),
58                KvInitError::Store(err) => Some(err),
59            }
60        }
61    }
62
63    pub struct KvHandle {
64        engine: SharedEngine,
65        auth: Arc<SharedAuthService>,
66        #[allow(dead_code)]
67        data_dir: PathBuf,
68        #[allow(dead_code)]
69        temp_dir: Option<TempDir>,
70    }
71
72    impl fmt::Debug for KvHandle {
73        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74            f.debug_struct("KvHandle")
75                .field("data_dir", &self.data_dir)
76                .field(
77                    "temp_dir",
78                    &self.temp_dir.as_ref().map(|dir| dir.path().to_path_buf()),
79                )
80                .finish()
81        }
82    }
83
84    impl KvHandle {
85        pub fn data_dir(&self) -> &Path {
86            &self.data_dir
87        }
88
89        #[cfg(test)]
90        pub fn temp_dir_path(&self) -> Option<&Path> {
91            self.temp_dir.as_ref().map(|dir| dir.path())
92        }
93
94        pub fn put_bytes(&self, key: &[u8], value: &[u8]) -> Result<(), KvError> {
95            let (version, timestamp) = monotonic_version_timestamp();
96            self.engine.submit(Command::Set {
97                key: key.to_vec(),
98                value: value.to_vec(),
99                version,
100                timestamp,
101            })?;
102            Ok(())
103        }
104
105        pub fn put_secret(&self, key: &[u8], value: &[u8]) {
106            if let Err(err) = self.put_bytes(key, value) {
107                warn!(?err, "Failed to persist secret to KV");
108            }
109        }
110
111        pub fn get_bytes(&self, key: &[u8]) -> Result<Option<Vec<u8>>, KvError> {
112            self.engine.get(key)
113        }
114
115        pub fn auth(&self) -> Arc<SharedAuthService> {
116            Arc::clone(&self.auth)
117        }
118
119        pub fn get_prefix(&self, prefix: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>, KvError> {
120            self.engine.get_prefix(prefix)
121        }
122    }
123
124    pub fn initialize(dir: Option<&Path>) -> Result<KvHandle, KvInitError> {
125        let (data_dir, temp_dir) = match dir {
126            Some(path) => {
127                fs::create_dir_all(path).map_err(KvInitError::CreateDir)?;
128                (path.to_path_buf(), None)
129            }
130            None => {
131                let temp = TempDirBuilder::new()
132                    .prefix("hightower-kv")
133                    .tempdir()
134                    .map_err(KvInitError::TempDir)?;
135                (temp.path().to_path_buf(), Some(temp))
136            }
137        };
138
139        let mut config = StoreConfig::default();
140        config.data_dir = data_dir.to_string_lossy().into_owned();
141
142        let engine = SingleNodeEngine::with_config(config).map_err(KvInitError::Store)?;
143        let master_key = load_or_initialize_master_key(&engine).map_err(KvInitError::Store)?;
144        let (engine, auth) = engine.into_argon2_hasher_aes_gcm_auth_service(master_key);
145        let auth = Arc::new(auth);
146        let temporary = temp_dir.is_some();
147        debug!(path = %data_dir.display(), temporary, "Initialized key-value store");
148
149        Ok(KvHandle {
150            engine,
151            auth,
152            data_dir,
153            temp_dir,
154        })
155    }
156
157    fn load_or_initialize_master_key(engine: &SingleNodeEngine) -> Result<[u8; 32], KvError> {
158        match engine.get(AUTH_MASTER_KEY)? {
159            Some(bytes) => match <[u8; 32]>::try_from(bytes.as_slice()) {
160                Ok(key) => Ok(key),
161                Err(_) => {
162                    warn!(
163                        stored_len = bytes.len(),
164                        "Stored auth master key has unexpected length; generating a new key"
165                    );
166                    generate_and_persist_master_key(engine)
167                }
168            },
169            None => generate_and_persist_master_key(engine),
170        }
171    }
172
173    fn generate_and_persist_master_key(engine: &SingleNodeEngine) -> Result<[u8; 32], KvError> {
174        let mut key = [0u8; 32];
175        rand::thread_rng().fill_bytes(&mut key);
176        let (version, timestamp) = monotonic_version_timestamp();
177        engine.submit(Command::Set {
178            key: AUTH_MASTER_KEY.to_vec(),
179            value: key.to_vec(),
180            version,
181            timestamp,
182        })?;
183        info!("Provisioned new auth master key");
184        Ok(key)
185    }
186
187    fn monotonic_version_timestamp() -> (u64, i64) {
188        let now = SystemTime::now()
189            .duration_since(UNIX_EPOCH)
190            .unwrap_or_default();
191        let millis = now.as_millis();
192        let version = millis.min(u64::MAX as u128) as u64;
193        let timestamp = millis.min(i64::MAX as u128) as i64;
194        (version, timestamp)
195    }
196
197    #[cfg(test)]
198    mod tests {
199        use super::*;
200
201        #[test]
202        fn initialize_uses_provided_directory() {
203            let temp_root = TempDirBuilder::new()
204                .prefix("hightower-test")
205                .tempdir()
206                .unwrap();
207            let target = temp_root.path().join("kv-store");
208
209            let handle = initialize(Some(&target)).expect("kv init succeeds");
210
211            assert_eq!(handle.data_dir(), target.as_path());
212            assert!(target.exists());
213            assert!(handle.temp_dir_path().is_none());
214        }
215
216        #[test]
217        fn initialize_falls_back_to_temp_directory() {
218            let handle = initialize(None).expect("kv init succeeds");
219
220            let temp_dir = handle.temp_dir_path().expect("temp dir is retained");
221            assert!(temp_dir.exists());
222            assert!(handle.data_dir().starts_with(temp_dir));
223        }
224
225        #[test]
226        fn put_bytes_persists_values() {
227            let handle = initialize(None).expect("kv init succeeds");
228            let key = b"kv-tests/cert";
229            let value = b"payload";
230
231            handle.put_bytes(key, value).expect("write succeeds");
232
233            let stored = handle.get_bytes(key).expect("read succeeds");
234            assert_eq!(stored, Some(value.to_vec()));
235        }
236
237        #[test]
238        fn put_secret_does_not_panic_on_failure() {
239            let handle = initialize(None).expect("kv init succeeds");
240            let key = b"kv-tests/secret";
241
242            handle.put_secret(key, b"secret");
243            let stored = handle.get_bytes(key).expect("read succeeds");
244            assert!(stored.is_some());
245        }
246    }
247}
248
249mod token {
250    use std::env::VarError;
251    use std::fmt;
252
253    #[derive(Debug, PartialEq, Eq)]
254    pub enum TokenError {
255        Missing,
256    }
257
258    impl fmt::Display for TokenError {
259        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260            match self {
261                TokenError::Missing => {
262                    write!(f, "HT_AUTH_KEY environment variable must be set.")
263                }
264            }
265        }
266    }
267
268    impl std::error::Error for TokenError {}
269
270    pub fn fetch<F>(mut lookup: F) -> Result<String, TokenError>
271    where
272        F: FnMut(&str) -> Result<String, VarError>,
273    {
274        lookup("HT_AUTH_KEY").map_err(|_| TokenError::Missing)
275    }
276
277    #[cfg(test)]
278    mod tests {
279        use super::*;
280
281        #[test]
282        fn fetch_returns_token_when_present() {
283            let token = fetch(|_| Ok(String::from("123"))).expect("token should be present");
284
285            assert_eq!(token, "123");
286        }
287
288        #[test]
289        fn fetch_reports_missing_token() {
290            let error =
291                fetch(|_| Err(VarError::NotPresent)).expect_err("token should be missing");
292
293            assert_eq!(error, TokenError::Missing);
294        }
295    }
296}
297
298pub use kv::{GatewayAuthService, KvHandle, KvInitError, initialize as initialize_kv};
299pub use token::{TokenError, fetch as fetch_token};
300
301pub const NODE_NAME_KEY: &[u8] = b"nodes/name";
302pub const HT_AUTH_KEY: &[u8] = b"secrets/ht_auth_key";
303pub const GATEWAY_URL_KEY: &[u8] = b"gateway/url";
304const GATEWAY_URL_ENV: &str = "HT_GATEWAY_URL";
305const DEFAULT_AUTH_USERNAME_ENV: &str = "HT_DEFAULT_USER";
306const DEFAULT_AUTH_PASSWORD_ENV: &str = "HT_DEFAULT_PASSWORD";
307const DEFAULT_AUTH_USERNAME: &str = "admin";
308const DEFAULT_AUTH_PASSWORD: &str = "admin";
309
310#[derive(Clone)]
311pub struct CommonContext {
312    pub kv: NamespacedKv,
313    pub auth: Arc<GatewayAuthService>,
314}
315
316impl CommonContext {
317    pub fn new(kv: KvHandle) -> Self {
318        let kv = NamespacedKv::from_handle(kv);
319        let auth = kv.auth_service();
320        Self { kv, auth }
321    }
322
323    pub fn namespaced(&self, prefix: &[u8]) -> Self {
324        Self {
325            kv: self.kv.clone_with_additional_prefix(prefix),
326            auth: Arc::clone(&self.auth),
327        }
328    }
329}
330
331impl fmt::Debug for CommonContext {
332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333        f.debug_struct("CommonContext")
334            .field("kv", &self.kv)
335            .finish()
336    }
337}
338
339#[derive(Clone, Debug)]
340pub struct NamespacedKv {
341    inner: Arc<KvHandle>,
342    prefix: Option<Vec<u8>>,
343}
344
345impl NamespacedKv {
346    pub fn from_handle(kv: KvHandle) -> Self {
347        Self {
348            inner: Arc::new(kv),
349            prefix: None,
350        }
351    }
352
353    fn with_parts(inner: Arc<KvHandle>, prefix: Option<Vec<u8>>) -> Self {
354        Self { inner, prefix }
355    }
356
357    pub fn clone_with_additional_prefix(&self, additional: &[u8]) -> Self {
358        let prefix = match &self.prefix {
359            Some(existing) => {
360                let mut composed = existing.clone();
361                if !existing.is_empty() {
362                    composed.push(b'/');
363                }
364                composed.extend_from_slice(additional);
365                composed
366            }
367            None => additional.to_vec(),
368        };
369
370        Self::with_parts(Arc::clone(&self.inner), Some(prefix))
371    }
372
373    pub fn put_bytes(&self, key: &[u8], value: &[u8]) -> Result<(), hightower_kv::Error> {
374        let key = self.prefixed_key(key);
375        self.inner.put_bytes(key.as_ref(), value)
376    }
377
378    pub fn put_secret(&self, key: &[u8], value: &[u8]) {
379        let key = self.prefixed_key(key);
380        self.inner.put_secret(key.as_ref(), value);
381    }
382
383    pub fn get_bytes(&self, key: &[u8]) -> Result<Option<Vec<u8>>, hightower_kv::Error> {
384        let key = self.prefixed_key(key);
385        self.inner.get_bytes(key.as_ref())
386    }
387
388    pub fn auth_service(&self) -> Arc<GatewayAuthService> {
389        self.inner.auth()
390    }
391
392    pub fn list_by_prefix(
393        &self,
394        prefix: &[u8],
395    ) -> Result<Vec<(Vec<u8>, Vec<u8>)>, hightower_kv::Error> {
396        let full_prefix = match self.prefixed_key(prefix) {
397            Cow::Borrowed(bytes) => bytes.to_vec(),
398            Cow::Owned(bytes) => bytes,
399        };
400
401        let entries = self.inner.get_prefix(&full_prefix)?;
402
403        let mut results = Vec::new();
404        for (key, value) in entries {
405            let mut remainder = &key[full_prefix.len()..];
406            if remainder.first() == Some(&b'/') {
407                remainder = &remainder[1..];
408            }
409            results.push((remainder.to_vec(), value));
410        }
411
412        Ok(results)
413    }
414
415    fn prefixed_key<'a>(&self, key: &'a [u8]) -> Cow<'a, [u8]> {
416        match &self.prefix {
417            Some(prefix) if !prefix.is_empty() => {
418                let mut composed = Vec::with_capacity(prefix.len() + 1 + key.len());
419                composed.extend_from_slice(prefix);
420                composed.push(b'/');
421                composed.extend_from_slice(key);
422                Cow::Owned(composed)
423            }
424            _ => Cow::Borrowed(key),
425        }
426    }
427}
428
429#[derive(Debug)]
430pub enum ContextError {
431    Token(TokenError),
432    Kv(KvInitError),
433    Auth(hightower_kv::Error),
434}
435
436impl fmt::Display for ContextError {
437    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438        match self {
439            ContextError::Token(err) => write!(f, "failed to read HT_AUTH_KEY: {}", err),
440            ContextError::Kv(err) => write!(f, "failed to initialize key-value store: {}", err),
441            ContextError::Auth(err) => write!(f, "failed to bootstrap auth service: {}", err),
442        }
443    }
444}
445
446impl Error for ContextError {
447    fn source(&self) -> Option<&(dyn Error + 'static)> {
448        match self {
449            ContextError::Token(err) => Some(err),
450            ContextError::Kv(err) => Some(err),
451            ContextError::Auth(err) => Some(err),
452        }
453    }
454}
455
456pub fn initialize_with_token_source<F>(
457    kv_path: Option<&Path>,
458    mut lookup: F,
459) -> Result<CommonContext, ContextError>
460where
461    F: FnMut(&str) -> Result<String, VarError>,
462{
463    let token = token::fetch(|key| lookup(key)).map_err(ContextError::Token)?;
464    initialize_with_token(kv_path, token)
465}
466
467pub fn initialize_with_token(
468    kv_path: Option<&Path>,
469    token: String,
470) -> Result<CommonContext, ContextError> {
471    let kv = kv::initialize(kv_path).map_err(ContextError::Kv)?;
472    let context = CommonContext::new(kv);
473    context.kv.put_secret(HT_AUTH_KEY, token.as_bytes());
474
475    if let Ok(gateway_url) = std::env::var(GATEWAY_URL_ENV) {
476        context.kv.put_secret(GATEWAY_URL_KEY, gateway_url.as_bytes());
477    }
478
479    bootstrap_default_user(&context).map_err(ContextError::Auth)?;
480    Ok(context)
481}
482
483fn bootstrap_default_user(context: &CommonContext) -> Result<(), hightower_kv::Error> {
484    let username = std::env::var(DEFAULT_AUTH_USERNAME_ENV)
485        .unwrap_or_else(|_| DEFAULT_AUTH_USERNAME.to_string())
486        .trim()
487        .to_owned();
488
489    if username.is_empty() {
490        tracing::warn!("Skipping default auth bootstrap; username is empty");
491        return Ok(());
492    }
493
494    let password = std::env::var(DEFAULT_AUTH_PASSWORD_ENV)
495        .unwrap_or_else(|_| DEFAULT_AUTH_PASSWORD.to_string());
496
497    if password.trim().is_empty() {
498        tracing::warn!("Skipping default auth bootstrap; password is empty");
499        return Ok(());
500    }
501
502    match context.auth.create_user(&username, &password) {
503        Ok(_) => {
504            tracing::info!(username = %username, "Bootstrapped default auth user");
505            Ok(())
506        }
507        Err(hightower_kv::Error::Conflict(_)) => {
508            tracing::debug!(username = %username, "Default auth user already exists");
509            Ok(())
510        }
511        Err(err) => Err(err),
512    }
513}
514
515#[cfg(test)]
516pub mod fixtures {
517    use crate::context::{CommonContext, initialize_kv};
518    use tempfile::TempDir;
519
520    pub fn context() -> CommonContext {
521        let temp = TempDir::new().expect("tempdir");
522        let kv = initialize_kv(Some(temp.path())).expect("kv init");
523        CommonContext::new(kv)
524    }
525}
526
527pub mod env {
528    use std::sync::{Mutex, MutexGuard, OnceLock};
529
530    pub const DISABLE_GATEWAY_REGISTRATION_ENV: &str = "HT_DISABLE_ROOT_REGISTRATION";
531
532    fn registration_lock() -> &'static Mutex<()> {
533        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
534        LOCK.get_or_init(|| Mutex::new(()))
535    }
536
537    pub struct RegistrationGuard {
538        lock: Option<MutexGuard<'static, ()>>,
539    }
540
541    impl RegistrationGuard {
542        fn new(lock: MutexGuard<'static, ()>) -> Self {
543            Self { lock: Some(lock) }
544        }
545    }
546
547    impl Drop for RegistrationGuard {
548        fn drop(&mut self) {
549            unsafe {
550                std::env::remove_var(DISABLE_GATEWAY_REGISTRATION_ENV);
551            }
552            drop(self.lock.take());
553        }
554    }
555
556    pub fn disable_gateway_registration() -> RegistrationGuard {
557        let guard = registration_lock().lock().expect("registration lock");
558        unsafe {
559            std::env::set_var(DISABLE_GATEWAY_REGISTRATION_ENV, "1");
560        }
561        RegistrationGuard::new(guard)
562    }
563
564    pub struct RegistrationEnableGuard {
565        lock: Option<MutexGuard<'static, ()>>,
566        previous: Option<String>,
567    }
568
569    impl RegistrationEnableGuard {
570        fn new(lock: MutexGuard<'static, ()>, previous: Option<String>) -> Self {
571            Self {
572                lock: Some(lock),
573                previous,
574            }
575        }
576    }
577
578    impl Drop for RegistrationEnableGuard {
579        fn drop(&mut self) {
580            unsafe {
581                if let Some(value) = self.previous.take() {
582                    std::env::set_var(DISABLE_GATEWAY_REGISTRATION_ENV, value);
583                } else {
584                    std::env::remove_var(DISABLE_GATEWAY_REGISTRATION_ENV);
585                }
586            }
587            drop(self.lock.take());
588        }
589    }
590
591    pub fn enable_gateway_registration() -> RegistrationEnableGuard {
592        let guard = registration_lock().lock().expect("registration lock");
593        let previous = std::env::var(DISABLE_GATEWAY_REGISTRATION_ENV).ok();
594        unsafe {
595            std::env::remove_var(DISABLE_GATEWAY_REGISTRATION_ENV);
596        }
597        RegistrationEnableGuard::new(guard, previous)
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604    use std::env::VarError;
605    use tempfile::TempDir;
606
607    #[test]
608    fn initialize_with_token_source_persists_token() {
609        let temp = TempDir::new().expect("tempdir");
610        let context =
611            initialize_with_token_source(Some(temp.path()), |_| Ok("test-auth".into()))
612                .expect("initialize");
613
614        let stored = context
615            .kv
616            .get_bytes(HT_AUTH_KEY)
617            .expect("kv read")
618            .expect("value present");
619        assert_eq!(stored, b"test-auth");
620    }
621
622    #[test]
623    fn initialize_with_token_source_reports_missing_token() {
624        let error = initialize_with_token_source(None, |_| Err(VarError::NotPresent))
625            .expect_err("missing token");
626        assert!(matches!(error, ContextError::Token(TokenError::Missing)));
627    }
628
629    #[test]
630    fn initialize_with_token_bootstraps_default_user() {
631        use std::sync::{Mutex, OnceLock};
632
633        fn env_lock() -> &'static Mutex<()> {
634            static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
635            LOCK.get_or_init(|| Mutex::new(()))
636        }
637
638        let _guard = env_lock().lock().expect("env lock");
639
640        let previous_user = std::env::var(DEFAULT_AUTH_USERNAME_ENV).ok();
641        let previous_password = std::env::var(DEFAULT_AUTH_PASSWORD_ENV).ok();
642        unsafe {
643            std::env::remove_var(DEFAULT_AUTH_USERNAME_ENV);
644            std::env::remove_var(DEFAULT_AUTH_PASSWORD_ENV);
645        }
646
647        let temp = TempDir::new().expect("tempdir");
648        let context = initialize_with_token(Some(temp.path()), "token".into())
649            .expect("context initialized");
650
651        assert!(
652            context
653                .auth
654                .verify_password(DEFAULT_AUTH_USERNAME, DEFAULT_AUTH_PASSWORD)
655                .expect("default password verification")
656        );
657
658        match previous_user {
659            Some(value) => unsafe { std::env::set_var(DEFAULT_AUTH_USERNAME_ENV, value) },
660            None => unsafe { std::env::remove_var(DEFAULT_AUTH_USERNAME_ENV) },
661        }
662
663        match previous_password {
664            Some(value) => unsafe { std::env::set_var(DEFAULT_AUTH_PASSWORD_ENV, value) },
665            None => unsafe { std::env::remove_var(DEFAULT_AUTH_PASSWORD_ENV) },
666        }
667    }
668
669    #[test]
670    fn context_creates_isolated_store() {
671        let ctx_a = fixtures::context();
672        let ctx_b = fixtures::context();
673
674        ctx_a
675            .kv
676            .put_bytes(b"test/key", b"value-a")
677            .expect("store a");
678
679        let stored = ctx_b.kv.get_bytes(b"test/key").expect("read b");
680        assert!(stored.is_none(), "unexpected shared state");
681    }
682
683    #[test]
684    fn disable_gateway_registration_sets_env() {
685        {
686            let _guard = env::disable_gateway_registration();
687            let value = std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).unwrap();
688            assert_eq!(value, "1");
689        }
690
691        assert!(std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).is_err());
692    }
693
694    #[test]
695    fn enable_gateway_registration_restores_previous_state() {
696        use std::sync::{Mutex, OnceLock};
697
698        fn test_env_lock() -> &'static Mutex<()> {
699            static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
700            LOCK.get_or_init(|| Mutex::new(()))
701        }
702
703        let _test_lock = test_env_lock().lock().expect("test env lock");
704
705        let previous = std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).ok();
706
707        unsafe {
708            std::env::set_var(env::DISABLE_GATEWAY_REGISTRATION_ENV, "1");
709        }
710
711        {
712            let _guard = env::enable_gateway_registration();
713            assert!(
714                std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).is_err(),
715                "env var should be cleared while guard is active"
716            );
717        }
718
719        let restored =
720            std::env::var(env::DISABLE_GATEWAY_REGISTRATION_ENV).expect("env var restored");
721        assert_eq!(restored, "1");
722
723        match previous {
724            Some(value) => unsafe { std::env::set_var(env::DISABLE_GATEWAY_REGISTRATION_ENV, value) },
725            None => unsafe { std::env::remove_var(env::DISABLE_GATEWAY_REGISTRATION_ENV) },
726        }
727    }
728}