Archived
3
0

initial commit

This commit is contained in:
2025-03-04 11:22:53 +01:00
commit 5bb655b7de
32 changed files with 6229 additions and 0 deletions

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# Zprávy zumepro
## Návrh systému
![diagram návrhu sysému](./docs/exports/system_arch.svg)

1
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dst/

22
docs/Makefile Normal file
View 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

View 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
View 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
View File

@@ -0,0 +1,8 @@
/target
# Added by cargo
#
# already existing elements were commented out
#/target

746
lib/inferium/Cargo.lock generated Normal file
View 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
View 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
View File

@@ -0,0 +1,3 @@
# inferium
A small HTTP library written in Rust

View 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();
});
}

View 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());
})
}

View 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())
}

View 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()
);
}
}

View 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())
}

View 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
View File

@@ -0,0 +1 @@
target/

7
lib/inferium/proc/Cargo.lock generated Normal file
View 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"

View File

@@ -0,0 +1,9 @@
[package]
name = "proc"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]

View 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

File diff suppressed because it is too large Load Diff

200
lib/inferium/src/headers.rs Normal file
View 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

File diff suppressed because it is too large Load Diff

32
lib/inferium/src/lib.rs Normal file
View 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};

View 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
View 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");
}

View 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)),
})
}
}

View 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");
}

View 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;

View 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"
);
}

View File

@@ -0,0 +1 @@
pub mod h1;

View 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
View 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"),
}