mirror of
https://github.com/dstotijn/hetty
synced 2024-11-12 23:17:12 +00:00
Add request filter for intercept
This commit is contained in:
parent
d051d48941
commit
f4074a8060
18 changed files with 500 additions and 69 deletions
|
@ -6,6 +6,7 @@ query ActiveProject {
|
|||
settings {
|
||||
intercept {
|
||||
enabled
|
||||
requestFilter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
import { useApolloClient } from "@apollo/client";
|
||||
import { TabContext, TabPanel } from "@mui/lab";
|
||||
import TabList from "@mui/lab/TabList";
|
||||
import { Box, FormControl, FormControlLabel, FormHelperText, Switch, Tab, Typography } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormHelperText,
|
||||
Switch,
|
||||
Tab,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { SwitchBaseProps } from "@mui/material/internal/SwitchBase";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useActiveProject } from "lib/ActiveProjectContext";
|
||||
import Link from "lib/components/Link";
|
||||
import { ActiveProjectDocument, useUpdateInterceptSettingsMutation } from "lib/graphql/generated";
|
||||
import { withoutTypename } from "lib/graphql/omitTypename";
|
||||
|
||||
enum TabValue {
|
||||
Intercept = "intercept",
|
||||
|
@ -16,24 +28,55 @@ enum TabValue {
|
|||
export default function Settings(): JSX.Element {
|
||||
const client = useApolloClient();
|
||||
const activeProject = useActiveProject();
|
||||
const [updateInterceptSettings, updateIntercepSettingsResult] = useUpdateInterceptSettingsMutation();
|
||||
const [updateInterceptSettings, updateIntercepSettingsResult] = useUpdateInterceptSettingsMutation({
|
||||
onCompleted(data) {
|
||||
client.cache.updateQuery({ query: ActiveProjectDocument }, (cachedData) => ({
|
||||
activeProject: {
|
||||
...cachedData.activeProject,
|
||||
settings: {
|
||||
...cachedData.activeProject.settings,
|
||||
intercept: data.updateInterceptSettings,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
setInterceptReqFilter(data.updateInterceptSettings.requestFilter || "");
|
||||
},
|
||||
});
|
||||
|
||||
const [interceptReqFilter, setInterceptReqFilter] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
setInterceptReqFilter(activeProject?.settings.intercept.requestFilter || "");
|
||||
}, [activeProject?.settings.intercept.requestFilter]);
|
||||
|
||||
const handleInterceptEnabled: SwitchBaseProps["onChange"] = (e, checked) => {
|
||||
if (!activeProject) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
const handleInterceptEnabled: SwitchBaseProps["onChange"] = (_, checked) => {
|
||||
updateInterceptSettings({
|
||||
variables: {
|
||||
input: {
|
||||
...withoutTypename(activeProject.settings.intercept),
|
||||
enabled: checked,
|
||||
},
|
||||
},
|
||||
onCompleted(data) {
|
||||
client.cache.updateQuery({ query: ActiveProjectDocument }, (cachedData) => ({
|
||||
activeProject: {
|
||||
...cachedData.activeProject,
|
||||
settings: {
|
||||
intercept: data.updateInterceptSettings,
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
const handleInterceptReqFilter = () => {
|
||||
if (!activeProject) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateInterceptSettings({
|
||||
variables: {
|
||||
input: {
|
||||
...withoutTypename(activeProject.settings.intercept),
|
||||
requestFilter: interceptReqFilter,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -52,7 +95,7 @@ export default function Settings(): JSX.Element {
|
|||
<Typography paragraph sx={{ mb: 4 }}>
|
||||
Settings allow you to tweak the behaviour of Hetty’s features.
|
||||
</Typography>
|
||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||
<Typography variant="h5" sx={{ mb: 2 }}>
|
||||
Project settings
|
||||
</Typography>
|
||||
{!activeProject && (
|
||||
|
@ -86,6 +129,49 @@ export default function Settings(): JSX.Element {
|
|||
<Link href="/proxy/intercept">manual review</Link>.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<Typography variant="h6" sx={{ mt: 3 }}>
|
||||
Rules
|
||||
</Typography>
|
||||
<form>
|
||||
<FormControl sx={{ width: "50%" }}>
|
||||
<TextField
|
||||
label="Request filter"
|
||||
placeholder={`method = "GET" OR url =~ "/foobar"`}
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
value={interceptReqFilter}
|
||||
onChange={(e) => setInterceptReqFilter(e.target.value)}
|
||||
InputProps={{
|
||||
sx: { fontFamily: "'JetBrains Mono', monospace" },
|
||||
autoCorrect: "false",
|
||||
spellCheck: "false",
|
||||
}}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
margin="normal"
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
<FormHelperText>
|
||||
Filter expression to match incoming requests on. When set, only matching requests are intercepted.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="text"
|
||||
color="primary"
|
||||
size="large"
|
||||
sx={{
|
||||
mt: 2,
|
||||
py: 1.8,
|
||||
}}
|
||||
onClick={handleInterceptReqFilter}
|
||||
disabled={updateIntercepSettingsResult.loading}
|
||||
startIcon={updateIntercepSettingsResult.loading ? <CircularProgress size={22} /> : undefined}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</form>
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
|
||||
updateInterceptSettings(input: $input) {
|
||||
enabled
|
||||
requestFilter
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,6 +119,7 @@ export type HttpResponseLog = {
|
|||
export type InterceptSettings = {
|
||||
__typename?: 'InterceptSettings';
|
||||
enabled: Scalars['Boolean'];
|
||||
requestFilter?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type ModifyRequestInput = {
|
||||
|
@ -315,6 +316,7 @@ export type SenderRequestInput = {
|
|||
|
||||
export type UpdateInterceptSettingsInput = {
|
||||
enabled: Scalars['Boolean'];
|
||||
requestFilter?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type CancelRequestMutationVariables = Exact<{
|
||||
|
@ -341,7 +343,7 @@ export type ModifyRequestMutation = { __typename?: 'Mutation', modifyRequest: {
|
|||
export type ActiveProjectQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ActiveProjectQuery = { __typename?: 'Query', activeProject?: { __typename?: 'Project', id: string, name: string, isActive: boolean, settings: { __typename?: 'ProjectSettings', intercept: { __typename?: 'InterceptSettings', enabled: boolean } } } | null };
|
||||
export type ActiveProjectQuery = { __typename?: 'Query', activeProject?: { __typename?: 'Project', id: string, name: string, isActive: boolean, settings: { __typename?: 'ProjectSettings', intercept: { __typename?: 'InterceptSettings', enabled: boolean, requestFilter?: string | null } } } | null };
|
||||
|
||||
export type CloseProjectMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
@ -453,7 +455,7 @@ export type UpdateInterceptSettingsMutationVariables = Exact<{
|
|||
}>;
|
||||
|
||||
|
||||
export type UpdateInterceptSettingsMutation = { __typename?: 'Mutation', updateInterceptSettings: { __typename?: 'InterceptSettings', enabled: boolean } };
|
||||
export type UpdateInterceptSettingsMutation = { __typename?: 'Mutation', updateInterceptSettings: { __typename?: 'InterceptSettings', enabled: boolean, requestFilter?: string | null } };
|
||||
|
||||
export type GetInterceptedRequestsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
@ -579,6 +581,7 @@ export const ActiveProjectDocument = gql`
|
|||
settings {
|
||||
intercept {
|
||||
enabled
|
||||
requestFilter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1244,6 +1247,7 @@ export const UpdateInterceptSettingsDocument = gql`
|
|||
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
|
||||
updateInterceptSettings(input: $input) {
|
||||
enabled
|
||||
requestFilter
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -19,6 +19,9 @@ function createApolloClient() {
|
|||
},
|
||||
},
|
||||
},
|
||||
ProjectSettings: {
|
||||
merge: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -105,7 +105,8 @@ type ComplexityRoot struct {
|
|||
}
|
||||
|
||||
InterceptSettings struct {
|
||||
Enabled func(childComplexity int) int
|
||||
Enabled func(childComplexity int) int
|
||||
RequestFilter func(childComplexity int) int
|
||||
}
|
||||
|
||||
ModifyRequestResult struct {
|
||||
|
@ -438,6 +439,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||
|
||||
return e.complexity.InterceptSettings.Enabled(childComplexity), true
|
||||
|
||||
case "InterceptSettings.requestFilter":
|
||||
if e.complexity.InterceptSettings.RequestFilter == nil {
|
||||
break
|
||||
}
|
||||
|
||||
return e.complexity.InterceptSettings.RequestFilter(childComplexity), true
|
||||
|
||||
case "ModifyRequestResult.success":
|
||||
if e.complexity.ModifyRequestResult.Success == nil {
|
||||
break
|
||||
|
@ -1057,10 +1065,12 @@ type CancelRequestResult {
|
|||
|
||||
input UpdateInterceptSettingsInput {
|
||||
enabled: Boolean!
|
||||
requestFilter: String
|
||||
}
|
||||
|
||||
type InterceptSettings {
|
||||
enabled: Boolean!
|
||||
requestFilter: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
@ -2440,6 +2450,38 @@ func (ec *executionContext) _InterceptSettings_enabled(ctx context.Context, fiel
|
|||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
||||
}
|
||||
|
||||
func (ec *executionContext) _InterceptSettings_requestFilter(ctx context.Context, field graphql.CollectedField, obj *InterceptSettings) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ec.Error(ctx, ec.Recover(ctx, r))
|
||||
ret = graphql.Null
|
||||
}
|
||||
}()
|
||||
fc := &graphql.FieldContext{
|
||||
Object: "InterceptSettings",
|
||||
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.RequestFilter, 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) _ModifyRequestResult_success(ctx context.Context, field graphql.CollectedField, obj *ModifyRequestResult) (ret graphql.Marshaler) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
|
@ -5632,6 +5674,14 @@ func (ec *executionContext) unmarshalInputUpdateInterceptSettingsInput(ctx conte
|
|||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
case "requestFilter":
|
||||
var err error
|
||||
|
||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestFilter"))
|
||||
it.RequestFilter, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||
if err != nil {
|
||||
return it, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6012,6 +6062,8 @@ func (ec *executionContext) _InterceptSettings(ctx context.Context, sel ast.Sele
|
|||
if out.Values[i] == graphql.Null {
|
||||
invalids++
|
||||
}
|
||||
case "requestFilter":
|
||||
out.Values[i] = ec._InterceptSettings_requestFilter(ctx, field, obj)
|
||||
default:
|
||||
panic("unknown field " + strconv.Quote(field.Name))
|
||||
}
|
||||
|
|
|
@ -83,7 +83,8 @@ type HTTPResponseLog struct {
|
|||
}
|
||||
|
||||
type InterceptSettings struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RequestFilter *string `json:"requestFilter"`
|
||||
}
|
||||
|
||||
type ModifyRequestInput struct {
|
||||
|
@ -164,7 +165,8 @@ type SenderRequestInput struct {
|
|||
}
|
||||
|
||||
type UpdateInterceptSettingsInput struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RequestFilter *string `json:"requestFilter"`
|
||||
}
|
||||
|
||||
type HTTPMethod string
|
||||
|
|
|
@ -184,11 +184,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) {
|
||||
|
@ -199,11 +197,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) {
|
||||
|
@ -214,16 +210,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),
|
||||
Settings: &ProjectSettings{
|
||||
Intercept: &InterceptSettings{
|
||||
Enabled: p.Settings.InterceptEnabled,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
project := parseProject(r.ProjectService, p)
|
||||
|
||||
return &project, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Projects(ctx context.Context) ([]Project, error) {
|
||||
|
@ -234,11 +223,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
|
||||
|
@ -603,6 +588,15 @@ func (r *mutationResolver) UpdateInterceptSettings(
|
|||
Enabled: input.Enabled,
|
||||
}
|
||||
|
||||
if input.RequestFilter != nil && *input.RequestFilter != "" {
|
||||
expr, err := search.ParseQuery(*input.RequestFilter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse search query: %w", err)
|
||||
}
|
||||
|
||||
settings.RequestFilter = expr
|
||||
}
|
||||
|
||||
err := r.ProjectService.UpdateInterceptSettings(ctx, settings)
|
||||
if errors.Is(err, proj.ErrNoProject) {
|
||||
return nil, noActiveProjectErr(ctx)
|
||||
|
@ -610,9 +604,16 @@ func (r *mutationResolver) UpdateInterceptSettings(
|
|||
return nil, fmt.Errorf("could not update intercept settings: %w", err)
|
||||
}
|
||||
|
||||
return &InterceptSettings{
|
||||
updated := &InterceptSettings{
|
||||
Enabled: settings.Enabled,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if settings.RequestFilter != nil {
|
||||
reqFilter := settings.RequestFilter.String()
|
||||
updated.RequestFilter = &reqFilter
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func parseSenderRequest(req sender.Request) (SenderRequest, error) {
|
||||
|
@ -720,6 +721,26 @@ func parseHTTPRequest(req *http.Request) (HTTPRequest, error) {
|
|||
return httpReq, 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{
|
||||
Enabled: p.Settings.InterceptEnabled,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if p.Settings.InterceptRequestFilter != nil {
|
||||
interceptReqFilter := p.Settings.InterceptRequestFilter.String()
|
||||
project.Settings.Intercept.RequestFilter = &interceptReqFilter
|
||||
}
|
||||
|
||||
return project
|
||||
}
|
||||
|
||||
func stringPtrToRegexp(s *string) (*regexp.Regexp, error) {
|
||||
if s == nil {
|
||||
return nil, nil
|
||||
|
|
|
@ -149,10 +149,12 @@ type CancelRequestResult {
|
|||
|
||||
input UpdateInterceptSettingsInput {
|
||||
enabled: Boolean!
|
||||
requestFilter: String
|
||||
}
|
||||
|
||||
type InterceptSettings {
|
||||
enabled: Boolean!
|
||||
requestFilter: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
|
|
@ -62,7 +62,8 @@ type Settings struct {
|
|||
ReqLogSearchExpr search.Expression
|
||||
|
||||
// Intercept settings
|
||||
InterceptEnabled bool
|
||||
InterceptEnabled bool
|
||||
InterceptRequestFilter search.Expression
|
||||
|
||||
// Sender settings
|
||||
SenderOnlyFindInScope bool
|
||||
|
@ -132,7 +133,8 @@ func (svc *service) CloseProject() error {
|
|||
svc.reqLogSvc.SetBypassOutOfScopeRequests(false)
|
||||
svc.reqLogSvc.SetFindReqsFilter(reqlog.FindRequestsFilter{})
|
||||
svc.interceptSvc.UpdateSettings(intercept.Settings{
|
||||
Enabled: false,
|
||||
Enabled: false,
|
||||
RequestFilter: nil,
|
||||
})
|
||||
svc.senderSvc.SetActiveProjectID(ulid.ULID{})
|
||||
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
|
||||
|
@ -177,7 +179,8 @@ func (svc *service) OpenProject(ctx context.Context, projectID ulid.ULID) (Proje
|
|||
|
||||
// Intercept settings.
|
||||
svc.interceptSvc.UpdateSettings(intercept.Settings{
|
||||
Enabled: project.Settings.InterceptEnabled,
|
||||
Enabled: project.Settings.InterceptEnabled,
|
||||
RequestFilter: project.Settings.InterceptRequestFilter,
|
||||
})
|
||||
|
||||
// Sender settings.
|
||||
|
@ -294,6 +297,7 @@ func (svc *service) UpdateInterceptSettings(ctx context.Context, settings interc
|
|||
}
|
||||
|
||||
project.Settings.InterceptEnabled = settings.Enabled
|
||||
project.Settings.InterceptRequestFilter = settings.RequestFilter
|
||||
|
||||
err = svc.repo.UpsertProject(ctx, project)
|
||||
if err != nil {
|
||||
|
|
229
pkg/proxy/intercept/filter.go
Normal file
229
pkg/proxy/intercept/filter.go
Normal file
|
@ -0,0 +1,229 @@
|
|||
package intercept
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/dstotijn/hetty/pkg/scope"
|
||||
"github.com/dstotijn/hetty/pkg/search"
|
||||
)
|
||||
|
||||
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) {
|
||||
if req.URL == nil {
|
||||
return "", nil
|
||||
}
|
||||
return req.URL.String(), nil
|
||||
},
|
||||
"method": func(req *http.Request) (string, error) { return req.Method, nil },
|
||||
"body": func(req *http.Request) (string, error) {
|
||||
if req.Body == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
return string(body), nil
|
||||
},
|
||||
}
|
||||
|
||||
// MatchRequestFilter returns true if an HTTP request matches the request filter expression.
|
||||
func MatchRequestFilter(req *http.Request, expr search.Expression) (bool, error) {
|
||||
switch e := expr.(type) {
|
||||
case search.PrefixExpression:
|
||||
return matchReqPrefixExpr(req, e)
|
||||
case search.InfixExpression:
|
||||
return matchReqInfixExpr(req, e)
|
||||
case search.StringLiteral:
|
||||
return matchReqStringLiteral(req, e)
|
||||
default:
|
||||
return false, fmt.Errorf("expression type (%T) not supported", expr)
|
||||
}
|
||||
}
|
||||
|
||||
func matchReqPrefixExpr(req *http.Request, expr search.PrefixExpression) (bool, error) {
|
||||
switch expr.Operator {
|
||||
case search.TokOpNot:
|
||||
match, err := MatchRequestFilter(req, expr.Right)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return !match, nil
|
||||
default:
|
||||
return false, errors.New("operator is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
func matchReqInfixExpr(req *http.Request, expr search.InfixExpression) (bool, error) {
|
||||
switch expr.Operator {
|
||||
case search.TokOpAnd:
|
||||
left, err := MatchRequestFilter(req, expr.Left)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
right, err := MatchRequestFilter(req, expr.Right)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return left && right, nil
|
||||
case search.TokOpOr:
|
||||
left, err := MatchRequestFilter(req, expr.Left)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
right, err := MatchRequestFilter(req, expr.Right)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return left || right, nil
|
||||
}
|
||||
|
||||
left, ok := expr.Left.(search.StringLiteral)
|
||||
if !ok {
|
||||
return false, errors.New("left operand must be a string literal")
|
||||
}
|
||||
|
||||
leftVal, err := getMappedStringLiteralFromReq(req, left.Value)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get string literal from request for left operand: %w", err)
|
||||
}
|
||||
|
||||
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe {
|
||||
right, ok := expr.Right.(search.RegexpLiteral)
|
||||
if !ok {
|
||||
return false, errors.New("right operand must be a regular expression")
|
||||
}
|
||||
|
||||
switch expr.Operator {
|
||||
case search.TokOpRe:
|
||||
return right.MatchString(leftVal), nil
|
||||
case search.TokOpNotRe:
|
||||
return !right.MatchString(leftVal), nil
|
||||
}
|
||||
}
|
||||
|
||||
right, ok := expr.Right.(search.StringLiteral)
|
||||
if !ok {
|
||||
return false, errors.New("right operand must be a string literal")
|
||||
}
|
||||
|
||||
rightVal, err := getMappedStringLiteralFromReq(req, right.Value)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get string literal from request for right operand: %w", err)
|
||||
}
|
||||
|
||||
switch expr.Operator {
|
||||
case search.TokOpEq:
|
||||
return leftVal == rightVal, nil
|
||||
case search.TokOpNotEq:
|
||||
return leftVal != rightVal, nil
|
||||
case search.TokOpGt:
|
||||
// TODO(?) attempt to parse as int.
|
||||
return leftVal > rightVal, nil
|
||||
case search.TokOpLt:
|
||||
// TODO(?) attempt to parse as int.
|
||||
return leftVal < rightVal, nil
|
||||
case search.TokOpGtEq:
|
||||
// TODO(?) attempt to parse as int.
|
||||
return leftVal >= rightVal, nil
|
||||
case search.TokOpLtEq:
|
||||
// TODO(?) attempt to parse as int.
|
||||
return leftVal <= rightVal, nil
|
||||
default:
|
||||
return false, errors.New("unsupported operator")
|
||||
}
|
||||
}
|
||||
|
||||
func getMappedStringLiteralFromReq(req *http.Request, s string) (string, error) {
|
||||
fn, ok := reqFilterKeyFns[s]
|
||||
if ok {
|
||||
return fn(req)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func matchReqStringLiteral(req *http.Request, strLiteral search.StringLiteral) (bool, error) {
|
||||
for _, fn := range reqFilterKeyFns {
|
||||
value, err := fn(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(value), strings.ToLower(strLiteral.Value)) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func MatchRequestScope(req *http.Request, s *scope.Scope) (bool, error) {
|
||||
for _, rule := range s.Rules() {
|
||||
if rule.URL != nil && req.URL != nil {
|
||||
if matches := rule.URL.MatchString(req.URL.String()); matches {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
for key, values := range req.Header {
|
||||
var keyMatches, valueMatches bool
|
||||
|
||||
if rule.Header.Key != nil {
|
||||
if matches := rule.Header.Key.MatchString(key); matches {
|
||||
keyMatches = true
|
||||
}
|
||||
}
|
||||
|
||||
if rule.Header.Value != nil {
|
||||
for _, value := range values {
|
||||
if matches := rule.Header.Value.MatchString(value); matches {
|
||||
valueMatches = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When only key or value is set, match on whatever is set.
|
||||
// When both are set, both must match.
|
||||
switch {
|
||||
case rule.Header.Key != nil && rule.Header.Value == nil && keyMatches:
|
||||
return true, nil
|
||||
case rule.Header.Key == nil && rule.Header.Value != nil && valueMatches:
|
||||
return true, nil
|
||||
case rule.Header.Key != nil && rule.Header.Value != nil && keyMatches && valueMatches:
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if rule.Body != nil {
|
||||
body, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to read request body: %w", err)
|
||||
}
|
||||
|
||||
req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
if matches := rule.Body.Match(body); matches {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
|
@ -3,6 +3,7 @@ package intercept
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
|
@ -11,6 +12,7 @@ import (
|
|||
|
||||
"github.com/dstotijn/hetty/pkg/log"
|
||||
"github.com/dstotijn/hetty/pkg/proxy"
|
||||
"github.com/dstotijn/hetty/pkg/search"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -28,15 +30,17 @@ type Request struct {
|
|||
}
|
||||
|
||||
type Service struct {
|
||||
mu *sync.RWMutex
|
||||
requests map[ulid.ULID]Request
|
||||
logger log.Logger
|
||||
enabled bool
|
||||
mu *sync.RWMutex
|
||||
requests map[ulid.ULID]Request
|
||||
logger log.Logger
|
||||
enabled bool
|
||||
reqFilter search.Expression
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Logger log.Logger
|
||||
Enabled bool
|
||||
Logger log.Logger
|
||||
Enabled bool
|
||||
RequestFilter search.Expression
|
||||
}
|
||||
|
||||
// RequestIDs implements sort.Interface.
|
||||
|
@ -44,10 +48,11 @@ type RequestIDs []ulid.ULID
|
|||
|
||||
func NewService(cfg Config) *Service {
|
||||
s := &Service{
|
||||
mu: &sync.RWMutex{},
|
||||
requests: make(map[ulid.ULID]Request),
|
||||
logger: cfg.Logger,
|
||||
enabled: cfg.Enabled,
|
||||
mu: &sync.RWMutex{},
|
||||
requests: make(map[ulid.ULID]Request),
|
||||
logger: cfg.Logger,
|
||||
enabled: cfg.Enabled,
|
||||
reqFilter: cfg.RequestFilter,
|
||||
}
|
||||
|
||||
if s.logger == nil {
|
||||
|
@ -102,6 +107,20 @@ func (svc *Service) Intercept(ctx context.Context, req *http.Request) (*http.Req
|
|||
return req, 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.Request)
|
||||
done := make(chan struct{})
|
||||
|
||||
|
@ -197,6 +216,7 @@ func (svc *Service) UpdateSettings(settings Settings) {
|
|||
}
|
||||
|
||||
svc.enabled = settings.Enabled
|
||||
svc.reqFilter = settings.RequestFilter
|
||||
}
|
||||
|
||||
// Request returns an intercepted request by ID. It's safe for concurrent use.
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package intercept
|
||||
|
||||
import "github.com/dstotijn/hetty/pkg/search"
|
||||
|
||||
type Settings struct {
|
||||
Enabled bool
|
||||
Enabled bool
|
||||
RequestFilter search.Expression
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package reqlog
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -100,7 +99,7 @@ func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, erro
|
|||
leftVal := reqLog.getMappedStringLiteral(left.Value)
|
||||
|
||||
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe {
|
||||
right, ok := expr.Right.(*regexp.Regexp)
|
||||
right, ok := expr.Right.(search.RegexpLiteral)
|
||||
if !ok {
|
||||
return false, errors.New("right operand must be a regular expression")
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package search
|
|||
import (
|
||||
"encoding/gob"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
@ -50,13 +51,17 @@ type StringLiteral struct {
|
|||
}
|
||||
|
||||
func (sl StringLiteral) String() string {
|
||||
return sl.Value
|
||||
return strconv.Quote(sl.Value)
|
||||
}
|
||||
|
||||
type RegexpLiteral struct {
|
||||
*regexp.Regexp
|
||||
}
|
||||
|
||||
func (rl RegexpLiteral) String() string {
|
||||
return strconv.Quote(rl.Regexp.String())
|
||||
}
|
||||
|
||||
func (rl RegexpLiteral) MarshalBinary() ([]byte, error) {
|
||||
return []byte(rl.Regexp.String()), nil
|
||||
}
|
||||
|
|
|
@ -208,7 +208,7 @@ func parseInfixExpression(p *Parser, left Expression) (Expression, error) {
|
|||
return nil, fmt.Errorf("could not compile regular expression %q: %w", rightStr.Value, err)
|
||||
}
|
||||
|
||||
right = re
|
||||
right = RegexpLiteral{re}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ func TestParseQuery(t *testing.T) {
|
|||
expectedExpression: InfixExpression{
|
||||
Operator: TokOpRe,
|
||||
Left: StringLiteral{Value: "foo"},
|
||||
Right: regexp.MustCompile("bar"),
|
||||
Right: RegexpLiteral{regexp.MustCompile("bar")},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
|
@ -104,7 +104,7 @@ func TestParseQuery(t *testing.T) {
|
|||
expectedExpression: InfixExpression{
|
||||
Operator: TokOpNotRe,
|
||||
Left: StringLiteral{Value: "foo"},
|
||||
Right: regexp.MustCompile("bar"),
|
||||
Right: RegexpLiteral{regexp.MustCompile("bar")},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
|
@ -197,7 +197,7 @@ func TestParseQuery(t *testing.T) {
|
|||
Right: InfixExpression{
|
||||
Operator: TokOpRe,
|
||||
Left: StringLiteral{Value: "baz"},
|
||||
Right: regexp.MustCompile("yolo"),
|
||||
Right: RegexpLiteral{regexp.MustCompile("yolo")},
|
||||
},
|
||||
},
|
||||
expectedError: nil,
|
||||
|
|
|
@ -3,7 +3,6 @@ package sender
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/oklog/ulid"
|
||||
|
@ -93,7 +92,7 @@ func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) {
|
|||
leftVal := req.getMappedStringLiteral(left.Value)
|
||||
|
||||
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe {
|
||||
right, ok := expr.Right.(*regexp.Regexp)
|
||||
right, ok := expr.Right.(search.RegexpLiteral)
|
||||
if !ok {
|
||||
return false, errors.New("right operand must be a regular expression")
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue