From 02408b51961bee09738822ea4ff701e7d6829085 Mon Sep 17 00:00:00 2001 From: David Stotijn Date: Wed, 23 Mar 2022 14:31:27 +0100 Subject: [PATCH] Add intercept module --- admin/.eslintrc.json | 7 +- admin/src/features/Layout.tsx | 26 +- .../intercept/components/EditRequest.tsx | 366 +++ .../intercept/components/Intercept.tsx | 21 + .../intercept/components/Requests.tsx | 33 + .../intercept/graphql/cancelRequest.graphql | 5 + .../intercept/graphql/cancelResponse.graphql | 5 + .../graphql/interceptedRequest.graphql | 24 + .../intercept/graphql/modifyRequest.graphql | 5 + .../intercept/graphql/modifyResponse.graphql | 5 + .../projects/components/ProjectList.tsx | 7 + .../projects/graphql/activeProject.graphql | 15 + .../features/reqlog/components/Actions.tsx | 61 + .../reqlog/components/RequestLogs.tsx | 10 +- .../src/features/reqlog/components/Search.tsx | 24 - .../sender/components/EditRequest.tsx | 169 +- .../features/settings/components/Settings.tsx | 294 +++ .../graphql/updateInterceptSettings.graphql | 8 + admin/src/lib/ActiveProjectContext.tsx | 6 +- admin/src/lib/InterceptedRequestsContext.tsx | 22 + admin/src/lib/components/Link.tsx | 94 + admin/src/lib/components/ResponseTabs.tsx | 34 +- admin/src/lib/components/UrlBar.tsx | 122 + admin/src/lib/graphql/generated.tsx | 479 +++- .../lib/graphql/interceptedRequests.graphql | 11 + admin/src/lib/graphql/useApollo.ts | 17 +- admin/src/lib/updateKeyPairItem.ts | 16 + admin/src/lib/updateURLQueryParams.ts | 28 + admin/src/pages/_app.tsx | 11 +- admin/src/pages/proxy/intercept/index.tsx | 12 + admin/src/pages/settings/index.tsx | 12 + admin/yarn.lock | 6 +- cmd/hetty/hetty.go | 17 +- pkg/api/generated.go | 2214 +++++++++++++++++ pkg/api/models_gen.go | 80 +- pkg/api/resolvers.go | 345 ++- pkg/api/schema.graphql | 85 + pkg/proj/proj.go | 68 +- pkg/proxy/gzip.go | 35 + pkg/proxy/intercept/filter.go | 395 +++ pkg/proxy/intercept/intercept.go | 452 ++++ pkg/proxy/intercept/intercept_test.go | 270 ++ pkg/proxy/intercept/settings.go | 10 + pkg/proxy/proxy.go | 76 +- pkg/reqlog/reqlog.go | 56 +- pkg/reqlog/reqlog_test.go | 4 +- pkg/reqlog/search.go | 3 +- pkg/search/ast.go | 7 +- pkg/search/parser.go | 2 +- pkg/search/parser_test.go | 6 +- pkg/sender/search.go | 3 +- 51 files changed, 5779 insertions(+), 304 deletions(-) create mode 100644 admin/src/features/intercept/components/EditRequest.tsx create mode 100644 admin/src/features/intercept/components/Intercept.tsx create mode 100644 admin/src/features/intercept/components/Requests.tsx create mode 100644 admin/src/features/intercept/graphql/cancelRequest.graphql create mode 100644 admin/src/features/intercept/graphql/cancelResponse.graphql create mode 100644 admin/src/features/intercept/graphql/interceptedRequest.graphql create mode 100644 admin/src/features/intercept/graphql/modifyRequest.graphql create mode 100644 admin/src/features/intercept/graphql/modifyResponse.graphql create mode 100644 admin/src/features/projects/graphql/activeProject.graphql create mode 100644 admin/src/features/reqlog/components/Actions.tsx create mode 100644 admin/src/features/settings/components/Settings.tsx create mode 100644 admin/src/features/settings/graphql/updateInterceptSettings.graphql create mode 100644 admin/src/lib/InterceptedRequestsContext.tsx create mode 100644 admin/src/lib/components/Link.tsx create mode 100644 admin/src/lib/components/UrlBar.tsx create mode 100644 admin/src/lib/graphql/interceptedRequests.graphql create mode 100644 admin/src/lib/updateKeyPairItem.ts create mode 100644 admin/src/lib/updateURLQueryParams.ts create mode 100644 admin/src/pages/proxy/intercept/index.tsx create mode 100644 admin/src/pages/settings/index.tsx create mode 100644 pkg/proxy/gzip.go create mode 100644 pkg/proxy/intercept/filter.go create mode 100644 pkg/proxy/intercept/intercept.go create mode 100644 pkg/proxy/intercept/intercept_test.go create mode 100644 pkg/proxy/intercept/settings.go diff --git a/admin/.eslintrc.json b/admin/.eslintrc.json index a901afe..c1f671b 100644 --- a/admin/.eslintrc.json +++ b/admin/.eslintrc.json @@ -17,7 +17,12 @@ "prettier/prettier": ["error"], "@next/next/no-css-tags": "off", "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "ignoreRestSiblings": true + } + ], "import/default": "off", diff --git a/admin/src/features/Layout.tsx b/admin/src/features/Layout.tsx index d4fd924..6495d0f 100644 --- a/admin/src/features/Layout.tsx +++ b/admin/src/features/Layout.tsx @@ -1,11 +1,12 @@ +import AltRouteIcon from "@mui/icons-material/AltRoute"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import FolderIcon from "@mui/icons-material/Folder"; +import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted"; import HomeIcon from "@mui/icons-material/Home"; import LocationSearchingIcon from "@mui/icons-material/LocationSearching"; import MenuIcon from "@mui/icons-material/Menu"; import SendIcon from "@mui/icons-material/Send"; -import SettingsEthernetIcon from "@mui/icons-material/SettingsEthernet"; import { Theme, useTheme, @@ -19,6 +20,7 @@ import { CSSObject, Box, ListItemText, + Badge, } from "@mui/material"; import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar"; import MuiDrawer from "@mui/material/Drawer"; @@ -28,15 +30,18 @@ import Link from "next/link"; import React, { useState } from "react"; import { useActiveProject } from "lib/ActiveProjectContext"; +import { useInterceptedRequests } from "lib/InterceptedRequestsContext"; export enum Page { Home, GetStarted, + Intercept, Projects, ProxySetup, ProxyLogs, Sender, Scope, + Settings, } const drawerWidth = 240; @@ -135,6 +140,7 @@ interface Props { export function Layout({ title, page, children }: Props): JSX.Element { const activeProject = useActiveProject(); + const interceptedRequests = useInterceptedRequests(); const theme = useTheme(); const [open, setOpen] = useState(false); @@ -204,12 +210,24 @@ export function Layout({ title, page, children }: Props): JSX.Element { - + - + - + + + + + + + + + + + + + diff --git a/admin/src/features/intercept/components/EditRequest.tsx b/admin/src/features/intercept/components/EditRequest.tsx new file mode 100644 index 0000000..e9d0708 --- /dev/null +++ b/admin/src/features/intercept/components/EditRequest.tsx @@ -0,0 +1,366 @@ +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"; +import { useRouter } from "next/router"; +import React, { useEffect, useState } from "react"; + +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 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"; +import updateURLQueryParams from "lib/updateURLQueryParams"; + +function EditRequest(): JSX.Element { + const router = useRouter(); + const interceptedRequests = useInterceptedRequests(); + + useEffect(() => { + // If there's no request selected and there are pending reqs, navigate to + // the first one in the list. This helps you quickly review/handle reqs + // without having to manually select the next one in the requests table. + if (router.isReady && !router.query.id && interceptedRequests?.length) { + const req = interceptedRequests[0]; + router.replace(`/proxy/intercept?id=${req.id}`); + } + }, [router, interceptedRequests]); + + const reqId = router.query.id as string | undefined; + + const [method, setMethod] = useState(HttpMethod.Get); + const [url, setURL] = useState(""); + const [proto, setProto] = useState(HttpProto.Http20); + const [queryParams, setQueryParams] = useState([{ key: "", value: "" }]); + 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) => { + const updated = updateKeyPairItem(key, value, idx, prev); + setURL((prev) => updateURLQueryParams(prev, updated)); + return updated; + }); + }; + const handleQueryParamDelete = (idx: number) => { + setQueryParams((prev) => { + const updated = prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length)); + setURL((prev) => updateURLQueryParams(prev, updated)); + return updated; + }); + }; + + const handleReqHeaderChange = (key: string, value: string, idx: number) => { + setReqHeaders((prev) => updateKeyPairItem(key, value, idx, prev)); + }; + 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) => { + setURL(url); + + const questionMarkIndex = url.indexOf("?"); + if (questionMarkIndex === -1) { + setQueryParams([{ key: "", value: "" }]); + return; + } + + const newQueryParams = queryParamsFromURL(url); + // Push empty row. + newQueryParams.push({ key: "", value: "" }); + setQueryParams(newQueryParams); + }; + + const getReqResult = useGetInterceptedRequestQuery({ + variables: { id: reqId as string }, + skip: reqId === undefined, + onCompleted: ({ interceptedRequest }) => { + if (!interceptedRequest) { + return; + } + + setURL(interceptedRequest.url); + setMethod(interceptedRequest.method); + setReqBody(interceptedRequest.body || ""); + + const newQueryParams = queryParamsFromURL(interceptedRequest.url); + // Push empty row. + newQueryParams.push({ key: "", value: "" }); + setQueryParams(newQueryParams); + + 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?.response ? getReqResult?.data?.interceptedRequest : undefined; + const interceptedRes = reqId ? getReqResult?.data?.interceptedRequest?.response : undefined; + + const [modifyRequest, modifyReqResult] = useModifyRequestMutation(); + const [cancelRequest, cancelReqResult] = useCancelRequestMutation(); + + const [modifyResponse, modifyResResult] = useModifyResponseMutation(); + const [cancelResponse, cancelResResult] = useCancelResponseMutation(); + + const onActionCompleted = () => { + setURL(""); + setMethod(HttpMethod.Get); + setReqBody(""); + setQueryParams([]); + setReqHeaders([]); + router.replace(`/proxy/intercept`); + }; + + const handleFormSubmit: React.FormEventHandler = (e) => { + e.preventDefault(); + + 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, + }); + } + + 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, + }, + }, + update(cache) { + cache.modify({ + fields: { + interceptedRequests(existing: HttpRequest[], { readField }) { + return existing.filter((ref) => interceptedRes.id !== readField("id", ref)); + }, + }, + }); + }, + onCompleted: onActionCompleted, + }); + } + }; + + const handleReqCancelClick = () => { + if (!interceptedReq) { + return; + } + + cancelRequest({ + variables: { + id: interceptedReq.id, + }, + update(cache) { + cache.modify({ + fields: { + interceptedRequests(existing: HttpRequest[], { readField }) { + return existing.filter((ref) => interceptedReq.id !== readField("id", ref)); + }, + }, + }); + }, + onCompleted: onActionCompleted, + }); + }; + + 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 ( + + + + + {!interceptedRes && ( + <> + + + + )} + {interceptedRes && ( + <> + + + + )} + + + + + + + {modifyReqResult.error && ( + + {modifyReqResult.error.message} + + )} + {cancelReqResult.error && ( + + {cancelReqResult.error.message} + + )} + + + + {interceptedReq && ( + + + Request + + + + )} + {interceptedRes && ( + + + + Response + + {interceptedRes && ( + + + + )} + + + + )} + + + ); +} + +export default EditRequest; diff --git a/admin/src/features/intercept/components/Intercept.tsx b/admin/src/features/intercept/components/Intercept.tsx new file mode 100644 index 0000000..ccc7f68 --- /dev/null +++ b/admin/src/features/intercept/components/Intercept.tsx @@ -0,0 +1,21 @@ +import { Box } from "@mui/material"; + +import EditRequest from "./EditRequest"; +import Requests from "./Requests"; + +import SplitPane from "lib/components/SplitPane"; + +export default function Sender(): JSX.Element { + return ( + + + + + + + + + + + ); +} diff --git a/admin/src/features/intercept/components/Requests.tsx b/admin/src/features/intercept/components/Requests.tsx new file mode 100644 index 0000000..f6d06a8 --- /dev/null +++ b/admin/src/features/intercept/components/Requests.tsx @@ -0,0 +1,33 @@ +import { Box, Paper, Typography } from "@mui/material"; +import { useRouter } from "next/router"; + +import { useInterceptedRequests } from "lib/InterceptedRequestsContext"; +import RequestsTable from "lib/components/RequestsTable"; + +function Requests(): JSX.Element { + const interceptedRequests = useInterceptedRequests(); + + const router = useRouter(); + const activeId = router.query.id as string | undefined; + + const handleRowClick = (id: string) => { + router.push(`/proxy/intercept?id=${id}`); + }; + + return ( + + {interceptedRequests && interceptedRequests.length > 0 && ( + + )} + + {interceptedRequests?.length === 0 && ( + + No pending intercepted requests. + + )} + + + ); +} + +export default Requests; diff --git a/admin/src/features/intercept/graphql/cancelRequest.graphql b/admin/src/features/intercept/graphql/cancelRequest.graphql new file mode 100644 index 0000000..3658c52 --- /dev/null +++ b/admin/src/features/intercept/graphql/cancelRequest.graphql @@ -0,0 +1,5 @@ +mutation CancelRequest($id: ID!) { + cancelRequest(id: $id) { + success + } +} 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 new file mode 100644 index 0000000..ee321f4 --- /dev/null +++ b/admin/src/features/intercept/graphql/interceptedRequest.graphql @@ -0,0 +1,24 @@ +query GetInterceptedRequest($id: ID!) { + interceptedRequest(id: $id) { + id + url + method + proto + headers { + key + value + } + body + response { + id + proto + statusCode + statusReason + headers { + key + value + } + body + } + } +} diff --git a/admin/src/features/intercept/graphql/modifyRequest.graphql b/admin/src/features/intercept/graphql/modifyRequest.graphql new file mode 100644 index 0000000..ed1f06a --- /dev/null +++ b/admin/src/features/intercept/graphql/modifyRequest.graphql @@ -0,0 +1,5 @@ +mutation ModifyRequest($request: ModifyRequestInput!) { + modifyRequest(request: $request) { + success + } +} 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/features/projects/components/ProjectList.tsx b/admin/src/features/projects/components/ProjectList.tsx index 3ad2dd0..7e156c5 100644 --- a/admin/src/features/projects/components/ProjectList.tsx +++ b/admin/src/features/projects/components/ProjectList.tsx @@ -2,6 +2,7 @@ import CloseIcon from "@mui/icons-material/Close"; import DeleteIcon from "@mui/icons-material/Delete"; import DescriptionIcon from "@mui/icons-material/Description"; import LaunchIcon from "@mui/icons-material/Launch"; +import SettingsIcon from "@mui/icons-material/Settings"; import { Alert } from "@mui/lab"; import { Avatar, @@ -29,6 +30,7 @@ import React, { useState } from "react"; import useOpenProjectMutation from "../hooks/useOpenProjectMutation"; +import Link, { NextLinkComposed } from "lib/components/Link"; import { ProjectsQuery, useCloseProjectMutation, @@ -179,6 +181,11 @@ function ProjectList(): JSX.Element { {project.name} {project.isActive && (Active)} + + + + + {project.isActive && ( closeProject()}> diff --git a/admin/src/features/projects/graphql/activeProject.graphql b/admin/src/features/projects/graphql/activeProject.graphql new file mode 100644 index 0000000..4aa3711 --- /dev/null +++ b/admin/src/features/projects/graphql/activeProject.graphql @@ -0,0 +1,15 @@ +query ActiveProject { + activeProject { + id + name + isActive + settings { + intercept { + requestsEnabled + responsesEnabled + requestFilter + responseFilter + } + } + } +} diff --git a/admin/src/features/reqlog/components/Actions.tsx b/admin/src/features/reqlog/components/Actions.tsx new file mode 100644 index 0000000..e95e1f7 --- /dev/null +++ b/admin/src/features/reqlog/components/Actions.tsx @@ -0,0 +1,61 @@ +import AltRouteIcon from "@mui/icons-material/AltRoute"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { Alert } from "@mui/lab"; +import { Badge, Button, IconButton, Tooltip } from "@mui/material"; +import Link from "next/link"; + +import { useActiveProject } from "lib/ActiveProjectContext"; +import { useInterceptedRequests } from "lib/InterceptedRequestsContext"; +import { ConfirmationDialog, useConfirmationDialog } from "lib/components/ConfirmationDialog"; +import { HttpRequestLogsDocument, useClearHttpRequestLogMutation } from "lib/graphql/generated"; + +function Actions(): JSX.Element { + const activeProject = useActiveProject(); + const interceptedRequests = useInterceptedRequests(); + const [clearHTTPRequestLog, clearLogsResult] = useClearHttpRequestLogMutation({ + refetchQueries: [{ query: HttpRequestLogsDocument }], + }); + const clearHTTPConfirmationDialog = useConfirmationDialog(); + + return ( +
+ + All proxy logs are going to be removed. This action cannot be undone. + + + {clearLogsResult.error && Failed to clear HTTP logs: {clearLogsResult.error}} + + {(activeProject?.settings.intercept.requestsEnabled || activeProject?.settings.intercept.responsesEnabled) && ( + + + + )} + + + + + + +
+ ); +} + +export default Actions; diff --git a/admin/src/features/reqlog/components/RequestLogs.tsx b/admin/src/features/reqlog/components/RequestLogs.tsx index daf17a3..787d529 100644 --- a/admin/src/features/reqlog/components/RequestLogs.tsx +++ b/admin/src/features/reqlog/components/RequestLogs.tsx @@ -14,6 +14,7 @@ import { import { useRouter } from "next/router"; import { useState } from "react"; +import Actions from "./Actions"; import LogDetail from "./LogDetail"; import Search from "./Search"; @@ -94,7 +95,14 @@ export function RequestLogs(): JSX.Element { return ( - + + + + + + + + diff --git a/admin/src/features/reqlog/components/Search.tsx b/admin/src/features/reqlog/components/Search.tsx index 93406d4..99e0760 100644 --- a/admin/src/features/reqlog/components/Search.tsx +++ b/admin/src/features/reqlog/components/Search.tsx @@ -1,4 +1,3 @@ -import DeleteIcon from "@mui/icons-material/Delete"; import FilterListIcon from "@mui/icons-material/FilterList"; import SearchIcon from "@mui/icons-material/Search"; import { Alert } from "@mui/lab"; @@ -17,11 +16,8 @@ import { import IconButton from "@mui/material/IconButton"; import React, { useRef, useState } from "react"; -import { ConfirmationDialog, useConfirmationDialog } from "lib/components/ConfirmationDialog"; import { HttpRequestLogFilterDocument, - HttpRequestLogsDocument, - useClearHttpRequestLogMutation, useHttpRequestLogFilterQuery, useSetHttpRequestLogFilterMutation, } from "lib/graphql/generated"; @@ -49,11 +45,6 @@ function Search(): JSX.Element { }, }); - const [clearHTTPRequestLog, clearHTTPRequestLogResult] = useClearHttpRequestLogMutation({ - refetchQueries: [{ query: HttpRequestLogsDocument }], - }); - const clearHTTPConfirmationDialog = useConfirmationDialog(); - const filterRef = useRef(null); const [filterOpen, setFilterOpen] = useState(false); @@ -81,7 +72,6 @@ function Search(): JSX.Element { - - - - - - - - - - All proxy logs are going to be removed. This action cannot be undone. - ); } diff --git a/admin/src/features/sender/components/EditRequest.tsx b/admin/src/features/sender/components/EditRequest.tsx index 04f3402..df24bd8 100644 --- a/admin/src/features/sender/components/EditRequest.tsx +++ b/admin/src/features/sender/components/EditRequest.tsx @@ -1,15 +1,4 @@ -import { - Alert, - Box, - BoxProps, - Button, - InputLabel, - FormControl, - MenuItem, - Select, - TextField, - Typography, -} from "@mui/material"; +import { Alert, Box, Button, Typography } from "@mui/material"; import { useRouter } from "next/router"; import React, { useState } from "react"; @@ -17,76 +6,16 @@ import { KeyValuePair, sortKeyValuePairs } from "lib/components/KeyValuePair"; import RequestTabs from "lib/components/RequestTabs"; import Response from "lib/components/Response"; import SplitPane from "lib/components/SplitPane"; +import UrlBar, { HttpMethod, HttpProto, httpProtoMap } from "lib/components/UrlBar"; import { GetSenderRequestQuery, useCreateOrUpdateSenderRequestMutation, - HttpProtocol, useGetSenderRequestQuery, useSendRequestMutation, } from "lib/graphql/generated"; import { queryParamsFromURL } from "lib/queryParamsFromURL"; - -enum HttpMethod { - Get = "GET", - Post = "POST", - Put = "PUT", - Patch = "PATCH", - Delete = "DELETE", - Head = "HEAD", - Options = "OPTIONS", - Connect = "CONNECT", - Trace = "TRACE", -} - -enum HttpProto { - Http10 = "HTTP/1.0", - Http11 = "HTTP/1.1", - Http20 = "HTTP/2.0", -} - -const httpProtoMap = new Map([ - [HttpProto.Http10, HttpProtocol.Http10], - [HttpProto.Http11, HttpProtocol.Http11], - [HttpProto.Http20, HttpProtocol.Http20], -]); - -function updateKeyPairItem(key: string, value: string, idx: number, items: KeyValuePair[]): KeyValuePair[] { - const updated = [...items]; - updated[idx] = { key, value }; - - // Append an empty key-value pair if the last item in the array isn't blank - // anymore. - if (items.length - 1 === idx && items[idx].key === "" && items[idx].value === "") { - updated.push({ key: "", value: "" }); - } - - return updated; -} - -function updateURLQueryParams(url: string, queryParams: KeyValuePair[]) { - // Note: We don't use the `URL` interface, because we're potentially dealing - // with malformed/incorrect URLs, which would yield TypeErrors when constructed - // via `URL`. - let newURL = url; - - const questionMarkIndex = url.indexOf("?"); - if (questionMarkIndex !== -1) { - newURL = newURL.slice(0, questionMarkIndex); - } - - const searchParams = new URLSearchParams(); - for (const { key, value } of queryParams.filter(({ key }) => key !== "")) { - searchParams.append(key, value); - } - - const rawQueryParams = decodeURI(searchParams.toString()); - - if (rawQueryParams == "") { - return newURL; - } - - return newURL + "?" + rawQueryParams; -} +import updateKeyPairItem from "lib/updateKeyPairItem"; +import updateURLQueryParams from "lib/updateURLQueryParams"; function EditRequest(): JSX.Element { const router = useRouter(); @@ -263,94 +192,4 @@ function EditRequest(): JSX.Element { ); } -interface UrlBarProps extends BoxProps { - method: HttpMethod; - onMethodChange: (method: HttpMethod) => void; - url: string; - onUrlChange: (url: string) => void; - proto: HttpProto; - onProtoChange: (proto: HttpProto) => void; -} - -function UrlBar(props: UrlBarProps) { - const { method, onMethodChange, url, onUrlChange, proto, onProtoChange, ...other } = props; - - return ( - - - Method - - - onUrlChange(e.target.value)} - required - variant="outlined" - InputLabelProps={{ - shrink: true, - }} - InputProps={{ - sx: { - ".MuiOutlinedInput-notchedOutline": { - borderRadius: 0, - }, - }, - }} - sx={{ flexGrow: 1 }} - /> - - Protocol - - - - ); -} - export default EditRequest; diff --git a/admin/src/features/settings/components/Settings.tsx b/admin/src/features/settings/components/Settings.tsx new file mode 100644 index 0000000..1e65899 --- /dev/null +++ b/admin/src/features/settings/components/Settings.tsx @@ -0,0 +1,294 @@ +import { useApolloClient } from "@apollo/client"; +import { TabContext, TabPanel } from "@mui/lab"; +import TabList from "@mui/lab/TabList"; +import { + Alert, + Box, + Button, + CircularProgress, + FormControl, + FormControlLabel, + FormHelperText, + Snackbar, + Switch, + Tab, + TextField, + TextFieldProps, + Typography, +} from "@mui/material"; +import { SwitchBaseProps } from "@mui/material/internal/SwitchBase"; +import { useEffect, useState } from "react"; + +import { useActiveProject } from "lib/ActiveProjectContext"; +import Link from "lib/components/Link"; +import { ActiveProjectDocument, useUpdateInterceptSettingsMutation } from "lib/graphql/generated"; +import { withoutTypename } from "lib/graphql/omitTypename"; + +enum TabValue { + Intercept = "intercept", +} + +function FilterTextField(props: TextFieldProps): JSX.Element { + return ( + + ); +} + +export default function Settings(): JSX.Element { + const client = useApolloClient(); + const activeProject = useActiveProject(); + const [updateInterceptSettings, updateIntercepSettingsResult] = useUpdateInterceptSettingsMutation({ + onCompleted(data) { + client.cache.updateQuery({ query: ActiveProjectDocument }, (cachedData) => ({ + activeProject: { + ...cachedData.activeProject, + settings: { + ...cachedData.activeProject.settings, + intercept: data.updateInterceptSettings, + }, + }, + })); + + setInterceptReqFilter(data.updateInterceptSettings.requestFilter || ""); + setInterceptResFilter(data.updateInterceptSettings.responseFilter || ""); + setSettingsUpdatedOpen(true); + }, + }); + + const [interceptReqFilter, setInterceptReqFilter] = useState(""); + const [interceptResFilter, setInterceptResFilter] = useState(""); + + useEffect(() => { + setInterceptReqFilter(activeProject?.settings.intercept.requestFilter || ""); + }, [activeProject?.settings.intercept.requestFilter]); + + useEffect(() => { + setInterceptResFilter(activeProject?.settings.intercept.responseFilter || ""); + }, [activeProject?.settings.intercept.responseFilter]); + + const handleReqInterceptEnabled: SwitchBaseProps["onChange"] = (e, checked) => { + if (!activeProject) { + e.preventDefault(); + return; + } + + updateInterceptSettings({ + variables: { + input: { + ...withoutTypename(activeProject.settings.intercept), + requestsEnabled: checked, + }, + }, + }); + }; + + const handleResInterceptEnabled: SwitchBaseProps["onChange"] = (e, checked) => { + if (!activeProject) { + e.preventDefault(); + return; + } + + updateInterceptSettings({ + variables: { + input: { + ...withoutTypename(activeProject.settings.intercept), + responsesEnabled: checked, + }, + }, + }); + }; + + const handleInterceptReqFilter = () => { + if (!activeProject) { + return; + } + + updateInterceptSettings({ + variables: { + input: { + ...withoutTypename(activeProject.settings.intercept), + requestFilter: interceptReqFilter, + }, + }, + }); + }; + + const handleInterceptResFilter = () => { + if (!activeProject) { + return; + } + + updateInterceptSettings({ + variables: { + input: { + ...withoutTypename(activeProject.settings.intercept), + responseFilter: interceptResFilter, + }, + }, + }); + }; + + const [tabValue, setTabValue] = useState(TabValue.Intercept); + const [settingsUpdatedOpen, setSettingsUpdatedOpen] = useState(false); + + const handleSettingsUpdatedClose = (_: Event | React.SyntheticEvent, reason?: string) => { + if (reason === "clickaway") { + return; + } + + setSettingsUpdatedOpen(false); + }; + + const tabSx = { + textTransform: "none", + }; + + return ( + + + + Intercept settings have been updated. + + + + + Settings + + + Settings allow you to tweak the behaviour of Hetty’s features. + + + Project settings + + {!activeProject && ( + + There is no project active. To configure project settings, first open a project. + + )} + {activeProject && ( + <> + + setTabValue(value)} sx={{ borderBottom: 1, borderColor: "divider" }}> + + + + + + Requests + + + + } + label="Enable request interception" + labelPlacement="start" + sx={{ display: "inline-block", m: 0 }} + /> + + When enabled, incoming HTTP requests to the proxy are stalled for{" "} + manual review. + + +
+ + setInterceptReqFilter(e.target.value)} + /> + + Filter expression to match incoming requests on. When set, only matching requests are intercepted. + + + +
+ + Responses + + + + } + label="Enable response interception" + labelPlacement="start" + sx={{ display: "inline-block", m: 0 }} + /> + + When enabled, HTTP responses received by the proxy are stalled for{" "} + manual review. + + +
+ + setInterceptResFilter(e.target.value)} + /> + + Filter expression to match received responses on. When set, only matching responses are intercepted. + + + +
+
+
+ + )} +
+ ); +} diff --git a/admin/src/features/settings/graphql/updateInterceptSettings.graphql b/admin/src/features/settings/graphql/updateInterceptSettings.graphql new file mode 100644 index 0000000..ea9c516 --- /dev/null +++ b/admin/src/features/settings/graphql/updateInterceptSettings.graphql @@ -0,0 +1,8 @@ +mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) { + updateInterceptSettings(input: $input) { + requestsEnabled + responsesEnabled + requestFilter + responseFilter + } +} diff --git a/admin/src/lib/ActiveProjectContext.tsx b/admin/src/lib/ActiveProjectContext.tsx index 3bc35bc..a28eca6 100644 --- a/admin/src/lib/ActiveProjectContext.tsx +++ b/admin/src/lib/ActiveProjectContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext } from "react"; -import { Project, useProjectsQuery } from "./graphql/generated"; +import { Project, useActiveProjectQuery } from "./graphql/generated"; const ActiveProjectContext = createContext(null); @@ -9,8 +9,8 @@ interface Props { } export function ActiveProjectProvider({ children }: Props): JSX.Element { - const { data } = useProjectsQuery(); - const project = data?.projects.find((project) => project.isActive) || null; + const { data } = useActiveProjectQuery(); + const project = data?.activeProject || null; return {children}; } diff --git a/admin/src/lib/InterceptedRequestsContext.tsx b/admin/src/lib/InterceptedRequestsContext.tsx new file mode 100644 index 0000000..f2f81dc --- /dev/null +++ b/admin/src/lib/InterceptedRequestsContext.tsx @@ -0,0 +1,22 @@ +import React, { createContext, useContext } from "react"; + +import { GetInterceptedRequestsQuery, useGetInterceptedRequestsQuery } from "./graphql/generated"; + +const InterceptedRequestsContext = createContext(null); + +interface Props { + children?: React.ReactNode | undefined; +} + +export function InterceptedRequestsProvider({ children }: Props): JSX.Element { + const { data } = useGetInterceptedRequestsQuery({ + pollInterval: 1000, + }); + const reqs = data?.interceptedRequests || null; + + return {children}; +} + +export function useInterceptedRequests() { + return useContext(InterceptedRequestsContext); +} diff --git a/admin/src/lib/components/Link.tsx b/admin/src/lib/components/Link.tsx new file mode 100644 index 0000000..a9d8a51 --- /dev/null +++ b/admin/src/lib/components/Link.tsx @@ -0,0 +1,94 @@ +import MuiLink, { LinkProps as MuiLinkProps } from "@mui/material/Link"; +import { styled } from "@mui/material/styles"; +import clsx from "clsx"; +import NextLink, { LinkProps as NextLinkProps } from "next/link"; +import { useRouter } from "next/router"; +import * as React from "react"; + +// Add support for the sx prop for consistency with the other branches. +const Anchor = styled("a")({}); + +interface NextLinkComposedProps + extends Omit, "href">, + Omit { + to: NextLinkProps["href"]; + linkAs?: NextLinkProps["as"]; +} + +export const NextLinkComposed = React.forwardRef(function NextLinkComposed( + props, + ref +) { + const { to, linkAs, replace, scroll, shallow, prefetch, locale, ...other } = props; + + return ( + + + + ); +}); + +export type LinkProps = { + activeClassName?: string; + as?: NextLinkProps["as"]; + href: NextLinkProps["href"]; + linkAs?: NextLinkProps["as"]; // Useful when the as prop is shallow by styled(). + noLinkStyle?: boolean; +} & Omit & + Omit; + +// A styled version of the Next.js Link component: +// https://nextjs.org/docs/api-reference/next/link +const Link = React.forwardRef(function Link(props, ref) { + const { + activeClassName = "active", + as, + className: classNameProps, + href, + linkAs: linkAsProp, + locale, + noLinkStyle, + prefetch, + replace, + role, // Link don't have roles. + scroll, + shallow, + ...other + } = props; + + const router = useRouter(); + const pathname = typeof href === "string" ? href : href.pathname; + const className = clsx(classNameProps, { + [activeClassName]: router.pathname === pathname && activeClassName, + }); + + const isExternal = typeof href === "string" && (href.indexOf("http") === 0 || href.indexOf("mailto:") === 0); + + if (isExternal) { + if (noLinkStyle) { + return ; + } + + return ; + } + + const linkAs = linkAsProp || as; + const nextjsProps = { to: href, linkAs, replace, scroll, shallow, prefetch, locale }; + + if (noLinkStyle) { + return ; + } + + return ; +}); + +export default Link; 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/components/UrlBar.tsx b/admin/src/lib/components/UrlBar.tsx new file mode 100644 index 0000000..b4c6d5a --- /dev/null +++ b/admin/src/lib/components/UrlBar.tsx @@ -0,0 +1,122 @@ +import { Box, BoxProps, FormControl, InputLabel, MenuItem, Select, TextField } from "@mui/material"; + +import { HttpProtocol } from "lib/graphql/generated"; + +export enum HttpMethod { + Get = "GET", + Post = "POST", + Put = "PUT", + Patch = "PATCH", + Delete = "DELETE", + Head = "HEAD", + Options = "OPTIONS", + Connect = "CONNECT", + Trace = "TRACE", +} + +export enum HttpProto { + Http10 = "HTTP/1.0", + Http11 = "HTTP/1.1", + Http20 = "HTTP/2.0", +} + +export const httpProtoMap = new Map([ + [HttpProto.Http10, HttpProtocol.Http10], + [HttpProto.Http11, HttpProtocol.Http11], + [HttpProto.Http20, HttpProtocol.Http20], +]); + +interface UrlBarProps extends BoxProps { + method: HttpMethod; + onMethodChange?: (method: HttpMethod) => void; + url: string; + onUrlChange?: (url: string) => void; + proto: HttpProto; + onProtoChange?: (proto: HttpProto) => void; +} + +function UrlBar(props: UrlBarProps) { + const { method, onMethodChange, url, onUrlChange, proto, onProtoChange, ...other } = props; + + return ( + + + Method + + + onUrlChange && onUrlChange(e.target.value)} + required + variant="outlined" + InputLabelProps={{ + shrink: true, + }} + InputProps={{ + sx: { + ".MuiOutlinedInput-notchedOutline": { + borderRadius: 0, + }, + }, + }} + sx={{ flexGrow: 1 }} + /> + + Protocol + + + + ); +} + +export default UrlBar; diff --git a/admin/src/lib/graphql/generated.tsx b/admin/src/lib/graphql/generated.tsx index 8677011..c64dcbf 100644 --- a/admin/src/lib/graphql/generated.tsx +++ b/admin/src/lib/graphql/generated.tsx @@ -18,6 +18,16 @@ export type Scalars = { URL: any; }; +export type CancelRequestResult = { + __typename?: 'CancelRequestResult'; + success: Scalars['Boolean']; +}; + +export type CancelResponseResult = { + __typename?: 'CancelResponseResult'; + success: Scalars['Boolean']; +}; + export type ClearHttpRequestLogResult = { __typename?: 'ClearHTTPRequestLogResult'; success: Scalars['Boolean']; @@ -67,6 +77,17 @@ export enum HttpProtocol { Http20 = 'HTTP20' } +export type HttpRequest = { + __typename?: 'HttpRequest'; + body?: Maybe; + headers: Array; + id: Scalars['ID']; + method: HttpMethod; + proto: HttpProtocol; + response?: Maybe; + url: Scalars['URL']; +}; + export type HttpRequestLog = { __typename?: 'HttpRequestLog'; body?: Maybe; @@ -90,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; @@ -101,8 +133,47 @@ export type HttpResponseLog = { statusReason: Scalars['String']; }; +export type InterceptSettings = { + __typename?: 'InterceptSettings'; + requestFilter?: Maybe; + requestsEnabled: Scalars['Boolean']; + responseFilter?: Maybe; + responsesEnabled: Scalars['Boolean']; +}; + +export type ModifyRequestInput = { + body?: InputMaybe; + headers?: InputMaybe>; + id: Scalars['ID']; + method: HttpMethod; + modifyResponse?: InputMaybe; + proto: HttpProtocol; + url: Scalars['URL']; +}; + +export type ModifyRequestResult = { + __typename?: '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; @@ -110,11 +181,24 @@ export type Mutation = { createSenderRequestFromHttpRequestLog: SenderRequest; deleteProject: DeleteProjectResult; deleteSenderRequests: DeleteSenderRequestsResult; + modifyRequest: ModifyRequestResult; + modifyResponse: ModifyResponseResult; openProject?: Maybe; sendRequest: SenderRequest; setHttpRequestLogFilter?: Maybe; setScope: Array; setSenderRequestFilter?: Maybe; + updateInterceptSettings: InterceptSettings; +}; + + +export type MutationCancelRequestArgs = { + id: Scalars['ID']; +}; + + +export type MutationCancelResponseArgs = { + requestID: Scalars['ID']; }; @@ -138,6 +222,16 @@ export type MutationDeleteProjectArgs = { }; +export type MutationModifyRequestArgs = { + request: ModifyRequestInput; +}; + + +export type MutationModifyResponseArgs = { + response: ModifyResponseInput; +}; + + export type MutationOpenProjectArgs = { id: Scalars['ID']; }; @@ -162,11 +256,22 @@ export type MutationSetSenderRequestFilterArgs = { filter?: InputMaybe; }; + +export type MutationUpdateInterceptSettingsArgs = { + input: UpdateInterceptSettingsInput; +}; + export type Project = { __typename?: 'Project'; id: Scalars['ID']; isActive: Scalars['Boolean']; name: Scalars['String']; + settings: ProjectSettings; +}; + +export type ProjectSettings = { + __typename?: 'ProjectSettings'; + intercept: InterceptSettings; }; export type Query = { @@ -175,6 +280,8 @@ export type Query = { httpRequestLog?: Maybe; httpRequestLogFilter?: Maybe; httpRequestLogs: Array; + interceptedRequest?: Maybe; + interceptedRequests: Array; projects: Array; scope: Array; senderRequest?: Maybe; @@ -187,6 +294,11 @@ export type QueryHttpRequestLogArgs = { }; +export type QueryInterceptedRequestArgs = { + id: Scalars['ID']; +}; + + export type QuerySenderRequestArgs = { id: Scalars['ID']; }; @@ -248,6 +360,53 @@ export type SenderRequestInput = { url: Scalars['URL']; }; +export type UpdateInterceptSettingsInput = { + requestFilter?: InputMaybe; + requestsEnabled: Scalars['Boolean']; + responseFilter?: InputMaybe; + responsesEnabled: Scalars['Boolean']; +}; + +export type CancelRequestMutationVariables = Exact<{ + id: Scalars['ID']; +}>; + + +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 }>, 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; +}>; + + +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; }>; + + +export type ActiveProjectQuery = { __typename?: 'Query', activeProject?: { __typename?: 'Project', id: string, name: string, isActive: boolean, settings: { __typename?: 'ProjectSettings', intercept: { __typename?: 'InterceptSettings', requestsEnabled: boolean, responsesEnabled: boolean, requestFilter?: string | null, responseFilter?: string | null } } } | null }; + export type CloseProjectMutationVariables = Exact<{ [key: string]: never; }>; @@ -353,7 +512,249 @@ export type GetSenderRequestsQueryVariables = Exact<{ [key: string]: never; }>; export type GetSenderRequestsQuery = { __typename?: 'Query', senderRequests: Array<{ __typename?: 'SenderRequest', id: string, url: any, method: HttpMethod, response?: { __typename?: 'HttpResponseLog', id: string, statusCode: number, statusReason: string } | null }> }; +export type UpdateInterceptSettingsMutationVariables = Exact<{ + input: UpdateInterceptSettingsInput; +}>; + +export type UpdateInterceptSettingsMutation = { __typename?: 'Mutation', updateInterceptSettings: { __typename?: 'InterceptSettings', requestsEnabled: boolean, responsesEnabled: boolean, requestFilter?: string | null, responseFilter?: string | null } }; + +export type GetInterceptedRequestsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type 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` + mutation CancelRequest($id: ID!) { + cancelRequest(id: $id) { + success + } +} + `; +export type CancelRequestMutationFn = Apollo.MutationFunction; + +/** + * __useCancelRequestMutation__ + * + * To run a mutation, you first call `useCancelRequestMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCancelRequestMutation` 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 [cancelRequestMutation, { data, loading, error }] = useCancelRequestMutation({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useCancelRequestMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CancelRequestDocument, options); + } +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) { + id + url + method + proto + headers { + key + value + } + body + response { + id + proto + statusCode + statusReason + headers { + key + value + } + body + } + } +} + `; + +/** + * __useGetInterceptedRequestQuery__ + * + * To run a query within a React component, call `useGetInterceptedRequestQuery` and pass it any options that fit your needs. + * When your component renders, `useGetInterceptedRequestQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetInterceptedRequestQuery({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetInterceptedRequestQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetInterceptedRequestDocument, options); + } +export function useGetInterceptedRequestLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetInterceptedRequestDocument, options); + } +export type GetInterceptedRequestQueryHookResult = ReturnType; +export type GetInterceptedRequestLazyQueryHookResult = ReturnType; +export type GetInterceptedRequestQueryResult = Apollo.QueryResult; +export const ModifyRequestDocument = gql` + mutation ModifyRequest($request: ModifyRequestInput!) { + modifyRequest(request: $request) { + success + } +} + `; +export type ModifyRequestMutationFn = Apollo.MutationFunction; + +/** + * __useModifyRequestMutation__ + * + * To run a mutation, you first call `useModifyRequestMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useModifyRequestMutation` 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 [modifyRequestMutation, { data, loading, error }] = useModifyRequestMutation({ + * variables: { + * request: // value for 'request' + * }, + * }); + */ +export function useModifyRequestMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(ModifyRequestDocument, options); + } +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 { + id + name + isActive + settings { + intercept { + requestsEnabled + responsesEnabled + requestFilter + responseFilter + } + } + } +} + `; + +/** + * __useActiveProjectQuery__ + * + * To run a query within a React component, call `useActiveProjectQuery` and pass it any options that fit your needs. + * When your component renders, `useActiveProjectQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useActiveProjectQuery({ + * variables: { + * }, + * }); + */ +export function useActiveProjectQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(ActiveProjectDocument, options); + } +export function useActiveProjectLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(ActiveProjectDocument, options); + } +export type ActiveProjectQueryHookResult = ReturnType; +export type ActiveProjectLazyQueryHookResult = ReturnType; +export type ActiveProjectQueryResult = Apollo.QueryResult; export const CloseProjectDocument = gql` mutation CloseProject { closeProject { @@ -982,4 +1383,80 @@ export function useGetSenderRequestsLazyQuery(baseOptions?: Apollo.LazyQueryHook } export type GetSenderRequestsQueryHookResult = ReturnType; export type GetSenderRequestsLazyQueryHookResult = ReturnType; -export type GetSenderRequestsQueryResult = Apollo.QueryResult; \ No newline at end of file +export type GetSenderRequestsQueryResult = Apollo.QueryResult; +export const UpdateInterceptSettingsDocument = gql` + mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) { + updateInterceptSettings(input: $input) { + requestsEnabled + responsesEnabled + requestFilter + responseFilter + } +} + `; +export type UpdateInterceptSettingsMutationFn = Apollo.MutationFunction; + +/** + * __useUpdateInterceptSettingsMutation__ + * + * To run a mutation, you first call `useUpdateInterceptSettingsMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateInterceptSettingsMutation` 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 [updateInterceptSettingsMutation, { data, loading, error }] = useUpdateInterceptSettingsMutation({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function useUpdateInterceptSettingsMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpdateInterceptSettingsDocument, options); + } +export type UpdateInterceptSettingsMutationHookResult = ReturnType; +export type UpdateInterceptSettingsMutationResult = Apollo.MutationResult; +export type UpdateInterceptSettingsMutationOptions = Apollo.BaseMutationOptions; +export const GetInterceptedRequestsDocument = gql` + query GetInterceptedRequests { + interceptedRequests { + id + url + method + response { + statusCode + statusReason + } + } +} + `; + +/** + * __useGetInterceptedRequestsQuery__ + * + * To run a query within a React component, call `useGetInterceptedRequestsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetInterceptedRequestsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetInterceptedRequestsQuery({ + * variables: { + * }, + * }); + */ +export function useGetInterceptedRequestsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetInterceptedRequestsDocument, options); + } +export function useGetInterceptedRequestsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetInterceptedRequestsDocument, options); + } +export type GetInterceptedRequestsQueryHookResult = ReturnType; +export type GetInterceptedRequestsLazyQueryHookResult = ReturnType; +export type GetInterceptedRequestsQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/admin/src/lib/graphql/interceptedRequests.graphql b/admin/src/lib/graphql/interceptedRequests.graphql new file mode 100644 index 0000000..03ede0b --- /dev/null +++ b/admin/src/lib/graphql/interceptedRequests.graphql @@ -0,0 +1,11 @@ +query GetInterceptedRequests { + interceptedRequests { + id + url + method + response { + statusCode + statusReason + } + } +} diff --git a/admin/src/lib/graphql/useApollo.ts b/admin/src/lib/graphql/useApollo.ts index 9f2782a..b809fab 100644 --- a/admin/src/lib/graphql/useApollo.ts +++ b/admin/src/lib/graphql/useApollo.ts @@ -8,7 +8,22 @@ function createApolloClient() { link: new HttpLink({ uri: "/api/graphql/", }), - cache: new InMemoryCache(), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + interceptedRequests: { + merge(_, incoming) { + return incoming; + }, + }, + }, + }, + ProjectSettings: { + merge: true, + }, + }, + }), }); } diff --git a/admin/src/lib/updateKeyPairItem.ts b/admin/src/lib/updateKeyPairItem.ts new file mode 100644 index 0000000..e1d531b --- /dev/null +++ b/admin/src/lib/updateKeyPairItem.ts @@ -0,0 +1,16 @@ +import { KeyValuePair } from "./components/KeyValuePair"; + +function updateKeyPairItem(key: string, value: string, idx: number, items: KeyValuePair[]): KeyValuePair[] { + const updated = [...items]; + updated[idx] = { key, value }; + + // Append an empty key-value pair if the last item in the array isn't blank + // anymore. + if (items.length - 1 === idx && items[idx].key === "" && items[idx].value === "") { + updated.push({ key: "", value: "" }); + } + + return updated; +} + +export default updateKeyPairItem; diff --git a/admin/src/lib/updateURLQueryParams.ts b/admin/src/lib/updateURLQueryParams.ts new file mode 100644 index 0000000..17bdbce --- /dev/null +++ b/admin/src/lib/updateURLQueryParams.ts @@ -0,0 +1,28 @@ +import { KeyValuePair } from "./components/KeyValuePair"; + +function updateURLQueryParams(url: string, queryParams: KeyValuePair[]) { + // Note: We don't use the `URL` interface, because we're potentially dealing + // with malformed/incorrect URLs, which would yield TypeErrors when constructed + // via `URL`. + let newURL = url; + + const questionMarkIndex = url.indexOf("?"); + if (questionMarkIndex !== -1) { + newURL = newURL.slice(0, questionMarkIndex); + } + + const searchParams = new URLSearchParams(); + for (const { key, value } of queryParams.filter(({ key }) => key !== "")) { + searchParams.append(key, value); + } + + const rawQueryParams = decodeURI(searchParams.toString()); + + if (rawQueryParams == "") { + return newURL; + } + + return newURL + "?" + rawQueryParams; +} + +export default updateURLQueryParams; diff --git a/admin/src/pages/_app.tsx b/admin/src/pages/_app.tsx index e7eda29..c69ad84 100644 --- a/admin/src/pages/_app.tsx +++ b/admin/src/pages/_app.tsx @@ -7,6 +7,7 @@ import Head from "next/head"; import React from "react"; import { ActiveProjectProvider } from "lib/ActiveProjectContext"; +import { InterceptedRequestsProvider } from "lib/InterceptedRequestsContext"; import { useApollo } from "lib/graphql/useApollo"; import createEmotionCache from "lib/mui/createEmotionCache"; import theme from "lib/mui/theme"; @@ -32,10 +33,12 @@ export default function MyApp(props: MyAppProps) { - - - - + + + + + + diff --git a/admin/src/pages/proxy/intercept/index.tsx b/admin/src/pages/proxy/intercept/index.tsx new file mode 100644 index 0000000..635bc3d --- /dev/null +++ b/admin/src/pages/proxy/intercept/index.tsx @@ -0,0 +1,12 @@ +import { Layout, Page } from "features/Layout"; +import Intercept from "features/intercept/components/Intercept"; + +function ProxyIntercept(): JSX.Element { + return ( + + + + ); +} + +export default ProxyIntercept; diff --git a/admin/src/pages/settings/index.tsx b/admin/src/pages/settings/index.tsx new file mode 100644 index 0000000..64150dd --- /dev/null +++ b/admin/src/pages/settings/index.tsx @@ -0,0 +1,12 @@ +import { Layout, Page } from "features/Layout"; +import Settings from "features/settings/components/Settings"; + +function Index(): JSX.Element { + return ( + + + + ); +} + +export default Index; diff --git a/admin/yarn.lock b/admin/yarn.lock index f527df5..2ef2927 100644 --- a/admin/yarn.lock +++ b/admin/yarn.lock @@ -10,9 +10,9 @@ "@jridgewell/trace-mapping" "^0.3.0" "@apollo/client@^3.2.0": - version "3.5.8" - resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.5.8.tgz#7215b974c5988b6157530eb69369209210349fe0" - integrity sha512-MAm05+I1ullr64VLpZwon/ISnkMuNLf6vDqgo9wiMhHYBGT4yOAbAIseRdjCHZwfSx/7AUuBgaTNOssZPIr6FQ== + version "3.5.10" + resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.5.10.tgz#43463108a6e07ae602cca0afc420805a19339a71" + integrity sha512-tL3iSpFe9Oldq7gYikZK1dcYxp1c01nlSwtsMz75382HcI6fvQXyFXUCJTTK3wgO2/ckaBvRGw7VqjFREdVoRw== dependencies: "@graphql-typed-document-node/core" "^3.0.0" "@wry/context" "^0.6.0" diff --git a/cmd/hetty/hetty.go b/cmd/hetty/hetty.go index 90d7095..7b74f69 100644 --- a/cmd/hetty/hetty.go +++ b/cmd/hetty/hetty.go @@ -28,6 +28,7 @@ import ( "github.com/dstotijn/hetty/pkg/db/badger" "github.com/dstotijn/hetty/pkg/proj" "github.com/dstotijn/hetty/pkg/proxy" + "github.com/dstotijn/hetty/pkg/proxy/intercept" "github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/scope" "github.com/dstotijn/hetty/pkg/sender" @@ -175,16 +176,21 @@ func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error { Logger: cmd.config.logger.Named("reqlog").Sugar(), }) + interceptService := intercept.NewService(intercept.Config{ + Logger: cmd.config.logger.Named("intercept").Sugar(), + }) + senderService := sender.NewService(sender.Config{ Repository: badger, ReqLogService: reqLogService, }) projService, err := proj.NewService(proj.Config{ - Repository: badger, - ReqLogService: reqLogService, - SenderService: senderService, - Scope: scope, + Repository: badger, + InterceptService: interceptService, + ReqLogService: reqLogService, + SenderService: senderService, + Scope: scope, }) if err != nil { cmd.config.logger.Fatal("Failed to create new projects service.", zap.Error(err)) @@ -201,6 +207,8 @@ 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 { @@ -231,6 +239,7 @@ func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error { adminRouter.Path(gqlEndpoint).Handler(api.HTTPHandler(&api.Resolver{ ProjectService: projService, RequestLogService: reqLogService, + InterceptService: interceptService, SenderService: senderService, }, gqlEndpoint)) diff --git a/pkg/api/generated.go b/pkg/api/generated.go index 4aea32f..5a064e3 100644 --- a/pkg/api/generated.go +++ b/pkg/api/generated.go @@ -45,6 +45,14 @@ type DirectiveRoot struct { } type ComplexityRoot struct { + CancelRequestResult struct { + Success func(childComplexity int) int + } + + CancelResponseResult struct { + Success func(childComplexity int) int + } + ClearHTTPRequestLogResult struct { Success func(childComplexity int) int } @@ -66,6 +74,16 @@ type ComplexityRoot struct { Value func(childComplexity int) int } + 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 + Response func(childComplexity int) int + URL func(childComplexity int) int + } + HTTPRequestLog struct { Body func(childComplexity int) int Headers func(childComplexity int) int @@ -82,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 @@ -91,7 +118,24 @@ type ComplexityRoot struct { StatusReason func(childComplexity int) int } + InterceptSettings struct { + RequestFilter func(childComplexity int) int + RequestsEnabled func(childComplexity int) int + ResponseFilter func(childComplexity int) int + ResponsesEnabled func(childComplexity int) int + } + + ModifyRequestResult 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 @@ -99,17 +143,25 @@ type ComplexityRoot struct { CreateSenderRequestFromHTTPRequestLog func(childComplexity int, id ulid.ULID) int 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 SetScope func(childComplexity int, scope []ScopeRuleInput) int SetSenderRequestFilter func(childComplexity int, filter *SenderRequestFilterInput) int + UpdateInterceptSettings func(childComplexity int, input UpdateInterceptSettingsInput) int } Project struct { ID func(childComplexity int) int IsActive func(childComplexity int) int Name func(childComplexity int) int + Settings func(childComplexity int) int + } + + ProjectSettings struct { + Intercept func(childComplexity int) int } Query struct { @@ -117,6 +169,8 @@ type ComplexityRoot struct { HTTPRequestLog func(childComplexity int, id ulid.ULID) int HTTPRequestLogFilter func(childComplexity int) int HTTPRequestLogs func(childComplexity int) int + InterceptedRequest func(childComplexity int, id ulid.ULID) int + InterceptedRequests func(childComplexity int) int Projects func(childComplexity int) int Scope func(childComplexity int) int SenderRequest func(childComplexity int, id ulid.ULID) int @@ -165,6 +219,11 @@ type MutationResolver interface { CreateSenderRequestFromHTTPRequestLog(ctx context.Context, id ulid.ULID) (*SenderRequest, error) SendRequest(ctx context.Context, id ulid.ULID) (*SenderRequest, error) 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 { HTTPRequestLog(ctx context.Context, id ulid.ULID) (*HTTPRequestLog, error) @@ -175,6 +234,8 @@ type QueryResolver interface { Scope(ctx context.Context) ([]ScopeRule, error) SenderRequest(ctx context.Context, id ulid.ULID) (*SenderRequest, error) SenderRequests(ctx context.Context) ([]SenderRequest, error) + InterceptedRequests(ctx context.Context) ([]HTTPRequest, error) + InterceptedRequest(ctx context.Context, id ulid.ULID) (*HTTPRequest, error) } type executableSchema struct { @@ -192,6 +253,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in _ = ec switch typeName + "." + field { + case "CancelRequestResult.success": + if e.complexity.CancelRequestResult.Success == nil { + break + } + + 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 @@ -234,6 +309,55 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.HTTPHeader.Value(childComplexity), true + case "HttpRequest.body": + if e.complexity.HTTPRequest.Body == nil { + break + } + + return e.complexity.HTTPRequest.Body(childComplexity), true + + case "HttpRequest.headers": + if e.complexity.HTTPRequest.Headers == nil { + break + } + + return e.complexity.HTTPRequest.Headers(childComplexity), true + + case "HttpRequest.id": + if e.complexity.HTTPRequest.ID == nil { + break + } + + return e.complexity.HTTPRequest.ID(childComplexity), true + + case "HttpRequest.method": + if e.complexity.HTTPRequest.Method == nil { + break + } + + return e.complexity.HTTPRequest.Method(childComplexity), true + + case "HttpRequest.proto": + if e.complexity.HTTPRequest.Proto == nil { + break + } + + 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 + } + + return e.complexity.HTTPRequest.URL(childComplexity), true + case "HttpRequestLog.body": if e.complexity.HTTPRequestLog.Body == nil { break @@ -304,6 +428,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 @@ -346,6 +512,72 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.HTTPResponseLog.StatusReason(childComplexity), true + case "InterceptSettings.requestFilter": + if e.complexity.InterceptSettings.RequestFilter == nil { + break + } + + return e.complexity.InterceptSettings.RequestFilter(childComplexity), true + + case "InterceptSettings.requestsEnabled": + if e.complexity.InterceptSettings.RequestsEnabled == nil { + break + } + + return e.complexity.InterceptSettings.RequestsEnabled(childComplexity), true + + case "InterceptSettings.responseFilter": + if e.complexity.InterceptSettings.ResponseFilter == nil { + break + } + + return e.complexity.InterceptSettings.ResponseFilter(childComplexity), true + + case "InterceptSettings.responsesEnabled": + if e.complexity.InterceptSettings.ResponsesEnabled == nil { + break + } + + return e.complexity.InterceptSettings.ResponsesEnabled(childComplexity), true + + case "ModifyRequestResult.success": + if e.complexity.ModifyRequestResult.Success == nil { + break + } + + 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 + } + + args, err := ec.field_Mutation_cancelRequest_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + 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 @@ -415,6 +647,30 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.DeleteSenderRequests(childComplexity), true + case "Mutation.modifyRequest": + if e.complexity.Mutation.ModifyRequest == nil { + break + } + + args, err := ec.field_Mutation_modifyRequest_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + 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 @@ -475,6 +731,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.SetSenderRequestFilter(childComplexity, args["filter"].(*SenderRequestFilterInput)), true + case "Mutation.updateInterceptSettings": + if e.complexity.Mutation.UpdateInterceptSettings == nil { + break + } + + args, err := ec.field_Mutation_updateInterceptSettings_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.UpdateInterceptSettings(childComplexity, args["input"].(UpdateInterceptSettingsInput)), true + case "Project.id": if e.complexity.Project.ID == nil { break @@ -496,6 +764,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Project.Name(childComplexity), true + case "Project.settings": + if e.complexity.Project.Settings == nil { + break + } + + return e.complexity.Project.Settings(childComplexity), true + + case "ProjectSettings.intercept": + if e.complexity.ProjectSettings.Intercept == nil { + break + } + + return e.complexity.ProjectSettings.Intercept(childComplexity), true + case "Query.activeProject": if e.complexity.Query.ActiveProject == nil { break @@ -529,6 +811,25 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.HTTPRequestLogs(childComplexity), true + case "Query.interceptedRequest": + if e.complexity.Query.InterceptedRequest == nil { + break + } + + args, err := ec.field_Query_interceptedRequest_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.InterceptedRequest(childComplexity, args["id"].(ulid.ULID)), true + + case "Query.interceptedRequests": + if e.complexity.Query.InterceptedRequests == nil { + break + } + + return e.complexity.Query.InterceptedRequests(childComplexity), true + case "Query.projects": if e.complexity.Query.Projects == nil { break @@ -770,6 +1071,11 @@ type Project { id: ID! name: String! isActive: Boolean! + settings: ProjectSettings! +} + +type ProjectSettings { + intercept: InterceptSettings! } type ScopeRule { @@ -856,6 +1162,77 @@ type SenderRequestFilter { searchExpression: String } +type HttpRequest { + id: ID! + url: URL! + method: HttpMethod! + 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 { + id: ID! + url: URL! + method: HttpMethod! + proto: HttpProtocol! + headers: [HttpHeaderInput!] + body: String + modifyResponse: Boolean +} + +type ModifyRequestResult { + success: Boolean! +} + +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 { + requestsEnabled: Boolean! + responsesEnabled: Boolean! + requestFilter: String + responseFilter: String +} + +type InterceptSettings { + requestsEnabled: Boolean! + responsesEnabled: Boolean! + requestFilter: String + responseFilter: String +} + type Query { httpRequestLog(id: ID!): HttpRequestLog httpRequestLogs: [HttpRequestLog!]! @@ -865,6 +1242,8 @@ type Query { scope: [ScopeRule!]! senderRequest(id: ID!): SenderRequest senderRequests: [SenderRequest!]! + interceptedRequests: [HttpRequest!]! + interceptedRequest(id: ID!): HttpRequest } type Mutation { @@ -882,6 +1261,13 @@ type Mutation { createSenderRequestFromHttpRequestLog(id: ID!): SenderRequest! sendRequest(id: ID!): SenderRequest! deleteSenderRequests: DeleteSenderRequestsResult! + modifyRequest(request: ModifyRequestInput!): ModifyRequestResult! + cancelRequest(id: ID!): CancelRequestResult! + modifyResponse(response: ModifyResponseInput!): ModifyResponseResult! + cancelResponse(requestID: ID!): CancelResponseResult! + updateInterceptSettings( + input: UpdateInterceptSettingsInput! + ): InterceptSettings! } enum HttpMethod { @@ -913,6 +1299,36 @@ var parsedSchema = gqlparser.MustLoadSchema(sources...) // region ***************************** args.gotpl ***************************** +func (ec *executionContext) field_Mutation_cancelRequest_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["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNID2githubᚗcomᚋoklogᚋulidᚐULID(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + 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{}{} @@ -973,6 +1389,36 @@ func (ec *executionContext) field_Mutation_deleteProject_args(ctx context.Contex return args, nil } +func (ec *executionContext) field_Mutation_modifyRequest_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 ModifyRequestInput + if tmp, ok := rawArgs["request"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("request")) + arg0, err = ec.unmarshalNModifyRequestInput2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyRequestInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["request"] = arg0 + 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{}{} @@ -1048,6 +1494,21 @@ func (ec *executionContext) field_Mutation_setSenderRequestFilter_args(ctx conte return args, nil } +func (ec *executionContext) field_Mutation_updateInterceptSettings_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { + var err error + args := map[string]interface{}{} + var arg0 UpdateInterceptSettingsInput + if tmp, ok := rawArgs["input"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("input")) + arg0, err = ec.unmarshalNUpdateInterceptSettingsInput2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐUpdateInterceptSettingsInput(ctx, tmp) + if err != nil { + return nil, err + } + } + args["input"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1078,6 +1539,21 @@ func (ec *executionContext) field_Query_httpRequestLog_args(ctx context.Context, return args, nil } +func (ec *executionContext) field_Query_interceptedRequest_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["id"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + arg0, err = ec.unmarshalNID2githubᚗcomᚋoklogᚋulidᚐULID(ctx, tmp) + if err != nil { + return nil, err + } + } + args["id"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_senderRequest_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -1131,6 +1607,76 @@ func (ec *executionContext) field___Type_fields_args(ctx context.Context, rawArg // region **************************** field.gotpl ***************************** +func (ec *executionContext) _CancelRequestResult_success(ctx context.Context, field graphql.CollectedField, obj *CancelRequestResult) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "CancelRequestResult", + 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) _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 { @@ -1341,6 +1887,245 @@ func (ec *executionContext) _HttpHeader_value(ctx context.Context, field graphql return ec.marshalNString2string(ctx, field.Selections, res) } +func (ec *executionContext) _HttpRequest_id(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.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) _HttpRequest_url(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.URL, 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.(*url.URL) + fc.Result = res + return ec.marshalNURL2ᚖnetᚋurlᚐURL(ctx, field.Selections, res) +} + +func (ec *executionContext) _HttpRequest_method(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.Method, 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.(HTTPMethod) + fc.Result = res + return ec.marshalNHttpMethod2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPMethod(ctx, field.Selections, res) +} + +func (ec *executionContext) _HttpRequest_proto(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.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) _HttpRequest_headers(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.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) _HttpRequest_body(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.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) _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 { @@ -1682,6 +2467,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 { @@ -1889,6 +2881,210 @@ func (ec *executionContext) _HttpResponseLog_headers(ctx context.Context, field return ec.marshalNHttpHeader2ᚕgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPHeaderᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _InterceptSettings_requestsEnabled(ctx context.Context, field graphql.CollectedField, obj *InterceptSettings) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "InterceptSettings", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.RequestsEnabled, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) _InterceptSettings_responsesEnabled(ctx context.Context, field graphql.CollectedField, obj *InterceptSettings) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "InterceptSettings", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ResponsesEnabled, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(bool) + fc.Result = res + return ec.marshalNBoolean2bool(ctx, field.Selections, res) +} + +func (ec *executionContext) _InterceptSettings_requestFilter(ctx context.Context, field graphql.CollectedField, obj *InterceptSettings) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "InterceptSettings", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.RequestFilter, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _InterceptSettings_responseFilter(ctx context.Context, field graphql.CollectedField, obj *InterceptSettings) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "InterceptSettings", + Field: field, + Args: nil, + IsMethod: false, + IsResolver: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.ResponseFilter, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) _ModifyRequestResult_success(ctx context.Context, field graphql.CollectedField, obj *ModifyRequestResult) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ModifyRequestResult", + 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) _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 { @@ -2360,6 +3556,216 @@ func (ec *executionContext) _Mutation_deleteSenderRequests(ctx context.Context, return ec.marshalNDeleteSenderRequestsResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐDeleteSenderRequestsResult(ctx, field.Selections, res) } +func (ec *executionContext) _Mutation_modifyRequest(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_modifyRequest_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().ModifyRequest(rctx, args["request"].(ModifyRequestInput)) + }) + 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.(*ModifyRequestResult) + fc.Result = res + return ec.marshalNModifyRequestResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyRequestResult(ctx, field.Selections, res) +} + +func (ec *executionContext) _Mutation_cancelRequest(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_cancelRequest_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().CancelRequest(rctx, args["id"].(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.(*CancelRequestResult) + fc.Result = res + 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 { + 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_updateInterceptSettings_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().UpdateInterceptSettings(rctx, args["input"].(UpdateInterceptSettingsInput)) + }) + 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.(*InterceptSettings) + fc.Result = res + return ec.marshalNInterceptSettings2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐInterceptSettings(ctx, field.Selections, res) +} + func (ec *executionContext) _Project_id(ctx context.Context, field graphql.CollectedField, obj *Project) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2465,6 +3871,76 @@ func (ec *executionContext) _Project_isActive(ctx context.Context, field graphql return ec.marshalNBoolean2bool(ctx, field.Selections, res) } +func (ec *executionContext) _Project_settings(ctx context.Context, field graphql.CollectedField, obj *Project) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "Project", + 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.Settings, 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.(*ProjectSettings) + fc.Result = res + return ec.marshalNProjectSettings2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐProjectSettings(ctx, field.Selections, res) +} + +func (ec *executionContext) _ProjectSettings_intercept(ctx context.Context, field graphql.CollectedField, obj *ProjectSettings) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ProjectSettings", + 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.Intercept, 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.(*InterceptSettings) + fc.Result = res + return ec.marshalNInterceptSettings2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐInterceptSettings(ctx, field.Selections, res) +} + func (ec *executionContext) _Query_httpRequestLog(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -2747,6 +4223,80 @@ func (ec *executionContext) _Query_senderRequests(ctx context.Context, field gra return ec.marshalNSenderRequest2ᚕgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐSenderRequestᚄ(ctx, field.Selections, res) } +func (ec *executionContext) _Query_interceptedRequests(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: "Query", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + 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 ec.resolvers.Query().InterceptedRequests(rctx) + }) + 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.([]HTTPRequest) + fc.Result = res + return ec.marshalNHttpRequest2ᚕgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPRequestᚄ(ctx, field.Selections, res) +} + +func (ec *executionContext) _Query_interceptedRequest(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: "Query", + Field: field, + Args: nil, + IsMethod: true, + IsResolver: true, + } + + ctx = graphql.WithFieldContext(ctx, fc) + rawArgs := field.ArgumentMap(ec.Variables) + args, err := ec.field_Query_interceptedRequest_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.Query().InterceptedRequest(rctx, args["id"].(ulid.ULID)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*HTTPRequest) + fc.Result = res + return ec.marshalOHttpRequest2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPRequest(ctx, field.Selections, res) +} + func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -4532,6 +6082,140 @@ func (ec *executionContext) unmarshalInputHttpRequestLogFilterInput(ctx context. return it, nil } +func (ec *executionContext) unmarshalInputModifyRequestInput(ctx context.Context, obj interface{}) (ModifyRequestInput, error) { + var it ModifyRequestInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "id": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + it.ID, err = ec.unmarshalNID2githubᚗcomᚋoklogᚋulidᚐULID(ctx, v) + if err != nil { + return it, err + } + case "url": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("url")) + it.URL, err = ec.unmarshalNURL2ᚖnetᚋurlᚐURL(ctx, v) + if err != nil { + return it, err + } + case "method": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("method")) + it.Method, err = ec.unmarshalNHttpMethod2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPMethod(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 "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 + } + } + } + + return it, nil +} + func (ec *executionContext) unmarshalInputScopeHeaderInput(ctx context.Context, obj interface{}) (ScopeHeaderInput, error) { var it ScopeHeaderInput asMap := map[string]interface{}{} @@ -4696,6 +6380,53 @@ func (ec *executionContext) unmarshalInputSenderRequestInput(ctx context.Context return it, nil } +func (ec *executionContext) unmarshalInputUpdateInterceptSettingsInput(ctx context.Context, obj interface{}) (UpdateInterceptSettingsInput, error) { + var it UpdateInterceptSettingsInput + asMap := map[string]interface{}{} + for k, v := range obj.(map[string]interface{}) { + asMap[k] = v + } + + for k, v := range asMap { + switch k { + case "requestsEnabled": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestsEnabled")) + it.RequestsEnabled, err = ec.unmarshalNBoolean2bool(ctx, v) + if err != nil { + return it, err + } + case "responsesEnabled": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("responsesEnabled")) + it.ResponsesEnabled, err = ec.unmarshalNBoolean2bool(ctx, v) + if err != nil { + return it, err + } + case "requestFilter": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestFilter")) + it.RequestFilter, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + case "responseFilter": + var err error + + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("responseFilter")) + it.ResponseFilter, err = ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + } + } + + return it, nil +} + // endregion **************************** input.gotpl ***************************** // region ************************** interface.gotpl *************************** @@ -4704,6 +6435,60 @@ func (ec *executionContext) unmarshalInputSenderRequestInput(ctx context.Context // region **************************** object.gotpl **************************** +var cancelRequestResultImplementors = []string{"CancelRequestResult"} + +func (ec *executionContext) _CancelRequestResult(ctx context.Context, sel ast.SelectionSet, obj *CancelRequestResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, cancelRequestResultImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("CancelRequestResult") + case "success": + out.Values[i] = ec._CancelRequestResult_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 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 { @@ -4844,6 +6629,57 @@ func (ec *executionContext) _HttpHeader(ctx context.Context, sel ast.SelectionSe return out } +var httpRequestImplementors = []string{"HttpRequest"} + +func (ec *executionContext) _HttpRequest(ctx context.Context, sel ast.SelectionSet, obj *HTTPRequest) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, httpRequestImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("HttpRequest") + case "id": + out.Values[i] = ec._HttpRequest_id(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "url": + out.Values[i] = ec._HttpRequest_url(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "method": + out.Values[i] = ec._HttpRequest_method(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "proto": + out.Values[i] = ec._HttpRequest_proto(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "headers": + out.Values[i] = ec._HttpRequest_headers(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + 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)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var httpRequestLogImplementors = []string{"HttpRequestLog"} func (ec *executionContext) _HttpRequestLog(ctx context.Context, sel ast.SelectionSet, obj *HTTPRequestLog) graphql.Marshaler { @@ -4929,6 +6765,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 { @@ -4978,6 +6863,96 @@ func (ec *executionContext) _HttpResponseLog(ctx context.Context, sel ast.Select return out } +var interceptSettingsImplementors = []string{"InterceptSettings"} + +func (ec *executionContext) _InterceptSettings(ctx context.Context, sel ast.SelectionSet, obj *InterceptSettings) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, interceptSettingsImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("InterceptSettings") + case "requestsEnabled": + out.Values[i] = ec._InterceptSettings_requestsEnabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "responsesEnabled": + out.Values[i] = ec._InterceptSettings_responsesEnabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "requestFilter": + out.Values[i] = ec._InterceptSettings_requestFilter(ctx, field, obj) + case "responseFilter": + out.Values[i] = ec._InterceptSettings_responseFilter(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + +var modifyRequestResultImplementors = []string{"ModifyRequestResult"} + +func (ec *executionContext) _ModifyRequestResult(ctx context.Context, sel ast.SelectionSet, obj *ModifyRequestResult) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, modifyRequestResultImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ModifyRequestResult") + case "success": + out.Values[i] = ec._ModifyRequestResult_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 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 { @@ -5041,6 +7016,31 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { invalids++ } + case "modifyRequest": + out.Values[i] = ec._Mutation_modifyRequest(ctx, field) + if out.Values[i] == graphql.Null { + invalids++ + } + case "cancelRequest": + out.Values[i] = ec._Mutation_cancelRequest(ctx, field) + 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 { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -5078,6 +7078,38 @@ func (ec *executionContext) _Project(ctx context.Context, sel ast.SelectionSet, if out.Values[i] == graphql.Null { invalids++ } + case "settings": + out.Values[i] = ec._Project_settings(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 projectSettingsImplementors = []string{"ProjectSettings"} + +func (ec *executionContext) _ProjectSettings(ctx context.Context, sel ast.SelectionSet, obj *ProjectSettings) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, projectSettingsImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ProjectSettings") + case "intercept": + out.Values[i] = ec._ProjectSettings_intercept(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -5204,6 +7236,31 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } return res }) + case "interceptedRequests": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_interceptedRequests(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) + case "interceptedRequest": + field := field + out.Concurrently(i, func() (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query_interceptedRequest(ctx, field) + return res + }) case "__type": out.Values[i] = ec._Query___type(ctx, field) case "__schema": @@ -5622,6 +7679,34 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se return res } +func (ec *executionContext) marshalNCancelRequestResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelRequestResult(ctx context.Context, sel ast.SelectionSet, v CancelRequestResult) graphql.Marshaler { + return ec._CancelRequestResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalNCancelRequestResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐCancelRequestResult(ctx context.Context, sel ast.SelectionSet, v *CancelRequestResult) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + 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) } @@ -5751,6 +7836,54 @@ func (ec *executionContext) marshalNHttpProtocol2githubᚗcomᚋdstotijnᚋhetty return v } +func (ec *executionContext) marshalNHttpRequest2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPRequest(ctx context.Context, sel ast.SelectionSet, v HTTPRequest) graphql.Marshaler { + return ec._HttpRequest(ctx, sel, &v) +} + +func (ec *executionContext) marshalNHttpRequest2ᚕgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPRequestᚄ(ctx context.Context, sel ast.SelectionSet, v []HTTPRequest) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalNHttpRequest2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPRequest(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) marshalNHttpRequestLog2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPRequestLog(ctx context.Context, sel ast.SelectionSet, v HTTPRequestLog) graphql.Marshaler { return ec._HttpRequestLog(ctx, sel, &v) } @@ -5829,6 +7962,58 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } +func (ec *executionContext) marshalNInterceptSettings2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐInterceptSettings(ctx context.Context, sel ast.SelectionSet, v InterceptSettings) graphql.Marshaler { + return ec._InterceptSettings(ctx, sel, &v) +} + +func (ec *executionContext) marshalNInterceptSettings2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐInterceptSettings(ctx context.Context, sel ast.SelectionSet, v *InterceptSettings) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._InterceptSettings(ctx, sel, v) +} + +func (ec *executionContext) unmarshalNModifyRequestInput2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyRequestInput(ctx context.Context, v interface{}) (ModifyRequestInput, error) { + res, err := ec.unmarshalInputModifyRequestInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNModifyRequestResult2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyRequestResult(ctx context.Context, sel ast.SelectionSet, v ModifyRequestResult) graphql.Marshaler { + return ec._ModifyRequestResult(ctx, sel, &v) +} + +func (ec *executionContext) marshalNModifyRequestResult2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐModifyRequestResult(ctx context.Context, sel ast.SelectionSet, v *ModifyRequestResult) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + 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) } @@ -5877,6 +8062,16 @@ func (ec *executionContext) marshalNProject2ᚕgithubᚗcomᚋdstotijnᚋhetty return ret } +func (ec *executionContext) marshalNProjectSettings2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐProjectSettings(ctx context.Context, sel ast.SelectionSet, v *ProjectSettings) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + return ec._ProjectSettings(ctx, sel, v) +} + func (ec *executionContext) marshalNScopeRule2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐScopeRule(ctx context.Context, sel ast.SelectionSet, v ScopeRule) graphql.Marshaler { return ec._ScopeRule(ctx, sel, &v) } @@ -6065,6 +8260,11 @@ func (ec *executionContext) marshalNURL2ᚖnetᚋurlᚐURL(ctx context.Context, return res } +func (ec *executionContext) unmarshalNUpdateInterceptSettingsInput2githubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐUpdateInterceptSettingsInput(ctx context.Context, v interface{}) (UpdateInterceptSettingsInput, error) { + res, err := ec.unmarshalInputUpdateInterceptSettingsInput(ctx, v) + return res, graphql.ErrorOnPath(ctx, err) +} + func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler { return ec.___Directive(ctx, sel, &v) } @@ -6449,6 +8649,13 @@ func (ec *executionContext) marshalOHttpProtocol2ᚖgithubᚗcomᚋdstotijnᚋhe return v } +func (ec *executionContext) marshalOHttpRequest2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPRequest(ctx context.Context, sel ast.SelectionSet, v *HTTPRequest) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._HttpRequest(ctx, sel, v) +} + func (ec *executionContext) marshalOHttpRequestLog2ᚖgithubᚗcomᚋdstotijnᚋhettyᚋpkgᚋapiᚐHTTPRequestLog(ctx context.Context, sel ast.SelectionSet, v *HTTPRequestLog) graphql.Marshaler { if v == nil { return graphql.Null @@ -6471,6 +8678,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 8f4d3e0..9c480b5 100644 --- a/pkg/api/models_gen.go +++ b/pkg/api/models_gen.go @@ -12,6 +12,14 @@ import ( "github.com/oklog/ulid" ) +type CancelRequestResult struct { + Success bool `json:"success"` +} + +type CancelResponseResult struct { + Success bool `json:"success"` +} + type ClearHTTPRequestLogResult struct { Success bool `json:"success"` } @@ -38,6 +46,16 @@ type HTTPHeaderInput struct { Value string `json:"value"` } +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"` + Response *HTTPResponse `json:"response"` +} + type HTTPRequestLog struct { ID ulid.ULID `json:"id"` URL string `json:"url"` @@ -59,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"` @@ -69,10 +97,49 @@ type HTTPResponseLog struct { Headers []HTTPHeader `json:"headers"` } +type InterceptSettings struct { + RequestsEnabled bool `json:"requestsEnabled"` + ResponsesEnabled bool `json:"responsesEnabled"` + RequestFilter *string `json:"requestFilter"` + ResponseFilter *string `json:"responseFilter"` +} + +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"` + 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"` - IsActive bool `json:"isActive"` + ID ulid.ULID `json:"id"` + Name string `json:"name"` + IsActive bool `json:"isActive"` + Settings *ProjectSettings `json:"settings"` +} + +type ProjectSettings struct { + Intercept *InterceptSettings `json:"intercept"` } type ScopeHeader struct { @@ -128,6 +195,13 @@ type SenderRequestInput struct { Body *string `json:"body"` } +type UpdateInterceptSettingsInput struct { + RequestsEnabled bool `json:"requestsEnabled"` + ResponsesEnabled bool `json:"responsesEnabled"` + RequestFilter *string `json:"requestFilter"` + ResponseFilter *string `json:"responseFilter"` +} + type HTTPMethod string const ( diff --git a/pkg/api/resolvers.go b/pkg/api/resolvers.go index c481441..e334b33 100644 --- a/pkg/api/resolvers.go +++ b/pkg/api/resolvers.go @@ -3,9 +3,12 @@ package api //go:generate go run github.com/99designs/gqlgen import ( + "bytes" "context" "errors" "fmt" + "io" + "io/ioutil" "net/http" "regexp" "strings" @@ -15,6 +18,8 @@ import ( "github.com/vektah/gqlparser/v2/gqlerror" "github.com/dstotijn/hetty/pkg/proj" + "github.com/dstotijn/hetty/pkg/proxy" + "github.com/dstotijn/hetty/pkg/proxy/intercept" "github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/scope" "github.com/dstotijn/hetty/pkg/search" @@ -36,6 +41,7 @@ var revHTTPProtocolMap = map[HTTPProtocol]string{ type Resolver struct { ProjectService proj.Service RequestLogService reqlog.Service + InterceptService *intercept.Service SenderService sender.Service } @@ -179,11 +185,9 @@ func (r *mutationResolver) CreateProject(ctx context.Context, name string) (*Pro return nil, fmt.Errorf("could not open project: %w", err) } - return &Project{ - ID: p.ID, - Name: p.Name, - IsActive: r.ProjectService.IsProjectActive(p.ID), - }, nil + project := parseProject(r.ProjectService, p) + + return &project, nil } func (r *mutationResolver) OpenProject(ctx context.Context, id ulid.ULID) (*Project, error) { @@ -194,11 +198,9 @@ func (r *mutationResolver) OpenProject(ctx context.Context, id ulid.ULID) (*Proj return nil, fmt.Errorf("could not open project: %w", err) } - return &Project{ - ID: p.ID, - Name: p.Name, - IsActive: r.ProjectService.IsProjectActive(p.ID), - }, nil + project := parseProject(r.ProjectService, p) + + return &project, nil } func (r *queryResolver) ActiveProject(ctx context.Context) (*Project, error) { @@ -209,11 +211,9 @@ func (r *queryResolver) ActiveProject(ctx context.Context) (*Project, error) { return nil, fmt.Errorf("could not open project: %w", err) } - return &Project{ - ID: p.ID, - Name: p.Name, - IsActive: r.ProjectService.IsProjectActive(p.ID), - }, nil + project := parseProject(r.ProjectService, p) + + return &project, nil } func (r *queryResolver) Projects(ctx context.Context) ([]Project, error) { @@ -224,11 +224,7 @@ func (r *queryResolver) Projects(ctx context.Context) ([]Project, error) { projects := make([]Project, len(p)) for i, proj := range p { - projects[i] = Project{ - ID: proj.ID, - Name: proj.Name, - IsActive: r.ProjectService.IsProjectActive(proj.ID), - } + projects[i] = parseProject(r.ProjectService, proj) } return projects, nil @@ -520,6 +516,166 @@ func (r *mutationResolver) DeleteSenderRequests(ctx context.Context) (*DeleteSen return &DeleteSenderRequestsResult{true}, nil } +func (r *queryResolver) InterceptedRequests(ctx context.Context) (httpReqs []HTTPRequest, err error) { + items := r.InterceptService.Items() + + for _, item := range items { + req, err := parseInterceptItem(item) + if err != nil { + return nil, err + } + + httpReqs = append(httpReqs, req) + } + + return httpReqs, nil +} + +func (r *queryResolver) InterceptedRequest(ctx context.Context, id ulid.ULID) (*HTTPRequest, error) { + 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) + } + + req, err := parseInterceptItem(item) + if err != nil { + return nil, err + } + + return &req, nil +} + +func (r *mutationResolver) ModifyRequest(ctx context.Context, input ModifyRequestInput) (*ModifyRequestResult, error) { + body := "" + if input.Body != nil { + body = *input.Body + } + + //nolint:noctx + req, err := http.NewRequest(input.Method.String(), input.URL.String(), strings.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("failed to construct HTTP request: %w", err) + } + + for _, header := range input.Headers { + req.Header.Add(header.Key, header.Value) + } + + err = r.InterceptService.ModifyRequest(input.ID, req, input.ModifyResponse) + if err != nil { + return nil, fmt.Errorf("could not modify http request: %w", err) + } + + return &ModifyRequestResult{Success: true}, nil +} + +func (r *mutationResolver) CancelRequest(ctx context.Context, id ulid.ULID) (*CancelRequestResult, error) { + err := r.InterceptService.CancelRequest(id) + if err != nil { + return nil, fmt.Errorf("could not cancel http request: %w", err) + } + + 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) + } + + var body string + if input.Body != nil { + body = *input.Body + } + + res.Body = io.NopCloser(strings.NewReader(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, +) (*InterceptSettings, error) { + settings := intercept.Settings{ + RequestsEnabled: input.RequestsEnabled, + ResponsesEnabled: input.ResponsesEnabled, + } + + if input.RequestFilter != nil && *input.RequestFilter != "" { + expr, err := search.ParseQuery(*input.RequestFilter) + if err != nil { + return nil, fmt.Errorf("could not parse request filter: %w", err) + } + + settings.RequestFilter = expr + } + + if input.ResponseFilter != nil && *input.ResponseFilter != "" { + expr, err := search.ParseQuery(*input.ResponseFilter) + if err != nil { + return nil, fmt.Errorf("could not parse response filter: %w", err) + } + + settings.ResponseFilter = expr + } + + err := r.ProjectService.UpdateInterceptSettings(ctx, settings) + if errors.Is(err, proj.ErrNoProject) { + return nil, noActiveProjectErr(ctx) + } else if err != nil { + return nil, fmt.Errorf("could not update intercept settings: %w", err) + } + + updated := &InterceptSettings{ + RequestsEnabled: settings.RequestsEnabled, + ResponsesEnabled: settings.ResponsesEnabled, + } + + if settings.RequestFilter != nil { + reqFilter := settings.RequestFilter.String() + updated.RequestFilter = &reqFilter + } + + if settings.ResponseFilter != nil { + resFilter := settings.ResponseFilter.String() + updated.ResponseFilter = &resFilter + } + + return updated, nil +} + func parseSenderRequest(req sender.Request) (SenderRequest, error) { method := HTTPMethod(req.Method) if method != "" && !method.IsValid() { @@ -575,6 +731,155 @@ func parseSenderRequest(req sender.Request) (SenderRequest, error) { return senderReq, nil } +func parseHTTPRequest(req *http.Request) (HTTPRequest, error) { + method := HTTPMethod(req.Method) + if method != "" && !method.IsValid() { + return HTTPRequest{}, fmt.Errorf("http request has invalid method: %v", method) + } + + reqProto := httpProtocolMap[req.Proto] + if !reqProto.IsValid() { + return HTTPRequest{}, fmt.Errorf("http request has invalid protocol: %v", req.Proto) + } + + id, ok := proxy.RequestIDFromContext(req.Context()) + if !ok { + return HTTPRequest{}, errors.New("http request has missing ID") + } + + httpReq := HTTPRequest{ + ID: id, + URL: req.URL, + Method: method, + Proto: HTTPProtocol(req.Proto), + } + + if req.Header != nil { + httpReq.Headers = make([]HTTPHeader, 0) + + for key, values := range req.Header { + for _, value := range values { + httpReq.Headers = append(httpReq.Headers, HTTPHeader{ + Key: key, + Value: value, + }) + } + } + } + + if req.Body != nil { + body, err := ioutil.ReadAll(req.Body) + if err != nil { + return HTTPRequest{}, fmt.Errorf("failed to read request body: %w", err) + } + + req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + bodyStr := string(body) + httpReq.Body = &bodyStr + } + + 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, + Name: p.Name, + IsActive: projSvc.IsProjectActive(p.ID), + Settings: &ProjectSettings{ + Intercept: &InterceptSettings{ + RequestsEnabled: p.Settings.InterceptRequests, + ResponsesEnabled: p.Settings.InterceptResponses, + }, + }, + } + + if p.Settings.InterceptRequestFilter != nil { + interceptReqFilter := p.Settings.InterceptRequestFilter.String() + project.Settings.Intercept.RequestFilter = &interceptReqFilter + } + + if p.Settings.InterceptResponseFilter != nil { + interceptResFilter := p.Settings.InterceptResponseFilter.String() + project.Settings.Intercept.ResponseFilter = &interceptResFilter + } + + return project +} + func stringPtrToRegexp(s *string) (*regexp.Regexp, error) { if s == nil { return nil, nil diff --git a/pkg/api/schema.graphql b/pkg/api/schema.graphql index 5bbfd05..e33d398 100644 --- a/pkg/api/schema.graphql +++ b/pkg/api/schema.graphql @@ -30,6 +30,11 @@ type Project { id: ID! name: String! isActive: Boolean! + settings: ProjectSettings! +} + +type ProjectSettings { + intercept: InterceptSettings! } type ScopeRule { @@ -116,6 +121,77 @@ type SenderRequestFilter { searchExpression: String } +type HttpRequest { + id: ID! + url: URL! + method: HttpMethod! + 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 { + id: ID! + url: URL! + method: HttpMethod! + proto: HttpProtocol! + headers: [HttpHeaderInput!] + body: String + modifyResponse: Boolean +} + +type ModifyRequestResult { + success: Boolean! +} + +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 { + requestsEnabled: Boolean! + responsesEnabled: Boolean! + requestFilter: String + responseFilter: String +} + +type InterceptSettings { + requestsEnabled: Boolean! + responsesEnabled: Boolean! + requestFilter: String + responseFilter: String +} + type Query { httpRequestLog(id: ID!): HttpRequestLog httpRequestLogs: [HttpRequestLog!]! @@ -125,6 +201,8 @@ type Query { scope: [ScopeRule!]! senderRequest(id: ID!): SenderRequest senderRequests: [SenderRequest!]! + interceptedRequests: [HttpRequest!]! + interceptedRequest(id: ID!): HttpRequest } type Mutation { @@ -142,6 +220,13 @@ type Mutation { createSenderRequestFromHttpRequestLog(id: ID!): SenderRequest! sendRequest(id: ID!): SenderRequest! deleteSenderRequests: DeleteSenderRequestsResult! + modifyRequest(request: ModifyRequestInput!): ModifyRequestResult! + cancelRequest(id: ID!): CancelRequestResult! + modifyResponse(response: ModifyResponseInput!): ModifyResponseResult! + cancelResponse(requestID: ID!): CancelResponseResult! + updateInterceptSettings( + input: UpdateInterceptSettingsInput! + ): InterceptSettings! } enum HttpMethod { diff --git a/pkg/proj/proj.go b/pkg/proj/proj.go index 5456df7..e86a930 100644 --- a/pkg/proj/proj.go +++ b/pkg/proj/proj.go @@ -11,6 +11,7 @@ import ( "github.com/oklog/ulid" + "github.com/dstotijn/hetty/pkg/proxy/intercept" "github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/scope" "github.com/dstotijn/hetty/pkg/search" @@ -33,10 +34,12 @@ type Service interface { SetScopeRules(ctx context.Context, rules []scope.Rule) error SetRequestLogFindFilter(ctx context.Context, filter reqlog.FindRequestsFilter) error SetSenderRequestFindFilter(ctx context.Context, filter sender.FindRequestsFilter) error + UpdateInterceptSettings(ctx context.Context, settings intercept.Settings) error } type service struct { repo Repository + interceptSvc *intercept.Service reqLogSvc reqlog.Service senderSvc sender.Service scope *scope.Scope @@ -53,13 +56,22 @@ type Project struct { } type Settings struct { + // Request log settings ReqLogBypassOutOfScope bool ReqLogOnlyFindInScope bool ReqLogSearchExpr search.Expression + // Intercept settings + InterceptRequests bool + InterceptResponses bool + InterceptRequestFilter search.Expression + InterceptResponseFilter search.Expression + + // Sender settings SenderOnlyFindInScope bool SenderSearchExpr search.Expression + // Scope settings ScopeRules []scope.Rule } @@ -73,19 +85,21 @@ var ( var nameRegexp = regexp.MustCompile(`^[\w\d\s]+$`) type Config struct { - Repository Repository - ReqLogService reqlog.Service - SenderService sender.Service - Scope *scope.Scope + Repository Repository + InterceptService *intercept.Service + ReqLogService reqlog.Service + SenderService sender.Service + Scope *scope.Scope } // NewService returns a new Service. func NewService(cfg Config) (Service, error) { return &service{ - repo: cfg.Repository, - reqLogSvc: cfg.ReqLogService, - senderSvc: cfg.SenderService, - scope: cfg.Scope, + repo: cfg.Repository, + interceptSvc: cfg.InterceptService, + reqLogSvc: cfg.ReqLogService, + senderSvc: cfg.SenderService, + scope: cfg.Scope, }, nil } @@ -120,6 +134,12 @@ func (svc *service) CloseProject() error { svc.reqLogSvc.SetActiveProjectID(ulid.ULID{}) svc.reqLogSvc.SetBypassOutOfScopeRequests(false) svc.reqLogSvc.SetFindReqsFilter(reqlog.FindRequestsFilter{}) + svc.interceptSvc.UpdateSettings(intercept.Settings{ + RequestsEnabled: false, + ResponsesEnabled: false, + RequestFilter: nil, + ResponseFilter: nil, + }) svc.senderSvc.SetActiveProjectID(ulid.ULID{}) svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{}) svc.scope.SetRules(nil) @@ -152,6 +172,7 @@ func (svc *service) OpenProject(ctx context.Context, projectID ulid.ULID) (Proje svc.activeProjectID = project.ID + // Request log settings. svc.reqLogSvc.SetFindReqsFilter(reqlog.FindRequestsFilter{ ProjectID: project.ID, OnlyInScope: project.Settings.ReqLogOnlyFindInScope, @@ -160,6 +181,15 @@ func (svc *service) OpenProject(ctx context.Context, projectID ulid.ULID) (Proje svc.reqLogSvc.SetBypassOutOfScopeRequests(project.Settings.ReqLogBypassOutOfScope) svc.reqLogSvc.SetActiveProjectID(project.ID) + // Intercept settings. + svc.interceptSvc.UpdateSettings(intercept.Settings{ + RequestsEnabled: project.Settings.InterceptRequests, + ResponsesEnabled: project.Settings.InterceptResponses, + RequestFilter: project.Settings.InterceptRequestFilter, + ResponseFilter: project.Settings.InterceptResponseFilter, + }) + + // Sender settings. svc.senderSvc.SetActiveProjectID(project.ID) svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{ ProjectID: project.ID, @@ -167,6 +197,7 @@ func (svc *service) OpenProject(ctx context.Context, projectID ulid.ULID) (Proje SearchExpr: project.Settings.SenderSearchExpr, }) + // Scope settings. svc.scope.SetRules(project.Settings.ScopeRules) return project, nil @@ -264,3 +295,24 @@ func (svc *service) SetSenderRequestFindFilter(ctx context.Context, filter sende func (svc *service) IsProjectActive(projectID ulid.ULID) bool { return projectID.Compare(svc.activeProjectID) == 0 } + +func (svc *service) UpdateInterceptSettings(ctx context.Context, settings intercept.Settings) error { + project, err := svc.ActiveProject(ctx) + if err != nil { + return err + } + + project.Settings.InterceptRequests = settings.RequestsEnabled + project.Settings.InterceptResponses = settings.ResponsesEnabled + project.Settings.InterceptRequestFilter = settings.RequestFilter + project.Settings.InterceptResponseFilter = settings.ResponseFilter + + err = svc.repo.UpsertProject(ctx, project) + if err != nil { + return fmt.Errorf("proj: failed to update project: %w", err) + } + + svc.interceptSvc.UpdateSettings(settings) + + return nil +} diff --git a/pkg/proxy/gzip.go b/pkg/proxy/gzip.go new file mode 100644 index 0000000..eb01eb7 --- /dev/null +++ b/pkg/proxy/gzip.go @@ -0,0 +1,35 @@ +package proxy + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "net/http" +) + +func gunzipResponseBody(res *http.Response) error { + if res.Header.Get("Content-Encoding") != "gzip" { + return nil + } + + gzipReader, err := gzip.NewReader(res.Body) + if err != nil { + return fmt.Errorf("proxy: could not create gzip reader: %w", err) + } + defer gzipReader.Close() + + buf := &bytes.Buffer{} + + //nolint:gosec + if _, err := io.Copy(buf, gzipReader); err != nil { + return fmt.Errorf("proxy: could not read gzipped response body: %w", err) + } + + res.Body = io.NopCloser(buf) + res.Header.Del("Content-Encoding") + res.Header.Set("Content-Length", fmt.Sprint(buf.Len())) + res.ContentLength = int64(buf.Len()) + + return nil +} diff --git a/pkg/proxy/intercept/filter.go b/pkg/proxy/intercept/filter.go new file mode 100644 index 0000000..5be81c3 --- /dev/null +++ b/pkg/proxy/intercept/filter.go @@ -0,0 +1,395 @@ +package intercept + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "strconv" + "strings" + + "github.com/dstotijn/hetty/pkg/scope" + "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) { + if req.URL == nil { + return "", nil + } + return req.URL.String(), nil + }, + "method": func(req *http.Request) (string, error) { return req.Method, nil }, + "body": func(req *http.Request) (string, error) { + if req.Body == nil { + return "", nil + } + + body, err := io.ReadAll(req.Body) + if err != nil { + return "", err + } + + req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + return string(body), nil + }, +} + +//nolint:unparam +var resFilterKeyFns = map[string]func(res *http.Response) (string, error){ + "proto": func(res *http.Response) (string, error) { return res.Proto, nil }, + "statusCode": func(res *http.Response) (string, error) { return strconv.Itoa(res.StatusCode), nil }, + "statusReason": func(res *http.Response) (string, error) { + statusReasonSubs := strings.SplitN(res.Status, " ", 2) + + if len(statusReasonSubs) != 2 { + return "", fmt.Errorf("invalid response status %q", res.Status) + } + return statusReasonSubs[1], nil + }, + "body": func(res *http.Response) (string, error) { + if res.Body == nil { + return "", nil + } + + body, err := io.ReadAll(res.Body) + if err != nil { + return "", err + } + + res.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + + return string(body), nil + }, +} + +// MatchRequestFilter returns true if an HTTP request matches the request filter expression. +func MatchRequestFilter(req *http.Request, expr search.Expression) (bool, error) { + switch e := expr.(type) { + case search.PrefixExpression: + return matchReqPrefixExpr(req, e) + case search.InfixExpression: + return matchReqInfixExpr(req, e) + case search.StringLiteral: + return matchReqStringLiteral(req, e) + default: + return false, fmt.Errorf("expression type (%T) not supported", expr) + } +} + +func matchReqPrefixExpr(req *http.Request, expr search.PrefixExpression) (bool, error) { + switch expr.Operator { + case search.TokOpNot: + match, err := MatchRequestFilter(req, expr.Right) + if err != nil { + return false, err + } + + return !match, nil + default: + return false, errors.New("operator is not supported") + } +} + +func matchReqInfixExpr(req *http.Request, expr search.InfixExpression) (bool, error) { + switch expr.Operator { + case search.TokOpAnd: + left, err := MatchRequestFilter(req, expr.Left) + if err != nil { + return false, err + } + + right, err := MatchRequestFilter(req, expr.Right) + if err != nil { + return false, err + } + + return left && right, nil + case search.TokOpOr: + left, err := MatchRequestFilter(req, expr.Left) + if err != nil { + return false, err + } + + right, err := MatchRequestFilter(req, expr.Right) + if err != nil { + return false, err + } + + return left || right, nil + } + + left, ok := expr.Left.(search.StringLiteral) + if !ok { + return false, errors.New("left operand must be a string literal") + } + + leftVal, err := getMappedStringLiteralFromReq(req, left.Value) + if err != nil { + return false, fmt.Errorf("failed to get string literal from request for left operand: %w", err) + } + + if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe { + right, ok := expr.Right.(search.RegexpLiteral) + if !ok { + return false, errors.New("right operand must be a regular expression") + } + + switch expr.Operator { + case search.TokOpRe: + return right.MatchString(leftVal), nil + case search.TokOpNotRe: + return !right.MatchString(leftVal), nil + } + } + + right, ok := expr.Right.(search.StringLiteral) + if !ok { + return false, errors.New("right operand must be a string literal") + } + + rightVal, err := getMappedStringLiteralFromReq(req, right.Value) + if err != nil { + return false, fmt.Errorf("failed to get string literal from request for right operand: %w", err) + } + + switch expr.Operator { + case search.TokOpEq: + return leftVal == rightVal, nil + case search.TokOpNotEq: + return leftVal != rightVal, nil + case search.TokOpGt: + // TODO(?) attempt to parse as int. + return leftVal > rightVal, nil + case search.TokOpLt: + // TODO(?) attempt to parse as int. + return leftVal < rightVal, nil + case search.TokOpGtEq: + // TODO(?) attempt to parse as int. + return leftVal >= rightVal, nil + case search.TokOpLtEq: + // TODO(?) attempt to parse as int. + return leftVal <= rightVal, nil + default: + return false, errors.New("unsupported operator") + } +} + +func getMappedStringLiteralFromReq(req *http.Request, s string) (string, error) { + fn, ok := reqFilterKeyFns[s] + if ok { + return fn(req) + } + + return s, nil +} + +func matchReqStringLiteral(req *http.Request, strLiteral search.StringLiteral) (bool, error) { + for _, fn := range reqFilterKeyFns { + value, err := fn(req) + if err != nil { + return false, err + } + + if strings.Contains(strings.ToLower(value), strings.ToLower(strLiteral.Value)) { + return true, nil + } + } + + return false, nil +} + +func MatchRequestScope(req *http.Request, s *scope.Scope) (bool, error) { + for _, rule := range s.Rules() { + if rule.URL != nil && req.URL != nil { + if matches := rule.URL.MatchString(req.URL.String()); matches { + return true, nil + } + } + + for key, values := range req.Header { + var keyMatches, valueMatches bool + + if rule.Header.Key != nil { + if matches := rule.Header.Key.MatchString(key); matches { + keyMatches = true + } + } + + if rule.Header.Value != nil { + for _, value := range values { + if matches := rule.Header.Value.MatchString(value); matches { + valueMatches = true + break + } + } + } + + // When only key or value is set, match on whatever is set. + // When both are set, both must match. + switch { + case rule.Header.Key != nil && rule.Header.Value == nil && keyMatches: + return true, nil + case rule.Header.Key == nil && rule.Header.Value != nil && valueMatches: + return true, nil + case rule.Header.Key != nil && rule.Header.Value != nil && keyMatches && valueMatches: + return true, nil + } + } + + if rule.Body != nil { + body, err := io.ReadAll(req.Body) + if err != nil { + return false, fmt.Errorf("failed to read request body: %w", err) + } + + req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + + if matches := rule.Body.Match(body); matches { + return true, nil + } + } + } + + return false, nil +} + +// MatchResponseFilter returns true if an HTTP response matches the response filter expression. +func MatchResponseFilter(res *http.Response, expr search.Expression) (bool, error) { + switch e := expr.(type) { + case search.PrefixExpression: + return matchResPrefixExpr(res, e) + case search.InfixExpression: + return matchResInfixExpr(res, e) + case search.StringLiteral: + return matchResStringLiteral(res, e) + default: + return false, fmt.Errorf("expression type (%T) not supported", expr) + } +} + +func matchResPrefixExpr(res *http.Response, expr search.PrefixExpression) (bool, error) { + switch expr.Operator { + case search.TokOpNot: + match, err := MatchResponseFilter(res, expr.Right) + if err != nil { + return false, err + } + + return !match, nil + default: + return false, errors.New("operator is not supported") + } +} + +func matchResInfixExpr(res *http.Response, expr search.InfixExpression) (bool, error) { + switch expr.Operator { + case search.TokOpAnd: + left, err := MatchResponseFilter(res, expr.Left) + if err != nil { + return false, err + } + + right, err := MatchResponseFilter(res, expr.Right) + if err != nil { + return false, err + } + + return left && right, nil + case search.TokOpOr: + left, err := MatchResponseFilter(res, expr.Left) + if err != nil { + return false, err + } + + right, err := MatchResponseFilter(res, expr.Right) + if err != nil { + return false, err + } + + return left || right, nil + } + + left, ok := expr.Left.(search.StringLiteral) + if !ok { + return false, errors.New("left operand must be a string literal") + } + + leftVal, err := getMappedStringLiteralFromRes(res, left.Value) + if err != nil { + return false, fmt.Errorf("failed to get string literal from response for left operand: %w", err) + } + + if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe { + right, ok := expr.Right.(search.RegexpLiteral) + if !ok { + return false, errors.New("right operand must be a regular expression") + } + + switch expr.Operator { + case search.TokOpRe: + return right.MatchString(leftVal), nil + case search.TokOpNotRe: + return !right.MatchString(leftVal), nil + } + } + + right, ok := expr.Right.(search.StringLiteral) + if !ok { + return false, errors.New("right operand must be a string literal") + } + + rightVal, err := getMappedStringLiteralFromRes(res, right.Value) + if err != nil { + return false, fmt.Errorf("failed to get string literal from response for right operand: %w", err) + } + + switch expr.Operator { + case search.TokOpEq: + return leftVal == rightVal, nil + case search.TokOpNotEq: + return leftVal != rightVal, nil + case search.TokOpGt: + // TODO(?) attempt to parse as int. + return leftVal > rightVal, nil + case search.TokOpLt: + // TODO(?) attempt to parse as int. + return leftVal < rightVal, nil + case search.TokOpGtEq: + // TODO(?) attempt to parse as int. + return leftVal >= rightVal, nil + case search.TokOpLtEq: + // TODO(?) attempt to parse as int. + return leftVal <= rightVal, nil + default: + return false, errors.New("unsupported operator") + } +} + +func getMappedStringLiteralFromRes(res *http.Response, s string) (string, error) { + fn, ok := resFilterKeyFns[s] + if ok { + return fn(res) + } + + return s, nil +} + +func matchResStringLiteral(res *http.Response, strLiteral search.StringLiteral) (bool, error) { + for _, fn := range resFilterKeyFns { + value, err := fn(res) + if err != nil { + return false, err + } + + if strings.Contains(strings.ToLower(value), strings.ToLower(strLiteral.Value)) { + return true, nil + } + } + + return false, nil +} diff --git a/pkg/proxy/intercept/intercept.go b/pkg/proxy/intercept/intercept.go new file mode 100644 index 0000000..caf2f99 --- /dev/null +++ b/pkg/proxy/intercept/intercept.go @@ -0,0 +1,452 @@ +package intercept + +import ( + "context" + "errors" + "fmt" + "net/http" + "sort" + "sync" + + "github.com/oklog/ulid" + + "github.com/dstotijn/hetty/pkg/log" + "github.com/dstotijn/hetty/pkg/proxy" + "github.com/dstotijn/hetty/pkg/search" +) + +var ( + 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 { + req *http.Request + ch chan<- *http.Request + 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 { + reqMu *sync.RWMutex + resMu *sync.RWMutex + requests map[ulid.ULID]Request + responses map[ulid.ULID]Response + logger log.Logger + + requestsEnabled bool + responsesEnabled bool + reqFilter search.Expression + resFilter search.Expression +} + +type Config struct { + Logger log.Logger + RequestsEnabled bool + ResponsesEnabled bool + RequestFilter search.Expression + ResponseFilter search.Expression +} + +// RequestIDs implements sort.Interface. +type RequestIDs []ulid.ULID + +func NewService(cfg Config) *Service { + s := &Service{ + reqMu: &sync.RWMutex{}, + resMu: &sync.RWMutex{}, + requests: make(map[ulid.ULID]Request), + responses: make(map[ulid.ULID]Response), + logger: cfg.Logger, + requestsEnabled: cfg.RequestsEnabled, + responsesEnabled: cfg.ResponsesEnabled, + reqFilter: cfg.RequestFilter, + resFilter: cfg.ResponseFilter, + } + + if s.logger == nil { + s.logger = log.NewNopLogger() + } + + return s +} + +// 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.InterceptRequest(req.Context(), req) + + switch { + case errors.Is(err, ErrRequestAborted): + svc.logger.Debugw("Stopping intercept, request was aborted.") + // Prevent further processing by replacing req.Context with a cancelled context value. + // This will cause the http.Roundtripper in the `proxy` package to + // handle this request as an error. + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + *req = *req.WithContext(ctx) + case errors.Is(err, context.Canceled): + svc.logger.Debugw("Stopping intercept, context was cancelled.") + case err != nil: + svc.logger.Errorw("Failed to intercept request.", + "error", err) + default: + *req = *modifiedReq + next(req) + } + } +} + +// 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: context doesn't have an ID.") + return req, nil + } + + if !svc.requestsEnabled { + // If request intercept is disabled, return the incoming request as-is. + svc.logger.Debugw("Bypassed request interception: feature disabled.") + return req, nil + } + + 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 request interception: request rules don't match.") + return req, nil + } + } + + ch := make(chan *http.Request) + done := make(chan struct{}) + + svc.reqMu.Lock() + svc.requests[reqID] = Request{ + req: req, + ch: ch, + done: done, + } + 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.reqMu.Lock() + defer svc.reqMu.Unlock() + delete(svc.requests, reqID) + }() + + select { + case modReq := <-ch: + if modReq == nil { + return nil, ErrRequestAborted + } + + return modReq, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +// 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, modifyResponse *bool) error { + svc.reqMu.RLock() + req, ok := svc.requests[reqID] + 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 + case req.ch <- modReq: + return nil + } +} + +// CancelRequest ensures an intercepted request is dropped. +func (svc *Service) CancelRequest(reqID ulid.ULID) error { + return svc.ModifyRequest(reqID, nil, nil) +} + +func (svc *Service) ClearRequests() { + svc.reqMu.Lock() + defer svc.reqMu.Unlock() + + for _, req := range svc.requests { + select { + case <-req.done: + case req.ch <- nil: + } + } +} + +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)) + + for id := range svc.requests { + reqIDs = append(reqIDs, id) + } + + for id := range svc.responses { + reqIDs = append(reqIDs, id) + } + + 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 requests `enabled` -> `disabled`, clear any pending reqs. + if svc.requestsEnabled && !settings.RequestsEnabled { + svc.ClearRequests() + } + + // When updating from responses `enabled` -> `disabled`, clear any pending responses. + if svc.responsesEnabled && !settings.ResponsesEnabled { + svc.ClearResponses() + } + + svc.requestsEnabled = settings.RequestsEnabled + svc.responsesEnabled = settings.ResponsesEnabled + svc.reqFilter = settings.RequestFilter + svc.resFilter = settings.ResponseFilter +} + +// ItemByID returns an intercepted item (request and possible response) by ID. It's safe for concurrent use. +func (svc *Service) ItemByID(id ulid.ULID) (Item, error) { + svc.reqMu.RLock() + defer svc.reqMu.RUnlock() + + svc.resMu.RLock() + defer svc.resMu.RUnlock() + + item := Item{} + found := false + + if req, ok := svc.requests[id]; ok { + item.Request = req.req + found = true + } + + 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 { + return len(ids) +} + +func (ids RequestIDs) Less(i, j int) bool { + return ids[i].Compare(ids[j]) == -1 +} + +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 global response intercept is disabled and interception is *not* explicitly enabled for this response: bypass. + if !svc.responsesEnabled && !(ok && shouldIntercept) { + svc.logger.Debugw("Bypassed response interception: feature disabled.") + return res, nil + } + + if svc.resFilter != nil { + match, err := MatchResponseFilter(res, svc.resFilter) + if err != nil { + return nil, fmt.Errorf("intercept: failed to match response rules for response (id: %v): %w", + reqID.String(), err, + ) + } + + if !match { + svc.logger.Debugw("Bypassed response interception: response rules don't match.") + return res, 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 new file mode 100644 index 0000000..2652a93 --- /dev/null +++ b/pkg/proxy/intercept/intercept_test.go @@ -0,0 +1,270 @@ +package intercept_test + +import ( + "context" + "errors" + "math/rand" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/oklog/ulid" + "go.uber.org/zap" + + "github.com/dstotijn/hetty/pkg/proxy" + "github.com/dstotijn/hetty/pkg/proxy/intercept" +) + +//nolint:gosec +var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano())) + +func TestRequestModifier(t *testing.T) { + t.Parallel() + + t.Run("modify request that's not found", func(t *testing.T) { + t.Parallel() + + logger, _ := zap.NewDevelopment() + svc := intercept.NewService(intercept.Config{ + Logger: logger.Sugar(), + RequestsEnabled: true, + ResponsesEnabled: false, + }) + + reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + + err := svc.ModifyRequest(reqID, nil, nil) + if !errors.Is(err, intercept.ErrRequestNotFound) { + t.Fatalf("expected `intercept.ErrRequestNotFound`, got: %v", err) + } + }) + + t.Run("modify request that's done", func(t *testing.T) { + t.Parallel() + + logger, _ := zap.NewDevelopment() + svc := intercept.NewService(intercept.Config{ + Logger: logger.Sugar(), + RequestsEnabled: true, + ResponsesEnabled: false, + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + req := httptest.NewRequest("GET", "https://example.com/foo", nil) + reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + *req = *req.WithContext(ctx) + *req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID)) + + next := func(req *http.Request) {} + go svc.RequestModifier(next)(req) + + // Wait shortly, to allow the req modifier goroutine to add `req` to the + // array of intercepted reqs. + time.Sleep(10 * time.Millisecond) + cancel() + + 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) + } + }) + + t.Run("modify intercepted request", func(t *testing.T) { + t.Parallel() + + req := httptest.NewRequest("GET", "https://example.com/foo", nil) + req.Header.Set("X-Foo", "foo") + + reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + *req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID)) + + modReq := req.Clone(context.Background()) + modReq.Header.Set("X-Foo", "bar") + + logger, _ := zap.NewDevelopment() + svc := intercept.NewService(intercept.Config{ + Logger: logger.Sugar(), + RequestsEnabled: true, + ResponsesEnabled: false, + }) + + var got *http.Request + + next := func(req *http.Request) { + got = req.Clone(context.Background()) + } + + var wg sync.WaitGroup + wg.Add(1) + + go func() { + svc.RequestModifier(next)(req) + wg.Done() + }() + + // Wait shortly, to allow the req modifier goroutine to add `req` to the + // array of intercepted reqs. + time.Sleep(10 * time.Millisecond) + + err := svc.ModifyRequest(reqID, modReq, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + wg.Wait() + + if got == nil { + t.Fatal("expected `got` not to be nil") + } + + if exp := "bar"; exp != got.Header.Get("X-Foo") { + t.Fatalf("incorrect modified request header value (expected: %v, got: %v)", exp, got.Header.Get("X-Foo")) + } + }) +} + +func TestResponseModifier(t *testing.T) { + t.Parallel() + + t.Run("modify response that's not found", func(t *testing.T) { + t.Parallel() + + logger, _ := zap.NewDevelopment() + svc := intercept.NewService(intercept.Config{ + Logger: logger.Sugar(), + RequestsEnabled: false, + ResponsesEnabled: true, + }) + + reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + + err := svc.ModifyResponse(reqID, nil) + if !errors.Is(err, intercept.ErrRequestNotFound) { + t.Fatalf("expected `intercept.ErrRequestNotFound`, got: %v", err) + } + }) + + t.Run("modify response of request that's done", func(t *testing.T) { + t.Parallel() + + logger, _ := zap.NewDevelopment() + svc := intercept.NewService(intercept.Config{ + Logger: logger.Sugar(), + RequestsEnabled: false, + ResponsesEnabled: true, + }) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + req := httptest.NewRequest("GET", "https://example.com/foo", nil) + reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + *req = *req.WithContext(ctx) + *req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID)) + + res := &http.Response{ + Request: req, + Header: make(http.Header), + } + res.Header.Add("X-Foo", "foo") + + var modErr error + var wg sync.WaitGroup + wg.Add(1) + + next := func(res *http.Response) error { return nil } + go func() { + defer wg.Done() + modErr = svc.ResponseModifier(next)(res) + }() + + // Wait shortly, to allow the res modifier goroutine to add `res` to the + // array of intercepted responses. + time.Sleep(10 * time.Millisecond) + cancel() + + modRes := *res + modRes.Header = make(http.Header) + modRes.Header.Set("X-Foo", "bar") + + err := svc.ModifyResponse(reqID, &modRes) + if !errors.Is(err, intercept.ErrRequestDone) { + t.Fatalf("expected `intercept.ErrRequestDone`, got: %v", err) + } + + wg.Wait() + + if !errors.Is(modErr, context.Canceled) { + t.Fatalf("expected `context.Canceled`, got: %v", modErr) + } + }) + + t.Run("modify intercepted response", func(t *testing.T) { + t.Parallel() + + req := httptest.NewRequest("GET", "https://example.com/foo", nil) + req.Header.Set("X-Foo", "foo") + + reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + *req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID)) + + res := &http.Response{ + Request: req, + Header: make(http.Header), + } + res.Header.Add("X-Foo", "foo") + + modRes := *res + modRes.Header = make(http.Header) + modRes.Header.Set("X-Foo", "bar") + + logger, _ := zap.NewDevelopment() + svc := intercept.NewService(intercept.Config{ + Logger: logger.Sugar(), + RequestsEnabled: false, + ResponsesEnabled: true, + }) + + var gotHeader string + + var next proxy.ResponseModifyFunc = func(res *http.Response) error { + gotHeader = res.Header.Get("X-Foo") + return nil + } + + var modErr error + var wg sync.WaitGroup + wg.Add(1) + + go func() { + modErr = svc.ResponseModifier(next)(res) + wg.Done() + }() + + // Wait shortly, to allow the res modifier goroutine to add `req` to the + // array of intercepted reqs. + time.Sleep(10 * time.Millisecond) + + err := svc.ModifyResponse(reqID, &modRes) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + wg.Wait() + + if modErr != nil { + t.Fatalf("unexpected error: %v", modErr) + } + + if exp := "bar"; exp != gotHeader { + t.Fatalf("incorrect modified request header value (expected: %v, got: %v)", exp, gotHeader) + } + }) +} diff --git a/pkg/proxy/intercept/settings.go b/pkg/proxy/intercept/settings.go new file mode 100644 index 0000000..15bc154 --- /dev/null +++ b/pkg/proxy/intercept/settings.go @@ -0,0 +1,10 @@ +package intercept + +import "github.com/dstotijn/hetty/pkg/search" + +type Settings struct { + RequestsEnabled bool + ResponsesEnabled bool + RequestFilter search.Expression + ResponseFilter search.Expression +} diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index 0e30f65..75cb65d 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -7,16 +7,24 @@ import ( "crypto/x509" "errors" "fmt" + "math/rand" "net" "net/http" "net/http/httputil" + "strings" + "time" + + "github.com/oklog/ulid" "github.com/dstotijn/hetty/pkg/log" ) +//nolint:gosec +var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano())) + type contextKey int -const ReqLogIDKey contextKey = 0 +const reqIDKey contextKey = 0 // Proxy implements http.Handler and offers MITM behaviour for modifying // HTTP requests and responses. @@ -54,7 +62,25 @@ func NewProxy(cfg Config) (*Proxy, error) { p.logger = log.NewNopLogger() } + transport := &http.Transport{ + // Values taken from `http.DefaultTransport`. + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + + // Non-default transport values. + DisableCompression: true, + } + p.handler = &httputil.ReverseProxy{ + Transport: transport, Director: p.modifyRequest, ModifyResponse: p.modifyResponse, ErrorHandler: p.errorHandler, @@ -69,6 +95,10 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + ctx := context.WithValue(r.Context(), reqIDKey, reqID) + *r = *r.WithContext(ctx) + p.handler.ServeHTTP(w, r) } @@ -91,6 +121,25 @@ func (p *Proxy) modifyRequest(r *http.Request) { // set this header. r.Header["X-Forwarded-For"] = nil + // Strip unsupported encodings. + if acceptEncs := r.Header.Get("Accept-Encoding"); acceptEncs != "" { + directives := strings.Split(acceptEncs, ",") + updated := make([]string, 0, len(directives)) + + for _, directive := range directives { + stripped := strings.TrimSpace(directive) + if strings.HasPrefix(stripped, "*") || strings.HasPrefix(stripped, "gzip") { + updated = append(updated, stripped) + } + } + + if len(updated) == 0 { + r.Header.Del("Accept-Encoding") + } else { + r.Header.Set("Accept-Encoding", strings.Join(updated, ", ")) + } + } + fn := nopReqModifier for i := len(p.reqModifiers) - 1; i >= 0; i-- { @@ -103,6 +152,11 @@ func (p *Proxy) modifyRequest(r *http.Request) { func (p *Proxy) modifyResponse(res *http.Response) error { fn := nopResModifier + // TODO: Make decompressing gzip formatted response bodies a configurable project setting. + if err := gunzipResponseBody(res); err != nil { + return fmt.Errorf("proxy: failed to gunzip response body: %w", err) + } + for i := len(p.resModifiers) - 1; i >= 0; i-- { fn = p.resModifiers[i](fn) } @@ -110,6 +164,15 @@ func (p *Proxy) modifyResponse(res *http.Response) error { return fn(res) } +func WithRequestID(ctx context.Context, id ulid.ULID) context.Context { + return context.WithValue(ctx, reqIDKey, id) +} + +func RequestIDFromContext(ctx context.Context) (ulid.ULID, bool) { + id, ok := ctx.Value(reqIDKey).(ulid.ULID) + return id, ok +} + // handleConnect hijacks the incoming HTTP request and sets up an HTTP tunnel. // During the TLS handshake with the client, we use the proxy's CA config to // create a certificate on-the-fly. @@ -170,13 +233,14 @@ 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) { - return + 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.") } - p.logger.Errorw("Failed to proxy request.", - "error", err) - w.WriteHeader(http.StatusBadGateway) } diff --git a/pkg/reqlog/reqlog.go b/pkg/reqlog/reqlog.go index ce562e9..a348cb6 100644 --- a/pkg/reqlog/reqlog.go +++ b/pkg/reqlog/reqlog.go @@ -2,16 +2,13 @@ package reqlog import ( "bytes" - "compress/gzip" "context" "errors" "fmt" "io" "io/ioutil" - "math/rand" "net/http" "net/url" - "time" "github.com/oklog/ulid" @@ -23,16 +20,16 @@ import ( type contextKey int -const LogBypassedKey contextKey = 0 +const ( + LogBypassedKey contextKey = iota + ReqLogIDKey +) var ( ErrRequestNotFound = errors.New("reqlog: request not found") ErrProjectIDMustBeSet = errors.New("reqlog: project ID must be set") ) -//nolint:gosec -var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano())) - type RequestLog struct { ID ulid.ULID ProjectID ulid.ULID @@ -170,8 +167,14 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM return } + reqID, ok := proxy.RequestIDFromContext(req.Context()) + if !ok { + svc.logger.Errorw("Bypassed logging: request doesn't have an ID.") + return + } + reqLog := RequestLog{ - ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), + ID: reqID, ProjectID: svc.activeProjectID, Method: clone.Method, URL: clone.URL, @@ -191,7 +194,7 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM "reqLogID", reqLog.ID.String(), "url", reqLog.URL.String()) - ctx := context.WithValue(req.Context(), proxy.ReqLogIDKey, reqLog.ID) + ctx := context.WithValue(req.Context(), ReqLogIDKey, reqLog.ID) *req = *req.WithContext(ctx) } } @@ -206,21 +209,23 @@ func (svc *service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon return nil } - reqLogID, ok := res.Request.Context().Value(proxy.ReqLogIDKey).(ulid.ULID) + reqLogID, ok := res.Request.Context().Value(ReqLogIDKey).(ulid.ULID) if !ok { return errors.New("reqlog: request is missing ID") } clone := *res - // TODO: Use io.LimitReader. - body, err := ioutil.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("reqlog: could not read response body: %w", err) - } + if res.Body != nil { + // TODO: Use io.LimitReader. + body, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("reqlog: could not read response body: %w", err) + } - res.Body = ioutil.NopCloser(bytes.NewBuffer(body)) - clone.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + res.Body = io.NopCloser(bytes.NewBuffer(body)) + clone.Body = io.NopCloser(bytes.NewBuffer(body)) + } go func() { if err := svc.storeResponse(context.Background(), reqLogID, &clone); err != nil { @@ -261,23 +266,6 @@ func (svc *service) BypassOutOfScopeRequests() bool { } func ParseHTTPResponse(res *http.Response) (ResponseLog, error) { - if res.Header.Get("Content-Encoding") == "gzip" { - gzipReader, err := gzip.NewReader(res.Body) - if err != nil { - return ResponseLog{}, fmt.Errorf("reqlog: could not create gzip reader: %w", err) - } - defer gzipReader.Close() - - buf := &bytes.Buffer{} - - //nolint:gosec - if _, err := io.Copy(buf, gzipReader); err != nil { - return ResponseLog{}, fmt.Errorf("reqlog: could not read gzipped response body: %w", err) - } - - res.Body = io.NopCloser(buf) - } - body, err := io.ReadAll(res.Body) if err != nil { return ResponseLog{}, fmt.Errorf("reqlog: could not read body: %w", err) diff --git a/pkg/reqlog/reqlog_test.go b/pkg/reqlog/reqlog_test.go index 38c0e7b..25eebeb 100644 --- a/pkg/reqlog/reqlog_test.go +++ b/pkg/reqlog/reqlog_test.go @@ -41,6 +41,8 @@ func TestRequestModifier(t *testing.T) { } reqModFn := svc.RequestModifier(next) req := httptest.NewRequest("GET", "https://example.com/", strings.NewReader("bar")) + reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + req = req.WithContext(proxy.WithRequestID(req.Context(), reqID)) reqModFn(req) @@ -88,7 +90,7 @@ func TestResponseModifier(t *testing.T) { req := httptest.NewRequest("GET", "https://example.com/", strings.NewReader("bar")) reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) - req = req.WithContext(context.WithValue(req.Context(), proxy.ReqLogIDKey, reqLogID)) + req = req.WithContext(context.WithValue(req.Context(), reqlog.ReqLogIDKey, reqLogID)) res := &http.Response{ Request: req, diff --git a/pkg/reqlog/search.go b/pkg/reqlog/search.go index 7ad4f1c..9fa6053 100644 --- a/pkg/reqlog/search.go +++ b/pkg/reqlog/search.go @@ -3,7 +3,6 @@ package reqlog import ( "errors" "fmt" - "regexp" "strconv" "strings" @@ -100,7 +99,7 @@ func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, erro leftVal := reqLog.getMappedStringLiteral(left.Value) if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe { - right, ok := expr.Right.(*regexp.Regexp) + right, ok := expr.Right.(search.RegexpLiteral) if !ok { return false, errors.New("right operand must be a regular expression") } diff --git a/pkg/search/ast.go b/pkg/search/ast.go index afa01a6..0e0d048 100644 --- a/pkg/search/ast.go +++ b/pkg/search/ast.go @@ -3,6 +3,7 @@ package search import ( "encoding/gob" "regexp" + "strconv" "strings" ) @@ -50,13 +51,17 @@ type StringLiteral struct { } func (sl StringLiteral) String() string { - return sl.Value + return strconv.Quote(sl.Value) } type RegexpLiteral struct { *regexp.Regexp } +func (rl RegexpLiteral) String() string { + return strconv.Quote(rl.Regexp.String()) +} + func (rl RegexpLiteral) MarshalBinary() ([]byte, error) { return []byte(rl.Regexp.String()), nil } diff --git a/pkg/search/parser.go b/pkg/search/parser.go index f95bde9..85a5bd3 100644 --- a/pkg/search/parser.go +++ b/pkg/search/parser.go @@ -208,7 +208,7 @@ func parseInfixExpression(p *Parser, left Expression) (Expression, error) { return nil, fmt.Errorf("could not compile regular expression %q: %w", rightStr.Value, err) } - right = re + right = RegexpLiteral{re} } } diff --git a/pkg/search/parser_test.go b/pkg/search/parser_test.go index 1598ef8..6b538ad 100644 --- a/pkg/search/parser_test.go +++ b/pkg/search/parser_test.go @@ -94,7 +94,7 @@ func TestParseQuery(t *testing.T) { expectedExpression: InfixExpression{ Operator: TokOpRe, Left: StringLiteral{Value: "foo"}, - Right: regexp.MustCompile("bar"), + Right: RegexpLiteral{regexp.MustCompile("bar")}, }, expectedError: nil, }, @@ -104,7 +104,7 @@ func TestParseQuery(t *testing.T) { expectedExpression: InfixExpression{ Operator: TokOpNotRe, Left: StringLiteral{Value: "foo"}, - Right: regexp.MustCompile("bar"), + Right: RegexpLiteral{regexp.MustCompile("bar")}, }, expectedError: nil, }, @@ -197,7 +197,7 @@ func TestParseQuery(t *testing.T) { Right: InfixExpression{ Operator: TokOpRe, Left: StringLiteral{Value: "baz"}, - Right: regexp.MustCompile("yolo"), + Right: RegexpLiteral{regexp.MustCompile("yolo")}, }, }, expectedError: nil, diff --git a/pkg/sender/search.go b/pkg/sender/search.go index 2fa1e29..950ae80 100644 --- a/pkg/sender/search.go +++ b/pkg/sender/search.go @@ -3,7 +3,6 @@ package sender import ( "errors" "fmt" - "regexp" "strings" "github.com/oklog/ulid" @@ -93,7 +92,7 @@ func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) { leftVal := req.getMappedStringLiteral(left.Value) if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe { - right, ok := expr.Right.(*regexp.Regexp) + right, ok := expr.Right.(search.RegexpLiteral) if !ok { return false, errors.New("right operand must be a regular expression") }