atagen

vineyard

public
Star0
Watch0
25d14e49ab6861630869e2129d40652900bc5833
1
ssh://brewery@pls.lobotomise.me:33/atagen/vineyard.git

Blame: src/server.rs

25d14e49ab68atagen2026-09-24 11:07 UTC1
use std::collections::HashMap;
25d14e49ab68atagen2026-09-24 11:07 UTC2
use std::convert::Infallible;
25d14e49ab68atagen2026-09-24 11:07 UTC3
use std::net::{IpAddr, SocketAddr};
25d14e49ab68atagen2026-09-24 11:07 UTC4
use std::sync::{Arc, Mutex, PoisonError};
25d14e49ab68atagen2026-09-24 11:07 UTC5
use std::time::Duration;
25d14e49ab68atagen2026-09-24 11:07 UTC6
25d14e49ab68atagen2026-09-24 11:07 UTC7
use anyhow::Context as _;
25d14e49ab68atagen2026-09-24 11:07 UTC8
use http_body_util::Full;
25d14e49ab68atagen2026-09-24 11:07 UTC9
use hyper::body::{Bytes, Incoming};
25d14e49ab68atagen2026-09-24 11:07 UTC10
use hyper::header::{ACCEPT, ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE, HeaderValue, UPGRADE};
25d14e49ab68atagen2026-09-24 11:07 UTC11
use hyper::server::conn::http1::Builder as Http1Builder;
25d14e49ab68atagen2026-09-24 11:07 UTC12
use hyper::service::service_fn;
25d14e49ab68atagen2026-09-24 11:07 UTC13
use hyper::upgrade::on as upgrade;
25d14e49ab68atagen2026-09-24 11:07 UTC14
use hyper::{Method, Request, Response, StatusCode};
25d14e49ab68atagen2026-09-24 11:07 UTC15
use hyper_util::rt::{TokioIo, TokioTimer};
25d14e49ab68atagen2026-09-24 11:07 UTC16
use nostr_database::NostrDatabase;
25d14e49ab68atagen2026-09-24 11:07 UTC17
use serde_json::json;
25d14e49ab68atagen2026-09-24 11:07 UTC18
use tokio::net::TcpListener;
25d14e49ab68atagen2026-09-24 11:07 UTC19
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
25d14e49ab68atagen2026-09-24 11:07 UTC20
use tokio::task::{JoinHandle, spawn as spawn_task};
25d14e49ab68atagen2026-09-24 11:07 UTC21
use tokio::time::timeout;
25d14e49ab68atagen2026-09-24 11:07 UTC22
use tokio_tungstenite::tungstenite::handshake::server::create_response;
25d14e49ab68atagen2026-09-24 11:07 UTC23
use tokio_util::sync::CancellationToken;
25d14e49ab68atagen2026-09-24 11:07 UTC24
use tokio_util::task::TaskTracker;
25d14e49ab68atagen2026-09-24 11:07 UTC25
25d14e49ab68atagen2026-09-24 11:07 UTC26
use crate::config::Config;
25d14e49ab68atagen2026-09-24 11:07 UTC27
use crate::federation::spawn as spawn_federation;
25d14e49ab68atagen2026-09-24 11:07 UTC28
use crate::policy::Policy;
25d14e49ab68atagen2026-09-24 11:07 UTC29
use crate::relay::Relay;
25d14e49ab68atagen2026-09-24 11:07 UTC30
use crate::store::Store;
25d14e49ab68atagen2026-09-24 11:07 UTC31
25d14e49ab68atagen2026-09-24 11:07 UTC32
type Body = Full<Bytes>;
25d14e49ab68atagen2026-09-24 11:07 UTC33
25d14e49ab68atagen2026-09-24 11:07 UTC34
struct ConnectionLease {
25d14e49ab68atagen2026-09-24 11:07 UTC35
    ip: IpAddr,
25d14e49ab68atagen2026-09-24 11:07 UTC36
    peers: Arc<Mutex<HashMap<IpAddr, usize>>>,
25d14e49ab68atagen2026-09-24 11:07 UTC37
    _permit: OwnedSemaphorePermit,
25d14e49ab68atagen2026-09-24 11:07 UTC38
}
25d14e49ab68atagen2026-09-24 11:07 UTC39
25d14e49ab68atagen2026-09-24 11:07 UTC40
impl Drop for ConnectionLease {
25d14e49ab68atagen2026-09-24 11:07 UTC41
    fn drop(&mut self) {
25d14e49ab68atagen2026-09-24 11:07 UTC42
        let mut peers = self.peers.lock().unwrap_or_else(PoisonError::into_inner);
25d14e49ab68atagen2026-09-24 11:07 UTC43
        if let Some(count) = peers.get_mut(&self.ip) {
25d14e49ab68atagen2026-09-24 11:07 UTC44
            *count -= 1;
25d14e49ab68atagen2026-09-24 11:07 UTC45
            if *count == 0 {
25d14e49ab68atagen2026-09-24 11:07 UTC46
                peers.remove(&self.ip);
25d14e49ab68atagen2026-09-24 11:07 UTC47
            }
25d14e49ab68atagen2026-09-24 11:07 UTC48
        }
25d14e49ab68atagen2026-09-24 11:07 UTC49
    }
25d14e49ab68atagen2026-09-24 11:07 UTC50
}
25d14e49ab68atagen2026-09-24 11:07 UTC51
25d14e49ab68atagen2026-09-24 11:07 UTC52
#[derive(Clone)]
25d14e49ab68atagen2026-09-24 11:07 UTC53
struct HttpState {
25d14e49ab68atagen2026-09-24 11:07 UTC54
    relay: Relay,
25d14e49ab68atagen2026-09-24 11:07 UTC55
    store: Arc<Store>,
25d14e49ab68atagen2026-09-24 11:07 UTC56
    config: Arc<Config>,
25d14e49ab68atagen2026-09-24 11:07 UTC57
    websocket_path: String,
25d14e49ab68atagen2026-09-24 11:07 UTC58
    tasks: TaskTracker,
25d14e49ab68atagen2026-09-24 11:07 UTC59
    stop: CancellationToken,
25d14e49ab68atagen2026-09-24 11:07 UTC60
}
25d14e49ab68atagen2026-09-24 11:07 UTC61
25d14e49ab68atagen2026-09-24 11:07 UTC62
pub struct RunningRelay {
25d14e49ab68atagen2026-09-24 11:07 UTC63
    address: SocketAddr,
25d14e49ab68atagen2026-09-24 11:07 UTC64
    path: String,
25d14e49ab68atagen2026-09-24 11:07 UTC65
    state: HttpState,
25d14e49ab68atagen2026-09-24 11:07 UTC66
    listener: JoinHandle<anyhow::Result<()>>,
25d14e49ab68atagen2026-09-24 11:07 UTC67
}
25d14e49ab68atagen2026-09-24 11:07 UTC68
25d14e49ab68atagen2026-09-24 11:07 UTC69
impl RunningRelay {
25d14e49ab68atagen2026-09-24 11:07 UTC70
    #[must_use]
25d14e49ab68atagen2026-09-24 11:07 UTC71
    pub fn url(&self) -> String {
25d14e49ab68atagen2026-09-24 11:07 UTC72
        format!("ws://{}{}", self.address, self.path)
25d14e49ab68atagen2026-09-24 11:07 UTC73
    }
25d14e49ab68atagen2026-09-24 11:07 UTC74
25d14e49ab68atagen2026-09-24 11:07 UTC75
    pub async fn shutdown(self) -> anyhow::Result<()> {
25d14e49ab68atagen2026-09-24 11:07 UTC76
        self.state.stop.cancel();
25d14e49ab68atagen2026-09-24 11:07 UTC77
        self.listener.await.context("joining listener")??;
25d14e49ab68atagen2026-09-24 11:07 UTC78
        self.state.tasks.close();
25d14e49ab68atagen2026-09-24 11:07 UTC79
        timeout(Duration::from_secs(30), self.state.tasks.wait())
25d14e49ab68atagen2026-09-24 11:07 UTC80
            .await
25d14e49ab68atagen2026-09-24 11:07 UTC81
            .context("relay connections did not drain")?;
25d14e49ab68atagen2026-09-24 11:07 UTC82
        Ok(())
25d14e49ab68atagen2026-09-24 11:07 UTC83
    }
25d14e49ab68atagen2026-09-24 11:07 UTC84
}
25d14e49ab68atagen2026-09-24 11:07 UTC85
25d14e49ab68atagen2026-09-24 11:07 UTC86
pub async fn spawn(config: Config) -> anyhow::Result<RunningRelay> {
25d14e49ab68atagen2026-09-24 11:07 UTC87
    config.validate()?;
25d14e49ab68atagen2026-09-24 11:07 UTC88
    let store = Store::open(&config).await?;
25d14e49ab68atagen2026-09-24 11:07 UTC89
    let policy_database: Arc<dyn NostrDatabase> = Arc::<Store>::clone(&store);
25d14e49ab68atagen2026-09-24 11:07 UTC90
    let policy = Policy::new(policy_database, &config)?;
25d14e49ab68atagen2026-09-24 11:07 UTC91
    let listener = TcpListener::bind(config.listen)
25d14e49ab68atagen2026-09-24 11:07 UTC92
        .await
25d14e49ab68atagen2026-09-24 11:07 UTC93
        .context("binding relay listener")?;
25d14e49ab68atagen2026-09-24 11:07 UTC94
    let address = listener.local_addr()?;
25d14e49ab68atagen2026-09-24 11:07 UTC95
    let path = config.websocket_path()?;
25d14e49ab68atagen2026-09-24 11:07 UTC96
    let stop = CancellationToken::new();
25d14e49ab68atagen2026-09-24 11:07 UTC97
    let relay = Relay::new(
25d14e49ab68atagen2026-09-24 11:07 UTC98
        Arc::clone(&store),
25d14e49ab68atagen2026-09-24 11:07 UTC99
        policy.clone(),
25d14e49ab68atagen2026-09-24 11:07 UTC100
        config.limits.clone(),
25d14e49ab68atagen2026-09-24 11:07 UTC101
        stop.clone(),
25d14e49ab68atagen2026-09-24 11:07 UTC102
    );
25d14e49ab68atagen2026-09-24 11:07 UTC103
    let state = HttpState {
25d14e49ab68atagen2026-09-24 11:07 UTC104
        relay,
25d14e49ab68atagen2026-09-24 11:07 UTC105
        store,
25d14e49ab68atagen2026-09-24 11:07 UTC106
        config: Arc::new(config),
25d14e49ab68atagen2026-09-24 11:07 UTC107
        websocket_path: path.clone(),
25d14e49ab68atagen2026-09-24 11:07 UTC108
        tasks: TaskTracker::new(),
25d14e49ab68atagen2026-09-24 11:07 UTC109
        stop,
25d14e49ab68atagen2026-09-24 11:07 UTC110
    };
25d14e49ab68atagen2026-09-24 11:07 UTC111
    let task_state = state.clone();
25d14e49ab68atagen2026-09-24 11:07 UTC112
    spawn_federation(
25d14e49ab68atagen2026-09-24 11:07 UTC113
        Arc::clone(&state.config),
25d14e49ab68atagen2026-09-24 11:07 UTC114
        Arc::clone(&state.store),
25d14e49ab68atagen2026-09-24 11:07 UTC115
        state.relay.clone(),
25d14e49ab68atagen2026-09-24 11:07 UTC116
        policy,
25d14e49ab68atagen2026-09-24 11:07 UTC117
        &state.tasks,
25d14e49ab68atagen2026-09-24 11:07 UTC118
        state.stop.clone(),
25d14e49ab68atagen2026-09-24 11:07 UTC119
    );
25d14e49ab68atagen2026-09-24 11:07 UTC120
    let listener_task = spawn_task(async move { accept_connections(listener, task_state).await });
25d14e49ab68atagen2026-09-24 11:07 UTC121
    tracing::info!(%address, %path, "relay is listening");
25d14e49ab68atagen2026-09-24 11:07 UTC122
    Ok(RunningRelay {
25d14e49ab68atagen2026-09-24 11:07 UTC123
        address,
25d14e49ab68atagen2026-09-24 11:07 UTC124
        path,
25d14e49ab68atagen2026-09-24 11:07 UTC125
        state,
25d14e49ab68atagen2026-09-24 11:07 UTC126
        listener: listener_task,
25d14e49ab68atagen2026-09-24 11:07 UTC127
    })
25d14e49ab68atagen2026-09-24 11:07 UTC128
}
25d14e49ab68atagen2026-09-24 11:07 UTC129
25d14e49ab68atagen2026-09-24 11:07 UTC130
async fn accept_connections(listener: TcpListener, state: HttpState) -> anyhow::Result<()> {
25d14e49ab68atagen2026-09-24 11:07 UTC131
    let slots = Arc::new(Semaphore::new(state.config.limits.max_connections));
25d14e49ab68atagen2026-09-24 11:07 UTC132
    let peers = Arc::new(Mutex::new(HashMap::<IpAddr, usize>::new()));
25d14e49ab68atagen2026-09-24 11:07 UTC133
    loop {
25d14e49ab68atagen2026-09-24 11:07 UTC134
        let (stream, address) = tokio::select! {
25d14e49ab68atagen2026-09-24 11:07 UTC135
            biased;
25d14e49ab68atagen2026-09-24 11:07 UTC136
            () = state.stop.cancelled() => return Ok(()),
25d14e49ab68atagen2026-09-24 11:07 UTC137
            accepted = listener.accept() => accepted?,
25d14e49ab68atagen2026-09-24 11:07 UTC138
        };
25d14e49ab68atagen2026-09-24 11:07 UTC139
        let Ok(permit) = Arc::clone(&slots).try_acquire_owned() else {
25d14e49ab68atagen2026-09-24 11:07 UTC140
            continue;
25d14e49ab68atagen2026-09-24 11:07 UTC141
        };
25d14e49ab68atagen2026-09-24 11:07 UTC142
        let mut peer_counts = peers.lock().unwrap_or_else(PoisonError::into_inner);
25d14e49ab68atagen2026-09-24 11:07 UTC143
        let count = peer_counts.entry(address.ip()).or_default();
25d14e49ab68atagen2026-09-24 11:07 UTC144
        if *count >= state.config.limits.max_connections_per_ip {
25d14e49ab68atagen2026-09-24 11:07 UTC145
            continue;
25d14e49ab68atagen2026-09-24 11:07 UTC146
        }
25d14e49ab68atagen2026-09-24 11:07 UTC147
        *count += 1;
25d14e49ab68atagen2026-09-24 11:07 UTC148
        drop(peer_counts);
25d14e49ab68atagen2026-09-24 11:07 UTC149
        let lease = Arc::new(ConnectionLease {
25d14e49ab68atagen2026-09-24 11:07 UTC150
            ip: address.ip(),
25d14e49ab68atagen2026-09-24 11:07 UTC151
            peers: Arc::clone(&peers),
25d14e49ab68atagen2026-09-24 11:07 UTC152
            _permit: permit,
25d14e49ab68atagen2026-09-24 11:07 UTC153
        });
25d14e49ab68atagen2026-09-24 11:07 UTC154
        let handler_state = state.clone();
25d14e49ab68atagen2026-09-24 11:07 UTC155
        let stop = state.stop.clone();
25d14e49ab68atagen2026-09-24 11:07 UTC156
        state.tasks.spawn(async move {
25d14e49ab68atagen2026-09-24 11:07 UTC157
            let handler = service_fn(move |request| {
25d14e49ab68atagen2026-09-24 11:07 UTC158
                handle_request(
25d14e49ab68atagen2026-09-24 11:07 UTC159
                    request,
25d14e49ab68atagen2026-09-24 11:07 UTC160
                    address,
25d14e49ab68atagen2026-09-24 11:07 UTC161
                    handler_state.clone(),
25d14e49ab68atagen2026-09-24 11:07 UTC162
                    Arc::clone(&lease),
25d14e49ab68atagen2026-09-24 11:07 UTC163
                )
25d14e49ab68atagen2026-09-24 11:07 UTC164
            });
25d14e49ab68atagen2026-09-24 11:07 UTC165
            let mut builder = Http1Builder::new();
25d14e49ab68atagen2026-09-24 11:07 UTC166
            builder.timer(TokioTimer::new()).header_read_timeout(Duration::from_secs(10));
25d14e49ab68atagen2026-09-24 11:07 UTC167
            let connection = builder.serve_connection(TokioIo::new(stream), handler).with_upgrades();
25d14e49ab68atagen2026-09-24 11:07 UTC168
            tokio::pin!(connection);
25d14e49ab68atagen2026-09-24 11:07 UTC169
            tokio::select! {
25d14e49ab68atagen2026-09-24 11:07 UTC170
                result = &mut connection => {
25d14e49ab68atagen2026-09-24 11:07 UTC171
                    if let Err(error) = result { tracing::debug!(%address, %error, "http connection ended"); }
25d14e49ab68atagen2026-09-24 11:07 UTC172
                }
25d14e49ab68atagen2026-09-24 11:07 UTC173
                () = stop.cancelled() => {}
25d14e49ab68atagen2026-09-24 11:07 UTC174
            }
25d14e49ab68atagen2026-09-24 11:07 UTC175
        });
25d14e49ab68atagen2026-09-24 11:07 UTC176
    }
25d14e49ab68atagen2026-09-24 11:07 UTC177
}
25d14e49ab68atagen2026-09-24 11:07 UTC178
25d14e49ab68atagen2026-09-24 11:07 UTC179
async fn handle_request(
25d14e49ab68atagen2026-09-24 11:07 UTC180
    mut request: Request<Incoming>,
25d14e49ab68atagen2026-09-24 11:07 UTC181
    address: SocketAddr,
25d14e49ab68atagen2026-09-24 11:07 UTC182
    state: HttpState,
25d14e49ab68atagen2026-09-24 11:07 UTC183
    lease: Arc<ConnectionLease>,
25d14e49ab68atagen2026-09-24 11:07 UTC184
) -> Result<Response<Body>, Infallible> {
25d14e49ab68atagen2026-09-24 11:07 UTC185
    if state.stop.is_cancelled() {
25d14e49ab68atagen2026-09-24 11:07 UTC186
        return Ok(response(
25d14e49ab68atagen2026-09-24 11:07 UTC187
            StatusCode::SERVICE_UNAVAILABLE,
25d14e49ab68atagen2026-09-24 11:07 UTC188
            "text/plain",
25d14e49ab68atagen2026-09-24 11:07 UTC189
            "relay is stopping",
25d14e49ab68atagen2026-09-24 11:07 UTC190
        ));
25d14e49ab68atagen2026-09-24 11:07 UTC191
    }
25d14e49ab68atagen2026-09-24 11:07 UTC192
    if request.method() != Method::GET {
25d14e49ab68atagen2026-09-24 11:07 UTC193
        return Ok(response(
25d14e49ab68atagen2026-09-24 11:07 UTC194
            StatusCode::METHOD_NOT_ALLOWED,
25d14e49ab68atagen2026-09-24 11:07 UTC195
            "text/plain",
25d14e49ab68atagen2026-09-24 11:07 UTC196
            "GET required",
25d14e49ab68atagen2026-09-24 11:07 UTC197
        ));
25d14e49ab68atagen2026-09-24 11:07 UTC198
    }
25d14e49ab68atagen2026-09-24 11:07 UTC199
    if request.uri().path() == "/healthz" {
25d14e49ab68atagen2026-09-24 11:07 UTC200
        let healthy = timeout(Duration::from_secs(2), state.store.healthy())
25d14e49ab68atagen2026-09-24 11:07 UTC201
            .await
25d14e49ab68atagen2026-09-24 11:07 UTC202
            .unwrap_or(false);
25d14e49ab68atagen2026-09-24 11:07 UTC203
        return Ok(response(
25d14e49ab68atagen2026-09-24 11:07 UTC204
            if healthy {
25d14e49ab68atagen2026-09-24 11:07 UTC205
                StatusCode::OK
25d14e49ab68atagen2026-09-24 11:07 UTC206
            } else {
25d14e49ab68atagen2026-09-24 11:07 UTC207
                StatusCode::SERVICE_UNAVAILABLE
25d14e49ab68atagen2026-09-24 11:07 UTC208
            },
25d14e49ab68atagen2026-09-24 11:07 UTC209
            "text/plain",
25d14e49ab68atagen2026-09-24 11:07 UTC210
            if healthy {
25d14e49ab68atagen2026-09-24 11:07 UTC211
                "ok"
25d14e49ab68atagen2026-09-24 11:07 UTC212
            } else {
25d14e49ab68atagen2026-09-24 11:07 UTC213
                "database unavailable"
25d14e49ab68atagen2026-09-24 11:07 UTC214
            },
25d14e49ab68atagen2026-09-24 11:07 UTC215
        ));
25d14e49ab68atagen2026-09-24 11:07 UTC216
    }
25d14e49ab68atagen2026-09-24 11:07 UTC217
    if request.uri().path() != state.websocket_path {
25d14e49ab68atagen2026-09-24 11:07 UTC218
        return Ok(response(StatusCode::NOT_FOUND, "text/plain", "not found"));
25d14e49ab68atagen2026-09-24 11:07 UTC219
    }
25d14e49ab68atagen2026-09-24 11:07 UTC220
    if !request.headers().contains_key(UPGRADE) {
25d14e49ab68atagen2026-09-24 11:07 UTC221
        let wants_information = request
25d14e49ab68atagen2026-09-24 11:07 UTC222
            .headers()
25d14e49ab68atagen2026-09-24 11:07 UTC223
            .get(ACCEPT)
25d14e49ab68atagen2026-09-24 11:07 UTC224
            .and_then(|value| value.to_str().ok())
25d14e49ab68atagen2026-09-24 11:07 UTC225
            .is_some_and(|value| {
25d14e49ab68atagen2026-09-24 11:07 UTC226
                value.split(',').any(|media_type| {
25d14e49ab68atagen2026-09-24 11:07 UTC227
                    media_type.trim().split(';').next() == Some("application/nostr+json")
25d14e49ab68atagen2026-09-24 11:07 UTC228
                })
25d14e49ab68atagen2026-09-24 11:07 UTC229
            });
25d14e49ab68atagen2026-09-24 11:07 UTC230
        return Ok(if wants_information {
25d14e49ab68atagen2026-09-24 11:07 UTC231
            response(
25d14e49ab68atagen2026-09-24 11:07 UTC232
                StatusCode::OK,
25d14e49ab68atagen2026-09-24 11:07 UTC233
                "application/nostr+json",
25d14e49ab68atagen2026-09-24 11:07 UTC234
                &information(&state.config),
25d14e49ab68atagen2026-09-24 11:07 UTC235
            )
25d14e49ab68atagen2026-09-24 11:07 UTC236
        } else {
25d14e49ab68atagen2026-09-24 11:07 UTC237
            response(
25d14e49ab68atagen2026-09-24 11:07 UTC238
                StatusCode::OK,
25d14e49ab68atagen2026-09-24 11:07 UTC239
                "text/plain",
25d14e49ab68atagen2026-09-24 11:07 UTC240
                "vineyard\na persistent nostr relay for git forges\n",
25d14e49ab68atagen2026-09-24 11:07 UTC241
            )
25d14e49ab68atagen2026-09-24 11:07 UTC242
        });
25d14e49ab68atagen2026-09-24 11:07 UTC243
    }
25d14e49ab68atagen2026-09-24 11:07 UTC244
    let upgrade = upgrade(&mut request);
25d14e49ab68atagen2026-09-24 11:07 UTC245
    let Ok(handshake) = create_response(&request.map(|_body| ())) else {
25d14e49ab68atagen2026-09-24 11:07 UTC246
        return Ok(response(
25d14e49ab68atagen2026-09-24 11:07 UTC247
            StatusCode::BAD_REQUEST,
25d14e49ab68atagen2026-09-24 11:07 UTC248
            "text/plain",
25d14e49ab68atagen2026-09-24 11:07 UTC249
            "invalid WebSocket handshake",
25d14e49ab68atagen2026-09-24 11:07 UTC250
        ));
25d14e49ab68atagen2026-09-24 11:07 UTC251
    };
25d14e49ab68atagen2026-09-24 11:07 UTC252
    let relay = state.relay.clone();
25d14e49ab68atagen2026-09-24 11:07 UTC253
    state.tasks.spawn(async move {
25d14e49ab68atagen2026-09-24 11:07 UTC254
        let _lease = lease;
25d14e49ab68atagen2026-09-24 11:07 UTC255
        if let Ok(Ok(stream)) = timeout(Duration::from_secs(10), upgrade).await {
25d14e49ab68atagen2026-09-24 11:07 UTC256
            if let Err(error) = relay.take_connection(TokioIo::new(stream), address).await {
25d14e49ab68atagen2026-09-24 11:07 UTC257
                tracing::debug!(%address, %error, "websocket connection ended");
25d14e49ab68atagen2026-09-24 11:07 UTC258
            }
25d14e49ab68atagen2026-09-24 11:07 UTC259
        } else {
25d14e49ab68atagen2026-09-24 11:07 UTC260
            tracing::debug!(%address, "websocket upgrade did not complete");
25d14e49ab68atagen2026-09-24 11:07 UTC261
        }
25d14e49ab68atagen2026-09-24 11:07 UTC262
    });
25d14e49ab68atagen2026-09-24 11:07 UTC263
    Ok(handshake.map(|()| Full::new(Bytes::new())))
25d14e49ab68atagen2026-09-24 11:07 UTC264
}
25d14e49ab68atagen2026-09-24 11:07 UTC265
25d14e49ab68atagen2026-09-24 11:07 UTC266
fn response(status: StatusCode, content_type: &str, body: &str) -> Response<Body> {
25d14e49ab68atagen2026-09-24 11:07 UTC267
    let mut response = Response::new(Full::new(Bytes::copy_from_slice(body.as_bytes())));
25d14e49ab68atagen2026-09-24 11:07 UTC268
    *response.status_mut() = status;
25d14e49ab68atagen2026-09-24 11:07 UTC269
    if let Ok(value) = HeaderValue::from_str(content_type) {
25d14e49ab68atagen2026-09-24 11:07 UTC270
        response.headers_mut().insert(CONTENT_TYPE, value);
25d14e49ab68atagen2026-09-24 11:07 UTC271
    }
25d14e49ab68atagen2026-09-24 11:07 UTC272
    response
25d14e49ab68atagen2026-09-24 11:07 UTC273
        .headers_mut()
25d14e49ab68atagen2026-09-24 11:07 UTC274
        .insert(ACCESS_CONTROL_ALLOW_ORIGIN, HeaderValue::from_static("*"));
25d14e49ab68atagen2026-09-24 11:07 UTC275
    response
25d14e49ab68atagen2026-09-24 11:07 UTC276
}
25d14e49ab68atagen2026-09-24 11:07 UTC277
25d14e49ab68atagen2026-09-24 11:07 UTC278
fn information(config: &Config) -> String {
25d14e49ab68atagen2026-09-24 11:07 UTC279
    json!({
25d14e49ab68atagen2026-09-24 11:07 UTC280
        "name": "vineyard",
25d14e49ab68atagen2026-09-24 11:07 UTC281
        "description": "a persistent nostr relay for git forges",
25d14e49ab68atagen2026-09-24 11:07 UTC282
        "software": "vineyard",
25d14e49ab68atagen2026-09-24 11:07 UTC283
        "version": env!("CARGO_PKG_VERSION"),
25d14e49ab68atagen2026-09-24 11:07 UTC284
        "supported_nips": [1, 9, 11, 34, 65],
25d14e49ab68atagen2026-09-24 11:07 UTC285
        "forge_pubkeys": config.forge_pubkeys,
25d14e49ab68atagen2026-09-24 11:07 UTC286
        "limitation": {
25d14e49ab68atagen2026-09-24 11:07 UTC287
            "max_message_length": config.limits.max_message_bytes,
25d14e49ab68atagen2026-09-24 11:07 UTC288
            "max_event_tags": config.limits.max_tags,
25d14e49ab68atagen2026-09-24 11:07 UTC289
            "max_subscriptions": config.limits.max_subscriptions,
25d14e49ab68atagen2026-09-24 11:07 UTC290
            "max_filters": config.limits.max_filters,
25d14e49ab68atagen2026-09-24 11:07 UTC291
            "max_limit": config.limits.max_query_results,
25d14e49ab68atagen2026-09-24 11:07 UTC292
            "max_subid_length": 64,
25d14e49ab68atagen2026-09-24 11:07 UTC293
            "auth_required": false,
25d14e49ab68atagen2026-09-24 11:07 UTC294
            "payment_required": false,
25d14e49ab68atagen2026-09-24 11:07 UTC295
            "restricted_writes": true
25d14e49ab68atagen2026-09-24 11:07 UTC296
        }
25d14e49ab68atagen2026-09-24 11:07 UTC297
    })
25d14e49ab68atagen2026-09-24 11:07 UTC298
    .to_string()
25d14e49ab68atagen2026-09-24 11:07 UTC299
}