Skip to content

Commit

Permalink
Some more rendezvous implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
sandhose committed Apr 5, 2024
1 parent 994600e commit b7f14d6
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 26 deletions.
27 changes: 25 additions & 2 deletions rust/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,39 @@

#![allow(clippy::new_ret_no_self)]

use http::StatusCode;
use std::collections::HashMap;

use http::{HeaderMap, StatusCode};
use pyo3::import_exception;

import_exception!(synapse.api.errors, SynapseError);

impl SynapseError {
pub fn new(code: StatusCode, message: &'static str) -> pyo3::PyErr {
// TODO: additional headers and matrix error code
SynapseError::new_err((code.as_u16(), message))
}

pub fn new_with_headers(
code: StatusCode,
message: &'static str,
headers: HeaderMap,
) -> pyo3::PyErr {
let headers = headers
.iter()
.map(|(key, value)| {
(
key.to_string(),
value
.to_str()
// XXX: will that ever throw?
.expect("header value is valid ASCII")
.to_owned(),
)
})
.collect::<HashMap<String, String>>();

SynapseError::new_err((code.as_u16(), message, None::<()>, headers))
}
}

import_exception!(synapse.api.errors, NotFoundError);
Expand Down
93 changes: 71 additions & 22 deletions rust/src/rendezvous/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@
use std::{collections::HashMap, time::Duration};

use bytes::Bytes;
use headers::{ContentLength, ContentType, HeaderMapExt};
use http::{Response, StatusCode};
use pyo3::{pyclass, pymethods, types::PyModule, PyAny, PyResult, Python};
use headers::{
AccessControlAllowOrigin, AccessControlExposeHeaders, ContentLength, ContentType, HeaderMapExt,
IfMatch, IfNoneMatch,
};
use http::{header::ETAG, HeaderMap, Response, StatusCode, Uri};
use pyo3::{
exceptions::PyValueError, pyclass, pymethods, types::PyModule, PyAny, PyResult, Python,
};
use ulid::Ulid;

