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,122 @@
|
|
|
1
|
+
use boring2::{
|
|
2
|
+
pkcs12::Pkcs12,
|
|
3
|
+
pkey::{PKey, Private},
|
|
4
|
+
x509::X509,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
use crate::Error;
|
|
8
|
+
|
|
9
|
+
/// Represents a private key and X509 cert as a client certificate.
|
|
10
|
+
#[derive(Debug, Clone)]
|
|
11
|
+
pub struct Identity {
|
|
12
|
+
pkey: PKey<Private>,
|
|
13
|
+
cert: X509,
|
|
14
|
+
chain: Vec<X509>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl Identity {
|
|
18
|
+
/// Parses a DER-formatted PKCS #12 archive, using the specified password to decrypt the key.
|
|
19
|
+
///
|
|
20
|
+
/// The archive should contain a leaf certificate and its private key, as well any intermediate
|
|
21
|
+
/// certificates that allow clients to build a chain to a trusted root.
|
|
22
|
+
/// The chain certificates should be in order from the leaf certificate towards the root.
|
|
23
|
+
///
|
|
24
|
+
/// PKCS #12 archives typically have the file extension `.p12` or `.pfx`, and can be created
|
|
25
|
+
/// with the OpenSSL `pkcs12` tool:
|
|
26
|
+
///
|
|
27
|
+
/// ```bash
|
|
28
|
+
/// openssl pkcs12 -export -out identity.pfx -inkey key.pem -in cert.pem -certfile chain_certs.pem
|
|
29
|
+
/// ```
|
|
30
|
+
///
|
|
31
|
+
/// # Examples
|
|
32
|
+
///
|
|
33
|
+
/// ```
|
|
34
|
+
/// # use std::fs::File;
|
|
35
|
+
/// # use std::io::Read;
|
|
36
|
+
/// # fn pkcs12() -> Result<(), Box<dyn std::error::Error>> {
|
|
37
|
+
/// let mut buf = Vec::new();
|
|
38
|
+
/// File::open("my-ident.pfx")?.read_to_end(&mut buf)?;
|
|
39
|
+
/// let pkcs12 = wreq::Identity::from_pkcs12_der(&buf, "my-privkey-password")?;
|
|
40
|
+
/// # drop(pkcs12);
|
|
41
|
+
/// # Ok(())
|
|
42
|
+
/// # }
|
|
43
|
+
/// ```
|
|
44
|
+
pub fn from_pkcs12_der(buf: &[u8], pass: &str) -> crate::Result<Identity> {
|
|
45
|
+
let pkcs12 = Pkcs12::from_der(buf).map_err(Error::tls)?;
|
|
46
|
+
let parsed = pkcs12.parse(pass).map_err(Error::tls)?;
|
|
47
|
+
Ok(Identity {
|
|
48
|
+
pkey: parsed.pkey,
|
|
49
|
+
cert: parsed.cert,
|
|
50
|
+
// > The stack is the reverse of what you might expect due to the way
|
|
51
|
+
// > PKCS12_parse is implemented, so we need to load it backwards.
|
|
52
|
+
// > https://github.com/sfackler/rust-native-tls/commit/05fb5e583be589ab63d9f83d986d095639f8ec44
|
|
53
|
+
chain: parsed.chain.into_iter().flatten().rev().collect(),
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Parses a chain of PEM encoded X509 certificates, with the leaf certificate first.
|
|
58
|
+
/// `key` is a PEM encoded PKCS #8 formatted private key for the leaf certificate.
|
|
59
|
+
///
|
|
60
|
+
/// The certificate chain should contain any intermediate certificates that should be sent to
|
|
61
|
+
/// clients to allow them to build a chain to a trusted root.
|
|
62
|
+
///
|
|
63
|
+
/// A certificate chain here means a series of PEM encoded certificates concatenated together.
|
|
64
|
+
///
|
|
65
|
+
/// # Examples
|
|
66
|
+
///
|
|
67
|
+
/// ```
|
|
68
|
+
/// # use std::fs;
|
|
69
|
+
/// # fn pkcs8() -> Result<(), Box<dyn std::error::Error>> {
|
|
70
|
+
/// let cert = fs::read("client.pem")?;
|
|
71
|
+
/// let key = fs::read("key.pem")?;
|
|
72
|
+
/// let pkcs8 = wreq::Identity::from_pkcs8_pem(&cert, &key)?;
|
|
73
|
+
/// # drop(pkcs8);
|
|
74
|
+
/// # Ok(())
|
|
75
|
+
/// # }
|
|
76
|
+
/// ```
|
|
77
|
+
pub fn from_pkcs8_pem(buf: &[u8], key: &[u8]) -> crate::Result<Identity> {
|
|
78
|
+
if !key.starts_with(b"-----BEGIN PRIVATE KEY-----") {
|
|
79
|
+
return Err(Error::builder("expected PKCS#8 PEM"));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let pkey = PKey::private_key_from_pem(key).map_err(Error::tls)?;
|
|
83
|
+
let mut cert_chain = X509::stack_from_pem(buf).map_err(Error::tls)?.into_iter();
|
|
84
|
+
let cert = cert_chain.next().ok_or_else(|| {
|
|
85
|
+
Error::builder("at least one certificate must be provided to create an identity")
|
|
86
|
+
})?;
|
|
87
|
+
let chain = cert_chain.collect();
|
|
88
|
+
Ok(Identity { pkey, cert, chain })
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pub(crate) fn add_to_tls(
|
|
92
|
+
&self,
|
|
93
|
+
connector: &mut boring2::ssl::SslConnectorBuilder,
|
|
94
|
+
) -> crate::Result<()> {
|
|
95
|
+
connector.set_certificate(&self.cert).map_err(Error::tls)?;
|
|
96
|
+
connector.set_private_key(&self.pkey).map_err(Error::tls)?;
|
|
97
|
+
for cert in self.chain.iter() {
|
|
98
|
+
// https://www.openssl.org/docs/manmaster/man3/SSL_CTX_add_extra_chain_cert.html
|
|
99
|
+
// specifies that "When sending a certificate chain, extra chain certificates are
|
|
100
|
+
// sent in order following the end entity certificate."
|
|
101
|
+
connector
|
|
102
|
+
.add_extra_chain_cert(cert.clone())
|
|
103
|
+
.map_err(Error::tls)?;
|
|
104
|
+
}
|
|
105
|
+
Ok(())
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
#[cfg(test)]
|
|
110
|
+
mod test {
|
|
111
|
+
use super::Identity;
|
|
112
|
+
|
|
113
|
+
#[test]
|
|
114
|
+
fn identity_from_pkcs12_der_invalid() {
|
|
115
|
+
Identity::from_pkcs12_der(b"not der", "nope").unwrap_err();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[test]
|
|
119
|
+
fn identity_from_pkcs8_pem_invalid() {
|
|
120
|
+
Identity::from_pkcs8_pem(b"not pem", b"not key").unwrap_err();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
use boring2::x509::store::{X509Store, X509StoreBuilder};
|
|
2
|
+
|
|
3
|
+
use super::{Certificate, CertificateInput};
|
|
4
|
+
use crate::{Error, Result};
|
|
5
|
+
|
|
6
|
+
pub fn parse_certs<'c, I>(
|
|
7
|
+
certs: I,
|
|
8
|
+
parser: fn(&'c [u8]) -> crate::Result<Certificate>,
|
|
9
|
+
) -> Result<X509Store>
|
|
10
|
+
where
|
|
11
|
+
I: IntoIterator,
|
|
12
|
+
I::Item: Into<CertificateInput<'c>>,
|
|
13
|
+
{
|
|
14
|
+
let mut store = X509StoreBuilder::new().map_err(Error::tls)?;
|
|
15
|
+
let certs = filter_map_certs(certs, parser);
|
|
16
|
+
process_certs(certs.into_iter(), &mut store)?;
|
|
17
|
+
Ok(store.build())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub fn parse_certs_with_stack<C, F>(certs: C, parse: F) -> Result<X509Store>
|
|
21
|
+
where
|
|
22
|
+
C: AsRef<[u8]>,
|
|
23
|
+
F: Fn(C) -> Result<Vec<Certificate>>,
|
|
24
|
+
{
|
|
25
|
+
let mut store = X509StoreBuilder::new().map_err(Error::tls)?;
|
|
26
|
+
let certs = parse(certs)?;
|
|
27
|
+
process_certs(certs.into_iter(), &mut store)?;
|
|
28
|
+
Ok(store.build())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub fn process_certs<I>(iter: I, store: &mut X509StoreBuilder) -> Result<()>
|
|
32
|
+
where
|
|
33
|
+
I: Iterator<Item = Certificate>,
|
|
34
|
+
{
|
|
35
|
+
let mut valid_count = 0;
|
|
36
|
+
let mut invalid_count = 0;
|
|
37
|
+
for cert in iter {
|
|
38
|
+
if let Err(_err) = store.add_cert(cert.0) {
|
|
39
|
+
invalid_count += 1;
|
|
40
|
+
warn!("tls failed to parse certificate: {:?}", _err);
|
|
41
|
+
} else {
|
|
42
|
+
valid_count += 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if valid_count == 0 && invalid_count > 0 {
|
|
47
|
+
return Err(Error::builder("invalid certificate"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Ok(())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub fn filter_map_certs<'c, I>(
|
|
54
|
+
certs: I,
|
|
55
|
+
parser: fn(&'c [u8]) -> Result<Certificate>,
|
|
56
|
+
) -> impl Iterator<Item = Certificate>
|
|
57
|
+
where
|
|
58
|
+
I: IntoIterator,
|
|
59
|
+
I::Item: Into<CertificateInput<'c>>,
|
|
60
|
+
{
|
|
61
|
+
certs
|
|
62
|
+
.into_iter()
|
|
63
|
+
.map(Into::into)
|
|
64
|
+
.filter_map(move |data| match data.with_parser(parser) {
|
|
65
|
+
Ok(cert) => Some(cert),
|
|
66
|
+
Err(_err) => {
|
|
67
|
+
warn!("tls failed to parse certificate: {:?}", _err);
|
|
68
|
+
None
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
use std::sync::Arc;
|
|
2
|
+
|
|
3
|
+
use boring2::{
|
|
4
|
+
ssl::SslConnectorBuilder,
|
|
5
|
+
x509::store::{X509Store, X509StoreBuilder},
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
use super::{
|
|
9
|
+
Certificate, CertificateInput,
|
|
10
|
+
parser::{filter_map_certs, parse_certs, parse_certs_with_stack, process_certs},
|
|
11
|
+
};
|
|
12
|
+
use crate::{Error, Result};
|
|
13
|
+
|
|
14
|
+
/// A builder for constructing a `CertStore`.
|
|
15
|
+
pub struct CertStoreBuilder {
|
|
16
|
+
builder: Result<X509StoreBuilder>,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ====== impl CertStoreBuilder ======
|
|
20
|
+
|
|
21
|
+
impl CertStoreBuilder {
|
|
22
|
+
/// Adds a DER-encoded certificate to the certificate store.
|
|
23
|
+
#[inline]
|
|
24
|
+
pub fn add_der_cert<'c, C>(self, cert: C) -> Self
|
|
25
|
+
where
|
|
26
|
+
C: Into<CertificateInput<'c>>,
|
|
27
|
+
{
|
|
28
|
+
self.parse_cert(cert, Certificate::from_der)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Adds a PEM-encoded certificate to the certificate store.
|
|
32
|
+
#[inline]
|
|
33
|
+
pub fn add_pem_cert<'c, C>(self, cert: C) -> Self
|
|
34
|
+
where
|
|
35
|
+
C: Into<CertificateInput<'c>>,
|
|
36
|
+
{
|
|
37
|
+
self.parse_cert(cert, Certificate::from_pem)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Adds multiple DER-encoded certificates to the certificate store.
|
|
41
|
+
#[inline]
|
|
42
|
+
pub fn add_der_certs<'c, I>(self, certs: I) -> Self
|
|
43
|
+
where
|
|
44
|
+
I: IntoIterator,
|
|
45
|
+
I::Item: Into<CertificateInput<'c>>,
|
|
46
|
+
{
|
|
47
|
+
self.parse_certs(certs, Certificate::from_der)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Adds multiple PEM-encoded certificates to the certificate store.
|
|
51
|
+
#[inline]
|
|
52
|
+
pub fn add_pem_certs<'c, I>(self, certs: I) -> Self
|
|
53
|
+
where
|
|
54
|
+
I: IntoIterator,
|
|
55
|
+
I::Item: Into<CertificateInput<'c>>,
|
|
56
|
+
{
|
|
57
|
+
self.parse_certs(certs, Certificate::from_pem)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Adds a PEM-encoded certificate stack to the certificate store.
|
|
61
|
+
pub fn add_stack_pem_certs<C>(mut self, certs: C) -> Self
|
|
62
|
+
where
|
|
63
|
+
C: AsRef<[u8]>,
|
|
64
|
+
{
|
|
65
|
+
if let Ok(ref mut builder) = self.builder {
|
|
66
|
+
let result = Certificate::stack_from_pem(certs.as_ref())
|
|
67
|
+
.and_then(|certs| process_certs(certs.into_iter(), builder));
|
|
68
|
+
|
|
69
|
+
if let Err(err) = result {
|
|
70
|
+
self.builder = Err(err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
self
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Load certificates from their default locations.
|
|
77
|
+
///
|
|
78
|
+
/// These locations are read from the `SSL_CERT_FILE` and `SSL_CERT_DIR`
|
|
79
|
+
/// environment variables if present, or defaults specified at OpenSSL
|
|
80
|
+
/// build time otherwise.
|
|
81
|
+
pub fn set_default_paths(mut self) -> Self {
|
|
82
|
+
if let Ok(ref mut builder) = self.builder {
|
|
83
|
+
if let Err(err) = builder.set_default_paths() {
|
|
84
|
+
self.builder = Err(Error::tls(err));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
self
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Constructs the `CertStore`.
|
|
91
|
+
///
|
|
92
|
+
/// This method finalizes the builder and constructs the `CertStore`
|
|
93
|
+
/// containing all the added certificates.
|
|
94
|
+
#[inline]
|
|
95
|
+
pub fn build(self) -> Result<CertStore> {
|
|
96
|
+
self.builder
|
|
97
|
+
.map(X509StoreBuilder::build)
|
|
98
|
+
.map(Arc::new)
|
|
99
|
+
.map(CertStore)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
impl CertStoreBuilder {
|
|
104
|
+
fn parse_cert<'c, C, P>(mut self, cert: C, parser: P) -> Self
|
|
105
|
+
where
|
|
106
|
+
C: Into<CertificateInput<'c>>,
|
|
107
|
+
P: Fn(&'c [u8]) -> Result<Certificate>,
|
|
108
|
+
{
|
|
109
|
+
if let Ok(ref mut builder) = self.builder {
|
|
110
|
+
let input = cert.into();
|
|
111
|
+
let result = input
|
|
112
|
+
.with_parser(parser)
|
|
113
|
+
.and_then(|cert| builder.add_cert(cert.0).map_err(Error::tls));
|
|
114
|
+
|
|
115
|
+
if let Err(err) = result {
|
|
116
|
+
self.builder = Err(err);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
self
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fn parse_certs<'c, I>(mut self, certs: I, parser: fn(&'c [u8]) -> Result<Certificate>) -> Self
|
|
123
|
+
where
|
|
124
|
+
I: IntoIterator,
|
|
125
|
+
I::Item: Into<CertificateInput<'c>>,
|
|
126
|
+
{
|
|
127
|
+
if let Ok(ref mut builder) = self.builder {
|
|
128
|
+
let certs = filter_map_certs(certs, parser);
|
|
129
|
+
if let Err(err) = process_certs(certs, builder) {
|
|
130
|
+
self.builder = Err(err);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
self
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// A thread-safe certificate store for TLS connections.
|
|
138
|
+
///
|
|
139
|
+
/// [`CertStore`] manages a collection of trusted certificates used for verifying peer identities.
|
|
140
|
+
/// It is designed to be shared and reused across requests and connections, similar to `Client`.
|
|
141
|
+
///
|
|
142
|
+
/// Internally, [`CertStore`] uses an [`Arc`] for reference counting, so you do **not** need to wrap
|
|
143
|
+
/// it in an additional [`Rc`] or [`Arc`] for sharing between threads or tasks.
|
|
144
|
+
///
|
|
145
|
+
/// To configure a [`CertStore`], use [`CertStore::builder()`]. You can also construct it from DER
|
|
146
|
+
/// or PEM certificates, or load system defaults.
|
|
147
|
+
///
|
|
148
|
+
/// [`Rc`]: std::rc::Rc
|
|
149
|
+
/// [`Arc`]: std::sync::Arc
|
|
150
|
+
#[derive(Clone)]
|
|
151
|
+
pub struct CertStore(Arc<X509Store>);
|
|
152
|
+
|
|
153
|
+
// ====== impl CertStore ======
|
|
154
|
+
|
|
155
|
+
impl CertStore {
|
|
156
|
+
/// Creates a new `CertStoreBuilder`.
|
|
157
|
+
#[inline]
|
|
158
|
+
pub fn builder() -> CertStoreBuilder {
|
|
159
|
+
CertStoreBuilder {
|
|
160
|
+
builder: X509StoreBuilder::new().map_err(Error::builder),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Creates a new `CertStore` from a collection of DER-encoded certificates.
|
|
165
|
+
#[inline]
|
|
166
|
+
pub fn from_der_certs<'c, C>(certs: C) -> Result<CertStore>
|
|
167
|
+
where
|
|
168
|
+
C: IntoIterator,
|
|
169
|
+
C::Item: Into<CertificateInput<'c>>,
|
|
170
|
+
{
|
|
171
|
+
parse_certs(certs, Certificate::from_der)
|
|
172
|
+
.map(Arc::new)
|
|
173
|
+
.map(CertStore)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Creates a new `CertStore` from a collection of PEM-encoded certificates.
|
|
177
|
+
#[inline]
|
|
178
|
+
pub fn from_pem_certs<'c, C>(certs: C) -> Result<CertStore>
|
|
179
|
+
where
|
|
180
|
+
C: IntoIterator,
|
|
181
|
+
C::Item: Into<CertificateInput<'c>>,
|
|
182
|
+
{
|
|
183
|
+
parse_certs(certs, Certificate::from_pem)
|
|
184
|
+
.map(Arc::new)
|
|
185
|
+
.map(CertStore)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// Creates a new `CertStore` from a PEM-encoded certificate stack.
|
|
189
|
+
#[inline]
|
|
190
|
+
pub fn from_pem_stack<C>(certs: C) -> Result<CertStore>
|
|
191
|
+
where
|
|
192
|
+
C: AsRef<[u8]>,
|
|
193
|
+
{
|
|
194
|
+
parse_certs_with_stack(certs, Certificate::stack_from_pem)
|
|
195
|
+
.map(Arc::new)
|
|
196
|
+
.map(CertStore)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
impl CertStore {
|
|
201
|
+
#[inline]
|
|
202
|
+
pub(crate) fn add_to_tls(&self, tls: &mut SslConnectorBuilder) {
|
|
203
|
+
tls.set_cert_store_ref(&self.0);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
impl Default for CertStore {
|
|
208
|
+
fn default() -> Self {
|
|
209
|
+
#[cfg(feature = "webpki-roots")]
|
|
210
|
+
static LOAD_CERTS: std::sync::LazyLock<CertStore> = std::sync::LazyLock::new(|| {
|
|
211
|
+
CertStore::builder()
|
|
212
|
+
.add_der_certs(webpki_root_certs::TLS_SERVER_ROOT_CERTS)
|
|
213
|
+
.build()
|
|
214
|
+
.expect("failed to load default cert store")
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
#[cfg(not(feature = "webpki-roots"))]
|
|
218
|
+
{
|
|
219
|
+
CertStore::builder()
|
|
220
|
+
.set_default_paths()
|
|
221
|
+
.build()
|
|
222
|
+
.expect("failed to load default cert store")
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#[cfg(feature = "webpki-roots")]
|
|
226
|
+
LOAD_CERTS.clone()
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
mod identity;
|
|
2
|
+
mod parser;
|
|
3
|
+
mod store;
|
|
4
|
+
|
|
5
|
+
use boring2::x509::X509;
|
|
6
|
+
|
|
7
|
+
pub use self::{
|
|
8
|
+
identity::Identity,
|
|
9
|
+
store::{CertStore, CertStoreBuilder},
|
|
10
|
+
};
|
|
11
|
+
use crate::Error;
|
|
12
|
+
|
|
13
|
+
/// A certificate input.
|
|
14
|
+
pub enum CertificateInput<'c> {
|
|
15
|
+
/// Raw DER or PEM data.
|
|
16
|
+
Raw(&'c [u8]),
|
|
17
|
+
/// An already parsed certificate.
|
|
18
|
+
Parsed(Certificate),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl<'a> CertificateInput<'a> {
|
|
22
|
+
pub(crate) fn with_parser<F>(self, parser: F) -> crate::Result<Certificate>
|
|
23
|
+
where
|
|
24
|
+
F: Fn(&'a [u8]) -> crate::Result<Certificate>,
|
|
25
|
+
{
|
|
26
|
+
match self {
|
|
27
|
+
CertificateInput::Raw(data) => parser(data),
|
|
28
|
+
CertificateInput::Parsed(cert) => Ok(cert),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl From<Certificate> for CertificateInput<'_> {
|
|
34
|
+
fn from(cert: Certificate) -> Self {
|
|
35
|
+
CertificateInput::Parsed(cert)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl<'c, T: AsRef<[u8]> + ?Sized + 'c> From<&'c T> for CertificateInput<'c> {
|
|
40
|
+
fn from(value: &'c T) -> CertificateInput<'c> {
|
|
41
|
+
CertificateInput::Raw(value.as_ref())
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// A certificate.
|
|
46
|
+
#[derive(Clone)]
|
|
47
|
+
pub struct Certificate(X509);
|
|
48
|
+
|
|
49
|
+
impl Certificate {
|
|
50
|
+
/// Parse a certificate from DER data.
|
|
51
|
+
#[inline]
|
|
52
|
+
pub fn from_der<C: AsRef<[u8]>>(cert: C) -> crate::Result<Self> {
|
|
53
|
+
X509::from_der(cert.as_ref()).map(Self).map_err(Error::tls)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Parse a certificate from PEM data.
|
|
57
|
+
#[inline]
|
|
58
|
+
pub fn from_pem<C: AsRef<[u8]>>(cert: C) -> crate::Result<Self> {
|
|
59
|
+
X509::from_pem(cert.as_ref()).map(Self).map_err(Error::tls)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Parse a stack of certificates from DER data.
|
|
63
|
+
#[inline]
|
|
64
|
+
pub fn stack_from_pem<C: AsRef<[u8]>>(cert: C) -> crate::Result<Vec<Self>> {
|
|
65
|
+
let certs = X509::stack_from_pem(cert.as_ref()).map_err(Error::tls)?;
|
|
66
|
+
Ok(certs.into_iter().map(Self).collect())
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//! TLS options configuration
|
|
2
|
+
//!
|
|
3
|
+
//! By default, a `Client` will make use of BoringSSL for TLS.
|
|
4
|
+
//!
|
|
5
|
+
//! - Various parts of TLS can also be configured or even disabled on the `ClientBuilder`.
|
|
6
|
+
|
|
7
|
+
pub(crate) mod conn;
|
|
8
|
+
mod keylog;
|
|
9
|
+
mod options;
|
|
10
|
+
mod x509;
|
|
11
|
+
|
|
12
|
+
use boring2::ssl;
|
|
13
|
+
pub use boring2::ssl::{CertificateCompressionAlgorithm, ExtensionType};
|
|
14
|
+
use bytes::{BufMut, Bytes, BytesMut};
|
|
15
|
+
|
|
16
|
+
pub use self::{
|
|
17
|
+
keylog::KeyLog,
|
|
18
|
+
options::{TlsOptions, TlsOptionsBuilder},
|
|
19
|
+
x509::{CertStore, CertStoreBuilder, Certificate, Identity},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/// Http extension carrying extra TLS layer information.
|
|
23
|
+
/// Made available to clients on responses when `tls_info` is set.
|
|
24
|
+
#[derive(Debug, Clone)]
|
|
25
|
+
pub struct TlsInfo {
|
|
26
|
+
pub(crate) peer_certificate: Option<Bytes>,
|
|
27
|
+
pub(crate) peer_certificate_chain: Option<Vec<Bytes>>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl TlsInfo {
|
|
31
|
+
/// Get the DER encoded leaf certificate of the peer.
|
|
32
|
+
pub fn peer_certificate(&self) -> Option<&[u8]> {
|
|
33
|
+
self.peer_certificate.as_deref()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Get the DER encoded certificate chain of the peer.
|
|
37
|
+
///
|
|
38
|
+
/// This includes the leaf certificate on the client side.
|
|
39
|
+
pub fn peer_certificate_chain(&self) -> Option<impl Iterator<Item = &[u8]>> {
|
|
40
|
+
self.peer_certificate_chain
|
|
41
|
+
.as_ref()
|
|
42
|
+
.map(|v| v.iter().map(|b| b.as_ref()))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// A TLS protocol version.
|
|
47
|
+
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
|
48
|
+
pub struct TlsVersion(ssl::SslVersion);
|
|
49
|
+
|
|
50
|
+
impl TlsVersion {
|
|
51
|
+
/// Version 1.0 of the TLS protocol.
|
|
52
|
+
pub const TLS_1_0: TlsVersion = TlsVersion(ssl::SslVersion::TLS1);
|
|
53
|
+
|
|
54
|
+
/// Version 1.1 of the TLS protocol.
|
|
55
|
+
pub const TLS_1_1: TlsVersion = TlsVersion(ssl::SslVersion::TLS1_1);
|
|
56
|
+
|
|
57
|
+
/// Version 1.2 of the TLS protocol.
|
|
58
|
+
pub const TLS_1_2: TlsVersion = TlsVersion(ssl::SslVersion::TLS1_2);
|
|
59
|
+
|
|
60
|
+
/// Version 1.3 of the TLS protocol.
|
|
61
|
+
pub const TLS_1_3: TlsVersion = TlsVersion(ssl::SslVersion::TLS1_3);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// A TLS ALPN protocol.
|
|
65
|
+
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
|
66
|
+
pub struct AlpnProtocol(&'static [u8]);
|
|
67
|
+
|
|
68
|
+
impl AlpnProtocol {
|
|
69
|
+
/// Prefer HTTP/1.1
|
|
70
|
+
pub const HTTP1: AlpnProtocol = AlpnProtocol(b"http/1.1");
|
|
71
|
+
|
|
72
|
+
/// Prefer HTTP/2
|
|
73
|
+
pub const HTTP2: AlpnProtocol = AlpnProtocol(b"h2");
|
|
74
|
+
|
|
75
|
+
/// Prefer HTTP/3
|
|
76
|
+
pub const HTTP3: AlpnProtocol = AlpnProtocol(b"h3");
|
|
77
|
+
|
|
78
|
+
/// Create a new [`AlpnProtocol`] from a static byte slice.
|
|
79
|
+
#[inline]
|
|
80
|
+
pub const fn new(value: &'static [u8]) -> Self {
|
|
81
|
+
AlpnProtocol(value)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[inline]
|
|
85
|
+
fn encode(self) -> Bytes {
|
|
86
|
+
Self::encode_sequence(std::iter::once(&self))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn encode_sequence<'a, I>(items: I) -> Bytes
|
|
90
|
+
where
|
|
91
|
+
I: IntoIterator<Item = &'a AlpnProtocol>,
|
|
92
|
+
{
|
|
93
|
+
let mut buf = BytesMut::new();
|
|
94
|
+
for item in items {
|
|
95
|
+
buf.put_u8(item.0.len() as u8);
|
|
96
|
+
buf.extend_from_slice(item.0);
|
|
97
|
+
}
|
|
98
|
+
buf.freeze()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// A TLS ALPS protocol.
|
|
103
|
+
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
|
104
|
+
pub struct AlpsProtocol(&'static [u8]);
|
|
105
|
+
|
|
106
|
+
impl AlpsProtocol {
|
|
107
|
+
/// Prefer HTTP/1.1
|
|
108
|
+
pub const HTTP1: AlpsProtocol = AlpsProtocol(b"http/1.1");
|
|
109
|
+
|
|
110
|
+
/// Prefer HTTP/2
|
|
111
|
+
pub const HTTP2: AlpsProtocol = AlpsProtocol(b"h2");
|
|
112
|
+
|
|
113
|
+
/// Prefer HTTP/3
|
|
114
|
+
pub const HTTP3: AlpsProtocol = AlpsProtocol(b"h3");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[cfg(test)]
|
|
118
|
+
mod tests {
|
|
119
|
+
use super::*;
|
|
120
|
+
|
|
121
|
+
#[test]
|
|
122
|
+
fn alpn_protocol_encode() {
|
|
123
|
+
let alpn = AlpnProtocol::encode_sequence(&[AlpnProtocol::HTTP1, AlpnProtocol::HTTP2]);
|
|
124
|
+
assert_eq!(alpn, Bytes::from_static(b"\x08http/1.1\x02h2"));
|
|
125
|
+
|
|
126
|
+
let alpn = AlpnProtocol::encode_sequence(&[AlpnProtocol::HTTP3]);
|
|
127
|
+
assert_eq!(alpn, Bytes::from_static(b"\x02h3"));
|
|
128
|
+
|
|
129
|
+
let alpn = AlpnProtocol::encode_sequence(&[AlpnProtocol::HTTP1, AlpnProtocol::HTTP3]);
|
|
130
|
+
assert_eq!(alpn, Bytes::from_static(b"\x08http/1.1\x02h3"));
|
|
131
|
+
|
|
132
|
+
let alpn = AlpnProtocol::encode_sequence(&[AlpnProtocol::HTTP2, AlpnProtocol::HTTP3]);
|
|
133
|
+
assert_eq!(alpn, Bytes::from_static(b"\x02h2\x02h3"));
|
|
134
|
+
|
|
135
|
+
let alpn = AlpnProtocol::encode_sequence(&[
|
|
136
|
+
AlpnProtocol::HTTP1,
|
|
137
|
+
AlpnProtocol::HTTP2,
|
|
138
|
+
AlpnProtocol::HTTP3,
|
|
139
|
+
]);
|
|
140
|
+
assert_eq!(alpn, Bytes::from_static(b"\x08http/1.1\x02h2\x02h3"));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
fn alpn_protocol_encode_single() {
|
|
145
|
+
let alpn = AlpnProtocol::HTTP1.encode();
|
|
146
|
+
assert_eq!(alpn, b"\x08http/1.1".as_ref());
|
|
147
|
+
|
|
148
|
+
let alpn = AlpnProtocol::HTTP2.encode();
|
|
149
|
+
assert_eq!(alpn, b"\x02h2".as_ref());
|
|
150
|
+
|
|
151
|
+
let alpn = AlpnProtocol::HTTP3.encode();
|
|
152
|
+
assert_eq!(alpn, b"\x02h3".as_ref());
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
macro_rules! debug {
|
|
2
|
+
($($arg:tt)+) => {
|
|
3
|
+
{
|
|
4
|
+
#[cfg(feature = "tracing")]
|
|
5
|
+
{
|
|
6
|
+
::tracing::debug!($($arg)+);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
macro_rules! trace {
|
|
13
|
+
($($arg:tt)*) => {
|
|
14
|
+
{
|
|
15
|
+
#[cfg(feature = "tracing")]
|
|
16
|
+
{
|
|
17
|
+
::tracing::trace!($($arg)+);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
macro_rules! trace_span {
|
|
24
|
+
($($arg:tt)*) => {
|
|
25
|
+
{
|
|
26
|
+
#[cfg(feature = "tracing")]
|
|
27
|
+
{
|
|
28
|
+
let _span = ::tracing::trace_span!($($arg)+);
|
|
29
|
+
let _ = _span.entered();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
macro_rules! warn {
|
|
36
|
+
($($arg:tt)*) => {
|
|
37
|
+
{
|
|
38
|
+
#[cfg(feature = "tracing")]
|
|
39
|
+
{
|
|
40
|
+
::tracing::warn!($($arg)+);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
macro_rules! error {
|
|
47
|
+
($($arg:tt)*) => {
|
|
48
|
+
{
|
|
49
|
+
#[cfg(feature = "tracing")]
|
|
50
|
+
{
|
|
51
|
+
::tracing::error!($($arg)+);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|