1use serde_json::{json, Value};
10
11pub const MAX_REQUEST_HEAD: usize = 16 * 1024;
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct RequestHead {
17 pub method: String,
18 pub target: String,
19 pub version: String,
20 pub headers: Vec<(String, String)>,
22}
23
24impl RequestHead {
25 pub fn header(&self, name: &str) -> Option<&str> {
26 self.headers
27 .iter()
28 .find(|(k, _)| k.eq_ignore_ascii_case(name))
29 .map(|(_, v)| v.as_str())
30 }
31
32 fn header_has_token(&self, name: &str, token: &str) -> bool {
33 self.headers
34 .iter()
35 .filter(|(k, _)| k.eq_ignore_ascii_case(name))
36 .any(|(_, v)| v.split(',').any(|t| t.trim().eq_ignore_ascii_case(token)))
37 }
38
39 pub fn wants_upgrade(&self) -> bool {
41 self.header_has_token("upgrade", "websocket")
42 }
43
44 pub fn wants_relay_info(&self) -> bool {
46 self.header("accept")
47 .is_some_and(|a| a.to_ascii_lowercase().contains("application/nostr+json"))
48 }
49}
50
51pub fn find_head_end(buf: &[u8]) -> Option<usize> {
53 buf.windows(4).position(|w| w == b"\r\n\r\n")
54}
55
56pub fn parse_request_head(bytes: &[u8]) -> Result<RequestHead, String> {
58 let text = std::str::from_utf8(bytes).map_err(|_| "request head is not UTF-8".to_string())?;
59 let mut lines = text.split("\r\n");
60 let mut parts = lines.next().unwrap_or("").split(' ');
61 let method = parts.next().unwrap_or("").to_string();
62 let target = parts.next().unwrap_or("").to_string();
63 let version = parts.next().unwrap_or("").to_string();
64 if method.is_empty() || target.is_empty() || !version.starts_with("HTTP/") {
65 return Err("malformed request line".to_string());
66 }
67 let mut headers = Vec::new();
68 for line in lines {
69 if line.is_empty() {
70 break;
71 }
72 let (name, value) = line
73 .split_once(':')
74 .ok_or_else(|| "malformed header line".to_string())?;
75 if name.is_empty() || name.ends_with(' ') || name.starts_with([' ', '\t']) {
76 return Err("malformed header name".to_string());
77 }
78 headers.push((name.to_ascii_lowercase(), value.trim().to_string()));
79 }
80 Ok(RequestHead {
81 method,
82 target,
83 version,
84 headers,
85 })
86}
87
88pub fn validate_upgrade(head: &RequestHead) -> Result<String, (u16, &'static str)> {
91 if !head.method.eq_ignore_ascii_case("GET") {
92 return Err((405, "a WebSocket upgrade is a GET"));
93 }
94 if head.version != "HTTP/1.1" {
95 return Err((505, "a WebSocket upgrade needs HTTP/1.1"));
96 }
97 if !head.header_has_token("connection", "upgrade") {
98 return Err((400, "missing Connection: Upgrade"));
99 }
100 if head.header("sec-websocket-version") != Some("13") {
101 return Err((426, "this relay speaks WebSocket version 13"));
102 }
103 let key = head
104 .header("sec-websocket-key")
105 .ok_or((400, "missing Sec-WebSocket-Key"))?;
106 use base64::Engine as _;
107 match base64::engine::general_purpose::STANDARD.decode(key) {
108 Ok(raw) if raw.len() == 16 => Ok(key.to_string()),
109 _ => Err((400, "Sec-WebSocket-Key is not a base64 16-byte nonce")),
110 }
111}
112
113pub fn accept_response(key: &str) -> String {
115 let accept = tokio_tungstenite::tungstenite::handshake::derive_accept_key(key.as_bytes());
116 format!(
117 "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\
118 Sec-WebSocket-Accept: {accept}\r\n\r\n"
119 )
120}
121
122const CORS: &str = "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Headers: *\r\n\
125 Access-Control-Allow-Methods: GET, OPTIONS\r\n";
126
127pub fn response(status: u16, content_type: &str, extra_headers: &str, body: &str) -> Vec<u8> {
129 let phrase = match status {
130 200 => "OK",
131 204 => "No Content",
132 400 => "Bad Request",
133 405 => "Method Not Allowed",
134 408 => "Request Timeout",
135 426 => "Upgrade Required",
136 431 => "Request Header Fields Too Large",
137 503 => "Service Unavailable",
138 505 => "HTTP Version Not Supported",
139 _ => "Error",
140 };
141 let mut out = format!("HTTP/1.1 {status} {phrase}\r\n{extra_headers}");
142 if status == 426 {
143 out.push_str("Sec-WebSocket-Version: 13\r\n");
144 }
145 out.push_str("Connection: close\r\n");
146 if !body.is_empty() {
147 out.push_str(&format!("Content-Type: {content_type}\r\n"));
148 }
149 out.push_str(&format!("Content-Length: {}\r\n\r\n{body}", body.len()));
150 out.into_bytes()
151}
152
153pub fn error_response(status: u16, reason: &str) -> Vec<u8> {
155 response(
156 status,
157 "text/plain; charset=utf-8",
158 "",
159 &format!("{reason}\n"),
160 )
161}
162
163pub fn browser_response() -> Vec<u8> {
165 response(
166 200,
167 "text/plain; charset=utf-8",
168 CORS,
169 "Please use a Nostr client to connect.\n",
170 )
171}
172
173pub fn preflight_response() -> Vec<u8> {
175 response(204, "", CORS, "")
176}
177
178pub fn relay_info_response(document: &Value) -> Vec<u8> {
180 let body = serde_json::to_string(document).expect("relay info is plain JSON");
181 response(200, "application/nostr+json", CORS, &body)
182}
183
184#[derive(Debug, Clone)]
186pub struct RelayInfo {
187 pub name: String,
188 pub description: String,
189 pub supported_nips: Vec<u64>,
190 pub relay_pubkey: String,
192}
193
194impl RelayInfo {
195 pub fn document(&self) -> Value {
196 use super::wire;
197 json!({
198 "name": self.name,
199 "description": self.description,
200 "self": self.relay_pubkey,
201 "supported_nips": self.supported_nips,
202 "software": "https://github.com/netget-net/netget",
203 "version": env!("CARGO_PKG_VERSION"),
204 "limitation": {
205 "max_message_length": wire::MAX_MESSAGE_BYTES,
206 "max_subscriptions": wire::MAX_SUBSCRIPTIONS,
207 "max_filters": wire::MAX_FILTERS,
208 "max_subid_length": wire::MAX_SUBSCRIPTION_ID_CHARS,
209 "max_event_tags": wire::MAX_TAGS,
210 "auth_required": false,
211 "payment_required": false,
212 },
213 })
214 }
215}