1use bcrypt::verify as bcrypt_verify;
10use headers::{Authorization, HeaderMap, HeaderMapExt, authorization::Basic};
11use hyper::{Request, Response, StatusCode, header::WWW_AUTHENTICATE};
12
13use crate::Error;
14use crate::error_page;
15use crate::exts::http::MethodExt;
16use crate::handler::RequestHandlerOpts;
17
18pub(crate) fn init(credentials: &str, handler_opts: &mut RequestHandlerOpts) {
20 credentials.trim().clone_into(&mut handler_opts.basic_auth);
21 tracing::info!(
22 "basic authentication: enabled={}",
23 !handler_opts.basic_auth.is_empty()
24 );
25}
26
27pub(crate) fn pre_process<T>(
29 opts: &RequestHandlerOpts,
30 req: &Request<T>,
31) -> Option<Result<Response<crate::body::Body>, Error>> {
32 if opts.basic_auth.is_empty() {
33 return None;
34 }
35
36 let method = req.method();
37 if method.is_options() {
38 return None;
39 }
40
41 let uri = req.uri();
42 if let Some((user_id, password)) = opts.basic_auth.split_once(':') {
43 let err = check_request(req.headers(), user_id, password).err()?;
44 tracing::warn!("basic authentication failed {:?}", err);
45 let mut result = error_page::error_response(
46 uri,
47 method,
48 &StatusCode::UNAUTHORIZED,
49 &opts.page404,
50 &opts.page50x,
51 );
52 if let Ok(ref mut resp) = result {
53 resp.headers_mut().insert(
54 WWW_AUTHENTICATE,
55 hyper::header::HeaderValue::from_static(
56 "Basic realm=\"Static Web Server\", charset=\"UTF-8\"",
57 ),
58 );
59 }
60 Some(result)
61 } else {
62 tracing::error!("invalid basic authentication `user_id:password` pairs");
63 Some(error_page::error_response(
64 uri,
65 method,
66 &StatusCode::INTERNAL_SERVER_ERROR,
67 &opts.page404,
68 &opts.page50x,
69 ))
70 }
71}
72
73pub fn check_request(headers: &HeaderMap, userid: &str, password: &str) -> Result<(), StatusCode> {
76 let credentials = headers
77 .typed_get::<Authorization<Basic>>()
78 .ok_or(StatusCode::UNAUTHORIZED)?;
79
80 let user_match = credentials.0.username() == userid;
81 let password_match = bcrypt_verify(credentials.0.password(), password)
82 .inspect_err(|err| tracing::error!("bcrypt password verification error: {:?}", err))
83 .unwrap_or(false);
84 let valid = user_match && password_match;
85 valid.then_some(()).ok_or(StatusCode::UNAUTHORIZED)
86}
87
88#[cfg(test)]
89mod tests {
90 use super::{check_request, pre_process};
91 use crate::body;
92 use crate::body::Body;
93 use crate::{Error, handler::RequestHandlerOpts};
94 use headers::HeaderMap;
95 use hyper::{Request, Response, StatusCode, header::WWW_AUTHENTICATE};
96
97 fn make_request(method: &str, auth_header: &str) -> Request<Body> {
98 let mut builder = Request::builder();
99 if !auth_header.is_empty() {
100 builder = builder.header("Authorization", auth_header);
101 }
102 builder.method(method).uri("/").body(body::empty()).unwrap()
103 }
104
105 fn is_401(result: Option<Result<Response<Body>, Error>>) -> bool {
106 if let Some(Ok(response)) = result {
107 response.status() == StatusCode::UNAUTHORIZED
108 && response.headers().get(WWW_AUTHENTICATE).is_some()
109 } else {
110 false
111 }
112 }
113
114 fn is_500(result: Option<Result<Response<crate::body::Body>, Error>>) -> bool {
115 if let Some(Ok(response)) = result {
116 response.status() == StatusCode::INTERNAL_SERVER_ERROR
117 } else {
118 false
119 }
120 }
121
122 #[test]
123 fn test_auth_disabled() {
124 assert!(
125 pre_process(
126 &RequestHandlerOpts {
127 basic_auth: "".into(),
128 ..Default::default()
129 },
130 &make_request("GET", "Basic anE6anE=")
131 )
132 .is_none()
133 );
134 }
135
136 #[test]
137 fn test_invalid_auth_configuration() {
138 assert!(is_500(pre_process(
139 &RequestHandlerOpts {
140 basic_auth: "xyz".into(),
141 ..Default::default()
142 },
143 &make_request("GET", "Basic anE6anE=")
144 )));
145 }
146
147 #[test]
148 fn test_options_request() {
149 assert!(
150 pre_process(
151 &RequestHandlerOpts {
152 basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
153 .into(),
154 ..Default::default()
155 },
156 &make_request("OPTIONS", "")
157 )
158 .is_none()
159 );
160 }
161
162 #[test]
163 fn test_valid_auth() {
164 let mut headers = HeaderMap::new();
165 headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
166 assert!(
167 check_request(
168 &headers,
169 "jq",
170 "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
171 )
172 .is_ok()
173 );
174
175 assert!(
176 pre_process(
177 &RequestHandlerOpts {
178 basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
179 .into(),
180 ..Default::default()
181 },
182 &make_request("GET", "Basic anE6anE=")
183 )
184 .is_none()
185 );
186 }
187
188 #[test]
189 fn test_invalid_auth_header() {
190 let headers = HeaderMap::new();
191 assert!(check_request(&headers, "jq", "").is_err());
192
193 assert!(is_401(pre_process(
194 &RequestHandlerOpts {
195 basic_auth: "jq:".into(),
196 ..Default::default()
197 },
198 &make_request("GET", "")
199 )));
200 }
201
202 #[test]
203 fn test_invalid_auth_pairs() {
204 let mut headers = HeaderMap::new();
205 headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
206 assert!(check_request(&headers, "xyz", "").is_err());
207
208 assert!(is_401(pre_process(
209 &RequestHandlerOpts {
210 basic_auth: "xyz:".into(),
211 ..Default::default()
212 },
213 &make_request("GET", "Basic anE6anE=")
214 )));
215 }
216
217 #[test]
218 fn test_invalid_auth() {
219 let mut headers = HeaderMap::new();
220 headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
221 assert!(
222 check_request(
223 &headers,
224 "abc",
225 "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
226 )
227 .is_err()
228 );
229 assert!(check_request(&headers, "jq", "password").is_err());
230 assert!(check_request(&headers, "", "password").is_err());
231 assert!(check_request(&headers, "jq", "").is_err());
232
233 assert!(is_401(pre_process(
234 &RequestHandlerOpts {
235 basic_auth: "abc:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
236 .into(),
237 ..Default::default()
238 },
239 &make_request("GET", "Basic anE6anE=")
240 )));
241 assert!(is_401(pre_process(
242 &RequestHandlerOpts {
243 basic_auth: "jq:password".into(),
244 ..Default::default()
245 },
246 &make_request("GET", "Basic anE6anE=")
247 )));
248 assert!(is_401(pre_process(
249 &RequestHandlerOpts {
250 basic_auth: ":password".into(),
251 ..Default::default()
252 },
253 &make_request("GET", "Basic anE6anE=")
254 )));
255 assert!(is_401(pre_process(
256 &RequestHandlerOpts {
257 basic_auth: "jq:".into(),
258 ..Default::default()
259 },
260 &make_request("GET", "Basic anE6anE=")
261 )));
262 }
263
264 #[test]
265 fn test_invalid_auth_encoding() {
266 let mut headers = HeaderMap::new();
267 headers.insert("Authorization", "Basic xyz".parse().unwrap());
268 assert!(
269 check_request(
270 &headers,
271 "jq",
272 "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
273 )
274 .is_err()
275 );
276
277 assert!(is_401(pre_process(
278 &RequestHandlerOpts {
279 basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
280 .into(),
281 ..Default::default()
282 },
283 &make_request("GET", "Basic xyz")
284 )));
285 }
286
287 #[test]
288 fn test_invalid_auth_encoding2() {
289 let mut headers = HeaderMap::new();
290 headers.insert("Authorization", "abcd".parse().unwrap());
291 assert!(
292 check_request(
293 &headers,
294 "jq",
295 "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
296 )
297 .is_err()
298 );
299
300 assert!(is_401(pre_process(
301 &RequestHandlerOpts {
302 basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
303 .into(),
304 ..Default::default()
305 },
306 &make_request("GET", "abcd")
307 )));
308 }
309}