Compare commits

...

64 Commits

Author SHA1 Message Date
4e5e257f24 Fix merge fail 2022-11-28 10:08:58 +01:00
f0ae726c9d Small translation errors 2022-11-28 10:00:05 +01:00
112ed7cedd Merge tag 'v0.3.66' into max 2022-11-28 09:54:00 +01:00
41034f7334 Mini fixes 2022-11-28 09:14:46 +01:00
asonix
89a9e20d4a Bump version 2022-11-23 12:58:29 -06:00
asonix
39b8b1d3fa Add compression middleware - not zstd 2022-11-23 12:57:56 -06:00
asonix
96eb028145 Revert "Add compression middleware"
This reverts commit aa8ddfa637.
2022-11-23 12:50:46 -06:00
asonix
aad0cc990e Bump version 2022-11-23 12:39:19 -06:00
asonix
aa8ddfa637 Add compression middleware 2022-11-23 12:30:17 -06:00
asonix
c6adc9f77b Propogate Span into bcrypt verify 2022-11-23 11:58:44 -06:00
asonix
68a0b7c574 Bump version 2022-11-23 11:53:58 -06:00
asonix
d7adaeb38d Move joining instructions before server list 2022-11-23 11:52:05 -06:00
asonix
149ec1d14f Minify HTML 2022-11-23 11:51:51 -06:00
asonix
d7a720b6c4 clippy, replace indexmap with btreemap 2022-11-23 11:25:13 -06:00
asonix
e2f3727d07 Bump version 2022-11-23 11:21:05 -06:00
asonix
e9f312bed5 Measure bcrypt, change DashMap to RwLock<HashMap for collector 2022-11-23 11:13:30 -06:00
asonix
1a638f7f8d Improve debug middleware 2022-11-23 10:44:11 -06:00
asonix
ed0ea6521e Improve Timings middleware 2022-11-23 10:44:01 -06:00
asonix
e987149757 Bump version 2022-11-23 08:27:36 -06:00
asonix
01e283a065 Update deps 2022-11-23 08:27:05 -06:00
asonix
ab7d940de9 Improve error in signature verification (again) 2022-11-22 15:25:42 -06:00
asonix
5cd0b21ae3 Improve error in signature verification 2022-11-22 15:11:56 -06:00
asonix
b53ec4d980 More useful default logging 2022-11-21 23:12:31 -06:00
asonix
c3b50bc94e Bump version 2022-11-21 14:26:51 -06:00
asonix
88329a79e2 clippy 2022-11-21 14:25:24 -06:00
asonix
a77a4cde22 Add an 'About' section to the relay 2022-11-21 14:23:37 -06:00
asonix
5043892981 Fix compile issue 2022-11-21 11:28:25 -06:00
asonix
8afc16786d WIP: Don't collect path-based metrics for 404, 405 2022-11-21 11:27:22 -06:00
asonix
cdaf3b2fa3 Bump deps 2022-11-21 11:16:57 -06:00
asonix
37e3b17966 Bump version 2022-11-21 11:16:48 -06:00
asonix
9133dd7688 Add optional footer blurb 2022-11-21 11:16:21 -06:00
asonix
a0195d94aa Bump version 2022-11-20 22:47:49 -06:00
asonix
d8f3f1d0e9 Add one more log in TLS config 2022-11-20 22:47:20 -06:00
asonix
205e794b9e Add more logging around TLS config issues 2022-11-20 22:46:20 -06:00
asonix
73cc4862d9 Bump deps 2022-11-20 21:43:09 -06:00
asonix
981a6779bf Bump version 2022-11-20 21:42:59 -06:00
asonix
5d33dba103 Add support for binding TLS 2022-11-20 21:42:38 -06:00
asonix
a3eb785b9e Update defaults to be more prod friendly 2022-11-20 16:25:27 -06:00
asonix
efc918a826 Update deps 2022-11-20 12:09:43 -06:00
asonix
13cd308358 clippy 2022-11-20 12:09:17 -06:00
asonix
df70a28ca3 Bump version 2022-11-20 12:07:44 -06:00
asonix
162dd1cb0e Add more launch logging 2022-11-20 12:07:27 -06:00
asonix
df3063e75f Improve concurrency for larger systems 2022-11-20 12:06:10 -06:00
asonix
d44db2eab5 Bump version 2022-11-19 23:44:52 -06:00
asonix
7ec56d2af2 clippy 2022-11-19 23:44:35 -06:00
asonix
9f6e0bc722 Bump version 2022-11-19 23:35:20 -06:00
asonix
3500f85f44 Move blocking setup out of actix systems 2022-11-19 23:35:00 -06:00
asonix
a154fbb504 Bump version 2022-11-19 22:39:27 -06:00
asonix
9ede941ff7 Increase concurrency 2022-11-19 22:38:58 -06:00
asonix
4267f52a7e Bump deps 2022-11-19 21:52:06 -06:00
asonix
9272ba0d4c More logging when ending main 2022-11-19 21:51:04 -06:00
asonix
8d0d39b1fc Bump version 2022-11-19 21:33:09 -06:00
asonix
787c8312bc Make better use of cores for jobs 2022-11-19 21:32:45 -06:00
asonix
95f98ec052 Update readme 2022-11-19 20:40:17 -06:00
asonix
8fa24aa243 Bump version 2022-11-19 20:36:24 -06:00
asonix
cecc35ae85 Add timings metrics middleware 2022-11-19 20:35:45 -06:00
asonix
4e1a782bea Fix merge 2022-11-19 18:57:34 -06:00
asonix
9a9d09c0c4 Bump version 2022-11-19 18:27:16 -06:00
asonix
99c3ec0b75 Improve presentation of stats 2022-11-19 18:26:47 -06:00
asonix
f892a50f2c Add metrics printer 2022-11-19 17:45:01 -06:00
asonix
c322798ba3 Add metrics collector, admin route 2022-11-19 17:28:15 -06:00
asonix
c8b81bb9aa Add metrics dependencies 2022-11-19 14:47:47 -06:00
asonix
902ce5d3c2 New module structure 2022-11-19 14:47:32 -06:00
asonix
261805004b Update background-jobs 2022-11-19 14:45:13 -06:00
37 changed files with 1388 additions and 181 deletions

8
.env
View File

@ -1,5 +1,11 @@
HOSTNAME=localhost:8079 HOSTNAME=localhost:8079
PORT=8079 PORT=8079
HTTPS=false
DEBUG=true
RESTRICTED_MODE=true RESTRICTED_MODE=true
API_TOKEN=somesecretpassword VALIDATE_SIGNATURES=false
API_TOKEN=kjsdhfkwjenrkajhsdakjsnd
FOOTER_BLURB="Opéré par <a href=\"https://mastodon.xolus.net/@max\">@max</a>"
LOCAL_DOMAINS="xolus.net"
LOCAL_BLURB="<p>Relais ActivityPub francophone</p>"
# OPENTELEMETRY_URL=http://localhost:4317 # OPENTELEMETRY_URL=http://localhost:4317

361
Cargo.lock generated
View File

