package starnet import ( "context" "crypto/tls" "crypto/x509" "errors" "fmt" "net" "net/url" "strings" ) var ( // ErrInvalidMethod 无效的 HTTP 方法 ErrInvalidMethod = errors.New("starnet: invalid HTTP method") // ErrInvalidURL 无效的 URL ErrInvalidURL = errors.New("starnet: invalid URL") // ErrInvalidIP 无效的 IP 地址 ErrInvalidIP = errors.New("starnet: invalid IP address") // ErrInvalidDNS 无效的 DNS 服务器 ErrInvalidDNS = errors.New("starnet: invalid DNS server") // ErrNilClient HTTP Client 为 nil ErrNilClient = errors.New("starnet: http client is nil") // ErrNilReader Reader 为 nil ErrNilReader = errors.New("starnet: reader is nil") // ErrFileNotFound 文件不存在 ErrFileNotFound = errors.New("starnet: file not found") // ErrRequestNotPrepared 请求未准备好 ErrRequestNotPrepared = errors.New("starnet: request not prepared") // ErrBodyAlreadyConsumed Body 已被消费 ErrBodyAlreadyConsumed = errors.New("starnet: response body already consumed") // ErrRespBodyTooLarge 响应体超过允许上限 ErrRespBodyTooLarge = errors.New("starnet: response body too large") // ErrPingInvalidTimeout ping 超时参数无效 ErrPingInvalidTimeout = errors.New("starnet: invalid ping timeout") // ErrPingPermissionDenied ping 需要更高权限(raw socket) ErrPingPermissionDenied = errors.New("starnet: ping permission denied") // ErrPingProtocolUnsupported ping 协议/地址族不受当前平台支持 ErrPingProtocolUnsupported = errors.New("starnet: ping protocol unsupported") // ErrPingNoResolvedTarget ping 目标无法解析为可用地址 ErrPingNoResolvedTarget = errors.New("starnet: ping target not resolved") ) // wrapError 包装错误,添加上下文信息 func wrapError(err error, format string, args ...interface{}) error { if err == nil { return nil } msg := fmt.Sprintf(format, args...) return fmt.Errorf("%s: %w", msg, err) } var ( // ErrNilConn indicates a nil net.Conn argument. ErrNilConn = errors.New("starnet: nil connection") // ErrNonTLSNotAllowed indicates plain TCP was detected while non-TLS is forbidden. ErrNonTLSNotAllowed = errors.New("starnet: non-TLS connection not allowed") // ErrNotTLS indicates caller asked for TLS-only object but conn is plain TCP. ErrNotTLS = errors.New("starnet: connection is not TLS") // ErrNoTLSConfig indicates TLS was detected but no usable TLS config is available. ErrNoTLSConfig = errors.New("starnet: no TLS config available") ) // ErrorKind is a normalized high-level category for request errors. type ErrorKind string const ( ErrorKindNone ErrorKind = "none" ErrorKindCanceled ErrorKind = "canceled" ErrorKindTimeout ErrorKind = "timeout" ErrorKindDNS ErrorKind = "dns" ErrorKindTLS ErrorKind = "tls" ErrorKindProxy ErrorKind = "proxy" ErrorKindOther ErrorKind = "other" ) // IsCanceled reports whether err is a cancellation-related error. func IsCanceled(err error) bool { if err == nil { return false } if errors.Is(err, context.Canceled) { return true } msg := strings.ToLower(err.Error()) return strings.Contains(msg, "context canceled") || strings.Contains(msg, "operation was canceled") || strings.Contains(msg, "request canceled") } // ClassifyError maps low-level errors to a stable category for business handling. func ClassifyError(err error) ErrorKind { if err == nil { return ErrorKindNone } if IsCanceled(err) { return ErrorKindCanceled } if IsProxy(err) { return ErrorKindProxy } if IsDNS(err) { return ErrorKindDNS } if IsTLS(err) { return ErrorKindTLS } if IsTimeout(err) { return ErrorKindTimeout } return ErrorKindOther } // IsTimeout reports whether err is a timeout-related error. func IsTimeout(err error) bool { if err == nil { return false } if errors.Is(err, context.DeadlineExceeded) { return true } var uerr *url.Error if errors.As(err, &uerr) && uerr.Timeout() { return true } var nerr net.Error if errors.As(err, &nerr) && nerr.Timeout() { return true } msg := strings.ToLower(err.Error()) return strings.Contains(msg, "timeout") || strings.Contains(msg, "deadline exceeded") } // IsDNS reports whether err is a DNS resolution related error. func IsDNS(err error) bool { if err == nil { return false } var derr *net.DNSError if errors.As(err, &derr) { return true } msg := strings.ToLower(err.Error()) if strings.Contains(msg, "no such host") || strings.Contains(msg, "server misbehaving") || strings.Contains(msg, "temporary failure in name resolution") { return true } return strings.Contains(msg, "lookup ") && (strings.Contains(msg, "dns") || strings.Contains(msg, "i/o timeout")) } // IsTLS reports whether err is TLS/Certificate related. func IsTLS(err error) bool { if err == nil { return false } if errors.Is(err, ErrNotTLS) || errors.Is(err, ErrNoTLSConfig) || errors.Is(err, ErrNonTLSNotAllowed) { return true } var recErr tls.RecordHeaderError if errors.As(err, &recErr) { return true } var uaErr x509.UnknownAuthorityError if errors.As(err, &uaErr) { return true } var hnErr x509.HostnameError if errors.As(err, &hnErr) { return true } var certErr x509.CertificateInvalidError if errors.As(err, &certErr) { return true } var rootsErr x509.SystemRootsError if errors.As(err, &rootsErr) { return true } msg := strings.ToLower(err.Error()) return strings.Contains(msg, "tls:") || strings.Contains(msg, "x509:") } // IsProxy reports whether err is proxy related. func IsProxy(err error) bool { if err == nil { return false } if isProxyMessage(strings.ToLower(err.Error())) { return true } var uerr *url.Error if errors.As(err, &uerr) { if strings.Contains(strings.ToLower(uerr.Op), "proxy") { return true } if uerr.Err != nil && isProxyMessage(strings.ToLower(uerr.Err.Error())) { return true } } var opErr *net.OpError if errors.As(err, &opErr) && strings.Contains(strings.ToLower(opErr.Op), "proxy") { return true } return false } func isProxyMessage(msg string) bool { return strings.Contains(msg, "proxyconnect") || strings.Contains(msg, "proxy error") || strings.Contains(msg, "proxy authentication required") || strings.Contains(msg, "proxy: unknown scheme") || strings.Contains(msg, "socks connect") || strings.Contains(msg, "socks5") }