wreq-rb 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Cargo.lock +2688 -0
- data/Cargo.toml +6 -0
- data/README.md +179 -0
- data/ext/wreq_rb/Cargo.toml +39 -0
- data/ext/wreq_rb/extconf.rb +22 -0
- data/ext/wreq_rb/src/client.rs +565 -0
- data/ext/wreq_rb/src/error.rs +25 -0
- data/ext/wreq_rb/src/lib.rs +20 -0
- data/ext/wreq_rb/src/response.rs +132 -0
- data/lib/wreq-rb/version.rb +5 -0
- data/lib/wreq-rb.rb +17 -0
- data/patches/0001-add-transfer-size-tracking.patch +292 -0
- data/vendor/wreq/Cargo.toml +306 -0
- data/vendor/wreq/LICENSE +202 -0
- data/vendor/wreq/README.md +122 -0
- data/vendor/wreq/examples/cert_store.rs +77 -0
- data/vendor/wreq/examples/connect_via_lower_priority_tokio_runtime.rs +258 -0
- data/vendor/wreq/examples/emulation.rs +118 -0
- data/vendor/wreq/examples/form.rs +14 -0
- data/vendor/wreq/examples/http1_websocket.rs +37 -0
- data/vendor/wreq/examples/http2_websocket.rs +45 -0
- data/vendor/wreq/examples/json_dynamic.rs +41 -0
- data/vendor/wreq/examples/json_typed.rs +47 -0
- data/vendor/wreq/examples/keylog.rs +16 -0
- data/vendor/wreq/examples/request_with_emulation.rs +115 -0
- data/vendor/wreq/examples/request_with_interface.rs +37 -0
- data/vendor/wreq/examples/request_with_local_address.rs +16 -0
- data/vendor/wreq/examples/request_with_proxy.rs +13 -0
- data/vendor/wreq/examples/request_with_redirect.rs +22 -0
- data/vendor/wreq/examples/request_with_version.rs +15 -0
- data/vendor/wreq/examples/tor_socks.rs +24 -0
- data/vendor/wreq/examples/unix_socket.rs +33 -0
- data/vendor/wreq/src/client/body.rs +304 -0
- data/vendor/wreq/src/client/conn/conn.rs +231 -0
- data/vendor/wreq/src/client/conn/connector.rs +549 -0
- data/vendor/wreq/src/client/conn/http.rs +1023 -0
- data/vendor/wreq/src/client/conn/proxy/socks.rs +233 -0
- data/vendor/wreq/src/client/conn/proxy/tunnel.rs +260 -0
- data/vendor/wreq/src/client/conn/proxy.rs +39 -0
- data/vendor/wreq/src/client/conn/tls_info.rs +98 -0
- data/vendor/wreq/src/client/conn/uds.rs +44 -0
- data/vendor/wreq/src/client/conn/verbose.rs +149 -0
- data/vendor/wreq/src/client/conn.rs +323 -0
- data/vendor/wreq/src/client/core/body/incoming.rs +485 -0
- data/vendor/wreq/src/client/core/body/length.rs +118 -0
- data/vendor/wreq/src/client/core/body.rs +34 -0
- data/vendor/wreq/src/client/core/common/buf.rs +149 -0
- data/vendor/wreq/src/client/core/common/rewind.rs +141 -0
- data/vendor/wreq/src/client/core/common/watch.rs +76 -0
- data/vendor/wreq/src/client/core/common.rs +3 -0
- data/vendor/wreq/src/client/core/conn/http1.rs +342 -0
- data/vendor/wreq/src/client/core/conn/http2.rs +307 -0
- data/vendor/wreq/src/client/core/conn.rs +11 -0
- data/vendor/wreq/src/client/core/dispatch.rs +299 -0
- data/vendor/wreq/src/client/core/error.rs +435 -0
- data/vendor/wreq/src/client/core/ext.rs +201 -0
- data/vendor/wreq/src/client/core/http1.rs +178 -0
- data/vendor/wreq/src/client/core/http2.rs +483 -0
- data/vendor/wreq/src/client/core/proto/h1/conn.rs +988 -0
- data/vendor/wreq/src/client/core/proto/h1/decode.rs +1170 -0
- data/vendor/wreq/src/client/core/proto/h1/dispatch.rs +684 -0
- data/vendor/wreq/src/client/core/proto/h1/encode.rs +580 -0
- data/vendor/wreq/src/client/core/proto/h1/io.rs +879 -0
- data/vendor/wreq/src/client/core/proto/h1/role.rs +694 -0
- data/vendor/wreq/src/client/core/proto/h1.rs +104 -0
- data/vendor/wreq/src/client/core/proto/h2/client.rs +650 -0
- data/vendor/wreq/src/client/core/proto/h2/ping.rs +539 -0
- data/vendor/wreq/src/client/core/proto/h2.rs +379 -0
- data/vendor/wreq/src/client/core/proto/headers.rs +138 -0
- data/vendor/wreq/src/client/core/proto.rs +58 -0
- data/vendor/wreq/src/client/core/rt/bounds.rs +57 -0
- data/vendor/wreq/src/client/core/rt/timer.rs +150 -0
- data/vendor/wreq/src/client/core/rt/tokio.rs +99 -0
- data/vendor/wreq/src/client/core/rt.rs +25 -0
- data/vendor/wreq/src/client/core/upgrade.rs +267 -0
- data/vendor/wreq/src/client/core.rs +16 -0
- data/vendor/wreq/src/client/emulation.rs +161 -0
- data/vendor/wreq/src/client/http/client/error.rs +142 -0
- data/vendor/wreq/src/client/http/client/exec.rs +29 -0
- data/vendor/wreq/src/client/http/client/extra.rs +77 -0
- data/vendor/wreq/src/client/http/client/lazy.rs +79 -0
- data/vendor/wreq/src/client/http/client/pool.rs +1105 -0
- data/vendor/wreq/src/client/http/client/util.rs +104 -0
- data/vendor/wreq/src/client/http/client.rs +1003 -0
- data/vendor/wreq/src/client/http/future.rs +99 -0
- data/vendor/wreq/src/client/http.rs +1629 -0
- data/vendor/wreq/src/client/layer/config/options.rs +156 -0
- data/vendor/wreq/src/client/layer/config.rs +116 -0
- data/vendor/wreq/src/client/layer/cookie.rs +161 -0
- data/vendor/wreq/src/client/layer/decoder.rs +139 -0
- data/vendor/wreq/src/client/layer/redirect/future.rs +270 -0
- data/vendor/wreq/src/client/layer/redirect/policy.rs +63 -0
- data/vendor/wreq/src/client/layer/redirect.rs +145 -0
- data/vendor/wreq/src/client/layer/retry/classify.rs +105 -0
- data/vendor/wreq/src/client/layer/retry/scope.rs +51 -0
- data/vendor/wreq/src/client/layer/retry.rs +151 -0
- data/vendor/wreq/src/client/layer/timeout/body.rs +233 -0
- data/vendor/wreq/src/client/layer/timeout/future.rs +90 -0
- data/vendor/wreq/src/client/layer/timeout.rs +177 -0
- data/vendor/wreq/src/client/layer.rs +15 -0
- data/vendor/wreq/src/client/multipart.rs +717 -0
- data/vendor/wreq/src/client/request.rs +818 -0
- data/vendor/wreq/src/client/response.rs +534 -0
- data/vendor/wreq/src/client/ws/json.rs +99 -0
- data/vendor/wreq/src/client/ws/message.rs +453 -0
- data/vendor/wreq/src/client/ws.rs +714 -0
- data/vendor/wreq/src/client.rs +27 -0
- data/vendor/wreq/src/config.rs +140 -0
- data/vendor/wreq/src/cookie.rs +579 -0
- data/vendor/wreq/src/dns/gai.rs +249 -0
- data/vendor/wreq/src/dns/hickory.rs +78 -0
- data/vendor/wreq/src/dns/resolve.rs +180 -0
- data/vendor/wreq/src/dns.rs +69 -0
- data/vendor/wreq/src/error.rs +502 -0
- data/vendor/wreq/src/ext.rs +398 -0
- data/vendor/wreq/src/hash.rs +143 -0
- data/vendor/wreq/src/header.rs +506 -0
- data/vendor/wreq/src/into_uri.rs +187 -0
- data/vendor/wreq/src/lib.rs +586 -0
- data/vendor/wreq/src/proxy/mac.rs +82 -0
- data/vendor/wreq/src/proxy/matcher.rs +806 -0
- data/vendor/wreq/src/proxy/uds.rs +66 -0
- data/vendor/wreq/src/proxy/win.rs +31 -0
- data/vendor/wreq/src/proxy.rs +569 -0
- data/vendor/wreq/src/redirect.rs +575 -0
- data/vendor/wreq/src/retry.rs +198 -0
- data/vendor/wreq/src/sync.rs +129 -0
- data/vendor/wreq/src/tls/conn/cache.rs +123 -0
- data/vendor/wreq/src/tls/conn/cert_compression.rs +125 -0
- data/vendor/wreq/src/tls/conn/ext.rs +82 -0
- data/vendor/wreq/src/tls/conn/macros.rs +34 -0
- data/vendor/wreq/src/tls/conn/service.rs +138 -0
- data/vendor/wreq/src/tls/conn.rs +681 -0
- data/vendor/wreq/src/tls/keylog/handle.rs +64 -0
- data/vendor/wreq/src/tls/keylog.rs +99 -0
- data/vendor/wreq/src/tls/options.rs +464 -0
- data/vendor/wreq/src/tls/x509/identity.rs +122 -0
- data/vendor/wreq/src/tls/x509/parser.rs +71 -0
- data/vendor/wreq/src/tls/x509/store.rs +228 -0
- data/vendor/wreq/src/tls/x509.rs +68 -0
- data/vendor/wreq/src/tls.rs +154 -0
- data/vendor/wreq/src/trace.rs +55 -0
- data/vendor/wreq/src/util.rs +122 -0
- data/vendor/wreq/tests/badssl.rs +228 -0
- data/vendor/wreq/tests/brotli.rs +350 -0
- data/vendor/wreq/tests/client.rs +1098 -0
- data/vendor/wreq/tests/connector_layers.rs +227 -0
- data/vendor/wreq/tests/cookie.rs +306 -0
- data/vendor/wreq/tests/deflate.rs +347 -0
- data/vendor/wreq/tests/emulation.rs +260 -0
- data/vendor/wreq/tests/gzip.rs +347 -0
- data/vendor/wreq/tests/layers.rs +261 -0
- data/vendor/wreq/tests/multipart.rs +165 -0
- data/vendor/wreq/tests/proxy.rs +438 -0
- data/vendor/wreq/tests/redirect.rs +629 -0
- data/vendor/wreq/tests/retry.rs +135 -0
- data/vendor/wreq/tests/support/delay_server.rs +117 -0
- data/vendor/wreq/tests/support/error.rs +16 -0
- data/vendor/wreq/tests/support/layer.rs +183 -0
- data/vendor/wreq/tests/support/mod.rs +9 -0
- data/vendor/wreq/tests/support/server.rs +232 -0
- data/vendor/wreq/tests/timeouts.rs +281 -0
- data/vendor/wreq/tests/unix_socket.rs +135 -0
- data/vendor/wreq/tests/upgrade.rs +98 -0
- data/vendor/wreq/tests/zstd.rs +559 -0
- metadata +225 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
//! multipart/form-data
|
|
2
|
+
|
|
3
|
+
use std::{borrow::Cow, pin::Pin};
|
|
4
|
+
|
|
5
|
+
use bytes::Bytes;
|
|
6
|
+
use futures_util::{Stream, StreamExt, future, stream};
|
|
7
|
+
use http_body_util::BodyExt;
|
|
8
|
+
use mime_guess::Mime;
|
|
9
|
+
use percent_encoding::{self, AsciiSet, NON_ALPHANUMERIC};
|
|
10
|
+
#[cfg(feature = "stream")]
|
|
11
|
+
use {std::io, std::path::Path, tokio::fs::File};
|
|
12
|
+
|
|
13
|
+
use super::Body;
|
|
14
|
+
use crate::header::HeaderMap;
|
|
15
|
+
|
|
16
|
+
/// An async multipart/form-data request.
|
|
17
|
+
#[derive(Debug)]
|
|
18
|
+
pub struct Form {
|
|
19
|
+
inner: FormParts<Part>,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// A field in a multipart form.
|
|
23
|
+
#[derive(Debug)]
|
|
24
|
+
pub struct Part {
|
|
25
|
+
meta: PartMetadata,
|
|
26
|
+
value: Body,
|
|
27
|
+
body_length: Option<u64>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[derive(Debug)]
|
|
31
|
+
pub(crate) struct FormParts<P> {
|
|
32
|
+
pub(crate) boundary: String,
|
|
33
|
+
pub(crate) computed_headers: Vec<Vec<u8>>,
|
|
34
|
+
pub(crate) fields: Vec<(Cow<'static, str>, P)>,
|
|
35
|
+
pub(crate) percent_encoding: PercentEncoding,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[derive(Debug)]
|
|
39
|
+
pub(crate) struct PartMetadata {
|
|
40
|
+
mime: Option<Mime>,
|
|
41
|
+
file_name: Option<Cow<'static, str>>,
|
|
42
|
+
pub(crate) headers: HeaderMap,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub(crate) trait PartProps {
|
|
46
|
+
fn value_len(&self) -> Option<u64>;
|
|
47
|
+
fn metadata(&self) -> &PartMetadata;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ===== impl Form =====
|
|
51
|
+
|
|
52
|
+
impl Default for Form {
|
|
53
|
+
fn default() -> Self {
|
|
54
|
+
Self::new()
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
impl Form {
|
|
59
|
+
/// Creates a new async Form without any content.
|
|
60
|
+
pub fn new() -> Form {
|
|
61
|
+
Form {
|
|
62
|
+
inner: FormParts::new(),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Get the boundary that this form will use.
|
|
67
|
+
#[inline]
|
|
68
|
+
pub fn boundary(&self) -> &str {
|
|
69
|
+
self.inner.boundary()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Add a data field with supplied name and value.
|
|
73
|
+
///
|
|
74
|
+
/// # Examples
|
|
75
|
+
///
|
|
76
|
+
/// ```
|
|
77
|
+
/// let form = wreq::multipart::Form::new()
|
|
78
|
+
/// .text("username", "seanmonstar")
|
|
79
|
+
/// .text("password", "secret");
|
|
80
|
+
/// ```
|
|
81
|
+
pub fn text<T, U>(self, name: T, value: U) -> Form
|
|
82
|
+
where
|
|
83
|
+
T: Into<Cow<'static, str>>,
|
|
84
|
+
U: Into<Cow<'static, str>>,
|
|
85
|
+
{
|
|
86
|
+
self.part(name, Part::text(value))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Adds a file field.
|
|
90
|
+
///
|
|
91
|
+
/// The path will be used to try to guess the filename and mime.
|
|
92
|
+
///
|
|
93
|
+
/// # Examples
|
|
94
|
+
///
|
|
95
|
+
/// ```no_run
|
|
96
|
+
/// # async fn run() -> std::io::Result<()> {
|
|
97
|
+
/// let form = wreq::multipart::Form::new()
|
|
98
|
+
/// .file("key", "/path/to/file")
|
|
99
|
+
/// .await?;
|
|
100
|
+
/// # Ok(())
|
|
101
|
+
/// # }
|
|
102
|
+
/// ```
|
|
103
|
+
///
|
|
104
|
+
/// # Errors
|
|
105
|
+
///
|
|
106
|
+
/// Errors when the file cannot be opened.
|
|
107
|
+
#[cfg(feature = "stream")]
|
|
108
|
+
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
|
|
109
|
+
pub async fn file<T, U>(self, name: T, path: U) -> io::Result<Form>
|
|
110
|
+
where
|
|
111
|
+
T: Into<Cow<'static, str>>,
|
|
112
|
+
U: AsRef<Path>,
|
|
113
|
+
{
|
|
114
|
+
Ok(self.part(name, Part::file(path).await?))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Adds a customized Part.
|
|
118
|
+
pub fn part<T>(self, name: T, part: Part) -> Form
|
|
119
|
+
where
|
|
120
|
+
T: Into<Cow<'static, str>>,
|
|
121
|
+
{
|
|
122
|
+
self.with_inner(move |inner| inner.part(name, part))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Configure this `Form` to percent-encode using the `path-segment` rules.
|
|
126
|
+
pub fn percent_encode_path_segment(self) -> Form {
|
|
127
|
+
self.with_inner(|inner| inner.percent_encode_path_segment())
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Configure this `Form` to percent-encode using the `attr-char` rules.
|
|
131
|
+
pub fn percent_encode_attr_chars(self) -> Form {
|
|
132
|
+
self.with_inner(|inner| inner.percent_encode_attr_chars())
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Configure this `Form` to skip percent-encoding
|
|
136
|
+
pub fn percent_encode_noop(self) -> Form {
|
|
137
|
+
self.with_inner(|inner| inner.percent_encode_noop())
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Consume this instance and transform into an instance of Body for use in a request.
|
|
141
|
+
pub(crate) fn stream(self) -> Body {
|
|
142
|
+
if self.inner.fields.is_empty() {
|
|
143
|
+
return Body::empty();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
Body::stream(self.into_stream())
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Produce a stream of the bytes in this `Form`, consuming it.
|
|
150
|
+
pub fn into_stream(mut self) -> impl Stream<Item = Result<Bytes, crate::Error>> + Send + Sync {
|
|
151
|
+
if self.inner.fields.is_empty() {
|
|
152
|
+
let empty_stream: Pin<
|
|
153
|
+
Box<dyn Stream<Item = Result<Bytes, crate::Error>> + Send + Sync>,
|
|
154
|
+
> = Box::pin(futures_util::stream::empty());
|
|
155
|
+
return empty_stream;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// create initial part to init reduce chain
|
|
159
|
+
let (name, part) = self.inner.fields.remove(0);
|
|
160
|
+
let start = Box::pin(self.part_stream(name, part))
|
|
161
|
+
as Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send + Sync>>;
|
|
162
|
+
|
|
163
|
+
let fields = self.inner.take_fields();
|
|
164
|
+
// for each field, chain an additional stream
|
|
165
|
+
let stream = fields.into_iter().fold(start, |memo, (name, part)| {
|
|
166
|
+
let part_stream = self.part_stream(name, part);
|
|
167
|
+
Box::pin(memo.chain(part_stream))
|
|
168
|
+
as Pin<Box<dyn Stream<Item = crate::Result<Bytes>> + Send + Sync>>
|
|
169
|
+
});
|
|
170
|
+
// append special ending boundary
|
|
171
|
+
let last = stream::once(future::ready(Ok(
|
|
172
|
+
format!("--{}--\r\n", self.boundary()).into()
|
|
173
|
+
)));
|
|
174
|
+
Box::pin(stream.chain(last))
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Generate a crate::core::Body stream for a single Part instance of a Form request.
|
|
178
|
+
pub(crate) fn part_stream<T>(
|
|
179
|
+
&mut self,
|
|
180
|
+
name: T,
|
|
181
|
+
part: Part,
|
|
182
|
+
) -> impl Stream<Item = Result<Bytes, crate::Error>> + use<T>
|
|
183
|
+
where
|
|
184
|
+
T: Into<Cow<'static, str>>,
|
|
185
|
+
{
|
|
186
|
+
// start with boundary
|
|
187
|
+
let boundary = stream::once(future::ready(Ok(
|
|
188
|
+
format!("--{}\r\n", self.boundary()).into()
|
|
189
|
+
)));
|
|
190
|
+
// append headers
|
|
191
|
+
let header = stream::once(future::ready(Ok({
|
|
192
|
+
let mut h = self
|
|
193
|
+
.inner
|
|
194
|
+
.percent_encoding
|
|
195
|
+
.encode_headers(&name.into(), &part.meta);
|
|
196
|
+
h.extend_from_slice(b"\r\n\r\n");
|
|
197
|
+
h.into()
|
|
198
|
+
})));
|
|
199
|
+
// then append form data followed by terminating CRLF
|
|
200
|
+
boundary
|
|
201
|
+
.chain(header)
|
|
202
|
+
.chain(part.value.into_data_stream())
|
|
203
|
+
.chain(stream::once(future::ready(Ok("\r\n".into()))))
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
pub(crate) fn compute_length(&mut self) -> Option<u64> {
|
|
207
|
+
self.inner.compute_length()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn with_inner<F>(self, func: F) -> Self
|
|
211
|
+
where
|
|
212
|
+
F: FnOnce(FormParts<Part>) -> FormParts<Part>,
|
|
213
|
+
{
|
|
214
|
+
Form {
|
|
215
|
+
inner: func(self.inner),
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ===== impl Part =====
|
|
221
|
+
|
|
222
|
+
impl Part {
|
|
223
|
+
/// Makes a text parameter.
|
|
224
|
+
pub fn text<T>(value: T) -> Part
|
|
225
|
+
where
|
|
226
|
+
T: Into<Cow<'static, str>>,
|
|
227
|
+
{
|
|
228
|
+
let body = match value.into() {
|
|
229
|
+
Cow::Borrowed(slice) => Body::from(slice),
|
|
230
|
+
Cow::Owned(string) => Body::from(string),
|
|
231
|
+
};
|
|
232
|
+
Part::new(body, None)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/// Makes a new parameter from arbitrary bytes.
|
|
236
|
+
pub fn bytes<T>(value: T) -> Part
|
|
237
|
+
where
|
|
238
|
+
T: Into<Cow<'static, [u8]>>,
|
|
239
|
+
{
|
|
240
|
+
let body = match value.into() {
|
|
241
|
+
Cow::Borrowed(slice) => Body::from(slice),
|
|
242
|
+
Cow::Owned(vec) => Body::from(vec),
|
|
243
|
+
};
|
|
244
|
+
Part::new(body, None)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Makes a new parameter from an arbitrary stream.
|
|
248
|
+
pub fn stream<T: Into<Body>>(value: T) -> Part {
|
|
249
|
+
Part::new(value.into(), None)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Makes a new parameter from an arbitrary stream with a known length. This is particularly
|
|
253
|
+
/// useful when adding something like file contents as a stream, where you can know the content
|
|
254
|
+
/// length beforehand.
|
|
255
|
+
pub fn stream_with_length<T: Into<Body>>(value: T, length: u64) -> Part {
|
|
256
|
+
Part::new(value.into(), Some(length))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// Makes a file parameter.
|
|
260
|
+
///
|
|
261
|
+
/// # Errors
|
|
262
|
+
///
|
|
263
|
+
/// Errors when the file cannot be opened.
|
|
264
|
+
#[cfg(feature = "stream")]
|
|
265
|
+
#[cfg_attr(docsrs, doc(cfg(feature = "stream")))]
|
|
266
|
+
pub async fn file<T: AsRef<Path>>(path: T) -> io::Result<Part> {
|
|
267
|
+
let path = path.as_ref();
|
|
268
|
+
let file_name = path
|
|
269
|
+
.file_name()
|
|
270
|
+
.map(|filename| filename.to_string_lossy().into_owned());
|
|
271
|
+
let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
|
|
272
|
+
let mime = mime_guess::from_ext(ext).first_or_octet_stream();
|
|
273
|
+
let file = File::open(path).await?;
|
|
274
|
+
let len = file.metadata().await.map(|m| m.len()).ok();
|
|
275
|
+
let field = match len {
|
|
276
|
+
Some(len) => Part::stream_with_length(file, len),
|
|
277
|
+
None => Part::stream(file),
|
|
278
|
+
}
|
|
279
|
+
.mime(mime);
|
|
280
|
+
|
|
281
|
+
Ok(if let Some(file_name) = file_name {
|
|
282
|
+
field.file_name(file_name)
|
|
283
|
+
} else {
|
|
284
|
+
field
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
fn new(value: Body, body_length: Option<u64>) -> Part {
|
|
289
|
+
Part {
|
|
290
|
+
meta: PartMetadata::new(),
|
|
291
|
+
value,
|
|
292
|
+
body_length,
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// Tries to set the mime of this part.
|
|
297
|
+
pub fn mime_str(self, mime: &str) -> crate::Result<Part> {
|
|
298
|
+
Ok(self.mime(mime.parse().map_err(crate::Error::builder)?))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Re-export when mime 0.4 is available, with split MediaType/MediaRange.
|
|
302
|
+
fn mime(self, mime: Mime) -> Part {
|
|
303
|
+
self.with_inner(move |inner| inner.mime(mime))
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// Sets the filename, builder style.
|
|
307
|
+
pub fn file_name<T>(self, filename: T) -> Part
|
|
308
|
+
where
|
|
309
|
+
T: Into<Cow<'static, str>>,
|
|
310
|
+
{
|
|
311
|
+
self.with_inner(move |inner| inner.file_name(filename))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/// Sets custom headers for the part.
|
|
315
|
+
pub fn headers(self, headers: HeaderMap) -> Part {
|
|
316
|
+
self.with_inner(move |inner| inner.headers(headers))
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
fn with_inner<F>(self, func: F) -> Self
|
|
320
|
+
where
|
|
321
|
+
F: FnOnce(PartMetadata) -> PartMetadata,
|
|
322
|
+
{
|
|
323
|
+
Part {
|
|
324
|
+
meta: func(self.meta),
|
|
325
|
+
..self
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
impl PartProps for Part {
|
|
331
|
+
fn value_len(&self) -> Option<u64> {
|
|
332
|
+
if self.body_length.is_some() {
|
|
333
|
+
self.body_length
|
|
334
|
+
} else {
|
|
335
|
+
self.value.content_length()
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
fn metadata(&self) -> &PartMetadata {
|
|
340
|
+
&self.meta
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ===== impl FormParts =====
|
|
345
|
+
|
|
346
|
+
impl<P: PartProps> FormParts<P> {
|
|
347
|
+
pub(crate) fn new() -> Self {
|
|
348
|
+
FormParts {
|
|
349
|
+
boundary: gen_boundary(),
|
|
350
|
+
computed_headers: Vec::new(),
|
|
351
|
+
fields: Vec::new(),
|
|
352
|
+
percent_encoding: PercentEncoding::PathSegment,
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
pub(crate) fn boundary(&self) -> &str {
|
|
357
|
+
&self.boundary
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/// Adds a customized Part.
|
|
361
|
+
pub(crate) fn part<T>(mut self, name: T, part: P) -> Self
|
|
362
|
+
where
|
|
363
|
+
T: Into<Cow<'static, str>>,
|
|
364
|
+
{
|
|
365
|
+
self.fields.push((name.into(), part));
|
|
366
|
+
self
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/// Configure this `Form` to percent-encode using the `path-segment` rules.
|
|
370
|
+
pub(crate) fn percent_encode_path_segment(mut self) -> Self {
|
|
371
|
+
self.percent_encoding = PercentEncoding::PathSegment;
|
|
372
|
+
self
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/// Configure this `Form` to percent-encode using the `attr-char` rules.
|
|
376
|
+
pub(crate) fn percent_encode_attr_chars(mut self) -> Self {
|
|
377
|
+
self.percent_encoding = PercentEncoding::AttrChar;
|
|
378
|
+
self
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/// Configure this `Form` to skip percent-encoding
|
|
382
|
+
pub(crate) fn percent_encode_noop(mut self) -> Self {
|
|
383
|
+
self.percent_encoding = PercentEncoding::NoOp;
|
|
384
|
+
self
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// If predictable, computes the length the request will have
|
|
388
|
+
// The length should be predictable if only String and file fields have been added,
|
|
389
|
+
// but not if a generic reader has been added;
|
|
390
|
+
pub(crate) fn compute_length(&mut self) -> Option<u64> {
|
|
391
|
+
let mut length = 0u64;
|
|
392
|
+
for (name, field) in self.fields.iter() {
|
|
393
|
+
match field.value_len() {
|
|
394
|
+
Some(value_length) => {
|
|
395
|
+
// We are constructing the header just to get its length. To not have to
|
|
396
|
+
// construct it again when the request is sent we cache these headers.
|
|
397
|
+
let header = self.percent_encoding.encode_headers(name, field.metadata());
|
|
398
|
+
let header_length = header.len();
|
|
399
|
+
self.computed_headers.push(header);
|
|
400
|
+
// The additions mimic the format string out of which the field is constructed
|
|
401
|
+
// in Reader. Not the cleanest solution because if that format string is
|
|
402
|
+
// ever changed then this formula needs to be changed too which is not an
|
|
403
|
+
// obvious dependency in the code.
|
|
404
|
+
length += 2
|
|
405
|
+
+ self.boundary().len() as u64
|
|
406
|
+
+ 2
|
|
407
|
+
+ header_length as u64
|
|
408
|
+
+ 4
|
|
409
|
+
+ value_length
|
|
410
|
+
+ 2
|
|
411
|
+
}
|
|
412
|
+
_ => return None,
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// If there is at least one field there is a special boundary for the very last field.
|
|
416
|
+
if !self.fields.is_empty() {
|
|
417
|
+
length += 2 + self.boundary().len() as u64 + 4
|
|
418
|
+
}
|
|
419
|
+
Some(length)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// Take the fields vector of this instance, replacing with an empty vector.
|
|
423
|
+
fn take_fields(&mut self) -> Vec<(Cow<'static, str>, P)> {
|
|
424
|
+
std::mem::take(&mut self.fields)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ===== impl PartMetadata =====
|
|
429
|
+
|
|
430
|
+
impl PartMetadata {
|
|
431
|
+
pub(crate) fn new() -> Self {
|
|
432
|
+
PartMetadata {
|
|
433
|
+
mime: None,
|
|
434
|
+
file_name: None,
|
|
435
|
+
headers: HeaderMap::default(),
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
pub(crate) fn mime(mut self, mime: Mime) -> Self {
|
|
440
|
+
self.mime = Some(mime);
|
|
441
|
+
self
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
pub(crate) fn file_name<T>(mut self, filename: T) -> Self
|
|
445
|
+
where
|
|
446
|
+
T: Into<Cow<'static, str>>,
|
|
447
|
+
{
|
|
448
|
+
self.file_name = Some(filename.into());
|
|
449
|
+
self
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
pub(crate) fn headers<T>(mut self, headers: T) -> Self
|
|
453
|
+
where
|
|
454
|
+
T: Into<HeaderMap>,
|
|
455
|
+
{
|
|
456
|
+
self.headers = headers.into();
|
|
457
|
+
self
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
|
462
|
+
const FRAGMENT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS
|
|
463
|
+
.add(b' ')
|
|
464
|
+
.add(b'"')
|
|
465
|
+
.add(b'<')
|
|
466
|
+
.add(b'>')
|
|
467
|
+
.add(b'`');
|
|
468
|
+
|
|
469
|
+
// https://url.spec.whatwg.org/#path-percent-encode-set
|
|
470
|
+
const PATH_ENCODE_SET: &AsciiSet = &FRAGMENT_ENCODE_SET.add(b'#').add(b'?').add(b'{').add(b'}');
|
|
471
|
+
|
|
472
|
+
const PATH_SEGMENT_ENCODE_SET: &AsciiSet = &PATH_ENCODE_SET.add(b'/').add(b'%');
|
|
473
|
+
|
|
474
|
+
// https://tools.ietf.org/html/rfc8187#section-3.2.1
|
|
475
|
+
const ATTR_CHAR_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
|
|
476
|
+
.remove(b'!')
|
|
477
|
+
.remove(b'#')
|
|
478
|
+
.remove(b'$')
|
|
479
|
+
.remove(b'&')
|
|
480
|
+
.remove(b'+')
|
|
481
|
+
.remove(b'-')
|
|
482
|
+
.remove(b'.')
|
|
483
|
+
.remove(b'^')
|
|
484
|
+
.remove(b'_')
|
|
485
|
+
.remove(b'`')
|
|
486
|
+
.remove(b'|')
|
|
487
|
+
.remove(b'~');
|
|
488
|
+
|
|
489
|
+
#[derive(Debug)]
|
|
490
|
+
pub(crate) enum PercentEncoding {
|
|
491
|
+
PathSegment,
|
|
492
|
+
AttrChar,
|
|
493
|
+
NoOp,
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
impl PercentEncoding {
|
|
497
|
+
pub(crate) fn encode_headers(&self, name: &str, field: &PartMetadata) -> Vec<u8> {
|
|
498
|
+
let mut buf = Vec::new();
|
|
499
|
+
buf.extend_from_slice(b"Content-Disposition: form-data; ");
|
|
500
|
+
|
|
501
|
+
match self.percent_encode(name) {
|
|
502
|
+
Cow::Borrowed(value) => {
|
|
503
|
+
// nothing has been percent encoded
|
|
504
|
+
buf.extend_from_slice(b"name=\"");
|
|
505
|
+
buf.extend_from_slice(value.as_bytes());
|
|
506
|
+
buf.extend_from_slice(b"\"");
|
|
507
|
+
}
|
|
508
|
+
Cow::Owned(value) => {
|
|
509
|
+
// something has been percent encoded
|
|
510
|
+
buf.extend_from_slice(b"name*=utf-8''");
|
|
511
|
+
buf.extend_from_slice(value.as_bytes());
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// According to RFC7578 Section 4.2, `filename*=` syntax is invalid.
|
|
516
|
+
// See https://github.com/seanmonstar/reqwest/issues/419.
|
|
517
|
+
if let Some(filename) = &field.file_name {
|
|
518
|
+
buf.extend_from_slice(b"; filename=\"");
|
|
519
|
+
let legal_filename = filename
|
|
520
|
+
.replace('\\', "\\\\")
|
|
521
|
+
.replace('"', "\\\"")
|
|
522
|
+
.replace('\r', "\\\r")
|
|
523
|
+
.replace('\n', "\\\n");
|
|
524
|
+
buf.extend_from_slice(legal_filename.as_bytes());
|
|
525
|
+
buf.extend_from_slice(b"\"");
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if let Some(mime) = &field.mime {
|
|
529
|
+
buf.extend_from_slice(b"\r\nContent-Type: ");
|
|
530
|
+
buf.extend_from_slice(mime.as_ref().as_bytes());
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
for (k, v) in field.headers.iter() {
|
|
534
|
+
buf.extend_from_slice(b"\r\n");
|
|
535
|
+
buf.extend_from_slice(k.as_str().as_bytes());
|
|
536
|
+
buf.extend_from_slice(b": ");
|
|
537
|
+
buf.extend_from_slice(v.as_bytes());
|
|
538
|
+
}
|
|
539
|
+
buf
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
fn percent_encode<'a>(&self, value: &'a str) -> Cow<'a, str> {
|
|
543
|
+
use percent_encoding::utf8_percent_encode as percent_encode;
|
|
544
|
+
|
|
545
|
+
match self {
|
|
546
|
+
Self::PathSegment => percent_encode(value, PATH_SEGMENT_ENCODE_SET).into(),
|
|
547
|
+
Self::AttrChar => percent_encode(value, ATTR_CHAR_ENCODE_SET).into(),
|
|
548
|
+
Self::NoOp => value.into(),
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
fn gen_boundary() -> String {
|
|
554
|
+
use crate::util::fast_random as random;
|
|
555
|
+
|
|
556
|
+
let a = random();
|
|
557
|
+
let b = random();
|
|
558
|
+
let c = random();
|
|
559
|
+
let d = random();
|
|
560
|
+
|
|
561
|
+
format!("{a:016x}-{b:016x}-{c:016x}-{d:016x}")
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
#[cfg(test)]
|
|
565
|
+
mod tests {
|
|
566
|
+
use std::future;
|
|
567
|
+
|
|
568
|
+
use futures_util::{TryStreamExt, stream};
|
|
569
|
+
use tokio::{self, runtime};
|
|
570
|
+
|
|
571
|
+
use super::*;
|
|
572
|
+
|
|
573
|
+
#[test]
|
|
574
|
+
fn form_empty() {
|
|
575
|
+
let form = Form::new();
|
|
576
|
+
|
|
577
|
+
let rt = runtime::Builder::new_current_thread()
|
|
578
|
+
.enable_all()
|
|
579
|
+
.build()
|
|
580
|
+
.expect("new rt");
|
|
581
|
+
let body = form.stream().into_data_stream();
|
|
582
|
+
let s = body.map_ok(|try_c| try_c.to_vec()).try_concat();
|
|
583
|
+
|
|
584
|
+
let out = rt.block_on(s);
|
|
585
|
+
assert!(out.unwrap().is_empty());
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
#[test]
|
|
589
|
+
fn stream_to_end() {
|
|
590
|
+
let mut form = Form::new()
|
|
591
|
+
.part(
|
|
592
|
+
"reader1",
|
|
593
|
+
Part::stream(Body::stream(stream::once(future::ready::<
|
|
594
|
+
Result<String, crate::Error>,
|
|
595
|
+
>(Ok(
|
|
596
|
+
"part1".to_owned()
|
|
597
|
+
))))),
|
|
598
|
+
)
|
|
599
|
+
.part("key1", Part::text("value1"))
|
|
600
|
+
.part(
|
|
601
|
+
"key2",
|
|
602
|
+
Part::text("value2").mime(mime_guess::mime::IMAGE_BMP),
|
|
603
|
+
)
|
|
604
|
+
.part(
|
|
605
|
+
"reader2",
|
|
606
|
+
Part::stream(Body::stream(stream::once(future::ready::<
|
|
607
|
+
Result<String, crate::Error>,
|
|
608
|
+
>(Ok(
|
|
609
|
+
"part2".to_owned()
|
|
610
|
+
))))),
|
|
611
|
+
)
|
|
612
|
+
.part("key3", Part::text("value3").file_name("filename"));
|
|
613
|
+
form.inner.boundary = "boundary".to_string();
|
|
614
|
+
let expected = "--boundary\r\n\
|
|
615
|
+
Content-Disposition: form-data; name=\"reader1\"\r\n\r\n\
|
|
616
|
+
part1\r\n\
|
|
617
|
+
--boundary\r\n\
|
|
618
|
+
Content-Disposition: form-data; name=\"key1\"\r\n\r\n\
|
|
619
|
+
value1\r\n\
|
|
620
|
+
--boundary\r\n\
|
|
621
|
+
Content-Disposition: form-data; name=\"key2\"\r\n\
|
|
622
|
+
Content-Type: image/bmp\r\n\r\n\
|
|
623
|
+
value2\r\n\
|
|
624
|
+
--boundary\r\n\
|
|
625
|
+
Content-Disposition: form-data; name=\"reader2\"\r\n\r\n\
|
|
626
|
+
part2\r\n\
|
|
627
|
+
--boundary\r\n\
|
|
628
|
+
Content-Disposition: form-data; name=\"key3\"; filename=\"filename\"\r\n\r\n\
|
|
629
|
+
value3\r\n--boundary--\r\n";
|
|
630
|
+
let rt = runtime::Builder::new_current_thread()
|
|
631
|
+
.enable_all()
|
|
632
|
+
.build()
|
|
633
|
+
.expect("new rt");
|
|
634
|
+
let body = form.stream().into_data_stream();
|
|
635
|
+
let s = body.map(|try_c| try_c.map(|r| r.to_vec())).try_concat();
|
|
636
|
+
|
|
637
|
+
let out = rt.block_on(s).unwrap();
|
|
638
|
+
// These prints are for debug purposes in case the test fails
|
|
639
|
+
println!(
|
|
640
|
+
"START REAL\n{}\nEND REAL",
|
|
641
|
+
std::str::from_utf8(&out).unwrap()
|
|
642
|
+
);
|
|
643
|
+
println!("START EXPECTED\n{expected}\nEND EXPECTED");
|
|
644
|
+
assert_eq!(std::str::from_utf8(&out).unwrap(), expected);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
#[test]
|
|
648
|
+
fn stream_to_end_with_header() {
|
|
649
|
+
let mut part = Part::text("value2").mime(mime_guess::mime::IMAGE_BMP);
|
|
650
|
+
let mut headers = HeaderMap::new();
|
|
651
|
+
headers.insert("Hdr3", "/a/b/c".parse().unwrap());
|
|
652
|
+
part = part.headers(headers);
|
|
653
|
+
let mut form = Form::new().part("key2", part);
|
|
654
|
+
form.inner.boundary = "boundary".to_string();
|
|
655
|
+
let expected = "--boundary\r\n\
|
|
656
|
+
Content-Disposition: form-data; name=\"key2\"\r\n\
|
|
657
|
+
Content-Type: image/bmp\r\n\
|
|
658
|
+
hdr3: /a/b/c\r\n\
|
|
659
|
+
\r\n\
|
|
660
|
+
value2\r\n\
|
|
661
|
+
--boundary--\r\n";
|
|
662
|
+
let rt = runtime::Builder::new_current_thread()
|
|
663
|
+
.enable_all()
|
|
664
|
+
.build()
|
|
665
|
+
.expect("new rt");
|
|
666
|
+
let body = form.stream().into_data_stream();
|
|
667
|
+
let s = body.map(|try_c| try_c.map(|r| r.to_vec())).try_concat();
|
|
668
|
+
|
|
669
|
+
let out = rt.block_on(s).unwrap();
|
|
670
|
+
// These prints are for debug purposes in case the test fails
|
|
671
|
+
println!(
|
|
672
|
+
"START REAL\n{}\nEND REAL",
|
|
673
|
+
std::str::from_utf8(&out).unwrap()
|
|
674
|
+
);
|
|
675
|
+
println!("START EXPECTED\n{expected}\nEND EXPECTED");
|
|
676
|
+
assert_eq!(std::str::from_utf8(&out).unwrap(), expected);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
#[test]
|
|
680
|
+
fn correct_content_length() {
|
|
681
|
+
// Setup an arbitrary data stream
|
|
682
|
+
let stream_data = b"just some stream data";
|
|
683
|
+
let stream_len = stream_data.len();
|
|
684
|
+
let stream_data = stream_data
|
|
685
|
+
.chunks(3)
|
|
686
|
+
.map(|c| Ok::<_, std::io::Error>(Bytes::from(c)));
|
|
687
|
+
let the_stream = futures_util::stream::iter(stream_data);
|
|
688
|
+
|
|
689
|
+
let bytes_data = b"some bytes data".to_vec();
|
|
690
|
+
let bytes_len = bytes_data.len();
|
|
691
|
+
|
|
692
|
+
let stream_part = Part::stream_with_length(Body::stream(the_stream), stream_len as u64);
|
|
693
|
+
let body_part = Part::bytes(bytes_data);
|
|
694
|
+
|
|
695
|
+
// A simple check to make sure we get the configured body length
|
|
696
|
+
assert_eq!(stream_part.value_len().unwrap(), stream_len as u64);
|
|
697
|
+
|
|
698
|
+
// Make sure it delegates to the underlying body if length is not specified
|
|
699
|
+
assert_eq!(body_part.value_len().unwrap(), bytes_len as u64);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
#[test]
|
|
703
|
+
fn header_percent_encoding() {
|
|
704
|
+
let name = "start%'\"\r\nßend";
|
|
705
|
+
let field = Part::text("");
|
|
706
|
+
|
|
707
|
+
assert_eq!(
|
|
708
|
+
PercentEncoding::PathSegment.encode_headers(name, &field.meta),
|
|
709
|
+
&b"Content-Disposition: form-data; name*=utf-8''start%25'%22%0D%0A%C3%9Fend"[..]
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
assert_eq!(
|
|
713
|
+
PercentEncoding::AttrChar.encode_headers(name, &field.meta),
|
|
714
|
+
&b"Content-Disposition: form-data; name*=utf-8''start%25%27%22%0D%0A%C3%9Fend"[..]
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
}
|