Files
NianAIGC/backend/internal/webhook/http_sender.go

83 lines
2.8 KiB
Go

// Package webhook owns the generation callback wire and transport policy.
package webhook
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"net/url"
"time"
)
// NewPublicHTTPSender constructs the production sender with a transport that
// resolves, validates, and then directly dials the same public IP answer.
func NewPublicHTTPSender(timeout time.Duration, policy *PublicDestinationValidator) (*HTTPSender, error) {
if policy == nil || timeout <= 0 {
return nil, errors.New("webhook HTTP sender requires a bounded client and destination policy")
}
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = nil
transport.DialContext = policy.DialContext
return NewHTTPSender(&http.Client{Timeout: timeout, Transport: transport}, policy.Validate)
}
// HTTPSender is a bounded callback transport. Destination policy remains
// injectable at composition time so deployments can enforce DNS/IP controls
// without coupling the byte/HMAC contract to one network environment.
type HTTPSender struct {
client *http.Client
validate func(context.Context, *url.URL) error
}
func NewHTTPSender(client *http.Client, validate func(context.Context, *url.URL) error) (*HTTPSender, error) {
if client == nil || client.Timeout <= 0 || validate == nil {
return nil, errors.New("webhook HTTP sender requires a bounded client and destination policy")
}
copy := *client
previous := copy.CheckRedirect
copy.CheckRedirect = func(request *http.Request, via []*http.Request) error {
if len(via) >= 5 {
return errors.New("webhook redirect limit exceeded")
}
if err := validate(request.Context(), request.URL); err != nil {
return err
}
if previous != nil {
return previous(request, via)
}
return nil
}
return &HTTPSender{client: &copy, validate: validate}, nil
}
func (sender *HTTPSender) Send(ctx context.Context, input Request) (Response, error) {
target, err := url.Parse(input.URL)
if err != nil || (target.Scheme != "http" && target.Scheme != "https") || target.Host == "" || target.User != nil {
return Response{}, errors.New("webhook destination is invalid")
}
if err := sender.validate(ctx, target); err != nil {
return Response{}, errors.New("webhook destination is not allowed")
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(input.Body))
if err != nil {
return Response{}, errors.New("build webhook request")
}
for key, value := range input.Headers {
request.Header.Set(key, value)
}
response, err := sender.client.Do(request)
if err != nil {
if ctx.Err() != nil {
return Response{}, ctx.Err()
}
return Response{}, errors.New("send webhook request")
}
defer response.Body.Close()
_, _ = io.Copy(io.Discard, io.LimitReader(response.Body, 64<<10))
return Response{Status: response.StatusCode}, nil
}
var _ Sender = (*HTTPSender)(nil)