diff --git a/internal/handler/dns.go b/internal/handler/dns.go index f542956..9d8a85a 100644 --- a/internal/handler/dns.go +++ b/internal/handler/dns.go @@ -1,45 +1,142 @@ package handler import ( + "encoding/base64" "net" + "os/exec" + "strings" "time" "github.com/gin-gonic/gin" ) func handleDns(c *gin.Context, url string, params map[string]interface{}) { - // 执行DNS查询 - start := time.Now() - ips, err := net.LookupIP(url) - lookupTime := time.Since(start).Milliseconds() + // 获取seq参数 + seq := "" + if seqVal, ok := params["seq"].(string); ok { + 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 { - c.JSON(200, gin.H{ - "type": "ceDns", - "url": url, - "error": err.Error(), - }) + result["error"] = err.Error() + c.JSON(200, result) return } - // 格式化IP列表 + // 解析dig输出 + lines := strings.Split(outputStr, "\n") + inAnswerSection := false ipList := make([]map[string]interface{}, 0) - for _, ip := range ips { - ipType := "A" - if ip.To4() == nil { - ipType = "AAAA" + + 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) + } + } } - ipList = append(ipList, map[string]interface{}{ - "type": ipType, - "ip": ip.String(), - }) } - c.JSON(200, gin.H{ - "type": "ceDns", - "url": url, - "ips": ipList, - "lookup_time": lookupTime, - }) -} + // 如果没有从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 { + ipType := "A" + if ip.To4() == nil { + ipType = "AAAA" + } + ipItem := map[string]interface{}{ + "url": hostname, + "type": ipType, + "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() + } + } + + result["ips"] = ipList + c.JSON(200, result) +} diff --git a/internal/handler/get.go b/internal/handler/get.go index 4ca27cb..ace65ea 100644 --- a/internal/handler/get.go +++ b/internal/handler/get.go @@ -1,32 +1,496 @@ package handler import ( + "encoding/base64" + "fmt" + "io" + "net" "net/http" + "net/url" + "strings" + "sync" "time" "github.com/gin-gonic/gin" ) -func handleGet(c *gin.Context, url string, params map[string]interface{}) { - // TODO: 实现HTTP GET测试 - // 这里先返回一个简单的响应 - c.JSON(http.StatusOK, gin.H{ - "type": "ceGet", - "url": url, - "statuscode": 200, - "totaltime": time.Since(time.Now()).Milliseconds(), - "response": "OK", - }) +// timingTransport 用于跟踪HTTP请求的各个阶段时间 +type timingTransport struct { + transport http.RoundTripper + nameLookup time.Duration + connect time.Duration + startTransfer time.Duration + total time.Duration + primaryIP string + mu sync.Mutex } -func handlePost(c *gin.Context, url string, params map[string]interface{}) { - // TODO: 实现HTTP POST测试 - c.JSON(http.StatusOK, gin.H{ - "type": "cePost", - "url": url, - "statuscode": 200, - "totaltime": time.Since(time.Now()).Milliseconds(), - "response": "OK", - }) +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", + "url": urlStr, + "error": "URL格式错误", + }) + 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, 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": "cePost", + "url": urlStr, + "error": "URL格式错误", + }) + 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) +} diff --git a/internal/handler/ping.go b/internal/handler/ping.go index 562600e..8804d58 100644 --- a/internal/handler/ping.go +++ b/internal/handler/ping.go @@ -1,8 +1,10 @@ package handler import ( + "encoding/base64" "net" "os/exec" + "regexp" "strconv" "strings" @@ -10,69 +12,154 @@ import ( ) func handlePing(c *gin.Context, url string, params map[string]interface{}) { - // 执行ping命令 - cmd := exec.Command("ping", "-c", "4", url) - output, err := cmd.CombinedOutput() - if err != nil { - c.JSON(200, gin.H{ - "type": "cePing", - "url": url, - "error": err.Error(), - }) - return + // 获取seq参数 + seq := "" + if seqVal, ok := params["seq"].(string); ok { + seq = seqVal } - // 解析ping输出 - result := parsePingOutput(string(output), url) - c.JSON(200, result) -} + // 解析URL,提取hostname + hostname := url + 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{}{ + "seq": seq, "type": "cePing", "url": url, "ip": "", } - // 解析IP地址 - lines := strings.Split(output, "\n") + // 编码完整输出为base64(header字段) + 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 { if strings.Contains(line, "PING") { - // 提取IP地址 - parts := strings.Fields(line) - for _, part := range parts { - if net.ParseIP(part) != nil { - result["ip"] = part - break + // 提取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) + for _, part := range parts { + if ip := net.ParseIP(part); ip != nil { + result["ip"] = part + 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") { - // 解析丢包率 - parts := strings.Fields(line) - for i, part := range parts { - if part == "packet" && i+2 < len(parts) { - if loss, err := strconv.ParseFloat(strings.Trim(parts[i+1], "%"), 64); err == nil { - result["packets_losrat"] = loss + // 格式如: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) + 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 loss, err := strconv.ParseFloat(strings.Trim(parts[i+1], "%"), 64); err == nil { + result["packets_losrat"] = loss + } } } } } - if strings.Contains(line, "min/avg/max") { - // 解析延迟统计 - parts := strings.Fields(line) - for _, part := range parts { - if strings.Contains(part, "/") { - times := strings.Split(part, "/") - if len(times) >= 3 { - if min, err := strconv.ParseFloat(times[0], 64); err == nil { - result["time_min"] = min - } - if avg, err := strconv.ParseFloat(times[1], 64); err == nil { - result["time_avg"] = avg - } - if max, err := strconv.ParseFloat(times[2], 64); err == nil { - result["time_max"] = 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) + for _, part := range parts { + if strings.Contains(part, "/") { + times := strings.Split(part, "/") + if len(times) >= 3 { + if min, err := strconv.ParseFloat(times[0], 64); err == nil { + result["time_min"] = min + } + if avg, err := strconv.ParseFloat(times[1], 64); err == nil { + result["time_avg"] = avg + } + if max, err := strconv.ParseFloat(times[2], 64); err == nil { + result["time_max"] = max + } } } } @@ -80,6 +167,5 @@ func parsePingOutput(output, url string) map[string]interface{} { } } - return result + c.JSON(200, result) } - diff --git a/internal/handler/socket.go b/internal/handler/socket.go index f9dd909..1e00abf 100644 --- a/internal/handler/socket.go +++ b/internal/handler/socket.go @@ -10,22 +10,66 @@ import ( ) func handleSocket(c *gin.Context, url string, params map[string]interface{}) { - // 解析host:port格式 - parts := strings.Split(url, ":") - if len(parts) != 2 { - c.JSON(200, gin.H{ - "type": "ceSocket", - "url": url, - "error": "格式错误,需要 host:port", - }) - return + // 获取seq参数 + seq := "" + if seqVal, ok := params["seq"].(string); ok { + seq = seqVal } - host := parts[0] - portStr := parts[1] - port, err := strconv.Atoi(portStr) + // 解析host:port格式 + var host, portStr string + 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 { c.JSON(200, gin.H{ + "seq": seq, "type": "ceSocket", "url": url, "error": "端口格式错误", @@ -33,27 +77,56 @@ func handleSocket(c *gin.Context, url string, params map[string]interface{}) { return } + // 准备结果 + result := map[string]interface{}{ + "seq": seq, + "type": "ceSocket", + "url": url, + "port": port, + "ip": "", + "result": "false", + } + + // 解析域名或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(host, portStr), 5*time.Second) + conn, err := net.DialTimeout("tcp", net.JoinHostPort(ip, portStr), 5*time.Second) if err != nil { - c.JSON(200, gin.H{ - "type": "ceSocket", - "url": url, - "host": host, - "port": port, - "result": "false", - "error": err.Error(), - }) + result["result"] = "false" + if err.Error() != "" { + result["error"] = err.Error() + } + c.JSON(200, result) return } defer conn.Close() - c.JSON(200, gin.H{ - "type": "ceSocket", - "url": url, - "host": host, - "port": port, - "result": "true", - }) + result["result"] = "true" + c.JSON(200, result) } - diff --git a/internal/handler/trace.go b/internal/handler/trace.go index 9285f89..708ebac 100644 --- a/internal/handler/trace.go +++ b/internal/handler/trace.go @@ -8,11 +8,31 @@ import ( ) 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命令 - cmd := exec.Command("traceroute", url) + cmd := exec.Command("traceroute", "-m", "30", "-n", hostname) output, err := cmd.CombinedOutput() if err != nil { c.JSON(200, gin.H{ + "seq": seq, "type": "ceTrace", "url": url, "error": err.Error(), @@ -31,6 +51,7 @@ func handleTrace(c *gin.Context, url string, params map[string]interface{}) { } c.JSON(200, gin.H{ + "seq": seq, "type": "ceTrace", "url": url, "trace_result": traceResult,