mirror of
				https://github.com/oneclickvirt/ecs.git
				synced 2025-10-31 19:03:00 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			605 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			605 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package utils
 | ||
| 
 | ||
| import (
 | ||
| 	"bufio"
 | ||
| 	"bytes"
 | ||
| 	"context"
 | ||
| 	"fmt"
 | ||
| 	"io"
 | ||
| 	"net"
 | ||
| 	"net/http"
 | ||
| 	"os"
 | ||
| 	"path/filepath"
 | ||
| 	"regexp"
 | ||
| 	"strconv"
 | ||
| 	"strings"
 | ||
| 	"sync"
 | ||
| 	"time"
 | ||
| 	"unicode/utf8"
 | ||
| 
 | ||
| 	"github.com/imroc/req/v3"
 | ||
| 	"github.com/oneclickvirt/UnlockTests/uts"
 | ||
| 	bnetwork "github.com/oneclickvirt/basics/network"
 | ||
| 	"github.com/oneclickvirt/basics/system"
 | ||
| 	butils "github.com/oneclickvirt/basics/utils"
 | ||
| 	. "github.com/oneclickvirt/defaultset"
 | ||
| 	"github.com/oneclickvirt/security/network"
 | ||
| )
 | ||
| 
 | ||
| // 获取本程序本日及总执行的统计信息
 | ||
| type StatsResponse struct {
 | ||
| 	Counter   string `json:"counter"`
 | ||
| 	Action    string `json:"action"`
 | ||
| 	Total     int    `json:"total"`
 | ||
| 	Daily     int    `json:"daily"`
 | ||
| 	Date      string `json:"date"`
 | ||
| 	Timestamp string `json:"timestamp"`
 | ||
| }
 | ||
| 
 | ||
| // 获取最新的Github的仓库中的版本
 | ||
| type GitHubRelease struct {
 | ||
| 	TagName string `json:"tag_name"`
 | ||
| }
 | ||
| 
 | ||
| // PrintCenteredTitle 根据指定的宽度打印居中标题
 | ||
| func PrintCenteredTitle(title string, width int) {
 | ||
| 	// 计算字符串的字符数
 | ||
| 	titleLength := utf8.RuneCountInString(title)
 | ||
| 	totalPadding := width - titleLength
 | ||
| 	padding := totalPadding / 2
 | ||
| 	paddingStr := strings.Repeat("-", padding)
 | ||
| 	fmt.Println(paddingStr + title + paddingStr + strings.Repeat("-", totalPadding%2))
 | ||
| }
 | ||
| 
 | ||
| // PrintHead 根据语言打印头部信息
 | ||
