├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── examples ├── README.md ├── hyper_client.rs ├── hyper_reqbuilder.rs ├── hyper_server.rs ├── iron.rs ├── iron_intercept.rs ├── nickel.rs ├── rocket.rs └── tiny_http.rs ├── fuzz ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── fuzzer_dict └── fuzzers │ ├── logger.rs │ └── server_basic.rs ├── fuzz_server.sh ├── lorem_ipsum.txt └── src ├── bin ├── form_test.rs ├── read_file.rs └── test_form.html ├── client ├── hyper.rs ├── lazy.rs ├── mod.rs └── sized.rs ├── lib.rs ├── local_test.rs ├── mock.rs └── server ├── boundary.rs ├── field.rs ├── hyper.rs ├── iron.rs ├── mod.rs ├── nickel.rs ├── save.rs └── tiny_http.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | *.swp 4 | *~ 5 | # Compiled files 6 | *.o 7 | *.so 8 | *.rlib 9 | *.dll 10 | 11 | # Executables 12 | *.exe 13 | 14 | # Generated by Cargo 15 | target/ 16 | .idea/ 17 | *.iml 18 | 19 | dump.bin 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | branches: 4 | except: 5 | - fuzzing 6 | rust: 7 | - 1.33.0 8 | - stable 9 | - beta 10 | - nightly 11 | os: 12 | - linux 13 | - osx 14 | env: 15 | global: 16 | - RUST_LOG=multipart=trace RUST_BACKTRACE=1 ARGS= 17 | matrix: 18 | include: 19 | - rust: stable 20 | env: ARGS+=--no-default-features --features "nickel" 21 | - rust: stable 22 | env: ARGS+=--features "use_arc_str" 23 | - rust: nightly 24 | env: ARGS+=--features "nightly,rocket" 25 | script: 26 | - cargo build --verbose $ARGS; 27 | - cargo test --verbose $ARGS -- --test-threads=1; 28 | - cargo doc --verbose $ARGS; 29 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multipart" 3 | 4 | version = "0.18.0" 5 | 6 | authors = ["Austin Bonander "] 7 | 8 | description = "A backend-agnostic extension for HTTP libraries that provides support for POST multipart/form-data requests on both client and server." 9 | 10 | keywords = ["form-data", "hyper", "iron", "http", "upload"] 11 | 12 | repository = "http://github.com/abonander/multipart" 13 | 14 | documentation = "http://docs.rs/multipart/" 15 | 16 | license = "MIT OR Apache-2.0" 17 | 18 | readme = "README.md" 19 | 20 | [dependencies] 21 | lazy_static = { version = "1.2.0", optional = true } 22 | log = "0.4" 23 | mime = "0.3.14" 24 | mime_guess = "2.0.1" 25 | rand = "0.8" 26 | safemem = { version = "0.3", optional = true } 27 | tempfile = "3" 28 | clippy = { version = ">=0.0, <0.1", optional = true} 29 | 30 | #Server Dependencies 31 | buf_redux = { version = "0.8", optional = true, default-features = false } 32 | httparse = { version = "1.2", optional = true } 33 | twoway = { version = "0.1", optional = true } 34 | quick-error = { version = "1.2", optional = true } 35 | 36 | # Optional Integrations 37 | hyper = { version = ">=0.9, <0.11", optional = true, default-features = false } 38 | iron = { version = ">=0.4,<0.7", optional = true } 39 | tiny_http = { version = "0.6", optional = true } 40 | nickel = { version = ">=0.10.1", optional = true } 41 | 42 | # Only for Rocket example but dev-dependencies can't be optional 43 | rocket = { version = "0.4", optional = true } 44 | 45 | [dev-dependencies] 46 | env_logger = "0.5" 47 | 48 | [features] 49 | client = [] 50 | default = ["client", "hyper", "iron", "mock", "nickel", "server", "tiny_http"] 51 | server = ["buf_redux", "httparse", "quick-error", "safemem", "twoway"] 52 | mock = [] 53 | nightly = [] 54 | bench = [] 55 | 56 | [[example]] 57 | name = "hyper_client" 58 | required-features = ["client", "mock", "hyper"] 59 | 60 | [[example]] 61 | name = "hyper_reqbuilder" 62 | required-features = ["client", "mock", "hyper"] 63 | 64 | [[example]] 65 | name = "hyper_server" 66 | required-features = ["mock", "hyper", "server"] 67 | 68 | [[example]] 69 | name = "iron" 70 | required-features = ["mock", "iron", "server"] 71 | 72 | [[example]] 73 | name = "iron_intercept" 74 | required-features = ["mock", "iron", "server"] 75 | 76 | [[example]] 77 | name = "nickel" 78 | required-features = ["mock", "nickel", "server"] 79 | 80 | [[example]] 81 | name = "tiny_http" 82 | required-features = ["mock", "tiny_http", "server"] 83 | 84 | [[example]] 85 | name = "rocket" 86 | required-features = ["mock", "rocket", "server"] 87 | 88 | [[bin]] 89 | name = "form_test" 90 | required-features = ["mock", "hyper", "server"] 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Austin Bonander 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 The `multipart` Crate Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multipart [![Build Status](https://travis-ci.org/abonander/multipart.svg?branch=master)](https://travis-ci.org/abonander/multipart) [![On Crates.io](https://img.shields.io/crates/v/multipart.svg)](https://crates.io/crates/multipart) 2 | 3 | Client- and server-side abstractions for HTTP file uploads (POST requests with `Content-Type: multipart/form-data`). 4 | 5 | Supports several different (**sync**hronous API) HTTP crates. 6 | **Async**hronous (i.e. `futures`-based) API support will be provided by [multipart-async]. 7 | 8 | ##### Minimum supported Rust version: 1.36.0 9 | 10 | ##### Maintenance Status: Passive 11 | 12 | As the web ecosystem in Rust moves towards asynchronous APIs, the need for this crate in synchronous 13 | API form becomes dubious. This crate in its current form is usable enough, so as of June 2020 it 14 | is now in passive maintenance mode; bug reports will be addressed as time permits and PRs will be 15 | accepted but otherwise no new development of the existing API is taking place. 16 | 17 | Look for a release of [multipart-async] soon which targets newer releases of Hyper. 18 | 19 | ### [Documentation](http://docs.rs/multipart/) 20 | 21 | ## Integrations 22 | 23 | Example files demonstrating how to use `multipart` with these crates are available under [`examples/`](examples). 24 | 25 | ### [Hyper ![](https://img.shields.io/crates/v/hyper.svg)](https://crates.io/crates/hyper) 26 | via the `hyper` feature (enabled by default). 27 | 28 | **Note: Hyper 0.9, 0.10 (synchronous API) only**; support for asynchronous APIs will be provided by [multipart-async]. 29 | 30 | Client integration includes support for regular `hyper::client::Request` objects via `multipart::client::Multipart`, as well 31 | as integration with the new `hyper::Client` API via `multipart::client::lazy::Multipart` (new in 0.5). 32 | 33 | Server integration for `hyper::server::Request` via `multipart::server::Multipart`. 34 | 35 | ### [Iron ![](https://img.shields.io/crates/v/iron.svg)](https://crates.io/crates/iron) 36 | via the `iron` feature. 37 | 38 | Provides regular server-side integration with `iron::Request` via `multipart::server::Multipart`, 39 | as well as a convenient `BeforeMiddleware` implementation in `multipart::server::iron::Intercept`. 40 | 41 | ### [Nickel ![](https://img.shields.io/crates/v/nickel.svg)](https://crates.io/crates/nickel) returning to `multipart` in 0.14! 42 | via the `nickel` feature. 43 | 44 | Provides server-side integration with `&mut nickel::Request` via `multipart::server::Multipart`. 45 | 46 | ### [tiny_http ![](https://img.shields.io/crates/v/tiny_http.svg)](https://crates.io/crates/tiny_http) 47 | via the `tiny_http` feature. 48 | 49 | Provides server-side integration with `tiny_http::Request` via `multipart::server::Multipart`. 50 | 51 | ### [Rocket ![](https://img.shields.io/crates/v/rocket.svg)](https://crates.io/crates/rocket) 52 | 53 | Direct integration is not provided as the Rocket folks seem to want to handle `multipart/form-data` 54 | behind the scenes which would supercede any integration with `multipart`. However, an example is available 55 | showing how to use `multipart` on a Rocket server: [examples/rocket.rs](examples/rocket.rs) 56 | 57 | ## ⚡ Powered By ⚡ 58 | 59 | ### [buf_redux ![](https://img.shields.io/crates/v/buf_redux.svg)](https://crates.io/crates/buf_redux) 60 | 61 | Customizable drop-in `std::io::BufReader` replacement, created to be used in this crate. 62 | Needed because it can read more bytes into the buffer without the buffer being empty, necessary 63 | when a boundary falls across two reads. (It was easier to author a new crate than try to get this added 64 | to `std::io::BufReader`.) 65 | 66 | ### [httparse ![](https://img.shields.io/crates/v/httparse.svg)](https://crates.io/crates/httparse) 67 | 68 | Fast, zero-copy HTTP header parsing, used to read field headers in `multipart/form-data` request bodies. 69 | 70 | ### [twoway ![](https://img.shields.io/crates/v/twoway.svg)](https://crates.io/crates/twoway) 71 | 72 | Fast string and byte-string search. Used to find boundaries in the request body. Uses SIMD acceleration 73 | when possible. 74 | 75 | ## License 76 | 77 | Licensed under either of 78 | 79 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 80 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 81 | 82 | at your option. 83 | 84 | ## Contribution 85 | 86 | Unless you explicitly state otherwise, any contribution intentionally submitted 87 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 88 | additional terms or conditions. 89 | 90 | [multipart-async]: https://github.com/abonander/multipart-async 91 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | `multipart` Examples 2 | =========================== 3 | 4 | These example files show how to use `multipart` with the various crates it integrates with. 5 | 6 | These files carry the same licenses as [`multipart` itself](https://github.com/abonander/multipart#license), though this may be lightened to a copyright-free license in the near future. 7 | 8 | ## Client 9 | 10 | Examples for the client-side integrations of `multipart`'s API. 11 | 12 | [`hyper_client`](hyper_client.rs) 13 | --------------------------------- 14 | Author: [abonander] 15 | 16 | This example showcases usage of `multipart` with the `hyper::client::Request` API. 17 | 18 | ``` 19 | $ cargo run --example hyper_client 20 | ``` 21 | 22 | [`hyper_reqbuilder`](hyper_reqbuilder.rs) 23 | ----------------------------------------- 24 | Author: [abonander] 25 | 26 | This example showcases usage of `multipart` with Hyper's new `Client` API, 27 | via the lazy-writing capabilities of `multipart::client::lazy`. 28 | 29 | ``` 30 | $ cargo run --example hyper_reqbuilder 31 | ``` 32 | 33 | ## Server 34 | 35 | [`hyper_server`](hyper_server.rs) 36 | --------------------------------- 37 | Author: [Puhrez] 38 | 39 | This example shows how to use `multipart` with a [`hyper::Server`] (http://hyper.rs/) to intercept multipart requests. 40 | 41 | ``` 42 | $ cargo run --example hyper_server 43 | ``` 44 | 45 | [`iron`](iron.rs) 46 | ----------------- 47 | Author: [White-Oak] 48 | 49 | This example shows how to use `multipart` with the [Iron web application framework](https://github.com/iron/iron), via `multipart`'s support 50 | for the `iron::Request` type. 51 | 52 | To run: 53 | 54 | ``` 55 | $ cargo run --features iron --example iron 56 | ``` 57 | 58 | [`iron_intercept`](iron_intercept.rs) 59 | ------------------------------------- 60 | Author: [abonander] 61 | 62 | This example shows how to use `multipart`'s specialized `Intercept` middleware with Iron, which reads out all fields and 63 | files to local storage so they can be accessed arbitrarily. 64 | 65 | ``` 66 | $ cargo run --features iron --example iron_intercept 67 | ``` 68 | 69 | [`tiny_http`](tiny_http.rs) 70 | --------------------------- 71 | Author: [White-Oak] 72 | 73 | This example shows how to use `multipart` with the [`tiny_http` crate](https://crates.io/crates/tiny_http), via `multipart`'s support for the `tiny_http::Request` type. 74 | 75 | ``` 76 | $ cargo run --features tiny_http --example tiny_http 77 | ``` 78 | 79 | [`hyper_server`](hyper_server.rs) 80 | --------------------------------- 81 | Author: [Puhrez] 82 | 83 | This example shows how to use `multipart` with a [`hyper::Server`] (http://hyper.rs/) to intercept multipart requests. 84 | 85 | ``` 86 | $ cargo run --example hyper_server 87 | ``` 88 | 89 | [`nickel`](nickel.rs) 90 | --------------------- 91 | Author: [iamsebastian] 92 | 93 | This example shows how to use `multipart` to handle multipart uploads in [nickel.rs](https://nickel.rs). 94 | 95 | ``` 96 | $ cargo run --example nickel --features nickel 97 | ``` 98 | 99 | [Rocket](rocket.rs) 100 | ------------------- 101 | Author: [abonander] 102 | 103 | This example shows how `multipart`'s server API can be used with [Rocket](https://rocket.rs) without 104 | explicit support (the Rocket folks seem to want to handle `multipart/form-data` behind the scenes 105 | but haven't gotten around to implementing it yet; this would supercede any integration from `multipart`). 106 | 107 | ``` 108 | $ cargo run --example rocket --features "rocket" 109 | ``` 110 | 111 | [iamsebastian]: https://github.com/iamsebastian 112 | [Puhrez]: https://github.com/puhrez 113 | [White-Oak]: https://github.com/white-oak 114 | [abonander]: https://github.com/abonander 115 | 116 | -------------------------------------------------------------------------------- /examples/hyper_client.rs: -------------------------------------------------------------------------------- 1 | extern crate hyper; 2 | extern crate multipart; 3 | 4 | use hyper::client::Request; 5 | use hyper::method::Method; 6 | use hyper::net::Streaming; 7 | 8 | use multipart::client::Multipart; 9 | 10 | use std::io::Read; 11 | 12 | fn main() { 13 | let url = "http://localhost:80".parse() 14 | .expect("Failed to parse URL"); 15 | 16 | let request = Request::new(Method::Post, url) 17 | .expect("Failed to create request"); 18 | 19 | let mut multipart = Multipart::from_request(request) 20 | .expect("Failed to create Multipart"); 21 | 22 | write_body(&mut multipart) 23 | .expect("Failed to write multipart body"); 24 | 25 | let mut response = multipart.send().expect("Failed to send multipart request"); 26 | 27 | if !response.status.is_success() { 28 | let mut res = String::new(); 29 | response.read_to_string(&mut res).expect("failed to read response"); 30 | println!("response reported unsuccessful: {:?}\n {}", response, res); 31 | } 32 | 33 | // Optional: read out response 34 | } 35 | 36 | fn write_body(multi: &mut Multipart>) -> hyper::Result<()> { 37 | let mut binary = "Hello world from binary!".as_bytes(); 38 | 39 | multi.write_text("text", "Hello, world!")?; 40 | multi.write_file("file", "lorem_ipsum.txt")?; 41 | // &[u8] impl Read 42 | multi.write_stream("binary", &mut binary, None, None) 43 | .and(Ok(())) 44 | } 45 | -------------------------------------------------------------------------------- /examples/hyper_reqbuilder.rs: -------------------------------------------------------------------------------- 1 | extern crate hyper; 2 | extern crate multipart; 3 | 4 | use hyper::Client; 5 | 6 | use multipart::client::lazy::Multipart; 7 | 8 | fn main() { 9 | let mut binary = "Hello world in binary!".as_bytes(); 10 | 11 | let _response = Multipart::new() 12 | .add_text("text", "Hello, world!") 13 | .add_file("file", "lorem_ipsum.txt") 14 | // A little extra type info needed. 15 | .add_stream("binary", &mut binary, None as Option<&str>, None) 16 | // Request is sent here 17 | .client_request(&Client::new(), "http://localhost:80") 18 | .expect("Error sending multipart request"); 19 | } -------------------------------------------------------------------------------- /examples/hyper_server.rs: -------------------------------------------------------------------------------- 1 | extern crate hyper; 2 | extern crate multipart; 3 | 4 | use std::io; 5 | use hyper::server::{Handler, Server, Request, Response}; 6 | use hyper::status::StatusCode; 7 | use hyper::server::response::Response as HyperResponse; 8 | use multipart::server::hyper::{Switch, MultipartHandler, HyperRequest}; 9 | use multipart::server::{Multipart, Entries, SaveResult}; 10 | use multipart::mock::StdoutTee; 11 | 12 | struct NonMultipart; 13 | impl Handler for NonMultipart { 14 | fn handle(&self, _: Request, mut res: Response) { 15 | *res.status_mut() = StatusCode::ImATeapot; 16 | res.send(b"Please send a multipart req :(\n").unwrap(); 17 | } 18 | } 19 | 20 | struct EchoMultipart; 21 | impl MultipartHandler for EchoMultipart { 22 | fn handle_multipart(&self, mut multipart: Multipart, res: HyperResponse) { 23 | match multipart.save().temp() { 24 | SaveResult::Full(entries) => process_entries(res, entries).unwrap(), 25 | SaveResult::Partial(entries, error) => { 26 | println!("Errors saving multipart:\n{:?}", error); 27 | process_entries(res, entries.into()).unwrap(); 28 | } 29 | SaveResult::Error(error) => { 30 | println!("Errors saving multipart:\n{:?}", error); 31 | res.send(format!("An error occurred {}", error).as_bytes()).unwrap(); 32 | } 33 | }; 34 | } 35 | } 36 | 37 | fn process_entries(res: HyperResponse, entries: Entries) -> io::Result<()> { 38 | let mut res = res.start()?; 39 | let stdout = io::stdout(); 40 | let out = StdoutTee::new(&mut res, &stdout); 41 | entries.write_debug(out) 42 | } 43 | 44 | fn main() { 45 | println!("Listening on 0.0.0.0:3333"); 46 | Server::http("0.0.0.0:3333").unwrap().handle( 47 | Switch::new( 48 | NonMultipart, 49 | EchoMultipart 50 | )).unwrap(); 51 | } 52 | -------------------------------------------------------------------------------- /examples/iron.rs: -------------------------------------------------------------------------------- 1 | extern crate multipart; 2 | extern crate iron; 3 | 4 | extern crate env_logger; 5 | 6 | use std::io::{self, Write}; 7 | use multipart::mock::StdoutTee; 8 | use multipart::server::{Multipart, Entries, SaveResult}; 9 | use iron::prelude::*; 10 | use iron::status; 11 | 12 | fn main() { 13 | env_logger::init(); 14 | 15 | Iron::new(process_request).http("localhost:80").expect("Could not bind localhost:80"); 16 | } 17 | 18 | /// Processes a request and returns response or an occured error. 19 | fn process_request(request: &mut Request) -> IronResult { 20 | // Getting a multipart reader wrapper 21 | match Multipart::from_request(request) { 22 | Ok(mut multipart) => { 23 | // Fetching all data and processing it. 24 | // save().temp() reads the request fully, parsing all fields and saving all files 25 | // in a new temporary directory under the OS temporary directory. 26 | match multipart.save().temp() { 27 | SaveResult::Full(entries) => process_entries(entries), 28 | SaveResult::Partial(entries, reason) => { 29 | process_entries(entries.keep_partial())?; 30 | Ok(Response::with(( 31 | status::BadRequest, 32 | format!("error reading request: {}", reason.unwrap_err()) 33 | ))) 34 | } 35 | SaveResult::Error(error) => Ok(Response::with(( 36 | status::BadRequest, 37 | format!("error reading request: {}", error) 38 | ))), 39 | } 40 | } 41 | Err(_) => { 42 | Ok(Response::with((status::BadRequest, "The request is not multipart"))) 43 | } 44 | } 45 | } 46 | 47 | /// Processes saved entries from multipart request. 48 | /// Returns an OK response or an error. 49 | fn process_entries(entries: Entries) -> IronResult { 50 | let mut data = Vec::new(); 51 | 52 | { 53 | let stdout = io::stdout(); 54 | let tee = StdoutTee::new(&mut data, &stdout); 55 | entries.write_debug(tee).map_err(|e| { 56 | IronError::new( 57 | e, 58 | (status::InternalServerError, "Error printing request fields") 59 | ) 60 | })?; 61 | } 62 | 63 | let _ = writeln!(data, "Entries processed"); 64 | 65 | Ok(Response::with((status::Ok, data))) 66 | } 67 | -------------------------------------------------------------------------------- /examples/iron_intercept.rs: -------------------------------------------------------------------------------- 1 | extern crate iron; 2 | extern crate multipart; 3 | 4 | use iron::prelude::*; 5 | 6 | use multipart::server::Entries; 7 | use multipart::server::iron::Intercept; 8 | 9 | fn main() { 10 | // We start with a basic request handler chain. 11 | let mut chain = Chain::new(|req: &mut Request| 12 | if let Some(entries) = req.extensions.get::() { 13 | Ok(Response::with(format!("{:?}", entries))) 14 | } else { 15 | Ok(Response::with("Not a multipart request")) 16 | } 17 | ); 18 | 19 | // `Intercept` will read out the entries and place them as an extension in the request. 20 | // It has various builder-style methods for changing how it will behave, but has sane settings 21 | // by default. 22 | chain.link_before(Intercept::default()); 23 | 24 | Iron::new(chain).http("localhost:80").unwrap(); 25 | } -------------------------------------------------------------------------------- /examples/nickel.rs: -------------------------------------------------------------------------------- 1 | extern crate multipart; 2 | extern crate nickel; 3 | 4 | use std::io::{self, Write}; 5 | use nickel::{Action, HttpRouter, MiddlewareResult, Nickel, Request, Response}; 6 | use nickel::status::StatusCode; 7 | 8 | use multipart::server::nickel::MultipartBody; 9 | use multipart::server::{Entries, SaveResult}; 10 | use multipart::mock::StdoutTee; 11 | 12 | fn handle_multipart<'mw>(req: &mut Request, mut res: Response<'mw>) -> MiddlewareResult<'mw> { 13 | match (*req).multipart_body() { 14 | Some(mut multipart) => { 15 | match multipart.save().temp() { 16 | SaveResult::Full(entries) => process_entries(res, entries), 17 | 18 | SaveResult::Partial(entries, e) => { 19 | println!("Partial errors ... {:?}", e); 20 | return process_entries(res, entries.keep_partial()); 21 | }, 22 | 23 | SaveResult::Error(e) => { 24 | println!("There are errors in multipart POSTing ... {:?}", e); 25 | res.set(StatusCode::InternalServerError); 26 | return res.send(format!("Server could not handle multipart POST! {:?}", e)); 27 | }, 28 | } 29 | } 30 | None => { 31 | res.set(StatusCode::BadRequest); 32 | return res.send("Request seems not was a multipart request") 33 | } 34 | } 35 | } 36 | 37 | /// Processes saved entries from multipart request. 38 | /// Returns an OK response or an error. 39 | fn process_entries<'mw>(res: Response<'mw>, entries: Entries) -> MiddlewareResult<'mw> { 40 | let stdout = io::stdout(); 41 | let mut res = res.start()?; 42 | if let Err(e) = entries.write_debug(StdoutTee::new(&mut res, &stdout)) { 43 | writeln!(res, "Error while reading entries: {}", e).expect("writeln"); 44 | } 45 | 46 | Ok(Action::Halt(res)) 47 | } 48 | 49 | fn main() { 50 | let mut srv = Nickel::new(); 51 | 52 | srv.post("/multipart_upload/", handle_multipart); 53 | 54 | // Start this example via: 55 | // 56 | // `cargo run --example nickel --features nickel` 57 | // 58 | // And - if you are in the root of this repository - do an example 59 | // upload via: 60 | // 61 | // `curl -F file=@LICENSE 'http://localhost:6868/multipart_upload/'` 62 | srv.listen("127.0.0.1:6868").expect("Failed to bind server"); 63 | } 64 | -------------------------------------------------------------------------------- /examples/rocket.rs: -------------------------------------------------------------------------------- 1 | // Example usage with Rocket (https://rocket.rs) 2 | // 3 | // Direct integration is not provided at this time as it appears the Rocket folks would prefer 4 | // to handle multipart requests behind the scenes. 5 | #![feature(proc_macro_hygiene, decl_macro)] 6 | #![feature(plugin, custom_attribute)] 7 | 8 | extern crate multipart; 9 | #[macro_use] 10 | extern crate rocket; 11 | 12 | use multipart::mock::StdoutTee; 13 | use multipart::server::Multipart; 14 | use multipart::server::save::Entries; 15 | use multipart::server::save::SaveResult::*; 16 | 17 | use rocket::Data; 18 | use rocket::http::{ContentType, Status}; 19 | use rocket::response::Stream; 20 | use rocket::response::status::Custom; 21 | 22 | use std::io::{self, Cursor, Write}; 23 | 24 | #[post("/upload", data = "")] 25 | // signature requires the request to have a `Content-Type` 26 | fn multipart_upload(cont_type: &ContentType, data: Data) -> Result>>, Custom> { 27 | // this and the next check can be implemented as a request guard but it seems like just 28 | // more boilerplate than necessary 29 | if !cont_type.is_form_data() { 30 | return Err(Custom( 31 | Status::BadRequest, 32 | "Content-Type not multipart/form-data".into() 33 | )); 34 | } 35 | 36 | let (_, boundary) = cont_type.params().find(|&(k, _)| k == "boundary").ok_or_else( 37 | || Custom( 38 | Status::BadRequest, 39 | "`Content-Type: multipart/form-data` boundary param not provided".into() 40 | ) 41 | )?; 42 | 43 | match process_upload(boundary, data) { 44 | Ok(resp) => Ok(Stream::from(Cursor::new(resp))), 45 | Err(err) => Err(Custom(Status::InternalServerError, err.to_string())) 46 | } 47 | } 48 | 49 | fn process_upload(boundary: &str, data: Data) -> io::Result> { 50 | let mut out = Vec::new(); 51 | 52 | // saves all fields, any field longer than 10kB goes to a temporary directory 53 | // Entries could implement FromData though that would give zero control over 54 | // how the files are saved; Multipart would be a good impl candidate though 55 | match Multipart::with_body(data.open(), boundary).save().temp() { 56 | Full(entries) => process_entries(entries, &mut out)?, 57 | Partial(partial, reason) => { 58 | writeln!(out, "Request partially processed: {:?}", reason)?; 59 | if let Some(field) = partial.partial { 60 | writeln!(out, "Stopped on field: {:?}", field.source.headers)?; 61 | } 62 | 63 | process_entries(partial.entries, &mut out)? 64 | }, 65 | Error(e) => return Err(e), 66 | } 67 | 68 | Ok(out) 69 | } 70 | 71 | // having a streaming output would be nice; there's one for returning a `Read` impl 72 | // but not one that you can `write()` to 73 | fn process_entries(entries: Entries, mut out: &mut Vec) -> io::Result<()> { 74 | { 75 | let stdout = io::stdout(); 76 | let tee = StdoutTee::new(&mut out, &stdout); 77 | entries.write_debug(tee)?; 78 | } 79 | 80 | writeln!(out, "Entries processed") 81 | } 82 | 83 | fn main() { 84 | rocket::ignite().mount("/", routes![multipart_upload]).launch(); 85 | } 86 | -------------------------------------------------------------------------------- /examples/tiny_http.rs: -------------------------------------------------------------------------------- 1 | extern crate tiny_http; 2 | extern crate multipart; 3 | 4 | use std::io::{self, Cursor, Write}; 5 | use multipart::server::{Multipart, Entries, SaveResult}; 6 | use multipart::mock::StdoutTee; 7 | use tiny_http::{Response, StatusCode, Request}; 8 | fn main() { 9 | // Starting a server on `localhost:80` 10 | let server = tiny_http::Server::http("localhost:80").expect("Could not bind localhost:80"); 11 | loop { 12 | // This blocks until the next request is received 13 | let mut request = server.recv().unwrap(); 14 | 15 | // Processes a request and returns response or an occured error 16 | let result = process_request(&mut request); 17 | let resp = match result { 18 | Ok(resp) => resp, 19 | Err(e) => { 20 | println!("An error has occured during request proccessing: {:?}", e); 21 | build_response(500, "The received data was not correctly proccessed on the server") 22 | } 23 | }; 24 | 25 | // Answers with a response to a client 26 | request.respond(resp).unwrap(); 27 | } 28 | } 29 | 30 | type RespBody = Cursor>; 31 | 32 | /// Processes a request and returns response or an occured error. 33 | fn process_request(request: &mut Request) -> io::Result> { 34 | // Getting a multipart reader wrapper 35 | match Multipart::from_request(request) { 36 | Ok(mut multipart) => { 37 | // Fetching all data and processing it. 38 | // save().temp() reads the request fully, parsing all fields and saving all files 39 | // in a new temporary directory under the OS temporary directory. 40 | match multipart.save().temp() { 41 | SaveResult::Full(entries) => process_entries(entries), 42 | SaveResult::Partial(entries, reason) => { 43 | process_entries(entries.keep_partial())?; 44 | // We don't set limits 45 | Err(reason.unwrap_err()) 46 | } 47 | SaveResult::Error(error) => Err(error), 48 | } 49 | } 50 | Err(_) => Ok(build_response(400, "The request is not multipart")), 51 | } 52 | } 53 | 54 | /// Processes saved entries from multipart request. 55 | /// Returns an OK response or an error. 56 | fn process_entries(entries: Entries) -> io::Result> { 57 | let mut data = Vec::new(); 58 | 59 | { 60 | let stdout = io::stdout(); 61 | let tee = StdoutTee::new(&mut data, &stdout); 62 | entries.write_debug(tee)?; 63 | } 64 | 65 | writeln!(data, "Entries processed")?; 66 | 67 | Ok(build_response(200, data)) 68 | } 69 | 70 | fn build_response>>(status_code: u16, data: D) -> Response { 71 | let data = data.into(); 72 | let data_len = data.len(); 73 | Response::new(StatusCode(status_code), 74 | vec![], 75 | Cursor::new(data), 76 | Some(data_len), 77 | None) 78 | } 79 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | libfuzzer 4 | corpus 5 | artifacts 6 | -------------------------------------------------------------------------------- /fuzz/Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "multipart-fuzz" 3 | version = "0.0.1" 4 | dependencies = [ 5 | "libfuzzer-sys 0.1.0 (git+https://github.com/rust-fuzz/libfuzzer-sys.git)", 6 | "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 7 | "multipart 0.11.0", 8 | ] 9 | 10 | [[package]] 11 | name = "buf_redux" 12 | version = "0.6.1" 13 | source = "registry+https://github.com/rust-lang/crates.io-index" 14 | dependencies = [ 15 | "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", 16 | "safemem 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 17 | ] 18 | 19 | [[package]] 20 | name = "gcc" 21 | version = "0.3.45" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | 24 | [[package]] 25 | name = "httparse" 26 | version = "1.2.1" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | 29 | [[package]] 30 | name = "libc" 31 | version = "0.2.21" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | 34 | [[package]] 35 | name = "libfuzzer-sys" 36 | version = "0.1.0" 37 | source = "git+https://github.com/rust-fuzz/libfuzzer-sys.git#36a3928eef5c3c38eb0f251962395bb510c39d46" 38 | dependencies = [ 39 | "gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)", 40 | ] 41 | 42 | [[package]] 43 | name = "log" 44 | version = "0.3.7" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | 47 | [[package]] 48 | name = "memchr" 49 | version = "0.1.11" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | dependencies = [ 52 | "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", 53 | ] 54 | 55 | [[package]] 56 | name = "mime" 57 | version = "0.2.3" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | dependencies = [ 60 | "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 61 | ] 62 | 63 | [[package]] 64 | name = "mime_guess" 65 | version = "1.8.1" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | dependencies = [ 68 | "mime 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 69 | "phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", 70 | "phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", 71 | "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 72 | ] 73 | 74 | [[package]] 75 | name = "multipart" 76 | version = "0.11.0" 77 | dependencies = [ 78 | "buf_redux 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", 79 | "httparse 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 80 | "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", 81 | "mime 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", 82 | "mime_guess 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 83 | "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 84 | "safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 85 | "tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 86 | "twoway 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 87 | ] 88 | 89 | [[package]] 90 | name = "phf" 91 | version = "0.7.21" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | dependencies = [ 94 | "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", 95 | ] 96 | 97 | [[package]] 98 | name = "phf_codegen" 99 | version = "0.7.21" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | dependencies = [ 102 | "phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", 103 | "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", 104 | ] 105 | 106 | [[package]] 107 | name = "phf_generator" 108 | version = "0.7.21" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | dependencies = [ 111 | "phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)", 112 | "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 113 | ] 114 | 115 | [[package]] 116 | name = "phf_shared" 117 | version = "0.7.21" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | dependencies = [ 120 | "siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 121 | "unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 122 | ] 123 | 124 | [[package]] 125 | name = "rand" 126 | version = "0.3.15" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | dependencies = [ 129 | "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", 130 | ] 131 | 132 | [[package]] 133 | name = "rustc_version" 134 | version = "0.1.7" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | dependencies = [ 137 | "semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", 138 | ] 139 | 140 | [[package]] 141 | name = "safemem" 142 | version = "0.1.1" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | 145 | [[package]] 146 | name = "safemem" 147 | version = "0.2.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | 150 | [[package]] 151 | name = "semver" 152 | version = "0.1.20" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | 155 | [[package]] 156 | name = "siphasher" 157 | version = "0.2.2" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | 160 | [[package]] 161 | name = "tempdir" 162 | version = "0.3.5" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | dependencies = [ 165 | "rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 166 | ] 167 | 168 | [[package]] 169 | name = "twoway" 170 | version = "0.1.3" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | dependencies = [ 173 | "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", 174 | ] 175 | 176 | [[package]] 177 | name = "unicase" 178 | version = "1.4.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | dependencies = [ 181 | "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 182 | ] 183 | 184 | [metadata] 185 | "checksum buf_redux 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e1497634c131ba13483b6e8123f69e219253b018bb32949eefd55c6b5051585d" 186 | "checksum gcc 0.3.45 (registry+https://github.com/rust-lang/crates.io-index)" = "40899336fb50db0c78710f53e87afc54d8c7266fb76262fecc78ca1a7f09deae" 187 | "checksum httparse 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a6e7a63e511f9edffbab707141fbb8707d1a3098615fb2adbd5769cdfcc9b17d" 188 | "checksum libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "88ee81885f9f04bff991e306fea7c1c60a5f0f9e409e99f6b40e3311a3363135" 189 | "checksum libfuzzer-sys 0.1.0 (git+https://github.com/rust-fuzz/libfuzzer-sys.git)" = "" 190 | "checksum log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "5141eca02775a762cc6cd564d8d2c50f67c0ea3a372cbf1c51592b3e029e10ad" 191 | "checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" 192 | "checksum mime 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5514f038123342d01ee5f95129e4ef1e0470c93bc29edf058a46f9ee3ba6737e" 193 | "checksum mime_guess 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "76da6df85047af8c0edfa53f48eb1073012ce1cc95c8fedc0a374f659a89dd65" 194 | "checksum phf 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "cb325642290f28ee14d8c6201159949a872f220c62af6e110a56ea914fbe42fc" 195 | "checksum phf_codegen 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d62594c0bb54c464f633175d502038177e90309daf2e0158be42ed5f023ce88f" 196 | "checksum phf_generator 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "6b07ffcc532ccc85e3afc45865469bf5d9e4ef5bfcf9622e3cfe80c2d275ec03" 197 | "checksum phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "07e24b0ca9643bdecd0632f2b3da6b1b89bbb0030e0b992afc1113b23a7bc2f2" 198 | "checksum rand 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "022e0636ec2519ddae48154b028864bdce4eaf7d35226ab8e65c611be97b189d" 199 | "checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" 200 | "checksum safemem 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "725b3bf47ae40b4abcd27b5f0a9540369426a29f7b905649b3e1468e13e22009" 201 | "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f" 202 | "checksum semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" 203 | "checksum siphasher 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0df90a788073e8d0235a67e50441d47db7c8ad9debd91cbf43736a2a92d36537" 204 | "checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6" 205 | "checksum twoway 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e267e178055eb3b081224bbef62d4f508ae3c9f000b6ae6ccdb04a0d9c34b77f" 206 | "checksum unicase 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "13a5906ca2b98c799f4b1ab4557b76367ebd6ae5ef14930ec841c74aed5f3764" 207 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "multipart-fuzz" 3 | version = "0.0.1" 4 | authors = ["Automatically generated"] 5 | publish = false 6 | 7 | [package.metadata] 8 | cargo-fuzz = true 9 | 10 | [dependencies] 11 | log = "*" 12 | 13 | [dependencies.multipart] 14 | path = ".." 15 | default-features = false 16 | features = ["mock", "client", "server"] 17 | 18 | [dependencies.libfuzzer-sys] 19 | git = "https://github.com/rust-fuzz/libfuzzer-sys.git" 20 | 21 | # Prevent this from interfering with workspaces 22 | [workspace] 23 | members = ["."] 24 | 25 | [[bin]] 26 | name = "server_basic" 27 | path = "fuzzers/server_basic.rs" 28 | -------------------------------------------------------------------------------- /fuzz/fuzzer_dict: -------------------------------------------------------------------------------- 1 | "Content-Disposition: form-data; name=" 2 | # CR LF 3 | "\x0D\x0A" 4 | "Content-Type:" 5 | "filename=" 6 | "--12--34--56" 7 | "--12--34--56--" -------------------------------------------------------------------------------- /fuzz/fuzzers/logger.rs: -------------------------------------------------------------------------------- 1 | extern crate log; 2 | 3 | use self::log::{LogLevelFilter, Log, LogMetadata, LogRecord}; 4 | 5 | const MAX_LOG_LEVEL: LogLevelFilter = LogLevelFilter::Off; 6 | 7 | struct Logger; 8 | 9 | impl Log for Logger { 10 | fn enabled(&self, metadata: &LogMetadata) -> bool { 11 | metadata.level() <= MAX_LOG_LEVEL 12 | } 13 | 14 | fn log(&self, record: &LogRecord) { 15 | println!("{}: {}", record.level(), record.args()); 16 | } 17 | } 18 | 19 | static LOGGER: Logger = Logger; 20 | 21 | pub fn init() { 22 | let _ = unsafe { 23 | log::set_logger_raw(|max_lvl| { 24 | max_lvl.set(MAX_LOG_LEVEL); 25 | &LOGGER 26 | }) 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /fuzz/fuzzers/server_basic.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | extern crate libfuzzer_sys; 3 | extern crate multipart; 4 | 5 | #[macro_use] 6 | extern crate log; 7 | 8 | use multipart::server::{Multipart, MultipartData}; 9 | use multipart::mock::ServerRequest; 10 | 11 | mod logger; 12 | 13 | use std::io::BufRead; 14 | 15 | const BOUNDARY: &'static str = "--12--34--56"; 16 | 17 | #[export_name="rust_fuzzer_test_input"] 18 | pub extern fn go(data: &[u8]) { 19 | logger::init(); 20 | 21 | info!("Fuzzing started! Data len: {}", data.len()); 22 | 23 | do_fuzz(data); 24 | 25 | info!("Finished fuzzing iteration"); 26 | } 27 | 28 | fn do_fuzz(data: &[u8]) { 29 | 30 | if data.len() < BOUNDARY.len() { return; } 31 | 32 | let req = ServerRequest::new(data, BOUNDARY); 33 | 34 | info!("Request constructed!"); 35 | 36 | let mut multipart = if let Ok(multi) = Multipart::from_request(req) { 37 | multi 38 | } else { 39 | panic!("This shouldn't have failed") 40 | }; 41 | 42 | // A lot of requests will be malformed 43 | while let Ok(Some(entry)) = multipart.read_entry() { 44 | info!("read_entry() loop!"); 45 | match entry.data { 46 | MultipartData::Text(_) => (), 47 | MultipartData::File(mut file) => loop { 48 | let consume = file.fill_buf().expect("This shouldn't fail").len(); 49 | 50 | info!("Consume amt: {}", consume); 51 | 52 | if consume == 0 { break; } 53 | file.consume(consume); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /fuzz_server.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # pwd 3 | cargo fuzz run server_basic -- -dict=fuzz/fuzzer_dict -only_ascii=1 -timeout=60 ${FUZZ_LEN:+ -max_len=$FUZZ_LEN} $@ 4 | -------------------------------------------------------------------------------- /lorem_ipsum.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed dignissim, lectus a placerat vestibulum, mi massa dapibus ante, placerat efficitur dui est non lorem. Donec metus lacus, ultricies id suscipit sed, varius et augue. Vivamus erat lectus, interdum in quam non, euismod venenatis eros. Sed vitae magna orci. Duis finibus velit sem, eu luctus urna fringilla vitae. Suspendisse volutpat eros a tincidunt porttitor. Sed ut massa pretium, tempor neque nec, lobortis quam. Etiam vestibulum mauris eu sem consectetur, condimentum fermentum libero vulputate. Vestibulum porttitor leo et blandit condimentum. Pellentesque auctor odio eros, nec placerat lorem ultrices vitae. Suspendisse pretium tellus a ipsum sagittis consequat. Nullam pulvinar ligula ut fermentum laoreet. Maecenas rhoncus ut neque vitae tincidunt. Maecenas tincidunt at orci sed scelerisque. Sed porttitor tincidunt purus, ut efficitur leo lobortis vitae. Aenean et orci dolor. 2 | 3 | Vestibulum at laoreet felis. Cras et justo libero. Morbi pulvinar tincidunt odio, id finibus magna tincidunt non. Nulla facilisi. In at finibus lacus. Phasellus non volutpat dui. Vivamus porta fermentum dignissim. Nulla facilisi. Mauris laoreet semper ex lacinia interdum. Donec et dui non orci cursus scelerisque vulputate non neque. Fusce efficitur maximus turpis tempor interdum. Proin sit amet nunc pretium, varius dui sed, pretium nulla. Integer commodo orci ut felis bibendum feugiat. 4 | 5 | In interdum pulvinar tellus, quis porta eros consectetur in. Ut pharetra sem quam, id congue urna tempus eu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed egestas mattis ex quis malesuada. Quisque justo enim, posuere non arcu id, dictum pulvinar ante. Curabitur at rhoncus turpis. Donec in odio ut dolor elementum ultricies. Integer massa lacus, ullamcorper non nisi sed, scelerisque commodo lacus. Aliquam erat volutpat. Etiam eleifend libero tincidunt lobortis dignissim. Aliquam in odio sed libero sollicitudin pharetra. 6 | 7 | In quis consectetur ex, nec tempus mi. Donec commodo urna augue, non hendrerit mi lobortis et. Duis a augue laoreet, pulvinar purus luctus, rhoncus est. Quisque sodales sollicitudin augue ac bibendum. Sed a metus risus. Nulla non nulla nisl. Aenean erat velit, tempor id pellentesque eu, volutpat vitae dolor. Praesent commodo, dui in luctus aliquet, est tortor vehicula nibh, sed sollicitudin dui elit eu purus. Integer lacinia rutrum convallis. Nullam varius fringilla dui, elementum finibus magna tincidunt id. Praesent et cursus purus, vitae blandit purus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Aliquam vel cursus neque. -------------------------------------------------------------------------------- /src/bin/form_test.rs: -------------------------------------------------------------------------------- 1 | extern crate hyper; 2 | extern crate multipart; 3 | 4 | use multipart::server::Multipart; 5 | 6 | use hyper::header::ContentType; 7 | use hyper::server::*; 8 | 9 | use std::fs::File; 10 | use std::io; 11 | 12 | fn main() { 13 | let listening = Server::http("127.0.0.1:0").expect("failed to bind socket") 14 | .handle(read_multipart).expect("failed to handle request"); 15 | 16 | println!("bound socket to: {}", listening.socket); 17 | } 18 | 19 | fn read_multipart(req: Request, mut resp: Response) { 20 | if let Ok(mut multipart) = Multipart::from_request(req) { 21 | if let Err(e) = multipart.foreach_entry(|_| {}) { 22 | println!("error handling field: {}", e); 23 | } 24 | } 25 | 26 | let mut file = File::open("src/bin/test_form.html") 27 | .expect("failed to open src/bind/test_form.html"); 28 | 29 | resp.headers_mut().set(ContentType("text/html".parse().unwrap())); 30 | 31 | let mut resp = resp.start().expect("failed to open response"); 32 | io::copy(&mut file, &mut resp).expect("failed to write response"); 33 | } 34 | -------------------------------------------------------------------------------- /src/bin/read_file.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] extern crate log; 2 | extern crate multipart; 3 | extern crate rand; 4 | 5 | use multipart::server::Multipart; 6 | 7 | use rand::{Rng, ThreadRng}; 8 | 9 | use std::fs::File; 10 | use std::env; 11 | use std::io::{self, Read}; 12 | 13 | const LOG_LEVEL: log::LevelFilter = log::LevelFilter::Debug; 14 | 15 | struct SimpleLogger; 16 | 17 | impl log::Log for SimpleLogger { 18 | fn enabled(&self, metadata: &log::Metadata) -> bool { 19 | LOG_LEVEL.to_level() 20 | .map_or(false, |level| metadata.level() <= level) 21 | } 22 | 23 | fn log(&self, record: &log::Record) { 24 | if self.enabled(record.metadata()) { 25 | println!("{} - {}", record.level(), record.args()); 26 | } 27 | } 28 | 29 | fn flush(&self) {} 30 | } 31 | 32 | static LOGGER: SimpleLogger = SimpleLogger; 33 | 34 | fn main() { 35 | log::set_logger(&LOGGER).expect("Could not initialize logger"); 36 | 37 | let mut args = env::args().skip(1); 38 | 39 | let boundary = args.next().expect("Boundary must be provided as the first argument"); 40 | 41 | let file = args.next().expect("Filename must be provided as the second argument"); 42 | 43 | let file = File::open(file).expect("Could not open file"); 44 | 45 | let reader = RandomReader { 46 | inner: file, 47 | rng: rand::thread_rng() 48 | }; 49 | 50 | let mut multipart = Multipart::with_body(reader, boundary); 51 | 52 | while let Some(field) = multipart.read_entry().unwrap() { 53 | println!("Read field: {:?}", field.headers.name); 54 | } 55 | 56 | println!("All entries read!"); 57 | } 58 | 59 | struct RandomReader { 60 | inner: R, 61 | rng: ThreadRng, 62 | } 63 | 64 | impl Read for RandomReader { 65 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 66 | if buf.len() == 0 { 67 | debug!("RandomReader::read() passed a zero-sized buffer."); 68 | return Ok(0); 69 | } 70 | 71 | let len = self.rng.gen_range(1, buf.len() + 1); 72 | 73 | self.inner.read(&mut buf[..len]) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/bin/test_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Multipart-Async Form Test 6 | 7 | 8 |
9 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/client/hyper.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 `multipart` Crate Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | //! Client-side integration with [Hyper](https://github.com/hyperium/hyper). 8 | //! Enabled with the `hyper` feature (on by default). 9 | //! 10 | //! Contains `impl HttpRequest for Request` and `impl HttpStream for Request`. 11 | //! 12 | //! Also see: [`lazy::Multipart::client_request()`](../lazy/struct.Multipart.html#method.client_request) 13 | //! and [`lazy::Multipart::client_request_mut()`](../lazy/struct.Multipart.html#method.client_request_mut) 14 | //! (adaptors for `hyper::client::RequestBuilder`). 15 | use hyper::client::request::Request; 16 | use hyper::client::response::Response; 17 | use hyper::header::{ContentType, ContentLength}; 18 | use hyper::method::Method; 19 | use hyper::net::{Fresh, Streaming}; 20 | 21 | use hyper::Error as HyperError; 22 | 23 | use hyper::mime::{Mime, TopLevel, SubLevel, Attr, Value}; 24 | 25 | use super::{HttpRequest, HttpStream}; 26 | 27 | /// #### Feature: `hyper` 28 | impl HttpRequest for Request { 29 | type Stream = Request; 30 | type Error = HyperError; 31 | 32 | /// # Panics 33 | /// If `self.method() != Method::Post`. 34 | fn apply_headers(&mut self, boundary: &str, content_len: Option) -> bool { 35 | if self.method() != Method::Post { 36 | error!( 37 | "Expected Hyper request method to be `Post`, was actually `{:?}`", 38 | self.method() 39 | ); 40 | 41 | return false; 42 | } 43 | 44 | let headers = self.headers_mut(); 45 | 46 | headers.set(ContentType(multipart_mime(boundary))); 47 | 48 | if let Some(size) = content_len { 49 | headers.set(ContentLength(size)); 50 | } 51 | 52 | debug!("Hyper headers: {}", headers); 53 | 54 | true 55 | } 56 | 57 | fn open_stream(self) -> Result { 58 | self.start() 59 | } 60 | } 61 | 62 | /// #### Feature: `hyper` 63 | impl HttpStream for Request { 64 | type Request = Request; 65 | type Response = Response; 66 | type Error = HyperError; 67 | 68 | fn finish(self) -> Result { 69 | self.send() 70 | } 71 | } 72 | 73 | /// Create a `Content-Type: multipart/form-data;boundary={bound}` 74 | pub fn content_type(bound: &str) -> ContentType { 75 | ContentType(multipart_mime(bound)) 76 | } 77 | 78 | fn multipart_mime(bound: &str) -> Mime { 79 | Mime( 80 | TopLevel::Multipart, SubLevel::Ext("form-data".into()), 81 | vec![(Attr::Ext("boundary".into()), Value::Ext(bound.into()))] 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /src/client/lazy.rs: -------------------------------------------------------------------------------- 1 | //! Multipart requests which write out their data in one fell swoop. 2 | use mime::Mime; 3 | 4 | use std::borrow::Cow; 5 | use std::error::Error; 6 | use std::fs::File; 7 | use std::path::{Path, PathBuf}; 8 | 9 | use std::io::prelude::*; 10 | use std::io::Cursor; 11 | use std::{fmt, io}; 12 | 13 | use super::{HttpRequest, HttpStream}; 14 | 15 | macro_rules! try_lazy ( 16 | ($field:expr, $try:expr) => ( 17 | match $try { 18 | Ok(ok) => ok, 19 | Err(e) => return Err(LazyError::with_field($field.into(), e)), 20 | } 21 | ); 22 | ($try:expr) => ( 23 | match $try { 24 | Ok(ok) => ok, 25 | Err(e) => return Err(LazyError::without_field(e)), 26 | } 27 | ) 28 | ); 29 | 30 | /// A `LazyError` wrapping `std::io::Error`. 31 | pub type LazyIoError<'a> = LazyError<'a, io::Error>; 32 | 33 | /// `Result` type for `LazyIoError`. 34 | pub type LazyIoResult<'a, T> = Result>; 35 | 36 | /// An error for lazily written multipart requests, including the original error as well 37 | /// as the field which caused the error, if applicable. 38 | pub struct LazyError<'a, E> { 39 | /// The field that caused the error. 40 | /// If `None`, there was a problem opening the stream to write or finalizing the stream. 41 | pub field_name: Option>, 42 | /// The inner error. 43 | pub error: E, 44 | /// Private field for back-compat. 45 | _priv: (), 46 | } 47 | 48 | impl<'a, E> LazyError<'a, E> { 49 | fn without_field>(error: E_) -> Self { 50 | LazyError { 51 | field_name: None, 52 | error: error.into(), 53 | _priv: (), 54 | } 55 | } 56 | 57 | fn with_field>(field_name: Cow<'a, str>, error: E_) -> Self { 58 | LazyError { 59 | field_name: Some(field_name), 60 | error: error.into(), 61 | _priv: (), 62 | } 63 | } 64 | 65 | fn transform_err>(self) -> LazyError<'a, E_> { 66 | LazyError { 67 | field_name: self.field_name, 68 | error: self.error.into(), 69 | _priv: (), 70 | } 71 | } 72 | } 73 | 74 | /// Take `self.error`, discarding `self.field_name`. 75 | impl<'a> Into for LazyError<'a, io::Error> { 76 | fn into(self) -> io::Error { 77 | self.error 78 | } 79 | } 80 | 81 | impl<'a, E: Error> Error for LazyError<'a, E> { 82 | fn description(&self) -> &str { 83 | self.error.description() 84 | } 85 | 86 | fn cause(&self) -> Option<&dyn Error> { 87 | Some(&self.error) 88 | } 89 | } 90 | 91 | impl<'a, E: fmt::Debug> fmt::Debug for LazyError<'a, E> { 92 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 93 | if let Some(ref field_name) = self.field_name { 94 | fmt.write_fmt(format_args!( 95 | "LazyError (on field {:?}): {:?}", 96 | field_name, self.error 97 | )) 98 | } else { 99 | fmt.write_fmt(format_args!("LazyError (misc): {:?}", self.error)) 100 | } 101 | } 102 | } 103 | 104 | impl<'a, E: fmt::Display> fmt::Display for LazyError<'a, E> { 105 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 106 | if let Some(ref field_name) = self.field_name { 107 | fmt.write_fmt(format_args!( 108 | "Error writing field {:?}: {}", 109 | field_name, self.error 110 | )) 111 | } else { 112 | fmt.write_fmt(format_args!( 113 | "Error opening or flushing stream: {}", 114 | self.error 115 | )) 116 | } 117 | } 118 | } 119 | 120 | /// A multipart request which writes all fields at once upon being provided an output stream. 121 | /// 122 | /// Sacrifices static dispatch for support for dynamic construction. Reusable. 123 | /// 124 | /// #### Lifetimes 125 | /// * `'n`: Lifetime for field **n**ames; will only escape this struct in `LazyIoError<'n>`. 126 | /// * `'d`: Lifetime for **d**ata: will only escape this struct in `PreparedFields<'d>`. 127 | #[derive(Debug, Default)] 128 | pub struct Multipart<'n, 'd> { 129 | fields: Vec>, 130 | } 131 | 132 | impl<'n, 'd> Multipart<'n, 'd> { 133 | /// Initialize a new lazy dynamic request. 134 | pub fn new() -> Self { 135 | Default::default() 136 | } 137 | 138 | /// Add a text field to this request. 139 | pub fn add_text(&mut self, name: N, text: T) -> &mut Self 140 | where 141 | N: Into>, 142 | T: Into>, 143 | { 144 | self.fields.push(Field { 145 | name: name.into(), 146 | data: Data::Text(text.into()), 147 | }); 148 | 149 | self 150 | } 151 | 152 | /// Add a file field to this request. 153 | /// 154 | /// ### Note 155 | /// Does not check if `path` exists. 156 | pub fn add_file(&mut self, name: N, path: P) -> &mut Self 157 | where 158 | N: Into>, 159 | P: IntoCowPath<'d>, 160 | { 161 | self.fields.push(Field { 162 | name: name.into(), 163 | data: Data::File(path.into_cow_path()), 164 | }); 165 | 166 | self 167 | } 168 | 169 | /// Add a generic stream field to this request, 170 | pub fn add_stream( 171 | &mut self, 172 | name: N, 173 | stream: R, 174 | filename: Option, 175 | mime: Option, 176 | ) -> &mut Self 177 | where 178 | N: Into>, 179 | R: Read + 'd, 180 | F: Into>, 181 | { 182 | self.fields.push(Field { 183 | name: name.into(), 184 | data: Data::Stream(Stream { 185 | content_type: mime.unwrap_or(mime::APPLICATION_OCTET_STREAM), 186 | filename: filename.map(|f| f.into()), 187 | stream: Box::new(stream), 188 | }), 189 | }); 190 | 191 | self 192 | } 193 | 194 | /// Convert `req` to `HttpStream`, write out the fields in this request, and finish the 195 | /// request, returning the response if successful, or the first error encountered. 196 | /// 197 | /// If any files were added by path they will now be opened for reading. 198 | pub fn send( 199 | &mut self, 200 | mut req: R, 201 | ) -> Result<::Response, LazyError<'n, ::Error>> 202 | { 203 | let mut prepared = self.prepare().map_err(LazyError::transform_err)?; 204 | 205 | req.apply_headers(prepared.boundary(), prepared.content_len()); 206 | 207 | let mut stream = try_lazy!(req.open_stream()); 208 | 209 | try_lazy!(io::copy(&mut prepared, &mut stream)); 210 | 211 | stream.finish().map_err(LazyError::without_field) 212 | } 213 | 214 | /// Export the multipart data contained in this lazy request as an adaptor which implements `Read`. 215 | /// 216 | /// During this step, if any files were added by path then they will be opened for reading 217 | /// and their length measured. 218 | pub fn prepare(&mut self) -> LazyIoResult<'n, PreparedFields<'d>> { 219 | PreparedFields::from_fields(&mut self.fields) 220 | } 221 | } 222 | 223 | #[derive(Debug)] 224 | struct Field<'n, 'd> { 225 | name: Cow<'n, str>, 226 | data: Data<'n, 'd>, 227 | } 228 | 229 | enum Data<'n, 'd> { 230 | Text(Cow<'d, str>), 231 | File(Cow<'d, Path>), 232 | Stream(Stream<'n, 'd>), 233 | } 234 | 235 | impl<'n, 'd> fmt::Debug for Data<'n, 'd> { 236 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 237 | match *self { 238 | Data::Text(ref text) => write!(f, "Data::Text({:?})", text), 239 | Data::File(ref path) => write!(f, "Data::File({:?})", path), 240 | Data::Stream(_) => f.write_str("Data::Stream(Box)"), 241 | } 242 | } 243 | } 244 | 245 | struct Stream<'n, 'd> { 246 | filename: Option>, 247 | content_type: Mime, 248 | stream: Box, 249 | } 250 | 251 | /// The result of [`Multipart::prepare()`](struct.Multipart.html#method.prepare). 252 | /// 253 | /// Implements `Read`, contains the entire request body. 254 | /// 255 | /// Individual files/streams are dropped as they are read to completion. 256 | /// 257 | /// ### Note 258 | /// The fields in the request may have been reordered to simplify the preparation step. 259 | /// No compliant server implementation will be relying on the specific ordering of fields anyways. 260 | pub struct PreparedFields<'d> { 261 | text_data: Cursor>, 262 | streams: Vec>, 263 | end_boundary: Cursor, 264 | content_len: Option, 265 | } 266 | 267 | impl<'d> PreparedFields<'d> { 268 | fn from_fields<'n>(fields: &mut Vec>) -> Result> { 269 | debug!("Field count: {}", fields.len()); 270 | 271 | // One of the two RFCs specifies that any bytes before the first boundary are to be 272 | // ignored anyway 273 | let mut boundary = format!("\r\n--{}", super::gen_boundary()); 274 | 275 | let mut text_data = Vec::new(); 276 | let mut streams = Vec::new(); 277 | let mut content_len = 0u64; 278 | let mut use_len = true; 279 | 280 | for field in fields.drain(..) { 281 | match field.data { 282 | Data::Text(text) => write!( 283 | text_data, 284 | "{}\r\nContent-Disposition: form-data; \ 285 | name=\"{}\"\r\n\r\n{}", 286 | boundary, field.name, text 287 | ) 288 | .unwrap(), 289 | Data::File(file) => { 290 | let (stream, len) = PreparedField::from_path(field.name, &file, &boundary)?; 291 | content_len += len; 292 | streams.push(stream); 293 | } 294 | Data::Stream(stream) => { 295 | use_len = false; 296 | 297 | streams.push(PreparedField::from_stream( 298 | &field.name, 299 | &boundary, 300 | &stream.content_type, 301 | stream.filename.as_ref().map(|f| &**f), 302 | stream.stream, 303 | )); 304 | } 305 | } 306 | } 307 | 308 | // So we don't write a spurious end boundary 309 | if text_data.is_empty() && streams.is_empty() { 310 | boundary = String::new(); 311 | } else { 312 | boundary.push_str("--"); 313 | } 314 | 315 | content_len += boundary.len() as u64; 316 | 317 | Ok(PreparedFields { 318 | text_data: Cursor::new(text_data), 319 | streams, 320 | end_boundary: Cursor::new(boundary), 321 | content_len: if use_len { Some(content_len) } else { None }, 322 | }) 323 | } 324 | 325 | /// Get the content-length value for this set of fields, if applicable (all fields are sized, 326 | /// i.e. not generic streams). 327 | pub fn content_len(&self) -> Option { 328 | self.content_len 329 | } 330 | 331 | /// Get the boundary that was used to serialize the request. 332 | pub fn boundary(&self) -> &str { 333 | let boundary = self.end_boundary.get_ref(); 334 | 335 | // Get just the bare boundary string 336 | &boundary[4..boundary.len() - 2] 337 | } 338 | } 339 | 340 | impl<'d> Read for PreparedFields<'d> { 341 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 342 | if buf.is_empty() { 343 | debug!("PreparedFields::read() was passed a zero-sized buffer."); 344 | return Ok(0); 345 | } 346 | 347 | let mut total_read = 0; 348 | 349 | while total_read < buf.len() && !cursor_at_end(&self.end_boundary) { 350 | let buf = &mut buf[total_read..]; 351 | 352 | total_read += if !cursor_at_end(&self.text_data) { 353 | self.text_data.read(buf)? 354 | } else if let Some(mut field) = self.streams.pop() { 355 | match field.read(buf) { 356 | Ok(0) => continue, 357 | res => { 358 | self.streams.push(field); 359 | res 360 | } 361 | }? 362 | } else { 363 | self.end_boundary.read(buf)? 364 | }; 365 | } 366 | 367 | Ok(total_read) 368 | } 369 | } 370 | 371 | struct PreparedField<'d> { 372 | header: Cursor>, 373 | stream: Box, 374 | } 375 | 376 | impl<'d> PreparedField<'d> { 377 | fn from_path<'n>( 378 | name: Cow<'n, str>, 379 | path: &Path, 380 | boundary: &str, 381 | ) -> Result<(Self, u64), LazyIoError<'n>> { 382 | let (content_type, filename) = super::mime_filename(&path); 383 | 384 | let file = try_lazy!(name, File::open(path)); 385 | let content_len = try_lazy!(name, file.metadata()).len(); 386 | 387 | let stream = Self::from_stream(&name, boundary, &content_type, filename, Box::new(file)); 388 | 389 | let content_len = content_len + (stream.header.get_ref().len() as u64); 390 | 391 | Ok((stream, content_len)) 392 | } 393 | 394 | fn from_stream( 395 | name: &str, 396 | boundary: &str, 397 | content_type: &Mime, 398 | filename: Option<&str>, 399 | stream: Box, 400 | ) -> Self { 401 | let mut header = Vec::new(); 402 | 403 | write!( 404 | header, 405 | "{}\r\nContent-Disposition: form-data; name=\"{}\"", 406 | boundary, name 407 | ) 408 | .unwrap(); 409 | 410 | if let Some(filename) = filename { 411 | write!(header, "; filename=\"{}\"", filename).unwrap(); 412 | } 413 | 414 | write!(header, "\r\nContent-Type: {}\r\n\r\n", content_type).unwrap(); 415 | 416 | PreparedField { 417 | header: Cursor::new(header), 418 | stream, 419 | } 420 | } 421 | } 422 | 423 | impl<'d> Read for PreparedField<'d> { 424 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 425 | debug!("PreparedField::read()"); 426 | 427 | if !cursor_at_end(&self.header) { 428 | self.header.read(buf) 429 | } else { 430 | self.stream.read(buf) 431 | } 432 | } 433 | } 434 | 435 | impl<'d> fmt::Debug for PreparedField<'d> { 436 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 437 | f.debug_struct("PreparedField") 438 | .field("header", &self.header) 439 | .field("stream", &"Box") 440 | .finish() 441 | } 442 | } 443 | 444 | /// Conversion trait necessary for `Multipart::add_file()` to accept borrowed or owned strings 445 | /// and borrowed or owned paths 446 | pub trait IntoCowPath<'a> { 447 | /// Self-explanatory, hopefully 448 | fn into_cow_path(self) -> Cow<'a, Path>; 449 | } 450 | 451 | impl<'a> IntoCowPath<'a> for Cow<'a, Path> { 452 | fn into_cow_path(self) -> Cow<'a, Path> { 453 | self 454 | } 455 | } 456 | 457 | impl IntoCowPath<'static> for PathBuf { 458 | fn into_cow_path(self) -> Cow<'static, Path> { 459 | self.into() 460 | } 461 | } 462 | 463 | impl<'a> IntoCowPath<'a> for &'a Path { 464 | fn into_cow_path(self) -> Cow<'a, Path> { 465 | self.into() 466 | } 467 | } 468 | 469 | impl IntoCowPath<'static> for String { 470 | fn into_cow_path(self) -> Cow<'static, Path> { 471 | PathBuf::from(self).into() 472 | } 473 | } 474 | 475 | impl<'a> IntoCowPath<'a> for &'a str { 476 | fn into_cow_path(self) -> Cow<'a, Path> { 477 | Path::new(self).into() 478 | } 479 | } 480 | 481 | fn cursor_at_end>(cursor: &Cursor) -> bool { 482 | cursor.position() == (cursor.get_ref().as_ref().len() as u64) 483 | } 484 | 485 | #[cfg(feature = "hyper")] 486 | mod hyper { 487 | use hyper::client::{Body, Client, IntoUrl, RequestBuilder, Response}; 488 | use hyper::Result as HyperResult; 489 | 490 | impl<'n, 'd> super::Multipart<'n, 'd> { 491 | /// #### Feature: `hyper` 492 | /// Complete a POST request with the given `hyper::client::Client` and URL. 493 | /// 494 | /// Supplies the fields in the body, optionally setting the content-length header if 495 | /// applicable (all added fields were text or files, i.e. no streams). 496 | pub fn client_request( 497 | &mut self, 498 | client: &Client, 499 | url: U, 500 | ) -> HyperResult { 501 | self.client_request_mut(client, url, |r| r) 502 | } 503 | 504 | /// #### Feature: `hyper` 505 | /// Complete a POST request with the given `hyper::client::Client` and URL; 506 | /// allows mutating the `hyper::client::RequestBuilder` via the passed closure. 507 | /// 508 | /// Note that the body, and the `ContentType` and `ContentLength` headers will be 509 | /// overwritten, either by this method or by Hyper. 510 | pub fn client_request_mut RequestBuilder>( 511 | &mut self, 512 | client: &Client, 513 | url: U, 514 | mut_fn: F, 515 | ) -> HyperResult { 516 | let mut fields = match self.prepare() { 517 | Ok(fields) => fields, 518 | Err(err) => { 519 | error!("Error preparing request: {}", err); 520 | return Err(err.error.into()); 521 | } 522 | }; 523 | 524 | mut_fn(client.post(url)) 525 | .header(::client::hyper::content_type(fields.boundary())) 526 | .body(fields.to_body()) 527 | .send() 528 | } 529 | } 530 | 531 | impl<'d> super::PreparedFields<'d> { 532 | /// #### Feature: `hyper` 533 | /// Convert `self` to `hyper::client::Body`. 534 | #[cfg_attr(feature = "clippy", warn(wrong_self_convention))] 535 | pub fn to_body<'b>(&'b mut self) -> Body<'b> 536 | where 537 | 'd: 'b, 538 | { 539 | if let Some(content_len) = self.content_len { 540 | Body::SizedBody(self, content_len) 541 | } else { 542 | Body::ChunkedBody(self) 543 | } 544 | } 545 | } 546 | } 547 | -------------------------------------------------------------------------------- /src/client/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 `multipart` Crate Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | //! The client-side abstraction for multipart requests. Enabled with the `client` feature. 8 | //! 9 | //! Use this when sending POST requests with files to a server. 10 | use mime::Mime; 11 | 12 | use std::borrow::Cow; 13 | use std::fs::File; 14 | use std::io; 15 | use std::io::prelude::*; 16 | 17 | use std::path::Path; 18 | 19 | #[cfg(feature = "hyper")] 20 | pub mod hyper; 21 | 22 | pub mod lazy; 23 | 24 | mod sized; 25 | 26 | pub use self::sized::SizedRequest; 27 | 28 | const BOUNDARY_LEN: usize = 16; 29 | 30 | macro_rules! map_self { 31 | ($selff:expr, $try:expr) => { 32 | match $try { 33 | Ok(_) => Ok($selff), 34 | Err(err) => Err(err.into()), 35 | } 36 | }; 37 | } 38 | 39 | /// The entry point of the client-side multipart API. 40 | /// 41 | /// Though they perform I/O, the `.write_*()` methods do not return `io::Result<_>` in order to 42 | /// facilitate method chaining. Upon the first error, all subsequent API calls will be no-ops until 43 | /// `.send()` is called, at which point the error will be reported. 44 | pub struct Multipart { 45 | writer: MultipartWriter<'static, S>, 46 | } 47 | 48 | impl Multipart<()> { 49 | /// Create a new `Multipart` to wrap a request. 50 | /// 51 | /// ## Returns Error 52 | /// If `req.open_stream()` returns an error. 53 | pub fn from_request(req: R) -> Result, R::Error> { 54 | let (boundary, stream) = open_stream(req, None)?; 55 | 56 | Ok(Multipart { 57 | writer: MultipartWriter::new(stream, boundary), 58 | }) 59 | } 60 | } 61 | 62 | impl Multipart { 63 | /// Write a text field to this multipart request. 64 | /// `name` and `val` can be either owned `String` or `&str`. 65 | /// 66 | /// ## Errors 67 | /// If something went wrong with the HTTP stream. 68 | pub fn write_text, V: AsRef>( 69 | &mut self, 70 | name: N, 71 | val: V, 72 | ) -> Result<&mut Self, S::Error> { 73 | map_self!(self, self.writer.write_text(name.as_ref(), val.as_ref())) 74 | } 75 | 76 | /// Open a file pointed to by `path` and write its contents to the multipart request, 77 | /// supplying its filename and guessing its `Content-Type` from its extension. 78 | /// 79 | /// If you want to set these values manually, or use another type that implements `Read`, 80 | /// use `.write_stream()`. 81 | /// 82 | /// `name` can be either `String` or `&str`, and `path` can be `PathBuf` or `&Path`. 83 | /// 84 | /// ## Errors 85 | /// If there was a problem opening the file (was a directory or didn't exist), 86 | /// or if something went wrong with the HTTP stream. 87 | pub fn write_file, P: AsRef>( 88 | &mut self, 89 | name: N, 90 | path: P, 91 | ) -> Result<&mut Self, S::Error> { 92 | let name = name.as_ref(); 93 | let path = path.as_ref(); 94 | 95 | map_self!(self, self.writer.write_file(name, path)) 96 | } 97 | 98 | /// Write a byte stream to the multipart request as a file field, supplying `filename` if given, 99 | /// and `content_type` if given or `"application/octet-stream"` if not. 100 | /// 101 | /// `name` can be either `String` or `&str`, and `read` can take the `Read` by-value or 102 | /// with an `&mut` borrow. 103 | /// 104 | /// ## Warning 105 | /// The given `Read` **must** be able to read to EOF (end of file/no more data), meaning 106 | /// `Read::read()` returns `Ok(0)`. If it never returns EOF it will be read to infinity 107 | /// and the request will never be completed. 108 | /// 109 | /// When using `SizedRequest` this also can cause out-of-control memory usage as the 110 | /// multipart data has to be written to an in-memory buffer so its size can be calculated. 111 | /// 112 | /// Use `Read::take()` if you wish to send data from a `Read` 113 | /// that will never return EOF otherwise. 114 | /// 115 | /// ## Errors 116 | /// If the reader returned an error, or if something went wrong with the HTTP stream. 117 | // RFC: How to format this declaration? 118 | pub fn write_stream, St: Read>( 119 | &mut self, 120 | name: N, 121 | stream: &mut St, 122 | filename: Option<&str>, 123 | content_type: Option, 124 | ) -> Result<&mut Self, S::Error> { 125 | let name = name.as_ref(); 126 | 127 | map_self!( 128 | self, 129 | self.writer 130 | .write_stream(stream, name, filename, content_type) 131 | ) 132 | } 133 | 134 | /// Finalize the request and return the response from the server, or the last error if set. 135 | pub fn send(self) -> Result { 136 | self.writer 137 | .finish() 138 | .map_err(io::Error::into) 139 | .and_then(|body| body.finish()) 140 | } 141 | } 142 | 143 | impl Multipart> 144 | where 145 | ::Error: From, 146 | { 147 | /// Create a new `Multipart` using the `SizedRequest` wrapper around `req`. 148 | pub fn from_request_sized(req: R) -> Result { 149 | Multipart::from_request(SizedRequest::from_request(req)) 150 | } 151 | } 152 | 153 | /// A trait describing an HTTP request that can be used to send multipart data. 154 | pub trait HttpRequest { 155 | /// The HTTP stream type that can be opend by this request, to which the multipart data will be 156 | /// written. 157 | type Stream: HttpStream; 158 | /// The error type for this request. 159 | /// Must be compatible with `io::Error` as well as `Self::HttpStream::Error` 160 | type Error: From + Into<::Error>; 161 | 162 | /// Set the `Content-Type` header to `multipart/form-data` and supply the `boundary` value. 163 | /// If `content_len` is given, set the `Content-Length` header to its value. 164 | /// 165 | /// Return `true` if any and all sanity checks passed and the stream is ready to be opened, 166 | /// or `false` otherwise. 167 | fn apply_headers(&mut self, boundary: &str, content_len: Option) -> bool; 168 | 169 | /// Open the request stream and return it or any error otherwise. 170 | fn open_stream(self) -> Result; 171 | } 172 | 173 | /// A trait describing an open HTTP stream that can be written to. 174 | pub trait HttpStream: Write { 175 | /// The request type that opened this stream. 176 | type Request: HttpRequest; 177 | /// The response type that will be returned after the request is completed. 178 | type Response; 179 | /// The error type for this stream. 180 | /// Must be compatible with `io::Error` as well as `Self::Request::Error`. 181 | type Error: From + From<::Error>; 182 | 183 | /// Finalize and close the stream and return the response object, or any error otherwise. 184 | fn finish(self) -> Result; 185 | } 186 | 187 | impl HttpRequest for () { 188 | type Stream = io::Sink; 189 | type Error = io::Error; 190 | 191 | fn apply_headers(&mut self, _: &str, _: Option) -> bool { 192 | true 193 | } 194 | fn open_stream(self) -> Result { 195 | Ok(io::sink()) 196 | } 197 | } 198 | 199 | impl HttpStream for io::Sink { 200 | type Request = (); 201 | type Response = (); 202 | type Error = io::Error; 203 | 204 | fn finish(self) -> Result { 205 | Ok(()) 206 | } 207 | } 208 | 209 | fn gen_boundary() -> String { 210 | ::random_alphanumeric(BOUNDARY_LEN) 211 | } 212 | 213 | fn open_stream( 214 | mut req: R, 215 | content_len: Option, 216 | ) -> Result<(String, R::Stream), R::Error> { 217 | let boundary = gen_boundary(); 218 | req.apply_headers(&boundary, content_len); 219 | req.open_stream().map(|stream| (boundary, stream)) 220 | } 221 | 222 | struct MultipartWriter<'a, W> { 223 | inner: W, 224 | boundary: Cow<'a, str>, 225 | data_written: bool, 226 | } 227 | 228 | impl<'a, W: Write> MultipartWriter<'a, W> { 229 | fn new>>(inner: W, boundary: B) -> Self { 230 | MultipartWriter { 231 | inner, 232 | boundary: boundary.into(), 233 | data_written: false, 234 | } 235 | } 236 | 237 | fn write_boundary(&mut self) -> io::Result<()> { 238 | if self.data_written { 239 | self.inner.write_all(b"\r\n")?; 240 | } 241 | 242 | write!(self.inner, "--{}\r\n", self.boundary) 243 | } 244 | 245 | fn write_text(&mut self, name: &str, text: &str) -> io::Result<()> { 246 | chain_result! { 247 | self.write_field_headers(name, None, None), 248 | self.inner.write_all(text.as_bytes()) 249 | } 250 | } 251 | 252 | fn write_file(&mut self, name: &str, path: &Path) -> io::Result<()> { 253 | let (content_type, filename) = mime_filename(path); 254 | let mut file = File::open(path)?; 255 | self.write_stream(&mut file, name, filename, Some(content_type)) 256 | } 257 | 258 | fn write_stream( 259 | &mut self, 260 | stream: &mut S, 261 | name: &str, 262 | filename: Option<&str>, 263 | content_type: Option, 264 | ) -> io::Result<()> { 265 | // This is necessary to make sure it is interpreted as a file on the server end. 266 | let content_type = Some(content_type.unwrap_or(mime::APPLICATION_OCTET_STREAM)); 267 | 268 | chain_result! { 269 | self.write_field_headers(name, filename, content_type), 270 | io::copy(stream, &mut self.inner), 271 | Ok(()) 272 | } 273 | } 274 | 275 | fn write_field_headers( 276 | &mut self, 277 | name: &str, 278 | filename: Option<&str>, 279 | content_type: Option, 280 | ) -> io::Result<()> { 281 | chain_result! { 282 | // Write the first boundary, or the boundary for the previous field. 283 | self.write_boundary(), 284 | { self.data_written = true; Ok(()) }, 285 | write!(self.inner, "Content-Disposition: form-data; name=\"{}\"", name), 286 | filename.map(|filename| write!(self.inner, "; filename=\"{}\"", filename)) 287 | .unwrap_or(Ok(())), 288 | content_type.map(|content_type| write!(self.inner, "\r\nContent-Type: {}", content_type)) 289 | .unwrap_or(Ok(())), 290 | self.inner.write_all(b"\r\n\r\n") 291 | } 292 | } 293 | 294 | fn finish(mut self) -> io::Result { 295 | if self.data_written { 296 | self.inner.write_all(b"\r\n")?; 297 | } 298 | 299 | // always write the closing boundary, even for empty bodies 300 | // trailing CRLF is optional but Actix requires it due to a naive implementation: 301 | // https://github.com/actix/actix-web/issues/598 302 | write!(self.inner, "--{}--\r\n", self.boundary)?; 303 | Ok(self.inner) 304 | } 305 | } 306 | 307 | fn mime_filename(path: &Path) -> (Mime, Option<&str>) { 308 | let content_type = ::mime_guess::from_path(path); 309 | let filename = opt_filename(path); 310 | (content_type.first_or_octet_stream(), filename) 311 | } 312 | 313 | fn opt_filename(path: &Path) -> Option<&str> { 314 | path.file_name().and_then(|filename| filename.to_str()) 315 | } 316 | -------------------------------------------------------------------------------- /src/client/sized.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 `multipart` Crate Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | //! Sized/buffered wrapper around `HttpRequest`. 8 | 9 | use client::{HttpRequest, HttpStream}; 10 | 11 | use std::io; 12 | use std::io::prelude::*; 13 | 14 | /// A wrapper around a request object that measures the request body and sets the `Content-Length` 15 | /// header to its size in bytes. 16 | /// 17 | /// Sized requests are more human-readable and use less bandwidth 18 | /// (as chunking adds [visual noise and overhead][chunked-example]), 19 | /// but they must be able to load their entirety, including the contents of all files 20 | /// and streams, into memory so the request body can be measured. 21 | /// 22 | /// You should really only use sized requests if you intend to inspect the data manually on the 23 | /// server side, as it will produce a more human-readable request body. Also, of course, if the 24 | /// server doesn't support chunked requests or otherwise rejects them. 25 | /// 26 | /// [chunked-example]: http://en.wikipedia.org/wiki/Chunked_transfer_encoding#Example 27 | pub struct SizedRequest { 28 | inner: R, 29 | buffer: Vec, 30 | boundary: String, 31 | } 32 | 33 | impl SizedRequest { 34 | #[doc(hidden)] 35 | pub fn from_request(req: R) -> SizedRequest { 36 | SizedRequest { 37 | inner: req, 38 | buffer: Vec::new(), 39 | boundary: String::new(), 40 | } 41 | } 42 | } 43 | 44 | impl Write for SizedRequest { 45 | fn write(&mut self, data: &[u8]) -> io::Result { 46 | self.buffer.write(data) 47 | } 48 | 49 | fn flush(&mut self) -> io::Result<()> { Ok(()) } 50 | } 51 | 52 | impl HttpRequest for SizedRequest 53 | where ::Error: From { 54 | type Stream = Self; 55 | type Error = R::Error; 56 | 57 | /// `SizedRequest` ignores `_content_len` because it sets its own later. 58 | fn apply_headers(&mut self, boundary: &str, _content_len: Option) -> bool { 59 | self.boundary.clear(); 60 | self.boundary.push_str(boundary); 61 | true 62 | } 63 | 64 | fn open_stream(mut self) -> Result { 65 | self.buffer.clear(); 66 | Ok(self) 67 | } 68 | } 69 | 70 | impl HttpStream for SizedRequest 71 | where ::Error: From { 72 | type Request = Self; 73 | type Response = <::Stream as HttpStream>::Response; 74 | type Error = <::Stream as HttpStream>::Error; 75 | 76 | fn finish(mut self) -> Result { 77 | let content_len = self.buffer.len() as u64; 78 | 79 | if !self.inner.apply_headers(&self.boundary, Some(content_len)) { 80 | return Err(io::Error::new( 81 | io::ErrorKind::Other, 82 | "SizedRequest failed to apply headers to wrapped request." 83 | ).into()); 84 | } 85 | 86 | let mut req = self.inner.open_stream()?; 87 | io::copy(&mut &self.buffer[..], &mut req)?; 88 | req.finish() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 `multipart` Crate Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | //! Client- and server-side abstractions for HTTP `multipart/form-data` requests. 8 | //! 9 | //! ### Features: 10 | //! This documentation is built with all features enabled. 11 | //! 12 | //! * `client`: The client-side abstractions for generating multipart requests. 13 | //! 14 | //! * `server`: The server-side abstractions for parsing multipart requests. 15 | //! 16 | //! * `mock`: Provides mock implementations of core `client` and `server` traits for debugging 17 | //! or non-standard use. 18 | //! 19 | //! * `hyper`: Integration with the [Hyper](https://crates.io/crates/hyper) HTTP library 20 | //! for client and/or server depending on which other feature flags are set. 21 | //! 22 | //! * `iron`: Integration with the [Iron](http://crates.io/crates/iron) web application 23 | //! framework. See the [`server::iron`](server/iron/index.html) module for more information. 24 | //! 25 | //! * `nickel` (returning in 0.14!): Integration with the [Nickel](https://crates.io/crates/nickel) 26 | //! web application framework. See the [`server::nickel`](server/nickel/index.html) module for more 27 | //! information. 28 | //! 29 | //! * `tiny_http`: Integration with the [`tiny_http`](https://crates.io/crates/tiny_http) 30 | //! crate. See the [`server::tiny_http`](server/tiny_http/index.html) module for more information. 31 | //! 32 | //! ### Note: Work in Progress 33 | //! I have left a number of Request-for-Comments (RFC) questions on various APIs and other places 34 | //! in the code as there are some cases where I'm not sure what the desirable behavior is. 35 | //! 36 | //! I have opened an issue as a place to collect responses and discussions for these questions 37 | //! [on Github](https://github.com/abonander/multipart/issues/96). Please quote the RFC-statement 38 | //! (and/or link to its source line) and provide your feedback there. 39 | #![cfg_attr(feature = "clippy", feature(plugin))] 40 | #![cfg_attr(feature = "clippy", plugin(clippy))] 41 | #![cfg_attr(feature = "clippy", deny(clippy))] 42 | #![cfg_attr(feature = "bench", feature(test))] 43 | #![deny(missing_docs)] 44 | 45 | #[macro_use] 46 | extern crate log; 47 | 48 | extern crate mime; 49 | extern crate mime_guess; 50 | extern crate rand; 51 | extern crate tempfile; 52 | 53 | #[cfg(feature = "quick-error")] 54 | #[macro_use] 55 | extern crate quick_error; 56 | 57 | #[cfg(feature = "server")] 58 | extern crate safemem; 59 | 60 | #[cfg(feature = "hyper")] 61 | extern crate hyper; 62 | 63 | #[cfg(feature = "iron")] 64 | extern crate iron; 65 | 66 | #[cfg(feature = "tiny_http")] 67 | extern crate tiny_http; 68 | 69 | #[cfg(test)] 70 | extern crate env_logger; 71 | 72 | #[cfg(any(feature = "mock", test))] 73 | pub mod mock; 74 | 75 | use rand::Rng; 76 | 77 | /// Chain a series of results together, with or without previous results. 78 | /// 79 | /// ``` 80 | /// #[macro_use] extern crate multipart; 81 | /// 82 | /// fn try_add_one(val: u32) -> Result { 83 | /// if val < 5 { 84 | /// Ok(val + 1) 85 | /// } else { 86 | /// Err(val) 87 | /// } 88 | /// } 89 | /// 90 | /// fn main() { 91 | /// let res = chain_result! { 92 | /// try_add_one(1), 93 | /// prev -> try_add_one(prev), 94 | /// prev -> try_add_one(prev), 95 | /// prev -> try_add_one(prev) 96 | /// }; 97 | /// 98 | /// println!("{:?}", res); 99 | /// } 100 | /// 101 | /// ``` 102 | #[macro_export] 103 | macro_rules! chain_result { 104 | ($first_expr:expr, $($try_expr:expr),*) => ( 105 | $first_expr $(.and_then(|_| $try_expr))* 106 | ); 107 | ($first_expr:expr, $($($arg:ident),+ -> $try_expr:expr),*) => ( 108 | $first_expr $(.and_then(|$($arg),+| $try_expr))* 109 | ); 110 | } 111 | 112 | #[cfg(feature = "client")] 113 | pub mod client; 114 | #[cfg(feature = "server")] 115 | pub mod server; 116 | 117 | #[cfg(all(test, feature = "client", feature = "server"))] 118 | mod local_test; 119 | 120 | fn random_alphanumeric(len: usize) -> String { 121 | rand::thread_rng() 122 | .sample_iter(&rand::distributions::Alphanumeric) 123 | .take(len) 124 | .map(|c| c as char) 125 | .collect() 126 | } 127 | 128 | #[cfg(test)] 129 | fn init_log() { 130 | let _ = env_logger::try_init(); 131 | } 132 | -------------------------------------------------------------------------------- /src/local_test.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 `multipart` Crate Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | use mock::{ClientRequest, HttpBuffer}; 8 | 9 | use server::{FieldHeaders, MultipartField, ReadEntry}; 10 | 11 | use mime::Mime; 12 | 13 | use rand::seq::SliceRandom; 14 | use rand::{self, Rng}; 15 | 16 | use std::collections::hash_map::{Entry, OccupiedEntry}; 17 | use std::collections::{HashMap, HashSet}; 18 | use std::fmt; 19 | use std::io::prelude::*; 20 | use std::io::Cursor; 21 | use std::iter::{self, FromIterator}; 22 | 23 | const MIN_FIELDS: usize = 1; 24 | const MAX_FIELDS: usize = 3; 25 | 26 | const MIN_LEN: usize = 2; 27 | const MAX_LEN: usize = 5; 28 | const MAX_DASHES: usize = 2; 29 | 30 | fn collect_rand, T, F: FnMut() -> T>(mut gen: F) -> C { 31 | (0..rand::thread_rng().gen_range(MIN_FIELDS..MAX_FIELDS)) 32 | .map(|_| gen()) 33 | .collect() 34 | } 35 | 36 | macro_rules! expect_fmt ( 37 | ($val:expr, $($args:tt)*) => ( 38 | match $val { 39 | Some(val) => val, 40 | None => panic!($($args)*), 41 | } 42 | ); 43 | ); 44 | 45 | /// The error is provided as the `err` format argument 46 | macro_rules! expect_ok_fmt ( 47 | ($val:expr, $($args:tt)*) => ( 48 | match $val { 49 | Ok(val) => val, 50 | Err(e) => panic!($($args)*, err=e), 51 | } 52 | ); 53 | ); 54 | 55 | fn get_field<'m, V>( 56 | field: &FieldHeaders, 57 | fields: &'m mut HashMap, 58 | ) -> Option> { 59 | match fields.entry(field.name.to_string()) { 60 | Entry::Occupied(occupied) => Some(occupied), 61 | Entry::Vacant(_) => None, 62 | } 63 | } 64 | 65 | #[derive(Debug)] 66 | struct TestFields { 67 | texts: HashMap>, 68 | files: HashMap>, 69 | } 70 | 71 | impl TestFields { 72 | fn gen() -> Self { 73 | TestFields { 74 | texts: collect_rand(|| (gen_string(), collect_rand(gen_string))), 75 | files: collect_rand(|| (gen_string(), FileEntry::gen_many())), 76 | } 77 | } 78 | 79 | fn check_field(&mut self, mut field: MultipartField) -> M { 80 | // text/plain fields would be considered a file by `TestFields` 81 | if field.headers.content_type.is_none() { 82 | let mut text_entries = expect_fmt!( 83 | get_field(&field.headers, &mut self.texts), 84 | "Got text field that wasn't in original dataset: {:?}", 85 | field.headers 86 | ); 87 | 88 | let mut text = String::new(); 89 | expect_ok_fmt!( 90 | field.data.read_to_string(&mut text), 91 | "error failed to read text data to string: {:?}\n{err}", 92 | field.headers 93 | ); 94 | 95 | assert!( 96 | text_entries.get_mut().remove(&text), 97 | "Got field text data that wasn't in original data set: {:?}\n{:?}\n{:?}", 98 | field.headers, 99 | text, 100 | text_entries.get(), 101 | ); 102 | 103 | if text_entries.get().is_empty() { 104 | text_entries.remove_entry(); 105 | } 106 | 107 | return field.data.into_inner(); 108 | } 109 | 110 | let mut file_entries = expect_fmt!( 111 | get_field(&field.headers, &mut self.files), 112 | "Got file field that wasn't in original dataset: {:?}", 113 | field.headers 114 | ); 115 | 116 | let field_name = field.headers.name.clone(); 117 | let (test_entry, inner) = FileEntry::from_field(field); 118 | 119 | assert!( 120 | file_entries.get_mut().remove(&test_entry), 121 | "Got field entry that wasn't in original dataset: name: {:?}\n{:?}\nEntries: {:?}", 122 | field_name, 123 | test_entry, 124 | file_entries.get() 125 | ); 126 | 127 | if file_entries.get().is_empty() { 128 | file_entries.remove_entry(); 129 | } 130 | 131 | return inner; 132 | } 133 | 134 | fn assert_is_empty(&self) { 135 | assert!( 136 | self.texts.is_empty(), 137 | "Text Fields were not exhausted! {:?}", 138 | self.texts 139 | ); 140 | assert!( 141 | self.files.is_empty(), 142 | "File Fields were not exhausted! {:?}", 143 | self.files 144 | ); 145 | } 146 | } 147 | 148 | #[derive(Debug, Hash, PartialEq, Eq)] 149 | struct FileEntry { 150 | content_type: Mime, 151 | filename: Option, 152 | data: PrintHex, 153 | } 154 | 155 | impl FileEntry { 156 | fn from_field(mut field: MultipartField) -> (FileEntry, M) { 157 | let mut data = Vec::new(); 158 | expect_ok_fmt!( 159 | field.data.read_to_end(&mut data), 160 | "Error reading file field: {:?}\n{err}", 161 | field.headers 162 | ); 163 | 164 | ( 165 | FileEntry { 166 | content_type: field 167 | .headers 168 | .content_type 169 | .unwrap_or(mime::APPLICATION_OCTET_STREAM), 170 | filename: field.headers.filename, 171 | data: PrintHex(data), 172 | }, 173 | field.data.into_inner(), 174 | ) 175 | } 176 | 177 | fn gen_many() -> HashSet { 178 | collect_rand(Self::gen) 179 | } 180 | 181 | fn gen() -> Self { 182 | let filename = match gen_bool() { 183 | true => Some(gen_string()), 184 | false => None, 185 | }; 186 | 187 | let data = PrintHex(match gen_bool() { 188 | true => gen_string().into_bytes(), 189 | false => gen_bytes(), 190 | }); 191 | 192 | FileEntry { 193 | content_type: rand_mime(), 194 | filename, 195 | data, 196 | } 197 | } 198 | 199 | fn filename(&self) -> Option<&str> { 200 | self.filename.as_ref().map(|s| &**s) 201 | } 202 | } 203 | 204 | #[derive(PartialEq, Eq, Hash)] 205 | struct PrintHex(Vec); 206 | 207 | impl fmt::Debug for PrintHex { 208 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 209 | write!(f, "[")?; 210 | 211 | let mut written = false; 212 | 213 | for byte in &self.0 { 214 | write!(f, "{:X}", byte)?; 215 | 216 | if written { 217 | write!(f, ", ")?; 218 | } 219 | 220 | written = true; 221 | } 222 | 223 | write!(f, "]") 224 | } 225 | } 226 | 227 | macro_rules! do_test ( 228 | ($client_test:ident, $server_test:ident) => ( 229 | ::init_log(); 230 | 231 | info!("Client Test: {:?} Server Test: {:?}", stringify!($client_test), 232 | stringify!($server_test)); 233 | 234 | let mut test_fields = TestFields::gen(); 235 | 236 | trace!("Fields for test: {:?}", test_fields); 237 | 238 | let buf = $client_test(&test_fields); 239 | 240 | trace!( 241 | "\n==Test Buffer Begin==\n{}\n==Test Buffer End==", 242 | String::from_utf8_lossy(&buf.buf) 243 | ); 244 | 245 | $server_test(buf, &mut test_fields); 246 | 247 | test_fields.assert_is_empty(); 248 | ); 249 | ); 250 | 251 | #[test] 252 | fn reg_client_reg_server() { 253 | do_test!(test_client, test_server); 254 | } 255 | 256 | #[test] 257 | fn reg_client_entry_server() { 258 | do_test!(test_client, test_server_entry_api); 259 | } 260 | 261 | #[test] 262 | fn lazy_client_reg_server() { 263 | do_test!(test_client_lazy, test_server); 264 | } 265 | 266 | #[test] 267 | fn lazy_client_entry_server() { 268 | do_test!(test_client_lazy, test_server_entry_api); 269 | } 270 | 271 | mod extended { 272 | use super::{test_client, test_client_lazy, test_server, test_server_entry_api, TestFields}; 273 | 274 | use std::time::Instant; 275 | 276 | const TIME_LIMIT_SECS: u64 = 600; 277 | 278 | #[test] 279 | #[ignore] 280 | fn reg_client_reg_server() { 281 | let started = Instant::now(); 282 | 283 | while started.elapsed().as_secs() < TIME_LIMIT_SECS { 284 | do_test!(test_client, test_server); 285 | } 286 | } 287 | 288 | #[test] 289 | #[ignore] 290 | fn reg_client_entry_server() { 291 | let started = Instant::now(); 292 | 293 | while started.elapsed().as_secs() < TIME_LIMIT_SECS { 294 | do_test!(test_client, test_server_entry_api); 295 | } 296 | } 297 | 298 | #[test] 299 | #[ignore] 300 | fn lazy_client_reg_server() { 301 | let started = Instant::now(); 302 | 303 | while started.elapsed().as_secs() < TIME_LIMIT_SECS { 304 | do_test!(test_client_lazy, test_server); 305 | } 306 | } 307 | 308 | #[test] 309 | #[ignore] 310 | fn lazy_client_entry_server() { 311 | let started = Instant::now(); 312 | 313 | while started.elapsed().as_secs() < TIME_LIMIT_SECS { 314 | do_test!(test_client_lazy, test_server_entry_api); 315 | } 316 | } 317 | } 318 | 319 | fn gen_bool() -> bool { 320 | rand::thread_rng().gen() 321 | } 322 | 323 | fn gen_string() -> String { 324 | use rand::distributions::Alphanumeric; 325 | 326 | let mut rng_1 = rand::thread_rng(); 327 | let mut rng_2 = rand::thread_rng(); 328 | 329 | let str_len_1 = rng_1.gen_range(MIN_LEN..=MAX_LEN); 330 | let str_len_2 = rng_2.gen_range(MIN_LEN..=MAX_LEN); 331 | let num_dashes = rng_1.gen_range(0..=MAX_DASHES); 332 | 333 | rng_1 334 | .sample_iter(&Alphanumeric) 335 | .take(str_len_1) 336 | .chain(iter::repeat(b'-').take(num_dashes)) 337 | .chain(rng_2.sample_iter(&Alphanumeric).take(str_len_2)) 338 | .map(|c| c as char) 339 | .collect() 340 | } 341 | 342 | fn gen_bytes() -> Vec { 343 | gen_string().into_bytes() 344 | } 345 | 346 | fn test_client(test_fields: &TestFields) -> HttpBuffer { 347 | use client::Multipart; 348 | 349 | let request = ClientRequest::default(); 350 | 351 | let mut test_files = test_fields 352 | .files 353 | .iter() 354 | .flat_map(|(name, files)| files.iter().map(move |file| (name, file))); 355 | 356 | let test_texts = test_fields 357 | .texts 358 | .iter() 359 | .flat_map(|(name, texts)| texts.iter().map(move |text| (name, text))); 360 | 361 | let mut multipart = Multipart::from_request(request).unwrap(); 362 | 363 | // Intersperse file fields amongst text fields 364 | for (name, text) in test_texts { 365 | if let Some((file_name, file)) = test_files.next() { 366 | multipart 367 | .write_stream( 368 | file_name, 369 | &mut &*file.data.0, 370 | file.filename(), 371 | Some(file.content_type.clone()), 372 | ) 373 | .unwrap(); 374 | } 375 | 376 | multipart.write_text(name, text).unwrap(); 377 | } 378 | 379 | // Write remaining files 380 | for (file_name, file) in test_files { 381 | multipart 382 | .write_stream( 383 | file_name, 384 | &mut &*file.data.0, 385 | file.filename(), 386 | Some(file.content_type.clone()), 387 | ) 388 | .unwrap(); 389 | } 390 | 391 | multipart.send().unwrap() 392 | } 393 | 394 | fn test_client_lazy(test_fields: &TestFields) -> HttpBuffer { 395 | use client::lazy::Multipart; 396 | 397 | let mut multipart = Multipart::new(); 398 | 399 | let mut test_files = test_fields 400 | .files 401 | .iter() 402 | .flat_map(|(name, files)| files.iter().map(move |file| (name, file))); 403 | 404 | let test_texts = test_fields 405 | .texts 406 | .iter() 407 | .flat_map(|(name, texts)| texts.iter().map(move |text| (name, text))); 408 | 409 | for (name, text) in test_texts { 410 | if let Some((file_name, file)) = test_files.next() { 411 | multipart.add_stream( 412 | &**file_name, 413 | Cursor::new(&file.data.0), 414 | file.filename(), 415 | Some(file.content_type.clone()), 416 | ); 417 | } 418 | 419 | multipart.add_text(&**name, &**text); 420 | } 421 | 422 | for (file_name, file) in test_files { 423 | multipart.add_stream( 424 | &**file_name, 425 | Cursor::new(&file.data.0), 426 | file.filename(), 427 | Some(file.content_type.clone()), 428 | ); 429 | } 430 | 431 | let mut prepared = multipart.prepare().unwrap(); 432 | 433 | let mut buf = Vec::new(); 434 | 435 | let boundary = prepared.boundary().to_owned(); 436 | let content_len = prepared.content_len(); 437 | 438 | prepared.read_to_end(&mut buf).unwrap(); 439 | 440 | HttpBuffer::with_buf(buf, boundary, content_len) 441 | } 442 | 443 | fn test_server(buf: HttpBuffer, fields: &mut TestFields) { 444 | use server::Multipart; 445 | 446 | let server_buf = buf.for_server(); 447 | 448 | if let Some(content_len) = server_buf.content_len { 449 | assert!( 450 | content_len == server_buf.data.len() as u64, 451 | "Supplied content_len different from actual" 452 | ); 453 | } 454 | 455 | let mut multipart = Multipart::from_request(server_buf) 456 | .unwrap_or_else(|_| panic!("Buffer should be multipart!")); 457 | 458 | while let Some(field) = multipart.read_entry_mut().unwrap_opt() { 459 | fields.check_field(field); 460 | } 461 | } 462 | 463 | fn test_server_entry_api(buf: HttpBuffer, fields: &mut TestFields) { 464 | use server::Multipart; 465 | 466 | let server_buf = buf.for_server(); 467 | 468 | if let Some(content_len) = server_buf.content_len { 469 | assert!( 470 | content_len == server_buf.data.len() as u64, 471 | "Supplied content_len different from actual" 472 | ); 473 | } 474 | 475 | let mut multipart = Multipart::from_request(server_buf) 476 | .unwrap_or_else(|_| panic!("Buffer should be multipart!")); 477 | 478 | let entry = multipart 479 | .into_entry() 480 | .expect_alt("Expected entry, got none", "Error reading entry"); 481 | multipart = fields.check_field(entry); 482 | 483 | while let Some(entry) = multipart.into_entry().unwrap_opt() { 484 | multipart = fields.check_field(entry); 485 | } 486 | } 487 | 488 | fn rand_mime() -> Mime { 489 | [ 490 | mime::APPLICATION_OCTET_STREAM, 491 | mime::TEXT_PLAIN, 492 | mime::IMAGE_PNG, 493 | ] 494 | .choose(&mut rand::thread_rng()) 495 | .unwrap() 496 | .clone() 497 | } 498 | -------------------------------------------------------------------------------- /src/mock.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 `multipart` Crate Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | //! Mocked types for client-side and server-side APIs. 8 | use std::io::{self, Read, Write}; 9 | use std::fmt; 10 | 11 | use rand::{self, Rng}; 12 | use rand::prelude::ThreadRng; 13 | 14 | /// A mock implementation of `client::HttpRequest` which can spawn an `HttpBuffer`. 15 | /// 16 | /// `client::HttpRequest` impl requires the `client` feature. 17 | #[derive(Default, Debug)] 18 | pub struct ClientRequest { 19 | boundary: Option, 20 | content_len: Option, 21 | } 22 | 23 | #[cfg(feature = "client")] 24 | impl ::client::HttpRequest for ClientRequest { 25 | type Stream = HttpBuffer; 26 | type Error = io::Error; 27 | 28 | fn apply_headers(&mut self, boundary: &str, content_len: Option) -> bool { 29 | self.boundary = Some(boundary.into()); 30 | self.content_len = content_len; 31 | true 32 | } 33 | 34 | /// ## Panics 35 | /// If `apply_headers()` was not called. 36 | fn open_stream(self) -> Result { 37 | debug!("ClientRequest::open_stream called! {:?}", self); 38 | let boundary = self.boundary.expect("ClientRequest::set_headers() was not called!"); 39 | 40 | Ok(HttpBuffer::new_empty(boundary, self.content_len)) 41 | } 42 | } 43 | 44 | 45 | /// A writable buffer which stores the boundary and content-length, if provided. 46 | /// 47 | /// Implements `client::HttpStream` if the `client` feature is enabled. 48 | pub struct HttpBuffer { 49 | /// The buffer containing the raw bytes. 50 | pub buf: Vec, 51 | /// The multipart boundary. 52 | pub boundary: String, 53 | /// The value of the content-length header, if set. 54 | pub content_len: Option, 55 | rng: ThreadRng, 56 | } 57 | 58 | impl HttpBuffer { 59 | /// Create an empty buffer with the given boundary and optional content-length. 60 | pub fn new_empty(boundary: String, content_len: Option) -> HttpBuffer { 61 | Self::with_buf(Vec::new(), boundary, content_len) 62 | } 63 | 64 | /// Wrap the given buffer with the given boundary and optional content-length. 65 | pub fn with_buf(buf: Vec, boundary: String, content_len: Option) -> Self { 66 | HttpBuffer { 67 | buf, 68 | boundary, 69 | content_len, 70 | rng: rand::thread_rng() 71 | } 72 | } 73 | 74 | /// Get a `ServerRequest` wrapping the data in this buffer. 75 | pub fn for_server(&self) -> ServerRequest { 76 | ServerRequest { 77 | data: &self.buf, 78 | boundary: &self.boundary, 79 | content_len: self.content_len, 80 | rng: rand::thread_rng(), 81 | } 82 | } 83 | } 84 | 85 | impl Write for HttpBuffer { 86 | /// To simulate a network connection, this will copy a random number of bytes 87 | /// from `buf` to the buffer. 88 | fn write(&mut self, buf: &[u8]) -> io::Result { 89 | if buf.is_empty() { 90 | debug!("HttpBuffer::write() was passed a zero-sized buffer."); 91 | return Ok(0); 92 | } 93 | 94 | // Simulate the randomness of a network connection by not always reading everything 95 | let len = self.rng.gen_range(1..=buf.len()); 96 | 97 | self.buf.write(&buf[..len]) 98 | } 99 | 100 | fn flush(&mut self) -> io::Result<()> { 101 | self.buf.flush() 102 | } 103 | } 104 | 105 | #[cfg(feature = "client")] 106 | impl ::client::HttpStream for HttpBuffer { 107 | type Request = ClientRequest; 108 | type Response = HttpBuffer; 109 | type Error = io::Error; 110 | 111 | /// Returns `Ok(self)`. 112 | fn finish(self) -> Result { Ok(self) } 113 | } 114 | 115 | impl fmt::Debug for HttpBuffer { 116 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 117 | f.debug_struct("multipart::mock::HttpBuffer") 118 | .field("buf", &self.buf) 119 | .field("boundary", &self.boundary) 120 | .field("content_len", &self.content_len) 121 | .finish() 122 | } 123 | } 124 | 125 | /// A mock implementation of `server::HttpRequest` that can be read. 126 | /// 127 | /// Implements `server::HttpRequest` if the `server` feature is enabled. 128 | pub struct ServerRequest<'a> { 129 | /// Slice of the source `HttpBuffer::buf` 130 | pub data: &'a [u8], 131 | /// The multipart boundary. 132 | pub boundary: &'a str, 133 | /// The value of the content-length header, if set. 134 | pub content_len: Option, 135 | rng: ThreadRng, 136 | } 137 | 138 | impl<'a> ServerRequest<'a> { 139 | /// Create a new `ServerRequest` with the given data and boundary. 140 | /// 141 | /// Assumes `content_len: None` 142 | pub fn new(data: &'a [u8], boundary: &'a str) -> Self { 143 | ServerRequest { 144 | data, 145 | boundary, 146 | content_len: None, 147 | rng: rand::thread_rng(), 148 | } 149 | } 150 | } 151 | 152 | impl<'a> Read for ServerRequest<'a> { 153 | /// To simulate a network connection, this will copy a random number of bytes 154 | /// from the buffer to `out`. 155 | fn read(&mut self, out: &mut [u8]) -> io::Result { 156 | if out.is_empty() { 157 | debug!("ServerRequest::read() was passed a zero-sized buffer."); 158 | return Ok(0); 159 | } 160 | 161 | // Simulate the randomness of a network connection by not always reading everything 162 | let len = self.rng.gen_range(1..=out.len()); 163 | self.data.read(&mut out[..len]) 164 | } 165 | } 166 | 167 | #[cfg(feature = "server")] 168 | impl<'a> ::server::HttpRequest for ServerRequest<'a> { 169 | type Body = Self; 170 | 171 | fn multipart_boundary(&self) -> Option<&str> { Some(self.boundary) } 172 | 173 | fn body(self) -> Self::Body { 174 | self 175 | } 176 | } 177 | 178 | /// A `Write` adapter that duplicates all data written to the inner writer as well as stdout. 179 | pub struct StdoutTee<'s, W> { 180 | inner: W, 181 | stdout: io::StdoutLock<'s>, 182 | } 183 | 184 | impl<'s, W> StdoutTee<'s, W> { 185 | /// Constructor 186 | pub fn new(inner: W, stdout: &'s io::Stdout) -> Self { 187 | Self { 188 | inner, stdout: stdout.lock(), 189 | } 190 | } 191 | } 192 | 193 | impl<'s, W: Write> Write for StdoutTee<'s, W> { 194 | fn write(&mut self, buf: &[u8]) -> io::Result { 195 | self.inner.write_all(buf)?; 196 | self.stdout.write(buf) 197 | } 198 | 199 | fn flush(&mut self) -> io::Result<()> { 200 | self.inner.flush()?; 201 | self.stdout.flush() 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/server/boundary.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 `multipart` Crate Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | 8 | //! Boundary parsing for `multipart` requests. 9 | 10 | use ::safemem; 11 | 12 | use super::buf_redux::BufReader; 13 | use super::buf_redux::policy::MinBuffered; 14 | use super::twoway; 15 | 16 | use std::cmp; 17 | use std::borrow::Borrow; 18 | 19 | use std::io; 20 | use std::io::prelude::*; 21 | 22 | use self::State::*; 23 | 24 | pub const MIN_BUF_SIZE: usize = 1024; 25 | 26 | #[derive(Debug, PartialEq, Eq)] 27 | enum State { 28 | Searching, 29 | BoundaryRead, 30 | AtEnd 31 | } 32 | 33 | /// A struct implementing `Read` and `BufRead` that will yield bytes until it sees a given sequence. 34 | #[derive(Debug)] 35 | pub struct BoundaryReader { 36 | source: BufReader, 37 | boundary: Vec, 38 | search_idx: usize, 39 | state: State, 40 | } 41 | 42 | impl BoundaryReader where R: Read { 43 | /// Internal API 44 | pub fn from_reader>>(reader: R, boundary: B) -> BoundaryReader { 45 | let mut boundary = boundary.into(); 46 | safemem::prepend(b"--", &mut boundary); 47 | let source = BufReader::new(reader).set_policy(MinBuffered(MIN_BUF_SIZE)); 48 | 49 | BoundaryReader { 50 | source, 51 | boundary, 52 | search_idx: 0, 53 | state: Searching, 54 | } 55 | } 56 | 57 | fn read_to_boundary(&mut self) -> io::Result<&[u8]> { 58 | let buf = self.source.fill_buf()?; 59 | 60 | trace!("Buf: {:?}", String::from_utf8_lossy(buf)); 61 | 62 | debug!("Before search Buf len: {} Search idx: {} State: {:?}", 63 | buf.len(), self.search_idx, self.state); 64 | 65 | if self.state == BoundaryRead || self.state == AtEnd { 66 | return Ok(&buf[..self.search_idx]) 67 | } 68 | 69 | if self.state == Searching && self.search_idx < buf.len() { 70 | let lookahead = &buf[self.search_idx..]; 71 | 72 | // Look for the boundary, or if it isn't found, stop near the end. 73 | match find_boundary(lookahead, &self.boundary) { 74 | Ok(found_idx) => { 75 | self.search_idx += found_idx; 76 | self.state = BoundaryRead; 77 | }, 78 | Err(yield_len) => { 79 | self.search_idx += yield_len; 80 | } 81 | } 82 | } 83 | 84 | debug!("After search Buf len: {} Search idx: {} State: {:?}", 85 | buf.len(), self.search_idx, self.state); 86 | 87 | // back up the cursor to before the boundary's preceding CRLF if we haven't already 88 | if self.search_idx >= 2 && !buf[self.search_idx..].starts_with(b"\r\n") { 89 | let two_bytes_before = &buf[self.search_idx - 2 .. self.search_idx]; 90 | 91 | trace!("Two bytes before: {:?} ({:?}) (\"\\r\\n\": {:?})", 92 | String::from_utf8_lossy(two_bytes_before), two_bytes_before, b"\r\n"); 93 | 94 | if two_bytes_before == *b"\r\n" { 95 | debug!("Subtract two!"); 96 | self.search_idx -= 2; 97 | } 98 | } 99 | 100 | let ret_buf = &buf[..self.search_idx]; 101 | 102 | trace!("Returning buf: {:?}", String::from_utf8_lossy(ret_buf)); 103 | 104 | Ok(ret_buf) 105 | } 106 | 107 | pub fn set_min_buf_size(&mut self, min_buf_size: usize) { 108 | // ensure the minimum buf size is at least enough to find a boundary with some extra 109 | let min_buf_size = cmp::max(self.boundary.len() * 2, min_buf_size); 110 | 111 | self.source.policy_mut().0 = min_buf_size; 112 | } 113 | 114 | pub fn consume_boundary(&mut self) -> io::Result { 115 | if self.state == AtEnd { 116 | return Ok(false); 117 | } 118 | 119 | while self.state == Searching { 120 | debug!("Boundary not found yet"); 121 | 122 | let buf_len = self.read_to_boundary()?.len(); 123 | 124 | if buf_len == 0 && self.state == Searching { 125 | return Err(io::Error::new(io::ErrorKind::UnexpectedEof, 126 | "unexpected end of request body")); 127 | } 128 | 129 | debug!("Discarding {} bytes", buf_len); 130 | 131 | self.consume(buf_len); 132 | } 133 | 134 | let consume_amt = { 135 | let buf = self.source.fill_buf()?; 136 | 137 | // if the boundary is found we should have at least this much in-buffer 138 | let mut consume_amt = self.search_idx + self.boundary.len(); 139 | 140 | // we don't care about data before the cursor 141 | let bnd_segment = &buf[self.search_idx..]; 142 | 143 | if bnd_segment.starts_with(b"\r\n") { 144 | // preceding CRLF needs to be consumed as well 145 | consume_amt += 2; 146 | 147 | // assert that we've found the boundary after the CRLF 148 | debug_assert_eq!(*self.boundary, bnd_segment[2 .. self.boundary.len() + 2]); 149 | } else { 150 | // assert that we've found the boundary 151 | debug_assert_eq!(*self.boundary, bnd_segment[..self.boundary.len()]); 152 | } 153 | 154 | // include the trailing CRLF or -- 155 | consume_amt += 2; 156 | 157 | if buf.len() < consume_amt { 158 | return Err(io::Error::new(io::ErrorKind::UnexpectedEof, 159 | "not enough bytes to verify boundary")); 160 | } 161 | 162 | // we have enough bytes to verify 163 | self.state = Searching; 164 | 165 | let last_two = &buf[consume_amt - 2 .. consume_amt]; 166 | 167 | match last_two { 168 | b"\r\n" => self.state = Searching, 169 | b"--" => self.state = AtEnd, 170 | _ => return Err(io::Error::new( 171 | io::ErrorKind::InvalidData, 172 | format!("unexpected bytes following multipart boundary: {:X} {:X}", 173 | last_two[0], last_two[1]) 174 | )), 175 | } 176 | 177 | consume_amt 178 | }; 179 | 180 | trace!("Consuming {} bytes, remaining buf: {:?}", 181 | consume_amt, 182 | String::from_utf8_lossy(self.source.buffer())); 183 | 184 | self.source.consume(consume_amt); 185 | 186 | if cfg!(debug_assertions) { 187 | 188 | } 189 | 190 | self.search_idx = 0; 191 | 192 | trace!("Consumed boundary (state: {:?}), remaining buf: {:?}", self.state, 193 | String::from_utf8_lossy(self.source.buffer())); 194 | 195 | Ok(self.state != AtEnd) 196 | } 197 | } 198 | 199 | /// Find the boundary occurrence or the highest length to safely yield 200 | fn find_boundary(buf: &[u8], boundary: &[u8]) -> Result { 201 | if let Some(idx) = twoway::find_bytes(buf, boundary) { 202 | return Ok(idx); 203 | } 204 | 205 | let search_start = buf.len().saturating_sub(boundary.len()); 206 | 207 | // search for just the boundary fragment 208 | for i in search_start .. buf.len() { 209 | if boundary.starts_with(&buf[i..]) { 210 | return Err(i); 211 | } 212 | } 213 | 214 | Err(buf.len()) 215 | } 216 | 217 | #[cfg(feature = "bench")] 218 | impl<'a> BoundaryReader> { 219 | fn new_with_bytes(bytes: &'a [u8], boundary: &str) -> Self { 220 | Self::from_reader(io::Cursor::new(bytes), boundary) 221 | } 222 | 223 | fn reset(&mut self) { 224 | // Dump buffer and reset cursor 225 | self.source.seek(io::SeekFrom::Start(0)); 226 | self.state = Searching; 227 | self.search_idx = 0; 228 | } 229 | } 230 | 231 | impl Borrow for BoundaryReader { 232 | fn borrow(&self) -> &R { 233 | self.source.get_ref() 234 | } 235 | } 236 | 237 | impl Read for BoundaryReader where R: Read { 238 | fn read(&mut self, out: &mut [u8]) -> io::Result { 239 | let read = { 240 | let mut buf = self.read_to_boundary()?; 241 | // This shouldn't ever be an error so unwrapping is fine. 242 | buf.read(out).unwrap() 243 | }; 244 | 245 | self.consume(read); 246 | Ok(read) 247 | } 248 | } 249 | 250 | impl BufRead for BoundaryReader where R: Read { 251 | fn fill_buf(&mut self) -> io::Result<&[u8]> { 252 | self.read_to_boundary() 253 | } 254 | 255 | fn consume(&mut self, amt: usize) { 256 | let true_amt = cmp::min(amt, self.search_idx); 257 | 258 | debug!("Consume! amt: {} true amt: {}", amt, true_amt); 259 | 260 | self.source.consume(true_amt); 261 | self.search_idx -= true_amt; 262 | } 263 | } 264 | 265 | #[cfg(test)] 266 | mod test { 267 | use super::BoundaryReader; 268 | 269 | use std::io; 270 | use std::io::prelude::*; 271 | 272 | const BOUNDARY: &'static str = "boundary"; 273 | const TEST_VAL: &'static str = "--boundary\r\n\ 274 | dashed-value-1\r\n\ 275 | --boundary\r\n\ 276 | dashed-value-2\r\n\ 277 | --boundary--"; 278 | 279 | #[test] 280 | fn test_boundary() { 281 | ::init_log(); 282 | 283 | debug!("Testing boundary (no split)"); 284 | 285 | let src = &mut TEST_VAL.as_bytes(); 286 | let mut reader = BoundaryReader::from_reader(src, BOUNDARY); 287 | 288 | let mut buf = String::new(); 289 | 290 | test_boundary_reader(&mut reader, &mut buf); 291 | } 292 | 293 | struct SplitReader<'a> { 294 | left: &'a [u8], 295 | right: &'a [u8], 296 | } 297 | 298 | impl<'a> SplitReader<'a> { 299 | fn split(data: &'a [u8], at: usize) -> SplitReader<'a> { 300 | let (left, right) = data.split_at(at); 301 | 302 | SplitReader { 303 | left: left, 304 | right: right, 305 | } 306 | } 307 | } 308 | 309 | impl<'a> Read for SplitReader<'a> { 310 | fn read(&mut self, dst: &mut [u8]) -> io::Result { 311 | fn copy_bytes_partial(src: &mut &[u8], dst: &mut [u8]) -> usize { 312 | src.read(dst).unwrap() 313 | } 314 | 315 | let mut copy_amt = copy_bytes_partial(&mut self.left, dst); 316 | 317 | if copy_amt == 0 { 318 | copy_amt = copy_bytes_partial(&mut self.right, dst) 319 | }; 320 | 321 | Ok(copy_amt) 322 | } 323 | } 324 | 325 | #[test] 326 | fn test_split_boundary() { 327 | ::init_log(); 328 | 329 | debug!("Testing boundary (split)"); 330 | 331 | let mut buf = String::new(); 332 | 333 | // Substitute for `.step_by()` being unstable. 334 | for split_at in 0 .. TEST_VAL.len(){ 335 | debug!("Testing split at: {}", split_at); 336 | 337 | let src = SplitReader::split(TEST_VAL.as_bytes(), split_at); 338 | let mut reader = BoundaryReader::from_reader(src, BOUNDARY); 339 | test_boundary_reader(&mut reader, &mut buf); 340 | } 341 | } 342 | 343 | fn test_boundary_reader(reader: &mut BoundaryReader, buf: &mut String) { 344 | buf.clear(); 345 | 346 | debug!("Read 1"); 347 | let _ = reader.read_to_string(buf).unwrap(); 348 | assert!(buf.is_empty(), "Buffer not empty: {:?}", buf); 349 | buf.clear(); 350 | 351 | debug!("Consume 1"); 352 | reader.consume_boundary().unwrap(); 353 | 354 | debug!("Read 2"); 355 | let _ = reader.read_to_string(buf).unwrap(); 356 | assert_eq!(buf, "dashed-value-1"); 357 | buf.clear(); 358 | 359 | debug!("Consume 2"); 360 | reader.consume_boundary().unwrap(); 361 | 362 | debug!("Read 3"); 363 | let _ = reader.read_to_string(buf).unwrap(); 364 | assert_eq!(buf, "dashed-value-2"); 365 | buf.clear(); 366 | 367 | debug!("Consume 3"); 368 | reader.consume_boundary().unwrap(); 369 | 370 | debug!("Read 4"); 371 | let _ = reader.read_to_string(buf).unwrap(); 372 | assert_eq!(buf, ""); 373 | } 374 | 375 | #[test] 376 | fn test_empty_body() { 377 | ::init_log(); 378 | 379 | // empty body contains closing boundary only 380 | let mut body: &[u8] = b"--boundary--"; 381 | 382 | let ref mut buf = String::new(); 383 | let mut reader = BoundaryReader::from_reader(&mut body, BOUNDARY); 384 | 385 | debug!("Consume 1"); 386 | assert_eq!(reader.consume_boundary().unwrap(), false); 387 | 388 | debug!("Read 1"); 389 | let _ = reader.read_to_string(buf).unwrap(); 390 | assert_eq!(buf, ""); 391 | buf.clear(); 392 | 393 | debug!("Consume 2"); 394 | assert_eq!(reader.consume_boundary().unwrap(), false); 395 | } 396 | 397 | #[test] 398 | fn test_leading_crlf() { 399 | ::init_log(); 400 | 401 | let mut body: &[u8] = b"\r\n\r\n--boundary\r\n\ 402 | asdf1234\ 403 | \r\n\r\n--boundary--"; 404 | 405 | let ref mut buf = String::new(); 406 | let mut reader = BoundaryReader::from_reader(&mut body, BOUNDARY); 407 | 408 | 409 | debug!("Consume 1"); 410 | assert_eq!(reader.consume_boundary().unwrap(), true); 411 | 412 | debug!("Read 1"); 413 | let _ = reader.read_to_string(buf).unwrap(); 414 | assert_eq!(buf, "asdf1234\r\n"); 415 | buf.clear(); 416 | 417 | debug!("Consume 2"); 418 | assert_eq!(reader.consume_boundary().unwrap(), false); 419 | 420 | debug!("Read 2 (empty)"); 421 | let _ = reader.read_to_string(buf).unwrap(); 422 | assert_eq!(buf, ""); 423 | } 424 | 425 | #[test] 426 | fn test_trailing_crlf() { 427 | ::init_log(); 428 | 429 | let mut body: &[u8] = b"--boundary\r\n\ 430 | asdf1234\ 431 | \r\n\r\n--boundary\r\n\ 432 | hjkl5678\r\n--boundary--"; 433 | 434 | let ref mut buf = String::new(); 435 | let mut reader = BoundaryReader::from_reader(&mut body, BOUNDARY); 436 | 437 | debug!("Consume 1"); 438 | assert_eq!(reader.consume_boundary().unwrap(), true); 439 | 440 | debug!("Read 1"); 441 | 442 | // Repro for https://github.com/abonander/multipart/issues/93 443 | // These two reads should produce the same buffer 444 | let buf1 = reader.read_to_boundary().unwrap().to_owned(); 445 | let buf2 = reader.read_to_boundary().unwrap().to_owned(); 446 | assert_eq!(buf1, buf2); 447 | 448 | let _ = reader.read_to_string(buf).unwrap(); 449 | assert_eq!(buf, "asdf1234\r\n"); 450 | buf.clear(); 451 | 452 | debug!("Consume 2"); 453 | assert_eq!(reader.consume_boundary().unwrap(), true); 454 | 455 | debug!("Read 2"); 456 | let _ = reader.read_to_string(buf).unwrap(); 457 | assert_eq!(buf, "hjkl5678"); 458 | buf.clear(); 459 | 460 | debug!("Consume 3"); 461 | assert_eq!(reader.consume_boundary().unwrap(), false); 462 | 463 | debug!("Read 3 (empty)"); 464 | let _ = reader.read_to_string(buf).unwrap(); 465 | assert_eq!(buf, ""); 466 | } 467 | 468 | // https://github.com/abonander/multipart/issues/93#issuecomment-343610587 469 | #[test] 470 | fn test_trailing_lflf() { 471 | ::init_log(); 472 | 473 | let mut body: &[u8] = b"--boundary\r\n\ 474 | asdf1234\ 475 | \n\n\r\n--boundary\r\n\ 476 | hjkl5678\r\n--boundary--"; 477 | 478 | let ref mut buf = String::new(); 479 | let mut reader = BoundaryReader::from_reader(&mut body, BOUNDARY); 480 | 481 | debug!("Consume 1"); 482 | assert_eq!(reader.consume_boundary().unwrap(), true); 483 | 484 | debug!("Read 1"); 485 | 486 | // same as above 487 | let buf1 = reader.read_to_boundary().unwrap().to_owned(); 488 | let buf2 = reader.read_to_boundary().unwrap().to_owned(); 489 | assert_eq!(buf1, buf2); 490 | 491 | let _ = reader.read_to_string(buf).unwrap(); 492 | assert_eq!(buf, "asdf1234\n\n"); 493 | buf.clear(); 494 | 495 | debug!("Consume 2"); 496 | assert_eq!(reader.consume_boundary().unwrap(), true); 497 | 498 | debug!("Read 2"); 499 | let _ = reader.read_to_string(buf).unwrap(); 500 | assert_eq!(buf, "hjkl5678"); 501 | buf.clear(); 502 | 503 | debug!("Consume 3"); 504 | assert_eq!(reader.consume_boundary().unwrap(), false); 505 | 506 | debug!("Read 3 (empty)"); 507 | let _ = reader.read_to_string(buf).unwrap(); 508 | assert_eq!(buf, ""); 509 | } 510 | 511 | // https://github.com/abonander/multipart/issues/104 512 | #[test] 513 | fn test_unterminated_body() { 514 | ::init_log(); 515 | 516 | let mut body: &[u8] = b"--boundary\r\n\ 517 | asdf1234\ 518 | \n\n\r\n--boundary\r\n\ 519 | hjkl5678 "; 520 | 521 | let ref mut buf = String::new(); 522 | let mut reader = BoundaryReader::from_reader(&mut body, BOUNDARY); 523 | 524 | debug!("Consume 1"); 525 | assert_eq!(reader.consume_boundary().unwrap(), true); 526 | 527 | debug!("Read 1"); 528 | 529 | // same as above 530 | let buf1 = reader.read_to_boundary().unwrap().to_owned(); 531 | let buf2 = reader.read_to_boundary().unwrap().to_owned(); 532 | assert_eq!(buf1, buf2); 533 | 534 | let _ = reader.read_to_string(buf).unwrap(); 535 | assert_eq!(buf, "asdf1234\n\n"); 536 | buf.clear(); 537 | 538 | debug!("Consume 2"); 539 | assert_eq!(reader.consume_boundary().unwrap(), true); 540 | 541 | debug!("Read 2"); 542 | let _ = reader.read_to_string(buf).unwrap(); 543 | assert_eq!(buf, "hjkl5678 "); 544 | buf.clear(); 545 | 546 | debug!("Consume 3 - expecting error"); 547 | reader.consume_boundary().unwrap_err(); 548 | } 549 | 550 | #[test] 551 | fn test_lone_boundary() { 552 | let mut body: &[u8] = b"--boundary"; 553 | let mut reader = BoundaryReader::from_reader(&mut body, "boundary"); 554 | reader.consume_boundary().unwrap_err(); 555 | } 556 | 557 | #[test] 558 | fn test_invalid_boundary() { 559 | let mut body: &[u8] = b"--boundary\x00\x00"; 560 | let mut reader = BoundaryReader::from_reader(&mut body, "boundary"); 561 | reader.consume_boundary().unwrap_err(); 562 | } 563 | 564 | #[test] 565 | fn test_skip_field() { 566 | let mut body: &[u8] = b"--boundary\r\nfield1\r\n--boundary\r\nfield2\r\n--boundary--"; 567 | let mut reader = BoundaryReader::from_reader(&mut body, "boundary"); 568 | 569 | assert_eq!(reader.consume_boundary().unwrap(), true); 570 | // skip `field1` 571 | assert_eq!(reader.consume_boundary().unwrap(), true); 572 | 573 | let mut buf = String::new(); 574 | reader.read_to_string(&mut buf).unwrap(); 575 | assert_eq!(buf, "field2"); 576 | 577 | assert_eq!(reader.consume_boundary().unwrap(), false); 578 | } 579 | 580 | #[cfg(feature = "bench")] 581 | mod bench { 582 | extern crate test; 583 | use self::test::Bencher; 584 | 585 | use super::*; 586 | 587 | #[bench] 588 | fn bench_boundary_reader(b: &mut Bencher) { 589 | let mut reader = BoundaryReader::new_with_bytes(TEST_VAL.as_bytes(), BOUNDARY); 590 | let mut buf = String::with_capacity(256); 591 | 592 | b.iter(|| { 593 | reader.reset(); 594 | test_boundary_reader(&mut reader, &mut buf); 595 | }); 596 | } 597 | } 598 | } 599 | -------------------------------------------------------------------------------- /src/server/field.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 `multipart` Crate Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | 8 | //! `multipart` field header parsing. 9 | use mime::Mime; 10 | 11 | use std::error::Error; 12 | use std::io::{self, BufRead, Read}; 13 | use std::{fmt, str}; 14 | 15 | use std::sync::Arc; 16 | 17 | use super::httparse::{self, Error as HttparseError, Header, Status, EMPTY_HEADER}; 18 | 19 | use self::ReadEntryResult::*; 20 | 21 | use super::save::SaveBuilder; 22 | 23 | const EMPTY_STR_HEADER: StrHeader<'static> = StrHeader { name: "", val: "" }; 24 | 25 | macro_rules! invalid_cont_disp { 26 | ($reason: expr, $cause: expr) => { 27 | return Err(ParseHeaderError::InvalidContDisp( 28 | $reason, 29 | $cause.to_string(), 30 | )); 31 | }; 32 | } 33 | 34 | /// Not exposed 35 | #[derive(Copy, Clone, Debug)] 36 | pub struct StrHeader<'a> { 37 | name: &'a str, 38 | val: &'a str, 39 | } 40 | 41 | struct DisplayHeaders<'s, 'a: 's>(&'s [StrHeader<'a>]); 42 | 43 | impl<'s, 'a: 's> fmt::Display for DisplayHeaders<'s, 'a> { 44 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 45 | for hdr in self.0 { 46 | writeln!(f, "{}: {}", hdr.name, hdr.val)?; 47 | } 48 | 49 | Ok(()) 50 | } 51 | } 52 | 53 | fn with_headers(r: &mut R, closure: F) -> Result 54 | where 55 | R: BufRead, 56 | F: FnOnce(&[StrHeader]) -> Ret, 57 | { 58 | const HEADER_LEN: usize = 4; 59 | 60 | let consume; 61 | let ret; 62 | 63 | let mut last_len = 0; 64 | 65 | loop { 66 | // this should return a larger buffer each time 67 | let buf = r.fill_buf()?; 68 | 69 | // buffer has stopped growing 70 | if buf.len() == last_len { 71 | return Err(ParseHeaderError::TooLarge); 72 | } 73 | 74 | let mut raw_headers = [EMPTY_HEADER; HEADER_LEN]; 75 | 76 | match httparse::parse_headers(buf, &mut raw_headers)? { 77 | // read more and try again 78 | Status::Partial => last_len = buf.len(), 79 | Status::Complete((consume_, raw_headers)) => { 80 | let mut headers = [EMPTY_STR_HEADER; HEADER_LEN]; 81 | let headers = copy_headers(raw_headers, &mut headers)?; 82 | debug!("Parsed headers: {:?}", headers); 83 | consume = consume_; 84 | ret = closure(headers); 85 | break; 86 | } 87 | } 88 | } 89 | 90 | r.consume(consume); 91 | Ok(ret) 92 | } 93 | 94 | fn copy_headers<'h, 'b: 'h>( 95 | raw: &[Header<'b>], 96 | headers: &'h mut [StrHeader<'b>], 97 | ) -> io::Result<&'h [StrHeader<'b>]> { 98 | for (raw, header) in raw.iter().zip(&mut *headers) { 99 | header.name = raw.name; 100 | header.val = io_str_utf8(raw.value)?; 101 | } 102 | 103 | Ok(&headers[..raw.len()]) 104 | } 105 | 106 | /// The headers that (may) appear before a `multipart/form-data` field. 107 | /// 108 | /// ### Warning: Values are Client-Provided 109 | /// Everything in this struct are values from the client and should be considered **untrustworthy**. 110 | /// This crate makes no effort to validate or sanitize any client inputs. 111 | #[derive(Clone, Debug)] 112 | pub struct FieldHeaders { 113 | /// The field's name from the form. 114 | pub name: Arc, 115 | 116 | /// The filename of this entry, if supplied. This is not guaranteed to match the original file 117 | /// or even to be a valid filename for the current platform. 118 | pub filename: Option, 119 | 120 | /// The MIME type (`Content-Type` value) of this file, if supplied by the client. 121 | /// 122 | /// If this is not supplied, the content-type of the field should default to `text/plain` as 123 | /// per [IETF RFC 7578, section 4.4](https://tools.ietf.org/html/rfc7578#section-4.4), but this 124 | /// should not be implicitly trusted. This crate makes no attempt to identify or validate 125 | /// the content-type of the actual field data. 126 | pub content_type: Option, 127 | } 128 | 129 | impl FieldHeaders { 130 | /// Parse the field headers from the passed `BufRead`, consuming the relevant bytes. 131 | fn read_from(r: &mut R) -> Result { 132 | with_headers(r, Self::parse)? 133 | } 134 | 135 | fn parse(headers: &[StrHeader]) -> Result { 136 | let cont_disp = ContentDisp::parse_required(headers)?; 137 | 138 | Ok(FieldHeaders { 139 | name: cont_disp.field_name.into(), 140 | filename: cont_disp.filename, 141 | content_type: parse_content_type(headers)?, 142 | }) 143 | } 144 | } 145 | 146 | /// The `Content-Disposition` header. 147 | struct ContentDisp { 148 | /// The name of the `multipart/form-data` field. 149 | field_name: String, 150 | /// The optional filename for this field. 151 | filename: Option, 152 | } 153 | 154 | impl ContentDisp { 155 | fn parse_required(headers: &[StrHeader]) -> Result { 156 | let header = if let Some(header) = find_header(headers, "Content-Disposition") { 157 | header 158 | } else { 159 | return Err(ParseHeaderError::MissingContentDisposition( 160 | DisplayHeaders(headers).to_string(), 161 | )); 162 | }; 163 | 164 | // Content-Disposition: ? 165 | let after_disp_type = match split_once(header.val, ';') { 166 | Some((disp_type, after_disp_type)) => { 167 | // assert Content-Disposition: form-data 168 | // but needs to be parsed out to trim the spaces (allowed by spec IIRC) 169 | if disp_type.trim() != "form-data" { 170 | invalid_cont_disp!("unexpected Content-Disposition value", disp_type); 171 | } 172 | after_disp_type 173 | } 174 | None => invalid_cont_disp!( 175 | "expected additional data after Content-Disposition type", 176 | header.val 177 | ), 178 | }; 179 | 180 | // Content-Disposition: form-data; name=? 181 | let (field_name, filename) = match get_str_after("name=", ';', after_disp_type) { 182 | None => invalid_cont_disp!( 183 | "expected field name and maybe filename, got", 184 | after_disp_type 185 | ), 186 | // Content-Disposition: form-data; name={field_name}; filename=? 187 | Some((field_name, after_field_name)) => { 188 | let field_name = trim_quotes(field_name); 189 | let filename = get_str_after("filename=", ';', after_field_name) 190 | .map(|(filename, _)| trim_quotes(filename).to_owned()); 191 | (field_name, filename) 192 | } 193 | }; 194 | 195 | Ok(ContentDisp { 196 | field_name: field_name.to_owned(), 197 | filename, 198 | }) 199 | } 200 | } 201 | 202 | fn parse_content_type(headers: &[StrHeader]) -> Result, ParseHeaderError> { 203 | if let Some(header) = find_header(headers, "Content-Type") { 204 | // Boundary parameter will be parsed into the `Mime` 205 | debug!("Found Content-Type: {:?}", header.val); 206 | Ok(Some(header.val.parse::().map_err(|_| { 207 | ParseHeaderError::MimeError(header.val.into()) 208 | })?)) 209 | } else { 210 | Ok(None) 211 | } 212 | } 213 | 214 | /// A field in a multipart request with its associated headers and data. 215 | #[derive(Debug)] 216 | pub struct MultipartField { 217 | /// The headers for this field, including the name, filename, and content-type, if provided. 218 | /// 219 | /// ### Warning: Values are Client-Provided 220 | /// Everything in this struct are values from the client and should be considered **untrustworthy**. 221 | /// This crate makes no effort to validate or sanitize any client inputs. 222 | pub headers: FieldHeaders, 223 | 224 | /// The field's data. 225 | pub data: MultipartData, 226 | } 227 | 228 | impl MultipartField { 229 | /// Returns `true` if this field has no content-type or the content-type is `text/...`. 230 | /// 231 | /// This typically means it can be read to a string, but it could still be using an unsupported 232 | /// character encoding, so decoding to `String` needs to ensure that the data is valid UTF-8. 233 | /// 234 | /// Note also that the field contents may be too large to reasonably fit in memory. 235 | /// The `.save()` adapter can be used to enforce a size limit. 236 | /// 237 | /// Detecting character encodings by any means is (currently) beyond the scope of this crate. 238 | pub fn is_text(&self) -> bool { 239 | self.headers 240 | .content_type 241 | .as_ref() 242 | .map_or(true, |ct| ct.type_() == mime::TEXT) 243 | } 244 | 245 | /// Read the next entry in the request. 246 | pub fn next_entry(self) -> ReadEntryResult { 247 | self.data.into_inner().read_entry() 248 | } 249 | 250 | /// Update `self` as the next entry. 251 | /// 252 | /// Returns `Ok(Some(self))` if another entry was read, `Ok(None)` if the end of the body was 253 | /// reached, and `Err(e)` for any errors that occur. 254 | pub fn next_entry_inplace(&mut self) -> io::Result> 255 | where 256 | for<'a> &'a mut M: ReadEntry, 257 | { 258 | let multipart = self.data.take_inner(); 259 | 260 | match multipart.read_entry() { 261 | Entry(entry) => { 262 | *self = entry; 263 | Ok(Some(self)) 264 | } 265 | End(multipart) => { 266 | self.data.give_inner(multipart); 267 | Ok(None) 268 | } 269 | Error(multipart, err) => { 270 | self.data.give_inner(multipart); 271 | Err(err) 272 | } 273 | } 274 | } 275 | } 276 | 277 | /// The data of a field in a `multipart/form-data` request. 278 | /// 279 | /// You can read it to EOF, or use the `save()` adaptor to save it to disk/memory. 280 | #[derive(Debug)] 281 | pub struct MultipartData { 282 | inner: Option, 283 | } 284 | 285 | const DATA_INNER_ERR: &str = "MultipartFile::inner taken and not replaced; this is likely \ 286 | caused by a logic error in `multipart` or by resuming after \ 287 | a previously caught panic.\nPlease open an issue with the \ 288 | relevant backtrace and debug logs at \ 289 | https://github.com/abonander/multipart"; 290 | 291 | impl MultipartData 292 | where 293 | M: ReadEntry, 294 | { 295 | /// Get a builder type which can save the field with or without a size limit. 296 | pub fn save(&mut self) -> SaveBuilder<&mut Self> { 297 | SaveBuilder::new(self) 298 | } 299 | 300 | /// Take the inner `Multipart` or `&mut Multipart` 301 | pub fn into_inner(self) -> M { 302 | self.inner.expect(DATA_INNER_ERR) 303 | } 304 | 305 | /// Set the minimum buffer size that `BufRead::fill_buf(self)` will return 306 | /// until the end of the stream is reached. Set this as small as you can tolerate 307 | /// to minimize `read()` calls (`read()` won't be called again until the buffer 308 | /// is smaller than this). 309 | /// 310 | /// This value is reset between fields. 311 | pub fn set_min_buf_size(&mut self, min_buf_size: usize) { 312 | self.inner_mut().set_min_buf_size(min_buf_size) 313 | } 314 | 315 | fn inner_mut(&mut self) -> &mut M { 316 | self.inner.as_mut().expect(DATA_INNER_ERR) 317 | } 318 | 319 | fn take_inner(&mut self) -> M { 320 | self.inner.take().expect(DATA_INNER_ERR) 321 | } 322 | 323 | fn give_inner(&mut self, inner: M) { 324 | self.inner = Some(inner); 325 | } 326 | } 327 | 328 | impl Read for MultipartData { 329 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 330 | self.inner_mut().source_mut().read(buf) 331 | } 332 | } 333 | 334 | /// In this implementation, `fill_buf()` can return more data with each call. 335 | /// 336 | /// Use `set_min_buf_size()` if you require a minimum buffer length. 337 | impl BufRead for MultipartData { 338 | fn fill_buf(&mut self) -> io::Result<&[u8]> { 339 | self.inner_mut().source_mut().fill_buf() 340 | } 341 | 342 | fn consume(&mut self, amt: usize) { 343 | self.inner_mut().source_mut().consume(amt) 344 | } 345 | } 346 | 347 | fn split_once(s: &str, delim: char) -> Option<(&str, &str)> { 348 | s.find(delim).map(|idx| s.split_at(idx)) 349 | } 350 | 351 | fn trim_quotes(s: &str) -> &str { 352 | s.trim_matches('"') 353 | } 354 | 355 | /// Get the string after `needle` in `haystack`, stopping before `end_val_delim` 356 | fn get_str_after<'a>( 357 | needle: &str, 358 | end_val_delim: char, 359 | haystack: &'a str, 360 | ) -> Option<(&'a str, &'a str)> { 361 | let val_start_idx = try_opt!(haystack.find(needle)) + needle.len(); 362 | let val_end_idx = haystack[val_start_idx..] 363 | .find(end_val_delim) 364 | .map_or(haystack.len(), |end_idx| end_idx + val_start_idx); 365 | Some(( 366 | &haystack[val_start_idx..val_end_idx], 367 | &haystack[val_end_idx..], 368 | )) 369 | } 370 | 371 | fn io_str_utf8(buf: &[u8]) -> io::Result<&str> { 372 | str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) 373 | } 374 | 375 | fn find_header<'a, 'b>(headers: &'a [StrHeader<'b>], name: &str) -> Option<&'a StrHeader<'b>> { 376 | // Field names are case insensitive and consist of ASCII characters 377 | // only (see https://tools.ietf.org/html/rfc822#section-3.2). 378 | headers 379 | .iter() 380 | .find(|header| header.name.eq_ignore_ascii_case(name)) 381 | } 382 | 383 | /// Common trait for `Multipart` and `&mut Multipart` 384 | pub trait ReadEntry: PrivReadEntry + Sized { 385 | /// Attempt to read the next entry in the multipart stream. 386 | fn read_entry(mut self) -> ReadEntryResult { 387 | self.set_min_buf_size(super::boundary::MIN_BUF_SIZE); 388 | 389 | debug!("ReadEntry::read_entry()"); 390 | 391 | if !try_read_entry!(self; self.consume_boundary()) { 392 | return End(self); 393 | } 394 | 395 | let field_headers: FieldHeaders = try_read_entry!(self; self.read_headers()); 396 | 397 | if let Some(ct) = field_headers.content_type.as_ref() { 398 | if ct.type_() == mime::MULTIPART { 399 | // fields of this type are sent by (supposedly) no known clients 400 | // (https://tools.ietf.org/html/rfc7578#appendix-A) so I'd be fascinated 401 | // to hear about any in the wild 402 | info!( 403 | "Found nested multipart field: {:?}:\r\n\ 404 | Please report this client's User-Agent and any other available details \ 405 | at https://github.com/abonander/multipart/issues/56", 406 | field_headers 407 | ); 408 | } 409 | } 410 | 411 | Entry(MultipartField { 412 | headers: field_headers, 413 | data: MultipartData { inner: Some(self) }, 414 | }) 415 | } 416 | 417 | /// Equivalent to `read_entry()` but takes `&mut self` 418 | fn read_entry_mut(&mut self) -> ReadEntryResult<&mut Self> { 419 | ReadEntry::read_entry(self) 420 | } 421 | } 422 | 423 | impl ReadEntry for T where T: PrivReadEntry {} 424 | 425 | /// Public trait but not re-exported. 426 | pub trait PrivReadEntry { 427 | type Source: BufRead; 428 | 429 | fn source_mut(&mut self) -> &mut Self::Source; 430 | 431 | fn set_min_buf_size(&mut self, min_buf_size: usize); 432 | 433 | /// Consume the next boundary. 434 | /// Returns `true` if a field should follow, `false` otherwise. 435 | fn consume_boundary(&mut self) -> io::Result; 436 | 437 | fn read_headers(&mut self) -> Result { 438 | FieldHeaders::read_from(self.source_mut()) 439 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) 440 | } 441 | 442 | fn read_to_string(&mut self) -> io::Result { 443 | let mut buf = String::new(); 444 | 445 | match self.source_mut().read_to_string(&mut buf) { 446 | Ok(_) => Ok(buf), 447 | Err(err) => Err(err), 448 | } 449 | } 450 | } 451 | 452 | impl<'a, M: ReadEntry> PrivReadEntry for &'a mut M { 453 | type Source = M::Source; 454 | 455 | fn source_mut(&mut self) -> &mut M::Source { 456 | (**self).source_mut() 457 | } 458 | 459 | fn set_min_buf_size(&mut self, min_buf_size: usize) { 460 | (**self).set_min_buf_size(min_buf_size) 461 | } 462 | 463 | fn consume_boundary(&mut self) -> io::Result { 464 | (**self).consume_boundary() 465 | } 466 | } 467 | 468 | /// Ternary result type returned by `ReadEntry::next_entry()`, 469 | /// `Multipart::into_entry()` and `MultipartField::next_entry()`. 470 | pub enum ReadEntryResult> { 471 | /// The next entry was found. 472 | Entry(Entry), 473 | /// No more entries could be read. 474 | End(M), 475 | /// An error occurred. 476 | Error(M, io::Error), 477 | } 478 | 479 | impl ReadEntryResult { 480 | /// Convert `self` into `Result>` as follows: 481 | /// 482 | /// * `Entry(entry) -> Ok(Some(entry))` 483 | /// * `End(_) -> Ok(None)` 484 | /// * `Error(_, err) -> Err(err)` 485 | pub fn into_result(self) -> io::Result> { 486 | match self { 487 | ReadEntryResult::Entry(entry) => Ok(Some(entry)), 488 | ReadEntryResult::End(_) => Ok(None), 489 | ReadEntryResult::Error(_, err) => Err(err), 490 | } 491 | } 492 | 493 | /// Attempt to unwrap `Entry`, panicking if this is `End` or `Error`. 494 | pub fn unwrap(self) -> Entry { 495 | self.expect_alt( 496 | "`ReadEntryResult::unwrap()` called on `End` value", 497 | "`ReadEntryResult::unwrap()` called on `Error` value: {:?}", 498 | ) 499 | } 500 | 501 | /// Attempt to unwrap `Entry`, panicking if this is `End` or `Error` 502 | /// with the given message. Adds the error's message in the `Error` case. 503 | pub fn expect(self, msg: &str) -> Entry { 504 | self.expect_alt(msg, msg) 505 | } 506 | 507 | /// Attempt to unwrap `Entry`, panicking if this is `End` or `Error`. 508 | /// If this is `End`, panics with `end_msg`; if `Error`, panics with `err_msg` 509 | /// as well as the error's message. 510 | pub fn expect_alt(self, end_msg: &str, err_msg: &str) -> Entry { 511 | match self { 512 | Entry(entry) => entry, 513 | End(_) => panic!("{}", end_msg), 514 | Error(_, err) => panic!("{}: {:?}", err_msg, err), 515 | } 516 | } 517 | 518 | /// Attempt to unwrap as `Option`, panicking in the `Error` case. 519 | pub fn unwrap_opt(self) -> Option { 520 | self.expect_opt("`ReadEntryResult::unwrap_opt()` called on `Error` value") 521 | } 522 | 523 | /// Attempt to unwrap as `Option`, panicking in the `Error` case 524 | /// with the given message as well as the error's message. 525 | pub fn expect_opt(self, msg: &str) -> Option { 526 | match self { 527 | Entry(entry) => Some(entry), 528 | End(_) => None, 529 | Error(_, err) => panic!("{}: {:?}", msg, err), 530 | } 531 | } 532 | } 533 | 534 | const GENERIC_PARSE_ERR: &str = "an error occurred while parsing field headers"; 535 | 536 | quick_error! { 537 | #[derive(Debug)] 538 | enum ParseHeaderError { 539 | /// The `Content-Disposition` header was not found 540 | MissingContentDisposition(headers: String) { 541 | display(x) -> ("{}:\n{}", x.description(), headers) 542 | description("\"Content-Disposition\" header not found in field headers") 543 | } 544 | InvalidContDisp(reason: &'static str, cause: String) { 545 | display(x) -> ("{}: {}: {}", x.description(), reason, cause) 546 | description("invalid \"Content-Disposition\" header") 547 | } 548 | /// The header was found but could not be parsed 549 | TokenizeError(err: HttparseError) { 550 | description(GENERIC_PARSE_ERR) 551 | display(x) -> ("{}: {}", x.description(), err) 552 | cause(err) 553 | from() 554 | } 555 | MimeError(cont_type: String) { 556 | description("Failed to parse Content-Type") 557 | display(this) -> ("{}: {}", this.description(), cont_type) 558 | } 559 | TooLarge { 560 | description("field headers section ridiculously long or missing trailing CRLF-CRLF") 561 | } 562 | /// IO error 563 | Io(err: io::Error) { 564 | description("an io error occurred while parsing the headers") 565 | display(x) -> ("{}: {}", x.description(), err) 566 | cause(err) 567 | from() 568 | } 569 | } 570 | } 571 | 572 | #[test] 573 | fn test_find_header() { 574 | let headers = [ 575 | StrHeader { 576 | name: "Content-Type", 577 | val: "text/plain", 578 | }, 579 | StrHeader { 580 | name: "Content-disposition", 581 | val: "form-data", 582 | }, 583 | StrHeader { 584 | name: "content-transfer-encoding", 585 | val: "binary", 586 | }, 587 | ]; 588 | 589 | assert_eq!( 590 | find_header(&headers, "Content-Type").unwrap().val, 591 | "text/plain" 592 | ); 593 | assert_eq!( 594 | find_header(&headers, "Content-Disposition").unwrap().val, 595 | "form-data" 596 | ); 597 | assert_eq!( 598 | find_header(&headers, "Content-Transfer-Encoding") 599 | .unwrap() 600 | .val, 601 | "binary" 602 | ); 603 | } 604 | -------------------------------------------------------------------------------- /src/server/hyper.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 `multipart` Crate Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | //! Server-side integration with [Hyper](https://github.com/hyperium/hyper). 8 | //! Enabled with the `hyper` feature (on by default). 9 | //! 10 | //! Also contains an implementation of [`HttpRequest`](../trait.HttpRequest.html) 11 | //! for `hyper::server::Request` and `&mut hyper::server::Request`. 12 | use hyper::net::Fresh; 13 | use hyper::header::ContentType; 14 | use hyper::method::Method; 15 | use hyper::server::{Handler, Request, Response}; 16 | 17 | pub use hyper::server::Request as HyperRequest; 18 | 19 | use hyper::mime::{Mime, TopLevel, SubLevel, Attr, Value}; 20 | 21 | use super::{Multipart, HttpRequest}; 22 | 23 | /// A container that implements `hyper::server::Handler` which will switch 24 | /// the handler implementation depending on if the incoming request is multipart or not. 25 | /// 26 | /// Create an instance with `new()` and pass it to `hyper::server::Server::listen()` where 27 | /// you would normally pass a `Handler` instance. 28 | /// 29 | /// A convenient wrapper for `Multipart::from_request()`. 30 | pub struct Switch { 31 | normal: H, 32 | multipart: M, 33 | } 34 | 35 | impl Switch where H: Handler, M: MultipartHandler { 36 | /// Create a new `Switch` instance where 37 | /// `normal` handles normal Hyper requests and `multipart` handles Multipart requests 38 | pub fn new(normal: H, multipart: M) -> Switch { 39 | Switch { normal, multipart } 40 | } 41 | } 42 | 43 | impl Handler for Switch where H: Handler, M: MultipartHandler { 44 | fn handle<'a, 'k>(&'a self, req: Request<'a, 'k>, res: Response<'a, Fresh>) { 45 | match Multipart::from_request(req) { 46 | Ok(multi) => self.multipart.handle_multipart(multi, res), 47 | Err(req) => self.normal.handle(req, res), 48 | } 49 | } 50 | } 51 | 52 | /// A trait defining a type that can handle an incoming multipart request. 53 | /// 54 | /// Extends to closures of the type `Fn(Multipart, Response)`, 55 | /// and subsequently static functions. 56 | pub trait MultipartHandler: Send + Sync { 57 | /// Generate a response from this multipart request. 58 | fn handle_multipart<'a, 'k>(&self, 59 | multipart: Multipart>, 60 | response: Response<'a, Fresh>); 61 | } 62 | 63 | impl MultipartHandler for F 64 | where F: Fn(Multipart, Response), F: Send + Sync { 65 | fn handle_multipart<'a, 'k>(&self, 66 | multipart: Multipart>, 67 | response: Response<'a, Fresh>) { 68 | (*self)(multipart, response); 69 | } 70 | } 71 | 72 | impl<'a, 'b> HttpRequest for HyperRequest<'a, 'b> { 73 | type Body = Self; 74 | 75 | fn multipart_boundary(&self) -> Option<&str> { 76 | if self.method != Method::Post { 77 | return None; 78 | } 79 | 80 | self.headers.get::().and_then(|ct| { 81 | let ContentType(ref mime) = *ct; 82 | let params = match *mime { 83 | Mime(TopLevel::Multipart, SubLevel::FormData, ref params) => params, 84 | _ => return None, 85 | }; 86 | 87 | params.iter().find(|&&(ref name, _)| 88 | match *name { 89 | Attr::Boundary => true, 90 | _ => false, 91 | } 92 | ).and_then(|&(_, ref val)| 93 | match *val { 94 | Value::Ext(ref val) => Some(&**val), 95 | _ => None, 96 | } 97 | ) 98 | }) 99 | } 100 | 101 | fn body(self) -> Self { 102 | self 103 | } 104 | } 105 | 106 | impl<'r, 'a, 'b> HttpRequest for &'r mut HyperRequest<'a, 'b> { 107 | type Body = Self; 108 | 109 | fn multipart_boundary(&self) -> Option<&str> { 110 | if self.method != Method::Post { 111 | return None; 112 | } 113 | 114 | self.headers.get::().and_then(|ct| { 115 | let ContentType(ref mime) = *ct; 116 | let params = match *mime { 117 | Mime(TopLevel::Multipart, SubLevel::FormData, ref params) => params, 118 | _ => return None, 119 | }; 120 | 121 | params.iter().find(|&&(ref name, _)| 122 | match *name { 123 | Attr::Boundary => true, 124 | _ => false, 125 | } 126 | ).and_then(|&(_, ref val)| 127 | match *val { 128 | Value::Ext(ref val) => Some(&**val), 129 | _ => None, 130 | } 131 | ) 132 | }) 133 | } 134 | 135 | fn body(self) -> Self::Body { 136 | self 137 | } 138 | } 139 | 140 | -------------------------------------------------------------------------------- /src/server/iron.rs: -------------------------------------------------------------------------------- 1 | //! Integration with the [Iron](https://github.com/iron/iron) framework, enabled with the `iron` feature (optional). Includes a `BeforeMiddleware` implementation. 2 | //! 3 | //! Not shown here: `impl `[`HttpRequest`](../trait.HttpRequest.html#implementors)` for 4 | //! iron::Request`. 5 | 6 | use iron::headers::ContentType; 7 | use iron::mime::{Mime, TopLevel, SubLevel}; 8 | use iron::request::{Body as IronBody, Request as IronRequest}; 9 | use iron::typemap::Key; 10 | use iron::{BeforeMiddleware, IronError, IronResult}; 11 | 12 | use std::path::PathBuf; 13 | use std::{error, fmt, io}; 14 | use tempfile; 15 | 16 | use super::{FieldHeaders, HttpRequest, Multipart}; 17 | use super::save::{Entries, PartialReason, TempDir}; 18 | use super::save::SaveResult::*; 19 | 20 | impl<'r, 'a, 'b> HttpRequest for &'r mut IronRequest<'a, 'b> { 21 | type Body = &'r mut IronBody<'a, 'b>; 22 | 23 | fn multipart_boundary(&self) -> Option<&str> { 24 | let content_type = try_opt!(self.headers.get::()); 25 | if let Mime(TopLevel::Multipart, SubLevel::FormData, _) = **content_type { 26 | content_type.get_param("boundary").map(|b| b.as_str()) 27 | } else { 28 | None 29 | } 30 | } 31 | 32 | fn body(self) -> &'r mut IronBody<'a, 'b> { 33 | &mut self.body 34 | } 35 | } 36 | 37 | /// The default file size limit for [`Intercept`](struct.Intercept.html), in bytes. 38 | pub const DEFAULT_FILE_SIZE_LIMIT: u64 = 2 * 1024 * 1024; 39 | 40 | /// The default file count limit for [`Intercept`](struct.Intercept.html). 41 | pub const DEFAULT_FILE_COUNT_LIMIT: u32 = 16; 42 | 43 | /// A `BeforeMiddleware` for Iron which will intercept and read-out multipart requests and store 44 | /// the result in the request. 45 | /// 46 | /// Successful reads will be placed in the `extensions: TypeMap` field of `iron::Request` as an 47 | /// [`Entries`](../struct.Entries.html) instance (as both key-type and value): 48 | /// 49 | /// ```no_run 50 | /// extern crate iron; 51 | /// extern crate multipart; 52 | /// 53 | /// use iron::prelude::*; 54 | /// 55 | /// use multipart::server::Entries; 56 | /// use multipart::server::iron::Intercept; 57 | /// 58 | /// fn main() { 59 | /// let mut chain = Chain::new(|req: &mut Request| if let Some(entries) = 60 | /// req.extensions.get::() { 61 | /// 62 | /// Ok(Response::with(format!("{:?}", entries))) 63 | /// } else { 64 | /// Ok(Response::with("Not a multipart request")) 65 | /// }); 66 | /// 67 | /// chain.link_before(Intercept::default()); 68 | /// 69 | /// Iron::new(chain).http("localhost:80").unwrap(); 70 | /// } 71 | /// ``` 72 | /// 73 | /// Any errors during which occur during reading will be passed on as `IronError`. 74 | #[derive(Debug)] 75 | pub struct Intercept { 76 | /// The parent directory for all temporary directories created by this middleware. 77 | /// Will be created if it doesn't exist (lazy). 78 | /// 79 | /// If omitted, uses the OS temporary directory. 80 | /// 81 | /// Default value: `None`. 82 | pub temp_dir_path: Option, 83 | /// The size limit of uploaded files, in bytes. 84 | /// 85 | /// Files which exceed this size will be rejected. 86 | /// See the `limit_behavior` field for more info. 87 | /// 88 | /// Default value: [`DEFAULT_FILE_SIZE_LIMIT`](constant.default_file_size_limit.html) 89 | pub file_size_limit: u64, 90 | /// The limit on the number of files which will be saved from 91 | /// the request. Requests which exceed this count will be rejected. 92 | /// 93 | /// Default value: [`DEFAULT_FILE_COUNT_LIMT`](constant.default_file_count_limit.html) 94 | pub file_count_limit: u32, 95 | /// What to do when a file count or size limit has been exceeded. 96 | /// 97 | /// See [`LimitBehavior`](enum.limitbehavior.html) for more info. 98 | pub limit_behavior: LimitBehavior, 99 | } 100 | 101 | impl Intercept { 102 | /// Set the `temp_dir_path` for this middleware. 103 | pub fn temp_dir_path>(self, path: P) -> Self { 104 | Intercept { temp_dir_path: Some(path.into()), .. self } 105 | } 106 | 107 | /// Set the `file_size_limit` for this middleware. 108 | pub fn file_size_limit(self, limit: u64) -> Self { 109 | Intercept { file_size_limit: limit, .. self } 110 | } 111 | 112 | /// Set the `file_count_limit` for this middleware. 113 | pub fn file_count_limit(self, limit: u32) -> Self { 114 | Intercept { file_count_limit: limit, .. self } 115 | } 116 | 117 | /// Set the `limit_behavior` for this middleware. 118 | pub fn limit_behavior(self, behavior: LimitBehavior) -> Self { 119 | Intercept { limit_behavior: behavior, .. self } 120 | } 121 | 122 | fn read_request(&self, req: &mut IronRequest) -> IronResult> { 123 | let multipart = match Multipart::from_request(req) { 124 | Ok(multipart) => multipart, 125 | Err(_) => return Ok(None), 126 | }; 127 | 128 | let tempdir = self.temp_dir_path.as_ref() 129 | .map_or_else( 130 | || tempfile::Builder::new().prefix("multipart-iron").tempdir(), 131 | |path| tempfile::Builder::new().prefix("multipart-iron").tempdir_in(path) 132 | ) 133 | .map_err(|e| io_to_iron(e, "Error opening temporary directory for request."))?; 134 | 135 | match self.limit_behavior { 136 | LimitBehavior::ThrowError => self.read_request_strict(multipart, tempdir), 137 | LimitBehavior::Continue => self.read_request_lenient(multipart, tempdir), 138 | } 139 | } 140 | 141 | fn read_request_strict(&self, mut multipart: IronMultipart, tempdir: TempDir) -> IronResult> { 142 | match multipart.save().size_limit(self.file_size_limit) 143 | .count_limit(self.file_count_limit) 144 | .with_temp_dir(tempdir) { 145 | Full(entries) => Ok(Some(entries)), 146 | Partial(_, PartialReason::Utf8Error(_)) => unreachable!(), 147 | Partial(_, PartialReason::IoError(err)) => Err(io_to_iron(err, "Error midway through request")), 148 | Partial(_, PartialReason::CountLimit) => Err(FileCountLimitError(self.file_count_limit).into()), 149 | Partial(partial, PartialReason::SizeLimit) => { 150 | let partial = partial.partial.expect(EXPECT_PARTIAL_FILE); 151 | Err( 152 | FileSizeLimitError { 153 | field: partial.source.headers, 154 | }.into() 155 | ) 156 | }, 157 | Error(err) => Err(io_to_iron(err, "Error at start of request")), 158 | } 159 | } 160 | 161 | fn read_request_lenient(&self, mut multipart: IronMultipart, tempdir: TempDir) -> IronResult> { 162 | let mut entries = match multipart.save().size_limit(self.file_size_limit) 163 | .count_limit(self.file_count_limit) 164 | .with_temp_dir(tempdir) { 165 | Full(entries) => return Ok(Some(entries)), 166 | Partial(_, PartialReason::IoError(err)) => return Err(io_to_iron(err, "Error midway through request")), 167 | Partial(partial, _) => partial.keep_partial(), 168 | Error(err) => return Err(io_to_iron(err, "Error at start of request")), 169 | }; 170 | 171 | loop { 172 | entries = match multipart.save().size_limit(self.file_size_limit) 173 | .count_limit(self.file_count_limit) 174 | .with_entries(entries) { 175 | Full(entries) => return Ok(Some(entries)), 176 | Partial(_, PartialReason::IoError(err)) => return Err(io_to_iron(err, "Error midway through request")), 177 | Partial(partial, _) => partial.keep_partial(), 178 | Error(err) => return Err(io_to_iron(err, "Error at start of request")), 179 | }; 180 | } 181 | } 182 | } 183 | 184 | type IronMultipart<'r, 'a, 'b> = Multipart<&'r mut IronBody<'a, 'b>>; 185 | 186 | const EXPECT_PARTIAL_FILE: &str = "File size limit hit but the offending \ 187 | file was not available; this is a bug."; 188 | 189 | impl Default for Intercept { 190 | fn default() -> Self { 191 | Intercept { 192 | temp_dir_path: None, 193 | file_size_limit: DEFAULT_FILE_SIZE_LIMIT, 194 | file_count_limit: DEFAULT_FILE_COUNT_LIMIT, 195 | limit_behavior: LimitBehavior::ThrowError, 196 | } 197 | } 198 | } 199 | 200 | impl BeforeMiddleware for Intercept { 201 | fn before(&self, req: &mut IronRequest) -> IronResult<()> { 202 | self.read_request(req)? 203 | .map(|entries| req.extensions.insert::(entries)); 204 | 205 | Ok(()) 206 | } 207 | } 208 | 209 | impl Key for Entries { 210 | type Value = Self; 211 | } 212 | 213 | /// The behavior of `Intercept` when a file size or count limit is exceeded. 214 | #[derive(Clone, Copy, Debug)] 215 | #[repr(u32)] 216 | pub enum LimitBehavior { 217 | /// Return an error from the middleware describing the issue. 218 | ThrowError, 219 | /// Ignore the limit. 220 | /// 221 | /// In the case of file size limits, the offending file will be truncated 222 | /// in the result. 223 | /// 224 | /// In the case of file count limits, the request will be completed. 225 | Continue, 226 | } 227 | 228 | /// An error returned from `Intercept` when the size limit 229 | /// for an individual file is exceeded. 230 | #[derive(Debug)] 231 | pub struct FileSizeLimitError { 232 | /// The field where the error occurred. 233 | pub field: FieldHeaders, 234 | } 235 | 236 | impl error::Error for FileSizeLimitError { 237 | fn description(&self) -> &str { 238 | "file size limit reached" 239 | } 240 | } 241 | 242 | impl fmt::Display for FileSizeLimitError { 243 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 244 | match self.field.filename { 245 | Some(ref filename) => write!(f, "File size limit reached for field \"{}\" (filename: \"{}\")", self.field.name, filename), 246 | None => write!(f, "File size limit reached for field \"{}\" (no filename)", self.field.name), 247 | } 248 | } 249 | } 250 | 251 | impl Into for FileSizeLimitError { 252 | fn into(self) -> IronError { 253 | let desc_str = self.to_string(); 254 | IronError::new(self, desc_str) 255 | } 256 | } 257 | 258 | /// An error returned from `Intercept` when the file count limit 259 | /// for a single request was exceeded. 260 | #[derive(Debug)] 261 | pub struct FileCountLimitError(u32); 262 | 263 | impl error::Error for FileCountLimitError { 264 | fn description(&self) -> &str { 265 | "file count limit reached" 266 | } 267 | } 268 | 269 | impl fmt::Display for FileCountLimitError { 270 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 271 | write!(f, "File count limit reached for request. Limit: {}", self.0) 272 | } 273 | } 274 | 275 | impl Into for FileCountLimitError { 276 | fn into(self) -> IronError { 277 | let desc_string = self.to_string(); 278 | IronError::new(self, desc_string) 279 | } 280 | } 281 | 282 | fn io_to_iron>(err: io::Error, msg: M) -> IronError { 283 | IronError::new(err, msg.into()) 284 | } 285 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 `multipart` Crate Developers 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | //! The server-side abstraction for multipart requests. Enabled with the `server` feature. 8 | //! 9 | //! Use this when you are implementing an HTTP server and want to 10 | //! to accept, parse, and serve HTTP `multipart/form-data` requests (file uploads). 11 | //! 12 | //! See the `Multipart` struct for more info. 13 | 14 | pub extern crate buf_redux; 15 | extern crate httparse; 16 | extern crate twoway; 17 | 18 | use std::borrow::Borrow; 19 | use std::io::prelude::*; 20 | use std::io; 21 | 22 | use self::boundary::BoundaryReader; 23 | 24 | use self::field::PrivReadEntry; 25 | 26 | pub use self::field::{FieldHeaders, MultipartField, MultipartData, ReadEntry, ReadEntryResult}; 27 | 28 | use self::save::SaveBuilder; 29 | 30 | pub use self::save::{Entries, SaveResult, SavedField}; 31 | 32 | macro_rules! try_opt ( 33 | ($expr:expr) => ( 34 | match $expr { 35 | Some(val) => val, 36 | None => return None, 37 | } 38 | ); 39 | ($expr:expr, $before_ret:expr) => ( 40 | match $expr { 41 | Some(val) => val, 42 | None => { 43 | $before_ret; 44 | return None; 45 | } 46 | } 47 | ) 48 | ); 49 | 50 | macro_rules! try_read_entry { 51 | ($self_:expr; $try:expr) => ( 52 | match $try { 53 | Ok(res) => res, 54 | Err(err) => return ::server::ReadEntryResult::Error($self_, err), 55 | } 56 | ) 57 | } 58 | 59 | mod boundary; 60 | mod field; 61 | 62 | #[cfg(feature = "hyper")] 63 | pub mod hyper; 64 | 65 | #[cfg(feature = "iron")] 66 | pub mod iron; 67 | 68 | #[cfg(feature = "tiny_http")] 69 | pub mod tiny_http; 70 | 71 | #[cfg(feature = "nickel")] 72 | pub mod nickel; 73 | 74 | pub mod save; 75 | 76 | /// The server-side implementation of `multipart/form-data` requests. 77 | /// 78 | /// Implements `Borrow` to allow access to the request body, if desired. 79 | pub struct Multipart { 80 | reader: BoundaryReader, 81 | } 82 | 83 | impl Multipart<()> { 84 | /// If the given `HttpRequest` is a multipart/form-data POST request, 85 | /// return the request body wrapped in the multipart reader. Otherwise, 86 | /// returns the original request. 87 | pub fn from_request(req: R) -> Result, R> { 88 | //FIXME: move `map` expr to `Some` arm when nonlexical borrow scopes land. 89 | let boundary = match req.multipart_boundary().map(String::from) { 90 | Some(boundary) => boundary, 91 | None => return Err(req), 92 | }; 93 | 94 | Ok(Multipart::with_body(req.body(), boundary)) 95 | } 96 | } 97 | 98 | impl Multipart { 99 | /// Construct a new `Multipart` with the given body reader and boundary. 100 | /// 101 | /// ## Note: `boundary` 102 | /// This will prepend the requisite `--` to the boundary string as documented in 103 | /// [IETF RFC 1341, Section 7.2.1: "Multipart: the common syntax"][rfc1341-7.2.1]. 104 | /// Simply pass the value of the `boundary` key from the `Content-Type` header in the 105 | /// request (or use `Multipart::from_request()`, if supported). 106 | /// 107 | /// [rfc1341-7.2.1]: https://tools.ietf.org/html/rfc1341#page-30 108 | pub fn with_body>(body: R, boundary: Bnd) -> Self { 109 | let boundary = boundary.into(); 110 | 111 | info!("Multipart::with_boundary(_, {:?})", boundary); 112 | 113 | Multipart { 114 | reader: BoundaryReader::from_reader(body, boundary), 115 | } 116 | } 117 | 118 | /// Read the next entry from this multipart request, returning a struct with the field's name and 119 | /// data. See `MultipartField` for more info. 120 | /// 121 | /// ## Warning: Risk of Data Loss 122 | /// If the previously returned entry had contents of type `MultipartField::File`, 123 | /// calling this again will discard any unread contents of that entry. 124 | pub fn read_entry(&mut self) -> io::Result>> { 125 | self.read_entry_mut().into_result() 126 | } 127 | 128 | /// Read the next entry from this multipart request, returning a struct with the field's name and 129 | /// data. See `MultipartField` for more info. 130 | pub fn into_entry(self) -> ReadEntryResult { 131 | self.read_entry() 132 | } 133 | 134 | /// Call `f` for each entry in the multipart request. 135 | /// 136 | /// This is a substitute for Rust not supporting streaming iterators (where the return value 137 | /// from `next()` borrows the iterator for a bound lifetime). 138 | /// 139 | /// Returns `Ok(())` when all fields have been read, or the first error. 140 | pub fn foreach_entry(&mut self, mut foreach: F) -> io::Result<()> where F: FnMut(MultipartField<&mut Self>) { 141 | loop { 142 | match self.read_entry() { 143 | Ok(Some(field)) => foreach(field), 144 | Ok(None) => return Ok(()), 145 | Err(err) => return Err(err), 146 | } 147 | } 148 | } 149 | 150 | /// Get a builder type for saving the files in this request to the filesystem. 151 | /// 152 | /// See [`SaveBuilder`](save/struct.SaveBuilder.html) for more information. 153 | pub fn save(&mut self) -> SaveBuilder<&mut Self> { 154 | SaveBuilder::new(self) 155 | } 156 | } 157 | 158 | impl Borrow for Multipart { 159 | fn borrow(&self) -> &R { 160 | self.reader.borrow() 161 | } 162 | } 163 | 164 | impl PrivReadEntry for Multipart { 165 | type Source = BoundaryReader; 166 | 167 | fn source_mut(&mut self) -> &mut BoundaryReader { 168 | &mut self.reader 169 | } 170 | 171 | fn set_min_buf_size(&mut self, min_buf_size: usize) { 172 | self.reader.set_min_buf_size(min_buf_size) 173 | } 174 | 175 | /// Consume the next boundary. 176 | /// Returns `true` if a field should follow this boundary, `false` otherwise. 177 | fn consume_boundary(&mut self) -> io::Result { 178 | debug!("Consume boundary!"); 179 | self.reader.consume_boundary() 180 | } 181 | } 182 | 183 | /// A server-side HTTP request that may or may not be multipart. 184 | /// 185 | /// May be implemented by mutable references if providing the request or body by-value is 186 | /// undesirable. 187 | pub trait HttpRequest { 188 | /// The body of this request. 189 | type Body: Read; 190 | /// Get the boundary string of this request if it is a POST request 191 | /// with the `Content-Type` header set to `multipart/form-data`. 192 | /// 193 | /// The boundary string should be supplied as an extra value of the `Content-Type` header, e.g. 194 | /// `Content-Type: multipart/form-data; boundary={boundary}`. 195 | fn multipart_boundary(&self) -> Option<&str>; 196 | 197 | /// Return the request body for reading. 198 | fn body(self) -> Self::Body; 199 | } 200 | 201 | #[test] 202 | fn issue_104() { 203 | ::init_log(); 204 | 205 | use std::io::Cursor; 206 | 207 | let body = "\ 208 | POST /test.html HTTP/1.1\r\n\ 209 | Host: example.org\r\n\ 210 | Content-Type: multipart/form-data;boundary=\"boundary\"\r\n\r\n\ 211 | Content-Disposition: form-data; name=\"field1\"\r\n\r\n\ 212 | value1\r\n\ 213 | Content-Disposition: form-data; name=\"field2\"; filename=\"example.txt\"\r\n\r\n\ 214 | value2 "; 215 | 216 | let request = Cursor::new(body); 217 | 218 | let mut multipart = Multipart::with_body(request, "boundary"); 219 | multipart.foreach_entry(|_field| {/* Do nothing */}).unwrap_err(); 220 | } 221 | 222 | #[test] 223 | fn issue_114() { 224 | ::init_log(); 225 | 226 | fn consume_all(mut rdr: R) { 227 | loop { 228 | let consume = rdr.fill_buf().unwrap().len(); 229 | if consume == 0 { return; } 230 | rdr.consume(consume); 231 | } 232 | } 233 | 234 | use std::io::Cursor; 235 | 236 | let body = "\ 237 | --------------------------c616e5fded96a3c7\r\n\ 238 | Content-Disposition: form-data; name=\"key1\"\r\n\r\n\ 239 | v1,\r\n\ 240 | --------------------------c616e5fded96a3c7\r\n\ 241 | Content-Disposition: form-data; name=\"key2\"\r\n\r\n\ 242 | v2,\r\n\ 243 | --------------------------c616e5fded96a3c7\r\n\ 244 | Content-Disposition: form-data; name=\"key3\"\r\n\r\n\ 245 | v3\r\n\ 246 | --------------------------c616e5fded96a3c7--\r\n"; 247 | 248 | let request = Cursor::new(body); 249 | let mut multipart = Multipart::with_body(request, "------------------------c616e5fded96a3c7"); 250 | 251 | // one error if you do nothing 252 | multipart.foreach_entry(|_entry| { /* do nothing */}).unwrap(); 253 | 254 | // a different error if you skip the first field 255 | multipart.foreach_entry(|entry| if *entry.headers.name != *"key1" { consume_all(entry.data); }) 256 | .unwrap(); 257 | 258 | 259 | multipart.foreach_entry(|_entry| () /* match entry.headers.name.as_str() { 260 | "file" => { 261 | let mut vec = Vec::new(); 262 | entry.data.read_to_end(&mut vec).expect("can't read"); 263 | // message.file = String::from_utf8(vec).ok(); 264 | println!("key file got"); 265 | } 266 | 267 | "key1" => { 268 | let mut vec = Vec::new(); 269 | entry.data.read_to_end(&mut vec).expect("can't read"); 270 | // message.key1 = String::from_utf8(vec).ok(); 271 | println!("key1 got"); 272 | } 273 | 274 | "key2" => { 275 | let mut vec = Vec::new(); 276 | entry.data.read_to_end(&mut vec).expect("can't read"); 277 | // message.key2 = String::from_utf8(vec).ok(); 278 | println!("key2 got"); 279 | } 280 | 281 | _ => { 282 | // as multipart has a bug https://github.com/abonander/multipart/issues/114 283 | // we manually do read_to_end here 284 | //let mut _vec = Vec::new(); 285 | //entry.data.read_to_end(&mut _vec).expect("can't read"); 286 | println!("key neglected"); 287 | } 288 | }*/) 289 | .expect("Unable to iterate multipart?") 290 | } 291 | -------------------------------------------------------------------------------- /src/server/nickel.rs: -------------------------------------------------------------------------------- 1 | //! Support for `multipart/form-data` bodies in [Nickel](https://nickel.rs). 2 | pub extern crate nickel; 3 | 4 | use self::nickel::hyper; 5 | use self::hyper::header::ContentType; 6 | 7 | pub use self::nickel::Request as NickelRequest; 8 | pub use self::nickel::hyper::server::Request as HyperRequest; 9 | 10 | use server::{HttpRequest, Multipart}; 11 | 12 | /// A wrapper for `&mut nickel::Request` which implements `multipart::server::HttpRequest`. 13 | /// 14 | /// Necessary because this crate cannot directly provide an impl of `HttpRequest` for 15 | /// `&mut NickelRequest`. 16 | pub struct Maybe<'r, 'mw: 'r, 'server: 'mw, D: 'mw>(pub &'r mut NickelRequest<'mw, 'server, D>); 17 | 18 | impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> HttpRequest for Maybe<'r, 'mw, 'server, D> { 19 | type Body = &'r mut HyperRequest<'mw, 'server>; 20 | 21 | fn multipart_boundary(&self) -> Option<&str> { 22 | // we can't use the impl from the `hyper` module because it might be the wrong version 23 | let cont_type = try_opt!(self.0.origin.headers.get::()); 24 | cont_type.get_param("boundary").map(|v| v.as_str()) 25 | } 26 | 27 | fn body(self) -> Self::Body { 28 | &mut self.0.origin 29 | } 30 | } 31 | 32 | /// Extension trait for getting the `multipart/form-data` body from `nickel::Request`. 33 | /// 34 | /// Implemented for `nickel::Request`. 35 | pub trait MultipartBody<'mw, 'server> { 36 | /// Get a multipart reader for the request body, if the request is of the right type. 37 | fn multipart_body(&mut self) -> Option>>; 38 | } 39 | 40 | impl<'mw, 'server, D: 'mw> MultipartBody<'mw, 'server> for NickelRequest<'mw, 'server, D> { 41 | fn multipart_body(&mut self) -> Option>> { 42 | Multipart::from_request(Maybe(self)).ok() 43 | } 44 | } 45 | 46 | impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> AsRef<&'r mut NickelRequest<'mw, 'server, D>> for Maybe<'r, 'mw, 'server, D> { 47 | fn as_ref(&self) -> &&'r mut NickelRequest<'mw, 'server, D> { 48 | &self.0 49 | } 50 | } 51 | 52 | impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> AsMut<&'r mut NickelRequest<'mw, 'server, D>> for Maybe<'r, 'mw, 'server, D> { 53 | fn as_mut(&mut self) -> &mut &'r mut NickelRequest<'mw, 'server, D> { 54 | &mut self.0 55 | } 56 | } 57 | 58 | impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> Into<&'r mut NickelRequest<'mw, 'server, D>> for Maybe<'r, 'mw, 'server, D> { 59 | fn into(self) -> &'r mut NickelRequest<'mw, 'server, D> { 60 | self.0 61 | } 62 | } 63 | 64 | impl<'r, 'mw: 'r, 'server: 'mw, D: 'mw> From<&'r mut NickelRequest<'mw, 'server, D>> for Maybe<'r, 'mw, 'server, D> { 65 | fn from(req: &'r mut NickelRequest<'mw, 'server, D>) -> Self { 66 | Maybe(req) 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/server/tiny_http.rs: -------------------------------------------------------------------------------- 1 | //! Integration with [`tiny_http`](https://github.com/frewsxcv/tiny-http) with the `tiny_http` 2 | //! feature (optional). 3 | //! 4 | //! Contains `impl `[`HttpRequest`](../trait.HttpRequest.html)` for tiny_http::Request` (not shown 5 | //! here; see [`HttpRequest`'s implementors](../trait.HttpRequest.html#implementors)). 6 | 7 | pub use tiny_http::Request as TinyHttpRequest; 8 | 9 | use super::HttpRequest; 10 | 11 | use std::io::Read; 12 | 13 | impl<'r> HttpRequest for &'r mut TinyHttpRequest { 14 | type Body = &'r mut dyn Read; 15 | 16 | fn multipart_boundary(&self) -> Option<&str> { 17 | const BOUNDARY: &str = "boundary="; 18 | 19 | let content_type = try_opt!(self 20 | .headers() 21 | .iter() 22 | .find(|header| header.field.equiv("Content-Type"))) 23 | .value 24 | .as_str(); 25 | let start = try_opt!(content_type.find(BOUNDARY)) + BOUNDARY.len(); 26 | let end = content_type[start..] 27 | .find(';') 28 | .map_or(content_type.len(), |end| start + end); 29 | 30 | Some(&content_type[start..end]) 31 | } 32 | 33 | fn body(self) -> Self::Body { 34 | self.as_reader() 35 | } 36 | } 37 | --------------------------------------------------------------------------------