honeymaker 0.12.2 → 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 +17 -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 +27 -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,550 @@
|
|
|
1
|
+
#![allow(dead_code)]
|
|
2
|
+
//! Local doubles for the transport tests: a TLS PKI, a scriptable target, an HTTP CONNECT proxy.
|
|
3
|
+
//! Each double counts connections and the request bytes it received, so a test can prove that a
|
|
4
|
+
//! failure classed as pre-transmission really sent nothing.
|
|
5
|
+
use rcgen::{BasicConstraints, CertificateParams, IsCa, KeyPair};
|
|
6
|
+
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
|
|
7
|
+
use std::net::SocketAddr;
|
|
8
|
+
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
|
|
9
|
+
use std::sync::{Arc, Mutex};
|
|
10
|
+
use std::time::Duration;
|
|
11
|
+
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
|
12
|
+
use tokio::net::{TcpListener, TcpStream};
|
|
13
|
+
use tokio_rustls::TlsAcceptor;
|
|
14
|
+
|
|
15
|
+
pub struct Pki {
|
|
16
|
+
pub roots: Arc<rustls::RootCertStore>,
|
|
17
|
+
pub trusted: TlsAcceptor,
|
|
18
|
+
pub untrusted: TlsAcceptor,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fn provider() -> Arc<rustls::crypto::CryptoProvider> {
|
|
22
|
+
Arc::new(rustls::crypto::ring::default_provider())
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// A CA and a leaf for localhost/127.0.0.1 signed by it.
|
|
26
|
+
fn issue() -> (
|
|
27
|
+
CertificateDer<'static>,
|
|
28
|
+
CertificateDer<'static>,
|
|
29
|
+
PrivateKeyDer<'static>,
|
|
30
|
+
) {
|
|
31
|
+
let ca_key = KeyPair::generate().unwrap();
|
|
32
|
+
let mut ca = CertificateParams::new(Vec::<String>::new()).unwrap();
|
|
33
|
+
ca.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
|
34
|
+
let ca_cert = ca.self_signed(&ca_key).unwrap();
|
|
35
|
+
let leaf_key = KeyPair::generate().unwrap();
|
|
36
|
+
let leaf = CertificateParams::new(vec!["localhost".to_string(), "127.0.0.1".to_string()])
|
|
37
|
+
.unwrap()
|
|
38
|
+
.signed_by(&leaf_key, &ca_cert, &ca_key)
|
|
39
|
+
.unwrap();
|
|
40
|
+
let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(leaf_key.serialize_der()));
|
|
41
|
+
(ca_cert.der().clone(), leaf.der().clone(), key)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn acceptor(leaf: CertificateDer<'static>, key: PrivateKeyDer<'static>) -> TlsAcceptor {
|
|
45
|
+
let cfg = rustls::ServerConfig::builder_with_provider(provider())
|
|
46
|
+
.with_safe_default_protocol_versions()
|
|
47
|
+
.unwrap()
|
|
48
|
+
.with_no_client_auth()
|
|
49
|
+
.with_single_cert(vec![leaf], key)
|
|
50
|
+
.unwrap();
|
|
51
|
+
TlsAcceptor::from(Arc::new(cfg))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pub fn pki() -> Pki {
|
|
55
|
+
let (ca, leaf, key) = issue();
|
|
56
|
+
let (_unknown_ca, bad_leaf, bad_key) = issue();
|
|
57
|
+
let mut roots = rustls::RootCertStore::empty();
|
|
58
|
+
roots.add(ca).unwrap();
|
|
59
|
+
Pki {
|
|
60
|
+
roots: Arc::new(roots),
|
|
61
|
+
trusted: acceptor(leaf, key),
|
|
62
|
+
untrusted: acceptor(bad_leaf, bad_key),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[derive(Clone, Default)]
|
|
67
|
+
pub struct Seen {
|
|
68
|
+
conns: Arc<AtomicUsize>,
|
|
69
|
+
request_bytes: Arc<AtomicUsize>,
|
|
70
|
+
heads: Arc<Mutex<Vec<String>>>,
|
|
71
|
+
/// Set once a whole request has been read: every byte the double writes after it is the reply.
|
|
72
|
+
answering: Arc<std::sync::atomic::AtomicBool>,
|
|
73
|
+
times: Arc<Mutex<Times>>,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// When the double first got request bytes, when the request was complete, and when it answered.
|
|
77
|
+
#[derive(Clone, Copy, Debug, Default)]
|
|
78
|
+
pub struct Times {
|
|
79
|
+
pub first_byte: Option<std::time::Instant>,
|
|
80
|
+
pub request_done: Option<std::time::Instant>,
|
|
81
|
+
pub replied: Option<std::time::Instant>,
|
|
82
|
+
}
|
|
83
|
+
impl Seen {
|
|
84
|
+
pub fn conns(&self) -> usize {
|
|
85
|
+
self.conns.load(SeqCst)
|
|
86
|
+
}
|
|
87
|
+
pub fn request_bytes(&self) -> usize {
|
|
88
|
+
self.request_bytes.load(SeqCst)
|
|
89
|
+
}
|
|
90
|
+
/// Each request's head and body as text, lowercased (header names are case-insensitive).
|
|
91
|
+
pub fn heads(&self) -> Vec<String> {
|
|
92
|
+
self.heads.lock().unwrap().clone()
|
|
93
|
+
}
|
|
94
|
+
pub fn times(&self) -> Times {
|
|
95
|
+
*self.times.lock().unwrap()
|
|
96
|
+
}
|
|
97
|
+
fn mark(&self, f: impl FnOnce(&mut Times)) {
|
|
98
|
+
f(&mut self.times.lock().unwrap())
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pub struct Double {
|
|
103
|
+
pub addr: SocketAddr,
|
|
104
|
+
pub seen: Seen,
|
|
105
|
+
}
|
|
106
|
+
impl Double {
|
|
107
|
+
pub fn https(&self) -> String {
|
|
108
|
+
format!("https://127.0.0.1:{}", self.addr.port())
|
|
109
|
+
}
|
|
110
|
+
pub fn https_localhost(&self) -> String {
|
|
111
|
+
format!("https://localhost:{}", self.addr.port())
|
|
112
|
+
}
|
|
113
|
+
pub fn http(&self) -> String {
|
|
114
|
+
format!("http://127.0.0.1:{}", self.addr.port())
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// A raw HTTP/1.1 response with a correct Content-Length.
|
|
119
|
+
pub fn http(status: &str, content_type: &str, body: &str) -> &'static str {
|
|
120
|
+
Box::leak(
|
|
121
|
+
format!("HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len())
|
|
122
|
+
.into_boxed_str(),
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
pub const OK_JSON: &str = r#"{"error":[],"result":{"a":1}}"#;
|
|
127
|
+
|
|
128
|
+
#[derive(Clone, Copy)]
|
|
129
|
+
pub enum Tls {
|
|
130
|
+
Plain,
|
|
131
|
+
Trusted,
|
|
132
|
+
Untrusted,
|
|
133
|
+
NeverHandshake,
|
|
134
|
+
#[allow(clippy::enum_variant_names)] // Name prescribed by the shared test-double interface.
|
|
135
|
+
NotTls,
|
|
136
|
+
/// Trusted TLS, but the encrypted reply reaches the socket as the relay decides.
|
|
137
|
+
Relayed(Relay),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// How the encrypted bytes of the reply are passed to the client's socket.
|
|
141
|
+
#[derive(Clone, Copy)]
|
|
142
|
+
pub enum Relay {
|
|
143
|
+
/// `chunk` bytes at a time, `ms` apart: records arrive fragmented but keep progressing.
|
|
144
|
+
Trickle { chunk: usize, ms: u64 },
|
|
145
|
+
/// The first `n` encrypted bytes of the reply, then nothing: a stall inside a record.
|
|
146
|
+
StallAfter(usize),
|
|
147
|
+
/// The client's raw TCP bytes are taken `chunk` at a time, `ms` apart, until `budget` bytes have
|
|
148
|
+
/// passed; then the rest drains at full speed and the reply goes out untouched. Transmission
|
|
149
|
+
/// keeps progressing for a known time, whatever chunk sizes TLS produces above it.
|
|
150
|
+
ThrottleUpload {
|
|
151
|
+
chunk: usize,
|
|
152
|
+
ms: u64,
|
|
153
|
+
budget: usize,
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#[derive(Clone, Copy)]
|
|
158
|
+
pub enum Act {
|
|
159
|
+
Reply(&'static str),
|
|
160
|
+
/// Read the request, then say nothing.
|
|
161
|
+
Hang,
|
|
162
|
+
/// Read the request, then close (FIN; under TLS without close_notify).
|
|
163
|
+
Close,
|
|
164
|
+
/// Read the request, then reset (RST).
|
|
165
|
+
Reset,
|
|
166
|
+
/// Read this many request bytes, then stop reading (the client's writes back up).
|
|
167
|
+
ReadSomeThenStop(usize),
|
|
168
|
+
/// A 200 promising 50 bytes, 4 sent, then close.
|
|
169
|
+
Partial,
|
|
170
|
+
/// The reply in 4 pieces with this many milliseconds between them.
|
|
171
|
+
Trickle(&'static str, u64),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
pub async fn closed_port() -> u16 {
|
|
175
|
+
TcpListener::bind("127.0.0.1:0")
|
|
176
|
+
.await
|
|
177
|
+
.unwrap()
|
|
178
|
+
.local_addr()
|
|
179
|
+
.unwrap()
|
|
180
|
+
.port()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fn find(buf: &[u8], pat: &[u8]) -> Option<usize> {
|
|
184
|
+
buf.windows(pat.len()).position(|w| w == pat)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// The request's head (original case) and body; `seen.heads` gets both, lowercased.
|
|
188
|
+
pub(crate) async fn read_request<S: AsyncRead + Unpin>(
|
|
189
|
+
s: &mut S,
|
|
190
|
+
seen: &Seen,
|
|
191
|
+
) -> Option<(String, String)> {
|
|
192
|
+
let mut buf = Vec::new();
|
|
193
|
+
let mut chunk = [0u8; 4096];
|
|
194
|
+
let head_end = loop {
|
|
195
|
+
if let Some(i) = find(&buf, b"\r\n\r\n") {
|
|
196
|
+
break i + 4;
|
|
197
|
+
}
|
|
198
|
+
let n = s.read(&mut chunk).await.ok()?;
|
|
199
|
+
if n == 0 {
|
|
200
|
+
return None;
|
|
201
|
+
}
|
|
202
|
+
seen.mark(|t| {
|
|
203
|
+
t.first_byte.get_or_insert_with(std::time::Instant::now);
|
|
204
|
+
});
|
|
205
|
+
seen.request_bytes.fetch_add(n, SeqCst);
|
|
206
|
+
buf.extend_from_slice(&chunk[..n]);
|
|
207
|
+
};
|
|
208
|
+
let head = String::from_utf8_lossy(&buf[..head_end]).into_owned();
|
|
209
|
+
let len = head
|
|
210
|
+
.to_ascii_lowercase()
|
|
211
|
+
.lines()
|
|
212
|
+
.find_map(|l| {
|
|
213
|
+
l.strip_prefix("content-length:")
|
|
214
|
+
.map(|v| v.trim().parse::<usize>().unwrap_or(0))
|
|
215
|
+
})
|
|
216
|
+
.unwrap_or(0);
|
|
217
|
+
while buf.len() < head_end + len {
|
|
218
|
+
let n = s.read(&mut chunk).await.ok()?;
|
|
219
|
+
if n == 0 {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
seen.request_bytes.fetch_add(n, SeqCst);
|
|
223
|
+
buf.extend_from_slice(&chunk[..n]);
|
|
224
|
+
}
|
|
225
|
+
let body = String::from_utf8_lossy(&buf[head_end..]).into_owned();
|
|
226
|
+
seen.heads
|
|
227
|
+
.lock()
|
|
228
|
+
.unwrap()
|
|
229
|
+
.push(format!("{head}{body}").to_ascii_lowercase());
|
|
230
|
+
seen.mark(|t| t.request_done = Some(std::time::Instant::now()));
|
|
231
|
+
seen.answering.store(true, SeqCst);
|
|
232
|
+
Some((head, body))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async fn serve<S: AsyncRead + AsyncWrite + Unpin>(mut s: S, act: Act, seen: &Seen) {
|
|
236
|
+
if let Act::ReadSomeThenStop(n) = act {
|
|
237
|
+
let mut buf = vec![0u8; n];
|
|
238
|
+
if s.read_exact(&mut buf).await.is_ok() {
|
|
239
|
+
seen.request_bytes.fetch_add(n, SeqCst);
|
|
240
|
+
}
|
|
241
|
+
tokio::time::sleep(Duration::from_secs(30)).await;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if read_request(&mut s, seen).await.is_none() {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
match act {
|
|
248
|
+
Act::Reply(raw) => {
|
|
249
|
+
let _ = s.write_all(raw.as_bytes()).await;
|
|
250
|
+
seen.mark(|t| t.replied = Some(std::time::Instant::now()));
|
|
251
|
+
let _ = s.shutdown().await;
|
|
252
|
+
}
|
|
253
|
+
Act::Hang => tokio::time::sleep(Duration::from_secs(30)).await,
|
|
254
|
+
Act::Close | Act::Reset | Act::ReadSomeThenStop(_) => {}
|
|
255
|
+
Act::Partial => {
|
|
256
|
+
let _ = s
|
|
257
|
+
.write_all(b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 50\r\n\r\n{\"a\"")
|
|
258
|
+
.await;
|
|
259
|
+
}
|
|
260
|
+
Act::Trickle(raw, ms) => {
|
|
261
|
+
let step = raw.len().div_ceil(4);
|
|
262
|
+
for piece in raw.as_bytes().chunks(step) {
|
|
263
|
+
tokio::time::sleep(Duration::from_millis(ms)).await;
|
|
264
|
+
if s.write_all(piece).await.is_err() {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
let _ = s.shutdown().await;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
pub async fn target(tls: Tls, act: Act, pki: &Pki) -> Double {
|
|
274
|
+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
275
|
+
let addr = listener.local_addr().unwrap();
|
|
276
|
+
let seen = Seen::default();
|
|
277
|
+
let (all, trusted, untrusted) = (seen.clone(), pki.trusted.clone(), pki.untrusted.clone());
|
|
278
|
+
tokio::spawn(async move {
|
|
279
|
+
while let Ok((tcp, _)) = listener.accept().await {
|
|
280
|
+
all.conns.fetch_add(1, SeqCst);
|
|
281
|
+
let (seen, trusted, untrusted) = (all.clone(), trusted.clone(), untrusted.clone());
|
|
282
|
+
tokio::spawn(async move {
|
|
283
|
+
if matches!(act, Act::Reset) {
|
|
284
|
+
#[allow(deprecated)]
|
|
285
|
+
let _ = tcp.set_linger(Some(Duration::ZERO));
|
|
286
|
+
}
|
|
287
|
+
match tls {
|
|
288
|
+
Tls::Plain => serve(tcp, act, &seen).await,
|
|
289
|
+
Tls::Trusted => {
|
|
290
|
+
if let Ok(s) = trusted.accept(tcp).await {
|
|
291
|
+
serve(s, act, &seen).await
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
Tls::Untrusted => {
|
|
295
|
+
let _ = untrusted.accept(tcp).await;
|
|
296
|
+
}
|
|
297
|
+
Tls::NeverHandshake => {
|
|
298
|
+
tokio::time::sleep(Duration::from_secs(30)).await;
|
|
299
|
+
drop(tcp)
|
|
300
|
+
}
|
|
301
|
+
Tls::NotTls => {
|
|
302
|
+
let mut tcp = tcp;
|
|
303
|
+
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
304
|
+
let _ = tcp.write_all(b"garbage\r\n\r\n").await;
|
|
305
|
+
}
|
|
306
|
+
Tls::Relayed(relay) => relayed(tcp, relay, act, trusted, seen).await,
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
Double { addr, seen }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/// TLS is served over an in-memory pipe; the relay copies the client's bytes straight in, and the
|
|
315
|
+
/// server's bytes out as `relay` says once the request has been read (so only the reply is shaped).
|
|
316
|
+
async fn relayed(tcp: TcpStream, relay: Relay, act: Act, acceptor: TlsAcceptor, seen: Seen) {
|
|
317
|
+
let (server_side, relay_side) = tokio::io::duplex(1 << 16);
|
|
318
|
+
let served = seen.clone();
|
|
319
|
+
tokio::spawn(async move {
|
|
320
|
+
if let Ok(s) = acceptor.accept(server_side).await {
|
|
321
|
+
serve(s, act, &served).await
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
let (mut tcp_r, mut tcp_w) = tcp.into_split();
|
|
325
|
+
let (mut rel_r, mut rel_w) = tokio::io::split(relay_side);
|
|
326
|
+
tokio::spawn(async move {
|
|
327
|
+
let Relay::ThrottleUpload { chunk, ms, budget } = relay else {
|
|
328
|
+
let _ = tokio::io::copy(&mut tcp_r, &mut rel_w).await;
|
|
329
|
+
return;
|
|
330
|
+
};
|
|
331
|
+
let (mut piece, mut passed) = (vec![0u8; chunk], 0usize);
|
|
332
|
+
loop {
|
|
333
|
+
if passed < budget {
|
|
334
|
+
tokio::time::sleep(Duration::from_millis(ms)).await;
|
|
335
|
+
}
|
|
336
|
+
let n = match tcp_r.read(&mut piece).await {
|
|
337
|
+
Ok(0) | Err(_) => return,
|
|
338
|
+
Ok(n) => n,
|
|
339
|
+
};
|
|
340
|
+
if rel_w.write_all(&piece[..n]).await.is_err() {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
passed += n;
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
let mut buf = vec![0u8; 16 * 1024];
|
|
347
|
+
let mut sent_after = 0usize;
|
|
348
|
+
loop {
|
|
349
|
+
let n = match rel_r.read(&mut buf).await {
|
|
350
|
+
Ok(0) | Err(_) => return,
|
|
351
|
+
Ok(n) => n,
|
|
352
|
+
};
|
|
353
|
+
if !seen.answering.load(SeqCst) || matches!(relay, Relay::ThrottleUpload { .. }) {
|
|
354
|
+
if tcp_w.write_all(&buf[..n]).await.is_err() {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
match relay {
|
|
360
|
+
Relay::Trickle { chunk, ms } => {
|
|
361
|
+
for piece in buf[..n].chunks(chunk) {
|
|
362
|
+
tokio::time::sleep(Duration::from_millis(ms)).await;
|
|
363
|
+
if tcp_w.write_all(piece).await.is_err() {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
Relay::ThrottleUpload { .. } => unreachable!("written above"),
|
|
369
|
+
Relay::StallAfter(limit) => {
|
|
370
|
+
let take = limit.saturating_sub(sent_after).min(n);
|
|
371
|
+
let _ = tcp_w.write_all(&buf[..take]).await;
|
|
372
|
+
sent_after += take;
|
|
373
|
+
if sent_after >= limit {
|
|
374
|
+
tokio::time::sleep(Duration::from_secs(30)).await;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
#[derive(Clone, Copy)]
|
|
383
|
+
pub enum ProxyAct {
|
|
384
|
+
/// Answer 200 and splice to this upstream.
|
|
385
|
+
Tunnel(SocketAddr),
|
|
386
|
+
/// Answer CONNECT with this raw response, then close.
|
|
387
|
+
Answer(&'static str),
|
|
388
|
+
Hang,
|
|
389
|
+
Close,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// An HTTP proxy that records each CONNECT head (lowercased) and acts.
|
|
393
|
+
pub async fn proxy(act: ProxyAct) -> Double {
|
|
394
|
+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
395
|
+
let addr = listener.local_addr().unwrap();
|
|
396
|
+
let seen = Seen::default();
|
|
397
|
+
let all = seen.clone();
|
|
398
|
+
tokio::spawn(async move {
|
|
399
|
+
while let Ok((mut s, _)) = listener.accept().await {
|
|
400
|
+
all.conns.fetch_add(1, SeqCst);
|
|
401
|
+
let seen = all.clone();
|
|
402
|
+
tokio::spawn(async move {
|
|
403
|
+
if read_request(&mut s, &seen).await.is_none() {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
match act {
|
|
407
|
+
ProxyAct::Tunnel(up) => {
|
|
408
|
+
let _ = s
|
|
409
|
+
.write_all(b"HTTP/1.1 200 Connection established\r\n\r\n")
|
|
410
|
+
.await;
|
|
411
|
+
if let Ok(mut u) = TcpStream::connect(up).await {
|
|
412
|
+
let _ = tokio::io::copy_bidirectional(&mut s, &mut u).await;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
ProxyAct::Answer(raw) => {
|
|
416
|
+
let _ = s.write_all(raw.as_bytes()).await;
|
|
417
|
+
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
418
|
+
}
|
|
419
|
+
ProxyAct::Hang => tokio::time::sleep(Duration::from_secs(30)).await,
|
|
420
|
+
ProxyAct::Close => {}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
Double { addr, seen }
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
use std::collections::{HashMap, VecDeque};
|
|
429
|
+
|
|
430
|
+
#[derive(Clone)]
|
|
431
|
+
pub enum KReply {
|
|
432
|
+
Http(u16, &'static str, String),
|
|
433
|
+
/// Read the request, then close without answering.
|
|
434
|
+
Close,
|
|
435
|
+
/// Read the request, then say nothing.
|
|
436
|
+
Hang,
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
pub fn kok(v: serde_json::Value) -> KReply {
|
|
440
|
+
KReply::Http(200, "application/json", v.to_string())
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
#[derive(Clone, Debug)]
|
|
444
|
+
pub struct Recorded {
|
|
445
|
+
pub method: String,
|
|
446
|
+
pub target: String,
|
|
447
|
+
pub headers: Vec<(String, String)>,
|
|
448
|
+
pub body: String,
|
|
449
|
+
}
|
|
450
|
+
impl Recorded {
|
|
451
|
+
pub fn header(&self, name: &str) -> Option<&str> {
|
|
452
|
+
self.headers
|
|
453
|
+
.iter()
|
|
454
|
+
.find(|(k, _)| k.eq_ignore_ascii_case(name))
|
|
455
|
+
.map(|(_, v)| v.as_str())
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/// A plain-HTTP Kraken: replies per path (each call takes the next; the last repeats), every
|
|
460
|
+
/// request recorded. An unscripted path answers 404 so a test fails loudly.
|
|
461
|
+
pub struct Kraken {
|
|
462
|
+
pub addr: SocketAddr,
|
|
463
|
+
requests: Arc<Mutex<Vec<Recorded>>>,
|
|
464
|
+
conns: Arc<AtomicUsize>,
|
|
465
|
+
}
|
|
466
|
+
impl Kraken {
|
|
467
|
+
pub fn url(&self) -> String {
|
|
468
|
+
format!("http://127.0.0.1:{}", self.addr.port())
|
|
469
|
+
}
|
|
470
|
+
pub fn requests(&self) -> Vec<Recorded> {
|
|
471
|
+
self.requests.lock().unwrap().clone()
|
|
472
|
+
}
|
|
473
|
+
pub fn conns(&self) -> usize {
|
|
474
|
+
self.conns.load(SeqCst)
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
pub async fn kraken(script: Vec<(&'static str, Vec<KReply>)>) -> Kraken {
|
|
479
|
+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
480
|
+
let addr = listener.local_addr().unwrap();
|
|
481
|
+
let script: Arc<Mutex<HashMap<String, VecDeque<KReply>>>> = Arc::new(Mutex::new(
|
|
482
|
+
script
|
|
483
|
+
.into_iter()
|
|
484
|
+
.map(|(p, r)| (p.to_string(), r.into()))
|
|
485
|
+
.collect(),
|
|
486
|
+
));
|
|
487
|
+
let requests = Arc::new(Mutex::new(Vec::new()));
|
|
488
|
+
let conns = Arc::new(AtomicUsize::new(0));
|
|
489
|
+
let (rq, cn) = (requests.clone(), conns.clone());
|
|
490
|
+
tokio::spawn(async move {
|
|
491
|
+
while let Ok((mut s, _)) = listener.accept().await {
|
|
492
|
+
cn.fetch_add(1, SeqCst);
|
|
493
|
+
let (script, rq) = (script.clone(), rq.clone());
|
|
494
|
+
tokio::spawn(async move {
|
|
495
|
+
let Some((head, body)) = read_request(&mut s, &Seen::default()).await else {
|
|
496
|
+
return;
|
|
497
|
+
};
|
|
498
|
+
let mut lines = head.lines();
|
|
499
|
+
let mut first = lines.next().unwrap_or("").split(' ');
|
|
500
|
+
let (method, target) = (
|
|
501
|
+
first.next().unwrap_or("").to_string(),
|
|
502
|
+
first.next().unwrap_or("").to_string(),
|
|
503
|
+
);
|
|
504
|
+
let headers = lines
|
|
505
|
+
.filter_map(|l| l.split_once(": "))
|
|
506
|
+
.map(|(k, v)| (k.to_string(), v.to_string()))
|
|
507
|
+
.collect();
|
|
508
|
+
rq.lock().unwrap().push(Recorded {
|
|
509
|
+
method,
|
|
510
|
+
target: target.clone(),
|
|
511
|
+
headers,
|
|
512
|
+
body,
|
|
513
|
+
});
|
|
514
|
+
let path = target.split('?').next().unwrap_or("").to_string();
|
|
515
|
+
let reply = {
|
|
516
|
+
let mut all = script.lock().unwrap();
|
|
517
|
+
all.get_mut(&path).and_then(|q| {
|
|
518
|
+
if q.len() > 1 {
|
|
519
|
+
q.pop_front()
|
|
520
|
+
} else {
|
|
521
|
+
q.front().cloned()
|
|
522
|
+
}
|
|
523
|
+
})
|
|
524
|
+
};
|
|
525
|
+
match reply {
|
|
526
|
+
Some(KReply::Http(status, ct, body)) => {
|
|
527
|
+
let raw = format!(
|
|
528
|
+
"HTTP/1.1 {status} X\r\nContent-Type: {ct}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
|
|
529
|
+
body.len()
|
|
530
|
+
);
|
|
531
|
+
let _ = s.write_all(raw.as_bytes()).await;
|
|
532
|
+
let _ = s.shutdown().await;
|
|
533
|
+
}
|
|
534
|
+
Some(KReply::Close) => {}
|
|
535
|
+
Some(KReply::Hang) => tokio::time::sleep(Duration::from_secs(30)).await,
|
|
536
|
+
None => {
|
|
537
|
+
let _ = s
|
|
538
|
+
.write_all(b"HTTP/1.1 404 X\r\nContent-Length: 10\r\n\r\nunscripted")
|
|
539
|
+
.await;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
Kraken {
|
|
546
|
+
addr,
|
|
547
|
+
requests,
|
|
548
|
+
conns,
|
|
549
|
+
}
|
|
550
|
+
}
|