diff --git a/.woodpecker.yml b/.woodpecker.yml
new file mode 100644
index 0000000..19c16b4
--- /dev/null
+++ b/.woodpecker.yml
@@ -0,0 +1,37 @@
+pipeline:
+  fmt:
+    group: default
+    image: rust_full
+    commands:
+      - cargo fmt --all -- --check
+
+  clippy:
+    group: default
+    image: rust_full
+    commands:
+      - cargo clippy --all-features
+
+  test:
+    group: default
+    image: rust_full
+    commands:
+      - cargo test
+
+  build:
+    group: default
+    image: rust_full
+    commands:
+      - cargo build
+      - cargo build --release
+
+  build-deb:
+    group: default
+    image: rust_full
+    commands:
+      - cargo deb
+
+  doc:
+    group: default
+    image: rust_full
+    commands:
+      - cargo doc
diff --git a/Cargo.lock b/Cargo.lock
index e7c10da..3d535bc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,43 +4,84 @@ version = 3
 
 [[package]]
 name = "aho-corasick"
-version = "0.7.18"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
+checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
 dependencies = [
  "memchr",
 ]
 
 [[package]]
-name = "ansi_term"
-version = "0.12.1"
+name = "anstream"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
 dependencies = [
- "winapi",
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is-terminal",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
+dependencies = [
+ "anstyle",
+ "windows-sys",
 ]
 
 [[package]]
 name = "anyhow"
-version = "1.0.45"
+version = "1.0.71"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7"
+checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 
 [[package]]
 name = "async-stream"
-version = "0.3.2"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "171374e7e3b2504e0e5236e3b59260560f9fe94bfe9ac39ba5e4e929c5590625"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
 dependencies = [
  "async-stream-impl",
  "futures-core",
+ "pin-project-lite",
 ]
 
 [[package]]
 name = "async-stream-impl"
-version = "0.3.2"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "648ed8c8d2ce5409ccd57453d9d1b214b342a0d69376a6feda1fd6cae3299308"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -49,9 +90,9 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.51"
+version = "0.1.68"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
+checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -60,12 +101,9 @@ dependencies = [
 
 [[package]]
 name = "atomic"
-version = "0.5.1"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c"
-dependencies = [
- "autocfg",
-]
+checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
 
 [[package]]
 name = "atty"
@@ -73,28 +111,22 @@ version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.1.19",
  "libc",
  "winapi",
 ]
 
 [[package]]
 name = "autocfg"
-version = "1.0.1"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
-
-[[package]]
-name = "base-x"
-version = "0.2.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
 name = "base64"
-version = "0.13.0"
+version = "0.21.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
 
 [[package]]
 name = "binascii"
@@ -109,31 +141,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
-name = "block-buffer"
-version = "0.9.0"
+name = "bitflags"
+version = "2.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
 dependencies = [
  "generic-array",
 ]
 
 [[package]]
 name = "bumpalo"
-version = "3.8.0"
+version = "3.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c"
+checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
 
 [[package]]
 name = "bytes"
-version = "1.1.0"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
 
 [[package]]
 name = "cc"
-version = "1.0.72"
+version = "1.0.79"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
 
 [[package]]
 name = "cfg-if"
@@ -142,59 +180,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
-name = "chrono"
-version = "0.4.19"
+name = "clap"
+version = "4.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
+checksum = "ca8f255e4b8027970e78db75e78831229c9815fdbfa67eb1a1b777a62e24b4a0"
 dependencies = [
- "libc",
- "num-integer",
- "num-traits",
- "winapi",
+ "clap_builder",
+ "clap_derive",
+ "once_cell",
 ]
 
 [[package]]
-name = "clap"
-version = "3.0.0-beta.5"
+name = "clap_builder"
+version = "4.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63"
+checksum = "acd4f3c17c83b0ba34ffbc4f8bbd74f079413f747f84a6f89292f138057e36ab"
 dependencies = [
- "atty",
- "bitflags",
- "clap_derive",
- "indexmap",
- "lazy_static",
- "os_str_bytes",
+ "anstream",
+ "anstyle",
+ "bitflags 1.3.2",
+ "clap_lex",
  "strsim",
- "termcolor",
- "textwrap",
- "unicase",
 ]
 
 [[package]]
 name = "clap_derive"
-version = "3.0.0-beta.5"
+version = "4.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3"
+checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f"
 dependencies = [
  "heck",
- "proc-macro-error",
  "proc-macro2",
  "quote",
  "syn",
 ]
 
 [[package]]
-name = "const_fn"
-version = "0.4.8"
+name = "clap_lex"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f92cfa0fd5690b3cf8c1ef2cabbd9b7ef22fa53cf5e1f92b05103f6d5d1cf6e7"
+checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
 
 [[package]]
 name = "cookie"
-version = "0.15.1"
+version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5f1c7727e460397e56abc4bddc1d49e07a1ad78fc98eb2e1c8f032a58a2f80d"
+checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24"
 dependencies = [
  "percent-encoding",
  "time",
@@ -203,28 +240,28 @@ dependencies = [
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.1"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469"
+checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
 dependencies = [
  "libc",
 ]
 
 [[package]]
-name = "crypto-mac"
-version = "0.11.1"
+name = "crypto-common"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 dependencies = [
  "generic-array",
- "subtle",
+ "typenum",
 ]
 
 [[package]]
 name = "devise"
-version = "0.3.1"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50c7580b072f1c8476148f16e0a0d5dedddab787da98d86c5082c5e9ed8ab595"
+checksum = "d6eacefd3f541c66fc61433d65e54e0e46e0a029a819a7dbbc7a7b489e8a85f8"
 dependencies = [
  "devise_codegen",
  "devise_core",
@@ -232,9 +269,9 @@ dependencies = [
 
 [[package]]
 name = "devise_codegen"
-version = "0.3.1"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "123c73e7a6e51b05c75fe1a1b2f4e241399ea5740ed810b0e3e6cacd9db5e7b2"
+checksum = "9c8cf4b8dd484ede80fd5c547592c46c3745a617c8af278e2b72bea86b2dfed6"
 dependencies = [
  "devise_core",
  "quote",
@@ -242,11 +279,11 @@ dependencies = [
 
 [[package]]
 name = "devise_core"
-version = "0.3.1"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "841ef46f4787d9097405cac4e70fb8644fc037b526e8c14054247c0263c400d0"
+checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a"
 dependencies = [
- "bitflags",
+ "bitflags 2.3.1",
  "proc-macro2",
  "proc-macro2-diagnostics",
  "quote",
@@ -255,11 +292,13 @@ dependencies = [
 
 [[package]]
 name = "digest"
-version = "0.9.0"
+version = "0.10.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
 dependencies = [
- "generic-array",
+ "block-buffer",
+ "crypto-common",
+ "subtle",
 ]
 
 [[package]]
@@ -273,9 +312,9 @@ dependencies = [
 
 [[package]]
 name = "dirs-sys"
-version = "0.3.6"
+version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
 dependencies = [
  "libc",
  "redox_users",
@@ -283,43 +322,31 @@ dependencies = [
 ]
 
 [[package]]
-name = "discard"
+name = "dunce"
 version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
-
-[[package]]
-name = "dtoa"
-version = "0.4.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
-
-[[package]]
-name = "dunce"
-version = "1.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "453440c271cf5577fd2a40e4942540cb7d0d2f85e27c8d07dd0023c925a67541"
+checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
 
 [[package]]
 name = "either"
-version = "1.6.1"
+version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
 
 [[package]]
 name = "encoding_rs"
-version = "0.8.29"
+version = "0.8.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
+checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
 dependencies = [
  "cfg-if",
 ]
 
 [[package]]
 name = "env_logger"
-version = "0.9.0"
+version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3"
+checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
 dependencies = [
  "atty",
  "humantime",
@@ -329,10 +356,40 @@ dependencies = [
 ]
 
 [[package]]
-name = "figment"
-version = "0.10.6"
+name = "errno"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "790b4292c72618abbab50f787a477014fe15634f96291de45672ce46afe122df"
+checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "figment"
+version = "0.10.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4547e226f4c9ab860571e070a9034192b3175580ecea38da34fcdb53a018c9a5"
 dependencies = [
  "atomic",
  "pear",
@@ -350,24 +407,22 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
 [[package]]
 name = "fsio"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09e87827efaf94c7a44b562ff57de06930712fe21b530c3797cdede26e6377eb"
+checksum = "de6fce87c901c64837f745e7fffddeca1de8e054b544ba82c419905d40a0e1be"
 dependencies = [
  "dunce",
  "rand",
- "users",
 ]
 
 [[package]]
 name = "futures"
-version = "0.3.17"
+version = "0.3.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca"
+checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
 dependencies = [
  "futures-channel",
  "futures-core",
- "futures-executor",
  "futures-io",
  "futures-sink",
  "futures-task",
@@ -376,9 +431,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.17"
+version = "0.3.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -386,91 +441,63 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.17"
+version = "0.3.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
-
-[[package]]
-name = "futures-executor"
-version = "0.3.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
-dependencies = [
- "futures-core",
- "futures-task",
- "futures-util",
-]
+checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
 
 [[package]]
 name = "futures-io"
-version = "0.3.17"
+version = "0.3.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377"
-
-[[package]]
-name = "futures-macro"
-version = "0.3.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb"
-dependencies = [
- "autocfg",
- "proc-macro-hack",
- "proc-macro2",
- "quote",
- "syn",
-]
+checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
 
 [[package]]
 name = "futures-sink"
-version = "0.3.17"
+version = "0.3.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11"
+checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
 
 [[package]]
 name = "futures-task"
-version = "0.3.17"
+version = "0.3.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
 
 [[package]]
 name = "futures-util"
-version = "0.3.17"
+version = "0.3.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
 dependencies = [
- "autocfg",
  "futures-channel",
  "futures-core",
  "futures-io",
- "futures-macro",
  "futures-sink",
  "futures-task",
  "memchr",
  "pin-project-lite",
  "pin-utils",
- "proc-macro-hack",
- "proc-macro-nested",
  "slab",
 ]
 
 [[package]]
 name = "generator"
-version = "0.7.0"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1d9279ca822891c1a4dae06d185612cf8fc6acfe5dff37781b41297811b12ee"
+checksum = "f3e123d9ae7c02966b4d892e550bdc32164f05853cd40ab570650ad600596a8a"
 dependencies = [
  "cc",
  "libc",
  "log",
  "rustversion",
- "winapi",
+ "windows",
 ]
 
 [[package]]
 name = "generic-array"
-version = "0.14.4"
+version = "0.14.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
 dependencies = [
  "typenum",
  "version_check",
@@ -478,9 +505,9 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.3"
+version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
 dependencies = [
  "cfg-if",
  "libc",
@@ -489,15 +516,15 @@ dependencies = [
 
 [[package]]
 name = "glob"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
 
 [[package]]
 name = "h2"
-version = "0.3.7"
+version = "0.3.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55"
+checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782"
 dependencies = [
  "bytes",
  "fnv",
@@ -514,18 +541,15 @@ dependencies = [
 
 [[package]]
 name = "hashbrown"
-version = "0.11.2"
+version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 
 [[package]]
 name = "heck"
-version = "0.3.3"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
-dependencies = [
- "unicode-segmentation",
-]
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 
 [[package]]
 name = "hermit-abi"
@@ -536,6 +560,21 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -544,19 +583,18 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
 
 [[package]]
 name = "hmac"
-version = "0.11.0"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
 dependencies = [
- "crypto-mac",
  "digest",
 ]
 
 [[package]]
 name = "http"
-version = "0.2.5"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b"
+checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
 dependencies = [
  "bytes",
  "fnv",
@@ -565,9 +603,9 @@ dependencies = [
 
 [[package]]
 name = "http-body"
-version = "0.4.4"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
 dependencies = [
  "bytes",
  "http",
@@ -576,15 +614,15 @@ dependencies = [
 
 [[package]]
 name = "httparse"
-version = "1.5.1"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
 
 [[package]]
 name = "httpdate"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
 
 [[package]]
 name = "humantime"
@@ -594,9 +632,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 
 [[package]]
 name = "hyper"
-version = "0.14.14"
+version = "0.14.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b"
+checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -618,9 +656,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "1.7.0"
+version = "1.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
 dependencies = [
  "autocfg",
  "hashbrown",
@@ -629,9 +667,9 @@ dependencies = [
 
 [[package]]
 name = "inlinable_string"
-version = "0.1.14"
+version = "0.1.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3094308123a0e9fd59659ce45e22de9f53fc1d2ac6e1feb9fef988e4f76cad77"
+checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
 
 [[package]]
 name = "instant"
@@ -643,25 +681,48 @@ dependencies = [
 ]
 
 [[package]]
-name = "ipnet"
-version = "2.3.1"
+name = "io-lifetimes"
+version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f"
 dependencies = [
  "serde",
 ]
 
 [[package]]
-name = "itoa"
-version = "0.4.8"
+name = "is-terminal"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
 
 [[package]]
 name = "js-sys"
-version = "0.3.55"
+version = "0.3.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84"
+checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -674,39 +735,43 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.107"
+version = "0.2.146"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219"
+checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b"
 
 [[package]]
 name = "linked-hash-map"
-version = "0.5.4"
+version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
 
 [[package]]
 name = "lock_api"
-version = "0.4.5"
+version = "0.4.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
+checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
 dependencies = [
+ "autocfg",
  "scopeguard",
 ]
 
 [[package]]
 name = "log"
-version = "0.4.14"
+version = "0.4.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
-dependencies = [
- "cfg-if",
-]
+checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
 
 [[package]]
 name = "loom"
-version = "0.5.2"
+version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2b9df80a3804094bf49bb29881d18f6f05048db72127e84e09c26fc7c2324f5"
+checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
 dependencies = [
  "cfg-if",
  "generator",
@@ -719,52 +784,41 @@ dependencies = [
 
 [[package]]
 name = "matchers"
-version = "0.0.1"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1"
+checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
 dependencies = [
  "regex-automata",
 ]
 
 [[package]]
 name = "memchr"
-version = "2.4.1"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
 [[package]]
 name = "mime"
-version = "0.3.16"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
 
 [[package]]
 name = "mio"
-version = "0.7.14"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
+checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
 dependencies = [
  "libc",
- "log",
- "miow",
- "ntapi",
- "winapi",
-]
-
-[[package]]
-name = "miow"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
-dependencies = [
- "winapi",
+ "wasi",
+ "windows-sys",
 ]
 
 [[package]]
 name = "multer"
-version = "2.0.1"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "408327e2999b839cd1af003fc01b2019a6c10a1361769542203f6fedc5179680"
+checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2"
 dependencies = [
  "bytes",
  "encoding_rs",
@@ -772,103 +826,74 @@ dependencies = [
  "http",
  "httparse",
  "log",
+ "memchr",
  "mime",
- "spin 0.9.2",
+ "spin 0.9.8",
  "tokio",
  "tokio-util",
- "twoway",
  "version_check",
 ]
 
 [[package]]
-name = "ntapi"
-version = "0.3.6"
+name = "nu-ansi-term"
+version = "0.46.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
 dependencies = [
+ "overload",
  "winapi",
 ]
 
-[[package]]
-name = "num-integer"
-version = "0.1.44"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
-dependencies = [
- "autocfg",
- "num-traits",
-]
-
-[[package]]
-name = "num-traits"
-version = "0.2.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
-dependencies = [
- "autocfg",
-]
-
 [[package]]
 name = "num_cpus"
-version = "1.13.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.2.6",
  "libc",
 ]
 
 [[package]]
 name = "once_cell"
-version = "1.8.0"
+version = "1.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
 
 [[package]]
-name = "opaque-debug"
-version = "0.3.0"
+name = "overload"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
-
-[[package]]
-name = "os_str_bytes"
-version = "4.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799"
-dependencies = [
- "memchr",
-]
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
 
 [[package]]
 name = "parking_lot"
-version = "0.11.2"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
 dependencies = [
- "instant",
  "lock_api",
  "parking_lot_core",
 ]
 
 [[package]]
 name = "parking_lot_core"
-version = "0.8.5"
+version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
 dependencies = [
  "cfg-if",
- "instant",
  "libc",
- "redox_syscall",
+ "redox_syscall 0.3.5",
  "smallvec",
- "winapi",
+ "windows-targets",
 ]
 
 [[package]]
 name = "pear"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "15e44241c5e4c868e3eaa78b7c1848cadd6344ed4f54d029832d32b415a58702"
+checksum = "0ec95680a7087503575284e5063e14b694b7a9c0b065e5dceec661e0497127e8"
 dependencies = [
  "inlinable_string",
  "pear_codegen",
@@ -877,9 +902,9 @@ dependencies = [
 
 [[package]]
 name = "pear_codegen"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82a5ca643c2303ecb740d506539deba189e16f2754040a42901cd8105d0282d0"
+checksum = "9661a3a53f93f09f2ea882018e4d7c88f6ff2956d809a276060476fd8c879d3c"
 dependencies = [
  "proc-macro2",
  "proc-macro2-diagnostics",
@@ -889,15 +914,15 @@ dependencies = [
 
 [[package]]
 name = "percent-encoding"
-version = "2.1.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
 
 [[package]]
 name = "pin-project-lite"
-version = "0.2.7"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
 
 [[package]]
 name = "pin-utils"
@@ -907,60 +932,24 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
 [[package]]
 name = "ppv-lite86"
-version = "0.2.15"
+version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
-
-[[package]]
-name = "proc-macro-error"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
-dependencies = [
- "proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn",
- "version_check",
-]
-
-[[package]]
-name = "proc-macro-error-attr"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
-dependencies = [
- "proc-macro2",
- "quote",
- "version_check",
-]
-
-[[package]]
-name = "proc-macro-hack"
-version = "0.5.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
-
-[[package]]
-name = "proc-macro-nested"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.32"
+version = "1.0.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43"
+checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406"
 dependencies = [
- "unicode-xid",
+ "unicode-ident",
 ]
 
 [[package]]
 name = "proc-macro2-diagnostics"
-version = "0.9.1"
+version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada"
+checksum = "606c4ba35817e2922a308af55ad51bab3645b59eae5c570d4a6cf07e36bd493b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -971,23 +960,22 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.10"
+version = "1.0.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
+checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
 dependencies = [
  "proc-macro2",
 ]
 
 [[package]]
 name = "rand"
-version = "0.8.4"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
 dependencies = [
  "libc",
  "rand_chacha",
  "rand_core",
- "rand_hc",
 ]
 
 [[package]]
@@ -1002,55 +990,56 @@ dependencies = [
 
 [[package]]
 name = "rand_core"
-version = "0.6.3"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
  "getrandom",
 ]
 
-[[package]]
-name = "rand_hc"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
-dependencies = [
- "rand_core",
-]
-
 [[package]]
 name = "redox_syscall"
-version = "0.2.10"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
 dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
+dependencies = [
+ "bitflags 1.3.2",
 ]
 
 [[package]]
 name = "redox_users"
-version = "0.4.0"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
 dependencies = [
  "getrandom",
- "redox_syscall",
+ "redox_syscall 0.2.16",
+ "thiserror",
 ]
 
 [[package]]
 name = "ref-cast"
-version = "1.0.6"
+version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "300f2a835d808734ee295d45007adacb9ebb29dd3ae2424acfa17930cae541da"
+checksum = "f43faa91b1c8b36841ee70e97188a869d37ae21759da6846d4be66de5bf7b12c"
 dependencies = [
  "ref-cast-impl",
 ]
 
 [[package]]
 name = "ref-cast-impl"
-version = "1.0.6"
+version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c38e3aecd2b21cb3959637b883bb3714bc7e43f0268b9a29d3743ee3e55cdd2"
+checksum = "8d2275aab483050ab2a7364c1a46604865ee7d6906684e08db0f090acf74f9e7"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1059,13 +1048,13 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.5.4"
+version = "1.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
+checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax",
+ "regex-syntax 0.7.2",
 ]
 
 [[package]]
@@ -1074,23 +1063,20 @@ version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
 dependencies = [
- "regex-syntax",
+ "regex-syntax 0.6.29",
 ]
 
 [[package]]
 name = "regex-syntax"
-version = "0.6.25"
+version = "0.6.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
 
 [[package]]
-name = "remove_dir_all"
-version = "0.5.3"
+name = "regex-syntax"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
-dependencies = [
- "winapi",
-]
+checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
 
 [[package]]
 name = "ring"
@@ -1109,20 +1095,20 @@ dependencies = [
 
 [[package]]
 name = "rocket"
-version = "0.5.0-rc.1"
+version = "0.5.0-rc.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a71c18c42a0eb15bf3816831caf0dad11e7966f2a41aaf486a701979c4dd1f2"
+checksum = "58734f7401ae5cfd129685b48f61182331745b357b96f2367f01aebaf1cc9cc9"
 dependencies = [
  "async-stream",
  "async-trait",
  "atomic",
- "atty",
  "binascii",
  "bytes",
  "either",
  "figment",
  "futures",
  "indexmap",
+ "is-terminal",
  "log",
  "memchr",
  "multer",
@@ -1147,9 +1133,9 @@ dependencies = [
 
 [[package]]
 name = "rocket_codegen"
-version = "0.5.0-rc.1"
+version = "0.5.0-rc.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "66f5fa462f7eb958bba8710c17c5d774bbbd59809fa76fb1957af7e545aea8bb"
+checksum = "7093353f14228c744982e409259fb54878ba9563d08214f2d880d59ff2fc508b"
 dependencies = [
  "devise",
  "glob",
@@ -1163,23 +1149,24 @@ dependencies = [
 
 [[package]]
 name = "rocket_http"
-version = "0.5.0-rc.1"
+version = "0.5.0-rc.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23c8b7d512d2fcac2316ebe590cde67573844b99e6cc9ee0f53375fa16e25ebd"
+checksum = "936012c99162a03a67f37f9836d5f938f662e26f2717809761a9ac46432090f4"
 dependencies = [
  "cookie",
  "either",
+ "futures",
  "http",
  "hyper",
  "indexmap",
  "log",
  "memchr",
- "mime",
- "parking_lot",
  "pear",
  "percent-encoding",
  "pin-project-lite",
  "ref-cast",
+ "rustls",
+ "rustls-pemfile",
  "serde",
  "smallvec",
  "stable-pattern",
@@ -1200,21 +1187,25 @@ dependencies = [
 ]
 
 [[package]]
-name = "rustc_version"
-version = "0.2.3"
+name = "rustix"
+version = "0.37.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0"
 dependencies = [
- "semver",
+ "bitflags 1.3.2",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
 ]
 
 [[package]]
 name = "rustls"
-version = "0.19.1"
+version = "0.20.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7"
+checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
 dependencies = [
- "base64",
  "log",
  "ring",
  "sct",
@@ -1222,22 +1213,31 @@ dependencies = [
 ]
 
 [[package]]
-name = "rustversion"
-version = "1.0.5"
+name = "rustls-pemfile"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088"
+checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
+dependencies = [
+ "base64",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
 
 [[package]]
 name = "ryu"
-version = "1.0.5"
+version = "1.0.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
 
 [[package]]
 name = "scoped-tls"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
 
 [[package]]
 name = "scopeguard"
@@ -1247,43 +1247,28 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
 
 [[package]]
 name = "sct"
-version = "0.6.1"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce"
+checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4"
 dependencies = [
  "ring",
  "untrusted",
 ]
 
-[[package]]
-name = "semver"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
-dependencies = [
- "semver-parser",
-]
-
-[[package]]
-name = "semver-parser"
-version = "0.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
-
 [[package]]
 name = "serde"
-version = "1.0.130"
+version = "1.0.164"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.130"
+version = "1.0.164"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1292,9 +1277,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.70"
+version = "1.0.96"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3"
+checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
 dependencies = [
  "itoa",
  "ryu",
@@ -1302,34 +1287,45 @@ dependencies = [
 ]
 
 [[package]]
-name = "serde_yaml"
-version = "0.8.21"
+name = "serde_regex"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af"
+checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf"
+dependencies = [
+ "regex",
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.8.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
 dependencies = [
- "dtoa",
  "indexmap",
+ "ryu",
  "serde",
  "yaml-rust",
 ]
 
-[[package]]
-name = "sha1"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
-
 [[package]]
 name = "sha2"
-version = "0.9.8"
+version = "0.10.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa"
+checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
 dependencies = [
- "block-buffer",
  "cfg-if",
  "cpufeatures",
  "digest",
- "opaque-debug",
 ]
 
 [[package]]
@@ -1343,30 +1339,33 @@ dependencies = [
 
 [[package]]
 name = "signal-hook-registry"
-version = "1.4.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
 dependencies = [
  "libc",
 ]
 
 [[package]]
 name = "slab"
-version = "0.4.5"
+version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
+dependencies = [
+ "autocfg",
+]
 
 [[package]]
 name = "smallvec"
-version = "1.7.0"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
 
 [[package]]
 name = "socket2"
-version = "0.4.2"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516"
+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
 dependencies = [
  "libc",
  "winapi",
@@ -1380,9 +1379,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
 
 [[package]]
 name = "spin"
-version = "0.9.2"
+version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
 
 [[package]]
 name = "stable-pattern"
@@ -1393,73 +1392,15 @@ dependencies = [
  "memchr",
 ]
 
-[[package]]
-name = "standback"
-version = "0.2.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff"
-dependencies = [
- "version_check",
-]
-
 [[package]]
 name = "state"
-version = "0.5.2"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87cf4f5369e6d3044b5e365c9690f451516ac8f0954084622b49ea3fde2f6de5"
+checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b"
 dependencies = [
  "loom",
 ]
 
-[[package]]
-name = "stdweb"
-version = "0.4.20"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
-dependencies = [
- "discard",
- "rustc_version",
- "stdweb-derive",
- "stdweb-internal-macros",
- "stdweb-internal-runtime",
- "wasm-bindgen",
-]
-
-[[package]]
-name = "stdweb-derive"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
-dependencies = [
- "proc-macro2",
- "quote",
- "serde",
- "serde_derive",
- "syn",
-]
-
-[[package]]
-name = "stdweb-internal-macros"
-version = "0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
-dependencies = [
- "base-x",
- "proc-macro2",
- "quote",
- "serde",
- "serde_derive",
- "serde_json",
- "sha1",
- "syn",
-]
-
-[[package]]
-name = "stdweb-internal-runtime"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
-
 [[package]]
 name = "strsim"
 version = "0.10.0"
@@ -1468,67 +1409,58 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
 [[package]]
 name = "subtle"
-version = "2.4.1"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
 
 [[package]]
 name = "syn"
-version = "1.0.81"
+version = "2.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966"
+checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
 dependencies = [
  "proc-macro2",
  "quote",
- "unicode-xid",
+ "unicode-ident",
 ]
 
 [[package]]
 name = "tempfile"
-version = "3.2.0"
+version = "3.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
 dependencies = [
+ "autocfg",
  "cfg-if",
- "libc",
- "rand",
- "redox_syscall",
- "remove_dir_all",
- "winapi",
+ "fastrand",
+ "redox_syscall 0.3.5",
+ "rustix",
+ "windows-sys",
 ]
 
 [[package]]
 name = "termcolor"
-version = "1.1.2"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
 dependencies = [
  "winapi-util",
 ]
 
-[[package]]
-name = "textwrap"
-version = "0.14.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
-dependencies = [
- "unicode-width",
-]
-
 [[package]]
 name = "thiserror"
-version = "1.0.30"
+version = "1.0.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
+checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.30"
+version = "1.0.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
+checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1537,75 +1469,64 @@ dependencies = [
 
 [[package]]
 name = "thread_local"
-version = "1.1.3"
+version = "1.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
 dependencies = [
+ "cfg-if",
  "once_cell",
 ]
 
 [[package]]
 name = "time"
-version = "0.2.27"
+version = "0.3.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242"
+checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd"
 dependencies = [
- "const_fn",
- "libc",
- "standback",
- "stdweb",
+ "itoa",
+ "serde",
+ "time-core",
  "time-macros",
- "version_check",
- "winapi",
 ]
 
+[[package]]
+name = "time-core"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
+
 [[package]]
 name = "time-macros"
-version = "0.1.1"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1"
+checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
 dependencies = [
- "proc-macro-hack",
- "time-macros-impl",
-]
-
-[[package]]
-name = "time-macros-impl"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f"
-dependencies = [
- "proc-macro-hack",
- "proc-macro2",
- "quote",
- "standback",
- "syn",
+ "time-core",
 ]
 
 [[package]]
 name = "tokio"
-version = "1.13.0"
+version = "1.28.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
+checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2"
 dependencies = [
  "autocfg",
  "bytes",
  "libc",
- "memchr",
  "mio",
  "num_cpus",
- "once_cell",
  "pin-project-lite",
  "signal-hook-registry",
+ "socket2",
  "tokio-macros",
- "winapi",
+ "windows-sys",
 ]
 
 [[package]]
 name = "tokio-macros"
-version = "1.5.1"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "114383b041aa6212c579467afa0075fbbdd0718de036100bc0ba7961d8cb9095"
+checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1614,9 +1535,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-rustls"
-version = "0.22.0"
+version = "0.23.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
+checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
 dependencies = [
  "rustls",
  "tokio",
@@ -1625,9 +1546,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-stream"
-version = "0.1.8"
+version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
+checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
 dependencies = [
  "futures-core",
  "pin-project-lite",
@@ -1636,38 +1557,63 @@ dependencies = [
 
 [[package]]
 name = "tokio-util"
-version = "0.6.9"
+version = "0.7.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0"
+checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
 dependencies = [
  "bytes",
  "futures-core",
  "futures-sink",
- "log",
  "pin-project-lite",
  "tokio",
+ "tracing",
 ]
 
 [[package]]
 name = "toml"
-version = "0.5.8"
+version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
 dependencies = [
  "serde",
 ]
 
 [[package]]
-name = "tower-service"
-version = "0.3.1"
+name = "toml_edit"
+version = "0.19.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
+checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
 
 [[package]]
 name = "tracing"
-version = "0.1.29"
+version = "0.1.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
 dependencies = [
  "cfg-if",
  "pin-project-lite",
@@ -1677,9 +1623,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-attributes"
-version = "0.1.18"
+version = "0.1.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
+checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1688,129 +1634,85 @@ dependencies = [
 
 [[package]]
 name = "tracing-core"
-version = "0.1.21"
+version = "0.1.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
+checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
 dependencies = [
- "lazy_static",
+ "once_cell",
+ "valuable",
 ]
 
 [[package]]
 name = "tracing-log"
-version = "0.1.2"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3"
+checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
 dependencies = [
  "lazy_static",
  "log",
  "tracing-core",
 ]
 
-[[package]]
-name = "tracing-serde"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b"
-dependencies = [
- "serde",
- "tracing-core",
-]
-
 [[package]]
 name = "tracing-subscriber"
-version = "0.2.25"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71"
+checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
 dependencies = [
- "ansi_term",
- "chrono",
- "lazy_static",
  "matchers",
+ "nu-ansi-term",
+ "once_cell",
  "regex",
- "serde",
- "serde_json",
  "sharded-slab",
  "smallvec",
  "thread_local",
  "tracing",
  "tracing-core",
  "tracing-log",
- "tracing-serde",
 ]
 
 [[package]]
 name = "try-lock"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
-
-[[package]]
-name = "twoway"
-version = "0.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47"
-dependencies = [
- "memchr",
- "unchecked-index",
-]
+checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
 
 [[package]]
 name = "typenum"
-version = "1.14.0"
+version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
+checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
 
 [[package]]
 name = "ubyte"
-version = "0.10.1"
+version = "0.10.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42756bb9e708855de2f8a98195643dff31a97f0485d90d8467b39dc24be9e8fe"
+checksum = "c81f0dae7d286ad0d9366d7679a77934cfc3cf3a8d67e82669794412b2368fe6"
 dependencies = [
  "serde",
 ]
 
 [[package]]
 name = "uncased"
-version = "0.9.6"
+version = "0.9.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0"
+checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68"
 dependencies = [
  "serde",
  "version_check",
 ]
 
 [[package]]
-name = "unchecked-index"
-version = "0.2.2"
+name = "unicode-ident"
+version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
-
-[[package]]
-name = "unicase"
-version = "2.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
-dependencies = [
- "version_check",
-]
-
-[[package]]
-name = "unicode-segmentation"
-version = "1.8.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
-
-[[package]]
-name = "unicode-width"
-version = "0.1.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
 
 [[package]]
 name = "unicode-xid"
-version = "0.2.2"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
 
 [[package]]
 name = "untrusted"
@@ -1819,20 +1721,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
 
 [[package]]
-name = "users"
-version = "0.11.0"
+name = "utf8parse"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
-dependencies = [
- "libc",
- "log",
-]
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "valuable"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 
 [[package]]
 name = "version_check"
-version = "0.9.3"
+version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
 [[package]]
 name = "want"
@@ -1846,15 +1750,15 @@ dependencies = [
 
 [[package]]
 name = "wasi"
-version = "0.10.2+wasi-snapshot-preview1"
+version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.78"
+version = "0.2.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce"
+checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73"
 dependencies = [
  "cfg-if",
  "wasm-bindgen-macro",
@@ -1862,13 +1766,13 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.78"
+version = "0.2.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b"
+checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb"
 dependencies = [
  "bumpalo",
- "lazy_static",
  "log",
+ "once_cell",
  "proc-macro2",
  "quote",
  "syn",
@@ -1877,9 +1781,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.78"
+version = "0.2.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9"
+checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -1887,9 +1791,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.78"
+version = "0.2.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab"
+checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1900,15 +1804,15 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.78"
+version = "0.2.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc"
+checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
 
 [[package]]
 name = "web-sys"
-version = "0.3.55"
+version = "0.3.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb"
+checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -1916,7 +1820,7 @@ dependencies = [
 
 [[package]]
 name = "webhookey"
-version = "0.1.5"
+version = "0.1.6"
 dependencies = [
  "anyhow",
  "clap",
@@ -1931,6 +1835,7 @@ dependencies = [
  "run_script",
  "serde",
  "serde_json",
+ "serde_regex",
  "serde_yaml",
  "sha2",
  "thiserror",
@@ -1938,9 +1843,9 @@ dependencies = [
 
 [[package]]
 name = "webpki"
-version = "0.21.4"
+version = "0.22.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
+checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
 dependencies = [
  "ring",
  "untrusted",
@@ -1977,6 +1882,90 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "windows"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "winnow"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "yaml-rust"
 version = "0.4.5"
@@ -1988,6 +1977,6 @@ dependencies = [
 
 [[package]]
 name = "yansi"
-version = "0.5.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71"
+checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
diff --git a/Cargo.toml b/Cargo.toml
index d49ab4e..b7396f9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "webhookey"
-version = "0.1.5"
+version = "0.1.6"
 authors = ["finga <webhookey@onders.org>"]
 edition = "2021"
 license = "GPL-3.0-or-later"
@@ -11,22 +11,23 @@ description = "Trigger scripts via http(s) requests"
 tls = ["rocket/tls"]
 
 [dependencies]
+anyhow = "1.0"
+clap = { version = "4.3", features = ["derive"] }
+dirs = "4.0"
+env_logger = "0.9"
+hex = "0.4"
+hmac = "0.12"
+ipnet = { version = "2.3", features = ["serde"] }
+log = "0.4"
+regex = "1.5"
 rocket = "0.5.0-rc.1"
+run_script = "0.9"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
+serde_regex = "1.1"
 serde_yaml = "0.8"
-regex = "1.5"
-dirs = "4.0"
-anyhow = "1.0"
-log = "0.4"
-env_logger = "0.9"
-hmac = "0.11"
-sha2 = "0.9"
-hex = "0.4"
-ipnet = { version = "2.3", features = ["serde"] }
+sha2 = "0.10"
 thiserror = "1.0"
-run_script = "0.9"
-clap = "3.0.0-beta.5"
 
 [package.metadata.deb]
 extended-description = "Webhookey receives requests in form of a so called Webhook as for example sent by Gitea. Those requests are matched against configured filters, if a filter matches, values from the header and the body can be passed to scripts as parameters which are then executed subsequently."
@@ -36,6 +37,7 @@ assets = [
        ["config.yml", "etc/webhookey/", "644"],
        ["target/release/webhookey", "usr/bin/", "755"],
        ["README.md", "usr/share/doc/webhookey/README", "644"],
+       ["webhookey.1", "usr/share/man/man1/", "644"],
        ["debian/service", "lib/systemd/system/webhookey.service", "644"],
 ]
 conf-files = ["/etc/webhookey/config.yml"]
diff --git a/README.md b/README.md
index 9990ed7..c48045a 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,9 @@
 # Webhookey
-Webhookey is a web server listening for requests as for example sent by
-gitea's webhooks. Further, Webhookey allows you to specify rules
+![status-badge](https://ci.onders.org/api/badges/finga/webhookey/status.svg?branch=main)
+
+Webhookey is a web server listening for
+[webhooks](https://en.wikipedia.org/wiki/Webhook) as for example sent
+by Gitea's webhooks. Further, Webhookey allows you to specify rules
 which are matched against the data received to trigger certain
 actions.
 
@@ -62,7 +65,10 @@ Whereas `<config_dir>` depends on the platform:
 #### Metrics
 A metrics page can optionally enabled to query stats of the currently
 running webhookey instance. Note that stats are lost between restarts
-of webhookey as those are not stored persistently.
+of webhookey as those are not stored persistently. The `metrics`
+structure is optional as well as the `ip_filter`. The `ip_filter`
+supports either `allow` or `deny` containing a list of IPv4 and IPv6
+addresses and networks.
 
 Example:
 ```yaml
@@ -98,16 +104,17 @@ hooks:
       - secret_key_02
     filter:
       or:
-        - json:
-            pointer: /ref
-            regex: refs/heads/master
+        - not:
+            json:
+              pointer: /ref
+              regex: refs/heads/dev
         - and:
           - json:
               pointer: /ref
               regex: refs/heads/a_branch
-          - json:
-              pointer: /after
-              regex: f6e5fe4fe37df76629112d55cc210718b6a55e7e
+          - header:
+              field: X-Gitea-Event
+              regex: push
 ```
 
 ##### Command
@@ -126,25 +133,28 @@ Use values from header fields sent with the HTTP request.
 
 Example: `{{ header X-Gitea-Event }}`.
 
-##### Allow and Deny
-To allow or deny specific network ranges source is an optional
-configuration parameter which either contains an allow or a deny field
-with sequences containing networks. Note that IPv6 addresses have to
-be put in single quotes due to the colons.
+##### IP Filter
+Specific IPv4 and IPv6 addresses and/or ranges ranges can be allowed
+or denied. The `ip_filter` is optional and has to contain either an
+`allow` or a `deny` field which contains a sequence of IPv4 or IPv6
+addresses or CIDR network ranges. Note that IPv6 addresses have to be
+quoted due to the colons.
 
 Example:
 ```yaml
-allow:
-  - 127.0.0.1
-  - 127.0.0.1/31
-  - "::1"
+ip_filter:
+  allow:
+    - 127.0.0.1
+    - 127.0.0.1/31
+    - "::1"
 ```
 
 ```yaml
-deny:
-  - 127.0.0.1
-  - 127.0.0.1/31
-  - "::1"
+ip_filter:
+  deny:
+    - 127.0.0.1
+    - 127.0.0.1/31
+    - "::1"
 ```
 
 ##### Signature
@@ -163,10 +173,20 @@ hook should be executed.
 
 ###### Conjunction Filters
 Conjunction filters contain lists of other filters.
+- `not`: Logical negation.
 - `and`: Logical conjunction.
 - `or`: Logical disjunction.
 
 ###### Concrete Filters
+- `header`:
+
+  The `header` filter matches a regular expression on a field from the
+  received http(s) request header.
+
+  - `field`: The header field which should be matched.
+  - `regex`: Regular expression which has to match the specified
+    header field.
+
 - `json`:
 
   The `json` filter matches a regular expression on a field from the
diff --git a/Rocket.toml b/Rocket.toml
new file mode 100644
index 0000000..ebb03f7
--- /dev/null
+++ b/Rocket.toml
@@ -0,0 +1,2 @@
+[default]
+ident = "Webhookey"
diff --git a/src/cli.rs b/src/cli.rs
index 63c024d..49a6d69 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,4 +1,4 @@
-use clap::{crate_authors, crate_version, AppSettings, Parser};
+use clap::Parser;
 
 #[derive(Debug, Parser)]
 pub enum Command {
@@ -7,16 +7,10 @@ pub enum Command {
 }
 
 #[derive(Debug, Parser)]
-#[clap(
-    version = crate_version!(),
-    author = crate_authors!(", "),
-    global_setting = AppSettings::InferSubcommands,
-    global_setting = AppSettings::PropagateVersion,
-)]
 pub struct Opts {
     /// Provide a path to the configuration file
-    #[clap(short, long, value_name = "FILE")]
+    #[arg(short, long, value_name = "FILE")]
     pub config: Option<String>,
-    #[clap(subcommand)]
+    #[command(subcommand)]
     pub command: Option<Command>,
 }
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..64b0eb4
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,52 @@
+use crate::{filters::IpFilter, hooks::Hook};
+use anyhow::{bail, Result};
+use log::info;
+use serde::{Deserialize, Serialize};
+use std::{collections::BTreeMap, fs::File};
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct MetricsConfig {
+    pub enabled: bool,
+    pub ip_filter: Option<IpFilter>,
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct Config {
+    pub metrics: Option<MetricsConfig>,
+    pub hooks: BTreeMap<String, Hook>,
+}
+
+pub fn get_config() -> Result<File> {
+    // Look for config in CWD..
+    if let Ok(config) = File::open("config.yml") {
+        info!("Loading configuration from `./config.yml`");
+
+        return Ok(config);
+    }
+
+    // ..look for user path config..
+    if let Some(mut path) = dirs::config_dir() {
+        path.push("webhookey/config.yml");
+
+        if let Ok(config) = File::open(&path) {
+            info!(
+                "Loading configuration from `{}`",
+                path.to_str().unwrap_or("<path unprintable>"),
+            );
+
+            return Ok(config);
+        }
+    }
+
+    // ..look for systemwide config..
+    if let Ok(config) = File::open("/etc/webhookey/config.yml") {
+        info!("Loading configuration from `/etc/webhookey/config.yml`");
+
+        return Ok(config);
+    }
+
+    // ..you had your chance.
+    bail!("No configuration file found.");
+}
diff --git a/src/filters.rs b/src/filters.rs
new file mode 100644
index 0000000..dc74864
--- /dev/null
+++ b/src/filters.rs
@@ -0,0 +1,157 @@
+use crate::WebhookeyError;
+use anyhow::Result;
+use ipnet::IpNet;
+use log::{debug, error, trace};
+use regex::Regex;
+use rocket::{http::HeaderMap, Request};
+use serde::{Deserialize, Serialize};
+use std::net::IpAddr;
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields, untagged)]
+pub enum AddrType {
+    IpAddr(IpAddr),
+    IpNet(IpNet),
+}
+
+impl AddrType {
+    pub fn matches(&self, client_ip: &IpAddr) -> bool {
+        match self {
+            AddrType::IpAddr(addr) => addr == client_ip,
+            AddrType::IpNet(net) => net.contains(client_ip),
+        }
+    }
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields, rename_all = "lowercase")]
+pub enum IpFilter {
+    Allow(Vec<AddrType>),
+    Deny(Vec<AddrType>),
+}
+
+impl IpFilter {
+    pub fn validate(&self, client_ip: &IpAddr) -> bool {
+        match self {
+            IpFilter::Allow(list) => list.iter().any(|i| i.matches(client_ip)),
+            IpFilter::Deny(list) => !list.iter().any(|i| i.matches(client_ip)),
+        }
+    }
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+pub struct HeaderFilter {
+    pub field: String,
+    #[serde(with = "serde_regex")]
+    pub regex: Regex,
+}
+
+impl HeaderFilter {
+    pub fn evaluate(&self, headers: &HeaderMap) -> Result<bool, WebhookeyError> {
+        trace!(
+            "Matching `{}` on `{}` from received header",
+            &self.regex,
+            &self.field,
+        );
+
+        if let Some(value) = headers.get_one(&self.field) {
+            if self.regex.is_match(value) {
+                debug!("Regex `{}` for `{}` matches", &self.regex, &self.field);
+
+                return Ok(true);
+            }
+        }
+
+        debug!(
+            "Regex `{}` for header field `{}` does not match",
+            &self.regex, &self.field
+        );
+
+        Ok(false)
+    }
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct JsonFilter {
+    pub pointer: String,
+    #[serde(with = "serde_regex")]
+    pub regex: Regex,
+}
+
+impl JsonFilter {
+    pub fn evaluate(&self, data: &serde_json::Value) -> Result<bool, WebhookeyError> {
+        trace!(
+            "Matching `{}` on `{}` from received json",
+            &self.regex,
+            &self.pointer,
+        );
+
+        if let Some(value) = data.pointer(&self.pointer) {
+            if self.regex.is_match(&crate::get_string(value)?) {
+                debug!("Regex `{}` for `{}` matches", &self.regex, &self.pointer);
+
+                return Ok(true);
+            }
+        }
+
+        debug!(
+            "Regex `{}` for json field `{}` does not match",
+            &self.regex, &self.pointer
+        );
+
+        Ok(false)
+    }
+}
+
+macro_rules! interrelate {
+    ($request:expr, $data:expr, $filters:expr, $relation:ident) => {{
+        let (mut results, mut errors) = (Vec::new(), Vec::new());
+
+        $filters
+            .iter()
+            .map(|filter| filter.evaluate($request, $data))
+            .for_each(|item| match item {
+                Ok(o) => results.push(o),
+                Err(e) => errors.push(e),
+            });
+
+        if errors.is_empty() {
+            Ok(results.iter().$relation(|r| *r))
+        } else {
+            errors
+                .iter()
+                .for_each(|e| error!("Could not evaluate Filter: {}", e));
+
+            Err(WebhookeyError::InvalidFilter)
+        }
+    }};
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields, rename_all = "lowercase")]
+pub enum FilterType {
+    Not(Box<FilterType>),
+    And(Vec<FilterType>),
+    Or(Vec<FilterType>),
+    #[serde(rename = "header")]
+    HeaderFilter(HeaderFilter),
+    #[serde(rename = "json")]
+    JsonFilter(JsonFilter),
+}
+
+impl FilterType {
+    pub fn evaluate(
+        &self,
+        request: &Request,
+        data: &serde_json::Value,
+    ) -> Result<bool, WebhookeyError> {
+        match self {
+            FilterType::Not(filter) => Ok(!filter.evaluate(request, data)?),
+            FilterType::And(filters) => interrelate!(request, data, filters, all),
+            FilterType::Or(filters) => interrelate!(request, data, filters, any),
+            FilterType::HeaderFilter(filter) => filter.evaluate(request.headers()),
+            FilterType::JsonFilter(filter) => filter.evaluate(data),
+        }
+    }
+}
diff --git a/src/hooks.rs b/src/hooks.rs
new file mode 100644
index 0000000..b653b00
--- /dev/null
+++ b/src/hooks.rs
@@ -0,0 +1,804 @@
+use crate::{
+    filters::{FilterType, IpFilter},
+    Config, Metrics, WebhookeyError,
+};
+use anyhow::{anyhow, bail, Result};
+use hmac::{Hmac, Mac};
+use log::{debug, error, info, trace, warn};
+use rocket::{
+    data::{FromData, ToByteUnit},
+    futures::TryFutureExt,
+    http::{HeaderMap, Status},
+    outcome::Outcome::{self, Failure, Success},
+    post,
+    tokio::io::AsyncReadExt,
+    Data, Request, State,
+};
+use run_script::ScriptOptions;
+use serde::{Deserialize, Serialize};
+use sha2::Sha256;
+use std::{
+    collections::BTreeMap,
+    net::{IpAddr, Ipv4Addr, SocketAddr},
+    sync::atomic::Ordering,
+};
+
+fn accept_ip(hook_name: &str, client_ip: &IpAddr, ip: &IpFilter) -> bool {
+    if ip.validate(client_ip) {
+        info!("Allow hook `{}` from {}", &hook_name, &client_ip);
+        return true;
+    }
+
+    warn!("Deny hook `{}` from {}", &hook_name, &client_ip);
+    false
+}
+
+fn get_header_field<'a>(headers: &'a HeaderMap, param: &str) -> Result<&'a str> {
+    headers
+        .get_one(param)
+        .ok_or_else(|| anyhow!("Could not extract event parameter from header"))
+}
+
+fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
+    let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
+        .map_err(|e| anyhow!("Could not create hasher with secret: {}", e))?;
+    mac.update(data);
+    let raw_signature = hex::decode(signature.as_bytes())?;
+    mac.verify_slice(&raw_signature)
+        .map_err(|e| anyhow!("{}", e))
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct Hook {
+    command: String,
+    signature: String,
+    ip_filter: Option<IpFilter>,
+    secrets: Vec<String>,
+    filter: FilterType,
+}
+
+impl Hook {
+    fn get_command(
+        &self,
+        hook_name: &str,
+        request: &Request,
+        data: &mut serde_json::Value,
+    ) -> Result<String> {
+        debug!("Replacing parameters for command of  hook `{}`", hook_name);
+
+        Hook::replace_parameters(&self.command, request.headers(), data)
+    }
+
+    fn replace_parameters(
+        input: &str,
+        headers: &HeaderMap,
+        data: &serde_json::Value,
+    ) -> Result<String> {
+        let mut command = String::new();
+        let command_template = &mut input.chars();
+
+        while let Some(i) = command_template.next() {
+            if i == '{' {
+                if let Some('{') = command_template.next() {
+                    let mut token = String::new();
+
+                    while let Some(i) = command_template.next() {
+                        if i == '}' {
+                            if let Some('}') = command_template.next() {
+                                let expr = token.trim().split(' ').collect::<Vec<&str>>();
+
+                                let replaced = match expr.first() {
+                                    Some(&"header") => get_header_field(
+                                        headers,
+                                        expr.get(1).ok_or_else(|| {
+                                            anyhow!("Missing parameter for `header` expression")
+                                        })?,
+                                    )?
+                                    .to_string(),
+                                    Some(pointer) => crate::get_string(
+                                        data.pointer(pointer).ok_or_else(|| {
+                                            anyhow!(
+                                                "Could not find field refered to in parameter `{}`",
+                                                pointer
+                                            )
+                                        })?,
+                                    )?,
+                                    None => bail!("Invalid expression `{}`", token),
+                                };
+
+                                command.push_str(&replaced);
+
+                                trace!("Replace `{}` with: {}", token, replaced);
+
+                                break;
+                            } else {
+                                command.push('}');
+                                command.push(i);
+                            }
+                        } else {
+                            token.push(i);
+                        }
+                    }
+                } else {
+                    command.push('{');
+                    command.push(i);
+                }
+            } else {
+                command.push(i);
+            }
+        }
+
+        Ok(command)
+    }
+}
+
+#[derive(Debug)]
+pub struct Hooks {
+    pub inner: BTreeMap<String, String>,
+}
+
+impl Hooks {
+    pub async fn get_commands(
+        request: &Request<'_>,
+        data: Data<'_>,
+    ) -> Result<Self, WebhookeyError> {
+        let mut buffer = Vec::new();
+        let size = data
+            .open(1_i32.megabytes())
+            .read_to_end(&mut buffer)
+            .map_err(WebhookeyError::Io)
+            .await?;
+        info!("Data of size {} received", size);
+
+        let config = request.guard::<&State<Config>>().await.unwrap(); // should never fail
+        let mut valid = false;
+        let mut result = BTreeMap::new();
+        let client_ip = &request
+            .client_ip()
+            .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
+
+        let hooks = config.hooks.iter().filter(|(name, hook)| {
+            if let Some(ip) = &hook.ip_filter {
+                accept_ip(name, client_ip, ip)
+            } else {
+                info!(
+                    "Allow hook `{}` from {}, no IP filter was configured",
+                    &name, &client_ip
+                );
+                true
+            }
+        });
+
+        for (hook_name, hook) in hooks {
+            let signature = request
+                .headers()
+                .get_one(&hook.signature)
+                .ok_or(WebhookeyError::InvalidSignature)?;
+
+            let secrets = hook
+                .secrets
+                .iter()
+                .map(|secret| validate_request(secret, signature, &buffer));
+
+            for secret in secrets {
+                match secret {
+                    Ok(()) => {
+                        trace!("Valid signature found for hook `{}`", hook_name);
+
+                        valid = true;
+
+                        let mut data: serde_json::Value =
+                            serde_json::from_slice(&buffer).map_err(WebhookeyError::Serde)?;
+
+                        match hook.filter.evaluate(request, &data) {
+                            Ok(true) => match hook.get_command(hook_name, request, &mut data) {
+                                Ok(command) => {
+                                    info!("Filter for `{}` matched", &hook_name);
+                                    result.insert(hook_name.to_string(), command);
+                                    break;
+                                }
+                                Err(e) => error!("{}", e),
+                            },
+                            Ok(false) => info!("Filter for `{}` did not match", &hook_name),
+                            Err(error) => {
+                                error!("Could not match filter for `{}`: {}", &hook_name, error)
+                            }
+                        }
+                    }
+                    Err(e) => trace!("Hook `{}` could not validate request: {}", &hook_name, e),
+                }
+            }
+        }
+
+        if !valid {
+            return Err(WebhookeyError::Unauthorized(*client_ip));
+        }
+
+        Ok(Hooks { inner: result })
+    }
+}
+
+#[rocket::async_trait]
+impl<'r> FromData<'r> for Hooks {
+    type Error = WebhookeyError;
+
+    async fn from_data(
+        request: &'r Request<'_>,
+        data: Data<'r>,
+    ) -> Outcome<Self, (Status, Self::Error), Data<'r>> {
+        {
+            request
+                .guard::<&State<Metrics>>()
+                .await
+                .unwrap() // TODO: Check if unwrap need to be fixed
+                .requests_received
+                .fetch_add(1, Ordering::Relaxed);
+        }
+
+        match Hooks::get_commands(request, data).await {
+            Ok(hooks) => {
+                if hooks.inner.is_empty() {
+                    let client_ip = &request
+                        .client_ip()
+                        .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
+
+                    request
+                        .guard::<&State<Metrics>>()
+                        .await
+                        .unwrap() // TODO: Check if unwrap need to be fixed
+                        .hooks_unmatched
+                        .fetch_add(1, Ordering::Relaxed);
+
+                    warn!("Unmatched hook from {}", &client_ip);
+                    return Failure((Status::NotFound, WebhookeyError::UnmatchedHook(*client_ip)));
+                }
+
+                Success(hooks)
+            }
+            Err(WebhookeyError::Unauthorized(e)) => {
+                error!("{}", WebhookeyError::Unauthorized(e));
+
+                request
+                    .guard::<&State<Metrics>>()
+                    .await
+                    .unwrap() // TODO: Check if unwrap need to be fixed
+                    .hooks_forbidden
+                    .fetch_add(1, Ordering::Relaxed);
+
+                Failure((Status::Unauthorized, WebhookeyError::Unauthorized(e)))
+            }
+            Err(e) => {
+                error!("{}", e);
+
+                request
+                    .guard::<&State<Metrics>>()
+                    .await
+                    .unwrap() // TODO: Check if unwrap need to be fixed
+                    .requests_invalid
+                    .fetch_add(1, Ordering::Relaxed);
+
+                Failure((Status::BadRequest, e))
+            }
+        }
+    }
+}
+
+#[post("/", format = "json", data = "<hooks>")]
+pub async fn receive_hook<'a>(
+    address: SocketAddr,
+    hooks: Hooks,
+    metrics: &State<Metrics>,
+) -> Status {
+    info!("Post request received from: {}", address);
+
+    hooks.inner.iter().for_each(|(name, command)| {
+        info!("Execute `{}` from hook `{}`", &command, &name);
+
+        match run_script::run(command, &vec![], &ScriptOptions::new()) {
+            Ok((status, stdout, stderr)) => {
+                info!("Command `{}` exited with return code: {}", &command, status);
+                trace!("Output of command `{}` on stdout: {:?}", &command, &stdout);
+                debug!("Output of command `{}` on stderr: {:?}", &command, &stderr);
+
+                metrics.commands_executed.fetch_add(1, Ordering::Relaxed);
+
+                let _ = match status {
+                    0 => metrics.commands_successful.fetch_add(1, Ordering::Relaxed),
+                    _ => metrics.commands_failed.fetch_add(1, Ordering::Relaxed),
+                };
+            }
+            Err(e) => {
+                error!("Execution of `{}` failed: {}", &command, e);
+
+                metrics
+                    .commands_execution_failed
+                    .fetch_add(1, Ordering::Relaxed);
+            }
+        }
+
+        metrics.hooks_successful.fetch_add(1, Ordering::Relaxed);
+    });
+
+    Status::Ok
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        config::MetricsConfig,
+        filters::{AddrType, FilterType, HeaderFilter, JsonFilter},
+        hooks::Hook,
+        Metrics,
+    };
+    use regex::Regex;
+    use rocket::{
+        http::{ContentType, Header, Status},
+        local::asynchronous::Client,
+        routes,
+    };
+    use serde_json::json;
+    use std::collections::BTreeMap;
+
+    #[rocket::async_test]
+    async fn secret() {
+        let mut hooks = BTreeMap::new();
+
+        hooks.insert(
+            "test_hook".to_string(),
+            Hook {
+                command: "".to_string(),
+                signature: "X-Gitea-Signature".to_string(),
+                ip_filter: None,
+                secrets: vec!["valid".to_string()],
+                filter: FilterType::JsonFilter(JsonFilter {
+                    pointer: "*".to_string(),
+                    regex: Regex::new(".*").unwrap(),
+                }),
+            },
+        );
+
+        let config = Config {
+            metrics: None,
+            hooks: hooks,
+        };
+
+        let rocket = rocket::build()
+            .mount("/", routes![receive_hook])
+            .manage(config)
+            .manage(Metrics::default());
+
+        let client = Client::tracked(rocket).await.unwrap();
+        let response = client
+            .post("/")
+            .header(Header::new(
+                "X-Gitea-Signature",
+                "28175a0035f637f3cbb85afee9f9d319631580e7621cf790cd16ca063a2f820e",
+            ))
+            .header(ContentType::JSON)
+            .remote("127.0.0.1:8000".parse().unwrap())
+            .body(&serde_json::to_string(&json!({ "foo": "bar" })).unwrap())
+            .dispatch();
+
+        assert_eq!(response.await.status(), Status::NotFound);
+
+        let response = client
+            .post("/")
+            .header(Header::new("X-Gitea-Signature", "beef"))
+            .header(ContentType::JSON)
+            .remote("127.0.0.1:8000".parse().unwrap())
+            .body(&serde_json::to_string(&json!({ "foo": "bar" })).unwrap())
+            .dispatch();
+
+        assert_eq!(response.await.status(), Status::Unauthorized);
+
+        let response = client
+            .post("/")
+            .header(Header::new(
+                "X-Gitea-Signature",
+                "c5c315d76318362ec129ca629b50b626bba09ad3d7ba4cc0f4c0afe4a90537a0",
+            ))
+            .header(ContentType::JSON)
+            .remote("127.0.0.1:8000".parse().unwrap())
+            .body(r#"{ "not_secret": "invalid" "#)
+            .dispatch();
+
+        assert_eq!(response.await.status(), Status::BadRequest);
+
+        let response = client
+            .post("/")
+            .header(Header::new("X-Gitea-Signature", "foobar"))
+            .header(ContentType::JSON)
+            .remote("127.0.0.1:8000".parse().unwrap())
+            .dispatch();
+
+        assert_eq!(response.await.status(), Status::Unauthorized);
+    }
+
+    #[rocket::async_test]
+    async fn parse_command_request() {
+        let mut hooks = BTreeMap::new();
+
+        hooks.insert(
+            "test_hook0".to_string(),
+            Hook {
+                command:
+                    "/usr/bin/echo {{ /repository/full_name }} --foo {{ /pull_request/base/ref }}"
+                        .to_string(),
+                signature: "X-Gitea-Signature".to_string(),
+                ip_filter: None,
+                secrets: vec!["valid".to_string()],
+                filter: FilterType::JsonFilter(JsonFilter {
+                    pointer: "/foo".to_string(),
+                    regex: Regex::new("bar").unwrap(),
+                }),
+            },
+        );
+
+        hooks.insert(
+            "test_hook2".to_string(),
+            Hook {
+                command: "/usr/bin/echo {{ /repository/full_name }} {{ /pull_request/base/ref }}"
+                    .to_string(),
+                signature: "X-Gitea-Signature".to_string(),
+                ip_filter: None,
+                secrets: vec!["valid".to_string()],
+                filter: FilterType::JsonFilter(JsonFilter {
+                    pointer: "/foo".to_string(),
+                    regex: Regex::new("bar").unwrap(),
+                }),
+            },
+        );
+
+        hooks.insert(
+            "test_hook3".to_string(),
+            Hook {
+                command: "/usr/bin/echo {{ /repository/full_name }} {{ /pull_request/base/ref }}"
+                    .to_string(),
+                signature: "X-Gitea-Signature".to_string(),
+                ip_filter: None,
+                secrets: vec!["valid".to_string()],
+                filter: FilterType::Not(Box::new(FilterType::JsonFilter(JsonFilter {
+                    pointer: "/foobar".to_string(),
+                    regex: Regex::new("bar").unwrap(),
+                }))),
+            },
+        );
+
+        let config = Config {
+            metrics: None,
+            hooks: hooks,
+        };
+
+        let rocket = rocket::build()
+            .mount("/", routes![receive_hook])
+            .manage(config)
+            .manage(Metrics::default());
+
+        let client = Client::tracked(rocket).await.unwrap();
+
+        let response = client
+            .post("/")
+            .header(Header::new(
+                "X-Gitea-Signature",
+                "693b733871ecb684651a813c82936df683c9e4a816581f385353e06170545400",
+            ))
+            .header(ContentType::JSON)
+            .remote("127.0.0.1:8000".parse().unwrap())
+            .body(
+                &serde_json::to_string(&json!({
+                    "foo": "bar",
+                    "repository": {
+                        "full_name": "keith"
+                    },
+                    "pull_request": {
+                        "base": {
+                            "ref": "main"
+                        }
+                    }
+                }))
+                .unwrap(),
+            )
+            .dispatch();
+
+        assert_eq!(response.await.status(), Status::Ok);
+    }
+
+    #[rocket::async_test]
+    async fn parse_invalid_command_request() {
+        let mut hooks = BTreeMap::new();
+
+        hooks.insert(
+            "test_hook".to_string(),
+            Hook {
+                command: "/usr/bin/echo {{ /repository/full }} {{ /pull_request/base/ref }}"
+                    .to_string(),
+                signature: "X-Gitea-Signature".to_string(),
+                ip_filter: None,
+                secrets: vec!["valid".to_string()],
+                filter: FilterType::JsonFilter(JsonFilter {
+                    pointer: "/foo".to_string(),
+                    regex: Regex::new("bar").unwrap(),
+                }),
+            },
+        );
+
+        let config = Config {
+            metrics: None,
+            hooks: hooks,
+        };
+
+        let rocket = rocket::build()
+            .mount("/", routes![receive_hook])
+            .manage(config)
+            .manage(Metrics::default());
+
+        let client = Client::tracked(rocket).await.unwrap();
+
+        let response = client
+            .post("/")
+            .header(Header::new(
+                "X-Gitea-Signature",
+                "693b733871ecb684651a813c82936df683c9e4a816581f385353e06170545400",
+            ))
+            .header(ContentType::JSON)
+            .remote("127.0.0.1:8000".parse().unwrap())
+            .body(
+                &serde_json::to_string(&json!({
+                    "foo": "bar",
+                    "repository": {
+                        "full_name": "keith"
+                    },
+                    "pull_request": {
+                        "base": {
+                            "ref": "main"
+                        }
+                    }
+                }))
+                .unwrap(),
+            )
+            .dispatch();
+
+        assert_eq!(response.await.status(), Status::NotFound);
+    }
+
+    #[test]
+    fn parse_command() {
+        let mut headers = HeaderMap::new();
+        headers.add_raw("X-Gitea-Event", "something");
+
+        assert_eq!(
+            Hook::replace_parameters("command", &headers, &serde_json::Value::Null).unwrap(),
+            "command"
+        );
+
+        assert_eq!(
+            Hook::replace_parameters("    command", &headers, &serde_json::Value::Null).unwrap(),
+            "    command"
+        );
+
+        assert_eq!(
+            Hook::replace_parameters("command    ", &headers, &serde_json::Value::Null).unwrap(),
+            "command    "
+        );
+
+        assert_eq!(
+            Hook::replace_parameters("    command    ", &headers, &serde_json::Value::Null)
+                .unwrap(),
+            "    command    "
+        );
+
+        assert_eq!(
+            Hook::replace_parameters("command command    ", &headers, &serde_json::Value::Null)
+                .unwrap(),
+            "command command    "
+        );
+
+        assert_eq!(
+            Hook::replace_parameters("{{ /foo }}   command", &headers, &json!({ "foo": "bar" }))
+                .unwrap(),
+            "bar   command"
+        );
+
+        assert_eq!(
+            Hook::replace_parameters(
+                "   command  {{ /foo }}  ",
+                &headers,
+                &json!({ "foo": "bar" })
+            )
+            .unwrap(),
+            "   command  bar  "
+        );
+
+        assert_eq!(
+            Hook::replace_parameters(
+                "{{ /foo }}   command{{/field1/foo}}",
+                &headers,
+                &json!({ "foo": "bar", "field1": { "foo": "baz" } })
+            )
+            .unwrap(),
+            "bar   commandbaz"
+        );
+
+        assert_eq!(
+            Hook::replace_parameters(
+                "   command  {{ /foo }}  ",
+                &headers,
+                &json!({ "foo": "bar" })
+            )
+            .unwrap(),
+            "   command  bar  "
+        );
+
+        assert_eq!(
+            Hook::replace_parameters(
+                "   {{ /field1/foo }}   command",
+                &headers,
+                &json!({ "field1": { "foo": "bar" } })
+            )
+            .unwrap(),
+            "   bar   command"
+        );
+
+        assert_eq!(
+            Hook::replace_parameters(
+                "   {{ header X-Gitea-Event }}   command",
+                &headers,
+                &json!({ "field1": { "foo": "bar" } })
+            )
+            .unwrap(),
+            "   something   command"
+        );
+
+        assert_eq!(
+            Hook::replace_parameters(
+                "   {{ header X-Gitea-Event }} {{ /field1/foo }}  command",
+                &headers,
+                &json!({ "field1": { "foo": "bar" } })
+            )
+            .unwrap(),
+            "   something bar  command"
+        );
+
+        assert_eq!(
+            Hook::replace_parameters(
+                "   {{ header X-Gitea-Event }} {{ /field1/foo }} {{ /field1/bar }} {{ /field2/foo }} --command{{ /cmd }}",
+                &headers,
+                &json!({ "field1": { "foo": "bar", "bar": "baz" }, "field2": { "foo": "qux" }, "cmd": " else"})
+            )
+            .unwrap(),
+            "   something bar baz qux --command else"
+        );
+    }
+
+    #[test]
+    fn parse_config() {
+        let config: Config = serde_yaml::from_str(
+            r#"---
+hooks:
+  hook1:
+    command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf
+    signature: X-Gitea-Signature
+    ip_filter:
+      allow:
+        - 127.0.0.1/31
+    secrets:
+      - secret_key_01
+      - secret_key_02
+    filter:
+      json:
+        pointer: /ref
+        regex: refs/heads/master
+  hook2:
+    command: /usr/bin/local/script_xy.sh asdfasdf
+    signature: X-Gitea-Signature
+    secrets:
+      - secret_key_01
+      - secret_key_02
+    filter:
+      and:
+        - json:
+            pointer: /ref
+            regex: refs/heads/master
+        - header:
+            field: X-Gitea-Signature
+            regex: f6e5fe4fe37df76629112d55cc210718b6a55e7e"#,
+        )
+        .unwrap();
+
+        assert_eq!(
+            serde_yaml::to_string(&config).unwrap(),
+            serde_yaml::to_string(&Config {
+                metrics: None,
+                hooks: BTreeMap::from([
+                    (
+                        "hook1".to_string(),
+                        Hook {
+                            command: "/usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf"
+                                .to_string(),
+                            signature: "X-Gitea-Signature".to_string(),
+                            ip_filter: Some(IpFilter::Allow(vec![AddrType::IpNet(
+                                "127.0.0.1/31".parse().unwrap()
+                            )])),
+                            secrets: vec!["secret_key_01".to_string(), "secret_key_02".to_string()],
+                            filter: FilterType::JsonFilter(JsonFilter {
+                                pointer: "/ref".to_string(),
+                                regex: Regex::new("refs/heads/master").unwrap(),
+                            }),
+                        }
+                    ),
+                    (
+                        "hook2".to_string(),
+                        Hook {
+                            command: "/usr/bin/local/script_xy.sh asdfasdf".to_string(),
+                            signature: "X-Gitea-Signature".to_string(),
+                            ip_filter: None,
+                            secrets: vec!["secret_key_01".to_string(), "secret_key_02".to_string()],
+                            filter: FilterType::And(vec![
+                                FilterType::JsonFilter(JsonFilter {
+                                    pointer: "/ref".to_string(),
+                                    regex: Regex::new("refs/heads/master").unwrap(),
+                                }),
+                                FilterType::HeaderFilter(HeaderFilter {
+                                    field: "X-Gitea-Signature".to_string(),
+                                    regex: Regex::new("f6e5fe4fe37df76629112d55cc210718b6a55e7e")
+                                        .unwrap(),
+                                }),
+                            ]),
+                        }
+                    )
+                ])
+            })
+            .unwrap()
+        );
+
+        let config: Config = serde_yaml::from_str(
+            r#"---
+metrics:
+  enabled: true
+hooks:
+  hook1:
+    command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf
+    signature: X-Gitea-Signature
+    ip_filter:
+      allow:
+        - 127.0.0.1/31
+    secrets:
+      - secret_key_01
+      - secret_key_02
+    filter:
+      json:
+        pointer: /ref
+        regex: refs/heads/master"#,
+        )
+        .unwrap();
+
+        assert_eq!(
+            serde_yaml::to_string(&config).unwrap(),
+            serde_yaml::to_string(&Config {
+                metrics: Some(MetricsConfig {
+                    enabled: true,
+                    ip_filter: None
+                }),
+                hooks: BTreeMap::from([(
+                    "hook1".to_string(),
+                    Hook {
+                        command: "/usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf"
+                            .to_string(),
+                        signature: "X-Gitea-Signature".to_string(),
+                        ip_filter: Some(IpFilter::Allow(vec![AddrType::IpNet(
+                            "127.0.0.1/31".parse().unwrap()
+                        )])),
+                        secrets: vec!["secret_key_01".to_string(), "secret_key_02".to_string()],
+                        filter: FilterType::JsonFilter(JsonFilter {
+                            pointer: "/ref".to_string(),
+                            regex: Regex::new("refs/heads/master").unwrap(),
+                        }),
+                    }
+                ),])
+            })
+            .unwrap()
+        );
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index 7b161c4..a18261b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,454 +1,43 @@
-use anyhow::{anyhow, bail, Result};
-use clap::Parser;
-use hmac::{Hmac, Mac, NewMac};
-use log::{debug, error, info, trace, warn};
-use rocket::{
-    data::{FromData, ToByteUnit},
-    futures::TryFutureExt,
-    get,
-    http::{HeaderMap, Status},
-    outcome::Outcome::{self, Failure, Success},
-    post, routes,
-    tokio::io::AsyncReadExt,
-    Data, Request, State,
-};
-use run_script::ScriptOptions;
-use serde::{Deserialize, Serialize};
-use sha2::Sha256;
-
-use std::{
-    collections::BTreeMap,
-    fs::File,
-    io::BufReader,
-    net::{IpAddr, Ipv4Addr, SocketAddr},
-    sync::atomic::{AtomicUsize, Ordering},
-};
-
 mod cli;
-mod webhooks;
+mod config;
+mod filters;
+mod hooks;
+mod metrics;
 
-use crate::{
-    cli::Opts,
-    webhooks::{FilterType, IpFilter, WebhookeyError},
-};
+use crate::{cli::Opts, config::Config, metrics::Metrics};
+use anyhow::Result;
+use clap::Parser;
+use log::{debug, error, trace};
+use rocket::routes;
+use std::{fs::File, io::BufReader, net::IpAddr};
+use thiserror::Error;
 
-#[derive(Debug, Default)]
-struct WebhookeyMetrics {
-    requests_received: AtomicUsize,
-    requests_invalid: AtomicUsize,
-    hooks_successful: AtomicUsize,
-    hooks_forbidden: AtomicUsize,
-    hooks_unmatched: AtomicUsize,
-    commands_executed: AtomicUsize,
-    commands_execution_failed: AtomicUsize,
-    commands_successful: AtomicUsize,
-    commands_failed: AtomicUsize,
+#[derive(Debug, Error)]
+pub enum WebhookeyError {
+    #[error("Could not extract signature from header")]
+    InvalidSignature,
+    #[error("Unauthorized request from `{0}`")]
+    Unauthorized(IpAddr),
+    #[error("Unmatched hook from `{0}`")]
+    UnmatchedHook(IpAddr),
+    #[error("Could not evaluate filter request")]
+    InvalidFilter,
+    #[error("IO Error")]
+    Io(#[from] std::io::Error),
+    #[error("Serde Error")]
+    Serde(#[from] serde_json::Error),
 }
 
-#[derive(Debug, Deserialize, Serialize)]
-#[serde(deny_unknown_fields)]
-struct MetricsConfig {
-    enabled: bool,
-    ip_filter: Option<IpFilter>,
-}
-
-#[derive(Debug, Deserialize, Serialize)]
-#[serde(deny_unknown_fields)]
-struct Config {
-    metrics: Option<MetricsConfig>,
-    hooks: BTreeMap<String, Hook>,
-}
-
-#[derive(Debug, Deserialize, Serialize)]
-#[serde(deny_unknown_fields)]
-struct Hook {
-    command: String,
-    signature: String,
-    ip_filter: Option<IpFilter>,
-    secrets: Vec<String>,
-    filter: FilterType,
-}
-
-impl Hook {
-    fn get_command(
-        &self,
-        hook_name: &str,
-        request: &Request,
-        data: &mut serde_json::Value,
-    ) -> Result<String> {
-        debug!("Replacing parameters for command of  hook `{}`", hook_name);
-
-        Hook::replace_parameters(&self.command, request.headers(), data)
-    }
-
-    fn replace_parameters(
-        input: &str,
-        headers: &HeaderMap,
-        data: &serde_json::Value,
-    ) -> Result<String> {
-        let mut command = String::new();
-        let command_template = &mut input.chars();
-
-        while let Some(i) = command_template.next() {
-            if i == '{' {
-                if let Some('{') = command_template.next() {
-                    let mut token = String::new();
-
-                    while let Some(i) = command_template.next() {
-                        if i == '}' {
-                            if let Some('}') = command_template.next() {
-                                let expr = token.trim().split(' ').collect::<Vec<&str>>();
-
-                                let replaced = match expr.get(0) {
-                                    Some(&"header") => get_header_field(
-                                        headers,
-                                        expr.get(1).ok_or_else(|| {
-                                            anyhow!("Missing parameter for `header` expression")
-                                        })?,
-                                    )?
-                                    .to_string(),
-                                    Some(pointer) => webhooks::get_string(
-                                        data.pointer(pointer).ok_or_else(|| {
-                                            anyhow!(
-                                                "Could not find field refered to in parameter `{}`",
-                                                pointer
-                                            )
-                                        })?,
-                                    )?,
-                                    None => bail!("Missing expression in variable `{}`", token),
-                                };
-
-                                command.push_str(&replaced);
-
-                                trace!("Replace `{}` with: {}", token, replaced);
-
-                                break;
-                            } else {
-                                command.push('}');
-                                command.push(i);
-                            }
-                        } else {
-                            token.push(i);
-                        }
-                    }
-                } else {
-                    command.push('{');
-                    command.push(i);
-                }
-            } else {
-                command.push(i);
-            }
-        }
-
-        Ok(command)
-    }
-}
-
-#[derive(Debug)]
-struct Hooks {
-    inner: BTreeMap<String, String>,
-}
-
-impl Hooks {
-    async fn get_commands(request: &Request<'_>, data: Data<'_>) -> Result<Self, WebhookeyError> {
-        let mut buffer = Vec::new();
-        let size = data
-            .open(256_i32.kilobytes())
-            .read_to_end(&mut buffer)
-            .map_err(WebhookeyError::Io)
-            .await?;
-        info!("Data of size {} received", size);
-
-        let config = request.guard::<&State<Config>>().await.unwrap(); // should never fail
-        let mut valid = false;
-        let mut result = BTreeMap::new();
-        let client_ip = &request
-            .client_ip()
-            .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
-
-        let hooks = config.hooks.iter().filter(|(name, hook)| {
-            if let Some(ip) = &hook.ip_filter {
-                accept_ip(name, client_ip, ip)
-            } else {
-                info!(
-                    "Allow hook `{}` from {}, no IP filter was configured",
-                    &name, &client_ip
-                );
-                true
-            }
-        });
-
-        for (hook_name, hook) in hooks {
-            let signature = request
-                .headers()
-                .get_one(&hook.signature)
-                .ok_or(WebhookeyError::InvalidSignature)?;
-
-            let secrets = hook
-                .secrets
-                .iter()
-                .map(|secret| validate_request(secret, signature, &buffer));
-
-            for secret in secrets {
-                match secret {
-                    Ok(()) => {
-                        trace!("Valid signature found for hook `{}`", hook_name);
-
-                        valid = true;
-
-                        let mut data: serde_json::Value =
-                            serde_json::from_slice(&buffer).map_err(WebhookeyError::Serde)?;
-
-                        match hook.filter.evaluate(&data) {
-                            Ok(true) => match hook.get_command(hook_name, request, &mut data) {
-                                Ok(command) => {
-                                    info!("Filter for `{}` matched", &hook_name);
-                                    result.insert(hook_name.to_string(), command);
-                                    break;
-                                }
-                                Err(e) => error!("{}", e),
-                            },
-                            Ok(false) => info!("Filter for `{}` did not match", &hook_name),
-                            Err(error) => {
-                                error!("Could not match filter for `{}`: {}", &hook_name, error)
-                            }
-                        }
-                    }
-                    Err(e) => trace!("Hook `{}` could not validate request: {}", &hook_name, e),
-                }
-            }
-        }
-
-        if !valid {
-            return Err(WebhookeyError::Unauthorized(*client_ip));
-        }
-
-        Ok(Hooks { inner: result })
-    }
-}
-
-fn accept_ip(hook_name: &str, client_ip: &IpAddr, ip: &IpFilter) -> bool {
-    if ip.validate(client_ip) {
-        info!("Allow hook `{}` from {}", &hook_name, &client_ip);
-        return true;
-    }
-
-    warn!("Deny hook `{}` from {}", &hook_name, &client_ip);
-    false
-}
-
-fn validate_request(secret: &str, signature: &str, data: &[u8]) -> Result<()> {
-    let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
-        .map_err(|e| anyhow!("Could not create hasher with secret: {}", e))?;
-    mac.update(data);
-    let raw_signature = hex::decode(signature.as_bytes())?;
-    mac.verify(&raw_signature).map_err(|e| anyhow!("{}", e))
-}
-
-fn get_header_field<'a>(headers: &'a HeaderMap, param: &str) -> Result<&'a str> {
-    headers
-        .get_one(param)
-        .ok_or_else(|| anyhow!("Could not extract event parameter from header"))
-}
-
-fn get_config() -> Result<File> {
-    // Look for config in CWD..
-    if let Ok(config) = File::open("config.yml") {
-        info!("Loading configuration from `./config.yml`");
-
-        return Ok(config);
-    }
-
-    // ..look for user path config..
-    if let Some(mut path) = dirs::config_dir() {
-        path.push("webhookey/config.yml");
-
-        if let Ok(config) = File::open(&path) {
-            info!(
-                "Loading configuration from `{}`",
-                path.to_str().unwrap_or("<path unprintable>"),
-            );
-
-            return Ok(config);
+pub fn get_string(data: &serde_json::Value) -> Result<String, WebhookeyError> {
+    match &data {
+        serde_json::Value::Bool(bool) => Ok(bool.to_string()),
+        serde_json::Value::Number(number) => Ok(number.to_string()),
+        serde_json::Value::String(string) => Ok(string.as_str().to_string()),
+        x => {
+            error!("Could not get string from: {:?}", x);
+            unimplemented!()
         }
     }
-
-    // ..look for systemwide config..
-    if let Ok(config) = File::open("/etc/webhookey/config.yml") {
-        info!("Loading configuration from `/etc/webhookey/config.yml`");
-
-        return Ok(config);
-    }
-
-    // ..you had your chance.
-    bail!("No configuration file found.");
-}
-
-fn get_metrics(metrics: &WebhookeyMetrics) -> String {
-    format!(
-        r"# HELP webhookey_requests_received Number of requests received
-# TYPE webhookey_requests_received gauge
-webhookey_requests_received {}
-# HELP webhookey_requests_invalid Number of invalid requests received
-# TYPE webhookey_requests_invalid gauge
-webhookey_requests_invalid {}
-# HELP webhookey_hooks_successful Number of successfully executed hooks
-# TYPE webhookey_hooks_successful gauge
-webhookey_hooks_sucessful {}
-# HELP webhookey_hooks_forbidden Number of forbidden requests
-# TYPE webhookey_hooks_forbidden gauge
-webhookey_hooks_forbidden {}
-# HELP webhookey_hooks_unmatched Number of unmatched requests
-# TYPE webhookey_hooks_unmatched gauge
-webhookey_hooks_unmatched {}
-# HELP webhookey_commands_executed Number of commands executed
-# TYPE webhookey_commands_executed gauge
-webhookey_commands_executed {}
-# HELP webhookey_commands_execution_failed Number of commands failed to execute
-# TYPE webhookey_commands_execution_failed gauge
-webhookey_commands_execution_failed {}
-# HELP webhookey_commands_successful Number of executed commands returning return code 0
-# TYPE webhookey_commands_successful gauge
-webhookey_commands_successful {}
-# HELP webhookey_commands_failed Number of executed commands returning different return code than 0
-# TYPE webhookey_commands_failed gauge
-webhookey_commands_failed {}
-",
-        metrics.requests_received.load(Ordering::Relaxed),
-        metrics.requests_invalid.load(Ordering::Relaxed),
-        metrics.hooks_successful.load(Ordering::Relaxed),
-        metrics.hooks_forbidden.load(Ordering::Relaxed),
-        metrics.hooks_unmatched.load(Ordering::Relaxed),
-        metrics.commands_executed.load(Ordering::Relaxed),
-        metrics.commands_execution_failed.load(Ordering::Relaxed),
-        metrics.commands_successful.load(Ordering::Relaxed),
-        metrics.commands_failed.load(Ordering::Relaxed),
-    )
-}
-
-#[rocket::async_trait]
-impl<'r> FromData<'r> for Hooks {
-    type Error = WebhookeyError;
-
-    async fn from_data(
-        request: &'r Request<'_>,
-        data: Data<'r>,
-    ) -> Outcome<Self, (Status, Self::Error), Data<'r>> {
-        {
-            request
-                .guard::<&State<WebhookeyMetrics>>()
-                .await
-                .unwrap() // TODO: Check if unwrap need to be fixed
-                .requests_received
-                .fetch_add(1, Ordering::Relaxed);
-        }
-
-        match Hooks::get_commands(request, data).await {
-            Ok(hooks) => {
-                if hooks.inner.is_empty() {
-                    let client_ip = &request
-                        .client_ip()
-                        .unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED));
-
-                    request
-                        .guard::<&State<WebhookeyMetrics>>()
-                        .await
-                        .unwrap() // TODO: Check if unwrap need to be fixed
-                        .hooks_unmatched
-                        .fetch_add(1, Ordering::Relaxed);
-
-                    warn!("Unmatched hook from {}", &client_ip);
-                    return Failure((Status::NotFound, WebhookeyError::UnmatchedHook(*client_ip)));
-                }
-
-                Success(hooks)
-            }
-            Err(WebhookeyError::Unauthorized(e)) => {
-                error!("{}", WebhookeyError::Unauthorized(e));
-
-                request
-                    .guard::<&State<WebhookeyMetrics>>()
-                    .await
-                    .unwrap() // TODO: Check if unwrap need to be fixed
-                    .hooks_forbidden
-                    .fetch_add(1, Ordering::Relaxed);
-
-                Failure((Status::Unauthorized, WebhookeyError::Unauthorized(e)))
-            }
-            Err(e) => {
-                error!("{}", e);
-
-                request
-                    .guard::<&State<WebhookeyMetrics>>()
-                    .await
-                    .unwrap() // TODO: Check if unwrap need to be fixed
-                    .requests_invalid
-                    .fetch_add(1, Ordering::Relaxed);
-
-                Failure((Status::BadRequest, e))
-            }
-        }
-    }
-}
-
-#[post("/", format = "json", data = "<hooks>")]
-async fn receive_hook<'a>(
-    address: SocketAddr,
-    hooks: Hooks,
-    metrics: &State<WebhookeyMetrics>,
-) -> Status {
-    info!("Post request received from: {}", address);
-
-    hooks.inner.iter().for_each(|(name, command)| {
-        info!("Execute `{}` from hook `{}`", &command, &name);
-
-        match run_script::run(command, &vec![], &ScriptOptions::new()) {
-            Ok((status, stdout, stderr)) => {
-                info!("Command `{}` exited with return code: {}", &command, status);
-                trace!("Output of command `{}` on stdout: {:?}", &command, &stdout);
-                debug!("Output of command `{}` on stderr: {:?}", &command, &stderr);
-
-                metrics.commands_executed.fetch_add(1, Ordering::Relaxed);
-
-                let _ = match status {
-                    0 => metrics.commands_successful.fetch_add(1, Ordering::Relaxed),
-                    _ => metrics.commands_failed.fetch_add(1, Ordering::Relaxed),
-                };
-            }
-            Err(e) => {
-                error!("Execution of `{}` failed: {}", &command, e);
-
-                metrics
-                    .commands_execution_failed
-                    .fetch_add(1, Ordering::Relaxed);
-            }
-        }
-    });
-
-    Status::Ok
-}
-
-#[get("/metrics")]
-async fn metrics(
-    address: SocketAddr,
-    metrics: &State<WebhookeyMetrics>,
-    config: &State<Config>,
-) -> Option<String> {
-    if let Some(metrics_config) = &config.metrics {
-        if metrics_config.enabled {
-            if let Some(filter) = &metrics_config.ip_filter {
-                if filter.validate(&address.ip()) {
-                    return Some(get_metrics(metrics));
-                }
-            } else {
-                return Some(get_metrics(metrics));
-            }
-        }
-    }
-
-    warn!("Forbidden request for metrics: {:?}", address);
-
-    None
 }
 
 #[rocket::main]
@@ -459,7 +48,7 @@ async fn main() -> Result<()> {
 
     let config: Config = match cli.config {
         Some(config) => serde_yaml::from_reader(BufReader::new(File::open(config)?))?,
-        _ => serde_yaml::from_reader(BufReader::new(get_config()?))?,
+        _ => serde_yaml::from_reader(BufReader::new(config::get_config()?))?,
     };
 
     trace!("Parsed configuration:\n{}", serde_yaml::to_string(&config)?);
@@ -471,467 +60,11 @@ async fn main() -> Result<()> {
     }
 
     rocket::build()
-        .mount("/", routes![receive_hook, metrics])
+        .mount("/", routes![hooks::receive_hook, metrics::metrics])
         .manage(config)
-        .manage(WebhookeyMetrics::default())
+        .manage(Metrics::default())
         .launch()
         .await?;
 
     Ok(())
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::webhooks::{AddrType, JsonFilter};
-    use rocket::{
-        http::{ContentType, Header},
-        local::asynchronous::Client,
-    };
-    use serde_json::json;
-
-    #[rocket::async_test]
-    async fn secret() {
-        let mut hooks = BTreeMap::new();
-
-        hooks.insert(
-            "test_hook".to_string(),
-            Hook {
-                command: "".to_string(),
-                signature: "X-Gitea-Signature".to_string(),
-                ip_filter: None,
-                secrets: vec!["valid".to_string()],
-                filter: FilterType::JsonFilter(JsonFilter {
-                    pointer: "*".to_string(),
-                    regex: "*".to_string(),
-                }),
-            },
-        );
-
-        let config = Config {
-            metrics: None,
-            hooks: hooks,
-        };
-
-        let rocket = rocket::build()
-            .mount("/", routes![receive_hook])
-            .manage(config)
-            .manage(WebhookeyMetrics::default());
-
-        let client = Client::tracked(rocket).await.unwrap();
-        let response = client
-            .post("/")
-            .header(Header::new(
-                "X-Gitea-Signature",
-                "28175a0035f637f3cbb85afee9f9d319631580e7621cf790cd16ca063a2f820e",
-            ))
-            .header(ContentType::JSON)
-            .remote("127.0.0.1:8000".parse().unwrap())
-            .body(&serde_json::to_string(&json!({ "foo": "bar" })).unwrap())
-            .dispatch();
-
-        assert_eq!(response.await.status(), Status::NotFound);
-
-        let response = client
-            .post("/")
-            .header(Header::new("X-Gitea-Signature", "beef"))
-            .header(ContentType::JSON)
-            .remote("127.0.0.1:8000".parse().unwrap())
-            .body(&serde_json::to_string(&json!({ "foo": "bar" })).unwrap())
-            .dispatch();
-
-        assert_eq!(response.await.status(), Status::Unauthorized);
-
-        let response = client
-            .post("/")
-            .header(Header::new(
-                "X-Gitea-Signature",
-                "c5c315d76318362ec129ca629b50b626bba09ad3d7ba4cc0f4c0afe4a90537a0",
-            ))
-            .header(ContentType::JSON)
-            .remote("127.0.0.1:8000".parse().unwrap())
-            .body(r#"{ "not_secret": "invalid" "#)
-            .dispatch();
-
-        assert_eq!(response.await.status(), Status::BadRequest);
-
-        let response = client
-            .post("/")
-            .header(Header::new("X-Gitea-Signature", "foobar"))
-            .header(ContentType::JSON)
-            .remote("127.0.0.1:8000".parse().unwrap())
-            .dispatch();
-
-        assert_eq!(response.await.status(), Status::Unauthorized);
-    }
-
-    #[test]
-    fn parse_command() {
-        let mut headers = HeaderMap::new();
-        headers.add_raw("X-Gitea-Event", "something");
-
-        assert_eq!(
-            Hook::replace_parameters("command", &headers, &serde_json::Value::Null).unwrap(),
-            "command"
-        );
-
-        assert_eq!(
-            Hook::replace_parameters("    command", &headers, &serde_json::Value::Null).unwrap(),
-            "    command"
-        );
-
-        assert_eq!(
-            Hook::replace_parameters("command    ", &headers, &serde_json::Value::Null).unwrap(),
-            "command    "
-        );
-
-        assert_eq!(
-            Hook::replace_parameters("    command    ", &headers, &serde_json::Value::Null)
-                .unwrap(),
-            "    command    "
-        );
-
-        assert_eq!(
-            Hook::replace_parameters("command command    ", &headers, &serde_json::Value::Null)
-                .unwrap(),
-            "command command    "
-        );
-
-        assert_eq!(
-            Hook::replace_parameters("{{ /foo }}   command", &headers, &json!({ "foo": "bar" }))
-                .unwrap(),
-            "bar   command"
-        );
-
-        assert_eq!(
-            Hook::replace_parameters(
-                "   command  {{ /foo }}  ",
-                &headers,
-                &json!({ "foo": "bar" })
-            )
-            .unwrap(),
-            "   command  bar  "
-        );
-
-        assert_eq!(
-            Hook::replace_parameters(
-                "{{ /foo }}   command{{/field1/foo}}",
-                &headers,
-                &json!({ "foo": "bar", "field1": { "foo": "baz" } })
-            )
-            .unwrap(),
-            "bar   commandbaz"
-        );
-
-        assert_eq!(
-            Hook::replace_parameters(
-                "   command  {{ /foo }}  ",
-                &headers,
-                &json!({ "foo": "bar" })
-            )
-            .unwrap(),
-            "   command  bar  "
-        );
-
-        assert_eq!(
-            Hook::replace_parameters(
-                "   {{ /field1/foo }}   command",
-                &headers,
-                &json!({ "field1": { "foo": "bar" } })
-            )
-            .unwrap(),
-            "   bar   command"
-        );
-
-        assert_eq!(
-            Hook::replace_parameters(
-                "   {{ header X-Gitea-Event }}   command",
-                &headers,
-                &json!({ "field1": { "foo": "bar" } })
-            )
-            .unwrap(),
-            "   something   command"
-        );
-
-        assert_eq!(
-            Hook::replace_parameters(
-                "   {{ header X-Gitea-Event }} {{ /field1/foo }}  command",
-                &headers,
-                &json!({ "field1": { "foo": "bar" } })
-            )
-            .unwrap(),
-            "   something bar  command"
-        );
-
-        assert_eq!(
-            Hook::replace_parameters(
-                "   {{ header X-Gitea-Event }} {{ /field1/foo }} {{ /field1/bar }} {{ /field2/foo }} --command{{ /cmd }}",
-                &headers,
-                &json!({ "field1": { "foo": "bar", "bar": "baz" }, "field2": { "foo": "qux" }, "cmd": " else"})
-            )
-            .unwrap(),
-            "   something bar baz qux --command else"
-        );
-    }
-
-    #[rocket::async_test]
-    async fn parse_command_request() {
-        let mut hooks = BTreeMap::new();
-
-        hooks.insert(
-            "test_hook".to_string(),
-            Hook {
-                command:
-                    "/usr/bin/echo {{ /repository/full_name }} --foo {{ /pull_request/base/ref }}"
-                        .to_string(),
-                signature: "X-Gitea-Signature".to_string(),
-                ip_filter: None,
-                secrets: vec!["valid".to_string()],
-                filter: FilterType::JsonFilter(JsonFilter {
-                    pointer: "/foo".to_string(),
-                    regex: "bar".to_string(),
-                }),
-            },
-        );
-
-        hooks.insert(
-            "test_hook".to_string(),
-            Hook {
-                command: "/usr/bin/echo {{ /repository/full_name }} {{ /pull_request/base/ref }}"
-                    .to_string(),
-                signature: "X-Gitea-Signature".to_string(),
-                ip_filter: None,
-                secrets: vec!["valid".to_string()],
-                filter: FilterType::JsonFilter(JsonFilter {
-                    pointer: "/foo".to_string(),
-                    regex: "bar".to_string(),
-                }),
-            },
-        );
-
-        let config = Config {
-            metrics: None,
-            hooks: hooks,
-        };
-
-        let rocket = rocket::build()
-            .mount("/", routes![receive_hook])
-            .manage(config)
-            .manage(WebhookeyMetrics::default());
-
-        let client = Client::tracked(rocket).await.unwrap();
-
-        let response = client
-            .post("/")
-            .header(Header::new(
-                "X-Gitea-Signature",
-                "693b733871ecb684651a813c82936df683c9e4a816581f385353e06170545400",
-            ))
-            .header(ContentType::JSON)
-            .remote("127.0.0.1:8000".parse().unwrap())
-            .body(
-                &serde_json::to_string(&json!({
-                    "foo": "bar",
-                    "repository": {
-                        "full_name": "keith"
-                    },
-                    "pull_request": {
-                        "base": {
-                            "ref": "main"
-                        }
-                    }
-                }))
-                .unwrap(),
-            )
-            .dispatch();
-
-        assert_eq!(response.await.status(), Status::Ok);
-    }
-
-    #[rocket::async_test]
-    async fn parse_invalid_command_request() {
-        let mut hooks = BTreeMap::new();
-
-        hooks.insert(
-            "test_hook".to_string(),
-            Hook {
-                command: "/usr/bin/echo {{ /repository/full }} {{ /pull_request/base/ref }}"
-                    .to_string(),
-                signature: "X-Gitea-Signature".to_string(),
-                ip_filter: None,
-                secrets: vec!["valid".to_string()],
-                filter: FilterType::JsonFilter(JsonFilter {
-                    pointer: "/foo".to_string(),
-                    regex: "bar".to_string(),
-                }),
-            },
-        );
-
-        let config = Config {
-            metrics: None,
-            hooks: hooks,
-        };
-
-        let rocket = rocket::build()
-            .mount("/", routes![receive_hook])
-            .manage(config)
-            .manage(WebhookeyMetrics::default());
-
-        let client = Client::tracked(rocket).await.unwrap();
-
-        let response = client
-            .post("/")
-            .header(Header::new(
-                "X-Gitea-Signature",
-                "693b733871ecb684651a813c82936df683c9e4a816581f385353e06170545400",
-            ))
-            .header(ContentType::JSON)
-            .remote("127.0.0.1:8000".parse().unwrap())
-            .body(
-                &serde_json::to_string(&json!({
-                    "foo": "bar",
-                    "repository": {
-                        "full_name": "keith"
-                    },
-                    "pull_request": {
-                        "base": {
-                            "ref": "main"
-                        }
-                    }
-                }))
-                .unwrap(),
-            )
-            .dispatch();
-
-        assert_eq!(response.await.status(), Status::NotFound);
-    }
-
-    #[test]
-    fn parse_config() {
-        let config: Config = serde_yaml::from_str(
-            r#"---
-hooks:
-  hook1:
-    command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf
-    signature: X-Gitea-Signature
-    ip_filter:
-      allow:
-        - 127.0.0.1/31
-    secrets:
-      - secret_key_01
-      - secret_key_02
-    filter:
-      json:
-        pointer: /ref
-        regex: refs/heads/master
-  hook2:
-    command: /usr/bin/local/script_xy.sh asdfasdf
-    signature: X-Gitea-Signature
-    secrets:
-      - secret_key_01
-      - secret_key_02
-    filter:
-      and:
-        - json:
-            pointer: /ref
-            regex: refs/heads/master
-        - json:
-            pointer: /after
-            regex: f6e5fe4fe37df76629112d55cc210718b6a55e7e"#,
-        )
-        .unwrap();
-
-        assert_eq!(
-            serde_yaml::to_string(&config).unwrap(),
-            serde_yaml::to_string(&Config {
-                metrics: None,
-                hooks: BTreeMap::from([
-                    (
-                        "hook1".to_string(),
-                        Hook {
-                            command: "/usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf"
-                                .to_string(),
-                            signature: "X-Gitea-Signature".to_string(),
-                            ip_filter: Some(IpFilter::Allow(vec![AddrType::IpNet(
-                                "127.0.0.1/31".parse().unwrap()
-                            )])),
-                            secrets: vec!["secret_key_01".to_string(), "secret_key_02".to_string()],
-                            filter: FilterType::JsonFilter(JsonFilter {
-                                pointer: "/ref".to_string(),
-                                regex: "refs/heads/master".to_string(),
-                            }),
-                        }
-                    ),
-                    (
-                        "hook2".to_string(),
-                        Hook {
-                            command: "/usr/bin/local/script_xy.sh asdfasdf".to_string(),
-                            signature: "X-Gitea-Signature".to_string(),
-                            ip_filter: None,
-                            secrets: vec!["secret_key_01".to_string(), "secret_key_02".to_string()],
-                            filter: FilterType::And(vec![
-                                FilterType::JsonFilter(JsonFilter {
-                                    pointer: "/ref".to_string(),
-                                    regex: "refs/heads/master".to_string(),
-                                }),
-                                FilterType::JsonFilter(JsonFilter {
-                                    pointer: "/after".to_string(),
-                                    regex: "f6e5fe4fe37df76629112d55cc210718b6a55e7e".to_string(),
-                                }),
-                            ]),
-                        }
-                    )
-                ])
-            })
-            .unwrap()
-        );
-
-        let config: Config = serde_yaml::from_str(
-            r#"---
-metrics:
-  enabled: true
-hooks:
-  hook1:
-    command: /usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf
-    signature: X-Gitea-Signature
-    ip_filter:
-      allow:
-        - 127.0.0.1/31
-    secrets:
-      - secret_key_01
-      - secret_key_02
-    filter:
-      json:
-        pointer: /ref
-        regex: refs/heads/master"#,
-        )
-        .unwrap();
-
-        assert_eq!(
-            serde_yaml::to_string(&config).unwrap(),
-            serde_yaml::to_string(&Config {
-                metrics: Some(MetricsConfig {
-                    enabled: true,
-                    ip_filter: None
-                }),
-                hooks: BTreeMap::from([(
-                    "hook1".to_string(),
-                    Hook {
-                        command: "/usr/bin/local/script_xy.sh {{ /field2/foo }} asdfasdf"
-                            .to_string(),
-                        signature: "X-Gitea-Signature".to_string(),
-                        ip_filter: Some(IpFilter::Allow(vec![AddrType::IpNet(
-                            "127.0.0.1/31".parse().unwrap()
-                        )])),
-                        secrets: vec!["secret_key_01".to_string(), "secret_key_02".to_string()],
-                        filter: FilterType::JsonFilter(JsonFilter {
-                            pointer: "/ref".to_string(),
-                            regex: "refs/heads/master".to_string(),
-                        }),
-                    }
-                ),])
-            })
-            .unwrap()
-        );
-    }
-}
diff --git a/src/metrics.rs b/src/metrics.rs
new file mode 100644
index 0000000..71c4fae
--- /dev/null
+++ b/src/metrics.rs
@@ -0,0 +1,91 @@
+use crate::Config;
+use log::warn;
+use rocket::{get, State};
+use std::{
+    net::SocketAddr,
+    sync::atomic::{AtomicUsize, Ordering},
+};
+
+#[derive(Debug, Default)]
+pub struct Metrics {
+    pub requests_received: AtomicUsize,
+    pub requests_invalid: AtomicUsize,
+    pub hooks_successful: AtomicUsize,
+    pub hooks_forbidden: AtomicUsize,
+    pub hooks_unmatched: AtomicUsize,
+    pub commands_executed: AtomicUsize,
+    pub commands_execution_failed: AtomicUsize,
+    pub commands_successful: AtomicUsize,
+    pub commands_failed: AtomicUsize,
+}
+
+#[get("/metrics")]
+pub async fn metrics(
+    address: SocketAddr,
+    metrics: &State<Metrics>,
+    config: &State<Config>,
+) -> Option<String> {
+    // Are metrics configured?
+    if let Some(metrics_config) = &config.metrics {
+        // Are metrics enabled?
+        if metrics_config.enabled {
+            // Is a filter configured?
+            if let Some(filter) = &metrics_config.ip_filter {
+                // Does the request match the filter?
+                if filter.validate(&address.ip()) {
+                    return Some(metrics.get_metrics());
+                }
+            } else {
+                return Some(metrics.get_metrics());
+            }
+        }
+    }
+
+    warn!("Forbidden request for metrics: {:?}", address);
+
+    None
+}
+
+impl Metrics {
+    fn get_metrics(&self) -> String {
+        format!(
+            r"# HELP webhookey_requests_received Number of requests received
+# TYPE webhookey_requests_received gauge
+webhookey_requests_received {}
+# HELP webhookey_requests_invalid Number of invalid requests received
+# TYPE webhookey_requests_invalid gauge
+webhookey_requests_invalid {}
+# HELP webhookey_hooks_successful Number of successfully executed hooks
+# TYPE webhookey_hooks_successful gauge
+webhookey_hooks_sucessful {}
+# HELP webhookey_hooks_forbidden Number of forbidden requests
+# TYPE webhookey_hooks_forbidden gauge
+webhookey_hooks_forbidden {}
+# HELP webhookey_hooks_unmatched Number of unmatched requests
+# TYPE webhookey_hooks_unmatched gauge
+webhookey_hooks_unmatched {}
+# HELP webhookey_commands_executed Number of commands executed
+# TYPE webhookey_commands_executed gauge
+webhookey_commands_executed {}
+# HELP webhookey_commands_execution_failed Number of commands failed to execute
+# TYPE webhookey_commands_execution_failed gauge
+webhookey_commands_execution_failed {}
+# HELP webhookey_commands_successful Number of executed commands returning return code 0
+# TYPE webhookey_commands_successful gauge
+webhookey_commands_successful {}
+# HELP webhookey_commands_failed Number of executed commands returning different return code than 0
+# TYPE webhookey_commands_failed gauge
+webhookey_commands_failed {}
+",
+            self.requests_received.load(Ordering::Relaxed),
+            self.requests_invalid.load(Ordering::Relaxed),
+            self.hooks_successful.load(Ordering::Relaxed),
+            self.hooks_forbidden.load(Ordering::Relaxed),
+            self.hooks_unmatched.load(Ordering::Relaxed),
+            self.commands_executed.load(Ordering::Relaxed),
+            self.commands_execution_failed.load(Ordering::Relaxed),
+            self.commands_successful.load(Ordering::Relaxed),
+            self.commands_failed.load(Ordering::Relaxed),
+        )
+    }
+}
diff --git a/src/webhooks.rs b/src/webhooks.rs
deleted file mode 100644
index efd9fb6..0000000
--- a/src/webhooks.rs
+++ /dev/null
@@ -1,163 +0,0 @@
-use anyhow::Result;
-use ipnet::IpNet;
-use log::{debug, error, trace};
-use regex::Regex;
-use serde::{Deserialize, Serialize};
-use std::net::IpAddr;
-use thiserror::Error;
-
-#[derive(Debug, Error)]
-pub enum WebhookeyError {
-    #[error("Could not extract signature from header")]
-    InvalidSignature,
-    #[error("Unauthorized request from `{0}`")]
-    Unauthorized(IpAddr),
-    #[error("Unmatched hook from `{0}`")]
-    UnmatchedHook(IpAddr),
-    #[error("Could not evaluate filter request")]
-    InvalidFilter,
-    #[error("IO Error")]
-    Io(std::io::Error),
-    #[error("Serde Error")]
-    Serde(serde_json::Error),
-    #[error("Regex Error")]
-    Regex(regex::Error),
-}
-
-#[derive(Debug, Deserialize, Serialize)]
-#[serde(deny_unknown_fields, untagged)]
-pub enum AddrType {
-    IpAddr(IpAddr),
-    IpNet(IpNet),
-}
-
-impl AddrType {
-    pub fn matches(&self, client_ip: &IpAddr) -> bool {
-        match self {
-            AddrType::IpAddr(addr) => addr == client_ip,
-            AddrType::IpNet(net) => net.contains(client_ip),
-        }
-    }
-}
-
-#[derive(Debug, Deserialize, Serialize)]
-#[serde(deny_unknown_fields, rename_all = "lowercase")]
-pub enum IpFilter {
-    Allow(Vec<AddrType>),
-    Deny(Vec<AddrType>),
-}
-
-impl IpFilter {
-    pub fn validate(&self, client_ip: &IpAddr) -> bool {
-        match self {
-            IpFilter::Allow(list) => list.iter().any(|i| i.matches(client_ip)),
-            IpFilter::Deny(list) => !list.iter().any(|i| i.matches(client_ip)),
-        }
-    }
-}
-
-#[derive(Debug, Deserialize, Serialize)]
-#[serde(deny_unknown_fields)]
-pub struct JsonFilter {
-    pub pointer: String,
-    pub regex: String,
-}
-
-impl JsonFilter {
-    pub fn evaluate(&self, data: &serde_json::Value) -> Result<bool, WebhookeyError> {
-        trace!(
-            "Matching `{}` on `{}` from received json",
-            &self.regex,
-            &self.pointer,
-        );
-
-        let regex = Regex::new(&self.regex).map_err(WebhookeyError::Regex)?;
-
-        if let Some(value) = data.pointer(&self.pointer) {
-            if regex.is_match(&get_string(value)?) {
-                debug!("Regex `{}` for `{}` matches", &self.regex, &self.pointer);
-
-                return Ok(true);
-            }
-        }
-
-        debug!(
-            "Regex `{}` for `{}` does not match",
-            &self.regex, &self.pointer
-        );
-
-        Ok(false)
-    }
-}
-
-#[derive(Debug, Deserialize, Serialize)]
-#[serde(deny_unknown_fields, rename_all = "lowercase")]
-pub enum FilterType {
-    And(Vec<FilterType>),
-    Or(Vec<FilterType>),
-    #[serde(rename = "json")]
-    JsonFilter(JsonFilter),
-}
-
-impl FilterType {
-    pub fn evaluate(&self, data: &serde_json::Value) -> Result<bool, WebhookeyError> {
-        match self {
-            FilterType::And(filters) => {
-                let (mut results, mut errors) = (Vec::new(), Vec::new());
-
-                filters
-                    .iter()
-                    .map(|filter| filter.evaluate(data))
-                    .for_each(|item| match item {
-                        Ok(o) => results.push(o),
-                        Err(e) => errors.push(e),
-                    });
-
-                if errors.is_empty() {
-                    Ok(results.iter().all(|r| *r))
-                } else {
-                    errors
-                        .iter()
-                        .for_each(|e| error!("Could not evaluate Filter: {}", e));
-
-                    Err(WebhookeyError::InvalidFilter)
-                }
-            }
-            FilterType::Or(filters) => {
-                let (mut results, mut errors) = (Vec::new(), Vec::new());
-
-                filters
-                    .iter()
-                    .map(|filter| filter.evaluate(data))
-                    .for_each(|item| match item {
-                        Ok(o) => results.push(o),
-                        Err(e) => errors.push(e),
-                    });
-
-                if errors.is_empty() {
-                    Ok(results.iter().any(|r| *r))
-                } else {
-                    errors
-                        .iter()
-                        .for_each(|e| error!("Could not evaluate Filter: {}", e));
-
-                    Err(WebhookeyError::InvalidFilter)
-                }
-            }
-            // FilterType::HeaderFilter(filter) => todo!(),
-            FilterType::JsonFilter(filter) => filter.evaluate(data),
-        }
-    }
-}
-
-pub fn get_string(data: &serde_json::Value) -> Result<String, WebhookeyError> {
-    match &data {
-        serde_json::Value::Bool(bool) => Ok(bool.to_string()),
-        serde_json::Value::Number(number) => Ok(number.to_string()),
-        serde_json::Value::String(string) => Ok(string.as_str().to_string()),
-        x => {
-            error!("Could not get string from: {:?}", x);
-            unimplemented!()
-        }
-    }
-}
diff --git a/webhookey.1 b/webhookey.1
new file mode 100644
index 0000000..e0ca688
--- /dev/null
+++ b/webhookey.1
@@ -0,0 +1,57 @@
+.TH WEBHOOKEY 1 "26 Nov 2021" "webhookey" "Linux"
+.SH NAME
+webhookey \- Receive webhooks and act upon them
+.SH SYNOPSIS
+.B webhookey [OPTIONS] [SUBCOMMAND]
+.SH DESCRIPTION
+\fBwebhookey\fR receives http(s) requests in form of webhooks. Those
+webhooks are matched against configured filters. If a filter matches,
+a command (which can also incorporate data contained in the received
+header or body) is executed.
+.SH OPTIONS
+.TP
+.BR "-c", " --config " <FILE>
+Provide a path to the configuration file.
+.TP
+.BR "-h", " --help"
+Print help information.
+.TP
+.BR "-V", " --version"
+Print version information.
+.SH SUBCOMMAND
+.TP
+.B configtest
+- Verifies if the configuration can be parsed without errors.
+.TP
+.B help
+- Print the general help message or the help of the given subcommand(s).
+.SH ENVIRONMENT
+.TP
+.B ROCKET_ADDRESS
+The IP address webhookey listens on (default: 127.0.0.1).
+.TP
+.B ROCKET_PORT
+The port webhookey listens on (default: 8000).
+.TP
+.B ROCKET_WORKERS
+The numbers of threads to use (default: CPU core count).
+.TP
+.B RUST_LOG
+Set the Log level, which can be one one of "error", "warn", "info",
+"debug", "trace" (default: "error").
+.SH EXAMPLES
+.PP
+webhookey configtest
+.RS 4
+Return either "Config is OK" and return code 0 or an error description
+and return code 1.
+.RE
+.PP
+webhookey
+.RS 4
+Start webhookey.
+.RE
+.SH REPORTING BUGS
+To report any bugs file an issue at
+https://git.onders.org/finga/webhookey/issues/new?template=bug.md or
+send an email to <bug-report@onders.org>.