feat(vhost): 增强 server_name 匹配支持通配符和正则

按 nginx server_name 规则实现多类型匹配:
- 精确匹配:example.com
- 前缀通配:*.example.com(最长匹配优先)
- 后缀通配:example.*
- 正则匹配:~regex(按配置顺序)
- 匹配优先级:精确 → 前缀通配 → 后缀通配 → 正则 → 默认

更新 AddHost 返回 error 以支持正则编译错误反馈

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-17 09:26:47 +08:00
parent 359afb5e24
commit c418baeb90
2 changed files with 151 additions and 20 deletions

View File

@ -17,6 +17,10 @@
package server
import (
"fmt"
"regexp"
"strings"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/netutil"
)
@ -25,14 +29,30 @@ import (
//
// 管理多个虚拟主机,根据请求的 Host 头分发到对应的处理器。
// 支持默认主机作为未匹配请求的 fallback。
// 支持精确匹配、前缀通配(*.example.com、后缀通配example.*)和正则匹配。
type VHostManager struct {
// hosts 虚拟主机映射,按 server name 索引
// 精确匹配
hosts map[string]*VirtualHost
// 前缀通配 - suffix mapO(1) 查找)
wildcardSuffixMap map[string]*VirtualHost // suffix -> vhost
// 后缀通配 - TLD map
wildcardTLDMap map[string]*VirtualHost // TLD -> vhost
// 正则匹配
regexHosts []*RegexHostMatcher
// defaultHost 默认主机,处理未匹配的 Host 头请求
defaultHost *VirtualHost
}
// RegexHostMatcher 正则主机匹配器。
type RegexHostMatcher struct {
pattern *regexp.Regexp
vhost *VirtualHost
}
// VirtualHost 虚拟主机。
//
// 代表一个虚拟主机配置,包含名称和对应的请求处理器。
@ -50,19 +70,66 @@ type VirtualHost struct {
// - *VHostManager: 新创建的管理器实例
func NewVHostManager() *VHostManager {
return &VHostManager{
hosts: make(map[string]*VirtualHost),
hosts: make(map[string]*VirtualHost),
wildcardSuffixMap: make(map[string]*VirtualHost),
wildcardTLDMap: make(map[string]*VirtualHost),
regexHosts: make([]*RegexHostMatcher, 0),
}
}
// AddHost 添加虚拟主机。
//
// 支持以下 server_name 格式:
// - 精确匹配: "example.com"
// - 前缀通配: "*.example.com"(匹配任意子域名)
// - 后缀通配: "example.*"(匹配任意 TLD
// - 正则匹配: "~regex"(以 ~ 开头,后面是正则表达式)
//
// 参数:
// - name: 虚拟主机名称(域名)
// - handler: 请求处理器
func (v *VHostManager) AddHost(name string, handler fasthttp.RequestHandler) {
v.hosts[name] = &VirtualHost{
name: name,
handler: handler,
//
// 返回值:
// - error: 正则表达式无效时返回错误
func (v *VHostManager) AddHost(name string, handler fasthttp.RequestHandler) error {
if strings.HasPrefix(name, "~") {
// 正则匹配
pattern := name[1:]
re, err := regexp.Compile(pattern)
if err != nil {
return fmt.Errorf("invalid regex pattern: %w", err)
}
v.regexHosts = append(v.regexHosts, &RegexHostMatcher{
pattern: re,
vhost: &VirtualHost{
name: name,
handler: handler,
},
})
return nil
} else if strings.HasPrefix(name, "*.") {
// 前缀通配 *.example.com
suffix := name[2:]
v.wildcardSuffixMap[suffix] = &VirtualHost{
name: name,
handler: handler,
}
return nil
} else if strings.HasSuffix(name, ".*") {
// 后缀通配 example.*
tld := name[:len(name)-2]
v.wildcardTLDMap[tld] = &VirtualHost{
name: name,
handler: handler,
}
return nil
} else {
// 精确匹配
v.hosts[name] = &VirtualHost{
name: name,
handler: handler,
}
return nil
}
}
@ -77,6 +144,72 @@ func (v *VHostManager) SetDefault(handler fasthttp.RequestHandler) {
}
}
// findLongestWildcardPrefix 查找最长的通配符前缀匹配。
//
// 按 nginx 规则,从最长子域名开始匹配,例如:
// "a.b.example.com" 优先匹配 "*.b.example.com",其次 "*.example.com"。
//
// 参数:
// - host: 主机名
//
// 返回值:
// - *VirtualHost: 匹配的虚拟主机,未匹配返回 nil
func (v *VHostManager) findLongestWildcardPrefix(host string) *VirtualHost {
parts := strings.Split(host, ".")
for i := 1; i < len(parts); i++ {
suffix := strings.Join(parts[i:], ".")
if vhost, ok := v.wildcardSuffixMap[suffix]; ok {
return vhost
}
}
return nil
}
// FindHost 根据主机名查找虚拟主机。
//
// 匹配优先级nginx server_name 规则):
// 1. 精确匹配
// 2. 最长前缀通配(*.example.com
// 3. 后缀通配example.*
// 4. 正则匹配(按配置顺序)
// 5. 默认主机
//
// 参数:
// - host: 主机名
//
// 返回值:
// - *VirtualHost: 匹配的虚拟主机
func (v *VHostManager) FindHost(host string) *VirtualHost {
// 1. 精确匹配
if vhost, ok := v.hosts[host]; ok {
return vhost
}
// 2. 最长前缀通配 *.example.com
if vhost := v.findLongestWildcardPrefix(host); vhost != nil {
return vhost
}
// 3. 后缀通配 example.*
parts := strings.Split(host, ".")
if len(parts) >= 2 {
tld := parts[0]
if vhost, ok := v.wildcardTLDMap[tld]; ok {
return vhost
}
}
// 4. 正则匹配(按配置顺序)
for _, m := range v.regexHosts {
if m.pattern.MatchString(host) {
return m.vhost
}
}
// 5. 默认主机
return v.defaultHost
}
// Handler 返回虚拟主机选择器。
//
// 返回值:
@ -85,10 +218,8 @@ func (v *VHostManager) Handler() fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
host := netutil.StripPort(string(ctx.Host()))
if vhost, ok := v.hosts[host]; ok {
if vhost := v.FindHost(host); vhost != nil {
vhost.handler(ctx)
} else if v.defaultHost != nil {
v.defaultHost.handler(ctx)
} else {
ctx.Error("Host not found", fasthttp.StatusNotFound)
}

View File

@ -21,7 +21,7 @@ func TestVHostManager_Handler(t *testing.T) {
t.Run("匹配已知主机", func(t *testing.T) {
manager := NewVHostManager()
hostCalled := false
manager.AddHost("example.com", mockHandler("example", &hostCalled))
_ = manager.AddHost("example.com", mockHandler("example", &hostCalled))
handler := manager.Handler()
ctx := &fasthttp.RequestCtx{}
@ -40,7 +40,7 @@ func TestVHostManager_Handler(t *testing.T) {
t.Run("匹配带端口的主机", func(t *testing.T) {
manager := NewVHostManager()
hostCalled := false
manager.AddHost("example.com", mockHandler("example", &hostCalled))
_ = manager.AddHost("example.com", mockHandler("example", &hostCalled))
handler := manager.Handler()
ctx := &fasthttp.RequestCtx{}
@ -60,7 +60,7 @@ func TestVHostManager_Handler(t *testing.T) {
manager := NewVHostManager()
exampleCalled := false
defaultCalled := false
manager.AddHost("example.com", mockHandler("example", &exampleCalled))
_ = manager.AddHost("example.com", mockHandler("example", &exampleCalled))
manager.SetDefault(mockHandler("default", &defaultCalled))
handler := manager.Handler()
@ -83,7 +83,7 @@ func TestVHostManager_Handler(t *testing.T) {
t.Run("无匹配无默认返回404", func(t *testing.T) {
manager := NewVHostManager()
exampleCalled := false
manager.AddHost("example.com", mockHandler("example", &exampleCalled))
_ = manager.AddHost("example.com", mockHandler("example", &exampleCalled))
handler := manager.Handler()
ctx := &fasthttp.RequestCtx{}
@ -102,7 +102,7 @@ func TestVHostManager_Handler(t *testing.T) {
t.Run("IPv6地址Host", func(t *testing.T) {
manager := NewVHostManager()
ipv6Called := false
manager.AddHost("[::1]", mockHandler("ipv6", &ipv6Called))
_ = manager.AddHost("[::1]", mockHandler("ipv6", &ipv6Called))
handler := manager.Handler()
ctx := &fasthttp.RequestCtx{}
@ -157,7 +157,7 @@ func TestVHostManager_AddHost(t *testing.T) {
t.Run("添加单个主机", func(t *testing.T) {
manager := NewVHostManager()
called := false
manager.AddHost("test.com", mockHandler("test", &called))
_ = manager.AddHost("test.com", mockHandler("test", &called))
handler := manager.Handler()
ctx := &fasthttp.RequestCtx{}
@ -174,8 +174,8 @@ func TestVHostManager_AddHost(t *testing.T) {
manager := NewVHostManager()
host1Called := false
host2Called := false
manager.AddHost("host1.com", mockHandler("host1", &host1Called))
manager.AddHost("host2.com", mockHandler("host2", &host2Called))
_ = manager.AddHost("host1.com", mockHandler("host1", &host1Called))
_ = manager.AddHost("host2.com", mockHandler("host2", &host2Called))
handler := manager.Handler()
@ -200,8 +200,8 @@ func TestVHostManager_AddHost(t *testing.T) {
manager := NewVHostManager()
firstCalled := false
secondCalled := false
manager.AddHost("test.com", mockHandler("first", &firstCalled))
manager.AddHost("test.com", mockHandler("second", &secondCalled))
_ = manager.AddHost("test.com", mockHandler("first", &firstCalled))
_ = manager.AddHost("test.com", mockHandler("second", &secondCalled))
handler := manager.Handler()
ctx := &fasthttp.RequestCtx{}
@ -281,7 +281,7 @@ func TestVHostManager_PortStripping(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
manager := NewVHostManager()
called := false
manager.AddHost(tt.expected, mockHandler("matched", &called))
_ = manager.AddHost(tt.expected, mockHandler("matched", &called))
handler := manager.Handler()
ctx := &fasthttp.RequestCtx{}