|
1
|
use std::collections::HashMap; |
|
2
|
use std::io::{Read, Write}; |
|
3
|
use std::net::{TcpListener, TcpStream}; |
|
4
|
|
|
5
|
struct AuthServer { |
|
6
|
users: HashMap<String, String>, |
|
7
|
upstream_host: String, |
|
8
|
smtp_port: u16, |
|
9
|
imap_port: u16, |
|
10
|
} |
|
11
|
|
|
12
|
impl AuthServer { |
|
13
|
fn new() -> Self { |
|
14
|
let mut users = HashMap::new(); |
|
15
|
// TODO: replace with secure storage (DB/KMS). This is only a demo. |
|
16
|
users.insert("admin".to_string(), "your_admin_password".to_string()); |
|
17
|
users.insert("user".to_string(), "your_user_password".to_string()); |
|
18
|
|
|
19
|
AuthServer { |
|
20
|
users, |
|
21
|
upstream_host: "127.0.0.1".to_string(), |
|
22
|
smtp_port: 2525, // Your smbcloud-mail-smtp SMTP server |
|
23
|
imap_port: 1143, // Your smbcloud-mail-smtp IMAP server |
|
24
|
} |
|
25
|
} |
|
26
|
|
|
27
|
fn handle_client(&self, mut stream: TcpStream) { |
|
28
|
let Ok((method, _path, headers)) = read_http_request(&mut stream) else { |
|
29
|
let _ = stream.write_all(b"HTTP/1.1 500 Internal Server Error\r\n\r\n"); |
|
30
|
let _ = stream.flush(); |
|
31
|
return; |
|
32
|
}; |
|
33
|
|
|
34
|
if method != "GET" && method != "POST" { |
|
35
|
let _ = stream.write_all(b"HTTP/1.1 405 Method Not Allowed\r\n\r\n"); |
|
36
|
let _ = stream.flush(); |
|
37
|
return; |
|
38
|
} |
|
39
|
|
|
40
|
let method_hdr = get_hdr(&headers, "auth-method").unwrap_or("plain"); |
|
41
|
let user = get_hdr(&headers, "auth-user").unwrap_or(""); |
|
42
|
let pass = get_hdr(&headers, "auth-pass").unwrap_or(""); |
|
43
|
let proto = get_hdr(&headers, "auth-protocol").unwrap_or("smtp"); |
|
44
|
|
|
45
|
// Validate credentials |
|
46
|
let mut ok = false; |
|
47
|
if method_hdr.eq_ignore_ascii_case("plain") { |
|
48
|
if !user.is_empty() && !pass.is_empty() { |
|
49
|
if let Some(stored) = self.users.get(user) { |
|
50
|
ok = stored == pass; |
|
51
|
} |
|
52
|
} |
|
53
|
} |
|
54
|
|
|
55
|
if ok { |
|
56
|
let (host, port) = match proto { |
|
57
|
p if p.eq_ignore_ascii_case("smtp") => (&self.upstream_host, self.smtp_port), |
|
58
|
p if p.eq_ignore_ascii_case("imap") => (&self.upstream_host, self.imap_port), |
|
59
|
_ => (&self.upstream_host, self.smtp_port), |
|
60
|
}; |
|
61
|
|
|
62
|
let resp = format!( |
|
63
|
"HTTP/1.1 200 OK\r\n\ |
|
64
|
Auth-Status: OK\r\n\ |
|
65
|
Auth-Server: {host}\r\n\ |
|
66
|
Auth-Port: {port}\r\n\ |
|
67
|
Auth-User: {user}\r\n\ |
|
68
|
Auth-Wait: 0\r\n\ |
|
69
|
\r\n" |
|
70
|
); |
|
71
|
let _ = stream.write_all(resp.as_bytes()); |
|
72
|
let _ = stream.flush(); |
|
73
|
} else { |
|
74
|
let resp = |
|
75
|
"HTTP/1.1 200 OK\r\nAuth-Status: Invalid login or password\r\nAuth-Wait: 2\r\n\r\n"; |
|
76
|
let _ = stream.write_all(resp.as_bytes()); |
|
77
|
let _ = stream.flush(); |
|
78
|
} |
|
79
|
} |
|
80
|
} |
|
81
|
|
|
82
|
fn get_hdr<'a>(headers: &'a Vec<(String, String)>, name: &str) -> Option<&'a str> { |
|
83
|
headers |
|
84
|
.iter() |
|
85
|
.find(|(k, _)| k.eq_ignore_ascii_case(name)) |
|
86
|
.map(|(_, v)| v.as_str()) |
|
87
|
} |
|
88
|
|
|
89
|
fn read_http_request( |
|
90
|
stream: &mut TcpStream, |
|
91
|
) -> Result<(String, String, Vec<(String, String)>), ()> { |
|
92
|
let mut buf = Vec::with_capacity(4096); |
|
93
|
let mut tmp = [0u8; 1024]; |
|
94
|
loop { |
|
95
|
let n = stream.read(&mut tmp).map_err(|_| ())?; |
|
96
|
if n == 0 { |
|
97
|
break; |
|
98
|
} |
|
99
|
buf.extend_from_slice(&tmp[..n]); |
|
100
|
if memchr_crlfcrlf(&buf) { |
|
101
|
break; |
|
102
|
} |
|
103
|
if buf.len() > 64 * 1024 { |
|
104
|
break; |
|
105
|
} |
|
106
|
} |
|
107
|
|
|
108
|
let req = String::from_utf8_lossy(&buf); |
|
109
|
let mut lines = req.split("\r\n"); |
|
110
|
|
|
111
|
let req_line = lines.next().ok_or(())?; |
|
112
|
let mut parts = req_line.split_whitespace(); |
|
113
|
let method = parts.next().unwrap_or("").to_string(); |
|
114
|
let path = parts.next().unwrap_or("/").to_string(); |
|
115
|
|
|
116
|
let mut headers = Vec::new(); |
|
117
|
for line in lines { |
|
118
|
if line.is_empty() { |
|
119
|
break; |
|
120
|
} |
|
121
|
if let Some((k, v)) = line.split_once(':') { |
|
122
|
headers.push((k.trim().to_string(), v.trim().to_string())); |
|
123
|
} |
|
124
|
} |
|
125
|
Ok((method, path, headers)) |
|
126
|
} |
|
127
|
|
|
128
|
fn memchr_crlfcrlf(buf: &[u8]) -> bool { |
|
129
|
buf.windows(4).any(|w| w == b"\r\n\r\n") |
|
130
|
} |
|
131
|
|
|
132
|
fn main() -> std::io::Result<()> { |
|
133
|
let auth = AuthServer::new(); |
|
134
|
let listener = TcpListener::bind("127.0.0.1:8080")?; |
|
135
|
eprintln!("Mail auth_http server listening on 127.0.0.1:8080"); |
|
136
|
|
|
137
|
for stream in listener.incoming() { |
|
138
|
match stream { |
|
139
|
Ok(stream) => { |
|
140
|
auth.handle_client(stream); |
|
141
|
} |
|
142
|
Err(e) => eprintln!("accept error: {e}"), |
|
143
|
} |
|
144
|
} |
|
145
|
Ok(()) |
|
146
|
} |