quack-ruby 0.1.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/LICENSE +21 -0
- data/LICENSE-DuckDB +7 -0
- data/LICENSE-QuackProtocol +21 -0
- data/LICENSE-Rust-dependencies.json +2057 -0
- data/README.md +229 -0
- data/ext/quack/Cargo.lock +1803 -0
- data/ext/quack/Cargo.toml +17 -0
- data/ext/quack/extconf.rb +4 -0
- data/ext/quack/src/lib.rs +647 -0
- data/ext/quack/vendor/quack_protocol/Cargo.lock +1907 -0
- data/ext/quack/vendor/quack_protocol/Cargo.toml +41 -0
- data/ext/quack/vendor/quack_protocol/LICENSE +21 -0
- data/ext/quack/vendor/quack_protocol/PATCHES.md +16 -0
- data/ext/quack/vendor/quack_protocol/README.md +272 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/columns.rs +399 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/mod.rs +134 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/schema.rs +103 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/tests.rs +851 -0
- data/ext/quack/vendor/quack_protocol/src/arrow/values.rs +150 -0
- data/ext/quack/vendor/quack_protocol/src/binary.rs +504 -0
- data/ext/quack/vendor/quack_protocol/src/builders.rs +217 -0
- data/ext/quack/vendor/quack_protocol/src/client.rs +1486 -0
- data/ext/quack/vendor/quack_protocol/src/constants.rs +11 -0
- data/ext/quack/vendor/quack_protocol/src/errors.rs +103 -0
- data/ext/quack/vendor/quack_protocol/src/inspection.rs +250 -0
- data/ext/quack/vendor/quack_protocol/src/json.rs +225 -0
- data/ext/quack/vendor/quack_protocol/src/lib.rs +54 -0
- data/ext/quack/vendor/quack_protocol/src/logical_types.rs +977 -0
- data/ext/quack/vendor/quack_protocol/src/messages.rs +843 -0
- data/ext/quack/vendor/quack_protocol/src/pool.rs +741 -0
- data/ext/quack/vendor/quack_protocol/src/protocol_tests.rs +759 -0
- data/ext/quack/vendor/quack_protocol/src/proxy_error.rs +77 -0
- data/ext/quack/vendor/quack_protocol/src/sql.rs +359 -0
- data/ext/quack/vendor/quack_protocol/src/tls.rs +354 -0
- data/ext/quack/vendor/quack_protocol/src/values.rs +77 -0
- data/ext/quack/vendor/quack_protocol/src/vector.rs +1364 -0
- data/ext/quack/vendor/quack_protocol/testdata/tls/key.der +0 -0
- data/ext/quack/vendor/quack_protocol/testdata/tls/spki.der +0 -0
- data/ext/quack/vendor/quack_protocol/testdata/tls/v1-cert.der +0 -0
- data/ext/quack/vendor/quack_protocol/testdata/tls/v3-cert.der +0 -0
- data/lib/quack/codec.rb +263 -0
- data/lib/quack/connection.rb +131 -0
- data/lib/quack/logical_types.rb +72 -0
- data/lib/quack/result.rb +64 -0
- data/lib/quack/sql.rb +183 -0
- data/lib/quack/types.rb +40 -0
- data/lib/quack/variant.rb +298 -0
- data/lib/quack.rb +34 -0
- metadata +136 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
use std::sync::Arc;
|
|
2
|
+
|
|
3
|
+
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
|
4
|
+
use rustls::crypto::{CryptoProvider, verify_tls13_signature_with_raw_key};
|
|
5
|
+
use rustls::pki_types::{CertificateDer, ServerName, SubjectPublicKeyInfoDer, UnixTime};
|
|
6
|
+
use rustls::{ClientConfig, DigitallySignedStruct, SignatureScheme};
|
|
7
|
+
use x509_parser::prelude::{FromDer, X509Certificate};
|
|
8
|
+
|
|
9
|
+
use crate::errors::{QuackError, Result};
|
|
10
|
+
|
|
11
|
+
const SHA256_LEN: usize = 32;
|
|
12
|
+
|
|
13
|
+
/// SHA-256 fingerprint of the one server certificate a client trusts.
|
|
14
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
15
|
+
pub(crate) struct CertificatePin([u8; SHA256_LEN]);
|
|
16
|
+
|
|
17
|
+
impl CertificatePin {
|
|
18
|
+
/// Parses a fingerprint the way DuckDB's `ssl_fingerprint` does: hex
|
|
19
|
+
/// digits in any case, optionally separated by colons or spaces, with an
|
|
20
|
+
/// optional `sha256:` prefix.
|
|
21
|
+
pub(crate) fn parse(fingerprint: &str) -> Result<Self> {
|
|
22
|
+
let trimmed = fingerprint.trim();
|
|
23
|
+
let digits = match trimmed.get(..7) {
|
|
24
|
+
Some(prefix) if prefix.eq_ignore_ascii_case("sha256:") => &trimmed[7..],
|
|
25
|
+
_ => trimmed,
|
|
26
|
+
};
|
|
27
|
+
let digits: String = digits.chars().filter(|c| !matches!(c, ':' | ' ')).collect();
|
|
28
|
+
let mut bytes = [0; SHA256_LEN];
|
|
29
|
+
hex::decode_to_slice(&digits, &mut bytes).map_err(|err| {
|
|
30
|
+
QuackError::protocol(format!(
|
|
31
|
+
"invalid ssl_fingerprint {fingerprint:?}: expected the 64 hex digits of the \
|
|
32
|
+
server certificate's SHA-256 fingerprint, optionally separated by colons ({err})"
|
|
33
|
+
))
|
|
34
|
+
})?;
|
|
35
|
+
Ok(Self(bytes))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn matches(&self, certificate: &CertificateDer<'_>) -> bool {
|
|
39
|
+
ring::digest::digest(&ring::digest::SHA256, certificate.as_ref()).as_ref() == self.0
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// A rustls client configuration that trusts exactly the certificate with
|
|
44
|
+
/// `pin`'s fingerprint, which is how DuckDB clients authenticate a Quack
|
|
45
|
+
/// server's self-signed certificate. CA and hostname checks are skipped; the
|
|
46
|
+
/// handshake signature is still verified against the pinned certificate's key.
|
|
47
|
+
///
|
|
48
|
+
/// Pinned connections use TLS 1.3 only: the signature is checked against the
|
|
49
|
+
/// certificate's raw public key, which rustls supports for TLS 1.3, so that
|
|
50
|
+
/// the X.509 v1 certificates `quack_generate_keys()` produces are accepted
|
|
51
|
+
/// (webpki parses v3 certificates only).
|
|
52
|
+
pub(crate) fn pinned_client_config(pin: CertificatePin) -> Result<ClientConfig> {
|
|
53
|
+
let provider = Arc::new(rustls::crypto::ring::default_provider());
|
|
54
|
+
let verifier = PinnedCertVerifier {
|
|
55
|
+
pin,
|
|
56
|
+
provider: Arc::clone(&provider),
|
|
57
|
+
};
|
|
58
|
+
let mut config = ClientConfig::builder_with_provider(provider)
|
|
59
|
+
.with_protocol_versions(&[&rustls::version::TLS13])
|
|
60
|
+
.map_err(|err| QuackError::protocol(format!("failed to configure TLS: {err}")))?
|
|
61
|
+
.dangerous()
|
|
62
|
+
.with_custom_certificate_verifier(Arc::new(verifier))
|
|
63
|
+
.with_no_client_auth();
|
|
64
|
+
// The ALPN list reqwest sets on the configurations it builds itself.
|
|
65
|
+
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
|
66
|
+
Ok(config)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[derive(Debug)]
|
|
70
|
+
struct PinnedCertVerifier {
|
|
71
|
+
pin: CertificatePin,
|
|
72
|
+
provider: Arc<CryptoProvider>,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
impl ServerCertVerifier for PinnedCertVerifier {
|
|
76
|
+
fn verify_server_cert(
|
|
77
|
+
&self,
|
|
78
|
+
end_entity: &CertificateDer<'_>,
|
|
79
|
+
_intermediates: &[CertificateDer<'_>],
|
|
80
|
+
_server_name: &ServerName<'_>,
|
|
81
|
+
_ocsp_response: &[u8],
|
|
82
|
+
_now: UnixTime,
|
|
83
|
+
) -> std::result::Result<ServerCertVerified, rustls::Error> {
|
|
84
|
+
if self.pin.matches(end_entity) {
|
|
85
|
+
Ok(ServerCertVerified::assertion())
|
|
86
|
+
} else {
|
|
87
|
+
Err(rustls::Error::General(
|
|
88
|
+
"server certificate does not match the pinned ssl_fingerprint".to_string(),
|
|
89
|
+
))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fn verify_tls12_signature(
|
|
94
|
+
&self,
|
|
95
|
+
_message: &[u8],
|
|
96
|
+
_cert: &CertificateDer<'_>,
|
|
97
|
+
_dss: &DigitallySignedStruct,
|
|
98
|
+
) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
|
|
99
|
+
Err(rustls::Error::General(
|
|
100
|
+
"pinned connections use TLS 1.3 only".to_string(),
|
|
101
|
+
))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn verify_tls13_signature(
|
|
105
|
+
&self,
|
|
106
|
+
message: &[u8],
|
|
107
|
+
cert: &CertificateDer<'_>,
|
|
108
|
+
dss: &DigitallySignedStruct,
|
|
109
|
+
) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
|
|
110
|
+
let spki = subject_public_key_info(cert.as_ref()).ok_or_else(|| {
|
|
111
|
+
rustls::Error::General(
|
|
112
|
+
"server certificate has no readable subject public key".to_string(),
|
|
113
|
+
)
|
|
114
|
+
})?;
|
|
115
|
+
verify_tls13_signature_with_raw_key(
|
|
116
|
+
message,
|
|
117
|
+
&SubjectPublicKeyInfoDer::from(spki),
|
|
118
|
+
dss,
|
|
119
|
+
&self.provider.signature_verification_algorithms,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
|
124
|
+
self.provider
|
|
125
|
+
.signature_verification_algorithms
|
|
126
|
+
.supported_schemes()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// The DER-encoded `subjectPublicKeyInfo` of an X.509 certificate of any
|
|
131
|
+
/// version, or `None` if `certificate` is not exactly one well-formed
|
|
132
|
+
/// certificate.
|
|
133
|
+
fn subject_public_key_info(certificate: &[u8]) -> Option<&[u8]> {
|
|
134
|
+
match X509Certificate::from_der(certificate) {
|
|
135
|
+
Ok(([], certificate)) => Some(certificate.tbs_certificate.subject_pki.raw),
|
|
136
|
+
_ => None,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// A local HTTPS server for handshake tests, and certificates to serve.
|
|
141
|
+
#[cfg(test)]
|
|
142
|
+
pub(crate) mod test_server {
|
|
143
|
+
use std::sync::Arc;
|
|
144
|
+
|
|
145
|
+
use rustls::ServerConfig;
|
|
146
|
+
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
|
|
147
|
+
use rustls::server::{ClientHello, ResolvesServerCert};
|
|
148
|
+
use rustls::sign::CertifiedKey;
|
|
149
|
+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
150
|
+
use tokio::net::TcpListener;
|
|
151
|
+
use tokio_rustls::TlsAcceptor;
|
|
152
|
+
|
|
153
|
+
// Two self-signed certificates for one P-256 key: an X.509 v1 certificate,
|
|
154
|
+
// the version `quack_generate_keys()` produces, and a v3 one from
|
|
155
|
+
// `openssl req -x509`. The fingerprints are what
|
|
156
|
+
// `openssl x509 -noout -fingerprint -sha256` prints for them.
|
|
157
|
+
pub(crate) const V1_CERT: &[u8] = include_bytes!("../testdata/tls/v1-cert.der");
|
|
158
|
+
pub(crate) const V1_CERT_FINGERPRINT: &str = "01:50:96:0A:D8:17:B9:61:A1:A9:08:AB:4A:96:D0:A0:10:C3:02:87:B1:DB:AC:EE:33:92:4F:15:C1:2B:53:B1";
|
|
159
|
+
pub(crate) const V3_CERT: &[u8] = include_bytes!("../testdata/tls/v3-cert.der");
|
|
160
|
+
pub(crate) const V3_CERT_FINGERPRINT: &str = "A5:B7:18:96:27:21:76:41:25:8E:6E:68:0E:1E:DF:F5:31:23:F0:45:CA:44:69:82:9A:86:42:EE:C6:5E:05:33";
|
|
161
|
+
// The PKCS#8 private key both certificates are issued for.
|
|
162
|
+
const KEY: &[u8] = include_bytes!("../testdata/tls/key.der");
|
|
163
|
+
|
|
164
|
+
/// Serves `response`, a raw HTTP/1.1 response, over TLS 1.3 with
|
|
165
|
+
/// `certificate` to every connection on a local port, and returns the port.
|
|
166
|
+
pub(crate) async fn serve(certificate: &'static [u8], response: String) -> u16 {
|
|
167
|
+
let provider = Arc::new(rustls::crypto::ring::default_provider());
|
|
168
|
+
// Not `with_single_cert`: it checks the key against the certificate
|
|
169
|
+
// through webpki, which rejects v1 certificates.
|
|
170
|
+
let key = provider
|
|
171
|
+
.key_provider
|
|
172
|
+
.load_private_key(PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(KEY)))
|
|
173
|
+
.unwrap();
|
|
174
|
+
let certified = CertifiedKey::new(vec![CertificateDer::from(certificate)], key);
|
|
175
|
+
let config = ServerConfig::builder_with_provider(provider)
|
|
176
|
+
.with_protocol_versions(&[&rustls::version::TLS13])
|
|
177
|
+
.unwrap()
|
|
178
|
+
.with_no_client_auth()
|
|
179
|
+
.with_cert_resolver(Arc::new(SingleCert(Arc::new(certified))));
|
|
180
|
+
let acceptor = TlsAcceptor::from(Arc::new(config));
|
|
181
|
+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
182
|
+
let port = listener.local_addr().unwrap().port();
|
|
183
|
+
tokio::spawn(async move {
|
|
184
|
+
while let Ok((stream, _)) = listener.accept().await {
|
|
185
|
+
let acceptor = acceptor.clone();
|
|
186
|
+
let response = response.clone();
|
|
187
|
+
tokio::spawn(async move {
|
|
188
|
+
// Handshakes a test expects to fail end here.
|
|
189
|
+
let Ok(mut stream) = acceptor.accept(stream).await else {
|
|
190
|
+
return;
|
|
191
|
+
};
|
|
192
|
+
let mut request = Vec::new();
|
|
193
|
+
let mut buf = [0; 1024];
|
|
194
|
+
// Headers are enough: no test needs the body.
|
|
195
|
+
while !request.windows(4).any(|window| window == b"\r\n\r\n") {
|
|
196
|
+
match stream.read(&mut buf).await {
|
|
197
|
+
Ok(0) | Err(_) => return,
|
|
198
|
+
Ok(n) => request.extend_from_slice(&buf[..n]),
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
let _ = stream.write_all(response.as_bytes()).await;
|
|
202
|
+
let _ = stream.shutdown().await;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
port
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#[derive(Debug)]
|
|
210
|
+
struct SingleCert(Arc<CertifiedKey>);
|
|
211
|
+
|
|
212
|
+
impl ResolvesServerCert for SingleCert {
|
|
213
|
+
fn resolve(&self, _hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
|
|
214
|
+
Some(Arc::clone(&self.0))
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
pub(crate) fn ok_response() -> String {
|
|
219
|
+
"HTTP/1.1 200 OK\r\ncontent-length: 2\r\nconnection: close\r\n\r\nok".to_string()
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#[cfg(test)]
|
|
224
|
+
mod tests {
|
|
225
|
+
use std::error::Error as _;
|
|
226
|
+
|
|
227
|
+
use super::test_server::{self, V1_CERT, V1_CERT_FINGERPRINT, V3_CERT, V3_CERT_FINGERPRINT};
|
|
228
|
+
use super::*;
|
|
229
|
+
|
|
230
|
+
const FINGERPRINT: &str = "2B:31:F0:0A:9C:44:D1:7E:65:B8:03:FE:12:34:56:78:9A:BC:DE:F0:01:23:45:67:89:AB:CD:EF:10:32:54:EA";
|
|
231
|
+
// `openssl pkey -pubout -outform der` for the key both test certificates share.
|
|
232
|
+
const SPKI: &[u8] = include_bytes!("../testdata/tls/spki.der");
|
|
233
|
+
|
|
234
|
+
async fn get_pinned(port: u16, fingerprint: &str) -> reqwest::Result<String> {
|
|
235
|
+
let config = pinned_client_config(CertificatePin::parse(fingerprint).unwrap()).unwrap();
|
|
236
|
+
reqwest::Client::builder()
|
|
237
|
+
.use_preconfigured_tls(config)
|
|
238
|
+
.build()?
|
|
239
|
+
.get(format!("https://127.0.0.1:{port}/"))
|
|
240
|
+
.send()
|
|
241
|
+
.await?
|
|
242
|
+
.text()
|
|
243
|
+
.await
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The error and its sources, which is where reqwest keeps the TLS error.
|
|
247
|
+
fn error_chain(err: &reqwest::Error) -> String {
|
|
248
|
+
let mut chain = err.to_string();
|
|
249
|
+
let mut source = err.source();
|
|
250
|
+
while let Some(err) = source {
|
|
251
|
+
chain.push_str(&format!(": {err}"));
|
|
252
|
+
source = err.source();
|
|
253
|
+
}
|
|
254
|
+
chain
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[test]
|
|
258
|
+
fn parses_duckdb_fingerprint_spellings() {
|
|
259
|
+
let expected = CertificatePin::parse(FINGERPRINT).unwrap();
|
|
260
|
+
let bare = FINGERPRINT.replace(':', "");
|
|
261
|
+
for spelling in [
|
|
262
|
+
bare.clone(),
|
|
263
|
+
bare.to_lowercase(),
|
|
264
|
+
format!("sha256:{FINGERPRINT}"),
|
|
265
|
+
format!("SHA256:{bare}"),
|
|
266
|
+
format!(" {} ", FINGERPRINT.replace(':', " ")),
|
|
267
|
+
] {
|
|
268
|
+
assert_eq!(
|
|
269
|
+
CertificatePin::parse(&spelling).unwrap(),
|
|
270
|
+
expected,
|
|
271
|
+
"{spelling}"
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
assert_eq!(expected.0[0], 0x2B);
|
|
275
|
+
assert_eq!(expected.0[31], 0xEA);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#[test]
|
|
279
|
+
fn rejects_malformed_fingerprints() {
|
|
280
|
+
for bad in [
|
|
281
|
+
"",
|
|
282
|
+
"2B:31",
|
|
283
|
+
&FINGERPRINT.replace("EA", "EAFF"),
|
|
284
|
+
&FINGERPRINT.replace("2B", "ZZ"),
|
|
285
|
+
"sha1:2B31F00A9C44D17E65B803FE123456789ABCDEF00123456789ABCDEF103254EA",
|
|
286
|
+
] {
|
|
287
|
+
let err = CertificatePin::parse(bad).unwrap_err();
|
|
288
|
+
assert!(
|
|
289
|
+
err.to_string().contains("invalid ssl_fingerprint"),
|
|
290
|
+
"{bad}: {err}"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn verifier_accepts_only_the_pinned_certificate() {
|
|
297
|
+
let cert = CertificateDer::from(V1_CERT);
|
|
298
|
+
let server_name = ServerName::try_from("quack.example").unwrap();
|
|
299
|
+
let verify = |fingerprint: &str| {
|
|
300
|
+
PinnedCertVerifier {
|
|
301
|
+
pin: CertificatePin::parse(fingerprint).unwrap(),
|
|
302
|
+
provider: Arc::new(rustls::crypto::ring::default_provider()),
|
|
303
|
+
}
|
|
304
|
+
.verify_server_cert(&cert, &[], &server_name, &[], UnixTime::now())
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
assert!(verify(V1_CERT_FINGERPRINT).is_ok());
|
|
308
|
+
let err = verify(FINGERPRINT).unwrap_err();
|
|
309
|
+
assert!(err.to_string().contains("ssl_fingerprint"), "{err}");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
#[test]
|
|
313
|
+
fn reads_the_public_key_of_v1_and_v3_certificates() {
|
|
314
|
+
for cert in [V1_CERT, V3_CERT] {
|
|
315
|
+
assert_eq!(subject_public_key_info(cert), Some(SPKI));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
#[test]
|
|
320
|
+
fn malformed_certificates_have_no_public_key() {
|
|
321
|
+
assert_eq!(subject_public_key_info(&[]), None);
|
|
322
|
+
assert_eq!(subject_public_key_info(&V1_CERT[..V1_CERT.len() / 2]), None);
|
|
323
|
+
assert_eq!(subject_public_key_info(&[V1_CERT, &[0]].concat()), None);
|
|
324
|
+
assert_eq!(subject_public_key_info(SPKI), None);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#[tokio::test]
|
|
328
|
+
async fn pinned_handshake_succeeds_with_v1_and_v3_certificates() {
|
|
329
|
+
for (cert, fingerprint) in [
|
|
330
|
+
(V1_CERT, V1_CERT_FINGERPRINT),
|
|
331
|
+
(V3_CERT, V3_CERT_FINGERPRINT),
|
|
332
|
+
] {
|
|
333
|
+
let port = test_server::serve(cert, test_server::ok_response()).await;
|
|
334
|
+
assert_eq!(
|
|
335
|
+
get_pinned(port, fingerprint).await.unwrap(),
|
|
336
|
+
"ok",
|
|
337
|
+
"{fingerprint}"
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
#[tokio::test]
|
|
343
|
+
async fn pinned_handshake_fails_for_another_certificate() {
|
|
344
|
+
let port = test_server::serve(V1_CERT, test_server::ok_response()).await;
|
|
345
|
+
for fingerprint in [FINGERPRINT, V3_CERT_FINGERPRINT] {
|
|
346
|
+
let err = get_pinned(port, fingerprint).await.unwrap_err();
|
|
347
|
+
let chain = error_chain(&err);
|
|
348
|
+
assert!(
|
|
349
|
+
chain.contains("does not match the pinned ssl_fingerprint"),
|
|
350
|
+
"{chain}"
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
use chrono::NaiveDate;
|
|
2
|
+
|
|
3
|
+
use crate::errors::{QuackError, Result};
|
|
4
|
+
use crate::vector::{
|
|
5
|
+
DateValue, DecimalValue, IntervalValue, TimeTzValue, TimeUnit, TimeValue, TimestampUnit,
|
|
6
|
+
TimestampValue, Value,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
pub fn decimal_value(value: impl ToString, width: u64, scale: u64) -> Result<Value> {
|
|
10
|
+
Ok(Value::Decimal(DecimalValue {
|
|
11
|
+
value: parse_decimal_value(&value.to_string(), scale)?,
|
|
12
|
+
width,
|
|
13
|
+
scale,
|
|
14
|
+
}))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn date_value(days: i32) -> Value {
|
|
18
|
+
Value::Date(DateValue { days })
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub fn date_from_iso_date(value: &str) -> Result<Value> {
|
|
22
|
+
let date = NaiveDate::parse_from_str(value, "%Y-%m-%d")
|
|
23
|
+
.map_err(|_| QuackError::protocol(format!("invalid ISO date {value}")))?;
|
|
24
|
+
let epoch = NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid epoch");
|
|
25
|
+
Ok(date_value((date - epoch).num_days() as i32))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub fn time_value(value: i64, unit: TimeUnit) -> Value {
|
|
29
|
+
Value::Time(TimeValue { unit, value })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub fn time_tz_value(bits: i64) -> Value {
|
|
33
|
+
Value::TimeTz(TimeTzValue { bits })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub fn timestamp_value(value: i64, unit: TimestampUnit, timezone_utc: bool) -> Value {
|
|
37
|
+
Value::Timestamp(TimestampValue {
|
|
38
|
+
unit,
|
|
39
|
+
value,
|
|
40
|
+
timezone_utc,
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub fn interval_value(months: i32, days: i32, micros: i64) -> Value {
|
|
45
|
+
Value::Interval(IntervalValue {
|
|
46
|
+
months,
|
|
47
|
+
days,
|
|
48
|
+
micros,
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn parse_decimal_value(value: &str, scale: u64) -> Result<i128> {
|
|
53
|
+
let text = value.trim();
|
|
54
|
+
let negative = text.starts_with('-');
|
|
55
|
+
let unsigned = text.strip_prefix(['-', '+']).unwrap_or(text);
|
|
56
|
+
let (integer, fraction) = unsigned.split_once('.').unwrap_or((unsigned, ""));
|
|
57
|
+
if !integer.bytes().all(|byte| byte.is_ascii_digit())
|
|
58
|
+
|| !fraction.bytes().all(|byte| byte.is_ascii_digit())
|
|
59
|
+
{
|
|
60
|
+
return Err(QuackError::protocol(format!(
|
|
61
|
+
"invalid decimal value {value}"
|
|
62
|
+
)));
|
|
63
|
+
}
|
|
64
|
+
let mut padded_fraction = fraction.to_string();
|
|
65
|
+
while padded_fraction.len() < scale as usize {
|
|
66
|
+
padded_fraction.push('0');
|
|
67
|
+
}
|
|
68
|
+
padded_fraction.truncate(scale as usize);
|
|
69
|
+
let unscaled = format!(
|
|
70
|
+
"{}{}",
|
|
71
|
+
if integer.is_empty() { "0" } else { integer },
|
|
72
|
+
padded_fraction
|
|
73
|
+
)
|
|
74
|
+
.parse::<i128>()
|
|
75
|
+
.map_err(|_| QuackError::protocol(format!("invalid decimal value {value}")))?;
|
|
76
|
+
Ok(if negative { -unscaled } else { unscaled })
|
|
77
|
+
}
|