Skip to content

Commit 8160d4e

Browse files
committed
test(storage): cover GCS HTTPS OpenDAL TLS
1 parent 0b26a07 commit 8160d4e

4 files changed

Lines changed: 166 additions & 3 deletions

File tree

quickwit/Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quickwit/quickwit-storage/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ reqwest = { workspace = true, optional = true }
6262
http = { workspace = true }
6363
mockall = { workspace = true }
6464
proptest = { workspace = true }
65+
reqwest-013 = { package = "reqwest", version = "0.13", default-features = false }
66+
rustls = { workspace = true }
6567
tokio = { workspace = true }
68+
tokio-rustls = { workspace = true }
6669
tracing-subscriber = { workspace = true }
6770

6871
aws-sdk-s3 = { workspace = true }

quickwit/quickwit-storage/src/opendal_storage/base.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,30 @@ impl OpendalStorage {
6060
cfg: opendal::services::Gcs,
6161
) -> Result<Self, StorageResolverError> {
6262
let op = Operator::new(cfg)?.finish();
63-
Ok(Self {
63+
Ok(Self::from_operator(uri, op))
64+
}
65+
66+
fn from_operator(uri: Uri, op: Operator) -> Self {
67+
Self {
6468
uri,
6569
op,
6670
// limits are the same as on S3
6771
multipart_policy: MultiPartPolicy::default(),
68-
})
72+
}
73+
}
74+
75+
#[cfg(test)]
76+
// Lets local HTTPS tests trust a private CA without changing global trust,
77+
// while still using Quickwit's GCS storage construction and read path.
78+
pub(super) fn new_google_cloud_storage_with_http_client_for_test(
79+
uri: Uri,
80+
cfg: opendal::services::Gcs,
81+
http_client: opendal::raw::HttpClient,
82+
) -> Result<Self, StorageResolverError> {
83+
let op = Operator::new(cfg)?
84+
.layer(opendal::layers::HttpClientLayer::new(http_client))
85+
.finish();
86+
Ok(Self::from_operator(uri, op))
6987
}
7088

7189
#[cfg(feature = "integration-testsuite")]

