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 let user_match = credentials.0.username() == userid;
78 let password_match = bcrypt_verify(credentials.0.password(), password)
79 .inspect_err(|err| tracing::error!("bcrypt password verification error: {:?}", err))
80 .unwrap_or(false);
81 let valid = user_match && password_match;
82 valid.then_some(()).ok_or(StatusCode::UNAUTHORIZED)
83}
84
85#[cfg(test)]
86mod tests {
87 use super::{check_request, pre_process};
88 use crate::{Error, handler::RequestHandlerOpts};
89 use headers::HeaderMap;
90 use hyper::{Body, Request, Response, StatusCode, header::WWW_AUTHENTICATE};
91
92 fn make_request(method: &str, auth_header: &str) -> Request<Body> {
93 let mut builder = Request::builder();
94 if !auth_header.is_empty() {
95 builder = builder.header("Authorization", auth_header);
96 }
97 builder.method(method).uri("/").body(Body::empty()).unwrap()
98 }
99
100 fn is_401(result: Option<Result<Response<Body>, Error>>) -> bool {
101 if let Some(Ok(response)) = result {
102 response.status() == StatusCode::UNAUTHORIZED
103 && response.headers().get(WWW_AUTHENTICATE).is_some()
104 } else {
105 false
106 }
107 }
108
109 fn is_500(result: Option<Result<Response<Body>, Error>>) -> bool {
110 if let Some(Ok(response)) = result {
111 response.status() == StatusCode::INTERNAL_SERVER_ERROR
112 } else {
113 false
114 }
115 }
116
117 #[test]
118 fn test_auth_disabled() {
119 assert!(
120 pre_process(
121 &RequestHandlerOpts {
122 basic_auth: "".into(),
123 ..Default::default()
124 },
125 &make_request("GET", "Basic anE6anE=")
126 )
127 .is_none()
128 );
129 }
130
131 #[test]
132 fn test_invalid_auth_configuration() {
133 assert!(is_500(pre_process(
134 &RequestHandlerOpts {
135 basic_auth: "xyz".into(),
136 ..Default::default()
137 },
138 &make_request("GET", "Basic anE6anE=")
139 )));
140 }
141
142 #[test]
143 fn test_options_request() {
144 assert!(
145 pre_process(
146 &RequestHandlerOpts {
147 basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
148 .into(),
149 ..Default::default()
150 },
151 &make_request("OPTIONS", "")
152 )
153 .is_none()
154 );
155 }
156
157 #[test]
158 fn test_valid_auth() {
159 let mut headers = HeaderMap::new();
160 headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
161 assert!(
162 check_request(
163 &headers,
164 "jq",
165 "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
166 )
167 .is_ok()
168 );
169
170 assert!(
171 pre_process(
172 &RequestHandlerOpts {
173 basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
174 .into(),
175 ..Default::default()
176 },
177 &make_request("GET", "Basic anE6anE=")
178 )
179 .is_none()
180 );
181 }
182
183 #[test]
184 fn test_invalid_auth_header() {
185 let headers = HeaderMap::new();
186 assert!(check_request(&headers, "jq", "").is_err());
187
188 assert!(is_401(pre_process(
189 &RequestHandlerOpts {
190 basic_auth: "jq:".into(),
191 ..Default::default()
192 },
193 &make_request("GET", "")
194 )));
195 }
196
197 #[test]
198 fn test_invalid_auth_pairs() {
199 let mut headers = HeaderMap::new();
200 headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
201 assert!(check_request(&headers, "xyz", "").is_err());
202
203 assert!(is_401(pre_process(
204 &RequestHandlerOpts {
205 basic_auth: "xyz:".into(),
206 ..Default::default()
207 },
208 &make_request("GET", "Basic anE6anE=")
209 )));
210 }
211
212 #[test]
213 fn test_invalid_auth() {
214 let mut headers = HeaderMap::new();
215 headers.insert("Authorization", "Basic anE6anE=".parse().unwrap());
216 assert!(
217 check_request(
218 &headers,
219 "abc",
220 "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
221 )
222 .is_err()
223 );
224 assert!(check_request(&headers, "jq", "password").is_err());
225 assert!(check_request(&headers, "", "password").is_err());
226 assert!(check_request(&headers, "jq", "").is_err());
227
228 assert!(is_401(pre_process(
229 &RequestHandlerOpts {
230 basic_auth: "abc:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
231 .into(),
232 ..Default::default()
233 },
234 &make_request("GET", "Basic anE6anE=")
235 )));
236 assert!(is_401(pre_process(
237 &RequestHandlerOpts {
238 basic_auth: "jq:password".into(),
239 ..Default::default()
240 },
241 &make_request("GET", "Basic anE6anE=")
242 )));
243 assert!(is_401(pre_process(
244 &RequestHandlerOpts {
245 basic_auth: ":password".into(),
246 ..Default::default()
247 },
248 &make_request("GET", "Basic anE6anE=")
249 )));
250 assert!(is_401(pre_process(
251 &RequestHandlerOpts {
252 basic_auth: "jq:".into(),
253 ..Default::default()
254 },
255 &make_request("GET", "Basic anE6anE=")
256 )));
257 }
258
259 #[test]
260 fn test_invalid_auth_encoding() {
261 let mut headers = HeaderMap::new();
262 headers.insert("Authorization", "Basic xyz".parse().unwrap());
263 assert!(
264 check_request(
265 &headers,
266 "jq",
267 "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
268 )
269 .is_err()
270 );
271
272 assert!(is_401(pre_process(
273 &RequestHandlerOpts {
274 basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
275 .into(),
276 ..Default::default()
277 },
278 &make_request("GET", "Basic xyz")
279 )));
280 }
281
282 #[test]
283 fn test_invalid_auth_encoding2() {
284 let mut headers = HeaderMap::new();
285 headers.insert("Authorization", "abcd".parse().unwrap());
286 assert!(
287 check_request(
288 &headers,
289 "jq",
290 "$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
291 )
292 .is_err()
293 );
294
295 assert!(is_401(pre_process(
296 &RequestHandlerOpts {
297 basic_auth: "jq:$2y$05$32zazJ1yzhlDHnt26L3MFOgY0HVqPmDUvG0KUx6cjf9RDiUGp/M9q"
298 .into(),
299 ..Default::default()
300 },
301 &make_request("GET", "abcd")
302 )));
303 }
304}