cli_shared/
client_config.rs1use std::net::{IpAddr, SocketAddr};
5
6use wire::AuthToken;
7
8#[derive(Debug, Clone)]
10pub struct ClientConfig {
11 pub client_id: String,
13 pub token: Option<AuthToken>,
15 pub auth_proof_key_pem: Option<String>,
17 pub server_key: Option<String>,
20 pub tls_enabled: bool,
22 pub tls_domain_name: Option<String>,
24 pub tls_ca_certificate_pem: Option<String>,
26 pub tls_skip_verify: bool,
28 pub allow_insecure: bool,
34 pub timeout_secs: u64,
36 pub compression: bool,
38 pub chunk_size: usize,
40 pub chunked_transfer: bool,
42 pub resumable_transfer: bool,
44 pub pack_transfer: bool,
46 pub partial_fetch: bool,
48}
49
50impl ClientConfig {
51 pub fn new(client_id: impl Into<String>) -> Self {
53 Self {
54 client_id: client_id.into(),
55 token: None,
56 auth_proof_key_pem: None,
57 server_key: None,
58 tls_enabled: false,
59 tls_domain_name: None,
60 tls_ca_certificate_pem: None,
61 tls_skip_verify: false,
62 allow_insecure: false,
63 timeout_secs: 30,
64 compression: true,
65 chunk_size: 64 * 1024,
66 chunked_transfer: true,
67 resumable_transfer: true,
68 pack_transfer: true,
69 partial_fetch: true,
70 }
71 }
72
73 pub fn with_token(mut self, token: AuthToken) -> Self {
75 self.token = Some(token);
76 self
77 }
78
79 pub fn with_auth_proof_key_pem(mut self, pem: impl Into<String>) -> Self {
81 self.auth_proof_key_pem = Some(pem.into());
82 self
83 }
84
85 pub fn with_server_key(mut self, key: impl Into<String>) -> Self {
87 self.server_key = Some(key.into());
88 self
89 }
90
91 pub fn with_tls(mut self, skip_verify: bool) -> Self {
93 self.tls_enabled = true;
94 self.tls_skip_verify = skip_verify;
95 self
96 }
97
98 pub fn with_tls_domain_name(mut self, domain_name: impl Into<String>) -> Self {
99 self.tls_domain_name = Some(domain_name.into());
100 self
101 }
102
103 pub fn with_tls_ca_certificate_pem(mut self, pem: impl Into<String>) -> Self {
104 self.tls_enabled = true;
105 self.tls_ca_certificate_pem = Some(pem.into());
106 self
107 }
108
109 pub fn with_timeout(mut self, secs: u64) -> Self {
111 self.timeout_secs = secs;
112 self
113 }
114
115 pub fn without_compression(mut self) -> Self {
117 self.compression = false;
118 self
119 }
120
121 pub fn with_chunk_size(mut self, size: usize) -> Self {
123 self.chunk_size = size;
124 self
125 }
126
127 pub fn with_chunked_transfer(mut self, enabled: bool) -> Self {
129 self.chunked_transfer = enabled;
130 self
131 }
132
133 pub fn with_resumable_transfer(mut self, enabled: bool) -> Self {
135 self.resumable_transfer = enabled;
136 self
137 }
138
139 pub fn with_pack_transfer(mut self, enabled: bool) -> Self {
141 self.pack_transfer = enabled;
142 self
143 }
144
145 pub fn with_partial_fetch(mut self, enabled: bool) -> Self {
147 self.partial_fetch = enabled;
148 self
149 }
150
151 pub fn with_allow_insecure(mut self, allow: bool) -> Self {
153 self.allow_insecure = allow;
154 self
155 }
156}
157
158impl Default for ClientConfig {
159 fn default() -> Self {
160 Self::new("heddle-client")
161 }
162}
163
164pub fn is_loopback_ip(ip: IpAddr) -> bool {
166 ip.is_loopback()
167}
168
169pub fn cleartext_connect_allowed(
175 addr: SocketAddr,
176 tls_enabled: bool,
177 allow_insecure: bool,
178) -> bool {
179 tls_enabled || is_loopback_ip(addr.ip()) || allow_insecure
180}
181
182pub fn cleartext_refused_message(addr: SocketAddr) -> String {
184 format!(
185 "refusing cleartext connection to non-loopback address {addr}; \
186enable TLS (remote.tls_enabled / HEDDLE_REMOTE_TLS) or pass --insecure \
187(or set remote.insecure=true / HEDDLE_REMOTE_INSECURE=1) for intentional cleartext \
188(e.g. VPN → VPS testing)"
189 )
190}
191
192#[cfg(test)]
193mod tests {
194 use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
195
196 use super::*;
197
198 #[test]
199 fn loopback_cleartext_allowed_without_insecure() {
200 let v4 = SocketAddr::from((Ipv4Addr::LOCALHOST, 8421));
201 let v6 = SocketAddr::from((Ipv6Addr::LOCALHOST, 8421));
202 assert!(cleartext_connect_allowed(v4, false, false));
203 assert!(cleartext_connect_allowed(v6, false, false));
204 }
205
206 #[test]
207 fn non_loopback_cleartext_requires_insecure() {
208 let addr = SocketAddr::from((Ipv4Addr::new(203, 0, 113, 10), 8421));
209 assert!(!cleartext_connect_allowed(addr, false, false));
210 assert!(cleartext_connect_allowed(addr, false, true));
211 assert!(cleartext_connect_allowed(addr, true, false));
212 }
213
214 #[test]
215 fn is_loopback_classifies_hosts() {
216 assert!(is_loopback_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
217 assert!(is_loopback_ip(IpAddr::V6(Ipv6Addr::LOCALHOST)));
218 assert!(!is_loopback_ip(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
219 }
220}