25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 1 | use std::collections::HashMap; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 2 | use std::convert::Infallible; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 3 | use std::net::{IpAddr, SocketAddr}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 4 | use std::sync::{Arc, Mutex, PoisonError}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 5 | use std::time::Duration; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 6 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 7 | use anyhow::Context as _; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 8 | use http_body_util::Full; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 9 | use hyper::body::{Bytes, Incoming}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 10 | use hyper::header::{ACCEPT, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE, HeaderValue, UPGRADE}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 11 | use hyper::server::conn::http1::Builder as Http1Builder; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 12 | use hyper::service::service_fn; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 13 | use hyper::upgrade::on as upgrade; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 14 | use hyper::{Method, Request, Response, StatusCode}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 15 | use hyper_util::rt::{TokioIo, TokioTimer}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 16 | use nostr_database::NostrDatabase; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 17 | use serde_json::json; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 18 | use tokio::net::TcpListener; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 19 | use tokio::sync::{OwnedSemaphorePermit, Semaphore}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 20 | use tokio::task::{JoinHandle, spawn as spawn_task}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 21 | use tokio::time::timeout; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 22 | use tokio_tungstenite::tungstenite::handshake::server::create_response; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 23 | use tokio_util::sync::CancellationToken; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 24 | use tokio_util::task::TaskTracker; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 25 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 26 | use crate::config::Config; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 27 | use crate::federation::spawn as spawn_federation; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 28 | use crate::policy::Policy; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 29 | use crate::relay::Relay; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 30 | use crate::store::Store; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 31 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 32 | type Body = Full<Bytes>; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 33 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 34 | struct ConnectionLease { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 35 | ip: IpAddr, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 36 | peers: Arc<Mutex<HashMap<IpAddr, usize>>>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 37 | _permit: OwnedSemaphorePermit, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 38 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 39 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 40 | impl Drop for ConnectionLease { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 41 | fn drop(&mut self) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 42 | let mut peers = self.peers.lock().unwrap_or_else(PoisonError::into_inner); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 43 | if let Some(count) = peers.get_mut(&self.ip) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 44 | *count -= 1; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 45 | if *count == 0 { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 46 | peers.remove(&self.ip); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 47 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 48 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 49 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 50 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 51 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 52 | #[derive(Clone)] |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 53 | struct HttpState { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 54 | relay: Relay, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 55 | store: Arc<Store>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 56 | config: Arc<Config>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 57 | websocket_path: String, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 58 | tasks: TaskTracker, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 59 | stop: CancellationToken, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 60 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 61 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 62 | pub struct RunningRelay { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 63 | address: SocketAddr, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 64 | path: String, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 65 | state: HttpState, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 66 | listener: JoinHandle<anyhow::Result<()>>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 67 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 68 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 69 | impl RunningRelay { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 70 | #[must_use] |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 71 | pub fn url(&self) -> String { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 72 | format!("ws://{}{}", self.address, self.path) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 73 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 74 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 75 | pub async fn shutdown(self) -> anyhow::Result<()> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 76 | self.state.stop.cancel(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 77 | self.listener.await.context("joining listener")??; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 78 | self.state.tasks.close(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 79 | timeout(Duration::from_secs(30), self.state.tasks.wait()) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 80 | .await |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 81 | .context("relay connections did not drain")?; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 82 | Ok(()) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 83 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 84 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 85 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 86 | pub async fn spawn(config: Config) -> anyhow::Result<RunningRelay> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 87 | config.validate()?; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 88 | let store = Store::open(&config).await?; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 89 | let policy_database: Arc<dyn NostrDatabase> = Arc::<Store>::clone(&store); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 90 | let policy = Policy::new(policy_database, &config)?; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 91 | let listener = TcpListener::bind(config.listen) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 92 | .await |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 93 | .context("binding relay listener")?; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 94 | let address = listener.local_addr()?; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 95 | let path = config.websocket_path()?; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 96 | let stop = CancellationToken::new(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 97 | let relay = Relay::new( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 98 | Arc::clone(&store), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 99 | policy.clone(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 100 | config.limits.clone(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 101 | stop.clone(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 102 | ); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 103 | let state = HttpState { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 104 | relay, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 105 | store, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 106 | config: Arc::new(config), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 107 | websocket_path: path.clone(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 108 | tasks: TaskTracker::new(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 109 | stop, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 110 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 111 | let task_state = state.clone(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 112 | spawn_federation( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 113 | Arc::clone(&state.config), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 114 | Arc::clone(&state.store), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 115 | state.relay.clone(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 116 | policy, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 117 | &state.tasks, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 118 | state.stop.clone(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 119 | ); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 120 | let listener_task = spawn_task(async move { accept_connections(listener, task_state).await }); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 121 | tracing::info!(%address, %path, "relay is listening"); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 122 | Ok(RunningRelay { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 123 | address, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 124 | path, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 125 | state, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 126 | listener: listener_task, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 127 | }) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 128 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 129 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 130 | async fn accept_connections(listener: TcpListener, state: HttpState) -> anyhow::Result<()> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 131 | let slots = Arc::new(Semaphore::new(state.config.limits.max_connections)); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 132 | let peers = Arc::new(Mutex::new(HashMap::<IpAddr, usize>::new())); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 133 | loop { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 134 | let (stream, address) = tokio::select! { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 135 | biased; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 136 | () = state.stop.cancelled() => return Ok(()), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 137 | accepted = listener.accept() => accepted?, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 138 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 139 | let Ok(permit) = Arc::clone(&slots).try_acquire_owned() else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 140 | continue; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 141 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 142 | let mut peer_counts = peers.lock().unwrap_or_else(PoisonError::into_inner); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 143 | let count = peer_counts.entry(address.ip()).or_default(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 144 | if *count >= state.config.limits.max_connections_per_ip { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 145 | continue; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 146 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 147 | *count += 1; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 148 | drop(peer_counts); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 149 | let lease = Arc::new(ConnectionLease { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 150 | ip: address.ip(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 151 | peers: Arc::clone(&peers), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 152 | _permit: permit, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 153 | }); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 154 | let handler_state = state.clone(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 155 | let stop = state.stop.clone(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 156 | state.tasks.spawn(async move { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 157 | let handler = service_fn(move |request| { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 158 | handle_request( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 159 | request, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 160 | address, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 161 | handler_state.clone(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 162 | Arc::clone(&lease), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 163 | ) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 164 | }); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 165 | let mut builder = Http1Builder::new(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 166 | builder.timer(TokioTimer::new()).header_read_timeout(Duration::from_secs(10)); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 167 | let connection = builder.serve_connection(TokioIo::new(stream), handler).with_upgrades(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 168 | tokio::pin!(connection); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 169 | tokio::select! { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 170 | result = &mut connection => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 171 | if let Err(error) = result { tracing::debug!(%address, %error, "http connection ended"); } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 172 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 173 | () = stop.cancelled() => {} |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 174 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 175 | }); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 176 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 177 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 178 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 179 | async fn handle_request( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 180 | mut request: Request<Incoming>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 181 | address: SocketAddr, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 182 | state: HttpState, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 183 | lease: Arc<ConnectionLease>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 184 | ) -> Result<Response<Body>, Infallible> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 185 | if state.stop.is_cancelled() { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 186 | return Ok(response( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 187 | StatusCode::SERVICE_UNAVAILABLE, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 188 | "text/plain", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 189 | "relay is stopping", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 190 | )); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 191 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 192 | if request.method() != Method::GET { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 193 | return Ok(response( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 194 | StatusCode::METHOD_NOT_ALLOWED, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 195 | "text/plain", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 196 | "GET required", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 197 | )); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 198 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 199 | if request.uri().path() == "/healthz" { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 200 | let healthy = timeout(Duration::from_secs(2), state.store.healthy()) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 201 | .await |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 202 | .unwrap_or(false); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 203 | return Ok(response( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 204 | if healthy { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 205 | StatusCode::OK |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 206 | } else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 207 | StatusCode::SERVICE_UNAVAILABLE |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 208 | }, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 209 | "text/plain", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 210 | if healthy { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 211 | "ok" |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 212 | } else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 213 | "database unavailable" |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 214 | }, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 215 | )); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 216 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 217 | if request.uri().path() != state.websocket_path { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 218 | return Ok(response(StatusCode::NOT_FOUND, "text/plain", "not found")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 219 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 220 | if !request.headers().contains_key(UPGRADE) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 221 | let wants_information = request |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 222 | .headers() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 223 | .get(ACCEPT) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 224 | .and_then(|value| value.to_str().ok()) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 225 | .is_some_and(|value| { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 226 | value.split(',').any(|media_type| { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 227 | media_type.trim().split(';').next() == Some("application/nostr+json") |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 228 | }) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 229 | }); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 230 | return Ok(if wants_information { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 231 | response( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 232 | StatusCode::OK, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 233 | "application/nostr+json", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 234 | &information(&state.config), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 235 | ) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 236 | } else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 237 | response( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 238 | StatusCode::OK, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 239 | "text/plain", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 240 | "vineyard\na persistent nostr relay for git forges\n", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 241 | ) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 242 | }); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 243 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 244 | let upgrade = upgrade(&mut request); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 245 | let Ok(handshake) = create_response(&request.map(|_body| ())) else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 246 | return Ok(response( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 247 | StatusCode::BAD_REQUEST, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 248 | "text/plain", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 249 | "invalid WebSocket handshake", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 250 | )); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 251 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 252 | let relay = state.relay.clone(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 253 | state.tasks.spawn(async move { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 254 | let _lease = lease; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 255 | if let Ok(Ok(stream)) = timeout(Duration::from_secs(10), upgrade).await { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 256 | if let Err(error) = relay.take_connection(TokioIo::new(stream), address).await { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 257 | tracing::debug!(%address, %error, "websocket connection ended"); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 258 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 259 | } else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 260 | tracing::debug!(%address, "websocket upgrade did not complete"); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 261 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 262 | }); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 263 | Ok(handshake.map(|()| Full::new(Bytes::new()))) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 264 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 265 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 266 | fn response(status: StatusCode, content_type: &str, body: &str) -> Response<Body> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 267 | let mut response = Response::new(Full::new(Bytes::copy_from_slice(body.as_bytes()))); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 268 | *response.status_mut() = status; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 269 | if let Ok(value) = HeaderValue::from_str(content_type) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 270 | response.headers_mut().insert(CONTENT_TYPE, value); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 271 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 272 | response |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 273 | .headers_mut() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 274 | .insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 275 | response |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 276 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 277 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 278 | fn information(config: &Config) -> String { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 279 | json!({ |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 280 | "name": "vineyard", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 281 | "description": "a persistent nostr relay for git forges", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 282 | "software": "vineyard", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 283 | "version": env!("CARGO_PKG_VERSION"), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 284 | "supported_nips": [1, 9, 11, 34, 65], |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 285 | "forge_pubkeys": config.forge_pubkeys, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 286 | "limitation": { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 287 | "max_message_length": config.limits.max_message_bytes, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 288 | "max_event_tags": config.limits.max_tags, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 289 | "max_subscriptions": config.limits.max_subscriptions, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 290 | "max_filters": config.limits.max_filters, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 291 | "max_limit": config.limits.max_query_results, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 292 | "max_subid_length": 64, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 293 | "auth_required": false, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 294 | "payment_required": false, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 295 | "restricted_writes": true |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 296 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 297 | }) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 298 | .to_string() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 299 | } |