diff --git a/Cargo.lock b/Cargo.lock index 0a9e381b..2a19211c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,9 +156,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -228,11 +228,11 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.1" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ - "axum-core 0.5.0", + "axum-core 0.5.6", "base64", "bytes", "form_urlencoded", @@ -248,19 +248,17 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite 0.26.1", + "tokio-tungstenite", "tower", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -285,22 +283,20 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.0" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -471,18 +467,19 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cloudflare" -version = "0.13.0" +version = "0.14.1" +source = "git+https://github.com/mentimeter/cloudflare-rs?rev=77cc181830aed80ce7a7f53a517c59b16f868802#77cc181830aed80ce7a7f53a517c59b16f868802" dependencies = [ "chrono", "http", - "percent-encoding", - "reqwest 0.12.12", + "reqwest 0.12.28", "serde", "serde_json", - "serde_urlencoded", + "serde_qs", "serde_with", "thiserror 2.0.11", "url", + "urlencoding", "uuid", ] @@ -530,6 +527,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -815,13 +824,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] -name = "encoding_rs" -version = "0.8.35" +name = "encode_unicode" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "enum-as-inner" @@ -876,9 +882,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filetime" @@ -1352,7 +1358,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 0.26.11", ] [[package]] @@ -1590,6 +1596,19 @@ dependencies = [ "serde", ] +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -1629,9 +1648,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" @@ -1736,6 +1755,7 @@ dependencies = [ "flate2", "hex", "hickory-resolver", + "indicatif", "linkup", "linkup-local-server", "log", @@ -1759,7 +1779,8 @@ dependencies = [ name = "linkup-local-server" version = "0.1.0" dependencies = [ - "axum 0.8.1", + "async-trait", + "axum 0.8.8", "axum-server", "futures", "hickory-server", @@ -1772,9 +1793,11 @@ dependencies = [ "rustls", "rustls-native-certs", "rustls-pemfile", + "serde", + "serde_json", "thiserror 2.0.11", "tokio", - "tokio-tungstenite 0.28.0", + "tokio-tungstenite", "tower", "tower-http", ] @@ -1784,24 +1807,23 @@ name = "linkup-server-tests" version = "0.1.0" dependencies = [ "anyhow", - "axum 0.8.1", + "axum 0.8.8", "futures", "http", "linkup", "linkup-local-server", - "reqwest 0.12.12", + "reqwest 0.13.2", "rstest", "serde_json", "tokio", - "tokio-tungstenite 0.26.1", + "tokio-tungstenite", ] [[package]] name = "linkup-worker" version = "0.1.0" dependencies = [ - "async-trait", - "axum 0.8.1", + "axum 0.8.8", "base64", "cloudflare", "console_error_panic_hook", @@ -1810,12 +1832,10 @@ dependencies = [ "http", "linkup", "regex", - "reqwest 0.12.12", + "reqwest 0.13.2", "serde", "serde_json", "tower-service", - "wasm-bindgen", - "wasm-bindgen-futures", "worker", ] @@ -2019,9 +2039,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -2149,9 +2169,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags", "cfg-if", @@ -2181,9 +2201,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -2270,15 +2290,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "powerfmt" @@ -2585,17 +2605,14 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" -version = "0.12.12" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64", "bytes", - "encoding_rs", - "futures-channel", "futures-core", "futures-util", - "h2", "http", "http-body", "http-body-util", @@ -2603,34 +2620,30 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", - "mime", + "mime_guess", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", "quinn", "rustls", - "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tokio-rustls", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", - "windows-registry", + "webpki-roots 1.0.6", ] [[package]] @@ -2871,9 +2884,9 @@ checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -2949,10 +2962,11 @@ checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ + "serde_core", "serde_derive", ] @@ -2978,11 +2992,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -3011,6 +3034,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2316d01592c3382277c5062105510e35e0a6bfb2851e30028485f7af8cf1240" +dependencies = [ + "itoa", + "percent-encoding", + "ryu", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3237,27 +3272,6 @@ dependencies = [ "windows 0.62.2", ] -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tagptr" version = "0.2.0" @@ -3277,16 +3291,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.15.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "cfg-if", "fastrand", - "getrandom 0.2.15", + "getrandom 0.4.1", "once_cell", - "rustix 0.38.43", - "windows-sys 0.59.0", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -3449,18 +3462,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tungstenite" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4bf6fecd69fcdede0ec680aaf474cdab988f9de6bc73d3758f0160e3b7025a" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.26.1", -] - [[package]] name = "tokio-tungstenite" version = "0.28.0" @@ -3474,7 +3475,7 @@ dependencies = [ "rustls-pki-types", "tokio", "tokio-rustls", - "tungstenite 0.28.0", + "tungstenite", ] [[package]] @@ -3520,7 +3521,6 @@ dependencies = [ "tokio", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -3560,7 +3560,6 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ - "log", "pin-project-lite", "tracing-core", ] @@ -3610,24 +3609,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tungstenite" -version = "0.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http", - "httparse", - "log", - "rand 0.8.5", - "sha1", - "thiserror 2.0.11", - "utf-8", -] - [[package]] name = "tungstenite" version = "0.28.0" @@ -3671,12 +3652,24 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -3701,6 +3694,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -3954,9 +3953,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.7" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -4135,17 +4143,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-registry" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" -dependencies = [ - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 78c5256e..8a084a2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,28 @@ [workspace] resolver = "2" -members = ["linkup-cli", "linkup", "worker", "local-server", "server-tests", "cloudflare"] +members = ["linkup-cli", "linkup", "worker", "local-server", "server-tests"] + +[workspace.dependencies] +anyhow = "1.0.95" +async-trait = "0.1.86" +axum = { version = "0.8.8", default-features = false } +base64 = "0.22.1" +# TODO(augustoccesar)[2026-04-13]: Point this to mentimeter/cloudflare-rs until the fixes are merged upstream. +cloudflare = { git = "https://github.com/mentimeter/cloudflare-rs", rev = "77cc181830aed80ce7a7f53a517c59b16f868802" } +futures = "0.3.31" +hex = "0.4.3" +http = "1.2.0" +rand = "0.10.0" +regex = "1.11.1" +reqwest = { version = "0.13.2", default-features = false } +serde = "1.0.217" +serde_json = "1.0.138" +sha2 = "0.10.8" +thiserror = "2.0.11" +tokio = "1.49.0" +tokio-tungstenite = "0.28.0" +url = { version = "2.5.4", features = ["serde"] } + +linkup = { path = "linkup" } +linkup-local-server = { path = "local-server" } diff --git a/cloudflare/Cargo.toml b/cloudflare/Cargo.toml deleted file mode 100644 index d21facfb..00000000 --- a/cloudflare/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "cloudflare" -version = "0.13.0" -authors = ["Noah Kennedy ", "Jeff Hiner ", "Kenneth Eversole "] -repository = "https://github.com/cloudflare/cloudflare-rs" -edition = "2018" -description = "Rust library for the Cloudflare v4 API" -keywords = ["cloudflare", "api", "client"] -categories = ["api-bindings", "web-programming::http-client"] -license = "BSD-3-Clause" - -[features] -default = ["default-tls"] -blocking = ["reqwest/blocking"] -default-tls = ["reqwest/default-tls"] -rustls-tls = ["reqwest/rustls-tls"] -spec = [] -endpoint-spec = [] - -[dependencies] -chrono = { version = "0.4", default-features = false, features = [ - "clock", - "serde", - "std", - "wasmbind", -] } -http = "1" -percent-encoding = "2.1.0" -reqwest = { version = "0.12.12", default-features = false, features = ["json"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde_with = { version = "3", features = ["base64"] } -serde_urlencoded = "0.7.1" -thiserror = "2" -url = "2.2" -uuid = { version = "1.0", features = ["serde"] } diff --git a/cloudflare/README.md b/cloudflare/README.md deleted file mode 100644 index 869f816d..00000000 --- a/cloudflare/README.md +++ /dev/null @@ -1,2 +0,0 @@ -This is being created so that we can move faster while using the [cloudflare-rs](https://github.com/cloudflare/cloudflare-rs) repo. -There are quire a few gaps on the upstream repo, so this is one way that we can do the necessary changes for us for now. Hopefully we can remove this and fully use the official repo in the future. diff --git a/cloudflare/src/endpoints/account/list_accounts.rs b/cloudflare/src/endpoints/account/list_accounts.rs deleted file mode 100644 index 4d1f128c..00000000 --- a/cloudflare/src/endpoints/account/list_accounts.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::Account; - -use crate::framework::endpoint::{serialize_query, EndpointSpec, Method}; -use crate::framework::OrderDirection; - -use serde::Serialize; - -/// List Accounts -/// List all accounts you have ownership or verified access to -/// -#[derive(Debug)] -pub struct ListAccounts { - pub params: Option, -} - -impl EndpointSpec> for ListAccounts { - fn method(&self) -> Method { - Method::GET - } - fn path(&self) -> String { - "accounts".to_string() - } - #[inline] - fn query(&self) -> Option { - serialize_query(&self.params) - } -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug, Default)] -pub struct ListAccountsParams { - pub page: Option, - pub per_page: Option, - pub direction: Option, -} diff --git a/cloudflare/src/endpoints/account/mod.rs b/cloudflare/src/endpoints/account/mod.rs deleted file mode 100644 index 2954079c..00000000 --- a/cloudflare/src/endpoints/account/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::framework::response::ApiResult; -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -pub mod list_accounts; -pub use list_accounts::ListAccounts; - -/// Cloudflare Accounts -/// An Account is the root object which owns other resources such as zones, load balancers and billing details. -/// -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct Account { - /// Account identifier tag. - pub id: String, - /// Account name - pub name: String, - /// Account Settings - pub settings: Option, - /// describes when the account was created - pub created_on: Option>, -} - -/// Cloudflare Accounts Settings -/// An object containing the enforce two factor auth property. -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct Settings { - /// Indicates whether or not membership in this account requires that Two-Factor Authentication is enabled - enforce_twofactor: bool, -} - -/// Cloudflare Accounts Details -/// An Account is the root object which owns other resources such as zones, load balancers and billing details. -/// -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct AccountDetails { - /// Account identifier tag. - pub id: String, - /// Account name - pub name: String, -} - -impl ApiResult for Account {} -impl ApiResult for Vec {} diff --git a/cloudflare/src/endpoints/cfd_tunnel/create_tunnel.rs b/cloudflare/src/endpoints/cfd_tunnel/create_tunnel.rs deleted file mode 100644 index 1f23870d..00000000 --- a/cloudflare/src/endpoints/cfd_tunnel/create_tunnel.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::endpoints::cfd_tunnel::{ConfigurationSrc, Tunnel}; -use serde::Serialize; -use serde_with::serde_as; - -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// Create a Cfd Tunnel -/// This creates the Tunnel, which can then be routed and ran. Creating the Tunnel per se is only -/// a metadata operation (i.e. no Tunnel is running at this point). -/// -#[derive(Debug)] -pub struct CreateTunnel<'a> { - pub account_identifier: &'a str, - pub params: Params<'a>, -} - -impl<'a> EndpointSpec for CreateTunnel<'a> { - fn method(&self) -> Method { - Method::POST - } - fn path(&self) -> String { - format!("accounts/{}/cfd_tunnel", self.account_identifier) - } - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.params).unwrap(); - Some(body) - } -} - -/// Params for creating a Named Argo Tunnel -#[serde_as] -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug)] -pub struct Params<'a> { - /// The name for the Tunnel to be created. It must be unique within the account. - pub name: &'a str, - /// This should be a base64 string and it should be at least 32 bytes long. - pub tunnel_secret: &'a str, - - pub config_src: &'a ConfigurationSrc, - - /// Arbitrary metadata for the tunnel. - pub metadata: Option, -} diff --git a/cloudflare/src/endpoints/cfd_tunnel/data_structures.rs b/cloudflare/src/endpoints/cfd_tunnel/data_structures.rs deleted file mode 100644 index 997e04a1..00000000 --- a/cloudflare/src/endpoints/cfd_tunnel/data_structures.rs +++ /dev/null @@ -1,103 +0,0 @@ -use chrono::{offset::Utc, DateTime}; -use serde::{Deserialize, Serialize}; -use std::net::IpAddr; -use uuid::Uuid; - -use crate::framework::response::ApiResult; - -/// A Cfd Tunnel -/// This is an Cfd Tunnel that has been created. It can be used for routing and subsequent running. -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct Tunnel { - pub id: Uuid, - pub created_at: DateTime, - pub deleted_at: Option>, - pub name: String, - pub connections: Vec, - pub metadata: serde_json::Value, -} - -#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] -pub struct TunnelWithConnections { - pub id: Uuid, - pub account_tag: String, - pub created_at: DateTime, - pub deleted_at: Option>, - pub name: String, - pub connections: Vec, - pub conns_active_at: Option>, - pub conns_inactive_at: Option>, - // tun_type can be inferred from metadata - #[serde(flatten)] - pub metadata: serde_json::Value, - pub status: TunnelStatusType, - // This field is only present for tunnels that make sense to report (e.g: Cfd_Tunnel), which - // are the ones that can be managed via UI or dash in terms of their YAML file. - #[serde(skip_serializing_if = "Option::is_none")] - pub remote_config: Option, -} - -#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Copy, Eq, Hash)] -#[serde(rename_all = "snake_case")] -pub enum TunnelStatusType { - Inactive, // Tunnel has been created but a connection has yet to be registered - Down, // Tunnel is down and all connections are unregistered - Degraded, // Tunnel health is degraded but still serving connections - Healthy, // Tunnel is healthy -} - -#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] -pub enum ConfigurationSrc { - #[serde(rename = "local")] - #[default] - Local, - #[serde(rename = "cloudflare")] - Cloudflare, -} -/// An active connection for a Cfd Tunnel -#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, Hash)] -pub struct ActiveConnection { - pub colo_name: String, - /// Deprecated, use `id` instead. - pub uuid: Uuid, - pub id: Uuid, - pub is_pending_reconnect: bool, - pub origin_ip: IpAddr, - pub opened_at: DateTime, - pub client_id: Uuid, - pub client_version: String, -} - -impl ApiResult for Tunnel {} -impl ApiResult for Vec {} - -/// The result of a route request for a Cfd Tunnel -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -#[serde(untagged)] -pub enum RouteResult { - Dns(DnsRouteResult), - Lb(LoadBalancerRouteResult), -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct DnsRouteResult { - pub cname: Change, - pub name: String, - pub dns_tag: String, -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct LoadBalancerRouteResult { - pub load_balancer: Change, - pub pool: Change, -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum Change { - Unchanged, - New, - Updated, -} - -impl ApiResult for RouteResult {} diff --git a/cloudflare/src/endpoints/cfd_tunnel/delete_tunnel.rs b/cloudflare/src/endpoints/cfd_tunnel/delete_tunnel.rs deleted file mode 100644 index d7348bb4..00000000 --- a/cloudflare/src/endpoints/cfd_tunnel/delete_tunnel.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::framework::endpoint::{serialize_query, EndpointSpec, Method}; -use serde::Serialize; - -use super::Tunnel; - -/// Delete a tunnel -/// -#[derive(Debug)] -pub struct DeleteTunnel<'a> { - pub account_identifier: &'a str, - pub tunnel_id: &'a str, - pub params: Params, -} - -impl<'a> EndpointSpec for DeleteTunnel<'a> { - fn method(&self) -> Method { - Method::DELETE - } - fn path(&self) -> String { - format!( - "accounts/{}/cfd_tunnel/{}", - self.account_identifier, self.tunnel_id - ) - } - #[inline] - fn query(&self) -> Option { - serialize_query(&self.params) - } -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug, Default)] -pub struct Params { - // should delete tunnel connections if any exists - pub cascade: bool, -} diff --git a/cloudflare/src/endpoints/cfd_tunnel/list_tunnels.rs b/cloudflare/src/endpoints/cfd_tunnel/list_tunnels.rs deleted file mode 100644 index 7562cd66..00000000 --- a/cloudflare/src/endpoints/cfd_tunnel/list_tunnels.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::endpoints::cfd_tunnel::Tunnel; -use chrono::{DateTime, Utc}; -use serde::Serialize; - -use crate::framework::endpoint::{serialize_query, EndpointSpec, Method}; - -/// List/search tunnels in an account. -/// -#[derive(Debug)] -pub struct ListTunnels<'a> { - pub account_identifier: &'a str, - pub params: Params, -} - -impl<'a> EndpointSpec> for ListTunnels<'a> { - fn method(&self) -> Method { - Method::GET - } - fn path(&self) -> String { - format!("accounts/{}/cfd_tunnel", self.account_identifier) - } - #[inline] - fn query(&self) -> Option { - serialize_query(&self.params) - } -} - -/// Params for filtering listed tunnels -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug, Default)] -pub struct Params { - pub name: Option, - pub uuid: Option, - pub is_deleted: Option, - pub existed_at: Option>, - pub was_active_at: Option>, - pub include_prefix: Option, - pub was_inactive_at: Option>, - pub exclude_prefix: Option, - #[serde(flatten)] - pub pagination_params: Option, -} - -#[derive(Serialize, Clone, Debug)] -pub struct PaginationParams { - pub page: u64, - pub per_page: u64, -} diff --git a/cloudflare/src/endpoints/cfd_tunnel/mod.rs b/cloudflare/src/endpoints/cfd_tunnel/mod.rs deleted file mode 100644 index 1841ae5e..00000000 --- a/cloudflare/src/endpoints/cfd_tunnel/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod create_tunnel; -mod data_structures; -pub mod delete_tunnel; -pub mod list_tunnels; -pub mod route_dns; -pub mod update_tunnel; - -pub use data_structures::*; diff --git a/cloudflare/src/endpoints/cfd_tunnel/route_dns.rs b/cloudflare/src/endpoints/cfd_tunnel/route_dns.rs deleted file mode 100644 index 823101d3..00000000 --- a/cloudflare/src/endpoints/cfd_tunnel/route_dns.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::framework::endpoint::{EndpointSpec, Method}; - -use super::RouteResult; -use serde::Serialize; -use uuid::Uuid; - -/// Route for a Named Argo Tunnel -/// This creates a new route for the identified Tunnel. More than 1 route may co-exist for the same -/// Tunnel. -/// Note that this modifies only metadata on Cloudflare side to route traffic to the Tunnel, but -/// it is still up to the user to run the Tunnel to receive that traffic. -#[derive(Debug)] -pub struct RouteTunnel<'a> { - pub zone_tag: &'a str, - pub tunnel_id: Uuid, - pub params: Params<'a>, -} - -impl<'a> EndpointSpec for RouteTunnel<'a> { - fn method(&self) -> Method { - Method::PUT - } - fn path(&self) -> String { - format!("zones/{}/tunnels/{}/routes", self.zone_tag, self.tunnel_id) - } - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.params).unwrap(); - Some(body) - } -} - -/// Params for routing a Named Argo Tunnel -#[derive(Serialize, Clone, Debug)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum Params<'a> { - Dns { user_hostname: &'a str }, - Lb { lb_name: &'a str, lb_pool: &'a str }, -} diff --git a/cloudflare/src/endpoints/cfd_tunnel/update_tunnel.rs b/cloudflare/src/endpoints/cfd_tunnel/update_tunnel.rs deleted file mode 100644 index b2c5aba8..00000000 --- a/cloudflare/src/endpoints/cfd_tunnel/update_tunnel.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::endpoints::cfd_tunnel::Tunnel; -use serde::Serialize; -use serde_with::{ - base64::{Base64, Standard}, - formats::Padded, - serde_as, -}; - -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// Create a Cfd Tunnel -/// This creates the Tunnel, which can then be routed and ran. Creating the Tunnel per se is only -/// a metadata operation (i.e. no Tunnel is running at this point). -/// -#[derive(Debug)] -pub struct UpdateTunnel<'a> { - pub account_identifier: &'a str, - pub tunnel_id: &'a str, - pub params: Params<'a>, -} - -impl<'a> EndpointSpec for UpdateTunnel<'a> { - fn method(&self) -> Method { - Method::PATCH - } - fn path(&self) -> String { - format!( - "accounts/{}/cfd_tunnel/{}", - self.account_identifier, self.tunnel_id - ) - } - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.params).unwrap(); - Some(body) - } -} - -/// Params for updating a Cfd Tunnel -#[serde_as] -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug)] -pub struct Params<'a> { - /// The new name for the Tunnel - pub name: &'a str, - /// The byte array (with 32 or more bytes) representing a secret for the tunnel. This is - /// encoded into JSON as a base64 String. This secret is necessary to run the tunnel. - #[serde_as(as = "Base64")] - pub tunnel_secret: &'a Vec, - - /// Arbitrary metadata for the tunnel. - pub metadata: Option, -} diff --git a/cloudflare/src/endpoints/dns.rs b/cloudflare/src/endpoints/dns.rs deleted file mode 100644 index f1b45c8a..00000000 --- a/cloudflare/src/endpoints/dns.rs +++ /dev/null @@ -1,289 +0,0 @@ -use crate::framework::{ - endpoint::{serialize_query, EndpointSpec, Method}, - response::ApiResult, -}; -/// -use crate::framework::{OrderDirection, SearchMatch}; -use chrono::offset::Utc; -use chrono::DateTime; -use serde::{Deserialize, Serialize}; -use std::net::{Ipv4Addr, Ipv6Addr}; - -/// Batch operations to DNS Records -/// -#[derive(Debug)] -pub struct BatchDnsRecords<'a> { - pub zone_identifier: &'a str, - pub params: BatchDnsRecordsParams, -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug)] -pub struct BatchDnsRecordsParams { - pub deletes: Option>, - // TODO(augustoccesar)[2025-03-05]: Add support for patches, posts and puts. Then - // upstreams it to cloudflare-rs. -} - -#[derive(Debug, Deserialize)] -pub struct BatchDnsRecordsResponse { - pub deletes: Option>, - // TODO(augustoccesar)[2025-03-05]: Add support for patches, posts and puts. Then - // upstreams it to cloudflare-rs. -} - -impl ApiResult for BatchDnsRecordsResponse {} - -impl<'a> EndpointSpec for BatchDnsRecords<'a> { - fn method(&self) -> Method { - Method::POST - } - - fn path(&self) -> String { - format!("zones/{}/dns_records/batch", self.zone_identifier) - } -} - -/// List DNS Records -/// -#[derive(Debug)] -pub struct ListDnsRecords<'a> { - pub zone_identifier: &'a str, - pub params: ListDnsRecordsParams, -} -impl<'a> EndpointSpec> for ListDnsRecords<'a> { - fn method(&self) -> Method { - Method::GET - } - fn path(&self) -> String { - format!("zones/{}/dns_records", self.zone_identifier) - } - #[inline] - fn query(&self) -> Option { - serialize_query(&self.params) - } -} - -/// Create DNS Record -/// -#[derive(Debug)] -pub struct CreateDnsRecord<'a> { - pub zone_identifier: &'a str, - pub params: CreateDnsRecordParams<'a>, -} - -impl<'a> EndpointSpec for CreateDnsRecord<'a> { - fn method(&self) -> Method { - Method::POST - } - fn path(&self) -> String { - format!("zones/{}/dns_records", self.zone_identifier) - } - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.params).unwrap(); - Some(body) - } -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug)] -pub struct CreateDnsRecordParams<'a> { - /// Time to live for DNS record. Value of 1 is 'automatic' - pub ttl: Option, - /// Used with some records like MX and SRV to determine priority. - /// If you do not supply a priority for an MX record, a default value of 0 will be set - pub priority: Option, - /// Whether the record is receiving the performance and security benefits of Cloudflare - pub proxied: Option, - /// DNS record name - pub name: &'a str, - /// Type of the DNS record that also holds the record value - #[serde(flatten)] - pub content: DnsContent, -} - -/// Delete DNS Record -/// -#[derive(Debug)] -pub struct DeleteDnsRecord<'a> { - pub zone_identifier: &'a str, - pub identifier: &'a str, -} -impl<'a> EndpointSpec for DeleteDnsRecord<'a> { - fn method(&self) -> Method { - Method::DELETE - } - fn path(&self) -> String { - format!( - "zones/{}/dns_records/{}", - self.zone_identifier, self.identifier - ) - } -} - -/// Update DNS Record -/// -#[derive(Debug)] -pub struct UpdateDnsRecord<'a> { - pub zone_identifier: &'a str, - pub identifier: &'a str, - pub params: UpdateDnsRecordParams<'a>, -} - -impl<'a> EndpointSpec for UpdateDnsRecord<'a> { - fn method(&self) -> Method { - Method::PUT - } - fn path(&self) -> String { - format!( - "zones/{}/dns_records/{}", - self.zone_identifier, self.identifier - ) - } - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.params).unwrap(); - Some(body) - } -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug)] -pub struct UpdateDnsRecordParams<'a> { - /// Time to live for DNS record. Value of 1 is 'automatic' - pub ttl: Option, - /// Whether the record is receiving the performance and security benefits of Cloudflare - pub proxied: Option, - /// DNS record name - pub name: &'a str, - /// Type of the DNS record that also holds the record value - #[serde(flatten)] - pub content: DnsContent, -} - -/// Patch DNS Record -/// -#[derive(Debug)] -pub struct PatchDnsRecord<'a> { - pub zone_identifier: &'a str, - pub identifier: &'a str, - pub params: PatchDnsRecordParams<'a>, -} - -impl<'a> EndpointSpec for PatchDnsRecord<'a> { - fn method(&self) -> Method { - Method::PATCH - } - fn path(&self) -> String { - format!( - "zones/{}/dns_records/{}", - self.zone_identifier, self.identifier - ) - } - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.params).unwrap(); - Some(body) - } -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug)] -pub struct PatchDnsRecordParams<'a> { - /// Time to live for DNS record. Value of 1 is 'automatic' - pub ttl: Option, - /// Whether the record is receiving the performance and security benefits of Cloudflare - pub proxied: Option, - /// DNS record name - pub name: &'a str, - /// Type of the DNS record that also holds the record value - #[serde(flatten)] - pub content: DnsContent, -} - -#[derive(Serialize, Clone, Debug)] -#[serde(rename_all = "lowercase")] -pub enum ListDnsRecordsOrder { - Type, - Name, - Content, - Ttl, - Proxied, -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug, Default)] -pub struct ListDnsRecordsParamsName { - pub contains: Option, - pub endswith: Option, - pub startswith: Option, - pub exact: Option, -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug, Default)] -pub struct ListDnsRecordsParams { - #[serde(flatten)] - pub record_type: Option, - pub name: Option, - pub page: Option, - pub per_page: Option, - pub order: Option, - pub direction: Option, - #[serde(rename = "match")] - pub search_match: Option, -} - -/// Extra Cloudflare-specific information about the record -#[derive(Deserialize, Debug)] -pub struct Meta {} - -/// Type of the DNS record, along with the associated value. -/// When we add support for other types (LOC/SRV/...), the `meta` field should also probably be encoded -/// here as an associated, strongly typed value. -#[derive(Deserialize, Serialize, Clone, Debug)] -#[serde(tag = "type")] -#[allow(clippy::upper_case_acronyms)] -pub enum DnsContent { - A { content: Ipv4Addr }, - AAAA { content: Ipv6Addr }, - CNAME { content: String }, - NS { content: String }, - MX { content: String, priority: u16 }, - TXT { content: String }, - SRV { content: String }, -} - -#[derive(Deserialize, Debug)] -pub struct DeleteDnsRecordResponse { - /// DNS record identifier tag - pub id: String, -} - -#[derive(Deserialize, Debug)] -pub struct DnsRecord { - /// Extra Cloudflare-specific information about the record - pub meta: Meta, - /// DNS record name - pub name: String, - /// Time to live for DNS record. Value of 1 is 'automatic' - pub ttl: u32, - /// When the record was last modified - pub modified_on: DateTime, - /// When the record was created - pub created_on: DateTime, - /// Whether this record can be modified/deleted (true means it's managed by Cloudflare) - pub proxiable: bool, - /// Type of the DNS record that also holds the record value - #[serde(flatten)] - pub content: DnsContent, - /// DNS record identifier tag - pub id: String, - /// Whether the record is receiving the performance and security benefits of Cloudflare - pub proxied: bool, -} - -impl ApiResult for DnsRecord {} -impl ApiResult for Vec {} -impl ApiResult for DeleteDnsRecordResponse {} diff --git a/cloudflare/src/endpoints/mod.rs b/cloudflare/src/endpoints/mod.rs deleted file mode 100644 index dfba903f..00000000 --- a/cloudflare/src/endpoints/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -/*! -Implementations of the Endpoint trait for individual Cloudflare API endpoints, e.g. DNS or Workers. -If you want to add a new Cloudflare API to this crate, simply add a new submodule of this `endpoints` -module. - */ -pub mod account; -pub mod cfd_tunnel; -pub mod dns; -pub mod plan; -pub mod user; -pub mod workers; -pub mod workerskv; -pub mod zone; diff --git a/cloudflare/src/endpoints/plan.rs b/cloudflare/src/endpoints/plan.rs deleted file mode 100644 index 01e12f58..00000000 --- a/cloudflare/src/endpoints/plan.rs +++ /dev/null @@ -1,33 +0,0 @@ -use serde::Deserialize; - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "lowercase")] -/// Free plans won't have a Frequency, so most responses should accept Option instead. -pub enum Frequency { - #[serde(rename = "")] - Nil, - Weekly, - Monthly, - Quarterly, - Yearly, -} - -#[derive(Deserialize, Debug)] -pub struct Plan { - /// Plan identifier tag - pub id: String, - /// The plan name - pub name: String, - /// The price of the subscription that will be billed, in US dollars - pub price: f64, - /// The monetary unit in which pricing information is displayed - pub currency: String, - /// The frequency at which you will be billed for this plan - pub frequency: Option, - /// A 'friendly' identifier to indicate to the UI what plan the object is - pub legacy_id: String, - /// If the zone is subscribed to this plan - pub is_subscribed: bool, - /// If the zone is allowed to subscribe to this plan - pub can_subscribe: bool, -} diff --git a/cloudflare/src/endpoints/user.rs b/cloudflare/src/endpoints/user.rs deleted file mode 100644 index 0d4113f1..00000000 --- a/cloudflare/src/endpoints/user.rs +++ /dev/null @@ -1,101 +0,0 @@ -use crate::framework::endpoint::{EndpointSpec, Method}; -use crate::framework::response::ApiResult; - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -/// Get User Details -/// Gets information about a user -/// - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct Organization { - id: String, - name: String, - status: String, // Whether or not the user is a member of the organization or has an inivitation pending - permissions: Vec, // Access permissions for this User - roles: Vec, // List of role names for the User at the Organization -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct UserDetails { - pub organizations: Vec, - #[serde(default)] - pub betas: Vec, - pub telephone: Option, - pub zipcode: Option, - pub last_name: Option, - pub modified_on: DateTime, - pub username: String, - pub created_on: DateTime, - pub country: Option, - pub two_factor_authentication_enabled: bool, - pub first_name: Option, - pub id: String, - pub suspended: bool, - pub email: String, -} -impl ApiResult for UserDetails {} - -#[test] -fn handles_empty_betas_field() { - // note: omitted `betas` field from json data - const JSON_RESPONSE: &str = r#" - { - "id": "1234567890abcdef", - "email": "user@example.com", - "username": "user", - "first_name": null, - "last_name": null, - "telephone": null, - "country": null, - "zipcode": null, - "two_factor_authentication_enabled": false, - "two_factor_authentication_locked": false, - "created_on": "2015-02-24T13:03:05.255956Z", - "modified_on": "2018-06-10T23:50:04.029596Z", - "organizations": [], - "has_pro_zones": false, - "has_business_zones": false, - "has_enterprise_zones": false, - "suspended": false - }"#; - - let user_details: UserDetails = serde_json::from_str(JSON_RESPONSE).unwrap(); - assert!(user_details.betas.is_empty()); -} - -#[derive(Debug)] -pub struct GetUserDetails {} - -impl EndpointSpec for GetUserDetails { - fn method(&self) -> Method { - Method::GET - } - fn path(&self) -> String { - "user".to_string() - } -} - -/// Validate User Token -/// Returns whether a given token is valid or not. -/// -/// -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct UserTokenStatus { - pub id: String, - pub status: String, -} -impl ApiResult for UserTokenStatus {} - -#[derive(Debug)] -pub struct GetUserTokenStatus {} - -impl EndpointSpec for GetUserTokenStatus { - fn method(&self) -> Method { - Method::GET - } - fn path(&self) -> String { - "user/tokens/verify".to_string() - } -} diff --git a/cloudflare/src/endpoints/workers/create_route.rs b/cloudflare/src/endpoints/workers/create_route.rs deleted file mode 100644 index 376d191b..00000000 --- a/cloudflare/src/endpoints/workers/create_route.rs +++ /dev/null @@ -1,38 +0,0 @@ -use super::WorkersRouteIdOnly; - -use crate::framework::endpoint::{EndpointSpec, Method}; - -use serde::Serialize; - -/// Create a Route -/// Creates a route mapping the given pattern to the given script -/// -#[derive(Debug)] -pub struct CreateRoute<'a> { - pub zone_identifier: &'a str, - pub params: CreateRouteParams, -} - -impl<'a> EndpointSpec for CreateRoute<'a> { - fn method(&self) -> Method { - Method::POST - } - fn path(&self) -> String { - format!("zones/{}/workers/routes", self.zone_identifier) - } - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.params).unwrap(); - Some(body) - } -} - -/// pattern: the zone name along with glob-style wildcards -/// e.g. "example.net/*" -/// script: Name of the script to apply when the route is matched. -/// The route is skipped when this is blank/missing. -#[derive(Serialize, Clone, Debug)] -pub struct CreateRouteParams { - pub pattern: String, - pub script: Option, -} diff --git a/cloudflare/src/endpoints/workers/create_secret.rs b/cloudflare/src/endpoints/workers/create_secret.rs deleted file mode 100644 index ff97f9a3..00000000 --- a/cloudflare/src/endpoints/workers/create_secret.rs +++ /dev/null @@ -1,45 +0,0 @@ -use super::WorkersSecret; - -use crate::framework::endpoint::{EndpointSpec, Method}; - -use serde::Serialize; - -/// Create Secret -/// -#[derive(Debug)] -pub struct CreateSecret<'a> { - /// Account ID of script owner - pub account_identifier: &'a str, - /// The name of the script to attach the secret to - pub script_name: &'a str, - /// The contents of the secret - pub params: CreateSecretParams, -} - -impl<'a> EndpointSpec for CreateSecret<'a> { - fn method(&self) -> Method { - Method::PUT - } - fn path(&self) -> String { - format!( - "accounts/{}/workers/scripts/{}/secrets", - self.account_identifier, self.script_name - ) - } - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.params).unwrap(); - Some(body) - } -} - -#[derive(Serialize, Clone, Debug)] -pub struct CreateSecretParams { - /// the variable name of the secret that will be bound to the script - pub name: String, - /// the string value of the secret - pub text: String, - // type of binding (e.g.secret_text) - #[serde(rename = "type")] - pub secret_type: String, -} diff --git a/cloudflare/src/endpoints/workers/create_tail.rs b/cloudflare/src/endpoints/workers/create_tail.rs deleted file mode 100644 index 4fe6f43a..00000000 --- a/cloudflare/src/endpoints/workers/create_tail.rs +++ /dev/null @@ -1,48 +0,0 @@ -use super::WorkersTail; - -use crate::framework::endpoint::{EndpointSpec, Method}; - -use serde::Serialize; - -/// Create Tail -/// -#[derive(Debug)] -pub struct CreateTail<'a> { - /// Account ID of owner of the script - pub account_identifier: &'a str, - /// The name of the script to tail - pub script_name: &'a str, - /// V1 of tailing involved creating a separate URL, - /// which is still possible. - /// - /// V2 does not involve a separate URL, so it can - /// be omitted. - pub params: CreateTailParams, -} - -impl<'a> EndpointSpec for CreateTail<'a> { - fn method(&self) -> Method { - Method::POST - } - fn path(&self) -> String { - format!( - "accounts/{}/workers/scripts/{}/tails", - self.account_identifier, self.script_name - ) - } - #[inline] - fn body(&self) -> Option { - if self.params.url.is_some() { - let body = serde_json::to_string(&self.params).unwrap(); - Some(body) - } else { - None - } - } -} - -#[derive(Serialize, Clone, Debug, Default)] -pub struct CreateTailParams { - /// URL to which to send events - pub url: Option, -} diff --git a/cloudflare/src/endpoints/workers/delete_do.rs b/cloudflare/src/endpoints/workers/delete_do.rs deleted file mode 100644 index ad224cb0..00000000 --- a/cloudflare/src/endpoints/workers/delete_do.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// Delete a Durable Object namespace -#[derive(Debug)] -pub struct DeleteDurableObject<'a> { - /// account ID where the Durable Object is present - pub account_id: &'a str, - /// namespace ID of the Durable Object - pub namespace_id: &'a str, -} - -impl<'a> EndpointSpec<()> for DeleteDurableObject<'a> { - fn method(&self) -> Method { - Method::DELETE - } - - fn path(&self) -> String { - format!( - "accounts/{}/workers/durable_objects/namespaces/{}", - self.account_id, self.namespace_id - ) - } -} diff --git a/cloudflare/src/endpoints/workers/delete_route.rs b/cloudflare/src/endpoints/workers/delete_route.rs deleted file mode 100644 index ca252032..00000000 --- a/cloudflare/src/endpoints/workers/delete_route.rs +++ /dev/null @@ -1,24 +0,0 @@ -use super::WorkersRouteIdOnly; - -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// Delete a Route -/// Deletes a route by route id -/// -#[derive(Debug)] -pub struct DeleteRoute<'a> { - pub zone_identifier: &'a str, - pub identifier: &'a str, -} - -impl<'a> EndpointSpec for DeleteRoute<'a> { - fn method(&self) -> Method { - Method::DELETE - } - fn path(&self) -> String { - format!( - "zones/{}/workers/routes/{}", - self.zone_identifier, self.identifier - ) - } -} diff --git a/cloudflare/src/endpoints/workers/delete_script.rs b/cloudflare/src/endpoints/workers/delete_script.rs deleted file mode 100644 index e249d3a7..00000000 --- a/cloudflare/src/endpoints/workers/delete_script.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::framework::endpoint::{EndpointSpec, Method}; -use crate::framework::response::ApiResult; - -use serde::{Deserialize, Serialize}; - -/// Delete Workers script -/// -#[derive(Debug)] -pub struct DeleteScript<'a> { - /// account id of owner of the script - pub account_id: &'a str, - /// the name of the script to be removed - pub script_name: &'a str, -} - -impl<'a> EndpointSpec for DeleteScript<'a> { - fn method(&self) -> Method { - Method::DELETE - } - fn path(&self) -> String { - format!( - "accounts/{}/workers/scripts/{}", - self.account_id, self.script_name - ) - } -} - -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct ScriptDeleteID { - pub id: String, -} -impl ApiResult for ScriptDeleteID {} diff --git a/cloudflare/src/endpoints/workers/delete_secret.rs b/cloudflare/src/endpoints/workers/delete_secret.rs deleted file mode 100644 index 09eee7ba..00000000 --- a/cloudflare/src/endpoints/workers/delete_secret.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// Delete Secret -/// -#[derive(Debug)] -pub struct DeleteSecret<'a> { - /// account id of owner of the script - pub account_identifier: &'a str, - /// the name of the script to remove the secret from - pub script_name: &'a str, - /// the variable name of the secret - pub secret_name: &'a str, -} - -impl<'a> EndpointSpec<()> for DeleteSecret<'a> { - fn method(&self) -> Method { - Method::DELETE - } - fn path(&self) -> String { - format!( - "accounts/{}/workers/scripts/{}/secrets/{}", - self.account_identifier, self.script_name, self.secret_name - ) - } -} diff --git a/cloudflare/src/endpoints/workers/delete_tail.rs b/cloudflare/src/endpoints/workers/delete_tail.rs deleted file mode 100644 index 838d6270..00000000 --- a/cloudflare/src/endpoints/workers/delete_tail.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// Delete Tail -/// -#[derive(Debug)] -pub struct DeleteTail<'a> { - /// Account id of owner of the script - pub account_identifier: &'a str, - /// The name of the script to remove the Tail session from - pub script_name: &'a str, - /// The unique identifier of the Tail session - pub tail_id: &'a str, -} - -impl<'a> EndpointSpec<()> for DeleteTail<'a> { - fn method(&self) -> Method { - Method::DELETE - } - fn path(&self) -> String { - format!( - "accounts/{}/workers/scripts/{}/tails/{}", - self.account_identifier, self.script_name, self.tail_id - ) - } -} diff --git a/cloudflare/src/endpoints/workers/list_bindings.rs b/cloudflare/src/endpoints/workers/list_bindings.rs deleted file mode 100644 index 718b4e02..00000000 --- a/cloudflare/src/endpoints/workers/list_bindings.rs +++ /dev/null @@ -1,25 +0,0 @@ -use super::WorkersBinding; -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// List Bindings -/// Lists all bindings for a given script -#[derive(Debug)] -pub struct ListBindings<'a> { - /// account id of owner of the script - pub account_id: &'a str, - /// name of script to list bindings for - pub script_name: &'a str, -} - -impl<'a> EndpointSpec> for ListBindings<'a> { - fn method(&self) -> Method { - Method::GET - } - - fn path(&self) -> String { - format!( - "accounts/{}/workers/scripts/{}/bindings", - self.account_id, self.script_name - ) - } -} diff --git a/cloudflare/src/endpoints/workers/list_routes.rs b/cloudflare/src/endpoints/workers/list_routes.rs deleted file mode 100644 index bde68257..00000000 --- a/cloudflare/src/endpoints/workers/list_routes.rs +++ /dev/null @@ -1,20 +0,0 @@ -use super::WorkersRoute; - -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// List Routes -/// Lists all route mappings for a given zone -/// -#[derive(Debug)] -pub struct ListRoutes<'a> { - pub zone_identifier: &'a str, -} - -impl<'a> EndpointSpec> for ListRoutes<'a> { - fn method(&self) -> Method { - Method::GET - } - fn path(&self) -> String { - format!("zones/{}/workers/routes", self.zone_identifier) - } -} diff --git a/cloudflare/src/endpoints/workers/list_schedules.rs b/cloudflare/src/endpoints/workers/list_schedules.rs deleted file mode 100644 index 7496d760..00000000 --- a/cloudflare/src/endpoints/workers/list_schedules.rs +++ /dev/null @@ -1,38 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::WorkersSchedule; - -use crate::framework::{ - endpoint::{EndpointSpec, Method}, - response::ApiResult, -}; - -/// List Schedules -/// -#[derive(Debug)] -pub struct ListSchedules<'a> { - /// Account ID of owner of the script - pub account_identifier: &'a str, - /// The name of the script to list the schedules - pub script_name: &'a str, -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct ListSchedulesResponse { - pub schedules: Vec, -} - -impl ApiResult for ListSchedulesResponse {} - -impl<'a> EndpointSpec for ListSchedules<'a> { - fn method(&self) -> Method { - Method::GET - } - - fn path(&self) -> String { - format!( - "accounts/{}/workers/scripts/{}/schedules", - self.account_identifier, self.script_name - ) - } -} diff --git a/cloudflare/src/endpoints/workers/list_secrets.rs b/cloudflare/src/endpoints/workers/list_secrets.rs deleted file mode 100644 index 773322dc..00000000 --- a/cloudflare/src/endpoints/workers/list_secrets.rs +++ /dev/null @@ -1,24 +0,0 @@ -use super::WorkersSecret; - -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// List Secrets -/// Lists all secrets mappings for a given script -/// -#[derive(Debug)] -pub struct ListSecrets<'a> { - pub account_identifier: &'a str, - pub script_name: &'a str, -} - -impl<'a> EndpointSpec> for ListSecrets<'a> { - fn method(&self) -> Method { - Method::GET - } - fn path(&self) -> String { - format!( - "accounts/{}/workers/scripts/{}/secrets", - self.account_identifier, self.script_name - ) - } -} diff --git a/cloudflare/src/endpoints/workers/list_tails.rs b/cloudflare/src/endpoints/workers/list_tails.rs deleted file mode 100644 index a7630b28..00000000 --- a/cloudflare/src/endpoints/workers/list_tails.rs +++ /dev/null @@ -1,24 +0,0 @@ -use super::WorkersTail; - -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// List Tails -/// Lists all active Tail sessions for a given Worker -/// -#[derive(Debug)] -pub struct ListTails<'a> { - pub account_identifier: &'a str, - pub script_name: &'a str, -} - -impl<'a> EndpointSpec> for ListTails<'a> { - fn method(&self) -> Method { - Method::GET - } - fn path(&self) -> String { - format!( - "accounts/{}/workers/scripts/{}/tails", - self.account_identifier, self.script_name - ) - } -} diff --git a/cloudflare/src/endpoints/workers/mod.rs b/cloudflare/src/endpoints/workers/mod.rs deleted file mode 100644 index a60f8ba9..00000000 --- a/cloudflare/src/endpoints/workers/mod.rs +++ /dev/null @@ -1,394 +0,0 @@ -use crate::framework::response::ApiResult; - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -mod create_route; -mod create_secret; -mod create_tail; -mod delete_do; -mod delete_route; -mod delete_script; -mod delete_secret; -mod delete_tail; -mod list_bindings; -mod list_routes; -mod list_schedules; -mod list_secrets; -mod list_tails; -mod send_tail_heartbeat; -mod upsert_schedules; - -pub use create_route::{CreateRoute, CreateRouteParams}; -pub use create_secret::{CreateSecret, CreateSecretParams}; -pub use create_tail::{CreateTail, CreateTailParams}; -pub use delete_do::DeleteDurableObject; -pub use delete_route::DeleteRoute; -pub use delete_script::DeleteScript; -pub use delete_secret::DeleteSecret; -pub use delete_tail::DeleteTail; -pub use list_bindings::ListBindings; -pub use list_routes::ListRoutes; -pub use list_schedules::{ListSchedules, ListSchedulesResponse}; -pub use list_secrets::ListSecrets; -pub use list_tails::ListTails; -pub use send_tail_heartbeat::SendTailHeartbeat; -pub use upsert_schedules::UpsertSchedules; - -/// Workers KV Route -/// Routes are basic patterns used to enable or disable workers that match requests. -/// -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct WorkersRoute { - /// Namespace identifier tag. - pub id: String, - /// The basic pattern that should map to the script - pub pattern: String, - /// Name of the script to apply when the route is matched. - /// The route is skipped when this is blank/missing. - pub script: Option, -} - -impl ApiResult for WorkersRoute {} -impl ApiResult for Vec {} - -/// A variant of WorkersRoute returned by the CreateRoute endpoint -/// We could make `pattern` and `script` into `Option` types -/// but it feels wrong. -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct WorkersRouteIdOnly { - /// Namespace identifier tag. - pub id: String, -} - -impl ApiResult for WorkersRouteIdOnly {} - -/// Secrets attach to a single script to be readable in only the script -/// -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct WorkersSecret { - pub name: String, - #[serde(rename = "type")] - pub secret_type: String, -} - -impl ApiResult for WorkersSecret {} -impl ApiResult for Vec {} // to parse arrays too - -/// A Tail is attached to a single Worker and is impermanent -/// -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct WorkersTail { - pub id: String, - pub url: Option, - pub expires_at: DateTime, -} - -impl ApiResult for WorkersTail {} -impl ApiResult for Vec {} - -// Binding for a Workers Script -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum WorkersBinding { - Ai { - name: String, - }, - AnalyticsEngine { - name: String, - dataset: String, - }, - Assets { - name: String, - }, - BrowserRendering { - name: String, - }, - D1 { - name: String, - id: String, - }, - DurableObjectNamespace { - name: String, - class_name: String, - }, - Hyperdrive { - name: String, - id: String, - }, - KvNamespace { - name: String, - namespace_id: String, - }, - MtlsCertificate { - name: String, - certificate_id: String, - }, - PlainText { - name: String, - text: String, - }, - Queue { - name: String, - queue_name: String, - }, - R2Bucket { - name: String, - bucket_name: String, - }, - SecretText { - name: String, - text: Option, - }, - Service { - name: String, - service: String, - environment: String, - }, - TailConsumer { - service: String, - }, - Vectorize { - name: String, - index_name: String, - }, - VersionMetadata { - name: String, - }, -} - -impl ApiResult for WorkersBinding {} -impl ApiResult for Vec {} - -// Schedule for a Workers Script -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Default, PartialOrd, Ord)] -pub struct WorkersSchedule { - pub created_on: Option, - pub cron: Option, - pub modified_on: Option, -} - -impl ApiResult for WorkersSchedule {} -impl ApiResult for Vec {} - -#[cfg(test)] -mod tests { - use std::collections::VecDeque; - - use super::WorkersBinding; - - #[test] - fn test_deserializing_worker_bindings() { - // https://developers.cloudflare.com/workers/configuration/multipart-upload-metadata/#bindings - let payload = serde_json::json!( - [ - { - "type": "ai", - "name": "" - }, - { - "type": "analytics_engine", - "name": "", - "dataset": "" - }, - { - "type": "assets", - "name": "" - }, - { - "type": "browser_rendering", - "name": "" - }, - { - "type": "d1", - "name": "", - "id": "" - }, - { - "type": "durable_object_namespace", - "name": "", - "class_name": "" - }, - { - "type": "hyperdrive", - "name": "", - "id": "" - }, - { - "type": "kv_namespace", - "name": "", - "namespace_id": "" - }, - { - "type": "mtls_certificate", - "name": "", - "certificate_id": "" - }, - { - "type": "plain_text", - "name": "", - "text": "" - }, - { - "type": "queue", - "name": "", - "queue_name": "" - }, - { - "type": "r2_bucket", - "name": "", - "bucket_name": "" - }, - { - "type": "secret_text", - "name": "", - "text": "" - }, - { - "type": "service", - "name": "", - "service": "", - "environment": "production" - }, - { - "type": "tail_consumer", - "service": "" - }, - { - "type": "vectorize", - "name": "", - "index_name": "" - }, - { - "type": "version_metadata", - "name": "" - } - ] - ); - - let result: Result, serde_json::Error> = - serde_json::from_value(payload); - assert!(result.is_ok()); - - let mut bindings = VecDeque::from(result.unwrap()); - assert_eq!(17, bindings.len()); - - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::Ai { - name: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::AnalyticsEngine { - name: "".to_string(), - dataset: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::Assets { - name: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::BrowserRendering { - name: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::D1 { - name: "".to_string(), - id: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::DurableObjectNamespace { - name: "".to_string(), - class_name: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::Hyperdrive { - name: "".to_string(), - id: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::KvNamespace { - name: "".to_string(), - namespace_id: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::MtlsCertificate { - name: "".to_string(), - certificate_id: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::PlainText { - name: "".to_string(), - text: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::Queue { - name: "".to_string(), - queue_name: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::R2Bucket { - name: "".to_string(), - bucket_name: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::SecretText { - name: "".to_string(), - text: Some("".to_string()) - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::Service { - name: "".to_string(), - service: "".to_string(), - environment: "production".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::TailConsumer { - service: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::Vectorize { - name: "".to_string(), - index_name: "".to_string() - } - ); - assert_eq!( - bindings.pop_front().unwrap(), - WorkersBinding::VersionMetadata { - name: "".to_string() - } - ); - - assert!(bindings.is_empty()); - } -} diff --git a/cloudflare/src/endpoints/workers/send_tail_heartbeat.rs b/cloudflare/src/endpoints/workers/send_tail_heartbeat.rs deleted file mode 100644 index 9da8deda..00000000 --- a/cloudflare/src/endpoints/workers/send_tail_heartbeat.rs +++ /dev/null @@ -1,27 +0,0 @@ -use super::WorkersTail; - -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// Send Tail Heartbeat -/// -#[derive(Debug)] -pub struct SendTailHeartbeat<'a> { - /// Account ID of owner of the script - pub account_identifier: &'a str, - /// The name of the script to tail - pub script_name: &'a str, - /// The unique identifier of the tail session - pub tail_id: &'a str, -} - -impl<'a> EndpointSpec for SendTailHeartbeat<'a> { - fn method(&self) -> Method { - Method::POST - } - fn path(&self) -> String { - format!( - "accounts/{}/workers/scripts/{}/tails/{}/heartbeat", - self.account_identifier, self.script_name, self.tail_id - ) - } -} diff --git a/cloudflare/src/endpoints/workers/upsert_schedules.rs b/cloudflare/src/endpoints/workers/upsert_schedules.rs deleted file mode 100644 index 718ca59f..00000000 --- a/cloudflare/src/endpoints/workers/upsert_schedules.rs +++ /dev/null @@ -1,45 +0,0 @@ -use serde::Deserialize; - -use super::WorkersSchedule; - -use crate::framework::{ - endpoint::{EndpointSpec, Method}, - response::ApiResult, -}; - -/// Upsert Schedules -/// -#[derive(Debug)] -pub struct UpsertSchedules<'a> { - /// Account ID of owner of the script - pub account_identifier: &'a str, - /// The name of the script to upsert the schedules - pub script_name: &'a str, - /// Params for upserting the schedules - pub schedules: Vec, -} - -#[derive(Debug, Deserialize)] -pub struct UpsertSchedulesResponse { - pub schedules: Vec, -} - -impl ApiResult for UpsertSchedulesResponse {} - -impl<'a> EndpointSpec for UpsertSchedules<'a> { - fn method(&self) -> Method { - Method::PUT - } - - fn path(&self) -> String { - format!( - "accounts/{}/workers/scripts/{}/schedules", - self.account_identifier, self.script_name - ) - } - - #[inline] - fn body(&self) -> Option { - Some(serde_json::to_string(&self.schedules).unwrap()) - } -} diff --git a/cloudflare/src/endpoints/workerskv/create_namespace.rs b/cloudflare/src/endpoints/workerskv/create_namespace.rs deleted file mode 100644 index 849b9974..00000000 --- a/cloudflare/src/endpoints/workerskv/create_namespace.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::WorkersKvNamespace; - -use crate::framework::endpoint::{EndpointSpec, Method}; - -use serde::Serialize; - -/// Create a Namespace -/// Creates a namespace under the given title. -/// A 400 is returned if the account already owns a namespace with this title. -/// A namespace must be explicitly deleted to be replaced. -/// -#[derive(Debug)] -pub struct CreateNamespace<'a> { - pub account_identifier: &'a str, - pub params: CreateNamespaceParams, -} - -impl<'a> EndpointSpec for CreateNamespace<'a> { - fn method(&self) -> Method { - Method::POST - } - fn path(&self) -> String { - format!("accounts/{}/storage/kv/namespaces", self.account_identifier) - } - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.params).unwrap(); - Some(body) - } -} - -#[derive(Serialize, Clone, Debug)] -pub struct CreateNamespaceParams { - pub title: String, -} diff --git a/cloudflare/src/endpoints/workerskv/delete_bulk.rs b/cloudflare/src/endpoints/workerskv/delete_bulk.rs deleted file mode 100644 index f29cc448..00000000 --- a/cloudflare/src/endpoints/workerskv/delete_bulk.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// Delete Key-Value Pairs in Bulk -/// Deletes multiple key-value pairs from Workers KV at once. -/// A 404 is returned if a delete action is for a namespace ID the account doesn't have. -/// -#[derive(Debug)] -pub struct DeleteBulk<'a> { - pub account_identifier: &'a str, - pub namespace_identifier: &'a str, - pub bulk_keys: Vec, -} - -impl<'a> EndpointSpec<()> for DeleteBulk<'a> { - fn method(&self) -> Method { - Method::DELETE - } - fn path(&self) -> String { - format!( - "accounts/{}/storage/kv/namespaces/{}/bulk", - self.account_identifier, self.namespace_identifier - ) - } - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.bulk_keys).unwrap(); - Some(body) - } - // default content-type is already application/json -} diff --git a/cloudflare/src/endpoints/workerskv/delete_key.rs b/cloudflare/src/endpoints/workerskv/delete_key.rs deleted file mode 100644 index fe43d875..00000000 --- a/cloudflare/src/endpoints/workerskv/delete_key.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// Delete a key-value pair from Workers KV -/// Deletes a given key from the given namespace in Workers KV. -/// Returns 404 if the given namespace id is not found for an account. -/// -#[derive(Debug)] -pub struct DeleteKey<'a> { - pub account_identifier: &'a str, - pub namespace_identifier: &'a str, - pub key: &'a str, -} - -impl<'a> EndpointSpec<()> for DeleteKey<'a> { - fn method(&self) -> Method { - Method::DELETE - } - fn path(&self) -> String { - format!( - "accounts/{}/storage/kv/namespaces/{}/values/{}", - self.account_identifier, - self.namespace_identifier, - super::url_encode_key(self.key) - ) - } -} diff --git a/cloudflare/src/endpoints/workerskv/list_namespace_keys.rs b/cloudflare/src/endpoints/workerskv/list_namespace_keys.rs deleted file mode 100644 index 9d75db67..00000000 --- a/cloudflare/src/endpoints/workerskv/list_namespace_keys.rs +++ /dev/null @@ -1,38 +0,0 @@ -use super::Key; - -use crate::framework::endpoint::{serialize_query, EndpointSpec, Method}; - -use serde::Serialize; - -/// List a Namespace's Keys -/// -#[derive(Debug)] -pub struct ListNamespaceKeys<'a> { - pub account_identifier: &'a str, - pub namespace_identifier: &'a str, - pub params: ListNamespaceKeysParams, -} - -impl<'a> EndpointSpec> for ListNamespaceKeys<'a> { - fn method(&self) -> Method { - Method::GET - } - fn path(&self) -> String { - format!( - "accounts/{}/storage/kv/namespaces/{}/keys", - self.account_identifier, self.namespace_identifier - ) - } - #[inline] - fn query(&self) -> Option { - serialize_query(&self.params) - } -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug, Default)] -pub struct ListNamespaceKeysParams { - pub limit: Option, - pub cursor: Option, - pub prefix: Option, -} diff --git a/cloudflare/src/endpoints/workerskv/list_namespaces.rs b/cloudflare/src/endpoints/workerskv/list_namespaces.rs deleted file mode 100644 index adc57a9c..00000000 --- a/cloudflare/src/endpoints/workerskv/list_namespaces.rs +++ /dev/null @@ -1,34 +0,0 @@ -use super::WorkersKvNamespace; - -use crate::framework::endpoint::{serialize_query, EndpointSpec, Method}; - -use serde::Serialize; - -/// List Namespaces -/// Returns the namespaces owned by an account -/// -#[derive(Debug)] -pub struct ListNamespaces<'a> { - pub account_identifier: &'a str, - pub params: ListNamespacesParams, -} - -impl<'a> EndpointSpec> for ListNamespaces<'a> { - fn method(&self) -> Method { - Method::GET - } - fn path(&self) -> String { - format!("accounts/{}/storage/kv/namespaces", self.account_identifier) - } - #[inline] - fn query(&self) -> Option { - serialize_query(&self.params) - } -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Clone, Debug, Default)] -pub struct ListNamespacesParams { - pub page: Option, - pub per_page: Option, -} diff --git a/cloudflare/src/endpoints/workerskv/mod.rs b/cloudflare/src/endpoints/workerskv/mod.rs deleted file mode 100644 index 378fa2a6..00000000 --- a/cloudflare/src/endpoints/workerskv/mod.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::framework::response::ApiResult; -use chrono::DateTime; -use chrono::{TimeZone, Utc}; -use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; -use serde::{Deserialize, Deserializer, Serialize}; - -pub mod create_namespace; -pub mod delete_bulk; -pub mod delete_key; -pub mod list_namespace_keys; -pub mod list_namespaces; -pub mod remove_namespace; -pub mod rename_namespace; -pub mod write_bulk; - -// Upgrading to percent_encode 2.x unfortunately removed this prebaked const. -// We need to re-assemble it by combining "control" ASCII characters with other characters -// which are invalid or reserved in URIs. Non-ASCII characters are always encoded. - -// https://docs.rs/percent-encoding/1.0.0/src/percent_encoding/lib.rs.html#104 -const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &CONTROLS - // "QUERY_ENCODE_SET" additions: - .add(b' ') - .add(b'"') - .add(b'#') - .add(b'<') - .add(b'>') - // "DEFAULT_ENCODE_SET" additions: - .add(b'`') - .add(b'?') - .add(b'{') - .add(b'}') - // "PATH_SEGMENT_ENCODE_SET" additions - .add(b'%') - .add(b'/') - // The following were NOT in PATH_SEGMENT but are URI reserved characters not covered above. - // ':' and '@' are explicitly permitted in paths, so we don't add them. - .add(b'[') - .add(b']'); - -/// Workers KV Namespace -/// A Namespace is a collection of key-value pairs stored in Workers KV. -/// -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct WorkersKvNamespace { - /// Namespace identifier tag. - pub id: String, - /// A human-readable string name for a Namespace. - pub title: String, -} - -impl ApiResult for WorkersKvNamespace {} - -impl ApiResult for Vec {} - -#[serde_with::skip_serializing_none] -#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)] -pub struct Key { - pub name: String, - #[serde(default)] - #[serde(deserialize_with = "deserialize_option_timestamp")] - pub expiration: Option>, -} - -pub fn deserialize_option_timestamp<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - let s: Option = Option::deserialize(deserializer)?; - if let Some(s) = s { - return Ok(Utc.timestamp_opt(s, 0).single()); - } - - Ok(None) -} - -impl ApiResult for Key {} - -impl ApiResult for Vec {} - -fn url_encode_key(key: &str) -> String { - percent_encode(key.as_bytes(), PATH_SEGMENT_ENCODE_SET).to_string() -} diff --git a/cloudflare/src/endpoints/workerskv/remove_namespace.rs b/cloudflare/src/endpoints/workerskv/remove_namespace.rs deleted file mode 100644 index 3c9c87d0..00000000 --- a/cloudflare/src/endpoints/workerskv/remove_namespace.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::framework::endpoint::{EndpointSpec, Method}; - -/// Remove a Namespace -/// Deletes the namespace corresponding to the given ID. -/// -#[derive(Debug)] -pub struct RemoveNamespace<'a> { - pub account_identifier: &'a str, - pub namespace_identifier: &'a str, -} - -impl<'a> EndpointSpec<()> for RemoveNamespace<'a> { - fn method(&self) -> Method { - Method::DELETE - } - - fn path(&self) -> String { - format!( - "accounts/{}/storage/kv/namespaces/{}", - self.account_identifier, self.namespace_identifier - ) - } -} diff --git a/cloudflare/src/endpoints/workerskv/rename_namespace.rs b/cloudflare/src/endpoints/workerskv/rename_namespace.rs deleted file mode 100644 index 17dc9b7d..00000000 --- a/cloudflare/src/endpoints/workerskv/rename_namespace.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::framework::endpoint::{EndpointSpec, Method}; - -use serde::Serialize; - -/// Rename a Namespace -/// Modifies a namespace's title. -/// -#[derive(Debug)] -pub struct RenameNamespace<'a> { - pub account_identifier: &'a str, - pub namespace_identifier: &'a str, - pub params: RenameNamespaceParams, -} - -impl<'a> EndpointSpec<()> for RenameNamespace<'a> { - fn method(&self) -> Method { - Method::PUT - } - fn path(&self) -> String { - format!( - "accounts/{}/storage/kv/namespaces/{}", - self.account_identifier, self.namespace_identifier - ) - } - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.params).unwrap(); - Some(body) - } -} - -#[derive(Serialize, Clone, Debug)] -pub struct RenameNamespaceParams { - pub title: String, -} diff --git a/cloudflare/src/endpoints/workerskv/write_bulk.rs b/cloudflare/src/endpoints/workerskv/write_bulk.rs deleted file mode 100644 index c59f889a..00000000 --- a/cloudflare/src/endpoints/workerskv/write_bulk.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::framework::endpoint::{EndpointSpec, Method}; - -use serde::{Deserialize, Serialize}; - -/// Write Key-Value Pairs in Bulk -/// Writes multiple key-value pairs to Workers KV at once. -/// A 404 is returned if a write action is for a namespace ID the account doesn't have. -/// -#[derive(Debug)] -pub struct WriteBulk<'a> { - pub account_identifier: &'a str, - pub namespace_identifier: &'a str, - pub bulk_key_value_pairs: Vec, -} - -impl<'a> EndpointSpec<()> for WriteBulk<'a> { - fn method(&self) -> Method { - Method::PUT - } - fn path(&self) -> String { - format!( - "accounts/{}/storage/kv/namespaces/{}/bulk", - self.account_identifier, self.namespace_identifier - ) - } - - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.bulk_key_value_pairs).unwrap(); - Some(body) - } - // default content-type is already application/json -} - -#[serde_with::skip_serializing_none] -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct KeyValuePair { - pub key: String, - pub value: String, - pub expiration: Option, - pub expiration_ttl: Option, - pub base64: Option, -} diff --git a/cloudflare/src/endpoints/zone.rs b/cloudflare/src/endpoints/zone.rs deleted file mode 100644 index cda15ab1..00000000 --- a/cloudflare/src/endpoints/zone.rs +++ /dev/null @@ -1,208 +0,0 @@ -use crate::endpoints::{account::AccountDetails, plan::Plan}; -use crate::framework::endpoint::serialize_query; -use crate::framework::{ - endpoint::{EndpointSpec, Method}, - response::ApiResult, -}; -use crate::framework::{OrderDirection, SearchMatch}; -use chrono::offset::Utc; -use chrono::DateTime; -use serde::{Deserialize, Serialize}; - -/// List Zones -/// List, search, sort, and filter your zones -/// -#[derive(Debug)] -pub struct ListZones { - pub params: ListZonesParams, -} - -impl EndpointSpec> for ListZones { - fn method(&self) -> Method { - Method::GET - } - fn path(&self) -> String { - "zones".to_string() - } - #[inline] - fn query(&self) -> Option { - serialize_query(&self.params) - } -} - -/// Zone Details -/// -#[derive(Debug)] -pub struct ZoneDetails<'a> { - pub identifier: &'a str, -} -impl<'a> EndpointSpec for ZoneDetails<'a> { - fn method(&self) -> Method { - Method::GET - } - fn path(&self) -> String { - format!("zones/{}", self.identifier) - } -} - -/// Add Zone -/// -pub struct CreateZone<'a> { - pub params: CreateZoneParams<'a>, -} -impl<'a> EndpointSpec<()> for CreateZone<'a> { - fn method(&self) -> Method { - Method::POST - } - - fn path(&self) -> String { - "zones".to_string() - } - - #[inline] - fn body(&self) -> Option { - let body = serde_json::to_string(&self.params).unwrap(); - Some(body) - } -} - -#[derive(Serialize, Clone, Debug, Default)] -pub struct CreateZoneParams<'a> { - pub name: &'a str, - pub account: &'a str, - pub jump_start: Option, - #[serde(rename = "type")] - pub zone_type: Option, -} - -#[derive(Serialize, Clone, Debug, Default)] -pub struct ListZonesParams { - pub name: Option, - pub status: Option, - pub page: Option, - pub per_page: Option, - pub order: Option, - pub direction: Option, - #[serde(rename = "match")] - pub search_match: Option, -} - -#[derive(Serialize, Clone, Debug)] -#[serde(rename_all = "lowercase")] -pub enum ListZonesOrder { - Name, - Status, - Email, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename = "status", rename_all = "lowercase")] -pub enum Status { - Active, - Pending, - Initializing, - Moved, - Deleted, - Deactivated, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "lowercase", tag = "type")] -pub enum Owner { - User { - id: Option, - email: Option, - }, - Organization { - id: Option, - name: Option, - }, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "lowercase")] -pub enum Type { - Full, - Partial, -} - -#[derive(Deserialize, Debug)] -pub struct HostingPartner { - /// Host company name - pub name: String, - /// The host's website URL - pub website: String, -} - -#[derive(Deserialize, Debug)] -pub struct Meta { - /// Maximum custom certificates that can be uploaded/used. - pub custom_certificate_quota: u32, - /// Maximum page rules that can be created. - pub page_rule_quota: u32, - /// Indicates if URLs on the zone have been identified as hosting phishing content. - pub phishing_detected: bool, -} - -/// A Zone is a domain name along with its subdomains and other identities -/// -#[derive(Deserialize, Debug)] -pub struct Zone { - /// Zone identifier tag - pub id: String, - /// The domain name - pub name: String, - /// Information about the account the zone belongs to - pub account: AccountDetails, - /// The last time proof of ownership was detected and the zone was made active - pub activated_on: DateTime, - /// A list of beta features in which the zone is participating - pub betas: Option>, - /// When the zone was created - pub created_on: DateTime, - /// Exists only with a deactivated status and indicates the reason the zone is not resolving on - /// the Cloudflare network. - pub deactivation_reason: Option, - /// The interval (in seconds) from when development mode expires (positive integer) or last - /// expired (negative integer) for the domain. If development mode has never been enabled, this - /// value is 0. - pub development_mode: i32, - /// Hosting partner information, if the zone signed up via a Cloudflare hosting partner - pub host: Option, - /// Metadata about the domain. - pub meta: Meta, - /// When the zone was last modified - pub modified_on: DateTime, - /// Cloudflare-assigned name servers. This is only populated for zones that use Cloudflare DNS - pub name_servers: Vec, - /// DNS host at the time of switching to Cloudflare - pub original_dnshost: Option, - /// Original name servers before moving to Cloudflare - pub original_name_servers: Option>, - /// Registrar for the domain at the time of switching to Cloudflare - pub original_registrar: Option, - /// Information about the owner of the zone - pub owner: Owner, - /// Indicates if the zone is only using Cloudflare DNS services. A true value means the zone - /// will not receive security or performance benefits. - pub paused: bool, - /// Available permissions on the zone for the current user requesting the item - pub permissions: Vec, - /// A zone plan - pub plan: Option, - /// A zone plan - pub plan_pending: Option, - /// Status of the zone - pub status: Status, - /// An array of domains used for custom name servers. This is only available for Business and - /// Enterprise plans. - pub vanity_name_servers: Option>, - /// A full zone implies that DNS is hosted with Cloudflare. A partial zone is typically a - /// partner-hosted zone or a CNAME setup. - #[serde(rename = "type")] - pub zone_type: Type, -} - -// TODO: This should probably be a derive macro -impl ApiResult for Zone {} -impl ApiResult for Vec {} diff --git a/cloudflare/src/framework/async_api.rs b/cloudflare/src/framework/async_api.rs deleted file mode 100644 index 3c54f42d..00000000 --- a/cloudflare/src/framework/async_api.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::framework::{ - auth, - auth::{AuthClient, Credentials}, - endpoint::Endpoint, - response::{ApiErrors, ApiFailure, ApiSuccess}, - response::{ApiResponse, ApiResult}, - Environment, HttpApiClientConfig, -}; - -/// A Cloudflare API client that makes requests asynchronously. -pub struct Client { - environment: Environment, - credentials: auth::Credentials, - http_client: reqwest::Client, -} - -impl AuthClient for reqwest::RequestBuilder { - fn auth(mut self, credentials: &Credentials) -> Self { - for (k, v) in credentials.headers() { - self = self.header(k, v); - } - self - } -} - -impl Client { - pub fn new( - credentials: auth::Credentials, - config: HttpApiClientConfig, - environment: Environment, - ) -> Result { - #[allow(unused_mut)] - let mut builder = reqwest::Client::builder().default_headers(config.default_headers); - - #[cfg(not(target_arch = "wasm32"))] - { - use std::net::SocketAddr; - - // There is no resolve method in wasm. - if let Some(address) = config.resolve_ip { - let url = url::Url::from(&environment); - builder = builder.resolve( - url.host_str() - .expect("Environment url should have a hostname"), - SocketAddr::new(address, 443), - ); - } - - // There are no timeouts in wasm. The property is documented as no-op in wasm32. - builder = builder.timeout(config.http_timeout); - } - - let http_client = builder.build()?; - - Ok(Client { - environment, - credentials, - http_client, - }) - } - - /// Issue an API request of the given type. - pub async fn request( - &self, - endpoint: &(dyn Endpoint + Send + Sync), - ) -> ApiResponse - where - ResultType: ApiResult, - { - // Build the request - let mut request = self - .http_client - .request(endpoint.method(), endpoint.url(&self.environment)); - - if let Some(body) = endpoint.body() { - request = request.body(body); - request = request.header( - reqwest::header::CONTENT_TYPE, - endpoint.content_type().as_ref(), - ); - } - - request = request.auth(&self.credentials); - let response = request.send().await?; - map_api_response(response).await - } -} - -// If the response is 2XX and parses, return Success. -// If the response is 2XX and doesn't parse, return Invalid. -// If the response isn't 2XX, return Failure, with API errors if they were included. -async fn map_api_response( - resp: reqwest::Response, -) -> ApiResponse { - let status = resp.status(); - if status.is_success() { - let parsed: Result, reqwest::Error> = resp.json().await; - match parsed { - Ok(api_resp) => Ok(api_resp), - Err(e) => Err(ApiFailure::Invalid(e)), - } - } else { - let parsed: Result = resp.json().await; - let errors = parsed.unwrap_or_default(); - Err(ApiFailure::Error(status, errors)) - } -} diff --git a/cloudflare/src/framework/auth.rs b/cloudflare/src/framework/auth.rs deleted file mode 100644 index 9ffef243..00000000 --- a/cloudflare/src/framework/auth.rs +++ /dev/null @@ -1,24 +0,0 @@ -#[derive(Clone, Debug)] -pub enum Credentials { - UserAuthKey { email: String, key: String }, - UserAuthToken { token: String }, - Service { key: String }, -} - -impl Credentials { - pub fn headers(&self) -> Vec<(&'static str, String)> { - match self { - Self::UserAuthKey { email, key } => { - vec![("X-Auth-Email", email.clone()), ("X-Auth-Key", key.clone())] - } - Self::UserAuthToken { token } => { - vec![("Authorization", format!("Bearer {}", token.clone()))] - } - Self::Service { key } => vec![("X-Auth-User-Service-Key", key.clone())], - } - } -} - -pub trait AuthClient { - fn auth(self, credentials: &Credentials) -> Self; -} diff --git a/cloudflare/src/framework/blocking_api.rs b/cloudflare/src/framework/blocking_api.rs deleted file mode 100644 index c3a24f62..00000000 --- a/cloudflare/src/framework/blocking_api.rs +++ /dev/null @@ -1,75 +0,0 @@ -use reqwest::blocking::RequestBuilder; -use std::net::SocketAddr; - -use crate::framework::auth::Credentials; -use crate::framework::{ - auth, auth::AuthClient, endpoint, response, response::map_api_response, Environment, - HttpApiClient, HttpApiClientConfig, -}; - -impl HttpApiClient { - pub fn new( - credentials: auth::Credentials, - config: HttpApiClientConfig, - environment: Environment, - ) -> Result { - let mut builder = reqwest::blocking::Client::builder() - .timeout(config.http_timeout) - .default_headers(config.default_headers); - - if let Some(address) = config.resolve_ip { - let url = url::Url::from(&environment); - builder = builder.resolve( - url.host_str() - .expect("Environment url should have a hostname"), - SocketAddr::new(address, 443), - ); - } - let http_client = builder.build()?; - - Ok(HttpApiClient { - environment, - credentials, - http_client, - }) - } - - // TODO: This should probably just implement request for the Reqwest client itself :) - // TODO: It should also probably be called `ReqwestApiClient` rather than `HttpApiClient`. - /// Synchronously send a request to the Cloudflare API. - pub fn request( - &self, - endpoint: &dyn endpoint::Endpoint, - ) -> response::ApiResponse - where - ResultType: response::ApiResult, - { - // Build the request - let mut request = self - .http_client - .request(endpoint.method(), endpoint.url(&self.environment)); - - if let Some(body) = endpoint.body() { - request = request.body(body); - request = request.header( - reqwest::header::CONTENT_TYPE, - endpoint.content_type().as_ref(), - ); - } - - request = request.auth(&self.credentials); - - let response = request.send()?; - - map_api_response(response) - } -} - -impl AuthClient for RequestBuilder { - fn auth(mut self, credentials: &Credentials) -> Self { - for (k, v) in credentials.headers() { - self = self.header(k, v); - } - self - } -} diff --git a/cloudflare/src/framework/endpoint.rs b/cloudflare/src/framework/endpoint.rs deleted file mode 100644 index b013467d..00000000 --- a/cloudflare/src/framework/endpoint.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::framework::response::ApiResult; -use crate::framework::Environment; -use serde::Serialize; -use std::borrow::Cow; -use url::Url; - -pub use http::Method; - -#[cfg(feature = "endpoint-spec")] -pub use spec::EndpointSpec; -#[cfg(not(feature = "endpoint-spec"))] -pub(crate) use spec::EndpointSpec; - -pub mod spec { - use super::*; - - /// Represents a specification for an API call that can be built into an HTTP request and sent. - /// New endpoints should implement this trait. - /// - /// If the request succeeds, the call will resolve to a `ResultType`. - pub trait EndpointSpec - where - ResultType: ApiResult, - { - /// The HTTP Method used for this endpoint (e.g. GET, PATCH, DELETE) - fn method(&self) -> http::Method; - - /// The relative URL path for this endpoint - fn path(&self) -> String; - - /// The url-encoded query string associated with this endpoint. Defaults to `None`. - /// - /// Implementors should inline this. - #[inline] - fn query(&self) -> Option { - None - } - - /// The HTTP body associated with this endpoint. If not implemented, defaults to `None`. - /// - /// Implementors should inline this. - #[inline] - fn body(&self) -> Option { - None - } - - /// Builds and returns a formatted full URL, including query, for the endpoint. - /// - /// Implementors should generally not override this. - fn url(&self, environment: &Environment) -> Url { - let mut url = Url::from(environment).join(&self.path()).unwrap(); - url.set_query(self.query().as_deref()); - url - } - - /// If `body` is populated, indicates the body MIME type (defaults to JSON). - /// - /// Implementors generally do not need to override this. - fn content_type(&self) -> Cow<'static, str> { - Cow::Borrowed("application/json") - } - } -} -// Auto-implement the public Endpoint trait for EndpointInternal implementors. -impl> Endpoint for U {} - -/// An API call that can be built into an HTTP request and sent. -/// -/// If the request succeeds, the call will resolve to a `ResultType`. -pub trait Endpoint: spec::EndpointSpec {} - -/// A utility function for serializing parameters into a URL query string. -#[inline] -pub fn serialize_query(q: &Q) -> Option { - serde_urlencoded::to_string(q).ok() -} diff --git a/cloudflare/src/framework/mod.rs b/cloudflare/src/framework/mod.rs deleted file mode 100644 index 68ebec54..00000000 --- a/cloudflare/src/framework/mod.rs +++ /dev/null @@ -1,93 +0,0 @@ -/*! -This module controls how requests are sent to Cloudflare's API, and how responses are parsed from it. - */ -pub mod async_api; -pub mod auth; -// There is no blocking implementation for wasm. -#[cfg(all(feature = "blocking", not(target_arch = "wasm32")))] -pub mod blocking_api; -pub mod endpoint; -pub mod response; - -use serde::Serialize; -use std::net::IpAddr; -use std::time::Duration; - -#[derive(thiserror::Error, Debug)] -/// Errors encountered while trying to connect to the Cloudflare API -pub enum Error { - /// An error via the `reqwest` crate - #[error("Reqwest returned an error when connecting to the Cloudflare API: {0}")] - ReqwestError(#[from] reqwest::Error), -} - -#[derive(Serialize, Clone, Debug)] -pub enum OrderDirection { - #[serde(rename = "asc")] - Ascending, - #[serde(rename = "desc")] - Descending, -} - -/// Used as a parameter to API calls that search for a resource (e.g. DNS records). -/// Tells the API whether to return results that match all search requirements or at least one (any). -#[derive(Serialize, Clone, Debug)] -#[serde(rename_all = "lowercase")] -pub enum SearchMatch { - /// Match all search requirements - All, - /// Match at least one search requirement - Any, -} - -/// Which environment (host path) to use for API calls -#[derive(Debug)] -pub enum Environment { - /// The production endpoint: `https://api.cloudflare.com/client/v4` - Production, - /// A custom endpoint - Custom(url::Url), -} - -impl<'a> From<&'a Environment> for url::Url { - fn from(environment: &Environment) -> Self { - match environment { - Environment::Production => { - url::Url::parse("https://api.cloudflare.com/client/v4/").unwrap() - } - Environment::Custom(url) => url.clone(), - } - } -} - -// There is no blocking support for wasm. -#[cfg(all(feature = "blocking", not(target_arch = "wasm32")))] -/// Synchronous Cloudflare API client. -pub struct HttpApiClient { - environment: Environment, - credentials: auth::Credentials, - http_client: reqwest::blocking::Client, -} - -/// Configuration for the API client. Allows users to customize its behaviour. -pub struct HttpApiClientConfig { - /// The maximum time limit for an API request. If a request takes longer than this, it will be - /// cancelled. - /// Note: this configuration has no effect when the target is wasm32. - pub http_timeout: Duration, - /// A default set of HTTP headers which will be sent with each API request. - pub default_headers: http::HeaderMap, - /// A specific IP to use when establishing a connection - /// Note: this configuration has no effect when the target is wasm32. - pub resolve_ip: Option, -} - -impl Default for HttpApiClientConfig { - fn default() -> Self { - HttpApiClientConfig { - http_timeout: Duration::from_secs(30), - default_headers: http::HeaderMap::default(), - resolve_ip: None, - } - } -} diff --git a/cloudflare/src/framework/response/apifail.rs b/cloudflare/src/framework/response/apifail.rs deleted file mode 100644 index 0d4cf636..00000000 --- a/cloudflare/src/framework/response/apifail.rs +++ /dev/null @@ -1,93 +0,0 @@ -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serde_json::value::Value as JValue; -use std::collections::HashMap; -use std::error::Error; -use std::fmt::{self, Debug, Write as _}; - -/// Note that APIError's `eq` implementation only compares `code` and `message`. -/// It does NOT compare the `other` values. -#[derive(Deserialize, Serialize, Debug)] -pub struct ApiError { - pub code: u16, - pub message: String, - #[serde(flatten)] - pub other: HashMap, -} - -/// Note that APIErrors's `eq` implementation only compares `code` and `message`. -/// It does NOT compare the `other` values. -#[derive(Deserialize, Serialize, Debug, Default)] -pub struct ApiErrors { - #[serde(flatten)] - pub other: HashMap, - pub errors: Vec, -} - -impl PartialEq for ApiErrors { - fn eq(&self, other: &Self) -> bool { - self.errors == other.errors - } -} - -impl PartialEq for ApiError { - fn eq(&self, other: &Self) -> bool { - self.code == other.code && self.message == other.message - } -} - -impl Eq for ApiError {} -impl Eq for ApiErrors {} -impl Error for ApiError {} - -impl fmt::Display for ApiError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Error {}: {}", self.code, self.message) - } -} - -pub trait ApiResult: DeserializeOwned + Debug {} - -#[derive(Debug)] -pub enum ApiFailure { - Error(reqwest::StatusCode, ApiErrors), - Invalid(reqwest::Error), -} - -impl Error for ApiFailure {} - -impl PartialEq for ApiFailure { - fn eq(&self, other: &ApiFailure) -> bool { - match (self, other) { - (ApiFailure::Invalid(e1), ApiFailure::Invalid(e2)) => e1.to_string() == e2.to_string(), - (ApiFailure::Error(status1, e1), ApiFailure::Error(status2, e2)) => { - status1 == status2 && e1 == e2 - } - _ => false, - } - } -} -impl Eq for ApiFailure {} - -impl fmt::Display for ApiFailure { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ApiFailure::Error(status, api_errors) => { - let mut output = format!("HTTP {status}"); - for err in &api_errors.errors { - let _ = write!(output, "\n{}: {} ({:?})", err.code, err.message, err.other); - } - for (k, v) in &api_errors.other { - let _ = write!(output, "\n{k}: {v}"); - } - write!(f, "{output}") - } - ApiFailure::Invalid(err) => write!(f, "{err}"), - } - } -} - -impl From for ApiFailure { - fn from(error: reqwest::Error) -> Self { - ApiFailure::Invalid(error) - } -} diff --git a/cloudflare/src/framework/response/mod.rs b/cloudflare/src/framework/response/mod.rs deleted file mode 100644 index b30caca3..00000000 --- a/cloudflare/src/framework/response/mod.rs +++ /dev/null @@ -1,83 +0,0 @@ -mod apifail; - -pub use apifail::*; -use serde::{Deserialize, Serialize}; -use serde_json::value::Value as JsonValue; - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ApiSuccess { - pub result: ResultType, - pub result_info: Option, - #[serde(default)] - pub messages: JsonValue, - #[serde(default)] - pub errors: Vec, -} - -pub type ApiResponse = Result, ApiFailure>; - -// There is no blocking implementation for wasm. -#[cfg(all(feature = "blocking", not(target_arch = "wasm32")))] -// If the response is 200 and parses, return Success. -// If the response is 200 and doesn't parse, return Invalid. -// If the response isn't 200, return Failure, with API errors if they were included. -pub fn map_api_response( - resp: reqwest::blocking::Response, -) -> ApiResponse { - let status = resp.status(); - if status.is_success() { - let parsed: Result, reqwest::Error> = resp.json(); - match parsed { - Ok(api_resp) => Ok(api_resp), - Err(e) => Err(ApiFailure::Invalid(e)), - } - } else { - let parsed: Result = resp.json(); - let errors = parsed.unwrap_or_default(); - Err(ApiFailure::Error(status, errors)) - } -} - -/// Some endpoints return nothing. That's OK. -impl ApiResult for () {} - -#[cfg(all(test, feature = "blocking", not(target_arch = "wasm32")))] -mod tests { - use super::*; - use std::collections::HashMap; - - #[test] - fn api_failure_eq() { - let err1 = ApiFailure::Error( - reqwest::StatusCode::NOT_FOUND, - ApiErrors { - errors: vec![ApiError { - code: 1000, - message: "some failed".to_owned(), - other: HashMap::new(), - }], - other: HashMap::new(), - }, - ); - assert_eq!(err1, err1); - - let err2 = ApiFailure::Error( - reqwest::StatusCode::NOT_FOUND, - ApiErrors { - errors: vec![ApiError { - code: 1000, - message: "some different thing failed".to_owned(), - other: HashMap::new(), - }], - other: HashMap::new(), - }, - ); - assert_ne!(err2, err1); - - let not_real_website = "notavalid:url.evena little"; - let fail = ApiFailure::Invalid(reqwest::blocking::get(not_real_website).unwrap_err()); - assert_eq!(fail, fail); - assert_ne!(fail, err1); - assert_ne!(fail, err2); - } -} diff --git a/cloudflare/src/lib.rs b/cloudflare/src/lib.rs deleted file mode 100644 index d67cd9c4..00000000 --- a/cloudflare/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -#![forbid(unsafe_code)] -#![allow(clippy::needless_lifetimes)] - -pub mod endpoints; -pub mod framework; - -/// Linkup <-> Cloudflare specific features. Changes here will not be upstreamed to cloudflare-rs. -pub mod linkup; diff --git a/cloudflare/src/linkup.rs b/cloudflare/src/linkup.rs deleted file mode 100644 index 19e8f48f..00000000 --- a/cloudflare/src/linkup.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::{ - endpoints, - framework::{self, response::ApiFailure}, -}; - -pub async fn tunnel_prefix( - client: &framework::async_api::Client, - zone_id: &str, -) -> Result { - let req = endpoints::zone::ZoneDetails { - identifier: zone_id, - }; - - let zone = client.request(&req).await?; - - let zone_name = zone.result.name.replace(".", "-"); - let tunnel_name = format!("linkup-tunnel-{}-", zone_name); - - Ok(tunnel_name) -} diff --git a/linkup-cli/Cargo.toml b/linkup-cli/Cargo.toml index 98242419..f1ec8185 100644 --- a/linkup-cli/Cargo.toml +++ b/linkup-cli/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "linkup-cli" version = "3.5.0" -edition = "2021" +edition = "2024" build = "build.rs" [[bin]] @@ -9,41 +9,42 @@ name = "linkup" path = "src/main.rs" [dependencies] -anyhow = "1" +anyhow = { workspace = true } clap = { version = "4.5.27", features = ["derive", "cargo"] } clap_complete = "4.5.42" -cloudflare = { path = "../cloudflare", default-features = false, features = [ +cloudflare = { workspace = true, features = [ "rustls-tls", ] } colored = "3.0.0" ctrlc = { version = "3.4.5", features = ["termination"] } hickory-resolver = { version = "0.25.2", features = ["tokio"] } -linkup = { path = "../linkup" } -linkup-local-server = { path = "../local-server" } +linkup = { workspace = true } +linkup-local-server = { workspace = true } log = "0.4.25" -rand = "0.10.0" -regex = "1.11.1" -reqwest = { version = "0.13.2", default-features = false, features = [ +rand = { workspace = true } +regex = { workspace = true } +reqwest = { workspace = true, features = [ "json", "multipart", "blocking", "rustls", "query", ] } -serde = "1.0.217" -serde_json = "1.0.137" +serde = { workspace = true } +serde_json = { workspace = true } serde_yaml = "0.9.34-deprecated" -tokio = { version = "1.43.0", features = ["macros"] } -thiserror = "2.0.11" -url = { version = "2.5.4", features = ["serde"] } -base64 = "0.22.1" +tokio = { workspace = true, features = ["macros"] } +thiserror = { workspace = true } +url = { workspace = true } +base64 = { workspace = true } env_logger = "0.11.6" crossterm = "0.29.0" sysinfo = "0.38.2" -sha2 = "0.10.8" -hex = "0.4.3" +sha2 = { workspace = true } +hex = { workspace = true } tar = "0.4.43" flate2 = "1.0.35" +indicatif = "0.18.4" [dev-dependencies] mockall = "0.14.0" diff --git a/linkup-cli/src/commands/completion.rs b/linkup-cli/src/commands/completion.rs index 7db1fe39..ea1be4b9 100644 --- a/linkup-cli/src/commands/completion.rs +++ b/linkup-cli/src/commands/completion.rs @@ -1,7 +1,7 @@ use std::io::stdout; use clap::{Command, CommandFactory}; -use clap_complete::{generate, Generator, Shell}; +use clap_complete::{Generator, Shell, generate}; use crate::{Cli, Result}; @@ -19,6 +19,11 @@ pub fn completion(args: &Args) -> Result<()> { Ok(()) } -fn print_completions(gen: &G, cmd: &mut Command) { - generate(gen.clone(), cmd, cmd.get_name().to_string(), &mut stdout()); +fn print_completions(generator: &G, cmd: &mut Command) { + generate( + generator.clone(), + cmd, + cmd.get_name().to_string(), + &mut stdout(), + ); } diff --git a/linkup-cli/src/commands/deploy/api.rs b/linkup-cli/src/commands/deploy/api.rs index a92c1cd5..3c80bd85 100644 --- a/linkup-cli/src/commands/deploy/api.rs +++ b/linkup-cli/src/commands/deploy/api.rs @@ -1,11 +1,11 @@ -use reqwest::{multipart, Client}; +use reqwest::{Client, multipart}; use serde::{Deserialize, Serialize}; use serde_json::json; use super::{ + DeployError, auth::CloudflareApiAuth, resources::{DNSRecord, Rule, WorkerMetadata, WorkerScriptInfo, WorkerScriptPart}, - DeployError, }; pub trait CloudflareApi { @@ -795,20 +795,19 @@ impl CloudflareApi for AccountCloudflareApi { return Err(DeployError::OtherError); } - if let Some(records) = data.result { - if let Some(r) = records + if let Some(records) = data.result + && let Some(r) = records .into_iter() .find(|r| r.comment == Some(comment.clone())) - { - return Ok(Some(DNSRecord { - id: r.id, - name: r.name, - record_type: r.record_type, - content: r.content, - comment: comment.clone(), - proxied: r.proxied.unwrap_or(false), - })); - } + { + return Ok(Some(DNSRecord { + id: r.id, + name: r.name, + record_type: r.record_type, + content: r.content, + comment: comment.clone(), + proxied: r.proxied.unwrap_or(false), + })); } Ok(None) diff --git a/linkup-cli/src/commands/deploy/cf_deploy.rs b/linkup-cli/src/commands/deploy/cf_deploy.rs index 821920ce..37c34487 100644 --- a/linkup-cli/src/commands/deploy/cf_deploy.rs +++ b/linkup-cli/src/commands/deploy/cf_deploy.rs @@ -1,6 +1,6 @@ +use crate::Result; use crate::commands::deploy::auth; use crate::commands::deploy::resources::cf_resources; -use crate::Result; use super::api::{AccountCloudflareApi, CloudflareApi}; use super::console_notify::ConsoleNotifier; @@ -58,12 +58,12 @@ pub async fn deploy(args: &DeployArgs) -> Result<()> { zone_ids_strings.clone(), Box::new(auth), ); - let cloudflare_client = cloudflare::framework::async_api::Client::new( + let cloudflare_client = cloudflare::framework::client::async_api::Client::new( cloudflare::framework::auth::Credentials::UserAuthKey { email: args.email.clone(), key: args.api_key.clone(), }, - cloudflare::framework::HttpApiClientConfig::default(), + cloudflare::framework::client::ClientConfig::default(), cloudflare::framework::Environment::Production, ) .expect("Cloudflare API Client to have been created"); @@ -90,7 +90,7 @@ pub async fn deploy(args: &DeployArgs) -> Result<()> { pub async fn deploy_to_cloudflare( resources: &TargetCfResources, api: &impl CloudflareApi, - cloudflare_client: &cloudflare::framework::async_api::Client, + cloudflare_client: &cloudflare::framework::client::async_api::Client, notifier: &impl DeployNotifier, ) -> Result<()> { // 1) Check what needs to change @@ -125,7 +125,8 @@ pub async fn deploy_to_cloudflare( #[cfg(test)] mod tests { use cloudflare::framework::{ - async_api::Client, auth, endpoint::spec::EndpointSpec, Environment, HttpApiClientConfig, + Environment, auth, client::ClientConfig, client::async_api::Client, + endpoint::spec::EndpointSpec, }; use mockito::ServerGuard; use std::cell::RefCell; @@ -135,8 +136,9 @@ mod tests { api::Token, cf_destroy::destroy_from_cloudflare, resources::{ - rules_equal, DNSRecord, KvNamespace, Rule, TargectCfZoneResources, TargetCacheRules, + DNSRecord, KvNamespace, Rule, TargectCfZoneResources, TargetCacheRules, TargetDNSRecord, TargetWorkerRoute, WorkerMetadata, WorkerScriptInfo, WorkerScriptPart, + rules_equal, }, }; @@ -149,8 +151,8 @@ mod tests { email: "test@example.com".to_string(), key: "test-api-key".to_string(), }, - HttpApiClientConfig::default(), - Environment::Custom(mock_server_url), + ClientConfig::default(), + Environment::Custom(mock_server_url.to_string()), ) .unwrap() } @@ -167,7 +169,7 @@ mod tests { let res = serde_json::to_string(&cloudflare::framework::response::ApiSuccess::> { result: vec![], result_info: None, - messages: serde_json::json!([]), + messages: vec![], errors: vec![], }) .unwrap(); @@ -191,7 +193,7 @@ mod tests { > { result: cloudflare::endpoints::workers::ListSchedulesResponse { schedules: vec![] }, result_info: None, - messages: serde_json::json!([]), + messages: vec![], errors: vec![], }) .unwrap(); @@ -651,9 +653,11 @@ export default { let dns_records = api.dns_records.borrow(); assert_eq!(dns_records.len(), 1); assert_eq!(dns_records[0].name, "linkup-integration-test"); - assert!(dns_records[0] - .content - .contains("linkup-integration-test-script.workers.dev")); + assert!( + dns_records[0] + .content + .contains("linkup-integration-test-script.workers.dev") + ); // Check route created let routes = api.worker_routes.borrow(); @@ -760,12 +764,12 @@ export default { Box::new(global_api_auth), ); - let cloudflare_client = cloudflare::framework::async_api::Client::new( + let cloudflare_client = cloudflare::framework::client::async_api::Client::new( cloudflare::framework::auth::Credentials::UserAuthKey { email, key: api_key, }, - cloudflare::framework::HttpApiClientConfig::default(), + cloudflare::framework::client::ClientConfig::default(), cloudflare::framework::Environment::Production, ) .expect("Cloudflare API Client to have been created"); diff --git a/linkup-cli/src/commands/deploy/cf_destroy.rs b/linkup-cli/src/commands/deploy/cf_destroy.rs index b0fb9d1d..29a441e2 100644 --- a/linkup-cli/src/commands/deploy/cf_destroy.rs +++ b/linkup-cli/src/commands/deploy/cf_destroy.rs @@ -1,7 +1,7 @@ +use crate::Result; use crate::commands::deploy::{ api::AccountCloudflareApi, auth, console_notify::ConsoleNotifier, resources::cf_resources, }; -use crate::Result; use super::{api::CloudflareApi, cf_deploy::DeployNotifier, resources::TargetCfResources}; @@ -40,12 +40,12 @@ pub async fn destroy(args: &DestroyArgs) -> Result<()> { Box::new(auth), ); - let cloudflare_client = cloudflare::framework::async_api::Client::new( + let cloudflare_client = cloudflare::framework::client::async_api::Client::new( cloudflare::framework::auth::Credentials::UserAuthKey { email: args.email.clone(), key: args.api_key.clone(), }, - cloudflare::framework::HttpApiClientConfig::default(), + cloudflare::framework::client::ClientConfig::default(), cloudflare::framework::Environment::Production, ) .expect("Cloudflare API Client to have been created"); @@ -73,7 +73,7 @@ pub async fn destroy(args: &DestroyArgs) -> Result<()> { pub async fn destroy_from_cloudflare( resources: &TargetCfResources, api: &impl CloudflareApi, - cloudflare_client: &cloudflare::framework::async_api::Client, + cloudflare_client: &cloudflare::framework::client::async_api::Client, notifier: &impl DeployNotifier, ) -> Result<()> { // 1) Check which resources actually exist and need removal diff --git a/linkup-cli/src/commands/deploy/mod.rs b/linkup-cli/src/commands/deploy/mod.rs index 5546aa0b..c28d7414 100644 --- a/linkup-cli/src/commands/deploy/mod.rs +++ b/linkup-cli/src/commands/deploy/mod.rs @@ -5,5 +5,23 @@ mod cf_destroy; mod console_notify; mod resources; -pub use cf_deploy::{deploy, DeployArgs, DeployError}; -pub use cf_destroy::{destroy, DestroyArgs}; +pub use cf_deploy::{DeployArgs, DeployError, deploy}; +pub use cf_destroy::{DestroyArgs, destroy}; + +// TODO(augustoccesar)[2026-04-13]: This function is duplicated on workers/src/tunnel.rs. +// We can probably find a place to unify them. +pub async fn tunnel_prefix( + client: &cloudflare::framework::client::async_api::Client, + zone_id: &str, +) -> Result { + let req = cloudflare::endpoints::zones::zone::ZoneDetails { + identifier: zone_id, + }; + + let zone = client.request(&req).await?; + + let zone_name = zone.result.name.replace(".", "-"); + let tunnel_name = format!("linkup-tunnel-{}-", zone_name); + + Ok(tunnel_name) +} diff --git a/linkup-cli/src/commands/deploy/resources.rs b/linkup-cli/src/commands/deploy/resources.rs index 0aa3c87c..ac3cdc01 100644 --- a/linkup-cli/src/commands/deploy/resources.rs +++ b/linkup-cli/src/commands/deploy/resources.rs @@ -5,7 +5,7 @@ use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use super::{api::CloudflareApi, cf_deploy::DeployNotifier, DeployError}; +use super::{DeployError, api::CloudflareApi, cf_deploy::DeployNotifier}; const LINKUP_WORKER_SHIM: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shim.mjs")); const LINKUP_WORKER_INDEX_WASM: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/index.wasm")); @@ -238,7 +238,7 @@ impl TargetCfResources { pub async fn check_deploy_plan( &self, api: &impl CloudflareApi, - cloudflare_client: &cloudflare::framework::async_api::Client, + cloudflare_client: &cloudflare::framework::client::async_api::Client, ) -> Result { println!("Checking account token."); let account_token_action = self.check_account_token(api).await?; @@ -300,7 +300,7 @@ impl TargetCfResources { pub async fn check_worker_script( &self, api: &impl CloudflareApi, - cloudflare_client: &cloudflare::framework::async_api::Client, + cloudflare_client: &cloudflare::framework::client::async_api::Client, account_token_plan: &Option, ) -> Result, DeployError> { let script_name = &self.worker_script_name; @@ -517,7 +517,7 @@ impl TargetCfResources { /// on every deploy, so we use this to check if one already exists. pub async fn check_worker_token( &self, - client: &cloudflare::framework::async_api::Client, + client: &cloudflare::framework::client::async_api::Client, ) -> Result, DeployError> { let req = cloudflare::endpoints::workers::ListBindings { account_id: &self.account_id, @@ -527,7 +527,7 @@ impl TargetCfResources { let bindings = match client.request(&req).await { Ok(response) => response.result, Err(cloudflare::framework::response::ApiFailure::Error(StatusCode::NOT_FOUND, _)) => { - return Ok(None) + return Ok(None); } Err(error) => return Err(DeployError::from(error)), }; @@ -536,10 +536,10 @@ impl TargetCfResources { use cloudflare::endpoints::workers::WorkersBinding; // NOTE(augustoccesar)[2025-02-26]: We are saving WORKER_TOKEN as plain text, so we don't need other binding types - if let WorkersBinding::PlainText { name, text } = binding { - if name == "WORKER_TOKEN" { - return Ok(Some(text)); - } + if let WorkersBinding::PlainText { name, text } = binding + && name == "WORKER_TOKEN" + { + return Ok(Some(text)); } } @@ -548,7 +548,7 @@ impl TargetCfResources { pub async fn check_worker_schedules( &self, - client: &cloudflare::framework::async_api::Client, + client: &cloudflare::framework::client::async_api::Client, ) -> Result, DeployError> { let req = cloudflare::endpoints::workers::ListSchedules { account_identifier: &self.account_id, @@ -596,7 +596,7 @@ impl TargetCfResources { pub async fn execute_deploy_plan( &self, api: &impl CloudflareApi, - client: &cloudflare::framework::async_api::Client, + client: &cloudflare::framework::client::async_api::Client, plan: &DeployPlan, notifier: &impl DeployNotifier, ) -> Result<(), DeployError> { @@ -642,25 +642,21 @@ impl TargetCfResources { name, namespace_id, } = binding + && *name == kv_namespace.binding { - if *name == kv_namespace.binding { - *namespace_id = kv_ns_id.clone(); - break; - } + *namespace_id = kv_ns_id.clone(); + break; } } } if let Some(token) = token { for binding in final_metadata.bindings.iter_mut() { - if let cloudflare::endpoints::workers::WorkersBinding::SecretText { - name, - text, - } = binding + if let cloudflare::endpoints::workers::WorkersBinding::SecretText { name, text } = + binding + && *name == "CLOUDFLARE_API_TOKEN" { - if *name == "CLOUDFLARE_API_TOKEN" { - *text = Some(token.clone()); - } + *text = Some(token.clone()); } } } @@ -772,7 +768,7 @@ impl TargetCfResources { let schedules = schedules.clone(); - let req = cloudflare::endpoints::workers::UpsertSchedules { + let req = cloudflare::endpoints::workers::UpdateSchedules { account_identifier: &self.account_id, script_name: &self.worker_script_name, schedules, @@ -863,7 +859,7 @@ impl TargetCfResources { pub async fn execute_destroy_plan( &self, api: &impl CloudflareApi, - cloudflare_client: &cloudflare::framework::async_api::Client, + cloudflare_client: &cloudflare::framework::client::async_api::Client, plan: &DestroyPlan, notifier: &impl DeployNotifier, ) -> Result<(), DeployError> { @@ -924,23 +920,23 @@ impl TargetCfResources { } // Cleanup all the tunnels - let tunnel_prefix = - cloudflare::linkup::tunnel_prefix(cloudflare_client, &self.tunnel_zone_id).await?; - let req = cloudflare::endpoints::cfd_tunnel::list_tunnels::ListTunnels { - account_identifier: &self.account_id, - params: cloudflare::endpoints::cfd_tunnel::list_tunnels::Params { - is_deleted: Some(false), - include_prefix: Some(tunnel_prefix.clone()), - pagination_params: Some( - // TODO(augustoccesar)[2025-03-05]: Implement pagination - cloudflare::endpoints::cfd_tunnel::list_tunnels::PaginationParams { - page: 1, - per_page: 1000, - }, - ), - ..Default::default() - }, - }; + let tunnel_prefix = super::tunnel_prefix(cloudflare_client, &self.tunnel_zone_id).await?; + let req: cloudflare::endpoints::cfd_tunnel::list_tunnels::ListTunnels<'_> = + cloudflare::endpoints::cfd_tunnel::list_tunnels::ListTunnels { + account_identifier: &self.account_id, + params: cloudflare::endpoints::cfd_tunnel::list_tunnels::Params { + is_deleted: Some(false), + include_prefix: Some(tunnel_prefix.clone()), + pagination_params: Some( + // TODO(augustoccesar)[2025-03-05]: Implement pagination + cloudflare::endpoints::cfd_tunnel::list_tunnels::PaginationParams { + page: 1, + per_page: 1000, + }, + ), + ..Default::default() + }, + }; match cloudflare_client.request(&req).await { Ok(res) => { @@ -978,11 +974,11 @@ impl TargetCfResources { } // Cleanup all tunnels DNS records - let list_tunnel_dns_req = cloudflare::endpoints::dns::ListDnsRecords { + let list_tunnel_dns_req = cloudflare::endpoints::dns::dns::ListDnsRecords { zone_identifier: &self.tunnel_zone_id, - params: cloudflare::endpoints::dns::ListDnsRecordsParams { - name: Some(cloudflare::endpoints::dns::ListDnsRecordsParamsName { - startswith: Some(tunnel_prefix), + params: cloudflare::endpoints::dns::dns::ListDnsRecordsParams { + name: Some(cloudflare::endpoints::dns::dns::ListDnsRecordsParamsName { + starts_with: Some(tunnel_prefix), ..Default::default() }), page: Some(1), @@ -1003,7 +999,7 @@ impl TargetCfResources { dns_records.iter().map(|record| record.id.clone()).collect(); for record in dns_records_to_delete { - let delete_req = cloudflare::endpoints::dns::DeleteDnsRecord { + let delete_req = cloudflare::endpoints::dns::dns::DeleteDnsRecord { zone_identifier: &self.tunnel_zone_id, identifier: &record, }; diff --git a/linkup-cli/src/commands/health.rs b/linkup-cli/src/commands/health.rs index 6a625d11..ae56827d 100644 --- a/linkup-cli/src/commands/health.rs +++ b/linkup-cli/src/commands/health.rs @@ -9,10 +9,9 @@ use std::{ }; use crate::{ - linkup_dir_path, - local_config::LocalState, - services::{self, find_service_pid, BackgroundService}, - Result, + Result, linkup_dir_path, + services::{self, BackgroundService}, + state::State, }; use super::local_dns; @@ -62,7 +61,7 @@ struct Session { } impl Session { - fn load(state: Option<&LocalState>) -> Self { + fn load(state: Option<&State>) -> Self { match state { Some(state) => Self { name: Some(state.linkup.session_name.clone()), @@ -90,23 +89,21 @@ struct OrphanProcess { pub struct BackgroundServices { pub linkup_server: BackgroundServiceHealth, cloudflared: BackgroundServiceHealth, - dns_server: BackgroundServiceHealth, possible_orphan_processes: Vec, } #[derive(Debug, Serialize)] pub enum BackgroundServiceHealth { - Unknown, NotInstalled, Stopped, Running(u32), } impl BackgroundServices { - pub fn load(state: Option<&LocalState>) -> Self { + pub fn load(_state: Option<&State>) -> Self { let mut managed_pids: Vec = Vec::with_capacity(4); - let linkup_server = match find_service_pid(services::LocalServer::ID) { + let linkup_server = match services::LocalServer::find_pid() { Some(pid) => { managed_pids.push(pid); @@ -116,7 +113,7 @@ impl BackgroundServices { }; let cloudflared = if services::is_cloudflared_installed() { - match find_service_pid(services::CloudflareTunnel::ID) { + match services::CloudflareTunnel::find_pid() { Some(pid) => { managed_pids.push(pid); @@ -128,33 +125,9 @@ impl BackgroundServices { BackgroundServiceHealth::NotInstalled }; - let dns_server = match find_service_pid(services::LocalDnsServer::ID) { - Some(pid) => { - managed_pids.push(pid); - - BackgroundServiceHealth::Running(pid.as_u32()) - } - None => match state { - // If there is no state, we cannot know if local-dns is installed since we depend on - // the domains listed on it. - Some(state) => { - if local_dns::is_installed(&crate::local_config::managed_domains( - Some(state), - &None, - )) { - BackgroundServiceHealth::Stopped - } else { - BackgroundServiceHealth::NotInstalled - } - } - None => BackgroundServiceHealth::Unknown, - }, - }; - Self { linkup_server, cloudflared, - dns_server, possible_orphan_processes: find_potential_orphan_processes(managed_pids), } } @@ -174,10 +147,10 @@ fn find_potential_orphan_processes(managed_pids: Vec) -> Vec) -> Result { + fn load(state: Option<&State>) -> Result { // If there is no state, we cannot know if local-dns is installed since we depend on // the domains listed on it. let is_installed = state.as_ref().map(|state| { - local_dns::is_installed(&crate::local_config::managed_domains(Some(state), &None)) + local_dns::is_installed(&crate::state::managed_domains(Some(state), &None)) }); Ok(Self { @@ -309,7 +282,7 @@ struct Health { impl Health { pub fn load() -> Result { - let state = LocalState::load().ok(); + let state = State::load().ok(); let session = Session::load(state.as_ref()); Ok(Self { @@ -360,15 +333,6 @@ impl Display for Health { BackgroundServiceHealth::NotInstalled => writeln!(f, "{}", "NOT INSTALLED".yellow())?, BackgroundServiceHealth::Stopped => writeln!(f, "{}", "NOT RUNNING".yellow())?, BackgroundServiceHealth::Running(pid) => writeln!(f, "{} ({})", "RUNNING".blue(), pid)?, - BackgroundServiceHealth::Unknown => writeln!(f, "{}", "UNKNOWN".yellow())?, - } - - write!(f, " - DNS Server ")?; - match &self.background_services.dns_server { - BackgroundServiceHealth::NotInstalled => writeln!(f, "{}", "NOT INSTALLED".yellow())?, - BackgroundServiceHealth::Stopped => writeln!(f, "{}", "NOT RUNNING".yellow())?, - BackgroundServiceHealth::Running(pid) => writeln!(f, "{} ({})", "RUNNING".blue(), pid)?, - BackgroundServiceHealth::Unknown => writeln!(f, "{}", "UNKNOWN".yellow())?, } write!(f, " - Cloudflared ")?; @@ -376,7 +340,6 @@ impl Display for Health { BackgroundServiceHealth::NotInstalled => writeln!(f, "{}", "NOT INSTALLED".yellow())?, BackgroundServiceHealth::Stopped => writeln!(f, "{}", "NOT RUNNING".yellow())?, BackgroundServiceHealth::Running(pid) => writeln!(f, "{} ({})", "RUNNING".blue(), pid)?, - BackgroundServiceHealth::Unknown => writeln!(f, "{}", "UNKNOWN".yellow())?, } writeln!(f, "{}", "Linkup:".bold().italic())?; diff --git a/linkup-cli/src/commands/local.rs b/linkup-cli/src/commands/local.rs index f994f1d6..5c6e3b8d 100644 --- a/linkup-cli/src/commands/local.rs +++ b/linkup-cli/src/commands/local.rs @@ -2,9 +2,9 @@ use anyhow::anyhow; use colored::Colorize; use crate::{ - local_config::{upload_state, LocalState, ServiceTarget}, - services::{self, find_service_pid, BackgroundService}, Result, + services::{self, BackgroundService}, + state::{ServiceTarget, State, upload_state}, }; #[derive(clap::Args)] @@ -25,7 +25,7 @@ pub async fn local(args: &Args) -> Result<()> { return Err(anyhow!("No service names provided")); } - if !LocalState::exists() { + if !State::exists() { println!( "{}", "Seems like you don't have any state yet to point to local.".yellow() @@ -35,7 +35,7 @@ pub async fn local(args: &Args) -> Result<()> { return Ok(()); } - if find_service_pid(services::LocalServer::ID).is_none() { + if services::LocalServer::find_pid().is_none() { println!( "{}", "Seems like your local Linkup server is not running. Please run 'linkup start' first." @@ -45,7 +45,7 @@ pub async fn local(args: &Args) -> Result<()> { return Ok(()); } - let mut state = LocalState::load()?; + let mut state = State::load()?; if args.all { for service in state.services.iter_mut() { @@ -56,7 +56,7 @@ pub async fn local(args: &Args) -> Result<()> { let service = state .services .iter_mut() - .find(|s| s.name.as_str() == service_name) + .find(|s| s.config.name.as_str() == service_name) .ok_or_else(|| anyhow!("Service with name '{}' does not exist", service_name))?; service.current = ServiceTarget::Local; diff --git a/linkup-cli/src/commands/local_dns.rs b/linkup-cli/src/commands/local_dns.rs index 0a1560aa..5308b30b 100644 --- a/linkup-cli/src/commands/local_dns.rs +++ b/linkup-cli/src/commands/local_dns.rs @@ -4,11 +4,11 @@ use std::{ }; use crate::{ - commands, is_sudo, linkup_certs_dir_path, - local_config::{self, managed_domains, top_level_domains, LocalState}, - sudo_su, Result, + Result, commands, is_sudo, linkup_certs_dir_path, + state::{self, State, managed_domains, top_level_domains}, + sudo_su, }; -use anyhow::{anyhow, Context}; +use anyhow::{Context, anyhow}; use clap::Subcommand; use linkup_local_server::certificates::{ setup_self_signed_certificates, uninstall_self_signed_certificates, @@ -50,7 +50,7 @@ pub async fn install(config_arg: &Option) -> Result<()> { ensure_resolver_dir()?; - let domains = managed_domains(LocalState::load().ok().as_ref(), config_arg); + let domains = managed_domains(State::load().ok().as_ref(), config_arg); install_resolvers(&top_level_domains(&domains))?; @@ -76,9 +76,10 @@ pub async fn uninstall(config_arg: &Option) -> Result<()> { commands::stop(&commands::StopArgs {}, false)?; - let managed_top_level_domains = local_config::top_level_domains( - &local_config::managed_domains(LocalState::load().ok().as_ref(), config_arg), - ); + let managed_top_level_domains = state::top_level_domains(&state::managed_domains( + State::load().ok().as_ref(), + config_arg, + )); uninstall_resolvers(&managed_top_level_domains)?; uninstall_self_signed_certificates(&linkup_certs_dir_path()) diff --git a/linkup-cli/src/commands/mod.rs b/linkup-cli/src/commands/mod.rs index 808ab18f..266ff3a3 100644 --- a/linkup-cli/src/commands/mod.rs +++ b/linkup-cli/src/commands/mod.rs @@ -5,7 +5,6 @@ pub mod local; pub mod local_dns; pub mod preview; pub mod remote; -pub mod reset; pub mod server; pub mod start; pub mod status; @@ -13,18 +12,17 @@ pub mod stop; pub mod uninstall; pub mod update; -pub use {completion::completion, completion::Args as CompletionArgs}; -pub use {deploy::deploy, deploy::DeployArgs}; -pub use {deploy::destroy, deploy::DestroyArgs}; -pub use {health::health, health::Args as HealthArgs}; -pub use {local::local, local::Args as LocalArgs}; -pub use {local_dns::local_dns, local_dns::Args as LocalDnsArgs}; -pub use {preview::preview, preview::Args as PreviewArgs}; -pub use {remote::remote, remote::Args as RemoteArgs}; -pub use {reset::reset, reset::Args as ResetArgs}; -pub use {server::server, server::Args as ServerArgs}; -pub use {start::start, start::Args as StartArgs}; -pub use {status::status, status::Args as StatusArgs}; -pub use {stop::stop, stop::Args as StopArgs}; -pub use {uninstall::uninstall, uninstall::Args as UninstallArgs}; -pub use {update::update, update::Args as UpdateArgs}; +pub use {completion::Args as CompletionArgs, completion::completion}; +pub use {deploy::DeployArgs, deploy::deploy}; +pub use {deploy::DestroyArgs, deploy::destroy}; +pub use {health::Args as HealthArgs, health::health}; +pub use {local::Args as LocalArgs, local::local}; +pub use {local_dns::Args as LocalDnsArgs, local_dns::local_dns}; +pub use {preview::Args as PreviewArgs, preview::preview}; +pub use {remote::Args as RemoteArgs, remote::remote}; +pub use {server::Args as ServerArgs, server::server}; +pub use {start::Args as StartArgs, start::start}; +pub use {status::Args as StatusArgs, status::status}; +pub use {stop::Args as StopArgs, stop::stop}; +pub use {uninstall::Args as UninstallArgs, uninstall::uninstall}; +pub use {update::Args as UpdateArgs, update::update}; diff --git a/linkup-cli/src/commands/preview.rs b/linkup-cli/src/commands/preview.rs index b26a9d83..4436b7a7 100644 --- a/linkup-cli/src/commands/preview.rs +++ b/linkup-cli/src/commands/preview.rs @@ -1,10 +1,11 @@ -use crate::commands::status::{format_state_domains, SessionStatus}; -use crate::local_config::{config_path, get_config}; -use crate::worker_client::WorkerClient; use crate::Result; +use crate::commands::status::{SessionStatus, format_state_domains}; +use crate::state::{config_path, get_config}; +use crate::worker_client::WorkerClient; use anyhow::Context; use clap::builder::ValueParser; use linkup::CreatePreviewRequest; +use url::Url; #[derive(clap::Args)] pub struct Args { @@ -14,7 +15,7 @@ pub struct Args { required = true, num_args = 1.., )] - services: Vec<(String, String)>, + services: Vec<(String, Url)>, #[arg(long, help = "Print the request body instead of sending it.")] print_request: bool, @@ -24,7 +25,7 @@ pub async fn preview(args: &Args, config: &Option) -> Result<()> { let config_path = config_path(config)?; let input_config = get_config(&config_path)?; let create_preview_request: CreatePreviewRequest = - input_config.create_preview_request(&args.services); + linkup::create_preview_req_from_config(&input_config, &args.services); let url = input_config.linkup.worker_url.clone(); if args.print_request { diff --git a/linkup-cli/src/commands/remote.rs b/linkup-cli/src/commands/remote.rs index 2fd638f6..196db287 100644 --- a/linkup-cli/src/commands/remote.rs +++ b/linkup-cli/src/commands/remote.rs @@ -1,7 +1,7 @@ use crate::{ - local_config::{upload_state, LocalState, ServiceTarget}, - services::{self, find_service_pid, BackgroundService}, Result, + services::{self, BackgroundService}, + state::{ServiceTarget, State, upload_state}, }; use anyhow::anyhow; @@ -25,7 +25,7 @@ pub async fn remote(args: &Args) -> Result<()> { return Err(anyhow!("No service names provided")); } - if !LocalState::exists() { + if !State::exists() { println!( "{}", "Seems like you don't have any state yet to point to remote.".yellow() @@ -35,9 +35,9 @@ pub async fn remote(args: &Args) -> Result<()> { return Ok(()); } - let mut state = LocalState::load()?; + let mut state = State::load()?; - if find_service_pid(services::LocalServer::ID).is_none() { + if services::LocalServer::find_pid().is_none() { println!( "{}", "Seems like your local Linkup server is not running. Please run 'linkup start' first." @@ -56,7 +56,7 @@ pub async fn remote(args: &Args) -> Result<()> { let service = state .services .iter_mut() - .find(|s| s.name.as_str() == service_name) + .find(|s| s.config.name.as_str() == service_name) .ok_or_else(|| anyhow!("Service with name '{}' does not exist", service_name))?; service.current = ServiceTarget::Remote; diff --git a/linkup-cli/src/commands/reset.rs b/linkup-cli/src/commands/reset.rs deleted file mode 100644 index b700d9cf..00000000 --- a/linkup-cli/src/commands/reset.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::{commands, local_config::LocalState, Result}; - -#[derive(clap::Args)] -pub struct Args {} - -pub async fn reset(_args: &Args) -> Result<()> { - let _ = LocalState::load()?; - - commands::stop(&commands::StopArgs {}, false)?; - commands::start(&commands::StartArgs { no_tunnel: false }, false, &None).await?; - - Ok(()) -} diff --git a/linkup-cli/src/commands/server.rs b/linkup-cli/src/commands/server.rs index fa488143..4b1e260b 100644 --- a/linkup-cli/src/commands/server.rs +++ b/linkup-cli/src/commands/server.rs @@ -1,78 +1,19 @@ +use std::path::PathBuf; + use crate::Result; use linkup::MemoryStringStore; -use tokio::select; #[derive(clap::Args)] pub struct Args { - #[command(subcommand)] - server_kind: ServerKind, -} - -#[derive(clap::Subcommand)] -pub enum ServerKind { - LocalWorker { - #[arg(long)] - certs_dir: String, - }, - - Dns { - #[arg(long)] - session_name: String, - #[arg(long, value_parser, num_args = 1.., value_delimiter = ',')] - domains: Vec, - }, + #[arg(long)] + certs_dir: String, } pub async fn server(args: &Args) -> Result<()> { - match &args.server_kind { - ServerKind::LocalWorker { certs_dir } => { - let config_store = MemoryStringStore::default(); - - let http_config_store = config_store.clone(); - let handler_http = tokio::spawn(async move { - linkup_local_server::start_server_http(http_config_store) - .await - .unwrap(); - }); - - let handler_https = { - use std::path::PathBuf; - - let https_config_store = config_store.clone(); - let https_certs_dir = PathBuf::from(certs_dir); - - Some(tokio::spawn(async move { - linkup_local_server::start_server_https(https_config_store, &https_certs_dir) - .await; - })) - }; - - match handler_https { - Some(handler_https) => { - select! { - _ = handler_http => (), - _ = handler_https => (), - } - } - None => { - handler_http.await.unwrap(); - } - } - } - ServerKind::Dns { - session_name, - domains, - } => { - let session_name = session_name.clone(); - let domains = domains.clone(); - - let handler_dns = tokio::spawn(async move { - linkup_local_server::start_dns_server(session_name, domains).await; - }); + let config_store = MemoryStringStore::default(); + let https_certs_dir = PathBuf::from(&args.certs_dir); - handler_dns.await.unwrap(); - } - } + linkup_local_server::start(config_store, &https_certs_dir).await; Ok(()) } diff --git a/linkup-cli/src/commands/start.rs b/linkup-cli/src/commands/start.rs index 6ba8347e..6cf3bada 100644 --- a/linkup-cli/src/commands/start.rs +++ b/linkup-cli/src/commands/start.rs @@ -1,70 +1,44 @@ use std::{ - collections::HashMap, fs, - io::stdout, path::{Path, PathBuf}, - sync, - thread::{self, sleep, JoinHandle}, - time::Duration, }; -use anyhow::{anyhow, Context, Error}; +use anyhow::{Context, Error, anyhow}; use colored::Colorize; -use crossterm::{cursor, ExecutableCommand}; +use indicatif::{MultiProgress, ProgressBar}; +use crate::{Result, state::State}; use crate::{ - commands::status::{format_state_domains, SessionStatus}, + commands::status::{SessionStatus, format_state_domains}, env_files::write_to_env_file, - local_config::{config_path, config_to_state, get_config}, services::{self, BackgroundService}, + state::{config_path, config_to_state, get_config}, }; -use crate::{local_config::LocalState, Result}; - -const LOADING_CHARS: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; #[derive(clap::Args)] -pub struct Args { - #[clap( - short, - long, - help = "Start linkup in partial mode without a tunnel. Not all requests will succeed." - )] - pub no_tunnel: bool, -} - -pub async fn start(args: &Args, fresh_state: bool, config_arg: &Option) -> Result<()> { - let mut state = if fresh_state { - let state = load_and_save_state(config_arg, args.no_tunnel)?; - set_linkup_env(state.clone())?; - - state - } else { - LocalState::load()? - }; +pub struct Args {} - let status_update_channel = sync::mpsc::channel::(); +pub async fn start(_args: &Args, config_arg: &Option) -> Result<()> { + let mut state = load_and_save_state(config_arg)?; + set_linkup_env(state.clone())?; let local_server = services::LocalServer::new(); let cloudflare_tunnel = services::CloudflareTunnel::new(); - let local_dns_server = services::LocalDnsServer::new(); - let mut display_thread: Option> = None; - let display_channel = sync::mpsc::channel::(); + let multi_progress = MultiProgress::new(); - // If we are doing RUST_LOG=debug to debug if there is anything wrong, having the display thread make so it - // overwrites some of the output since it does some cursor moving. - // So in that case, we do not start the display thread. - if !log::log_enabled!(log::Level::Debug) { - display_thread = Some(spawn_display_thread( - &[ - services::LocalServer::NAME, - services::CloudflareTunnel::NAME, - services::LocalDnsServer::NAME, - ], - status_update_channel.1, - display_channel.1, - )); - } + multi_progress + .println("Background services:") + .expect("printing should not fail"); + multi_progress + .println(format!("{:<20} {:<10}", "NAME".bold(), "STATUS".bold())) + .expect("printing should not fail"); + + let local_server_progress = multi_progress.add(ProgressBar::new_spinner()); + local_server.prepare_progress_bar(&local_server_progress); + + let cloudflare_tunnel_progress = multi_progress.add(ProgressBar::new_spinner()); + cloudflare_tunnel.prepare_progress_bar(&cloudflare_tunnel_progress); // To make sure that we get the last update to the display thread before the error is bubbled up, // we store any error that might happen on one of the steps and only return it after we have @@ -72,7 +46,7 @@ pub async fn start(args: &Args, fresh_state: bool, config_arg: &Option) let mut exit_error: Option = None; match local_server - .run_with_progress(&mut state, status_update_channel.0.clone()) + .run_with_progress(&mut state, &local_server_progress) .await { Ok(_) => (), @@ -81,7 +55,7 @@ pub async fn start(args: &Args, fresh_state: bool, config_arg: &Option) if exit_error.is_none() { match cloudflare_tunnel - .run_with_progress(&mut state, status_update_channel.0.clone()) + .run_with_progress(&mut state, &cloudflare_tunnel_progress) .await { Ok(_) => (), @@ -89,20 +63,8 @@ pub async fn start(args: &Args, fresh_state: bool, config_arg: &Option) } } - if exit_error.is_none() { - match local_dns_server - .run_with_progress(&mut state, status_update_channel.0.clone()) - .await - { - Ok(_) => (), - Err(err) => exit_error = Some(err), - } - } - - if let Some(display_thread) = display_thread { - display_channel.0.send(true).unwrap(); - display_thread.join().unwrap(); - } + local_server_progress.finish(); + cloudflare_tunnel_progress.finish(); if let Some(exit_error) = exit_error { return Err(exit_error).context("Failed to start CLI"); @@ -113,139 +75,36 @@ pub async fn start(args: &Args, fresh_state: bool, config_arg: &Option) domains: format_state_domains(&state.linkup.session_name, &state.domains), }; - println!(); + print!("\n\n"); status.print(); Ok(()) } -/// This spawns a background thread that is responsible for updating the terminal with the information -/// about the start of the services. -/// -/// # Arguments -/// * `names` - These are the names of the services that are going to be displayed here. These is also -/// the "keys" that the status receiver will listen to for updating. -/// -/// * `status_update_receiver` - This is a [`sync::mpsc::Receiver`] on which this thread will listen -/// for updates. -/// -/// * `exit_signal_receiver` - This is also a [`sync::mpsc::Receiver`], where, to make sure that we always -/// show the last update, the exit of the display thread is done by receiving any message on this receiver. -fn spawn_display_thread( - names: &[&str], - status_update_receiver: sync::mpsc::Receiver, - exit_signal_receiver: sync::mpsc::Receiver, -) -> thread::JoinHandle<()> { - let rows = names.len(); - - println!("Background services:"); - println!("{:<20} {:<10}", "NAME".bold(), "STATUS".bold()); - - let names: Vec = names.iter().map(|name| String::from(*name)).collect(); - thread::spawn(move || { - std::io::stdout().execute(cursor::Hide).unwrap(); - let mut loop_iter = 0; - let mut statuses = HashMap::::new(); - - loop { - if loop_iter == 0 { - // For the first loop, make sure we add all the services with a pending status. - for name in &names { - statuses.insert( - name.clone(), - services::RunUpdate { - id: name.clone(), - status: services::RunStatus::Pending, - details: None, - }, - ); - } - } else { - crossterm::execute!(std::io::stdout(), cursor::MoveUp(rows as u16)).unwrap(); - } - - for name in &names { - let latest_update = statuses.get(name).unwrap(); - let mut formatted_status = match &latest_update.status { - services::RunStatus::Starting => { - LOADING_CHARS[loop_iter % LOADING_CHARS.len()].to_string() - } - status => status.to_string(), - }; - - if let Some(details) = &latest_update.details { - formatted_status.push_str(&format!(" ({})", details)); - } - - let colored_status = match latest_update.status { - services::RunStatus::Started => formatted_status.blue(), - services::RunStatus::Error => formatted_status.yellow(), - _ => formatted_status.normal(), - }; - - // This is necessary in case the previous update was a longer line - // than the one that is going to be shown now. - stdout() - .execute(crossterm::terminal::Clear( - crossterm::terminal::ClearType::CurrentLine, - )) - .unwrap(); - - println!("{:<20} {:<10}", name, colored_status) - } - - match &status_update_receiver.try_recv() { - Ok(status_update) => { - statuses.insert(status_update.id.clone(), status_update.clone()); - } - Err(std::sync::mpsc::TryRecvError::Disconnected) => break, - Err(std::sync::mpsc::TryRecvError::Empty) => { - // To make sure we exit on the right order, only check for the exit signal in case - // we are not receiving more updates on the `status_update_receiver`. - match exit_signal_receiver.try_recv() { - Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => break, - _ => (), - } - } - } - - loop_iter += 1; - sleep(Duration::from_millis(50)); - } - - std::io::stdout().execute(cursor::Show).unwrap(); - }) -} - -fn set_linkup_env(state: LocalState) -> Result<()> { +fn set_linkup_env(state: State) -> Result<()> { // Set env vars to linkup for service in &state.services { - if let Some(d) = &service.directory { + if let Some(d) = &service.config.directory { set_service_env(d.clone(), state.linkup.config_path.clone())? } } Ok(()) } -fn load_and_save_state(config_arg: &Option, no_tunnel: bool) -> Result { - let previous_state = LocalState::load(); +fn load_and_save_state(config_arg: &Option) -> Result { let config_path = config_path(config_arg)?; let input_config = get_config(&config_path)?; - let mut state = config_to_state(input_config.clone(), config_path, no_tunnel); - - // Reuse previous session name if possible - if let Ok(ps) = previous_state { - state.linkup.session_name = ps.linkup.session_name; - state.linkup.session_token = ps.linkup.session_token; + let mut state = config_to_state(input_config.clone(), config_path); - // Maintain tunnel state until it is rewritten - if !no_tunnel && ps.linkup.tunnel.is_some() { - state.linkup.tunnel = ps.linkup.tunnel; - } + if let Ok(previous_state) = State::load() { + state.linkup.session_name = previous_state.linkup.session_name; + state.linkup.session_token = previous_state.linkup.session_token; + state.linkup.tunnel = previous_state.linkup.tunnel; } state.save()?; + Ok(state) } diff --git a/linkup-cli/src/commands/status.rs b/linkup-cli/src/commands/status.rs index 7b22a5ac..c55085a7 100644 --- a/linkup-cli/src/commands/status.rs +++ b/linkup-cli/src/commands/status.rs @@ -1,47 +1,24 @@ use anyhow::Context; use colored::{ColoredString, Colorize}; -use crossterm::{cursor, execute, style::Print, terminal}; -use linkup::{get_additional_headers, HeaderMap, StorableDomain, TargetService}; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; +use linkup::{Domain, HeaderMap, TargetService, config::HealthConfig, get_additional_headers}; use serde::{Deserialize, Serialize}; -use std::{ - io::stdout, - ops::Deref, - sync::mpsc::Receiver, - thread::{self, sleep}, - time::Duration, -}; +use std::{collections::HashMap, sync::mpsc::Receiver, thread, time::Duration}; use crate::{ - commands, - local_config::{HealthConfig, LocalService, LocalState, ServiceTarget}, - services, + commands, services, + state::{LocalService, ServiceTarget, State}, }; -const LOADING_CHARS: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; -const MIN_WIDTH_FOR_LOCATION: usize = 110; -const MIN_WIDTH_FOR_KIND: usize = 50; - #[derive(clap::Args)] pub struct Args { // Output status in JSON format #[arg(long)] pub json: bool, - - #[arg(short, long)] - all: bool, } pub fn status(args: &Args) -> anyhow::Result<()> { - // TODO(augustocesar)[2024-10-28]: Remove --all/-a in a future release. - // Do not print the warning in case of JSON so it doesn't break any usage if the result of the command - // is passed on to somewhere else. - if args.all && !args.json { - let warning = "--all/-a is a noop now. All services statuses will always be shown. \ - This arg will be removed in a future release.\n"; - println!("{}", warning.yellow()); - } - - if !LocalState::exists() { + if !State::exists() { println!( "{}", "Seems like you don't have any state yet, so there is no status to report.".yellow() @@ -51,7 +28,7 @@ pub fn status(args: &Args) -> anyhow::Result<()> { return Ok(()); } - let state = LocalState::load().context("Failed to load local state")?; + let state = State::load().context("Failed to load local state")?; let linkup_services = linkup_services(&state); let all_services = state.clone().services.into_iter().chain(linkup_services); @@ -96,65 +73,62 @@ pub fn status(args: &Args) -> anyhow::Result<()> { _ => println!("{}", "Linkup is not currently running.\n".yellow()), } - let mut stdout = stdout(); - - execute!(stdout, cursor::Hide, terminal::DisableLineWrap)?; - - ctrlc::set_handler(move || { - execute!(std::io::stdout(), cursor::Show, terminal::EnableLineWrap).unwrap(); - std::process::exit(130); - }) - .expect("Failed to set CTRL+C handler"); + let multi_progress = MultiProgress::new(); + + multi_progress + .println(format!( + "{:<22} {:<16} {:<8} {}", + "SERVICE NAME".bold(), + "COMPONENT KIND".bold(), + "STATUS".bold(), + "LOCATION".bold(), + )) + .expect("printing should not fail"); + + let mut services_progress_bars: HashMap = HashMap::new(); + + let in_progress_style = ProgressStyle::with_template("{prefix} {spinner:<8.white} {msg:!}") + .unwrap() + .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"); + + let done_style = ProgressStyle::with_template("{prefix} {msg}").unwrap(); + + for service in &status.services { + let progress_bar = multi_progress.add(ProgressBar::new_spinner()); + progress_bar.set_style(in_progress_style.clone()); + progress_bar.set_prefix(format!( + "{:<22} {:<16}", + service.name, service.component_kind + )); + progress_bar.set_message(service.service.current_url().to_string()); + progress_bar.enable_steady_tick(Duration::from_millis(50)); + + services_progress_bars.insert(service.name.clone(), progress_bar); + } - let mut iteration = 0; - let mut loading_char_iteration = 0; let mut updated_services = 0; - loop { - while let Some((name, server_status)) = status_receiver.try_iter().next() { - for service_status in status.services.iter_mut() { - if service_status.name == name { - service_status.status = server_status.clone(); - updated_services += 1; - } - } - } - // It has to print the services statuses at least once before we can move the cursor - // to the start of the stuses section. - if iteration > 0 { - // +1 to include the header since it is also dynamic based on the width of the terminal. - execute!(stdout, cursor::MoveUp((status.services.len() + 1) as u16))?; - } - - let (terminal_width, _) = terminal::size().unwrap(); + for (name, server_status) in status_receiver.iter() { + for service_status in status.services.iter_mut() { + if service_status.name == name { + service_status.status = server_status.clone(); - execute!( - stdout, - terminal::Clear(terminal::ClearType::CurrentLine), - Print(table_header(terminal_width)) - )?; + if let Some(pb) = services_progress_bars.get(&name) { + let status_text = format!("{:<8}", server_status.colored()); + let location = service_status.service.current_url().to_string(); - for i in 0..status.services.len() { - let status = &status.services[i]; + pb.set_style(done_style.clone()); + pb.finish_with_message(format!("{} {}", status_text, location)); + } - execute!( - stdout, - terminal::Clear(terminal::ClearType::CurrentLine), - Print(status.as_table_row(loading_char_iteration, terminal_width)) - )?; + updated_services += 1; + } } if updated_services == status.services.len() { break; } - - loading_char_iteration = (iteration + 1) % LOADING_CHARS.len(); - iteration += 1; - - sleep(Duration::from_millis(50)); } - - execute!(stdout, cursor::Show, terminal::EnableLineWrap).unwrap(); } Ok(()) @@ -191,44 +165,6 @@ struct ServiceStatus { priority: i8, } -impl ServiceStatus { - pub fn as_table_row(&self, loading_iter: usize, terminal_width: u16) -> String { - let terminal_width = terminal_width as usize; - - let display_status = match &self.status { - ServerStatus::Loading => LOADING_CHARS[loading_iter].to_string().normal(), - status => status.colored(), - }; - - let mut status_name = ColoredString::from(self.name.clone()); - let mut status_component_kind = ColoredString::from(self.component_kind.clone()); - let mut status_location = ColoredString::from(self.service.current_url().to_string()); - - if status_component_kind.deref() == "local" { - status_name = status_name.bright_magenta(); - status_component_kind = status_component_kind.bright_magenta(); - status_location = status_location.bright_magenta(); - }; - - let mut output = String::with_capacity(MIN_WIDTH_FOR_LOCATION); - output.push_str(&format!("{:<22}", status_name)); - - if terminal_width > MIN_WIDTH_FOR_KIND { - output.push_str(&format!("{:<16}", status_component_kind)); - } - - output.push_str(&format!("{:<8}", display_status)); - - if terminal_width > MIN_WIDTH_FOR_LOCATION { - output.push_str(&status_location); - } - - output.push('\n'); - - output - } -} - #[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] pub enum ServerStatus { Ok, @@ -248,28 +184,7 @@ impl ServerStatus { } } -fn table_header(terminal_width: u16) -> String { - let terminal_width = terminal_width as usize; - - let mut output = String::with_capacity(110); - output.push_str(&format!("{:<22}", "SERVICE NAME")); - - if terminal_width > MIN_WIDTH_FOR_KIND { - output.push_str(&format!("{:<16}", "COMPONENT KIND")); - } - - output.push_str(&format!("{:<8}", "STATUS")); - - if terminal_width > MIN_WIDTH_FOR_LOCATION { - output.push_str("LOCATION"); - } - - output.push('\n'); - - output -} - -pub fn format_state_domains(session_name: &str, domains: &[StorableDomain]) -> Vec { +pub fn format_state_domains(session_name: &str, domains: &[Domain]) -> Vec { // Filter out domains that are subdomains of other domains let filtered_domains = domains .iter() @@ -287,45 +202,51 @@ pub fn format_state_domains(session_name: &str, domains: &[StorableDomain]) -> V .collect() } -fn linkup_services(state: &LocalState) -> Vec { +fn linkup_services(state: &State) -> Vec { let local_url = services::LocalServer::url(); vec![ LocalService { - name: "linkup_local_server".to_string(), - remote: local_url.clone(), - local: local_url.clone(), current: ServiceTarget::Local, - directory: None, - rewrites: vec![], - health: Some(HealthConfig { - path: Some("/linkup/check".to_string()), - ..Default::default() - }), + config: linkup::config::ServiceConfig { + name: "linkup_local_server".to_string(), + remote: local_url.clone(), + local: local_url.clone(), + directory: None, + rewrites: None, + health: Some(HealthConfig { + path: Some("/linkup/check".to_string()), + ..Default::default() + }), + }, }, LocalService { - name: "linkup_remote_server".to_string(), - remote: state.linkup.worker_url.clone(), - local: state.linkup.worker_url.clone(), current: ServiceTarget::Remote, - directory: None, - rewrites: vec![], - health: Some(HealthConfig { - path: Some("/linkup/check".to_string()), - ..Default::default() - }), + config: linkup::config::ServiceConfig { + name: "linkup_remote_server".to_string(), + remote: state.linkup.worker_url.clone(), + local: state.linkup.worker_url.clone(), + directory: None, + rewrites: None, + health: Some(HealthConfig { + path: Some("/linkup/check".to_string()), + ..Default::default() + }), + }, }, LocalService { - name: "tunnel".to_string(), - remote: state.get_tunnel_url(), - local: state.get_tunnel_url(), current: ServiceTarget::Remote, - directory: None, - rewrites: vec![], - health: Some(HealthConfig { - path: Some("/linkup/check".to_string()), - ..Default::default() - }), + config: linkup::config::ServiceConfig { + name: "tunnel".to_string(), + remote: state.get_tunnel_url(), + local: state.get_tunnel_url(), + directory: None, + rewrites: None, + health: Some(HealthConfig { + path: Some("/linkup/check".to_string()), + ..Default::default() + }), + }, }, ] } @@ -334,7 +255,7 @@ fn service_status(service: &LocalService, session_name: &str) -> ServerStatus { let mut acceptable_statuses_override: Option> = None; let mut url = service.current_url(); - if let Some(health_config) = &service.health { + if let Some(health_config) = &service.config.health { if let Some(path) = &health_config.path { url = url.join(path).unwrap(); } @@ -349,7 +270,7 @@ fn service_status(service: &LocalService, session_name: &str) -> ServerStatus { &HeaderMap::new(), session_name, &TargetService { - name: service.name.clone(), + name: service.config.name.clone(), url: url.to_string(), }, ); @@ -424,7 +345,7 @@ where let priority = service_priority(&service); ServiceStatus { - name: service.name.clone(), + name: service.config.name.clone(), component_kind: service.current.to_string(), status: ServerStatus::Loading, service, @@ -443,7 +364,7 @@ where thread::spawn(move || { let status = service_status(&service_clone, &session_name); - tx.send((service_clone.name.clone(), status)) + tx.send((service_clone.config.name.clone(), status)) .expect("Failed to send service status"); }); } @@ -454,15 +375,13 @@ where } fn is_internal_service(service: &LocalService) -> bool { - service.name == "linkup_local_server" - || service.name == "linkup_remote_server" - || service.name == "tunnel" + let service_name = &service.config.name; + + service_name == "linkup_local_server" + || service_name == "linkup_remote_server" + || service_name == "tunnel" } fn service_priority(service: &LocalService) -> i8 { - if is_internal_service(service) { - 1 - } else { - 2 - } + if is_internal_service(service) { 1 } else { 2 } } diff --git a/linkup-cli/src/commands/stop.rs b/linkup-cli/src/commands/stop.rs index 2bbbb06a..4d2a8e7f 100644 --- a/linkup-cli/src/commands/stop.rs +++ b/linkup-cli/src/commands/stop.rs @@ -4,25 +4,28 @@ use std::path::{Path, PathBuf}; use anyhow::Context; use crate::env_files::clear_env_file; -use crate::local_config::LocalState; -use crate::services::{stop_service, BackgroundService}; -use crate::{services, Result}; +use crate::services::BackgroundService; +use crate::state::State; +use crate::{Result, services}; #[derive(clap::Args)] pub struct Args {} pub fn stop(_args: &Args, clear_env: bool) -> Result<()> { - match (LocalState::load(), clear_env) { + match (State::load(), clear_env) { (Ok(state), true) => { // Reset env vars back to what they were before for service in &state.services { - let remove_res = match &service.directory { + let remove_res = match &service.config.directory { Some(d) => remove_service_env(d.clone(), state.linkup.config_path.clone()), None => Ok(()), }; if let Err(e) = remove_res { - println!("Could not remove env for service {}: {}", service.name, e); + println!( + "Could not remove env for service {}: {}", + service.config.name, e + ); } } } @@ -32,9 +35,8 @@ pub fn stop(_args: &Args, clear_env: bool) -> Result<()> { } } - stop_service(services::LocalServer::ID); - stop_service(services::CloudflareTunnel::ID); - stop_service(services::LocalDnsServer::ID); + services::LocalServer::stop(); + services::CloudflareTunnel::stop(); println!("Stopped linkup"); diff --git a/linkup-cli/src/commands/uninstall.rs b/linkup-cli/src/commands/uninstall.rs index d4f1b6f1..ed54d70d 100644 --- a/linkup-cli/src/commands/uninstall.rs +++ b/linkup-cli/src/commands/uninstall.rs @@ -1,8 +1,8 @@ use std::{fs, process}; use crate::{ - commands, commands::local_dns, linkup_dir_path, linkup_exe_path, local_config::managed_domains, - local_config::LocalState, prompt, InstallationMethod, Result, + InstallationMethod, Result, commands, commands::local_dns, linkup_dir_path, linkup_exe_path, + prompt, state::State, state::managed_domains, }; #[cfg(target_os = "linux")] @@ -24,10 +24,7 @@ pub async fn uninstall(_args: &Args, config_arg: &Option) -> Result<()> commands::stop(&commands::StopArgs {}, true)?; - if local_dns::is_installed(&managed_domains( - LocalState::load().ok().as_ref(), - config_arg, - )) { + if local_dns::is_installed(&managed_domains(State::load().ok().as_ref(), config_arg)) { local_dns::uninstall(config_arg).await?; } diff --git a/linkup-cli/src/commands/update.rs b/linkup-cli/src/commands/update.rs index d228c113..9b7b5a36 100644 --- a/linkup-cli/src/commands/update.rs +++ b/linkup-cli/src/commands/update.rs @@ -2,7 +2,7 @@ use anyhow::Context; #[cfg(not(target_os = "linux"))] use std::fs; -use crate::{commands, current_version, linkup_exe_path, release, InstallationMethod, Result}; +use crate::{InstallationMethod, Result, commands, current_version, linkup_exe_path, release}; #[cfg(target_os = "linux")] use crate::{is_sudo, sudo_su}; diff --git a/linkup-cli/src/env_files.rs b/linkup-cli/src/env_files.rs index 80f756ee..59925a4f 100644 --- a/linkup-cli/src/env_files.rs +++ b/linkup-cli/src/env_files.rs @@ -11,10 +11,10 @@ use crate::Result; const LINKUP_ENV_SEPARATOR: &str = "##### Linkup environment - DO NOT EDIT #####"; pub fn write_to_env_file(service: &str, dev_env_path: &PathBuf, env_path: &PathBuf) -> Result<()> { - if let Ok(env_content) = fs::read_to_string(env_path) { - if env_content.contains(LINKUP_ENV_SEPARATOR) { - return Ok(()); - } + if let Ok(env_content) = fs::read_to_string(env_path) + && env_content.contains(LINKUP_ENV_SEPARATOR) + { + return Ok(()); } let mut dev_env_content = fs::read_to_string(dev_env_path).with_context(|| { diff --git a/linkup-cli/src/main.rs b/linkup-cli/src/main.rs index 785d21d5..b82b36da 100644 --- a/linkup-cli/src/main.rs +++ b/linkup-cli/src/main.rs @@ -1,6 +1,6 @@ use std::{env, fs, io::ErrorKind, path::PathBuf}; -use anyhow::{anyhow, Context}; +use anyhow::{Context, anyhow}; use clap::{Parser, Subcommand}; use colored::Colorize; use thiserror::Error; @@ -10,9 +10,9 @@ pub use linkup::Version; mod commands; mod env_files; -mod local_config; mod release; mod services; +mod state; mod worker_client; const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -206,9 +206,6 @@ enum Commands { #[clap(about = "Stop a running linkup session")] Stop(commands::StopArgs), - #[clap(about = "Reset a linkup session")] - Reset(commands::ResetArgs), - #[clap(about = "Route session traffic to a local service")] Local(commands::LocalArgs), @@ -257,9 +254,8 @@ async fn main() -> anyhow::Result<()> { match &cli.command { Commands::Health(args) => commands::health(args), - Commands::Start(args) => commands::start(args, true, &cli.config).await, + Commands::Start(args) => commands::start(args, &cli.config).await, Commands::Stop(args) => commands::stop(args, true), - Commands::Reset(args) => commands::reset(args).await, Commands::Local(args) => commands::local(args).await, Commands::Remote(args) => commands::remote(args).await, Commands::Status(args) => commands::status(args), diff --git a/linkup-cli/src/release.rs b/linkup-cli/src/release.rs index b150837c..e2e23c03 100644 --- a/linkup-cli/src/release.rs +++ b/linkup-cli/src/release.rs @@ -4,7 +4,7 @@ mod github { use flate2::read::GzDecoder; use linkup::VersionError; use reqwest::header::HeaderValue; - use serde::{de::DeserializeOwned, Deserialize, Serialize}; + use serde::{Deserialize, Serialize, de::DeserializeOwned}; use tar::Archive; use url::Url; diff --git a/linkup-cli/src/services/cloudflare_tunnel.rs b/linkup-cli/src/services/cloudflare_tunnel.rs index 3d9ee091..b08d5503 100644 --- a/linkup-cli/src/services/cloudflare_tunnel.rs +++ b/linkup-cli/src/services/cloudflare_tunnel.rs @@ -7,15 +7,16 @@ use std::{ time::Duration, }; -use hickory_resolver::{config::ResolverOpts, proto::rr::RecordType, TokioResolver}; +use hickory_resolver::{TokioResolver, config::ResolverOpts, proto::rr::RecordType}; +use indicatif::ProgressBar; use log::debug; use serde::{Deserialize, Serialize}; use tokio::time::sleep; use url::Url; -use crate::{linkup_file_path, local_config::LocalState, worker_client::WorkerClient, Result}; +use crate::{Result, linkup_file_path, state::State, worker_client::WorkerClient}; -use super::{find_service_pid, BackgroundService, PidError}; +use super::{BackgroundService, PidError}; #[derive(thiserror::Error, Debug)] #[allow(dead_code)] @@ -129,7 +130,7 @@ impl CloudflareTunnel { false } - fn update_state(&self, tunnel_url: &Url, state: &mut LocalState) -> Result<()> { + fn update_state(&self, tunnel_url: &Url, state: &mut State) -> Result<()> { debug!("Adding tunnel url {} to the state", tunnel_url.as_str()); state.linkup.tunnel = Some(tunnel_url.clone()); @@ -145,14 +146,10 @@ impl BackgroundService for CloudflareTunnel { const ID: &str = "cloudflare-tunnel"; const NAME: &str = "Cloudflare Tunnel"; - async fn run_with_progress( - &self, - state: &mut LocalState, - status_sender: std::sync::mpsc::Sender, - ) -> Result<()> { + async fn run_with_progress(&self, state: &mut State, progress_bar: &ProgressBar) -> Result<()> { if !state.should_use_tunnel() { self.notify_update_with_details( - &status_sender, + progress_bar, super::RunStatus::Skipped, "Requested no tunnel", ); @@ -162,7 +159,7 @@ impl BackgroundService for CloudflareTunnel { if state.linkup.session_name.is_empty() { self.notify_update_with_details( - &status_sender, + progress_bar, super::RunStatus::Error, "Empty session name", ); @@ -170,9 +167,9 @@ impl BackgroundService for CloudflareTunnel { return Err(Error::InvalidSessionName(state.linkup.session_name.clone()).into()); } - if find_service_pid(Self::ID).is_some() { + if Self::find_pid().is_some() { self.notify_update_with_details( - &status_sender, + progress_bar, super::RunStatus::Started, "Was already running", ); @@ -180,7 +177,7 @@ impl BackgroundService for CloudflareTunnel { return Ok(()); } - self.notify_update(&status_sender, super::RunStatus::Starting); + self.notify_update(progress_bar, super::RunStatus::Starting); let tunnel_url = self .start( @@ -201,7 +198,7 @@ impl BackgroundService for CloudflareTunnel { pid_file_ready_attempt += 1; self.notify_update_with_details( - &status_sender, + progress_bar, super::RunStatus::Starting, format!("Waiting for tunnel... retry #{}", pid_file_ready_attempt), ); @@ -211,7 +208,7 @@ impl BackgroundService for CloudflareTunnel { if !pid_file_exists { self.notify_update_with_details( - &status_sender, + progress_bar, super::RunStatus::Error, "Failed to start tunnel", ); @@ -219,7 +216,7 @@ impl BackgroundService for CloudflareTunnel { return Err(Error::PidfileNotFound.into()); } - self.notify_update(&status_sender, super::RunStatus::Starting); + self.notify_update(progress_bar, super::RunStatus::Starting); } // DNS Propagation check @@ -232,7 +229,7 @@ impl BackgroundService for CloudflareTunnel { dns_propagation_attempt += 1; self.notify_update_with_details( - &status_sender, + progress_bar, super::RunStatus::Starting, format!( "Waiting for tunnel DNS to propagate... retry #{}", @@ -245,7 +242,7 @@ impl BackgroundService for CloudflareTunnel { if !dns_propagated { self.notify_update_with_details( - &status_sender, + progress_bar, super::RunStatus::Error, "Failed to propagate tunnel DNS", ); @@ -253,15 +250,15 @@ impl BackgroundService for CloudflareTunnel { return Err(Error::DNSNotPropagated.into()); } - self.notify_update(&status_sender, super::RunStatus::Starting); + self.notify_update(progress_bar, super::RunStatus::Starting); } match self.update_state(&tunnel_url, state) { Ok(_) => { - self.notify_update(&status_sender, super::RunStatus::Started); + self.notify_update(progress_bar, super::RunStatus::Started); } Err(e) => { - self.notify_update(&status_sender, super::RunStatus::Error); + self.notify_update(progress_bar, super::RunStatus::Error); return Err(e); } @@ -271,7 +268,7 @@ impl BackgroundService for CloudflareTunnel { } Err(e) => { self.notify_update_with_details( - &status_sender, + progress_bar, super::RunStatus::Error, "Failed to start", ); diff --git a/linkup-cli/src/services/local_dns_server.rs b/linkup-cli/src/services/local_dns_server.rs deleted file mode 100644 index 80638c08..00000000 --- a/linkup-cli/src/services/local_dns_server.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::{ - env, - fs::File, - os::unix::process::CommandExt, - path::PathBuf, - process::{self, Stdio}, -}; - -use anyhow::Context; - -use crate::{commands::local_dns, linkup_file_path, local_config::LocalState, Result}; - -use super::BackgroundService; - -pub struct LocalDnsServer { - stdout_file_path: PathBuf, - stderr_file_path: PathBuf, -} - -impl LocalDnsServer { - pub fn new() -> Self { - Self { - stdout_file_path: linkup_file_path("localdns-stdout"), - stderr_file_path: linkup_file_path("localdns-stderr"), - } - } - - fn start(&self, session_name: &str, domains: &[String]) -> Result<()> { - log::debug!("Starting {}", Self::NAME); - - let stdout_file = File::create(&self.stdout_file_path)?; - let stderr_file = File::create(&self.stderr_file_path)?; - - let mut command = process::Command::new( - env::current_exe().context("Failed to get the current executable")?, - ); - command.env("RUST_LOG", "debug"); - command.env("LINKUP_SERVICE_ID", Self::ID); - command.args([ - "server", - "dns", - "--session-name", - session_name, - "--domains", - &domains.join(","), - ]); - - command - .process_group(0) - .stdout(stdout_file) - .stderr(stderr_file) - .stdin(Stdio::null()) - .spawn()?; - - Ok(()) - } -} - -impl BackgroundService for LocalDnsServer { - const ID: &str = "linkup-local-dns-server"; - const NAME: &str = "Local DNS server"; - - async fn run_with_progress( - &self, - state: &mut LocalState, - status_sender: std::sync::mpsc::Sender, - ) -> Result<()> { - self.notify_update(&status_sender, super::RunStatus::Starting); - - let session_name = state.linkup.session_name.clone(); - let domains = state.domain_strings(); - - if !local_dns::is_installed(&domains) { - self.notify_update_with_details( - &status_sender, - super::RunStatus::Skipped, - "Not installed", - ); - - return Ok(()); - } - - if let Err(e) = self.start(&session_name, &domains) { - self.notify_update_with_details( - &status_sender, - super::RunStatus::Error, - "Failed to start", - ); - - return Err(e); - } - - self.notify_update(&status_sender, super::RunStatus::Started); - - Ok(()) - } -} diff --git a/linkup-cli/src/services/local_server.rs b/linkup-cli/src/services/local_server.rs index 7bbeb420..cdfedb17 100644 --- a/linkup-cli/src/services/local_server.rs +++ b/linkup-cli/src/services/local_server.rs @@ -8,14 +8,15 @@ use std::{ }; use anyhow::Context; +use indicatif::ProgressBar; use reqwest::StatusCode; use tokio::time::sleep; use url::Url; use crate::{ - linkup_certs_dir_path, linkup_file_path, - local_config::{upload_state, LocalState}, - worker_client, Result, + Result, linkup_certs_dir_path, linkup_file_path, + state::{State, upload_state}, + worker_client, }; use super::{BackgroundService, PidError}; @@ -66,7 +67,6 @@ impl LocalServer { command.env("LINKUP_SERVICE_ID", Self::ID); command.args([ "server", - "local-worker", "--certs-dir", linkup_certs_dir_path().to_str().unwrap(), ]); @@ -93,7 +93,7 @@ impl LocalServer { matches!(response, Ok(res) if res.status() == StatusCode::OK) } - async fn update_state(&self, state: &mut LocalState) -> Result<()> { + async fn update_state(&self, state: &mut State) -> Result<()> { let session_name = upload_state(state).await?; state.linkup.session_name = session_name; @@ -109,16 +109,12 @@ impl BackgroundService for LocalServer { const ID: &str = "linkup-local-server"; const NAME: &str = "Linkup local server"; - async fn run_with_progress( - &self, - state: &mut LocalState, - status_sender: std::sync::mpsc::Sender, - ) -> Result<()> { - self.notify_update(&status_sender, super::RunStatus::Starting); + async fn run_with_progress(&self, state: &mut State, progress_bar: &ProgressBar) -> Result<()> { + self.notify_update(progress_bar, super::RunStatus::Starting); if self.reachable().await { self.notify_update_with_details( - &status_sender, + progress_bar, super::RunStatus::Started, "Was already running", ); @@ -128,7 +124,7 @@ impl BackgroundService for LocalServer { if let Err(e) = self.start() { self.notify_update_with_details( - &status_sender, + progress_bar, super::RunStatus::Error, "Failed to start", ); @@ -146,7 +142,7 @@ impl BackgroundService for LocalServer { attempts += 1; self.notify_update_with_details( - &status_sender, + progress_bar, super::RunStatus::Starting, format!("Waiting for server... retry #{}", attempts), ); @@ -155,7 +151,7 @@ impl BackgroundService for LocalServer { } (false, 10..) => { self.notify_update_with_details( - &status_sender, + progress_bar, super::RunStatus::Error, "Failed to reach server", ); @@ -167,10 +163,10 @@ impl BackgroundService for LocalServer { match self.update_state(state).await { Ok(_) => { - self.notify_update(&status_sender, super::RunStatus::Started); + self.notify_update(progress_bar, super::RunStatus::Started); } Err(e) => { - self.notify_update(&status_sender, super::RunStatus::Error); + self.notify_update(progress_bar, super::RunStatus::Error); return Err(e); } } diff --git a/linkup-cli/src/services/mod.rs b/linkup-cli/src/services/mod.rs index 3559ef4d..69b3056f 100644 --- a/linkup-cli/src/services/mod.rs +++ b/linkup-cli/src/services/mod.rs @@ -1,21 +1,20 @@ -use std::{fmt::Display, sync}; +use std::{fmt::Display, sync::LazyLock, time::Duration}; +use indicatif::{ProgressBar, ProgressStyle}; use sysinfo::{ProcessRefreshKind, RefreshKind, System}; use thiserror::Error; mod cloudflare_tunnel; -mod local_dns_server; mod local_server; -pub use local_dns_server::LocalDnsServer; pub use local_server::LocalServer; pub use sysinfo::{Pid, Signal}; pub use { - cloudflare_tunnel::is_installed as is_cloudflared_installed, cloudflare_tunnel::CloudflareTunnel, + cloudflare_tunnel::is_installed as is_cloudflared_installed, }; -use crate::local_config::LocalState; +use crate::state::State; #[derive(Clone)] pub enum RunStatus { @@ -26,88 +25,91 @@ pub enum RunStatus { Error, } -impl Display for RunStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Pending => write!(f, "pending"), - Self::Starting => write!(f, "starting"), - Self::Started => write!(f, "started"), - Self::Skipped => write!(f, "skipped"), - Self::Error => write!(f, "error"), - } - } -} - -#[derive(Clone)] -pub struct RunUpdate { - pub id: String, - pub status: RunStatus, - pub details: Option, +#[derive(Error, Debug)] +pub enum PidError { + #[error("no pid file: {0}")] + NoPidFile(String), + #[error("bad pid file: {0}")] + BadPidFile(String), } pub trait BackgroundService { const ID: &str; const NAME: &str; + fn prepare_progress_bar(&self, progress_bar: &ProgressBar) { + progress_bar.set_prefix(Self::NAME); + progress_bar.set_style(STATIC_STYLE.clone()); + progress_bar.set_message(RunStatus::Pending.to_string()); + } + async fn run_with_progress( &self, - local_state: &mut LocalState, - status_sender: sync::mpsc::Sender, + local_state: &mut State, + progress_bar: &ProgressBar, ) -> anyhow::Result<()>; - fn notify_update(&self, status_sender: &sync::mpsc::Sender, status: RunStatus) { - status_sender - .send(RunUpdate { - id: String::from(Self::NAME), - status, - details: None, - }) - .unwrap(); + fn stop() { + if let Some(pid) = Self::find_pid() { + system() + .process(pid) + .map(|process| process.kill_with(Signal::Interrupt)); + } + } + + fn notify_update(&self, progress_bar: &ProgressBar, status: RunStatus) { + match status { + RunStatus::Starting => { + progress_bar.set_style(IN_PROGRESS_STYLE.clone()); + progress_bar.enable_steady_tick(Duration::from_millis(50)); + } + _ => progress_bar.set_style(STATIC_STYLE.clone()), + } + + progress_bar.set_message(status.to_string()); } fn notify_update_with_details( &self, - status_sender: &sync::mpsc::Sender, + progress_bar: &ProgressBar, status: RunStatus, - details: impl Into, + details: impl Display, ) { - status_sender - .send(RunUpdate { - id: String::from(Self::NAME), - status, - details: Some(details.into()), - }) - .unwrap(); + progress_bar.set_message(format!("{status} ({details})")); + + match status { + RunStatus::Starting => { + progress_bar.enable_steady_tick(Duration::from_millis(50)); + progress_bar.set_style(IN_PROGRESS_STYLE.clone()) + } + _ => progress_bar.set_style(STATIC_STYLE.clone()), + } } -} -#[derive(Error, Debug)] -pub enum PidError { - #[error("no pid file: {0}")] - NoPidFile(String), - #[error("bad pid file: {0}")] - BadPidFile(String), -} - -pub fn find_service_pid(service_id: &str) -> Option { - for (pid, process) in system().processes() { - if process - .environ() - .iter() - .any(|item| item.to_string_lossy() == format!("LINKUP_SERVICE_ID={service_id}")) - { - return Some(*pid); + fn find_pid() -> Option { + for (pid, process) in system().processes() { + if process + .environ() + .iter() + .any(|item| item.to_string_lossy() == format!("LINKUP_SERVICE_ID={}", Self::ID)) + { + return Some(*pid); + } } - } - None + None + } } -pub fn stop_service(service_id: &str) { - if let Some(pid) = find_service_pid(service_id) { - system() - .process(pid) - .map(|process| process.kill_with(Signal::Interrupt)); +impl Display for RunStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Pending => write!(f, "pending"), + Self::Starting => write!(f, "starting"), + Self::Started => write!(f, "started"), + Self::Skipped => write!(f, "skipped"), + Self::Error => write!(f, "error"), + } } } @@ -116,3 +118,12 @@ pub fn system() -> System { RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()), ) } + +static STATIC_STYLE: LazyLock = + LazyLock::new(|| ProgressStyle::with_template("{prefix:<20} {msg}").unwrap()); + +static IN_PROGRESS_STYLE: LazyLock = LazyLock::new(|| { + ProgressStyle::with_template("{prefix:<20} {spinner:.blue}") + .unwrap() + .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏") +}); diff --git a/linkup-cli/src/local_config.rs b/linkup-cli/src/state.rs similarity index 60% rename from linkup-cli/src/local_config.rs rename to linkup-cli/src/state.rs index 4d37da45..52697bed 100644 --- a/linkup-cli/src/local_config.rs +++ b/linkup-cli/src/state.rs @@ -6,28 +6,25 @@ use std::{ use anyhow::Context; use rand::distr::{Alphanumeric, SampleString}; +use regex::Regex; use serde::{Deserialize, Serialize}; use url::Url; -use linkup::{ - CreatePreviewRequest, StorableDomain, StorableRewrite, StorableService, StorableSession, - UpdateSessionRequest, -}; +use linkup::{Domain, Session, SessionService, UpdateSessionRequest}; use crate::{ - linkup_file_path, services, + LINKUP_CONFIG_ENV, LINKUP_STATE_FILE, Result, linkup_file_path, services, worker_client::{self, WorkerClient}, - Result, LINKUP_CONFIG_ENV, LINKUP_STATE_FILE, }; -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] -pub struct LocalState { +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct State { pub linkup: LinkupState, - pub domains: Vec, + pub domains: Vec, pub services: Vec, } -impl LocalState { +impl State { pub fn load() -> anyhow::Result { let state_file_path = linkup_file_path(LINKUP_STATE_FILE); let content = fs::read_to_string(&state_file_path) @@ -70,7 +67,7 @@ impl LocalState { pub fn domain_strings(&self) -> Vec { self.domains .iter() - .map(|storable_domain| storable_domain.domain.clone()) + .map(|domain| domain.domain.clone()) .collect::>() } @@ -79,7 +76,7 @@ impl LocalState { } } -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct LinkupState { pub session_name: String, pub session_token: String, @@ -87,31 +84,27 @@ pub struct LinkupState { pub worker_token: String, pub config_path: String, pub tunnel: Option, - pub cache_routes: Option>, -} - -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Default)] -pub struct HealthConfig { - pub path: Option, - pub statuses: Option>, + #[serde( + default, + serialize_with = "linkup::serde_ext::serialize_opt_vec_regex", + deserialize_with = "linkup::serde_ext::deserialize_opt_vec_regex" + )] + pub cache_routes: Option>, } -#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct LocalService { - pub name: String, - pub remote: Url, - pub local: Url, pub current: ServiceTarget, - pub directory: Option, - pub rewrites: Vec, - pub health: Option, + + #[serde(flatten)] + pub config: linkup::config::ServiceConfig, } impl LocalService { pub fn current_url(&self) -> Url { match self.current { - ServiceTarget::Local => self.local.clone(), - ServiceTarget::Remote => self.remote.clone(), + ServiceTarget::Local => self.config.local.clone(), + ServiceTarget::Remote => self.config.remote.clone(), } } } @@ -131,106 +124,37 @@ impl Display for ServiceTarget { } } -#[derive(Deserialize, Clone)] -pub struct YamlLocalConfig { - pub linkup: LinkupConfig, - pub services: Vec, - pub domains: Vec, -} - -impl YamlLocalConfig { - pub fn create_preview_request(&self, services: &[(String, String)]) -> CreatePreviewRequest { - let services = self - .services - .iter() - .map(|yaml_local_service: &YamlLocalService| { - let name = yaml_local_service.name.clone(); - let mut location = yaml_local_service.remote.clone(); - - for (param_service_name, param_service_url) in services { - if param_service_name == &name { - location = Url::parse(param_service_url).unwrap(); - } - } - - StorableService { - name, - location, - rewrites: yaml_local_service.rewrites.clone(), - } - }) - .collect(); - - CreatePreviewRequest { - services, - domains: self.domains.clone(), - cache_routes: self.linkup.cache_routes.clone(), - } - } -} - -#[derive(Deserialize, Clone)] -pub struct LinkupConfig { - pub worker_url: Url, - pub worker_token: String, - cache_routes: Option>, -} - -#[derive(Deserialize, Clone)] -pub struct YamlLocalService { - name: String, - remote: Url, - local: Url, - directory: Option, - rewrites: Option>, - health: Option, -} - #[derive(Debug)] -pub struct ServerConfig { - pub local: StorableSession, - pub remote: StorableSession, +pub struct ServersSessions { + pub local: Session, + pub remote: Session, } -pub fn config_to_state( - yaml_config: YamlLocalConfig, - config_path: String, - no_tunnel: bool, -) -> LocalState { +pub fn config_to_state(config: linkup::config::Config, config_path: String) -> State { let random_token = Alphanumeric.sample_string(&mut rand::rng(), 16); - let tunnel = match no_tunnel { - true => None, - false => Some(Url::parse("http://tunnel-not-yet-set").expect("default url parses")), - }; - let linkup = LinkupState { session_name: String::new(), session_token: random_token, - worker_token: yaml_config.linkup.worker_token, + worker_token: config.linkup.worker_token, config_path, - worker_url: yaml_config.linkup.worker_url, - tunnel, - cache_routes: yaml_config.linkup.cache_routes, + worker_url: config.linkup.worker_url, + tunnel: Some(Url::parse("http://tunnel-not-yet-set").expect("default url parses")), + cache_routes: config.linkup.cache_routes, }; - let services = yaml_config + let services = config .services .into_iter() - .map(|yaml_service| LocalService { - name: yaml_service.name, - remote: yaml_service.remote, - local: yaml_service.local, + .map(|service_config| LocalService { + config: service_config.clone(), current: ServiceTarget::Remote, - directory: yaml_service.directory, - rewrites: yaml_service.rewrites.unwrap_or_default(), - health: yaml_service.health, }) .collect::>(); - let domains = yaml_config.domains; + let domains = config.domains; - LocalState { + State { linkup, domains, services, @@ -258,7 +182,7 @@ pub fn config_path(config_arg: &Option) -> Result { } } -pub fn get_config(config_path: &str) -> Result { +pub fn get_config(config_path: &str) -> Result { let content = fs::read_to_string(config_path) .with_context(|| format!("Failed to read config file {config_path:?}"))?; @@ -268,25 +192,25 @@ pub fn get_config(config_path: &str) -> Result { // This method gets the local state and uploads it to both the local linkup server and // the remote linkup server (worker). -pub async fn upload_state(state: &LocalState) -> Result { +pub async fn upload_state(state: &State) -> Result { let local_url = services::LocalServer::url(); - let server_config = ServerConfig::from(state); + let servers_sessions = ServersSessions::from(state); let session_name = &state.linkup.session_name; - let server_session_name = upload_config_to_server( + let server_session_name = upload_session_to_server( &state.linkup.worker_url, &state.linkup.worker_token, session_name, - server_config.remote, + servers_sessions.remote, ) .await?; - let local_session_name = upload_config_to_server( + let local_session_name = upload_session_to_server( &local_url, &state.linkup.worker_token, &server_session_name, - server_config.local, + servers_sessions.local, ) .await?; @@ -303,18 +227,18 @@ pub async fn upload_state(state: &LocalState) -> Result { Ok(server_session_name) } -async fn upload_config_to_server( +async fn upload_session_to_server( linkup_url: &Url, worker_token: &str, desired_name: &str, - config: StorableSession, + session: Session, ) -> Result { let session_update_req = UpdateSessionRequest { - session_token: config.session_token, + session_token: session.session_token, desired_name: desired_name.to_string(), - services: config.services, - domains: config.domains, - cache_routes: config.cache_routes, + services: session.services, + domains: session.domains, + cache_routes: session.cache_routes, }; let session_name = WorkerClient::new(linkup_url, worker_token) @@ -324,65 +248,65 @@ async fn upload_config_to_server( Ok(session_name) } -impl From<&LocalState> for ServerConfig { - fn from(state: &LocalState) -> Self { +impl From<&State> for ServersSessions { + fn from(state: &State) -> Self { let local_server_services = state .services .iter() - .map(|service| StorableService { - name: service.name.clone(), + .map(|service| SessionService { + name: service.config.name.clone(), location: if service.current == ServiceTarget::Remote { - service.remote.clone() + service.config.remote.clone() } else { - service.local.clone() + service.config.local.clone() }, - rewrites: Some(service.rewrites.clone()), + rewrites: service.config.rewrites.clone(), }) - .collect::>(); + .collect::>(); let remote_server_services = state .services .iter() - .map(|service| StorableService { - name: service.name.clone(), + .map(|service| SessionService { + name: service.config.name.clone(), location: if service.current == ServiceTarget::Remote { - service.remote.clone() + service.config.remote.clone() } else { state.get_tunnel_url() }, - rewrites: Some(service.rewrites.clone()), + rewrites: service.config.rewrites.clone(), }) - .collect::>(); + .collect::>(); - let local_storable_session = StorableSession { + let local_session = Session { session_token: state.linkup.session_token.clone(), services: local_server_services, domains: state.domains.clone(), cache_routes: state.linkup.cache_routes.clone(), }; - let remote_storable_session = StorableSession { + let remote_session = Session { session_token: state.linkup.session_token.clone(), services: remote_server_services, domains: state.domains.clone(), cache_routes: state.linkup.cache_routes.clone(), }; - ServerConfig { - local: local_storable_session, - remote: remote_storable_session, + ServersSessions { + local: local_session, + remote: remote_session, } } } -pub fn managed_domains(state: Option<&LocalState>, cfg_path: &Option) -> Vec { +pub fn managed_domains(state: Option<&State>, cfg_path: &Option) -> Vec { let config_domains = match config_path(cfg_path).ok() { Some(cfg_path) => match get_config(&cfg_path) { Ok(config) => Some( config .domains .iter() - .map(|storable_domain| storable_domain.domain.clone()) + .map(|domain| domain.domain.clone()) .collect::>(), ), Err(_) => None, @@ -453,8 +377,8 @@ domains: #[test] fn test_config_to_state() { let input_str = String::from(CONF_STR); - let yaml_config = serde_yaml::from_str(&input_str).unwrap(); - let local_state = config_to_state(yaml_config, "./path/to/config.yaml".to_string(), false); + let config = serde_yaml::from_str(&input_str).unwrap(); + let local_state = config_to_state(config, "./path/to/config.yaml".to_string()); assert_eq!(local_state.linkup.config_path, "./path/to/config.yaml"); @@ -468,40 +392,45 @@ domains: ); assert_eq!(local_state.services.len(), 2); - assert_eq!(local_state.services[0].name, "frontend"); + assert_eq!(local_state.services[0].config.name, "frontend"); assert_eq!( - local_state.services[0].remote, + local_state.services[0].config.remote, Url::parse("http://remote-service1.example.com").unwrap() ); assert_eq!( - local_state.services[0].local, + local_state.services[0].config.local, Url::parse("http://localhost:8000").unwrap() ); assert_eq!(local_state.services[0].current, ServiceTarget::Remote); - assert_eq!(local_state.services[0].health, None); + assert!(local_state.services[0].config.health.is_none()); - assert_eq!(local_state.services[0].rewrites.len(), 1); - assert_eq!(local_state.services[1].name, "backend"); assert_eq!( - local_state.services[1].remote, + local_state.services[0] + .config + .rewrites + .as_ref() + .unwrap() + .len(), + 1 + ); + assert_eq!(local_state.services[1].config.name, "backend"); + assert_eq!( + local_state.services[1].config.remote, Url::parse("http://remote-service2.example.com").unwrap() ); assert_eq!( - local_state.services[1].local, + local_state.services[1].config.local, Url::parse("http://localhost:8001").unwrap() ); - assert_eq!(local_state.services[1].rewrites.len(), 0); + assert!(local_state.services[1].config.rewrites.is_none()); assert_eq!( - local_state.services[1].directory, + local_state.services[1].config.directory, Some("../backend".to_string()) ); - assert_eq!( - local_state.services[1].health, - Some(HealthConfig { - path: Some("/health".to_string()), - statuses: Some(vec![200, 304]), - }) - ); + assert!(local_state.services[1].config.health.is_some()); + let health = local_state.services[1].config.health.as_ref().unwrap(); + assert_eq!(health.path, Some("/health".to_string())); + assert_eq!(health.statuses, Some(vec![200, 304])); assert_eq!(local_state.domains.len(), 2); assert_eq!(local_state.domains[0].domain, "example.com"); diff --git a/linkup-cli/src/worker_client.rs b/linkup-cli/src/worker_client.rs index f69fcb2e..9f34d216 100644 --- a/linkup-cli/src/worker_client.rs +++ b/linkup-cli/src/worker_client.rs @@ -1,10 +1,8 @@ use linkup::{CreatePreviewRequest, UpdateSessionRequest}; -use reqwest::{header, StatusCode}; +use reqwest::{StatusCode, header}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::local_config::YamlLocalConfig; - #[derive(thiserror::Error, Debug)] pub enum Error { #[error("{0}")] @@ -103,21 +101,20 @@ impl WorkerClient { .send() .await?; - match response.status() { - StatusCode::OK => { - let content = response.text().await?; - Ok(content) - } - _ => Err(Error::Response( + if response.status().is_success() { + let content = response.text().await?; + Ok(content) + } else { + Err(Error::Response( response.status(), response.text().await.unwrap_or_else(|_| "".to_string()), - )), + )) } } } -impl From<&YamlLocalConfig> for WorkerClient { - fn from(config: &YamlLocalConfig) -> Self { +impl From<&linkup::config::Config> for WorkerClient { + fn from(config: &linkup::config::Config) -> Self { Self::new(&config.linkup.worker_url, &config.linkup.worker_token) } } diff --git a/linkup/Cargo.toml b/linkup/Cargo.toml index 93240fb7..90e75009 100644 --- a/linkup/Cargo.toml +++ b/linkup/Cargo.toml @@ -1,19 +1,19 @@ [package] name = "linkup" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] -hex = "0.4.3" -rand = "0.10.0" -regex = "1.11.1" -serde = "1.0.217" -serde_json = "1.0.137" -sha2 = "0.10.8" -thiserror = "2.0.11" +hex = { workspace = true } +rand = { workspace = true } +regex = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +thiserror = { workspace = true } unicase = "2.8.1" -url = { version = "2.5.4", features = ["serde"] } -http = "1.2.0" +url = { workspace = true } +http = { workspace = true } [dev-dependencies] -tokio = { version = "1", features = ["test-util", "macros"] } +tokio = { workspace = true, features = ["test-util", "macros"] } diff --git a/linkup/src/config.rs b/linkup/src/config.rs new file mode 100644 index 00000000..8a06a216 --- /dev/null +++ b/linkup/src/config.rs @@ -0,0 +1,40 @@ +use regex::Regex; +use serde::{Deserialize, Serialize}; +use url::Url; + +use crate::{Domain, Rewrite}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Config { + pub linkup: LinkupConfig, + pub services: Vec, + pub domains: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct LinkupConfig { + pub worker_url: Url, + pub worker_token: String, + #[serde( + default, + deserialize_with = "crate::serde_ext::deserialize_opt_vec_regex", + serialize_with = "crate::serde_ext::serialize_opt_vec_regex" + )] + pub cache_routes: Option>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct ServiceConfig { + pub name: String, + pub remote: Url, + pub local: Url, + pub directory: Option, + pub rewrites: Option>, + pub health: Option, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct HealthConfig { + pub path: Option, + pub statuses: Option>, +} diff --git a/linkup/src/headers.rs b/linkup/src/headers.rs index 5e6efe69..64df5c12 100644 --- a/linkup/src/headers.rs +++ b/linkup/src/headers.rs @@ -164,13 +164,15 @@ impl From for HeaderMap { impl From for HttpHeaderMap { fn from(linkup_headers: HeaderMap) -> Self { let mut http_headers = HttpHeaderMap::new(); + for (key, value) in linkup_headers.into_iter() { - if let Ok(http_value) = HttpHeaderValue::from_str(&value) { - if let Ok(http_key) = http::header::HeaderName::from_bytes(key.as_bytes()) { - http_headers.insert(http_key, http_value); - } + if let Ok(http_value) = HttpHeaderValue::from_str(&value) + && let Ok(http_key) = http::header::HeaderName::from_bytes(key.as_bytes()) + { + http_headers.insert(http_key, http_value); } } + http_headers } } @@ -178,7 +180,7 @@ impl From for HttpHeaderMap { #[cfg(test)] mod tests { use super::normalize_cookie_header; - use http::{header::COOKIE, HeaderMap, HeaderValue}; + use http::{HeaderMap, HeaderValue, header::COOKIE}; #[test] fn normalizes_multiple_cookie_headers_with_semicolon() { diff --git a/linkup/src/lib.rs b/linkup/src/lib.rs index cc3f3a35..80959f4c 100644 --- a/linkup/src/lib.rs +++ b/linkup/src/lib.rs @@ -1,3 +1,6 @@ +pub mod config; +pub mod serde_ext; + mod headers; mod memory_session_store; mod name_gen; @@ -37,9 +40,9 @@ pub enum SessionError { // this workspace), we should return `impl Future` instead of having `async fn` so that we can add and ensure // any desired bounds. pub trait StringStore { - fn get(&self, key: String) -> impl Future, SessionError>>; - fn exists(&self, key: String) -> impl Future>; - fn put(&self, key: String, value: String) -> impl Future>; + fn get(&self, key: &str) -> impl Future, SessionError>>; + fn exists(&self, key: &str) -> impl Future>; + fn put(&self, key: &str, value: &str) -> impl Future>; } #[derive(PartialEq)] @@ -162,32 +165,32 @@ pub fn get_target_service( // If there was a destination created in a previous linkup, we don't want to // re-do path rewrites, so we use the destination service. - if let Some(destination_service) = headers.get(HeaderName::LinkupDestination) { - if let Some(service) = config.services.get(destination_service) { - let target = redirect(target.clone(), &service.origin, Some(path.to_string())); - return Some(TargetService { - name: destination_service.to_string(), - url: target.to_string(), - }); - } + if let Some(destination_service) = headers.get(HeaderName::LinkupDestination) + && let Some(service) = config.get_service(destination_service) + { + let target = redirect(target.clone(), &service.location, Some(path.to_string())); + return Some(TargetService { + name: destination_service.to_string(), + url: target.to_string(), + }); } - let url_target = config.domains.get(&get_target_domain(url, session_name)); + let url_target = config.get_domain(&get_target_domain(url, session_name)); // Forwarded hosts persist over the tunnel - let forwarded_host_target = config.domains.get(&get_target_domain( + let forwarded_host_target = config.get_domain(&get_target_domain( headers.get_or_default(HeaderName::ForwardedHost, "does-not-exist"), session_name, )); // This is more for e2e tests to work - let referer_target = config.domains.get(&get_target_domain( + let referer_target = config.get_domain(&get_target_domain( headers.get_or_default(HeaderName::Referer, "does-not-exist"), session_name, )); // This one is for redirects, where the referer doesn't exist - let origin_target = config.domains.get(&get_target_domain( + let origin_target = config.get_domain(&get_target_domain( headers.get_or_default(HeaderName::Origin, "does-not-exist"), session_name, )); @@ -203,30 +206,34 @@ pub fn get_target_service( }; if let Some(domain) = target_domain { - let service_name = domain - .routes - .iter() - .find_map(|route| { - if route.path.is_match(path) { - Some(route.service.clone()) - } else { - None - } - }) - .unwrap_or_else(|| domain.default_service.clone()); + let service_name = match &domain.routes { + Some(routes) => routes + .iter() + .find_map(|route| { + if route.path.is_match(path) { + Some(route.service.clone()) + } else { + None + } + }) + .unwrap_or_else(|| domain.default_service.clone()), + None => domain.default_service.clone(), + }; - if let Some(service) = config.services.get(&service_name) { + if let Some(service) = config.get_service(&service_name) { let mut new_path = path.to_string(); - for modifier in &service.rewrites { - if modifier.source.is_match(&new_path) { - new_path = modifier - .source - .replace_all(&new_path, &modifier.target) - .to_string(); + if let Some(rewrites) = &service.rewrites { + for modifier in rewrites { + if modifier.source.is_match(&new_path) { + new_path = modifier + .source + .replace_all(&new_path, &modifier.target) + .to_string(); + } } } - let target = redirect(target, &service.origin, Some(new_path)); + let target = redirect(target, &service.location, Some(new_path)); return Some(TargetService { name: service_name, url: target.to_string(), @@ -371,7 +378,7 @@ mod tests { let config: Session = config_value.try_into().unwrap(); let name = sessions - .store_session(config, NameKind::Animal, "".to_string()) + .store_session(config, NameKind::Animal, "") .await .unwrap(); @@ -517,7 +524,7 @@ mod tests { let input_config: Session = input_config_value.try_into().unwrap(); let name = sessions - .store_session(input_config, NameKind::Animal, "".to_string()) + .store_session(input_config, NameKind::Animal, "") .await .unwrap(); @@ -607,7 +614,7 @@ mod tests { let input_config: Session = input_config_value.try_into().unwrap(); let name = sessions - .store_session(input_config, NameKind::Animal, "".to_string()) + .store_session(input_config, NameKind::Animal, "") .await .unwrap(); @@ -658,7 +665,7 @@ mod tests { let input_config: Session = input_config_value.try_into().unwrap(); let name = sessions - .store_session(input_config, NameKind::Animal, "".to_string()) + .store_session(input_config, NameKind::Animal, "") .await .unwrap(); diff --git a/linkup/src/memory_session_store.rs b/linkup/src/memory_session_store.rs index eaf487cf..f1d9bbb5 100644 --- a/linkup/src/memory_session_store.rs +++ b/linkup/src/memory_session_store.rs @@ -1,24 +1,24 @@ +use crate::{SessionError, StringStore}; + use std::{ collections::HashMap, sync::{Arc, RwLock}, }; -use crate::{SessionError, StringStore}; - #[derive(Default, Clone)] pub struct MemoryStringStore(Arc>>); impl StringStore for MemoryStringStore { - async fn get(&self, key: String) -> Result, SessionError> { + async fn get(&self, key: &str) -> Result, SessionError> { match self.0.read() { - Ok(l) => Ok(l.get(key.as_str()).cloned()), + Ok(l) => Ok(l.get(key).cloned()), Err(e) => Err(SessionError::GetError(e.to_string())), } } - async fn exists(&self, key: String) -> Result { + async fn exists(&self, key: &str) -> Result { let value = match self.0.read() { - Ok(l) => Ok(l.get(&key).cloned()), + Ok(l) => Ok(l.get(key).cloned()), Err(e) => return Err(SessionError::GetError(e.to_string())), }?; @@ -28,9 +28,9 @@ impl StringStore for MemoryStringStore { } } - async fn put(&self, key: String, value: String) -> Result<(), SessionError> { + async fn put(&self, key: &str, value: &str) -> Result<(), SessionError> { match self.0.write() { - Ok(mut l) => Ok(l.insert(key, value)), + Ok(mut l) => Ok(l.insert(key.to_owned(), value.to_owned())), Err(e) => Err(SessionError::PutError(e.to_string())), }?; diff --git a/linkup/src/name_gen.rs b/linkup/src/name_gen.rs index 1194d8ed..5a8499d5 100644 --- a/linkup/src/name_gen.rs +++ b/linkup/src/name_gen.rs @@ -2,7 +2,6 @@ use rand::{ distr::{Alphanumeric, SampleString}, seq::IndexedRandom, }; -use sha2::{Digest, Sha256}; pub fn random_animal() -> String { let mut rand = rand::rng(); @@ -17,18 +16,6 @@ pub fn random_animal() -> String { format!("{adjective}-{animal}") } -pub fn deterministic_six_char_hash(input: &str) -> String { - let mut hasher = Sha256::new(); - - hasher.update(input); - - let result = hasher.finalize(); - let hex_string = hex::encode(result); - - // Truncate the hexadecimal string to 6 characters - hex_string[..6].to_string() -} - pub fn random_six_char() -> String { Alphanumeric .sample_string(&mut rand::rng(), 6) diff --git a/linkup/src/serde_ext.rs b/linkup/src/serde_ext.rs new file mode 100644 index 00000000..b9b46514 --- /dev/null +++ b/linkup/src/serde_ext.rs @@ -0,0 +1,136 @@ +use std::str::FromStr; + +use regex::Regex; +use serde::{Deserialize, Deserializer, Serializer, ser::SerializeSeq}; + +pub fn serialize_regex(regex: &Regex, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(regex.as_str()) +} + +pub fn deserialize_regex<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + Regex::from_str(&s).map_err(serde::de::Error::custom) +} + +pub fn serialize_opt_vec_regex( + regexes: &Option>, + serializer: S, +) -> Result +where + S: Serializer, +{ + match regexes { + Some(regexes) => { + let mut seq = serializer.serialize_seq(Some(regexes.len()))?; + + for regex in regexes { + seq.serialize_element(regex.as_str())?; + } + + seq.end() + } + None => serializer.serialize_none(), + } +} + +pub fn deserialize_opt_vec_regex<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let regexes_str: Option> = Option::deserialize(deserializer)?; + let Some(regexes_str) = regexes_str else { + return Ok(None); + }; + + let mut regexes: Vec = Vec::with_capacity(regexes_str.len()); + + for regex_str in regexes_str { + let regex = Regex::from_str(®ex_str).map_err(serde::de::Error::custom)?; + regexes.push(regex); + } + + Ok(Some(regexes)) +} + +#[cfg(test)] +mod tests { + use regex::Regex; + use serde::{Deserialize, Serialize}; + + #[test] + fn test_serialize_deserialize_regex() { + #[derive(Serialize, Deserialize)] + struct A { + #[serde( + deserialize_with = "crate::serde_ext::deserialize_regex", + serialize_with = "crate::serde_ext::serialize_regex" + )] + reg_field: Regex, + } + + let record = A { + reg_field: Regex::new("abc: (.+)").unwrap(), + }; + + let serialized_record = serde_json::to_string(&record).unwrap(); + assert_eq!(r#"{"reg_field":"abc: (.+)"}"#, &serialized_record); + + let des_record: A = serde_json::from_str(&serialized_record).unwrap(); + assert!(des_record.reg_field.is_match("abc: foo")); + + let captures = des_record.reg_field.captures("abc: foo").unwrap(); + assert_eq!("foo", captures.get(1).unwrap().as_str()); + } + + #[test] + fn test_serialize_deserialize_opt_vec_regex() { + #[derive(Serialize, Deserialize)] + struct A { + #[serde( + deserialize_with = "crate::serde_ext::deserialize_opt_vec_regex", + serialize_with = "crate::serde_ext::serialize_opt_vec_regex" + )] + reg_field: Option>, + + #[serde( + deserialize_with = "crate::serde_ext::deserialize_opt_vec_regex", + serialize_with = "crate::serde_ext::serialize_opt_vec_regex" + )] + reg_field2: Option>, + + #[serde( + deserialize_with = "crate::serde_ext::deserialize_opt_vec_regex", + serialize_with = "crate::serde_ext::serialize_opt_vec_regex" + )] + reg_field3: Option>, + } + + let record = A { + reg_field: None, + reg_field2: Some(vec![]), + reg_field3: Some(vec![Regex::new("abc: (.+)").unwrap()]), + }; + + let serialized_record = serde_json::to_string(&record).unwrap(); + assert_eq!( + r#"{"reg_field":null,"reg_field2":[],"reg_field3":["abc: (.+)"]}"#, + &serialized_record + ); + + let des_record: A = serde_json::from_str(&serialized_record).unwrap(); + + assert!(des_record.reg_field.is_none()); + + assert!(des_record.reg_field2.is_some()); + assert!(des_record.reg_field2.unwrap().is_empty()); + + assert!(des_record.reg_field3.is_some()); + assert!(des_record.reg_field3.unwrap()[0].is_match("abc: foo")); + } +} diff --git a/linkup/src/session.rs b/linkup/src/session.rs index 894b18cc..6007b918 100644 --- a/linkup/src/session.rs +++ b/linkup/src/session.rs @@ -1,98 +1,88 @@ -use std::{ - cmp::Ordering, - collections::{HashMap, HashSet}, -}; +use sha2::{Digest, Sha256}; +use std::collections::HashSet; use thiserror::Error; use regex::Regex; use serde::{Deserialize, Serialize}; use url::Url; -pub const PREVIEW_SESSION_TOKEN: &str = "preview_session"; +use crate::config::Config; -#[derive(Clone, Debug)] -pub struct Session { - pub session_token: String, - pub services: HashMap, - pub domains: HashMap, - pub domain_selection_order: Vec, - pub cache_routes: Option>, -} - -#[derive(Clone, Debug)] -pub struct Service { - pub origin: Url, - pub rewrites: Vec, -} - -#[derive(Clone, Debug)] -pub struct Rewrite { - pub source: Regex, - pub target: String, -} +pub const PREVIEW_SESSION_TOKEN: &str = "preview_session"; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Domain { + pub domain: String, pub default_service: String, - pub routes: Vec, + pub routes: Option>, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Route { + #[serde( + serialize_with = "crate::serde_ext::serialize_regex", + deserialize_with = "crate::serde_ext::deserialize_regex" + )] pub path: Regex, pub service: String, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct UpdateSessionRequest { pub desired_name: String, pub session_token: String, - pub services: Vec, - pub domains: Vec, - pub cache_routes: Option>, + pub services: Vec, + pub domains: Vec, + #[serde( + default, + serialize_with = "crate::serde_ext::serialize_opt_vec_regex", + deserialize_with = "crate::serde_ext::deserialize_opt_vec_regex" + )] + pub cache_routes: Option>, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct CreatePreviewRequest { - pub services: Vec, - pub domains: Vec, - pub cache_routes: Option>, + pub services: Vec, + pub domains: Vec, + #[serde( + default, + serialize_with = "crate::serde_ext::serialize_opt_vec_regex", + deserialize_with = "crate::serde_ext::deserialize_opt_vec_regex" + )] + pub cache_routes: Option>, } -#[derive(Debug, Deserialize, Serialize)] -pub struct StorableSession { +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Session { pub session_token: String, - pub services: Vec, - pub domains: Vec, - pub cache_routes: Option>, + pub services: Vec, + pub domains: Vec, + #[serde( + default, + serialize_with = "crate::serde_ext::serialize_opt_vec_regex", + deserialize_with = "crate::serde_ext::deserialize_opt_vec_regex" + )] + pub cache_routes: Option>, } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct StorableService { +pub struct SessionService { pub name: String, pub location: Url, - pub rewrites: Option>, + pub rewrites: Option>, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] -pub struct StorableRewrite { - pub source: String, +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Rewrite { + #[serde( + serialize_with = "crate::serde_ext::serialize_regex", + deserialize_with = "crate::serde_ext::deserialize_regex" + )] + pub source: Regex, pub target: String, } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] -pub struct StorableDomain { - pub domain: String, - pub default_service: String, - pub routes: Option>, -} - -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] -pub struct StorableRoute { - pub path: String, - pub service: String, -} - #[derive(Error, Debug)] pub enum ConfigError { #[error("linkup session json format error: {0}")] @@ -109,135 +99,73 @@ pub enum ConfigError { Empty, } -impl From for StorableSession { - fn from(req: UpdateSessionRequest) -> Self { - StorableSession { - session_token: req.session_token, - services: req.services, - domains: req.domains, - cache_routes: req.cache_routes, - } +impl Session { + pub fn get_service(&self, service_name: &str) -> Option<&SessionService> { + self.services + .iter() + .find(|service| service.name == service_name) } -} -impl TryFrom for Session { - type Error = ConfigError; - - fn try_from(value: UpdateSessionRequest) -> Result { - let storable: StorableSession = value.into(); - storable.try_into() + pub fn get_domain(&self, domain: &str) -> Option<&Domain> { + self.domains + .iter() + .find(|domain_record| domain_record.domain == domain) } -} -impl From for StorableSession { - fn from(req: CreatePreviewRequest) -> Self { - StorableSession { - session_token: PREVIEW_SESSION_TOKEN.to_string(), - services: req.services, - domains: req.domains, - cache_routes: req.cache_routes, + pub fn sha(&self) -> String { + let mut sorted_self = self.clone(); + sorted_self.services.sort_by(|a, b| a.name.cmp(&b.name)); + sorted_self.domains.sort_by(|a, b| a.domain.cmp(&b.domain)); + + if let Some(cache_routes) = sorted_self.cache_routes.as_mut() { + cache_routes.sort_by(|a, b| a.as_str().cmp(b.as_str())); } - } -} -impl TryFrom for Session { - type Error = ConfigError; + let serialized_self = + serde_json::to_string(&sorted_self).expect("Session should be serializable"); - fn try_from(value: CreatePreviewRequest) -> Result { - let storable: StorableSession = value.into(); - storable.try_into() - } -} + let mut hasher = Sha256::new(); -impl TryFrom for Rewrite { - type Error = ConfigError; + hasher.update(serialized_self); - fn try_from(value: StorableRewrite) -> Result { - let source: Result = Regex::new(&value.source); - match source { - Err(e) => Err(ConfigError::InvalidRegex(value.source, e)), - Ok(s) => Ok(Rewrite { - source: s, - target: value.target, - }), - } + let result = hasher.finalize(); + hex::encode(result) } } -impl TryFrom for Route { +impl TryFrom for Session { type Error = ConfigError; - fn try_from(value: StorableRoute) -> Result { - let path = Regex::new(&value.path); - match path { - Err(e) => Err(ConfigError::InvalidRegex(value.path, e)), - Ok(p) => Ok(Route { - path: p, - service: value.service, - }), - } + fn try_from(req: UpdateSessionRequest) -> Result { + let session = Self { + session_token: req.session_token, + services: req.services, + domains: req.domains, + cache_routes: req.cache_routes, + }; + + validate_not_empty(&session)?; + validate_services(&session)?; + + Ok(session) } } -impl TryFrom for Session { +impl TryFrom for Session { type Error = ConfigError; - fn try_from(value: StorableSession) -> Result { - validate_not_empty(&value)?; - validate_service_references(&value)?; - - let mut services: HashMap = HashMap::new(); - let mut domains: HashMap = HashMap::new(); - - for stored_service in value.services { - validate_url_origin(&stored_service.location)?; - - let rewrites = match stored_service.rewrites { - Some(pm) => pm.into_iter().map(|r| r.try_into()).collect(), - None => Ok(Vec::new()), - }?; - - let service = Service { - origin: stored_service.location, - rewrites, - }; - - services.insert(stored_service.name, service); - } - - for stored_domain in value.domains { - let routes = match stored_domain.routes { - Some(dr) => dr.into_iter().map(|r| r.try_into()).collect(), - None => Ok(Vec::new()), - }?; - - let domain = Domain { - default_service: stored_domain.default_service, - routes, - }; - - domains.insert(stored_domain.domain, domain); - } - - let domain_names = domains.keys().cloned().collect(); - - let cache_routes = match value.cache_routes { - Some(cr) => Some( - cr.into_iter() - .map(|r| Regex::new(&r)) - .collect::, regex::Error>>() - .map_err(|e| ConfigError::InvalidRegex("cache route".to_string(), e))?, - ), - None => None, + fn try_from(req: CreatePreviewRequest) -> Result { + let session = Self { + session_token: PREVIEW_SESSION_TOKEN.to_string(), + services: req.services, + domains: req.domains, + cache_routes: req.cache_routes, }; - Ok(Session { - session_token: value.session_token, - services, - domains, - domain_selection_order: choose_domain_ordering(domain_names), - cache_routes, - }) + validate_not_empty(&session)?; + validate_services(&session)?; + + Ok(session) } } @@ -245,157 +173,66 @@ impl TryFrom for Session { type Error = ConfigError; fn try_from(value: serde_json::Value) -> Result { - let session_yml_res: Result = - serde_json::from_value(value); + let session = serde_json::from_value(value)?; - match session_yml_res { - Err(e) => Err(ConfigError::JsonFormat(e)), - Ok(c) => c.try_into(), - } + validate_not_empty(&session)?; + validate_services(&session)?; + + Ok(session) } } -impl From for StorableSession { - fn from(value: Session) -> Self { - let mut services: Vec = value - .services - .into_iter() - .map(|(name, service)| { - let rewrites = if service.rewrites.is_empty() { - None - } else { - Some( - service - .rewrites - .into_iter() - .map(|path_modifier| StorableRewrite { - source: path_modifier.source.to_string(), - target: path_modifier.target, - }) - .collect(), - ) - }; - - StorableService { - name, - location: service.origin, - rewrites, - } - }) - .collect(); - - services.sort_by(|a, b| a.name.cmp(&b.name)); - - let mut domains: Vec = value - .domains - .into_iter() - .map(|(domain, domain_data)| { - let default_service = domain_data.default_service; - let routes = if domain_data.routes.is_empty() { - None - } else { - Some( - domain_data - .routes - .into_iter() - .map(|route| StorableRoute { - path: route.path.to_string(), - service: route.service, - }) - .collect(), - ) - }; - - StorableDomain { - domain, - default_service, - routes, - } - }) - .collect(); - - domains.sort_by(|a, b| a.domain.cmp(&b.domain)); +pub fn create_preview_req_from_config( + config: &Config, + services_overwrite: &[(String, Url)], +) -> CreatePreviewRequest { + let mut session_services: Vec = Vec::with_capacity(config.services.len()); - let cache_routes = value.cache_routes.map(|cr| { - cr.into_iter() - .map(|r| r.to_string()) - .collect::>() - }); + for service in &config.services { + let service_overwrite = services_overwrite + .iter() + .find(|overwrite| overwrite.0 == service.name); - StorableSession { - session_token: value.session_token, - services, - domains, - cache_routes, - } - } -} - -pub fn update_session_req_from_json(input_json: String) -> Result<(String, Session), ConfigError> { - let update_session_req_res: Result = - serde_json::from_str(&input_json); - - match update_session_req_res { - Err(e) => Err(ConfigError::JsonFormat(e)), - Ok(c) => { - let server_conf = StorableSession { - session_token: c.session_token, - services: c.services, - domains: c.domains, - cache_routes: c.cache_routes, - } - .try_into(); + let location = match service_overwrite { + Some((_, location_overwrite)) => location_overwrite.clone(), + None => service.remote.clone(), + }; - match server_conf { - Err(e) => Err(e), - Ok(sc) => Ok((c.desired_name, sc)), - } - } + session_services.push(SessionService { + name: service.name.clone(), + location, + rewrites: service.rewrites.clone(), + }); } -} -pub fn create_preview_req_from_json(input_json: String) -> Result { - let update_session_req_res: Result = - serde_json::from_str(&input_json); - - match update_session_req_res { - Err(e) => Err(ConfigError::JsonFormat(e)), - Ok(c) => { - let server_conf = StorableSession { - session_token: String::from(PREVIEW_SESSION_TOKEN), - services: c.services, - domains: c.domains, - cache_routes: None, - } - .try_into(); - - match server_conf { - Err(e) => Err(e), - Ok(sc) => Ok(sc), - } - } + CreatePreviewRequest { + services: session_services, + domains: config.domains.clone(), + cache_routes: config.linkup.cache_routes.clone(), } } -fn validate_not_empty(server_config: &StorableSession) -> Result<(), ConfigError> { - if server_config.services.is_empty() { +fn validate_not_empty(session: &Session) -> Result<(), ConfigError> { + if session.services.is_empty() { return Err(ConfigError::Empty); } - if server_config.domains.is_empty() { + if session.domains.is_empty() { return Err(ConfigError::Empty); } Ok(()) } -fn validate_service_references(server_config: &StorableSession) -> Result<(), ConfigError> { - let service_names: HashSet<&str> = server_config - .services - .iter() - .map(|s| s.name.as_str()) - .collect(); +fn validate_services(session: &Session) -> Result<(), ConfigError> { + let mut service_names: HashSet<&str> = HashSet::new(); - for domain in &server_config.domains { + for service in &session.services { + validate_url_origin(&service.location)?; + + service_names.insert(&service.name); + } + + for domain in &session.domains { if !service_names.contains(&domain.default_service.as_str()) { return Err(ConfigError::NoSuchService( domain.default_service.to_string(), @@ -427,37 +264,6 @@ fn validate_url_origin(url: &Url) -> Result<(), ConfigError> { Ok(()) } -fn choose_domain_ordering(domains: Vec) -> Vec { - let mut sorted_domains = domains; - sorted_domains.sort_by(|a, b| { - let a_subdomains: Vec<&str> = a.split('.').collect(); - let b_subdomains: Vec<&str> = b.split('.').collect(); - - let a_len = a_subdomains.len(); - let b_len = b_subdomains.len(); - - if a_len != b_len { - b_len.cmp(&a_len) - } else { - a_subdomains - .iter() - .zip(b_subdomains.iter()) - .map(|(a_sub, b_sub)| b_sub.len().cmp(&a_sub.len())) - .find(|&ord| ord != Ordering::Equal) - .unwrap_or(Ordering::Equal) - } - }); - - sorted_domains -} - -pub fn session_to_json(session: Session) -> String { - let storable_session: StorableSession = session.into(); - - // This should never fail, due to previous validation - serde_json::to_string(&storable_session).unwrap() -} - #[cfg(test)] mod tests { use super::*; @@ -504,127 +310,75 @@ mod tests { "#; #[test] - fn test_convert_server_config() { + fn test_convert_session() { let input_str = String::from(CONF_STR); - let server_config_value = serde_json::from_str::(&input_str).unwrap(); - let server_config: Session = server_config_value.try_into().unwrap(); - check_means_same_as_input_conf(&server_config); + let session_value = serde_json::from_str::(&input_str).unwrap(); + let session: Session = session_value.try_into().unwrap(); + check_means_same_as_input_conf(&session); // Inverse should mean the same thing - let output_conf = session_to_json(server_config); - let output_conf_value = serde_json::from_str::(&output_conf).unwrap(); - let second_server_conf: Session = output_conf_value.try_into().unwrap(); - check_means_same_as_input_conf(&second_server_conf); + let output_session = serde_json::to_string(&session).unwrap(); + let output_session_value = + serde_json::from_str::(&output_session).unwrap(); + let second_session: Session = output_session_value.try_into().unwrap(); + check_means_same_as_input_conf(&second_session); } - fn check_means_same_as_input_conf(server_config: &Session) { + fn check_means_same_as_input_conf(session: &Session) { // Test services - assert_eq!(server_config.services.len(), 2); - assert!(server_config.services.contains_key("frontend")); - assert!(server_config.services.contains_key("backend")); + assert_eq!(session.services.len(), 2); + + let frontend_service = session.get_service("frontend").unwrap(); assert_eq!( - server_config.services.get("frontend").unwrap().origin, + frontend_service.location, Url::parse("http://localhost:8000").unwrap() ); + assert_eq!( - server_config.services.get("frontend").unwrap().rewrites[0] - .source - .as_str(), - "/foo/(.*)" - ); - assert_eq!( - server_config.services.get("frontend").unwrap().rewrites[0].target, - "/bar/$1" + Some(1), + frontend_service + .rewrites + .as_ref() + .map(|rewrites| rewrites.len()) ); + + let frontend_service_rewrite = &frontend_service.rewrites.as_ref().unwrap()[0]; + assert_eq!(frontend_service_rewrite.source.as_str(), "/foo/(.*)"); + assert_eq!(frontend_service_rewrite.target, "/bar/$1"); + + let backend_service = session.get_service("backend").unwrap(); assert_eq!( - server_config.services.get("backend").unwrap().origin, + backend_service.location, Url::parse("http://localhost:8001").unwrap() ); - assert!(server_config - .services - .get("backend") - .unwrap() - .rewrites - .is_empty()); + assert!(backend_service.rewrites.is_none()); // Test domains - assert_eq!(server_config.domains.len(), 2); - assert!(server_config.domains.contains_key("example.com")); - assert!(server_config.domains.contains_key("api.example.com")); - assert_eq!( - server_config - .domains - .get("example.com") - .unwrap() - .default_service, - "frontend" - ); - assert_eq!( - server_config.domains.get("example.com").unwrap().routes[0] - .path - .as_str(), - "/api/v1/.*" - ); - assert_eq!( - server_config.domains.get("example.com").unwrap().routes[0].service, - "backend" - ); + assert_eq!(2, session.domains.len()); + + let example_domain = session.get_domain("example.com").unwrap(); + assert_eq!(example_domain.default_service, "frontend"); + assert_eq!( - server_config - .domains - .get("api.example.com") - .unwrap() - .default_service, - "backend" + Some(1), + example_domain.routes.as_ref().map(|routes| routes.len()) ); - assert!(server_config - .domains - .get("api.example.com") - .unwrap() - .routes - .is_empty()); - - assert_eq!(server_config.cache_routes.as_ref().unwrap().len(), 1); + + let example_domain_route = &example_domain.routes.as_ref().unwrap()[0]; + assert_eq!(example_domain_route.path.as_str(), "/api/v1/.*"); + assert_eq!(example_domain_route.service, "backend"); + + let api_domain = session.get_domain("api.example.com").unwrap(); + assert_eq!(api_domain.default_service, "backend"); + assert!(api_domain.routes.is_none()); + + // Test cache routes + + assert_eq!(session.cache_routes.as_ref().unwrap().len(), 1); assert_eq!( - server_config.cache_routes.as_ref().unwrap()[0].as_str(), + session.cache_routes.as_ref().unwrap()[0].as_str(), "/static/.*" ); } - - #[test] - fn test_choose_domain_ordering() { - let input = vec![ - "example.com".to_string(), - "api.example.com".to_string(), - "render-api.example.com".to_string(), - "another-example.com".to_string(), - ]; - - let expected_output = vec![ - "render-api.example.com".to_string(), - "api.example.com".to_string(), - "another-example.com".to_string(), - "example.com".to_string(), - ]; - - assert_eq!(choose_domain_ordering(input), expected_output); - } - - #[test] - fn test_choose_domain_ordering_with_same_length() { - let input = vec![ - "a.domain.com".to_string(), - "b.domain.com".to_string(), - "c.domain.com".to_string(), - ]; - - let expected_output = vec![ - "a.domain.com".to_string(), - "b.domain.com".to_string(), - "c.domain.com".to_string(), - ]; - - assert_eq!(choose_domain_ordering(input), expected_output); - } } diff --git a/linkup/src/session_allocator.rs b/linkup/src/session_allocator.rs index 55b2999f..abff21a2 100644 --- a/linkup/src/session_allocator.rs +++ b/linkup/src/session_allocator.rs @@ -1,7 +1,7 @@ use crate::{ - extract_tracestate_session, first_subdomain, headers::HeaderName, - name_gen::deterministic_six_char_hash, random_animal, random_six_char, session_to_json, ConfigError, HeaderMap, NameKind, Session, SessionError, StringStore, + extract_tracestate_session, first_subdomain, headers::HeaderName, random_animal, + random_six_char, }; pub struct SessionAllocator<'a, S: StringStore> { @@ -19,37 +19,34 @@ impl<'a, S: StringStore> SessionAllocator<'a, S> { headers: &HeaderMap, ) -> Result<(String, Session), SessionError> { let url_name = first_subdomain(url); - if let Some(config) = self.get_session_config(url_name.to_string()).await? { + if let Some(config) = self.get_session_config(&url_name).await? { return Ok((url_name, config)); } if let Some(forwarded_host) = headers.get(HeaderName::ForwardedHost) { let forwarded_host_name = first_subdomain(forwarded_host); - if let Some(config) = self - .get_session_config(forwarded_host_name.to_string()) - .await? - { + if let Some(config) = self.get_session_config(&forwarded_host_name).await? { return Ok((forwarded_host_name, config)); } } if let Some(referer) = headers.get(HeaderName::Referer) { let referer_name = first_subdomain(referer); - if let Some(config) = self.get_session_config(referer_name.to_string()).await? { + if let Some(config) = self.get_session_config(&referer_name).await? { return Ok((referer_name, config)); } } if let Some(origin) = headers.get(HeaderName::Origin) { let origin_name = first_subdomain(origin); - if let Some(config) = self.get_session_config(origin_name.to_string()).await? { + if let Some(config) = self.get_session_config(&origin_name).await? { return Ok((origin_name, config)); } } if let Some(tracestate) = headers.get(HeaderName::TraceState) { let trace_name = extract_tracestate_session(tracestate); - if let Some(config) = self.get_session_config(trace_name.to_string()).await? { + if let Some(config) = self.get_session_config(&trace_name).await? { return Ok((trace_name, config)); } } @@ -59,45 +56,41 @@ impl<'a, S: StringStore> SessionAllocator<'a, S> { pub async fn store_session( &self, - config: Session, + session: Session, name_kind: NameKind, - desired_name: String, + desired_name: &str, ) -> Result { - let config_str = session_to_json(config.clone()); - let name = self - .choose_name(desired_name, config.session_token, name_kind, &config_str) + .choose_name(desired_name, &session.session_token, name_kind, &session) .await?; - self.store.put(name.clone(), config_str).await?; + let serialized_session = serde_json::to_string(&session) + .map_err(|error| SessionError::ConfigErr(error.to_string()))?; + + self.store.put(&name, &serialized_session).await?; Ok(name) } async fn choose_name( &self, - desired_name: String, - session_token: String, + desired_name: &str, + session_token: &str, name_kind: NameKind, - config_json: &str, + session: &Session, ) -> Result { - if desired_name.is_empty() { - return self - .new_session_name(name_kind, desired_name, config_json) - .await; - } - - if let Some(session) = self.get_session_config(desired_name.clone()).await? { - if session.session_token == session_token { - return Ok(desired_name); - } + if !desired_name.is_empty() + && let Some(session) = self.get_session_config(desired_name).await? + && session.session_token == session_token + { + return Ok(desired_name.to_owned()); } - self.new_session_name(name_kind, desired_name, config_json) + self.new_session_name(name_kind, desired_name, session) .await } - async fn get_session_config(&self, name: String) -> Result, SessionError> { + async fn get_session_config(&self, name: &str) -> Result, SessionError> { let value = match self.store.get(name).await { Ok(Some(v)) => v, Ok(None) => return Ok(None), @@ -117,17 +110,17 @@ impl<'a, S: StringStore> SessionAllocator<'a, S> { async fn new_session_name( &self, name_kind: NameKind, - desired_name: String, - config_json: &str, + desired_name: &str, + session: &Session, ) -> Result { if name_kind == NameKind::SixChar { - return Ok(deterministic_six_char_hash(config_json)); + return Ok(session.sha()[..6].to_string()); } let mut key = String::new(); - if !desired_name.is_empty() && !self.store.exists(desired_name.clone()).await? { - key = desired_name; + if !desired_name.is_empty() && !self.store.exists(desired_name).await? { + key = desired_name.to_owned(); } if key.is_empty() { @@ -140,7 +133,7 @@ impl<'a, S: StringStore> SessionAllocator<'a, S> { random_six_char() }; - if !self.store.exists(generated_key.clone()).await? { + if !self.store.exists(&generated_key).await? { key = generated_key; break; } @@ -156,7 +149,7 @@ impl<'a, S: StringStore> SessionAllocator<'a, S> { ) -> Result { for _ in 0..max_attempts { let generated_key = random_animal(); - if !self.store.exists(generated_key.clone()).await? { + if !self.store.exists(&generated_key).await? { return Ok(generated_key); } } @@ -168,7 +161,7 @@ impl<'a, S: StringStore> SessionAllocator<'a, S> { #[cfg(test)] mod tests { use super::*; - use crate::{create_preview_req_from_json, MemoryStringStore}; + use crate::{CreatePreviewRequest, MemoryStringStore}; #[tokio::test] async fn identical_preview_requests_reuse_same_name() { @@ -201,15 +194,19 @@ mod tests { }) .to_string(); - let first_session = create_preview_req_from_json(request_json.clone()).unwrap(); - let second_session = create_preview_req_from_json(request_json).unwrap(); + let first_session = + Session::try_from(serde_json::from_str::(&request_json).unwrap()) + .unwrap(); + + let mut second_session = first_session.clone(); + second_session.services.reverse(); let first_name = allocator - .store_session(first_session, NameKind::SixChar, String::new()) + .store_session(first_session, NameKind::SixChar, "") .await .unwrap(); let second_name = allocator - .store_session(second_session, NameKind::SixChar, String::new()) + .store_session(second_session, NameKind::SixChar, "") .await .unwrap(); diff --git a/local-server/Cargo.toml b/local-server/Cargo.toml index a476930d..c62eb0f9 100644 --- a/local-server/Cargo.toml +++ b/local-server/Cargo.toml @@ -1,16 +1,17 @@ [package] name = "linkup-local-server" version = "0.1.0" -edition = "2021" +edition = "2024" [lib] name = "linkup_local_server" path = "src/lib.rs" [dependencies] -axum = { version = "0.8.1", features = ["http2", "json", "ws"] } +axum = { workspace = true, features = ["http2", "json", "ws"] } axum-server = { version = "0.8.0", features = ["tls-rustls"] } -http = "1.2.0" +async-trait = { workspace = true } +http = { workspace = true } hickory-server = { version = "0.25.1", features = ["resolver"] } hyper = { version = "1.5.2", features = ["server"] } hyper-rustls = { version = "0.27.5", default-features = false, features = [ @@ -18,17 +19,19 @@ hyper-rustls = { version = "0.27.5", default-features = false, features = [ "ring", ] } hyper-util = { version = "0.1.10", features = ["client-legacy"] } -futures = "0.3.31" -linkup = { path = "../linkup" } +futures = { workspace = true } +linkup = { workspace = true } rustls = { version = "0.23.37", default-features = false, features = ["ring"] } rustls-native-certs = "0.8.1" -thiserror = "2.0.11" -tokio = { version = "1.49.0", features = [ +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = [ "macros", "signal", "rt-multi-thread", ] } -tokio-tungstenite = { version = "0.28.0", features = [ +tokio-tungstenite = { workspace = true, features = [ "rustls-tls-native-roots", ] } tower-http = { version = "0.6.2", features = ["trace"] } diff --git a/local-server/src/certificates/mod.rs b/local-server/src/certificates/mod.rs index 66594a83..4a3c1f6f 100644 --- a/local-server/src/certificates/mod.rs +++ b/local-server/src/certificates/mod.rs @@ -90,8 +90,8 @@ pub fn setup_self_signed_certificates( if !is_nss_installed() { println!("It seems like you have Firefox installed."); println!( - "For self-signed certificates to work with Firefox, you need to have nss installed." - ); + "For self-signed certificates to work with Firefox, you need to have nss installed." + ); let nss_url = if cfg!(target_os = "macos") { "`brew install nss`" } else { diff --git a/local-server/src/lib.rs b/local-server/src/lib.rs index 0a726d88..a8f3f696 100644 --- a/local-server/src/lib.rs +++ b/local-server/src/lib.rs @@ -1,13 +1,14 @@ use axum::{ + Extension, Router, body::Body, extract::{DefaultBodyLimit, Json, Request}, http::StatusCode, response::{IntoResponse, Response}, routing::{any, get, post}, - Extension, Router, }; use axum_server::tls_rustls::RustlsConfig; use hickory_server::{ + ServerFuture, authority::{Catalog, ZoneType}, proto::{ rr::{Name, RData, Record}, @@ -17,29 +18,31 @@ use hickory_server::{ config::{NameServerConfig, NameServerConfigGroup, ResolverOpts}, name_server::TokioConnectionProvider, }, + server::{RequestHandler, ResponseHandler, ResponseInfo}, store::{ forwarder::{ForwardAuthority, ForwardConfig}, in_memory::InMemoryAuthority, }, - ServerFuture, }; -use http::{header::HeaderMap, HeaderName, HeaderValue, Uri}; +use http::{HeaderName, HeaderValue, Uri, header::HeaderMap}; use hyper_rustls::HttpsConnector; use hyper_util::{ - client::legacy::{connect::HttpConnector, Client}, + client::legacy::{Client, connect::HttpConnector}, rt::TokioExecutor, }; use linkup::{ - allow_all_cors, get_additional_headers, get_target_service, MemoryStringStore, NameKind, - Session, SessionAllocator, TargetService, UpdateSessionRequest, + MemoryStringStore, NameKind, Session, SessionAllocator, TargetService, UpdateSessionRequest, + allow_all_cors, get_additional_headers, get_target_service, }; use rustls::ServerConfig; use std::{ net::{Ipv4Addr, SocketAddr}, + ops::Deref, + path::PathBuf, str::FromStr, }; use std::{path::Path, sync::Arc}; -use tokio::{net::UdpSocket, signal}; +use tokio::{net::UdpSocket, select, signal, sync::RwLock}; use tokio_tungstenite::tungstenite::client::IntoClientRequest; use tower::ServiceBuilder; use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer}; @@ -79,7 +82,43 @@ impl IntoResponse for ApiError { } } -pub fn linkup_router(config_store: MemoryStringStore) -> Router { +#[derive(Clone)] +pub struct DnsCatalog(Arc>); + +impl DnsCatalog { + pub fn new() -> Self { + Self(Arc::new(RwLock::new(Catalog::new()))) + } +} + +impl Default for DnsCatalog { + fn default() -> Self { + Self::new() + } +} + +impl Deref for DnsCatalog { + type Target = Arc>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[async_trait::async_trait] +impl RequestHandler for DnsCatalog { + async fn handle_request( + &self, + request: &hickory_server::server::Request, + response_handle: R, + ) -> ResponseInfo { + let catalog = self.read().await; + + catalog.handle_request(request, response_handle).await + } +} + +pub fn linkup_router(config_store: MemoryStringStore, dns_catalog: DnsCatalog) -> Router { let client = https_client(); Router::new() @@ -87,6 +126,7 @@ pub fn linkup_router(config_store: MemoryStringStore) -> Router { .route("/linkup/check", get(always_ok)) .fallback(any(linkup_request_handler)) .layer(Extension(config_store)) + .layer(Extension(dns_catalog)) .layer(Extension(client)) .layer( ServiceBuilder::new() @@ -99,7 +139,34 @@ pub fn linkup_router(config_store: MemoryStringStore) -> Router { ) } -pub async fn start_server_https(config_store: MemoryStringStore, certs_dir: &Path) { +pub async fn start(config_store: MemoryStringStore, certs_dir: &Path) { + let dns_catalog = DnsCatalog::new(); + + let http_config_store = config_store.clone(); + let https_config_store = config_store.clone(); + let https_certs_dir = PathBuf::from(certs_dir); + + select! { + () = start_server_http(http_config_store, dns_catalog.clone()) => { + println!("HTTP server shut down"); + }, + () = start_server_https(https_config_store, &https_certs_dir, dns_catalog.clone()) => { + println!("HTTPS server shut down"); + }, + () = start_dns_server(dns_catalog.clone()) => { + println!("DNS server shut down"); + }, + () = shutdown_signal() => { + println!("Shutdown signal received, stopping all servers"); + } + } +} + +async fn start_server_https( + config_store: MemoryStringStore, + certs_dir: &Path, + dns_catalog: DnsCatalog, +) { let _ = rustls::crypto::ring::default_provider().install_default(); let sni = match certificates::WildcardSniResolver::load_dir(certs_dir) { @@ -118,10 +185,10 @@ pub async fn start_server_https(config_store: MemoryStringStore, certs_dir: &Pat .with_cert_resolver(Arc::new(sni)); server_config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; - let app = linkup_router(config_store); + let app = linkup_router(config_store, dns_catalog); let addr = SocketAddr::from(([0, 0, 0, 0], 443)); - println!("listening on {}", &addr); + println!("HTTPS listening on {}", &addr); axum_server::bind_rustls(addr, RustlsConfig::from_config(Arc::new(server_config))) .serve(app.into_make_service()) @@ -129,39 +196,22 @@ pub async fn start_server_https(config_store: MemoryStringStore, certs_dir: &Pat .expect("failed to start HTTPS server"); } -pub async fn start_server_http(config_store: MemoryStringStore) -> std::io::Result<()> { - let app = linkup_router(config_store); +async fn start_server_http(config_store: MemoryStringStore, dns_catalog: DnsCatalog) { + let app = linkup_router(config_store, dns_catalog); let addr = SocketAddr::from(([0, 0, 0, 0], 80)); - println!("listening on {}", &addr); + println!("HTTP listening on {}", &addr); - let listener = tokio::net::TcpListener::bind(addr).await?; - axum::serve(listener, app) - .with_graceful_shutdown(shutdown_signal()) - .await?; + let listener = tokio::net::TcpListener::bind(addr) + .await + .expect("failed to bind to address"); - Ok(()) + axum::serve(listener, app) + .await + .expect("failed to start HTTP server"); } -pub async fn start_dns_server(linkup_session_name: String, domains: Vec) { - let mut catalog = Catalog::new(); - - for domain in &domains { - let record_name = Name::from_str(&format!("{linkup_session_name}.{domain}.")).unwrap(); - - let authority = InMemoryAuthority::empty(record_name.clone(), ZoneType::Primary, false); - - let record = Record::from_rdata( - record_name.clone(), - 3600, - RData::A(Ipv4Addr::new(127, 0, 0, 1).into()), - ); - - authority.upsert(record, 0).await; - - catalog.upsert(record_name.clone().into(), vec![Arc::new(authority)]); - } - +async fn start_dns_server(dns_catalog: DnsCatalog) { let cf_name_server = NameServerConfig::new("1.1.1.1:53".parse().unwrap(), Protocol::Udp); let forward_config = ForwardConfig { name_servers: NameServerConfigGroup::from(vec![cf_name_server]), @@ -174,12 +224,15 @@ pub async fn start_dns_server(linkup_session_name: String, domains: Vec) .build() .unwrap(); - catalog.upsert(Name::root().into(), vec![Arc::new(forwarder)]); + { + let mut catalog = dns_catalog.write().await; + catalog.upsert(Name::root().into(), vec![Arc::new(forwarder)]); + } let addr = SocketAddr::from(([0, 0, 0, 0], 8053)); let sock = UdpSocket::bind(&addr).await.unwrap(); - let mut server = ServerFuture::new(catalog); + let mut server = ServerFuture::new(dns_catalog); server.register_socket(sock); println!("listening on {addr}"); @@ -249,11 +302,12 @@ async fn linkup_request_handler( let mut cookie_values: Vec = Vec::new(); for (key, value) in req.headers() { if key == http::header::COOKIE { - if let Ok(cookie_value) = value.to_str().map(str::trim) { - if !cookie_value.is_empty() { - cookie_values.push(cookie_value.to_string()); - } + if let Ok(cookie_value) = value.to_str().map(str::trim) + && !cookie_value.is_empty() + { + cookie_values.push(cookie_value.to_string()); } + continue; } @@ -351,7 +405,7 @@ async fn handle_http_req( ), StatusCode::BAD_GATEWAY, ) - .into_response() + .into_response(); } }; @@ -362,9 +416,16 @@ async fn handle_http_req( async fn linkup_config_handler( Extension(store): Extension, + Extension(dns_catalog): Extension, Json(update_req): Json, ) -> impl IntoResponse { let desired_name = update_req.desired_name.clone(); + let domains = update_req + .domains + .iter() + .map(|domain| domain.domain.clone()) + .collect::>(); + let server_conf: Session = match update_req.try_into() { Ok(conf) => conf, Err(e) => { @@ -372,36 +433,56 @@ async fn linkup_config_handler( format!("Failed to parse server config: {} - local server", e), StatusCode::BAD_REQUEST, ) - .into_response() + .into_response(); } }; let sessions = SessionAllocator::new(&store); - let session_name = sessions - .store_session(server_conf, NameKind::Animal, desired_name) + let session_name_result = sessions + .store_session(server_conf, NameKind::Animal, &desired_name) .await; - let name = match session_name { + let session_name = match session_name_result { Ok(session_name) => session_name, Err(e) => { return ApiError::new( format!("Failed to store server config: {}", e), StatusCode::INTERNAL_SERVER_ERROR, ) - .into_response() + .into_response(); } }; - (StatusCode::OK, name).into_response() + for domain in &domains { + let full_domain = format!("{session_name}.{domain}"); + + register_dns_record(&dns_catalog, &full_domain).await; + } + + (StatusCode::OK, session_name).into_response() } async fn always_ok() -> &'static str { "OK" } -async fn shutdown_signal() { - let _ = signal::ctrl_c().await; - println!("signal received, starting graceful shutdown"); +async fn register_dns_record(dns_catalog: &DnsCatalog, domain: &str) { + let mut catalog = dns_catalog.write().await; + + let record_name = Name::from_str(&format!("{}.", domain)) + .expect("dns record from domain should always succeed"); + + let authority = InMemoryAuthority::empty(record_name.clone(), ZoneType::Primary, false); + + let record = Record::from_rdata( + record_name.clone(), + 3600, + RData::A(Ipv4Addr::new(127, 0, 0, 1).into()), + ); + + authority.upsert(record, 0).await; + + catalog.upsert(record_name.clone().into(), vec![Arc::new(authority)]); } fn https_client() -> HttpsClient { @@ -425,3 +506,27 @@ fn https_client() -> HttpsClient { Client::builder(TokioExecutor::new()).build(https) } + +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to start SIGINT handler"); + }; + + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to start SIGTERM handler") + .recv() + .await; + }; + + tokio::select! { + () = ctrl_c => { + println!("Received SIGINT signal"); + }, + () = terminate => { + println!("Received SIGTERM signal"); + }, + } +} diff --git a/local-server/src/ws.rs b/local-server/src/ws.rs index da66b6b9..bb730f2c 100644 --- a/local-server/src/ws.rs +++ b/local-server/src/ws.rs @@ -1,12 +1,12 @@ use std::{future::Future, pin::Pin}; -use axum::extract::{ws::WebSocket, FromRequestParts, WebSocketUpgrade}; +use axum::extract::{FromRequestParts, WebSocketUpgrade, ws::WebSocket}; use futures::{SinkExt, StreamExt}; -use http::{request::Parts, StatusCode}; +use http::{StatusCode, request::Parts}; use tokio::net::TcpStream; use tokio_tungstenite::{ - tungstenite::{self, Message}, MaybeTlsStream, WebSocketStream, + tungstenite::{self, Message}, }; pub struct ExtractOptionalWebSocketUpgrade(pub Option); @@ -76,7 +76,7 @@ pub fn context_handle_socket( ) -> WrappedSocketHandler { Box::new(move |downstream: WebSocket| { Box::pin(async move { - use futures::future::{select, Either}; + use futures::future::{Either, select}; let (mut upstream_write, mut upstream_read) = upstream_ws.split(); let (mut downstream_write, mut downstream_read) = downstream.split(); diff --git a/server-tests/Cargo.toml b/server-tests/Cargo.toml index d1282d0e..db58b9c8 100644 --- a/server-tests/Cargo.toml +++ b/server-tests/Cargo.toml @@ -1,20 +1,20 @@ [package] name = "linkup-server-tests" version = "0.1.0" -edition = "2021" +edition = "2024" [dev-dependencies] -linkup = { path = "../linkup" } -linkup-local-server = { path = "../local-server" } -http = "1.2.0" -reqwest = { version = "0.12.12", default-features = false, features = [ +linkup = { workspace = true } +linkup-local-server = { workspace = true } +http = { workspace = true } +reqwest = { workspace = true, features = [ "blocking", - "rustls-tls", + "rustls", ] } -anyhow = "1.0.95" -futures = "0.3.31" -axum = { version = "0.8.1", features = ["ws"] } -serde_json = "1.0.137" -tokio-tungstenite = "0.26.1" -tokio = { version = "1.43.0", features = ["test-util", "macros"] } +anyhow = { workspace = true } +futures = { workspace = true } +axum = { workspace = true, features = ["ws"] } +serde_json = { workspace = true } +tokio-tungstenite = { workspace = true } +tokio = { workspace = true, features = ["test-util", "macros"] } rstest = "0.24.0" diff --git a/server-tests/tests/helpers.rs b/server-tests/tests/helpers.rs index b44d40a0..b37605ad 100644 --- a/server-tests/tests/helpers.rs +++ b/server-tests/tests/helpers.rs @@ -1,7 +1,7 @@ use std::process::Command; -use linkup::{MemoryStringStore, StorableDomain, StorableService, UpdateSessionRequest}; -use linkup_local_server::linkup_router; +use linkup::{Domain, MemoryStringStore, SessionService, UpdateSessionRequest}; +use linkup_local_server::{DnsCatalog, linkup_router}; use reqwest::Url; use tokio::net::TcpListener; @@ -14,7 +14,7 @@ pub enum ServerKind { pub async fn setup_server(kind: ServerKind) -> String { match kind { ServerKind::Local => { - let app = linkup_router(MemoryStringStore::default()); + let app = linkup_router(MemoryStringStore::default(), DnsCatalog::new()); // Bind to a random port assigned by the OS let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -57,12 +57,12 @@ pub fn create_session_request(name: String, fe_location: Option) -> Stri let req = UpdateSessionRequest { desired_name: name, session_token: "token".to_string(), - domains: vec![StorableDomain { + domains: vec![Domain { domain: "example.com".to_string(), default_service: "frontend".to_string(), routes: None, }], - services: vec![StorableService { + services: vec![SessionService { name: "frontend".to_string(), location: Url::parse(&location).unwrap(), rewrites: None, diff --git a/server-tests/tests/http_test.rs b/server-tests/tests/http_test.rs index f2cc662b..e3c8dd08 100644 --- a/server-tests/tests/http_test.rs +++ b/server-tests/tests/http_test.rs @@ -1,10 +1,10 @@ use axum::{ + Router, response::{AppendHeaders, Redirect}, routing::{any, get}, - Router, }; use helpers::ServerKind; -use http::{header::SET_COOKIE, StatusCode}; +use http::{StatusCode, header::SET_COOKIE}; use rstest::rstest; use tokio::net::TcpListener; diff --git a/server-tests/tests/server_test.rs b/server-tests/tests/server_test.rs index 3a560aab..2ca38cdc 100644 --- a/server-tests/tests/server_test.rs +++ b/server-tests/tests/server_test.rs @@ -1,5 +1,5 @@ use helpers::ServerKind; -use linkup::{CreatePreviewRequest, StorableDomain, StorableService}; +use linkup::{CreatePreviewRequest, Domain, SessionService}; use reqwest::Url; use rstest::rstest; @@ -85,12 +85,12 @@ pub fn create_preview_request(fe_location: Option) -> String { None => "http://example.com".to_string(), }; let req = CreatePreviewRequest { - domains: vec![StorableDomain { + domains: vec![Domain { domain: "example.com".to_string(), default_service: "frontend".to_string(), routes: None, }], - services: vec![StorableService { + services: vec![SessionService { name: "frontend".to_string(), location: Url::parse(&location).unwrap(), rewrites: None, diff --git a/server-tests/tests/ws_test.rs b/server-tests/tests/ws_test.rs index 51469ed6..14cb0deb 100644 --- a/server-tests/tests/ws_test.rs +++ b/server-tests/tests/ws_test.rs @@ -1,8 +1,8 @@ use std::str::FromStr; +use axum::Router; use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; use axum::response::IntoResponse; -use axum::Router; use futures::{SinkExt, StreamExt}; use helpers::ServerKind; use http::{HeaderName, HeaderValue}; diff --git a/worker/Cargo.toml b/worker/Cargo.toml index 7b8cce64..0f95af39 100644 --- a/worker/Cargo.toml +++ b/worker/Cargo.toml @@ -1,31 +1,26 @@ [package] name = "linkup-worker" version = "0.1.0" -edition = "2021" +edition = "2024" [lib] crate-type = ["cdylib"] [dependencies] -axum = { version = "0.8.1", features = [ +axum = { workspace = true, features = [ "json", "query", -], default-features = false } -base64 = "0.22.1" +] } +base64 = { workspace = true } console_error_panic_hook = { version = "0.1.7" } -cloudflare = { path = "../cloudflare" } -futures = "0.3.31" +cloudflare = { workspace = true, features = ["default-tls"] } +futures = { workspace = true } getrandom = { version = "0.4.1", features = ["wasm_js"] } -http = "1.2.0" -linkup = { path = "../linkup" } -regex = "1.11.1" -serde = "1" -serde_json = "1.0.138" +http = { workspace = true } +linkup = { workspace = true } +regex = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } tower-service = "0.3.3" worker = { version = "0.5.0", features = ["http", "axum"] } -reqwest = "0.12.12" - -# These are required to use worker-rs DurableObjects -async-trait = "0.1.86" -wasm-bindgen = "0.2.100" -wasm-bindgen-futures = "0.4.50" +reqwest = { workspace = true, features = ["default-tls"] } diff --git a/worker/src/kv_store.rs b/worker/src/kv_store.rs index 994a59a4..f16f9648 100644 --- a/worker/src/kv_store.rs +++ b/worker/src/kv_store.rs @@ -12,15 +12,15 @@ impl CfWorkerStringStore { } impl StringStore for CfWorkerStringStore { - async fn get(&self, key: String) -> Result, SessionError> { - match self.kv.get(key.as_str()).text().await { + async fn get(&self, key: &str) -> Result, SessionError> { + match self.kv.get(key).text().await { Ok(v) => Ok(v), Err(e) => Err(SessionError::GetError(e.to_string())), } } - async fn exists(&self, key: String) -> Result { - let value = match self.kv.get(key.as_str()).text().await { + async fn exists(&self, key: &str) -> Result { + let value = match self.kv.get(key).text().await { Ok(v) => Ok(v), Err(e) => return Err(SessionError::GetError(e.to_string())), }?; @@ -31,8 +31,8 @@ impl StringStore for CfWorkerStringStore { } } - async fn put(&self, key: String, value: String) -> Result<(), SessionError> { - let mut put = match self.kv.put(&key, value) { + async fn put(&self, key: &str, value: &str) -> Result<(), SessionError> { + let mut put = match self.kv.put(key, value) { Ok(p) => p, Err(e) => return Err(SessionError::PutError(e.to_string())), }; diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 83c8ec1c..18a45d49 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -1,23 +1,23 @@ use axum::{ + Router, extract::{Json, Query, Request, State}, http::StatusCode, - middleware::{from_fn_with_state, Next}, + middleware::{Next, from_fn_with_state}, response::IntoResponse, routing::{any, get, post}, - Router, }; use http::{HeaderMap, Uri}; use http_error::HttpError; use kv_store::CfWorkerStringStore; use linkup::{ - allow_all_cors, get_additional_headers, get_target_service, CreatePreviewRequest, NameKind, - Session, SessionAllocator, UpdateSessionRequest, Version, VersionChannel, + CreatePreviewRequest, NameKind, Session, SessionAllocator, UpdateSessionRequest, Version, + VersionChannel, allow_all_cors, get_additional_headers, get_target_service, }; use serde::{Deserialize, Serialize}; use tower_service::Service; use worker::{ - console_error, console_log, console_warn, event, kv::KvStore, Env, Fetch, HttpRequest, - HttpResponse, + Env, Fetch, HttpRequest, HttpResponse, console_error, console_log, console_warn, event, + kv::KvStore, }; use ws::handle_ws_resp; @@ -93,7 +93,6 @@ pub fn linkup_router(state: LinkupState) -> Router { .route("/linkup/tunnel", get(get_tunnel_handler)) .route("/linkup/check", get(always_ok)) .route("/linkup/no-tunnel", get(no_tunnel)) - .route("/linkup", any(deprecated_linkup_session_handler)) .route_layer(from_fn_with_state(state.clone(), authenticate)) // Fallback for all other requests .fallback(any(linkup_request_handler)) @@ -145,8 +144,7 @@ async fn get_tunnel_handler( let cf_client = cloudflare_client(&state.cloudflare.api_token); let tunnel_prefix = - match cloudflare::linkup::tunnel_prefix(&cf_client, &state.cloudflare.tunnel_zone_id).await - { + match tunnel::tunnel_prefix(&cf_client, &state.cloudflare.tunnel_zone_id).await { Ok(prefix) => prefix, Err(error) => { console_error!("Failed resolve tunnel prefix: {}", error); @@ -207,15 +205,15 @@ async fn linkup_session_handler( Ok(conf) => conf, Err(e) => { return HttpError::new( - format!("Failed to parse server config: {} - local server", e), + format!("Failed to parse server config: {} - Worker", e), StatusCode::BAD_REQUEST, ) - .into_response() + .into_response(); } }; let session_name = sessions - .store_session(server_conf, NameKind::Animal, desired_name) + .store_session(server_conf, NameKind::Animal, &desired_name) .await; let name = match session_name { @@ -225,7 +223,7 @@ async fn linkup_session_handler( format!("Failed to store server config: {}", e), StatusCode::INTERNAL_SERVER_ERROR, ) - .into_response() + .into_response(); } }; @@ -244,15 +242,15 @@ async fn linkup_preview_handler( Ok(conf) => conf, Err(e) => { return HttpError::new( - format!("Failed to parse server config: {} - local server", e), + format!("Failed to parse server config: {} - Worker", e), StatusCode::BAD_REQUEST, ) - .into_response() + .into_response(); } }; let session_name = sessions - .store_session(server_conf, NameKind::SixChar, String::from("")) + .store_session(server_conf, NameKind::SixChar, "") .await; let name = match session_name { @@ -262,7 +260,7 @@ async fn linkup_preview_handler( format!("Failed to store server config: {}", e), StatusCode::INTERNAL_SERVER_ERROR, ) - .into_response() + .into_response(); } }; @@ -295,8 +293,7 @@ async fn linkup_request_handler( Ok(session) => session, Err(_) => { return HttpError::new( - "Linkup was unable to determine the session origin of the request. - Make sure your request includes a valid session ID in the referer or tracestate headers. - Local Server".to_string(), + "Linkup was unable to determine the session origin of the request.\nMake sure your request includes a valid session ID in the referer or tracestate headers. - Worker".to_string(), StatusCode::UNPROCESSABLE_ENTITY, ) .into_response() @@ -307,9 +304,7 @@ async fn linkup_request_handler( Some(result) => result, None => { return HttpError::new( - "The request belonged to a session, but there was no target for the request. - Check your routing rules in the linkup config for a match. - Local Server" - .to_string(), + "The request belonged to a session, but there was no target for the request.\nCheck your routing rules in the linkup config for a match. - Worker".to_string(), StatusCode::NOT_FOUND, ) .into_response() @@ -337,26 +332,26 @@ async fn linkup_request_handler( format!("Failed to parse request: {}", e), StatusCode::BAD_REQUEST, ) - .into_response() + .into_response(); } }; let cacheable_req = is_cacheable_request(&upstream_request, &config); let cache_key = get_cache_key(&upstream_request, &session_name).unwrap_or_default(); - if cacheable_req { - if let Some(upstream_response) = get_cached_req(cache_key.clone()).await { - let resp: HttpResponse = match upstream_response.try_into() { - Ok(resp) => resp, - Err(e) => { - return HttpError::new( - format!("Failed to parse cached response: {}", e), - StatusCode::BAD_GATEWAY, - ) - .into_response() - } - }; - return resp.into_response(); - } + + if cacheable_req && let Some(upstream_response) = get_cached_req(cache_key.clone()).await { + let resp: HttpResponse = match upstream_response.try_into() { + Ok(resp) => resp, + Err(e) => { + return HttpError::new( + format!("Failed to parse cached response: {}", e), + StatusCode::BAD_GATEWAY, + ) + .into_response(); + } + }; + + return resp.into_response(); } let mut upstream_response = match Fetch::Request(upstream_request).send().await { @@ -366,7 +361,7 @@ async fn linkup_request_handler( format!("Failed to fetch from target service: {}", e), StatusCode::BAD_GATEWAY, ) - .into_response() + .into_response(); } }; @@ -381,7 +376,7 @@ async fn linkup_request_handler( format!("Failed to clone response: {}", e), StatusCode::BAD_GATEWAY, ) - .into_response() + .into_response(); } }; if let Err(e) = set_cached_req(cache_key, cache_clone).await { @@ -458,7 +453,7 @@ async fn handle_http_resp(worker_resp: worker::Response) -> impl IntoResponse { format!("Failed to parse response: {}", e), StatusCode::BAD_GATEWAY, ) - .into_response() + .into_response(); } }; resp.headers_mut().extend(allow_all_cors()); @@ -506,12 +501,12 @@ async fn set_cached_req(cache_key: String, resp: worker::Response) -> worker::Re Ok(()) } -fn cloudflare_client(api_token: &str) -> cloudflare::framework::async_api::Client { - cloudflare::framework::async_api::Client::new( +fn cloudflare_client(api_token: &str) -> cloudflare::framework::client::async_api::Client { + cloudflare::framework::client::async_api::Client::new( cloudflare::framework::auth::Credentials::UserAuthToken { token: api_token.to_string(), }, - cloudflare::framework::HttpApiClientConfig::default(), + cloudflare::framework::client::ClientConfig::default(), cloudflare::framework::Environment::Production, ) .expect("Cloudflare API Client to have been created") @@ -591,12 +586,3 @@ async fn authenticate( next.run(request).await } - -#[worker::send] -async fn deprecated_linkup_session_handler() -> impl IntoResponse { - ( - StatusCode::NOT_FOUND, - "This endpoint was deprecated in linkup 2.0, please check that your cli is up to date", - ) - .into_response() -} diff --git a/worker/src/tunnel.rs b/worker/src/tunnel.rs index cb0ae5c0..54dd773f 100644 --- a/worker/src/tunnel.rs +++ b/worker/src/tunnel.rs @@ -1,5 +1,10 @@ use std::fmt::Display; +use cloudflare::{ + endpoints, + framework::{client::async_api::Client, response::ApiFailure}, +}; + use crate::TunnelData; #[derive(Debug)] @@ -38,7 +43,7 @@ pub async fn create_tunnel( account_identifier: account_id, params: cloudflare::endpoints::cfd_tunnel::create_tunnel::Params { name: tunnel_name, - tunnel_secret: &tunnel_secret, + tunnel_secret: &tunnel_secret.as_bytes().to_vec(), config_src: &cloudflare::endpoints::cfd_tunnel::ConfigurationSrc::Local, metadata: None, }, @@ -50,12 +55,12 @@ pub async fn create_tunnel( .map_err(|err| CreateTunnelError::CreateCloudflareTunnel(err.to_string()))? .result; - let create_dns_req = cloudflare::endpoints::dns::CreateDnsRecord { + let create_dns_req = cloudflare::endpoints::dns::dns::CreateDnsRecord { zone_identifier: zone_id, - params: cloudflare::endpoints::dns::CreateDnsRecordParams { + params: cloudflare::endpoints::dns::dns::CreateDnsRecordParams { proxied: Some(true), name: tunnel_name, - content: cloudflare::endpoints::dns::DnsContent::CNAME { + content: cloudflare::endpoints::dns::dns::DnsContent::CNAME { content: format!("{}.cfargotunnel.com", tunnel.id), }, ttl: None, @@ -68,7 +73,7 @@ pub async fn create_tunnel( .await .map_err(|err| CreateTunnelError::CreateDNS(err.to_string()))?; - let get_zone_req = cloudflare::endpoints::zone::ZoneDetails { + let get_zone_req = cloudflare::endpoints::zones::zone::ZoneDetails { identifier: zone_id, }; @@ -134,10 +139,10 @@ pub async fn delete_tunnel( .await .map_err(|error| DeleteTunnelError::DeleteCloudflareTunnel(error.to_string()))?; - let get_dns_record_req = cloudflare::endpoints::dns::ListDnsRecords { + let get_dns_record_req = cloudflare::endpoints::dns::dns::ListDnsRecords { zone_identifier: zone_id, - params: cloudflare::endpoints::dns::ListDnsRecordsParams { - record_type: Some(cloudflare::endpoints::dns::DnsContent::CNAME { + params: cloudflare::endpoints::dns::dns::ListDnsRecordsParams { + record_type: Some(cloudflare::endpoints::dns::dns::DnsContent::CNAME { content: format!("{}.cfargotunnel.com", tunnel_id), }), ..Default::default() @@ -154,17 +159,17 @@ pub async fn delete_tunnel( 0 => { return Err(DeleteTunnelError::GetDNSRecord( "Fetching DNS for tunnel returned empty".to_string(), - )) + )); } 1 => &records[0], 2.. => { return Err(DeleteTunnelError::GetDNSRecord( "Fetching DNS for tunnel returned more than one record".to_string(), - )) + )); } }; - let delete_dns_record_red = cloudflare::endpoints::dns::DeleteDnsRecord { + let delete_dns_record_red = cloudflare::endpoints::dns::dns::DeleteDnsRecord { zone_identifier: zone_id, identifier: &record.id, }; @@ -176,3 +181,18 @@ pub async fn delete_tunnel( Ok(()) } + +// TODO(augustoccesar)[2026-04-13]: This function is duplicated on linkup-cli/src/commands/deploy/mod.rs +// We can probably find a place to unify them. +pub async fn tunnel_prefix(client: &Client, zone_id: &str) -> Result { + let req = endpoints::zones::zone::ZoneDetails { + identifier: zone_id, + }; + + let zone = client.request(&req).await?; + + let zone_name = zone.result.name.replace(".", "-"); + let tunnel_name = format!("linkup-tunnel-{}-", zone_name); + + Ok(tunnel_name) +} diff --git a/worker/src/ws.rs b/worker/src/ws.rs index cfec1f1a..c1d7d58e 100644 --- a/worker/src/ws.rs +++ b/worker/src/ws.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use axum::{http::StatusCode, response::IntoResponse}; use http::{HeaderName, HeaderValue}; use linkup::allow_all_cors; -use worker::{console_log, Error, HttpResponse, WebSocket, WebSocketPair, WebsocketEvent}; +use worker::{Error, HttpResponse, WebSocket, WebSocketPair, WebsocketEvent, console_log}; use futures::{ future::{self, Either}, @@ -25,7 +25,7 @@ pub async fn handle_ws_resp(upstream_response: worker::Response) -> impl IntoRes format!("Failed to connect to destination: {}", e), StatusCode::BAD_GATEWAY, ) - .into_response() + .into_response(); } }; @@ -36,7 +36,7 @@ pub async fn handle_ws_resp(upstream_response: worker::Response) -> impl IntoRes format!("Failed to create source websocket: {}", e), StatusCode::INTERNAL_SERVER_ERROR, ) - .into_response() + .into_response(); } }; let downstream_ws_server = downstream_ws.server; @@ -102,7 +102,7 @@ pub async fn handle_ws_resp(upstream_response: worker::Response) -> impl IntoRes format!("Failed to create response from websocket: {}", e), StatusCode::INTERNAL_SERVER_ERROR, ) - .into_response() + .into_response(); } }; @@ -113,7 +113,7 @@ pub async fn handle_ws_resp(upstream_response: worker::Response) -> impl IntoRes format!("Failed to parse response: {}", e), StatusCode::BAD_GATEWAY, ) - .into_response() + .into_response(); } };