]>
Commit | Line | Data |
---|---|---|
ffe908f6 | 1 | use std::error::Error as StdError; |
25024fa6 | 2 | use std::future::Future; |
1c96afd0 | 3 | use std::pin::Pin; |
25024fa6 | 4 | use std::sync::Arc; |
0f19f212 | 5 | use std::sync::Mutex; |
25024fa6 WB |
6 | |
7 | use http::request::Request; | |
25024fa6 WB |
8 | use http::uri::PathAndQuery; |
9 | use http::{StatusCode, Uri}; | |
1c96afd0 WB |
10 | use hyper::body::{Body, HttpBody}; |
11 | use openssl::hash::MessageDigest; | |
12 | use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; | |
13 | use openssl::x509::{self, X509}; | |
14 | use serde::Serialize; | |
25024fa6 | 15 | |
1c96afd0 | 16 | use proxmox_login::ticket::Validity; |
0f19f212 | 17 | use proxmox_login::{Login, SecondFactorChallenge, TicketResult}; |
25024fa6 WB |
18 | |
19 | use crate::auth::AuthenticationKind; | |
604e4676 | 20 | use crate::error::ParseFingerprintError; |
0f19f212 | 21 | use crate::{Error, Token}; |
25024fa6 | 22 | |
1c96afd0 | 23 | use super::{HttpApiClient, HttpApiResponse}; |
25024fa6 | 24 | |
1c96afd0 WB |
25 | #[derive(Default)] |
26 | pub enum TlsOptions { | |
27 | /// Default TLS verification. | |
28 | #[default] | |
29 | Verify, | |
30 | ||
31 | /// Insecure: ignore invalid certificates. | |
32 | Insecure, | |
33 | ||
34 | /// Expect a specific certificate fingerprint. | |
35 | Fingerprint(Vec<u8>), | |
36 | ||
37 | /// Verify with a specific PEM formatted CA. | |
38 | CaCert(X509), | |
39 | ||
40 | /// Use a callback for certificate verification. | |
41 | Callback(Box<dyn Fn(bool, &mut x509::X509StoreContextRef) -> bool + Send + Sync + 'static>), | |
25024fa6 WB |
42 | } |
43 | ||
604e4676 WB |
44 | impl TlsOptions { |
45 | pub fn parse_fingerprint(fp: &str) -> Result<Self, ParseFingerprintError> { | |
46 | use hex::FromHex; | |
47 | ||
48 | let hex: Vec<u8> = fp | |
49 | .as_bytes() | |
50 | .iter() | |
51 | .copied() | |
52 | .filter(|&b| b != b':') | |
53 | .collect(); | |
54 | ||
55 | let fp = <[u8; 32]>::from_hex(&hex).map_err(|_| ParseFingerprintError)?; | |
56 | ||
57 | Ok(Self::Fingerprint(fp.into())) | |
58 | } | |
59 | } | |
60 | ||
1c96afd0 WB |
61 | /// A Proxmox API client base backed by a [`proxmox_http::Client`]. |
62 | pub struct Client { | |
a7435e75 | 63 | api_url: Uri, |
0f19f212 | 64 | auth: Mutex<Option<Arc<AuthenticationKind>>>, |
1c96afd0 | 65 | client: Arc<proxmox_http::client::Client>, |
25024fa6 WB |
66 | pve_compat: bool, |
67 | } | |
68 | ||
1c96afd0 WB |
69 | impl Client { |
70 | /// Create a new client instance which will connect to the provided endpoint. | |
71 | pub fn new(api_url: Uri) -> Self { | |
72 | Client::with_client(api_url, Arc::new(proxmox_http::client::Client::new())) | |
ba6a6286 WB |
73 | } |
74 | ||
1c96afd0 WB |
75 | /// Instantiate a client for an API with a given HTTP client instance. |
76 | pub fn with_client(api_url: Uri, client: Arc<proxmox_http::client::Client>) -> Self { | |
77 | Self { | |
78 | api_url, | |
79 | auth: Mutex::new(None), | |
80 | client, | |
81 | pve_compat: false, | |
82 | } | |
83 | } | |
84 | ||
85 | /// Create a new client instance which will connect to the provided endpoint. | |
86 | pub fn with_options( | |
87 | api_url: Uri, | |
88 | tls_options: TlsOptions, | |
89 | http_options: proxmox_http::HttpOptions, | |
90 | ) -> Result<Self, Error> { | |
91 | let mut connector = SslConnector::builder(SslMethod::tls_client()) | |
92 | .map_err(|err| Error::internal("failed to create ssl connector builder", err))?; | |
93 | ||
94 | match tls_options { | |
95 | TlsOptions::Verify => (), | |
96 | TlsOptions::Insecure => connector.set_verify(SslVerifyMode::NONE), | |
97 | TlsOptions::Fingerprint(expected_fingerprint) => { | |
98 | connector.set_verify_callback(SslVerifyMode::PEER, move |valid, chain| { | |
99 | if valid { | |
100 | return true; | |
101 | } | |
102 | verify_fingerprint(chain, &expected_fingerprint) | |
103 | }); | |
104 | } | |
105 | TlsOptions::Callback(cb) => { | |
106 | connector | |
107 | .set_verify_callback(SslVerifyMode::PEER, move |valid, chain| cb(valid, chain)); | |
108 | } | |
109 | TlsOptions::CaCert(ca) => { | |
110 | let mut store = openssl::x509::store::X509StoreBuilder::new().map_err(|err| { | |
111 | Error::internal("failed to create certificate store builder", err) | |
112 | })?; | |
113 | store | |
114 | .add_cert(ca) | |
115 | .map_err(|err| Error::internal("failed to build certificate store", err))?; | |
116 | connector.set_cert_store(store.build()); | |
117 | } | |
118 | } | |
119 | ||
120 | let client = | |
121 | proxmox_http::client::Client::with_ssl_connector(connector.build(), http_options); | |
122 | ||
123 | Ok(Self::with_client(api_url, Arc::new(client))) | |
124 | } | |
125 | ||
126 | /// Get the underlying client object. | |
127 | pub fn http_client(&self) -> &Arc<proxmox_http::client::Client> { | |
128 | &self.client | |
ba6a6286 WB |
129 | } |
130 | ||
25024fa6 WB |
131 | /// Get a reference to the current authentication information. |
132 | pub fn authentication(&self) -> Option<Arc<AuthenticationKind>> { | |
133 | self.auth.lock().unwrap().clone() | |
134 | } | |
135 | ||
a909d578 WB |
136 | /// Get a serialized version of the ticket if one is used. |
137 | /// | |
138 | /// This returns `None` when using an API token and `Error::Unauthorized` if not logged in. | |
139 | pub fn serialize_ticket(&self) -> Result<Option<Vec<u8>>, Error> { | |
140 | let auth = self.authentication().ok_or(Error::Unauthorized)?; | |
141 | let auth = match &*auth { | |
142 | AuthenticationKind::Token(_) => return Ok(None), | |
143 | AuthenticationKind::Ticket(auth) => auth, | |
144 | }; | |
145 | Ok(Some(serde_json::to_vec(auth).map_err(|err| { | |
146 | Error::internal("failed to serialize ticket", err) | |
147 | })?)) | |
148 | } | |
149 | ||
bea97ccc | 150 | #[deprecated(note = "use set_authentication instead")] |
1c96afd0 | 151 | /// Replace the authentication information with an API token. |
25024fa6 | 152 | pub fn use_api_token(&self, token: Token) { |
bea97ccc WB |
153 | self.set_authentication(token); |
154 | } | |
155 | ||
156 | /// Replace the currently used authentication. | |
157 | /// | |
158 | /// This can be a `Token` or an [`Authentication`](proxmox_login::Authentication). | |
159 | pub fn set_authentication(&self, auth: impl Into<AuthenticationKind>) { | |
160 | *self.auth.lock().unwrap() = Some(Arc::new(auth.into())); | |
25024fa6 | 161 | } |
25024fa6 | 162 | |
1c96afd0 WB |
163 | /// Drop the current authentication information. |
164 | pub fn logout(&self) { | |
165 | self.auth.lock().unwrap().take(); | |
166 | } | |
25024fa6 | 167 | |
25024fa6 WB |
168 | /// Enable Proxmox VE login API compatibility. This is required to support TFA authentication |
169 | /// on Proxmox VE APIs which require the `new-format` option. | |
170 | pub fn set_pve_compatibility(&mut self, compatibility: bool) { | |
171 | self.pve_compat = compatibility; | |
172 | } | |
25024fa6 | 173 | |
1c96afd0 WB |
174 | /// Get the currently used API url. |
175 | pub fn api_url(&self) -> &Uri { | |
176 | &self.api_url | |
177 | } | |
178 | ||
179 | /// Build a URI relative to the current API endpoint. | |
180 | fn build_uri(&self, path_and_query: &str) -> Result<Uri, Error> { | |
181 | let parts = self.api_url.clone().into_parts(); | |
182 | let mut builder = http::uri::Builder::new(); | |
183 | if let Some(scheme) = parts.scheme { | |
184 | builder = builder.scheme(scheme); | |
185 | } | |
186 | if let Some(authority) = parts.authority { | |
187 | builder = builder.authority(authority) | |
188 | } | |
189 | builder | |
190 | .path_and_query( | |
191 | path_and_query | |
192 | .parse::<PathAndQuery>() | |
193 | .map_err(|err| Error::internal("failed to parse uri", err))?, | |
194 | ) | |
195 | .build() | |
196 | .map_err(|err| Error::internal("failed to build Uri", err)) | |
197 | } | |
198 | ||
199 | /// Perform an *unauthenticated* HTTP request. | |
200 | async fn authenticated_request( | |
201 | client: Arc<proxmox_http::client::Client>, | |
202 | auth: Arc<AuthenticationKind>, | |
203 | method: http::Method, | |
204 | uri: Uri, | |
205 | json_body: Option<String>, | |
206 | ) -> Result<HttpApiResponse, Error> { | |
f20f9bb9 WB |
207 | let request = auth.set_auth_headers(Request::builder().method(method).uri(uri)); |
208 | ||
209 | let request = if let Some(body) = json_body { | |
210 | request | |
211 | .header(http::header::CONTENT_TYPE, "application/json") | |
212 | .body(body.into()) | |
213 | } else { | |
214 | request.body(Default::default()) | |
215 | } | |
216 | .map_err(|err| Error::internal("failed to build request", err))?; | |
1c96afd0 WB |
217 | |
218 | let response = client.request(request).await.map_err(Error::Anyhow)?; | |
219 | ||
220 | if response.status() == StatusCode::UNAUTHORIZED { | |
221 | return Err(Error::Unauthorized); | |
222 | } | |
223 | ||
224 | let (response, body) = response.into_parts(); | |
225 | let body = read_body(body).await?; | |
226 | ||
227 | if !response.status.is_success() { | |
228 | // FIXME: Decode json errors... | |
229 | //match serde_json::from_slice(&data) | |
230 | // Ok(value) => | |
231 | // if value["error"] | |
232 | let data = | |
233 | String::from_utf8(body).map_err(|_| Error::Other("API returned non-utf8 data"))?; | |
234 | ||
235 | return Err(Error::api(response.status, data)); | |
25024fa6 | 236 | } |
1c96afd0 | 237 | |
ffe908f6 WB |
238 | let content_type = match response.headers.get(http::header::CONTENT_TYPE) { |
239 | None => None, | |
240 | Some(value) => Some( | |
241 | value | |
242 | .to_str() | |
243 | .map_err(|err| Error::internal("bad Content-Type header", err))? | |
244 | .to_owned(), | |
245 | ), | |
246 | }; | |
247 | ||
1c96afd0 WB |
248 | Ok(HttpApiResponse { |
249 | status: response.status.as_u16(), | |
ffe908f6 | 250 | content_type, |
1c96afd0 WB |
251 | body, |
252 | }) | |
25024fa6 WB |
253 | } |
254 | ||
0f19f212 | 255 | /// Assert that we are authenticated and return the `AuthenticationKind`. |
1c96afd0 | 256 | /// Otherwise returns `Error::Unauthorized`. |
0f19f212 | 257 | pub fn login_auth(&self) -> Result<Arc<AuthenticationKind>, Error> { |
25024fa6 WB |
258 | self.auth |
259 | .lock() | |
260 | .unwrap() | |
261 | .clone() | |
0f19f212 | 262 | .ok_or_else(|| Error::Unauthorized) |
25024fa6 WB |
263 | } |
264 | ||
1c96afd0 WB |
265 | /// Check to see if we need to refresh the ticket. Note that it is an error to call this when |
266 | /// logged out, which will return `Error::Unauthorized`. | |
267 | /// | |
268 | /// Tokens are always valid. | |
269 | pub fn ticket_validity(&self) -> Result<Validity, Error> { | |
270 | match &*self.login_auth()? { | |
271 | AuthenticationKind::Token(_) => Ok(Validity::Valid), | |
272 | AuthenticationKind::Ticket(auth) => Ok(auth.ticket.validity()), | |
273 | } | |
274 | } | |
275 | ||
276 | /// If the ticket expires soon (has a validity of [`Validity::Refresh`]), this will attempt to | |
277 | /// refresh the ticket. | |
278 | pub async fn maybe_refresh_ticket(&self) -> Result<(), Error> { | |
279 | if let Validity::Refresh = self.ticket_validity()? { | |
280 | self.refresh_ticket().await?; | |
281 | } | |
282 | ||
283 | Ok(()) | |
284 | } | |
285 | ||
ffe908f6 WB |
286 | async fn do_login_request(&self, request: proxmox_login::Request) -> Result<Vec<u8>, Error> { |
287 | let request = http::Request::builder() | |
288 | .method(http::Method::POST) | |
289 | .uri(request.url) | |
290 | .header(http::header::CONTENT_TYPE, request.content_type) | |
291 | .header( | |
292 | http::header::CONTENT_LENGTH, | |
293 | request.content_length.to_string(), | |
294 | ) | |
295 | .body(request.body.into()) | |
296 | .map_err(|err| Error::internal("error building login http request", err))?; | |
297 | ||
298 | let api_response = self.client.request(request).await.map_err(Error::Anyhow)?; | |
299 | if !api_response.status().is_success() { | |
300 | return Err(Error::api(api_response.status(), "authentication failed")); | |
301 | } | |
302 | ||
303 | let (_, body) = api_response.into_parts(); | |
304 | let body = read_body(body).await?; | |
305 | ||
306 | Ok(body) | |
307 | } | |
308 | ||
1c96afd0 WB |
309 | /// Attempt to refresh the current ticket. |
310 | /// | |
311 | /// If not logged in at all yet, `Error::Unauthorized` will be returned. | |
312 | pub async fn refresh_ticket(&self) -> Result<(), Error> { | |
313 | let auth = self.login_auth()?; | |
314 | let auth = match &*auth { | |
315 | AuthenticationKind::Token(_) => return Ok(()), | |
316 | AuthenticationKind::Ticket(auth) => auth, | |
317 | }; | |
318 | ||
319 | let login = Login::renew(self.api_url.to_string(), auth.ticket.to_string()) | |
320 | .map_err(Error::Ticket)?; | |
1c96afd0 | 321 | |
ffe908f6 | 322 | let api_response = self.do_login_request(login.request()).await?; |
1c96afd0 | 323 | |
ffe908f6 | 324 | match login.response(&api_response)? { |
1c96afd0 WB |
325 | TicketResult::Full(auth) => { |
326 | *self.auth.lock().unwrap() = Some(Arc::new(auth.into())); | |
327 | Ok(()) | |
328 | } | |
329 | TicketResult::TfaRequired(_) => Err(proxmox_login::error::ResponseError::Msg( | |
330 | "ticket refresh returned a TFA challenge", | |
331 | ) | |
332 | .into()), | |
333 | } | |
334 | } | |
ffe908f6 WB |
335 | |
336 | /// Attempt to login. | |
337 | /// | |
338 | /// This will propagate the PVE compatibility state and then perform the `Login` request via | |
339 | /// the inner http client. | |
340 | /// | |
341 | /// If the authentication is complete, `None` is returned and the authentication state updated. | |
342 | /// If a 2nd factor is required, `Some` is returned. | |
343 | pub async fn login(&self, login: Login) -> Result<Option<SecondFactorChallenge>, Error> { | |
344 | let login = login.pve_compatibility(self.pve_compat); | |
345 | ||
346 | let api_response = self.do_login_request(login.request()).await?; | |
347 | ||
348 | Ok(match login.response(&api_response)? { | |
349 | TicketResult::TfaRequired(challenge) => Some(challenge), | |
350 | TicketResult::Full(auth) => { | |
351 | *self.auth.lock().unwrap() = Some(Arc::new(auth.into())); | |
352 | None | |
353 | } | |
354 | }) | |
355 | } | |
356 | ||
357 | /// Attempt to finish a 2nd factor login. | |
358 | /// | |
359 | /// This will propagate the PVE compatibility state and then perform the `Login` request via | |
360 | /// the inner http client. | |
361 | pub async fn login_tfa( | |
362 | &self, | |
363 | challenge: SecondFactorChallenge, | |
364 | challenge_response: proxmox_login::Request, | |
365 | ) -> Result<(), Error> { | |
366 | let api_response = self.do_login_request(challenge_response).await?; | |
367 | ||
368 | let auth = challenge.response(&api_response)?; | |
369 | *self.auth.lock().unwrap() = Some(Arc::new(auth.into())); | |
370 | Ok(()) | |
371 | } | |
1c96afd0 WB |
372 | } |
373 | ||
374 | async fn read_body(mut body: Body) -> Result<Vec<u8>, Error> { | |
375 | let mut data = Vec::<u8>::new(); | |
376 | while let Some(more) = body.data().await { | |
377 | let more = more.map_err(|err| Error::internal("error reading response body", err))?; | |
378 | data.extend(&more[..]); | |
379 | } | |
380 | Ok(data) | |
381 | } | |
382 | ||
383 | impl HttpApiClient for Client { | |
ffe908f6 WB |
384 | type ResponseFuture<'a> = |
385 | Pin<Box<dyn Future<Output = Result<HttpApiResponse, Error>> + Send + 'a>>; | |
1c96afd0 | 386 | |
ffe908f6 | 387 | fn get<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> { |
1c96afd0 | 388 | Box::pin(async move { |
ffe908f6 WB |
389 | let auth = self.login_auth()?; |
390 | let uri = self.build_uri(path_and_query)?; | |
391 | let client = Arc::clone(&self.client); | |
1c96afd0 WB |
392 | Self::authenticated_request(client, auth, http::Method::GET, uri, None).await |
393 | }) | |
394 | } | |
395 | ||
ffe908f6 | 396 | fn post<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a> |
1c96afd0 WB |
397 | where |
398 | T: ?Sized + Serialize, | |
399 | { | |
ffe908f6 WB |
400 | let params = serde_json::to_string(params) |
401 | .map_err(|err| Error::internal("failed to serialize parametres", err)); | |
402 | ||
1c96afd0 | 403 | Box::pin(async move { |
ffe908f6 WB |
404 | let params = params?; |
405 | let auth = self.login_auth()?; | |
406 | let uri = self.build_uri(path_and_query)?; | |
407 | let client = Arc::clone(&self.client); | |
1c96afd0 WB |
408 | Self::authenticated_request(client, auth, http::Method::POST, uri, Some(params)).await |
409 | }) | |
410 | } | |
411 | ||
022fdacb DM |
412 | fn post_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> { |
413 | Box::pin(async move { | |
414 | let auth = self.login_auth()?; | |
415 | let uri = self.build_uri(path_and_query)?; | |
416 | let client = Arc::clone(&self.client); | |
228ce9d6 | 417 | Self::authenticated_request(client, auth, http::Method::POST, uri, None).await |
022fdacb DM |
418 | }) |
419 | } | |
420 | ||
a3322e49 WB |
421 | fn put<'a, T>(&'a self, path_and_query: &'a str, params: &T) -> Self::ResponseFuture<'a> |
422 | where | |
423 | T: ?Sized + Serialize, | |
424 | { | |
425 | let params = serde_json::to_string(params) | |
426 | .map_err(|err| Error::internal("failed to serialize parametres", err)); | |
427 | ||
428 | Box::pin(async move { | |
429 | let params = params?; | |
430 | let auth = self.login_auth()?; | |
431 | let uri = self.build_uri(path_and_query)?; | |
432 | let client = Arc::clone(&self.client); | |
433 | Self::authenticated_request(client, auth, http::Method::PUT, uri, Some(params)).await | |
434 | }) | |
435 | } | |
436 | ||
437 | fn put_without_body<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> { | |
438 | Box::pin(async move { | |
439 | let auth = self.login_auth()?; | |
440 | let uri = self.build_uri(path_and_query)?; | |
441 | let client = Arc::clone(&self.client); | |
442 | Self::authenticated_request(client, auth, http::Method::PUT, uri, None).await | |
443 | }) | |
444 | } | |
445 | ||
ffe908f6 | 446 | fn delete<'a>(&'a self, path_and_query: &'a str) -> Self::ResponseFuture<'a> { |
1c96afd0 | 447 | Box::pin(async move { |
ffe908f6 WB |
448 | let auth = self.login_auth()?; |
449 | let uri = self.build_uri(path_and_query)?; | |
450 | let client = Arc::clone(&self.client); | |
1c96afd0 WB |
451 | Self::authenticated_request(client, auth, http::Method::DELETE, uri, None).await |
452 | }) | |
453 | } | |
454 | } | |
455 | ||
1c96afd0 WB |
456 | fn verify_fingerprint(chain: &x509::X509StoreContextRef, expected_fingerprint: &[u8]) -> bool { |
457 | let Some(cert) = chain.current_cert() else { | |
458 | log::error!("no certificate in chain?"); | |
459 | return false; | |
460 | }; | |
461 | ||
462 | let fp = match cert.digest(MessageDigest::sha256()) { | |
463 | Err(err) => { | |
464 | log::error!("error calculating certificate fingerprint: {err}"); | |
465 | return false; | |
466 | } | |
467 | Ok(fp) => fp, | |
468 | }; | |
469 | ||
470 | if expected_fingerprint != fp.as_ref() { | |
471 | log::error!("bad fingerprint: {}", fp_string(&fp)); | |
472 | log::error!("expected fingerprint: {}", fp_string(&expected_fingerprint)); | |
473 | return false; | |
474 | } | |
475 | ||
476 | true | |
477 | } | |
478 | ||
479 | fn fp_string(fp: &[u8]) -> String { | |
480 | use std::fmt::Write as _; | |
481 | ||
482 | let mut out = String::new(); | |
483 | for b in fp { | |
484 | if !out.is_empty() { | |
485 | out.push(':'); | |
486 | } | |
487 | let _ = write!(out, "{b:02x}"); | |
488 | } | |
489 | out | |
490 | } | |
491 | ||
ffe908f6 WB |
492 | impl Error { |
493 | pub(crate) fn internal<E>(context: &'static str, err: E) -> Self | |
25024fa6 | 494 | where |
ffe908f6 | 495 | E: StdError + Send + Sync + 'static, |
25024fa6 | 496 | { |
ffe908f6 | 497 | Self::Internal(context, Box::new(err)) |
25024fa6 | 498 | } |
25024fa6 WB |
499 | } |
500 | ||
ffe908f6 WB |
501 | impl AuthenticationKind { |
502 | pub fn set_auth_headers(&self, request: http::request::Builder) -> http::request::Builder { | |
503 | match self { | |
504 | AuthenticationKind::Ticket(auth) => auth.set_auth_headers(request), | |
505 | AuthenticationKind::Token(auth) => auth.set_auth_headers(request), | |
25024fa6 WB |
506 | } |
507 | } | |
25024fa6 | 508 | |
ffe908f6 WB |
509 | pub fn userid(&self) -> &str { |
510 | match self { | |
511 | AuthenticationKind::Ticket(auth) => &auth.userid, | |
512 | AuthenticationKind::Token(auth) => &auth.userid, | |
25024fa6 | 513 | } |
25024fa6 WB |
514 | } |
515 | } |