]>
git.proxmox.com Git - proxmox.git/blob - proxmox-login/src/ticket.rs
b37952c5b194cd8b27e605b7dfffbda0ef5b9bd6
1 //! Ticket related data.
5 use serde
::{Deserialize, Serialize}
;
7 use crate::error
::TicketError
;
8 use crate::tfa
::TfaChallenge
;
10 /// The repsonse to a ticket call can either be a complete ticket, or a TFA challenge.
11 #[derive(Clone, Debug, Serialize, Deserialize)]
12 pub(crate) enum TicketResponse
{
14 Tfa(String
, TfaChallenge
),
17 impl std
::str::FromStr
for TicketResponse
{
18 type Err
= TicketError
;
20 fn from_str(ticket
: &str) -> Result
<Self, TicketError
> {
21 let pos
= ticket
.find('
:'
).ok_or(TicketError
)?
;
22 match ticket
[pos
..].strip_prefix(":!tfa!") {
23 Some(challenge
) => match challenge
.find('
:'
) {
25 let challenge
: std
::borrow
::Cow
<[u8]> =
26 percent_encoding
::percent_decode_str(&challenge
[..pos
]).into();
27 let challenge
= serde_json
::from_slice(&challenge
).map_err(|_
| TicketError
)?
;
28 Ok(TicketResponse
::Tfa(ticket
.to_string(), challenge
))
30 None
=> Err(TicketError
),
32 None
=> ticket
.parse().map(TicketResponse
::Full
),
37 /// An API ticket string. Serializable so it can be stored for later reuse.
38 #[derive(Clone, Debug)]
44 // timestamp_len: u16,
47 /// Tickets are valid for 2 hours.
48 const TICKET_LIFETIME
: i64 = 2 * 3600;
49 /// We refresh during the last half hour.
50 const REFRESH_EARLY_BY
: i64 = 1800;
53 /// The ticket's product prefix.
54 pub fn product(&self) -> &str {
55 &self.data
[..usize::from(self.product_len
)]
58 /// The userid contained in the ticket.
59 pub fn userid(&self) -> &str {
60 let start
= usize::from(self.product_len
) + 1;
61 let len
= usize::from(self.userid_len
);
62 &self.data
[start
..(start
+ len
)]
65 /// Thet ticket's timestamp as a UNIX epoch.
66 pub fn timestamp(&self) -> i64 {
70 /// The ticket age in seconds.
71 pub fn age(&self) -> i64 {
72 epoch_i64() - self.timestamp
75 /// This is a convenience check for the ticket's validity assuming the usual ticket lifetime of
77 pub fn validity(&self) -> Validity
{
79 if age
> TICKET_LIFETIME
{
81 } else if age
>= TICKET_LIFETIME
- REFRESH_EARLY_BY
{
88 /// Get the cookie in the form `<PRODUCT>AuthCookie=Ticket`.
89 pub fn cookie(&self) -> String
{
90 format
!("{}AuthCookie={}", self.product(), self.data
)
94 /// Whether a ticket should be refreshed or is already invalid and needs to be completely renewed.
95 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
97 /// The ticket is still valid for longer than half an hour.
100 /// The ticket is within its final half hour validity period and should be renewed with the
101 /// ticket as password.
104 /// The ticket is already invalid and a new ticket needs to be created.
109 /// Simply check whether the ticket is considered valid even if it should be renewed.
110 pub fn is_valid(self) -> bool
{
111 matches
!(self, Validity
::Valid
| Validity
::Refresh
)
115 impl std
::str::FromStr
for Ticket
{
116 type Err
= TicketError
;
118 fn from_str(s
: &str) -> Result
<Self, TicketError
> {
122 let product_len
= s
.find('
:'
).ok_or(TicketError
)?
;
123 if product_len
>= 10 {
125 return Err(TicketError
);
127 let s
= &s
[(product_len
+ 1)..];
130 let userid_len
= s
.find('
:'
).ok_or(TicketError
)?
;
131 if !s
[..userid_len
].contains('@'
) {
132 return Err(TicketError
);
134 let s
= &s
[(userid_len
+ 1)..];
137 let timestamp_len
= s
.find('
:'
).ok_or(TicketError
)?
;
138 let timestamp
= i64::from_str_radix(&s
[..timestamp_len
], 16).map_err(|_
| TicketError
)?
;
140 let s
= &s
[(timestamp_len
+ 1)..];
142 let s
= s
.strip_prefix('
:'
).ok_or(TicketError
)?
;
144 return Err(TicketError
);
148 product_len
: u16::try_from(product_len
).map_err(|_
| TicketError
)?
,
149 userid_len
: u16::try_from(userid_len
).map_err(|_
| TicketError
)?
,
150 //timestamp_len: u16::try_from(timestamp_len).map_err(|_| TicketError)?,
157 impl fmt
::Display
for Ticket
{
158 fn fmt(&self, f
: &mut fmt
::Formatter
) -> fmt
::Result
{
159 f
.write_str(&self.data
)
163 impl From
<Ticket
> for String
{
164 fn from(ticket
: Ticket
) -> String
{
169 impl From
<Ticket
> for Box
<str> {
170 fn from(ticket
: Ticket
) -> Box
<str> {
175 impl Serialize
for Ticket
{
176 fn serialize
<S
>(&self, serializer
: S
) -> Result
<S
::Ok
, S
::Error
>
178 S
: serde
::Serializer
,
180 serializer
.serialize_str(&self.data
)
184 impl<'de
> Deserialize
<'de
> for Ticket
{
185 fn deserialize
<D
>(deserializer
: D
) -> Result
<Self, D
::Error
>
187 D
: serde
::Deserializer
<'de
>,
189 use serde
::de
::Error
;
191 std
::borrow
::Cow
::<'de
, str>::deserialize(deserializer
)?
193 .map_err(D
::Error
::custom
)
197 /// A finished authentication state.
199 /// This is serializable / deserializable in order to be able to easily store it.
200 #[derive(Clone, Debug, Serialize, Deserialize)]
201 #[serde(rename_all = "kebab-case")]
202 pub struct Authentication
{
203 /// The API URL this authentication info belongs to.
206 /// The user id in the form of `username@realm`.
209 /// The authentication ticket.
212 /// The cluster name (if any)
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub clustername
: Option
<String
>,
216 /// The CSRFPreventionToken header.
217 #[serde(rename = "CSRFPreventionToken")]
218 pub csrfprevention_token
: String
,
221 impl Authentication
{
222 /// Get the ticket cookie in the form `<PRODUCT>AuthCookie=Ticket`.
223 pub fn cookie(&self) -> String
{
227 #[cfg(feature = "http")]
228 /// Add authentication headers to a request.
230 /// This is equivalent to doing:
233 /// .header(http::header::COOKIE, auth.cookie())
234 /// .header(proxmox_login::CSRF_HEADER_NAME, &auth.csrfprevention_token)
236 pub fn set_auth_headers(&self, request
: http
::request
::Builder
) -> http
::request
::Builder
{
238 .header(http
::header
::COOKIE
, self.cookie())
239 .header(crate::CSRF_HEADER_NAME
, &self.csrfprevention_token
)
243 #[cfg(target_arch="wasm32")]
244 fn epoch_i64() -> i64 {
245 (js_sys
::Date
::now() / 1000.0) as i64
248 #[cfg(not(target_arch="wasm32"))]
249 fn epoch_i64() -> i64 {
250 use std
::time
::{SystemTime, UNIX_EPOCH}
;
252 let now
= SystemTime
::now();
253 if now
> UNIX_EPOCH
{
254 i64::try_from(now
.duration_since(UNIX_EPOCH
).unwrap().as_secs()).unwrap_or(0)
256 -i64::try_from(UNIX_EPOCH
.duration_since(now
).unwrap().as_secs()).unwrap_or(0)