scl_lib/
client.rs

1// SPDX-License-Identifier: EUPL-1.2
2pub use async_trait::async_trait;
3
4use reqwest::{
5    header::{self, HeaderMap, HeaderValue},
6    Client, Response,
7};
8pub use reqwest::{Error, StatusCode};
9
10use std::ops::Deref;
11
12use crate::{
13    api_objects::{SclEvent, SclObject},
14    tls_config::TlsConfig,
15};
16
17use crate::LogError;
18
19use self::error::ClientError;
20
21/// This module contains a SclClient-specific error type and associated trait implementations.
22pub mod error {
23    /// A specialized `Result` type for SclClient operations.
24    pub type Result<T> = std::result::Result<T, ClientError>;
25
26    /// General error type for any SclClient errors.
27    #[derive(Debug)]
28    pub enum ClientError {
29        /// Custom error that does not belong to any other category.
30        Custom(String),
31        /// Wrapper for a `reqwest::Error`.
32        Http(reqwest::Error),
33        /// Wrapper for a `serde_json::Error`.
34        Decode(serde_json::Error),
35    }
36
37    impl std::fmt::Display for ClientError {
38        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39            use ClientError::*;
40            match self {
41                Custom(e) => e.fmt(f),
42                Http(e) => e.fmt(f),
43                Decode(e) => e.fmt(f),
44            }
45        }
46    }
47
48    impl std::error::Error for ClientError {}
49
50    impl From<reqwest::Error> for ClientError {
51        fn from(e: reqwest::Error) -> Self {
52            Self::Http(e)
53        }
54    }
55
56    impl From<serde_json::Error> for ClientError {
57        fn from(e: serde_json::Error) -> Self {
58            Self::Decode(e)
59        }
60    }
61}
62
63/// A trait for objects which can be passed as [EventHandler] to the `watch` function of a
64/// [SclClient].
65#[async_trait]
66pub trait EventHandler {
67    type Item: SclObject;
68    async fn handle_event(&mut self, event: SclEvent<Self::Item>);
69}
70
71/// A builder to construct a request object that can be used with the SclClient.
72///
73/// All members are optional and can be omitted. Specify them to create more complex requests.
74///
75/// # Examples
76///
77/// ```
78/// use scl_lib::api_objects::VirtualMachine;
79/// use scl_lib::client::SclRequest;
80///
81/// let req = SclRequest::<VirtualMachine>::new()
82///     .obj_name("vm-01")
83///     .sc("sc-01")
84///     .query(&[("node", "node-01")]);
85/// ```
86#[derive(Clone, Debug, Default)]
87pub struct SclRequest<'a, T: SclObject> {
88    pub obj_name: Option<String>,
89    pub sc_name: Option<String>,
90    pub body: Option<&'a T>,
91    pub query: &'a [(&'a str, &'a str)],
92}
93
94impl<'a, T: SclObject> SclRequest<'a, T> {
95    /// Creates and initializes a new [SclRequest] object.
96    pub fn new() -> Self {
97        Self {
98            obj_name: None,
99            body: None,
100            sc_name: None,
101            query: &[],
102        }
103    }
104
105    /// Sets the name of the request object to use as `T::path()/<name>` when constructing the URL.
106    pub fn obj_name(mut self, name: &str) -> Self {
107        self.obj_name = Some(name.into());
108        self
109    }
110
111    /// Adds a [SclObject] as body data to the request object.
112    pub fn body(mut self, obj: &'a T) -> Self {
113        self.body = Some(obj);
114        self
115    }
116
117    /// Sets the name of the the separation context to use as `/scs/<sc_name>` when constructing
118    /// the URL.
119    pub fn sc(mut self, sc_name: &str) -> Self {
120        self.sc_name = Some(sc_name.into());
121        self
122    }
123
124    /// Specify query parameters for the request.
125    pub fn query(mut self, query: &'a [(&'a str, &'a str)]) -> Self {
126        self.query = query;
127        self
128    }
129
130    /// Constructs a URL path from all member variables and returns it as [String].
131    pub fn url_path(&self) -> String {
132        T::api_endpoint(self.sc_name.as_deref(), self.obj_name.as_deref())
133    }
134}
135
136/// A wrapper object for a [Client] to provide convenient functions for interacting with the SCL
137/// API.
138///
139/// It implements the [Deref] trait so that custom requests can still be made directly via the
140/// wrapped client object.
141///
142/// # Examples
143///
144/// ```no_run
145/// use scl_lib::api_objects::VirtualMachine;
146/// use scl_lib::client::error::ClientError;
147/// use scl_lib::client::{reqwest_client, SclClient, SclRequest};
148///
149/// #[tokio::main]
150/// async fn main() -> Result<(), ClientError> {
151///     // Create client instance
152///     let client = {
153///         let reqwest_client = reqwest_client(None, None)
154///             .map_err(|e| ClientError::Custom(e.to_string()))?;
155///         SclClient::new("https://127.0.0.1:8008/api/v1".to_string(), reqwest_client)
156///     };
157///     // List all VMs of node "node-01"
158///     let vms: Vec<VirtualMachine> = client.list(
159///         &SclRequest::new().query(&[("node", "node-01")])
160///     ).await.unwrap();
161///     // Get the VM with the name "vm-01" of separation context "sc-01"
162///     let vm: VirtualMachine = client.show(
163///         &SclRequest::new().obj_name("vm-01").sc("sc-01")
164///     ).await.unwrap();
165///     // Update the virtual machine
166///     let _ = client.update(
167///         &SclRequest::new().obj_name(vm.metadata.name()).sc(&vm.separation_context).body(&vm)
168///     ).await.unwrap();
169///     // Delete the VM
170///     let _ = client.delete(
171///         &SclRequest::<VirtualMachine>::new().obj_name(vm.metadata.name())
172///     ).await.unwrap();
173///     Ok(())
174/// }
175/// ```
176#[derive(Clone)]
177pub struct SclClient {
178    pub api_url: String,
179    client: Client,
180}
181
182impl SclClient {
183    /// Creates a new [SclClient] from a URL string and an preconfigured [Client].
184    pub fn new(api_url: String, client: Client) -> Self {
185        Self { api_url, client }
186    }
187
188    /// Performs a HTTP GET request to the SCL API to fetch and return all [SclObject]s of type
189    /// `T`.
190    pub async fn list<T: SclObject>(&self, req: &SclRequest<'_, T>) -> Result<Vec<T>, Error> {
191        let url = format!("{}{}", self.api_url, req.url_path());
192        self.client
193            .get(url)
194            .query(req.query)
195            .send()
196            .await
197            .log_err()?
198            .error_for_status()
199            .log_err()?
200            .json::<Vec<T>>()
201            .await
202    }
203
204    /// Performs a HTTP GET request to the SCL API to fetch and return a object of type `T`.
205    pub async fn show<T: SclObject>(&self, req: &SclRequest<'_, T>) -> Result<T, Error> {
206        let url = format!("{}{}", self.api_url, req.url_path());
207        self.client
208            .get(url)
209            .send()
210            .await
211            .log_err()?
212            .error_for_status()
213            .log_err()?
214            .json::<T>()
215            .await
216    }
217
218    /// Performs a HTTP DELETE request to the SCL API to delete a object of type `T`.
219    pub async fn delete<T: SclObject>(&self, req: &SclRequest<'_, T>) -> Result<(), Error> {
220        let url = format!("{}{}", self.api_url, req.url_path());
221        self.client
222            .delete(url)
223            .send()
224            .await
225            .log_err()?
226            .error_for_status()
227            .log_err()?;
228        Ok(())
229    }
230
231    /// Performs a HTTP POST request to the SCL API to create a object of type `T`.
232    pub async fn create<T: SclObject>(&self, req: &SclRequest<'_, T>) -> Result<(), Error> {
233        let url = format!("{}{}", self.api_url, req.url_path());
234        match req.body {
235            Some(body) => self.client.post(url).json(body),
236            None => self.client.post(url),
237        }
238        .send()
239        .await
240        .log_err()?
241        .error_for_status()
242        .log_err()?;
243        Ok(())
244    }
245
246    /// Performs a HTTP PUT request to the SCL API to update a object of type `T`.
247    pub async fn update<T: SclObject>(&self, req: &SclRequest<'_, T>) -> Result<(), Error> {
248        let url = format!("{}{}", self.api_url, req.url_path());
249        match req.body {
250            Some(body) => self.client.put(url).json(body),
251            None => self.client.put(url),
252        }
253        .send()
254        .await
255        .log_err()?
256        .error_for_status()
257        .log_err()?;
258        Ok(())
259    }
260
261    /// Performs an HTTP GET request to the watch endpoint of the item associated with the
262    /// EventHandler and calls the `handle` function for every received SclEvent.
263    ///
264    /// Invalid events are logged and then the function waits for further data. The function either
265    /// returns `Ok(())` when the bytestream ends, or returns an [reqwest::Error] if the connection
266    /// fails or breaks. For an automatic reconnect the function call should be executed in a
267    /// `loop{ }`.
268    pub async fn watch<T: EventHandler>(&self, mut handler: T) -> Result<(), reqwest::Error> {
269        let url = format!(
270            "{}/watch{}",
271            self.api_url,
272            T::Item::api_endpoint(None, None)
273        );
274        let mut resp = self.client.get(url).send().await?.error_for_status()?;
275        while let Some(bytes) = resp.chunk().await.log_err()? {
276            match serde_json::from_slice::<SclEvent<T::Item>>(&bytes) {
277                Ok(event) => handler.handle_event(event).await,
278                Err(err) => log::error!("{}", err),
279            };
280        }
281        Ok(())
282    }
283
284    /// Reads chunks of bytes from an open HTTP stream, parses them into an `SclEvent<T>` object
285    /// and then returns the result. Returns a `client::error::Error` if the stream fails to read
286    /// or the response body has been exhausted.
287    pub async fn recv_event<T: SclObject>(resp: &mut Response) -> error::Result<SclEvent<T>> {
288        if let Some(bytes) = resp.chunk().await.log_err()? {
289            Ok(serde_json::from_slice::<SclEvent<T>>(&bytes)?)
290        } else {
291            Err(error::ClientError::Custom(
292                "Empty Chunk: Response body has been exhausted.".to_string(),
293            ))
294        }
295    }
296}
297
298impl Deref for SclClient {
299    type Target = Client;
300
301    fn deref(&self) -> &Self::Target {
302        &self.client
303    }
304}
305
306pub fn reqwest_client(
307    additional_headers: Option<HeaderMap<HeaderValue>>,
308    tls: Option<&TlsConfig>,
309) -> Result<reqwest::Client, Box<dyn std::error::Error>> {
310    let mut headers = HeaderMap::new();
311    // Set ACCEPT and CONTENT_TYPE headers to application/json
312    headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
313    headers.insert(
314        header::CONTENT_TYPE,
315        HeaderValue::from_static("application/json"),
316    );
317    // Add additional headers
318    if let Some(additional_headers) = additional_headers {
319        for (key, value) in additional_headers.iter() {
320            headers.insert(key, value.clone());
321        }
322    }
323
324    let mut client_builder = Client::builder().default_headers(headers);
325
326    if let Some(tls) = tls {
327        use rustls::{
328            client::{ClientConfig, ServerCertVerifier},
329            Certificate, PrivateKey,
330        };
331        use std::sync::Arc;
332        // rustls wants all certificates and keys in DER format, but we generate them in PEM format.
333        // For this reason we have to convert them into DER.
334        //
335        // Parse CA certificate.
336        let ca_certs: Vec<Certificate> = rustls_pemfile::certs(&mut tls.ca_cert.as_ref())?
337            .into_iter()
338            .map(Certificate)
339            .collect();
340
341        // Parse client certificate.
342        let client_cert = rustls_pemfile::certs(&mut tls.client_cert.as_ref())?
343            .into_iter()
344            .map(Certificate)
345            .collect();
346        // Parse client key.
347        let client_key = PrivateKey(
348            rustls_pemfile::pkcs8_private_keys(&mut tls.client_key.as_ref())?
349                .first()
350                .ok_or("No PKCS8-encoded private key found.".to_string())?
351                .to_vec(),
352        );
353        // Parse certificate revocation list.
354        let crls = {
355            use rustls::{server::UnparsedCertRevocationList, CertRevocationListError};
356
357            rustls_pemfile::crls(&mut tls.crl.as_ref())
358                .map_err(|e| ClientError::Custom(format!("Failed to parse CRL file: {e:?}")))?
359                .into_iter()
360                .map(|crl| UnparsedCertRevocationList(crl).parse())
361                .collect::<Result<Vec<webpki::OwnedCertRevocationList>, CertRevocationListError>>()
362                .map_err(|e| format!("Failed to parse one or more CRL: {e:?}"))?
363        };
364
365        // Use the custom ServerCertCrlVerifier.
366        let custom_verifier = verify::WebPkiVerifierWithCrl::new(ca_certs, crls);
367        let verifier: Arc<dyn ServerCertVerifier> = Arc::new(custom_verifier);
368
369        // Explicitly want this type annotation here as a compile-time check because
370        // `use_preconfigured_tls()` below accepts the `Any` type as an argument
371        // and downcasts to a ClientConfig at runtime.
372        let tls_config: ClientConfig = ClientConfig::builder()
373            .with_safe_defaults()
374            .with_custom_certificate_verifier(verifier)
375            .with_client_auth_cert(client_cert, client_key)?;
376
377        client_builder = client_builder.use_preconfigured_tls(tls_config);
378    }
379
380    Ok(client_builder.build()?)
381}
382
383mod verify {
384    use rustls::{
385        client::{verify_server_name, ServerCertVerified, ServerCertVerifier},
386        server::ParsedCertificate,
387        Certificate, Error,
388    };
389
390    // The elements of this module are a 1:1 copy of the upstream
391    // [rustls::verify module](https://github.com/rustls/rustls/blob/v/0.21.7/rustls/src/verify.rs),
392    // but needs to be redifined because they are not public but required to implement a custom
393    // ServerCertVerifier.
394    //
395    // Copyright (c) 2016 Joseph Birr-Pixton <jpixton@gmail.com>
396    // https://github.com/rustls/rustls/blob/v/0.21.7/LICENSE-MIT
397    mod rustls_verifier {
398        type SignatureAlgorithms = &'static [&'static webpki::SignatureAlgorithm];
399        /// Which signature verification mechanisms we support.  No particular
400        /// order.
401        pub(super) static SUPPORTED_SIG_ALGS: SignatureAlgorithms = &[
402            &webpki::ECDSA_P256_SHA256,
403            &webpki::ECDSA_P256_SHA384,
404            &webpki::ECDSA_P384_SHA256,
405            &webpki::ECDSA_P384_SHA384,
406            &webpki::ED25519,
407            &webpki::RSA_PSS_2048_8192_SHA256_LEGACY_KEY,
408            &webpki::RSA_PSS_2048_8192_SHA384_LEGACY_KEY,
409            &webpki::RSA_PSS_2048_8192_SHA512_LEGACY_KEY,
410            &webpki::RSA_PKCS1_2048_8192_SHA256,
411            &webpki::RSA_PKCS1_2048_8192_SHA384,
412            &webpki::RSA_PKCS1_2048_8192_SHA512,
413            &webpki::RSA_PKCS1_3072_8192_SHA384,
414        ];
415
416        pub(super) fn pki_error(error: webpki::Error) -> rustls::Error {
417            use rustls::{CertRevocationListError, CertificateError};
418            use std::sync::Arc;
419            use webpki::Error::*;
420            match error {
421                BadDer | BadDerTime => CertificateError::BadEncoding.into(),
422                CertNotValidYet => CertificateError::NotValidYet.into(),
423                CertExpired | InvalidCertValidity => CertificateError::Expired.into(),
424                UnknownIssuer => CertificateError::UnknownIssuer.into(),
425                CertNotValidForName => CertificateError::NotValidForName.into(),
426                CertRevoked => CertificateError::Revoked.into(),
427                IssuerNotCrlSigner => CertRevocationListError::IssuerInvalidForCrl.into(),
428
429                InvalidSignatureForPublicKey
430                | UnsupportedSignatureAlgorithm
431                | UnsupportedSignatureAlgorithmForPublicKey => {
432                    CertificateError::BadSignature.into()
433                }
434
435                InvalidCrlSignatureForPublicKey
436                | UnsupportedCrlSignatureAlgorithm
437                | UnsupportedCrlSignatureAlgorithmForPublicKey => {
438                    CertRevocationListError::BadSignature.into()
439                }
440
441                _ => CertificateError::Other(Arc::new(error)).into(),
442            }
443        }
444    }
445    /// Basicly the same as the [upstream WebPkiVerifier](https://github.com/rustls/rustls/blob/v/0.21.7/rustls/src/verify.rs#L387),
446    /// but instead of passing a empty vector to the [`verify_for_usage()` function](https://github.com/rustls/rustls/blob/v/0.21.7/rustls/src/verify.rs#L352)
447    /// the content of the specfied certificate revocation list file is provided as input.
448    pub struct WebPkiVerifierWithCrl {
449        roots: Vec<Certificate>,
450        crls: Vec<webpki::OwnedCertRevocationList>,
451    }
452
453    impl WebPkiVerifierWithCrl {
454        pub fn new(roots: Vec<Certificate>, crls: Vec<webpki::OwnedCertRevocationList>) -> Self {
455            Self { roots, crls }
456        }
457    }
458
459    impl ServerCertVerifier for WebPkiVerifierWithCrl {
460        fn verify_server_cert(
461            &self,
462            end_entity: &Certificate,
463            intermediates: &[Certificate],
464            server_name: &rustls::ServerName,
465            _scts: &mut dyn Iterator<Item = &[u8]>,
466            ocsp_response: &[u8],
467            now: std::time::SystemTime,
468        ) -> Result<ServerCertVerified, Error> {
469            let cert = webpki::EndEntityCert::try_from(end_entity.0.as_ref()).unwrap();
470
471            let chain: Vec<&[u8]> = intermediates.iter().map(|cert| cert.0.as_ref()).collect();
472            let trust_roots: Vec<webpki::TrustAnchor> = self
473                .roots
474                .iter()
475                .map(|crt| webpki::TrustAnchor::try_from_cert_der(&crt.0).unwrap())
476                .collect();
477            let webpki_now =
478                webpki::Time::try_from(now).map_err(|_| Error::FailedToGetCurrentTime)?;
479
480            #[allow(trivial_casts)] // Cast to &dyn trait is required.
481            let crls = self
482                .crls
483                .iter()
484                .map(|crl| crl as &dyn webpki::CertRevocationList)
485                .collect::<Vec<_>>();
486
487            cert.verify_for_usage(
488                rustls_verifier::SUPPORTED_SIG_ALGS,
489                &trust_roots,
490                &chain,
491                webpki_now,
492                webpki::KeyUsage::server_auth(),
493                crls.as_slice(),
494            )
495            .map_err(rustls_verifier::pki_error)?;
496
497            if !ocsp_response.is_empty() {
498                log::trace!("Unvalidated OCSP response: {:?}", ocsp_response.to_vec());
499            }
500            let cert = ParsedCertificate::try_from(end_entity)?;
501
502            verify_server_name(&cert, server_name)?;
503            Ok(ServerCertVerified::assertion())
504        }
505    }
506}