2022-02-22 14:10:39 +01:00
|
|
|
package sender
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
connect "connectrpc.com/connect"
|
|
|
|
"github.com/oklog/ulid/v2"
|
|
|
|
"google.golang.org/protobuf/proto"
|
2022-02-22 14:10:39 +01:00
|
|
|
|
2022-03-31 15:12:54 +02:00
|
|
|
"github.com/dstotijn/hetty/pkg/filter"
|
2025-02-05 21:54:59 +01:00
|
|
|
httppb "github.com/dstotijn/hetty/pkg/http"
|
2022-02-22 14:10:39 +01:00
|
|
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
|
|
|
"github.com/dstotijn/hetty/pkg/scope"
|
|
|
|
)
|
|
|
|
|
|
|
|
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")
|
|
|
|
)
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
type Service struct {
|
2025-02-05 21:54:59 +01:00
|
|
|
activeProjectID string
|
|
|
|
reqsFilter *RequestsFilter
|
2022-02-22 14:10:39 +01:00
|
|
|
scope *scope.Scope
|
|
|
|
repo Repository
|
2025-01-04 00:39:40 +01:00
|
|
|
reqLogSvc *reqlog.Service
|
2022-02-22 14:10:39 +01:00
|
|
|
httpClient *http.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
type Config struct {
|
|
|
|
Scope *scope.Scope
|
|
|
|
Repository Repository
|
2025-01-04 00:39:40 +01:00
|
|
|
ReqLogService *reqlog.Service
|
2022-02-22 14:10:39 +01:00
|
|
|
HTTPClient *http.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
type SendError struct {
|
|
|
|
err error
|
|
|
|
}
|
|
|
|
|
2025-01-04 00:39:40 +01:00
|
|
|
func NewService(cfg Config) *Service {
|
|
|
|
svc := &Service{
|
2022-02-22 14:10:39 +01:00
|
|
|
repo: cfg.Repository,
|
|
|
|
reqLogSvc: cfg.ReqLogService,
|
|
|
|
httpClient: defaultHTTPClient,
|
|
|
|
scope: cfg.Scope,
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg.HTTPClient != nil {
|
|
|
|
svc.httpClient = cfg.HTTPClient
|
|
|
|
}
|
|
|
|
|
|
|
|
return svc
|
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
func (svc *Service) GetRequestByID(ctx context.Context, req *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error) {
|
|
|
|
if svc.activeProjectID == "" {
|
|
|
|
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
|
|
|
|
}
|
2022-02-22 14:10:39 +01:00
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
senderReq, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, req.Msg.RequestId)
|
|
|
|
if err != nil {
|
|
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("sender: failed to find request: %w", err))
|
|
|
|
}
|
2022-02-22 14:10:39 +01:00
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
return &connect.Response[GetRequestByIDResponse]{
|
|
|
|
Msg: &GetRequestByIDResponse{Request: senderReq},
|
|
|
|
}, nil
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
func (svc *Service) ListRequests(ctx context.Context, req *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error) {
|
|
|
|
reqs, err := svc.repo.FindSenderRequests(ctx, svc.activeProjectID, svc.filterRequest)
|
2022-02-22 14:10:39 +01:00
|
|
|
if err != nil {
|
2025-02-05 21:54:59 +01:00
|
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to find requests: %w", err))
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
return &connect.Response[ListRequestsResponse]{
|
|
|
|
Msg: &ListRequestsResponse{Requests: reqs},
|
|
|
|
}, nil
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
func (svc *Service) filterRequest(req *Request) (bool, error) {
|
|
|
|
if svc.reqsFilter.OnlyInScope {
|
|
|
|
if svc.scope != nil && !req.MatchScope(svc.scope) {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if svc.reqsFilter.SearchExpr == "" {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
expr, err := filter.ParseQuery(svc.reqsFilter.SearchExpr)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("failed to parse search expression: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
match, err := req.Matches(expr)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("failed to match search expression for sender request (id: %v): %w",
|
|
|
|
req.Id, err,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return match, nil
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
func (svc *Service) CreateOrUpdateRequest(ctx context.Context, req *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error) {
|
|
|
|
if svc.activeProjectID == "" {
|
|
|
|
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
r := proto.Clone(req.Msg.Request).(*Request)
|
|
|
|
|
|
|
|
if r == nil {
|
|
|
|
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("sender: request is nil"))
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
if r.HttpRequest == nil {
|
|
|
|
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("sender: request.http_request is nil"))
|
|
|
|
}
|
2022-02-22 14:10:39 +01:00
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
if r.Id == "" {
|
|
|
|
r.Id = ulid.Make().String()
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
r.ProjectId = svc.activeProjectID
|
|
|
|
|
|
|
|
if r.HttpRequest.Method == httppb.Method_METHOD_UNSPECIFIED {
|
|
|
|
r.HttpRequest.Method = httppb.Method_METHOD_GET
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
if r.HttpRequest.Protocol == httppb.Protocol_PROTOCOL_UNSPECIFIED {
|
|
|
|
r.HttpRequest.Protocol = httppb.Protocol_PROTOCOL_HTTP20
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
err := svc.repo.StoreSenderRequest(ctx, r)
|
2022-02-22 14:10:39 +01:00
|
|
|
if err != nil {
|
2025-02-05 21:54:59 +01:00
|
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store request: %w", err))
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
return &connect.Response[CreateOrUpdateRequestResponse]{
|
|
|
|
Msg: &CreateOrUpdateRequestResponse{
|
|
|
|
Request: r,
|
|
|
|
},
|
|
|
|
}, nil
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
func (svc *Service) CloneFromRequestLog(ctx context.Context, req *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error) {
|
|
|
|
if svc.activeProjectID == "" {
|
|
|
|
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
reqLog, err := svc.reqLogSvc.FindRequestLogByID(ctx, req.Msg.RequestLogId)
|
2022-02-22 14:10:39 +01:00
|
|
|
if err != nil {
|
2025-02-05 21:54:59 +01:00
|
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to find request log: %w", err))
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
clonedReqLog := proto.Clone(reqLog).(*reqlog.HttpRequestLog)
|
|
|
|
|
|
|
|
senderReq := &Request{
|
|
|
|
Id: ulid.Make().String(),
|
|
|
|
ProjectId: svc.activeProjectID,
|
|
|
|
SourceRequestLogId: clonedReqLog.Id,
|
|
|
|
HttpRequest: clonedReqLog.Request,
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
err = svc.repo.StoreSenderRequest(ctx, senderReq)
|
2022-02-22 14:10:39 +01:00
|
|
|
if err != nil {
|
2025-02-05 21:54:59 +01:00
|
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store request: %w", err))
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
return &connect.Response[CloneFromRequestLogResponse]{Msg: &CloneFromRequestLogResponse{
|
|
|
|
Request: senderReq,
|
|
|
|
}}, nil
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
func (svc *Service) SetRequestsFilter(filter *RequestsFilter) {
|
|
|
|
svc.reqsFilter = filter
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
func (svc *Service) RequestsFilter() *RequestsFilter {
|
|
|
|
return svc.reqsFilter
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
func (svc *Service) SendRequest(ctx context.Context, connReq *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error) {
|
|
|
|
req, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, connReq.Msg.RequestId)
|
2022-02-22 14:10:39 +01:00
|
|
|
if err != nil {
|
2025-02-05 21:54:59 +01:00
|
|
|
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("sender: failed to find request: %w", err))
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
httpReq, err := parseHTTPRequest(ctx, req)
|
|
|
|
if err != nil {
|
2025-02-05 21:54:59 +01:00
|
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to parse HTTP request: %w", err))
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
httpRes, err := svc.sendHTTPRequest(httpReq)
|
2022-02-22 14:10:39 +01:00
|
|
|
if err != nil {
|
2025-02-05 21:54:59 +01:00
|
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: could not send HTTP request: %w", err))
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
req.HttpResponse = httpRes
|
2025-01-13 23:15:18 +01:00
|
|
|
|
|
|
|
err = svc.repo.StoreSenderRequest(ctx, req)
|
2022-02-22 14:10:39 +01:00
|
|
|
if err != nil {
|
2025-02-05 21:54:59 +01:00
|
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store sender response log: %w", err))
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
return &connect.Response[SendRequestResponse]{
|
|
|
|
Msg: &SendRequestResponse{
|
|
|
|
Request: req,
|
|
|
|
},
|
|
|
|
}, nil
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
func parseHTTPRequest(ctx context.Context, req *Request) (*http.Request, error) {
|
|
|
|
ctx = context.WithValue(ctx, protoCtxKey{}, req.GetHttpRequest().GetProtocol())
|
2022-02-22 14:10:39 +01:00
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
httpReq, err := http.NewRequestWithContext(ctx,
|
|
|
|
req.GetHttpRequest().GetMethod().String(),
|
|
|
|
req.GetHttpRequest().GetUrl(),
|
|
|
|
bytes.NewReader(req.GetHttpRequest().GetBody()),
|
|
|
|
)
|
2022-02-22 14:10:39 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to construct HTTP request: %w", err)
|
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
for _, header := range req.GetHttpRequest().GetHeaders() {
|
|
|
|
httpReq.Header.Add(header.Key, header.Value)
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return httpReq, nil
|
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
func (svc *Service) sendHTTPRequest(httpReq *http.Request) (*httppb.Response, error) {
|
2022-02-22 14:10:39 +01:00
|
|
|
res, err := svc.httpClient.Do(httpReq)
|
|
|
|
if err != nil {
|
2025-02-05 21:54:59 +01:00
|
|
|
return nil, &SendError{err}
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
resLog, err := httppb.ParseHTTPResponse(res)
|
2022-02-22 14:10:39 +01:00
|
|
|
if err != nil {
|
2025-02-05 21:54:59 +01:00
|
|
|
return nil, fmt.Errorf("failed to parse http response: %w", err)
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return resLog, err
|
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
func (svc *Service) SetActiveProjectID(id string) {
|
2022-02-22 14:10:39 +01:00
|
|
|
svc.activeProjectID = id
|
|
|
|
}
|
|
|
|
|
2025-02-05 21:54:59 +01:00
|
|
|
func (svc *Service) DeleteRequests(ctx context.Context, req *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error) {
|
|
|
|
if svc.activeProjectID == "" {
|
|
|
|
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
|
|
|
|
}
|
|
|
|
|
|
|
|
err := svc.repo.DeleteSenderRequests(ctx, svc.activeProjectID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to delete requests: %w", err))
|
|
|
|
}
|
|
|
|
|
|
|
|
return &connect.Response[DeleteRequestsResponse]{}, nil
|
2022-02-22 14:10:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (e SendError) Error() string {
|
|
|
|
return fmt.Sprintf("failed to send HTTP request: %v", e.err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e SendError) Unwrap() error {
|
|
|
|
return e.err
|
|
|
|
}
|