use std::future::Future;
use std::time::Duration;
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
use axum::http::HeaderMap;
use axum::response::{IntoResponse, Response};
use super::channel::{Broadcast, ChannelError};
use super::error::{RealtimeError, admission_status};
use super::origin::OriginPolicy;
use super::registry::Registry;
use super::shutdown::ShutdownConfig;
pub trait Authorizer: Clone + Send + Sync + 'static {
fn authorize(
&self,
headers: &HeaderMap,
channel_id: &str,
) -> impl Future<Output = Option<Broadcast>> + Send;
}
#[derive(Clone)]
pub struct AllowAll {
broadcast: Broadcast,
}
impl AllowAll {
#[must_use]
pub fn new(broadcast: Broadcast) -> Self {
Self { broadcast }
}
}
impl Authorizer for AllowAll {
fn authorize(
&self,
_headers: &HeaderMap,
_channel_id: &str,
) -> impl Future<Output = Option<Broadcast>> + Send {
let broadcast = self.broadcast.clone();
std::future::ready(Some(broadcast))
}
}
#[derive(Debug, Clone, Copy)]
pub struct WsLimits {
pub max_message_size: usize,
pub max_frame_size: usize,
pub heartbeat_interval: Duration,
pub heartbeat_timeout: Duration,
}
impl WsLimits {
#[must_use]
pub fn conservative() -> Self {
Self {
max_message_size: 64 * 1024,
max_frame_size: 64 * 1024,
heartbeat_interval: Duration::from_secs(20),
heartbeat_timeout: Duration::from_secs(40),
}
}
}
#[derive(Clone)]
pub struct WebSocketEndpoint<A: Authorizer> {
authorizer: A,
origin: OriginPolicy,
registry: Registry,
limits: WsLimits,
shutdown: ShutdownConfig,
}
impl<A: Authorizer> std::fmt::Debug for WebSocketEndpoint<A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WebSocketEndpoint")
.field("limits", &self.limits)
.field("max_connections", &self.shutdown.max_connections())
.finish_non_exhaustive()
}
}
impl<A: Authorizer> WebSocketEndpoint<A> {
#[must_use]
pub fn new(
authorizer: A,
origin: OriginPolicy,
registry: Registry,
limits: WsLimits,
shutdown: ShutdownConfig,
) -> Self {
Self {
authorizer,
origin,
registry,
limits,
shutdown,
}
}
pub async fn handle(
self,
ws: WebSocketUpgrade,
headers: HeaderMap,
channel_id: String,
) -> Response {
let origin_header = headers.get("origin").cloned();
if matches!(
self.origin.authorize(origin_header.as_ref()),
super::origin::OriginDecision::Denied
) {
return admission_status(&RealtimeError::Origin).into_response();
}
let broadcast = match self.authorizer.authorize(&headers, &channel_id).await {
Some(b) => b,
None => return admission_status(&RealtimeError::Unauthorized).into_response(),
};
let guard = match self.registry.acquire(self.shutdown.max_connections()) {
Ok(g) => g,
Err(e) => return admission_status(&e).into_response(),
};
let limits = self.limits;
let shutdown = self.shutdown.clone();
ws.max_message_size(limits.max_message_size)
.max_frame_size(limits.max_frame_size)
.on_upgrade(move |socket| run_connection(socket, broadcast, guard, limits, shutdown))
}
}
async fn run_connection(
mut socket: WebSocket,
broadcast: Broadcast,
_guard: super::registry::ConnectionGuard,
limits: WsLimits,
shutdown: ShutdownConfig,
) {
let mut sub = broadcast.subscribe();
let mut heartbeat = tokio::time::interval(limits.heartbeat_interval);
heartbeat.tick().await;
let mut last_pong = tokio::time::Instant::now();
loop {
tokio::select! {
_ = shutdown.drain_notified() => {
let _ = socket.send(Message::Close(None)).await;
break;
}
msg = socket.recv() => {
match msg {
Some(Ok(msg)) => {
match msg {
Message::Close(_) => break,
Message::Pong(_) => {
last_pong = tokio::time::Instant::now();
}
_ => {}
}
}
Some(Err(_)) | None => break,
}
}
res = sub.recv() => {
match res {
Ok(payload) => {
let _ = socket
.send(Message::Binary(bytes::Bytes::from(
payload.as_bytes().to_vec(),
)))
.await;
}
Err(ChannelError::Closed) => break,
Err(ChannelError::Lagged) => continue,
Err(ChannelError::Full) => continue,
}
}
_ = heartbeat.tick() => {
let elapsed = tokio::time::Instant::now().duration_since(last_pong);
if elapsed > limits.heartbeat_timeout {
let _ = socket.send(Message::Close(None)).await;
break;
}
let _ = socket.send(Message::Ping(bytes::Bytes::new())).await;
}
}
}
}