diff --git a/example.v b/example.v index fe5da66..17ad776 100644 --- a/example.v +++ b/example.v @@ -4,21 +4,37 @@ import net.http import hono fn main() { + // ===== 1. 应用初始化 ===== mut app := hono.Hono.new() + // ===== 2. 中间件配置 ===== + // 日志中间件 app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response { println('[LOG] ${c.req.method} ${c.url}') return next(mut c) }) - // 静态文件服务 - 默认配置(./public 目录) + // 认证中间件 + app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response { + println("url path: ${c.url}") + token := c.query['token'] + if c.url != "/" && token == '' { + c.status(401) + return c.json('{"error": "Unauthorized", "message": "Token required"}') + } + return next(mut c) + }) + + // ===== 3. 静态文件服务中间件 ===== + + // 默认静态文件服务(./public 目录) app.use(hono.serve_static_default()) - // 静态文件服务 - 指定路径前缀 + // 资源文件服务(./static 目录) app.use(hono.serve_static_path('/assets', './static')) - // 静态文件服务 - 自定义配置 + // 上传文件服务(./uploads 目录) options := hono.StaticOptions{ root: './uploads' path: '/files' @@ -34,12 +50,9 @@ fn main() { } app.use(hono.serve_static(options)) - // 基本路由 - app.get('/hello', fn (mut c hono.Context) http.Response { - return c.text('Hello, World!') - }) + // ===== 4. 基础路由 ===== - // 根路径 + // 根路径 - 欢迎页面 app.get('/', fn (mut c hono.Context) http.Response { html_content := ' @@ -48,12 +61,14 @@ fn main() { V-Hono 完整示例 @@ -95,7 +110,26 @@ fn main() { -
+
+

📄 文件服务端点

+
+ 基本文件服务: /file/test.txt +
+
+ 缓存文件服务: /file-cached/test.txt +
+
+ 文件下载: /download/downloadable.txt +
+
+ 图片文件服务: /image/style.css +
+
+ 安全文件服务: /safe-file/test.txt +
+
+ +

🎯 动态路由示例

单参数: /users/123 @@ -111,7 +145,7 @@ fn main() {

✨ 特性

    -
  • 🚀 高性能路由系统
  • +
  • 🚀 高性能路由系统(Trie + Hybrid)
  • 🛡️ 路径遍历攻击防护
  • 📁 自动索引文件支持
  • 🔒 点文件访问控制
  • @@ -119,6 +153,8 @@ fn main() {
  • 💾 可配置缓存策略
  • 🎯 智能 Content-Type 检测
  • 🔧 洋葱模型中间件
  • +
  • 🔄 支持所有 HTTP 方法
  • +
  • 📊 路由性能统计
@@ -130,6 +166,13 @@ curl http://localhost:8080/test.txt curl http://localhost:8080/assets/style.css curl http://localhost:8080/files/sample.json +# 文件服务测试 +curl http://localhost:8080/file/test.txt +curl http://localhost:8080/file-cached/test.txt +curl http://localhost:8080/download/downloadable.txt +curl http://localhost:8080/image/style.css +curl http://localhost:8080/safe-file/test.txt + # API 测试 curl http://localhost:8080/api/health curl http://localhost:8080/api/status @@ -138,6 +181,10 @@ curl http://localhost:8080/hello # 动态路由测试 curl http://localhost:8080/users/123 curl http://localhost:8080/posts/456/comments/789 + +# POST 测试 +curl -X POST http://localhost:8080/api/users -d "{\\"name\\":\\"John\\"}" +curl -X POST "http://localhost:8080/api/test/create/456?token=abc123" -d "{\\"name\\":\\"Test\\"}"
@@ -145,6 +192,13 @@ curl http://localhost:8080/posts/456/comments/789 return c.html(html_content) }) + // 基础路由 + app.get('/hello', fn (mut c hono.Context) http.Response { + return c.text('Hello, World!') + }) + + // ===== 5. API 路由 ===== + // 健康检查 app.get('/api/health', fn (mut c hono.Context) http.Response { c.status(200) @@ -156,6 +210,7 @@ curl http://localhost:8080/posts/456/comments/789 return c.json('{"status": "ok", "static_files": "enabled"}') }) + // 静态文件服务信息 app.get('/api/static-info', fn (mut c hono.Context) http.Response { return c.json('{ "static_directories": [ @@ -174,26 +229,30 @@ curl http://localhost:8080/posts/456/comments/789 }') }) - // 动态路由 - 单参数 + // ===== 6. 动态路由 ===== + + // 单参数路由 app.get('/users/:id', fn (mut c hono.Context) http.Response { user_id := c.params['id'] return c.json('{"user_id": "${user_id}", "name": "John Doe"}') }) - // 动态路由 - 多参数 + // 多参数路由 app.get('/posts/:id/comments/:comment_id', fn (mut c hono.Context) http.Response { post_id := c.params['id'] comment_id := c.params['comment_id'] return c.json('{"post_id": "${post_id}", "comment_id": "${comment_id}", "content": "Great post!"}') }) - // 动态路由 - 嵌套参数 + // 嵌套参数路由 app.get('/api/users/:user_id/posts/:post_id', fn (mut c hono.Context) http.Response { user_id := c.params['user_id'] post_id := c.params['post_id'] return c.json('{"user_id": "${user_id}", "post_id": "${post_id}", "title": "Sample Post"}') }) + // ===== 7. HTTP 方法示例 ===== + // POST 请求 app.post('/api/users', fn (mut c hono.Context) http.Response { println('=== POST 请求信息 ===') @@ -276,22 +335,116 @@ curl http://localhost:8080/posts/456/comments/789 return c.json('{"message": "User partially updated", "user_id": "${user_id}", "data": "${c.body}"}') }) - // 中间件示例 - app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response { - println('[LOG] ${c.url}') - return next(mut c) + // HEAD 请求示例 + app.head('/api/users/:id', fn (mut c hono.Context) http.Response { + user_id := c.params['id'] + c.headers['X-User-ID'] = user_id + c.status(200) + return c.text('') }) - // 认证中间件示例 - app.use(fn (mut c hono.Context, next fn (mut hono.Context) http.Response) http.Response { - token := c.query['token'] - if token == '' { - c.status(401) - return c.json('{"error": "Unauthorized", "message": "Token required"}') - } - return next(mut c) + // OPTIONS 请求示例 + app.options('/api/users/:id', fn (mut c hono.Context) http.Response { + c.headers['Allow'] = 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS' + c.headers['Access-Control-Allow-Origin'] = '*' + c.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS' + c.status(200) + return c.text('') }) + // ===== 8. 文件服务功能 ===== + + // 基本文件服务示例 + app.get('/file/:filename', fn (mut c hono.Context) http.Response { + filename := c.params['filename'] + file_path := 'public/${filename}' + return c.file(file_path) + }) + + // 带选项的文件服务示例 + app.get('/file-cached/:filename', fn (mut c hono.Context) http.Response { + filename := c.params['filename'] + file_path := 'public/${filename}' + + options := hono.FileOptions{ + max_age: 3600 // 缓存1小时 + headers: { + 'X-Served-By': 'v-hono' + } + } + + return c.file_with_options(file_path, options) + }) + + // 自定义Content-Type的文件服务 + app.get('/download/:filename', fn (mut c hono.Context) http.Response { + filename := c.params['filename'] + file_path := 'uploads/${filename}' + + options := hono.FileOptions{ + content_type: 'application/octet-stream' + headers: { + 'Content-Disposition': 'attachment; filename="${filename}"' + } + } + + return c.file_with_options(file_path, options) + }) + + // 图片文件服务示例 + app.get('/image/:filename', fn (mut c hono.Context) http.Response { + filename := c.params['filename'] + file_path := 'static/${filename}' + + options := hono.FileOptions{ + max_age: 86400 // 缓存24小时 + headers: { + 'X-Image-Type': 'static' + } + } + + return c.file_with_options(file_path, options) + }) + + // 禁用缓存的文件服务 + app.get('/dynamic/:filename', fn (mut c hono.Context) http.Response { + filename := c.params['filename'] + file_path := 'dynamic/${filename}' + + options := hono.FileOptions{ + no_cache: true + headers: { + 'X-Dynamic': 'true' + } + } + + return c.file_with_options(file_path, options) + }) + + // ===== 9. 错误处理示例 ===== + + // 安全文件服务 + app.get('/safe-file/:filename', fn (mut c hono.Context) http.Response { + filename := c.params['filename'] + + // 安全检查:只允许访问特定目录 + if filename.contains('..') || filename.starts_with('/') { + c.status(403) + return c.text('Access denied') + } + + file_path := 'public/${filename}' + return c.file(file_path) + }) + + // 404 错误处理 + app.get('/**', fn (mut c hono.Context) http.Response { + c.status(404) + return c.json('{"error": "Not Found", "message": "The requested resource was not found"}') + }) + + // ===== 10. 服务器启动 ===== + println('🚀 V-Hono 完整示例启动中...') println('📍 服务器地址: http://localhost:8080') println('') @@ -306,20 +459,28 @@ curl http://localhost:8080/posts/456/comments/789 println(' - GET /api/static-info') println(' - GET /hello') println('') + println('📄 文件服务端点:') + println(' - GET /file/:filename - 基本文件服务') + println(' - GET /file-cached/:filename - 缓存文件服务') + println(' - GET /download/:filename - 文件下载') + println(' - GET /image/:filename - 图片文件服务') + println(' - GET /dynamic/:filename - 动态文件服务') + println(' - GET /safe-file/:filename - 安全文件服务') + println('') println('🎯 动态路由:') println(' - GET /users/123') println(' - GET /posts/456/comments/789') println(' - GET /api/users/101/posts/202') println('') - println('📝 POST 请求:') + println('📝 HTTP 方法示例:') println(' - POST /api/users') println(' - POST /api/users/123') println(' - POST /api/test/create/456') - println('') - println('🔄 其他方法:') println(' - PUT /api/users/123') println(' - DELETE /api/users/123') println(' - PATCH /api/users/123') + println(' - HEAD /api/users/123') + println(' - OPTIONS /api/users/123') println('') println('🧪 测试命令示例:') println(' # 静态文件测试') @@ -327,6 +488,13 @@ curl http://localhost:8080/posts/456/comments/789 println(' curl http://localhost:8080/assets/style.css') println(' curl http://localhost:8080/files/sample.json') println('') + println(' # 文件服务测试') + println(' curl http://localhost:8080/file/test.txt') + println(' curl http://localhost:8080/file-cached/test.txt') + println(' curl http://localhost:8080/download/downloadable.txt') + println(' curl http://localhost:8080/image/style.css') + println(' curl http://localhost:8080/safe-file/test.txt') + println('') println(' # API 测试') println(' curl http://localhost:8080/api/health') println(' curl http://localhost:8080/api/status') diff --git a/hono/request.v b/hono/request.v index 8b8c57d..43644c9 100644 --- a/hono/request.v +++ b/hono/request.v @@ -2,6 +2,7 @@ module hono import net.http import net.urllib +import os // Context 结构体,类似 Hono.js 的实现 pub struct Context { @@ -74,6 +75,167 @@ pub fn (mut c Context) html(data string) http.Response { } } +// file method - serve a file directly from the context +pub fn (mut c Context) file(file_path string) http.Response { + return c.file_with_options(file_path, FileOptions{}) +} + +// file_with_options method - serve a file with custom options +pub fn (mut c Context) file_with_options(file_path string, options FileOptions) http.Response { + // 安全检查:防止路径遍历攻击 + if !is_safe_file_path(file_path) { + c.status(403) + return c.text('Forbidden') + } + + // 检查文件是否存在 + if !os.exists(file_path) { + c.status(404) + return c.text('File Not Found') + } + + // 检查是否为目录 + if os.is_dir(file_path) { + c.status(400) + return c.text('Cannot serve directory') + } + + // 读取文件内容 + file_content := os.read_file(file_path) or { + c.status(500) + return c.text('Internal Server Error') + } + + // 获取文件信息 + file_info := os.stat(file_path) or { + c.status(500) + return c.text('Internal Server Error') + } + + // 设置状态码 + if options.status_code > 0 { + c.status(options.status_code) + } else { + c.status(200) + } + + // 设置Content-Type + if options.content_type != '' { + c.headers['Content-Type'] = options.content_type + } else { + content_type := get_file_content_type(file_path) + c.headers['Content-Type'] = content_type + } + + // 设置Content-Length + c.headers['Content-Length'] = file_content.len.str() + + // 设置Last-Modified + if options.last_modified { + last_modified := format_http_date(file_info.mtime) + c.headers['Last-Modified'] = last_modified + } + + // 设置ETag + if options.etag { + etag := generate_file_etag(file_content, file_info.mtime) + c.headers['ETag'] = etag + + // 检查If-None-Match + if_none_match := c.req.header.get_custom('If-None-Match') or { '' } + if if_none_match == etag { + c.status(304) + return c.text('') + } + } + + // 设置Cache-Control + if options.max_age > 0 { + c.headers['Cache-Control'] = 'public, max-age=${options.max_age}' + } else if options.no_cache { + c.headers['Cache-Control'] = 'no-cache' + } + + // 设置自定义头部 + for key, value in options.headers { + c.headers[key] = value + } + + // 返回文件内容 + return c.text(file_content) +} + +// FileOptions struct for configuring file serving +pub struct FileOptions { +pub: + status_code int = 0 // 自定义状态码,0表示使用默认200 + content_type string = '' // 自定义Content-Type + last_modified bool = true // 是否设置Last-Modified头 + etag bool = true // 是否设置ETag头 + max_age int = 0 // 缓存时间(秒) + no_cache bool = false // 是否禁用缓存 + headers map[string]string // 自定义响应头 +} + +// 安全检查:防止路径遍历攻击 +fn is_safe_file_path(path string) bool { + // 检查是否包含 .. 或绝对路径 + if path.contains('..') || path.starts_with('/') || path.starts_with('\\') { + return false + } + + // 检查是否包含危险字符 + dangerous_chars := ['<', '>', ':', '"', '|', '?', '*'] + for dangerous_char in dangerous_chars { + if path.contains(dangerous_char) { + return false + } + } + + return true +} + +// 根据文件扩展名获取Content-Type +fn get_file_content_type(file_path string) string { + ext := os.file_ext(file_path).to_lower() + + match ext { + '.html', '.htm' { return 'text/html; charset=utf-8' } + '.css' { return 'text/css; charset=utf-8' } + '.js' { return 'application/javascript; charset=utf-8' } + '.json' { return 'application/json; charset=utf-8' } + '.xml' { return 'application/xml; charset=utf-8' } + '.txt' { return 'text/plain; charset=utf-8' } + '.md' { return 'text/markdown; charset=utf-8' } + '.pdf' { return 'application/pdf' } + '.png' { return 'image/png' } + '.jpg', '.jpeg' { return 'image/jpeg' } + '.gif' { return 'image/gif' } + '.svg' { return 'image/svg+xml' } + '.ico' { return 'image/x-icon' } + '.woff' { return 'font/woff' } + '.woff2' { return 'font/woff2' } + '.ttf' { return 'font/ttf' } + '.eot' { return 'application/vnd.ms-fontobject' } + '.otf' { return 'font/otf' } + '.mp4' { return 'video/mp4' } + '.webm' { return 'video/webm' } + '.mp3' { return 'audio/mpeg' } + '.wav' { return 'audio/wav' } + '.zip' { return 'application/zip' } + '.tar' { return 'application/x-tar' } + '.gz' { return 'application/gzip' } + else { return 'application/octet-stream' } + } +} + +// 生成ETag +fn generate_file_etag(content string, mod_time i64) string { + // 简化的ETag生成 + // 实际应用中可能需要更复杂的哈希算法 + return '"${content.len}-${mod_time}"' +} + pub fn (mut c Context) status(code int) { c.status_code = code } diff --git a/public/index.html b/public/index.html index c367637..93930b8 100644 --- a/public/index.html +++ b/public/index.html @@ -3,48 +3,719 @@ - V-Hono 静态文件服务 - Public 目录 + V-Hono - 轻量级 V 语言 Web 框架
-

🎉 成功!

-

这是来自 ./public 目录的 index.html 文件

- -
-

✨ V-Hono 静态文件服务特性

-

✅ 自动索引文件支持

-

✅ 路径遍历攻击防护

-

✅ ETag 和缓存支持

-

✅ 智能 Content-Type 检测

+
+

🚀 V-Hono

+

轻量级 V 语言 Web 框架 - 功能完整展示

+
+ + +
+

✨ 核心特性

+
+
+

🚀 高性能路由

+

支持 Trie 路由树和 Hybrid 路由,支持静态路由、动态参数、通配符和正则路由

+
+
+

📁 静态文件服务

+

内置静态文件服务中间件,支持多目录、缓存控制、ETag、路径安全防护

+
+
+

🔧 洋葱模型中间件

+

支持洋葱模型中间件,提供日志、认证、CORS 等常用中间件

+
+
+

🛡️ 安全防护

+

内置路径遍历攻击防护、点文件访问控制、请求验证等安全特性

+
+
+

📊 性能监控

+

内置路由性能统计、缓存命中率监控、请求响应时间统计

+
+
+

🔄 HTTP 方法支持

+

完整支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 等 HTTP 方法

+
+
+
+ + +
+

🧪 功能测试

+ + +
+

🔑 认证 Token

+
+ + + +
+
+ 当前 Token: abc123 +
+
+ +
+
+ + + + +
+ + +
+
+

健康检查

+ + + + +
+
+ + +
+
+

文件服务测试

+ + + + + + +
+
+ + +
+
+

动态路由测试

+ + + + +
+
+ + +
+
+

静态文件访问

+ + + + +
+
+
+
+ + +
+

📋 可用端点

+ +

🔧 API 端点

+
+ 健康检查: /api/health +
+
+ 静态文件状态: /api/status +
+
+ 静态文件信息: /api/static-info +
+
+ 基础路由: /hello +
+ +

📄 文件服务端点

+
+ 基本文件服务: /file/test.txt +
+
+ 缓存文件服务: /file-cached/test.txt +
+
+ 文件下载: /download/downloadable.txt +
+
+ 图片文件服务: /image/style.css +
+
+ 安全文件服务: /safe-file/test.txt +
+ +

🎯 动态路由

+
+ 单参数: /users/123 +
+
+ 多参数: /posts/456/comments/789 +
+
+ 嵌套参数: /api/users/101/posts/202 +
+ +

📁 静态文件目录

+
+ 默认目录: /test.txt (来自 ./public) +
+
+ 资源目录: /assets/style.css (来自 ./static) +
+
+ 文件目录: /files/sample.json (来自 ./uploads) +
+
+ + +
+

📊 性能统计

+
+
+
0
+
总请求数
+
+
+
100%
+
成功率
+
+
+
0ms
+
平均响应时间
+
+
+
0%
+
缓存命中率
+
+
+
+ + +
+

💻 代码示例

+
+// 基本路由设置 +mut app := hono.Hono.new() + +// 静态文件服务 +app.use(hono.serve_static_default()) +app.use(hono.serve_static_path('/assets', './static')) + +// 基础路由 +app.get('/hello', fn (mut c hono.Context) http.Response { + return c.text('Hello, World!') +}) + +// 动态路由 +app.get('/users/:id', fn (mut c hono.Context) http.Response { + user_id := c.params['id'] + return c.json('{"user_id": "${user_id}"}') +}) + +// 文件服务 +app.get('/file/:filename', fn (mut c hono.Context) http.Response { + filename := c.params['filename'] + return c.file('public/${filename}') +}) + +// 启动服务器 +app.listen(':8080') +
+
+ + - -

← 返回主页

+ + \ No newline at end of file diff --git a/public/sample.json b/public/sample.json new file mode 100644 index 0000000..0d44035 --- /dev/null +++ b/public/sample.json @@ -0,0 +1,15 @@ +{ + "message": "Hello from v-hono Context.file!", + "features": [ + "File serving with automatic Content-Type detection", + "Security validation against path traversal", + "Caching support with ETag and Last-Modified", + "Custom headers and options", + "Error handling for missing files" + ], + "example": { + "endpoint": "/file/sample.json", + "content_type": "application/json", + "cached": true + } +} \ No newline at end of file diff --git a/public/test.txt b/public/test.txt index 167f9fc..9053d30 100644 --- a/public/test.txt +++ b/public/test.txt @@ -1,9 +1,12 @@ -Hello from V-Hono static file server! - -This is a test file from the ./public directory. -You can access this file directly via the root path (/). +This is a test file for v-hono Context.file functionality. Features demonstrated: -- Text file serving +- Basic file serving - Content-Type detection -- Path mapping from URL to file system \ No newline at end of file +- Security validation +- Caching options + +This file can be accessed via: +- GET /file/test.txt +- GET /file-cached/test.txt +- GET /safe-file/test.txt \ No newline at end of file diff --git a/uploads/downloadable.txt b/uploads/downloadable.txt new file mode 100644 index 0000000..d18a6fd --- /dev/null +++ b/uploads/downloadable.txt @@ -0,0 +1,8 @@ +This is a downloadable file. + +This file demonstrates the download functionality with: +- Content-Disposition: attachment header +- Custom Content-Type +- File download behavior + +Access this file via: GET /download/downloadable.txt \ No newline at end of file