集成动态域名功能

This commit is contained in:
古大羊 2022-07-26 20:16:14 +08:00
parent 88ec9b4606
commit a6cb6bb455
88 changed files with 24720 additions and 8649 deletions

3
.gitignore vendored
View File

@ -5,7 +5,7 @@
*.so
*.dylib
relayports
goports
lucky
# Test binary, built with `go test -c`
*.test
@ -16,6 +16,7 @@ goports
*.log
*.upx
# Dependency directories (remove the comment below to include it)
# vendor/
/dist/

View File

@ -1,10 +1,7 @@
# goports
一个主要功能和socat类似,主要实现公网ipv6 tcp/udp 转发至 内网ipv4 tcp/udp 的端口转发小工具.
# Lucky(大吉)
<!-- TOC -->
- [goports](#goports)
- [Lucky(大吉)](#)
- [特性](#特性)
- [使用](#使用)
- [转发规则格式](#转发规则格式)
@ -17,19 +14,42 @@
## 特性
- 后端golang,前端vue3
- 支持Windows、Linux系统支持x86、ARM、MIPS、MIPSLE等架构
- 支持界面化(web后台)管理转发规则,单条转发规则支持设置多个转发端口,一键开关指定转发规则
- 单条规则支持黑白名单安全模式切换,白名单模式可以让没有安全验证的内网服务端口稍微安全一丢丢暴露到公网
- Web后台支持查看最新100条日志
- 另有精简版不带后台,支持命令行快捷设置转发规则,有利于空间有限的嵌入式设备运行.
- 这是一个自用的,目前主要运行在自己的主路由(小米ax6000)里面的程序.
- 后端golang,前端vue3
- 支持Windows、Linux系统支持x86、ARM、MIPS、MIPSLE等架构
- 目前已经实现的功能有
- 1.替代socat,主要用于公网IPv6 tcp/udp转 内网ipv4
- 支持界面化(web后台)管理转发规则,单条转发规则支持设置多个转发端口,一键开关指定转发规则
- 单条规则支持黑白名单安全模式切换,白名单模式可以让没有安全验证的内网服务端口稍微安全一丢丢暴露到公网
- Web后台支持查看最新100条日志
- 另有精简版不带后台,支持命令行快捷设置转发规则,有利于空间有限的嵌入式设备运行.(不再提供编译版本,如有需求可以自己编译)
- 2.动态域名服务
- 参考和部分代码来自 https://github.com/jeessy2/ddns-go
- 在ddns-go的基础上主要改进/增加的功能有
- 1.同时支持接入多个不同的DNS服务商
- 2.支持http/https/socks5代理设置
- 3.自定义(Callback)和Webhook支持自定义headers
- 4.支持BasicAuth
- 5.DDNS任务列表即可了解全部信息(包含错误信息),无需单独查看日志.
- 6.调用DNS服务商接口更新域名信息前可以先通过DNS解析域名比较IP,减少对服务商接口调用.
- 其它细节功能自己慢慢发现...
- 没有文档,后台各处的提示信息已经足够多.
- 将要实现的功能
- 有建议可联系作者.
## 使用
- [百度网盘下载地址](https://pan.baidu.com/s/1NfumD9XjYU3OTeVmbu6vOQ?pwd=6666)
百度网盘版本可能会更新比较频繁,
github release更新 只发布未经upx压缩的版本,精简版和upx压缩版会放上百度网盘
- 默认后台管理地址 http://<运行设备IP>:16601
默认登录账号: 666
@ -38,15 +58,15 @@
- 常规使用请用 -c <配置文件路径> 指定配置文件的路由方式运行 , -p <后台端口> 可以指定后台管理端口
```bash
#仅指定配置文件路径(如果配置文件不存在会自动创建),建议使用绝对路径
goports -c 666.conf
lucky -c 666.conf
#同时指定后台端口 8899
goports -c 666.conf -p 8899
lucky -c 666.conf -p 8899
```
- 命令行直接运行转发规则,注意后台无法编辑修改命令行启动的转发规则,主要用在不带后台的精简版
```bash
#指定后台端口8899
goports -p 8899 <转发规则1> <转发规则2> <转发规则3>...<<转发规则N>
lucky -p 8899 <转发规则1> <转发规则2> <转发规则3>...<<转发规则N>
```
## 转发规则格式
@ -96,6 +116,17 @@
![规则列表](./previews/relayrules.png)
![](./previews/whitelistset.png)
![](./previews/whitelist.png)
#### 动态域名服务
![](./previews/ddnslist.png)
![](./previews/iphistroy.png)
![](./previews/webhookhistroy.png)
![](./previews/domainsync.png)
#开发编译

View File

@ -8,7 +8,7 @@ import (
"strings"
"sync"
"github.com/ljymc/goports/thirdlib/gdylib/pool"
"github.com/gdy666/lucky/thirdlib/gdylib/pool"
)
type Proxy interface {

View File

@ -11,7 +11,7 @@ import (
"time"
"github.com/fatedier/golib/errors"
"github.com/ljymc/goports/thirdlib/gdylib/pool"
"github.com/gdy666/lucky/thirdlib/gdylib/pool"
)
const UDP_DEFAULT_PACKAGE_SIZE = 1500

View File

@ -1,2 +1,2 @@
cd ./web/goports-adminviews
cd ./web/adminviews
npm run build

View File

@ -1,45 +0,0 @@
{
"AdminWebListen": ":16601",
"ProxyCountLimit": 128,
"GlobalMaxConnections": 10240,
"RelayRuleList": [
{
"Name": "FTP",
"Configurestr": "tcp@:20000-20010,20021to192.168.31.180",
"Enable": true,
"Options": {
"UDPPackageSize": 1500,
"SingleProxyMaxConnections": 256
}
},
{
"Name": "Socket5 均衡负载",
"Configurestr": "tcp@:6666to192.168.31.180:7890,192.168.31.148:7890",
"Enable": true,
"Options": {
"UDPPackageSize": 1500,
"SingleProxyMaxConnections": 256
}
},
{
"Name": "FileBrowser",
"Configurestr": "tcp@:28888to192.168.31.180",
"Enable": true,
"Options": {
"UDPPackageSize": 1500,
"SingleProxyMaxConnections": 256
}
},
{
"Name": "DNS转发",
"Configurestr": "tcp,udp@:53to192.168.31.1",
"Enable": true,
"Options": {
"UDPPackageSize": 1496,
"SingleProxyMaxConnections": 256,
"UDPProxyPerformanceMode": true,
"UDPShortMode": true
}
}
]
}

View File

@ -9,9 +9,9 @@ import (
"strings"
"sync"
"github.com/ljymc/goports/base"
"github.com/ljymc/goports/thirdlib/gdylib/fileutils"
"github.com/ljymc/goports/thirdlib/gdylib/stringsp"
"github.com/gdy666/lucky/base"
"github.com/gdy666/lucky/thirdlib/gdylib/fileutils"
"github.com/gdy666/lucky/thirdlib/gdylib/stringsp"
)
const defaultAdminAccount = "666"
@ -60,6 +60,8 @@ type ProgramConfigure struct {
RelayRuleList []ConfigureRelayRule `json:"RelayRuleList"`
WhiteListConfigure WhiteListConfigure `json:"WhiteListConfigure"`
BlackListConfigure BlackListConfigure `json:"BlackListConfigure"`
DDNSConfigure DDNSConfigure `json:"DDNSConfigure"` //DDNS 参数设置
DDNSTaskList []DDNSTask `json:"DDNSTaskList"`
}
var programConfigureMutex sync.RWMutex
@ -70,6 +72,10 @@ var configurePath string
var checkConfigureFileOnce sync.Once
var configureFileSign int8 = -1
// func GetConfigMutex() *sync.RWMutex {
// return &programConfigureMutex
// }
func GetAuthAccount() map[string]string {
programConfigureMutex.RLock()
defer programConfigureMutex.RUnlock()
@ -103,6 +109,13 @@ func GetBaseConfigure() BaseConfigure {
return baseConf
}
func GetDDNSConfigure() DDNSConfigure {
programConfigureMutex.RLock()
defer programConfigureMutex.RUnlock()
conf := programConfigure.DDNSConfigure
return conf
}
//保存基础配置
func SetBaseConfigure(conf *BaseConfigure) error {
programConfigureMutex.Lock()
@ -115,6 +128,30 @@ func SetBaseConfigure(conf *BaseConfigure) error {
return Save()
}
func SetDDNSConfigure(conf *DDNSConfigure) error {
programConfigureMutex.Lock()
defer programConfigureMutex.Unlock()
if conf.Intervals < 30 {
conf.Intervals = 30
}
if conf.Intervals > 3600 {
conf.Intervals = 3600
}
if conf.FirstCheckDelay < 0 {
conf.FirstCheckDelay = 0
}
if conf.FirstCheckDelay > 3600 {
conf.FirstCheckDelay = 3600
}
programConfigure.DDNSConfigure = *conf
return Save()
}
func Read(filePath string) (err error) {
pc, err := readProgramConfigure(filePath)
@ -226,6 +263,14 @@ func loadDefaultConfigure(
pc.BaseConfigure.AdminWebListenPort = defaultAdminListenPort
}
if pc.DDNSConfigure.Intervals < 30 {
pc.DDNSConfigure.Intervals = 30
}
if pc.DDNSConfigure.FirstCheckDelay <= 0 {
pc.DDNSConfigure.FirstCheckDelay = 0
}
return &pc
}

617
config/ddns.go Normal file
View File

@ -0,0 +1,617 @@
package config
import (
"fmt"
"io/ioutil"
"log"
"regexp"
"strings"
"sync"
"time"
"github.com/gdy666/lucky/thirdlib/gdylib/httputils"
"github.com/gdy666/lucky/thirdlib/gdylib/stringsp"
)
// Ipv4Reg IPv4正则
const Ipv4Reg = `((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])`
// Ipv6Reg IPv6正则
const Ipv6Reg = `((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))`
var ipUrlAddrMap sync.Map
type DDNSConfigure struct {
Enable bool `json:"Enable"`
HttpClientSecureVerify bool `json:"HttpClientSecureVerify"`
FirstCheckDelay int `json:"FirstCheckDelay"` //首次检查延迟时间
Intervals int `json:"Intervals"`
}
type DDNSTask struct {
TaskName string `json:"TaskName"`
TaskKey string `json:"TaskKey"` //添加任务时随机生成,方便管理任务(修改删除)
//规则类型 IPv4/IPv6
TaskType string `json:"TaskType"`
Enable bool
// 获取IP类型 url/netInterface
GetType string `json:"GetType"`
URL []string `json:"URL"`
NetInterface string `json:"NetInterface"`
IPReg string `json:"IPReg"`
Domains []string `json:"Domains"`
DNS DNSConfig `json:"DNS"`
Webhook
TTL string `json:"TTL"`
HttpClientTimeout int `json:"HttpClientTimeout"`
//-------------------------------------
//IpCache IpCache `json:"-"`
DomainsState DomainsState `json:"-"`
}
type Webhook struct {
WebhookEnable bool `json:"WebhookEnable"` //Webhook开关
WebhookCallOnGetIPfail bool `json:"WebhookCallOnGetIPfail"` //获取IP失败时触发Webhook 开关
WebhookURL string `json:"WebhookURL"`
WebhookMethod string `json:"WebhookMethod"`
WebhookHeaders []string `json:"WebhookHeaders"`
WebhookRequestBody string `json:"WebhookRequestBody"`
WebhookSuccessContent []string `json:"WebhookSuccessContent"` //接口调用成功包含的内容
WebhookProxy string `json:"WebhookProxy"` //使用DNS代理设置 ""表示禁用,"dns"表示使用dns的代理设置
WebhookProxyAddr string `json:"WebhookProxyAddr"` //代理服务器IP
WebhookProxyUser string `json:"WebhookProxyUser"` //代理用户
WebhookProxyPassword string `json:"WebhookProxyPassword"` //代理密码
}
// DNSConfig DNS配置
type DNSConfig struct {
// 名称。如alidns,webhook
Name string `json:"Name"`
ID string `json:"ID"`
Secret string `json:"Secret"`
ForceInterval int `json:"ForceInterval"` //(秒)即使IP没有变化,到一定时间后依然强制更新或先DNS解析比较IP再更新
ResolverDoaminCheck bool `json:"ResolverDoaminCheck"` //调用callback同步前先解析一次域名,如果IP相同就不同步
DNSServerList []string `json:"DNSServerList"` //DNS服务器列表
Callback DNSCallback `json:"Callback"`
HttpClientProxyType string `json:"HttpClientProxyType"` //http client代理服务器设置
HttpClientProxyAddr string `json:"HttpClientProxyAddr"` //代理服务器IP
HttpClientProxyUser string `json:"HttpClientProxyUser"` //代理用户
HttpClientProxyPassword string `json:"HttpClientProxyPassword"` //代理密码
}
type DNSCallback struct {
URL string `json:"URL"` //请求地址
Method string `json:"Method"` //请求方法
Headers []string `json:"Headers"`
RequestBody string `json:"RequestBody"`
Server string `json:"Server"` //预设服务商
CallbackSuccessContent []string `json:"CallbackSuccessContent"` //接口调用成功包含内容
}
//Check 检测IP是否有改变
func (d *DDNSTask) IPChangeCheck(newAddr string) bool {
if newAddr == "" {
return true
}
// 地址改变
if d.DomainsState.IpAddr != newAddr {
//log.Printf("公网地址改变:[%s]===>[%s]", d.DomainsInfo.IpAddr, newAddr)
d.DomainsState.IpAddr = newAddr
return true
}
return false
}
var checkIPv4URLList = []string{"https://4.ipw.cn", "http://v4.ip.zxinc.org/getip", "https://myip4.ipip.net", "https://www.taobao.com/help/getip.php", "https://ddns.oray.com/checkip", "https://ip.3322.net", "https://v4.myip.la"}
var checkIPv6URLList = []string{"https://6.ipw.cn", "https://ipv6.ddnspod.com", "http://v6.ip.zxinc.org/getip", "https://speed.neu6.edu.cn/getIP.php", "https://v6.ident.me", "https://v6.myip.la"}
var DefaultIPv6DNSServerList = []string{
"[2001:4860:4860::8888]:53", //谷歌
"[2001:4860:4860::8844]:53", //谷歌
"[2606:4700:4700::64]:53", //cloudflare
"[2606:4700:4700::6400]:53", //cloudflare
"[240C::6666]:53", //下一代互联网北京研究中心
"[240C::6644]:53", //下一代互联网北京研究中心
"[2402:4e00::]:53", //dnspod
//"[2400:3200::1]:53", //阿里
// "[2400:3200:baba::1]:53", //阿里
"[240e:4c:4008::1]:53", //中国电信
"[240e:4c:4808::1]:53", //中国电信
"[2408:8899::8]:53", //中国联通
"[2408:8888::8]:53", //中国联通
"[2409:8088::a]:53", //中国移动
"[2409:8088::b]:53", //中国移动
"[2001:dc7:1000::1]:53", //CNNIC
"[2400:da00::6666]:53", //百度
}
var DefaultIPv4DNSServerList = []string{
"1.1.1.1:53",
"1.2.4.8:53",
"8.8.8.8:53",
"9.9.9.9:53",
"8.8.4.4:53",
"114.114.114.114:53",
"223.5.5.5:53",
"223.6.6.6:53",
"101.226.4.6:53",
"218.30.118.6:53",
"119.28.28.28:53",
}
// func SetDDNSTaskIpCacheForceCompareByTaskKey(taskKey string, force bool) {
// programConfigureMutex.Lock()
// defer programConfigureMutex.Unlock()
// taskIndex := -1
// for i := range programConfigure.DDNSTaskList {
// if programConfigure.DDNSTaskList[i].TaskKey == taskKey {
// taskIndex = i
// break
// }
// }
// if taskIndex == -1 {
// return
// }
// programConfigure.DDNSTaskList[taskIndex].IpCache.ForceCompare = force
// }
func CleanIPUrlAddrMap() {
keys := []string{}
ipUrlAddrMap.Range(func(key, value any) bool {
keys = append(keys, key.(string))
return true
})
for _, k := range keys {
ipUrlAddrMap.Delete(k)
}
}
func DDNSTaskListTaskDetailsInit() {
programConfigureMutex.Lock()
defer programConfigureMutex.Unlock()
for i := range programConfigure.DDNSTaskList {
programConfigure.DDNSTaskList[i].DomainsState.Init(programConfigure.DDNSTaskList[i].Domains)
programConfigure.DDNSTaskList[i].DomainsState.SetDomainUpdateStatus(UpdateWaiting, "")
//
if programConfigure.DDNSTaskList[i].DNS.ForceInterval < 60 {
programConfigure.DDNSTaskList[i].DNS.ForceInterval = 60
} else if programConfigure.DDNSTaskList[i].DNS.ForceInterval > 360000 {
programConfigure.DDNSTaskList[i].DNS.ForceInterval = 360000
}
if programConfigure.DDNSTaskList[i].HttpClientTimeout < 3 {
programConfigure.DDNSTaskList[i].HttpClientTimeout = 3
} else if programConfigure.DDNSTaskList[i].HttpClientTimeout > 60 {
programConfigure.DDNSTaskList[i].HttpClientTimeout = 60
}
}
}
func DDNSTaskIPCacheCheck(taskKey, ip string) (bool, error) {
programConfigureMutex.Lock()
defer programConfigureMutex.Unlock()
taskIndex := -1
for i := range programConfigure.DDNSTaskList {
if programConfigure.DDNSTaskList[i].TaskKey == taskKey {
taskIndex = i
break
}
}
if taskIndex == -1 {
return true, fmt.Errorf("找不到key对应的DDNS任务")
}
return programConfigure.DDNSTaskList[taskIndex].IPChangeCheck(ip), nil
}
func DDNSTaskSetWebhookCallResult(taskKey string, result bool, message string) {
programConfigureMutex.Lock()
defer programConfigureMutex.Unlock()
taskIndex := -1
for i := range programConfigure.DDNSTaskList {
if programConfigure.DDNSTaskList[i].TaskKey == taskKey {
taskIndex = i
break
}
}
if taskIndex == -1 {
return
}
log.Printf("DDNSTaskSetWebhookCallResult %s", taskKey)
}
type DDNSTaskDetails struct {
DDNSTask
TaskState DomainsState `json:"TaskState"`
}
func GetDDNSTaskList() []DDNSTaskDetails {
programConfigureMutex.RLock()
defer programConfigureMutex.RUnlock()
var resList []DDNSTaskDetails
for i := range programConfigure.DDNSTaskList {
var info DDNSTaskDetails
programConfigure.DDNSTaskList[i].DomainsState.Mutex.RLock()
info.DDNSTask = programConfigure.DDNSTaskList[i]
info.TaskState = programConfigure.DDNSTaskList[i].DomainsState
programConfigure.DDNSTaskList[i].DomainsState.Mutex.RUnlock()
resList = append(resList, info)
}
return resList
}
func GetDDNSTaskByKey(taskKey string) *DDNSTaskDetails {
programConfigureMutex.Lock()
defer programConfigureMutex.Unlock()
taskIndex := -1
for i := range programConfigure.DDNSTaskList {
if programConfigure.DDNSTaskList[i].TaskKey == taskKey {
taskIndex = i
break
}
}
if taskIndex == -1 {
return nil
}
var info DDNSTaskDetails
programConfigure.DDNSTaskList[taskIndex].DomainsState.Mutex.RLock()
info.DDNSTask = programConfigure.DDNSTaskList[taskIndex]
info.DomainsState = programConfigure.DDNSTaskList[taskIndex].DomainsState
programConfigure.DDNSTaskList[taskIndex].DomainsState.Mutex.RUnlock()
return &info
}
func DDNSTaskListFlushDomainsDetails(taskKey string, state *DomainsState) {
programConfigureMutex.Lock()
defer programConfigureMutex.Unlock()
taskIndex := -1
for i := range programConfigure.DDNSTaskList {
if programConfigure.DDNSTaskList[i].TaskKey == taskKey {
taskIndex = i
break
}
}
if taskIndex == -1 {
return
}
var checkDomains []*Domain
//防止有域名被删除
for _, new := range state.Domains {
for j, pre := range programConfigure.DDNSTaskList[taskIndex].DomainsState.Domains {
if strings.Compare(new.String(), pre.String()) == 0 {
checkDomains = append(checkDomains, programConfigure.DDNSTaskList[taskIndex].DomainsState.Domains[j])
break
}
}
}
state.Domains = checkDomains
programConfigure.DDNSTaskList[taskIndex].DomainsState = *state
}
func DDNSTaskListAdd(task *DDNSTask) error {
programConfigureMutex.Lock()
defer programConfigureMutex.Unlock()
task.TaskKey = stringsp.GetRandomString(16)
task.DomainsState.Init(task.Domains)
task.DomainsState.SetDomainUpdateStatus(UpdateWaiting, "")
programConfigure.DDNSTaskList = append(programConfigure.DDNSTaskList, *task)
return Save()
}
func DDNSTaskListDelete(taskKey string) error {
programConfigureMutex.Lock()
defer programConfigureMutex.Unlock()
taskIndex := -1
for i := range programConfigure.DDNSTaskList {
if programConfigure.DDNSTaskList[i].TaskKey == taskKey {
taskIndex = i
break
}
}
if taskIndex == -1 {
return fmt.Errorf("找不到需要删除的DDNS任务")
}
programConfigure.DDNSTaskList = DeleteDDNSTaskListlice(programConfigure.DDNSTaskList, taskIndex)
return Save()
}
func CheckDDNSTaskAvalid(task *DDNSTask) error {
if len(task.URL) == 0 {
if task.TaskType == "IPv6" {
task.URL = checkIPv6URLList
} else {
task.URL = checkIPv4URLList
}
}
switch task.DNS.Name {
case "cloudflare":
if task.DNS.Secret == "" {
return fmt.Errorf("cloudflare token不能为空")
}
case "callback":
if task.DNS.Callback.URL == "" {
return fmt.Errorf("callback URL不能为空")
}
if task.DNS.Callback.Method == "" {
return fmt.Errorf("请选择callback method")
}
default:
if task.DNS.ID == "" || task.DNS.Secret == "" {
return fmt.Errorf("dns服务商相关参数不能为空")
}
}
if len(task.Domains) <= 0 {
return fmt.Errorf("域名列表不能为空")
}
return nil
}
func EnableDDNSTaskByKey(taskKey string, enable bool) error {
programConfigureMutex.Lock()
defer programConfigureMutex.Unlock()
taskIndex := -1
for i := range programConfigure.DDNSTaskList {
if programConfigure.DDNSTaskList[i].TaskKey == taskKey {
taskIndex = i
break
}
}
if taskIndex == -1 {
return fmt.Errorf("开关DDNS任务失败,TaskKey不存在")
}
programConfigure.DDNSTaskList[taskIndex].Enable = enable
if enable {
programConfigure.DDNSTaskList[taskIndex].DomainsState.SetDomainUpdateStatus(UpdateWaiting, "")
} else {
programConfigure.DDNSTaskList[taskIndex].DomainsState.SetDomainUpdateStatus(UpdateStop, "")
}
return Save()
}
func UpdateDomainsStateByTaskKey(taskKey string, status updateStatusType, message string) {
programConfigureMutex.Lock()
defer programConfigureMutex.Unlock()
taskIndex := -1
for i := range programConfigure.DDNSTaskList {
if programConfigure.DDNSTaskList[i].TaskKey == taskKey {
taskIndex = i
break
}
}
if taskIndex == -1 {
return
}
programConfigure.DDNSTaskList[taskIndex].DomainsState.SetDomainUpdateStatus(status, message)
}
func UpdateTaskToDDNSTaskList(taskKey string, task DDNSTask) error {
programConfigureMutex.Lock()
defer programConfigureMutex.Unlock()
taskIndex := -1
for i := range programConfigure.DDNSTaskList {
if programConfigure.DDNSTaskList[i].TaskKey == taskKey {
taskIndex = i
break
}
}
if taskIndex == -1 {
return fmt.Errorf("找不到需要更新的DDNS任务")
}
programConfigure.DDNSTaskList[taskIndex].TaskName = task.TaskName
programConfigure.DDNSTaskList[taskIndex].TaskType = task.TaskType
programConfigure.DDNSTaskList[taskIndex].Enable = task.Enable
programConfigure.DDNSTaskList[taskIndex].GetType = task.GetType
programConfigure.DDNSTaskList[taskIndex].URL = task.URL
programConfigure.DDNSTaskList[taskIndex].NetInterface = task.NetInterface
programConfigure.DDNSTaskList[taskIndex].IPReg = task.IPReg
programConfigure.DDNSTaskList[taskIndex].Domains = task.Domains
programConfigure.DDNSTaskList[taskIndex].DNS = task.DNS
programConfigure.DDNSTaskList[taskIndex].Webhook = task.Webhook
programConfigure.DDNSTaskList[taskIndex].TTL = task.TTL
programConfigure.DDNSTaskList[taskIndex].DomainsState.IpAddr = ""
programConfigure.DDNSTaskList[taskIndex].DomainsState.Init(task.Domains)
programConfigure.DDNSTaskList[taskIndex].DomainsState.SetDomainUpdateStatus(UpdateWaiting, "")
programConfigure.DDNSTaskList[taskIndex].HttpClientTimeout = task.HttpClientTimeout
return Save()
}
func DeleteDDNSTaskListlice(a []DDNSTask, deleteIndex int) []DDNSTask {
j := 0
for i := range a {
if i != deleteIndex {
a[j] = a[i]
j++
}
}
return a[:j]
}
//****************************
func (d *DDNSTask) GetIpAddr() (result string) {
if d.TaskType == "IPv6" {
return d.getIpv6Addr()
}
return d.getIpv4Addr()
}
// getIpv4Addr 获得IPv4地址
func (d *DDNSTask) getIpv4Addr() (result string) {
// 判断从哪里获取IP
if d.GetType == "netInterface" {
result = GetIPFromNetInterface("IPv4", d.NetInterface, d.IPReg)
// 从网卡获取IP
// ipv4, _, err := GetNetInterface()
// if err != nil {
// log.Println("从网卡获得IPv4失败!")
// return
// }
// for _, netInterface := range ipv4 {
// if netInterface.NetInterfaceName == d.NetInterface && len(netInterface.AddressList) > 0 {
// return netInterface.AddressList[0]
// }
// }
// log.Println("从网卡中获得IPv4失败! 网卡名: ", d.NetInterface)
return
}
ddnsGlobalConf := GetDDNSConfigure()
client, err := httputils.CreateHttpClient(
ddnsGlobalConf.HttpClientSecureVerify,
"",
"",
"",
"",
time.Duration(d.HttpClientTimeout)*time.Second)
if err != nil {
log.Printf("%s", err.Error())
return
}
for _, url := range d.URL {
url = strings.TrimSpace(url)
mapIp, ok := ipUrlAddrMap.Load(url)
if ok {
//log.Printf("URL[%s]已缓存IP[%s]", url, mapIp)
result = mapIp.(string)
return
}
resp, err := client.Get(url)
if err != nil {
//log.Printf("连接失败!%s查看接口能否返回IPv4地址</a>,", url)
continue
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("读取IPv4结果失败! 接口:%s", url)
continue
}
comp := regexp.MustCompile(Ipv4Reg)
result = comp.FindString(string(body))
if result != "" {
ipUrlAddrMap.Store(url, result)
return
}
// else {
// log.Printf("获取IPv4结果失败! 接口: %s ,返回值: %s\n", url, result)
// }
}
log.Printf("所有查询公网IPv4的接口均获取IPv4结果失败,请检查接口或当前网络情况")
return
}
// getIpv6Addr 获得IPv6地址
func (d *DDNSTask) getIpv6Addr() (result string) {
// 判断从哪里获取IP
if d.GetType == "netInterface" {
// 从网卡获取IP
// _, ipv6, err := GetNetInterface()
// if err != nil {
// log.Println("从网卡获得IPv6失败!")
// return
// }
// for _, netInterface := range ipv6 {
// if netInterface.NetInterfaceName == d.NetInterface && len(netInterface.AddressList) > 0 {
// if d.IPReg != "" {
// log.Printf("IPv6将使用正则表达式 %s 进行匹配\n", d.IPReg)
// for i := 0; i < len(netInterface.AddressList); i++ {
// matched, err := regexp.MatchString(d.IPReg, netInterface.AddressList[i])
// if matched && err == nil {
// log.Println("匹配成功! 匹配到地址: ", netInterface.AddressList[i])
// return netInterface.AddressList[i]
// }
// log.Printf("第 %d 个地址 %s 不匹配, 将匹配下一个地址\n", i+1, netInterface.AddressList[i])
// }
// log.Println("没有匹配到任何一个IPv6地址, 将使用第一个地址")
// }
// return netInterface.AddressList[0]
// }
// }
// log.Println("从网卡中获得IPv6失败! 网卡名: ", d.NetInterface)
result = GetIPFromNetInterface("IPv6", d.NetInterface, d.IPReg)
return
}
ddnsGlobalConf := GetDDNSConfigure()
client, err := httputils.CreateHttpClient(
!ddnsGlobalConf.HttpClientSecureVerify,
"",
"",
"",
"",
time.Duration(d.HttpClientTimeout)*time.Second)
if err != nil {
log.Printf("%s", err.Error())
return
}
for _, url := range d.URL {
url = strings.TrimSpace(url)
mapIp, ok := ipUrlAddrMap.Load(url)
if ok {
//log.Printf("URL[%s]已缓存IP[%s]", url, mapIp)
result = mapIp.(string)
return
}
resp, err := client.Get(url)
if err != nil {
//log.Printf("连接失败! %s查看接口能否返回IPv6地址 ", url)
continue
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("读取IPv6结果失败! 接口: ", url)
continue
}
comp := regexp.MustCompile(Ipv6Reg)
result = comp.FindString(string(body))
if result != "" {
ipUrlAddrMap.Store(url, result)
return
}
}
log.Printf("所有查询公网IPv6的接口均获取IPv6结果失败,请检查接口或当前网络情况")
return
}

242
config/domains.go Normal file
View File

@ -0,0 +1,242 @@
package config
import (
"log"
"strings"
"sync"
"time"
)
const (
// UpdatedNothing 未改变
UpdatedNothing updateStatusType = "域名IP和公网IP一致"
// UpdatedFailed 更新失败
UpdatedFailed = "失败"
// UpdatedSuccess 更新成功
UpdatedSuccess = "成功"
// UpdateStop 暂停
UpdateStop = "暂停"
// UpdateWaiting
UpdateWaiting = "等待更新"
)
// 固定的主域名
var staticMainDomains = []string{"com.cn", "org.cn", "net.cn", "ac.cn", "eu.org"}
// 获取ip失败的次数
// Domains Ipv4/Ipv6 DomainsState
type DomainsState struct {
IpAddr string
Domains []*Domain
WebhookCallTime string `json:"WebhookCallTime"` //最后触发时间
WebhookCallResult bool `json:"WebhookCallResult"` //触发结果
WebhookCallErrorMsg string `json:"WebhookCallErrorMsg"` //触发错误信息
LastSyncTime time.Time `json:"-"` //记录最新一次同步操作时间
IPAddrHistory []any `json:"IPAddrHistory"`
WebhookCallHistroy []any `json:"WebhookCallHistroy"`
Mutex *sync.RWMutex `json:"-"`
}
type IPAddrHistoryItem struct {
IPaddr string
RecordTime string
}
// Domain 域名实体
type Domain struct {
DomainName string
SubDomain string
UpdateStatus updateStatusType // 更新状态
LastUpdateStatusTime string //最后更新状态的时间
Message string
UpdateHistroy []any
rwmutex *sync.RWMutex
}
type UpdateHistroyItem struct {
UpdateStatus string
UpdateTime string
}
type WebhookCallHistroyItem struct {
CallTime string
CallResult string
}
func (d *DomainsState) SetIPAddr(ipaddr string) {
d.Mutex.Lock()
defer d.Mutex.Unlock()
if d.IpAddr == ipaddr {
return
}
d.IpAddr = ipaddr
hi := IPAddrHistoryItem{IPaddr: ipaddr, RecordTime: time.Now().Local().Format("2006-01-02 15:04:05")}
d.IPAddrHistory = append(d.IPAddrHistory, hi)
if len(d.IPAddrHistory) > 10 {
d.IPAddrHistory = DeleteAnyListlice(d.IPAddrHistory, 0)
}
}
func (d Domain) String() string {
if d.SubDomain != "" {
return d.SubDomain + "." + d.DomainName
}
return d.DomainName
}
func DeleteAnyListlice(a []any, deleteIndex int) []any {
j := 0
for i := range a {
if i != deleteIndex {
a[j] = a[i]
j++
}
}
return a[:j]
}
func (d *DomainsState) SetDomainUpdateStatus(status updateStatusType, message string) {
for i := range d.Domains {
d.Domains[i].SetDomainUpdateStatus(status, message)
}
}
func (d *DomainsState) SetWebhookResult(result bool, errMsg string) {
d.WebhookCallResult = result
d.WebhookCallErrorMsg = errMsg
d.WebhookCallTime = time.Now().Format("2006-01-02 15:04:05")
cr := "成功"
if !result {
cr = "出错"
}
hi := WebhookCallHistroyItem{CallResult: cr, CallTime: time.Now().Local().Format("2006-01-02 15:04:05")}
d.WebhookCallHistroy = append(d.WebhookCallHistroy, hi)
if len(d.WebhookCallHistroy) > 10 {
d.WebhookCallHistroy = DeleteAnyListlice(d.WebhookCallHistroy, 0)
}
}
func (d *Domain) SetDomainUpdateStatus(status updateStatusType, message string) {
d.rwmutex.Lock()
defer d.rwmutex.Unlock()
d.UpdateStatus = status
if status != UpdateWaiting && status != UpdateStop {
d.LastUpdateStatusTime = time.Now().Format("2006-01-02 15:04:05")
// 状态更新历史记录
hi := UpdateHistroyItem{UpdateStatus: string(status), UpdateTime: d.LastUpdateStatusTime}
d.UpdateHistroy = append(d.UpdateHistroy, hi)
if len(d.UpdateHistroy) > 10 {
d.UpdateHistroy = DeleteAnyListlice(d.UpdateHistroy, 0)
}
}
d.Message = message
}
// GetFullDomain 获得全部的,子域名
func (d Domain) GetFullDomain() string {
if d.SubDomain != "" {
return d.SubDomain + "." + d.DomainName
}
return "@" + "." + d.DomainName
}
// GetSubDomain 获得子域名,为空返回@
// 阿里云dnspod需要
func (d Domain) GetSubDomain() string {
if d.SubDomain != "" {
return d.SubDomain
}
return "@"
}
func (d *DomainsState) Init(domains []string) {
if d.Mutex == nil {
d.Mutex = &sync.RWMutex{}
}
d.Domains = d.checkParseDomains(domains)
}
// checkParseDomains 校验并解析用户输入的域名
func (d *DomainsState) checkParseDomains(domainArr []string) (domains []*Domain) {
for _, domainStr := range domainArr {
domainStr = strings.TrimSpace(domainStr)
if domainStr != "" {
dp := strings.Split(domainStr, ":")
dplen := len(dp)
if dplen == 1 { // 自动识别域名
domain := &Domain{}
domain.rwmutex = d.Mutex
sp := strings.Split(domainStr, ".")
length := len(sp)
if length <= 1 {
log.Println(domainStr, "域名不正确")
continue
}
// 处理域名
domain.DomainName = sp[length-2] + "." + sp[length-1]
// 如包含在org.cn等顶级域名下后三个才为用户主域名
for _, staticMainDomain := range staticMainDomains {
if staticMainDomain == domain.DomainName {
domain.DomainName = sp[length-3] + "." + domain.DomainName
break
}
}
domainLen := len(domainStr) - len(domain.DomainName)
if domainLen > 0 {
domain.SubDomain = domainStr[:domainLen-1]
} else {
domain.SubDomain = domainStr[:domainLen]
}
domains = append(domains, domain)
} else if dplen == 2 { // 主机记录:域名 格式
domain := &Domain{}
domain.rwmutex = d.Mutex
sp := strings.Split(dp[1], ".")
length := len(sp)
if length <= 1 {
log.Println(domainStr, "域名不正确")
continue
}
domain.DomainName = dp[1]
domain.SubDomain = dp[0]
domains = append(domains, domain)
} else {
log.Println(domainStr, "域名不正确")
}
}
}
return
}
// CheckIPChange 检测公网IP是否改变
func (domains *DomainsState) CheckIPChange(recordType, taskKey string, getAddrFunc func() string) (ipAddr string, change bool, retDomains []*Domain) {
ipAddr = getAddrFunc()
checkIPChange, err := DDNSTaskIPCacheCheck(taskKey, domains.IpAddr)
if err != nil {
log.Printf("DDNSTaskIPCacheCheck 失败:%s", err.Error())
}
if err != nil || checkIPChange {
return ipAddr, true, domains.Domains
}
//IP没变化
return ipAddr, false, domains.Domains
}

25
config/info.go Normal file
View File

@ -0,0 +1,25 @@
package config
import "runtime"
type AppInfo struct {
AppName string
Version string
OS string
ARCH string
Date string
}
var appInfo AppInfo
func GetAppInfo() *AppInfo {
return &appInfo
}
func InitAppInfo(version, date string) {
appInfo.AppName = "Lucky(大吉)"
appInfo.Version = version
appInfo.Date = date
appInfo.OS = runtime.GOOS
appInfo.ARCH = runtime.GOARCH
}

154
config/netInterface.go Normal file
View File

@ -0,0 +1,154 @@
package config
import (
"log"
"net"
"regexp"
"strconv"
"strings"
)
// NetInterface 本机网络
type NetInterface struct {
NetInterfaceName string
AddressList []string
}
// GetNetInterface 获得网卡地址
// 返回ipv4, ipv6地址
func GetNetInterface() (ipv4NetInterfaces []NetInterface, ipv6NetInterfaces []NetInterface, err error) {
allNetInterfaces, err := net.Interfaces()
if err != nil {
//fmt.Println("net.Interfaces failed, err:", err.Error())
return ipv4NetInterfaces, ipv6NetInterfaces, err
}
// https://en.wikipedia.org/wiki/IPv6_address#General_allocation
//_, ipv6Unicast, _ := net.ParseCIDR("2000::/3")
for i := 0; i < len(allNetInterfaces); i++ {
if (allNetInterfaces[i].Flags & net.FlagUp) != 0 {
addrs, _ := allNetInterfaces[i].Addrs()
ipv4 := []string{}
ipv6 := []string{}
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && ipnet.IP.IsGlobalUnicast() {
_, bits := ipnet.Mask.Size()
// 需匹配全局单播地址
//if bits == 128 && ipv6Unicast.Contains(ipnet.IP) {
if bits == 128 {
ipv6 = append(ipv6, ipnet.IP.String())
}
if bits == 32 {
ipv4 = append(ipv4, ipnet.IP.String())
}
}
}
if len(ipv4) > 0 {
ipv4NetInterfaces = append(
ipv4NetInterfaces,
NetInterface{
NetInterfaceName: allNetInterfaces[i].Name,
AddressList: ipv4,
},
)
}
if len(ipv6) > 0 {
ipv6NetInterfaces = append(
ipv6NetInterfaces,
NetInterface{
NetInterfaceName: allNetInterfaces[i].Name,
AddressList: ipv6,
},
)
}
}
}
return ipv4NetInterfaces, ipv6NetInterfaces, nil
}
func GetIPFromNetInterface(ipType, netinterface, ipreg string) string {
ipv4NetInterfaces, ipv6NetInterfaces, err := GetNetInterface()
if err != nil {
log.Printf("获取网卡信息出错:%s", err.Error())
return ""
}
var netInterfaces []NetInterface
switch ipType {
case "IPv6":
netInterfaces = ipv6NetInterfaces
case "IPv4":
netInterfaces = ipv4NetInterfaces
default:
log.Printf("未知IP类型")
return ""
}
var addressList []string
for i := range netInterfaces {
if netInterfaces[i].NetInterfaceName == netinterface {
addressList = netInterfaces[i].AddressList
break
}
}
if len(addressList) <= 0 {
return ""
}
if ipreg == "" { //默认返回第一个IP
return addressList[0]
}
ipN, err := strconv.Atoi(ipreg)
if err == nil { //选择第N个IP
if len(addressList) < ipN {
log.Printf("当前选择网卡[%s]的第[%d]个IP,超出列表范围", netinterface, ipN)
return ""
}
return addressList[ipN-1]
}
for i := range addressList {
matched, err := regexp.MatchString(ipreg, addressList[i])
if matched && err == nil {
log.Printf("正则匹配上")
return addressList[i]
}
}
if len(ipreg) <= 1 {
return ""
}
if ipreg[len(ipreg)-1] == '*' {
prefixStr := ipreg[:len(ipreg)-1]
log.Printf("匹配以 %s 开头的IP", prefixStr)
for i := range addressList {
if strings.HasPrefix(addressList[i], prefixStr) {
return addressList[i]
}
}
return ""
}
if ipreg[0] == '*' {
suffixStr := ipreg[1:]
log.Printf("匹配以 %s 结尾的IP", suffixStr)
for i := range addressList {
if strings.HasSuffix(addressList[i], suffixStr) {
return addressList[i]
}
}
return ""
}
return ""
}

243
config/webhook.go Normal file
View File

@ -0,0 +1,243 @@
package config
import (
"fmt"
"strings"
"time"
"github.com/gdy666/lucky/thirdlib/gdylib/httputils"
)
// updateStatusType 更新状态
type updateStatusType string
// ExecWebhook 添加或更新IPv4/IPv6记录
func (d *DDNSTask) ExecWebhook(domains *DomainsState) {
if !d.WebhookEnable {
return
}
if domains.IpAddr == "" && !d.WebhookCallOnGetIPfail {
return
}
tryUpdate := hasDomainTryToUpdate(domains.Domains)
if d.WebhookURL != "" && tryUpdate {
//log.Printf("DDNS任务【%s】触发Webhook", d.TaskName)
url := replaceWebhookPara(domains, d.WebhookURL)
requestBody := replaceWebhookPara(domains, d.WebhookRequestBody)
//headersStr := cb.task.DNS.Callback.Headers
var headerStrList []string
for i := range d.WebhookHeaders {
header := replaceWebhookPara(domains, d.WebhookHeaders[i])
headerStrList = append(headerStrList, header)
}
headers := httputils.CreateHeadersMap(headerStrList)
succcssCotentList := []string{}
for i := range d.WebhookSuccessContent {
content := replaceWebhookPara(domains, d.WebhookSuccessContent[i])
succcssCotentList = append(succcssCotentList, content)
}
callErr := d.webhookHttpClientDo(d.WebhookMethod, url, requestBody, headers, succcssCotentList)
if callErr != nil {
//log.Printf("WebHook 调用出错:%s", callErr.Error())
domains.SetWebhookResult(false, callErr.Error())
return
}
//log.Printf("Webhook 调用成功")
domains.SetWebhookResult(true, "")
}
}
func WebhookTest(d *DDNSTask, url, method, WebhookRequestBody, proxy, addr, user, passwd string, headerList, successContentListraw []string) (string, error) {
url = replaceWebhookTestPara(url)
requestBody := replaceWebhookTestPara(WebhookRequestBody)
//headersStr := cb.task.DNS.Callback.Headers
var headerStrList []string
for i := range headerList {
header := replaceWebhookTestPara(headerList[i])
headerStrList = append(headerStrList, header)
}
headers := httputils.CreateHeadersMap(headerStrList)
succcssCotentList := []string{}
for i := range successContentListraw {
content := replaceWebhookTestPara(successContentListraw[i])
succcssCotentList = append(succcssCotentList, content)
}
globalDDNSConf := GetDDNSConfigure()
proxyType := ""
proxyAddr := ""
proxyUser := ""
proxyPasswd := ""
switch proxy {
case "dns":
{
if d.DNS.HttpClientProxyType != "" && d.DNS.HttpClientProxyAddr != "" {
proxyType = d.DNS.HttpClientProxyType
proxyAddr = d.DNS.HttpClientProxyAddr
proxyUser = d.DNS.HttpClientProxyUser
proxyPasswd = d.DNS.HttpClientProxyPassword
}
}
case "http", "https", "socks5":
{
proxyType = proxy
proxyAddr = addr
proxyUser = user
proxyPasswd = passwd
}
default:
}
//fmt.Printf("proxyType:%s\taddr:%s\t,user[%s]passwd[%s]\n", proxyType, proxyAddr, proxyUser, proxyPasswd)
//dnsConf := cb.task.DNS
respStr, err := httputils.GetStringGoutDoHttpRequest(
method,
url,
requestBody,
proxyType,
proxyAddr,
proxyUser,
proxyPasswd,
headers,
!globalDDNSConf.HttpClientSecureVerify,
time.Second*20)
if err != nil {
return "", fmt.Errorf("webhookTest 调用接口[%s]出错:%s", url, err.Error())
}
for _, successContent := range succcssCotentList {
if strings.Contains(respStr, successContent) {
return respStr, nil
}
}
return respStr, fmt.Errorf("接口调用出错,未匹配预设成功返回的字符串")
}
func (d *DDNSTask) webhookHttpClientDo(method, url, requestBody string, headers map[string]string, callbackSuccessContent []string) error {
globalDDNSConf := GetDDNSConfigure()
proxyType := ""
proxyAddr := ""
proxyUser := ""
proxyPasswd := ""
switch d.WebhookProxy {
case "dns":
{
if d.DNS.HttpClientProxyType != "" && d.DNS.HttpClientProxyAddr != "" {
proxyType = d.DNS.HttpClientProxyType
proxyAddr = d.DNS.HttpClientProxyAddr
proxyUser = d.DNS.HttpClientProxyUser
proxyPasswd = d.DNS.HttpClientProxyPassword
}
}
case "http", "https", "socks5":
{
proxyType = d.WebhookProxy
proxyAddr = d.WebhookProxyAddr
proxyUser = d.WebhookProxyUser
proxyPasswd = d.WebhookProxyPassword
}
default:
}
//dnsConf := cb.task.DNS
respStr, err := httputils.GetStringGoutDoHttpRequest(
method,
url,
requestBody,
proxyType,
proxyAddr,
proxyUser,
proxyPasswd,
headers,
!globalDDNSConf.HttpClientSecureVerify,
time.Second*20)
if err != nil {
return fmt.Errorf("webhook 调用接口[%s]出错:%s", url, err.Error())
}
for _, successContent := range callbackSuccessContent {
if strings.Contains(respStr, successContent) {
return nil
}
}
return fmt.Errorf("webhook 调用接口失败:\n%s", respStr)
}
// DomainsIsChange
func hasDomainTryToUpdate(domains []*Domain) bool {
for _, v46 := range domains {
switch v46.UpdateStatus {
case UpdatedFailed:
return true
case UpdatedSuccess:
return true
default:
}
}
return false
}
// replaceWebhookTestPara WebhookTest替换参数 #{successDomains},#{failedDomains}
func replaceWebhookTestPara(orgPara string) (newPara string) {
orgPara = strings.ReplaceAll(orgPara, "#{ipAddr}", "66.66.66.66")
orgPara = strings.ReplaceAll(orgPara, "#{successDomains}", "baidu.com,google.com")
orgPara = strings.ReplaceAll(orgPara, "#{failedDomains}", "weibo.com,github.com")
return orgPara
}
// replacePara 替换参数 #{successDomains},#{failedDomains}
func replaceWebhookPara(d *DomainsState, orgPara string) (newPara string) {
ipAddrText := d.IpAddr
if ipAddrText == "" {
ipAddrText = "获取IP失败"
}
orgPara = strings.ReplaceAll(orgPara, "#{ipAddr}", ipAddrText)
successDomains, failedDomains := getDomainsStr(d.Domains)
orgPara = strings.ReplaceAll(orgPara, "#{successDomains}", successDomains)
orgPara = strings.ReplaceAll(orgPara, "#{failedDomains}", failedDomains)
return orgPara
}
// getDomainsStr 用逗号分割域名,分类域名返回,成功和失败的
func getDomainsStr(domains []*Domain) (string, string) {
var successDomainBuf strings.Builder
var failedDomainsBuf strings.Builder
for _, v46 := range domains {
if v46.UpdateStatus == UpdatedFailed {
if failedDomainsBuf.Len() > 0 {
failedDomainsBuf.WriteString(",")
}
failedDomainsBuf.WriteString(v46.String())
continue
}
if successDomainBuf.Len() > 0 {
successDomainBuf.WriteString(",")
}
successDomainBuf.WriteString(v46.String())
}
return successDomainBuf.String(), failedDomainsBuf.String()
}

159
ddns/alidns.go Normal file
View File

@ -0,0 +1,159 @@
package ddns
import (
"bytes"
"log"
"net/http"
"net/url"
"github.com/gdy666/lucky/config"
"github.com/gdy666/lucky/thirdlib/gdylib/httputils"
"github.com/gdy666/lucky/thirdlib/jeessy2/ddns-go/util"
)
const (
alidnsEndpoint string = "https://alidns.aliyuncs.com/"
)
// https://help.aliyun.com/document_detail/29776.html?spm=a2c4g.11186623.6.672.715a45caji9dMA
// Alidns Alidns
type Alidns struct {
DNSCommon
TTL string
}
// AlidnsSubDomainRecords 记录
type AlidnsSubDomainRecords struct {
TotalCount int
DomainRecords struct {
Record []struct {
DomainName string
RecordID string
Value string
}
}
}
// AlidnsResp 修改/添加返回结果
type AlidnsResp struct {
RecordID string
RequestID string
}
// Init 初始化
func (ali *Alidns) Init(task *config.DDNSTask) {
ali.DNSCommon.Init(task)
if task.TTL == "" {
// 默认600s
ali.TTL = "600"
} else {
ali.TTL = task.TTL
}
ali.SetCreateUpdateDomainFunc(ali.createUpdateDomain)
}
func (ali *Alidns) createUpdateDomain(recordType, ipAddr string, domain *config.Domain) {
var record AlidnsSubDomainRecords
// 获取当前域名信息
params := url.Values{}
params.Set("Action", "DescribeSubDomainRecords")
params.Set("DomainName", domain.DomainName)
params.Set("SubDomain", domain.GetFullDomain())
params.Set("Type", recordType)
err := ali.request(params, &record)
if err != nil {
return
}
if record.TotalCount > 0 {
// 存在,更新
ali.modify(record, domain, recordType, ipAddr)
} else {
// 不存在,创建
ali.create(domain, recordType, ipAddr)
}
}
// 创建
func (ali *Alidns) create(domain *config.Domain, recordType string, ipAddr string) {
params := url.Values{}
params.Set("Action", "AddDomainRecord")
params.Set("DomainName", domain.DomainName)
params.Set("RR", domain.GetSubDomain())
params.Set("Type", recordType)
params.Set("Value", ipAddr)
params.Set("TTL", ali.TTL)
var result AlidnsResp
err := ali.request(params, &result)
if err == nil && result.RecordID != "" {
//log.Printf("新增域名解析 %s 成功IP: %s", domain, ipAddr)
domain.SetDomainUpdateStatus(config.UpdatedSuccess, "")
} else {
//log.Printf("新增域名解析 %s 失败!", domain)
domain.SetDomainUpdateStatus(config.UpdatedFailed, err.Error())
}
}
// 修改
func (ali *Alidns) modify(record AlidnsSubDomainRecords, domain *config.Domain, recordType string, ipAddr string) {
// 相同不修改
if len(record.DomainRecords.Record) > 0 && record.DomainRecords.Record[0].Value == ipAddr {
//log.Printf("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
domain.SetDomainUpdateStatus(config.UpdatedNothing, "")
return
}
params := url.Values{}
params.Set("Action", "UpdateDomainRecord")
params.Set("RR", domain.GetSubDomain())
params.Set("RecordId", record.DomainRecords.Record[0].RecordID)
params.Set("Type", recordType)
params.Set("Value", ipAddr)
params.Set("TTL", ali.TTL)
var result AlidnsResp
err := ali.request(params, &result)
if err == nil && result.RecordID != "" {
//log.Printf("更新域名解析 %s 成功IP: %s", domain, ipAddr)
domain.SetDomainUpdateStatus(config.UpdatedSuccess, "")
} else {
//log.Printf("更新域名解析 %s 失败!", domain)
domain.SetDomainUpdateStatus(config.UpdatedFailed, err.Error())
}
}
// request 统一请求接口
func (ali *Alidns) request(params url.Values, result interface{}) (err error) {
util.AliyunSigner(ali.task.DNS.ID, ali.task.DNS.Secret, &params)
req, err := http.NewRequest(
"GET",
alidnsEndpoint,
bytes.NewBuffer(nil),
)
req.URL.RawQuery = params.Encode()
if err != nil {
log.Println("http.NewRequest失败. Error: ", err)
return
}
client, err := ali.CreateHTTPClient()
if err != nil {
return err
}
resp, err := client.Do(req)
//err = util.GetHTTPResponse(resp, alidnsEndpoint, err, result)
err = httputils.GetAndParseJSONResponseFromHttpResponse(resp, result)
return
}

196
ddns/baidu.go Normal file
View File

@ -0,0 +1,196 @@
package ddns
import (
"bytes"
"encoding/json"
"log"
"net/http"
"strconv"
"github.com/gdy666/lucky/config"
"github.com/gdy666/lucky/thirdlib/gdylib/httputils"
"github.com/gdy666/lucky/thirdlib/jeessy2/ddns-go/util"
)
// https://cloud.baidu.com/doc/BCD/s/4jwvymhs7
const (
baiduEndpoint = "https://bcd.baidubce.com"
)
type BaiduCloud struct {
DNSCommon
TTL int
}
// BaiduRecord 单条解析记录
type BaiduRecord struct {
RecordId uint `json:"recordId"`
Domain string `json:"domain"`
View string `json:"view"`
Rdtype string `json:"rdtype"`
TTL int `json:"ttl"`
Rdata string `json:"rdata"`
ZoneName string `json:"zoneName"`
Status string `json:"status"`
}
// BaiduRecordsResp 获取解析列表拿到的结果
type BaiduRecordsResp struct {
TotalCount int `json:"totalCount"`
Result []BaiduRecord `json:"result"`
}
// BaiduListRequest 获取解析列表请求的body json
type BaiduListRequest struct {
Domain string `json:"domain"`
PageNum int `json:"pageNum"`
PageSize int `json:"pageSize"`
}
// BaiduModifyRequest 修改解析请求的body json
type BaiduModifyRequest struct {
RecordId uint `json:"recordId"`
Domain string `json:"domain"`
View string `json:"view"`
RdType string `json:"rdType"`
TTL int `json:"ttl"`
Rdata string `json:"rdata"`
ZoneName string `json:"zoneName"`
}
// BaiduCreateRequest 创建新解析请求的body json
type BaiduCreateRequest struct {
Domain string `json:"domain"`
RdType string `json:"rdType"`
TTL int `json:"ttl"`
Rdata string `json:"rdata"`
ZoneName string `json:"zoneName"`
}
func (baidu *BaiduCloud) Init(task *config.DDNSTask) {
baidu.DNSCommon.Init(task)
if task.TTL == "" {
// 默认300s
baidu.TTL = 300
} else {
ttl, err := strconv.Atoi(task.TTL)
if err != nil {
baidu.TTL = 300
} else {
baidu.TTL = ttl
}
}
baidu.SetCreateUpdateDomainFunc(baidu.createUpdateDomain)
}
func (baidu *BaiduCloud) createUpdateDomain(recordType, ipAddr string, domain *config.Domain) {
var records BaiduRecordsResp
requestBody := BaiduListRequest{
Domain: domain.DomainName,
PageNum: 1,
PageSize: 1000,
}
err := baidu.request("POST", baiduEndpoint+"/v1/domain/resolve/list", requestBody, &records)
if err != nil {
return
}
find := false
for _, record := range records.Result {
if record.Domain == domain.GetSubDomain() {
//存在就去更新
baidu.modify(record, domain, recordType, ipAddr)
find = true
break
}
}
if !find {
//没找到,去创建
baidu.create(domain, recordType, ipAddr)
}
}
//create 创建新的解析
func (baidu *BaiduCloud) create(domain *config.Domain, recordType string, ipAddr string) {
var baiduCreateRequest = BaiduCreateRequest{
Domain: domain.GetSubDomain(), //处理一下@
RdType: recordType,
TTL: baidu.TTL,
Rdata: ipAddr,
ZoneName: domain.DomainName,
}
var result BaiduRecordsResp
err := baidu.request("POST", baiduEndpoint+"/v1/domain/resolve/add", baiduCreateRequest, &result)
if err == nil {
//log.Printf("新增域名解析 %s 成功IP: %s", domain, ipAddr)
domain.SetDomainUpdateStatus(config.UpdatedSuccess, "")
} else {
//log.Printf("新增域名解析 %s 失败!", domain)
domain.SetDomainUpdateStatus(config.UpdatedFailed, err.Error())
}
}
//modify 更新解析
func (baidu *BaiduCloud) modify(record BaiduRecord, domain *config.Domain, rdType string, ipAddr string) {
//没有变化直接跳过
if record.Rdata == ipAddr {
//log.Printf("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
domain.SetDomainUpdateStatus(config.UpdatedNothing, "")
return
}
var baiduModifyRequest = BaiduModifyRequest{
RecordId: record.RecordId,
Domain: record.Domain,
View: record.View,
RdType: rdType,
TTL: record.TTL,
Rdata: ipAddr,
ZoneName: record.ZoneName,
}
var result BaiduRecordsResp
err := baidu.request("POST", baiduEndpoint+"/v1/domain/resolve/edit", baiduModifyRequest, &result)
if err == nil {
//log.Printf("更新域名解析 %s 成功IP: %s", domain, ipAddr)
domain.SetDomainUpdateStatus(config.UpdatedSuccess, "")
} else {
//log.Printf("更新域名解析 %s 失败!", domain)
domain.SetDomainUpdateStatus(config.UpdatedFailed, err.Error())
}
}
// request 统一请求接口
func (baidu *BaiduCloud) request(method string, url string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err := http.NewRequest(
method,
url,
bytes.NewBuffer(jsonStr),
)
if err != nil {
log.Println("http.NewRequest失败. Error: ", err)
return
}
util.BaiduSigner(baidu.task.DNS.ID, baidu.task.DNS.Secret, req)
client, err := baidu.CreateHTTPClient()
if err != nil {
return err
}
resp, err := client.Do(req)
err = httputils.GetAndParseJSONResponseFromHttpResponse(resp, result)
return
}

106
ddns/callback.go Normal file
View File

@ -0,0 +1,106 @@
package ddns
import (
"fmt"
"strings"
"time"
"github.com/gdy666/lucky/config"
"github.com/gdy666/lucky/thirdlib/gdylib/httputils"
)
type Callback struct {
DNSCommon
TTL string
}
// Init 初始化
func (cb *Callback) Init(task *config.DDNSTask) {
cb.DNSCommon.Init(task)
if task.TTL == "" {
// 默认600
cb.TTL = "600"
} else {
cb.TTL = task.TTL
}
cb.SetCreateUpdateDomainFunc(cb.createUpdateDomain)
}
func CopyHeadersMap(sm map[string]string) map[string]string {
dm := make(map[string]string)
for k, v := range sm {
dm[k] = v
}
return dm
}
func (cb *Callback) createUpdateDomain(recordType, ipAddr string, domain *config.Domain) {
url := replacePara(cb.task.DNS.Callback.URL, ipAddr, domain, recordType, cb.TTL)
requestBody := replacePara(cb.task.DNS.Callback.RequestBody, ipAddr, domain, recordType, cb.TTL)
//headersStr := cb.task.DNS.Callback.Headers
var headerStrList []string
for i := range cb.task.DNS.Callback.Headers {
header := replacePara(cb.task.DNS.Callback.Headers[i], ipAddr, domain, recordType, cb.TTL)
headerStrList = append(headerStrList, header)
}
headers := httputils.CreateHeadersMap(headerStrList)
succcssCotentList := []string{}
for i := range cb.task.DNS.Callback.CallbackSuccessContent {
content := replacePara(cb.task.DNS.Callback.CallbackSuccessContent[i], ipAddr, domain, recordType, cb.TTL)
succcssCotentList = append(succcssCotentList, content)
}
callErr := cb.CallbackHttpClientDo(cb.task.DNS.Callback.Method, url, requestBody, headers, succcssCotentList)
if callErr != nil {
domain.SetDomainUpdateStatus(config.UpdatedFailed, callErr.Error())
return
}
domain.SetDomainUpdateStatus(config.UpdatedSuccess, "")
}
// replacePara 替换参数
func replacePara(orgPara, ipAddr string, domain *config.Domain, recordType string, ttl string) (newPara string) {
orgPara = strings.ReplaceAll(orgPara, "#{ip}", ipAddr)
orgPara = strings.ReplaceAll(orgPara, "#{domain}", domain.String())
orgPara = strings.ReplaceAll(orgPara, "#{recordType}", recordType)
orgPara = strings.ReplaceAll(orgPara, "#{ttl}", ttl)
return orgPara
}
func (cb *Callback) CallbackHttpClientDo(method, url, requestBody string, headers map[string]string, callbackSuccessContent []string) error {
globalDDNSConf := config.GetDDNSConfigure()
dnsConf := cb.task.DNS
respStr, err := httputils.GetStringGoutDoHttpRequest(
method,
url,
requestBody,
dnsConf.HttpClientProxyType,
dnsConf.HttpClientProxyAddr,
dnsConf.HttpClientProxyUser,
dnsConf.HttpClientProxyPassword,
headers,
!globalDDNSConf.HttpClientSecureVerify,
time.Duration(cb.task.HttpClientTimeout)*time.Second)
if err != nil {
return fmt.Errorf("Callback 调用接口[%s]出错:%s", url, err.Error())
}
//log.Printf("接口[%s]调用响应:%s\n", url, respStr)
for _, successContent := range callbackSuccessContent {
if strings.Contains(respStr, successContent) {
return nil
}
}
return fmt.Errorf("调用接口失败:\n%s", respStr)
}

204
ddns/cloudflare.go Normal file
View File

@ -0,0 +1,204 @@
package ddns
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"github.com/gdy666/lucky/config"
"github.com/gdy666/lucky/thirdlib/gdylib/httputils"
)
const (
zonesAPI string = "https://api.cloudflare.com/client/v4/zones"
)
// Cloudflare Cloudflare实现
type Cloudflare struct {
DNSCommon
TTL int
}
// CloudflareZonesResp cloudflare zones返回结果
type CloudflareZonesResp struct {
CloudflareStatus
Result []struct {
ID string
Name string
Status string
Paused bool
}
}
// CloudflareRecordsResp records
type CloudflareRecordsResp struct {
CloudflareStatus
Result []CloudflareRecord
}
// CloudflareRecord 记录实体
type CloudflareRecord struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
Proxied bool `json:"proxied"`
TTL int `json:"ttl"`
}
// CloudflareStatus 公共状态
type CloudflareStatus struct {
Success bool
Messages []string
}
// Init 初始化
func (cf *Cloudflare) Init(task *config.DDNSTask) {
cf.DNSCommon.Init(task)
if task.TTL == "" {
// 默认1 auto ttl
cf.TTL = 1
} else {
ttl, err := strconv.Atoi(task.TTL)
if err != nil {
cf.TTL = 1
} else {
cf.TTL = ttl
}
}
cf.SetCreateUpdateDomainFunc(cf.createUpdateDomain)
}
func (cf *Cloudflare) createUpdateDomain(recordType, ipAddr string, domain *config.Domain) {
result, err := cf.getZones(domain)
if err != nil || len(result.Result) != 1 {
return
}
zoneID := result.Result[0].ID
var records CloudflareRecordsResp
// getDomains 最多更新前50条
err = cf.request(
"GET",
fmt.Sprintf(zonesAPI+"/%s/dns_records?type=%s&name=%s&per_page=50", zoneID, recordType, domain),
nil,
&records,
)
if err != nil || !records.Success {
return
}
if len(records.Result) > 0 {
// 更新
cf.modify(records, zoneID, domain, recordType, ipAddr)
} else {
// 新增
cf.create(zoneID, domain, recordType, ipAddr)
}
}
// 创建
func (cf *Cloudflare) create(zoneID string, domain *config.Domain, recordType string, ipAddr string) {
record := &CloudflareRecord{
Type: recordType,
Name: domain.String(),
Content: ipAddr,
Proxied: false,
TTL: cf.TTL,
}
var status CloudflareStatus
err := cf.request(
"POST",
fmt.Sprintf(zonesAPI+"/%s/dns_records", zoneID),
record,
&status,
)
if err == nil && status.Success {
//log.Printf("新增域名解析 %s 成功IP: %s", domain, ipAddr)
domain.SetDomainUpdateStatus(config.UpdatedSuccess, "")
} else {
//log.Printf("新增域名解析 %s 失败Messages: %s", domain, status.Messages)
domain.SetDomainUpdateStatus(config.UpdatedFailed, err.Error())
}
}
// 修改
func (cf *Cloudflare) modify(result CloudflareRecordsResp, zoneID string, domain *config.Domain, recordType string, ipAddr string) {
for _, record := range result.Result {
// 相同不修改
if record.Content == ipAddr {
//log.Printf("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
domain.SetDomainUpdateStatus(config.UpdatedNothing, "")
continue
}
var status CloudflareStatus
record.Content = ipAddr
record.TTL = cf.TTL
err := cf.request(
"PUT",
fmt.Sprintf(zonesAPI+"/%s/dns_records/%s", zoneID, record.ID),
record,
&status,
)
if err == nil && status.Success {
//log.Printf("更新域名解析 %s 成功IP: %s", domain, ipAddr)
domain.SetDomainUpdateStatus(config.UpdatedSuccess, "")
} else {
//log.Printf("更新域名解析 %s 失败Messages: %s", domain, status.Messages)
errMsg := "更新失败"
if err != nil {
errMsg = err.Error()
}
domain.SetDomainUpdateStatus(config.UpdatedFailed, errMsg)
}
}
}
// 获得域名记录列表
func (cf *Cloudflare) getZones(domain *config.Domain) (result CloudflareZonesResp, err error) {
err = cf.request(
"GET",
fmt.Sprintf(zonesAPI+"?name=%s&status=%s&per_page=%s", domain.DomainName, "active", "50"),
nil,
&result,
)
return
}
// request 统一请求接口
func (cf *Cloudflare) request(method string, url string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err := http.NewRequest(
method,
url,
bytes.NewBuffer(jsonStr),
)
if err != nil {
log.Println("http.NewRequest失败. Error: ", err)
return
}
req.Header.Set("Authorization", "Bearer "+cf.task.DNS.Secret)
req.Header.Set("Content-Type", "application/json")
client, err := cf.CreateHTTPClient()
if err != nil {
return err
}
resp, err := client.Do(req)
err = httputils.GetAndParseJSONResponseFromHttpResponse(resp, result)
return
}

108
ddns/ddns.go Normal file
View File

@ -0,0 +1,108 @@
package ddns
import (
"log"
"sync"
"time"
"github.com/gdy666/lucky/config"
"github.com/gdy666/lucky/thirdlib/gdylib/service"
)
var DDNSService *service.Service
func init() {
DDNSService, _ = service.NewService("ddns")
DDNSService.SetTimerFunc(syncAllDomainsOnce)
DDNSService.SetEventFunc(syncTaskDomainsOnce)
}
// Run 定时运行
func Run(firstDelay time.Duration, delay time.Duration) {
log.Printf("DDNS 第一次运行将等待 %d 秒后运行 (等待网络)", int(firstDelay.Seconds()))
<-time.After(firstDelay)
DDNSService.Start()
}
var wg sync.WaitGroup
// RunOnce RunOnce
func syncAllDomainsOnce(params ...any) {
ddnsTaskList := config.GetDDNSTaskList()
config.CleanIPUrlAddrMap()
ddnsConf := config.GetDDNSConfigure()
for index := range ddnsTaskList {
task := ddnsTaskList[index]
if !task.Enable {
config.UpdateDomainsStateByTaskKey(task.TaskKey, config.UpdateStop, "")
continue
}
wg.Add(1)
go func() {
defer func() {
wg.Done()
recoverErr := recover()
if recoverErr == nil {
return
}
log.Printf("syncDDNSTask[%s]panic:\n%v", task.TaskName, recoverErr)
}()
syncDDNSTask(&task)
}()
<-time.After(time.Second)
}
wg.Wait()
//log.Printf("syncAllDomainsOnce 任务完成")
DDNSService.Timer = time.NewTimer(time.Second * time.Duration(ddnsConf.Intervals))
}
func syncTaskDomainsOnce(params ...any) {
serverMsg := (params[1]).(service.ServiceMsg)
taskKey := serverMsg.Params[0].(string)
switch serverMsg.Type {
case "syncDDNSTask":
{
//log.Printf("syncTaskDomainsOnce 单DDNS任务更新%s", taskKey)
task := config.GetDDNSTaskByKey(taskKey)
syncDDNSTask(task)
}
default:
return
}
}
func syncDDNSTask(task *config.DDNSTaskDetails) {
if task == nil {
return
}
var dnsSelected DNS
switch task.DNS.Name {
case "alidns":
dnsSelected = &Alidns{}
case "dnspod":
dnsSelected = &Dnspod{}
case "cloudflare":
dnsSelected = &Cloudflare{}
case "huaweicloud":
dnsSelected = &Huaweicloud{}
case "callback":
dnsSelected = &Callback{}
case "baiducloud":
dnsSelected = &BaiduCloud{}
default:
return
}
dnsSelected.Init(&task.DDNSTask)
dnsSelected.AddUpdateDomainRecords()
//task.DomainsState.IpAddr = ipaddr
task.ExecWebhook(&task.DomainsState)
config.DDNSTaskListFlushDomainsDetails(task.TaskKey, &task.DomainsState)
}

10
ddns/dns.go Normal file
View File

@ -0,0 +1,10 @@
package ddns
import "github.com/gdy666/lucky/config"
// DNS interface
type DNS interface {
Init(task *config.DDNSTask)
// 添加或更新IPv4/IPv6记录
AddUpdateDomainRecords() string
}

191
ddns/dnscommon.go Normal file
View File

@ -0,0 +1,191 @@
package ddns
import (
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/gdy666/lucky/config"
"github.com/gdy666/lucky/thirdlib/gdylib/httputils"
"github.com/miekg/dns"
"golang.org/x/net/idna"
)
type DNSCommon struct {
//Domains *config.Domains
createUpdateDomainFunc func(recordType, ipaddr string, domain *config.Domain)
task *config.DDNSTask
}
func (d *DNSCommon) SetCreateUpdateDomainFunc(f func(recordType, ipaddr string, domain *config.Domain)) {
d.createUpdateDomainFunc = f
}
func (d *DNSCommon) Init(task *config.DDNSTask) {
d.task = task
}
//添加或更新IPv4/IPv6记录
func (d *DNSCommon) AddUpdateDomainRecords() string {
if d.task.TaskType == "IPv6" {
return d.addUpdateDomainRecords("AAAA")
}
return d.addUpdateDomainRecords("A")
}
func (d *DNSCommon) addUpdateDomainRecords(recordType string) string {
ipAddr, change, domains := d.task.DomainsState.CheckIPChange(recordType, d.task.TaskKey, d.task.GetIpAddr)
d.task.DomainsState.SetIPAddr(ipAddr)
if ipAddr == "" {
d.task.DomainsState.SetDomainUpdateStatus(config.UpdatedFailed, "获取公网IP失败")
return ipAddr
}
preFaildDomains := []*config.Domain{}
if time.Since(d.task.DomainsState.LastSyncTime) > time.Second*time.Duration(d.task.DNS.ForceInterval) {
//log.Printf("DDNS任务[%s]强制更新", d.task.TaskName)
change = true
goto sync
}
//设置原先状态成功的为继续成功
//不成功的就更新
if !change { //公网IP没有改变
for i := range domains { //如果原先状态成功或不改变就刷新时间
if domains[i].UpdateStatus == config.UpdatedNothing || domains[i].UpdateStatus == config.UpdatedSuccess {
domains[i].SetDomainUpdateStatus(config.UpdatedNothing, "")
continue
}
preFaildDomains = append(preFaildDomains, domains[i])
}
if len(preFaildDomains) == 0 {
return ipAddr
}
domains = preFaildDomains
}
sync:
if change {
defer func() {
//记录最近一次同步操作时间
d.task.DomainsState.LastSyncTime = time.Now()
}()
}
for _, domain := range domains {
if d.createUpdateDomainFunc == nil {
log.Printf("ddns createUpdateDomainFunc undefine")
break
}
if d.task.DNS.ResolverDoaminCheck {
domainResolverIPaddr, _ := ResolveDomainAtServerList(recordType, domain.String(), d.task.DNS.DNSServerList)
if domainResolverIPaddr == ipAddr {
domain.SetDomainUpdateStatus(config.UpdatedNothing, "")
continue
}
}
d.createUpdateDomainFunc(recordType, ipAddr, domain)
}
return ipAddr
}
//--------------------------------------------------------------------------------------------------
func (d *DNSCommon) CreateHTTPClient() (*http.Client, error) {
ddnsGlobalConf := config.GetDDNSConfigure()
return httputils.CreateHttpClient(
!ddnsGlobalConf.HttpClientSecureVerify,
d.task.DNS.HttpClientProxyType,
d.task.DNS.HttpClientProxyAddr,
d.task.DNS.HttpClientProxyUser,
d.task.DNS.HttpClientProxyPassword,
time.Duration(d.task.HttpClientTimeout)*time.Second)
}
//---------------------------------------------------------------------------------------------------
func ResolveDomainAtServerList(queryType, domain string, dnsServerList []string) (string, error) {
if len(dnsServerList) == 0 {
if queryType == "AAAA" {
dnsServerList = config.DefaultIPv6DNSServerList
} else {
dnsServerList = config.DefaultIPv4DNSServerList
}
}
//some name that ought to exist, does not exist (NXDOMAIN)
querytype, querytypeOk := dns.StringToType[strings.ToUpper(queryType)]
if !querytypeOk {
return "", fmt.Errorf("queryType error:%s", queryType)
}
domain = dns.Fqdn(domain)
domain, err := idna.ToASCII(domain)
if err != nil {
return "", fmt.Errorf(` idna.ToASCII(domain) error:%s`, err.Error())
}
m := new(dns.Msg)
m.SetQuestion(domain, querytype)
m.MsgHdr.RecursionDesired = true
c := new(dns.Client)
noExistTimes := 0
for _, dnsServer := range dnsServerList {
c.Net = ""
ipaddr, err := resolveDomain(m, c, dnsServer)
if err != nil {
//log.Printf("[%s]===>[%s][%s] ResolveDomain error:%s", dnsServer, queryType, domain, err.Error())
if strings.Contains(err.Error(), "some name that ought to exist, does not exist (NXDOMAIN)") {
noExistTimes++
if noExistTimes >= 4 {
return "", fmt.Errorf("解析域名[%s][%s]IP失败:noExistTimes", queryType, domain)
}
}
continue
}
return ipaddr, nil
}
return "", fmt.Errorf("解析域名[%s][%s]IP失败", queryType, domain)
}
func resolveDomain(msg *dns.Msg, client *dns.Client, dnsServer string) (string, error) {
Redo:
if in, _, err := client.Exchange(msg, dnsServer); err == nil { // Second return value is RTT, not used for now
if in.MsgHdr.Truncated {
client.Net = "tcp"
goto Redo
}
switch in.MsgHdr.Rcode {
case dns.RcodeServerFailure:
return "", fmt.Errorf("the name server encountered an internal failure while processing this request (SERVFAIL)")
case dns.RcodeNameError:
return "", fmt.Errorf("some name that ought to exist, does not exist (NXDOMAIN)")
case dns.RcodeRefused:
return "", fmt.Errorf("the name server refuses to perform the specified operation for policy or security reasons (REFUSED)")
default:
//fmt.Printf("in.Answer len:%d\n", len(in.Answer))
for _, rr := range in.Answer {
//fmt.Printf("rr.String :%s\n", rr.String())
return strings.Replace(rr.String(), rr.Header().String(), "", -1), nil
}
}
}
return "", fmt.Errorf("DNS server could not be reached")
}

167
ddns/dnspod.go Normal file
View File

@ -0,0 +1,167 @@
package ddns
import (
"fmt"
"net/http"
"net/url"
"github.com/gdy666/lucky/config"
"github.com/gdy666/lucky/thirdlib/gdylib/httputils"
)
const (
recordListAPI string = "https://dnsapi.cn/Record.List"
recordModifyURL string = "https://dnsapi.cn/Record.Modify"
recordCreateAPI string = "https://dnsapi.cn/Record.Create"
)
// https://cloud.tencent.com/document/api/302/8516
// Dnspod 腾讯云dns实现
type Dnspod struct {
DNSCommon
TTL string
}
// DnspodRecordListResp recordListAPI结果
type DnspodRecordListResp struct {
DnspodStatus
Records []struct {
ID string
Name string
Type string
Value string
Enabled string
}
}
// DnspodStatus DnspodStatus
type DnspodStatus struct {
Status struct {
Code string
Message string
}
}
// Init 初始化
func (dnspod *Dnspod) Init(task *config.DDNSTask) {
dnspod.DNSCommon.Init(task)
if task.TTL == "" {
// 默认600s
dnspod.TTL = "600"
} else {
dnspod.TTL = task.TTL
}
dnspod.SetCreateUpdateDomainFunc(dnspod.createUpdateDomain)
}
func (dnspod *Dnspod) createUpdateDomain(recordType, ipAddr string, domain *config.Domain) {
result, err := dnspod.getRecordList(domain, recordType)
if err != nil {
return
}
if len(result.Records) > 0 {
// 更新
dnspod.modify(result, domain, recordType, ipAddr)
} else {
// 新增
dnspod.create(result, domain, recordType, ipAddr)
}
}
// 创建
func (dnspod *Dnspod) create(result DnspodRecordListResp, domain *config.Domain, recordType string, ipAddr string) {
status, err := dnspod.commonRequest(
recordCreateAPI,
url.Values{
"login_token": {dnspod.task.DNS.ID + "," + dnspod.task.DNS.Secret},
"domain": {domain.DomainName},
"sub_domain": {domain.GetSubDomain()},
"record_type": {recordType},
"record_line": {"默认"},
"value": {ipAddr},
"ttl": {dnspod.TTL},
"format": {"json"},
},
domain,
)
if err == nil && status.Status.Code == "1" {
//log.Printf("新增域名解析 %s 成功IP: %s", domain, ipAddr)
domain.SetDomainUpdateStatus(config.UpdatedSuccess, "")
} else {
//log.Printf("新增域名解析 %s 失败Code: %s, Message: %s", domain, status.Status.Code, status.Status.Message)
domain.SetDomainUpdateStatus(config.UpdatedFailed, fmt.Sprintf("Code: %s, Message: %s", status.Status.Code, status.Status.Message))
}
}
// 修改
func (dnspod *Dnspod) modify(result DnspodRecordListResp, domain *config.Domain, recordType string, ipAddr string) {
for _, record := range result.Records {
// 相同不修改
if record.Value == ipAddr {
//log.Printf("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
domain.SetDomainUpdateStatus(config.UpdatedNothing, "")
continue
}
status, err := dnspod.commonRequest(
recordModifyURL,
url.Values{
"login_token": {dnspod.task.DNS.ID + "," + dnspod.task.DNS.Secret},
"domain": {domain.DomainName},
"sub_domain": {domain.GetSubDomain()},
"record_type": {recordType},
"record_line": {"默认"},
"record_id": {record.ID},
"value": {ipAddr},
"ttl": {dnspod.TTL},
"format": {"json"},
},
domain,
)
if err == nil && status.Status.Code == "1" {
//log.Printf("更新域名解析 %s 成功IP: %s", domain, ipAddr)
domain.SetDomainUpdateStatus(config.UpdatedSuccess, "")
} else {
//log.Printf("更新域名解析 %s 失败Code: %s, Message: %s", domain, status.Status.Code, status.Status.Message)
domain.SetDomainUpdateStatus(config.UpdatedFailed, fmt.Sprintf("Code: %s, Message: %s", status.Status.Code, status.Status.Message))
}
}
}
// 公共
func (dnspod *Dnspod) commonRequest(apiAddr string, values url.Values, domain *config.Domain) (status DnspodStatus, err error) {
resp, err := http.PostForm(
apiAddr,
values,
)
err = httputils.GetAndParseJSONResponseFromHttpResponse(resp, &status)
return
}
// 获得域名记录列表
func (dnspod *Dnspod) getRecordList(domain *config.Domain, typ string) (result DnspodRecordListResp, err error) {
values := url.Values{
"login_token": {dnspod.task.DNS.ID + "," + dnspod.task.DNS.Secret},
"domain": {domain.DomainName},
"record_type": {typ},
"sub_domain": {domain.GetSubDomain()},
"format": {"json"},
}
client, e := dnspod.CreateHTTPClient()
if e != nil {
err = e
return
}
resp, err := client.PostForm(
recordListAPI,
values,
)
err = httputils.GetAndParseJSONResponseFromHttpResponse(resp, result)
return
}

221
ddns/huawei.go Normal file
View File

@ -0,0 +1,221 @@
package ddns
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"github.com/gdy666/lucky/config"
"github.com/gdy666/lucky/thirdlib/gdylib/httputils"
"github.com/gdy666/lucky/thirdlib/jeessy2/ddns-go/util"
)
const (
huaweicloudEndpoint string = "https://dns.myhuaweicloud.com"
)
// https://support.huaweicloud.com/api-dns/dns_api_64001.html
// Huaweicloud Huaweicloud
type Huaweicloud struct {
DNSCommon
TTL int
}
// HuaweicloudZonesResp zones response
type HuaweicloudZonesResp struct {
Zones []struct {
ID string
Name string
Recordsets []HuaweicloudRecordsets
}
}
// HuaweicloudRecordsResp 记录返回结果
type HuaweicloudRecordsResp struct {
Recordsets []HuaweicloudRecordsets
}
// HuaweicloudRecordsets 记录
type HuaweicloudRecordsets struct {
ID string
Name string `json:"name"`
ZoneID string `json:"zone_id"`
Status string
Type string `json:"type"`
TTL int `json:"ttl"`
Records []string `json:"records"`
}
// Init 初始化
func (hw *Huaweicloud) Init(task *config.DDNSTask) {
hw.DNSCommon.Init(task)
if task.TTL == "" {
// 默认300s
hw.TTL = 300
} else {
ttl, err := strconv.Atoi(task.TTL)
if err != nil {
hw.TTL = 300
} else {
hw.TTL = ttl
}
}
hw.SetCreateUpdateDomainFunc(hw.createUpdateDomain)
}
func (hw *Huaweicloud) createUpdateDomain(recordType, ipAddr string, domain *config.Domain) {
var records HuaweicloudRecordsResp
err := hw.request(
"GET",
fmt.Sprintf(huaweicloudEndpoint+"/v2/recordsets?type=%s&name=%s", recordType, domain),
nil,
&records,
)
if err != nil {
return
}
find := false
for _, record := range records.Recordsets {
// 名称相同才更新。华为云默认是模糊搜索
if record.Name == domain.String()+"." {
// 更新
hw.modify(record, domain, recordType, ipAddr)
find = true
break
}
}
if !find {
// 新增
hw.create(domain, recordType, ipAddr)
}
}
// 创建
func (hw *Huaweicloud) create(domain *config.Domain, recordType string, ipAddr string) {
zone, err := hw.getZones(domain)
if err != nil {
return
}
if len(zone.Zones) == 0 {
log.Println("未能找到公网域名, 请检查域名是否添加")
return
}
zoneID := zone.Zones[0].ID
for _, z := range zone.Zones {
if z.Name == domain.DomainName+"." {
zoneID = z.ID
break
}
}
record := &HuaweicloudRecordsets{
Type: recordType,
Name: domain.String() + ".",
Records: []string{ipAddr},
TTL: hw.TTL,
}
var result HuaweicloudRecordsets
err = hw.request(
"POST",
fmt.Sprintf(huaweicloudEndpoint+"/v2/zones/%s/recordsets", zoneID),
record,
&result,
)
if err == nil && (len(result.Records) > 0 && result.Records[0] == ipAddr) {
//log.Printf("新增域名解析 %s 成功IP: %s", domain, ipAddr)
domain.SetDomainUpdateStatus(config.UpdatedSuccess, "")
} else {
//log.Printf("新增域名解析 %s 失败Status: %s", domain, result.Status)
domain.SetDomainUpdateStatus(config.UpdatedFailed, result.Status)
}
}
// 修改
func (hw *Huaweicloud) modify(record HuaweicloudRecordsets, domain *config.Domain, recordType string, ipAddr string) {
// 相同不修改
if len(record.Records) > 0 && record.Records[0] == ipAddr {
//log.Printf("你的IP %s 没有变化, 域名 %s", ipAddr, domain)
domain.SetDomainUpdateStatus(config.UpdatedNothing, "")
return
}
var request map[string]interface{} = make(map[string]interface{})
request["records"] = []string{ipAddr}
request["ttl"] = hw.TTL
var result HuaweicloudRecordsets
err := hw.request(
"PUT",
fmt.Sprintf(huaweicloudEndpoint+"/v2/zones/%s/recordsets/%s", record.ZoneID, record.ID),
&request,
&result,
)
if err == nil && (len(result.Records) > 0 && result.Records[0] == ipAddr) {
//log.Printf("更新域名解析 %s 成功IP: %s, 状态: %s", domain, ipAddr, result.Status)
domain.SetDomainUpdateStatus(config.UpdatedSuccess, "")
} else {
//log.Printf("更新域名解析 %s 失败Status: %s", domain, result.Status)
domain.SetDomainUpdateStatus(config.UpdatedFailed, result.Status)
}
}
// 获得域名记录列表
func (hw *Huaweicloud) getZones(domain *config.Domain) (result HuaweicloudZonesResp, err error) {
err = hw.request(
"GET",
fmt.Sprintf(huaweicloudEndpoint+"/v2/zones?name=%s", domain.DomainName),
nil,
&result,
)
return
}
// request 统一请求接口
func (hw *Huaweicloud) request(method string, url string, data interface{}, result interface{}) (err error) {
jsonStr := make([]byte, 0)
if data != nil {
jsonStr, _ = json.Marshal(data)
}
req, err := http.NewRequest(
method,
url,
bytes.NewBuffer(jsonStr),
)
if err != nil {
log.Println("http.NewRequest失败. Error: ", err)
return
}
s := util.Signer{
Key: hw.task.DNS.ID,
Secret: hw.task.DNS.Secret,
}
s.Sign(req)
req.Header.Add("content-type", "application/json")
client, err := hw.CreateHTTPClient()
if err != nil {
return err
}
resp, err := client.Do(req)
err = httputils.GetAndParseJSONResponseFromHttpResponse(resp, result)
return
}

View File

@ -6,7 +6,7 @@ package main
import (
"fmt"
"github.com/ljymc/goports/thirdlib/gdylib/recoverutil"
"github.com/gdy666/lucky/thirdlib/gdylib/recoverutil"
)
func init() {

22
go.mod
View File

@ -1,17 +1,16 @@
module github.com/ljymc/goports
module github.com/gdy666/lucky
go 1.18
require (
github.com/fatedier/golib v0.2.0
golang.org/x/net v0.0.0-20220524220425-1d687d428aca // indirect
)
require (
github.com/gin-contrib/gzip v0.0.5
github.com/gin-contrib/gzip v0.0.6
github.com/gin-gonic/gin v1.8.1
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/shirou/gopsutil/v3 v3.22.5
github.com/guonaihong/gout v0.2.12
github.com/miekg/dns v1.1.50
github.com/shirou/gopsutil/v3 v3.22.6
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985
)
require (
@ -28,17 +27,18 @@ require (
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/tklauser/go-sysconf v0.3.10 // indirect
github.com/tklauser/numcpus v0.4.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
)
replace github.com/ljymc/toolbox/fileutils => ../toolbox/fileutils/

55
go.sum
View File

@ -4,11 +4,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatedier/golib v0.2.0 h1:8BxiUcjleBlXBYlTNUllD8KZZHaFU/NP/vP0Yu1Fkpg=
github.com/fatedier/golib v0.2.0/go.mod h1:e2NPpBGUFsHDjXrfP1B5aK3S0+yUeVxgqfc3go3KNj0=
github.com/gin-contrib/gzip v0.0.5 h1:mhnVU32YnnBh2LPH2iqRqsA/eR7SAqRaD388jL2s/j0=
github.com/gin-contrib/gzip v0.0.5/go.mod h1:OPIK6HR0Um2vNmBUTlayD7qle4yVVRZT0PyhdUigrKk=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
@ -36,6 +36,10 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/guonaihong/gout v0.2.12 h1:ZNtg0Nq6yzlBNgDhFF2YaRdngZCIPuijgaA0s+rCvfY=
github.com/guonaihong/gout v0.2.12/go.mod h1:25bJRA+9fErgSvEUF5UAazc9dME+VdkjrFu7yrrVQUg=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@ -55,6 +59,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
@ -63,6 +69,8 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
@ -70,15 +78,18 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:Om
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/shirou/gopsutil/v3 v3.22.5 h1:atX36I/IXgFiB81687vSiBI5zrMsxcIBkP9cQMJQoJA=
github.com/shirou/gopsutil/v3 v3.22.5/go.mod h1:so9G9VzeHt/hsd0YwqprnjHnfARAUktauykSbr+y2gA=
github.com/shirou/gopsutil/v3 v3.22.6 h1:FnHOFOh+cYAM0C30P+zysPISzlknLC5Z1G4EAElznfQ=
github.com/shirou/gopsutil/v3 v3.22.6/go.mod h1:EdIubSnZhbAvBS1yJ7Xi+AShB/hxwLHOMz4MCYz7yMs=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=
github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o=
@ -88,24 +99,37 @@ github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220524220425-1d687d428aca h1:xTaFYiPROfpPhqrfTIDXj0ri1SpfueYT951s4bAuDO8=
golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8=
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -116,12 +140,19 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -135,5 +166,5 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

BIN
goports Normal file

Binary file not shown.

21
main.go
View File

@ -10,9 +10,10 @@ import (
"syscall"
"time"
"github.com/ljymc/goports/base"
"github.com/ljymc/goports/config"
"github.com/ljymc/goports/rule"
"github.com/gdy666/lucky/base"
"github.com/gdy666/lucky/config"
"github.com/gdy666/lucky/ddns"
"github.com/gdy666/lucky/rule"
)
var (
@ -28,17 +29,16 @@ var (
var (
runMode = "prod"
version = "unknown"
version = "dev"
commit = "none"
date = "unknown"
)
var runTime time.Time
// go build && ./goports 127.0.0.199:7000,443to192.168.31.1:80,443 20100-20110to192.168.31.1:20100-20110
func main() {
flag.Parse()
config.InitAppInfo(version, date)
err := config.Read(*configureFileURL)
if err != nil {
@ -81,6 +81,15 @@ func main() {
LoadRuleFromConfigFile(gcf)
rule.EnableAllRelayRule() //开启规则
config.DDNSTaskListTaskDetailsInit()
ddnsConf := config.GetDDNSConfigure()
if ddnsConf.Enable {
ddns.Run(time.Duration(ddnsConf.FirstCheckDelay)*time.Second, time.Duration(ddnsConf.Intervals)*time.Second)
}
//ddns.RunTimer(time.Second, time.Second*30)
//initProxyList()
//*****************

BIN
previews/ddnslist.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

BIN
previews/domainsync.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
previews/iphistroy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
previews/webhookhistroy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -5,8 +5,8 @@ import (
"fmt"
"sync"
"github.com/ljymc/goports/base"
"github.com/ljymc/goports/config"
"github.com/gdy666/lucky/base"
"github.com/gdy666/lucky/config"
)
var globalRelayRules *[]RelayRule

View File

@ -2,7 +2,7 @@
package rule
import (
"github.com/ljymc/goports/base"
"github.com/gdy666/lucky/base"
)
type RelayRuleProxyInfo struct {

View File

@ -8,7 +8,7 @@ import (
"strconv"
"strings"
"github.com/ljymc/goports/base"
"github.com/gdy666/lucky/base"
)
type RelayRule struct {

View File

@ -0,0 +1,151 @@
package httputils
import (
"compress/gzip"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"time"
"golang.org/x/net/proxy"
)
func GetAndParseJSONResponseFromHttpResponse(resp *http.Response, result interface{}) error {
bytes, err := GetBytesFromHttpResponse(resp)
if err != nil {
return fmt.Errorf("GetBytesFromHttpResponse err:%s", err.Error())
}
if len(bytes) > 0 {
err = json.Unmarshal(bytes, &result)
if err != nil {
//log.Printf("请求接口解析json结果失败! ERROR: %s\n", err)
return fmt.Errorf("GetAndParseJSONResponseFromHttpResponse 解析JSON结果出错%s", err.Error())
}
}
return nil
}
//GetStringFromHttpResponse 从response获取
func GetBytesFromHttpResponse(resp *http.Response) ([]byte, error) {
if resp.Body == nil {
return []byte{}, nil
}
defer resp.Body.Close()
var body []byte
var err error
if resp.Header.Get("Content-Encoding") == "gzip" {
reader, err := gzip.NewReader(resp.Body)
if err != nil {
return []byte{}, err
}
body, err = ioutil.ReadAll(reader)
return body, err
}
body, err = ioutil.ReadAll(resp.Body)
return body, err
}
//GetStringFromHttpResponse 从response获取
func GetStringFromHttpResponse(resp *http.Response) (string, error) {
respBytes, err := GetBytesFromHttpResponse(resp)
if err != nil {
return "", err
}
return string(respBytes), nil
}
func NewTransport(secureSkipVerify bool, proxyType, proxyUrl, user, passwd string) (*http.Transport, error) {
var transport *http.Transport
proxyType = strings.ToLower(proxyType)
switch proxyType {
case "http", "https":
{
if !strings.Contains(proxyUrl, "http") {
proxyUrl = fmt.Sprintf("%s://%s", proxyType, proxyUrl)
}
urlProxy, err := url.Parse(proxyUrl)
if err != nil {
return nil, fmt.Errorf("NewTransport=>proxy url.Parse error:%s", err.Error())
}
if user != "" && passwd != "" {
urlProxy.User = url.UserPassword(user, passwd)
}
transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: secureSkipVerify},
Proxy: http.ProxyURL(urlProxy),
Dial: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
IdleConnTimeout: 10 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}
}
case "socket5", "socks5":
{
var userAuth proxy.Auth
if user != "" && passwd != "" {
userAuth.User = user
userAuth.Password = passwd
}
dialer, err := proxy.SOCKS5("tcp", proxyUrl, &userAuth, proxy.Direct)
if err != nil {
return nil, fmt.Errorf("NewTransport=>proxy.SOCKS5 error:%s", err.Error())
}
transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: secureSkipVerify},
DialContext: func(ctx context.Context, network, addr string) (conn net.Conn, err error) {
return dialer.Dial(network, addr)
},
Dial: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
IdleConnTimeout: 10 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}
}
default:
{
transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: secureSkipVerify},
Dial: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
IdleConnTimeout: 10 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}
}
}
return transport, nil
}
func CreateHeadersMap(headers []string) map[string]string {
hm := make(map[string]string)
for _, header := range headers {
kvSpliteIndex := strings.Index(header, ":")
if kvSpliteIndex < 0 {
continue
}
if kvSpliteIndex+1 > len(header) {
continue
}
key := header[:kvSpliteIndex]
value := header[kvSpliteIndex+1:]
hm[key] = value
}
return hm
}

View File

@ -0,0 +1,94 @@
package httputils
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/guonaihong/gout"
"github.com/guonaihong/gout/dataflow"
)
func NewGout(secureSkipVerify bool, proxyType, proxyUrl, user, passwd string, timeout time.Duration) (*dataflow.Gout, error) {
httpClient, err := CreateHttpClient(secureSkipVerify, proxyType, proxyUrl, user, passwd, timeout)
if err != nil {
return nil, fmt.Errorf("CreateHttpClient error:%s", err.Error())
}
return gout.New(httpClient), nil
}
func GetAndParseJSONResponseFromGoutDoHttpRequest(method, url, requestBody, proxyType, proxyUrl, user, passwd string, headers map[string]string, secureSkipVerify bool, timeout time.Duration, result interface{}) error {
bytes, err := GetBytesFromGoutDoHttpRequest(method, url, requestBody, proxyType, proxyUrl, user, passwd, headers, secureSkipVerify, timeout)
if err != nil {
return fmt.Errorf("GetBytesFromHttpResponse err:%s", err.Error())
}
if len(bytes) > 0 {
err = json.Unmarshal(bytes, &result)
if err != nil {
return fmt.Errorf("GetAndParseJSONResponseFromHttpResponse 解析JSON结果出错%s", err.Error())
}
}
return nil
}
func GetStringGoutDoHttpRequest(method, url, requestBody, proxyType, proxyUrl, user, passwd string, headers map[string]string, secureSkipVerify bool, timeout time.Duration) (string, error) {
bytes, err := GetBytesFromGoutDoHttpRequest(method, url, requestBody, proxyType, proxyUrl, user, passwd, headers, secureSkipVerify, timeout)
if err != nil {
return "", err
}
return string(bytes), nil
}
func GetBytesFromGoutDoHttpRequest(method, url, requestBody, proxyType, proxyUrl, user, passwd string, headers map[string]string, secureSkipVerify bool, timeout time.Duration) ([]byte, error) {
gout, err := NewGout(
secureSkipVerify,
proxyType,
proxyUrl,
user,
passwd, timeout)
if err != nil {
return []byte{}, fmt.Errorf("GoutDoHttpRequest err:%s", err.Error())
}
switch strings.ToLower(method) {
case "get":
gout.GET(url)
case "post":
gout.POST(url)
case "put":
gout.PUT(url)
case "delete":
gout.DELETE(url)
default:
return []byte{}, fmt.Errorf("未支持的Callback请求方法:%s", method)
}
basicAuthUserName, BasicAuthUserNameOk := headers["BasicAuthUserName"]
basicAuthPassword, BasicAuthPasswordOk := headers["BasicAuthPassword"]
if BasicAuthUserNameOk && BasicAuthPasswordOk {
gout.SetBasicAuth(basicAuthUserName, basicAuthPassword)
}
delete(headers, "BasicAuthUserName")
delete(headers, "BasicAuthPassword")
if len(requestBody) > 0 && method != "get" {
if json.Valid([]byte(requestBody)) {
gout.SetJSON(requestBody)
} else {
gout.SetWWWForm(requestBody)
}
}
gout.SetHeader(headers)
//gout.SetTimeout(timeout)
resp, err := gout.Response()
if err != nil {
return []byte{}, fmt.Errorf("gout.Response() error:%s", err.Error())
}
return GetBytesFromHttpResponse(resp)
}

View File

@ -0,0 +1,19 @@
package httputils
import (
"net/http"
"time"
)
func CreateHttpClient(secureSkipVerify bool, proxyType, proxyUrl, user, passwd string, timeout time.Duration) (*http.Client, error) {
transport, err := NewTransport(secureSkipVerify, proxyType, proxyUrl, user, passwd)
if err != nil {
return nil, err
}
httpClient := &http.Client{
Timeout: timeout,
Transport: transport}
return httpClient, nil
}

View File

@ -7,8 +7,8 @@ import (
"strings"
"time"
"github.com/ljymc/goports/thirdlib/gdylib/fileutils"
"github.com/ljymc/goports/thirdlib/gdylib/stderrredirect"
"github.com/gdy666/lucky/thirdlib/gdylib/fileutils"
"github.com/gdy666/lucky/thirdlib/gdylib/stderrredirect"
)
//RecoverHandler 恢复处理

View File

@ -0,0 +1,297 @@
package service
import (
"context"
"fmt"
"log"
"sync"
"time"
)
const (
//StateStop 未启动
StateStop = 0
//StateRunning 正在运行
StateRunning = 1
//StateStopping 正在结束
StateStopping = 2
)
//ServiceMsg 服务消息
type ServiceMsg struct {
Type string
Params []any
}
//var serviceMap map[string]*Service //用于保存已创建的服务
//var serverMapRWLock sync.RWMutex
var serviceMap = struct {
sync.RWMutex
services map[string]*Service
}{
services: map[string]*Service{},
}
//Service 服务
type Service struct {
Name string //服务名称
serviceMutex sync.Mutex
State uint8 //服务状态 //服务运行状态 , 0:未启动 1:正在运行 2:正在结束
NextAction string //动作名称
DefaultAction string //默认动作
TimerFunc func(...any) //定时回调函数
EventFunc func(...any) //事件回调函数
StopFinishedCallback func(...any) //服务停止后的回调函数
eventChan chan any
Timer *time.Timer
//Args []interface{}
context context.Context
cancelFunc context.CancelFunc
}
//NewService 创建服务对象
func NewService(name string) (*Service, error) {
serviceMap.Lock()
defer serviceMap.Unlock()
if _, ok := serviceMap.services[name]; ok {
return nil, fmt.Errorf("命名为[%s]服务已存在", name)
}
if _, ok := serviceMap.services[name]; !ok {
service := Service{Name: name, State: StateStop}
service.eventChan = make(chan any, 32)
serviceMap.services[name] = &service
return &service, nil
}
panic(fmt.Sprintf("命名为[%s]服务已存在", name))
//return nil, fmt.Errorf("命名为[%s]服务已存在", name)
}
//SetDefaultAction 设置默认action
func (s *Service) SetDefaultAction(action string) *Service {
s.DefaultAction = action
return s
}
//SetTimerFunc 设置定时功能函数
func (s *Service) SetTimerFunc(timerFunc func(...any)) *Service {
s.TimerFunc = timerFunc
return s
}
//SetEventFunc 设置时间功能函数
func (s *Service) SetEventFunc(eventFunc func(...any)) *Service {
s.EventFunc = eventFunc
return s
}
//SetStopFinishedCallback 设置服务停止后的回调函数
func (s *Service) SetStopFinishedCallback(f func(...any)) *Service {
s.StopFinishedCallback = f
return s
}
//Start 服务启动
func (s *Service) Start(vs ...any) error {
s.serviceMutex.Lock()
defer s.serviceMutex.Unlock()
if s.State == StateRunning {
text := fmt.Sprintf("服务 [%s]已启动,无需再次启动", s.Name)
return fmt.Errorf(text)
}
if s.State == StateStopping {
text := fmt.Sprintf("服务[%s]正在结束,请结束后再次启动", s.Name)
return fmt.Errorf(text)
}
s.State = StateRunning
s.NextAction = s.DefaultAction
log.Printf("服务[%s] 启动", s.Name)
s.context, s.cancelFunc = context.WithCancel(context.Background())
go s.loop(vs)
return nil
}
//Stop 服务结束
func (s *Service) Stop() error {
s.serviceMutex.Lock()
defer s.serviceMutex.Unlock()
if s.State == StateStop {
text := fmt.Sprintf("服务[%s]未启动,无须停止", s.Name)
return fmt.Errorf(text)
}
if s.State == StateStopping {
text := fmt.Sprintf("服务[%s]正在结束,无须再次结束.", s.Name)
return fmt.Errorf(text)
}
if s.cancelFunc == nil {
return fmt.Errorf("服务[%s]context nil", s.Name)
}
s.cancelFunc()
s.State = StateStopping
return nil
}
func (s *Service) loop(params ...any) {
defer func() {
recoverErr := recover()
if recoverErr == nil {
return
}
log.Printf("service[%s] panic:\n%v", s.Name, recoverErr)
s.State = StateStop
log.Printf("server[%s] restart", s.Name)
s.Start()
}()
s.Timer = time.NewTimer(0)
for {
select {
case <-s.Timer.C:
{
if s.TimerFunc != nil { //如果设置了定时回调的话
s.TimerFunc(s, params)
}
}
case <-s.context.Done():
{
if s.State == StateStopping {
s.State = StateStop
log.Printf("服务[%s] 停止", s.Name)
if s.StopFinishedCallback != nil {
s.StopFinishedCallback()
}
s.context = nil
s.cancelFunc = nil
return
}
if s.State == StateStop {
log.Printf("服务[%s] 状态有误,结束服务失败", s.Name)
return
}
if s.State == StateRunning {
log.Printf("服务[%s] 状态有误,请使用正确途径结束", s.Name)
return
}
}
case msg := <-s.eventChan:
{
if s.EventFunc != nil {
s.EventFunc(s, msg)
}
}
}
}
}
//Restart 重启服务
func (s *Service) Restart(params ...any) error {
s.Stop()
var timeout time.Duration
if len(params) == 0 {
timeout = time.Second * 15
} else {
timeout = params[0].(time.Duration)
}
preTime := time.Now()
for {
waitTime := time.Now()
if s.State == StateStop {
return s.Start()
}
if waitTime.Sub(preTime) > timeout {
return fmt.Errorf("重启服务[%s]超时", s.Name)
}
<-time.After(time.Millisecond * 100)
}
}
//Message 发送消息给service
//t 消息类型
//params 消息内容
func (s *Service) Message(t string, params ...any) error {
s.serviceMutex.Lock()
defer s.serviceMutex.Unlock()
if s.State != StateRunning {
return fmt.Errorf("[%s]服务已关闭或者正在关闭,无法处理消息 %v,", s.Name, params)
}
msg := ServiceMsg{Type: t}
for i := range params {
msg.Params = append(msg.Params, params[i])
}
select {
case s.eventChan <- msg:
default:
return fmt.Errorf("[%s]服务EventChan阻塞,无法处理event", s.Name)
}
return nil
}
//*************************************
//GetService 查询服务根据服务名称
func GetService(name string) (*Service, error) {
serviceMap.RLock()
defer serviceMap.RUnlock()
if _, ok := serviceMap.services[name]; !ok {
return nil, fmt.Errorf("service[%s]不存在", name)
}
service := serviceMap.services[name]
return service, nil
}
//Message 发送消息到service,根据serviceName
func Message(serviceName string, msgType string, params ...any) error {
service, err := GetService(serviceName)
if err != nil {
return fmt.Errorf("service[%s] Message faild,:%s", serviceName, err.Error())
}
return service.Message(msgType, params...)
}
//Stop 服务停止,根据serviceName
func Stop(serviceName string) error {
service, err := GetService(serviceName)
if err != nil {
return fmt.Errorf("service[%s] stop faild:%s", serviceName, err.Error())
}
return service.Stop()
}
//Restart 服务重启
func Restart(serviceName string) error {
service, err := GetService(serviceName)
if err != nil {
return fmt.Errorf("restart servcie[%s] faild,:%s", serviceName, err.Error())
}
return service.Restart()
}
//Start 服务启动
func Start(serviceName string) error {
service, err := GetService(serviceName)
if err != nil {
return fmt.Errorf("servcie[%s] start faild,:%s", serviceName, err.Error())
}
return service.Start()
}

View File

@ -7,7 +7,7 @@ import (
"sync"
"syscall"
"github.com/ljymc/goports/thirdlib/gdylib/fileutils"
"github.com/gdy666/lucky/thirdlib/gdylib/fileutils"
)
var PanicFile *os.File

View File

@ -7,7 +7,7 @@ import (
"sync"
"syscall"
"github.com/ljymc/goports/thirdlib/gdylib/fileutils"
"github.com/gdy666/lucky/thirdlib/gdylib/fileutils"
)
//错误输出重定向,用于捕获闪退信息

View File

@ -0,0 +1,97 @@
package util
import (
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"fmt"
"hash"
"io"
"net/url"
)
// https://github.com/rosbit/aliyun-sign/blob/master/aliyun-sign.go
var (
signMethodMap = map[string]func() hash.Hash{
"HMAC-SHA1": sha1.New,
"HMAC-SHA256": sha256.New,
"HMAC-MD5": md5.New,
}
)
func HmacSign(signMethod string, httpMethod string, appKeySecret string, vals url.Values) (signature []byte) {
key := []byte(appKeySecret + "&")
var h hash.Hash
if method, ok := signMethodMap[signMethod]; ok {
h = hmac.New(method, key)
} else {
h = hmac.New(sha1.New, key)
}
makeDataToSign(h, httpMethod, vals)
return h.Sum(nil)
}
func HmacSignToB64(signMethod string, httpMethod string, appKeySecret string, vals url.Values) (signature string) {
return base64.StdEncoding.EncodeToString(HmacSign(signMethod, httpMethod, appKeySecret, vals))
}
type strToEnc struct {
s string
e bool
}
func makeDataToSign(w io.Writer, httpMethod string, vals url.Values) {
in := make(chan *strToEnc)
go func() {
in <- &strToEnc{s: httpMethod}
in <- &strToEnc{s: "&"}
in <- &strToEnc{s: "/", e: true}
in <- &strToEnc{s: "&"}
in <- &strToEnc{s: vals.Encode(), e: true}
close(in)
}()
specialUrlEncode(in, w)
}
var (
encTilde = "%7E" // '~' -> "%7E"
encBlank = []byte("%20") // ' ' -> "%20"
tilde = []byte("~")
)
func specialUrlEncode(in <-chan *strToEnc, w io.Writer) {
for s := range in {
if !s.e {
io.WriteString(w, s.s)
continue
}
l := len(s.s)
for i := 0; i < l; {
ch := s.s[i]
switch ch {
case '%':
if encTilde == s.s[i:i+3] {
w.Write(tilde)
i += 3
continue
}
fallthrough
case '*', '/', '&', '=':
fmt.Fprintf(w, "%%%02X", ch)
case '+':
w.Write(encBlank)
default:
fmt.Fprintf(w, "%c", ch)
}
i += 1
}
}
}

View File

@ -0,0 +1,20 @@
package util
import (
"net/url"
"strconv"
"time"
)
// AliyunSigner AliyunSigner
func AliyunSigner(accessKeyID, accessSecret string, params *url.Values) {
// 公共参数
params.Set("SignatureMethod", "HMAC-SHA1")
params.Set("SignatureNonce", strconv.FormatInt(time.Now().UnixNano(), 10))
params.Set("AccessKeyId", accessKeyID)
params.Set("SignatureVersion", "1.0")
params.Set("Timestamp", time.Now().UTC().Format("2006-01-02T15:04:05Z"))
params.Set("Format", "JSON")
params.Set("Version", "2015-01-09")
params.Set("Signature", HmacSignToB64("HMAC-SHA1", "GET", accessSecret, *params))
}

View File

@ -0,0 +1,58 @@
package util
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strings"
"time"
)
// https://cloud.baidu.com/doc/Reference/s/Njwvz1wot
const (
BaiduDateFormat = "2006-01-02T15:04:05Z"
expirationPeriod = "1800"
)
func HmacSha256Hex(secret, message string) string {
key := []byte(secret)
h := hmac.New(sha256.New, key)
h.Write([]byte(message))
sha := hex.EncodeToString(h.Sum(nil))
return sha
}
func BaiduCanonicalURI(r *http.Request) string {
pattens := strings.Split(r.URL.Path, "/")
var uri []string
for _, v := range pattens {
uri = append(uri, escape(v))
}
urlpath := strings.Join(uri, "/")
if len(urlpath) == 0 || urlpath[len(urlpath)-1] != '/' {
urlpath = urlpath + "/"
}
return urlpath[0 : len(urlpath)-1]
}
// BaiduSigner set Authorization header
func BaiduSigner(accessKeyID, accessSecret string, r *http.Request) {
//format: bce-auth-v1/{accessKeyId}/{timestamp}/{expirationPeriodInSeconds}
authStringPrefix := "bce-auth-v1/" + accessKeyID + "/" + time.Now().UTC().Format(BaiduDateFormat) + "/" + expirationPeriod
baiduCanonicalURL := BaiduCanonicalURI(r)
//format: HTTP Method + "\n" + CanonicalURI + "\n" + CanonicalQueryString + "\n" + CanonicalHeaders
//由于仅仅调用三个POST接口且不会更改这里CanonicalQueryString和CanonicalHeaders直接写死
CanonicalReq := fmt.Sprintf("%s\n%s\n%s\n%s", r.Method, baiduCanonicalURL, "", "host:bcd.baidubce.com")
signingKey := HmacSha256Hex(accessSecret, authStringPrefix)
signature := HmacSha256Hex(signingKey, CanonicalReq)
//format: authStringPrefix/{signedHeaders}/{signature}
authString := authStringPrefix + "/host/" + signature
r.Header.Set(HeaderAuthorization, authString)
}

View File

@ -0,0 +1,42 @@
// based on https://github.com/golang/go/blob/master/src/net/url/url.go
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package util
func shouldEscape(c byte) bool {
if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '_' || c == '-' || c == '~' || c == '.' {
return false
}
return true
}
func escape(s string) string {
hexCount := 0
for i := 0; i < len(s); i++ {
c := s[i]
if shouldEscape(c) {
hexCount++
}
}
if hexCount == 0 {
return s
}
t := make([]byte, len(s)+2*hexCount)
j := 0
for i := 0; i < len(s); i++ {
switch c := s[i]; {
case shouldEscape(c):
t[j] = '%'
t[j+1] = "0123456789ABCDEF"[c>>4]
t[j+2] = "0123456789ABCDEF"[c&15]
j += 3
default:
t[j] = s[i]
j++
}
}
return string(t)
}

View File

@ -0,0 +1,208 @@
// HWS API Gateway Signature
// based on https://github.com/datastream/aws/blob/master/signv4.go
// Copyright (c) 2014, Xianjie
package util
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"fmt"
"io/ioutil"
"net/http"
"sort"
"strings"
"time"
)
const (
BasicDateFormat = "20060102T150405Z"
Algorithm = "SDK-HMAC-SHA256"
HeaderXDate = "X-Sdk-Date"
HeaderHost = "host"
HeaderAuthorization = "Authorization"
HeaderContentSha256 = "X-Sdk-Content-Sha256"
)
func hmacsha256(key []byte, data string) ([]byte, error) {
h := hmac.New(sha256.New, []byte(key))
if _, err := h.Write([]byte(data)); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
// Build a CanonicalRequest from a regular request string
//
// CanonicalRequest =
// HTTPRequestMethod + '\n' +
// CanonicalURI + '\n' +
// CanonicalQueryString + '\n' +
// CanonicalHeaders + '\n' +
// SignedHeaders + '\n' +
// HexEncode(Hash(RequestPayload))
func CanonicalRequest(r *http.Request, signedHeaders []string) (string, error) {
var hexencode string
var err error
if hex := r.Header.Get(HeaderContentSha256); hex != "" {
hexencode = hex
} else {
data, err := RequestPayload(r)
if err != nil {
return "", err
}
hexencode, err = HexEncodeSHA256Hash(data)
if err != nil {
return "", err
}
}
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", r.Method, CanonicalURI(r), CanonicalQueryString(r), CanonicalHeaders(r, signedHeaders), strings.Join(signedHeaders, ";"), hexencode), err
}
// CanonicalURI returns request uri
func CanonicalURI(r *http.Request) string {
pattens := strings.Split(r.URL.Path, "/")
var uri []string
for _, v := range pattens {
uri = append(uri, escape(v))
}
urlpath := strings.Join(uri, "/")
if len(urlpath) == 0 || urlpath[len(urlpath)-1] != '/' {
urlpath = urlpath + "/"
}
return urlpath
}
// CanonicalQueryString
func CanonicalQueryString(r *http.Request) string {
var keys []string
query := r.URL.Query()
for key := range query {
keys = append(keys, key)
}
sort.Strings(keys)
var a []string
for _, key := range keys {
k := escape(key)
sort.Strings(query[key])
for _, v := range query[key] {
kv := fmt.Sprintf("%s=%s", k, escape(v))
a = append(a, kv)
}
}
queryStr := strings.Join(a, "&")
r.URL.RawQuery = queryStr
return queryStr
}
// CanonicalHeaders
func CanonicalHeaders(r *http.Request, signerHeaders []string) string {
var a []string
header := make(map[string][]string)
for k, v := range r.Header {
header[strings.ToLower(k)] = v
}
for _, key := range signerHeaders {
value := header[key]
if strings.EqualFold(key, HeaderHost) {
value = []string{r.Host}
}
sort.Strings(value)
for _, v := range value {
a = append(a, key+":"+strings.TrimSpace(v))
}
}
return fmt.Sprintf("%s\n", strings.Join(a, "\n"))
}
// SignedHeaders
func SignedHeaders(r *http.Request) []string {
var a []string
for key := range r.Header {
a = append(a, strings.ToLower(key))
}
sort.Strings(a)
return a
}
// RequestPayload
func RequestPayload(r *http.Request) ([]byte, error) {
if r.Body == nil {
return []byte(""), nil
}
b, err := ioutil.ReadAll(r.Body)
if err != nil {
return []byte(""), err
}
r.Body = ioutil.NopCloser(bytes.NewBuffer(b))
return b, err
}
// Create a "String to Sign".
func StringToSign(canonicalRequest string, t time.Time) (string, error) {
hash := sha256.New()
_, err := hash.Write([]byte(canonicalRequest))
if err != nil {
return "", err
}
return fmt.Sprintf("%s\n%s\n%x",
Algorithm, t.UTC().Format(BasicDateFormat), hash.Sum(nil)), nil
}
// Create the HWS Signature.
func SignStringToSign(stringToSign string, signingKey []byte) (string, error) {
hm, err := hmacsha256(signingKey, stringToSign)
return fmt.Sprintf("%x", hm), err
}
// HexEncodeSHA256Hash returns hexcode of sha256
func HexEncodeSHA256Hash(body []byte) (string, error) {
hash := sha256.New()
if body == nil {
body = []byte("")
}
_, err := hash.Write(body)
return fmt.Sprintf("%x", hash.Sum(nil)), err
}
// Get the finalized value for the "Authorization" header. The signature parameter is the output from SignStringToSign
func AuthHeaderValue(signature, accessKey string, signedHeaders []string) string {
return fmt.Sprintf("%s Access=%s, SignedHeaders=%s, Signature=%s", Algorithm, accessKey, strings.Join(signedHeaders, ";"), signature)
}
// Signature HWS meta
type Signer struct {
Key string
Secret string
}
// SignRequest set Authorization header
func (s *Signer) Sign(r *http.Request) error {
var t time.Time
var err error
var dt string
if dt = r.Header.Get(HeaderXDate); dt != "" {
t, err = time.Parse(BasicDateFormat, dt)
}
if err != nil || dt == "" {
t = time.Now()
r.Header.Set(HeaderXDate, t.UTC().Format(BasicDateFormat))
}
signedHeaders := SignedHeaders(r)
canonicalRequest, err := CanonicalRequest(r, signedHeaders)
if err != nil {
return err
}
stringToSign, err := StringToSign(canonicalRequest, t)
if err != nil {
return err
}
signature, err := SignStringToSign(stringToSign, []byte(s.Secret))
if err != nil {
return err
}
authValue := AuthHeaderValue(signature, s.Key, signedHeaders)
r.Header.Set(HeaderAuthorization, authValue)
return nil
}

View File

@ -0,0 +1,35 @@
package util
import (
"net"
"strings"
)
// IsPrivateNetwork 是否为私有地址
// https://en.wikipedia.org/wiki/Private_network
func IsPrivateNetwork(remoteAddr string) bool {
lastIndex := strings.LastIndex(remoteAddr, ":")
if lastIndex < 1 {
return false
}
remoteAddr = remoteAddr[:lastIndex]
// ipv6
if strings.HasPrefix(remoteAddr, "[") && strings.HasSuffix(remoteAddr, "]") {
remoteAddr = remoteAddr[1 : len(remoteAddr)-1]
}
if ip := net.ParseIP(remoteAddr); ip != nil {
return ip.IsLoopback() || // 127/8, ::1
ip.IsPrivate() || // 10/8, 172.16/12, 192.168/16, fc00::/7
ip.IsLinkLocalUnicast() // 169.254/16, fe80::/10
}
// localhost
if remoteAddr == "localhost" {
return true
}
return false
}

2
web.go
View File

@ -5,7 +5,7 @@ package main
import (
"fmt"
"github.com/ljymc/goports/web"
"github.com/gdy666/lucky/web"
"log"
)

View File

@ -1,4 +1,4 @@
# goports-adminviews
# lucky-adminviews
This template should help get you started developing with Vue 3 in Vite.

5
web/adminviews/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
}

53
web/adminviews/components.d.ts vendored Normal file
View File

@ -0,0 +1,53 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
declare module '@vue/runtime-core' {
export interface GlobalComponents {
About: typeof import('./src/components/About.vue')['default']
BlackLists: typeof import('./src/components/BlackLists.vue')['default']
DDNS: typeof import('./src/components/DDNS.vue')['default']
DDNSSet: typeof import('./src/components/DDNSSet.vue')['default']
ElAffix: typeof import('element-plus/es')['ElAffix']
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
Loading: typeof import('element-plus/es')['ElLoadingDirective']
Log: typeof import('./src/components/Log.vue')['default']
Login: typeof import('./src/components/Login.vue')['default']
Pmenu: typeof import('./src/components/Pmenu.vue')['default']
PSet: typeof import('./src/components/PSet.vue')['default']
RelaySet: typeof import('./src/components/RelaySet.vue')['default']
Status: typeof import('./src/components/Status.vue')['default']
WhiteLists: typeof import('./src/components/WhiteLists.vue')['default']
WhiteListSet: typeof import('./src/components/WhiteListSet.vue')['default']
}
}
export {}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -7,9 +7,9 @@
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1.0, maximum-scale=1.0,minimum-scale=1.0" />
<title>goports-admin</title>
<script type="module" crossorigin src="/assets/index.8e296adf.js"></script>
<link rel="stylesheet" href="/assets/index.60201501.css">
<title>Lucky(大吉)</title>
<script type="module" crossorigin src="/assets/index.b93c8c58.js"></script>
<link rel="stylesheet" href="/assets/index.48f5287e.css">
</head>
<body style="margin:0">
<div id="app"></div>

View File

@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1.0, maximum-scale=1.0,minimum-scale=1.0" />
<title>goports-admin</title>
<title>Lucky(大吉)</title>
</head>
<body style="margin:0">
<div id="app"></div>

17487
web/adminviews/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"name": "goports-adminviews",
"name": "lucky-adminviews",
"version": "0.1.0",
"scripts": {
"dev": "vite",

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -23,6 +23,9 @@
<WhiteListSet v-if="global.currentPage.value=='#whitelistset'?true:false"></WhiteListSet>
<WhiteLists v-if="global.currentPage.value=='#whitelists'?true:false"></WhiteLists>
<BlackLists v-if="global.currentPage.value=='#blacklists'?true:false"></BlackLists>
<DDNS v-if="global.currentPage.value=='#ddnstasklist'?true:false"></DDNS>
<DDNSSet v-if="global.currentPage.value=='#ddnsset'?true:false"></DDNSSet>
<About v-if="global.currentPage.value=='#about'?true:false"></About>
</el-main>
</el-container>
@ -45,9 +48,11 @@ import Login from './components/login.vue';
import WhiteListSet from './components/WhiteListSet.vue';
import WhiteLists from './components/WhiteLists.vue';
import BlackLists from './components/BlackLists.vue';
import DDNS from './components/DDNS.vue';
import {apiGetVersion} from "./apis/utils.js"
import DDNSSet from './components/DDNSSet.vue';
//console.log("111")

View File

@ -15,13 +15,13 @@
*/
export default {
getStorage () { // 先获取该项目的 命名存储空间 下的storage数据 maneger
return JSON.parse(window.localStorage.getItem("goports") || "{}");
return JSON.parse(window.localStorage.getItem("lucky") || "{}");
},
setItem (key, val) {
let storage = this.getStorage()
// console.log("setItem", storage);
storage[key] = val; // 为当前对象添加 需要存储的值
window.localStorage.setItem("goports", JSON.stringify(storage)) // 保存到本地
window.localStorage.setItem("lucky", JSON.stringify(storage)) // 保存到本地
},
getItem (key) {
return this.getStorage()[key]

View File

@ -58,6 +58,45 @@ export function apiAddRule(data) {
})
}
export function apiAddDDNSTask(data) {
return httpRequest({
url: '/api/ddns',
method: 'post',
headers:{'Authorization':GetToken()},
data:data
})
}
export function apiAlterDDNSTask(taskKey,data) {
return httpRequest({
url: '/api/ddns',
method: 'put',
headers:{'Authorization':GetToken()},
data:data,
params:{key:taskKey}
})
}
export function apiDeleteDDNSTask(taskKey) {
return httpRequest({
url: '/api/ddns',
method: 'delete',
headers:{'Authorization':GetToken()},
params:{_:new Date().valueOf(),key:taskKey}
})
}
export function apiGetDDNSTaskList() {
return httpRequest({
url: '/api/ddnstasklist',
method: 'get',
headers:{'Authorization':GetToken()},
params:{_:new Date().valueOf()}
})
}
export function apiDeleteRule(configure) {
return httpRequest({
url: '/api/rule',
@ -85,6 +124,15 @@ export function apiRuleEnable(key,enable) {
})
}
export function apiDDNSTaskEnable(key,enable) {
return httpRequest({
url: '/api/ddns/enable',
method: 'get',
headers:{'Authorization':GetToken()},
params:{_:new Date().valueOf(),enable:enable,key:key}
})
}
export function apiQueryBaseConfigure() {
return httpRequest({
url: '/api/baseconfigure',
@ -94,6 +142,15 @@ export function apiQueryBaseConfigure() {
})
}
export function apiQueryDDNSConfigure() {
return httpRequest({
url: '/api/ddns/configure',
method: 'get',
headers:{'Authorization':GetToken()},
params:{_:new Date().valueOf()}
})
}
export function apiAlterBaseConfigure(data) {
return httpRequest({
url: '/api/baseconfigure',
@ -103,6 +160,15 @@ export function apiAlterBaseConfigure(data) {
})
}
export function apiAlterDDNSConfigure(data) {
return httpRequest({
url: '/api/ddns/configure',
method: 'put',
headers:{'Authorization':GetToken()},
data:data
})
}
export function apiLogin(data) {
return httpRequest({
url: '/api/login',
@ -213,3 +279,43 @@ export function apiLogout() {
headers:{'Authorization':GetToken()},
})
}
export function apiGetNetinterfaces() {
return httpRequest({
url: '/api/netinterfaces',
method: 'get',
headers:{'Authorization':GetToken()},
params:{_:new Date().valueOf()}
})
}
export function apiGetIPRegTest(iptype,netinterface,ipreg) {
return httpRequest({
url: '/api/ipregtest',
method: 'get',
headers:{'Authorization':GetToken()},
params:{_:new Date().valueOf(),iptype:iptype,netinterface:netinterface,ipreg:ipreg}
})
}
///api/webhooktest
export function apiWebhookTest(taskKey,data) {
return httpRequest({
url: 'api/webhooktest',
headers:{'Authorization':GetToken()},
method: 'post',
data:data,
params:{key:taskKey}
})
}
export function apiGetAPPInfo() {
return httpRequest({
url: '/api/info',
method: 'get',
headers:{'Authorization':GetToken()},
params:{_:new Date().valueOf()}
})
}

View File

Before

Width:  |  Height:  |  Size: 308 B

After

Width:  |  Height:  |  Size: 308 B

View File

@ -0,0 +1,106 @@
<template>
<div class="PageRadius" :style="{
borderRadius: 'base',
}">
<div class="InfoDivRadius">
<div class="line">
{{Info.AppName}}&nbsp;&nbsp;&nbsp;version:{{Info.Version}}
</div>
<div class="line">
{{Info.OS}}({{Info.ARCH}})
</div>
<div class="line">
作者:古大羊 &nbsp;编译于{{Info.Date}}
</div>
<div class="line">
<el-link type="primary" href="tencent://message/?uin=272288814&Site=&Menu=yes" target="_blank">QQ联系作者</el-link>
&nbsp;&nbsp;&nbsp;邮箱: 272288814@qq.com
</div>
<div class="line">
Github&nbsp;&nbsp;<el-link type="primary" href="https://github.com/gdy666/lucky" target="_blank">https://github.com/gdy666/lucky</el-link>
</div>
<div class="line">
Gitee&nbsp;&nbsp;<el-link type="primary" href="https://gitee.com/gdy666/lucky" target="_blank">https://gitee.com/gdy666/lucky</el-link>
</div>
<div class="line">
<el-link type="primary" href="https://pan.baidu.com/s/1NfumD9XjYU3OTeVmbu6vOQ?pwd=6666" target="_blank">最新版本可访问百度网盘</el-link>
</div>
<div>
本项目借鉴引用或参考的第三方开源项目: <el-link type="primary" href="https://github.com/fatedier/frp" target="_blank">frp</el-link> <el-link type="primary" href="https://github.com/jeessy2/ddns-go" target="_blank">ddns-go</el-link>
</div>
<div class="line">
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, computed, reactive } from 'vue'
import { apiGetAPPInfo } from '../apis/utils'
import {MessageShow} from '../utils/ui'
var Info = ref({
AppName:"Lucky",
Version:"1.0.0",
OS:"unknow",
ARCH:"unknow",
Date:"2022-07-25"
})
const queryAPPInfo = ()=>{
apiGetAPPInfo().then((res) => {
if (res.ret==0){
Info.value = res.info
return
}
MessageShow("error", "获取App信息出错")
}).catch((error) => {
console.log("获获取App信息出错:" + error)
MessageShow("error", "获取App信息出错")
})
}
onMounted(() => {
queryAPPInfo()
})
</script>
<style scoped>
.InfoDivRadius {
border: 2px solid var(--el-border-color);
border-radius: 10px;
margin:auto;
margin-top:50px;
width: 495px;
height: fit-content;
padding:auto;
padding-top: 20px;
padding-bottom: 30px;
}
.line {
margin-bottom: 5px;
}
</style>

View File

@ -48,7 +48,7 @@
</el-scrollbar>
<el-dialog v-model="addBlackListDialogVisible" title="添加黑名单IP" draggable :show-close="false" width="400px">
<el-dialog v-model="addBlackListDialogVisible" title="添加黑名单IP" draggable :show-close="false" :close-on-click-modal="false" width="400px">
<el-form :model="addBlackListForm">
<el-form-item label="IP" label-width="auto">

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,184 @@
<template>
<div class="PageRadius" :style="{
borderRadius: 'base',
}" v-loading="logLoading" element-loading-background="transparent">
<el-scrollbar height="100%">
<div class="formradius" :style="{
borderRadius: 'base',
}">
<el-form :model="form" class="SetForm" label-width="auto">
<el-tooltip content="如果不需要DDNS动态域名服务请不要打开这个开关" placement="top">
<el-form-item label="动态域名服务开关" id="adminListen">
<el-switch v-model="form.Enable" class="mb-1" inline-prompt
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949" width="50px"
active-text="开启" inactive-text="停用" />
</el-form-item>
</el-tooltip>
<el-tooltip content="多数嵌入式设备启用这个开关会导致https访问失败" placement="top">
<el-form-item label="Http(s) 客户端 安全证书验证" id="adminListen">
<el-switch v-model="form.HttpClientSecureVerify" class="mb-1" inline-prompt
style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949" width="50px"
active-text="启用" inactive-text="禁用" />
</el-form-item>
</el-tooltip>
<el-tooltip content="首次执行任务延迟时间,最小0秒,最长3600秒" placement="top">
<el-form-item label="首次执行任务延迟(秒)" label-width="auto" min="0" max="3600">
<el-input-number v-model="form.FirstCheckDelay" autocomplete="off" />
</el-form-item>
</el-tooltip>
<el-tooltip content="DDNS任务每次执行的时间间隔,最小30秒,最长3600秒" placement="top">
<el-form-item label="时间间隔(秒)" label-width="auto" :min="30" :max="3600">
<el-input-number v-model="form.Intervals" autocomplete="off" />
</el-form-item>
</el-tooltip>
</el-form>
<el-button type="primary" round @click="RequestAlterDDNSConfigure">保存修改</el-button>
</div>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref, computed, reactive } from 'vue'
import { apiQueryDDNSConfigure, apiAlterDDNSConfigure } from '../apis/utils'
import { MessageShow } from '../utils/ui'
const logLoading = ref(true)
const rawData = {
Enable: false,
HttpClientSecureVerify: false,
Intervals: 0,
FirstCheckDelay: 0,
}
const form = ref(rawData)
const preFormData = ref(rawData)
const resetFormData = () => {
form.value.Enable = preFormData.value.Enable
form.value.HttpClientSecureVerify = preFormData.value.HttpClientSecureVerify
}
const syncToPreFormData = (data: any) => {
preFormData.value.Enable = data.value.Enable
preFormData.value.HttpClientSecureVerify = data.value.HttpClientSecureVerify
}
const queryDDConfigure = () => {
apiQueryDDNSConfigure().then((res) => {
if (res.ret == 0) {
logLoading.value = false
form.value = res.ddnsconfigure
syncToPreFormData(form)
return
}
MessageShow("error", "获取DDNS配置出错")
}).catch((error) => {
MessageShow("error", "获取DDNS配置出错")
})
}
const RequestAlterDDNSConfigure = () => {
apiAlterDDNSConfigure(form.value).then((res) => {
if (res.ret == 0) {
MessageShow("success", "配置修改成功")
//syncToPreFormData(form)
return
}
resetFormData()
MessageShow("error", res.msg)
}).catch((error) => {
console.log("配置修改失败,网络请求出错:" + error)
MessageShow("error", "配置修改失败,网络请求出错")
resetFormData()
})
}
onMounted(() => {
queryDDConfigure()
})
</script>
<style scoped>
.SetForm {
margin-top: 15px;
margin-left: 20px;
}
.formradius {
border: 0px solid var(--el-border-color);
border-radius: 0;
margin: 0 auto;
width: fit-content;
padding: 15px;
}
#adminListen {
width: 360px;
}
#adminAccount {
width: 30vw;
max-width: 360px;
min-width: 300px;
}
#adminPassword {
width: 30vw;
max-width: 360px;
min-width: 300px;
}
#proxyCountLimit {
width: 360px;
}
#globalMaxConnections {
width: 360px;
}
</style>

