use std::{fs::File, io, sync::LazyLock};
use actix_web::{
App, Error, HttpRequest, HttpServer, error,
http::header::{HeaderName, HeaderValue},
middleware::Logger,
web::{self, Bytes},
};
use actix_web_lab::extract::{Json, RequestSignature, RequestSignatureScheme};
use ed25519_dalek::{Signature, StreamVerifier, VerifyingKey};
use hex_literal::hex;
use rustls::{ServerConfig, pki_types::PrivateKeyDer};
use rustls_pemfile::{certs, pkcs8_private_keys};
use tracing::info;
const APP_PUBLIC_KEY_BYTES: &[u8; 32] =
&hex!("d7d9a14753b591be99a0c5721be8083b1e486c3fcdc6ac08bfb63a6e5c204569");
static SIG_HDR_NAME: HeaderName = HeaderName::from_static("x-signature-ed25519");
static TS_HDR_NAME: HeaderName = HeaderName::from_static("x-signature-timestamp");
static APP_PUBLIC_KEY: LazyLock<VerifyingKey> =
LazyLock::new(|| VerifyingKey::from_bytes(APP_PUBLIC_KEY_BYTES).unwrap());
struct DiscordWebhook {
candidate_signature: Signature,
verifier: StreamVerifier,
}
impl DiscordWebhook {
fn get_timestamp(req: &HttpRequest) -> Result<&[u8], Error> {
req.headers()
.get(&TS_HDR_NAME)
.map(HeaderValue::as_bytes)
.ok_or_else(|| error::ErrorUnauthorized("timestamp not provided"))
}
fn get_signature(req: &HttpRequest) -> Result<Signature, Error> {
let sig: [u8; 64] = req
.headers()
.get(&SIG_HDR_NAME)
.map(HeaderValue::as_bytes)
.map(hex::decode)
.transpose()
.map_err(|_| error::ErrorInternalServerError("invalid signature"))?
.ok_or_else(|| error::ErrorUnauthorized("signature not provided"))?
.try_into()
.map_err(|_| error::ErrorInternalServerError("invalid signature"))?;
Ok(Signature::from(sig))
}
}
impl RequestSignatureScheme for DiscordWebhook {
type Signature = Signature;
type Error = Error;
async fn init(req: &HttpRequest) -> Result<Self, Self::Error> {
let ts = Self::get_timestamp(req)?.to_owned();
let candidate_signature = Self::get_signature(req)?;
let mut verifier = APP_PUBLIC_KEY
.verify_stream(&candidate_signature)
.map_err(error::ErrorBadRequest)?;
verifier.update(ts);
Ok(Self {
candidate_signature,
verifier,
})
}
async fn consume_chunk(&mut self, _req: &HttpRequest, chunk: Bytes) -> Result<(), Self::Error> {
self.verifier.update(chunk);
Ok(())
}
async fn finalize(self, _req: &HttpRequest) -> Result<Self::Signature, Self::Error> {
self.verifier.finalize_and_verify().map_err(|_| {
error::ErrorUnauthorized("given signature does not match calculated signature")
})?;
Ok(self.candidate_signature)
}
fn verify(
signature: Self::Signature,
_req: &HttpRequest,
) -> Result<Self::Signature, Self::Error> {
Ok(signature)
}
}
#[actix_web::main]
async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
info!("staring server at http://0.0.0.0:443");
HttpServer::new(|| {
App::new().wrap(Logger::default().log_target("@")).route(
"/webhook",
web::post().to(
|body: RequestSignature<Json<serde_json::Value>, DiscordWebhook>| async move {
let (Json(form), _) = body.into_parts();
println!("{}", serde_json::to_string_pretty(&form).unwrap());
web::Json(serde_json::json!({
"type": 1
}))
},
),
)
})
.workers(2)
.bind_rustls_0_23(("0.0.0.0", 443), load_rustls_config())?
.run()
.await
}
fn load_rustls_config() -> rustls::ServerConfig {
let config = ServerConfig::builder().with_no_client_auth();
let cert_file = &mut io::BufReader::new(File::open("fullchain.pem").unwrap());
let key_file = &mut io::BufReader::new(File::open("privkey.pem").unwrap());
let cert_chain = certs(cert_file).collect::<Result<Vec<_>, _>>().unwrap();
let keys = pkcs8_private_keys(key_file)
.flat_map(Result::ok)
.next()
.map(PrivateKeyDer::Pkcs8)
.unwrap();
config.with_single_cert(cert_chain, keys).unwrap()
}