Add scaffolding for scope package

This commit is contained in:
David Stotijn
2020-10-01 21:46:35 +02:00
parent 46caa05d20
commit d48f1f058d
7 changed files with 177 additions and 12 deletions

View File

@ -14,6 +14,7 @@ import (
"github.com/dstotijn/hetty/pkg/db/cayley" "github.com/dstotijn/hetty/pkg/db/cayley"
"github.com/dstotijn/hetty/pkg/proxy" "github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground" "github.com/99designs/gqlgen/graphql/playground"
@ -64,7 +65,11 @@ func main() {
} }
defer db.Close() 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) p, err := proxy.NewProxy(caCert, caKey)
if err != nil { if err != nil {

View File

@ -20,7 +20,8 @@ type queryResolver struct{ *Resolver }
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
func (r *queryResolver) HTTPRequestLogs(ctx context.Context) ([]HTTPRequestLog, error) { 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 { if err != nil {
return nil, fmt.Errorf("could not query repository for requests: %v", err) return nil, fmt.Errorf("could not query repository for requests: %v", err)
} }

View File

@ -14,6 +14,7 @@ import (
"github.com/cayleygraph/cayley" "github.com/cayleygraph/cayley"
"github.com/cayleygraph/cayley/graph" "github.com/cayleygraph/cayley/graph"
"github.com/cayleygraph/cayley/graph/kv" "github.com/cayleygraph/cayley/graph/kv"
cpath "github.com/cayleygraph/cayley/graph/path"
"github.com/cayleygraph/cayley/schema" "github.com/cayleygraph/cayley/schema"
"github.com/cayleygraph/quad" "github.com/cayleygraph/quad"
"github.com/cayleygraph/quad/voc" "github.com/cayleygraph/quad/voc"
@ -21,6 +22,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
) )
type HTTPRequest struct { type HTTPRequest struct {
@ -107,17 +109,33 @@ func (db *Database) Close() error {
return db.store.Close() 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() db.mu.Lock()
defer db.mu.Unlock() defer db.mu.Unlock()
var reqLogs []reqlog.Request var reqLogs []reqlog.Request
var reqs []HTTPRequest var reqs []HTTPRequest
path := cayley.StartPath(db.store, quad.IRI("hy:HTTPRequest")).In(quad.IRI(rdf.Type)) reqPath := cayley.StartPath(db.store, quad.IRI("hy:HTTPRequest")).In(quad.IRI(rdf.Type))
err := path.Iterate(ctx).EachValue(db.store, func(v quad.Value) { 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 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) log.Printf("[ERROR] Could not load sub-graph for http requests: %v", err)
return return
} }

View File

@ -11,6 +11,8 @@ import (
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/google/uuid" "github.com/google/uuid"
) )
@ -27,6 +29,8 @@ type Proxy struct {
// TODO: Add mutex for modifier funcs. // TODO: Add mutex for modifier funcs.
reqModifiers []RequestModifyMiddleware reqModifiers []RequestModifyMiddleware
resModifiers []ResponseModifyMiddleware resModifiers []ResponseModifyMiddleware
scope *scope.Scope
} }
// NewProxy returns a new Proxy. // NewProxy returns a new Proxy.

View File

@ -3,11 +3,12 @@ package reqlog
import ( import (
"context" "context"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/google/uuid" "github.com/google/uuid"
) )
type Repository interface { 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) FindRequestLogByID(ctx context.Context, id uuid.UUID) (Request, error)
AddRequestLog(ctx context.Context, reqLog Request) error AddRequestLog(ctx context.Context, reqLog Request) error
AddResponseLog(ctx context.Context, resLog Response) error AddResponseLog(ctx context.Context, resLog Response) error

View File

@ -12,9 +12,15 @@ import (
"time" "time"
"github.com/dstotijn/hetty/pkg/proxy" "github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/google/uuid" "github.com/google/uuid"
) )
type contextKey int
const LogBypassedKey contextKey = 0
var ErrRequestNotFound = errors.New("reqlog: request not found") var ErrRequestNotFound = errors.New("reqlog: request not found")
type Request struct { type Request struct {
@ -33,15 +39,37 @@ type Response struct {
} }
type Service struct { type Service struct {
repo Repository BypassOutOfScopeRequests bool
scope *scope.Scope
repo Repository
} }
func NewService(repo Repository) *Service { type FindRequestsOptions struct {
return &Service{repo} OmitOutOfScope bool
} }
func (svc *Service) FindAllRequests(ctx context.Context) ([]Request, error) { type Config struct {
return svc.repo.FindAllRequestLogs(ctx) 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) { 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)) 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) reqID, _ := req.Context().Value(proxy.ReqIDKey).(uuid.UUID)
if reqID == uuid.Nil { if reqID == uuid.Nil {
log.Println("[ERROR] Request is missing a related request ID") 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 return err
} }
if bypassed, _ := res.Request.Context().Value(LogBypassedKey).(bool); bypassed {
return nil
}
reqID, _ := res.Request.Context().Value(proxy.ReqIDKey).(uuid.UUID) reqID, _ := res.Request.Context().Value(proxy.ReqIDKey).(uuid.UUID)
if reqID == uuid.Nil { if reqID == uuid.Nil {
return errors.New("reqlog: request is missing ID") return errors.New("reqlog: request is missing ID")

96
pkg/scope/scope.go Normal file
View File

@ -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
}