improve: 使用标准项目结构

This commit is contained in:
Mmx233
2023-03-01 18:58:11 +08:00
parent cb11426bd6
commit 3c63e9ddc3
29 changed files with 178 additions and 183 deletions

View File

@@ -0,0 +1,60 @@
package controllers
import (
"fmt"
"github.com/Mmx233/BitSrunLoginGo/internal/global"
"github.com/Mmx233/tool"
"github.com/howeyc/fsnotify"
"os"
"time"
)
type daemon struct {
Mark string
Path string
}
// Daemon 后台模式控制包
var Daemon = daemon{
Mark: fmt.Sprint(time.Now().UnixNano()),
Path: global.Config.Settings.Daemon.Path,
}
// MarkDaemon 写入后台标记文件
func (a *daemon) MarkDaemon() error {
return tool.File.WriteAll(a.Path, []byte(a.Mark))
}
// CheckDaemon 检查后台标记文件
func (a *daemon) CheckDaemon() bool {
if data, err := tool.File.ReadAll(a.Path); err != nil {
return false
} else {
return string(data) == a.Mark
}
}
// DaemonChan 后台标记文件监听
func (a *daemon) DaemonChan() bool {
f, err := fsnotify.NewWatcher()
if err != nil {
panic(err)
}
err = f.Watch(Daemon.Path)
if err != nil {
panic(err)
}
for {
select {
case event := <-f.Event:
if event.IsModify() && a.CheckDaemon() {
continue
}
os.Exit(0)
case e := <-f.Error:
panic(e)
}
}
}

View File

@@ -0,0 +1,67 @@
package controllers
import (
"github.com/Mmx233/BitSrunLoginGo/internal/global"
"os"
"os/exec"
"time"
"github.com/Mmx233/BitSrunLoginGo/tools"
log "github.com/sirupsen/logrus"
)
// Guardian 守护模式逻辑
func Guardian() {
GuardianDuration := time.Duration(global.Config.Settings.Guardian.Duration) * time.Second
if global.Config.Settings.Daemon.Enable {
go Daemon.DaemonChan()
if e := Daemon.MarkDaemon(); e != nil {
log.Warnln("写入daemon标记文件失败: ", e)
}
}
var c = make(chan bool)
for {
go func() {
defer func() {
_ = recover()
}()
if global.Config.Settings.Basic.Interfaces == "" { //单网卡
e := Login(nil, true)
if e != nil {
log.Errorln("登录出错: ", e)
}
} else { //多网卡
interfaces, e := tools.GetInterfaceAddr()
if e == nil {
for _, eth := range interfaces {
log.Debugf("使用 %s 网口登录 ", eth.Name)
e = Login(eth.Addr, true)
if e != nil {
log.Errorln("网口 ", eth.Name+" 登录出错: ", e)
}
}
}
}
c <- false
}()
<-c
time.Sleep(GuardianDuration)
}
}
// EnterGuardian 守护模式入口控制是否进入daemon
func EnterGuardian() {
log.Infoln("[以守护模式启动]")
if global.Config.Settings.Daemon.Enable || global.Flags.Daemon {
if err := exec.Command(os.Args[0], append(os.Args[1:], "--running-daemon")...).Start(); err != nil {
log.Fatalln("启动守护失败: ", err)
}
log.Infoln("[进入后台进程模式]")
return
}
Guardian()
}

View File

