Add request filter for intercept

This commit is contained in:
David Stotijn 2022-03-15 19:32:29 +01:00
parent d051d48941
commit f4074a8060
No known key found for this signature in database
GPG key ID: B23243A9C47CEE2D
18 changed files with 500 additions and 69 deletions

View file

@ -6,6 +6,7 @@ query ActiveProject {
settings {
intercept {
enabled
requestFilter
}
}
}

View file

@ -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 Hettys 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>
</>

View file

@ -1,5 +1,6 @@
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
updateInterceptSettings(input: $input) {
enabled
requestFilter
}
}

View file

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

View file

@ -19,6 +19,9 @@ function createApolloClient() {
},
},
},
ProjectSettings: {
merge: true,
},
},
}),
});

View file

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

View file

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

View file

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

View file

@ -149,10 +149,12 @@ type CancelRequestResult {
input UpdateInterceptSettingsInput {
enabled: Boolean!
requestFilter: String
}
type InterceptSettings {
enabled: Boolean!
requestFilter: String
}
type Query {

View file

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

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

View file

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

View file

@ -1,5 +1,8 @@
package intercept
import "github.com/dstotijn/hetty/pkg/search"
type Settings struct {
Enabled bool
Enabled bool
RequestFilter search.Expression
}

View file

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

View file

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

View file

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

View file

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

View file

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