use axum::extract::State;
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::response::{IntoResponse, Response};
use hmac::{Hmac, Mac};
use std::convert::Infallible;
use std::sync::Arc;
use std::time::Duration;
use exocortex_cluster::{ClusterNode, Replay};
use exocortex_storage::Storage;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SseAuth {
OptionalToken,
RequiredToken,
}
pub fn sse_router<S: Storage + 'static>(
cluster: Arc<ClusterNode<S>>,
auth: SseAuth,
) -> axum::Router {
axum::Router::new()
.route("/v1/changes", axum::routing::get(handler))
.with_state((cluster, auth))
}
pub fn derive_client_sse_key(cluster_key: &[u8; 32], token: &str) -> [u8; 32] {
let mut mac = <Hmac<sha2::Sha256> as Mac>::new_from_slice(cluster_key)
.expect("HMAC accepts any key length");
mac.update(b"sse-client:");
mac.update(token.as_bytes());
mac.finalize().into_bytes().into()
}
async fn handler<S: Storage + 'static>(
State((cluster, auth)): State<(Arc<ClusterNode<S>>, SseAuth)>,
axum::extract::RawQuery(q): axum::extract::RawQuery,
) -> Response {
let mut token = None;
let mut since_lsn = None;
if let Some(qs) = q.as_deref() {
for pair in qs.split('&') {
if let Some((k, v)) = pair.split_once('=') {
match k {
"token" => token = Some(v.to_string()),
"since_lsn" => since_lsn = v.parse::<u64>().ok(),
_ => {}
}
}
}
}
if auth == SseAuth::RequiredToken && token.is_none() {
return (http::StatusCode::UNAUTHORIZED, "missing SSE token").into_response();
}
let replay = match since_lsn {
Some(n) => match cluster.replay_since(n) {
Replay::Fresh(envs) => envs,
Replay::TooOld => {
let mut resp = (http::StatusCode::CONFLICT, "Resync Required").into_response();
if let Ok(v) = http::HeaderValue::from_str(&cluster.replay_floor().to_string()) {
resp.headers_mut().insert("x-exocortex-min-lsn", v);
}
return resp;
}
},
None => vec![],
};
let rx = cluster.subscribe_local();
let node_id = cluster.node_id.to_string();
let client_key = token.map(|t| derive_client_sse_key(&cluster.hmac_key, &t));
let stream = async_stream::stream! {
let mut rx = tokio_stream::wrappers::BroadcastStream::new(rx);
use futures::StreamExt;
yield Ok::<Event, Infallible>(Event::default().comment(format!("exocortex {node_id}")));
for mut env in replay {
if let Some(key) = &client_key {
if env.hmac.is_empty() || cluster.verify_hmac(&env).is_ok() {
resign(key, &mut env);
}
}
let payload = B64::encode(&prost_encode(&env));
yield Ok(Event::default().event("inv").data(payload));
}
while let Some(item) = rx.next().await {
if let Ok(mut env) = item {
if let Some(key) = &client_key {
if env.hmac.is_empty() || cluster.verify_hmac(&env).is_ok() {
resign(key, &mut env);
}
}
let payload = B64::encode(&prost_encode(&env));
yield Ok(Event::default().event("inv").data(payload));
}
}
};
Sse::new(stream)
.keep_alive(KeepAlive::new().interval(Duration::from_secs(5)))
.into_response()
}
fn resign(key: &[u8; 32], env: &mut exocortex_wire::cluster::v1::InvalidationEnvelope) {
use prost::Message;
env.hmac = vec![];
let mut mac =
<Hmac<sha2::Sha256> as Mac>::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(&env.encode_to_vec());
env.hmac = mac.finalize().into_bytes().to_vec();
}
struct B64;
impl B64 {
const T: &'static [u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn encode(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b = [
chunk[0],
chunk.get(1).copied().unwrap_or(0),
chunk.get(2).copied().unwrap_or(0),
];
let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
out.push(Self::T[(n >> 18) as usize & 63] as char);
out.push(Self::T[(n >> 12) as usize & 63] as char);
out.push(if chunk.len() > 1 {
Self::T[(n >> 6) as usize & 63] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
Self::T[n as usize & 63] as char
} else {
'='
});
}
out
}
}
fn prost_encode(env: &exocortex_wire::cluster::v1::InvalidationEnvelope) -> Vec<u8> {
use prost::Message;
env.encode_to_vec()
}