diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-01 14:31:32 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2025-12-01 15:22:38 +0000 |
| commit | d2ac69816567f092fe0d4661723bc43778cb481b (patch) | |
| tree | e8b51b61a6a7b0ab1a214adebe4e237143b01f0b /src | |
| parent | 7a78815e29b01c83f3d0ec195ba717a2eba8cd37 (diff) | |
fix cargo clippy and fmt warnings
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.rs | 8 | ||||
| -rw-r--r-- | src/git/handlers.rs | 1 | ||||
| -rw-r--r-- | src/git/mod.rs | 13 | ||||
| -rw-r--r-- | src/git/protocol.rs | 18 | ||||
| -rw-r--r-- | src/git/subprocess.rs | 34 | ||||
| -rw-r--r-- | src/http/mod.rs | 125 | ||||
| -rw-r--r-- | src/http/nip11.rs | 34 | ||||
| -rw-r--r-- | src/nostr/builder.rs | 14 | ||||
| -rw-r--r-- | src/nostr/events.rs | 18 |
9 files changed, 140 insertions, 125 deletions
diff --git a/src/config.rs b/src/config.rs index f04b7d8..9b0d0b8 100644 --- a/src/config.rs +++ b/src/config.rs | |||
| @@ -5,8 +5,10 @@ use std::env; | |||
| 5 | /// Database backend type for the relay | 5 | /// Database backend type for the relay |
| 6 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] | 6 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] |
| 7 | #[serde(rename_all = "lowercase")] | 7 | #[serde(rename_all = "lowercase")] |
| 8 | #[derive(Default)] | ||
| 8 | pub enum DatabaseBackend { | 9 | pub enum DatabaseBackend { |
| 9 | /// In-memory database (default, fastest, no persistence) | 10 | /// In-memory database (default, fastest, no persistence) |
| 11 | #[default] | ||
| 10 | Memory, | 12 | Memory, |
| 11 | /// NostrDB backend (persistent, optimized for Nostr) | 13 | /// NostrDB backend (persistent, optimized for Nostr) |
| 12 | NostrDb, | 14 | NostrDb, |
| @@ -14,12 +16,6 @@ pub enum DatabaseBackend { | |||
| 14 | Lmdb, | 16 | Lmdb, |
| 15 | } | 17 | } |
| 16 | 18 | ||
| 17 | impl Default for DatabaseBackend { | ||
| 18 | fn default() -> Self { | ||
| 19 | Self::Memory | ||
| 20 | } | ||
| 21 | } | ||
| 22 | |||
| 23 | impl std::str::FromStr for DatabaseBackend { | 19 | impl std::str::FromStr for DatabaseBackend { |
| 24 | type Err = anyhow::Error; | 20 | type Err = anyhow::Error; |
| 25 | 21 | ||
diff --git a/src/git/handlers.rs b/src/git/handlers.rs index 00f2449..e84cabb 100644 --- a/src/git/handlers.rs +++ b/src/git/handlers.rs | |||
| @@ -5,7 +5,6 @@ | |||
| 5 | use http_body_util::Full; | 5 | use http_body_util::Full; |
| 6 | use hyper::{body::Bytes, Response, StatusCode}; | 6 | use hyper::{body::Bytes, Response, StatusCode}; |
| 7 | use nostr_relay_builder::prelude::MemoryDatabase; | 7 | use nostr_relay_builder::prelude::MemoryDatabase; |
| 8 | use nostr_sdk::EventId; | ||
| 9 | use std::path::PathBuf; | 8 | use std::path::PathBuf; |
| 10 | use std::sync::Arc; | 9 | use std::sync::Arc; |
| 11 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; | 10 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; |
diff --git a/src/git/mod.rs b/src/git/mod.rs index 494f8b9..a783782 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs | |||
| @@ -306,11 +306,7 @@ pub fn parse_git_url(path: &str) -> Option<(&str, &str, &str)> { | |||
| 306 | let subpath = parts[2]; | 306 | let subpath = parts[2]; |
| 307 | 307 | ||
| 308 | // Extract identifier (remove .git suffix if present for the middle part) | 308 | // Extract identifier (remove .git suffix if present for the middle part) |
| 309 | let identifier = if repo_part.ends_with(".git") { | 309 | let identifier = repo_part.strip_suffix(".git").unwrap_or(repo_part); |
| 310 | &repo_part[..repo_part.len() - 4] | ||
| 311 | } else { | ||
| 312 | repo_part | ||
| 313 | }; | ||
| 314 | 310 | ||
| 315 | Some((npub, identifier, subpath)) | 311 | Some((npub, identifier, subpath)) |
| 316 | } | 312 | } |
| @@ -343,7 +339,12 @@ mod tests { | |||
| 343 | 339 | ||
| 344 | // Initialize bare repository | 340 | // Initialize bare repository |
| 345 | Command::new("git") | 341 | Command::new("git") |
| 346 | .args(["init", "--bare", "--initial-branch=main", bare_repo.to_str().unwrap()]) | 342 | .args([ |
| 343 | "init", | ||
| 344 | "--bare", | ||
| 345 | "--initial-branch=main", | ||
| 346 | bare_repo.to_str().unwrap(), | ||
| 347 | ]) | ||
| 347 | .output() | 348 | .output() |
| 348 | .unwrap(); | 349 | .unwrap(); |
| 349 | 350 | ||
diff --git a/src/git/protocol.rs b/src/git/protocol.rs index 93177de..8592c27 100644 --- a/src/git/protocol.rs +++ b/src/git/protocol.rs | |||
| @@ -55,11 +55,11 @@ impl PktLine { | |||
| 55 | return Err(ProtocolError::InsufficientData); | 55 | return Err(ProtocolError::InsufficientData); |
| 56 | } | 56 | } |
| 57 | 57 | ||
| 58 | let len_str = std::str::from_utf8(&input[0..4]) | 58 | let len_str = |
| 59 | .map_err(|_| ProtocolError::InvalidLength)?; | 59 | std::str::from_utf8(&input[0..4]).map_err(|_| ProtocolError::InvalidLength)?; |
| 60 | 60 | ||
| 61 | let len = u16::from_str_radix(len_str, 16) | 61 | let len = |
| 62 | .map_err(|_| ProtocolError::InvalidLength)? as usize; | 62 | u16::from_str_radix(len_str, 16).map_err(|_| ProtocolError::InvalidLength)? as usize; |
| 63 | 63 | ||
| 64 | if len == 0 { | 64 | if len == 0 { |
| 65 | // Flush packet | 65 | // Flush packet |
| @@ -81,19 +81,19 @@ impl PktLine { | |||
| 81 | /// Parse all pkt-lines from bytes | 81 | /// Parse all pkt-lines from bytes |
| 82 | pub fn parse_all(mut input: &[u8]) -> Result<Vec<Self>, ProtocolError> { | 82 | pub fn parse_all(mut input: &[u8]) -> Result<Vec<Self>, ProtocolError> { |
| 83 | let mut packets = Vec::new(); | 83 | let mut packets = Vec::new(); |
| 84 | 84 | ||
| 85 | while !input.is_empty() { | 85 | while !input.is_empty() { |
| 86 | let (packet, remaining) = Self::parse(input)?; | 86 | let (packet, remaining) = Self::parse(input)?; |
| 87 | let is_flush = matches!(packet, PktLine::Flush); | 87 | let is_flush = matches!(packet, PktLine::Flush); |
| 88 | packets.push(packet); | 88 | packets.push(packet); |
| 89 | input = remaining; | 89 | input = remaining; |
| 90 | 90 | ||
| 91 | // Stop at flush packet | 91 | // Stop at flush packet |
| 92 | if is_flush { | 92 | if is_flush { |
| 93 | break; | 93 | break; |
| 94 | } | 94 | } |
| 95 | } | 95 | } |
| 96 | 96 | ||
| 97 | Ok(packets) | 97 | Ok(packets) |
| 98 | } | 98 | } |
| 99 | } | 99 | } |
| @@ -259,4 +259,4 @@ mod tests { | |||
| 259 | "application/x-git-upload-pack-result" | 259 | "application/x-git-upload-pack-result" |
| 260 | ); | 260 | ); |
| 261 | } | 261 | } |
| 262 | } \ No newline at end of file | 262 | } |
diff --git a/src/git/subprocess.rs b/src/git/subprocess.rs index c95bce5..2d9a981 100644 --- a/src/git/subprocess.rs +++ b/src/git/subprocess.rs | |||
| @@ -28,9 +28,9 @@ impl GitSubprocess { | |||
| 28 | advertise: bool, | 28 | advertise: bool, |
| 29 | ) -> std::io::Result<Self> { | 29 | ) -> std::io::Result<Self> { |
| 30 | let repo_path = repo_path.as_ref(); | 30 | let repo_path = repo_path.as_ref(); |
| 31 | 31 | ||
| 32 | let mut cmd = Command::new("git"); | 32 | let mut cmd = Command::new("git"); |
| 33 | 33 | ||
| 34 | // GRASP-01 requirement: MUST include `allow-reachable-sha1-in-want` and | 34 | // GRASP-01 requirement: MUST include `allow-reachable-sha1-in-want` and |
| 35 | // `allow-tip-sha1-in-want` in advertisement and serve available oids. | 35 | // `allow-tip-sha1-in-want` in advertisement and serve available oids. |
| 36 | // These config options must be passed before the command name. | 36 | // These config options must be passed before the command name. |
| @@ -38,22 +38,22 @@ impl GitSubprocess { | |||
| 38 | cmd.arg("uploadpack.allowReachableSHA1InWant=true"); | 38 | cmd.arg("uploadpack.allowReachableSHA1InWant=true"); |
| 39 | cmd.arg("-c"); | 39 | cmd.arg("-c"); |
| 40 | cmd.arg("uploadpack.allowTipSHA1InWant=true"); | 40 | cmd.arg("uploadpack.allowTipSHA1InWant=true"); |
| 41 | 41 | ||
| 42 | cmd.arg(service.command_name()); | 42 | cmd.arg(service.command_name()); |
| 43 | 43 | ||
| 44 | if advertise { | 44 | if advertise { |
| 45 | cmd.arg("--advertise-refs"); | 45 | cmd.arg("--advertise-refs"); |
| 46 | } | 46 | } |
| 47 | 47 | ||
| 48 | cmd.arg("--stateless-rpc"); | 48 | cmd.arg("--stateless-rpc"); |
| 49 | cmd.arg(repo_path); | 49 | cmd.arg(repo_path); |
| 50 | 50 | ||
| 51 | cmd.stdin(Stdio::piped()); | 51 | cmd.stdin(Stdio::piped()); |
| 52 | cmd.stdout(Stdio::piped()); | 52 | cmd.stdout(Stdio::piped()); |
| 53 | cmd.stderr(Stdio::piped()); | 53 | cmd.stderr(Stdio::piped()); |
| 54 | 54 | ||
| 55 | let child = cmd.spawn()?; | 55 | let child = cmd.spawn()?; |
| 56 | 56 | ||
| 57 | Ok(Self { child }) | 57 | Ok(Self { child }) |
| 58 | } | 58 | } |
| 59 | 59 | ||
| @@ -101,8 +101,8 @@ impl GitSubprocess { | |||
| 101 | #[cfg(test)] | 101 | #[cfg(test)] |
| 102 | mod tests { | 102 | mod tests { |
| 103 | use super::*; | 103 | use super::*; |
| 104 | use tempfile::TempDir; | ||
| 105 | use std::process::Command as StdCommand; | 104 | use std::process::Command as StdCommand; |
| 105 | use tempfile::TempDir; | ||
| 106 | 106 | ||
| 107 | fn create_bare_repo() -> TempDir { | 107 | fn create_bare_repo() -> TempDir { |
| 108 | let dir = TempDir::new().unwrap(); | 108 | let dir = TempDir::new().unwrap(); |
| @@ -118,11 +118,8 @@ mod tests { | |||
| 118 | #[tokio::test] | 118 | #[tokio::test] |
| 119 | async fn test_spawn_upload_pack_advertise() { | 119 | async fn test_spawn_upload_pack_advertise() { |
| 120 | let repo = create_bare_repo(); | 120 | let repo = create_bare_repo(); |
| 121 | let mut proc = GitSubprocess::spawn( | 121 | let mut proc = GitSubprocess::spawn(GitService::UploadPack, repo.path(), true) |
| 122 | GitService::UploadPack, | 122 | .expect("Failed to spawn git"); |
| 123 | repo.path(), | ||
| 124 | true, | ||
| 125 | ).expect("Failed to spawn git"); | ||
| 126 | 123 | ||
| 127 | // Should have spawned successfully | 124 | // Should have spawned successfully |
| 128 | assert!(proc.stdout().is_some()); | 125 | assert!(proc.stdout().is_some()); |
| @@ -135,15 +132,12 @@ mod tests { | |||
| 135 | #[tokio::test] | 132 | #[tokio::test] |
| 136 | async fn test_spawn_receive_pack() { | 133 | async fn test_spawn_receive_pack() { |
| 137 | let repo = create_bare_repo(); | 134 | let repo = create_bare_repo(); |
| 138 | let mut proc = GitSubprocess::spawn( | 135 | let mut proc = GitSubprocess::spawn(GitService::ReceivePack, repo.path(), false) |
| 139 | GitService::ReceivePack, | 136 | .expect("Failed to spawn git"); |
| 140 | repo.path(), | ||
| 141 | false, | ||
| 142 | ).expect("Failed to spawn git"); | ||
| 143 | 137 | ||
| 144 | assert!(proc.stdout().is_some()); | 138 | assert!(proc.stdout().is_some()); |
| 145 | assert!(proc.stdin().is_some()); | 139 | assert!(proc.stdin().is_some()); |
| 146 | 140 | ||
| 147 | let _ = proc.kill().await; | 141 | let _ = proc.kill().await; |
| 148 | } | 142 | } |
| 149 | } \ No newline at end of file | 143 | } |
diff --git a/src/http/mod.rs b/src/http/mod.rs index 07b47ee..f43cf86 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs | |||
| @@ -9,20 +9,20 @@ use std::net::SocketAddr; | |||
| 9 | use std::pin::Pin; | 9 | use std::pin::Pin; |
| 10 | use std::sync::Arc; | 10 | use std::sync::Arc; |
| 11 | 11 | ||
| 12 | use base64::Engine; | ||
| 13 | use http_body_util::{BodyExt, Full}; | ||
| 12 | use hyper::body::{Bytes, Incoming}; | 14 | use hyper::body::{Bytes, Incoming}; |
| 13 | use hyper::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, UPGRADE}; | 15 | use hyper::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, UPGRADE}; |
| 14 | use hyper::server::conn::http1; | 16 | use hyper::server::conn::http1; |
| 15 | use hyper::service::Service; | 17 | use hyper::service::Service; |
| 16 | use hyper::{Method, Request, Response}; | 18 | use hyper::{Method, Request, Response}; |
| 17 | use hyper_util::rt::TokioIo; | 19 | use hyper_util::rt::TokioIo; |
| 18 | use http_body_util::{BodyExt, Full}; | 20 | use nostr_relay_builder::prelude::MemoryDatabase; |
| 21 | use nostr_relay_builder::LocalRelay; | ||
| 19 | use nostr_sdk::hashes::sha1::Hash as Sha1Hash; | 22 | use nostr_sdk::hashes::sha1::Hash as Sha1Hash; |
| 20 | use nostr_sdk::hashes::{Hash, HashEngine}; | 23 | use nostr_sdk::hashes::{Hash, HashEngine}; |
| 21 | use nostr_sdk::PublicKey; | 24 | use nostr_sdk::PublicKey; |
| 22 | use nostr_relay_builder::prelude::MemoryDatabase; | ||
| 23 | use nostr_relay_builder::LocalRelay; | ||
| 24 | use tokio::net::TcpListener; | 25 | use tokio::net::TcpListener; |
| 25 | use base64::Engine; | ||
| 26 | 26 | ||
| 27 | use crate::config::Config; | 27 | use crate::config::Config; |
| 28 | use crate::git; | 28 | use crate::git; |
| @@ -50,7 +50,12 @@ struct HttpService { | |||
| 50 | } | 50 | } |
| 51 | 51 | ||
| 52 | impl HttpService { | 52 | impl HttpService { |
| 53 | fn new(relay: LocalRelay, config: Config, remote: SocketAddr, database: Arc<MemoryDatabase>) -> Self { | 53 | fn new( |
| 54 | relay: LocalRelay, | ||
| 55 | config: Config, | ||
| 56 | remote: SocketAddr, | ||
| 57 | database: Arc<MemoryDatabase>, | ||
| 58 | ) -> Self { | ||
| 54 | Self { | 59 | Self { |
| 55 | relay, | 60 | relay, |
| 56 | config, | 61 | config, |
| @@ -77,10 +82,12 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 77 | // GRASP-01 spec line 47: Respond to OPTIONS with 204 No Content | 82 | // GRASP-01 spec line 47: Respond to OPTIONS with 204 No Content |
| 78 | if method == Method::OPTIONS { | 83 | if method == Method::OPTIONS { |
| 79 | return Box::pin(async move { | 84 | return Box::pin(async move { |
| 80 | Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp")) | 85 | Ok( |
| 81 | .status(204) | 86 | add_cors_headers(Response::builder().header("server", "ngit-grasp")) |
| 82 | .body(Full::new(Bytes::new())) | 87 | .status(204) |
| 83 | .unwrap()) | 88 | .body(Full::new(Bytes::new())) |
| 89 | .unwrap(), | ||
| 90 | ) | ||
| 84 | }); | 91 | }); |
| 85 | } | 92 | } |
| 86 | 93 | ||
| @@ -89,41 +96,47 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 89 | let npub = npub.to_string(); | 96 | let npub = npub.to_string(); |
| 90 | let identifier = identifier.to_string(); | 97 | let identifier = identifier.to_string(); |
| 91 | let subpath = subpath.to_string(); | 98 | let subpath = subpath.to_string(); |
| 92 | 99 | ||
| 93 | tracing::debug!("Git request: {} {} (npub={}, id={}, subpath={})", | 100 | tracing::debug!( |
| 94 | method, path, npub, identifier, subpath); | 101 | "Git request: {} {} (npub={}, id={}, subpath={})", |
| 102 | method, | ||
| 103 | path, | ||
| 104 | npub, | ||
| 105 | identifier, | ||
| 106 | subpath | ||
| 107 | ); | ||
| 95 | 108 | ||
| 96 | let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); | 109 | let repo_path = git::resolve_repo_path(&git_data_path, &npub, &identifier); |
| 97 | 110 | ||
| 98 | return Box::pin(async move { | 111 | return Box::pin(async move { |
| 99 | // Collect request body once before the match statement | 112 | // Collect request body once before the match statement |
| 100 | let body_bytes = req.collect().await | 113 | let body_bytes = req |
| 114 | .collect() | ||
| 115 | .await | ||
| 101 | .map(|collected| collected.to_bytes()) | 116 | .map(|collected| collected.to_bytes()) |
| 102 | .unwrap_or_else(|_| Bytes::new()); | 117 | .unwrap_or_else(|_| Bytes::new()); |
| 103 | 118 | ||
| 104 | let result = match (method.as_ref(), subpath.as_str()) { | 119 | let result = match (method.as_ref(), subpath.as_str()) { |
| 105 | // GET /info/refs?service=git-upload-pack or git-receive-pack | 120 | // GET /info/refs?service=git-upload-pack or git-receive-pack |
| 106 | (m, sp) if m == Method::GET && sp.starts_with("info/refs") => { | 121 | (m, sp) if m == Method::GET && sp.starts_with("info/refs") => { |
| 107 | // Parse query string for service parameter | 122 | // Parse query string for service parameter |
| 108 | let service = query.as_deref().unwrap_or("") | 123 | let service = query |
| 124 | .as_deref() | ||
| 125 | .unwrap_or("") | ||
| 109 | .strip_prefix("service=") | 126 | .strip_prefix("service=") |
| 110 | .and_then(git::protocol::GitService::from_query_param); | 127 | .and_then(git::protocol::GitService::from_query_param); |
| 111 | 128 | ||
| 112 | match service { | 129 | match service { |
| 113 | Some(svc) => { | 130 | Some(svc) => git::handlers::handle_info_refs(repo_path, svc).await, |
| 114 | git::handlers::handle_info_refs(repo_path, svc).await | 131 | None => Err(git::handlers::GitError::RepositoryNotFound), |
| 115 | } | ||
| 116 | None => { | ||
| 117 | Err(git::handlers::GitError::RepositoryNotFound) | ||
| 118 | } | ||
| 119 | } | 132 | } |
| 120 | } | 133 | } |
| 121 | 134 | ||
| 122 | // POST /git-upload-pack (clone/fetch) | 135 | // POST /git-upload-pack (clone/fetch) |
| 123 | (m, "git-upload-pack") if m == Method::POST => { | 136 | (m, "git-upload-pack") if m == Method::POST => { |
| 124 | git::handlers::handle_upload_pack(repo_path, body_bytes).await | 137 | git::handlers::handle_upload_pack(repo_path, body_bytes).await |
| 125 | } | 138 | } |
| 126 | 139 | ||
| 127 | // POST /git-receive-pack (push) - with GRASP authorization via database | 140 | // POST /git-receive-pack (push) - with GRASP authorization via database |
| 128 | (m, "git-receive-pack") if m == Method::POST => { | 141 | (m, "git-receive-pack") if m == Method::POST => { |
| 129 | // Convert npub (bech32) to hex pubkey for authorization | 142 | // Convert npub (bech32) to hex pubkey for authorization |
| @@ -137,33 +150,41 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 137 | .unwrap()); | 150 | .unwrap()); |
| 138 | } | 151 | } |
| 139 | }; | 152 | }; |
| 140 | 153 | ||
| 141 | git::handlers::handle_receive_pack( | 154 | git::handlers::handle_receive_pack( |
| 142 | repo_path, | 155 | repo_path, |
| 143 | body_bytes.clone(), | 156 | body_bytes.clone(), |
| 144 | Some(database.clone()), | 157 | Some(database.clone()), |
| 145 | &identifier, | 158 | &identifier, |
| 146 | &owner_pubkey_hex, | 159 | &owner_pubkey_hex, |
| 147 | ).await | 160 | ) |
| 148 | } | 161 | .await |
| 149 | |||
| 150 | _ => { | ||
| 151 | Err(git::handlers::GitError::RepositoryNotFound) | ||
| 152 | } | 162 | } |
| 163 | |||
| 164 | _ => Err(git::handlers::GitError::RepositoryNotFound), | ||
| 153 | }; | 165 | }; |
| 154 | 166 | ||
| 155 | match result { | 167 | match result { |
| 156 | Ok(response) => { | 168 | Ok(response) => { |
| 157 | // Add CORS headers to successful Git responses | 169 | // Add CORS headers to successful Git responses |
| 158 | let (parts, body) = response.into_parts(); | 170 | let (parts, body) = response.into_parts(); |
| 159 | Ok(add_cors_headers(Response::builder() | 171 | Ok(add_cors_headers(Response::builder().status(parts.status)) |
| 160 | .status(parts.status)) | 172 | .header( |
| 161 | .header("content-type", parts.headers.get("content-type") | 173 | "content-type", |
| 162 | .and_then(|v| v.to_str().ok()) | 174 | parts |
| 163 | .unwrap_or("application/octet-stream")) | 175 | .headers |
| 164 | .header("cache-control", parts.headers.get("cache-control") | 176 | .get("content-type") |
| 165 | .and_then(|v| v.to_str().ok()) | 177 | .and_then(|v| v.to_str().ok()) |
| 166 | .unwrap_or("no-cache")) | 178 | .unwrap_or("application/octet-stream"), |
| 179 | ) | ||
| 180 | .header( | ||
| 181 | "cache-control", | ||
| 182 | parts | ||
| 183 | .headers | ||
| 184 | .get("cache-control") | ||
| 185 | .and_then(|v| v.to_str().ok()) | ||
| 186 | .unwrap_or("no-cache"), | ||
| 187 | ) | ||
| 167 | .body(body) | 188 | .body(body) |
| 168 | .unwrap()) | 189 | .unwrap()) |
| 169 | } | 190 | } |
| @@ -191,15 +212,20 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 191 | tracing::error!("Failed to serialize NIP-11 document: {}", e); | 212 | tracing::error!("Failed to serialize NIP-11 document: {}", e); |
| 192 | "{}".to_string() | 213 | "{}".to_string() |
| 193 | }); | 214 | }); |
| 194 | 215 | ||
| 195 | tracing::debug!("Serving NIP-11 relay information document to {}", self.remote); | 216 | tracing::debug!( |
| 196 | 217 | "Serving NIP-11 relay information document to {}", | |
| 218 | self.remote | ||
| 219 | ); | ||
| 220 | |||
| 197 | return Box::pin(async move { | 221 | return Box::pin(async move { |
| 198 | Ok(add_cors_headers(Response::builder().header("server", "ngit-grasp")) | 222 | Ok( |
| 199 | .status(200) | 223 | add_cors_headers(Response::builder().header("server", "ngit-grasp")) |
| 200 | .header("content-type", "application/nostr+json") | 224 | .status(200) |
| 201 | .body(Full::new(Bytes::from(json))) | 225 | .header("content-type", "application/nostr+json") |
| 202 | .unwrap()) | 226 | .body(Full::new(Bytes::from(json))) |
| 227 | .unwrap(), | ||
| 228 | ) | ||
| 203 | }); | 229 | }); |
| 204 | } | 230 | } |
| 205 | } | 231 | } |
| @@ -221,12 +247,13 @@ impl Service<Request<Incoming>> for HttpService { | |||
| 221 | 247 | ||
| 222 | let addr = self.remote; | 248 | let addr = self.remote; |
| 223 | let relay = self.relay.clone(); | 249 | let relay = self.relay.clone(); |
| 224 | 250 | ||
| 225 | tokio::spawn(async move { | 251 | tokio::spawn(async move { |
| 226 | match hyper::upgrade::on(req).await { | 252 | match hyper::upgrade::on(req).await { |
| 227 | Ok(upgraded) => { | 253 | Ok(upgraded) => { |
| 228 | tracing::info!("WebSocket connection established from {}", addr); | 254 | tracing::info!("WebSocket connection established from {}", addr); |
| 229 | if let Err(e) = relay.take_connection(TokioIo::new(upgraded), addr).await | 255 | if let Err(e) = |
| 256 | relay.take_connection(TokioIo::new(upgraded), addr).await | ||
| 230 | { | 257 | { |
| 231 | tracing::error!("Relay error for {}: {}", addr, e); | 258 | tracing::error!("Relay error for {}: {}", addr, e); |
| 232 | } | 259 | } |
| @@ -288,12 +315,12 @@ pub async fn run_server( | |||
| 288 | tracing::info!("Domain: {}", config.domain); | 315 | tracing::info!("Domain: {}", config.domain); |
| 289 | 316 | ||
| 290 | let listener = TcpListener::bind(&bind_addr).await?; | 317 | let listener = TcpListener::bind(&bind_addr).await?; |
| 291 | 318 | ||
| 292 | loop { | 319 | loop { |
| 293 | let (socket, addr) = listener.accept().await?; | 320 | let (socket, addr) = listener.accept().await?; |
| 294 | let io = TokioIo::new(socket); | 321 | let io = TokioIo::new(socket); |
| 295 | let service = HttpService::new(relay.clone(), config.clone(), addr, database.clone()); | 322 | let service = HttpService::new(relay.clone(), config.clone(), addr, database.clone()); |
| 296 | 323 | ||
| 297 | tokio::spawn(async move { | 324 | tokio::spawn(async move { |
| 298 | if let Err(e) = http1::Builder::new() | 325 | if let Err(e) = http1::Builder::new() |
| 299 | .serve_connection(io, service) | 326 | .serve_connection(io, service) |
diff --git a/src/http/nip11.rs b/src/http/nip11.rs index a93ee5f..593ef9a 100644 --- a/src/http/nip11.rs +++ b/src/http/nip11.rs | |||
| @@ -1,10 +1,9 @@ | |||
| 1 | use crate::config::Config; | ||
| 1 | /// NIP-11 Relay Information Document | 2 | /// NIP-11 Relay Information Document |
| 2 | /// | 3 | /// |
| 3 | /// Implements NIP-11 relay information endpoint with GRASP-01 extensions. | 4 | /// Implements NIP-11 relay information endpoint with GRASP-01 extensions. |
| 4 | /// See: https://github.com/nostr-protocol/nips/blob/master/11.md | 5 | /// See: https://github.com/nostr-protocol/nips/blob/master/11.md |
| 5 | |||
| 6 | use serde::{Deserialize, Serialize}; | 6 | use serde::{Deserialize, Serialize}; |
| 7 | use crate::config::Config; | ||
| 8 | 7 | ||
| 9 | /// NIP-11 Relay Information Document | 8 | /// NIP-11 Relay Information Document |
| 10 | /// | 9 | /// |
| @@ -14,37 +13,36 @@ use crate::config::Config; | |||
| 14 | pub struct RelayInformationDocument { | 13 | pub struct RelayInformationDocument { |
| 15 | /// Relay name | 14 | /// Relay name |
| 16 | pub name: String, | 15 | pub name: String, |
| 17 | 16 | ||
| 18 | /// Relay description | 17 | /// Relay description |
| 19 | pub description: String, | 18 | pub description: String, |
| 20 | 19 | ||
| 21 | /// Relay owner's public key (hex format) | 20 | /// Relay owner's public key (hex format) |
| 22 | #[serde(skip_serializing_if = "Option::is_none")] | 21 | #[serde(skip_serializing_if = "Option::is_none")] |
| 23 | pub pubkey: Option<String>, | 22 | pub pubkey: Option<String>, |
| 24 | 23 | ||
| 25 | /// Contact information for relay admin | 24 | /// Contact information for relay admin |
| 26 | #[serde(skip_serializing_if = "Option::is_none")] | 25 | #[serde(skip_serializing_if = "Option::is_none")] |
| 27 | pub contact: Option<String>, | 26 | pub contact: Option<String>, |
| 28 | 27 | ||
| 29 | /// List of NIPs supported by this relay | 28 | /// List of NIPs supported by this relay |
| 30 | pub supported_nips: Vec<u16>, | 29 | pub supported_nips: Vec<u16>, |
| 31 | 30 | ||
| 32 | /// Relay software identifier | 31 | /// Relay software identifier |
| 33 | pub software: String, | 32 | pub software: String, |
| 34 | 33 | ||
| 35 | /// Software version | 34 | /// Software version |
| 36 | pub version: String, | 35 | pub version: String, |
| 37 | 36 | ||
| 38 | // GRASP-01 Extensions (lines 11-14 of GRASP-01 spec) | 37 | // GRASP-01 Extensions (lines 11-14 of GRASP-01 spec) |
| 39 | |||
| 40 | /// List of supported GRASPs (e.g., ["GRASP-01"]) | 38 | /// List of supported GRASPs (e.g., ["GRASP-01"]) |
| 41 | /// Required by GRASP-01 specification line 12 | 39 | /// Required by GRASP-01 specification line 12 |
| 42 | pub supported_grasps: Vec<String>, | 40 | pub supported_grasps: Vec<String>, |
| 43 | 41 | ||
| 44 | /// Repository acceptance criteria description | 42 | /// Repository acceptance criteria description |
| 45 | /// Required by GRASP-01 specification line 13 | 43 | /// Required by GRASP-01 specification line 13 |
| 46 | pub repo_acceptance_criteria: String, | 44 | pub repo_acceptance_criteria: String, |
| 47 | 45 | ||
| 48 | /// Curation policy (present if curated, absent otherwise) | 46 | /// Curation policy (present if curated, absent otherwise) |
| 49 | /// Optional per GRASP-01 specification line 14 | 47 | /// Optional per GRASP-01 specification line 14 |
| 50 | #[serde(skip_serializing_if = "Option::is_none")] | 48 | #[serde(skip_serializing_if = "Option::is_none")] |
| @@ -66,7 +64,7 @@ impl RelayInformationDocument { | |||
| 66 | ], | 64 | ], |
| 67 | software: env!("CARGO_PKG_NAME").to_string(), | 65 | software: env!("CARGO_PKG_NAME").to_string(), |
| 68 | version: env!("CARGO_PKG_VERSION").to_string(), | 66 | version: env!("CARGO_PKG_VERSION").to_string(), |
| 69 | 67 | ||
| 70 | // GRASP-01 Extensions | 68 | // GRASP-01 Extensions |
| 71 | supported_grasps: vec!["GRASP-01".to_string()], | 69 | supported_grasps: vec!["GRASP-01".to_string()], |
| 72 | repo_acceptance_criteria: format!( | 70 | repo_acceptance_criteria: format!( |
| @@ -77,7 +75,7 @@ impl RelayInformationDocument { | |||
| 77 | curation: None, // Not a curated relay - only SPAM prevention via GRASP-01 policy | 75 | curation: None, // Not a curated relay - only SPAM prevention via GRASP-01 policy |
| 78 | } | 76 | } |
| 79 | } | 77 | } |
| 80 | 78 | ||
| 81 | /// Serialize to JSON string | 79 | /// Serialize to JSON string |
| 82 | pub fn to_json(&self) -> Result<String, serde_json::Error> { | 80 | pub fn to_json(&self) -> Result<String, serde_json::Error> { |
| 83 | serde_json::to_string_pretty(self) | 81 | serde_json::to_string_pretty(self) |
| @@ -102,7 +100,7 @@ mod tests { | |||
| 102 | }; | 100 | }; |
| 103 | 101 | ||
| 104 | let doc = RelayInformationDocument::from_config(&config); | 102 | let doc = RelayInformationDocument::from_config(&config); |
| 105 | 103 | ||
| 106 | assert_eq!(doc.name, "Test Relay"); | 104 | assert_eq!(doc.name, "Test Relay"); |
| 107 | assert_eq!(doc.description, "A test relay"); | 105 | assert_eq!(doc.description, "A test relay"); |
| 108 | assert_eq!(doc.pubkey, Some("npub1test".to_string())); | 106 | assert_eq!(doc.pubkey, Some("npub1test".to_string())); |
| @@ -129,7 +127,7 @@ mod tests { | |||
| 129 | 127 | ||
| 130 | let doc = RelayInformationDocument::from_config(&config); | 128 | let doc = RelayInformationDocument::from_config(&config); |
| 131 | let json = doc.to_json().expect("Failed to serialize to JSON"); | 129 | let json = doc.to_json().expect("Failed to serialize to JSON"); |
| 132 | 130 | ||
| 133 | // Verify JSON contains expected fields | 131 | // Verify JSON contains expected fields |
| 134 | assert!(json.contains("\"name\"")); | 132 | assert!(json.contains("\"name\"")); |
| 135 | assert!(json.contains("\"description\"")); | 133 | assert!(json.contains("\"description\"")); |
| @@ -137,10 +135,10 @@ mod tests { | |||
| 137 | assert!(json.contains("\"supported_grasps\"")); | 135 | assert!(json.contains("\"supported_grasps\"")); |
| 138 | assert!(json.contains("\"repo_acceptance_criteria\"")); | 136 | assert!(json.contains("\"repo_acceptance_criteria\"")); |
| 139 | assert!(json.contains("GRASP-01")); | 137 | assert!(json.contains("GRASP-01")); |
| 140 | 138 | ||
| 141 | // Verify it's valid JSON by parsing | 139 | // Verify it's valid JSON by parsing |
| 142 | let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON"); | 140 | let parsed: serde_json::Value = serde_json::from_str(&json).expect("Invalid JSON"); |
| 143 | assert_eq!(parsed["name"], "Test Relay"); | 141 | assert_eq!(parsed["name"], "Test Relay"); |
| 144 | assert_eq!(parsed["supported_grasps"][0], "GRASP-01"); | 142 | assert_eq!(parsed["supported_grasps"][0], "GRASP-01"); |
| 145 | } | 143 | } |
| 146 | } \ No newline at end of file | 144 | } |
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs index 8e9926a..97fd17e 100644 --- a/src/nostr/builder.rs +++ b/src/nostr/builder.rs | |||
| @@ -51,7 +51,7 @@ impl Nip34WritePolicy { | |||
| 51 | /// Create a bare git repository if it doesn't exist | 51 | /// Create a bare git repository if it doesn't exist |
| 52 | /// Path format: <git_data_path>/<npub>/<identifier>.git | 52 | /// Path format: <git_data_path>/<npub>/<identifier>.git |
| 53 | fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> { | 53 | fn ensure_bare_repository(&self, announcement: &RepositoryAnnouncement) -> Result<(), String> { |
| 54 | let repo_path = self.git_data_path.join(&announcement.repo_path()); | 54 | let repo_path = self.git_data_path.join(announcement.repo_path()); |
| 55 | 55 | ||
| 56 | // Check if repository already exists | 56 | // Check if repository already exists |
| 57 | if repo_path.exists() { | 57 | if repo_path.exists() { |
| @@ -69,7 +69,7 @@ impl Nip34WritePolicy { | |||
| 69 | 69 | ||
| 70 | // Initialize bare repository using git command | 70 | // Initialize bare repository using git command |
| 71 | let output = std::process::Command::new("git") | 71 | let output = std::process::Command::new("git") |
| 72 | .args(&["init", "--bare", repo_path.to_str().unwrap()]) | 72 | .args(["init", "--bare", repo_path.to_str().unwrap()]) |
| 73 | .output() | 73 | .output() |
| 74 | .map_err(|e| format!("Failed to execute git init: {}", e))?; | 74 | .map_err(|e| format!("Failed to execute git init: {}", e))?; |
| 75 | 75 | ||
| @@ -482,7 +482,7 @@ impl Nip34WritePolicy { | |||
| 482 | }; | 482 | }; |
| 483 | 483 | ||
| 484 | // Build repository path | 484 | // Build repository path |
| 485 | let repo_path = self.git_data_path.join(&announcement.repo_path()); | 485 | let repo_path = self.git_data_path.join(announcement.repo_path()); |
| 486 | 486 | ||
| 487 | // Validate the ref | 487 | // Validate the ref |
| 488 | match git::validate_nostr_ref(&repo_path, &event_id, &expected_commit) { | 488 | match git::validate_nostr_ref(&repo_path, &event_id, &expected_commit) { |
| @@ -631,8 +631,8 @@ impl Nip34WritePolicy { | |||
| 631 | let kind_u16 = event.kind.as_u16(); | 631 | let kind_u16 = event.kind.as_u16(); |
| 632 | 632 | ||
| 633 | // Check if this is any kind of replaceable event | 633 | // Check if this is any kind of replaceable event |
| 634 | let is_regular_replaceable = kind_u16 >= 10000 && kind_u16 < 20000; | 634 | let is_regular_replaceable = (10000..20000).contains(&kind_u16); |
| 635 | let is_parameterized_replaceable = kind_u16 >= 30000 && kind_u16 < 40000; | 635 | let is_parameterized_replaceable = (30000..40000).contains(&kind_u16); |
| 636 | 636 | ||
| 637 | if is_regular_replaceable || is_parameterized_replaceable { | 637 | if is_regular_replaceable || is_parameterized_replaceable { |
| 638 | // Build the appropriate address format based on event type | 638 | // Build the appropriate address format based on event type |
| @@ -669,7 +669,7 @@ impl Nip34WritePolicy { | |||
| 669 | ]; | 669 | ]; |
| 670 | 670 | ||
| 671 | for tag_type in &addressable_tags { | 671 | for tag_type in &addressable_tags { |
| 672 | let filter = Filter::new().custom_tag(tag_type.clone(), address.clone()); | 672 | let filter = Filter::new().custom_tag(*tag_type, address.clone()); |
| 673 | 673 | ||
| 674 | match database.query(filter).await { | 674 | match database.query(filter).await { |
| 675 | Ok(events) => { | 675 | Ok(events) => { |
| @@ -691,7 +691,7 @@ impl Nip34WritePolicy { | |||
| 691 | ]; | 691 | ]; |
| 692 | 692 | ||
| 693 | for tag_type in &event_id_tags { | 693 | for tag_type in &event_id_tags { |
| 694 | let filter = Filter::new().custom_tag(tag_type.clone(), event_id_hex.clone()); | 694 | let filter = Filter::new().custom_tag(*tag_type, event_id_hex.clone()); |
| 695 | 695 | ||
| 696 | match database.query(filter).await { | 696 | match database.query(filter).await { |
| 697 | Ok(events) => { | 697 | Ok(events) => { |
diff --git a/src/nostr/events.rs b/src/nostr/events.rs index 6a62ccd..050bfdd 100644 --- a/src/nostr/events.rs +++ b/src/nostr/events.rs | |||
| @@ -322,9 +322,9 @@ impl RepositoryState { | |||
| 322 | 322 | ||
| 323 | /// Get the HEAD branch name (without refs/heads/ prefix) | 323 | /// Get the HEAD branch name (without refs/heads/ prefix) |
| 324 | pub fn get_head_branch(&self) -> Option<&str> { | 324 | pub fn get_head_branch(&self) -> Option<&str> { |
| 325 | self.head.as_ref().and_then(|h| { | 325 | self.head |
| 326 | h.strip_prefix("refs/heads/") | 326 | .as_ref() |
| 327 | }) | 327 | .and_then(|h| h.strip_prefix("refs/heads/")) |
| 328 | } | 328 | } |
| 329 | 329 | ||
| 330 | /// Check if the HEAD commit is available in the git repository | 330 | /// Check if the HEAD commit is available in the git repository |
| @@ -397,7 +397,7 @@ pub fn validate_state(event: &Event) -> Result<()> { | |||
| 397 | #[cfg(test)] | 397 | #[cfg(test)] |
| 398 | mod tests { | 398 | mod tests { |
| 399 | use super::*; | 399 | use super::*; |
| 400 | use nostr_sdk::{EventBuilder, Keys, Tag}; | 400 | use nostr_sdk::{EventBuilder, Keys}; |
| 401 | 401 | ||
| 402 | fn create_test_keys() -> Keys { | 402 | fn create_test_keys() -> Keys { |
| 403 | Keys::generate() | 403 | Keys::generate() |
| @@ -618,7 +618,10 @@ mod tests { | |||
| 618 | 618 | ||
| 619 | let announcement = RepositoryAnnouncement::from_event(event).unwrap(); | 619 | let announcement = RepositoryAnnouncement::from_event(event).unwrap(); |
| 620 | assert_eq!(announcement.maintainers.len(), 1); | 620 | assert_eq!(announcement.maintainers.len(), 1); |
| 621 | assert_eq!(announcement.maintainers[0], maintainer_keys.public_key().to_hex()); | 621 | assert_eq!( |
| 622 | announcement.maintainers[0], | ||
| 623 | maintainer_keys.public_key().to_hex() | ||
| 624 | ); | ||
| 622 | } | 625 | } |
| 623 | 626 | ||
| 624 | #[test] | 627 | #[test] |
| @@ -727,10 +730,7 @@ mod tests { | |||
| 727 | 730 | ||
| 728 | let keys = create_test_keys(); | 731 | let keys = create_test_keys(); |
| 729 | let tags = vec![ | 732 | let tags = vec![ |
| 730 | Tag::custom( | 733 | Tag::custom(nostr_sdk::TagKind::d(), vec!["test-repo".to_string()]), |
| 731 | nostr_sdk::TagKind::d(), | ||
| 732 | vec!["test-repo".to_string()], | ||
| 733 | ), | ||
| 734 | Tag::custom( | 734 | Tag::custom( |
| 735 | nostr_sdk::TagKind::Custom("refs/heads/main".into()), | 735 | nostr_sdk::TagKind::Custom("refs/heads/main".into()), |
| 736 | vec!["a1b2c3d4".to_string()], | 736 | vec!["a1b2c3d4".to_string()], |