From d48f1f058d02b60c608916eea0451d2cb85ad8fa Mon Sep 17 00:00:00 2001 From: David Stotijn Date: Thu, 1 Oct 2020 21:46:35 +0200 Subject: [PATCH] Add scaffolding for `scope` package --- cmd/hetty/main.go | 7 ++- pkg/api/resolvers.go | 3 +- pkg/db/cayley/cayley.go | 26 +++++++++-- pkg/proxy/proxy.go | 4 ++ pkg/reqlog/repo.go | 3 +- pkg/reqlog/reqlog.go | 50 ++++++++++++++++++--- pkg/scope/scope.go | 96 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 pkg/scope/scope.go diff --git a/cmd/hetty/main.go b/cmd/hetty/main.go index ee53b2c..ba70048 100644 --- a/cmd/hetty/main.go +++ b/cmd/hetty/main.go @@ -14,6 +14,7 @@ import ( "github.com/dstotijn/hetty/pkg/db/cayley" "github.com/dstotijn/hetty/pkg/proxy" "github.com/dstotijn/hetty/pkg/reqlog" + "github.com/dstotijn/hetty/pkg/scope" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" @@ -64,7 +65,11 @@ func main() { } defer db.Close() - reqLogService := reqlog.NewService(db) + scope := scope.New(nil) + reqLogService := reqlog.NewService(reqlog.Config{ + Scope: scope, + Repository: db, + }) p, err := proxy.NewProxy(caCert, caKey) if err != nil { diff --git a/pkg/api/resolvers.go b/pkg/api/resolvers.go index be892ae..5d66841 100644 --- a/pkg/api/resolvers.go +++ b/pkg/api/resolvers.go @@ -20,7 +20,8 @@ type queryResolver struct{ *Resolver } func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } func (r *queryResolver) HTTPRequestLogs(ctx context.Context) ([]HTTPRequestLog, error) { - reqs, err := r.RequestLogService.FindAllRequests(ctx) + opts := reqlog.FindRequestsOptions{OmitOutOfScope: false} + reqs, err := r.RequestLogService.FindRequests(ctx, opts) if err != nil { return nil, fmt.Errorf("could not query repository for requests: %v", err) } diff --git a/pkg/db/cayley/cayley.go b/pkg/db/cayley/cayley.go index c5e44d0..6814269 100644 --- a/pkg/db/cayley/cayley.go +++ b/pkg/db/cayley/cayley.go @@ -14,6 +14,7 @@ import ( "github.com/cayleygraph/cayley" "github.com/cayleygraph/cayley/graph" "github.com/cayleygraph/cayley/graph/kv" + cpath "github.com/cayleygraph/cayley/graph/path" "github.com/cayleygraph/cayley/schema" "github.com/cayleygraph/quad" "github.com/cayleygraph/quad/voc" @@ -21,6 +22,7 @@ import ( "github.com/google/uuid" "github.com/dstotijn/hetty/pkg/reqlog" + "github.com/dstotijn/hetty/pkg/scope" ) type HTTPRequest struct { @@ -107,17 +109,33 @@ func (db *Database) Close() error { return db.store.Close() } -func (db *Database) FindAllRequestLogs(ctx context.Context) ([]reqlog.Request, error) { +func (db *Database) FindRequestLogs(ctx context.Context, opts reqlog.FindRequestsOptions, scope *scope.Scope) ([]reqlog.Request, error) { db.mu.Lock() defer db.mu.Unlock() var reqLogs []reqlog.Request var reqs []HTTPRequest - path := cayley.StartPath(db.store, quad.IRI("hy:HTTPRequest")).In(quad.IRI(rdf.Type)) - err := path.Iterate(ctx).EachValue(db.store, func(v quad.Value) { + reqPath := cayley.StartPath(db.store, quad.IRI("hy:HTTPRequest")).In(quad.IRI(rdf.Type)) + if opts.OmitOutOfScope { + var filterPath *cpath.Path + for _, rule := range scope.Rules() { + if rule.URL != nil { + if filterPath == nil { + filterPath = reqPath.Out(quad.IRI("hy:url")).Regex(rule.URL).In(quad.IRI("hy:url")) + } else { + filterPath = filterPath.Or(reqPath.Out(quad.IRI("hy:url")).Regex(rule.URL).In(quad.IRI("hy:url"))) + } + } + } + if filterPath != nil { + reqPath = filterPath + } + } + + err := reqPath.Iterate(ctx).EachValue(db.store, func(v quad.Value) { var req HTTPRequest - if err := db.schema.LoadToDepth(ctx, db.store, &req, -1, v); err != nil { + if err := db.schema.LoadToDepth(ctx, db.store, &req, 0, v); err != nil { log.Printf("[ERROR] Could not load sub-graph for http requests: %v", err) return } diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 844c879..2e51fae 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -11,6 +11,8 @@ import ( "net/http" "net/http/httputil" + "github.com/dstotijn/hetty/pkg/scope" + "github.com/google/uuid" ) @@ -27,6 +29,8 @@ type Proxy struct { // TODO: Add mutex for modifier funcs. reqModifiers []RequestModifyMiddleware resModifiers []ResponseModifyMiddleware + + scope *scope.Scope } // NewProxy returns a new Proxy. diff --git a/pkg/reqlog/repo.go b/pkg/reqlog/repo.go index 9d0759a..6185523 100644 --- a/pkg/reqlog/repo.go +++ b/pkg/reqlog/repo.go @@ -3,11 +3,12 @@ package reqlog import ( "context" + "github.com/dstotijn/hetty/pkg/scope" "github.com/google/uuid" ) type Repository interface { - FindAllRequestLogs(ctx context.Context) ([]Request, error) + FindRequestLogs(ctx context.Context, opts FindRequestsOptions, scope *scope.Scope) ([]Request, error) FindRequestLogByID(ctx context.Context, id uuid.UUID) (Request, error) AddRequestLog(ctx context.Context, reqLog Request) error AddResponseLog(ctx context.Context, resLog Response) error diff --git a/pkg/reqlog/reqlog.go b/pkg/reqlog/reqlog.go index b5d3747..c024272 100644 --- a/pkg/reqlog/reqlog.go +++ b/pkg/reqlog/reqlog.go @@ -12,9 +12,15 @@ import ( "time" "github.com/dstotijn/hetty/pkg/proxy" + "github.com/dstotijn/hetty/pkg/scope" + "github.com/google/uuid" ) +type contextKey int + +const LogBypassedKey contextKey = 0 + var ErrRequestNotFound = errors.New("reqlog: request not found") type Request struct { @@ -33,15 +39,37 @@ type Response struct { } type Service struct { - repo Repository + BypassOutOfScopeRequests bool + + scope *scope.Scope + repo Repository } -func NewService(repo Repository) *Service { - return &Service{repo} +type FindRequestsOptions struct { + OmitOutOfScope bool } -func (svc *Service) FindAllRequests(ctx context.Context) ([]Request, error) { - return svc.repo.FindAllRequestLogs(ctx) +type Config struct { + Scope *scope.Scope + Repository Repository + BypassOutOfScopeRequests bool +} + +func NewService(cfg Config) *Service { + return &Service{ + scope: cfg.Scope, + repo: cfg.Repository, + BypassOutOfScopeRequests: cfg.BypassOutOfScopeRequests, + } +} + +func (svc *Service) FindRequests(ctx context.Context, opts FindRequestsOptions) ([]Request, error) { + var scope *scope.Scope + if opts.OmitOutOfScope { + scope = svc.scope + } + + return svc.repo.FindRequestLogs(ctx, opts, scope) } func (svc *Service) FindRequestLogByID(ctx context.Context, id uuid.UUID) (Request, error) { @@ -99,6 +127,14 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) } + // Bypass logging if this setting is enabled and the incoming request + // doens't match any rules of the scope. + if svc.BypassOutOfScopeRequests && !svc.scope.Match(clone, body) { + ctx := context.WithValue(req.Context(), LogBypassedKey, true) + req = req.WithContext(ctx) + return + } + reqID, _ := req.Context().Value(proxy.ReqIDKey).(uuid.UUID) if reqID == uuid.Nil { log.Println("[ERROR] Request is missing a related request ID") @@ -119,6 +155,10 @@ func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon return err } + if bypassed, _ := res.Request.Context().Value(LogBypassedKey).(bool); bypassed { + return nil + } + reqID, _ := res.Request.Context().Value(proxy.ReqIDKey).(uuid.UUID) if reqID == uuid.Nil { return errors.New("reqlog: request is missing ID") diff --git a/pkg/scope/scope.go b/pkg/scope/scope.go new file mode 100644 index 0000000..84c31b1 --- /dev/null +++ b/pkg/scope/scope.go @@ -0,0 +1,96 @@ +package scope + +import ( + "net/http" + "regexp" + "sync" +) + +type Scope struct { + mu sync.Mutex + rules []Rule +} + +type Rule struct { + URL *regexp.Regexp + Header Header + Body *regexp.Regexp +} + +type Header struct { + Key *regexp.Regexp + Value *regexp.Regexp +} + +func New(rules []Rule) *Scope { + s := &Scope{} + if rules != nil { + s.rules = rules + } + return s +} + +func (s *Scope) Rules() []Rule { + return s.rules +} + +func (s *Scope) SetRules(rules []Rule) { + s.mu.Lock() + defer s.mu.Unlock() + + s.rules = rules +} + +func (s *Scope) Match(req *http.Request, body []byte) bool { + // TODO(?): Do we need to lock here as well? + for _, rule := range s.rules { + if matches := rule.Match(req, body); matches { + return true + } + } + + return false +} + +func (r Rule) Match(req *http.Request, body []byte) bool { + if r.URL != nil { + if matches := r.URL.MatchString(req.URL.String()); matches { + return true + } + } + + for key, values := range req.Header { + var keyMatches, valueMatches bool + if r.Header.Key != nil { + if matches := r.Header.Key.MatchString(key); matches { + keyMatches = true + } + } + if r.Header.Value != nil { + for _, value := range values { + if matches := r.Header.Value.MatchString(value); matches { + valueMatches = true + break + } + } + } + // When only key or value is set, match on whatever is set. + // When both are set, both must match. + switch { + case r.Header.Key != nil && r.Header.Value == nil && keyMatches: + return true + case r.Header.Key == nil && r.Header.Value != nil && valueMatches: + return true + case r.Header.Key != nil && r.Header.Value != nil && keyMatches && valueMatches: + return true + } + } + + if r.Body != nil { + if matches := r.Body.Match(body); matches { + return true + } + } + + return false +}