feat(caddy): 支持分销站点独立域名 On-Demand TLS 自动签发 SSL 证书

- 后端新增 GET /api/distribution/check-domain 作为 Caddy on_demand_tls ask 白名单端点
- 补充 CheckDomain 相关场景自动化测试
- Caddyfile 启用 on_demand_tls 并配置动态分销前台入口
This commit is contained in:
yml2213
2026-09-16 14:20:44 +08:00
parent dc13b5dc82
commit 3465475756
4 changed files with 68 additions and 6 deletions
@@ -378,6 +378,27 @@ func (h *Handler) GetPublicSiteInfo(c *gin.Context) {
response.OK(c, info)
}
// CheckDomain 供网关(如 Caddy on_demand_tls ask)在按需申请 SSL 证书时校验域名白名单。
// 仅当域名属于系统中正常运行(active)的分销站点时返回 200,其余情况返回 404,
// 防止任意外部未授权域名解析到该服务器刷爆 Let's Encrypt 证书申请配额。
func (h *Handler) CheckDomain(c *gin.Context) {
domain := strings.ToLower(strings.TrimSpace(c.Query("domain")))
if domain == "" {
c.String(http.StatusBadRequest, "domain required")
return
}
if colon := strings.Index(domain, ":"); colon != -1 {
domain = domain[:colon]
}
info, err := h.service.GetPublicSiteInfo(c.Request.Context(), "", domain)
if err != nil || info == nil || info.Status != "active" {
c.String(http.StatusNotFound, "domain not allowed")
return
}
c.String(http.StatusOK, "ok")
}
func (h *Handler) ListPaymentChannels(c *gin.Context) {
siteID := parseQueryUint(c, "site_id")
if siteID <= 0 {
@@ -55,6 +55,7 @@ func setupHandlerTestEnv(t *testing.T) *handlerTestEnv {
pub := r.Group("/api/v1/distribution")
{
pub.GET("/site-info", h.GetPublicSiteInfo)
pub.GET("/check-domain", h.CheckDomain)
pub.GET("/sites/:id/listings", h.ListPublicSiteListings)
pub.POST("/auth/login", h.Login)
}
@@ -187,6 +188,28 @@ func TestHandler_PublicEndpoints(t *testing.T) {
t.Fatalf("expected 404 for not found site, got %d: %s", w4.Code, w4.Body.String())
}
// 1.5 CheckDomain 验证(Caddy on_demand_tls ask 接口)
// 活跃站点域名返回 200 "ok"
w5 := env.request("GET", "/api/v1/distribution/check-domain?domain=pub.example.com", nil, 0, nil)
if w5.Code != http.StatusOK || w5.Body.String() != "ok" {
t.Fatalf("expected 200 ok, got %d: %s", w5.Code, w5.Body.String())
}
// 带端口号时正常截断并校验通过
w5Port := env.request("GET", "/api/v1/distribution/check-domain?domain=pub.example.com:443", nil, 0, nil)
if w5Port.Code != http.StatusOK || w5Port.Body.String() != "ok" {
t.Fatalf("expected 200 ok with port, got %d: %s", w5Port.Code, w5Port.Body.String())
}
// 未绑定或不存在的域名返回 404
w6 := env.request("GET", "/api/v1/distribution/check-domain?domain=unregistered.com", nil, 0, nil)
if w6.Code != http.StatusNotFound {
t.Fatalf("expected 404 for unregistered domain, got %d: %s", w6.Code, w6.Body.String())
}
// 空域名返回 400
w7 := env.request("GET", "/api/v1/distribution/check-domain", nil, 0, nil)
if w7.Code != http.StatusBadRequest {
t.Fatalf("expected 400 for empty domain, got %d: %s", w7.Code, w7.Body.String())
}
// 2. ListPublicSiteListings 验证
acc := model.GameAccount{
Title: "极品号-1", GameName: "cfhd", HafCoinAmount: 50_000_000, Status: "active",