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/http | |
| parent | 7a78815e29b01c83f3d0ec195ba717a2eba8cd37 (diff) | |
fix cargo clippy and fmt warnings
Diffstat (limited to 'src/http')
| -rw-r--r-- | src/http/mod.rs | 125 | ||||
| -rw-r--r-- | src/http/nip11.rs | 34 |
2 files changed, 92 insertions, 67 deletions
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 | } |