lean_ctx/core/
proxy_endpoint.rs1pub const DEFAULT_PROXY_PORT: u16 = 4444;
9
10pub fn proxy_timeout() -> std::time::Duration {
12 if let Ok(val) = std::env::var("LEAN_CTX_PROXY_TIMEOUT_MS")
13 && let Ok(ms) = val.parse::<u64>()
14 {
15 return std::time::Duration::from_millis(ms);
16 }
17 if let Some(ms) = crate::core::config::Config::load().proxy_timeout_ms {
18 return std::time::Duration::from_millis(ms);
19 }
20 std::time::Duration::from_millis(200)
21}
22
23pub fn lean_ctx_proxy_responding(port: u16) -> bool {
27 use std::io::{Read, Write};
28 use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream};
29
30 let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);
31 let Ok(mut stream) = TcpStream::connect_timeout(&addr, proxy_timeout()) else {
32 return false;
33 };
34 let _ = stream.set_read_timeout(Some(std::time::Duration::from_secs(3)));
35 let req =
36 format!("GET /health HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\r\n");
37 if stream.write_all(req.as_bytes()).is_err() {
38 return false;
39 }
40 let mut buf = Vec::with_capacity(1024);
41 let _ = stream.take(8 * 1024).read_to_end(&mut buf);
42 is_lean_ctx_health_response(&String::from_utf8_lossy(&buf))
43}
44
45fn is_lean_ctx_health_response(response: &str) -> bool {
48 let status_ok = response
49 .lines()
50 .next()
51 .is_some_and(|line| line.starts_with("HTTP/1.") && line.split(' ').nth(1) == Some("200"));
52 status_ok && response.contains("\"service\":\"lean-ctx-proxy\"")
53}
54
55pub fn configured_proxy_live() -> bool {
59 crate::core::config::Config::load().proxy_enabled == Some(true)
60 && lean_ctx_proxy_responding(default_port())
61}
62
63pub fn default_port() -> u16 {
64 if let Ok(val) = std::env::var("LEAN_CTX_PROXY_PORT")
65 && let Ok(port) = val.parse::<u16>()
66 {
67 return port;
68 }
69 let cfg = crate::core::config::Config::load();
70 if let Some(port) = cfg.proxy_port {
71 return port;
72 }
73 uid_based_port()
74}
75
76pub fn uid_based_port() -> u16 {
80 #[cfg(unix)]
81 {
82 let uid = unsafe { libc::getuid() } as u16;
85 let offset = uid.saturating_sub(1000) % 1000;
86 DEFAULT_PROXY_PORT + offset
87 }
88 #[cfg(not(unix))]
89 {
90 DEFAULT_PROXY_PORT
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn uid_port_first_regular_user() {
100 assert_eq!(DEFAULT_PROXY_PORT, 4444);
102 }
103
104 #[test]
105 fn uid_port_no_overflow() {
106 let port = DEFAULT_PROXY_PORT + 999;
109 assert_eq!(port, 5443);
110 assert!(port < u16::MAX);
111 }
112
113 #[test]
114 fn uid_port_system_accounts_get_base() {
115 let uid: u16 = 500;
117 let offset = uid.saturating_sub(1000) % 1000;
118 assert_eq!(DEFAULT_PROXY_PORT + offset, DEFAULT_PROXY_PORT);
119 }
120
121 #[test]
122 fn proxy_timeout_default_200ms() {
123 if std::env::var("LEAN_CTX_PROXY_TIMEOUT_MS").is_ok() {
124 return;
125 }
126 assert_eq!(proxy_timeout(), std::time::Duration::from_millis(200));
127 }
128
129 #[test]
130 fn proxy_timeout_is_non_zero() {
131 let t = proxy_timeout();
132 assert!(t.as_millis() > 0);
133 }
134
135 #[test]
136 fn proxy_liveness_ignores_a_foreign_process_on_the_port() {
137 use std::io::{Read, Write};
138 let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
141 let port = listener.local_addr().expect("addr").port();
142 let server = std::thread::spawn(move || {
143 let bodies = [
144 r#"{"status":"ok"}"#,
145 r#"{"pid":1,"service":"lean-ctx-proxy","status":"ok"}"#,
146 ];
147 for body in bodies {
148 let (mut stream, _) = listener.accept().expect("accept");
149 let mut req = [0u8; 512];
150 let _ = stream.read(&mut req);
151 let resp = format!(
152 "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\r\n{body}",
153 body.len()
154 );
155 let _ = stream.write_all(resp.as_bytes());
156 }
157 });
158 assert!(
159 !lean_ctx_proxy_responding(port),
160 "foreign 200 must not count"
161 );
162 assert!(
163 lean_ctx_proxy_responding(port),
164 "lean-ctx /health must count"
165 );
166 server.join().expect("server thread");
167 }
168}