From 5c13aef41b45c07cab8f60d2dcc874d535405239 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 27 May 2024 06:28:10 +0100 Subject: [PATCH] refactor: migrate web server to Actix Web --- Cargo.lock | 556 ++++++++++++++++++++++++++--- Cargo.toml | 7 +- src/engine/fut/crawl.rs | 13 +- src/engine/mod.rs | 13 +- src/interactors/crates.rs | 27 +- src/interactors/github.rs | 22 +- src/interactors/mod.rs | 19 +- src/interactors/rustsec.rs | 25 +- src/main.rs | 58 ++- src/models/repo.rs | 4 +- src/server/assets.rs | 14 +- src/server/error.rs | 79 +++++ src/server/mod.rs | 606 +++++++++++++------------------- src/server/views/badge.rs | 11 +- src/server/views/html/error.rs | 27 +- src/server/views/html/index.rs | 3 +- src/server/views/html/mod.rs | 14 +- src/server/views/html/status.rs | 19 +- src/utils/cache.rs | 4 +- 19 files changed, 944 insertions(+), 577 deletions(-) create mode 100644 src/server/error.rs diff --git a/Cargo.lock b/Cargo.lock index 101fc26..4dbc783 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,238 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" +[[package]] +name = "actix-codec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "actix-http" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more 0.99.18", + "encoding_rs", + "flate2", + "futures-core", + "h2 0.3.26", + "http 0.2.12", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha1", + "smallvec", + "tokio", + "tokio-util", + "tracing", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" +dependencies = [ + "bytestring", + "cfg-if", + "http 0.2.12", + "regex", + "regex-lite", + "serde", + "tracing", +] + +[[package]] +name = "actix-rt" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eda4e2a6e042aa4e55ac438a2ae052d3b5da0ecf83d7411e1a368946925208" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca2549781d8dd6d75c40cf6b6051260a2cc2f3c62343d761a969a0640646894" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if", + "cookie", + "derive_more 0.99.18", + "encoding_rs", + "futures-core", + "futures-util", + "impl-more", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "regex-lite", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "actix-web-lab" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7675c1a84eec1b179c844cdea8488e3e409d8e4984026e92fa96c87dd86f33c6" +dependencies = [ + "actix-http", + "actix-router", + "actix-service", + "actix-utils", + "actix-web", + "actix-web-lab-derive", + "ahash", + "arc-swap", + "async-trait", + "bytes", + "bytestring", + "csv", + "derive_more 0.99.18", + "futures-core", + "futures-util", + "http 0.2.12", + "impl-more", + "itertools", + "local-channel", + "mediatype", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_html_form", + "serde_json", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "actix-web-lab-derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa0b287c8de4a76b691f29dbb5451e8dd5b79d777eaf87350c9b0cbfdb5e968" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "addr2line" version = "0.22.0" @@ -36,6 +268,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -50,6 +283,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -102,6 +350,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-trait" +version = "0.1.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -161,6 +420,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "brotli" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bstr" version = "1.10.0" @@ -190,6 +470,15 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +[[package]] +name = "bytestring" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d80203ea6b29df88012294f62733de21cfeab47f17b41af3a38bc30a03ee72" +dependencies = [ + "bytes", +] + [[package]] name = "cadence" version = "1.4.0" @@ -223,6 +512,8 @@ version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -257,6 +548,23 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -377,6 +685,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +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 = "cvss" version = "2.0.0" @@ -431,6 +760,19 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "derive_more" version = "1.0.0" @@ -1939,17 +2281,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1969,7 +2300,7 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body 1.0.1", + "http-body", "pin-project-lite", ] @@ -1985,30 +2316,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hyper" -version = "0.14.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.4.1" @@ -2020,7 +2327,7 @@ dependencies = [ "futures-util", "h2 0.4.6", "http 1.1.0", - "http-body 1.0.1", + "http-body", "httparse", "itoa", "pin-project-lite", @@ -2037,7 +2344,7 @@ checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.1", + "hyper", "hyper-util", "rustls", "rustls-native-certs", @@ -2056,7 +2363,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.4.1", + "hyper", "hyper-util", "native-tls", "tokio", @@ -2074,8 +2381,8 @@ dependencies = [ "futures-channel", "futures-util", "http 1.1.0", - "http-body 1.0.1", - "hyper 1.4.1", + "http-body", + "hyper", "pin-project-lite", "socket2", "tokio", @@ -2123,6 +2430,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "impl-more" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206ca75c9c03ba3d4ace2460e57b189f39f43de612c2f85836e65c929701bb2d" + [[package]] name = "indexmap" version = "1.9.3" @@ -2161,6 +2474,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -2192,6 +2514,15 @@ dependencies = [ "jiff-tzdb", ] +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.70" @@ -2220,6 +2551,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lasso" version = "0.7.3" @@ -2258,6 +2595,23 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "local-channel" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" +dependencies = [ + "futures-core", + "futures-sink", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" + [[package]] name = "lock_api" version = "0.4.12" @@ -2322,6 +2676,12 @@ dependencies = [ "syn", ] +[[package]] +name = "mediatype" +version = "0.19.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8878cd8d1b3c8c8ae4b2ba0a36652b7cf192f618a599a7fbdfa25cffd4ea72dd" + [[package]] name = "memchr" version = "2.7.4" @@ -2369,6 +2729,7 @@ checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi", "libc", + "log", "wasi", "windows-sys 0.52.0", ] @@ -2521,6 +2882,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2836,6 +3203,12 @@ dependencies = [ "regex-syntax 0.8.4", ] +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + [[package]] name = "regex-syntax" version = "0.6.29" @@ -2872,9 +3245,9 @@ dependencies = [ "futures-util", "h2 0.4.6", "http 1.1.0", - "http-body 1.0.1", + "http-body", "http-body-util", - "hyper 1.4.1", + "hyper", "hyper-rustls", "hyper-tls", "hyper-util", @@ -2924,12 +3297,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "route-recognizer" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2942,6 +3309,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.35" @@ -3122,6 +3498,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_html_form" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" +dependencies = [ + "form_urlencoded", + "indexmap 2.5.0", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_json" version = "1.0.127" @@ -3196,6 +3585,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha1_smol" version = "1.0.1" @@ -3221,11 +3621,13 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" name = "shiny-robots" version = "0.1.0" dependencies = [ + "actix-web", + "actix-web-lab", "anyhow", "badge", "cadence", "crates-index", - "derive_more", + "derive_more 1.0.0", "dotenvy", "either", "error_reporter", @@ -3233,16 +3635,15 @@ dependencies = [ "futures-util", "gix 0.63.0", "grass", - "hyper 0.14.30", "indexmap 2.5.0", "lru_time_cache", "maud", + "mime", "once_cell", "parking_lot", "pulldown-cmark", "relative-path", "reqwest", - "route-recognizer", "rustsec", "semver", "serde", @@ -3261,6 +3662,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -3492,7 +3902,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -3530,6 +3942,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -3644,6 +4067,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -4127,3 +4551,31 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 72b94c0..dda3a8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ edition = "2021" [dependencies] badge = { path = "./libs/badge" } +actix-web = "4" +actix-web-lab = "0.20" anyhow = "1" cadence = "1" crates-index = { version = "3", default-features = false, features = ["git"] } @@ -22,23 +24,22 @@ dotenvy = "0.15" either = "1.12" font-awesome-as-a-crate = "0.3" futures-util = { version = "0.3", default-features = false, features = ["std"] } -hyper = { version = "0.14.10", features = ["full"] } error_reporter = "1" indexmap = { version = "2", features = ["serde"] } lru_time_cache = "0.11" maud = "0.26" +mime = "0.3" once_cell = "1" parking_lot = "0.12" pulldown-cmark = "0.12" relative-path = { version = "1", features = ["serde"] } reqwest = { version = "0.12", features = ["json"] } -route-recognizer = "0.3" rustsec = "0.29" semver = { version = "1", features = ["serde"] } serde = { version = "1", features = ["derive"] } serde_urlencoded = "0.7" serde_with = "3" -tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros", "sync", "time"] } +tokio = { version = "1.24.2", features = ["rt", "macros", "sync", "time"] } toml = "0.8" tracing = "0.1.30" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/src/engine/fut/crawl.rs b/src/engine/fut/crawl.rs index c30d88a..815c109 100644 --- a/src/engine/fut/crawl.rs +++ b/src/engine/fut/crawl.rs @@ -1,5 +1,7 @@ use anyhow::Error; -use futures_util::{future::BoxFuture, stream::FuturesOrdered, FutureExt as _, StreamExt as _}; +use futures_util::{ + future::LocalBoxFuture, stream::FuturesOrdered, FutureExt as _, StreamExt as _, +}; use relative_path::RelativePathBuf; use crate::{ @@ -16,8 +18,9 @@ pub async fn crawl_manifest( entry_point: RelativePathBuf, ) -> anyhow::Result { let mut crawler = ManifestCrawler::new(); - let mut futures: FuturesOrdered>> = - FuturesOrdered::new(); + let mut futures: FuturesOrdered< + LocalBoxFuture<'static, Result<(RelativePathBuf, String), Error>>, + > = FuturesOrdered::new(); let engine2 = engine.clone(); let repo_path2 = repo_path.clone(); @@ -28,7 +31,7 @@ pub async fn crawl_manifest( .await?; Ok((entry_point, contents)) } - .boxed(); + .boxed_local(); futures.push_back(fut); @@ -47,7 +50,7 @@ pub async fn crawl_manifest( let contents = engine.retrieve_manifest_at_path(&repo_path, &path).await?; Ok((path, contents)) } - .boxed(); + .boxed_local(); futures.push_back(fut); } diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 34e89ea..d94b94b 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -5,14 +5,14 @@ use std::{ time::{Duration, Instant}, }; +use actix_web::dev::Service; use anyhow::{anyhow, Error}; use cadence::{MetricSink, NopMetricSink, StatsdClient}; use futures_util::{ future::try_join_all, - stream::{self, BoxStream}, + stream::{self, LocalBoxStream}, StreamExt as _, }; -use hyper::service::Service; use once_cell::sync::Lazy; use relative_path::{RelativePath, RelativePathBuf}; use rustsec::database::Database; @@ -38,7 +38,7 @@ mod machines; use self::fut::{analyze_dependencies, crawl_manifest}; -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] pub struct Engine { metrics: Arc, query_crate: Cache, @@ -255,7 +255,10 @@ impl Engine { Ok(latest) } - fn fetch_releases<'a, I>(&'a self, names: I) -> BoxStream<'a, anyhow::Result>> + fn fetch_releases<'a, I>( + &'a self, + names: I, + ) -> LocalBoxStream<'a, anyhow::Result>> where I: IntoIterator, ::IntoIter: Send + 'a, @@ -277,7 +280,7 @@ impl Engine { ) -> Result { let manifest_path = path.join(RelativePath::new("Cargo.toml")); - let mut service = self.retrieve_file_at_path.clone(); + let service = self.retrieve_file_at_path.clone(); service.call((repo_path.clone(), manifest_path)).await } diff --git a/src/interactors/crates.rs b/src/interactors/crates.rs index 1a07f5c..fcf47af 100644 --- a/src/interactors/crates.rs +++ b/src/interactors/crates.rs @@ -1,19 +1,16 @@ -use std::{ - fmt, str, - task::{Context, Poll}, -}; +use std::{fmt, str}; +use actix_web::dev::Service; use anyhow::{anyhow, Error}; use crates_index::{Crate, DependencyKind}; -use futures_util::FutureExt as _; -use hyper::service::Service; +use futures_util::{future::LocalBoxFuture, FutureExt as _}; use semver::{Version, VersionReq}; use serde::Deserialize; use tokio::task::spawn_blocking; use crate::{ models::crates::{CrateDep, CrateDeps, CrateName, CratePath, CrateRelease}, - BoxFuture, ManagedIndex, + ManagedIndex, }; const CRATES_API_BASE_URI: &str = "https://crates.io/api/v1"; @@ -86,13 +83,11 @@ impl fmt::Debug for QueryCrate { impl Service for QueryCrate { type Response = QueryCrateResponse; type Error = Error; - type Future = BoxFuture>; + type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } + actix_web::dev::always_ready!(); - fn call(&mut self, crate_name: CrateName) -> Self::Future { + fn call(&self, crate_name: CrateName) -> Self::Future { let index = self.index.clone(); Self::query(index, crate_name).boxed() } @@ -150,13 +145,11 @@ impl fmt::Debug for GetPopularCrates { impl Service<()> for GetPopularCrates { type Response = Vec; type Error = Error; - type Future = BoxFuture>; + type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } + actix_web::dev::always_ready!(); - fn call(&mut self, _req: ()) -> Self::Future { + fn call(&self, _req: ()) -> Self::Future { let client = self.client.clone(); Self::query(client).boxed() } diff --git a/src/interactors/github.rs b/src/interactors/github.rs index 72bb684..9eb089a 100644 --- a/src/interactors/github.rs +++ b/src/interactors/github.rs @@ -1,17 +1,11 @@ -use std::{ - fmt, - task::{Context, Poll}, -}; +use std::fmt; +use actix_web::dev::Service; use anyhow::Error; -use futures_util::FutureExt as _; -use hyper::service::Service; +use futures_util::{future::LocalBoxFuture, FutureExt as _}; use serde::Deserialize; -use crate::{ - models::repo::{RepoPath, Repository}, - BoxFuture, -}; +use crate::models::repo::{RepoPath, Repository}; const GITHUB_API_BASE_URI: &str = "https://api.github.com"; @@ -72,13 +66,11 @@ impl fmt::Debug for GetPopularRepos { impl Service<()> for GetPopularRepos { type Response = Vec; type Error = Error; - type Future = BoxFuture>; + type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } + actix_web::dev::always_ready!(); - fn call(&mut self, _req: ()) -> Self::Future { + fn call(&self, _req: ()) -> Self::Future { let client = self.client.clone(); Self::query(client).boxed() } diff --git a/src/interactors/mod.rs b/src/interactors/mod.rs index 030174f..1643012 100644 --- a/src/interactors/mod.rs +++ b/src/interactors/mod.rs @@ -1,14 +1,11 @@ -use std::{ - fmt, - task::{Context, Poll}, -}; +use std::fmt; +use actix_web::dev::Service; use anyhow::{anyhow, Error}; -use futures_util::FutureExt as _; -use hyper::service::Service; +use futures_util::{future::LocalBoxFuture, FutureExt as _}; use relative_path::RelativePathBuf; -use crate::{models::repo::RepoPath, BoxFuture}; +use crate::models::repo::RepoPath; pub mod crates; pub mod github; @@ -43,13 +40,11 @@ impl RetrieveFileAtPath { impl Service<(RepoPath, RelativePathBuf)> for RetrieveFileAtPath { type Response = String; type Error = Error; - type Future = BoxFuture>; + type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } + actix_web::dev::always_ready!(); - fn call(&mut self, (repo_path, path): (RepoPath, RelativePathBuf)) -> Self::Future { + fn call(&self, (repo_path, path): (RepoPath, RelativePathBuf)) -> Self::Future { let client = self.client.clone(); Self::query(client, repo_path, path).boxed() } diff --git a/src/interactors/rustsec.rs b/src/interactors/rustsec.rs index 46ddd55..84a9ae5 100644 --- a/src/interactors/rustsec.rs +++ b/src/interactors/rustsec.rs @@ -1,16 +1,10 @@ -use std::{ - fmt, - sync::Arc, - task::{Context, Poll}, -}; +use std::{fmt, sync::Arc}; +use actix_web::dev::Service; use anyhow::Error; -use futures_util::FutureExt as _; -use hyper::service::Service; +use futures_util::{future::LocalBoxFuture, FutureExt as _}; use rustsec::database::Database; -use crate::BoxFuture; - #[derive(Clone)] pub struct FetchAdvisoryDatabase { client: reqwest::Client, @@ -30,20 +24,19 @@ impl FetchAdvisoryDatabase { impl Service<()> for FetchAdvisoryDatabase { type Response = Arc; type Error = Error; - type Future = BoxFuture>; + type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } + actix_web::dev::always_ready!(); - fn call(&mut self, _req: ()) -> Self::Future { + fn call(&self, _req: ()) -> Self::Future { let client = self.client.clone(); - Self::fetch(client).boxed() + Self::fetch(client).boxed_local() } } impl fmt::Debug for FetchAdvisoryDatabase { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("FetchAdvisoryDatabase") + f.debug_struct("FetchAdvisoryDatabase") + .finish_non_exhaustive() } } diff --git a/src/main.rs b/src/main.rs index c5414ac..1327571 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,20 +3,14 @@ use std::{ env, - future::Future, - net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}, - pin::Pin, + net::{Ipv4Addr, UdpSocket}, time::Duration, }; +use actix_web::{middleware::Logger, web}; +use actix_web_lab::{extract::ThinData, middleware::NormalizePath}; use cadence::{QueuingMetricSink, UdpMetricSink}; -use hyper::{ - server::conn::AddrStream, - service::{make_service_fn, service_fn}, - Server, -}; use reqwest::redirect::Policy as RedirectPolicy; -use tracing::Instrument as _; mod engine; mod interactors; @@ -25,10 +19,7 @@ mod parsers; mod server; mod utils; -use self::{engine::Engine, server::App, utils::index::ManagedIndex}; - -/// Future crate's BoxFuture without the explicit lifetime parameter. -pub type BoxFuture = Pin + Send>>; +use self::{engine::Engine, utils::index::ManagedIndex}; const DEPS_RS_UA: &str = "deps.rs"; @@ -59,7 +50,7 @@ fn init_tracing_subscriber() { .init(); } -#[tokio::main] +#[tokio::main(flavor = "current_thread")] async fn main() { dotenvy::dotenv().ok(); init_tracing_subscriber(); @@ -77,8 +68,6 @@ async fn main() { .parse() .expect("could not read port"); - let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port); - let index = ManagedIndex::new(); { @@ -92,25 +81,24 @@ async fn main() { let mut engine = Engine::new(client.clone(), index); engine.set_metrics(metrics); - let make_svc = make_service_fn(move |_socket: &AddrStream| { - let engine = engine.clone(); - - async move { - let server = App::new(engine.clone()); - Ok::<_, hyper::Error>(service_fn(move |req| { - let server = server.clone(); - async move { - let path = req.uri().path().to_owned(); - - server - .handle(req) - .instrument(tracing::info_span!("@", %path)) - .await - } - })) - } - }); - let server = Server::bind(&addr).serve(make_svc); + let server = actix_web::HttpServer::new(move || { + actix_web::App::new() + .app_data(ThinData(engine.clone())) + .service(server::index) + .service(server::crate_redirect) + .service(server::crate_latest_status_svg) + .service(server::crate_status_svg) + .service(server::crate_status_html) + .service(server::repo_status_svg) + .service(server::repo_status_html) + .configure(server::static_files) + .default_service(web::to(server::not_found)) + .wrap(NormalizePath::trim()) + .wrap(Logger::default()) + }) + .bind_auto_h2c((Ipv4Addr::UNSPECIFIED, port)) + .unwrap() + .run(); tracing::info!("Server running on port {port}"); diff --git a/src/models/repo.rs b/src/models/repo.rs index 2927ab2..f0b5879 100644 --- a/src/models/repo.rs +++ b/src/models/repo.rs @@ -100,7 +100,7 @@ impl FromStr for RepoSite { if let Some((site, domain)) = input.split_once('/') { match site { "gitea" => Ok(RepoSite::Gitea(domain.parse()?)), - _ => Err(anyhow!("unknown repo site identifier")), + site => Err(anyhow!("unknown repo site identifier: {site}")), } } else { match input { @@ -109,7 +109,7 @@ impl FromStr for RepoSite { "bitbucket" => Ok(RepoSite::Bitbucket), "sourcehut" => Ok(RepoSite::Sourcehut), "codeberg" => Ok(RepoSite::Codeberg), - _ => Err(anyhow!("unknown repo site identifier")), + site => Err(anyhow!("unknown repo site identifier: {site}")), } } } diff --git a/src/server/assets.rs b/src/server/assets.rs index 7172d7f..7fdddc5 100644 --- a/src/server/assets.rs +++ b/src/server/assets.rs @@ -4,11 +4,9 @@ pub const STATIC_STYLE_CSS_PATH: &str = concat!( include_str!(concat!(env!("OUT_DIR"), "/style.css.sha1")), ".css" ); -pub const STATIC_STYLE_CSS_ETAG: &str = concat!( - "\"", - include_str!(concat!(env!("OUT_DIR"), "/style.css.sha1")), - "\"" -); +pub const STATIC_STYLE_CSS_ETAG: &str = include_str!(concat!(env!("OUT_DIR"), "/style.css.sha1")); + +pub const STATIC_FAVICON_PATH: &str = "/static/logo.svg"; pub static STATIC_FAVICON: &[u8] = include_bytes!("../../assets/logo.svg"); pub static STATIC_LINKS_JS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/links.js")); @@ -17,8 +15,4 @@ pub const STATIC_LINKS_JS_PATH: &str = concat!( include_str!(concat!(env!("OUT_DIR"), "/links.js.sha1")), ".js" ); -pub const STATIC_LINKS_JS_ETAG: &str = concat!( - "\"", - include_str!(concat!(env!("OUT_DIR"), "/links.js.sha1")), - "\"" -); +pub const STATIC_LINKS_JS_ETAG: &str = include_str!(concat!(env!("OUT_DIR"), "/links.js.sha1")); diff --git a/src/server/error.rs b/src/server/error.rs new file mode 100644 index 0000000..c761abe --- /dev/null +++ b/src/server/error.rs @@ -0,0 +1,79 @@ +use actix_web::{ + http::{header::ContentType, StatusCode}, + HttpResponse, ResponseError, +}; +use derive_more::Display; +use maud::Markup; + +use crate::server::views::html::error::{render, render_404}; + +#[derive(Debug, Display)] +pub(crate) enum ServerError { + #[display("Could not retrieve popular items")] + PopularItemsFailed, + + #[display("Crate not found")] + CrateNotFound, + + #[display("Could not parse crate path")] + BadCratePath, + + #[display("Could not fetch crate information")] + CrateFetchFailed, + + #[display("Could not parse repository path")] + BadRepoPath, + + #[display("Crate/repo analysis failed")] + AnalysisFailed(Markup), +} + +impl ResponseError for ServerError { + fn status_code(&self) -> StatusCode { + match self { + ServerError::PopularItemsFailed => StatusCode::INTERNAL_SERVER_ERROR, + ServerError::CrateNotFound => StatusCode::NOT_FOUND, + ServerError::BadCratePath => StatusCode::BAD_REQUEST, + ServerError::CrateFetchFailed => StatusCode::NOT_FOUND, + ServerError::BadRepoPath => StatusCode::BAD_REQUEST, + ServerError::AnalysisFailed(_) => StatusCode::BAD_REQUEST, + } + } + + fn error_response(&self) -> HttpResponse { + let mut res = HttpResponse::build(self.status_code()); + let res = res.insert_header(ContentType::html()); + + match self { + ServerError::PopularItemsFailed => res.body(render(self.to_string(), "").0), + + ServerError::CrateNotFound => res.body(render_404().0), + + ServerError::BadCratePath => res.body( + render( + self.to_string(), + "Please make sure to provide a valid crate name and version.", + ) + .0, + ), + + ServerError::CrateFetchFailed => res.body( + render( + self.to_string(), + "Please make sure to provide a valid crate name.", + ) + .0, + ), + + ServerError::BadRepoPath => res.body( + render( + self.to_string(), + "Please make sure to provide a valid repository path.", + ) + .0, + ), + + Self::AnalysisFailed(html) => res.body(html.0.clone()), + } + } +} diff --git a/src/server/mod.rs b/src/server/mod.rs index 127bd49..a849278 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,21 +1,35 @@ -use std::{env, sync::Arc, time::Instant}; +use std::env; +use actix_web::{ + get, + http::{ + header::{ContentType, ETag, EntityTag}, + Uri, + }, + web::{Redirect, ServiceConfig}, + Either, HttpResponse, Resource, Responder, +}; +use actix_web_lab::{ + extract::{Path, ThinData}, + header::{CacheControl, CacheDirective}, + respond::Html, +}; +use assets::STATIC_FAVICON_PATH; use badge::BadgeStyle; use futures_util::future; -use hyper::{ - header::{CACHE_CONTROL, CONTENT_TYPE, ETAG, LOCATION}, - Body, Error as HyperError, Method, Request, Response, StatusCode, -}; use once_cell::sync::Lazy; -use route_recognizer::{Params, Router}; use semver::VersionReq; use serde::Deserialize; mod assets; +mod error; mod views; -use self::assets::{ - STATIC_LINKS_JS_ETAG, STATIC_LINKS_JS_PATH, STATIC_STYLE_CSS_ETAG, STATIC_STYLE_CSS_PATH, +use self::{ + assets::{ + STATIC_LINKS_JS_ETAG, STATIC_LINKS_JS_PATH, STATIC_STYLE_CSS_ETAG, STATIC_STYLE_CSS_PATH, + }, + error::ServerError, }; use crate::{ engine::{AnalyzeDependenciesOutcome, Engine}, @@ -35,392 +49,260 @@ enum StatusFormat { Svg, } -#[derive(Debug, Clone, Copy)] -enum StaticFile { - StyleCss, - FaviconPng, - LinksJs, +#[get("/")] +pub(crate) async fn index(ThinData(engine): ThinData) -> actix_web::Result { + let popular = future::try_join(engine.get_popular_repos(), engine.get_popular_crates()).await; + + match popular { + Err(err) => { + tracing::error!(%err); + Err(ServerError::PopularItemsFailed.into()) + } + Ok((popular_repos, popular_crates)) => Ok(Html::new( + views::html::index::render(popular_repos, popular_crates).0, + )), + } } -enum Route { - Index, - Static(StaticFile), - RepoStatus(StatusFormat), - CrateRedirect, - CrateStatus(StatusFormat), - LatestCrateBadge, +#[get("/repo/{site:.+?}/{qual}/{name}/status.svg")] +pub(crate) async fn repo_status_svg( + ThinData(engine): ThinData, + uri: Uri, + Path(params): Path<(String, String, String)>, +) -> actix_web::Result { + repo_status(engine, uri, params, StatusFormat::Svg).await } -#[derive(Clone)] -pub struct App { - engine: Engine, - router: Arc>, +#[get("/repo/{site:.+?}/{qual}/{name}")] +pub(crate) async fn repo_status_html( + ThinData(engine): ThinData, + uri: Uri, + Path(params): Path<(String, String, String)>, +) -> actix_web::Result { + repo_status(engine, uri, params, StatusFormat::Html).await } -impl App { - pub fn new(engine: Engine) -> App { - let mut router = Router::new(); - - router.add("/", Route::Index); - - router.add(STATIC_STYLE_CSS_PATH, Route::Static(StaticFile::StyleCss)); - router.add("/static/logo.svg", Route::Static(StaticFile::FaviconPng)); - router.add(STATIC_LINKS_JS_PATH, Route::Static(StaticFile::LinksJs)); - - router.add( - "/repo/*site/:qual/:name", - Route::RepoStatus(StatusFormat::Html), - ); - router.add( - "/repo/*site/:qual/:name/status.svg", - Route::RepoStatus(StatusFormat::Svg), - ); - - router.add("/crate/:name", Route::CrateRedirect); - router.add( - "/crate/:name/:version", - Route::CrateStatus(StatusFormat::Html), - ); - router.add("/crate/:name/latest/status.svg", Route::LatestCrateBadge); - router.add( - "/crate/:name/:version/status.svg", - Route::CrateStatus(StatusFormat::Svg), - ); - - App { - engine, - router: Arc::new(router), +async fn repo_status( + engine: Engine, + uri: Uri, + (site, qual, name): (String, String, String), + format: StatusFormat, +) -> actix_web::Result { + let extra_knobs = ExtraConfig::from_query_string(uri.query()); + + let repo_path_result = RepoPath::from_parts(&site, &qual, &name); + + let repo_path = match repo_path_result { + Ok(repo_path) => repo_path, + Err(err) => { + tracing::error!(%err); + return Err(ServerError::BadRepoPath.into()); } - } + }; - pub async fn handle(&self, req: Request) -> Result, HyperError> { - let start = Instant::now(); + let analyze_result = engine + .analyze_repo_dependencies(repo_path.clone(), &extra_knobs.path) + .await; - // allows `/path/` to also match `/path` - let normalized_path = req.uri().path().trim_end_matches('/'); - - let res = if let Ok(route_match) = self.router.recognize(normalized_path) { - match (req.method(), route_match.handler()) { - (&Method::GET, Route::Index) => self.index(req, route_match.params().clone()).await, - - (&Method::GET, Route::RepoStatus(format)) => { - self.repo_status(req, route_match.params().clone(), *format) - .await - } + match analyze_result { + Err(err) => { + tracing::error!(%err); + let response = + status_format_analysis(None, format, SubjectPath::Repo(repo_path), extra_knobs); - (&Method::GET, Route::CrateStatus(format)) => { - self.crate_status(req, route_match.params().clone(), *format) - .await - } - - (&Method::GET, Route::LatestCrateBadge) => { - self.crate_status(req, route_match.params().clone(), StatusFormat::Svg) - .await - } - - (&Method::GET, Route::CrateRedirect) => { - self.crate_redirect(req, route_match.params().clone()).await - } - - (&Method::GET, Route::Static(file)) => Ok(App::static_file(*file)), - - _ => Ok(not_found()), - } - } else { - Ok(not_found()) - }; - - let end = Instant::now(); - let diff = end - start; + Ok(response) + } - match &res { - Ok(res) => tracing::info!( - status = %res.status(), - time = %format_args!("{}ms", diff.as_millis()), - ), - Err(err) => tracing::error!(%err), - }; + Ok(analysis_outcome) => { + let response = status_format_analysis( + Some(analysis_outcome), + format, + SubjectPath::Repo(repo_path), + extra_knobs, + ); - res + Ok(response) + } } } -impl App { - async fn index( - &self, - _req: Request, - _params: Params, - ) -> Result, HyperError> { - let engine = self.engine.clone(); - - let popular = - future::try_join(engine.get_popular_repos(), engine.get_popular_crates()).await; - - match popular { - Err(err) => { - tracing::error!(%err); - let mut response = - views::html::error::render("Could not retrieve popular items", ""); - *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; - Ok(response) - } - Ok((popular_repos, popular_crates)) => { - Ok(views::html::index::render(popular_repos, popular_crates)) - } +#[get("/crate/{name}")] +async fn crate_redirect( + ThinData(engine): ThinData, + Path((name,)): Path<(String,)>, +) -> actix_web::Result { + let crate_name_result = name.parse::(); + + let crate_name = match crate_name_result { + Ok(crate_name) => crate_name, + Err(err) => { + tracing::error!(%err); + return Err(ServerError::BadCratePath.into()); } - } + }; + + let release_result = engine + .find_latest_stable_crate_release(crate_name, VersionReq::STAR) + .await + .inspect_err(|err| { + tracing::error!(%err); + }); + + let Ok(Some(release)) = release_result else { + return Err(ServerError::CrateFetchFailed.into()); + }; + + let redirect_url = format!( + "{}/crate/{}/{}", + &SELF_BASE_URL as &str, + release.name.as_ref(), + release.version + ); + + Ok(Redirect::to(redirect_url)) +} - async fn repo_status( - &self, - req: Request, - params: Params, - format: StatusFormat, - ) -> Result, HyperError> { - let server = self.clone(); - - let site = params.find("site").expect("route param 'site' not found"); - let qual = params.find("qual").expect("route param 'qual' not found"); - let name = params.find("name").expect("route param 'name' not found"); - - let extra_knobs = ExtraConfig::from_query_string(req.uri().query()); - - let repo_path_result = RepoPath::from_parts(site, qual, name); - - match repo_path_result { - Err(err) => { - tracing::error!(%err); - let mut response = views::html::error::render( - "Could not parse repository path", - "Please make sure to provide a valid repository path.", - ); - *response.status_mut() = StatusCode::BAD_REQUEST; - Ok(response) - } +#[get("/crate/{name}/{version}")] +async fn crate_status_html( + ThinData(engine): ThinData, + uri: Uri, + Path((name, version)): Path<(String, String)>, +) -> actix_web::Result { + crate_status(engine, uri, (name, Some(version)), StatusFormat::Html).await +} - Ok(repo_path) => { - let analyze_result = server - .engine - .analyze_repo_dependencies(repo_path.clone(), &extra_knobs.path) - .await; - - match analyze_result { - Err(err) => { - tracing::error!(%err); - let response = App::status_format_analysis( - None, - format, - SubjectPath::Repo(repo_path), - extra_knobs, - ); - Ok(response) - } - Ok(analysis_outcome) => { - let response = App::status_format_analysis( - Some(analysis_outcome), - format, - SubjectPath::Repo(repo_path), - extra_knobs, - ); - Ok(response) - } - } - } - } - } +#[get("/crate/{name}/latest/status.svg")] +async fn crate_latest_status_svg( + ThinData(engine): ThinData, + uri: Uri, + Path((name,)): Path<(String,)>, +) -> actix_web::Result { + crate_status(engine, uri, (name, None), StatusFormat::Svg).await +} - async fn crate_redirect( - &self, - _req: Request, - params: Params, - ) -> Result, HyperError> { - let engine = self.engine.clone(); - - let name = params.find("name").expect("route param 'name' not found"); - let crate_name_result = name.parse::(); - - match crate_name_result { - Err(err) => { - tracing::error!(%err); - let mut response = views::html::error::render( - "Could not parse crate name", - "Please make sure to provide a valid crate name.", - ); - *response.status_mut() = StatusCode::BAD_REQUEST; - Ok(response) - } +#[get("/crate/{name}/{version}/status.svg")] +async fn crate_status_svg( + ThinData(engine): ThinData, + uri: Uri, + Path((name, version)): Path<(String, String)>, +) -> actix_web::Result { + crate_status(engine, uri, (name, Some(version)), StatusFormat::Svg).await +} - Ok(crate_name) => { - let release_result = engine - .find_latest_stable_crate_release(crate_name, VersionReq::STAR) - .await; - - match release_result { - Err(err) => { - tracing::error!(%err); - let mut response = views::html::error::render( - "Could not fetch crate information", - "Please make sure to provide a valid crate name.", - ); - *response.status_mut() = StatusCode::NOT_FOUND; - Ok(response) - } - Ok(None) => { - let mut response = views::html::error::render( - "Could not fetch crate information", - "Please make sure to provide a valid crate name.", - ); - *response.status_mut() = StatusCode::NOT_FOUND; - Ok(response) - } - Ok(Some(release)) => { - let redirect_url = format!( - "{}/crate/{}/{}", - &SELF_BASE_URL as &str, - release.name.as_ref(), - release.version - ); - - let res = Response::builder() - .status(StatusCode::TEMPORARY_REDIRECT) - .header(LOCATION, redirect_url) - .body(Body::empty()) - .unwrap(); - - Ok(res) - } +async fn crate_status( + engine: Engine, + uri: Uri, + (name, version): (String, Option), + format: StatusFormat, +) -> actix_web::Result { + let version = match version { + Some(ver) => ver.to_owned(), + None => { + let crate_name = match name.parse() { + Ok(name) => name, + Err(_) => return Err(ServerError::BadCratePath.into()), + }; + + match engine + .find_latest_stable_crate_release(crate_name, VersionReq::STAR) + .await + { + Ok(Some(latest_rel)) => latest_rel.version.to_string(), + + Ok(None) => return Err(ServerError::CrateNotFound.into()), + + Err(err) => { + tracing::error!(%err); + return Err(ServerError::CrateFetchFailed.into()); } } } - } + }; - async fn crate_status( - &self, - req: Request, - params: Params, - format: StatusFormat, - ) -> Result, HyperError> { - let server = self.clone(); - - let name = params.find("name").expect("route param 'name' not found"); - - let version = match params.find("version") { - Some(ver) => ver.to_owned(), - None => { - let crate_name = match name.parse() { - Ok(name) => name, - Err(_) => { - let mut response = views::html::error::render( - "Could not parse crate path", - "Please make sure to provide a valid crate name and version.", - ); - *response.status_mut() = StatusCode::BAD_REQUEST; - return Ok(response); - } - }; - - match server - .engine - .find_latest_stable_crate_release(crate_name, VersionReq::STAR) - .await - { - Ok(Some(latest_rel)) => latest_rel.version.to_string(), - Ok(None) => return Ok(not_found()), - Err(err) => { - tracing::error!(%err); - let mut response = views::html::error::render( - "Could not fetch crate information", - "Please make sure to provide a valid crate name.", - ); - *response.status_mut() = StatusCode::NOT_FOUND; - return Ok(response); - } - } - } - }; - - let crate_path_result = CratePath::from_parts(name, &version); - let badge_knobs = ExtraConfig::from_query_string(req.uri().query()); - - match crate_path_result { - Err(err) => { - tracing::error!(%err); - let mut response = views::html::error::render( - "Could not parse crate path", - "Please make sure to provide a valid crate name and version.", - ); - *response.status_mut() = StatusCode::BAD_REQUEST; - Ok(response) - } - Ok(crate_path) => { - let analyze_result = server - .engine - .analyze_crate_dependencies(crate_path.clone()) - .await; - - match analyze_result { - Err(err) => { - tracing::error!(%err); - let response = App::status_format_analysis( - None, - format, - SubjectPath::Crate(crate_path), - badge_knobs, - ); - Ok(response) - } - Ok(analysis_outcome) => { - let response = App::status_format_analysis( - Some(analysis_outcome), - format, - SubjectPath::Crate(crate_path), - badge_knobs, - ); - - Ok(response) - } - } - } + let crate_path_result = CratePath::from_parts(&name, &version); + let badge_knobs = ExtraConfig::from_query_string(uri.query()); + + match crate_path_result { + Err(err) => { + tracing::error!(%err); + Err(ServerError::BadCratePath.into()) } - } - fn status_format_analysis( - analysis_outcome: Option, - format: StatusFormat, - subject_path: SubjectPath, - badge_knobs: ExtraConfig, - ) -> Response { - match format { - StatusFormat::Svg => views::badge::response(analysis_outcome.as_ref(), badge_knobs), - StatusFormat::Html => { - views::html::status::render(analysis_outcome, subject_path, badge_knobs) - } + Ok(crate_path) => { + let analysis_outcome = engine + .analyze_crate_dependencies(crate_path.clone()) + .await + .inspect_err(|err| { + tracing::error!(%err); + }) + .ok(); + + let response = status_format_analysis( + analysis_outcome, + format, + SubjectPath::Crate(crate_path), + badge_knobs, + ); + + Ok(response) } } +} - fn static_file(file: StaticFile) -> Response { - match file { - StaticFile::StyleCss => Response::builder() - .header(CONTENT_TYPE, "text/css; charset=utf-8") - .header(ETAG, STATIC_STYLE_CSS_ETAG) - .header(CACHE_CONTROL, "public, max-age=365000000, immutable") - .body(Body::from(assets::STATIC_STYLE_CSS)) - .unwrap(), - StaticFile::FaviconPng => Response::builder() - .header(CONTENT_TYPE, "image/svg+xml") - .body(Body::from(assets::STATIC_FAVICON)) - .unwrap(), - StaticFile::LinksJs => Response::builder() - .header(CONTENT_TYPE, "text/javascript; charset=utf-8") - .header(ETAG, STATIC_LINKS_JS_ETAG) - .header(CACHE_CONTROL, "public, max-age=365000000, immutable") - .body(Body::from(assets::STATIC_LINKS_JS)) - .unwrap(), - } +fn status_format_analysis( + analysis_outcome: Option, + format: StatusFormat, + subject_path: SubjectPath, + badge_knobs: ExtraConfig, +) -> impl Responder { + match format { + StatusFormat::Svg => Either::Left(views::badge::response( + analysis_outcome.as_ref(), + badge_knobs, + )), + + StatusFormat::Html => Either::Right(views::html::status::response( + analysis_outcome, + subject_path, + badge_knobs, + )), } } -fn not_found() -> Response { - views::html::error::render_404() +pub(crate) fn static_files(cfg: &mut ServiceConfig) { + cfg.service(Resource::new(STATIC_STYLE_CSS_PATH).get(|| async { + HttpResponse::Ok() + .insert_header(ContentType(mime::TEXT_CSS_UTF_8)) + .insert_header(ETag(EntityTag::new_strong( + STATIC_STYLE_CSS_ETAG.to_owned(), + ))) + .insert_header(CacheControl(vec![ + CacheDirective::Public, + CacheDirective::MaxAge(365000000), + CacheDirective::Immutable, + ])) + .body(assets::STATIC_STYLE_CSS) + })) + .service(Resource::new(STATIC_FAVICON_PATH).get(|| async { + HttpResponse::Ok() + .insert_header(ContentType(mime::IMAGE_SVG)) + .body(assets::STATIC_FAVICON) + })) + .service(Resource::new(STATIC_LINKS_JS_PATH).get(|| async { + HttpResponse::Ok() + .insert_header(ContentType(mime::APPLICATION_JAVASCRIPT_UTF_8)) + .insert_header(ETag(EntityTag::new_strong(STATIC_LINKS_JS_ETAG.to_owned()))) + .insert_header(CacheControl(vec![ + CacheDirective::Public, + CacheDirective::MaxAge(365000000), + CacheDirective::Immutable, + ])) + .body(assets::STATIC_LINKS_JS) + })); +} + +pub(crate) async fn not_found() -> impl Responder { + Html::new(views::html::error::render_404().0) } static SELF_BASE_URL: Lazy = diff --git a/src/server/views/badge.rs b/src/server/views/badge.rs index 760c7b9..9745bbc 100644 --- a/src/server/views/badge.rs +++ b/src/server/views/badge.rs @@ -1,5 +1,5 @@ +use actix_web::{http::header::ContentType, HttpResponse}; use badge::{Badge, BadgeOptions}; -use hyper::{header::CONTENT_TYPE, Body, Response}; use crate::{engine::AnalyzeDependenciesOutcome, server::ExtraConfig}; @@ -68,11 +68,10 @@ pub fn badge( pub fn response( analysis_outcome: Option<&AnalyzeDependenciesOutcome>, badge_knobs: ExtraConfig, -) -> Response { +) -> HttpResponse { let badge = badge(analysis_outcome, badge_knobs).to_svg(); - Response::builder() - .header(CONTENT_TYPE, "image/svg+xml; charset=utf-8") - .body(Body::from(badge)) - .unwrap() + HttpResponse::Ok() + .insert_header(ContentType(mime::IMAGE_SVG)) + .body(badge) } diff --git a/src/server/views/html/error.rs b/src/server/views/html/error.rs index f5802e2..d91772f 100644 --- a/src/server/views/html/error.rs +++ b/src/server/views/html/error.rs @@ -1,14 +1,12 @@ -use hyper::{ - header::{CACHE_CONTROL, CONTENT_TYPE}, - Body, Response, StatusCode, -}; -use maud::html; +use maud::{html, Markup}; use crate::server::assets::STATIC_STYLE_CSS_PATH; -pub fn render(title: &str, descr: &str) -> Response { +pub fn render(title: impl Into, desc: &str) -> Markup { + let title = title.into(); + super::render_html( - title, + title.clone(), html! { section class="hero is-light" { div class="hero-head" { (super::render_navbar()) } @@ -17,7 +15,7 @@ pub fn render(title: &str, descr: &str) -> Response { div class="container" { div class="notification is-danger" { p class="title is-3" { (title) } - p { (descr) } + p { (desc) } } } } @@ -26,8 +24,8 @@ pub fn render(title: &str, descr: &str) -> Response { ) } -pub fn render_404() -> Response { - let rendered = html! { +pub fn render_404() -> Markup { + html! { html { head { meta charset="utf-8"; @@ -53,12 +51,5 @@ pub fn render_404() -> Response { (super::render_footer(None)) } } - }; - - Response::builder() - .status(StatusCode::NOT_FOUND) - .header(CONTENT_TYPE, "text/html; charset=utf-8") - .header(CACHE_CONTROL, "public, max-age=300, immutable") - .body(Body::from(rendered.0)) - .unwrap() + } } diff --git a/src/server/views/html/index.rs b/src/server/views/html/index.rs index 748eca0..902c3a2 100644 --- a/src/server/views/html/index.rs +++ b/src/server/views/html/index.rs @@ -1,4 +1,3 @@ -use hyper::{Body, Response}; use maud::{html, Markup}; use crate::{ @@ -161,7 +160,7 @@ fn popular_table(popular_repos: Vec, popular_crates: Vec) } } -pub fn render(popular_repos: Vec, popular_crates: Vec) -> Response { +pub fn render(popular_repos: Vec, popular_crates: Vec) -> Markup { super::render_html( "Keep your dependencies up-to-date", html! { diff --git a/src/server/views/html/mod.rs b/src/server/views/html/mod.rs index 844eb4a..9eacfea 100644 --- a/src/server/views/html/mod.rs +++ b/src/server/views/html/mod.rs @@ -1,6 +1,5 @@ use std::time::Duration; -use hyper::{header::CONTENT_TYPE, Body, Response}; use maud::{html, Markup, Render, DOCTYPE}; pub mod error; @@ -9,8 +8,10 @@ pub mod status; use crate::server::{assets::STATIC_STYLE_CSS_PATH, SELF_BASE_URL}; -fn render_html(title: &str, body: B) -> Response { - let rendered = html! { +fn render_html(title: impl Into, body: B) -> Markup { + let title = title.into(); + + html! { (DOCTYPE) html { head { @@ -24,12 +25,7 @@ fn render_html(title: &str, body: B) -> Response { } body { (body) } } - }; - - Response::builder() - .header(CONTENT_TYPE, "text/html; charset=utf-8") - .body(Body::from(rendered.0)) - .unwrap() + } } fn render_navbar() -> Markup { diff --git a/src/server/views/html/status.rs b/src/server/views/html/status.rs index 8017e94..597bd44 100644 --- a/src/server/views/html/status.rs +++ b/src/server/views/html/status.rs @@ -1,5 +1,6 @@ +use actix_web::Responder; +use actix_web_lab::respond::Html; use font_awesome_as_a_crate::{svg as fa, Type as FaType}; -use hyper::{Body, Response}; use indexmap::IndexMap; use maud::{html, Markup, PreEscaped}; use pulldown_cmark::{html, Parser}; @@ -13,9 +14,11 @@ use crate::{ repo::RepoSite, SubjectPath, }, - server::{views::badge, ExtraConfig}, + server::{error::ServerError, views::badge, ExtraConfig}, }; +use super::render_html; + fn get_crates_url(name: impl AsRef) -> String { format!("https://crates.io/crates/{}", name.as_ref()) } @@ -453,11 +456,11 @@ fn render_success( } } -pub fn render( +pub fn response( analysis_outcome: Option, subject_path: SubjectPath, extra_config: ExtraConfig, -) -> Response { +) -> actix_web::Result { let title = match subject_path { SubjectPath::Repo(ref repo_path) => { format!("{} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref()) @@ -468,8 +471,12 @@ pub fn render( }; if let Some(outcome) = analysis_outcome { - super::render_html(&title, render_success(outcome, subject_path, extra_config)) + Ok(Html::new(render_html( + &title, + render_success(outcome, subject_path, extra_config), + ))) } else { - super::render_html(&title, render_failure(subject_path)) + let html = render_html(&title, render_failure(subject_path)); + Err(ServerError::AnalysisFailed(html).into()) } } diff --git a/src/utils/cache.rs b/src/utils/cache.rs index a09c706..6ee0887 100644 --- a/src/utils/cache.rs +++ b/src/utils/cache.rs @@ -1,7 +1,7 @@ use std::{fmt, sync::Arc, time::Duration}; +use actix_web::dev::Service; use derive_more::{Display, Error, From}; -use hyper::service::Service; use lru_time_cache::LruCache; use tokio::sync::Mutex; @@ -65,7 +65,7 @@ where cache = "miss", ); - let mut service = self.inner.clone(); + let service = self.inner.clone(); let fresh = service.call(req.clone()).await?; {