@@ -0,0 +1,89 @@
package controllers
import (
global2 "github.com/Mmx233/BitSrunLoginGo/internal/global"
dns2 "github.com/Mmx233/BitSrunLoginGo/internal/pkg/dns"
"github.com/Mmx233/BitSrunLoginGo/pkg/srun"
"github.com/Mmx233/BitSrunLoginGo/tools"
log "github.com/sirupsen/logrus"
"net"
"net/http"
)
// Login 登录逻辑
func Login(localAddr net.Addr, debugOutput bool) error {
// 登录状态检查
httpClient := tools.HttpPackSelect(localAddr).Client
conf := &srun.Conf{
Https: global2.Config.Settings.Basic.Https,
LoginInfo: srun.LoginInfo{
Form: &global2.Config.Form,
Meta: &global2.Config.Meta,
},
Client: httpClient,
}
var output func(args ...interface{})
if debugOutput {
output = log.Debugln
} else {
output = log.Infoln
}
output("正在获取登录状态")
online, ip, e := srun.LoginStatus(conf)
if e != nil {
return e
}
if localAddr != nil && global2.Config.Settings.Basic.UseDhcpIP {
ip = localAddr.(*net.TCPAddr).IP.String()
} else if global2.Flags.ClientIP != "" {
ip = global2.Flags.ClientIP
}
log.Debugln("认证客户端 ip: ", ip)
// 登录执行
if online {
output("已登录~")
if global2.Config.Settings.DDNS.Enable && global2.Config.Settings.Guardian.Enable && ipLast != ip {
if ddns(ip, httpClient) == nil {
ipLast = ip
}
}
return nil
} else {
log.Infoln("检测到用户未登录,开始尝试登录...")
if e = srun.DoLogin(ip, conf); e != nil {
return e
}
log.Infoln("登录成功~")
if global2.Config.Settings.DDNS.Enable {
_ = ddns(ip, httpClient)
}
}
return nil
}
var ipLast string
func ddns(ip string, httpClient *http.Client) error {
return dns2.Run(&dns2.Config{
Provider: global2.Config.Settings.DDNS.Provider,
IP: ip,
Domain: global2.Config.Settings.DDNS.Domain,
TTL: global2.Config.Settings.DDNS.TTL,
Conf: global2.Config.Settings.DDNS.Config,
Http: httpClient,
})
}

67
internal/global/config.go Normal file
View File

@@ -0,0 +1,67 @@
package global
import (
"github.com/Mmx233/BitSrunLoginGo/internal/global/models"
"github.com/Mmx233/BitSrunLoginGo/pkg/srun"
"github.com/Mmx233/tool"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"os"
"time"
)
var Config models.Config
var Timeout time.Duration
func readConfig() {
//配置文件默认值
viper.SetDefault("form", srun.LoginForm{
Domain: "www.msftconnecttest.com",
UserType: "cmcc",
})
viper.SetDefault("meta", srun.LoginMeta{
N: "200",
Type: "1",
Acid: "5",
Enc: "srun_bx1",
})
viper.SetDefault("settings", models.Settings{
Basic: models.Basic{
Timeout: 5,
},
Daemon: models.Daemon{
Path: ".BitSrun",
},
Guardian: models.Guardian{
Duration: 300,
},
Log: models.Log{
FilePath: "./",
},
DDNS: models.DDNS{
Enable: false,
TTL: 600,
Domain: "www.example.com",
},
})
//生成配置文件
if !tool.File.Exists(Flags.Path) {
e := viper.WriteConfigAs(Flags.Path)
if e != nil {
log.Fatalln("[init] 生成配置文件失败:", e)
}
log.Infoln("[init] 已生成配置文件,请编辑 '" + Flags.Path + "' 然后重试")
os.Exit(0)
}
//读取配置文件
viper.SetConfigFile(Flags.Path)
if e := viper.ReadInConfig(); e != nil {
log.Fatalln("[init] 读取配置文件失败:", e)
}
if e := viper.Unmarshal(&Config); e != nil {
log.Fatalln("[init] 解析配置文件失败:", e)
}
}

24
internal/global/flags.go Normal file
View File

@@ -0,0 +1,24 @@
package global
import (
"flag"
)
var Flags struct {
//配置文件路径
Path string
//daemon模式内置标记
RunningDaemon bool
//强制daemon
Daemon bool
//指定 client ip
ClientIP string
}
func initFlags() {
flag.StringVar(&Flags.Path, "config", "Config.yaml", "config path")
flag.StringVar(&Flags.ClientIP, "ip", "", "client ip for login")
flag.BoolVar(&Flags.RunningDaemon, "running-daemon", false, "")
flag.BoolVar(&Flags.Daemon, "daemon", false, "")
flag.Parse()
}

18
internal/global/init.go Normal file
View File

@@ -0,0 +1,18 @@
package global
import (
"time"
)
func init() {
initFlags()
//配置文件初始化
readConfig()
//初始化常变量
Timeout = time.Duration(Config.Settings.Basic.Timeout) * time.Second
//初始化日志配置
initLog()
}

