Add intercept module

This commit is contained in:
David Stotijn
2022-03-23 14:31:27 +01:00
parent 6ffc55cde3
commit 02408b5196
51 changed files with 5779 additions and 304 deletions

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,14 @@ import (
"github.com/oklog/ulid"
)
type CancelRequestResult struct {
Success bool `json:"success"`
}
type CancelResponseResult struct {
Success bool `json:"success"`
}
type ClearHTTPRequestLogResult struct {
Success bool `json:"success"`
}
@ -38,6 +46,16 @@ type HTTPHeaderInput struct {
Value string `json:"value"`
}
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"`
Response *HTTPResponse `json:"response"`
}
type HTTPRequestLog struct {
ID ulid.ULID `json:"id"`
URL string `json:"url"`
@ -59,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"`
@ -69,10 +97,49 @@ type HTTPResponseLog struct {
Headers []HTTPHeader `json:"headers"`
}
type InterceptSettings struct {
RequestsEnabled bool `json:"requestsEnabled"`
ResponsesEnabled bool `json:"responsesEnabled"`
RequestFilter *string `json:"requestFilter"`
ResponseFilter *string `json:"responseFilter"`
}
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"`
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"`
IsActive bool `json:"isActive"`
ID ulid.ULID `json:"id"`
Name string `json:"name"`
IsActive bool `json:"isActive"`
Settings *ProjectSettings `json:"settings"`
}
type ProjectSettings struct {
Intercept *InterceptSettings `json:"intercept"`
}
type ScopeHeader struct {
@ -128,6 +195,13 @@ type SenderRequestInput struct {
Body *string `json:"body"`
}
type UpdateInterceptSettingsInput struct {
RequestsEnabled bool `json:"requestsEnabled"`
ResponsesEnabled bool `json:"responsesEnabled"`
RequestFilter *string `json:"requestFilter"`
ResponseFilter *string `json:"responseFilter"`
}
type HTTPMethod string
const (

View File

@ -3,9 +3,12 @@ package api
//go:generate go run github.com/99designs/gqlgen
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"regexp"
"strings"
@ -15,6 +18,8 @@ import (
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/proxy/intercept"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
@ -36,6 +41,7 @@ var revHTTPProtocolMap = map[HTTPProtocol]string{
type Resolver struct {
ProjectService proj.Service
RequestLogService reqlog.Service
InterceptService *intercept.Service
SenderService sender.Service
}
@ -179,11 +185,9 @@ func (r *mutationResolver) CreateProject(ctx context.Context, name string) (*Pro
return nil, fmt.Errorf("could not open project: %w", err)
}
return &Project{
ID: p.ID,
Name: p.Name,
IsActive: r.ProjectService.IsProjectActive(p.ID),
}, nil
project := parseProject(r.ProjectService, p)
return &project, nil
}
func (r *mutationResolver) OpenProject(ctx context.Context, id ulid.ULID) (*Project, error) {
@ -194,11 +198,9 @@ func (r *mutationResolver) OpenProject(ctx context.Context, id ulid.ULID) (*Proj
return nil, fmt.Errorf("could not open project: %w", err)
}
return &Project{
ID: p.ID,
Name: p.Name,
IsActive: r.ProjectService.IsProjectActive(p.ID),
}, nil
project := parseProject(r.ProjectService, p)
return &project, nil
}
func (r *queryResolver) ActiveProject(ctx context.Context) (*Project, error) {
@ -209,11 +211,9 @@ func (r *queryResolver) ActiveProject(ctx context.Context) (*Project, error) {
return nil, fmt.Errorf("could not open project: %w", err)
}
return &Project{
ID: p.ID,
Name: p.Name,
IsActive: r.ProjectService.IsProjectActive(p.ID),
}, nil
project := parseProject(r.ProjectService, p)
return &project, nil
}
func (r *queryResolver) Projects(ctx context.Context) ([]Project, error) {
@ -224,11 +224,7 @@ func (r *queryResolver) Projects(ctx context.Context) ([]Project, error) {
projects := make([]Project, len(p))
for i, proj := range p {
projects[i] = Project{
ID: proj.ID,
Name: proj.Name,
IsActive: r.ProjectService.IsProjectActive(proj.ID),
}
projects[i] = parseProject(r.ProjectService, proj)
}
return projects, nil
@ -520,6 +516,166 @@ func (r *mutationResolver) DeleteSenderRequests(ctx context.Context) (*DeleteSen
return &DeleteSenderRequestsResult{true}, nil
}
func (r *queryResolver) InterceptedRequests(ctx context.Context) (httpReqs []HTTPRequest, err error) {
items := r.InterceptService.Items()
for _, item := range items {
req, err := parseInterceptItem(item)
if err != nil {
return nil, err
}
httpReqs = append(httpReqs, req)
}
return httpReqs, nil
}
func (r *queryResolver) InterceptedRequest(ctx context.Context, id ulid.ULID) (*HTTPRequest, error) {
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)
}
req, err := parseInterceptItem(item)
if err != nil {
return nil, err
}
return &req, nil
}
func (r *mutationResolver) ModifyRequest(ctx context.Context, input ModifyRequestInput) (*ModifyRequestResult, error) {
body := ""
if input.Body != nil {
body = *input.Body
}
//nolint:noctx
req, err := http.NewRequest(input.Method.String(), input.URL.String(), strings.NewReader(body))
if err != nil {
return nil, fmt.Errorf("failed to construct HTTP request: %w", err)
}
for _, header := range input.Headers {
req.Header.Add(header.Key, header.Value)
}
err = r.InterceptService.ModifyRequest(input.ID, req, input.ModifyResponse)
if err != nil {
return nil, fmt.Errorf("could not modify http request: %w", err)
}
return &ModifyRequestResult{Success: true}, nil
}
func (r *mutationResolver) CancelRequest(ctx context.Context, id ulid.ULID) (*CancelRequestResult, error) {
err := r.InterceptService.CancelRequest(id)
if err != nil {
return nil, fmt.Errorf("could not cancel http request: %w", err)
}
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)
}
var body string
if input.Body != nil {
body = *input.Body
}
res.Body = io.NopCloser(strings.NewReader(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,
) (*InterceptSettings, error) {
settings := intercept.Settings{
RequestsEnabled: input.RequestsEnabled,
ResponsesEnabled: input.ResponsesEnabled,
}
if input.RequestFilter != nil && *input.RequestFilter != "" {
expr, err := search.ParseQuery(*input.RequestFilter)
if err != nil {
return nil, fmt.Errorf("could not parse request filter: %w", err)
}
settings.RequestFilter = expr
}
if input.ResponseFilter != nil && *input.ResponseFilter != "" {
expr, err := search.ParseQuery(*input.ResponseFilter)
if err != nil {
return nil, fmt.Errorf("could not parse response filter: %w", err)
}
settings.ResponseFilter = expr
}
err := r.ProjectService.UpdateInterceptSettings(ctx, settings)
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
} else if err != nil {
return nil, fmt.Errorf("could not update intercept settings: %w", err)
}
updated := &InterceptSettings{
RequestsEnabled: settings.RequestsEnabled,
ResponsesEnabled: settings.ResponsesEnabled,
}
if settings.RequestFilter != nil {
reqFilter := settings.RequestFilter.String()
updated.RequestFilter = &reqFilter
}
if settings.ResponseFilter != nil {
resFilter := settings.ResponseFilter.String()
updated.ResponseFilter = &resFilter
}
return updated, nil
}
func parseSenderRequest(req sender.Request) (SenderRequest, error) {
method := HTTPMethod(req.Method)
if method != "" && !method.IsValid() {
@ -575,6 +731,155 @@ func parseSenderRequest(req sender.Request) (SenderRequest, error) {
return senderReq, nil
}
func parseHTTPRequest(req *http.Request) (HTTPRequest, error) {
method := HTTPMethod(req.Method)
if method != "" && !method.IsValid() {
return HTTPRequest{}, fmt.Errorf("http request has invalid method: %v", method)
}
reqProto := httpProtocolMap[req.Proto]
if !reqProto.IsValid() {
return HTTPRequest{}, fmt.Errorf("http request has invalid protocol: %v", req.Proto)
}
id, ok := proxy.RequestIDFromContext(req.Context())
if !ok {
return HTTPRequest{}, errors.New("http request has missing ID")
}
httpReq := HTTPRequest{
ID: id,
URL: req.URL,
Method: method,
Proto: HTTPProtocol(req.Proto),
}
if req.Header != nil {
httpReq.Headers = make([]HTTPHeader, 0)
for key, values := range req.Header {
for _, value := range values {
httpReq.Headers = append(httpReq.Headers, HTTPHeader{
Key: key,
Value: value,
})
}
}
}
if req.Body != nil {
body, err := ioutil.ReadAll(req.Body)
if err != nil {
return HTTPRequest{}, fmt.Errorf("failed to read request body: %w", err)
}
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
bodyStr := string(body)
httpReq.Body = &bodyStr
}
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,
Name: p.Name,
IsActive: projSvc.IsProjectActive(p.ID),
Settings: &ProjectSettings{
Intercept: &InterceptSettings{
RequestsEnabled: p.Settings.InterceptRequests,
ResponsesEnabled: p.Settings.InterceptResponses,
},
},
}
if p.Settings.InterceptRequestFilter != nil {
interceptReqFilter := p.Settings.InterceptRequestFilter.String()
project.Settings.Intercept.RequestFilter = &interceptReqFilter
}
if p.Settings.InterceptResponseFilter != nil {
interceptResFilter := p.Settings.InterceptResponseFilter.String()
project.Settings.Intercept.ResponseFilter = &interceptResFilter
}
return project
}
func stringPtrToRegexp(s *string) (*regexp.Regexp, error) {
if s == nil {
return nil, nil

View File

@ -30,6 +30,11 @@ type Project {
id: ID!
name: String!
isActive: Boolean!
settings: ProjectSettings!
}
type ProjectSettings {
intercept: InterceptSettings!
}
type ScopeRule {
@ -116,6 +121,77 @@ type SenderRequestFilter {
searchExpression: String
}
type HttpRequest {
id: ID!
url: URL!
method: HttpMethod!
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 {
id: ID!
url: URL!
method: HttpMethod!
proto: HttpProtocol!
headers: [HttpHeaderInput!]
body: String
modifyResponse: Boolean
}
type ModifyRequestResult {
success: Boolean!
}
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 {
requestsEnabled: Boolean!
responsesEnabled: Boolean!
requestFilter: String
responseFilter: String
}
type InterceptSettings {
requestsEnabled: Boolean!
responsesEnabled: Boolean!
requestFilter: String
responseFilter: String
}
type Query {
httpRequestLog(id: ID!): HttpRequestLog
httpRequestLogs: [HttpRequestLog!]!
@ -125,6 +201,8 @@ type Query {
scope: [ScopeRule!]!
senderRequest(id: ID!): SenderRequest
senderRequests: [SenderRequest!]!
interceptedRequests: [HttpRequest!]!
interceptedRequest(id: ID!): HttpRequest
}
type Mutation {
@ -142,6 +220,13 @@ type Mutation {
createSenderRequestFromHttpRequestLog(id: ID!): SenderRequest!
sendRequest(id: ID!): SenderRequest!
deleteSenderRequests: DeleteSenderRequestsResult!
modifyRequest(request: ModifyRequestInput!): ModifyRequestResult!
cancelRequest(id: ID!): CancelRequestResult!
modifyResponse(response: ModifyResponseInput!): ModifyResponseResult!
cancelResponse(requestID: ID!): CancelResponseResult!
updateInterceptSettings(
input: UpdateInterceptSettingsInput!
): InterceptSettings!
}
enum HttpMethod {