#![allow(clippy::result_large_err)]
use std::sync::{Arc, LazyLock};
use std::time::Duration;
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use openraft::error::{
InstallSnapshotError, NetworkError, RPCError, RaftError, RemoteError, Unreachable,
};
use openraft::network::{RPCOption, RaftNetwork, RaftNetworkFactory};
use openraft::raft::{
AppendEntriesRequest, AppendEntriesResponse, InstallSnapshotRequest, InstallSnapshotResponse,
VoteRequest, VoteResponse,
};
use openraft::BasicNode;
use serde::de::DeserializeOwned;
use serde::Serialize;
use subtle::ConstantTimeEq;
use super::{NodeId, Raft, Reply, Request, TypeConfig};
const SECRET_HEADER: &str = "x-orch-cluster-secret";
const NONCE_HEADER: &str = "x-orch-nonce";
const PROOF_HEADER: &str = "x-orch-proof";
static CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.unwrap_or_default()
});
#[derive(Clone)]
struct App {
raft: Raft,
secret: Arc<String>,
me: Arc<Hello>,
addr: Arc<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, serde::Deserialize)]
pub struct Hello {
pub name: String,
pub id: NodeId,
pub witness: bool,
#[serde(default)]
pub leader: Option<NodeId>,
#[serde(default)]
pub term: u64,
#[serde(default)]
pub proof: String,
}
fn mac(secret: &str, parts: &[&str]) -> String {
use hmac::{Hmac, Mac};
let mut m =
Hmac::<sha2::Sha256>::new_from_slice(secret.as_bytes()).expect("HMAC takes any key");
for part in parts {
m.update(part.as_bytes());
m.update(b"\0");
}
m.finalize()
.into_bytes()
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
#[derive(Debug, Clone, Serialize, serde::Deserialize)]
pub struct Join {
pub id: NodeId,
pub addr: String,
}
fn authorized(app: &App, headers: &HeaderMap) -> bool {
let given = headers
.get(SECRET_HEADER)
.map(|v| v.as_bytes())
.unwrap_or_default();
given.ct_eq(app.secret.as_bytes()).into()
}
pub(crate) fn router(raft: Raft, secret: Arc<String>, me: Hello, addr: String) -> Router {
Router::new()
.route("/raft/append", post(append))
.route("/raft/snapshot", post(snapshot))
.route("/raft/vote", post(vote))
.route("/cluster/write", post(write))
.route("/cluster/hello", get(hello))
.route("/cluster/join", post(join))
.with_state(App {
raft,
secret,
me: Arc::new(me),
addr: Arc::new(addr),
})
}
async fn hello(State(a): State<App>, headers: HeaderMap) -> Response {
let header = |name: &str| {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_string()
};
let nonce = header(NONCE_HEADER);
let expected = mac(&a.secret, &["probe", &nonce, &a.addr]);
let proven: bool = header(PROOF_HEADER)
.as_bytes()
.ct_eq(expected.as_bytes())
.into();
if nonce.len() < 16 || !proven {
return StatusCode::UNAUTHORIZED.into_response();
}
let m = a.raft.metrics().borrow().clone();
Json(Hello {
leader: m.current_leader,
term: m.current_term,
proof: mac(&a.secret, &["hello", &nonce, &a.me.name, &a.addr]),
..(*a.me).clone()
})
.into_response()
}
async fn join(State(a): State<App>, headers: HeaderMap, Json(req): Json<Join>) -> Response {
if !authorized(&a, &headers) {
return StatusCode::UNAUTHORIZED.into_response();
}
let node = BasicNode::new(&req.addr);
match a.raft.add_learner(req.id, node, false).await {
Ok(_) => StatusCode::OK.into_response(),
Err(RaftError::APIError(openraft::error::ClientWriteError::ForwardToLeader(f))) => {
match f.leader_node {
Some(leader) => match send_join(&leader.addr, &a.secret, &req).await {
Ok(()) => StatusCode::OK.into_response(),
Err(e) => (StatusCode::BAD_GATEWAY, e.to_string()).into_response(),
},
None => (StatusCode::SERVICE_UNAVAILABLE, "no leader yet").into_response(),
}
}
Err(e) => (StatusCode::SERVICE_UNAVAILABLE, e.to_string()).into_response(),
}
}
pub async fn send_join(addr: &str, secret: &str, req: &Join) -> anyhow::Result<()> {
let res = CLIENT
.post(format!("http://{addr}/cluster/join"))
.header(SECRET_HEADER, secret)
.json(req)
.send()
.await?;
anyhow::ensure!(
res.status().is_success(),
"join refused by {addr} ({}): {}",
res.status(),
res.text().await?
);
Ok(())
}
pub async fn probe(addr: &str, secret: &str, expect_name: Option<&str>) -> Option<Hello> {
let nonce = uuid::Uuid::new_v4().simple().to_string();
let res = CLIENT
.get(format!("http://{addr}/cluster/hello"))
.header(NONCE_HEADER, &nonce)
.header(PROOF_HEADER, mac(secret, &["probe", &nonce, addr]))
.timeout(Duration::from_secs(1))
.send()
.await
.ok()?;
let hello: Hello = res.error_for_status().ok()?.json().await.ok()?;
if expect_name.is_some_and(|name| name != hello.name) {
return None;
}
let expected = mac(secret, &["hello", &nonce, &hello.name, addr]);
let proven: bool = hello.proof.as_bytes().ct_eq(expected.as_bytes()).into();
proven.then_some(hello)
}
async fn append(
State(a): State<App>,
headers: HeaderMap,
Json(req): Json<AppendEntriesRequest<TypeConfig>>,
) -> Response {
if !authorized(&a, &headers) {
return StatusCode::UNAUTHORIZED.into_response();
}
Json(a.raft.append_entries(req).await).into_response()
}
async fn snapshot(
State(a): State<App>,
headers: HeaderMap,
Json(req): Json<InstallSnapshotRequest<TypeConfig>>,
) -> Response {
if !authorized(&a, &headers) {
return StatusCode::UNAUTHORIZED.into_response();
}
Json(a.raft.install_snapshot(req).await).into_response()
}
async fn vote(
State(a): State<App>,
headers: HeaderMap,
Json(req): Json<VoteRequest<NodeId>>,
) -> Response {
if !authorized(&a, &headers) {
return StatusCode::UNAUTHORIZED.into_response();
}
Json(a.raft.vote(req).await).into_response()
}
async fn write(State(a): State<App>, headers: HeaderMap, Json(req): Json<Request>) -> Response {
if !authorized(&a, &headers) {
return StatusCode::UNAUTHORIZED.into_response();
}
match a.raft.client_write(req).await {
Ok(r) => Json(r.data).into_response(),
Err(e @ RaftError::APIError(openraft::error::ClientWriteError::ForwardToLeader(_))) => {
(StatusCode::MISDIRECTED_REQUEST, e.to_string()).into_response()
}
Err(e) => (StatusCode::SERVICE_UNAVAILABLE, e.to_string()).into_response(),
}
}
#[derive(Debug)]
pub(crate) enum ForwardError {
NotApplied(String),
Unknown(anyhow::Error),
}
pub(crate) async fn forward(
addr: &str,
secret: &str,
req: &Request,
) -> Result<Reply, ForwardError> {
let res = CLIENT
.post(format!("http://{addr}/cluster/write"))
.header(SECRET_HEADER, secret)
.json(req)
.send()
.await
.map_err(|e| {
if e.is_connect() {
ForwardError::NotApplied(e.to_string())
} else {
ForwardError::Unknown(e.into())
}
})?;
let status = res.status();
if status == StatusCode::MISDIRECTED_REQUEST {
let body = res.text().await.unwrap_or_default();
return Err(ForwardError::NotApplied(format!(
"{addr} is not the leader: {body}"
)));
}
if !status.is_success() {
let body = res.text().await.unwrap_or_default();
return Err(ForwardError::Unknown(anyhow::anyhow!(
"leader at {addr} refused the write ({status}): {body}"
)));
}
res.json()
.await
.map_err(|e| ForwardError::Unknown(e.into()))
}
pub(crate) struct HttpNetwork {
pub secret: Arc<String>,
}
impl RaftNetworkFactory<TypeConfig> for HttpNetwork {
type Network = Peer;
async fn new_client(&mut self, target: NodeId, node: &BasicNode) -> Peer {
Peer {
target,
addr: node.addr.clone(),
secret: self.secret.clone(),
}
}
}
pub(crate) struct Peer {
target: NodeId,
addr: String,
secret: Arc<String>,
}
impl Peer {
async fn send<Req, Resp, E>(
&self,
path: &str,
req: Req,
option: &RPCOption,
) -> Result<Resp, RPCError<NodeId, BasicNode, RaftError<NodeId, E>>>
where
Req: Serialize,
Resp: DeserializeOwned,
E: std::error::Error + DeserializeOwned,
{
let res = CLIENT
.post(format!("http://{}/raft/{path}", self.addr))
.header(SECRET_HEADER, self.secret.as_str())
.timeout(option.hard_ttl())
.json(&req)
.send()
.await
.map_err(|e| {
if e.is_connect() {
RPCError::Unreachable(Unreachable::new(&e))
} else {
RPCError::Network(NetworkError::new(&e))
}
})?;
let res = res
.error_for_status()
.map_err(|e| RPCError::Network(NetworkError::new(&e)))?;
let body: Result<Resp, RaftError<NodeId, E>> = res
.json()
.await
.map_err(|e| RPCError::Network(NetworkError::new(&e)))?;
body.map_err(|e| RPCError::RemoteError(RemoteError::new(self.target, e)))
}
}
impl RaftNetwork<TypeConfig> for Peer {
async fn append_entries(
&mut self,
req: AppendEntriesRequest<TypeConfig>,
option: RPCOption,
) -> Result<AppendEntriesResponse<NodeId>, RPCError<NodeId, BasicNode, RaftError<NodeId>>> {
self.send("append", req, &option).await
}
async fn install_snapshot(
&mut self,
req: InstallSnapshotRequest<TypeConfig>,
option: RPCOption,
) -> Result<
InstallSnapshotResponse<NodeId>,
RPCError<NodeId, BasicNode, RaftError<NodeId, InstallSnapshotError>>,
> {
self.send("snapshot", req, &option).await
}
async fn vote(
&mut self,
req: VoteRequest<NodeId>,
option: RPCOption,
) -> Result<VoteResponse<NodeId>, RPCError<NodeId, BasicNode, RaftError<NodeId>>> {
self.send("vote", req, &option).await
}
}