mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Add intercept response filter
This commit is contained in:
@ -5,8 +5,10 @@ query ActiveProject {
|
|||||||
isActive
|
isActive
|
||||||
settings {
|
settings {
|
||||||
intercept {
|
intercept {
|
||||||
enabled
|
requestsEnabled
|
||||||
|
responsesEnabled
|
||||||
requestFilter
|
requestFilter
|
||||||
|
responseFilter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ function Actions(): JSX.Element {
|
|||||||
|
|
||||||
{clearLogsResult.error && <Alert severity="error">Failed to clear HTTP logs: {clearLogsResult.error}</Alert>}
|
{clearLogsResult.error && <Alert severity="error">Failed to clear HTTP logs: {clearLogsResult.error}</Alert>}
|
||||||
|
|
||||||
{activeProject?.settings.intercept.enabled && (
|
{(activeProject?.settings.intercept.requestsEnabled || activeProject?.settings.intercept.responsesEnabled) && (
|
||||||
<Link href="/proxy/intercept/?id=" passHref>
|
<Link href="/proxy/intercept/?id=" passHref>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
Tab,
|
Tab,
|
||||||
TextField,
|
TextField,
|
||||||
|
TextFieldProps,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { SwitchBaseProps } from "@mui/material/internal/SwitchBase";
|
import { SwitchBaseProps } from "@mui/material/internal/SwitchBase";
|
||||||
@ -25,6 +26,26 @@ enum TabValue {
|
|||||||
Intercept = "intercept",
|
Intercept = "intercept",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FilterTextField(props: TextFieldProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
InputProps={{
|
||||||
|
sx: { fontFamily: "'JetBrains Mono', monospace" },
|
||||||
|
autoCorrect: "false",
|
||||||
|
spellCheck: "false",
|
||||||
|
}}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}}
|
||||||
|
margin="normal"
|
||||||
|
sx={{ mr: 1 }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Settings(): JSX.Element {
|
export default function Settings(): JSX.Element {
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
const activeProject = useActiveProject();
|
const activeProject = useActiveProject();
|
||||||
@ -41,16 +62,22 @@ export default function Settings(): JSX.Element {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setInterceptReqFilter(data.updateInterceptSettings.requestFilter || "");
|
setInterceptReqFilter(data.updateInterceptSettings.requestFilter || "");
|
||||||
|
setInterceptResFilter(data.updateInterceptSettings.responseFilter || "");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [interceptReqFilter, setInterceptReqFilter] = useState("");
|
const [interceptReqFilter, setInterceptReqFilter] = useState("");
|
||||||
|
const [interceptResFilter, setInterceptResFilter] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInterceptReqFilter(activeProject?.settings.intercept.requestFilter || "");
|
setInterceptReqFilter(activeProject?.settings.intercept.requestFilter || "");
|
||||||
}, [activeProject?.settings.intercept.requestFilter]);
|
}, [activeProject?.settings.intercept.requestFilter]);
|
||||||
|
|
||||||
const handleInterceptEnabled: SwitchBaseProps["onChange"] = (e, checked) => {
|
useEffect(() => {
|
||||||
|
setInterceptResFilter(activeProject?.settings.intercept.responseFilter || "");
|
||||||
|
}, [activeProject?.settings.intercept.responseFilter]);
|
||||||
|
|
||||||
|
const handleReqInterceptEnabled: SwitchBaseProps["onChange"] = (e, checked) => {
|
||||||
if (!activeProject) {
|
if (!activeProject) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
@ -60,7 +87,23 @@ export default function Settings(): JSX.Element {
|
|||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
...withoutTypename(activeProject.settings.intercept),
|
...withoutTypename(activeProject.settings.intercept),
|
||||||
enabled: checked,
|
requestsEnabled: checked,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResInterceptEnabled: SwitchBaseProps["onChange"] = (e, checked) => {
|
||||||
|
if (!activeProject) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateInterceptSettings({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
...withoutTypename(activeProject.settings.intercept),
|
||||||
|
responsesEnabled: checked,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -81,6 +124,21 @@ export default function Settings(): JSX.Element {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInterceptResFilter = () => {
|
||||||
|
if (!activeProject) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateInterceptSettings({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
...withoutTypename(activeProject.settings.intercept),
|
||||||
|
responseFilter: interceptResFilter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const [tabValue, setTabValue] = useState(TabValue.Intercept);
|
const [tabValue, setTabValue] = useState(TabValue.Intercept);
|
||||||
|
|
||||||
const tabSx = {
|
const tabSx = {
|
||||||
@ -111,16 +169,19 @@ export default function Settings(): JSX.Element {
|
|||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanel value={TabValue.Intercept} sx={{ px: 0 }}>
|
<TabPanel value={TabValue.Intercept} sx={{ px: 0 }}>
|
||||||
<FormControl>
|
<Typography variant="h6" sx={{ mt: 3, mb: 1 }}>
|
||||||
|
Requests
|
||||||
|
</Typography>
|
||||||
|
<FormControl sx={{ mb: 2 }}>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Switch
|
<Switch
|
||||||
disabled={updateIntercepSettingsResult.loading}
|
disabled={updateIntercepSettingsResult.loading}
|
||||||
onChange={handleInterceptEnabled}
|
onChange={handleReqInterceptEnabled}
|
||||||
checked={activeProject.settings.intercept.enabled}
|
checked={activeProject.settings.intercept.requestsEnabled}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Enable proxy interception"
|
label="Enable request interception"
|
||||||
labelPlacement="start"
|
labelPlacement="start"
|
||||||
sx={{ display: "inline-block", m: 0 }}
|
sx={{ display: "inline-block", m: 0 }}
|
||||||
/>
|
/>
|
||||||
@ -129,28 +190,13 @@ 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>
|
<form>
|
||||||
<FormControl sx={{ width: "50%" }}>
|
<FormControl sx={{ width: "50%" }}>
|
||||||
<TextField
|
<FilterTextField
|
||||||
label="Request filter"
|
label="Request filter"
|
||||||
placeholder={`method = "GET" OR url =~ "/foobar"`}
|
placeholder={`Example: method = "GET" OR url =~ "/foobar"`}
|
||||||
color="primary"
|
|
||||||
variant="outlined"
|
|
||||||
value={interceptReqFilter}
|
value={interceptReqFilter}
|
||||||
onChange={(e) => setInterceptReqFilter(e.target.value)}
|
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>
|
<FormHelperText>
|
||||||
Filter expression to match incoming requests on. When set, only matching requests are intercepted.
|
Filter expression to match incoming requests on. When set, only matching requests are intercepted.
|
||||||
@ -172,6 +218,55 @@ export default function Settings(): JSX.Element {
|
|||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
<Typography variant="h6" sx={{ mt: 3 }}>
|
||||||
|
Responses
|
||||||
|
</Typography>
|
||||||
|
<FormControl sx={{ mb: 2 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
disabled={updateIntercepSettingsResult.loading}
|
||||||
|
onChange={handleResInterceptEnabled}
|
||||||
|
checked={activeProject.settings.intercept.responsesEnabled}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Enable response interception"
|
||||||
|
labelPlacement="start"
|
||||||
|
sx={{ display: "inline-block", m: 0 }}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
When enabled, HTTP responses received by the proxy are stalled for{" "}
|
||||||
|
<Link href="/proxy/intercept">manual review</Link>.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<form>
|
||||||
|
<FormControl sx={{ width: "50%" }}>
|
||||||
|
<FilterTextField
|
||||||
|
label="Response filter"
|
||||||
|
placeholder={`Example: statusCode =~ "^2" OR body =~ "foobar"`}
|
||||||
|
value={interceptResFilter}
|
||||||
|
onChange={(e) => setInterceptResFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
<FormHelperText>
|
||||||
|
Filter expression to match received responses on. When set, only matching responses are intercepted.
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
sx={{
|
||||||
|
mt: 2,
|
||||||
|
py: 1.8,
|
||||||
|
}}
|
||||||
|
onClick={handleInterceptResFilter}
|
||||||
|
disabled={updateIntercepSettingsResult.loading}
|
||||||
|
startIcon={updateIntercepSettingsResult.loading ? <CircularProgress size={22} /> : undefined}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabContext>
|
</TabContext>
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
|
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
|
||||||
updateInterceptSettings(input: $input) {
|
updateInterceptSettings(input: $input) {
|
||||||
enabled
|
requestsEnabled
|
||||||
|
responsesEnabled
|
||||||
requestFilter
|
requestFilter
|
||||||
|
responseFilter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,8 +135,10 @@ export type HttpResponseLog = {
|
|||||||
|
|
||||||
export type InterceptSettings = {
|
export type InterceptSettings = {
|
||||||
__typename?: 'InterceptSettings';
|
__typename?: 'InterceptSettings';
|
||||||
enabled: Scalars['Boolean'];
|
|
||||||
requestFilter?: Maybe<Scalars['String']>;
|
requestFilter?: Maybe<Scalars['String']>;
|
||||||
|
requestsEnabled: Scalars['Boolean'];
|
||||||
|
responseFilter?: Maybe<Scalars['String']>;
|
||||||
|
responsesEnabled: Scalars['Boolean'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModifyRequestInput = {
|
export type ModifyRequestInput = {
|
||||||
@ -359,8 +361,10 @@ export type SenderRequestInput = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateInterceptSettingsInput = {
|
export type UpdateInterceptSettingsInput = {
|
||||||
enabled: Scalars['Boolean'];
|
|
||||||
requestFilter?: InputMaybe<Scalars['String']>;
|
requestFilter?: InputMaybe<Scalars['String']>;
|
||||||
|
requestsEnabled: Scalars['Boolean'];
|
||||||
|
responseFilter?: InputMaybe<Scalars['String']>;
|
||||||
|
responsesEnabled: Scalars['Boolean'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CancelRequestMutationVariables = Exact<{
|
export type CancelRequestMutationVariables = Exact<{
|
||||||
@ -401,7 +405,7 @@ export type ModifyResponseMutation = { __typename?: 'Mutation', modifyResponse:
|
|||||||
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, requestFilter?: string | null } } } | null };
|
export type ActiveProjectQuery = { __typename?: 'Query', activeProject?: { __typename?: 'Project', id: string, name: string, isActive: boolean, settings: { __typename?: 'ProjectSettings', intercept: { __typename?: 'InterceptSettings', requestsEnabled: boolean, responsesEnabled: boolean, requestFilter?: string | null, responseFilter?: string | null } } } | null };
|
||||||
|
|
||||||
export type CloseProjectMutationVariables = Exact<{ [key: string]: never; }>;
|
export type CloseProjectMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@ -513,7 +517,7 @@ export type UpdateInterceptSettingsMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type UpdateInterceptSettingsMutation = { __typename?: 'Mutation', updateInterceptSettings: { __typename?: 'InterceptSettings', enabled: boolean, requestFilter?: string | null } };
|
export type UpdateInterceptSettingsMutation = { __typename?: 'Mutation', updateInterceptSettings: { __typename?: 'InterceptSettings', requestsEnabled: boolean, responsesEnabled: boolean, requestFilter?: string | null, responseFilter?: string | null } };
|
||||||
|
|
||||||
export type GetInterceptedRequestsQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetInterceptedRequestsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@ -715,8 +719,10 @@ export const ActiveProjectDocument = gql`
|
|||||||
isActive
|
isActive
|
||||||
settings {
|
settings {
|
||||||
intercept {
|
intercept {
|
||||||
enabled
|
requestsEnabled
|
||||||
|
responsesEnabled
|
||||||
requestFilter
|
requestFilter
|
||||||
|
responseFilter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1381,8 +1387,10 @@ export type GetSenderRequestsQueryResult = Apollo.QueryResult<GetSenderRequestsQ
|
|||||||
export const UpdateInterceptSettingsDocument = gql`
|
export const UpdateInterceptSettingsDocument = gql`
|
||||||
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
|
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
|
||||||
updateInterceptSettings(input: $input) {
|
updateInterceptSettings(input: $input) {
|
||||||
enabled
|
requestsEnabled
|
||||||
|
responsesEnabled
|
||||||
requestFilter
|
requestFilter
|
||||||
|
responseFilter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -119,8 +119,10 @@ type ComplexityRoot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
InterceptSettings struct {
|
InterceptSettings struct {
|
||||||
Enabled func(childComplexity int) int
|
|
||||||
RequestFilter func(childComplexity int) int
|
RequestFilter func(childComplexity int) int
|
||||||
|
RequestsEnabled func(childComplexity int) int
|
||||||
|
ResponseFilter func(childComplexity int) int
|
||||||
|
ResponsesEnabled func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
ModifyRequestResult struct {
|
ModifyRequestResult struct {
|
||||||
@ -510,13 +512,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.HTTPResponseLog.StatusReason(childComplexity), true
|
return e.complexity.HTTPResponseLog.StatusReason(childComplexity), true
|
||||||
|
|
||||||
case "InterceptSettings.enabled":
|
|
||||||
if e.complexity.InterceptSettings.Enabled == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.InterceptSettings.Enabled(childComplexity), true
|
|
||||||
|
|
||||||
case "InterceptSettings.requestFilter":
|
case "InterceptSettings.requestFilter":
|
||||||
if e.complexity.InterceptSettings.RequestFilter == nil {
|
if e.complexity.InterceptSettings.RequestFilter == nil {
|
||||||
break
|
break
|
||||||
@ -524,6 +519,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.InterceptSettings.RequestFilter(childComplexity), true
|
return e.complexity.InterceptSettings.RequestFilter(childComplexity), true
|
||||||
|
|
||||||
|
case "InterceptSettings.requestsEnabled":
|
||||||
|
if e.complexity.InterceptSettings.RequestsEnabled == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.InterceptSettings.RequestsEnabled(childComplexity), true
|
||||||
|
|
||||||
|
case "InterceptSettings.responseFilter":
|
||||||
|
if e.complexity.InterceptSettings.ResponseFilter == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.InterceptSettings.ResponseFilter(childComplexity), true
|
||||||
|
|
||||||
|
case "InterceptSettings.responsesEnabled":
|
||||||
|
if e.complexity.InterceptSettings.ResponsesEnabled == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.InterceptSettings.ResponsesEnabled(childComplexity), true
|
||||||
|
|
||||||
case "ModifyRequestResult.success":
|
case "ModifyRequestResult.success":
|
||||||
if e.complexity.ModifyRequestResult.Success == nil {
|
if e.complexity.ModifyRequestResult.Success == nil {
|
||||||
break
|
break
|
||||||
@ -1204,13 +1220,17 @@ type CancelResponseResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input UpdateInterceptSettingsInput {
|
input UpdateInterceptSettingsInput {
|
||||||
enabled: Boolean!
|
requestsEnabled: Boolean!
|
||||||
|
responsesEnabled: Boolean!
|
||||||
requestFilter: String
|
requestFilter: String
|
||||||
|
responseFilter: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterceptSettings {
|
type InterceptSettings {
|
||||||
enabled: Boolean!
|
requestsEnabled: Boolean!
|
||||||
|
responsesEnabled: Boolean!
|
||||||
requestFilter: String
|
requestFilter: String
|
||||||
|
responseFilter: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
@ -2861,7 +2881,7 @@ func (ec *executionContext) _HttpResponseLog_headers(ctx context.Context, field
|
|||||||
return ec.marshalNHttpHeader2ᚕgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPHeaderᚄ(ctx, field.Selections, res)
|
return ec.marshalNHttpHeader2ᚕgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPHeaderᚄ(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _InterceptSettings_enabled(ctx context.Context, field graphql.CollectedField, obj *InterceptSettings) (ret graphql.Marshaler) {
|
func (ec *executionContext) _InterceptSettings_requestsEnabled(ctx context.Context, field graphql.CollectedField, obj *InterceptSettings) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
ec.Error(ctx, ec.Recover(ctx, r))
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
@ -2879,7 +2899,42 @@ func (ec *executionContext) _InterceptSettings_enabled(ctx context.Context, fiel
|
|||||||
ctx = graphql.WithFieldContext(ctx, fc)
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
ctx = rctx // use context from middleware stack in children
|
ctx = rctx // use context from middleware stack in children
|
||||||
return obj.Enabled, nil
|
return obj.RequestsEnabled, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(bool)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _InterceptSettings_responsesEnabled(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.ResponsesEnabled, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ec.Error(ctx, err)
|
ec.Error(ctx, err)
|
||||||
@ -2928,6 +2983,38 @@ func (ec *executionContext) _InterceptSettings_requestFilter(ctx context.Context
|
|||||||
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _InterceptSettings_responseFilter(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.ResponseFilter, 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 {
|
||||||
@ -6302,11 +6389,19 @@ func (ec *executionContext) unmarshalInputUpdateInterceptSettingsInput(ctx conte
|
|||||||
|
|
||||||
for k, v := range asMap {
|
for k, v := range asMap {
|
||||||
switch k {
|
switch k {
|
||||||
case "enabled":
|
case "requestsEnabled":
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("enabled"))
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestsEnabled"))
|
||||||
it.Enabled, err = ec.unmarshalNBoolean2bool(ctx, v)
|
it.RequestsEnabled, err = ec.unmarshalNBoolean2bool(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
|
case "responsesEnabled":
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("responsesEnabled"))
|
||||||
|
it.ResponsesEnabled, err = ec.unmarshalNBoolean2bool(ctx, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return it, err
|
return it, err
|
||||||
}
|
}
|
||||||
@ -6318,6 +6413,14 @@ func (ec *executionContext) unmarshalInputUpdateInterceptSettingsInput(ctx conte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return it, err
|
return it, err
|
||||||
}
|
}
|
||||||
|
case "responseFilter":
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("responseFilter"))
|
||||||
|
it.ResponseFilter, err = ec.unmarshalOString2ᚖstring(ctx, v)
|
||||||
|
if err != nil {
|
||||||
|
return it, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6771,13 +6874,20 @@ func (ec *executionContext) _InterceptSettings(ctx context.Context, sel ast.Sele
|
|||||||
switch field.Name {
|
switch field.Name {
|
||||||
case "__typename":
|
case "__typename":
|
||||||
out.Values[i] = graphql.MarshalString("InterceptSettings")
|
out.Values[i] = graphql.MarshalString("InterceptSettings")
|
||||||
case "enabled":
|
case "requestsEnabled":
|
||||||
out.Values[i] = ec._InterceptSettings_enabled(ctx, field, obj)
|
out.Values[i] = ec._InterceptSettings_requestsEnabled(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
|
case "responsesEnabled":
|
||||||
|
out.Values[i] = ec._InterceptSettings_responsesEnabled(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
invalids++
|
invalids++
|
||||||
}
|
}
|
||||||
case "requestFilter":
|
case "requestFilter":
|
||||||
out.Values[i] = ec._InterceptSettings_requestFilter(ctx, field, obj)
|
out.Values[i] = ec._InterceptSettings_requestFilter(ctx, field, obj)
|
||||||
|
case "responseFilter":
|
||||||
|
out.Values[i] = ec._InterceptSettings_responseFilter(ctx, field, obj)
|
||||||
default:
|
default:
|
||||||
panic("unknown field " + strconv.Quote(field.Name))
|
panic("unknown field " + strconv.Quote(field.Name))
|
||||||
}
|
}
|
||||||
|
@ -98,8 +98,10 @@ type HTTPResponseLog struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InterceptSettings struct {
|
type InterceptSettings struct {
|
||||||
Enabled bool `json:"enabled"`
|
RequestsEnabled bool `json:"requestsEnabled"`
|
||||||
|
ResponsesEnabled bool `json:"responsesEnabled"`
|
||||||
RequestFilter *string `json:"requestFilter"`
|
RequestFilter *string `json:"requestFilter"`
|
||||||
|
ResponseFilter *string `json:"responseFilter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModifyRequestInput struct {
|
type ModifyRequestInput struct {
|
||||||
@ -194,8 +196,10 @@ type SenderRequestInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateInterceptSettingsInput struct {
|
type UpdateInterceptSettingsInput struct {
|
||||||
Enabled bool `json:"enabled"`
|
RequestsEnabled bool `json:"requestsEnabled"`
|
||||||
|
ResponsesEnabled bool `json:"responsesEnabled"`
|
||||||
RequestFilter *string `json:"requestFilter"`
|
RequestFilter *string `json:"requestFilter"`
|
||||||
|
ResponseFilter *string `json:"responseFilter"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPMethod string
|
type HTTPMethod string
|
||||||
|
@ -596,10 +596,13 @@ func (r *mutationResolver) ModifyResponse(
|
|||||||
return nil, fmt.Errorf("malformed HTTP version: %q", res.Proto)
|
return nil, fmt.Errorf("malformed HTTP version: %q", res.Proto)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var body string
|
||||||
if input.Body != nil {
|
if input.Body != nil {
|
||||||
res.Body = io.NopCloser(strings.NewReader(*input.Body))
|
body = *input.Body
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.Body = io.NopCloser(strings.NewReader(body))
|
||||||
|
|
||||||
for _, header := range input.Headers {
|
for _, header := range input.Headers {
|
||||||
res.Header.Add(header.Key, header.Value)
|
res.Header.Add(header.Key, header.Value)
|
||||||
}
|
}
|
||||||
@ -626,18 +629,28 @@ func (r *mutationResolver) UpdateInterceptSettings(
|
|||||||
input UpdateInterceptSettingsInput,
|
input UpdateInterceptSettingsInput,
|
||||||
) (*InterceptSettings, error) {
|
) (*InterceptSettings, error) {
|
||||||
settings := intercept.Settings{
|
settings := intercept.Settings{
|
||||||
Enabled: input.Enabled,
|
RequestsEnabled: input.RequestsEnabled,
|
||||||
|
ResponsesEnabled: input.ResponsesEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.RequestFilter != nil && *input.RequestFilter != "" {
|
if input.RequestFilter != nil && *input.RequestFilter != "" {
|
||||||
expr, err := search.ParseQuery(*input.RequestFilter)
|
expr, err := search.ParseQuery(*input.RequestFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not parse search query: %w", err)
|
return nil, fmt.Errorf("could not parse request filter: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.RequestFilter = expr
|
settings.RequestFilter = expr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if input.ResponseFilter != nil && *input.ResponseFilter != "" {
|
||||||
|
expr, err := search.ParseQuery(*input.ResponseFilter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse response filter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ResponseFilter = expr
|
||||||
|
}
|
||||||
|
|
||||||
err := r.ProjectService.UpdateInterceptSettings(ctx, settings)
|
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)
|
||||||
@ -646,7 +659,8 @@ func (r *mutationResolver) UpdateInterceptSettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
updated := &InterceptSettings{
|
updated := &InterceptSettings{
|
||||||
Enabled: settings.Enabled,
|
RequestsEnabled: settings.RequestsEnabled,
|
||||||
|
ResponsesEnabled: settings.ResponsesEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.RequestFilter != nil {
|
if settings.RequestFilter != nil {
|
||||||
@ -654,6 +668,11 @@ func (r *mutationResolver) UpdateInterceptSettings(
|
|||||||
updated.RequestFilter = &reqFilter
|
updated.RequestFilter = &reqFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if settings.ResponseFilter != nil {
|
||||||
|
resFilter := settings.ResponseFilter.String()
|
||||||
|
updated.ResponseFilter = &resFilter
|
||||||
|
}
|
||||||
|
|
||||||
return updated, nil
|
return updated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -842,7 +861,8 @@ func parseProject(projSvc proj.Service, p proj.Project) Project {
|
|||||||
IsActive: projSvc.IsProjectActive(p.ID),
|
IsActive: projSvc.IsProjectActive(p.ID),
|
||||||
Settings: &ProjectSettings{
|
Settings: &ProjectSettings{
|
||||||
Intercept: &InterceptSettings{
|
Intercept: &InterceptSettings{
|
||||||
Enabled: p.Settings.InterceptEnabled,
|
RequestsEnabled: p.Settings.InterceptRequests,
|
||||||
|
ResponsesEnabled: p.Settings.InterceptResponses,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -852,6 +872,11 @@ func parseProject(projSvc proj.Service, p proj.Project) Project {
|
|||||||
project.Settings.Intercept.RequestFilter = &interceptReqFilter
|
project.Settings.Intercept.RequestFilter = &interceptReqFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.Settings.InterceptResponseFilter != nil {
|
||||||
|
interceptResFilter := p.Settings.InterceptResponseFilter.String()
|
||||||
|
project.Settings.Intercept.ResponseFilter = &interceptResFilter
|
||||||
|
}
|
||||||
|
|
||||||
return project
|
return project
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,13 +179,17 @@ type CancelResponseResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input UpdateInterceptSettingsInput {
|
input UpdateInterceptSettingsInput {
|
||||||
enabled: Boolean!
|
requestsEnabled: Boolean!
|
||||||
|
responsesEnabled: Boolean!
|
||||||
requestFilter: String
|
requestFilter: String
|
||||||
|
responseFilter: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterceptSettings {
|
type InterceptSettings {
|
||||||
enabled: Boolean!
|
requestsEnabled: Boolean!
|
||||||
|
responsesEnabled: Boolean!
|
||||||
requestFilter: String
|
requestFilter: String
|
||||||
|
responseFilter: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
|
@ -62,8 +62,10 @@ type Settings struct {
|
|||||||
ReqLogSearchExpr search.Expression
|
ReqLogSearchExpr search.Expression
|
||||||
|
|
||||||
// Intercept settings
|
// Intercept settings
|
||||||
InterceptEnabled bool
|
InterceptRequests bool
|
||||||
|
InterceptResponses bool
|
||||||
InterceptRequestFilter search.Expression
|
InterceptRequestFilter search.Expression
|
||||||
|
InterceptResponseFilter search.Expression
|
||||||
|
|
||||||
// Sender settings
|
// Sender settings
|
||||||
SenderOnlyFindInScope bool
|
SenderOnlyFindInScope bool
|
||||||
@ -133,8 +135,10 @@ 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,
|
RequestsEnabled: false,
|
||||||
|
ResponsesEnabled: false,
|
||||||
RequestFilter: nil,
|
RequestFilter: nil,
|
||||||
|
ResponseFilter: nil,
|
||||||
})
|
})
|
||||||
svc.senderSvc.SetActiveProjectID(ulid.ULID{})
|
svc.senderSvc.SetActiveProjectID(ulid.ULID{})
|
||||||
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
|
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
|
||||||
@ -179,8 +183,10 @@ 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,
|
RequestsEnabled: project.Settings.InterceptRequests,
|
||||||
|
ResponsesEnabled: project.Settings.InterceptResponses,
|
||||||
RequestFilter: project.Settings.InterceptRequestFilter,
|
RequestFilter: project.Settings.InterceptRequestFilter,
|
||||||
|
ResponseFilter: project.Settings.InterceptResponseFilter,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Sender settings.
|
// Sender settings.
|
||||||
@ -296,8 +302,10 @@ func (svc *service) UpdateInterceptSettings(ctx context.Context, settings interc
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
project.Settings.InterceptEnabled = settings.Enabled
|
project.Settings.InterceptRequests = settings.RequestsEnabled
|
||||||
|
project.Settings.InterceptResponses = settings.ResponsesEnabled
|
||||||
project.Settings.InterceptRequestFilter = settings.RequestFilter
|
project.Settings.InterceptRequestFilter = settings.RequestFilter
|
||||||
|
project.Settings.InterceptResponseFilter = settings.ResponseFilter
|
||||||
|
|
||||||
err = svc.repo.UpsertProject(ctx, project)
|
err = svc.repo.UpsertProject(ctx, project)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
@ -38,6 +39,34 @@ var reqFilterKeyFns = map[string]func(req *http.Request) (string, error){
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:unparam
|
||||||
|
var resFilterKeyFns = map[string]func(res *http.Response) (string, error){
|
||||||
|
"proto": func(res *http.Response) (string, error) { return res.Proto, nil },
|
||||||
|
"statusCode": func(res *http.Response) (string, error) { return strconv.Itoa(res.StatusCode), nil },
|
||||||
|
"statusReason": func(res *http.Response) (string, error) {
|
||||||
|
statusReasonSubs := strings.SplitN(res.Status, " ", 2)
|
||||||
|
|
||||||
|
if len(statusReasonSubs) != 2 {
|
||||||
|
return "", fmt.Errorf("invalid response status %q", res.Status)
|
||||||
|
}
|
||||||
|
return statusReasonSubs[1], nil
|
||||||
|
},
|
||||||
|
"body": func(res *http.Response) (string, error) {
|
||||||
|
if res.Body == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||||
|
|
||||||
|
return string(body), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// MatchRequestFilter returns true if an HTTP request matches the request filter expression.
|
// MatchRequestFilter returns true if an HTTP request matches the request filter expression.
|
||||||
func MatchRequestFilter(req *http.Request, expr search.Expression) (bool, error) {
|
func MatchRequestFilter(req *http.Request, expr search.Expression) (bool, error) {
|
||||||
switch e := expr.(type) {
|
switch e := expr.(type) {
|
||||||
@ -228,3 +257,139 @@ func MatchRequestScope(req *http.Request, s *scope.Scope) (bool, error) {
|
|||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MatchResponseFilter returns true if an HTTP response matches the response filter expression.
|
||||||
|
func MatchResponseFilter(res *http.Response, expr search.Expression) (bool, error) {
|
||||||
|
switch e := expr.(type) {
|
||||||
|
case search.PrefixExpression:
|
||||||
|
return matchResPrefixExpr(res, e)
|
||||||
|
case search.InfixExpression:
|
||||||
|
return matchResInfixExpr(res, e)
|
||||||
|
case search.StringLiteral:
|
||||||
|
return matchResStringLiteral(res, e)
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("expression type (%T) not supported", expr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchResPrefixExpr(res *http.Response, expr search.PrefixExpression) (bool, error) {
|
||||||
|
switch expr.Operator {
|
||||||
|
case search.TokOpNot:
|
||||||
|
match, err := MatchResponseFilter(res, expr.Right)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return !match, nil
|
||||||
|
default:
|
||||||
|
return false, errors.New("operator is not supported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchResInfixExpr(res *http.Response, expr search.InfixExpression) (bool, error) {
|
||||||
|
switch expr.Operator {
|
||||||
|
case search.TokOpAnd:
|
||||||
|
left, err := MatchResponseFilter(res, expr.Left)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
right, err := MatchResponseFilter(res, expr.Right)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return left && right, nil
|
||||||
|
case search.TokOpOr:
|
||||||
|
left, err := MatchResponseFilter(res, expr.Left)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
right, err := MatchResponseFilter(res, 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 := getMappedStringLiteralFromRes(res, left.Value)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get string literal from response 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 := getMappedStringLiteralFromRes(res, right.Value)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get string literal from response 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 getMappedStringLiteralFromRes(res *http.Response, s string) (string, error) {
|
||||||
|
fn, ok := resFilterKeyFns[s]
|
||||||
|
if ok {
|
||||||
|
return fn(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchResStringLiteral(res *http.Response, strLiteral search.StringLiteral) (bool, error) {
|
||||||
|
for _, fn := range resFilterKeyFns {
|
||||||
|
value, err := fn(res)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(strings.ToLower(value), strings.ToLower(strLiteral.Value)) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
@ -53,14 +53,19 @@ type Service struct {
|
|||||||
requests map[ulid.ULID]Request
|
requests map[ulid.ULID]Request
|
||||||
responses map[ulid.ULID]Response
|
responses map[ulid.ULID]Response
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
enabled bool
|
|
||||||
|
requestsEnabled bool
|
||||||
|
responsesEnabled bool
|
||||||
reqFilter search.Expression
|
reqFilter search.Expression
|
||||||
|
resFilter search.Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Logger log.Logger
|
Logger log.Logger
|
||||||
Enabled bool
|
RequestsEnabled bool
|
||||||
|
ResponsesEnabled bool
|
||||||
RequestFilter search.Expression
|
RequestFilter search.Expression
|
||||||
|
ResponseFilter search.Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestIDs implements sort.Interface.
|
// RequestIDs implements sort.Interface.
|
||||||
@ -73,8 +78,10 @@ func NewService(cfg Config) *Service {
|
|||||||
requests: make(map[ulid.ULID]Request),
|
requests: make(map[ulid.ULID]Request),
|
||||||
responses: make(map[ulid.ULID]Response),
|
responses: make(map[ulid.ULID]Response),
|
||||||
logger: cfg.Logger,
|
logger: cfg.Logger,
|
||||||
enabled: cfg.Enabled,
|
requestsEnabled: cfg.RequestsEnabled,
|
||||||
|
responsesEnabled: cfg.ResponsesEnabled,
|
||||||
reqFilter: cfg.RequestFilter,
|
reqFilter: cfg.RequestFilter,
|
||||||
|
resFilter: cfg.ResponseFilter,
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.logger == nil {
|
if s.logger == nil {
|
||||||
@ -122,7 +129,7 @@ func (svc *Service) InterceptRequest(ctx context.Context, req *http.Request) (*h
|
|||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !svc.enabled {
|
if !svc.requestsEnabled {
|
||||||
// If request intercept is disabled, return the incoming request as-is.
|
// If request intercept is disabled, return the incoming request as-is.
|
||||||
svc.logger.Debugw("Bypassed request interception: feature disabled.")
|
svc.logger.Debugw("Bypassed request interception: feature disabled.")
|
||||||
return req, nil
|
return req, nil
|
||||||
@ -267,14 +274,20 @@ func (svc *Service) Items() []Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (svc *Service) UpdateSettings(settings Settings) {
|
func (svc *Service) UpdateSettings(settings Settings) {
|
||||||
// When updating from `enabled` -> `disabled`, clear any pending reqs.
|
// When updating from requests `enabled` -> `disabled`, clear any pending reqs.
|
||||||
if svc.enabled && !settings.Enabled {
|
if svc.requestsEnabled && !settings.RequestsEnabled {
|
||||||
svc.ClearRequests()
|
svc.ClearRequests()
|
||||||
|
}
|
||||||
|
|
||||||
|
// When updating from responses `enabled` -> `disabled`, clear any pending responses.
|
||||||
|
if svc.responsesEnabled && !settings.ResponsesEnabled {
|
||||||
svc.ClearResponses()
|
svc.ClearResponses()
|
||||||
}
|
}
|
||||||
|
|
||||||
svc.enabled = settings.Enabled
|
svc.requestsEnabled = settings.RequestsEnabled
|
||||||
|
svc.responsesEnabled = settings.ResponsesEnabled
|
||||||
svc.reqFilter = settings.RequestFilter
|
svc.reqFilter = settings.RequestFilter
|
||||||
|
svc.resFilter = settings.ResponseFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
// ItemByID returns an intercepted item (request and possible response) by ID. It's safe for concurrent use.
|
// ItemByID returns an intercepted item (request and possible response) by ID. It's safe for concurrent use.
|
||||||
@ -358,25 +371,25 @@ func (svc *Service) InterceptResponse(ctx context.Context, res *http.Response) (
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !svc.enabled {
|
// If global response intercept is disabled and interception is *not* explicitly enabled for this response: bypass.
|
||||||
// If the feature is disabled, return the response as-is.
|
if !svc.responsesEnabled && !(ok && shouldIntercept) {
|
||||||
svc.logger.Debugw("Bypassed response interception: feature disabled.")
|
svc.logger.Debugw("Bypassed response interception: feature disabled.")
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if svc.reqFilter != nil {
|
if svc.resFilter != nil {
|
||||||
// match, err := MatchRequestFilter(req, svc.reqFilter)
|
match, err := MatchResponseFilter(res, svc.resFilter)
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// return nil, fmt.Errorf("intercept: failed to match request rules for request (id: %v): %w",
|
return nil, fmt.Errorf("intercept: failed to match response rules for response (id: %v): %w",
|
||||||
// reqID.String(), err,
|
reqID.String(), err,
|
||||||
// )
|
)
|
||||||
// }
|
}
|
||||||
|
|
||||||
// if !match {
|
if !match {
|
||||||
// svc.logger.Debugw("Bypassed interception: request rules don't match.")
|
svc.logger.Debugw("Bypassed response interception: response rules don't match.")
|
||||||
// return req, nil
|
return res, nil
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
ch := make(chan *http.Response)
|
ch := make(chan *http.Response)
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
@ -29,7 +29,8 @@ func TestRequestModifier(t *testing.T) {
|
|||||||
logger, _ := zap.NewDevelopment()
|
logger, _ := zap.NewDevelopment()
|
||||||
svc := intercept.NewService(intercept.Config{
|
svc := intercept.NewService(intercept.Config{
|
||||||
Logger: logger.Sugar(),
|
Logger: logger.Sugar(),
|
||||||
Enabled: true,
|
RequestsEnabled: true,
|
||||||
|
ResponsesEnabled: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||||
@ -46,7 +47,8 @@ func TestRequestModifier(t *testing.T) {
|
|||||||
logger, _ := zap.NewDevelopment()
|
logger, _ := zap.NewDevelopment()
|
||||||
svc := intercept.NewService(intercept.Config{
|
svc := intercept.NewService(intercept.Config{
|
||||||
Logger: logger.Sugar(),
|
Logger: logger.Sugar(),
|
||||||
Enabled: true,
|
RequestsEnabled: true,
|
||||||
|
ResponsesEnabled: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@ -89,7 +91,8 @@ func TestRequestModifier(t *testing.T) {
|
|||||||
logger, _ := zap.NewDevelopment()
|
logger, _ := zap.NewDevelopment()
|
||||||
svc := intercept.NewService(intercept.Config{
|
svc := intercept.NewService(intercept.Config{
|
||||||
Logger: logger.Sugar(),
|
Logger: logger.Sugar(),
|
||||||
Enabled: true,
|
RequestsEnabled: true,
|
||||||
|
ResponsesEnabled: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
var got *http.Request
|
var got *http.Request
|
||||||
|
@ -3,6 +3,8 @@ package intercept
|
|||||||
import "github.com/dstotijn/hetty/pkg/search"
|
import "github.com/dstotijn/hetty/pkg/search"
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
Enabled bool
|
RequestsEnabled bool
|
||||||
|
ResponsesEnabled bool
|
||||||
RequestFilter search.Expression
|
RequestFilter search.Expression
|
||||||
|
ResponseFilter search.Expression
|
||||||
}
|
}
|
||||||
|
@ -217,14 +217,16 @@ func (svc *service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
|
|||||||
|
|
||||||
clone := *res
|
clone := *res
|
||||||
|
|
||||||
|
if res.Body != nil {
|
||||||
// TODO: Use io.LimitReader.
|
// TODO: Use io.LimitReader.
|
||||||
body, err := ioutil.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("reqlog: could not read response body: %w", err)
|
return fmt.Errorf("reqlog: could not read response body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
res.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||||
clone.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
clone.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := svc.storeResponse(context.Background(), reqLogID, &clone); err != nil {
|
if err := svc.storeResponse(context.Background(), reqLogID, &clone); err != nil {
|
||||||
|
Reference in New Issue
Block a user