Initial commit

This commit is contained in:
2026-05-20 16:13:58 +08:00
commit ceffbe382e
500 changed files with 157469 additions and 0 deletions
+875
View File
@@ -0,0 +1,875 @@
package domainser
import (
"context"
"fmt"
"math/rand"
"net/http"
"net/url"
"stat/common/log"
"stat/common/utils"
"stat/models/domainmod"
"stat/statg"
"strings"
"sync"
"time"
"go.mongodb.org/mongo-driver/bson"
"github.com/ddliu/go-httpclient"
)
const (
ApiKey = "a45d5d2c6c231f28f9bcccf4092eb246"
ApiDomain = "https://api.boce.com"
ApiCheckDuration = time.Second * 10
ApiCheckTimeout = time.Minute * 2
ApiCheckTryCount = 6
)
type NodeCache struct {
List NodeList
Last time.Time
Lock sync.Mutex
}
type MutexResult struct {
List domainmod.CheckResultList
Lock sync.Mutex
}
var (
Cache = &NodeCache{
List: make(NodeList, 0),
Last: time.Time{},
Lock: sync.Mutex{},
}
)
func (c *NodeCache) NodeList() (NodeList, error) {
ret := make(NodeList, 0)
c.Lock.Lock()
if len(c.List) > 0 && c.Last.After(time.Now().Add(-24*time.Hour)) {
for _, node := range c.List {
ret = append(ret, node)
}
c.Lock.Unlock()
return ret, nil
}
c.Lock.Unlock()
list, err := GetNodeList()
if err != nil {
return ret, err
}
if len(list) > 0 {
c.Lock.Lock()
c.List = list
c.Last = time.Now()
for _, node := range c.List {
ret = append(ret, node)
}
c.Lock.Unlock()
return ret, nil
} else {
return nil, fmt.Errorf("node list is empty")
}
}
func GetNodeList() (NodeList, error) {
addr := fmt.Sprintf("https://task.boce.com/v3/node/list?key=%s", ApiKey)
res := &CommonResp{}
tryCount := 0
for {
err := Get(addr, res, url.Values{})
if err != nil {
tryCount++
if tryCount > ApiCheckTryCount {
return nil, err
}
time.Sleep(ApiCheckDuration)
continue
}
break
}
if res.ErrorCode != 0 {
return nil, fmt.Errorf("获取检测节点列表失败:%s", GetApiErrorMessage(res.ErrorCode))
}
return *res.Data.List, nil
}
func (s *MutexResult) OnFinish(result *domainmod.CheckResult) {
s.Lock.Lock()
defer s.Lock.Unlock()
s.List = append(s.List, result)
}
func (s *MutexResult) RealErrors() []error {
ret := make([]error, 0)
for _, item := range s.List {
if item.CheckSucc && item.Error != nil {
ret = append(ret, item.Error)
}
}
return ret
}
func (s *MutexResult) AllSuccess() bool {
for _, item := range s.List {
if item.CheckSucc && item.Error != nil {
return false
}
}
return true
}
func (s *MutexResult) JumpErrors() []error {
ret := make([]error, 0)
for _, item := range s.List {
if !item.CheckSucc && item.Error != nil {
ret = append(ret, item.Error)
}
}
return ret
}
func (s *MutexResult) Reports() string {
var ret string
for _, item := range s.List {
if item.Error != nil {
ret += "\n" + item.Error.Error() + "\n"
}
if len(item.NodeErrors) > 0 {
ret += strings.Join(item.NodeErrors, "\n\n")
}
}
return ret
}
func (s *MutexResult) CheckStatus() (passCount int, jumpCount int, errorCount int) {
for _, item := range s.List {
if item.CheckSucc {
if item.Error != nil {
errorCount++
} else {
passCount++
}
} else {
jumpCount++
}
}
return
}
var ApiErrorMessage = map[int]string{
0: "成功",
1: "鉴权失败",
2: "参数错误",
3: "波点不足或波点未配置",
4: "生成任务id失败或任务id失效",
}
func GetApiErrorMessage(code int) string {
ret := ApiErrorMessage[code]
if ret == "" {
return "未知错误"
}
return ret
}
var ApiPathMap = map[domainmod.CheckType]ApiPath{
domainmod.CheckTypeWeb: {
Create: "/v3/task/create/curl",
Query: "/v3/task/curl/",
Sleep: 5 * time.Minute,
NeedCreate: true,
},
domainmod.CheckTypeDNS: {
Create: "/v3/task/create/dig",
Query: "/v3/task/dig/",
Sleep: 5 * time.Minute,
NeedCreate: true,
},
domainmod.CheckTypePing: {
Create: "/v3/task/create/ping",
Query: "/v3/task/ping/",
Sleep: 5 * time.Minute,
NeedCreate: true,
},
domainmod.CheckTypeHijack: {
Create: "/v3/task/create/hijack",
Query: "/v3/task/hijack/",
Sleep: 5 * time.Minute,
NeedCreate: true,
},
domainmod.CheckTypePollute: {
Create: "",
Query: "/v3/task/create/pollute",
NeedCreate: false,
},
domainmod.CheckTypeQQ: {
Create: "",
Query: "/v3/task/create/qq",
NeedCreate: false,
},
domainmod.CheckTypeWeChat: {
Create: "",
Query: "/v3/task/create/wechat",
NeedCreate: false,
},
domainmod.CheckTypeBlack: {
Create: "",
Query: "/v3/task/create/blacklist",
NeedCreate: false,
},
domainmod.CheckTypeGFW: {
Create: "",
Query: "/v3/task/create/wall",
NeedCreate: false,
},
}
func init() {
httpclient.Defaults(httpclient.Map{
"OPT_CONNECTTIMEOUT": 30 * time.Second,
"OPT_TIMEOUT": 10 * time.Second,
"Accept-Encoding": "gzip,deflate",
})
}
func GetChecker(item *domainmod.Domain, mode domainmod.CheckType, onFinish FnCheckFinish, nodes ...Node) Checker {
nodeList := make(NodeList, 0)
nodeList = append(nodeList, nodes...)
switch mode {
case domainmod.CheckTypeWeb:
return NewWebChecker(item, mode, onFinish, nodeList)
case domainmod.CheckTypeDNS:
return NewWebChecker(item, mode, onFinish, nodeList)
case domainmod.CheckTypePing:
return NewWebChecker(item, mode, onFinish, nodeList)
case domainmod.CheckTypeHijack:
return NewHijackChecker(item, mode, onFinish, nodeList)
case domainmod.CheckTypePollute:
return NewPolluteChecker(item, mode, onFinish)
case domainmod.CheckTypeQQ:
return NewQQChecker(item, mode, onFinish)
case domainmod.CheckTypeWeChat:
return NewWeChatChecker(item, mode, onFinish)
case domainmod.CheckTypeBlack:
return NewBlackChecker(item, mode, onFinish)
case domainmod.CheckTypeGFW:
return NewGFWChecker(item, mode, onFinish)
default:
return nil
}
}
type DomainChecker struct {
Items domainmod.DomainList
}
func (c *DomainChecker) StartCheck() error {
nodes, err := Cache.NodeList()
if err != nil {
return err
}
wg := utils.NewWaitGroup()
for _, domain := range c.Items {
_domain := domain
wg.Add(func() {
CheckDomain(_domain, nodes)
})
}
wg.RunWaits()
return nil
}
func CheckDomain(domain *domainmod.Domain, nodes NodeList) {
ctx := context.Background()
begin := time.Now()
status := domainmod.CheckStatusChecking
doc := &domainmod.DomainDoc{
Status: &status,
CheckTime: &begin,
UpdatedAt: &begin,
}
if err := domainmod.UpdateOneDomain(ctx, bson.M{"_id": domain.ID}, bson.M{"$set": doc}); err != nil {
log.Warn("CheckDomain failed", log.E(err))
return
}
rets := MutexResult{
List: make(domainmod.CheckResultList, 0),
Lock: sync.Mutex{},
}
wg := utils.NewWaitGroup()
for _, check := range domain.Checks {
_check := check
wg.Add(func() {
if checker := GetChecker(domain, _check, rets.OnFinish, nodes...); checker != nil {
time.Sleep(time.Duration(rand.Intn(10)) * time.Second) // 防并发访问
checker.Check()
}
})
}
wg.RunWaits()
end := time.Now()
doc = &domainmod.DomainDoc{
CheckFinish: &end,
UpdatedAt: &end,
}
passCount, jumpCount, errCount := rets.CheckStatus()
doc.Results = &rets.List
if errCount > 0 {
report := rets.Reports()
doc.Report = &report
status0 := domainmod.CheckStatusFailed
//doc.Status = utils.PValue(domainmod.CheckStatusFailed)
if passCount > 0 {
//doc.Status = utils.PValue(domainmod.CheckStatusPartPass)
status0 = domainmod.CheckStatusPartPass
}
doc.Status = &status0
} else {
if passCount > 0 {
if jumpCount > 0 {
//doc.Status = utils.PValue(domainmod.CheckStatusPartPass)
//doc.Report = utils.PValue(rets.Reports())
status0 := domainmod.CheckStatusPartPass
report := rets.Reports()
doc.Status = &status0
doc.Report = &report
} else {
//doc.Status = utils.PValue(domainmod.CheckStatusSucc)
//doc.Report = utils.PValue("完全通过")
status0 := domainmod.CheckStatusSucc
report := "完全通过"
doc.Status = &status0
doc.Report = &report
}
} else {
//doc.Status = utils.PValue(domainmod.CheckStatusNone)
//doc.Report = utils.PValue(rets.Reports())
status0 := domainmod.CheckStatusNone
report := rets.Reports()
doc.Status = &status0
doc.Report = &report
}
}
if err := domainmod.UpdateOneDomain(ctx, bson.M{"_id": domain.ID}, bson.M{"$set": doc}); err != nil {
log.Warn("CheckDomain failed", log.E(err))
return
}
if errCount > 0 {
text := fmt.Sprintf("域名检测:【%s】\n", domain.Name)
text += fmt.Sprintf("开始时间:【%s】\n", begin.Format("2006-01-02T15:04:05"))
text += fmt.Sprintf("结束时间:【%s】\n", end.Format("2006-01-02T15:04:05"))
text += fmt.Sprintf("检测用时:【%s】\n", end.Sub(begin))
text += fmt.Sprintf("检测结果:【%s】\n", *doc.Status)
for _, result := range rets.List {
if result.CheckSucc {
if result.Error == nil {
text += fmt.Sprintf("%s: %s\n", result.CheckType.Name(), "\U0001F7E2 通过")
} else {
if result.NodeCount == 0 {
text += fmt.Sprintf("%s: %s\n", result.CheckType.Name(), "🔴 未通过")
} else {
if result.SuccRate < result.MinRate {
if result.SuccRate < 25 {
text += fmt.Sprintf("%s: %s\n", result.CheckType.Name(), "🔴 未通过")
} else {
text += fmt.Sprintf("%s: %s\n", result.CheckType.Name(), "\U0001F7E0 通过")
}
} else {
text += fmt.Sprintf("%s: %s\n", result.CheckType.Name(), "\U0001F7E2 通过")
}
}
}
} else {
text += fmt.Sprintf("%s: %s\n", result.CheckType.Name(), "🕓 超时")
}
}
if domain.Redirected {
text += fmt.Sprintf("域名跳转:【已做301跳转✅】\n")
text += fmt.Sprintf("跳转地址:【==>%s】\n", domain.RedirectDomain)
} else {
text += fmt.Sprintf("域名跳转:【未做301跳转🈚】\n")
}
statg.Bot.SendDomainMessage(text)
}
}
type WebChecker struct {
Item *domainmod.Domain
Mode domainmod.CheckType
Finish FnCheckFinish
Nodes NodeList
}
func NewWebChecker(item *domainmod.Domain, mode domainmod.CheckType, onFinish FnCheckFinish, nodes NodeList) *WebChecker {
return &WebChecker{
Item: item,
Mode: mode,
Finish: onFinish,
Nodes: nodes,
}
}
func (s *WebChecker) Check() error {
ret := &domainmod.CheckResult{
Domain: s.Item.Name,
CheckType: s.Mode,
BeginTime: time.Now(),
MinRate: s.Item.MinRate,
}
defer s.Finish(ret)
api, ok := ApiPathMap[s.Mode]
if !ok {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("域名%s的检测类型%d不支持", s.Item.Name, s.Mode)
return ret.Error
}
addr, err := api.GetCreateUrl(url.Values{
"node_ids": []string{s.Nodes.SIdList(s.Item.MaxNodes)},
"host": []string{s.Item.Name},
})
if err != nil {
ret.EndTime = time.Now()
ret.Error = err
return ret.Error
}
tryCount := 0
res := &CommonResp{}
for {
err = Get(addr, res, url.Values{})
if err != nil {
tryCount++
if tryCount > ApiCheckTryCount {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("创建【%s】的【%s】检测任务超时:%s", s.Item.Name, s.Mode.Name(), err.Error())
return ret.Error
}
time.Sleep(ApiCheckDuration)
continue
}
break
}
if res.ErrorCode != 0 {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("创建【%s】的【%s】检测任务失败:%s", s.Item.Name, s.Mode.Name(), GetApiErrorMessage(res.ErrorCode))
return ret.Error
}
// 获取查询ID
if res.Data.Id == nil || *res.Data.Id == "" {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("创建【%s】的【%s】检测任务失败:任务ID未空", s.Item.Name, s.Mode.Name())
return ret.Error
}
TaskId := *res.Data.Id
// 去查询结果
tryCount = 0
addr, err = api.GetQueryUrlWithTaskId(TaskId, url.Values{})
end := time.Now().Add(ApiCheckTimeout)
for {
time.Sleep(ApiCheckDuration)
webResp := &WebResp{}
err = Get(addr, webResp, url.Values{})
if err != nil {
tryCount++
if tryCount > ApiCheckTryCount {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("查询【%s】的【%s】检测结果超时:%s", s.Item.Name, s.Mode.Name(), err.Error())
return ret.Error
}
continue
}
ret.CheckSucc = webResp.Done
if webResp.Done {
for _, node := range webResp.List {
if node.ErrorCode != 0 {
ret.FailedCount++
ret.NodeErrors = append(ret.NodeErrors, node.ErrorString())
} else {
ret.NodeCount++
if node.HttpCode == http.StatusOK {
ret.SuccCount++
} else {
ret.NodeErrors = append(ret.NodeErrors, node.ErrorString())
}
}
}
if ret.NodeCount > 0 {
ret.SuccRate = float64(ret.SuccCount*100) / float64(ret.NodeCount)
if ret.SuccRate < ret.MinRate {
ret.Error = fmt.Errorf("节点通过率%.1f%%<%.1f%%,未通过!", ret.SuccRate, ret.MinRate)
}
}
break
} else {
if end.Before(time.Now()) {
ret.Error = fmt.Errorf("查询【%s】的【%s】检测超时", s.Item.Name, s.Mode.Name())
break
}
}
}
ret.EndTime = time.Now()
return ret.Error
}
type HijackChecker struct {
Item *domainmod.Domain
Mode domainmod.CheckType
Finish FnCheckFinish
Nodes NodeList
}
func NewHijackChecker(item *domainmod.Domain, mode domainmod.CheckType, onFinish FnCheckFinish, nodes NodeList) *HijackChecker {
return &HijackChecker{
Item: item,
Mode: mode,
Finish: onFinish,
Nodes: nodes,
}
}
func (s *HijackChecker) Check() error {
ret := &domainmod.CheckResult{
Domain: s.Item.Name,
CheckType: s.Mode,
BeginTime: time.Now(),
MinRate: s.Item.MinRate,
}
defer s.Finish(ret)
api, ok := ApiPathMap[s.Mode]
if !ok {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("域名%s的检测类型%d不支持", s.Item.Name, s.Mode)
return ret.Error
}
addr, err := api.GetCreateUrl(url.Values{
"node_ids": []string{s.Nodes.SIdList(s.Item.MaxNodes)},
"host": []string{s.Item.Name},
})
if err != nil {
ret.EndTime = time.Now()
ret.Error = err
return ret.Error
}
tryCount := 0
res := &CommonResp{}
for {
err = Get(addr, res, url.Values{})
if err != nil {
tryCount++
if tryCount > ApiCheckTryCount {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("创建【%s】的【%s】检测任务超时:%s", s.Item.Name, s.Mode.Name(), err.Error())
return ret.Error
}
time.Sleep(ApiCheckDuration)
continue
}
break
}
if res.ErrorCode != 0 {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("创建【%s】的【%s】检测任务失败:%s", s.Item.Name, s.Mode.Name(), GetApiErrorMessage(res.ErrorCode))
return ret.Error
}
// 获取查询ID
if res.Data.Id == nil || *res.Data.Id == "" {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("创建【%s】的【%s】检测任务失败:任务ID未空", s.Item.Name, s.Mode.Name())
return ret.Error
}
TaskId := *res.Data.Id
// 去查询结果
tryCount = 0
addr, err = api.GetQueryUrlWithTaskId(TaskId, url.Values{})
end := time.Now().Add(ApiCheckTimeout)
for {
time.Sleep(ApiCheckDuration)
webResp := &HijackResp{}
err = Get(addr, webResp, url.Values{})
if err != nil {
tryCount++
if tryCount > ApiCheckTryCount {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("查询【%s】的【%s】接口错误【%d】次,超过最大重试次数:%s", s.Item.Name, s.Mode.Name(), tryCount, err.Error())
return ret.Error
}
continue
}
ret.CheckSucc = webResp.Done
if webResp.Done {
for _, node := range webResp.List {
ret.NodeCount++
if node.Hijack {
ret.FailedCount++
ret.NodeErrors = append(ret.NodeErrors, node.ErrorString())
} else {
ret.SuccCount++
}
}
if ret.NodeCount > 0 {
ret.SuccRate = float64(ret.SuccCount*100) / float64(ret.NodeCount)
if ret.SuccRate < ret.MinRate {
ret.Error = fmt.Errorf("节点通过率%.1f%%<%.1f%%,未通过!", ret.SuccRate, ret.MinRate)
}
}
break
} else {
if end.Before(time.Now()) {
ret.Error = fmt.Errorf("查询【%s】的【%s】检测超时", s.Item.Name, s.Mode.Name())
break
}
}
}
ret.EndTime = time.Now()
return ret.Error
}
type FuncCheckResult func(ret *domainmod.CheckResult, data map[string]int)
func QueryCheck(item *domainmod.Domain, mode domainmod.CheckType, onFinish FnCheckFinish, onCheck FuncCheckResult) error {
ret := &domainmod.CheckResult{
Domain: item.Name,
CheckType: mode,
BeginTime: time.Now(),
MinRate: item.MinRate,
}
defer onFinish(ret)
api, ok := ApiPathMap[mode]
if !ok {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("查询接口地址【%s】的【%s】出错", item.Name, mode.Name())
return ret.Error
}
addr, err := api.GetQueryUrl(url.Values{"host": []string{item.Name}})
polluteResp := &PolluteResp{}
tryCount := 0
for {
tryCount++
err = Get(addr, polluteResp, url.Values{})
if err != nil {
if tryCount > ApiCheckTryCount {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("查询【%s】的【%s】接口错误【%d】次,超过最大重试次数:%s", item.Name, mode.Name(), tryCount, err.Error())
return ret.Error
}
time.Sleep(ApiCheckDuration)
continue
}
break
}
if polluteResp.ErrorCode != 0 {
ret.EndTime = time.Now()
ret.Error = fmt.Errorf("查询【%s】的【%s】返回值不是成功:%s", item.Name, mode.Name(), GetApiErrorMessage(polluteResp.ErrorCode))
return ret.Error
}
ret.CheckSucc = true
onCheck(ret, polluteResp.Data)
ret.EndTime = time.Now()
return ret.Error
}
type PolluteChecker struct {
Item *domainmod.Domain
Mode domainmod.CheckType
Finish FnCheckFinish
}
func NewPolluteChecker(item *domainmod.Domain, mode domainmod.CheckType, onFinish FnCheckFinish) *PolluteChecker {
return &PolluteChecker{
Item: item,
Mode: mode,
Finish: onFinish,
}
}
func (s *PolluteChecker) Check() error {
return QueryCheck(s.Item, s.Mode, s.Finish, func(ret *domainmod.CheckResult, data map[string]int) {
if code, ok := data[s.Item.Name]; ok { //(-1 未注册,0 正常, 1 污染)
if ok {
if code == 0 {
ret.SuccRate = 100.0
} else {
if code == 1 {
ret.Error = fmt.Errorf("域名被污染")
} else if code == -1 {
ret.Error = fmt.Errorf("域名未注册")
} else {
ret.Error = fmt.Errorf("未知错误码%d", code)
}
}
} else {
ret.Error = fmt.Errorf("返回值中未检测到域名")
}
}
})
}
type QQChecker struct {
Item *domainmod.Domain
Mode domainmod.CheckType
Finish FnCheckFinish
}
func NewQQChecker(item *domainmod.Domain, mode domainmod.CheckType, onFinish FnCheckFinish) *QQChecker {
return &QQChecker{
Item: item,
Mode: mode,
Finish: onFinish,
}
}
func (s *QQChecker) Check() error {
return QueryCheck(s.Item, s.Mode, s.Finish, func(ret *domainmod.CheckResult, data map[string]int) {
if code, ok := data[s.Item.Name]; ok { //1正常, 2拦截, 3-检测失败
if ok {
if code == 1 {
ret.CheckSucc = true
ret.SuccRate = 100.0
} else {
if code == 2 {
ret.CheckSucc = true
ret.Error = fmt.Errorf("已被拦截")
} else if code == 3 {
ret.Error = fmt.Errorf("检测失败")
} else {
ret.Error = fmt.Errorf("检测值【%d】不知道意义", code)
}
}
} else {
ret.Error = fmt.Errorf("返回值中未检测到域名")
}
}
})
}
type WeChatChecker struct {
Item *domainmod.Domain
Mode domainmod.CheckType
Finish FnCheckFinish
}
func NewWeChatChecker(item *domainmod.Domain, mode domainmod.CheckType, onFinish FnCheckFinish) *WeChatChecker {
return &WeChatChecker{
Item: item,
Mode: mode,
Finish: onFinish,
}
}
func (s *WeChatChecker) Check() error {
return QueryCheck(s.Item, s.Mode, s.Finish, func(ret *domainmod.CheckResult, data map[string]int) {
if code, ok := data[s.Item.Name]; ok { //1正常, 2拦截(域名被封), 3-检测失败,4-拦截(非微信官方链接,需跳转访问)
if ok {
if code == 1 || code == 4 {
ret.CheckSucc = true
ret.SuccRate = 100.0
} else {
if code == 2 {
ret.CheckSucc = true
ret.Error = fmt.Errorf("已被拦截")
} else if code == 3 {
ret.Error = fmt.Errorf("检测失败")
} else {
ret.Error = fmt.Errorf("检测值【%d】不知道意义", code)
}
}
} else {
ret.Error = fmt.Errorf("返回值中未检测到域名")
}
}
})
}
type BlackChecker struct {
Item *domainmod.Domain
Mode domainmod.CheckType
Finish FnCheckFinish
}
func NewBlackChecker(item *domainmod.Domain, mode domainmod.CheckType, onFinish FnCheckFinish) *BlackChecker {
return &BlackChecker{
Item: item,
Mode: mode,
Finish: onFinish,
}
}
func (s *BlackChecker) Check() error {
return QueryCheck(s.Item, s.Mode, s.Finish, func(ret *domainmod.CheckResult, data map[string]int) {
if code, ok := data[s.Item.Name]; ok { //1-正常, 2-在黑名单
if ok {
if code == 1 {
ret.CheckSucc = true
ret.SuccRate = 100.0
} else {
if code == 2 {
ret.CheckSucc = true
ret.Error = fmt.Errorf("在黑名单")
} else {
ret.Error = fmt.Errorf("检测值【%d】不知道意义", code)
}
}
} else {
ret.Error = fmt.Errorf("返回值中未检测到域名")
}
}
})
}
type GFWChecker struct {
Item *domainmod.Domain
Mode domainmod.CheckType
Finish FnCheckFinish
}
func NewGFWChecker(item *domainmod.Domain, mode domainmod.CheckType, onFinish FnCheckFinish) *GFWChecker {
return &GFWChecker{
Item: item,
Mode: mode,
Finish: onFinish,
}
}
func (s *GFWChecker) Check() error {
return QueryCheck(s.Item, s.Mode, s.Finish, func(ret *domainmod.CheckResult, data map[string]int) {
if code, ok := data[s.Item.Name]; ok { //0 正常, 1 被墙, 2 疑似被墙, 3 无资源请重试,4 域名格式错误
if ok {
if code == 0 { // 正常
ret.CheckSucc = true
ret.SuccRate = 100.0
} else {
if code == 1 {
ret.CheckSucc = true
ret.Error = fmt.Errorf("域名被墙")
} else if code == 2 {
ret.CheckSucc = true
ret.Error = fmt.Errorf("疑似被墙")
} else if code == 3 {
ret.Error = fmt.Errorf("无资源请重试")
} else if code == 4 {
ret.Error = fmt.Errorf("域名格式错误")
} else {
ret.Error = fmt.Errorf("检测值【%d】不知道意义", code)
}
}
} else {
ret.Error = fmt.Errorf("返回值中未检测到域名")
}
}
})
}
+135
View File
@@ -0,0 +1,135 @@
package domainser
import (
"fmt"
"math/rand"
"sort"
"stat/models/domainmod"
"strings"
"time"
)
type FnCheckFinish func(result *domainmod.CheckResult)
type Checker interface {
Check() error
}
type ApiPath struct {
Create string
Query string
Sleep time.Duration
NeedCreate bool
}
type Node struct {
Id int `json:"id"`
NodeName string `json:"node_name"`
IspName string `json:"isp_name"`
IspCode int `json:"isp_code"`
Rand int
}
type NodeList []Node
func (n NodeList) Len() int { return len(n) }
func (n NodeList) Less(i, j int) bool {
return n[i].Rand < n[j].Rand
}
func (n NodeList) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
func (n NodeList) SIdList(max int32) string {
if max < 1 {
max = 1
}
for _, node := range n {
node.Rand = rand.Int()
}
sort.Sort(n)
ss := make([]string, 0)
for _, node := range n {
if strings.Contains(node.IspName, "电信") || strings.Contains(node.IspName, "移动") || strings.Contains(node.IspName, "联通") {
ss = append(ss, fmt.Sprintf("%d", node.Id))
}
}
if len(ss) > int(max) {
ss = ss[:max]
}
return strings.Join(ss, ",")
}
type CommonResp struct {
ErrorCode int `json:"error_code"`
Error string `json:"error"`
Data struct {
Id *string `json:"id"`
List *NodeList `json:"list"`
} `json:"data"`
}
type DomainResp struct {
ErrorCode int `json:"error_code"`
Error string `json:"error"`
Data map[string]int `json:"data"`
}
type WebTestNode struct {
NodeId int `json:"node_id"`
NodeName string `json:"node_name"`
Host string `json:"host"`
OriginIp string `json:"origin_ip"`
HttpCode int `json:"http_code"`
RemoteIp string `json:"remote_ip"`
SizeDownload int `json:"size_download"`
SpeedDownload int `json:"speed_download"`
TimeConnect float64 `json:"time_connect"`
TimeNamelookup float64 `json:"time_namelookup"`
TimeStarttransfer float64 `json:"time_starttransfer"`
TimeTotal float64 `json:"time_total"`
ReportSource string `json:"report_source"`
SessionId string `json:"session_id"`
IpRegion string `json:"ip_region"`
IpIsp string `json:"ip_isp"`
ErrorCode int `json:"error_code"`
Error string `json:"error"`
TimeId string `json:"time_id"`
}
type WebTestNodeList []*WebTestNode
func (n *WebTestNode) ErrorString() string {
return fmt.Sprintf("节点【%s】检测失败", n.NodeName)
}
type WebResp struct {
Id string `json:"id"`
MaxNode int `json:"max_node"`
Done bool `json:"done"`
List WebTestNodeList `json:"list"`
}
type HijackTestNode struct {
NodeId int `json:"node_id"`
Hijack bool `json:"hijack"`
NodeName string `json:"node_name"`
RemoteIp string `json:"remote_ip"`
Url string `json:"url"`
ReportSource string `json:"report_source"`
}
func (n *HijackTestNode) ErrorString() string {
return fmt.Sprintf("节点【%s】已劫持", n.NodeName)
}
type HijackResp struct {
Done bool `json:"done"`
HijackNum int `json:"hijack_num"`
Id string `json:"id"`
List []*HijackTestNode `json:"list"`
MaxNode int `json:"max_node"`
}
type PolluteResp struct {
ErrorCode int `json:"error_code"`
Error string `json:"error"`
Data map[string]int `json:"data"`
}
+76
View File
@@ -0,0 +1,76 @@
package domainser
import (
"context"
"stat/common/enum/consts"
"stat/common/log"
"stat/common/statutils"
"stat/models/domainmod"
"stat/statg"
"time"
"go.mongodb.org/mongo-driver/bson"
"github.com/robfig/cron/v3"
)
var (
checkCron *cron.Cron
)
func init() {
checkCron = cron.New(cron.WithSeconds(), cron.WithChain(cron.SkipIfStillRunning(&log.Logger{}))) // 秒级定时任务
}
// Start 定时器任务启动
func Start() {
log.Info("domain checker start...")
ctx := context.Background()
if statg.Conf.RunMod != consts.RunModRelease {
return
}
_, _ = checkCron.AddFunc("0 0/5 * * * ?", func() {
statutils.ServerMon.RunWithName(func() error {
return DomainCheck(ctx)
}, statutils.GetFuncName(DomainCheck))
})
checkCron.Start()
}
func DomainCheck(ctx context.Context) error {
list, err := domainmod.FindManyDomain(ctx, bson.M{
"enable": true,
"checkTime": bson.M{"$lt": time.Now().Add(-10 * time.Minute)},
})
if err != nil {
return err
}
redirectedChecks := []domainmod.CheckType{domainmod.CheckTypePollute}
unRedirectedChecks := []domainmod.CheckType{domainmod.CheckTypePollute, domainmod.CheckTypeGFW}
domains := make(domainmod.DomainList, 0)
for _, item := range list {
if item.Redirected {
item.Checks = redirectedChecks
} else {
item.Checks = unRedirectedChecks
}
if item.Status == domainmod.CheckStatusNone {
if item.CheckTime.Before(time.Now().Add(-10 * time.Minute)) {
domains = append(domains, item)
}
} else {
if item.CheckTime.Before(time.Now().Add(time.Duration(-item.CycleHour) * time.Hour)) {
domains = append(domains, item)
}
}
}
if len(domains) == 0 {
return nil
}
domainChecker := &DomainChecker{
Items: domains,
}
return domainChecker.StartCheck()
}
+95
View File
@@ -0,0 +1,95 @@
package domainser
import (
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/ddliu/go-httpclient"
)
func ParseDomain(name string) (string, error) {
name = strings.TrimSpace(name)
if strings.Contains(strings.ToLower(name), "http:") || strings.Contains(strings.ToLower(name), "https:") {
u, err := url.Parse(name)
if err != nil {
return "", err
}
if u.Host == "" {
return "", fmt.Errorf("没有检测到有效的域名")
}
if net.ParseIP(u.Host) != nil {
return "", fmt.Errorf("只能填写域名,不能填写IP地址")
}
host, _, err := net.SplitHostPort(u.Host)
if err != nil {
return "", fmt.Errorf("域名无效")
}
return host, nil
} else {
re, err := regexp.Compile(`^(?:[_a-z0-9](?:[_a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?)?$`)
if err != nil {
return "", err
}
if !re.MatchString(name) {
return "", fmt.Errorf("请输入正确的域名")
}
return name, nil
}
}
func (n *ApiPath) GetCreateUrl(param url.Values) (string, error) {
param.Set("key", ApiKey)
if n.NeedCreate {
base, err := url.Parse(ApiDomain + n.Create)
if err != nil {
return "", err
}
base.RawQuery = param.Encode()
return base.String(), nil
} else {
return "", fmt.Errorf("do not need create url")
}
}
func (n *ApiPath) GetQueryUrlWithTaskId(taskId string, param url.Values) (string, error) {
param.Set("key", ApiKey)
base, err := url.Parse(ApiDomain + n.Query + taskId)
if err != nil {
return "", err
}
base.RawQuery = param.Encode()
return base.String(), nil
}
func (n *ApiPath) GetQueryUrl(param url.Values) (string, error) {
param.Set("key", ApiKey)
base, err := url.Parse(ApiDomain + n.Query)
if err != nil {
return "", err
}
base.RawQuery = param.Encode()
return base.String(), nil
}
func Get(url string, resp interface{}, param url.Values) error {
res, err := httpclient.Get(url, param)
if err != nil {
return fmt.Errorf("访问检测网站失败:%s", err.Error())
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("访问检测网站失败, HTTP Code:%d, Error:%s", res.StatusCode, res.Status)
}
data, err := res.ReadAll()
if err != nil {
return fmt.Errorf("读取检测网站返回数据失败:%s", err.Error())
}
if err = json.Unmarshal(data, resp); err != nil {
return fmt.Errorf("解码检测网站返回数据失败:%s", err.Error())
}
return nil
}
+133
View File
@@ -0,0 +1,133 @@
package domainser
import (
"context"
"fmt"
"stat/api/proto"
"stat/common/stdLog"
"stat/common/utils"
"stat/models/appInfoMod"
"stat/models/domainmod"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
)
// 获取DomainInfo列表
func GetDomainInfoList(ctx context.Context, param proto.ListDomainInfoReq) (code stdLog.CodeType, data interface{}, err error) {
resp := proto.ListDomainInfoRes{
Total: 0,
List: make(domainmod.DomainList, 0),
}
skip, limit := utils.ToDBPage(param.PageNum, param.PageSize)
filter := bson.M{}
if param.Name != nil {
filter["name"] = bson.M{"$regex": fmt.Sprintf("^%s", *param.Name)}
}
if param.AppId != nil {
filter["appId"] = *param.AppId
}
if param.Enable != nil {
filter["enable"] = *param.Enable
}
if param.Status != nil {
filter["status"] = *param.Status
}
if param.Category != nil {
filter["category"] = *param.Category
}
if param.Check != nil {
filter["checks"] = bson.M{"$in": *param.Check}
}
opts := options.Find().SetSkip(skip).SetLimit(limit)
count, err := domainmod.CountDomains(ctx, filter)
if err != nil {
code = stdLog.Failure
return
}
aList, err := domainmod.FindManyDomain(ctx, filter, opts)
if err != nil {
code = stdLog.Failure
return
}
appMap, _ := appInfoMod.GetAllAppIdNameMap(ctx)
for _, item := range aList {
item.AppName = appMap[item.AppId]
}
resp.Total = count
resp.List = aList
data = resp
code = stdLog.Success
return
}
// 添加DomainInfo
func AddDomainInfo(ctx context.Context, param proto.AddDomainInfoReq) (code stdLog.CodeType, data interface{}, err error) {
name, err := ParseDomain(param.Name)
if err != nil {
code = stdLog.ParamError
return
}
param.Name = name
now := time.Now()
param.UpdatedAt = now
param.CreatedAt = now
if param.CycleHour < 1 {
param.CycleHour = 1
}
if err = domainmod.InsertOneDomain(ctx, &param.Domain); err != nil {
code = stdLog.Failure
return
}
code = stdLog.Success
return
}
// 更新DomainInfo
func UpdateDomainInfo(ctx context.Context, param proto.UpdateDomainInfoReq) (code stdLog.CodeType,
data interface{}, err error) {
now := time.Now()
param.Update.UpdatedAt = &now
if param.Update.Name != nil {
var name string
name, err = ParseDomain(*param.Update.Name)
if err != nil {
code = stdLog.ParamError
return
}
param.Update.Name = &name
}
if param.Update.CycleHour != nil {
if *param.Update.CycleHour < 1 {
hour := int64(1)
param.Update.CycleHour = &hour
}
}
if err = domainmod.UpdateOneDomain(ctx, bson.M{"_id": param.Id}, bson.M{"$set": param.Update}); err != nil {
code = stdLog.Failure
return
}
code = stdLog.Success
return
}
// 删除DomainInfo
func DelDomainInfo(ctx context.Context, param proto.DelDomainInfoReq) (code stdLog.CodeType, data interface{}, err error) {
if _, err = domainmod.DelOneDomain(ctx, bson.M{"_id": param.Id}); err != nil {
code = stdLog.Failure
return
}
code = stdLog.Success
return
}