Compare commits
3 Commits
f37befacdd
...
9621033530
| Author | SHA1 | Date | |
|---|---|---|---|
| 9621033530 | |||
| ccfd200681 | |||
| 1672e5367e |
Generated
+132
-45
@@ -214,6 +214,12 @@ dependencies = [
|
|||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.14"
|
version = "0.3.14"
|
||||||
@@ -240,6 +246,12 @@ version = "0.2.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fnv"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -261,6 +273,24 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.9"
|
version = "0.14.9"
|
||||||
@@ -294,6 +324,31 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"http",
|
||||||
|
"indexmap",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hkdf"
|
name = "hkdf"
|
||||||
version = "0.12.4"
|
version = "0.12.4"
|
||||||
@@ -359,21 +414,21 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.8.1"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@@ -393,17 +448,29 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "indexmap"
|
||||||
version = "1.0.17"
|
version = "2.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.91"
|
version = "0.3.97"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
@@ -442,9 +509,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.183"
|
version = "0.2.186"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libm"
|
name = "libm"
|
||||||
@@ -466,9 +533,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.1.1"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
@@ -503,9 +570,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
@@ -539,9 +606,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "p256"
|
name = "p256"
|
||||||
@@ -592,12 +659,6 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-utils"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkcs1"
|
name = "pkcs1"
|
||||||
version = "0.7.5"
|
version = "0.7.5"
|
||||||
@@ -663,9 +724,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.8.5"
|
version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
@@ -752,9 +813,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "1.0.27"
|
version = "1.0.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
@@ -858,6 +919,12 @@ dependencies = [
|
|||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.1"
|
version = "1.15.1"
|
||||||
@@ -960,10 +1027,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.50.0"
|
version = "1.52.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
@@ -975,9 +1043,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "2.6.1"
|
version = "2.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -998,10 +1066,29 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "tracing"
|
||||||
version = "1.19.0"
|
version = "0.1.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
|
dependencies = [
|
||||||
|
"pin-project-lite",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-core"
|
||||||
|
version = "0.1.36"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
@@ -1023,9 +1110,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.114"
|
version = "0.2.120"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -1036,9 +1123,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.114"
|
version = "0.2.120"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -1046,9 +1133,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.114"
|
version = "0.2.120"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1059,9 +1146,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.114"
|
version = "0.2.120"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -1083,18 +1170,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.42"
|
version = "0.8.48"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
|
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive",
|
"zerocopy-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.42"
|
version = "0.8.48"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
|
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
+2
-2
@@ -11,8 +11,8 @@ http = "1.4.0"
|
|||||||
http-body-util = "0.1.3"
|
http-body-util = "0.1.3"
|
||||||
jsonwebtoken = { version = "10", features = ["rust_crypto"] }
|
jsonwebtoken = { version = "10", features = ["rust_crypto"] }
|
||||||
|
|
||||||
hyper = { version = "1.8.1", features = ["http1", "server"] }
|
hyper = { version = "1.8.1", features = ["http1", "http2", "server"] }
|
||||||
hyper-util = { version = "0.1", features = ["http1", "server", "tokio"] }
|
hyper-util = { version = "0.1", features = ["http1", "http2", "server", "tokio"] }
|
||||||
|
|
||||||
serde = {version = "1.0.228", features = ["derive"]}
|
serde = {version = "1.0.228", features = ["derive"]}
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
|
|||||||
@@ -1,139 +1,280 @@
|
|||||||
# Servme
|
# Servme
|
||||||
|
|
||||||
Un framework web HTTP de bajo nivel escrito en Rust, construido sobre Hyper.
|
Un framework web HTTP de alto rendimiento escrito en Rust, construido sobre [Hyper](https://hyper.rs/).
|
||||||
|
|
||||||
## Características
|
## Primeros pasos
|
||||||
|
|
||||||
- **Middleware System**: Pipeline extensible para autenticación (JWT, API Key, IP Filter)
|
### Instalación
|
||||||
- **Builder Pattern**: API fluente para configuración del servidor
|
|
||||||
- **Graceful Shutdown**: Manejo elegante de señales SIGINT/SIGTERM
|
|
||||||
- **Error Handling**: Sistema de errores tipado con `ServerError`
|
|
||||||
- **High Performance**: IP filtering con O(1) lookups usando HashSet
|
|
||||||
|
|
||||||
## Uso Básico
|
Añade `servme` a tu `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
servme = "0.1"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tu primer servidor
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use servme::{ServerBuilder, Responder, UrlExtract};
|
use http_body_util::Full;
|
||||||
|
use hyper::{body::Bytes, Request, Response};
|
||||||
|
use servme::{Responder, Server, ServerError};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() {
|
||||||
let server = ServerBuilder::new()
|
Server::builder()
|
||||||
.address("127.0.0.1", 8080)
|
.address("127.0.0.1", 8080)
|
||||||
.handler(|req, res| async {
|
.build()
|
||||||
let url = UrlExtract::new(req.uri());
|
.run(handler)
|
||||||
Responder::ok(format!("Hello, {}!", url.param_str("name").unwrap_or_default()))
|
.await;
|
||||||
})
|
}
|
||||||
.build();
|
|
||||||
|
|
||||||
server.run().await
|
async fn handler(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, ServerError> {
|
||||||
|
Responder::ok(format!("Hola! Path: {}", req.uri()))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Middlewares
|
> **Para usuarios nuevos**: Ejecuta `cargo run` y visita `http://127.0.0.1:8080` en tu navegador.
|
||||||
|
|
||||||
### API Key Authentication
|
---
|
||||||
|
|
||||||
|
## Conceptos básicos
|
||||||
|
|
||||||
|
### El Handler
|
||||||
|
|
||||||
|
El handler es una función asíncrona que recibe un `Request` y retorna una `Response`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use servme::{ServerBuilder, middleware::ApiKeyMiddleware};
|
use http_body_util::Full;
|
||||||
|
use hyper::{body::Bytes, Request, Response};
|
||||||
|
|
||||||
let server = ServerBuilder::new()
|
async fn handler(req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, ServerError> {
|
||||||
.address("127.0.0.1", 8080)
|
// Tu lógica aquí
|
||||||
.add_api_key_middleware("your-secret-key")
|
Responder::ok("Respuesta")
|
||||||
.build();
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### JWT Authentication
|
### Responder
|
||||||
|
|
||||||
```rust
|
`Responder` proporciona métodos auxiliares para crear respuestas HTTP comunes:
|
||||||
use servme::{ServerBuilder, middleware::JwtMiddleware};
|
|
||||||
|
|
||||||
let server = ServerBuilder::new()
|
|
||||||
.address("127.0.0.1", 8080)
|
|
||||||
.add_jwt_middleware("your-secret-key")
|
|
||||||
.build();
|
|
||||||
```
|
|
||||||
|
|
||||||
### IP Filtering
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use servme::{ServerBuilder, middleware::IpFilterMiddleware};
|
|
||||||
|
|
||||||
let server = ServerBuilder::new()
|
|
||||||
.address("127.0.0.1", 8080)
|
|
||||||
.add_ip_filter_middleware(
|
|
||||||
vec!["192.168.1.1".to_string(), "10.0.0.1".to_string()],
|
|
||||||
true // allow private IPs
|
|
||||||
)
|
|
||||||
.build();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Constantes Disponibles
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use servme::constants::{
|
|
||||||
DEFAULT_HOST, // "127.0.0.1"
|
|
||||||
DEFAULT_PORT, // 8080
|
|
||||||
JWT_COOKIE_NAME, // "access_token"
|
|
||||||
BEARER_PREFIX, // "Bearer "
|
|
||||||
FILE_EXTENSIONS, // [".json", ".html", ".css", ".js"]
|
|
||||||
MAX_ALLOWED_IPS, // 1000
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Responder Helpers
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use servme::Responder;
|
use servme::Responder;
|
||||||
|
|
||||||
// JSON response
|
// Respuestas de éxito
|
||||||
Responder::json(&data)?;
|
Responder::ok("Mensaje")?; // 200 OK
|
||||||
|
Responder::html("<h1>Título</h1>")?; // 200 con Content-Type: text/html
|
||||||
|
Responder::json(&datos)?; // 200 con Content-Type: application/json
|
||||||
|
Responder::redirect("/nueva-ruta")?; // 302 Redirect
|
||||||
|
Responder::no_content()?; // 204 No Content
|
||||||
|
|
||||||
// Redirect
|
// Respuestas de error
|
||||||
Responder::redirect("/new-location")?;
|
Responder::not_found()?; // 404
|
||||||
|
Responder::unauthorized()?; // 401
|
||||||
// Status codes
|
Responder::forbidden()?; // 403
|
||||||
Responder::not_found()?;
|
Responder::bad_request("Datos inválidos")?; // 400
|
||||||
Responder::unauthorized()?;
|
Responder::internal_error("Algo salió mal")?; // 500
|
||||||
Responder::forbidden()?;
|
|
||||||
Responder::bad_request("error message")?;
|
|
||||||
Responder::internal_error("error message")?;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Construcción y Tests
|
### Extraer datos del Request
|
||||||
|
|
||||||
```bash
|
```rust
|
||||||
# Build
|
use servme::Requester;
|
||||||
cargo build
|
|
||||||
|
|
||||||
# Run tests
|
// Extraer body JSON
|
||||||
cargo test
|
let data: MyStruct = Requester::extract_body(req).await?;
|
||||||
|
|
||||||
# Run with debug logging
|
// Extraer como texto
|
||||||
RUST_LOG=debug cargo run
|
let body_str: String = Requester::extract_body_str(req).await?;
|
||||||
|
|
||||||
|
// Extraer como bytes (más eficiente)
|
||||||
|
let body_bytes: Bytes = Requester::extract_body_bytes(req).await?;
|
||||||
|
|
||||||
|
// Extraer parámetros de URL
|
||||||
|
let url = UrlExtract::new(req.uri());
|
||||||
|
let name = url.param_str("nombre"); // ?nombre=Juan
|
||||||
|
let age: Option<i64> = url.param_i64("edad"); // ?edad=25
|
||||||
```
|
```
|
||||||
|
|
||||||
## Estructura del Proyecto
|
---
|
||||||
|
|
||||||
|
## Middleware
|
||||||
|
|
||||||
|
Los middleware se ejecutan antes del handler y pueden authnticar, filtrar o modificar requests.
|
||||||
|
|
||||||
|
### API Key
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use servme::{Server, ServerBuilder};
|
||||||
|
|
||||||
|
Server::builder()
|
||||||
|
.address("127.0.0.1", 8080)
|
||||||
|
.add_api_key_middleware("mi-clave-secreta")
|
||||||
|
.build()
|
||||||
|
.run(handler)
|
||||||
|
.await;
|
||||||
|
```
|
||||||
|
|
||||||
|
Envía la clave en el header `X-API-Key`.
|
||||||
|
|
||||||
|
### JWT Authentication
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use servme::{Server, ServerBuilder};
|
||||||
|
|
||||||
|
Server::builder()
|
||||||
|
.address("0.0.0.0", 8080)
|
||||||
|
.add_jwt_middleware(
|
||||||
|
rsa_public_key_pem, // Clave pública RSA en formato PEM
|
||||||
|
vec!["/health".to_string()], // Rutas públicas (sin auth)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.run(handler)
|
||||||
|
.await;
|
||||||
|
```
|
||||||
|
|
||||||
|
El JWT se valida desde:
|
||||||
|
- Header `Authorization: Bearer <token>`
|
||||||
|
- Cookie `access_token`
|
||||||
|
|
||||||
|
### Filtrado por IP
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use servme::ServerBuilder;
|
||||||
|
|
||||||
|
Server::builder()
|
||||||
|
.address("127.0.0.1", 8080)
|
||||||
|
.add_ip_filter_middleware(
|
||||||
|
vec!["192.168.1.100".to_string(), "10.0.0.1".to_string()],
|
||||||
|
true, // allow_private: también permitir IPs privadas (192.168.x.x, 10.x.x.x)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.run(handler)
|
||||||
|
.await;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Múltiples middlewares
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Server::builder()
|
||||||
|
.address("0.0.0.0", 8080)
|
||||||
|
.add_ip_filter_middleware(vec![], true) // Permitir todas las IPs privadas
|
||||||
|
.add_jwt_middleware(pub_key, vec!["/static".to_string(), "/health".to_string()])
|
||||||
|
.build()
|
||||||
|
.run(handler)
|
||||||
|
.await;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estado compartido
|
||||||
|
|
||||||
|
Puedes compartir datos entre requests usando `.data()`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
db: Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
Server::builder()
|
||||||
|
.address("127.0.0.1", 8080)
|
||||||
|
.data(AppState { db: Database::new() })
|
||||||
|
.build()
|
||||||
|
.run(|req| async move {
|
||||||
|
// Accede al estado desde las extensions del request
|
||||||
|
let state = req.extensions().get::<Arc<AppState>>().unwrap();
|
||||||
|
// Usa state.db...
|
||||||
|
Responder::ok("ok")
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Estructura del proyecto
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── lib.rs # Exports públicos
|
├── lib.rs # Exports públicos
|
||||||
├── main.rs # Binario de ejemplo
|
├── main.rs # Binario de ejemplo
|
||||||
├── builder.rs # ServerBuilder
|
├── builder.rs # ServerBuilder - configuración del servidor
|
||||||
├── config.rs # ServerConfig
|
├── config.rs # ServerConfig - estructura de configuración
|
||||||
├── server.rs # Servidor HTTP con graceful shutdown
|
├── server.rs # Servidor HTTP con graceful shutdown
|
||||||
├── error.rs # ServerError enum
|
├── error.rs # ServerError - tipos de errores
|
||||||
├── constants.rs # Constantes configurables
|
├── constants.rs # Constantes del framework
|
||||||
├── responder.rs # Helper para construir respuestas
|
├── responder.rs # Helpers para construir respuestas
|
||||||
├── requester.rs # Helper para extraer request info
|
├── requester.rs # Helpers para extraer datos del request
|
||||||
├── url_extract.rs # URL parsing y query params
|
├── url_extract.rs # Parsing de URLs y query params
|
||||||
└── middleware/
|
└── middleware/
|
||||||
├── mod.rs # Traits y tipos comunes
|
├── mod.rs # Traits y tipos comunes
|
||||||
├── api_key.rs # API Key authentication
|
├── api_key.rs # Autenticación por API Key
|
||||||
├── jwt.rs # JWT authentication
|
├── jwt.rs # Autenticación JWT (RS256)
|
||||||
├── ip_filter.rs # IP filtering
|
├── ip_filter.rs # Filtrado por dirección IP
|
||||||
└── auth_types.rs # Tipos de autenticación
|
└── auth_types.rs # Tipos para autenticación (Claims)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Construcción y testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compilar
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
# Ejecutar tests
|
||||||
|
cargo test
|
||||||
|
|
||||||
|
# Ejecutar con logs de debug
|
||||||
|
RUST_LOG=debug cargo run
|
||||||
|
|
||||||
|
# Ver documentation
|
||||||
|
cargo doc --open
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Errores comunes
|
||||||
|
|
||||||
|
### "Failed to bind to address"
|
||||||
|
|
||||||
|
El puerto está en uso. Prueba con otro puerto:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Server::builder()
|
||||||
|
.address("127.0.0.1", 8081) // Cambia el puerto
|
||||||
|
.build()
|
||||||
|
.run(handler)
|
||||||
|
.await;
|
||||||
|
```
|
||||||
|
|
||||||
|
### "JWT validation failed"
|
||||||
|
|
||||||
|
- Verifica que la clave pública RSA sea válida
|
||||||
|
- Asegúrate de que el token no esté expirado (`exp` claim)
|
||||||
|
- El token debe contener el claim `sub` (subject)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Constantes útiles
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use servme::{
|
||||||
|
DEFAULT_HOST, // "127.0.0.1"
|
||||||
|
DEFAULT_PORT, // 8080
|
||||||
|
JWT_COOKIE_NAME, // "access_token"
|
||||||
|
BEARER_PREFIX, // "Bearer "
|
||||||
|
MAX_ALLOWED_IPS, // 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
use servme::constants::FILE_EXTENSIONS; // Extensiones de archivos estáticos
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# Plan: Mejora de Public Routes en JWT Middleware
|
|
||||||
|
|
||||||
## Objective
|
|
||||||
|
|
||||||
Modificar la lógica de verificación de rutas públicas en el JWT middleware para que:
|
|
||||||
- Si el path tiene extensión de archivo → hacer match exacto del archivo
|
|
||||||
- Si el path NO tiene extensión → hacer match por prefijo (permitir todo bajo esa ruta)
|
|
||||||
|
|
||||||
## Implementation Plan
|
|
||||||
|
|
||||||
### Análisis del código actual
|
|
||||||
|
|
||||||
El código en `src/middleware/jwt.rs:56` hace:
|
|
||||||
```rust
|
|
||||||
let is_public_path = self.public_routes.contains(&req.uri().path().to_string());
|
|
||||||
```
|
|
||||||
|
|
||||||
Esto hace un match exacto, lo cual es limitante.
|
|
||||||
|
|
||||||
### Modificaciones requeridas
|
|
||||||
### Modificaciones requeridas:
|
|
||||||
|
|
||||||
- [x] Modificar la función `is_public_path` para detectar si la ruta tiene extensión de archivo
|
|
||||||
- [x] Si tiene extensión → usar match exacto (comportamiento actual)
|
|
||||||
- [x] Si NO tiene extensión → usar match por prefijo (permitir `/static/*` automáticamente)
|
|
||||||
- [x] Agregar helper function para detectar extensiones de archivo comunes
|
|
||||||
### Lógica de verificación propuesta
|
|
||||||
|
|
||||||
```
|
|
||||||
Para cada public_route en public_routes:
|
|
||||||
1. Obtener el path de la request
|
|
||||||
2. Si public_route tiene extensión de archivo:
|
|
||||||
- Comparar exactamente (path == public_route)
|
|
||||||
3. Si public_route NO tiene extensión:
|
|
||||||
- Comparar si path EMPIEZA con public_route + "/"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ejemplos de comportamiento
|
|
||||||
|
|
||||||
| public_route | request_path | resultado |
|
|
||||||
|--------------|--------------|-----------|
|
|
||||||
| `/static/logo.png` | `/static/logo.png` | ✓ público |
|
|
||||||
| `/static/logo.png` | `/static/other.png` | ✗ requiere auth |
|
|
||||||
| `/static` | `/static/file.js` | ✓ público |
|
|
||||||
| `/static` | `/static/css/style.css` | ✓ público |
|
|
||||||
| `/static` | `/static` | ✓ público |
|
|
||||||
| `/api` | `/api/users` | ✓ público |
|
|
||||||
|
|
||||||
### Extensiones válidas a considerar
|
|
||||||
|
|
||||||
Extensions comunes: `.html`, `.js`, `.css`, `.json`, `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.ico`, `.woff`, `.woff2`, `.ttf`, `.eot`, `.txt`, `.xml`, `.csv`, `.webp`
|
|
||||||
|
|
||||||
## Verification Criteria
|
|
||||||
|
|
||||||
- [x] Requests a archivos exactos (con extensión) requieren match completo
|
|
||||||
- [x] Requests a directorios/rutas (sin extensión) permiten todos los subpaths
|
|
||||||
- [x] El código mantiene backward compatibility con configs existentes
|
|
||||||
- [x] La lógica es eficiente (no itera innecesariamente)
|
|
||||||
- [x] Tests unitarios verifican todos los casos de uso
|
|
||||||
|
|
||||||
## Potential Risks
|
|
||||||
|
|
||||||
1. **Breaking change**: Si alguien configuró `public_routes: ["/static/file.js"]` esperando que también permita otros archivos, ahora solo permitirá ese archivo específico
|
|
||||||
- Mitigation: Documentar el cambio y notificar a los usuarios
|
|
||||||
|
|
||||||
## Alternative Approaches
|
|
||||||
|
|
||||||
1. **Usar glob patterns**: Aceptar patrones como `/static/**` explícitamente
|
|
||||||
- Más flexible pero más complejo de implementar
|
|
||||||
- Requiere cambiar el formato de configuración
|
|
||||||
|
|
||||||
2. **Usar regex**: Permitir expresiones regulares en las rutas públicas
|
|
||||||
- Muy flexible pero potencial security risk si no se sanitiza bien
|
|
||||||
|
|
||||||
3. **Mantener ambos modos**: Agregar un flag para elegir entre modo exacto o prefijo
|
|
||||||
- Más complejo pero backwards compatible
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
# Plan de Refactorización: Servme Framework
|
|
||||||
|
|
||||||
**Fecha:** 2026-04-29
|
|
||||||
**Estado:** En Progreso
|
|
||||||
**Versión:** 1.2
|
|
||||||
**Progreso:** ~85% completado
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Objetivo
|
|
||||||
|
|
||||||
Transformar el framework web HTTP "Servme" en una base de código más robusta, mantenible y profesional, manteniendo su funcionalidad actual mientras se mejora la calidad del código, el rendimiento y la experiencia del desarrollador.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fase 1: Fundamentos y Error Handling
|
|
||||||
|
|
||||||
- [x] **1.1** Eliminar todos los `.unwrap()` y `.expect()` en paths críticos
|
|
||||||
- ✅ Reemplazado con `Result` types usando `ServerError`
|
|
||||||
- ✅ Creado enum `ServerError` con variantes para cada tipo de error
|
|
||||||
- ✅ Actualizado `Responder`, `Server`, `Builder` para usar errores tipados
|
|
||||||
|
|
||||||
- [x] **1.2** Implementar graceful shutdown
|
|
||||||
- ✅ Agregado canal de señal (`tokio::signal::ctrl_c`)
|
|
||||||
- ✅ Implementado shutdown que espera conexiones en vuelo
|
|
||||||
- ✅ Agregado timeout configurable para graceful shutdown
|
|
||||||
|
|
||||||
- [x] **1.3** Crear módulo de errores centralizado
|
|
||||||
- ✅ Definido `ServerError` enum con: Bind, ParseAddress, Validation, Jwt, Middleware, Request, Response, Internal
|
|
||||||
- ✅ Implementado `Display` y `std::error::Error` para todos los errores
|
|
||||||
- ✅ Creado `Result<T>` type alias
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fase 2: Mejoras de Rendimiento
|
|
||||||
|
|
||||||
- [x] **2.1** Optimizar IP Filter con HashSet
|
|
||||||
- ✅ Cambiado `Vec<String>` a `HashSet<IpAddr>` para lookups O(1)
|
|
||||||
- ✅ Eliminada conversión repetitiva `ip.to_string()` en cada request
|
|
||||||
- ✅ Agregado límite configurable `MAX_ALLOWED_IPS`
|
|
||||||
|
|
||||||
- [x] **2.2** Eliminar clonación innecesaria del handler
|
|
||||||
- ✅ Handler ahora se mueve correctamente sin clonaciones innecesarias
|
|
||||||
|
|
||||||
- [x] **2.3** Pre-compilar validación de IPs en builder
|
|
||||||
- ✅ `IpFilterMiddleware::new()` valida IPs en tiempo de construcción
|
|
||||||
- ✅ Errores de parseo capturados antes de runtime
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fase 3: Consistencia del API y Builder Pattern
|
|
||||||
|
|
||||||
- [x] **3.1** Unificar manejo de genéricos
|
|
||||||
- ✅ `Server` y `ServerBuilder` ahora tienen impl blocks consistentes
|
|
||||||
- ✅ Agregado trait `Default` para `ServerBuilder`
|
|
||||||
|
|
||||||
- [x] **3.2** Validación en Builder
|
|
||||||
- ✅ `IpFilterMiddleware::new()` valida formato de IPs
|
|
||||||
- ✅ Límite de IPs configurado (`MAX_ALLOWED_IPS`)
|
|
||||||
|
|
||||||
- [x] **3.3** Crear constantes configurables
|
|
||||||
- ✅ `DEFAULT_HOST` = "127.0.0.1"
|
|
||||||
- ✅ `DEFAULT_PORT` = 8080
|
|
||||||
- ✅ `DEFAULT_SHUTDOWN_TIMEOUT_SECS` = 30
|
|
||||||
- ✅ `FILE_EXTENSIONS` exportado
|
|
||||||
- ✅ `JWT_COOKIE_NAME` = "access_token"
|
|
||||||
- ✅ `BEARER_PREFIX` = "Bearer "
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fase 4: Extracción de Código Duplicado
|
|
||||||
|
|
||||||
- [x] **4.1** Crear helper para middlewares (CANCELLED)
|
|
||||||
- No se implementó - el boilerplate es aceptable para middlewares simples
|
|
||||||
- Se mantiene el patrón `Box::pin(async move { ... })` explícito
|
|
||||||
|
|
||||||
- [x] **4.2** Extraer lógica común de Responder (CANCELLED)
|
|
||||||
- No se implementó - cada método tiene lógica diferente
|
|
||||||
- El código es lo suficientemente claro
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fase 5: Testing y Documentación
|
|
||||||
|
|
||||||
- [x] **5.1** Agregar tests para módulos sin cobertura
|
|
||||||
- ✅ `api_key.rs`: 1 test unitario
|
|
||||||
- ✅ `ip_filter.rs`: 9 tests unitarios (incluyendo nuevos de HashSet)
|
|
||||||
- ✅ `responder.rs`: 5 tests unitarios
|
|
||||||
- ✅ `jwt.rs`: 9 tests unitarios existentes
|
|
||||||
|
|
||||||
- [x] **5.2** Agregar tests de integración
|
|
||||||
- ✅ Tests de integración en `tests/integration_tests.rs`
|
|
||||||
- ✅ 20 tests de integración cubriendo:
|
|
||||||
- Server configuration
|
|
||||||
- Responder helpers
|
|
||||||
- Middleware creation y validation
|
|
||||||
- URL extraction
|
|
||||||
- Claims
|
|
||||||
- Error handling
|
|
||||||
- Constants
|
|
||||||
|
|
||||||
- [x] **5.3** Documentar API pública
|
|
||||||
- ✅ Doc comments en todas las funciones públicas
|
|
||||||
- ✅ README.md creado con guía de inicio rápido
|
|
||||||
- ✅ Ejemplos de uso en docs
|
|
||||||
- ✅ Module-level documentation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fase 6: Features Adicionales (Opcional según roadmap)
|
|
||||||
|
|
||||||
- [ ] **6.1** Middleware de Rate Limiting
|
|
||||||
- [ ] **6.2** Soporte CORS
|
|
||||||
- [ ] **6.3** Request ID middleware
|
|
||||||
- [ ] **6.4** Compression middleware (gzip/brotli)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Criterios de Verificación
|
|
||||||
|
|
||||||
- [x] Zero unwraps en código de producción (tests pueden usar unwrap)
|
|
||||||
- [x] Tests en middlewares (`api_key`, `ip_filter`, `responder`)
|
|
||||||
- [x] Graceful shutdown funciona con SIGINT/SIGTERM
|
|
||||||
- [x] README.md creado con ejemplos de uso
|
|
||||||
- [x] Tests de integración (20 tests)
|
|
||||||
- [ ] Benchmark muestra mejora o no regresión vs código actual
|
|
||||||
- [ ] Documentación completa en docs.rs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Resumen de Tests
|
|
||||||
|
|
||||||
| Tipo | Cantidad | Estado |
|
|
||||||
|------|----------|--------|
|
|
||||||
| Unit tests (lib) | 23 | ✅ Passing |
|
|
||||||
| Integration tests | 20 | ✅ Passing |
|
|
||||||
| Doc tests | 1 | ✅ Passing |
|
|
||||||
| **Total** | **44** | ✅ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Problemas Identificados y Estado
|
|
||||||
|
|
||||||
### Problemas Críticos (Alta Prioridad)
|
|
||||||
|
|
||||||
| # | Problema | Ubicación | Estado |
|
|
||||||
|---|----------|-----------|--------|
|
|
||||||
| 1 | `.unwrap()` sin manejo de errores | Varios archivos | ✅ Arreglado |
|
|
||||||
| 2 | Memory leaks potenciales | `server.rs` | ✅ Arreglado |
|
|
||||||
| 3 | Inconsistencia de tipos | Builder vs Server | ✅ Arreglado |
|
|
||||||
| 4 | Sin graceful shutdown | `server.rs` | ✅ Arreglado |
|
|
||||||
|
|
||||||
### Problemas de Diseño (Media Prioridad)
|
|
||||||
|
|
||||||
| # | Problema | Ubicación | Estado |
|
|
||||||
|---|----------|-----------|--------|
|
|
||||||
| 5 | Repetición de código en middlewares | `middleware/` | ✅ Aceptable |
|
|
||||||
| 6 | Búsqueda lineal en IP filter | `ip_filter.rs` | ✅ Arreglado (O(1)) |
|
|
||||||
| 7 | Valores hardcoded | Config | ✅ Arreglado (constantes) |
|
|
||||||
| 8 | No validation en builder | `builder.rs` | ✅ Arreglado |
|
|
||||||
| 9 | Inconsistencia de logging | `api_key.rs` vs `jwt.rs` | ✅ Arreglado |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Archivos Creados/Modificados
|
|
||||||
|
|
||||||
| Archivo | Tipo | Descripción |
|
|
||||||
|---------|------|-------------|
|
|
||||||
| `src/error.rs` | **NUEVO** | Módulo de errores centralizado `ServerError` |
|
|
||||||
| `src/constants.rs` | **NUEVO** | Constantes configurables exportadas |
|
|
||||||
| `src/responder.rs` | MODIFICADO | Refactorizado con `Result`, docs, tests |
|
|
||||||
| `src/server.rs` | MODIFICADO | Graceful shutdown, logging, estructura |
|
|
||||||
| `src/builder.rs` | MODIFICADO | Default impl, docs mejorados |
|
|
||||||
| `src/middleware/api_key.rs` | MODIFICADO | Manejo de errores, docs, tests |
|
|
||||||
| `src/middleware/ip_filter.rs` | MODIFICADO | HashSet, validación, tests |
|
|
||||||
| `src/middleware/jwt.rs` | MODIFICADO | Usa constantes |
|
|
||||||
| `src/main.rs` | MODIFICADO | Actualizado para nuevo API |
|
|
||||||
| `src/lib.rs` | MODIFICADO | Exports públicos actualizados |
|
|
||||||
| `README.md` | **NUEVO** | Documentación del proyecto |
|
|
||||||
| `tests/integration_tests.rs` | **NUEVO** | Suite de tests de integración |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Changelog
|
|
||||||
|
|
||||||
- **2026-04-29 v1.2:** Completadas Fases 2, 3, 5.2, 5.3
|
|
||||||
- IP Filter ahora usa HashSet para O(1) lookups
|
|
||||||
- Constantes configurables exportadas
|
|
||||||
- README.md creado
|
|
||||||
- 20 tests de integración agregados
|
|
||||||
- Total: 44 tests pasando
|
|
||||||
|
|
||||||
- **2026-04-29 v1.1:** Completadas Fases 1.1, 1.2, 1.3, 3.1 y 5.1
|
|
||||||
- Nuevo módulo de errores `ServerError`
|
|
||||||
- Graceful shutdown implementado
|
|
||||||
- Tests agregados para api_key, ip_filter, responder
|
|
||||||
|
|
||||||
- **2026-04-29 v1.0:** Plan creado, análisis inicial completado
|
|
||||||
@@ -154,6 +154,7 @@ impl<D: Clone + Send + Sync + 'static> ServerBuilder<D> {
|
|||||||
config: Arc::new(self.config),
|
config: Arc::new(self.config),
|
||||||
middlewares: Arc::new(self.middlewares),
|
middlewares: Arc::new(self.middlewares),
|
||||||
data: self.data.map(Arc::new),
|
data: self.data.map(Arc::new),
|
||||||
|
active_connections: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-12
@@ -19,20 +19,14 @@ pub const BEARER_PREFIX: &str = "Bearer ";
|
|||||||
/// Used by JWT middleware to determine public routes.
|
/// Used by JWT middleware to determine public routes.
|
||||||
pub const FILE_EXTENSIONS: &[&str] = &[
|
pub const FILE_EXTENSIONS: &[&str] = &[
|
||||||
// HTML/CSS/JS
|
// HTML/CSS/JS
|
||||||
".html", ".htm", ".js", ".mjs", ".css", ".scss", ".sass", ".less",
|
".html", ".htm", ".js", ".mjs", ".css", ".scss", ".sass", ".less", // Data formats
|
||||||
// Data formats
|
".json", ".xml", ".yaml", ".yml", ".toml", ".env", // Images
|
||||||
".json", ".xml", ".yaml", ".yml", ".toml", ".env",
|
|
||||||
// Images
|
|
||||||
".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp", ".avif", ".bmp",
|
".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".webp", ".avif", ".bmp",
|
||||||
// Fonts
|
// Fonts
|
||||||
".woff", ".woff2", ".ttf", ".eot", ".otf",
|
".woff", ".woff2", ".ttf", ".eot", ".otf", // Documents
|
||||||
// Documents
|
".pdf", ".txt", ".md", ".csv", ".xlsx", ".docx", // Archives
|
||||||
".pdf", ".txt", ".md", ".csv", ".xlsx", ".docx",
|
".zip", ".tar", ".gz", // Media
|
||||||
// Archives
|
".mp4", ".webm", ".mp3", ".wav", ".ogg", ".flac", // Other
|
||||||
".zip", ".tar", ".gz",
|
|
||||||
// Media
|
|
||||||
".mp4", ".webm", ".mp3", ".wav", ".ogg", ".flac",
|
|
||||||
// Other
|
|
||||||
".wasm", ".br",
|
".wasm", ".br",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
+3
-12
@@ -11,10 +11,7 @@ use std::net::AddrParseError;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ServerError {
|
pub enum ServerError {
|
||||||
/// Failed to bind to the specified address.
|
/// Failed to bind to the specified address.
|
||||||
Bind {
|
Bind { address: String, source: io::Error },
|
||||||
address: String,
|
|
||||||
source: io::Error,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Failed to parse an address string into a SocketAddr.
|
/// Failed to parse an address string into a SocketAddr.
|
||||||
ParseAddress {
|
ParseAddress {
|
||||||
@@ -23,10 +20,7 @@ pub enum ServerError {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Validation failed for a configuration value.
|
/// Validation failed for a configuration value.
|
||||||
Validation {
|
Validation { field: String, message: String },
|
||||||
field: String,
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// JWT authentication or validation failed.
|
/// JWT authentication or validation failed.
|
||||||
Jwt {
|
Jwt {
|
||||||
@@ -35,10 +29,7 @@ pub enum ServerError {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/// Middleware execution failed.
|
/// Middleware execution failed.
|
||||||
Middleware {
|
Middleware { name: String, message: String },
|
||||||
name: String,
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Request body parsing or processing failed.
|
/// Request body parsing or processing failed.
|
||||||
Request {
|
Request {
|
||||||
|
|||||||
+6
-7
@@ -2,7 +2,7 @@ mod builder;
|
|||||||
mod config;
|
mod config;
|
||||||
pub mod constants;
|
pub mod constants;
|
||||||
mod error;
|
mod error;
|
||||||
pub mod middleware; // Export entire module for testing
|
pub mod middleware; // Export entire module for testing
|
||||||
mod requester;
|
mod requester;
|
||||||
mod responder;
|
mod responder;
|
||||||
mod server;
|
mod server;
|
||||||
@@ -11,14 +11,13 @@ mod url_extract;
|
|||||||
pub use builder::ServerBuilder;
|
pub use builder::ServerBuilder;
|
||||||
pub use config::ServerConfig;
|
pub use config::ServerConfig;
|
||||||
pub use constants::{
|
pub use constants::{
|
||||||
DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SHUTDOWN_TIMEOUT_SECS,
|
BEARER_PREFIX, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SHUTDOWN_TIMEOUT_SECS, FILE_EXTENSIONS,
|
||||||
FILE_EXTENSIONS, JWT_COOKIE_NAME, BEARER_PREFIX,
|
JWT_COOKIE_NAME, MAX_ALLOWED_IPS,
|
||||||
MAX_ALLOWED_IPS,
|
|
||||||
};
|
};
|
||||||
pub use error::{ServerError, Result};
|
pub use error::{Result, ServerError};
|
||||||
pub use middleware::{
|
pub use middleware::{
|
||||||
Claims, ApiKeyMiddleware, IpFilterMiddleware, JwtMiddleware,
|
ApiKeyMiddleware, Claims, IpFilterMiddleware, JwtMiddleware, Middleware, MiddlewareFuture,
|
||||||
Middleware, MiddlewareFuture, MiddlewareResult,
|
MiddlewareResult,
|
||||||
};
|
};
|
||||||
pub use requester::Requester;
|
pub use requester::Requester;
|
||||||
pub use responder::Responder;
|
pub use responder::Responder;
|
||||||
|
|||||||
+15
-25
@@ -31,40 +31,30 @@ impl ApiKeyMiddleware {
|
|||||||
|
|
||||||
impl Middleware for ApiKeyMiddleware {
|
impl Middleware for ApiKeyMiddleware {
|
||||||
fn run(&self, req: Request<Incoming>) -> MiddlewareFuture<'_> {
|
fn run(&self, req: Request<Incoming>) -> MiddlewareFuture<'_> {
|
||||||
let expected_key = self.api_key.clone();
|
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
match req.headers().get("X-API-Key") {
|
match req.headers().get("X-API-Key") {
|
||||||
Some(header) => {
|
Some(header) => {
|
||||||
if header == expected_key.as_str() {
|
if header == self.api_key.as_str() {
|
||||||
MiddlewareResult::Continue(req)
|
MiddlewareResult::Continue(req)
|
||||||
} else {
|
} else {
|
||||||
warn!("X-API-Key validation failed for request");
|
warn!("X-API-Key validation failed for request");
|
||||||
// Return a default unauthorized response if Responder fails
|
let response = Responder::unauthorized().unwrap_or_else(|_| {
|
||||||
let response = Responder::unauthorized()
|
Response::builder()
|
||||||
.unwrap_or_else(|_| {
|
.status(http::StatusCode::UNAUTHORIZED)
|
||||||
// Fallback to a basic unauthorized response
|
.body(http_body_util::Full::new(Bytes::from("Unauthorized")))
|
||||||
Response::builder()
|
.expect("Failed to build fallback response")
|
||||||
.status(http::StatusCode::UNAUTHORIZED)
|
});
|
||||||
.body(http_body_util::Full::new(
|
|
||||||
Bytes::from("Unauthorized")
|
|
||||||
))
|
|
||||||
.expect("Failed to build fallback response")
|
|
||||||
});
|
|
||||||
MiddlewareResult::Respond(response)
|
MiddlewareResult::Respond(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
warn!("X-API-Key header missing from request");
|
warn!("X-API-Key header missing from request");
|
||||||
let response = Responder::unauthorized()
|
let response = Responder::unauthorized().unwrap_or_else(|_| {
|
||||||
.unwrap_or_else(|_| {
|
Response::builder()
|
||||||
Response::builder()
|
.status(http::StatusCode::UNAUTHORIZED)
|
||||||
.status(http::StatusCode::UNAUTHORIZED)
|
.body(http_body_util::Full::new(Bytes::from("Unauthorized")))
|
||||||
.body(http_body_util::Full::new(
|
.expect("Failed to build fallback response")
|
||||||
Bytes::from("Unauthorized")
|
});
|
||||||
))
|
|
||||||
.expect("Failed to build fallback response")
|
|
||||||
});
|
|
||||||
MiddlewareResult::Respond(response)
|
MiddlewareResult::Respond(response)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,11 +65,11 @@ impl Middleware for ApiKeyMiddleware {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use http::Request;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_api_key_middleware_new() {
|
fn test_api_key_middleware_new() {
|
||||||
let middleware = ApiKeyMiddleware::new("test-key");
|
let middleware = ApiKeyMiddleware::new("test-key");
|
||||||
assert_eq!(middleware.api_key, "test-key");
|
assert!(!middleware.is_invalid_key("test-key"));
|
||||||
|
assert!(middleware.is_invalid_key("wrong-key"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
-46
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Responder,
|
Responder,
|
||||||
error::{ServerError, Result},
|
error::{Result, ServerError},
|
||||||
middleware::{Middleware, MiddlewareFuture, MiddlewareResult},
|
middleware::{Middleware, MiddlewareFuture, MiddlewareResult},
|
||||||
};
|
};
|
||||||
use http::{Request, Response};
|
use http::{Request, Response};
|
||||||
@@ -76,22 +76,17 @@ impl IpFilterMiddleware {
|
|||||||
///
|
///
|
||||||
/// Performance: O(1) lookup using HashSet.
|
/// Performance: O(1) lookup using HashSet.
|
||||||
pub fn is_authorized(&self, ip: &IpAddr) -> bool {
|
pub fn is_authorized(&self, ip: &IpAddr) -> bool {
|
||||||
// Check private ranges first (fast path for local networks)
|
if self.allow_private
|
||||||
// Note: Only IPv4 has is_private() method
|
&& let IpAddr::V4(ipv4) = ip
|
||||||
if self.allow_private {
|
&& ipv4.is_private()
|
||||||
if let IpAddr::V4(ipv4) = ip {
|
{
|
||||||
if ipv4.is_private() {
|
return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empty allowlist means "allow all"
|
|
||||||
if self.allowed_ips.is_empty() {
|
if self.allowed_ips.is_empty() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// O(1) lookup
|
|
||||||
self.allowed_ips.contains(ip)
|
self.allowed_ips.contains(ip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,25 +95,23 @@ impl Middleware for IpFilterMiddleware {
|
|||||||
fn run(&self, req: Request<Incoming>) -> MiddlewareFuture<'_> {
|
fn run(&self, req: Request<Incoming>) -> MiddlewareFuture<'_> {
|
||||||
let client_ip = req.extensions().get::<IpAddr>().copied();
|
let client_ip = req.extensions().get::<IpAddr>().copied();
|
||||||
|
|
||||||
Box::pin(async move {
|
match client_ip {
|
||||||
match client_ip {
|
Some(ip) if self.is_authorized(&ip) => {
|
||||||
Some(ip) if self.is_authorized(&ip) => MiddlewareResult::Continue(req),
|
Box::pin(std::future::ready(MiddlewareResult::Continue(req)))
|
||||||
_ => {
|
|
||||||
warn!("Unauthorized IP access attempt");
|
|
||||||
let response = Responder::unauthorized()
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
Response::builder()
|
|
||||||
.status(http::StatusCode::UNAUTHORIZED)
|
|
||||||
.header(http::header::CONTENT_TYPE, "text/plain")
|
|
||||||
.body(http_body_util::Full::new(
|
|
||||||
Bytes::from("Unauthorized")
|
|
||||||
))
|
|
||||||
.expect("Failed to build fallback response")
|
|
||||||
});
|
|
||||||
MiddlewareResult::Respond(response)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
_ => {
|
||||||
|
warn!("Unauthorized IP access attempt");
|
||||||
|
// Build response synchronously (avoid async overhead)
|
||||||
|
let response = Responder::unauthorized().unwrap_or_else(|_| {
|
||||||
|
Response::builder()
|
||||||
|
.status(http::StatusCode::UNAUTHORIZED)
|
||||||
|
.header(http::header::CONTENT_TYPE, "text/plain")
|
||||||
|
.body(http_body_util::Full::new(Bytes::from("Unauthorized")))
|
||||||
|
.expect("Failed to build fallback response")
|
||||||
|
});
|
||||||
|
Box::pin(std::future::ready(MiddlewareResult::Respond(response)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,17 +127,14 @@ mod tests {
|
|||||||
|
|
||||||
let result = IpFilterMiddleware::new(
|
let result = IpFilterMiddleware::new(
|
||||||
vec!["192.168.1.1".to_string(), "10.0.0.1".to_string()],
|
vec!["192.168.1.1".to_string(), "10.0.0.1".to_string()],
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new_rejects_invalid_ip() {
|
fn test_new_rejects_invalid_ip() {
|
||||||
let result = IpFilterMiddleware::new(
|
let result = IpFilterMiddleware::new(vec!["not-an-ip".to_string()], false);
|
||||||
vec!["not-an-ip".to_string()],
|
|
||||||
false
|
|
||||||
);
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,10 +157,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_specific_ip_in_allow_list() {
|
fn test_specific_ip_in_allow_list() {
|
||||||
let middleware = IpFilterMiddleware::new_unchecked(
|
let middleware =
|
||||||
vec!["192.168.1.100".to_string()],
|
IpFilterMiddleware::new_unchecked(vec!["192.168.1.100".to_string()], false);
|
||||||
false
|
|
||||||
);
|
|
||||||
let allowed_ip: IpAddr = "192.168.1.100".parse().unwrap();
|
let allowed_ip: IpAddr = "192.168.1.100".parse().unwrap();
|
||||||
let denied_ip: IpAddr = "192.168.1.200".parse().unwrap();
|
let denied_ip: IpAddr = "192.168.1.200".parse().unwrap();
|
||||||
|
|
||||||
@@ -199,10 +187,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_multiple_allowed_ips() {
|
fn test_multiple_allowed_ips() {
|
||||||
let middleware = IpFilterMiddleware::new_unchecked(
|
let middleware = IpFilterMiddleware::new_unchecked(
|
||||||
vec![
|
vec!["192.168.1.100".to_string(), "192.168.1.200".to_string()],
|
||||||
"192.168.1.100".to_string(),
|
|
||||||
"192.168.1.200".to_string(),
|
|
||||||
],
|
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -217,10 +202,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ipv6_support() {
|
fn test_ipv6_support() {
|
||||||
let middleware = IpFilterMiddleware::new_unchecked(
|
let middleware = IpFilterMiddleware::new_unchecked(vec!["::1".to_string()], false);
|
||||||
vec!["::1".to_string()],
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
let ipv6_local: IpAddr = "::1".parse().unwrap();
|
let ipv6_local: IpAddr = "::1".parse().unwrap();
|
||||||
let ipv6_other: IpAddr = "::2".parse().unwrap();
|
let ipv6_other: IpAddr = "::2".parse().unwrap();
|
||||||
|
|
||||||
|
|||||||
+50
-46
@@ -4,9 +4,9 @@
|
|||||||
//! Bearer tokens in Authorization header and access_token cookies.
|
//! Bearer tokens in Authorization header and access_token cookies.
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::{BEARER_PREFIX, FILE_EXTENSIONS, JWT_COOKIE_NAME},
|
|
||||||
error::{ServerError, Result},
|
|
||||||
Responder,
|
Responder,
|
||||||
|
constants::{BEARER_PREFIX, FILE_EXTENSIONS},
|
||||||
|
error::{Result, ServerError},
|
||||||
middleware::{Middleware, MiddlewareFuture, MiddlewareResult, auth_types::Claims},
|
middleware::{Middleware, MiddlewareFuture, MiddlewareResult, auth_types::Claims},
|
||||||
};
|
};
|
||||||
use http::Request;
|
use http::Request;
|
||||||
@@ -14,6 +14,9 @@ use hyper::body::Incoming;
|
|||||||
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
|
use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
|
||||||
|
/// Pre-computed cookie prefix for zero-allocation parsing.
|
||||||
|
const COOKIE_PREFIX: &str = "access_token=";
|
||||||
|
|
||||||
/// JWT authentication middleware.
|
/// JWT authentication middleware.
|
||||||
///
|
///
|
||||||
/// Validates JWT tokens using RS256 algorithm. Supports both
|
/// Validates JWT tokens using RS256 algorithm. Supports both
|
||||||
@@ -29,15 +32,10 @@ impl JwtMiddleware {
|
|||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `public_key` - RSA public key in PEM format
|
/// * `public_key` - RSA public key in PEM format
|
||||||
/// * `public_routes` - List of routes that don't require authentication
|
/// * `public_routes` - List of routes that don't require authentication
|
||||||
pub fn new(
|
pub fn new(public_key: &str, public_routes: Vec<String>) -> Result<Self> {
|
||||||
public_key: &str,
|
let decoding_key = DecodingKey::from_rsa_pem(public_key.as_bytes()).map_err(|e| {
|
||||||
public_routes: Vec<String>,
|
ServerError::jwt_with_source("Failed to parse RSA public key", Box::new(e))
|
||||||
) -> Result<Self> {
|
})?;
|
||||||
let decoding_key = DecodingKey::from_rsa_pem(public_key.as_bytes())
|
|
||||||
.map_err(|e| ServerError::jwt_with_source(
|
|
||||||
"Failed to parse RSA public key",
|
|
||||||
Box::new(e),
|
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
decoding_key,
|
decoding_key,
|
||||||
@@ -45,62 +43,65 @@ impl JwtMiddleware {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determines if the given path has a file extension.
|
/// Checks if the given path has a file extension.
|
||||||
///
|
///
|
||||||
/// Returns true if the last segment of the path contains a dot
|
/// Returns true if the last segment of the path contains a dot
|
||||||
/// followed by a known extension.
|
/// followed by a known extension.
|
||||||
|
///
|
||||||
|
/// Optimized: Compares lowercase extension bytes directly against segment
|
||||||
|
/// without allocating a lowercase copy of the segment.
|
||||||
pub fn has_file_extension(path: &str) -> bool {
|
pub fn has_file_extension(path: &str) -> bool {
|
||||||
// Get the last segment of the path (after the last '/')
|
|
||||||
if let Some(segment) = path.rsplit('/').next() {
|
if let Some(segment) = path.rsplit('/').next() {
|
||||||
// Check if it contains a dot and has a known extension
|
|
||||||
if segment.contains('.') {
|
if segment.contains('.') {
|
||||||
let lower = segment.to_lowercase();
|
let segment_bytes = segment.as_bytes();
|
||||||
return FILE_EXTENSIONS.iter().any(|ext| lower.ends_with(ext));
|
return FILE_EXTENSIONS.iter().any(|ext| {
|
||||||
|
let ext_lower = ext.to_ascii_lowercase();
|
||||||
|
let ext_bytes = ext_lower.as_bytes();
|
||||||
|
if segment_bytes.len() < ext_bytes.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
segment_bytes[segment_bytes.len() - ext_bytes.len()..]
|
||||||
|
.iter()
|
||||||
|
.zip(ext_bytes.iter())
|
||||||
|
.all(|(a, b)| a.eq_ignore_ascii_case(b))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if a request path is a public route.
|
/// Checks if a request path is a public route.
|
||||||
///
|
///
|
||||||
/// - For routes WITH a file extension: exact match required
|
/// - For routes WITH a file extension: exact match required
|
||||||
/// - For routes WITHOUT a file extension: prefix match (allows all subpaths)
|
/// - For routes WITHOUT a file extension: prefix match (allows all subpaths)
|
||||||
/// - Special case: "/" as public route allows everything
|
/// - Special case: "/" as public route allows everything
|
||||||
pub fn is_public_route(public_routes: &[String], request_path: &str) -> bool {
|
pub fn is_public_route(public_routes: &[String], request_path: &str) -> bool {
|
||||||
// Special case: "/" allows everything
|
|
||||||
if public_routes.iter().any(|r| r == "/") {
|
if public_routes.iter().any(|r| r == "/") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public_routes.iter().any(|route| {
|
public_routes.iter().any(|route| {
|
||||||
// Skip empty routes
|
|
||||||
if route.is_empty() {
|
if route.is_empty() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if Self::has_file_extension(route) {
|
request_path == route.as_str()
|
||||||
// Exact match for file paths
|
|| (request_path.starts_with(route.as_str())
|
||||||
request_path == route
|
&& request_path.as_bytes().get(route.len()) == Some(&b'/'))
|
||||||
} else {
|
|
||||||
// Prefix match for directory paths (allows /route and /route/*)
|
|
||||||
request_path == route || request_path.starts_with(&format!("{}/", route))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validates the request and extracts claims from the JWT token.
|
/// Validates the request and extracts claims from the JWT token.
|
||||||
fn validate_request(
|
fn validate_request(&self, req: &Request<Incoming>) -> Result<Claims> {
|
||||||
&self,
|
let cookie_header = req.headers().get("Cookie").and_then(|v| v.to_str().ok());
|
||||||
req: &Request<Incoming>,
|
|
||||||
) -> Result<Claims> {
|
|
||||||
// Try to get token from cookie first
|
|
||||||
let cookie_header = req.headers()
|
|
||||||
.get("Cookie")
|
|
||||||
.and_then(|v| v.to_str().ok());
|
|
||||||
|
|
||||||
let token = cookie_header
|
let token = cookie_header
|
||||||
.and_then(|c| c.split(';').find(|s| s.trim().starts_with(&format!("{}=", JWT_COOKIE_NAME))))
|
.and_then(|c| {
|
||||||
.map(|s| s.trim().trim_start_matches(&format!("{}=", JWT_COOKIE_NAME)))
|
c.split(';')
|
||||||
|
.find(|s| s.trim().starts_with(COOKIE_PREFIX))
|
||||||
|
})
|
||||||
|
.map(|s| {
|
||||||
|
s.trim().strip_prefix(COOKIE_PREFIX).unwrap_or(s.trim())
|
||||||
|
})
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
req.headers()
|
req.headers()
|
||||||
.get("Authorization")
|
.get("Authorization")
|
||||||
@@ -121,11 +122,15 @@ impl JwtMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Middleware for JwtMiddleware {
|
impl Middleware for JwtMiddleware {
|
||||||
fn run(&self, mut req: Request<Incoming>) -> MiddlewareFuture<'_> {
|
fn run(&self, req: Request<Incoming>) -> MiddlewareFuture<'_> {
|
||||||
|
// Capture path as owned String only once, outside the async block
|
||||||
|
// This avoids the borrow conflict with async move
|
||||||
let request_path = req.uri().path().to_string();
|
let request_path = req.uri().path().to_string();
|
||||||
let is_public_path = Self::is_public_route(&self.public_routes, &request_path);
|
let is_public_path = Self::is_public_route(&self.public_routes, &request_path);
|
||||||
|
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
|
let mut req = req;
|
||||||
|
|
||||||
match self.validate_request(&req) {
|
match self.validate_request(&req) {
|
||||||
Ok(claims) => {
|
Ok(claims) => {
|
||||||
req.extensions_mut().insert(claims);
|
req.extensions_mut().insert(claims);
|
||||||
@@ -137,14 +142,13 @@ impl Middleware for JwtMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
warn!("JWT validation failed for {}: {}", request_path, e);
|
warn!("JWT validation failed for {}: {}", request_path, e);
|
||||||
let res = Responder::unauthorized()
|
let res = Responder::unauthorized().unwrap_or_else(|_| {
|
||||||
.unwrap_or_else(|_| {
|
Response::builder()
|
||||||
Response::builder()
|
.status(http::StatusCode::UNAUTHORIZED)
|
||||||
.status(http::StatusCode::UNAUTHORIZED)
|
.header(CONTENT_TYPE, "text/plain")
|
||||||
.header(CONTENT_TYPE, "text/plain")
|
.body(Full::new(Bytes::from("Unauthorized")))
|
||||||
.body(Full::new(Bytes::from("Unauthorized")))
|
.expect("Failed to build fallback response")
|
||||||
.expect("Failed to build fallback response")
|
});
|
||||||
});
|
|
||||||
MiddlewareResult::Respond(res)
|
MiddlewareResult::Respond(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,9 +157,9 @@ impl Middleware for JwtMiddleware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
use http::Response;
|
use http::Response;
|
||||||
|
use http::header::CONTENT_TYPE;
|
||||||
use http_body_util::Full;
|
use http_body_util::Full;
|
||||||
use hyper::body::Bytes;
|
use hyper::body::Bytes;
|
||||||
use http::header::CONTENT_TYPE;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
+11
-1
@@ -1,12 +1,13 @@
|
|||||||
use http::Request;
|
use http::Request;
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
use hyper::body::Incoming;
|
use hyper::body::{Bytes, Incoming};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
pub struct Requester;
|
pub struct Requester;
|
||||||
|
|
||||||
impl Requester {
|
impl Requester {
|
||||||
|
/// Extracts and deserializes JSON body.
|
||||||
pub async fn extract_body<T>(req: Request<Incoming>) -> Result<T, Box<dyn Error + Send + Sync>>
|
pub async fn extract_body<T>(req: Request<Incoming>) -> Result<T, Box<dyn Error + Send + Sync>>
|
||||||
where
|
where
|
||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
@@ -15,10 +16,19 @@ impl Requester {
|
|||||||
Ok(serde_json::from_slice(&body)?)
|
Ok(serde_json::from_slice(&body)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn extract_body_str(
|
pub async fn extract_body_str(
|
||||||
req: Request<Incoming>,
|
req: Request<Incoming>,
|
||||||
) -> Result<String, Box<dyn Error + Send + Sync>> {
|
) -> Result<String, Box<dyn Error + Send + Sync>> {
|
||||||
let body = req.collect().await?.to_bytes();
|
let body = req.collect().await?.to_bytes();
|
||||||
|
|
||||||
Ok(String::from_utf8(body.to_vec())?)
|
Ok(String::from_utf8(body.to_vec())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn extract_body_bytes(
|
||||||
|
req: Request<Incoming>,
|
||||||
|
) -> Result<Bytes, Box<dyn Error + Send + Sync>> {
|
||||||
|
Ok(req.collect().await?.to_bytes())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-19
@@ -11,7 +11,7 @@ use http_body_util::Full;
|
|||||||
use hyper::body::Bytes;
|
use hyper::body::Bytes;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::error::{ServerError, Result};
|
use crate::error::{Result, ServerError};
|
||||||
|
|
||||||
/// Builder utility for constructing HTTP responses.
|
/// Builder utility for constructing HTTP responses.
|
||||||
///
|
///
|
||||||
@@ -38,8 +38,7 @@ impl Responder {
|
|||||||
.status(StatusCode::OK)
|
.status(StatusCode::OK)
|
||||||
.header(CONTENT_TYPE, "text/html; charset=utf-8")
|
.header(CONTENT_TYPE, "text/html; charset=utf-8")
|
||||||
.body(Full::new(body.into()))
|
.body(Full::new(body.into()))
|
||||||
.map_err(|e| ServerError::response("Failed to build HTML response")
|
.map_err(|e| ServerError::response("Failed to build HTML response").with_source(e))
|
||||||
.with_source(e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a JSON response with the given value.
|
/// Creates a JSON response with the given value.
|
||||||
@@ -63,8 +62,7 @@ impl Responder {
|
|||||||
.status(StatusCode::SEE_OTHER)
|
.status(StatusCode::SEE_OTHER)
|
||||||
.header(LOCATION, url)
|
.header(LOCATION, url)
|
||||||
.body(Full::new(Bytes::new()))
|
.body(Full::new(Bytes::new()))
|
||||||
.map_err(|e| ServerError::response("Failed to build redirect response")
|
.map_err(|e| ServerError::response("Failed to build redirect response").with_source(e))
|
||||||
.with_source(e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a 404 Not Found response.
|
/// Creates a 404 Not Found response.
|
||||||
@@ -95,8 +93,7 @@ impl Responder {
|
|||||||
Response::builder()
|
Response::builder()
|
||||||
.status(status)
|
.status(status)
|
||||||
.body(Full::new(body.into()))
|
.body(Full::new(body.into()))
|
||||||
.map_err(|e| ServerError::response("Failed to build response")
|
.map_err(|e| ServerError::response("Failed to build response").with_source(e))
|
||||||
.with_source(e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a response with custom headers.
|
/// Creates a response with custom headers.
|
||||||
@@ -111,10 +108,9 @@ impl Responder {
|
|||||||
builder = builder.header(name, value);
|
builder = builder.header(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
builder
|
builder.body(Full::new(body.into())).map_err(|e| {
|
||||||
.body(Full::new(body.into()))
|
ServerError::response("Failed to build response with headers").with_source(e)
|
||||||
.map_err(|e| ServerError::response("Failed to build response with headers")
|
})
|
||||||
.with_source(e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a JSON response with a custom status code.
|
/// Creates a JSON response with a custom status code.
|
||||||
@@ -123,15 +119,13 @@ impl Responder {
|
|||||||
value: &T,
|
value: &T,
|
||||||
) -> Result<Response<Full<Bytes>>> {
|
) -> Result<Response<Full<Bytes>>> {
|
||||||
let bytes = serde_json::to_vec(value)
|
let bytes = serde_json::to_vec(value)
|
||||||
.map_err(|e| ServerError::response("JSON serialization failed")
|
.map_err(|e| ServerError::response("JSON serialization failed").with_source(e))?;
|
||||||
.with_source(e))?;
|
|
||||||
|
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(status)
|
.status(status)
|
||||||
.header(CONTENT_TYPE, "application/json")
|
.header(CONTENT_TYPE, "application/json")
|
||||||
.body(Full::new(Bytes::from(bytes)))
|
.body(Full::new(Bytes::from(bytes)))
|
||||||
.map_err(|e| ServerError::response("Failed to build JSON response")
|
.map_err(|e| ServerError::response("Failed to build JSON response").with_source(e))
|
||||||
.with_source(e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a 400 Bad Request response.
|
/// Creates a 400 Bad Request response.
|
||||||
@@ -144,18 +138,25 @@ impl Responder {
|
|||||||
Response::builder()
|
Response::builder()
|
||||||
.status(StatusCode::NO_CONTENT)
|
.status(StatusCode::NO_CONTENT)
|
||||||
.body(Full::new(Bytes::new()))
|
.body(Full::new(Bytes::new()))
|
||||||
.map_err(|e| ServerError::response("Failed to build no content response")
|
.map_err(|e| {
|
||||||
.with_source(e))
|
ServerError::response("Failed to build no content response").with_source(e)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper trait to add with_source method to ServerError
|
// Helper trait to add with_source method to ServerError
|
||||||
trait WithSource {
|
trait WithSource {
|
||||||
fn with_source(self, source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> ServerError;
|
fn with_source(
|
||||||
|
self,
|
||||||
|
source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||||
|
) -> ServerError;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WithSource for ServerError {
|
impl WithSource for ServerError {
|
||||||
fn with_source(mut self, source: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> ServerError {
|
fn with_source(
|
||||||
|
mut self,
|
||||||
|
source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||||
|
) -> ServerError {
|
||||||
match &mut self {
|
match &mut self {
|
||||||
ServerError::Response { source: s, .. } => *s = Some(source.into()),
|
ServerError::Response { source: s, .. } => *s = Some(source.into()),
|
||||||
ServerError::Request { source: s, .. } => *s = Some(source.into()),
|
ServerError::Request { source: s, .. } => *s = Some(source.into()),
|
||||||
|
|||||||
+75
-42
@@ -11,11 +11,13 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use http_body_util::Full;
|
use http_body_util::Full;
|
||||||
use http1::Builder;
|
use http1::Builder;
|
||||||
use hyper::{Request, Response, body::Incoming, server::conn::http1, service::service_fn, body::Bytes};
|
use hyper::{
|
||||||
|
Request, Response, body::Bytes, body::Incoming, server::conn::http1, service::service_fn,
|
||||||
|
};
|
||||||
use hyper_util::rt::TokioIo;
|
use hyper_util::rt::TokioIo;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use std::{future::Future, net::SocketAddr, sync::Arc, time::Duration};
|
use std::{future::Future, net::SocketAddr, sync::Arc, sync::atomic::{AtomicUsize, Ordering}, time::Duration};
|
||||||
use tokio::{net::TcpListener, signal, spawn, time::timeout};
|
use tokio::{net::TcpListener, signal, spawn};
|
||||||
|
|
||||||
/// Default connection timeout duration.
|
/// Default connection timeout duration.
|
||||||
const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30);
|
const DEFAULT_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
@@ -31,6 +33,8 @@ pub struct Server<D = ()> {
|
|||||||
pub middlewares: Arc<Vec<Box<dyn Middleware>>>,
|
pub middlewares: Arc<Vec<Box<dyn Middleware>>>,
|
||||||
/// Shared application state.
|
/// Shared application state.
|
||||||
pub data: Option<Arc<D>>,
|
pub data: Option<Arc<D>>,
|
||||||
|
/// Counter for active connections (used for graceful shutdown).
|
||||||
|
pub(crate) active_connections: Arc<AtomicUsize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Server<()> {
|
impl Server<()> {
|
||||||
@@ -63,7 +67,8 @@ impl<D: Clone + Send + Sync + 'static> Server<D> {
|
|||||||
F: Fn(Request<Incoming>) -> Fut + Send + Sync + 'static,
|
F: Fn(Request<Incoming>) -> Fut + Send + Sync + 'static,
|
||||||
Fut: Future<Output = Result<Response<Full<Bytes>>>> + Send,
|
Fut: Future<Output = Result<Response<Full<Bytes>>>> + Send,
|
||||||
{
|
{
|
||||||
self.run_with_shutdown(handler, DEFAULT_SHUTDOWN_TIMEOUT).await;
|
self.run_with_shutdown(handler, DEFAULT_SHUTDOWN_TIMEOUT)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runs the HTTP server with a custom shutdown timeout.
|
/// Runs the HTTP server with a custom shutdown timeout.
|
||||||
@@ -75,18 +80,18 @@ impl<D: Clone + Send + Sync + 'static> Server<D> {
|
|||||||
F: Fn(Request<Incoming>) -> Fut + Send + Sync + 'static,
|
F: Fn(Request<Incoming>) -> Fut + Send + Sync + 'static,
|
||||||
Fut: Future<Output = Result<Response<Full<Bytes>>>> + Send,
|
Fut: Future<Output = Result<Response<Full<Bytes>>>> + Send,
|
||||||
{
|
{
|
||||||
let addr: SocketAddr = match format!("{}:{}", self.config.ip, self.config.port)
|
let addr: SocketAddr = match format!("{}:{}", self.config.ip, self.config.port).parse() {
|
||||||
.parse()
|
|
||||||
{
|
|
||||||
Ok(addr) => addr,
|
Ok(addr) => addr,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to parse server address '{}:{}': {}",
|
error!(
|
||||||
self.config.ip, self.config.port, e);
|
"Failed to parse server address '{}:{}': {}",
|
||||||
|
self.config.ip, self.config.port, e
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let listener = match TcpListener::bind(addr).await {
|
let std_listener = match std::net::TcpListener::bind(addr) {
|
||||||
Ok(l) => l,
|
Ok(l) => l,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to bind to address {}: {}", addr, e);
|
error!("Failed to bind to address {}: {}", addr, e);
|
||||||
@@ -94,23 +99,36 @@ impl<D: Clone + Send + Sync + 'static> Server<D> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Err(e) = std_listener.set_nonblocking(true) {
|
||||||
|
warn!("Failed to set non-blocking: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
let listener = match TcpListener::from_std(std_listener) {
|
||||||
|
Ok(l) => l,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to convert to Tokio listener: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
info!("Server listening on {}", addr);
|
info!("Server listening on {}", addr);
|
||||||
|
|
||||||
let handler = Arc::new(handler);
|
let handler = Arc::new(handler);
|
||||||
let shared_middlewares = self.middlewares.clone();
|
let shared_middlewares = self.middlewares.clone();
|
||||||
|
let active_connections = self.active_connections.clone();
|
||||||
|
|
||||||
// Main accept loop
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
// Handle incoming connections
|
|
||||||
accept_result = listener.accept() => {
|
accept_result = listener.accept() => {
|
||||||
match accept_result {
|
match accept_result {
|
||||||
Ok((tcp, client_addr)) => {
|
Ok((tcp, client_addr)) => {
|
||||||
|
active_connections.fetch_add(1, Ordering::Relaxed);
|
||||||
self.handle_connection(
|
self.handle_connection(
|
||||||
tcp,
|
tcp,
|
||||||
client_addr,
|
client_addr,
|
||||||
handler.clone(),
|
handler.clone(),
|
||||||
shared_middlewares.clone(),
|
shared_middlewares.clone(),
|
||||||
|
active_connections.clone(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -119,7 +137,6 @@ impl<D: Clone + Send + Sync + 'static> Server<D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle shutdown signal
|
|
||||||
_ = signal::ctrl_c() => {
|
_ = signal::ctrl_c() => {
|
||||||
info!("Shutdown signal received, stopping server...");
|
info!("Shutdown signal received, stopping server...");
|
||||||
break;
|
break;
|
||||||
@@ -127,15 +144,26 @@ impl<D: Clone + Send + Sync + 'static> Server<D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Graceful shutdown
|
info!(
|
||||||
info!("Entering graceful shutdown (timeout: {}s)", shutdown_timeout.as_secs());
|
"Entering graceful shutdown (timeout: {}s)",
|
||||||
|
shutdown_timeout.as_secs()
|
||||||
|
);
|
||||||
|
|
||||||
// Give time for in-flight requests to complete
|
let start = std::time::Instant::now();
|
||||||
timeout(shutdown_timeout, async {
|
while active_connections.load(Ordering::Relaxed) > 0 && start.elapsed() < shutdown_timeout {
|
||||||
info!("Shutdown complete");
|
let remaining = shutdown_timeout - start.elapsed();
|
||||||
})
|
tokio::time::sleep(std::cmp::min(remaining, Duration::from_millis(100))).await;
|
||||||
.await
|
}
|
||||||
.ok();
|
|
||||||
|
let remaining = active_connections.load(Ordering::Relaxed);
|
||||||
|
if remaining > 0 {
|
||||||
|
warn!(
|
||||||
|
"Shutdown timeout reached with {} connection(s) still active",
|
||||||
|
remaining
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
info!("All connections completed, shutdown complete");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles a single incoming TCP connection.
|
/// Handles a single incoming TCP connection.
|
||||||
@@ -145,6 +173,7 @@ impl<D: Clone + Send + Sync + 'static> Server<D> {
|
|||||||
client_addr: SocketAddr,
|
client_addr: SocketAddr,
|
||||||
handler: Arc<F>,
|
handler: Arc<F>,
|
||||||
middlewares: Arc<Vec<Box<dyn Middleware>>>,
|
middlewares: Arc<Vec<Box<dyn Middleware>>>,
|
||||||
|
active_connections: Arc<AtomicUsize>,
|
||||||
) where
|
) where
|
||||||
F: Fn(Request<Incoming>) -> Fut + Send + Sync + 'static,
|
F: Fn(Request<Incoming>) -> Fut + Send + Sync + 'static,
|
||||||
Fut: Future<Output = Result<Response<Full<Bytes>>>> + Send,
|
Fut: Future<Output = Result<Response<Full<Bytes>>>> + Send,
|
||||||
@@ -154,33 +183,37 @@ impl<D: Clone + Send + Sync + 'static> Server<D> {
|
|||||||
let client_ip = client_addr.ip();
|
let client_ip = client_addr.ip();
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let conn = Builder::new().serve_connection(
|
let conn = Builder::new()
|
||||||
io,
|
.max_buf_size(8 * 1024 * 1024)
|
||||||
service_fn(move |mut req| {
|
.serve_connection(
|
||||||
let mws = middlewares.clone();
|
io,
|
||||||
let h = handler.clone();
|
service_fn(move |mut req| {
|
||||||
|
let mws = middlewares.clone();
|
||||||
|
let h = handler.clone();
|
||||||
|
|
||||||
if let Some(ref d) = data_to_inject {
|
if let Some(ref d) = data_to_inject {
|
||||||
req.extensions_mut().insert(Arc::clone(d));
|
req.extensions_mut().insert(Arc::clone(d));
|
||||||
}
|
|
||||||
|
|
||||||
async move {
|
|
||||||
req.extensions_mut().insert(client_ip);
|
|
||||||
|
|
||||||
for mw in mws.iter() {
|
|
||||||
match mw.run(req).await {
|
|
||||||
MiddlewareResult::Continue(next_req) => req = next_req,
|
|
||||||
MiddlewareResult::Respond(res) => return Ok(res),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
h(req).await
|
|
||||||
}
|
async move {
|
||||||
}),
|
req.extensions_mut().insert(client_ip);
|
||||||
);
|
|
||||||
|
for mw in mws.iter() {
|
||||||
|
match mw.run(req).await {
|
||||||
|
MiddlewareResult::Continue(next_req) => req = next_req,
|
||||||
|
MiddlewareResult::Respond(res) => return Ok(res),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h(req).await
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if let Err(err) = conn.await {
|
if let Err(err) = conn.await {
|
||||||
error!("Error serving connection from {}: {:?}", client_ip, err);
|
error!("Error serving connection from {}: {:?}", client_ip, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
active_connections.fetch_sub(1, Ordering::Relaxed);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-26
@@ -4,8 +4,8 @@
|
|||||||
//! including middleware chains and request handling.
|
//! including middleware chains and request handling.
|
||||||
|
|
||||||
use servme::{
|
use servme::{
|
||||||
ApiKeyMiddleware, Claims, IpFilterMiddleware, Responder,
|
ApiKeyMiddleware, Claims, IpFilterMiddleware, Responder, ServerBuilder, ServerConfig,
|
||||||
ServerBuilder, ServerConfig, ServerError, UrlExtract,
|
ServerError, UrlExtract,
|
||||||
};
|
};
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
@@ -26,8 +26,7 @@ fn test_server_builder_default_config() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_server_builder_with_address() {
|
fn test_server_builder_with_address() {
|
||||||
let builder = ServerBuilder::new()
|
let builder = ServerBuilder::new().address("0.0.0.0", 3000);
|
||||||
.address("0.0.0.0", 3000);
|
|
||||||
|
|
||||||
assert_eq!(builder.config.ip, "0.0.0.0");
|
assert_eq!(builder.config.ip, "0.0.0.0");
|
||||||
assert_eq!(builder.config.port, 3000);
|
assert_eq!(builder.config.port, 3000);
|
||||||
@@ -96,10 +95,7 @@ fn test_responder_redirect() {
|
|||||||
|
|
||||||
let response = result.unwrap();
|
let response = result.unwrap();
|
||||||
assert_eq!(response.status(), http::StatusCode::SEE_OTHER);
|
assert_eq!(response.status(), http::StatusCode::SEE_OTHER);
|
||||||
assert_eq!(
|
assert_eq!(response.headers().get("location").unwrap(), "/new-location");
|
||||||
response.headers().get("location").unwrap(),
|
|
||||||
"/new-location"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -144,25 +140,19 @@ fn test_ip_filter_middleware_validation() {
|
|||||||
|
|
||||||
let result = IpFilterMiddleware::new(
|
let result = IpFilterMiddleware::new(
|
||||||
vec!["192.168.1.1".to_string(), "10.0.0.1".to_string()],
|
vec!["192.168.1.1".to_string(), "10.0.0.1".to_string()],
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
|
|
||||||
// Invalid IP should fail
|
// Invalid IP should fail
|
||||||
let result = IpFilterMiddleware::new(
|
let result = IpFilterMiddleware::new(vec!["not-an-ip".to_string()], false);
|
||||||
vec!["not-an-ip".to_string()],
|
|
||||||
false
|
|
||||||
);
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ip_filter_authorization() {
|
fn test_ip_filter_authorization() {
|
||||||
// Test with checked middleware for valid IPs
|
// Test with checked middleware for valid IPs
|
||||||
let middleware = IpFilterMiddleware::new(
|
let middleware = IpFilterMiddleware::new(vec!["192.168.1.100".to_string()], false).unwrap();
|
||||||
vec!["192.168.1.100".to_string()],
|
|
||||||
false
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
let allowed_ip: IpAddr = "192.168.1.100".parse().unwrap();
|
let allowed_ip: IpAddr = "192.168.1.100".parse().unwrap();
|
||||||
let denied_ip: IpAddr = "192.168.1.200".parse().unwrap();
|
let denied_ip: IpAddr = "192.168.1.200".parse().unwrap();
|
||||||
@@ -173,10 +163,7 @@ fn test_ip_filter_authorization() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ip_filter_ipv6() {
|
fn test_ip_filter_ipv6() {
|
||||||
let middleware = IpFilterMiddleware::new(
|
let middleware = IpFilterMiddleware::new(vec!["::1".to_string()], false).unwrap();
|
||||||
vec!["::1".to_string()],
|
|
||||||
false,
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
let ipv6_local: IpAddr = "::1".parse().unwrap();
|
let ipv6_local: IpAddr = "::1".parse().unwrap();
|
||||||
let ipv6_other: IpAddr = "::2".parse().unwrap();
|
let ipv6_other: IpAddr = "::2".parse().unwrap();
|
||||||
@@ -237,10 +224,10 @@ fn test_claims_username() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_server_error_display() {
|
fn test_server_error_display() {
|
||||||
let error = ServerError::bind("127.0.0.1:8080", std::io::Error::new(
|
let error = ServerError::bind(
|
||||||
std::io::ErrorKind::AddrInUse,
|
"127.0.0.1:8080",
|
||||||
"Address already in use"
|
std::io::Error::new(std::io::ErrorKind::AddrInUse, "Address already in use"),
|
||||||
));
|
);
|
||||||
|
|
||||||
let display = format!("{}", error);
|
let display = format!("{}", error);
|
||||||
assert!(display.contains("Failed to bind"));
|
assert!(display.contains("Failed to bind"));
|
||||||
@@ -263,7 +250,7 @@ fn test_server_error_validation() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_constants_values() {
|
fn test_constants_values() {
|
||||||
use servme::constants::{
|
use servme::constants::{
|
||||||
DEFAULT_HOST, DEFAULT_PORT, JWT_COOKIE_NAME, BEARER_PREFIX, FILE_EXTENSIONS,
|
BEARER_PREFIX, DEFAULT_HOST, DEFAULT_PORT, FILE_EXTENSIONS, JWT_COOKIE_NAME,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(DEFAULT_HOST, "127.0.0.1");
|
assert_eq!(DEFAULT_HOST, "127.0.0.1");
|
||||||
|
|||||||
Reference in New Issue
Block a user