mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Add request filter for intercept
This commit is contained in:
@ -6,6 +6,7 @@ query ActiveProject {
|
|||||||
settings {
|
settings {
|
||||||
intercept {
|
intercept {
|
||||||
enabled
|
enabled
|
||||||
|
requestFilter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,25 @@
|
|||||||
import { useApolloClient } from "@apollo/client";
|
import { useApolloClient } from "@apollo/client";
|
||||||
import { TabContext, TabPanel } from "@mui/lab";
|
import { TabContext, TabPanel } from "@mui/lab";
|
||||||
import TabList from "@mui/lab/TabList";
|
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 { SwitchBaseProps } from "@mui/material/internal/SwitchBase";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useActiveProject } from "lib/ActiveProjectContext";
|
import { useActiveProject } from "lib/ActiveProjectContext";
|
||||||
import Link from "lib/components/Link";
|
import Link from "lib/components/Link";
|
||||||
import { ActiveProjectDocument, useUpdateInterceptSettingsMutation } from "lib/graphql/generated";
|
import { ActiveProjectDocument, useUpdateInterceptSettingsMutation } from "lib/graphql/generated";
|
||||||
|
import { withoutTypename } from "lib/graphql/omitTypename";
|
||||||
|
|
||||||
enum TabValue {
|
enum TabValue {
|
||||||
Intercept = "intercept",
|
Intercept = "intercept",
|
||||||
@ -16,24 +28,55 @@ enum TabValue {
|
|||||||
export default function Settings(): JSX.Element {
|
export default function Settings(): JSX.Element {
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const activeProject = useActiveProject();
|
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({
|
updateInterceptSettings({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
|
...withoutTypename(activeProject.settings.intercept),
|
||||||
enabled: checked,
|
enabled: checked,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onCompleted(data) {
|
});
|
||||||
client.cache.updateQuery({ query: ActiveProjectDocument }, (cachedData) => ({
|
};
|
||||||
activeProject: {
|
|
||||||
...cachedData.activeProject,
|
const handleInterceptReqFilter = () => {
|
||||||
settings: {
|
if (!activeProject) {
|
||||||
intercept: data.updateInterceptSettings,
|
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 }}>
|
<Typography paragraph sx={{ mb: 4 }}>
|
||||||
Settings allow you to tweak the behaviour of Hetty’s features.
|
Settings allow you to tweak the behaviour of Hetty’s features.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="h6" sx={{ mb: 2 }}>
|
<Typography variant="h5" sx={{ mb: 2 }}>
|
||||||
Project settings
|
Project settings
|
||||||
</Typography>
|
</Typography>
|
||||||
{!activeProject && (
|
{!activeProject && (
|
||||||
@ -86,6 +129,49 @@ export default function Settings(): JSX.Element {
|
|||||||
<Link href="/proxy/intercept">manual review</Link>.
|
<Link href="/proxy/intercept">manual review</Link>.
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
</FormControl>
|
</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>
|
</TabPanel>
|
||||||
</TabContext>
|
</TabContext>
|
||||||
</>
|
</>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
|
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
|
||||||
updateInterceptSettings(input: $input) {
|
updateInterceptSettings(input: $input) {
|
||||||
enabled
|
enabled
|
||||||
|
requestFilter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -119,6 +119,7 @@ export type HttpResponseLog = {
|
|||||||
export type InterceptSettings = {
|
export type InterceptSettings = {
|
||||||
__typename?: 'InterceptSettings';
|
__typename?: 'InterceptSettings';
|
||||||
enabled: Scalars['Boolean'];
|
enabled: Scalars['Boolean'];
|
||||||
|
requestFilter?: Maybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModifyRequestInput = {
|
export type ModifyRequestInput = {
|
||||||
@ -315,6 +316,7 @@ export type SenderRequestInput = {
|
|||||||
|
|
||||||
export type UpdateInterceptSettingsInput = {
|
export type UpdateInterceptSettingsInput = {
|
||||||
enabled: Scalars['Boolean'];
|
enabled: Scalars['Boolean'];
|
||||||
|
requestFilter?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CancelRequestMutationVariables = Exact<{
|
export type CancelRequestMutationVariables = Exact<{
|
||||||
@ -341,7 +343,7 @@ export type ModifyRequestMutation = { __typename?: 'Mutation', modifyRequest: {
|
|||||||
export type ActiveProjectQueryVariables = Exact<{ [key: string]: never; }>;
|
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; }>;
|
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; }>;
|
export type GetInterceptedRequestsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@ -579,6 +581,7 @@ export const ActiveProjectDocument = gql`
|
|||||||
settings {
|
settings {
|
||||||
intercept {
|
intercept {
|
||||||
enabled
|
enabled
|
||||||
|
requestFilter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1244,6 +1247,7 @@ export const UpdateInterceptSettingsDocument = gql`
|
|||||||
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
|
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
|
||||||
updateInterceptSettings(input: $input) {
|
updateInterceptSettings(input: $input) {
|
||||||
enabled
|
enabled
|
||||||
|
requestFilter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -19,6 +19,9 @@ function createApolloClient() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
ProjectSettings: {
|
||||||
|
merge: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
@ -105,7 +105,8 @@ type ComplexityRoot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InterceptSettings struct {
|
InterceptSettings struct {
|
||||||
Enabled func(childComplexity int) int
|
Enabled func(childComplexity int) int
|
||||||
|
RequestFilter func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
ModifyRequestResult struct {
|
ModifyRequestResult struct {
|
||||||
@ -438,6 +439,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.InterceptSettings.Enabled(childComplexity), true
|
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":
|
case "ModifyRequestResult.success":
|
||||||
if e.complexity.ModifyRequestResult.Success == nil {
|
if e.complexity.ModifyRequestResult.Success == nil {
|
||||||
break
|
break
|
||||||
@ -1057,10 +1065,12 @@ type CancelRequestResult {
|
|||||||
|
|
||||||
input UpdateInterceptSettingsInput {
|
input UpdateInterceptSettingsInput {
|
||||||
enabled: Boolean!
|
enabled: Boolean!
|
||||||
|
requestFilter: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterceptSettings {
|
type InterceptSettings {
|
||||||
enabled: Boolean!
|
enabled: Boolean!
|
||||||
|
requestFilter: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
@ -2440,6 +2450,38 @@ func (ec *executionContext) _InterceptSettings_enabled(ctx context.Context, fiel
|
|||||||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
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) {
|
func (ec *executionContext) _ModifyRequestResult_success(ctx context.Context, field graphql.CollectedField, obj *ModifyRequestResult) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@ -5632,6 +5674,14 @@ func (ec *executionContext) unmarshalInputUpdateInterceptSettingsInput(ctx conte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return it, err
|
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 {
|
if out.Values[i] == graphql.Null {
|
||||||
invalids++
|
invalids++
|
||||||
}
|
}
|
||||||
|
case "requestFilter":
|
||||||
|
out.Values[i] = ec._InterceptSettings_requestFilter(ctx, field, obj)
|
||||||
default:
|
default:
|
||||||
panic("unknown field " + strconv.Quote(field.Name))
|
panic("unknown field " + strconv.Quote(field.Name))
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,8 @@ type HTTPResponseLog struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InterceptSettings struct {
|
type InterceptSettings struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
RequestFilter *string `json:"requestFilter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModifyRequestInput struct {
|
type ModifyRequestInput struct {
|
||||||
@ -164,7 +165,8 @@ type SenderRequestInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateInterceptSettingsInput struct {
|
type UpdateInterceptSettingsInput struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
|
RequestFilter *string `json:"requestFilter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPMethod string
|
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 nil, fmt.Errorf("could not open project: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Project{
|
project := parseProject(r.ProjectService, p)
|
||||||
ID: p.ID,
|
|
||||||
Name: p.Name,
|
return &project, nil
|
||||||
IsActive: r.ProjectService.IsProjectActive(p.ID),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) OpenProject(ctx context.Context, id ulid.ULID) (*Project, error) {
|
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 nil, fmt.Errorf("could not open project: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Project{
|
project := parseProject(r.ProjectService, p)
|
||||||
ID: p.ID,
|
|
||||||
Name: p.Name,
|
return &project, nil
|
||||||
IsActive: r.ProjectService.IsProjectActive(p.ID),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) ActiveProject(ctx context.Context) (*Project, error) {
|
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 nil, fmt.Errorf("could not open project: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Project{
|
project := parseProject(r.ProjectService, p)
|
||||||
ID: p.ID,
|
|
||||||
Name: p.Name,
|
return &project, nil
|
||||||
IsActive: r.ProjectService.IsProjectActive(p.ID),
|
|
||||||
Settings: &ProjectSettings{
|
|
||||||
Intercept: &InterceptSettings{
|
|
||||||
Enabled: p.Settings.InterceptEnabled,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) Projects(ctx context.Context) ([]Project, error) {
|
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))
|
projects := make([]Project, len(p))
|
||||||
for i, proj := range p {
|
for i, proj := range p {
|
||||||
projects[i] = Project{
|
projects[i] = parseProject(r.ProjectService, proj)
|
||||||
ID: proj.ID,
|
|
||||||
Name: proj.Name,
|
|
||||||
IsActive: r.ProjectService.IsProjectActive(proj.ID),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return projects, nil
|
return projects, nil
|
||||||
@ -603,6 +588,15 @@ func (r *mutationResolver) UpdateInterceptSettings(
|
|||||||
Enabled: input.Enabled,
|
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)
|
err := r.ProjectService.UpdateInterceptSettings(ctx, settings)
|
||||||
if errors.Is(err, proj.ErrNoProject) {
|
if errors.Is(err, proj.ErrNoProject) {
|
||||||
return nil, noActiveProjectErr(ctx)
|
return nil, noActiveProjectErr(ctx)
|
||||||
@ -610,9 +604,16 @@ func (r *mutationResolver) UpdateInterceptSettings(
|
|||||||
return nil, fmt.Errorf("could not update intercept settings: %w", err)
|
return nil, fmt.Errorf("could not update intercept settings: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &InterceptSettings{
|
updated := &InterceptSettings{
|
||||||
Enabled: settings.Enabled,
|
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) {
|
func parseSenderRequest(req sender.Request) (SenderRequest, error) {
|
||||||
@ -720,6 +721,26 @@ func parseHTTPRequest(req *http.Request) (HTTPRequest, error) {
|
|||||||
return httpReq, nil
|
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) {
|
func stringPtrToRegexp(s *string) (*regexp.Regexp, error) {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -149,10 +149,12 @@ type CancelRequestResult {
|
|||||||
|
|
||||||
input UpdateInterceptSettingsInput {
|
input UpdateInterceptSettingsInput {
|
||||||
enabled: Boolean!
|
enabled: Boolean!
|
||||||
|
requestFilter: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterceptSettings {
|
type InterceptSettings {
|
||||||
enabled: Boolean!
|
enabled: Boolean!
|
||||||
|
requestFilter: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
@ -62,7 +62,8 @@ type Settings struct {
|
|||||||
ReqLogSearchExpr search.Expression
|
ReqLogSearchExpr search.Expression
|
||||||
|
|
||||||
// Intercept settings
|
// Intercept settings
|
||||||
InterceptEnabled bool
|
InterceptEnabled bool
|
||||||
|
InterceptRequestFilter search.Expression
|
||||||
|
|
||||||
// Sender settings
|
// Sender settings
|
||||||
SenderOnlyFindInScope bool
|
SenderOnlyFindInScope bool
|
||||||
@ -132,7 +133,8 @@ func (svc *service) CloseProject() error {
|
|||||||
svc.reqLogSvc.SetBypassOutOfScopeRequests(false)
|
svc.reqLogSvc.SetBypassOutOfScopeRequests(false)
|
||||||
svc.reqLogSvc.SetFindReqsFilter(reqlog.FindRequestsFilter{})
|
svc.reqLogSvc.SetFindReqsFilter(reqlog.FindRequestsFilter{})
|
||||||
svc.interceptSvc.UpdateSettings(intercept.Settings{
|
svc.interceptSvc.UpdateSettings(intercept.Settings{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
|
RequestFilter: nil,
|
||||||
})
|
})
|
||||||
svc.senderSvc.SetActiveProjectID(ulid.ULID{})
|
svc.senderSvc.SetActiveProjectID(ulid.ULID{})
|
||||||
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
|
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
|
||||||
@ -177,7 +179,8 @@ func (svc *service) OpenProject(ctx context.Context, projectID ulid.ULID) (Proje
|
|||||||
|
|
||||||
// Intercept settings.
|
// Intercept settings.
|
||||||
svc.interceptSvc.UpdateSettings(intercept.Settings{
|
svc.interceptSvc.UpdateSettings(intercept.Settings{
|
||||||
Enabled: project.Settings.InterceptEnabled,
|
Enabled: project.Settings.InterceptEnabled,
|
||||||
|
RequestFilter: project.Settings.InterceptRequestFilter,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Sender settings.
|
// Sender settings.
|
||||||
@ -294,6 +297,7 @@ func (svc *service) UpdateInterceptSettings(ctx context.Context, settings interc
|
|||||||
}
|
}
|
||||||
|
|
||||||
project.Settings.InterceptEnabled = settings.Enabled
|
project.Settings.InterceptEnabled = settings.Enabled
|
||||||
|
project.Settings.InterceptRequestFilter = settings.RequestFilter
|
||||||
|
|
||||||
err = svc.repo.UpsertProject(ctx, project)
|
err = svc.repo.UpsertProject(ctx, project)
|
||||||
if err != nil {
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
@ -11,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/dstotijn/hetty/pkg/log"
|
"github.com/dstotijn/hetty/pkg/log"
|
||||||
"github.com/dstotijn/hetty/pkg/proxy"
|
"github.com/dstotijn/hetty/pkg/proxy"
|
||||||
|
"github.com/dstotijn/hetty/pkg/search"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -28,15 +30,17 @@ type Request struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
mu *sync.RWMutex
|
mu *sync.RWMutex
|
||||||
requests map[ulid.ULID]Request
|
requests map[ulid.ULID]Request
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
enabled bool
|
enabled bool
|
||||||
|
reqFilter search.Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Logger log.Logger
|
Logger log.Logger
|
||||||
Enabled bool
|
Enabled bool
|
||||||
|
RequestFilter search.Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestIDs implements sort.Interface.
|
// RequestIDs implements sort.Interface.
|
||||||
@ -44,10 +48,11 @@ type RequestIDs []ulid.ULID
|
|||||||
|
|
||||||
func NewService(cfg Config) *Service {
|
func NewService(cfg Config) *Service {
|
||||||
s := &Service{
|
s := &Service{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
requests: make(map[ulid.ULID]Request),
|
requests: make(map[ulid.ULID]Request),
|
||||||
logger: cfg.Logger,
|
logger: cfg.Logger,
|
||||||
enabled: cfg.Enabled,
|
enabled: cfg.Enabled,
|
||||||
|
reqFilter: cfg.RequestFilter,
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.logger == nil {
|
if s.logger == nil {
|
||||||
@ -102,6 +107,20 @@ func (svc *Service) Intercept(ctx context.Context, req *http.Request) (*http.Req
|
|||||||
return req, nil
|
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)
|
ch := make(chan *http.Request)
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
|
||||||
@ -197,6 +216,7 @@ func (svc *Service) UpdateSettings(settings Settings) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
svc.enabled = settings.Enabled
|
svc.enabled = settings.Enabled
|
||||||
|
svc.reqFilter = settings.RequestFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request returns an intercepted request by ID. It's safe for concurrent use.
|
// Request returns an intercepted request by ID. It's safe for concurrent use.
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package intercept
|
package intercept
|
||||||
|
|
||||||
|
import "github.com/dstotijn/hetty/pkg/search"
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
|
RequestFilter search.Expression
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package reqlog
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -100,7 +99,7 @@ func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, erro
|
|||||||
leftVal := reqLog.getMappedStringLiteral(left.Value)
|
leftVal := reqLog.getMappedStringLiteral(left.Value)
|
||||||
|
|
||||||
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe {
|
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe {
|
||||||
right, ok := expr.Right.(*regexp.Regexp)
|
right, ok := expr.Right.(search.RegexpLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("right operand must be a regular expression")
|
return false, errors.New("right operand must be a regular expression")
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package search
|
|||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -50,13 +51,17 @@ type StringLiteral struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (sl StringLiteral) String() string {
|
func (sl StringLiteral) String() string {
|
||||||
return sl.Value
|
return strconv.Quote(sl.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegexpLiteral struct {
|
type RegexpLiteral struct {
|
||||||
*regexp.Regexp
|
*regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rl RegexpLiteral) String() string {
|
||||||
|
return strconv.Quote(rl.Regexp.String())
|
||||||
|
}
|
||||||
|
|
||||||
func (rl RegexpLiteral) MarshalBinary() ([]byte, error) {
|
func (rl RegexpLiteral) MarshalBinary() ([]byte, error) {
|
||||||
return []byte(rl.Regexp.String()), nil
|
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)
|
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{
|
expectedExpression: InfixExpression{
|
||||||
Operator: TokOpRe,
|
Operator: TokOpRe,
|
||||||
Left: StringLiteral{Value: "foo"},
|
Left: StringLiteral{Value: "foo"},
|
||||||
Right: regexp.MustCompile("bar"),
|
Right: RegexpLiteral{regexp.MustCompile("bar")},
|
||||||
},
|
},
|
||||||
expectedError: nil,
|
expectedError: nil,
|
||||||
},
|
},
|
||||||
@ -104,7 +104,7 @@ func TestParseQuery(t *testing.T) {
|
|||||||
expectedExpression: InfixExpression{
|
expectedExpression: InfixExpression{
|
||||||
Operator: TokOpNotRe,
|
Operator: TokOpNotRe,
|
||||||
Left: StringLiteral{Value: "foo"},
|
Left: StringLiteral{Value: "foo"},
|
||||||
Right: regexp.MustCompile("bar"),
|
Right: RegexpLiteral{regexp.MustCompile("bar")},
|
||||||
},
|
},
|
||||||
expectedError: nil,
|
expectedError: nil,
|
||||||
},
|
},
|
||||||
@ -197,7 +197,7 @@ func TestParseQuery(t *testing.T) {
|
|||||||
Right: InfixExpression{
|
Right: InfixExpression{
|
||||||
Operator: TokOpRe,
|
Operator: TokOpRe,
|
||||||
Left: StringLiteral{Value: "baz"},
|
Left: StringLiteral{Value: "baz"},
|
||||||
Right: regexp.MustCompile("yolo"),
|
Right: RegexpLiteral{regexp.MustCompile("yolo")},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedError: nil,
|
expectedError: nil,
|
||||||
|
@ -3,7 +3,6 @@ package sender
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/oklog/ulid"
|
"github.com/oklog/ulid"
|
||||||
@ -93,7 +92,7 @@ func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) {
|
|||||||
leftVal := req.getMappedStringLiteral(left.Value)
|
leftVal := req.getMappedStringLiteral(left.Value)
|
||||||
|
|
||||||
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe {
|
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe {
|
||||||
right, ok := expr.Right.(*regexp.Regexp)
|
right, ok := expr.Right.(search.RegexpLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("right operand must be a regular expression")
|
return false, errors.New("right operand must be a regular expression")
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user