Add: AUTH

This commit is contained in:
2025-06-29 01:06:21 +08:00
parent 4e7010c730
commit c912244bd7
2 changed files with 210 additions and 26 deletions
+2
View File
@@ -19,3 +19,5 @@ env_logger = "0.11.8"
uuid = { version = "1.6.2", features = ["v4"] }
sqlx = { version = "0.7.1", features = ["runtime-tokio-rustls", "sqlite"] }
httparse = "1.10.1"
toml-cfg = "0.2.0"
once_cell = "1.21.3"
+208 -26
View File
@@ -5,6 +5,7 @@ use hex;
use httparse::{EMPTY_HEADER, Request};
use log::{Level, debug, error, info, log_enabled};
use secp256k1::{Message as SecpMessage, PublicKey, Secp256k1, ecdsa::Signature};
use serde::de;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
@@ -14,6 +15,7 @@ use sqlx::Row;
use sqlx::query;
use sqlx::sqlite::SqlitePool;
use std::env;
use std::time::Duration;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use std::{collections::HashMap, sync::Arc};
@@ -25,6 +27,7 @@ use tokio::{
};
use tokio_tungstenite::{WebSocketStream, accept_async, tungstenite::protocol::Message};
use uuid::Uuid;
use once_cell::sync::Lazy;
const DEFAULT_BIND_ADDR: &str = "0.0.0.0:8080";
const DEFAULT_DB_PATH: &str = "nostr.db";
@@ -114,6 +117,7 @@ struct ServerInfo {
software: &'static str,
supported_nips: Vec<u32>,
version: &'static str,
auth_required: bool,
}
#[derive(Debug, Clone)]
@@ -122,10 +126,39 @@ struct Subscription {
}
struct ClientConnection {
id: Uuid,
connected_at: u64,
subscriptions: HashMap<String, Subscription>,
sender: mpsc::Sender<String>,
authenticated: bool,
pubkey: Option<String>,
}
static SERVER_INFO: Lazy<ServerInfo> = Lazy::new(|| {
ServerInfo {
contact: "https://www.moec.top/",
description: "Powered by laoXong.",
limitation: Limitation {
max_event_tags: 5000,
max_event_time_newer_than_now: 900,
max_event_time_older_than_now: 315576000,
max_filters: 100,
max_limit: 500,
max_message_length: 524288,
max_subid_length: 100,
max_subscriptions: 20,
min_prefix: 10,
},
name: "A rust nostr relay by laoXong",
pubkey: "63abd4f817e39cca4e6abb6e6cf3e133bb718cf8ec28b38c1645e84d7a6190c6",
software: "https://git.moe.gift/laoxong/nostr-relay",
supported_nips: vec![1, 2, 5, 65],
version: env!("CARGO_PKG_VERSION"),
auth_required: false,
}
});
#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();
@@ -158,7 +191,7 @@ async fn main() -> Result<()> {
}
async fn handle_connection_multiplex(
mut stream: TcpStream,
stream: TcpStream,
tx: broadcast::Sender<String>,
rx: broadcast::Receiver<String>,
pool: Arc<SqlitePool>,
@@ -213,28 +246,9 @@ async fn handle_connection_multiplex(
async fn handle_http_info(mut stream: TcpStream) -> Result<(), anyhow::Error> {
let mut buffer = vec![0; 1024];
stream.read(&mut buffer).await?;
let info = ServerInfo {
contact: "https://www.moec.top/",
description: "Powered by laoXong.",
limitation: Limitation {
max_event_tags: 5000,
max_event_time_newer_than_now: 900,
max_event_time_older_than_now: 315576000,
max_filters: 100,
max_limit: 500,
max_message_length: 524288,
max_subid_length: 100,
max_subscriptions: 20,
min_prefix: 10,
},
name: "A rust nostr relay by laoXong",
pubkey: "63abd4f817e39cca4e6abb6e6cf3e133bb718cf8ec28b38c1645e84d7a6190c6",
software: "https://git.moe.gift/laoxong/nostr-relay",
supported_nips: vec![1, 2, 5, 65],
version: env!("CARGO_PKG_VERSION"),
};
let json = serde_json::to_string(&info).expect("Failed to serialize server info");
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\
@@ -426,8 +440,12 @@ async fn handle_ws_connection(
let client_id = Uuid::new_v4();
let client_conn = Arc::new(RwLock::new(ClientConnection {
id: client_id.clone(),
connected_at: SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(),
subscriptions: HashMap::new(),
sender: client_tx.clone(),
authenticated: false,
pubkey: None,
}));
// 注册客户端
{
@@ -436,6 +454,11 @@ async fn handle_ws_connection(
}
info!("Client {} connected", client_id);
let auth_msg = format!("[\"AUTH\", \"{}\"]", client_id);
if let Err(e) = ws_sender.send(Message::Text(auth_msg.into())).await {
error!("Failed to send AUTH challenge: {}", e);
}
debug!("Sent AUTH challenge to client {}", client_id);
let client_conn_for_send = client_conn.clone();
let send_task = tokio::spawn(async move {
loop {
@@ -480,7 +503,7 @@ async fn handle_ws_connection(
debug!("Received text: {}", text);
let pool = pool.clone();
let client_conn_for_msg = client_conn.clone();
match handle_message(&text, &pool, &client_tx, &client_id, &client_conn_for_msg, &event_tx).await {
match handle_message(&text, &pool, &client_tx, &client_conn_for_msg, &event_tx).await {
Ok(_) => {
debug!("Message handled successfully");
}
@@ -506,7 +529,6 @@ async fn handle_message(
text: &str,
pool: &SqlitePool,
to_client_msg_tx: &mpsc::Sender<String>,
client_id: &Uuid,
client_conn: &Arc<RwLock<ClientConnection>>,
event_tx: &broadcast::Sender<String>,
) -> Result<(), anyhow::Error> {
@@ -526,6 +548,19 @@ async fn handle_message(
.ok_or_else(|| anyhow!("First element must be a string"))?
{
"REQ" => {
if SERVER_INFO.auth_required {
let conn = client_conn.read().await;
if !conn.authenticated {
RelayMessage::send_closed(
&conn.id.to_string(),
"auth-required:Authentication required".to_string(),
to_client_msg_tx,
)
.await;
debug!("Sending message to Client {}: not authenticated", conn.id);
return Ok(());
}
}
if arr.len() < 3 {
RelayMessage::send_notice(
"Not enough array elements".to_string(),
@@ -588,6 +623,19 @@ async fn handle_message(
}
"EVENT" => {
if !SERVER_INFO.auth_required {
let conn = client_conn.read().await;
if !conn.authenticated {
RelayMessage::send_closed(
&conn.id.to_string(),
"auth-required:Authentication required".to_string(),
to_client_msg_tx,
)
.await;
debug!("Sending message to Client {}: not authenticated", conn.id);
return Ok(());
}
}
let event: NostrEvent = serde_json::from_value(arr[1].clone())
.map_err(|e| anyhow!("Event parse error: {}", e))?;
debug!("EVENT received: {:?}", event);
@@ -625,6 +673,17 @@ async fn handle_message(
}
"CLOSE" => {
if SERVER_INFO.auth_required {
if !client_conn.read().await.authenticated {
RelayMessage::send_closed(
"AUTH",
"auth-required:Authentication required".to_string(),
to_client_msg_tx,
)
.await;
return Ok(());
}
}
let sub_id = arr
.get(1)
.and_then(Value::as_str)
@@ -641,7 +700,25 @@ async fn handle_message(
Ok(())
}
other => Err(anyhow!("Unknown command: {}", other)),
"AUTH" => {
if arr.len() < 2 {
RelayMessage::send_notice(
"AUTH message requires an event".to_string(),
to_client_msg_tx,
)
.await;
return Ok(());
}
let auth_event: NostrEvent = serde_json::from_value(arr[1].clone())
.map_err(|e| anyhow!("AUTH event parse error: {}", e))?;
auth_event.handle_auth_event(client_conn, to_client_msg_tx).await?;
Ok(())
}
other => {
return Err(anyhow!("Unknown command: {}", other));
}
}
}
@@ -755,12 +832,115 @@ impl NostrEvent {
.await?;
Ok(())
}
async fn handle_auth_event(
&self,
client_conn: &Arc<RwLock<ClientConnection>>,
to_client_msg_tx: &mpsc::Sender<String>,
) -> Result<(), anyhow::Error> {
// 验证认证事件
if self.kind != 22242 {
RelayMessage::send_ok(
&self,
false,
"AUTH event must be kind 22242".to_string(),
to_client_msg_tx,
)
.await;
return Ok(());
}
// 验证事件签名
if !self.verify() {
RelayMessage::send_notice(
"Invalid AUTH event signature".to_string(),
to_client_msg_tx,
)
.await;
return Ok(());
}
// 检查挑战
let mut relay_url = None;
let mut challenge = None;
for tag in &self.tags {
if tag.len() >= 2 {
match tag[0].as_str() {
"relay" => relay_url = Some(&tag[1]),
"challenge" => challenge = Some(&tag[1]),
_ => {}
}
}
}
let challenge = match challenge {
Some(c) => c,
None => {
RelayMessage::send_notice(
"AUTH event missing challenge tag".to_string(),
to_client_msg_tx,
)
.await;
return Ok(());
}
};
// 验证挑战是否匹配
let is_valid_challenge = {
let conn = client_conn.read().await;
if conn.id.to_string() == *challenge {
match SystemTime::now().duration_since(UNIX_EPOCH + Duration::from_secs(conn.connected_at)) {
Ok(elapsed) => elapsed <= Duration::from_secs(15 * 60),
Err(_) => false,
}
} else {
false
}
};
if !is_valid_challenge {
RelayMessage::send_notice(
"Invalid or expired challenge".to_string(),
to_client_msg_tx,
)
.await;
return Ok(());
}
// 验证时间戳
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
if self.created_at < now - 600 || self.created_at > now + 60 {
RelayMessage::send_notice(
"AUTH event timestamp out of acceptable range".to_string(),
to_client_msg_tx,
)
.await;
return Ok(());
}
// 认证成功,更新客户端状态
{
let mut conn = client_conn.write().await;
conn.authenticated = true;
conn.pubkey = Some(self.pubkey.clone());
}
info!("Client authenticated with pubkey: {}", self.pubkey);
RelayMessage::send_notice(
"Authentication successful".to_string(),
to_client_msg_tx,
)
.await;
Ok(())
}
}
impl Filter {
async fn select(&self, pool: &SqlitePool) -> Result<Vec<NostrEvent>, sqlx::Error> {
let mut sql = QueryBuilder::new(
"SELECT id, pubkey, created_at, kind, tags, content, sig FROM events WHERE 1=1",
"SELECT id, pubkey, created_at, kind, tags, content, sig FROM events WHERE kind != 22242",
);
// ID过滤
@@ -961,3 +1141,5 @@ impl RelayMessage {
}
}