initial commit
This commit is contained in:
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Zprávy zumepro
|
||||||
|
|
||||||
|
## Návrh systému
|
||||||
|
|
||||||
|

|
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dst/
|
22
docs/Makefile
Normal file
22
docs/Makefile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.PHONY: build
|
||||||
|
build: dst .WAIT \
|
||||||
|
dst/system_arch.svg
|
||||||
|
|
||||||
|
export: exports .WAIT \
|
||||||
|
exports/system_arch.svg
|
||||||
|
|
||||||
|
dst:
|
||||||
|
mkdir dst
|
||||||
|
|
||||||
|
exports:
|
||||||
|
mkdir exports
|
||||||
|
|
||||||
|
dst/%.svg: %.dot
|
||||||
|
dot -Tsvg $< > $@
|
||||||
|
|
||||||
|
exports/%.svg: dst/%.svg
|
||||||
|
svgcleaner $< $@
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -rf dst
|
1
docs/exports/system_arch.svg
Normal file
1
docs/exports/system_arch.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg height="188pt" viewBox="0 0 141.07 188" width="141pt" xmlns="http://www.w3.org/2000/svg"><g transform="translate(4 184)"><path d="m-4 4v-188h141.07v188z" fill="#fff"/><g fill="none"><path d="m113.16-108h-89.25l-4 4v32h89.25l4-4z" stroke="#000"/><path d="m109.16-104h-89.25" stroke="#000"/><path d="m109.16-104v32" stroke="#000"/><path d="m109.16-104 4-4" stroke="#000"/></g><text font-family="arial" font-size="14" text-anchor="middle" x="66.53" y="-84.58">REST server</text><ellipse cx="66.53" cy="-18" fill="none" rx="66.53" ry="18" stroke="#000"/><text font-family="arial" font-size="14" text-anchor="middle" x="66.53" y="-12.57">API na zprávy</text><path d="m66.53-71.7v24.16" fill="none" stroke="#000"/><path d="m70.03-47.62-3.5 10-3.5-10z" stroke="#000"/><path d="m110.16-180h-87.25v36h87.25z" fill="none" stroke="#000"/><text font-family="arial" font-size="14" text-anchor="middle" x="66.53" y="-156.57">REST klient</text><path d="m66.53-143.7v24.16" fill="none" stroke="#000"/><path d="m70.03-119.62-3.5 10-3.5-10z" stroke="#000"/></g></svg>
|
After Width: | Height: | Size: 1.0 KiB |
11
docs/system_arch.dot
Normal file
11
docs/system_arch.dot
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
digraph sysdiag {
|
||||||
|
graph [fontname="arial"];
|
||||||
|
node [fontname="arial"];
|
||||||
|
edge [fontname="arial"];
|
||||||
|
|
||||||
|
RS [shape=box3d,label="REST server"]
|
||||||
|
ZA [label="API na zprávy"]
|
||||||
|
RS -> ZA
|
||||||
|
RC [shape=box,label="REST klient"]
|
||||||
|
RC -> RS
|
||||||
|
}
|
8
lib/inferium/.gitignore
vendored
Normal file
8
lib/inferium/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/target
|
||||||
|
|
||||||
|
|
||||||
|
# Added by cargo
|
||||||
|
#
|
||||||
|
# already existing elements were commented out
|
||||||
|
|
||||||
|
#/target
|
746
lib/inferium/Cargo.lock
generated
Normal file
746
lib/inferium/Cargo.lock
generated
Normal file
@@ -0,0 +1,746 @@
|
|||||||
|
# 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 = "autocfg"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-lc-rs"
|
||||||
|
version = "1.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4cd755adf9707cf671e31d944a189be3deaaeee11c8bc1d669bb8022ac90fbd0"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-sys",
|
||||||
|
"paste",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-lc-sys"
|
||||||
|
version = "0.26.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0f9dd2e03ee80ca2822dd6ea431163d2ef259f2066a4d6ccaca6d9dcb386aa43"
|
||||||
|
dependencies = [
|
||||||
|
"bindgen",
|
||||||
|
"cc",
|
||||||
|
"cmake",
|
||||||
|
"dunce",
|
||||||
|
"fs_extra",
|
||||||
|
"paste",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "bindgen"
|
||||||
|
version = "0.69.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cexpr",
|
||||||
|
"clang-sys",
|
||||||
|
"itertools",
|
||||||
|
"lazy_static",
|
||||||
|
"lazycell",
|
||||||
|
"log",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"rustc-hash",
|
||||||
|
"shlex",
|
||||||
|
"syn",
|
||||||
|
"which",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
|
||||||
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cexpr"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "clang-sys"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"libc",
|
||||||
|
"libloading",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmake"
|
||||||
|
version = "0.1.54"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dunce"
|
||||||
|
version = "1.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs_extra"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gimli"
|
||||||
|
version = "0.31.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "glob"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "home"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inferium"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazycell"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.169"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libloading"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.4.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.26"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object"
|
||||||
|
version = "0.36.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.29"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.93"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||||
|
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 = "ring"
|
||||||
|
version = "0.17.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da5349ae27d3887ca812fb375b45a4fbb36d8d12d2df394968cd86e35683fe73"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-hash"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "0.38.44"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-rs",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.102.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-rs",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.98"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.43.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.26.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
|
||||||
|
dependencies = [
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "0.26.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "which"
|
||||||
|
version = "4.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"home",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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-sys"
|
||||||
|
version = "0.59.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||||
|
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"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
24
lib/inferium/Cargo.toml
Normal file
24
lib/inferium/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "inferium"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "A small HTTP library"
|
||||||
|
authors = ["Ondřej Mekina <ondrej@mekina.cz>"]
|
||||||
|
keywords = ["http", "inferium", "zumepro"]
|
||||||
|
categories = ["network-programming", "web-programming:http-client", "web-programming:http-server"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
proc = { path = "./proc/" }
|
||||||
|
tokio = { version = "1.43.0", features = ["full"], optional = true }
|
||||||
|
tokio-rustls = { version = "0.26.1", optional = true }
|
||||||
|
webpki-roots = { version = "0.26.8", optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
full = ["async", "tokio-full"]
|
||||||
|
dev = ["full", "testing", "dep:webpki-roots"]
|
||||||
|
async = []
|
||||||
|
tokio-full = ["async", "tokio-net", "tokio-unixsocks", "tokio-tls"]
|
||||||
|
tokio-net = ["async", "dep:tokio"]
|
||||||
|
tokio-unixsocks = ["async", "dep:tokio"]
|
||||||
|
tokio-tls = ["dep:tokio-rustls", "tokio-net"]
|
||||||
|
testing = []
|
3
lib/inferium/README.md
Normal file
3
lib/inferium/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# inferium
|
||||||
|
|
||||||
|
A small HTTP library written in Rust
|
60
lib/inferium/benches/client.rs
Normal file
60
lib/inferium/benches/client.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
extern crate test;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
extern crate inferium;
|
||||||
|
use inferium::h1::{SyncClient, Response, ResponseHead, ProtocolVariant};
|
||||||
|
use inferium::{Status, HeaderValue};
|
||||||
|
use inferium::TestSyncStream;
|
||||||
|
|
||||||
|
fn parse_response_sync_inner() {
|
||||||
|
let src = "HTTP/1.1 200 OK\r\nserver: inferium\r\n\r\n".as_bytes().to_vec();
|
||||||
|
let stream = TestSyncStream::<4>::new(&src);
|
||||||
|
let mut client = SyncClient::<TestSyncStream<4>>::new(stream);
|
||||||
|
let target = Response::HeadersOnly(ResponseHead::new(
|
||||||
|
Status::Ok,
|
||||||
|
ProtocolVariant::HTTP1_1,
|
||||||
|
HashMap::from([
|
||||||
|
("server".into(), HeaderValue::new(vec!["inferium".to_string()]))
|
||||||
|
])
|
||||||
|
));
|
||||||
|
assert_eq!(client.receive_response().unwrap(), target);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_response_sync_inner_body() {
|
||||||
|
let mut src = "HTTP/1.1 200 OK\r\ncontent-length: 50\r\n\r\n".as_bytes().to_vec();
|
||||||
|
src.extend_from_slice(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||||
|
let stream = TestSyncStream::<4>::new(&src);
|
||||||
|
let mut client = SyncClient::<TestSyncStream<4>>::new(stream);
|
||||||
|
let target_head = ResponseHead::new(
|
||||||
|
Status::Ok,
|
||||||
|
ProtocolVariant::HTTP1_1,
|
||||||
|
HashMap::from([
|
||||||
|
("content-length".into(), HeaderValue::new(vec!["50".to_string()]))
|
||||||
|
])
|
||||||
|
);
|
||||||
|
let Response::WithSizedBody((h, mut b)) = client.receive_response().unwrap() else {
|
||||||
|
panic!();
|
||||||
|
};
|
||||||
|
let b = b.recv_all().unwrap();
|
||||||
|
assert_eq!(h, target_head);
|
||||||
|
assert_eq!(b, b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn parse_response_sync(b: &mut Bencher) {
|
||||||
|
b.bytes = 37;
|
||||||
|
b.iter(|| {
|
||||||
|
parse_response_sync_inner();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn parse_response_sync_with_body(b: &mut Bencher) {
|
||||||
|
b.bytes = 39 + 13;
|
||||||
|
b.iter(|| {
|
||||||
|
parse_response_sync_inner_body();
|
||||||
|
});
|
||||||
|
}
|
43
lib/inferium/benches/status_code_parsing.rs
Normal file
43
lib/inferium/benches/status_code_parsing.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#![feature(test)]
|
||||||
|
extern crate test;
|
||||||
|
use test::Bencher;
|
||||||
|
|
||||||
|
extern crate inferium;
|
||||||
|
use inferium::Status;
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn baseline(b: &mut Bencher) {
|
||||||
|
fn status_test_from_slice(raw: &[u8]) -> Result<Status, ()> {
|
||||||
|
match raw {
|
||||||
|
b"200" => Ok(Status::Ok),
|
||||||
|
b"404" => Ok(Status::NotFound),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.iter(|| {
|
||||||
|
assert_eq!(status_test_from_slice(b"200".as_slice()), Ok(Status::Ok));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn valid_ok(b: &mut Bencher) {
|
||||||
|
b.iter(|| {
|
||||||
|
assert_eq!(Status::try_from(b"200".as_slice()), Ok(Status::Ok));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn valid_internal_server_error(b: &mut Bencher) {
|
||||||
|
b.iter(|| {
|
||||||
|
assert_eq!(Status::try_from(b"500".as_slice()), Ok(Status::InternalServerError));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[bench]
|
||||||
|
fn invalid(b: &mut Bencher) {
|
||||||
|
b.iter(|| {
|
||||||
|
assert!(Status::try_from(b"690".as_slice()).is_err());
|
||||||
|
})
|
||||||
|
}
|
94
lib/inferium/examples/going_async.rs
Normal file
94
lib/inferium/examples/going_async.rs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// This is an async port of `examples/simple_server.rs`. Please see that example first.
|
||||||
|
//
|
||||||
|
// Features `async` and `tokio-net` must be enabled for this example to compile.
|
||||||
|
// It is also possible to enable feature `full` (which will enable all the features).
|
||||||
|
|
||||||
|
use std::{collections::HashMap, net::SocketAddr};
|
||||||
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
use inferium::{
|
||||||
|
h1::{ProtocolVariant, Request, ResponseHead, ServerSendError, AsyncServer},
|
||||||
|
HeaderKey, Method, Status, TokioInet
|
||||||
|
};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let listener = TcpListener::bind("localhost:8080").await.unwrap();
|
||||||
|
loop {
|
||||||
|
let (conn, addr) = listener.accept().await.unwrap();
|
||||||
|
// Here we are creating a new asynchronous task for every client.
|
||||||
|
// This will fork off in an asynchronous manner and won't block our accept loop.
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
// We created this new async block, so we need to `.await` on this function to propagate
|
||||||
|
// the future from the function to the top of the spawned task (this async block).
|
||||||
|
handle_client(conn, addr).await;
|
||||||
|
});
|
||||||
|
// You can now handle multiple clients at once... congratulations.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_client(conn: TcpStream, addr: SocketAddr) {
|
||||||
|
println!("connection from {addr:?}");
|
||||||
|
let mut server_handler = AsyncServer::<TokioInet>::new(TokioInet::new(conn));
|
||||||
|
// When receiving or sending - we call the same functions with `.await` appended (in an async
|
||||||
|
// context). This will automatically poll the returned future from the top of the context.
|
||||||
|
// The polling is handled by tokio here - so we don't need to worry about it.
|
||||||
|
while let Ok(request) = server_handler.receive_request().await {
|
||||||
|
match handle_request(request, addr) {
|
||||||
|
Ok((h, b)) => if let Err(_) = send_response(h, b, &mut server_handler).await { break; },
|
||||||
|
Err(()) => break,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!("ended connection for {addr:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_request<T>(
|
||||||
|
req: Request<T>, addr: SocketAddr
|
||||||
|
) -> Result<(ResponseHead, &'static [u8]), ()> {
|
||||||
|
let Request::HeadersOnly(headers) = req else {
|
||||||
|
return Err(());
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("req from {addr:?}: {headers}");
|
||||||
|
|
||||||
|
const OK_RESPONSE: &[u8] = b"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
<p>Hello from inferium.</p>
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
const NOT_FOUND_RESPONSE: &[u8] = b"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Not found</h1>
|
||||||
|
<p>This page was not found</p>
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
|
||||||
|
Ok(match (headers.method(), headers.uri().path()) {
|
||||||
|
(&Method::GET, "/") => (ResponseHead::new(
|
||||||
|
Status::Ok,
|
||||||
|
ProtocolVariant::HTTP1_0,
|
||||||
|
HashMap::from([
|
||||||
|
(HeaderKey::SERVER, "inferium".parse().unwrap()),
|
||||||
|
(HeaderKey::CONTENT_LENGTH, OK_RESPONSE.len().into()),
|
||||||
|
])
|
||||||
|
), OK_RESPONSE),
|
||||||
|
|
||||||
|
_ => (ResponseHead::new(
|
||||||
|
Status::NotFound,
|
||||||
|
ProtocolVariant::HTTP1_0,
|
||||||
|
HashMap::from([
|
||||||
|
(HeaderKey::SERVER, "inferium".parse().unwrap()),
|
||||||
|
(HeaderKey::CONTENT_LENGTH, NOT_FOUND_RESPONSE.len().into()),
|
||||||
|
])
|
||||||
|
), NOT_FOUND_RESPONSE),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_response(
|
||||||
|
response: ResponseHead, body: &[u8], conn: &mut AsyncServer<TokioInet>
|
||||||
|
)-> Result<(), ServerSendError> {
|
||||||
|
conn.send_response(&response).await?;
|
||||||
|
conn.send_body_bytes(body).await.map_err(|e| e.try_into().unwrap())
|
||||||
|
}
|
88
lib/inferium/examples/https_client.rs
Normal file
88
lib/inferium/examples/https_client.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// This is a port of a client from `examples/start_here.rs`. Please see that example first.
|
||||||
|
// Also... maybe brush up on some async tasks since we are going to need them here (there is an
|
||||||
|
// example on async with inferium in `examples/going_async.rs`).
|
||||||
|
//
|
||||||
|
// Features `async`, `tokio-net` and `webpki-roots` dependency must be enabled for this example to
|
||||||
|
// compile. We recommend enabling the `dev` feature when running this example.
|
||||||
|
|
||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio_rustls::{
|
||||||
|
rustls::{
|
||||||
|
pki_types::ServerName,
|
||||||
|
ClientConfig,
|
||||||
|
RootCertStore
|
||||||
|
},
|
||||||
|
TlsConnector,
|
||||||
|
TlsStream
|
||||||
|
};
|
||||||
|
use inferium::{
|
||||||
|
h1::{
|
||||||
|
ProtocolVariant,
|
||||||
|
RequestHead,
|
||||||
|
Response,
|
||||||
|
AsyncClient
|
||||||
|
},
|
||||||
|
HeaderKey,
|
||||||
|
Method,
|
||||||
|
TokioRustls
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn run_tls_handshake(raw_stream: TcpStream) -> TlsStream<TcpStream> {
|
||||||
|
let mut root_certs = RootCertStore::empty();
|
||||||
|
root_certs.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||||
|
let config = ClientConfig::builder()
|
||||||
|
.with_root_certificates(root_certs)
|
||||||
|
.with_no_client_auth();
|
||||||
|
let connector = TlsConnector::from(Arc::new(config));
|
||||||
|
let verify_server_name = ServerName::try_from("zumepro.cz").unwrap();
|
||||||
|
TlsStream::Client(connector.connect(verify_server_name, raw_stream).await.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let stream = TcpStream::connect("zumepro.cz:443").await.unwrap();
|
||||||
|
let stream = run_tls_handshake(stream).await;
|
||||||
|
let conn = TokioRustls::new(stream);
|
||||||
|
let mut client = AsyncClient::<TokioRustls>::new(conn);
|
||||||
|
|
||||||
|
let to_send = RequestHead::new(
|
||||||
|
Method::GET, "/".parse().unwrap(), ProtocolVariant::HTTP1_1,
|
||||||
|
HashMap::from([
|
||||||
|
(HeaderKey::USER_AGENT, "Mozilla/5.0 (inferium)".parse().unwrap()),
|
||||||
|
(HeaderKey::HOST, "zumepro.cz".parse().unwrap()),
|
||||||
|
(HeaderKey::CONNECTION, "close".parse().unwrap())
|
||||||
|
])
|
||||||
|
);
|
||||||
|
println!("----------> Sending\n\n{to_send}\n");
|
||||||
|
client.send_request(&to_send).await.unwrap();
|
||||||
|
let response = client.receive_response().await.unwrap();
|
||||||
|
|
||||||
|
let (header, body) = match response {
|
||||||
|
Response::HeadersOnly(h) => (h, None),
|
||||||
|
Response::WithSizedBody((_, _)) => panic!(),
|
||||||
|
Response::WithChunkedBody((h, b)) => (h, Some(b)),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("----------< Received\n\n{header}\n");
|
||||||
|
|
||||||
|
if let Some(mut body) = body {
|
||||||
|
|
||||||
|
// Since our zumepro server sends bodies chunked - we will need to handle it.
|
||||||
|
// This simple loop just collects all the chunks into the res vector.
|
||||||
|
let mut res = Vec::new();
|
||||||
|
while let Some(mut chunk) = body.get_chunk_async().await.unwrap() {
|
||||||
|
// Now here is a difference between sync and async.
|
||||||
|
//
|
||||||
|
// For easy body manipulation and no redundant trait pollution, a body within an
|
||||||
|
// asynchronous stream can be sent/received using the same methods as a synchronous one,
|
||||||
|
// but with the suffix `_async`.
|
||||||
|
res.append(&mut chunk.recv_all_async().await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"----------< Body\n\n{:?}\n",
|
||||||
|
std::str::from_utf8(&res).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
84
lib/inferium/examples/simple_server.rs
Normal file
84
lib/inferium/examples/simple_server.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use std::{collections::HashMap, net::{SocketAddr, TcpListener}};
|
||||||
|
use inferium::{
|
||||||
|
h1::{ProtocolVariant, Request, ResponseHead, ServerSendError, SyncServer},
|
||||||
|
HeaderKey, Method, Status, StdInet
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let listener = TcpListener::bind("localhost:8080").unwrap();
|
||||||
|
loop {
|
||||||
|
let (conn, addr) = listener.accept().unwrap();
|
||||||
|
println!("connection from {addr:?}");
|
||||||
|
let mut server_handler = SyncServer::<StdInet>::new(StdInet::new(conn));
|
||||||
|
// We'll serve the client as long as it sends valid requests.
|
||||||
|
// Note that this will effectively block other clients.
|
||||||
|
while let Ok(request) = server_handler.receive_request() {
|
||||||
|
// This matching is here to provide a way of controlling the while loop.
|
||||||
|
match handle_request(request, addr) {
|
||||||
|
Ok((h, b)) => if let Err(_) = send_response(h, b, &mut server_handler) { break; },
|
||||||
|
Err(()) => break,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!("ended connection for {addr:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_request<T>(
|
||||||
|
req: Request<T>, addr: SocketAddr
|
||||||
|
) -> Result<(ResponseHead, &'static [u8]), ()> {
|
||||||
|
let Request::HeadersOnly(headers) = req else {
|
||||||
|
// We will not handle POST requests with bodies - so let's tell the client to f*ck off.
|
||||||
|
return Err(());
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("req from {addr:?}: {headers}");
|
||||||
|
|
||||||
|
const OK_RESPONSE: &[u8] = b"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
<p>Hello from inferium.</p>
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
const NOT_FOUND_RESPONSE: &[u8] = b"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Not found</h1>
|
||||||
|
<p>This page was not found</p>
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
|
||||||
|
// The URI can contain both path and parameters - so we're just getting the path here.
|
||||||
|
Ok(match (headers.method(), headers.uri().path()) {
|
||||||
|
// The ok response with our index page
|
||||||
|
(&Method::GET, "/") => (ResponseHead::new(
|
||||||
|
Status::Ok,
|
||||||
|
ProtocolVariant::HTTP1_0,
|
||||||
|
HashMap::from([
|
||||||
|
(HeaderKey::SERVER, "inferium".parse().unwrap()),
|
||||||
|
(HeaderKey::CONTENT_LENGTH, OK_RESPONSE.len().into()),
|
||||||
|
])
|
||||||
|
), OK_RESPONSE),
|
||||||
|
|
||||||
|
// The not found response with an example not found page
|
||||||
|
_ => (ResponseHead::new(
|
||||||
|
Status::NotFound,
|
||||||
|
ProtocolVariant::HTTP1_0,
|
||||||
|
HashMap::from([
|
||||||
|
(HeaderKey::SERVER, "inferium".parse().unwrap()),
|
||||||
|
(HeaderKey::CONTENT_LENGTH, NOT_FOUND_RESPONSE.len().into()),
|
||||||
|
])
|
||||||
|
), NOT_FOUND_RESPONSE),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_response(
|
||||||
|
response: ResponseHead, body: &[u8], conn: &mut SyncServer<StdInet>
|
||||||
|
)-> Result<(), ServerSendError> {
|
||||||
|
conn.send_response(&response)?;
|
||||||
|
// The send body can fail on an I/O error or if the content-length header does not match the
|
||||||
|
// actual sent length in this scenario. But we know that we have the correct length so with
|
||||||
|
// `.try_into().unwrap()` we tell inferium to convert the error and panic on (not so much)
|
||||||
|
// possible body length discrepancy.
|
||||||
|
conn.send_body_bytes(body).map_err(|e| e.try_into().unwrap())
|
||||||
|
}
|
101
lib/inferium/examples/start_here.rs
Normal file
101
lib/inferium/examples/start_here.rs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Hello, and welcome to inferium. A performance-oriented small HTTP library written in Rust that
|
||||||
|
// keeps you (the user) in charge.
|
||||||
|
|
||||||
|
// Let's first import some necessary things.
|
||||||
|
|
||||||
|
// In inferium - HashMaps are used to store uri parameters and headers.
|
||||||
|
use std::collections::HashMap;
|
||||||
|
// TcpStream is needed if we want to connect to the internet.
|
||||||
|
use std::net::TcpStream;
|
||||||
|
|
||||||
|
use inferium::{
|
||||||
|
// The h1 module contains all the protocol specific things for HTTP/1.(0/1).
|
||||||
|
h1::{
|
||||||
|
// ProtocolVariant contains variants with the protocol versions supported in this module:
|
||||||
|
// - HTTP/1.1
|
||||||
|
// - HTTP/1.0
|
||||||
|
ProtocolVariant,
|
||||||
|
// RequestHead contains headers and the HTTP request headline (method, path, protocol).
|
||||||
|
RequestHead,
|
||||||
|
// Response here is a wrapper for a response that can have the following:
|
||||||
|
// - Headers only
|
||||||
|
// - Headers and body (with a known length or chunked)
|
||||||
|
//
|
||||||
|
// ! The body in the response is not yet collected. It is up to you if you wish to discard
|
||||||
|
// the connection or receive and collect the response body into some structure.
|
||||||
|
//
|
||||||
|
// The same things here go for the Request object which is nearly the same except that the
|
||||||
|
// headline contains protocol and status instead.
|
||||||
|
Response,
|
||||||
|
// Sync client is a stream wrapper that helps us keep track of the open connection and
|
||||||
|
// perform request/response operations.
|
||||||
|
//
|
||||||
|
// The server equivalent is SyncServer.
|
||||||
|
SyncClient
|
||||||
|
},
|
||||||
|
// Header key contains various known header keys, but can also store arbitrary (unknown) header
|
||||||
|
// key in the OTHER variant.
|
||||||
|
HeaderKey,
|
||||||
|
Method,
|
||||||
|
// StdInet here is a stream wrapper that allows the TcpStream to be used by inferium. There is
|
||||||
|
// also a unix socket equivalent and some asynchronous io wrappers.
|
||||||
|
StdInet
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Let's first create a connection... nothing weird here.
|
||||||
|
let conn = StdInet::new(TcpStream::connect("zumepro.cz:80").unwrap());
|
||||||
|
// And a client...
|
||||||
|
let mut client = SyncClient::<StdInet>::new(conn);
|
||||||
|
|
||||||
|
// Now let's create a request to send
|
||||||
|
let to_send = RequestHead::new(
|
||||||
|
// The path here is parsed into an HTTP path object (which also supports parameters)
|
||||||
|
// I'm using HTTP/1.0 in this example as HTTP/1.1 automatically infers a compatibility with
|
||||||
|
// chunked encoding (which I'm not even trying to handle here).
|
||||||
|
Method::GET, "/".parse().unwrap(), ProtocolVariant::HTTP1_0,
|
||||||
|
HashMap::from([
|
||||||
|
// All headers are HeaderKey - HeaderValue pairs. We can parse the header value into
|
||||||
|
// the desired object.
|
||||||
|
//
|
||||||
|
// Constructing arbitrary header key is supported using the OTHER variant - however
|
||||||
|
// it's not recommended as a violation of the HTTP protocol can happen.
|
||||||
|
//
|
||||||
|
// If you really want to construct an arbitrary header key - please carefully check
|
||||||
|
// that all of the symbols are valid.
|
||||||
|
(HeaderKey::USER_AGENT, "Mozilla/5.0 (inferium)".parse().unwrap()),
|
||||||
|
(HeaderKey::HOST, "zumepro.cz".parse().unwrap()),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
println!("----------> Sending\n\n{to_send}\n");
|
||||||
|
// Let's send the request - this is pretty straightforward.
|
||||||
|
client.send_request(&to_send).unwrap();
|
||||||
|
// As is receiving a response.
|
||||||
|
let response = client.receive_response().unwrap();
|
||||||
|
|
||||||
|
// Now (as we discussed earlier) - the response can have a body.
|
||||||
|
// In this example we'll try to handle a basic body with a known size.
|
||||||
|
let (header, body) = match response {
|
||||||
|
// Extracting the headers if no body is present.
|
||||||
|
Response::HeadersOnly(h) => (h, None),
|
||||||
|
// Extracting both the headers and the body if body is present.
|
||||||
|
Response::WithSizedBody((h, b)) => (h, Some(b)),
|
||||||
|
// We will not handle chunked responses in this example.
|
||||||
|
Response::WithChunkedBody((_, _)) => panic!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// inferium kindly provides a simple way to print the head of a request/response.
|
||||||
|
// It will be formatted pretty close to the actual protocol plaintext representation.
|
||||||
|
println!("----------< Received\n\n{header}\n");
|
||||||
|
|
||||||
|
// And finally... if we have a body, we'll print it.
|
||||||
|
if let Some(mut body) = body {
|
||||||
|
println!(
|
||||||
|
"----------< Body\n\n{:?}\n",
|
||||||
|
// A body is always returned in bytes. It's up to you to decode it however you see fit.
|
||||||
|
std::str::from_utf8(&mut body.recv_all().unwrap()).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// And you're done. Come on... try to run it.
|
||||||
|
}
|
1
lib/inferium/proc/.gitignore
vendored
Normal file
1
lib/inferium/proc/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
target/
|
7
lib/inferium/proc/Cargo.lock
generated
Normal file
7
lib/inferium/proc/Cargo.lock
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc"
|
||||||
|
version = "0.1.0"
|
9
lib/inferium/proc/Cargo.toml
Normal file
9
lib/inferium/proc/Cargo.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "proc"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
175
lib/inferium/proc/src/lib.rs
Normal file
175
lib/inferium/proc/src/lib.rs
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
use proc_macro::{Delimiter, TokenStream, TokenTree};
|
||||||
|
extern crate proc_macro;
|
||||||
|
|
||||||
|
#[proc_macro_derive(AutoimplHkeys)]
|
||||||
|
pub fn autoimpl_hkeys(item: TokenStream) -> TokenStream {
|
||||||
|
let mut iter = item.into_iter();
|
||||||
|
let data = autoimpl_names_get_idents(&mut iter).expect("could not parse the name enum");
|
||||||
|
|
||||||
|
let obj_ident = data.obj_ident;
|
||||||
|
let mut cases_from = String::new();
|
||||||
|
let mut cases_into = String::new();
|
||||||
|
let mut cases_text = String::new();
|
||||||
|
let mut cases_display = String::new();
|
||||||
|
for entry in data.names {
|
||||||
|
let entry_transformed = autoimpl_hkeys_transform(&entry);
|
||||||
|
cases_from.push_str(&format!("{entry_transformed:?} => Self::{entry},\n"));
|
||||||
|
cases_into.push_str(&format!(
|
||||||
|
"{obj_ident}::{entry} => String::from({entry_transformed:?}),\n"
|
||||||
|
));
|
||||||
|
cases_text.push_str(&format!("{obj_ident}::{entry} => b{entry_transformed:?},\n"));
|
||||||
|
cases_display.push_str(
|
||||||
|
&format!("{obj_ident}::{entry} => write!(f, {entry_transformed:?}),")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("impl From<&str> for {obj_ident} {{
|
||||||
|
fn from(s: &str) -> Self {{
|
||||||
|
match s.to_lowercase().as_str() {{
|
||||||
|
{cases_from}
|
||||||
|
v => Self::OTHER(v.to_string()),
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
impl From<{obj_ident}> for String {{
|
||||||
|
fn from(val: {obj_ident}) -> Self {{
|
||||||
|
match val {{
|
||||||
|
{cases_into}
|
||||||
|
{obj_ident}::OTHER(v) => v.clone(),
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
impl {obj_ident} {{
|
||||||
|
pub(crate) fn text(&self) -> &[u8] {{
|
||||||
|
match self {{
|
||||||
|
{cases_text}
|
||||||
|
Self::OTHER(v) => v.as_bytes(),
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
impl std::fmt::Display for {obj_ident} {{
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {{
|
||||||
|
match self {{
|
||||||
|
{cases_display}
|
||||||
|
Self::OTHER(v) => write!(f, \"{{v}}\"),
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}").parse().expect("could not parse the created autoimpl")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[proc_macro_derive(AutoimplMethods)]
|
||||||
|
pub fn autoimpl_methods(item: TokenStream) -> TokenStream {
|
||||||
|
let mut iter = item.into_iter();
|
||||||
|
let data = autoimpl_names_get_idents(&mut iter).expect("could not parse the name enum");
|
||||||
|
|
||||||
|
let obj_ident = data.obj_ident;
|
||||||
|
let mut cases_from = String::new();
|
||||||
|
let mut cases_into = String::new();
|
||||||
|
let mut cases_text = String::new();
|
||||||
|
for entry in data.names {
|
||||||
|
cases_from.push_str(&format!("{entry:?} => Ok(Self::{entry}),\n"));
|
||||||
|
cases_into.push_str(&format!("{obj_ident}::{entry} => {entry:?},\n"));
|
||||||
|
cases_text.push_str(&format!("{obj_ident}::{entry} => b{entry:?},\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("impl std::str::FromStr for {obj_ident} {{
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {{
|
||||||
|
match s {{
|
||||||
|
{cases_from}
|
||||||
|
_ => Err(()),
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
impl From<{obj_ident}> for &'static str {{
|
||||||
|
fn from(val: {obj_ident}) -> Self {{
|
||||||
|
match val {{
|
||||||
|
{cases_into}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
impl {obj_ident} {{
|
||||||
|
pub(crate) fn text(&self) -> &'static [u8] {{
|
||||||
|
match self {{
|
||||||
|
{cases_text}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}").parse().expect("could not parse the created autoimpl")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn autoimpl_hkeys_transform(name: &str) -> String {
|
||||||
|
name.to_lowercase().replace("_", "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! get_tok {
|
||||||
|
(req $($rest:tt)+) => {
|
||||||
|
get_tok!($($rest)+)?.ok_or(())?
|
||||||
|
};
|
||||||
|
|
||||||
|
($iter:ident == $t:ident $val:literal) => {{
|
||||||
|
get_tok!($t $iter).map(
|
||||||
|
|v| v.map(|v| v.to_string() == $val)
|
||||||
|
).unwrap_or_else(|_| Some(false))
|
||||||
|
}};
|
||||||
|
|
||||||
|
($iter:ident != $t:ident $val:literal) => {
|
||||||
|
get_tok!($t $iter).map(
|
||||||
|
|v| v.map(|v| v.to_string() != $val)
|
||||||
|
).unwrap_or_else(|_| Some(false))
|
||||||
|
};
|
||||||
|
|
||||||
|
($t:ident $iter:ident) => {{
|
||||||
|
match $iter.next() {
|
||||||
|
Some(TokenTree::$t(val)) => Ok(Some(val)),
|
||||||
|
None => Ok(None),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NameData {
|
||||||
|
obj_ident: String,
|
||||||
|
names: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn autoimpl_names_get_idents<I: Iterator<Item = TokenTree>>(
|
||||||
|
iter: &mut I
|
||||||
|
) -> Result<NameData, ()> {
|
||||||
|
while let Some(is_enum_start) = get_tok!(iter == Ident "enum") {
|
||||||
|
if is_enum_start {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let obj_ident = get_tok!(req Ident iter).to_string();
|
||||||
|
let mut iter = {
|
||||||
|
let group = get_tok!(req Group iter);
|
||||||
|
if group.delimiter() != Delimiter::Brace {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
group.stream().into_iter()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut names = Vec::new();
|
||||||
|
loop {
|
||||||
|
let Some(name) = get_tok!(Ident iter)? else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if name.to_string() == "OTHER" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
names.push(name.to_string());
|
||||||
|
match get_tok!(iter == Punct ",") {
|
||||||
|
Some(true) => {},
|
||||||
|
Some(false) => Err(())?,
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(NameData { obj_ident, names })
|
||||||
|
}
|
1130
lib/inferium/src/body.rs
Normal file
1130
lib/inferium/src/body.rs
Normal file
File diff suppressed because it is too large
Load Diff
200
lib/inferium/src/headers.rs
Normal file
200
lib/inferium/src/headers.rs
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
use proc::AutoimplHkeys;
|
||||||
|
|
||||||
|
/// Accepted valid HTTP header keys
|
||||||
|
///
|
||||||
|
/// These can be sent by both the client and the server.
|
||||||
|
/// The calling implementation shall check and verify the validity of the headers, or may ignore
|
||||||
|
/// any invalid ones.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[derive(AutoimplHkeys)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum HeaderKey {
|
||||||
|
ACCEPT,
|
||||||
|
ACCEPT_CHARSET,
|
||||||
|
ACCEPT_ENCODING,
|
||||||
|
ACCEPT_LANGUAGE,
|
||||||
|
ACCEPT_RANGES,
|
||||||
|
ACCESS_CONTROL_ALLOW_CREDENTIALS,
|
||||||
|
ACCESS_CONTROL_ALLOW_HEADERS,
|
||||||
|
ACCESS_CONTROL_ALLOW_METHODS,
|
||||||
|
ACCESS_CONTROL_ALLOW_ORIGIN,
|
||||||
|
ACCESS_CONTROL_EXPOSE_HEADERS,
|
||||||
|
ACCESS_CONTROL_MAX_AGE,
|
||||||
|
ACCESS_CONTROL_REQUEST_HEADERS,
|
||||||
|
ACCESS_CONTROL_REQUEST_METHOD,
|
||||||
|
AGE,
|
||||||
|
ALLOW,
|
||||||
|
ALT_SVC,
|
||||||
|
AUTHORIZATION,
|
||||||
|
CACHE_CONTROL,
|
||||||
|
CACHE_STATUS,
|
||||||
|
CDN_CACHE_CONTROL,
|
||||||
|
CONNECTION,
|
||||||
|
CONTENT_DISPOSITION,
|
||||||
|
CONTENT_ENCODING,
|
||||||
|
CONTENT_LANGUAGE,
|
||||||
|
CONTENT_LENGTH,
|
||||||
|
CONTENT_LOCATION,
|
||||||
|
CONTENT_RANGE,
|
||||||
|
CONTENT_SECURITY_POLICY,
|
||||||
|
CONTENT_SECURITY_POLICY_REPORT_ONLY,
|
||||||
|
CONTENT_TYPE,
|
||||||
|
COOKIE,
|
||||||
|
DNT,
|
||||||
|
DATE,
|
||||||
|
ETAG,
|
||||||
|
EXPECT,
|
||||||
|
EXPIRES,
|
||||||
|
FORWARDED,
|
||||||
|
FROM,
|
||||||
|
HOST,
|
||||||
|
IF_MATCH,
|
||||||
|
IF_MODIFIED_SINCE,
|
||||||
|
IF_NONE_MATCH,
|
||||||
|
IF_RANGE,
|
||||||
|
IF_UNMODIFIED_SINCE,
|
||||||
|
LAST_MODIFIED,
|
||||||
|
LINK,
|
||||||
|
LOCATION,
|
||||||
|
MAX_FORWARDS,
|
||||||
|
ORIGIN,
|
||||||
|
PRAGMA,
|
||||||
|
PROXY_AUTHENTICATE,
|
||||||
|
PROXY_AUTHORIZATION,
|
||||||
|
PUBLIC_KEY_PINS,
|
||||||
|
PUBLIC_KEY_PINS_REPORT_ONLY,
|
||||||
|
RANGE,
|
||||||
|
REFERER,
|
||||||
|
REFERRER_POLICY,
|
||||||
|
REFRESH,
|
||||||
|
RETRY_AFTER,
|
||||||
|
SEC_WEBSOCKET_ACCEPT,
|
||||||
|
SEC_WEBSOCKET_EXTENSIONS,
|
||||||
|
SEC_WEBSOCKET_KEY,
|
||||||
|
SEC_WEBSOCKET_PROTOCOL,
|
||||||
|
SEC_WEBSOCKET_VERSION,
|
||||||
|
SERVER,
|
||||||
|
SET_COOKIE,
|
||||||
|
STRICT_TRANSPORT_SECURITY,
|
||||||
|
TE,
|
||||||
|
TRAILER,
|
||||||
|
TRANSFER_ENCODING,
|
||||||
|
UPGRADE,
|
||||||
|
UPGRADE_INSECURE_REQUESTS,
|
||||||
|
USER_AGENT,
|
||||||
|
VARY,
|
||||||
|
VIA,
|
||||||
|
WARNING,
|
||||||
|
WWW_AUTHENTICATE,
|
||||||
|
X_CONTENT_TYPE_OPTIONS,
|
||||||
|
X_DNS_PREFETCH_CONTROL,
|
||||||
|
X_FRAME_OPTIONS,
|
||||||
|
X_XSS_PROTECTION,
|
||||||
|
OTHER(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type containing all the header values for a given header key.
|
||||||
|
///
|
||||||
|
/// Header entries are not omitted if duplicit, but chained to this type.
|
||||||
|
///
|
||||||
|
/// Some header keys (such as _cookie_) may require multiple entries. Inferium allows for all
|
||||||
|
/// header keys to have duplicit entries.
|
||||||
|
///
|
||||||
|
/// This type does however provide a way to query the first entry for a given key for easy
|
||||||
|
/// manipulation.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Hash, Default, Debug)]
|
||||||
|
pub struct HeaderValue {
|
||||||
|
inner: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for HeaderValue {
|
||||||
|
type Err = ();
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if !is_valid(s) {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
inner: vec![s.to_string()],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! autoimpl_valid_hval {
|
||||||
|
([$($from:ty),*]) => {
|
||||||
|
$(autoimpl_valid_hval!($from);)*
|
||||||
|
};
|
||||||
|
($from: ty) => {
|
||||||
|
impl From<$from> for HeaderValue {
|
||||||
|
fn from(value: $from) -> Self {
|
||||||
|
Self { inner: vec![value.to_string()] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
autoimpl_valid_hval!([usize, u32, u64, u128, i32, i64, i128]);
|
||||||
|
|
||||||
|
fn is_valid(v: &str) -> bool {
|
||||||
|
for ch in v.chars() {
|
||||||
|
match ch {
|
||||||
|
'a'..='z' | 'A'..='Z' | '0'..='9' | ':' | ' ' | ',' | '.' | '=' | '&' | '*' | '/' |
|
||||||
|
'-' | '!' | '#' | '\'' | '(' | ')' | '+' | ';' | '@' | '[' | ']' | '~' => {},
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeaderValue {
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn new(inner: Vec<String>) -> Self {
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn add(&mut self, val: String) {
|
||||||
|
self.inner.push(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query the first entry for this header key.
|
||||||
|
///
|
||||||
|
/// See the documentation of [`HeaderValue`] for more information.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// This will panic if this instance of [`HeaderValue`] is in an invalid state, i.e. has no
|
||||||
|
/// value and hence should not exist as an instance at all.
|
||||||
|
#[inline]
|
||||||
|
pub fn get(&self) -> &str {
|
||||||
|
self.inner.first().expect("invalid header value state")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all the entries for this header key.
|
||||||
|
///
|
||||||
|
/// See the documentation of [`HeaderValue`] for more information.
|
||||||
|
#[inline]
|
||||||
|
pub fn all(&self) -> &Vec<String> {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::HeaderKey;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_cache_control_raw() {
|
||||||
|
assert_eq!(HeaderKey::from("Cache-Control"), HeaderKey::CACHE_CONTROL);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn into_user_agent_raw() {
|
||||||
|
assert_eq!(String::from(HeaderKey::USER_AGENT), "user-agent");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_raw() {
|
||||||
|
assert_eq!(HeaderKey::from("cache-agent"), HeaderKey::OTHER("cache-agent".to_string()));
|
||||||
|
}
|
||||||
|
}
|
1013
lib/inferium/src/io.rs
Normal file
1013
lib/inferium/src/io.rs
Normal file
File diff suppressed because it is too large
Load Diff
32
lib/inferium/src/lib.rs
Normal file
32
lib/inferium/src/lib.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
pub mod settings;
|
||||||
|
|
||||||
|
mod io;
|
||||||
|
pub use io::{StdInet, StdUnix};
|
||||||
|
#[cfg(feature = "tokio-net")]
|
||||||
|
pub use io::TokioInet;
|
||||||
|
#[cfg(feature = "tokio-unixsocks")]
|
||||||
|
pub use io::TokioUnix;
|
||||||
|
#[cfg(feature = "tokio-tls")]
|
||||||
|
pub use io::TokioRustls;
|
||||||
|
#[cfg(feature = "testing")]
|
||||||
|
pub use io::TestSyncStream;
|
||||||
|
#[cfg(all(feature = "testing", feature = "async"))]
|
||||||
|
pub use io::TestAsyncStream;
|
||||||
|
|
||||||
|
mod proto;
|
||||||
|
pub use proto::h1;
|
||||||
|
|
||||||
|
mod status;
|
||||||
|
pub use status::Status;
|
||||||
|
|
||||||
|
mod headers;
|
||||||
|
pub use headers::{HeaderKey, HeaderValue};
|
||||||
|
|
||||||
|
mod method;
|
||||||
|
pub use method::Method;
|
||||||
|
|
||||||
|
mod path;
|
||||||
|
pub use path::{HttpPath, HttpPathParseError};
|
||||||
|
|
||||||
|
mod body;
|
||||||
|
pub use body::{Incoming, SizedIn, Outgoing};
|
51
lib/inferium/src/method.rs
Normal file
51
lib/inferium/src/method.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use proc::AutoimplMethods;
|
||||||
|
|
||||||
|
#[derive(AutoimplMethods)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Method {
|
||||||
|
GET,
|
||||||
|
HEAD,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
|
DELETE,
|
||||||
|
CONNECT,
|
||||||
|
OPTIONS,
|
||||||
|
TRACE,
|
||||||
|
PATCH,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Method {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", <&'static str>::from(*self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::method::Method;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn method_from_raw() {
|
||||||
|
let src = "GET";
|
||||||
|
assert_eq!(src.parse::<Method>(), Ok(Method::GET));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn method_to_raw() {
|
||||||
|
assert_eq!(<&'static str>::from(Method::GET), "GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn method_lowercase() {
|
||||||
|
let src = "get";
|
||||||
|
assert_eq!(src.parse::<Method>(), Err(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn method_nonexistent() {
|
||||||
|
let src = "GOST";
|
||||||
|
assert_eq!(src.parse::<Method>(), Err(()));
|
||||||
|
}
|
||||||
|
}
|
286
lib/inferium/src/path.rs
Normal file
286
lib/inferium/src/path.rs
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// A valid HTTP path with possible GET parameters.
|
||||||
|
///
|
||||||
|
/// This struct provides multiple guarantees:
|
||||||
|
/// - The path is a valid [`String`]
|
||||||
|
/// - The path's first character is "/"
|
||||||
|
/// - The path is not empty (the shortest allowed path is "/")
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HttpPath {
|
||||||
|
pub(crate) path: String,
|
||||||
|
pub(crate) params: Option<HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum HttpPathParseError {
|
||||||
|
/// The URI could not be converted into a valid [`String`].
|
||||||
|
InvalidString,
|
||||||
|
/// The path does not begin with a slash.
|
||||||
|
NoslashStart,
|
||||||
|
/// The path contains invalid characters as per
|
||||||
|
/// [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3).
|
||||||
|
InvalidPath,
|
||||||
|
/// The query could not be parsed.
|
||||||
|
InvalidGetParams,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for HttpPath {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.serialize_to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for HttpPath {
|
||||||
|
type Err = HttpPathParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if !s.starts_with('/') {
|
||||||
|
return Err(HttpPathParseError::NoslashStart);
|
||||||
|
}
|
||||||
|
let mut value = s.split('?');
|
||||||
|
let Some(path) = validate_http_path_chars(&mut value.next().unwrap().chars()) else {
|
||||||
|
return Err(HttpPathParseError::InvalidPath);
|
||||||
|
};
|
||||||
|
let params = parse_get_params(value.next())
|
||||||
|
.map_err(|_| HttpPathParseError::InvalidGetParams)?;
|
||||||
|
Ok(HttpPath {
|
||||||
|
path,
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&[u8]> for HttpPath {
|
||||||
|
type Error = HttpPathParseError;
|
||||||
|
|
||||||
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
let value = String::from_utf8(Vec::from(value))
|
||||||
|
.map_err(|_| HttpPathParseError::InvalidString)?;
|
||||||
|
if !value.starts_with('/') {
|
||||||
|
return Err(HttpPathParseError::NoslashStart);
|
||||||
|
}
|
||||||
|
let mut value = value.split('?');
|
||||||
|
let Some(path) = validate_http_path_chars(&mut value.next().unwrap().chars()) else {
|
||||||
|
return Err(HttpPathParseError::InvalidPath);
|
||||||
|
};
|
||||||
|
let params = parse_get_params(value.next())
|
||||||
|
.map_err(|_| HttpPathParseError::InvalidGetParams)?;
|
||||||
|
Ok(HttpPath {
|
||||||
|
path,
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_params(params: &HashMap<String, String>) -> Vec<u8> {
|
||||||
|
let mut res = Vec::new();
|
||||||
|
let mut first = true;
|
||||||
|
for entry in params.iter() {
|
||||||
|
if !first {
|
||||||
|
res.push(b'&');
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
res.extend_from_slice(entry.0.as_bytes());
|
||||||
|
res.push(b'=');
|
||||||
|
res.extend_from_slice(entry.1.as_bytes());
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_params_to_string(params: &HashMap<String, String>) -> String {
|
||||||
|
let mut res = String::new();
|
||||||
|
let mut first = true;
|
||||||
|
for entry in params.iter() {
|
||||||
|
if !first {
|
||||||
|
res.push('&');
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
res.push_str(entry.0);
|
||||||
|
res.push('=');
|
||||||
|
res.push_str(entry.1);
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpPath {
|
||||||
|
pub(crate) fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = Vec::new();
|
||||||
|
res.extend_from_slice(self.path.as_bytes());
|
||||||
|
if let Some(ref p) = self.params {
|
||||||
|
res.push(b'?');
|
||||||
|
res.extend_from_slice(&serialize_params(p));
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_to_string(&self) -> String {
|
||||||
|
let mut res = String::new();
|
||||||
|
res.push_str(&self.path);
|
||||||
|
if let Some(ref p) = self.params {
|
||||||
|
res.push('?');
|
||||||
|
res.push_str(&serialize_params_to_string(p));
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn path(&self) -> &str {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn params(&self) -> &Option<HashMap<String, String>> {
|
||||||
|
&self.params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_get_params(raw: Option<&str>) ->
|
||||||
|
Result<Option<HashMap<String, String>>, ()> {
|
||||||
|
let raw = match raw {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
let mut res = HashMap::new();
|
||||||
|
let mut tmp = String::new();
|
||||||
|
let mut key = None;
|
||||||
|
for cur in raw.chars() {
|
||||||
|
parse_get_params_handle_char(
|
||||||
|
&mut tmp,
|
||||||
|
cur,
|
||||||
|
&mut key,
|
||||||
|
&mut res,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if let (Some(k), 1..) = (key, tmp.len()) {
|
||||||
|
res.insert(k.to_string(), tmp);
|
||||||
|
};
|
||||||
|
Ok(Some(res))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn parse_get_params_handle_char(
|
||||||
|
tmp: &mut String,
|
||||||
|
cur: char,
|
||||||
|
key: &mut Option<String>,
|
||||||
|
res: &mut HashMap<String, String>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
println!("cur: {cur}, key: {key:?}");
|
||||||
|
match (cur, &key) {
|
||||||
|
('&', Some(k)) => {
|
||||||
|
res.insert(k.to_string(), std::mem::take(tmp));
|
||||||
|
*key = None;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
('=', None) => {
|
||||||
|
*key = Some(std::mem::take(tmp));
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
('&', None) => Err(()),
|
||||||
|
('=', Some(_)) => Err(()),
|
||||||
|
(c, _) => {
|
||||||
|
tmp.push(c);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_http_path_chars<I: Iterator<Item = char>>(iter: &mut I) -> Option<String> {
|
||||||
|
let mut res = String::with_capacity(iter.size_hint().1?);
|
||||||
|
for ch in iter {
|
||||||
|
match ch {
|
||||||
|
'a'..='z' |
|
||||||
|
'A'..='Z' |
|
||||||
|
'0'..='9' |
|
||||||
|
'/' |
|
||||||
|
'.' | '-' | '_' | '~' | '!' | '$' | '&' | '\'' |
|
||||||
|
'(' | ')' | '*' | '+' | ',' | ';' | '=' | ':' | '@' => res.push(ch),
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod parse_path {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use super::{HttpPath, HttpPathParseError};
|
||||||
|
|
||||||
|
macro_rules! test {
|
||||||
|
($name: ident, $raw_uri: literal, ok path $path: literal) => {
|
||||||
|
test!($name, $raw_uri, Ok(HttpPath { path: $path.to_string(), params: None }));
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
$name: ident, $raw_uri: literal,
|
||||||
|
ok path $path: literal params [$($key: ident : $value: literal),* $(,)?]
|
||||||
|
) => {
|
||||||
|
test!($name, $raw_uri, Ok(HttpPath {
|
||||||
|
path: $path.to_string(),
|
||||||
|
params: Some(HashMap::from([$((stringify!($key).to_string(), $value.to_string())),*])),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
($name: ident, $raw_uri: literal, $res: expr) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let raw = $raw_uri.as_bytes().to_vec();
|
||||||
|
let raw = raw.as_slice();
|
||||||
|
assert_eq!(HttpPath::try_from(raw), $res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
test!(valid_path_noparams, "/hello/world", ok path "/hello/world");
|
||||||
|
test!(invalid_path_noslash, "hello/world", Err(HttpPathParseError::NoslashStart));
|
||||||
|
test!(valid_singleparam, "/hello/world?hello=world", ok path "/hello/world" params [
|
||||||
|
hello: "world"
|
||||||
|
]);
|
||||||
|
test!(valid_multiparam, "/hello/world?hello=world&how=areyou", ok path "/hello/world" params [
|
||||||
|
hello: "world",
|
||||||
|
how: "areyou",
|
||||||
|
]);
|
||||||
|
test!(invalid_params, "/hello/world?&", Err(HttpPathParseError::InvalidGetParams));
|
||||||
|
test!(path_invalid_char, "/hč", Err(HttpPathParseError::InvalidPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod serialize_path {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use super::HttpPath;
|
||||||
|
|
||||||
|
macro_rules! test {
|
||||||
|
(@uri $path: literal, [$(,)?]) => {
|
||||||
|
HttpPath {
|
||||||
|
path: $path.to_string(),
|
||||||
|
params: None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(@uri $path: literal, [$($qk: ident : $qv: literal),+$(,)?]) => {
|
||||||
|
HttpPath {
|
||||||
|
path: $path.to_string(),
|
||||||
|
params: Some(HashMap::from([$((stringify!($qk).to_string(), $qv.to_string())),*])),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
($name: ident, $path: literal [$($qk: ident : $qv: literal),*$(,)?], $target: literal) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let src = test!(@uri $path, [$($qk:$qv),*]);
|
||||||
|
assert_eq!(src.serialize(), $target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test!(simple_slash, "/"[], b"/");
|
||||||
|
test!(slash_with_singleparam_query, "/"[
|
||||||
|
action: "none",
|
||||||
|
], b"/?action=none");
|
||||||
|
test!(path_with_singleparam_query, "/hello/world"[
|
||||||
|
hello: "world",
|
||||||
|
], b"/hello/world?hello=world");
|
||||||
|
}
|
666
lib/inferium/src/proto/h1/exports.rs
Normal file
666
lib/inferium/src/proto/h1/exports.rs
Normal file
@@ -0,0 +1,666 @@
|
|||||||
|
use crate::{
|
||||||
|
body::{ChunkedIn, Incoming, Outgoing, SizedIn, SizedOut},
|
||||||
|
headers::HeaderKey, io::{self, PrependableStream, Receive, Send}
|
||||||
|
};
|
||||||
|
use super::{
|
||||||
|
head::{BadRequest, BadResponse, RequestHead, ResponseHead},
|
||||||
|
stream_handler::{
|
||||||
|
ExpectedBody,
|
||||||
|
StreamHandler,
|
||||||
|
StreamHandlerReceiveError,
|
||||||
|
StreamHandlerSendError
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
use {crate::io::{AsyncSend, AsyncReceive}, std::future::Future};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum OutgoingBody {
|
||||||
|
Sized(usize),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronous HTTP client handler.
|
||||||
|
///
|
||||||
|
/// Calling I/O operations on this will block. Refer to [`AsyncClient`] (with enabled `async`
|
||||||
|
/// feature) for the asynchronous equivalent.
|
||||||
|
///
|
||||||
|
/// You can send request headers and receive responses from the server. Sending requests with body
|
||||||
|
/// is supported, but relies on the caller to send the body correctly - inferium does not force you
|
||||||
|
/// to respect the HTTP protocol completely.
|
||||||
|
///
|
||||||
|
/// This structure owns the underlying stream and can send multiple requests and responses to the
|
||||||
|
/// other endpoint no matter the protocol (inferium will allow you to send multiple requests in a
|
||||||
|
/// single HTTP/1.0 connection if you wish to).
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SyncClient<T: Send + Receive> {
|
||||||
|
handler: StreamHandler<T>,
|
||||||
|
should_send_body: OutgoingBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronous HTTP server handler.
|
||||||
|
///
|
||||||
|
/// Calling I/O operations on this will block. Refer to [`AsyncServer`] (with enabled `async`
|
||||||
|
/// feature) for the asynchronous equivalent.
|
||||||
|
///
|
||||||
|
/// You can receive requests and send response headers to the client. When sending responses with
|
||||||
|
/// body, the caller must send the body correctly - inferium does not force you to respect the HTTP
|
||||||
|
/// protocol completely.
|
||||||
|
///
|
||||||
|
/// This structure owns the underlying stream and can send multiple requests and responses to the
|
||||||
|
/// other endpoint no matter the protocol (inferium will allow you to send multiple responses in a
|
||||||
|
/// single HTTP/1.0 connection if you wish to).
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SyncServer<T: Send + Receive> {
|
||||||
|
handler: StreamHandler<T>,
|
||||||
|
should_send_body: OutgoingBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asynchronous HTTP client handler.
|
||||||
|
///
|
||||||
|
/// Calling I/O operations on this will not block and return a future. Refer to [`SyncClient`]
|
||||||
|
/// for the synchronous equivalent.
|
||||||
|
///
|
||||||
|
/// You can send request headers and receive responses from the server. Sending requests with body
|
||||||
|
/// is supported, but relies on the caller to send the body correctly - inferium does not force you
|
||||||
|
/// to respect the HTTP protocol completely.
|
||||||
|
///
|
||||||
|
/// This structure owns the underlying stream and can send multiple requests and responses to the
|
||||||
|
/// other endpoint no matter the protocol (inferium will allow you to send multiple requests in a
|
||||||
|
/// single HTTP/1.0 connection if you wish to).
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AsyncClient<T: AsyncSend + AsyncReceive> {
|
||||||
|
handler: StreamHandler<T>,
|
||||||
|
should_send_body: OutgoingBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asynchronous HTTP server handler.
|
||||||
|
///
|
||||||
|
/// Calling I/O operations on this will not block and return a future. Refer to [`SyncServer`]
|
||||||
|
/// for the synchronous equivalent.
|
||||||
|
///
|
||||||
|
/// You can receive requests and send response headers to the client. When sending responses with
|
||||||
|
/// body, the caller must send the body correctly - inferium does not force you to respect the HTTP
|
||||||
|
/// protocol completely.
|
||||||
|
///
|
||||||
|
/// This structure owns the underlying stream and can send multiple requests and responses to the
|
||||||
|
/// other endpoint no matter the protocol (inferium will allow you to send multiple responses in a
|
||||||
|
/// single HTTP/1.0 connection if you wish to).
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AsyncServer<T: AsyncSend + AsyncReceive> {
|
||||||
|
handler: StreamHandler<T>,
|
||||||
|
should_send_body: OutgoingBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! autoimpl_new {
|
||||||
|
($for: ident [$(#$attr: tt $of_type: ty),*$(,)?]) => {
|
||||||
|
$(#$attr impl $for<$of_type> {
|
||||||
|
pub fn new(stream: $of_type) -> Self {
|
||||||
|
Self { handler: StreamHandler::new(stream), should_send_body: OutgoingBody::None }
|
||||||
|
}
|
||||||
|
})*
|
||||||
|
};
|
||||||
|
($for: ident [$($of_type: ty),*$(,)?]) => {
|
||||||
|
$(impl $for<$of_type> {
|
||||||
|
pub fn new(stream: $of_type) -> Self {
|
||||||
|
Self { handler: StreamHandler::new(stream), should_send_body: OutgoingBody::None }
|
||||||
|
}
|
||||||
|
})*
|
||||||
|
};
|
||||||
|
($($for: ident $impl_list: tt),+$(,)?) => {
|
||||||
|
$(autoimpl_new!($for $impl_list);)+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
autoimpl_new! {
|
||||||
|
// All supported synchronous I/O streams
|
||||||
|
SyncClient [ io::StdInet, io::StdUnix ],
|
||||||
|
SyncServer [ io::StdInet, io::StdUnix ],
|
||||||
|
|
||||||
|
// All supported asynchronous I/O streams
|
||||||
|
AsyncClient [
|
||||||
|
#[cfg(all(feature = "async", feature = "tokio-net"))] io::TokioInet,
|
||||||
|
#[cfg(all(feature = "async", feature = "tokio-unixsocks"))] io::TokioUnix,
|
||||||
|
#[cfg(all(feature = "async", feature = "tokio-tls"))] io::TokioRustls,
|
||||||
|
],
|
||||||
|
AsyncServer [
|
||||||
|
#[cfg(all(feature = "async", feature = "tokio-net"))] io::TokioInet,
|
||||||
|
#[cfg(all(feature = "async", feature = "tokio-unixsocks"))] io::TokioUnix,
|
||||||
|
#[cfg(all(feature = "async", feature = "tokio-tls"))] io::TokioRustls,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "testing", test))]
|
||||||
|
impl<'a, const CHUNK_SIZE: usize> SyncClient<io::TestSyncStream<'a, CHUNK_SIZE>> {
|
||||||
|
pub fn new(stream: io::TestSyncStream<'a, CHUNK_SIZE>) -> Self {
|
||||||
|
Self { handler: StreamHandler::new(stream), should_send_body: OutgoingBody::None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(any(feature = "testing", test), feature = "async"))]
|
||||||
|
impl<'a, const CHUNK_SIZE: usize> AsyncClient<io::TestAsyncStream<'a, CHUNK_SIZE>> {
|
||||||
|
pub fn new(stream: io::TestAsyncStream<'a, CHUNK_SIZE>) -> Self {
|
||||||
|
Self { handler: StreamHandler::new(stream), should_send_body: OutgoingBody::None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "testing", test))]
|
||||||
|
impl<'a, const CHUNK_SIZE: usize> SyncServer<io::TestSyncStream<'a, CHUNK_SIZE>> {
|
||||||
|
pub fn new(stream: io::TestSyncStream<'a, CHUNK_SIZE>) -> Self {
|
||||||
|
Self { handler: StreamHandler::new(stream), should_send_body: OutgoingBody::None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(any(feature = "testing", test), feature = "async"))]
|
||||||
|
impl<'a, const CHUNK_SIZE: usize> AsyncServer<io::TestAsyncStream<'a, CHUNK_SIZE>> {
|
||||||
|
pub fn new(stream: io::TestAsyncStream<'a, CHUNK_SIZE>) -> Self {
|
||||||
|
Self { handler: StreamHandler::new(stream), should_send_body: OutgoingBody::None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ClientSendError {
|
||||||
|
/// This is a usage error.
|
||||||
|
///
|
||||||
|
/// It is returned when the caller fails to either send the required body or receive the
|
||||||
|
/// advertised body from the other endpoint, and thus the protocol state is violated.
|
||||||
|
StateViolated,
|
||||||
|
InvalidContentLength,
|
||||||
|
IO(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ServerSendError {
|
||||||
|
/// This is a usage error.
|
||||||
|
///
|
||||||
|
/// It is returned when the caller fails to either send the required body or receive the
|
||||||
|
/// advertised body from the other endpoint, and thus the protocol state is violated.
|
||||||
|
StateViolated,
|
||||||
|
InvalidContentLength,
|
||||||
|
IO(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StreamHandlerSendError> for ClientSendError {
|
||||||
|
fn from(value: StreamHandlerSendError) -> Self {
|
||||||
|
match value {
|
||||||
|
StreamHandlerSendError::RequiresBodyPolling => ClientSendError::StateViolated,
|
||||||
|
StreamHandlerSendError::IO(e) => ClientSendError::IO(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StreamHandlerSendError> for ServerSendError {
|
||||||
|
fn from(value: StreamHandlerSendError) -> Self {
|
||||||
|
match value {
|
||||||
|
StreamHandlerSendError::RequiresBodyPolling => ServerSendError::StateViolated,
|
||||||
|
StreamHandlerSendError::IO(e) => ServerSendError::IO(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ClientReceiveError {
|
||||||
|
/// This is a usage error.
|
||||||
|
///
|
||||||
|
/// It is returned when the caller fails to either send the required body or receive the
|
||||||
|
/// advertised body from the other endpoint, and thus the protocol state is violated.
|
||||||
|
StateViolated,
|
||||||
|
/// If the response head being received is too large to fit into the pre-allocated buffer.
|
||||||
|
HeadTooLarge,
|
||||||
|
/// The response head could not be parsed. If additional details are known - they will be
|
||||||
|
/// contained in the inner value.
|
||||||
|
InvalidHead(Option<BadResponse>),
|
||||||
|
IO(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ServerReceiveError {
|
||||||
|
/// This is a usage error.
|
||||||
|
///
|
||||||
|
/// It is returned when the caller fails to either send the required body or receive the
|
||||||
|
/// advertised body from the other endpoint, and thus the protocol state is violated.
|
||||||
|
StateViolated,
|
||||||
|
/// If the request head being received is too large to fit into the pre-allocated buffer.
|
||||||
|
HeadTooLarge,
|
||||||
|
/// The request head could not be parsed. If additional details are known - they will be
|
||||||
|
/// contained in the inner value.
|
||||||
|
InvalidHead(Option<BadRequest>),
|
||||||
|
IO(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StreamHandlerReceiveError<BadResponse>> for ClientReceiveError {
|
||||||
|
fn from(value: StreamHandlerReceiveError<BadResponse>) -> Self {
|
||||||
|
match value {
|
||||||
|
StreamHandlerReceiveError::IO(e) => Self::IO(e),
|
||||||
|
StreamHandlerReceiveError::NoData => Self::InvalidHead(None),
|
||||||
|
StreamHandlerReceiveError::ParsingError(e) => Self::InvalidHead(Some(e)),
|
||||||
|
StreamHandlerReceiveError::HeaderTooLarge => Self::HeadTooLarge,
|
||||||
|
StreamHandlerReceiveError::RequiresBodyPolling => Self::StateViolated,
|
||||||
|
StreamHandlerReceiveError::InvalidExpectedBody => Self::InvalidHead(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StreamHandlerReceiveError<BadRequest>> for ServerReceiveError {
|
||||||
|
fn from(value: StreamHandlerReceiveError<BadRequest>) -> Self {
|
||||||
|
match value {
|
||||||
|
StreamHandlerReceiveError::IO(e) => Self::IO(e),
|
||||||
|
StreamHandlerReceiveError::NoData => Self::InvalidHead(None),
|
||||||
|
StreamHandlerReceiveError::ParsingError(e) => Self::InvalidHead(Some(e)),
|
||||||
|
StreamHandlerReceiveError::HeaderTooLarge => Self::HeadTooLarge,
|
||||||
|
StreamHandlerReceiveError::RequiresBodyPolling => Self::StateViolated,
|
||||||
|
StreamHandlerReceiveError::InvalidExpectedBody => Self::InvalidHead(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A generic error possibly returned when sending a body.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum BodySendError {
|
||||||
|
/// The real body input length to send did not match the body length advertised in the headers.
|
||||||
|
LengthDiscrepancy,
|
||||||
|
IO(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<BodySendError> for ServerSendError {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: BodySendError) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
BodySendError::LengthDiscrepancy => Err(()),
|
||||||
|
BodySendError::IO(e) => Ok(Self::IO(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<BodySendError> for ClientSendError {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: BodySendError) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
BodySendError::LengthDiscrepancy => Err(()),
|
||||||
|
BodySendError::IO(e) => Ok(Self::IO(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<crate::body::SendError> for BodySendError {
|
||||||
|
fn from(value: crate::body::SendError) -> Self {
|
||||||
|
match value {
|
||||||
|
crate::body::SendError::LengthDiscrepancy => Self::LengthDiscrepancy,
|
||||||
|
crate::body::SendError::IO(e) => Self::IO(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for BodySendError {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
Self::IO(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A received response with possible incoming body.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub enum Response<'a, T> {
|
||||||
|
/// The response does not advertise an incoming body in any known way.
|
||||||
|
HeadersOnly(ResponseHead),
|
||||||
|
/// The response advertised a body using the `content-length` header.
|
||||||
|
WithSizedBody((ResponseHead, Incoming<'a, SizedIn<'a, PrependableStream<T>>>)),
|
||||||
|
/// The response advertised a chunked body using the `transfer-encoding` header.
|
||||||
|
WithChunkedBody((ResponseHead, Incoming<'a, ChunkedIn<'a, PrependableStream<T>>>)),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A received request with possible incoming body.
|
||||||
|
pub enum Request<'a, T> {
|
||||||
|
/// The request does not advertise an incoming body in any known way.
|
||||||
|
HeadersOnly(RequestHead),
|
||||||
|
/// The request advertised a body using the `content-length` header.
|
||||||
|
WithSizedBody((RequestHead, Incoming<'a, SizedIn<'a, PrependableStream<T>>>)),
|
||||||
|
/// The request advertised a chunked body using the `transfer-encoding` header.
|
||||||
|
WithChunkedBody((RequestHead, Incoming<'a, ChunkedIn<'a, PrependableStream<T>>>)),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_outgoing_req_content_length(head: &RequestHead) -> Result<Option<usize>, ClientSendError> {
|
||||||
|
let Some(l) = head.headers.get(&HeaderKey::CONTENT_LENGTH) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
Ok(Some(l.get().parse().map_err(|_| ClientSendError::InvalidContentLength)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_outgoing_res_content_length(head: &ResponseHead) -> Result<Option<usize>, ServerSendError> {
|
||||||
|
let Some(l) = head.headers.get(&HeaderKey::CONTENT_LENGTH) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
Ok(Some(l.get().parse().map_err(|_| ServerSendError::InvalidContentLength)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_sync<'a, D: Iterator<Item = &'a [u8]>, T: Send>(
|
||||||
|
data_source: &'a mut D,
|
||||||
|
stream: &'a mut PrependableStream<T>,
|
||||||
|
length: usize,
|
||||||
|
) -> Result<(), crate::body::SendError> {
|
||||||
|
Outgoing::new(SizedOut::new(data_source, stream, length)).send_all()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
async fn send_async<'a, F: Future<Output = &'a [u8]>, D: Iterator<Item = F>, T: AsyncSend>(
|
||||||
|
data_source: &'a mut D,
|
||||||
|
stream: &'a mut PrependableStream<T>,
|
||||||
|
length: usize,
|
||||||
|
) -> Result<(), crate::body::SendError> {
|
||||||
|
Outgoing::new_async(SizedOut::new(data_source, stream, length)).send_all_async().await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_sync_slc<T: Send>(
|
||||||
|
data_source: &[u8], stream: &mut PrependableStream<T>
|
||||||
|
) -> Result<(), BodySendError> {
|
||||||
|
let mut ptr = 0;
|
||||||
|
while ptr < data_source.len() {
|
||||||
|
ptr += stream.send(&data_source[ptr..])?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
async fn send_async_slc<T: AsyncSend>(
|
||||||
|
data_source: &[u8], stream: &mut PrependableStream<T>
|
||||||
|
) -> Result<(), BodySendError> {
|
||||||
|
let mut ptr = 0;
|
||||||
|
while ptr < data_source.len() {
|
||||||
|
ptr += stream.send(&data_source[ptr..]).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send + Receive> SyncClient<T> {
|
||||||
|
/// Send an HTTP request to the other endpoint (ideally a server).
|
||||||
|
///
|
||||||
|
/// If the `content-length` header is set, this will require you (the caller) to send a body
|
||||||
|
/// with the advertised content length before sending the next request.
|
||||||
|
pub fn send_request(&mut self, req_head: &RequestHead) -> Result<(), ClientSendError> {
|
||||||
|
match self.should_send_body {
|
||||||
|
OutgoingBody::None => {},
|
||||||
|
_ => return Err(ClientSendError::StateViolated),
|
||||||
|
}
|
||||||
|
if let Some(l) = get_outgoing_req_content_length(req_head)? {
|
||||||
|
self.should_send_body = OutgoingBody::Sized(l);
|
||||||
|
}
|
||||||
|
Ok(self.handler.send_request(req_head)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send the body to the other endpoint.
|
||||||
|
///
|
||||||
|
/// Note that this will only consume the iterator until the desired size is sent.
|
||||||
|
///
|
||||||
|
/// If no body should be sent (no prior request has advertised a successive body), this will
|
||||||
|
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||||
|
/// iterator.
|
||||||
|
///
|
||||||
|
/// If you wish to send a body from a loaded source (not an iterator) - refer to
|
||||||
|
/// [`SyncClient::send_body_bytes`].
|
||||||
|
pub fn send_body<'a, D: Iterator<Item = &'a [u8]>>(&'a mut self, data_source: &'a mut D) ->
|
||||||
|
Result<(), BodySendError> {
|
||||||
|
match self.should_send_body {
|
||||||
|
OutgoingBody::Sized(l) => send_sync(data_source, &mut self.handler.inner, l)?,
|
||||||
|
OutgoingBody::None => {},
|
||||||
|
}
|
||||||
|
self.should_send_body = OutgoingBody::None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send the body to the other endpoint.
|
||||||
|
///
|
||||||
|
/// If no body should be sent (no prior request has advertised a successive body), this will
|
||||||
|
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||||
|
/// iterator.
|
||||||
|
///
|
||||||
|
/// If you wish to send a body from a streamed source (an iterator) - refer to
|
||||||
|
/// [`SyncClient::send_body`].
|
||||||
|
pub fn send_body_bytes(&mut self, data_source: &[u8]) -> Result<(), BodySendError> {
|
||||||
|
let advertised_length = match self.should_send_body {
|
||||||
|
OutgoingBody::Sized(l) => l,
|
||||||
|
OutgoingBody::None => return Ok(()),
|
||||||
|
};
|
||||||
|
if advertised_length != data_source.len() {
|
||||||
|
return Err(BodySendError::LengthDiscrepancy);
|
||||||
|
}
|
||||||
|
send_sync_slc(data_source, &mut self.handler.inner)?;
|
||||||
|
self.should_send_body = OutgoingBody::None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive an HTTP response from the other endpoint (ideally a server).
|
||||||
|
///
|
||||||
|
/// If a body is advertised by the other endpoint, you (the caller) will then have to poll the
|
||||||
|
/// returned body object until the expected content length is consumed before receiving another
|
||||||
|
/// response from the endpoint.
|
||||||
|
pub fn receive_response(&mut self) -> Result<Response<T>, ClientReceiveError> {
|
||||||
|
let received = self.handler.receive_response()?;
|
||||||
|
Ok(match received.1 {
|
||||||
|
None => Response::HeadersOnly(received.0),
|
||||||
|
Some(ExpectedBody::Sized(b)) => Response::WithSizedBody((received.0, b)),
|
||||||
|
Some(ExpectedBody::Chunked(b)) => Response::WithChunkedBody((received.0, b)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send + Receive> SyncServer<T> {
|
||||||
|
/// Send an HTTP response to the other endpoint (ideally a client).
|
||||||
|
///
|
||||||
|
/// If the `content-length` header is set, this will require you (the caller) to send a body
|
||||||
|
/// with the advertised content length before sending the next response.
|
||||||
|
pub fn send_response(&mut self, req_head: &ResponseHead) -> Result<(), ServerSendError> {
|
||||||
|
match self.should_send_body {
|
||||||
|
OutgoingBody::None => {},
|
||||||
|
_ => return Err(ServerSendError::StateViolated),
|
||||||
|
}
|
||||||
|
if let Some(l) = get_outgoing_res_content_length(req_head)? {
|
||||||
|
self.should_send_body = OutgoingBody::Sized(l);
|
||||||
|
}
|
||||||
|
Ok(self.handler.send_response(req_head)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send the body to the other endpoint.
|
||||||
|
///
|
||||||
|
/// Note that this will only consume the iterator until the desired size is sent.
|
||||||
|
///
|
||||||
|
/// If no body should be sent (no prior response has advertised a successive body), this will
|
||||||
|
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||||
|
/// iterator.
|
||||||
|
///
|
||||||
|
/// If you wish to send a body from a loaded source (not an iterator) - refer to
|
||||||
|
/// [`SyncServer::send_body_bytes`].
|
||||||
|
pub fn send_body<'a, D: Iterator<Item = &'a [u8]>>(&'a mut self, data_source: &'a mut D) ->
|
||||||
|
Result<(), BodySendError> {
|
||||||
|
match self.should_send_body {
|
||||||
|
OutgoingBody::Sized(l) => send_sync(data_source, &mut self.handler.inner, l)?,
|
||||||
|
OutgoingBody::None => (),
|
||||||
|
}
|
||||||
|
self.should_send_body = OutgoingBody::None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send the body to the other endpoint.
|
||||||
|
///
|
||||||
|
/// If no body should be sent (no prior response has advertised a successive body), this will
|
||||||
|
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||||
|
/// iterator.
|
||||||
|
///
|
||||||
|
/// If you wish to send a body from a streamed source (an iterator) - refer to
|
||||||
|
/// [`SyncServer::send_body`].
|
||||||
|
pub fn send_body_bytes(&mut self, data_source: &[u8]) -> Result<(), BodySendError> {
|
||||||
|
let advertised_length = match self.should_send_body {
|
||||||
|
OutgoingBody::Sized(l) => l,
|
||||||
|
OutgoingBody::None => return Ok(()),
|
||||||
|
};
|
||||||
|
if advertised_length != data_source.len() {
|
||||||
|
return Err(BodySendError::LengthDiscrepancy);
|
||||||
|
}
|
||||||
|
send_sync_slc(data_source, &mut self.handler.inner)?;
|
||||||
|
self.should_send_body = OutgoingBody::None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive an HTTP request from the other endpoint (ideally a client).
|
||||||
|
///
|
||||||
|
/// If a body is advertised by the other endpoint, you (the caller) will then have to poll the
|
||||||
|
/// returned body object until the expected content length is consumed before receiving another
|
||||||
|
/// request from the endpoint.
|
||||||
|
pub fn receive_request(&mut self) -> Result<Request<T>, ServerReceiveError> {
|
||||||
|
let received = self.handler.receive_request()?;
|
||||||
|
Ok(match received.1 {
|
||||||
|
None => Request::HeadersOnly(received.0),
|
||||||
|
Some(ExpectedBody::Sized(b)) => Request::WithSizedBody((received.0, b)),
|
||||||
|
Some(ExpectedBody::Chunked(b)) => Request::WithChunkedBody((received.0, b)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
impl<T: AsyncSend + AsyncReceive> AsyncClient<T> {
|
||||||
|
/// Send an HTTP request to the other endpoint (ideally a server).
|
||||||
|
///
|
||||||
|
/// If the `content-length` header is set, this will require you (the caller) to send a body
|
||||||
|
/// with the advertised content length before sending the next request.
|
||||||
|
pub async fn send_request(&mut self, req_head: &RequestHead) -> Result<(), ClientSendError> {
|
||||||
|
match self.should_send_body {
|
||||||
|
OutgoingBody::None => {},
|
||||||
|
_ => return Err(ClientSendError::StateViolated),
|
||||||
|
}
|
||||||
|
if let Some(l) = get_outgoing_req_content_length(req_head)? {
|
||||||
|
self.should_send_body = OutgoingBody::Sized(l);
|
||||||
|
}
|
||||||
|
Ok(self.handler.send_request_async(req_head).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send the body to the other endpoint.
|
||||||
|
///
|
||||||
|
/// Note that this will only consume the iterator until the desired size is sent.
|
||||||
|
///
|
||||||
|
/// If no body should be sent (no prior request has advertised a successive body), this will
|
||||||
|
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||||
|
/// iterator.
|
||||||
|
///
|
||||||
|
/// If you wish to send a body from a loaded source (not an iterator) - refer to
|
||||||
|
/// [`AsyncClient::send_body_bytes`].
|
||||||
|
pub async fn send_body<'a, F: Future<Output = &'a [u8]>, D: Iterator<Item = F>>(
|
||||||
|
&'a mut self, data_source: &'a mut D
|
||||||
|
) -> Result<(), BodySendError> {
|
||||||
|
match self.should_send_body {
|
||||||
|
OutgoingBody::Sized(l) => send_async(data_source, &mut self.handler.inner, l).await?,
|
||||||
|
OutgoingBody::None => (),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send the body to the other endpoint.
|
||||||
|
///
|
||||||
|
/// If no body should be sent (no prior request has advertised a successive body), this will
|
||||||
|
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||||
|
/// iterator.
|
||||||
|
///
|
||||||
|
/// If you wish to send a body from a streamed source (an iterator) - refer to
|
||||||
|
/// [`AsyncClient::send_body`].
|
||||||
|
pub async fn send_body_bytes(&mut self, data_source: &[u8]) -> Result<(), BodySendError> {
|
||||||
|
let advertised_length = match self.should_send_body {
|
||||||
|
OutgoingBody::Sized(l) => l,
|
||||||
|
OutgoingBody::None => return Ok(()),
|
||||||
|
};
|
||||||
|
if advertised_length != data_source.len() {
|
||||||
|
return Err(BodySendError::LengthDiscrepancy);
|
||||||
|
}
|
||||||
|
send_async_slc(data_source, &mut self.handler.inner).await?;
|
||||||
|
self.should_send_body = OutgoingBody::None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive an HTTP response from the other endpoint (ideally a server).
|
||||||
|
///
|
||||||
|
/// If a body is advertised by the other endpoint, you (the caller) will then have to poll the
|
||||||
|
/// returned body object until the expected content length is consumed before receiving another
|
||||||
|
/// response from the endpoint.
|
||||||
|
pub async fn receive_response(&mut self) -> Result<Response<T>, ClientReceiveError> {
|
||||||
|
let received = self.handler.receive_response_async().await?;
|
||||||
|
Ok(match received.1 {
|
||||||
|
None => Response::HeadersOnly(received.0),
|
||||||
|
Some(ExpectedBody::Sized(b)) => Response::WithSizedBody((received.0, b)),
|
||||||
|
Some(ExpectedBody::Chunked(b)) => Response::WithChunkedBody((received.0, b)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
impl<T: AsyncSend + AsyncReceive> AsyncServer<T> {
|
||||||
|
/// Send an HTTP response to the other endpoint (ideally a client).
|
||||||
|
///
|
||||||
|
/// If the `content-length` header is set, this will require you (the caller) to send a body
|
||||||
|
/// with the advertised content length before sending the next response.
|
||||||
|
pub async fn send_response(&mut self, req_head: &ResponseHead) -> Result<(), ServerSendError> {
|
||||||
|
match self.should_send_body {
|
||||||
|
OutgoingBody::None => {},
|
||||||
|
_ => return Err(ServerSendError::StateViolated),
|
||||||
|
}
|
||||||
|
if let Some(l) = get_outgoing_res_content_length(req_head)? {
|
||||||
|
self.should_send_body = OutgoingBody::Sized(l);
|
||||||
|
}
|
||||||
|
Ok(self.handler.send_response_async(req_head).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send the body to the other endpoint.
|
||||||
|
///
|
||||||
|
/// Note that this will only consume the iterator until the desired size is sent.
|
||||||
|
///
|
||||||
|
/// If no body should be sent (no prior response has advertised a successive body), this will
|
||||||
|
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||||
|
/// iterator.
|
||||||
|
///
|
||||||
|
/// If you wish to send a body from a loaded source (not an iterator) - refer to
|
||||||
|
/// [`AsyncServer::send_body_bytes`].
|
||||||
|
pub async fn send_body<'a, F: Future<Output = &'a [u8]>, D: Iterator<Item = F>>(
|
||||||
|
&'a mut self, data_source: &'a mut D
|
||||||
|
) -> Result<(), BodySendError> {
|
||||||
|
match self.should_send_body {
|
||||||
|
OutgoingBody::Sized(l) => send_async(data_source, &mut self.handler.inner, l).await?,
|
||||||
|
OutgoingBody::None => (),
|
||||||
|
}
|
||||||
|
self.should_send_body = OutgoingBody::None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send the body to the other endpoint.
|
||||||
|
///
|
||||||
|
/// If no body should be sent (no prior response has advertised a successive body), this will
|
||||||
|
/// immediatelly return with an empty Ok - will not send anything nor consume anything from the
|
||||||
|
/// iterator.
|
||||||
|
///
|
||||||
|
/// If you wish to send a body from a streamed source (an iterator) - refer to
|
||||||
|
/// [`AsyncServer::send_body`].
|
||||||
|
pub async fn send_body_bytes(&mut self, data_source: &[u8]) -> Result<(), BodySendError> {
|
||||||
|
let advertised_length = match self.should_send_body {
|
||||||
|
OutgoingBody::Sized(l) => l,
|
||||||
|
OutgoingBody::None => return Ok(()),
|
||||||
|
};
|
||||||
|
if advertised_length != data_source.len() {
|
||||||
|
return Err(BodySendError::LengthDiscrepancy);
|
||||||
|
}
|
||||||
|
send_async_slc(data_source, &mut self.handler.inner).await?;
|
||||||
|
self.should_send_body = OutgoingBody::None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to receive an HTTP request from the other endpoint (ideally a client).
|
||||||
|
///
|
||||||
|
/// If a body is advertised by the other endpoint, you (the caller) will then have to poll the
|
||||||
|
/// returned body object until the expected content length is consumed before receiving another
|
||||||
|
/// request from the endpoint.
|
||||||
|
pub async fn receive_request(&mut self) -> Result<Request<T>, ServerReceiveError> {
|
||||||
|
let received = self.handler.receive_request_async().await?;
|
||||||
|
Ok(match received.1 {
|
||||||
|
None => Request::HeadersOnly(received.0),
|
||||||
|
Some(ExpectedBody::Sized(b)) => Request::WithSizedBody((received.0, b)),
|
||||||
|
Some(ExpectedBody::Chunked(b)) => Request::WithChunkedBody((received.0, b)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
618
lib/inferium/src/proto/h1/head.rs
Normal file
618
lib/inferium/src/proto/h1/head.rs
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::{
|
||||||
|
headers::{HeaderKey, HeaderValue},
|
||||||
|
method::Method,
|
||||||
|
path::{HttpPath, HttpPathParseError},
|
||||||
|
status::Status
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub enum ProtocolVariant {
|
||||||
|
HTTP1_0,
|
||||||
|
HTTP1_1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ProtocolVariant {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::HTTP1_0 => write!(f, "HTTP/1.0"),
|
||||||
|
Self::HTTP1_1 => write!(f, "HTTP/1.1"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProtocolVariant {
|
||||||
|
pub(crate) fn text(&self) -> &'static [u8] {
|
||||||
|
match self {
|
||||||
|
Self::HTTP1_1 => b"HTTP/1.1",
|
||||||
|
Self::HTTP1_0 => b"HTTP/1.0",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&[u8]> for ProtocolVariant {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
b"HTTP/1.0" => Ok(Self::HTTP1_0),
|
||||||
|
b"HTTP/1.1" => Ok(Self::HTTP1_1),
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_header(header: (&HeaderKey, &HeaderValue)) -> Vec<u8> {
|
||||||
|
let mut res = Vec::new();
|
||||||
|
for value in header.1.all() {
|
||||||
|
res.extend_from_slice(header.0.text());
|
||||||
|
res.extend_from_slice(b": ");
|
||||||
|
res.extend_from_slice(value.as_bytes());
|
||||||
|
res.extend_from_slice(b"\r\n");
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_header(header: (&HeaderKey, &HeaderValue)) -> String {
|
||||||
|
let mut res = String::new();
|
||||||
|
for value in header.1.all() {
|
||||||
|
res.push_str(&format!("{}: {}", header.0, value));
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RequestHead {
|
||||||
|
pub(crate) method: Method,
|
||||||
|
pub(crate) path: HttpPath,
|
||||||
|
pub(crate) protocol: ProtocolVariant,
|
||||||
|
pub(crate) headers: HashMap<HeaderKey, HeaderValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestHead {
|
||||||
|
#[inline]
|
||||||
|
pub fn method(&self) -> &Method {
|
||||||
|
&self.method
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn uri(&self) -> &HttpPath {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn proto(&self) -> &ProtocolVariant {
|
||||||
|
&self.protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn headers(&self) -> &HashMap<HeaderKey, HeaderValue> {
|
||||||
|
&self.headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RequestHead {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
writeln!(f, "{} {} {}", self.method, self.path, self.protocol)?;
|
||||||
|
for entry in self.headers.iter() {
|
||||||
|
writeln!(f, "{}", format_header(entry))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestHead {
|
||||||
|
pub fn new(
|
||||||
|
method: Method,
|
||||||
|
path: HttpPath,
|
||||||
|
protocol: ProtocolVariant,
|
||||||
|
headers: HashMap<HeaderKey, HeaderValue>,
|
||||||
|
) -> Self {
|
||||||
|
Self { method, path, protocol, headers }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestHead {
|
||||||
|
pub(crate) fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = Vec::new();
|
||||||
|
res.extend_from_slice(self.method.text());
|
||||||
|
res.push(b' ');
|
||||||
|
res.extend_from_slice(&self.path.serialize());
|
||||||
|
res.push(b' ');
|
||||||
|
res.extend_from_slice(self.protocol.text());
|
||||||
|
res.extend_from_slice(b"\r\n");
|
||||||
|
for entry in self.headers.iter() {
|
||||||
|
res.extend_from_slice(&serialize_header(entry));
|
||||||
|
}
|
||||||
|
res.extend_from_slice(b"\r\n");
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum BadRequest {
|
||||||
|
HeadLine,
|
||||||
|
InvalidPath(HttpPathParseError),
|
||||||
|
InvalidMethod,
|
||||||
|
InvalidProtocol,
|
||||||
|
InvalidHeaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HttpPathParseError> for BadRequest {
|
||||||
|
fn from(value: HttpPathParseError) -> Self {
|
||||||
|
Self::InvalidPath(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_request_head(raw: &[u8]) -> Result<RequestHead, BadRequest> {
|
||||||
|
const PAT_SPACE: &[u8] = b" ";
|
||||||
|
const JUT_SPACE: &[usize] = &[0];
|
||||||
|
const PAT_CRLF: &[u8] = b"\r\n";
|
||||||
|
const JUT_CRLF: &[usize] = &[0, 0];
|
||||||
|
|
||||||
|
// Method
|
||||||
|
let Some(meth_end) = try_find(raw, PAT_SPACE, JUT_SPACE, &0) else {
|
||||||
|
return Err(BadRequest::HeadLine);
|
||||||
|
};
|
||||||
|
let meth: Method = std::str::from_utf8(&raw[..meth_end])
|
||||||
|
.map_err(|_| BadRequest::InvalidMethod)?.parse().map_err(|_| BadRequest::InvalidMethod)?;
|
||||||
|
|
||||||
|
// Path
|
||||||
|
let path_start = meth_end + PAT_SPACE.len();
|
||||||
|
let Some(path_end) = try_find(raw, PAT_SPACE, JUT_SPACE, &path_start) else {
|
||||||
|
return Err(BadRequest::HeadLine);
|
||||||
|
};
|
||||||
|
let path = HttpPath::try_from(&raw[path_start..path_end])?;
|
||||||
|
|
||||||
|
// Protocol
|
||||||
|
let proto_start = path_end + PAT_SPACE.len();
|
||||||
|
let Some(proto_end) = try_find(raw, PAT_CRLF, JUT_CRLF, &proto_start) else {
|
||||||
|
return Err(BadRequest::HeadLine);
|
||||||
|
};
|
||||||
|
let proto: ProtocolVariant = (&raw[proto_start..proto_end]).try_into()
|
||||||
|
.map_err(|_| BadRequest::InvalidProtocol)?;
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
let mut res = HashMap::new();
|
||||||
|
let mut header_start = proto_end + PAT_CRLF.len();
|
||||||
|
loop {
|
||||||
|
let Some(header_end) = try_find(raw, PAT_CRLF, JUT_CRLF, &header_start) else {
|
||||||
|
return Err(BadRequest::InvalidHeaders);
|
||||||
|
};
|
||||||
|
if header_start == header_end {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parse_header(&raw[header_start..header_end], &mut res)
|
||||||
|
.map_err(|_| BadRequest::InvalidHeaders)?;
|
||||||
|
header_start = header_end + PAT_CRLF.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(RequestHead {
|
||||||
|
method: meth,
|
||||||
|
path,
|
||||||
|
protocol: proto,
|
||||||
|
headers: res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct ResponseHead {
|
||||||
|
pub(crate) status: Status,
|
||||||
|
pub(crate) protocol: ProtocolVariant,
|
||||||
|
pub(crate) headers: HashMap<HeaderKey, HeaderValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseHead {
|
||||||
|
#[inline]
|
||||||
|
pub fn status(&self) -> &Status {
|
||||||
|
&self.status
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn proto(&self) -> &ProtocolVariant {
|
||||||
|
&self.protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn headers(&self) -> &HashMap<HeaderKey, HeaderValue> {
|
||||||
|
&self.headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseHead {
|
||||||
|
pub fn new(
|
||||||
|
status: Status, protocol: ProtocolVariant, headers: HashMap<HeaderKey, HeaderValue>
|
||||||
|
) -> Self {
|
||||||
|
Self { status, protocol, headers }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ResponseHead {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
writeln!(f, "{} {}", self.protocol, self.status)?;
|
||||||
|
for entry in self.headers.iter() {
|
||||||
|
writeln!(f, "{}", format_header(entry))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResponseHead {
|
||||||
|
pub(crate) fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = Vec::new();
|
||||||
|
res.extend_from_slice(self.protocol.text());
|
||||||
|
res.push(b' ');
|
||||||
|
res.extend_from_slice(self.status.num());
|
||||||
|
res.push(b' ');
|
||||||
|
res.extend_from_slice(self.status.text());
|
||||||
|
res.extend_from_slice(b"\r\n");
|
||||||
|
for header in self.headers.iter() {
|
||||||
|
res.extend_from_slice(&serialize_header(header));
|
||||||
|
}
|
||||||
|
res.extend_from_slice(b"\r\n");
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum BadResponse {
|
||||||
|
/// The headline (parseable example: `HTTP/1.1 200 OK`) could not be parsed.
|
||||||
|
HeadLine,
|
||||||
|
/// The response status code is unknown.
|
||||||
|
InvalidStatusCode,
|
||||||
|
/// The advertised protocol is not supported.
|
||||||
|
InvalidProtocol,
|
||||||
|
/// Some headers are invalid. Either they have unknown keys or invalid syntax.
|
||||||
|
InvalidHeaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn parse_response_head(raw: &[u8]) -> Result<ResponseHead, BadResponse> {
|
||||||
|
const PAT_SPACE: &[u8] = b" ";
|
||||||
|
const JUT_SPACE: &[usize] = &[0];
|
||||||
|
const PAT_CRLF: &[u8] = b"\r\n";
|
||||||
|
const JUT_CRLF: &[usize] = &[0, 0];
|
||||||
|
|
||||||
|
// Protocol
|
||||||
|
let Some(proto_end) = try_find(raw, PAT_SPACE, JUT_SPACE, &0) else {
|
||||||
|
return Err(BadResponse::HeadLine);
|
||||||
|
};
|
||||||
|
let proto: ProtocolVariant = (&raw[..proto_end]).try_into()
|
||||||
|
.map_err(|_| BadResponse::InvalidProtocol)?;
|
||||||
|
|
||||||
|
// Status code
|
||||||
|
let status_start = proto_end + PAT_SPACE.len();
|
||||||
|
let Some(status_end) = try_find(raw, PAT_SPACE, JUT_SPACE, &status_start) else {
|
||||||
|
return Err(BadResponse::HeadLine);
|
||||||
|
};
|
||||||
|
let status: Status = Status::try_from(&raw[status_start..status_end])
|
||||||
|
.map_err(|_| BadResponse::InvalidStatusCode)?;
|
||||||
|
|
||||||
|
let Some(headline_end) = try_find(raw, PAT_CRLF, JUT_CRLF, &(status_end+PAT_SPACE.len())) else {
|
||||||
|
return Err(BadResponse::HeadLine);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
let mut res = HashMap::new();
|
||||||
|
let mut header_start = headline_end + PAT_CRLF.len();
|
||||||
|
loop {
|
||||||
|
let Some(header_end) = try_find(raw, PAT_CRLF, JUT_CRLF, &header_start) else {
|
||||||
|
return Err(BadResponse::InvalidHeaders);
|
||||||
|
};
|
||||||
|
if header_start == header_end {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parse_header(&raw[header_start..header_end], &mut res)
|
||||||
|
.map_err(|_| BadResponse::InvalidHeaders)?;
|
||||||
|
header_start = header_end + PAT_CRLF.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ResponseHead {
|
||||||
|
status,
|
||||||
|
protocol: proto,
|
||||||
|
headers: res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_header(
|
||||||
|
raw: &[u8],
|
||||||
|
res: &mut HashMap<HeaderKey, HeaderValue>,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let delim = try_find(raw, b": ", &[0, 0], &0).ok_or(())?;
|
||||||
|
let hkey: HeaderKey = std::str::from_utf8(&raw[..delim]).map_err(|_| ())?.into();
|
||||||
|
let hval: String = String::from_utf8(raw[delim+2..].to_vec()).map_err(|_| ())?;
|
||||||
|
if hval.is_empty() {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let entry = match res.get_mut(&hkey) {
|
||||||
|
Some(v) => v,
|
||||||
|
None => {
|
||||||
|
res.insert(hkey.clone(), HeaderValue::default());
|
||||||
|
res.get_mut(&hkey).unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
entry.add(hval);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_find(
|
||||||
|
haystack: &[u8],
|
||||||
|
pat: &[u8],
|
||||||
|
jumptable: &[usize],
|
||||||
|
start_from: &usize,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let mut pat_ptr = 0_usize;
|
||||||
|
for (idx, cur) in haystack[*start_from..].iter().enumerate() {
|
||||||
|
if pat_ptr >= pat.len() {
|
||||||
|
return Some(idx + start_from - pat.len());
|
||||||
|
}
|
||||||
|
if *cur == pat[pat_ptr] {
|
||||||
|
pat_ptr += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try_find_update_jumptable(cur, pat, &mut pat_ptr, jumptable);
|
||||||
|
}
|
||||||
|
if pat_ptr >= pat.len() {
|
||||||
|
return Some(haystack.len() - pat.len());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_find_update_jumptable(
|
||||||
|
hay: &u8,
|
||||||
|
pat: &[u8],
|
||||||
|
pat_ptr: &mut usize,
|
||||||
|
jumptable: &[usize],
|
||||||
|
) {
|
||||||
|
while *pat_ptr != 0 {
|
||||||
|
*pat_ptr = jumptable[*pat_ptr];
|
||||||
|
if *hay == pat[*pat_ptr] {
|
||||||
|
*pat_ptr += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod patfind {
|
||||||
|
use super::try_find;
|
||||||
|
|
||||||
|
macro_rules! test {
|
||||||
|
($name: ident, $src: literal, $pat: literal, $jt:tt, $start_from:literal, $res:expr) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let src = $src.as_bytes().to_vec();
|
||||||
|
let src = src.as_slice();
|
||||||
|
let pat = $pat.as_bytes().to_vec();
|
||||||
|
let pat = pat.as_slice();
|
||||||
|
let jt = vec!$jt;
|
||||||
|
let jt = jt.as_slice();
|
||||||
|
assert_eq!(try_find(src, pat, jt, &$start_from), $res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test!(valid_singlebyte, "Hello, world!", ",", [0], 0, Some(5));
|
||||||
|
test!(valid_multibyte, "Hello, world!", ", ", [0, 0], 0, Some(5));
|
||||||
|
test!(valid_begin_nostart, "Hello, world!", ", ", [0, 0], 3, Some(5));
|
||||||
|
test!(valid_begin_startpat, "Hello, world!", ", ", [0, 0], 5, Some(5));
|
||||||
|
test!(invalid_begin_midpat, "Hello, world!", ", ", [0, 0], 6, None);
|
||||||
|
test!(recurse_jumptable_01, "AAAAAB", "AAAB", [0, 0, 1, 2], 0, Some(2));
|
||||||
|
test!(recurse_jumptable_02, "ABABABC", "ABABC", [0, 0, 0, 1, 2], 0, Some(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod parse_request_head {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
headers::{HeaderKey, HeaderValue},
|
||||||
|
method::Method,
|
||||||
|
path::HttpPath,
|
||||||
|
proto::h1::head::{BadRequest, ProtocolVariant}
|
||||||
|
};
|
||||||
|
use super::{parse_request_head, RequestHead};
|
||||||
|
|
||||||
|
macro_rules! test_inner {
|
||||||
|
(@get_params [$($pk: ident : $pv: literal),+]$(,)?) => {
|
||||||
|
Some(HashMap::from([$((stringify!($pk).to_string(), $pv.to_string())),*]))
|
||||||
|
};
|
||||||
|
|
||||||
|
(@get_params []) => {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! test {
|
||||||
|
(
|
||||||
|
$name: ident,
|
||||||
|
$src: literal,
|
||||||
|
ok $method: ident $path: literal ? [$($pk: ident : $pv: literal),*$(,)?] $proto: ident,
|
||||||
|
[$($hk: ident : $hv: literal),*$(,)?]
|
||||||
|
) => {
|
||||||
|
test!($name, $src, Ok(RequestHead {
|
||||||
|
method: Method::$method,
|
||||||
|
path: HttpPath{ path:$path.to_string(), params:test_inner!(@get_params[$($pk:$pv),*]) },
|
||||||
|
protocol: ProtocolVariant::$proto,
|
||||||
|
headers: HashMap::from([$((HeaderKey::$hk,HeaderValue::new(vec![$hv.to_string()]))),*]),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
($name: ident, $src: literal, $res: expr) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let src = $src.as_bytes().to_vec();
|
||||||
|
let src = src.as_slice();
|
||||||
|
assert_eq!(parse_request_head(src), $res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test!(valid_request, "GET /hello HTTP/1.0\r\n\r\n",
|
||||||
|
ok GET "/hello"?[] HTTP1_0,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(valid_request_single_header, "GET / HTTP/1.1\r\nUser-Agent: Mozilla/5.0\r\n\r\n",
|
||||||
|
ok GET "/"?[] HTTP1_1,
|
||||||
|
[
|
||||||
|
USER_AGENT: "Mozilla/5.0",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(
|
||||||
|
valid_request_multi_header,
|
||||||
|
"GET /api?action=test HTTP/1.1\r\nUser-Agent: Mozilla/5.0\r\nConnection: close\r\n\r\n",
|
||||||
|
ok GET "/api"?[action: "test"] HTTP1_1,
|
||||||
|
[
|
||||||
|
USER_AGENT: "Mozilla/5.0",
|
||||||
|
CONNECTION: "close",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(invalid_method, "GHOST / HTTP/1.1\r\n\r\n", Err(BadRequest::InvalidMethod));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod parse_response_head {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
headers::{HeaderKey, HeaderValue},
|
||||||
|
proto::h1::head::{BadResponse, ProtocolVariant}
|
||||||
|
};
|
||||||
|
use super::{parse_response_head, ResponseHead, Status};
|
||||||
|
|
||||||
|
macro_rules! test {
|
||||||
|
(
|
||||||
|
$name: ident,
|
||||||
|
$src: literal,
|
||||||
|
ok $proto: ident $status: ident,
|
||||||
|
[$($hk: ident : $hv: literal),*$(,)?]
|
||||||
|
) => {
|
||||||
|
test!($name, $src, Ok(ResponseHead {
|
||||||
|
status: Status::$status,
|
||||||
|
protocol: ProtocolVariant::$proto,
|
||||||
|
headers: HashMap::from([$((HeaderKey::$hk,HeaderValue::new(vec![$hv.to_string()]))),*]),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
($name: ident, $src: literal, $res: expr) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let src = $src.as_bytes().to_vec();
|
||||||
|
let src = src.as_slice();
|
||||||
|
assert_eq!(parse_response_head(src), $res);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test!(valid_request, "HTTP/1.1 200 OK\r\n\r\n",
|
||||||
|
ok HTTP1_1 Ok,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(valid_request_single_header, "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n",
|
||||||
|
ok HTTP1_0 Ok,
|
||||||
|
[
|
||||||
|
CONTENT_TYPE: "text/html",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(
|
||||||
|
valid_request_multi_header,
|
||||||
|
"HTTP/1.1 200 OK\r\nServer: inferium\r\nConnection: close\r\n\r\n",
|
||||||
|
ok HTTP1_1 Ok,
|
||||||
|
[
|
||||||
|
SERVER: "inferium",
|
||||||
|
CONNECTION: "close",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(invalid_protocol, "PROTO 200 OK\r\n\r\n", Err(BadResponse::InvalidProtocol));
|
||||||
|
test!(invalid_status_code, "HTTP/1.1 42069 OK\r\n\r\n", Err(BadResponse::InvalidStatusCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod construct_outgoing {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::{headers::{HeaderKey, HeaderValue}, status::Status, path::HttpPath, method::Method};
|
||||||
|
use super::{ResponseHead, RequestHead, ProtocolVariant};
|
||||||
|
|
||||||
|
macro_rules! test {
|
||||||
|
(@headers [$($hk: ident : $hv: literal),*$(,)?]) => {
|
||||||
|
HashMap::from([$((HeaderKey::$hk, HeaderValue::new(vec![$hv.to_string()]))),*])
|
||||||
|
};
|
||||||
|
|
||||||
|
(@uri $path: literal [$($hk: ident : $hv: literal),+$(,)?]) => {
|
||||||
|
HttpPath {
|
||||||
|
path: $path.to_string(),
|
||||||
|
params: Some(HashMap::from([$((stringify!($hk).to_string(), $hv.to_string())),+])),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(@uri $path: literal [$(,)?]) => {
|
||||||
|
HttpPath {
|
||||||
|
path: $path.to_string(),
|
||||||
|
params: None,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
($name: ident, res $proto: ident $status: ident, [$($hk: ident : $hv: literal),*$(,)?],
|
||||||
|
$target: literal
|
||||||
|
) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let src = ResponseHead {
|
||||||
|
protocol: ProtocolVariant::$proto,
|
||||||
|
status: Status::$status,
|
||||||
|
headers: test!(@headers [$($hk:$hv),*]),
|
||||||
|
};
|
||||||
|
assert_eq!(src.serialize(), $target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
$name: ident,
|
||||||
|
req $method: ident $path: literal [$($qk: ident : $qv: literal),*$(,)?] $proto: ident,
|
||||||
|
[$($hk: ident : $hv: literal),*$(,)?],
|
||||||
|
$target: literal
|
||||||
|
) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let src = RequestHead {
|
||||||
|
method: Method::$method,
|
||||||
|
path: test!(@uri $path [$($qk : $qv),*]),
|
||||||
|
protocol: ProtocolVariant::$proto,
|
||||||
|
headers: test!(@headers [$($hk : $hv),*]),
|
||||||
|
};
|
||||||
|
assert_eq!(src.serialize(), $target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test!(response_simple_noheaders, res HTTP1_1 Ok, [], b"HTTP/1.1 200 OK\r\n\r\n");
|
||||||
|
test!(response_with_single_header_01, res HTTP1_0 NotFound, [
|
||||||
|
SERVER: "inferium",
|
||||||
|
], b"HTTP/1.0 404 Not Found\r\nserver: inferium\r\n\r\n");
|
||||||
|
test!(response_with_single_header_02, res HTTP1_1 Forbidden, [
|
||||||
|
SERVER: "inferium",
|
||||||
|
], b"HTTP/1.1 403 Forbidden\r\nserver: inferium\r\n\r\n");
|
||||||
|
|
||||||
|
test!(request_simple_noheaders, req GET "/"[] HTTP1_1, [], b"GET / HTTP/1.1\r\n\r\n");
|
||||||
|
test!(request_simple_single_header, req GET "/"[] HTTP1_1, [
|
||||||
|
USER_AGENT: "inferium",
|
||||||
|
], b"GET / HTTP/1.1\r\nuser-agent: inferium\r\n\r\n");
|
||||||
|
test!(request_path_single_header, req GET "/well/hello/there"[] HTTP1_0, [
|
||||||
|
USER_AGENT: "inferium",
|
||||||
|
], b"GET /well/hello/there HTTP/1.0\r\nuser-agent: inferium\r\n\r\n");
|
||||||
|
test!(request_path_with_query_single_header, req GET "/well/hello/there"[
|
||||||
|
hello: "world"
|
||||||
|
] HTTP1_0, [
|
||||||
|
USER_AGENT: "inferium",
|
||||||
|
], b"GET /well/hello/there?hello=world HTTP/1.0\r\nuser-agent: inferium\r\n\r\n");
|
||||||
|
}
|
26
lib/inferium/src/proto/h1/mod.rs
Normal file
26
lib/inferium/src/proto/h1/mod.rs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
mod head;
|
||||||
|
mod stream_handler;
|
||||||
|
mod exports;
|
||||||
|
|
||||||
|
pub use exports::{
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
SyncClient,
|
||||||
|
SyncServer,
|
||||||
|
ClientSendError,
|
||||||
|
ClientReceiveError,
|
||||||
|
ServerSendError,
|
||||||
|
ServerReceiveError,
|
||||||
|
BodySendError,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
pub use exports::{
|
||||||
|
AsyncClient,
|
||||||
|
AsyncServer,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use head::{
|
||||||
|
RequestHead,
|
||||||
|
ResponseHead,
|
||||||
|
};
|
||||||
|
pub use head::ProtocolVariant;
|
600
lib/inferium/src/proto/h1/stream_handler.rs
Normal file
600
lib/inferium/src/proto/h1/stream_handler.rs
Normal file
@@ -0,0 +1,600 @@
|
|||||||
|
use crate::{
|
||||||
|
body::{ChunkedIn, Incoming, SizedIn},
|
||||||
|
headers::HeaderKey,
|
||||||
|
io::{PrependableStream, ReaderError, ReaderValue, Receive, Send, SyncReader},
|
||||||
|
proto::h1::head::{parse_request_head, parse_response_head},
|
||||||
|
settings::BUF_SIZE_HEAD, HeaderValue
|
||||||
|
};
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
use crate::io::{AsyncReceive, AsyncSend, AsyncReader};
|
||||||
|
use super::head::{BadRequest, BadResponse, RequestHead, ResponseHead};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(super) struct StreamHandler<T> {
|
||||||
|
pub(super) inner: PrependableStream<T>,
|
||||||
|
/// Whether the caller has received the entire HTTP body from the other endpoint (if needed).
|
||||||
|
has_exhausted_body: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(test, derive(Debug))]
|
||||||
|
pub(super) enum StreamHandlerReceiveError<T> {
|
||||||
|
HeaderTooLarge,
|
||||||
|
RequiresBodyPolling,
|
||||||
|
ParsingError(T),
|
||||||
|
InvalidExpectedBody,
|
||||||
|
NoData,
|
||||||
|
IO(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BadRequest> for StreamHandlerReceiveError<BadRequest> {
|
||||||
|
fn from(value: BadRequest) -> Self {
|
||||||
|
Self::ParsingError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BadResponse> for StreamHandlerReceiveError<BadResponse> {
|
||||||
|
fn from(value: BadResponse) -> Self {
|
||||||
|
Self::ParsingError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(test, derive(Debug))]
|
||||||
|
pub(super) enum StreamHandlerSendError {
|
||||||
|
RequiresBodyPolling,
|
||||||
|
IO(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for StreamHandlerSendError {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
Self::IO(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> StreamHandler<T> {
|
||||||
|
pub(super) fn new(inner: T) -> Self {
|
||||||
|
Self { inner: PrependableStream::new(inner), has_exhausted_body: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! leaky {
|
||||||
|
($stream: expr, $head: ident, $body: ident) => {{
|
||||||
|
$stream.prepend_to_read($body);
|
||||||
|
$head
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(test, derive(Debug))]
|
||||||
|
pub(super) enum ExpectedBody<'a, T> {
|
||||||
|
Sized(Incoming<'a, SizedIn<'a, T>>),
|
||||||
|
Chunked(Incoming<'a, ChunkedIn<'a, T>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
type FullResponse<'a, T> = (ResponseHead, Option<ExpectedBody<'a, PrependableStream<T>>>);
|
||||||
|
type FullRequest<'a, T> = (RequestHead, Option<ExpectedBody<'a, PrependableStream<T>>>);
|
||||||
|
|
||||||
|
fn construct_response_body<T: Receive>(
|
||||||
|
sh: &mut StreamHandler<T>,
|
||||||
|
res: ResponseHead,
|
||||||
|
) -> Result<FullResponse<T>, StreamHandlerReceiveError<BadResponse>> {
|
||||||
|
if let Some(transfer_encoding) = res.headers.get(&HeaderKey::TRANSFER_ENCODING) {
|
||||||
|
if transfer_encoding != &HeaderValue::new(vec!["chunked".to_string()]) {
|
||||||
|
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||||
|
}
|
||||||
|
sh.has_exhausted_body = false;
|
||||||
|
return Ok((res, Some(ExpectedBody::Chunked(Incoming::<ChunkedIn<_>>::new(
|
||||||
|
ChunkedIn::new(&mut sh.inner), &mut sh.has_exhausted_body
|
||||||
|
)))));
|
||||||
|
}
|
||||||
|
if let Some(content_length) = res.headers.get(&HeaderKey::CONTENT_LENGTH) {
|
||||||
|
let Ok(content_length): Result<usize, _> = content_length.get().parse() else {
|
||||||
|
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||||
|
};
|
||||||
|
sh.has_exhausted_body = false;
|
||||||
|
return Ok((res, Some(ExpectedBody::Sized(Incoming::<SizedIn<_>>::new(
|
||||||
|
SizedIn::new(&mut sh.inner, content_length), &mut sh.has_exhausted_body
|
||||||
|
)))));
|
||||||
|
}
|
||||||
|
Ok((res, None))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn construct_request_body<T: Receive>(
|
||||||
|
sh: &mut StreamHandler<T>,
|
||||||
|
req: RequestHead,
|
||||||
|
) -> Result<FullRequest<T>, StreamHandlerReceiveError<BadRequest>> {
|
||||||
|
if let Some(transfer_encoding) = req.headers.get(&HeaderKey::TRANSFER_ENCODING) {
|
||||||
|
if transfer_encoding != &HeaderValue::new(vec!["chunked".to_string()]) {
|
||||||
|
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||||
|
}
|
||||||
|
sh.has_exhausted_body = false;
|
||||||
|
return Ok((req, Some(ExpectedBody::Chunked(Incoming::<ChunkedIn<_>>::new(
|
||||||
|
ChunkedIn::new(&mut sh.inner), &mut sh.has_exhausted_body
|
||||||
|
)))));
|
||||||
|
}
|
||||||
|
if let Some(content_length) = req.headers.get(&HeaderKey::CONTENT_LENGTH) {
|
||||||
|
let Ok(content_length): Result<usize, _> = content_length.get().parse() else {
|
||||||
|
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||||
|
};
|
||||||
|
sh.has_exhausted_body = false;
|
||||||
|
return Ok((req, Some(ExpectedBody::Sized(Incoming::<SizedIn<_>>::new(
|
||||||
|
SizedIn::new(&mut sh.inner, content_length), &mut sh.has_exhausted_body
|
||||||
|
)))));
|
||||||
|
}
|
||||||
|
Ok((req, None))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
fn construct_response_body_async<T: AsyncReceive>(
|
||||||
|
sh: &mut StreamHandler<T>,
|
||||||
|
res: ResponseHead,
|
||||||
|
) -> Result<FullResponse<T>, StreamHandlerReceiveError<BadResponse>> {
|
||||||
|
if let Some(transfer_encoding) = res.headers.get(&HeaderKey::TRANSFER_ENCODING) {
|
||||||
|
if transfer_encoding != &HeaderValue::new(vec!["chunked".to_string()]) {
|
||||||
|
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||||
|
}
|
||||||
|
sh.has_exhausted_body = false;
|
||||||
|
return Ok((res, Some(ExpectedBody::Chunked(Incoming::<ChunkedIn<_>>::new_async(
|
||||||
|
ChunkedIn::new(&mut sh.inner), &mut sh.has_exhausted_body
|
||||||
|
)))));
|
||||||
|
}
|
||||||
|
if let Some(content_length) = res.headers.get(&HeaderKey::CONTENT_LENGTH) {
|
||||||
|
let Ok(content_length): Result<usize, _> = content_length.get().parse() else {
|
||||||
|
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||||
|
};
|
||||||
|
sh.has_exhausted_body = false;
|
||||||
|
return Ok((res, Some(ExpectedBody::Sized(Incoming::<SizedIn<_>>::new_async(
|
||||||
|
SizedIn::new(&mut sh.inner, content_length), &mut sh.has_exhausted_body
|
||||||
|
)))));
|
||||||
|
}
|
||||||
|
Ok((res, None))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
fn construct_request_body_async<T: AsyncReceive>(
|
||||||
|
sh: &mut StreamHandler<T>,
|
||||||
|
req: RequestHead,
|
||||||
|
) -> Result<FullRequest<T>, StreamHandlerReceiveError<BadRequest>> {
|
||||||
|
if let Some(transfer_encoding) = req.headers.get(&HeaderKey::TRANSFER_ENCODING) {
|
||||||
|
if transfer_encoding != &HeaderValue::new(vec!["chunked".to_string()]) {
|
||||||
|
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||||
|
}
|
||||||
|
sh.has_exhausted_body = false;
|
||||||
|
return Ok((req, Some(ExpectedBody::Chunked(Incoming::<ChunkedIn<_>>::new_async(
|
||||||
|
ChunkedIn::new(&mut sh.inner), &mut sh.has_exhausted_body
|
||||||
|
)))));
|
||||||
|
}
|
||||||
|
if let Some(content_length) = req.headers.get(&HeaderKey::CONTENT_LENGTH) {
|
||||||
|
let Ok(content_length): Result<usize, _> = content_length.get().parse() else {
|
||||||
|
return Err(StreamHandlerReceiveError::InvalidExpectedBody);
|
||||||
|
};
|
||||||
|
sh.has_exhausted_body = false;
|
||||||
|
return Ok((req, Some(ExpectedBody::Sized(Incoming::<SizedIn<_>>::new_async(
|
||||||
|
SizedIn::new(&mut sh.inner, content_length), &mut sh.has_exhausted_body
|
||||||
|
)))));
|
||||||
|
}
|
||||||
|
Ok((req, None))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Receive> StreamHandler<T> {
|
||||||
|
pub(super) fn receive_request(&mut self)
|
||||||
|
-> Result<FullRequest<T>, StreamHandlerReceiveError<BadRequest>> {
|
||||||
|
if !self.has_exhausted_body {
|
||||||
|
return Err(StreamHandlerReceiveError::RequiresBodyPolling);
|
||||||
|
}
|
||||||
|
let mut buf = [0_u8; BUF_SIZE_HEAD];
|
||||||
|
let received = {
|
||||||
|
let mut reader = SyncReader::new(&mut self.inner);
|
||||||
|
reader.recv_until(b"\r\n\r\n", &[0, 0, 0, 1], &mut buf)
|
||||||
|
};
|
||||||
|
let header = match received {
|
||||||
|
Ok(ReaderValue::ExactRead { up_to_delimiter: h }) => h,
|
||||||
|
Ok(ReaderValue::LeakyRead { up_to_delimiter: h, rest: b }) => leaky!(self.inner, h, b),
|
||||||
|
Err(ReaderError::IO(e))=>return Err(StreamHandlerReceiveError::IO(e)),
|
||||||
|
Err(ReaderError::NoData)=>return Err(StreamHandlerReceiveError::NoData),
|
||||||
|
Err(ReaderError::BufferOverflow)=>return Err(StreamHandlerReceiveError::HeaderTooLarge),
|
||||||
|
}.len();
|
||||||
|
let header = &buf[..header+4];
|
||||||
|
let header = parse_request_head(header)?;
|
||||||
|
construct_request_body(self, header)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn receive_response(&mut self)
|
||||||
|
-> Result<FullResponse<T>, StreamHandlerReceiveError<BadResponse>> {
|
||||||
|
if !self.has_exhausted_body {
|
||||||
|
return Err(StreamHandlerReceiveError::RequiresBodyPolling);
|
||||||
|
}
|
||||||
|
let mut buf = [0_u8; BUF_SIZE_HEAD];
|
||||||
|
let received = {
|
||||||
|
let mut reader = SyncReader::new(&mut self.inner);
|
||||||
|
reader.recv_until(b"\r\n\r\n", &[0, 0, 0, 1], &mut buf)
|
||||||
|
};
|
||||||
|
let header = match received {
|
||||||
|
Ok(ReaderValue::ExactRead { up_to_delimiter: h }) => h,
|
||||||
|
Ok(ReaderValue::LeakyRead { up_to_delimiter: h, rest: b }) => leaky!(self.inner, h, b),
|
||||||
|
Err(ReaderError::IO(e))=>return Err(StreamHandlerReceiveError::IO(e)),
|
||||||
|
Err(ReaderError::NoData)=>return Err(StreamHandlerReceiveError::NoData),
|
||||||
|
Err(ReaderError::BufferOverflow)=>return Err(StreamHandlerReceiveError::HeaderTooLarge),
|
||||||
|
}.len();
|
||||||
|
let header = &buf[..header+4];
|
||||||
|
let header = parse_response_head(header)?;
|
||||||
|
construct_response_body(self, header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Send> StreamHandler<T> {
|
||||||
|
pub(super) fn send_request(&mut self, req: &RequestHead) -> Result<(), StreamHandlerSendError> {
|
||||||
|
if !self.has_exhausted_body {
|
||||||
|
return Err(StreamHandlerSendError::RequiresBodyPolling);
|
||||||
|
}
|
||||||
|
let serialized = req.serialize();
|
||||||
|
let serialized = serialized.as_slice();
|
||||||
|
let mut ptr = 0;
|
||||||
|
while ptr < serialized.len() {
|
||||||
|
ptr += self.inner.send(&serialized[ptr..])?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn send_response(
|
||||||
|
&mut self, res: &ResponseHead
|
||||||
|
) -> Result<(), StreamHandlerSendError> {
|
||||||
|
if !self.has_exhausted_body {
|
||||||
|
return Err(StreamHandlerSendError::RequiresBodyPolling);
|
||||||
|
}
|
||||||
|
let serialized = res.serialize();
|
||||||
|
let serialized = serialized.as_slice();
|
||||||
|
let mut ptr = 0;
|
||||||
|
while ptr < serialized.len() {
|
||||||
|
ptr += self.inner.send(&serialized[ptr..])?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
impl<T: AsyncReceive> StreamHandler<T> {
|
||||||
|
pub(super) async fn receive_request_async(&mut self)
|
||||||
|
-> Result<FullRequest<T>, StreamHandlerReceiveError<BadRequest>> {
|
||||||
|
if !self.has_exhausted_body {
|
||||||
|
return Err(StreamHandlerReceiveError::RequiresBodyPolling);
|
||||||
|
}
|
||||||
|
let mut buf = [0_u8; BUF_SIZE_HEAD];
|
||||||
|
let received = {
|
||||||
|
let mut reader = AsyncReader::new(&mut self.inner);
|
||||||
|
reader.recv_until(b"\r\n\r\n", &[0, 0, 0, 1], &mut buf).await
|
||||||
|
};
|
||||||
|
let header = match received {
|
||||||
|
Ok(ReaderValue::ExactRead { up_to_delimiter: h }) => h,
|
||||||
|
Ok(ReaderValue::LeakyRead { up_to_delimiter: h, rest: b }) => leaky!(self.inner, h, b),
|
||||||
|
Err(ReaderError::IO(e))=>return Err(StreamHandlerReceiveError::IO(e)),
|
||||||
|
Err(ReaderError::NoData)=>return Err(StreamHandlerReceiveError::NoData),
|
||||||
|
Err(ReaderError::BufferOverflow)=>return Err(StreamHandlerReceiveError::HeaderTooLarge),
|
||||||
|
}.len();
|
||||||
|
let header = &buf[..header+4];
|
||||||
|
let header = parse_request_head(header)?;
|
||||||
|
construct_request_body_async(self, header)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn receive_response_async(&mut self)
|
||||||
|
-> Result<FullResponse<T>, StreamHandlerReceiveError<BadResponse>> {
|
||||||
|
if !self.has_exhausted_body {
|
||||||
|
return Err(StreamHandlerReceiveError::RequiresBodyPolling);
|
||||||
|
}
|
||||||
|
let mut buf = [0_u8; BUF_SIZE_HEAD];
|
||||||
|
let received = {
|
||||||
|
let mut reader = AsyncReader::new(&mut self.inner);
|
||||||
|
reader.recv_until(b"\r\n\r\n", &[0, 0, 0, 1], &mut buf).await
|
||||||
|
};
|
||||||
|
let header = match received {
|
||||||
|
Ok(ReaderValue::ExactRead { up_to_delimiter: h }) => h,
|
||||||
|
Ok(ReaderValue::LeakyRead { up_to_delimiter: h, rest: b }) => leaky!(self.inner, h, b),
|
||||||
|
Err(ReaderError::IO(e))=>return Err(StreamHandlerReceiveError::IO(e)),
|
||||||
|
Err(ReaderError::NoData)=>return Err(StreamHandlerReceiveError::NoData),
|
||||||
|
Err(ReaderError::BufferOverflow)=>return Err(StreamHandlerReceiveError::HeaderTooLarge),
|
||||||
|
}.len();
|
||||||
|
let header = &buf[..header+4];
|
||||||
|
let header = parse_response_head(header)?;
|
||||||
|
construct_response_body_async(self, header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
impl<T: AsyncSend> StreamHandler<T> {
|
||||||
|
pub(super) async fn send_request_async(
|
||||||
|
&mut self, req: &RequestHead
|
||||||
|
) -> Result<(), StreamHandlerSendError> {
|
||||||
|
if !self.has_exhausted_body {
|
||||||
|
return Err(StreamHandlerSendError::RequiresBodyPolling);
|
||||||
|
}
|
||||||
|
let serialized = req.serialize();
|
||||||
|
let serialized = serialized.as_slice();
|
||||||
|
let mut ptr = 0;
|
||||||
|
while ptr < serialized.len() {
|
||||||
|
ptr += self.inner.send(&serialized[ptr..]).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn send_response_async(
|
||||||
|
&mut self, res: &ResponseHead
|
||||||
|
) -> Result<(), StreamHandlerSendError> {
|
||||||
|
if !self.has_exhausted_body {
|
||||||
|
return Err(StreamHandlerSendError::RequiresBodyPolling);
|
||||||
|
}
|
||||||
|
let serialized = res.serialize();
|
||||||
|
let serialized = serialized.as_slice();
|
||||||
|
let mut ptr = 0;
|
||||||
|
while ptr < serialized.len() {
|
||||||
|
ptr += self.inner.send(&serialized[ptr..]).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod receive {
|
||||||
|
use crate::{
|
||||||
|
io::TestSyncStream,
|
||||||
|
method::Method,
|
||||||
|
path::HttpPath,
|
||||||
|
headers::{HeaderKey, HeaderValue},
|
||||||
|
proto::h1::head::{RequestHead, ResponseHead, ProtocolVariant},
|
||||||
|
status::Status,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "async")]
|
||||||
|
use crate::io::TestAsyncStream;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use super::{StreamHandler, ExpectedBody};
|
||||||
|
|
||||||
|
macro_rules! test_inner {
|
||||||
|
(
|
||||||
|
@request_head $method: ident,
|
||||||
|
$path: literal,
|
||||||
|
$proto: ident, [$($qk: ident : $qv: literal),*], [$($hk: ident : $hv: literal),*]
|
||||||
|
) => {
|
||||||
|
RequestHead {
|
||||||
|
method: Method::$method,
|
||||||
|
path: test_inner!(@http_path $path, [$($qk : $qv),*]),
|
||||||
|
protocol: ProtocolVariant::$proto,
|
||||||
|
headers: test_inner!(@headers [$($hk : $hv),*]),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(
|
||||||
|
@response_head
|
||||||
|
$proto: ident, $status: ident, [$($hk: ident : $hv: literal),*]
|
||||||
|
) => {
|
||||||
|
ResponseHead {
|
||||||
|
status: Status::$status,
|
||||||
|
protocol: ProtocolVariant::$proto,
|
||||||
|
headers: test_inner!(@headers [$($hk : $hv),*]),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(@http_path $path: literal, [$($hk: ident : $hv: literal),+]) => {
|
||||||
|
HttpPath { path: $path.to_string(), params: Some(HashMap::from([$(($hk, $hv)),+])) }
|
||||||
|
};
|
||||||
|
(@http_path $path: literal, []) => {
|
||||||
|
HttpPath { path: $path.to_string(), params: None }
|
||||||
|
};
|
||||||
|
|
||||||
|
(@headers [$($hk: ident : $hv :literal),*]) => {
|
||||||
|
HashMap::from([$((HeaderKey::$hk, HeaderValue::new(Vec::from([$hv.to_string()])))),*])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! test {
|
||||||
|
(
|
||||||
|
$name: ident $name_async: ident,
|
||||||
|
req $src: literal,
|
||||||
|
$method: ident $path: literal [$($qk: ident : $qv: literal),*] $proto: ident,
|
||||||
|
[$($hk: ident : $hv: literal),*$(,)?]
|
||||||
|
) => {
|
||||||
|
test!(@inner $name, $name_async, req $src, test_inner!(
|
||||||
|
@request_head $method, $path, $proto, [$($qk:$qv),*], [$($hk:$hv),*]
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
$name: ident $name_async: ident,
|
||||||
|
req $src: literal,
|
||||||
|
$method: ident $path: literal [$($qk: ident : $qv: literal),*] $proto: ident,
|
||||||
|
[$($hk: ident : $hv: literal),*$(,)?],
|
||||||
|
$body: literal
|
||||||
|
) => {
|
||||||
|
test!(@inner $name, $name_async, req $src, test_inner!(
|
||||||
|
@request_head $method, $path, $proto, [$($qk:$qv),*], [$($hk:$hv),*]
|
||||||
|
), $body);
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
$name: ident $name_async: ident,
|
||||||
|
res $src: literal,
|
||||||
|
$proto: ident $status: ident,
|
||||||
|
[$($hk: ident : $hv: literal),*$(,)?]
|
||||||
|
) => {
|
||||||
|
test!(@inner $name, $name_async, res $src, test_inner!(
|
||||||
|
@response_head $proto, $status, [$($hk:$hv),*]
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
$name: ident $name_async: ident,
|
||||||
|
res $src: literal,
|
||||||
|
$proto: ident $status: ident,
|
||||||
|
[$($hk: ident : $hv: literal),*$(,)?],
|
||||||
|
$body: literal
|
||||||
|
) => {
|
||||||
|
test!(@inner $name, $name_async, res $src, test_inner!(
|
||||||
|
@response_head $proto, $status, [$($hk:$hv),*]
|
||||||
|
), $body);
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
@inner $name: ident, $name_async: ident, req $src: literal, $res_head: expr
|
||||||
|
) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let mut src = $src.into();
|
||||||
|
let stream = TestSyncStream::<4>::new(&mut src);
|
||||||
|
let mut handler = StreamHandler::new(stream);
|
||||||
|
let (head, body) = handler.receive_request().unwrap();
|
||||||
|
assert_eq!(head, $res_head);
|
||||||
|
assert!(body.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "async", any(feature = "tokio-net", feature = "tokio-unixsocks")))]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn $name_async() {
|
||||||
|
let mut src = $src.into();
|
||||||
|
let stream = TestAsyncStream::<4>::new(&mut src);
|
||||||
|
let mut handler = StreamHandler::new(stream);
|
||||||
|
let (head, body) = handler.receive_request_async().await.unwrap();
|
||||||
|
assert_eq!(head, $res_head);
|
||||||
|
assert!(body.is_none());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(
|
||||||
|
@inner $name: ident, $name_async: ident, req $src: literal, $res_head: expr, $res_body: expr
|
||||||
|
) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let mut src = $src.into();
|
||||||
|
let stream = TestSyncStream::<4>::new(&mut src);
|
||||||
|
let mut handler = StreamHandler::new(stream);
|
||||||
|
let (head, body) = handler.receive_request().unwrap();
|
||||||
|
assert_eq!(head, $res_head);
|
||||||
|
let Some(ExpectedBody::Sized(mut body)) = body else { panic!("body mismatch"); };
|
||||||
|
assert_eq!(body.recv_all().unwrap(), $res_body.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "async", any(feature = "tokio-net", feature = "tokio-unixsocks")))]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn $name_async() {
|
||||||
|
let mut src = $src.into();
|
||||||
|
let stream = TestAsyncStream::<4>::new(&mut src);
|
||||||
|
let mut handler = StreamHandler::new(stream);
|
||||||
|
let (head, body) = handler.receive_request_async().await.unwrap();
|
||||||
|
assert_eq!(head, $res_head);
|
||||||
|
let Some(ExpectedBody::Sized(mut body)) = body else { panic!("body mismatch"); };
|
||||||
|
assert_eq!(body.recv_all_async().await.unwrap(), $res_body.to_vec());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
@inner $name: ident, $name_async: ident, res $src: literal, $res_head: expr
|
||||||
|
) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let mut src = $src.into();
|
||||||
|
let stream = TestSyncStream::<4>::new(&mut src);
|
||||||
|
let mut handler = StreamHandler::new(stream);
|
||||||
|
let (head, body) = handler.receive_response().unwrap();
|
||||||
|
assert_eq!(head, $res_head);
|
||||||
|
assert!(body.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "async", any(feature = "tokio-net", feature = "tokio-unixsocks")))]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn $name_async() {
|
||||||
|
let mut src = $src.into();
|
||||||
|
let stream = TestAsyncStream::<4>::new(&mut src);
|
||||||
|
let mut handler = StreamHandler::new(stream);
|
||||||
|
let (head, body) = handler.receive_response_async().await.unwrap();
|
||||||
|
assert_eq!(head, $res_head);
|
||||||
|
assert!(body.is_none());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(
|
||||||
|
@inner $name: ident, $name_async: ident, res $src: literal, $res_head: expr, $res_body: expr
|
||||||
|
) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let mut src = $src.into();
|
||||||
|
let stream = TestSyncStream::<4>::new(&mut src);
|
||||||
|
let mut handler = StreamHandler::new(stream);
|
||||||
|
let (head, body) = handler.receive_response().unwrap();
|
||||||
|
assert_eq!(head, $res_head);
|
||||||
|
let Some(ExpectedBody::Sized(mut body)) = body else { panic!("body mismatch"); };
|
||||||
|
assert_eq!(body.recv_all().unwrap(), $res_body.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "async", any(feature = "tokio-net", feature = "tokio-unixsocks")))]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn $name_async() {
|
||||||
|
let mut src = $src.into();
|
||||||
|
let stream = TestAsyncStream::<4>::new(&mut src);
|
||||||
|
let mut handler = StreamHandler::new(stream);
|
||||||
|
let (head, body) = handler.receive_response_async().await.unwrap();
|
||||||
|
assert_eq!(head, $res_head);
|
||||||
|
let Some(ExpectedBody::Sized(mut body)) = body else { panic!("body mismatch"); };
|
||||||
|
assert_eq!(body.recv_all_async().await.unwrap(), $res_body.to_vec());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test!(
|
||||||
|
request_valid_no_body async_request_valid_no_body,
|
||||||
|
req "GET / HTTP/1.1\r\nserver: inferium\r\n\r\n",
|
||||||
|
GET "/"[] HTTP1_1,
|
||||||
|
[
|
||||||
|
SERVER: "inferium",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(
|
||||||
|
request_valid_body async_request_valid_body,
|
||||||
|
req "GET / HTTP/1.1\r\ncontent-length: 4\r\n\r\ntest",
|
||||||
|
GET "/"[] HTTP1_1,
|
||||||
|
[
|
||||||
|
CONTENT_LENGTH: "4",
|
||||||
|
],
|
||||||
|
b"test"
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(
|
||||||
|
response_valid_no_body async_response_valid_no_body,
|
||||||
|
res "HTTP/1.0 200 OK\r\nserver: inferium\r\n\r\n",
|
||||||
|
HTTP1_0 Ok,
|
||||||
|
[
|
||||||
|
SERVER: "inferium",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(
|
||||||
|
response_valid_body async_response_valid_body,
|
||||||
|
res "HTTP/1.1 200 OK\r\nserver: inferium\r\nconnection: close\r\ncontent-length: 4\r\n\r\n\
|
||||||
|
test",
|
||||||
|
HTTP1_1 Ok,
|
||||||
|
[
|
||||||
|
SERVER: "inferium",
|
||||||
|
CONNECTION: "close",
|
||||||
|
CONTENT_LENGTH: "4",
|
||||||
|
],
|
||||||
|
b"test"
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(
|
||||||
|
response_short_body async_response_short_body,
|
||||||
|
res "HTTP/1.1 200 OK\r\nserver: inferium\r\nconnection: close\r\ncontent-length: 4\r\n\r\n\
|
||||||
|
tes",
|
||||||
|
HTTP1_1 Ok,
|
||||||
|
[
|
||||||
|
SERVER: "inferium",
|
||||||
|
CONNECTION: "close",
|
||||||
|
CONTENT_LENGTH: "4",
|
||||||
|
],
|
||||||
|
b"tes"
|
||||||
|
);
|
||||||
|
|
||||||
|
test!(
|
||||||
|
response_long_body async_response_long_body,
|
||||||
|
res "HTTP/1.1 200 OK\r\nserver: inferium\r\nconnection: close\r\ncontent-length: 4\r\n\r\n\
|
||||||
|
testing",
|
||||||
|
HTTP1_1 Ok,
|
||||||
|
[
|
||||||
|
SERVER: "inferium",
|
||||||
|
CONNECTION: "close",
|
||||||
|
CONTENT_LENGTH: "4",
|
||||||
|
],
|
||||||
|
b"test"
|
||||||
|
);
|
||||||
|
}
|
1
lib/inferium/src/proto/mod.rs
Normal file
1
lib/inferium/src/proto/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod h1;
|
2
lib/inferium/src/settings.rs
Normal file
2
lib/inferium/src/settings.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub const BUF_SIZE_HEAD: usize = 8192;
|
||||||
|
pub const BUF_SIZE_BODY: usize = 4096;
|
121
lib/inferium/src/status.rs
Normal file
121
lib/inferium/src/status.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
macro_rules! http_status {
|
||||||
|
(
|
||||||
|
$(#$objdoc: tt)+
|
||||||
|
$($ident: ident = ($number: literal, $text: literal)),*$(,)?
|
||||||
|
) => {
|
||||||
|
$(#$objdoc)+
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum Status {
|
||||||
|
$($ident),*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&[u8]> for Status {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
match value { $($number => Ok(Self::$ident),)* _ => Err(()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Status {
|
||||||
|
pub fn num(&self) -> &[u8] {
|
||||||
|
match self { $(Self::$ident => $number),* }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text(&self) -> &[u8] {
|
||||||
|
match self { $(Self::$ident => $text),* }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Status {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
|
match self { $(Self::$ident => write_status(self, f)?),* }
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_status(this: &Status, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
|
let num = unsafe { std::str::from_utf8_unchecked(this.num()) };
|
||||||
|
let text = unsafe { std::str::from_utf8_unchecked(this.text()) };
|
||||||
|
write!(f, "{num} {text}")
|
||||||
|
}
|
||||||
|
|
||||||
|
http_status! {
|
||||||
|
/// HTTP response status codes and their names
|
||||||
|
///
|
||||||
|
/// Numbers and names are represented in byte arrays for faster response parsing and
|
||||||
|
/// construction.
|
||||||
|
|
||||||
|
// Informational
|
||||||
|
Continue = (b"100", b"Continue"),
|
||||||
|
SwitchingProtocols = (b"101", b"Switching Protocols"),
|
||||||
|
Processing = (b"102", b"Processing"),
|
||||||
|
EarlyHints = (b"103", b"Early Hints"),
|
||||||
|
|
||||||
|
// Successful
|
||||||
|
Ok = (b"200", b"OK"),
|
||||||
|
Created = (b"201", b"Created"),
|
||||||
|
Accepted = (b"202", b"Accepted"),
|
||||||
|
NonAuthoritativeInformation = (b"203", b"Non-Authoritative Information"),
|
||||||
|
NoContent = (b"204", b"No Content"),
|
||||||
|
ResetContent = (b"205", b"Reset Content"),
|
||||||
|
PartialContent = (b"206", b"Partial Content"),
|
||||||
|
MultiStatus = (b"207", b"Multi-Status"),
|
||||||
|
AlreadyReported = (b"208", b"AlreadyReported"),
|
||||||
|
ImUsed = (b"226", b"IM Used"),
|
||||||
|
|
||||||
|
// Redirection
|
||||||
|
MultipleChoices = (b"300", b"Multiple Choices"),
|
||||||
|
MovedPermanently = (b"301", b"Moved Permanently"),
|
||||||
|
Found = (b"302", b"Found"),
|
||||||
|
SeeOther = (b"303", b"See Other"),
|
||||||
|
NotModified = (b"304", b"Not Modified"),
|
||||||
|
TemporaryRedirect = (b"307", b"Temporary Redirect"),
|
||||||
|
PermanentRedirect = (b"308", b"Permanent Redirect"),
|
||||||
|
|
||||||
|
// Client errors
|
||||||
|
BadRequest = (b"400", b"Bad Request"),
|
||||||
|
Unauthorized = (b"401", b"Unauthorized"),
|
||||||
|
PaymentRequired = (b"402", b"Payment Required"),
|
||||||
|
Forbidden = (b"403", b"Forbidden"),
|
||||||
|
NotFound = (b"404", b"Not Found"),
|
||||||
|
MethodNotAllowed = (b"405", b"MethodNotAllowed"),
|
||||||
|
NotAcceptable = (b"406", b"Not Acceptable"),
|
||||||
|
ProxyAuthenticationRequired = (b"407", b"Proxy Authentication Required"),
|
||||||
|
RequestTimeout = (b"408", b"Request Timeout"),
|
||||||
|
Conflict = (b"409", b"Conflict"),
|
||||||
|
Gone = (b"410", b"Gone"),
|
||||||
|
LengthRequired = (b"411", b"Length Required"),
|
||||||
|
PreconditionFailed = (b"412", b"Precondition Failed"),
|
||||||
|
ContentTooLarge = (b"413", b"Content Too Large"),
|
||||||
|
UriTooLong = (b"414", b"URI Too Long"),
|
||||||
|
UnsupportedMediaType = (b"415", b"Unsupported Media Type"),
|
||||||
|
RangeNotSatisfiable = (b"416", b"Range Not Satisfiable"),
|
||||||
|
ExpectationFailed = (b"417", b"Expectation Failed"),
|
||||||
|
ImATeapot = (b"418", b"I'm a teapot"),
|
||||||
|
MisdirectedRequest = (b"421", b"Misdirected Request"),
|
||||||
|
UnprocessableContent = (b"422", b"Unprocessable Content"),
|
||||||
|
Locked = (b"423", b"Locked"),
|
||||||
|
FailedDependency = (b"424", b"Failed Dependency"),
|
||||||
|
TooEarly = (b"425", b"Too Early"),
|
||||||
|
UpgradeRequired = (b"426", b"Upgrade Required"),
|
||||||
|
PreconditionRequired = (b"428", b"Precondition Required"),
|
||||||
|
TooManyRequests = (b"429", b"Too Many Requests"),
|
||||||
|
RequestHeaderFieldsTooLarge = (b"431", b"Request Header Fields Too Large"),
|
||||||
|
UnavailableForLegalReasons = (b"451", b"Unavailable For Legal Reasons"),
|
||||||
|
|
||||||
|
// Server errors
|
||||||
|
InternalServerError = (b"500", b"Internal Server Error"),
|
||||||
|
NotImplemented = (b"501", b"Not Implemented"),
|
||||||
|
BadGateway = (b"502", b"Bad Gateway"),
|
||||||
|
ServiceUnavailable = (b"503", b"Service Unavailable"),
|
||||||
|
GatewayTimeout = (b"504", b"Gateway Timeout"),
|
||||||
|
HttpVersionNotSupported = (b"505", b"HTTP Version Not Supported"),
|
||||||
|
VariantAlsoNegotiates = (b"506", b"Variant Also Negotiates"),
|
||||||
|
InsufficientStorage = (b"507", b"Insufficient Storage"),
|
||||||
|
LoopDetected = (b"508", b"Loop Detected"),
|
||||||
|
NotExtended = (b"510", b"Not Extended"),
|
||||||
|
NetworkAuthenticationRequired = (b"511", b"Network Authentication Required"),
|
||||||
|
}
|
Reference in New Issue
Block a user