Add: AUTH
This commit is contained in:
@@ -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
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user