diff --git a/admin/src/features/projects/graphql/activeProject.graphql b/admin/src/features/projects/graphql/activeProject.graphql
index 4327af0..4aa3711 100644
--- a/admin/src/features/projects/graphql/activeProject.graphql
+++ b/admin/src/features/projects/graphql/activeProject.graphql
@@ -5,8 +5,10 @@ query ActiveProject {
isActive
settings {
intercept {
- enabled
+ requestsEnabled
+ responsesEnabled
requestFilter
+ responseFilter
}
}
}
diff --git a/admin/src/features/reqlog/components/Actions.tsx b/admin/src/features/reqlog/components/Actions.tsx
index bc16d7b..e95e1f7 100644
--- a/admin/src/features/reqlog/components/Actions.tsx
+++ b/admin/src/features/reqlog/components/Actions.tsx
@@ -29,7 +29,7 @@ function Actions(): JSX.Element {
{clearLogsResult.error && Failed to clear HTTP logs: {clearLogsResult.error}}
- {activeProject?.settings.intercept.enabled && (
+ {(activeProject?.settings.intercept.requestsEnabled || activeProject?.settings.intercept.responsesEnabled) && (
+ );
+}
+
export default function Settings(): JSX.Element {
const client = useApolloClient();
const activeProject = useActiveProject();
@@ -41,16 +62,22 @@ export default function Settings(): JSX.Element {
}));
setInterceptReqFilter(data.updateInterceptSettings.requestFilter || "");
+ setInterceptResFilter(data.updateInterceptSettings.responseFilter || "");
},
});
const [interceptReqFilter, setInterceptReqFilter] = useState("");
+ const [interceptResFilter, setInterceptResFilter] = useState("");
useEffect(() => {
setInterceptReqFilter(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) {
e.preventDefault();
return;
@@ -60,7 +87,23 @@ export default function Settings(): JSX.Element {
variables: {
input: {
...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 tabSx = {
@@ -111,16 +169,19 @@ export default function Settings(): JSX.Element {
-
+
+ Requests
+
+
}
- label="Enable proxy interception"
+ label="Enable request interception"
labelPlacement="start"
sx={{ display: "inline-block", m: 0 }}
/>
@@ -129,28 +190,13 @@ export default function Settings(): JSX.Element {
manual review.
-
- Rules
-
+
+ Responses
+
+
+
+ }
+ label="Enable response interception"
+ labelPlacement="start"
+ sx={{ display: "inline-block", m: 0 }}
+ />
+
+ When enabled, HTTP responses received by the proxy are stalled for{" "}
+ manual review.
+
+
+
>
diff --git a/admin/src/features/settings/graphql/updateInterceptSettings.graphql b/admin/src/features/settings/graphql/updateInterceptSettings.graphql
index 0bcf99f..ea9c516 100644
--- a/admin/src/features/settings/graphql/updateInterceptSettings.graphql
+++ b/admin/src/features/settings/graphql/updateInterceptSettings.graphql
@@ -1,6 +1,8 @@
mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) {
updateInterceptSettings(input: $input) {
- enabled
+ requestsEnabled
+ responsesEnabled
requestFilter
+ responseFilter
}
}
diff --git a/admin/src/lib/graphql/generated.tsx b/admin/src/lib/graphql/generated.tsx
index 6cd3ed7..c64dcbf 100644
--- a/admin/src/lib/graphql/generated.tsx
+++ b/admin/src/lib/graphql/generated.tsx
@@ -135,8 +135,10 @@ export type HttpResponseLog = {
export type InterceptSettings = {
__typename?: 'InterceptSettings';
- enabled: Scalars['Boolean'];
requestFilter?: Maybe;
+ requestsEnabled: Scalars['Boolean'];
+ responseFilter?: Maybe;
+ responsesEnabled: Scalars['Boolean'];
};
export type ModifyRequestInput = {
@@ -359,8 +361,10 @@ export type SenderRequestInput = {
};
export type UpdateInterceptSettingsInput = {
- enabled: Scalars['Boolean'];
requestFilter?: InputMaybe;
+ requestsEnabled: Scalars['Boolean'];
+ responseFilter?: InputMaybe;
+ responsesEnabled: Scalars['Boolean'];
};
export type CancelRequestMutationVariables = Exact<{
@@ -401,7 +405,7 @@ export type ModifyResponseMutation = { __typename?: 'Mutation', modifyResponse:
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; }>;
@@ -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; }>;
@@ -715,8 +719,10 @@ export const ActiveProjectDocument = gql`
isActive
settings {
intercept {
- enabled
+ requestsEnabled
+ responsesEnabled
requestFilter
+ responseFilter
}
}
}
@@ -1381,8 +1387,10 @@ export type GetSenderRequestsQueryResult = Apollo.QueryResult 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
+}
diff --git a/pkg/proxy/intercept/intercept.go b/pkg/proxy/intercept/intercept.go
index 06c8441..caf2f99 100644
--- a/pkg/proxy/intercept/intercept.go
+++ b/pkg/proxy/intercept/intercept.go
@@ -53,14 +53,19 @@ type Service struct {
requests map[ulid.ULID]Request
responses map[ulid.ULID]Response
logger log.Logger
- enabled bool
- reqFilter search.Expression
+
+ requestsEnabled bool
+ responsesEnabled bool
+ reqFilter search.Expression
+ resFilter search.Expression
}
type Config struct {
- Logger log.Logger
- Enabled bool
- RequestFilter search.Expression
+ Logger log.Logger
+ RequestsEnabled bool
+ ResponsesEnabled bool
+ RequestFilter search.Expression
+ ResponseFilter search.Expression
}
// RequestIDs implements sort.Interface.
@@ -68,13 +73,15 @@ type RequestIDs []ulid.ULID
func NewService(cfg Config) *Service {
s := &Service{
- reqMu: &sync.RWMutex{},
- resMu: &sync.RWMutex{},
- requests: make(map[ulid.ULID]Request),
- responses: make(map[ulid.ULID]Response),
- logger: cfg.Logger,
- enabled: cfg.Enabled,
- reqFilter: cfg.RequestFilter,
+ reqMu: &sync.RWMutex{},
+ resMu: &sync.RWMutex{},
+ requests: make(map[ulid.ULID]Request),
+ responses: make(map[ulid.ULID]Response),
+ logger: cfg.Logger,
+ requestsEnabled: cfg.RequestsEnabled,
+ responsesEnabled: cfg.ResponsesEnabled,
+ reqFilter: cfg.RequestFilter,
+ resFilter: cfg.ResponseFilter,
}
if s.logger == nil {
@@ -122,7 +129,7 @@ func (svc *Service) InterceptRequest(ctx context.Context, req *http.Request) (*h
return req, nil
}
- if !svc.enabled {
+ if !svc.requestsEnabled {
// If request intercept is disabled, return the incoming request as-is.
svc.logger.Debugw("Bypassed request interception: feature disabled.")
return req, nil
@@ -267,14 +274,20 @@ func (svc *Service) Items() []Item {
}
func (svc *Service) UpdateSettings(settings Settings) {
- // When updating from `enabled` -> `disabled`, clear any pending reqs.
- if svc.enabled && !settings.Enabled {
+ // When updating from requests `enabled` -> `disabled`, clear any pending reqs.
+ if svc.requestsEnabled && !settings.RequestsEnabled {
svc.ClearRequests()
+ }
+
+ // When updating from responses `enabled` -> `disabled`, clear any pending responses.
+ if svc.responsesEnabled && !settings.ResponsesEnabled {
svc.ClearResponses()
}
- svc.enabled = settings.Enabled
+ svc.requestsEnabled = settings.RequestsEnabled
+ svc.responsesEnabled = settings.ResponsesEnabled
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.
@@ -358,25 +371,25 @@ func (svc *Service) InterceptResponse(ctx context.Context, res *http.Response) (
return res, nil
}
- if !svc.enabled {
- // If the feature is disabled, return the response as-is.
+ // If global response intercept is disabled and interception is *not* explicitly enabled for this response: bypass.
+ if !svc.responsesEnabled && !(ok && shouldIntercept) {
svc.logger.Debugw("Bypassed response interception: feature disabled.")
return res, 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 svc.resFilter != nil {
+ match, err := MatchResponseFilter(res, svc.resFilter)
+ if err != nil {
+ return nil, fmt.Errorf("intercept: failed to match response rules for response (id: %v): %w",
+ reqID.String(), err,
+ )
+ }
- // if !match {
- // svc.logger.Debugw("Bypassed interception: request rules don't match.")
- // return req, nil
- // }
- // }
+ if !match {
+ svc.logger.Debugw("Bypassed response interception: response rules don't match.")
+ return res, nil
+ }
+ }
ch := make(chan *http.Response)
done := make(chan struct{})
diff --git a/pkg/proxy/intercept/intercept_test.go b/pkg/proxy/intercept/intercept_test.go
index 2ed20f8..bb5e91d 100644
--- a/pkg/proxy/intercept/intercept_test.go
+++ b/pkg/proxy/intercept/intercept_test.go
@@ -28,8 +28,9 @@ func TestRequestModifier(t *testing.T) {
logger, _ := zap.NewDevelopment()
svc := intercept.NewService(intercept.Config{
- Logger: logger.Sugar(),
- Enabled: true,
+ Logger: logger.Sugar(),
+ RequestsEnabled: true,
+ ResponsesEnabled: false,
})
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
@@ -45,8 +46,9 @@ func TestRequestModifier(t *testing.T) {
logger, _ := zap.NewDevelopment()
svc := intercept.NewService(intercept.Config{
- Logger: logger.Sugar(),
- Enabled: true,
+ Logger: logger.Sugar(),
+ RequestsEnabled: true,
+ ResponsesEnabled: false,
})
ctx, cancel := context.WithCancel(context.Background())
@@ -88,8 +90,9 @@ func TestRequestModifier(t *testing.T) {
logger, _ := zap.NewDevelopment()
svc := intercept.NewService(intercept.Config{
- Logger: logger.Sugar(),
- Enabled: true,
+ Logger: logger.Sugar(),
+ RequestsEnabled: true,
+ ResponsesEnabled: false,
})
var got *http.Request
diff --git a/pkg/proxy/intercept/settings.go b/pkg/proxy/intercept/settings.go
index 084b35d..15bc154 100644
--- a/pkg/proxy/intercept/settings.go
+++ b/pkg/proxy/intercept/settings.go
@@ -3,6 +3,8 @@ package intercept
import "github.com/dstotijn/hetty/pkg/search"
type Settings struct {
- Enabled bool
- RequestFilter search.Expression
+ RequestsEnabled bool
+ ResponsesEnabled bool
+ RequestFilter search.Expression
+ ResponseFilter search.Expression
}
diff --git a/pkg/reqlog/reqlog.go b/pkg/reqlog/reqlog.go
index d7e652e..65b7f2e 100644
--- a/pkg/reqlog/reqlog.go
+++ b/pkg/reqlog/reqlog.go
@@ -217,14 +217,16 @@ func (svc *service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
clone := *res
- // TODO: Use io.LimitReader.
- body, err := ioutil.ReadAll(res.Body)
- if err != nil {
- return fmt.Errorf("reqlog: could not read response body: %w", err)
- }
+ if res.Body != nil {
+ // TODO: Use io.LimitReader.
+ body, err := io.ReadAll(res.Body)
+ if err != nil {
+ return fmt.Errorf("reqlog: could not read response body: %w", err)
+ }
- res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
- clone.Body = ioutil.NopCloser(bytes.NewBuffer(body))
+ res.Body = io.NopCloser(bytes.NewBuffer(body))
+ clone.Body = io.NopCloser(bytes.NewBuffer(body))
+ }
go func() {
if err := svc.storeResponse(context.Background(), reqLogID, &clone); err != nil {