client-api: Add helper methods to convert SystemTime from/to a HTTP date

This commit is contained in:
Kévin Commaille 2024-05-08 13:46:29 +02:00 committed by Kévin Commaille
parent b4d0ab42a3
commit 10c7e59c57
4 changed files with 87 additions and 56 deletions

View File

@ -6,17 +6,22 @@ use as_variant::as_variant;
use bytes::{BufMut, Bytes}; use bytes::{BufMut, Bytes};
use ruma_common::{ use ruma_common::{
api::{ api::{
error::{FromHttpResponseError, IntoHttpError, MatrixErrorBody}, error::{
FromHttpResponseError, HeaderDeserializationError, HeaderSerializationError,
IntoHttpError, MatrixErrorBody,
},
EndpointError, OutgoingResponse, EndpointError, OutgoingResponse,
}, },
RoomVersionId, RoomVersionId,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{from_slice as from_json_slice, Value as JsonValue}; use serde_json::{from_slice as from_json_slice, Value as JsonValue};
use thiserror::Error; use web_time::{Duration, SystemTime};
use web_time::{Duration, SystemTime, UNIX_EPOCH};
use crate::PrivOwnedStr; use crate::{
http_headers::{http_date_to_system_time, system_time_to_http_date},
PrivOwnedStr,
};
/// Deserialize and Serialize implementations for ErrorKind. /// Deserialize and Serialize implementations for ErrorKind.
/// Separate module because it's a lot of code. /// Separate module because it's a lot of code.
@ -372,9 +377,8 @@ impl EndpointError for Error {
ErrorKind::LimitExceeded { retry_after } => { ErrorKind::LimitExceeded { retry_after } => {
// The Retry-After header takes precedence over the retry_after_ms field in // The Retry-After header takes precedence over the retry_after_ms field in
// the body. // the body.
if let Some(retry_after_header) = headers if let Some(Ok(retry_after_header)) =
.get(http::header::RETRY_AFTER) headers.get(http::header::RETRY_AFTER).map(RetryAfter::try_from)
.and_then(RetryAfter::from_header_value)
{ {
*retry_after = Some(retry_after_header); *retry_after = Some(retry_after_header);
} }
@ -567,59 +571,31 @@ pub enum RetryAfter {
DateTime(SystemTime), DateTime(SystemTime),
} }
impl RetryAfter { impl TryFrom<&http::HeaderValue> for RetryAfter {
fn from_header_value(value: &http::HeaderValue) -> Option<Self> { type Error = HeaderDeserializationError;
let bytes = value.as_bytes();
if bytes.iter().all(|b| b.is_ascii_digit()) { fn try_from(value: &http::HeaderValue) -> Result<Self, Self::Error> {
if value.as_bytes().iter().all(|b| b.is_ascii_digit()) {
// It should be a duration. // It should be a duration.
Some(Self::Delay(Duration::from_secs(u64::from_str(value.to_str().ok()?).ok()?))) Ok(Self::Delay(Duration::from_secs(u64::from_str(value.to_str()?)?)))
} else { } else {
// It should be a date. // It should be a date.
let ts = date_header::parse(bytes).ok()?; Ok(Self::DateTime(http_date_to_system_time(value)?))
Some(Self::DateTime(UNIX_EPOCH.checked_add(Duration::from_secs(ts))?))
} }
} }
} }
impl TryFrom<&RetryAfter> for http::HeaderValue { impl TryFrom<&RetryAfter> for http::HeaderValue {
type Error = RetryAfterInvalidDateTime; type Error = HeaderSerializationError;
fn try_from(value: &RetryAfter) -> Result<Self, Self::Error> { fn try_from(value: &RetryAfter) -> Result<Self, Self::Error> {
match value { match value {
RetryAfter::Delay(duration) => Ok(duration.as_secs().into()), RetryAfter::Delay(duration) => Ok(duration.as_secs().into()),
RetryAfter::DateTime(time) => { RetryAfter::DateTime(time) => system_time_to_http_date(time),
let mut buffer = [0; 29];
let duration =
time.duration_since(UNIX_EPOCH).map_err(|_| RetryAfterInvalidDateTime)?;
date_header::format(duration.as_secs(), &mut buffer)
.map_err(|_| RetryAfterInvalidDateTime)?;
let value = http::HeaderValue::from_bytes(&buffer)
.expect("date_header should produce a valid header value");
Ok(value)
}
} }
} }
} }
/// An error when converting a [`RetryAfter`] to a [`http::HeaderValue`].
///
/// Happens when the `DateTime` is too far in the past (before the Unix epoch) or the
/// future (after the year 9999).
#[derive(Debug, Error)]
#[allow(clippy::exhaustive_structs)]
#[error(
"Retry-After header serialization failed: the datetime is too far in the past or the future"
)]
pub struct RetryAfterInvalidDateTime;
impl From<RetryAfterInvalidDateTime> for IntoHttpError {
fn from(_value: RetryAfterInvalidDateTime) -> Self {
IntoHttpError::RetryAfterInvalidDatetime
}
}
/// Extension trait for `FromHttpResponseError<ruma_client_api::Error>`. /// Extension trait for `FromHttpResponseError<ruma_client_api::Error>`.
pub trait FromHttpResponseErrorExt { pub trait FromHttpResponseErrorExt {
/// If `self` is a server error in the `errcode` + `error` format expected /// If `self` is a server error in the `errcode` + `error` format expected

View File

@ -1,10 +1,40 @@
//! Custom HTTP headers not defined in the `http` crate. //! Helpers for HTTP headers with the `http` crate.
#![allow(clippy::declare_interior_mutable_const)] #![allow(clippy::declare_interior_mutable_const)]
use http::header::HeaderName; use http::{header::HeaderName, HeaderValue};
use ruma_common::api::error::{HeaderDeserializationError, HeaderSerializationError};
use web_time::{Duration, SystemTime, UNIX_EPOCH};
/// The [`Cross-Origin-Resource-Policy`] HTTP response header. /// The [`Cross-Origin-Resource-Policy`] HTTP response header.
/// ///
/// [`Cross-Origin-Resource-Policy`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy /// [`Cross-Origin-Resource-Policy`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy
pub const CROSS_ORIGIN_RESOURCE_POLICY: HeaderName = pub const CROSS_ORIGIN_RESOURCE_POLICY: HeaderName =
HeaderName::from_static("cross-origin-resource-policy"); HeaderName::from_static("cross-origin-resource-policy");
/// Convert as `SystemTime` to a HTTP date header value.
pub fn system_time_to_http_date(
time: &SystemTime,
) -> Result<HeaderValue, HeaderSerializationError> {
let mut buffer = [0; 29];
let duration =
time.duration_since(UNIX_EPOCH).map_err(|_| HeaderSerializationError::InvalidHttpDate)?;
date_header::format(duration.as_secs(), &mut buffer)
.map_err(|_| HeaderSerializationError::InvalidHttpDate)?;
Ok(http::HeaderValue::from_bytes(&buffer)
.expect("date_header should produce a valid header value"))
}
/// Convert a header value representing a HTTP date to a `SystemTime`.
pub fn http_date_to_system_time(
value: &HeaderValue,
) -> Result<SystemTime, HeaderDeserializationError> {
let bytes = value.as_bytes();
let ts = date_header::parse(bytes).map_err(|_| HeaderDeserializationError::InvalidHttpDate)?;
UNIX_EPOCH
.checked_add(Duration::from_secs(ts))
.ok_or(HeaderDeserializationError::InvalidHttpDate)
}

View File

@ -5,6 +5,7 @@ Bug fixes:
- Allow to deserialize `Ruleset` with missing fields. - Allow to deserialize `Ruleset` with missing fields.
Breaking changes: Breaking changes:
- The power levels fields in `PushConditionRoomCtx` are grouped in an optional `power_levels` field. - The power levels fields in `PushConditionRoomCtx` are grouped in an optional `power_levels` field.
If the field is missing, push rules that depend on it will never match. However, this allows to If the field is missing, push rules that depend on it will never match. However, this allows to
match the `.m.rule.invite_for_me` push rule because usually the `invite_state` doesn't include match the `.m.rule.invite_for_me` push rule because usually the `invite_state` doesn't include
@ -14,6 +15,7 @@ Breaking changes:
- `deserialize_as_f64_or_string` has been extended to also support parsing integers, and renamed to - `deserialize_as_f64_or_string` has been extended to also support parsing integers, and renamed to
`deserialize_as_number_or_string` to reflect that. `deserialize_as_number_or_string` to reflect that.
- The http crate had a major version bump to version 1.1 - The http crate had a major version bump to version 1.1
- `IntoHttpError::Header` now contains a `HeaderSerializationError`
Improvements: Improvements:

View File

@ -2,7 +2,7 @@
//! converting between http requests / responses and ruma's representation of //! converting between http requests / responses and ruma's representation of
//! matrix API requests / responses. //! matrix API requests / responses.
use std::{error::Error as StdError, fmt, sync::Arc}; use std::{error::Error as StdError, fmt, num::ParseIntError, sync::Arc};
use bytes::{BufMut, Bytes}; use bytes::{BufMut, Bytes};
use serde_json::{from_slice as from_json_slice, Value as JsonValue}; use serde_json::{from_slice as from_json_slice, Value as JsonValue};
@ -127,20 +127,19 @@ pub enum IntoHttpError {
/// Header serialization failed. /// Header serialization failed.
#[error("header serialization failed: {0}")] #[error("header serialization failed: {0}")]
Header(#[from] http::header::InvalidHeaderValue), Header(#[from] HeaderSerializationError),
/// Retry-After header serialization failed because the datetime provided is after the year
/// 9999.
#[error(
"Retry-After header serialization failed: the year of the datetime is bigger than 9999"
)]
RetryAfterInvalidDatetime,
/// HTTP request construction failed. /// HTTP request construction failed.
#[error("HTTP request construction failed: {0}")] #[error("HTTP request construction failed: {0}")]
Http(#[from] http::Error), Http(#[from] http::Error),
} }
impl From<http::header::InvalidHeaderValue> for IntoHttpError {
fn from(value: http::header::InvalidHeaderValue) -> Self {
Self::Header(value.into())
}
}
/// An error when converting a http request to one of ruma's endpoint-specific request types. /// An error when converting a http request to one of ruma's endpoint-specific request types.
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[non_exhaustive] #[non_exhaustive]
@ -258,13 +257,21 @@ impl From<http::header::ToStrError> for DeserializationError {
} }
} }
/// An error with the http headers. /// An error when deserializing the HTTP headers.
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[non_exhaustive] #[non_exhaustive]
pub enum HeaderDeserializationError { pub enum HeaderDeserializationError {
/// Failed to convert `http::header::HeaderValue` to `str`. /// Failed to convert `http::header::HeaderValue` to `str`.
#[error("{0}")] #[error("{0}")]
ToStrError(http::header::ToStrError), ToStrError(#[from] http::header::ToStrError),
/// Failed to convert `http::header::HeaderValue` to an integer.
#[error("{0}")]
ParseIntError(#[from] ParseIntError),
/// Failed to parse a HTTP date from a `http::header::Value`.
#[error("failed to parse HTTP date")]
InvalidHttpDate,
/// The given required header is missing. /// The given required header is missing.
#[error("missing header `{0}`")] #[error("missing header `{0}`")]
@ -303,3 +310,19 @@ impl fmt::Display for IncorrectArgumentCount {
} }
impl StdError for IncorrectArgumentCount {} impl StdError for IncorrectArgumentCount {}
/// An error when serializing the HTTP headers.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum HeaderSerializationError {
/// Failed to convert a header value to `http::header::HeaderValue`.
#[error(transparent)]
ToHeaderValue(#[from] http::header::InvalidHeaderValue),
/// The `SystemTime` could not be converted to a HTTP date.
///
/// This only happens if the `SystemTime` provided is too far in the past (before the Unix
/// epoch) or the future (after the year 9999).
#[error("invalid HTTP date")]
InvalidHttpDate,
}