1#[cfg(unix)]
4pub use unix_impl::Client;
5
6#[cfg(not(unix))]
7pub use stub_impl::Client;
8
9#[cfg(not(unix))]
13mod stub_impl {
14 use std::path::Path;
15
16 use crate::error::{Error, Result};
17 use crate::proto::{Freshness, Request, Response};
18
19 pub struct Client {
21 _private: (),
22 }
23
24 impl Client {
25 pub fn try_connect(_socket: &Path) -> Option<Client> {
27 None
28 }
29
30 pub fn request(&mut self, _req: &Request) -> Result<Response> {
32 Self::unsupported()
33 }
34
35 pub fn request_fresh(&mut self, _req: &Request, _freshness: Freshness) -> Result<Response> {
37 Self::unsupported()
38 }
39
40 pub fn request_routed(
42 &mut self,
43 _root: &std::path::Path,
44 _req: &Request,
45 ) -> Result<Response> {
46 Self::unsupported()
47 }
48
49 pub fn request_routed_fresh(
51 &mut self,
52 _root: &std::path::Path,
53 _req: &Request,
54 _freshness: Freshness,
55 ) -> Result<Response> {
56 Self::unsupported()
57 }
58
59 fn unsupported() -> Result<Response> {
60 Err(Error::other(
61 "greplm daemon is not supported on this platform",
62 ))
63 }
64 }
65}
66
67#[cfg(unix)]
68mod unix_impl {
69 use std::io::{BufRead, BufReader, Read, Write};
70 use std::os::unix::net::UnixStream;
71 use std::path::Path;
72 use std::time::Duration;
73
74 use crate::error::{Error, Result};
75 use crate::proto::{Freshness, LocalRequest, Request, Response, RoutedRequest};
76
77 const WRITE_TIMEOUT: Duration = Duration::from_secs(10);
79 const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(30);
83 const MAX_RESPONSE_BYTES: u64 = 256 * 1024 * 1024;
87
88 pub struct Client {
90 reader: BufReader<UnixStream>,
91 writer: UnixStream,
92 }
93
94 impl Client {
95 pub fn try_connect(socket: &Path) -> Option<Client> {
98 let stream = UnixStream::connect(socket).ok()?;
99 if let (Some(peer), me) = (peer_uid(&stream), unsafe { libc::getuid() }) {
104 if peer != me {
105 tracing::warn!(
106 "daemon socket {} is owned by uid {peer}, not {me}; ignoring it",
107 socket.display()
108 );
109 return None;
110 }
111 }
112 let read_timeout = std::env::var("GREPLM_DAEMON_TIMEOUT_MS")
118 .ok()
119 .and_then(|s| s.parse::<u64>().ok())
120 .filter(|&ms| ms > 0)
121 .map(Duration::from_millis)
122 .unwrap_or(DEFAULT_READ_TIMEOUT);
123 let _ = stream.set_read_timeout(Some(read_timeout));
124 let _ = stream.set_write_timeout(Some(WRITE_TIMEOUT));
125 let reader = BufReader::new(stream.try_clone().ok()?);
126 Some(Client {
127 reader,
128 writer: stream,
129 })
130 }
131
132 pub fn request(&mut self, req: &Request) -> Result<Response> {
135 self.request_fresh(req, Freshness::Lazy)
136 }
137
138 pub fn request_fresh(&mut self, req: &Request, freshness: Freshness) -> Result<Response> {
140 let env = LocalRequest {
141 freshness,
142 req: req.clone(),
143 };
144 self.round_trip(&env)
145 }
146
147 pub fn request_routed(&mut self, root: &Path, req: &Request) -> Result<Response> {
150 self.request_routed_fresh(root, req, Freshness::Lazy)
151 }
152
153 pub fn request_routed_fresh(
155 &mut self,
156 root: &Path,
157 req: &Request,
158 freshness: Freshness,
159 ) -> Result<Response> {
160 let routed = RoutedRequest {
161 root: root.to_path_buf(),
162 freshness,
163 req: req.clone(),
164 };
165 self.round_trip(&routed)
166 }
167
168 fn round_trip<T: serde::Serialize>(&mut self, value: &T) -> Result<Response> {
169 let mut bytes = serde_json::to_vec(value)?;
170 bytes.push(b'\n');
171 self.writer.write_all(&bytes).map_err(Error::PlainIo)?;
172 self.writer.flush().map_err(Error::PlainIo)?;
173 let mut line = String::new();
174 let n = (&mut self.reader)
177 .take(MAX_RESPONSE_BYTES)
178 .read_line(&mut line)
179 .map_err(Error::PlainIo)?;
180 if n == 0 {
181 return Err(Error::other("daemon closed connection"));
182 }
183 if n as u64 >= MAX_RESPONSE_BYTES && !line.ends_with('\n') {
184 return Err(Error::other("daemon response too large"));
185 }
186 let resp: Response = serde_json::from_str(line.trim())?;
187 Ok(resp)
188 }
189 }
190
191 fn peer_uid(stream: &UnixStream) -> Option<u32> {
195 use std::os::unix::io::AsRawFd;
196 let fd = stream.as_raw_fd();
197 #[cfg(any(target_os = "linux", target_os = "android"))]
198 {
199 let mut cred = libc::ucred {
200 pid: 0,
201 uid: 0,
202 gid: 0,
203 };
204 let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
205 let rc = unsafe {
206 libc::getsockopt(
207 fd,
208 libc::SOL_SOCKET,
209 libc::SO_PEERCRED,
210 &mut cred as *mut libc::ucred as *mut libc::c_void,
211 &mut len,
212 )
213 };
214 (rc == 0).then_some(cred.uid)
215 }
216 #[cfg(not(any(target_os = "linux", target_os = "android")))]
217 {
218 let mut uid: libc::uid_t = 0;
219 let mut gid: libc::gid_t = 0;
220 let rc = unsafe { libc::getpeereid(fd, &mut uid, &mut gid) };
221 (rc == 0).then_some(uid)
222 }
223 }
224
225 #[cfg(test)]
226 mod tests {
227 use super::*;
228 use std::os::unix::net::UnixListener;
229
230 #[test]
233 fn peer_uid_is_self_over_a_socket() {
234 let dir = std::env::temp_dir().join(format!("greplm-peer-{}", std::process::id()));
235 std::fs::create_dir_all(&dir).unwrap();
236 let sock = dir.join("t.sock");
237 let _ = std::fs::remove_file(&sock);
238 let listener = UnixListener::bind(&sock).unwrap();
239 let client = UnixStream::connect(&sock).unwrap();
240 let (server, _) = listener.accept().unwrap();
241 let me = unsafe { libc::getuid() };
242 assert_eq!(peer_uid(&client), Some(me));
243 assert_eq!(peer_uid(&server), Some(me));
244 let _ = std::fs::remove_file(&sock);
245 let _ = std::fs::remove_dir(&dir);
246 }
247 }
248}