honeymaker 0.12.3 → 0.12.4
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 +4 -4
- data/.github/workflows/native-gems.yml +100 -0
- data/.github/workflows/release.yml +36 -0
- data/.github/workflows/test.yml +19 -4
- data/Cargo.lock +1078 -0
- data/Cargo.toml +14 -0
- data/README.md +39 -0
- data/Rakefile +28 -16
- data/crates/honeymaker/Cargo.toml +18 -0
- data/crates/honeymaker/src/encode.rs +106 -0
- data/crates/honeymaker/src/kraken/lookup.rs +140 -0
- data/crates/honeymaker/src/kraken/mod.rs +5 -0
- data/crates/honeymaker/src/kraken/normalize.rs +536 -0
- data/crates/honeymaker/src/kraken/requests.rs +286 -0
- data/crates/honeymaker/src/kraken/sign.rs +166 -0
- data/crates/honeymaker/src/kraken/trades.rs +201 -0
- data/crates/honeymaker/src/lib.rs +10 -0
- data/crates/honeymaker/src/num.rs +115 -0
- data/crates/honeymaker/src/semantics.rs +100 -0
- data/crates/honeymaker/src/types.rs +85 -0
- data/crates/honeymaker/tests/kraken_stage2.rs +592 -0
- data/crates/honeymaker/tests/kraken_trades_pager.rs +307 -0
- data/crates/honeymaker/tests/kraken_vectors.rs +78 -0
- data/crates/honeymaker/tests/vectors/decimal.json +146 -0
- data/crates/honeymaker/tests/vectors/kraken_add_order_wire.json +74 -0
- data/crates/honeymaker/tests/vectors/kraken_signing.json +3314 -0
- data/crates/honeymaker/tests/vectors/kraken_trades_pager.json +1583 -0
- data/crates/honeymaker-client/Cargo.toml +30 -0
- data/crates/honeymaker-client/examples/probe.rs +147 -0
- data/crates/honeymaker-client/src/kraken.rs +514 -0
- data/crates/honeymaker-client/src/lib.rs +4 -0
- data/crates/honeymaker-client/src/transport/connect.rs +583 -0
- data/crates/honeymaker-client/src/transport/io_timeout.rs +352 -0
- data/crates/honeymaker-client/src/transport/response.rs +169 -0
- data/crates/honeymaker-client/src/transport.rs +467 -0
- data/crates/honeymaker-client/tests/classification.rs +203 -0
- data/crates/honeymaker-client/tests/kraken_client.rs +949 -0
- data/crates/honeymaker-client/tests/response.rs +522 -0
- data/crates/honeymaker-client/tests/support/mod.rs +550 -0
- data/crates/honeymaker-client/tests/transport.rs +828 -0
- data/ext/honeymaker_native/Cargo.toml +16 -0
- data/ext/honeymaker_native/extconf.rb +6 -0
- data/ext/honeymaker_native/src/convert.rs +150 -0
- data/ext/honeymaker_native/src/kraken.rs +362 -0
- data/ext/honeymaker_native/src/lib.rs +90 -0
- data/ext/honeymaker_native/src/ruby_decimal.rs +90 -0
- data/lib/honeymaker/clients/bybit.rb +19 -2
- data/lib/honeymaker/clients/coinbase.rb +3 -2
- data/lib/honeymaker/clients/kraken.rb +6 -1
- data/lib/honeymaker/exchanges/coinbase.rb +5 -4
- data/lib/honeymaker/native/kraken.rb +263 -0
- data/lib/honeymaker/native.rb +42 -0
- data/lib/honeymaker/version.rb +1 -1
- data/lib/honeymaker.rb +12 -2
- data/script/parity/kraken_client_live.rb +191 -0
- data/script/parity/kraken_live.rb +70 -0
- data/script/parity/kraken_transport.rb +604 -0
- data/script/parity/resolve.c +49 -0
- data/script/release/push_gems.sh +28 -0
- data/script/vectors/kraken.rb +50 -0
- data/script/vectors/kraken_stage2.rb +146 -0
- data/test/fixtures/coinbase_products.json +1 -1
- data/test/honeymaker/clients/bybit_client_test.rb +25 -0
- data/test/honeymaker/clients/coinbase_client_test.rb +14 -0
- data/test/honeymaker/clients/kraken_client_test.rb +15 -0
- data/test/honeymaker/exchanges/coinbase_test.rb +22 -2
- data/test/native/backend_switch_test.rb +53 -0
- data/test/native/convert_test.rb +137 -0
- data/test/native/gem_isolation_test.rb +17 -0
- data/test/native/loader_test.rb +29 -0
- data/test/native/lockstep_test.rb +23 -0
- data/test/native/signing_smoke_test.rb +28 -0
- data/test/parity/kraken_client_parity_test.rb +398 -0
- data/test/parity/kraken_exchange_parity_test.rb +172 -0
- data/test/release/branch_hardening_test.rb +34 -0
- data/test/release/push_gems_test.rb +79 -0
- data/test/smoke.rb +24 -0
- data/test/support/kraken_signing_smoke.rb +21 -0
- data/test/support/parity_helper.rb +62 -0
- data/test/support/venue_double.rb +100 -0
- metadata +84 -4
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
//! Net::HTTP's read_timeout / write_timeout: a limit on each wait for the socket, not on the whole
|
|
2
|
+
//! exchange. A peer that keeps sending, however slowly, never times out.
|
|
3
|
+
use std::future::Future;
|
|
4
|
+
use std::io;
|
|
5
|
+
use std::pin::Pin;
|
|
6
|
+
use std::sync::atomic::{AtomicBool, Ordering::SeqCst};
|
|
7
|
+
use std::sync::{Arc, Mutex};
|
|
8
|
+
use std::task::{Context, Poll, Waker};
|
|
9
|
+
use std::time::Duration;
|
|
10
|
+
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
|
11
|
+
use tokio::time::{Sleep, sleep};
|
|
12
|
+
|
|
13
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
14
|
+
pub(crate) enum Stall {
|
|
15
|
+
Read,
|
|
16
|
+
Write,
|
|
17
|
+
}
|
|
18
|
+
impl std::fmt::Display for Stall {
|
|
19
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
20
|
+
f.write_str(match self {
|
|
21
|
+
Stall::Read => "Net::ReadTimeout",
|
|
22
|
+
Stall::Write => "Net::WriteTimeout",
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
impl std::error::Error for Stall {}
|
|
27
|
+
|
|
28
|
+
/// Shared across the HTTP body, the TLS stream, and the raw socket below TLS.
|
|
29
|
+
/// A body queued by hyper is not yet transmitted: only the subsequent successful
|
|
30
|
+
/// flush above TLS can enable response timeouts below TLS.
|
|
31
|
+
#[derive(Default)]
|
|
32
|
+
pub(crate) struct RequestProgress {
|
|
33
|
+
queued: AtomicBool,
|
|
34
|
+
flushed: AtomicBool,
|
|
35
|
+
reader: Mutex<Option<Waker>>,
|
|
36
|
+
}
|
|
37
|
+
impl RequestProgress {
|
|
38
|
+
pub(crate) fn body_queued(&self) {
|
|
39
|
+
self.queued.store(true, SeqCst);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn flushed(&self) {
|
|
43
|
+
let mut reader = self.reader.lock().unwrap();
|
|
44
|
+
if self.queued.load(SeqCst)
|
|
45
|
+
&& !self.flushed.swap(true, SeqCst)
|
|
46
|
+
&& let Some(waker) = reader.take()
|
|
47
|
+
{
|
|
48
|
+
waker.wake();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn can_read(&self, cx: &Context<'_>) -> bool {
|
|
53
|
+
let mut reader = self.reader.lock().unwrap();
|
|
54
|
+
if self.flushed.load(SeqCst) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
*reader = Some(cx.waker().clone());
|
|
58
|
+
false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Observes flush completion above TLS, after hyper has drained its HTTP buffer.
|
|
63
|
+
pub(crate) struct RequestIo<S> {
|
|
64
|
+
inner: S,
|
|
65
|
+
progress: Arc<RequestProgress>,
|
|
66
|
+
wrote: bool,
|
|
67
|
+
}
|
|
68
|
+
impl<S> RequestIo<S> {
|
|
69
|
+
pub(crate) fn new(inner: S, progress: Arc<RequestProgress>) -> Self {
|
|
70
|
+
Self {
|
|
71
|
+
inner,
|
|
72
|
+
progress,
|
|
73
|
+
wrote: false,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
impl<S: AsyncRead + Unpin> AsyncRead for RequestIo<S> {
|
|
78
|
+
fn poll_read(
|
|
79
|
+
self: Pin<&mut Self>,
|
|
80
|
+
cx: &mut Context<'_>,
|
|
81
|
+
buf: &mut ReadBuf<'_>,
|
|
82
|
+
) -> Poll<io::Result<()>> {
|
|
83
|
+
Pin::new(&mut self.get_mut().inner).poll_read(cx, buf)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
impl<S: AsyncWrite + Unpin> AsyncWrite for RequestIo<S> {
|
|
87
|
+
fn poll_write(
|
|
88
|
+
self: Pin<&mut Self>,
|
|
89
|
+
cx: &mut Context<'_>,
|
|
90
|
+
buf: &[u8],
|
|
91
|
+
) -> Poll<io::Result<usize>> {
|
|
92
|
+
let this = self.get_mut();
|
|
93
|
+
let result = Pin::new(&mut this.inner).poll_write(cx, buf);
|
|
94
|
+
if matches!(result, Poll::Ready(Ok(n)) if n > 0) {
|
|
95
|
+
this.wrote = true;
|
|
96
|
+
}
|
|
97
|
+
result
|
|
98
|
+
}
|
|
99
|
+
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
100
|
+
let this = self.get_mut();
|
|
101
|
+
let result = Pin::new(&mut this.inner).poll_flush(cx);
|
|
102
|
+
if this.wrote && matches!(result, Poll::Ready(Ok(()))) {
|
|
103
|
+
this.progress.flushed();
|
|
104
|
+
}
|
|
105
|
+
result
|
|
106
|
+
}
|
|
107
|
+
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
108
|
+
Pin::new(&mut self.get_mut().inner).poll_shutdown(cx)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pub(crate) struct TimeoutIo<S> {
|
|
113
|
+
inner: S,
|
|
114
|
+
progress: Option<Arc<RequestProgress>>,
|
|
115
|
+
read: Duration,
|
|
116
|
+
write: Duration,
|
|
117
|
+
read_timer: Option<Pin<Box<Sleep>>>,
|
|
118
|
+
write_timer: Option<Pin<Box<Sleep>>>,
|
|
119
|
+
/// A write is waiting on the socket: the request is still being transmitted.
|
|
120
|
+
writing: bool,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
impl<S> TimeoutIo<S> {
|
|
124
|
+
pub(crate) fn new(inner: S, read: Duration, write: Duration) -> Self {
|
|
125
|
+
Self {
|
|
126
|
+
inner,
|
|
127
|
+
progress: None,
|
|
128
|
+
read,
|
|
129
|
+
write,
|
|
130
|
+
read_timer: None,
|
|
131
|
+
write_timer: None,
|
|
132
|
+
writing: false,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
pub(crate) fn for_request(
|
|
137
|
+
inner: S,
|
|
138
|
+
read: Duration,
|
|
139
|
+
write: Duration,
|
|
140
|
+
progress: Arc<RequestProgress>,
|
|
141
|
+
) -> Self {
|
|
142
|
+
Self {
|
|
143
|
+
progress: Some(progress),
|
|
144
|
+
..Self::new(inner, read, write)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// A write finished. If it moved bytes (or ends a wait), transmission progressed, so the
|
|
149
|
+
/// response wait starts afresh from now: Net::HTTP only starts reading once it has written.
|
|
150
|
+
/// The existing Sleep is reset rather than dropped, so it keeps the reader's waker.
|
|
151
|
+
fn wrote(&mut self, progressed: bool) {
|
|
152
|
+
self.write_timer = None;
|
|
153
|
+
if (progressed || self.writing)
|
|
154
|
+
&& let Some(t) = self.read_timer.as_mut()
|
|
155
|
+
{
|
|
156
|
+
t.as_mut().reset(tokio::time::Instant::now() + self.read);
|
|
157
|
+
}
|
|
158
|
+
self.writing = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fn stalled<T>(
|
|
163
|
+
timer: &mut Option<Pin<Box<Sleep>>>,
|
|
164
|
+
limit: Duration,
|
|
165
|
+
which: Stall,
|
|
166
|
+
cx: &mut Context<'_>,
|
|
167
|
+
) -> Poll<io::Result<T>> {
|
|
168
|
+
let t = timer.get_or_insert_with(|| Box::pin(sleep(limit)));
|
|
169
|
+
match t.as_mut().poll(cx) {
|
|
170
|
+
Poll::Ready(()) => {
|
|
171
|
+
*timer = None;
|
|
172
|
+
Poll::Ready(Err(io::Error::new(io::ErrorKind::TimedOut, which)))
|
|
173
|
+
}
|
|
174
|
+
Poll::Pending => Poll::Pending,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
impl<S: AsyncRead + Unpin> AsyncRead for TimeoutIo<S> {
|
|
179
|
+
fn poll_read(
|
|
180
|
+
self: Pin<&mut Self>,
|
|
181
|
+
cx: &mut Context<'_>,
|
|
182
|
+
buf: &mut ReadBuf<'_>,
|
|
183
|
+
) -> Poll<io::Result<()>> {
|
|
184
|
+
let this = self.get_mut();
|
|
185
|
+
match Pin::new(&mut this.inner).poll_read(cx, buf) {
|
|
186
|
+
Poll::Ready(r) => {
|
|
187
|
+
this.read_timer = None;
|
|
188
|
+
Poll::Ready(r)
|
|
189
|
+
}
|
|
190
|
+
// While the request is still going out, a read wait is not a response wait (hyper
|
|
191
|
+
// polls reads alongside writes); the write timer governs until the write completes.
|
|
192
|
+
Poll::Pending if this.progress.as_ref().is_some_and(|p| !p.can_read(cx)) => {
|
|
193
|
+
Poll::Pending
|
|
194
|
+
}
|
|
195
|
+
Poll::Pending if this.writing => Poll::Pending,
|
|
196
|
+
Poll::Pending => stalled(&mut this.read_timer, this.read, Stall::Read, cx),
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
impl<S: AsyncWrite + Unpin> TimeoutIo<S> {
|
|
202
|
+
fn write_side<T>(
|
|
203
|
+
&mut self,
|
|
204
|
+
cx: &mut Context<'_>,
|
|
205
|
+
poll: Poll<io::Result<T>>,
|
|
206
|
+
progressed: impl FnOnce(&T) -> bool,
|
|
207
|
+
) -> Poll<io::Result<T>> {
|
|
208
|
+
match poll {
|
|
209
|
+
Poll::Ready(Ok(v)) => {
|
|
210
|
+
self.wrote(progressed(&v));
|
|
211
|
+
Poll::Ready(Ok(v))
|
|
212
|
+
}
|
|
213
|
+
Poll::Ready(Err(e)) => {
|
|
214
|
+
self.write_timer = None;
|
|
215
|
+
self.writing = false;
|
|
216
|
+
Poll::Ready(Err(e))
|
|
217
|
+
}
|
|
218
|
+
Poll::Pending => {
|
|
219
|
+
self.writing = true;
|
|
220
|
+
stalled(&mut self.write_timer, self.write, Stall::Write, cx)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
impl<S: AsyncWrite + Unpin> AsyncWrite for TimeoutIo<S> {
|
|
227
|
+
fn poll_write(
|
|
228
|
+
self: Pin<&mut Self>,
|
|
229
|
+
cx: &mut Context<'_>,
|
|
230
|
+
buf: &[u8],
|
|
231
|
+
) -> Poll<io::Result<usize>> {
|
|
232
|
+
let this = self.get_mut();
|
|
233
|
+
let poll = Pin::new(&mut this.inner).poll_write(cx, buf);
|
|
234
|
+
this.write_side(cx, poll, |n| *n > 0)
|
|
235
|
+
}
|
|
236
|
+
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
237
|
+
let this = self.get_mut();
|
|
238
|
+
let poll = Pin::new(&mut this.inner).poll_flush(cx);
|
|
239
|
+
this.write_side(cx, poll, |_| false)
|
|
240
|
+
}
|
|
241
|
+
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
|
242
|
+
let this = self.get_mut();
|
|
243
|
+
let poll = Pin::new(&mut this.inner).poll_shutdown(cx);
|
|
244
|
+
this.write_side(cx, poll, |_| false)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
#[cfg(test)]
|
|
249
|
+
mod tests {
|
|
250
|
+
use super::*;
|
|
251
|
+
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
|
252
|
+
|
|
253
|
+
fn stall_of(e: &io::Error) -> Option<Stall> {
|
|
254
|
+
e.get_ref().and_then(|x| x.downcast_ref::<Stall>()).copied()
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[tokio::test(start_paused = true)]
|
|
258
|
+
async fn a_silent_peer_times_out_after_the_read_limit() {
|
|
259
|
+
let (a, _b) = duplex(64);
|
|
260
|
+
let mut io = TimeoutIo::new(a, Duration::from_secs(30), Duration::from_secs(10));
|
|
261
|
+
let started = tokio::time::Instant::now();
|
|
262
|
+
let e = io.read(&mut [0u8; 8]).await.unwrap_err();
|
|
263
|
+
assert_eq!(
|
|
264
|
+
(e.kind(), stall_of(&e)),
|
|
265
|
+
(io::ErrorKind::TimedOut, Some(Stall::Read))
|
|
266
|
+
);
|
|
267
|
+
assert_eq!(started.elapsed(), Duration::from_secs(30));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#[tokio::test(start_paused = true)]
|
|
271
|
+
async fn a_slow_but_steady_peer_never_times_out() {
|
|
272
|
+
let (a, mut b) = duplex(64);
|
|
273
|
+
tokio::spawn(async move {
|
|
274
|
+
for _ in 0..5 {
|
|
275
|
+
sleep(Duration::from_secs(29)).await;
|
|
276
|
+
b.write_all(b"x").await.unwrap();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
let mut io = TimeoutIo::new(a, Duration::from_secs(30), Duration::from_secs(10));
|
|
280
|
+
let mut buf = [0u8; 5];
|
|
281
|
+
io.read_exact(&mut buf).await.unwrap();
|
|
282
|
+
assert_eq!(&buf, b"xxxxx");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[tokio::test(start_paused = true)]
|
|
286
|
+
async fn write_progress_restarts_the_response_wait() {
|
|
287
|
+
let (a, _b) = duplex(64);
|
|
288
|
+
let io = TimeoutIo::new(a, Duration::from_secs(30), Duration::from_secs(10));
|
|
289
|
+
let (mut r, mut w) = tokio::io::split(io);
|
|
290
|
+
let started = tokio::time::Instant::now();
|
|
291
|
+
let reader = async { r.read(&mut [0u8; 8]).await.unwrap_err() };
|
|
292
|
+
let writer = async {
|
|
293
|
+
sleep(Duration::from_secs(20)).await;
|
|
294
|
+
w.write_all(b"x").await.unwrap(); // the request's last byte goes out at 20 s
|
|
295
|
+
};
|
|
296
|
+
let (e, ()) = tokio::join!(reader, writer);
|
|
297
|
+
assert_eq!(stall_of(&e), Some(Stall::Read));
|
|
298
|
+
assert_eq!(
|
|
299
|
+
started.elapsed(),
|
|
300
|
+
Duration::from_secs(50),
|
|
301
|
+
"30 s after the last write, not after the first read"
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#[tokio::test(start_paused = true)]
|
|
306
|
+
async fn a_peer_that_stops_reading_times_out_the_write() {
|
|
307
|
+
let (a, _b) = duplex(8);
|
|
308
|
+
let mut io = TimeoutIo::new(a, Duration::from_secs(30), Duration::from_secs(10));
|
|
309
|
+
let e = io.write_all(&[0u8; 64]).await.unwrap_err();
|
|
310
|
+
assert_eq!(stall_of(&e), Some(Stall::Write));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#[cfg(test)]
|
|
315
|
+
mod completion_tests {
|
|
316
|
+
use super::*;
|
|
317
|
+
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
|
318
|
+
|
|
319
|
+
#[tokio::test(start_paused = true)]
|
|
320
|
+
async fn response_wait_starts_only_after_the_complete_request_is_flushed() {
|
|
321
|
+
let (a, _peer) = duplex(64);
|
|
322
|
+
let progress = Arc::new(RequestProgress::default());
|
|
323
|
+
let raw = TimeoutIo::for_request(
|
|
324
|
+
a,
|
|
325
|
+
Duration::from_secs(30),
|
|
326
|
+
Duration::from_secs(10),
|
|
327
|
+
progress.clone(),
|
|
328
|
+
);
|
|
329
|
+
let io = RequestIo::new(raw, progress.clone());
|
|
330
|
+
let (mut reader, mut writer) = tokio::io::split(io);
|
|
331
|
+
let started = tokio::time::Instant::now();
|
|
332
|
+
let read = async { reader.read(&mut [0; 1]).await.unwrap_err() };
|
|
333
|
+
let write = async {
|
|
334
|
+
// An initial flush and a partial request must not enable the response timer.
|
|
335
|
+
writer.flush().await.unwrap();
|
|
336
|
+
writer.write_all(b"head").await.unwrap();
|
|
337
|
+
writer.flush().await.unwrap();
|
|
338
|
+
sleep(Duration::from_secs(40)).await;
|
|
339
|
+
progress.body_queued();
|
|
340
|
+
writer.write_all(b"body").await.unwrap();
|
|
341
|
+
// Even when all plaintext is accepted, wait until TLS has flushed it.
|
|
342
|
+
sleep(Duration::from_secs(40)).await;
|
|
343
|
+
writer.flush().await.unwrap();
|
|
344
|
+
};
|
|
345
|
+
let (err, ()) = tokio::join!(read, write);
|
|
346
|
+
assert_eq!(
|
|
347
|
+
err.get_ref().unwrap().downcast_ref::<Stall>(),
|
|
348
|
+
Some(&Stall::Read)
|
|
349
|
+
);
|
|
350
|
+
assert_eq!(started.elapsed(), Duration::from_secs(110));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
//! What the gem's Net::HTTP + Faraday stack turns an HTTP answer into (Ruling R6).
|
|
2
|
+
use super::{BodyFraming, Cause, Failure, Raw, Reply};
|
|
3
|
+
use flate2::read::{GzDecoder, ZlibDecoder};
|
|
4
|
+
use serde::de::{Deserialize, Deserializer, Visitor};
|
|
5
|
+
use std::collections::HashSet;
|
|
6
|
+
use std::fmt;
|
|
7
|
+
use std::io::Read;
|
|
8
|
+
|
|
9
|
+
/// Faraday::Response::Json#process_response_type?: the media type before `;`, unstripped and
|
|
10
|
+
/// case-sensitive, matched against /\bjson$/.
|
|
11
|
+
fn json_type(content_type: Option<&str>) -> bool {
|
|
12
|
+
let t = content_type.unwrap_or("").split(';').next().unwrap_or("");
|
|
13
|
+
t.strip_suffix("json").is_some_and(|head| {
|
|
14
|
+
!head
|
|
15
|
+
.chars()
|
|
16
|
+
.last()
|
|
17
|
+
.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_')
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Ruby String#strip: ASCII whitespace and NUL.
|
|
22
|
+
fn blank(b: &[u8]) -> bool {
|
|
23
|
+
b.iter()
|
|
24
|
+
.all(|c| matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r' | 0))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Net::HTTPResponse#inflater: gzip, x-gzip and deflate (any case) are inflated unless a
|
|
28
|
+
/// Content-Range is present; Zlib::Inflate.new(32 + MAX_WBITS) auto-detects a zlib or gzip header,
|
|
29
|
+
/// so both are accepted for each. Recognized encodings are removed even on empty bodies.
|
|
30
|
+
/// A Zlib error escaping Faraday's adapter is a client_error, post-send.
|
|
31
|
+
pub fn decode(mut raw: Raw) -> Result<Raw, Failure> {
|
|
32
|
+
let enc = raw.content_encoding.as_deref().map(str::to_ascii_lowercase);
|
|
33
|
+
if raw.content_range {
|
|
34
|
+
return Ok(raw);
|
|
35
|
+
}
|
|
36
|
+
match enc.as_deref() {
|
|
37
|
+
Some("gzip" | "x-gzip" | "deflate") => raw.content_encoding = None,
|
|
38
|
+
Some("none" | "identity") => {
|
|
39
|
+
raw.content_encoding = None;
|
|
40
|
+
return Ok(raw);
|
|
41
|
+
}
|
|
42
|
+
_ => return Ok(raw),
|
|
43
|
+
}
|
|
44
|
+
if raw.body.is_empty() {
|
|
45
|
+
return Ok(raw);
|
|
46
|
+
}
|
|
47
|
+
let mut out = Vec::new();
|
|
48
|
+
let result = if raw.body.starts_with(&[0x1f, 0x8b]) {
|
|
49
|
+
GzDecoder::new(&raw.body[..]).read_to_end(&mut out)
|
|
50
|
+
} else {
|
|
51
|
+
ZlibDecoder::new(&raw.body[..]).read_to_end(&mut out)
|
|
52
|
+
};
|
|
53
|
+
if let Err(e) = result {
|
|
54
|
+
if e.kind() != std::io::ErrorKind::UnexpectedEof {
|
|
55
|
+
return Err(Failure::new(Cause::Other, format!("Zlib::DataError: {e}")));
|
|
56
|
+
}
|
|
57
|
+
// net-http 0.9.1 read_body_0 returns from inside the inflater block for length/chunked
|
|
58
|
+
// bodies. Its ensure then suppresses finish's error because `success` was never set.
|
|
59
|
+
if raw.framing == BodyFraming::CloseDelimited {
|
|
60
|
+
return Err(Failure::new(Cause::Other, "Zlib::BufError: buffer error"));
|
|
61
|
+
}
|
|
62
|
+
// Ruby's block-form Inflate#inflate yields full 16 KiB buffers, flushing the remainder
|
|
63
|
+
// only on stream end. An incomplete stream never delivers that pending remainder.
|
|
64
|
+
const RUBY_ZLIB_BUFFER: usize = 16 * 1024;
|
|
65
|
+
out.truncate(out.len() / RUBY_ZLIB_BUFFER * RUBY_ZLIB_BUFFER);
|
|
66
|
+
}
|
|
67
|
+
raw.body = out.into();
|
|
68
|
+
Ok(raw)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub fn shape(method: &str, url: &str, raw: Raw) -> Reply {
|
|
72
|
+
let text = || String::from_utf8_lossy(&raw.body).into_owned();
|
|
73
|
+
if (400..=599).contains(&raw.status) {
|
|
74
|
+
let message = if raw.body.is_empty() {
|
|
75
|
+
if raw.status == 407 {
|
|
76
|
+
"407 \"Proxy Authentication Required\"".into()
|
|
77
|
+
} else {
|
|
78
|
+
format!(
|
|
79
|
+
"the server responded with status {} for {} {url}",
|
|
80
|
+
raw.status,
|
|
81
|
+
method.to_uppercase()
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
text()
|
|
86
|
+
};
|
|
87
|
+
return Reply::Status {
|
|
88
|
+
status: raw.status,
|
|
89
|
+
message,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if !json_type(raw.content_type.as_deref()) || blank(&raw.body) {
|
|
93
|
+
return Reply::NotJson;
|
|
94
|
+
}
|
|
95
|
+
match parse_strict(&raw.body) {
|
|
96
|
+
Some(v) => Reply::Parsed(v),
|
|
97
|
+
None => Reply::Status {
|
|
98
|
+
status: raw.status,
|
|
99
|
+
message: text(),
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// JSON 3.0.2 accepts invalid UTF-8 in strings. Rust values use replacement characters;
|
|
105
|
+
/// downstream interpretation uses ASCII tokens. Syntax (including escapes and surrogates)
|
|
106
|
+
/// remains strict, and duplicate keys are compared as unescaped bytes before lossy conversion.
|
|
107
|
+
fn parse_strict(body: &[u8]) -> Option<serde_json::Value> {
|
|
108
|
+
let text = String::from_utf8_lossy(body);
|
|
109
|
+
let value = serde_json::from_str(&text).ok()?;
|
|
110
|
+
validate_structure(body)?;
|
|
111
|
+
Some(value)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// The lossy parse has already checked grammar. Walk the original tokens to check byte-key
|
|
115
|
+
/// identity and depth: distinct invalid keys can collapse in Value, hiding keys or subtrees.
|
|
116
|
+
fn validate_structure(mut body: &[u8]) -> Option<()> {
|
|
117
|
+
let mut containers: Vec<Option<HashSet<Vec<u8>>>> = Vec::new();
|
|
118
|
+
while let Some((&token, rest)) = body.split_first() {
|
|
119
|
+
match token {
|
|
120
|
+
b'{' | b'[' => {
|
|
121
|
+
containers.push((token == b'{').then(HashSet::new));
|
|
122
|
+
// json 3.0.2 handles empty containers before incrementing current_nesting.
|
|
123
|
+
let closing = if token == b'{' { b'}' } else { b']' };
|
|
124
|
+
if containers.len() > 100
|
|
125
|
+
&& rest.iter().find(|b| !b.is_ascii_whitespace()) != Some(&closing)
|
|
126
|
+
{
|
|
127
|
+
return None;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
b'}' | b']' => {
|
|
131
|
+
containers.pop()?;
|
|
132
|
+
}
|
|
133
|
+
b'"' => {
|
|
134
|
+
let mut strings =
|
|
135
|
+
serde_json::Deserializer::from_slice(body).into_iter::<ByteString>();
|
|
136
|
+
let string = strings.next()?.ok()?.0;
|
|
137
|
+
body = &body[strings.byte_offset()..];
|
|
138
|
+
if body.iter().find(|b| !b.is_ascii_whitespace()) == Some(&b':')
|
|
139
|
+
&& !containers.last_mut()?.as_mut()?.insert(string)
|
|
140
|
+
{
|
|
141
|
+
return None;
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
_ => {}
|
|
146
|
+
}
|
|
147
|
+
body = rest;
|
|
148
|
+
}
|
|
149
|
+
Some(())
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// serde's byte-string decoder retains invalid UTF-8 and unescapes keys for comparison.
|
|
153
|
+
/// Its permissive escape handling is safe only after the strict string parse above.
|
|
154
|
+
struct ByteString(Vec<u8>);
|
|
155
|
+
impl<'de> Deserialize<'de> for ByteString {
|
|
156
|
+
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
|
|
157
|
+
d.deserialize_bytes(ByteStringVisitor)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
struct ByteStringVisitor;
|
|
161
|
+
impl<'de> Visitor<'de> for ByteStringVisitor {
|
|
162
|
+
type Value = ByteString;
|
|
163
|
+
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
164
|
+
f.write_str("a JSON string")
|
|
165
|
+
}
|
|
166
|
+
fn visit_bytes<E>(self, bytes: &[u8]) -> Result<Self::Value, E> {
|
|
167
|
+
Ok(ByteString(bytes.to_vec()))
|
|
168
|
+
}
|
|
169
|
+
}
|