1use crate::{
8 core::{ProxyError, ProxyRequest},
9 debug_fmt, error_fmt,
10 security::{SecurityProvider, SecurityStage},
11 trace_fmt, warn_fmt,
12};
13use async_trait::async_trait;
14use base64::{Engine as _, engine::general_purpose};
15use globset::{Glob, GlobSet, GlobSetBuilder};
16use serde::Deserialize;
17use subtle::ConstantTimeEq;
18
19const BASIC: &str = "basic ";
20
21#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
22pub struct RouteRuleConfig {
23 pub methods: Vec<String>,
24 pub path: String,
25}
26
27#[derive(Debug)]
28struct RouteRule {
29 methods: Vec<String>,
30 paths: GlobSet,
31}
32
33impl RouteRule {
34 fn matches(&self, method: &str, path: &str) -> bool {
35 let method_match = self.methods.iter().any(|m| m == "*" || m == method);
36 let path_match = self.paths.is_match(path);
37
38 trace_fmt!(
39 "BasicAuthProvider",
40 "Basic Auth bypass rule check: method={} path={} -> method_match={} path_match={}",
41 method,
42 path,
43 method_match,
44 path_match
45 );
46
47 method_match && path_match
48 }
49}
50
51#[derive(Debug, Clone, Deserialize, serde::Serialize)]
53pub struct BasicAuthConfig {
54 pub credentials: Vec<String>,
56 #[serde(default)]
58 pub bypass: Vec<RouteRuleConfig>,
59}
60
61#[derive(Debug)]
63pub struct BasicAuthProvider {
64 valid_credentials: Vec<(String, String)>,
65 rules: Vec<RouteRule>,
66}
67
68impl BasicAuthProvider {
69 pub fn new(cfg: BasicAuthConfig) -> Result<Self, ProxyError> {
70 let mut valid_credentials = Vec::new();
71 for cred_pair in cfg.credentials {
72 let parts: Vec<&str> = cred_pair.splitn(2, ':').collect();
73 if parts.len() == 2 {
74 valid_credentials.push((parts[0].to_string(), parts[1].to_string()));
75 } else {
76 let err =
77 ProxyError::SecurityError(format!("Invalid credential format: {cred_pair}"));
78 error_fmt!("BasicAuthProvider", "{}", err);
79 return Err(err);
80 }
81 }
82
83 let mut rules = Vec::with_capacity(cfg.bypass.len());
84 for raw in cfg.bypass {
85 let mut builder = GlobSetBuilder::new();
86 match Glob::new(&raw.path) {
87 Ok(glob) => {
88 builder.add(glob);
89 rules.push(RouteRule {
90 methods: raw.methods.iter().map(|m| m.to_ascii_uppercase()).collect(),
91 paths: match builder.build() {
92 Ok(set) => set,
93 Err(e) => {
94 let err = ProxyError::SecurityError(format!(
95 "Failed to build glob set for path {}: {}",
96 raw.path, e
97 ));
98 error_fmt!("BasicAuthProvider", "{}", err);
99 return Err(err);
100 }
101 },
102 });
103 debug_fmt!(
104 "BasicAuthProvider",
105 "Added Basic Auth bypass rule: methods={:?}, path={}",
106 raw.methods,
107 raw.path
108 );
109 }
110 Err(e) => {
111 let err = ProxyError::SecurityError(format!(
112 "Invalid glob pattern in bypass rule: {e}"
113 ));
114 error_fmt!("BasicAuthProvider", "{}", err);
115 return Err(err);
116 }
117 }
118 }
119
120 Ok(Self {
121 valid_credentials,
122 rules,
123 })
124 }
125
126 pub fn validate_credentials_constant_time(&self, username: &str, password: &str) -> bool {
131 let mut valid = false;
132
133 for (stored_username, stored_password) in &self.valid_credentials {
136 let username_match = stored_username.as_bytes().ct_eq(username.as_bytes());
137 let password_match = stored_password.as_bytes().ct_eq(password.as_bytes());
138
139 let both_match = username_match & password_match;
141
142 valid |= bool::from(both_match);
144 }
145
146 valid
147 }
148
149 #[inline]
150 fn is_bypassed(&self, method: &str, path: &str) -> bool {
151 let bypassed = self.rules.iter().any(|r| r.matches(method, path));
152 if bypassed {
153 debug_fmt!(
154 "BasicAuthProvider",
155 "Basic Auth bypass for {} {}",
156 method,
157 path
158 );
159 }
160 bypassed
161 }
162}
163
164#[async_trait]
165impl SecurityProvider for BasicAuthProvider {
166 fn name(&self) -> &str {
167 "Basic"
168 }
169
170 fn stage(&self) -> SecurityStage {
171 SecurityStage::Pre
172 }
173
174 async fn pre(&self, req: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
175 if self.is_bypassed(&req.method.to_string(), &req.path) {
177 debug_fmt!(
178 "BasicAuthProvider",
179 "Basic Auth bypass for {} {}",
180 req.method,
181 req.path
182 );
183 return Ok(req);
184 }
185
186 debug_fmt!(
187 "BasicAuthProvider",
188 "Basic Auth validating request: {} {}",
189 req.method,
190 req.path
191 );
192
193 let auth_header = match req.headers.get("authorization") {
195 Some(h) => match h.to_str() {
196 Ok(s) => s,
197 Err(e) => {
198 let err =
199 ProxyError::SecurityError(format!("Invalid authorization header: {e}"));
200 warn_fmt!("BasicAuthProvider", "{}", err);
201 return Err(err);
202 }
203 },
204 None => {
205 let err = ProxyError::SecurityError("Missing authorization header".to_string());
206 warn_fmt!("BasicAuthProvider", "{}", err);
207 return Err(err);
208 }
209 };
210
211 if !auth_header.to_lowercase().starts_with(BASIC) {
212 let err = ProxyError::SecurityError(format!(
213 "Invalid authorization scheme: expected 'Basic', got '{}'",
214 auth_header.split_whitespace().next().unwrap_or("")
215 ));
216 warn_fmt!("BasicAuthProvider", "{}", err);
217 return Err(err);
218 }
219
220 let encoded_credentials = &auth_header[BASIC.len()..];
221 if encoded_credentials.is_empty() {
222 let err = ProxyError::SecurityError("Empty basic auth credentials".to_string());
223 warn_fmt!("BasicAuthProvider", "{}", err);
224 return Err(err);
225 }
226
227 let decoded_credentials = match general_purpose::STANDARD.decode(encoded_credentials) {
229 Ok(bytes) => match String::from_utf8(bytes) {
230 Ok(s) => s,
231 Err(e) => {
232 let err =
233 ProxyError::SecurityError(format!("Invalid UTF-8 in credentials: {e}"));
234 warn_fmt!("BasicAuthProvider", "{}", err);
235 return Err(err);
236 }
237 },
238 Err(e) => {
239 let err =
240 ProxyError::SecurityError(format!("Failed to base64 decode credentials: {e}"));
241 warn_fmt!("BasicAuthProvider", "{}", err);
242 return Err(err);
243 }
244 };
245
246 let parts: Vec<&str> = decoded_credentials.splitn(2, ':').collect();
247 if parts.len() != 2 {
248 let err = ProxyError::SecurityError("Invalid basic auth credential format".to_string());
249 warn_fmt!("BasicAuthProvider", "{}", err);
250 return Err(err);
251 }
252 let username = parts[0];
253 let password = parts[1];
254
255 if self.validate_credentials_constant_time(username, password) {
258 debug_fmt!(
259 "BasicAuthProvider",
260 "Basic Auth validation successful for user: {}",
261 username
262 );
263 Ok(req)
264 } else {
265 let err = ProxyError::SecurityError("Invalid basic auth credentials".to_string());
266 warn_fmt!("BasicAuthProvider", "{}", err);
267 Err(err)
268 }
269 }
270}