Basic implementation of websocket
This commit is contained in:
15
matrixgw_backend/Cargo.lock
generated
15
matrixgw_backend/Cargo.lock
generated
@@ -241,6 +241,20 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-ws"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a3a1fb4f9f2794b0aadaf2ba5f14a6f034c7e86957b458c506a8cb75953f2d99"
|
||||||
|
dependencies = [
|
||||||
|
"actix-codec",
|
||||||
|
"actix-http",
|
||||||
|
"actix-web",
|
||||||
|
"bytestring",
|
||||||
|
"futures-core",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "adler2"
|
name = "adler2"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -3026,6 +3040,7 @@ dependencies = [
|
|||||||
"actix-remote-ip",
|
"actix-remote-ip",
|
||||||
"actix-session",
|
"actix-session",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
|
"actix-ws",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base16ct 0.3.0",
|
"base16ct 0.3.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
|||||||
@@ -32,4 +32,5 @@ matrix-sdk = "0.14.0"
|
|||||||
url = "2.5.7"
|
url = "2.5.7"
|
||||||
ractor = "0.15.9"
|
ractor = "0.15.9"
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
lazy-regex = "3.4.2"
|
lazy-regex = "3.4.2"
|
||||||
|
actix-ws = "0.3.0"
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Auth header
|
/// Auth header
|
||||||
pub const API_AUTH_HEADER: &str = "x-client-auth";
|
pub const API_AUTH_HEADER: &str = "x-client-auth";
|
||||||
|
|
||||||
@@ -16,3 +18,11 @@ pub mod sessions {
|
|||||||
/// Authenticated ID
|
/// Authenticated ID
|
||||||
pub const USER_ID: &str = "uid";
|
pub const USER_ID: &str = "uid";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How often heartbeat pings are sent.
|
||||||
|
///
|
||||||
|
/// Should be half (or less) of the acceptable client timeout.
|
||||||
|
pub const WS_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// How long before lack of client response causes a timeout.
|
||||||
|
pub const WS_CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ pub mod matrix_link_controller;
|
|||||||
pub mod matrix_sync_thread_controller;
|
pub mod matrix_sync_thread_controller;
|
||||||
pub mod server_controller;
|
pub mod server_controller;
|
||||||
pub mod tokens_controller;
|
pub mod tokens_controller;
|
||||||
|
pub mod ws_controller;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum HttpFailure {
|
pub enum HttpFailure {
|
||||||
@@ -18,6 +19,8 @@ pub enum HttpFailure {
|
|||||||
OpenID(Box<dyn Error>),
|
OpenID(Box<dyn Error>),
|
||||||
#[error("an unspecified internal error occurred: {0}")]
|
#[error("an unspecified internal error occurred: {0}")]
|
||||||
InternalError(#[from] anyhow::Error),
|
InternalError(#[from] anyhow::Error),
|
||||||
|
#[error("Actix web error: {0}")]
|
||||||
|
ActixError(#[from] actix_web::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseError for HttpFailure {
|
impl ResponseError for HttpFailure {
|
||||||
|
|||||||
198
matrixgw_backend/src/controllers/ws_controller.rs
Normal file
198
matrixgw_backend/src/controllers/ws_controller.rs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
use crate::broadcast_messages::BroadcastMessage;
|
||||||
|
use crate::constants;
|
||||||
|
use crate::controllers::HttpResult;
|
||||||
|
use crate::extractors::auth_extractor::{AuthExtractor, AuthenticatedMethod};
|
||||||
|
use crate::extractors::matrix_client_extractor::MatrixClientExtractor;
|
||||||
|
use crate::matrix_connection::matrix_client::MatrixClient;
|
||||||
|
use crate::matrix_connection::matrix_manager::MatrixManagerMsg;
|
||||||
|
use actix_web::dev::Payload;
|
||||||
|
use actix_web::{FromRequest, HttpRequest, web};
|
||||||
|
use actix_ws::Message;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use matrix_sdk::ruma::OwnedRoomId;
|
||||||
|
use matrix_sdk::ruma::events::room::message::RoomMessageEventContent;
|
||||||
|
use ractor::ActorRef;
|
||||||
|
use std::time::Instant;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
use tokio::sync::broadcast::Receiver;
|
||||||
|
use tokio::time::interval;
|
||||||
|
|
||||||
|
/// Messages sent to the client
|
||||||
|
#[derive(Debug, serde::Serialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum WsMessage {
|
||||||
|
/// Room message event
|
||||||
|
RoomMessageEvent {
|
||||||
|
event: RoomMessageEventContent,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Main WS route
|
||||||
|
pub async fn ws(
|
||||||
|
req: HttpRequest,
|
||||||
|
stream: web::Payload,
|
||||||
|
tx: web::Data<broadcast::Sender<BroadcastMessage>>,
|
||||||
|
manager: web::Data<ActorRef<MatrixManagerMsg>>,
|
||||||
|
) -> HttpResult {
|
||||||
|
// Forcefully ignore request payload by manually extracting authentication information
|
||||||
|
let client = MatrixClientExtractor::from_request(&req, &mut Payload::None).await?;
|
||||||
|
|
||||||
|
// Ensure sync thread is started
|
||||||
|
ractor::cast!(
|
||||||
|
manager,
|
||||||
|
MatrixManagerMsg::StartSyncThread(client.auth.user.email.clone())
|
||||||
|
)
|
||||||
|
.expect("Failed to start sync thread prior to running WebSocket!");
|
||||||
|
|
||||||
|
let rx = tx.subscribe();
|
||||||
|
|
||||||
|
let (res, session, msg_stream) = actix_ws::handle(&req, stream)?;
|
||||||
|
|
||||||
|
// spawn websocket handler (and don't await it) so that the response is returned immediately
|
||||||
|
actix_web::rt::spawn(ws_handler(
|
||||||
|
session,
|
||||||
|
msg_stream,
|
||||||
|
client.auth,
|
||||||
|
client.client,
|
||||||
|
rx,
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn ws_handler(
|
||||||
|
mut session: actix_ws::Session,
|
||||||
|
mut msg_stream: actix_ws::MessageStream,
|
||||||
|
auth: AuthExtractor,
|
||||||
|
client: MatrixClient,
|
||||||
|
mut rx: Receiver<BroadcastMessage>,
|
||||||
|
) {
|
||||||
|
log::info!(
|
||||||
|
"WS connected for user {:?} / auth method={}",
|
||||||
|
client.email,
|
||||||
|
auth.method.light_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut last_heartbeat = Instant::now();
|
||||||
|
let mut interval = interval(constants::WS_HEARTBEAT_INTERVAL);
|
||||||
|
|
||||||
|
let reason = loop {
|
||||||
|
// waits for either `msg_stream` to receive a message from the client, the broadcast channel
|
||||||
|
// to send a message, or the heartbeat interval timer to tick, yielding the value of
|
||||||
|
// whichever one is ready first
|
||||||
|
tokio::select! {
|
||||||
|
ws_msg = rx.recv() => {
|
||||||
|
let msg = match ws_msg {
|
||||||
|
Ok(msg) => msg,
|
||||||
|
Err(broadcast::error::RecvError::Closed) => break None,
|
||||||
|
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
match msg {
|
||||||
|
BroadcastMessage::APITokenDeleted(t) => {
|
||||||
|
match &auth.method{
|
||||||
|
AuthenticatedMethod::Token(tok) if tok.id == t.id => {
|
||||||
|
log::info!(
|
||||||
|
"closing WS session of user {:?} as associated token was deleted {:?}",
|
||||||
|
client.email,
|
||||||
|
t.base.name
|
||||||
|
);
|
||||||
|
break None;
|
||||||
|
}
|
||||||
|
_=>{}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
BroadcastMessage::UserDisconnectedFromMatrix(mail) if mail == auth.user.email => {
|
||||||
|
log::info!(
|
||||||
|
"closing WS session of user {mail:?} as user was disconnected from Matrix"
|
||||||
|
);
|
||||||
|
break None;
|
||||||
|
}
|
||||||
|
|
||||||
|
BroadcastMessage::RoomMessageEvent{user, event, room} if user == auth.user.email => {
|
||||||
|
// Send the message to the websocket
|
||||||
|
if let Ok(msg) = serde_json::to_string(&WsMessage::RoomMessageEvent {
|
||||||
|
event:event.content,
|
||||||
|
room_id: room.room_id().to_owned(),
|
||||||
|
}) && let Err(e) = session.text(msg).await {
|
||||||
|
log::error!("Failed to send SyncEvent: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// heartbeat interval ticked
|
||||||
|
_tick = interval.tick() => {
|
||||||
|
// if no heartbeat ping/pong received recently, close the connection
|
||||||
|
if Instant::now().duration_since(last_heartbeat) > constants::WS_CLIENT_TIMEOUT {
|
||||||
|
log::info!(
|
||||||
|
"client has not sent heartbeat in over {:?}; disconnecting",constants::WS_CLIENT_TIMEOUT
|
||||||
|
);
|
||||||
|
|
||||||
|
break None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// send heartbeat ping
|
||||||
|
let _ = session.ping(b"").await;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Websocket messages
|
||||||
|
msg = msg_stream.next() => {
|
||||||
|
let msg = match msg {
|
||||||
|
// received message from WebSocket client
|
||||||
|
Some(Ok(msg)) => msg,
|
||||||
|
|
||||||
|
// client WebSocket stream error
|
||||||
|
Some(Err(err)) => {
|
||||||
|
log::error!("{err}");
|
||||||
|
break None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// client WebSocket stream ended
|
||||||
|
None => break None
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("msg: {msg:?}");
|
||||||
|
|
||||||
|
match msg {
|
||||||
|
Message::Text(s) => {
|
||||||
|
log::info!("Text message from WS: {s}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Binary(_) => {
|
||||||
|
// drop client's binary messages
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Close(reason) => {
|
||||||
|
break reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Ping(bytes) => {
|
||||||
|
last_heartbeat = Instant::now();
|
||||||
|
let _ = session.pong(&bytes).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Pong(_) => {
|
||||||
|
last_heartbeat = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
Message::Continuation(_) => {
|
||||||
|
log::warn!("no support for continuation frames");
|
||||||
|
}
|
||||||
|
|
||||||
|
// no-op; ignore
|
||||||
|
Message::Nop => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// attempt to close connection gracefully
|
||||||
|
let _ = session.close(reason).await;
|
||||||
|
|
||||||
|
log::info!("WS disconnected for user {:?}", client.email);
|
||||||
|
}
|
||||||
@@ -28,6 +28,16 @@ pub enum AuthenticatedMethod {
|
|||||||
Token(APIToken),
|
Token(APIToken),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AuthenticatedMethod {
|
||||||
|
pub fn light_str(&self) -> String {
|
||||||
|
match self {
|
||||||
|
AuthenticatedMethod::Cookie => "Cookie".to_string(),
|
||||||
|
AuthenticatedMethod::Dev => "DevAuthentication".to_string(),
|
||||||
|
AuthenticatedMethod::Token(t) => format!("Token({:?} - {})", t.id, t.base.name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AuthExtractor {
|
pub struct AuthExtractor {
|
||||||
pub user: User,
|
pub user: User,
|
||||||
pub method: AuthenticatedMethod,
|
pub method: AuthenticatedMethod,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use matrixgw_backend::broadcast_messages::BroadcastMessage;
|
|||||||
use matrixgw_backend::constants;
|
use matrixgw_backend::constants;
|
||||||
use matrixgw_backend::controllers::{
|
use matrixgw_backend::controllers::{
|
||||||
auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller,
|
auth_controller, matrix_link_controller, matrix_sync_thread_controller, server_controller,
|
||||||
tokens_controller,
|
tokens_controller, ws_controller,
|
||||||
};
|
};
|
||||||
use matrixgw_backend::matrix_connection::matrix_manager::MatrixManagerActor;
|
use matrixgw_backend::matrix_connection::matrix_manager::MatrixManagerActor;
|
||||||
use matrixgw_backend::users::User;
|
use matrixgw_backend::users::User;
|
||||||
@@ -133,6 +133,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/api/matrix_sync/status",
|
"/api/matrix_sync/status",
|
||||||
web::get().to(matrix_sync_thread_controller::status),
|
web::get().to(matrix_sync_thread_controller::status),
|
||||||
)
|
)
|
||||||
|
.service(web::resource("/api/ws").route(web::get().to(ws_controller::ws)))
|
||||||
})
|
})
|
||||||
.workers(4)
|
.workers(4)
|
||||||
.bind(&AppConfig::get().listen_address)?
|
.bind(&AppConfig::get().listen_address)?
|
||||||
|
|||||||
Reference in New Issue
Block a user