hetty/pkg/sender/sender.go
2022-03-31 15:12:54 +02:00

251 lines
6.1 KiB
Go

package sender
import (
"bytes"
"context"
"errors"
"fmt"
"math/rand"
"net/http"
"net/url"
"time"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
var defaultHTTPClient = &http.Client{
Transport: &HTTPTransport{},
Timeout: 30 * time.Second,
}
var (
ErrProjectIDMustBeSet = errors.New("sender: project ID must be set")
ErrRequestNotFound = errors.New("sender: request not found")
)
type Service interface {
FindRequestByID(ctx context.Context, id ulid.ULID) (Request, error)
FindRequests(ctx context.Context) ([]Request, error)
CreateOrUpdateRequest(ctx context.Context, req Request) (Request, error)
CloneFromRequestLog(ctx context.Context, reqLogID ulid.ULID) (Request, error)
DeleteRequests(ctx context.Context, projectID ulid.ULID) error
SendRequest(ctx context.Context, id ulid.ULID) (Request, error)
SetActiveProjectID(ulid.ULID)
SetFindReqsFilter(filter FindRequestsFilter)
FindReqsFilter() FindRequestsFilter
}
type service struct {
activeProjectID ulid.ULID
findReqsFilter FindRequestsFilter
scope *scope.Scope
repo Repository
reqLogSvc reqlog.Service
httpClient *http.Client
}
type FindRequestsFilter struct {
ProjectID ulid.ULID
OnlyInScope bool
SearchExpr filter.Expression
}
type Config struct {
Scope *scope.Scope
Repository Repository
ReqLogService reqlog.Service
HTTPClient *http.Client
}
type SendError struct {
err error
}
func NewService(cfg Config) Service {
svc := &service{
repo: cfg.Repository,
reqLogSvc: cfg.ReqLogService,
httpClient: defaultHTTPClient,
scope: cfg.Scope,
}
if cfg.HTTPClient != nil {
svc.httpClient = cfg.HTTPClient
}
return svc
}
type Request struct {
ID ulid.ULID
ProjectID ulid.ULID
SourceRequestLogID ulid.ULID
URL *url.URL
Method string
Proto string
Header http.Header
Body []byte
Response *reqlog.ResponseLog
}
func (svc *service) FindRequestByID(ctx context.Context, id ulid.ULID) (Request, error) {
req, err := svc.repo.FindSenderRequestByID(ctx, id)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to find request: %w", err)
}
return req, nil
}
func (svc *service) FindRequests(ctx context.Context) ([]Request, error) {
return svc.repo.FindSenderRequests(ctx, svc.findReqsFilter, svc.scope)
}
func (svc *service) CreateOrUpdateRequest(ctx context.Context, req Request) (Request, error) {
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
return Request{}, ErrProjectIDMustBeSet
}
if req.ID.Compare(ulid.ULID{}) == 0 {
req.ID = ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
}
req.ProjectID = svc.activeProjectID
if req.Method == "" {
req.Method = http.MethodGet
}
if req.Proto == "" {
req.Proto = HTTPProto20
}
if !isValidProto(req.Proto) {
return Request{}, fmt.Errorf("sender: unsupported HTTP protocol: %v", req.Proto)
}
err := svc.repo.StoreSenderRequest(ctx, req)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to store request: %w", err)
}
return req, nil
}
func (svc *service) CloneFromRequestLog(ctx context.Context, reqLogID ulid.ULID) (Request, error) {
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
return Request{}, ErrProjectIDMustBeSet
}
reqLog, err := svc.reqLogSvc.FindRequestLogByID(ctx, reqLogID)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to find request log: %w", err)
}
req := Request{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: svc.activeProjectID,
SourceRequestLogID: reqLogID,
Method: reqLog.Method,
URL: reqLog.URL,
Proto: HTTPProto20, // Attempt HTTP/2.
Header: reqLog.Header,
Body: reqLog.Body,
}
err = svc.repo.StoreSenderRequest(ctx, req)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to store request: %w", err)
}
return req, nil
}
func (svc *service) SetFindReqsFilter(filter FindRequestsFilter) {
svc.findReqsFilter = filter
}
func (svc *service) FindReqsFilter() FindRequestsFilter {
return svc.findReqsFilter
}
func (svc *service) SendRequest(ctx context.Context, id ulid.ULID) (Request, error) {
req, err := svc.repo.FindSenderRequestByID(ctx, id)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to find request: %w", err)
}
httpReq, err := parseHTTPRequest(ctx, req)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to parse HTTP request: %w", err)
}
resLog, err := svc.sendHTTPRequest(httpReq)
if err != nil {
return Request{}, fmt.Errorf("sender: could not send HTTP request: %w", err)
}
err = svc.repo.StoreResponseLog(ctx, id, resLog)
if err != nil {
return Request{}, fmt.Errorf("sender: failed to store sender response log: %w", err)
}
req.Response = &resLog
return req, nil
}
func parseHTTPRequest(ctx context.Context, req Request) (*http.Request, error) {
ctx = context.WithValue(ctx, protoCtxKey{}, req.Proto)
httpReq, err := http.NewRequestWithContext(ctx, req.Method, req.URL.String(), bytes.NewReader(req.Body))
if err != nil {
return nil, fmt.Errorf("failed to construct HTTP request: %w", err)
}
if req.Header != nil {
httpReq.Header = req.Header
}
return httpReq, nil
}
func (svc *service) sendHTTPRequest(httpReq *http.Request) (reqlog.ResponseLog, error) {
res, err := svc.httpClient.Do(httpReq)
if err != nil {
return reqlog.ResponseLog{}, &SendError{err}
}
defer res.Body.Close()
resLog, err := reqlog.ParseHTTPResponse(res)
if err != nil {
return reqlog.ResponseLog{}, fmt.Errorf("failed to parse http response: %w", err)
}
return resLog, err
}
func (svc *service) SetActiveProjectID(id ulid.ULID) {
svc.activeProjectID = id
}
func (svc *service) DeleteRequests(ctx context.Context, projectID ulid.ULID) error {
return svc.repo.DeleteSenderRequests(ctx, projectID)
}
func (e SendError) Error() string {
return fmt.Sprintf("failed to send HTTP request: %v", e.err)
}
func (e SendError) Unwrap() error {
return e.err
}