Files
nostr-relay/src/handlers.rs
T
laoxong 6c42d5d654
Build and Push Docker Image / build_docker_image (push) Failing after 9m40s
Add: Split main.rs
2025-08-04 02:01:16 +08:00

258 lines
7.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use anyhow::{Result, anyhow};
use httparse::{EMPTY_HEADER, Request};
use log::{error, info};
use sqlx::SqlitePool;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::RwLock;
use tokio::sync::broadcast;
use crate::constants::SERVER_INFO;
use crate::models::ClientConnection;
use crate::ws_logic::handle_ws_connection;
// 处理多路复用连接(HTTP 或 WebSocket
pub async fn handle_connection_multiplex(
stream: TcpStream,
tx: broadcast::Sender<String>,
rx: broadcast::Receiver<String>,
pool: Arc<SqlitePool>,
clients: Arc<RwLock<HashMap<uuid::Uuid, Arc<RwLock<ClientConnection>>>>>,
trust_accounts: Arc<RwLock<HashSet<String>>>,
) -> Result<(), anyhow::Error> {
// 分配足够大的缓冲区,一次读完整个 HTTP 头
let mut buf = vec![0u8; 4096];
let n = stream.peek(&mut buf).await?;
let req_bytes = &buf[..n];
let mut headers: [httparse::Header; 32] = [EMPTY_HEADER; 32];
let mut req = Request::new(&mut headers);
let status = req.parse(req_bytes)?; // 解析 HTTP 请求
if status.is_partial() {
// 如果缓冲区不够大,没能完全解析请求头
anyhow::bail!("Request headers too large");
}
let method = req.method.unwrap_or("");
// 将请求头转换为 HashMap 便于查找
let header_map = req
.headers
.iter()
.filter_map(|h| {
let name = h.name.to_ascii_lowercase();
let val = std::str::from_utf8(h.value).ok()?;
Some((name, val))
})
.collect::<std::collections::HashMap<_, _>>();
// 判断是否是 WebSocket 升级请求
if method == "GET"
&& header_map
.get("upgrade")
.map(|&v| v.eq_ignore_ascii_case("websocket"))
.unwrap_or(false)
{
handle_ws_connection(stream, tx, rx, pool, clients, trust_accounts).await;
} else {
// 处理普通 HTTP 请求
if let Some(accept) = header_map.get("accept") {
if accept.contains("application/json") || accept.contains("application/nostr+json") {
handle_http_info(stream).await?; // 返回 Nostr Relay Info (NIP-11)
} else if accept.contains("text/html") {
handle_http_dashboard(stream).await?; // 返回 HTML 仪表盘
} else {
handle_http_info(stream).await?; // 默认返回信息
}
} else {
handle_http_info(stream).await?; // 默认返回信息
}
}
Ok(())
}
// 处理 HTTP 信息请求 (NIP-11)
pub async fn handle_http_info(mut stream: TcpStream) -> Result<(), anyhow::Error> {
// 读完请求体的剩余部分,保证连接可以关闭
let mut buffer = vec![0; 1024];
let _ = stream.read(&mut buffer).await?;
let json = serde_json::to_string(&*SERVER_INFO).expect("Failed to serialize server info");
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: application/nostr+json\r\n\
Access-Control-Allow-Origin: *\r\n\
Content-Length: {}\r\n\
\r\n\
{}",
json.len(),
json
);
stream.write_all(response.as_bytes()).await?;
stream.flush().await?;
Ok(())
}
// 处理 HTTP 仪表盘请求
pub async fn handle_http_dashboard(mut stream: TcpStream) -> Result<(), anyhow::Error> {
// 读完请求体的剩余部分,保证连接可以关闭
let mut buffer = vec![0; 1024];
let _ = stream.read(&mut buffer).await?;
// HTML 页面内容
let html = format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nostr Relay Dashboard</title>
<style>
:root {{
--bg: #fff;
--fg: #333;
--card: #f9f9f9;
--accent: #0052cc;
--accent-light: #e6ebff;
}}
[data-theme="dark"] {{
--bg: #1e1e1e;
--fg: #ddd;
--card: #2a2a2a;
--accent: #4e8cff;
--accent-light: #3a3f58;
}}
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{
background: var(--bg);
color: var(--fg);
font-family: "Segoe UI", Roboto, sans-serif;
line-height: 1.6;
padding: 20px;
transition: background 0.3s, color 0.3s;
}}
.toggle {{
position: fixed;
top: 20px; right: 20px;
background: var(--card);
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
transition: background 0.3s;
}}
.container {{
max-width: 600px;
margin: 60px auto 0;
background: var(--card);
border-radius: 8px;
padding: 30px;
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
transition: background 0.3s;
}}
h1 {{
font-size: 2rem;
margin-bottom: 10px;
display: flex;
align-items: center;
}}
h1 .rocket {{
font-size: 1.5rem;
margin-right: 8px;
}}
.badge {{
background: var(--accent);
color: #fff;
font-size: 0.8rem;
padding: 2px 8px;
border-radius: 12px;
margin-left: auto;
}}
.info {{
margin: 20px 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}}
.info p {{
background: var(--accent-light);
padding: 12px;
border-radius: 6px;
}}
.info p strong {{
display: block;
margin-bottom: 4px;
}}
.connect-btn {{
display: inline-block;
background: var(--accent);
color: #fff;
text-decoration: none;
padding: 10px 20px;
border-radius: 6px;
font-weight: bold;
transition: background 0.3s;
}}
.connect-btn:hover {{
background: #003ba1;
}}
@media (max-width: 480px) {{
.info {{
grid-template-columns: 1fr;
}}
.toggle {{
top: auto;
bottom: 20px;
}}
}}
</style>
</head>
<body data-theme="light">
<button class="toggle" onclick="toggleTheme()">切换 夜/日 间模式</button>
<div class="container">
<h1><span class="rocket">🚀</span>Nostr Relay Server<span class="badge">Ver.{}</span></h1>
<div class="info">
<p><strong>WebSocket URL</strong><br>wss://nostr-relay.moe.gift</p>
<p><strong>Status</strong><br>✅ Running</p>
</div>
<p>使用任意兼容 Nostr 协议的客户端连接到上面的 WebSocket 地址,即可发布和接收事件。</p>
</div>
<script>
function toggleTheme() {{
const html = document.body;
const next = html.getAttribute("data-theme") === "light" ? "dark" : "light";
html.setAttribute("data-theme", next);
localStorage.setItem("theme", next);
}}
// 页面加载时恢复上次主题
(function(){{
const saved = localStorage.getItem("theme");
if (saved) document.body.setAttribute("data-theme", saved);
}})();
</script>
</body>
</html>
"#,
env!("CARGO_PKG_VERSION")
);
let response = format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: text/html; charset=utf-8\r\n\
Content-Length: {}\r\n\
Access-Control-Allow-Origin: *\r\n\
\r\n\
{}",
html.len(),
html
);
stream.write_all(response.as_bytes()).await?;
stream.flush().await?;
Ok(())
}