48
internal/global/log.go Normal file
View File

@@ -0,0 +1,48 @@
package global
import (
"io"
"os"
"strings"
"time"
nested "github.com/antonfisher/nested-logrus-formatter"
log "github.com/sirupsen/logrus"
)
func initLog() {
if Config.Settings.Log.DebugLevel {
log.SetLevel(log.DebugLevel)
}
if Config.Settings.Log.WriteFile {
//日志路径初始化与处理
if !strings.HasSuffix(Config.Settings.Log.FilePath, "/") {
Config.Settings.Log.FilePath += "/"
}
e := os.MkdirAll(Config.Settings.Log.FilePath, os.ModePerm)
if e != nil {
log.Fatalln(e)
}
if Config.Settings.Log.FileName == "" {
Config.Settings.Log.FileName = time.Now().Format("2006.01.02-15.04.05") + ".log"
}
f, e := os.OpenFile(Config.Settings.Log.FilePath+Config.Settings.Log.FileName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if e != nil {
log.Fatalln(e)
}
//设置双重输出
mw := io.MultiWriter(os.Stdout, f)
log.SetOutput(mw)
//设置输出格式
log.SetFormatter(&nested.Formatter{
HideKeys: true,
NoColors: Config.Settings.Log.WriteFile,
TimestampFormat: "2006-01-02 15:04:05",
})
}
}

View File

@@ -0,0 +1,52 @@
package models
import (
"github.com/Mmx233/BitSrunLoginGo/pkg/srun"
)
type Daemon struct {
Enable bool `json:"enable" yaml:"enable" mapstructure:"enable"`
Path string `json:"path" yaml:"path" mapstructure:"path"`
}
type Guardian struct {
Enable bool `json:"enable" yaml:"enable" mapstructure:"enable"`
Duration uint `json:"duration" yaml:"duration" mapstructure:"duration"`
}
type Basic struct {
Https bool `json:"https" yaml:"https" mapstructure:"https"`
SkipCertVerify bool `json:"skip_cert_verify" yaml:"skip_cert_verify" mapstructure:"skip_cert_verify"`
Timeout uint `json:"timeout" yaml:"timeout" mapstructure:"timeout"`
Interfaces string `json:"interfaces" yaml:"interfaces" mapstructure:"interfaces"`
UseDhcpIP bool `json:"use_dhcp_ip" yaml:"use_dhcp_ip" mapstructure:"use_dhcp_ip"`
}
type Log struct {
DebugLevel bool `json:"debug_level" yaml:"debug_level" mapstructure:"debug_level"`
WriteFile bool `json:"write_file" yaml:"write_file" mapstructure:"write_file"`
FilePath string `json:"log_path" yaml:"log_path" mapstructure:"log_path"`
FileName string `json:"log_name" yaml:"log_name" mapstructure:"log_name"`
}
type DDNS struct {
Enable bool `json:"enable" yaml:"enable" mapstructure:"enable"`
TTL uint `json:"ttl" yaml:"ttl" mapstructure:"ttl"`
Domain string `json:"domain" yaml:"domain" mapstructure:"domain"`
Provider string `json:"provider" yaml:"provider" mapstructure:"provider"`
Config map[string]interface{} `mapstructure:",remain"`
}
type Settings struct {
Basic Basic `json:"basic" yaml:"basic" mapstructure:"basic"`
Guardian Guardian `json:"guardian" yaml:"guardian" mapstructure:"guardian"`
Daemon Daemon `json:"daemon" yaml:"daemon" mapstructure:"daemon"`
Log Log `json:"log" yaml:"log" mapstructure:"log"`
DDNS DDNS `json:"ddns" yaml:"ddns" mapstructure:"ddns"`
}
type Config struct {
Form srun.LoginForm `json:"form" yaml:"form" mapstructure:"form"`
Meta srun.LoginMeta `json:"meta" yaml:"meta" mapstructure:"meta"`
Settings Settings `json:"settings" yaml:"settings" mapstructure:"settings"`
}

View File

@@ -0,0 +1,182 @@
package aliyun
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
dnsUtil2 "github.com/Mmx233/BitSrunLoginGo/internal/pkg/dns/util"
"github.com/Mmx233/tool"
"math/rand"
"net/http"
"net/url"
"sort"
"time"
)
type DnsProvider struct {
TTL uint `mapstructure:"-"`
Http *tool.Http `mapstructure:"-"`
AccessKeyId string `mapstructure:"access_key_id"`
AccessKeySecret string `mapstructure:"access_key_secret"`
}
func New(ttl uint, conf map[string]interface{}, Http *http.Client) (*DnsProvider, error) {
var p = DnsProvider{
TTL: ttl,
Http: tool.NewHttpTool(Http),
}
e := dnsUtil2.DecodeConfig(conf, &p)
if e != nil {
return nil, e
}
if p.AccessKeyId == "" || p.AccessKeySecret == "" {
return nil, errors.New("aliyun AccessKey 不能为空")
}
return &p, nil
}
func (a DnsProvider) SendRequest(Type, Action string, data map[string]interface{}) (*http.Response, error) {
var reqOpt = tool.DoHttpReq{
Url: "https://alidns.aliyuncs.com",
}
rand.Seed(time.Now().UnixNano())
data["Format"] = "json"
data["Version"] = "2015-01-09"
data["SignatureMethod"] = "HMAC-SHA1"
data["SignatureVersion"] = "1.0"
data["SignatureNonce"] = fmt.Sprint(tool.Rand.Num(10000000, 90000000))
data["Timestamp"] = time.Now().UTC().Format("2006-01-02T15:04:05Z")
data["Action"] = Action
data["AccessKeyId"] = a.AccessKeyId
signStr := Type + "&" + url.QueryEscape("/") + "&"
var keys = make([]string, len(data))
var i int
for k := range data {
keys[i] = k
i++
}
sort.Strings(keys)
for i, k := range keys {
str := k + "=" + url.QueryEscape(fmt.Sprint(data[k]))
if i == 0 {
str = url.QueryEscape(str)
} else {
str = url.QueryEscape("&" + str)
}
signStr += str
}
mac := hmac.New(sha1.New, []byte(a.AccessKeySecret+"&"))
_, e := mac.Write([]byte(signStr))
if e != nil {
return nil, e
}
data["Signature"] = base64.StdEncoding.EncodeToString(mac.Sum(nil))
if Type == "GET" || Type == "DELETE" {
reqOpt.Query = data
} else {
reqOpt.Body = data
}
resp, e := a.Http.Request(Type, &reqOpt)
if e != nil {
return nil, e
}
if resp.StatusCode > 299 {
defer resp.Body.Close()
var res Response
if e = json.NewDecoder(resp.Body).Decode(&res); e != nil {
return nil, e
}
return nil, errors.New(res.Message)
}
return resp, nil
}
func (a DnsProvider) DomainRecordStatus(subDomain, rootDomain string) (*DomainStatus, bool, error) {
resp, e := a.SendRequest("GET", "DescribeDomainRecords", map[string]interface{}{
"DomainName": rootDomain,
"SearchMode": "EXACT",
"KeyWord": subDomain,
"PageSize": 1,
"Type": "A",
})
if e != nil {
return nil, false, e
}
defer resp.Body.Close()
var res DomainStatusRes
if e = json.NewDecoder(resp.Body).Decode(&res); e != nil {
return nil, false, e
}
if res.TotalCount == 0 || len(res.DomainRecords.Record) == 0 {
return nil, false, nil
}
return &res.DomainRecords.Record[0], true, nil
}
func (a DnsProvider) UpdateRecord(RecordId, subDomain, ip string) error {
resp, e := a.SendRequest("POST", "UpdateDomainRecord", map[string]interface{}{
"RecordId": RecordId,
"RR": subDomain,
"Type": "A",
"Value": ip,
"TTL": a.TTL,
})
if e != nil {
return e
}
defer resp.Body.Close()
return nil
}
func (a DnsProvider) NewRecord(subDomain, rootDomain, ip string) error {
resp, e := a.SendRequest("POST", "AddDomainRecord", map[string]interface{}{
"DomainName": rootDomain,
"RR": subDomain,
"Type": "A",
"Value": ip,
"TTL": a.TTL,
})
if e != nil {
return e
}
defer resp.Body.Close()
return nil
}
func (a DnsProvider) SetDomainRecord(domain, ip string) error {
subDomain, rootDomain, e := dnsUtil2.DecodeDomain(domain)
if e != nil {
return e
}
record, exist, e := a.DomainRecordStatus(subDomain, rootDomain)
if e != nil {
return e
}
if exist {
if record.Value == ip {
return nil
}
return a.UpdateRecord(record.RecordId, subDomain, ip)
} else {
return a.NewRecord(subDomain, rootDomain, ip)
}
}

