@@ -113,9 +113,55 @@ fn parse_google_uri(uri: &Uri) -> Option<(String, PathBuf)> {
113113
114114#[ cfg( test) ]
115115mod 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 \n range: 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