1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
//! [`HttpClient`] and [`build_client`]: the connection-pooled client
//! [`crate::send`]/[`crate::send_prepared`] send through, and its redirect
//! bookkeeping.
use ;
use Duration;
use crate;
use crateSendraError;
use crateRedirectHop;
/// Redirect hops recorded during the request currently in flight through a
/// given [`HttpClient`].
///
/// A `reqwest::redirect::Policy` closure has no way to hand its caller
/// anything back directly — it only decides follow/stop/error — so this is
/// the side channel: the policy pushes a hop here as it sees each one, and
/// [`send_prepared`](crate::send_prepared) drains it right after that request finishes. The client
/// is built once per run and reused by every request in it (see
/// [`build_client`]), so the log is cleared at the *start* of each send
/// rather than trusted to be empty — nothing else empties it, and requests in
/// a run are sent one at a time, never concurrently, so there is never more
/// than one request's hops in it at once.
///
/// **This assumes strictly sequential sends through one [`HttpClient`].**
/// There is exactly one log per client, shared by every request that client
/// ever sends, and a hop is attributed to "whatever is currently between the
/// clear in `send_prepared` and the drain right after it" — not to any
/// particular request. Two requests sent concurrently through the same
/// client would race on that log and could easily come back with each
/// other's redirect hops, or a merged chain that belongs to neither. Nothing
/// today does that — `run_requests` in `sendra-cli` awaits each request
/// before starting the next — but if a future feature sends requests from
/// one client in parallel (a `--repeat`/retry feature that fires several at
/// once, say, or any other parallel send path), this mechanism has to change
/// with it: most likely one log per in-flight request rather than one per
/// client, or a channel instead of a shared `Vec`.
type RedirectLog = ;
/// The HTTP client [`send`](crate::send) and [`send_prepared`](crate::send_prepared) send through.
///
/// A thin wrapper around `reqwest::Client` rather than a re-export of it, so
/// that the redirect chain a request's `reqwest::redirect::Policy` observes
/// has somewhere to be recorded and read back — see [`RedirectLog`]. A
/// front-end builds one with [`build_client`] and passes it around without
/// taking a direct dependency on reqwest.
/// Build the HTTP client a run sends every one of its requests through.
///
/// **Once per run, not once per request.** A `reqwest::Client` owns the
/// connection pool: the TLS session, the kept-alive TCP connection and the
/// resolved DNS for a host all live in it, and all of it is thrown away with
/// the client. Building one per request means a collection of twenty requests
/// against one API pays twenty TLS handshakes to send twenty requests, which is
/// most of the wall clock for a run that does nothing else. Built once and
/// borrowed by every send, the second request onwards reuses the connection the
/// first opened.
///
/// It is a function taking a `&Config` rather than a method on `Config`
/// because a client is not configuration: it holds sockets, it is cheap to
/// clone and expensive to rebuild, and it belongs to a *run*, whereas the
/// config it is built from is a resolved set of values that outlives any
/// particular one. The config decides six things here — the timeout, the
/// redirect policy, whether TLS certificates are verified, which proxy (if
/// any) requests go through, which client certificate (if any) to present
/// for mutual TLS, and whether cookies received are stored and resent
/// automatically — and nothing else about the client is configurable;
/// reqwest's own pool defaults are what a command-line tool wants.
///
/// **Cookies are opt-in.** [`Config::cookie_jar`] defaults to `false`,
/// matching curl's own default of not persisting cookies across requests
/// unless `-c`/`-b` is passed. When enabled, this hands the client
/// reqwest's own in-memory jar (`ClientBuilder::cookie_store(true)`) rather
/// than a jar Sendra owns: there is no persistence to disk and nothing
/// beyond one invocation to manage, so reqwest's default implementation is
/// exactly what is needed. A request whose `headers:` already sets `Cookie`
/// is left alone — reqwest only fills in the jar's `Cookie` header when the
/// request does not already carry one, confirmed by reading reqwest's own
/// `CookieService` rather than assumed, so an explicit `Cookie:` header
/// always wins outright rather than merging with the jar; Sendra raises no
/// conflict for this the way it
/// does for `auth:` plus an explicit `Authorization` header, since the two
/// are not the same field the way `auth:` resolves *into* `Authorization` —
/// the jar operates beneath any one request's headers, at the client's own
/// connection machinery. Cookies received in response to that request are
/// still stored in the jar regardless of the request's own `Cookie` header,
/// so a later request with no explicit header of its own picks them up.
///
/// **The jar sees every hop of a redirect chain, not just the final
/// response.** reqwest layers its cookie handling *underneath* its
/// redirect-following — each hop of a chain is a separate request/response
/// pair the jar's `CookieService` processes on its own, confirmed by
/// reading reqwest's source rather than assumed — so a `Set-Cookie` on an
/// intermediate hop is stored just as reliably as one on the final
/// response, and is even available to *later* hops in the same chain. This
/// is a genuine advantage over `capture`'s manual `Set-Cookie` capture,
/// which can only see the final response's headers once redirects have
/// been followed — see the module doc comment on
/// [`crate::capture`] for that limitation. For a login flow that redirects
/// through an intermediate hop before setting its session cookie, the jar
/// is the only one of the two that can pick it up.
///
/// Fails when reqwest cannot construct a client at all (a TLS backend that
/// will not initialise, say), when [`Config::proxy`] does not parse as a URL
/// reqwest accepts, or when the client certificate cannot be built — either
/// because `client_cert`/`client_key` names a file that cannot be read
/// ([`SendraError::ClientCertIo`]), only one of the pair is set
/// ([`SendraError::ClientCertIncomplete`]), or the files read do not form a
/// valid identity ([`SendraError::Client`]) — all fatal to the whole run: a
/// malformed `proxy:`/`--proxy` value, or an unusable client certificate,
/// means no request in this run could ever have gone anywhere, same as a
/// client reqwest itself refuses to build.
/// Build the client certificate identity for mutual TLS, or `None` when
/// `config` sets neither `client_cert` nor `client_key`.
///
/// Reads both files and concatenates them into one buffer — certificate PEM,
/// then key PEM — because reqwest's `rustls-tls` backend exposes exactly one
/// identity constructor, [`reqwest::Identity::from_pem`], and it wants both
/// halves in a single buffer rather than as two arguments. (The two-argument
/// and PKCS#12 constructors exist on `reqwest::Identity` but are gated behind
/// the `native-tls` feature, which this workspace does not enable — see
/// [`Config::client_cert`]'s doc comment.) A file that cannot be read is
/// [`SendraError::ClientCertIo`], naming the path, before reqwest ever sees
/// it; a buffer reqwest cannot parse as a valid identity is
/// [`SendraError::Client`], the same variant every other client-construction
/// failure here uses.
///
/// Exactly one of `client_cert`/`client_key` being set is refused as
/// [`SendraError::ClientCertIncomplete`] — see that variant's doc comment for
/// why this is checked here rather than earlier: CLI overrides for one half
/// and a config value for the other are a valid combination, and both are
/// folded into `config` before this ever runs.
/// Raised by the custom redirect policy in [`build_client`] when a chain runs
/// past the configured maximum.
///
/// **Exceeding the limit is an error, the same as reqwest's own default
/// behaviour today.** A response was never short of one — the chain simply
/// did not resolve within the hops the config allows — so there is no single
/// "last response reached" that would not misrepresent what happened, the way
/// there would be for a hop that landed on a plain 3xx with redirects turned
/// off entirely. This reaches the caller as [`SendraError::Network`], wrapping
/// reqwest's own redirect error, exactly like a DNS or TLS failure.