diff --git a/admin/src/features/intercept/components/EditRequest.tsx b/admin/src/features/intercept/components/EditRequest.tsx index e939531..e9d0708 100644 --- a/admin/src/features/intercept/components/EditRequest.tsx +++ b/admin/src/features/intercept/components/EditRequest.tsx @@ -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([{ key: "", value: "" }]); - const [headers, setHeaders] = useState([{ key: "", value: "" }]); - const [body, setBody] = useState(""); + const [reqHeaders, setReqHeaders] = useState([{ key: "", value: "" }]); + const [resHeaders, setResHeaders] = useState([{ 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 ( @@ -184,64 +250,114 @@ function EditRequest(): JSX.Element { onProtoChange={interceptedReq ? setProto : undefined} sx={{ flex: "1 auto" }} /> - - + {!interceptedRes && ( + <> + + + + )} + {interceptedRes && ( + <> + + + + )} - {modifyResult.error && ( + {modifyReqResult.error && ( - {modifyResult.error.message} + {modifyReqResult.error.message} )} - {cancelResult.error && ( + {cancelReqResult.error && ( - {cancelResult.error.message} + {cancelReqResult.error.message} )} - - - + + {interceptedReq && ( + Request - - + )} + {interceptedRes && ( + + + + Response + + {interceptedRes && ( + + + + )} + + - + )} ); diff --git a/admin/src/features/intercept/graphql/cancelResponse.graphql b/admin/src/features/intercept/graphql/cancelResponse.graphql new file mode 100644 index 0000000..31b12c2 --- /dev/null +++ b/admin/src/features/intercept/graphql/cancelResponse.graphql @@ -0,0 +1,5 @@ +mutation CancelResponse($requestID: ID!) { + cancelResponse(requestID: $requestID) { + success + } +} diff --git a/admin/src/features/intercept/graphql/interceptedRequest.graphql b/admin/src/features/intercept/graphql/interceptedRequest.graphql index ec2749b..ee321f4 100644 --- a/admin/src/features/intercept/graphql/interceptedRequest.graphql +++ b/admin/src/features/intercept/graphql/interceptedRequest.graphql @@ -9,5 +9,16 @@ query GetInterceptedRequest($id: ID!) { value } body + response { + id + proto + statusCode + statusReason + headers { + key + value + } + body + } } } diff --git a/admin/src/features/intercept/graphql/modifyResponse.graphql b/admin/src/features/intercept/graphql/modifyResponse.graphql new file mode 100644 index 0000000..558638e --- /dev/null +++ b/admin/src/features/intercept/graphql/modifyResponse.graphql @@ -0,0 +1,5 @@ +mutation ModifyResponse($response: ModifyResponseInput!) { + modifyResponse(response: $response) { + success + } +} diff --git a/admin/src/lib/components/ResponseTabs.tsx b/admin/src/lib/components/ResponseTabs.tsx index 3a88fa2..62ce1cb 100644 --- a/admin/src/lib/components/ResponseTabs.tsx +++ b/admin/src/lib/components/ResponseTabs.tsx @@ -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 ( @@ -43,20 +48,25 @@ function ResponseTabs(props: ResponseTabsProps): JSX.Element { label={"Body" + (body?.length ? ` (${body.length} byte` + (body.length > 1 ? "s" : "") + ")" : "")} sx={tabSx} /> - + - {body && } + {hasResponse && ( + { + onBodyChange && onBodyChange(value || ""); + }} + monacoOptions={{ readOnly: onBodyChange === undefined }} + contentType={contentType} + /> + )} {!hasResponse && reqNotSent} - {headers.length > 0 && } + {hasResponse && } {!hasResponse && reqNotSent} diff --git a/admin/src/lib/graphql/generated.tsx b/admin/src/lib/graphql/generated.tsx index ceebcf1..6cd3ed7 100644 --- a/admin/src/lib/graphql/generated.tsx +++ b/admin/src/lib/graphql/generated.tsx @@ -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; url: Scalars['URL']; }; @@ -105,6 +111,17 @@ export type HttpRequestLogFilterInput = { searchExpression?: InputMaybe; }; +export type HttpResponse = { + __typename?: 'HttpResponse'; + body?: Maybe; + headers: Array; + /** 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; @@ -127,6 +144,7 @@ export type ModifyRequestInput = { headers?: InputMaybe>; id: Scalars['ID']; method: HttpMethod; + modifyResponse?: InputMaybe; proto: HttpProtocol; url: Scalars['URL']; }; @@ -136,9 +154,24 @@ export type ModifyRequestResult = { success: Scalars['Boolean']; }; +export type ModifyResponseInput = { + body?: InputMaybe; + headers?: InputMaybe>; + 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; sendRequest: SenderRequest; setHttpRequestLogFilter?: Maybe; @@ -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; export type CancelRequestMutationResult = Apollo.MutationResult; export type CancelRequestMutationOptions = Apollo.BaseMutationOptions; +export const CancelResponseDocument = gql` + mutation CancelResponse($requestID: ID!) { + cancelResponse(requestID: $requestID) { + success + } +} + `; +export type CancelResponseMutationFn = Apollo.MutationFunction; + +/** + * __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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CancelResponseDocument, options); + } +export type CancelResponseMutationHookResult = ReturnType; +export type CancelResponseMutationResult = Apollo.MutationResult; +export type CancelResponseMutationOptions = Apollo.BaseMutationOptions; 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; export type ModifyRequestMutationResult = Apollo.MutationResult; export type ModifyRequestMutationOptions = Apollo.BaseMutationOptions; +export const ModifyResponseDocument = gql` + mutation ModifyResponse($response: ModifyResponseInput!) { + modifyResponse(response: $response) { + success + } +} + `; +export type ModifyResponseMutationFn = Apollo.MutationFunction; + +/** + * __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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(ModifyResponseDocument, options); + } +export type ModifyResponseMutationHookResult = ReturnType; +export type ModifyResponseMutationResult = Apollo.MutationResult; +export type ModifyResponseMutationOptions = Apollo.BaseMutationOptions; export const ActiveProjectDocument = gql` query ActiveProject { activeProject { @@ -1283,6 +1418,10 @@ export const GetInterceptedRequestsDocument = gql` id url method + response { + statusCode + statusReason + } } } `; diff --git a/admin/src/lib/graphql/interceptedRequests.graphql b/admin/src/lib/graphql/interceptedRequests.graphql index 8616a40..03ede0b 100644 --- a/admin/src/lib/graphql/interceptedRequests.graphql +++ b/admin/src/lib/graphql/interceptedRequests.graphql @@ -3,5 +3,9 @@ query GetInterceptedRequests { id url method + response { + statusCode + statusReason + } } } diff --git a/cmd/hetty/hetty.go b/cmd/hetty/hetty.go index d7e3672..7b74f69 100644 --- a/cmd/hetty/hetty.go +++ b/cmd/hetty/hetty.go @@ -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 { diff --git a/pkg/api/generated.go b/pkg/api/generated.go index 479ce2b..2b6a609 100644 --- a/pkg/api/generated.go +++ b/pkg/api/generated.go @@ -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 diff --git a/pkg/api/models_gen.go b/pkg/api/models_gen.go index 203cb94..9459fb6 100644 --- a/pkg/api/models_gen.go +++ b/pkg/api/models_gen.go @@ -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"` diff --git a/pkg/api/resolvers.go b/pkg/api/resolvers.go index 577e7ea..f78e9af 100644 --- a/pkg/api/resolvers.go +++ b/pkg/api/resolvers.go @@ -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, diff --git a/pkg/api/schema.graphql b/pkg/api/schema.graphql index c8ba293..88ad21f 100644 --- a/pkg/api/schema.graphql +++ b/pkg/api/schema.graphql @@ -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! diff --git a/pkg/proxy/intercept/filter.go b/pkg/proxy/intercept/filter.go index f208a60..ee15ad9 100644 --- a/pkg/proxy/intercept/filter.go +++ b/pkg/proxy/intercept/filter.go @@ -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) { diff --git a/pkg/proxy/intercept/intercept.go b/pkg/proxy/intercept/intercept.go index ac0b563..06c8441 100644 --- a/pkg/proxy/intercept/intercept.go +++ b/pkg/proxy/intercept/intercept.go @@ -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) +} diff --git a/pkg/proxy/intercept/intercept_test.go b/pkg/proxy/intercept/intercept_test.go index fe683bd..2ed20f8 100644 --- a/pkg/proxy/intercept/intercept_test.go +++ b/pkg/proxy/intercept/intercept_test.go @@ -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) } diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 0f4c57a..99c9283 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -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)