mirror of
https://github.com/jeessy2/ddns-go.git
synced 2025-10-20 15:33:46 +08:00
Login page (#1094)
* feat: Add a login page * feat: Modify save rules, more secure * remove remoteAddr == "localhost" * "登录失败次数过多,请等待 %d 分钟后再试 * cookie remove secure * set cookie expires time by `NotAllowWanAccess` * prettier * fix: rename * feat: auto login if unfilled * feat: auto login if there is no username/password * auto login if no username/password
This commit is contained in:
16
main.go
16
main.go
@ -156,14 +156,16 @@ func faviconFsFunc(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
func runWebServer() error {
|
||||
// 启动静态文件服务
|
||||
http.HandleFunc("/static/", web.BasicAuth(staticFsFunc))
|
||||
http.HandleFunc("/favicon.ico", web.BasicAuth(faviconFsFunc))
|
||||
http.HandleFunc("/static/", web.AuthAssert(staticFsFunc))
|
||||
http.HandleFunc("/favicon.ico", web.AuthAssert(faviconFsFunc))
|
||||
http.HandleFunc("/login", web.AuthAssert(web.Login))
|
||||
http.HandleFunc("/loginFunc", web.AuthAssert(web.LoginFunc))
|
||||
|
||||
http.HandleFunc("/", web.BasicAuth(web.Writing))
|
||||
http.HandleFunc("/save", web.BasicAuth(web.Save))
|
||||
http.HandleFunc("/logs", web.BasicAuth(web.Logs))
|
||||
http.HandleFunc("/clearLog", web.BasicAuth(web.ClearLog))
|
||||
http.HandleFunc("/webhookTest", web.BasicAuth(web.WebhookTest))
|
||||
http.HandleFunc("/", web.Auth(web.Writing))
|
||||
http.HandleFunc("/save", web.Auth(web.Save))
|
||||
http.HandleFunc("/logs", web.Auth(web.Logs))
|
||||
http.HandleFunc("/clearLog", web.Auth(web.ClearLog))
|
||||
http.HandleFunc("/webhookTest", web.Auth(web.WebhookTest))
|
||||
|
||||
util.Log("监听 %s", *listen)
|
||||
|
||||
|
@ -226,6 +226,7 @@ const I18N_MAP = {
|
||||
"Ipv4CmdHelp": "Get IPv4 through command, only use the first matching IPv4 address of standard output(stdout). Such as: ip -4 addr show eth1",
|
||||
"Ipv6CmdHelp": "Get IPv6 through command, only use the first matching IPv6 address of standard output(stdout). Such as: ip -6 addr show eth1",
|
||||
"NetInterfaceEmptyHelp": '<span style="color: red">No available network card found</span>',
|
||||
"Login": 'Login',
|
||||
},
|
||||
'zh-cn': {
|
||||
'Logs': '日志',
|
||||
@ -287,5 +288,6 @@ const I18N_MAP = {
|
||||
<a target="blank" href="https://github.com/jeessy2/ddns-go/wiki/通过命令获取IP参考">点击参考更多</a>
|
||||
`,
|
||||
"NetInterfaceEmptyHelp": '<span style="color: red">没有找到可用的网卡</span>',
|
||||
"Login": '登录',
|
||||
}
|
||||
};
|
||||
|
22
static/theme.js
Normal file
22
static/theme.js
Normal file
@ -0,0 +1,22 @@
|
||||
function toggleTheme(write = false) {
|
||||
const docEle = document.documentElement;
|
||||
if (docEle.getAttribute("data-theme") === "dark") {
|
||||
docEle.removeAttribute("data-theme");
|
||||
write && localStorage.setItem("theme", "light");
|
||||
} else {
|
||||
docEle.setAttribute("data-theme", "dark");
|
||||
write && localStorage.setItem("theme", "dark");
|
||||
}
|
||||
}
|
||||
|
||||
const theme = localStorage.getItem("theme") ??
|
||||
(window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light");
|
||||
|
||||
if (theme === "dark") {
|
||||
toggleTheme();
|
||||
}
|
||||
|
||||
// 主题切换
|
||||
document.getElementById("themeButton").addEventListener('click', () => toggleTheme(true));
|
@ -95,6 +95,9 @@ const request = {
|
||||
},
|
||||
get: async function(path, data, parseFunc) {
|
||||
const response = await fetch(`${this.baseURL}${path}?${this.stringify(data)}`)
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url
|
||||
}
|
||||
return await (parseFunc||this.parse)(response)
|
||||
},
|
||||
post: async function(path, data, parseFunc) {
|
||||
@ -105,6 +108,9 @@ const request = {
|
||||
method: 'POST',
|
||||
body: data
|
||||
})
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url
|
||||
}
|
||||
return await (parseFunc||this.parse)(response)
|
||||
}
|
||||
}
|
@ -53,11 +53,10 @@ func init() {
|
||||
message.SetString(language.English, "Callback调用失败, 异常信息: %s", "Webhook called failed! Exception: %s")
|
||||
|
||||
// save
|
||||
message.SetString(language.English, "若通过公网访问, 仅允许在ddns-go启动后 5 分钟内完成首次配置", "If accessed via the public network, only allow the first configuration to be completed within 5 minutes after ddns-go starts")
|
||||
message.SetString(language.English, "若从未设置过帐号密码, 仅允许在ddns-go启动后 5 分钟内设置, 请重启ddns-go", "If you have never set an account password, you can only set it within 5 minutes after ddns-go starts, please restart ddns-go")
|
||||
message.SetString(language.English, "启用外网访问, 必须输入登录用户名/密码", "Enable external network access, you must enter the login username/password")
|
||||
message.SetString(language.English, "修改 '通过命令获取' 必须设置帐号密码,请先设置帐号密码", "Modify 'Get by command' must set username/password, please set username/password first")
|
||||
message.SetString(language.English, "密码不安全!尝试使用更长的密码", "insecure password, try using a longer password")
|
||||
message.SetString(language.English, "请在ddns-go启动后 5 分钟内完成首次配置", "Please complete the first configuration within 5 minutes after ddns-go starts")
|
||||
message.SetString(language.English, "之前未设置帐号密码, 仅允许在ddns-go启动后 5 分钟内设置, 请重启ddns-go", "The username/password has not been set before, only allowed to set within 5 minutes after ddns-go starts, please restart ddns-go")
|
||||
message.SetString(language.English, "必须输入登录用户名/密码", "Must enter login username/password")
|
||||
message.SetString(language.English, "密码不安全!尝试使用更复杂的密码", "Password is not secure! Try using a more complex password")
|
||||
message.SetString(language.English, "数据解析失败, 请刷新页面重试", "Data parsing failed, please refresh the page and try again")
|
||||
message.SetString(language.English, "第 %s 个配置未填写域名", "The %s config does not fill in the domain")
|
||||
|
||||
@ -113,6 +112,11 @@ func init() {
|
||||
message.SetString(language.English, "失败", "failed")
|
||||
message.SetString(language.English, "成功", "success")
|
||||
|
||||
// Login
|
||||
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")
|
||||
|
||||
}
|
||||
|
||||
func Log(key string, args ...interface{}) {
|
||||
|
@ -28,14 +28,6 @@ func IsPrivateNetwork(remoteAddr string) bool {
|
||||
ip.IsLinkLocalUnicast() // 169.254/16, fe80::/10
|
||||
}
|
||||
|
||||
// localhost
|
||||
if remoteAddr == "localhost" {
|
||||
return true
|
||||
}
|
||||
// private domain eg. .cluster.local
|
||||
if strings.HasSuffix(remoteAddr, ".local") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
31
util/token.go
Normal file
31
util/token.go
Normal file
@ -0,0 +1,31 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateToken 生成Token
|
||||
func GenerateToken(username string) string {
|
||||
key := []byte(generateRandomKey())
|
||||
h := hmac.New(sha256.New, key)
|
||||
msg := fmt.Sprintf("%s%d", username, time.Now().Unix())
|
||||
h.Write([]byte(msg))
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// generateRandomKey 生成随机密钥
|
||||
func generateRandomKey() string {
|
||||
// 设置随机种子
|
||||
source := rand.NewSource(time.Now().UnixNano())
|
||||
random := rand.New(source)
|
||||
|
||||
// 生成随机的64位整数
|
||||
randomNumber := random.Uint64()
|
||||
|
||||
return fmt.Sprint(randomNumber)
|
||||
}
|
70
web/auth.go
Normal file
70
web/auth.go
Normal file
@ -0,0 +1,70 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jeessy2/ddns-go/v6/config"
|
||||
"github.com/jeessy2/ddns-go/v6/util"
|
||||
)
|
||||
|
||||
// ViewFunc func
|
||||
type ViewFunc func(http.ResponseWriter, *http.Request)
|
||||
|
||||
// Auth 验证Token是否已经通过
|
||||
func Auth(f ViewFunc) ViewFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenInCookie, err := r.Cookie("token")
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "./login", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
conf, _ := config.GetConfigCached()
|
||||
|
||||
// 禁止公网访问
|
||||
if conf.NotAllowWanAccess {
|
||||
if !util.IsPrivateNetwork(r.RemoteAddr) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
util.Log("%q 被禁止从公网访问", util.GetRequestIPStr(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 验证token
|
||||
if tokenInSystem != "" && tokenInSystem == tokenInCookie.Value {
|
||||
f(w, r) // 执行被装饰的函数
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "./login", http.StatusTemporaryRedirect)
|
||||
}
|
||||
}
|
||||
|
||||
// AuthAssert 保护静态等文件不被公网访问
|
||||
func AuthAssert(f ViewFunc) ViewFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
conf, err := config.GetConfigCached()
|
||||
|
||||
// 配置文件为空, 启动时间超过3小时禁止从公网访问
|
||||
if err != nil &&
|
||||
time.Now().Unix()-startTime > 3*60*60 && !util.IsPrivateNetwork(r.RemoteAddr) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
util.Log("%q 配置文件为空, 超过3小时禁止从公网访问", util.GetRequestIPStr(r))
|
||||
return
|
||||
}
|
||||
|
||||
// 禁止公网访问
|
||||
if conf.NotAllowWanAccess {
|
||||
if !util.IsPrivateNetwork(r.RemoteAddr) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
util.Log("%q 被禁止从公网访问", util.GetRequestIPStr(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
f(w, r) // 执行被装饰的函数
|
||||
|
||||
}
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jeessy2/ddns-go/v6/config"
|
||||
"github.com/jeessy2/ddns-go/v6/util"
|
||||
)
|
||||
|
||||
// ViewFunc func
|
||||
type ViewFunc func(http.ResponseWriter, *http.Request)
|
||||
|
||||
type loginDetect struct {
|
||||
FailTimes int
|
||||
}
|
||||
|
||||
var ld = &loginDetect{}
|
||||
|
||||
// BasicAuth basic auth
|
||||
func BasicAuth(f ViewFunc) ViewFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
conf, err := config.GetConfigCached()
|
||||
|
||||
// 配置文件为空, 启动时间超过3小时禁止从公网访问
|
||||
if err != nil && time.Now().Unix()-startTime > 3*60*60 &&
|
||||
(!util.IsPrivateNetwork(r.RemoteAddr) || !util.IsPrivateNetwork(r.Host)) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
util.Log("%q 配置文件为空, 超过3小时禁止从公网访问", util.GetRequestIPStr(r))
|
||||
return
|
||||
}
|
||||
|
||||
// 禁止公网访问
|
||||
if conf.NotAllowWanAccess {
|
||||
if !util.IsPrivateNetwork(r.RemoteAddr) || !util.IsPrivateNetwork(r.Host) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
util.Log("%q 被禁止从公网访问", util.GetRequestIPStr(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 帐号或密码为空。跳过
|
||||
if conf.Username == "" && conf.Password == "" {
|
||||
// 执行被装饰的函数
|
||||
f(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if ld.FailTimes >= 5 {
|
||||
util.Log("%q 登陆失败超过5次! 并延时5分钟响应", util.GetRequestIPStr(r))
|
||||
time.Sleep(5 * time.Minute)
|
||||
if ld.FailTimes >= 5 {
|
||||
ld.FailTimes = 0
|
||||
}
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// 认证帐号密码
|
||||
basicAuthPrefix := "Basic "
|
||||
|
||||
// 获取 request header
|
||||
auth := r.Header.Get("Authorization")
|
||||
// 如果是 http basic auth
|
||||
if strings.HasPrefix(auth, basicAuthPrefix) {
|
||||
// 解码认证信息
|
||||
payload, err := base64.StdEncoding.DecodeString(
|
||||
auth[len(basicAuthPrefix):],
|
||||
)
|
||||
if err == nil {
|
||||
pair := bytes.SplitN(payload, []byte(":"), 2)
|
||||
if len(pair) == 2 &&
|
||||
bytes.Equal(pair[0], []byte(conf.Username)) &&
|
||||
bytes.Equal(pair[1], []byte(conf.Password)) {
|
||||
ld.FailTimes = 0
|
||||
// 执行被装饰的函数
|
||||
f(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ld.FailTimes = ld.FailTimes + 1
|
||||
util.Log("%q 帐号密码不正确", util.GetRequestIPStr(r))
|
||||
}
|
||||
|
||||
// 认证失败,提示 401 Unauthorized
|
||||
// Restricted 可以改成其他的值
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
// 401 状态码
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
util.Log("%q 请求登陆", util.GetRequestIPStr(r))
|
||||
}
|
||||
}
|
124
web/login.go
Executable file
124
web/login.go
Executable file
@ -0,0 +1,124 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/jeessy2/ddns-go/v6/config"
|
||||
"github.com/jeessy2/ddns-go/v6/util"
|
||||
)
|
||||
|
||||
//go:embed login.html
|
||||
var loginEmbedFile embed.FS
|
||||
|
||||
// only need one token
|
||||
var tokenInSystem = ""
|
||||
|
||||
// 登录检测
|
||||
type loginDetect struct {
|
||||
failedTimes uint32 // 失败次数
|
||||
ticker *time.Ticker // 定时器
|
||||
}
|
||||
|
||||
var ld = &loginDetect{ticker: time.NewTicker(5 * time.Minute)}
|
||||
|
||||
// Login login page
|
||||
func Login(writer http.ResponseWriter, request *http.Request) {
|
||||
tmpl, err := template.ParseFS(loginEmbedFile, "login.html")
|
||||
if err != nil {
|
||||
fmt.Println("Error happened..")
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
conf, _ := config.GetConfigCached()
|
||||
|
||||
err = tmpl.Execute(writer, struct {
|
||||
EmptyUser bool // 未填写用户名和密码
|
||||
}{
|
||||
EmptyUser: conf.Username == "" && conf.Password == "",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Error happened..")
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// LoginFunc login func
|
||||
func LoginFunc(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if ld.failedTimes >= 5 {
|
||||
lockMinute := loginUnlock()
|
||||
returnError(w, util.LogStr("登录失败次数过多,请等待 %d 分钟后再试", lockMinute))
|
||||
return
|
||||
}
|
||||
|
||||
// 从请求中读取 JSON 数据
|
||||
var data struct {
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password"`
|
||||
}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&data)
|
||||
|
||||
if err != nil {
|
||||
returnError(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
conf, _ := config.GetConfigCached()
|
||||
|
||||
// 登陆成功
|
||||
if data.Username == conf.Username && data.Password == conf.Password {
|
||||
ld.ticker.Stop()
|
||||
ld.failedTimes = 0
|
||||
tokenInSystem = util.GenerateToken(data.Username)
|
||||
|
||||
// 设置cookie过期时间为1天
|
||||
cookieTimeout := 24
|
||||
if conf.NotAllowWanAccess {
|
||||
// 内网访问cookie过期时间为30天
|
||||
cookieTimeout = 24 * 30
|
||||
}
|
||||
|
||||
// return cookie
|
||||
cookie := http.Cookie{
|
||||
Name: "token",
|
||||
Value: tokenInSystem,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(time.Hour * time.Duration(cookieTimeout)),
|
||||
}
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
util.Log("%q 登陆成功", util.GetRequestIPStr(r))
|
||||
returnOK(w, util.LogStr("登陆成功"), tokenInSystem)
|
||||
return
|
||||
}
|
||||
|
||||
ld.failedTimes = ld.failedTimes + 1
|
||||
util.Log("%q 帐号密码不正确", util.GetRequestIPStr(r))
|
||||
returnError(w, util.LogStr("用户名或密码错误"))
|
||||
}
|
||||
|
||||
// loginUnlock login unlock, return minute
|
||||
func loginUnlock() (minute uint32) {
|
||||
ld.failedTimes = ld.failedTimes + 1
|
||||
x := ld.failedTimes
|
||||
if x > 1440 {
|
||||
x = 1440 // 最多等待一天
|
||||
}
|
||||
ld.ticker.Reset(time.Duration(x) * time.Minute)
|
||||
|
||||
go func(ticker *time.Ticker) {
|
||||
for range ticker.C {
|
||||
ld.failedTimes = 0
|
||||
ticker.Stop()
|
||||
}
|
||||
}(ld.ticker)
|
||||
|
||||
return x
|
||||
}
|
135
web/login.html
Executable file
135
web/login.html
Executable file
@ -0,0 +1,135 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="author" content="jeessy2" />
|
||||
<title>DDNS-GO</title>
|
||||
<link
|
||||
class="theme"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="./static/common.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="./static/bootstrap.min.css" />
|
||||
<link rel="stylesheet" href="./static/theme-button.css" />
|
||||
<script src="./static/constant.js"></script>
|
||||
<script src="./static/utils.js"></script>
|
||||
<script src="./static/i18n.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div class="navbar navbar-dark bg-dark shadow-sm">
|
||||
<div class="button-container container d-flex justify-content-between">
|
||||
<a
|
||||
target="blank"
|
||||
href="https://github.com/jeessy2/ddns-go"
|
||||
class="navbar-brand d-flex align-items-center"
|
||||
>
|
||||
<strong>DDNS-GO</strong>
|
||||
</a>
|
||||
<span class="theme-button gg-dark-mode" id="themeButton"></span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main role="main">
|
||||
<div class="row" style="margin-top: 10%">
|
||||
<div class="col-md-4 offset-md-4 align-self-center">
|
||||
<form id="login">
|
||||
<div class="portlet">
|
||||
<h5 data-i18n="Login" class="portlet__head">Login</h5>
|
||||
<div
|
||||
class="portlet__body"
|
||||
style="justify-content: center; align-items: center"
|
||||
>
|
||||
<div class="form-group row">
|
||||
<label
|
||||
for="Username"
|
||||
data-i18n="Username"
|
||||
class="col-sm-2 col-form-label"
|
||||
>Username</label
|
||||
>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
class="form-control form"
|
||||
name="Username"
|
||||
id="Username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label
|
||||
for="Password"
|
||||
data-i18n="Password"
|
||||
class="col-sm-2 col-form-label"
|
||||
>Password</label
|
||||
>
|
||||
<div class="col-sm-10">
|
||||
<input
|
||||
class="form-control form"
|
||||
type="password"
|
||||
name="Password"
|
||||
id="Password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10 offset-sm-2">
|
||||
<button data-i18n="Login" class="btn btn-primary login_btn">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
<script src="./static/theme.js"></script>
|
||||
<script>
|
||||
// 登录
|
||||
document.querySelectorAll(".login_btn").forEach(($el) => {
|
||||
$el.addEventListener("click", async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
const resp = await request.post("./loginFunc", {
|
||||
Username: document.getElementById("Username").value,
|
||||
Password: document.getElementById("Password").value,
|
||||
});
|
||||
|
||||
if (resp.Code !== 200) {
|
||||
showMessage({
|
||||
content: resp.Msg,
|
||||
type: "error",
|
||||
duration: 5000,
|
||||
});
|
||||
} else {
|
||||
window.location.href = "./";
|
||||
}
|
||||
} catch (err) {
|
||||
showMessage({
|
||||
content: err.toString(),
|
||||
type: "error",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
// 用户为空尝试自动登录
|
||||
if ('{{.EmptyUser}}' === 'true') {
|
||||
document.querySelectorAll(".login_btn").forEach(($el) => {
|
||||
$el.click()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</html>
|
34
web/return_json.go
Normal file
34
web/return_json.go
Normal file
@ -0,0 +1,34 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Result Result
|
||||
type Result struct {
|
||||
Code int // 状态
|
||||
Msg string // 消息
|
||||
Data interface{} // 数据
|
||||
}
|
||||
|
||||
// returnError 返回错误信息
|
||||
func returnError(w http.ResponseWriter, msg string) {
|
||||
result := &Result{}
|
||||
|
||||
result.Code = http.StatusInternalServerError
|
||||
result.Msg = msg
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
// returnOK 返回成功信息
|
||||
func returnOK(w http.ResponseWriter, msg string, data interface{}) {
|
||||
result := &Result{}
|
||||
|
||||
result.Code = http.StatusOK
|
||||
result.Msg = msg
|
||||
result.Data = data
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
36
web/save.go
36
web/save.go
@ -54,19 +54,15 @@ func checkAndSave(request *http.Request) string {
|
||||
accept := request.Header.Get("Accept-Language")
|
||||
conf.Lang = util.InitLogLang(accept)
|
||||
|
||||
// 验证安全性后才允许设置保存配置文件:
|
||||
// 首次设置 && 必需在服务启动的 5 分钟内
|
||||
if time.Now().Unix()-startTime > 5*60 {
|
||||
// 首次设置 && 通过外网访问 必需在服务启动的 5 分钟内
|
||||
if firstTime &&
|
||||
(!util.IsPrivateNetwork(request.RemoteAddr) || !util.IsPrivateNetwork(request.Host)) {
|
||||
return util.LogStr("若通过公网访问, 仅允许在ddns-go启动后 5 分钟内完成首次配置")
|
||||
if firstTime {
|
||||
return util.LogStr("请在ddns-go启动后 5 分钟内完成首次配置")
|
||||
}
|
||||
|
||||
// 非首次设置 && 从未设置过帐号密码 && 本次设置了帐号或密码 必须在5分钟内
|
||||
if !firstTime &&
|
||||
(conf.Username == "" && conf.Password == "") &&
|
||||
// 之前未设置帐号密码 && 本次设置了帐号或密码 必须在5分钟内
|
||||
if (conf.Username == "" && conf.Password == "") &&
|
||||
(usernameNew != "" || passwordNew != "") {
|
||||
return util.LogStr("若从未设置过帐号密码, 仅允许在ddns-go启动后 5 分钟内设置, 请重启ddns-go")
|
||||
return util.LogStr("之前未设置帐号密码, 仅允许在ddns-go启动后 5 分钟内设置, 请重启ddns-go")
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,25 +73,25 @@ func checkAndSave(request *http.Request) string {
|
||||
conf.WebhookRequestBody = strings.TrimSpace(data.WebhookRequestBody)
|
||||
conf.WebhookHeaders = strings.TrimSpace(data.WebhookHeaders)
|
||||
|
||||
// 如启用公网访问,帐号密码不能为空
|
||||
if !conf.NotAllowWanAccess && (conf.Username == "" || conf.Password == "") {
|
||||
return util.LogStr("启用外网访问, 必须输入登录用户名/密码")
|
||||
// 帐号密码不能为空
|
||||
if conf.Username == "" || conf.Password == "" {
|
||||
return util.LogStr("必须输入登录用户名/密码")
|
||||
}
|
||||
|
||||
// 如果密码不为空则检查是否够强, 内/外网要求强度不同
|
||||
if passwordNew != "" {
|
||||
if conf.Password != "" {
|
||||
var minEntropyBits float64 = 50
|
||||
if conf.NotAllowWanAccess {
|
||||
minEntropyBits = 25
|
||||
}
|
||||
err = passwordvalidator.Validate(passwordNew, minEntropyBits)
|
||||
err = passwordvalidator.Validate(conf.Password, minEntropyBits)
|
||||
if err != nil {
|
||||
return util.LogStr("密码不安全!尝试使用更长的密码")
|
||||
return util.LogStr("密码不安全!尝试使用更复杂的密码")
|
||||
}
|
||||
}
|
||||
|
||||
dnsConfFromJS := data.DnsConf
|
||||
dnsConfArray := []config.DnsConfig{}
|
||||
var dnsConfArray []config.DnsConfig
|
||||
empty := dnsConf4JS{}
|
||||
for k, v := range dnsConfFromJS {
|
||||
if v == empty {
|
||||
@ -135,12 +131,6 @@ func checkAndSave(request *http.Request) string {
|
||||
if dnsConf.DNS.Secret == secretHide {
|
||||
dnsConf.DNS.Secret = c.DNS.Secret
|
||||
}
|
||||
|
||||
// 修改cmd需要验证:必须设置帐号密码
|
||||
if (conf.Username == "" && conf.Password == "") &&
|
||||
(c.Ipv4.Cmd != dnsConf.Ipv4.Cmd || c.Ipv6.Cmd != dnsConf.Ipv6.Cmd) {
|
||||
return util.LogStr("修改 '通过命令获取' 必须设置帐号密码,请先设置帐号密码")
|
||||
}
|
||||
}
|
||||
|
||||
dnsConfArray = append(dnsConfArray, dnsConf)
|
||||
|
@ -1096,30 +1096,7 @@
|
||||
</script>
|
||||
|
||||
<!-- 主题色相关的函数和初始化 -->
|
||||
<script>
|
||||
function toggleTheme(write = false) {
|
||||
const docEle = document.documentElement;
|
||||
if (docEle.getAttribute("data-theme") === "dark") {
|
||||
docEle.removeAttribute("data-theme");
|
||||
write && localStorage.setItem("theme", "light");
|
||||
} else {
|
||||
docEle.setAttribute("data-theme", "dark");
|
||||
write && localStorage.setItem("theme", "dark");
|
||||
}
|
||||
}
|
||||
|
||||
const theme = localStorage.getItem("theme") ??
|
||||
(window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light");
|
||||
|
||||
if (theme === "dark") {
|
||||
toggleTheme();
|
||||
}
|
||||
|
||||
// 主题切换
|
||||
document.getElementById("themeButton").addEventListener('click', () => toggleTheme(true));
|
||||
</script>
|
||||
<script src="./static/theme.js"></script>
|
||||
|
||||
<!-- 测试相关 -->
|
||||
<script>
|
||||
|
Reference in New Issue
Block a user