package siteprobe import ( "context" "crypto/tls" "crypto/x509" "fmt" "net" "net/url" "strings" "time" "ezops/model" ) // parseSSLTarget 解析 SSL 拨测地址,支持 example.com:443 / https://example.com // Logan.Li func parseSSLTarget(raw string) (host, port string, err error) { raw = strings.TrimSpace(raw) if raw == "" { return "", "", fmt.Errorf("SSL 地址不能为空") } lower := strings.ToLower(raw) // 兼容 https:example.com(单斜杠写法) if strings.HasPrefix(lower, "https:") && !strings.HasPrefix(lower, "https://") { raw = "https://" + strings.TrimSpace(raw[len("https:"):]) lower = strings.ToLower(raw) } if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") || strings.HasPrefix(lower, "ssl://") { u, parseErr := url.Parse(raw) if parseErr != nil || u.Host == "" { return "", "", fmt.Errorf("SSL 地址格式无效: %s", raw) } if h, p, splitErr := net.SplitHostPort(u.Host); splitErr == nil { return h, p, nil } defPort := "443" if u.Scheme == "http" { defPort = "80" } return u.Hostname(), defPort, nil } // 去掉路径后缀,如 example.com:443/foo if idx := strings.Index(raw, "/"); idx >= 0 { raw = raw[:idx] } return splitHostPort(raw, "443") } // probeSSL 纯 TLS 握手检测证书(不发起 HTTP 请求)// Logan.Li func probeSSL(ctx context.Context, task model.SiteTask) ProbeOutcome { start := time.Now() host, port, err := parseSSLTarget(task.URL) if err != nil { return failOutcome(start, err.Error()) } addr := net.JoinHostPort(host, port) d := &net.Dialer{Timeout: time.Until(deadlineFromCtx(ctx))} conn, err := tls.DialWithDialer(d, "tcp", addr, &tls.Config{ ServerName: host, InsecureSkipVerify: !task.VerifySSL, }) elapsed := msSince(start) if err != nil { if ctx.Err() == context.DeadlineExceeded { return ProbeOutcome{Status: model.SiteResultTimeout, TotalTime: elapsed, ResponseTime: elapsed, ErrorMessage: "TLS 握手超时"} } return failOutcome(start, fmt.Sprintf("TLS 握手失败: %s", err.Error())) } defer conn.Close() remote := "" if addr := conn.RemoteAddr(); addr != nil { remote = addr.String() } out := ProbeOutcome{ Status: model.SiteResultSuccess, ResponseTime: elapsed, TotalTime: elapsed, RemoteIP: remote, } applyCertsToOutcome(&out, conn.ConnectionState().PeerCertificates) return out } // applyCertsToOutcome 从 TLS 证书链填充到期信息 // Logan.Li func applyCertsToOutcome(out *ProbeOutcome, certs []*x509.Certificate) { if len(certs) == 0 { return } cert := certs[0] expiry := cert.NotAfter days := int(time.Until(expiry).Hours() / 24) out.SSLCertExpiry = &expiry out.SSLDaysRemaining = days out.SSLCertIssuer = cert.Issuer.CommonName if out.SSLCertIssuer == "" { out.SSLCertIssuer = cert.Issuer.String() } out.SSLCertSubject = cert.Subject.CommonName if out.SSLCertSubject == "" { out.SSLCertSubject = cert.Subject.String() } } // IsSSLProbeTask 是否为 SSL 证书拨测任务 // Logan.Li func IsSSLProbeTask(task model.SiteTask) bool { return strings.EqualFold(string(task.TaskType), "ssl") }