diff --git a/.forgejo/workflows/build.yml b/.github/workflows/build.yml similarity index 95% rename from .forgejo/workflows/build.yml rename to .github/workflows/build.yml index 5b36e39..5f74e20 100644 --- a/.forgejo/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,9 +10,7 @@ on: jobs: prepare: - runs-on: docker - container: - image: ubuntu:latest + runs-on: ubuntu:latest steps: - uses: actions/checkout@v4 with: @@ -41,9 +39,7 @@ jobs: export PATH: $PATH:/usr/local/go/bin build: - runs-on: docker - container: - image: ubuntu:latest + runs-on: ubuntu:latest needs: prepare # 确保这个作业在 prepare 作业完成后运行 strategy: matrix: diff --git a/Caddyfile b/Caddyfile index 490851a..2dbe81a 100644 --- a/Caddyfile +++ b/Caddyfile @@ -57,13 +57,19 @@ } } -(header_realip) { +(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]} @@ -86,5 +92,4 @@ } } -import ./config.d/* -#import ./config/* \ No newline at end of file +import ./config.d/* \ No newline at end of file diff --git a/api/api.go b/api/api.go index b9b7d7f..c1fd7b3 100644 --- a/api/api.go +++ b/api/api.go @@ -31,6 +31,24 @@ func ApiGroup(v0 touka.IRouter, cdb *db.ConfigDB, cfg *config.Config) { api.GET("/config/templates", GetTemplates(cdb)) // 获取可用模板名称 + api.GET("/config/headers-presets", func(c *touka.Context) { + c.JSON(200, GetHeaderSetMetadataList()) + }) + + 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 + } + 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)) @@ -46,10 +64,7 @@ func ApiGroup(v0 touka.IRouter, cdb *db.ConfigDB, cfg *config.Config) { AuthLogout(c) }) auth.GET("/logout", func(c *touka.Context) { - // 重定向到/ - c.Redirect(302, "/") - c.Abort() - return + AuthLogout(c) }) auth.GET("/init", func(c *touka.Context) { // 返回是否init管理员 @@ -76,6 +91,7 @@ func ApiGroup(v0 touka.IRouter, cdb *db.ConfigDB, cfg *config.Config) { } c.JSON(200, touka.H{"message": "admin initialized"}) }) + auth.POST("resetpwd", ResetPassword(cdb)) } } diff --git a/api/auth.go b/api/auth.go index 8ffc8b6..e2c709d 100644 --- a/api/auth.go +++ b/api/auth.go @@ -13,12 +13,13 @@ import ( var ( exactMatchPaths = map[string]struct{}{ - "/login": {}, - "/login.html": {}, - "/v0/api/auth/login": {}, - "/v0/api/auth/init": {}, - "/init.html": {}, - "/favicon.ico": {}, + "/login": {}, + "/login.html": {}, + "/v0/api/auth/login": {}, + "/v0/api/auth/logout": {}, + "/v0/api/auth/init": {}, + "/init.html": {}, + "/favicon.ico": {}, } prefixMatchPaths = []string{ // 保持前缀匹配,因为数量少 "/js/", @@ -98,6 +99,7 @@ func AuthLogin(c *touka.Context, cfg *config.Config, cdb *db.ConfigDB) { c.JSON(http.StatusBadRequest, touka.H{"error": "Need username and password"}) return } + c.Infof("user login: %s password: %s", username, password) // 验证账户密码 pass, err := user.CheckLogin(username, password, cdb) @@ -119,9 +121,65 @@ func AuthLogin(c *touka.Context, cfg *config.Config, cdb *db.ConfigDB) { } func AuthLogout(c *touka.Context) { + session := sessions.Default(c) - session.Clear() session.Set("authenticated", false) - session.Save() + session.Clear() + err := session.Save() + if err != nil { + c.Errorf("Failed to save session: %v", err) + c.JSON(http.StatusInternalServerError, touka.H{"error": "Failed to save session"}) + return + } c.Redirect(http.StatusFound, "/login.html") + +} + +func ResetPassword(cdb *db.ConfigDB) touka.HandlerFunc { + return func(c *touka.Context) { + username := c.PostForm("username") + oldPassword := c.PostForm("old_password") + newPassword := c.PostForm("new_password") + // 验证是否为空 + if username == "" || oldPassword == "" || newPassword == "" { + c.JSON(400, touka.H{"error": "username and password are required"}) + return + } + // 验证用户是否存在 + exist, err := cdb.IsUserExists(username) + if err != nil { + c.JSON(500, touka.H{"error": err.Error()}) + return + } + if !exist { + //不正确的参数 + c.JSON(400, touka.H{"error": "user not exist"}) + return + } + // 是否可以重置 + ok, err := user.CheckLogin(username, oldPassword, cdb) + if err != nil { + c.JSON(500, touka.H{"error": err.Error()}) + return + } + if !ok { + // 错误的密码 + c.JSON(400, touka.H{"error": "current password is not correct"}) + return + } + // 更新密码 + hashpwd, err := user.HashPassword(newPassword) + if err != nil { + c.Errorf("Failed to hash password: %v", err) + c.JSON(500, touka.H{"error": err.Error()}) + return + } + err = cdb.UpdateUserPassword(username, hashpwd) + if err != nil { + c.JSON(500, touka.H{"error": err.Error()}) + return + } + // 进行logout + AuthLogout(c) + } } diff --git a/api/headers_presets.go b/api/headers_presets.go new file mode 100644 index 0000000..5cf3645 --- /dev/null +++ b/api/headers_presets.go @@ -0,0 +1,112 @@ +package api + +// HeaderSet 定义了一个可复用的、完整的HTTP头预设。 +// JSON标签用于API响应的序列化。 +type HeaderSet struct { + ID string `json:"id"` // 唯一ID, e.g., "real_ip_cloudflare" + Name string `json:"name"` // UI上显示的名称 (为简化, 此处不使用i18n key) + Description string `json:"description"` // UI上的提示文本 + Target string `json:"target"` // 目标: "global" 或 "upstream" + Headers map[string][]string `json:"headers"` // 预设的请求头键值对 +} + +// HeaderSetMetadata 是HeaderSet的轻量级版本, 用于在列表中显示, 不包含具体的Headers数据。 +type HeaderSetMetadata struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Target string `json:"target"` +} + +// registry 是一个私有变量, 作为所有Header预设的“数据库”。 +// 在这里添加、修改或删除预设。 +var registry = []HeaderSet{ + { + ID: "real_ip_cloudflare", + Name: "Cloudflare 真实IP", + Description: "添加从Cloudflare获取真实客户端IP所需的请求头。适用于通过Cloudflare代理的流量。", + Target: "upstream", // 此预设仅适用于上游请求头 + Headers: map[string][]string{ + "X-Forwarded-For": {"{http.request.header.CF-Connecting-IP}"}, + "X-Forwarded-Proto": {"{http.request.header.CF-Visitor}"}, + "X-Real-IP": {"{http.request.header.CF-Connecting-IP}"}, + }, + }, + // 真实IP-直接 + { + ID: "real_ip_direct", + Name: "真实IP (直接)", + Description: "当Caddy直接暴露在互联网上时, 使用此预设获取客户端真实IP。", + Target: "upstream", + Headers: map[string][]string{ + "X-Forwarded-For": {"{http.request.remote.host}"}, + "X-Forwarded-Proto": {"{http.request.scheme}"}, + "X-Real-IP": {"{http.request.remote.host}"}, + }, + }, + // 真实IP-中间层 + { + ID: "real_ip_intermediate", + Name: "真实IP (中间层)", + Description: "当Caddy位于反向代理或负载均衡器之后时, 使用此预设获取客户端真实IP。", + Target: "upstream", + Headers: map[string][]string{ + "X-Forwarded-For": {"{http.request.header.X-Forwarded-For}"}, + "X-Forwarded-Proto": {"{http.request.header.X-Forwarded-Proto}"}, + "X-Real-IP": {"{http.request.header.X-Real-IP}"}, + }, + }, + { + ID: "common_security_headers", + Name: "通用安全响应头", + Description: "添加一系列推荐的HTTP安全头以增强站点安全性 (HSTS, X-Frame-Options等)。", + Target: "global", // 此预设适用于全局响应头 + Headers: map[string][]string{ + "Strict-Transport-Security": {"max-age=31536000; includeSubDomains; preload"}, + "X-Frame-Options": {"SAMEORIGIN"}, + "X-Content-Type-Options": {"nosniff"}, + "Referrer-Policy": {"strict-origin-when-cross-origin"}, + "Permissions-Policy": {"geolocation=(), microphone=()"}, + }, + }, + { + ID: "cors_allow_all", + Name: "CORS (允许所有来源)", + Description: "添加允许所有来源跨域请求的响应头。警告: 生产环境请谨慎使用。", + Target: "global", + Headers: map[string][]string{ + "Access-Control-Allow-Origin": {"*"}, + "Access-Control-Allow-Methods": {"GET, POST, PUT, DELETE, OPTIONS"}, + "Access-Control-Allow-Headers": {"Content-Type, Authorization, X-Requested-With"}, + "Access-Control-Allow-Credentials": {"true"}, + }, + }, +} + +// GetHeaderSetMetadataList 返回所有可用预设的元数据列表。 +// 这个函数是线程安全的, 因为它只读取全局只读变量 registry。 +func GetHeaderSetMetadataList() []HeaderSetMetadata { + metadata := make([]HeaderSetMetadata, len(registry)) + for i, set := range registry { + metadata[i] = HeaderSetMetadata{ + ID: set.ID, + Name: set.Name, + Description: set.Description, + Target: set.Target, + } + } + return metadata +} + +// GetHeaderSetByID 通过其唯一ID查找并返回一个完整的预设。 +// 返回找到的预设和一个布尔值, 表示是否找到。 +func GetHeaderSetByID(id string) (*HeaderSet, bool) { + for _, set := range registry { + if set.ID == id { + // 返回该结构体的副本指针, 避免外部修改全局变量 + foundSet := set + return &foundSet, true + } + } + return nil, false +} diff --git a/deploy/install.sh b/deploy/install.sh index fc7fadc..67fe1e9 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -55,11 +55,10 @@ After=network.target network-online.target Requires=network-online.target [Service] -Type=notify +Type=exec User=root Group=root ExecStart=/root/data/caddy/caddydash -c ./config/config.toml -ExecReload=/root/data/caddy/caddydash WorkingDirectory=/root/data/caddy TimeoutStopSec=5s LimitNOFILE=1048576 @@ -85,11 +84,10 @@ After=network.target network-online.target Requires=network-online.target [Service] -Type=notify +Type=exec User=root Group=root ExecStart=/root/data/caddy/caddydash -c ./config/config.toml -ExecReload=/root/data/caddy/caddydash WorkingDirectory=/root/data/caddy TimeoutStopSec=5s LimitNOFILE=1048576 @@ -107,22 +105,6 @@ EOF echo -e "${green}>${white} $mikublue CaddyDash 用户名: ${username}" $white echo -e "${green}>${white} $mikublue CaddyDash 密码: ${password}" $white -} - -# 选择安装模式 -echo -e "${green}>${white} $mikublue 选择安装模式" $white -echo -e "${green}1.${white} $mikublue 不使用环境变量配置用户名密码" $white -echo -e "${green}2.${white} $mikublue 使用环境变量配置用户名密码" $white -read -p "请输入选项 [1/2]: " install_mode - -if [[ "$install_mode" == "2" ]]; then - read -p "请输入CaddyDash用户名: " caddydash_username - writeSystemdServiceWithPasswdEnv "$caddydash_username" -else - writeSystemdServiceNoEnv -fi - - } # 显示免责声明 @@ -160,7 +142,7 @@ mkdir -p /root/data/caddy/config echo -e "${green}>${white} $mikublue 下載主程序" $white input_version="$@" #获取输入的版本号 if [ -z "$input_version" ]; then - VERSION=$(curl -s https://raw.githubusercontent.com/WJQSERVER/caddy/main/Caddy-VERSION) + VERSION=$(curl -s https://raw.githubusercontent.com/WJQSERVER/caddy/main/TEST-VERSION) else VERSION=$input_version fi @@ -179,7 +161,7 @@ echo -e "${green}>${white} $mikublue 是否创建caddydash账户密码" $white read -p "是否创建caddydash账户密码? [Y/n]" choice if [[ "$choice" == "" || "$choice" == "Y" || "$choice" == "y" ]]; then read -p "请输入CaddyDash用户名: " caddydash_username - read -p "请输入CaddyDash密码: " caddydash_password + read -s -p "请输入CaddyDash密码: " caddydash_password writeSystemdServiceWithPasswdEnv "$caddydash_username" "$caddydash_password" else writeSystemdServiceNoEnv diff --git a/frontend/index.html b/frontend/index.html index 529e34f..288af39 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -19,9 +19,8 @@