diff --git a/README.md b/README.md index 65637cc34..845c6459e 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ - `-noweb` 不启动web服务 - `-skipVerify` 跳过证书验证 - `-dns` 自定义 DNS 服务器 + - `-resetPassword` 重置密码 - [可选] 参考示例 - 10分钟同步一次, 并指定了配置文件地址 ```bash @@ -60,6 +61,10 @@ ```bash ./ddns-go -s install -f 10 -cacheTimes 180 ``` + - 重置密码 + ```bash + ./ddns-go -resetPassword 123456 + ``` - [可选] 使用 [Homebrew](https://brew.sh) 安装 [ddns-go](https://formulae.brew.sh/formula/ddns-go): ```bash diff --git a/README_EN.md b/README_EN.md index 82589a394..35e4b9430 100644 --- a/README_EN.md +++ b/README_EN.md @@ -49,6 +49,7 @@ Automatically obtain your public IPv4 or IPv6 address and resolve it to the corr - `-noweb` does not start web service - `-skipVerify` skip certificate verification - `-dns` custom DNS server + - `-resetPassword` reset password - [Optional] Examples - 10 minutes to synchronize once, and the configuration file address is specified ```bash @@ -58,6 +59,10 @@ Automatically obtain your public IPv4 or IPv6 address and resolve it to the corr ```bash ./ddns-go -s install -f 10 -cacheTimes 180 ``` + - reset password + ```bash + ./ddns-go -resetPassword 123456 + ``` - [Optional] You can use [Homebrew](https://brew.sh) to install [ddns-go](https://formulae.brew.sh/formula/ddns-go) ```bash diff --git a/config/config.go b/config/config.go index ca65c304f..5a5b741fc 100755 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ package config import ( + "errors" "io" "log" "os" @@ -12,6 +13,7 @@ import ( "sync" "github.com/jeessy2/ddns-go/v6/util" + passwordvalidator "github.com/wagslane/go-password-validator" "gopkg.in/yaml.v3" ) @@ -116,13 +118,22 @@ func GetConfigCached() (conf Config, err error) { return *cache.ConfigSingle, err } -// CompatibleConfig 兼容v5.0.0之前的配置文件 +// CompatibleConfig 兼容之前的配置文件 func (conf *Config) CompatibleConfig() { // 如配置文件不为空, 兼容之前的语言为中文 if conf.Lang == "" { conf.Lang = "zh" } + // 如果之前密码不为空且不是bcrypt加密后的密码, 把密码加密并保存 + if conf.Password != "" && !util.IsHashedPassword(conf.Password) { + hashedPwd, err := util.HashPassword(conf.Password) + if err == nil { + conf.Password = hashedPwd + conf.SaveConfig() + } + } + // 兼容v5.0.0之前的配置文件 if len(conf.DnsConf) > 0 { return @@ -177,6 +188,43 @@ func (conf *Config) SaveConfig() (err error) { return } +// 重置密码 +func (conf *Config) ResetPassword(newPassword string) { + // 初始化语言 + util.InitLogLang(conf.Lang) + + // 先检查密码是否安全 + hashedPwd, err := conf.CheckPassword(newPassword) + if err != nil { + util.Log(err.Error()) + return + } + + // 保存配置 + conf.Password = hashedPwd + conf.SaveConfig() + util.Log("用户名 %s 的密码已重置成功! 请重启ddns-go", conf.Username) +} + +// CheckPassword 检查密码 +func (conf *Config) CheckPassword(newPassword string) (hashedPwd string, err error) { + var minEntropyBits float64 = 50 + if conf.NotAllowWanAccess { + minEntropyBits = 25 + } + err = passwordvalidator.Validate(newPassword, minEntropyBits) + if err != nil { + return "", errors.New(util.LogStr("密码不安全!尝试使用更复杂的密码")) + } + + // 加密密码 + hashedPwd, err = util.HashPassword(newPassword) + if err != nil { + return "", errors.New(util.LogStr("异常信息: %s", err.Error())) + } + return +} + func (conf *DnsConfig) getIpv4AddrFromInterface() string { ipv4, _, err := GetNetInterface() if err != nil { diff --git a/go.mod b/go.mod index 7dc7ebd40..6a54c2a70 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/wagslane/go-password-validator v0.3.0 golang.org/x/net v0.25.0 gopkg.in/yaml.v3 v3.0.1 + golang.org/x/crypto v0.23.0 ) require ( diff --git a/go.sum b/go.sum index 9fbe2cd68..6d89599f7 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I= github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/main.go b/main.go index 724560687..22ccec3ff 100644 --- a/main.go +++ b/main.go @@ -33,16 +33,16 @@ var updateFlag = flag.Bool("u", false, "Upgrade ddns-go to the latest version") var listen = flag.String("l", ":9876", "Listen address") // 更新频率(秒) -var every = flag.Int("f", 300, "Sync frequency(seconds)") +var every = flag.Int("f", 300, "Update frequency(seconds)") // 缓存次数 -var ipCacheTimes = flag.Int("cacheTimes", 5, "Interval N times compared with service providers") +var ipCacheTimes = flag.Int("cacheTimes", 5, "Cache times") // 服务管理 var serviceType = flag.String("s", "", "Service management (install|uninstall|restart)") // 配置文件路径 -var configFilePath = flag.String("c", util.GetConfigFilePathDefault(), "config file path") +var configFilePath = flag.String("c", util.GetConfigFilePathDefault(), "Custom configuration file path") // Web 服务 var noWebService = flag.Bool("noweb", false, "No web service") @@ -51,7 +51,10 @@ var noWebService = flag.Bool("noweb", false, "No web service") var skipVerify = flag.Bool("skipVerify", false, "Skip certificate verification") // 自定义 DNS 服务器 -var customDNS = flag.String("dns", "", "Custom DNS server, example: 8.8.8.8") +var customDNS = flag.String("dns", "", "Custom DNS server address, example: 8.8.8.8") + +// 重置密码 +var newPassword = flag.String("resetPassword", "", "Reset password to the one entered") //go:embed static var staticEmbeddedFiles embed.FS @@ -72,17 +75,28 @@ func main() { update.Self(version) return } + // 检查监听地址 if _, err := net.ResolveTCPAddr("tcp", *listen); err != nil { log.Fatalf("Parse listen address failed! Exception: %s", err) } + // 设置版本号 os.Setenv(web.VersionEnv, version) + // 设置配置文件路径 if *configFilePath != "" { absPath, _ := filepath.Abs(*configFilePath) os.Setenv(util.ConfigFilePathENV, absPath) } + // 重置密码 + if *newPassword != "" { + conf, _ := config.GetConfigCached() + conf.ResetPassword(*newPassword) + return + } + // 设置跳过证书验证 if *skipVerify { util.SetInsecureSkipVerify() } + // 设置自定义DNS if *customDNS != "" { util.SetDNS(*customDNS) } @@ -118,7 +132,7 @@ func main() { } func run() { - // 兼容v5.0.0之前的配置文件 + // 兼容之前的配置文件 conf, _ := config.GetConfigCached() conf.CompatibleConfig() // 初始化语言 diff --git a/static/constant.js b/static/constant.js index 2dd2e2a45..84b83836f 100644 --- a/static/constant.js +++ b/static/constant.js @@ -204,6 +204,7 @@ const I18N_MAP = { 'NotAllowWanAccessHelp': 'Default enabled, can prohibit access to this page from the public network', 'Username': 'Username', 'accountHelp': 'Please enter to protect your information security', + 'passwordHelp': 'If you need to change the password, please enter it here', 'Password': 'Password', 'WebhookURLHelp': ` 点击参考官方 Webhook 说明 diff --git a/util/bcrypt.go b/util/bcrypt.go new file mode 100644 index 000000000..94c98db7e --- /dev/null +++ b/util/bcrypt.go @@ -0,0 +1,29 @@ +package util + +import ( + "golang.org/x/crypto/bcrypt" +) + +// HashPassword 密码哈希 +func HashPassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hashedPassword), nil +} + +// PasswordOK 检查密码 +func PasswordOK(hashedPassword, password string) bool { + if hashedPassword == "" && password == "" { + return true + } + err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) + return err == nil +} + +// IsHashedPassword 是否是哈希密码 +func IsHashedPassword(password string) bool { + _, err := bcrypt.Cost([]byte(password)) + return err == nil +} diff --git a/util/messages.go b/util/messages.go index a9e94fc7a..810053671 100644 --- a/util/messages.go +++ b/util/messages.go @@ -116,6 +116,7 @@ func init() { message.SetString(language.English, "%q 登陆成功", "%q login successfully") message.SetString(language.English, "用户名或密码错误", "Username or password is incorrect") message.SetString(language.English, "登录失败次数过多,请等待 %d 分钟后再试", "Too many login failures, please try again after %d minutes") + message.SetString(language.English, "用户名 %s 的密码已重置成功! 请重启ddns-go", "The password of username %s has been reset successfully! Please restart ddns-go") } diff --git a/web/login.go b/web/login.go index 1b1c9cb92..cedbba11f 100755 --- a/web/login.go +++ b/web/login.go @@ -73,7 +73,7 @@ func LoginFunc(w http.ResponseWriter, r *http.Request) { conf, _ := config.GetConfigCached() // 登陆成功 - if data.Username == conf.Username && data.Password == conf.Password { + if data.Username == conf.Username && util.PasswordOK(conf.Password, data.Password) { ld.ticker.Stop() ld.failedTimes = 0 tokenInSystem = util.GenerateToken(data.Username) diff --git a/web/save.go b/web/save.go index 0b1f42ae7..c602afd6a 100755 --- a/web/save.go +++ b/web/save.go @@ -9,7 +9,6 @@ import ( "github.com/jeessy2/ddns-go/v6/config" "github.com/jeessy2/ddns-go/v6/dns" "github.com/jeessy2/ddns-go/v6/util" - passwordvalidator "github.com/wagslane/go-password-validator" ) var startTime = time.Now().Unix() @@ -67,29 +66,25 @@ func checkAndSave(request *http.Request) string { } conf.NotAllowWanAccess = data.NotAllowWanAccess - conf.Username = usernameNew - conf.Password = passwordNew conf.WebhookURL = strings.TrimSpace(data.WebhookURL) conf.WebhookRequestBody = strings.TrimSpace(data.WebhookRequestBody) conf.WebhookHeaders = strings.TrimSpace(data.WebhookHeaders) + // 如果新密码不为空则检查是否够强, 内/外网要求强度不同 + conf.Username = usernameNew + if passwordNew != "" { + hashedPwd, err := conf.CheckPassword(passwordNew) + if err != nil { + return err.Error() + } + conf.Password = hashedPwd + } + // 帐号密码不能为空 if conf.Username == "" || conf.Password == "" { return util.LogStr("必须输入登录用户名/密码") } - // 如果密码不为空则检查是否够强, 内/外网要求强度不同 - if conf.Password != "" { - var minEntropyBits float64 = 50 - if conf.NotAllowWanAccess { - minEntropyBits = 25 - } - err = passwordvalidator.Validate(conf.Password, minEntropyBits) - if err != nil { - return util.LogStr("密码不安全!尝试使用更复杂的密码") - } - } - dnsConfFromJS := data.DnsConf var dnsConfArray []config.DnsConfig empty := dnsConf4JS{} diff --git a/web/writing.go b/web/writing.go index 14129c78b..fcf891af9 100755 --- a/web/writing.go +++ b/web/writing.go @@ -58,7 +58,7 @@ func Writing(writer http.ResponseWriter, request *http.Request) { err = tmpl.Execute(writer, struct { DnsConf template.JS NotAllowWanAccess bool - config.User + Username string config.Webhook Version string Ipv4 []config.NetInterface @@ -66,7 +66,7 @@ func Writing(writer http.ResponseWriter, request *http.Request) { }{ DnsConf: template.JS(getDnsConfStr(conf.DnsConf)), NotAllowWanAccess: conf.NotAllowWanAccess, - User: conf.User, + Username: conf.User.Username, Webhook: conf.Webhook, Version: os.Getenv(VersionEnv), Ipv4: ipv4, diff --git a/web/writing.html b/web/writing.html index c3e4ce464..64cd1f668 100755 --- a/web/writing.html +++ b/web/writing.html @@ -574,12 +574,11 @@
IPv6
type="password" name="Password" id="Password" - value="{{.Password}}" autocomplete="new-password" aria-describedby="passwordHelp" />