use crate::{
Expand All @@ -29,18 +34,31 @@ mod session;

use self::session::Session;

fn prepare_headers(headers: &mut HeaderMap, session: &Session) {
headers.typed_insert(AccessControlAllowOrigin::ANY);
headers.typed_insert(AccessControlExposeHeaders::from_iter([ETAG]));
headers.typed_insert(session.etag());
headers.typed_insert(session.expires());
headers.typed_insert(session.last_modified());
}

// TODO: handle eviction
#[derive(Default)]
#[pyclass]
struct RendezVous {
base: Uri,
sessions: HashMap<Ulid, Session>,
}

#[pymethods]
impl RendezVous {
#[new]
fn new() -> Self {
RendezVous::default()
fn new(base: &str) -> PyResult<Self> {
let base = Uri::try_from(base).map_err(|_| PyValueError::new_err("Invalid base URI"))?;

Ok(Self {
base,
sessions: HashMap::new(),
})
}

fn handle_post(&mut self, twisted_request: &PyAny) -> PyResult<()> {
Expand All @@ -63,25 +81,21 @@ impl RendezVous {

let id = Ulid::new();

// XXX: this is lazy
let source_uri = request.uri();
let uri = format!("{source_uri}/{id}");
let uri = format!("{base}/{id}", base = self.base);

let body = request.into_body();

let session = Session::new(body, content_type.into(), Duration::from_secs(5 * 60));

let response = serde_json::json!({
"uri": uri,
"url": uri,
})
.to_string();

let mut response = Response::new(response.as_bytes());
*response.status_mut() = StatusCode::CREATED;
response.headers_mut().typed_insert(ContentType::json());
response.headers_mut().typed_insert(session.etag());
response.headers_mut().typed_insert(session.expires());
response.headers_mut().typed_insert(session.last_modified());
prepare_headers(response.headers_mut(), &session);
http_response_to_twisted(twisted_request, response)?;

self.sessions.insert(id, session);
Expand All @@ -90,19 +104,27 @@ impl RendezVous {
}

fn handle_get(&mut self, twisted_request: &PyAny, id: &str) -> PyResult<()> {
let _request = http_request_from_twisted(twisted_request)?;
let request = http_request_from_twisted(twisted_request)?;

// TODO: handle If-None-Match
let if_none_match: Option<IfNoneMatch> = request.headers().typed_get();

let id: Ulid = id.parse().map_err(|_| NotFoundError::new())?;
let session = self.sessions.get(&id).ok_or_else(NotFoundError::new)?;

if let Some(if_none_match) = if_none_match {
if !if_none_match.precondition_passes(&session.etag()) {
let mut response = Response::new(Bytes::new());
*response.status_mut() = StatusCode::NOT_MODIFIED;
prepare_headers(response.headers_mut(), session);
http_response_to_twisted(twisted_request, response)?;
return Ok(());
}
}

let mut response = Response::new(session.data());
*response.status_mut() = StatusCode::OK;
prepare_headers(response.headers_mut(), session);
response.headers_mut().typed_insert(session.content_type());
response.headers_mut().typed_insert(session.etag());
response.headers_mut().typed_insert(session.expires());
response.headers_mut().typed_insert(session.last_modified());
http_response_to_twisted(twisted_request, response)?;

Ok(())
Expand All @@ -111,22 +133,46 @@ impl RendezVous {
fn handle_put(&mut self, twisted_request: &PyAny, id: &str) -> PyResult<()> {
let request = http_request_from_twisted(twisted_request)?;

// TODO: handle If-Match
let ContentLength(content_length) = request.headers().typed_get().ok_or_else(|| {
SynapseError::new(StatusCode::BAD_REQUEST, "Missing Content-Length header")
})?;

if content_length > 1024 * 100 {
return Err(SynapseError::new(
StatusCode::BAD_REQUEST,
"Content-Length too large",
));
}

let content_type: ContentType = request.headers().typed_get().ok_or_else(|| {
SynapseError::new(StatusCode::BAD_REQUEST, "Missing Content-Type header")
})?;

let if_match: IfMatch = request
.headers()
.typed_get()
.ok_or_else(|| SynapseError::new(StatusCode::BAD_REQUEST, "Missing If-Match header"))?;

let data = request.into_body();

let id: Ulid = id.parse().map_err(|_| NotFoundError::new())?;
let session = self.sessions.get_mut(&id).ok_or_else(NotFoundError::new)?;

if !if_match.precondition_passes(&session.etag()) {
let mut headers = HeaderMap::new();
prepare_headers(&mut headers, session);
return Err(SynapseError::new_with_headers(
StatusCode::PRECONDITION_FAILED,
"Precondition failed",
headers,
));
}

session.update(data, content_type.into());

let mut response = Response::new(Bytes::new());
*response.status_mut() = StatusCode::ACCEPTED;
response.headers_mut().typed_insert(session.etag());
response.headers_mut().typed_insert(session.expires());
response.headers_mut().typed_insert(session.last_modified());
prepare_headers(response.headers_mut(), session);
http_response_to_twisted(twisted_request, response)?;

Ok(())
Expand All @@ -140,6 +186,9 @@ impl RendezVous {

let mut response = Response::new(Bytes::new());
*response.status_mut() = StatusCode::NO_CONTENT;
response
.headers_mut()
.typed_insert(AccessControlAllowOrigin::ANY);
http_response_to_twisted(twisted_request, response)?;

Ok(())
Expand Down
4 changes: 3 additions & 1 deletion synapse/rest/client/rendezvous.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def on_POST(self, request: SynapseRequest) -> None:
self._store.handle_post(request)

class MSC4108RendezvousSessionServlet(RestServlet):
# TODO: this should probably be mounted on the _synapse/client namespace
PATTERNS = client_patterns(
"/org.matrix.msc4108/rendezvous/(?P<session_id>[^/]+)$",
releases=[],
Expand All @@ -118,6 +119,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
MSC3886RendezvousServlet(hs).register(http_server)

# TODO: gate this behind a feature flag and store the rendezvous object in the HS
rendezvous = RendezVous()
base = hs.config.server.public_baseurl + "_matrix/client/unstable/org.matrix.msc4108/rendezvous"
rendezvous = RendezVous(base)
MSC4108RendezvousServlet(rendezvous).register(http_server)
MSC4108RendezvousSessionServlet(rendezvous).register(http_server)
3 changes: 3 additions & 0 deletions synapse/rest/client/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
"org.matrix.msc4069": self.config.experimental.msc4069_profile_inhibit_propagation,
# Allows clients to handle push for encrypted events.
"org.matrix.msc4028": self.config.experimental.msc4028_push_encrypted_events,

# TODO
"org.matrix.msc4108": True,
},
},
)
Expand Down
2 changes: 1 addition & 1 deletion synapse/synapse_rust/rendezvous.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from twisted.web.iweb import IRequest

class RendezVous:
def __init__(self): ...
def __init__(self, base: str): ...
def handle_post(self, request: IRequest): ...
def handle_get(self, request: IRequest, session_id: str): ...
def handle_put(self, request: IRequest, session_id: str): ...
Expand Down

0 comments on commit b7f14d6

Please sign in to comment.