1 use std
::error
::Error
as StdError
;
2 use std
::future
::Future
;
7 use http
::request
::Request
;
8 use http
::uri
::PathAndQuery
;
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}
;
17 use proxmox_login
::ticket
::Validity
;
18 use proxmox_login
::{Login, SecondFactorChallenge, TicketResult}
;
20 use crate::auth
::AuthenticationKind
;
21 use crate::error
::ParseFingerprintError
;
22 use crate::{Error, Token}
;
24 use super::{HttpApiClient, HttpApiResponse, HttpApiResponseStream}
;
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;
30 /// Default TLS verification.
34 /// Insecure: ignore invalid certificates.
37 /// Expect a specific certificate fingerprint.
40 /// Verify with a specific PEM formatted CA.
43 /// Use a callback for certificate verification.
44 Callback(Box
<TlsCallback
>),
48 pub fn parse_fingerprint(fp
: &str) -> Result
<Self, ParseFingerprintError
> {
55 .filter(|&b
| b
!= b'
:'
)
58 let fp
= <[u8; 32]>::from_hex(hex
).map_err(|_
| ParseFingerprintError
)?
;
60 Ok(Self::Fingerprint(fp
.into()))
64 /// A Proxmox API client base backed by a [`proxmox_http::Client`].
67 auth
: Mutex
<Option
<Arc
<AuthenticationKind
>>>,
68 client
: Arc
<proxmox_http
::client
::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()))
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 {
82 auth
: Mutex
::new(None
),
88 /// Create a new client instance which will connect to the provided endpoint.
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
))?
;
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
| {
105 verify_fingerprint(chain
, &expected_fingerprint
)
108 TlsOptions
::Callback(cb
) => {
110 .set_verify_callback(SslVerifyMode
::PEER
, move |valid
, chain
| cb(valid
, chain
));
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
)
118 .map_err(|err
| Error
::internal("failed to build certificate store", err
))?
;
119 connector
.set_cert_store(store
.build());
124 proxmox_http
::client
::Client
::with_ssl_connector(connector
.build(), http_options
);
126 Ok(Self::with_client(api_url
, Arc
::new(client
)))
129 /// Get the underlying client object.
130 pub fn http_client(&self) -> &Arc
<proxmox_http
::client
::Client
> {
134 /// Get a reference to the current authentication information.
135 pub fn authentication(&self) -> Option
<Arc
<AuthenticationKind
>> {
136 self.auth
.lock().unwrap().clone()
139 /// Get a serialized version of the ticket if one is used.
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
,
148 Ok(Some(serde_json
::to_vec(auth
).map_err(|err
| {
149 Error
::internal("failed to serialize ticket", err
)
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
);
159 /// Replace the currently used authentication.
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()));
166 /// Drop the current authentication information.
167 pub fn logout(&self) {
168 self.auth
.lock().unwrap().take();
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
;
177 /// Get the currently used API url.
178 pub fn api_url(&self) -> &Uri
{
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
);
189 if let Some(authority
) = parts
.authority
{
190 builder
= builder
.authority(authority
)
195 .parse
::<PathAndQuery
>()
196 .map_err(|err
| Error
::internal("failed to parse uri", err
))?
,
199 .map_err(|err
| Error
::internal("failed to build Uri", err
))
202 /// Perform an *unauthenticated* HTTP request.
203 async
fn send_authenticated_request(
204 client
: Arc
<proxmox_http
::client
::Client
>,
205 auth
: Arc
<AuthenticationKind
>,
208 json_body
: Option
<String
>,
209 // send an `Accept: application/json-seq` header.
211 ) -> Result
<(http
::response
::Parts
, hyper
::Body
), Error
> {
212 let mut request
= auth
.set_auth_headers(Request
::builder().method(method
).uri(uri
));
214 request
= request
.header(http
::header
::ACCEPT
, "application/json-seq");
217 let request
= if let Some(body
) = json_body
{
219 .header(http
::header
::CONTENT_TYPE
, "application/json")
222 request
.body(Default
::default())
224 .map_err(|err
| Error
::internal("failed to build request", err
))?
;
226 let response
= client
229 .map_err(|err
| Error
::Client(err
.into()))?
;
231 if response
.status() == StatusCode
::UNAUTHORIZED
{
232 return Err(Error
::Unauthorized
);
235 let (response
, body
) = response
.into_parts();
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)
244 String
::from_utf8(body
).map_err(|_
| Error
::Other("API returned non-utf8 data"))?
;
246 return Err(Error
::api(response
.status
, data
));
252 /// Perform an *unauthenticated* HTTP request.
253 async
fn authenticated_request(
254 client
: Arc
<proxmox_http
::client
::Client
>,
255 auth
: Arc
<AuthenticationKind
>,
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?
;
264 let content_type
= match response
.headers
.get(http
::header
::CONTENT_TYPE
) {
269 .map_err(|err
| Error
::internal("bad Content-Type header", err
))?
275 status
: response
.status
.as_u16(),
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
> {
288 .ok_or_else(|| Error
::Unauthorized
)
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`.
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()),
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?
;
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
)
316 .header(http
::header
::CONTENT_TYPE
, request
.content_type
)
318 http
::header
::CONTENT_LENGTH
,
319 request
.content_length
.to_string(),
321 .body(request
.body
.into())
322 .map_err(|err
| Error
::internal("error building login http request", err
))?
;
324 let api_response
= self
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"));
333 let (_
, body
) = api_response
.into_parts();
334 let body
= read_body(body
).await?
;
339 /// Attempt to refresh the current ticket.
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
,
349 let login
= Login
::renew(self.api_url
.to_string(), auth
.ticket
.to_string())
350 .map_err(Error
::Ticket
)?
;
352 let api_response
= self.do_login_request(login
.request()).await?
;
354 match login
.response(&api_response
)?
{
355 TicketResult
::Full(auth
) => {
356 *self.auth
.lock().unwrap() = Some(Arc
::new(auth
.into()));
359 TicketResult
::TfaRequired(_
) => Err(proxmox_login
::error
::ResponseError
::Msg(
360 "ticket refresh returned a TFA challenge",
366 /// Attempt to login.
368 /// This will propagate the PVE compatibility state and then perform the `Login` request via
369 /// the inner http client.
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
);
376 let api_response
= self.do_login_request(login
.request()).await?
;
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()));
387 /// Attempt to finish a 2nd factor login.
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(
393 challenge
: SecondFactorChallenge
,
394 challenge_response
: proxmox_login
::Request
,
395 ) -> Result
<(), Error
> {
396 let api_response
= self.do_login_request(challenge_response
).await?
;
398 let auth
= challenge
.response(&api_response
)?
;
399 *self.auth
.lock().unwrap() = Some(Arc
::new(auth
.into()));
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
[..]);
413 impl HttpApiClient
for Client
{
414 type ResponseFuture
<'a
> =
415 Pin
<Box
<dyn Future
<Output
= Result
<HttpApiResponse
, Error
>> + Send
+ 'a
>>;
417 type ResponseStreamFuture
<'a
> =
418 Pin
<Box
<dyn Future
<Output
= Result
<HttpApiResponseStream
<Self::Body
>, Error
>> + Send
+ 'a
>>;
420 type Body
= hyper
::Body
;
425 path_and_query
: &'a
str,
427 ) -> Self::ResponseFuture
<'a
>
433 serde_json
::to_string(¶ms
)
434 .map_err(|err
| Error
::internal("failed to serialize parameters", err
))
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
447 fn streaming_request
<'a
, T
>(
450 path_and_query
: &'a
str,
452 ) -> Self::ResponseStreamFuture
<'a
>
458 serde_json
::to_string(¶ms
)
459 .map_err(|err
| Error
::internal("failed to serialize parameters", err
))
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?
;
471 let content_type
= match response
.headers
.get(http
::header
::CONTENT_TYPE
) {
476 .map_err(|err
| Error
::internal("bad Content-Type header", err
))?
481 Ok(HttpApiResponseStream
{
482 status
: response
.status
.as_u16(),
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?");
496 let fp
= match cert
.digest(MessageDigest
::sha256()) {
498 log
::error
!("error calculating certificate fingerprint: {err}");
504 if expected_fingerprint
!= fp
.as_ref() {
505 log
::error
!("bad fingerprint: {}", fp_string(&fp
));
506 log
::error
!("expected fingerprint: {}", fp_string(expected_fingerprint
));
513 fn fp_string(fp
: &[u8]) -> String
{
514 use std
::fmt
::Write
as _
;
516 let mut out
= String
::new();
521 let _
= write
!(out
, "{b:02x}");
527 pub(crate) fn internal
<E
>(context
: &'
static str, err
: E
) -> Self
529 E
: StdError
+ Send
+ Sync
+ '
static,
531 Self::Internal(context
, Box
::new(err
))
535 impl AuthenticationKind
{
536 pub fn set_auth_headers(&self, request
: http
::request
::Builder
) -> http
::request
::Builder
{
538 AuthenticationKind
::Ticket(auth
) => auth
.set_auth_headers(request
),
539 AuthenticationKind
::Token(auth
) => auth
.set_auth_headers(request
),
543 pub fn userid(&self) -> &str {
545 AuthenticationKind
::Ticket(auth
) => &auth
.userid
,
546 AuthenticationKind
::Token(auth
) => &auth
.userid
,