diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index fec17ad..2d68626 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,12 +4,15 @@ on: [push] jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v1 + - name: Build + run: cargo build --verbose + - name: Build with Rusttls + run: cargo build --verbose --no-default-features --features rust-tls + - name: Run tests + run: cargo test --verbose + - name: Run tests with rust-tls + run: cargo test --verbose --no-default-features --features rust-tls diff --git a/.gitignore b/.gitignore index b309da6..a008342 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /target **/*.rs.bk -/.idea \ No newline at end of file +.DS_store \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fa1b5d1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: rust -rust: - - stable - - beta - - nightly -matrix: - allow_failures: - - rust: nightly -os: - - linux - - osx -script: - - cargo test - - cargo test --features rust-tls --no-default-features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58b6320..0935ec8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,4 @@ # Contributing -Code and documentation PRs are always welcome. Just remember to list out introduced changes. +Code contributions and documentation improvements are always welcome! Please include a clear description of the changes introduced in your pull requests. -If you want to suggest some improvements, report a bug, discuss a functionality, etc., -feel free to open an issue. +For suggestions, bug reports, or feature requests, please open an issue. diff --git a/Cargo.lock b/Cargo.lock index 20f0585..ee802d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,52 +1,123 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aws-lc-rs" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5932a7d9d28b0d2ea34c6b3779d35e3dd6f6345317c34e73438c4f1f29144151" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + [[package]] -name = "autocfg" -version = "1.0.1" +name = "aws-lc-sys" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "1826f2e4cfc2cd19ee53c42fbf68e2f81ec21108e0b7ecf6a71cf062137360fc" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] [[package]] name = "base64" -version = "0.13.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bitflags" -version = "1.2.1" +name = "bindgen" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] [[package]] -name = "bumpalo" -version = "3.4.0" +name = "bitflags" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "cc" -version = "1.0.66" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] [[package]] -name = "cfg-if" -version = "0.1.10" +name = "cexpr" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[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 = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] [[package]] name = "core-foundation" -version = "0.9.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -54,9 +125,43 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.2" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "foreign-types" @@ -73,65 +178,121 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[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.1.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "libc", "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + [[package]] name = "http_req" -version = "0.7.2" +version = "0.14.2" dependencies = [ + "base64", "native-tls", "rustls", + "rustls-pemfile", + "rustls-pki-types", "unicase", "webpki", "webpki-roots", + "zeroize", ] [[package]] -name = "js-sys" -version = "0.3.46" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ - "wasm-bindgen", + "either", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "jobserver" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] [[package]] name = "libc" -version = "0.2.81" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] -name = "log" -version = "0.4.11" +name = "libloading" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", + "windows-link", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "native-tls" -version = "0.2.6" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcc7939b5edc4e4f86b1b4a04bb1498afaaf871b1a6691838ed06fcb48d3a3f" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -143,39 +304,60 @@ dependencies = [ "tempfile", ] +[[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 = "once_cell" -version = "1.5.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" -version = "0.10.31" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d008f51b1acffa0d3450a68606e6a51c123012edaacb0f4e1426bd978869187" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ "bitflags", - "cfg-if 1.0.0", + "cfg-if", "foreign-types", - "lazy_static", "libc", + "once_cell", + "openssl-macros", "openssl-sys", ] +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "openssl-probe" -version = "0.1.2" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.59" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de52d8eabd217311538a39bba130d7dea1f1e118010fee7a033d966845e7d5fe" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ - "autocfg", "cc", "libc", "pkg-config", @@ -184,143 +366,165 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] -name = "ppv-lite86" -version = "0.2.10" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.7" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] -name = "rand" -version = "0.7.3" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" -dependencies = [ - "getrandom", - "libc", - "rand_chacha", - "rand_core", - "rand_hc", -] +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand_chacha" -version = "0.2.2" +name = "regex" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ - "ppv-lite86", - "rand_core", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] -name = "rand_core" -version = "0.5.1" +name = "regex-automata" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ - "getrandom", + "aho-corasick", + "memchr", + "regex-syntax", ] [[package]] -name = "rand_hc" -version = "0.2.0" +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ - "rand_core", + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", ] [[package]] -name = "redox_syscall" -version = "0.1.57" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "rustix" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "winapi", + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] -name = "ring" -version = "0.16.19" +name = "rustls" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ - "cc", - "libc", + "aws-lc-rs", + "log", "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] -name = "rustls" -version = "0.19.0" +name = "rustls-pemfile" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64", - "log", - "ring", - "sct", - "webpki", + "rustls-pki-types", ] [[package]] -name = "schannel" -version = "0.1.19" +name = "rustls-pki-types" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ - "lazy_static", - "winapi", + "zeroize", ] [[package]] -name = "sct" -version = "0.6.0" +name = "rustls-webpki" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ + "aws-lc-rs", "ring", + "rustls-pki-types", "untrusted", ] +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "security-framework" -version = "2.0.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1759c2e3c8580017a484a7ac56d3abc5a6c1feadf88db2f3633f12ae4268c69" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", "core-foundation", @@ -331,185 +535,218 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.0.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99b9d5e26d2a71633cc4f2ebae7cc9f874044e0c351a27e17892d76dce5678b" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", ] [[package]] -name = "spin" -version = "0.5.2" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "1.0.54" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2af957a63d6bd42255c359c93d9bfdb97076bd3b820897ce55ffbfbf107f44" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] name = "tempfile" -version = "3.1.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "cfg-if 0.1.10", - "libc", - "rand", - "redox_syscall", - "remove_dir_all", - "winapi", + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] name = "unicase" -version = "2.6.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check", -] +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] -name = "unicode-xid" -version = "0.2.1" +name = "unicode-ident" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "vcpkg" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" - -[[package]] -name = "version_check" -version = "0.9.2" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasm-bindgen" -version = "0.2.69" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "cfg-if 1.0.0", - "wasm-bindgen-macro", + "wit-bindgen", ] [[package]] -name = "wasm-bindgen-backend" -version = "0.2.69" +name = "webpki" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", + "ring", + "untrusted", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.69" +name = "webpki-roots" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ - "quote", - "wasm-bindgen-macro-support", + "rustls-pki-types", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.69" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "wasm-bindgen-shared" -version = "0.2.69" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] [[package]] -name = "web-sys" -version = "0.3.46" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "js-sys", - "wasm-bindgen", + "windows-link", ] [[package]] -name = "webpki" -version = "0.21.4" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "ring", - "untrusted", + "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 = "webpki-roots" -version = "0.21.0" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376" -dependencies = [ - "webpki", -] +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "winapi" -version = "0.3.9" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] +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 = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index dfa43e1..0ef231f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http_req" -version = "0.7.2" +version = "0.14.2" license = "MIT" description = "simple and lightweight HTTP client with built-in HTTPS support" repository = "https://github.com/jayjamesjay/http_req" @@ -8,30 +8,27 @@ authors = ["jayjamesjay"] readme = "README.md" categories = ["web-programming::http-client", "network-programming"] keywords = ["http", "client", "request"] -edition = "2018" +edition = "2021" [dependencies] -unicase = "^2.6" +unicase = "^2.8" +base64 = { version = "^0.22", optional = true } +zeroize = { version = "^1.8", features = ["zeroize_derive"], optional = true } +native-tls = { version = "^0.2", optional = true } +rustls = { version = "^0.23", optional = true } +rustls-pemfile = { version = "^2.2", optional = true } +rustls-pki-types = { version = "^1.13", features = ["alloc"], optional = true } +webpki = { version = "^0.22", optional = true } +webpki-roots = { version = "^1.0", optional = true } [features] -default = ["native-tls"] -rust-tls = ["rustls", "webpki", "webpki-roots"] - -[dependencies.native-tls] -version = "^0.2" -optional = true - -[dependencies.rustls] -version = "^0.19" -optional = true - -[dependencies.webpki] -version = "^0.21" -optional = true - -[dependencies.webpki-roots] -version = "^0.21" -optional = true - -[badges] -travis-ci = { repository = "jayjamesjay/http_req"} +default = ["native-tls", "auth"] +rust-tls = [ + "rustls", + "rustls-pki-types", + "webpki", + "webpki-roots", + "rustls-pemfile", + "auth", +] +auth = ["base64", "zeroize"] diff --git a/LICENSE b/LICENSE index 65f6868..8411ad3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2019 jayjamesjay +Copyright (c) 2018-2025 jayjamesjay Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7809830..2165a97 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,81 @@ # http_req -[![Build Status](https://travis-ci.org/jayjamesjay/http_req.svg?branch=master)](https://travis-ci.org/jayjamesjay/http_req) -[![Crates.io](https://img.shields.io/badge/crates.io-v0.7.2-orange.svg?longCache=true)](https://crates.io/crates/http_req) -[![Docs.rs](https://docs.rs/http_req/badge.svg)](https://docs.rs/http_req/0.7.2/http_req/) + +[![Rust](https://github.com/jayjamesjay/http_req/actions/workflows/rust.yml/badge.svg)](https://github.com/jayjamesjay/http_req/actions/workflows/rust.yml) +[![Crates.io](https://img.shields.io/badge/crates.io-v0.14.2-orange.svg?longCache=true)](https://crates.io/crates/http_req) +[![Docs.rs](https://docs.rs/http_req/badge.svg)](https://docs.rs/http_req/0.14.2/http_req/) Simple and lightweight HTTP client with built-in HTTPS support. +- HTTP and HTTPS via [rust-native-tls](https://crates.io/crates/native-tls) (or optionally [rustls](https://crates.io/crates/rustls)) +- Small binary size (0.7 MB for a basic GET request in the default configuratio) +- Minimal number of dependencies + ## Requirements -http_req by default uses [rust-native-tls](https://github.com/sfackler/rust-native-tls), -which uses TLS framework provided by OS on Windows and macOS, and OpenSSL -on all other platforms. But it also supports [rus-tls](https://crates.io/crates/rustls). + +http_req by default uses [rust-native-tls](https://crates.io/crates/native-tls), +which relies on TLS framework provided by OS on Windows and macOS, and OpenSSL +on all other platforms. But it also supports [rustls](https://crates.io/crates/rustls). + +## All functionalities + +- Support for both HTTP and HTTPS protocols via [rust-native-tls](https://crates.io/crates/native-tls) (or optionally [rustls](https://crates.io/crates/rustls)) +- Creating and sending HTTP requests using the `Request` type (with extended capabilities provided via `RequestMessage` and `Stream`) +- Representing HTTP responses with the `Response` type, allowing easy access to details like the status code and headers +- Handling redirects using the `RedirectPolicy` +- Support for Basic and Bearer authentication +- Processing responses with `Transfer-Encoding: chunked` +- Managing absolute `Uri`s and partial support for relative `Uri`s +- Enforcing timeouts on requests +- Downloading data in a streaming fashion, allowing direct saving to disk (minimizing RAM usage) +- `Error` handling system allowing for better debugging +- Utility functions for easily sending common request types: `get`, `head`, `post` + +## Usage + +### Default configuration + +In order to use `http_req` with default configuration, add the following lines to `Cargo.toml`: + +```toml +[dependencies] +http_req = "^0.14" +``` + +### Rustls + +In order to use `http_req` with `rustls` in your project, add the following lines to `Cargo.toml`: + +```toml +[dependencies] +http_req = { version="^0.14", default-features = false, features = ["rust-tls"] } +``` + +### HTTP only + +In order to use `http_req` without any additional features in your project (no HTTPS, no Authentication), add the following lines to `Cargo.toml`: + +```toml +[dependencies] +http_req = { version="^0.14", default-features = false } +``` ## Example -Basic GET request + +Basic HTTP GET request + ```rust use http_req::request; fn main() { - let mut writer = Vec::new(); //container for body of a response - let res = request::get("https://doc.rust-lang.org/", &mut writer).unwrap(); + let mut body = Vec::new(); //Container for body of a response. + let res = request::get("https://doc.rust-lang.org/", &mut body).unwrap(); println!("Status: {} {}", res.status_code(), res.reason()); } ``` -## How to use with `rustls`: -In order to use `http_req` with `rustls` in your project, add following lines to `Cargo.toml`: -```toml -[dependencies] -http_req = {version="^0.7", default-features = false, features = ["rust-tls"]} -``` +Take a look at [more examples](https://github.com/jayjamesjay/http_req/tree/master/examples) ## License + Licensed under [MIT](https://github.com/jayjamesjay/http_req/blob/master/LICENSE). diff --git a/benches/bench.rs b/benches/bench.rs index 5b6e411..8ba3b9a 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -2,10 +2,30 @@ extern crate http_req; extern crate test; -use http_req::{request::Request, response::Response, uri::Uri}; -use std::{fs::File, io::Read, time::Duration}; +use http_req::{request::RequestMessage, response::Response, uri::Uri}; +use std::{convert::TryFrom, fs::File, io::Read}; use test::Bencher; +const URI: &str = "https://www.rust-lang.org/"; +const BODY: [u8; 14] = [78, 97, 109, 101, 61, 74, 97, 109, 101, 115, 43, 74, 97, 121]; + +#[bench] +fn parse_uri(b: &mut Bencher) { + b.iter(|| Uri::try_from(URI)); +} + +#[bench] +fn parse_request(b: &mut Bencher) { + let uri = Uri::try_from(URI).unwrap(); + + b.iter(|| { + RequestMessage::new(&uri) + .header("Accept", "*/*") + .body(&BODY) + .parse(); + }); +} + #[bench] fn parse_response(b: &mut Bencher) { let mut content = Vec::new(); @@ -17,26 +37,3 @@ fn parse_response(b: &mut Bencher) { Response::try_from(&content, &mut body) }); } - -const URI: &str = "https://www.rust-lang.org/"; - -#[bench] -fn request_send(b: &mut Bencher) { - b.iter(|| { - let uri = URI.parse::().unwrap(); - let timeout = Some(Duration::from_secs(6)); - let mut writer = Vec::new(); - - let res = Request::new(&uri) - .timeout(timeout) - .send(&mut writer) - .unwrap(); - - res - }); -} - -#[bench] -fn parse_uri(b: &mut Bencher) { - b.iter(|| URI.parse::()); -} diff --git a/examples/advanced_request_get.rs b/examples/advanced_request_get.rs new file mode 100644 index 0000000..6b60897 --- /dev/null +++ b/examples/advanced_request_get.rs @@ -0,0 +1,46 @@ +use http_req::{ + request::RequestMessage, + response::Response, + stream::{self, Stream}, + uri::Uri, +}; +use std::{ + convert::TryFrom, + io::{BufReader, Read, Write}, + time::Duration, +}; + +fn main() { + // Parses a URI and assigns it to a variable `addr`. + let addr: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + + // Containers for a server's response. + let raw_head; + let mut body = Vec::new(); + + // Prepares a request message. + let request_msg = RequestMessage::new(&addr) + .header("Connection", "Close") + .parse(); + + // Connects to a server. Uses information from `addr`. + let mut stream = Stream::connect(&addr, Some(Duration::from_secs(60))).unwrap(); + stream = Stream::try_to_https(stream, &addr, None).unwrap(); + + // Makes a request to server. Sends the prepared message. + stream.write_all(&request_msg).unwrap(); + + // Wraps the stream in BufReader to make it easier to read from it. + // Reads a response from the server and saves the head to `raw_head`, and the body to `body`. + let mut stream = BufReader::new(stream); + raw_head = stream::read_head(&mut stream); + stream.read_to_end(&mut body).unwrap(); + + // Parses and processes the response. + let response = Response::from_head(&raw_head).unwrap(); + + // Prints infromation about the response. + println!("Status: {} {}", response.status_code(), response.reason()); + println!("Headers: {}", response.headers()); + //println!("{}", String::from_utf8_lossy(&body)); +} diff --git a/examples/authentication.rs b/examples/authentication.rs new file mode 100644 index 0000000..c67328b --- /dev/null +++ b/examples/authentication.rs @@ -0,0 +1,24 @@ +use http_req::{ + request::{Authentication, Request}, + uri::Uri, +}; + +fn main() { + // Container for body of a response. + let mut body = Vec::new(); + // URL of the website. + let uri = Uri::try_from("https://httpbin.org/basic-auth/foo/bar").unwrap(); + // Authentication details: username and password. + let auth = Authentication::basic("foo", "bar"); + + // Sends a HTTP GET request and processes the response. Saves body of the response to `body` variable. + let res = Request::new(&uri) + .authentication(auth) + .send(&mut body) + .unwrap(); + + //Prints details about the response. + println!("Status: {} {}", res.status_code(), res.reason()); + println!("Headers: {}", res.headers()); + println!("{}", String::from_utf8_lossy(&body)); +} diff --git a/examples/chunked.rs b/examples/chunked.rs new file mode 100644 index 0000000..8e258b2 --- /dev/null +++ b/examples/chunked.rs @@ -0,0 +1,12 @@ +use http_req::request; + +fn main() { + // Sends a HTTP GET request and processes the response. + let mut body = Vec::new(); + let res = request::get("https://jigsaw.w3.org/HTTP/ChunkedScript", &mut body).unwrap(); + + // Prints details about the response. + println!("Status: {} {}", res.status_code(), res.reason()); + println!("Headers: {}", res.headers()); + //println!("{}", String::from_utf8_lossy(&body)); +} diff --git a/examples/get.rs b/examples/get.rs index 1fbf882..a1d4856 100644 --- a/examples/get.rs +++ b/examples/get.rs @@ -1,10 +1,14 @@ use http_req::request; fn main() { - let mut writer = Vec::new(); //container for body of a response - let res = request::get("https://www.rust-lang.org/learn", &mut writer).unwrap(); + // Container for body of a response. + let mut body = Vec::new(); + // Sends a HTTP GET request and processes the response. Saves body of the response to `body` variable. + let res = request::get("https://www.rust-lang.org/learn", &mut body).unwrap(); + + //Prints details about the response. println!("Status: {} {}", res.status_code(), res.reason()); - println!("Headers {}", res.headers()); - //println!("{}", String::from_utf8_lossy(&writer)); + println!("Headers: {}", res.headers()); + //println!("{}", String::from_utf8_lossy(&body)); } diff --git a/examples/head.rs b/examples/head.rs index eb2f816..c20e80a 100644 --- a/examples/head.rs +++ b/examples/head.rs @@ -1,8 +1,10 @@ use http_req::request; fn main() { + // Sends a HTTP HEAD request and processes the response. let res = request::head("https://www.rust-lang.org/learn").unwrap(); + // Prints the details about the response. println!("Status: {} {}", res.status_code(), res.reason()); - println!("{:?}", res.headers()); + println!("Headers: {}", res.headers()); } diff --git a/examples/post.rs b/examples/post.rs index 982fd32..e1f498b 100644 --- a/examples/post.rs +++ b/examples/post.rs @@ -1,11 +1,17 @@ use http_req::request; fn main() { - let mut writer = Vec::new(); //container for body of a response - const BODY: &[u8; 27] = b"field1=value1&field2=value2"; - let res = request::post("https://httpbin.org/post", BODY, &mut writer).unwrap(); + // Container for body of a response. + let mut res_body = Vec::new(); + // Body of a request. + const REQ_BODY: &[u8; 27] = b"field1=value1&field2=value2"; + + // Sends a HTTP POST request and processes the response. + let res = request::post("https://httpbin.org/post", REQ_BODY, &mut res_body).unwrap(); + + // Prints details about the response. println!("Status: {} {}", res.status_code(), res.reason()); - println!("Headers {}", res.headers()); - //println!("{}", String::from_utf8_lossy(&writer)); + println!("Headers: {}", res.headers()); + //println!("{}", String::from_utf8_lossy(&res_body)); } diff --git a/examples/request_builder_get.rs b/examples/request_builder_get.rs deleted file mode 100644 index 5745571..0000000 --- a/examples/request_builder_get.rs +++ /dev/null @@ -1,27 +0,0 @@ -use http_req::{request::RequestBuilder, tls, uri::Uri}; -use std::net::TcpStream; - -fn main() { - //Parse uri and assign it to variable `addr` - let addr: Uri = "https://doc.rust-lang.org/".parse().unwrap(); - - //Connect to remote host - let stream = TcpStream::connect((addr.host().unwrap(), addr.corr_port())).unwrap(); - - //Open secure connection over TlsStream, because of `addr` (https) - let mut stream = tls::Config::default() - .connect(addr.host().unwrap_or(""), stream) - .unwrap(); - - //Container for response's body - let mut writer = Vec::new(); - - //Add header `Connection: Close` - let response = RequestBuilder::new(&addr) - .header("Connection", "Close") - .send(&mut stream, &mut writer) - .unwrap(); - - println!("Status: {} {}", response.status_code(), response.reason()); - //println!("{}", String::from_utf8_lossy(&writer)); -} diff --git a/src/chunked.rs b/src/chunked.rs index ffc3e4d..d23a200 100644 --- a/src/chunked.rs +++ b/src/chunked.rs @@ -1,15 +1,17 @@ -//! module chunked implements the wire protocol for HTTP's "chunked" Transfer-Encoding. -//! And it's a rust version of the reference implementation in [Go][1]. -//! -//! [1]: https://golang.google.cn/src/net/http/internal/chunked.go -//! +//! support for Transfer-Encoding: chunked -use std::io::{self, BufRead, BufReader, Error, ErrorKind, Read}; +use crate::CR_LF; +use std::{ + cmp, + io::{self, BufRead, BufReader, Error, ErrorKind, Read}, +}; const MAX_LINE_LENGTH: usize = 4096; -const CR_LF: [u8; 2] = [b'\r', b'\n']; -pub struct Reader { +/// Implements the wire protocol for HTTP's Transfer-Encoding: chunked. +/// +/// It's a Rust version of the [reference implementation in Go](https://golang.google.cn/src/net/http/internal/chunked.go) +pub struct ChunkReader { check_end: bool, eof: bool, err: Option, @@ -17,14 +19,13 @@ pub struct Reader { reader: BufReader, } -impl Read for Reader +impl Read for ChunkReader where R: Read, { fn read(&mut self, buf: &mut [u8]) -> io::Result { - // the length of data already read out - let mut consumed = 0usize; - let mut footer = [0u8; 2]; + let mut consumed = 0; + let mut footer: [u8; 2] = [0; 2]; while !self.eof && self.err.is_none() { if self.check_end { @@ -35,25 +36,23 @@ where break; } - if let Ok(_) = self.reader.read_exact(&mut footer) { - if footer != CR_LF { - self.err = Some(error_malformed_chunked_encoding()); - break; - } + if self.reader.read_exact(&mut footer).is_ok() && &footer != CR_LF { + self.err = Some(Error::new( + ErrorKind::InvalidData, + "Malformed chunked encoding", + )); + break; } self.check_end = false; } if self.n == 0 { - if consumed > 0 && !self.chunk_header_avaliable() { - // We've read enough. Don't potentially block - // reading a new chunk header. + if consumed > 0 && !self.chunk_header_available() { break; } self.begin_chunk(); - continue; } @@ -61,13 +60,9 @@ where break; } - let end = if consumed + self.n < buf.len() { - consumed + self.n - } else { - buf.len() - }; + let end = cmp::min(consumed + self.n, buf.len()); - let mut n0 = 0usize; + let mut n0: usize = 0; match self.reader.read(&mut buf[consumed..end]) { Ok(v) => n0 = v, Err(err) => self.err = Some(err), @@ -84,19 +79,45 @@ where } match self.err.as_ref() { - Some(v) => Err(Error::new( - v.kind(), - format!("wrapper by chunked: {}", v.to_string()), - )), + Some(v) => Err(Error::new(v.kind(), v.to_string())), None => Ok(consumed), } } } -impl Reader +impl BufRead for ChunkReader +where + R: Read, +{ + fn fill_buf(&mut self) -> io::Result<&[u8]> { + self.reader.fill_buf() + } + + fn consume(&mut self, amt: usize) { + self.reader.consume(amt) + } +} + +impl From> for ChunkReader where R: Read, { + fn from(value: BufReader) -> Self { + ChunkReader { + check_end: false, + eof: false, + err: None, + n: 0, + reader: value, + } + } +} + +impl ChunkReader +where + R: Read, +{ + /// Creates a new `ChunkReader` from `reader` pub fn new(reader: R) -> Self where R: Read, @@ -110,8 +131,11 @@ where } } + /// Begins a new chunk by reading and parsing its header. + /// + /// This function reads one full line representing the size of the next HTTP/1.x chunk. + /// If there is an error during this process (such as malformed data), it records that error for later use. fn begin_chunk(&mut self) { - // chunk-size CRLF let line = match read_chunk_line(&mut self.reader) { Ok(v) => v, Err(err) => { @@ -122,25 +146,26 @@ where match parse_hex_uint(line) { Ok(v) => self.n = v, - Err(err) => self.err = Some(Error::new(ErrorKind::Other, err)), + Err(err) => self.err = Some(Error::new(ErrorKind::InvalidData, err)), } self.eof = self.n == 0; } - fn chunk_header_avaliable(&self) -> bool { - self.reader.buffer().iter().find(|&&c| c == b'\n').is_some() + /// Checks whether a chunk header is available. + fn chunk_header_available(&self) -> bool { + self.reader.buffer().iter().any(|&c| c == b'\n') } } -fn error_line_too_long() -> Error { - Error::new(ErrorKind::Other, "header line too long") -} - -fn error_malformed_chunked_encoding() -> Error { - Error::new(ErrorKind::Other, "malformed chunked encoding") -} - +/// Checks if a given byte is an ASCII space character. +/// +/// This function checks whether a single byte, b, +/// represents one of the following characters: +/// - Space (ASCII 0x20) +/// - Tab (ASCII 0x09) +/// - Line Feed (ASCII 0xA) or Carriage Return (ASCII 0xD), which are used to move +/// positions in text. These two together indicate an end of line. fn is_ascii_space(b: u8) -> bool { match b { b' ' | b'\t' | b'\n' | b'\r' => true, @@ -148,18 +173,20 @@ fn is_ascii_space(b: u8) -> bool { } } -fn parse_hex_uint(data: Vec) -> Result { - let mut n = 0usize; +/// Parses an integer represented by hexadecimal digits from bytes. +fn parse_hex_uint<'a>(data: Vec) -> Result { + let mut n = 0; + for (i, v) in data.iter().enumerate() { if i == 16 { - return Err("http chunk length too large"); + return Err("HTTP chunk length is too large"); } let vv = match *v { b'0'..=b'9' => v - b'0', b'a'..=b'f' => v - b'a' + 10, b'A'..=b'F' => v - b'A' + 10, - _ => return Err("invalid byte in chunk length"), + _ => return Err("Invalid byte in chunk length"), }; n <<= 4; @@ -169,6 +196,7 @@ fn parse_hex_uint(data: Vec) -> Result { Ok(n) } +/// Reads a single chunk line from `BufReader`. fn read_chunk_line(b: &mut BufReader) -> io::Result> where R: Read, @@ -177,7 +205,10 @@ where b.read_until(b'\n', &mut line)?; if line.len() > MAX_LINE_LENGTH { - return Err(error_line_too_long()); + return Err(Error::new( + ErrorKind::InvalidData, + "Exceeded maximum line length", + )); } trim_trailing_whitespace(&mut line); @@ -186,47 +217,49 @@ where Ok(line) } +/// Removes any trailing chunk extensions from a vector containing bytes (`Vec`). fn remove_chunk_extension(v: &mut Vec) { - if let Some(idx) = v.iter().position(|v| *v == b';') { - v.resize(idx, 0); + if let Some(idx) = v.iter().position(|&v| v == b';') { + v.truncate(idx); } } +/// Remove any trailing whitespace characters (specifically ASCII spaces) +/// from the end of a vector containing bytes (`Vec`). fn trim_trailing_whitespace(v: &mut Vec) { - if v.len() == 0 { + if v.is_empty() { return; } - for i in (0..(v.len() - 1)).rev() { - if !is_ascii_space(v[i]) { - v.resize(i + 1, 0); - return; + while let Some(&last_byte) = v.last() { + if !is_ascii_space(last_byte) { + break; } - } - v.clear(); + v.pop(); + } } #[cfg(test)] mod tests { - use std::io::{self, Read}; - use super::*; + use std::io::{self, Read}; #[test] fn read() { let data: &[u8] = b"7\r\nhello, \r\n17\r\nworld! 0123456789abcdef\r\n0\r\n"; - let mut reader = Reader::new(data); + let mut reader = ChunkReader::new(data); let mut writer = vec![]; io::copy(&mut reader, &mut writer).expect("failed to dechunk"); assert_eq!("hello, world! 0123456789abcdef".as_bytes(), &writer[..]); } + #[test] fn read_multiple() { { let data: &[u8] = b"3\r\nfoo\r\n3\r\nbar\r\n0\r\n"; - let mut reader = Reader::new(data); + let mut reader = ChunkReader::new(data); let mut writer = vec![0u8; 10]; let n = reader.read(&mut writer).expect("unexpect error"); @@ -235,7 +268,7 @@ mod tests { } { let data: &[u8] = b"3\r\nfoo\r\n0\r\n"; - let mut reader = Reader::new(data); + let mut reader = ChunkReader::new(data); let mut writer = vec![0u8; 3]; let n = reader.read(&mut writer).expect("unexpect error"); @@ -243,15 +276,17 @@ mod tests { assert_eq!("foo".as_bytes(), &writer[..]); } } + #[test] fn read_partial() { let data: &[u8] = b"7\r\n1234567"; - let mut reader = Reader::new(data); + let mut reader = ChunkReader::new(data); let mut writer = vec![]; io::copy(&mut reader, &mut writer).expect("failed to dechunk"); assert_eq!("1234567".as_bytes(), &writer[..]); } + #[test] fn read_ignore_extensions() { let data_str = String::from("7;ext=\"some quoted string\"\r\n") @@ -260,7 +295,7 @@ mod tests { + "world! 0123456789abcdef\r\n" + "0;someextension=sometoken\r\n"; let data = data_str.as_bytes(); - let mut reader = Reader::new(data); + let mut reader = ChunkReader::new(data); let mut writer = vec![]; reader.read_to_end(&mut writer).expect("failed to dechunk"); diff --git a/src/error.rs b/src/error.rs index 55bf752..5f80e38 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,14 +1,30 @@ -//!error system -use std::{error, fmt, io, num, str}; +//! error system used around the library. +use std::{error, fmt, io, num, str, sync::mpsc}; + +/// Enum representing different parsing errors encountered by the library. #[derive(Debug, PartialEq)] pub enum ParseErr { + /// Error related to invalid UTF-8 character sequences encountered + /// during string processing or conversion operations. Utf8(str::Utf8Error), + + /// Failure in parsing integer values from strings using standard + /// number formats, such as those conforming to base 10 conventions. Int(num::ParseIntError), + + /// Issue encountered when processing status line from HTTP response message. StatusErr, + + /// Issue encountered when processing headers from HTTP response message. HeadersErr, + + /// Issue arising while processing URIs that contain invalid + /// characters or do not follow the URI specification. UriErr, - Invalid, + + /// Error indicating that provided string, vector, or other element + /// does not contain any values that could be parsed. Empty, } @@ -19,7 +35,7 @@ impl error::Error for ParseErr { match self { Utf8(e) => Some(e), Int(e) => Some(e), - StatusErr | HeadersErr | UriErr | Invalid | Empty => None, + _ => None, } } } @@ -29,13 +45,12 @@ impl fmt::Display for ParseErr { use self::ParseErr::*; let err = match self { - Utf8(_) => "invalid character", - Int(_) => "cannot parse number", - Invalid => "invalid value", - Empty => "nothing to parse", - StatusErr => "status line contains invalid values", - HeadersErr => "headers contain invalid values", - UriErr => "uri contains invalid characters", + Utf8(_) => "Invalid character sequence", + Int(_) => "Cannot parse number", + StatusErr => "Status line contains invalid values", + HeadersErr => "Headers contain invalid values", + UriErr => "URI contains invalid characters", + Empty => "Nothing to parse", }; write!(f, "ParseErr: {}", err) } @@ -53,11 +68,27 @@ impl From for ParseErr { } } +/// Enum representing various errors encountered by the library. #[derive(Debug)] pub enum Error { + /// IO error that occurred during file operations, + /// network connections, or any other type of I/O operation. IO(io::Error), + + /// Error encountered while parsing data using the library's functions. Parse(ParseErr), + + /// Timeout error, indicating that an operation timed out + /// after waiting for the specified duration. + Timeout, + + /// Error encountered while using TLS/SSL cryptographic protocols, + /// such as establishing secure connections with servers. Tls, + + /// Thread-related communication error, signifying an issue + /// that occurred during inter-thread communication. + Thread, } impl error::Error for Error { @@ -67,7 +98,7 @@ impl error::Error for Error { match self { IO(e) => Some(e), Parse(e) => Some(e), - Tls => None, + _ => None, } } } @@ -77,28 +108,16 @@ impl fmt::Display for Error { use self::Error::*; let err = match self { - IO(_) => "IO error", + IO(e) => &format!("IO Error - {}", e), + Parse(e) => return e.fmt(f), + Timeout => "Timeout error", Tls => "TLS error", - Parse(err) => return err.fmt(f), + Thread => "Thread communication error", }; write!(f, "Error: {}", err) } } -#[cfg(feature = "native-tls")] -impl From for Error { - fn from(_e: native_tls::Error) -> Self { - Error::Tls - } -} - -#[cfg(feature = "native-tls")] -impl From> for Error { - fn from(_e: native_tls::HandshakeError) -> Self { - Error::Tls - } -} - impl From for Error { fn from(e: io::Error) -> Self { Error::IO(e) @@ -116,3 +135,36 @@ impl From for Error { Error::Parse(ParseErr::Utf8(e)) } } + +impl From for Error { + fn from(_e: mpsc::RecvTimeoutError) -> Self { + Error::Timeout + } +} + +#[cfg(feature = "rust-tls")] +impl From for Error { + fn from(_e: rustls::Error) -> Self { + Error::Tls + } +} + +#[cfg(feature = "native-tls")] +impl From for Error { + fn from(_e: native_tls::Error) -> Self { + Error::Tls + } +} + +#[cfg(feature = "native-tls")] +impl From> for Error { + fn from(_e: native_tls::HandshakeError) -> Self { + Error::Tls + } +} + +impl From> for Error { + fn from(_e: mpsc::SendError) -> Self { + Error::Thread + } +} diff --git a/src/lib.rs b/src/lib.rs index 27bd641..d1b3950 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,22 +1,31 @@ -//!Simple HTTP client with built-in HTTPS support. -//!Currently it's in heavy development and may frequently change. +//! Simple HTTP client with built-in HTTPS support. //! -//!## Example -//!Basic GET request -//!``` -//!use http_req::request; +//! By default uses [rust-native-tls](https://github.com/sfackler/rust-native-tls), +//! which relies on TLS framework provided by OS on Windows and macOS, and OpenSSL +//! on all other platforms. But it also supports [rus-tls](https://crates.io/crates/rustls). //! -//!fn main() { -//! let mut writer = Vec::new(); //container for body of a response -//! let res = request::get("https://doc.rust-lang.org/", &mut writer).unwrap(); +//! ## Examples +//! Basic GET request +//! ``` +//! use http_req::request; //! -//! println!("Status: {} {}", res.status_code(), res.reason()); -//!} -//!``` +//! fn main() { +//! // Container for body of a response +//! let mut body = Vec::new(); +//! let res = request::get("https://doc.rust-lang.org/", &mut body).unwrap(); +//! +//! println!("Status: {} {}", res.status_code(), res.reason()); +//! } +//! ``` + +pub mod chunked; pub mod error; pub mod request; pub mod response; +pub mod stream; +#[cfg(any(feature = "native-tls", feature = "rust-tls"))] pub mod tls; pub mod uri; -mod chunked; +pub(crate) const CR_LF: &[u8; 2] = b"\r\n"; +pub(crate) const LF: u8 = 0xA; diff --git a/src/request.rs b/src/request.rs index c82c872..5f07af3 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,132 +1,32 @@ //! creating and sending HTTP requests + use crate::{ + chunked::ChunkReader, error, - response::{find_slice, Headers, Response, CR_LF_2}, - tls, + response::{Headers, Response}, + stream::{Stream, ThreadReceive, ThreadSend}, uri::Uri, }; +#[cfg(feature = "auth")] +use base64::prelude::*; use std::{ + convert::TryFrom, fmt, - io::{self, ErrorKind, Read, Write}, - net::{TcpStream, ToSocketAddrs}, + io::{BufReader, Write}, path::Path, + sync::mpsc, + thread, time::{Duration, Instant}, }; +#[cfg(feature = "auth")] +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; const CR_LF: &str = "\r\n"; -const BUF_SIZE: usize = 8 * 1024; -const SMALL_BUF_SIZE: usize = 8 * 10; -const TEST_FREQ: usize = 100; - -///Every iteration increases `count` by one. When `count` is equal to `stop`, `next()` -///returns `Some(true)` (and sets `count` to 0), otherwise returns `Some(false)`. -///Iterator never returns `None`. -pub struct Counter { - count: usize, - stop: usize, -} - -impl Counter { - pub fn new(stop: usize) -> Counter { - Counter { count: 0, stop } - } -} - -impl Iterator for Counter { - type Item = bool; - - fn next(&mut self) -> Option { - self.count += 1; - let breakpoint = self.count == self.stop; - - if breakpoint { - self.count = 0; - } - - Some(breakpoint) - } -} - -///Copies data from `reader` to `writer` until the `deadline` is reached. -///Returns how many bytes has been read. -pub fn copy_with_timeout(reader: &mut R, writer: &mut W, deadline: Instant) -> io::Result -where - R: Read + ?Sized, - W: Write + ?Sized, -{ - let mut buf = [0; BUF_SIZE]; - let mut copied = 0; - let mut counter = Counter::new(TEST_FREQ); - - loop { - let len = match reader.read(&mut buf) { - Ok(0) => return Ok(copied), - Ok(len) => len, - Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, - Err(e) => return Err(e), - }; - writer.write_all(&buf[..len])?; - copied += len as u64; - - if counter.next().unwrap() && Instant::now() >= deadline { - return Ok(copied); - } - } -} - -///Copies a given amount of bytes from `reader` to `writer`. -pub fn copy_exact(reader: &mut R, writer: &mut W, num_bytes: usize) -> io::Result<()> -where - R: Read + ?Sized, - W: Write + ?Sized, -{ - let mut buf = vec![0u8; num_bytes]; - - reader.read_exact(&mut buf)?; - writer.write_all(&mut buf) -} +const DEFAULT_REDIRECT_LIMIT: usize = 5; +const DEFAULT_REQ_TIMEOUT: u64 = 60 * 60; +const DEFAULT_CALL_TIMEOUT: u64 = 60; -///Reads data from `reader` and checks for specified `val`ue. When data contains specified value -///or `deadline` is reached, stops reading. Returns read data as array of two vectors: elements -///before and after the `val`. -pub fn copy_until( - reader: &mut R, - val: &[u8], - deadline: Instant, -) -> Result<[Vec; 2], io::Error> -where - R: Read + ?Sized, -{ - let mut buf = [0; SMALL_BUF_SIZE]; - let mut writer = Vec::new(); - let mut counter = Counter::new(TEST_FREQ); - let mut split_idx = 0; - - loop { - let len = match reader.read(&mut buf) { - Ok(0) => break, - Ok(len) => len, - Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, - Err(e) => return Err(e), - }; - - writer.write_all(&buf[..len])?; - - if let Some(i) = find_slice(&writer, val) { - split_idx = i; - break; - } - - if counter.next().unwrap() && Instant::now() >= deadline { - split_idx = writer.len(); - break; - } - } - - Ok([writer[..split_idx].to_vec(), writer[split_idx..].to_vec()]) -} - -///HTTP request methods +/// HTTP request methods #[derive(Debug, PartialEq, Clone, Copy)] pub enum Method { GET, @@ -134,29 +34,46 @@ pub enum Method { POST, PUT, DELETE, + CONNECT, OPTIONS, + TRACE, PATCH, } -impl fmt::Display for Method { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +impl Method { + /// Returns a string representation of an HTTP request method. + /// + /// # Examples + /// ``` + /// use http_req::request::Method; + /// + /// let method = Method::GET; + /// assert_eq!(method.as_str(), "GET"); + /// ``` + pub const fn as_str(&self) -> &str { use self::Method::*; - let method = match self { + match self { GET => "GET", HEAD => "HEAD", POST => "POST", PUT => "PUT", DELETE => "DELETE", + CONNECT => "CONNECT", OPTIONS => "OPTIONS", + TRACE => "TRACE", PATCH => "PATCH", - }; + } + } +} - write!(f, "{}", method) +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.as_str()) } } -///HTTP versions +/// HTTP versions #[derive(Debug, PartialEq, Clone, Copy)] pub enum HttpVersion { Http10, @@ -165,7 +82,16 @@ pub enum HttpVersion { } impl HttpVersion { - pub fn as_str(self) -> &'static str { + /// Returns a string representation of an HTTP version. + /// + /// # Examples + /// ``` + /// use http_req::request::HttpVersion; + /// + /// let version = HttpVersion::Http10; + /// assert_eq!(version.as_str(), "HTTP/1.0"); + /// ``` + pub const fn as_str(&self) -> &str { use self::HttpVersion::*; match self { @@ -182,94 +108,226 @@ impl fmt::Display for HttpVersion { } } -///Relatively low-level struct for making HTTP requests. -/// -///It can work with any stream that implements `Read` and `Write`. -///By default it does not close the connection after completion of the response. -/// -///# Examples -///``` -///use std::net::TcpStream; -///use http_req::{request::RequestBuilder, tls, uri::Uri, response::StatusCode}; -/// -///let addr: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); -///let mut writer = Vec::new(); +/// Authentication details: +/// - Basic: username and password +/// - Bearer: token +#[cfg(feature = "auth")] +#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop)] +pub struct Authentication(AuthenticationType); + +#[cfg(feature = "auth")] +impl Authentication { + /// Creates a new `Authentication` of type `Basic`. + /// + /// # Examples + /// ``` + /// use http_req::request::Authentication; + /// + /// let auth = Authentication::basic("foo", "bar"); + /// ``` + pub fn basic(username: &T, password: &U) -> Authentication + where + T: ToString + ?Sized, + U: ToString + ?Sized, + { + Authentication(AuthenticationType::Basic { + username: username.to_string(), + password: password.to_string(), + }) + } + + /// Creates a new `Authentication` of type `Bearer` + /// + /// # Examples + /// ``` + /// use http_req::request::Authentication; + /// + /// let auth = Authentication::bearer("secret_token"); + /// ``` + pub fn bearer(token: &T) -> Authentication + where + T: ToString + ?Sized, + { + Authentication(AuthenticationType::Bearer(token.to_string())) + } + + /// Generates an HTTP Authorization header. Returns a `key` & `value` pair. + /// + /// - Basic: uses base64 encoding on provided credentials + /// - Bearer: uses token as is + /// + /// # Examples + /// ``` + /// use http_req::request::Authentication; + /// + /// let auth = Authentication::bearer("secretToken"); + /// let (key, val) = auth.header(); + /// + /// assert_eq!(key, "Authorization"); + /// assert_eq!(val, "Bearer secretToken"); + /// ``` + pub fn header(&self) -> (String, String) { + let key = "Authorization".to_string(); + let val = String::with_capacity(200) + self.0.scheme() + " " + &self.0.credentials(); + + (key, val) + } +} + +/// Authentication types +#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop)] +#[cfg(feature = "auth")] +enum AuthenticationType { + Basic { username: String, password: String }, + Bearer(String), +} + +#[cfg(feature = "auth")] +impl AuthenticationType { + /// Returns the authentication scheme as a string. + const fn scheme(&self) -> &str { + use AuthenticationType::*; + + match self { + Basic { + username: _, + password: _, + } => "Basic", + Bearer(_) => "Bearer", + } + } + + /// Returns encoded credentials + fn credentials(&self) -> Zeroizing { + use AuthenticationType::*; + + match self { + Basic { username, password } => { + let credentials = Zeroizing::new(format!("{}:{}", username, password)); + Zeroizing::new(BASE64_STANDARD.encode(credentials.as_bytes())) + } + Bearer(token) => Zeroizing::new(token.to_string()), + } + } +} + +/// Allows control over redirects. +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum RedirectPolicy { + /// Follows redirect if limit is greater than 0. + Limit(usize), + /// Runs a function `F` to determine if the redirect should be followed. + Custom(F), +} + +impl RedirectPolicy +where + F: Fn(&str) -> bool, +{ + /// Evaluates the policy against specified conditions: + /// - `Limit`: Checks if limit is greater than 0 and decrements it by one each time a redirect is followed. + /// - `Custom`: Executes function `F` with the URI, returning its result to decide on following the redirect. + /// + /// # Examples + /// ``` + /// use http_req::request::RedirectPolicy; + /// + /// let uri: &str = "https://www.rust-lang.org/learn"; + /// + /// // Follows redirects up to 5 times as per `Limit` policy. + /// let mut policy_1: RedirectPolicy bool> = RedirectPolicy::Limit(5); + /// assert_eq!(policy_1.follow(&uri), true); // First call, limit is 5 + /// + /// // Does not follow redirects due to zero `Limit`. + /// let mut policy_2: RedirectPolicy bool> = RedirectPolicy::Limit(0); + /// assert_eq!(policy_2.follow(&uri), false); + /// + /// // Custom policy returning false, hence no redirect. + /// let mut policy_3: RedirectPolicy bool> = RedirectPolicy::Custom(|_| false); + /// assert_eq!(policy_3.follow(&uri), false); + ///``` + pub fn follow(&mut self, uri: &str) -> bool { + use self::RedirectPolicy::*; + + match self { + Limit(limit) => match limit { + 0 => false, + _ => { + *limit = *limit - 1; + true + } + }, + Custom(func) => func(uri), + } + } +} + +impl Default for RedirectPolicy +where + F: Fn(&str) -> bool, +{ + fn default() -> Self { + RedirectPolicy::Limit(DEFAULT_REDIRECT_LIMIT) + } +} + +/// Raw HTTP request message that can be sent to any stream. /// -///let stream = TcpStream::connect((addr.host().unwrap(), addr.corr_port())).unwrap(); -///let mut stream = tls::Config::default() -/// .connect(addr.host().unwrap_or(""), stream) -/// .unwrap(); +/// # Examples +/// ``` +/// use std::convert::TryFrom; +/// use http_req::{request::RequestMessage, uri::Uri}; /// -///let response = RequestBuilder::new(&addr) -/// .header("Connection", "Close") -/// .send(&mut stream, &mut writer) -/// .unwrap(); +/// let addr: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// -///assert_eq!(response.status_code(), StatusCode::new(200)); -///``` +/// let mut request_msg = RequestMessage::new(&addr) +/// .header("Connection", "Close") +/// .parse(); +/// ``` #[derive(Clone, Debug, PartialEq)] -pub struct RequestBuilder<'a> { - uri: &'a Uri, +pub struct RequestMessage<'a> { + uri: &'a Uri<'a>, method: Method, version: HttpVersion, headers: Headers, body: Option<&'a [u8]>, - timeout: Option, } -impl<'a> RequestBuilder<'a> { - ///Creates new `RequestBuilder` with default parameters - /// - ///# Examples - ///``` - ///use std::net::TcpStream; - ///use http_req::{request::RequestBuilder, tls, uri::Uri}; +impl<'a> RequestMessage<'a> { + /// Creates a new `RequestMessage` with default parameters. /// - ///let addr: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///let mut writer = Vec::new(); + /// # Examples + /// ``` + /// use std::convert::TryFrom; + /// use http_req::{request::RequestMessage, uri::Uri}; /// - ///let stream = TcpStream::connect((addr.host().unwrap(), addr.corr_port())).unwrap(); - ///let mut stream = tls::Config::default() - /// .connect(addr.host().unwrap_or(""), stream) - /// .unwrap(); + /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - ///let response = RequestBuilder::new(&addr) - /// .header("Connection", "Close") - /// .send(&mut stream, &mut writer) - /// .unwrap(); - ///``` - pub fn new(uri: &'a Uri) -> RequestBuilder<'a> { - RequestBuilder { + /// let request_msg = RequestMessage::new(&addr) + /// .header("Connection", "Close"); + /// ``` + pub fn new(uri: &'a Uri<'a>) -> RequestMessage<'a> { + RequestMessage { headers: Headers::default_http(uri), uri, method: Method::GET, version: HttpVersion::Http11, body: None, - timeout: None, } } - ///Sets request method + /// Sets the request method. /// - ///# Examples - ///``` - ///use std::net::TcpStream; - ///use http_req::{request::{RequestBuilder, Method}, tls, uri::Uri}; - /// - ///let addr: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///let mut writer = Vec::new(); - /// - ///let stream = TcpStream::connect((addr.host().unwrap(), addr.corr_port())).unwrap(); - ///let mut stream = tls::Config::default() - /// .connect(addr.host().unwrap_or(""), stream) - /// .unwrap(); - /// - ///let response = RequestBuilder::new(&addr) - /// .method(Method::HEAD) - /// .header("Connection", "Close") - /// .send(&mut stream, &mut writer) - /// .unwrap(); - ///``` + /// # Examples + /// ``` + /// use std::convert::TryFrom; + /// use http_req::{request::{RequestMessage, Method}, uri::Uri}; + /// + /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// + /// let request_msg = RequestMessage::new(&addr) + /// .method(Method::HEAD); + /// ``` pub fn method(&mut self, method: T) -> &mut Self where Method: From, @@ -278,28 +336,18 @@ impl<'a> RequestBuilder<'a> { self } - ///Sets HTTP version + /// Sets the HTTP version. /// - ///# Examples - ///``` - ///use std::net::TcpStream; - ///use http_req::{request::{RequestBuilder, HttpVersion}, tls, uri::Uri}; - /// - ///let addr: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///let mut writer = Vec::new(); - /// - ///let stream = TcpStream::connect((addr.host().unwrap(), addr.corr_port())).unwrap(); - ///let mut stream = tls::Config::default() - /// .connect(addr.host().unwrap_or(""), stream) - /// .unwrap(); - /// - ///let response = RequestBuilder::new(&addr) - /// .version(HttpVersion::Http10) - /// .header("Connection", "Close") - /// .send(&mut stream, &mut writer) - /// .unwrap(); - ///``` - + /// # Examples + /// ``` + /// use std::convert::TryFrom; + /// use http_req::{request::{RequestMessage, HttpVersion}, uri::Uri}; + /// + /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// + /// let request_msg = RequestMessage::new(&addr) + /// .version(HttpVersion::Http10); + /// ``` pub fn version(&mut self, version: T) -> &mut Self where HttpVersion: From, @@ -308,31 +356,24 @@ impl<'a> RequestBuilder<'a> { self } - ///Replaces all it's headers with headers passed to the function + /// Replaces all its headers with the provided headers. /// - ///# Examples - ///``` - ///use std::net::TcpStream; - ///use http_req::{request::{RequestBuilder, Method}, response::Headers, tls, uri::Uri}; - /// - ///let addr: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///let mut writer = Vec::new(); - ///let mut headers = Headers::new(); - ///headers.insert("Accept-Charset", "utf-8"); - ///headers.insert("Accept-Language", "en-US"); - ///headers.insert("Host", "rust-lang.org"); - ///headers.insert("Connection", "Close"); - /// - ///let stream = TcpStream::connect((addr.host().unwrap(), addr.corr_port())).unwrap(); - ///let mut stream = tls::Config::default() - /// .connect(addr.host().unwrap_or(""), stream) - /// .unwrap(); - /// - ///let response = RequestBuilder::new(&addr) - /// .headers(headers) - /// .send(&mut stream, &mut writer) - /// .unwrap(); - ///``` + /// # Examples + /// ``` + /// use std::convert::TryFrom; + /// use http_req::{request::RequestMessage, response::Headers, uri::Uri}; + /// + /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// + /// let mut headers = Headers::new(); + /// headers.insert("Accept-Charset", "utf-8"); + /// headers.insert("Accept-Language", "en-US"); + /// headers.insert("Host", "rust-lang.org"); + /// headers.insert("Connection", "Close"); + /// + /// let request_msg = RequestMessage::new(&addr) + /// .headers(headers); + /// ``` pub fn headers(&mut self, headers: T) -> &mut Self where Headers: From, @@ -341,26 +382,18 @@ impl<'a> RequestBuilder<'a> { self } - ///Adds new header to existing/default headers + /// Adds a new header to the existing/default headers. /// - ///# Examples - ///``` - ///use std::net::TcpStream; - ///use http_req::{request::{RequestBuilder, Method}, tls, uri::Uri}; + /// # Examples + /// ``` + /// use std::convert::TryFrom; + /// use http_req::{request::RequestMessage, response::Headers, uri::Uri}; /// - ///let addr: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///let mut writer = Vec::new(); + /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - ///let stream = TcpStream::connect((addr.host().unwrap(), addr.corr_port())).unwrap(); - ///let mut stream = tls::Config::default() - /// .connect(addr.host().unwrap_or(""), stream) - /// .unwrap(); - /// - ///let response = RequestBuilder::new(&addr) - /// .header("Connection", "Close") - /// .send(&mut stream, &mut writer) - /// .unwrap(); - ///``` + /// let request_msg = RequestMessage::new(&addr) + /// .header("Connection", "Close"); + /// ``` pub fn header(&mut self, key: &T, val: &U) -> &mut Self where T: ToString + ?Sized, @@ -370,183 +403,65 @@ impl<'a> RequestBuilder<'a> { self } - ///Sets body for request + /// Adds an authorization header to existing headers. /// - ///# Examples - ///``` - ///use std::net::TcpStream; - ///use http_req::{request::{RequestBuilder, Method}, tls, uri::Uri}; - /// - ///let addr: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///const body: &[u8; 27] = b"field1=value1&field2=value2"; - ///let mut writer = Vec::new(); - /// - ///let stream = TcpStream::connect((addr.host().unwrap(), addr.corr_port())).unwrap(); - ///let mut stream = tls::Config::default() - /// .connect(addr.host().unwrap_or(""), stream) - /// .unwrap(); - /// - ///let response = RequestBuilder::new(&addr) - /// .method(Method::POST) - /// .body(body) - /// .header("Content-Length", &body.len()) - /// .header("Connection", "Close") - /// .send(&mut stream, &mut writer) - /// .unwrap(); - ///``` - pub fn body(&mut self, body: &'a [u8]) -> &mut Self { - self.body = Some(body); - self - } - - ///Sets timeout for entire connection. + /// # Examples + /// ``` + /// use std::convert::TryFrom; + /// use http_req::{request::{RequestMessage, Authentication}, response::Headers, uri::Uri}; /// - ///# Examples - ///``` - ///use std::{net::TcpStream, time::{Duration, Instant}}; - ///use http_req::{request::RequestBuilder, tls, uri::Uri}; - /// - ///let addr: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///let mut writer = Vec::new(); - /// - ///let stream = TcpStream::connect((addr.host().unwrap(), addr.corr_port())).unwrap(); - ///let mut stream = tls::Config::default() - /// .connect(addr.host().unwrap_or(""), stream) - /// .unwrap(); - ///let timeout = Some(Duration::from_secs(3600)); - /// - ///let response = RequestBuilder::new(&addr) - /// .timeout(timeout) - /// .header("Connection", "Close") - /// .send(&mut stream, &mut writer) - /// .unwrap(); - ///``` - pub fn timeout(&mut self, timeout: Option) -> &mut Self + /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// + /// let request_msg = RequestMessage::new(&addr) + /// .authentication(Authentication::bearer("secret456token123")); + /// ``` + #[cfg(feature = "auth")] + pub fn authentication(&mut self, auth: T) -> &mut Self where - Duration: From, + Authentication: From, { - self.timeout = timeout.map(Duration::from); + let auth = Authentication::from(auth); + let (key, val) = auth.header(); + + self.headers.insert_raw(key, val); self } - ///Sends HTTP request in these steps: - /// - ///- Writes request message to `stream`. - ///- Writes response's body to `writer`. - ///- Returns response for this request. + /// Sets the body for the request. /// - ///# Examples + /// # Examples + /// ``` + /// use std::convert::TryFrom; + /// use http_req::{request::{RequestMessage, Method}, response::Headers, uri::Uri}; /// - ///HTTP - ///``` - ///use std::net::TcpStream; - ///use http_req::{request::RequestBuilder, uri::Uri}; - /// - /// //This address is automatically redirected to HTTPS, so response code will not ever be 200 - ///let addr: Uri = "http://www.rust-lang.org/learn".parse().unwrap(); - ///let mut writer = Vec::new(); - ///let mut stream = TcpStream::connect((addr.host().unwrap(), addr.corr_port())).unwrap(); - /// - ///let response = RequestBuilder::new(&addr) - /// .header("Connection", "Close") - /// .send(&mut stream, &mut writer) - /// .unwrap(); - ///``` + /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// const BODY: &[u8; 27] = b"field1=value1&field2=value2"; /// - ///HTTPS - ///``` - ///use std::net::TcpStream; - ///use http_req::{request::RequestBuilder, tls, uri::Uri}; + /// let request_msg = RequestMessage::new(&addr) + /// .method(Method::POST) + /// .body(BODY); + /// ``` + pub fn body(&mut self, body: &'a [u8]) -> &mut Self { + self.body = Some(body); + self.header("Content-Length", &body.len()); + self + } + + /// Parses the request message for this `RequestMessage`. /// - ///let addr: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///let mut writer = Vec::new(); + /// # Examples + /// ``` + /// use std::convert::TryFrom; + /// use http_req::{request::RequestMessage, uri::Uri}; /// - ///let stream = TcpStream::connect((addr.host().unwrap(), addr.corr_port())).unwrap(); - ///let mut stream = tls::Config::default() - /// .connect(addr.host().unwrap_or(""), stream) - /// .unwrap(); + /// let addr: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - ///let response = RequestBuilder::new(&addr) - /// .header("Connection", "Close") - /// .send(&mut stream, &mut writer) - /// .unwrap(); - ///``` - pub fn send(&self, stream: &mut T, writer: &mut U) -> Result - where - T: Write + Read, - U: Write, - { - self.write_msg(stream, &self.parse_msg())?; - - let head_deadline = match self.timeout { - Some(t) => Instant::now() + t, - None => Instant::now() + Duration::from_secs(360), - }; - let (res, body_part) = self.read_head(stream, head_deadline)?; - - if self.method == Method::HEAD { - return Ok(res); - } - - if let Some(v) = res.headers().get("Transfer-Encoding") { - if *v == "chunked" { - let mut dechunked = crate::chunked::Reader::new(body_part.as_slice().chain(stream)); - - if let Some(timeout) = self.timeout { - let deadline = Instant::now() + timeout; - copy_with_timeout(&mut dechunked, writer, deadline)?; - } else { - io::copy(&mut dechunked, writer)?; - } - - return Ok(res); - } - } - - writer.write_all(&body_part)?; - - if let Some(timeout) = self.timeout { - let deadline = Instant::now() + timeout; - copy_with_timeout(stream, writer, deadline)?; - } else { - let num_bytes = res.content_len().unwrap_or(0); - - if num_bytes > 0 { - copy_exact(stream, writer, num_bytes - body_part.len())?; - } else { - io::copy(stream, writer)?; - } - } - - Ok(res) - } - - ///Writes message to `stream` and flushes it - pub fn write_msg(&self, stream: &mut T, msg: &U) -> Result<(), io::Error> - where - T: Write, - U: AsRef<[u8]>, - { - stream.write_all(msg.as_ref())?; - stream.flush()?; - - Ok(()) - } - - ///Reads head of server's response - pub fn read_head( - &self, - stream: &mut T, - deadline: Instant, - ) -> Result<(Response, Vec), error::Error> { - let [head, body_part] = copy_until(stream, &CR_LF_2, deadline)?; - - Ok((Response::from_head(&head)?, body_part)) - } - - ///Parses request message for this `RequestBuilder` - pub fn parse_msg(&self) -> Vec { - let request_line = format!( + /// let mut request_msg = RequestMessage::new(&addr) + /// .header("Connection", "Close") + /// .parse(); + /// ``` + pub fn parse(&self) -> Vec { + let mut request_msg = format!( "{} {} {}{}", self.method, self.uri.resource(), @@ -554,240 +469,224 @@ impl<'a> RequestBuilder<'a> { CR_LF ); - let headers: String = self - .headers - .iter() - .map(|(k, v)| format!("{}: {}{}", k, v, CR_LF)) - .collect(); - - let mut request_msg = (request_line + &headers + CR_LF).as_bytes().to_vec(); + for (key, val) in self.headers.iter() { + request_msg = request_msg + key + ": " + val + CR_LF; + } - if let Some(b) = &self.body { - request_msg.extend(*b); + let mut request_msg = (request_msg + CR_LF).as_bytes().to_vec(); + if let Some(b) = self.body { + request_msg.extend(b); } request_msg } } -///Relatively higher-level struct for making HTTP requests. +/// Allows for making HTTP requests based on specified parameters. /// -///It creates stream (`TcpStream` or `TlsStream`) appropriate for the type of uri (`http`/`https`) -///By default it closes connection after completion of the response. +/// This implementation creates a stream (`TcpStream` or `TlsStream`) appropriate for the URI type (`http`/`https`). +/// By default, it closes the connection after completing the response. /// -///# Examples -///``` -///use http_req::{request::Request, uri::Uri, response::StatusCode}; +/// # Examples +/// ``` +/// use http_req::{request::Request, uri::Uri, response::StatusCode}; +/// use std::convert::TryFrom; /// -///let mut writer = Vec::new(); -///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); +/// let mut writer = Vec::new(); +/// let uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// -///let response = Request::new(&uri).send(&mut writer).unwrap();; -///assert_eq!(response.status_code(), StatusCode::new(200)); -///``` +/// let response = Request::new(&uri).send(&mut writer).unwrap(); +/// assert_eq!(response.status_code(), StatusCode::new(200)); +/// ``` /// #[derive(Clone, Debug, PartialEq)] pub struct Request<'a> { - inner: RequestBuilder<'a>, + message: RequestMessage<'a>, + redirect_policy: RedirectPolicy bool>, connect_timeout: Option, read_timeout: Option, write_timeout: Option, + timeout: Duration, root_cert_file_pem: Option<&'a Path>, } impl<'a> Request<'a> { - ///Creates new `Request` with default parameters + /// Creates a new `Request`. Initializes the request with default values and sets the "Connection" header to "Close". /// - ///# Examples - ///``` - ///use http_req::{request::Request, uri::Uri}; + /// # Examples + /// ``` + /// use http_req::{request::Request, uri::Uri}; + /// use std::convert::TryFrom; /// - ///let mut writer = Vec::new(); - ///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); + /// let uri: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - ///let response = Request::new(&uri).send(&mut writer).unwrap();; - ///``` + /// let request = Request::new(&uri); + /// ``` pub fn new(uri: &'a Uri) -> Request<'a> { - let mut builder = RequestBuilder::new(&uri); - builder.header("Connection", "Close"); + let mut message = RequestMessage::new(&uri); + message.header("Connection", "Close"); Request { - inner: builder, - connect_timeout: Some(Duration::from_secs(60)), - read_timeout: Some(Duration::from_secs(60)), - write_timeout: Some(Duration::from_secs(60)), + message, + redirect_policy: RedirectPolicy::default(), + connect_timeout: Some(Duration::from_secs(DEFAULT_CALL_TIMEOUT)), + read_timeout: Some(Duration::from_secs(DEFAULT_CALL_TIMEOUT)), + write_timeout: Some(Duration::from_secs(DEFAULT_CALL_TIMEOUT)), + timeout: Duration::from_secs(DEFAULT_REQ_TIMEOUT), root_cert_file_pem: None, } } - ///Sets request method + /// Sets the request method. /// - ///# Examples - ///``` - ///use http_req::{request::{Request, Method}, uri::Uri}; + /// # Examples + /// ``` + /// use http_req::{request::{Request, Method}, uri::Uri}; + /// use std::convert::TryFrom; /// - ///let mut writer = Vec::new(); - ///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); + /// let uri: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - ///let response = Request::new(&uri) - /// .method(Method::HEAD) - /// .send(&mut writer) - /// .unwrap(); - ///``` + /// let request = Request::new(&uri) + /// .method(Method::HEAD); + /// ``` pub fn method(&mut self, method: T) -> &mut Self where Method: From, { - self.inner.method(method); + self.message.method(method); self } - ///Sets HTTP version + /// Sets the HTTP version. /// - ///# Examples - ///``` - ///use http_req::{request::{Request, HttpVersion}, uri::Uri}; + /// # Examples + /// ``` + /// use http_req::{request::{Request, HttpVersion}, uri::Uri}; + /// use std::convert::TryFrom; /// - ///let mut writer = Vec::new(); - ///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); + /// let uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - ///let response = Request::new(&uri) - /// .version(HttpVersion::Http10) - /// .send(&mut writer) - /// .unwrap(); - ///``` - + /// let request = Request::new(&uri) + /// .version(HttpVersion::Http10); + /// ``` pub fn version(&mut self, version: T) -> &mut Self where HttpVersion: From, { - self.inner.version(version); + self.message.version(version); self } - ///Replaces all it's headers with headers passed to the function + /// Replaces all its headers with the provided headers. /// - ///# Examples - ///``` - ///use http_req::{request::Request, uri::Uri, response::Headers}; + /// # Examples + /// ``` + /// use http_req::{request::Request, response::Headers, uri::Uri}; + /// use std::convert::TryFrom; /// - ///let mut writer = Vec::new(); - ///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); + /// let uri: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - ///let mut headers = Headers::new(); - ///headers.insert("Accept-Charset", "utf-8"); - ///headers.insert("Accept-Language", "en-US"); - ///headers.insert("Host", "rust-lang.org"); - ///headers.insert("Connection", "Close"); + /// let mut headers = Headers::new(); + /// headers.insert("Accept-Charset", "utf-8"); + /// headers.insert("Accept-Language", "en-US"); + /// headers.insert("Host", "rust-lang.org"); + /// headers.insert("Connection", "Close"); /// - ///let response = Request::new(&uri) - /// .headers(headers) - /// .send(&mut writer) - /// .unwrap();; - ///``` + /// let request = Request::new(&uri) + /// .headers(headers); + /// ``` pub fn headers(&mut self, headers: T) -> &mut Self where Headers: From, { - self.inner.headers(headers); + self.message.headers(headers); self } - ///Adds header to existing/default headers + /// Adds a new header to the existing/default headers. /// - ///# Examples - ///``` - ///use http_req::{request::Request, uri::Uri}; + /// # Examples + /// ``` + /// use http_req::{request::Request, uri::Uri}; + /// use std::convert::TryFrom; /// - ///let mut writer = Vec::new(); - ///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); + /// let uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - ///let response = Request::new(&uri) - /// .header("Accept-Language", "en-US") - /// .send(&mut writer) - /// .unwrap(); - ///``` + /// let request = Request::new(&uri) + /// .header("Accept-Language", "en-US"); + /// ``` pub fn header(&mut self, key: &T, val: &U) -> &mut Self where T: ToString + ?Sized, U: ToString + ?Sized, { - self.inner.header(key, val); + self.message.header(key, val); self } - ///Sets body for request + /// Adds an authorization header to existing headers. /// - ///# Examples - ///``` - ///use http_req::{request::{Request, Method}, uri::Uri}; - /// - ///let mut writer = Vec::new(); - ///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///const body: &[u8; 27] = b"field1=value1&field2=value2"; - /// - ///let response = Request::new(&uri) - /// .method(Method::POST) - /// .header("Content-Length", &body.len()) - /// .body(body) - /// .send(&mut writer) - /// .unwrap(); - ///``` - pub fn body(&mut self, body: &'a [u8]) -> &mut Self { - self.inner.body(body); + /// # Examples + /// ``` + /// use std::convert::TryFrom; + /// use http_req::{request::{Request, Authentication}, uri::Uri}; + /// + /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// + /// let request = Request::new(&addr) + /// .authentication(Authentication::bearer("secret456token123")); + /// ``` + #[cfg(feature = "auth")] + pub fn authentication(&mut self, auth: T) -> &mut Self + where + Authentication: From, + { + self.message.authentication(auth); self } - ///Sets connection timeout of request. + /// Sets the body for the request. /// - ///# Examples - ///``` - ///use std::time::{Duration, Instant}; - ///use http_req::{request::Request, uri::Uri}; - /// - ///let mut writer = Vec::new(); - ///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///const body: &[u8; 27] = b"field1=value1&field2=value2"; - ///let timeout = Some(Duration::from_secs(3600)); - /// - ///let response = Request::new(&uri) - /// .timeout(timeout) - /// .send(&mut writer) - /// .unwrap(); - ///``` - pub fn timeout(&mut self, timeout: Option) -> &mut Self - where - Duration: From, - { - self.inner.timeout = timeout.map(Duration::from); + /// # Examples + /// ``` + /// use http_req::{request::{Request, Method}, uri::Uri}; + /// use std::convert::TryFrom; + /// + /// let uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// const body: &[u8; 27] = b"field1=value1&field2=value2"; + /// + /// let request = Request::new(&uri) + /// .method(Method::POST) + /// .header("Content-Length", &body.len()) + /// .body(body); + /// ``` + pub fn body(&mut self, body: &'a [u8]) -> &mut Self { + self.message.body(body); self } - ///Sets connect timeout while using internal `TcpStream` instance + /// Sets the connect timeout while using internal `TcpStream` instance. /// - ///- If there is a timeout, it will be passed to - /// [`TcpStream::connect_timeout`][TcpStream::connect_timeout]. - ///- If `None` is provided, [`TcpStream::connect`][TcpStream::connect] will - /// be used. A timeout will still be enforced by the operating system, but - /// the exact value depends on the platform. + /// - If there is a timeout, it will be passed to + /// [`TcpStream::connect_timeout`][TcpStream::connect_timeout]. + /// - If `None` is provided, [`TcpStream::connect`][TcpStream::connect] will + /// be used. A timeout will still be enforced by the operating system, but + /// the exact value depends on the platform. /// - ///[TcpStream::connect]: https://doc.rust-lang.org/std/net/struct.TcpStream.html#method.connect - ///[TcpStream::connect_timeout]: https://doc.rust-lang.org/std/net/struct.TcpStream.html#method.connect_timeout + /// [TcpStream::connect]: https://doc.rust-lang.org/std/net/struct.TcpStream.html#method.connect + /// [TcpStream::connect_timeout]: https://doc.rust-lang.org/std/net/struct.TcpStream.html#method.connect_timeout /// - ///# Examples - ///``` - ///use http_req::{request::Request, uri::Uri}; - ///use std::time::Duration; + /// # Examples + /// ``` + /// use http_req::{request::Request, uri::Uri}; + /// use std::{time::Duration, convert::TryFrom}; /// - ///let mut writer = Vec::new(); - ///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///const time: Option = Some(Duration::from_secs(10)); + /// let uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// const time: Option = Some(Duration::from_secs(10)); /// - ///let response = Request::new(&uri) - /// .connect_timeout(time) - /// .send(&mut writer) - /// .unwrap(); - ///``` + /// let request = Request::new(&uri) + /// .connect_timeout(time); + /// ``` pub fn connect_timeout(&mut self, timeout: Option) -> &mut Self where Duration: From, @@ -796,27 +695,24 @@ impl<'a> Request<'a> { self } - ///Sets read timeout on internal `TcpStream` instance + /// Sets the read timeout on internal `TcpStream` instance. /// - ///`timeout` will be passed to - ///[`TcpStream::set_read_timeout`][TcpStream::set_read_timeout]. + /// `timeout` will be passed to + /// [`TcpStream::set_read_timeout`][TcpStream::set_read_timeout]. /// - ///[TcpStream::set_read_timeout]: https://doc.rust-lang.org/std/net/struct.TcpStream.html#method.set_read_timeout + /// [TcpStream::set_read_timeout]: https://doc.rust-lang.org/std/net/struct.TcpStream.html#method.set_read_timeout /// - ///# Examples - ///``` - ///use http_req::{request::Request, uri::Uri}; - ///use std::time::Duration; + /// # Examples + /// ``` + /// use http_req::{request::Request, uri::Uri}; + /// use std::{time::Duration, convert::TryFrom}; /// - ///let mut writer = Vec::new(); - ///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///const time: Option = Some(Duration::from_secs(15)); + /// let uri: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// const time: Option = Some(Duration::from_secs(15)); /// - ///let response = Request::new(&uri) - /// .read_timeout(time) - /// .send(&mut writer) - /// .unwrap(); - ///``` + /// let request = Request::new(&uri) + /// .read_timeout(time); + /// ``` pub fn read_timeout(&mut self, timeout: Option) -> &mut Self where Duration: From, @@ -825,27 +721,24 @@ impl<'a> Request<'a> { self } - ///Sets write timeout on internal `TcpStream` instance + /// Sets the write timeout on internal `TcpStream` instance. /// - ///`timeout` will be passed to - ///[`TcpStream::set_write_timeout`][TcpStream::set_write_timeout]. + /// `timeout` will be passed to + /// [`TcpStream::set_write_timeout`][TcpStream::set_write_timeout]. /// - ///[TcpStream::set_write_timeout]: https://doc.rust-lang.org/std/net/struct.TcpStream.html#method.set_write_timeout + /// [TcpStream::set_write_timeout]: https://doc.rust-lang.org/std/net/struct.TcpStream.html#method.set_write_timeout /// - ///# Examples - ///``` - ///use http_req::{request::Request, uri::Uri}; - ///use std::time::Duration; + /// # Examples + /// ``` + /// use http_req::{request::Request, uri::Uri}; + /// use std::{time::Duration, convert::TryFrom}; /// - ///let mut writer = Vec::new(); - ///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///const time: Option = Some(Duration::from_secs(5)); + /// let uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// const time: Option = Some(Duration::from_secs(5)); /// - ///let response = Request::new(&uri) - /// .write_timeout(time) - /// .send(&mut writer) - /// .unwrap(); - ///``` + /// let request = Request::new(&uri) + /// .write_timeout(time); + /// ``` pub fn write_timeout(&mut self, timeout: Option) -> &mut Self where Duration: From, @@ -854,137 +747,217 @@ impl<'a> Request<'a> { self } - ///Add a file containing the PEM-encoded certificates that should be added in the trusted root store. + /// Sets the timeout for the entire request. + /// + /// Data is read from a stream until there is no more data to read or the timeout is exceeded. + /// + /// # Examples + /// ``` + /// use http_req::{request::Request, uri::Uri}; + /// use std::{time::Duration, convert::TryFrom}; + /// + /// let uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// const time: Duration = Duration::from_secs(5); + /// + /// let request = Request::new(&uri) + /// .timeout(time); + /// ``` + pub fn timeout(&mut self, timeout: T) -> &mut Self + where + Duration: From, + { + self.timeout = Duration::from(timeout); + self + } + + /// Adds the file containing the PEM-encoded certificates that should be added to the trusted root store. + /// + /// # Examples + /// ``` + /// use http_req::{request::Request, uri::Uri}; + /// use std::{time::Duration, convert::TryFrom, path::Path}; + /// + /// let uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// let path = Path::new("./foo/bar.txt"); + /// + /// let request = Request::new(&uri) + /// .root_cert_file_pem(&path); + /// ``` pub fn root_cert_file_pem(&mut self, file_path: &'a Path) -> &mut Self { self.root_cert_file_pem = Some(file_path); self } - ///Sends HTTP request. + /// Sets the redirect policy for the request. /// - ///Creates `TcpStream` (and wraps it with `TlsStream` if needed). Writes request message - ///to created stream. Returns response for this request. Writes response's body to `writer`. - /// - ///# Examples - ///``` - ///use http_req::{request::Request, uri::Uri}; + /// # Examples + /// ``` + /// use http_req::{request::{Request, RedirectPolicy}, uri::Uri}; + /// use std::{time::Duration, convert::TryFrom, path::Path}; /// - ///let mut writer = Vec::new(); - ///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); + /// let uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - ///let response = Request::new(&uri).send(&mut writer).unwrap(); - ///``` - pub fn send(&self, writer: &mut T) -> Result { - let host = self.inner.uri.host().unwrap_or(""); - let port = self.inner.uri.corr_port(); - let mut stream = match self.connect_timeout { - Some(timeout) => connect_timeout(host, port, timeout)?, - None => TcpStream::connect((host, port))?, - }; + /// let request = Request::new(&uri) + /// .redirect_policy(RedirectPolicy::Limit(5)); + /// ``` + pub fn redirect_policy(&mut self, policy: T) -> &mut Self + where + RedirectPolicy bool>: From, + { + self.redirect_policy = RedirectPolicy::from(policy); + self + } + /// Sends the HTTP request and returns `Response`. + /// + /// This method sets up a stream, writes the request message to it, and processes the response. + /// The connection is closed after processing. If the response indicates a redirect and the policy allows, + /// a new request is sent following the redirection. + /// + /// # Examples + /// ``` + /// use http_req::{request::Request, uri::Uri}; + /// use std::convert::TryFrom; + /// + /// let mut writer = Vec::new(); + /// let uri: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// + /// let response = Request::new(&uri).send(&mut writer).unwrap(); + /// ``` + pub fn send(&mut self, writer: &mut T) -> Result + where + T: Write, + { + // Set up a stream. + let mut stream = Stream::connect(self.message.uri, self.connect_timeout)?; stream.set_read_timeout(self.read_timeout)?; stream.set_write_timeout(self.write_timeout)?; - if self.inner.uri.scheme() == "https" { - let mut cnf = tls::Config::default(); - let cnf = match self.root_cert_file_pem { - Some(p) => cnf.add_root_cert_file_pem(p)?, - None => &mut cnf, - }; - let mut stream = cnf.connect(host, stream)?; - self.inner.send(&mut stream, writer) - } else { - self.inner.send(&mut stream, writer) + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] + { + stream = Stream::try_to_https(stream, self.message.uri, self.root_cert_file_pem)?; } - } -} -///Connects to target host with a timeout -pub fn connect_timeout(host: T, port: u16, timeout: U) -> io::Result -where - Duration: From, - T: AsRef, -{ - let host = host.as_ref(); - let timeout = Duration::from(timeout); - let addrs: Vec<_> = (host, port).to_socket_addrs()?.collect(); - let count = addrs.len(); - - for (idx, addr) in addrs.into_iter().enumerate() { - match TcpStream::connect_timeout(&addr, timeout) { - Ok(stream) => return Ok(stream), - Err(err) => match err.kind() { - io::ErrorKind::TimedOut => return Err(err), - _ => { - if idx + 1 == count { - return Err(err); - } + // Send the request message to the stream. + let request_msg = self.message.parse(); + stream.write_all(&request_msg)?; + + // Set up variables + let deadline = Instant::now() + self.timeout; + let (sender, receiver) = mpsc::channel(); + let (sender_supp, receiver_supp) = mpsc::channel(); + let mut raw_response_head: Vec = Vec::new(); + let mut buf_reader = BufReader::new(stream); + + // Read from the stream and send over data via `sender`. + thread::spawn(move || { + buf_reader.send_head(&sender); + + let params: Vec<&str> = receiver_supp.recv().unwrap_or(Vec::new()); + if !params.is_empty() && params.contains(&"non-empty") { + if params.contains(&"chunked") { + let mut buf_reader = ChunkReader::from(buf_reader); + buf_reader.send_all(&sender); + } else { + buf_reader.send_all(&sender); } - }, - }; - } + } + }); + + // Receive and process `head` of the response. + raw_response_head.receive(&receiver, deadline)?; + let response = Response::from_head(&raw_response_head)?; + + if response.status_code().is_redirect() { + if let Some(location) = response.headers().get("Location") { + if self.redirect_policy.follow(&location) { + let mut raw_uri = location.to_string(); + let uri = if Uri::is_relative(&raw_uri) { + self.message.uri.from_relative(&mut raw_uri) + } else { + Uri::try_from(raw_uri.as_str()) + }?; + + return Request::new(&uri) + .redirect_policy(self.redirect_policy) + .send(writer); + } + } + } + + let params = response.basic_info(&self.message.method).to_vec(); + sender_supp.send(params)?; - Err(io::Error::new( - io::ErrorKind::AddrNotAvailable, - format!("Could not resolve address for {:?}", host), - )) + // Receive and process `body` of the response. + let content_len = response.content_len().unwrap_or(1); + if content_len > 0 { + writer.receive_all(&receiver, deadline)?; + } + + Ok(response) + } } -///Creates and sends GET request. Returns response for this request. +/// Creates and sends a GET request. Returns the response for this request. /// -///# Examples -///``` -///use http_req::request; +/// # Examples +/// ``` +/// use http_req::request; /// -///let mut writer = Vec::new(); -///const uri: &str = "https://www.rust-lang.org/learn"; +/// let mut writer = Vec::new(); +/// const uri: &str = "https://www.rust-lang.org/learn"; /// -///let response = request::get(uri, &mut writer).unwrap(); -///``` -pub fn get, U: Write>(uri: T, writer: &mut U) -> Result { - let uri = uri.as_ref().parse::()?; - +/// let response = request::get(uri, &mut writer).unwrap(); +/// ``` +pub fn get(uri: T, writer: &mut U) -> Result +where + T: AsRef, + U: Write, +{ + let uri = Uri::try_from(uri.as_ref())?; Request::new(&uri).send(writer) } -///Creates and sends HEAD request. Returns response for this request. +/// Creates and sends a HEAD request. Returns the response for this request. /// -///# Examples -///``` -///use http_req::request; +/// # Examples +/// ``` +/// use http_req::request; /// -///const uri: &str = "https://www.rust-lang.org/learn"; -///let response = request::head(uri).unwrap(); -///``` -pub fn head>(uri: T) -> Result { +/// const uri: &str = "https://www.rust-lang.org/learn"; +/// let response = request::head(uri).unwrap(); +/// ``` +pub fn head(uri: T) -> Result +where + T: AsRef, +{ let mut writer = Vec::new(); - let uri = uri.as_ref().parse::()?; + let uri = Uri::try_from(uri.as_ref())?; Request::new(&uri).method(Method::HEAD).send(&mut writer) } -///Creates and sends POST request. Returns response for this request. +/// Creates and sends a POST request. Returns the response for this request. /// -///# Examples -///``` -///use http_req::request; +/// # Examples +/// ``` +/// use http_req::request; /// -///let mut writer = Vec::new(); -///const uri: &str = "https://www.rust-lang.org/learn"; -///const body: &[u8; 27] = b"field1=value1&field2=value2"; +/// let mut writer = Vec::new(); +/// const uri: &str = "https://www.rust-lang.org/learn"; +/// const body: &[u8; 27] = b"field1=value1&field2=value2"; /// -///let response = request::post(uri, body, &mut writer).unwrap(); -///``` -pub fn post, U: Write>( - uri: T, - body: &[u8], - writer: &mut U, -) -> Result { - let uri = uri.as_ref().parse::()?; +/// let response = request::post(uri, body, &mut writer).unwrap(); +/// ``` +pub fn post(uri: T, body: &[u8], writer: &mut U) -> Result +where + T: AsRef, + U: Write, +{ + let uri = Uri::try_from(uri.as_ref())?; Request::new(&uri) .method(Method::POST) - .header("Content-Length", &body.len()) .body(body) .send(writer) } @@ -993,107 +966,99 @@ pub fn post, U: Write>( mod tests { use super::*; use crate::{error::Error, response::StatusCode}; - use std::io::Cursor; + use std::io; const UNSUCCESS_CODE: StatusCode = StatusCode::new(400); const URI: &str = "http://doc.rust-lang.org/std/string/index.html"; const URI_S: &str = "https://doc.rust-lang.org/std/string/index.html"; const BODY: [u8; 14] = [78, 97, 109, 101, 61, 74, 97, 109, 101, 115, 43, 74, 97, 121]; - const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ - Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - Content-Type: text/html\r\n\ - Content-Length: 100\r\n\r\n\ - hello\r\n\r\nhello"; - - const RESPONSE_H: &[u8; 102] = b"HTTP/1.1 200 OK\r\n\ - Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - Content-Type: text/html\r\n\ - Content-Length: 100\r\n\r\n"; - #[test] - fn counter_new() { - let counter = Counter::new(200); - - assert_eq!(counter.count, 0); - assert_eq!(counter.stop, 200); + fn method_display() { + const METHOD: Method = Method::HEAD; + assert_eq!(&format!("{}", METHOD), "HEAD"); } #[test] - fn counter_next() { - let mut counter = Counter::new(5); - - assert_eq!(counter.next(), Some(false)); - assert_eq!(counter.next(), Some(false)); - assert_eq!(counter.next(), Some(false)); - assert_eq!(counter.next(), Some(false)); - assert_eq!(counter.next(), Some(true)); - assert_eq!(counter.next(), Some(false)); - assert_eq!(counter.next(), Some(false)); + #[cfg(feature = "auth")] + fn authentication_basic() { + let auth = Authentication::basic("user", "password123"); + assert_eq!( + auth, + Authentication(AuthenticationType::Basic { + username: "user".to_string(), + password: "password123".to_string() + }) + ); } #[test] - fn copy_data_until() { - let mut reader = Vec::new(); - reader.extend(&RESPONSE[..]); - - let mut reader = Cursor::new(reader); - - let [head, _body] = copy_until( - &mut reader, - &CR_LF_2, - Instant::now() + Duration::from_secs(360), - ) - .unwrap(); - assert_eq!(&head[..], &RESPONSE_H[..]); + #[cfg(feature = "auth")] + fn authentication_baerer() { + let auth = Authentication::bearer("456secret123token"); + assert_eq!( + auth, + Authentication(AuthenticationType::Bearer("456secret123token".to_string())) + ); } #[test] - fn method_display() { - const METHOD: Method = Method::HEAD; - assert_eq!(&format!("{}", METHOD), "HEAD"); + #[cfg(feature = "auth")] + fn authentication_header() { + { + let auth = Authentication::basic("user", "password123"); + let (key, val) = auth.header(); + assert_eq!(key, "Authorization".to_string()); + assert_eq!(val, "Basic dXNlcjpwYXNzd29yZDEyMw==".to_string()); + } + { + let auth = Authentication::bearer("456secret123token"); + let (key, val) = auth.header(); + assert_eq!(key, "Authorization".to_string()); + assert_eq!(val, "Bearer 456secret123token".to_string()); + } } #[test] - fn request_b_new() { - RequestBuilder::new(&URI.parse().unwrap()); - RequestBuilder::new(&URI_S.parse().unwrap()); + fn request_m_new() { + RequestMessage::new(&Uri::try_from(URI).unwrap()); + RequestMessage::new(&Uri::try_from(URI_S).unwrap()); } #[test] - fn request_b_method() { - let uri: Uri = URI.parse().unwrap(); - let mut req = RequestBuilder::new(&uri); + fn request_m_method() { + let uri = Uri::try_from(URI).unwrap(); + let mut req = RequestMessage::new(&uri); let req = req.method(Method::HEAD); assert_eq!(req.method, Method::HEAD); } #[test] - fn request_b_headers() { + fn request_m_headers() { let mut headers = Headers::new(); headers.insert("Accept-Charset", "utf-8"); headers.insert("Accept-Language", "en-US"); headers.insert("Host", "doc.rust-lang.org"); headers.insert("Connection", "Close"); - let uri: Uri = URI.parse().unwrap(); - let mut req = RequestBuilder::new(&uri); + let uri = Uri::try_from(URI).unwrap(); + let mut req = RequestMessage::new(&uri); let req = req.headers(headers.clone()); assert_eq!(req.headers, headers); } #[test] - fn request_b_header() { - let uri: Uri = URI.parse().unwrap(); - let mut req = RequestBuilder::new(&uri); + fn request_m_header() { + let uri = Uri::try_from(URI).unwrap(); + let mut req = RequestMessage::new(&uri); let k = "Connection"; let v = "Close"; let mut expect_headers = Headers::new(); expect_headers.insert("Host", "doc.rust-lang.org"); - expect_headers.insert("Referer", "http://doc.rust-lang.org/std/string/index.html"); + expect_headers.insert("User-Agent", "http_req/0.13.0"); expect_headers.insert(k, v); let req = req.header(k, v); @@ -1102,63 +1067,42 @@ mod tests { } #[test] - fn request_b_body() { - let uri: Uri = URI.parse().unwrap(); - let mut req = RequestBuilder::new(&uri); - let req = req.body(&BODY); - - assert_eq!(req.body, Some(BODY.as_ref())); - } + #[cfg(feature = "auth")] + fn request_m_authentication() { + let uri = Uri::try_from(URI).unwrap(); + let mut req = RequestMessage::new(&uri); + let token = "456secret123token"; + let k = "Authorization"; + let v = "Bearer ".to_string() + token; - #[test] - fn request_b_timeout() { - let uri = URI.parse().unwrap(); - let mut req = RequestBuilder::new(&uri); - let timeout = Some(Duration::from_secs(360)); + let mut expect_headers = Headers::new(); + expect_headers.insert("Host", "doc.rust-lang.org"); + expect_headers.insert("User-Agent", "http_req/0.13.0"); + expect_headers.insert(k, &v); - req.timeout(timeout); - assert_eq!(req.timeout, timeout); - } + let req = req.authentication(Authentication::bearer(token)); - #[ignore] - #[test] - fn request_b_send() { - let mut writer = Vec::new(); - let uri: Uri = URI.parse().unwrap(); - let mut stream = TcpStream::connect((uri.host().unwrap_or(""), uri.corr_port())).unwrap(); - - RequestBuilder::new(&URI.parse().unwrap()) - .header("Connection", "Close") - .send(&mut stream, &mut writer) - .unwrap(); + assert_eq!(req.headers, expect_headers); } - #[ignore] #[test] - fn request_b_send_secure() { - let mut writer = Vec::new(); - let uri: Uri = URI_S.parse().unwrap(); - - let stream = TcpStream::connect((uri.host().unwrap_or(""), uri.corr_port())).unwrap(); - let mut secure_stream = tls::Config::default() - .connect(uri.host().unwrap_or(""), stream) - .unwrap(); + fn request_m_body() { + let uri = Uri::try_from(URI).unwrap(); + let mut req = RequestMessage::new(&uri); + let req = req.body(&BODY); - RequestBuilder::new(&URI_S.parse().unwrap()) - .header("Connection", "Close") - .send(&mut secure_stream, &mut writer) - .unwrap(); + assert_eq!(req.body, Some(BODY.as_ref())); } #[test] - fn request_b_parse_msg() { - let uri = URI.parse().unwrap(); - let req = RequestBuilder::new(&uri); + fn request_m_parse() { + let uri = Uri::try_from(URI).unwrap(); + let req = RequestMessage::new(&uri); const DEFAULT_MSG: &str = "GET /std/string/index.html HTTP/1.1\r\n\ - Referer: http://doc.rust-lang.org/std/string/index.html\r\n\ - Host: doc.rust-lang.org\r\n\r\n"; - let msg = req.parse_msg(); + Host: doc.rust-lang.org\r\n\ + User-Agent: http_req/0.13.0\r\n\r\n"; + let msg = req.parse(); let msg = String::from_utf8_lossy(&msg).into_owned(); for line in DEFAULT_MSG.lines() { @@ -1172,17 +1116,17 @@ mod tests { #[test] fn request_new() { - let uri = URI.parse().unwrap(); + let uri = Uri::try_from(URI).unwrap(); Request::new(&uri); } #[test] fn request_method() { - let uri = URI.parse().unwrap(); + let uri = Uri::try_from(URI).unwrap(); let mut req = Request::new(&uri); req.method(Method::HEAD); - assert_eq!(req.inner.method, Method::HEAD); + assert_eq!(req.message.method, Method::HEAD); } #[test] @@ -1193,53 +1137,43 @@ mod tests { headers.insert("Host", "doc.rust-lang.org"); headers.insert("Connection", "Close"); - let uri: Uri = URI.parse().unwrap(); + let uri = Uri::try_from(URI).unwrap(); let mut req = Request::new(&uri); let req = req.headers(headers.clone()); - assert_eq!(req.inner.headers, headers); + assert_eq!(req.message.headers, headers); } #[test] fn request_header() { - let uri: Uri = URI.parse().unwrap(); + let uri = Uri::try_from(URI).unwrap(); let mut req = Request::new(&uri); let k = "Accept-Language"; let v = "en-US"; let mut expect_headers = Headers::new(); expect_headers.insert("Host", "doc.rust-lang.org"); - expect_headers.insert("Referer", "http://doc.rust-lang.org/std/string/index.html"); expect_headers.insert("Connection", "Close"); + expect_headers.insert("User-Agent", "http_req/0.13.0"); expect_headers.insert(k, v); let req = req.header(k, v); - assert_eq!(req.inner.headers, expect_headers); + assert_eq!(req.message.headers, expect_headers); } #[test] fn request_body() { - let uri = URI.parse().unwrap(); + let uri = Uri::try_from(URI).unwrap(); let mut req = Request::new(&uri); let req = req.body(&BODY); - assert_eq!(req.inner.body, Some(BODY.as_ref())); - } - - #[test] - fn request_timeout() { - let uri = URI.parse().unwrap(); - let mut request = Request::new(&uri); - let timeout = Some(Duration::from_secs(360)); - - request.timeout(timeout); - assert_eq!(request.inner.timeout, timeout); + assert_eq!(req.message.body, Some(BODY.as_ref())); } #[test] fn request_connect_timeout() { - let uri = URI.parse().unwrap(); + let uri = Uri::try_from(URI).unwrap(); let mut request = Request::new(&uri); request.connect_timeout(Some(Duration::from_nanos(1))); @@ -1252,41 +1186,38 @@ mod tests { }; } - #[ignore] #[test] fn request_read_timeout() { - let uri = URI.parse().unwrap(); + let uri = Uri::try_from(URI).unwrap(); let mut request = Request::new(&uri); - request.read_timeout(Some(Duration::from_nanos(1))); - - assert_eq!(request.read_timeout, Some(Duration::from_nanos(1))); + request.read_timeout(Some(Duration::from_nanos(100))); - let err = request.send(&mut io::sink()).unwrap_err(); - match err { - Error::IO(err) => match err.kind() { - io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut => {} - other => panic!( - "Expected error kind to be one of WouldBlock/TimedOut, got: {:?}", - other - ), - }, - other => panic!("Expected error to be io::Error, got: {:?}", other), - }; + assert_eq!(request.read_timeout, Some(Duration::from_nanos(100))); } #[test] fn request_write_timeout() { - let uri = URI.parse().unwrap(); + let uri = Uri::try_from(URI).unwrap(); let mut request = Request::new(&uri); request.write_timeout(Some(Duration::from_nanos(100))); assert_eq!(request.write_timeout, Some(Duration::from_nanos(100))); } + #[test] + fn request_timeout() { + let uri = Uri::try_from(URI).unwrap(); + let mut request = Request::new(&uri); + let timeout = Duration::from_secs(360); + + request.timeout(timeout); + assert_eq!(request.timeout, timeout); + } + #[test] fn request_send() { let mut writer = Vec::new(); - let uri = URI.parse().unwrap(); + let uri = Uri::try_from(URI).unwrap(); let res = Request::new(&uri).send(&mut writer).unwrap(); assert_ne!(res.status_code(), UNSUCCESS_CODE); @@ -1294,7 +1225,7 @@ mod tests { #[ignore] #[test] - fn request_get() { + fn fn_get() { let mut writer = Vec::new(); let res = get(URI, &mut writer).unwrap(); @@ -1308,7 +1239,7 @@ mod tests { #[ignore] #[test] - fn request_head() { + fn fn_head() { let res = head(URI).unwrap(); assert_ne!(res.status_code(), UNSUCCESS_CODE); @@ -1318,7 +1249,7 @@ mod tests { #[ignore] #[test] - fn request_post() { + fn fn_post() { let mut writer = Vec::new(); let res = post(URI, &BODY, &mut writer).unwrap(); diff --git a/src/response.rs b/src/response.rs index 2520f86..4ba9133 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,6 +1,8 @@ //! parsing server response + use crate::{ error::{Error, ParseErr}, + request::Method, uri::Uri, }; use std::{ @@ -13,9 +15,9 @@ use unicase::Ascii; pub(crate) const CR_LF_2: [u8; 4] = [13, 10, 13, 10]; -///Represents an HTTP response. +/// Represents an HTTP response. /// -///It contains `Headers` and `Status` parsed from response. +/// It contains `Headers` and `Status` parsed from response. #[derive(Debug, PartialEq, Clone)] pub struct Response { status: Status, @@ -23,19 +25,19 @@ pub struct Response { } impl Response { - ///Creates new `Response` with head - status and headers - parsed from a slice of bytes + /// Creates a new `Response` with head - status and headers - parsed from a slice of bytes. /// - ///# Examples - ///``` - ///use http_req::response::Response; + /// # Examples + /// ``` + /// use http_req::response::Response; /// - ///const HEAD: &[u8; 102] = b"HTTP/1.1 200 OK\r\n\ - /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - /// Content-Type: text/html\r\n\ - /// Content-Length: 100\r\n\r\n"; + /// const HEAD: &[u8; 102] = b"HTTP/1.1 200 OK\r\n\ + /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + /// Content-Type: text/html\r\n\ + /// Content-Length: 100\r\n\r\n"; /// - ///let response = Response::from_head(HEAD).unwrap(); - ///``` + /// let response = Response::from_head(HEAD).unwrap(); + /// ``` pub fn from_head(head: &[u8]) -> Result { let mut head = str::from_utf8(head)?.splitn(2, '\n'); @@ -45,142 +47,196 @@ impl Response { Ok(Response { status, headers }) } - ///Parses `Response` from slice of bytes. Writes it's body to `writer`. + /// Parses `Response` from slice of bytes. Writes its body to `writer`. /// - ///# Examples - ///``` - ///use http_req::response::Response; + /// # Examples + /// ``` + /// use http_req::response::Response; /// - ///const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ - /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - /// Content-Type: text/html\r\n\ - /// Content-Length: 100\r\n\r\n\ - /// hello\r\n\r\nhello"; - ///let mut body = Vec::new(); + /// const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ + /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + /// Content-Type: text/html\r\n\ + /// Content-Length: 100\r\n\r\n\ + /// hello\r\n\r\nhello"; + /// let mut body = Vec::new(); /// - ///let response = Response::try_from(RESPONSE, &mut body).unwrap(); - ///``` - pub fn try_from(res: &[u8], writer: &mut T) -> Result { + /// let response = Response::try_from(RESPONSE, &mut body).unwrap(); + /// ``` + pub fn try_from(res: &[u8], writer: &mut T) -> Result + where + T: Write, + { if res.is_empty() { - Err(Error::Parse(ParseErr::Empty)) - } else { - let pos = match find_slice(res, &CR_LF_2) { - Some(v) => v, - None => res.len(), - }; + return Err(Error::Parse(ParseErr::Empty)); + } - let response = Self::from_head(&res[..pos])?; - writer.write_all(&res[pos..])?; + let pos = match find_slice(res, &CR_LF_2) { + Some(v) => v, + None => res.len(), + }; + + // Attempt to parse the headers and status from the response + let response = Self::from_head(&res[..pos])?; - Ok(response) + // Write any remaining part of the bytes (assumed body) into writer + if pos < res.len() { + writer.write_all(&res[pos..])?; } + + Ok(response) } - ///Returns status code of this `Response`. + /// Returns status code of this `Response`. /// - ///# Examples - ///``` - ///use http_req::response::{Response, StatusCode}; + /// # Examples + /// ``` + /// use http_req::response::{Response, StatusCode}; /// - ///const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ - /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - /// Content-Type: text/html\r\n\ - /// Content-Length: 100\r\n\r\n\ - /// hello\r\n\r\nhello"; - ///let mut body = Vec::new(); + /// const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ + /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + /// Content-Type: text/html\r\n\ + /// Content-Length: 100\r\n\r\n\ + /// hello\r\n\r\nhello"; + /// let mut body = Vec::new(); /// - ///let response = Response::try_from(RESPONSE, &mut body).unwrap(); - ///assert_eq!(response.status_code(), StatusCode::new(200)); - ///``` - pub fn status_code(&self) -> StatusCode { + /// let response = Response::try_from(RESPONSE, &mut body).unwrap(); + /// assert_eq!(response.status_code(), StatusCode::new(200)); + /// ``` + pub const fn status_code(&self) -> StatusCode { self.status.code } - ///Returns HTTP version of this `Response`. + /// Returns HTTP version of this `Response`. /// - ///# Examples - ///``` - ///use http_req::response::Response; + /// # Examples + /// ``` + /// use http_req::response::Response; /// - ///const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ - /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - /// Content-Type: text/html\r\n\ - /// Content-Length: 100\r\n\r\n\ - /// hello\r\n\r\nhello"; - ///let mut body = Vec::new(); + /// const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ + /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + /// Content-Type: text/html\r\n\ + /// Content-Length: 100\r\n\r\n\ + /// hello\r\n\r\nhello"; + /// let mut body = Vec::new(); /// - ///let response = Response::try_from(RESPONSE, &mut body).unwrap(); - ///assert_eq!(response.version(), "HTTP/1.1"); - ///``` + /// let response = Response::try_from(RESPONSE, &mut body).unwrap(); + /// assert_eq!(response.version(), "HTTP/1.1"); + /// ``` pub fn version(&self) -> &str { &self.status.version } - ///Returns reason of this `Response`. + /// Returns reason of this `Response`. /// - ///# Examples - ///``` - ///use http_req::response::Response; + /// # Examples + /// ``` + /// use http_req::response::Response; /// - ///const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ - /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - /// Content-Type: text/html\r\n\ - /// Content-Length: 100\r\n\r\n\ - /// hello\r\n\r\nhello"; - ///let mut body = Vec::new(); + /// const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ + /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + /// Content-Type: text/html\r\n\ + /// Content-Length: 100\r\n\r\n\ + /// hello\r\n\r\nhello"; + /// let mut body = Vec::new(); /// - ///let response = Response::try_from(RESPONSE, &mut body).unwrap(); - ///assert_eq!(response.reason(), "OK"); - ///``` + /// let response = Response::try_from(RESPONSE, &mut body).unwrap(); + /// assert_eq!(response.reason(), "OK"); + /// ``` pub fn reason(&self) -> &str { &self.status.reason } - ///Returns headers of this `Response`. + /// Returns headers of this `Response`. /// - ///# Examples - ///``` - ///use http_req::response::Response; + /// # Examples + /// ``` + /// use http_req::response::Response; /// - ///const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ - /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - /// Content-Type: text/html\r\n\ - /// Content-Length: 100\r\n\r\n\ - /// hello\r\n\r\nhello"; - ///let mut body = Vec::new(); + /// const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ + /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + /// Content-Type: text/html\r\n\ + /// Content-Length: 100\r\n\r\n\ + /// hello\r\n\r\nhello"; + /// let mut body = Vec::new(); /// - ///let response = Response::try_from(RESPONSE, &mut body).unwrap(); - ///let headers = response.headers(); - ///``` - pub fn headers(&self) -> &Headers { + /// let response = Response::try_from(RESPONSE, &mut body).unwrap(); + /// let headers = response.headers(); + /// ``` + pub const fn headers(&self) -> &Headers { &self.headers } - ///Returns length of the content of this `Response` as a `Option`, according to information - ///included in headers. If there is no such an information, returns `None`. + /// Returns length of the content of this `Response` as an `Option`, according to information + /// included in headers. If there is no such information, returns `None`. /// - ///# Examples - ///``` - ///use http_req::response::Response; + /// # Examples + /// ``` + /// use http_req::response::Response; /// - ///const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ - /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - /// Content-Type: text/html\r\n\ - /// Content-Length: 100\r\n\r\n\ - /// hello\r\n\r\nhello"; - ///let mut body = Vec::new(); + /// const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ + /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + /// Content-Type: text/html\r\n\ + /// Content-Length: 100\r\n\r\n\ + /// hello\r\n\r\nhello"; + /// let mut body = Vec::new(); /// - ///let response = Response::try_from(RESPONSE, &mut body).unwrap(); - ///assert_eq!(response.content_len().unwrap(), 100); - ///``` + /// let response = Response::try_from(RESPONSE, &mut body).unwrap(); + /// assert_eq!(response.content_len().unwrap(), 100); + /// ``` pub fn content_len(&self) -> Option { self.headers() .get("Content-Length") .and_then(|len| len.parse().ok()) } + + /// Checks if Transfer-Encoding includes "chunked". + /// + /// # Examples + /// ``` + /// use http_req::response::Response; + /// + /// const RESPONSE: &[u8; 157] = b"HTTP/1.1 200 OK\r\n\ + /// Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + /// Content-Type: text/html\r\n\ + /// Transfer-Encoding: chunked\r\n\ + /// Content-Length: 100\r\n\r\n\ + /// hello\r\n\r\nhello"; + /// let mut body = Vec::new(); + /// + /// let response = Response::try_from(RESPONSE, &mut body).unwrap(); + /// assert!(response.is_chunked()); + /// ``` + pub fn is_chunked(&self) -> bool { + self.headers() + .get("Transfer-Encoding") + .is_some_and(|encodings| encodings.contains("chunked")) + } + + /// Returns basic information about the response as an array, including: + /// - chunked -> Transfer-Encoding includes "chunked" + /// - non-empty -> Content-Length is greater than 0 (or unknown) and method is not HEAD + pub fn basic_info<'a>(&self, method: &Method) -> [&'a str; 2] { + let mut params = [""; 2]; + let content_len = self.content_len().unwrap_or(1); + + if self.is_chunked() { + params[0] = "chunked"; + } + + if content_len > 0 && method != &Method::HEAD { + params[1] = "non-empty"; + } + + params + } } -///Status of HTTP response +/// Represents the status line of an HTTP response. +/// +/// The `Status` encapsulates 3 components: +/// - `version`: an HTTP version (e.g. "HTTP/1.1"). +/// - `code`: a status code (e.g. 200 for OK). +/// - `reason`: a reason phrase associated with the status code (e.g. "OK"). #[derive(PartialEq, Debug, Clone)] pub struct Status { version: String, @@ -188,6 +244,13 @@ pub struct Status { reason: String, } +impl Status { + /// Creates a new `Status` from a version, a code, and a reason. + pub fn new(version: &str, code: StatusCode, reason: &str) -> Status { + Status::from((version, code, reason)) + } +} + impl From<(T, U, V)> for Status where T: ToString, @@ -207,11 +270,13 @@ impl str::FromStr for Status { type Err = ParseErr; fn from_str(status_line: &str) -> Result { - let mut status_line = status_line.trim().splitn(3, ' '); + let mut parts = status_line.trim().splitn(3, ' '); + + let version = parts.next().ok_or(ParseErr::StatusErr)?; + let code: StatusCode = parts.next().ok_or(ParseErr::StatusErr)?.parse()?; - let version = status_line.next().ok_or(ParseErr::StatusErr)?; - let code: StatusCode = status_line.next().ok_or(ParseErr::StatusErr)?.parse()?; - let reason = match status_line.next() { + // Check if the reason phrase is provided + let reason = match parts.next() { Some(reason) => reason, None => code.reason().unwrap_or("Unknown"), }; @@ -220,98 +285,98 @@ impl str::FromStr for Status { } } -///Wrapper around HashMap, String> with additional functionality for parsing HTTP headers +/// Wrapper around `HashMap, String>` with additional functionality for parsing HTTP headers. /// -///# Example -///``` -///use http_req::response::Headers; +/// # Examples +/// ``` +/// use http_req::response::Headers; /// -///let mut headers = Headers::new(); -///headers.insert("Connection", "Close"); +/// let mut headers = Headers::new(); +/// headers.insert("Connection", "Close"); /// -///assert_eq!(headers.get("Connection"), Some(&"Close".to_string())) -///``` +/// assert_eq!(headers.get("Connection"), Some(&"Close".to_string())) +/// ``` #[derive(Debug, PartialEq, Clone, Default)] pub struct Headers(HashMap, String>); impl Headers { - ///Creates an empty `Headers`. + /// Creates an empty `Headers`. /// - ///The headers are initially created with a capacity of 0, so they will not allocate until - ///it is first inserted into. + /// The headers are initially created with a capacity of 0, so they will not allocate until + /// it is first inserted into. /// - ///# Examples - ///``` - ///use http_req::response::Headers; + /// # Examples + /// ``` + /// use http_req::response::Headers; /// - ///let mut headers = Headers::new(); - ///``` + /// let mut headers = Headers::new(); + /// ``` pub fn new() -> Headers { Headers(HashMap::new()) } - ///Creates empty `Headers` with the specified capacity. + /// Creates empty `Headers` with the specified capacity. /// - ///The headers will be able to hold at least capacity elements without reallocating. - ///If capacity is 0, the headers will not allocate. + /// The headers will be able to hold at least capacity elements without reallocating. + /// If capacity is 0, the headers will not allocate. /// - ///# Examples - ///``` - ///use http_req::response::Headers; + /// # Examples + /// ``` + /// use http_req::response::Headers; /// - ///let mut headers = Headers::with_capacity(200); - ///``` + /// let mut headers = Headers::with_capacity(200); + /// ``` pub fn with_capacity(capacity: usize) -> Headers { Headers(HashMap::with_capacity(capacity)) } - ///An iterator visiting all key-value pairs in arbitrary order. - ///The iterator's element type is (&Ascii, &String). + /// An iterator visiting all key-value pairs in arbitrary order. + /// The iterator's element type is `(&Ascii, &String)`. /// - ///# Examples - ///``` - ///use http_req::response::Headers; + /// # Examples + /// ``` + /// use http_req::response::Headers; /// - ///let mut headers = Headers::new(); - ///headers.insert("Accept-Charset", "utf-8"); - ///headers.insert("Accept-Language", "en-US"); - ///headers.insert("Connection", "Close"); + /// let mut headers = Headers::new(); + /// headers.insert("Accept-Charset", "utf-8"); + /// headers.insert("Accept-Language", "en-US"); + /// headers.insert("Connection", "Close"); /// - ///let mut iterator = headers.iter(); - ///``` - pub fn iter(&self) -> hash_map::Iter, String> { + /// let mut iterator = headers.iter(); + /// ``` + pub fn iter(&self) -> hash_map::Iter<'_, Ascii, String> { self.0.iter() } - ///Returns a reference to the value corresponding to the key. + /// Returns a reference to the value corresponding to the key. /// - ///# Examples - ///``` - ///use http_req::response::Headers; + /// # Examples + /// ``` + /// use http_req::response::Headers; /// - ///let mut headers = Headers::new(); - ///headers.insert("Accept-Charset", "utf-8"); + /// let mut headers = Headers::new(); + /// headers.insert("Accept-Charset", "utf-8"); /// - ///assert_eq!(headers.get("Accept-Charset"), Some(&"utf-8".to_string())) - ///``` + /// assert_eq!(headers.get("Accept-Charset"), Some(&"utf-8".to_string())) + /// ``` pub fn get(&self, k: &T) -> Option<&std::string::String> { self.0.get(&Ascii::new(k.to_string())) } - ///Inserts a key-value pair into the headers. + /// Inserts a key-value pair into the headers. /// - ///If the headers did not have this key present, None is returned. + /// If the headers did not have this key present, None is returned. /// - ///If the headers did have this key present, the value is updated, and the old value is returned. - ///The key is not updated, though; this matters for types that can be == without being identical. + /// If the headers did have this key present, the value is updated, and the old value is returned. + /// The key is not updated, though; this matters for types that can be == without being identical. /// - ///# Examples - ///``` - ///use http_req::response::Headers; + /// # Examples + /// ``` + /// use http_req::response::Headers; /// - ///let mut headers = Headers::new(); - ///headers.insert("Accept-Language", "en-US"); - ///``` + /// let mut headers = Headers::new(); + /// headers.insert("Accept-Language", "en-US"); + /// ``` pub fn insert(&mut self, key: &T, val: &U) -> Option where T: ToString + ?Sized, @@ -320,20 +385,38 @@ impl Headers { self.0.insert(Ascii::new(key.to_string()), val.to_string()) } - ///Creates default headers for a HTTP request + /// Inserts key-value pair into the headers and takes ownership over them. /// - ///# Examples - ///``` - ///use http_req::{response::Headers, uri::Uri}; + /// If the headers did not have this key present, None is returned. /// - ///let uri: Uri = "https://www.rust-lang.org/learn".parse().unwrap(); - ///let headers = Headers::default_http(&uri); - ///``` - pub fn default_http(uri: &Uri) -> Headers { - let mut headers = Headers::with_capacity(4); + /// If the headers did have this key present, the value is updated, and the old value is returned. + /// The key is not updated, though; this matters for types that can be == without being identical. + /// + /// # Examples + /// ``` + /// use http_req::response::Headers; + /// + /// let mut headers = Headers::new(); + /// headers.insert_raw("Accept-Language".to_string(), "en-US".to_string()); + /// ``` + pub fn insert_raw(&mut self, key: String, val: String) -> Option { + self.0.insert(Ascii::new(key), val) + } + /// Creates default headers for a HTTP request + /// + /// # Examples + /// ``` + /// use http_req::{response::Headers, uri::Uri}; + /// use std::convert::TryFrom; + /// + /// let uri: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// let headers = Headers::default_http(&uri); + /// ``` + pub fn default_http(uri: &Uri) -> Headers { + let mut headers = Headers::with_capacity(10); headers.insert("Host", &uri.host_header().unwrap_or_default()); - headers.insert("Referer", &uri); + headers.insert("User-Agent", "http_req/0.13.0"); headers } @@ -345,12 +428,17 @@ impl str::FromStr for Headers { fn from_str(s: &str) -> Result { let headers = s.trim(); + if headers.is_empty() { + return Err(ParseErr::HeadersErr); + } + if headers.lines().all(|e| e.contains(':')) { let headers = headers .lines() .map(|elem| { let idx = elem.find(':').unwrap(); let (key, value) = elem.split_at(idx); + (Ascii::new(key.to_string()), value[1..].trim().to_string()) }) .collect(); @@ -385,119 +473,122 @@ impl fmt::Display for Headers { } } -///Code sent by a server in response to a client's request. +/// Represents an HTTP status code. /// -///# Example -///``` -///use http_req::response::StatusCode; +/// An HTTP status code is a three-digit number sent by a server +/// in response to a client's request. /// -///const code: StatusCode = StatusCode::new(200); -///assert!(code.is_success()) -///``` +/// # Examples +/// ``` +/// use http_req::response::StatusCode; +/// +/// const code: StatusCode = StatusCode::new(200); +/// assert!(code.is_success()) +/// ``` #[derive(Debug, PartialEq, Clone, Copy)] pub struct StatusCode(u16); impl StatusCode { - ///Creates new StatusCode from `u16` value. + /// Creates a new `StatusCode` from `u16` value. /// - ///# Examples - ///``` - ///use http_req::response::StatusCode; + /// # Examples + /// ``` + /// use http_req::response::StatusCode; /// - ///const code: StatusCode = StatusCode::new(200); - ///``` + /// const code: StatusCode = StatusCode::new(200); + /// ``` pub const fn new(code: u16) -> StatusCode { StatusCode(code) } - ///Checks if this `StatusCode` is within 100-199, which indicates that it's Informational. + /// Checks if this `StatusCode` is within 100-199, which indicates that it's Informational. /// - ///# Examples - ///``` - ///use http_req::response::StatusCode; + /// # Examples + /// ``` + /// use http_req::response::StatusCode; /// - ///const code: StatusCode = StatusCode::new(101); - ///assert!(code.is_info()) - ///``` - pub fn is_info(self) -> bool { + /// const code: StatusCode = StatusCode::new(101); + /// assert!(code.is_info()) + /// ``` + pub const fn is_info(self) -> bool { self.0 >= 100 && self.0 < 200 } - ///Checks if this `StatusCode` is within 200-299, which indicates that it's Successful. + /// Checks if this `StatusCode` is within 200-299, which indicates that it's Successful. /// - ///# Examples - ///``` - ///use http_req::response::StatusCode; + /// # Examples + /// ``` + /// use http_req::response::StatusCode; /// - ///const code: StatusCode = StatusCode::new(204); - ///assert!(code.is_success()) - ///``` - pub fn is_success(self) -> bool { + /// const code: StatusCode = StatusCode::new(204); + /// assert!(code.is_success()) + /// ``` + pub const fn is_success(self) -> bool { self.0 >= 200 && self.0 < 300 } - ///Checks if this `StatusCode` is within 300-399, which indicates that it's Redirection. + /// Checks if this `StatusCode` is within 300-399, which indicates that it's Redirection. /// - ///# Examples - ///``` - ///use http_req::response::StatusCode; + /// # Examples + /// ``` + /// use http_req::response::StatusCode; /// - ///const code: StatusCode = StatusCode::new(301); - ///assert!(code.is_redirect()) - ///``` - pub fn is_redirect(self) -> bool { + /// const code: StatusCode = StatusCode::new(301); + /// assert!(code.is_redirect()) + /// ``` + pub const fn is_redirect(self) -> bool { self.0 >= 300 && self.0 < 400 } - ///Checks if this `StatusCode` is within 400-499, which indicates that it's Client Error. + /// Checks if this `StatusCode` is within 400-499, which indicates that it's Client Error. /// - ///# Examples - ///``` - ///use http_req::response::StatusCode; + /// # Examples + /// ``` + /// use http_req::response::StatusCode; /// - ///const code: StatusCode = StatusCode::new(400); - ///assert!(code.is_client_err()) - ///``` - pub fn is_client_err(self) -> bool { + /// const code: StatusCode = StatusCode::new(400); + /// assert!(code.is_client_err()) + /// ``` + pub const fn is_client_err(self) -> bool { self.0 >= 400 && self.0 < 500 } - ///Checks if this `StatusCode` is within 500-599, which indicates that it's Server Error. + /// Checks if this `StatusCode` is within 500-599, which indicates that it's Server Error. /// - ///# Examples - ///``` - ///use http_req::response::StatusCode; + /// # Examples + /// ``` + /// use http_req::response::StatusCode; /// - ///const code: StatusCode = StatusCode::new(503); - ///assert!(code.is_server_err()) - ///``` - pub fn is_server_err(self) -> bool { + /// const code: StatusCode = StatusCode::new(503); + /// assert!(code.is_server_err()) + /// ``` + pub const fn is_server_err(self) -> bool { self.0 >= 500 && self.0 < 600 } - ///Checks this `StatusCode` using closure `f` + /// Checks this `StatusCode` using closure `f` /// - ///# Examples - ///``` - ///use http_req::response::StatusCode; + /// # Examples + /// ``` + /// use http_req::response::StatusCode; /// - ///const code: StatusCode = StatusCode::new(203); - ///assert!(code.is(|i| i > 199 && i < 250)) - ///``` + /// const code: StatusCode = StatusCode::new(203); + /// assert!(code.is(|i| i > 199 && i < 250)) + /// ``` pub fn is bool>(self, f: F) -> bool { f(self.0) } - ///Returns `Reason-Phrase` corresponding to this `StatusCode` + /// Returns `Reason-Phrase` corresponding to this `StatusCode` /// - ///# Examples - ///``` - ///use http_req::response::StatusCode; + /// # Examples + /// ``` + /// use http_req::response::StatusCode; /// - ///const code: StatusCode = StatusCode::new(200); - ///assert_eq!(code.reason(), Some("OK")) - ///``` - pub fn reason(self) -> Option<&'static str> { + /// const code: StatusCode = StatusCode::new(200); + /// assert_eq!(code.reason(), Some("OK")) + /// ``` + pub const fn reason(&self) -> Option<&str> { let reason = match self.0 { 100 => "Continue", 101 => "Switching Protocols", @@ -596,7 +687,7 @@ impl str::FromStr for StatusCode { } } -///Finds elements slice `e` inside slice `data`. Returns position of the end of first match. +/// Finds elements slice `e` inside slice `data`. Returns position of the end of first match. pub fn find_slice(data: &[T], e: &[T]) -> Option where [T]: PartialEq, @@ -615,16 +706,23 @@ where #[cfg(test)] mod tests { use super::*; + use std::convert::TryFrom; const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ - Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - Content-Type: text/html\r\n\ - Content-Length: 100\r\n\r\n\ - hello\r\n\r\nhello"; + Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + Content-Type: text/html\r\n\ + Content-Length: 100\r\n\r\n\ + hello\r\n\r\nhello"; const RESPONSE_H: &[u8; 102] = b"HTTP/1.1 200 OK\r\n\ - Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ - Content-Type: text/html\r\n\ - Content-Length: 100\r\n\r\n"; + Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + Content-Type: text/html\r\n\ + Content-Length: 100\r\n\r\n"; + const RESPONSE_C: &[u8; 157] = b"HTTP/1.1 200 OK\r\n\ + Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + Content-Type: text/html\r\n\ + Transfer-Encoding: chunked\r\n\ + Content-Length: 100\r\n\r\n\ + hello\r\n\r\nhello"; const BODY: &[u8; 27] = b"hello\r\n\r\nhello"; const STATUS_LINE: &str = "HTTP/1.1 200 OK"; @@ -787,13 +885,11 @@ mod tests { #[test] fn headers_default_http() { - let uri = "http://doc.rust-lang.org/std/string/index.html" - .parse() - .unwrap(); + let uri = Uri::try_from("http://doc.rust-lang.org/std/string/index.html").unwrap(); let mut headers = Headers::with_capacity(4); headers.insert("Host", "doc.rust-lang.org"); - headers.insert("Referer", "http://doc.rust-lang.org/std/string/index.html"); + headers.insert("User-Agent", "http_req/0.13.0"); assert_eq!(Headers::default_http(&uri), headers); } @@ -951,6 +1047,41 @@ mod tests { assert_eq!(res.content_len(), Some(100)); } + #[test] + fn res_is_chunked() { + { + let mut writer = Vec::with_capacity(101); + let res = Response::try_from(RESPONSE, &mut writer).unwrap(); + + assert!(!res.is_chunked()); + } + { + let mut writer = Vec::with_capacity(101); + let res = Response::try_from(RESPONSE_C, &mut writer).unwrap(); + + assert!(res.is_chunked()); + } + } + + #[test] + fn res_basic_info() { + { + let mut writer = Vec::with_capacity(101); + let res = Response::try_from(RESPONSE, &mut writer).unwrap(); + let basic_info = res.basic_info(&Method::GET); + + assert!(basic_info.contains(&"non-empty")); + } + { + let mut writer = Vec::with_capacity(101); + let res = Response::try_from(RESPONSE_C, &mut writer).unwrap(); + let basic_info = res.basic_info(&Method::GET); + + assert!(basic_info.contains(&"non-empty")); + assert!(basic_info.contains(&"chunked")); + } + } + #[test] fn res_body() { { diff --git a/src/stream.rs b/src/stream.rs new file mode 100644 index 0000000..851bf6b --- /dev/null +++ b/src/stream.rs @@ -0,0 +1,535 @@ +//! TCP stream + +#[cfg(any(feature = "native-tls", feature = "rust-tls"))] +use crate::tls::{self, Conn}; +use crate::{ + error::{Error, ParseErr}, + uri::Uri, + CR_LF, LF, +}; +#[cfg(any(feature = "native-tls", feature = "rust-tls"))] +use std::path::Path; +use std::{ + io::{self, BufRead, Read, Write}, + net::{TcpStream, ToSocketAddrs}, + sync::mpsc::{Receiver, RecvTimeoutError, Sender}, + time::{Duration, Instant}, +}; + +const BUF_SIZE: usize = 16 * 1000; + +/// Wrapper around TCP stream for HTTP and HTTPS protocols. +/// Allows to perform common operations on underlying stream. +#[derive(Debug)] +pub enum Stream { + Http(TcpStream), + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] + Https(Conn), +} + +impl Stream { + /// Opens a TCP connection to a remote host with a connection timeout (if specified). + pub fn connect(uri: &Uri, connect_timeout: Option) -> Result { + let host = uri.host().ok_or(Error::Parse(ParseErr::UriErr))?; + let port = uri.corr_port(); + + let stream = match connect_timeout { + Some(timeout) => connect_with_timeout(host, port, timeout)?, + None => TcpStream::connect((host, port))?, + }; + + Ok(Stream::Http(stream)) + } + + /// Tries to establish a secure connection over TLS. + /// + /// Checks if `uri` scheme denotes a HTTPS protocol: + /// - If yes, attempts to establish a secure connection + /// - Otherwise, returns the `stream` without any modification + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] + pub fn try_to_https( + stream: Stream, + uri: &Uri, + root_cert_file_pem: Option<&Path>, + ) -> Result { + match stream { + Stream::Http(http_stream) => { + if uri.scheme() == "https" { + let host = uri.host().ok_or(Error::Parse(ParseErr::UriErr))?; + let mut cnf = tls::Config::default(); + + let cnf = match root_cert_file_pem { + Some(p) => cnf.add_root_cert_file_pem(p)?, + None => &mut cnf, + }; + + let stream = cnf.connect(host, http_stream)?; + Ok(Stream::Https(stream)) + } else { + Ok(Stream::Http(http_stream)) + } + } + Stream::Https(_) => Ok(stream), + } + } + + /// Sets the read timeout on the underlying TCP stream. + pub fn set_read_timeout(&mut self, dur: Option) -> Result<(), Error> { + match self { + Stream::Http(stream) => Ok(stream.set_read_timeout(dur)?), + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] + Stream::Https(conn) => Ok(conn.get_mut().set_read_timeout(dur)?), + } + } + + /// Sets the write timeout on the underlying TCP stream. + pub fn set_write_timeout(&mut self, dur: Option) -> Result<(), Error> { + match self { + Stream::Http(stream) => Ok(stream.set_write_timeout(dur)?), + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] + Stream::Https(conn) => Ok(conn.get_mut().set_write_timeout(dur)?), + } + } +} + +impl Read for Stream { + fn read(&mut self, buf: &mut [u8]) -> Result { + match self { + Stream::Http(stream) => stream.read(buf), + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] + Stream::Https(stream) => stream.read(buf), + } + } +} + +impl Write for Stream { + fn write(&mut self, buf: &[u8]) -> Result { + match self { + Stream::Http(stream) => stream.write(buf), + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] + Stream::Https(stream) => stream.write(buf), + } + } + + fn flush(&mut self) -> Result<(), io::Error> { + match self { + Stream::Http(stream) => stream.flush(), + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] + Stream::Https(stream) => stream.flush(), + } + } +} + +/// Trait that allows to send data from readers to other threads +pub trait ThreadSend { + /// Reads `head` of the response and sends it via `sender` + fn send_head(&mut self, sender: &Sender>); + + /// Reads all bytes until EOF and sends them via `sender` + fn send_all(&mut self, sender: &Sender>); +} + +impl ThreadSend for T +where + T: BufRead, +{ + fn send_head(&mut self, sender: &Sender>) { + let buf = read_head(self); + sender.send(buf).unwrap_or(()); + } + + fn send_all(&mut self, sender: &Sender>) { + loop { + let mut buf = [0; BUF_SIZE]; + + match self.read(&mut buf) { + Ok(0) | Err(_) => break, + Ok(len) => { + let filled_buf = buf[..len].to_vec(); + if sender.send(filled_buf).is_err() { + break; + } + } + } + } + } +} + +/// Trait that allows to receive data from receivers +pub trait ThreadReceive { + /// Receives data from `receiver` and writes them into this writer. + /// Fails if `deadline` is exceeded. + fn receive(&mut self, receiver: &Receiver>, deadline: Instant) -> Result<(), Error>; + + /// Continuously receives data from `receiver` until there is no more data + /// or `deadline` is exceeded. Writes received data into this writer. + fn receive_all(&mut self, receiver: &Receiver>, deadline: Instant) + -> Result<(), Error>; +} + +impl ThreadReceive for T +where + T: Write, +{ + fn receive(&mut self, receiver: &Receiver>, deadline: Instant) -> Result<(), Error> { + let now = Instant::now(); + + match receiver.recv_timeout(deadline - now) { + Ok(data_read) => self.write_all(&data_read).map_err(Error::IO), + Err(RecvTimeoutError::Timeout) => Err(Error::Timeout), + Err(RecvTimeoutError::Disconnected) => Ok(()), + } + } + + fn receive_all( + &mut self, + receiver: &Receiver>, + deadline: Instant, + ) -> Result<(), Error> { + execute_with_deadline(deadline, |remaining_time| { + match receiver.recv_timeout(remaining_time) { + Ok(data_read) => { + if let Err(e) = self.write_all(&data_read) { + return Err(Error::IO(e)); + } + Ok(false) + } + Err(RecvTimeoutError::Timeout) => Err(Error::Timeout), + Err(RecvTimeoutError::Disconnected) => Ok(true), + } + }) + } +} + +/// Connects to the target host with a specified timeout. +pub fn connect_with_timeout(host: T, port: u16, timeout: U) -> io::Result +where + Duration: From, + T: AsRef, +{ + let host = host.as_ref(); + let timeout = Duration::from(timeout); + let addrs: Vec<_> = (host, port).to_socket_addrs()?.collect(); + let count = addrs.len(); + + for (idx, addr) in addrs.into_iter().enumerate() { + match TcpStream::connect_timeout(&addr, timeout) { + Ok(stream) => return Ok(stream), + Err(err) => match err.kind() { + io::ErrorKind::TimedOut => return Err(err), + _ => { + if idx + 1 == count { + return Err(err); + } + } + }, + }; + } + + Err(io::Error::new( + io::ErrorKind::AddrNotAvailable, + format!("Could not resolve address for {:?}", host), + )) +} + +/// Executes a function in a loop until operation is completed or deadline is exceeded. +/// +/// It checks if a timeout was exceeded every iteration, therefore it limits +/// how many times a specific function can be called before the deadline. +/// For `execute_with_deadline` to meet the deadline, each call +/// to `func` needs to finish before the deadline. +/// +/// Key information about function `func`: +/// - is provided with information about remaining time +/// - must ensure that its execution will not take more time than specified in `remaining_time` +/// - needs to return `Some(true)` when the operation is complete, and `Some(false)` - when the operation is in progress +pub fn execute_with_deadline(deadline: Instant, mut func: F) -> Result<(), Error> +where + F: FnMut(Duration) -> Result, +{ + loop { + let now = Instant::now(); + let remaining_time = deadline - now; + + if deadline < now { + return Err(Error::Timeout); + } + + match func(remaining_time) { + Ok(true) => break, + Ok(false) => continue, + Err(e) => return Err(e), + } + } + + Ok(()) +} + +/// Reads the head of HTTP response from `reader`. +/// +/// Reads from `reader` (line by line) until a blank line is identified, +/// which indicates that all meta-information has been read. +pub fn read_head(reader: &mut B) -> Vec +where + B: BufRead, +{ + let mut buf = Vec::with_capacity(BUF_SIZE); + + loop { + match reader.read_until(LF, &mut buf) { + Ok(0) | Err(_) => break, + Ok(len) => { + let full_len = buf.len(); + + if len == 2 && &buf[full_len - 2..] == CR_LF { + break; + } + } + } + } + + buf +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{io::BufReader, sync::mpsc, thread}; + + const URI: &str = "http://doc.rust-lang.org/std/string/index.html"; + const URI_S: &str = "https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol"; + const TIMEOUT: Duration = Duration::from_secs(3); + const RESPONSE: &[u8; 129] = b"HTTP/1.1 200 OK\r\n\ + Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + Content-Type: text/html\r\n\ + Content-Length: 100\r\n\r\n\ + hello\r\n\r\nhello"; + + const RESPONSE_H: &[u8; 102] = b"HTTP/1.1 200 OK\r\n\ + Date: Sat, 11 Jan 2003 02:44:04 GMT\r\n\ + Content-Type: text/html\r\n\ + Content-Length: 100\r\n\r\n"; + + #[test] + fn stream_new() { + { + let uri = Uri::try_from(URI).unwrap(); + let stream = Stream::connect(&uri, None); + + assert!(stream.is_ok()); + } + { + let uri = Uri::try_from(URI).unwrap(); + let stream = Stream::connect(&uri, Some(TIMEOUT)); + + assert!(stream.is_ok()); + } + } + + #[test] + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] + fn stream_try_to_https() { + { + let uri = Uri::try_from(URI_S).unwrap(); + let stream = Stream::connect(&uri, None).unwrap(); + let https_stream = Stream::try_to_https(stream, &uri, None); + + assert!(https_stream.is_ok()); + + // Scheme is `https`, therefore stream should be converted into HTTPS variant + match https_stream.unwrap() { + Stream::Http(_) => assert!(false), + Stream::Https(_) => assert!(true), + } + } + { + let uri = Uri::try_from(URI).unwrap(); + let stream = Stream::connect(&uri, None).unwrap(); + let https_stream = Stream::try_to_https(stream, &uri, None); + + assert!(https_stream.is_ok()); + + // Scheme is `http`, therefore stream should returned without changes + match https_stream.unwrap() { + Stream::Http(_) => assert!(true), + Stream::Https(_) => assert!(false), + } + } + } + + #[test] + fn stream_set_read_timeot() { + { + let uri = Uri::try_from(URI).unwrap(); + let mut stream = Stream::connect(&uri, None).unwrap(); + stream.set_read_timeout(Some(TIMEOUT)).unwrap(); + + let inner_read_timeout = if let Stream::Http(inner) = stream { + inner.read_timeout().unwrap() + } else { + None + }; + + assert_eq!(inner_read_timeout, Some(TIMEOUT)); + } + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] + { + let uri = Uri::try_from(URI_S).unwrap(); + let mut stream = Stream::connect(&uri, None).unwrap(); + stream = Stream::try_to_https(stream, &uri, None).unwrap(); + stream.set_read_timeout(Some(TIMEOUT)).unwrap(); + + let inner_read_timeout = if let Stream::Https(inner) = stream { + inner.get_ref().read_timeout().unwrap() + } else { + None + }; + + assert_eq!(inner_read_timeout, Some(TIMEOUT)); + } + } + + #[test] + fn stream_set_write_timeot() { + { + let uri = Uri::try_from(URI).unwrap(); + let mut stream = Stream::connect(&uri, None).unwrap(); + stream.set_write_timeout(Some(TIMEOUT)).unwrap(); + + let inner_read_timeout = if let Stream::Http(inner) = stream { + inner.write_timeout().unwrap() + } else { + None + }; + + assert_eq!(inner_read_timeout, Some(TIMEOUT)); + } + #[cfg(any(feature = "native-tls", feature = "rust-tls"))] + { + let uri = Uri::try_from(URI_S).unwrap(); + let mut stream = Stream::connect(&uri, None).unwrap(); + stream = Stream::try_to_https(stream, &uri, None).unwrap(); + stream.set_write_timeout(Some(TIMEOUT)).unwrap(); + + let inner_read_timeout = if let Stream::Https(inner) = stream { + inner.get_ref().write_timeout().unwrap() + } else { + None + }; + + assert_eq!(inner_read_timeout, Some(TIMEOUT)); + } + } + + #[test] + fn thread_send_send_head() { + let (sender, receiver) = mpsc::channel(); + + thread::spawn(move || { + let mut reader = BufReader::new(RESPONSE.as_slice()); + reader.send_head(&sender); + }); + + let raw_head = receiver.recv().unwrap(); + assert_eq!(raw_head, RESPONSE_H); + } + + #[test] + fn thread_send_send_all() { + let (sender, receiver) = mpsc::channel(); + + thread::spawn(move || { + let mut reader = BufReader::new(RESPONSE.as_slice()); + reader.send_all(&sender); + }); + + let raw_head = receiver.recv().unwrap(); + assert_eq!(raw_head, RESPONSE); + } + + #[test] + fn thread_receive_receive() { + let (sender, receiver) = mpsc::channel(); + let deadline = Instant::now() + TIMEOUT; + + thread::spawn(move || { + let res = [RESPONSE[..50].to_vec(), RESPONSE[50..].to_vec()]; + + for part in res { + sender.send(part).unwrap(); + } + }); + + let mut buf = Vec::with_capacity(BUF_SIZE); + buf.receive(&receiver, deadline).unwrap(); + + assert_eq!(buf, RESPONSE[..50]); + } + + #[test] + fn thread_receive_receive_all() { + let (sender, receiver) = mpsc::channel(); + let deadline = Instant::now() + TIMEOUT; + + thread::spawn(move || { + let res = [RESPONSE[..50].to_vec(), RESPONSE[50..].to_vec()]; + + for part in res { + sender.send(part).unwrap(); + } + }); + + let mut buf = Vec::with_capacity(BUF_SIZE); + buf.receive_all(&receiver, deadline).unwrap(); + + assert_eq!(buf, RESPONSE); + } + + #[ignore] + #[test] + fn fn_execute_with_deadline() { + { + let star_time = Instant::now(); + let deadline = star_time + TIMEOUT; + + let timeout_err = execute_with_deadline(deadline, |_| { + let sleep_time = Duration::from_millis(500); + thread::sleep(sleep_time); + + Ok(false) + }); + + let end_time = Instant::now(); + let total_time = end_time.duration_since(star_time).as_secs(); + + assert_eq!(total_time, TIMEOUT.as_secs()); + assert!(timeout_err.is_err()); + } + { + let star_time = Instant::now(); + let deadline = star_time + TIMEOUT; + + execute_with_deadline(deadline, |_| { + let sleep_time = Duration::from_secs(1); + thread::sleep(sleep_time); + + Ok(true) + }) + .unwrap(); + + let end_time = Instant::now(); + let total_time = end_time.duration_since(star_time).as_secs(); + + assert_eq!(total_time, 1); + } + } + + #[test] + fn fn_read_head() { + let reader = RESPONSE.as_slice(); + let mut buf_reader = BufReader::new(reader); + let raw_head = read_head(&mut buf_reader); + + assert_eq!(raw_head, RESPONSE_H); + } +} diff --git a/src/tls.rs b/src/tls.rs index ebb5b91..bad4267 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -1,4 +1,4 @@ -//!secure connection over TLS +//! secure connection over TLS use crate::error::Error as HttpError; use std::{ @@ -11,33 +11,52 @@ use std::{ use std::io::prelude::*; #[cfg(feature = "rust-tls")] -use crate::error::ParseErr; +use rustls::{ClientConnection, StreamOwned}; +#[cfg(feature = "rust-tls")] +use rustls_pki_types::ServerName; #[cfg(not(any(feature = "native-tls", feature = "rust-tls")))] -compile_error!("one of the `native-tls` or `rust-tls` features must be enabled"); +compile_error!("One of the `native-tls` or `rust-tls` features must be enabled"); -///wrapper around TLS Stream, -///depends on selected TLS library +/// Wrapper around TLS Stream, depends on selected TLS library: +/// - native_tls: `TlsStream` +/// - rustls: `StreamOwned` +#[derive(Debug)] pub struct Conn { #[cfg(feature = "native-tls")] stream: native_tls::TlsStream, #[cfg(feature = "rust-tls")] - stream: rustls::StreamOwned, + stream: rustls::StreamOwned, +} + +impl Conn +where + S: io::Read + io::Write, +{ + /// Returns a reference to the underlying socket. + pub fn get_ref(&self) -> &S { + self.stream.get_ref() + } + + /// Returns a mutable reference to the underlying socket. + pub fn get_mut(&mut self) -> &mut S { + self.stream.get_mut() + } } -impl io::Read for Conn { +impl io::Read for Conn +where + S: io::Read + io::Write, +{ fn read(&mut self, buf: &mut [u8]) -> Result { let len = self.stream.read(buf); #[cfg(feature = "rust-tls")] { - // TODO: this api returns ConnectionAborted with a "..CloseNotify.." string. - // TODO: we should work out if self.stream.sess exposes enough information - // TODO: to not read in this situation, and return EOF directly. - // TODO: c.f. the checks in the implementation. connection_at_eof() doesn't - // TODO: seem to be exposed. The implementation: - // TODO: https://github.com/ctz/rustls/blob/f93c325ce58f2f1e02f09bcae6c48ad3f7bde542/src/session.rs#L789-L792 + // Handle ConnectionAborted for Rust-TLS + // Reference to the rustls implementation: + // https://github.com/ctz/rustls/blob/f93c325ce58f2f1e02f09bcae6c48ad3f7bde542/src/session.rs#L789-L792 if let Err(ref e) = len { if io::ErrorKind::ConnectionAborted == e.kind() { return Ok(0); @@ -49,21 +68,26 @@ impl io::Read for Conn { } } -impl io::Write for Conn { +impl io::Write for Conn +where + S: io::Read + io::Write, +{ fn write(&mut self, buf: &[u8]) -> Result { self.stream.write(buf) } + fn flush(&mut self) -> Result<(), io::Error> { self.stream.flush() } } -///client configuration +/// Client configuration for TLS connection. pub struct Config { #[cfg(feature = "native-tls")] extra_root_certs: Vec, + #[cfg(feature = "rust-tls")] - client_config: std::sync::Arc, + root_certs: std::sync::Arc, } impl Default for Config { @@ -76,37 +100,41 @@ impl Default for Config { #[cfg(feature = "rust-tls")] fn default() -> Self { - let mut config = rustls::ClientConfig::new(); - config - .root_store - .add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS); + let root_store = rustls::RootCertStore { + roots: webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect(), + }; Config { - client_config: std::sync::Arc::new(config), + root_certs: std::sync::Arc::new(root_store), } } } impl Config { + /// Adds root certificates (X.509) from PEM file. #[cfg(feature = "native-tls")] pub fn add_root_cert_file_pem(&mut self, file_path: &Path) -> Result<&mut Self, HttpError> { let f = File::open(file_path)?; let f = BufReader::new(f); let mut pem_crt = vec![]; + for line in f.lines() { let line = line?; let is_end_cert = line.contains("-----END"); pem_crt.append(&mut line.into_bytes()); pem_crt.push(b'\n'); + if is_end_cert { let crt = native_tls::Certificate::from_pem(&pem_crt)?; self.extra_root_certs.push(crt); pem_crt.clear(); } } + Ok(self) } + /// Establishes a secure connection. #[cfg(feature = "native-tls")] pub fn connect(&self, hostname: H, stream: S) -> Result, HttpError> where @@ -114,40 +142,59 @@ impl Config { S: io::Read + io::Write, { let mut connector_builder = native_tls::TlsConnector::builder(); + for crt in self.extra_root_certs.iter() { connector_builder.add_root_certificate((*crt).clone()); } + let connector = connector_builder.build()?; let stream = connector.connect(hostname.as_ref(), stream)?; Ok(Conn { stream }) } + /// Adds root certificates (X.509) from a PEM file. #[cfg(feature = "rust-tls")] pub fn add_root_cert_file_pem(&mut self, file_path: &Path) -> Result<&mut Self, HttpError> { let f = File::open(file_path)?; let mut f = BufReader::new(f); - let config = std::sync::Arc::make_mut(&mut self.client_config); - let _ = config - .root_store - .add_pem_file(&mut f) - .map_err(|_| HttpError::from(ParseErr::Invalid))?; + + let root_certs = std::sync::Arc::make_mut(&mut self.root_certs); + let mut file_certs = Vec::new(); + + for cert in rustls_pemfile::certs(&mut f) { + match cert { + Ok(item) => { + file_certs.push(item); + } + Err(e) => return Err(HttpError::IO(e)), + } + } + + root_certs.add_parsable_certificates(file_certs); + Ok(self) } + /// Establishes a secure connection. #[cfg(feature = "rust-tls")] pub fn connect(&self, hostname: H, stream: S) -> Result, HttpError> where H: AsRef, S: io::Read + io::Write, { - use rustls::{ClientSession, StreamOwned}; + let hostname = hostname.as_ref().to_string(); + + let client_config = rustls::ClientConfig::builder() + .with_root_certificates(self.root_certs.clone()) + .with_no_client_auth(); + + let session = ClientConnection::new( + std::sync::Arc::new(client_config), + ServerName::try_from(hostname).map_err(|_| HttpError::Tls)?, + ) + .map_err(|_| HttpError::Tls)?; - let session = ClientSession::new( - &self.client_config, - webpki::DNSNameRef::try_from_ascii_str(hostname.as_ref()) - .map_err(|_| HttpError::Tls)?, - ); let stream = StreamOwned::new(session, stream); Ok(Conn { stream }) diff --git a/src/uri.rs b/src/uri.rs index 08220c7..a1863ac 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -1,6 +1,8 @@ //! uri operations + use crate::error::{Error, ParseErr}; use std::{ + convert::TryFrom, fmt, ops::{Index, Range}, str, @@ -10,7 +12,7 @@ use std::{ const HTTP_PORT: u16 = 80; const HTTPS_PORT: u16 = 443; -///A (half-open) range bounded inclusively below and exclusively above (start..end) with `Copy`. +/// A (half-open) range bounded inclusively below and exclusively above (start..end) with `Copy`. #[derive(Copy, Clone, Debug, PartialOrd, PartialEq)] pub struct RangeC { pub start: usize, @@ -18,14 +20,17 @@ pub struct RangeC { } impl RangeC { - ///Creates new `RangeC` with `start` and `end`. + /// Creates new `RangeC` with `start` and `end`. + /// + /// # Examples + /// ``` + /// use http_req::uri::RangeC; /// - ///# Exmaples - ///``` - ///use http_req::uri::RangeC; + /// let range = RangeC::new(0, 20); /// - ///const range: RangeC = RangeC::new(0, 20); - ///``` + /// assert_eq!(range.start, 0); + /// assert_eq!(range.end, 20); + /// ``` pub const fn new(start: usize, end: usize) -> RangeC { RangeC { start, end } } @@ -40,6 +45,15 @@ impl From for Range { } } +impl Index for str { + type Output = str; + + #[inline] + fn index(&self, index: RangeC) -> &str { + &self[..][Range::from(index)] + } +} + impl Index for String { type Output = str; @@ -49,74 +63,84 @@ impl Index for String { } } -///Representation of Uniform Resource Identifier +/// Representation of Uniform Resource Identifier /// -///# Example -///``` -///use http_req::uri::Uri; +/// # Examples +/// ``` +/// use http_req::uri::Uri; +/// use std::convert::TryFrom; /// -///let uri: Uri = "https://user:info@foo.com:12/bar/baz?query#fragment".parse().unwrap(); -///assert_eq!(uri.host(), Some("foo.com")); -///``` +/// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); +/// assert_eq!(uri.host(), Some("foo.com")); +/// ``` #[derive(Clone, Debug, PartialEq)] -pub struct Uri { - inner: String, +pub struct Uri<'a> { + inner: &'a str, scheme: RangeC, - authority: Option, + authority: Option>, path: Option, query: Option, fragment: Option, } -impl Uri { - ///Returns scheme of this `Uri`. +impl<'a> Uri<'a> { + /// Returns a reference to the underlying &str. + pub fn get_ref(&self) -> &str { + self.inner + } + + /// Returns scheme of this `Uri`. /// - ///# Example - ///``` - ///use http_req::uri::Uri; + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// use std::convert::TryFrom; /// - ///let uri: Uri = "https://user:info@foo.com:12/bar/baz?query#fragment".parse().unwrap(); - ///assert_eq!(uri.scheme(), "https"); - ///``` + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); + /// assert_eq!(uri.scheme(), "https"); + /// ``` pub fn scheme(&self) -> &str { &self.inner[self.scheme] } - ///Returns information about the user included in this `Uri`. - /// - ///# Example - ///``` - ///use http_req::uri::Uri; + /// Returns information about the user included in this `Uri`. /// - ///let uri: Uri = "https://user:info@foo.com:12/bar/baz?query#fragment".parse().unwrap(); - ///assert_eq!(uri.user_info(), Some("user:info")); - ///``` + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// use std::convert::TryFrom; + /// + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); + /// assert_eq!(uri.user_info(), Some("user:info")); + /// ``` pub fn user_info(&self) -> Option<&str> { self.authority.as_ref().and_then(|a| a.user_info()) } - ///Returns host of this `Uri`. - /// - ///# Example - ///``` - ///use http_req::uri::Uri; + /// Returns host of this `Uri`. + /// + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// use std::convert::TryFrom; /// - ///let uri: Uri = "https://user:info@foo.com:12/bar/baz?query#fragment".parse().unwrap(); - ///assert_eq!(uri.host(), Some("foo.com")); - ///``` + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); + /// assert_eq!(uri.host(), Some("foo.com")); + /// ``` pub fn host(&self) -> Option<&str> { self.authority.as_ref().map(|a| a.host()) } - ///Returns host of this `Uri` to use in a header. - /// - ///# Example - ///``` - ///use http_req::uri::Uri; + /// Returns host of this `Uri` to use in a header. + /// + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// use std::convert::TryFrom; /// - ///let uri: Uri = "https://user:info@foo.com:12/bar/baz?query#fragment".parse().unwrap(); - ///assert_eq!(uri.host_header(), Some("foo.com:12".to_string())); - ///``` + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); + /// assert_eq!(uri.host_header(), Some("foo.com:12".to_string())); + /// ``` pub fn host_header(&self) -> Option { self.host().map(|h| match self.corr_port() { HTTP_PORT | HTTPS_PORT => h.to_string(), @@ -124,29 +148,31 @@ impl Uri { }) } - ///Returns port of this `Uri` - /// - ///# Example - ///``` - ///use http_req::uri::Uri; + /// Returns port of this `Uri` /// - ///let uri: Uri = "https://user:info@foo.com:12/bar/baz?query#fragment".parse().unwrap(); - ///assert_eq!(uri.port(), Some(12)); - ///``` + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// use std::convert::TryFrom; + /// + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); + /// assert_eq!(uri.port(), Some(12)); + /// ``` pub fn port(&self) -> Option { self.authority.as_ref().and_then(|a| a.port()) } - ///Returns port corresponding to this `Uri`. - ///Returns default port if it hasn't been set in the uri. - /// - ///# Example - ///``` - ///use http_req::uri::Uri; + /// Returns port corresponding to this `Uri`. + /// Returns default port if it hasn't been set in the uri. + /// + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// use std::convert::TryFrom; /// - ///let uri: Uri = "https://user:info@foo.com:12/bar/baz?query#fragment".parse().unwrap(); - ///assert_eq!(uri.corr_port(), 12); - ///``` + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); + /// assert_eq!(uri.corr_port(), 12); + /// ``` pub fn corr_port(&self) -> u16 { let default_port = match self.scheme() { "https" => HTTPS_PORT, @@ -159,118 +185,208 @@ impl Uri { } } - ///Returns path of this `Uri`. - /// - ///# Example - ///``` - ///use http_req::uri::Uri; + /// Returns path of this `Uri`. /// - ///let uri: Uri = "https://user:info@foo.com:12/bar/baz?query#fragment".parse().unwrap(); - ///assert_eq!(uri.path(), Some("/bar/baz")); - ///``` + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// use std::convert::TryFrom; + /// + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); + /// assert_eq!(uri.path(), Some("/bar/baz")); + /// ``` pub fn path(&self) -> Option<&str> { self.path.map(|r| &self.inner[r]) } - ///Returns query of this `Uri`. - /// - ///# Example - ///``` - ///use http_req::uri::Uri; + /// Returns query of this `Uri`. + /// + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// use std::convert::TryFrom; /// - ///let uri: Uri = "https://user:info@foo.com:12/bar/baz?query#fragment".parse().unwrap(); - ///assert_eq!(uri.query(), Some("query")); - ///``` + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); + /// assert_eq!(uri.query(), Some("query")); + /// ``` pub fn query(&self) -> Option<&str> { self.query.map(|r| &self.inner[r]) } - ///Returns fragment of this `Uri`. - /// - ///# Example - ///``` - ///use http_req::uri::Uri; + /// Returns fragment of this `Uri`. /// - ///let uri: Uri = "https://user:info@foo.com:12/bar/baz?query#fragment".parse().unwrap(); - ///assert_eq!(uri.fragment(), Some("fragment")); - ///``` + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// use std::convert::TryFrom; + /// + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); + /// assert_eq!(uri.fragment(), Some("fragment")); + /// ``` pub fn fragment(&self) -> Option<&str> { self.fragment.map(|r| &self.inner[r]) } - ///Returns resource `Uri` points to. - /// - ///# Example - ///``` - ///use http_req::uri::Uri; + /// Returns resource `Uri` points to. + /// + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// use std::convert::TryFrom; /// - ///let uri: Uri = "https://user:info@foo.com:12/bar/baz?query#fragment".parse().unwrap(); - ///assert_eq!(uri.resource(), "/bar/baz?query#fragment"); - ///``` + /// let uri: Uri = Uri::try_from("https://user:info@foo.com:12/bar/baz?query#fragment").unwrap(); + /// assert_eq!(uri.resource(), "/bar/baz?query#fragment"); + /// ``` pub fn resource(&self) -> &str { - let mut result = "/"; + match self.path { + Some(p) => &self.inner[p.start..], + None => "/", + } + } - for v in &[self.path, self.query, self.fragment] { - if let Some(r) = v { - result = &self.inner[r.start..]; - break; - } + /// Checks if &str is a relative uri. + /// + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// + /// assert!(Uri::is_relative("/relative/path")); + /// assert!(!Uri::is_relative("http://absolute.com")); + /// ``` + pub fn is_relative(raw_uri: &str) -> bool { + raw_uri.starts_with("/") + || raw_uri.starts_with("?") + || raw_uri.starts_with("#") + || !raw_uri.contains(":") + } + + /// Creates a new `Uri` from current uri and relative uri. + /// Writes the new uri (raw string) into `relative_uri`. + /// + /// # Examples + /// ``` + /// use http_req::uri::Uri; + /// use std::convert::TryFrom; + /// + /// let base_uri: Uri = Uri::try_from("https://example.com/base").unwrap(); + /// let mut relative_path = String::from("/relative/path"); + /// let new_uri = base_uri.from_relative(&mut relative_path).unwrap(); + /// + /// assert_eq!(new_uri.to_string(), "https://example.com/relative/path"); + /// ``` + pub fn from_relative(&'a self, relative_uri: &'a mut String) -> Result, Error> { + let inner_uri = self.inner; + let mut resource = self.resource().to_string(); + + resource = match &relative_uri.get(..1) { + Some("#") => Uri::add_part_start(&resource, relative_uri, "#"), + Some("?") => Uri::add_part_start(&self.path().unwrap_or("/"), relative_uri, "?"), + Some("/") => Uri::add_part_start(&resource, relative_uri, "/"), + _ => Uri::add_part_end(&resource, relative_uri, "/"), + }; + + *relative_uri = if let Some(p) = self.path { + inner_uri[..p.start].to_string() + &resource + } else { + inner_uri.trim_end_matches("/").to_string() + &resource + }; + + Uri::try_from(relative_uri.as_str()) + } + + /// Adds a part at the beginning of the base. + /// Finds the first occurance of a separator in a base and the first occurance of a separator in a part. + /// Joins all chars before the separator from the base, separator and all chars after the separator from the part. + fn add_part_start(base: &str, part: &str, separator: &str) -> String { + let base_idx = base.find(separator); + Uri::add_part(base, part, separator, base_idx) + } + + /// Adds a part at the end of the base. + /// Finds the last occurance of a separator in a base and the first occurance of a separator in a part. + /// Joins all chars before the separator from the base, separator and all chars after the separator from the part. + fn add_part_end(base: &str, part: &str, separator: &str) -> String { + let base_idx = base.rfind(separator); + Uri::add_part(base, part, separator, base_idx) + } + + /// Adds a part to the base with separator in between. + /// Base index defines where part should be added. + fn add_part(base: &str, part: &str, separator: &str, base_idx: Option) -> String { + let mut output = String::new(); + let part_idx = part.find(separator); + + if let Some(idx) = base_idx { + output += &base[..idx]; + } else { + output += base; + } + + output += separator; + + if let Some(idx) = part_idx { + output += &part[idx + 1..]; + } else { + output += part; } - result + output } } -impl fmt::Display for Uri { +impl<'a> fmt::Display for Uri<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let uri = if let Some(auth) = &self.authority { - let mut uri = self.inner.to_string(); + let mut uri = self.inner.to_string(); + + if let Some(auth) = &self.authority { let auth = auth.to_string(); let start = self.scheme.end + 3; uri.replace_range(start..(start + auth.len()), &auth); - uri - } else { - self.inner.to_string() - }; + } write!(f, "{}", uri) } } -impl str::FromStr for Uri { - type Err = Error; - - fn from_str(s: &str) -> Result { - let mut s = s.to_string(); - remove_spaces(&mut s); +impl<'a> TryFrom<&'a str> for Uri<'a> { + type Error = Error; + fn try_from(s: &'a str) -> Result { let (scheme, mut uri_part) = get_chunks(&s, Some(RangeC::new(0, s.len())), ":"); let scheme = scheme.ok_or(ParseErr::UriErr)?; + let (mut authority, mut query, mut fragment) = (None, None, None); - let mut authority = None; - - if let Some(u) = &uri_part { - if s[*u].contains("//") { + if let Some(u) = uri_part { + if s[u].contains("//") { let (auth, part) = get_chunks(&s, Some(RangeC::new(u.start + 2, u.end)), "/"); - authority = if let Some(a) = auth { - Some(s[a].parse()?) - } else { - None + if let Some(a) = auth { + authority = Some(Authority::try_from(&s[a])?) }; uri_part = part; } } - let (mut path, uri_part) = get_chunks(&s, uri_part, "?"); - - if authority.is_some() || &s[scheme] == "file" { - path = path.map(|p| RangeC::new(p.start - 1, p.end)); + if let Some(u) = uri_part { + if &s[u.start - 1..u.start] == "/" { + uri_part = Some(RangeC::new(u.start - 1, u.end)); + } } - let (query, fragment) = get_chunks(&s, uri_part, "#"); + let mut path = uri_part; + + if let Some(u) = uri_part { + if s[u].contains("?") && s[u].contains("#") { + (path, uri_part) = get_chunks(&s, uri_part, "?"); + (query, fragment) = get_chunks(&s, uri_part, "#"); + } else if s[u].contains("?") { + (path, query) = get_chunks(&s, uri_part, "?"); + } else if s[u].contains("#") { + (path, fragment) = get_chunks(&s, uri_part, "#"); + } + } Ok(Uri { inner: s, @@ -283,60 +399,64 @@ impl str::FromStr for Uri { } } -///Authority of Uri +/// Authority of Uri /// -///# Example -///``` -///use http_req::uri::Authority; +/// # Examples +/// ``` +/// use http_req::uri::Authority; +/// use std::convert::TryFrom; /// -///let auth: Authority = "user:info@foo.com:443".parse().unwrap(); -///assert_eq!(auth.host(), "foo.com"); -///``` +/// let auth: Authority = Authority::try_from("user:info@foo.com:443").unwrap(); +/// assert_eq!(auth.host(), "foo.com"); +/// ``` #[derive(Clone, Debug, PartialEq)] -pub struct Authority { - inner: String, +pub struct Authority<'a> { + inner: &'a str, username: Option, password: Option, host: RangeC, port: Option, } -impl Authority { - ///Returns username of this `Authority` +impl<'a> Authority<'a> { + /// Returns username of this `Authority` /// - ///# Example - ///``` - ///use http_req::uri::Authority; + /// # Examples + /// ``` + /// use http_req::uri::Authority; + /// use std::convert::TryFrom; /// - ///let auth: Authority = "user:info@foo.com:443".parse().unwrap(); - ///assert_eq!(auth.username(), Some("user")); - ///``` - pub fn username(&self) -> Option<&str> { + /// let auth: Authority = Authority::try_from("user:info@foo.com:443").unwrap(); + /// assert_eq!(auth.username(), Some("user")); + /// ``` + pub fn username(&self) -> Option<&'a str> { self.username.map(|r| &self.inner[r]) } - ///Returns password of this `Authority` + /// Returns password of this `Authority` /// - ///# Example - ///``` - ///use http_req::uri::Authority; + /// # Examples + /// ``` + /// use http_req::uri::Authority; + /// use std::convert::TryFrom; /// - ///let auth: Authority = "user:info@foo.com:443".parse().unwrap(); - ///assert_eq!(auth.password(), Some("info")); - ///``` + /// let auth: Authority = Authority::try_from("user:info@foo.com:443").unwrap(); + /// assert_eq!(auth.password(), Some("info")); + /// ``` pub fn password(&self) -> Option<&str> { self.password.map(|r| &self.inner[r]) } - ///Returns information about the user + /// Returns information about the user /// - ///# Example - ///``` - ///use http_req::uri::Authority; + /// # Examples + /// ``` + /// use http_req::uri::Authority; + /// use std::convert::TryFrom; /// - ///let auth: Authority = "user:info@foo.com:443".parse().unwrap(); - ///assert_eq!(auth.user_info(), Some("user:info")); - ///``` + /// let auth: Authority = Authority::try_from("user:info@foo.com:443").unwrap(); + /// assert_eq!(auth.user_info(), Some("user:info")); + /// ``` pub fn user_info(&self) -> Option<&str> { match (&self.username, &self.password) { (Some(u), Some(p)) => Some(&self.inner[u.start..p.end]), @@ -345,55 +465,51 @@ impl Authority { } } - ///Returns host of this `Authority` + /// Returns host of this `Authority` /// - ///# Example - ///``` - ///use http_req::uri::Authority; + /// # Examples + /// ``` + /// use http_req::uri::Authority; + /// use std::convert::TryFrom; /// - ///let auth: Authority = "user:info@foo.com:443".parse().unwrap(); - ///assert_eq!(auth.host(), "foo.com"); - ///``` + /// let auth: Authority = Authority::try_from("user:info@foo.com:443").unwrap(); + /// assert_eq!(auth.host(), "foo.com"); + /// ``` pub fn host(&self) -> &str { &self.inner[self.host] } - ///Returns port of this `Authority` + /// Returns port of this `Authority` /// - ///# Example - ///``` - ///use http_req::uri::Authority; + /// # Examples + /// ``` + /// use http_req::uri::Authority; + /// use std::convert::TryFrom; /// - ///let auth: Authority = "user:info@foo.com:443".parse().unwrap(); - ///assert_eq!(auth.port(), Some(443)); - ///``` + /// let auth: Authority = Authority::try_from("user:info@foo.com:443").unwrap(); + /// assert_eq!(auth.port(), Some(443)); + /// ``` pub fn port(&self) -> Option { self.port.as_ref().map(|p| self.inner[*p].parse().unwrap()) } } -impl str::FromStr for Authority { - type Err = ParseErr; +impl<'a> TryFrom<&'a str> for Authority<'a> { + type Error = ParseErr; - fn from_str(s: &str) -> Result { - let inner = s.to_string(); - - let mut username = None; - let mut password = None; + fn try_from(s: &'a str) -> Result { + let (mut username, mut password) = (None, None); let uri_part = if s.contains('@') { let (info, part) = get_chunks(&s, Some(RangeC::new(0, s.len())), "@"); - let (name, pass) = get_chunks(&s, info, ":"); - - username = name; - password = pass; + (username, password) = get_chunks(&s, info, ":"); part } else { Some(RangeC::new(0, s.len())) }; - let split_by = if s.contains(']') && s.contains('[') { + let split_by = if s.contains('[') && s.contains(']') { "]:" } else { ":" @@ -402,13 +518,13 @@ impl str::FromStr for Authority { let host = host.ok_or(ParseErr::UriErr)?; if let Some(p) = port { - if inner[p].parse::().is_err() { + if s[p].parse::().is_err() { return Err(ParseErr::UriErr); } } Ok(Authority { - inner, + inner: s, username, password, host, @@ -417,71 +533,61 @@ impl str::FromStr for Authority { } } -impl fmt::Display for Authority { +impl<'a> fmt::Display for Authority<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let auth = if let Some(pass) = self.password { - let range = Range::from(pass); + let mut auth = self.inner.to_string(); + if let Some(pass) = self.password { + let range = Range::from(pass); let hidden_pass = "*".repeat(range.len()); - let mut auth = self.inner.to_string(); - auth.replace_range(range, &hidden_pass); - auth - } else { - self.inner.to_string() - }; + auth.replace_range(range, &hidden_pass); + } write!(f, "{}", auth) } } -//Removes whitespace from `text` -fn remove_spaces(text: &mut String) { - text.retain(|c| !c.is_whitespace()); -} - -//Splits `s` by `separator`. If `separator` is found inside `s`, it will return two `Some` values -//consisting `RangeC` of each `&str`. If `separator` is at the end of `s` or it's not found, -//it will return tuple consisting `Some` with `RangeC` of entire `s` inside and None. +/// Splits `s` by `separator`. If `separator` is found inside `s`, it will return two `Some` values +/// consisting `RangeC` of each `&str`. If `separator` is at the end of `s` or it's not found, +/// it will return tuple consisting `Some` with `RangeC` of entire `s` inside and None. fn get_chunks<'a>( s: &'a str, range: Option, separator: &'a str, ) -> (Option, Option) { - if let Some(r) = range { - let range = Range::from(r); + let (mut before, mut after) = (None, None); - match s[range.clone()].find(separator) { + if let Some(range) = range { + match s[range].find(separator) { Some(i) => { - let mid = r.start + i + separator.len(); - let before = Some(RangeC::new(r.start, mid - 1)).filter(|r| r.start != r.end); - let after = Some(RangeC::new(mid, r.end)).filter(|r| r.start != r.end); - - (before, after) + let mid = range.start + i + separator.len(); + before = Some(RangeC::new(range.start, mid - 1)).filter(|r| r.start != r.end); + after = Some(RangeC::new(mid, range.end)).filter(|r| r.start != r.end); } None => { if !s[range].is_empty() { - (Some(r), None) - } else { - (None, None) + before = Some(range); } } } - } else { - (None, None) } + + (before, after) } #[cfg(test)] mod tests { use super::*; - const TEST_URIS: [&str; 5] = [ + const TEST_URIS: [&str; 7] = [ "https://user:info@foo.com:12/bar/baz?query#fragment", "file:///C:/Users/User/Pictures/screenshot.png", "https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol", "mailto:John.Doe@example.com", "https://[4b10:bbb0:0:d0::ba7:8001]:443/", + "http://example.com/?query=val", + "https://example.com/#fragment", ]; const TEST_AUTH: [&str; 4] = [ @@ -491,20 +597,22 @@ mod tests { "[4b10:bbb0:0:d0::ba7:8001]:443", ]; - #[test] - fn remove_space() { - let mut text = String::from("Hello World !"); - let expect = String::from("HelloWorld!"); - - remove_spaces(&mut text); - assert_eq!(text, expect); - } + const TEST_PARTS: [&str; 7] = [ + "?query123", + "/path", + "#fragment", + "other-path", + "#paragraph", + "./foo/bar/buz", + "?users#1551", + ]; #[test] fn uri_full_parse() { - let uri = "abc://username:password@example.com:123/path/data?key=value&key2=value2#fragid1" - .parse::() - .unwrap(); + let uri = Uri::try_from( + "abc://username:password@example.com:123/path/data?key=value&key2=value2#fragid1", + ) + .unwrap(); assert_eq!(uri.scheme(), "abc"); assert_eq!(uri.user_info(), Some("username:password")); @@ -519,7 +627,7 @@ mod tests { #[test] fn uri_parse() { for uri in TEST_URIS.iter() { - uri.parse::().unwrap(); + Uri::try_from(*uri).unwrap(); } } @@ -527,52 +635,57 @@ mod tests { fn uri_scheme() { let uris: Vec<_> = TEST_URIS .iter() - .map(|uri| uri.parse::().unwrap()) + .map(|uri| Uri::try_from(*uri).unwrap()) .collect(); + const RESULT: [&str; 7] = ["https", "file", "https", "mailto", "https", "http", "https"]; - assert_eq!(uris[0].scheme(), "https"); - assert_eq!(uris[1].scheme(), "file"); - assert_eq!(uris[2].scheme(), "https"); - assert_eq!(uris[3].scheme(), "mailto"); - assert_eq!(uris[4].scheme(), "https"); + for i in 0..RESULT.len() { + assert_eq!(uris[i].scheme(), RESULT[i]); + } } #[test] fn uri_uesr_info() { let uris: Vec<_> = TEST_URIS .iter() - .map(|uri| uri.parse::().unwrap()) + .map(|uri| Uri::try_from(*uri).unwrap()) .collect(); + const RESULT: [Option<&str>; 7] = [Some("user:info"), None, None, None, None, None, None]; - assert_eq!(uris[0].user_info(), Some("user:info")); - assert_eq!(uris[1].user_info(), None); - assert_eq!(uris[2].user_info(), None); - assert_eq!(uris[3].user_info(), None); - assert_eq!(uris[4].user_info(), None); + for i in 0..RESULT.len() { + assert_eq!(uris[i].user_info(), RESULT[i]); + } } #[test] fn uri_host() { let uris: Vec<_> = TEST_URIS .iter() - .map(|uri| uri.parse::().unwrap()) + .map(|uri| Uri::try_from(*uri).unwrap()) .collect(); - assert_eq!(uris[0].host(), Some("foo.com")); - assert_eq!(uris[1].host(), None); - assert_eq!(uris[2].host(), Some("en.wikipedia.org")); - assert_eq!(uris[3].host(), None); - assert_eq!(uris[4].host(), Some("[4b10:bbb0:0:d0::ba7:8001]")); + const RESULT: [Option<&str>; 7] = [ + Some("foo.com"), + None, + Some("en.wikipedia.org"), + None, + Some("[4b10:bbb0:0:d0::ba7:8001]"), + Some("example.com"), + Some("example.com"), + ]; + + for i in 0..RESULT.len() { + assert_eq!(uris[i].host(), RESULT[i]); + } } #[test] fn uri_host_header() { - let uri_def: Uri = "https://en.wikipedia.org:443/wiki/Hypertext_Transfer_Protocol" - .parse() - .unwrap(); + let uri_def: Uri = + Uri::try_from("https://en.wikipedia.org:443/wiki/Hypertext_Transfer_Protocol").unwrap(); let uris: Vec<_> = TEST_URIS .iter() - .map(|uri| uri.parse::().unwrap()) + .map(|uri| Uri::try_from(*uri).unwrap()) .collect(); assert_eq!(uris[0].host_header(), Some("foo.com:12".to_string())); @@ -584,13 +697,17 @@ mod tests { fn uri_port() { let uris: Vec<_> = TEST_URIS .iter() - .map(|uri| uri.parse::().unwrap()) + .map(|uri| Uri::try_from(*uri).unwrap()) .collect(); assert_eq!(uris[0].port(), Some(12)); assert_eq!(uris[4].port(), Some(443)); - for i in 1..3 { + for i in 1..4 { + assert_eq!(uris[i].port(), None); + } + + for i in 5..7 { assert_eq!(uris[i].port(), None); } } @@ -599,44 +716,59 @@ mod tests { fn uri_corr_port() { let uris: Vec<_> = TEST_URIS .iter() - .map(|uri| uri.parse::().unwrap()) + .map(|uri| Uri::try_from(*uri).unwrap()) .collect(); - assert_eq!(uris[0].corr_port(), 12); - assert_eq!(uris[1].corr_port(), HTTP_PORT); - assert_eq!(uris[2].corr_port(), HTTPS_PORT); - assert_eq!(uris[3].corr_port(), HTTP_PORT); - assert_eq!(uris[4].corr_port(), HTTPS_PORT); + const RESULT: [u16; 7] = [ + 12, HTTP_PORT, HTTPS_PORT, HTTP_PORT, HTTPS_PORT, HTTP_PORT, HTTPS_PORT, + ]; + + for i in 0..RESULT.len() { + assert_eq!(uris[i].corr_port(), RESULT[i]); + } } #[test] fn uri_path() { let uris: Vec<_> = TEST_URIS .iter() - .map(|uri| uri.parse::().unwrap()) + .map(|uri| Uri::try_from(*uri).unwrap()) .collect(); - assert_eq!(uris[0].path(), Some("/bar/baz")); - assert_eq!( - uris[1].path(), - Some("/C:/Users/User/Pictures/screenshot.png") - ); - assert_eq!(uris[2].path(), Some("/wiki/Hypertext_Transfer_Protocol")); - assert_eq!(uris[3].path(), Some("John.Doe@example.com")); - assert_eq!(uris[4].path(), None); + const RESULT: [Option<&str>; 7] = [ + Some("/bar/baz"), + Some("/C:/Users/User/Pictures/screenshot.png"), + Some("/wiki/Hypertext_Transfer_Protocol"), + Some("John.Doe@example.com"), + None, + Some("/"), + Some("/"), + ]; + + for i in 0..RESULT.len() { + assert_eq!(uris[i].path(), RESULT[i]); + } } #[test] fn uri_query() { let uris: Vec<_> = TEST_URIS .iter() - .map(|uri| uri.parse::().unwrap()) + .map(|uri| Uri::try_from(*uri).unwrap()) .collect(); - assert_eq!(uris[0].query(), Some("query")); - - for i in 1..4 { - assert_eq!(uris[i].query(), None); + const RESULT: [Option<&str>; 7] = [ + Some("query"), + None, + None, + None, + None, + Some("query=val"), + None, + ]; + + for i in 0..RESULT.len() { + assert_eq!(uris[i].query(), RESULT[i]); } } @@ -644,13 +776,21 @@ mod tests { fn uri_fragment() { let uris: Vec<_> = TEST_URIS .iter() - .map(|uri| uri.parse::().unwrap()) + .map(|uri| Uri::try_from(*uri).unwrap()) .collect(); - assert_eq!(uris[0].fragment(), Some("fragment")); - - for i in 1..4 { - assert_eq!(uris[i].fragment(), None); + const RESULT: [Option<&str>; 7] = [ + Some("fragment"), + None, + None, + None, + None, + None, + Some("fragment"), + ]; + + for i in 0..RESULT.len() { + assert_eq!(uris[i].fragment(), RESULT[i]); } } @@ -658,21 +798,110 @@ mod tests { fn uri_resource() { let uris: Vec<_> = TEST_URIS .iter() - .map(|uri| uri.parse::().unwrap()) + .map(|uri| Uri::try_from(*uri).unwrap()) + .collect(); + + const RESULT: [&str; 7] = [ + "/bar/baz?query#fragment", + "/C:/Users/User/Pictures/screenshot.png", + "/wiki/Hypertext_Transfer_Protocol", + "John.Doe@example.com", + "/", + "/?query=val", + "/#fragment", + ]; + + for i in 0..RESULT.len() { + assert_eq!(uris[i].resource(), RESULT[i]); + } + } + + #[test] + fn uri_is_relative() { + for i in 0..TEST_URIS.len() { + assert!(!Uri::is_relative(TEST_URIS[i])); + } + + for i in 0..TEST_PARTS.len() { + assert!(Uri::is_relative(TEST_PARTS[i])); + } + } + + #[test] + fn uri_from_relative() { + let uris: Vec<_> = TEST_URIS + .iter() + .map(|&uri| Uri::try_from(uri).unwrap()) .collect(); - assert_eq!(uris[0].resource(), "/bar/baz?query#fragment"); - assert_eq!(uris[1].resource(), "/C:/Users/User/Pictures/screenshot.png"); - assert_eq!(uris[2].resource(), "/wiki/Hypertext_Transfer_Protocol"); - assert_eq!(uris[3].resource(), "John.Doe@example.com"); - assert_eq!(uris[4].resource(), "/"); + const RESULT: [&str; 7] = [ + "https://user:info@foo.com:12/bar/baz?query123", + "file:///path", + "https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#fragment", + "mailto:John.Doe@example.com/other-path", + "https://[4b10:bbb0:0:d0::ba7:8001]:443/#paragraph", + "http://example.com/foo/bar/buz", + "https://example.com/?users#1551", + ]; + + for i in 0..RESULT.len() { + let mut uri_part = TEST_PARTS[i].to_string(); + + println!("{}", uris[i].resource()); + assert_eq!( + uris[i].from_relative(&mut uri_part).unwrap().inner, + RESULT[i] + ); + } + } + + #[test] + fn uri_add_part() { + const BASES: [&str; 2] = ["/bar/baz/fizz?query", "/bar/baz?query#some-fragment"]; + const RESULT: [&str; 2] = [ + "/bar/baz/fizz?query#another-fragment", + "/bar/baz?query#some-fragment#another-fragment", + ]; + + for i in 0..BASES.len() { + assert_eq!( + Uri::add_part(BASES[i], "#another-fragment", "#", Some(BASES[i].len())), + RESULT[i] + ); + } + } + + #[test] + fn uri_add_part_start() { + const BASES: [&str; 2] = ["/bar/baz/fizz?query", "/bar/baz?query#some-fragment"]; + const RESULT: [&str; 2] = [ + "/bar/baz/fizz?query#another-fragment", + "/bar/baz?query#another-fragment", + ]; + + for i in 0..BASES.len() { + assert_eq!( + Uri::add_part_start(BASES[i], "#another-fragment", "#"), + RESULT[i] + ); + } + } + + #[test] + fn uri_add_part_end() { + const BASES: [&str; 2] = ["/bar/baz/fizz?query", "/bar/baz?query#some-fragment"]; + const RESULT: [&str; 2] = ["/bar/baz/another", "/bar/another"]; + + for i in 0..BASES.len() { + assert_eq!(Uri::add_part_end(BASES[i], "./another", "/"), RESULT[i]); + } } #[test] fn uri_display() { let uris: Vec<_> = TEST_URIS .iter() - .map(|uri| uri.parse::().unwrap()) + .map(|uri| Uri::try_from(*uri).unwrap()) .collect(); assert_eq!( @@ -690,7 +919,7 @@ mod tests { fn authority_username() { let auths: Vec<_> = TEST_AUTH .iter() - .map(|auth| auth.parse::().unwrap()) + .map(|auth| Authority::try_from(*auth).unwrap()) .collect(); assert_eq!(auths[0].username(), Some("user")); @@ -703,7 +932,7 @@ mod tests { fn authority_password() { let auths: Vec<_> = TEST_AUTH .iter() - .map(|auth| auth.parse::().unwrap()) + .map(|auth| Authority::try_from(*auth).unwrap()) .collect(); assert_eq!(auths[0].password(), Some("info")); @@ -716,7 +945,7 @@ mod tests { fn authority_host() { let auths: Vec<_> = TEST_AUTH .iter() - .map(|auth| auth.parse::().unwrap()) + .map(|auth| Authority::try_from(*auth).unwrap()) .collect(); assert_eq!(auths[0].host(), "foo.com"); @@ -729,7 +958,7 @@ mod tests { fn authority_port() { let auths: Vec<_> = TEST_AUTH .iter() - .map(|auth| auth.parse::().unwrap()) + .map(|auth| Authority::try_from(*auth).unwrap()) .collect(); assert_eq!(auths[0].port(), Some(12)); @@ -741,7 +970,7 @@ mod tests { #[test] fn authority_from_str() { for auth in TEST_AUTH.iter() { - auth.parse::().unwrap(); + Authority::try_from(*auth).unwrap(); } } @@ -749,7 +978,7 @@ mod tests { fn authority_display() { let auths: Vec<_> = TEST_AUTH .iter() - .map(|auth| auth.parse::().unwrap()) + .map(|auth| Authority::try_from(*auth).unwrap()) .collect(); assert_eq!("user:****@foo.com:12", auths[0].to_string());