diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3a941de..3ec1ead 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Rust", - "image": "mcr.microsoft.com/devcontainers/rust:0-1-bullseye", + "image": "mcr.microsoft.com/devcontainers/rust:1.0.9-bookworm", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": {} }, diff --git a/Cargo.lock b/Cargo.lock index e9765a7..1d881f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,9 +61,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstyle" @@ -71,12 +71,6 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" -[[package]] -name = "anyhow" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" - [[package]] name = "arc-swap" version = "1.7.1" @@ -96,9 +90,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" dependencies = [ "proc-macro2", "quote", @@ -107,9 +101,9 @@ dependencies = [ [[package]] name = "atom_syndication" -version = "0.12.5" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee79fb83c725eae67b55218870813d2fc39fd85e4f1583848ef9f4f823cfe7c" +checksum = "d2f68d23e2cb4fd958c705b91a6b4c80ceeaf27a9e11651272a8389d5ce1a4a3" dependencies = [ "chrono", "derive_builder", @@ -139,6 +133,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "base2048" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71f4fe417e8cc3bb9b437dfa9290ce92bd2730ba5374719bdfd9147fbc8f17cd" + [[package]] name = "base64" version = "0.21.7" @@ -152,10 +152,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bitflags" -version = "2.6.0" +name = "bincode" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "block-buffer" @@ -179,9 +188,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.1" +version = "4.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -189,9 +198,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.0" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "serde", @@ -199,15 +208,15 @@ dependencies = [ [[package]] name = "build_html" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225eb82ce9e70dcc0cfa6e404d0f353326b6e163bf500ec4711cec317d11935c" +checksum = "01b01f54cbdd56298a506b086691594ded3b68dcbc9437adc87c616a35e7fc89" [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" @@ -217,9 +226,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cached" @@ -234,7 +243,7 @@ dependencies = [ "futures", "hashbrown 0.14.5", "once_cell", - "thiserror", + "thiserror 1.0.69", "tokio", "web-time", ] @@ -259,9 +268,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" [[package]] name = "cc" -version = "1.2.1" +version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "shlex", ] @@ -274,18 +283,18 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "num-traits", ] [[package]] name = "clap" -version = "4.5.21" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" dependencies = [ "clap_builder", "clap_derive", @@ -293,9 +302,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" dependencies = [ "anstyle", "clap_lex", @@ -303,9 +312,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck", "proc-macro2", @@ -315,21 +324,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" - -[[package]] -name = "common-words-all" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a6ff47eb813c9e315610ceca0ddd247827e22f2cdadc4189e4676a81470c77" -dependencies = [ - "anyhow", - "csv", - "glob", - "serde", -] +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cookie" @@ -368,9 +365,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -394,27 +391,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "csv" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" -dependencies = [ - "csv-core", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "csv-core" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" -dependencies = [ - "memchr", -] - [[package]] name = "darling" version = "0.20.10" @@ -556,25 +532,25 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fnv" @@ -635,9 +611,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand", "futures-core", @@ -689,7 +665,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets", ] [[package]] @@ -698,17 +686,11 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "globset" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" dependencies = [ "aho-corasick", "bstr", @@ -748,9 +730,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" @@ -760,15 +742,15 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" [[package]] -name = "hermit-abi" -version = "0.4.0" +name = "htmlescape" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" [[package]] name = "http" @@ -794,9 +776,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -812,9 +794,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.31" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -997,51 +979,55 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown 0.15.2", ] [[package]] name = "inventory" -version = "0.3.15" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" +checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" +dependencies = [ + "rustversion", +] [[package]] name = "is-terminal" -version = "0.4.13" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.4.0", + "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.164" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] name = "libflate" @@ -1069,9 +1055,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" [[package]] name = "lipsum" @@ -1085,9 +1071,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "lock_api" @@ -1101,9 +1087,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "memchr" @@ -1135,22 +1121,21 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi 0.3.9", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -1196,24 +1181,24 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "parking" @@ -1252,9 +1237,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -1289,13 +1274,31 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "quick-error" version = "1.2.3" @@ -1304,9 +1307,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.37.1" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" dependencies = [ "encoding_rs", "memchr", @@ -1314,9 +1317,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" dependencies = [ "proc-macro2", ] @@ -1327,6 +1330,8 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ + "libc", + "rand_chacha", "rand_core", ] @@ -1345,32 +1350,29 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - -[[package]] -name = "redox_syscall" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags", + "getrandom 0.2.15", ] [[package]] -name = "redsunlib" -version = "0.35.4" +name = "redlib" +version = "0.36.0" dependencies = [ "arc-swap", "async-recursion", + "base2048", "base64 0.22.1", + "bincode", "brotli", "build_html", "cached", + "chrono", "clap", - "common-words-all", "cookie", "dotenvy", "fastrand", "futures-lite", + "htmlescape", "hyper", "hyper-rustls", "libflate", @@ -1379,7 +1381,9 @@ dependencies = [ "once_cell", "percent-encoding", "pretty_env_logger", + "pulldown-cmark", "regex", + "revision", "rinja", "route-recognizer", "rss", @@ -1388,7 +1392,9 @@ dependencies = [ "serde", "serde_json", "serde_json_path", + "serde_urlencoded", "serde_yaml", + "tegen", "time", "tokio", "toml", @@ -1396,6 +1402,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.11.1" @@ -1426,16 +1441,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] -name = "ring" -version = "0.17.8" +name = "revision" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "22f53179a035f881adad8c4d58a2c599c6b4a8325b989c68d178d7a34d1b1e4c" +dependencies = [ + "revision-derive", +] + +[[package]] +name = "revision-derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0ec466e5d8dca9965eb6871879677bef5590cf7525ad96cae14376efb75073" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ring" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -1490,9 +1524,9 @@ checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" [[package]] name = "rss" -version = "2.0.10" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554a62b3dd5450fcbb0435b3db809f9dd3c6e9f5726172408f7ad3b57ed59057" +checksum = "b2107738f003660f0a91f56fd3e3bd3ab5d918b2ddaf1e1ec2136fb1c46f71bf" dependencies = [ "atom_syndication", "derive_builder", @@ -1502,9 +1536,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.5.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -1513,9 +1547,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.5.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae" dependencies = [ "proc-macro2", "quote", @@ -1526,9 +1560,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.5.0" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a" dependencies = [ "globset", "sha2", @@ -1543,21 +1577,21 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "0.38.41" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1603,6 +1637,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "rusty-forkfork" version = "0.4.0" @@ -1617,9 +1657,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -1692,9 +1732,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa39c7303dc58b5543c94d22c1766b0d31f2ee58306363ea622b10bbc075eaa2" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -1702,18 +1742,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.215" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.215" +version = "1.0.218" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", @@ -1722,9 +1762,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -1734,9 +1774,9 @@ dependencies = [ [[package]] name = "serde_json_path" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e176fbf9bd62f75c2d8be33207fa13af2f800a506635e89759e46f934c520f4d" +checksum = "b992cea3194eea663ba99a042d61cea4bd1872da37021af56f6a37e0359b9d33" dependencies = [ "inventory", "nom", @@ -1745,26 +1785,26 @@ dependencies = [ "serde_json", "serde_json_path_core", "serde_json_path_macros", - "thiserror", + "thiserror 2.0.12", ] [[package]] name = "serde_json_path_core" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea3bfd54a421bec8328aefede43ac9f18c8c7ded3b2afc8addd44b4813d99fd0" +checksum = "dde67d8dfe7d4967b5a95e247d4148368ddd1e753e500adb34b3ffe40c6bc1bc" dependencies = [ "inventory", "serde", "serde_json", - "thiserror", + "thiserror 2.0.12", ] [[package]] name = "serde_json_path_macros" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee05bac728cc5232af5c23896b34fbdd17cf0bb0c113440588aeeb1b57c6ba1f" +checksum = "517acfa7f77ddaf5c43d5f119c44a683774e130b4247b7d3210f8924506cfac8" dependencies = [ "inventory", "serde_json_path_core", @@ -1791,6 +1831,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -1841,26 +1893,20 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1875,9 +1921,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.87" +version = "2.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" dependencies = [ "proc-macro2", "quote", @@ -1896,13 +1942,23 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.14.0" +name = "tegen" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" +checksum = "10a2d5a357b7c859b410139734a875136473c3b18b1bbd8d5bdc1769d9002acd" +dependencies = [ + "rand", +] + +[[package]] +name = "tempfile" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567" dependencies = [ "cfg-if", "fastrand", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", @@ -1923,7 +1979,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] @@ -1938,10 +2003,21 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.3.36" +name = "thiserror-impl" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" dependencies = [ "deranged", "itoa", @@ -1956,15 +2032,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" dependencies = [ "num-conv", "time-core", @@ -1982,9 +2058,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.41.1" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", @@ -2000,9 +2076,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -2021,9 +2097,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -2034,9 +2110,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", @@ -2055,9 +2131,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap", "serde", @@ -2074,9 +2150,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-core", @@ -2084,9 +2160,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -2099,21 +2175,21 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicase" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unsafe-libyaml" @@ -2129,9 +2205,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -2152,11 +2228,11 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.11.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" dependencies = [ - "getrandom", + "getrandom 0.3.1", ] [[package]] @@ -2167,9 +2243,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] @@ -2200,10 +2276,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasm-bindgen" -version = "0.2.95" +name = "wasi" +version = "0.13.3+wasi-0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", @@ -2212,13 +2297,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -2227,9 +2311,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2237,9 +2321,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -2250,9 +2334,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-time" @@ -2357,13 +2444,22 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags", +] + [[package]] name = "write16" version = "1.0.0" @@ -2378,9 +2474,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -2390,9 +2486,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", @@ -2423,18 +2519,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index bb965c7..d82b019 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "redsunlib" description = " Alternative private front-end to Reddit" license = "AGPL-3.0-only" repository = "https://git.stardust.wtf/iridium/redsunlib" -version = "0.35.4" +version = "0.36.0" authors = [ "Matthew Esposito ", "spikecodes <19519553+spikecodes@users.noreply.github.com>", @@ -27,7 +27,7 @@ hyper = { version = "0.14.31", features = ["full"] } percent-encoding = "2.3.1" route-recognizer = "0.3.1" serde_json = "1.0.133" -tokio = { version = "1.35.1", features = ["full"] } +tokio = { version = "1.44.2", features = ["full"] } time = { version = "0.3.31", features = ["local-offset"] } url = "2.5.0" rust-embed = { version = "8.1.0", features = ["include-exclude"] } @@ -47,8 +47,15 @@ rss = "2.0.7" arc-swap = "1.7.1" serde_json_path = "0.7.1" async-recursion = "1.1.1" -common-words-all = { version = "0.0.2", default-features = false, features = ["english", "one"] } +pulldown-cmark = { version = "0.12.0", features = ["simd", "html"], default-features = false } hyper-rustls = { version = "0.24.2", features = [ "http2" ] } +tegen = "0.1.4" +serde_urlencoded = "0.7.1" +chrono = { version = "0.4.39", default-features = false, features = [ "std" ] } +htmlescape = "0.3.1" +bincode = "1.3.3" +base2048 = "2.0.2" +revision = "0.10.0" [dev-dependencies] diff --git a/Dockerfile.alpine b/Dockerfile.alpine new file mode 100644 index 0000000..051476a --- /dev/null +++ b/Dockerfile.alpine @@ -0,0 +1,45 @@ +# supported versions here: https://hub.docker.com/_/rust +ARG ALPINE_VERSION=3.20 + +######################## +## builder image +######################## +FROM rust:alpine${ALPINE_VERSION} AS builder + +RUN apk add --no-cache musl-dev + +WORKDIR /redlib + +# download (most) dependencies in their own layer +COPY Cargo.lock Cargo.toml ./ +RUN mkdir src && echo "fn main() { panic!(\"why am i running?\") }" > src/main.rs +RUN cargo build --release --locked --bin redlib +RUN rm ./src/main.rs && rmdir ./src + +# copy the source and build the redlib binary +COPY . ./ +RUN cargo build --release --locked --bin redlib +RUN echo "finished building redlib!" + +######################## +## release image +######################## +FROM alpine:${ALPINE_VERSION} AS release + +# Import redlib binary from builder +COPY --from=builder /redlib/target/release/redlib /usr/local/bin/redlib + +# Add non-root user for running redlib +RUN adduser --home /nonexistent --no-create-home --disabled-password redlib +USER redlib + +# Document that we intend to expose port 8080 to whoever runs the container +EXPOSE 8080 + +# Run a healthcheck every minute to make sure redlib is functional +HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1 + +# Add container metadata +LABEL org.opencontainers.image.authors="sigaloid" + +CMD ["redlib"] diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu new file mode 100644 index 0000000..2e277c5 --- /dev/null +++ b/Dockerfile.ubuntu @@ -0,0 +1,51 @@ +# supported versions here: https://hub.docker.com/_/rust +ARG RUST_BUILDER_VERSION=slim-bookworm +ARG UBUNTU_RELEASE_VERSION=noble + +######################## +## builder image +######################## +FROM rust:${RUST_BUILDER_VERSION} AS builder + +WORKDIR /redlib + +# download (most) dependencies in their own layer +COPY Cargo.lock Cargo.toml ./ +RUN mkdir src && echo "fn main() { panic!(\"why am i running?\") }" > src/main.rs +RUN cargo build --release --locked --bin redlib +RUN rm ./src/main.rs && rmdir ./src + +# copy the source and build the redlib binary +COPY . ./ +RUN cargo build --release --locked --bin redlib +RUN echo "finished building redlib!" + +######################## +## release image +######################## +FROM ubuntu:${UBUNTU_RELEASE_VERSION} AS release + +# Install ca-certificates +RUN apt-get update && apt-get install -y ca-certificates + +# Import redlib binary from builder +COPY --from=builder /redlib/target/release/redlib /usr/local/bin/redlib + +# Add non-root user for running redlib +RUN useradd \ + --no-create-home \ + --password "!" \ + --comment "user for running redlib" \ + redlib +USER redlib + +# Document that we intend to expose port 8080 to whoever runs the container +EXPOSE 8080 + +# Run a healthcheck every minute to make sure redlib is functional +HEALTHCHECK --interval=1m --timeout=3s CMD wget --spider --q http://localhost:8080/settings || exit 1 + +# Add container metadata +LABEL org.opencontainers.image.authors="sigaloid" + +CMD ["redlib"] diff --git a/README.md b/README.md index b71973c..2648a79 100644 --- a/README.md +++ b/README.md @@ -300,6 +300,17 @@ REDLIB_DEFAULT_USE_HLS = "on" > > If using Docker Compose, no changes are needed as the `.env` file is already referenced in `compose.yaml` via the `env_file: .env` line. +## Command Line Flags + +Redlib supports the following command line flags: + +- `-4`, `--ipv4-only`: Listen on IPv4 only. +- `-6`, `--ipv6-only`: Listen on IPv6 only. +- `-r`, `--redirect-https`: Redirect all HTTP requests to HTTPS (no longer functional). +- `-a`, `--address
`: Sets address to listen on. Default is `[::]`. +- `-p`, `--port `: Port to listen on. Default is `8080`. +- `-H`, `--hsts `: HSTS header to tell browsers that this site should only be accessed over HTTPS. Default is `604800`. + ## Instance settings Assign a default value for each instance-specific setting by passing environment variables to Redlib in the format `REDLIB_{X}`. Replace `{X}` with the setting name (see list below) in capital letters. @@ -326,7 +337,7 @@ Assign a default value for each user-modifiable setting by passing environment v | `WIDE` | `["on", "off"]` | `off` | | `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` | | `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` | -| `BLUR_SPOILER` | `["on", "off"]` | `off` | +| `BLUR_SPOILER` | `["on", "off"]` | `off` | | `SHOW_NSFW` | `["on", "off"]` | `off` | | `BLUR_NSFW` | `["on", "off"]` | `off` | | `USE_HLS` | `["on", "off"]` | `off` | @@ -340,3 +351,4 @@ Assign a default value for each user-modifiable setting by passing environment v | `HIDE_SIDEBAR_AND_SUMMARY` | `["on", "off"]` | `off` | | `HIDE_BANNER` | `["on", "off"]` | `off` | | `FIXED_NAVBAR` | `["on", "off"]` | `on` | +| `REMOVE_DEFAULT_FEEDS` | `["on", "off"]` | `off` | \ No newline at end of file diff --git a/app.json b/app.json index 6921a59e..f36e8ea 100644 --- a/app.json +++ b/app.json @@ -82,6 +82,9 @@ }, "REDLIB_FULL_URL": { "required": false + }, + "REDLIB_DEFAULT_REMOVE_DEFAULT_FEEDS": { + "required": false } } } diff --git a/flake.lock b/flake.lock index 4569244..2b0b585 100644 --- a/flake.lock +++ b/flake.lock @@ -1,17 +1,12 @@ { "nodes": { "crane": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, "locked": { - "lastModified": 1717025063, - "narHash": "sha256-dIubLa56W9sNNz0e8jGxrX3CAkPXsq7snuFA/Ie6dn8=", + "lastModified": 1731974733, + "narHash": "sha256-enYSSZVVl15FI5p+0Y5/Ckf5DZAvXe6fBrHxyhA/njc=", "owner": "ipetkov", "repo": "crane", - "rev": "480dff0be03dac0e51a8dfc26e882b0d123a450e", + "rev": "3cb338ce81076ce5e461cf77f7824476addb0e1c", "type": "github" }, "original": { @@ -25,11 +20,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -40,11 +35,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1717112898, - "narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=", + "lastModified": 1731890469, + "narHash": "sha256-D1FNZ70NmQEwNxpSSdTXCSklBH1z2isPR84J6DQrJGs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0", + "rev": "5083ec887760adfe12af64830a66807423a859a7", "type": "github" }, "original": { @@ -64,19 +59,16 @@ }, "rust-overlay": { "inputs": { - "flake-utils": [ - "flake-utils" - ], "nixpkgs": [ "nixpkgs" ] }, "locked": { - "lastModified": 1717121863, - "narHash": "sha256-/3sxIe7MZqF/jw1RTQCSmgTjwVod43mmrk84m50MJQ4=", + "lastModified": 1732069891, + "narHash": "sha256-moKx8AVJrViCSdA0e0nSsG8b1dAsObI4sRAtbqbvBY8=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "2a7b53172ed08f856b8382d7dcfd36a4e0cbd866", + "rev": "8509a51241c407d583b1963d5079585a992506e8", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 8bcacf6..0180c8d 100644 --- a/flake.nix +++ b/flake.nix @@ -4,19 +4,13 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - crane = { - url = "github:ipetkov/crane"; - inputs.nixpkgs.follows = "nixpkgs"; - }; + crane.url = "github:ipetkov/crane"; flake-utils.url = "github:numtide/flake-utils"; rust-overlay = { url = "github:oxalica/rust-overlay"; - inputs = { - nixpkgs.follows = "nixpkgs"; - flake-utils.follows = "flake-utils"; - }; + inputs.nixpkgs.follows = "nixpkgs"; }; }; diff --git a/scripts/update_oauth_resources.sh b/scripts/update_oauth_resources.sh index 1d6b486..7eeb959 100755 --- a/scripts/update_oauth_resources.sh +++ b/scripts/update_oauth_resources.sh @@ -24,7 +24,7 @@ echo "// Please do not edit manually" >> "$filename" echo "// Filled in with real app versions" >> "$filename" # Open the array in the source file -echo "pub static _IOS_APP_VERSION_LIST: &[&str; $ios_app_count] = &[" >> "$filename" +echo "pub const _IOS_APP_VERSION_LIST: &[&str; $ios_app_count] = &[" >> "$filename" num=0 @@ -39,12 +39,12 @@ done echo "];" >> "$filename" # Fetch Android app versions -page_1=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions/" | rg "" -r "https://apkcombo.com\$1" | sort | uniq) +page_1=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions/" | rg "" -r "https://apkcombo.com\$1" | sort | uniq | sed 's/ //g') # Append with pages -page_2=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=2" | rg "" -r "https://apkcombo.com\$1" | sort | uniq) -page_3=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=3" | rg "" -r "https://apkcombo.com\$1" | sort | uniq) -page_4=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=4" | rg "" -r "https://apkcombo.com\$1" | sort | uniq) -page_5=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=5" | rg "" -r "https://apkcombo.com\$1" | sort | uniq) +page_2=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=2" | rg "" -r "https://apkcombo.com\$1" | sort | uniq | sed 's/ //g') +page_3=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=3" | rg "" -r "https://apkcombo.com\$1" | sort | uniq | sed 's/ //g') +page_4=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=4" | rg "" -r "https://apkcombo.com\$1" | sort | uniq | sed 's/ //g') +page_5=$(curl -s "https://apkcombo.com/reddit/com.reddit.frontpage/old-versions?page=5" | rg "" -r "https://apkcombo.com\$1" | sort | uniq | sed 's/ //g') # Concatenate all pages versions="${page_1}" @@ -63,7 +63,7 @@ android_count=$(echo "$versions" | wc -l) echo -e "Fetching \e[32m$android_count Android app versions...\e[0m" # Append to the source file -echo "pub static ANDROID_APP_VERSION_LIST: &[&str; $android_count] = &[" >> "$filename" +echo "pub const ANDROID_APP_VERSION_LIST: &[&str; $android_count] = &[" >> "$filename" num=0 @@ -89,7 +89,7 @@ ios_count=$(echo "$table" | wc -l) echo -e "Fetching \e[34m$ios_count iOS versions...\e[0m" # Append to the source file -echo "pub static _IOS_OS_VERSION_LIST: &[&str; $ios_count] = &[" >> "$filename" +echo "pub const _IOS_OS_VERSION_LIST: &[&str; $ios_count] = &[" >> "$filename" num=0 diff --git a/src/client.rs b/src/client.rs index f74ab51..19e195e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,7 +19,7 @@ use std::{io, result::Result}; use crate::dbg_msg; use crate::oauth::{force_refresh_token, token_daemon, Oauth}; use crate::server::RequestExt; -use crate::utils::format_url; +use crate::utils::{format_url, Post}; const REDDIT_URL_BASE: &str = "https://oauth.reddit.com"; const REDDIT_URL_BASE_HOST: &str = "oauth.reddit.com"; @@ -45,7 +45,7 @@ pub static OAUTH_RATELIMIT_REMAINING: AtomicU16 = AtomicU16::new(99); pub static OAUTH_IS_ROLLING_OVER: AtomicBool = AtomicBool::new(false); -static URL_PAIRS: [(&str, &str); 2] = [ +const URL_PAIRS: [(&str, &str); 2] = [ (ALTERNATIVE_REDDIT_URL_BASE, ALTERNATIVE_REDDIT_URL_BASE_HOST), (REDDIT_SHORT_URL_BASE, REDDIT_SHORT_URL_BASE_HOST), ]; @@ -218,40 +218,28 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo // Construct the hyper client from the HTTPS connector. let client: &Lazy> = &CLIENT; - let (token, vendor_id, device_id, user_agent, loid) = { - let client = OAUTH_CLIENT.load_full(); - ( - client.token.clone(), - client.headers_map.get("Client-Vendor-Id").cloned().unwrap_or_default(), - client.headers_map.get("X-Reddit-Device-Id").cloned().unwrap_or_default(), - client.headers_map.get("User-Agent").cloned().unwrap_or_default(), - client.headers_map.get("x-reddit-loid").cloned().unwrap_or_default(), - ) - }; - // Build request to Reddit. When making a GET, request gzip compression. // (Reddit doesn't do brotli yet.) - let mut headers = vec![ - ("User-Agent", user_agent), - ("Client-Vendor-Id", vendor_id), - ("X-Reddit-Device-Id", device_id), - ("x-reddit-loid", loid), - ("Host", host.to_string()), - ("Authorization", format!("Bearer {token}")), - ("Accept-Encoding", if method == Method::GET { "gzip".into() } else { "identity".into() }), + let mut headers: Vec<(String, String)> = vec![ + ("Host".into(), host.into()), + ("Accept-Encoding".into(), if method == Method::GET { "gzip".into() } else { "identity".into() }), ( - "Cookie", + "Cookie".into(), if quarantine { "_options=%7B%22pref_quarantine_optin%22%3A%20true%2C%20%22pref_gated_sr_optin%22%3A%20true%7D".into() } else { "".into() }, ), - ("X-Reddit-Width", fastrand::u32(300..500).to_string()), - ("X-Reddit-DPR", "2".to_owned()), - ("Device-Name", format!("Android {}", fastrand::u8(9..=14))), ]; + { + let client = OAUTH_CLIENT.load_full(); + for (key, value) in client.headers_map.clone() { + headers.push((key, value)); + } + } + // shuffle headers: https://github.com/redlib-org/redlib/issues/324 fastrand::shuffle(&mut headers); @@ -274,7 +262,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo return Ok(response); }; let location_header = response.headers().get(header::LOCATION); - if location_header == Some(&HeaderValue::from_static("https://www.reddit.com/")) { + if location_header == Some(&HeaderValue::from_static(ALTERNATIVE_REDDIT_URL_BASE)) { return Err("Reddit response was invalid".to_string()); } return request( @@ -400,6 +388,12 @@ pub async fn json(path: String, quarantine: bool) -> Result { "Ratelimit remaining: Header says {remaining}, we have {current_rate_limit}. Resets in {reset}. Rollover: {}. Ratelimit used: {used}", if is_rolling_over { "yes" } else { "no" }, ); + + // If can parse remaining as a float, round to a u16 and save + if let Ok(val) = remaining.parse::() { + OAUTH_RATELIMIT_REMAINING.store(val.round() as u16, Ordering::SeqCst); + } + Some(reset) } else { None @@ -484,8 +478,57 @@ pub async fn json(path: String, quarantine: bool) -> Result { } } +async fn self_check(sub: &str) -> Result<(), String> { + let query = format!("/r/{sub}/hot.json?&raw_json=1"); + + match Post::fetch(&query, true).await { + Ok(_) => Ok(()), + Err(e) => Err(e), + } +} + +pub async fn rate_limit_check() -> Result<(), String> { + // First, check a subreddit. + self_check("reddit").await?; + // This will reduce the rate limit to 99. Assert this check. + if OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst) != 99 { + return Err(format!("Rate limit check failed: expected 99, got {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst))); + } + // Now, we switch out the OAuth client. + // This checks for the IP rate limit association. + force_refresh_token().await; + // Now, check a new sub to break cache. + self_check("rust").await?; + // Again, assert the rate limit check. + if OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst) != 99 { + return Err(format!("Rate limit check failed: expected 99, got {}", OAUTH_RATELIMIT_REMAINING.load(Ordering::SeqCst))); + } + + Ok(()) +} + #[cfg(test)] -static POPULAR_URL: &str = "/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL"; +use {crate::config::get_setting, sealed_test::prelude::*}; + +#[tokio::test(flavor = "multi_thread")] +async fn test_rate_limit_check() { + rate_limit_check().await.unwrap(); +} + +#[test] +#[sealed_test(env = [("REDLIB_DEFAULT_SUBSCRIPTIONS", "rust")])] +fn test_default_subscriptions() { + tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(async { + let subscriptions = get_setting("REDLIB_DEFAULT_SUBSCRIPTIONS"); + assert!(subscriptions.is_some()); + + // check rate limit + rate_limit_check().await.unwrap(); + }); +} + +#[cfg(test)] +const POPULAR_URL: &str = "/r/popular/hot.json?&raw_json=1&geo_filter=GLOBAL"; #[tokio::test(flavor = "multi_thread")] async fn test_localization_popular() { @@ -501,12 +544,6 @@ async fn test_obfuscated_share_link() { assert_eq!(canonical_path(share_link, 3).await, Ok(Some(canonical_link))); } -#[tokio::test(flavor = "multi_thread")] -async fn test_share_link_strip_json() { - let link = "/17krzvz".into(); - let canonical_link = "/comments/17krzvz".into(); - assert_eq!(canonical_path(link, 3).await, Ok(Some(canonical_link))); -} #[tokio::test(flavor = "multi_thread")] async fn test_private_sub() { let link = json("/r/suicide/about.json?raw_json=1".into(), true).await; diff --git a/src/config.rs b/src/config.rs index 9b9fe75..f36dd97 100644 --- a/src/config.rs +++ b/src/config.rs @@ -125,6 +125,9 @@ pub struct Config { #[serde(rename = "REDLIB_FULL_URL")] pub(crate) full_url: Option, + + #[serde(rename = "REDLIB_DEFAULT_REMOVE_DEFAULT_FEEDS")] + pub(crate) default_remove_default_feeds: Option, } impl Config { @@ -176,6 +179,7 @@ impl Config { pushshift: parse("REDLIB_PUSHSHIFT_FRONTEND"), enable_rss: parse("REDLIB_ENABLE_RSS"), full_url: parse("REDLIB_FULL_URL"), + default_remove_default_feeds: parse("REDLIB_DEFAULT_REMOVE_DEFAULT_FEEDS"), } } } @@ -209,6 +213,7 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option { "REDLIB_PUSHSHIFT_FRONTEND" => config.pushshift.clone(), "REDLIB_ENABLE_RSS" => config.enable_rss.clone(), "REDLIB_FULL_URL" => config.full_url.clone(), + "REDLIB_DEFAULT_REMOVE_DEFAULT_FEEDS" => config.default_remove_default_feeds.clone(), _ => None, } } diff --git a/src/instance_info.rs b/src/instance_info.rs index 00a142f..16261d5 100644 --- a/src/instance_info.rs +++ b/src/instance_info.rs @@ -128,6 +128,7 @@ impl InstanceInfo { ["Pushshift frontend", &convert(&self.config.pushshift)], ["RSS enabled", &convert(&self.config.enable_rss)], ["Full URL", &convert(&self.config.full_url)], + ["Remove default feeds", &convert(&self.config.default_remove_default_feeds)], //TODO: fallback to crate::config::DEFAULT_PUSHSHIFT_FRONTEND ]) .with_header_row(["Settings"]), @@ -172,6 +173,7 @@ impl InstanceInfo { Pushshift frontend: {:?}\n RSS enabled: {:?}\n Full URL: {:?}\n + Remove default feeds: {:?}\n Config:\n Banner: {:?}\n Hide awards: {:?}\n @@ -201,6 +203,7 @@ impl InstanceInfo { self.config.sfw_only, self.config.enable_rss, self.config.full_url, + self.config.default_remove_default_feeds, self.config.pushshift, self.config.banner, self.config.default_hide_awards, diff --git a/src/main.rs b/src/main.rs index 365624f..ca3b550 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,9 +9,9 @@ use std::str::FromStr; use futures_lite::FutureExt; use hyper::Uri; use hyper::{header::HeaderValue, Body, Request, Response}; -use log::info; +use log::{info, warn}; use once_cell::sync::Lazy; -use redsunlib::client::{canonical_path, proxy, CLIENT}; +use redsunlib::client::{canonical_path, proxy, rate_limit_check, CLIENT}; use redsunlib::server::{self, RequestExt}; use redsunlib::utils::{error, redirect, MascotAssets, ThemeAssets}; use redsunlib::{config, duplicates, headers, instance_info, post, search, settings, subreddit, user}; @@ -75,6 +75,17 @@ async fn ffmpeg() -> Result, String> { ) } +async fn opensearch() -> Result, String> { + Ok( + Response::builder() + .status(200) + .header("content-type", "application/opensearchdescription+xml") + .header("Cache-Control", "public, max-age=1209600, s-maxage=86400") + .body(include_bytes!("../static/opensearch.xml").as_ref().into()) + .unwrap_or_default(), + ) +} + async fn resource(body: &str, content_type: &str, cache: bool) -> Result, String> { let mut res = Response::builder() .status(200) @@ -132,6 +143,8 @@ async fn main() { let matches = Command::new("Redlib") .version(env!("CARGO_PKG_VERSION")) .about("Private front-end for Reddit written in Rust ") + .arg(Arg::new("ipv4-only").short('4').long("ipv4-only").help("Listen on IPv4 only").num_args(0)) + .arg(Arg::new("ipv6-only").short('6').long("ipv6-only").help("Listen on IPv6 only").num_args(0)) .arg( Arg::new("redirect-https") .short('r') @@ -170,11 +183,34 @@ async fn main() { ) .get_matches(); + match rate_limit_check().await { + Ok(()) => { + info!("[✅] Rate limit check passed"); + } + Err(e) => { + let mut message = format!("Rate limit check failed: {}", e); + message += "\nThis may cause issues with the rate limit."; + message += "\nPlease report this error with the above information."; + message += "\nhttps://github.com/redlib-org/redlib/issues/new?assignees=sigaloid&labels=bug&title=%F0%9F%90%9B+Bug+Report%3A+Rate+limit+mismatch"; + warn!("{}", message); + eprintln!("{}", message); + } + } + let address = matches.get_one::("address").unwrap(); let port = matches.get_one::("port").unwrap(); let hsts = matches.get_one("hsts").map(|m: &String| m.as_str()); - let listener = [address, ":", port].concat(); + let ipv4_only = std::env::var("IPV4_ONLY").is_ok() || matches.get_flag("ipv4-only"); + let ipv6_only = std::env::var("IPV6_ONLY").is_ok() || matches.get_flag("ipv6-only"); + + let listener = if ipv4_only { + format!("0.0.0.0:{}", port) + } else if ipv6_only { + format!("[::]:{}", port) + } else { + [address, ":", port].concat() + }; println!("Starting Redsunlib..."); @@ -233,6 +269,7 @@ async fn main() { app.at("/Inter.var.woff2").get(|_| font().boxed()); app.at("/touch-icon-iphone.png").get(|_| iphone_logo().boxed()); app.at("/apple-touch-icon.png").get(|_| iphone_logo().boxed()); + app.at("/opensearch.xml").get(|_| opensearch().boxed()); app .at("/videoUtils.js") .get(|_| resource(include_str!("../static/videoUtils.js"), "text/javascript", false).boxed()); @@ -245,6 +282,7 @@ async fn main() { app .at("/check_update.js") .get(|_| resource(include_str!("../static/check_update.js"), "text/javascript", false).boxed()); + app.at("/copy.js").get(|_| resource(include_str!("../static/copy.js"), "text/javascript", false).boxed()); app.at("/commits.json").get(|_| async move { proxy_commit_info().await }.boxed()); app.at("/instances.json").get(|_| async move { proxy_instances().await }.boxed()); @@ -304,6 +342,7 @@ async fn main() { // Configure settings app.at("/settings").get(|r| settings::get(r).boxed()).post(|r| settings::set(r).boxed()); app.at("/settings/restore").get(|r| settings::restore(r).boxed()); + app.at("/settings/encoded-restore").post(|r| settings::encoded_restore(r).boxed()); app.at("/settings/update").get(|r| settings::update(r).boxed()); // Mascots @@ -405,7 +444,7 @@ async fn main() { Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await, // Short link for post - Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{id}"), 3).await { + Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/comments/{id}"), 3).await { Ok(path_opt) => match path_opt { Some(path) => Ok(redirect(&path)), None => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await, diff --git a/src/oauth.rs b/src/oauth.rs index d15fa2f..fd8d831 100644 --- a/src/oauth.rs +++ b/src/oauth.rs @@ -9,11 +9,14 @@ use hyper::{client, Body, Method, Request}; use log::{debug, error, info, trace}; use serde_json::json; +use tegen::tegen::TextGenerator; use tokio::time::{error::Elapsed, timeout}; -static REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg"; +const REDDIT_ANDROID_OAUTH_CLIENT_ID: &str = "ohXpoqrZYub1kg"; -static AUTH_ENDPOINT: &str = "https://www.reddit.com"; +const AUTH_ENDPOINT: &str = "https://www.reddit.com"; + +const OAUTH_TIMEOUT: Duration = Duration::from_secs(5); // Spoofed client for Android devices #[derive(Debug, Clone, Default)] @@ -32,24 +35,30 @@ impl Oauth { loop { let attempt = Self::new_with_timeout().await; match attempt { - Ok(Some(oauth)) => { + Ok(Ok(oauth)) => { info!("[✅] Successfully created OAuth client"); return oauth; } - Ok(None) => { - error!("Failed to create OAuth client. Retrying in 5 seconds..."); - continue; + Ok(Err(e)) => { + error!("Failed to create OAuth client: {}. Retrying in 5 seconds...", { + match e { + AuthError::Hyper(error) => error.to_string(), + AuthError::SerdeDeserialize(error) => error.to_string(), + AuthError::Field((value, error)) => format!("{error}\n{value}"), + } + }); } - Err(duration) => { - error!("Failed to create OAuth client in {duration:?}. Retrying in 5 seconds..."); + Err(_) => { + error!("Failed to create OAuth client before timeout. Retrying in 5 seconds..."); } } + tokio::time::sleep(OAUTH_TIMEOUT).await; } } - async fn new_with_timeout() -> Result, Elapsed> { + async fn new_with_timeout() -> Result, Elapsed> { let mut oauth = Self::default(); - timeout(Duration::from_secs(5), oauth.login()).await.map(|result| result.map(|_| oauth)) + timeout(OAUTH_TIMEOUT, oauth.login()).await.map(|result: Result<(), AuthError>| result.map(|_| oauth)) } pub(crate) fn default() -> Self { @@ -66,7 +75,7 @@ impl Oauth { device, } } - async fn login(&mut self) -> Option<()> { + async fn login(&mut self) -> Result<(), AuthError> { // Construct URL for OAuth token let url = format!("{AUTH_ENDPOINT}/auth/v2/oauth/access-token/loid"); let mut builder = Request::builder().method(Method::POST).uri(&url); @@ -84,20 +93,21 @@ impl Oauth { // Set JSON body. I couldn't tell you what this means. But that's what the client sends let json = json!({ - "scopes": ["*","email"] + "scopes": ["*","email", "pii"] }); let body = Body::from(json.to_string()); // Build request let request = builder.body(body).unwrap(); - trace!("Sending token request..."); + trace!("Sending token request...\n\n{request:?}"); // Send request let client: &once_cell::sync::Lazy> = &CLIENT; - let resp = client.request(request).await.ok()?; + let resp = client.request(request).await?; trace!("Received response with status {} and length {:?}", resp.status(), resp.headers().get("content-length")); + trace!("OAuth headers: {:#?}", resp.headers()); // Parse headers - loid header _should_ be saved sent on subsequent token refreshes. // Technically it's not needed, but it's easy for Reddit API to check for this. @@ -105,30 +115,58 @@ impl Oauth { // Not worried about the privacy implications, since this is randomly changed // and really only as privacy-concerning as the OAuth token itself. if let Some(header) = resp.headers().get("x-reddit-loid") { - self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().ok()?.to_string()); + self.headers_map.insert("x-reddit-loid".to_owned(), header.to_str().unwrap().to_string()); } // Same with x-reddit-session if let Some(header) = resp.headers().get("x-reddit-session") { - self.headers_map.insert("x-reddit-session".to_owned(), header.to_str().ok()?.to_string()); + self.headers_map.insert("x-reddit-session".to_owned(), header.to_str().unwrap().to_string()); } trace!("Serializing response..."); // Serialize response - let body_bytes = hyper::body::to_bytes(resp.into_body()).await.ok()?; - let json: serde_json::Value = serde_json::from_slice(&body_bytes).ok()?; + let body_bytes = hyper::body::to_bytes(resp.into_body()).await?; + let json: serde_json::Value = serde_json::from_slice(&body_bytes)?; trace!("Accessing relevant fields..."); // Save token and expiry - self.token = json.get("access_token")?.as_str()?.to_string(); - self.expires_in = json.get("expires_in")?.as_u64()?; + self.token = json + .get("access_token") + .ok_or_else(|| AuthError::Field((json.clone(), "access_token")))? + .as_str() + .ok_or_else(|| AuthError::Field((json.clone(), "access_token: as_str")))? + .to_string(); + self.expires_in = json + .get("expires_in") + .ok_or_else(|| AuthError::Field((json.clone(), "expires_in")))? + .as_u64() + .ok_or_else(|| AuthError::Field((json.clone(), "expires_in: as_u64")))?; self.headers_map.insert("Authorization".to_owned(), format!("Bearer {}", self.token)); info!("[✅] Success - Retrieved token \"{}...\", expires in {}", &self.token[..32], self.expires_in); - Some(()) + Ok(()) + } +} + +#[derive(Debug)] +enum AuthError { + Hyper(hyper::Error), + SerdeDeserialize(serde_json::Error), + Field((serde_json::Value, &'static str)), +} + +impl From for AuthError { + fn from(err: hyper::Error) -> Self { + AuthError::Hyper(err) + } +} + +impl From for AuthError { + fn from(err: serde_json::Error) -> Self { + AuthError::SerdeDeserialize(err) } } @@ -185,11 +223,22 @@ impl Device { let android_user_agent = format!("Reddit/{android_app_version}/Android {android_version}"); + let qos = fastrand::u32(1000..=100_000); + let qos: f32 = qos as f32 / 1000.0; + let qos = format!("{:.3}", qos); + + let codecs = TextGenerator::new().generate("available-codecs=video/avc, video/hevc{, video/x-vnd.on2.vp9|}"); + // Android device headers - let headers = HashMap::from([ - ("Client-Vendor-Id".into(), uuid.clone()), - ("X-Reddit-Device-Id".into(), uuid.clone()), + let headers: HashMap = HashMap::from([ ("User-Agent".into(), android_user_agent), + ("x-reddit-retry".into(), "algo=no-retries".into()), + ("x-reddit-compression".into(), "1".into()), + ("x-reddit-qos".into(), qos), + ("x-reddit-media-codecs".into(), codecs), + ("Content-Type".into(), "application/json; charset=UTF-8".into()), + ("client-vendor-id".into(), uuid.clone()), + ("X-Reddit-Device-Id".into(), uuid.clone()), ]); info!("[🔄] Spoofing Android client with headers: {headers:?}, uuid: \"{uuid}\", and OAuth ID \"{REDDIT_ANDROID_OAUTH_CLIENT_ID}\""); diff --git a/src/oauth_resources.rs b/src/oauth_resources.rs index 3272939..01928c3 100644 --- a/src/oauth_resources.rs +++ b/src/oauth_resources.rs @@ -2,8 +2,38 @@ // Rerun scripts/update_oauth_resources.sh to update this file // Please do not edit manually // Filled in with real app versions -pub static _IOS_APP_VERSION_LIST: &[&str; 1] = &[""]; -pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ +pub const _IOS_APP_VERSION_LIST: &[&str; 1] = &[""]; +pub const ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ + "Version 2024.22.1/Build 1652272", + "Version 2024.23.1/Build 1665606", + "Version 2024.24.1/Build 1682520", + "Version 2024.25.0/Build 1693595", + "Version 2024.25.2/Build 1700401", + "Version 2024.25.3/Build 1703490", + "Version 2024.26.0/Build 1710470", + "Version 2024.26.1/Build 1717435", + "Version 2024.28.0/Build 1737665", + "Version 2024.28.1/Build 1741165", + "Version 2024.30.0/Build 1770787", + "Version 2024.31.0/Build 1786202", + "Version 2024.32.0/Build 1809095", + "Version 2024.32.1/Build 1813258", + "Version 2024.33.0/Build 1819908", + "Version 2024.34.0/Build 1837909", + "Version 2024.35.0/Build 1861437", + "Version 2024.36.0/Build 1875012", + "Version 2024.37.0/Build 1888053", + "Version 2024.38.0/Build 1902791", + "Version 2024.39.0/Build 1916713", + "Version 2024.40.0/Build 1928580", + "Version 2024.41.0/Build 1941199", + "Version 2024.41.1/Build 1947805", + "Version 2024.42.0/Build 1952440", + "Version 2024.43.0/Build 1972250", + "Version 2024.44.0/Build 1988458", + "Version 2024.45.0/Build 2001943", + "Version 2024.46.0/Build 2012731", + "Version 2024.47.0/Build 2029755", "Version 2023.48.0/Build 1319123", "Version 2023.49.0/Build 1321715", "Version 2023.49.1/Build 1322281", @@ -31,9 +61,9 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ "Version 2024.20.0/Build 1612800", "Version 2024.20.1/Build 1615586", "Version 2024.20.2/Build 1624969", + "Version 2024.20.3/Build 1624970", "Version 2024.21.0/Build 1631686", "Version 2024.22.0/Build 1645257", - "Version 2024.22.1/Build 1652272", "Version 2023.21.0/Build 956283", "Version 2023.22.0/Build 968223", "Version 2023.23.0/Build 983896", @@ -124,35 +154,5 @@ pub static ANDROID_APP_VERSION_LIST: &[&str; 150] = &[ "Version 2022.40.0/Build 624782", "Version 2022.41.0/Build 630468", "Version 2022.41.1/Build 634168", - "Version 2021.39.1/Build 372418", - "Version 2021.41.0/Build 376052", - "Version 2021.42.0/Build 378193", - "Version 2021.43.0/Build 382019", - "Version 2021.44.0/Build 385129", - "Version 2021.45.0/Build 387663", - "Version 2021.46.0/Build 392043", - "Version 2021.47.0/Build 394342", - "Version 2022.10.0/Build 429896", - "Version 2022.1.0/Build 402829", - "Version 2022.11.0/Build 433004", - "Version 2022.12.0/Build 436848", - "Version 2022.13.0/Build 442084", - "Version 2022.13.1/Build 444621", - "Version 2022.14.1/Build 452742", - "Version 2022.15.0/Build 455453", - "Version 2022.16.0/Build 462377", - "Version 2022.17.0/Build 468480", - "Version 2022.18.0/Build 473740", - "Version 2022.19.1/Build 482464", - "Version 2022.2.0/Build 405543", - "Version 2022.3.0/Build 408637", - "Version 2022.4.0/Build 411368", - "Version 2022.5.0/Build 414731", - "Version 2022.6.0/Build 418391", - "Version 2022.6.1/Build 419585", - "Version 2022.6.2/Build 420562", - "Version 2022.7.0/Build 420849", - "Version 2022.8.0/Build 423906", - "Version 2022.9.0/Build 426592", ]; -pub static _IOS_OS_VERSION_LIST: &[&str; 1] = &[""]; +pub const _IOS_OS_VERSION_LIST: &[&str; 1] = &[""]; diff --git a/src/search.rs b/src/search.rs index 5962c2e..88dcfdd 100644 --- a/src/search.rs +++ b/src/search.rs @@ -75,7 +75,11 @@ pub async fn find(req: Request) -> Result, String> { return Ok(redirect(&format!("/{query}"))); } - if query.starts_with("u/") { + if query.starts_with("R/") { + return Ok(redirect(&format!("/r{}", &query[1..]))); + } + + if query.starts_with("u/") || query.starts_with("U/") { return Ok(redirect(&format!("/user{}", &query[1..]))); } diff --git a/src/server.rs b/src/server.rs index 15c56ad..a287de2 100644 --- a/src/server.rs +++ b/src/server.rs @@ -25,7 +25,7 @@ use std::{ str::{from_utf8, Split}, string::ToString, }; -use time::Duration; +use time::OffsetDateTime; use crate::dbg_msg; @@ -170,10 +170,8 @@ impl ResponseExt for Response { } fn remove_cookie(&mut self, name: String) { - let mut cookie = Cookie::from(name); - cookie.set_path("/"); - cookie.set_max_age(Duration::seconds(1)); - if let Ok(val) = header::HeaderValue::from_str(&cookie.to_string()) { + let removal_cookie = Cookie::build(name).path("/").http_only(true).expires(OffsetDateTime::now_utc()); + if let Ok(val) = header::HeaderValue::from_str(&removal_cookie.to_string()) { self.headers_mut().append("Set-Cookie", val); } } @@ -240,8 +238,14 @@ impl Server { path.pop(); } + // Replace HEAD with GET for routing + let (method, is_head) = match req.method() { + &Method::HEAD => (&Method::GET, true), + method => (method, false), + }; + // Match the visited path with an added route - match router.recognize(&format!("/{}{}", req.method().as_str(), path)) { + match router.recognize(&format!("/{}{}", method.as_str(), path)) { // If a route was configured for this path Ok(found) => { let mut parammed = req; @@ -253,17 +257,21 @@ impl Server { match func.await { Ok(mut res) => { res.headers_mut().extend(def_headers); - let _ = compress_response(&req_headers, &mut res).await; + if is_head { + *res.body_mut() = Body::empty(); + } else { + let _ = compress_response(&req_headers, &mut res).await; + } Ok(res) } - Err(msg) => new_boilerplate(def_headers, req_headers, 500, Body::from(msg)).await, + Err(msg) => new_boilerplate(def_headers, req_headers, 500, if is_head { Body::empty() } else { Body::from(msg) }).await, } } .boxed() } // If there was a routing error - Err(e) => new_boilerplate(def_headers, req_headers, 404, e.into()).boxed(), + Err(e) => new_boilerplate(def_headers, req_headers, 404, if is_head { Body::empty() } else { e.into() }).boxed(), } })) } @@ -274,8 +282,19 @@ impl Server { // Bind server to address specified above. Gracefully shut down if CTRL+C is pressed let server = HyperServer::bind(address).serve(make_svc).with_graceful_shutdown(async { + #[cfg(windows)] // Wait for the CTRL+C signal tokio::signal::ctrl_c().await.expect("Failed to install CTRL+C signal handler"); + + #[cfg(unix)] + { + // Wait for CTRL+C or SIGTERM signals + let mut signal_terminate = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()).expect("Failed to install SIGTERM signal handler"); + tokio::select! { + _ = tokio::signal::ctrl_c() => (), + _ = signal_terminate.recv() => () + } + } }); server.boxed() diff --git a/src/settings.rs b/src/settings.rs index bed5c9c..53380cb 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -4,12 +4,15 @@ use std::collections::HashMap; // CRATES use crate::server::ResponseExt; -use crate::utils::{redirect, template, Preferences}; +use crate::subreddit::join_until_size_limit; +use crate::utils::{deflate_decompress, redirect, template, Preferences}; use cookie::Cookie; use futures_lite::StreamExt; use hyper::{Body, Request, Response}; use rinja::Template; use time::{Duration, OffsetDateTime}; +use tokio::time::timeout; +use url::form_urlencoded; // STRUCTS #[derive(Template)] @@ -21,7 +24,7 @@ struct SettingsTemplate { // CONSTANTS -const PREFS: [&str; 22] = [ +const PREFS: [&str; 23] = [ "theme", "mascot", "redsunlib_colorway", @@ -44,6 +47,7 @@ const PREFS: [&str; 22] = [ "hide_score", "disable_visit_reddit_confirmation", "video_quality", + "remove_default_feeds", ]; // FUNCTIONS @@ -140,6 +144,119 @@ fn set_cookies_method(req: Request, remove_cookies: bool) -> Response = subscriptions.expect("Subscriptions").split('+').map(str::to_string).collect(); + + // Start at 0 to keep track of what number we need to start deleting old subscription cookies from + let mut subscriptions_number_to_delete_from = 0; + + // Starting at 0 so we handle the subscription cookie without a number first + for (subscriptions_number, list) in join_until_size_limit(&sub_list).into_iter().enumerate() { + let subscriptions_cookie = if subscriptions_number == 0 { + "subscriptions".to_string() + } else { + format!("subscriptions{}", subscriptions_number) + }; + + response.insert_cookie( + Cookie::build((subscriptions_cookie, list)) + .path("/") + .http_only(true) + .expires(OffsetDateTime::now_utc() + Duration::weeks(52)) + .into(), + ); + + subscriptions_number_to_delete_from += 1; + } + + // While subscriptionsNUMBER= is in the string of cookies add a response removing that cookie + while cookies_string.contains(&format!("subscriptions{subscriptions_number_to_delete_from}=")) { + // Remove that subscriptions cookie + response.remove_cookie(format!("subscriptions{subscriptions_number_to_delete_from}")); + + // Increment subscriptions cookie number + subscriptions_number_to_delete_from += 1; + } + } else { + // Remove unnumbered subscriptions cookie + response.remove_cookie("subscriptions".to_string()); + + // Starts at one to deal with the first numbered subscription cookie and onwards + let mut subscriptions_number_to_delete_from = 1; + + // While subscriptionsNUMBER= is in the string of cookies add a response removing that cookie + while cookies_string.contains(&format!("subscriptions{subscriptions_number_to_delete_from}=")) { + // Remove that subscriptions cookie + response.remove_cookie(format!("subscriptions{subscriptions_number_to_delete_from}")); + + // Increment subscriptions cookie number + subscriptions_number_to_delete_from += 1; + } + } + + // If there are filters to restore set them and delete any old filters cookies, otherwise delete them all + if filters.is_some() { + let filters_list: Vec = filters.expect("Filters").split('+').map(str::to_string).collect(); + + // Start at 0 to keep track of what number we need to start deleting old subscription cookies from + let mut filters_number_to_delete_from = 0; + + // Starting at 0 so we handle the subscription cookie without a number first + for (filters_number, list) in join_until_size_limit(&filters_list).into_iter().enumerate() { + let filters_cookie = if filters_number == 0 { + "filters".to_string() + } else { + format!("filters{}", filters_number) + }; + + response.insert_cookie( + Cookie::build((filters_cookie, list)) + .path("/") + .http_only(true) + .expires(OffsetDateTime::now_utc() + Duration::weeks(52)) + .into(), + ); + + filters_number_to_delete_from += 1; + } + + // While filtersNUMBER= is in the string of cookies add a response removing that cookie + while cookies_string.contains(&format!("filters{filters_number_to_delete_from}=")) { + // Remove that filters cookie + response.remove_cookie(format!("filters{filters_number_to_delete_from}")); + + // Increment filters cookie number + filters_number_to_delete_from += 1; + } + } else { + // Remove unnumbered filters cookie + response.remove_cookie("filters".to_string()); + + // Starts at one to deal with the first numbered subscription cookie and onwards + let mut filters_number_to_delete_from = 1; + + // While filtersNUMBER= is in the string of cookies add a response removing that cookie + while cookies_string.contains(&format!("filters{filters_number_to_delete_from}=")) { + // Remove that sfilters cookie + response.remove_cookie(format!("filters{filters_number_to_delete_from}")); + + // Increment filters cookie number + filters_number_to_delete_from += 1; + } + } + response } @@ -151,3 +268,35 @@ pub async fn restore(req: Request) -> Result, String> { pub async fn update(req: Request) -> Result, String> { Ok(set_cookies_method(req, false)) } + +pub async fn encoded_restore(req: Request) -> Result, String> { + let body = hyper::body::to_bytes(req.into_body()) + .await + .map_err(|e| format!("Failed to get bytes from request body: {}", e))?; + + if body.len() > 1024 * 1024 { + return Err("Request body too large".to_string()); + } + + let encoded_prefs = form_urlencoded::parse(&body) + .find(|(key, _)| key == "encoded_prefs") + .map(|(_, value)| value) + .ok_or_else(|| "encoded_prefs parameter not found in request body".to_string())?; + + let bytes = base2048::decode(&encoded_prefs).ok_or_else(|| "Failed to decode base2048 encoded preferences".to_string())?; + + let out = timeout(std::time::Duration::from_secs(1), async { deflate_decompress(bytes) }) + .await + .map_err(|e| format!("Failed to decompress bytes: {}", e))??; + + let mut prefs: Preferences = timeout(std::time::Duration::from_secs(1), async { bincode::deserialize(&out) }) + .await + .map_err(|e| format!("Failed to deserialize preferences: {}", e))? + .map_err(|e| format!("Failed to deserialize bytes into Preferences struct: {}", e))?; + + prefs.available_themes = vec![]; + + let url = format!("/settings/restore/?{}", prefs.to_urlencoded()?); + + Ok(redirect(&url)) +} diff --git a/src/user.rs b/src/user.rs index 6866700..a612d17 100644 --- a/src/user.rs +++ b/src/user.rs @@ -5,6 +5,8 @@ use crate::client::json; use crate::server::RequestExt; use crate::utils::{error, filter_posts, format_url, get_filters, nsfw_landing, param, setting, template, Post, Preferences, User}; use crate::{config, utils}; +use chrono::DateTime; +use htmlescape::decode_html; use hyper::{Body, Request, Response}; use log::trace; use rinja::Template; @@ -165,9 +167,10 @@ pub async fn rss(req: Request) -> Result, String> { .into_iter() .map(|post| Item { title: Some(post.title.to_string()), - link: Some(utils::get_post_url(&post)), + link: Some(format_url(&utils::get_post_url(&post))), author: Some(post.author.name), - content: Some(rewrite_urls(&post.body)), + pub_date: Some(DateTime::from_timestamp(post.created_ts as i64, 0).unwrap_or_default().to_rfc2822()), + content: Some(rewrite_urls(&decode_html(&post.body).unwrap())), ..Default::default() }) .collect::>(), diff --git a/src/utils.rs b/src/utils.rs index 8fc638a..9069742 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -8,16 +8,19 @@ use crate::config::{self, get_setting}; use crate::{client::json, server::RequestExt}; use cookie::Cookie; use hyper::{Body, Request, Response}; +use libflate::deflate::{Decoder, Encoder}; use log::error; use once_cell::sync::Lazy; use regex::Regex; +use revision::revisioned; use rinja::Template; use rust_embed::RustEmbed; -use serde::Serialize; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; use serde_json_path::{JsonPath, JsonPathExt}; use std::collections::{HashMap, HashSet}; use std::env; +use std::io::{Read, Write}; use std::str::FromStr; use std::string::ToString; use time::{macros::format_description, Duration, OffsetDateTime}; @@ -233,6 +236,14 @@ impl Media { // If this post contains a gallery of images gallery = GalleryMedia::parse(&data["gallery_data"]["items"], &data["media_metadata"]); + ("gallery", &data["url"], None) + } else if data["crosspost_parent_list"][0]["is_gallery"].as_bool().unwrap_or_default() { + // If this post contains a gallery of images + gallery = GalleryMedia::parse( + &data["crosspost_parent_list"][0]["gallery_data"]["items"], + &data["crosspost_parent_list"][0]["media_metadata"], + ); + ("gallery", &data["url"], None) } else if data["is_reddit_media_domain"].as_bool().unwrap_or_default() && data["domain"] == "i.redd.it" { // If this post contains a reddit media (image) URL. @@ -542,6 +553,14 @@ pub struct ErrorTemplate { pub url: String, } +#[derive(Template)] +#[template(path = "info.html")] +pub struct InfoTemplate { + pub msg: String, + pub prefs: Preferences, + pub url: String, +} + /// Template for NSFW landing page. The landing page is displayed when a page's /// content is wholly NSFW, but a user has not enabled the option to view NSFW /// posts. @@ -603,35 +622,86 @@ pub struct Params { pub before: Option, } -#[derive(Default)] +#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[revisioned(revision = 1)] pub struct Preferences { + #[revision(start = 1)] + #[serde(skip_serializing, skip_deserializing)] pub available_themes: Vec, + #[revision(start = 1)] pub available_mascots: Vec, + #[revision(start = 1)] pub theme: String, + #[revision(start = 1)] pub mascot: String, + #[revision(start = 1)] pub redsunlib_colorway: String, + #[revision(start = 1)] pub front_page: String, + #[revision(start = 1)] pub layout: String, + #[revision(start = 1)] pub wide: String, + #[revision(start = 1)] pub blur_spoiler: String, + #[revision(start = 1)] pub show_nsfw: String, + #[revision(start = 1)] pub blur_nsfw: String, + #[revision(start = 1)] pub hide_hls_notification: String, + #[revision(start = 1)] pub video_quality: String, + #[revision(start = 1)] pub hide_sidebar_and_summary: String, + #[revision(start = 1)] pub hide_banner: String, + #[revision(start = 1)] pub use_hls: String, + #[revision(start = 1)] pub ffmpeg_video_downloads: String, + #[revision(start = 1)] pub autoplay_videos: String, + #[revision(start = 1)] pub fixed_navbar: String, + #[revision(start = 1)] pub disable_visit_reddit_confirmation: String, + #[revision(start = 1)] pub comment_sort: String, + #[revision(start = 1)] pub post_sort: String, + #[revision(start = 1)] + #[serde(serialize_with = "serialize_vec_with_plus", deserialize_with = "deserialize_vec_with_plus")] pub subscriptions: Vec, + #[revision(start = 1)] pub quicklist: Vec, + #[revision(start = 1)] + #[serde(serialize_with = "serialize_vec_with_plus", deserialize_with = "deserialize_vec_with_plus")] pub filters: Vec, + #[revision(start = 1)] pub hide_awards: String, + #[revision(start = 1)] pub hide_score: String, + #[revision(start = 1)] + pub remove_default_feeds: String, +} + +fn serialize_vec_with_plus(vec: &[String], serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&vec.join("+")) +} + +fn deserialize_vec_with_plus<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let string = String::deserialize(deserializer)?; + if string.is_empty() { + return Ok(Vec::new()); + } + Ok(string.split('+').map(|s| s.to_string()).collect()) } #[derive(RustEmbed)] @@ -689,8 +759,36 @@ impl Preferences { quicklist: setting(req, "quicklist").split('+').map(String::from).filter(|s| !s.is_empty()).collect(), hide_awards: setting(req, "hide_awards"), hide_score: setting(req, "hide_score"), + remove_default_feeds: setting(req, "remove_default_feeds"), } } + + pub fn to_urlencoded(&self) -> Result { + serde_urlencoded::to_string(self).map_err(|e| e.to_string()) + } + + pub fn to_bincode(&self) -> Result, String> { + bincode::serialize(self).map_err(|e| e.to_string()) + } + pub fn to_compressed_bincode(&self) -> Result, String> { + deflate_compress(self.to_bincode()?) + } + pub fn to_bincode_str(&self) -> Result { + Ok(base2048::encode(&self.to_compressed_bincode()?)) + } +} + +pub fn deflate_compress(i: Vec) -> Result, String> { + let mut e = Encoder::new(Vec::new()); + e.write_all(&i).map_err(|e| e.to_string())?; + e.finish().into_result().map_err(|e| e.to_string()) +} + +pub fn deflate_decompress(i: Vec) -> Result, String> { + let mut decoder = Decoder::new(&i[..]); + let mut out = Vec::new(); + decoder.read_to_end(&mut out).map_err(|e| format!("Failed to read from gzip decoder: {}", e))?; + Ok(out) } /// Gets a `HashSet` of filters from the cookie in the given `Request`. @@ -746,7 +844,15 @@ pub async fn parse_post(post: &Value) -> Post { get_setting("REDLIB_PUSHSHIFT_FRONTEND").unwrap_or_else(|| String::from(crate::config::DEFAULT_PUSHSHIFT_FRONTEND)), ) } else { - rewrite_urls(&val(post, "selftext_html")) + let selftext = val(post, "selftext"); + if selftext.contains("```") { + let mut html_output = String::new(); + let parser = pulldown_cmark::Parser::new(&selftext); + pulldown_cmark::html::push_html(&mut html_output, parser); + rewrite_urls(&html_output) + } else { + rewrite_urls(&val(post, "selftext_html")) + } }; // Build a post using data parsed from Reddit post API @@ -837,18 +943,72 @@ pub fn param(path: &str, value: &str) -> Option { // Retrieve the value of a setting by name pub fn setting(req: &Request, name: &str) -> String { // Parse a cookie value from request - req - .cookie(name) - .unwrap_or_else(|| { - // If there is no cookie for this setting, try receiving a default from the config - if let Some(default) = get_setting(&format!("REDLIB_DEFAULT_{}", name.to_uppercase())) { - Cookie::new(name, default) - } else { - Cookie::from(name) - } - }) - .value() - .to_string() + + // If this was called with "subscriptions" and the "subscriptions" cookie has a value + if name == "subscriptions" && req.cookie("subscriptions").is_some() { + // Create subscriptions string + let mut subscriptions = String::new(); + + // Default subscriptions cookie + if req.cookie("subscriptions").is_some() { + subscriptions.push_str(req.cookie("subscriptions").unwrap().value()); + } + + // Start with first numbered subscription cookie + let mut subscriptions_number = 1; + + // While whatever subscriptionsNUMBER cookie we're looking at has a value + while req.cookie(&format!("subscriptions{}", subscriptions_number)).is_some() { + // Push whatever subscriptionsNUMBER cookie we're looking at into the subscriptions string + subscriptions.push_str(req.cookie(&format!("subscriptions{}", subscriptions_number)).unwrap().value()); + + // Increment subscription cookie number + subscriptions_number += 1; + } + + // Return the subscriptions cookies as one large string + subscriptions + } + // If this was called with "filters" and the "filters" cookie has a value + else if name == "filters" && req.cookie("filters").is_some() { + // Create filters string + let mut filters = String::new(); + + // Default filters cookie + if req.cookie("filters").is_some() { + filters.push_str(req.cookie("filters").unwrap().value()); + } + + // Start with first numbered filters cookie + let mut filters_number = 1; + + // While whatever filtersNUMBER cookie we're looking at has a value + while req.cookie(&format!("filters{}", filters_number)).is_some() { + // Push whatever filtersNUMBER cookie we're looking at into the filters string + filters.push_str(req.cookie(&format!("filters{}", filters_number)).unwrap().value()); + + // Increment filters cookie number + filters_number += 1; + } + + // Return the filters cookies as one large string + filters + } + // The above two still come to this if there was no existing value + else { + req + .cookie(name) + .unwrap_or_else(|| { + // If there is no cookie for this setting, try receiving a default from the config + if let Some(default) = get_setting(&format!("REDLIB_DEFAULT_{}", name.to_uppercase())) { + Cookie::new(name, default) + } else { + Cookie::from(name) + } + }) + .value() + .to_string() + } } // Retrieve the value of a setting by name or the default value @@ -864,11 +1024,12 @@ pub fn setting_or_default(req: &Request, name: &str, default: String) -> S // Detect and redirect in the event of a random subreddit pub async fn catch_random(sub: &str, additional: &str) -> Result, String> { if sub == "random" || sub == "randnsfw" { - let new_sub = json(format!("/r/{sub}/about.json?raw_json=1"), false).await?["data"]["display_name"] - .as_str() - .unwrap_or_default() - .to_string(); - Ok(redirect(&format!("/r/{new_sub}{additional}"))) + Ok(redirect(&format!( + "/r/{}{additional}", + json(format!("/r/{sub}/about.json?raw_json=1"), false).await?["data"]["display_name"] + .as_str() + .unwrap_or_default() + ))) } else { Err("No redirect needed".to_string()) } @@ -946,9 +1107,20 @@ pub fn format_url(url: &str) -> String { } } +static REGEX_BULLET: Lazy = Lazy::new(|| Regex::new(r"(?m)^- (.*)$").unwrap()); +static REGEX_BULLET_CONSECUTIVE_LINES: Lazy = Lazy::new(|| Regex::new(r"\n