Files
hetty/pkg/reqlog/reqlog.go

224 lines
5.0 KiB
Go
Raw Normal View History

2019-12-01 14:07:12 +01:00
package reqlog
import (
"bytes"
"compress/gzip"
2020-09-26 23:36:48 +02:00
"context"
"errors"
"fmt"
2022-01-21 11:45:54 +01:00
"io"
"io/ioutil"
"log"
2022-01-21 11:45:54 +01:00
"math/rand"
2019-12-01 14:07:12 +01:00
"net/http"
2022-01-21 11:45:54 +01:00
"net/url"
"time"
2022-01-21 11:45:54 +01:00
"github.com/oklog/ulid"
2020-09-22 18:33:02 +02:00
"github.com/dstotijn/hetty/pkg/proxy"
2020-10-01 21:46:35 +02:00
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
2019-12-01 14:07:12 +01:00
)
2020-10-01 21:46:35 +02:00
type contextKey int
const LogBypassedKey contextKey = 0
2022-01-21 11:45:54 +01:00
var (
ErrRequestNotFound = errors.New("reqlog: request not found")
ErrProjectIDMustBeSet = errors.New("reqlog: project ID must be set")
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
type RequestLog struct {
ID ulid.ULID
ProjectID ulid.ULID
2020-10-29 20:54:17 +01:00
2022-01-21 11:45:54 +01:00
URL *url.URL
Method string
Proto string
Header http.Header
Body []byte
2022-01-21 11:45:54 +01:00
Response *ResponseLog
2019-12-01 14:07:12 +01:00
}
2022-01-21 11:45:54 +01:00
type ResponseLog struct {
Proto string
StatusCode int
Status string
Header http.Header
Body []byte
2019-12-01 14:07:12 +01:00
}
type Service struct {
2020-10-01 21:46:35 +02:00
BypassOutOfScopeRequests bool
2020-10-29 20:54:17 +01:00
FindReqsFilter FindRequestsFilter
2022-01-21 11:45:54 +01:00
ActiveProjectID ulid.ULID
2020-10-01 21:46:35 +02:00
scope *scope.Scope
repo Repository
}
2020-10-29 20:54:17 +01:00
type FindRequestsFilter struct {
2022-01-21 11:45:54 +01:00
ProjectID ulid.ULID
OnlyInScope bool
SearchExpr search.Expression
2020-10-01 21:46:35 +02:00
}
type Config struct {
2022-01-21 11:45:54 +01:00
Scope *scope.Scope
Repository Repository
2019-12-01 14:07:12 +01:00
}
2020-10-01 21:46:35 +02:00
func NewService(cfg Config) *Service {
2022-01-21 11:45:54 +01:00
return &Service{
repo: cfg.Repository,
scope: cfg.Scope,
2020-10-01 21:46:35 +02:00
}
2020-10-29 20:54:17 +01:00
}
2022-01-21 11:45:54 +01:00
func (svc *Service) FindRequests(ctx context.Context) ([]RequestLog, error) {
2020-10-29 20:54:17 +01:00
return svc.repo.FindRequestLogs(ctx, svc.FindReqsFilter, svc.scope)
}
2022-01-21 11:45:54 +01:00
func (svc *Service) FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error) {
2020-09-26 23:36:48 +02:00
return svc.repo.FindRequestLogByID(ctx, id)
}
2022-01-21 11:45:54 +01:00
func (svc *Service) ClearRequests(ctx context.Context, projectID ulid.ULID) error {
return svc.repo.ClearRequestLogs(ctx, projectID)
}
2022-01-21 11:45:54 +01:00
func (svc *Service) storeResponse(ctx context.Context, reqLogID ulid.ULID, res *http.Response) error {
if res.Header.Get("Content-Encoding") == "gzip" {
2022-01-21 11:45:54 +01:00
gzipReader, err := gzip.NewReader(res.Body)
if err != nil {
2022-01-21 11:45:54 +01:00
return fmt.Errorf("could not create gzip reader: %w", err)
}
defer gzipReader.Close()
2021-04-25 16:23:53 +02:00
2022-01-21 11:45:54 +01:00
buf := &bytes.Buffer{}
if _, err := io.Copy(buf, gzipReader); err != nil {
return fmt.Errorf("could not read gzipped response body: %w", err)
}
2022-01-21 11:45:54 +01:00
res.Body = io.NopCloser(buf)
}
body, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("could not read body: %w", err)
}
2022-01-21 11:45:54 +01:00
resLog := ResponseLog{
Proto: res.Proto,
StatusCode: res.StatusCode,
Status: res.Status,
Header: res.Header,
Body: body,
}
return svc.repo.StoreResponseLog(ctx, reqLogID, resLog)
}
func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
return func(req *http.Request) {
next(req)
clone := req.Clone(req.Context())
2021-04-25 16:23:53 +02:00
var body []byte
2021-04-25 16:23:53 +02:00
if req.Body != nil {
// TODO: Use io.LimitReader.
var err error
2021-04-25 16:23:53 +02:00
body, err = ioutil.ReadAll(req.Body)
if err != nil {
log.Printf("[ERROR] Could not read request body for logging: %v", err)
return
}
2021-04-25 16:23:53 +02:00
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
2022-01-21 11:45:54 +01:00
clone.Body = ioutil.NopCloser(bytes.NewBuffer(body))
}
2022-01-21 11:45:54 +01:00
// Bypass logging if no project is active.
if svc.ActiveProjectID.Compare(ulid.ULID{}) == 0 {
2020-10-01 21:46:35 +02:00
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
2020-10-04 11:50:03 +02:00
*req = *req.WithContext(ctx)
2021-04-25 16:23:53 +02:00
2020-10-01 21:46:35 +02:00
return
}
2022-01-21 11:45:54 +01:00
// Bypass logging if this setting is enabled and the incoming request
// doesn't match any scope rules.
if svc.BypassOutOfScopeRequests && !svc.scope.Match(clone, body) {
2020-10-11 17:09:39 +02:00
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
*req = *req.WithContext(ctx)
2021-04-25 16:23:53 +02:00
2020-10-11 17:09:39 +02:00
return
2022-01-21 11:45:54 +01:00
}
reqLog := RequestLog{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: svc.ActiveProjectID,
Method: clone.Method,
URL: clone.URL,
Proto: clone.Proto,
Header: clone.Header,
Body: body,
}
err := svc.repo.StoreRequestLog(req.Context(), reqLog)
if err != nil {
2020-10-04 11:50:03 +02:00
log.Printf("[ERROR] Could not store request log: %v", err)
return
}
2021-04-25 16:23:53 +02:00
2022-01-21 11:45:54 +01:00
ctx := context.WithValue(req.Context(), proxy.ReqLogIDKey, reqLog.ID)
2020-10-04 11:50:03 +02:00
*req = *req.WithContext(ctx)
}
}
func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
return func(res *http.Response) error {
if err := next(res); err != nil {
return err
}
2020-10-01 21:46:35 +02:00
if bypassed, _ := res.Request.Context().Value(LogBypassedKey).(bool); bypassed {
return nil
}
2022-01-21 11:45:54 +01:00
reqLogID, ok := res.Request.Context().Value(proxy.ReqLogIDKey).(ulid.ULID)
if !ok {
return errors.New("reqlog: request is missing ID")
}
clone := *res
// TODO: Use io.LimitReader.
body, err := ioutil.ReadAll(res.Body)
if err != nil {
2021-04-25 16:23:53 +02:00
return fmt.Errorf("reqlog: could not read response body: %w", err)
}
2021-04-25 16:23:53 +02:00
res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
2022-01-21 11:45:54 +01:00
clone.Body = ioutil.NopCloser(bytes.NewBuffer(body))
go func() {
2022-01-21 11:45:54 +01:00
if err := svc.storeResponse(context.Background(), reqLogID, &clone); err != nil {
log.Printf("[ERROR] Could not store response log: %v", err)
}
}()
return nil
}
2019-12-01 14:07:12 +01:00
}