From 89c69eae8e75d2b00794087d9ef74fd4856d0f88 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 20 Nov 2025 21:36:09 +0000 Subject: replace actix with hyper --- src/http/landing.rs | 35 ++----------- src/http/mod.rs | 142 ++++++++++++++++++++++++++++++++++++++++++++------ src/http/websocket.rs | 73 -------------------------- 3 files changed, 131 insertions(+), 119 deletions(-) delete mode 100644 src/http/websocket.rs (limited to 'src') diff --git a/src/http/landing.rs b/src/http/landing.rs index 976ec50..55ffb26 100644 --- a/src/http/landing.rs +++ b/src/http/landing.rs @@ -1,40 +1,15 @@ /// Landing Page Handler /// -/// Serves the HTML landing page or upgrades to WebSocket for Nostr relay connections. -use actix_web::{web, HttpRequest, HttpResponse, Result}; -use nostr_relay_builder::LocalRelay; - +/// Generates HTML landing page for the Nostr relay. use crate::config::Config; -/// Handle landing page or WebSocket upgrade -pub async fn handle( - req: HttpRequest, - stream: web::Payload, - config: web::Data, - relay: web::Data, -) -> Result { - // Check if this is a WebSocket upgrade request - if let Some(upgrade) = req.headers().get("upgrade") { - if upgrade - .to_str() - .unwrap_or("") - .eq_ignore_ascii_case("websocket") - { - // Delegate to WebSocket handler - return crate::http::websocket::handle(req, stream, relay).await; - } - } - - // Otherwise, serve the landing page - let html = format!( +/// Generate the HTML landing page +pub fn get_html(config: &Config) -> String { + format!( include_str!("../../templates/landing.html"), relay_name = config.relay_name, relay_description = config.relay_description, domain = config.domain, bind_address = config.bind_address, - ); - - Ok(HttpResponse::Ok() - .content_type("text/html; charset=utf-8") - .body(html)) + ) } diff --git a/src/http/mod.rs b/src/http/mod.rs index b434c69..4690790 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,30 +1,140 @@ /// HTTP Server Module /// -/// Provides actix-web HTTP server with WebSocket upgrade support for the Nostr relay. +/// Provides hyper HTTP server with WebSocket upgrade support for the Nostr relay. pub mod landing; -pub mod websocket; -use actix_web::{middleware, web, App, HttpServer}; +use std::future::Future; +use std::net::SocketAddr; +use std::pin::Pin; + +use hyper::body::Incoming; +use hyper::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, UPGRADE}; +use hyper::server::conn::http1; +use hyper::service::Service; +use hyper::{Request, Response}; +use hyper_util::rt::TokioIo; +use nostr_sdk::hashes::sha1::Hash as Sha1Hash; +use nostr_sdk::hashes::{Hash, HashEngine}; use nostr_relay_builder::LocalRelay; +use tokio::net::TcpListener; +use base64::Engine; use crate::config::Config; +/// HTTP Service that serves both WebSocket (relay) and HTML landing page +struct HttpService { + relay: LocalRelay, + config: Config, + remote: SocketAddr, +} + +impl HttpService { + fn new(relay: LocalRelay, config: Config, remote: SocketAddr) -> Self { + Self { + relay, + config, + remote, + } + } +} + +impl Service> for HttpService { + type Response = Response; + type Error = String; + type Future = Pin> + Send>>; + + fn call(&self, req: Request) -> Self::Future { + let base = Response::builder().header("server", "ngit-grasp"); + + // Check if this is a WebSocket upgrade request + if let (Some(c), Some(w)) = ( + req.headers().get("connection"), + req.headers().get("upgrade"), + ) { + if c.to_str() + .map(|s| s.to_lowercase() == "upgrade") + .unwrap_or(false) + && w.to_str() + .map(|s| s.to_lowercase() == "websocket") + .unwrap_or(false) + { + let key = req.headers().get("sec-websocket-key"); + let derived = key.map(|k| derive_accept_key(k.as_bytes())); + + let addr = self.remote; + let relay = self.relay.clone(); + + tokio::spawn(async move { + match hyper::upgrade::on(req).await { + Ok(upgraded) => { + tracing::info!("WebSocket connection established from {}", addr); + if let Err(e) = relay.take_connection(TokioIo::new(upgraded), addr).await + { + tracing::error!("Relay error for {}: {}", addr, e); + } + tracing::info!("WebSocket connection closed for {}", addr); + } + Err(e) => tracing::error!("Upgrade error: {}", e), + } + }); + + return Box::pin(async move { + Ok(base + .status(101) + .header(CONNECTION, "upgrade") + .header(UPGRADE, "websocket") + .header(SEC_WEBSOCKET_ACCEPT, derived.unwrap()) + .body("".to_string()) + .unwrap()) + }); + } + } + + // Serve landing page for HTTP requests + let html = landing::get_html(&self.config); + Box::pin(async move { + Ok(base + .status(200) + .header("content-type", "text/html; charset=utf-8") + .body(html) + .unwrap()) + }) + } +} + +/// Derive the `Sec-WebSocket-Accept` response header from a `Sec-WebSocket-Key` request header +fn derive_accept_key(request_key: &[u8]) -> String { + const WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + let mut engine = Sha1Hash::engine(); + engine.input(request_key); + engine.input(WS_GUID); + let hash: Sha1Hash = Sha1Hash::from_engine(engine); + base64::prelude::BASE64_STANDARD.encode(hash) +} + /// Start the HTTP server with integrated Nostr relay pub async fn run_server(config: Config, relay: LocalRelay) -> anyhow::Result<()> { - let bind_addr = config.bind_address.clone(); + let bind_addr: SocketAddr = config.bind_address.parse()?; tracing::info!("Starting HTTP server on {}", bind_addr); + tracing::info!("Relay name: {}", config.relay_name); + tracing::info!("Domain: {}", config.domain); - HttpServer::new(move || { - App::new() - .app_data(web::Data::new(config.clone())) - .app_data(web::Data::new(relay.clone())) - .wrap(middleware::Logger::default()) - .route("/", web::get().to(landing::handle)) - }) - .bind(&bind_addr)? - .run() - .await?; - - Ok(()) + let listener = TcpListener::bind(&bind_addr).await?; + + loop { + let (socket, addr) = listener.accept().await?; + let io = TokioIo::new(socket); + let service = HttpService::new(relay.clone(), config.clone(), addr); + + tokio::spawn(async move { + if let Err(e) = http1::Builder::new() + .serve_connection(io, service) + .with_upgrades() + .await + { + tracing::error!("Failed to handle request from {}: {}", addr, e); + } + }); + } } diff --git a/src/http/websocket.rs b/src/http/websocket.rs deleted file mode 100644 index 0171013..0000000 --- a/src/http/websocket.rs +++ /dev/null @@ -1,73 +0,0 @@ -/// WebSocket Handler -/// -/// Handles WebSocket upgrade requests and passes connections to the Nostr relay. -use actix_web::{web, Error, HttpRequest, HttpResponse, Result}; -use actix_ws::Message; -use futures_util::StreamExt; -use nostr_relay_builder::LocalRelay; - -/// Handle WebSocket upgrade and relay connection -pub async fn handle( - req: HttpRequest, - stream: web::Payload, - relay: web::Data, -) -> Result { - let (response, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?; - - let peer_addr = req - .peer_addr() - .unwrap_or_else(|| "0.0.0.0:0".parse().unwrap()); - - tracing::debug!("WebSocket connection from {}", peer_addr); - - // Spawn task to handle the WebSocket connection - // TODO: Will use relay.take_connection() for full Nostr relay integration - let _relay = relay.get_ref().clone(); - actix_web::rt::spawn(async move { - // Create a channel to communicate between actix-ws and relay - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - - // Spawn task to send messages from relay to client - let mut session_clone = session.clone(); - actix_web::rt::spawn(async move { - while let Some(msg) = rx.recv().await { - if session_clone.text(msg).await.is_err() { - break; - } - } - }); - - // Handle incoming messages from client - while let Some(Ok(msg)) = msg_stream.next().await { - match msg { - Message::Text(text) => { - // For now, just echo back - will integrate with relay in next phase - tracing::debug!("Received text message: {}", text); - if let Err(e) = tx.send(text.to_string()) { - tracing::error!("Failed to send message: {}", e); - break; - } - } - Message::Binary(_) => { - tracing::warn!("Received unexpected binary message"); - } - Message::Close(_) => { - tracing::debug!("Client closed connection"); - break; - } - Message::Ping(bytes) => { - if session.pong(&bytes).await.is_err() { - break; - } - } - Message::Pong(_) => {} - Message::Continuation(_) => {} - Message::Nop => {} - } - } - - tracing::debug!("WebSocket connection closed for {}", peer_addr); - }); - - Ok(response) -} -- cgit v1.2.3