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