Add support for intercepting HTTP responses

This commit is contained in:
David Stotijn
2022-03-18 16:40:05 +01:00
parent f4074a8060
commit cf55456c42
16 changed files with 1627 additions and 167 deletions

View File

@ -49,6 +49,10 @@ type ComplexityRoot struct {
Success func(childComplexity int) int
}
CancelResponseResult struct {
Success func(childComplexity int) int
}
ClearHTTPRequestLogResult struct {
Success func(childComplexity int) int
}
@ -71,12 +75,13 @@ type ComplexityRoot struct {
}
HTTPRequest struct {
Body func(childComplexity int) int
Headers func(childComplexity int) int
ID func(childComplexity int) int
Method func(childComplexity int) int
Proto func(childComplexity int) int
URL func(childComplexity int) int
Body func(childComplexity int) int
Headers func(childComplexity int) int
ID func(childComplexity int) int
Method func(childComplexity int) int
Proto func(childComplexity int) int
Response func(childComplexity int) int
URL func(childComplexity int) int
}
HTTPRequestLog struct {
@ -95,6 +100,15 @@ type ComplexityRoot struct {
SearchExpression func(childComplexity int) int
}
HTTPResponse struct {
Body func(childComplexity int) int
Headers func(childComplexity int) int
ID func(childComplexity int) int
Proto func(childComplexity int) int
StatusCode func(childComplexity int) int
StatusReason func(childComplexity int) int
}
HTTPResponseLog struct {
Body func(childComplexity int) int
Headers func(childComplexity int) int
@ -113,8 +127,13 @@ type ComplexityRoot struct {
Success func(childComplexity int) int
}
ModifyResponseResult struct {
Success func(childComplexity int) int
}
Mutation struct {
CancelRequest func(childComplexity int, id ulid.ULID) int
CancelResponse func(childComplexity int, requestID ulid.ULID) int
ClearHTTPRequestLog func(childComplexity int) int
CloseProject func(childComplexity int) int
CreateOrUpdateSenderRequest func(childComplexity int, request SenderRequestInput) int
@ -123,6 +142,7 @@ type ComplexityRoot struct {
DeleteProject func(childComplexity int, id ulid.ULID) int
DeleteSenderRequests func(childComplexity int) int
ModifyRequest func(childComplexity int, request ModifyRequestInput) int
ModifyResponse func(childComplexity int, response ModifyResponseInput) int
OpenProject func(childComplexity int, id ulid.ULID) int
SendRequest func(childComplexity int, id ulid.ULID) int
SetHTTPRequestLogFilter func(childComplexity int, filter *HTTPRequestLogFilterInput) int
@ -199,6 +219,8 @@ type MutationResolver interface {
DeleteSenderRequests(ctx context.Context) (*DeleteSenderRequestsResult, error)
ModifyRequest(ctx context.Context, request ModifyRequestInput) (*ModifyRequestResult, error)
CancelRequest(ctx context.Context, id ulid.ULID) (*CancelRequestResult, error)
ModifyResponse(ctx context.Context, response ModifyResponseInput) (*ModifyResponseResult, error)
CancelResponse(ctx context.Context, requestID ulid.ULID) (*CancelResponseResult, error)
UpdateInterceptSettings(ctx context.Context, input UpdateInterceptSettingsInput) (*InterceptSettings, error)
}
type QueryResolver interface {
@ -236,6 +258,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.CancelRequestResult.Success(childComplexity), true
case "CancelResponseResult.success":
if e.complexity.CancelResponseResult.Success == nil {
break
}
return e.complexity.CancelResponseResult.Success(childComplexity), true
case "ClearHTTPRequestLogResult.success":
if e.complexity.ClearHTTPRequestLogResult.Success == nil {
break
@ -313,6 +342,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.HTTPRequest.Proto(childComplexity), true
case "HttpRequest.response":
if e.complexity.HTTPRequest.Response == nil {
break
}
return e.complexity.HTTPRequest.Response(childComplexity), true
case "HttpRequest.url":
if e.complexity.HTTPRequest.URL == nil {
break
@ -390,6 +426,48 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.HTTPRequestLogFilter.SearchExpression(childComplexity), true
case "HttpResponse.body":
if e.complexity.HTTPResponse.Body == nil {
break
}
return e.complexity.HTTPResponse.Body(childComplexity), true
case "HttpResponse.headers":
if e.complexity.HTTPResponse.Headers == nil {
break
}
return e.complexity.HTTPResponse.Headers(childComplexity), true
case "HttpResponse.id":
if e.complexity.HTTPResponse.ID == nil {
break
}
return e.complexity.HTTPResponse.ID(childComplexity), true
case "HttpResponse.proto":
if e.complexity.HTTPResponse.Proto == nil {
break
}
return e.complexity.HTTPResponse.Proto(childComplexity), true
case "HttpResponse.statusCode":
if e.complexity.HTTPResponse.StatusCode == nil {
break
}
return e.complexity.HTTPResponse.StatusCode(childComplexity), true
case "HttpResponse.statusReason":
if e.complexity.HTTPResponse.StatusReason == nil {
break
}
return e.complexity.HTTPResponse.StatusReason(childComplexity), true
case "HttpResponseLog.body":
if e.complexity.HTTPResponseLog.Body == nil {
break
@ -453,6 +531,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ModifyRequestResult.Success(childComplexity), true
case "ModifyResponseResult.success":
if e.complexity.ModifyResponseResult.Success == nil {
break
}
return e.complexity.ModifyResponseResult.Success(childComplexity), true
case "Mutation.cancelRequest":
if e.complexity.Mutation.CancelRequest == nil {
break
@ -465,6 +550,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.CancelRequest(childComplexity, args["id"].(ulid.ULID)), true
case "Mutation.cancelResponse":
if e.complexity.Mutation.CancelResponse == nil {
break
}
args, err := ec.field_Mutation_cancelResponse_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.CancelResponse(childComplexity, args["requestID"].(ulid.ULID)), true
case "Mutation.clearHTTPRequestLog":
if e.complexity.Mutation.ClearHTTPRequestLog == nil {
break
@ -546,6 +643,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.ModifyRequest(childComplexity, args["request"].(ModifyRequestInput)), true
case "Mutation.modifyResponse":
if e.complexity.Mutation.ModifyResponse == nil {
break
}
args, err := ec.field_Mutation_modifyResponse_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.ModifyResponse(childComplexity, args["response"].(ModifyResponseInput)), true
case "Mutation.openProject":
if e.complexity.Mutation.OpenProject == nil {
break
@ -1044,6 +1153,19 @@ type HttpRequest {
proto: HttpProtocol!
headers: [HttpHeader!]!
body: String
response: HttpResponse
}
type HttpResponse {
"""
Will be the same ID as its related request ID.
"""
id: ID!
proto: HttpProtocol!
statusCode: Int!
statusReason: String!
body: String
headers: [HttpHeader!]!
}
input ModifyRequestInput {
@ -1053,6 +1175,7 @@ input ModifyRequestInput {
proto: HttpProtocol!
headers: [HttpHeaderInput!]
body: String
modifyResponse: Boolean
}
type ModifyRequestResult {
@ -1063,6 +1186,23 @@ type CancelRequestResult {
success: Boolean!
}
input ModifyResponseInput {
requestID: ID!
proto: HttpProtocol!
headers: [HttpHeaderInput!]
body: String
statusCode: Int!
statusReason: String!
}
type ModifyResponseResult {
success: Boolean!
}
type CancelResponseResult {
success: Boolean!
}
input UpdateInterceptSettingsInput {
enabled: Boolean!
requestFilter: String
@ -1103,6 +1243,8 @@ type Mutation {
deleteSenderRequests: DeleteSenderRequestsResult!
modifyRequest(request: ModifyRequestInput!): ModifyRequestResult!
cancelRequest(id: ID!): CancelRequestResult!
modifyResponse(response: ModifyResponseInput!): ModifyResponseResult!
cancelResponse(requestID: ID!): CancelResponseResult!
updateInterceptSettings(
input: UpdateInterceptSettingsInput!
): InterceptSettings!
@ -1152,6 +1294,21 @@ func (ec *executionContext) field_Mutation_cancelRequest_args(ctx context.Contex
return args, nil
}
func (ec *executionContext) field_Mutation_cancelResponse_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 ulid.ULID
if tmp, ok := rawArgs["requestID"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestID"))
arg0, err = ec.unmarshalNID2githubᚗcomᚋoklogᚋulidᚐULID(ctx, tmp)
if err != nil {
return nil, err
}
}
args["requestID"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_createOrUpdateSenderRequest_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@ -1227,6 +1384,21 @@ func (ec *executionContext) field_Mutation_modifyRequest_args(ctx context.Contex
return args, nil
}
func (ec *executionContext) field_Mutation_modifyResponse_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 ModifyResponseInput
if tmp, ok := rawArgs["response"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("response"))
arg0, err = ec.unmarshalNModifyResponseInput2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyResponseInput(ctx, tmp)
if err != nil {
return nil, err
}
}
args["response"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_openProject_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@ -1450,6 +1622,41 @@ func (ec *executionContext) _CancelRequestResult_success(ctx context.Context, fi
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _CancelResponseResult_success(ctx context.Context, field graphql.CollectedField, obj *CancelResponseResult) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "CancelResponseResult",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: 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.Success, 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.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _ClearHTTPRequestLogResult_success(ctx context.Context, field graphql.CollectedField, obj *ClearHTTPRequestLogResult) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -1867,6 +2074,38 @@ func (ec *executionContext) _HttpRequest_body(ctx context.Context, field graphql
return ec.marshalOString2ᚖstring(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,
IsResolver: 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ᚋhettyᚋpkgᚋapiᚐHTTPResponse(ctx, field.Selections, res)
}
func (ec *executionContext) _HttpRequestLog_id(ctx context.Context, field graphql.CollectedField, obj *HTTPRequestLog) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -2208,6 +2447,213 @@ func (ec *executionContext) _HttpRequestLogFilter_searchExpression(ctx context.C
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _HttpResponse_id(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,
IsResolver: 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.ID, 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.(ulid.ULID)
fc.Result = res
return ec.marshalNID2githubᚗcomᚋoklogᚋulidᚐULID(ctx, field.Selections, res)
}
func (ec *executionContext) _HttpResponse_proto(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,
IsResolver: 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.Proto, 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.(HTTPProtocol)
fc.Result = res
return ec.marshalNHttpProtocol2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPProtocol(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,
IsResolver: 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_statusReason(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,
IsResolver: 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.StatusReason, 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) _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,
IsResolver: 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) _HttpResponse_headers(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,
IsResolver: 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.Headers, 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.([]HTTPHeader)
fc.Result = res
return ec.marshalNHttpHeader2ᚕgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPHeaderᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _HttpResponseLog_id(ctx context.Context, field graphql.CollectedField, obj *HTTPResponseLog) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -2517,6 +2963,41 @@ func (ec *executionContext) _ModifyRequestResult_success(ctx context.Context, fi
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _ModifyResponseResult_success(ctx context.Context, field graphql.CollectedField, obj *ModifyResponseResult) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "ModifyResponseResult",
Field: field,
Args: nil,
IsMethod: false,
IsResolver: 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.Success, 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.(bool)
fc.Result = res
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_createProject(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -3072,6 +3553,90 @@ func (ec *executionContext) _Mutation_cancelRequest(ctx context.Context, field g
return ec.marshalNCancelRequestResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelRequestResult(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_modifyResponse(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_modifyResponse_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().ModifyResponse(rctx, args["response"].(ModifyResponseInput))
})
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.(*ModifyResponseResult)
fc.Result = res
return ec.marshalNModifyResponseResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyResponseResult(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_cancelResponse(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_cancelResponse_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().CancelResponse(rctx, args["requestID"].(ulid.ULID))
})
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.(*CancelResponseResult)
fc.Result = res
return ec.marshalNCancelResponseResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelResponseResult(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_updateInterceptSettings(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -5487,6 +6052,77 @@ func (ec *executionContext) unmarshalInputModifyRequestInput(ctx context.Context
if err != nil {
return it, err
}
case "modifyResponse":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("modifyResponse"))
it.ModifyResponse, err = ec.unmarshalOBoolean2ᚖbool(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputModifyResponseInput(ctx context.Context, obj interface{}) (ModifyResponseInput, error) {
var it ModifyResponseInput
asMap := map[string]interface{}{}
for k, v := range obj.(map[string]interface{}) {
asMap[k] = v
}
for k, v := range asMap {
switch k {
case "requestID":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestID"))
it.RequestID, err = ec.unmarshalNID2githubᚗcomᚋoklogᚋulidᚐULID(ctx, v)
if err != nil {
return it, err
}
case "proto":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("proto"))
it.Proto, err = ec.unmarshalNHttpProtocol2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPProtocol(ctx, v)
if err != nil {
return it, err
}
case "headers":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("headers"))
it.Headers, err = ec.unmarshalOHttpHeaderInput2ᚕgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPHeaderInputᚄ(ctx, v)
if err != nil {
return it, err
}
case "body":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("body"))
it.Body, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
case "statusCode":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("statusCode"))
it.StatusCode, err = ec.unmarshalNInt2int(ctx, v)
if err != nil {
return it, err
}
case "statusReason":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("statusReason"))
it.StatusReason, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
}
}
@ -5723,6 +6359,33 @@ func (ec *executionContext) _CancelRequestResult(ctx context.Context, sel ast.Se
return out
}
var cancelResponseResultImplementors = []string{"CancelResponseResult"}
func (ec *executionContext) _CancelResponseResult(ctx context.Context, sel ast.SelectionSet, obj *CancelResponseResult) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, cancelResponseResultImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("CancelResponseResult")
case "success":
out.Values[i] = ec._CancelResponseResult_success(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 clearHTTPRequestLogResultImplementors = []string{"ClearHTTPRequestLogResult"}
func (ec *executionContext) _ClearHTTPRequestLogResult(ctx context.Context, sel ast.SelectionSet, obj *ClearHTTPRequestLogResult) graphql.Marshaler {
@ -5901,6 +6564,8 @@ func (ec *executionContext) _HttpRequest(ctx context.Context, sel ast.SelectionS
}
case "body":
out.Values[i] = ec._HttpRequest_body(ctx, field, obj)
case "response":
out.Values[i] = ec._HttpRequest_response(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@ -5997,6 +6662,55 @@ func (ec *executionContext) _HttpRequestLogFilter(ctx context.Context, sel ast.S
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 "id":
out.Values[i] = ec._HttpResponse_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "proto":
out.Values[i] = ec._HttpResponse_proto(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "statusCode":
out.Values[i] = ec._HttpResponse_statusCode(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "statusReason":
out.Values[i] = ec._HttpResponse_statusReason(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "body":
out.Values[i] = ec._HttpResponse_body(ctx, field, obj)
case "headers":
out.Values[i] = ec._HttpResponse_headers(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 httpResponseLogImplementors = []string{"HttpResponseLog"}
func (ec *executionContext) _HttpResponseLog(ctx context.Context, sel ast.SelectionSet, obj *HTTPResponseLog) graphql.Marshaler {
@ -6102,6 +6816,33 @@ func (ec *executionContext) _ModifyRequestResult(ctx context.Context, sel ast.Se
return out
}
var modifyResponseResultImplementors = []string{"ModifyResponseResult"}
func (ec *executionContext) _ModifyResponseResult(ctx context.Context, sel ast.SelectionSet, obj *ModifyResponseResult) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, modifyResponseResultImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("ModifyResponseResult")
case "success":
out.Values[i] = ec._ModifyResponseResult_success(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 mutationImplementors = []string{"Mutation"}
func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@ -6175,6 +6916,16 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
invalids++
}
case "modifyResponse":
out.Values[i] = ec._Mutation_modifyResponse(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "cancelResponse":
out.Values[i] = ec._Mutation_cancelResponse(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "updateInterceptSettings":
out.Values[i] = ec._Mutation_updateInterceptSettings(ctx, field)
if out.Values[i] == graphql.Null {
@ -6832,6 +7583,20 @@ func (ec *executionContext) marshalNCancelRequestResult2ᚖgithubᚗcomᚋdstoti
return ec._CancelRequestResult(ctx, sel, v)
}
func (ec *executionContext) marshalNCancelResponseResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelResponseResult(ctx context.Context, sel ast.SelectionSet, v CancelResponseResult) graphql.Marshaler {
return ec._CancelResponseResult(ctx, sel, &v)
}
func (ec *executionContext) marshalNCancelResponseResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelResponseResult(ctx context.Context, sel ast.SelectionSet, v *CancelResponseResult) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._CancelResponseResult(ctx, sel, v)
}
func (ec *executionContext) marshalNClearHTTPRequestLogResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐClearHTTPRequestLogResult(ctx context.Context, sel ast.SelectionSet, v ClearHTTPRequestLogResult) graphql.Marshaler {
return ec._ClearHTTPRequestLogResult(ctx, sel, &v)
}
@ -7120,6 +7885,25 @@ func (ec *executionContext) marshalNModifyRequestResult2ᚖgithubᚗcomᚋdstoti
return ec._ModifyRequestResult(ctx, sel, v)
}
func (ec *executionContext) unmarshalNModifyResponseInput2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyResponseInput(ctx context.Context, v interface{}) (ModifyResponseInput, error) {
res, err := ec.unmarshalInputModifyResponseInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNModifyResponseResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyResponseResult(ctx context.Context, sel ast.SelectionSet, v ModifyResponseResult) graphql.Marshaler {
return ec._ModifyResponseResult(ctx, sel, &v)
}
func (ec *executionContext) marshalNModifyResponseResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyResponseResult(ctx context.Context, sel ast.SelectionSet, v *ModifyResponseResult) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._ModifyResponseResult(ctx, sel, v)
}
func (ec *executionContext) marshalNProject2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐProject(ctx context.Context, sel ast.SelectionSet, v Project) graphql.Marshaler {
return ec._Project(ctx, sel, &v)
}
@ -7784,6 +8568,13 @@ func (ec *executionContext) unmarshalOHttpRequestLogFilterInput2ᚖgithubᚗcom
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalOHttpResponse2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋ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) marshalOHttpResponseLog2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPResponseLog(ctx context.Context, sel ast.SelectionSet, v *HTTPResponseLog) graphql.Marshaler {
if v == nil {
return graphql.Null

View File

@ -16,6 +16,10 @@ type CancelRequestResult struct {
Success bool `json:"success"`
}
type CancelResponseResult struct {
Success bool `json:"success"`
}
type ClearHTTPRequestLogResult struct {
Success bool `json:"success"`
}
@ -43,12 +47,13 @@ type HTTPHeaderInput struct {
}
type HTTPRequest struct {
ID ulid.ULID `json:"id"`
URL *url.URL `json:"url"`
Method HTTPMethod `json:"method"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeader `json:"headers"`
Body *string `json:"body"`
ID ulid.ULID `json:"id"`
URL *url.URL `json:"url"`
Method HTTPMethod `json:"method"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeader `json:"headers"`
Body *string `json:"body"`
Response *HTTPResponse `json:"response"`
}
type HTTPRequestLog struct {
@ -72,6 +77,16 @@ type HTTPRequestLogFilterInput struct {
SearchExpression *string `json:"searchExpression"`
}
type HTTPResponse struct {
// Will be the same ID as its related request ID.
ID ulid.ULID `json:"id"`
Proto HTTPProtocol `json:"proto"`
StatusCode int `json:"statusCode"`
StatusReason string `json:"statusReason"`
Body *string `json:"body"`
Headers []HTTPHeader `json:"headers"`
}
type HTTPResponseLog struct {
// Will be the same ID as its related request ID.
ID ulid.ULID `json:"id"`
@ -88,18 +103,32 @@ type InterceptSettings struct {
}
type ModifyRequestInput struct {
ID ulid.ULID `json:"id"`
URL *url.URL `json:"url"`
Method HTTPMethod `json:"method"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeaderInput `json:"headers"`
Body *string `json:"body"`
ID ulid.ULID `json:"id"`
URL *url.URL `json:"url"`
Method HTTPMethod `json:"method"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeaderInput `json:"headers"`
Body *string `json:"body"`
ModifyResponse *bool `json:"modifyResponse"`
}
type ModifyRequestResult struct {
Success bool `json:"success"`
}
type ModifyResponseInput struct {
RequestID ulid.ULID `json:"requestID"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeaderInput `json:"headers"`
Body *string `json:"body"`
StatusCode int `json:"statusCode"`
StatusReason string `json:"statusReason"`
}
type ModifyResponseResult struct {
Success bool `json:"success"`
}
type Project struct {
ID ulid.ULID `json:"id"`
Name string `json:"name"`

View File

@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"regexp"
@ -515,36 +516,35 @@ func (r *mutationResolver) DeleteSenderRequests(ctx context.Context) (*DeleteSen
return &DeleteSenderRequestsResult{true}, nil
}
func (r *queryResolver) InterceptedRequests(ctx context.Context) ([]HTTPRequest, error) {
reqs := r.InterceptService.Requests()
httpReqs := make([]HTTPRequest, len(reqs))
func (r *queryResolver) InterceptedRequests(ctx context.Context) (httpReqs []HTTPRequest, err error) {
items := r.InterceptService.Items()
for i, req := range reqs {
req, err := parseHTTPRequest(req)
for _, item := range items {
req, err := parseInterceptItem(item)
if err != nil {
return nil, err
}
httpReqs[i] = req
httpReqs = append(httpReqs, req)
}
return httpReqs, nil
}
func (r *queryResolver) InterceptedRequest(ctx context.Context, id ulid.ULID) (*HTTPRequest, error) {
req, err := r.InterceptService.RequestByID(id)
item, err := r.InterceptService.ItemByID(id)
if errors.Is(err, intercept.ErrRequestNotFound) {
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("could not get request by ID: %w", err)
}
httpReq, err := parseHTTPRequest(req)
req, err := parseInterceptItem(item)
if err != nil {
return nil, err
}
return &httpReq, nil
return &req, nil
}
func (r *mutationResolver) ModifyRequest(ctx context.Context, input ModifyRequestInput) (*ModifyRequestResult, error) {
@ -563,7 +563,7 @@ func (r *mutationResolver) ModifyRequest(ctx context.Context, input ModifyReques
req.Header.Add(header.Key, header.Value)
}
err = r.InterceptService.ModifyRequest(input.ID, req)
err = r.InterceptService.ModifyRequest(input.ID, req, input.ModifyResponse)
if err != nil {
return nil, fmt.Errorf("could not modify http request: %w", err)
}
@ -580,6 +580,47 @@ func (r *mutationResolver) CancelRequest(ctx context.Context, id ulid.ULID) (*Ca
return &CancelRequestResult{Success: true}, nil
}
func (r *mutationResolver) ModifyResponse(
ctx context.Context,
input ModifyResponseInput,
) (*ModifyResponseResult, error) {
res := &http.Response{
Header: make(http.Header),
Status: fmt.Sprintf("%v %v", input.StatusCode, input.StatusReason),
StatusCode: input.StatusCode,
Proto: revHTTPProtocolMap[input.Proto],
}
var ok bool
if res.ProtoMajor, res.ProtoMinor, ok = http.ParseHTTPVersion(res.Proto); !ok {
return nil, fmt.Errorf("malformed HTTP version: %q", res.Proto)
}
if input.Body != nil {
res.Body = io.NopCloser(strings.NewReader(*input.Body))
}
for _, header := range input.Headers {
res.Header.Add(header.Key, header.Value)
}
err := r.InterceptService.ModifyResponse(input.RequestID, res)
if err != nil {
return nil, fmt.Errorf("could not modify http request: %w", err)
}
return &ModifyResponseResult{Success: true}, nil
}
func (r *mutationResolver) CancelResponse(ctx context.Context, requestID ulid.ULID) (*CancelResponseResult, error) {
err := r.InterceptService.CancelResponse(requestID)
if err != nil {
return nil, fmt.Errorf("could not cancel http response: %w", err)
}
return &CancelResponseResult{Success: true}, nil
}
func (r *mutationResolver) UpdateInterceptSettings(
ctx context.Context,
input UpdateInterceptSettingsInput,
@ -721,6 +762,79 @@ func parseHTTPRequest(req *http.Request) (HTTPRequest, error) {
return httpReq, nil
}
func parseHTTPResponse(res *http.Response) (HTTPResponse, error) {
resProto := httpProtocolMap[res.Proto]
if !resProto.IsValid() {
return HTTPResponse{}, fmt.Errorf("http response has invalid protocol: %v", res.Proto)
}
id, ok := proxy.RequestIDFromContext(res.Request.Context())
if !ok {
return HTTPResponse{}, errors.New("http response has missing ID")
}
httpRes := HTTPResponse{
ID: id,
Proto: resProto,
StatusCode: res.StatusCode,
}
statusReasonSubs := strings.SplitN(res.Status, " ", 2)
if len(statusReasonSubs) == 2 {
httpRes.StatusReason = statusReasonSubs[1]
}
if res.Header != nil {
httpRes.Headers = make([]HTTPHeader, 0)
for key, values := range res.Header {
for _, value := range values {
httpRes.Headers = append(httpRes.Headers, HTTPHeader{
Key: key,
Value: value,
})
}
}
}
if res.Body != nil {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return HTTPResponse{}, fmt.Errorf("failed to read response body: %w", err)
}
res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
bodyStr := string(body)
httpRes.Body = &bodyStr
}
return httpRes, nil
}
func parseInterceptItem(item intercept.Item) (req HTTPRequest, err error) {
if item.Response != nil {
req, err = parseHTTPRequest(item.Response.Request)
if err != nil {
return HTTPRequest{}, err
}
res, err := parseHTTPResponse(item.Response)
if err != nil {
return HTTPRequest{}, err
}
req.Response = &res
} else if item.Request != nil {
req, err = parseHTTPRequest(item.Request)
if err != nil {
return HTTPRequest{}, err
}
}
return req, nil
}
func parseProject(projSvc proj.Service, p proj.Project) Project {
project := Project{
ID: p.ID,

View File

@ -128,6 +128,19 @@ type HttpRequest {
proto: HttpProtocol!
headers: [HttpHeader!]!
body: String
response: HttpResponse
}
type HttpResponse {
"""
Will be the same ID as its related request ID.
"""
id: ID!
proto: HttpProtocol!
statusCode: Int!
statusReason: String!
body: String
headers: [HttpHeader!]!
}
input ModifyRequestInput {
@ -137,6 +150,7 @@ input ModifyRequestInput {
proto: HttpProtocol!
headers: [HttpHeaderInput!]
body: String
modifyResponse: Boolean
}
type ModifyRequestResult {
@ -147,6 +161,23 @@ type CancelRequestResult {
success: Boolean!
}
input ModifyResponseInput {
requestID: ID!
proto: HttpProtocol!
headers: [HttpHeaderInput!]
body: String
statusCode: Int!
statusReason: String!
}
type ModifyResponseResult {
success: Boolean!
}
type CancelResponseResult {
success: Boolean!
}
input UpdateInterceptSettingsInput {
enabled: Boolean!
requestFilter: String
@ -187,6 +218,8 @@ type Mutation {
deleteSenderRequests: DeleteSenderRequestsResult!
modifyRequest(request: ModifyRequestInput!): ModifyRequestResult!
cancelRequest(id: ID!): CancelRequestResult!
modifyResponse(response: ModifyResponseInput!): ModifyResponseResult!
cancelResponse(requestID: ID!): CancelResponseResult!
updateInterceptSettings(
input: UpdateInterceptSettingsInput!
): InterceptSettings!

View File

@ -13,6 +13,7 @@ import (
"github.com/dstotijn/hetty/pkg/search"
)
//nolint:unparam
var reqFilterKeyFns = map[string]func(req *http.Request) (string, error){
"proto": func(req *http.Request) (string, error) { return req.Proto, nil },
"url": func(req *http.Request) (string, error) {

View File

@ -16,11 +16,16 @@ import (
)
var (
ErrRequestAborted = errors.New("intercept: request was aborted")
ErrRequestNotFound = errors.New("intercept: request not found")
ErrRequestDone = errors.New("intercept: request is done")
ErrRequestAborted = errors.New("intercept: request was aborted")
ErrRequestNotFound = errors.New("intercept: request not found")
ErrRequestDone = errors.New("intercept: request is done")
ErrResponseNotFound = errors.New("intercept: response not found")
)
type contextKey int
const interceptResponseKey contextKey = 0
// Request represents a server received HTTP request, alongside a channel for sending a modified version of it to the
// routine that's awaiting it. Also contains a channel for receiving a cancellation signal.
type Request struct {
@ -29,9 +34,24 @@ type Request struct {
done <-chan struct{}
}
// Response represents an HTTP response from a proxied request, alongside a channel for sending a modified version of it
// to the routine that's awaiting it. Also contains a channel for receiving a cancellation signal.
type Response struct {
res *http.Response
ch chan<- *http.Response
done <-chan struct{}
}
type Item struct {
Request *http.Request
Response *http.Response
}
type Service struct {
mu *sync.RWMutex
reqMu *sync.RWMutex
resMu *sync.RWMutex
requests map[ulid.ULID]Request
responses map[ulid.ULID]Response
logger log.Logger
enabled bool
reqFilter search.Expression
@ -48,8 +68,10 @@ type RequestIDs []ulid.ULID
func NewService(cfg Config) *Service {
s := &Service{
mu: &sync.RWMutex{},
reqMu: &sync.RWMutex{},
resMu: &sync.RWMutex{},
requests: make(map[ulid.ULID]Request),
responses: make(map[ulid.ULID]Response),
logger: cfg.Logger,
enabled: cfg.Enabled,
reqFilter: cfg.RequestFilter,
@ -62,13 +84,12 @@ func NewService(cfg Config) *Service {
return s
}
// RequestModifier is a proxy.RequestModifyMiddleware for intercepting HTTP
// requests.
// RequestModifier is a proxy.RequestModifyMiddleware for intercepting HTTP requests.
func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
return func(req *http.Request) {
// This is a blocking operation, that gets unblocked when either a modified request is returned or an error
// (typically `context.Canceled`).
modifiedReq, err := svc.Intercept(req.Context(), req)
modifiedReq, err := svc.InterceptRequest(req.Context(), req)
switch {
case errors.Is(err, ErrRequestAborted):
@ -86,24 +107,24 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
svc.logger.Errorw("Failed to intercept request.",
"error", err)
default:
*req = *modifiedReq.WithContext(req.Context())
*req = *modifiedReq
next(req)
}
}
}
// Intercept adds an HTTP request to an array of pending intercepted requests, alongside channels used for sending a
// cancellation signal and receiving a modified request. It's safe for concurrent use.
func (svc *Service) Intercept(ctx context.Context, req *http.Request) (*http.Request, error) {
// InterceptRequest adds an HTTP request to an array of pending intercepted requests, alongside channels used for
// sending a cancellation signal and receiving a modified request. It's safe for concurrent use.
func (svc *Service) InterceptRequest(ctx context.Context, req *http.Request) (*http.Request, error) {
reqID, ok := proxy.RequestIDFromContext(ctx)
if !ok {
svc.logger.Errorw("Failed to intercept: request doesn't have an ID.")
svc.logger.Errorw("Failed to intercept: context doesn't have an ID.")
return req, nil
}
if !svc.enabled {
// If intercept is disabled, return the incoming request as-is.
svc.logger.Debugw("Bypassed interception: module disabled.")
// If request intercept is disabled, return the incoming request as-is.
svc.logger.Debugw("Bypassed request interception: feature disabled.")
return req, nil
}
@ -116,7 +137,7 @@ func (svc *Service) Intercept(ctx context.Context, req *http.Request) (*http.Req
}
if !match {
svc.logger.Debugw("Bypassed interception: request rules don't match.")
svc.logger.Debugw("Bypassed request interception: request rules don't match.")
return req, nil
}
}
@ -124,20 +145,20 @@ func (svc *Service) Intercept(ctx context.Context, req *http.Request) (*http.Req
ch := make(chan *http.Request)
done := make(chan struct{})
svc.mu.Lock()
svc.reqMu.Lock()
svc.requests[reqID] = Request{
req: req,
ch: ch,
done: done,
}
svc.mu.Unlock()
svc.reqMu.Unlock()
// Whatever happens next (modified request returned, or a context cancelled error), any blocked channel senders
// should be unblocked, and the request should be removed from the requests queue.
defer func() {
close(done)
svc.mu.Lock()
defer svc.mu.Unlock()
svc.reqMu.Lock()
defer svc.reqMu.Unlock()
delete(svc.requests, reqID)
}()
@ -155,15 +176,20 @@ func (svc *Service) Intercept(ctx context.Context, req *http.Request) (*http.Req
// ModifyRequest sends a modified HTTP request to the related channel, or returns ErrRequestDone when the request was
// cancelled. It's safe for concurrent use.
func (svc *Service) ModifyRequest(reqID ulid.ULID, modReq *http.Request) error {
svc.mu.RLock()
func (svc *Service) ModifyRequest(reqID ulid.ULID, modReq *http.Request, modifyResponse *bool) error {
svc.reqMu.RLock()
req, ok := svc.requests[reqID]
svc.mu.RUnlock()
svc.reqMu.RUnlock()
if !ok {
return ErrRequestNotFound
}
*modReq = *modReq.WithContext(req.req.Context())
if modifyResponse != nil {
*modReq = *modReq.WithContext(WithInterceptResponse(modReq.Context(), *modifyResponse))
}
select {
case <-req.done:
return ErrRequestDone
@ -174,12 +200,12 @@ func (svc *Service) ModifyRequest(reqID ulid.ULID, modReq *http.Request) error {
// CancelRequest ensures an intercepted request is dropped.
func (svc *Service) CancelRequest(reqID ulid.ULID) error {
return svc.ModifyRequest(reqID, nil)
return svc.ModifyRequest(reqID, nil, nil)
}
func (svc *Service) ClearRequests() {
svc.mu.Lock()
defer svc.mu.Unlock()
svc.reqMu.Lock()
defer svc.reqMu.Unlock()
for _, req := range svc.requests {
select {
@ -189,47 +215,94 @@ func (svc *Service) ClearRequests() {
}
}
// Requests returns a list of pending intercepted requests. It's safe for concurrent use.
func (svc *Service) Requests() []*http.Request {
svc.mu.RLock()
defer svc.mu.RUnlock()
func (svc *Service) ClearResponses() {
svc.resMu.Lock()
defer svc.resMu.Unlock()
for _, res := range svc.responses {
select {
case <-res.done:
case res.ch <- nil:
}
}
}
// Items returns a list of pending items (requests and responses). It's safe for concurrent use.
func (svc *Service) Items() []Item {
svc.reqMu.RLock()
defer svc.reqMu.RUnlock()
svc.resMu.RLock()
defer svc.resMu.RUnlock()
reqIDs := make([]ulid.ULID, 0, len(svc.requests)+len(svc.responses))
ids := make([]ulid.ULID, 0, len(svc.requests))
for id := range svc.requests {
ids = append(ids, id)
reqIDs = append(reqIDs, id)
}
sort.Sort(RequestIDs(ids))
reqs := make([]*http.Request, len(ids))
for i, id := range ids {
reqs[i] = svc.requests[id].req
for id := range svc.responses {
reqIDs = append(reqIDs, id)
}
return reqs
sort.Sort(RequestIDs(reqIDs))
items := make([]Item, len(reqIDs))
for i, id := range reqIDs {
item := Item{}
if req, ok := svc.requests[id]; ok {
item.Request = req.req
}
if res, ok := svc.responses[id]; ok {
item.Response = res.res
}
items[i] = item
}
return items
}
func (svc *Service) UpdateSettings(settings Settings) {
// When updating from `enabled` -> `disabled`, clear any pending reqs.
if svc.enabled && !settings.Enabled {
svc.ClearRequests()
svc.ClearResponses()
}
svc.enabled = settings.Enabled
svc.reqFilter = settings.RequestFilter
}
// Request returns an intercepted request by ID. It's safe for concurrent use.
func (svc *Service) RequestByID(id ulid.ULID) (*http.Request, error) {
svc.mu.RLock()
defer svc.mu.RUnlock()
// ItemByID returns an intercepted item (request and possible response) by ID. It's safe for concurrent use.
func (svc *Service) ItemByID(id ulid.ULID) (Item, error) {
svc.reqMu.RLock()
defer svc.reqMu.RUnlock()
req, ok := svc.requests[id]
if !ok {
return nil, ErrRequestNotFound
svc.resMu.RLock()
defer svc.resMu.RUnlock()
item := Item{}
found := false
if req, ok := svc.requests[id]; ok {
item.Request = req.req
found = true
}
return req.req, nil
if res, ok := svc.responses[id]; ok {
item.Response = res.res
found = true
}
if !found {
return Item{}, ErrRequestNotFound
}
return item, nil
}
func (ids RequestIDs) Len() int {
@ -243,3 +316,124 @@ func (ids RequestIDs) Less(i, j int) bool {
func (ids RequestIDs) Swap(i, j int) {
ids[i], ids[j] = ids[j], ids[i]
}
func WithInterceptResponse(ctx context.Context, value bool) context.Context {
return context.WithValue(ctx, interceptResponseKey, value)
}
func ShouldInterceptResponseFromContext(ctx context.Context) (bool, bool) {
shouldIntercept, ok := ctx.Value(interceptResponseKey).(bool)
return shouldIntercept, ok
}
// ResponseModifier is a proxy.ResponseModifyMiddleware for intercepting HTTP responses.
func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
return func(res *http.Response) error {
// This is a blocking operation, that gets unblocked when either a modified response is returned or an error.
//nolint:bodyclose
modifiedRes, err := svc.InterceptResponse(res.Request.Context(), res)
if err != nil {
return fmt.Errorf("failed to intercept response: %w", err)
}
*res = *modifiedRes
return next(res)
}
}
// InterceptResponse adds an HTTP response to an array of pending intercepted responses, alongside channels used for
// sending a cancellation signal and receiving a modified response. It's safe for concurrent use.
func (svc *Service) InterceptResponse(ctx context.Context, res *http.Response) (*http.Response, error) {
reqID, ok := proxy.RequestIDFromContext(ctx)
if !ok {
svc.logger.Errorw("Failed to intercept: context doesn't have an ID.")
return res, nil
}
shouldIntercept, ok := ShouldInterceptResponseFromContext(ctx)
if ok && !shouldIntercept {
// If the related request explicitly disabled response intercept, return the response as-is.
svc.logger.Debugw("Bypassed response interception: related request explicitly disabled response intercept.")
return res, nil
}
if !svc.enabled {
// If the feature is disabled, return the response as-is.
svc.logger.Debugw("Bypassed response interception: feature disabled.")
return res, nil
}
// if svc.reqFilter != nil {
// match, err := MatchRequestFilter(req, svc.reqFilter)
// if err != nil {
// return nil, fmt.Errorf("intercept: failed to match request rules for request (id: %v): %w",
// reqID.String(), err,
// )
// }
// if !match {
// svc.logger.Debugw("Bypassed interception: request rules don't match.")
// return req, nil
// }
// }
ch := make(chan *http.Response)
done := make(chan struct{})
svc.resMu.Lock()
svc.responses[reqID] = Response{
res: res,
ch: ch,
done: done,
}
svc.resMu.Unlock()
// Whatever happens next (modified response returned, or a context cancelled error), any blocked channel senders
// should be unblocked, and the response should be removed from the responses queue.
defer func() {
close(done)
svc.resMu.Lock()
defer svc.resMu.Unlock()
delete(svc.responses, reqID)
}()
select {
case modRes := <-ch:
if modRes == nil {
return nil, ErrRequestAborted
}
return modRes, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
// ModifyResponse sends a modified HTTP response to the related channel, or returns ErrRequestDone when the related
// request was cancelled. It's safe for concurrent use.
func (svc *Service) ModifyResponse(reqID ulid.ULID, modRes *http.Response) error {
svc.resMu.RLock()
res, ok := svc.responses[reqID]
svc.resMu.RUnlock()
if !ok {
return ErrRequestNotFound
}
if modRes != nil {
modRes.Request = res.res.Request
}
select {
case <-res.done:
return ErrRequestDone
case res.ch <- modRes:
return nil
}
}
// CancelResponse ensures an intercepted response is dropped.
func (svc *Service) CancelResponse(reqID ulid.ULID) error {
return svc.ModifyResponse(reqID, nil)
}

View File

@ -34,7 +34,7 @@ func TestRequestModifier(t *testing.T) {
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
err := svc.ModifyRequest(reqID, nil)
err := svc.ModifyRequest(reqID, nil, nil)
if !errors.Is(err, intercept.ErrRequestNotFound) {
t.Fatalf("expected `intercept.ErrRequestNotFound`, got: %v", err)
}
@ -65,7 +65,10 @@ func TestRequestModifier(t *testing.T) {
time.Sleep(10 * time.Millisecond)
cancel()
err := svc.ModifyRequest(reqID, nil)
modReq := req.Clone(req.Context())
modReq.Header.Set("X-Foo", "bar")
err := svc.ModifyRequest(reqID, modReq, nil)
if !errors.Is(err, intercept.ErrRequestDone) {
t.Fatalf("expected `intercept.ErrRequestDone`, got: %v", err)
}
@ -107,7 +110,7 @@ func TestRequestModifier(t *testing.T) {
// array of intercepted reqs.
time.Sleep(10 * time.Millisecond)
err := svc.ModifyRequest(reqID, modReq)
err := svc.ModifyRequest(reqID, modReq, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

View File

@ -13,8 +13,9 @@ import (
"net/http/httputil"
"time"
"github.com/dstotijn/hetty/pkg/log"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/log"
)
//nolint:gosec
@ -189,9 +190,12 @@ func (p *Proxy) clientTLSConn(conn net.Conn) (*tls.Conn, error) {
}
func (p *Proxy) errorHandler(w http.ResponseWriter, r *http.Request, err error) {
if !errors.Is(err, context.Canceled) {
switch {
case !errors.Is(err, context.Canceled):
p.logger.Errorw("Failed to proxy request.",
"error", err)
case errors.Is(err, context.Canceled):
p.logger.Debugw("Proxy request was cancelled.")
}
w.WriteHeader(http.StatusBadGateway)