tiedcrossing_client/
lib.rs

1// SPDX-FileCopyrightText: 2022 Profian Inc. <opensource@profian.com>
2// SPDX-License-Identifier: AGPL-3.0-only
3
4#![warn(rust_2018_idioms, unused_lifetimes, unused_qualifications, clippy::all)]
5#![forbid(unsafe_code)]
6
7mod entity;
8mod repo;
9mod tag;
10mod tree;
11mod user;
12
13pub use entity::*;
14pub use repo::*;
15pub use tag::*;
16pub use tree::*;
17pub use user::*;
18
19pub use drawbridge_type as types;
20
21pub use anyhow::{Context, Result};
22pub use mime;
23pub use url::Url;
24
25use std::sync::Arc;
26
27use drawbridge_type::{RepositoryContext, TagContext, TreeContext, UserContext};
28
29use rustls::cipher_suite::{
30    TLS13_AES_128_GCM_SHA256, TLS13_AES_256_GCM_SHA384, TLS13_CHACHA20_POLY1305_SHA256,
31};
32use rustls::kx_group::{SECP256R1, SECP384R1, X25519};
33use rustls::version::TLS13;
34use rustls::{Certificate, OwnedTrustAnchor, PrivateKey, RootCertStore};
35
36#[derive(Clone, Debug)]
37pub struct Client {
38    inner: ureq::Agent,
39    root: Url,
40    token: Option<String>,
41}
42
43impl Client {
44    pub fn builder(url: Url) -> ClientBuilder {
45        ClientBuilder::new(url)
46    }
47
48    pub fn new(url: Url) -> Result<Self> {
49        Self::builder(url).build()
50    }
51
52    fn url(&self, path: &str) -> Result<Url> {
53        format!("{}{path}", self.root)
54            .parse()
55            .context("failed to construct URL")
56    }
57
58    pub fn user(&self, UserContext { name }: &UserContext) -> User<'_> {
59        User::new(Entity::new(self), name)
60    }
61
62    pub fn repository<'a>(
63        &'a self,
64        RepositoryContext { owner, name }: &'a RepositoryContext,
65    ) -> Repository<'_> {
66        self.user(owner).repository(name)
67    }
68
69    pub fn tag<'a>(&'a self, TagContext { repository, name }: &'a TagContext) -> Tag<'_> {
70        self.repository(repository).tag(name)
71    }
72
73    pub fn tree<'a>(&'a self, TreeContext { tag, path }: &'a TreeContext) -> Node<'_> {
74        self.tag(tag).path(path)
75    }
76}
77
78#[derive(Clone, Debug)]
79pub struct ClientBuilder {
80    url: Url,
81    credentials: Option<(Vec<Certificate>, PrivateKey)>,
82    roots: Option<RootCertStore>,
83    token: Option<String>,
84}
85
86impl ClientBuilder {
87    pub fn new(url: Url) -> Self {
88        Self {
89            url,
90            credentials: None,
91            roots: None,
92            token: None,
93        }
94    }
95
96    pub fn credentials(self, cert: Vec<Certificate>, key: PrivateKey) -> Self {
97        Self {
98            credentials: Some((cert, key)),
99            ..self
100        }
101    }
102
103    pub fn roots(self, roots: RootCertStore) -> Self {
104        Self {
105            roots: Some(roots),
106            ..self
107        }
108    }
109
110    pub fn token(self, token: impl Into<String>) -> Self {
111        Self {
112            token: Some(token.into()),
113            ..self
114        }
115    }
116
117    pub fn build(self) -> Result<Client> {
118        let root = self
119            .url
120            .join(&format!("api/v{}", env!("CARGO_PKG_VERSION")))
121            .context("failed to contruct URL")?;
122
123        let tls = rustls::ClientConfig::builder()
124            .with_cipher_suites(&[
125                TLS13_AES_256_GCM_SHA384,
126                TLS13_AES_128_GCM_SHA256,
127                TLS13_CHACHA20_POLY1305_SHA256,
128            ])
129            .with_kx_groups(&[&X25519, &SECP384R1, &SECP256R1])
130            .with_protocol_versions(&[&TLS13])?
131            .with_root_certificates(if let Some(roots) = self.roots {
132                roots
133            } else {
134                let mut root_store = RootCertStore::empty();
135                root_store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.iter().map(
136                    |ta| {
137                        OwnedTrustAnchor::from_subject_spki_name_constraints(
138                            ta.subject,
139                            ta.spki,
140                            ta.name_constraints,
141                        )
142                    },
143                ));
144                root_store
145            });
146        let tls = if let Some((cert, key)) = self.credentials {
147            tls.with_single_cert(cert, key)?
148        } else {
149            tls.with_no_client_auth()
150        };
151
152        Ok(Client {
153            inner: ureq::AgentBuilder::new().tls_config(Arc::new(tls)).build(),
154            root,
155            token: self.token,
156        })
157    }
158}