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:
@@ -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",
|
||||
|
||||
@@ -437,6 +437,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
// Gin 同一前缀只能有一种通配符名;管理端已用 /sites/:id/*,公开列表必须同名。
|
||||
api.GET("/distribution/sites/:id/listings", distributionHandler.ListPublicSiteListings)
|
||||
api.GET("/distribution/site-info", distributionHandler.GetPublicSiteInfo)
|
||||
api.GET("/distribution/check-domain", distributionHandler.CheckDomain)
|
||||
api.POST("/payments/leshua/notify", paymentHandler.LeshuaNotify)
|
||||
api.POST("/payments/lakala/notify", paymentHandler.LakalaNotify)
|
||||
api.POST("/payments/shuncheng/notify", paymentHandler.ShunchengNotify)
|
||||
|
||||
+23
-6
@@ -1,12 +1,14 @@
|
||||
{
|
||||
email {$CADDY_EMAIL}
|
||||
on_demand_tls {
|
||||
ask http://backend:8080/api/distribution/check-domain
|
||||
}
|
||||
}
|
||||
|
||||
# 反代到后端时,用 Caddy 实测到的对端地址覆盖 X-Real-IP,
|
||||
# 防止客户端伪造 X-Forwarded-For 影响限流、审计和支付下单的 client_ip。
|
||||
(backend_proxy) {
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
}
|
||||
|
||||
# 公开 API 反代片段:主站与选号网共用。
|
||||
@@ -45,9 +47,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
# 主站与过渡子域名共享同一套配置:域名迁移期间新旧域名同时可访问,
|
||||
# 避免 DNS 缓存未生效导致部分用户打不开。迁移完成后可移除 h5.{$CADDY_DOMAIN}。
|
||||
{$CADDY_DOMAIN}, h5.{$CADDY_DOMAIN} {
|
||||
# 前台商城静态资源与安全头配置(主站与所有分销站点共享):
|
||||
# 前端通过 window.location.hostname 自动向 /api/distribution/site-info 请求分销配置。
|
||||
(storefront) {
|
||||
encode zstd gzip
|
||||
|
||||
header {
|
||||
@@ -67,7 +69,7 @@
|
||||
respond 404
|
||||
}
|
||||
|
||||
# 其余流量返回主站前端静态资源。
|
||||
# 其余流量返回主站/分销前台前端静态资源。
|
||||
handle {
|
||||
root * /srv/dist
|
||||
try_files {path} /index.html
|
||||
@@ -75,6 +77,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
# 主站与过渡子域名共享同一套配置:域名迁移期间新旧域名同时可访问,
|
||||
# 避免 DNS 缓存未生效导致部分用户打不开。迁移完成后可移除 h5.{$CADDY_DOMAIN}。
|
||||
{$CADDY_DOMAIN}, h5.{$CADDY_DOMAIN} {
|
||||
import storefront
|
||||
}
|
||||
|
||||
# 分销管理后台使用独立 React + Ant Design 静态资源,API 与主站复用 backend。
|
||||
distribution.{$CADDY_DOMAIN} {
|
||||
encode zstd gzip
|
||||
@@ -107,5 +115,14 @@ monitor.{$CADDY_DOMAIN} {
|
||||
|
||||
# gitea 自托管代码库
|
||||
gitea.hao858.com {
|
||||
reverse_proxy gitea:3000
|
||||
reverse_proxy gitea:3000
|
||||
}
|
||||
|
||||
# 分销站点动态域名入口:
|
||||
# 支持任意二级域名及分销商独立域名,首次访问时通过 on_demand_tls 触发后端白名单校验并自动签发 SSL 证书。
|
||||
https:// {
|
||||
tls {
|
||||
on_demand
|
||||
}
|
||||
import storefront
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user