diff --git a/.env b/.env index c7d64d4..a9465f8 100644 --- a/.env +++ b/.env @@ -1,5 +1,11 @@ HOSTNAME=localhost:8079 PORT=8079 +HTTPS=false +DEBUG=true RESTRICTED_MODE=true +VALIDATE_SIGNATURES=false API_TOKEN=kjsdhfkwjenrkajhsdakjsnd +FOOTER_BLURB="Opéré par @max" +LOCAL_DOMAINS="xolus.net" +LOCAL_BLURB="

Relais ActivityPub francophone

" # OPENTELEMETRY_URL=http://localhost:4317 diff --git a/Cargo.lock b/Cargo.lock index e38d3f2..5bd9064 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,14 +63,17 @@ dependencies = [ "actix-codec", "actix-rt", "actix-service", + "actix-tls", "actix-utils", "ahash", "base64", "bitflags", + "brotli", "bytes", "bytestring", "derive_more", "encoding_rs", + "flate2", "futures-core", "h2", "http", @@ -192,6 +195,7 @@ dependencies = [ "actix-rt", "actix-server", "actix-service", + "actix-tls", "actix-utils", "ahash", "bytes", @@ -251,13 +255,28 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "ammonia" version = "3.2.1" @@ -279,7 +298,7 @@ checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "ap-relay" -version = "0.3.50" +version = "0.3.66" dependencies = [ "activitystreams", "activitystreams-ext", @@ -300,13 +319,20 @@ dependencies = [ "futures-util", "http-signature-normalization-actix", "lru", + "metrics", + "metrics-util", "mime", + "minify-html", "opentelemetry", "opentelemetry-otlp", + "pin-project-lite", + "quanta", "rand", "rsa", "rsa-magic-public-key", "ructe", + "rustls", + "rustls-pemfile", "serde", "serde_json", "sha2", @@ -346,6 +372,12 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164" +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "async-mutex" version = "1.4.0" @@ -485,9 +517,9 @@ dependencies = [ [[package]] name = "background-jobs" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793a813f9145c5f3a27b8dcd834c0927de68bbd60d53a369e5894f3cc5759020" +checksum = "62dc7cfc967d6714768097a876ca2941a54a26976c6d3c95ea6da48974890970" dependencies = [ "background-jobs-actix", "background-jobs-core", @@ -495,15 +527,16 @@ dependencies = [ [[package]] name = "background-jobs-actix" -version = "0.13.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47263ad9c5679419347dae655c2fa2cba078b0eaa51ac758d4f0e9690c06910b" +checksum = "99f8bfe0a984c8d0bc7e67b376cc05e0b9015fdd3ee878900046120ef781c47e" dependencies = [ "actix-rt", "anyhow", "async-mutex", "async-trait", "background-jobs-core", + "metrics", "serde", "serde_json", "thiserror", @@ -515,14 +548,15 @@ dependencies = [ [[package]] name = "background-jobs-core" -version = "0.13.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e78e842fe2ae461319e3d1843c12e301630e65650332b02032ac70b0dfc66f" +checksum = "1274e49ae8eff1fc6b4943660e59ce2f2e13e65a23a707924a50a40c7b94fc4d" dependencies = [ "actix-rt", "anyhow", "async-trait", "event-listener", + "metrics", "serde", "serde_json", "thiserror", @@ -562,6 +596,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.3" @@ -581,6 +627,27 @@ dependencies = [ "cipher", ] +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.11.1" @@ -601,9 +668,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "bytestring" @@ -616,9 +683,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.76" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" [[package]] name = "cfg-if" @@ -692,7 +759,7 @@ dependencies = [ "async-trait", "json5", "lazy_static", - "nom", + "nom 7.1.1", "pathdiff", "ron", "rust-ini", @@ -780,9 +847,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.11" +version = "0.9.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" dependencies = [ "autocfg", "cfg-if", @@ -793,9 +860,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if", ] @@ -810,6 +877,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "css-minify" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692b185e3b7c9af96b3195f3021f53a931d896968ed2ad3fb1cdb6558b30c9ab" +dependencies = [ + "derive_more", + "indexmap", + "nom 6.1.2", +] + [[package]] name = "darling" version = "0.13.4" @@ -929,6 +1007,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "erasable" version = "1.2.1" @@ -995,6 +1079,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "futf" version = "0.1.5" @@ -1121,7 +1211,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -1161,7 +1251,7 @@ dependencies = [ "base64", "byteorder", "flate2", - "nom", + "nom 7.1.1", "num-traits", ] @@ -1233,9 +1323,9 @@ dependencies = [ [[package]] name = "http-signature-normalization-actix" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86dfd54a1764ad79376b8dbf29e5bf918a463eb5ec66c90cd0388508289af6f0" +checksum = "7483d0ee4d093fa4bfe5956cd405492c07808a5064a29cfe3960d474f21f39c2" dependencies = [ "actix-http", "actix-rt", @@ -1437,6 +1527,19 @@ dependencies = [ "spin", ] +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.137" @@ -1507,6 +1610,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "maplit" version = "1.0.2" @@ -1556,13 +1668,56 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" -version = "0.6.5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] +[[package]] +name = "metrics" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b9b8653cec6897f73b519a43fba5ee3d50f62fe9af80b428accdcc093b4a849" +dependencies = [ + "ahash", + "metrics-macros", + "portable-atomic", +] + +[[package]] +name = "metrics-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731f8ecebd9f3a4aa847dfe75455e4757a45da40a7793d2f0b1f9b6ed18b23f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "metrics-util" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d24dc2dbae22bff6f1f9326ffce828c9f07ef9cc1e8002e5279f845432a30a" +dependencies = [ + "aho-corasick", + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown", + "indexmap", + "metrics", + "num_cpus", + "ordered-float", + "parking_lot 0.12.1", + "portable-atomic", + "quanta", + "radix_trie", + "sketches-ddsketch", +] + [[package]] name = "mime" version = "0.3.16" @@ -1579,6 +1734,29 @@ dependencies = [ "unicase", ] +[[package]] +name = "minify-html" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f84854d62363972a73c3d8331b85a479366a0871a83f2a01ac11b9ba787c10" +dependencies = [ + "aho-corasick", + "css-minify", + "lazy_static", + "memchr", + "minify-js", +] + +[[package]] +name = "minify-js" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe033709f5a1159736cf7e22748518ffb75af26f3a6264d52ecc8bb38c68c36" +dependencies = [ + "lazy_static", + "parse-js", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1602,7 +1780,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys", ] @@ -1624,6 +1802,28 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nom" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "nom" version = "7.1.1" @@ -1642,7 +1842,7 @@ checksum = "37794436ca3029a3089e0b95d42da1f0b565ad271e4d3bb4bad0c7bb70b10605" dependencies = [ "bytecount", "memchr", - "nom", + "nom 7.1.1", ] [[package]] @@ -1822,6 +2022,15 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "ordered-float" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7940cf2ca942593318d07fcf2596cdca60a85c9e7fab408a5e21a4f9dcd40d87" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.4.3" @@ -1834,9 +2043,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.4.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5bf27447411e9ee3ff51186bf7a08e16c341efdde93f4d823e8844429bed7e" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "overload" @@ -1892,6 +2101,17 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "parse-js" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bb85ec60d22b9e6d4adac1e3dbdaf3903a4485f476c5f4dd7ed1285cbf4dad" +dependencies = [ + "aho-corasick", + "lazy_static", + "memchr", +] + [[package]] name = "paste" version = "1.0.9" @@ -2065,6 +2285,12 @@ dependencies = [ "spki", ] +[[package]] +name = "portable-atomic" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15eb2c6e362923af47e13c23ca5afb859e83d54452c55b0b9ac763b8f7c1ac16" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2175,6 +2401,22 @@ dependencies = [ "prost", ] +[[package]] +name = "quanta" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e31331286705f455e56cca62e0e717158474ff02b7936c1fa596d983f4ae27" +dependencies = [ + "crossbeam-utils", + "libc", + "mach", + "once_cell", + "raw-cpuid", + "wasi 0.10.2+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quote" version = "1.0.21" @@ -2184,6 +2426,22 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.8.5" @@ -2214,6 +2472,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "raw-cpuid" +version = "10.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6823ea29436221176fe662da99998ad3b4db2c7f31e7b6f5fe43adccd6320bb" +dependencies = [ + "bitflags", +] + [[package]] name = "rc-box" version = "1.2.0" @@ -2376,7 +2643,7 @@ dependencies = [ "arc-swap", "fastrand", "lazy_static", - "nom", + "nom 7.1.1", "nom_locate", "num-bigint", "num-integer", @@ -2396,7 +2663,7 @@ dependencies = [ "itertools 0.10.5", "md5", "mime", - "nom", + "nom 7.1.1", "rsass", ] @@ -2490,9 +2757,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa", "ryu", @@ -2579,6 +2846,12 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" +[[package]] +name = "sketches-ddsketch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceb945e54128e09c43d8e4f1277851bd5044c6fc540bbaa2ad888f60b3da9ae7" + [[package]] name = "slab" version = "0.4.7" @@ -2636,6 +2909,12 @@ dependencies = [ "der", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "string_cache" version = "0.8.4" @@ -2703,6 +2982,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "teloxide" version = "0.11.2" @@ -3287,6 +3572,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3496,6 +3787,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index dcddf46..fbc9e5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ap-relay" description = "A simple activitypub relay" -version = "0.3.50" +version = "0.3.66" authors = ["asonix "] license = "AGPL-3.0" readme = "README.md" @@ -23,7 +23,11 @@ default = [] [dependencies] anyhow = "1.0" actix-rt = "2.7.0" -actix-web = { version = "4.0.1", default-features = false } +actix-web = { version = "4.0.1", default-features = false, features = [ + "rustls", + "compress-brotli", + "compress-gzip", +] } actix-webfinger = "0.4.0" activitystreams = "0.7.0-alpha.19" activitystreams-ext = "0.1.0-alpha.2" @@ -38,12 +42,19 @@ dashmap = "5.1.0" dotenv = "0.15.0" futures-util = "0.3.17" lru = "0.8.0" +metrics = "0.20.1" +metrics-util = "0.14.0" mime = "0.3.16" +minify-html = "0.10.0" opentelemetry = { version = "0.18", features = ["rt-tokio"] } opentelemetry-otlp = "0.11" +pin-project-lite = "0.2.9" +quanta = "0.10.1" rand = "0.8" rsa = "0.7" rsa-magic-public-key = "0.6.0" +rustls = "0.20.7" +rustls-pemfile = "1.0.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = { version = "0.10", features = ["oid"] } @@ -70,7 +81,7 @@ tokio = { version = "1", features = ["macros", "sync"] } uuid = { version = "1", features = ["v4", "serde"] } [dependencies.background-jobs] -version = "0.13.0" +version = "0.14.0" default-features = false features = ["background-jobs-actix", "error-logging"] diff --git a/README.md b/README.md index 1506ca2..64d873e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ $ sudo docker run --rm -it \ -e ADDR=0.0.0.0 \ -e SLED_PATH=/mnt/sled/db-0.34 \ -p 8080:8080 \ - asonix/relay:0.3.23 + asonix/relay:0.3.52 ``` This will launch the relay with the database stored in "./sled/db-0.34" and listening on port 8080 #### Cargo @@ -98,6 +98,11 @@ API_TOKEN=somepasswordishtoken OPENTELEMETRY_URL=localhost:4317 TELEGRAM_TOKEN=secret TELEGRAM_ADMIN_HANDLE=your_handle +TLS_KEY=/path/to/key +TLS_CERT=/path/to/cert +FOOTER_BLURB="Contact @asonix for inquiries" +LOCAL_DOMAINS=masto.asonix.dog +LOCAL_BLURB="

Welcome to my cool relay where I have cool relay things happening. I hope you enjoy your stay!

" ``` #### Descriptions @@ -112,15 +117,15 @@ Whether to print incoming activities to the console when requests hit the /inbox ##### `RESTRICTED_MODE` This setting enables an 'allowlist' setup where only servers that have been explicitly enabled through the `relay -a` command can join the relay. This is `false` by default. If `RESTRICTED_MODE` is not enabled, then manually allowing domains with `relay -a` has no effect. ##### `VALIDATE_SIGNATURES` -This setting enforces checking HTTP signatures on incoming activities. It defaults to `false` but should be set to `true` in production scenarios +This setting enforces checking HTTP signatures on incoming activities. It defaults to `true` ##### `HTTPS` -Whether the current server is running on an HTTPS port or not. This is used for generating URLs to the current running relay. By default it is set to `false`, but should be `true` in production scenarios. +Whether the current server is running on an HTTPS port or not. This is used for generating URLs to the current running relay. By default it is set to `true` ##### `PUBLISH_BLOCKS` Whether or not to publish a list of blocked domains in the `nodeinfo` metadata for the server. It defaults to `false`. ##### `SLED_PATH` Where to store the on-disk database of connected servers. This defaults to `./sled/db-0.34`. ##### `RUST_LOG` -The log level to print. Available levels are `ERROR`, `WARN`, `INFO`, `DEBUG`, and `TRACE`. You can also specify module paths to enable some logs but not others, such as `RUST_LOG=warn,tracing_actix_web=info,relay=info` +The log level to print. Available levels are `ERROR`, `WARN`, `INFO`, `DEBUG`, and `TRACE`. You can also specify module paths to enable some logs but not others, such as `RUST_LOG=warn,tracing_actix_web=info,relay=info`. This defaults to `warn` ##### `SOURCE_REPO` The URL to the source code for the relay. This defaults to `https://git.asonix.dog/asonix/relay`, but should be changed if you're running a fork hosted elsewhere. ##### `API_TOKEN` @@ -131,6 +136,16 @@ A URL for exporting opentelemetry spans. This is mostly useful for debugging. Th A Telegram Bot Token for running the relay administration bot. There is no default. ##### `TELEGRAM_ADMIN_HANDLE` The handle of the telegram user allowed to administer the relay. There is no default. +##### `TLS_KEY` +Optional - This is specified if you are running the relay directly on the internet and have a TLS key to provide HTTPS for your relay +##### `TLS_CERT` +Optional - This is specified if you are running the relay directly on the internet and have a TLS certificate chain to provide HTTPS for your relay +##### `FOOTER_BLURB` +Optional - Add custom notes in the footer of the page +##### `LOCAL_DOMAINS` +Optional - domains of mastodon servers run by the same admin as the relay +##### `LOCAL_BLURB` +Optional - description for the relay ### Subscribing Mastodon admins can subscribe to this relay by adding the `/inbox` route to their relay settings. diff --git a/scss/index.scss b/scss/index.scss index b9f7fcf..b7e85bc 100644 --- a/scss/index.scss +++ b/scss/index.scss @@ -41,7 +41,7 @@ header { } } -section { +article { background-color: #fff; color: #333; border: 1px solid #e5e5e5; @@ -51,8 +51,16 @@ section { max-width: 700px; padding-bottom: 32px; - > p:first-child { - margin-top: 0; + section { + border-bottom: 1px solid #e5e5e5; + + > h4:first-child, + > p:first-child { + margin-top: 0; + } + > p:last-child { + margin-bottom: 0; + } } h3 { @@ -67,13 +75,13 @@ section { li { padding-top: 36px; - border-bottom: 1px solid #e5e5e5; } .padded { padding: 0 24px; } + .local-explainer, .joining { padding: 24px; } @@ -174,9 +182,11 @@ footer { li { padding: 0; - border-bottom: none; } } + article section { + border-bottom: none; + } } } @@ -241,7 +251,7 @@ footer { padding: 24px; } - section { + article { border-left: none; border-right: none; border-radius: 0; diff --git a/src/admin/client.rs b/src/admin/client.rs index f63151f..3602487 100644 --- a/src/admin/client.rs +++ b/src/admin/client.rs @@ -1,5 +1,6 @@ use crate::{ admin::{AllowedDomains, BlockedDomains, ConnectedActors, Domains}, + collector::Snapshot, config::{AdminUrlKind, Config}, error::{Error, ErrorKind}, }; @@ -50,6 +51,10 @@ pub(crate) async fn connected(client: &Client, config: &Config) -> Result Result { + get_results(client, config, AdminUrlKind::Stats).await +} + async fn get_results( client: &Client, config: &Config, diff --git a/src/admin/routes.rs b/src/admin/routes.rs index c33efca..6578bfd 100644 --- a/src/admin/routes.rs +++ b/src/admin/routes.rs @@ -1,9 +1,13 @@ use crate::{ admin::{AllowedDomains, BlockedDomains, ConnectedActors, Domains}, + collector::{MemoryCollector, Snapshot}, error::Error, extractors::Admin, }; -use actix_web::{web::Json, HttpResponse}; +use actix_web::{ + web::{Data, Json}, + HttpResponse, +}; pub(crate) async fn allow( admin: Admin, @@ -58,3 +62,10 @@ pub(crate) async fn connected(admin: Admin) -> Result, Err Ok(Json(ConnectedActors { connected_actors })) } + +pub(crate) async fn stats( + _admin: Admin, + collector: Data, +) -> Result, Error> { + Ok(Json(collector.snapshot())) +} diff --git a/src/args.rs b/src/args.rs index 6b6c054..18a4059 100644 --- a/src/args.rs +++ b/src/args.rs @@ -14,11 +14,14 @@ pub(crate) struct Args { #[arg(short, long, help = "List allowed and blocked domains")] list: bool, + + #[arg(short, long, help = "Get statistics from the server")] + stats: bool, } impl Args { pub(crate) fn any(&self) -> bool { - !self.blocks.is_empty() || !self.allowed.is_empty() || self.list + !self.blocks.is_empty() || !self.allowed.is_empty() || self.list || self.stats } pub(crate) fn new() -> Self { @@ -40,4 +43,8 @@ impl Args { pub(crate) fn list(&self) -> bool { self.list } + + pub(crate) fn stats(&self) -> bool { + self.stats + } } diff --git a/src/collector.rs b/src/collector.rs new file mode 100644 index 0000000..0d3536d --- /dev/null +++ b/src/collector.rs @@ -0,0 +1,414 @@ +use metrics::{Key, Recorder, SetRecorderError}; +use metrics_util::{ + registry::{AtomicStorage, GenerationalStorage, Recency, Registry}, + MetricKindMask, Summary, +}; +use quanta::Clock; +use std::{ + collections::{BTreeMap, HashMap}, + sync::{atomic::Ordering, Arc, RwLock}, + time::Duration, +}; + +const SECONDS: u64 = 1; +const MINUTES: u64 = 60 * SECONDS; +const HOURS: u64 = 60 * MINUTES; +const DAYS: u64 = 24 * HOURS; + +type DistributionMap = BTreeMap, Summary>; + +#[derive(Clone)] +pub struct MemoryCollector { + inner: Arc, +} + +struct Inner { + descriptions: RwLock>, + distributions: RwLock>, + recency: Recency, + registry: Registry>, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct Counter { + labels: BTreeMap, + value: u64, +} + +impl std::fmt::Display for Counter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let labels = self + .labels + .iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join(", "); + + write!(f, "{} - {}", labels, self.value) + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct Gauge { + labels: BTreeMap, + value: f64, +} + +impl std::fmt::Display for Gauge { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let labels = self + .labels + .iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join(", "); + + write!(f, "{} - {}", labels, self.value) + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +struct Histogram { + labels: BTreeMap, + value: Vec<(f64, Option)>, +} + +impl std::fmt::Display for Histogram { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let labels = self + .labels + .iter() + .map(|(k, v)| format!("{}: {}", k, v)) + .collect::>() + .join(", "); + + let value = self + .value + .iter() + .map(|(k, v)| { + if let Some(v) = v { + format!("{}: {:.6}", k, v) + } else { + format!("{}: None,", k) + } + }) + .collect::>() + .join(", "); + + write!(f, "{} - {}", labels, value) + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub(crate) struct Snapshot { + counters: HashMap>, + gauges: HashMap>, + histograms: HashMap>, +} + +const PAIRS: [((&str, &str), &str); 2] = [ + ( + ( + "background-jobs.worker.started", + "background-jobs.worker.finished", + ), + "background-jobs.worker.running", + ), + ( + ( + "background-jobs.job.started", + "background-jobs.job.finished", + ), + "background-jobs.job.running", + ), +]; + +#[derive(Default)] +struct MergeCounter { + start: Option, + finish: Option, +} + +impl MergeCounter { + fn merge(self) -> Option { + match (self.start, self.finish) { + (Some(start), Some(end)) => Some(Counter { + labels: start.labels, + value: start.value.saturating_sub(end.value), + }), + (Some(only), None) => Some(only), + (None, Some(only)) => Some(Counter { + labels: only.labels, + value: 0, + }), + (None, None) => None, + } + } +} + +impl Snapshot { + pub(crate) fn present(self) { + if !self.counters.is_empty() { + println!("Counters"); + let mut merging = HashMap::new(); + for (key, counters) in self.counters { + if let Some(((start, _), name)) = PAIRS + .iter() + .find(|((start, finish), _)| *start == key || *finish == key) + { + let entry = merging.entry(name).or_insert_with(HashMap::new); + + for counter in counters { + let mut merge_counter = entry + .entry(counter.labels.clone()) + .or_insert_with(MergeCounter::default); + if key == *start { + merge_counter.start = Some(counter); + } else { + merge_counter.finish = Some(counter); + } + } + + continue; + } + + println!("\t{}", key); + for counter in counters { + println!("\t\t{}", counter); + } + } + + for (key, counters) in merging { + println!("\t{}", key); + + for (_, counter) in counters { + if let Some(counter) = counter.merge() { + println!("\t\t{}", counter); + } + } + } + } + + if !self.gauges.is_empty() { + println!("Gauges"); + for (key, gauges) in self.gauges { + println!("\t{}", key); + + for gauge in gauges { + println!("\t\t{}", gauge); + } + } + } + + if !self.histograms.is_empty() { + println!("Histograms"); + for (key, histograms) in self.histograms { + println!("\t{}", key); + + for histogram in histograms { + println!("\t\t{}", histogram); + } + } + } + } +} + +fn key_to_parts(key: &Key) -> (String, Vec<(String, String)>) { + let labels = key + .labels() + .into_iter() + .map(|label| (label.key().to_string(), label.value().to_string())) + .collect(); + let name = key.name().to_string(); + (name, labels) +} + +impl Inner { + fn snapshot_counters(&self) -> HashMap> { + let mut counters = HashMap::new(); + + for (key, counter) in self.registry.get_counter_handles() { + let gen = counter.get_generation(); + if !self.recency.should_store_counter(&key, gen, &self.registry) { + continue; + } + + let (name, labels) = key_to_parts(&key); + let value = counter.get_inner().load(Ordering::Acquire); + counters.entry(name).or_insert_with(Vec::new).push(Counter { + labels: labels.into_iter().collect(), + value, + }); + } + + counters + } + + fn snapshot_gauges(&self) -> HashMap> { + let mut gauges = HashMap::new(); + + for (key, gauge) in self.registry.get_gauge_handles() { + let gen = gauge.get_generation(); + if !self.recency.should_store_gauge(&key, gen, &self.registry) { + continue; + } + + let (name, labels) = key_to_parts(&key); + let value = f64::from_bits(gauge.get_inner().load(Ordering::Acquire)); + gauges.entry(name).or_insert_with(Vec::new).push(Gauge { + labels: labels.into_iter().collect(), + value, + }) + } + + gauges + } + + fn snapshot_histograms(&self) -> HashMap> { + for (key, histogram) in self.registry.get_histogram_handles() { + let gen = histogram.get_generation(); + let (name, labels) = key_to_parts(&key); + + if !self + .recency + .should_store_histogram(&key, gen, &self.registry) + { + let mut d = self.distributions.write().unwrap(); + let delete_by_name = if let Some(by_name) = d.get_mut(&name) { + by_name.remove(&labels); + by_name.is_empty() + } else { + false + }; + drop(d); + + if delete_by_name { + self.descriptions.write().unwrap().remove(&name); + } + + continue; + } + + let mut d = self.distributions.write().unwrap(); + let outer_entry = d.entry(name.clone()).or_insert_with(BTreeMap::new); + + let entry = outer_entry + .entry(labels) + .or_insert_with(Summary::with_defaults); + + histogram.get_inner().clear_with(|samples| { + for sample in samples { + entry.add(*sample); + } + }) + } + + let d = self.distributions.read().unwrap().clone(); + d.into_iter() + .map(|(key, value)| { + ( + key, + value + .into_iter() + .map(|(labels, summary)| Histogram { + labels: labels.into_iter().collect(), + value: [0.001, 0.01, 0.05, 0.1, 0.5, 0.9, 0.99, 1.0] + .into_iter() + .map(|q| (q, summary.quantile(q))) + .collect(), + }) + .collect(), + ) + }) + .collect() + } + + fn snapshot(&self) -> Snapshot { + Snapshot { + counters: self.snapshot_counters(), + gauges: self.snapshot_gauges(), + histograms: self.snapshot_histograms(), + } + } +} + +impl MemoryCollector { + pub(crate) fn new() -> Self { + MemoryCollector { + inner: Arc::new(Inner { + descriptions: Default::default(), + distributions: Default::default(), + recency: Recency::new( + Clock::new(), + MetricKindMask::ALL, + Some(Duration::from_secs(5 * DAYS)), + ), + registry: Registry::new(GenerationalStorage::atomic()), + }), + } + } + + pub(crate) fn install(&self) -> Result<(), SetRecorderError> { + metrics::set_boxed_recorder(Box::new(self.clone())) + } + + pub(crate) fn snapshot(&self) -> Snapshot { + self.inner.snapshot() + } + + fn add_description_if_missing( + &self, + key: &metrics::KeyName, + description: metrics::SharedString, + ) { + let mut d = self.inner.descriptions.write().unwrap(); + d.entry(key.as_str().to_owned()).or_insert(description); + } +} + +impl Recorder for MemoryCollector { + fn describe_counter( + &self, + key: metrics::KeyName, + _: Option, + description: metrics::SharedString, + ) { + self.add_description_if_missing(&key, description) + } + + fn describe_gauge( + &self, + key: metrics::KeyName, + _: Option, + description: metrics::SharedString, + ) { + self.add_description_if_missing(&key, description) + } + + fn describe_histogram( + &self, + key: metrics::KeyName, + _: Option, + description: metrics::SharedString, + ) { + self.add_description_if_missing(&key, description) + } + + fn register_counter(&self, key: &Key) -> metrics::Counter { + self.inner + .registry + .get_or_create_counter(key, |c| c.clone().into()) + } + + fn register_gauge(&self, key: &Key) -> metrics::Gauge { + self.inner + .registry + .get_or_create_gauge(key, |c| c.clone().into()) + } + + fn register_histogram(&self, key: &Key) -> metrics::Histogram { + self.inner + .registry + .get_or_create_histogram(key, |c| c.clone().into()) + } +} diff --git a/src/config.rs b/src/config.rs index 284f807..1d4cf86 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,8 +14,9 @@ use activitystreams::{ }; use config::Environment; use http_signature_normalization_actix::prelude::{VerifyDigest, VerifySignature}; +use rustls::{Certificate, PrivateKey}; use sha2::{Digest, Sha256}; -use std::{net::IpAddr, path::PathBuf}; +use std::{io::BufReader, net::IpAddr, path::PathBuf}; use uuid::Uuid; #[derive(Clone, Debug, serde::Deserialize)] @@ -34,6 +35,11 @@ pub(crate) struct ParsedConfig { telegram_token: Option, telegram_admin_handle: Option, api_token: Option, + tls_key: Option, + tls_cert: Option, + footer_blurb: Option, + local_domains: Option, + local_blurb: Option, } #[derive(Clone)] @@ -52,6 +58,16 @@ pub struct Config { telegram_token: Option, telegram_admin_handle: Option, api_token: Option, + tls: Option, + footer_blurb: Option, + local_domains: Vec, + local_blurb: Option, +} + +#[derive(Clone)] +struct TlsConfig { + key: PathBuf, + cert: PathBuf, } #[derive(Debug)] @@ -77,6 +93,7 @@ pub enum AdminUrlKind { Allowed, Blocked, Connected, + Stats, } impl std::fmt::Debug for Config { @@ -99,6 +116,11 @@ impl std::fmt::Debug for Config { .field("telegram_token", &"[redacted]") .field("telegram_admin_handle", &self.telegram_admin_handle) .field("api_token", &"[redacted]") + .field("tls_key", &"[redacted]") + .field("tls_cert", &"[redacted]") + .field("footer_blurb", &self.footer_blurb) + .field("local_domains", &self.local_domains) + .field("local_blurb", &self.local_blurb) .finish() } } @@ -111,8 +133,8 @@ impl Config { .set_default("port", 8080u64)? .set_default("debug", true)? .set_default("restricted_mode", false)? - .set_default("validate_signatures", false)? - .set_default("https", false)? + .set_default("validate_signatures", true)? + .set_default("https", true)? .set_default("publish_blocks", false)? .set_default("sled_path", "./sled/db-0-34")? .set_default("source_repo", "https://git.asonix.dog/asonix/relay")? @@ -120,6 +142,11 @@ impl Config { .set_default("telegram_token", None as Option<&str>)? .set_default("telegram_admin_handle", None as Option<&str>)? .set_default("api_token", None as Option<&str>)? + .set_default("tls_key", None as Option<&str>)? + .set_default("tls_cert", None as Option<&str>)? + .set_default("footer_blurb", None as Option<&str>)? + .set_default("local_domains", None as Option<&str>)? + .set_default("local_blurb", None as Option<&str>)? .add_source(Environment::default()) .build()?; @@ -128,6 +155,26 @@ impl Config { let scheme = if config.https { "https" } else { "http" }; let base_uri = iri!(format!("{}://{}", scheme, config.hostname)).into_absolute(); + let tls = match (config.tls_key, config.tls_cert) { + (Some(key), Some(cert)) => Some(TlsConfig { key, cert }), + (Some(_), None) => { + tracing::warn!("TLS_KEY is set but TLS_CERT isn't , not building TLS config"); + None + } + (None, Some(_)) => { + tracing::warn!("TLS_CERT is set but TLS_KEY isn't , not building TLS config"); + None + } + (None, None) => None, + }; + + let local_domains = config + .local_domains + .iter() + .flat_map(|s| s.split(',')) + .map(|d| d.to_string()) + .collect(); + Ok(Config { hostname: config.hostname, addr: config.addr, @@ -143,9 +190,76 @@ impl Config { telegram_token: config.telegram_token, telegram_admin_handle: config.telegram_admin_handle, api_token: config.api_token, + tls, + footer_blurb: config.footer_blurb, + local_domains, + local_blurb: config.local_blurb, }) } + pub(crate) fn open_keys(&self) -> Result, PrivateKey)>, Error> { + let tls = if let Some(tls) = &self.tls { + tls + } else { + tracing::warn!("No TLS config present"); + return Ok(None); + }; + + let mut certs_reader = BufReader::new(std::fs::File::open(&tls.cert)?); + let certs = rustls_pemfile::certs(&mut certs_reader)?; + + if certs.is_empty() { + tracing::warn!("No certs read from certificate file"); + return Ok(None); + } + + let mut key_reader = BufReader::new(std::fs::File::open(&tls.key)?); + let key = rustls_pemfile::read_one(&mut key_reader)?; + + let certs = certs.into_iter().map(Certificate).collect(); + + let key = if let Some(key) = key { + match key { + rustls_pemfile::Item::RSAKey(der) => PrivateKey(der), + rustls_pemfile::Item::PKCS8Key(der) => PrivateKey(der), + rustls_pemfile::Item::ECKey(der) => PrivateKey(der), + _ => { + tracing::warn!("Unknown key format: {:?}", key); + return Ok(None); + } + } + } else { + tracing::warn!("Failed to read private key"); + return Ok(None); + }; + + Ok(Some((certs, key))) + } + + pub(crate) fn footer_blurb(&self) -> Option> { + if let Some(blurb) = &self.footer_blurb { + if !blurb.is_empty() { + return Some(crate::templates::Html(ammonia::clean(blurb))); + } + } + + None + } + + pub(crate) fn local_blurb(&self) -> Option> { + if let Some(blurb) = &self.local_blurb { + if !blurb.is_empty() { + return Some(crate::templates::Html(ammonia::clean(blurb))); + } + } + + None + } + + pub(crate) fn local_domains(&self) -> &[String] { + &self.local_domains + } + pub(crate) fn sled_path(&self) -> &PathBuf { &self.sled_path } @@ -338,6 +452,8 @@ impl Config { .try_resolve(IriRelativeStr::new("api/v1/admin/blocked")?.as_ref())?, AdminUrlKind::Connected => FixedBaseResolver::new(self.base_uri.as_ref()) .try_resolve(IriRelativeStr::new("api/v1/admin/connected")?.as_ref())?, + AdminUrlKind::Stats => FixedBaseResolver::new(self.base_uri.as_ref()) + .try_resolve(IriRelativeStr::new("api/v1/admin/stats")?.as_ref())?, }; Ok(iri) diff --git a/src/data/mod.rs b/src/data.rs similarity index 100% rename from src/data/mod.rs rename to src/data.rs diff --git a/src/error.rs b/src/error.rs index 93c3a29..8fcd05d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -100,6 +100,12 @@ pub(crate) enum ErrorKind { #[error("Couldn't sign digest")] Signature(#[from] signature::Error), + #[error("Couldn't read signature")] + ReadSignature(signature::Error), + + #[error("Couldn't verify signature")] + VerifySignature(signature::Error), + #[error("Couldn't parse the signature header")] HeaderValidation(#[from] actix_web::http::header::InvalidHeaderValue), diff --git a/src/extractors.rs b/src/extractors.rs index d6cf1b1..572da34 100644 --- a/src/extractors.rs +++ b/src/extractors.rs @@ -11,7 +11,7 @@ use actix_web::{ use bcrypt::{BcryptError, DEFAULT_COST}; use futures_util::future::LocalBoxFuture; use http_signature_normalization_actix::prelude::InvalidHeaderValue; -use std::{convert::Infallible, str::FromStr}; +use std::{convert::Infallible, str::FromStr, time::Instant}; use tracing_error::SpanTrace; use crate::db::Db; @@ -61,7 +61,8 @@ impl Admin { hashed_api_token: Data, x_api_token: XApiToken, ) -> Result<(), Error> { - if actix_web::web::block(move || hashed_api_token.verify(x_api_token)) + let span = tracing::Span::current(); + if actix_web::web::block(move || span.in_scope(|| hashed_api_token.verify(x_api_token))) .await .map_err(Error::canceled)?? { @@ -178,10 +179,15 @@ impl FromRequest for Admin { type Future = LocalBoxFuture<'static, Result>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + let now = Instant::now(); let res = Self::prepare_verify(req); Box::pin(async move { let (db, c, t) = res?; Self::verify(c, t).await?; + metrics::histogram!( + "relay.admin.verify", + now.elapsed().as_micros() as f64 / 1_000_000_f64 + ); Ok(Admin { db }) }) } diff --git a/src/jobs/mod.rs b/src/jobs.rs similarity index 91% rename from src/jobs/mod.rs rename to src/jobs.rs index 6dfdbe9..014bd72 100644 --- a/src/jobs/mod.rs +++ b/src/jobs.rs @@ -7,8 +7,8 @@ mod nodeinfo; mod process_listeners; pub(crate) use self::{ - contact::QueryContact, deliver::Deliver, deliver_many::DeliverMany, - instance::QueryInstance, nodeinfo::QueryNodeinfo, + contact::QueryContact, deliver::Deliver, deliver_many::DeliverMany, instance::QueryInstance, + nodeinfo::QueryNodeinfo, }; use crate::{ @@ -22,7 +22,7 @@ use background_jobs::{ memory_storage::{ActixTimer, Storage}, Job, Manager, QueueHandle, WorkerConfig, }; -use std::time::Duration; +use std::{convert::TryFrom, num::NonZeroUsize, time::Duration}; fn debug_object(activity: &serde_json::Value) -> &serde_json::Value { let mut object = &activity["object"]["type"]; @@ -44,6 +44,9 @@ pub(crate) fn create_workers( media: MediaCache, config: Config, ) -> (Manager, JobServer) { + let parallelism = std::thread::available_parallelism() + .unwrap_or_else(|_| NonZeroUsize::try_from(1).expect("nonzero")); + let shared = WorkerConfig::new_managed(Storage::new(ActixTimer), move |queue_handle| { JobState::new( state.clone(), @@ -64,8 +67,10 @@ pub(crate) fn create_workers( .register::() .register::() .register::() - .set_worker_count("default", 16) - .start(); + .set_worker_count("maintenance", 2) + .set_worker_count("apub", 2) + .set_worker_count("deliver", 8) + .start_with_threads(parallelism); shared.every(Duration::from_secs(60 * 5), Listeners); diff --git a/src/jobs/apub/mod.rs b/src/jobs/apub.rs similarity index 100% rename from src/jobs/apub/mod.rs rename to src/jobs/apub.rs diff --git a/src/jobs/apub/announce.rs b/src/jobs/apub/announce.rs index 26048c1..93dec67 100644 --- a/src/jobs/apub/announce.rs +++ b/src/jobs/apub/announce.rs @@ -67,6 +67,7 @@ impl ActixJob for Announce { type Future = Pin>>>; const NAME: &'static str = "relay::jobs::apub::Announce"; + const QUEUE: &'static str = "apub"; fn run(self, state: Self::State) -> Self::Future { Box::pin(async move { self.perform(state).await.map_err(Into::into) }) diff --git a/src/jobs/apub/follow.rs b/src/jobs/apub/follow.rs index d78b544..10cae22 100644 --- a/src/jobs/apub/follow.rs +++ b/src/jobs/apub/follow.rs @@ -116,6 +116,7 @@ impl ActixJob for Follow { type Future = Pin>>>; const NAME: &'static str = "relay::jobs::apub::Follow"; + const QUEUE: &'static str = "apub"; fn run(self, state: Self::State) -> Self::Future { Box::pin(async move { self.perform(state).await.map_err(Into::into) }) diff --git a/src/jobs/apub/forward.rs b/src/jobs/apub/forward.rs index 1f72a32..f5e191b 100644 --- a/src/jobs/apub/forward.rs +++ b/src/jobs/apub/forward.rs @@ -52,6 +52,7 @@ impl ActixJob for Forward { type Future = Pin>>>; const NAME: &'static str = "relay::jobs::apub::Forward"; + const QUEUE: &'static str = "apub"; fn run(self, state: Self::State) -> Self::Future { Box::pin(async move { self.perform(state).await.map_err(Into::into) }) diff --git a/src/jobs/apub/reject.rs b/src/jobs/apub/reject.rs index f6ee0e7..2384426 100644 --- a/src/jobs/apub/reject.rs +++ b/src/jobs/apub/reject.rs @@ -38,6 +38,7 @@ impl ActixJob for Reject { type Future = Pin>>>; const NAME: &'static str = "relay::jobs::apub::Reject"; + const QUEUE: &'static str = "apub"; fn run(self, state: Self::State) -> Self::Future { Box::pin(async move { self.perform(state).await.map_err(Into::into) }) diff --git a/src/jobs/apub/undo.rs b/src/jobs/apub/undo.rs index 0359bf2..b55d4ae 100644 --- a/src/jobs/apub/undo.rs +++ b/src/jobs/apub/undo.rs @@ -53,6 +53,7 @@ impl ActixJob for Undo { type Future = Pin>>>; const NAME: &'static str = "relay::jobs::apub::Undo"; + const QUEUE: &'static str = "apub"; fn run(self, state: Self::State) -> Self::Future { Box::pin(async move { self.perform(state).await.map_err(Into::into) }) diff --git a/src/jobs/contact.rs b/src/jobs/contact.rs index 288941a..a98ac8f 100644 --- a/src/jobs/contact.rs +++ b/src/jobs/contact.rs @@ -86,6 +86,7 @@ impl ActixJob for QueryContact { type Future = Pin>>>; const NAME: &'static str = "relay::jobs::QueryContact"; + const QUEUE: &'static str = "maintenance"; fn run(self, state: Self::State) -> Self::Future { Box::pin(async move { self.perform(state).await.map_err(Into::into) }) diff --git a/src/jobs/deliver.rs b/src/jobs/deliver.rs index 9c01d8d..223608e 100644 --- a/src/jobs/deliver.rs +++ b/src/jobs/deliver.rs @@ -55,6 +55,7 @@ impl ActixJob for Deliver { type Future = Pin>>>; const NAME: &'static str = "relay::jobs::Deliver"; + const QUEUE: &'static str = "deliver"; const BACKOFF: Backoff = Backoff::Exponential(8); fn run(self, state: Self::State) -> Self::Future { diff --git a/src/jobs/deliver_many.rs b/src/jobs/deliver_many.rs index 58cdc1f..fc9107c 100644 --- a/src/jobs/deliver_many.rs +++ b/src/jobs/deliver_many.rs @@ -50,6 +50,7 @@ impl ActixJob for DeliverMany { type Future = LocalBoxFuture<'static, Result<(), anyhow::Error>>; const NAME: &'static str = "relay::jobs::DeliverMany"; + const QUEUE: &'static str = "deliver"; fn run(self, state: Self::State) -> Self::Future { Box::pin(async move { self.perform(state).await.map_err(Into::into) }) diff --git a/src/jobs/instance.rs b/src/jobs/instance.rs index d3e2719..4339845 100644 --- a/src/jobs/instance.rs +++ b/src/jobs/instance.rs @@ -110,6 +110,7 @@ impl ActixJob for QueryInstance { type Future = Pin>>>; const NAME: &'static str = "relay::jobs::QueryInstance"; + const QUEUE: &'static str = "maintenance"; fn run(self, state: Self::State) -> Self::Future { Box::pin(async move { self.perform(state).await.map_err(Into::into) }) diff --git a/src/jobs/nodeinfo.rs b/src/jobs/nodeinfo.rs index dbe2717..fe86ad8 100644 --- a/src/jobs/nodeinfo.rs +++ b/src/jobs/nodeinfo.rs @@ -100,6 +100,7 @@ impl ActixJob for QueryNodeinfo { type Future = Pin>>>; const NAME: &'static str = "relay::jobs::QueryNodeinfo"; + const QUEUE: &'static str = "maintenance"; fn run(self, state: Self::State) -> Self::Future { Box::pin(async move { self.perform(state).await.map_err(Into::into) }) diff --git a/src/jobs/process_listeners.rs b/src/jobs/process_listeners.rs index bba4e00..1cad2e4 100644 --- a/src/jobs/process_listeners.rs +++ b/src/jobs/process_listeners.rs @@ -28,6 +28,7 @@ impl ActixJob for Listeners { type Future = Pin>>>; const NAME: &'static str = "relay::jobs::Listeners"; + const QUEUE: &'static str = "maintenance"; fn run(self, state: Self::State) -> Self::Future { Box::pin(async move { self.perform(state).await.map_err(Into::into) }) diff --git a/src/main.rs b/src/main.rs index 1d80a0d..a428991 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,11 +2,13 @@ #![allow(clippy::needless_borrow)] use activitystreams::iri_string::types::IriString; -use actix_web::{web, App, HttpServer}; +use actix_web::{middleware::Compress, web, App, HttpServer}; +use collector::MemoryCollector; #[cfg(feature = "console")] use console_subscriber::ConsoleLayer; use opentelemetry::{sdk::Resource, KeyValue}; use opentelemetry_otlp::WithExportConfig; +use rustls::ServerConfig; use tracing_actix_web::TracingLogger; use tracing_error::ErrorLayer; use tracing_log::LogTracer; @@ -15,6 +17,7 @@ use tracing_subscriber::{filter::Targets, fmt::format::FmtSpan, layer::Subscribe mod admin; mod apub; mod args; +mod collector; mod config; mod data; mod db; @@ -32,7 +35,7 @@ use self::{ data::{ActorCache, MediaCache, State}, db::Db, jobs::create_workers, - middleware::{DebugPayload, RelayResolver}, + middleware::{DebugPayload, RelayResolver, Timings}, routes::{actor, inbox, index, nodeinfo, nodeinfo_meta, statics}, }; @@ -43,7 +46,7 @@ fn init_subscriber( LogTracer::init()?; let targets: Targets = std::env::var("RUST_LOG") - .unwrap_or_else(|_| "info".into()) + .unwrap_or_else(|_| "warn,actix_web=debug,actix_server=debug,tracing_actix_web=info".into()) .parse()?; let format_layer = tracing_subscriber::fmt::layer() @@ -91,72 +94,120 @@ fn init_subscriber( Ok(()) } -#[actix_rt::main] -async fn main() -> Result<(), anyhow::Error> { +fn main() -> Result<(), anyhow::Error> { dotenv::dotenv().ok(); let config = Config::build()?; init_subscriber(Config::software_name(), config.opentelemetry_url())?; + let collector = MemoryCollector::new(); + collector.install()?; let args = Args::new(); if args.any() { - let client = requests::build_client(&config.user_agent()); - - if !args.blocks().is_empty() || !args.allowed().is_empty() { - if args.undo() { - admin::client::unblock(&client, &config, args.blocks().to_vec()).await?; - admin::client::disallow(&client, &config, args.allowed().to_vec()).await?; - } else { - admin::client::block(&client, &config, args.blocks().to_vec()).await?; - admin::client::allow(&client, &config, args.allowed().to_vec()).await?; - } - println!("Updated lists"); - } - - if args.list() { - let (blocked, allowed, connected) = tokio::try_join!( - admin::client::blocked(&client, &config), - admin::client::allowed(&client, &config), - admin::client::connected(&client, &config) - )?; - - let mut report = String::from("Report:\n"); - if !allowed.allowed_domains.is_empty() { - report += "\nAllowed\n\t"; - report += &allowed.allowed_domains.join("\n\t"); - } - if !blocked.blocked_domains.is_empty() { - report += "\n\nBlocked\n\t"; - report += &blocked.blocked_domains.join("\n\t"); - } - if !connected.connected_actors.is_empty() { - report += "\n\nConnected\n\t"; - report += &connected.connected_actors.join("\n\t"); - } - report += "\n"; - println!("{report}"); - } - - return Ok(()); + return client_main(config, args); } + tracing::warn!("Opening DB"); let db = Db::build(&config)?; - let media = MediaCache::new(db.clone()); - let state = State::build(db.clone()).await?; + tracing::warn!("Building caches"); let actors = ActorCache::new(db.clone()); + let media = MediaCache::new(db.clone()); + server_main(db, actors, media, collector, config)?; + + tracing::warn!("Application exit"); + + Ok(()) +} + +#[actix_rt::main] +async fn client_main(config: Config, args: Args) -> Result<(), anyhow::Error> { + actix_rt::spawn(do_client_main(config, args)).await? +} + +async fn do_client_main(config: Config, args: Args) -> Result<(), anyhow::Error> { + let client = requests::build_client(&config.user_agent()); + + if !args.blocks().is_empty() || !args.allowed().is_empty() { + if args.undo() { + admin::client::unblock(&client, &config, args.blocks().to_vec()).await?; + admin::client::disallow(&client, &config, args.allowed().to_vec()).await?; + } else { + admin::client::block(&client, &config, args.blocks().to_vec()).await?; + admin::client::allow(&client, &config, args.allowed().to_vec()).await?; + } + println!("Updated lists"); + } + + if args.list() { + let (blocked, allowed, connected) = tokio::try_join!( + admin::client::blocked(&client, &config), + admin::client::allowed(&client, &config), + admin::client::connected(&client, &config) + )?; + + let mut report = String::from("Report:\n"); + if !allowed.allowed_domains.is_empty() { + report += "\nAllowed\n\t"; + report += &allowed.allowed_domains.join("\n\t"); + } + if !blocked.blocked_domains.is_empty() { + report += "\n\nBlocked\n\t"; + report += &blocked.blocked_domains.join("\n\t"); + } + if !connected.connected_actors.is_empty() { + report += "\n\nConnected\n\t"; + report += &connected.connected_actors.join("\n\t"); + } + report += "\n"; + println!("{report}"); + } + + if args.stats() { + let stats = admin::client::stats(&client, &config).await?; + stats.present(); + } + + Ok(()) +} + +#[actix_rt::main] +async fn server_main( + db: Db, + actors: ActorCache, + media: MediaCache, + collector: MemoryCollector, + config: Config, +) -> Result<(), anyhow::Error> { + actix_rt::spawn(do_server_main(db, actors, media, collector, config)).await? +} + +async fn do_server_main( + db: Db, + actors: ActorCache, + media: MediaCache, + collector: MemoryCollector, + config: Config, +) -> Result<(), anyhow::Error> { + tracing::warn!("Creating state"); + let state = State::build(db.clone()).await?; + + tracing::warn!("Creating workers"); let (manager, job_server) = create_workers(state.clone(), actors.clone(), media.clone(), config.clone()); if let Some((token, admin_handle)) = config.telegram_info() { + tracing::warn!("Creating telegram handler"); telegram::start(admin_handle.to_owned(), db.clone(), token); } + let keys = config.open_keys()?; + let bind_address = config.bind_address(); - HttpServer::new(move || { + let server = HttpServer::new(move || { let app = App::new() .app_data(web::Data::new(db.clone())) .app_data(web::Data::new(state.clone())) @@ -164,7 +215,8 @@ async fn main() -> Result<(), anyhow::Error> { .app_data(web::Data::new(actors.clone())) .app_data(web::Data::new(config.clone())) .app_data(web::Data::new(job_server.clone())) - .app_data(web::Data::new(media.clone())); + .app_data(web::Data::new(media.clone())) + .app_data(web::Data::new(collector.clone())); let app = if let Some(data) = config.admin_config() { app.app_data(data) @@ -172,7 +224,9 @@ async fn main() -> Result<(), anyhow::Error> { app }; - app.wrap(TracingLogger::default()) + app.wrap(Compress::default()) + .wrap(TracingLogger::default()) + .wrap(Timings) .service(web::resource("/").route(web::get().to(index))) .service(web::resource("/media/{path}").route(web::get().to(routes::media))) .service( @@ -203,16 +257,35 @@ async fn main() -> Result<(), anyhow::Error> { .route("/unblock", web::post().to(admin::routes::unblock)) .route("/allowed", web::get().to(admin::routes::allowed)) .route("/blocked", web::get().to(admin::routes::blocked)) - .route("/connected", web::get().to(admin::routes::connected)), + .route("/connected", web::get().to(admin::routes::connected)) + .route("/stats", web::get().to(admin::routes::stats)), ), ) - }) - .bind(bind_address)? - .run() - .await?; + }); + + if let Some((certs, key)) = keys { + tracing::warn!("Binding to {}:{} with TLS", bind_address.0, bind_address.1); + let server_config = ServerConfig::builder() + .with_safe_default_cipher_suites() + .with_safe_default_kx_groups() + .with_safe_default_protocol_versions()? + .with_no_client_auth() + .with_single_cert(certs, key)?; + server + .bind_rustls(bind_address, server_config)? + .run() + .await?; + } else { + tracing::warn!("Binding to {}:{}", bind_address.0, bind_address.1); + server.bind(bind_address)?.run().await?; + } + + tracing::warn!("Server closed"); drop(manager); + tracing::warn!("Main complete"); + Ok(()) } diff --git a/src/middleware/mod.rs b/src/middleware.rs similarity index 77% rename from src/middleware/mod.rs rename to src/middleware.rs index e11344d..93d6a68 100644 --- a/src/middleware/mod.rs +++ b/src/middleware.rs @@ -1,7 +1,9 @@ mod payload; +mod timings; mod verifier; mod webfinger; pub(crate) use payload::DebugPayload; +pub(crate) use timings::Timings; pub(crate) use verifier::MyVerify; pub(crate) use webfinger::RelayResolver; diff --git a/src/middleware/payload.rs b/src/middleware/payload.rs index 3175f98..bae7774 100644 --- a/src/middleware/payload.rs +++ b/src/middleware/payload.rs @@ -5,7 +5,7 @@ use actix_web::{ HttpMessage, }; use futures_util::{ - future::{LocalBoxFuture, TryFutureExt}, + future::TryFutureExt, stream::{once, TryStreamExt}, }; use std::{ @@ -45,7 +45,7 @@ where { type Response = S::Response; type Error = S::Error; - type Future = LocalBoxFuture<'static, Result>; + type Future = S::Future; fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { self.1.poll_ready(cx) @@ -68,13 +68,9 @@ where )), }); - let fut = self.1.call(req); - - Box::pin(async move { fut.await }) + self.1.call(req) } else { - let fut = self.1.call(req); - - Box::pin(async move { fut.await }) + self.1.call(req) } } } diff --git a/src/middleware/timings.rs b/src/middleware/timings.rs new file mode 100644 index 0000000..a311a75 --- /dev/null +++ b/src/middleware/timings.rs @@ -0,0 +1,143 @@ +use actix_web::{ + body::MessageBody, + dev::{Service, ServiceRequest, ServiceResponse, Transform}, + http::StatusCode, +}; +use std::{ + future::{ready, Future, Ready}, + time::Instant, +}; + +pub(crate) struct Timings; +pub(crate) struct TimingsMiddleware(S); + +struct LogOnDrop { + begin: Instant, + path: String, + method: String, + arm: bool, +} + +pin_project_lite::pin_project! { + pub(crate) struct TimingsFuture { + #[pin] + future: F, + + log_on_drop: Option, + } +} + +pin_project_lite::pin_project! { + pub(crate) struct TimingsBody { + #[pin] + body: B, + + log_on_drop: LogOnDrop, + } +} + +impl Drop for LogOnDrop { + fn drop(&mut self) { + if self.arm { + let duration = self.begin.elapsed(); + metrics::histogram!("relay.request.complete", duration, "path" => self.path.clone(), "method" => self.method.clone()); + } + } +} + +impl Transform for Timings +where + S: Service, Error = actix_web::Error>, + S::Future: 'static, +{ + type Response = ServiceResponse>; + type Error = S::Error; + type InitError = (); + type Transform = TimingsMiddleware; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(TimingsMiddleware(service))) + } +} + +impl Service for TimingsMiddleware +where + S: Service, Error = actix_web::Error>, + S::Future: 'static, +{ + type Response = ServiceResponse>; + type Error = S::Error; + type Future = TimingsFuture; + + fn poll_ready( + &self, + ctx: &mut core::task::Context<'_>, + ) -> std::task::Poll> { + self.0.poll_ready(ctx) + } + + fn call(&self, req: ServiceRequest) -> Self::Future { + let log_on_drop = LogOnDrop { + begin: Instant::now(), + path: req.path().to_string(), + method: req.method().to_string(), + arm: false, + }; + + let future = self.0.call(req); + + TimingsFuture { + future, + log_on_drop: Some(log_on_drop), + } + } +} + +impl Future for TimingsFuture +where + F: Future, actix_web::Error>>, +{ + type Output = Result>, actix_web::Error>; + + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + let this = self.project(); + + let res = std::task::ready!(this.future.poll(cx)); + + let mut log_on_drop = this + .log_on_drop + .take() + .expect("TimingsFuture polled after completion"); + + let status = match &res { + Ok(res) => res.status(), + Err(e) => e.as_response_error().status_code(), + }; + + log_on_drop.arm = + status != StatusCode::NOT_FOUND && status != StatusCode::METHOD_NOT_ALLOWED; + + let res = res.map(|r| r.map_body(|_, body| TimingsBody { body, log_on_drop })); + + std::task::Poll::Ready(res) + } +} + +impl MessageBody for TimingsBody { + type Error = B::Error; + + fn size(&self) -> actix_web::body::BodySize { + self.body.size() + } + + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll>> { + self.project().body.poll_next(cx) + } +} diff --git a/src/middleware/verifier.rs b/src/middleware/verifier.rs index 8234787..21d5b06 100644 --- a/src/middleware/verifier.rs +++ b/src/middleware/verifier.rs @@ -16,7 +16,7 @@ use std::{future::Future, pin::Pin}; pub(crate) struct MyVerify(pub Requests, pub ActorCache, pub State); impl MyVerify { - #[tracing::instrument("Verify signature", skip(self, signature))] + #[tracing::instrument("Verify request", skip(self, signature, signing_string))] async fn verify( &self, algorithm: Option, @@ -106,6 +106,7 @@ impl PublicKeyResponse { } } +#[tracing::instrument("Verify signature")] async fn do_verify( public_key: &str, signature: String, @@ -113,15 +114,20 @@ async fn do_verify( ) -> Result<(), Error> { let public_key = RsaPublicKey::from_public_key_pem(public_key.trim())?; + let span = tracing::Span::current(); web::block(move || { - let decoded = base64::decode(signature)?; - let signature = Signature::from_bytes(&decoded)?; - let hashed = Sha256::new_with_prefix(signing_string.as_bytes()); + span.in_scope(|| { + let decoded = base64::decode(signature)?; + let signature = Signature::from_bytes(&decoded).map_err(ErrorKind::ReadSignature)?; + let hashed = Sha256::new_with_prefix(signing_string.as_bytes()); - let verifying_key = VerifyingKey::new_with_prefix(public_key); - verifying_key.verify_digest(hashed, &signature)?; + let verifying_key = VerifyingKey::new_with_prefix(public_key); + verifying_key + .verify_digest(hashed, &signature) + .map_err(ErrorKind::VerifySignature)?; - Ok(()) as Result<(), Error> + Ok(()) as Result<(), Error> + }) }) .await??; diff --git a/src/routes/mod.rs b/src/routes.rs similarity index 100% rename from src/routes/mod.rs rename to src/routes.rs diff --git a/src/routes/index.rs b/src/routes/index.rs index 4e141d7..835e2b8 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -7,6 +7,19 @@ use actix_web::{web, HttpResponse}; use rand::{seq::SliceRandom, thread_rng}; use std::io::BufWriter; +const MINIFY_CONFIG: minify_html::Cfg = minify_html::Cfg { + do_not_minify_doctype: true, + ensure_spec_compliant_unquoted_attribute_values: true, + keep_closing_tags: true, + keep_html_and_head_opening_tags: false, + keep_spaces_between_attributes: true, + keep_comments: false, + minify_js: true, + minify_css: true, + remove_bangs: true, + remove_processing_instructions: true, +}; + fn open_reg(node: &Node) -> bool { node.instance .as_ref() @@ -20,7 +33,28 @@ pub(crate) async fn route( state: web::Data, config: web::Data, ) -> Result { - let mut nodes = state.node_cache().nodes().await?; + let all_nodes = state.node_cache().nodes().await?; + + let mut nodes = Vec::new(); + let mut local = Vec::new(); + + for node in all_nodes { + if node + .base + .authority_str() + .map(|authority| { + config + .local_domains() + .iter() + .any(|domain| domain.as_str() == authority) + }) + .unwrap_or(false) + { + local.push(node); + } else { + nodes.push(node); + } + } nodes.sort_by(|lhs, rhs| match (open_reg(lhs), open_reg(rhs)) { (true, true) | (false, false) => std::cmp::Ordering::Equal, @@ -37,11 +71,13 @@ pub(crate) async fn route( let mut buf = BufWriter::new(Vec::new()); - crate::templates::index(&mut buf, &nodes, &config)?; - let buf = buf.into_inner().map_err(|e| { + crate::templates::index(&mut buf, &local, &nodes, &config)?; + let html = buf.into_inner().map_err(|e| { tracing::error!("Error rendering template, {}", e.error()); ErrorKind::FlushBuffer })?; - Ok(HttpResponse::Ok().content_type("text/html").body(buf)) + let html = minify_html::minify(&html, &MINIFY_CONFIG); + + Ok(HttpResponse::Ok().content_type("text/html").body(html)) } diff --git a/templates/admin.rs.html b/templates/admin.rs.html index e9d89aa..1af6bc2 100644 --- a/templates/admin.rs.html +++ b/templates/admin.rs.html @@ -4,15 +4,15 @@ @(contact: &Contact, base: &IriString)
-
-
- @contact.display_name's avatar -
-
-
-

@contact.display_name

-

- @@@contact.username@if let Some(authority) = base.authority_str() {@@@authority} -

-
+
+
+ @contact.display_name's avatar +
+
+
+

@contact.display_name

+

+ @@@contact.username@if let Some(authority) = base.authority_str() {@@@authority} +

+
diff --git a/templates/index.rs.html b/templates/index.rs.html index e1d4268..b52155c 100644 --- a/templates/index.rs.html +++ b/templates/index.rs.html @@ -4,7 +4,7 @@ data::Node, templates::{info, instance, statics::index_css}, }; -@(nodes: &[Node], config: &Config) +@(local: &[Node], nodes: &[Node], config: &Config) @@ -24,16 +24,23 @@ templates::{info, instance, statics::index_css},
-
-

@nodes.len() instances fédérées.

- @if nodes.is_empty() { -

Aucune instance fédérée en ce moment.

- } else { + @if !local.is_empty() || config.local_blurb().is_some() { +
+

About

+
+ @if let Some(blurb) = config.local_blurb() { + @blurb + } else { +

Ces domaines sont administrés par la même équipe que ce relais.

+ } +
+ @if !local.is_empty() {
    - @for node in nodes { + @for node in local { @if let Some(inst) = node.instance.as_ref() {
  • - @:instance(inst, node.info.as_ref().map(|info| { info.software.as_ref() }), node.contact.as_ref(), &node.base) + @:instance(inst, node.info.as_ref().map(|info| { info.software.as_ref() }), node.contact.as_ref(), + &node.base)
  • } else { @if let Some(inf) = node.info.as_ref() { @@ -45,10 +52,11 @@ templates::{info, instance, statics::index_css}, }
} -
-
-

Rejoindre

-
+
+ } +
+

Joining

+
@if config.restricted_mode() {

Ce relais est restreint. @@ -77,10 +85,34 @@ templates::{info, instance, statics::index_css},

Vérifiez la documentation de votre installation, qui suit probablement la convention de Mastodon ou de Pleroma.

-

-
+ + + @if !nodes.is_empty() { +
+

@nodes.len() Connected Servers

+
    + @for node in nodes { + @if let Some(inst) = node.instance.as_ref() { +
  • + @:instance(inst, node.info.as_ref().map(|info| { info.software.as_ref() }), node.contact.as_ref(), + &node.base) +
  • + } else { + @if let Some(inf) = node.info.as_ref() { +
  • + @:info(inf, &node.base) +
  • + } + } + } +
+
+ }