1use regex::Regex;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::net::{IpAddr, Ipv4Addr};
5use std::path::PathBuf;
6
7use super::audit::AuditLog;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct NetworkConfig {
12 pub allowed_networks: Vec<String>,
14
15 pub allowed_hosts: Vec<String>,
17
18 pub audit_log: bool,
20
21 pub audit_log_path: Option<String>,
23}
24
25impl Default for NetworkConfig {
26 fn default() -> Self {
27 NetworkConfig {
28 allowed_networks: vec![
29 "10.0.0.0/8".to_string(),
30 "172.16.0.0/12".to_string(),
31 "192.168.0.0/16".to_string(),
32 ],
33 allowed_hosts: vec![],
34 audit_log: true,
35 audit_log_path: Some("~/.lit/audit.log".to_string()),
36 }
37 }
38}
39
40impl NetworkConfig {
41 pub fn load() -> Result<Self, String> {
43 let config_path = Self::config_path()?;
44
45 if !config_path.exists() {
46 return Ok(NetworkConfig::default());
47 }
48
49 let content = fs::read_to_string(&config_path)
50 .map_err(|e| format!("Failed to read config: {}", e))?;
51
52 toml::from_str(&content).map_err(|e| format!("Failed to parse config: {}", e))
53 }
54
55 fn config_path() -> Result<PathBuf, String> {
57 let home = dirs::home_dir().ok_or("Could not find home directory")?;
58 Ok(home.join(".litconfig"))
59 }
60
61 pub fn save(&self) -> Result<(), String> {
63 let config_path = Self::config_path()?;
64
65 let content = toml::to_string_pretty(self)
66 .map_err(|e| format!("Failed to serialize config: {}", e))?;
67
68 fs::write(&config_path, content).map_err(|e| format!("Failed to write config: {}", e))
69 }
70}
71
72pub struct NetworkValidator {
74 config: NetworkConfig,
75}
76
77impl NetworkValidator {
78 pub fn new() -> Result<Self, String> {
80 let config = NetworkConfig::load()?;
81 Ok(NetworkValidator { config })
82 }
83
84 pub fn validate_url(&self, url: &str) -> Result<(), String> {
86 let url_parts = self.parse_url(url)?;
88
89 if url_parts.protocol != "lit" {
91 return Err(format!(
92 "Invalid protocol '{}'. Only 'lit://' protocol is allowed for LAN operations",
93 url_parts.protocol
94 ));
95 }
96
97 self.validate_host(&url_parts.host)?;
99
100 if self.config.audit_log {
102 self.log_access(url)?;
103 }
104
105 Ok(())
106 }
107
108 fn parse_url(&self, url: &str) -> Result<UrlParts, String> {
110 let re =
111 Regex::new(r"^([a-z]+)://([^/]+)(.*)$").map_err(|e| format!("Regex error: {}", e))?;
112
113 let captures = re
114 .captures(url)
115 .ok_or_else(|| format!("Invalid URL format: {}", url))?;
116
117 Ok(UrlParts {
118 protocol: captures.get(1).unwrap().as_str().to_string(),
119 host: captures.get(2).unwrap().as_str().to_string(),
120 path: captures.get(3).unwrap().as_str().to_string(),
121 })
122 }
123
124 fn validate_host(&self, host: &str) -> Result<(), String> {
126 if let Ok(ip) = host.parse::<IpAddr>() {
128 return self.validate_ip(&ip);
129 }
130
131 if self.config.allowed_hosts.iter().any(|h| h == host) {
133 return Ok(());
134 }
135
136 Err(format!(
141 "Host '{}' is not in the allowed LAN hosts list. \
142 Configure allowed hosts in ~/.litconfig",
143 host
144 ))
145 }
146
147 fn validate_ip(&self, ip: &IpAddr) -> Result<(), String> {
149 match ip {
150 IpAddr::V4(ipv4) => self.validate_ipv4(ipv4),
151 IpAddr::V6(_) => Err("IPv6 not supported yet".to_string()),
152 }
153 }
154
155 fn validate_ipv4(&self, ip: &Ipv4Addr) -> Result<(), String> {
157 for network in &self.config.allowed_networks {
158 if self.ip_in_cidr(ip, network)? {
159 return Ok(());
160 }
161 }
162
163 Err(format!(
164 "IP address '{}' is not in any allowed LAN network range. \
165 Configure allowed networks in ~/.litconfig",
166 ip
167 ))
168 }
169
170 fn ip_in_cidr(&self, ip: &Ipv4Addr, cidr: &str) -> Result<bool, String> {
172 let parts: Vec<&str> = cidr.split('/').collect();
173
174 if parts.len() != 2 {
175 return Err(format!("Invalid CIDR notation: {}", cidr));
176 }
177
178 let network_ip: Ipv4Addr = parts[0]
179 .parse()
180 .map_err(|e| format!("Invalid IP in CIDR: {}", e))?;
181
182 let prefix_len: u8 = parts[1]
183 .parse()
184 .map_err(|e| format!("Invalid prefix length in CIDR: {}", e))?;
185
186 if prefix_len > 32 {
187 return Err("Invalid prefix length: must be 0-32".to_string());
188 }
189
190 let mask = if prefix_len == 0 {
191 0u32
192 } else {
193 !0u32 << (32 - prefix_len)
194 };
195
196 let network_int = u32::from_be_bytes(network_ip.octets());
197 let ip_int = u32::from_be_bytes(ip.octets());
198
199 Ok((network_int & mask) == (ip_int & mask))
200 }
201
202 fn log_access(&self, url: &str) -> Result<(), String> {
204 if self.config.audit_log {
205 let audit_path = self.config.audit_log_path.as_deref();
207 let audit = AuditLog::new(audit_path)?;
208 audit.log("NETWORK_ACCESS", url)?;
209 }
210
211 Ok(())
212 }
213}
214
215#[allow(dead_code)]
216struct UrlParts {
217 protocol: String,
218 host: String,
219 path: String,
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_cidr_matching() {
228 let config = NetworkConfig::default();
229 let validator = NetworkValidator { config };
230
231 let ip: Ipv4Addr = "192.168.1.100".parse().unwrap();
232 assert!(validator.ip_in_cidr(&ip, "192.168.0.0/16").unwrap());
233 assert!(!validator.ip_in_cidr(&ip, "10.0.0.0/8").unwrap());
234 }
235
236 #[test]
237 fn test_url_parsing() {
238 let config = NetworkConfig::default();
239 let validator = NetworkValidator { config };
240
241 let url = "lit://192.168.1.100/repo.lit";
242 let parts = validator.parse_url(url).unwrap();
243
244 assert_eq!(parts.protocol, "lit");
245 assert_eq!(parts.host, "192.168.1.100");
246 assert_eq!(parts.path, "/repo.lit");
247 }
248}