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