Fork of daniellemaywood.uk/gleam — Wasm codegen work
4.0 kB
126 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2021 The Gleam contributors
3
4use std::convert::TryInto;
5use std::sync::OnceLock;
6
7use async_trait::async_trait;
8use camino::Utf8PathBuf;
9use gleam_core::{
10 Error, Result,
11 error::{FileIoAction, FileKind},
12};
13use http::{Request, Response};
14use reqwest::{Certificate, Client};
15
16use crate::fs;
17
18static REQWEST_CLIENT: OnceLock<Client> = OnceLock::new();
19
20#[derive(Debug)]
21pub struct HttpClient;
22
23impl HttpClient {
24 pub fn new() -> Self {
25 Self
26 }
27
28 pub fn boxed() -> Box<Self> {
29 Box::new(Self::new())
30 }
31}
32
33#[async_trait]
34impl gleam_core::io::HttpClient for HttpClient {
35 async fn send(&self, request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>> {
36 tracing::debug!(
37 method = request.method().as_str(),
38 url = request.uri().to_string(),
39 "http-send",
40 );
41 let request = request
42 .try_into()
43 .expect("Unable to convert HTTP request for use by reqwest library");
44 let client = init_client().map_err(Error::http)?;
45 let mut response = client.execute(request).await.map_err(Error::http)?;
46 let mut builder = Response::builder()
47 .status(response.status())
48 .version(response.version());
49 if let Some(headers) = builder.headers_mut() {
50 std::mem::swap(headers, response.headers_mut());
51 }
52 builder
53 .body(response.bytes().await.map_err(Error::http)?.to_vec())
54 .map_err(Error::http)
55 }
56}
57
58fn init_client() -> Result<&'static Client, Error> {
59 if let Some(client) = REQWEST_CLIENT.get() {
60 return Ok(client);
61 }
62
63 let client = build_client()?;
64 Ok(REQWEST_CLIENT.get_or_init(|| client))
65}
66
67/// Build the HTTP client with an appropriate certificate trust store.
68///
69/// On most platforms reqwest's default `rustls` configuration uses
70/// `rustls-platform-verifier`, which delegates to the operating system's trust
71/// manager. This respects OS-installed and enterprise root certificates along
72/// with the system's own trust decisions.
73///
74/// On Android that verifier reaches the trust manager by calling into the JVM,
75/// and without it panics on the first request (see issue #5823). There we read
76/// the same system trust store from the filesystem and configure those roots
77/// explicitly instead.
78fn build_client() -> Result<Client, Error> {
79 if cfg!(target_os = "android") {
80 let mut certificates = system_certificates();
81 if let Ok(certificate_path) = std::env::var("GLEAM_CACERTS_PATH") {
82 let certificate = read_certificate(&certificate_path)?;
83 certificates.push(certificate);
84 }
85
86 Client::builder()
87 .tls_certs_only(certificates)
88 .build()
89 .map_err(Error::http)
90 } else {
91 let Ok(certificate_path) = std::env::var("GLEAM_CACERTS_PATH") else {
92 return Client::builder().build().map_err(Error::http);
93 };
94
95 tracing::trace!("Using GLEAM_CACERTS_PATH environment variable");
96 let certificate = read_certificate(&certificate_path)?;
97 Client::builder()
98 .add_root_certificate(certificate)
99 .build()
100 .map_err(Error::http)
101 }
102}
103
104fn read_certificate(path: &str) -> Result<Certificate, Error> {
105 let bytes = fs::read_bytes(path)?;
106 Certificate::from_pem(&bytes).map_err(|error| Error::FileIo {
107 kind: FileKind::File,
108 action: FileIoAction::Parse,
109 path: Utf8PathBuf::from(path),
110 err: Some(error.to_string()),
111 })
112}
113
114/// Load the system trust store (only used on Android)
115fn system_certificates() -> Vec<Certificate> {
116 let loaded = rustls_native_certs::load_native_certs();
117 for error in &loaded.errors {
118 tracing::warn!("Failed to load a system certificate: {error}");
119 }
120
121 loaded
122 .certs
123 .iter()
124 .filter_map(|certificate| Certificate::from_der(certificate.as_ref()).ok())
125 .collect()
126}