refactor: streaming, security hardening, and MCP removal
Major overhaul of server architecture and security posture: - Streaming: Unified all I/O through PIPESIZE (8192-byte) buffers. POST bodies stream via MpscReader through the save pipeline. GET content streams from disk via decompression to client. Removed save_item_with_reader, get_item_content_info, ChannelReader. 413 responses keep partial items (nonfatal by design). - Security: XSS protection in all HTML pages via html_escape crate. Security headers middleware (nosniff, frame deny, referrer policy). CORS tightened to explicit headers. Input validation for tags (256 chars), metadata (128/4096), pagination (10k cap). Config file reads use from_utf8_lossy. Generic error messages in HTML. Diff endpoint has 10 MB per-item cap. max_body_size config option. - Panics eliminated: Path unwraps → proper error propagation. Mutex unwraps → map_err (registries) / expect with message (local). - MCP removed: Deleted all MCP code, rmcp dependency, mcp feature. - Docs: Updated README, DESIGN, AGENTS to reflect all changes.
This commit is contained in:
@@ -6,11 +6,22 @@ use axum::{
|
||||
extract::{Path, Query, State},
|
||||
response::{Html, Response},
|
||||
};
|
||||
use html_escape::{encode_double_quoted_attribute, encode_text};
|
||||
use log::debug;
|
||||
use rusqlite::Connection;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Escape text content for safe HTML insertion.
|
||||
fn esc(s: &str) -> String {
|
||||
encode_text(s).to_string()
|
||||
}
|
||||
|
||||
/// Escape attribute values for safe HTML attribute insertion.
|
||||
fn esc_attr(s: &str) -> String {
|
||||
encode_double_quoted_attribute(s).to_string()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
/// Query parameters for the item list endpoint.
|
||||
///
|
||||
@@ -62,7 +73,7 @@ fn default_count() -> usize {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// let app = pages::add_routes(axum::Router::new());
|
||||
/// ```
|
||||
pub fn add_routes(app: axum::Router<AppState>) -> axum::Router<AppState> {
|
||||
@@ -90,7 +101,9 @@ async fn list_items(
|
||||
.map_err(|_| Html("<html><body>Internal Server Error</body></html>".to_string()))?;
|
||||
Ok(response)
|
||||
}
|
||||
Err(e) => Err(Html(format!("<html><body>Error: {e}</body></html>"))),
|
||||
Err(_e) => Err(Html(
|
||||
"<html><body>An internal error occurred</body></html>".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +134,8 @@ fn build_item_list(
|
||||
|
||||
// Apply pagination
|
||||
let start = params.start;
|
||||
let end = std::cmp::min(start + params.count, sorted_items.len());
|
||||
let count = params.count.min(10000);
|
||||
let end = std::cmp::min(start + count, sorted_items.len());
|
||||
let page_items = if start < sorted_items.len() {
|
||||
sorted_items[start..std::cmp::min(end, sorted_items.len())].to_vec()
|
||||
} else {
|
||||
@@ -153,11 +167,11 @@ fn build_item_list(
|
||||
// Collect all tags from all items, keeping track of their timestamps
|
||||
let mut all_tags_with_time: Vec<(String, chrono::DateTime<chrono::Utc>)> = Vec::new();
|
||||
for item in &sorted_items {
|
||||
if let Some(item_id) = item.id {
|
||||
if let Some(tags) = tags_map.get(&item_id) {
|
||||
for tag in tags {
|
||||
all_tags_with_time.push((tag.name.clone(), item.ts));
|
||||
}
|
||||
if let Some(item_id) = item.id
|
||||
&& let Some(tags) = tags_map.get(&item_id)
|
||||
{
|
||||
for tag in tags {
|
||||
all_tags_with_time.push((tag.name.clone(), item.ts));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,7 +198,9 @@ fn build_item_list(
|
||||
html.push_str("<p>");
|
||||
for tag in recent_tags {
|
||||
html.push_str(&format!(
|
||||
"<a href=\"/?tags={tag}\" style=\"margin-right: 8px;\">{tag}</a>"
|
||||
"<a href=\"/?tags={}\" style=\"margin-right: 8px;\">{}</a>",
|
||||
esc_attr(&tag),
|
||||
esc(&tag)
|
||||
));
|
||||
}
|
||||
html.push_str("</p>");
|
||||
@@ -196,7 +212,7 @@ fn build_item_list(
|
||||
// Table headers
|
||||
html.push_str("<tr>");
|
||||
for column in columns {
|
||||
html.push_str(&format!("<th>{}</th>", column.label));
|
||||
html.push_str(&format!("<th>{}</th>", esc(&column.label)));
|
||||
}
|
||||
html.push_str("<th>Actions</th>");
|
||||
html.push_str("</tr>");
|
||||
@@ -229,7 +245,13 @@ fn build_item_list(
|
||||
// Make sure we're using all tags for the item
|
||||
let tag_links: Vec<String> = tags
|
||||
.iter()
|
||||
.map(|t| format!("<a href=\"/?tags={}\">{}</a>", t.name, t.name))
|
||||
.map(|t| {
|
||||
format!(
|
||||
"<a href=\"/?tags={}\">{}</a>",
|
||||
esc_attr(&t.name),
|
||||
esc(&t.name)
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
tag_links.join(", ")
|
||||
}
|
||||
@@ -268,7 +290,15 @@ fn build_item_list(
|
||||
crate::config::ColumnAlignment::Center => "text-align: center;",
|
||||
};
|
||||
|
||||
html.push_str(&format!("<td style=\"{align_style}\">{display_value}</td>"));
|
||||
let rendered_value = if column.name == "tags" {
|
||||
display_value // Already contains escaped HTML links
|
||||
} else {
|
||||
esc(&display_value)
|
||||
};
|
||||
|
||||
html.push_str(&format!(
|
||||
"<td style=\"{align_style}\">{rendered_value}</td>"
|
||||
));
|
||||
}
|
||||
|
||||
// Actions column
|
||||
@@ -361,7 +391,9 @@ async fn show_item(
|
||||
.map_err(|_| Html("<html><body>Internal Server Error</body></html>".to_string()))?;
|
||||
Ok(response)
|
||||
}
|
||||
Err(e) => Err(Html(format!("<html><body>Error: {e}</body></html>"))),
|
||||
Err(_e) => Err(Html(
|
||||
"<html><body>An internal error occurred</body></html>".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,7 +428,7 @@ fn build_item_details(conn: &Connection, id: i64) -> Result<String> {
|
||||
));
|
||||
html.push_str(&format!(
|
||||
"<tr><th>Compression</th><td>{}</td></tr>",
|
||||
item.compression
|
||||
esc(&item.compression)
|
||||
));
|
||||
|
||||
// Tags row
|
||||
@@ -406,7 +438,13 @@ fn build_item_details(conn: &Connection, id: i64) -> Result<String> {
|
||||
} else {
|
||||
let tag_links: Vec<String> = tags
|
||||
.iter()
|
||||
.map(|t| format!("<a href=\"/?tags={}\">{}</a>", t.name, t.name))
|
||||
.map(|t| {
|
||||
format!(
|
||||
"<a href=\"/?tags={}\">{}</a>",
|
||||
esc_attr(&t.name),
|
||||
esc(&t.name)
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
html.push_str(&tag_links.join(", "));
|
||||
}
|
||||
@@ -419,7 +457,8 @@ fn build_item_details(conn: &Connection, id: i64) -> Result<String> {
|
||||
for meta in metas {
|
||||
html.push_str(&format!(
|
||||
"<tr><th>{}</th><td>{}</td></tr>",
|
||||
meta.name, meta.value
|
||||
esc(&meta.name),
|
||||
esc(&meta.value)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user