tiedcrossing_client/
lib.rs1#![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}