@zap-js/server 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +18 -14
- package/package.json +4 -9
- package/src/bin/zap.rs +0 -154
- package/src/config.rs +0 -253
- package/src/connection_pool.rs +0 -404
- package/src/error.rs +0 -380
- package/src/handler.rs +0 -89
- package/src/ipc.js +0 -10
- package/src/ipc.rs +0 -499
- package/src/lib.rs +0 -433
- package/src/metrics.rs +0 -264
- package/src/proxy.rs +0 -436
- package/src/reliability.rs +0 -917
- package/src/request.rs +0 -60
- package/src/request_id.rs +0 -97
- package/src/response.rs +0 -182
- package/src/rpc.js +0 -14
- package/src/server.rs +0 -597
- package/src/static.rs +0 -572
- package/src/types.js +0 -21
- package/src/utils.rs +0 -18
- package/src/websocket.rs +0 -429
package/src/static.rs
DELETED
|
@@ -1,572 +0,0 @@
|
|
|
1
|
-
//! Static file serving functionality for ZapServer
|
|
2
|
-
//!
|
|
3
|
-
//! Provides high-performance static file serving with:
|
|
4
|
-
//! - ETag generation (weak or strong)
|
|
5
|
-
//! - Last-Modified headers
|
|
6
|
-
//! - Conditional request handling (304 Not Modified)
|
|
7
|
-
//! - Cache-Control configuration
|
|
8
|
-
//! - Content-Type detection
|
|
9
|
-
//! - Directory traversal protection
|
|
10
|
-
|
|
11
|
-
use std::collections::HashMap;
|
|
12
|
-
use std::path::PathBuf;
|
|
13
|
-
use std::time::SystemTime;
|
|
14
|
-
use zap_core::{Response, StatusCode};
|
|
15
|
-
use crate::error::ZapError;
|
|
16
|
-
use crate::response::ZapResponse;
|
|
17
|
-
|
|
18
|
-
/// ETag generation strategy
|
|
19
|
-
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
20
|
-
pub enum ETagStrategy {
|
|
21
|
-
/// Weak ETag from mtime + size (fast, no hashing)
|
|
22
|
-
/// Format: W/"size-mtime_hex"
|
|
23
|
-
#[default]
|
|
24
|
-
Weak,
|
|
25
|
-
/// Strong ETag using SHA256 hash (slower but precise)
|
|
26
|
-
/// Format: "sha256_hex"
|
|
27
|
-
Strong,
|
|
28
|
-
/// Disable ETag generation
|
|
29
|
-
None,
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/// Static file handler configuration
|
|
33
|
-
#[derive(Debug, Clone)]
|
|
34
|
-
pub struct StaticHandler {
|
|
35
|
-
/// URL prefix (e.g., "/assets")
|
|
36
|
-
pub prefix: String,
|
|
37
|
-
/// Local directory path
|
|
38
|
-
pub directory: PathBuf,
|
|
39
|
-
/// Options for static serving
|
|
40
|
-
pub options: StaticOptions,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/// Static file serving options
|
|
44
|
-
#[derive(Debug, Clone)]
|
|
45
|
-
pub struct StaticOptions {
|
|
46
|
-
/// Enable directory listing
|
|
47
|
-
pub directory_listing: bool,
|
|
48
|
-
/// Set Cache-Control header
|
|
49
|
-
pub cache_control: Option<String>,
|
|
50
|
-
/// Custom headers
|
|
51
|
-
pub headers: HashMap<String, String>,
|
|
52
|
-
/// Enable compression
|
|
53
|
-
pub compress: bool,
|
|
54
|
-
/// ETag generation strategy (default: Weak)
|
|
55
|
-
pub etag_strategy: ETagStrategy,
|
|
56
|
-
/// Enable Last-Modified header (default: true)
|
|
57
|
-
pub enable_last_modified: bool,
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
impl Default for StaticOptions {
|
|
61
|
-
fn default() -> Self {
|
|
62
|
-
Self {
|
|
63
|
-
directory_listing: false,
|
|
64
|
-
cache_control: Some("public, max-age=3600".to_string()),
|
|
65
|
-
headers: HashMap::new(),
|
|
66
|
-
compress: true,
|
|
67
|
-
etag_strategy: ETagStrategy::default(),
|
|
68
|
-
enable_last_modified: true,
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/// File metadata for caching headers
|
|
74
|
-
#[derive(Debug, Clone)]
|
|
75
|
-
struct FileMetadata {
|
|
76
|
-
size: u64,
|
|
77
|
-
modified: SystemTime,
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
impl StaticHandler {
|
|
81
|
-
/// Create a new static handler
|
|
82
|
-
pub fn new<P: Into<PathBuf>>(prefix: &str, directory: P) -> Self {
|
|
83
|
-
Self {
|
|
84
|
-
prefix: prefix.to_string(),
|
|
85
|
-
directory: directory.into(),
|
|
86
|
-
options: StaticOptions::default(),
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/// Create a new static handler with options
|
|
91
|
-
pub fn new_with_options<P: Into<PathBuf>>(
|
|
92
|
-
prefix: &str,
|
|
93
|
-
directory: P,
|
|
94
|
-
options: StaticOptions,
|
|
95
|
-
) -> Self {
|
|
96
|
-
Self {
|
|
97
|
-
prefix: prefix.to_string(),
|
|
98
|
-
directory: directory.into(),
|
|
99
|
-
options,
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/// Handle a static file request with conditional request support
|
|
104
|
-
pub async fn handle(&self, path: &str) -> Result<Option<ZapResponse>, ZapError> {
|
|
105
|
-
self.handle_with_headers(path, &HashMap::new()).await
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/// Handle a static file request with request headers for conditional handling
|
|
109
|
-
pub async fn handle_with_headers(
|
|
110
|
-
&self,
|
|
111
|
-
path: &str,
|
|
112
|
-
request_headers: &HashMap<String, String>,
|
|
113
|
-
) -> Result<Option<ZapResponse>, ZapError> {
|
|
114
|
-
if !path.starts_with(&self.prefix) {
|
|
115
|
-
return Ok(None);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
let file_path = path.strip_prefix(&self.prefix).unwrap_or("");
|
|
119
|
-
// Handle empty path or root
|
|
120
|
-
let file_path = if file_path.is_empty() || file_path == "/" {
|
|
121
|
-
"index.html"
|
|
122
|
-
} else {
|
|
123
|
-
file_path.trim_start_matches('/')
|
|
124
|
-
};
|
|
125
|
-
let full_path = self.directory.join(file_path);
|
|
126
|
-
|
|
127
|
-
// Security check: ensure path doesn't escape the directory
|
|
128
|
-
let canonical_dir = self.directory.canonicalize().unwrap_or_else(|_| self.directory.clone());
|
|
129
|
-
let canonical_path = full_path.canonicalize();
|
|
130
|
-
|
|
131
|
-
if let Ok(canonical) = &canonical_path {
|
|
132
|
-
if !canonical.starts_with(&canonical_dir) {
|
|
133
|
-
return Ok(Some(ZapResponse::Custom(Response::forbidden("Access denied"))));
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Get file metadata
|
|
138
|
-
let metadata = match tokio::fs::metadata(&full_path).await {
|
|
139
|
-
Ok(m) if m.is_file() => m,
|
|
140
|
-
_ => return Ok(None),
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
let file_meta = FileMetadata {
|
|
144
|
-
size: metadata.len(),
|
|
145
|
-
modified: metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
// Generate ETag if enabled
|
|
149
|
-
let etag = self.generate_etag(&file_meta, &full_path).await;
|
|
150
|
-
|
|
151
|
-
// Generate Last-Modified header value
|
|
152
|
-
let last_modified = if self.options.enable_last_modified {
|
|
153
|
-
Some(format_http_date(file_meta.modified))
|
|
154
|
-
} else {
|
|
155
|
-
None
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// Check conditional request headers
|
|
159
|
-
if let Some(ref etag_value) = etag {
|
|
160
|
-
// Check If-None-Match
|
|
161
|
-
if let Some(if_none_match) = request_headers.get("if-none-match")
|
|
162
|
-
.or_else(|| request_headers.get("If-None-Match"))
|
|
163
|
-
{
|
|
164
|
-
if etags_match(if_none_match, etag_value) {
|
|
165
|
-
return Ok(Some(self.not_modified_response(&etag, &last_modified)));
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Check If-Modified-Since
|
|
171
|
-
if let Some(ref last_mod) = last_modified {
|
|
172
|
-
if let Some(if_modified_since) = request_headers.get("if-modified-since")
|
|
173
|
-
.or_else(|| request_headers.get("If-Modified-Since"))
|
|
174
|
-
{
|
|
175
|
-
if let Some(since_time) = parse_http_date(if_modified_since) {
|
|
176
|
-
// File not modified since the specified time
|
|
177
|
-
if file_meta.modified <= since_time {
|
|
178
|
-
return Ok(Some(self.not_modified_response(&etag, &Some(last_mod.clone()))));
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Read file and serve
|
|
185
|
-
match tokio::fs::read(&full_path).await {
|
|
186
|
-
Ok(contents) => {
|
|
187
|
-
let content_type = mime_guess::from_path(&full_path)
|
|
188
|
-
.first_or_octet_stream()
|
|
189
|
-
.to_string();
|
|
190
|
-
|
|
191
|
-
let mut response = Response::new()
|
|
192
|
-
.status(StatusCode::OK)
|
|
193
|
-
.content_type(content_type)
|
|
194
|
-
.body(contents);
|
|
195
|
-
|
|
196
|
-
// Add cache control if specified
|
|
197
|
-
if let Some(cache_control) = &self.options.cache_control {
|
|
198
|
-
response = response.cache_control(cache_control);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Add ETag header
|
|
202
|
-
if let Some(etag_value) = etag {
|
|
203
|
-
response = response.header("ETag", etag_value);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Add Last-Modified header
|
|
207
|
-
if let Some(last_mod) = last_modified {
|
|
208
|
-
response = response.header("Last-Modified", last_mod);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Add custom headers
|
|
212
|
-
for (key, value) in &self.options.headers {
|
|
213
|
-
response = response.header(key, value);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
Ok(Some(ZapResponse::Custom(response)))
|
|
217
|
-
}
|
|
218
|
-
Err(_) => Ok(Some(ZapResponse::Custom(
|
|
219
|
-
Response::internal_server_error("Failed to read file"),
|
|
220
|
-
))),
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/// Generate ETag based on configured strategy
|
|
225
|
-
async fn generate_etag(&self, meta: &FileMetadata, path: &PathBuf) -> Option<String> {
|
|
226
|
-
match self.options.etag_strategy {
|
|
227
|
-
ETagStrategy::Weak => {
|
|
228
|
-
// Weak ETag from size + mtime
|
|
229
|
-
let mtime_secs = meta
|
|
230
|
-
.modified
|
|
231
|
-
.duration_since(SystemTime::UNIX_EPOCH)
|
|
232
|
-
.map(|d| d.as_secs())
|
|
233
|
-
.unwrap_or(0);
|
|
234
|
-
Some(format!("W/\"{:x}-{:x}\"", meta.size, mtime_secs))
|
|
235
|
-
}
|
|
236
|
-
ETagStrategy::Strong => {
|
|
237
|
-
// Strong ETag using SHA256 hash of content
|
|
238
|
-
match tokio::fs::read(path).await {
|
|
239
|
-
Ok(contents) => {
|
|
240
|
-
use sha2::{Digest, Sha256};
|
|
241
|
-
let mut hasher = Sha256::new();
|
|
242
|
-
hasher.update(&contents);
|
|
243
|
-
let hash = hasher.finalize();
|
|
244
|
-
// Use first 16 bytes (32 hex chars) for reasonable length
|
|
245
|
-
Some(format!("\"{}\"", hex::encode(&hash[..16])))
|
|
246
|
-
}
|
|
247
|
-
Err(_) => None,
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
ETagStrategy::None => None,
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/// Generate a 304 Not Modified response
|
|
255
|
-
fn not_modified_response(
|
|
256
|
-
&self,
|
|
257
|
-
etag: &Option<String>,
|
|
258
|
-
last_modified: &Option<String>,
|
|
259
|
-
) -> ZapResponse {
|
|
260
|
-
let mut response = Response::new().status(StatusCode::NOT_MODIFIED);
|
|
261
|
-
|
|
262
|
-
// Add cache control if specified
|
|
263
|
-
if let Some(cache_control) = &self.options.cache_control {
|
|
264
|
-
response = response.cache_control(cache_control);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Add ETag header
|
|
268
|
-
if let Some(etag_value) = etag {
|
|
269
|
-
response = response.header("ETag", etag_value);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Add Last-Modified header
|
|
273
|
-
if let Some(last_mod) = last_modified {
|
|
274
|
-
response = response.header("Last-Modified", last_mod);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
ZapResponse::Custom(response)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/// Handle static file requests from a list of handlers
|
|
282
|
-
pub async fn handle_static_files(
|
|
283
|
-
handlers: &[StaticHandler],
|
|
284
|
-
path: &str,
|
|
285
|
-
) -> Result<Option<ZapResponse>, ZapError> {
|
|
286
|
-
handle_static_files_with_headers(handlers, path, &HashMap::new()).await
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/// Handle static file requests with request headers for conditional handling
|
|
290
|
-
pub async fn handle_static_files_with_headers(
|
|
291
|
-
handlers: &[StaticHandler],
|
|
292
|
-
path: &str,
|
|
293
|
-
request_headers: &HashMap<String, String>,
|
|
294
|
-
) -> Result<Option<ZapResponse>, ZapError> {
|
|
295
|
-
for handler in handlers {
|
|
296
|
-
if let Some(response) = handler.handle_with_headers(path, request_headers).await? {
|
|
297
|
-
return Ok(Some(response));
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
Ok(None)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// ============================================================================
|
|
304
|
-
// HTTP Date Formatting (RFC 7231)
|
|
305
|
-
// ============================================================================
|
|
306
|
-
|
|
307
|
-
/// Format a SystemTime as an HTTP-date (RFC 7231)
|
|
308
|
-
/// Example: "Wed, 21 Oct 2015 07:28:00 GMT"
|
|
309
|
-
fn format_http_date(time: SystemTime) -> String {
|
|
310
|
-
use std::time::UNIX_EPOCH;
|
|
311
|
-
|
|
312
|
-
let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default();
|
|
313
|
-
let secs = duration.as_secs() as i64;
|
|
314
|
-
|
|
315
|
-
// Calculate date components
|
|
316
|
-
// Using a simplified algorithm for days since epoch
|
|
317
|
-
let days_since_epoch = secs / 86400;
|
|
318
|
-
let time_of_day = secs % 86400;
|
|
319
|
-
|
|
320
|
-
let hours = time_of_day / 3600;
|
|
321
|
-
let minutes = (time_of_day % 3600) / 60;
|
|
322
|
-
let seconds = time_of_day % 60;
|
|
323
|
-
|
|
324
|
-
// Calculate year, month, day using a simplified algorithm
|
|
325
|
-
// This is accurate for dates after 1970
|
|
326
|
-
let (year, month, day, weekday) = days_to_ymd(days_since_epoch);
|
|
327
|
-
|
|
328
|
-
let weekday_name = match weekday {
|
|
329
|
-
0 => "Thu", // Jan 1, 1970 was a Thursday
|
|
330
|
-
1 => "Fri",
|
|
331
|
-
2 => "Sat",
|
|
332
|
-
3 => "Sun",
|
|
333
|
-
4 => "Mon",
|
|
334
|
-
5 => "Tue",
|
|
335
|
-
6 => "Wed",
|
|
336
|
-
_ => "???",
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
let month_name = match month {
|
|
340
|
-
1 => "Jan",
|
|
341
|
-
2 => "Feb",
|
|
342
|
-
3 => "Mar",
|
|
343
|
-
4 => "Apr",
|
|
344
|
-
5 => "May",
|
|
345
|
-
6 => "Jun",
|
|
346
|
-
7 => "Jul",
|
|
347
|
-
8 => "Aug",
|
|
348
|
-
9 => "Sep",
|
|
349
|
-
10 => "Oct",
|
|
350
|
-
11 => "Nov",
|
|
351
|
-
12 => "Dec",
|
|
352
|
-
_ => "???",
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
format!(
|
|
356
|
-
"{}, {:02} {} {} {:02}:{:02}:{:02} GMT",
|
|
357
|
-
weekday_name, day, month_name, year, hours, minutes, seconds
|
|
358
|
-
)
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/// Convert days since epoch to year, month, day, weekday
|
|
362
|
-
fn days_to_ymd(days: i64) -> (i32, u32, u32, u32) {
|
|
363
|
-
// Weekday: Thursday = 0 for Jan 1, 1970
|
|
364
|
-
let weekday = ((days % 7) + 7) % 7;
|
|
365
|
-
|
|
366
|
-
// Algorithm adapted from Howard Hinnant's date algorithms
|
|
367
|
-
let z = days + 719468;
|
|
368
|
-
let era = if z >= 0 { z } else { z - 146096 } / 146097;
|
|
369
|
-
let doe = (z - era * 146097) as u32;
|
|
370
|
-
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
371
|
-
let y = yoe as i64 + era * 400;
|
|
372
|
-
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
373
|
-
let mp = (5 * doy + 2) / 153;
|
|
374
|
-
let d = doy - (153 * mp + 2) / 5 + 1;
|
|
375
|
-
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
|
376
|
-
let year = if m <= 2 { y + 1 } else { y };
|
|
377
|
-
|
|
378
|
-
(year as i32, m, d, weekday as u32)
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/// Parse an HTTP-date (RFC 7231) to SystemTime
|
|
382
|
-
/// Supports: "Wed, 21 Oct 2015 07:28:00 GMT"
|
|
383
|
-
fn parse_http_date(date_str: &str) -> Option<SystemTime> {
|
|
384
|
-
use std::time::{Duration, UNIX_EPOCH};
|
|
385
|
-
|
|
386
|
-
// Parse format: "Wed, 21 Oct 2015 07:28:00 GMT"
|
|
387
|
-
let parts: Vec<&str> = date_str.split_whitespace().collect();
|
|
388
|
-
if parts.len() < 5 {
|
|
389
|
-
return None;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Parse day (skip weekday)
|
|
393
|
-
let day: u32 = parts[1].trim_end_matches(',').parse().ok()?;
|
|
394
|
-
|
|
395
|
-
// Parse month
|
|
396
|
-
let month: u32 = match parts[2].to_lowercase().as_str() {
|
|
397
|
-
"jan" => 1,
|
|
398
|
-
"feb" => 2,
|
|
399
|
-
"mar" => 3,
|
|
400
|
-
"apr" => 4,
|
|
401
|
-
"may" => 5,
|
|
402
|
-
"jun" => 6,
|
|
403
|
-
"jul" => 7,
|
|
404
|
-
"aug" => 8,
|
|
405
|
-
"sep" => 9,
|
|
406
|
-
"oct" => 10,
|
|
407
|
-
"nov" => 11,
|
|
408
|
-
"dec" => 12,
|
|
409
|
-
_ => return None,
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
// Parse year
|
|
413
|
-
let year: i32 = parts[3].parse().ok()?;
|
|
414
|
-
|
|
415
|
-
// Parse time
|
|
416
|
-
let time_parts: Vec<&str> = parts[4].split(':').collect();
|
|
417
|
-
if time_parts.len() != 3 {
|
|
418
|
-
return None;
|
|
419
|
-
}
|
|
420
|
-
let hours: u32 = time_parts[0].parse().ok()?;
|
|
421
|
-
let minutes: u32 = time_parts[1].parse().ok()?;
|
|
422
|
-
let seconds: u32 = time_parts[2].parse().ok()?;
|
|
423
|
-
|
|
424
|
-
// Convert to seconds since epoch
|
|
425
|
-
let days = ymd_to_days(year, month, day)?;
|
|
426
|
-
let total_secs = days as u64 * 86400 + hours as u64 * 3600 + minutes as u64 * 60 + seconds as u64;
|
|
427
|
-
|
|
428
|
-
Some(UNIX_EPOCH + Duration::from_secs(total_secs))
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/// Convert year, month, day to days since epoch
|
|
432
|
-
fn ymd_to_days(year: i32, month: u32, day: u32) -> Option<i64> {
|
|
433
|
-
// Algorithm adapted from Howard Hinnant's date algorithms
|
|
434
|
-
let y = if month <= 2 { year - 1 } else { year } as i64;
|
|
435
|
-
let m = if month <= 2 { month + 12 } else { month } as i64;
|
|
436
|
-
let era = if y >= 0 { y } else { y - 399 } / 400;
|
|
437
|
-
let yoe = y - era * 400;
|
|
438
|
-
let doy = (153 * (m - 3) + 2) / 5 + day as i64 - 1;
|
|
439
|
-
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
|
440
|
-
let days = era * 146097 + doe - 719468;
|
|
441
|
-
Some(days)
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// ============================================================================
|
|
445
|
-
// ETag Matching
|
|
446
|
-
// ============================================================================
|
|
447
|
-
|
|
448
|
-
/// Check if an If-None-Match header value matches an ETag
|
|
449
|
-
/// Handles multiple ETags separated by commas and weak/strong comparison
|
|
450
|
-
fn etags_match(if_none_match: &str, etag: &str) -> bool {
|
|
451
|
-
// Handle wildcard
|
|
452
|
-
if if_none_match.trim() == "*" {
|
|
453
|
-
return true;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Normalize for weak comparison
|
|
457
|
-
let normalize = |s: &str| -> String {
|
|
458
|
-
let s = s.trim();
|
|
459
|
-
// Strip W/ prefix for weak ETags
|
|
460
|
-
let s = s.strip_prefix("W/").unwrap_or(s);
|
|
461
|
-
// Remove surrounding quotes
|
|
462
|
-
s.trim_matches('"').to_string()
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
let etag_normalized = normalize(etag);
|
|
466
|
-
|
|
467
|
-
// Check each ETag in the If-None-Match header
|
|
468
|
-
for candidate in if_none_match.split(',') {
|
|
469
|
-
if normalize(candidate) == etag_normalized {
|
|
470
|
-
return true;
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
false
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// ============================================================================
|
|
478
|
-
// Tests
|
|
479
|
-
// ============================================================================
|
|
480
|
-
|
|
481
|
-
#[cfg(test)]
|
|
482
|
-
mod tests {
|
|
483
|
-
use super::*;
|
|
484
|
-
|
|
485
|
-
#[test]
|
|
486
|
-
fn test_etag_strategy_default() {
|
|
487
|
-
assert_eq!(ETagStrategy::default(), ETagStrategy::Weak);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
#[test]
|
|
491
|
-
fn test_static_options_default() {
|
|
492
|
-
let opts = StaticOptions::default();
|
|
493
|
-
assert!(!opts.directory_listing);
|
|
494
|
-
assert!(opts.compress);
|
|
495
|
-
assert!(opts.enable_last_modified);
|
|
496
|
-
assert_eq!(opts.etag_strategy, ETagStrategy::Weak);
|
|
497
|
-
assert_eq!(opts.cache_control, Some("public, max-age=3600".to_string()));
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
#[test]
|
|
501
|
-
fn test_etags_match() {
|
|
502
|
-
// Exact match
|
|
503
|
-
assert!(etags_match("\"abc123\"", "\"abc123\""));
|
|
504
|
-
|
|
505
|
-
// Weak ETag comparison
|
|
506
|
-
assert!(etags_match("W/\"abc123\"", "\"abc123\""));
|
|
507
|
-
assert!(etags_match("\"abc123\"", "W/\"abc123\""));
|
|
508
|
-
assert!(etags_match("W/\"abc123\"", "W/\"abc123\""));
|
|
509
|
-
|
|
510
|
-
// Multiple ETags
|
|
511
|
-
assert!(etags_match("\"other\", \"abc123\"", "\"abc123\""));
|
|
512
|
-
assert!(etags_match("\"abc123\", \"other\"", "\"abc123\""));
|
|
513
|
-
|
|
514
|
-
// Wildcard
|
|
515
|
-
assert!(etags_match("*", "\"anything\""));
|
|
516
|
-
|
|
517
|
-
// No match
|
|
518
|
-
assert!(!etags_match("\"different\"", "\"abc123\""));
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
#[test]
|
|
522
|
-
fn test_format_http_date() {
|
|
523
|
-
use std::time::{Duration, UNIX_EPOCH};
|
|
524
|
-
|
|
525
|
-
// Test a known date: Jan 1, 1970 00:00:00 GMT (epoch)
|
|
526
|
-
let epoch = UNIX_EPOCH;
|
|
527
|
-
let date_str = format_http_date(epoch);
|
|
528
|
-
assert!(date_str.contains("1970"));
|
|
529
|
-
assert!(date_str.contains("Jan"));
|
|
530
|
-
assert!(date_str.contains("GMT"));
|
|
531
|
-
|
|
532
|
-
// Test a later date
|
|
533
|
-
let later = UNIX_EPOCH + Duration::from_secs(1445412480); // Oct 21, 2015 07:28:00
|
|
534
|
-
let date_str = format_http_date(later);
|
|
535
|
-
assert!(date_str.contains("2015"));
|
|
536
|
-
assert!(date_str.contains("Oct"));
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
#[test]
|
|
540
|
-
fn test_parse_http_date() {
|
|
541
|
-
// Test parsing
|
|
542
|
-
let parsed = parse_http_date("Wed, 21 Oct 2015 07:28:00 GMT");
|
|
543
|
-
assert!(parsed.is_some());
|
|
544
|
-
|
|
545
|
-
// Invalid format
|
|
546
|
-
let invalid = parse_http_date("invalid date");
|
|
547
|
-
assert!(invalid.is_none());
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
#[test]
|
|
551
|
-
fn test_static_handler_creation() {
|
|
552
|
-
let handler = StaticHandler::new("/assets", "./public");
|
|
553
|
-
assert_eq!(handler.prefix, "/assets");
|
|
554
|
-
assert_eq!(handler.directory, PathBuf::from("./public"));
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
#[test]
|
|
558
|
-
fn test_static_handler_with_options() {
|
|
559
|
-
let options = StaticOptions {
|
|
560
|
-
directory_listing: true,
|
|
561
|
-
cache_control: Some("no-cache".to_string()),
|
|
562
|
-
etag_strategy: ETagStrategy::Strong,
|
|
563
|
-
enable_last_modified: false,
|
|
564
|
-
..Default::default()
|
|
565
|
-
};
|
|
566
|
-
|
|
567
|
-
let handler = StaticHandler::new_with_options("/downloads", "./files", options);
|
|
568
|
-
assert!(handler.options.directory_listing);
|
|
569
|
-
assert_eq!(handler.options.etag_strategy, ETagStrategy::Strong);
|
|
570
|
-
assert!(!handler.options.enable_last_modified);
|
|
571
|
-
}
|
|
572
|
-
}
|
package/src/types.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @zap-js/server/types
|
|
3
|
-
*
|
|
4
|
-
* TypeScript types for server-side features
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// RPC types
|
|
8
|
-
export type {
|
|
9
|
-
RpcMessage,
|
|
10
|
-
RpcCallMessage,
|
|
11
|
-
RpcResponseMessage,
|
|
12
|
-
RpcErrorMessage,
|
|
13
|
-
} from '../../client/internal/runtime/src/types.js';
|
|
14
|
-
|
|
15
|
-
// IPC types
|
|
16
|
-
export type {
|
|
17
|
-
IpcMessage,
|
|
18
|
-
InvokeHandlerMessage,
|
|
19
|
-
HandlerResponseMessage,
|
|
20
|
-
ErrorMessage,
|
|
21
|
-
} from '../../client/internal/runtime/src/types.js';
|
package/src/utils.rs
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
//! Utility functions for ZapServer
|
|
2
|
-
|
|
3
|
-
use zap_core::Method;
|
|
4
|
-
use crate::error::ZapError;
|
|
5
|
-
|
|
6
|
-
/// Convert hyper Method to our Method enum
|
|
7
|
-
pub fn convert_method(method: &hyper::Method) -> Result<Method, ZapError> {
|
|
8
|
-
match *method {
|
|
9
|
-
hyper::Method::GET => Ok(Method::GET),
|
|
10
|
-
hyper::Method::POST => Ok(Method::POST),
|
|
11
|
-
hyper::Method::PUT => Ok(Method::PUT),
|
|
12
|
-
hyper::Method::PATCH => Ok(Method::PATCH),
|
|
13
|
-
hyper::Method::DELETE => Ok(Method::DELETE),
|
|
14
|
-
hyper::Method::HEAD => Ok(Method::HEAD),
|
|
15
|
-
hyper::Method::OPTIONS => Ok(Method::OPTIONS),
|
|
16
|
-
_ => Err(ZapError::http(format!("Unsupported method: {}", method))),
|
|
17
|
-
}
|
|
18
|
-
}
|