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