]> git.proxmox.com Git - proxmox.git/blame - proxmox-client/src/client.rs
client: do a POST instead of PUT in `post_without_body`
[proxmox.git] / proxmox-client / src / client.rs
CommitLineData
ffe908f6 1use std::error::Error as StdError;
25024fa6 2use std::future::Future;
1c96afd0 3use std::pin::Pin;
25024fa6 4use std::sync::Arc;
0f19f212 5use std::sync::Mutex;
25024fa6
WB
6
7use http::request::Request;
25024fa6
WB
8use http::uri::PathAndQuery;
9use http::{StatusCode, Uri};
1c96afd0
WB
10use hyper::body::{Body, HttpBody};
11use openssl::hash::MessageDigest;
12use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
13use openssl::x509::{self, X509};
14use serde::Serialize;
25024fa6 15
1c96afd0 16use proxmox_login::ticket::Validity;
0f19f212 17use proxmox_login::{Login, SecondFactorChallenge, TicketResult};
25024fa6
WB
18
19use crate::auth::AuthenticationKind;
604e4676 20use crate::error::ParseFingerprintError;
0f19f212 21use crate::{Error, Token};
25024fa6 22
1c96afd0 23use super::{HttpApiClient, HttpApiResponse};
25024fa6 24
1c96afd0
WB
25#[derive(Default)]
26pub 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
44impl 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`].
62pub 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
69impl 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
374async 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
383impl 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
456fn 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
479fn 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
492impl 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
501impl 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}