Add support for intercepting HTTP responses

This commit is contained in:
David Stotijn
2022-03-18 16:40:05 +01:00
parent f4074a8060
commit cf55456c42
16 changed files with 1627 additions and 167 deletions

View File

@ -1,4 +1,5 @@
import CancelIcon from "@mui/icons-material/Cancel";
import DownloadIcon from "@mui/icons-material/Download";
import SendIcon from "@mui/icons-material/Send";
import SettingsIcon from "@mui/icons-material/Settings";
import { Alert, Box, Button, CircularProgress, IconButton, Tooltip, Typography } from "@mui/material";
@ -9,15 +10,17 @@ import { useInterceptedRequests } from "lib/InterceptedRequestsContext";
import { KeyValuePair, sortKeyValuePairs } from "lib/components/KeyValuePair";
import Link from "lib/components/Link";
import RequestTabs from "lib/components/RequestTabs";
import Response from "lib/components/Response";
import SplitPane from "lib/components/SplitPane";
import ResponseStatus from "lib/components/ResponseStatus";
import ResponseTabs from "lib/components/ResponseTabs";
import UrlBar, { HttpMethod, HttpProto, httpProtoMap } from "lib/components/UrlBar";
import {
HttpProtocol,
HttpRequest,
useCancelRequestMutation,
useCancelResponseMutation,
useGetInterceptedRequestQuery,
useModifyRequestMutation,
useModifyResponseMutation,
} from "lib/graphql/generated";
import { queryParamsFromURL } from "lib/queryParamsFromURL";
import updateKeyPairItem from "lib/updateKeyPairItem";
@ -43,8 +46,10 @@ function EditRequest(): JSX.Element {
const [url, setURL] = useState("");
const [proto, setProto] = useState(HttpProto.Http20);
const [queryParams, setQueryParams] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
const [headers, setHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
const [body, setBody] = useState("");
const [reqHeaders, setReqHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
const [resHeaders, setResHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
const [reqBody, setReqBody] = useState("");
const [resBody, setResBody] = useState("");
const handleQueryParamChange = (key: string, value: string, idx: number) => {
setQueryParams((prev) => {
@ -61,11 +66,18 @@ function EditRequest(): JSX.Element {
});
};
const handleHeaderChange = (key: string, value: string, idx: number) => {
setHeaders((prev) => updateKeyPairItem(key, value, idx, prev));
const handleReqHeaderChange = (key: string, value: string, idx: number) => {
setReqHeaders((prev) => updateKeyPairItem(key, value, idx, prev));
};
const handleHeaderDelete = (idx: number) => {
setHeaders((prev) => prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length)));
const handleReqHeaderDelete = (idx: number) => {
setReqHeaders((prev) => prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length)));
};
const handleResHeaderChange = (key: string, value: string, idx: number) => {
setResHeaders((prev) => updateKeyPairItem(key, value, idx, prev));
};
const handleResHeaderDelete = (idx: number) => {
setResHeaders((prev) => prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length)));
};
const handleURLChange = (url: string) => {
@ -93,63 +105,95 @@ function EditRequest(): JSX.Element {
setURL(interceptedRequest.url);
setMethod(interceptedRequest.method);
setBody(interceptedRequest.body || "");
setReqBody(interceptedRequest.body || "");
const newQueryParams = queryParamsFromURL(interceptedRequest.url);
// Push empty row.
newQueryParams.push({ key: "", value: "" });
setQueryParams(newQueryParams);
const newHeaders = sortKeyValuePairs(interceptedRequest.headers || []);
setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
const newReqHeaders = sortKeyValuePairs(interceptedRequest.headers || []);
setReqHeaders([...newReqHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
setResBody(interceptedRequest.response?.body || "");
const newResHeaders = sortKeyValuePairs(interceptedRequest.response?.headers || []);
setResHeaders([...newResHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
},
});
const interceptedReq = reqId ? getReqResult?.data?.interceptedRequest : undefined;
const interceptedReq =
reqId && !getReqResult?.data?.interceptedRequest?.response ? getReqResult?.data?.interceptedRequest : undefined;
const interceptedRes = reqId ? getReqResult?.data?.interceptedRequest?.response : undefined;
const [modifyRequest, modifyResult] = useModifyRequestMutation();
const [cancelRequest, cancelResult] = useCancelRequestMutation();
const [modifyRequest, modifyReqResult] = useModifyRequestMutation();
const [cancelRequest, cancelReqResult] = useCancelRequestMutation();
const [modifyResponse, modifyResResult] = useModifyResponseMutation();
const [cancelResponse, cancelResResult] = useCancelResponseMutation();
const onActionCompleted = () => {
setURL("");
setMethod(HttpMethod.Get);
setBody("");
setReqBody("");
setQueryParams([]);
setHeaders([]);
setReqHeaders([]);
router.replace(`/proxy/intercept`);
};
const handleFormSubmit: React.FormEventHandler = (e) => {
e.preventDefault();
if (!interceptedReq) {
return;
if (interceptedReq) {
modifyRequest({
variables: {
request: {
id: interceptedReq.id,
url,
method,
proto: httpProtoMap.get(proto) || HttpProtocol.Http20,
headers: reqHeaders.filter((kv) => kv.key !== ""),
body: reqBody || undefined,
},
},
update(cache) {
cache.modify({
fields: {
interceptedRequests(existing: HttpRequest[], { readField }) {
return existing.filter((ref) => interceptedReq.id !== readField("id", ref));
},
},
});
},
onCompleted: onActionCompleted,
});
}
modifyRequest({
variables: {
request: {
id: interceptedReq.id,
url,
method,
proto: httpProtoMap.get(proto) || HttpProtocol.Http20,
headers: headers.filter((kv) => kv.key !== ""),
body: body || undefined,
},
},
update(cache) {
cache.modify({
fields: {
interceptedRequests(existing: HttpRequest[], { readField }) {
return existing.filter((ref) => interceptedReq.id !== readField("id", ref));
},
if (interceptedRes) {
modifyResponse({
variables: {
response: {
requestID: interceptedRes.id,
proto: interceptedRes.proto, // TODO: Allow modifying
statusCode: interceptedRes.statusCode, // TODO: Allow modifying
statusReason: interceptedRes.statusReason, // TODO: Allow modifying
headers: resHeaders.filter((kv) => kv.key !== ""),
body: resBody || undefined,
},
});
},
onCompleted: onActionCompleted,
});
},
update(cache) {
cache.modify({
fields: {
interceptedRequests(existing: HttpRequest[], { readField }) {
return existing.filter((ref) => interceptedRes.id !== readField("id", ref));
},
},
});
},
onCompleted: onActionCompleted,
});
}
};
const handleCancelClick = () => {
const handleReqCancelClick = () => {
if (!interceptedReq) {
return;
}
@ -171,6 +215,28 @@ function EditRequest(): JSX.Element {
});
};
const handleResCancelClick = () => {
if (!interceptedRes) {
return;
}
cancelResponse({
variables: {
requestID: interceptedRes.id,
},
update(cache) {
cache.modify({
fields: {
interceptedRequests(existing: HttpRequest[], { readField }) {
return existing.filter((ref) => interceptedRes.id !== readField("id", ref));
},
},
});
},
onCompleted: onActionCompleted,
});
};
return (
<Box display="flex" flexDirection="column" height="100%" gap={2}>
<Box component="form" autoComplete="off" onSubmit={handleFormSubmit}>
@ -184,64 +250,114 @@ function EditRequest(): JSX.Element {
onProtoChange={interceptedReq ? setProto : undefined}
sx={{ flex: "1 auto" }}
/>
<Button
variant="contained"
disableElevation
type="submit"
disabled={!interceptedReq || modifyResult.loading || cancelResult.loading}
startIcon={modifyResult.loading ? <CircularProgress size={22} /> : <SendIcon />}
>
Send
</Button>
<Button
variant="contained"
color="error"
disableElevation
onClick={handleCancelClick}
disabled={!interceptedReq || modifyResult.loading || cancelResult.loading}
startIcon={cancelResult.loading ? <CircularProgress size={22} /> : <CancelIcon />}
>
Cancel
</Button>
{!interceptedRes && (
<>
<Button
variant="contained"
disableElevation
type="submit"
disabled={!interceptedReq || modifyReqResult.loading || cancelReqResult.loading}
startIcon={modifyReqResult.loading ? <CircularProgress size={22} /> : <SendIcon />}
>
Send
</Button>
<Button
variant="contained"
color="error"
disableElevation
onClick={handleReqCancelClick}
disabled={!interceptedReq || modifyReqResult.loading || cancelReqResult.loading}
startIcon={cancelReqResult.loading ? <CircularProgress size={22} /> : <CancelIcon />}
>
Cancel
</Button>
</>
)}
{interceptedRes && (
<>
<Button
variant="contained"
disableElevation
type="submit"
disabled={modifyResResult.loading || cancelResResult.loading}
endIcon={modifyResResult.loading ? <CircularProgress size={22} /> : <DownloadIcon />}
>
Receive
</Button>
<Button
variant="contained"
color="error"
disableElevation
onClick={handleResCancelClick}
disabled={modifyResResult.loading || cancelResResult.loading}
endIcon={cancelResResult.loading ? <CircularProgress size={22} /> : <CancelIcon />}
>
Cancel
</Button>
</>
)}
<Tooltip title="Intercept settings">
<IconButton LinkComponent={Link} href="/settings#intercept">
<SettingsIcon />
</IconButton>
</Tooltip>
</Box>
{modifyResult.error && (
{modifyReqResult.error && (
<Alert severity="error" sx={{ mt: 1 }}>
{modifyResult.error.message}
{modifyReqResult.error.message}
</Alert>
)}
{cancelResult.error && (
{cancelReqResult.error && (
<Alert severity="error" sx={{ mt: 1 }}>
{cancelResult.error.message}
{cancelReqResult.error.message}
</Alert>
)}
</Box>
<Box flex="1 auto" position="relative">
<SplitPane split="vertical" size={"50%"}>
<Box sx={{ height: "100%", mr: 2, pb: 2, position: "relative" }}>
<Box flex="1 auto" overflow="scroll">
{interceptedReq && (
<Box sx={{ height: "100%", pb: 2 }}>
<Typography variant="overline" color="textSecondary" sx={{ position: "absolute", right: 0, mt: 1.2 }}>
Request
</Typography>
<RequestTabs
queryParams={interceptedReq ? queryParams : []}
headers={interceptedReq ? headers : []}
body={body}
headers={interceptedReq ? reqHeaders : []}
body={reqBody}
onQueryParamChange={interceptedReq ? handleQueryParamChange : undefined}
onQueryParamDelete={interceptedReq ? handleQueryParamDelete : undefined}
onHeaderChange={interceptedReq ? handleHeaderChange : undefined}
onHeaderDelete={interceptedReq ? handleHeaderDelete : undefined}
onBodyChange={interceptedReq ? setBody : undefined}
onHeaderChange={interceptedReq ? handleReqHeaderChange : undefined}
onHeaderDelete={interceptedReq ? handleReqHeaderDelete : undefined}
onBodyChange={interceptedReq ? setReqBody : undefined}
/>
</Box>
<Box sx={{ height: "100%", position: "relative", ml: 2, pb: 2 }}>
<Response response={null} />
)}
{interceptedRes && (
<Box sx={{ height: "100%", pb: 2 }}>
<Box sx={{ position: "absolute", right: 0, mt: 1.4 }}>
<Typography variant="overline" color="textSecondary" sx={{ float: "right", ml: 3 }}>
Response
</Typography>
{interceptedRes && (
<Box sx={{ float: "right", mt: 0.2 }}>
<ResponseStatus
proto={interceptedRes.proto}
statusCode={interceptedRes.statusCode}
statusReason={interceptedRes.statusReason}
/>
</Box>
)}
</Box>
<ResponseTabs
headers={interceptedRes ? resHeaders : []}
body={resBody}
onHeaderChange={interceptedRes ? handleResHeaderChange : undefined}
onHeaderDelete={interceptedRes ? handleResHeaderDelete : undefined}
onBodyChange={interceptedRes ? setResBody : undefined}
hasResponse={interceptedRes !== undefined && interceptedRes !== null}
/>
</Box>
</SplitPane>
)}
</Box>
</Box>
);

View File

@ -0,0 +1,5 @@
mutation CancelResponse($requestID: ID!) {
cancelResponse(requestID: $requestID) {
success
}
}

View File

@ -9,5 +9,16 @@ query GetInterceptedRequest($id: ID!) {
value
}
body
response {
id
proto
statusCode
statusReason
headers {
key
value
}
body
}
}
}

View File

@ -0,0 +1,5 @@
mutation ModifyResponse($response: ModifyResponseInput!) {
modifyResponse(response: $response) {
success
}
}

View File

@ -2,13 +2,16 @@ import { TabContext, TabList, TabPanel } from "@mui/lab";
import { Box, Paper, Tab, Typography } from "@mui/material";
import React, { useState } from "react";
import { KeyValuePairTable, KeyValuePair, KeyValuePairTableProps } from "./KeyValuePair";
import Editor from "lib/components/Editor";
import { KeyValuePairTable } from "lib/components/KeyValuePair";
import { HttpResponseLog } from "lib/graphql/generated";
interface ResponseTabsProps {
headers: HttpResponseLog["headers"];
body: HttpResponseLog["body"];
headers: KeyValuePair[];
onHeaderChange?: KeyValuePairTableProps["onChange"];
onHeaderDelete?: KeyValuePairTableProps["onDelete"];
body?: string | null;
onBodyChange?: (value: string) => void;
hasResponse: boolean;
}
@ -24,7 +27,7 @@ const reqNotSent = (
);
function ResponseTabs(props: ResponseTabsProps): JSX.Element {
const { headers, body, hasResponse } = props;
const { headers, onHeaderChange, onHeaderDelete, body, onBodyChange, hasResponse } = props;
const [tabValue, setTabValue] = useState(TabValue.Body);
const contentType = headers.find((header) => header.key.toLowerCase() === "content-type")?.value;
@ -33,6 +36,8 @@ function ResponseTabs(props: ResponseTabsProps): JSX.Element {
textTransform: "none",
};
const headersLength = onHeaderChange ? headers.length - 1 : headers.length;
return (
<Box height="100%" sx={{ display: "flex", flexDirection: "column" }}>
<TabContext value={tabValue}>
@ -43,20 +48,25 @@ function ResponseTabs(props: ResponseTabsProps): JSX.Element {
label={"Body" + (body?.length ? ` (${body.length} byte` + (body.length > 1 ? "s" : "") + ")" : "")}
sx={tabSx}
/>
<Tab
value={TabValue.Headers}
label={"Headers" + (headers.length ? ` (${headers.length})` : "")}
sx={tabSx}
/>
<Tab value={TabValue.Headers} label={"Headers" + (headersLength ? ` (${headersLength})` : "")} sx={tabSx} />
</TabList>
</Box>
<Box flex="1 auto" overflow="hidden">
<TabPanel value={TabValue.Body} sx={{ p: 0, height: "100%" }}>
{body && <Editor content={body} contentType={contentType} />}
{hasResponse && (
<Editor
content={body || ""}
onChange={(value) => {
onBodyChange && onBodyChange(value || "");
}}
monacoOptions={{ readOnly: onBodyChange === undefined }}
contentType={contentType}
/>
)}
{!hasResponse && reqNotSent}
</TabPanel>
<TabPanel value={TabValue.Headers} sx={{ p: 0, height: "100%", overflow: "scroll" }}>
{headers.length > 0 && <KeyValuePairTable items={headers} />}
{hasResponse && <KeyValuePairTable items={headers} onChange={onHeaderChange} onDelete={onHeaderDelete} />}
{!hasResponse && reqNotSent}
</TabPanel>
</Box>

View File

@ -23,6 +23,11 @@ export type CancelRequestResult = {
success: Scalars['Boolean'];
};
export type CancelResponseResult = {
__typename?: 'CancelResponseResult';
success: Scalars['Boolean'];
};
export type ClearHttpRequestLogResult = {
__typename?: 'ClearHTTPRequestLogResult';
success: Scalars['Boolean'];
@ -79,6 +84,7 @@ export type HttpRequest = {
id: Scalars['ID'];
method: HttpMethod;
proto: HttpProtocol;
response?: Maybe<HttpResponse>;
url: Scalars['URL'];
};
@ -105,6 +111,17 @@ export type HttpRequestLogFilterInput = {
searchExpression?: InputMaybe<Scalars['String']>;
};
export type HttpResponse = {
__typename?: 'HttpResponse';
body?: Maybe<Scalars['String']>;
headers: Array<HttpHeader>;
/** Will be the same ID as its related request ID. */
id: Scalars['ID'];
proto: HttpProtocol;
statusCode: Scalars['Int'];
statusReason: Scalars['String'];
};
export type HttpResponseLog = {
__typename?: 'HttpResponseLog';
body?: Maybe<Scalars['String']>;
@ -127,6 +144,7 @@ export type ModifyRequestInput = {
headers?: InputMaybe<Array<HttpHeaderInput>>;
id: Scalars['ID'];
method: HttpMethod;
modifyResponse?: InputMaybe<Scalars['Boolean']>;
proto: HttpProtocol;
url: Scalars['URL'];
};
@ -136,9 +154,24 @@ export type ModifyRequestResult = {
success: Scalars['Boolean'];
};
export type ModifyResponseInput = {
body?: InputMaybe<Scalars['String']>;
headers?: InputMaybe<Array<HttpHeaderInput>>;
proto: HttpProtocol;
requestID: Scalars['ID'];
statusCode: Scalars['Int'];
statusReason: Scalars['String'];
};
export type ModifyResponseResult = {
__typename?: 'ModifyResponseResult';
success: Scalars['Boolean'];
};
export type Mutation = {
__typename?: 'Mutation';
cancelRequest: CancelRequestResult;
cancelResponse: CancelResponseResult;
clearHTTPRequestLog: ClearHttpRequestLogResult;
closeProject: CloseProjectResult;
createOrUpdateSenderRequest: SenderRequest;
@ -147,6 +180,7 @@ export type Mutation = {
deleteProject: DeleteProjectResult;
deleteSenderRequests: DeleteSenderRequestsResult;
modifyRequest: ModifyRequestResult;
modifyResponse: ModifyResponseResult;
openProject?: Maybe<Project>;
sendRequest: SenderRequest;
setHttpRequestLogFilter?: Maybe<HttpRequestLogFilter>;
@ -161,6 +195,11 @@ export type MutationCancelRequestArgs = {
};
export type MutationCancelResponseArgs = {
requestID: Scalars['ID'];
};
export type MutationCreateOrUpdateSenderRequestArgs = {
request: SenderRequestInput;
};
@ -186,6 +225,11 @@ export type MutationModifyRequestArgs = {
};
export type MutationModifyResponseArgs = {
response: ModifyResponseInput;
};
export type MutationOpenProjectArgs = {
id: Scalars['ID'];
};
@ -326,12 +370,19 @@ export type CancelRequestMutationVariables = Exact<{
export type CancelRequestMutation = { __typename?: 'Mutation', cancelRequest: { __typename?: 'CancelRequestResult', success: boolean } };
export type CancelResponseMutationVariables = Exact<{
requestID: Scalars['ID'];
}>;
export type CancelResponseMutation = { __typename?: 'Mutation', cancelResponse: { __typename?: 'CancelResponseResult', success: boolean } };
export type GetInterceptedRequestQueryVariables = Exact<{
id: Scalars['ID'];
}>;
export type GetInterceptedRequestQuery = { __typename?: 'Query', interceptedRequest?: { __typename?: 'HttpRequest', id: string, url: any, method: HttpMethod, proto: HttpProtocol, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null };
export type GetInterceptedRequestQuery = { __typename?: 'Query', interceptedRequest?: { __typename?: 'HttpRequest', id: string, url: any, method: HttpMethod, proto: HttpProtocol, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }>, response?: { __typename?: 'HttpResponse', id: string, proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null };
export type ModifyRequestMutationVariables = Exact<{
request: ModifyRequestInput;
@ -340,6 +391,13 @@ export type ModifyRequestMutationVariables = Exact<{
export type ModifyRequestMutation = { __typename?: 'Mutation', modifyRequest: { __typename?: 'ModifyRequestResult', success: boolean } };
export type ModifyResponseMutationVariables = Exact<{
response: ModifyResponseInput;
}>;
export type ModifyResponseMutation = { __typename?: 'Mutation', modifyResponse: { __typename?: 'ModifyResponseResult', success: boolean } };
export type ActiveProjectQueryVariables = Exact<{ [key: string]: never; }>;
@ -460,7 +518,7 @@ export type UpdateInterceptSettingsMutation = { __typename?: 'Mutation', updateI
export type GetInterceptedRequestsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetInterceptedRequestsQuery = { __typename?: 'Query', interceptedRequests: Array<{ __typename?: 'HttpRequest', id: string, url: any, method: HttpMethod }> };
export type GetInterceptedRequestsQuery = { __typename?: 'Query', interceptedRequests: Array<{ __typename?: 'HttpRequest', id: string, url: any, method: HttpMethod, response?: { __typename?: 'HttpResponse', statusCode: number, statusReason: string } | null }> };
export const CancelRequestDocument = gql`
@ -496,6 +554,39 @@ export function useCancelRequestMutation(baseOptions?: Apollo.MutationHookOption
export type CancelRequestMutationHookResult = ReturnType<typeof useCancelRequestMutation>;
export type CancelRequestMutationResult = Apollo.MutationResult<CancelRequestMutation>;
export type CancelRequestMutationOptions = Apollo.BaseMutationOptions<CancelRequestMutation, CancelRequestMutationVariables>;
export const CancelResponseDocument = gql`
mutation CancelResponse($requestID: ID!) {
cancelResponse(requestID: $requestID) {
success
}
}
`;
export type CancelResponseMutationFn = Apollo.MutationFunction<CancelResponseMutation, CancelResponseMutationVariables>;
/**
* __useCancelResponseMutation__
*
* To run a mutation, you first call `useCancelResponseMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCancelResponseMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [cancelResponseMutation, { data, loading, error }] = useCancelResponseMutation({
* variables: {
* requestID: // value for 'requestID'
* },
* });
*/
export function useCancelResponseMutation(baseOptions?: Apollo.MutationHookOptions<CancelResponseMutation, CancelResponseMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CancelResponseMutation, CancelResponseMutationVariables>(CancelResponseDocument, options);
}
export type CancelResponseMutationHookResult = ReturnType<typeof useCancelResponseMutation>;
export type CancelResponseMutationResult = Apollo.MutationResult<CancelResponseMutation>;
export type CancelResponseMutationOptions = Apollo.BaseMutationOptions<CancelResponseMutation, CancelResponseMutationVariables>;
export const GetInterceptedRequestDocument = gql`
query GetInterceptedRequest($id: ID!) {
interceptedRequest(id: $id) {
@ -508,6 +599,17 @@ export const GetInterceptedRequestDocument = gql`
value
}
body
response {
id
proto
statusCode
statusReason
headers {
key
value
}
body
}
}
}
`;
@ -572,6 +674,39 @@ export function useModifyRequestMutation(baseOptions?: Apollo.MutationHookOption
export type ModifyRequestMutationHookResult = ReturnType<typeof useModifyRequestMutation>;
export type ModifyRequestMutationResult = Apollo.MutationResult<ModifyRequestMutation>;
export type ModifyRequestMutationOptions = Apollo.BaseMutationOptions<ModifyRequestMutation, ModifyRequestMutationVariables>;
export const ModifyResponseDocument = gql`
mutation ModifyResponse($response: ModifyResponseInput!) {
modifyResponse(response: $response) {
success
}
}
`;
export type ModifyResponseMutationFn = Apollo.MutationFunction<ModifyResponseMutation, ModifyResponseMutationVariables>;
/**
* __useModifyResponseMutation__
*
* To run a mutation, you first call `useModifyResponseMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useModifyResponseMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [modifyResponseMutation, { data, loading, error }] = useModifyResponseMutation({
* variables: {
* response: // value for 'response'
* },
* });
*/
export function useModifyResponseMutation(baseOptions?: Apollo.MutationHookOptions<ModifyResponseMutation, ModifyResponseMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ModifyResponseMutation, ModifyResponseMutationVariables>(ModifyResponseDocument, options);
}
export type ModifyResponseMutationHookResult = ReturnType<typeof useModifyResponseMutation>;
export type ModifyResponseMutationResult = Apollo.MutationResult<ModifyResponseMutation>;
export type ModifyResponseMutationOptions = Apollo.BaseMutationOptions<ModifyResponseMutation, ModifyResponseMutationVariables>;
export const ActiveProjectDocument = gql`
query ActiveProject {
activeProject {
@ -1283,6 +1418,10 @@ export const GetInterceptedRequestsDocument = gql`
id
url
method
response {
statusCode
statusReason
}
}
}
`;

View File

@ -3,5 +3,9 @@ query GetInterceptedRequests {
id
url
method
response {
statusCode
statusReason
}
}
}

View File

@ -208,6 +208,7 @@ func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error {
proxy.UseRequestModifier(reqLogService.RequestModifier)
proxy.UseResponseModifier(reqLogService.ResponseModifier)
proxy.UseRequestModifier(interceptService.RequestModifier)
proxy.UseResponseModifier(interceptService.ResponseModifier)
fsSub, err := fs.Sub(adminContent, "admin")
if err != nil {

View File

@ -49,6 +49,10 @@ type ComplexityRoot struct {
Success func(childComplexity int) int
}
CancelResponseResult struct {
Success func(childComplexity int) int
}
ClearHTTPRequestLogResult struct {
Success func(childComplexity int) int
}
@ -71,12 +75,13 @@ type ComplexityRoot struct {
}
HTTPRequest struct {
Body func(childComplexity int) int
Headers func(childComplexity int) int
ID func(childComplexity int) int
Method func(childComplexity int) int
Proto func(childComplexity int) int
URL func(childComplexity int) int
Body func(childComplexity int) int
Headers func(childComplexity int) int
ID func(childComplexity int) int
Method func(childComplexity int) int
Proto func(childComplexity int) int
Response func(childComplexity int) int
URL func(childComplexity int) int
}
HTTPRequestLog struct {
@ -95,6 +100,15 @@ type ComplexityRoot struct {
SearchExpression func(childComplexity int) int
}
HTTPResponse struct {
Body func(childComplexity int) int
Headers func(childComplexity int) int
ID func(childComplexity int) int
Proto func(childComplexity int) int
StatusCode func(childComplexity int) int
StatusReason func(childComplexity int) int
}
HTTPResponseLog struct {
Body func(childComplexity int) int
Headers func(childComplexity int) int
@ -113,8 +127,13 @@ type ComplexityRoot struct {
Success func(childComplexity int) int
}
ModifyResponseResult struct {
Success func(childComplexity int) int
}
Mutation struct {
CancelRequest func(childComplexity int, id ulid.ULID) int
CancelResponse func(childComplexity int, requestID ulid.ULID) int
ClearHTTPRequestLog func(childComplexity int) int
CloseProject func(childComplexity int) int
CreateOrUpdateSenderRequest func(childComplexity int, request SenderRequestInput) int
@ -123,6 +142,7 @@ type ComplexityRoot struct {
DeleteProject func(childComplexity int, id ulid.ULID) int
DeleteSenderRequests func(childComplexity int) int
ModifyRequest func(childComplexity int, request ModifyRequestInput) int
ModifyResponse func(childComplexity int, response ModifyResponseInput) int
OpenProject func(childComplexity int, id ulid.ULID) int
SendRequest func(childComplexity int, id ulid.ULID) int
SetHTTPRequestLogFilter func(childComplexity int, filter *HTTPRequestLogFilterInput) int
@ -199,6 +219,8 @@ type MutationResolver interface {
DeleteSenderRequests(ctx context.Context) (*DeleteSenderRequestsResult, error)
ModifyRequest(ctx context.Context, request ModifyRequestInput) (*ModifyRequestResult, error)
CancelRequest(ctx context.Context, id ulid.ULID) (*CancelRequestResult, error)
ModifyResponse(ctx context.Context, response ModifyResponseInput) (*ModifyResponseResult, error)
CancelResponse(ctx context.Context, requestID ulid.ULID) (*CancelResponseResult, error)
UpdateInterceptSettings(ctx context.Context, input UpdateInterceptSettingsInput) (*InterceptSettings, error)
}
type QueryResolver interface {
@ -236,6 +258,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.CancelRequestResult.Success(childComplexity), true
case "CancelResponseResult.success":
if e.complexity.CancelResponseResult.Success == nil {
break
}
return e.complexity.CancelResponseResult.Success(childComplexity), true
case "ClearHTTPRequestLogResult.success":
if e.complexity.ClearHTTPRequestLogResult.Success == nil {
break
@ -313,6 +342,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.HTTPRequest.Proto(childComplexity), true
case "HttpRequest.response":
if e.complexity.HTTPRequest.Response == nil {
break
}
return e.complexity.HTTPRequest.Response(childComplexity), true
case "HttpRequest.url":
if e.complexity.HTTPRequest.URL == nil {
break
@ -390,6 +426,48 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.HTTPRequestLogFilter.SearchExpression(childComplexity), true
case "HttpResponse.body":
if e.complexity.HTTPResponse.Body == nil {
break
}
return e.complexity.HTTPResponse.Body(childComplexity), true
case "HttpResponse.headers":
if e.complexity.HTTPResponse.Headers == nil {
break
}
return e.complexity.HTTPResponse.Headers(childComplexity), true
case "HttpResponse.id":
if e.complexity.HTTPResponse.ID == nil {
break
}
return e.complexity.HTTPResponse.ID(childComplexity), true
case "HttpResponse.proto":
if e.complexity.HTTPResponse.Proto == nil {
break
}
return e.complexity.HTTPResponse.Proto(childComplexity), true
case "HttpResponse.statusCode":
if e.complexity.HTTPResponse.StatusCode == nil {
break
}
return e.complexity.HTTPResponse.StatusCode(childComplexity), true
case "HttpResponse.statusReason":
if e.complexity.HTTPResponse.StatusReason == nil {
break
}
return e.complexity.HTTPResponse.StatusReason(childComplexity), true
case "HttpResponseLog.body":
if e.complexity.HTTPResponseLog.Body == nil {
break
@ -453,6 +531,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ModifyRequestResult.Success(childComplexity), true
case "ModifyResponseResult.success":
if e.complexity.ModifyResponseResult.Success == nil {
break
}
return e.complexity.ModifyResponseResult.Success(childComplexity), true
case "Mutation.cancelRequest":
if e.complexity.Mutation.CancelRequest == nil {
break
@ -465,6 +550,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.CancelRequest(childComplexity, args["id"].(ulid.ULID)), true
case "Mutation.cancelResponse":
if e.complexity.Mutation.CancelResponse == nil {
break
}
args, err := ec.field_Mutation_cancelResponse_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.CancelResponse(childComplexity, args["requestID"].(ulid.ULID)), true
case "Mutation.clearHTTPRequestLog":
if e.complexity.Mutation.ClearHTTPRequestLog == nil {
break
@ -546,6 +643,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.ModifyRequest(childComplexity, args["request"].(ModifyRequestInput)), true
case "Mutation.modifyResponse":
if e.complexity.Mutation.ModifyResponse == nil {
break
}
args, err := ec.field_Mutation_modifyResponse_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.ModifyResponse(childComplexity, args["response"].(ModifyResponseInput)), true
case "Mutation.openProject":
if e.complexity.Mutation.OpenProject == nil {
break
@ -1044,6 +1153,19 @@ type HttpRequest {
proto: HttpProtocol!
headers: [HttpHeader!]!
body: String
response: HttpResponse
}
type HttpResponse {
"""
Will be the same ID as its related request ID.
"""
id: ID!
proto: HttpProtocol!
statusCode: Int!
statusReason: String!
body: String
headers: [HttpHeader!]!
}
input ModifyRequestInput {
@ -1053,6 +1175,7 @@ input ModifyRequestInput {
proto: HttpProtocol!
headers: [HttpHeaderInput!]
body: String
modifyResponse: Boolean
}
type ModifyRequestResult {
@ -1063,6 +1186,23 @@ type CancelRequestResult {
success: Boolean!
}
input ModifyResponseInput {
requestID: ID!
proto: HttpProtocol!
headers: [HttpHeaderInput!]
body: String
statusCode: Int!
statusReason: String!
}
type ModifyResponseResult {
success: Boolean!
}
type CancelResponseResult {
success: Boolean!
}
input UpdateInterceptSettingsInput {
enabled: Boolean!
requestFilter: String
@ -1103,6 +1243,8 @@ type Mutation {
deleteSenderRequests: DeleteSenderRequestsResult!
modifyRequest(request: ModifyRequestInput!): ModifyRequestResult!
cancelRequest(id: ID!): CancelRequestResult!
modifyResponse(response: ModifyResponseInput!): ModifyResponseResult!
cancelResponse(requestID: ID!): CancelResponseResult!
updateInterceptSettings(
input: UpdateInterceptSettingsInput!
): InterceptSettings!
@ -1152,6 +1294,21 @@ func (ec *executionContext) field_Mutation_cancelRequest_args(ctx context.Contex
return args, nil
}
func (ec *executionContext) field_Mutation_cancelResponse_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 ulid.ULID
if tmp, ok := rawArgs["requestID"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestID"))
arg0, err = ec.unmarshalNID2githubᚗcomᚋoklogᚋulidᚐULID(ctx, tmp)
if err != nil {
return nil, err
}
}
args["requestID"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_createOrUpdateSenderRequest_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@ -1227,6 +1384,21 @@ func (ec *executionContext) field_Mutation_modifyRequest_args(ctx context.Contex
return args, nil
}
func (ec *executionContext) field_Mutation_modifyResponse_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 ModifyResponseInput
if tmp, ok := rawArgs["response"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("response"))
arg0, err = ec.unmarshalNModifyResponseInput2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyResponseInput(ctx, tmp)
if err != nil {
return nil, err
}
}
args["response"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_openProject_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@ -1450,6 +1622,41 @@ func (ec *executionContext) _CancelRequestResult_success(ctx context.Context, fi
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _CancelResponseResult_success(ctx context.Context, field graphql.CollectedField, obj *CancelResponseResult) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "CancelResponseResult",
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.Success, 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) _ClearHTTPRequestLogResult_success(ctx context.Context, field graphql.CollectedField, obj *ClearHTTPRequestLogResult) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -1867,6 +2074,38 @@ func (ec *executionContext) _HttpRequest_body(ctx context.Context, field graphql
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _HttpRequest_response(ctx context.Context, field graphql.CollectedField, obj *HTTPRequest) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "HttpRequest",
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.Response, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*HTTPResponse)
fc.Result = res
return ec.marshalOHttpResponse2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPResponse(ctx, field.Selections, res)
}
func (ec *executionContext) _HttpRequestLog_id(ctx context.Context, field graphql.CollectedField, obj *HTTPRequestLog) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -2208,6 +2447,213 @@ func (ec *executionContext) _HttpRequestLogFilter_searchExpression(ctx context.C
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
}
func (ec *executionContext) _HttpResponse_id(ctx context.Context, field graphql.CollectedField, obj *HTTPResponse) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "HttpResponse",
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.ID, 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.(ulid.ULID)
fc.Result = res
return ec.marshalNID2githubᚗcomᚋoklogᚋulidᚐULID(ctx, field.Selections, res)
}
func (ec *executionContext) _HttpResponse_proto(ctx context.Context, field graphql.CollectedField, obj *HTTPResponse) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "HttpResponse",
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.Proto, 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.(HTTPProtocol)
fc.Result = res
return ec.marshalNHttpProtocol2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPProtocol(ctx, field.Selections, res)
}
func (ec *executionContext) _HttpResponse_statusCode(ctx context.Context, field graphql.CollectedField, obj *HTTPResponse) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "HttpResponse",
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.StatusCode, 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.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _HttpResponse_statusReason(ctx context.Context, field graphql.CollectedField, obj *HTTPResponse) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "HttpResponse",
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.StatusReason, 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.(string)
fc.Result = res
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _HttpResponse_body(ctx context.Context, field graphql.CollectedField, obj *HTTPResponse) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "HttpResponse",
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.Body, 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) _HttpResponse_headers(ctx context.Context, field graphql.CollectedField, obj *HTTPResponse) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "HttpResponse",
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.Headers, 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.([]HTTPHeader)
fc.Result = res
return ec.marshalNHttpHeader2ᚕgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPHeaderᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _HttpResponseLog_id(ctx context.Context, field graphql.CollectedField, obj *HTTPResponseLog) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -2517,6 +2963,41 @@ func (ec *executionContext) _ModifyRequestResult_success(ctx context.Context, fi
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
}
func (ec *executionContext) _ModifyResponseResult_success(ctx context.Context, field graphql.CollectedField, obj *ModifyResponseResult) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "ModifyResponseResult",
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.Success, 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) _Mutation_createProject(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -3072,6 +3553,90 @@ func (ec *executionContext) _Mutation_cancelRequest(ctx context.Context, field g
return ec.marshalNCancelRequestResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelRequestResult(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_modifyResponse(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_modifyResponse_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().ModifyResponse(rctx, args["response"].(ModifyResponseInput))
})
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.(*ModifyResponseResult)
fc.Result = res
return ec.marshalNModifyResponseResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyResponseResult(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_cancelResponse(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_cancelResponse_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().CancelResponse(rctx, args["requestID"].(ulid.ULID))
})
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.(*CancelResponseResult)
fc.Result = res
return ec.marshalNCancelResponseResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelResponseResult(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_updateInterceptSettings(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -5487,6 +6052,77 @@ func (ec *executionContext) unmarshalInputModifyRequestInput(ctx context.Context
if err != nil {
return it, err
}
case "modifyResponse":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("modifyResponse"))
it.ModifyResponse, err = ec.unmarshalOBoolean2ᚖbool(ctx, v)
if err != nil {
return it, err
}
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputModifyResponseInput(ctx context.Context, obj interface{}) (ModifyResponseInput, error) {
var it ModifyResponseInput
asMap := map[string]interface{}{}
for k, v := range obj.(map[string]interface{}) {
asMap[k] = v
}
for k, v := range asMap {
switch k {
case "requestID":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestID"))
it.RequestID, err = ec.unmarshalNID2githubᚗcomᚋoklogᚋulidᚐULID(ctx, v)
if err != nil {
return it, err
}
case "proto":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("proto"))
it.Proto, err = ec.unmarshalNHttpProtocol2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPProtocol(ctx, v)
if err != nil {
return it, err
}
case "headers":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("headers"))
it.Headers, err = ec.unmarshalOHttpHeaderInput2ᚕgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPHeaderInputᚄ(ctx, v)
if err != nil {
return it, err
}
case "body":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("body"))
it.Body, err = ec.unmarshalOString2ᚖstring(ctx, v)
if err != nil {
return it, err
}
case "statusCode":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("statusCode"))
it.StatusCode, err = ec.unmarshalNInt2int(ctx, v)
if err != nil {
return it, err
}
case "statusReason":
var err error
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("statusReason"))
it.StatusReason, err = ec.unmarshalNString2string(ctx, v)
if err != nil {
return it, err
}
}
}
@ -5723,6 +6359,33 @@ func (ec *executionContext) _CancelRequestResult(ctx context.Context, sel ast.Se
return out
}
var cancelResponseResultImplementors = []string{"CancelResponseResult"}
func (ec *executionContext) _CancelResponseResult(ctx context.Context, sel ast.SelectionSet, obj *CancelResponseResult) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, cancelResponseResultImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("CancelResponseResult")
case "success":
out.Values[i] = ec._CancelResponseResult_success(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var clearHTTPRequestLogResultImplementors = []string{"ClearHTTPRequestLogResult"}
func (ec *executionContext) _ClearHTTPRequestLogResult(ctx context.Context, sel ast.SelectionSet, obj *ClearHTTPRequestLogResult) graphql.Marshaler {
@ -5901,6 +6564,8 @@ func (ec *executionContext) _HttpRequest(ctx context.Context, sel ast.SelectionS
}
case "body":
out.Values[i] = ec._HttpRequest_body(ctx, field, obj)
case "response":
out.Values[i] = ec._HttpRequest_response(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@ -5997,6 +6662,55 @@ func (ec *executionContext) _HttpRequestLogFilter(ctx context.Context, sel ast.S
return out
}
var httpResponseImplementors = []string{"HttpResponse"}
func (ec *executionContext) _HttpResponse(ctx context.Context, sel ast.SelectionSet, obj *HTTPResponse) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, httpResponseImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("HttpResponse")
case "id":
out.Values[i] = ec._HttpResponse_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "proto":
out.Values[i] = ec._HttpResponse_proto(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "statusCode":
out.Values[i] = ec._HttpResponse_statusCode(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "statusReason":
out.Values[i] = ec._HttpResponse_statusReason(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "body":
out.Values[i] = ec._HttpResponse_body(ctx, field, obj)
case "headers":
out.Values[i] = ec._HttpResponse_headers(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var httpResponseLogImplementors = []string{"HttpResponseLog"}
func (ec *executionContext) _HttpResponseLog(ctx context.Context, sel ast.SelectionSet, obj *HTTPResponseLog) graphql.Marshaler {
@ -6102,6 +6816,33 @@ func (ec *executionContext) _ModifyRequestResult(ctx context.Context, sel ast.Se
return out
}
var modifyResponseResultImplementors = []string{"ModifyResponseResult"}
func (ec *executionContext) _ModifyResponseResult(ctx context.Context, sel ast.SelectionSet, obj *ModifyResponseResult) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, modifyResponseResultImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("ModifyResponseResult")
case "success":
out.Values[i] = ec._ModifyResponseResult_success(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var mutationImplementors = []string{"Mutation"}
func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
@ -6175,6 +6916,16 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
invalids++
}
case "modifyResponse":
out.Values[i] = ec._Mutation_modifyResponse(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "cancelResponse":
out.Values[i] = ec._Mutation_cancelResponse(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
case "updateInterceptSettings":
out.Values[i] = ec._Mutation_updateInterceptSettings(ctx, field)
if out.Values[i] == graphql.Null {
@ -6832,6 +7583,20 @@ func (ec *executionContext) marshalNCancelRequestResult2ᚖgithubᚗcomᚋdstoti
return ec._CancelRequestResult(ctx, sel, v)
}
func (ec *executionContext) marshalNCancelResponseResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelResponseResult(ctx context.Context, sel ast.SelectionSet, v CancelResponseResult) graphql.Marshaler {
return ec._CancelResponseResult(ctx, sel, &v)
}
func (ec *executionContext) marshalNCancelResponseResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelResponseResult(ctx context.Context, sel ast.SelectionSet, v *CancelResponseResult) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._CancelResponseResult(ctx, sel, v)
}
func (ec *executionContext) marshalNClearHTTPRequestLogResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐClearHTTPRequestLogResult(ctx context.Context, sel ast.SelectionSet, v ClearHTTPRequestLogResult) graphql.Marshaler {
return ec._ClearHTTPRequestLogResult(ctx, sel, &v)
}
@ -7120,6 +7885,25 @@ func (ec *executionContext) marshalNModifyRequestResult2ᚖgithubᚗcomᚋdstoti
return ec._ModifyRequestResult(ctx, sel, v)
}
func (ec *executionContext) unmarshalNModifyResponseInput2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyResponseInput(ctx context.Context, v interface{}) (ModifyResponseInput, error) {
res, err := ec.unmarshalInputModifyResponseInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNModifyResponseResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyResponseResult(ctx context.Context, sel ast.SelectionSet, v ModifyResponseResult) graphql.Marshaler {
return ec._ModifyResponseResult(ctx, sel, &v)
}
func (ec *executionContext) marshalNModifyResponseResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyResponseResult(ctx context.Context, sel ast.SelectionSet, v *ModifyResponseResult) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._ModifyResponseResult(ctx, sel, v)
}
func (ec *executionContext) marshalNProject2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐProject(ctx context.Context, sel ast.SelectionSet, v Project) graphql.Marshaler {
return ec._Project(ctx, sel, &v)
}
@ -7784,6 +8568,13 @@ func (ec *executionContext) unmarshalOHttpRequestLogFilterInput2ᚖgithubᚗcom
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalOHttpResponse2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPResponse(ctx context.Context, sel ast.SelectionSet, v *HTTPResponse) graphql.Marshaler {
if v == nil {
return graphql.Null
}
return ec._HttpResponse(ctx, sel, v)
}
func (ec *executionContext) marshalOHttpResponseLog2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPResponseLog(ctx context.Context, sel ast.SelectionSet, v *HTTPResponseLog) graphql.Marshaler {
if v == nil {
return graphql.Null

View File

@ -16,6 +16,10 @@ type CancelRequestResult struct {
Success bool `json:"success"`
}
type CancelResponseResult struct {
Success bool `json:"success"`
}
type ClearHTTPRequestLogResult struct {
Success bool `json:"success"`
}
@ -43,12 +47,13 @@ type HTTPHeaderInput struct {
}
type HTTPRequest struct {
ID ulid.ULID `json:"id"`
URL *url.URL `json:"url"`
Method HTTPMethod `json:"method"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeader `json:"headers"`
Body *string `json:"body"`
ID ulid.ULID `json:"id"`
URL *url.URL `json:"url"`
Method HTTPMethod `json:"method"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeader `json:"headers"`
Body *string `json:"body"`
Response *HTTPResponse `json:"response"`
}
type HTTPRequestLog struct {
@ -72,6 +77,16 @@ type HTTPRequestLogFilterInput struct {
SearchExpression *string `json:"searchExpression"`
}
type HTTPResponse struct {
// Will be the same ID as its related request ID.
ID ulid.ULID `json:"id"`
Proto HTTPProtocol `json:"proto"`
StatusCode int `json:"statusCode"`
StatusReason string `json:"statusReason"`
Body *string `json:"body"`
Headers []HTTPHeader `json:"headers"`
}
type HTTPResponseLog struct {
// Will be the same ID as its related request ID.
ID ulid.ULID `json:"id"`
@ -88,18 +103,32 @@ type InterceptSettings struct {
}
type ModifyRequestInput struct {
ID ulid.ULID `json:"id"`
URL *url.URL `json:"url"`
Method HTTPMethod `json:"method"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeaderInput `json:"headers"`
Body *string `json:"body"`
ID ulid.ULID `json:"id"`
URL *url.URL `json:"url"`
Method HTTPMethod `json:"method"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeaderInput `json:"headers"`
Body *string `json:"body"`
ModifyResponse *bool `json:"modifyResponse"`
}
type ModifyRequestResult struct {
Success bool `json:"success"`
}
type ModifyResponseInput struct {
RequestID ulid.ULID `json:"requestID"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeaderInput `json:"headers"`
Body *string `json:"body"`
StatusCode int `json:"statusCode"`
StatusReason string `json:"statusReason"`
}
type ModifyResponseResult struct {
Success bool `json:"success"`
}
type Project struct {
ID ulid.ULID `json:"id"`
Name string `json:"name"`

View File

@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"regexp"
@ -515,36 +516,35 @@ func (r *mutationResolver) DeleteSenderRequests(ctx context.Context) (*DeleteSen
return &DeleteSenderRequestsResult{true}, nil
}
func (r *queryResolver) InterceptedRequests(ctx context.Context) ([]HTTPRequest, error) {
reqs := r.InterceptService.Requests()
httpReqs := make([]HTTPRequest, len(reqs))
func (r *queryResolver) InterceptedRequests(ctx context.Context) (httpReqs []HTTPRequest, err error) {
items := r.InterceptService.Items()
for i, req := range reqs {
req, err := parseHTTPRequest(req)
for _, item := range items {
req, err := parseInterceptItem(item)
if err != nil {
return nil, err
}
httpReqs[i] = req
httpReqs = append(httpReqs, req)
}
return httpReqs, nil
}
func (r *queryResolver) InterceptedRequest(ctx context.Context, id ulid.ULID) (*HTTPRequest, error) {
req, err := r.InterceptService.RequestByID(id)
item, err := r.InterceptService.ItemByID(id)
if errors.Is(err, intercept.ErrRequestNotFound) {
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("could not get request by ID: %w", err)
}
httpReq, err := parseHTTPRequest(req)
req, err := parseInterceptItem(item)
if err != nil {
return nil, err
}
return &httpReq, nil
return &req, nil
}
func (r *mutationResolver) ModifyRequest(ctx context.Context, input ModifyRequestInput) (*ModifyRequestResult, error) {
@ -563,7 +563,7 @@ func (r *mutationResolver) ModifyRequest(ctx context.Context, input ModifyReques
req.Header.Add(header.Key, header.Value)
}
err = r.InterceptService.ModifyRequest(input.ID, req)
err = r.InterceptService.ModifyRequest(input.ID, req, input.ModifyResponse)
if err != nil {
return nil, fmt.Errorf("could not modify http request: %w", err)
}
@ -580,6 +580,47 @@ func (r *mutationResolver) CancelRequest(ctx context.Context, id ulid.ULID) (*Ca
return &CancelRequestResult{Success: true}, nil
}
func (r *mutationResolver) ModifyResponse(
ctx context.Context,
input ModifyResponseInput,
) (*ModifyResponseResult, error) {
res := &http.Response{
Header: make(http.Header),
Status: fmt.Sprintf("%v %v", input.StatusCode, input.StatusReason),
StatusCode: input.StatusCode,
Proto: revHTTPProtocolMap[input.Proto],
}
var ok bool
if res.ProtoMajor, res.ProtoMinor, ok = http.ParseHTTPVersion(res.Proto); !ok {
return nil, fmt.Errorf("malformed HTTP version: %q", res.Proto)
}
if input.Body != nil {
res.Body = io.NopCloser(strings.NewReader(*input.Body))
}
for _, header := range input.Headers {
res.Header.Add(header.Key, header.Value)
}
err := r.InterceptService.ModifyResponse(input.RequestID, res)
if err != nil {
return nil, fmt.Errorf("could not modify http request: %w", err)
}
return &ModifyResponseResult{Success: true}, nil
}
func (r *mutationResolver) CancelResponse(ctx context.Context, requestID ulid.ULID) (*CancelResponseResult, error) {
err := r.InterceptService.CancelResponse(requestID)
if err != nil {
return nil, fmt.Errorf("could not cancel http response: %w", err)
}
return &CancelResponseResult{Success: true}, nil
}
func (r *mutationResolver) UpdateInterceptSettings(
ctx context.Context,
input UpdateInterceptSettingsInput,
@ -721,6 +762,79 @@ func parseHTTPRequest(req *http.Request) (HTTPRequest, error) {
return httpReq, nil
}
func parseHTTPResponse(res *http.Response) (HTTPResponse, error) {
resProto := httpProtocolMap[res.Proto]
if !resProto.IsValid() {
return HTTPResponse{}, fmt.Errorf("http response has invalid protocol: %v", res.Proto)
}
id, ok := proxy.RequestIDFromContext(res.Request.Context())
if !ok {
return HTTPResponse{}, errors.New("http response has missing ID")
}
httpRes := HTTPResponse{
ID: id,
Proto: resProto,
StatusCode: res.StatusCode,
}
statusReasonSubs := strings.SplitN(res.Status, " ", 2)
if len(statusReasonSubs) == 2 {
httpRes.StatusReason = statusReasonSubs[1]
}
if res.Header != nil {
httpRes.Headers = make([]HTTPHeader, 0)
for key, values := range res.Header {
for _, value := range values {
httpRes.Headers = append(httpRes.Headers, HTTPHeader{
Key: key,
Value: value,
})
}
}
}
if res.Body != nil {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return HTTPResponse{}, fmt.Errorf("failed to read response body: %w", err)
}
res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
bodyStr := string(body)
httpRes.Body = &bodyStr
}
return httpRes, nil
}
func parseInterceptItem(item intercept.Item) (req HTTPRequest, err error) {
if item.Response != nil {
req, err = parseHTTPRequest(item.Response.Request)
if err != nil {
return HTTPRequest{}, err
}
res, err := parseHTTPResponse(item.Response)
if err != nil {
return HTTPRequest{}, err
}
req.Response = &res
} else if item.Request != nil {
req, err = parseHTTPRequest(item.Request)
if err != nil {
return HTTPRequest{}, err
}
}
return req, nil
}
func parseProject(projSvc proj.Service, p proj.Project) Project {
project := Project{
ID: p.ID,

View File

@ -128,6 +128,19 @@ type HttpRequest {
proto: HttpProtocol!
headers: [HttpHeader!]!
body: String
response: HttpResponse
}
type HttpResponse {
"""
Will be the same ID as its related request ID.
"""
id: ID!
proto: HttpProtocol!
statusCode: Int!
statusReason: String!
body: String
headers: [HttpHeader!]!
}
input ModifyRequestInput {
@ -137,6 +150,7 @@ input ModifyRequestInput {
proto: HttpProtocol!
headers: [HttpHeaderInput!]
body: String
modifyResponse: Boolean
}
type ModifyRequestResult {
@ -147,6 +161,23 @@ type CancelRequestResult {
success: Boolean!
}
input ModifyResponseInput {
requestID: ID!
proto: HttpProtocol!
headers: [HttpHeaderInput!]
body: String
statusCode: Int!
statusReason: String!
}
type ModifyResponseResult {
success: Boolean!
}
type CancelResponseResult {
success: Boolean!
}
input UpdateInterceptSettingsInput {
enabled: Boolean!
requestFilter: String
@ -187,6 +218,8 @@ type Mutation {
deleteSenderRequests: DeleteSenderRequestsResult!
modifyRequest(request: ModifyRequestInput!): ModifyRequestResult!
cancelRequest(id: ID!): CancelRequestResult!
modifyResponse(response: ModifyResponseInput!): ModifyResponseResult!
cancelResponse(requestID: ID!): CancelResponseResult!
updateInterceptSettings(
input: UpdateInterceptSettingsInput!
): InterceptSettings!

View File

@ -13,6 +13,7 @@ import (
"github.com/dstotijn/hetty/pkg/search"
)
//nolint:unparam
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) {

View File

@ -16,11 +16,16 @@ import (
)
var (
ErrRequestAborted = errors.New("intercept: request was aborted")
ErrRequestNotFound = errors.New("intercept: request not found")
ErrRequestDone = errors.New("intercept: request is done")
ErrRequestAborted = errors.New("intercept: request was aborted")
ErrRequestNotFound = errors.New("intercept: request not found")
ErrRequestDone = errors.New("intercept: request is done")
ErrResponseNotFound = errors.New("intercept: response not found")
)
type contextKey int
const interceptResponseKey contextKey = 0
// Request represents a server received HTTP request, alongside a channel for sending a modified version of it to the
// routine that's awaiting it. Also contains a channel for receiving a cancellation signal.
type Request struct {
@ -29,9 +34,24 @@ type Request struct {
done <-chan struct{}
}
// Response represents an HTTP response from a proxied request, alongside a channel for sending a modified version of it
// to the routine that's awaiting it. Also contains a channel for receiving a cancellation signal.
type Response struct {
res *http.Response
ch chan<- *http.Response
done <-chan struct{}
}
type Item struct {
Request *http.Request
Response *http.Response
}
type Service struct {
mu *sync.RWMutex
reqMu *sync.RWMutex
resMu *sync.RWMutex
requests map[ulid.ULID]Request
responses map[ulid.ULID]Response
logger log.Logger
enabled bool
reqFilter search.Expression
@ -48,8 +68,10 @@ type RequestIDs []ulid.ULID
func NewService(cfg Config) *Service {
s := &Service{
mu: &sync.RWMutex{},
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,
@ -62,13 +84,12 @@ func NewService(cfg Config) *Service {
return s
}
// RequestModifier is a proxy.RequestModifyMiddleware for intercepting HTTP
// requests.
// RequestModifier is a proxy.RequestModifyMiddleware for intercepting HTTP requests.
func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
return func(req *http.Request) {
// This is a blocking operation, that gets unblocked when either a modified request is returned or an error
// (typically `context.Canceled`).
modifiedReq, err := svc.Intercept(req.Context(), req)
modifiedReq, err := svc.InterceptRequest(req.Context(), req)
switch {
case errors.Is(err, ErrRequestAborted):
@ -86,24 +107,24 @@ func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
svc.logger.Errorw("Failed to intercept request.",
"error", err)
default:
*req = *modifiedReq.WithContext(req.Context())
*req = *modifiedReq
next(req)
}
}
}
// Intercept adds an HTTP request to an array of pending intercepted requests, alongside channels used for sending a
// cancellation signal and receiving a modified request. It's safe for concurrent use.
func (svc *Service) Intercept(ctx context.Context, req *http.Request) (*http.Request, error) {
// InterceptRequest adds an HTTP request to an array of pending intercepted requests, alongside channels used for
// sending a cancellation signal and receiving a modified request. It's safe for concurrent use.
func (svc *Service) InterceptRequest(ctx context.Context, req *http.Request) (*http.Request, error) {
reqID, ok := proxy.RequestIDFromContext(ctx)
if !ok {
svc.logger.Errorw("Failed to intercept: request doesn't have an ID.")
svc.logger.Errorw("Failed to intercept: context doesn't have an ID.")
return req, nil
}
if !svc.enabled {
// If intercept is disabled, return the incoming request as-is.
svc.logger.Debugw("Bypassed interception: module disabled.")
// If request intercept is disabled, return the incoming request as-is.
svc.logger.Debugw("Bypassed request interception: feature disabled.")
return req, nil
}
@ -116,7 +137,7 @@ func (svc *Service) Intercept(ctx context.Context, req *http.Request) (*http.Req
}
if !match {
svc.logger.Debugw("Bypassed interception: request rules don't match.")
svc.logger.Debugw("Bypassed request interception: request rules don't match.")
return req, nil
}
}
@ -124,20 +145,20 @@ func (svc *Service) Intercept(ctx context.Context, req *http.Request) (*http.Req
ch := make(chan *http.Request)
done := make(chan struct{})
svc.mu.Lock()
svc.reqMu.Lock()
svc.requests[reqID] = Request{
req: req,
ch: ch,
done: done,
}
svc.mu.Unlock()
svc.reqMu.Unlock()
// Whatever happens next (modified request returned, or a context cancelled error), any blocked channel senders
// should be unblocked, and the request should be removed from the requests queue.
defer func() {
close(done)
svc.mu.Lock()
defer svc.mu.Unlock()
svc.reqMu.Lock()
defer svc.reqMu.Unlock()
delete(svc.requests, reqID)
}()
@ -155,15 +176,20 @@ func (svc *Service) Intercept(ctx context.Context, req *http.Request) (*http.Req
// ModifyRequest sends a modified HTTP request to the related channel, or returns ErrRequestDone when the request was
// cancelled. It's safe for concurrent use.
func (svc *Service) ModifyRequest(reqID ulid.ULID, modReq *http.Request) error {
svc.mu.RLock()
func (svc *Service) ModifyRequest(reqID ulid.ULID, modReq *http.Request, modifyResponse *bool) error {
svc.reqMu.RLock()
req, ok := svc.requests[reqID]
svc.mu.RUnlock()
svc.reqMu.RUnlock()
if !ok {
return ErrRequestNotFound
}
*modReq = *modReq.WithContext(req.req.Context())
if modifyResponse != nil {
*modReq = *modReq.WithContext(WithInterceptResponse(modReq.Context(), *modifyResponse))
}
select {
case <-req.done:
return ErrRequestDone
@ -174,12 +200,12 @@ func (svc *Service) ModifyRequest(reqID ulid.ULID, modReq *http.Request) error {
// CancelRequest ensures an intercepted request is dropped.
func (svc *Service) CancelRequest(reqID ulid.ULID) error {
return svc.ModifyRequest(reqID, nil)
return svc.ModifyRequest(reqID, nil, nil)
}
func (svc *Service) ClearRequests() {
svc.mu.Lock()
defer svc.mu.Unlock()
svc.reqMu.Lock()
defer svc.reqMu.Unlock()
for _, req := range svc.requests {
select {
@ -189,47 +215,94 @@ func (svc *Service) ClearRequests() {
}
}
// Requests returns a list of pending intercepted requests. It's safe for concurrent use.
func (svc *Service) Requests() []*http.Request {
svc.mu.RLock()
defer svc.mu.RUnlock()
func (svc *Service) ClearResponses() {
svc.resMu.Lock()
defer svc.resMu.Unlock()
for _, res := range svc.responses {
select {
case <-res.done:
case res.ch <- nil:
}
}
}
// Items returns a list of pending items (requests and responses). It's safe for concurrent use.
func (svc *Service) Items() []Item {
svc.reqMu.RLock()
defer svc.reqMu.RUnlock()
svc.resMu.RLock()
defer svc.resMu.RUnlock()
reqIDs := make([]ulid.ULID, 0, len(svc.requests)+len(svc.responses))
ids := make([]ulid.ULID, 0, len(svc.requests))
for id := range svc.requests {
ids = append(ids, id)
reqIDs = append(reqIDs, id)
}
sort.Sort(RequestIDs(ids))
reqs := make([]*http.Request, len(ids))
for i, id := range ids {
reqs[i] = svc.requests[id].req
for id := range svc.responses {
reqIDs = append(reqIDs, id)
}
return reqs
sort.Sort(RequestIDs(reqIDs))
items := make([]Item, len(reqIDs))
for i, id := range reqIDs {
item := Item{}
if req, ok := svc.requests[id]; ok {
item.Request = req.req
}
if res, ok := svc.responses[id]; ok {
item.Response = res.res
}
items[i] = item
}
return items
}
func (svc *Service) UpdateSettings(settings Settings) {
// When updating from `enabled` -> `disabled`, clear any pending reqs.
if svc.enabled && !settings.Enabled {
svc.ClearRequests()
svc.ClearResponses()
}
svc.enabled = settings.Enabled
svc.reqFilter = settings.RequestFilter
}
// Request returns an intercepted request by ID. It's safe for concurrent use.
func (svc *Service) RequestByID(id ulid.ULID) (*http.Request, error) {
svc.mu.RLock()
defer svc.mu.RUnlock()
// ItemByID returns an intercepted item (request and possible response) by ID. It's safe for concurrent use.
func (svc *Service) ItemByID(id ulid.ULID) (Item, error) {
svc.reqMu.RLock()
defer svc.reqMu.RUnlock()
req, ok := svc.requests[id]
if !ok {
return nil, ErrRequestNotFound
svc.resMu.RLock()
defer svc.resMu.RUnlock()
item := Item{}
found := false
if req, ok := svc.requests[id]; ok {
item.Request = req.req
found = true
}
return req.req, nil
if res, ok := svc.responses[id]; ok {
item.Response = res.res
found = true
}
if !found {
return Item{}, ErrRequestNotFound
}
return item, nil
}
func (ids RequestIDs) Len() int {
@ -243,3 +316,124 @@ func (ids RequestIDs) Less(i, j int) bool {
func (ids RequestIDs) Swap(i, j int) {
ids[i], ids[j] = ids[j], ids[i]
}
func WithInterceptResponse(ctx context.Context, value bool) context.Context {
return context.WithValue(ctx, interceptResponseKey, value)
}
func ShouldInterceptResponseFromContext(ctx context.Context) (bool, bool) {
shouldIntercept, ok := ctx.Value(interceptResponseKey).(bool)
return shouldIntercept, ok
}
// ResponseModifier is a proxy.ResponseModifyMiddleware for intercepting HTTP responses.
func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
return func(res *http.Response) error {
// This is a blocking operation, that gets unblocked when either a modified response is returned or an error.
//nolint:bodyclose
modifiedRes, err := svc.InterceptResponse(res.Request.Context(), res)
if err != nil {
return fmt.Errorf("failed to intercept response: %w", err)
}
*res = *modifiedRes
return next(res)
}
}
// InterceptResponse adds an HTTP response to an array of pending intercepted responses, alongside channels used for
// sending a cancellation signal and receiving a modified response. It's safe for concurrent use.
func (svc *Service) InterceptResponse(ctx context.Context, res *http.Response) (*http.Response, error) {
reqID, ok := proxy.RequestIDFromContext(ctx)
if !ok {
svc.logger.Errorw("Failed to intercept: context doesn't have an ID.")
return res, nil
}
shouldIntercept, ok := ShouldInterceptResponseFromContext(ctx)
if ok && !shouldIntercept {
// If the related request explicitly disabled response intercept, return the response as-is.
svc.logger.Debugw("Bypassed response interception: related request explicitly disabled response intercept.")
return res, nil
}
if !svc.enabled {
// If the feature is disabled, return the response as-is.
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 !match {
// svc.logger.Debugw("Bypassed interception: request rules don't match.")
// return req, nil
// }
// }
ch := make(chan *http.Response)
done := make(chan struct{})
svc.resMu.Lock()
svc.responses[reqID] = Response{
res: res,
ch: ch,
done: done,
}
svc.resMu.Unlock()
// Whatever happens next (modified response returned, or a context cancelled error), any blocked channel senders
// should be unblocked, and the response should be removed from the responses queue.
defer func() {
close(done)
svc.resMu.Lock()
defer svc.resMu.Unlock()
delete(svc.responses, reqID)
}()
select {
case modRes := <-ch:
if modRes == nil {
return nil, ErrRequestAborted
}
return modRes, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
// ModifyResponse sends a modified HTTP response to the related channel, or returns ErrRequestDone when the related
// request was cancelled. It's safe for concurrent use.
func (svc *Service) ModifyResponse(reqID ulid.ULID, modRes *http.Response) error {
svc.resMu.RLock()
res, ok := svc.responses[reqID]
svc.resMu.RUnlock()
if !ok {
return ErrRequestNotFound
}
if modRes != nil {
modRes.Request = res.res.Request
}
select {
case <-res.done:
return ErrRequestDone
case res.ch <- modRes:
return nil
}
}
// CancelResponse ensures an intercepted response is dropped.
func (svc *Service) CancelResponse(reqID ulid.ULID) error {
return svc.ModifyResponse(reqID, nil)
}

View File

@ -34,7 +34,7 @@ func TestRequestModifier(t *testing.T) {
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
err := svc.ModifyRequest(reqID, nil)
err := svc.ModifyRequest(reqID, nil, nil)
if !errors.Is(err, intercept.ErrRequestNotFound) {
t.Fatalf("expected `intercept.ErrRequestNotFound`, got: %v", err)
}
@ -65,7 +65,10 @@ func TestRequestModifier(t *testing.T) {
time.Sleep(10 * time.Millisecond)
cancel()
err := svc.ModifyRequest(reqID, nil)
modReq := req.Clone(req.Context())
modReq.Header.Set("X-Foo", "bar")
err := svc.ModifyRequest(reqID, modReq, nil)
if !errors.Is(err, intercept.ErrRequestDone) {
t.Fatalf("expected `intercept.ErrRequestDone`, got: %v", err)
}
@ -107,7 +110,7 @@ func TestRequestModifier(t *testing.T) {
// array of intercepted reqs.
time.Sleep(10 * time.Millisecond)
err := svc.ModifyRequest(reqID, modReq)
err := svc.ModifyRequest(reqID, modReq, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

View File

@ -13,8 +13,9 @@ import (
"net/http/httputil"
"time"
"github.com/dstotijn/hetty/pkg/log"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/log"
)
//nolint:gosec
@ -189,9 +190,12 @@ func (p *Proxy) clientTLSConn(conn net.Conn) (*tls.Conn, error) {
}
func (p *Proxy) errorHandler(w http.ResponseWriter, r *http.Request, err error) {
if !errors.Is(err, context.Canceled) {
switch {
case !errors.Is(err, context.Canceled):
p.logger.Errorw("Failed to proxy request.",
"error", err)
case errors.Is(err, context.Canceled):
p.logger.Debugw("Proxy request was cancelled.")
}
w.WriteHeader(http.StatusBadGateway)