完1
This commit is contained in:
@@ -1,45 +1,142 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"net"
|
"net"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleDns(c *gin.Context, url string, params map[string]interface{}) {
|
func handleDns(c *gin.Context, url string, params map[string]interface{}) {
|
||||||
// 执行DNS查询
|
// 获取seq参数
|
||||||
start := time.Now()
|
seq := ""
|
||||||
ips, err := net.LookupIP(url)
|
if seqVal, ok := params["seq"].(string); ok {
|
||||||
lookupTime := time.Since(start).Milliseconds()
|
seq = seqVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取dig类型参数
|
||||||
|
digType := ""
|
||||||
|
if dt, ok := params["dt"].(string); ok {
|
||||||
|
digType = dt
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取DNS服务器参数
|
||||||
|
dnsServer := ""
|
||||||
|
if ds, ok := params["ds"].(string); ok {
|
||||||
|
dnsServer = ds
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析URL,提取hostname
|
||||||
|
hostname := url
|
||||||
|
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
||||||
|
parts := strings.Split(url, "//")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
hostParts := strings.Split(parts[1], "/")
|
||||||
|
hostname = hostParts[0]
|
||||||
|
if idx := strings.Index(hostname, ":"); idx != -1 {
|
||||||
|
hostname = hostname[:idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备结果
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"seq": seq,
|
||||||
|
"type": "ceDns",
|
||||||
|
"requrl": hostname,
|
||||||
|
"ips": []interface{}{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建dig命令
|
||||||
|
args := []string{hostname}
|
||||||
|
if digType != "" {
|
||||||
|
args = append([]string{"-t", digType}, args...)
|
||||||
|
}
|
||||||
|
if dnsServer != "" {
|
||||||
|
args = append([]string{"@" + dnsServer}, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("dig", args...)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
outputStr := string(output)
|
||||||
|
|
||||||
|
// 编码完整输出为base64(header字段)
|
||||||
|
result["header"] = base64.StdEncoding.EncodeToString([]byte(outputStr))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(200, gin.H{
|
result["error"] = err.Error()
|
||||||
"type": "ceDns",
|
c.JSON(200, result)
|
||||||
"url": url,
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化IP列表
|
// 解析dig输出
|
||||||
|
lines := strings.Split(outputStr, "\n")
|
||||||
|
inAnswerSection := false
|
||||||
ipList := make([]map[string]interface{}, 0)
|
ipList := make([]map[string]interface{}, 0)
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.Contains(line, "ANSWER SECTION") {
|
||||||
|
inAnswerSection = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inAnswerSection {
|
||||||
|
if line == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// 解析dig输出行,格式如:example.com. 300 IN A 192.168.1.1
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) >= 5 {
|
||||||
|
recordType := parts[3] // IN
|
||||||
|
recordClass := parts[4] // A, AAAA, CNAME等
|
||||||
|
recordValue := ""
|
||||||
|
if len(parts) > 5 {
|
||||||
|
recordValue = strings.Join(parts[5:], " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理A和AAAA记录
|
||||||
|
if recordClass == "A" || recordClass == "AAAA" {
|
||||||
|
ipItem := map[string]interface{}{
|
||||||
|
"url": parts[0],
|
||||||
|
"type": recordClass,
|
||||||
|
"ip": recordValue,
|
||||||
|
}
|
||||||
|
ipList = append(ipList, ipItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有从dig输出解析到IP,尝试使用net.LookupIP
|
||||||
|
if len(ipList) == 0 {
|
||||||
|
start := time.Now()
|
||||||
|
ips, err := net.LookupIP(hostname)
|
||||||
|
lookupTime := time.Since(start)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
ipType := "A"
|
ipType := "A"
|
||||||
if ip.To4() == nil {
|
if ip.To4() == nil {
|
||||||
ipType = "AAAA"
|
ipType = "AAAA"
|
||||||
}
|
}
|
||||||
ipList = append(ipList, map[string]interface{}{
|
ipItem := map[string]interface{}{
|
||||||
|
"url": hostname,
|
||||||
"type": ipType,
|
"type": ipType,
|
||||||
"ip": ip.String(),
|
"ip": ip.String(),
|
||||||
})
|
}
|
||||||
|
ipList = append(ipList, ipItem)
|
||||||
|
}
|
||||||
|
// 更新header,包含lookup时间信息
|
||||||
|
lookupInfo := fmt.Sprintf("Lookup time: %v\n", lookupTime)
|
||||||
|
result["header"] = base64.StdEncoding.EncodeToString([]byte(outputStr + lookupInfo))
|
||||||
|
} else {
|
||||||
|
result["error"] = err.Error()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
result["ips"] = ipList
|
||||||
"type": "ceDns",
|
c.JSON(200, result)
|
||||||
"url": url,
|
|
||||||
"ips": ipList,
|
|
||||||
"lookup_time": lookupTime,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,496 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleGet(c *gin.Context, url string, params map[string]interface{}) {
|
// timingTransport 用于跟踪HTTP请求的各个阶段时间
|
||||||
// TODO: 实现HTTP GET测试
|
type timingTransport struct {
|
||||||
// 这里先返回一个简单的响应
|
transport http.RoundTripper
|
||||||
c.JSON(http.StatusOK, gin.H{
|
nameLookup time.Duration
|
||||||
|
connect time.Duration
|
||||||
|
startTransfer time.Duration
|
||||||
|
total time.Duration
|
||||||
|
primaryIP string
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTimingTransport() *timingTransport {
|
||||||
|
return &timingTransport{
|
||||||
|
transport: &http.Transport{
|
||||||
|
DisableKeepAlives: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
start := time.Now()
|
||||||
|
host := req.URL.Hostname()
|
||||||
|
port := req.URL.Port()
|
||||||
|
if port == "" {
|
||||||
|
if req.URL.Scheme == "https" {
|
||||||
|
port = "443"
|
||||||
|
} else {
|
||||||
|
port = "80"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS查询时间
|
||||||
|
dnsStart := time.Now()
|
||||||
|
ips, err := net.LookupIP(host)
|
||||||
|
dnsTime := time.Since(dnsStart)
|
||||||
|
|
||||||
|
t.mu.Lock()
|
||||||
|
t.nameLookup = dnsTime
|
||||||
|
if len(ips) > 0 {
|
||||||
|
// 优先使用IPv4
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.To4() != nil {
|
||||||
|
t.primaryIP = ip.String()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.primaryIP == "" && len(ips) > 0 {
|
||||||
|
t.primaryIP = ips[0].String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.mu.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TCP连接时间(如果已知IP)
|
||||||
|
var connectTime time.Duration
|
||||||
|
if t.primaryIP != "" {
|
||||||
|
connectStart := time.Now()
|
||||||
|
conn, err := net.DialTimeout("tcp", net.JoinHostPort(t.primaryIP, port), 5*time.Second)
|
||||||
|
connectTime = time.Since(connectStart)
|
||||||
|
if err == nil {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行HTTP请求
|
||||||
|
httpStart := time.Now()
|
||||||
|
resp, err := t.transport.RoundTrip(req)
|
||||||
|
httpTime := time.Since(httpStart)
|
||||||
|
totalTime := time.Since(start)
|
||||||
|
|
||||||
|
t.mu.Lock()
|
||||||
|
if connectTime > 0 {
|
||||||
|
t.connect = connectTime
|
||||||
|
} else {
|
||||||
|
// 如果没有单独测量连接时间,使用HTTP请求时间的一部分
|
||||||
|
t.connect = httpTime / 3
|
||||||
|
}
|
||||||
|
t.total = totalTime
|
||||||
|
if resp != nil {
|
||||||
|
// 首字节时间 = DNS + 连接 + HTTP请求开始到响应头的时间
|
||||||
|
t.startTransfer = dnsTime + connectTime + (httpTime / 2)
|
||||||
|
if t.startTransfer > totalTime {
|
||||||
|
t.startTransfer = totalTime * 2 / 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.mu.Unlock()
|
||||||
|
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleGet(c *gin.Context, urlStr string, params map[string]interface{}) {
|
||||||
|
// 获取seq参数
|
||||||
|
seq := ""
|
||||||
|
if seqVal, ok := params["seq"].(string); ok {
|
||||||
|
seq = seqVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析URL
|
||||||
|
if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") {
|
||||||
|
urlStr = "http://" + urlStr
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"seq": seq,
|
||||||
"type": "ceGet",
|
"type": "ceGet",
|
||||||
"url": url,
|
"url": urlStr,
|
||||||
"statuscode": 200,
|
"error": "URL格式错误",
|
||||||
"totaltime": time.Since(time.Now()).Milliseconds(),
|
|
||||||
"response": "OK",
|
|
||||||
})
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备结果
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"seq": seq,
|
||||||
|
"type": "ceGet",
|
||||||
|
"url": urlStr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建自定义Transport用于时间跟踪
|
||||||
|
timingTransport := newTimingTransport()
|
||||||
|
|
||||||
|
// 创建HTTP客户端
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: timingTransport,
|
||||||
|
Timeout: 15 * time.Second,
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
// 跟随重定向,最多20次
|
||||||
|
if len(via) >= 20 {
|
||||||
|
return fmt.Errorf("重定向次数过多")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建请求
|
||||||
|
req, err := http.NewRequest("GET", urlStr, nil)
|
||||||
|
if err != nil {
|
||||||
|
result["error"] = err.Error()
|
||||||
|
result["ip"] = "访问失败"
|
||||||
|
result["totaltime"] = "*"
|
||||||
|
result["downtime"] = "*"
|
||||||
|
result["downsize"] = "*"
|
||||||
|
result["downspeed"] = "*"
|
||||||
|
result["firstbytetime"] = "*"
|
||||||
|
result["conntime"] = "*"
|
||||||
|
result["size"] = "*"
|
||||||
|
c.JSON(200, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置User-Agent
|
||||||
|
userAgents := []string{
|
||||||
|
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38",
|
||||||
|
"Mozilla/5.0 (Linux; Android 7.0; SM-G892A Build/NRD90M; wv) AppleWebKit/537.36",
|
||||||
|
"Mozilla/5.0 (Linux; Android 8.1; EML-L29 Build/HUAWEIEML-L29) AppleWebKit/537.36",
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", userAgents[0])
|
||||||
|
req.Header.Set("Accept-Encoding", "gzip")
|
||||||
|
|
||||||
|
// 执行请求
|
||||||
|
startTime := time.Now()
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
// 错误处理
|
||||||
|
errMsg := err.Error()
|
||||||
|
if strings.Contains(errMsg, "no such host") {
|
||||||
|
result["ip"] = "域名无法解析"
|
||||||
|
} else if strings.Contains(errMsg, "connection refused") || strings.Contains(errMsg, "timeout") {
|
||||||
|
result["ip"] = "无法连接"
|
||||||
|
} else if strings.Contains(errMsg, "deadline exceeded") || strings.Contains(errMsg, "timeout") {
|
||||||
|
result["ip"] = "访问超时"
|
||||||
|
} else {
|
||||||
|
result["ip"] = "访问失败"
|
||||||
|
}
|
||||||
|
result["error"] = errMsg
|
||||||
|
result["totaltime"] = "*"
|
||||||
|
result["downtime"] = "*"
|
||||||
|
result["downsize"] = "*"
|
||||||
|
result["downspeed"] = "*"
|
||||||
|
result["firstbytetime"] = "*"
|
||||||
|
result["conntime"] = "*"
|
||||||
|
result["size"] = "*"
|
||||||
|
c.JSON(200, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 获取时间信息
|
||||||
|
timingTransport.mu.Lock()
|
||||||
|
nameLookupTime := timingTransport.nameLookup
|
||||||
|
connectTime := timingTransport.connect
|
||||||
|
firstByteTime := timingTransport.startTransfer
|
||||||
|
totalTime := timingTransport.total
|
||||||
|
primaryIP := timingTransport.primaryIP
|
||||||
|
timingTransport.mu.Unlock()
|
||||||
|
|
||||||
|
// 如果primaryIP为空,尝试从URL获取
|
||||||
|
if primaryIP == "" {
|
||||||
|
host := parsedURL.Hostname()
|
||||||
|
if net.ParseIP(host) != nil {
|
||||||
|
primaryIP = host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建header字符串(base64编码)
|
||||||
|
headerBuilder := strings.Builder{}
|
||||||
|
headerBuilder.WriteString(fmt.Sprintf("%s %s\r\n", resp.Proto, resp.Status))
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
headerBuilder.WriteString(fmt.Sprintf("%s: %s\r\n", k, strings.Join(v, ", ")))
|
||||||
|
}
|
||||||
|
headerBuilder.WriteString("\r\n")
|
||||||
|
headerBytes := []byte(headerBuilder.String())
|
||||||
|
result["header"] = base64.StdEncoding.EncodeToString(headerBytes)
|
||||||
|
|
||||||
|
// 读取响应体(限制大小)
|
||||||
|
bodyReader := io.LimitReader(resp.Body, 1024*1024) // 限制1MB
|
||||||
|
bodyStartTime := time.Now()
|
||||||
|
body, err := io.ReadAll(bodyReader)
|
||||||
|
bodyReadTime := time.Now().Sub(bodyStartTime)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
result["error"] = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSize := int64(len(body))
|
||||||
|
statusCode := resp.StatusCode
|
||||||
|
|
||||||
|
// 如果首字节时间为0,使用连接时间
|
||||||
|
if firstByteTime == 0 {
|
||||||
|
firstByteTime = connectTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// 总时间 = 实际请求时间
|
||||||
|
if totalTime == 0 {
|
||||||
|
totalTime = time.Since(startTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算下载时间(使用实际读取时间)
|
||||||
|
downloadTime := bodyReadTime
|
||||||
|
if downloadTime <= 0 {
|
||||||
|
downloadTime = totalTime - firstByteTime
|
||||||
|
if downloadTime < 0 {
|
||||||
|
downloadTime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算下载速度(字节/秒)
|
||||||
|
var downloadSpeed float64
|
||||||
|
if downloadTime > 0 {
|
||||||
|
downloadSpeed = float64(downloadSize) / downloadTime.Seconds()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
sizeStr := formatSize(downloadSize)
|
||||||
|
downSizeStr := formatSizeKB(downloadSize)
|
||||||
|
|
||||||
|
// 填充结果
|
||||||
|
result["ip"] = primaryIP
|
||||||
|
result["statuscode"] = statusCode
|
||||||
|
result["nslookuptime"] = roundFloat(nameLookupTime.Seconds(), 3)
|
||||||
|
result["conntime"] = roundFloat(connectTime.Seconds(), 3)
|
||||||
|
result["firstbytetime"] = roundFloat(firstByteTime.Seconds(), 3)
|
||||||
|
result["totaltime"] = roundFloat(totalTime.Seconds(), 3)
|
||||||
|
result["downtime"] = roundFloat(downloadTime.Seconds(), 6)
|
||||||
|
result["downsize"] = downSizeStr
|
||||||
|
result["downspeed"] = downloadSpeed
|
||||||
|
result["size"] = sizeStr
|
||||||
|
|
||||||
|
c.JSON(200, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePost(c *gin.Context, url string, params map[string]interface{}) {
|
func handlePost(c *gin.Context, urlStr string, params map[string]interface{}) {
|
||||||
// TODO: 实现HTTP POST测试
|
// 获取seq参数
|
||||||
c.JSON(http.StatusOK, gin.H{
|
seq := ""
|
||||||
|
if seqVal, ok := params["seq"].(string); ok {
|
||||||
|
seq = seqVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析URL
|
||||||
|
if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") {
|
||||||
|
urlStr = "http://" + urlStr
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(urlStr)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(200, gin.H{
|
||||||
|
"seq": seq,
|
||||||
"type": "cePost",
|
"type": "cePost",
|
||||||
"url": url,
|
"url": urlStr,
|
||||||
"statuscode": 200,
|
"error": "URL格式错误",
|
||||||
"totaltime": time.Since(time.Now()).Milliseconds(),
|
|
||||||
"response": "OK",
|
|
||||||
})
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 准备结果
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"seq": seq,
|
||||||
|
"type": "cePost",
|
||||||
|
"url": urlStr,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取POST数据
|
||||||
|
postData := "abc=123"
|
||||||
|
if data, ok := params["data"].(string); ok && data != "" {
|
||||||
|
postData = data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建自定义Transport用于时间跟踪
|
||||||
|
timingTransport := newTimingTransport()
|
||||||
|
|
||||||
|
// 创建HTTP客户端
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: timingTransport,
|
||||||
|
Timeout: 15 * time.Second,
|
||||||
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||||
|
if len(via) >= 20 {
|
||||||
|
return fmt.Errorf("重定向次数过多")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建POST请求
|
||||||
|
req, err := http.NewRequest("POST", urlStr, strings.NewReader(postData))
|
||||||
|
if err != nil {
|
||||||
|
result["error"] = err.Error()
|
||||||
|
result["ip"] = "访问失败"
|
||||||
|
result["totaltime"] = "*"
|
||||||
|
result["downtime"] = "*"
|
||||||
|
result["downsize"] = "*"
|
||||||
|
result["downspeed"] = "*"
|
||||||
|
result["firstbytetime"] = "*"
|
||||||
|
result["conntime"] = "*"
|
||||||
|
result["size"] = "*"
|
||||||
|
c.JSON(200, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38")
|
||||||
|
req.Header.Set("Accept-Encoding", "gzip")
|
||||||
|
|
||||||
|
// 执行请求
|
||||||
|
startTime := time.Now()
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
errMsg := err.Error()
|
||||||
|
if strings.Contains(errMsg, "no such host") {
|
||||||
|
result["ip"] = "域名无法解析"
|
||||||
|
} else if strings.Contains(errMsg, "connection refused") || strings.Contains(errMsg, "timeout") {
|
||||||
|
result["ip"] = "无法连接"
|
||||||
|
} else if strings.Contains(errMsg, "deadline exceeded") || strings.Contains(errMsg, "timeout") {
|
||||||
|
result["ip"] = "访问超时"
|
||||||
|
} else {
|
||||||
|
result["ip"] = "访问失败"
|
||||||
|
}
|
||||||
|
result["error"] = errMsg
|
||||||
|
result["totaltime"] = "*"
|
||||||
|
result["downtime"] = "*"
|
||||||
|
result["downsize"] = "*"
|
||||||
|
result["downspeed"] = "*"
|
||||||
|
result["firstbytetime"] = "*"
|
||||||
|
result["conntime"] = "*"
|
||||||
|
result["size"] = "*"
|
||||||
|
c.JSON(200, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 获取时间信息
|
||||||
|
timingTransport.mu.Lock()
|
||||||
|
nameLookupTime := timingTransport.nameLookup
|
||||||
|
connectTime := timingTransport.connect
|
||||||
|
firstByteTime := timingTransport.startTransfer
|
||||||
|
totalTime := timingTransport.total
|
||||||
|
primaryIP := timingTransport.primaryIP
|
||||||
|
timingTransport.mu.Unlock()
|
||||||
|
|
||||||
|
// 如果primaryIP为空,尝试从URL获取
|
||||||
|
if primaryIP == "" {
|
||||||
|
host := parsedURL.Hostname()
|
||||||
|
if net.ParseIP(host) != nil {
|
||||||
|
primaryIP = host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建header字符串(base64编码)
|
||||||
|
headerBuilder := strings.Builder{}
|
||||||
|
headerBuilder.WriteString(fmt.Sprintf("%s %s\r\n", resp.Proto, resp.Status))
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
headerBuilder.WriteString(fmt.Sprintf("%s: %s\r\n", k, strings.Join(v, ", ")))
|
||||||
|
}
|
||||||
|
headerBuilder.WriteString("\r\n")
|
||||||
|
headerBytes := []byte(headerBuilder.String())
|
||||||
|
result["header"] = base64.StdEncoding.EncodeToString(headerBytes)
|
||||||
|
|
||||||
|
// 读取响应体(限制大小)
|
||||||
|
bodyReader := io.LimitReader(resp.Body, 1024*1024)
|
||||||
|
bodyStartTime := time.Now()
|
||||||
|
body, err := io.ReadAll(bodyReader)
|
||||||
|
bodyReadTime := time.Since(bodyStartTime)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
result["error"] = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSize := int64(len(body))
|
||||||
|
statusCode := resp.StatusCode
|
||||||
|
|
||||||
|
// 如果首字节时间为0,使用连接时间
|
||||||
|
if firstByteTime == 0 {
|
||||||
|
firstByteTime = connectTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// 总时间 = 实际请求时间
|
||||||
|
if totalTime == 0 {
|
||||||
|
totalTime = time.Since(startTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算下载时间(使用实际读取时间)
|
||||||
|
downloadTime := bodyReadTime
|
||||||
|
if downloadTime <= 0 {
|
||||||
|
downloadTime = totalTime - firstByteTime
|
||||||
|
if downloadTime < 0 {
|
||||||
|
downloadTime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadSpeed float64
|
||||||
|
if downloadTime > 0 {
|
||||||
|
downloadSpeed = float64(downloadSize) / downloadTime.Seconds()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
sizeStr := formatSize(downloadSize)
|
||||||
|
downSizeStr := formatSizeKB(downloadSize)
|
||||||
|
|
||||||
|
// 填充结果
|
||||||
|
result["ip"] = primaryIP
|
||||||
|
result["statuscode"] = statusCode
|
||||||
|
result["nslookuptime"] = roundFloat(nameLookupTime.Seconds(), 3)
|
||||||
|
result["conntime"] = roundFloat(connectTime.Seconds(), 3)
|
||||||
|
result["firstbytetime"] = roundFloat(firstByteTime.Seconds(), 3)
|
||||||
|
result["totaltime"] = roundFloat(totalTime.Seconds(), 3)
|
||||||
|
result["downtime"] = roundFloat(downloadTime.Seconds(), 6)
|
||||||
|
result["downsize"] = downSizeStr
|
||||||
|
result["downspeed"] = downloadSpeed
|
||||||
|
result["size"] = sizeStr
|
||||||
|
|
||||||
|
c.JSON(200, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 辅助函数
|
||||||
|
func roundFloat(val float64, precision int) float64 {
|
||||||
|
multiplier := 1.0
|
||||||
|
for i := 0; i < precision; i++ {
|
||||||
|
multiplier *= 10
|
||||||
|
}
|
||||||
|
return float64(int(val*multiplier+0.5)) / multiplier
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSize(bytes int64) string {
|
||||||
|
if bytes < 1024 {
|
||||||
|
return fmt.Sprintf("%dB", bytes)
|
||||||
|
}
|
||||||
|
kb := float64(bytes) / 1024
|
||||||
|
if kb < 1024 {
|
||||||
|
return fmt.Sprintf("%.3fKB", kb)
|
||||||
|
}
|
||||||
|
mb := kb / 1024
|
||||||
|
return fmt.Sprintf("%.3fMB", mb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatSizeKB(bytes int64) string {
|
||||||
|
kb := float64(bytes) / 1024
|
||||||
|
return fmt.Sprintf("%.3fKB", kb)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"net"
|
"net"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -10,47 +12,114 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func handlePing(c *gin.Context, url string, params map[string]interface{}) {
|
func handlePing(c *gin.Context, url string, params map[string]interface{}) {
|
||||||
// 执行ping命令
|
// 获取seq参数
|
||||||
cmd := exec.Command("ping", "-c", "4", url)
|
seq := ""
|
||||||
output, err := cmd.CombinedOutput()
|
if seqVal, ok := params["seq"].(string); ok {
|
||||||
if err != nil {
|
seq = seqVal
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"type": "cePing",
|
|
||||||
"url": url,
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析ping输出
|
// 解析URL,提取hostname
|
||||||
result := parsePingOutput(string(output), url)
|
hostname := url
|
||||||
c.JSON(200, result)
|
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
||||||
}
|
// 从URL中提取hostname
|
||||||
|
parts := strings.Split(url, "//")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
hostParts := strings.Split(parts[1], "/")
|
||||||
|
hostname = hostParts[0]
|
||||||
|
// 移除端口号
|
||||||
|
if idx := strings.Index(hostname, ":"); idx != -1 {
|
||||||
|
hostname = hostname[:idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func parsePingOutput(output, url string) map[string]interface{} {
|
// 执行ping命令
|
||||||
|
cmd := exec.Command("ping", "-c", "10", "-i", "0.5", hostname)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
outputStr := string(output)
|
||||||
|
|
||||||
|
// 准备结果
|
||||||
result := map[string]interface{}{
|
result := map[string]interface{}{
|
||||||
|
"seq": seq,
|
||||||
"type": "cePing",
|
"type": "cePing",
|
||||||
"url": url,
|
"url": url,
|
||||||
"ip": "",
|
"ip": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析IP地址
|
// 编码完整输出为base64(header字段)
|
||||||
lines := strings.Split(output, "\n")
|
result["header"] = base64.StdEncoding.EncodeToString([]byte(outputStr))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
result["error"] = err.Error()
|
||||||
|
c.JSON(200, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析ping输出
|
||||||
|
lines := strings.Split(outputStr, "\n")
|
||||||
|
|
||||||
|
// 解析IP地址(从PING行)
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if strings.Contains(line, "PING") {
|
if strings.Contains(line, "PING") {
|
||||||
// 提取IP地址
|
// 提取IP地址,格式如:PING example.com (192.168.1.1) 56(84) bytes of data.
|
||||||
|
re := regexp.MustCompile(`\(([0-9.]+)\)`)
|
||||||
|
matches := re.FindStringSubmatch(line)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
result["ip"] = matches[1]
|
||||||
|
} else {
|
||||||
|
// 尝试直接解析IP
|
||||||
parts := strings.Fields(line)
|
parts := strings.Fields(line)
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if net.ParseIP(part) != nil {
|
if ip := net.ParseIP(part); ip != nil {
|
||||||
result["ip"] = part
|
result["ip"] = part
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析包大小(bytes字段)
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "bytes of data") {
|
||||||
|
// 提取bytes,格式如:64 bytes from ...
|
||||||
|
re := regexp.MustCompile(`(\d+)\s+bytes`)
|
||||||
|
matches := re.FindStringSubmatch(line)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
result["bytes"] = matches[1]
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析统计信息
|
||||||
|
for _, line := range lines {
|
||||||
|
// 解析丢包率和包统计
|
||||||
if strings.Contains(line, "packets transmitted") {
|
if strings.Contains(line, "packets transmitted") {
|
||||||
// 解析丢包率
|
// 格式如:10 packets transmitted, 10 received, 0% packet loss
|
||||||
|
re := regexp.MustCompile(`(\d+)\s+packets\s+transmitted[,\s]+(\d+)\s+received[,\s]+(\d+(?:\.\d+)?)%`)
|
||||||
|
matches := re.FindStringSubmatch(line)
|
||||||
|
if len(matches) >= 4 {
|
||||||
|
result["packets_total"] = matches[1]
|
||||||
|
result["packets_recv"] = matches[2]
|
||||||
|
if loss, err := strconv.ParseFloat(matches[3], 64); err == nil {
|
||||||
|
result["packets_losrat"] = loss
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 备用解析方式
|
||||||
parts := strings.Fields(line)
|
parts := strings.Fields(line)
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
|
if part == "packets" && i+1 < len(parts) {
|
||||||
|
if total, err := strconv.Atoi(parts[i-1]); err == nil {
|
||||||
|
result["packets_total"] = strconv.Itoa(total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if part == "received" && i-1 >= 0 {
|
||||||
|
if recv, err := strconv.Atoi(parts[i-1]); err == nil {
|
||||||
|
result["packets_recv"] = strconv.Itoa(recv)
|
||||||
|
}
|
||||||
|
}
|
||||||
if part == "packet" && i+2 < len(parts) {
|
if part == "packet" && i+2 < len(parts) {
|
||||||
if loss, err := strconv.ParseFloat(strings.Trim(parts[i+1], "%"), 64); err == nil {
|
if loss, err := strconv.ParseFloat(strings.Trim(parts[i+1], "%"), 64); err == nil {
|
||||||
result["packets_losrat"] = loss
|
result["packets_losrat"] = loss
|
||||||
@@ -58,8 +127,25 @@ func parsePingOutput(output, url string) map[string]interface{} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if strings.Contains(line, "min/avg/max") {
|
}
|
||||||
// 解析延迟统计
|
|
||||||
|
// 解析时间统计(min/avg/max)
|
||||||
|
if strings.Contains(line, "min/avg/max") || strings.Contains(line, "rtt min/avg/max") {
|
||||||
|
// 格式如:rtt min/avg/max/mdev = 10.123/12.456/15.789/2.345 ms
|
||||||
|
re := regexp.MustCompile(`=\s*([0-9.]+)/([0-9.]+)/([0-9.]+)`)
|
||||||
|
matches := re.FindStringSubmatch(line)
|
||||||
|
if len(matches) >= 4 {
|
||||||
|
if min, err := strconv.ParseFloat(matches[1], 64); err == nil {
|
||||||
|
result["time_min"] = min
|
||||||
|
}
|
||||||
|
if avg, err := strconv.ParseFloat(matches[2], 64); err == nil {
|
||||||
|
result["time_avg"] = avg
|
||||||
|
}
|
||||||
|
if max, err := strconv.ParseFloat(matches[3], 64); err == nil {
|
||||||
|
result["time_max"] = max
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 备用解析方式
|
||||||
parts := strings.Fields(line)
|
parts := strings.Fields(line)
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if strings.Contains(part, "/") {
|
if strings.Contains(part, "/") {
|
||||||
@@ -79,7 +165,7 @@ func parsePingOutput(output, url string) map[string]interface{} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
c.JSON(200, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,22 +10,66 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func handleSocket(c *gin.Context, url string, params map[string]interface{}) {
|
func handleSocket(c *gin.Context, url string, params map[string]interface{}) {
|
||||||
// 解析host:port格式
|
// 获取seq参数
|
||||||
parts := strings.Split(url, ":")
|
seq := ""
|
||||||
if len(parts) != 2 {
|
if seqVal, ok := params["seq"].(string); ok {
|
||||||
c.JSON(200, gin.H{
|
seq = seqVal
|
||||||
"type": "ceSocket",
|
|
||||||
"url": url,
|
|
||||||
"error": "格式错误,需要 host:port",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
host := parts[0]
|
// 解析host:port格式
|
||||||
portStr := parts[1]
|
var host, portStr string
|
||||||
port, err := strconv.Atoi(portStr)
|
var port int
|
||||||
|
|
||||||
|
// 尝试从URL中解析
|
||||||
|
if strings.Contains(url, ":") {
|
||||||
|
parts := strings.Split(url, ":")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
host = parts[0]
|
||||||
|
portStr = parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 尝试从params中获取
|
||||||
|
if hostVal, ok := params["host"].(string); ok {
|
||||||
|
host = hostVal
|
||||||
|
}
|
||||||
|
if portVal, ok := params["port"].(string); ok {
|
||||||
|
portStr = portVal
|
||||||
|
} else if portVal, ok := params["port"].(float64); ok {
|
||||||
|
portStr = strconv.Itoa(int(portVal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果host为空,尝试从URL解析
|
||||||
|
if host == "" {
|
||||||
|
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
||||||
|
parts := strings.Split(url, "//")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
hostParts := strings.Split(parts[1], "/")
|
||||||
|
hostPort := hostParts[0]
|
||||||
|
if idx := strings.Index(hostPort, ":"); idx != -1 {
|
||||||
|
host = hostPort[:idx]
|
||||||
|
portStr = hostPort[idx+1:]
|
||||||
|
} else {
|
||||||
|
host = hostPort
|
||||||
|
if strings.HasPrefix(url, "https://") {
|
||||||
|
portStr = "443"
|
||||||
|
} else {
|
||||||
|
portStr = "80"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
host = url
|
||||||
|
portStr = "80"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析端口
|
||||||
|
var err error
|
||||||
|
port, err = strconv.Atoi(portStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
|
"seq": seq,
|
||||||
"type": "ceSocket",
|
"type": "ceSocket",
|
||||||
"url": url,
|
"url": url,
|
||||||
"error": "端口格式错误",
|
"error": "端口格式错误",
|
||||||
@@ -33,27 +77,56 @@ func handleSocket(c *gin.Context, url string, params map[string]interface{}) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 执行TCP连接测试
|
// 准备结果
|
||||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, portStr), 5*time.Second)
|
result := map[string]interface{}{
|
||||||
if err != nil {
|
"seq": seq,
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"type": "ceSocket",
|
"type": "ceSocket",
|
||||||
"url": url,
|
"url": url,
|
||||||
"host": host,
|
|
||||||
"port": port,
|
"port": port,
|
||||||
|
"ip": "",
|
||||||
"result": "false",
|
"result": "false",
|
||||||
"error": err.Error(),
|
}
|
||||||
})
|
|
||||||
|
// 解析域名或IP
|
||||||
|
var ip string
|
||||||
|
parsedIP := net.ParseIP(host)
|
||||||
|
if parsedIP != nil {
|
||||||
|
ip = host
|
||||||
|
} else {
|
||||||
|
// DNS解析
|
||||||
|
ips, err := net.LookupIP(host)
|
||||||
|
if err != nil {
|
||||||
|
result["ip"] = ""
|
||||||
|
result["result"] = "域名无法解析"
|
||||||
|
c.JSON(200, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(ips) > 0 {
|
||||||
|
ip = ips[0].String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result["ip"] = ip
|
||||||
|
|
||||||
|
// 检查IP是否有效
|
||||||
|
if ip == "" || ip == "0.0.0.0" || ip == "127.0.0.0" {
|
||||||
|
result["result"] = "false"
|
||||||
|
c.JSON(200, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行TCP连接测试
|
||||||
|
conn, err := net.DialTimeout("tcp", net.JoinHostPort(ip, portStr), 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
result["result"] = "false"
|
||||||
|
if err.Error() != "" {
|
||||||
|
result["error"] = err.Error()
|
||||||
|
}
|
||||||
|
c.JSON(200, result)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
result["result"] = "true"
|
||||||
"type": "ceSocket",
|
c.JSON(200, result)
|
||||||
"url": url,
|
|
||||||
"host": host,
|
|
||||||
"port": port,
|
|
||||||
"result": "true",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,31 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func handleTrace(c *gin.Context, url string, params map[string]interface{}) {
|
func handleTrace(c *gin.Context, url string, params map[string]interface{}) {
|
||||||
|
// 获取seq参数
|
||||||
|
seq := ""
|
||||||
|
if seqVal, ok := params["seq"].(string); ok {
|
||||||
|
seq = seqVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析URL,提取hostname
|
||||||
|
hostname := url
|
||||||
|
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
|
||||||
|
parts := strings.Split(url, "//")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
hostParts := strings.Split(parts[1], "/")
|
||||||
|
hostname = hostParts[0]
|
||||||
|
if idx := strings.Index(hostname, ":"); idx != -1 {
|
||||||
|
hostname = hostname[:idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 执行traceroute命令
|
// 执行traceroute命令
|
||||||
cmd := exec.Command("traceroute", url)
|
cmd := exec.Command("traceroute", "-m", "30", "-n", hostname)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
|
"seq": seq,
|
||||||
"type": "ceTrace",
|
"type": "ceTrace",
|
||||||
"url": url,
|
"url": url,
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
@@ -31,6 +51,7 @@ func handleTrace(c *gin.Context, url string, params map[string]interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
|
"seq": seq,
|
||||||
"type": "ceTrace",
|
"type": "ceTrace",
|
||||||
"url": url,
|
"url": url,
|
||||||
"trace_result": traceResult,
|
"trace_result": traceResult,
|
||||||
|
|||||||
Reference in New Issue
Block a user