From 84ac87f551fee7aaa0b38bd1c150dc8178a8ccf7 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Mon, 3 Oct 2022 22:19:08 +0200 Subject: [PATCH] WIP: better listeners - listen on UNIX domain sockets - handle TLS stuff - allow mounting only some resources --- Cargo.lock | 317 +++++++++++++++++++---------- crates/cli/Cargo.toml | 2 + crates/cli/src/commands/server.rs | 113 +++++++--- crates/config/Cargo.toml | 1 + crates/config/src/sections/http.rs | 246 ++++++++++++++++++++-- crates/config/src/sections/mod.rs | 2 +- crates/handlers/src/lib.rs | 80 ++++++-- crates/keystore/src/lib.rs | 17 ++ crates/listener/Cargo.toml | 14 ++ crates/listener/src/lib.rs | 16 ++ crates/listener/src/maybe_tls.rs | 213 +++++++++++++++++++ crates/listener/src/unix_or_tcp.rs | 212 +++++++++++++++++++ 12 files changed, 1063 insertions(+), 170 deletions(-) create mode 100644 crates/listener/Cargo.toml create mode 100644 crates/listener/src/lib.rs create mode 100644 crates/listener/src/maybe_tls.rs create mode 100644 crates/listener/src/unix_or_tcp.rs diff --git a/Cargo.lock b/Cargo.lock index 85ab3859..828fa9fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,15 +96,6 @@ dependencies = [ "libc", ] -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - [[package]] name = "anyhow" version = "1.0.65" @@ -130,9 +121,9 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "async-compression" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345fd392ab01f746c717b1357165b76f0b67a60192007b234058c9045fdcf695" +checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" dependencies = [ "brotli", "flate2", @@ -230,7 +221,7 @@ dependencies = [ "http", "hyper", "ring", - "time 0.3.14", + "time 0.3.15", "tokio", "tower", "tracing", @@ -362,7 +353,7 @@ dependencies = [ "percent-encoding", "regex", "ring", - "time 0.3.14", + "time 0.3.15", "tracing", ] @@ -462,10 +453,10 @@ version = "0.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e93b0c93a3b963da946a0b8ef3853a7252298eb75cdbfb21dad60f5ed0ded861" dependencies = [ - "itoa 1.0.3", + "itoa 1.0.4", "num-integer", "ryu", - "time 0.3.14", + "time 0.3.15", ] [[package]] @@ -507,7 +498,7 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.3", + "itoa 1.0.4", "matchit 0.5.0", "memchr", "mime", @@ -537,7 +528,7 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.3", + "itoa 1.0.4", "matchit 0.6.0", "memchr", "mime", @@ -860,9 +851,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.2" +version = "4.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c9484ccdc4cb8e7b117cbd0eb150c7c0f04464854e4679aeb50ef03b32d003" +checksum = "6bf8832993da70a4c6d13c581f4463c2bdda27b9bf1c5498dc4365543abe6d6f" dependencies = [ "atty", "bitflags", @@ -875,9 +866,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.0.1" +version = "4.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca689d7434ce44517a12a89456b2be4d1ea1cafcd8f581978c03d45f5a5c12a7" +checksum = "c42f169caba89a7d512b5418b09864543eeb4d497416c917d7137863bd2076ad" dependencies = [ "heck", "proc-macro-error", @@ -895,6 +886,16 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "const-oid" version = "0.9.0" @@ -923,7 +924,7 @@ dependencies = [ "rand", "sha2 0.10.6", "subtle", - "time 0.3.14", + "time 0.3.15", "version_check", ] @@ -1104,15 +1105,14 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" +checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset", - "once_cell", "scopeguard", ] @@ -1128,19 +1128,18 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" +checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" dependencies = [ "cfg-if", - "once_cell", ] [[package]] name = "crypto-bigint" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f2b443d17d49dad5ef0ede301c3179cc923b8822f3393b4d2c28c269dd4a122" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" dependencies = [ "generic-array", "rand_core", @@ -1192,13 +1191,57 @@ dependencies = [ [[package]] name = "ctr" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d14f329cfbaf5d0e06b5e87fff7e265d2673c5ea7d2c27691a2c107db1442a0" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ "cipher", ] +[[package]] +name = "cxx" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f83d0ebf42c6eafb8d7c52f7e5f2d3003b89c7aa4fd2b79229209459a849af8" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d050484b55975889284352b0ffc2ecbda25c0c55978017c132b29ba0818a86" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d2199b00553eda8012dfec8d3b1c75fce747cf27c169a270b3b99e3448ab78" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb67a6de1f602736dd7eaead0080cf3435df806c61b24b13328db128c58868f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "darling" version = "0.14.1" @@ -1488,9 +1531,9 @@ dependencies = [ [[package]] name = "figment" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bd154d9ae2f1bb0ada5b7eebd56529513efa5de7d2fc8c6adf33bc43260cf" +checksum = "4e56602b469b2201400dec66a66aec5a9b8761ee97cd1b8c96ab2483fcc16cc9" dependencies = [ "atomic", "parking_lot 0.12.1", @@ -1727,9 +1770,9 @@ dependencies = [ [[package]] name = "group" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7391856def869c1c81063a03457c676fbcd419709c3dfb33d8d319de484b154d" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ "ff", "rand_core", @@ -1869,7 +1912,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes 1.2.1", "fnv", - "itoa 1.0.3", + "itoa 1.0.4", ] [[package]] @@ -1928,7 +1971,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.3", + "itoa 1.0.4", "pin-project-lite", "socket2", "tokio", @@ -1981,17 +2024,28 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.50" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0" +checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed" dependencies = [ "android_system_properties", "core-foundation-sys", + "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "winapi", ] +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2132,9 +2186,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" [[package]] name = "ittapi" @@ -2253,9 +2307,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.133" +version = "0.2.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" +checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" [[package]] name = "libm" @@ -2263,6 +2317,15 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" +[[package]] +name = "link-cplusplus" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +dependencies = [ + "cc", +] + [[package]] name = "linux-raw-sys" version = "0.0.46" @@ -2366,6 +2429,7 @@ dependencies = [ "mas-email", "mas-handlers", "mas-http", + "mas-listener", "mas-policy", "mas-router", "mas-static-files", @@ -2380,6 +2444,7 @@ dependencies = [ "opentelemetry-semantic-conventions", "opentelemetry-zipkin", "prometheus", + "rustls 0.20.6", "serde_json", "serde_yaml", "tokio", @@ -2409,6 +2474,7 @@ dependencies = [ "mas-keystore", "pem-rfc7468", "rand", + "rustls-pemfile", "schemars", "serde", "serde_json", @@ -2607,6 +2673,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "mas-listener" +version = "0.1.0" +dependencies = [ + "futures-util", + "hyper", + "pin-project-lite", + "tokio", + "tokio-rustls 0.23.4", + "tracing", +] + [[package]] name = "mas-policy" version = "0.1.0" @@ -2828,6 +2906,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint-dig" version = "0.8.1" @@ -3142,6 +3230,12 @@ version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "p256" version = "0.11.1" @@ -3319,9 +3413,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb779fcf4bb850fbbb0edc96ff6cf34fd90c4b1a112ce042653280d9a7364048" +checksum = "dbc7bc69c062e492337d74d59b120c274fd3d261b6bf6d3207d499b4b379c41a" dependencies = [ "thiserror", "ucd-trie", @@ -3329,9 +3423,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502b62a6d0245378b04ffe0a7fb4f4419a4815fce813bd8a0ec89a56e07d67b1" +checksum = "60b75706b9642ebcb34dab3bc7750f811609a0eb1dd8b88c2d15bf628c1c65b2" dependencies = [ "pest", "pest_generator", @@ -3339,9 +3433,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451e629bf49b750254da26132f1a5a9d11fd8a95a3df51d15c4abd1ba154cb6c" +checksum = "f4f9272122f5979a6511a749af9db9bfc810393f63119970d7085fed1c4ea0db" dependencies = [ "pest", "pest_meta", @@ -3352,9 +3446,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcec162c71c45e269dfc3fc2916eaeb97feab22993a21bcce4721d08cd7801a6" +checksum = "4c8717927f9b79515e565a64fe46c38b8cd0427e64c40680b14a7365ab09ac8d" dependencies = [ "once_cell", "pest", @@ -3444,12 +3538,13 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs1" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e3a81571d9455414f4d59ce2830bc9d2654e2efc5460fd67b0e0a6a36b6753a" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" dependencies = [ "der", "pkcs8", + "spki", "zeroize", ] @@ -3512,9 +3607,9 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "prettyplease" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a49e86d2c26a24059894a3afa13fd17d063419b05dfb83f06d9c3566060c3f5a" +checksum = "c142c0e46b57171fe0c528bee8c5b7569e80f0c17e377cd0e30ea57dbc11bb51" dependencies = [ "proc-macro2", "syn", @@ -3546,9 +3641,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ "unicode-ident", ] @@ -3852,9 +3947,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.7.0-rc.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "237a0c597c9fdb501f592f52e8ab754dc43787dd8a8bf20c191f562817d50b08" +checksum = "96144aaefe4fa4c1846c404d1ccc3dc45c9b15c2e41591597294cb7ccc2dbfd7" dependencies = [ "byteorder", "digest 0.10.5", @@ -3922,9 +4017,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.35.10" +version = "0.35.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af895b90e5c071badc3136fc10ff0bcfc98747eadbaf43ed8f214e07ba8f8477" +checksum = "fbb2fda4666def1433b1b05431ab402e42a1084285477222b72d6c564c417cef" dependencies = [ "bitflags", "errno", @@ -4028,9 +4123,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1847b767a3d62d95cbf3d8a9f0e421cf57a0d8aa4f411d4b16525afb0284d4ed" +checksum = "2a5fb6c61f29e723026dc8e923d94c694313212abbecbbe5f55a7748eec5b307" dependencies = [ "chrono", "dyn-clone", @@ -4042,9 +4137,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4d7e1b012cb3d9129567661a63755ea4b8a7386d339dc945ae187e403c6743" +checksum = "f188d036977451159430f3b8dc82ec76364a42b7e289c2b18a9a18f4470058e9" dependencies = [ "proc-macro2", "quote", @@ -4058,6 +4153,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scratch" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" + [[package]] name = "scrypt" version = "0.10.0" @@ -4179,11 +4280,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" dependencies = [ - "itoa 1.0.3", + "itoa 1.0.4", "ryu", "serde", ] @@ -4195,7 +4296,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.3", + "itoa 1.0.4", "ryu", "serde", ] @@ -4213,7 +4314,7 @@ dependencies = [ "serde", "serde_json", "serde_with_macros", - "time 0.3.14", + "time 0.3.15", ] [[package]] @@ -4235,7 +4336,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8613d593412a0deb7bbd8de9d908efff5a0cb9ccd8f62c641e7b2ed2f57291d1" dependencies = [ "indexmap", - "itoa 1.0.3", + "itoa 1.0.4", "ryu", "serde", "unsafe-libyaml", @@ -4295,9 +4396,9 @@ dependencies = [ [[package]] name = "signature" -version = "1.6.3" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb766570a2825fa972bceff0d195727876a9cdf2460ab2e52d455dc2de47fd9" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ "digest 0.10.5", "rand_core", @@ -4335,9 +4436,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" @@ -4420,7 +4521,7 @@ dependencies = [ "hkdf", "hmac", "indexmap", - "itoa 1.0.3", + "itoa 1.0.4", "libc", "log", "md-5", @@ -4532,9 +4633,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" +checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" dependencies = [ "proc-macro2", "quote", @@ -4662,11 +4763,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c" dependencies = [ - "itoa 1.0.3", + "itoa 1.0.4", "libc", "num_threads", "serde", @@ -4759,9 +4860,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" dependencies = [ "futures-core", "pin-project-lite", @@ -4809,9 +4910,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11cd56bdb54ef93935a6a79dbd1d91f1ebd4c64150fd61654031fd6b8b775c91" +checksum = "55b9af819e54b8f33d453655bef9b9acc171568fb49523078d0cc4e7484200ec" dependencies = [ "async-stream", "async-trait", @@ -4841,9 +4942,9 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fbcd2800e34e743b9ae795867d5f77b535d3a3be69fd731e39145719752df8c" +checksum = "48c6fd7c2581e36d63388a9e04c350c21beb7a8b059580b2e93993c526899ddc" dependencies = [ "prettyplease", "proc-macro2", @@ -4902,9 +5003,9 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" @@ -4914,9 +5015,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.36" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "log", @@ -4932,15 +5033,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d48f71a791638519505cefafe162606f706c25592e4bde4d97600c0195312e" dependencies = [ "crossbeam-channel", - "time 0.3.14", + "time 0.3.15", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", @@ -4949,9 +5050,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", "valuable", @@ -4994,12 +5095,12 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" dependencies = [ - "ansi_term", "matchers", + "nu-ansi-term", "once_cell", "regex", "sharded-slab", @@ -5124,9 +5225,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] name = "unicode-normalization" @@ -5197,9 +5298,9 @@ checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9" [[package]] name = "uuid" -version = "1.1.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" [[package]] name = "valuable" @@ -5314,9 +5415,9 @@ checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "wasm-encoder" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7ca71c70a6de5b10968ae4d298e548366d9cd9588176e6ff8866f3c49c96ee" +checksum = "c64ac98d5d61192cc45c701b7e4bd0b9aff91e2edfc7a088406cfe2288581e2c" dependencies = [ "leb128", ] @@ -5520,9 +5621,9 @@ dependencies = [ [[package]] name = "wast" -version = "47.0.0" +version = "47.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117ccfc4262e62a28a13f0548a147f19ffe71e8a08be802af23ae4ea0bedad73" +checksum = "02b98502f3978adea49551e801a6687678e6015317d7d9470a67fe813393f2a8" dependencies = [ "leb128", "memchr", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 6d24c971..119094ca 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -20,6 +20,7 @@ argon2 = { version = "0.4.1", features = ["password-hash"] } watchman_client = "0.8.0" atty = "0.2.14" listenfd = "1.0.0" +rustls = "0.20.6" tracing = "0.1.36" tracing-appender = "0.2.2" @@ -44,6 +45,7 @@ mas-static-files = { path = "../static-files" } mas-storage = { path = "../storage" } mas-tasks = { path = "../tasks" } mas-templates = { path = "../templates" } +mas-listener = { path = "../listener" } [dev-dependencies] indoc = "1.0.7" diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index c259d45f..e6af30ee 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{net::SocketAddr, sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration}; use anyhow::Context; use clap::Parser; use futures_util::{ - future::FutureExt, + future::{FutureExt, OptionFuture}, stream::{StreamExt, TryStreamExt}, }; use hyper::Server; @@ -25,6 +25,7 @@ use mas_config::RootConfig; use mas_email::Mailer; use mas_handlers::{AppState, MatrixHomeserver}; use mas_http::ServerLayer; +use mas_listener::{maybe_tls::MaybeTlsAcceptor, unix_or_tcp::UnixOrTcpListener}; use mas_policy::PolicyFactory; use mas_router::{Route, UrlBuilder}; use mas_storage::MIGRATOR; @@ -245,39 +246,103 @@ impl Options { }); let signal = shutdown_signal().shared(); + let shutdown_signal = signal.clone(); let mut fd_manager = listenfd::ListenFd::from_env(); - let futs = listeners_config - .into_iter() - .map(|listener_config| { + let listeners = listeners_config.into_iter().map(|listener_config| { + // We have to borrow it here, not in the nested closure + let fd_manager = &mut fd_manager; + + // Let's first grab all the listeners in a synchronous manner + // This helps with the fd_manager mutable borrow + let listeners: Result, _> = listener_config + .binds + .iter() + .map(move |bind_config| bind_config.listener(fd_manager)) + .collect(); + + Ok((listener_config, listeners?)) + }); + + // Now that we have the listeners ready, we can do the rest concurrently + futures_util::stream::iter(listeners) + .try_for_each_concurrent(None, move |(config, listeners)| { let signal = signal.clone(); - let router = mas_handlers::router(state.clone()) - .nest(mas_router::StaticAsset::route(), static_files.clone()) - .layer(ServerLayer::default()); - let mut futs: Vec<_> = Vec::with_capacity(listener_config.binds.len()); - for bind in listener_config.binds { - let listener = bind.listener(&mut fd_manager)?; - let router = router.clone(); + let mut router = mas_handlers::empty_router(state.clone()); - let addr = listener.local_addr()?; - info!("Listening on http://{addr}"); - - let fut = Server::from_tcp(listener)? - .serve(router.into_make_service_with_connect_info::()) - .with_graceful_shutdown(signal.clone()); - futs.push(fut); + for resource in config.resources { + router = match resource { + mas_config::HttpResource::Health => { + router.merge(mas_handlers::healthcheck_router(state.clone())) + } + mas_config::HttpResource::Discovery => { + router.merge(mas_handlers::discovery_router(state.clone())) + } + mas_config::HttpResource::Human => { + router.merge(mas_handlers::human_router(state.clone())) + } + mas_config::HttpResource::Static => { + router.nest(mas_router::StaticAsset::route(), static_files.clone()) + } + mas_config::HttpResource::OAuth => { + router.merge(mas_handlers::api_router(state.clone())) + } + mas_config::HttpResource::Compat => { + router.merge(mas_handlers::compat_router(state.clone())) + } + } } - anyhow::Ok(futures_util::future::try_join_all(futs)) - }) - .collect::, _>>()?; + let router = router.layer(ServerLayer::default()); - futures_util::future::try_join_all(futs).await?; + async move { + let tls_config: OptionFuture<_> = config + .tls + .map(|tls_config| async move { + let (key, chain) = tls_config.load().await?; + let key = rustls::PrivateKey(key); + let chain = chain.into_iter().map(rustls::Certificate).collect(); + let mut config = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(chain, key) + .context("failed to build TLS server config")?; + config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + anyhow::Ok(Arc::new(config)) + }) + .into(); + let tls_config = tls_config.await.transpose()?; + + futures_util::stream::iter(listeners) + .map(Ok) + .try_for_each_concurrent(None, move |listener| { + let listener = MaybeTlsAcceptor::new(tls_config.clone(), listener); + + // Unless there is something really bad happening, we should be able to + // grab the local_addr here. Panicking here if it is not the case is + // probably fine. + let addr = listener.local_addr().unwrap(); + if listener.is_secure() { + info!("Listening on https://{addr:?}"); + } else { + info!("Listening on http://{addr:?}"); + } + + Server::builder(listener) + .serve(router.clone().into_make_service()) + .with_graceful_shutdown(signal.clone()) + }) + .await?; + + anyhow::Ok(()) + } + }) + .await?; // This ensures we're running, even if no listener are setup // This is useful for only running the task runner - signal.await; + shutdown_signal.await; Ok(()) } diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index a9bad83b..496631d6 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -27,6 +27,7 @@ lettre = { version = "0.10.1", default-features = false, features = ["serde", "b listenfd = "1.0.0" pem-rfc7468 = "0.6.0" +rustls-pemfile = "1.0.1" rand = "0.8.5" indoc = "1.0.7" diff --git a/crates/config/src/sections/http.rs b/crates/config/src/sections/http.rs index da3b486c..df4db3c3 100644 --- a/crates/config/src/sections/http.rs +++ b/crates/config/src/sections/http.rs @@ -13,22 +13,23 @@ // limitations under the License. use std::{ - net::{SocketAddr, TcpListener}, + borrow::Cow, + io::Cursor, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener, ToSocketAddrs}, + ops::Deref, + os::unix::net::UnixListener, path::PathBuf, }; -use anyhow::Context; +use anyhow::{bail, Context}; use async_trait::async_trait; use listenfd::ListenFd; +use mas_keystore::PrivateKey; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use url::Url; -use super::ConfigurationSection; - -fn default_http_address() -> String { - "[::]:8080".into() -} +use super::{secrets::PasswordOrFile, ConfigurationSection}; fn default_public_base() -> Url { "http://[::]:8080".parse().unwrap() @@ -47,9 +48,31 @@ fn http_address_example_4() -> &'static str { "0.0.0.0:8080" } +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] +#[serde(rename_all = "lowercase")] +pub enum UnixOrTcp { + Unix, + Tcp, +} + +impl UnixOrTcp { + pub const fn unix() -> Self { + Self::Unix + } + + pub const fn tcp() -> Self { + Self::Tcp + } +} + #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] #[serde(untagged)] pub enum BindConfig { + Listen { + host: Option, + port: u16, + }, + Address { #[schemars( example = "http_address_example_1", @@ -59,39 +82,202 @@ pub enum BindConfig { )] address: String, }, + + Unix { + socket: PathBuf, + }, + FileDescriptor { fd: usize, + + #[serde(default = "UnixOrTcp::tcp")] + kind: UnixOrTcp, }, } impl BindConfig { - pub fn listener(self, fd_manager: &mut ListenFd) -> Result { + // TODO: move this somewhere else + pub fn listener(&self, fd_manager: &mut ListenFd) -> Result + where + T: TryFrom + TryFrom, + >::Error: std::error::Error + Sync + Send + 'static, + >::Error: std::error::Error + Sync + Send + 'static, + { match self { + BindConfig::Listen { host, port } => { + let addrs = match host.as_deref() { + Some(host) => (host, *port) + .to_socket_addrs() + .context("could not parse listener host")? + .collect(), + + None => vec![ + SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), *port), + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), *port), + ], + }; + + let listener = TcpListener::bind(&addrs[..]).context("could not bind address")?; + listener.set_nonblocking(true)?; + Ok(listener.try_into()?) + } + BindConfig::Address { address } => { let addr: SocketAddr = address .parse() .context("could not parse listener address")?; let listener = TcpListener::bind(addr).context("could not bind address")?; - Ok(listener) + listener.set_nonblocking(true)?; + Ok(listener.try_into()?) } - BindConfig::FileDescriptor { fd } => { - let listener = fd_manager - .take_tcp_listener(fd)? - .context("no listener found on file descriptor")?; - // XXX: Do I need that? + BindConfig::Unix { socket } => { + let listener = UnixListener::bind(socket).context("could not bind socket")?; listener.set_nonblocking(true)?; - Ok(listener) + Ok(listener.try_into()?) + } + + BindConfig::FileDescriptor { + fd, + kind: UnixOrTcp::Tcp, + } => { + let listener = fd_manager + .take_tcp_listener(*fd)? + .context("no listener found on file descriptor")?; + listener.set_nonblocking(true)?; + Ok(listener.try_into()?) + } + + BindConfig::FileDescriptor { + fd, + kind: UnixOrTcp::Unix, + } => { + let listener = fd_manager + .take_unix_listener(*fd)? + .context("no unix socket found on file descriptor")?; + listener.set_nonblocking(true)?; + Ok(listener.try_into()?) } } } } +#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub enum KeyOrFile { + Key(String), + KeyFile(PathBuf), +} + +#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub enum CertificateOrFile { + Certificate(String), + CertificateFile(PathBuf), +} + +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] +pub struct TlsConfig { + #[serde(flatten)] + pub certificate: CertificateOrFile, + + #[serde(flatten)] + pub key: KeyOrFile, + + #[serde(flatten)] + pub password: Option, +} + +impl TlsConfig { + pub async fn load(&self) -> Result<(Vec, Vec>), anyhow::Error> { + let password = match &self.password { + Some(PasswordOrFile::Password(password)) => Some(Cow::Borrowed(password.as_str())), + Some(PasswordOrFile::PasswordFile(path)) => { + Some(Cow::Owned(tokio::fs::read_to_string(path).await?)) + } + None => None, + }; + + // Read the key either embedded in the config file or on disk + let key = match &self.key { + KeyOrFile::Key(key) => { + // If the key was embedded in the config file, assume it is formatted as PEM + if let Some(password) = password { + PrivateKey::load_encrypted_pem(key, password.as_bytes())? + } else { + PrivateKey::load_pem(key)? + } + } + KeyOrFile::KeyFile(path) => { + // When reading from disk, it might be either PEM or DER. `PrivateKey::load*` + // will try both. + let key = tokio::fs::read(path).await?; + if let Some(password) = password { + PrivateKey::load_encrypted(&key, password.as_bytes())? + } else { + PrivateKey::load(&key)? + } + } + }; + + // Re-serialize the key to PKCS#8 DER, so rustls can consume it + let key = key.to_pkcs8_der()?; + // This extracts the Vec out of the Zeroizing by copying it + // XXX: maybe we should keep that zeroizing? + let key = key.deref().clone(); + + let certificate_chain_pem = match &self.certificate { + CertificateOrFile::Certificate(pem) => Cow::Borrowed(pem.as_str()), + CertificateOrFile::CertificateFile(path) => { + Cow::Owned(tokio::fs::read_to_string(path).await?) + } + }; + + let mut certificate_chain_reader = Cursor::new(certificate_chain_pem.as_bytes()); + let certificate_chain = rustls_pemfile::certs(&mut certificate_chain_reader)?; + + if certificate_chain.is_empty() { + bail!("TLS certificate chain is empty (or invalid)") + } + + Ok((key, certificate_chain)) + } +} + +/// HTTP resources to mount +#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] +#[serde(tag = "name", rename_all = "lowercase")] +pub enum Resource { + /// Healthcheck endpoint (/health) + Health, + + /// OIDC discovery endpoints + Discovery, + + /// Pages destined to be viewed by humans + Human, + + /// OAuth-related APIs + OAuth, + + /// Matrix compatibility API + Compat, + + /// Static files + Static, +} + +/// Configuration of a listener #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] pub struct ListenerConfig { - pub name: Option, + /// List of resources to mount + pub resources: Vec, + /// List of sockets to bind pub binds: Vec, + + /// If set, makes the listener use TLS with the provided certificate and key + pub tls: Option, } /// Configuration related to the web server @@ -114,12 +300,28 @@ impl Default for HttpConfig { fn default() -> Self { Self { web_root: None, - listeners: vec![ListenerConfig { - name: None, - binds: vec![BindConfig::Address { - address: default_http_address(), - }], - }], + listeners: vec![ + ListenerConfig { + resources: vec![ + Resource::Discovery, + Resource::Human, + Resource::OAuth, + Resource::Compat, + Resource::Static, + ], + tls: None, + binds: vec![BindConfig::Address { + address: "[::]:8080".into(), + }], + }, + ListenerConfig { + resources: vec![Resource::Health], + tls: None, + binds: vec![BindConfig::Address { + address: "localhost:8081".into(), + }], + }, + ], public_base: default_public_base(), } } diff --git a/crates/config/src/sections/mod.rs b/crates/config/src/sections/mod.rs index 2c9b5b4f..41bd8193 100644 --- a/crates/config/src/sections/mod.rs +++ b/crates/config/src/sections/mod.rs @@ -32,7 +32,7 @@ pub use self::{ csrf::CsrfConfig, database::DatabaseConfig, email::{EmailConfig, EmailSmtpMode, EmailTransportConfig}, - http::HttpConfig, + http::{HttpConfig, Resource as HttpResource}, matrix::MatrixConfig, policy::PolicyConfig, secrets::SecretsConfig, diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 577124ef..18aca49e 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -50,6 +50,57 @@ pub use compat::MatrixHomeserver; pub use self::app_state::AppState; +#[must_use] +pub fn empty_router(state: Arc) -> Router +where + B: HttpBody + Send + 'static, + S: Send + Sync + 'static, +{ + Router::with_state_arc(state) +} + +#[must_use] +pub fn healthcheck_router(state: Arc) -> Router +where + B: HttpBody + Send + 'static, + S: Send + Sync + 'static, + PgPool: FromRef, +{ + Router::with_state_arc(state).route(mas_router::Healthcheck::route(), get(self::health::get)) +} + +#[must_use] +pub fn discovery_router(state: Arc) -> Router +where + B: HttpBody + Send + 'static, + S: Send + Sync + 'static, + Keystore: FromRef, + UrlBuilder: FromRef, +{ + Router::with_state_arc(state) + .route( + mas_router::OidcConfiguration::route(), + get(self::oauth2::discovery::get), + ) + .route( + mas_router::Webfinger::route(), + get(self::oauth2::webfinger::get), + ) + .layer( + CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_otel_headers([ + AUTHORIZATION, + ACCEPT, + ACCEPT_LANGUAGE, + CONTENT_LANGUAGE, + CONTENT_TYPE, + ]) + .max_age(Duration::from_secs(60 * 60)), + ) +} + #[must_use] #[allow(clippy::trait_duplication_in_bounds)] pub fn api_router(state: Arc) -> Router @@ -66,19 +117,6 @@ where { // All those routes are API-like, with a common CORS layer Router::with_state_arc(state) - .route(mas_router::Healthcheck::route(), get(self::health::get)) - .route( - mas_router::ChangePasswordDiscovery::route(), - get(|| async { mas_router::AccountPassword.go() }), - ) - .route( - mas_router::OidcConfiguration::route(), - get(self::oauth2::discovery::get), - ) - .route( - mas_router::Webfinger::route(), - get(self::oauth2::webfinger::get), - ) .route( mas_router::OAuth2Keys::route(), get(self::oauth2::keys::get), @@ -116,6 +154,7 @@ where .max_age(Duration::from_secs(60 * 60)), ) } + #[must_use] #[allow(clippy::trait_duplication_in_bounds)] pub fn compat_router(state: Arc) -> Router @@ -174,6 +213,10 @@ where { let templates = Templates::from_ref(&state); Router::with_state_arc(state) + .route( + mas_router::ChangePasswordDiscovery::route(), + get(|| async { mas_router::AccountPassword.go() }), + ) .route(mas_router::Index::route(), get(self::views::index::get)) .route( mas_router::Login::route(), @@ -267,11 +310,18 @@ where Mailer: FromRef, MatrixHomeserver: FromRef, { + let healthcheck_router = healthcheck_router(state.clone()); + let discovery_router = discovery_router(state.clone()); let api_router = api_router(state.clone()); let compat_router = compat_router(state.clone()); - let human_router = human_router(state); + let human_router = human_router(state.clone()); - human_router.merge(api_router).merge(compat_router) + Router::with_state_arc(state) + .merge(healthcheck_router) + .merge(discovery_router) + .merge(human_router) + .merge(api_router) + .merge(compat_router) } #[cfg(test)] diff --git a/crates/keystore/src/lib.rs b/crates/keystore/src/lib.rs index 235768ac..798528ca 100644 --- a/crates/keystore/src/lib.rs +++ b/crates/keystore/src/lib.rs @@ -26,6 +26,7 @@ use std::{ops::Deref, sync::Arc}; use der::{zeroize::Zeroizing, Decode}; +use elliptic_curve::pkcs8::EncodePrivateKey; use mas_iana::jose::{JsonWebKeyType, JsonWebSignatureAlg}; pub use mas_jose::jwk::{JsonWebKey, JsonWebKeySet}; use mas_jose::{ @@ -213,6 +214,22 @@ impl PrivateKey { Ok(der) } + /// Serialize the key as a PKCS8 DER document + /// + /// # Errors + /// + /// Returns an error if the encoding failed + pub fn to_pkcs8_der(&self) -> Result>, anyhow::Error> { + let der = match self { + PrivateKey::Rsa(key) => key.to_pkcs8_der()?, + PrivateKey::EcP256(key) => key.to_pkcs8_der()?, + PrivateKey::EcP384(key) => key.to_pkcs8_der()?, + PrivateKey::EcK256(key) => key.to_pkcs8_der()?, + }; + + Ok(der.to_bytes()) + } + /// Serialize the key as a PEM document /// /// It will use the most common format depending on the key type: PKCS1 for diff --git a/crates/listener/Cargo.toml b/crates/listener/Cargo.toml new file mode 100644 index 00000000..17092d84 --- /dev/null +++ b/crates/listener/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "mas-listener" +version = "0.1.0" +authors = ["Quentin Gliech "] +edition = "2021" +license = "Apache-2.0" + +[dependencies] +tokio = { version = "1.21.2", features = ["net"] } +pin-project-lite = "0.2.9" +hyper = { version = "0.14.20", features = ["server"] } +futures-util = "0.3.24" +tracing = "0.1.36" +tokio-rustls = "0.23.4" diff --git a/crates/listener/src/lib.rs b/crates/listener/src/lib.rs new file mode 100644 index 00000000..ba110718 --- /dev/null +++ b/crates/listener/src/lib.rs @@ -0,0 +1,16 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod maybe_tls; +pub mod unix_or_tcp; diff --git a/crates/listener/src/maybe_tls.rs b/crates/listener/src/maybe_tls.rs new file mode 100644 index 00000000..2bd99d1b --- /dev/null +++ b/crates/listener/src/maybe_tls.rs @@ -0,0 +1,213 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + ops::Deref, + pin::Pin, + sync::Arc, + task::{ready, Context, Poll}, +}; + +use futures_util::Future; +use hyper::server::accept::Accept; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio_rustls::rustls::{ServerConfig, ServerConnection}; + +pub enum MaybeTlsStream { + Handshaking(tokio_rustls::Accept), + Streaming(tokio_rustls::server::TlsStream), + Insecure(T), +} + +impl MaybeTlsStream { + pub fn new(stream: T, config: Option>) -> Self + where + T: AsyncRead + AsyncWrite + Unpin, + { + if let Some(config) = config { + let accept = tokio_rustls::TlsAcceptor::from(config).accept(stream); + MaybeTlsStream::Handshaking(accept) + } else { + MaybeTlsStream::Insecure(stream) + } + } + + /// Get a reference to the underlying IO stream + /// + /// Returns [`None`] if the stream closed before the TLS handshake finished. + /// It is guaranteed to return [`Some`] value after the handshake finished, + /// or if it is a non-TLS connection. + pub fn get_ref(&self) -> Option<&T> { + match self { + Self::Handshaking(accept) => accept.get_ref(), + Self::Streaming(stream) => { + let (inner, _) = stream.get_ref(); + Some(inner) + } + Self::Insecure(inner) => Some(inner), + } + } + + /// Get a ref to the [`ServerConnection`] of the establish TLS stream. + /// + /// Returns [`None`] if the connection is still handshaking and for non-TLS + /// connections. + pub fn get_tls_connection(&self) -> Option<&ServerConnection> { + match self { + Self::Streaming(stream) => { + let (_, conn) = stream.get_ref(); + Some(conn) + } + Self::Handshaking(_) | Self::Insecure(_) => None, + } + } +} + +impl AsyncRead for MaybeTlsStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &mut ReadBuf, + ) -> Poll> { + let pin = self.get_mut(); + match pin { + MaybeTlsStream::Handshaking(ref mut accept) => { + match ready!(Pin::new(accept).poll(cx)) { + Ok(mut stream) => { + let result = Pin::new(&mut stream).poll_read(cx, buf); + *pin = MaybeTlsStream::Streaming(stream); + result + } + Err(err) => Poll::Ready(Err(err)), + } + } + MaybeTlsStream::Streaming(ref mut stream) => Pin::new(stream).poll_read(cx, buf), + MaybeTlsStream::Insecure(ref mut stream) => Pin::new(stream).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for MaybeTlsStream +where + T: AsyncRead + AsyncWrite + Unpin, +{ + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let pin = self.get_mut(); + match pin { + MaybeTlsStream::Handshaking(ref mut accept) => { + match ready!(Pin::new(accept).poll(cx)) { + Ok(mut stream) => { + let result = Pin::new(&mut stream).poll_write(cx, buf); + *pin = MaybeTlsStream::Streaming(stream); + result + } + Err(err) => Poll::Ready(Err(err)), + } + } + MaybeTlsStream::Streaming(ref mut stream) => Pin::new(stream).poll_write(cx, buf), + MaybeTlsStream::Insecure(ref mut fallback) => Pin::new(fallback).poll_write(cx, buf), + } + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.get_mut() { + MaybeTlsStream::Handshaking { .. } => Poll::Ready(Ok(())), + MaybeTlsStream::Streaming(ref mut stream) => Pin::new(stream).poll_flush(cx), + MaybeTlsStream::Insecure(ref mut stream) => Pin::new(stream).poll_flush(cx), + } + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.get_mut() { + MaybeTlsStream::Handshaking { .. } => Poll::Ready(Ok(())), + MaybeTlsStream::Streaming(ref mut stream) => Pin::new(stream).poll_shutdown(cx), + MaybeTlsStream::Insecure(ref mut stream) => Pin::new(stream).poll_shutdown(cx), + } + } +} + +pub struct MaybeTlsAcceptor { + tls_config: Option>, + incoming: T, +} + +impl MaybeTlsAcceptor { + pub fn new(tls_config: Option>, incoming: T) -> Self { + Self { + tls_config, + incoming, + } + } + + pub fn new_secure(tls_config: Arc, incoming: T) -> Self { + Self { + tls_config: Some(tls_config), + incoming, + } + } + + pub fn new_insecure(incoming: T) -> Self { + Self { + tls_config: None, + incoming, + } + } + + pub const fn is_secure(&self) -> bool { + self.tls_config.is_some() + } +} + +impl Accept for MaybeTlsAcceptor +where + T: Accept + Unpin, + T::Conn: AsyncRead + AsyncWrite + Unpin, + T::Error: Into, +{ + type Conn = MaybeTlsStream; + type Error = std::io::Error; + + fn poll_accept( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let pin = self.get_mut(); + + let ret = match ready!(Pin::new(&mut pin.incoming).poll_accept(cx)) { + Some(Ok(sock)) => { + let config = pin.tls_config.clone(); + Some(Ok(MaybeTlsStream::new(sock, config))) + } + + Some(Err(e)) => Some(Err(e.into())), + None => None, + }; + + Poll::Ready(ret) + } +} + +impl Deref for MaybeTlsAcceptor { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.incoming + } +} diff --git a/crates/listener/src/unix_or_tcp.rs b/crates/listener/src/unix_or_tcp.rs new file mode 100644 index 00000000..86f8e73c --- /dev/null +++ b/crates/listener/src/unix_or_tcp.rs @@ -0,0 +1,212 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// TODO: Unlink the UNIX socket on drop? +// TODO: Proxy protocol + +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use futures_util::ready; +use hyper::server::accept::Accept; +use tokio::{ + io::{AsyncRead, AsyncWrite}, + net::{TcpListener, TcpStream, UnixListener, UnixStream}, +}; + +pub enum SocketAddr { + Unix(tokio::net::unix::SocketAddr), + Net(std::net::SocketAddr), +} + +impl From for SocketAddr { + fn from(value: tokio::net::unix::SocketAddr) -> Self { + Self::Unix(value) + } +} + +impl From for SocketAddr { + fn from(value: std::net::SocketAddr) -> Self { + Self::Net(value) + } +} + +impl std::fmt::Debug for SocketAddr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Unix(l) => std::fmt::Debug::fmt(l, f), + Self::Net(l) => std::fmt::Debug::fmt(l, f), + } + } +} + +pub enum UnixOrTcpListener { + Unix(UnixListener), + Tcp(TcpListener), +} + +impl From for UnixOrTcpListener { + fn from(listener: UnixListener) -> Self { + Self::Unix(listener) + } +} + +impl From for UnixOrTcpListener { + fn from(listener: TcpListener) -> Self { + Self::Tcp(listener) + } +} + +impl TryFrom for UnixOrTcpListener { + type Error = std::io::Error; + + fn try_from(listener: std::os::unix::net::UnixListener) -> Result { + Ok(Self::Unix(UnixListener::from_std(listener)?)) + } +} + +impl TryFrom for UnixOrTcpListener { + type Error = std::io::Error; + + fn try_from(listener: std::net::TcpListener) -> Result { + Ok(Self::Tcp(TcpListener::from_std(listener)?)) + } +} + +impl UnixOrTcpListener { + pub fn local_addr(&self) -> Result { + match self { + Self::Unix(listener) => listener.local_addr().map(SocketAddr::from), + Self::Tcp(listener) => listener.local_addr().map(SocketAddr::from), + } + } +} + +pin_project_lite::pin_project! { + #[project = UnixOrTcpConnectionProj] + pub enum UnixOrTcpConnection { + Unix { + #[pin] + stream: UnixStream, + }, + + Tcp { + #[pin] + stream: TcpStream, + }, + } +} + +impl UnixOrTcpConnection { + pub fn local_addr(&self) -> Result { + match self { + Self::Unix { stream, .. } => stream.local_addr().map(SocketAddr::from), + Self::Tcp { stream, .. } => stream.local_addr().map(SocketAddr::from), + } + } + + pub fn peer_addr(&self) -> Result { + match self { + Self::Unix { stream, .. } => stream.peer_addr().map(SocketAddr::from), + Self::Tcp { stream, .. } => stream.peer_addr().map(SocketAddr::from), + } + } +} + +impl Accept for UnixOrTcpListener { + type Error = std::io::Error; + type Conn = UnixOrTcpConnection; + + fn poll_accept( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> std::task::Poll>> { + let conn = match &*self { + Self::Unix(listener) => { + let (stream, _remote_addr) = ready!(listener.poll_accept(cx))?; + UnixOrTcpConnection::Unix { stream } + } + + Self::Tcp(listener) => { + let (stream, _remote_addr) = ready!(listener.poll_accept(cx))?; + UnixOrTcpConnection::Tcp { stream } + } + }; + + Poll::Ready(Some(Ok(conn))) + } +} + +impl AsyncRead for UnixOrTcpConnection { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + match self.project() { + UnixOrTcpConnectionProj::Unix { stream } => stream.poll_read(cx, buf), + UnixOrTcpConnectionProj::Tcp { stream } => stream.poll_read(cx, buf), + } + } +} + +impl AsyncWrite for UnixOrTcpConnection { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match self.project() { + UnixOrTcpConnectionProj::Unix { stream } => stream.poll_write(cx, buf), + UnixOrTcpConnectionProj::Tcp { stream } => stream.poll_write(cx, buf), + } + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.project() { + UnixOrTcpConnectionProj::Unix { stream } => stream.poll_flush(cx), + UnixOrTcpConnectionProj::Tcp { stream } => stream.poll_flush(cx), + } + } + + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match self.project() { + UnixOrTcpConnectionProj::Unix { stream } => stream.poll_shutdown(cx), + UnixOrTcpConnectionProj::Tcp { stream } => stream.poll_shutdown(cx), + } + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> Poll> { + match self.project() { + UnixOrTcpConnectionProj::Unix { stream } => stream.poll_write_vectored(cx, bufs), + UnixOrTcpConnectionProj::Tcp { stream } => stream.poll_write_vectored(cx, bufs), + } + } + + fn is_write_vectored(&self) -> bool { + match self { + UnixOrTcpConnection::Unix { stream } => stream.is_write_vectored(), + UnixOrTcpConnection::Tcp { stream } => stream.is_write_vectored(), + } + } +}