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