upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/http
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 14:31:32 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-12-01 15:22:38 +0000
commitd2ac69816567f092fe0d4661723bc43778cb481b (patch)
treee8b51b61a6a7b0ab1a214adebe4e237143b01f0b /src/http
parent7a78815e29b01c83f3d0ec195ba717a2eba8cd37 (diff)
fix cargo clippy and fmt warnings
Diffstat (limited to 'src/http')
-rw-r--r--src/http/mod.rs125
-rw-r--r--src/http/nip11.rs34
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;
9use std::pin::Pin; 9use std::pin::Pin;
10use std::sync::Arc; 10use std::sync::Arc;
11 11
12use base64::Engine;
13use http_body_util::{BodyExt, Full};
12use hyper::body::{Bytes, Incoming}; 14use hyper::body::{Bytes, Incoming};
13use hyper::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, UPGRADE}; 15use hyper::header::{CONNECTION, SEC_WEBSOCKET_ACCEPT, UPGRADE};
14use hyper::server::conn::http1; 16use hyper::server::conn::http1;
15use hyper::service::Service; 17use hyper::service::Service;
16use hyper::{Method, Request, Response}; 18use hyper::{Method, Request, Response};
17use hyper_util::rt::TokioIo; 19use hyper_util::rt::TokioIo;
18use http_body_util::{BodyExt, Full}; 20use nostr_relay_builder::prelude::MemoryDatabase;
21use nostr_relay_builder::LocalRelay;
19use nostr_sdk::hashes::sha1::Hash as Sha1Hash; 22use nostr_sdk::hashes::sha1::Hash as Sha1Hash;
20use nostr_sdk::hashes::{Hash, HashEngine}; 23use nostr_sdk::hashes::{Hash, HashEngine};
21use nostr_sdk::PublicKey; 24use nostr_sdk::PublicKey;
22use nostr_relay_builder::prelude::MemoryDatabase;
23use nostr_relay_builder::LocalRelay;
24use tokio::net::TcpListener; 25use tokio::net::TcpListener;
25use base64::Engine;
26 26
27use crate::config::Config; 27use crate::config::Config;
28use crate::git; 28use crate::git;
@@ -50,7 +50,12 @@ struct HttpService {
50} 50}
51 51
52impl HttpService { 52impl 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 @@
1use 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
6use serde::{Deserialize, Serialize}; 6use serde::{Deserialize, Serialize};
7use 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;
14pub struct RelayInformationDocument { 13pub 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}