251 lines
6.2 KiB
Go
251 lines
6.2 KiB
Go
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")
|
||
}
|