258 lines
7.3 KiB
Rust
258 lines
7.3 KiB
Rust
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(())
|
||
}
|