mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Compare commits
8 Commits
v0.7.0
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
aa3f9d3da9 | |||
f7def87d0f | |||
aa9822854d | |||
2ce4218a30 | |||
fd27955e11 | |||
426a7d5f96 | |||
21b679dc91 | |||
e4f468d4d2 |
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@ -1,3 +1,3 @@
|
|||||||
# These are supported funding model platforms
|
github: dstotijn
|
||||||
|
|
||||||
patreon: dstotijn
|
patreon: dstotijn
|
||||||
|
custom: "https://www.paypal.com/paypalme/dstotijn"
|
||||||
|
@ -35,7 +35,7 @@ linters-settings:
|
|||||||
godot:
|
godot:
|
||||||
capital: true
|
capital: true
|
||||||
ireturn:
|
ireturn:
|
||||||
allow: "error,empty,anon,stdlib,.*(or|er)$,github.com/99designs/gqlgen/graphql.Marshaler,github.com/dstotijn/hetty/pkg/api.QueryResolver,github.com/dstotijn/hetty/pkg/search.Expression"
|
allow: "error,empty,anon,stdlib,.*(or|er)$,github.com/99designs/gqlgen/graphql.Marshaler,github.com/dstotijn/hetty/pkg/api.QueryResolver,github.com/dstotijn/hetty/pkg/filter.Expression"
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
|
@ -69,6 +69,31 @@ scoop:
|
|||||||
description: An HTTP toolkit for security research.
|
description: An HTTP toolkit for security research.
|
||||||
license: MIT
|
license: MIT
|
||||||
|
|
||||||
|
dockers:
|
||||||
|
- extra_files:
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
- pkg
|
||||||
|
- cmd
|
||||||
|
- admin
|
||||||
|
image_templates:
|
||||||
|
- "ghcr.io/dstotijn/hetty:{{ .Version }}"
|
||||||
|
- "ghcr.io/dstotijn/hetty:{{ .Major }}"
|
||||||
|
- "ghcr.io/dstotijn/hetty:{{ .Major }}.{{ .Minor }}"
|
||||||
|
- "ghcr.io/dstotijn/hetty:latest"
|
||||||
|
- "dstotijn/hetty:{{ .Version }}"
|
||||||
|
- "dstotijn/hetty:{{ .Major }}"
|
||||||
|
- "dstotijn/hetty:{{ .Major }}.{{ .Minor }}"
|
||||||
|
- "dstotijn/hetty:latest"
|
||||||
|
build_flag_templates:
|
||||||
|
- "--pull"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.source=https://github.com/dstotijn/hetty"
|
||||||
|
- "--build-arg=HETTY_VERSION={{.Version}}"
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: "checksums.txt"
|
name_template: "checksums.txt"
|
||||||
|
|
||||||
|
20
README.md
20
README.md
@ -64,8 +64,18 @@ Alternatively, you can [download the latest release from
|
|||||||
GitHub](https://github.com/dstotijn/hetty/releases/latest) for your OS and
|
GitHub](https://github.com/dstotijn/hetty/releases/latest) for your OS and
|
||||||
architecture, and move the binary to a directory in your `$PATH`. If your OS is
|
architecture, and move the binary to a directory in your `$PATH`. If your OS is
|
||||||
not available for one of the package managers or not listed in the GitHub
|
not available for one of the package managers or not listed in the GitHub
|
||||||
releases, you can compile from source _(link coming soon)_ or use a Docker image
|
releases, you can compile from source _(link coming soon)_.
|
||||||
_(link coming soon)_.
|
|
||||||
|
#### Docker
|
||||||
|
|
||||||
|
Docker images are distributed via [GitHub's Container registry](https://github.com/dstotijn/hetty/pkgs/container/hetty)
|
||||||
|
and [Docker Hub](https://hub.docker.com/r/dstotijn/hetty). To run Hetty via with a volume for database and certificate
|
||||||
|
storage, and port 8080 forwarded:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -v $HOME/.hetty:/root/.hetty -p 8080:8080 \
|
||||||
|
ghcr.io/dstotijn/hetty:latest
|
||||||
|
```
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
@ -136,9 +146,11 @@ Guidelines](CONTRIBUTING.md) for details.
|
|||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
<a href="https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=hetty">
|
<p><a href="https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=hetty">
|
||||||
<img src="https://hetty.xyz/img/tines-sponsorship-badge.png" width="140" alt="Sponsored by Tines">
|
<img src="https://hetty.xyz/img/tines-sponsorship-badge.png" width="140" alt="Sponsored by Tines">
|
||||||
</a>
|
</a></p>
|
||||||
|
|
||||||
|
💖 Are you enjoying Hetty? You can [sponsor me](https://github.com/sponsors/dstotijn)!
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
@ -51,6 +51,6 @@
|
|||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"prettier": "^2.1.2",
|
"prettier": "^2.1.2",
|
||||||
"typescript": "^4.0.3",
|
"typescript": "^4.0.3",
|
||||||
"webpack": "^5.67.0"
|
"webpack": "^5.76.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import { useRouter } from "next/router";
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useInterceptedRequests } from "lib/InterceptedRequestsContext";
|
import { useInterceptedRequests } from "lib/InterceptedRequestsContext";
|
||||||
import { KeyValuePair, sortKeyValuePairs } from "lib/components/KeyValuePair";
|
import { KeyValuePair } from "lib/components/KeyValuePair";
|
||||||
import Link from "lib/components/Link";
|
import Link from "lib/components/Link";
|
||||||
import RequestTabs from "lib/components/RequestTabs";
|
import RequestTabs from "lib/components/RequestTabs";
|
||||||
import ResponseStatus from "lib/components/ResponseStatus";
|
import ResponseStatus from "lib/components/ResponseStatus";
|
||||||
@ -112,11 +112,11 @@ function EditRequest(): JSX.Element {
|
|||||||
newQueryParams.push({ key: "", value: "" });
|
newQueryParams.push({ key: "", value: "" });
|
||||||
setQueryParams(newQueryParams);
|
setQueryParams(newQueryParams);
|
||||||
|
|
||||||
const newReqHeaders = sortKeyValuePairs(interceptedRequest.headers || []);
|
const newReqHeaders = interceptedRequest.headers || [];
|
||||||
setReqHeaders([...newReqHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
setReqHeaders([...newReqHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
||||||
|
|
||||||
setResBody(interceptedRequest.response?.body || "");
|
setResBody(interceptedRequest.response?.body || "");
|
||||||
const newResHeaders = sortKeyValuePairs(interceptedRequest.response?.headers || []);
|
const newResHeaders = interceptedRequest.response?.headers || [];
|
||||||
setResHeaders([...newResHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
setResHeaders([...newResHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -76,6 +76,7 @@ function Search(): JSX.Element {
|
|||||||
<ClickAwayListener onClickAway={handleClickAway}>
|
<ClickAwayListener onClickAway={handleClickAway}>
|
||||||
<Paper
|
<Paper
|
||||||
component="form"
|
component="form"
|
||||||
|
autoComplete="off"
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
ref={filterRef}
|
ref={filterRef}
|
||||||
sx={{
|
sx={{
|
||||||
@ -109,6 +110,8 @@ function Search(): JSX.Element {
|
|||||||
value={searchExpr}
|
value={searchExpr}
|
||||||
onChange={(e) => setSearchExpr(e.target.value)}
|
onChange={(e) => setSearchExpr(e.target.value)}
|
||||||
onFocus={() => setFilterOpen(true)}
|
onFocus={() => setFilterOpen(true)}
|
||||||
|
autoCorrect="false"
|
||||||
|
spellCheck="false"
|
||||||
/>
|
/>
|
||||||
<Tooltip title="Search">
|
<Tooltip title="Search">
|
||||||
<IconButton type="submit" sx={{ padding: 1.25 }}>
|
<IconButton type="submit" sx={{ padding: 1.25 }}>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Alert, Box, Button, Typography } from "@mui/material";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import { Alert, Box, Button, Fab, Tooltip, Typography, useTheme } from "@mui/material";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { KeyValuePair, sortKeyValuePairs } from "lib/components/KeyValuePair";
|
import { KeyValuePair } from "lib/components/KeyValuePair";
|
||||||
import RequestTabs from "lib/components/RequestTabs";
|
import RequestTabs from "lib/components/RequestTabs";
|
||||||
import Response from "lib/components/Response";
|
import Response from "lib/components/Response";
|
||||||
import SplitPane from "lib/components/SplitPane";
|
import SplitPane from "lib/components/SplitPane";
|
||||||
@ -17,15 +18,21 @@ import { queryParamsFromURL } from "lib/queryParamsFromURL";
|
|||||||
import updateKeyPairItem from "lib/updateKeyPairItem";
|
import updateKeyPairItem from "lib/updateKeyPairItem";
|
||||||
import updateURLQueryParams from "lib/updateURLQueryParams";
|
import updateURLQueryParams from "lib/updateURLQueryParams";
|
||||||
|
|
||||||
|
const defaultMethod = HttpMethod.Get;
|
||||||
|
const defaultProto = HttpProto.Http20;
|
||||||
|
const emptyKeyPair = [{ key: "", value: "" }];
|
||||||
|
|
||||||
function EditRequest(): JSX.Element {
|
function EditRequest(): JSX.Element {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const reqId = router.query.id as string | undefined;
|
const reqId = router.query.id as string | undefined;
|
||||||
|
|
||||||
const [method, setMethod] = useState(HttpMethod.Get);
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [method, setMethod] = useState(defaultMethod);
|
||||||
const [url, setURL] = useState("");
|
const [url, setURL] = useState("");
|
||||||
const [proto, setProto] = useState(HttpProto.Http20);
|
const [proto, setProto] = useState(defaultProto);
|
||||||
const [queryParams, setQueryParams] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
const [queryParams, setQueryParams] = useState<KeyValuePair[]>(emptyKeyPair);
|
||||||
const [headers, setHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
const [headers, setHeaders] = useState<KeyValuePair[]>(emptyKeyPair);
|
||||||
const [body, setBody] = useState("");
|
const [body, setBody] = useState("");
|
||||||
|
|
||||||
const handleQueryParamChange = (key: string, value: string, idx: number) => {
|
const handleQueryParamChange = (key: string, value: string, idx: number) => {
|
||||||
@ -83,7 +90,7 @@ function EditRequest(): JSX.Element {
|
|||||||
newQueryParams.push({ key: "", value: "" });
|
newQueryParams.push({ key: "", value: "" });
|
||||||
setQueryParams(newQueryParams);
|
setQueryParams(newQueryParams);
|
||||||
|
|
||||||
const newHeaders = sortKeyValuePairs(senderRequest.headers || []);
|
const newHeaders = senderRequest.headers || [];
|
||||||
setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
||||||
setResponse(senderRequest.response);
|
setResponse(senderRequest.response);
|
||||||
},
|
},
|
||||||
@ -131,8 +138,26 @@ function EditRequest(): JSX.Element {
|
|||||||
createOrUpdateRequestAndSend();
|
createOrUpdateRequestAndSend();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNewRequest = () => {
|
||||||
|
setURL("");
|
||||||
|
setMethod(defaultMethod);
|
||||||
|
setProto(defaultProto);
|
||||||
|
setQueryParams(emptyKeyPair);
|
||||||
|
setHeaders(emptyKeyPair);
|
||||||
|
setBody("");
|
||||||
|
setResponse(null);
|
||||||
|
router.push(`/sender`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flexDirection="column" height="100%" gap={2}>
|
<Box display="flex" flexDirection="column" height="100%" gap={2}>
|
||||||
|
<Box sx={{ position: "absolute", bottom: theme.spacing(2), right: theme.spacing(2) }}>
|
||||||
|
<Tooltip title="New request">
|
||||||
|
<Fab color="primary" onClick={handleNewRequest}>
|
||||||
|
<AddIcon />
|
||||||
|
</Fab>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
<Box component="form" autoComplete="off" onSubmit={handleFormSubmit}>
|
<Box component="form" autoComplete="off" onSubmit={handleFormSubmit}>
|
||||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||||
<UrlBar
|
<UrlBar
|
||||||
|
@ -184,20 +184,4 @@ export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTab
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortKeyValuePairs(items: KeyValuePair[]): KeyValuePair[] {
|
|
||||||
const sorted = [...items];
|
|
||||||
|
|
||||||
sorted.sort((a, b) => {
|
|
||||||
if (a.key < b.key) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (a.key > b.key) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return sorted;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default KeyValuePairTable;
|
export default KeyValuePairTable;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Box, Typography } from "@mui/material";
|
import { Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
import { sortKeyValuePairs } from "./KeyValuePair";
|
|
||||||
import ResponseTabs from "./ResponseTabs";
|
import ResponseTabs from "./ResponseTabs";
|
||||||
|
|
||||||
import ResponseStatus from "lib/components/ResponseStatus";
|
import ResponseStatus from "lib/components/ResponseStatus";
|
||||||
@ -29,7 +28,7 @@ function Response({ response }: ResponseProps): JSX.Element {
|
|||||||
</Box>
|
</Box>
|
||||||
<ResponseTabs
|
<ResponseTabs
|
||||||
body={response?.body}
|
body={response?.body}
|
||||||
headers={sortKeyValuePairs(response?.headers || [])}
|
headers={response?.headers || []}
|
||||||
hasResponse={response !== undefined && response !== null}
|
hasResponse={response !== undefined && response !== null}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1720,10 +1720,10 @@ acorn-jsx@^5.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
|
||||||
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
|
||||||
|
|
||||||
acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0:
|
acorn@^8.5.0, acorn@^8.7.0, acorn@^8.7.1:
|
||||||
version "8.7.0"
|
version "8.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
|
||||||
integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
|
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
|
||||||
|
|
||||||
agent-base@6:
|
agent-base@6:
|
||||||
version "6.0.2"
|
version "6.0.2"
|
||||||
@ -2612,10 +2612,10 @@ end-of-stream@^1.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
once "^1.4.0"
|
once "^1.4.0"
|
||||||
|
|
||||||
enhanced-resolve@^5.9.2:
|
enhanced-resolve@^5.10.0:
|
||||||
version "5.9.2"
|
version "5.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz#0224dcd6a43389ebfb2d55efee517e5466772dd9"
|
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
|
||||||
integrity sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==
|
integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs "^4.2.4"
|
graceful-fs "^4.2.4"
|
||||||
tapable "^2.2.0"
|
tapable "^2.2.0"
|
||||||
@ -3770,12 +3770,7 @@ json-buffer@3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
|
||||||
integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
|
integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
|
||||||
|
|
||||||
json-parse-better-errors@^1.0.2:
|
json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1:
|
||||||
version "1.0.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
|
|
||||||
integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
|
|
||||||
|
|
||||||
json-parse-even-better-errors@^2.3.0:
|
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
|
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
|
||||||
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
|
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
|
||||||
@ -5675,10 +5670,10 @@ value-or-promise@1.0.11, value-or-promise@^1.0.11:
|
|||||||
resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.11.tgz#3e90299af31dd014fe843fe309cefa7c1d94b140"
|
resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.11.tgz#3e90299af31dd014fe843fe309cefa7c1d94b140"
|
||||||
integrity sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==
|
integrity sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==
|
||||||
|
|
||||||
watchpack@^2.3.1:
|
watchpack@^2.4.0:
|
||||||
version "2.3.1"
|
version "2.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25"
|
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
||||||
integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==
|
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
|
||||||
dependencies:
|
dependencies:
|
||||||
glob-to-regexp "^0.4.1"
|
glob-to-regexp "^0.4.1"
|
||||||
graceful-fs "^4.1.2"
|
graceful-fs "^4.1.2"
|
||||||
@ -5710,34 +5705,34 @@ webpack-sources@^3.2.3:
|
|||||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
|
||||||
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
|
||||||
|
|
||||||
webpack@^5.67.0:
|
webpack@^5.76.0:
|
||||||
version "5.70.0"
|
version "5.76.0"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.70.0.tgz#3461e6287a72b5e6e2f4872700bc8de0d7500e6d"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.0.tgz#f9fb9fb8c4a7dbdcd0d56a98e56b8a942ee2692c"
|
||||||
integrity sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==
|
integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/eslint-scope" "^3.7.3"
|
"@types/eslint-scope" "^3.7.3"
|
||||||
"@types/estree" "^0.0.51"
|
"@types/estree" "^0.0.51"
|
||||||
"@webassemblyjs/ast" "1.11.1"
|
"@webassemblyjs/ast" "1.11.1"
|
||||||
"@webassemblyjs/wasm-edit" "1.11.1"
|
"@webassemblyjs/wasm-edit" "1.11.1"
|
||||||
"@webassemblyjs/wasm-parser" "1.11.1"
|
"@webassemblyjs/wasm-parser" "1.11.1"
|
||||||
acorn "^8.4.1"
|
acorn "^8.7.1"
|
||||||
acorn-import-assertions "^1.7.6"
|
acorn-import-assertions "^1.7.6"
|
||||||
browserslist "^4.14.5"
|
browserslist "^4.14.5"
|
||||||
chrome-trace-event "^1.0.2"
|
chrome-trace-event "^1.0.2"
|
||||||
enhanced-resolve "^5.9.2"
|
enhanced-resolve "^5.10.0"
|
||||||
es-module-lexer "^0.9.0"
|
es-module-lexer "^0.9.0"
|
||||||
eslint-scope "5.1.1"
|
eslint-scope "5.1.1"
|
||||||
events "^3.2.0"
|
events "^3.2.0"
|
||||||
glob-to-regexp "^0.4.1"
|
glob-to-regexp "^0.4.1"
|
||||||
graceful-fs "^4.2.9"
|
graceful-fs "^4.2.9"
|
||||||
json-parse-better-errors "^1.0.2"
|
json-parse-even-better-errors "^2.3.1"
|
||||||
loader-runner "^4.2.0"
|
loader-runner "^4.2.0"
|
||||||
mime-types "^2.1.27"
|
mime-types "^2.1.27"
|
||||||
neo-async "^2.6.2"
|
neo-async "^2.6.2"
|
||||||
schema-utils "^3.1.0"
|
schema-utils "^3.1.0"
|
||||||
tapable "^2.1.1"
|
tapable "^2.1.1"
|
||||||
terser-webpack-plugin "^5.1.3"
|
terser-webpack-plugin "^5.1.3"
|
||||||
watchpack "^2.3.1"
|
watchpack "^2.4.0"
|
||||||
webpack-sources "^3.2.3"
|
webpack-sources "^3.2.3"
|
||||||
|
|
||||||
whatwg-fetch@^3.4.1:
|
whatwg-fetch@^3.4.1:
|
||||||
|
@ -49,3 +49,17 @@ func UnmarshalURL(v interface{}) (*url.URL, error) {
|
|||||||
|
|
||||||
return u, nil
|
return u, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HTTPHeaders []HTTPHeader
|
||||||
|
|
||||||
|
func (h HTTPHeaders) Len() int {
|
||||||
|
return len(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HTTPHeaders) Less(i, j int) bool {
|
||||||
|
return h[i].Key < h[j].Key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HTTPHeaders) Swap(i, j int) {
|
||||||
|
h[i], h[j] = h[j], h[i]
|
||||||
|
}
|
||||||
|
@ -11,18 +11,19 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
"github.com/oklog/ulid"
|
"github.com/oklog/ulid"
|
||||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/filter"
|
||||||
"github.com/dstotijn/hetty/pkg/proj"
|
"github.com/dstotijn/hetty/pkg/proj"
|
||||||
"github.com/dstotijn/hetty/pkg/proxy"
|
"github.com/dstotijn/hetty/pkg/proxy"
|
||||||
"github.com/dstotijn/hetty/pkg/proxy/intercept"
|
"github.com/dstotijn/hetty/pkg/proxy/intercept"
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
|
||||||
"github.com/dstotijn/hetty/pkg/sender"
|
"github.com/dstotijn/hetty/pkg/sender"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -124,6 +125,8 @@ func parseRequestLog(reqLog reqlog.RequestLog) (HTTPRequestLog, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Sort(HTTPHeaders(log.Headers))
|
||||||
}
|
}
|
||||||
|
|
||||||
if reqLog.Response != nil {
|
if reqLog.Response != nil {
|
||||||
@ -172,6 +175,8 @@ func parseResponseLog(resLog reqlog.ResponseLog) (HTTPResponseLog, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Sort(HTTPHeaders(httpResLog.Headers))
|
||||||
}
|
}
|
||||||
|
|
||||||
return httpResLog, nil
|
return httpResLog, nil
|
||||||
@ -634,7 +639,7 @@ func (r *mutationResolver) UpdateInterceptSettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.RequestFilter != nil && *input.RequestFilter != "" {
|
if input.RequestFilter != nil && *input.RequestFilter != "" {
|
||||||
expr, err := search.ParseQuery(*input.RequestFilter)
|
expr, err := filter.ParseQuery(*input.RequestFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not parse request filter: %w", err)
|
return nil, fmt.Errorf("could not parse request filter: %w", err)
|
||||||
}
|
}
|
||||||
@ -643,7 +648,7 @@ func (r *mutationResolver) UpdateInterceptSettings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if input.ResponseFilter != nil && *input.ResponseFilter != "" {
|
if input.ResponseFilter != nil && *input.ResponseFilter != "" {
|
||||||
expr, err := search.ParseQuery(*input.ResponseFilter)
|
expr, err := filter.ParseQuery(*input.ResponseFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not parse response filter: %w", err)
|
return nil, fmt.Errorf("could not parse response filter: %w", err)
|
||||||
}
|
}
|
||||||
@ -710,6 +715,8 @@ func parseSenderRequest(req sender.Request) (SenderRequest, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Sort(HTTPHeaders(senderReq.Headers))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(req.Body) > 0 {
|
if len(req.Body) > 0 {
|
||||||
@ -765,6 +772,8 @@ func parseHTTPRequest(req *http.Request) (HTTPRequest, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Sort(HTTPHeaders(httpReq.Headers))
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Body != nil {
|
if req.Body != nil {
|
||||||
@ -815,6 +824,8 @@ func parseHTTPResponse(res *http.Response) (HTTPResponse, error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Sort(HTTPHeaders(httpRes.Headers))
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.Body != nil {
|
if res.Body != nil {
|
||||||
@ -905,43 +916,43 @@ func scopeToScopeRules(rules []scope.Rule) []ScopeRule {
|
|||||||
return scopeRules
|
return scopeRules
|
||||||
}
|
}
|
||||||
|
|
||||||
func findRequestsFilterFromInput(input *HTTPRequestLogFilterInput) (filter reqlog.FindRequestsFilter, err error) {
|
func findRequestsFilterFromInput(input *HTTPRequestLogFilterInput) (findFilter reqlog.FindRequestsFilter, err error) {
|
||||||
if input == nil {
|
if input == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.OnlyInScope != nil {
|
if input.OnlyInScope != nil {
|
||||||
filter.OnlyInScope = *input.OnlyInScope
|
findFilter.OnlyInScope = *input.OnlyInScope
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.SearchExpression != nil && *input.SearchExpression != "" {
|
if input.SearchExpression != nil && *input.SearchExpression != "" {
|
||||||
expr, err := search.ParseQuery(*input.SearchExpression)
|
expr, err := filter.ParseQuery(*input.SearchExpression)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return reqlog.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %w", err)
|
return reqlog.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
filter.SearchExpr = expr
|
findFilter.SearchExpr = expr
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func findSenderRequestsFilterFromInput(input *SenderRequestFilterInput) (filter sender.FindRequestsFilter, err error) {
|
func findSenderRequestsFilterFromInput(input *SenderRequestFilterInput) (findFilter sender.FindRequestsFilter, err error) {
|
||||||
if input == nil {
|
if input == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.OnlyInScope != nil {
|
if input.OnlyInScope != nil {
|
||||||
filter.OnlyInScope = *input.OnlyInScope
|
findFilter.OnlyInScope = *input.OnlyInScope
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.SearchExpression != nil && *input.SearchExpression != "" {
|
if input.SearchExpression != nil && *input.SearchExpression != "" {
|
||||||
expr, err := search.ParseQuery(*input.SearchExpression)
|
expr, err := filter.ParseQuery(*input.SearchExpression)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sender.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %w", err)
|
return sender.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
filter.SearchExpr = expr
|
findFilter.SearchExpr = expr
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -15,9 +15,9 @@ import (
|
|||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/oklog/ulid"
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/filter"
|
||||||
"github.com/dstotijn/hetty/pkg/proj"
|
"github.com/dstotijn/hetty/pkg/proj"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
@ -45,7 +45,7 @@ func TestUpsertProject(t *testing.T) {
|
|||||||
database := DatabaseFromBadgerDB(badgerDB)
|
database := DatabaseFromBadgerDB(badgerDB)
|
||||||
defer database.Close()
|
defer database.Close()
|
||||||
|
|
||||||
searchExpr, err := search.ParseQuery("foo AND bar OR NOT baz")
|
searchExpr, err := filter.ParseQuery("foo AND bar OR NOT baz")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
|
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package search
|
package filter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
@ -78,8 +78,10 @@ func (rl *RegexpLiteral) UnmarshalBinary(data []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gob.Register(PrefixExpression{})
|
// The `filter` package was previously named `search`.
|
||||||
gob.Register(InfixExpression{})
|
// We use the legacy names for backwards compatibility with existing database data.
|
||||||
gob.Register(StringLiteral{})
|
gob.RegisterName("github.com/dstotijn/hetty/pkg/search.PrefixExpression", PrefixExpression{})
|
||||||
gob.Register(RegexpLiteral{})
|
gob.RegisterName("github.com/dstotijn/hetty/pkg/search.InfixExpression", InfixExpression{})
|
||||||
|
gob.RegisterName("github.com/dstotijn/hetty/pkg/search.StringLiteral", StringLiteral{})
|
||||||
|
gob.RegisterName("github.com/dstotijn/hetty/pkg/search.RegexpLiteral", RegexpLiteral{})
|
||||||
}
|
}
|
212
pkg/filter/ast_test.go
Normal file
212
pkg/filter/ast_test.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
package filter_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/filter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExpressionString(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expression filter.Expression
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "string literal expression",
|
||||||
|
expression: filter.StringLiteral{Value: "foobar"},
|
||||||
|
expected: `"foobar"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boolean expression with equal operator",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpEq,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.StringLiteral{Value: "bar"},
|
||||||
|
},
|
||||||
|
expected: `("foo" = "bar")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boolean expression with not equal operator",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpNotEq,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.StringLiteral{Value: "bar"},
|
||||||
|
},
|
||||||
|
expected: `("foo" != "bar")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boolean expression with greater than operator",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpGt,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.StringLiteral{Value: "bar"},
|
||||||
|
},
|
||||||
|
expected: `("foo" > "bar")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boolean expression with less than operator",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpLt,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.StringLiteral{Value: "bar"},
|
||||||
|
},
|
||||||
|
expected: `("foo" < "bar")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boolean expression with greater than or equal operator",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpGtEq,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.StringLiteral{Value: "bar"},
|
||||||
|
},
|
||||||
|
expected: `("foo" >= "bar")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boolean expression with less than or equal operator",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpLtEq,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.StringLiteral{Value: "bar"},
|
||||||
|
},
|
||||||
|
expected: `("foo" <= "bar")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boolean expression with regular expression operator",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpRe,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.RegexpLiteral{regexp.MustCompile("bar")},
|
||||||
|
},
|
||||||
|
expected: `("foo" =~ "bar")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boolean expression with not regular expression operator",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpNotRe,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.RegexpLiteral{regexp.MustCompile("bar")},
|
||||||
|
},
|
||||||
|
expected: `("foo" !~ "bar")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boolean expression with AND, OR and NOT operators",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpAnd,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpOr,
|
||||||
|
Left: filter.StringLiteral{Value: "bar"},
|
||||||
|
Right: filter.PrefixExpression{
|
||||||
|
Operator: filter.TokOpNot,
|
||||||
|
Right: filter.StringLiteral{Value: "baz"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `("foo" AND ("bar" OR (NOT "baz")))`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boolean expression with nested group",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpOr,
|
||||||
|
Left: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpAnd,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.StringLiteral{Value: "bar"},
|
||||||
|
},
|
||||||
|
Right: filter.PrefixExpression{
|
||||||
|
Operator: filter.TokOpNot,
|
||||||
|
Right: filter.StringLiteral{Value: "baz"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `(("foo" AND "bar") OR (NOT "baz"))`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit boolean expression with string literal operands",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpAnd,
|
||||||
|
Left: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpAnd,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.StringLiteral{Value: "bar"},
|
||||||
|
},
|
||||||
|
Right: filter.StringLiteral{Value: "baz"},
|
||||||
|
},
|
||||||
|
expected: `(("foo" AND "bar") AND "baz")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit boolean expression nested in group",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpAnd,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.StringLiteral{Value: "bar"},
|
||||||
|
},
|
||||||
|
expected: `("foo" AND "bar")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit and explicit boolean expression with string literal operands",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpAnd,
|
||||||
|
Left: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpAnd,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpOr,
|
||||||
|
Left: filter.StringLiteral{Value: "bar"},
|
||||||
|
Right: filter.StringLiteral{Value: "baz"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Right: filter.StringLiteral{Value: "yolo"},
|
||||||
|
},
|
||||||
|
expected: `(("foo" AND ("bar" OR "baz")) AND "yolo")`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit boolean expression with comparison operands",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpAnd,
|
||||||
|
Left: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpEq,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.StringLiteral{Value: "bar"},
|
||||||
|
},
|
||||||
|
Right: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpRe,
|
||||||
|
Left: filter.StringLiteral{Value: "baz"},
|
||||||
|
Right: filter.RegexpLiteral{regexp.MustCompile("yolo")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `(("foo" = "bar") AND ("baz" =~ "yolo"))`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "eq operator takes precedence over boolean ops",
|
||||||
|
expression: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpOr,
|
||||||
|
Left: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpEq,
|
||||||
|
Left: filter.StringLiteral{Value: "foo"},
|
||||||
|
Right: filter.StringLiteral{Value: "bar"},
|
||||||
|
},
|
||||||
|
Right: filter.InfixExpression{
|
||||||
|
Operator: filter.TokOpEq,
|
||||||
|
Left: filter.StringLiteral{Value: "baz"},
|
||||||
|
Right: filter.StringLiteral{Value: "yolo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: `(("foo" = "bar") OR ("baz" = "yolo"))`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got := tt.expression.String()
|
||||||
|
if tt.expected != got {
|
||||||
|
t.Errorf("expected: %v, got: %v", tt.expected, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
82
pkg/filter/http.go
Normal file
82
pkg/filter/http.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MatchHTTPHeaders(op TokenType, expr Expression, headers http.Header) (bool, error) {
|
||||||
|
if headers == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch op {
|
||||||
|
case TokOpEq:
|
||||||
|
strLiteral, ok := expr.(StringLiteral)
|
||||||
|
if !ok {
|
||||||
|
return false, errors.New("filter: expression must be a string literal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return `true` if at least one header (<key>: <value>) is equal to the string literal.
|
||||||
|
for key, values := range headers {
|
||||||
|
for _, value := range values {
|
||||||
|
if strLiteral.Value == fmt.Sprintf("%v: %v", key, value) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
case TokOpNotEq:
|
||||||
|
strLiteral, ok := expr.(StringLiteral)
|
||||||
|
if !ok {
|
||||||
|
return false, errors.New("filter: expression must be a string literal")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return `true` if none of the headers (<key>: <value>) are equal to the string literal.
|
||||||
|
for key, values := range headers {
|
||||||
|
for _, value := range values {
|
||||||
|
if strLiteral.Value == fmt.Sprintf("%v: %v", key, value) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
case TokOpRe:
|
||||||
|
re, ok := expr.(RegexpLiteral)
|
||||||
|
if !ok {
|
||||||
|
return false, errors.New("filter: expression must be a regular expression")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return `true` if at least one header (<key>: <value>) matches the regular expression.
|
||||||
|
for key, values := range headers {
|
||||||
|
for _, value := range values {
|
||||||
|
if re.MatchString(fmt.Sprintf("%v: %v", key, value)) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
case TokOpNotRe:
|
||||||
|
re, ok := expr.(RegexpLiteral)
|
||||||
|
if !ok {
|
||||||
|
return false, errors.New("filter: expression must be a regular expression")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return `true` if none of the headers (<key>: <value>) match the regular expression.
|
||||||
|
for key, values := range headers {
|
||||||
|
for _, value := range values {
|
||||||
|
if re.MatchString(fmt.Sprintf("%v: %v", key, value)) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("filter: unsupported operator %q", op.String())
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package search
|
package filter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
@ -1,4 +1,4 @@
|
|||||||
package search
|
package filter
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package search
|
package filter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -88,7 +88,7 @@ func ParseQuery(input string) (expr Expression, err error) {
|
|||||||
p.nextToken()
|
p.nextToken()
|
||||||
|
|
||||||
if p.curTokenIs(TokEOF) {
|
if p.curTokenIs(TokEOF) {
|
||||||
return nil, fmt.Errorf("search: unexpected EOF")
|
return nil, fmt.Errorf("filter: unexpected EOF")
|
||||||
}
|
}
|
||||||
|
|
||||||
for !p.curTokenIs(TokEOF) {
|
for !p.curTokenIs(TokEOF) {
|
||||||
@ -96,7 +96,7 @@ func ParseQuery(input string) (expr Expression, err error) {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return nil, fmt.Errorf("search: could not parse expression: %w", err)
|
return nil, fmt.Errorf("filter: could not parse expression: %w", err)
|
||||||
case expr == nil:
|
case expr == nil:
|
||||||
expr = right
|
expr = right
|
||||||
default:
|
default:
|
@ -1,4 +1,4 @@
|
|||||||
package search
|
package filter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -20,7 +20,7 @@ func TestParseQuery(t *testing.T) {
|
|||||||
name: "empty query",
|
name: "empty query",
|
||||||
input: "",
|
input: "",
|
||||||
expectedExpression: nil,
|
expectedExpression: nil,
|
||||||
expectedError: errors.New("search: unexpected EOF"),
|
expectedError: errors.New("filter: unexpected EOF"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "string literal expression",
|
name: "string literal expression",
|
@ -11,10 +11,10 @@ import (
|
|||||||
|
|
||||||
"github.com/oklog/ulid"
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/filter"
|
||||||
"github.com/dstotijn/hetty/pkg/proxy/intercept"
|
"github.com/dstotijn/hetty/pkg/proxy/intercept"
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
|
||||||
"github.com/dstotijn/hetty/pkg/sender"
|
"github.com/dstotijn/hetty/pkg/sender"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -59,17 +59,17 @@ type Settings struct {
|
|||||||
// Request log settings
|
// Request log settings
|
||||||
ReqLogBypassOutOfScope bool
|
ReqLogBypassOutOfScope bool
|
||||||
ReqLogOnlyFindInScope bool
|
ReqLogOnlyFindInScope bool
|
||||||
ReqLogSearchExpr search.Expression
|
ReqLogSearchExpr filter.Expression
|
||||||
|
|
||||||
// Intercept settings
|
// Intercept settings
|
||||||
InterceptRequests bool
|
InterceptRequests bool
|
||||||
InterceptResponses bool
|
InterceptResponses bool
|
||||||
InterceptRequestFilter search.Expression
|
InterceptRequestFilter filter.Expression
|
||||||
InterceptResponseFilter search.Expression
|
InterceptResponseFilter filter.Expression
|
||||||
|
|
||||||
// Sender settings
|
// Sender settings
|
||||||
SenderOnlyFindInScope bool
|
SenderOnlyFindInScope bool
|
||||||
SenderSearchExpr search.Expression
|
SenderSearchExpr filter.Expression
|
||||||
|
|
||||||
// Scope settings
|
// Scope settings
|
||||||
ScopeRules []scope.Rule
|
ScopeRules []scope.Rule
|
||||||
|
@ -10,8 +10,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/filter"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:unparam
|
//nolint:unparam
|
||||||
@ -68,22 +68,22 @@ var resFilterKeyFns = map[string]func(res *http.Response) (string, error){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MatchRequestFilter returns true if an HTTP request matches the request filter expression.
|
// MatchRequestFilter returns true if an HTTP request matches the request filter expression.
|
||||||
func MatchRequestFilter(req *http.Request, expr search.Expression) (bool, error) {
|
func MatchRequestFilter(req *http.Request, expr filter.Expression) (bool, error) {
|
||||||
switch e := expr.(type) {
|
switch e := expr.(type) {
|
||||||
case search.PrefixExpression:
|
case filter.PrefixExpression:
|
||||||
return matchReqPrefixExpr(req, e)
|
return matchReqPrefixExpr(req, e)
|
||||||
case search.InfixExpression:
|
case filter.InfixExpression:
|
||||||
return matchReqInfixExpr(req, e)
|
return matchReqInfixExpr(req, e)
|
||||||
case search.StringLiteral:
|
case filter.StringLiteral:
|
||||||
return matchReqStringLiteral(req, e)
|
return matchReqStringLiteral(req, e)
|
||||||
default:
|
default:
|
||||||
return false, fmt.Errorf("expression type (%T) not supported", expr)
|
return false, fmt.Errorf("expression type (%T) not supported", expr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchReqPrefixExpr(req *http.Request, expr search.PrefixExpression) (bool, error) {
|
func matchReqPrefixExpr(req *http.Request, expr filter.PrefixExpression) (bool, error) {
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpNot:
|
case filter.TokOpNot:
|
||||||
match, err := MatchRequestFilter(req, expr.Right)
|
match, err := MatchRequestFilter(req, expr.Right)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -95,9 +95,9 @@ func matchReqPrefixExpr(req *http.Request, expr search.PrefixExpression) (bool,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchReqInfixExpr(req *http.Request, expr search.InfixExpression) (bool, error) {
|
func matchReqInfixExpr(req *http.Request, expr filter.InfixExpression) (bool, error) {
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpAnd:
|
case filter.TokOpAnd:
|
||||||
left, err := MatchRequestFilter(req, expr.Left)
|
left, err := MatchRequestFilter(req, expr.Left)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -109,7 +109,7 @@ func matchReqInfixExpr(req *http.Request, expr search.InfixExpression) (bool, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
return left && right, nil
|
return left && right, nil
|
||||||
case search.TokOpOr:
|
case filter.TokOpOr:
|
||||||
left, err := MatchRequestFilter(req, expr.Left)
|
left, err := MatchRequestFilter(req, expr.Left)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -123,7 +123,7 @@ func matchReqInfixExpr(req *http.Request, expr search.InfixExpression) (bool, er
|
|||||||
return left || right, nil
|
return left || right, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
left, ok := expr.Left.(search.StringLiteral)
|
left, ok := expr.Left.(filter.StringLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("left operand must be a string literal")
|
return false, errors.New("left operand must be a string literal")
|
||||||
}
|
}
|
||||||
@ -133,21 +133,30 @@ func matchReqInfixExpr(req *http.Request, expr search.InfixExpression) (bool, er
|
|||||||
return false, fmt.Errorf("failed to get string literal from request for left operand: %w", err)
|
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 {
|
if leftVal == "headers" {
|
||||||
right, ok := expr.Right.(search.RegexpLiteral)
|
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.Header)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return match, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if expr.Operator == filter.TokOpRe || expr.Operator == filter.TokOpNotRe {
|
||||||
|
right, ok := expr.Right.(filter.RegexpLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("right operand must be a regular expression")
|
return false, errors.New("right operand must be a regular expression")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpRe:
|
case filter.TokOpRe:
|
||||||
return right.MatchString(leftVal), nil
|
return right.MatchString(leftVal), nil
|
||||||
case search.TokOpNotRe:
|
case filter.TokOpNotRe:
|
||||||
return !right.MatchString(leftVal), nil
|
return !right.MatchString(leftVal), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
right, ok := expr.Right.(search.StringLiteral)
|
right, ok := expr.Right.(filter.StringLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("right operand must be a string literal")
|
return false, errors.New("right operand must be a string literal")
|
||||||
}
|
}
|
||||||
@ -158,20 +167,20 @@ func matchReqInfixExpr(req *http.Request, expr search.InfixExpression) (bool, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpEq:
|
case filter.TokOpEq:
|
||||||
return leftVal == rightVal, nil
|
return leftVal == rightVal, nil
|
||||||
case search.TokOpNotEq:
|
case filter.TokOpNotEq:
|
||||||
return leftVal != rightVal, nil
|
return leftVal != rightVal, nil
|
||||||
case search.TokOpGt:
|
case filter.TokOpGt:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal > rightVal, nil
|
return leftVal > rightVal, nil
|
||||||
case search.TokOpLt:
|
case filter.TokOpLt:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal < rightVal, nil
|
return leftVal < rightVal, nil
|
||||||
case search.TokOpGtEq:
|
case filter.TokOpGtEq:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal >= rightVal, nil
|
return leftVal >= rightVal, nil
|
||||||
case search.TokOpLtEq:
|
case filter.TokOpLtEq:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal <= rightVal, nil
|
return leftVal <= rightVal, nil
|
||||||
default:
|
default:
|
||||||
@ -188,7 +197,18 @@ func getMappedStringLiteralFromReq(req *http.Request, s string) (string, error)
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchReqStringLiteral(req *http.Request, strLiteral search.StringLiteral) (bool, error) {
|
func matchReqStringLiteral(req *http.Request, strLiteral filter.StringLiteral) (bool, error) {
|
||||||
|
for key, values := range req.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.Contains(
|
||||||
|
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
|
||||||
|
strings.ToLower(strLiteral.Value),
|
||||||
|
) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, fn := range reqFilterKeyFns {
|
for _, fn := range reqFilterKeyFns {
|
||||||
value, err := fn(req)
|
value, err := fn(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -259,22 +279,22 @@ func MatchRequestScope(req *http.Request, s *scope.Scope) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// MatchResponseFilter returns true if an HTTP response matches the response filter expression.
|
// MatchResponseFilter returns true if an HTTP response matches the response filter expression.
|
||||||
func MatchResponseFilter(res *http.Response, expr search.Expression) (bool, error) {
|
func MatchResponseFilter(res *http.Response, expr filter.Expression) (bool, error) {
|
||||||
switch e := expr.(type) {
|
switch e := expr.(type) {
|
||||||
case search.PrefixExpression:
|
case filter.PrefixExpression:
|
||||||
return matchResPrefixExpr(res, e)
|
return matchResPrefixExpr(res, e)
|
||||||
case search.InfixExpression:
|
case filter.InfixExpression:
|
||||||
return matchResInfixExpr(res, e)
|
return matchResInfixExpr(res, e)
|
||||||
case search.StringLiteral:
|
case filter.StringLiteral:
|
||||||
return matchResStringLiteral(res, e)
|
return matchResStringLiteral(res, e)
|
||||||
default:
|
default:
|
||||||
return false, fmt.Errorf("expression type (%T) not supported", expr)
|
return false, fmt.Errorf("expression type (%T) not supported", expr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchResPrefixExpr(res *http.Response, expr search.PrefixExpression) (bool, error) {
|
func matchResPrefixExpr(res *http.Response, expr filter.PrefixExpression) (bool, error) {
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpNot:
|
case filter.TokOpNot:
|
||||||
match, err := MatchResponseFilter(res, expr.Right)
|
match, err := MatchResponseFilter(res, expr.Right)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -286,9 +306,9 @@ func matchResPrefixExpr(res *http.Response, expr search.PrefixExpression) (bool,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchResInfixExpr(res *http.Response, expr search.InfixExpression) (bool, error) {
|
func matchResInfixExpr(res *http.Response, expr filter.InfixExpression) (bool, error) {
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpAnd:
|
case filter.TokOpAnd:
|
||||||
left, err := MatchResponseFilter(res, expr.Left)
|
left, err := MatchResponseFilter(res, expr.Left)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -300,7 +320,7 @@ func matchResInfixExpr(res *http.Response, expr search.InfixExpression) (bool, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
return left && right, nil
|
return left && right, nil
|
||||||
case search.TokOpOr:
|
case filter.TokOpOr:
|
||||||
left, err := MatchResponseFilter(res, expr.Left)
|
left, err := MatchResponseFilter(res, expr.Left)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -314,7 +334,7 @@ func matchResInfixExpr(res *http.Response, expr search.InfixExpression) (bool, e
|
|||||||
return left || right, nil
|
return left || right, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
left, ok := expr.Left.(search.StringLiteral)
|
left, ok := expr.Left.(filter.StringLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("left operand must be a string literal")
|
return false, errors.New("left operand must be a string literal")
|
||||||
}
|
}
|
||||||
@ -324,21 +344,30 @@ func matchResInfixExpr(res *http.Response, expr search.InfixExpression) (bool, e
|
|||||||
return false, fmt.Errorf("failed to get string literal from response for left operand: %w", err)
|
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 {
|
if leftVal == "headers" {
|
||||||
right, ok := expr.Right.(search.RegexpLiteral)
|
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, res.Header)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return match, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if expr.Operator == filter.TokOpRe || expr.Operator == filter.TokOpNotRe {
|
||||||
|
right, ok := expr.Right.(filter.RegexpLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("right operand must be a regular expression")
|
return false, errors.New("right operand must be a regular expression")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpRe:
|
case filter.TokOpRe:
|
||||||
return right.MatchString(leftVal), nil
|
return right.MatchString(leftVal), nil
|
||||||
case search.TokOpNotRe:
|
case filter.TokOpNotRe:
|
||||||
return !right.MatchString(leftVal), nil
|
return !right.MatchString(leftVal), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
right, ok := expr.Right.(search.StringLiteral)
|
right, ok := expr.Right.(filter.StringLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("right operand must be a string literal")
|
return false, errors.New("right operand must be a string literal")
|
||||||
}
|
}
|
||||||
@ -349,20 +378,20 @@ func matchResInfixExpr(res *http.Response, expr search.InfixExpression) (bool, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpEq:
|
case filter.TokOpEq:
|
||||||
return leftVal == rightVal, nil
|
return leftVal == rightVal, nil
|
||||||
case search.TokOpNotEq:
|
case filter.TokOpNotEq:
|
||||||
return leftVal != rightVal, nil
|
return leftVal != rightVal, nil
|
||||||
case search.TokOpGt:
|
case filter.TokOpGt:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal > rightVal, nil
|
return leftVal > rightVal, nil
|
||||||
case search.TokOpLt:
|
case filter.TokOpLt:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal < rightVal, nil
|
return leftVal < rightVal, nil
|
||||||
case search.TokOpGtEq:
|
case filter.TokOpGtEq:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal >= rightVal, nil
|
return leftVal >= rightVal, nil
|
||||||
case search.TokOpLtEq:
|
case filter.TokOpLtEq:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal <= rightVal, nil
|
return leftVal <= rightVal, nil
|
||||||
default:
|
default:
|
||||||
@ -379,7 +408,18 @@ func getMappedStringLiteralFromRes(res *http.Response, s string) (string, error)
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchResStringLiteral(res *http.Response, strLiteral search.StringLiteral) (bool, error) {
|
func matchResStringLiteral(res *http.Response, strLiteral filter.StringLiteral) (bool, error) {
|
||||||
|
for key, values := range res.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.Contains(
|
||||||
|
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
|
||||||
|
strings.ToLower(strLiteral.Value),
|
||||||
|
) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, fn := range resFilterKeyFns {
|
for _, fn := range resFilterKeyFns {
|
||||||
value, err := fn(res)
|
value, err := fn(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -10,9 +10,9 @@ import (
|
|||||||
|
|
||||||
"github.com/oklog/ulid"
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/filter"
|
||||||
"github.com/dstotijn/hetty/pkg/log"
|
"github.com/dstotijn/hetty/pkg/log"
|
||||||
"github.com/dstotijn/hetty/pkg/proxy"
|
"github.com/dstotijn/hetty/pkg/proxy"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -56,16 +56,16 @@ type Service struct {
|
|||||||
|
|
||||||
requestsEnabled bool
|
requestsEnabled bool
|
||||||
responsesEnabled bool
|
responsesEnabled bool
|
||||||
reqFilter search.Expression
|
reqFilter filter.Expression
|
||||||
resFilter search.Expression
|
resFilter filter.Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Logger log.Logger
|
Logger log.Logger
|
||||||
RequestsEnabled bool
|
RequestsEnabled bool
|
||||||
ResponsesEnabled bool
|
ResponsesEnabled bool
|
||||||
RequestFilter search.Expression
|
RequestFilter filter.Expression
|
||||||
ResponseFilter search.Expression
|
ResponseFilter filter.Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestIDs implements sort.Interface.
|
// RequestIDs implements sort.Interface.
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package intercept
|
package intercept
|
||||||
|
|
||||||
import "github.com/dstotijn/hetty/pkg/search"
|
import "github.com/dstotijn/hetty/pkg/filter"
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
RequestsEnabled bool
|
RequestsEnabled bool
|
||||||
ResponsesEnabled bool
|
ResponsesEnabled bool
|
||||||
RequestFilter search.Expression
|
RequestFilter filter.Expression
|
||||||
ResponseFilter search.Expression
|
ResponseFilter filter.Expression
|
||||||
}
|
}
|
||||||
|
@ -12,10 +12,10 @@ import (
|
|||||||
|
|
||||||
"github.com/oklog/ulid"
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/filter"
|
||||||
"github.com/dstotijn/hetty/pkg/log"
|
"github.com/dstotijn/hetty/pkg/log"
|
||||||
"github.com/dstotijn/hetty/pkg/proxy"
|
"github.com/dstotijn/hetty/pkg/proxy"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey int
|
type contextKey int
|
||||||
@ -77,7 +77,7 @@ type service struct {
|
|||||||
type FindRequestsFilter struct {
|
type FindRequestsFilter struct {
|
||||||
ProjectID ulid.ULID
|
ProjectID ulid.ULID
|
||||||
OnlyInScope bool
|
OnlyInScope bool
|
||||||
SearchExpr search.Expression
|
SearchExpr filter.Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -8,8 +8,8 @@ import (
|
|||||||
|
|
||||||
"github.com/oklog/ulid"
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/filter"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var reqLogSearchKeyFns = map[string]func(rl RequestLog) string{
|
var reqLogSearchKeyFns = map[string]func(rl RequestLog) string{
|
||||||
@ -36,22 +36,22 @@ var ResLogSearchKeyFns = map[string]func(rl ResponseLog) string{
|
|||||||
// TODO: Request and response headers search key functions.
|
// TODO: Request and response headers search key functions.
|
||||||
|
|
||||||
// Matches returns true if the supplied search expression evaluates to true.
|
// Matches returns true if the supplied search expression evaluates to true.
|
||||||
func (reqLog RequestLog) Matches(expr search.Expression) (bool, error) {
|
func (reqLog RequestLog) Matches(expr filter.Expression) (bool, error) {
|
||||||
switch e := expr.(type) {
|
switch e := expr.(type) {
|
||||||
case search.PrefixExpression:
|
case filter.PrefixExpression:
|
||||||
return reqLog.matchPrefixExpr(e)
|
return reqLog.matchPrefixExpr(e)
|
||||||
case search.InfixExpression:
|
case filter.InfixExpression:
|
||||||
return reqLog.matchInfixExpr(e)
|
return reqLog.matchInfixExpr(e)
|
||||||
case search.StringLiteral:
|
case filter.StringLiteral:
|
||||||
return reqLog.matchStringLiteral(e)
|
return reqLog.matchStringLiteral(e)
|
||||||
default:
|
default:
|
||||||
return false, fmt.Errorf("expression type (%T) not supported", expr)
|
return false, fmt.Errorf("expression type (%T) not supported", expr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (reqLog RequestLog) matchPrefixExpr(expr search.PrefixExpression) (bool, error) {
|
func (reqLog RequestLog) matchPrefixExpr(expr filter.PrefixExpression) (bool, error) {
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpNot:
|
case filter.TokOpNot:
|
||||||
match, err := reqLog.Matches(expr.Right)
|
match, err := reqLog.Matches(expr.Right)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -63,9 +63,9 @@ func (reqLog RequestLog) matchPrefixExpr(expr search.PrefixExpression) (bool, er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, error) {
|
func (reqLog RequestLog) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpAnd:
|
case filter.TokOpAnd:
|
||||||
left, err := reqLog.Matches(expr.Left)
|
left, err := reqLog.Matches(expr.Left)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -77,7 +77,7 @@ func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
return left && right, nil
|
return left && right, nil
|
||||||
case search.TokOpOr:
|
case filter.TokOpOr:
|
||||||
left, err := reqLog.Matches(expr.Left)
|
left, err := reqLog.Matches(expr.Left)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -91,28 +91,46 @@ func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, erro
|
|||||||
return left || right, nil
|
return left || right, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
left, ok := expr.Left.(search.StringLiteral)
|
left, ok := expr.Left.(filter.StringLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("left operand must be a string literal")
|
return false, errors.New("left operand must be a string literal")
|
||||||
}
|
}
|
||||||
|
|
||||||
leftVal := reqLog.getMappedStringLiteral(left.Value)
|
leftVal := reqLog.getMappedStringLiteral(left.Value)
|
||||||
|
|
||||||
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe {
|
if leftVal == "req.headers" {
|
||||||
right, ok := expr.Right.(search.RegexpLiteral)
|
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, reqLog.Header)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return match, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if leftVal == "res.headers" && reqLog.Response != nil {
|
||||||
|
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, reqLog.Response.Header)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to match response HTTP headers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return match, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if expr.Operator == filter.TokOpRe || expr.Operator == filter.TokOpNotRe {
|
||||||
|
right, ok := expr.Right.(filter.RegexpLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("right operand must be a regular expression")
|
return false, errors.New("right operand must be a regular expression")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpRe:
|
case filter.TokOpRe:
|
||||||
return right.MatchString(leftVal), nil
|
return right.MatchString(leftVal), nil
|
||||||
case search.TokOpNotRe:
|
case filter.TokOpNotRe:
|
||||||
return !right.MatchString(leftVal), nil
|
return !right.MatchString(leftVal), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
right, ok := expr.Right.(search.StringLiteral)
|
right, ok := expr.Right.(filter.StringLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("right operand must be a string literal")
|
return false, errors.New("right operand must be a string literal")
|
||||||
}
|
}
|
||||||
@ -120,20 +138,20 @@ func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, erro
|
|||||||
rightVal := reqLog.getMappedStringLiteral(right.Value)
|
rightVal := reqLog.getMappedStringLiteral(right.Value)
|
||||||
|
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpEq:
|
case filter.TokOpEq:
|
||||||
return leftVal == rightVal, nil
|
return leftVal == rightVal, nil
|
||||||
case search.TokOpNotEq:
|
case filter.TokOpNotEq:
|
||||||
return leftVal != rightVal, nil
|
return leftVal != rightVal, nil
|
||||||
case search.TokOpGt:
|
case filter.TokOpGt:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal > rightVal, nil
|
return leftVal > rightVal, nil
|
||||||
case search.TokOpLt:
|
case filter.TokOpLt:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal < rightVal, nil
|
return leftVal < rightVal, nil
|
||||||
case search.TokOpGtEq:
|
case filter.TokOpGtEq:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal >= rightVal, nil
|
return leftVal >= rightVal, nil
|
||||||
case search.TokOpLtEq:
|
case filter.TokOpLtEq:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal <= rightVal, nil
|
return leftVal <= rightVal, nil
|
||||||
default:
|
default:
|
||||||
@ -162,7 +180,18 @@ func (reqLog RequestLog) getMappedStringLiteral(s string) string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (reqLog RequestLog) matchStringLiteral(strLiteral search.StringLiteral) (bool, error) {
|
func (reqLog RequestLog) matchStringLiteral(strLiteral filter.StringLiteral) (bool, error) {
|
||||||
|
for key, values := range reqLog.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.Contains(
|
||||||
|
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
|
||||||
|
strings.ToLower(strLiteral.Value),
|
||||||
|
) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, fn := range reqLogSearchKeyFns {
|
for _, fn := range reqLogSearchKeyFns {
|
||||||
if strings.Contains(
|
if strings.Contains(
|
||||||
strings.ToLower(fn(reqLog)),
|
strings.ToLower(fn(reqLog)),
|
||||||
@ -173,6 +202,17 @@ func (reqLog RequestLog) matchStringLiteral(strLiteral search.StringLiteral) (bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if reqLog.Response != nil {
|
if reqLog.Response != nil {
|
||||||
|
for key, values := range reqLog.Response.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.Contains(
|
||||||
|
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
|
||||||
|
strings.ToLower(strLiteral.Value),
|
||||||
|
) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, fn := range ResLogSearchKeyFns {
|
for _, fn := range ResLogSearchKeyFns {
|
||||||
if strings.Contains(
|
if strings.Contains(
|
||||||
strings.ToLower(fn(*reqLog.Response)),
|
strings.ToLower(fn(*reqLog.Response)),
|
||||||
|
@ -3,8 +3,8 @@ package reqlog_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/filter"
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRequestLogMatch(t *testing.T) {
|
func TestRequestLogMatch(t *testing.T) {
|
||||||
@ -176,7 +176,7 @@ func TestRequestLogMatch(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
searchExpr, err := search.ParseQuery(tt.query)
|
searchExpr, err := filter.ParseQuery(tt.query)
|
||||||
assertError(t, nil, err)
|
assertError(t, nil, err)
|
||||||
|
|
||||||
got, err := tt.requestLog.Matches(searchExpr)
|
got, err := tt.requestLog.Matches(searchExpr)
|
||||||
|
@ -1,212 +0,0 @@
|
|||||||
package search_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestExpressionString(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
expression search.Expression
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "string literal expression",
|
|
||||||
expression: search.StringLiteral{Value: "foobar"},
|
|
||||||
expected: `"foobar"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "boolean expression with equal operator",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpEq,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.StringLiteral{Value: "bar"},
|
|
||||||
},
|
|
||||||
expected: `("foo" = "bar")`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "boolean expression with not equal operator",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpNotEq,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.StringLiteral{Value: "bar"},
|
|
||||||
},
|
|
||||||
expected: `("foo" != "bar")`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "boolean expression with greater than operator",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpGt,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.StringLiteral{Value: "bar"},
|
|
||||||
},
|
|
||||||
expected: `("foo" > "bar")`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "boolean expression with less than operator",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpLt,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.StringLiteral{Value: "bar"},
|
|
||||||
},
|
|
||||||
expected: `("foo" < "bar")`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "boolean expression with greater than or equal operator",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpGtEq,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.StringLiteral{Value: "bar"},
|
|
||||||
},
|
|
||||||
expected: `("foo" >= "bar")`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "boolean expression with less than or equal operator",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpLtEq,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.StringLiteral{Value: "bar"},
|
|
||||||
},
|
|
||||||
expected: `("foo" <= "bar")`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "boolean expression with regular expression operator",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpRe,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.RegexpLiteral{regexp.MustCompile("bar")},
|
|
||||||
},
|
|
||||||
expected: `("foo" =~ "bar")`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "boolean expression with not regular expression operator",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpNotRe,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.RegexpLiteral{regexp.MustCompile("bar")},
|
|
||||||
},
|
|
||||||
expected: `("foo" !~ "bar")`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "boolean expression with AND, OR and NOT operators",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpAnd,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.InfixExpression{
|
|
||||||
Operator: search.TokOpOr,
|
|
||||||
Left: search.StringLiteral{Value: "bar"},
|
|
||||||
Right: search.PrefixExpression{
|
|
||||||
Operator: search.TokOpNot,
|
|
||||||
Right: search.StringLiteral{Value: "baz"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: `("foo" AND ("bar" OR (NOT "baz")))`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "boolean expression with nested group",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpOr,
|
|
||||||
Left: search.InfixExpression{
|
|
||||||
Operator: search.TokOpAnd,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.StringLiteral{Value: "bar"},
|
|
||||||
},
|
|
||||||
Right: search.PrefixExpression{
|
|
||||||
Operator: search.TokOpNot,
|
|
||||||
Right: search.StringLiteral{Value: "baz"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: `(("foo" AND "bar") OR (NOT "baz"))`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "implicit boolean expression with string literal operands",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpAnd,
|
|
||||||
Left: search.InfixExpression{
|
|
||||||
Operator: search.TokOpAnd,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.StringLiteral{Value: "bar"},
|
|
||||||
},
|
|
||||||
Right: search.StringLiteral{Value: "baz"},
|
|
||||||
},
|
|
||||||
expected: `(("foo" AND "bar") AND "baz")`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "implicit boolean expression nested in group",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpAnd,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.StringLiteral{Value: "bar"},
|
|
||||||
},
|
|
||||||
expected: `("foo" AND "bar")`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "implicit and explicit boolean expression with string literal operands",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpAnd,
|
|
||||||
Left: search.InfixExpression{
|
|
||||||
Operator: search.TokOpAnd,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.InfixExpression{
|
|
||||||
Operator: search.TokOpOr,
|
|
||||||
Left: search.StringLiteral{Value: "bar"},
|
|
||||||
Right: search.StringLiteral{Value: "baz"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Right: search.StringLiteral{Value: "yolo"},
|
|
||||||
},
|
|
||||||
expected: `(("foo" AND ("bar" OR "baz")) AND "yolo")`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "implicit boolean expression with comparison operands",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpAnd,
|
|
||||||
Left: search.InfixExpression{
|
|
||||||
Operator: search.TokOpEq,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.StringLiteral{Value: "bar"},
|
|
||||||
},
|
|
||||||
Right: search.InfixExpression{
|
|
||||||
Operator: search.TokOpRe,
|
|
||||||
Left: search.StringLiteral{Value: "baz"},
|
|
||||||
Right: search.RegexpLiteral{regexp.MustCompile("yolo")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: `(("foo" = "bar") AND ("baz" =~ "yolo"))`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "eq operator takes precedence over boolean ops",
|
|
||||||
expression: search.InfixExpression{
|
|
||||||
Operator: search.TokOpOr,
|
|
||||||
Left: search.InfixExpression{
|
|
||||||
Operator: search.TokOpEq,
|
|
||||||
Left: search.StringLiteral{Value: "foo"},
|
|
||||||
Right: search.StringLiteral{Value: "bar"},
|
|
||||||
},
|
|
||||||
Right: search.InfixExpression{
|
|
||||||
Operator: search.TokOpEq,
|
|
||||||
Left: search.StringLiteral{Value: "baz"},
|
|
||||||
Right: search.StringLiteral{Value: "yolo"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: `(("foo" = "bar") OR ("baz" = "yolo"))`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
got := tt.expression.String()
|
|
||||||
if tt.expected != got {
|
|
||||||
t.Errorf("expected: %v, got: %v", tt.expected, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,9 +7,9 @@ import (
|
|||||||
|
|
||||||
"github.com/oklog/ulid"
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/filter"
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var senderReqSearchKeyFns = map[string]func(req Request) string{
|
var senderReqSearchKeyFns = map[string]func(req Request) string{
|
||||||
@ -29,22 +29,22 @@ var senderReqSearchKeyFns = map[string]func(req Request) string{
|
|||||||
// TODO: Request and response headers search key functions.
|
// TODO: Request and response headers search key functions.
|
||||||
|
|
||||||
// Matches returns true if the supplied search expression evaluates to true.
|
// Matches returns true if the supplied search expression evaluates to true.
|
||||||
func (req Request) Matches(expr search.Expression) (bool, error) {
|
func (req Request) Matches(expr filter.Expression) (bool, error) {
|
||||||
switch e := expr.(type) {
|
switch e := expr.(type) {
|
||||||
case search.PrefixExpression:
|
case filter.PrefixExpression:
|
||||||
return req.matchPrefixExpr(e)
|
return req.matchPrefixExpr(e)
|
||||||
case search.InfixExpression:
|
case filter.InfixExpression:
|
||||||
return req.matchInfixExpr(e)
|
return req.matchInfixExpr(e)
|
||||||
case search.StringLiteral:
|
case filter.StringLiteral:
|
||||||
return req.matchStringLiteral(e)
|
return req.matchStringLiteral(e)
|
||||||
default:
|
default:
|
||||||
return false, fmt.Errorf("expression type (%T) not supported", expr)
|
return false, fmt.Errorf("expression type (%T) not supported", expr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (req Request) matchPrefixExpr(expr search.PrefixExpression) (bool, error) {
|
func (req Request) matchPrefixExpr(expr filter.PrefixExpression) (bool, error) {
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpNot:
|
case filter.TokOpNot:
|
||||||
match, err := req.Matches(expr.Right)
|
match, err := req.Matches(expr.Right)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -56,9 +56,9 @@ func (req Request) matchPrefixExpr(expr search.PrefixExpression) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) {
|
func (req Request) matchInfixExpr(expr filter.InfixExpression) (bool, error) {
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpAnd:
|
case filter.TokOpAnd:
|
||||||
left, err := req.Matches(expr.Left)
|
left, err := req.Matches(expr.Left)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -70,7 +70,7 @@ func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return left && right, nil
|
return left && right, nil
|
||||||
case search.TokOpOr:
|
case filter.TokOpOr:
|
||||||
left, err := req.Matches(expr.Left)
|
left, err := req.Matches(expr.Left)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -84,28 +84,46 @@ func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) {
|
|||||||
return left || right, nil
|
return left || right, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
left, ok := expr.Left.(search.StringLiteral)
|
left, ok := expr.Left.(filter.StringLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("left operand must be a string literal")
|
return false, errors.New("left operand must be a string literal")
|
||||||
}
|
}
|
||||||
|
|
||||||
leftVal := req.getMappedStringLiteral(left.Value)
|
leftVal := req.getMappedStringLiteral(left.Value)
|
||||||
|
|
||||||
if expr.Operator == search.TokOpRe || expr.Operator == search.TokOpNotRe {
|
if leftVal == "req.headers" {
|
||||||
right, ok := expr.Right.(search.RegexpLiteral)
|
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.Header)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return match, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if leftVal == "res.headers" && req.Response != nil {
|
||||||
|
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.Response.Header)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to match response HTTP headers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return match, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if expr.Operator == filter.TokOpRe || expr.Operator == filter.TokOpNotRe {
|
||||||
|
right, ok := expr.Right.(filter.RegexpLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("right operand must be a regular expression")
|
return false, errors.New("right operand must be a regular expression")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpRe:
|
case filter.TokOpRe:
|
||||||
return right.MatchString(leftVal), nil
|
return right.MatchString(leftVal), nil
|
||||||
case search.TokOpNotRe:
|
case filter.TokOpNotRe:
|
||||||
return !right.MatchString(leftVal), nil
|
return !right.MatchString(leftVal), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
right, ok := expr.Right.(search.StringLiteral)
|
right, ok := expr.Right.(filter.StringLiteral)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, errors.New("right operand must be a string literal")
|
return false, errors.New("right operand must be a string literal")
|
||||||
}
|
}
|
||||||
@ -113,20 +131,20 @@ func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) {
|
|||||||
rightVal := req.getMappedStringLiteral(right.Value)
|
rightVal := req.getMappedStringLiteral(right.Value)
|
||||||
|
|
||||||
switch expr.Operator {
|
switch expr.Operator {
|
||||||
case search.TokOpEq:
|
case filter.TokOpEq:
|
||||||
return leftVal == rightVal, nil
|
return leftVal == rightVal, nil
|
||||||
case search.TokOpNotEq:
|
case filter.TokOpNotEq:
|
||||||
return leftVal != rightVal, nil
|
return leftVal != rightVal, nil
|
||||||
case search.TokOpGt:
|
case filter.TokOpGt:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal > rightVal, nil
|
return leftVal > rightVal, nil
|
||||||
case search.TokOpLt:
|
case filter.TokOpLt:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal < rightVal, nil
|
return leftVal < rightVal, nil
|
||||||
case search.TokOpGtEq:
|
case filter.TokOpGtEq:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal >= rightVal, nil
|
return leftVal >= rightVal, nil
|
||||||
case search.TokOpLtEq:
|
case filter.TokOpLtEq:
|
||||||
// TODO(?) attempt to parse as int.
|
// TODO(?) attempt to parse as int.
|
||||||
return leftVal <= rightVal, nil
|
return leftVal <= rightVal, nil
|
||||||
default:
|
default:
|
||||||
@ -155,7 +173,18 @@ func (req Request) getMappedStringLiteral(s string) string {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (req Request) matchStringLiteral(strLiteral search.StringLiteral) (bool, error) {
|
func (req Request) matchStringLiteral(strLiteral filter.StringLiteral) (bool, error) {
|
||||||
|
for key, values := range req.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.Contains(
|
||||||
|
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
|
||||||
|
strings.ToLower(strLiteral.Value),
|
||||||
|
) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, fn := range senderReqSearchKeyFns {
|
for _, fn := range senderReqSearchKeyFns {
|
||||||
if strings.Contains(
|
if strings.Contains(
|
||||||
strings.ToLower(fn(req)),
|
strings.ToLower(fn(req)),
|
||||||
@ -166,6 +195,17 @@ func (req Request) matchStringLiteral(strLiteral search.StringLiteral) (bool, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Response != nil {
|
if req.Response != nil {
|
||||||
|
for key, values := range req.Response.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
if strings.Contains(
|
||||||
|
strings.ToLower(fmt.Sprintf("%v: %v", key, value)),
|
||||||
|
strings.ToLower(strLiteral.Value),
|
||||||
|
) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, fn := range reqlog.ResLogSearchKeyFns {
|
for _, fn := range reqlog.ResLogSearchKeyFns {
|
||||||
if strings.Contains(
|
if strings.Contains(
|
||||||
strings.ToLower(fn(*req.Response)),
|
strings.ToLower(fn(*req.Response)),
|
||||||
|
@ -3,8 +3,8 @@ package sender_test
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/filter"
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
|
||||||
"github.com/dstotijn/hetty/pkg/sender"
|
"github.com/dstotijn/hetty/pkg/sender"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ func TestRequestLogMatch(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
searchExpr, err := search.ParseQuery(tt.query)
|
searchExpr, err := filter.ParseQuery(tt.query)
|
||||||
assertError(t, nil, err)
|
assertError(t, nil, err)
|
||||||
|
|
||||||
got, err := tt.senderReq.Matches(searchExpr)
|
got, err := tt.senderReq.Matches(searchExpr)
|
||||||
|
@ -12,9 +12,9 @@ import (
|
|||||||
|
|
||||||
"github.com/oklog/ulid"
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/filter"
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
"github.com/dstotijn/hetty/pkg/search"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
@ -54,7 +54,7 @@ type service struct {
|
|||||||
type FindRequestsFilter struct {
|
type FindRequestsFilter struct {
|
||||||
ProjectID ulid.ULID
|
ProjectID ulid.ULID
|
||||||
OnlyInScope bool
|
OnlyInScope bool
|
||||||
SearchExpr search.Expression
|
SearchExpr filter.Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Reference in New Issue
Block a user