@ -63,14 +63,17 @@ dependencies = [
"actix-codec", "actix-codec",
"actix-rt", "actix-rt",
"actix-service", "actix-service",
"actix-tls",
"actix-utils", "actix-utils",
"ahash", "ahash",
"base64", "base64",
"bitflags", "bitflags",
"brotli",
"bytes", "bytes",
"bytestring", "bytestring",
"derive_more", "derive_more",
"encoding_rs", "encoding_rs",
"flate2",
"futures-core", "futures-core",
"h2", "h2",
"http", "http",
@ -192,6 +195,7 @@ dependencies = [
"actix-rt", "actix-rt",
"actix-server", "actix-server",
"actix-service", "actix-service",
"actix-tls",
"actix-utils", "actix-utils",
"ahash", "ahash",
"bytes", "bytes",
@ -251,13 +255,28 @@ dependencies = [
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.19" version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [ dependencies = [
"memchr", "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]] [[package]]
name = "ammonia" name = "ammonia"
version = "3.2.1" version = "3.2.1"
@ -279,7 +298,7 @@ checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
[[package]] [[package]]
name = "ap-relay" name = "ap-relay"
version = "0.3.50" version = "0.3.66"
dependencies = [ dependencies = [
"activitystreams", "activitystreams",
"activitystreams-ext", "activitystreams-ext",
@ -300,13 +319,20 @@ dependencies = [
"futures-util", "futures-util",
"http-signature-normalization-actix", "http-signature-normalization-actix",
"lru", "lru",
"metrics",
"metrics-util",
"mime", "mime",
"minify-html",
"opentelemetry", "opentelemetry",
"opentelemetry-otlp", "opentelemetry-otlp",
"pin-project-lite",
"quanta",
"rand", "rand",
"rsa", "rsa",
"rsa-magic-public-key", "rsa-magic-public-key",
"ructe", "ructe",
"rustls",
"rustls-pemfile",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@ -346,6 +372,12 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164" checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164"
[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]] [[package]]
name = "async-mutex" name = "async-mutex"
version = "1.4.0" version = "1.4.0"
@ -485,9 +517,9 @@ dependencies = [
[[package]] [[package]]
name = "background-jobs" name = "background-jobs"
version = "0.13.0" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793a813f9145c5f3a27b8dcd834c0927de68bbd60d53a369e5894f3cc5759020" checksum = "62dc7cfc967d6714768097a876ca2941a54a26976c6d3c95ea6da48974890970"
dependencies = [ dependencies = [
"background-jobs-actix", "background-jobs-actix",
"background-jobs-core", "background-jobs-core",
@ -495,15 +527,16 @@ dependencies = [
[[package]] [[package]]
name = "background-jobs-actix" name = "background-jobs-actix"
version = "0.13.1" version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47263ad9c5679419347dae655c2fa2cba078b0eaa51ac758d4f0e9690c06910b" checksum = "99f8bfe0a984c8d0bc7e67b376cc05e0b9015fdd3ee878900046120ef781c47e"
dependencies = [ dependencies = [
"actix-rt", "actix-rt",
"anyhow", "anyhow",
"async-mutex", "async-mutex",
"async-trait", "async-trait",
"background-jobs-core", "background-jobs-core",
"metrics",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
@ -515,14 +548,15 @@ dependencies = [
[[package]] [[package]]
name = "background-jobs-core" name = "background-jobs-core"
version = "0.13.0" version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e78e842fe2ae461319e3d1843c12e301630e65650332b02032ac70b0dfc66f" checksum = "1274e49ae8eff1fc6b4943660e59ce2f2e13e65a23a707924a50a40c7b94fc4d"
dependencies = [ dependencies = [
"actix-rt", "actix-rt",
"anyhow", "anyhow",
"async-trait", "async-trait",
"event-listener", "event-listener",
"metrics",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
@ -562,6 +596,18 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 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]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.3" version = "0.10.3"
@ -581,6 +627,27 @@ dependencies = [
"cipher", "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]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.11.1" version = "3.11.1"
@ -601,9 +668,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.2.1" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
[[package]] [[package]]
name = "bytestring" name = "bytestring"
@ -616,9 +683,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.76" version = "1.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f" checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -692,7 +759,7 @@ dependencies = [
"async-trait", "async-trait",
"json5", "json5",
"lazy_static", "lazy_static",
"nom", "nom 7.1.1",
"pathdiff", "pathdiff",
"ron", "ron",
"rust-ini", "rust-ini",
@ -780,9 +847,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-epoch" name = "crossbeam-epoch"
version = "0.9.11" version = "0.9.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f916dfc5d356b0ed9dae65f1db9fc9770aa2851d2662b988ccf4fe3516e86348" checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"cfg-if", "cfg-if",
@ -793,9 +860,9 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.12" version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@ -810,6 +877,17 @@ dependencies = [
"typenum", "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]] [[package]]
name = "darling" name = "darling"
version = "0.13.4" version = "0.13.4"
@ -929,6 +1007,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "endian-type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]] [[package]]
name = "erasable" name = "erasable"
version = "1.2.1" version = "1.2.1"
@ -995,6 +1079,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "funty"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]] [[package]]
name = "futf" name = "futf"
version = "0.1.5" version = "0.1.5"
@ -1121,7 +1211,7 @@ checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
] ]
[[package]] [[package]]
@ -1161,7 +1251,7 @@ dependencies = [
"base64", "base64",
"byteorder", "byteorder",
"flate2", "flate2",
"nom", "nom 7.1.1",
"num-traits", "num-traits",
] ]
@ -1233,9 +1323,9 @@ dependencies = [
[[package]] [[package]]
name = "http-signature-normalization-actix" name = "http-signature-normalization-actix"
version = "0.6.1" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86dfd54a1764ad79376b8dbf29e5bf918a463eb5ec66c90cd0388508289af6f0" checksum = "7483d0ee4d093fa4bfe5956cd405492c07808a5064a29cfe3960d474f21f39c2"
dependencies = [ dependencies = [
"actix-http", "actix-http",
"actix-rt", "actix-rt",
@ -1437,6 +1527,19 @@ dependencies = [
"spin", "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]] [[package]]
name = "libc" name = "libc"
version = "0.2.137" version = "0.2.137"
@ -1507,6 +1610,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mach"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "maplit" name = "maplit"
version = "1.0.2" version = "1.0.2"
@ -1556,13 +1668,56 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]] [[package]]
name = "memoffset" name = "memoffset"
version = "0.6.5" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
dependencies = [ dependencies = [
"autocfg", "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]] [[package]]
name = "mime" name = "mime"
version = "0.3.16" version = "0.3.16"
@ -1579,6 +1734,29 @@ dependencies = [
"unicase", "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]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.2.1" version = "0.2.1"
@ -1602,7 +1780,7 @@ checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
"wasi", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys", "windows-sys",
] ]
@ -1624,6 +1802,28 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" 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]] [[package]]
name = "nom" name = "nom"
version = "7.1.1" version = "7.1.1"
@ -1642,7 +1842,7 @@ checksum = "37794436ca3029a3089e0b95d42da1f0b565ad271e4d3bb4bad0c7bb70b10605"
dependencies = [ dependencies = [
"bytecount", "bytecount",
"memchr", "memchr",
"nom", "nom 7.1.1",
] ]
[[package]] [[package]]
@ -1822,6 +2022,15 @@ dependencies = [
"tokio-stream", "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]] [[package]]
name = "ordered-multimap" name = "ordered-multimap"
version = "0.4.3" version = "0.4.3"
@ -1834,9 +2043,9 @@ dependencies = [
[[package]] [[package]]
name = "os_str_bytes" name = "os_str_bytes"
version = "6.4.0" version = "6.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5bf27447411e9ee3ff51186bf7a08e16c341efdde93f4d823e8844429bed7e" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
[[package]] [[package]]
name = "overload" name = "overload"
@ -1892,6 +2101,17 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "paste" name = "paste"
version = "1.0.9" version = "1.0.9"
@ -2065,6 +2285,12 @@ dependencies = [
"spki", "spki",
] ]
[[package]]
name = "portable-atomic"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15eb2c6e362923af47e13c23ca5afb859e83d54452c55b0b9ac763b8f7c1ac16"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.17" version = "0.2.17"
@ -2175,6 +2401,22 @@ dependencies = [
"prost", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.21" version = "1.0.21"
@ -2184,6 +2426,22 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -2214,6 +2472,15 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "raw-cpuid"
version = "10.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6823ea29436221176fe662da99998ad3b4db2c7f31e7b6f5fe43adccd6320bb"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "rc-box" name = "rc-box"
version = "1.2.0" version = "1.2.0"
@ -2376,7 +2643,7 @@ dependencies = [
"arc-swap", "arc-swap",
"fastrand", "fastrand",
"lazy_static", "lazy_static",
"nom", "nom 7.1.1",
"nom_locate", "nom_locate",
"num-bigint", "num-bigint",
"num-integer", "num-integer",
@ -2396,7 +2663,7 @@ dependencies = [
"itertools 0.10.5", "itertools 0.10.5",
"md5", "md5",
"mime", "mime",
"nom", "nom 7.1.1",
"rsass", "rsass",
] ]
@ -2490,9 +2757,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.88" version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7" checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -2579,6 +2846,12 @@ version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]]
name = "sketches-ddsketch"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ceb945e54128e09c43d8e4f1277851bd5044c6fc540bbaa2ad888f60b3da9ae7"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.7" version = "0.4.7"
@ -2636,6 +2909,12 @@ dependencies = [
"der", "der",
] ]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]] [[package]]
name = "string_cache" name = "string_cache"
version = "0.8.4" version = "0.8.4"
@ -2703,6 +2982,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e" checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e"
[[package]]
name = "tap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]] [[package]]
name = "teloxide" name = "teloxide"
version = "0.11.2" version = "0.11.2"
@ -3287,6 +3572,12 @@ dependencies = [
"try-lock", "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]] [[package]]
name = "wasi" name = "wasi"
version = "0.11.0+wasi-snapshot-preview1" version = "0.11.0+wasi-snapshot-preview1"
@ -3496,6 +3787,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "wyz"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]] [[package]]
name = "yaml-rust" name = "yaml-rust"
version = "0.4.5" version = "0.4.5"

View File

@ -1,7 +1,7 @@
[package] [package]
name = "ap-relay" name = "ap-relay"
description = "A simple activitypub relay" description = "A simple activitypub relay"
version = "0.3.50" version = "0.3.66"
authors = ["asonix <asonix@asonix.dog>"] authors = ["asonix <asonix@asonix.dog>"]
license = "AGPL-3.0" license = "AGPL-3.0"
readme = "README.md" readme = "README.md"
@ -23,7 +23,11 @@ default = []
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
actix-rt = "2.7.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" actix-webfinger = "0.4.0"
activitystreams = "0.7.0-alpha.19" activitystreams = "0.7.0-alpha.19"
activitystreams-ext = "0.1.0-alpha.2" activitystreams-ext = "0.1.0-alpha.2"
@ -38,12 +42,19 @@ dashmap = "5.1.0"
dotenv = "0.15.0" dotenv = "0.15.0"
futures-util = "0.3.17" futures-util = "0.3.17"
lru = "0.8.0" lru = "0.8.0"
metrics = "0.20.1"
metrics-util = "0.14.0"
mime = "0.3.16" mime = "0.3.16"
minify-html = "0.10.0"
opentelemetry = { version = "0.18", features = ["rt-tokio"] } opentelemetry = { version = "0.18", features = ["rt-tokio"] }
opentelemetry-otlp = "0.11" opentelemetry-otlp = "0.11"
pin-project-lite = "0.2.9"
quanta = "0.10.1"
rand = "0.8" rand = "0.8"
rsa = "0.7" rsa = "0.7"
rsa-magic-public-key = "0.6.0" rsa-magic-public-key = "0.6.0"
rustls = "0.20.7"
rustls-pemfile = "1.0.1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
sha2 = { version = "0.10", features = ["oid"] } sha2 = { version = "0.10", features = ["oid"] }
@ -70,7 +81,7 @@ tokio = { version = "1", features = ["macros", "sync"] }
uuid = { version = "1", features = ["v4", "serde"] } uuid = { version = "1", features = ["v4", "serde"] }
[dependencies.background-jobs] [dependencies.background-jobs]
version = "0.13.0" version = "0.14.0"
default-features = false default-features = false
features = ["background-jobs-actix", "error-logging"] features = ["background-jobs-actix", "error-logging"]

View File

@ -10,7 +10,7 @@ $ sudo docker run --rm -it \
-e ADDR=0.0.0.0 \ -e ADDR=0.0.0.0 \
-e SLED_PATH=/mnt/sled/db-0.34 \ -e SLED_PATH=/mnt/sled/db-0.34 \
-p 8080:8080 \ -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 This will launch the relay with the database stored in "./sled/db-0.34" and listening on port 8080
#### Cargo #### Cargo
@ -98,6 +98,11 @@ API_TOKEN=somepasswordishtoken
OPENTELEMETRY_URL=localhost:4317 OPENTELEMETRY_URL=localhost:4317
TELEGRAM_TOKEN=secret TELEGRAM_TOKEN=secret
TELEGRAM_ADMIN_HANDLE=your_handle TELEGRAM_ADMIN_HANDLE=your_handle
TLS_KEY=/path/to/key
TLS_CERT=/path/to/cert
FOOTER_BLURB="Contact <a href=\"https://masto.asonix.dog/@asonix\">@asonix</a> for inquiries"
LOCAL_DOMAINS=masto.asonix.dog
LOCAL_BLURB="<p>Welcome to my cool relay where I have cool relay things happening. I hope you enjoy your stay!</p>"
``` ```
#### Descriptions #### Descriptions
@ -112,15 +117,15 @@ Whether to print incoming activities to the console when requests hit the /inbox
##### `RESTRICTED_MODE` ##### `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. 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` ##### `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` ##### `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` ##### `PUBLISH_BLOCKS`
Whether or not to publish a list of blocked domains in the `nodeinfo` metadata for the server. It defaults to `false`. Whether or not to publish a list of blocked domains in the `nodeinfo` metadata for the server. It defaults to `false`.
##### `SLED_PATH` ##### `SLED_PATH`
Where to store the on-disk database of connected servers. This defaults to `./sled/db-0.34`. Where to store the on-disk database of connected servers. This defaults to `./sled/db-0.34`.
##### `RUST_LOG` ##### `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` ##### `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. 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` ##### `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. A Telegram Bot Token for running the relay administration bot. There is no default.
##### `TELEGRAM_ADMIN_HANDLE` ##### `TELEGRAM_ADMIN_HANDLE`
The handle of the telegram user allowed to administer the relay. There is no default. 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 ### Subscribing
Mastodon admins can subscribe to this relay by adding the `/inbox` route to their relay settings. Mastodon admins can subscribe to this relay by adding the `/inbox` route to their relay settings.

View File

@ -41,7 +41,7 @@ header {
} }
} }
section { article {
background-color: #fff; background-color: #fff;
color: #333; color: #333;
border: 1px solid #e5e5e5; border: 1px solid #e5e5e5;
@ -51,8 +51,16 @@ section {
max-width: 700px; max-width: 700px;
padding-bottom: 32px; padding-bottom: 32px;
> p:first-child { section {
margin-top: 0; border-bottom: 1px solid #e5e5e5;
> h4:first-child,
> p:first-child {
margin-top: 0;
}
> p:last-child {
margin-bottom: 0;
}
} }
h3 { h3 {
@ -67,13 +75,13 @@ section {
li { li {
padding-top: 36px; padding-top: 36px;
border-bottom: 1px solid #e5e5e5;
} }
.padded { .padded {
padding: 0 24px; padding: 0 24px;
} }
.local-explainer,
.joining { .joining {
padding: 24px; padding: 24px;
} }
@ -174,9 +182,11 @@ footer {
li { li {
padding: 0; padding: 0;
border-bottom: none;
} }
} }
article section {
border-bottom: none;
}
} }
} }
@ -241,7 +251,7 @@ footer {
padding: 24px; padding: 24px;
} }
section { article {
border-left: none; border-left: none;
border-right: none; border-right: none;
border-radius: 0; border-radius: 0;

View File

@ -1,5 +1,6 @@
use crate::{ use crate::{
admin::{AllowedDomains, BlockedDomains, ConnectedActors, Domains}, admin::{AllowedDomains, BlockedDomains, ConnectedActors, Domains},
collector::Snapshot,
config::{AdminUrlKind, Config}, config::{AdminUrlKind, Config},
error::{Error, ErrorKind}, error::{Error, ErrorKind},
}; };
@ -50,6 +51,10 @@ pub(crate) async fn connected(client: &Client, config: &Config) -> Result<Connec
get_results(client, config, AdminUrlKind::Connected).await get_results(client, config, AdminUrlKind::Connected).await
} }
pub(crate) async fn stats(client: &Client, config: &Config) -> Result<Snapshot, Error> {
get_results(client, config, AdminUrlKind::Stats).await
}
async fn get_results<T: DeserializeOwned>( async fn get_results<T: DeserializeOwned>(
client: &Client, client: &Client,
config: &Config, config: &Config,

View File

@ -1,9 +1,13 @@
use crate::{ use crate::{
admin::{AllowedDomains, BlockedDomains, ConnectedActors, Domains}, admin::{AllowedDomains, BlockedDomains, ConnectedActors, Domains},
collector::{MemoryCollector, Snapshot},
error::Error, error::Error,
extractors::Admin, extractors::Admin,
}; };
use actix_web::{web::Json, HttpResponse}; use actix_web::{
web::{Data, Json},
HttpResponse,
};
pub(crate) async fn allow( pub(crate) async fn allow(
admin: Admin, admin: Admin,
@ -58,3 +62,10 @@ pub(crate) async fn connected(admin: Admin) -> Result<Json<ConnectedActors>, Err
Ok(Json(ConnectedActors { connected_actors })) Ok(Json(ConnectedActors { connected_actors }))
} }
pub(crate) async fn stats(
_admin: Admin,
collector: Data<MemoryCollector>,
) -> Result<Json<Snapshot>, Error> {
Ok(Json(collector.snapshot()))
}

View File

@ -14,11 +14,14 @@ pub(crate) struct Args {
#[arg(short, long, help = "List allowed and blocked domains")] #[arg(short, long, help = "List allowed and blocked domains")]
list: bool, list: bool,
#[arg(short, long, help = "Get statistics from the server")]
stats: bool,
} }
impl Args { impl Args {
pub(crate) fn any(&self) -> bool { 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 { pub(crate) fn new() -> Self {
@ -40,4 +43,8 @@ impl Args {
pub(crate) fn list(&self) -> bool { pub(crate) fn list(&self) -> bool {
self.list self.list
} }
pub(crate) fn stats(&self) -> bool {
self.stats
}
} }

414
src/collector.rs Normal file
View File

@ -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<Vec<(String, String)>, Summary>;
#[derive(Clone)]
pub struct MemoryCollector {
inner: Arc<Inner>,
}
struct Inner {
descriptions: RwLock<HashMap<String, metrics::SharedString>>,
distributions: RwLock<HashMap<String, DistributionMap>>,
recency: Recency<Key>,
registry: Registry<Key, GenerationalStorage<AtomicStorage>>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Counter {
labels: BTreeMap<String, String>,
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::<Vec<_>>()
.join(", ");
write!(f, "{} - {}", labels, self.value)
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Gauge {
labels: BTreeMap<String, String>,
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::<Vec<_>>()
.join(", ");
write!(f, "{} - {}", labels, self.value)
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Histogram {
labels: BTreeMap<String, String>,
value: Vec<(f64, Option<f64>)>,
}
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::<Vec<_>>()
.join(", ");
let value = self
.value
.iter()
.map(|(k, v)| {
if let Some(v) = v {
format!("{}: {:.6}", k, v)
} else {
format!("{}: None,", k)
}
})
.collect::<Vec<_>>()
.join(", ");
write!(f, "{} - {}", labels, value)
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub(crate) struct Snapshot {
counters: HashMap<String, Vec<Counter>>,
gauges: HashMap<String, Vec<Gauge>>,
histograms: HashMap<String, Vec<Histogram>>,
}
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<Counter>,
finish: Option<Counter>,
}
impl MergeCounter {
fn merge(self) -> Option<Counter> {
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<String, Vec<Counter>> {
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<String, Vec<Gauge>> {
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<String, Vec<Histogram>> {
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<metrics::Unit>,
description: metrics::SharedString,
) {
self.add_description_if_missing(&key, description)
}
fn describe_gauge(
&self,
key: metrics::KeyName,
_: Option<metrics::Unit>,
description: metrics::SharedString,
) {
self.add_description_if_missing(&key, description)
}
fn describe_histogram(
&self,
key: metrics::KeyName,
_: Option<metrics::Unit>,
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())
}
}

View File

@ -14,8 +14,9 @@ use activitystreams::{
}; };
use config::Environment; use config::Environment;
use http_signature_normalization_actix::prelude::{VerifyDigest, VerifySignature}; use http_signature_normalization_actix::prelude::{VerifyDigest, VerifySignature};
use rustls::{Certificate, PrivateKey};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::{net::IpAddr, path::PathBuf}; use std::{io::BufReader, net::IpAddr, path::PathBuf};
use uuid::Uuid; use uuid::Uuid;
#[derive(Clone, Debug, serde::Deserialize)] #[derive(Clone, Debug, serde::Deserialize)]
@ -34,6 +35,11 @@ pub(crate) struct ParsedConfig {
telegram_token: Option<String>, telegram_token: Option<String>,
telegram_admin_handle: Option<String>, telegram_admin_handle: Option<String>,
api_token: Option<String>, api_token: Option<String>,
tls_key: Option<PathBuf>,
tls_cert: Option<PathBuf>,
footer_blurb: Option<String>,
local_domains: Option<String>,
local_blurb: Option<String>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -52,6 +58,16 @@ pub struct Config {
telegram_token: Option<String>, telegram_token: Option<String>,
telegram_admin_handle: Option<String>, telegram_admin_handle: Option<String>,
api_token: Option<String>, api_token: Option<String>,
tls: Option<TlsConfig>,
footer_blurb: Option<String>,
local_domains: Vec<String>,
local_blurb: Option<String>,
}
#[derive(Clone)]
struct TlsConfig {
key: PathBuf,
cert: PathBuf,
} }
#[derive(Debug)] #[derive(Debug)]
@ -77,6 +93,7 @@ pub enum AdminUrlKind {
Allowed, Allowed,
Blocked, Blocked,
Connected, Connected,
Stats,
} }
impl std::fmt::Debug for Config { impl std::fmt::Debug for Config {
@ -99,6 +116,11 @@ impl std::fmt::Debug for Config {
.field("telegram_token", &"[redacted]") .field("telegram_token", &"[redacted]")
.field("telegram_admin_handle", &self.telegram_admin_handle) .field("telegram_admin_handle", &self.telegram_admin_handle)
.field("api_token", &"[redacted]") .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() .finish()
} }
} }
@ -111,8 +133,8 @@ impl Config {
.set_default("port", 8080u64)? .set_default("port", 8080u64)?
.set_default("debug", true)? .set_default("debug", true)?
.set_default("restricted_mode", false)? .set_default("restricted_mode", false)?
.set_default("validate_signatures", false)? .set_default("validate_signatures", true)?
.set_default("https", false)? .set_default("https", true)?
.set_default("publish_blocks", false)? .set_default("publish_blocks", false)?
.set_default("sled_path", "./sled/db-0-34")? .set_default("sled_path", "./sled/db-0-34")?
.set_default("source_repo", "https://git.asonix.dog/asonix/relay")? .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_token", None as Option<&str>)?
.set_default("telegram_admin_handle", None as Option<&str>)? .set_default("telegram_admin_handle", None as Option<&str>)?
.set_default("api_token", 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()) .add_source(Environment::default())
.build()?; .build()?;
@ -128,6 +155,26 @@ impl Config {
let scheme = if config.https { "https" } else { "http" }; let scheme = if config.https { "https" } else { "http" };
let base_uri = iri!(format!("{}://{}", scheme, config.hostname)).into_absolute(); 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 { Ok(Config {
hostname: config.hostname, hostname: config.hostname,
addr: config.addr, addr: config.addr,
@ -143,9 +190,76 @@ impl Config {
telegram_token: config.telegram_token, telegram_token: config.telegram_token,
telegram_admin_handle: config.telegram_admin_handle, telegram_admin_handle: config.telegram_admin_handle,
api_token: config.api_token, 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<Option<(Vec<Certificate>, 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<crate::templates::Html<String>> {
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<crate::templates::Html<String>> {
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 { pub(crate) fn sled_path(&self) -> &PathBuf {
&self.sled_path &self.sled_path
} }
@ -338,6 +452,8 @@ impl Config {
.try_resolve(IriRelativeStr::new("api/v1/admin/blocked")?.as_ref())?, .try_resolve(IriRelativeStr::new("api/v1/admin/blocked")?.as_ref())?,
AdminUrlKind::Connected => FixedBaseResolver::new(self.base_uri.as_ref()) AdminUrlKind::Connected => FixedBaseResolver::new(self.base_uri.as_ref())
.try_resolve(IriRelativeStr::new("api/v1/admin/connected")?.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) Ok(iri)

View File

@ -100,6 +100,12 @@ pub(crate) enum ErrorKind {
#[error("Couldn't sign digest")] #[error("Couldn't sign digest")]
Signature(#[from] signature::Error), 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")] #[error("Couldn't parse the signature header")]
HeaderValidation(#[from] actix_web::http::header::InvalidHeaderValue), HeaderValidation(#[from] actix_web::http::header::InvalidHeaderValue),

View File

@ -11,7 +11,7 @@ use actix_web::{
use bcrypt::{BcryptError, DEFAULT_COST}; use bcrypt::{BcryptError, DEFAULT_COST};
use futures_util::future::LocalBoxFuture; use futures_util::future::LocalBoxFuture;
use http_signature_normalization_actix::prelude::InvalidHeaderValue; 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 tracing_error::SpanTrace;
use crate::db::Db; use crate::db::Db;
@ -61,7 +61,8 @@ impl Admin {
hashed_api_token: Data<AdminConfig>, hashed_api_token: Data<AdminConfig>,
x_api_token: XApiToken, x_api_token: XApiToken,
) -> Result<(), Error> { ) -> 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 .await
.map_err(Error::canceled)?? .map_err(Error::canceled)??
{ {
@ -178,10 +179,15 @@ impl FromRequest for Admin {
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>; type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let now = Instant::now();
let res = Self::prepare_verify(req); let res = Self::prepare_verify(req);
Box::pin(async move { Box::pin(async move {
let (db, c, t) = res?; let (db, c, t) = res?;
Self::verify(c, t).await?; Self::verify(c, t).await?;
metrics::histogram!(
"relay.admin.verify",
now.elapsed().as_micros() as f64 / 1_000_000_f64
);
Ok(Admin { db }) Ok(Admin { db })
}) })
} }

View File

@ -7,8 +7,8 @@ mod nodeinfo;
mod process_listeners; mod process_listeners;
pub(crate) use self::{ pub(crate) use self::{
contact::QueryContact, deliver::Deliver, deliver_many::DeliverMany, contact::QueryContact, deliver::Deliver, deliver_many::DeliverMany, instance::QueryInstance,
instance::QueryInstance, nodeinfo::QueryNodeinfo, nodeinfo::QueryNodeinfo,
}; };
use crate::{ use crate::{
@ -22,7 +22,7 @@ use background_jobs::{
memory_storage::{ActixTimer, Storage}, memory_storage::{ActixTimer, Storage},
Job, Manager, QueueHandle, WorkerConfig, 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 { fn debug_object(activity: &serde_json::Value) -> &serde_json::Value {
let mut object = &activity["object"]["type"]; let mut object = &activity["object"]["type"];
@ -44,6 +44,9 @@ pub(crate) fn create_workers(
media: MediaCache, media: MediaCache,
config: Config, config: Config,
) -> (Manager, JobServer) { ) -> (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| { let shared = WorkerConfig::new_managed(Storage::new(ActixTimer), move |queue_handle| {
JobState::new( JobState::new(
state.clone(), state.clone(),
@ -64,8 +67,10 @@ pub(crate) fn create_workers(
.register::<apub::Forward>() .register::<apub::Forward>()
.register::<apub::Reject>() .register::<apub::Reject>()
.register::<apub::Undo>() .register::<apub::Undo>()
.set_worker_count("default", 16) .set_worker_count("maintenance", 2)
.start(); .set_worker_count("apub", 2)
.set_worker_count("deliver", 8)
.start_with_threads(parallelism);
shared.every(Duration::from_secs(60 * 5), Listeners); shared.every(Duration::from_secs(60 * 5), Listeners);

View File

@ -67,6 +67,7 @@ impl ActixJob for Announce {
type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>;
const NAME: &'static str = "relay::jobs::apub::Announce"; const NAME: &'static str = "relay::jobs::apub::Announce";
const QUEUE: &'static str = "apub";
fn run(self, state: Self::State) -> Self::Future { fn run(self, state: Self::State) -> Self::Future {
Box::pin(async move { self.perform(state).await.map_err(Into::into) }) Box::pin(async move { self.perform(state).await.map_err(Into::into) })

View File

@ -116,6 +116,7 @@ impl ActixJob for Follow {
type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>;
const NAME: &'static str = "relay::jobs::apub::Follow"; const NAME: &'static str = "relay::jobs::apub::Follow";
const QUEUE: &'static str = "apub";
fn run(self, state: Self::State) -> Self::Future { fn run(self, state: Self::State) -> Self::Future {
Box::pin(async move { self.perform(state).await.map_err(Into::into) }) Box::pin(async move { self.perform(state).await.map_err(Into::into) })

View File

@ -52,6 +52,7 @@ impl ActixJob for Forward {
type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>;
const NAME: &'static str = "relay::jobs::apub::Forward"; const NAME: &'static str = "relay::jobs::apub::Forward";
const QUEUE: &'static str = "apub";
fn run(self, state: Self::State) -> Self::Future { fn run(self, state: Self::State) -> Self::Future {
Box::pin(async move { self.perform(state).await.map_err(Into::into) }) Box::pin(async move { self.perform(state).await.map_err(Into::into) })

View File

@ -38,6 +38,7 @@ impl ActixJob for Reject {
type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>;
const NAME: &'static str = "relay::jobs::apub::Reject"; const NAME: &'static str = "relay::jobs::apub::Reject";
const QUEUE: &'static str = "apub";
fn run(self, state: Self::State) -> Self::Future { fn run(self, state: Self::State) -> Self::Future {
Box::pin(async move { self.perform(state).await.map_err(Into::into) }) Box::pin(async move { self.perform(state).await.map_err(Into::into) })

View File

@ -53,6 +53,7 @@ impl ActixJob for Undo {
type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>;
const NAME: &'static str = "relay::jobs::apub::Undo"; const NAME: &'static str = "relay::jobs::apub::Undo";
const QUEUE: &'static str = "apub";
fn run(self, state: Self::State) -> Self::Future { fn run(self, state: Self::State) -> Self::Future {
Box::pin(async move { self.perform(state).await.map_err(Into::into) }) Box::pin(async move { self.perform(state).await.map_err(Into::into) })

View File

@ -86,6 +86,7 @@ impl ActixJob for QueryContact {
type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>;
const NAME: &'static str = "relay::jobs::QueryContact"; const NAME: &'static str = "relay::jobs::QueryContact";
const QUEUE: &'static str = "maintenance";
fn run(self, state: Self::State) -> Self::Future { fn run(self, state: Self::State) -> Self::Future {
Box::pin(async move { self.perform(state).await.map_err(Into::into) }) Box::pin(async move { self.perform(state).await.map_err(Into::into) })

View File

@ -55,6 +55,7 @@ impl ActixJob for Deliver {
type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>;
const NAME: &'static str = "relay::jobs::Deliver"; const NAME: &'static str = "relay::jobs::Deliver";
const QUEUE: &'static str = "deliver";
const BACKOFF: Backoff = Backoff::Exponential(8); const BACKOFF: Backoff = Backoff::Exponential(8);
fn run(self, state: Self::State) -> Self::Future { fn run(self, state: Self::State) -> Self::Future {

View File

@ -50,6 +50,7 @@ impl ActixJob for DeliverMany {
type Future = LocalBoxFuture<'static, Result<(), anyhow::Error>>; type Future = LocalBoxFuture<'static, Result<(), anyhow::Error>>;
const NAME: &'static str = "relay::jobs::DeliverMany"; const NAME: &'static str = "relay::jobs::DeliverMany";
const QUEUE: &'static str = "deliver";
fn run(self, state: Self::State) -> Self::Future { fn run(self, state: Self::State) -> Self::Future {
Box::pin(async move { self.perform(state).await.map_err(Into::into) }) Box::pin(async move { self.perform(state).await.map_err(Into::into) })

View File

@ -110,6 +110,7 @@ impl ActixJob for QueryInstance {
type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>;
const NAME: &'static str = "relay::jobs::QueryInstance"; const NAME: &'static str = "relay::jobs::QueryInstance";
const QUEUE: &'static str = "maintenance";
fn run(self, state: Self::State) -> Self::Future { fn run(self, state: Self::State) -> Self::Future {
Box::pin(async move { self.perform(state).await.map_err(Into::into) }) Box::pin(async move { self.perform(state).await.map_err(Into::into) })

View File

@ -100,6 +100,7 @@ impl ActixJob for QueryNodeinfo {
type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>;
const NAME: &'static str = "relay::jobs::QueryNodeinfo"; const NAME: &'static str = "relay::jobs::QueryNodeinfo";
const QUEUE: &'static str = "maintenance";
fn run(self, state: Self::State) -> Self::Future { fn run(self, state: Self::State) -> Self::Future {
Box::pin(async move { self.perform(state).await.map_err(Into::into) }) Box::pin(async move { self.perform(state).await.map_err(Into::into) })

View File

@ -28,6 +28,7 @@ impl ActixJob for Listeners {
type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>; type Future = Pin<Box<dyn Future<Output = Result<(), anyhow::Error>>>>;
const NAME: &'static str = "relay::jobs::Listeners"; const NAME: &'static str = "relay::jobs::Listeners";
const QUEUE: &'static str = "maintenance";
fn run(self, state: Self::State) -> Self::Future { fn run(self, state: Self::State) -> Self::Future {
Box::pin(async move { self.perform(state).await.map_err(Into::into) }) Box::pin(async move { self.perform(state).await.map_err(Into::into) })

View File

@ -2,11 +2,13 @@
#![allow(clippy::needless_borrow)] #![allow(clippy::needless_borrow)]
use activitystreams::iri_string::types::IriString; 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")] #[cfg(feature = "console")]
use console_subscriber::ConsoleLayer; use console_subscriber::ConsoleLayer;
use opentelemetry::{sdk::Resource, KeyValue}; use opentelemetry::{sdk::Resource, KeyValue};
use opentelemetry_otlp::WithExportConfig; use opentelemetry_otlp::WithExportConfig;
use rustls::ServerConfig;
use tracing_actix_web::TracingLogger; use tracing_actix_web::TracingLogger;
use tracing_error::ErrorLayer; use tracing_error::ErrorLayer;
use tracing_log::LogTracer; use tracing_log::LogTracer;
@ -15,6 +17,7 @@ use tracing_subscriber::{filter::Targets, fmt::format::FmtSpan, layer::Subscribe
mod admin; mod admin;
mod apub; mod apub;
mod args; mod args;
mod collector;
mod config; mod config;
mod data; mod data;
mod db; mod db;
@ -32,7 +35,7 @@ use self::{
data::{ActorCache, MediaCache, State}, data::{ActorCache, MediaCache, State},
db::Db, db::Db,
jobs::create_workers, jobs::create_workers,
middleware::{DebugPayload, RelayResolver}, middleware::{DebugPayload, RelayResolver, Timings},
routes::{actor, inbox, index, nodeinfo, nodeinfo_meta, statics}, routes::{actor, inbox, index, nodeinfo, nodeinfo_meta, statics},
}; };
@ -43,7 +46,7 @@ fn init_subscriber(
LogTracer::init()?; LogTracer::init()?;
let targets: Targets = std::env::var("RUST_LOG") 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()?; .parse()?;
let format_layer = tracing_subscriber::fmt::layer() let format_layer = tracing_subscriber::fmt::layer()
@ -91,72 +94,120 @@ fn init_subscriber(
Ok(()) Ok(())
} }
#[actix_rt::main] fn main() -> Result<(), anyhow::Error> {
async fn main() -> Result<(), anyhow::Error> {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
let config = Config::build()?; let config = Config::build()?;
init_subscriber(Config::software_name(), config.opentelemetry_url())?; init_subscriber(Config::software_name(), config.opentelemetry_url())?;
let collector = MemoryCollector::new();
collector.install()?;
let args = Args::new(); let args = Args::new();
if args.any() { if args.any() {
let client = requests::build_client(&config.user_agent()); return client_main(config, args);
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(());
} }
tracing::warn!("Opening DB");
let db = Db::build(&config)?; let db = Db::build(&config)?;
let media = MediaCache::new(db.clone()); tracing::warn!("Building caches");
let state = State::build(db.clone()).await?;
let actors = ActorCache::new(db.clone()); 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) = let (manager, job_server) =
create_workers(state.clone(), actors.clone(), media.clone(), config.clone()); create_workers(state.clone(), actors.clone(), media.clone(), config.clone());
if let Some((token, admin_handle)) = config.telegram_info() { if let Some((token, admin_handle)) = config.telegram_info() {
tracing::warn!("Creating telegram handler");
telegram::start(admin_handle.to_owned(), db.clone(), token); telegram::start(admin_handle.to_owned(), db.clone(), token);
} }
let keys = config.open_keys()?;
let bind_address = config.bind_address(); let bind_address = config.bind_address();
HttpServer::new(move || { let server = HttpServer::new(move || {
let app = App::new() let app = App::new()
.app_data(web::Data::new(db.clone())) .app_data(web::Data::new(db.clone()))
.app_data(web::Data::new(state.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(actors.clone()))
.app_data(web::Data::new(config.clone())) .app_data(web::Data::new(config.clone()))
.app_data(web::Data::new(job_server.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() { let app = if let Some(data) = config.admin_config() {
app.app_data(data) app.app_data(data)
@ -172,7 +224,9 @@ async fn main() -> Result<(), anyhow::Error> {
app 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("/").route(web::get().to(index)))
.service(web::resource("/media/{path}").route(web::get().to(routes::media))) .service(web::resource("/media/{path}").route(web::get().to(routes::media)))
.service( .service(
@ -203,16 +257,35 @@ async fn main() -> Result<(), anyhow::Error> {
.route("/unblock", web::post().to(admin::routes::unblock)) .route("/unblock", web::post().to(admin::routes::unblock))
.route("/allowed", web::get().to(admin::routes::allowed)) .route("/allowed", web::get().to(admin::routes::allowed))
.route("/blocked", web::get().to(admin::routes::blocked)) .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() if let Some((certs, key)) = keys {
.await?; 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); drop(manager);
tracing::warn!("Main complete");
Ok(()) Ok(())
} }

View File

@ -1,7 +1,9 @@
mod payload; mod payload;
mod timings;
mod verifier; mod verifier;
mod webfinger; mod webfinger;
pub(crate) use payload::DebugPayload; pub(crate) use payload::DebugPayload;
pub(crate) use timings::Timings;
pub(crate) use verifier::MyVerify; pub(crate) use verifier::MyVerify;
pub(crate) use webfinger::RelayResolver; pub(crate) use webfinger::RelayResolver;

View File

@ -5,7 +5,7 @@ use actix_web::{
HttpMessage, HttpMessage,
}; };
use futures_util::{ use futures_util::{
future::{LocalBoxFuture, TryFutureExt}, future::TryFutureExt,
stream::{once, TryStreamExt}, stream::{once, TryStreamExt},
}; };
use std::{ use std::{
@ -45,7 +45,7 @@ where
{ {
type Response = S::Response; type Response = S::Response;
type Error = S::Error; type Error = S::Error;
type Future = LocalBoxFuture<'static, Result<S::Response, S::Error>>; type Future = S::Future;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.1.poll_ready(cx) self.1.poll_ready(cx)
@ -68,13 +68,9 @@ where
)), )),
}); });
let fut = self.1.call(req); self.1.call(req)
Box::pin(async move { fut.await })
} else { } else {
let fut = self.1.call(req); self.1.call(req)
Box::pin(async move { fut.await })
} }
} }
} }

143
src/middleware/timings.rs Normal file
View File

@ -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>(S);
struct LogOnDrop {
begin: Instant,
path: String,
method: String,
arm: bool,
}
pin_project_lite::pin_project! {
pub(crate) struct TimingsFuture<F> {
#[pin]
future: F,
log_on_drop: Option<LogOnDrop>,
}
}
pin_project_lite::pin_project! {
pub(crate) struct TimingsBody<B> {
#[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<S, B> Transform<S, ServiceRequest> for Timings
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error>,
S::Future: 'static,
{
type Response = ServiceResponse<TimingsBody<B>>;
type Error = S::Error;
type InitError = ();
type Transform = TimingsMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(TimingsMiddleware(service)))
}
}
impl<S, B> Service<ServiceRequest> for TimingsMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error>,
S::Future: 'static,
{
type Response = ServiceResponse<TimingsBody<B>>;
type Error = S::Error;
type Future = TimingsFuture<S::Future>;
fn poll_ready(
&self,
ctx: &mut core::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
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<F, B> Future for TimingsFuture<F>
where
F: Future<Output = Result<ServiceResponse<B>, actix_web::Error>>,
{
type Output = Result<ServiceResponse<TimingsBody<B>>, actix_web::Error>;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Self::Output> {
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<B: MessageBody> MessageBody for TimingsBody<B> {
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<Option<Result<actix_web::web::Bytes, Self::Error>>> {
self.project().body.poll_next(cx)
}
}

View File

@ -16,7 +16,7 @@ use std::{future::Future, pin::Pin};
pub(crate) struct MyVerify(pub Requests, pub ActorCache, pub State); pub(crate) struct MyVerify(pub Requests, pub ActorCache, pub State);
impl MyVerify { impl MyVerify {
#[tracing::instrument("Verify signature", skip(self, signature))] #[tracing::instrument("Verify request", skip(self, signature, signing_string))]
async fn verify( async fn verify(
&self, &self,
algorithm: Option<Algorithm>, algorithm: Option<Algorithm>,
@ -106,6 +106,7 @@ impl PublicKeyResponse {
} }
} }
#[tracing::instrument("Verify signature")]
async fn do_verify( async fn do_verify(
public_key: &str, public_key: &str,
signature: String, signature: String,
@ -113,15 +114,20 @@ async fn do_verify(
) -> Result<(), Error> { ) -> Result<(), Error> {
let public_key = RsaPublicKey::from_public_key_pem(public_key.trim())?; let public_key = RsaPublicKey::from_public_key_pem(public_key.trim())?;
let span = tracing::Span::current();
web::block(move || { web::block(move || {
let decoded = base64::decode(signature)?; span.in_scope(|| {
let signature = Signature::from_bytes(&decoded)?; let decoded = base64::decode(signature)?;
let hashed = Sha256::new_with_prefix(signing_string.as_bytes()); 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); let verifying_key = VerifyingKey::new_with_prefix(public_key);
verifying_key.verify_digest(hashed, &signature)?; verifying_key
.verify_digest(hashed, &signature)
.map_err(ErrorKind::VerifySignature)?;
Ok(()) as Result<(), Error> Ok(()) as Result<(), Error>
})
}) })
.await??; .await??;

View File

@ -7,6 +7,19 @@ use actix_web::{web, HttpResponse};
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
use std::io::BufWriter; 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 { fn open_reg(node: &Node) -> bool {
node.instance node.instance
.as_ref() .as_ref()
@ -20,7 +33,28 @@ pub(crate) async fn route(
state: web::Data<State>, state: web::Data<State>,
config: web::Data<Config>, config: web::Data<Config>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
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)) { nodes.sort_by(|lhs, rhs| match (open_reg(lhs), open_reg(rhs)) {
(true, true) | (false, false) => std::cmp::Ordering::Equal, (true, true) | (false, false) => std::cmp::Ordering::Equal,
@ -37,11 +71,13 @@ pub(crate) async fn route(
let mut buf = BufWriter::new(Vec::new()); let mut buf = BufWriter::new(Vec::new());
crate::templates::index(&mut buf, &nodes, &config)?; crate::templates::index(&mut buf, &local, &nodes, &config)?;
let buf = buf.into_inner().map_err(|e| { let html = buf.into_inner().map_err(|e| {
tracing::error!("Error rendering template, {}", e.error()); tracing::error!("Error rendering template, {}", e.error());
ErrorKind::FlushBuffer 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))
} }

View File

@ -4,15 +4,15 @@
@(contact: &Contact, base: &IriString) @(contact: &Contact, base: &IriString)
<div class="admin"> <div class="admin">
<div class="left"> <div class="left">
<figure class="avatar"> <figure class="avatar">
<img src="@contact.avatar" alt="@contact.display_name's avatar"> <img src="@contact.avatar" alt="@contact.display_name's avatar">
</figure> </figure>
</div> </div>
<div class="right"> <div class="right">
<p class="display-name"><a href="@contact.url">@contact.display_name</a></p> <p class="display-name"><a href="@contact.url">@contact.display_name</a></p>
<p class="username"> <p class="username">
@@@contact.username@if let Some(authority) = base.authority_str() {@@@authority} @@@contact.username@if let Some(authority) = base.authority_str() {@@@authority}
</p> </p>
</div> </div>
</div> </div>

View File

@ -4,7 +4,7 @@ data::Node,
templates::{info, instance, statics::index_css}, templates::{info, instance, statics::index_css},
}; };
@(nodes: &[Node], config: &Config) @(local: &[Node], nodes: &[Node], config: &Config)
<!doctype html> <!doctype html>
<html> <html>
@ -12,7 +12,7 @@ templates::{info, instance, statics::index_css},
<head lang="en"> <head lang="en">
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>@config.hostname() | ActivityPub Relay</title> <title>@config.hostname() | Relais ActivityPub</title>
<link rel="stylesheet" href="/static/@index_css.name" type="text/css" /> <link rel="stylesheet" href="/static/@index_css.name" type="text/css" />
</head> </head>
@ -24,17 +24,23 @@ templates::{info, instance, statics::index_css},
</div> </div>
</header> </header>
<main> <main>
<section> @if !local.is_empty() || config.local_blurb().is_some() {
<h3>@nodes.len() instances fédérées</h3> <article>
<h3>@nodes.len() Connected Servers</h3> <h3>A Propos</h3>
@if nodes.is_empty() { <section class="local-explainer">
<p>Aucune instance fédérée en ce moment.</p> @if let Some(blurb) = config.local_blurb() {
} else { @blurb
} else {
<p>Ces domaines sont administrés par la même équipe que ce relais.</p>
}
</section>
@if !local.is_empty() {
<ul> <ul>
@for node in nodes { @for node in local {
@if let Some(inst) = node.instance.as_ref() { @if let Some(inst) = node.instance.as_ref() {
<li> <li>
@: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)
</li> </li>
} else { } else {
@if let Some(inf) = node.info.as_ref() { @if let Some(inf) = node.info.as_ref() {
@ -46,10 +52,11 @@ templates::{info, instance, statics::index_css},
} }
</ul> </ul>
} }
</section> </article>
<section> }
<h3>Rejoindre</h3> <article>
<article class="joining"> <a name="#joining"><h3>Rejoindre</h3></a>
<section class="joining">
@if config.restricted_mode() { @if config.restricted_mode() {
<h4> <h4>
Ce relais est restreint. Ce relais est restreint.
@ -78,10 +85,34 @@ templates::{info, instance, statics::index_css},
<p> <p>
Vérifiez la documentation de votre installation, qui suit probablement la convention de Mastodon ou de Pleroma. Vérifiez la documentation de votre installation, qui suit probablement la convention de Mastodon ou de Pleroma.
</p> </p>
</article> </section>
</section> </article>
@if !nodes.is_empty() {
<article>
<h3>@nodes.len() Connected Servers</h3>
<ul>
@for node in nodes {
@if let Some(inst) = node.instance.as_ref() {
<li>
@:instance(inst, node.info.as_ref().map(|info| { info.software.as_ref() }), node.contact.as_ref(),
&node.base)
</li>
} else {
@if let Some(inf) = node.info.as_ref() {
<li>
@:info(inf, &node.base)
</li>
}
}
}
</ul>
</article>
}
</main> </main>
<footer> <footer>
@if let Some(blurb) = config.footer_blurb() {
<div>@blurb</div>
}
<p> <p>
Code source de l'application disponible ici: Code source de l'application disponible ici:
<a href="@config.source_code()">@config.source_code()</a> <a href="@config.source_code()">@config.source_code()</a>

View File

@ -3,14 +3,14 @@
@(info: &Info, base: &IriString) @(info: &Info, base: &IriString)
<article class="info"> <section class="info">
@if let Some(authority) = base.authority_str() { @if let Some(authority) = base.authority_str() {
<h4 class="padded"><a href="@base">@authority</a></h4> <h4 class="padded"><a href="@base">@authority</a></h4>
}
<p class="padded">
Utilise @info.software, version @info.version.
@if info.reg {
Inscriptions ouvertes
} }
<p class="padded"> </p>
Utilise @info.software, version @info.version. </section>
@if info.reg {
Enregistrement ouvert.
}
</p>
</article>

View File

@ -3,24 +3,24 @@
@(instance: &Instance, software: Option<&str>, contact: Option<&Contact>, base: &IriString) @(instance: &Instance, software: Option<&str>, contact: Option<&Contact>, base: &IriString)
<article class="instance"> <section class="instance">
<h4 class="padded"><a href="@base">@instance.title</a></h4> <h4 class="padded"><a href="@base">@instance.title</a></h4>
<p class="padded"> <p class="padded">
@if let Some(software) = software { @if let Some(software) = software {
Utilise @software, version @instance.version. Utilise @software, version @instance.version.
} }
<br> <br>
@if instance.reg { @if instance.reg {
@if instance.requires_approval { @if instance.requires_approval {
<span class="moderated">Inscriptions soumises à approbation.</span> <span class="moderated">Inscriptions soumises à approbation.</span>
} else{ } else {
<span class="open">Inscriptions ouvertes.</span> <span class="open">Inscriptions ouvertes.</span>
} }
} else{ } else {
<span class="closed">Inscriptions fermées.</span> <span class="closed">Inscriptions fermées.</span>
} }
</p> </p>
@if !instance.description.trim().is_empty() || contact.is_some() { @if !instance.description.trim().is_empty() || contact.is_some() {
<div class="instance-info"> <div class="instance-info">
@if !instance.description.trim().is_empty() { @if !instance.description.trim().is_empty() {
<h5 class="instance-description">Description:</h5> <h5 class="instance-description">Description:</h5>
@ -30,10 +30,10 @@
</div> </div>
</div> </div>
} }
@if let Some(contact) = contact {
<h5 class="instance-admin">Administré par:</h5>
@:admin(contact, base)
}
</div> </div>
} @if let Some(contact) = contact {
</article> <h5 class="instance-admin">Administré par:</h5>
@:admin(contact, base)
}
}
</section>