OXYGENHUB

Coverage / crates/oxy-strata/src/http.rs 17.3%

14 of 81 lines run, 3 of 26 functions · run #260 on main · run missed

1
//! The HTTP client the cloud stores send their requests through: `ureq`
2
//! on rustls with the web roots, the client oxy-core itself uses, so the
3
//! stores cross-compile for every target oxy is released for. Each
4
//! request runs on the runtime's blocking pool; bodies are held whole,
5
//! which is how the stores use them (a bundle, a record, a listing).
6
7
use async_trait::async_trait;
8
use bytes::Bytes;
9
use http_body_util::{BodyExt, Full};
10
use object_store::client::{
11
    ClientOptions, HttpClient, HttpConnector, HttpError, HttpErrorKind, HttpRequest, HttpResponse,
12
    HttpResponseBody, HttpService,
13
};
14
use std::io::Read;
15
use std::time::Duration;
16
17
/// How long a request may take in all, and to connect.
18
const TIMEOUT: Duration = Duration::from_secs(600);
19
const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
20
21
/// A rustls configuration trusting the web roots.
222×
fn tls() -> Result<std::sync::Arc<rustls::ClientConfig>, String> {
232×
    oxy_core::protocol::tls_client_config(None).map_err(|e| e.to_string())
242×
}
25
26
/// Make the agent every request of a store goes through.
272×
pub fn agent() -> Result<ureq::Agent, String> {
282×
    Ok(ureq::AgentBuilder::new()
292×
        .tls_config(tls()?)
302×
        .timeout_connect(CONNECT_TIMEOUT)
312×
        .timeout(TIMEOUT)
322×
        .build())
332×
}
34
35
#[derive(Debug, Clone, Copy, Default)]
36
pub struct UreqConnector;
37
38
impl HttpConnector for UreqConnector {
392×
    fn connect(&self, _options: &ClientOptions) -> object_store::Result<HttpClient> {
402×
        let agent = agent().map_err(|e| object_store::Error::Generic {
41
            store: "ureq",
420×
            source: e.into(),
430×
        })?;
442×
        Ok(HttpClient::new(UreqService { agent }))
452×
    }
46
}
47
48
#[derive(Debug, Clone)]
49
struct UreqService {
50
    agent: ureq::Agent,
51
}
52
53
#[async_trait]
54
impl HttpService for UreqService {
550×
    async fn call(&self, req: HttpRequest) -> Result<HttpResponse, HttpError> {
56
        let (parts, body) = req.into_parts();
57
        let bytes = body.collect().await?.to_bytes();
58
        let agent = self.agent.clone();
590×
        tokio::task::spawn_blocking(move || send(&agent, parts, &bytes))
60
            .await
610×
            .map_err(|e| HttpError::new(HttpErrorKind::Interrupted, e))?
620×
    }
63
}
64
650×
fn send(
660×
    agent: &ureq::Agent,
670×
    parts: http::request::Parts,
680×
    bytes: &[u8],
690×
) -> Result<HttpResponse, HttpError> {
700×
    let url = parts.uri.to_string();
710×
    let mut r = agent.request(parts.method.as_str(), &url);
720×
    for (name, value) in &parts.headers {
730×
        if let Ok(v) = value.to_str() {
740×
            r = r.set(name.as_str(), v);
750×
        }
76
    }
770×
    let sent = if bytes.is_empty()
780×
        && matches!(
790×
            parts.method,
80
            http::Method::GET | http::Method::HEAD | http::Method::DELETE
81
        ) {
820×
        r.call()
83
    } else {
840×
        r.send_bytes(bytes)
85
    };
860×
    let resp = match sent {
870×
        Ok(resp) => resp,
88
        // A refusal is a response the store interprets (a 404, a 412).
890×
        Err(ureq::Error::Status(_, resp)) => resp,
900×
        Err(ureq::Error::Transport(t)) => {
910×
            let kind = match t.kind() {
92
                ureq::ErrorKind::Dns
93
                | ureq::ErrorKind::ConnectionFailed
940×
                | ureq::ErrorKind::ProxyConnect => HttpErrorKind::Connect,
950×
                ureq::ErrorKind::Io => HttpErrorKind::Timeout,
960×
                _ => HttpErrorKind::Request,
97
            };
980×
            return Err(HttpError::new(kind, t));
99
        }
100
    };
1010×
    let status = http::StatusCode::from_u16(resp.status())
1020×
        .map_err(|e| HttpError::new(HttpErrorKind::Decode, e))?;
1030×
    let mut builder = http::Response::builder().status(status);
1040×
    for name in resp.headers_names() {
1050×
        for value in resp.all(&name) {
1060×
            builder = builder.header(name.as_str(), value);
1070×
        }
108
    }
1090×
    let mut body = Vec::new();
1100×
    resp.into_reader()
1110×
        .read_to_end(&mut body)
1120×
        .map_err(|e| HttpError::new(HttpErrorKind::Decode, e))?;
1130×
    let body = HttpResponseBody::new(
1140×
        Full::new(Bytes::from(body)).map_err(|never: std::convert::Infallible| match never {}),
115
    );
1160×
    builder
1170×
        .body(body)
1180×
        .map_err(|e| HttpError::new(HttpErrorKind::Decode, e))
1190×
}
120
121
/// The region that holds an S3 bucket, as S3 itself reports it in
122
/// `x-amz-bucket-region` on any request, even a refused one.
1230×
pub fn bucket_region(bucket: &str) -> Option<String> {
1240×
    let agent = ureq::AgentBuilder::new()
1250×
        .tls_config(tls().ok()?)
1260×
        .timeout(Duration::from_secs(30))
1270×
        .redirects(0)
1280×
        .build();
129
    // S3's wildcard certificate covers one label: dotted names go by path.
1300×
    let url = if bucket.contains('.') {
1310×
        format!("https://s3.amazonaws.com/{bucket}")
132
    } else {
1330×
        format!("https://{bucket}.s3.amazonaws.com")
134
    };
1350×
    let resp = match agent.head(&url).call() {
1360×
        Ok(r) | Err(ureq::Error::Status(_, r)) => r,
1370×
        Err(_) => return None,
138
    };
1390×
    resp.header("x-amz-bucket-region").map(str::to_string)
1400×
}