View File

@@ -0,0 +1,19 @@
package aliyun
type Response struct {
Code string `json:"code"`
Message string `json:"message"`
}
type DomainStatus struct {
DomainName string `json:"DomainName"`
RecordId string `json:"RecordId"`
Value string `json:"Value"`
}
type DomainStatusRes struct {
TotalCount uint `json:"TotalCount"`
DomainRecords struct {
Record []DomainStatus `json:"Record"`
} `json:"DomainRecords"`
}

View File

@@ -0,0 +1,68 @@
package cloudflare
import (
"context"
"errors"
"github.com/Mmx233/BitSrunLoginGo/internal/pkg/dns/util"
"github.com/cloudflare/cloudflare-go"
"net/http"
)
type DnsProvider struct {
Api *cloudflare.API `mapstructure:"-"`
TTL int `mapstructure:"-"`
Zone string `mapstructure:"zone"`
ZoneResource *cloudflare.ResourceContainer `mapstructure:"-"`
Token string `mapstructure:"token"`
}
func New(ttl int, conf map[string]interface{}, Http *http.Client) (*DnsProvider, error) {
var p = DnsProvider{
TTL: ttl,
}
e := dnsUtil.DecodeConfig(conf, &p)
if e != nil {
return nil, e
}
if p.Zone == "" {
return nil, errors.New("cloudflare zone 不能为空")
}
p.ZoneResource = cloudflare.ZoneIdentifier(p.Zone)
if p.Token == "" {
return nil, errors.New("cloudflare token 不能为空")
}
p.Api, e = cloudflare.NewWithAPIToken(p.Token, cloudflare.HTTPClient(Http))
return &p, e
}
func (a DnsProvider) SetDomainRecord(domain, ip string) error {
records, _, e := a.Api.ListDNSRecords(context.Background(), a.ZoneResource, cloudflare.ListDNSRecordsParams{
Type: "A",
Name: domain,
})
if e != nil {
return e
}
if len(records) == 0 {
_, e = a.Api.CreateDNSRecord(context.Background(), a.ZoneResource, cloudflare.CreateDNSRecordParams{
Type: "A",
Name: domain,
Content: ip,
TTL: a.TTL,
})
return e
} else {
record := records[0]
if record.Content == ip {
return nil
}
return a.Api.UpdateDNSRecord(context.Background(), a.ZoneResource, cloudflare.UpdateDNSRecordParams{
ID: record.ID,
Content: ip,
})
}
}

