Files
meiseayoung 4e2c6a90c3 feat: add 7 built-in middleware components inspired by Hono.js
New Middleware:
- CORS: Cross-Origin Resource Sharing with full configuration support
- Cookie Helper: Cookie management with signed cookie support (HMAC-SHA256)
- JWT: JSON Web Token authentication (HS256/384/512)
- Bearer Auth: Simple Bearer token authentication
- Compression: Gzip and deflate response compression
- Rate Limiting: Request rate limiting with customizable stores
- Validator: Schema-based request validation

Features:
- Context store for middleware data sharing (get/set/get_client_ip)
- Middleware export module with utility functions
- Comprehensive property-based tests for all middleware
- Example file demonstrating all middleware usage

Files Added:
- cors.v, cookie.v, jwt.v, bearer_auth.v, compress.v, rate_limit.v, validator.v
- middleware.v (unified exports)
- examples/middleware_demo.v
- tests/test_*.v (9 new test files)

Documentation:
- Updated README.md with detailed middleware usage guide
- Added Schema Builder API documentation
2026-01-04 11:11:15 +08:00

327 lines
7.9 KiB
V

module hono
import time
import crypto.sha256
import encoding.base64
// SameSite 枚举 - Cookie 的 SameSite 属性
pub enum SameSite {
strict
lax
none_
}
// CookieOptions 结构体 - Cookie 配置选项
pub struct CookieOptions {
pub:
path string = '/'
domain string
max_age int // 秒
expires ?time.Time
http_only bool
secure bool
same_site SameSite = .lax
}
// get_cookie - 从请求中获取指定名称的 Cookie 值
// 返回 Cookie 值,如果不存在则返回 none
pub fn get_cookie(c Context, name string) ?string {
cookie_header := c.req.header.get_custom('Cookie') or { return none }
if cookie_header.len == 0 {
return none
}
// 解析 Cookie 头
cookies := parse_cookie_header(cookie_header)
if name in cookies {
return cookies[name]
}
return none
}
// get_all_cookies - 获取请求中的所有 Cookie
// 返回一个 map,包含所有 Cookie 的名称和值
pub fn get_all_cookies(c Context) map[string]string {
cookie_header := c.req.header.get_custom('Cookie') or { return map[string]string{} }
if cookie_header.len == 0 {
return map[string]string{}
}
return parse_cookie_header(cookie_header)
}
// set_cookie - 设置 Cookie
// 将 Cookie 添加到响应的 Set-Cookie 头中
pub fn set_cookie(mut c Context, name string, value string, options ...CookieOptions) {
opts := if options.len > 0 { options[0] } else { CookieOptions{} }
cookie_str := build_cookie_string(name, value, opts)
// 添加到响应头
// 如果已经有 Set-Cookie 头,需要追加
if existing := c.headers['Set-Cookie'] {
c.headers['Set-Cookie'] = '${existing}, ${cookie_str}'
} else {
c.headers['Set-Cookie'] = cookie_str
}
}
// delete_cookie - 删除 Cookie
// 通过设置过期时间为过去的时间来删除 Cookie
pub fn delete_cookie(mut c Context, name string, options ...CookieOptions) {
mut opts := if options.len > 0 { options[0] } else { CookieOptions{} }
// 设置 max_age 为 0 或负数,并设置过期时间为过去
expired_time := time.unix(0)
// 创建新的选项,保留 path 和 domain,但设置过期
delete_opts := CookieOptions{
path: opts.path
domain: opts.domain
max_age: 0
expires: expired_time
http_only: opts.http_only
secure: opts.secure
same_site: opts.same_site
}
cookie_str := build_cookie_string(name, '', delete_opts)
if existing := c.headers['Set-Cookie'] {
c.headers['Set-Cookie'] = '${existing}, ${cookie_str}'
} else {
c.headers['Set-Cookie'] = cookie_str
}
}
// parse_cookie_header - 解析 Cookie 请求头
// 将 "name1=value1; name2=value2" 格式解析为 map
fn parse_cookie_header(header string) map[string]string {
mut cookies := map[string]string{}
// 按分号分割
pairs := header.split(';')
for pair in pairs {
trimmed := pair.trim_space()
if trimmed.len == 0 {
continue
}
// 找到第一个等号的位置
eq_pos := trimmed.index('=') or { continue }
if eq_pos == 0 {
continue
}
name := trimmed[..eq_pos].trim_space()
value := if eq_pos + 1 < trimmed.len {
trimmed[eq_pos + 1..].trim_space()
} else {
''
}
// 移除值两端的引号(如果有)
cleaned_value := if value.len >= 2 && value[0] == `"` && value[value.len - 1] == `"` {
value[1..value.len - 1]
} else {
value
}
cookies[name] = cleaned_value
}
return cookies
}
// build_cookie_string - 构建 Set-Cookie 头的值
fn build_cookie_string(name string, value string, opts CookieOptions) string {
mut parts := []string{}
// 基本的 name=value
parts << '${name}=${value}'
// Path
if opts.path.len > 0 {
parts << 'Path=${opts.path}'
}
// Domain
if opts.domain.len > 0 {
parts << 'Domain=${opts.domain}'
}
// Max-Age
if opts.max_age != 0 {
parts << 'Max-Age=${opts.max_age}'
}
// Expires
if expires := opts.expires {
// 格式化为 HTTP 日期格式: Wed, 09 Jun 2021 10:18:14 GMT
expires_str := format_cookie_date(expires)
parts << 'Expires=${expires_str}'
}
// HttpOnly
if opts.http_only {
parts << 'HttpOnly'
}
// Secure
if opts.secure {
parts << 'Secure'
}
// SameSite
match opts.same_site {
.strict { parts << 'SameSite=Strict' }
.lax { parts << 'SameSite=Lax' }
.none_ { parts << 'SameSite=None' }
}
return parts.join('; ')
}
// format_cookie_date - 格式化时间为 Cookie 日期格式
// 格式: Wed, 09 Jun 2021 10:18:14 GMT
fn format_cookie_date(t time.Time) string {
days := ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
months := ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
day_name := days[t.day_of_week()]
month_name := months[t.month - 1]
return '${day_name}, ${t.day:02} ${month_name} ${t.year} ${t.hour:02}:${t.minute:02}:${t.second:02} GMT'
}
// set_signed_cookie - 设置签名 Cookie
// 使用 HMAC-SHA256 对 Cookie 值进行签名
pub fn set_signed_cookie(mut c Context, name string, value string, secret string, options ...CookieOptions) ! {
if secret.len == 0 {
return error('Secret is required for signed cookies')
}
// 生成签名
signature := generate_hmac_signature(value, secret)
// 将值和签名组合: value.signature
signed_value := '${value}.${signature}'
opts := if options.len > 0 { options[0] } else { CookieOptions{} }
set_cookie(mut c, name, signed_value, opts)
}
// get_signed_cookie - 获取并验证签名 Cookie
// 验证签名,如果有效则返回原始值,否则返回错误
pub fn get_signed_cookie(c Context, name string, secret string) !string {
if secret.len == 0 {
return error('Secret is required for signed cookies')
}
// 获取 Cookie 值
signed_value := get_cookie(c, name) or {
return error('Cookie not found: ${name}')
}
// 分离值和签名
dot_pos := signed_value.last_index('.') or {
return error('Invalid signed cookie format')
}
if dot_pos == 0 || dot_pos >= signed_value.len - 1 {
return error('Invalid signed cookie format')
}
value := signed_value[..dot_pos]
signature := signed_value[dot_pos + 1..]
// 验证签名
expected_signature := generate_hmac_signature(value, secret)
if !constant_time_compare(signature, expected_signature) {
return error('Invalid signature')
}
return value
}
// generate_hmac_signature - 使用 HMAC-SHA256 生成签名
fn generate_hmac_signature(value string, secret string) string {
// 手动实现 HMAC-SHA256
// HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))
// 其中 K' 是处理后的密钥,ipad = 0x36, opad = 0x5c
block_size := 64 // SHA256 块大小
// 处理密钥
mut key := secret.bytes()
if key.len > block_size {
// 如果密钥太长,先哈希
hash_result := sha256.sum(key)
key = []u8{len: 32}
for i := 0; i < 32; i++ {
key[i] = hash_result[i]
}
}
// 填充到块大小
for key.len < block_size {
key << u8(0)
}
// 计算 K' ⊕ ipad
mut i_key_pad := []u8{len: block_size}
for i := 0; i < block_size; i++ {
i_key_pad[i] = key[i] ^ u8(0x36)
}
// 计算 K' ⊕ opad
mut o_key_pad := []u8{len: block_size}
for i := 0; i < block_size; i++ {
o_key_pad[i] = key[i] ^ u8(0x5c)
}
// 计算内部哈希: H((K' ⊕ ipad) || m)
mut inner_data := i_key_pad.clone()
inner_data << value.bytes()
inner_hash_result := sha256.sum(inner_data)
mut inner_hash := []u8{len: 32}
for i := 0; i < 32; i++ {
inner_hash[i] = inner_hash_result[i]
}
// 计算外部哈希: H((K' ⊕ opad) || inner_hash)
mut outer_data := o_key_pad.clone()
outer_data << inner_hash
outer_hash_result := sha256.sum(outer_data)
mut outer_hash := []u8{len: 32}
for i := 0; i < 32; i++ {
outer_hash[i] = outer_hash_result[i]
}
// Base64URL 编码(不带填充)
return base64.url_encode(outer_hash).replace('=', '')
}
// constant_time_compare - 常量时间比较,防止时序攻击
fn constant_time_compare(a string, b string) bool {
if a.len != b.len {
return false
}
mut result := u8(0)
for i := 0; i < a.len; i++ {
result |= a[i] ^ b[i]
}
return result == 0
}