1use std::path::PathBuf;
8use std::sync::RwLock;
9
10use http::{HeaderMap, Method, Request, Response, StatusCode};
11use log::{debug, error};
12use serde_json::Value;
13
14use crate::config::{Config, ConfigError};
15
16pub struct Router {
21 path: PathBuf,
23 config: RwLock<Config>,
25 secrets: RwLock<Value>,
27}
28
29impl Router {
30 pub fn new<P: Into<PathBuf>>(path: P) -> Result<Self, ConfigError> {
32 let path = path.into();
33 let config = Config::from_path(&path)?;
34
35 Ok(Self {
36 path,
37
38 secrets: RwLock::new(config.secrets()?),
39 config: RwLock::new(config),
40 })
41 }
42
43 fn reload(&self) -> http::Result<Response<String>> {
45 match Config::from_path(&self.path) {
46 Ok(config) => {
47 {
48 let mut inner_config = self
49 .config
50 .write()
51 .expect("expected to be able to get a write lock on the configuration");
52
53 *inner_config = config;
54 }
55
56 self.reload_secrets()
57 },
58 Err(err) => {
59 error!("failed to load configuration: {:?}", err);
60
61 Response::builder()
62 .status(StatusCode::NOT_ACCEPTABLE)
63 .body(format!("{:?}", err))
64 },
65 }
66 }
67
68 fn reload_secrets(&self) -> http::Result<Response<String>> {
70 let config = self
71 .config
72 .read()
73 .expect("expected to be able to get a read lock on the configuration");
74
75 match config.secrets() {
76 Ok(secrets) => {
77 let mut inner_secrets = self
78 .secrets
79 .write()
80 .expect("expected to be able to get a write lock on the secrets");
81
82 *inner_secrets = secrets;
83
84 Response::builder()
85 .status(StatusCode::OK)
86 .body(String::new())
87 },
88 Err(err) => {
89 error!("failed to load secrets: {:?}", err);
90
91 Response::builder()
92 .status(StatusCode::NOT_ACCEPTABLE)
93 .body(format!("{:?}", err))
94 },
95 }
96 }
97
98 fn handle_impl(
100 &self,
101 path: &str,
102 headers: &HeaderMap,
103 data: &[u8],
104 object: Value,
105 ) -> http::Result<Response<()>> {
106 let config = self
107 .config
108 .read()
109 .expect("expected to be able to get a read lock on the configuration");
110
111 if let Some(handler) = config.post_paths.get(path) {
112 let secret = {
113 let secrets = self
114 .secrets
115 .read()
116 .expect("expected to be able to get a read lock on the secrets");
117
118 handler.lookup_secret(&secrets, &object).map(String::from)
119 };
120
121 if !handler.verify(headers, secret.as_ref().map(AsRef::as_ref), data) {
122 error!(
123 target: "handler",
124 "failed to verify the a webhook:\nheaders:\n{:?}\ndata:\n{}",
125 headers,
126 String::from_utf8_lossy(data),
127 );
128
129 return Response::builder()
130 .status(StatusCode::NOT_ACCEPTABLE)
131 .body(());
132 }
133
134 if let Some(kind) = handler.kind(headers, &object) {
135 if let Err(err) = handler.write_object(&kind, object.clone()) {
136 error!(
137 target: "handler",
138 "failed to write the {} object {}: {:?}",
139 kind,
140 object,
141 err,
142 );
143
144 }
146 }
147
148 Response::builder().status(StatusCode::ACCEPTED).body(())
149 } else {
150 Response::builder().status(StatusCode::NOT_FOUND).body(())
151 }
152 }
153
154 pub fn handle(&self, req: &Request<Vec<u8>>) -> Result<Response<String>, http::Error> {
159 let path = req.uri().path();
160
161 if path.is_empty() {
162 return Response::builder()
163 .status(StatusCode::NOT_FOUND)
164 .body(String::new());
165 }
166
167 let path = &path[1..];
169
170 debug!(
171 target: "handler",
172 "got a {} request at {}",
173 req.method(),
174 path,
175 );
176
177 Ok(match *req.method() {
178 Method::PUT => {
179 if path == "__reload" {
180 self.reload()?
181 } else if path == "__reload_secrets" {
182 self.reload_secrets()?
183 } else {
184 Response::builder()
185 .status(StatusCode::NOT_FOUND)
186 .body(String::new())?
187 }
188 },
189 Method::POST => {
190 let data = req.body();
191
192 serde_json::from_slice(data)
193 .map(|object| {
194 self.handle_impl(path, req.headers(), data, object)
195 .map(|rsp| rsp.map(|()| String::new()))
196 })
197 .unwrap_or_else(|err| {
198 Response::builder()
199 .status(StatusCode::BAD_REQUEST)
200 .body(format!("{:?}", err))
201 })?
202 },
203 _ => {
204 Response::builder()
205 .status(StatusCode::METHOD_NOT_ALLOWED)
206 .body(String::new())?
207 },
208 })
209 }
210}
211
212#[cfg(test)]
213mod test {
214 use std::ffi::OsStr;
215 use std::fs::{self, DirEntry, File, OpenOptions};
216 use std::path::Path;
217
218 use http::{Method, Request, StatusCode};
219 use serde_json::{json, Value};
220
221 use crate::router::Router;
222 use crate::test_utils;
223
224 fn create_router(path: &Path, config: Value, secrets: Value) -> Router {
225 let (config_path, _) = test_utils::write_config_secrets(path, config, secrets);
226 Router::new(config_path).unwrap()
227 }
228
229 fn hook_files(path: &Path) -> Vec<DirEntry> {
230 let current_dir = OsStr::new(".");
231 let parent_dir = OsStr::new("..");
232 fs::read_dir(path)
233 .unwrap()
234 .map(Result::unwrap)
235 .filter(|entry| {
236 let file_name = entry.file_name();
237 file_name != current_dir && file_name != parent_dir
238 })
239 .collect()
240 }
241
242 #[test]
243 fn test_reload() {
244 let tempdir = test_utils::create_tempdir();
245 let router = create_router(tempdir.path(), json!({}), json!({}));
246
247 {
249 let hook = json!({});
250 let req = Request::post("/test")
251 .body(serde_json::to_vec(&hook).unwrap())
252 .unwrap();
253 let rsp = router.handle(&req).unwrap();
254
255 assert_eq!(rsp.status(), StatusCode::NOT_FOUND);
256 assert_eq!(rsp.body(), "");
257 }
258
259 let test_path = tempdir.path().join("test");
261 fs::create_dir(&test_path).unwrap();
262 {
263 let mut fout = OpenOptions::new().write(true).open(&router.path).unwrap();
264 let config = json!({
265 "post_paths": {
266 "test": {
267 "path": test_path.to_str().unwrap(),
268 "filters": [],
269 },
270 },
271 });
272 serde_json::to_writer(&mut fout, &config).unwrap();
273 }
274
275 {
277 let req = Request::put("/__reload").body(Vec::new()).unwrap();
278 let rsp = router.handle(&req).unwrap();
279
280 assert_eq!(rsp.status(), StatusCode::OK);
281 assert_eq!(rsp.body(), "");
282 }
283
284 {
286 let hook = json!({});
287 let req = Request::post("/test")
288 .body(serde_json::to_vec(&hook).unwrap())
289 .unwrap();
290 let rsp = router.handle(&req).unwrap();
291
292 assert_eq!(rsp.status(), StatusCode::ACCEPTED);
293 assert_eq!(rsp.body(), "");
294 }
295
296 {
298 let hook_files = hook_files(&test_path);
299 assert!(hook_files.is_empty());
300 }
301 }
302
303 #[test]
304 fn test_reload_error() {
305 let router = {
306 let tempdir = test_utils::create_tempdir();
307 create_router(tempdir.path(), json!({}), json!({}))
308 };
309 let req = Request::put("/__reload").body(Vec::new()).unwrap();
310 let rsp = router.handle(&req).unwrap();
311
312 assert_eq!(rsp.status(), StatusCode::NOT_ACCEPTABLE);
313 assert!(
314 rsp.body().contains("Read {"),
315 "Error response did not match: {}",
316 rsp.body(),
317 );
318 }
319
320 #[test]
321 fn test_reload_broken_secrets() {
322 let tempdir = test_utils::create_tempdir();
323 let router = create_router(tempdir.path(), json!({}), json!({}));
324
325 {
327 let config = router
328 .config
329 .read()
330 .expect("expected to be able to get a read lock on the configuration");
331 let secrets_path = config.secrets_path().unwrap();
332 File::create(secrets_path).unwrap();
333 }
334
335 let req = Request::put("/__reload").body(Vec::new()).unwrap();
337 let rsp = router.handle(&req).unwrap();
338
339 assert_eq!(rsp.status(), StatusCode::NOT_ACCEPTABLE);
340 assert!(
341 rsp.body().contains("while parsing a value"),
342 "Error response did not match: {}",
343 rsp.body(),
344 );
345 }
346
347 #[test]
348 fn test_reload_secrets_error() {
349 let router = {
350 let tempdir = test_utils::create_tempdir();
351 create_router(tempdir.path(), json!({}), json!({}))
352 };
353 let req = Request::put("/__reload_secrets").body(Vec::new()).unwrap();
354 let rsp = router.handle(&req).unwrap();
355
356 assert_eq!(rsp.status(), StatusCode::NOT_ACCEPTABLE);
357 assert!(
358 rsp.body().contains("Read {"),
359 "Error response did not match: {}",
360 rsp.body(),
361 );
362 }
363
364 #[test]
365 fn test_invalid_put() {
366 let tempdir = test_utils::create_tempdir();
367 let router = create_router(tempdir.path(), json!({}), json!({}));
368 let req = Request::put("/not_an_endpoint").body(Vec::new()).unwrap();
369 let rsp = router.handle(&req).unwrap();
370
371 assert_eq!(rsp.status(), StatusCode::NOT_FOUND);
372 assert_eq!(rsp.body(), "");
373 }
374
375 #[test]
376 fn test_invalid_methods() {
377 let tempdir = test_utils::create_tempdir();
378 let router = create_router(tempdir.path(), json!({}), json!({}));
379 let invalid_methods = [
380 Method::GET,
381 Method::DELETE,
384 Method::HEAD,
385 Method::OPTIONS,
386 Method::CONNECT,
387 Method::PATCH,
388 Method::TRACE,
389 ];
390
391 for invalid_method in invalid_methods.iter().cloned() {
392 let mut req = Request::new(Vec::new());
393 *req.method_mut() = invalid_method;
394 let rsp = router.handle(&req).unwrap();
395
396 assert_eq!(rsp.status(), StatusCode::METHOD_NOT_ALLOWED);
397 assert_eq!(rsp.body(), "");
398 }
399 }
400
401 #[test]
402 fn test_bad_hook() {
403 let tempdir = test_utils::create_tempdir();
404 let router = create_router(tempdir.path(), json!({}), json!({}));
405
406 let req = Request::post("/test").body(Vec::new()).unwrap();
407 let rsp = router.handle(&req).unwrap();
408
409 assert_eq!(rsp.status(), StatusCode::BAD_REQUEST);
410 assert!(
411 rsp.body().contains("while parsing a value"),
412 "Error response did not match: {}",
413 rsp.body(),
414 );
415 }
416
417 #[test]
418 fn test_write_hook_error() {
419 let tempdir = test_utils::create_tempdir();
420 let test_path = tempdir.path().join("test");
421 let config = json!({
423 "post_paths": {
424 "test": {
425 "path": test_path.to_str().unwrap(),
426 "filters": [
427 {
428 "kind": "unknown",
429 },
430 ],
431 },
432 },
433 });
434 let router = create_router(tempdir.path(), config, json!({}));
435
436 let hook = json!({});
437 let req = Request::post("/test")
438 .body(serde_json::to_vec(&hook).unwrap())
439 .unwrap();
440 let rsp = router.handle(&req).unwrap();
441
442 assert_eq!(rsp.status(), StatusCode::ACCEPTED);
443 assert_eq!(rsp.body(), "");
444 }
445
446 #[test]
447 fn test_hook() {
448 let tempdir = test_utils::create_tempdir();
449 let test_path = tempdir.path().join("test");
450 fs::create_dir(&test_path).unwrap();
451 let config = json!({
452 "post_paths": {
453 "test": {
454 "path": test_path.to_str().unwrap(),
455 "filters": [
456 {
457 "kind": "unknown",
458 },
459 ],
460 },
461 },
462 });
463 let router = create_router(tempdir.path(), config, json!({}));
464
465 let hook = json!({});
466 let req = Request::post("/test")
467 .body(serde_json::to_vec(&hook).unwrap())
468 .unwrap();
469 let rsp = router.handle(&req).unwrap();
470
471 assert_eq!(rsp.status(), StatusCode::ACCEPTED);
472 assert_eq!(rsp.body(), "");
473
474 let hook_files = hook_files(&test_path);
475 assert_eq!(hook_files.len(), 1);
476
477 let path = hook_files[0].path();
478 let hook_contents = fs::read_to_string(path).unwrap();
479 let actual: Value = serde_json::from_str(&hook_contents).unwrap();
480
481 assert_eq!(
482 actual,
483 json!({
484 "kind": "unknown",
485 "data": {},
486 }),
487 );
488 }
489
490 #[test]
491 fn test_unverified_hook() {
492 let tempdir = test_utils::create_tempdir();
493 let test_path = tempdir.path().join("test");
494 fs::create_dir(&test_path).unwrap();
495 let config = json!({
496 "post_paths": {
497 "test": {
498 "path": test_path.to_str().unwrap(),
499 "filters": [
500 {
501 "kind": "unknown",
502 },
503 ],
504 "verification": {
505 "secret_key_lookup": "/secret",
506 "verification_header": "X-Verify-Webhook",
507 "compare": {
508 "type": "token",
509 },
510 },
511 },
512 },
513 });
514 let secrets = json!({
515 "secret": "secret",
516 });
517 let router = create_router(tempdir.path(), config, secrets);
518
519 let hook = json!({});
520 let req = Request::post("/test")
521 .body(serde_json::to_vec(&hook).unwrap())
522 .unwrap();
523 let rsp = router.handle(&req).unwrap();
524
525 assert_eq!(rsp.status(), StatusCode::NOT_ACCEPTABLE);
526 assert_eq!(rsp.body(), "");
527
528 let hook_files = hook_files(&test_path);
530 assert!(hook_files.is_empty());
531 }
532
533 #[test]
534 fn test_verified_hook() {
535 let tempdir = test_utils::create_tempdir();
536 let test_path = tempdir.path().join("test");
537 fs::create_dir(&test_path).unwrap();
538 let config = json!({
539 "post_paths": {
540 "test": {
541 "path": test_path.to_str().unwrap(),
542 "filters": [
543 {
544 "kind": "unknown",
545 },
546 ],
547 "verification": {
548 "secret_key_lookup": "secret",
549 "verification_header": "X-Verify-Webhook",
550 "compare": {
551 "type": "token",
552 },
553 },
554 },
555 },
556 });
557 let secrets = json!({
558 "secret": "secret",
559 });
560 let router = create_router(tempdir.path(), config, secrets);
561
562 let hook = json!({});
563 let req = Request::post("/test")
564 .header("X-Verify-Webhook", "secret")
565 .body(serde_json::to_vec(&hook).unwrap())
566 .unwrap();
567 let rsp = router.handle(&req).unwrap();
568
569 assert_eq!(rsp.status(), StatusCode::ACCEPTED);
570 assert_eq!(rsp.body(), "");
571
572 let hook_files = hook_files(&test_path);
573 assert_eq!(hook_files.len(), 1);
574
575 let path = hook_files[0].path();
576 let hook_contents = fs::read_to_string(path).unwrap();
577 let actual: Value = serde_json::from_str(&hook_contents).unwrap();
578
579 assert_eq!(
580 actual,
581 json!({
582 "kind": "unknown",
583 "data": {},
584 }),
585 );
586 }
587}