client-api: Add support for MSC4108 OIDC sign in and E2EE set up via QR code

This commit is contained in:
Damir Jelić 2024-05-07 12:09:50 +02:00
parent a12c38c532
commit 1c4f26af74
6 changed files with 220 additions and 0 deletions

View File

@ -1,5 +1,9 @@
# [unreleased]
Improvements:
- Add support for MSC4108 OIDC sign in and E2EE set up via QR code
# 0.18.0
Bug fixes:

View File

@ -50,6 +50,7 @@ unstable-msc3575 = []
unstable-msc3814 = []
unstable-msc3843 = []
unstable-msc3983 = []
unstable-msc4108 = []
unstable-msc4121 = []
[dependencies]
@ -67,6 +68,7 @@ serde = { workspace = true }
serde_html_form = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true, features = ["serde"] }
web-time = { workspace = true }
[dev-dependencies]

View File

@ -35,6 +35,8 @@ pub mod read_marker;
pub mod receipt;
pub mod redact;
pub mod relations;
#[cfg(feature = "unstable-msc4108")]
pub mod rendezvous;
pub mod room;
pub mod search;
pub mod server;

View File

@ -0,0 +1,3 @@
//! Endpoints for managing rendezvous sessions.
pub mod create_rendezvous_session;

View File

