feat: complete remaining Go backend modules
This commit is contained in:
1 parent
cea2751dc5
commit
aef5a97165
145 files changed
+18376
-199
No files matched your search
@@ -0,0 +1,82 @@
|
||||
// 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: ©, 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)
|
||||
Reference in new issue
Block a user