quickwit/quickwit-storage/src/opendal_storage/google_cloud_storage.rs

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,55 @@ fn parse_google_uri(uri: &Uri) -> Option<(String, PathBuf)> {
113113

114114
#[cfg(test)]
115115
mod tests {
116+
use std::path::Path;
117+
use std::sync::Arc;
118+
119+
use base64::Engine;
120+
use opendal::raw::HttpClient;
116121
use quickwit_common::uri::Uri;
122+
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivateSec1KeyDer};
123+
use tokio::io::{AsyncReadExt, AsyncWriteExt};
124+
use tokio::net::TcpListener;
125+
use tokio::task::JoinHandle;
126+
127+
use super::{OpendalStorage, parse_google_uri};
128+
use crate::Storage;
129+
130+
// Test-only CA and server certificate for 127.0.0.1/localhost, valid from
131+
// 2020 to 3020. The client trusts only this CA, so the test never depends
132+
// on the host root store.
133+
const TEST_CA_CERT_DER_BASE64: &str = concat!(
134+
"MIIBizCCATGgAwIBAgICEAEwCgYIKoZIzj0EAwIwGzEZMBcGA1UEAwwQUXVpY2t3",
135+
"aXQgVGVzdCBDQTAgFw0yMDAxMDEwMDAwMDBaGA8zMDIwMDEwMTAwMDAwMFowGzEZ",
136+
"MBcGA1UEAwwQUXVpY2t3aXQgVGVzdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH",
137+
"A0IABH+1ZvivhT0E5FydtoMGBkyenql8XPyFTPBhTfHycTjfTWJiETjILGadPLKY",
138+
"OZJky8ThPZUpKAux5M4SaazdX1WjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P",
139+
"AQH/BAQDAgEGMB0GA1UdDgQWBBQmOMvIHAegmBHwvdVGyguC/57/4zAfBgNVHSME",
140+
"GDAWgBQmOMvIHAegmBHwvdVGyguC/57/4zAKBggqhkjOPQQDAgNIADBFAiEAnE7M",
141+
"lcB35MOr+7WKDAhu/c6ZrpgRz+chqqfc3g5YTOECIEDmoPkOigkulNON67opCPaT",
142+
"y+MQhMA9KDEzE3t/CY9V",
143+
);
144+
145+
const TEST_SERVER_CERT_DER_BASE64: &str = concat!(
146+
"MIIBszCCAVqgAwIBAgICEAIwCgYIKoZIzj0EAwIwGzEZMBcGA1UEAwwQUXVpY2t3",
147+
"aXQgVGVzdCBDQTAgFw0yMDAxMDEwMDAwMDBaGA8zMDIwMDEwMTAwMDAwMFowFDES",
148+
"MBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEowPj",
149+
"3vpXPAkf04MNeGhaDBvtwMsmeipV57lSWx5K2FwXH7JDmt74k4HmQFB6JESy6FbM",
150+
"tAVhivr7kG5dWKK/sqOBkjCBjzAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIH",
151+
"gDATBgNVHSUEDDAKBggrBgEFBQcDATAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8A",
152+
"AAEwHQYDVR0OBBYEFES7BP5uQpa3+PktDVlgc9zYGIqDMB8GA1UdIwQYMBaAFCY4",
153+
"y8gcB6CYEfC91UbKC4L/nv/jMAoGCCqGSM49BAMCA0cAMEQCIGtIKEWRn7ec82TY",
154+
"s1jrUoKWnhzRDbZTUtvXORk190rHAiAosxVgu45TjDyuROKU39TxJ1z+JObhNGk8",
155+
"J6PkuOTFqg==",
156+
);
157+
158+
const TEST_SERVER_KEY_DER_BASE64: &str = concat!(
159+
"MHcCAQEEIIql19flBaZJE16Ivs8GjdJHedhuU5YFZgvIn4WaOs6HoAoGCCqGSM49",
160+
"AwEHoUQDQgAEowPj3vpXPAkf04MNeGhaDBvtwMsmeipV57lSWx5K2FwXH7JDmt74",
161+
"k4HmQFB6JESy6FbMtAVhivr7kG5dWKK/sg==",
162+
);
117163

118-
use super::parse_google_uri;
164+
type LocalHttpsGcsServer = (String, JoinHandle<anyhow::Result<()>>);
119165

120166
#[test]
121167
fn test_parse_google_uri() {
@@ -134,4 +180,97 @@ mod tests {
134180
assert_eq!(bucket, "test-bucket");
135181
assert_eq!(prefix.to_str().unwrap(), "indexes");
136182
}
183+
184+
#[tokio::test]
185+
async fn test_gcs_storage_get_slice_over_https_with_verified_tls() -> anyhow::Result<()> {
186+
let (endpoint, server_task) = start_local_https_gcs_server().await?;
187+
let ca_cert_der = decode_test_der(TEST_CA_CERT_DER_BASE64)?;
188+
let ca_cert = reqwest_013::Certificate::from_der(&ca_cert_der)?;
189+
let reqwest_client = reqwest_013::Client::builder()
190+
.no_proxy()
191+
.tls_certs_only([ca_cert])
192+
.build()?;
193+
194+
let cfg = opendal::services::Gcs::default()
195+
.bucket("quickwit-test-bucket")
196+
.endpoint(&endpoint)
197+
.allow_anonymous()
198+
.disable_config_load()
199+
.disable_vm_metadata();
200+
let storage = OpendalStorage::new_google_cloud_storage_with_http_client_for_test(
201+
Uri::for_test("gs://quickwit-test-bucket"),
202+
cfg,
203+
HttpClient::with(reqwest_client),
204+
)?;
205+
206+
let bytes = storage.get_slice(Path::new("hello.txt"), 0..2).await?;
207+
assert_eq!(bytes.as_slice(), b"ok");
208+
server_task.await??;
209+
Ok(())
210+
}
211+
212+
async fn start_local_https_gcs_server() -> anyhow::Result<LocalHttpsGcsServer> {
213+
let cert_chain = vec![CertificateDer::from(decode_test_der(
214+
TEST_SERVER_CERT_DER_BASE64,
215+
)?)];
216+
let private_key = PrivateKeyDer::Sec1(PrivateSec1KeyDer::from(decode_test_der(
217+
TEST_SERVER_KEY_DER_BASE64,
218+
)?));
219+
let tls_config = rustls::ServerConfig::builder()
220+
.with_no_client_auth()
221+
.with_single_cert(cert_chain, private_key)?;
222+
let tls_acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(tls_config));
223+
let listener = TcpListener::bind(("127.0.0.1", 0)).await?;
224+
let endpoint = format!("https://127.0.0.1:{}", listener.local_addr()?.port());
225+
226+
let server_task = tokio::spawn(async move {
227+
let (stream, _) = listener.accept().await?;
228+
let mut stream = tls_acceptor.accept(stream).await?;
229+
let mut request = Vec::new();
230+
let mut buffer = [0u8; 1024];
231+
loop {
232+
let bytes_read = stream.read(&mut buffer).await?;
233+
if bytes_read == 0 {
234+
break;
235+
}
236+
request.extend_from_slice(&buffer[..bytes_read]);
237+
if request.windows(4).any(|window| window == b"\r\n\r\n") {
238+
break;
239+
}
240+
}
241+
let request = String::from_utf8_lossy(&request);
242+
assert!(
243+
request.starts_with(
244+
"GET /storage/v1/b/quickwit-test-bucket/o/hello.txt?alt=media HTTP/"
245+
),
246+
"unexpected GCS request line: {request}"
247+
);
248+
assert!(
249+
request
250+
.to_ascii_lowercase()
251+
.contains("\r\nrange: bytes=0-1\r\n"),
252+
"expected range read request header: {request}"
253+
);
254+
255+
stream
256+
.write_all(
257+
b"HTTP/1.1 206 Partial Content\r\n\
258+
Content-Length: 2\r\n\
259+
Content-Range: bytes 0-1/2\r\n\
260+
Accept-Ranges: bytes\r\n\
261+
Connection: close\r\n\
262+
\r\n\
263+
ok",
264+
)
265+
.await?;
266+
stream.shutdown().await?;
267+
Ok(())
268+
});
269+
270+
Ok((endpoint, server_task))
271+
}
272+
273+
fn decode_test_der(base64_der: &str) -> anyhow::Result<Vec<u8>> {
274+
Ok(base64::engine::general_purpose::STANDARD.decode(base64_der)?)
275+
}
137276
}

0 commit comments

Comments
 (0)