View File

@@ -0,0 +1,71 @@
package dnspod
import (
dnsUtil2 "github.com/Mmx233/BitSrunLoginGo/internal/pkg/dns/util"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/regions"
dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323"
"net/http"
"strings"
)
type DnsProvider struct {
Client *dnspod.Client `mapstructure:"-"`
TTL uint64 `mapstructure:"-"`
SecretId string `mapstructure:"secret_id"`
SecretKey string `mapstructure:"secret_key"`
}
func New(ttl uint64, conf map[string]interface{}, Http http.RoundTripper) (*DnsProvider, error) {
var p = DnsProvider{TTL: ttl}
e := dnsUtil2.DecodeConfig(conf, &p)
if e != nil {
return nil, e
}
p.Client, e = dnspod.NewClient(common.NewCredential(p.SecretId, p.SecretKey), regions.Guangzhou, profile.NewClientProfile())
p.Client.WithHttpTransport(Http)
return &p, e
}
func (a DnsProvider) SetDomainRecord(domain, ip string) error {
subDomain, rootDomain, e := dnsUtil2.DecodeDomain(domain)
if e != nil {
return e
}
var (
recordType = "A"
recordLine = "默认"
limit uint64 = 1
)
reqRecordList := dnspod.NewDescribeRecordListRequest()
reqRecordList.Domain = &rootDomain
reqRecordList.Subdomain = &subDomain
reqRecordList.Limit = &limit
res, e := a.Client.DescribeRecordList(reqRecordList)
if (e != nil && strings.Contains(e.Error(), dnspod.RESOURCENOTFOUND_NODATAOFRECORD)) || (e == nil && len(res.Response.RecordList) == 0) {
reqNewRecord := dnspod.NewCreateRecordRequest()
reqNewRecord.TTL = &a.TTL
reqNewRecord.Domain = &rootDomain
reqNewRecord.RecordType = &recordType
reqNewRecord.RecordLine = &recordLine
reqNewRecord.Value = &ip
reqNewRecord.SubDomain = &subDomain
_, e = a.Client.CreateRecord(reqNewRecord)
return e
} else if e != nil {
return e
}
reqModifyRecord := dnspod.NewModifyRecordRequest()
reqModifyRecord.Domain = &rootDomain
reqModifyRecord.SubDomain = &subDomain
reqModifyRecord.Value = &ip
reqModifyRecord.RecordId = res.Response.RecordList[0].RecordId
reqModifyRecord.RecordLine = &recordLine
reqModifyRecord.RecordType = &recordType
_, e = a.Client.ModifyRecord(reqModifyRecord)
return e
}

