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
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user