From 5271e76c86292c4119f7c21bd4f6b17ce9193481 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 10 May 2024 17:22:12 +0200 Subject: [PATCH 1/7] feat: rate-limit proxy requests --- Cargo.lock | 366 ++++++++++++++++++++++++++--- Cargo.toml | 1 + src/config.rs | 6 + src/grpc.rs | 2 +- src/handlers/desktop_client_mfa.rs | 37 ++- src/handlers/enrollment.rs | 82 +++---- src/handlers/mod.rs | 24 +- src/handlers/password_reset.rs | 55 ++--- src/http.rs | 49 +++- 9 files changed, 472 insertions(+), 150 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93a8125..2db5b4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,9 +77,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220" [[package]] name = "anstyle-parse" @@ -343,6 +343,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytes" version = "1.5.0" @@ -469,6 +475,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crypto-common" version = "0.1.6" @@ -489,6 +501,19 @@ dependencies = [ "cipher", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "defguard-proxy" version = "0.4.0" @@ -510,6 +535,7 @@ dependencies = [ "tonic", "tonic-build", "tower-http", + "tower_governor", "tracing", "tracing-subscriber", "url", @@ -609,6 +635,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -616,6 +657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -624,6 +666,34 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.30" @@ -636,16 +706,28 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -685,6 +767,26 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "governor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand", + "smallvec", + "spinning_top", +] + [[package]] name = "h2" version = "0.3.24" @@ -697,7 +799,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.11", - "indexmap 2.1.0", + "indexmap 2.2.2", "slab", "tokio", "tokio-util", @@ -716,7 +818,7 @@ dependencies = [ "futures-sink", "futures-util", "http 1.0.0", - "indexmap 2.1.0", + "indexmap 2.2.2", "slab", "tokio", "tokio-util", @@ -911,12 +1013,11 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", - "futures-channel", "futures-util", "http 1.0.0", "http-body 1.0.0", @@ -924,7 +1025,6 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tracing", ] [[package]] @@ -949,9 +1049,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" dependencies = [ "equivalent", "hashbrown 0.14.3", @@ -981,6 +1081,15 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -989,9 +1098,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linux-raw-sys" @@ -999,6 +1108,16 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.20" @@ -1068,12 +1187,24 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nonempty" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1084,6 +1215,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num_cpus" version = "1.16.0" @@ -1127,6 +1264,29 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "parking_lot" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.1", + "smallvec", + "windows-targets 0.52.0", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1140,23 +1300,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.1.0", + "indexmap 2.2.2", ] [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", @@ -1187,6 +1347,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1272,6 +1438,21 @@ dependencies = [ "prost", ] +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quote" version = "1.0.35" @@ -1311,6 +1492,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "raw-cpuid" +version = "11.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e29830cbb1290e404f24c73af91c5d8d631ce7e128691e9477556b540cd01ecd" +dependencies = [ + "bitflags 2.4.2", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1320,6 +1510,15 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags 2.4.2", +] + [[package]] name = "regex" version = "1.10.3" @@ -1328,7 +1527,7 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.4", + "regex-automata 0.4.5", "regex-syntax 0.8.2", ] @@ -1343,9 +1542,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -1386,9 +1585,9 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", @@ -1461,6 +1660,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sct" version = "0.7.1" @@ -1496,18 +1701,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", @@ -1516,9 +1721,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", @@ -1598,6 +1803,15 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1635,7 +1849,7 @@ checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.4.1", "rustix", "windows-sys 0.52.0", ] @@ -1672,12 +1886,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "00b24b79b7a07f10209f19e683ca1e289d80b1e76ffa8c2b779718566a083679" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -1692,10 +1907,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -1716,9 +1932,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -1889,6 +2105,22 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +[[package]] +name = "tower_governor" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313fa625fea5790ed56360a30ea980e41229cf482b4835801a67ef1922bf63b9" +dependencies = [ + "axum 0.7.4", + "forwarded-header-value", + "governor", + "http 1.0.0", + "pin-project", + "thiserror", + "tower", + "tracing", +] + [[package]] name = "tracing" version = "0.1.40" @@ -2053,6 +2285,70 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "which" version = "4.4.2" diff --git a/Cargo.toml b/Cargo.toml index 1697a97..cd85194 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ clap = { version = "4.4", features = ["derive", "env", "cargo"] } # other utils dotenvy = "0.15" url = "2.4" +tower_governor = "0.4" [build-dependencies] tonic-build = { version = "0.10" } diff --git a/src/config.rs b/src/config.rs index 038a541..6f48e59 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,4 +25,10 @@ pub struct Config { #[arg(long, env = "DEFGUARD_PROXY_LOG_LEVEL", default_value_t = LevelFilter::Info)] pub log_level: LevelFilter, + + #[arg(long, env = "DEFGUARD_PROXY_RATELIMIT_PERSECOND", default_value_t = 0)] + pub rate_limit_per_second: u64, + + #[arg(long, env = "DEFGUARD_PROXY_RATELIMIT_BURST", default_value_t = 0)] + pub rate_limit_burst: u32, } diff --git a/src/grpc.rs b/src/grpc.rs index ba35fa7..0e140f8 100644 --- a/src/grpc.rs +++ b/src/grpc.rs @@ -37,7 +37,7 @@ impl ProxyServer { } } - /// Sends message to the other side of RPC, with given `payload` and optional 'device_info`. + /// Sends message to the other side of RPC, with given `payload` and optional `device_info`. /// Returns `tokio::sync::oneshot::Reveicer` to let the caller await reply. #[instrument(name = "send_grpc_message", level = "debug", skip(self))] pub fn send( diff --git a/src/handlers/desktop_client_mfa.rs b/src/handlers/desktop_client_mfa.rs index 36fc72e..858ff4b 100644 --- a/src/handlers/desktop_client_mfa.rs +++ b/src/handlers/desktop_client_mfa.rs @@ -1,3 +1,6 @@ +use axum::{extract::State, routing::post, Json, Router}; +use tracing::{error, info}; + use crate::{ error::ApiError, handlers::get_core_response, @@ -7,9 +10,8 @@ use crate::{ ClientMfaStartRequest, ClientMfaStartResponse, DeviceInfo, }, }; -use axum::{extract::State, routing::post, Json, Router}; -pub fn router() -> Router { +pub(crate) fn router() -> Router { Router::new() .route("/start", post(start_client_mfa)) .route("/finish", post(finish_client_mfa)) @@ -27,15 +29,13 @@ async fn start_client_mfa( device_info, )?; let payload = get_core_response(rx).await?; - match payload { - core_response::Payload::ClientMfaStart(response) => { - info!("Started desktop client authorization {req:?}"); - Ok(Json(response)) - } - _ => { - error!("Received invalid gRPC response type: {payload:?}"); - Err(ApiError::InvalidResponseType) - } + + if let core_response::Payload::ClientMfaStart(response) = payload { + info!("Started desktop client authorization {req:?}"); + Ok(Json(response)) + } else { + error!("Received invalid gRPC response type: {payload:#?}"); + Err(ApiError::InvalidResponseType) } } @@ -51,14 +51,11 @@ async fn finish_client_mfa( device_info, )?; let payload = get_core_response(rx).await?; - match payload { - core_response::Payload::ClientMfaFinish(response) => { - info!("Finished desktop client authorization"); - Ok(Json(response)) - } - _ => { - error!("Received invalid gRPC response type: {payload:?}"); - Err(ApiError::InvalidResponseType) - } + if let core_response::Payload::ClientMfaFinish(response) = payload { + info!("Finished desktop client authorization"); + Ok(Json(response)) + } else { + error!("Received invalid gRPC response type: {payload:#?}"); + Err(ApiError::InvalidResponseType) } } diff --git a/src/handlers/enrollment.rs b/src/handlers/enrollment.rs index 7e5856f..b8a0a21 100644 --- a/src/handlers/enrollment.rs +++ b/src/handlers/enrollment.rs @@ -40,22 +40,19 @@ pub async fn start_enrollment_process( .grpc_server .send(Some(core_request::Payload::EnrollmentStart(req)), None)?; let payload = get_core_response(rx).await?; - match payload { - core_response::Payload::EnrollmentStart(response) => { - info!( - "Started enrollment process for user {:?} by admin {:?}", - response.user, response.admin - ); - // set session cookie - let cookie = Cookie::build((ENROLLMENT_COOKIE_NAME, token)) - .expires(OffsetDateTime::from_unix_timestamp(response.deadline_timestamp).unwrap()); - - Ok((private_cookies.add(cookie), Json(response))) - } - _ => { - error!("Received invalid gRPC response type: {payload:?}"); - Err(ApiError::InvalidResponseType) - } + if let core_response::Payload::EnrollmentStart(response) = payload { + info!( + "Started enrollment process for user {:?} by admin {:?}", + response.user, response.admin + ); + // set session cookie + let cookie = Cookie::build((ENROLLMENT_COOKIE_NAME, token)) + .expires(OffsetDateTime::from_unix_timestamp(response.deadline_timestamp).unwrap()); + + Ok((private_cookies.add(cookie), Json(response))) + } else { + error!("Received invalid gRPC response type: {payload:#?}"); + Err(ApiError::InvalidResponseType) } } @@ -78,20 +75,17 @@ pub async fn activate_user( .grpc_server .send(Some(core_request::Payload::ActivateUser(req)), device_info)?; let payload = get_core_response(rx).await?; - match payload { - core_response::Payload::Empty(_) => { - if let Some(cookie) = private_cookies.get(ENROLLMENT_COOKIE_NAME) { - info!("Activated user - phone number {phone:?}"); - debug!("Enrollment finished. Removing session cookie"); - private_cookies = private_cookies.remove(cookie); - } - - Ok(private_cookies) - } - _ => { - error!("Received invalid gRPC response type: {payload:?}"); - Err(ApiError::InvalidResponseType) + if let core_response::Payload::Empty(()) = payload { + if let Some(cookie) = private_cookies.get(ENROLLMENT_COOKIE_NAME) { + info!("Activated user - phone number {phone:?}"); + debug!("Enrollment finished. Removing session cookie"); + private_cookies = private_cookies.remove(cookie); } + + Ok(private_cookies) + } else { + error!("Received invalid gRPC response type: {payload:#?}"); + Err(ApiError::InvalidResponseType) } } @@ -114,15 +108,12 @@ pub async fn create_device( .grpc_server .send(Some(core_request::Payload::NewDevice(req)), device_info)?; let payload = get_core_response(rx).await?; - match payload { - core_response::Payload::DeviceConfig(response) => { - info!("Added new device {name} {pubkey}"); - Ok(Json(response)) - } - _ => { - error!("Received invalid gRPC response type: {payload:?}"); - Err(ApiError::InvalidResponseType) - } + if let core_response::Payload::DeviceConfig(response) = payload { + info!("Added new device {name} {pubkey}"); + Ok(Json(response)) + } else { + error!("Received invalid gRPC response type: {payload:#?}"); + Err(ApiError::InvalidResponseType) } } @@ -144,14 +135,11 @@ pub async fn get_network_info( .grpc_server .send(Some(core_request::Payload::ExistingDevice(req)), None)?; let payload = get_core_response(rx).await?; - match payload { - core_response::Payload::DeviceConfig(response) => { - info!("Got network info for device {pubkey}"); - Ok(Json(response)) - } - _ => { - error!("Received invalid gRPC response type: {payload:?}"); - Err(ApiError::InvalidResponseType) - } + if let core_response::Payload::DeviceConfig(response) = payload { + info!("Got network info for device {pubkey}"); + Ok(Json(response)) + } else { + error!("Received invalid gRPC response type: {payload:#?}"); + Err(ApiError::InvalidResponseType) } } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index b88bb5f..2d1c62f 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -46,19 +46,15 @@ where /// /// Waits for core response with a given timeout and returns the response payload. async fn get_core_response(rx: Receiver) -> Result { - match timeout(Duration::from_secs(CORE_RESPONSE_TIMEOUT), rx).await { - Ok(core_response) => { - debug!("Got gRPC response from Defguard core: {core_response:?}"); - if let Ok(Payload::CoreError(core_error)) = core_response { - return Err(core_error.into()); - }; - core_response.map_err(|err| { - ApiError::Unexpected(format!("Failed to receive core response: {err}")) - }) - } - Err(_) => { - error!("Did not receive core response within {CORE_RESPONSE_TIMEOUT} seconds"); - Err(ApiError::CoreTimeout) - } + if let Ok(core_response) = timeout(Duration::from_secs(CORE_RESPONSE_TIMEOUT), rx).await { + debug!("Got gRPC response from Defguard core: {core_response:?}"); + if let Ok(Payload::CoreError(core_error)) = core_response { + return Err(core_error.into()); + }; + core_response + .map_err(|err| ApiError::Unexpected(format!("Failed to receive core response: {err}"))) + } else { + error!("Did not receive core response within {CORE_RESPONSE_TIMEOUT} seconds"); + Err(ApiError::CoreTimeout) } } diff --git a/src/handlers/password_reset.rs b/src/handlers/password_reset.rs index f09a9ce..dea6d61 100644 --- a/src/handlers/password_reset.rs +++ b/src/handlers/password_reset.rs @@ -32,15 +32,12 @@ pub async fn request_password_reset( device_info, )?; let payload = get_core_response(rx).await?; - match payload { - core_response::Payload::Empty(_) => { - info!("Started password reset request for {}", req.email); - Ok(()) - } - _ => { - error!("Received invalid gRPC response type: {payload:?}"); - Err(ApiError::InvalidResponseType) - } + if let core_response::Payload::Empty(()) = payload { + info!("Started password reset request for {}", req.email); + Ok(()) + } else { + error!("Received invalid gRPC response type: {payload:#?}"); + Err(ApiError::InvalidResponseType) } } @@ -66,19 +63,16 @@ pub async fn start_password_reset( device_info, )?; let payload = get_core_response(rx).await?; - match payload { - core_response::Payload::PasswordResetStart(response) => { - // set session cookie - let cookie = Cookie::build((PASSWORD_RESET_COOKIE_NAME, token)) - .expires(OffsetDateTime::from_unix_timestamp(response.deadline_timestamp).unwrap()); + if let core_response::Payload::PasswordResetStart(response) = payload { + // set session cookie + let cookie = Cookie::build((PASSWORD_RESET_COOKIE_NAME, token)) + .expires(OffsetDateTime::from_unix_timestamp(response.deadline_timestamp).unwrap()); - info!("Started password reset process"); - Ok((private_cookies.add(cookie), Json(response))) - } - _ => { - error!("Received invalid gRPC response type: {payload:?}"); - Err(ApiError::InvalidResponseType) - } + info!("Started password reset process"); + Ok((private_cookies.add(cookie), Json(response))) + } else { + error!("Received invalid gRPC response type: {payload:#?}"); + Err(ApiError::InvalidResponseType) } } @@ -100,17 +94,14 @@ pub async fn reset_password( .grpc_server .send(Some(core_request::Payload::PasswordReset(req)), device_info)?; let payload = get_core_response(rx).await?; - match payload { - core_response::Payload::Empty(_) => { - if let Some(cookie) = private_cookies.get(PASSWORD_RESET_COOKIE_NAME) { - info!("Password reset finished. Removing session cookie"); - private_cookies = private_cookies.remove(cookie); - } - Ok(private_cookies) - } - _ => { - error!("Received invalid gRPC response type: {payload:?}"); - Err(ApiError::InvalidResponseType) + if let core_response::Payload::Empty(()) = payload { + if let Some(cookie) = private_cookies.get(PASSWORD_RESET_COOKIE_NAME) { + info!("Password reset finished. Removing session cookie"); + private_cookies = private_cookies.remove(cookie); } + Ok(private_cookies) + } else { + error!("Received invalid gRPC response type: {payload:#?}"); + Err(ApiError::InvalidResponseType) } } diff --git a/src/http.rs b/src/http.rs index de80a2e..e1fbddc 100644 --- a/src/http.rs +++ b/src/http.rs @@ -1,6 +1,7 @@ use std::{ fs::read_to_string, net::{IpAddr, Ipv4Addr, SocketAddr}, + time::Duration, }; use anyhow::Context; @@ -17,6 +18,9 @@ use clap::crate_version; use serde::Serialize; use tokio::{net::TcpListener, task::JoinSet}; use tonic::transport::{Identity, Server, ServerTlsConfig}; +use tower_governor::{ + governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor, GovernorLayer, +}; use tower_http::{ services::{ServeDir, ServeFile}, trace::{self, TraceLayer}, @@ -33,6 +37,7 @@ use crate::{ pub(crate) static ENROLLMENT_COOKIE_NAME: &str = "defguard_proxy"; pub(crate) static PASSWORD_RESET_COOKIE_NAME: &str = "defguard_proxy_password_reset"; +const RATE_LIMITER_CLEANUP_PERIOD: Duration = Duration::from_secs(60); #[derive(Clone)] pub(crate) struct AppState { @@ -133,7 +138,44 @@ pub async fn run_server(config: Config) -> anyhow::Result<()> { let serve_web_dir = ServeDir::new("web/dist").fallback(ServeFile::new("web/dist/index.html")); let serve_images = ServeDir::new("web/src/shared/images/svg").not_found_service(handle_404.into_service()); - let app = Router::new() + + // Setup tower_governor rate-limiter + debug!( + "Configuring rate limiter, per_second: {}, burst: {}", + config.rate_limit_per_second, config.rate_limit_burst + ); + let governor_conf = GovernorConfigBuilder::default() + .key_extractor(SmartIpKeyExtractor) + .per_second(config.rate_limit_per_second) + .burst_size(config.rate_limit_burst) + .finish(); + + let governor_conf = if let Some(conf) = governor_conf { + let governor_limiter = conf.limiter().clone(); + + // Start background task to cleanup rate-limiter data + tokio::spawn(async move { + loop { + tokio::time::sleep(RATE_LIMITER_CLEANUP_PERIOD).await; + tracing::debug!( + "Cleaning-up rate limiter storage, current size: {}", + governor_limiter.len() + ); + governor_limiter.retain_recent(); + } + }); + info!( + "Configured rate limiter, per_second: {}, burst: {}", + config.rate_limit_per_second, config.rate_limit_burst + ); + Some(conf) + } else { + info!("Skipping rate limiter setup"); + None + }; + + // Build axum app + let mut app = Router::new() .nest( "/api/v1", Router::new() @@ -161,6 +203,11 @@ pub async fn run_server(config: Config) -> anyhow::Result<()> { }) .on_response(trace::DefaultOnResponse::new().level(Level::DEBUG)), ); + if let Some(conf) = governor_conf { + app = app.layer(GovernorLayer { + config: conf.into(), + }); + } debug!("Configured API server routing: {app:?}"); // Start web server. From 248e7c83253a0eec13f62f8a15618a228d5f589b Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Mon, 3 Jun 2024 12:14:07 +0200 Subject: [PATCH 2/7] fix: add workaround for wrong config file extension on some browsers (#62) * fix file extension on some browsers * remove charset as it's unsued according to the standard --- web/src/shared/utils/downloadWGConfig.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/shared/utils/downloadWGConfig.ts b/web/src/shared/utils/downloadWGConfig.ts index f3c357d..b395a07 100644 --- a/web/src/shared/utils/downloadWGConfig.ts +++ b/web/src/shared/utils/downloadWGConfig.ts @@ -2,7 +2,9 @@ import saveAs from 'file-saver'; export const downloadWGConfig = (config: string, fileName: string) => { const blob = new Blob([config.replace(/^[^\S\r\n]+|[^\S\r\n]+$/gm, '')], { - type: 'text/plain;charset=utf-8', + // octet-stream is used here as a workaround: some browsers will append + // an additional .txt extension to the file name if the MIME type is text/plain. + type: 'application/octet-stream', }); saveAs(blob, `${fileName.toLowerCase()}.conf`); }; From fcbc012da62b6abb2a9b430ba30a901292bb8259 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 12 Jun 2024 11:07:15 +0200 Subject: [PATCH 3/7] feat: properly interpret forbidden error codes (#63) * add permission denied error type * change error text message --- src/error.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/error.rs b/src/error.rs index d3bad06..b232fed 100644 --- a/src/error.rs +++ b/src/error.rs @@ -22,6 +22,8 @@ pub enum ApiError { CoreTimeout, #[error("Invalid core gRPC response type received")] InvalidResponseType, + #[error("Permission denied")] + PermissionDenied, } impl IntoResponse for ApiError { @@ -30,6 +32,7 @@ impl IntoResponse for ApiError { let (status, error_message) = match self { Self::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()), Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), + Self::PermissionDenied => (StatusCode::FORBIDDEN, self.to_string()), _ => ( StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string(), @@ -51,6 +54,7 @@ impl From for ApiError { match status.code() { Code::Unauthenticated => ApiError::Unauthorized, Code::InvalidArgument => ApiError::BadRequest(status.message().to_string()), + Code::PermissionDenied => ApiError::PermissionDenied, _ => ApiError::Unexpected(status.to_string()), } } From 29535cd53c27221bafe819a61782c4b8d25a4b6c Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:13:27 +0200 Subject: [PATCH 4/7] fix: update protobufs (#64) --- proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto b/proto index 29898d9..c71f378 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 29898d9cb502ef5e7bb1771b63e6a66debf31e7d +Subproject commit c71f37847279ee23220fcf9e0e45d2c365b3b8ee From e7d998b0a1f7488907ef2ecf523c0ff7772508ec Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:42:04 +0200 Subject: [PATCH 5/7] feat: add new logo (#65) * update logo * fix names --- web/src/pages/main/SelectPath.tsx | 8 ++--- .../components/svg/DefguardLogoText.tsx | 8 ++--- .../svg/EnrollmentSelectGraphic.tsx | 14 ++++++-- .../svg/PasswordResetSelectGraphic.tsx | 14 ++++++-- .../shared/images/svg/defguard-logo-text.svg | 35 +++++++++++++------ 5 files changed, 55 insertions(+), 24 deletions(-) diff --git a/web/src/pages/main/SelectPath.tsx b/web/src/pages/main/SelectPath.tsx index f8f7966..c01539e 100644 --- a/web/src/pages/main/SelectPath.tsx +++ b/web/src/pages/main/SelectPath.tsx @@ -2,8 +2,8 @@ import { useNavigate } from 'react-router-dom'; import { shallow } from 'zustand/shallow'; import { Card } from '../../shared/components/layout/Card/Card'; -import EnrollmentSelectGraphic from '../../shared/components/svg/EnrollmentSelectGraphic'; -import PasswordResetSelectGraphic from '../../shared/components/svg/PasswordResetSelectGraphic'; +import SvgEnrollmentSelectGraphic from '../../shared/components/svg/EnrollmentSelectGraphic'; +import SvgPasswordResetSelectGraphic from '../../shared/components/svg/PasswordResetSelectGraphic'; import { routes } from '../../shared/routes'; import { usePasswordResetStore } from '../passwordReset/hooks/usePasswordResetStore'; import { PathSelectCard } from './DeviceSetupMethodCard/DeviceSetupMethodCard'; @@ -19,7 +19,7 @@ export const SelectPath = () => { onSelect={() => navigate(routes.token)} title="Enrollment process" subtitle="Confirm your new account" - logo={} + logo={} /> { }} title="Password reset" subtitle="Reset password for existing account" - logo={} + logo={} /> ); diff --git a/web/src/shared/components/svg/DefguardLogoText.tsx b/web/src/shared/components/svg/DefguardLogoText.tsx index e201946..0df432e 100644 --- a/web/src/shared/components/svg/DefguardLogoText.tsx +++ b/web/src/shared/components/svg/DefguardLogoText.tsx @@ -2,15 +2,15 @@ import type { SVGProps } from 'react'; const SvgDefguardLogoText = (props: SVGProps) => ( ); diff --git a/web/src/shared/components/svg/EnrollmentSelectGraphic.tsx b/web/src/shared/components/svg/EnrollmentSelectGraphic.tsx index a556614..aea34c1 100644 --- a/web/src/shared/components/svg/EnrollmentSelectGraphic.tsx +++ b/web/src/shared/components/svg/EnrollmentSelectGraphic.tsx @@ -1,5 +1,13 @@ -const EnrollmentSelectGraphic = () => ( - +import type { SVGProps } from 'react'; +const SvgEnrollmentSelectGraphic = (props: SVGProps) => ( + ( /> ); -export default EnrollmentSelectGraphic; +export default SvgEnrollmentSelectGraphic; diff --git a/web/src/shared/components/svg/PasswordResetSelectGraphic.tsx b/web/src/shared/components/svg/PasswordResetSelectGraphic.tsx index 26504c2..426b8bf 100644 --- a/web/src/shared/components/svg/PasswordResetSelectGraphic.tsx +++ b/web/src/shared/components/svg/PasswordResetSelectGraphic.tsx @@ -1,5 +1,13 @@ -const PasswordResetSelectGraphic = () => ( - +import type { SVGProps } from 'react'; +const SvgPasswordResetSelectGraphic = (props: SVGProps) => ( + ( ); -export default PasswordResetSelectGraphic; +export default SvgPasswordResetSelectGraphic; diff --git a/web/src/shared/images/svg/defguard-logo-text.svg b/web/src/shared/images/svg/defguard-logo-text.svg index c6b32f8..141b666 100644 --- a/web/src/shared/images/svg/defguard-logo-text.svg +++ b/web/src/shared/images/svg/defguard-logo-text.svg @@ -1,10 +1,25 @@ - - - - - - - - - - + + + + + + + + + + \ No newline at end of file From 272d74688115741a67321acf7ce0865656035b39 Mon Sep 17 00:00:00 2001 From: Maciek Date: Tue, 25 Jun 2024 10:39:26 +0200 Subject: [PATCH 6/7] ci: build DEB & RPM packages (#50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add option to read config from file * add base fpm config * add systemd service config * add DEB build workflow * disable docker build for tests * setup RPM build * add link to docs * fix comment * embedd assets, make logs deserializable * build frontend * move binary to bin * restart job * add missing config value * add remaining config values * change dockerfile * cleanup * cleanup 2 * sort cargo toml * add newlines * Revert "sort cargo toml" This reverts commit 40ddb29bd9bfd7def5bd3a826da58d1c642f0822. * preserve categories * move get_config --------- Co-authored-by: Maciej Wójcik Co-authored-by: Aleksander <170264518+t-aleksander@users.noreply.github.com> --- .fpm | 6 ++ .github/workflows/release.yml | 56 ++++++++++++ Cargo.lock | 156 ++++++++++++++++++++++++++++++++++ Cargo.toml | 5 ++ Dockerfile | 28 +++--- defguard-proxy.service | 22 +++++ example-config.toml | 18 ++++ src/assets.rs | 48 +++++++++++ src/config.rs | 33 ++++++- src/http.rs | 20 ++--- src/lib.rs | 1 + src/main.rs | 7 +- 12 files changed, 369 insertions(+), 31 deletions(-) create mode 100644 .fpm create mode 100644 defguard-proxy.service create mode 100644 example-config.toml create mode 100644 src/assets.rs diff --git a/.fpm b/.fpm new file mode 100644 index 0000000..fbd7b5e --- /dev/null +++ b/.fpm @@ -0,0 +1,6 @@ +-s dir +--name defguard-proxy +--architecture x86_64 +--description "defguard proxy service" +--url "https://defguard.net/" +--maintainer "teonite" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 34cb975..dfa91a1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -113,6 +113,26 @@ jobs: [registry."docker.io"] mirrors = ["dockerhub-proxy.teonite.net"] + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + cache-dependency-path: ./web/pnpm-lock.yaml + + - name: Install frontend dependencies + run: pnpm install --ignore-scripts --frozen-lockfile + working-directory: web + + - name: Build frontend + run: pnpm build + working-directory: web + - name: Build release binary uses: actions-rs/cargo@v1 with: @@ -140,3 +160,39 @@ jobs: asset_path: defguard-proxy-${{ github.ref_name }}-${{ matrix.target }}.tar.gz asset_name: defguard-proxy-${{ github.ref_name }}-${{ matrix.target }}.tar.gz asset_content_type: application/octet-stream + + - name: Build DEB package + if: matrix.build == 'linux' + uses: bpicode/github-action-fpm@master + with: + fpm_args: "defguard-proxy-${{ github.ref_name }}-${{ matrix.target }}=/usr/bin/defguard-proxy defguard-proxy.service=/usr/lib/systemd/system/defguard-proxy.service example-config.toml=/etc/defguard/proxy.toml" + fpm_opts: "--debug --output-type deb --version ${{ env.VERSION }} --package defguard-proxy-${{ env.VERSION }}-${{ matrix.target }}.deb" + + - name: Upload DEB + if: matrix.build == 'linux' + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: defguard-proxy-${{ env.VERSION }}-${{ matrix.target }}.deb + asset_name: defguard-proxy-${{ env.VERSION }}-${{ matrix.target }}.deb + asset_content_type: application/octet-stream + + - name: Build RPM package + if: matrix.build == 'linux' + uses: bpicode/github-action-fpm@master + with: + fpm_args: "defguard-proxy-${{ github.ref_name }}-${{ matrix.target }}=/usr/bin/defguard-proxy defguard-proxy.service=/usr/lib/systemd/system/defguard-proxy.service example-config.toml=/etc/defguard/proxy.toml" + fpm_opts: "--debug --output-type rpm --version ${{ env.VERSION }} --package defguard-proxy-${{ env.VERSION }}-${{ matrix.target }}.rpm" + + - name: Upload RPM + if: matrix.build == 'linux' + uses: actions/upload-release-asset@v1.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create-release.outputs.upload_url }} + asset_path: defguard-proxy-${{ env.VERSION }}-${{ matrix.target }}.rpm + asset_name: defguard-proxy-${{ env.VERSION }}-${{ matrix.target }}.rpm + asset_content_type: application/octet-stream diff --git a/Cargo.lock b/Cargo.lock index 2db5b4e..ae541e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -343,6 +343,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -524,14 +534,18 @@ dependencies = [ "axum-extra", "clap", "dotenvy", + "log", + "mime_guess", "prost", "prost-build", + "rust-embed", "serde", "serde_json", "thiserror", "time", "tokio", "tokio-stream", + "toml", "tonic", "tonic-build", "tower-http", @@ -767,6 +781,19 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.5", + "regex-syntax 0.8.2", +] + [[package]] name = "governor" version = "0.6.3" @@ -1123,6 +1150,9 @@ name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "serde", +] [[package]] name = "matchers" @@ -1577,6 +1607,41 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rust-embed" +version = "8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9f96e283ec64401f30d3df8ee2aaeb2561f34c824381efa24a35f79bf40ee4" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c74a686185620830701348de757fd36bef4aa9680fd23c49fc539ddcc1af32" +dependencies = [ + "globset", + "sha2", + "walkdir", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1651,6 +1716,15 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.23" @@ -1740,6 +1814,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1763,6 +1846,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2003,6 +2097,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a4b9e8023eb94392d3dca65d717c53abc5dad49c07cb65bb8fcd87115fa325" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.2.2", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.10.2" @@ -2270,6 +2398,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2377,6 +2515,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2514,3 +2661,12 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818ce546a11a9986bc24f93d0cdf38a8a1a400f1473ea8c82e59f6e0ffab9249" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml index cd85194..c82ec54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,11 @@ tower-http = { version = "0.5", features = ["fs", "trace"] } # logging/tracing tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } +log = { version = "0.4", features = ["serde"] } # data de/serialization serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } +toml = { version = "0.8", default-features = false, features = ["parse"] } # gRPC tonic = { version = "0.10", features = ["gzip", "tls", "tls-roots"] } prost = "0.12" @@ -38,6 +40,9 @@ clap = { version = "4.4", features = ["derive", "env", "cargo"] } dotenvy = "0.15" url = "2.4" tower_governor = "0.4" +# UI embedding +rust-embed = { version = "8.4", features = ["include-exclude"] } +mime_guess = "2.0" [build-dependencies] tonic-build = { version = "0.10" } diff --git a/Dockerfile b/Dockerfile index 7514038..235fbb1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,15 @@ +FROM node:19-alpine3.16 as web + +WORKDIR /app +COPY web/package.json . +COPY web/pnpm-lock.yaml . +COPY web/.npmrc . +RUN npm i -g pnpm +RUN pnpm install --ignore-scripts --frozen-lockfile +COPY web/ . +RUN pnpm run generate-translation-types +RUN pnpm build + FROM rust:1.75 as chef WORKDIR /build @@ -19,24 +31,14 @@ COPY --from=planner /build/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json # build project +COPY --from=web /app/dist ./web/dist +COPY web/src/shared/images/svg ./web/src/shared/images/svg RUN apt-get update && apt-get -y install protobuf-compiler libprotobuf-dev COPY Cargo.toml Cargo.lock build.rs ./ COPY src src COPY proto proto RUN cargo install --locked --path . --root /build -FROM node:19-alpine3.16 as web - -WORKDIR /app -COPY web/package.json . -COPY web/pnpm-lock.yaml . -COPY web/.npmrc . -RUN npm i -g pnpm -RUN pnpm install --ignore-scripts --frozen-lockfile -COPY web/ . -RUN pnpm run generate-translation-types -RUN pnpm build - # run FROM debian:bookworm-slim as runtime RUN apt-get update -y && \ @@ -44,6 +46,4 @@ RUN apt-get update -y && \ rm -rf /var/lib/apt/lists/* WORKDIR /app COPY --from=builder /build/bin/defguard-proxy . -COPY --from=web /app/dist ./web/dist -COPY web/src/shared/images/svg ./web/src/shared/images/svg ENTRYPOINT ["./defguard-proxy"] diff --git a/defguard-proxy.service b/defguard-proxy.service new file mode 100644 index 0000000..cf21599 --- /dev/null +++ b/defguard-proxy.service @@ -0,0 +1,22 @@ +[Unit] +Description=defguard proxy service +Documentation=https://defguard.gitbook.io/defguard/ +Wants=network-online.target +After=network-online.target + +[Service] +DynamicUser=yes +User=defguard +ExecReload=/bin/kill -HUP $MAINPID +ExecStart=/usr/bin/defguard-proxy --config /etc/defguard/proxy.toml +KillMode=process +KillSignal=SIGINT +LimitNOFILE=65536 +LimitNPROC=infinity +Restart=on-failure +RestartSec=2 +TasksMax=infinity +OOMScoreAdjust=-1000 + +[Install] +WantedBy=multi-user.target diff --git a/example-config.toml b/example-config.toml new file mode 100644 index 0000000..025d18d --- /dev/null +++ b/example-config.toml @@ -0,0 +1,18 @@ +# This is an example config file for defguard proxy +# To use it fill in actual values for your deployment below + +# port the API server will listen on +http_port = 8080 +# port the gRPC server will listen on +grpc_port = 50051 + +# gRPC SSL configuration +# provide certificate and key to connect to gRPC server with HTTPS +# https://defguard.gitbook.io/defguard/features/setting-up-your-instance/docker-compose#grpc-ssl-setup +# Optional: path to cert file +# grpc_cert: proxy.crt +# Optional: path to key file +# grpc_key: proxy.key +log_level = "info" +rate_limit_per_second = 0 +rate_limit_burst = 0 diff --git a/src/assets.rs b/src/assets.rs new file mode 100644 index 0000000..0ae3ddc --- /dev/null +++ b/src/assets.rs @@ -0,0 +1,48 @@ +use axum::{ + http::{header, StatusCode, Uri}, + response::{IntoResponse, Response}, +}; +use rust_embed::Embed; + +pub async fn web_asset(uri: Uri) -> impl IntoResponse { + let mut path = uri.path().trim_start_matches('/').to_string(); + // Rewrite the path to match the structure of the embedded files + path.insert_str(0, "dist/"); + StaticFile(path) +} + +pub async fn index() -> impl IntoResponse { + web_asset(Uri::from_static("/index.html")).await +} + +pub async fn svg(uri: Uri) -> impl IntoResponse { + let mut path = uri.path().trim_start_matches('/').to_string(); + // Rewrite the path to match the structure of the embedded files + path.insert_str(0, "src/shared/images/"); + StaticFile(path) +} + +#[derive(Embed)] +#[folder = "web/"] +#[include = "dist/*"] +#[include = "src/shared/images/*"] +struct WebAsset; + +pub struct StaticFile(pub T); + +impl IntoResponse for StaticFile +where + T: Into, +{ + fn into_response(self) -> Response { + let path = self.0.into(); + + match WebAsset::get(path.as_str()) { + Some(content) => { + let mime = mime_guess::from_path(path).first_or_octet_stream(); + ([(header::CONTENT_TYPE, mime.as_ref())], content.data).into_response() + } + None => (StatusCode::NOT_FOUND, "404 Not Found").into_response(), + } + } +} diff --git a/src/config.rs b/src/config.rs index 6f48e59..55b1a48 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,10 @@ use clap::Parser; +use serde::Deserialize; +use std::{fs, io::Error as IoError}; +use tracing::info; use tracing::log::LevelFilter; -#[derive(Parser, Debug)] +#[derive(Parser, Debug, Deserialize)] #[command(version)] pub struct Config { // port the API server will listen on @@ -31,4 +34,32 @@ pub struct Config { #[arg(long, env = "DEFGUARD_PROXY_RATELIMIT_BURST", default_value_t = 0)] pub rate_limit_burst: u32, + + /// Configuration file path + #[arg(long = "config", short)] + #[serde(skip)] + config_path: Option, +} + +#[derive(thiserror::Error, Debug)] +pub enum ConfigError { + #[error("Failed to read config file")] + IoError(#[from] IoError), + #[error("Failed to parse config file")] + ParseError(#[from] toml::de::Error), +} + +pub fn get_config() -> Result { + // parse CLI arguments to get config file path + let cli_config = Config::parse(); + + // load config from file if one was specified + if let Some(config_path) = cli_config.config_path { + info!("Reading configuration from config file: {config_path:?}"); + let config_toml = fs::read_to_string(config_path)?; + let file_config: Config = toml::from_str(&config_toml)?; + return Ok(file_config); + } + + Ok(cli_config) } diff --git a/src/http.rs b/src/http.rs index e1fbddc..f4e0cb1 100644 --- a/src/http.rs +++ b/src/http.rs @@ -8,7 +8,6 @@ use anyhow::Context; use axum::{ body::Body, extract::{ConnectInfo, FromRef}, - handler::HandlerWithoutStateExt, http::{Request, StatusCode}, routing::get, serve, Json, Router, @@ -21,13 +20,11 @@ use tonic::transport::{Identity, Server, ServerTlsConfig}; use tower_governor::{ governor::GovernorConfigBuilder, key_extractor::SmartIpKeyExtractor, GovernorLayer, }; -use tower_http::{ - services::{ServeDir, ServeFile}, - trace::{self, TraceLayer}, -}; +use tower_http::trace::{self, TraceLayer}; use tracing::{info_span, Level}; use crate::{ + assets::{index, svg, web_asset}, config::Config, error::ApiError, grpc::ProxyServer, @@ -133,12 +130,6 @@ pub async fn run_server(config: Config) -> anyhow::Result<()> { .context("Error running gRPC server") }); - // Serve static frontend files. - debug!("Configuring API server routing"); - let serve_web_dir = ServeDir::new("web/dist").fallback(ServeFile::new("web/dist/index.html")); - let serve_images = - ServeDir::new("web/src/shared/images/svg").not_found_service(handle_404.into_service()); - // Setup tower_governor rate-limiter debug!( "Configuring rate limiter, per_second: {}, burst: {}", @@ -176,6 +167,10 @@ pub async fn run_server(config: Config) -> anyhow::Result<()> { // Build axum app let mut app = Router::new() + .route("/", get(index)) + .route("/*path", get(index)) + .route("/assets/*path", get(web_asset)) + .route("/svg/*path", get(svg)) .nest( "/api/v1", Router::new() @@ -185,8 +180,7 @@ pub async fn run_server(config: Config) -> anyhow::Result<()> { .route("/health", get(healthcheck)) .route("/info", get(app_info)), ) - .nest_service("/svg", serve_images) - .fallback_service(serve_web_dir) + .fallback_service(get(handle_404)) .with_state(shared_state) .layer( TraceLayer::new_for_http() diff --git a/src/lib.rs b/src/lib.rs index 9f35c30..fc1667a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod assets; pub mod config; mod error; mod grpc; diff --git a/src/main.rs b/src/main.rs index d2ac2ad..0ab5d28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ -use clap::Parser; -use defguard_proxy::{config::Config, http::run_server, tracing::init_tracing}; +use defguard_proxy::{config::get_config, http::run_server, tracing::init_tracing}; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -7,7 +6,9 @@ async fn main() -> anyhow::Result<()> { if dotenvy::from_filename(".env.local").is_err() { dotenvy::dotenv().ok(); } - let config = Config::parse(); + + // read config from env + let config = get_config()?; init_tracing(&config.log_level); // run API web server From a07b0e4c6c45d40d98520389a941cdc132b5b6b7 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 25 Jun 2024 12:01:29 +0200 Subject: [PATCH 7/7] chore: bump version 0.4 -> 0.5 (#67) * bump version * update lockfile --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae541e4..431b50f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -526,7 +526,7 @@ dependencies = [ [[package]] name = "defguard-proxy" -version = "0.4.0" +version = "0.5.0" dependencies = [ "anyhow", "axum 0.7.4", diff --git a/Cargo.toml b/Cargo.toml index c82ec54..b4f9eee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "defguard-proxy" -version = "0.4.0" +version = "0.5.0" edition = "2021" license = "Apache-2.0" homepage = "https://github.com/DefGuard/proxy"