From 211c11be2bfa811a45c872271de27f94176cc165 Mon Sep 17 00:00:00 2001 From: David Stotijn Date: Sat, 19 Sep 2020 01:27:55 +0200 Subject: [PATCH] Finish first working version of `reqlog` --- cmd/main.go | 48 +--- go.mod | 6 +- go.sum | 35 +-- modd.conf | 2 +- pkg/api/generated.go | 574 +++++++++++++++++++++++++++++------------ pkg/api/models_gen.go | 35 ++- pkg/api/resolvers.go | 39 ++- pkg/api/schema.graphql | 19 +- pkg/proxy/proxy.go | 17 +- pkg/reqlog/reqlog.go | 151 +++++++++-- 10 files changed, 646 insertions(+), 280 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 88b64a0..fac600d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,12 +1,9 @@ package main import ( - "bytes" "crypto/tls" "crypto/x509" "flag" - "fmt" - "io/ioutil" "log" "net" "net/http" @@ -44,51 +41,15 @@ func main() { log.Fatalf("[FATAL] Could not parse CA: %v", err) } - reqLogStore := reqlog.NewRequestLogStore() + reqLogService := reqlog.NewService() p, err := proxy.NewProxy(caCert, tlsCA.PrivateKey) if err != nil { log.Fatalf("[FATAL] Could not create Proxy: %v", err) } - p.UseRequestModifier(func(next proxy.RequestModifyFunc) proxy.RequestModifyFunc { - return func(req *http.Request) { - next(req) - clone := req.Clone(req.Context()) - var body []byte - if req.Body != nil { - // TODO: Use io.LimitReader. - body, err := ioutil.ReadAll(req.Body) - if err != nil { - log.Printf("[ERROR] Could not read request body for logging: %v", err) - return - } - req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) - } - reqLogStore.AddRequest(*clone, body) - } - }) - - p.UseResponseModifier(func(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc { - return func(res *http.Response) error { - if err := next(res); err != nil { - return err - } - clone := *res - var body []byte - if res.Body != nil { - // TODO: Use io.LimitReader. - var err error - body, err = ioutil.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("could not read response body: %v", err) - } - res.Body = ioutil.NopCloser(bytes.NewBuffer(body)) - } - reqLogStore.AddResponse(clone, body) - return nil - } - }) + p.UseRequestModifier(reqLogService.RequestModifier) + p.UseResponseModifier(reqLogService.ResponseModifier) var adminHandler http.Handler @@ -116,7 +77,7 @@ func main() { // GraphQL server. adminRouter.Path("/api/playground").Handler(playground.Handler("GraphQL Playground", "/api/graphql")) adminRouter.Path("/api/graphql").Handler(handler.NewDefaultServer(api.NewExecutableSchema(api.Config{Resolvers: &api.Resolver{ - RequestLogStore: &reqLogStore, + RequestLogService: &reqLogService, }}))) // Admin interface. @@ -131,6 +92,7 @@ func main() { TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}, // Disable HTTP/2 } + log.Println("[INFO] Running server on :8080 ...") err = s.ListenAndServe() if err != nil && err != http.ErrServerClosed { log.Fatalf("[FATAL] HTTP server closed: %v", err) diff --git a/go.mod b/go.mod index 34a84fc..7288687 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/dstotijn/gurp go 1.13 require ( - github.com/99designs/gqlgen v0.11.1 - github.com/gorilla/mux v1.7.3 - github.com/vektah/gqlparser v1.2.0 + github.com/99designs/gqlgen v0.11.3 + github.com/google/uuid v1.1.2 + github.com/gorilla/mux v1.7.4 github.com/vektah/gqlparser/v2 v2.0.1 ) diff --git a/go.sum b/go.sum index 70a8215..1480409 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,29 @@ -github.com/99designs/gqlgen v0.10.2 h1:FfjCqIWejHDJeLpQTI0neoZo5vDO3sdo5oNCucet3A0= -github.com/99designs/gqlgen v0.10.2/go.mod h1:aDB7oabSAyZ4kUHLEySsLxnWrBy3lA0A2gWKU+qoHwI= -github.com/99designs/gqlgen v0.11.1 h1:QoSL8/AAJ2T3UOeQbdnBR32JcG4pO08+P/g5jdbFkUg= -github.com/99designs/gqlgen v0.11.1/go.mod h1:vjFOyBZ7NwDl+GdSD4PFn7BQn5Fy7ohJwXn7Vk8zz+c= +github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4= +github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0= github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c h1:TUuUh0Xgj97tLMNtWtNvI9mIV6isjEb9lBMNv+77IGM= github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= @@ -43,23 +48,22 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= +github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= -github.com/vektah/gqlparser v1.2.0 h1:ntkSCX7F5ZJKl+HIVnmLaO269MruasVpNiMOjX9kgo0= -github.com/vektah/gqlparser v1.2.0/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o= github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -81,8 +85,7 @@ golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96b golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/modd.conf b/modd.conf index 03eb405..9655c70 100644 --- a/modd.conf +++ b/modd.conf @@ -1,6 +1,6 @@ @cert = $HOME/.ssh/gurp_cert.pem @key = $HOME/.ssh/gurp_key.pem -@dev = false +@dev = true @adminPath = $PWD/admin/build **/*.go { diff --git a/pkg/api/generated.go b/pkg/api/generated.go index aa899cb..48eec4b 100644 --- a/pkg/api/generated.go +++ b/pkg/api/generated.go @@ -42,19 +42,26 @@ type DirectiveRoot struct { } type ComplexityRoot struct { - Query struct { - GetRequests func(childComplexity int) int - } - - Request struct { + HTTPRequest struct { + Body func(childComplexity int) int Method func(childComplexity int) int + Response func(childComplexity int) int Timestamp func(childComplexity int) int URL func(childComplexity int) int } + + HTTPResponse struct { + Body func(childComplexity int) int + StatusCode func(childComplexity int) int + } + + Query struct { + GetHTTPRequests func(childComplexity int) int + } } type QueryResolver interface { - GetRequests(ctx context.Context) ([]Request, error) + GetHTTPRequests(ctx context.Context) ([]HTTPRequest, error) } type executableSchema struct { @@ -72,33 +79,61 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { - case "Query.getRequests": - if e.complexity.Query.GetRequests == nil { + case "HttpRequest.body": + if e.complexity.HTTPRequest.Body == nil { break } - return e.complexity.Query.GetRequests(childComplexity), true + return e.complexity.HTTPRequest.Body(childComplexity), true - case "Request.method": - if e.complexity.Request.Method == nil { + case "HttpRequest.method": + if e.complexity.HTTPRequest.Method == nil { break } - return e.complexity.Request.Method(childComplexity), true + return e.complexity.HTTPRequest.Method(childComplexity), true - case "Request.timestamp": - if e.complexity.Request.Timestamp == nil { + case "HttpRequest.response": + if e.complexity.HTTPRequest.Response == nil { break } - return e.complexity.Request.Timestamp(childComplexity), true + return e.complexity.HTTPRequest.Response(childComplexity), true - case "Request.url": - if e.complexity.Request.URL == nil { + case "HttpRequest.timestamp": + if e.complexity.HTTPRequest.Timestamp == nil { break } - return e.complexity.Request.URL(childComplexity), true + return e.complexity.HTTPRequest.Timestamp(childComplexity), true + + case "HttpRequest.url": + if e.complexity.HTTPRequest.URL == nil { + break + } + + return e.complexity.HTTPRequest.URL(childComplexity), true + + case "HttpResponse.body": + if e.complexity.HTTPResponse.Body == nil { + break + } + + return e.complexity.HTTPResponse.Body(childComplexity), true + + case "HttpResponse.statusCode": + if e.complexity.HTTPResponse.StatusCode == nil { + break + } + + return e.complexity.HTTPResponse.StatusCode(childComplexity), true + + case "Query.getHttpRequests": + if e.complexity.Query.GetHTTPRequests == nil { + break + } + + return e.complexity.Query.GetHTTPRequests(childComplexity), true } return 0, false @@ -150,23 +185,37 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er } var sources = []*ast.Source{ - &ast.Source{Name: "pkg/api/schema.graphql", Input: `type Request { + &ast.Source{Name: "pkg/api/schema.graphql", Input: `type HttpRequest { url: String! method: HttpMethod! + body: String timestamp: Time! + response: HttpResponse } +type HttpResponse { + statusCode: Int! + body: String +} type Query { - getRequests: [Request!]! + getHttpRequests: [HttpRequest!]! } enum HttpMethod { GET + HEAD POST + PUT + DELETE + CONNECT + OPTIONS + TRACE + PATCH } -scalar Time`, BuiltIn: false}, +scalar Time +`, BuiltIn: false}, } var parsedSchema = gqlparser.MustLoadSchema(sources...) @@ -224,7 +273,236 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** -func (ec *executionContext) _Query_getRequests(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { +func (ec *executionContext) _HttpRequest_url(ctx context.Context, field graphql.CollectedField, obj *HTTPRequest) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "HttpRequest", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.URL, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalNString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) _HttpRequest_method(ctx context.Context, field graphql.CollectedField, obj *HTTPRequest) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "HttpRequest", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Method, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(HTTPMethod) + fc.Result = res + return ec.marshalNHttpMethod2githubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐHTTPMethod(ctx, field.Selections, res) +} + +func (ec *executionContext) _HttpRequest_body(ctx context.Context, field graphql.CollectedField, obj *HTTPRequest) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "HttpRequest", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Body, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _HttpRequest_timestamp(ctx context.Context, field graphql.CollectedField, obj *HTTPRequest) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "HttpRequest", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Timestamp, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(time.Time) + fc.Result = res + return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) _HttpRequest_response(ctx context.Context, field graphql.CollectedField, obj *HTTPRequest) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "HttpRequest", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Response, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*HTTPResponse) + fc.Result = res + return ec.marshalOHttpResponse2ᚖgithubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐHTTPResponse(ctx, field.Selections, res) +} + +func (ec *executionContext) _HttpResponse_statusCode(ctx context.Context, field graphql.CollectedField, obj *HTTPResponse) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "HttpResponse", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.StatusCode, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(int) + fc.Result = res + return ec.marshalNInt2int(ctx, field.Selections, res) +} + +func (ec *executionContext) _HttpResponse_body(ctx context.Context, field graphql.CollectedField, obj *HTTPResponse) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "HttpResponse", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Body, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _Query_getHttpRequests(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -241,7 +519,7 @@ func (ec *executionContext) _Query_getRequests(ctx context.Context, field graphq ctx = graphql.WithFieldContext(ctx, fc) resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().GetRequests(rctx) + return ec.resolvers.Query().GetHTTPRequests(rctx) }) if err != nil { ec.Error(ctx, err) @@ -253,9 +531,9 @@ func (ec *executionContext) _Query_getRequests(ctx context.Context, field graphq } return graphql.Null } - res := resTmp.([]Request) + res := resTmp.([]HTTPRequest) fc.Result = res - return ec.marshalNRequest2ᚕgithubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐRequestᚄ(ctx, field.Selections, res) + return ec.marshalNHttpRequest2ᚕgithubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐHTTPRequestᚄ(ctx, field.Selections, res) } func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -327,108 +605,6 @@ func (ec *executionContext) _Query___schema(ctx context.Context, field graphql.C return ec.marshalO__Schema2ᚖgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐSchema(ctx, field.Selections, res) } -func (ec *executionContext) _Request_url(ctx context.Context, field graphql.CollectedField, obj *Request) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "Request", - Field: field, - Args: nil, - IsMethod: false, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.URL, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(string) - fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) -} - -func (ec *executionContext) _Request_method(ctx context.Context, field graphql.CollectedField, obj *Request) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "Request", - Field: field, - Args: nil, - IsMethod: false, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Method, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(HTTPMethod) - fc.Result = res - return ec.marshalNHttpMethod2githubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐHTTPMethod(ctx, field.Selections, res) -} - -func (ec *executionContext) _Request_timestamp(ctx context.Context, field graphql.CollectedField, obj *Request) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "Request", - Field: field, - Args: nil, - IsMethod: false, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Timestamp, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(time.Time) - fc.Result = res - return ec.marshalNTime2timeᚐTime(ctx, field.Selections, res) -} - func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -1492,6 +1668,76 @@ func (ec *executionContext) ___Type_ofType(ctx context.Context, field graphql.Co // region **************************** object.gotpl **************************** +var httpRequestImplementors = []string{"HttpRequest"} + +func (ec *executionContext) _HttpRequest(ctx context.Context, sel ast.SelectionSet, obj *HTTPRequest) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, httpRequestImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("HttpRequest") + case "url": + out.Values[i] = ec._HttpRequest_url(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "method": + out.Values[i] = ec._HttpRequest_method(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "body": + out.Values[i] = ec._HttpRequest_body(ctx, field, obj) + case "timestamp": + out.Values[i] = ec._HttpRequest_timestamp(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "response": + out.Values[i] = ec._HttpRequest_response(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + +var httpResponseImplementors = []string{"HttpResponse"} + +func (ec *executionContext) _HttpResponse(ctx context.Context, sel ast.SelectionSet, obj *HTTPResponse) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, httpResponseImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("HttpResponse") + case "statusCode": + out.Values[i] = ec._HttpResponse_statusCode(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "body": + out.Values[i] = ec._HttpResponse_body(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var queryImplementors = []string{"Query"} func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { @@ -1507,7 +1753,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Query") - case "getRequests": + case "getHttpRequests": field := field out.Concurrently(i, func() (res graphql.Marshaler) { defer func() { @@ -1515,7 +1761,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Query_getRequests(ctx, field) + res = ec._Query_getHttpRequests(ctx, field) if res == graphql.Null { atomic.AddUint32(&invalids, 1) } @@ -1536,43 +1782,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr return out } -var requestImplementors = []string{"Request"} - -func (ec *executionContext) _Request(ctx context.Context, sel ast.SelectionSet, obj *Request) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, requestImplementors) - - out := graphql.NewFieldSet(fields) - var invalids uint32 - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("Request") - case "url": - out.Values[i] = ec._Request_url(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } - case "method": - out.Values[i] = ec._Request_method(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } - case "timestamp": - out.Values[i] = ec._Request_timestamp(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch() - if invalids > 0 { - return graphql.Null - } - return out -} - var __DirectiveImplementors = []string{"__Directive"} func (ec *executionContext) ___Directive(ctx context.Context, sel ast.SelectionSet, obj *introspection.Directive) graphql.Marshaler { @@ -1841,11 +2050,11 @@ func (ec *executionContext) marshalNHttpMethod2githubᚗcomᚋdstotijnᚋgurpᚋ return v } -func (ec *executionContext) marshalNRequest2githubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐRequest(ctx context.Context, sel ast.SelectionSet, v Request) graphql.Marshaler { - return ec._Request(ctx, sel, &v) +func (ec *executionContext) marshalNHttpRequest2githubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐHTTPRequest(ctx context.Context, sel ast.SelectionSet, v HTTPRequest) graphql.Marshaler { + return ec._HttpRequest(ctx, sel, &v) } -func (ec *executionContext) marshalNRequest2ᚕgithubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐRequestᚄ(ctx context.Context, sel ast.SelectionSet, v []Request) graphql.Marshaler { +func (ec *executionContext) marshalNHttpRequest2ᚕgithubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐHTTPRequestᚄ(ctx context.Context, sel ast.SelectionSet, v []HTTPRequest) graphql.Marshaler { ret := make(graphql.Array, len(v)) var wg sync.WaitGroup isLen1 := len(v) == 1 @@ -1869,7 +2078,7 @@ func (ec *executionContext) marshalNRequest2ᚕgithubᚗcomᚋdstotijnᚋgurpᚋ if !isLen1 { defer wg.Done() } - ret[i] = ec.marshalNRequest2githubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐRequest(ctx, sel, v[i]) + ret[i] = ec.marshalNHttpRequest2githubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐHTTPRequest(ctx, sel, v[i]) } if isLen1 { f(i) @@ -1882,6 +2091,20 @@ func (ec *executionContext) marshalNRequest2ᚕgithubᚗcomᚋdstotijnᚋgurpᚋ return ret } +func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v interface{}) (int, error) { + return graphql.UnmarshalInt(v) +} + +func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler { + res := graphql.MarshalInt(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + } + return res +} + func (ec *executionContext) unmarshalNString2string(ctx context.Context, v interface{}) (string, error) { return graphql.UnmarshalString(v) } @@ -2159,6 +2382,17 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast return ec.marshalOBoolean2bool(ctx, sel, *v) } +func (ec *executionContext) marshalOHttpResponse2githubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐHTTPResponse(ctx context.Context, sel ast.SelectionSet, v HTTPResponse) graphql.Marshaler { + return ec._HttpResponse(ctx, sel, &v) +} + +func (ec *executionContext) marshalOHttpResponse2ᚖgithubᚗcomᚋdstotijnᚋgurpᚋpkgᚋapiᚐHTTPResponse(ctx context.Context, sel ast.SelectionSet, v *HTTPResponse) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._HttpResponse(ctx, sel, v) +} + func (ec *executionContext) unmarshalOString2string(ctx context.Context, v interface{}) (string, error) { return graphql.UnmarshalString(v) } diff --git a/pkg/api/models_gen.go b/pkg/api/models_gen.go index ac26ad4..ebf343c 100644 --- a/pkg/api/models_gen.go +++ b/pkg/api/models_gen.go @@ -9,27 +9,48 @@ import ( "time" ) -type Request struct { - URL string `json:"url"` - Method HTTPMethod `json:"method"` - Timestamp time.Time `json:"timestamp"` +type HTTPRequest struct { + URL string `json:"url"` + Method HTTPMethod `json:"method"` + Body *string `json:"body"` + Timestamp time.Time `json:"timestamp"` + Response *HTTPResponse `json:"response"` +} + +type HTTPResponse struct { + StatusCode int `json:"statusCode"` + Body *string `json:"body"` } type HTTPMethod string const ( - HTTPMethodGet HTTPMethod = "GET" - HTTPMethodPost HTTPMethod = "POST" + HTTPMethodGet HTTPMethod = "GET" + HTTPMethodHead HTTPMethod = "HEAD" + HTTPMethodPost HTTPMethod = "POST" + HTTPMethodPut HTTPMethod = "PUT" + HTTPMethodDelete HTTPMethod = "DELETE" + HTTPMethodConnect HTTPMethod = "CONNECT" + HTTPMethodOptions HTTPMethod = "OPTIONS" + HTTPMethodTrace HTTPMethod = "TRACE" + HTTPMethodPatch HTTPMethod = "PATCH" ) var AllHTTPMethod = []HTTPMethod{ HTTPMethodGet, + HTTPMethodHead, HTTPMethodPost, + HTTPMethodPut, + HTTPMethodDelete, + HTTPMethodConnect, + HTTPMethodOptions, + HTTPMethodTrace, + HTTPMethodPatch, } func (e HTTPMethod) IsValid() bool { switch e { - case HTTPMethodGet, HTTPMethodPost: + case HTTPMethodGet, HTTPMethodHead, HTTPMethodPost, HTTPMethodPut, HTTPMethodDelete, HTTPMethodConnect, HTTPMethodOptions, HTTPMethodTrace, HTTPMethodPatch: return true } return false diff --git a/pkg/api/resolvers.go b/pkg/api/resolvers.go index 74009e1..a48078c 100644 --- a/pkg/api/resolvers.go +++ b/pkg/api/resolvers.go @@ -1,5 +1,7 @@ package api +//go:generate go run github.com/99designs/gqlgen + import ( "context" "fmt" @@ -8,27 +10,44 @@ import ( ) type Resolver struct { - RequestLogStore *reqlog.RequestLogStore + RequestLogService *reqlog.Service } type queryResolver struct{ *Resolver } func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } -func (r *queryResolver) GetRequests(ctx context.Context) ([]Request, error) { - reqs := r.RequestLogStore.Requests() - resp := make([]Request, len(reqs)) +func (r *queryResolver) GetHTTPRequests(ctx context.Context) ([]HTTPRequest, error) { + logs := r.RequestLogService.Requests() + reqs := make([]HTTPRequest, len(logs)) - for i := range resp { - method := HTTPMethod(reqs[i].Request.Method) + for i, log := range logs { + method := HTTPMethod(log.Request.Method) if !method.IsValid() { return nil, fmt.Errorf("request has invalid method: %v", method) } - resp[i] = Request{ - URL: reqs[i].Request.URL.String(), - Method: method, + + reqs[i] = HTTPRequest{ + URL: log.Request.URL.String(), + Method: method, + Timestamp: log.Timestamp, + } + + if len(log.Body) > 0 { + reqBody := string(log.Body) + reqs[i].Body = &reqBody + } + + if log.Response != nil { + reqs[i].Response = &HTTPResponse{ + StatusCode: log.Response.Response.StatusCode, + } + if len(log.Response.Body) > 0 { + resBody := string(log.Response.Body) + reqs[i].Response.Body = &resBody + } } } - return resp, nil + return reqs, nil } diff --git a/pkg/api/schema.graphql b/pkg/api/schema.graphql index 82d6d8e..3a7243b 100644 --- a/pkg/api/schema.graphql +++ b/pkg/api/schema.graphql @@ -1,17 +1,30 @@ -type Request { +type HttpRequest { url: String! method: HttpMethod! + body: String timestamp: Time! + response: HttpResponse } +type HttpResponse { + statusCode: Int! + body: String +} type Query { - getRequests: [Request!]! + getHttpRequests: [HttpRequest!]! } enum HttpMethod { GET + HEAD POST + PUT + DELETE + CONNECT + OPTIONS + TRACE + PATCH } -scalar Time \ No newline at end of file +scalar Time diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index f238ff3..844c879 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -10,8 +10,14 @@ import ( "net" "net/http" "net/http/httputil" + + "github.com/google/uuid" ) +type contextKey int + +const ReqIDKey contextKey = 0 + // Proxy implements http.Handler and offers MITM behaviour for modifying // HTTP requests and responses. type Proxy struct { @@ -46,6 +52,11 @@ func NewProxy(ca *x509.Certificate, key crypto.PrivateKey) (*Proxy, error) { } func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Add a unique request ID, to be used for correlating responses to requests. + reqID := uuid.New() + ctx := context.WithValue(r.Context(), ReqIDKey, reqID) + r = r.WithContext(ctx) + if r.Method == http.MethodConnect { p.handleConnect(w, r) return @@ -69,6 +80,10 @@ func (p *Proxy) modifyRequest(r *http.Request) { r.URL.Scheme = "https" } + // Setting `X-Forwarded-For` to `nil` ensures that http.ReverseProxy doesn't + // set this header. + r.Header["X-Forwarded-For"] = nil + fn := nopReqModifier for i := len(p.reqModifiers) - 1; i >= 0; i-- { @@ -119,7 +134,7 @@ func (p *Proxy) handleConnect(w http.ResponseWriter, r *http.Request) { l := &OnceAcceptListener{clientConnNotify.Conn} - err = http.Serve(l, p.handler) + err = http.Serve(l, p) if err != nil && err != ErrAlreadyAccepted { log.Printf("[ERROR] Serving HTTP request failed: %v", err) } diff --git a/pkg/reqlog/reqlog.go b/pkg/reqlog/reqlog.go index 465e955..3894c6b 100644 --- a/pkg/reqlog/reqlog.go +++ b/pkg/reqlog/reqlog.go @@ -1,51 +1,150 @@ package reqlog import ( + "bytes" + "compress/gzip" + "errors" + "fmt" + "io/ioutil" + "log" "net/http" "sync" + "time" + + "github.com/dstotijn/gurp/pkg/proxy" + "github.com/google/uuid" ) type Request struct { - Request http.Request - Body []byte + ID uuid.UUID + Request http.Request + Body []byte + Timestamp time.Time + Response *Response } -type response struct { - res http.Response - body []byte +type Response struct { + Response http.Response + Body []byte + Timestamp time.Time } -type RequestLogStore struct { - reqStore []Request - resStore []response - reqMu sync.Mutex - resMu sync.Mutex +type Service struct { + store []Request + mu sync.Mutex } -func NewRequestLogStore() RequestLogStore { - return RequestLogStore{ - reqStore: make([]Request, 0), - resStore: make([]response, 0), +func NewService() Service { + return Service{ + store: make([]Request, 0), } } -func (store *RequestLogStore) AddRequest(req http.Request, body []byte) { - store.reqMu.Lock() - defer store.reqMu.Unlock() +func (svc *Service) Requests() []Request { + // TODO(?): Is locking necessary here? + svc.mu.Lock() + defer svc.mu.Unlock() - store.reqStore = append(store.reqStore, Request{req, body}) + return svc.store } -func (store *RequestLogStore) Requests() []Request { - store.reqMu.Lock() - defer store.reqMu.Unlock() +func (svc *Service) addRequest(reqID uuid.UUID, req http.Request, body []byte) Request { + svc.mu.Lock() + defer svc.mu.Unlock() - return store.reqStore + reqLog := Request{ + ID: reqID, + Request: req, + Body: body, + Timestamp: time.Now(), + } + + svc.store = append(svc.store, reqLog) + + return reqLog } -func (store *RequestLogStore) AddResponse(res http.Response, body []byte) { - store.resMu.Lock() - defer store.resMu.Unlock() +func (svc *Service) addResponse(reqID uuid.UUID, res http.Response, body []byte) error { + svc.mu.Lock() + defer svc.mu.Unlock() - store.resStore = append(store.resStore, response{res, body}) + for i := range svc.store { + if svc.store[i].ID == reqID { + svc.store[i].Response = &Response{ + Response: res, + Body: body, + Timestamp: time.Now(), + } + return nil + } + } + + return fmt.Errorf("no request found with ID: %s", reqID) +} + +func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc { + return func(req *http.Request) { + next(req) + + clone := req.Clone(req.Context()) + var body []byte + if req.Body != nil { + // TODO: Use io.LimitReader. + var err error + body, err = ioutil.ReadAll(req.Body) + if err != nil { + log.Printf("[ERROR] Could not read request body for logging: %v", err) + return + } + req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + } + + reqID, _ := req.Context().Value(proxy.ReqIDKey).(uuid.UUID) + if reqID == uuid.Nil { + log.Println("[ERROR] Request is missing a related request ID") + return + } + + _ = svc.addRequest(reqID, *clone, body) + } +} + +func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc { + return func(res *http.Response) error { + if err := next(res); err != nil { + return err + } + + clone := *res + + // TODO: Use io.LimitReader. + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("reqlog: could not read response body: %v", err) + } + res.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + + if res.Header.Get("Content-Encoding") == "gzip" { + gzipReader, err := gzip.NewReader(bytes.NewBuffer(body)) + if err != nil { + return fmt.Errorf("reqlog: could not create gzip reader: %v", err) + } + defer gzipReader.Close() + body, err = ioutil.ReadAll(gzipReader) + if err != nil { + return fmt.Errorf("reqlog: could not read gzipped response body: %v", err) + } + } + + reqID, _ := res.Request.Context().Value(proxy.ReqIDKey).(uuid.UUID) + if reqID == uuid.Nil { + return errors.New("reqlog: request is missing ID") + } + + if err := svc.addResponse(reqID, clone, body); err != nil { + return fmt.Errorf("reqlog: could not add response: %v", err) + } + + return nil + } }