diff --git a/api/api.go b/api/api.go index c1fd7b3..70501ee 100644 --- a/api/api.go +++ b/api/api.go @@ -4,7 +4,7 @@ import ( "caddydash/apic" "caddydash/config" "caddydash/db" - "caddydash/user" + "caddydash/gen" "github.com/infinite-iroha/touka" ) @@ -21,40 +21,43 @@ func ApiGroup(v0 touka.IRouter, cdb *db.ConfigDB, cfg *config.Config) { }) // 配置参数相关 - api.GET("/config/file/:filename", GetConfig(cdb)) // 读取配置(与写入一致) - api.PUT("/config/file/:filename", PutConfig(cdb, cfg)) // 写入配置 - api.DELETE("/config/file/:filename", DeleteConfig(cdb, cfg)) //删除配置 + cfgr := api.Group("/config") + { + cfgr.GET("/file/:filename", GetConfig(cdb)) // 读取配置(与写入一致) + cfgr.PUT("/file/:filename", PutConfig(cdb, cfg)) // 写入配置 + cfgr.DELETE("/file/:filename", DeleteConfig(cdb, cfg)) //删除配置 - api.GET("/config/files/params", FilesParams(cdb)) // 获取所有配置, 需进行decode - api.GET("/config/files/templates", FilesTemplates(cdb)) // 获取所有模板 - api.GET("/config/files/rendered", FilesRendered(cdb)) // 获取所有渲染产物 + cfgr.GET("/files/params", FilesParams(cdb)) // 获取所有配置, 需进行decode + cfgr.GET("/files/templates", FilesTemplates(cdb)) // 获取所有模板 + cfgr.GET("/files/rendered", FilesRendered(cdb)) // 获取所有渲染产物 - api.GET("/config/templates", GetTemplates(cdb)) // 获取可用模板名称 + cfgr.GET("/templates", GetTemplates(cdb)) // 获取可用模板名称 - api.GET("/config/headers-presets", func(c *touka.Context) { - c.JSON(200, GetHeaderSetMetadataList()) - }) + cfgr.GET("/headers-presets", func(c *touka.Context) { + c.JSON(200, GetHeaderSetMetadataList()) + }) + cfgr.GET("/headers-presets/:name", GetHeadersPreset()) - api.GET("/config/headers-presets/:name", func(c *touka.Context) { - presetName := c.Param("name") - if presetName == "" { - c.JSON(400, touka.H{"error": "presetName is required"}) - return + glbr := api.Group("/global") + { + glbr.GET("/log/levels", func(c *touka.Context) { + c.JSON(200, gen.LogLevelList) + }) + glbr.GET("/tls/providers", func(c *touka.Context) { + c.JSON(200, gen.ProviderList) + }) } - preset, found := GetHeaderSetByID(presetName) - if !found { - c.JSON(404, touka.H{"error": "preset not found"}) - return - } - c.JSON(200, preset) - }) + } // caddy实例相关 - api.POST("/caddy/stop", apic.StopCaddy()) // 无需payload - api.POST("/caddy/run", apic.StartCaddy(cfg)) - api.POST("/caddy/restart", apic.RestartCaddy(cfg)) - api.GET("/caddy/status", apic.IsCaddyRunning()) + { + api.POST("/caddy/stop", apic.StopCaddy()) // 无需payload + api.POST("/caddy/run", apic.StartCaddy(cfg)) + api.POST("/caddy/restart", apic.RestartCaddy(cfg)) + api.GET("/caddy/status", apic.IsCaddyRunning()) + } + // 鉴权相关 auth := api.Group("/auth") { auth.POST("/login", func(c *touka.Context) { @@ -66,31 +69,8 @@ func ApiGroup(v0 touka.IRouter, cdb *db.ConfigDB, cfg *config.Config) { auth.GET("/logout", func(c *touka.Context) { AuthLogout(c) }) - auth.GET("/init", func(c *touka.Context) { - // 返回是否init管理员 - isInit := user.IsAdminInit() - if isInit { - c.JSON(200, touka.H{"admin_init": true}) - } else { - c.JSON(200, touka.H{"admin_init": false}) - } - }) - auth.POST("/init", func(c *touka.Context) { - username := c.PostForm("username") - password := c.PostForm("password") - // 验证是否为空 - if username == "" || password == "" { - c.JSON(400, touka.H{"error": "username and password are required"}) - return - } - // 初始化管理员 - err := user.InitAdminUser(username, password, cdb) - if err != nil { - c.JSON(500, touka.H{"error": err.Error()}) - return - } - c.JSON(200, touka.H{"message": "admin initialized"}) - }) + auth.GET("/init", AuthInitStatus()) + auth.POST("/init", AuthInitHandle(cdb)) auth.POST("resetpwd", ResetPassword(cdb)) } } diff --git a/api/auth.go b/api/auth.go index e2c709d..152b0b9 100644 --- a/api/auth.go +++ b/api/auth.go @@ -183,3 +183,34 @@ func ResetPassword(cdb *db.ConfigDB) touka.HandlerFunc { AuthLogout(c) } } + +func AuthInitHandle(cdb *db.ConfigDB) touka.HandlerFunc { + return func(c *touka.Context) { + username := c.PostForm("username") + password := c.PostForm("password") + // 验证是否为空 + if username == "" || password == "" { + c.JSON(400, touka.H{"error": "username and password are required"}) + return + } + // 初始化管理员 + err := user.InitAdminUser(username, password, cdb) + if err != nil { + c.JSON(500, touka.H{"error": err.Error()}) + return + } + c.JSON(200, touka.H{"message": "admin initialized"}) + } +} + +func AuthInitStatus() touka.HandlerFunc { + return func(c *touka.Context) { + // 返回是否init管理员 + isInit := user.IsAdminInit() + if isInit { + c.JSON(200, touka.H{"admin_init": true}) + } else { + c.JSON(200, touka.H{"admin_init": false}) + } + } +} diff --git a/api/config.go b/api/config.go index 03d5e6f..c58b5b1 100644 --- a/api/config.go +++ b/api/config.go @@ -119,107 +119,18 @@ func DeleteConfig(cdb *db.ConfigDB, cfg *config.Config) touka.HandlerFunc { } } -/* -func PutConfig(cdb *db.ConfigDB, cfg *config.Config) touka.HandlerFunc { +func GetHeadersPreset() touka.HandlerFunc { return func(c *touka.Context) { - filename := c.Param("filename") - var config gen.CaddyUniConfig - err := c.ShouldBindJSON(&config) - if err != nil { - c.JSON(500, touka.H{"error": err.Error()}) + presetName := c.Param("name") + if presetName == "" { + c.JSON(400, touka.H{"error": "presetName is required"}) return } - var ( - paramsGOB []byte - paramsOrigin []byte - ) - switch config.TmplType { - case "file_server": - caddyfscfg := gen.CaddyFileServerConfig{ - Domain: config.Domain, - FileDirPath: config.FileServer.FileDirPath, - EnableBrowser: config.FileServer.EnableBrowser, - Headers: gen.HeadersMapToHeadersUp(config.Headers), - EnableLog: config.Log.EnableLog, - LogDomain: config.Log.LogDomain, - EnableErrorPage: config.ErrorPage.EnableErrorPage, - EnableEncode: config.Encode.EnableEncode, - } - paramsGOB, err = gen.EncodeGobConfig(caddyfscfg) - if err != nil { - c.Warnf("encode gob config error: %v", err) - c.JSON(500, touka.H{"error": err.Error()}) - return - } - // 把json变为gob []byte - paramsOrigin, err = gen.EncodeGobConfig(config) - if err != nil { - c.Warnf("encode origin config error: %v", err) - c.JSON(500, touka.H{"error": err.Error()}) - return - } - paramsEntry := db.ParamsEntry{ - Filename: filename, - TemplateType: config.TmplType, - ParamsGOB: paramsGOB, - ParamsOrigin: paramsOrigin, - } - - err = WriteConfig(cdb, paramsEntry, cfg, filename) - if err != nil { - c.Warnf("write config error: %v", err) - c.JSON(500, touka.H{"error": err.Error()}) - return - } - - c.JSON(200, touka.H{"message": "config saved and rendered"}) - return - - case "reverse_proxy": - caddyrpCfg := gen.CaddyReverseProxyConfig{ - Domain: config.Domain, - ReverseProxy: config.UpStream.UpStream, - Headers: gen.HeadersMapToHeadersUp(config.Headers), - HeadersUp: gen.HeadersMapToHeadersUp(config.UpStream.UpStreamHeaders), - EnableLog: config.Log.EnableLog, - LogDomain: config.Log.LogDomain, - EnableErrorPage: config.ErrorPage.EnableErrorPage, - EnableEncode: config.Encode.EnableEncode, - } - paramsGOB, err = gen.EncodeGobConfig(caddyrpCfg) - if err != nil { - c.Warnf("encode gob config error: %v", err) - c.JSON(500, touka.H{"error": err.Error()}) - return - } - // 把json变为gob []byte - paramsOrigin, err = gen.EncodeGobConfig(config) - if err != nil { - c.Warnf("encode origin config error: %v", err) - c.JSON(500, touka.H{"error": err.Error()}) - return - } - paramsEntry := db.ParamsEntry{ - Filename: filename, - TemplateType: config.TmplType, - ParamsGOB: paramsGOB, - ParamsOrigin: paramsOrigin, - } - - err = WriteConfig(cdb, paramsEntry, cfg, filename) - if err != nil { - c.Warnf("write config error: %v", err) - c.JSON(500, touka.H{"error": err.Error()}) - return - } - - c.JSON(200, touka.H{"message": "config saved and rendered"}) - return - - default: - c.JSON(500, touka.H{"error": "unknown template type"}) + preset, found := GetHeaderSetByID(presetName) + if !found { + c.JSON(404, touka.H{"error": "preset not found"}) return } + c.JSON(200, preset) } } -*/ diff --git a/api/global.go b/api/global.go new file mode 100644 index 0000000..778f64e --- /dev/null +++ b/api/global.go @@ -0,0 +1 @@ +package api diff --git a/gen/struct.go b/gen/struct.go index 127cdad..134a3ad 100644 --- a/gen/struct.go +++ b/gen/struct.go @@ -1,29 +1,5 @@ package gen -/* -type CaddyReverseProxyConfig struct { - Domain string // 域名; 例如 example.com - ReverseProxy string // 反向代理目标; 例如 127.0.0.1:8080 (这里简化为单个目标) - Headers []string // 自定义响应Header - HeadersUp []string // 自定义请求头列表; 例如 ["XXX0 XX", "XXX1 XXX"] - EnableLog bool // 是否导入 log 指令 - LogDomain string // log 指令的域名参数 - EnableErrorPage bool // 是否导入 error_page 指令 - EnableEncode bool // 是否导入 encode 指令 -} - -type CaddyFileServerConfig struct { - Domain string // 域名; 例如 example.com - FileDirPath string // 文件目录 - EnableBrowser bool // 是否导入 browse 指令 - Headers []string // - EnableLog bool // 是否导入 log 指令 - LogDomain string // log 指令的域名参数 - EnableErrorPage bool // 是否导入 error_page 指令 - EnableEncode bool // 是否导入 encode 指令 -} -*/ - func HeadersMapToHeadersUp(headers map[string][]string) []string { var headersUp []string for key, values := range headers { @@ -77,3 +53,46 @@ type CaddyUniErrorPageConfig struct { type CaddyUniEncodeConfig struct { EnableEncode bool `json:"enable_encode"` } + +type CaddyGlobalConfig struct { + Debug bool `json:"debug"` + PortsConfig CaddyGlobalPortsConfig `json:"ports_config"` + Metrics bool `json:"metrics"` + LogConfig CaddyGlobalLogConfig `json:"log_config"` + TLSConfig CaddyGlobalTLSConfig `json:"tls_config"` +} + +type CaddyGlobalPortsConfig struct { + AdminPort string `json:"admin_port"` + HTTPPort uint16 `json:"http_port"` + HTTPSPort uint16 `json:"https_port"` +} + +type CaddyGlobalLogConfig struct { + Level string `json:"level"` + // 日志滚动配置 + RotateSize string `json:"rotate_size"` + RotateKeep string `json:"rotate_keep"` + RotateKeepForTime string `json:"rotate_keep_for_time"` +} + +// 维护一个日志等级列表 +// Possible levels: DEBUG, INFO, WARN, ERROR, PANIC, and FATAL +var LogLevelList = map[string]struct{}{ + "DEBUG": {}, + "INFO": {}, + "WARN": {}, + "ERROR": {}, + "PANIC": {}, + "FATAL": {}, +} + +type CaddyGlobalTLSConfig struct { + Provider string `json:"provider"` + Token string `json:"token"` +} + +// 维护一个提供商列表 +var ProviderList = map[string]struct{}{ + "cloudflare": {}, +} diff --git a/go.mod b/go.mod index 992692b..ab2d4fa 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,13 @@ require ( github.com/fenthope/record v0.0.3 github.com/fenthope/sessions v0.0.1 github.com/infinite-iroha/touka v0.2.2 - golang.org/x/crypto v0.37.0 + golang.org/x/crypto v0.39.0 modernc.org/sqlite v1.38.0 ) require ( github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect - github.com/WJQSERVER-STUDIO/httpc v0.7.0 // indirect + github.com/WJQSERVER-STUDIO/httpc v0.7.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fenthope/reco v0.0.3 // indirect github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 // indirect @@ -25,7 +25,7 @@ require ( github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/sys v0.33.0 // indirect modernc.org/libc v1.65.10 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/go.sum b/go.sum index 1974c0d..20f4580 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc= -github.com/WJQSERVER-STUDIO/httpc v0.7.0 h1:iHhqlxppJBjlmvsIjvLZKRbWXqSdbeSGGofjHGmqGJc= -github.com/WJQSERVER-STUDIO/httpc v0.7.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +github.com/WJQSERVER-STUDIO/httpc v0.7.1 h1:D3NlfY52pwKIOSzkdRrLinUynyKELrcPZEO8QjlBq2M= +github.com/WJQSERVER-STUDIO/httpc v0.7.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fenthope/reco v0.0.3 h1:RmnQ0D9a8PWtwOODawitTe4BztTnS9wYwrDbipISNq4= @@ -34,10 +34,10 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= -golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= -golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= -golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= diff --git a/gtmpl/caddyfile b/gtmpl/caddyfile new file mode 100644 index 0000000..e006a0d --- /dev/null +++ b/gtmpl/caddyfile @@ -0,0 +1,95 @@ +{ + debug + admin :2019 + http_port 80 + https_port 443 + metrics + order ja4h_header first + order webdav before file_server + order cache before rewrite + cache { + cache_name CaddyCache + } + log { + level INFO + output file ./log/caddy.log { + roll_size 10MB + roll_keep 10 + } + } +} + +(log) { + log { + format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` { + time_format "02/Jan/2006:15:04:05 -0700" + } + output file ./log/{args[0]}/access.log { + roll_size 10MB + roll_keep 10 + roll_keep_for 24h + } + } +} + +(error_page) { + handle_errors { + rewrite * /{err.status_code}.html + root * ./pages/errors + file_server + } +} + +(encode) { + encode { + zstd + br + gzip + minimum_length 512 + } +} + +(cache) { + cache { + allowed_http_verbs GET + stale {args[0]} + ttl {args[1]} + } +} + +(header_realip_cf) { + header_up X-Real-IP {remote_host} + header_up X-Real-IP {http.request.header.CF-Connecting-IP} + header_up X-Forwarded-For {http.request.header.CF-Connecting-IP} + header_up X-Forwarded-Proto {http.request.header.CF-Visitor} +} + +(header_realip) { + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} +} + +(tls) { + tls { + dns {args[0]} {args[1]} + } +} + +(rate_limit) { + route /* { + rate_limit {remote.ip} {args[0]}r/m 10000 429 + } +} + +(route_nocache) { + route {args[0]} { + rate_limit {remote.ip} {args[1]}r/m 10000 429 + cache { + stale 0s + ttl 0s + } + } +} + +import ./config.d/* diff --git a/tmpl/uni b/tmpl/uni index 78b7863..22f03ef 100644 --- a/tmpl/uni +++ b/tmpl/uni @@ -1,42 +1,42 @@ {{- if .DomainConfig.MutiDomains -}} - {{- range $i, $domain := .DomainConfig.Domains -}} - {{- if $i}} {{" "}}{{- end -}} - {{- . -}} - {{- end -}} { + {{- range $i, $domain := .DomainConfig.Domains -}} + {{- if $i}} {{" "}}{{- end -}} + {{- . -}} + {{- end -}} { {{- else -}} - {{- .DomainConfig.Domain}} { + {{- .DomainConfig.Domain}} { {{- end -}} - {{- if .Upstream.EnableUpStream}} - reverse_proxy { - to{{if .Upstream.MutiUpStreams}}{{range .Upstream.UpStreams}} {{.}}{{end}}{{else}} {{.Upstream.UpStream}}{{end}} + {{- if .Upstream.EnableUpStream}} + reverse_proxy { + to{{if .Upstream.MutiUpStreams}}{{range .Upstream.UpStreams}} {{.}}{{end}}{{else}} {{.Upstream.UpStream}}{{end}} - {{- range $key, $values := .Upstream.UpStreamHeaders}} - {{- range $values}} - header_up {{$key}} "{{.}}" - {{- end}} - {{- end}} - } - {{- else if .FileServer.EnableFileServer}} - root * {{.FileServer.FileDirPath}} - file_server{{if .FileServer.EnableBrowser}} browse{{end}} - {{- end}} + {{- range $key, $values := .Upstream.UpStreamHeaders}} + {{- range $values}} + header_up {{$key}} "{{.}}" + {{- end}} + {{- end}} + } + {{- else if .FileServer.EnableFileServer}} + root * {{.FileServer.FileDirPath}} + file_server{{if .FileServer.EnableBrowser}} browse{{end}} + {{- end}} - {{- range $key, $values := .Headers}} - {{- range $values}} - header {{$key}} "{{.}}" - {{- end}} - {{- end}} + {{- range $key, $values := .Headers}} + {{- range $values}} + header {{$key}} "{{.}}" + {{- end}} + {{- end}} - {{- if .Log.EnableLog}} - import log {{.Log.LogDomain}} - {{- end}} + {{- if .Log.EnableLog}} + import log {{.Log.LogDomain}} + {{- end}} - {{- if .ErrorPage.EnableErrorPage}} - import error_page - {{- end}} + {{- if .ErrorPage.EnableErrorPage}} + import error_page + {{- end}} - {{- if .Encode.EnableEncode}} - import encode - {{- end}} + {{- if .Encode.EnableEncode}} + import encode + {{- end}} } \ No newline at end of file