Skip to main content

via/
daemon.rs

1#[derive(Clone, serde::Deserialize, serde::Serialize)]
2pub struct AllowedOnePasswordRef {
3    pub id: String,
4    pub reference: String,
5}
6
7#[cfg(unix)]
8mod imp {
9    use std::collections::HashMap;
10    use std::env;
11    use std::fs;
12    use std::io::{self, BufRead, BufReader, Write};
13    #[cfg(target_os = "macos")]
14    use std::os::unix::ffi::OsStringExt;
15    #[cfg(any(target_os = "linux", target_os = "macos"))]
16    use std::os::unix::fs::MetadataExt;
17    use std::os::unix::fs::PermissionsExt;
18    #[cfg(any(target_os = "linux", target_os = "macos"))]
19    use std::os::unix::io::AsRawFd;
20    use std::os::unix::net::{UnixListener, UnixStream};
21    use std::path::{Path, PathBuf};
22    use std::process::{Command, Stdio};
23    use std::thread;
24    use std::time::{Duration, Instant};
25
26    use serde::{Deserialize, Serialize};
27
28    use crate::error::ViaError;
29    use crate::secrets::SecretValue;
30
31    const CONNECT_WAIT: Duration = Duration::from_secs(2);
32    const CONNECT_POLL: Duration = Duration::from_millis(50);
33    const IDLE_TIMEOUT: Duration = Duration::from_secs(15 * 60);
34
35    pub fn resolve_onepassword_secret(
36        config_hash: &str,
37        ref_id: &str,
38        ttl_seconds: u64,
39    ) -> Result<SecretValue, ViaError> {
40        let span = crate::timing::span("1password daemon resolve");
41        let response = match request_with_autostart(DaemonRequest::Resolve {
42            config_hash: config_hash.to_owned(),
43            ref_id: ref_id.to_owned(),
44            ttl_seconds,
45        }) {
46            Ok(response) => {
47                span.finish(format!(
48                    "cache={}",
49                    response.cache.as_deref().unwrap_or("unknown")
50                ));
51                response
52            }
53            Err(error) => {
54                span.finish("failed");
55                return Err(error);
56            }
57        };
58
59        if response.ok {
60            return response
61                .value
62                .ok_or_else(|| ViaError::InvalidConfig("daemon returned no secret".to_owned()));
63        }
64
65        Err(ViaError::ExternalCommandFailed {
66            program: "via daemon".to_owned(),
67            status: None,
68            stderr: response
69                .error
70                .unwrap_or_else(|| "failed to resolve secret".to_owned()),
71        })
72    }
73
74    pub fn register_onepassword_refs(
75        config_hash: &str,
76        account: Option<&str>,
77        refs: Vec<super::AllowedOnePasswordRef>,
78    ) -> Result<(), ViaError> {
79        let response = request_with_autostart(DaemonRequest::Register {
80            config_hash: config_hash.to_owned(),
81            account: account.map(str::to_owned),
82            refs,
83        })?;
84        if response.ok {
85            Ok(())
86        } else {
87            Err(daemon_response_error(
88                response,
89                "failed to register 1Password references",
90            ))
91        }
92    }
93
94    pub fn serve() -> Result<(), ViaError> {
95        let path = socket_path()?;
96        let listener = bind_listener(&path)?;
97        run_server(listener, &path)
98    }
99
100    fn bind_listener(path: &Path) -> Result<UnixListener, ViaError> {
101        prepare_socket_parent(path)?;
102        remove_stale_socket(path)?;
103
104        let listener = UnixListener::bind(path)?;
105        fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
106        listener.set_nonblocking(true)?;
107        Ok(listener)
108    }
109
110    fn remove_stale_socket(path: &Path) -> Result<(), ViaError> {
111        if path.exists() {
112            if UnixStream::connect(path).is_ok() {
113                return Err(ViaError::InvalidConfig(
114                    "via daemon is already running".to_owned(),
115                ));
116            }
117            fs::remove_file(path)?;
118        }
119
120        Ok(())
121    }
122
123    fn run_server(listener: UnixListener, path: &Path) -> Result<(), ViaError> {
124        let mut state = DaemonState::default();
125        let expected_client = daemon_executable_identity()?;
126        let mut last_activity = Instant::now();
127        loop {
128            match next_server_event(&listener, &mut last_activity)? {
129                ServerEvent::Connection(stream) => {
130                    let action = handle_stream(stream, &mut state, expected_client.as_ref());
131                    if action == DaemonAction::Stop {
132                        break;
133                    }
134                }
135                ServerEvent::NoConnection => {}
136                ServerEvent::IdleTimeout => break,
137            }
138        }
139
140        let _ = fs::remove_file(path);
141        Ok(())
142    }
143
144    fn next_server_event(
145        listener: &UnixListener,
146        last_activity: &mut Instant,
147    ) -> Result<ServerEvent, ViaError> {
148        match listener.accept() {
149            Ok((stream, _)) => {
150                *last_activity = Instant::now();
151                Ok(ServerEvent::Connection(stream))
152            }
153            Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
154                wait_for_connection(last_activity)
155            }
156            Err(error) => Err(error.into()),
157        }
158    }
159
160    fn wait_for_connection(last_activity: &Instant) -> Result<ServerEvent, ViaError> {
161        if last_activity.elapsed() >= IDLE_TIMEOUT {
162            Ok(ServerEvent::IdleTimeout)
163        } else {
164            thread::sleep(CONNECT_POLL);
165            Ok(ServerEvent::NoConnection)
166        }
167    }
168
169    pub fn status() -> Result<(), ViaError> {
170        control_request(DaemonRequest::Status, print_status, "status failed")
171    }
172
173    pub fn clear() -> Result<(), ViaError> {
174        control_request(
175            DaemonRequest::Clear,
176            |_| println!("via daemon: cache cleared"),
177            "clear failed",
178        )
179    }
180
181    pub fn stop() -> Result<(), ViaError> {
182        control_request(
183            DaemonRequest::Stop,
184            |_| println!("via daemon: stopped"),
185            "stop failed",
186        )
187    }
188
189    fn control_request(
190        daemon_request: DaemonRequest,
191        print_success: impl FnOnce(&ClientDaemonResponse),
192        fallback_error: &str,
193    ) -> Result<(), ViaError> {
194        match request(daemon_request) {
195            Ok(response) if response.ok => {
196                print_success(&response);
197                Ok(())
198            }
199            Ok(response) => Err(daemon_response_error(response, fallback_error)),
200            Err(error) if daemon_unavailable(&error) => {
201                println!("via daemon: stopped");
202                Ok(())
203            }
204            Err(error) => Err(error),
205        }
206    }
207
208    fn print_status(response: &ClientDaemonResponse) {
209        println!("via daemon: running");
210        println!("cached secrets: {}", response.entries.unwrap_or(0));
211    }
212
213    fn daemon_response_error(response: ClientDaemonResponse, fallback: &str) -> ViaError {
214        ViaError::ExternalCommandFailed {
215            program: "via daemon".to_owned(),
216            status: None,
217            stderr: response.error.unwrap_or_else(|| fallback.to_owned()),
218        }
219    }
220
221    fn request_with_autostart(
222        daemon_request: DaemonRequest,
223    ) -> Result<ClientDaemonResponse, ViaError> {
224        match request(daemon_request.clone()) {
225            Ok(response) => Ok(response),
226            Err(error) if daemon_unavailable(&error) => {
227                start_daemon()?;
228                request(daemon_request)
229            }
230            Err(error) => Err(error),
231        }
232    }
233
234    fn request(request: DaemonRequest) -> Result<ClientDaemonResponse, ViaError> {
235        let path = socket_path()?;
236        let mut stream = UnixStream::connect(path)?;
237        let raw = SecretValue::new(serde_json::to_string(&request)?);
238        stream.write_all(raw.expose().as_bytes())?;
239        stream.write_all(b"\n")?;
240
241        let mut line = String::new();
242        BufReader::new(stream).read_line(&mut line)?;
243        if line.trim().is_empty() {
244            return Err(ViaError::InvalidConfig(
245                "daemon returned an empty response".to_owned(),
246            ));
247        }
248        let line = SecretValue::new(line);
249
250        serde_json::from_str(line.expose()).map_err(Into::into)
251    }
252
253    fn start_daemon() -> Result<(), ViaError> {
254        let exe = env::current_exe()?;
255        let mut command = Command::new(exe);
256        command
257            .arg("daemon")
258            .arg("serve")
259            .stdin(Stdio::null())
260            .stdout(Stdio::null());
261        if crate::timing::enabled() {
262            command.stderr(Stdio::inherit());
263        } else {
264            command.stderr(Stdio::null());
265        }
266        command.spawn()?;
267
268        let started = Instant::now();
269        while started.elapsed() < CONNECT_WAIT {
270            if UnixStream::connect(socket_path()?).is_ok() {
271                return Ok(());
272            }
273            thread::sleep(CONNECT_POLL);
274        }
275
276        Err(ViaError::InvalidConfig(
277            "timed out waiting for via daemon to start".to_owned(),
278        ))
279    }
280
281    fn handle_stream(
282        mut stream: UnixStream,
283        state: &mut DaemonState,
284        expected_client: Option<&ExecutableIdentity>,
285    ) -> DaemonAction {
286        let response = match verify_peer_executable(&stream, expected_client) {
287            Ok(()) => handle_verified_stream(&mut stream, state),
288            Err(error) => {
289                DaemonResponseInternal::error(format!("daemon client verification failed: {error}"))
290            }
291        };
292        let action = if response.stop {
293            DaemonAction::Stop
294        } else {
295            DaemonAction::Continue
296        };
297
298        write_daemon_response(&mut stream, response);
299
300        action
301    }
302
303    fn handle_verified_stream(
304        stream: &mut UnixStream,
305        state: &mut DaemonState,
306    ) -> DaemonResponseInternal {
307        let mut line = String::new();
308        let mut reader = BufReader::new(stream);
309        match reader.read_line(&mut line) {
310            Ok(_) => {
311                let line = SecretValue::new(line);
312                match serde_json::from_str(line.expose()) {
313                    Ok(request) => state.handle(request),
314                    Err(error) => {
315                        DaemonResponseInternal::error(format!("invalid daemon request: {error}"))
316                    }
317                }
318            }
319            Err(error) => {
320                DaemonResponseInternal::error(format!("failed to read daemon request: {error}"))
321            }
322        }
323    }
324
325    fn write_daemon_response(stream: &mut UnixStream, response: DaemonResponseInternal) {
326        if let Ok(raw) = serde_json::to_string(&response.into_public()) {
327            let raw = SecretValue::new(raw);
328            let _ = stream.write_all(raw.expose().as_bytes());
329            let _ = stream.write_all(b"\n");
330        }
331    }
332
333    #[derive(Clone, Deserialize, Serialize)]
334    #[serde(tag = "type", rename_all = "snake_case")]
335    enum DaemonRequest {
336        Register {
337            config_hash: String,
338            account: Option<String>,
339            refs: Vec<super::AllowedOnePasswordRef>,
340        },
341        Resolve {
342            config_hash: String,
343            ref_id: String,
344            ttl_seconds: u64,
345        },
346        Clear,
347        Status,
348        Stop,
349    }
350
351    #[derive(Default)]
352    struct DaemonState {
353        cache: HashMap<SecretCacheKey, SecretCacheEntry>,
354        registrations: HashMap<String, RegisteredConfig>,
355    }
356
357    impl DaemonState {
358        fn handle(&mut self, request: DaemonRequest) -> DaemonResponseInternal {
359            self.prune_expired();
360
361            match request {
362                DaemonRequest::Register {
363                    config_hash,
364                    account,
365                    refs,
366                } => self.register(config_hash, account, refs),
367                DaemonRequest::Resolve {
368                    config_hash,
369                    ref_id,
370                    ttl_seconds,
371                } => self.resolve(config_hash, ref_id, ttl_seconds),
372                DaemonRequest::Clear => {
373                    self.cache.clear();
374                    self.registrations.clear();
375                    DaemonResponseInternal::ok()
376                }
377                DaemonRequest::Status => {
378                    let mut response = DaemonResponseInternal::ok();
379                    response.entries = Some(self.cache.len());
380                    response
381                }
382                DaemonRequest::Stop => {
383                    let mut response = DaemonResponseInternal::ok();
384                    response.stop = true;
385                    response
386                }
387            }
388        }
389
390        fn register(
391            &mut self,
392            config_hash: String,
393            account: Option<String>,
394            refs: Vec<super::AllowedOnePasswordRef>,
395        ) -> DaemonResponseInternal {
396            if config_hash.trim().is_empty() {
397                return DaemonResponseInternal::error("config hash must not be empty");
398            }
399
400            let refs = match normalize_allowed_refs(refs) {
401                Ok(refs) => refs,
402                Err(error) => return DaemonResponseInternal::error(error),
403            };
404            self.registrations
405                .insert(config_hash, RegisteredConfig { account, refs });
406            DaemonResponseInternal::ok()
407        }
408
409        fn resolve(
410            &mut self,
411            config_hash: String,
412            ref_id: String,
413            ttl_seconds: u64,
414        ) -> DaemonResponseInternal {
415            let Some(secret) = self.allowed_secret(&config_hash, &ref_id) else {
416                return DaemonResponseInternal::error(
417                    "secret reference is not registered for this config",
418                );
419            };
420            let key = SecretCacheKey {
421                config_hash,
422                ref_id,
423            };
424            if let Some(entry) = self.cache.get(&key) {
425                let mut response = DaemonResponseInternal::ok();
426                response.value = Some(entry.value.clone());
427                response.cache = Some("hit".to_owned());
428                return response;
429            }
430
431            match op_read(secret.account.as_deref(), &secret.reference) {
432                Ok(value) => {
433                    let ttl = Duration::from_secs(ttl_seconds.max(1));
434                    let response_value = value.clone();
435                    self.cache.insert(
436                        key,
437                        SecretCacheEntry {
438                            value,
439                            expires_at: Instant::now() + ttl,
440                        },
441                    );
442                    let mut response = DaemonResponseInternal::ok();
443                    response.value = Some(response_value);
444                    response.cache = Some("miss".to_owned());
445                    response
446                }
447                Err(error) => DaemonResponseInternal::error(error),
448            }
449        }
450
451        fn allowed_secret(&self, config_hash: &str, ref_id: &str) -> Option<AllowedSecret> {
452            let registration = self.registrations.get(config_hash)?;
453            let reference = registration.refs.get(ref_id)?;
454            Some(AllowedSecret {
455                account: registration.account.clone(),
456                reference: reference.clone(),
457            })
458        }
459
460        fn prune_expired(&mut self) {
461            let now = Instant::now();
462            self.cache.retain(|_, entry| entry.expires_at > now);
463        }
464    }
465
466    #[derive(Hash, Eq, PartialEq)]
467    struct SecretCacheKey {
468        config_hash: String,
469        ref_id: String,
470    }
471
472    struct RegisteredConfig {
473        account: Option<String>,
474        refs: HashMap<String, String>,
475    }
476
477    struct AllowedSecret {
478        account: Option<String>,
479        reference: String,
480    }
481
482    struct SecretCacheEntry {
483        value: SecretValue,
484        expires_at: Instant,
485    }
486
487    fn normalize_allowed_refs(
488        refs: Vec<super::AllowedOnePasswordRef>,
489    ) -> Result<HashMap<String, String>, String> {
490        let mut normalized = HashMap::new();
491        for allowed_ref in refs {
492            if allowed_ref.id.trim().is_empty() {
493                return Err("registered secret reference id must not be empty".to_owned());
494            }
495            if !allowed_ref.reference.starts_with("op://") {
496                return Err("registered secret reference must start with op://".to_owned());
497            }
498            normalized.insert(allowed_ref.id, allowed_ref.reference);
499        }
500        Ok(normalized)
501    }
502
503    #[derive(Serialize)]
504    struct WireDaemonResponse {
505        ok: bool,
506        #[serde(
507            skip_serializing_if = "Option::is_none",
508            serialize_with = "serialize_secret_value_option"
509        )]
510        value: Option<SecretValue>,
511        #[serde(skip_serializing_if = "Option::is_none")]
512        cache: Option<String>,
513        #[serde(skip_serializing_if = "Option::is_none")]
514        entries: Option<usize>,
515        #[serde(skip_serializing_if = "Option::is_none")]
516        error: Option<String>,
517    }
518
519    #[derive(Deserialize)]
520    struct ClientDaemonResponse {
521        ok: bool,
522        value: Option<SecretValue>,
523        cache: Option<String>,
524        entries: Option<usize>,
525        error: Option<String>,
526    }
527
528    struct DaemonResponseInternal {
529        ok: bool,
530        value: Option<SecretValue>,
531        cache: Option<String>,
532        entries: Option<usize>,
533        error: Option<String>,
534        stop: bool,
535    }
536
537    impl DaemonResponseInternal {
538        fn ok() -> Self {
539            Self {
540                ok: true,
541                value: None,
542                cache: None,
543                entries: None,
544                error: None,
545                stop: false,
546            }
547        }
548
549        fn error(error: impl Into<String>) -> Self {
550            Self {
551                ok: false,
552                value: None,
553                cache: None,
554                entries: None,
555                error: Some(error.into()),
556                stop: false,
557            }
558        }
559
560        fn into_public(self) -> WireDaemonResponse {
561            WireDaemonResponse {
562                ok: self.ok,
563                value: self.value,
564                cache: self.cache,
565                entries: self.entries,
566                error: self.error,
567            }
568        }
569    }
570
571    fn serialize_secret_value_option<S>(
572        value: &Option<SecretValue>,
573        serializer: S,
574    ) -> Result<S::Ok, S::Error>
575    where
576        S: serde::Serializer,
577    {
578        match value {
579            Some(value) => serializer.serialize_some(value.expose()),
580            None => serializer.serialize_none(),
581        }
582    }
583
584    fn op_read(account: Option<&str>, reference: &str) -> Result<SecretValue, String> {
585        let mut command = Command::new("op");
586        command.arg("read").arg(reference);
587        if let Some(account) = account {
588            command.arg("--account").arg(account);
589        }
590
591        let output = command
592            .output()
593            .map_err(|source| format!("program `op` was not found: {source}"))?;
594
595        if !output.status.success() {
596            return Err(format!(
597                "program `op` failed with status {:?}: {}",
598                output.status.code(),
599                String::from_utf8_lossy(&output.stderr).trim()
600            ));
601        }
602
603        Ok(SecretValue::from_utf8_lossy_trimmed(output.stdout))
604    }
605
606    fn socket_path() -> Result<PathBuf, ViaError> {
607        if let Some(path) = env_path("VIA_DAEMON_SOCKET") {
608            return Ok(path);
609        }
610
611        if let Some(runtime) = env_path("XDG_RUNTIME_DIR") {
612            return Ok(runtime.join("via").join("daemon.sock"));
613        }
614
615        Ok(env::temp_dir()
616            .join(format!("via-{}", user_id()))
617            .join("daemon.sock"))
618    }
619
620    fn prepare_socket_parent(path: &Path) -> Result<(), ViaError> {
621        let parent = path.parent().ok_or_else(|| {
622            ViaError::InvalidConfig("daemon socket path has no parent".to_owned())
623        })?;
624        fs::create_dir_all(parent)?;
625        fs::set_permissions(parent, fs::Permissions::from_mode(0o700))?;
626        Ok(())
627    }
628
629    fn env_path(name: &str) -> Option<PathBuf> {
630        env::var_os(name)
631            .filter(|value| !value.as_os_str().is_empty())
632            .map(PathBuf::from)
633    }
634
635    fn user_id() -> String {
636        env::var("UID")
637            .ok()
638            .filter(|value| !value.trim().is_empty())
639            .unwrap_or_else(|| {
640                env::var("USER")
641                    .ok()
642                    .map(|value| sanitize_path_part(&value))
643                    .filter(|value| !value.is_empty())
644                    .unwrap_or_else(|| "unknown".to_owned())
645            })
646    }
647
648    fn sanitize_path_part(value: &str) -> String {
649        value
650            .chars()
651            .filter(|character| character.is_ascii_alphanumeric() || *character == '_')
652            .collect()
653    }
654
655    fn daemon_unavailable(error: &ViaError) -> bool {
656        matches!(error, ViaError::Io(source) if matches!(
657            source.kind(),
658            io::ErrorKind::NotFound
659                | io::ErrorKind::ConnectionRefused
660                | io::ErrorKind::ConnectionReset
661                | io::ErrorKind::BrokenPipe
662        ))
663    }
664
665    #[derive(Clone)]
666    struct ExecutableIdentity {
667        path: PathBuf,
668        device: u64,
669        inode: u64,
670    }
671
672    impl ExecutableIdentity {
673        fn matches(&self, other: &Self) -> bool {
674            self.path == other.path || (self.device == other.device && self.inode == other.inode)
675        }
676    }
677
678    #[cfg(any(target_os = "linux", target_os = "macos"))]
679    fn daemon_executable_identity() -> Result<Option<ExecutableIdentity>, ViaError> {
680        Ok(Some(executable_identity_from_path(&env::current_exe()?)?))
681    }
682
683    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
684    fn daemon_executable_identity() -> Result<Option<ExecutableIdentity>, ViaError> {
685        Ok(None)
686    }
687
688    #[cfg(any(target_os = "linux", target_os = "macos"))]
689    fn verify_peer_executable(
690        stream: &UnixStream,
691        expected: Option<&ExecutableIdentity>,
692    ) -> Result<(), ViaError> {
693        let Some(expected) = expected else {
694            return Ok(());
695        };
696        let peer = peer_executable_identity(stream)?;
697        if expected.matches(&peer) {
698            Ok(())
699        } else {
700            Err(ViaError::InvalidConfig(
701                "daemon refused connection from executable other than via".to_owned(),
702            ))
703        }
704    }
705
706    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
707    fn verify_peer_executable(
708        _stream: &UnixStream,
709        _expected: Option<&ExecutableIdentity>,
710    ) -> Result<(), ViaError> {
711        Ok(())
712    }
713
714    #[cfg(any(target_os = "linux", target_os = "macos"))]
715    fn executable_identity_from_path(path: &Path) -> Result<ExecutableIdentity, ViaError> {
716        let metadata = fs::metadata(path)?;
717        Ok(executable_identity_from_parts(path.to_path_buf(), metadata))
718    }
719
720    #[cfg(any(target_os = "linux", target_os = "macos"))]
721    fn executable_identity_from_parts(path: PathBuf, metadata: fs::Metadata) -> ExecutableIdentity {
722        let path = fs::canonicalize(&path).unwrap_or(path);
723        ExecutableIdentity {
724            path,
725            device: metadata.dev(),
726            inode: metadata.ino(),
727        }
728    }
729
730    #[cfg(target_os = "linux")]
731    fn peer_executable_identity(stream: &UnixStream) -> Result<ExecutableIdentity, ViaError> {
732        let pid = linux_peer_pid(stream)?;
733        let proc_exe = PathBuf::from(format!("/proc/{pid}/exe"));
734        let metadata = fs::metadata(&proc_exe)?;
735        let path = fs::read_link(&proc_exe).unwrap_or_else(|_| proc_exe.clone());
736        Ok(executable_identity_from_parts(path, metadata))
737    }
738
739    #[cfg(target_os = "linux")]
740    fn linux_peer_pid(stream: &UnixStream) -> Result<libc::pid_t, ViaError> {
741        let mut credentials = std::mem::MaybeUninit::<libc::ucred>::uninit();
742        let mut length = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
743        // SAFETY: `credentials` points to valid writable memory for `length` bytes,
744        // and `stream.as_raw_fd()` is a live Unix socket file descriptor.
745        let result = unsafe {
746            libc::getsockopt(
747                stream.as_raw_fd(),
748                libc::SOL_SOCKET,
749                libc::SO_PEERCRED,
750                credentials.as_mut_ptr().cast(),
751                &mut length,
752            )
753        };
754        if result != 0 {
755            return Err(io::Error::last_os_error().into());
756        }
757        if length as usize != std::mem::size_of::<libc::ucred>() {
758            return Err(ViaError::InvalidConfig(
759                "daemon could not read peer process credentials".to_owned(),
760            ));
761        }
762
763        // SAFETY: `getsockopt` succeeded and wrote a complete `ucred` value.
764        Ok(unsafe { credentials.assume_init() }.pid)
765    }
766
767    #[cfg(target_os = "macos")]
768    fn peer_executable_identity(stream: &UnixStream) -> Result<ExecutableIdentity, ViaError> {
769        let pid = macos_peer_pid(stream)?;
770        let mut buffer = vec![0_u8; libc::PROC_PIDPATHINFO_MAXSIZE as usize];
771        // SAFETY: `buffer` is valid writable memory for `buffer.len()` bytes.
772        let length =
773            unsafe { libc::proc_pidpath(pid, buffer.as_mut_ptr().cast(), buffer.len() as u32) };
774        if length <= 0 {
775            return Err(io::Error::last_os_error().into());
776        }
777        buffer.truncate(length as usize);
778        let path = PathBuf::from(std::ffi::OsString::from_vec(buffer));
779        executable_identity_from_path(&path)
780    }
781
782    #[cfg(target_os = "macos")]
783    fn macos_peer_pid(stream: &UnixStream) -> Result<libc::pid_t, ViaError> {
784        let mut pid = std::mem::MaybeUninit::<libc::pid_t>::uninit();
785        let mut length = std::mem::size_of::<libc::pid_t>() as libc::socklen_t;
786        // SAFETY: `pid` points to valid writable memory for `length` bytes,
787        // and `stream.as_raw_fd()` is a live Unix socket file descriptor.
788        let result = unsafe {
789            libc::getsockopt(
790                stream.as_raw_fd(),
791                libc::SOL_LOCAL,
792                libc::LOCAL_PEERPID,
793                pid.as_mut_ptr().cast(),
794                &mut length,
795            )
796        };
797        if result != 0 {
798            return Err(io::Error::last_os_error().into());
799        }
800        if length as usize != std::mem::size_of::<libc::pid_t>() {
801            return Err(ViaError::InvalidConfig(
802                "daemon could not read peer process id".to_owned(),
803            ));
804        }
805
806        // SAFETY: `getsockopt` succeeded and wrote a complete `pid_t` value.
807        Ok(unsafe { pid.assume_init() })
808    }
809
810    #[derive(PartialEq, Eq)]
811    enum DaemonAction {
812        Continue,
813        Stop,
814    }
815
816    enum ServerEvent {
817        Connection(UnixStream),
818        NoConnection,
819        IdleTimeout,
820    }
821
822    #[cfg(test)]
823    mod tests {
824        use super::*;
825
826        #[test]
827        fn rejects_unregistered_resolve_request() {
828            let mut state = DaemonState::default();
829
830            let response = state.handle(DaemonRequest::Resolve {
831                config_hash: "config".to_owned(),
832                ref_id: "secret".to_owned(),
833                ttl_seconds: 300,
834            });
835
836            assert!(!response.ok);
837            assert!(response
838                .error
839                .as_deref()
840                .unwrap()
841                .contains("not registered"));
842        }
843
844        #[test]
845        fn rejects_registered_non_op_reference() {
846            let mut state = DaemonState::default();
847
848            let response = state.handle(DaemonRequest::Register {
849                config_hash: "config".to_owned(),
850                account: None,
851                refs: vec![super::super::AllowedOnePasswordRef {
852                    id: "secret".to_owned(),
853                    reference: "plaintext".to_owned(),
854                }],
855            });
856
857            assert!(!response.ok);
858            assert!(response
859                .error
860                .as_deref()
861                .unwrap()
862                .contains("must start with op://"));
863        }
864
865        #[test]
866        fn resolves_registered_ref_id_from_cache() {
867            let mut state = DaemonState::default();
868            let register = state.handle(DaemonRequest::Register {
869                config_hash: "config".to_owned(),
870                account: None,
871                refs: vec![super::super::AllowedOnePasswordRef {
872                    id: "secret".to_owned(),
873                    reference: "op://Private/Example/token".to_owned(),
874                }],
875            });
876            assert!(register.ok);
877            state.cache.insert(
878                SecretCacheKey {
879                    config_hash: "config".to_owned(),
880                    ref_id: "secret".to_owned(),
881                },
882                SecretCacheEntry {
883                    value: SecretValue::new("cached-secret".to_owned()),
884                    expires_at: Instant::now() + Duration::from_secs(300),
885                },
886            );
887
888            let response = state.handle(DaemonRequest::Resolve {
889                config_hash: "config".to_owned(),
890                ref_id: "secret".to_owned(),
891                ttl_seconds: 300,
892            });
893
894            assert!(response.ok);
895            assert_eq!(response.cache.as_deref(), Some("hit"));
896            assert_eq!(
897                response.value.as_ref().map(SecretValue::expose),
898                Some("cached-secret")
899            );
900        }
901
902        #[test]
903        fn clear_drops_cached_values_and_registered_refs() {
904            let mut state = DaemonState::default();
905            let register = state.handle(DaemonRequest::Register {
906                config_hash: "config".to_owned(),
907                account: None,
908                refs: vec![super::super::AllowedOnePasswordRef {
909                    id: "secret".to_owned(),
910                    reference: "op://Private/Example/token".to_owned(),
911                }],
912            });
913            assert!(register.ok);
914            state.cache.insert(
915                SecretCacheKey {
916                    config_hash: "config".to_owned(),
917                    ref_id: "secret".to_owned(),
918                },
919                SecretCacheEntry {
920                    value: SecretValue::new("cached-secret".to_owned()),
921                    expires_at: Instant::now() + Duration::from_secs(300),
922                },
923            );
924
925            let clear = state.handle(DaemonRequest::Clear);
926            assert!(clear.ok);
927            let response = state.handle(DaemonRequest::Resolve {
928                config_hash: "config".to_owned(),
929                ref_id: "secret".to_owned(),
930                ttl_seconds: 300,
931            });
932
933            assert!(!response.ok);
934            assert!(state.cache.is_empty());
935            assert!(state.registrations.is_empty());
936        }
937    }
938}
939
940#[cfg(not(unix))]
941mod imp {
942    use crate::error::ViaError;
943    use crate::secrets::SecretValue;
944
945    pub fn resolve_onepassword_secret(
946        _config_hash: &str,
947        _ref_id: &str,
948        _ttl_seconds: u64,
949    ) -> Result<SecretValue, ViaError> {
950        Err(ViaError::InvalidConfig(
951            "via daemon cache is only supported on Unix-like platforms".to_owned(),
952        ))
953    }
954
955    pub fn register_onepassword_refs(
956        _config_hash: &str,
957        _account: Option<&str>,
958        _refs: Vec<super::AllowedOnePasswordRef>,
959    ) -> Result<(), ViaError> {
960        Err(ViaError::InvalidConfig(
961            "via daemon cache is only supported on Unix-like platforms".to_owned(),
962        ))
963    }
964
965    pub fn serve() -> Result<(), ViaError> {
966        Err(ViaError::InvalidConfig(
967            "via daemon cache is only supported on Unix-like platforms".to_owned(),
968        ))
969    }
970
971    pub fn status() -> Result<(), ViaError> {
972        println!("via daemon: unsupported");
973        Ok(())
974    }
975
976    pub fn clear() -> Result<(), ViaError> {
977        println!("via daemon: unsupported");
978        Ok(())
979    }
980
981    pub fn stop() -> Result<(), ViaError> {
982        println!("via daemon: unsupported");
983        Ok(())
984    }
985}
986
987pub use imp::{clear, register_onepassword_refs, resolve_onepassword_secret, serve, status, stop};