mirror of
https://github.com/chirpstack/chirpstack.git
synced 2024-12-19 05:07:54 +00:00
Update rustls to 0.23.
This commit is contained in:
parent
ebc4065ca2
commit
dc57e6fe51
153
Cargo.lock
generated
153
Cargo.lock
generated
@ -680,7 +680,7 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn 2.0.58",
|
||||
"which 4.4.2",
|
||||
"which",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -854,7 +854,7 @@ dependencies = [
|
||||
"rsa",
|
||||
"rumqttc",
|
||||
"rust-embed",
|
||||
"rustls 0.22.3",
|
||||
"rustls 0.23.7",
|
||||
"rustls-native-certs 0.7.0",
|
||||
"rustls-pemfile 2.1.2",
|
||||
"serde",
|
||||
@ -1816,18 +1816,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gcp_auth"
|
||||
version = "0.11.1"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e155fdc0640589cc660d00e7ed7aa608479e20187b9ea352a77dd4443dd2d856"
|
||||
checksum = "536c79e79dde296a800738474691e97031769bed9b54e6dd0401b169d35d693d"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.0",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"home",
|
||||
"hyper 0.14.28",
|
||||
"hyper-rustls 0.25.0",
|
||||
"http 1.1.0",
|
||||
"http-body-util",
|
||||
"hyper 1.4.1",
|
||||
"hyper-rustls 0.27.2",
|
||||
"hyper-util",
|
||||
"ring",
|
||||
"rustls 0.22.3",
|
||||
"rustls-pemfile 2.1.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -1836,7 +1839,6 @@ dependencies = [
|
||||
"tracing",
|
||||
"tracing-futures",
|
||||
"url",
|
||||
"which 6.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1938,6 +1940,25 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http 1.1.0",
|
||||
"indexmap 2.2.6",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "handlebars"
|
||||
version = "6.0.0"
|
||||
@ -2167,7 +2188,7 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"httparse",
|
||||
@ -2183,13 +2204,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.2.0"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a"
|
||||
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2 0.4.5",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"httparse",
|
||||
@ -2200,22 +2222,6 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "399c78f9338483cb7e630c8474b07268983c6bd5acee012e4211f9f7bb21b070"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.28",
|
||||
"rustls 0.22.3",
|
||||
"rustls-native-certs 0.7.0",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.26.0"
|
||||
@ -2224,12 +2230,30 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"hyper 1.2.0",
|
||||
"hyper 1.4.1",
|
||||
"hyper-util",
|
||||
"rustls 0.22.3",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.25.0",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"hyper 1.4.1",
|
||||
"hyper-util",
|
||||
"rustls 0.23.7",
|
||||
"rustls-native-certs 0.7.0",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.0",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
@ -2247,16 +2271,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.3"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
|
||||
checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"hyper 1.2.0",
|
||||
"hyper 1.4.1",
|
||||
"pin-project-lite",
|
||||
"socket2 0.5.6",
|
||||
"tokio",
|
||||
@ -3566,7 +3590,7 @@ dependencies = [
|
||||
"sha1_smol",
|
||||
"socket2 0.5.6",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.25.0",
|
||||
"tokio-util",
|
||||
"url",
|
||||
]
|
||||
@ -3639,7 +3663,7 @@ dependencies = [
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"http-body-util",
|
||||
"hyper 1.2.0",
|
||||
"hyper 1.4.1",
|
||||
"hyper-rustls 0.26.0",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
@ -3658,7 +3682,7 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.25.0",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
@ -3777,7 +3801,7 @@ dependencies = [
|
||||
"rustls-webpki 0.102.2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.25.0",
|
||||
"url",
|
||||
]
|
||||
|
||||
@ -3898,6 +3922,21 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.102.2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-connector"
|
||||
version = "0.18.5"
|
||||
@ -4652,16 +4691,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-postgres-rustls"
|
||||
version = "0.11.1"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ea13f22eda7127c827983bdaf0d7fff9df21c8817bab02815ac277a21143677"
|
||||
checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"ring",
|
||||
"rustls 0.22.3",
|
||||
"rustls 0.23.7",
|
||||
"tokio",
|
||||
"tokio-postgres",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.26.0",
|
||||
"x509-certificate",
|
||||
]
|
||||
|
||||
@ -4690,6 +4728,17 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||
dependencies = [
|
||||
"rustls 0.23.7",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.15"
|
||||
@ -4771,7 +4820,7 @@ dependencies = [
|
||||
"axum",
|
||||
"base64 0.21.7",
|
||||
"bytes",
|
||||
"h2",
|
||||
"h2 0.3.26",
|
||||
"http 0.2.12",
|
||||
"http-body 0.4.6",
|
||||
"hyper 0.14.28",
|
||||
@ -5139,7 +5188,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.25.0",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@ -5254,18 +5303,6 @@ dependencies = [
|
||||
"rustix 0.38.32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "6.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7"
|
||||
dependencies = [
|
||||
"either",
|
||||
"home",
|
||||
"rustix 0.38.32",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.5.1"
|
||||
@ -5477,12 +5514,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||
|
||||
[[package]]
|
||||
name = "x509-certificate"
|
||||
version = "0.23.1"
|
||||
|
@ -39,9 +39,9 @@
|
||||
"async-connection-wrapper",
|
||||
] }
|
||||
tokio-postgres = "0.7"
|
||||
tokio-postgres-rustls = "0.11"
|
||||
tokio-postgres-rustls = "0.12"
|
||||
bigdecimal = "0.4"
|
||||
redis = { version = "0.25.2", features = ["tls-rustls", "tokio-rustls-comp"] }
|
||||
redis = { version = "0.25", features = ["tls-rustls", "tokio-rustls-comp"] }
|
||||
deadpool-redis = { version = "0.15", features = ["cluster"] }
|
||||
|
||||
# Logging
|
||||
@ -78,7 +78,7 @@
|
||||
sha2 = "0.10"
|
||||
urlencoding = "2.1"
|
||||
geohash = "0.13"
|
||||
gcp_auth = "0.11"
|
||||
gcp_auth = "0.12"
|
||||
lapin = "2.3"
|
||||
tokio-executor-trait = "2.1"
|
||||
tokio-reactor-trait = "1.1"
|
||||
@ -117,7 +117,12 @@
|
||||
pbkdf2 = { version = "0.12", features = ["simple"] }
|
||||
rand_core = { version = "0.6", features = ["std"] }
|
||||
jsonwebtoken = "9.2"
|
||||
rustls = "0.22"
|
||||
rustls = { version = "0.23", default-features = false, features = [
|
||||
"logging",
|
||||
"std",
|
||||
"tls12",
|
||||
"ring",
|
||||
] }
|
||||
rustls-native-certs = "0.7"
|
||||
rustls-pemfile = "2.1"
|
||||
pem = "3.0"
|
||||
|
@ -23,7 +23,7 @@ use tracing::{error, info, trace};
|
||||
|
||||
use super::GatewayBackend;
|
||||
use crate::config::GatewayBackendMqtt;
|
||||
use crate::helpers::tls::{get_root_certs, load_cert, load_key};
|
||||
use crate::helpers::tls22::{get_root_certs, load_cert, load_key};
|
||||
use crate::monitoring::prometheus;
|
||||
use crate::{downlink, uplink};
|
||||
use lrwn::region::CommonName;
|
||||
|
@ -1,2 +1,3 @@
|
||||
pub mod errors;
|
||||
pub mod tls;
|
||||
pub mod tls22; // rustls 0.22
|
||||
|
@ -2,8 +2,6 @@ use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use tokio::fs;
|
||||
|
||||
// Return root certificates, optionally with the provided ca_file appended.
|
||||
pub fn get_root_certs(ca_file: Option<String>) -> Result<rustls::RootCertStore> {
|
||||
@ -24,38 +22,6 @@ pub fn get_root_certs(ca_file: Option<String>) -> Result<rustls::RootCertStore>
|
||||
Ok(roots)
|
||||
}
|
||||
|
||||
pub async fn load_cert(cert_file: &str) -> Result<Vec<CertificateDer<'static>>> {
|
||||
let cert_s = fs::read_to_string(cert_file)
|
||||
.await
|
||||
.context("Read TLS certificate")?;
|
||||
let mut cert_b = cert_s.as_bytes();
|
||||
let certs = rustls_pemfile::certs(&mut cert_b);
|
||||
let mut out = Vec::new();
|
||||
for cert in certs {
|
||||
out.push(cert?.into_owned());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub async fn load_key(key_file: &str) -> Result<PrivateKeyDer<'static>> {
|
||||
let key_s = fs::read_to_string(key_file)
|
||||
.await
|
||||
.context("Read private key")?;
|
||||
let key_s = private_key_to_pkcs8(&key_s)?;
|
||||
let mut key_b = key_s.as_bytes();
|
||||
let mut keys = rustls_pemfile::pkcs8_private_keys(&mut key_b);
|
||||
if let Some(key) = keys.next() {
|
||||
match key {
|
||||
Ok(v) => return Ok(PrivateKeyDer::Pkcs8(v.clone_key())),
|
||||
Err(e) => {
|
||||
return Err(anyhow!("Error parsing private key, error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("No private key found"))
|
||||
}
|
||||
|
||||
pub fn private_key_to_pkcs8(pem: &str) -> Result<String> {
|
||||
if pem.contains("RSA PRIVATE KEY") {
|
||||
use rsa::{
|
||||
|
88
chirpstack/src/helpers/tls22.rs
Normal file
88
chirpstack/src/helpers/tls22.rs
Normal file
@ -0,0 +1,88 @@
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use rumqttc::tokio_rustls::rustls::{
|
||||
self,
|
||||
pki_types::{CertificateDer, PrivateKeyDer},
|
||||
};
|
||||
use tokio::fs;
|
||||
|
||||
// Return root certificates, optionally with the provided ca_file appended.
|
||||
pub fn get_root_certs(ca_file: Option<String>) -> Result<rustls::RootCertStore> {
|
||||
let mut roots = rustls::RootCertStore::empty();
|
||||
for cert in rustls_native_certs::load_native_certs()? {
|
||||
roots.add(cert)?;
|
||||
}
|
||||
|
||||
if let Some(ca_file) = &ca_file {
|
||||
let f = File::open(ca_file).context("Open CA certificate")?;
|
||||
let mut reader = BufReader::new(f);
|
||||
let certs = rustls_pemfile::certs(&mut reader);
|
||||
for cert in certs.flatten() {
|
||||
roots.add(cert)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(roots)
|
||||
}
|
||||
|
||||
pub async fn load_cert(cert_file: &str) -> Result<Vec<CertificateDer<'static>>> {
|
||||
let cert_s = fs::read_to_string(cert_file)
|
||||
.await
|
||||
.context("Read TLS certificate")?;
|
||||
let mut cert_b = cert_s.as_bytes();
|
||||
let certs = rustls_pemfile::certs(&mut cert_b);
|
||||
let mut out = Vec::new();
|
||||
for cert in certs {
|
||||
out.push(cert?.into_owned());
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub async fn load_key(key_file: &str) -> Result<PrivateKeyDer<'static>> {
|
||||
let key_s = fs::read_to_string(key_file)
|
||||
.await
|
||||
.context("Read private key")?;
|
||||
let key_s = private_key_to_pkcs8(&key_s)?;
|
||||
let mut key_b = key_s.as_bytes();
|
||||
let mut keys = rustls_pemfile::pkcs8_private_keys(&mut key_b);
|
||||
if let Some(key) = keys.next() {
|
||||
match key {
|
||||
Ok(v) => return Ok(PrivateKeyDer::Pkcs8(v.clone_key())),
|
||||
Err(e) => {
|
||||
return Err(anyhow!("Error parsing private key, error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("No private key found"))
|
||||
}
|
||||
|
||||
pub fn private_key_to_pkcs8(pem: &str) -> Result<String> {
|
||||
if pem.contains("RSA PRIVATE KEY") {
|
||||
use rsa::{
|
||||
pkcs1::DecodeRsaPrivateKey,
|
||||
pkcs8::{EncodePrivateKey, LineEnding},
|
||||
RsaPrivateKey,
|
||||
};
|
||||
|
||||
let pkey = RsaPrivateKey::from_pkcs1_pem(pem).context("Read RSA PKCS#1")?;
|
||||
let pkcs8_pem = pkey.to_pkcs8_pem(LineEnding::default())?;
|
||||
Ok(pkcs8_pem.as_str().to_owned())
|
||||
} else if pem.contains("EC PRIVATE KEY") {
|
||||
use elliptic_curve::{
|
||||
pkcs8::{EncodePrivateKey, LineEnding},
|
||||
SecretKey,
|
||||
};
|
||||
|
||||
// We assume it is a P256 based secret-key, which is the most popular curve.
|
||||
// Attempting to decode it as P256 is still better than just failing to read it.
|
||||
let pkey: SecretKey<p256::NistP256> =
|
||||
SecretKey::from_sec1_pem(pem).context("Read EC SEC1")?;
|
||||
let pkcs8_pem = pkey.to_pkcs8_pem(LineEnding::default())?;
|
||||
Ok(pkcs8_pem.as_str().to_owned())
|
||||
} else {
|
||||
Ok(pem.to_string())
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ use std::time::Duration;
|
||||
use anyhow::{Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use gcp_auth::{AuthenticationManager, CustomServiceAccount};
|
||||
use gcp_auth::{CustomServiceAccount, TokenProvider};
|
||||
use prost::Message;
|
||||
use reqwest::header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE};
|
||||
use reqwest::Client;
|
||||
@ -20,7 +20,7 @@ pub struct Integration {
|
||||
json: bool,
|
||||
project_id: String,
|
||||
topic_name: String,
|
||||
auth_manager: gcp_auth::AuthenticationManager,
|
||||
service_account: gcp_auth::CustomServiceAccount,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
@ -46,7 +46,6 @@ impl Integration {
|
||||
pub async fn new(conf: &GcpPubSubConfiguration) -> Result<Integration> {
|
||||
trace!("Initializing GCP Pub-Sub integration");
|
||||
let service_account = CustomServiceAccount::from_json(&conf.credentials_file)?;
|
||||
let auth_manager = AuthenticationManager::try_from(service_account)?;
|
||||
|
||||
Ok(Integration {
|
||||
json: match Encoding::try_from(conf.encoding)
|
||||
@ -57,7 +56,7 @@ impl Integration {
|
||||
},
|
||||
project_id: conf.project_id.clone(),
|
||||
topic_name: conf.topic_name.clone(),
|
||||
auth_manager,
|
||||
service_account,
|
||||
timeout: Duration::from_secs(5),
|
||||
})
|
||||
}
|
||||
@ -89,8 +88,8 @@ impl Integration {
|
||||
let pl = serde_json::to_string(&pl)?;
|
||||
|
||||
let token = self
|
||||
.auth_manager
|
||||
.get_token(&["https://www.googleapis.com/auth/pubsub"])
|
||||
.service_account
|
||||
.token(&["https://www.googleapis.com/auth/pubsub"])
|
||||
.await
|
||||
.context("Get GCP bearer token")?;
|
||||
|
||||
|
@ -19,7 +19,7 @@ use tracing::{error, info, trace, warn};
|
||||
|
||||
use super::Integration as IntegrationTrait;
|
||||
use crate::config::MqttIntegration as Config;
|
||||
use crate::helpers::tls::{get_root_certs, load_cert, load_key};
|
||||
use crate::helpers::tls22::{get_root_certs, load_cert, load_key};
|
||||
use chirpstack_api::integration;
|
||||
|
||||
pub struct Integration<'a> {
|
||||
|
Loading…
Reference in New Issue
Block a user