| func PrintHead(language string, width int, ecsVersion string) {
 | ||
| 	if language == "zh" {
 | ||
| 		PrintCenteredTitle("VPS融合怪测试", width)
 | ||
| 		fmt.Printf("版本:%s\n", ecsVersion)
 | ||
| 		fmt.Println("测评频道: https://t.me/+UHVoo2U4VyA5NTQ1\n" +
 | ||
| 			"Go项目地址:https://github.com/oneclickvirt/ecs\n" +
 | ||
| 			"Shell项目地址:https://github.com/spiritLHLS/ecs")
 | ||
| 	} else {
 | ||
| 		PrintCenteredTitle("VPS Fusion Monster Test", width)
 | ||
| 		fmt.Printf("Version: %s\n", ecsVersion)
 | ||
| 		fmt.Println("Review Channel: https://t.me/+UHVoo2U4VyA5NTQ1\n" +
 | ||
| 			"Go Project: https://github.com/oneclickvirt/ecs\n" +
 | ||
| 			"Shell Project: https://github.com/spiritLHLS/ecs")
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| func CheckChina(enableLogger bool) bool {
 | ||
| 	if enableLogger {
 | ||
| 		InitLogger()
 | ||
| 		defer Logger.Sync()
 | ||
| 	}
 | ||
| 	var selectChina bool
 | ||
| 	client := req.C()
 | ||
| 	client.SetTimeout(6 * time.Second)
 | ||
| 	client.R().
 | ||
| 		SetRetryCount(2).
 | ||
| 		SetRetryBackoffInterval(1*time.Second, 3*time.Second).
 | ||
| 		SetRetryFixedInterval(2 * time.Second)
 | ||
| 	ipapiURL := "https://ipapi.co/json"
 | ||
| 	ipapiResp, err := client.R().Get(ipapiURL)
 | ||
| 	if err != nil {
 | ||
| 		if enableLogger {
 | ||
| 			Logger.Info("无法获取IP信息:" + err.Error())
 | ||
| 		}
 | ||
| 		return false
 | ||
| 	}
 | ||
| 	defer ipapiResp.Body.Close()
 | ||
| 	ipapiBody, err := ipapiResp.ToString()
 | ||
| 	if err != nil {
 | ||
| 		if enableLogger {
 | ||
| 			Logger.Info("无法读取IP信息响应:" + err.Error())
 | ||
| 		}
 | ||
| 		return false
 | ||
| 	}
 | ||
| 	isInChina := strings.Contains(ipapiBody, "China")
 | ||
| 	if isInChina {
 | ||
| 		fmt.Println("根据 ipapi.co 提供的信息,当前IP可能在中国")
 | ||
| 		var input string
 | ||
| 		fmt.Print("是否选用中国专项测试(无流媒体测试,有三网Ping值测试)? ([y]/n) ")
 | ||
| 		fmt.Scanln(&input)
 | ||
| 		switch strings.ToLower(input) {
 | ||
| 		case "yes", "y":
 | ||
| 			fmt.Println("使用中国专项测试")
 | ||
| 			selectChina = true
 | ||
| 		case "no", "n":
 | ||
| 			fmt.Println("不使用中国专项测试")
 | ||
| 		default:
 | ||
| 			fmt.Println("使用中国专项测试")
 | ||
| 			selectChina = true
 | ||
| 		}
 | ||
| 	}
 | ||
| 	return selectChina
 | ||
| }
 | ||
| 
 | ||
| // OnlyBasicsIpInfo 仅检查和输出IP信息
 | ||
| func OnlyBasicsIpInfo(language string) (string, string, string) {
 | ||
| 	ipv4, ipv6, ipInfo, _, err := bnetwork.NetworkCheck("both", false, language)
 | ||
| 	if err != nil {
 | ||
| 		return "", "", ""
 | ||
| 	}
 | ||
| 	basicInfo := ipInfo
 | ||
| 	if strings.Contains(ipInfo, "IPV4") && strings.Contains(ipInfo, "IPV6") && ipv4 != "" && ipv6 != "" {
 | ||
| 		uts.IPV4 = true
 | ||
| 		uts.IPV6 = true
 | ||
| 	} else if strings.Contains(ipInfo, "IPV4") && ipv4 != "" {
 | ||
| 		uts.IPV4 = true
 | ||
| 		uts.IPV6 = false
 | ||
| 	} else if strings.Contains(ipInfo, "IPV6") && ipv6 != "" {
 | ||
| 		uts.IPV6 = true
 | ||
| 		uts.IPV4 = false
 | ||
| 	}
 | ||
| 	basicInfo = strings.ReplaceAll(basicInfo, "\n\n", "\n")
 | ||
| 	return ipv4, ipv6, basicInfo
 | ||
| }
 | ||
| 
 | ||
| // BasicsAndSecurityCheck 执行安全检查
 | ||
| func BasicsAndSecurityCheck(language, nt3CheckType string, securityCheckStatus bool) (string, string, string, string, string) {
 | ||
| 	var wgt sync.WaitGroup
 | ||
| 	var ipv4, ipv6, ipInfo, securityInfo, systemInfo string
 | ||
| 	wgt.Add(1)
 | ||
| 	go func() {
 | ||
| 		defer wgt.Done()
 | ||
| 		ipv4, ipv6, ipInfo, securityInfo, _ = network.NetworkCheck("both", securityCheckStatus, language)
 | ||
| 		// if err != nil {
 | ||
| 		// 	fmt.Println(err.Error())
 | ||
| 		// }
 | ||
| 	}()
 | ||
| 	wgt.Add(1)
 | ||
| 	go func() {
 | ||
| 		defer wgt.Done()
 | ||
| 		systemInfo = system.CheckSystemInfo(language)
 | ||
| 	}()
 | ||
| 	wgt.Wait()
 | ||
| 	basicInfo := systemInfo + ipInfo
 | ||
| 	if strings.Contains(ipInfo, "IPV4") && strings.Contains(ipInfo, "IPV6") && ipv4 != "" && ipv6 != "" {
 | ||
| 		uts.IPV4 = true
 | ||
| 		uts.IPV6 = true
 | ||
| 		if nt3CheckType == "" {
 | ||
| 			nt3CheckType = "ipv4"
 | ||
| 		}
 | ||
| 	} else if strings.Contains(ipInfo, "IPV4") && ipv4 != "" {
 | ||
| 		uts.IPV4 = true
 | ||
| 		uts.IPV6 = false
 | ||
| 		if nt3CheckType == "" {
 | ||
| 			nt3CheckType = "ipv4"
 | ||
| 		}
 | ||
| 	} else if strings.Contains(ipInfo, "IPV6") && ipv6 != "" {
 | ||
| 		uts.IPV6 = true
 | ||
| 		uts.IPV4 = false
 | ||
| 		if nt3CheckType == "" {
 | ||
| 			nt3CheckType = "ipv6"
 | ||
| 		}
 | ||
| 	}
 | ||
| 	if nt3CheckType == "ipv4" && !strings.Contains(ipInfo, "IPV4") && strings.Contains(ipInfo, "IPV6") {
 | ||
| 		nt3CheckType = "ipv6"
 | ||
| 	} else if nt3CheckType == "ipv6" && !strings.Contains(ipInfo, "IPV6") && strings.Contains(ipInfo, "IPV4") {
 | ||
| 		nt3CheckType = "ipv4"
 | ||
| 	}
 | ||
| 	basicInfo = strings.ReplaceAll(basicInfo, "\n\n", "\n")
 | ||
| 	return ipv4, ipv6, basicInfo, securityInfo, nt3CheckType
 | ||
| }
 | ||
| 
 | ||
| // CaptureOutput 捕获函数输出和错误输出,实时输出,并返回字符串
 | ||
| func CaptureOutput(f func()) string {
 | ||
| 	// 保存旧的 stdout 和 stderr
 | ||
| 	oldStdout := os.Stdout
 | ||
| 	oldStderr := os.Stderr
 | ||
| 	// 创建管道
 | ||
| 	stdoutPipeR, stdoutPipeW, err := os.Pipe()
 | ||
| 	if err != nil {
 | ||
| 		return "Error creating stdout pipe"
 | ||
| 	}
 | ||
| 	stderrPipeR, stderrPipeW, err := os.Pipe()
 | ||
| 	if err != nil {
 | ||
| 		stdoutPipeW.Close()
 | ||
| 		stdoutPipeR.Close()
 | ||
| 		return "Error creating stderr pipe"
 | ||
| 	}
 | ||
| 	// 替换标准输出和标准错误输出为管道写入端
 | ||
| 	os.Stdout = stdoutPipeW
 | ||
| 	os.Stderr = stderrPipeW
 | ||
| 	// 恢复标准输出和标准错误输出
 | ||
| 	defer func() {
 | ||
| 		os.Stdout = oldStdout
 | ||
| 		os.Stderr = oldStderr
 | ||
| 		stdoutPipeW.Close()
 | ||
| 		stderrPipeW.Close()
 | ||
| 		stdoutPipeR.Close()
 | ||
| 		stderrPipeR.Close()
 | ||
| 	}()
 | ||
| 	// 缓冲区
 | ||
| 	var stdoutBuf, stderrBuf bytes.Buffer
 | ||
| 	// 并发读取 stdout 和 stderr
 | ||
| 	done := make(chan struct{})
 | ||
| 	go func() {
 | ||
| 		multiWriter := io.MultiWriter(&stdoutBuf, oldStdout)
 | ||
| 		io.Copy(multiWriter, stdoutPipeR)
 | ||
| 		done <- struct{}{}
 | ||
| 	}()
 | ||
| 	go func() {
 | ||
| 		multiWriter := io.MultiWriter(&stderrBuf, oldStderr)
 | ||
| 		io.Copy(multiWriter, stderrPipeR)
 | ||
| 		done <- struct{}{}
 | ||
| 	}()
 | ||
| 	// 执行函数
 | ||
| 	f()
 | ||
| 	// 关闭管道写入端,让管道读取端可以读取所有数据
 | ||
| 	stdoutPipeW.Close()
 | ||
| 	stderrPipeW.Close()
 | ||
| 	// 等待两个 goroutine 完成
 | ||
| 	<-done
 | ||
| 	<-done
 | ||
| 	// 返回捕获的输出字符串
 | ||
| 	// stderrBuf.String()
 | ||
| 	return stdoutBuf.String()
 | ||
| }
 | ||
| 
 | ||
| // PrintAndCapture 捕获函数输出的同时打印内容
 | ||
| func PrintAndCapture(f func(), tempOutput, output string) string {
 | ||
| 	tempOutput = CaptureOutput(f)
 | ||
| 	output += tempOutput
 | ||
| 	return output
 | ||
| }
 | ||
| 
 | ||
| // UploadText 上传文本内容到指定URL
 | ||
| func UploadText(absPath string) (string, string, error) {
 | ||
| 	primaryURL := "http://hpaste.spiritlhl.net/api/UL/upload"
 | ||
| 	backupURL := "https://paste.spiritlhl.net/api/UL/upload"
 | ||
| 	token := network.SecurityUploadToken
 | ||
| 	client := req.C().SetTimeout(6 * time.Second)
 | ||
| 	client.R().
 | ||
| 		SetRetryCount(2).
 | ||
| 		SetRetryBackoffInterval(1*time.Second, 5*time.Second).
 | ||
| 		SetRetryFixedInterval(2 * time.Second)
 | ||
| 	// 打开文件
 | ||
| 	file, err := os.Open(absPath)
 | ||
| 	if err != nil {
 | ||
| 		return "", "", fmt.Errorf("failed to open file: %w", err)
 | ||
| 	}
 | ||
| 	defer file.Close()
 | ||
| 	// 获取文件信息并检查大小
 | ||
| 	fileInfo, err := file.Stat()
 | ||
| 	if err != nil {
 | ||
| 		return "", "", fmt.Errorf("failed to get file info: %w", err)
 | ||
| 	}
 | ||
| 	if fileInfo.Size() > 25*1024 { // 25KB
 | ||
| 		return "", "", fmt.Errorf("file size exceeds 25KB limit")
 | ||
| 	}
 | ||
| 	// 上传逻辑
 | ||
| 	upload := func(url string) (string, string, error) {
 | ||
| 		file, err := os.Open(absPath)
 | ||
| 		if err != nil {
 | ||
| 			return "", "", fmt.Errorf("failed to re-open file for %s: %w", url, err)
 | ||
| 		}
 | ||
| 		defer file.Close()
 | ||
| 		content, err := io.ReadAll(file)
 | ||
| 		if err != nil {
 | ||
| 			return "", "", fmt.Errorf("failed to read file content for %s: %w", url, err)
 | ||
| 		}
 | ||
| 		resp, err := client.R().
 | ||
| 			SetHeader("Authorization", token).
 | ||
| 			SetFileBytes("file", filepath.Base(absPath), content).
 | ||
| 			Post(url)
 | ||
| 		if err != nil {
 | ||
| 			return "", "", fmt.Errorf("failed to make request to %s: %w", url, err)
 | ||
| 		}
 | ||
| 		if resp.StatusCode >= 200 && resp.StatusCode <= 299 && resp.String() != "" {
 | ||
| 			fileID := strings.TrimSpace(resp.String())
 | ||
| 			if strings.Contains(fileID, "show") {
 | ||
| 				fileID = fileID[strings.LastIndex(fileID, "/")+1:]
 | ||
| 			}
 | ||
| 			httpURL := fmt.Sprintf("http://hpaste.spiritlhl.net/#/show/%s", fileID)
 | ||
| 			httpsURL := fmt.Sprintf("https://paste.spiritlhl.net/#/show/%s", fileID)
 | ||
| 			return httpURL, httpsURL, nil
 | ||
| 		}
 | ||
| 		return "", "", fmt.Errorf("upload failed for %s with status code: %d", url, resp.StatusCode)
 | ||
| 	}
 | ||
| 	// 尝试上传到主URL
 | ||
| 	httpURL, httpsURL, err := upload(primaryURL)
 | ||
| 	if err == nil {
 | ||
| 		return httpURL, httpsURL, nil
 | ||
| 	}
 | ||
| 	// 尝试上传到备份URL
 | ||
| 	httpURL, httpsURL, err = upload(backupURL)
 | ||
| 	if err != nil {
 | ||
| 		return "", "", fmt.Errorf("failed to upload to both primary and backup URLs: %w", err)
 | ||
| 	}
 | ||
| 	return httpURL, httpsURL, nil
 | ||
| }
 | ||
| 
 | ||
| // ProcessAndUpload 创建结果文件并上传文件
 | ||
| func ProcessAndUpload(output string, filePath string, enableUplaod bool) (string, string) {
 | ||
| 	// 使用 defer 来处理 panic
 | ||
| 	defer func() {
 | ||
| 		if r := recover(); r != nil {
 | ||
| 			fmt.Printf("处理上传时发生错误: %v\n", r)
 | ||
| 		}
 | ||
| 	}()
 | ||
| 	// 检查文件是否存在
 | ||
| 	if _, err := os.Stat(filePath); err == nil {
 | ||
| 		// 文件存在,删除文件
 | ||
| 		err = os.Remove(filePath)
 | ||
| 		if err != nil {
 | ||
| 			fmt.Println("无法删除文件:", err)
 | ||
| 			return "", ""
 | ||
| 		}
 | ||
| 	}
 | ||
| 	// 创建文件
 | ||
| 	file, err := os.Create(filePath)
 | ||
| 	if err != nil {
 | ||
| 		fmt.Println("无法创建文件:", err)
 | ||
| 		return "", ""
 | ||
| 	}
 | ||
| 	defer file.Close()
 | ||
| 	// 匹配 ANSI 转义序列
 | ||
| 	ansiRegex := regexp.MustCompile("\x1B\\[[0-9;]+[a-zA-Z]")
 | ||
| 	// 移除 ANSI 转义序列
 | ||
| 	cleanedOutput := ansiRegex.ReplaceAllString(output, "")
 | ||
| 	// 使用 bufio.Writer 提高写入效率
 | ||
| 	writer := bufio.NewWriter(file)
 | ||
| 	_, err = writer.WriteString(cleanedOutput)
 | ||
| 	if err != nil {
 | ||
| 		fmt.Println("无法写入文件:", err)
 | ||
| 		return "", ""
 | ||
| 	}
 | ||
| 	// 确保写入缓冲区的数据都刷新到文件中
 | ||
| 	err = writer.Flush()
 | ||
| 	if err != nil {
 | ||
| 		fmt.Println("无法刷新文件缓冲:", err)
 | ||
| 		return "", ""
 | ||
| 	}
 | ||
| 	fmt.Printf("测试结果已写入 %s\n", filePath)
 | ||
| 	if enableUplaod {
 | ||
| 		// 获取文件的绝对路径
 | ||
| 		absPath, err := filepath.Abs(filePath)
 | ||
| 		if err != nil {
 | ||
| 			fmt.Println("无法获取文件绝对路径:", err)
 | ||
| 			return "", ""
 | ||
| 		}
 | ||
| 		// 上传文件并生成短链接
 | ||
| 		http_url, https_url, err := UploadText(absPath)
 | ||
| 		if err != nil {
 | ||
| 			fmt.Println("上传失败,无法生成链接")
 | ||
| 			fmt.Println(err.Error())
 | ||
| 			return "", ""
 | ||
| 		}
 | ||
| 		return http_url, https_url
 | ||
| 	}
 | ||
| 	return "", ""
 | ||
| }
 | ||
| 
 | ||
| var StackType string
 | ||
| 
 | ||
| type NetCheckResult struct {
 | ||
| 	HasIPv4   bool
 | ||
| 	HasIPv6   bool
 | ||
| 	Connected bool
 | ||
| 	StackType string // "IPv4", "IPv6", "DualStack", "None"
 | ||
| }
 | ||
| 
 | ||
| func makeResolver(proto, dnsAddr string) *net.Resolver {
 | ||
| 	return &net.Resolver{
 | ||
| 		PreferGo: true,
 | ||
| 		Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
 | ||
| 			d := net.Dialer{
 | ||
| 				Timeout: 5 * time.Second,
 | ||
| 			}
 | ||
| 			return d.DialContext(ctx, proto, dnsAddr)
 | ||
| 		},
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // 前置联网能力检测
 | ||
| func CheckPublicAccess(timeout time.Duration) NetCheckResult {
 | ||
| 	if timeout < 2*time.Second {
 | ||
| 		timeout = 2 * time.Second
 | ||
| 	}
 | ||
| 	var wg sync.WaitGroup
 | ||
| 	resultChan := make(chan string, 8)
 | ||
| 	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 | ||
| 	defer cancel()
 | ||
| 	checks := []struct {
 | ||
| 		Tag  string
 | ||
| 		Addr string
 | ||
| 		Kind string // udp4, udp6, http4, http6
 | ||
| 	}{
 | ||
| 		// UDP DNS
 | ||
| 		{"IPv4", "223.5.5.5:53", "udp4"},              // 阿里 DNS
 | ||
| 		{"IPv4", "8.8.8.8:53", "udp4"},                // Google DNS
 | ||
| 		{"IPv6", "[2400:3200::1]:53", "udp6"},         // 阿里 IPv6 DNS
 | ||
| 		{"IPv6", "[2001:4860:4860::8888]:53", "udp6"}, // Google IPv6 DNS
 | ||
| 		// HTTP HEAD
 | ||
| 		{"IPv4", "https://www.baidu.com", "http4"},     // 百度
 | ||
| 		{"IPv4", "https://1.1.1.1", "http4"},           // Cloudflare
 | ||
| 		{"IPv6", "https://[2400:3200::1]", "http6"},    // 阿里 IPv6
 | ||
| 		{"IPv6", "https://[2606:4700::1111]", "http6"}, // Cloudflare IPv6
 | ||
| 	}
 | ||
| 	for _, check := range checks {
 | ||
| 		wg.Add(1)
 | ||
| 		go func(tag, addr, kind string) {
 | ||
| 			defer wg.Done()
 | ||
| 			defer func() {
 | ||
| 				if r := recover(); r != nil {
 | ||
| 				}
 | ||
| 			}()
 | ||
| 			switch kind {
 | ||
| 			case "udp4", "udp6":
 | ||
| 				dialer := &net.Dialer{
 | ||
| 					Timeout: timeout / 4,
 | ||
| 				}
 | ||
| 				conn, err := dialer.DialContext(ctx, kind, addr)
 | ||
| 				if err == nil && conn != nil {
 | ||
| 					conn.Close()
 | ||
| 					select {
 | ||
| 					case resultChan <- tag:
 | ||
| 					case <-ctx.Done():
 | ||
| 						return
 | ||
| 					}
 | ||
| 				}
 | ||
| 			case "http4", "http6":
 | ||
| 				var resolver *net.Resolver
 | ||
| 				if kind == "http4" {
 | ||
| 					resolver = makeResolver("udp4", "223.5.5.5:53")
 | ||
| 				} else {
 | ||
| 					resolver = makeResolver("udp6", "[2400:3200::1]:53")
 | ||
| 				}
 | ||
| 				dialer := &net.Dialer{
 | ||
| 					Timeout:  timeout / 4,
 | ||
| 					Resolver: resolver,
 | ||
| 				}
 | ||
| 				transport := &http.Transport{
 | ||
| 					DialContext:           dialer.DialContext,
 | ||
| 					MaxIdleConns:          1,
 | ||
| 					MaxIdleConnsPerHost:   1,
 | ||
| 					IdleConnTimeout:       time.Second,
 | ||
| 					TLSHandshakeTimeout:   timeout / 4,
 | ||
| 					ResponseHeaderTimeout: timeout / 4,
 | ||
| 					DisableKeepAlives:     true,
 | ||
| 				}
 | ||
| 				client := &http.Client{
 | ||
| 					Timeout:   timeout / 4,
 | ||
| 					Transport: transport,
 | ||
| 					CheckRedirect: func(req *http.Request, via []*http.Request) error {
 | ||
| 						return http.ErrUseLastResponse
 | ||
| 					},
 | ||
| 				}
 | ||
| 				req, err := http.NewRequestWithContext(ctx, "HEAD", addr, nil)
 | ||
| 				if err != nil {
 | ||
| 					return
 | ||
| 				}
 | ||
| 				resp, err := client.Do(req)
 | ||
| 				if err == nil && resp != nil {
 | ||
| 					if resp.Body != nil {
 | ||
| 						resp.Body.Close()
 | ||
| 					}
 | ||
| 					if resp.StatusCode < 500 {
 | ||
| 						select {
 | ||
| 						case resultChan <- tag:
 | ||
| 						case <-ctx.Done():
 | ||
| 							return
 | ||
| 						}
 | ||
| 					}
 | ||
| 				}
 | ||
| 			}
 | ||
| 		}(check.Tag, check.Addr, check.Kind)
 | ||
| 	}
 | ||
| 	go func() {
 | ||
| 		wg.Wait()
 | ||
| 		close(resultChan)
 | ||
| 	}()
 | ||
| 	hasV4 := false
 | ||
| 	hasV6 := false
 | ||
| 	for {
 | ||
| 		select {
 | ||
| 		case res, ok := <-resultChan:
 | ||
| 			if !ok {
 | ||
| 				goto result
 | ||
| 			}
 | ||
| 			if res == "IPv4" {
 | ||
| 				hasV4 = true
 | ||
| 			}
 | ||
| 			if res == "IPv6" {
 | ||
| 				hasV6 = true
 | ||
| 			}
 | ||
| 		case <-ctx.Done():
 | ||
| 			goto result
 | ||
| 		}
 | ||
| 	}
 | ||
| result:
 | ||
| 	stack := "None"
 | ||
| 	if hasV4 && hasV6 {
 | ||
| 		stack = "DualStack"
 | ||
| 	} else if hasV4 {
 | ||
| 		stack = "IPv4"
 | ||
| 	} else if hasV6 {
 | ||
| 		stack = "IPv6"
 | ||
| 	}
 | ||
| 	StackType = stack
 | ||
| 	butils.CheckPublicAccess(3 * time.Second) // 设置basics检测,避免部分测试未启用
 | ||
| 	return NetCheckResult{
 | ||
| 		HasIPv4:   hasV4,
 | ||
| 		HasIPv6:   hasV6,
 | ||
| 		Connected: hasV4 || hasV6,
 | ||
| 		StackType: stack,
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // 获取每日/总的程序执行统计信息
 | ||
| func GetGoescStats() (*StatsResponse, error) {
 | ||
| 	client := req.C().SetTimeout(5 * time.Second)
 | ||
| 	var stats StatsResponse
 | ||
| 	resp, err := client.R().
 | ||
| 		SetSuccessResult(&stats).
 | ||
| 		Get("https://hits.spiritlhl.net/goecs")
 | ||
| 	if err != nil {
 | ||
| 		return nil, err
 | ||
| 	}
 | ||
| 	if !resp.IsSuccessState() {
 | ||
| 		return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
 | ||
| 	}
 | ||
| 	return &stats, nil
 | ||
| }
 | ||
| 
 | ||
| // 统计结果单位转换
 | ||
| func FormatGoecsNumber(num int) string {
 | ||
| 	if num >= 1000000 {
 | ||
| 		return fmt.Sprintf("%.1fM", float64(num)/1000000)
 | ||
| 	} else if num >= 1000 {
 | ||
| 		return fmt.Sprintf("%.1fK", float64(num)/1000)
 | ||
| 	}
 | ||
| 	return fmt.Sprintf("%d", num)
 | ||
| }
 | ||
| 
 | ||
| // 通过Github的API检索仓库最新TAG的版本
 | ||
| func GetLatestEcsRelease() (*GitHubRelease, error) {
 | ||
| 	urls := []string{
 | ||
| 		"https://api.github.com/repos/oneclickvirt/ecs/releases/latest",
 | ||
| 		"https://fd.spiritlhl.top/https://api.github.com/repos/oneclickvirt/ecs/releases/latest",
 | ||
| 		"https://githubapi.spiritlhl.top/repos/oneclickvirt/ecs/releases/latest",
 | ||
| 		"https://githubapi.spiritlhl.workers.dev/repos/oneclickvirt/ecs/releases/latest",
 | ||
| 	}
 | ||
| 	client := req.C().SetTimeout(3 * time.Second)
 | ||
| 	for _, url := range urls {
 | ||
| 		var release GitHubRelease
 | ||
| 		resp, err := client.R().
 | ||
| 			SetSuccessResult(&release).
 | ||
| 			Get(url)
 | ||
| 		if err != nil {
 | ||
| 			continue
 | ||
| 		}
 | ||
| 		if resp.IsSuccessState() && release.TagName != "" {
 | ||
| 			return &release, nil
 | ||
| 		}
 | ||
| 	}
 | ||
| 	return nil, fmt.Errorf("failed to fetch release from all sources")
 | ||
| }
 | ||
| 
 | ||
| // 比较程序版本是否需要升级
 | ||
| func CompareVersions(v1, v2 string) int {
 | ||
| 	normalize := func(s string) []int {
 | ||
| 		s = strings.TrimPrefix(strings.ToLower(s), "v")
 | ||
| 		parts := strings.Split(s, ".")
 | ||
| 		result := make([]int, 3)
 | ||
| 		for i := 0; i < 3 && i < len(parts); i++ {
 | ||
| 			n, _ := strconv.Atoi(parts[i])
 | ||
| 			result[i] = n
 | ||
| 		}
 | ||
| 		return result
 | ||
| 	}
 | ||
| 	a := normalize(v1)
 | ||
| 	b := normalize(v2)
 | ||
| 	for i := 0; i < 3; i++ {
 | ||
| 		if a[i] < b[i] {
 | ||
| 			return -1
 | ||
| 		} else if a[i] > b[i] {
 | ||
| 			return 1
 | ||
| 		}
 | ||
| 	}
 | ||
| 	return 0
 | ||
| }
 | 