View File

@@ -0,0 +1,55 @@
package dns
import (
"errors"
"fmt"
"github.com/Mmx233/BitSrunLoginGo/internal/pkg/dns/aliyun"
"github.com/Mmx233/BitSrunLoginGo/internal/pkg/dns/cloudflare"
"github.com/Mmx233/BitSrunLoginGo/internal/pkg/dns/dnspod"
log "github.com/sirupsen/logrus"
)
func Run(c *Config) error {
log.Debugln("开始 DDNS 流程")
if c.TTL == 0 {
c.TTL = 600
}
// 配置解析
var dns Provider
var e error
switch c.Provider {
case "aliyun":
dns, e = aliyun.New(c.TTL, c.Conf, c.Http)
case "cloudflare":
dns, e = cloudflare.New(int(c.TTL), c.Conf, c.Http)
case "dnspod":
dns, e = dnspod.New(uint64(c.TTL), c.Conf, c.Http.Transport)
default:
var msg string
if c.Provider == "" {
msg = "DDNS 模块 dns 运营商不能为空"
} else {
msg = fmt.Sprintf("DDNS 模块 dns 运营商 %s 不支持", c.Provider)
}
log.Warnln(msg)
return errors.New(msg)
}
if e != nil {
log.Warnf("解析 DDNS 配置失败:%v", e)
return e
}
// 修改 dns 记录
if e = dns.SetDomainRecord(c.Domain, c.IP); e != nil {
log.Warnf("设置 dns 解析记录失败:%v", e)
return e
}
log.Debugln("DDNS 配置应用成功")
return nil
}

View File

@@ -0,0 +1,16 @@
package dns
import "net/http"
type Provider interface {
SetDomainRecord(domain, ip string) error
}
type Config struct {
Provider string
IP string
Domain string
TTL uint
Conf map[string]interface{}
Http *http.Client
}

View File

@@ -0,0 +1,9 @@
package dnsUtil
import (
"github.com/mitchellh/mapstructure"
)
func DecodeConfig(conf map[string]interface{}, output interface{}) error {
return mapstructure.Decode(conf, output)
}

View File

@@ -0,0 +1,18 @@
package dnsUtil
import (
"errors"
"strings"
)
func DecodeDomain(domain string) (subStr string, rootDomain string, e error) {
t := strings.Split(domain, ".")
if len(t) == 1 {
return "", "", errors.New("域名不合法")
} else if len(t) == 2 {
return "@", domain, nil
}
l := len(t)
return strings.Join(t[:l-2], "."), strings.Join(t[l-2:l], "."), nil
}