Files
nezha_domains/integration/agentcompat/internal/client/client.go
T
2026-07-20 04:48:15 +00:00

159 lines
4.2 KiB
Go

package client
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"sync/atomic"
"time"
"github.com/gorilla/websocket"
)
const (
defaultRequestTimeout = 10 * time.Second
defaultTransferTimeout = 5 * time.Minute
defaultMaxResponseBytes = int64(8 << 20)
defaultMaxTransferBytes = int64(100 << 20)
)
var ErrInvalidConfig = errors.New("client: invalid configuration")
type Config struct {
BaseURL string
HTTPClient *http.Client
WebSocketDialer *websocket.Dialer
BearerToken string
Origin string
RequestTimeout time.Duration
TransferTimeout time.Duration
MaxResponseBytes int64
MaxTransferBytes int64
}
type Client struct {
baseURL *url.URL
httpClient *http.Client
webSocketDialer *websocket.Dialer
bearerToken string
origin string
requestTimeout time.Duration
transferTimeout time.Duration
maxResponseBytes int64
maxTransferBytes int64
nextRequestID atomic.Uint64
requestCount atomic.Uint64
}
func (client *Client) RequestCount() uint64 {
return client.requestCount.Load()
}
func New(config Config) (*Client, error) {
baseURL, err := url.Parse(config.BaseURL)
if err != nil || baseURL.Host == "" || (baseURL.Scheme != "http" && baseURL.Scheme != "https") {
return nil, fmt.Errorf("base URL: %w", ErrInvalidConfig)
}
requestTimeout := config.RequestTimeout
if requestTimeout == 0 {
requestTimeout = defaultRequestTimeout
}
transferTimeout := config.TransferTimeout
if transferTimeout == 0 {
transferTimeout = defaultTransferTimeout
}
maxResponseBytes := config.MaxResponseBytes
if maxResponseBytes == 0 {
maxResponseBytes = defaultMaxResponseBytes
}
maxTransferBytes := config.MaxTransferBytes
if maxTransferBytes == 0 {
maxTransferBytes = defaultMaxTransferBytes
}
if requestTimeout < 0 || transferTimeout < 0 || maxResponseBytes < 1 || maxTransferBytes < 1 {
return nil, fmt.Errorf("request limits: %w", ErrInvalidConfig)
}
httpClient := &http.Client{}
if config.HTTPClient != nil {
clone := *config.HTTPClient
httpClient = &clone
}
if httpClient.Jar == nil {
jar, jarErr := cookiejar.New(nil)
if jarErr != nil {
return nil, fmt.Errorf("cookie jar: %w", jarErr)
}
httpClient.Jar = jar
}
httpClient.CheckRedirect = rejectRedirect
dialer := websocket.DefaultDialer
if config.WebSocketDialer != nil {
dialer = config.WebSocketDialer
}
dialerClone := *dialer
if dialerClone.HandshakeTimeout == 0 || dialerClone.HandshakeTimeout > requestTimeout {
dialerClone.HandshakeTimeout = requestTimeout
}
origin := strings.TrimSpace(config.Origin)
if origin == "" {
origin = baseURL.Scheme + "://" + baseURL.Host
}
return &Client{
baseURL: baseURL,
httpClient: httpClient,
webSocketDialer: &dialerClone,
bearerToken: strings.TrimSpace(config.BearerToken),
origin: origin,
requestTimeout: requestTimeout,
transferTimeout: transferTimeout,
maxResponseBytes: maxResponseBytes,
maxTransferBytes: maxTransferBytes,
}, nil
}
func rejectRedirect(*http.Request, []*http.Request) error {
return ErrRedirect
}
func (client *Client) requestContext(parent context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(parent, client.requestTimeout)
}
func (client *Client) transferContext(parent context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(parent, client.transferTimeout)
}
func (client *Client) resolvePath(path string) (*url.URL, error) {
reference, err := url.Parse(path)
if err != nil || reference.IsAbs() || reference.Host != "" {
return nil, fmt.Errorf("request path: %w", ErrInvalidConfig)
}
return client.baseURL.ResolveReference(reference), nil
}
func (client *Client) applyAuthenticatedHeaders(request *http.Request, includeCSRF bool) {
if client.bearerToken != "" {
request.Header.Set("Authorization", "Bearer "+client.bearerToken)
}
if client.origin != "" {
request.Header.Set("Origin", client.origin)
}
if !includeCSRF || client.httpClient.Jar == nil {
return
}
for _, cookie := range client.httpClient.Jar.Cookies(client.baseURL) {
if cookie.Name == "nz-csrf" {
request.Header.Set("X-CSRF-Token", cookie.Value)
return
}
}
}