feat: openssl based hoppscotch-relay for request forwarding (#4442)

This commit is contained in:
Shreyas
2024-10-24 14:20:51 +05:30
committed by GitHub
parent deedf35bf0
commit f52219bb95
28 changed files with 2284 additions and 859 deletions

View File

@@ -142,9 +142,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.89"
version = "1.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
[[package]]
name = "arbitrary"
@@ -267,7 +267,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -302,7 +302,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -516,9 +516,9 @@ dependencies = [
[[package]]
name = "brotli"
version = "6.0.0"
version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -543,9 +543,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
version = "1.18.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae"
checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d"
[[package]]
name = "byteorder"
@@ -561,9 +561,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.7.2"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
dependencies = [
"serde",
]
@@ -637,9 +637,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.28"
version = "1.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1"
checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
dependencies = [
"shlex",
]
@@ -887,7 +887,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
dependencies = [
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -897,7 +897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
dependencies = [
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -912,30 +912,25 @@ dependencies = [
[[package]]
name = "curl"
version = "0.4.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9fb4d13a1be2b58f14d60adba57c9834b78c62fd86c3e76a148f732686e9265"
source = "git+https://github.com/CuriousCorrelation/curl-rust.git#1ec8079cf527b9cf47cc7a48c68b458affdae273"
dependencies = [
"curl-sys",
"libc",
"openssl-probe",
"openssl-sys",
"schannel",
"socket2",
"windows-sys 0.52.0",
]
[[package]]
name = "curl-sys"
version = "0.4.77+curl-8.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f469e8a5991f277a208224f6c7ad72ecb5f986e36d09ae1f2c1bb9259478a480"
source = "git+https://github.com/CuriousCorrelation/curl-rust.git#1ec8079cf527b9cf47cc7a48c68b458affdae273"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
"windows-sys 0.52.0",
]
@@ -962,7 +957,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -986,7 +981,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -997,7 +992,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -1044,7 +1039,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -1057,7 +1052,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -1125,7 +1120,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -1157,7 +1152,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -1255,7 +1250,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -1423,7 +1418,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -1510,7 +1505,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -1764,7 +1759,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -1843,7 +1838,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -1946,15 +1941,12 @@ dependencies = [
"axum-extra",
"base16",
"chrono",
"curl",
"dashmap",
"env_logger",
"http",
"hoppscotch-relay",
"lazy_static",
"log",
"mockito",
"openssl",
"openssl-sys",
"rand 0.8.5",
"serde",
"serde_json",
@@ -1969,11 +1961,27 @@ dependencies = [
"tokio",
"tokio-util",
"tower-http",
"url-escape",
"uuid",
"x25519-dalek",
]
[[package]]
name = "hoppscotch-relay"
version = "0.1.1"
dependencies = [
"curl",
"env_logger",
"http",
"log",
"openssl",
"openssl-sys",
"serde",
"serde_json",
"thiserror",
"tokio-util",
"url-escape",
]
[[package]]
name = "html5ever"
version = "0.26.0"
@@ -2042,9 +2050,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "1.4.1"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
dependencies = [
"bytes",
"futures-channel",
@@ -2149,9 +2157,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.2"
version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae"
dependencies = [
"bytemuck",
"byteorder-lite",
@@ -2298,9 +2306,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "js-sys"
version = "0.3.70"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
dependencies = [
"wasm-bindgen",
]
@@ -2311,7 +2319,19 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc"
dependencies = [
"jsonptr",
"jsonptr 0.4.7",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "json-patch"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08"
dependencies = [
"jsonptr 0.6.3",
"serde",
"serde_json",
"thiserror",
@@ -2328,6 +2348,16 @@ dependencies = [
"serde_json",
]
[[package]]
name = "jsonptr"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "keyboard-types"
version = "0.7.0"
@@ -2384,9 +2414,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.159"
version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "libloading"
@@ -2674,7 +2704,7 @@ dependencies = [
"proc-macro-crate 2.0.2",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -2960,7 +2990,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -2971,9 +3001,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.3.2+3.3.2"
version = "300.4.0+3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a211a18d945ef7e648cc6e0058f4c548ee46aab922ea203e0d30e966ea23647b"
checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6"
dependencies = [
"cc",
]
@@ -3073,9 +3103,9 @@ dependencies = [
[[package]]
name = "pathdiff"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
[[package]]
name = "percent-encoding"
@@ -3187,7 +3217,7 @@ dependencies = [
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -3372,9 +3402,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.87"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
@@ -3708,9 +3738,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.14"
version = "0.23.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8"
checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993"
dependencies = [
"once_cell",
"ring",
@@ -3731,9 +3761,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55"
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
[[package]]
name = "rustls-webpki"
@@ -3748,9 +3778,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.17"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
[[package]]
name = "ryu"
@@ -3767,15 +3797,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "schemars"
version = "0.8.21"
@@ -3800,7 +3821,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -3846,9 +3867,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.210"
version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
dependencies = [
"serde_derive",
]
@@ -3866,13 +3887,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.210"
version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -3883,14 +3904,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
name = "serde_json"
version = "1.0.128"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa 1.0.11",
"memchr",
@@ -3916,7 +3937,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -3967,7 +3988,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -4220,9 +4241,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.79"
version = "2.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
dependencies = [
"proc-macro2",
"quote",
@@ -4304,7 +4325,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -4326,9 +4347,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.0.4"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44438500b50708bfc1e6083844e135d1b516325aae58710dcd8fb67e050ae87c"
checksum = "d3889b392db6d32a105d3757230ea0220090b8f94c90d3e60b6c5eb91178ab1b"
dependencies = [
"anyhow",
"bytes",
@@ -4377,16 +4398,16 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "935f9b3c49b22b3e2e485a57f46d61cd1ae07b1cbb2ba87387a387caf2d8c4e7"
checksum = "9f96827ccfb1aa40d55d0ded79562d18ba18566657a553f992a982d755148376"
dependencies = [
"anyhow",
"cargo_toml",
"dirs 5.0.1",
"glob",
"heck 0.5.0",
"json-patch",
"json-patch 3.0.1",
"schemars",
"semver",
"serde",
@@ -4399,14 +4420,14 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95d7443dd4f0b597704b6a14b964ee2ed16e99928d8e6292ae9825f09fbcd30e"
checksum = "8947f16f47becd9e9cd39b74ee337fd1981574d78819be18e4384d85e5a0b82f"
dependencies = [
"base64 0.22.1",
"brotli",
"ico",
"json-patch",
"json-patch 2.0.0",
"plist",
"png",
"proc-macro2",
@@ -4415,7 +4436,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"syn 2.0.79",
"syn 2.0.85",
"tauri-utils",
"thiserror",
"time",
@@ -4426,23 +4447,23 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d2c0963ccfc3f5194415f2cce7acc975942a8797fbabfb0aa1ed6f59326ae7f"
checksum = "8bd1c8d4a66799d3438747c3a79705cd665a95d6f24cb5f315413ff7a981fe2a"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"tauri-codegen",
"tauri-utils",
]
[[package]]
name = "tauri-plugin"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2e6660a409963e4d57b9bfab4addd141eeff41bd3a7fb14e13004a832cf7ef6"
checksum = "6fa4e6c94cb1d635f65a770c69e23de1bc054b0e4c554fa037a7cc7676333d39"
dependencies = [
"anyhow",
"glob",
@@ -4472,9 +4493,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-dialog"
version = "2.0.1"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddb2fe88b602461c118722c574e2775ab26a4e68886680583874b2f6520608b7"
checksum = "4307310e1d2c09ab110235834722e7c2b85099b683e1eb7342ab351b0be5ada3"
dependencies = [
"log",
"raw-window-handle",
@@ -4490,9 +4511,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs"
version = "2.0.1"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab300488ebec3487ca5f56289692e7e45feb07eea8d5e1dba497f7dc9dd9c407"
checksum = "96ba7d46e86db8c830d143ef90ab5a453328365b0cc834c24edea4267b16aba0"
dependencies = [
"anyhow",
"dunce",
@@ -4511,9 +4532,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-shell"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "371fb9aca2823990a2d0db7970573be5fdf07881fcaa2b835b29631feb84aec1"
checksum = "0ad7880c5586b6b2104be451e3d7fc0f3800c84bda69e9ba81c828f87cb34267"
dependencies = [
"encoding_rs",
"log",
@@ -4532,9 +4553,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-store"
version = "2.0.1"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5058f179f7215390fc5a68eeffcb805b7e2681d6e817a5d08094fae7ab649e68"
checksum = "e9a580be53f04bb62422d239aa798e88522877f58a0d4a0e745f030055a51bb4"
dependencies = [
"dunce",
"log",
@@ -4578,9 +4599,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8f437293d6f5e5dce829250f4dbdce4e0b52905e297a6689cc2963eb53ac728"
checksum = "a1ef7363e7229ac8d04e8a5d405670dbd43dde8fc4bc3bc56105c35452d03784"
dependencies = [
"dpi",
"gtk",
@@ -4597,9 +4618,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.1.1"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1431602bcc71f2f840ad623915c9842ecc32999b867c4a787d975a17a9625cc6"
checksum = "62fa2068e8498ad007b54d5773d03d57c3ff6dd96f8c8ce58beff44d0d5e0d30"
dependencies = [
"gtk",
"http",
@@ -4623,9 +4644,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c38b0230d6880cf6dd07b6d7dd7789a0869f98ac12146e0d18d1c1049215a045"
checksum = "1fc65d6f5c54e56b66258948a6d9e47a82ea41f4b5a7612bfbdd1634c2913ed0"
dependencies = [
"brotli",
"cargo_metadata",
@@ -4634,7 +4655,7 @@ dependencies = [
"glob",
"html5ever",
"infer",
"json-patch",
"json-patch 2.0.0",
"kuchikiki",
"log",
"memchr",
@@ -4699,22 +4720,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]]
name = "thiserror"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -4765,9 +4786,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.40.0"
version = "1.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998"
checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
dependencies = [
"backtrace",
"bytes",
@@ -4790,7 +4811,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -4938,7 +4959,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -5211,9 +5232,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
dependencies = [
"cfg-if",
"once_cell",
@@ -5222,24 +5243,24 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.43"
version = "0.4.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
dependencies = [
"cfg-if",
"js-sys",
@@ -5249,9 +5270,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -5259,22 +5280,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
[[package]]
name = "wasm-streams"
@@ -5305,9 +5326,9 @@ dependencies = [
[[package]]
name = "wayland-client"
version = "0.31.6"
version = "0.31.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3f45d1222915ef1fd2057220c1d9d9624b7654443ea35c3877f7a52bd0a5a2d"
checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280"
dependencies = [
"bitflags 2.6.0",
"rustix",
@@ -5317,9 +5338,9 @@ dependencies = [
[[package]]
name = "wayland-protocols"
version = "0.32.4"
version = "0.32.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b5755d77ae9040bb872a25026555ce4cb0ae75fd923e90d25fba07d81057de0"
checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e"
dependencies = [
"bitflags 2.6.0",
"wayland-backend",
@@ -5351,9 +5372,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.70"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -5434,7 +5455,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -5533,7 +5554,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -5544,7 +5565,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -5830,9 +5851,9 @@ dependencies = [
[[package]]
name = "wry"
version = "0.46.2"
version = "0.46.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fa1c8c760041c64ce6be99f83d6cb55fe3fcd85a1ad46d32895f6e65cee87ba"
checksum = "cd5cdf57c66813d97601181349c63b96994b3074fc3d7a31a8cce96e968e3bbd"
dependencies = [
"base64 0.22.1",
"block2",
@@ -6000,7 +6021,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]
@@ -6020,7 +6041,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.85",
]
[[package]]

View File

@@ -2,7 +2,7 @@
name = "hoppscotch-agent"
version = "0.1.1"
description = "A cross-platform HTTP request agent for Hoppscotch for advanced request handling including custom headers, certificates, proxies, and local system integration."
authors = ["CuriousCorrelation", "AndrewBastin"]
authors = ["AndrewBastin", "CuriousCorrelation"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -31,18 +31,14 @@ chrono = { version = "0.4", features = ["serde"] }
rand = "0.8.5"
log = "0.4.22"
env_logger = "0.11.5"
curl = { version = "0.4.47", features = ["ntlm", "static-curl", "static-ssl"] }
openssl = { version = "0.10.68", features = ["vendored"] }
openssl-sys = { version = "0.9.104", features = ["vendored"] }
url-escape = "0.1.1"
hoppscotch-relay = { path = "../../hoppscotch-relay" }
thiserror = "1.0.64"
tauri-plugin-store = "2.0.1"
tauri-plugin-store = "2.1.0"
x25519-dalek = { version = "2.0.1", features = ["getrandom"] }
base16 = "0.2.1"
aes-gcm = { version = "0.10.3", features = ["aes"] }
tauri-plugin-updater = "2.0.2"
tauri-plugin-dialog = "2.0.1"
http = "1.1.0"
lazy_static = "1.5.0"
[dev-dependencies]

View File

@@ -8,16 +8,14 @@ use axum_extra::{
headers::{authorization::Bearer, Authorization},
TypedHeader,
};
use hoppscotch_relay::{RequestWithMetadata, ResponseWithMetadata};
use std::sync::Arc;
use tauri::{AppHandle, Emitter};
use x25519_dalek::{EphemeralSecret, PublicKey};
use crate::{
error::{AppError, AppResult},
model::{
AuthKeyResponse, ConfirmedRegistrationRequest, HandshakeResponse, RequestDef,
RunRequestResponse,
},
model::{AuthKeyResponse, ConfirmedRegistrationRequest, HandshakeResponse},
state::{AppState, Registration},
util::EncryptedJson,
};
@@ -27,19 +25,18 @@ use serde_json::json;
use uuid::Uuid;
fn generate_otp() -> String {
let otp: u32 = rand::thread_rng().gen_range(0..1_000_000);
let otp: u32 = rand::thread_rng().gen_range(0..1_000_000);
format!("{:06}", otp)
format!("{:06}", otp)
}
pub async fn handshake(
State((_, app_handle)): State<(Arc<AppState>, AppHandle)>
State((_, app_handle)): State<(Arc<AppState>, AppHandle)>,
) -> AppResult<Json<HandshakeResponse>> {
Ok(Json(HandshakeResponse {
status: "success".to_string(),
__hoppscotch__agent__: true,
agent_version: app_handle.package_info().version.to_string()
agent_version: app_handle.package_info().version.to_string(),
}))
}
@@ -86,11 +83,11 @@ pub async fn verify_registration(
let agent_public_key = PublicKey::from(&agent_secret_key);
let their_public_key = {
let public_key_slice: &[u8; 32] = &base16::decode(&confirmed_registration.client_public_key_b16)
.map_err(|_| AppError::InvalidClientPublicKey)?
[0..32]
.try_into()
.map_err(|_| AppError::InvalidClientPublicKey)?;
let public_key_slice: &[u8; 32] =
&base16::decode(&confirmed_registration.client_public_key_b16)
.map_err(|_| AppError::InvalidClientPublicKey)?[0..32]
.try_into()
.map_err(|_| AppError::InvalidClientPublicKey)?;
PublicKey::from(public_key_slice.to_owned())
};
@@ -98,10 +95,13 @@ pub async fn verify_registration(
let shared_secret = agent_secret_key.diffie_hellman(&their_public_key);
let _ = state.update_registrations(app_handle.clone(), |regs| {
regs.insert(auth_key_copy, Registration {
registered_at: created_at,
shared_secret_b16: base16::encode_lower(shared_secret.as_bytes())
});
regs.insert(
auth_key_copy,
Registration {
registered_at: created_at,
shared_secret_b16: base16::encode_lower(shared_secret.as_bytes()),
},
);
})?;
let auth_payload = json!({
@@ -124,25 +124,28 @@ pub async fn run_request<T>(
State((state, _app_handle)): State<(Arc<AppState>, T)>,
TypedHeader(auth_header): TypedHeader<Authorization<Bearer>>,
headers: HeaderMap,
body: Bytes
) -> AppResult<EncryptedJson<RunRequestResponse>> {
let nonce = headers.get("X-Hopp-Nonce")
.ok_or(AppError::Unauthorized)?
.to_str()
.map_err(|_| AppError::Unauthorized)?;
body: Bytes,
) -> AppResult<EncryptedJson<ResponseWithMetadata>> {
let nonce = headers
.get("X-Hopp-Nonce")
.ok_or(AppError::Unauthorized)?
.to_str()
.map_err(|_| AppError::Unauthorized)?;
let req: RequestDef = state.validate_access_and_get_data(auth_header.token(), nonce, &body)
let req: RequestWithMetadata = state
.validate_access_and_get_data(auth_header.token(), nonce, &body)
.ok_or(AppError::Unauthorized)?;
let reg_info = state.get_registration_info(auth_header.token())
let req_id = req.req_id;
let reg_info = state
.get_registration_info(auth_header.token())
.ok_or(AppError::Unauthorized)?;
let cancel_token = tokio_util::sync::CancellationToken::new();
state.add_cancellation_token(req.req_id, cancel_token.clone());
let req_id = req.req_id;
let cancel_token_clone = cancel_token.clone();
// Execute the HTTP request in a blocking thread pool and handles cancellation.
//
// It:
@@ -158,9 +161,9 @@ pub async fn run_request<T>(
// - `spawn_blocking` moves this operation to a thread pool designed for
// blocking tasks, so other async operations to continue unblocked.
let result = tokio::select! {
res = tokio::task::spawn_blocking(move || crate::interceptor::run_request_task(&req, cancel_token_clone)) => {
res = tokio::task::spawn_blocking(move || hoppscotch_relay::run_request_task(&req, cancel_token_clone)) => {
match res {
Ok(task_result) => task_result,
Ok(task_result) => Ok(task_result?),
Err(_) => Err(AppError::InternalServerError),
}
},
@@ -171,11 +174,9 @@ pub async fn run_request<T>(
state.remove_cancellation_token(req_id);
result.map(|val| {
EncryptedJson {
result.map(|val| EncryptedJson {
key_b16: reg_info.shared_secret_b16,
data: val
}
data: val,
})
}

View File

@@ -42,6 +42,8 @@ pub enum AppError {
RegistrationSaveError,
#[error("Store error: {0}")]
TauriPluginStore(#[from] tauri_plugin_store::Error),
#[error("Relay error: {0}")]
Relay(#[from] hoppscotch_relay::RelayError),
}
impl IntoResponse for AppError {

View File

@@ -1,567 +0,0 @@
use crate::{
error::AppError,
model::{BodyDef, ClientCertDef, FormDataValue, KeyValuePair, RequestDef, RunRequestResponse},
util::get_status_text,
};
use curl::easy::{Easy, List};
use openssl::{pkcs12::Pkcs12, ssl::SslContextBuilder, x509::X509};
use openssl_sys::SSL_CTX;
use std::time::SystemTime;
use tokio_util::sync::CancellationToken;
pub(crate) fn run_request_task(
req: &RequestDef,
cancel_token: CancellationToken,
) -> Result<RunRequestResponse, AppError> {
let mut curl_handle = Easy::new();
curl_handle
.progress(true)
.map_err(|err| AppError::RequestRunError(err.description().to_string()))?;
curl_handle
.custom_request(&req.method)
.map_err(|_| AppError::InvalidMethod)?;
curl_handle
.url(&req.endpoint)
.map_err(|_| AppError::InvalidUrl)?;
curl_handle
.http_headers(get_headers_list(&req)?)
.map_err(|_| AppError::InvalidHeaders)?;
apply_body_to_curl_handle(&mut curl_handle, &req)?;
curl_handle
.ssl_verify_peer(req.validate_certs)
.map_err(|err| AppError::RequestRunError(err.description().to_string()))?;
curl_handle
.ssl_verify_host(req.validate_certs)
.map_err(|err| AppError::RequestRunError(err.description().to_string()))?;
apply_client_cert_to_curl_handle(&mut curl_handle, &req)?;
apply_proxy_config_to_curl_handle(&mut curl_handle, &req)?;
let mut response_body = Vec::new();
let mut response_headers = Vec::new();
let (start_time_ms, end_time_ms) = {
let mut transfer = curl_handle.transfer();
transfer
.ssl_ctx_function(|ssl_ctx_ptr| {
let cert_list = get_x509_certs_from_root_cert_bundle(&req);
if !cert_list.is_empty() {
let mut ssl_ctx_builder =
unsafe { SslContextBuilder::from_ptr(ssl_ctx_ptr as *mut SSL_CTX) };
let cert_store = ssl_ctx_builder.cert_store_mut();
for cert in cert_list {
if let Err(e) = cert_store.add_cert(cert) {
eprintln!("Failed writing cert into cert store: {}", e);
}
}
}
Ok(())
})
.map_err(|err| AppError::RequestRunError(err.description().to_string()))?;
transfer
.progress_function(|_, _, _, _| !cancel_token.is_cancelled())
.map_err(|err| AppError::RequestRunError(err.description().to_string()))?;
transfer
.header_function(|header| {
let header = String::from_utf8_lossy(header).into_owned();
if let Some((key, value)) = header.split_once(':') {
response_headers.push(KeyValuePair {
key: key.trim().to_string(),
value: value.trim().to_string(),
});
}
true
})
.map_err(|err| AppError::RequestRunError(err.description().to_string()))?;
transfer
.write_function(|data| {
response_body.extend_from_slice(data);
Ok(data.len())
})
.map_err(|err| AppError::RequestRunError(err.description().to_string()))?;
let start_time_ms = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
transfer
.perform()
.map_err(|err| AppError::RequestRunError(err.description().to_string()))?;
let end_time_ms = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
(start_time_ms, end_time_ms)
};
let response_status = curl_handle
.response_code()
.map_err(|err| AppError::RequestRunError(err.description().to_string()))?
as u16;
let response_status_text = get_status_text(response_status).to_string();
Ok(RunRequestResponse {
status: response_status,
status_text: response_status_text,
headers: response_headers,
data: response_body,
time_start_ms: start_time_ms,
time_end_ms: end_time_ms,
})
}
fn get_headers_list(req: &RequestDef) -> Result<List, AppError> {
let mut result = List::new();
for KeyValuePair { key, value } in &req.headers {
result
.append(&format!("{}: {}", key, value))
.map_err(|err| AppError::RequestRunError(err.description().to_string()))?;
}
Ok(result)
}
fn apply_body_to_curl_handle(curl_handle: &mut Easy, req: &RequestDef) -> Result<(), AppError> {
match &req.body {
Some(BodyDef::Text(text)) => {
curl_handle
.post_fields_copy(text.as_bytes())
.map_err(|err| {
AppError::RequestRunError(format!(
"Error while setting body: {}",
err.description()
))
})?;
}
Some(BodyDef::FormData(entries)) => {
let mut form = curl::easy::Form::new();
for entry in entries {
let mut part = form.part(&entry.key);
match &entry.value {
FormDataValue::Text(data) => {
part.contents(data.as_bytes());
}
FormDataValue::File {
filename,
data,
mime,
} => {
part.buffer(filename, data.clone()).content_type(mime);
}
};
part.add().map_err(|err| {
AppError::RequestRunError(format!(
"Error while setting body: {}",
err.description()
))
})?;
}
curl_handle.httppost(form).map_err(|err| {
AppError::RequestRunError(format!(
"Error while setting body: {}",
err.description()
))
})?;
}
Some(BodyDef::URLEncoded(entries)) => {
let data = entries
.iter()
.map(|KeyValuePair { key, value }| {
format!(
"{}={}",
&url_escape::encode_www_form_urlencoded(key),
url_escape::encode_www_form_urlencoded(value)
)
})
.collect::<Vec<String>>()
.join("&");
curl_handle
.post_fields_copy(data.as_bytes())
.map_err(|err| {
AppError::RequestRunError(format!(
"Error while setting body: {}",
err.description()
))
})?;
}
None => {}
};
Ok(())
}
fn apply_client_cert_to_curl_handle(handle: &mut Easy, req: &RequestDef) -> Result<(), AppError> {
match &req.client_cert {
Some(ClientCertDef::PEMCert {
certificate_pem,
key_pem,
}) => {
handle.ssl_cert_type("PEM").map_err(|err| {
AppError::RequestRunError(format!(
"Failed setting PEM Cert Type: {}",
err.description()
))
})?;
handle.ssl_cert_blob(certificate_pem).map_err(|err| {
AppError::RequestRunError(format!(
"Failed setting PEM Cert Blob: {}",
err.description()
))
})?;
handle.ssl_key_type("PEM").map_err(|err| {
AppError::RequestRunError(format!(
"Failed setting PEM key type: {}",
err.description()
))
})?;
handle.ssl_key_blob(key_pem).map_err(|err| {
AppError::RequestRunError(format!(
"Failed setting PEM Cert blob: {}",
err.description()
))
})?;
}
Some(ClientCertDef::PFXCert {
certificate_pfx,
password,
}) => {
let pkcs12 = Pkcs12::from_der(&certificate_pfx).map_err(|err| {
AppError::RequestRunError(format!(
"Failed to parse PFX certificate from DER: {}",
err
))
})?;
let parsed = pkcs12.parse2(password).map_err(|err| {
AppError::RequestRunError(format!(
"Failed to parse PFX certificate with provided password: {}",
err
))
})?;
if let (Some(cert), Some(key)) = (parsed.cert, parsed.pkey) {
let certificate_pem = cert.to_pem().map_err(|err| {
AppError::RequestRunError(format!(
"Failed to convert PFX certificate to PEM format: {}",
err
))
})?;
let key_pem = key.private_key_to_pem_pkcs8().map_err(|err| {
AppError::RequestRunError(format!(
"Failed to convert PFX private key to PEM format: {}",
err
))
})?;
handle.ssl_cert_type("PEM").map_err(|err| {
AppError::RequestRunError(format!(
"Failed setting PEM Cert Type for converted PFX: {}",
err.description()
))
})?;
handle.ssl_cert_blob(&certificate_pem).map_err(|err| {
AppError::RequestRunError(format!(
"Failed setting PEM Cert Blob for converted PFX: {}",
err.description()
))
})?;
handle.ssl_key_type("PEM").map_err(|err| {
AppError::RequestRunError(format!(
"Failed setting PEM key type for converted PFX: {}",
err.description()
))
})?;
handle.ssl_key_blob(&key_pem).map_err(|err| {
AppError::RequestRunError(format!(
"Failed setting PEM key blob for converted PFX: {}",
err.description()
))
})?;
} else {
return Err(AppError::RequestRunError(
"PFX certificate parsing succeeded, but either cert or private key is missing"
.to_string(),
));
}
}
None => {}
};
Ok(())
}
fn get_x509_certs_from_root_cert_bundle(req: &RequestDef) -> Vec<X509> {
req.root_cert_bundle_files
.iter()
.map(|pem_bundle| openssl::x509::X509::stack_from_pem(pem_bundle))
.filter_map(|certs| {
if let Ok(certs) = certs {
Some(certs)
} else {
None
}
})
.flatten()
.collect()
}
fn apply_proxy_config_to_curl_handle(handle: &mut Easy, req: &RequestDef) -> Result<(), AppError> {
if let Some(proxy_config) = &req.proxy {
handle
.proxy_auth(curl::easy::Auth::new().auto(true))
.map_err(|err| {
AppError::RequestRunError(format!(
"Failed to set proxy Auth Mode: {}",
err.description()
))
})?;
handle.proxy(&proxy_config.url).map_err(|err| {
AppError::RequestRunError(format!("Failed to set proxy URL: {}", err.description()))
})?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use crate::model::FormDataEntry;
use super::*;
use mockito::Server;
#[test]
fn test_run_request_task_success() {
let mut server = Server::new();
let mock = server
.mock("GET", "/test")
.with_status(200)
.with_header("content-type", "text/plain")
.with_body("Hello, World!")
.create();
let req = RequestDef {
req_id: 1,
method: "GET".to_string(),
endpoint: format!("{}/test", server.url()),
headers: vec![],
body: None,
validate_certs: false,
root_cert_bundle_files: vec![],
client_cert: None,
proxy: None,
};
let cancel_token = CancellationToken::new();
let result = run_request_task(&req, cancel_token);
assert!(result.is_ok());
let response = result.unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.status_text, "OK");
assert!(response
.headers
.iter()
.any(|h| h.key == "content-type" && h.value == "text/plain"));
assert_eq!(response.data, b"Hello, World!");
mock.assert();
}
#[test]
fn test_run_request_task_with_headers() {
let mut server = Server::new();
let mock = server
.mock("GET", "/test")
.match_header("X-Custom-Header", "TestValue")
.with_status(200)
.create();
let req = RequestDef {
req_id: 1,
method: "GET".to_string(),
endpoint: format!("{}/test", server.url()),
headers: vec![KeyValuePair {
key: "X-Custom-Header".to_string(),
value: "TestValue".to_string(),
}],
body: None,
validate_certs: false,
root_cert_bundle_files: vec![],
client_cert: None,
proxy: None,
};
let cancel_token = CancellationToken::new();
let result = run_request_task(&req, cancel_token);
assert!(result.is_ok());
mock.assert();
}
#[test]
fn test_run_request_task_with_body() {
let mut server = Server::new();
let mock = server
.mock("POST", "/test")
.match_body("test_body")
.with_status(201)
.create();
let req = RequestDef {
req_id: 1,
method: "POST".to_string(),
endpoint: format!("{}/test", server.url()),
headers: vec![],
body: Some(BodyDef::Text("test_body".to_string())),
validate_certs: false,
root_cert_bundle_files: vec![],
client_cert: None,
proxy: None,
};
let cancel_token = CancellationToken::new();
let result = run_request_task(&req, cancel_token);
assert!(result.is_ok());
assert_eq!(result.unwrap().status, 201);
mock.assert();
}
#[test]
fn test_run_request_task_with_url_encoded_body() {
let mut server = Server::new();
let mock = server
.mock("POST", "/test")
.match_body("key1=value1&key2=value2")
.with_status(200)
.create();
let req = RequestDef {
req_id: 1,
method: "POST".to_string(),
endpoint: format!("{}/test", server.url()),
headers: vec![],
body: Some(BodyDef::URLEncoded(vec![
KeyValuePair {
key: "key1".to_string(),
value: "value1".to_string(),
},
KeyValuePair {
key: "key2".to_string(),
value: "value2".to_string(),
},
])),
validate_certs: false,
root_cert_bundle_files: vec![],
client_cert: None,
proxy: None,
};
let cancel_token = CancellationToken::new();
let result = run_request_task(&req, cancel_token);
assert!(result.is_ok());
mock.assert();
}
#[test]
fn test_run_request_task_with_invalid_url() {
let req = RequestDef {
req_id: 1,
method: "GET".to_string(),
endpoint: "invalid_url".to_string(),
headers: vec![],
body: None,
validate_certs: false,
root_cert_bundle_files: vec![],
client_cert: None,
proxy: None,
};
let cancel_token = CancellationToken::new();
let result = run_request_task(&req, cancel_token);
assert!(result.is_err());
}
#[test]
fn test_run_request_task_with_form_data() {
let mut server = Server::new();
let mock = server
.mock("POST", "/test")
.match_header(
"content-type",
mockito::Matcher::Regex("multipart/form-data.*".to_string()),
)
.with_status(200)
.create();
let req = RequestDef {
req_id: 1,
method: "POST".to_string(),
endpoint: format!("{}/test", server.url()),
headers: vec![],
body: Some(BodyDef::FormData(vec![
FormDataEntry {
key: "text_field".to_string(),
value: FormDataValue::Text("text_value".to_string()),
},
FormDataEntry {
key: "file_field".to_string(),
value: FormDataValue::File {
filename: "test.txt".to_string(),
data: b"file_content".to_vec(),
mime: "text/plain".to_string(),
},
},
])),
validate_certs: false,
root_cert_bundle_files: vec![],
client_cert: None,
proxy: None,
};
let cancel_token = CancellationToken::new();
let result = run_request_task(&req, cancel_token);
assert!(result.is_ok());
mock.assert();
}
}

View File

@@ -1,6 +1,5 @@
pub mod controller;
pub mod error;
pub mod interceptor;
pub mod model;
pub mod route;
pub mod server;
@@ -73,7 +72,7 @@ pub fn run() {
});
};
let app_state = Arc::new(AppState::new(app_handle.clone()));
let app_state = Arc::new(AppState::new(app_handle.clone())?);
app.manage(app_state.clone());

View File

@@ -7,7 +7,7 @@ pub struct HandshakeResponse {
pub __hoppscotch__agent__: bool,
pub status: String,
pub agent_version: String
pub agent_version: String,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -18,7 +18,7 @@ pub struct ConfirmedRegistrationRequest {
/// to the agent so that the agent can establish a shared secret
/// which will be used to encrypt traffic between agent
/// and client after registration
pub client_public_key_b16: String
pub client_public_key_b16: String,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -30,74 +30,5 @@ pub struct AuthKeyResponse {
/// agent so that the client can establish a shared secret
/// which will be used to encrypt traffic between agent
/// and client after registration
pub agent_public_key_b16: String
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KeyValuePair {
pub key: String,
pub value: String,
}
#[derive(Debug, Deserialize)]
pub enum FormDataValue {
Text(String),
File {
filename: String,
data: Vec<u8>,
mime: String,
},
}
#[derive(Debug, Deserialize)]
pub struct FormDataEntry {
pub key: String,
pub value: FormDataValue,
}
#[derive(Debug, Deserialize)]
pub enum BodyDef {
Text(String),
URLEncoded(Vec<KeyValuePair>),
FormData(Vec<FormDataEntry>),
}
#[derive(Debug, Deserialize)]
pub struct RequestDef {
pub req_id: usize,
pub method: String,
pub endpoint: String,
pub headers: Vec<KeyValuePair>,
pub body: Option<BodyDef>,
pub validate_certs: bool,
pub root_cert_bundle_files: Vec<Vec<u8>>,
pub client_cert: Option<ClientCertDef>,
pub proxy: Option<ProxyConfig>,
}
#[derive(Debug, Deserialize)]
pub struct ProxyConfig {
pub url: String,
}
#[derive(Debug, Deserialize)]
pub enum ClientCertDef {
PEMCert {
certificate_pem: Vec<u8>,
key_pem: Vec<u8>,
},
PFXCert {
certificate_pfx: Vec<u8>,
password: String,
},
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RunRequestResponse {
pub status: u16,
pub status_text: String,
pub headers: Vec<KeyValuePair>,
pub data: Vec<u8>,
pub time_start_ms: u128,
pub time_end_ms: u128,
pub agent_public_key_b16: String,
}

View File

@@ -7,7 +7,7 @@ use tauri_plugin_store::StoreBuilder;
use tokio::sync::RwLock;
use tokio_util::sync::CancellationToken;
use crate::error::AppError;
use crate::error::{AppError, AppResult};
/// Describes one registered app instance
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -34,10 +34,10 @@ pub struct AppState {
}
impl AppState {
pub fn new(app_handle: tauri::AppHandle) -> Self {
let store = StoreBuilder::new(&app_handle, "app_data.bin").build();
pub fn new(app_handle: tauri::AppHandle) -> AppResult<Self> {
let store = StoreBuilder::new(&app_handle, "app_data.bin").build()?;
let _ = store.load();
let _ = store.reload();
// Try loading and parsing registrations from the store, if that failed,
// load the default list
@@ -46,11 +46,11 @@ impl AppState {
.and_then(|val| serde_json::from_value(val.clone()).ok())
.unwrap_or_else(|| DashMap::new());
Self {
Ok(Self {
active_registration_code: RwLock::new(None),
cancellation_tokens: DashMap::new(),
registrations,
}
})
}
/// Gets you a readonly reference to the registrations list
@@ -70,9 +70,9 @@ impl AppState {
) -> Result<(), AppError> {
update_func(&self.registrations);
let store = StoreBuilder::new(&app_handle, "app_data.bin").build();
let store = StoreBuilder::new(&app_handle, "app_data.bin").build()?;
let _ = store.load()?;
let _ = store.reload()?;
let _ = store
.delete("registrations")

View File

@@ -3,13 +3,6 @@ use axum::{body::Body, response::{IntoResponse, Response}};
use rand::rngs::OsRng;
use serde::Serialize;
pub fn get_status_text(status: u16) -> &'static str {
http::StatusCode::from_u16(status)
.map(|status| status.canonical_reason())
.unwrap_or(Some("Unknown Status"))
.unwrap_or("Unknown Status")
}
#[derive(Debug)]
pub struct EncryptedJson<T: Serialize> {
pub key_b16: String,

View File

@@ -79,6 +79,7 @@
"not_running_title": "Agent not detected",
"registration_title": "Agent registration",
"verify_ssl_certs": "Verify SSL Certificates",
"ca_certs": "CA Certificates",
"client_certs": "Client Certificates",
"use_http_proxy": "Use HTTP Proxy",
"proxy_capabilities": "Hoppscotch Agent supports HTTP/HTTPS/SOCKS proxies along with NTLM and Basic Auth in those proxies. Include the username and password for the proxy authentication in the URL itself.",
@@ -382,6 +383,7 @@
"gql_prettify_invalid_query": "Couldn't prettify an invalid query, solve query syntax errors and try again",
"incomplete_config_urls": "Incomplete configuration URLs",
"incorrect_email": "Incorrect email",
"invalid_file_type": "Invalid file type for `{filename}`.",
"invalid_link": "Invalid link",
"invalid_link_description": "The link you clicked is invalid or expired.",
"invalid_embed_link": "The embed does not exist or is invalid.",

View File

@@ -193,6 +193,7 @@ declare module 'vue' {
ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default']
InterceptorsAgentModalNativeCACertificates: typeof import('./components/interceptors/agent/ModalNativeCACertificates.vue')['default']
InterceptorsAgentModalNativeClientCertificates: typeof import('./components/interceptors/agent/ModalNativeClientCertificates.vue')['default']
InterceptorsAgentModalNativeClientCertsAdd: typeof import('./components/interceptors/agent/ModalNativeClientCertsAdd.vue')['default']
InterceptorsAgentRegistrationModal: typeof import('./components/interceptors/agent/RegistrationModal.vue')['default']

View File

@@ -0,0 +1,181 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('agent.ca_certs')"
@close="emit('hide-modal')"
>
<template #body>
<div class="flex flex-col space-y-4">
<ul
v-if="certificates.length > 0"
class="mx-4 border border-dividerDark rounded"
>
<li
v-for="(certificate, index) in certificates"
:key="index"
class="flex border-dividerDark px-2 items-center justify-between"
:class="{ 'border-t border-dividerDark': index !== 0 }"
>
<div class="truncate">
{{ certificate.filename }}
</div>
<div class="flex items-center">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="certificate.enabled ? IconCheckCircle : IconCircle"
:title="
certificate.enabled
? t('action.turn_off')
: t('action.turn_on')
"
color="green"
@click="toggleEntryEnabled(index)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconTrash"
:title="t('action.remove')"
@click="deleteEntry(index)"
/>
</div>
</li>
</ul>
<HoppButtonSecondary
class="mx-4"
:icon="IconPlus"
:label="t('agent.add_cert_file')"
:loading="selectedFiles && selectedFiles!.length > 0"
filled
outline
@click="openFilePicker"
/>
<p class="text-center text-secondaryLight">
Hoppscotch supports .crt, .cer or .pem files containing one or more
certificates.
</p>
</div>
</template>
<template #footer>
<div class="flex space-x-2">
<HoppButtonPrimary :label="t('action.save')" @click="save" />
<HoppButtonSecondary
:label="t('action.cancel')"
filled
outline
@click="emit('hide-modal')"
/>
</div>
</template>
</HoppSmartModal>
</template>
<!-- TODO: i18n -->
<script setup lang="ts">
import IconPlus from "~icons/lucide/plus"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconCircle from "~icons/lucide/circle"
import IconTrash from "~icons/lucide/trash"
import { useService } from "dioc/vue"
import { ref, watch } from "vue"
import { useFileDialog } from "@vueuse/core"
import { cloneDeep } from "lodash-es"
import { useI18n } from "@composables/i18n"
import {
CACertificateEntry,
AgentInterceptorService,
} from "~/platform/std/interceptors/agent"
import { useToast } from "@composables/toast"
import { hasValidExtension } from "~/helpers/utils/file-extension"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const nativeInterceptorService = useService(AgentInterceptorService)
const certificates = ref<CACertificateEntry[]>([])
const {
files: selectedFiles,
open: openFilePicker,
reset: resetFilePicker,
onChange: onSelectedFilesChange,
} = useFileDialog({
multiple: true,
})
const ALLOWED_EXTENSIONS = [".crt", ".cer", ".pem"]
function isValidCertType(filename: string): boolean {
return hasValidExtension(filename, ALLOWED_EXTENSIONS)
}
// When files are selected, add them to the list of certificates and reset the file list
onSelectedFilesChange(async (files) => {
if (files) {
const addedCertificates: CACertificateEntry[] = []
for (let i = 0; i < files.length; i++) {
const file = files[i]
if (!isValidCertType(file.name)) {
toast.error(t("error.invalid_file_type", { filename: file.name }))
continue
}
const data = new Uint8Array(await file.arrayBuffer())
addedCertificates.push({
filename: file.name,
enabled: true,
certificate: data,
})
}
certificates.value.push(...addedCertificates)
resetFilePicker()
}
})
// When the modal is shown, clone the certificates from the service,
// We only write to the service when the user clicks on save
watch(
() => props.show,
(show) => {
if (show) {
certificates.value = cloneDeep(
nativeInterceptorService.caCertificates.value
)
} else {
resetFilePicker()
}
}
)
function save() {
nativeInterceptorService.caCertificates.value = certificates.value
emit("hide-modal")
}
function deleteEntry(index: number) {
certificates.value.splice(index, 1)
}
function toggleEntryEnabled(index: number) {
certificates.value[index].enabled = !certificates.value[index].enabled
}
</script>

View File

@@ -9,37 +9,29 @@
</div>
<div class="flex space-x-4">
<!--
<HoppButtonSecondary
:icon="IconLucideFileBadge"
:label="'CA Certificates'"
outline
@click="showCACertificatesModal = true"
/>
-->
<!--
<HoppButtonSecondary
:icon="IconLucideFileKey"
:label="t('agent.client_certs')"
outline
@click="showClientCertificatesModal = true"
/>
-->
</div>
<!--
<ModalsNativeCACertificates
<InterceptorsAgentModalNativeCACertificates
:show="showCACertificatesModal"
@hide-modal="showCACertificatesModal = false"
/>
-->
<!--
<InterceptorsAgentModalNativeClientCertificates
:show="showClientCertificatesModal"
@hide-modal="showClientCertificatesModal = false"
/>
-->
<div class="pt-4 space-y-4">
<div class="flex items-center">
@@ -68,7 +60,7 @@
<script setup lang="ts">
import { computed, ref } from "vue"
import { useI18n } from "@composables/i18n"
// import IconLucideFileKey from "~icons/lucide/file-key"
import IconLucideFileKey from "~icons/lucide/file-key"
import { useService } from "dioc/vue"
import {
RequestDef,
@@ -84,8 +76,8 @@ const agentInterceptorService = useService(AgentInterceptorService)
const allowSSLVerification = agentInterceptorService.validateCerts
// const showCACertificatesModal = ref(false)
// const showClientCertificatesModal = ref(false)
const showCACertificatesModal = ref(false)
const showClientCertificatesModal = ref(false)
const allowProxy = ref(false)
const proxyURL = ref("")

View File

@@ -0,0 +1,7 @@
export function hasValidExtension(
filename: string,
allowedExtensions: string[]
): boolean {
const ext = filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2)
return allowedExtensions.includes(`.${ext.toLowerCase()}`)
}

View File

@@ -0,0 +1,3 @@
source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0="
use devenv

10
packages/hoppscotch-relay/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
/target
# Devenv
.devenv*
devenv.local.nix
# direnv
.direnv
# pre-commit
.pre-commit-config.yaml

644
packages/hoppscotch-relay/Cargo.lock generated Normal file
View File

@@ -0,0 +1,644 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anstream"
version = "0.6.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bytes"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]]
name = "cc"
version = "1.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "colorchoice"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "curl"
version = "0.4.47"
source = "git+https://github.com/CuriousCorrelation/curl-rust.git#1ec8079cf527b9cf47cc7a48c68b458affdae273"
dependencies = [
"curl-sys",
"libc",
"openssl-probe",
"openssl-sys",
"socket2",
]
[[package]]
name = "curl-sys"
version = "0.4.77+curl-8.10.1"
source = "git+https://github.com/CuriousCorrelation/curl-rust.git#1ec8079cf527b9cf47cc7a48c68b458affdae273"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"windows-sys",
]
[[package]]
name = "env_filter"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "hoppscotch-relay"
version = "0.1.1"
dependencies = [
"curl",
"env_logger",
"http",
"log",
"openssl",
"openssl-sys",
"serde",
"serde_json",
"thiserror",
"tokio-util",
"url-escape",
]
[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "libc"
version = "0.2.161"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1"
[[package]]
name = "libz-sys"
version = "1.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "miniz_oxide"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
]
[[package]]
name = "object"
version = "0.36.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "openssl"
version = "0.10.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "300.4.0+3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project-lite"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pkg-config"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "proc-macro2"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "socket2"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "syn"
version = "2.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
dependencies = [
"backtrace",
"pin-project-lite",
]
[[package]]
name = "tokio-util"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "url-escape"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44e0ce4d1246d075ca5abec4b41d33e87a6054d08e2366b63205665e950db218"
dependencies = [
"percent-encoding",
]
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View File

@@ -0,0 +1,21 @@
[package]
name = "hoppscotch-relay"
version = "0.1.1"
description = "A HTTP request-response relay used by Hoppscotch Desktop and Hoppscotch Agent for advanced request handling including custom headers, certificates, proxies, and local system integration."
authors = ["CuriousCorrelation"]
edition = "2021"
[dependencies]
curl = { git = "https://github.com/CuriousCorrelation/curl-rust.git", features = ["ntlm"] }
tokio-util = "0.7.12"
openssl = { version = "0.10.66", features = ["vendored"] }
# NOTE: This crate follows `openssl-sys` from https://github.com/CuriousCorrelation/curl-rust.git
# to avoid issues from version mismatch when compiling from source.
openssl-sys = { version = "0.9.64", features = ["vendored"] }
log = "0.4.22"
env_logger = "0.11.5"
thiserror = "1.0.64"
http = "1.1.0"
url-escape = "0.1.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 CuriousCorrelation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,201 @@
# Hoppscotch Relay
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
A high-performance HTTP request-response relay used by Hoppscotch Desktop and Hoppscotch Agent for advanced request handling including CORS override, custom headers, certificates, proxies, and local system integration. It uses a custom fork of curl-rust with static OpenSSL builds for consistent SSL/TLS behavior across different platforms.
## Features
- 🚀 **Full HTTP Support**: Handle GET, POST, PUT, DELETE, and other HTTP methods
- 📦 **Multiple Body Types**:
- Raw text/JSON
- URL-encoded forms
- Multipart form data
- File uploads
- 🔒 **Security**:
- Client certificate authentication (PEM & PFX/PKCS#12)
- Custom root certificate bundles
- Certificate validation control
- 🌐 **Proxy Support**:
- HTTP/HTTPS proxy configuration
- Authentication support
- NTLM support
-**Performance**:
- Async design
- Request cancellation support
- Progress logs
- 📊 **Detailed Metrics**:
- Response timing
- Status tracking
- Header parsing
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
hoppscotch-relay = "0.1.1"
```
## Usage
### Basic Request
```rust
use hoppscotch_relay::{RequestWithMetadata, KeyValuePair};
use tokio_util::sync::CancellationToken;
// Create a basic GET request
let request = RequestWithMetadata::new(
1, // Request ID
"GET".to_string(), // Method
"https://api.example.com/data".to_string(), // Endpoint
vec![ // Headers
KeyValuePair {
key: "Accept".to_string(),
value: "application/json".to_string(),
}
],
None, // Body
true, // Validate certificates
vec![], // Root certificate bundles
None, // Client certificate
None, // Proxy configuration
);
// Execute the request with cancellation support
let cancel_token = CancellationToken::new();
let response = hoppscotch_relay::run_request_task(&request, cancel_token)?;
println!("Status: {} {}", response.status, response.status_text);
println!("Response time: {}ms", response.time_end_ms - response.time_start_ms);
```
### POST Request with JSON Body
```rust
let mut request = RequestWithMetadata::new(
2,
"POST".to_string(),
"https://api.example.com/users".to_string(),
vec![
KeyValuePair {
key: "Content-Type".to_string(),
value: "application/json".to_string(),
}
],
Some(BodyDef::Text(r#"{"name": "John Doe"}"#.to_string())),
true,
vec![],
None,
None,
);
let response = hoppscotch_relay::run_request_task(&request, CancellationToken::new())?;
```
### File Upload with Form Data
```rust
let form_data = vec![
FormDataEntry {
key: "file".to_string(),
value: FormDataValue::File {
filename: "document.pdf".to_string(),
data: std::fs::read("document.pdf")?,
mime: "application/pdf".to_string(),
},
},
FormDataEntry {
key: "description".to_string(),
value: FormDataValue::Text("Important document".to_string()),
},
];
let mut request = RequestWithMetadata::new(
3,
"POST".to_string(),
"https://api.example.com/upload".to_string(),
vec![],
Some(BodyDef::FormData(form_data)),
true,
vec![],
None,
None,
);
```
### Client Certificate Authentication
```rust
let client_cert = ClientCertDef::PEMCert {
certificate_pem: std::fs::read("client.crt")?,
key_pem: std::fs::read("client.key")?,
};
let mut request = RequestWithMetadata::new(
4,
"GET".to_string(),
"https://secure-api.example.com".to_string(),
vec![],
None,
true,
vec![],
Some(client_cert),
None,
);
```
### Proxy Configuration
```rust
let proxy_config = ProxyConfig {
url: "http://proxy.example.com:8080".to_string(),
};
let mut request = RequestWithMetadata::new(
5,
"GET".to_string(),
"https://api.example.com".to_string(),
vec![],
None,
true,
vec![],
None,
Some(proxy_config),
);
```
## Request Cancellation
The library supports request cancellation through Tokio's `CancellationToken`:
```rust
use tokio_util::sync::CancellationToken;
let cancel_token = CancellationToken::new();
let cancel_token_clone = cancel_token.clone();
// Spawn the request in a separate task
let request_handle = tokio::spawn(async move {
hoppscotch_relay::run_request_task(&request, cancel_token_clone)
});
// Cancel the request after 5 seconds
tokio::time::sleep(Duration::from_secs(5)).await;
cancel_token.cancel();
```
## Building from Source
1. Clone the repository:
```bash
git clone https://github.com/hoppscotch/hoppscotch-relay
cd hoppscotch-relay
```
2. Build the project:
```bash
cargo build --release
```

View File

@@ -0,0 +1,153 @@
{
"nodes": {
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1729641677,
"owner": "cachix",
"repo": "devenv",
"rev": "4f634c92037d3fb7a7cc2feddc4d686ace83b57f",
"type": "github"
},
"original": {
"dir": "src/modules",
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1729578683,
"owner": "nix-community",
"repo": "fenix",
"rev": "d66cda53e8193a878742dcadb5bb75f4df7c3c0a",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1729501122,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "56c7c4a3f5fdbef5bf81c7d9c28fbb45dc626611",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1729449015,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "89172919243df199fe237ba0f776c3e3e3d72367",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1729104314,
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "3c3e88f0f544d6bb54329832616af7eb971b6be6",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"fenix": "fenix",
"nixpkgs": "nixpkgs",
"pre-commit-hooks": "pre-commit-hooks"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1729618852,
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "c2867868889a549562a7b53fb572719f852a8a6f",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -0,0 +1,54 @@
{ pkgs, lib, config, inputs, ... }:
{
# https://devenv.sh/packages/
packages = with pkgs; [
git
# Cargo
cargo-edit
];
# https://devenv.sh/basics/
env = {
APP_GREET = "Hoppscotch";
};
# https://devenv.sh/scripts/
scripts.hello.exec = "echo hello from $APP_GREET";
enterShell = ''
git --version
'';
# https://devenv.sh/tests/
enterTest = ''
echo "Running tests"
'';
# https://devenv.sh/integrations/dotenv/
dotenv.enable = true;
# https://devenv.sh/languages/
languages.rust = {
enable = true;
channel = "nightly";
components = [
"rustc"
"cargo"
"clippy"
"rustfmt"
"rust-analyzer"
"llvm-tools-preview"
"rust-src"
"rustc-codegen-cranelift-preview"
];
};
# https://devenv.sh/pre-commit-hooks/
# pre-commit.hooks.shellcheck.enable = true;
# https://devenv.sh/processes/
# processes.ping.exec = "ping example.com";
# See full reference at https://devenv.sh/reference/options/
}

View File

@@ -0,0 +1,23 @@
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
inputs:
# For NodeJS-22 and above
nixpkgs:
url: github:NixOS/nixpkgs/nixpkgs-unstable
# nixpkgs:
# url: github:cachix/devenv-nixpkgs/rolling
fenix:
url: github:nix-community/fenix
inputs:
nixpkgs:
follows: nixpkgs
# If you're using non-OSS software, you can set allowUnfree to true.
allowUnfree: true
# If you're willing to use a package that's vulnerable
# permittedInsecurePackages:
# - "openssl-1.1.1w"
# If you have more than one devenv you can merge them
#imports:
# - ./backend

View File

@@ -0,0 +1,16 @@
use serde::Serialize;
use thiserror::Error;
#[derive(Debug, Error, Serialize)]
pub enum RelayError {
#[error("Invalid method")]
InvalidMethod,
#[error("Invalid URL")]
InvalidUrl,
#[error("Invalid headers")]
InvalidHeaders,
#[error("Request run error: {0}")]
RequestRunError(String),
}
pub type RelayResult<T> = std::result::Result<T, RelayError>;

View File

@@ -0,0 +1,96 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KeyValuePair {
pub key: String,
pub value: String,
}
#[derive(Debug, Deserialize)]
pub enum FormDataValue {
Text(String),
File {
filename: String,
data: Vec<u8>,
mime: String,
},
}
#[derive(Debug, Deserialize)]
pub struct FormDataEntry {
pub key: String,
pub value: FormDataValue,
}
#[derive(Debug, Deserialize)]
pub enum BodyDef {
Text(String),
URLEncoded(Vec<KeyValuePair>),
FormData(Vec<FormDataEntry>),
}
#[derive(Debug, Deserialize)]
pub struct RequestWithMetadata {
pub req_id: usize,
pub method: String,
pub endpoint: String,
pub headers: Vec<KeyValuePair>,
pub body: Option<BodyDef>,
pub validate_certs: bool,
pub root_cert_bundle_files: Vec<Vec<u8>>,
pub client_cert: Option<ClientCertDef>,
pub proxy: Option<ProxyConfig>,
}
impl RequestWithMetadata {
pub fn new(
req_id: usize,
method: String,
endpoint: String,
headers: Vec<KeyValuePair>,
body: Option<BodyDef>,
validate_certs: bool,
root_cert_bundle_files: Vec<Vec<u8>>,
client_cert: Option<ClientCertDef>,
proxy: Option<ProxyConfig>,
) -> Self {
Self {
req_id,
method,
endpoint,
headers,
body,
validate_certs,
root_cert_bundle_files,
client_cert,
proxy,
}
}
}
#[derive(Debug, Deserialize)]
pub struct ProxyConfig {
pub url: String,
}
#[derive(Debug, Deserialize)]
pub enum ClientCertDef {
PEMCert {
certificate_pem: Vec<u8>,
key_pem: Vec<u8>,
},
PFXCert {
certificate_pfx: Vec<u8>,
password: String,
},
}
#[derive(Debug, Serialize)]
pub struct ResponseWithMetadata {
pub status: u16,
pub status_text: String,
pub headers: Vec<KeyValuePair>,
pub data: Vec<u8>,
pub time_start_ms: u128,
pub time_end_ms: u128,
}

View File

@@ -0,0 +1,23 @@
pub(crate) mod error;
pub(crate) mod interop;
pub(crate) mod relay;
pub(crate) mod util;
pub use error::{RelayError, RelayResult};
pub use interop::{RequestWithMetadata, ResponseWithMetadata};
pub use relay::run_request_task;
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

View File

@@ -0,0 +1,595 @@
use curl::easy::{Easy, List};
use openssl::{pkcs12::Pkcs12, ssl::SslContextBuilder, x509::X509};
use openssl_sys::SSL_CTX;
use std::time::SystemTime;
use tokio_util::sync::CancellationToken;
use crate::{
error::RelayError,
interop::{
BodyDef, ClientCertDef, FormDataValue, KeyValuePair, RequestWithMetadata,
ResponseWithMetadata,
},
util::get_status_text,
};
pub fn run_request_task(
req: &RequestWithMetadata,
cancel_token: CancellationToken,
) -> Result<ResponseWithMetadata, RelayError> {
log::info!(
"Starting request task: [Method: {}] [URL: {}] [Validate Certs: {}] [Has Body: {}] [Proxy Enabled: {}]",
req.method,
req.endpoint,
req.validate_certs,
req.body.is_some(),
req.proxy.is_some()
);
let mut curl_handle = Easy::new();
log::debug!("Initialized new curl handle with default settings");
match curl_handle.progress(true) {
Ok(_) => log::debug!("Progress tracking enabled for request monitoring"),
Err(err) => {
log::error!(
"Critical failure enabling progress tracking: {}\nError details: {:?}",
err,
err
);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
match curl_handle.custom_request(&req.method) {
Ok(_) => log::debug!("HTTP method set: {}", req.method),
Err(err) => {
log::error!("Failed to set HTTP method '{}'. Error: {}", req.method, err);
return Err(RelayError::InvalidMethod);
}
}
match curl_handle.url(&req.endpoint) {
Ok(_) => log::debug!("Target URL configured: {}", req.endpoint),
Err(err) => {
log::error!(
"URL configuration failed for '{}'\nError: {}",
req.endpoint,
err
);
return Err(RelayError::InvalidUrl);
}
}
let headers = match get_headers_list(&req) {
Ok(headers) => {
log::debug!("Generated headers list");
headers
}
Err(err) => {
log::error!("Header generation failed:\nError: {:?}", err);
return Err(err);
}
};
match curl_handle.http_headers(headers) {
Ok(_) => log::debug!("Successfully configured request headers"),
Err(err) => {
log::error!("Failed to set HTTP headers: {}", err);
return Err(RelayError::InvalidHeaders);
}
}
if let Err(err) = apply_body_to_curl_handle(&mut curl_handle, &req) {
log::error!(
"Request body application failed:\nError: {:?}\nContent-Type: {:?}",
err,
req.headers
.iter()
.find(|h| h.key.to_lowercase() == "content-type")
.map(|h| &h.value)
);
return Err(err);
}
log::debug!("Request body configured successfully");
match curl_handle.ssl_verify_peer(req.validate_certs) {
Ok(_) => log::debug!(
"SSL peer verification setting applied: {}",
req.validate_certs
),
Err(err) => {
log::error!(
"SSL peer verification configuration failed: {}\nRequested setting: {}",
err,
req.validate_certs
);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
match curl_handle.ssl_verify_host(req.validate_certs) {
Ok(_) => log::debug!(
"SSL host verification setting applied: {}",
req.validate_certs
),
Err(err) => {
log::error!(
"SSL host verification configuration failed: {}\nRequested setting: {}",
err,
req.validate_certs
);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
if let Err(err) = apply_client_cert_to_curl_handle(&mut curl_handle, &req) {
log::error!(
"Client certificate configuration failed:\nError: {:?}\nCert Info: {:#?}",
err,
req.client_cert.as_ref()
);
return Err(err);
}
log::debug!("Client certificate configuration successful");
if let Err(err) = apply_proxy_config_to_curl_handle(&mut curl_handle, &req) {
log::error!(
"Proxy configuration failed:\nError: {:?}\nProxy Info: {:?}",
err,
req.proxy.as_ref()
);
return Err(err);
}
log::debug!("Proxy configuration applied successfully");
let mut response_body = Vec::new();
let mut response_headers = Vec::new();
let (start_time_ms, end_time_ms) = {
let mut transfer = curl_handle.transfer();
log::debug!("Created curl transfer object for request execution");
match transfer.ssl_ctx_function(|ssl_ctx_ptr| {
let cert_list = match get_x509_certs_from_root_cert_bundle_safe(&req) {
Ok(certs) => {
log::debug!("Found {} certificates in root bundle", certs.len());
certs
}
Err(e) => {
log::error!("Failed to load certificates from bundle: {:?}", e);
return Ok(());
}
};
if !cert_list.is_empty() {
let mut ssl_ctx_builder =
unsafe { SslContextBuilder::from_ptr(ssl_ctx_ptr as *mut SSL_CTX) };
let cert_store = ssl_ctx_builder.cert_store_mut();
for (index, cert) in cert_list.iter().enumerate() {
log::debug!(
"Processing certificate {}: Subject: {:?}, Not Before: {:?}, Not After: {:?}",
index,
cert.subject_name(),
cert.not_before(),
cert.not_after()
);
if let Err(e) = cert_store.add_cert(cert.clone()) {
log::warn!(
"Failed to add certificate {} to store\nError: {}\nCert details: {:?}",
index,
e,
cert.subject_name()
);
} else {
log::debug!(
"Successfully added certificate {} to store\nSubject: {:?}",
index,
cert.subject_name()
);
}
}
// SAFETY: We need to prevent Rust from dropping the `SslContextBuilder` because
// the underlying `SSL_CTX` pointer is owned and managed by curl, not us.
// From curl docs: "libcurl does not guarantee the lifetime of the passed in
// object once this callback function has returned"
// and `SslContextBuilder` is just a safe wrapper around curl's `SSL_CTX` from
// `openssl_sys::SSL_CTX`.
// If dropped, Rust would try to free the `SSL_CTX` which curl still needs.
//
// This intentional "leak" is safe because:
// - We're only leaking the thin Rust wrapper
// - Curl manages the actual `SSL_CTX` memory
// - Curl will free the `SSL_CTX` during connection cleanup
//
// See: https://curl.se/libcurl/c/CURLOPT_SSL_CTX_FUNCTION.html
std::mem::forget(ssl_ctx_builder);
}
Ok(())
}) {
Ok(_) => log::debug!("SSL context function configured successfully"),
Err(err) => {
log::error!("SSL context function setup failed: {}", err);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
match transfer.progress_function(|dltotal, dlnow, ultotal, ulnow| {
let cancelled = cancel_token.is_cancelled();
if cancelled {
log::warn!(
"Request cancelled by user\nDownload: {}/{} bytes\nUpload: {}/{} bytes",
dlnow,
dltotal,
ulnow,
ultotal
);
} else {
log::debug!(
"Progress - Download: {}/{} bytes, Upload: {}/{} bytes",
dlnow,
dltotal,
ulnow,
ultotal
);
}
!cancelled
}) {
Ok(_) => log::debug!("Progress monitoring function configured"),
Err(err) => {
log::error!("Progress function setup failed: {}", err);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
match transfer.header_function(|header| {
let header = String::from_utf8_lossy(header).into_owned();
if let Some((key, value)) = header.split_once(':') {
log::debug!("Received header: [{}] = [{}]", key.trim(), value.trim());
response_headers.push(KeyValuePair {
key: key.trim().to_string(),
value: value.trim().to_string(),
});
} else {
log::debug!("Received header line (no key-value): {}", header.trim());
}
true
}) {
Ok(_) => log::debug!("Header processing function configured"),
Err(err) => {
log::error!("Header function setup failed: {}", err);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
match transfer.write_function(|data| {
let chunk_size = data.len();
response_body.extend_from_slice(data);
log::debug!(
"Received response chunk: {} bytes (Total size so far: {} bytes)",
chunk_size,
response_body.len()
);
Ok(chunk_size)
}) {
Ok(_) => log::debug!("Response body processing function configured"),
Err(err) => {
log::error!("Write function setup failed: {}", err);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
}
let start_time_ms = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
log::info!(
"Initiating request transfer at timestamp: {}",
start_time_ms
);
if let Err(err) = transfer.perform() {
log::error!(
"Request transfer failed:\nError: {}\nTime elapsed: {}ms",
err,
SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
- start_time_ms,
);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
let end_time_ms = SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
log::info!(
"Request transfer completed:\nDuration: {}ms",
end_time_ms - start_time_ms,
);
(start_time_ms, end_time_ms)
};
let response_status = match curl_handle.response_code() {
Ok(status) => {
let status = status as u16;
log::info!(
"Response status code: {} ({})",
status,
get_status_text(status)
);
status
}
Err(err) => {
log::error!("Failed to retrieve response code: {}", err);
return Err(RelayError::RequestRunError(err.description().to_string()));
}
};
let response_status_text = get_status_text(response_status).to_string();
log::info!(
"Request completed successfully:\nStatus: {} ({})\nDuration: {}ms\n\
Response size: {} bytes\nHeaders: {} received\nEndpoint: {}",
response_status,
response_status_text,
end_time_ms - start_time_ms,
response_body.len(),
response_headers.len(),
req.endpoint
);
Ok(ResponseWithMetadata {
status: response_status,
status_text: response_status_text,
headers: response_headers,
data: response_body,
time_start_ms: start_time_ms,
time_end_ms: end_time_ms,
})
}
fn get_headers_list(req: &RequestWithMetadata) -> Result<List, RelayError> {
let mut result = List::new();
for KeyValuePair { key, value } in &req.headers {
result
.append(&format!("{}: {}", key, value))
.map_err(|err| RelayError::RequestRunError(err.description().to_string()))?;
}
Ok(result)
}
fn apply_body_to_curl_handle(
curl_handle: &mut Easy,
req: &RequestWithMetadata,
) -> Result<(), RelayError> {
match &req.body {
Some(BodyDef::Text(text)) => {
curl_handle
.post_fields_copy(text.as_bytes())
.map_err(|err| {
RelayError::RequestRunError(format!(
"Error while setting body: {}",
err.description()
))
})?;
}
Some(BodyDef::FormData(entries)) => {
let mut form = curl::easy::Form::new();
for entry in entries {
let mut part = form.part(&entry.key);
match &entry.value {
FormDataValue::Text(data) => {
part.contents(data.as_bytes());
}
FormDataValue::File {
filename,
data,
mime,
} => {
part.buffer(filename, data.clone()).content_type(mime);
}
};
part.add().map_err(|err| {
RelayError::RequestRunError(format!(
"Error while setting body: {}",
err.description()
))
})?;
}
curl_handle.httppost(form).map_err(|err| {
RelayError::RequestRunError(format!(
"Error while setting body: {}",
err.description()
))
})?;
}
Some(BodyDef::URLEncoded(entries)) => {
let data = entries
.iter()
.map(|KeyValuePair { key, value }| {
format!(
"{}={}",
&url_escape::encode_www_form_urlencoded(key),
url_escape::encode_www_form_urlencoded(value)
)
})
.collect::<Vec<String>>()
.join("&");
curl_handle
.post_fields_copy(data.as_bytes())
.map_err(|err| {
RelayError::RequestRunError(format!(
"Error while setting body: {}",
err.description()
))
})?;
}
None => {}
};
Ok(())
}
fn apply_client_cert_to_curl_handle(
handle: &mut Easy,
req: &RequestWithMetadata,
) -> Result<(), RelayError> {
match &req.client_cert {
Some(ClientCertDef::PEMCert {
certificate_pem,
key_pem,
}) => {
handle.ssl_cert_type("PEM").map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM Cert Type: {}",
err.description()
))
})?;
handle.ssl_cert_blob(certificate_pem).map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM Cert Blob: {}",
err.description()
))
})?;
handle.ssl_key_type("PEM").map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM key type: {}",
err.description()
))
})?;
handle.ssl_key_blob(key_pem).map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM Cert blob: {}",
err.description()
))
})?;
}
Some(ClientCertDef::PFXCert {
certificate_pfx,
password,
}) => {
let pkcs12 = Pkcs12::from_der(&certificate_pfx).map_err(|err| {
RelayError::RequestRunError(format!(
"Failed to parse PFX certificate from DER: {}",
err
))
})?;
let parsed = pkcs12.parse2(password).map_err(|err| {
RelayError::RequestRunError(format!(
"Failed to parse PFX certificate with provided password: {}",
err
))
})?;
if let (Some(cert), Some(key)) = (parsed.cert, parsed.pkey) {
let certificate_pem = cert.to_pem().map_err(|err| {
RelayError::RequestRunError(format!(
"Failed to convert PFX certificate to PEM format: {}",
err
))
})?;
let key_pem = key.private_key_to_pem_pkcs8().map_err(|err| {
RelayError::RequestRunError(format!(
"Failed to convert PFX private key to PEM format: {}",
err
))
})?;
handle.ssl_cert_type("PEM").map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM Cert Type for converted PFX: {}",
err.description()
))
})?;
handle.ssl_cert_blob(&certificate_pem).map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM Cert Blob for converted PFX: {}",
err.description()
))
})?;
handle.ssl_key_type("PEM").map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM key type for converted PFX: {}",
err.description()
))
})?;
handle.ssl_key_blob(&key_pem).map_err(|err| {
RelayError::RequestRunError(format!(
"Failed setting PEM key blob for converted PFX: {}",
err.description()
))
})?;
} else {
return Err(RelayError::RequestRunError(
"PFX certificate parsing succeeded, but either cert or private key is missing"
.to_string(),
));
}
}
None => {}
};
Ok(())
}
fn get_x509_certs_from_root_cert_bundle_safe(
req: &RequestWithMetadata,
) -> Result<Vec<X509>, openssl::error::ErrorStack> {
let mut certs = Vec::new();
for pem_bundle in &req.root_cert_bundle_files {
match openssl::x509::X509::stack_from_pem(pem_bundle) {
Ok(mut bundle_certs) => certs.append(&mut bundle_certs),
Err(e) => {
log::warn!("Failed to parse certificate bundle: {:?}", e);
}
}
}
Ok(certs)
}
fn apply_proxy_config_to_curl_handle(
handle: &mut Easy,
req: &RequestWithMetadata,
) -> Result<(), RelayError> {
if let Some(proxy_config) = &req.proxy {
handle
.proxy_auth(curl::easy::Auth::new().auto(true))
.map_err(|err| {
RelayError::RequestRunError(format!(
"Failed to set proxy Auth Mode: {}",
err.description()
))
})?;
handle.proxy(&proxy_config.url).map_err(|err| {
RelayError::RequestRunError(format!("Failed to set proxy URL: {}", err.description()))
})?;
}
Ok(())
}

View File

@@ -0,0 +1,6 @@
pub fn get_status_text(status: u16) -> &'static str {
http::StatusCode::from_u16(status)
.map(|status| status.canonical_reason())
.unwrap_or(Some("Unknown Status"))
.unwrap_or("Unknown Status")
}