ddns-go/main.go
luopc abef25c866
add logout feature (#1301)
* add logout feature

* fix bug, optimize display

* optimize logout display

* keep Logs ui to logout

---------

Co-authored-by: luo.pengcheng <luo.pengcheng@ikasinfo.com>
2024-10-31 16:34:08 +08:00

418 lines
9.9 KiB
Go

package main
import (
"embed"
"errors"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"time"
"github.com/jeessy2/ddns-go/v6/config"
"github.com/jeessy2/ddns-go/v6/dns"
"github.com/jeessy2/ddns-go/v6/util"
"github.com/jeessy2/ddns-go/v6/util/update"
"github.com/jeessy2/ddns-go/v6/web"
"github.com/kardianos/service"
)
// ddns-go 版本
// ddns-go version
var versionFlag = flag.Bool("v", false, "ddns-go version")
// 更新 ddns-go
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, "Update frequency(seconds)")
// 缓存次数
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(), "Custom configuration file path")
// Web 服务
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 address, example: 8.8.8.8")
// 重置密码
var newPassword = flag.String("resetPassword", "", "Reset password to the one entered")
//go:embed static
var staticEmbeddedFiles embed.FS
//go:embed favicon.ico
var faviconEmbeddedFile embed.FS
// version
var version = "DEV"
func main() {
flag.Parse()
if *versionFlag {
fmt.Println(version)
return
}
if *updateFlag {
update.Self(version)
return
}
// 安卓 go/src/time/zoneinfo_android.go 固定localLoc 为 UTC
if runtime.GOOS == "android" {
util.FixTimezone()
}
// 检查监听地址
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, err := config.GetConfigCached()
if err == nil {
conf.ResetPassword(*newPassword)
} else {
util.Log("配置文件 %s 不存在, 可通过-c指定配置文件", *configFilePath)
}
return
}
// 设置跳过证书验证
if *skipVerify {
util.SetInsecureSkipVerify()
}
// 设置自定义DNS
if *customDNS != "" {
util.SetDNS(*customDNS)
}
os.Setenv(util.IPCacheTimesENV, strconv.Itoa(*ipCacheTimes))
switch *serviceType {
case "install":
installService()
case "uninstall":
uninstallService()
case "restart":
restartService()
default:
if util.IsRunInDocker() {
run()
} else {
s := getService()
status, _ := s.Status()
if status != service.StatusUnknown {
// 以服务方式运行
s.Run()
} else {
// 非服务方式运行
switch s.Platform() {
case "windows-service":
util.Log("可使用 .\\ddns-go.exe -s install 安装服务运行")
default:
util.Log("可使用 sudo ./ddns-go -s install 安装服务运行")
}
run()
}
}
}
}
func run() {
// 兼容之前的配置文件
conf, _ := config.GetConfigCached()
conf.CompatibleConfig()
// 初始化语言
util.InitLogLang(conf.Lang)
if !*noWebService {
go func() {
// 启动web服务
err := runWebServer()
if err != nil {
log.Println(err)
time.Sleep(time.Minute)
os.Exit(1)
}
}()
}
// 初始化备用DNS
util.InitBackupDNS(*customDNS, conf.Lang)
// 等待网络连接
util.WaitInternet(dns.Addresses)
// 定时运行
dns.RunTimer(time.Duration(*every) * time.Second)
}
func staticFsFunc(writer http.ResponseWriter, request *http.Request) {
http.FileServer(http.FS(staticEmbeddedFiles)).ServeHTTP(writer, request)
}
func faviconFsFunc(writer http.ResponseWriter, request *http.Request) {
http.FileServer(http.FS(faviconEmbeddedFile)).ServeHTTP(writer, request)
}
func runWebServer() error {
// 启动静态文件服务
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.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))
http.HandleFunc("/logout", web.Auth(web.Logout))
util.Log("监听 %s", *listen)
l, err := net.Listen("tcp", *listen)
if err != nil {
return errors.New(util.LogStr("监听端口发生异常, 请检查端口是否被占用! %s", err))
}
// 没有配置, 自动打开浏览器
autoOpenExplorer()
return http.Serve(l, nil)
}
type program struct{}
func (p *program) Start(s service.Service) error {
// Start should not block. Do the actual work async.
go p.run()
return nil
}
func (p *program) run() {
run()
}
func (p *program) Stop(s service.Service) error {
// Stop should not block. Return with a few seconds.
return nil
}
func getService() service.Service {
options := make(service.KeyValue)
var depends []string
// 确保服务等待网络就绪后再启动
switch service.ChosenSystem().String() {
case "unix-systemv":
options["SysvScript"] = sysvScript
case "windows-service":
// 将 Windows 服务的启动类型设为自动(延迟启动)
options["DelayedAutoStart"] = true
default:
// 向 Systemd 添加网络依赖
depends = append(depends, "Requires=network.target",
"After=network-online.target")
}
svcConfig := &service.Config{
Name: "ddns-go",
DisplayName: "ddns-go",
Description: "Simple and easy to use DDNS. Automatically update domain name resolution to public IP (Support Aliyun, Tencent Cloud, Dnspod, Cloudflare, Callback, Huawei Cloud, Baidu Cloud, Porkbun, GoDaddy...)",
Arguments: []string{"-l", *listen, "-f", strconv.Itoa(*every), "-cacheTimes", strconv.Itoa(*ipCacheTimes), "-c", *configFilePath},
Dependencies: depends,
Option: options,
}
if *noWebService {
svcConfig.Arguments = append(svcConfig.Arguments, "-noweb")
}
if *skipVerify {
svcConfig.Arguments = append(svcConfig.Arguments, "-skipVerify")
}
if *customDNS != "" {
svcConfig.Arguments = append(svcConfig.Arguments, "-dns", *customDNS)
}
prg := &program{}
s, err := service.New(prg, svcConfig)
if err != nil {
log.Fatalln(err)
}
return s
}
// 卸载服务
func uninstallService() {
s := getService()
s.Stop()
if service.ChosenSystem().String() == "unix-systemv" {
if _, err := exec.Command("/etc/init.d/ddns-go", "stop").Output(); err != nil {
log.Println(err)
}
}
if err := s.Uninstall(); err == nil {
util.Log("ddns-go 服务卸载成功")
} else {
util.Log("ddns-go 服务卸载失败, 异常信息: %s", err)
}
}
// 安装服务
func installService() {
s := getService()
status, err := s.Status()
if err != nil && status == service.StatusUnknown {
// 服务未知,创建服务
if err = s.Install(); err == nil {
s.Start()
util.Log("安装 ddns-go 服务成功! 请打开浏览器并进行配置")
if service.ChosenSystem().String() == "unix-systemv" {
if _, err := exec.Command("/etc/init.d/ddns-go", "enable").Output(); err != nil {
log.Println(err)
}
if _, err := exec.Command("/etc/init.d/ddns-go", "start").Output(); err != nil {
log.Println(err)
}
}
return
}
util.Log("安装 ddns-go 服务失败, 异常信息: %s", err)
}
if status != service.StatusUnknown {
util.Log("ddns-go 服务已安装, 无需再次安装")
}
}
// 重启服务
func restartService() {
s := getService()
status, err := s.Status()
if err == nil {
if status == service.StatusRunning {
if err = s.Restart(); err == nil {
util.Log("重启 ddns-go 服务成功")
}
} else if status == service.StatusStopped {
if err = s.Start(); err == nil {
util.Log("启动 ddns-go 服务成功")
}
}
} else {
util.Log("ddns-go 服务未安装, 请先安装服务")
}
}
// 打开浏览器
func autoOpenExplorer() {
_, err := config.GetConfigCached()
// 未找到配置文件
if err != nil {
if util.IsRunInDocker() {
// docker中运行, 提示
util.Log("Docker中运行, 请在浏览器中打开 http://docker主机IP:9876 进行配置")
} else {
// 主机运行, 打开浏览器
addr, err := net.ResolveTCPAddr("tcp", *listen)
if err != nil {
return
}
url := fmt.Sprintf("http://127.0.0.1:%d", addr.Port)
if addr.IP.IsGlobalUnicast() {
url = fmt.Sprintf("http://%s", addr.String())
}
go util.OpenExplorer(url)
}
}
}
const sysvScript = `#!/bin/sh /etc/rc.common
DESCRIPTION="{{.Description}}"
cmd="{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}"
name="ddns-go"
pid_file="/var/run/$name.pid"
stdout_log="/var/log/$name.log"
stderr_log="/var/log/$name.err"
START=99
get_pid() {
cat "$pid_file"
}
is_running() {
[ -f "$pid_file" ] && cat /proc/$(get_pid)/stat > /dev/null 2>&1
}
start() {
if is_running; then
echo "Already started"
else
echo "Starting $name"
{{if .WorkingDirectory}}cd '{{.WorkingDirectory}}'{{end}}
$cmd >> "$stdout_log" 2>> "$stderr_log" &
echo $! > "$pid_file"
if ! is_running; then
echo "Unable to start, see $stdout_log and $stderr_log"
exit 1
fi
fi
}
stop() {
if is_running; then
echo -n "Stopping $name.."
kill $(get_pid)
for i in $(seq 1 10)
do
if ! is_running; then
break
fi
echo -n "."
sleep 1
done
echo
if is_running; then
echo "Not stopped; may still be shutting down or shutdown may have failed"
exit 1
else
echo "Stopped"
if [ -f "$pid_file" ]; then
rm "$pid_file"
fi
fi
else
echo "Not running"
fi
}
restart() {
stop
if is_running; then
echo "Unable to stop, will not attempt to start"
exit 1
fi
start
}
`