@ -0,0 +1,207 @@
//! `POST /_matrix/client/*/rendezvous/`
//!
//! Create a rendezvous session.
pub mod unstable {
//! `msc4108` ([MSC])
//!
//! [MSC]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
use http::{
header::{CONTENT_LENGTH, CONTENT_TYPE, ETAG, EXPIRES, LAST_MODIFIED},
HeaderName,
};
#[cfg(feature = "client")]
use ruma_common::api::error::FromHttpResponseError;
use ruma_common::{
api::{error::HeaderDeserializationError, Metadata},
metadata,
};
use serde::{Deserialize, Serialize};
use url::Url;
use web_time::SystemTime;
const METADATA: Metadata = metadata! {
method: POST,
rate_limited: true,
authentication: None,
history: {
unstable => "/_matrix/client/unstable/org.matrix.msc4108/rendezvous",
}
};
/// Request type for the `POST` `rendezvous` endpoint.
#[derive(Debug, Default, Clone)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct Request {
/// Any data up to maximum size allowed by the server.
pub content: String,
}
#[cfg(feature = "client")]
impl ruma_common::api::OutgoingRequest for Request {
type EndpointError = crate::Error;
type IncomingResponse = Response;
const METADATA: Metadata = METADATA;
fn try_into_http_request<T: Default + bytes::BufMut>(
self,
base_url: &str,
_: ruma_common::api::SendAccessToken<'_>,
considering_versions: &'_ [ruma_common::api::MatrixVersion],
) -> Result<http::Request<T>, ruma_common::api::error::IntoHttpError> {
let url = METADATA.make_endpoint_url(considering_versions, base_url, &[], "")?;
let body = self.content.as_bytes();
let content_length = body.len();
Ok(http::Request::builder()
.method(METADATA.method)
.uri(url)
.header(CONTENT_TYPE, "text/plain")
.header(CONTENT_LENGTH, content_length)
.body(ruma_common::serde::slice_to_buf(body))?)
}
}
#[cfg(feature = "server")]
impl ruma_common::api::IncomingRequest for Request {
type EndpointError = crate::Error;
type OutgoingResponse = Response;
const METADATA: Metadata = METADATA;
fn try_from_http_request<B, S>(
request: http::Request<B>,
_path_args: &[S],
) -> Result<Self, ruma_common::api::error::FromHttpRequestError>
where
B: AsRef<[u8]>,
S: AsRef<str>,
{
const EXPECTED_CONTENT_TYPE: &str = "text/plain";
use ruma_common::api::error::DeserializationError;
let content_type = request
.headers()
.get(CONTENT_TYPE)
.ok_or(HeaderDeserializationError::MissingHeader(CONTENT_TYPE.to_string()))?;
let content_type = content_type.to_str()?;
if content_type != EXPECTED_CONTENT_TYPE {
Err(HeaderDeserializationError::InvalidHeaderValue {
header: CONTENT_TYPE.to_string(),
expected: EXPECTED_CONTENT_TYPE.to_owned(),
unexpected: content_type.to_owned(),
}
.into())
} else {
let body = request.into_body().as_ref().to_vec();
let content = String::from_utf8(body)
.map_err(|e| DeserializationError::Utf8(e.utf8_error()))?;
Ok(Self { content })
}
}
}
impl Request {
/// Creates a new `Request` with the given content.
pub fn new(content: String) -> Self {
Self { content }
}
}
/// Response type for the `POST` `rendezvous` endpoint.
#[derive(Debug, Clone)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct Response {
/// The absolute URL of the rendezvous session.
pub url: Url,
/// ETag for the current payload at the rendezvous session as
/// per [RFC7232](https://httpwg.org/specs/rfc7232.html#header.etag).
pub etag: String,
/// The expiry time of the rendezvous as per
/// [RFC7234](https://httpwg.org/specs/rfc7234.html#header.expires).
pub expires: SystemTime,
/// The last modified date of the payload as
/// per [RFC7232](https://httpwg.org/specs/rfc7232.html#header.last-modified)
pub last_modified: SystemTime,
}
#[derive(Serialize, Deserialize)]
struct ResponseBody {
url: Url,
}
#[cfg(feature = "client")]
impl ruma_common::api::IncomingResponse for Response {
type EndpointError = crate::Error;
fn try_from_http_response<T: AsRef<[u8]>>(
response: http::Response<T>,
) -> Result<Self, FromHttpResponseError<Self::EndpointError>> {
use ruma_common::api::EndpointError;
if response.status().as_u16() >= 400 {
return Err(FromHttpResponseError::Server(
Self::EndpointError::from_http_response(response),
));
}
let get_date = |header: HeaderName| -> Result<SystemTime, FromHttpResponseError<Self::EndpointError>> {
let date = response
.headers()
.get(&header)
.ok_or_else(|| HeaderDeserializationError::MissingHeader(header.to_string()))?;
let date = crate::http_headers::http_date_to_system_time(date)?;
Ok(date)
};
let etag = response
.headers()
.get(ETAG)
.ok_or(HeaderDeserializationError::MissingHeader(ETAG.to_string()))?
.to_str()?
.to_owned();
let expires = get_date(EXPIRES)?;
let last_modified = get_date(LAST_MODIFIED)?;
let body = response.into_body();
let body: ResponseBody = serde_json::from_slice(body.as_ref())?;
Ok(Self { url: body.url, etag, expires, last_modified })
}
}
#[cfg(feature = "server")]
impl ruma_common::api::OutgoingResponse for Response {
fn try_into_http_response<T: Default + bytes::BufMut>(
self,
) -> Result<http::Response<T>, ruma_common::api::error::IntoHttpError> {
use http::header::{CACHE_CONTROL, PRAGMA};
let body = ResponseBody { url: self.url.clone() };
let body = serde_json::to_vec(&body)?;
let body = ruma_common::serde::slice_to_buf(&body);
let expires = crate::http_headers::system_time_to_http_date(&self.expires)?;
let last_modified = crate::http_headers::system_time_to_http_date(&self.last_modified)?;
Ok(http::Response::builder()
.status(http::StatusCode::OK)
.header(CONTENT_TYPE, "application/json")
.header(PRAGMA, "no-cache")
.header(CACHE_CONTROL, "no-store")
.header(ETAG, self.etag)
.header(EXPIRES, expires)
.header(LAST_MODIFIED, last_modified)
.body(body)?)
}
}
}

View File

@ -222,6 +222,7 @@ unstable-msc3955 = ["ruma-events?/unstable-msc3955"]
unstable-msc3956 = ["ruma-events?/unstable-msc3956"]
unstable-msc3983 = ["ruma-client-api?/unstable-msc3983"]
unstable-msc4075 = ["ruma-events?/unstable-msc4075"]
unstable-msc4108 = ["ruma-client-api?/unstable-msc4108"]
unstable-msc4121 = ["ruma-client-api?/unstable-msc4121"]
unstable-msc4125 = ["ruma-federation-api?/unstable-msc4125"]
unstable-pdu = ["ruma-events?/unstable-pdu"]
@ -275,6 +276,7 @@ __ci = [
"unstable-msc3956",
"unstable-msc3983",
"unstable-msc4075",
"unstable-msc4108",
"unstable-msc4121",
"unstable-msc4125",
]