View File

@ -110,9 +110,11 @@ const Login = () => {
const keydown = (e) => {
if (e.keyCode == 13) {
if (e.keyCode == 13 && global.currentPage.value=="#login") {
Login()
return
}
return
}
onMounted(() => {

View File

@ -115,7 +115,7 @@ const rebootProgram = () => {
disableRebootButton.value = true;
ElMessageBox.confirm(
'确定要重启goports?',
'确定要重启lucky?',
'Warning',
{
confirmButtonText: '确认',

View File

@ -65,6 +65,35 @@
<el-sub-menu index="#ddns">
<template #title>
<el-icon>
<Promotion />
</el-icon>
<span>动态域名</span>
</template>
<el-menu-item index="#ddnstasklist">
<el-icon>
<List />
</el-icon>
<template #title>动态域名任务列表</template>
</el-menu-item>
<el-menu-item index="#ddnsset">
<el-icon>
<Setting />
</el-icon>
<template #title>动态域名设置</template>
</el-menu-item>
</el-sub-menu>
@ -76,6 +105,16 @@
</el-menu-item>
<el-divider style="margin-top: 0px;margin-bottom: 0px;" />
<el-menu-item index="#about">
<el-icon>
<Pointer />
</el-icon>
<template #title>关于</template>
</el-menu-item>
<el-divider style="margin-top: 0px;margin-bottom: 0px;" />
<el-menu-item index="#logout">
<el-icon>
@ -90,7 +129,7 @@
<div class="flex-grow" />
<el-menu-item index="#logo">goports {{ version }}</el-menu-item>
<el-menu-item index="#logo">Lucky {{ version }}</el-menu-item>
</el-menu>
</template>
@ -101,7 +140,7 @@
<script setup lang="ts">
import { inject, ref, onMounted } from 'vue';
import { SetHash, apiGetVersion } from '../apis/utils.js'
import { SetHash, apiGetVersion } from '../apis/utils.js'
import { ElMessageBox } from 'element-plus'
const global: any = inject("global")
@ -154,7 +193,8 @@ function handleSelect(key, keyPath, item, routeResult) {
})
break;
case "#logo":
window.open("https://github.com/ljymc/goports", "_blank");
//window.open("https://github.com/gdy666/lucky", "_blank");
location.hash ="#about"
break;
default:
SetHash(key)

View File

@ -190,7 +190,7 @@
<!--添加/修改规则对话框-->
<el-dialog v-model="dialogFormVisible" :title="dialogTitle" draggable :before-close="handleDialogClose"
:show-close="false" width="650px">
:show-close="false" :close-on-click-modal="false" width="650px">
<el-form :model="form">
<el-form-item label="名称" :label-width="formLabelWidth">
<el-input v-model="form.Name" placeholder="转发规则名称,可留空" autocomplete="off" />
@ -287,7 +287,7 @@ import { ElMessageBox } from 'element-plus'
import {MessageShow} from '../utils/ui'
import {isIP} from '../utils/utils'
import {isIP,StringToArrayList} from '../utils/utils'
//var timerID:any
@ -401,7 +401,7 @@ const ruleEnableClick = (enable, rule) => {
return
}
resolve(false)
MessageShow("success", "规则 " + ruleName + " " + configure + enableText + "失败")
MessageShow("error", "规则 " + ruleName + " " + configure + enableText + "失败")
if (res.syncres != undefined && res.syncres != "") {
Notification("warn", res.syncres, 0)
@ -409,7 +409,7 @@ const ruleEnableClick = (enable, rule) => {
}).catch((error) => {
resolve(false)
console.log("规则 " + ruleName + " " + configure + enableText + "失败" + ":请求出错" + error)
MessageShow("success", "规则 " + ruleName + " " + configure + enableText + "失败" + ":请求出错")
MessageShow("error", "规则 " + ruleName + " " + configure + enableText + "失败" + ":请求出错")
})
})
@ -455,21 +455,14 @@ const addRule = () => {
confirmText.value = "添加"
}
const converAddressListTextToList = (listStr: string) => {
let rawlist = listStr.split("\n")
let resList = new Array()
for (let i in rawlist) {
resList.push(rawlist[i].replace(/^\s+|\s+$/g, '').replace(/<\/?.+?>/g, "").replace(/[\r\n]/g, ""))
}
return resList
}
const addOrAlterRuleConfirm = () => {
if (!form.value.IsBalanceRelayType) {
form.value.BalanceTargetAddressList = []
} else {
form.value.BalanceTargetAddressList = converAddressListTextToList(formBalanceTargetAddressList.value)//formBalanceTargetAddressList.value.split(",")
form.value.BalanceTargetAddressList = StringToArrayList(formBalanceTargetAddressList.value)//formBalanceTargetAddressList.value.split(",")
}
if (!checkFormData()) {
@ -961,9 +954,7 @@ var timerID: any
onMounted(() => {
queryRuleList();
console.log("relaySet onmounted")
queryRuleList();
timerID = setInterval(() => {
queryRuleList();
}, 1000);
@ -1013,7 +1004,7 @@ var flushBalanceOptionsView = () => {
margin-top: 3px;
margin-right: 3px;
margin-bottom: 25px;
width:1200px;
min-width: 1200px;
}

View File

@ -4,8 +4,8 @@
<p class="status">CPU全局使用率:{{ status.usedCPU }}</p>
<p class="status">当前进程CPU使用率:{{ status.currentProcessUsedCPU }}</p>
<p class="status">进程协程数:{{ status.goroutine }} 占用内存:{{ status.processUsedMem }}</p>
<p class="status">goports全局连接数:{{ status.currentConnections }} </p>
<p class="status">goports全局限制连接数:{{ status.maxConnections }}</p>
<p class="status">Lucky 全局连接数:{{ status.currentConnections }} </p>
<p class="status">Lucky 全局限制连接数:{{ status.maxConnections }}</p>

View File

@ -47,7 +47,7 @@
<el-dialog v-model="addWhiteListDialogVisible" title="添加白名单IP" draggable :show-close="false" width="400px">
<el-dialog v-model="addWhiteListDialogVisible" title="添加白名单IP" draggable :show-close="false" :close-on-click-modal="false" width="400px">
<el-form :model="addWhiteListForm">
<el-form-item label="IP" label-width="auto">

View File

@ -1,4 +1,4 @@
import { ElMessage, ElMessageBox } from 'element-plus'
import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
// ElMessageBox.alert(message, {
@ -24,3 +24,11 @@ export function MessageShow(type:any,message: string) {
}
export function Notification (type, message, duration) {
ElNotification({
title: type.substring(0, 1).toUpperCase() + type.substring(1),
message: message,
type: type,
duration: duration,
})
}

View File

@ -11,7 +11,10 @@ export function isIP(ip :string){
return ipReg.test(ip)
}
const MenuIndexList = ["#status","#log","#relayset","#whitelistset","#whitelists","#blacklists","#set","#login"]
const MenuIndexList = ["#status",
"#log","#relayset","#whitelistset",
"#whitelists","#blacklists","#set",
"#login","#ddns","#ddnstasklist","#ddnsset","#about"]
export function PageExist(page:string) {
for(let i in MenuIndexList){
@ -24,3 +27,16 @@ export function PageExist(page:string) {
export const CurrentPage = ref("")
export function StringToArrayList(str : string){
let rawlist = str.split("\n")
let resList = new Array()
for (let i in rawlist) {
let item = rawlist[i].replace(/^\s+|\s+$/g, '').replace(/<\/?.+?>/g, "").replace(/[\r\n]/g, "")
if (item==""){
continue
}
resList.push(item)
}
return resList
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -21,18 +21,19 @@ import (
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"github.com/gdy666/lucky/base"
"github.com/gdy666/lucky/config"
"github.com/gdy666/lucky/rule"
"github.com/gdy666/lucky/thirdlib/gdylib/fileutils"
"github.com/gdy666/lucky/thirdlib/gdylib/ginutils"
"github.com/gdy666/lucky/thirdlib/gdylib/service"
"github.com/golang-jwt/jwt"
"github.com/ljymc/goports/base"
"github.com/ljymc/goports/config"
"github.com/ljymc/goports/rule"
"github.com/ljymc/goports/thirdlib/gdylib/fileutils"
"github.com/ljymc/goports/thirdlib/gdylib/ginutils"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/process"
)
//go:embed goports-adminviews/dist
//go:embed adminviews/dist
var staticFs embed.FS
var stafs fs.FS
var loginErrorCount = int32(0)
@ -43,8 +44,8 @@ var rebootOnce sync.Once
//var cookieStore cookie.Store
func init() {
stafs, _ = fs.Sub(staticFs, "goports-adminviews/dist")
//cookieStore = cookie.NewStore([]byte("goports2022"))
stafs, _ = fs.Sub(staticFs, "adminviews/dist")
//cookieStore = cookie.NewStore([]byte("lucky2022"))
}
func RunAdminWeb(listen string) {
@ -62,7 +63,7 @@ func RunAdminWeb(listen string) {
r.Use(checkLocalIP)
//r.Use(sessions.Sessions("goportssession", cookieStore))
//r.Use(sessions.Sessions("luckysession", cookieStore))
r.Use(gzip.Gzip(gzip.DefaultCompression))
@ -98,9 +99,21 @@ func RunAdminWeb(listen string) {
authorized.GET("/api/blacklist", queryblacklist)
authorized.PUT("/api/blacklist/flush", flushblacklist)
authorized.DELETE("/api/blacklist", deleteblacklist)
authorized.POST("/api/ddns", addDDNS)
authorized.PUT("/api/ddns", alterDDNSTask)
authorized.GET("/api/ddnstasklist", ddnsTaskList)
authorized.DELETE("/api/ddns", deleteDDNSTask)
authorized.GET("/api/ddns/enable", enableddns)
authorized.GET("/api/ddns/configure", ddnsconfigure)
authorized.PUT("/api/ddns/configure", alterDDNSConfigure)
authorized.GET("/api/netinterfaces", netinterfaces)
authorized.GET("/api/ipregtest", IPRegTest)
authorized.POST("/api/webhooktest", webhookTest)
authorized.GET("/api/info", info)
r.PUT("/api/logout", logout)
}
r.POST("/api/login", login)
//r.GET("/FreeOSMemory", FreeOSMemory)
r.GET("/wl", whitelistBasicAuth, whilelistAdd)
r.GET("/wl/:url", whitelistBasicAuth, whilelistAdd)
@ -117,6 +130,213 @@ func RunAdminWeb(listen string) {
}
}
// func FreeOSMemory(c *gin.Context) {
// debug.FreeOSMemory()
// c.JSON(http.StatusOK, gin.H{"ret": 0})
// }
func info(c *gin.Context) {
info := config.GetAppInfo()
// var info struct {
// Version string
// OS string
// ARCH string
// Date string
// }
// info.Version =
c.JSON(http.StatusOK, gin.H{"ret": 0, "info": *info})
}
func enableddns(c *gin.Context) {
enable := c.Query("enable")
key := c.Query("key")
var err error
if enable == "true" {
err = config.EnableDDNSTaskByKey(key, true)
if err == nil {
service.Message("ddns", "syncDDNSTask", key)
}
} else {
err = config.EnableDDNSTaskByKey(key, false)
}
if err != nil {
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": fmt.Sprintf("开关DDNS任务出错:%s", err.Error())})
return
}
c.JSON(http.StatusOK, gin.H{"ret": 0, "msg": ""})
}
func deleteDDNSTask(c *gin.Context) {
taskKey := c.Query("key")
err := config.DDNSTaskListDelete(taskKey)
if err != nil {
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": fmt.Errorf("删除DDNS任务出错:%s", err.Error())})
return
}
c.JSON(http.StatusOK, gin.H{"ret": 0, "msg": ""})
}
func ddnsTaskList(c *gin.Context) {
conf := config.GetDDNSConfigure()
if !conf.Enable {
c.JSON(http.StatusOK, gin.H{"ret": 6, "msg": "请先在设置页面启用DDNS动态域名服务"})
return
}
taskList := config.GetDDNSTaskList()
c.JSON(http.StatusOK, gin.H{"ret": 0, "data": taskList})
}
func addDDNS(c *gin.Context) {
var requestObj config.DDNSTask
err := c.BindJSON(&requestObj)
if err != nil {
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": "请求解析出错"})
return
}
//fmt.Printf("addDDNS requestObj:%v\n", requestObj)
err = config.CheckDDNSTaskAvalid(&requestObj)
if err != nil {
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": err.Error()})
return
}
dealRequestDDNSTask(&requestObj)
err = config.DDNSTaskListAdd(&requestObj)
if err != nil {
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": "DDNS任务添加出错"})
return
}
if requestObj.Enable {
service.Message("ddns", "syncDDNSTask", requestObj.TaskKey)
}
c.JSON(http.StatusOK, gin.H{"ret": 0, "msg": ""})
}
func alterDDNSTask(c *gin.Context) {
taskKey := c.Query("key")
var requestObj config.DDNSTask
err := c.BindJSON(&requestObj)
if err != nil {
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": "请求解析出错"})
return
}
err = config.CheckDDNSTaskAvalid(&requestObj)
if err != nil {
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": err.Error()})
return
}
dealRequestDDNSTask(&requestObj)
err = config.UpdateTaskToDDNSTaskList(taskKey, requestObj)
if err != nil {
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": fmt.Sprintf("更新DDNS任务出错:%s", err.Error())})
return
}
if requestObj.Enable {
service.Message("ddns", "syncDDNSTask", taskKey)
}
c.JSON(http.StatusOK, gin.H{"ret": 0, "msg": ""})
}
func dealRequestDDNSTask(t *config.DDNSTask) {
if t.DNS.Name == "callback" {
t.DNS.ID = ""
t.DNS.Secret = ""
t.DNS.Callback.URL = strings.TrimSpace(t.DNS.Callback.URL)
//requestObj.DNS.Callback.CallbackSuccessContent = strings.TrimSpace(requestObj.DNS.Callback.CallbackSuccessContent)
t.DNS.Callback.RequestBody = strings.TrimSpace(t.DNS.Callback.RequestBody)
} else {
t.DNS.Callback = config.DNSCallback{}
}
if !t.DNS.ResolverDoaminCheck && len(t.DNS.DNSServerList) > 0 {
t.DNS.DNSServerList = []string{}
}
if t.DNS.ResolverDoaminCheck && (len(t.DNS.DNSServerList) == 0 || (len(t.DNS.DNSServerList) == 1 && t.DNS.DNSServerList[0] == "")) {
if t.TaskType == "IPv6" {
t.DNS.DNSServerList = config.DefaultIPv6DNSServerList
} else {
t.DNS.DNSServerList = config.DefaultIPv4DNSServerList
}
}
if t.DNS.HttpClientProxyType != "" && t.DNS.HttpClientProxyAddr == "" {
t.DNS.HttpClientProxyType = ""
}
if t.DNS.HttpClientProxyType == "" {
t.DNS.HttpClientProxyAddr = ""
t.DNS.HttpClientProxyUser = ""
t.DNS.HttpClientProxyPassword = ""
}
if t.GetType == "url" {
t.NetInterface = ""
t.IPReg = ""
}
if t.GetType == "netInterface" {
t.URL = []string{}
}
if !t.WebhookEnable {
t.WebhookHeaders = []string{}
t.WebhookMethod = ""
t.WebhookRequestBody = ""
t.WebhookURL = ""
t.WebhookSuccessContent = []string{}
t.WebhookProxy = ""
t.WebhookProxyAddr = ""
t.WebhookProxyUser = ""
t.WebhookProxyPassword = ""
}
if t.WebhookEnable {
if t.WebhookMethod == "get" {
t.WebhookRequestBody = ""
}
if t.WebhookProxy == "" {
t.WebhookProxyAddr = ""
t.WebhookProxyUser = ""
t.WebhookProxyPassword = ""
}
}
if t.DNS.ForceInterval < 60 {
t.DNS.ForceInterval = 60
} else if t.DNS.ForceInterval > 360000 {
t.DNS.ForceInterval = 360000
}
if t.HttpClientTimeout < 3 {
t.HttpClientTimeout = 3
} else if t.HttpClientTimeout > 60 {
t.HttpClientTimeout = 60
}
}
func logout(c *gin.Context) {
config.FlushLoginRandomKey()
c.JSON(http.StatusOK, gin.H{"ret": 0, "msg": "已注销登录"})
@ -376,11 +596,113 @@ func alterBaseConfigure(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ret": 0})
}
func alterDDNSConfigure(c *gin.Context) {
var requestObj config.DDNSConfigure
err := c.BindJSON(&requestObj)
if err != nil {
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": "请求解析出错"})
return
}
preConfigure := config.GetDDNSConfigure()
if preConfigure.Enable != requestObj.Enable {
//log.Printf("动态服务服务状态改变:%v", requestObj.Enable)
if requestObj.Enable {
service.Start("ddns")
} else {
service.Stop("ddns")
}
}
err = config.SetDDNSConfigure(&requestObj)
if err != nil {
c.JSON(http.StatusOK, gin.H{"ret": 2, "msg": "保存配置过程发生错误,请检测相关启动配置"})
return
}
c.JSON(http.StatusOK, gin.H{"ret": 0})
}
func baseconfigure(c *gin.Context) {
conf := config.GetBaseConfigure()
c.JSON(http.StatusOK, gin.H{"ret": 0, "baseconfigure": conf})
}
func netinterfaces(c *gin.Context) {
ipv4NetInterfaces, ipv6Netinterfaces, err := config.GetNetInterface()
if err != nil {
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": fmt.Sprintf("获取网卡列表出错:%s", err.Error())})
return
}
c.JSON(http.StatusOK, gin.H{"ret": 0, "data": gin.H{"IPv6NewInterfaces": ipv6Netinterfaces, "IPv4NewInterfaces": ipv4NetInterfaces}})
}
func webhookTest(c *gin.Context) {
key := c.Query("key")
ddnsTask := config.GetDDNSTaskByKey(key)
if ddnsTask == nil {
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": fmt.Sprintf("找不到key对应的DDNS任务:%s", key)})
return
}
var request struct {
WebhookURL string `json:"WebhookURL"`
WebhookMethod string `json:"WebhookMethod"`
WebhookHeaders []string `json:"WebhookHeaders"`
WebhookRequestBody string `json:"WebhookRequestBody"`
WebhookSuccessContent []string `json:"WebhookSuccessContent"` //接口调用成功包含的内容
WebhookProxy string `json:"WebhookProxy"` //使用DNS代理设置 ""表示禁用,"dns"表示使用dns的代理设置
WebhookProxyAddr string `json:"WebhookProxyAddr"` //代理服务器IP
WebhookProxyUser string `json:"WebhookProxyUser"` //代理用户
WebhookProxyPassword string `json:"WebhookProxyPassword"` //代理密码
}
err := c.Bind(&request)
if err != nil {
c.JSON(http.StatusOK, gin.H{"ret": 1, "msg": fmt.Sprintf("请求解析出错:%s", err.Error())})
return
}
responseStr, err := config.WebhookTest(&ddnsTask.DDNSTask,
request.WebhookURL,
request.WebhookMethod,
request.WebhookRequestBody,
request.WebhookProxy,
request.WebhookProxyAddr,
request.WebhookProxyUser,
request.WebhookProxyPassword,
request.WebhookHeaders,
request.WebhookSuccessContent)
//fmt.Printf("request:%s\n", request)
msg := "Webhook接口调用成功"
if err != nil {
msg = err.Error()
}
c.JSON(http.StatusOK, gin.H{"ret": 0, "msg": msg, "Response": responseStr})
}
func IPRegTest(c *gin.Context) {
iptype := c.Query("iptype")
netinterface := c.Query("netinterface")
ipreg := c.Query("ipreg")
ip := config.GetIPFromNetInterface(iptype, netinterface, ipreg)
c.JSON(http.StatusOK, gin.H{"ret": 0, "ip": ip})
}
func ddnsconfigure(c *gin.Context) {
conf := config.GetDDNSConfigure()
c.JSON(http.StatusOK, gin.H{"ret": 0, "ddnsconfigure": conf})
}
func enablerule(c *gin.Context) {
enable := c.Query("enable")
@ -483,10 +805,6 @@ func addrule(c *gin.Context) {
return
}
// configureStr := fmt.Sprintf("%s@%s:%sto%s:%s",
// requestRule.RelayType,
// requestRule.ListenIP, requestRule.ListenPorts,
// requestRule.TargetIP, requestRule.TargetPorts)
configureStr := requestRule.CreateMainConfigure()
r, err := rule.CreateRuleByConfigureAndOptions(requestRule.Name, configureStr, requestRule.Options)