Compare commits

..

20 Commits

Author SHA1 Message Date
6889c9c183 Replace GraphQL server with Connect RPC 2025-02-05 21:54:59 +01:00
52c83a1989 Update go version matrices for CI/CD workflows 2025-01-13 23:26:31 +01:00
7ed553866b Update README and usage docs to with new default DB file path 2025-01-13 23:18:22 +01:00
fcf0e1c51e Replace BadgerDB with bbolt 2025-01-13 23:15:18 +01:00
24c2ecfa4b Remove database mocks, replace Service interface indirection 2025-01-04 00:39:40 +01:00
d23b4ed024 Update copyright footer 2025-01-01 20:52:14 +01:00
ff9e4140aa Remove Tines as sponsor 2025-01-01 20:50:52 +01:00
f7def87d0f Add HTTP header support to string literal matching 2022-03-31 15:23:56 +02:00
aa9822854d Rename search package to filter package 2022-03-31 15:12:54 +02:00
2ce4218a30 Add filter support for HTTP headers 2022-03-31 14:53:40 +02:00
fd27955e11 Sort HTTP headers 2022-03-31 12:07:35 +02:00
426a7d5f96 Add "New request" button to Sender page 2022-03-31 11:23:17 +02:00
21b679dc91 Add new sponsorship options 2022-03-30 13:02:58 +02:00
e4f468d4d2 Publish Docker image to ghcr.io and docker.io 2022-03-30 11:50:16 +02:00
d3246b0918 Add intercept feature to README 2022-03-29 14:06:51 +02:00
61fd3fcc45 Update admin dependencies 2022-03-29 13:58:38 +02:00
d34258dfd1 Add links to intercept filter docs 2022-03-29 13:52:03 +02:00
0e9fb0ac91 Add tests for search.Expression interface implementations 2022-03-23 15:39:45 +01:00
02408b5196 Add intercept module 2022-03-23 14:31:27 +01:00
6ffc55cde3 Fix Snapcraft plugs config to allow network binding 2022-03-15 10:11:53 +01:00
83 changed files with 8276 additions and 15051 deletions

4
.github/FUNDING.yml vendored
View File

@ -1,3 +1,3 @@
# These are supported funding model platforms github: dstotijn
patreon: dstotijn patreon: dstotijn
custom: "https://www.paypal.com/paypalme/dstotijn"

View File

@ -5,7 +5,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
go: ["1.17", "1.16"] go: ["1.23", "1.22", "1.21"]
name: Go ${{ matrix.go }} - Build name: Go ${{ matrix.go }} - Build
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
go: ["1.17", "1.16"] go: ["1.23", "1.22", "1.21"]
name: Go ${{ matrix.go }} - Test name: Go ${{ matrix.go }} - Test
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@ -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/dstotijn/hetty/pkg/filter.Expression"
issues: issues:
exclude-rules: exclude-rules:

View File

@ -55,13 +55,8 @@ snapcrafts:
license: MIT license: MIT
apps: apps:
hetty: hetty:
plugs: ["home", "network", "network-bind", "personal-files"] command: hetty
plugs: plugs: ["network", "network-bind"]
personal-files:
read:
- $HOME/.hetty
write:
- $HOME/.hetty
scoop: scoop:
bucket: bucket:
@ -74,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"

View File

@ -17,6 +17,7 @@ features tailored to the needs of the infosec and bug bounty community.
- Machine-in-the-middle (MITM) HTTP proxy, with logs and advanced search - Machine-in-the-middle (MITM) HTTP proxy, with logs and advanced search
- HTTP client for manually creating/editing requests, and replay proxied requests - HTTP client for manually creating/editing requests, and replay proxied requests
- Intercept requests and responses for manual review (edit, send/receive, cancel)
- Scope support, to help keep work organized - Scope support, to help keep work organized
- Easy-to-use web based admin interface - Easy-to-use web based admin interface
- Project based database storage, to help keep work organized - Project based database storage, to help keep work organized
@ -50,11 +51,6 @@ brew install hettysoft/tap/hetty
sudo snap install hetty sudo snap install hetty
``` ```
⚠️ As of Sun 6 Mar 2022, we're awaiting Canonical to approve the necessary
Snapcraft privileges to allow Hetty to bind on a port. In the meantime, please
use one of the Linux [releases](https://github.com/dstotijn/hetty/releases) on
Github.
#### Windows #### Windows
```sh ```sh
@ -68,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
@ -90,12 +96,12 @@ $ hetty --help
Usage: Usage:
hetty [flags] [subcommand] [flags] hetty [flags] [subcommand] [flags]
Runs an HTTP server with (MITM) proxy, GraphQL service, and a web based admin interface. Runs an HTTP server with (MITM) proxy and a web based admin interface.
Options: Options:
--cert Path to root CA certificate. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_cert.pem") --cert Path to root CA certificate. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_cert.pem")
--key Path to root CA private key. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_key.pem") --key Path to root CA private key. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_key.pem")
--db Database directory path. (Default: "~/.hetty/db") --db Database file path. Creates file if it doesn't exist. (Default: "~/.hetty/hetty.db")
--addr TCP address for HTTP server to listen on, in the form \"host:port\". (Default: ":8080") --addr TCP address for HTTP server to listen on, in the form \"host:port\". (Default: ":8080")
--chrome Launch Chrome with proxy settings applied and certificate errors ignored. (Default: false) --chrome Launch Chrome with proxy settings applied and certificate errors ignored. (Default: false)
--verbose Enable verbose logging. --verbose Enable verbose logging.
@ -140,12 +146,10 @@ Guidelines](CONTRIBUTING.md) for details.
## Sponsors ## Sponsors
<a href="https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=hetty"> 💖 Are you enjoying Hetty? You can [sponsor me](https://github.com/sponsors/dstotijn)!
<img src="https://hetty.xyz/img/tines-sponsorship-badge.png" width="140" alt="Sponsored by Tines">
</a>
## License ## License
[MIT](LICENSE) [MIT](LICENSE)
© 2022 Hetty Software © 20192025 Hetty Software

View File

@ -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: "" }]);
}, },
}); });

View File

@ -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 }}>

View File

@ -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

View File

@ -16,6 +16,7 @@ import {
TextFieldProps, TextFieldProps,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import MaterialLink from "@mui/material/Link";
import { SwitchBaseProps } from "@mui/material/internal/SwitchBase"; import { SwitchBaseProps } from "@mui/material/internal/SwitchBase";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
@ -217,7 +218,13 @@ export default function Settings(): JSX.Element {
onChange={(e) => setInterceptReqFilter(e.target.value)} onChange={(e) => setInterceptReqFilter(e.target.value)}
/> />
<FormHelperText> <FormHelperText>
Filter expression to match incoming requests on. When set, only matching requests are intercepted. Filter expression to match incoming requests on. When set, only matching requests are intercepted.{" "}
<MaterialLink
href="https://hetty.xyz/docs/guides/intercept?utm_source=hettyapp#request-filter"
target="_blank"
>
Read docs.
</MaterialLink>
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
<Button <Button
@ -266,7 +273,13 @@ export default function Settings(): JSX.Element {
onChange={(e) => setInterceptResFilter(e.target.value)} onChange={(e) => setInterceptResFilter(e.target.value)}
/> />
<FormHelperText> <FormHelperText>
Filter expression to match received responses on. When set, only matching responses are intercepted. Filter expression to match received responses on. When set, only matching responses are intercepted.{" "}
<MaterialLink
href="https://hetty.xyz/docs/guides/intercept/?utm_source=hettyapp#response-filter"
target="_blank"
>
Read docs.
</MaterialLink>
</FormHelperText> </FormHelperText>
</FormControl> </FormControl>
<Button <Button

View File

@ -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;

View File

@ -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>

File diff suppressed because it is too large Load Diff

10
buf.gen.yaml Normal file
View File

@ -0,0 +1,10 @@
version: v2
plugins:
- local: protoc-gen-go
out: pkg
opt: paths=source_relative
- local: protoc-gen-connect-go
out: pkg
opt:
- paths=source_relative
- package_suffix # Generate `*.connect.go` files next to `*.pb.go` files.

12
buf.yaml Normal file
View File

@ -0,0 +1,12 @@
version: v2
modules:
- path: proto
lint:
use:
- STANDARD
except:
- PACKAGE_DIRECTORY_MATCH
- PACKAGE_VERSION_SUFFIX
breaking:
use:
- PACKAGE

View File

@ -16,16 +16,14 @@ import (
"strings" "strings"
"github.com/chromedp/chromedp" "github.com/chromedp/chromedp"
badgerdb "github.com/dgraph-io/badger/v3"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"github.com/peterbourgon/ff/v3/ffcli" "github.com/peterbourgon/ff/v3/ffcli"
"go.etcd.io/bbolt"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/dstotijn/hetty/pkg/api"
"github.com/dstotijn/hetty/pkg/chrome" "github.com/dstotijn/hetty/pkg/chrome"
"github.com/dstotijn/hetty/pkg/db/badger" "github.com/dstotijn/hetty/pkg/db/bolt"
"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"
@ -46,12 +44,12 @@ var hettyUsage = `
Usage: Usage:
hetty [flags] [subcommand] [flags] hetty [flags] [subcommand] [flags]
Runs an HTTP server with (MITM) proxy, GraphQL service, and a web based admin interface. Runs an HTTP server with (MITM) proxy and a web based admin interface.
Options: Options:
--cert Path to root CA certificate. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_cert.pem") --cert Path to root CA certificate. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_cert.pem")
--key Path to root CA private key. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_key.pem") --key Path to root CA private key. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_key.pem")
--db Database directory path. (Default: "~/.hetty/db") --db Database file path. Creates file if it doesn't exist. (Default: "~/.hetty/hetty.db")
--addr TCP address for HTTP server to listen on, in the form \"host:port\". (Default: ":8080") --addr TCP address for HTTP server to listen on, in the form \"host:port\". (Default: ":8080")
--chrome Launch Chrome with proxy settings applied and certificate errors ignored. (Default: false) --chrome Launch Chrome with proxy settings applied and certificate errors ignored. (Default: false)
--verbose Enable verbose logging. --verbose Enable verbose logging.
@ -89,7 +87,7 @@ func NewHettyCommand() (*ffcli.Command, *Config) {
"Path to root CA certificate. Creates a new certificate if file doesn't exist.") "Path to root CA certificate. Creates a new certificate if file doesn't exist.")
fs.StringVar(&cmd.key, "key", "~/.hetty/hetty_key.pem", fs.StringVar(&cmd.key, "key", "~/.hetty/hetty_key.pem",
"Path to root CA private key. Creates a new private key if file doesn't exist.") "Path to root CA private key. Creates a new private key if file doesn't exist.")
fs.StringVar(&cmd.db, "db", "~/.hetty/db", "Database directory path.") fs.StringVar(&cmd.db, "db", "~/.hetty/hetty.db", "Database file path. Creates file if it doesn't exist.")
fs.StringVar(&cmd.addr, "addr", ":8080", "TCP address to listen on, in the form \"host:port\".") fs.StringVar(&cmd.addr, "addr", ":8080", "TCP address to listen on, in the form \"host:port\".")
fs.BoolVar(&cmd.chrome, "chrome", false, "Launch Chrome with proxy settings applied and certificate errors ignored.") fs.BoolVar(&cmd.chrome, "chrome", false, "Launch Chrome with proxy settings applied and certificate errors ignored.")
fs.BoolVar(&cmd.version, "version", false, "Output version.") fs.BoolVar(&cmd.version, "version", false, "Output version.")
@ -154,25 +152,21 @@ func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error {
cmd.config.logger.Fatal("Failed to load or create CA key pair.", zap.Error(err)) cmd.config.logger.Fatal("Failed to load or create CA key pair.", zap.Error(err))
} }
// BadgerDB logs some verbose entries with `INFO` level, so unless dbLogger := cmd.config.logger.Named("boltdb").Sugar()
// we're running in debug mode, bump the minimal level to `WARN`. boltOpts := *bbolt.DefaultOptions
dbLogger := cmd.config.logger.Named("badgerdb").WithOptions(zap.IncreaseLevel(zapcore.WarnLevel)) boltOpts.Logger = &bolt.Logger{SugaredLogger: dbLogger}
dbSugaredLogger := dbLogger.Sugar() boltDB, err := bolt.OpenDatabase(dbPath, &boltOpts)
badger, err := badger.OpenDatabase(
badgerdb.DefaultOptions(dbPath).WithLogger(badger.NewLogger(dbSugaredLogger)),
)
if err != nil { if err != nil {
cmd.config.logger.Fatal("Failed to open database.", zap.Error(err)) cmd.config.logger.Fatal("Failed to open database.", zap.Error(err))
} }
defer badger.Close() defer boltDB.Close()
scope := &scope.Scope{} scope := &scope.Scope{}
reqLogService := reqlog.NewService(reqlog.Config{ reqLogService := reqlog.NewService(reqlog.Config{
Scope: scope, Scope: scope,
Repository: badger, Repository: boltDB,
Logger: cmd.config.logger.Named("reqlog").Sugar(), Logger: cmd.config.logger.Named("reqlog").Sugar(),
}) })
@ -181,12 +175,12 @@ func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error {
}) })
senderService := sender.NewService(sender.Config{ senderService := sender.NewService(sender.Config{
Repository: badger, Repository: boltDB,
ReqLogService: reqLogService, ReqLogService: reqLogService,
}) })
projService, err := proj.NewService(proj.Config{ projService, err := proj.NewService(proj.Config{
Repository: badger, Repository: boltDB,
InterceptService: interceptService, InterceptService: interceptService,
ReqLogService: reqLogService, ReqLogService: reqLogService,
SenderService: senderService, SenderService: senderService,
@ -234,14 +228,11 @@ func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error {
req.Method != http.MethodConnect && !strings.HasPrefix(req.RequestURI, "http://") req.Method != http.MethodConnect && !strings.HasPrefix(req.RequestURI, "http://")
}).Subrouter().StrictSlash(true) }).Subrouter().StrictSlash(true)
// GraphQL server. // Connect RPC server.
gqlEndpoint := "/api/graphql/" projPath, projHandler := proj.NewProjectServiceHandler(projService)
adminRouter.Path(gqlEndpoint).Handler(api.HTTPHandler(&api.Resolver{ adminRouter.PathPrefix(projPath).Handler(projHandler)
ProjectService: projService, reqlogPath, reqlogHandler := reqlog.NewHttpRequestLogServiceHandler(reqLogService)
RequestLogService: reqLogService, adminRouter.PathPrefix(reqlogPath).Handler(reqlogHandler)
InterceptService: interceptService,
SenderService: senderService,
}, gqlEndpoint))
// Admin interface. // Admin interface.
adminRouter.PathPrefix("").Handler(adminHandler) adminRouter.PathPrefix("").Handler(adminHandler)

43
go.mod
View File

@ -1,59 +1,34 @@
module github.com/dstotijn/hetty module github.com/dstotijn/hetty
go 1.17 go 1.23
toolchain go1.23.4
require ( require (
github.com/99designs/gqlgen v0.14.0 connectrpc.com/connect v1.18.1
github.com/chromedp/chromedp v0.7.8 github.com/chromedp/chromedp v0.7.8
github.com/dgraph-io/badger/v3 v3.2103.2 github.com/google/go-cmp v0.5.9
github.com/google/go-cmp v0.5.6
github.com/gorilla/mux v1.7.4 github.com/gorilla/mux v1.7.4
github.com/matryer/moq v0.2.5
github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-homedir v1.1.0
github.com/oklog/ulid v1.3.1 github.com/oklog/ulid/v2 v2.1.0
github.com/peterbourgon/ff/v3 v3.1.2 github.com/peterbourgon/ff/v3 v3.1.2
github.com/smallstep/truststore v0.11.0 github.com/smallstep/truststore v0.11.0
github.com/vektah/gqlparser/v2 v2.2.0 go.etcd.io/bbolt v1.4.0-beta.0
go.uber.org/zap v1.21.0 go.uber.org/zap v1.21.0
google.golang.org/protobuf v1.36.3
) )
require ( require (
github.com/agnivade/levenshtein v1.1.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf // indirect github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf // indirect
github.com/chromedp/sysutil v1.0.0 // indirect github.com/chromedp/sysutil v1.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0 // indirect github.com/gobwas/ws v1.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
github.com/golang/protobuf v1.3.1 // indirect
github.com/golang/snappy v0.0.3 // indirect
github.com/google/flatbuffers v1.12.1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.12.3 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/urfave/cli/v2 v2.1.1 // indirect
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e // indirect
go.opencensus.io v0.22.5 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect go.uber.org/multierr v1.6.0 // indirect
golang.org/x/mod v0.4.2 // indirect golang.org/x/sys v0.26.0 // indirect
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
golang.org/x/tools v0.1.5 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
) )

200
go.sum
View File

@ -1,120 +1,42 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
github.com/99designs/gqlgen v0.14.0 h1:Wg8aNYQUjMR/4v+W3xD+7SizOy6lSvVeQ06AobNQAXI= connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
github.com/99designs/gqlgen v0.14.0/go.mod h1:S7z4boV+Nx4VvzMUpVrY/YuHjFX4n7rDyuTqvAkuoRE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM=
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf h1:1omDWNUsWxn2HpiMiMuyRmzjl9uG7RP3IE6GTlpgJWU= github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf h1:1omDWNUsWxn2HpiMiMuyRmzjl9uG7RP3IE6GTlpgJWU=
github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U= github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
github.com/chromedp/chromedp v0.7.8 h1:JFPIFb28LPjcx6l6mUUzLOTD/TgswcTtg7KrDn8S/2I= github.com/chromedp/chromedp v0.7.8 h1:JFPIFb28LPjcx6l6mUUzLOTD/TgswcTtg7KrDn8S/2I=
github.com/chromedp/chromedp v0.7.8/go.mod h1:HcIUFBa5vA+u2QI3+xljiU59llUQ8lgGoLzYSCBfmUA= github.com/chromedp/chromedp v0.7.8/go.mod h1:HcIUFBa5vA+u2QI3+xljiU59llUQ8lgGoLzYSCBfmUA=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8=
github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M=
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU=
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/matryer/moq v0.2.5 h1:BGQISyhl7Gc9W/gMYmAJONh9mT6AYeyeTjNupNPknMs=
github.com/matryer/moq v0.2.5/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5 h1:1SoBaSPudixRecmlHXb/GxmaD3fLMtHIDN13QujwQuc= github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5 h1:1SoBaSPudixRecmlHXb/GxmaD3fLMtHIDN13QujwQuc=
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/peterbourgon/ff/v3 v3.1.2 h1:0GNhbRhO9yHA4CC27ymskOsuRpmX0YQxwxM9UPiP6JM= github.com/peterbourgon/ff/v3 v3.1.2 h1:0GNhbRhO9yHA4CC27ymskOsuRpmX0YQxwxM9UPiP6JM=
github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE= github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE=
@ -123,49 +45,16 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/smallstep/truststore v0.11.0 h1:JUTkQ4oHr40jHTS/A2t0usEhteMWG+45CDD2iJA/dIk= github.com/smallstep/truststore v0.11.0 h1:JUTkQ4oHr40jHTS/A2t0usEhteMWG+45CDD2iJA/dIk=
github.com/smallstep/truststore v0.11.0/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM= github.com/smallstep/truststore v0.11.0/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser/v2 v2.2.0 h1:bAc3slekAAJW6sZTi07aGq0OrfaCjj4jxARAaC7g2EM=
github.com/vektah/gqlparser/v2 v2.2.0/go.mod h1:i3mQIGIrbK2PD1RrCeMTlVbkF2FJ6WkU1KJlJlC+3F4=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= go.etcd.io/bbolt v1.4.0-beta.0 h1:U7Y9yH6ZojEo5/BDFMXDXD1RNx9L7iKxudzqR68jLaM=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.etcd.io/bbolt v1.4.0-beta.0/go.mod h1:Qv5yHB6jkQESXT/uVfxJgUPMqgAyhL0GLxcQaz9bSec=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
@ -174,95 +63,52 @@ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M= howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=

View File

@ -1,56 +0,0 @@
# Where are all the schema files located? globs are supported eg src/**/*.graphqls
schema:
- pkg/api/schema.graphql
# Where should the generated server code go?
exec:
filename: pkg/api/generated.go
package: api
# Uncomment to enable federation
# federation:
# filename: graph/generated/federation.go
# package: generated
# Where should any generated models go?
model:
filename: pkg/api/models_gen.go
package: api
# Where should the resolver implementations go?
resolver:
layout: single-file
filename: pkg/api/resolvers.go
dir: pkg/api
package: api
# Optional: turn on use `gqlgen:"fieldName"` tags in your models
# struct_tag: json
# Optional: turn on to use []Thing instead of []*Thing
omit_slice_element_pointers: true
# Optional: set to speed up generation time by not performing a final validation pass.
# skip_validation: true
# gqlgen will search for any type names in the schema in these go packages
# if they match it will use them, otherwise it will generate them.
# autobind:
# - "github.com/dstotijn/hetty/graph/model"
# This section declares type mapping between the GraphQL and go type systems
#
# The first line in each type will be used as defaults for resolver arguments and
# modelgen, the others will be allowed when binding to fields. Configure them to
# your liking
models:
ID:
model:
- github.com/dstotijn/hetty/pkg/api.ULID
URL:
model:
- github.com/dstotijn/hetty/pkg/api.URL
# Int:
# model:
# - github.com/99designs/gqlgen/graphql.Int
# - github.com/99designs/gqlgen/graphql.Int64
# - github.com/99designs/gqlgen/graphql.Int32

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +0,0 @@
package api
import (
"net/http"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/gorilla/mux"
)
func HTTPHandler(resolver *Resolver, gqlEndpoint string) http.Handler {
router := mux.NewRouter().SkipClean(true)
router.Methods("POST").Handler(
handler.NewDefaultServer(NewExecutableSchema(Config{
Resolvers: resolver,
})),
)
router.Methods("GET").Handler(playground.Handler("GraphQL Playground", gqlEndpoint))
return router
}

View File

@ -1,51 +0,0 @@
package api
import (
"fmt"
"io"
"net/url"
"strconv"
"github.com/99designs/gqlgen/graphql"
"github.com/oklog/ulid"
)
func MarshalULID(u ulid.ULID) graphql.Marshaler {
return graphql.WriterFunc(func(w io.Writer) {
fmt.Fprint(w, strconv.Quote(u.String()))
})
}
func UnmarshalULID(v interface{}) (ulid.ULID, error) {
rawULID, ok := v.(string)
if !ok {
return ulid.ULID{}, fmt.Errorf("ulid must be a string")
}
u, err := ulid.Parse(rawULID)
if err != nil {
return ulid.ULID{}, fmt.Errorf("failed to parse ULID: %w", err)
}
return u, nil
}
func MarshalURL(u *url.URL) graphql.Marshaler {
return graphql.WriterFunc(func(w io.Writer) {
fmt.Fprint(w, strconv.Quote(u.String()))
})
}
func UnmarshalURL(v interface{}) (*url.URL, error) {
rawURL, ok := v.(string)
if !ok {
return nil, fmt.Errorf("url must be a string")
}
u, err := url.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
return u, nil
}

View File

@ -1,301 +0,0 @@
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
package api
import (
"fmt"
"io"
"net/url"
"strconv"
"time"
"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"`
}
type CloseProjectResult struct {
Success bool `json:"success"`
}
type DeleteProjectResult struct {
Success bool `json:"success"`
}
type DeleteSenderRequestsResult struct {
Success bool `json:"success"`
}
type HTTPHeader struct {
Key string `json:"key"`
Value string `json:"value"`
}
type HTTPHeaderInput struct {
Key string `json:"key"`
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"`
Method HTTPMethod `json:"method"`
Proto string `json:"proto"`
Headers []HTTPHeader `json:"headers"`
Body *string `json:"body"`
Timestamp time.Time `json:"timestamp"`
Response *HTTPResponseLog `json:"response"`
}
type HTTPRequestLogFilter struct {
OnlyInScope bool `json:"onlyInScope"`
SearchExpression *string `json:"searchExpression"`
}
type HTTPRequestLogFilterInput struct {
OnlyInScope *bool `json:"onlyInScope"`
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"`
Proto HTTPProtocol `json:"proto"`
StatusCode int `json:"statusCode"`
StatusReason string `json:"statusReason"`
Body *string `json:"body"`
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"`
Settings *ProjectSettings `json:"settings"`
}
type ProjectSettings struct {
Intercept *InterceptSettings `json:"intercept"`
}
type ScopeHeader struct {
Key *string `json:"key"`
Value *string `json:"value"`
}
type ScopeHeaderInput struct {
Key *string `json:"key"`
Value *string `json:"value"`
}
type ScopeRule struct {
URL *string `json:"url"`
Header *ScopeHeader `json:"header"`
Body *string `json:"body"`
}
type ScopeRuleInput struct {
URL *string `json:"url"`
Header *ScopeHeaderInput `json:"header"`
Body *string `json:"body"`
}
type SenderRequest struct {
ID ulid.ULID `json:"id"`
SourceRequestLogID *ulid.ULID `json:"sourceRequestLogID"`
URL *url.URL `json:"url"`
Method HTTPMethod `json:"method"`
Proto HTTPProtocol `json:"proto"`
Headers []HTTPHeader `json:"headers"`
Body *string `json:"body"`
Timestamp time.Time `json:"timestamp"`
Response *HTTPResponseLog `json:"response"`
}
type SenderRequestFilter struct {
OnlyInScope bool `json:"onlyInScope"`
SearchExpression *string `json:"searchExpression"`
}
type SenderRequestFilterInput struct {
OnlyInScope *bool `json:"onlyInScope"`
SearchExpression *string `json:"searchExpression"`
}
type SenderRequestInput 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"`
}
type UpdateInterceptSettingsInput struct {
RequestsEnabled bool `json:"requestsEnabled"`
ResponsesEnabled bool `json:"responsesEnabled"`
RequestFilter *string `json:"requestFilter"`
ResponseFilter *string `json:"responseFilter"`
}
type HTTPMethod string
const (
HTTPMethodGet HTTPMethod = "GET"
HTTPMethodHead HTTPMethod = "HEAD"
HTTPMethodPost HTTPMethod = "POST"
HTTPMethodPut HTTPMethod = "PUT"
HTTPMethodDelete HTTPMethod = "DELETE"
HTTPMethodConnect HTTPMethod = "CONNECT"
HTTPMethodOptions HTTPMethod = "OPTIONS"
HTTPMethodTrace HTTPMethod = "TRACE"
HTTPMethodPatch HTTPMethod = "PATCH"
)
var AllHTTPMethod = []HTTPMethod{
HTTPMethodGet,
HTTPMethodHead,
HTTPMethodPost,
HTTPMethodPut,
HTTPMethodDelete,
HTTPMethodConnect,
HTTPMethodOptions,
HTTPMethodTrace,
HTTPMethodPatch,
}
func (e HTTPMethod) IsValid() bool {
switch e {
case HTTPMethodGet, HTTPMethodHead, HTTPMethodPost, HTTPMethodPut, HTTPMethodDelete, HTTPMethodConnect, HTTPMethodOptions, HTTPMethodTrace, HTTPMethodPatch:
return true
}
return false
}
func (e HTTPMethod) String() string {
return string(e)
}
func (e *HTTPMethod) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = HTTPMethod(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid HttpMethod", str)
}
return nil
}
func (e HTTPMethod) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type HTTPProtocol string
const (
HTTPProtocolHTTP10 HTTPProtocol = "HTTP10"
HTTPProtocolHTTP11 HTTPProtocol = "HTTP11"
HTTPProtocolHTTP20 HTTPProtocol = "HTTP20"
)
var AllHTTPProtocol = []HTTPProtocol{
HTTPProtocolHTTP10,
HTTPProtocolHTTP11,
HTTPProtocolHTTP20,
}
func (e HTTPProtocol) IsValid() bool {
switch e {
case HTTPProtocolHTTP10, HTTPProtocolHTTP11, HTTPProtocolHTTP20:
return true
}
return false
}
func (e HTTPProtocol) String() string {
return string(e)
}
func (e *HTTPProtocol) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = HTTPProtocol(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid HttpProtocol", str)
}
return nil
}
func (e HTTPProtocol) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}

View File

@ -1,994 +0,0 @@
package api
//go:generate go run github.com/99designs/gqlgen
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"regexp"
"strings"
"github.com/99designs/gqlgen/graphql"
"github.com/oklog/ulid"
"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"
"github.com/dstotijn/hetty/pkg/sender"
)
var httpProtocolMap = map[string]HTTPProtocol{
sender.HTTPProto10: HTTPProtocolHTTP10,
sender.HTTPProto11: HTTPProtocolHTTP11,
sender.HTTPProto20: HTTPProtocolHTTP20,
}
var revHTTPProtocolMap = map[HTTPProtocol]string{
HTTPProtocolHTTP10: sender.HTTPProto10,
HTTPProtocolHTTP11: sender.HTTPProto11,
HTTPProtocolHTTP20: sender.HTTPProto20,
}
type Resolver struct {
ProjectService proj.Service
RequestLogService reqlog.Service
InterceptService *intercept.Service
SenderService sender.Service
}
type (
queryResolver struct{ *Resolver }
mutationResolver struct{ *Resolver }
)
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
func (r *queryResolver) HTTPRequestLogs(ctx context.Context) ([]HTTPRequestLog, error) {
reqs, err := r.RequestLogService.FindRequests(ctx)
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
} else if err != nil {
return nil, fmt.Errorf("could not query repository for requests: %w", err)
}
logs := make([]HTTPRequestLog, len(reqs))
for i, req := range reqs {
req, err := parseRequestLog(req)
if err != nil {
return nil, err
}
logs[i] = req
}
return logs, nil
}
func (r *queryResolver) HTTPRequestLog(ctx context.Context, id ulid.ULID) (*HTTPRequestLog, error) {
log, err := r.RequestLogService.FindRequestLogByID(ctx, id)
if errors.Is(err, reqlog.ErrRequestNotFound) {
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("could not get request by ID: %w", err)
}
req, err := parseRequestLog(log)
if err != nil {
return nil, err
}
return &req, nil
}
func parseRequestLog(reqLog reqlog.RequestLog) (HTTPRequestLog, error) {
method := HTTPMethod(reqLog.Method)
if method != "" && !method.IsValid() {
return HTTPRequestLog{}, fmt.Errorf("request has invalid method: %v", method)
}
log := HTTPRequestLog{
ID: reqLog.ID,
Proto: reqLog.Proto,
Method: method,
Timestamp: ulid.Time(reqLog.ID.Time()),
}
if reqLog.URL != nil {
log.URL = reqLog.URL.String()
}
if len(reqLog.Body) > 0 {
bodyStr := string(reqLog.Body)
log.Body = &bodyStr
}
if reqLog.Header != nil {
log.Headers = make([]HTTPHeader, 0)
for key, values := range reqLog.Header {
for _, value := range values {
log.Headers = append(log.Headers, HTTPHeader{
Key: key,
Value: value,
})
}
}
}
if reqLog.Response != nil {
resLog, err := parseResponseLog(*reqLog.Response)
if err != nil {
return HTTPRequestLog{}, err
}
resLog.ID = reqLog.ID
log.Response = &resLog
}
return log, nil
}
func parseResponseLog(resLog reqlog.ResponseLog) (HTTPResponseLog, error) {
proto := httpProtocolMap[resLog.Proto]
if !proto.IsValid() {
return HTTPResponseLog{}, fmt.Errorf("sender response has invalid protocol: %v", resLog.Proto)
}
httpResLog := HTTPResponseLog{
Proto: proto,
StatusCode: resLog.StatusCode,
}
statusReasonSubs := strings.SplitN(resLog.Status, " ", 2)
if len(statusReasonSubs) == 2 {
httpResLog.StatusReason = statusReasonSubs[1]
}
if len(resLog.Body) > 0 {
bodyStr := string(resLog.Body)
httpResLog.Body = &bodyStr
}
if resLog.Header != nil {
httpResLog.Headers = make([]HTTPHeader, 0)
for key, values := range resLog.Header {
for _, value := range values {
httpResLog.Headers = append(httpResLog.Headers, HTTPHeader{
Key: key,
Value: value,
})
}
}
}
return httpResLog, nil
}
func (r *mutationResolver) CreateProject(ctx context.Context, name string) (*Project, error) {
p, err := r.ProjectService.CreateProject(ctx, name)
if errors.Is(err, proj.ErrInvalidName) {
return nil, gqlerror.Errorf("Project name must only contain alphanumeric or space chars.")
} else if err != nil {
return nil, fmt.Errorf("could not open project: %w", err)
}
project := parseProject(r.ProjectService, p)
return &project, nil
}
func (r *mutationResolver) OpenProject(ctx context.Context, id ulid.ULID) (*Project, error) {
p, err := r.ProjectService.OpenProject(ctx, id)
if errors.Is(err, proj.ErrInvalidName) {
return nil, gqlerror.Errorf("Project name must only contain alphanumeric or space chars.")
} else if err != nil {
return nil, fmt.Errorf("could not open project: %w", err)
}
project := parseProject(r.ProjectService, p)
return &project, nil
}
func (r *queryResolver) ActiveProject(ctx context.Context) (*Project, error) {
p, err := r.ProjectService.ActiveProject(ctx)
if errors.Is(err, proj.ErrNoProject) {
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("could not open project: %w", err)
}
project := parseProject(r.ProjectService, p)
return &project, nil
}
func (r *queryResolver) Projects(ctx context.Context) ([]Project, error) {
p, err := r.ProjectService.Projects(ctx)
if err != nil {
return nil, fmt.Errorf("could not get projects: %w", err)
}
projects := make([]Project, len(p))
for i, proj := range p {
projects[i] = parseProject(r.ProjectService, proj)
}
return projects, nil
}
func (r *queryResolver) Scope(ctx context.Context) ([]ScopeRule, error) {
rules := r.ProjectService.Scope().Rules()
return scopeToScopeRules(rules), nil
}
func regexpToStringPtr(r *regexp.Regexp) *string {
if r == nil {
return nil
}
s := r.String()
return &s
}
func (r *mutationResolver) CloseProject(ctx context.Context) (*CloseProjectResult, error) {
if err := r.ProjectService.CloseProject(); err != nil {
return nil, fmt.Errorf("could not close project: %w", err)
}
return &CloseProjectResult{true}, nil
}
func (r *mutationResolver) DeleteProject(ctx context.Context, id ulid.ULID) (*DeleteProjectResult, error) {
if err := r.ProjectService.DeleteProject(ctx, id); err != nil {
return nil, fmt.Errorf("could not delete project: %w", err)
}
return &DeleteProjectResult{
Success: true,
}, nil
}
func (r *mutationResolver) ClearHTTPRequestLog(ctx context.Context) (*ClearHTTPRequestLogResult, error) {
project, err := r.ProjectService.ActiveProject(ctx)
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
} else if err != nil {
return nil, fmt.Errorf("could not get active project: %w", err)
}
if err := r.RequestLogService.ClearRequests(ctx, project.ID); err != nil {
return nil, fmt.Errorf("could not clear request log: %w", err)
}
return &ClearHTTPRequestLogResult{true}, nil
}
func (r *mutationResolver) SetScope(ctx context.Context, input []ScopeRuleInput) ([]ScopeRule, error) {
rules := make([]scope.Rule, len(input))
for i, rule := range input {
u, err := stringPtrToRegexp(rule.URL)
if err != nil {
return nil, fmt.Errorf("invalid URL in scope rule: %w", err)
}
var headerKey, headerValue *regexp.Regexp
if rule.Header != nil {
headerKey, err = stringPtrToRegexp(rule.Header.Key)
if err != nil {
return nil, fmt.Errorf("invalid header key in scope rule: %w", err)
}
headerValue, err = stringPtrToRegexp(rule.Header.Key)
if err != nil {
return nil, fmt.Errorf("invalid header value in scope rule: %w", err)
}
}
body, err := stringPtrToRegexp(rule.Body)
if err != nil {
return nil, fmt.Errorf("invalid body in scope rule: %w", err)
}
rules[i] = scope.Rule{
URL: u,
Header: scope.Header{
Key: headerKey,
Value: headerValue,
},
Body: body,
}
}
err := r.ProjectService.SetScopeRules(ctx, rules)
if err != nil {
return nil, fmt.Errorf("could not set scope rules: %w", err)
}
return scopeToScopeRules(rules), nil
}
func (r *queryResolver) HTTPRequestLogFilter(ctx context.Context) (*HTTPRequestLogFilter, error) {
return findReqFilterToHTTPReqLogFilter(r.RequestLogService.FindReqsFilter()), nil
}
func (r *mutationResolver) SetHTTPRequestLogFilter(
ctx context.Context,
input *HTTPRequestLogFilterInput,
) (*HTTPRequestLogFilter, error) {
filter, err := findRequestsFilterFromInput(input)
if err != nil {
return nil, fmt.Errorf("could not parse request log filter: %w", err)
}
err = r.ProjectService.SetRequestLogFindFilter(ctx, filter)
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
} else if err != nil {
return nil, fmt.Errorf("could not set request log filter: %w", err)
}
return findReqFilterToHTTPReqLogFilter(filter), nil
}
func (r *queryResolver) SenderRequest(ctx context.Context, id ulid.ULID) (*SenderRequest, error) {
senderReq, err := r.SenderService.FindRequestByID(ctx, id)
if errors.Is(err, sender.ErrRequestNotFound) {
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("could not get request by ID: %w", err)
}
req, err := parseSenderRequest(senderReq)
if err != nil {
return nil, err
}
return &req, nil
}
func (r *queryResolver) SenderRequests(ctx context.Context) ([]SenderRequest, error) {
reqs, err := r.SenderService.FindRequests(ctx)
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
} else if err != nil {
return nil, fmt.Errorf("failed to find sender requests: %w", err)
}
senderReqs := make([]SenderRequest, len(reqs))
for i, req := range reqs {
req, err := parseSenderRequest(req)
if err != nil {
return nil, err
}
senderReqs[i] = req
}
return senderReqs, nil
}
func (r *mutationResolver) SetSenderRequestFilter(
ctx context.Context,
input *SenderRequestFilterInput,
) (*SenderRequestFilter, error) {
filter, err := findSenderRequestsFilterFromInput(input)
if err != nil {
return nil, fmt.Errorf("could not parse request log filter: %w", err)
}
err = r.ProjectService.SetSenderRequestFindFilter(ctx, filter)
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
} else if err != nil {
return nil, fmt.Errorf("could not set request log filter: %w", err)
}
return findReqFilterToSenderReqFilter(filter), nil
}
func (r *mutationResolver) CreateOrUpdateSenderRequest(
ctx context.Context,
input SenderRequestInput,
) (*SenderRequest, error) {
req := sender.Request{
URL: input.URL,
Header: make(http.Header),
}
if input.ID != nil {
req.ID = *input.ID
}
if input.Method != nil {
req.Method = input.Method.String()
}
if input.Proto != nil {
req.Proto = revHTTPProtocolMap[*input.Proto]
}
for _, header := range input.Headers {
req.Header.Add(header.Key, header.Value)
}
if input.Body != nil {
req.Body = []byte(*input.Body)
}
req, err := r.SenderService.CreateOrUpdateRequest(ctx, req)
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
} else if err != nil {
return nil, fmt.Errorf("could not create sender request: %w", err)
}
senderReq, err := parseSenderRequest(req)
if err != nil {
return nil, err
}
return &senderReq, nil
}
func (r *mutationResolver) CreateSenderRequestFromHTTPRequestLog(
ctx context.Context,
id ulid.ULID,
) (*SenderRequest, error) {
req, err := r.SenderService.CloneFromRequestLog(ctx, id)
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
} else if err != nil {
return nil, fmt.Errorf("could not create sender request from http request log: %w", err)
}
senderReq, err := parseSenderRequest(req)
if err != nil {
return nil, err
}
return &senderReq, nil
}
func (r *mutationResolver) SendRequest(ctx context.Context, id ulid.ULID) (*SenderRequest, error) {
// Use new context, because we don't want to risk interrupting sending the request
// or the subsequent storing of the response, e.g. if ctx gets cancelled or
// times out.
ctx2 := context.Background()
var sendErr *sender.SendError
//nolint:contextcheck
req, err := r.SenderService.SendRequest(ctx2, id)
switch {
case errors.Is(err, proj.ErrNoProject):
return nil, noActiveProjectErr(ctx)
case errors.As(err, &sendErr):
return nil, &gqlerror.Error{
Path: graphql.GetPath(ctx),
Message: fmt.Sprintf("Sending request failed: %v", sendErr.Unwrap()),
Extensions: map[string]interface{}{
"code": "send_request_failed",
},
}
case err != nil:
return nil, fmt.Errorf("could not send request: %w", err)
}
senderReq, err := parseSenderRequest(req)
if err != nil {
return nil, err
}
return &senderReq, nil
}
func (r *mutationResolver) DeleteSenderRequests(ctx context.Context) (*DeleteSenderRequestsResult, error) {
project, err := r.ProjectService.ActiveProject(ctx)
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
} else if err != nil {
return nil, fmt.Errorf("could not get active project: %w", err)
}
if err := r.SenderService.DeleteRequests(ctx, project.ID); err != nil {
return nil, fmt.Errorf("could not clear request log: %w", err)
}
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() {
return SenderRequest{}, fmt.Errorf("sender request has invalid method: %v", method)
}
reqProto := httpProtocolMap[req.Proto]
if !reqProto.IsValid() {
return SenderRequest{}, fmt.Errorf("sender request has invalid protocol: %v", req.Proto)
}
senderReq := SenderRequest{
ID: req.ID,
URL: req.URL,
Method: method,
Proto: HTTPProtocol(req.Proto),
Timestamp: ulid.Time(req.ID.Time()),
}
if req.SourceRequestLogID.Compare(ulid.ULID{}) != 0 {
senderReq.SourceRequestLogID = &req.SourceRequestLogID
}
if req.Header != nil {
senderReq.Headers = make([]HTTPHeader, 0)
for key, values := range req.Header {
for _, value := range values {
senderReq.Headers = append(senderReq.Headers, HTTPHeader{
Key: key,
Value: value,
})
}
}
}
if len(req.Body) > 0 {
bodyStr := string(req.Body)
senderReq.Body = &bodyStr
}
if req.Response != nil {
resLog, err := parseResponseLog(*req.Response)
if err != nil {
return SenderRequest{}, err
}
resLog.ID = req.ID
senderReq.Response = &resLog
}
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
}
return regexp.Compile(*s)
}
func scopeToScopeRules(rules []scope.Rule) []ScopeRule {
scopeRules := make([]ScopeRule, len(rules))
for i, rule := range rules {
scopeRules[i].URL = regexpToStringPtr(rule.URL)
if rule.Header.Key != nil || rule.Header.Value != nil {
scopeRules[i].Header = &ScopeHeader{
Key: regexpToStringPtr(rule.Header.Key),
Value: regexpToStringPtr(rule.Header.Value),
}
}
scopeRules[i].Body = regexpToStringPtr(rule.Body)
}
return scopeRules
}
func findRequestsFilterFromInput(input *HTTPRequestLogFilterInput) (filter reqlog.FindRequestsFilter, err error) {
if input == nil {
return
}
if input.OnlyInScope != nil {
filter.OnlyInScope = *input.OnlyInScope
}
if input.SearchExpression != nil && *input.SearchExpression != "" {
expr, err := search.ParseQuery(*input.SearchExpression)
if err != nil {
return reqlog.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %w", err)
}
filter.SearchExpr = expr
}
return
}
func findSenderRequestsFilterFromInput(input *SenderRequestFilterInput) (filter sender.FindRequestsFilter, err error) {
if input == nil {
return
}
if input.OnlyInScope != nil {
filter.OnlyInScope = *input.OnlyInScope
}
if input.SearchExpression != nil && *input.SearchExpression != "" {
expr, err := search.ParseQuery(*input.SearchExpression)
if err != nil {
return sender.FindRequestsFilter{}, fmt.Errorf("could not parse search query: %w", err)
}
filter.SearchExpr = expr
}
return
}
func findReqFilterToHTTPReqLogFilter(findReqFilter reqlog.FindRequestsFilter) *HTTPRequestLogFilter {
empty := reqlog.FindRequestsFilter{}
if findReqFilter == empty {
return nil
}
httpReqLogFilter := &HTTPRequestLogFilter{
OnlyInScope: findReqFilter.OnlyInScope,
}
if findReqFilter.SearchExpr != nil {
searchExpr := findReqFilter.SearchExpr.String()
httpReqLogFilter.SearchExpression = &searchExpr
}
return httpReqLogFilter
}
func findReqFilterToSenderReqFilter(findReqFilter sender.FindRequestsFilter) *SenderRequestFilter {
empty := sender.FindRequestsFilter{}
if findReqFilter == empty {
return nil
}
senderReqFilter := &SenderRequestFilter{
OnlyInScope: findReqFilter.OnlyInScope,
}
if findReqFilter.SearchExpr != nil {
searchExpr := findReqFilter.SearchExpr.String()
senderReqFilter.SearchExpression = &searchExpr
}
return senderReqFilter
}
func noActiveProjectErr(ctx context.Context) error {
return &gqlerror.Error{
Path: graphql.GetPath(ctx),
Message: "No active project.",
Extensions: map[string]interface{}{
"code": "no_active_project",
},
}
}

View File

@ -1,57 +0,0 @@
package badger
import (
"fmt"
"github.com/dgraph-io/badger/v3"
)
const (
// Key prefixes. Each prefix value should be unique.
projectPrefix = 0x00
reqLogPrefix = 0x01
resLogPrefix = 0x02
senderReqPrefix = 0x03
// Request log indices.
reqLogProjectIDIndex = 0x00
// Sender request indices.
senderReqProjectIDIndex = 0x00
)
// Database is used to store and retrieve data from an underlying Badger database.
type Database struct {
badger *badger.DB
}
// OpenDatabase opens a new Badger database.
func OpenDatabase(opts badger.Options) (*Database, error) {
db, err := badger.Open(opts)
if err != nil {
return nil, fmt.Errorf("badger: failed to open database: %w", err)
}
return &Database{badger: db}, nil
}
// Close closes the underlying Badger database.
func (db *Database) Close() error {
return db.badger.Close()
}
// DatabaseFromBadgerDB returns a Database with `db` set as the underlying
// Badger database.
func DatabaseFromBadgerDB(db *badger.DB) *Database {
return &Database{badger: db}
}
func entryKey(prefix, index byte, value []byte) []byte {
// Key consists of: | prefix (byte) | index (byte) | value
key := make([]byte, 2+len(value))
key[0] = prefix
key[1] = index
copy(key[2:len(value)+2], value)
return key
}

View File

@ -1,21 +0,0 @@
package badger
import (
"github.com/dgraph-io/badger/v3"
"go.uber.org/zap"
)
// Interface guard.
var _ badger.Logger = (*Logger)(nil)
type Logger struct {
*zap.SugaredLogger
}
func NewLogger(l *zap.SugaredLogger) *Logger {
return &Logger{l}
}
func (l *Logger) Warningf(template string, args ...interface{}) {
l.Warnf(template, args)
}

View File

@ -1,115 +0,0 @@
package badger
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"github.com/dgraph-io/badger/v3"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/proj"
)
func (db *Database) UpsertProject(ctx context.Context, project proj.Project) error {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(project)
if err != nil {
return fmt.Errorf("badger: failed to encode project: %w", err)
}
err = db.badger.Update(func(txn *badger.Txn) error {
return txn.Set(entryKey(projectPrefix, 0, project.ID[:]), buf.Bytes())
})
if err != nil {
return fmt.Errorf("badger: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) FindProjectByID(ctx context.Context, projectID ulid.ULID) (project proj.Project, err error) {
err = db.badger.View(func(txn *badger.Txn) error {
item, err := txn.Get(entryKey(projectPrefix, 0, projectID[:]))
if err != nil {
return err
}
err = item.Value(func(rawProject []byte) error {
return gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&project)
})
if err != nil {
return fmt.Errorf("failed to retrieve or parse project: %w", err)
}
return nil
})
if errors.Is(err, badger.ErrKeyNotFound) {
return proj.Project{}, proj.ErrProjectNotFound
}
if err != nil {
return proj.Project{}, fmt.Errorf("badger: failed to commit transaction: %w", err)
}
return project, nil
}
func (db *Database) DeleteProject(ctx context.Context, projectID ulid.ULID) error {
err := db.ClearRequestLogs(ctx, projectID)
if err != nil {
return fmt.Errorf("badger: failed to delete project request logs: %w", err)
}
err = db.DeleteSenderRequests(ctx, projectID)
if err != nil {
return fmt.Errorf("badger: failed to delete project sender requests: %w", err)
}
err = db.badger.Update(func(txn *badger.Txn) error {
return txn.Delete(entryKey(projectPrefix, 0, projectID[:]))
})
if err != nil {
return fmt.Errorf("badger: failed to delete project item: %w", err)
}
return nil
}
func (db *Database) Projects(ctx context.Context) ([]proj.Project, error) {
projects := make([]proj.Project, 0)
err := db.badger.View(func(txn *badger.Txn) error {
var rawProject []byte
prefix := entryKey(projectPrefix, 0, nil)
iterator := txn.NewIterator(badger.DefaultIteratorOptions)
defer iterator.Close()
for iterator.Seek(prefix); iterator.ValidForPrefix(prefix); iterator.Next() {
rawProject, err := iterator.Item().ValueCopy(rawProject)
if err != nil {
return fmt.Errorf("failed to copy value: %w", err)
}
var project proj.Project
err = gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&project)
if err != nil {
return fmt.Errorf("failed to decode project: %w", err)
}
projects = append(projects, project)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("badger: failed to commit transaction: %w", err)
}
return projects, nil
}

View File

@ -1,328 +0,0 @@
package badger
import (
"bytes"
"context"
"encoding/gob"
"errors"
"math/rand"
"regexp"
"testing"
"time"
badgerdb "github.com/dgraph-io/badger/v3"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
var regexpCompareOpt = cmp.Comparer(func(x, y *regexp.Regexp) bool {
switch {
case x == nil && y == nil:
return true
case x == nil || y == nil:
return false
default:
return x.String() == y.String()
}
})
func TestUpsertProject(t *testing.T) {
t.Parallel()
badgerDB, err := badgerdb.Open(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
database := DatabaseFromBadgerDB(badgerDB)
defer database.Close()
searchExpr, err := search.ParseQuery("foo AND bar OR NOT baz")
if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
exp := proj.Project{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
Name: "foobar",
Settings: proj.Settings{
ReqLogBypassOutOfScope: true,
ReqLogOnlyFindInScope: true,
ReqLogSearchExpr: searchExpr,
ScopeRules: []scope.Rule{
{
URL: regexp.MustCompile("^https://(.*)example.com(.*)$"),
Header: scope.Header{
Key: regexp.MustCompile("^X-Foo(.*)$"),
Value: regexp.MustCompile("^foo(.*)$"),
},
Body: regexp.MustCompile("^foo(.*)"),
},
},
},
}
err = database.UpsertProject(context.Background(), exp)
if err != nil {
t.Fatalf("unexpected error storing project: %v", err)
}
var rawProject []byte
err = badgerDB.View(func(txn *badgerdb.Txn) error {
item, err := txn.Get(entryKey(projectPrefix, 0, exp.ID[:]))
if err != nil {
return err
}
rawProject, err = item.ValueCopy(nil)
return err
})
if err != nil {
t.Fatalf("unexpected error retrieving project from database: %v", err)
}
got := proj.Project{}
err = gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&got)
if err != nil {
t.Fatalf("unexpected error decoding project: %v", err)
}
if diff := cmp.Diff(exp, got, regexpCompareOpt, cmpopts.IgnoreUnexported(proj.Project{})); diff != "" {
t.Fatalf("project not equal (-exp, +got):\n%v", diff)
}
}
func TestFindProjectByID(t *testing.T) {
t.Parallel()
t.Run("existing project", func(t *testing.T) {
t.Parallel()
badgerDB, err := badgerdb.Open(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
database := DatabaseFromBadgerDB(badgerDB)
defer database.Close()
exp := proj.Project{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
Name: "foobar",
Settings: proj.Settings{},
}
buf := bytes.Buffer{}
err = gob.NewEncoder(&buf).Encode(exp)
if err != nil {
t.Fatalf("unexpected error encoding project: %v", err)
}
err = badgerDB.Update(func(txn *badgerdb.Txn) error {
return txn.Set(entryKey(projectPrefix, 0, exp.ID[:]), buf.Bytes())
})
if err != nil {
t.Fatalf("unexpected error setting project: %v", err)
}
got, err := database.FindProjectByID(context.Background(), exp.ID)
if err != nil {
t.Fatalf("unexpected error finding project: %v", err)
}
if diff := cmp.Diff(exp, got, cmpopts.IgnoreUnexported(proj.Project{})); diff != "" {
t.Fatalf("project not equal (-exp, +got):\n%v", diff)
}
})
t.Run("project not found", func(t *testing.T) {
t.Parallel()
database, err := OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
_, err = database.FindProjectByID(context.Background(), projectID)
if !errors.Is(err, proj.ErrProjectNotFound) {
t.Fatalf("expected `proj.ErrProjectNotFound`, got: %v", err)
}
})
}
func TestDeleteProject(t *testing.T) {
t.Parallel()
badgerDB, err := badgerdb.Open(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
database := DatabaseFromBadgerDB(badgerDB)
defer database.Close()
// Store fixtures.
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
senderReqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
err = badgerDB.Update(func(txn *badgerdb.Txn) error {
// Project item.
if err := txn.Set(entryKey(projectPrefix, 0, projectID[:]), nil); err != nil {
return err
}
// Sender request items.
if err := txn.Set(entryKey(senderReqPrefix, 0, senderReqID[:]), nil); err != nil {
return err
}
if err := txn.Set(entryKey(resLogPrefix, 0, senderReqID[:]), nil); err != nil {
return err
}
err := txn.Set(entryKey(senderReqPrefix, senderReqProjectIDIndex, append(projectID[:], senderReqID[:]...)), nil)
if err != nil {
return err
}
// Request log items.
if err := txn.Set(entryKey(reqLogPrefix, 0, reqLogID[:]), nil); err != nil {
return err
}
if err := txn.Set(entryKey(resLogPrefix, 0, reqLogID[:]), nil); err != nil {
return err
}
err = txn.Set(entryKey(reqLogPrefix, reqLogProjectIDIndex, append(projectID[:], reqLogID[:]...)), nil)
if err != nil {
return err
}
return nil
})
if err != nil {
t.Fatalf("unexpected error creating fixtures: %v", err)
}
err = database.DeleteProject(context.Background(), projectID)
if err != nil {
t.Fatalf("unexpected error deleting project: %v", err)
}
// Assert project key was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(projectPrefix, 0, projectID[:]))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
// Assert request log item was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(reqLogPrefix, 0, reqLogID[:]))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
// Assert response log item related to request log was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(resLogPrefix, 0, reqLogID[:]))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
// Assert request log project ID index key was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(reqLogPrefix, reqLogProjectIDIndex, append(projectID[:], reqLogID[:]...)))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
// Assert sender request item was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(senderReqPrefix, 0, senderReqID[:]))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
// Assert response log item related to sender request was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(resLogPrefix, 0, senderReqID[:]))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
// Assert sender request project ID index key was deleted.
err = badgerDB.View(func(txn *badgerdb.Txn) error {
_, err := txn.Get(entryKey(senderReqPrefix, senderReqProjectIDIndex, append(projectID[:], senderReqID[:]...)))
return err
})
if !errors.Is(err, badgerdb.ErrKeyNotFound) {
t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err)
}
}
func TestProjects(t *testing.T) {
t.Parallel()
database, err := OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
exp := []proj.Project{
{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
Name: "one",
},
{
ID: ulid.MustNew(ulid.Timestamp(time.Now())+100, ulidEntropy),
Name: "two",
},
}
// Store fixtures.
for _, project := range exp {
err = database.UpsertProject(context.Background(), project)
if err != nil {
t.Fatalf("unexpected error creating project fixture: %v", err)
}
}
got, err := database.Projects(context.Background())
if err != nil {
t.Fatalf("unexpected error finding projects: %v", err)
}
if len(exp) != len(got) {
t.Fatalf("expected %v projects, got: %v", len(exp), len(got))
}
if diff := cmp.Diff(exp, got, cmpopts.IgnoreUnexported(proj.Project{})); diff != "" {
t.Fatalf("projects not equal (-exp, +got):\n%v", diff)
}
}

View File

@ -1,260 +0,0 @@
package badger
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"github.com/dgraph-io/badger/v3"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
)
func (db *Database) FindRequestLogs(
ctx context.Context,
filter reqlog.FindRequestsFilter,
scope *scope.Scope) ([]reqlog.RequestLog, error,
) {
if filter.ProjectID.Compare(ulid.ULID{}) == 0 {
return nil, reqlog.ErrProjectIDMustBeSet
}
txn := db.badger.NewTransaction(false)
defer txn.Discard()
reqLogIDs, err := findRequestLogIDsByProjectID(txn, filter.ProjectID)
if err != nil {
return nil, fmt.Errorf("badger: failed to find request log IDs: %w", err)
}
reqLogs := make([]reqlog.RequestLog, 0, len(reqLogIDs))
for _, reqLogID := range reqLogIDs {
reqLog, err := getRequestLogWithResponse(txn, reqLogID)
if err != nil {
return nil, fmt.Errorf("badger: failed to get request log (id: %v): %w", reqLogID.String(), err)
}
if filter.OnlyInScope {
if !reqLog.MatchScope(scope) {
continue
}
}
// Filter by search expression.
// TODO: Once pagination is introduced, this filter logic should be done
// as items are retrieved (e.g. when using a `badger.Iterator`).
if filter.SearchExpr != nil {
match, err := reqLog.Matches(filter.SearchExpr)
if err != nil {
return nil, fmt.Errorf(
"badger: failed to match search expression for request log (id: %v): %w",
reqLogID.String(), err,
)
}
if !match {
continue
}
}
reqLogs = append(reqLogs, reqLog)
}
return reqLogs, nil
}
func getRequestLogWithResponse(txn *badger.Txn, reqLogID ulid.ULID) (reqlog.RequestLog, error) {
item, err := txn.Get(entryKey(reqLogPrefix, 0, reqLogID[:]))
switch {
case errors.Is(err, badger.ErrKeyNotFound):
return reqlog.RequestLog{}, reqlog.ErrRequestNotFound
case err != nil:
return reqlog.RequestLog{}, fmt.Errorf("failed to lookup request log item: %w", err)
}
reqLog := reqlog.RequestLog{
ID: reqLogID,
}
err = item.Value(func(rawReqLog []byte) error {
err = gob.NewDecoder(bytes.NewReader(rawReqLog)).Decode(&reqLog)
if err != nil {
return fmt.Errorf("failed to decode request log: %w", err)
}
return nil
})
if err != nil {
return reqlog.RequestLog{}, fmt.Errorf("failed to retrieve or parse request log value: %w", err)
}
item, err = txn.Get(entryKey(resLogPrefix, 0, reqLogID[:]))
if errors.Is(err, badger.ErrKeyNotFound) {
return reqLog, nil
}
if err != nil {
return reqlog.RequestLog{}, fmt.Errorf("failed to get response log: %w", err)
}
err = item.Value(func(rawReslog []byte) error {
var resLog reqlog.ResponseLog
err = gob.NewDecoder(bytes.NewReader(rawReslog)).Decode(&resLog)
if err != nil {
return fmt.Errorf("failed to decode response log: %w", err)
}
reqLog.Response = &resLog
return nil
})
if err != nil {
return reqlog.RequestLog{}, fmt.Errorf("failed to retrieve or parse response log value: %w", err)
}
return reqLog, nil
}
func (db *Database) FindRequestLogByID(ctx context.Context, reqLogID ulid.ULID) (reqLog reqlog.RequestLog, err error) {
txn := db.badger.NewTransaction(false)
defer txn.Discard()
reqLog, err = getRequestLogWithResponse(txn, reqLogID)
if err != nil {
return reqlog.RequestLog{}, fmt.Errorf("badger: failed to get request log: %w", err)
}
return reqLog, nil
}
func (db *Database) StoreRequestLog(ctx context.Context, reqLog reqlog.RequestLog) error {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(reqLog)
if err != nil {
return fmt.Errorf("badger: failed to encode request log: %w", err)
}
entries := []*badger.Entry{
// Request log itself.
{
Key: entryKey(reqLogPrefix, 0, reqLog.ID[:]),
Value: buf.Bytes(),
},
// Index by project ID.
{
Key: entryKey(reqLogPrefix, reqLogProjectIDIndex, append(reqLog.ProjectID[:], reqLog.ID[:]...)),
},
}
err = db.badger.Update(func(txn *badger.Txn) error {
for i := range entries {
err := txn.SetEntry(entries[i])
if err != nil {
return err
}
}
return nil
})
if err != nil {
return fmt.Errorf("badger: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(resLog)
if err != nil {
return fmt.Errorf("badger: failed to encode response log: %w", err)
}
err = db.badger.Update(func(txn *badger.Txn) error {
return txn.SetEntry(&badger.Entry{
Key: entryKey(resLogPrefix, 0, reqLogID[:]),
Value: buf.Bytes(),
})
})
if err != nil {
return fmt.Errorf("badger: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) ClearRequestLogs(ctx context.Context, projectID ulid.ULID) error {
// Note: this transaction is used just for reading; we use the `badger.WriteBatch`
// API to bulk delete items.
txn := db.badger.NewTransaction(false)
defer txn.Discard()
reqLogIDs, err := findRequestLogIDsByProjectID(txn, projectID)
if err != nil {
return fmt.Errorf("badger: failed to find request log IDs: %w", err)
}
writeBatch := db.badger.NewWriteBatch()
defer writeBatch.Cancel()
for _, reqLogID := range reqLogIDs {
// Delete request logs.
err := writeBatch.Delete(entryKey(reqLogPrefix, 0, reqLogID[:]))
if err != nil {
return fmt.Errorf("badger: failed to delete request log: %w", err)
}
// Delete related response log.
err = writeBatch.Delete(entryKey(resLogPrefix, 0, reqLogID[:]))
if err != nil {
return fmt.Errorf("badger: failed to delete request log: %w", err)
}
}
if err := writeBatch.Flush(); err != nil {
return fmt.Errorf("badger: failed to commit batch write: %w", err)
}
err = db.badger.DropPrefix(entryKey(reqLogPrefix, reqLogProjectIDIndex, projectID[:]))
if err != nil {
return fmt.Errorf("badger: failed to drop request log project ID index items: %w", err)
}
return nil
}
func findRequestLogIDsByProjectID(txn *badger.Txn, projectID ulid.ULID) ([]ulid.ULID, error) {
reqLogIDs := make([]ulid.ULID, 0)
opts := badger.DefaultIteratorOptions
opts.PrefetchValues = false
opts.Reverse = true
iterator := txn.NewIterator(opts)
defer iterator.Close()
var projectIndexKey []byte
prefix := entryKey(reqLogPrefix, reqLogProjectIDIndex, projectID[:])
for iterator.Seek(append(prefix, 255)); iterator.ValidForPrefix(prefix); iterator.Next() {
projectIndexKey = iterator.Item().KeyCopy(projectIndexKey)
var id ulid.ULID
// The request log ID starts *after* the first 2 prefix and index bytes
// and the 16 byte project ID.
if err := id.UnmarshalBinary(projectIndexKey[18:]); err != nil {
return nil, fmt.Errorf("failed to parse request log ID: %w", err)
}
reqLogIDs = append(reqLogIDs, id)
}
return reqLogIDs, nil
}

View File

@ -1,127 +0,0 @@
package badger
import (
"context"
"errors"
"net/http"
"net/url"
"testing"
"time"
badgerdb "github.com/dgraph-io/badger/v3"
"github.com/google/go-cmp/cmp"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/reqlog"
)
func TestFindRequestLogs(t *testing.T) {
t.Parallel()
t.Run("without project ID in filter", func(t *testing.T) {
t.Parallel()
database, err := OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
filter := reqlog.FindRequestsFilter{}
_, err = database.FindRequestLogs(context.Background(), filter, nil)
if !errors.Is(err, reqlog.ErrProjectIDMustBeSet) {
t.Fatalf("expected `reqlog.ErrProjectIDMustBeSet`, got: %v", err)
}
})
t.Run("returns request logs and related response logs", func(t *testing.T) {
t.Parallel()
database, err := OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
fixtures := []reqlog.RequestLog{
{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: projectID,
URL: mustParseURL(t, "https://example.com/foobar"),
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"baz"},
},
Body: []byte("foo"),
Response: &reqlog.ResponseLog{
Proto: "HTTP/1.1",
Status: "200 OK",
StatusCode: 200,
Header: http.Header{
"X-Yolo": []string{"swag"},
},
Body: []byte("bar"),
},
},
{
ID: ulid.MustNew(ulid.Timestamp(time.Now())+100, ulidEntropy),
ProjectID: projectID,
URL: mustParseURL(t, "https://example.com/foo?bar=baz"),
Method: http.MethodGet,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"baz"},
},
},
}
// Store fixtures.
for _, reqLog := range fixtures {
err = database.StoreRequestLog(context.Background(), reqLog)
if err != nil {
t.Fatalf("unexpected error creating request log fixture: %v", err)
}
if reqLog.Response != nil {
err = database.StoreResponseLog(context.Background(), reqLog.ID, *reqLog.Response)
if err != nil {
t.Fatalf("unexpected error creating response log fixture: %v", err)
}
}
}
filter := reqlog.FindRequestsFilter{
ProjectID: projectID,
}
got, err := database.FindRequestLogs(context.Background(), filter, nil)
if err != nil {
t.Fatalf("unexpected error finding request logs: %v", err)
}
// We expect the found request logs are *reversed*, e.g. newest first.
exp := make([]reqlog.RequestLog, len(fixtures))
for i, j := 0, len(fixtures)-1; i < j; i, j = i+1, j-1 {
exp[i], exp[j] = fixtures[j], fixtures[i]
}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("request logs not equal (-exp, +got):\n%v", diff)
}
})
}
func mustParseURL(t *testing.T, s string) *url.URL {
t.Helper()
u, err := url.Parse(s)
if err != nil {
panic(err)
}
return u
}

View File

@ -1,240 +0,0 @@
package badger
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"github.com/dgraph-io/badger/v3"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/sender"
)
func (db *Database) StoreSenderRequest(ctx context.Context, req sender.Request) error {
buf := bytes.Buffer{}
err := gob.NewEncoder(&buf).Encode(req)
if err != nil {
return fmt.Errorf("badger: failed to encode sender request: %w", err)
}
entries := []*badger.Entry{
// Sender request itself.
{
Key: entryKey(senderReqPrefix, 0, req.ID[:]),
Value: buf.Bytes(),
},
// Index by project ID.
{
Key: entryKey(senderReqPrefix, senderReqProjectIDIndex, append(req.ProjectID[:], req.ID[:]...)),
},
}
err = db.badger.Update(func(txn *badger.Txn) error {
for i := range entries {
err := txn.SetEntry(entries[i])
if err != nil {
return err
}
}
return nil
})
if err != nil {
return fmt.Errorf("badger: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) FindSenderRequestByID(ctx context.Context, senderReqID ulid.ULID) (sender.Request, error) {
txn := db.badger.NewTransaction(false)
defer txn.Discard()
req, err := getSenderRequestWithResponseLog(txn, senderReqID)
if err != nil {
return sender.Request{}, fmt.Errorf("badger: failed to get sender request: %w", err)
}
return req, nil
}
func (db *Database) FindSenderRequests(
ctx context.Context,
filter sender.FindRequestsFilter,
scope *scope.Scope,
) ([]sender.Request, error) {
if filter.ProjectID.Compare(ulid.ULID{}) == 0 {
return nil, sender.ErrProjectIDMustBeSet
}
txn := db.badger.NewTransaction(false)
defer txn.Discard()
senderReqIDs, err := findSenderRequestIDsByProjectID(txn, filter.ProjectID)
if err != nil {
return nil, fmt.Errorf("badger: failed to find sender request IDs: %w", err)
}
senderReqs := make([]sender.Request, 0, len(senderReqIDs))
for _, id := range senderReqIDs {
senderReq, err := getSenderRequestWithResponseLog(txn, id)
if err != nil {
return nil, fmt.Errorf("badger: failed to get sender request (id: %v): %w", id.String(), err)
}
if filter.OnlyInScope {
if !senderReq.MatchScope(scope) {
continue
}
}
// Filter by search expression.
// TODO: Once pagination is introduced, this filter logic should be done
// as items are retrieved (e.g. when using a `badger.Iterator`).
if filter.SearchExpr != nil {
match, err := senderReq.Matches(filter.SearchExpr)
if err != nil {
return nil, fmt.Errorf(
"badger: failed to match search expression for sender request (id: %v): %w",
id.String(), err,
)
}
if !match {
continue
}
}
senderReqs = append(senderReqs, senderReq)
}
return senderReqs, nil
}
func (db *Database) DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error {
// Note: this transaction is used just for reading; we use the `badger.WriteBatch`
// API to bulk delete items.
txn := db.badger.NewTransaction(false)
defer txn.Discard()
senderReqIDs, err := findSenderRequestIDsByProjectID(txn, projectID)
if err != nil {
return fmt.Errorf("badger: failed to find sender request IDs: %w", err)
}
writeBatch := db.badger.NewWriteBatch()
defer writeBatch.Cancel()
for _, senderReqID := range senderReqIDs {
// Delete sender requests.
err := writeBatch.Delete(entryKey(senderReqPrefix, 0, senderReqID[:]))
if err != nil {
return fmt.Errorf("badger: failed to delete sender requests: %w", err)
}
// Delete related response log.
err = writeBatch.Delete(entryKey(resLogPrefix, 0, senderReqID[:]))
if err != nil {
return fmt.Errorf("badger: failed to delete request log: %w", err)
}
}
if err := writeBatch.Flush(); err != nil {
return fmt.Errorf("badger: failed to commit batch write: %w", err)
}
err = db.badger.DropPrefix(entryKey(senderReqPrefix, senderReqProjectIDIndex, projectID[:]))
if err != nil {
return fmt.Errorf("badger: failed to drop sender request project ID index items: %w", err)
}
return nil
}
func getSenderRequestWithResponseLog(txn *badger.Txn, senderReqID ulid.ULID) (sender.Request, error) {
item, err := txn.Get(entryKey(senderReqPrefix, 0, senderReqID[:]))
switch {
case errors.Is(err, badger.ErrKeyNotFound):
return sender.Request{}, sender.ErrRequestNotFound
case err != nil:
return sender.Request{}, fmt.Errorf("failed to lookup sender request item: %w", err)
}
req := sender.Request{
ID: senderReqID,
}
err = item.Value(func(rawSenderReq []byte) error {
err = gob.NewDecoder(bytes.NewReader(rawSenderReq)).Decode(&req)
if err != nil {
return fmt.Errorf("failed to decode sender request: %w", err)
}
return nil
})
if err != nil {
return sender.Request{}, fmt.Errorf("failed to retrieve or parse sender request value: %w", err)
}
item, err = txn.Get(entryKey(resLogPrefix, 0, senderReqID[:]))
if errors.Is(err, badger.ErrKeyNotFound) {
return req, nil
}
if err != nil {
return sender.Request{}, fmt.Errorf("failed to get response log: %w", err)
}
err = item.Value(func(rawReslog []byte) error {
var resLog reqlog.ResponseLog
err = gob.NewDecoder(bytes.NewReader(rawReslog)).Decode(&resLog)
if err != nil {
return fmt.Errorf("failed to decode response log: %w", err)
}
req.Response = &resLog
return nil
})
if err != nil {
return sender.Request{}, fmt.Errorf("failed to retrieve or parse response log value: %w", err)
}
return req, nil
}
func findSenderRequestIDsByProjectID(txn *badger.Txn, projectID ulid.ULID) ([]ulid.ULID, error) {
senderReqIDs := make([]ulid.ULID, 0)
opts := badger.DefaultIteratorOptions
opts.PrefetchValues = false
opts.Reverse = true
iterator := txn.NewIterator(opts)
defer iterator.Close()
var projectIndexKey []byte
prefix := entryKey(senderReqPrefix, senderReqProjectIDIndex, projectID[:])
for iterator.Seek(append(prefix, 255)); iterator.ValidForPrefix(prefix); iterator.Next() {
projectIndexKey = iterator.Item().KeyCopy(projectIndexKey)
var id ulid.ULID
// The request log ID starts *after* the first 2 prefix and index bytes
// and the 16 byte project ID.
if err := id.UnmarshalBinary(projectIndexKey[18:]); err != nil {
return nil, fmt.Errorf("failed to parse sender request ID: %w", err)
}
senderReqIDs = append(senderReqIDs, id)
}
return senderReqIDs, nil
}

View File

@ -1,204 +0,0 @@
package badger_test
import (
"context"
"errors"
"math/rand"
"net/http"
"net/url"
"testing"
"time"
badgerdb "github.com/dgraph-io/badger/v3"
"github.com/google/go-cmp/cmp"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/db/badger"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/sender"
)
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
var exampleURL = func() *url.URL {
u, err := url.Parse("https://example.com/foobar")
if err != nil {
panic(err)
}
return u
}()
func TestFindRequestByID(t *testing.T) {
t.Parallel()
database, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
// See: https://go.dev/blog/subtests#cleaning-up-after-a-group-of-parallel-tests
t.Run("group", func(t *testing.T) {
t.Run("sender request not found", func(t *testing.T) {
t.Parallel()
_, err := database.FindSenderRequestByID(context.Background(), ulid.ULID{})
if !errors.Is(err, sender.ErrRequestNotFound) {
t.Fatalf("expected `sender.ErrRequestNotFound`, got: %v", err)
}
})
t.Run("sender request found", func(t *testing.T) {
t.Parallel()
exp := sender.Request{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
SourceRequestLogID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
URL: exampleURL,
Method: http.MethodGet,
Proto: sender.HTTPProto20,
Header: http.Header{
"X-Foo": []string{"bar"},
},
Body: []byte("foo"),
}
err := database.StoreSenderRequest(context.Background(), exp)
if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
resLog := reqlog.ResponseLog{
Proto: "HTTP/2.0",
Status: "200 OK",
StatusCode: 200,
Header: http.Header{
"X-Yolo": []string{"swag"},
},
Body: []byte("bar"),
}
err = database.StoreResponseLog(context.Background(), exp.ID, resLog)
if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
exp.Response = &resLog
got, err := database.FindSenderRequestByID(context.Background(), exp.ID)
if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("sender request not equal (-exp, +got):\n%v", diff)
}
})
})
}
func TestFindSenderRequests(t *testing.T) {
t.Parallel()
t.Run("without project ID in filter", func(t *testing.T) {
t.Parallel()
database, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
filter := sender.FindRequestsFilter{}
_, err = database.FindSenderRequests(context.Background(), filter, nil)
if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
}
})
t.Run("returns sender requests and related response logs", func(t *testing.T) {
t.Parallel()
database, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true))
if err != nil {
t.Fatalf("failed to open badger database: %v", err)
}
defer database.Close()
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
fixtures := []sender.Request{
{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: projectID,
SourceRequestLogID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
URL: exampleURL,
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"baz"},
},
Body: []byte("foo"),
Response: &reqlog.ResponseLog{
Proto: "HTTP/1.1",
Status: "200 OK",
StatusCode: 200,
Header: http.Header{
"X-Yolo": []string{"swag"},
},
Body: []byte("bar"),
},
},
{
ID: ulid.MustNew(ulid.Timestamp(time.Now())+100, ulidEntropy),
ProjectID: projectID,
SourceRequestLogID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
URL: exampleURL,
Method: http.MethodGet,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"baz"},
},
},
}
// Store fixtures.
for _, senderReq := range fixtures {
err = database.StoreSenderRequest(context.Background(), senderReq)
if err != nil {
t.Fatalf("unexpected error creating request log fixture: %v", err)
}
if senderReq.Response != nil {
err = database.StoreResponseLog(context.Background(), senderReq.ID, *senderReq.Response)
if err != nil {
t.Fatalf("unexpected error creating response log fixture: %v", err)
}
}
}
filter := sender.FindRequestsFilter{
ProjectID: projectID,
}
got, err := database.FindSenderRequests(context.Background(), filter, nil)
if err != nil {
t.Fatalf("unexpected error finding sender requests: %v", err)
}
// We expect the found sender requests are *reversed*, e.g. newest first.
exp := make([]sender.Request, len(fixtures))
for i, j := 0, len(fixtures)-1; i < j; i, j = i+1, j-1 {
exp[i], exp[j] = fixtures[j], fixtures[i]
}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("sender requests not equal (-exp, +got):\n%v", diff)
}
})
}

60
pkg/db/bolt/bolt.go Normal file
View File

@ -0,0 +1,60 @@
package bolt
import (
"fmt"
bolt "go.etcd.io/bbolt"
)
// Database is used to store and retrieve data from an underlying Bolt database.
type Database struct {
bolt *bolt.DB
}
// OpenDatabase opens a new Bolt database.
func OpenDatabase(path string, opts *bolt.Options) (*Database, error) {
db, err := bolt.Open(path, 0o600, opts)
if err != nil {
return nil, fmt.Errorf("bolt: failed to open database: %w", err)
}
return DatabaseFromBoltDB(db)
}
// Close closes the underlying Bolt database.
func (db *Database) Close() error {
return db.bolt.Close()
}
// DatabaseFromBoltDB returns a Database with `db` set as the underlying Bolt
// database.
func DatabaseFromBoltDB(db *bolt.DB) (*Database, error) {
err := db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(projectsBucketName)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to create projects bucket: %w", err)
}
return &Database{bolt: db}, nil
}
func createNestedBucket(tx *bolt.Tx, names ...[]byte) (b *bolt.Bucket, err error) {
for i, name := range names {
if b == nil {
b, err = tx.CreateBucketIfNotExists(name)
} else {
b, err = b.CreateBucketIfNotExists(name)
}
if err != nil {
return nil, fmt.Errorf("bolt: failed to create nested bucket %q: %w", names[:i+1], err)
}
}
return b, nil
}

23
pkg/db/bolt/logger.go Normal file
View File

@ -0,0 +1,23 @@
package bolt
import (
bolt "go.etcd.io/bbolt"
"go.uber.org/zap"
)
// Interface guard.
var _ bolt.Logger = (*Logger)(nil)
type Logger struct {
*zap.SugaredLogger
}
// Warning implements bbolt.Logger.
func (l *Logger) Warning(v ...interface{}) {
l.Warn(v...)
}
// Warningf implements bbolt.Logger.
func (l *Logger) Warningf(format string, v ...interface{}) {
l.Warnf(format, v...)
}

174
pkg/db/bolt/proj.go Normal file
View File

@ -0,0 +1,174 @@
package bolt
import (
"context"
"errors"
"fmt"
bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
"github.com/dstotijn/hetty/pkg/proj"
)
var (
ErrProjectsBucketNotFound = errors.New("bolt: projects bucket not found")
ErrProjectBucketNotFound = errors.New("bolt: project bucket not found")
)
var (
projectsBucketName = []byte("projects")
projectKey = []byte("project")
)
func projectsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
b := tx.Bucket(projectsBucketName)
if b == nil {
return nil, ErrProjectsBucketNotFound
}
return b, nil
}
func projectBucket(tx *bolt.Tx, projectID string) (*bolt.Bucket, error) {
pb, err := projectsBucket(tx)
if err != nil {
return nil, err
}
b := pb.Bucket([]byte(projectID))
if b == nil {
return nil, ErrProjectBucketNotFound
}
return b, nil
}
func (db *Database) UpsertProject(ctx context.Context, project *proj.Project) error {
err := db.bolt.Update(func(tx *bolt.Tx) error {
b, err := createNestedBucket(tx, projectsBucketName, []byte(project.Id))
if err != nil {
return fmt.Errorf("bolt: failed to create project bucket: %w", err)
}
buf, err := proto.Marshal(project)
if err != nil {
return fmt.Errorf("bolt: failed to marshal project: %w", err)
}
err = b.Put(projectKey, buf)
if err != nil {
return fmt.Errorf("bolt: failed to upsert project: %w", err)
}
_, err = b.CreateBucketIfNotExists(reqLogsBucketName)
if err != nil {
return fmt.Errorf("bolt: failed to create request logs bucket: %w", err)
}
_, err = b.CreateBucketIfNotExists(senderReqsBucketName)
if err != nil {
return fmt.Errorf("bolt: failed to create sender requests bucket: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) FindProjectByID(ctx context.Context, projectID string) (*proj.Project, error) {
project := &proj.Project{}
err := db.bolt.View(func(tx *bolt.Tx) error {
bucket, err := projectBucket(tx, projectID)
if errors.Is(err, ErrProjectsBucketNotFound) || errors.Is(err, ErrProjectBucketNotFound) {
return proj.ErrProjectNotFound
}
if err != nil {
return err
}
rawProject := bucket.Get(projectKey)
if rawProject == nil {
return proj.ErrProjectNotFound
}
err = proto.Unmarshal(rawProject, project)
if err != nil {
return fmt.Errorf("failed to unmarshal project: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return project, nil
}
func (db *Database) DeleteProject(ctx context.Context, projectID string) error {
err := db.bolt.Update(func(tx *bolt.Tx) error {
pb, err := projectsBucket(tx)
if err != nil {
return err
}
err = pb.DeleteBucket([]byte(projectID))
if err != nil {
return fmt.Errorf("failed to delete project bucket: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) Projects(ctx context.Context) ([]*proj.Project, error) {
projects := make([]*proj.Project, 0)
err := db.bolt.View(func(tx *bolt.Tx) error {
pb, err := projectsBucket(tx)
if err != nil {
return err
}
err = pb.ForEachBucket(func(projectID []byte) error {
bucket, err := projectBucket(tx, string(projectID))
if err != nil {
return err
}
rawProject := bucket.Get(projectKey)
if rawProject == nil {
return proj.ErrProjectNotFound
}
project := &proj.Project{}
err = proto.Unmarshal(rawProject, project)
if err != nil {
return fmt.Errorf("failed to unmarshal project: %w", err)
}
projects = append(projects, project)
return nil
})
if err != nil {
return fmt.Errorf("failed to iterate over projects: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return projects, nil
}

233
pkg/db/bolt/proj_test.go Normal file
View File

@ -0,0 +1,233 @@
package bolt_test
import (
"context"
"errors"
"testing"
"github.com/oklog/ulid/v2"
"go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
"github.com/dstotijn/hetty/pkg/db/bolt"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/testutil"
)
func TestUpsertProject(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
exp := &proj.Project{
Id: "foobar-project-id",
Name: "foobar",
ReqLogBypassOutOfScope: true,
ReqLogFilter: &reqlog.RequestLogsFilter{
OnlyInScope: true,
SearchExpr: "foo AND bar OR NOT baz",
},
ScopeRules: []*scope.ScopeRule{
{
UrlRegexp: "^https://(.*)example.com(.*)$",
HeaderKeyRegexp: "^X-Foo(.*)$",
HeaderValueRegexp: "^foo(.*)$",
BodyRegexp: "^foo(.*)",
},
},
}
err = db.UpsertProject(context.Background(), exp)
if err != nil {
t.Fatalf("unexpected error storing project: %v", err)
}
var rawProject []byte
err = boltDB.View(func(tx *bbolt.Tx) error {
rawProject = tx.Bucket([]byte("projects")).Bucket([]byte(exp.Id)).Get([]byte("project"))
return nil
})
if err != nil {
t.Fatalf("unexpected error retrieving project from database: %v", err)
}
if rawProject == nil {
t.Fatalf("expected raw project to be retrieved, got: nil")
}
got := &proj.Project{}
err = proto.Unmarshal(rawProject, got)
if err != nil {
t.Fatalf("unexpected error decoding project: %v", err)
}
testutil.ProtoDiff(t, "project not equal", exp, got, "id")
}
func TestFindProjectByID(t *testing.T) {
t.Parallel()
t.Run("existing project", func(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
exp := &proj.Project{
Id: ulid.Make().String(),
}
buf, err := proto.Marshal(exp)
if err != nil {
t.Fatalf("unexpected error encoding project: %v", err)
}
err = boltDB.Update(func(tx *bbolt.Tx) error {
b, err := tx.Bucket([]byte("projects")).CreateBucket([]byte(exp.Id))
if err != nil {
return err
}
return b.Put([]byte("project"), buf)
})
if err != nil {
t.Fatalf("unexpected error setting project: %v", err)
}
got, err := db.FindProjectByID(context.Background(), exp.Id)
if err != nil {
t.Fatalf("unexpected error finding project: %v", err)
}
testutil.ProtoDiff(t, "project not equal", exp, got)
})
t.Run("project not found", func(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := ulid.Make().String()
_, err = db.FindProjectByID(context.Background(), projectID)
if !errors.Is(err, proj.ErrProjectNotFound) {
t.Fatalf("expected `proj.ErrProjectNotFound`, got: %v", err)
}
})
}
func TestDeleteProject(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
// Insert test fixture.
projectID := ulid.Make().String()
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error storing project: %v", err)
}
err = db.DeleteProject(context.Background(), projectID)
if err != nil {
t.Fatalf("unexpected error deleting project: %v", err)
}
var got *bbolt.Bucket
_ = boltDB.View(func(tx *bbolt.Tx) error {
got = tx.Bucket([]byte("projects")).Bucket([]byte(projectID))
return nil
})
if got != nil {
t.Fatalf("expected bucket to be nil, got: %v", got)
}
}
func TestProjects(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
exp := []*proj.Project{
{
Id: ulid.Make().String(),
Name: "one",
},
{
Id: ulid.Make().String(),
Name: "two",
},
}
// Store fixtures.
for _, project := range exp {
err = db.UpsertProject(context.Background(), project)
if err != nil {
t.Fatalf("unexpected error creating project fixture: %v", err)
}
}
got, err := db.Projects(context.Background())
if err != nil {
t.Fatalf("unexpected error finding projects: %v", err)
}
if len(exp) != len(got) {
t.Fatalf("expected %v projects, got: %v", len(exp), len(got))
}
testutil.ProtoSlicesDiff(t, "projects not equal", exp, got)
}

182
pkg/db/bolt/reqlog.go Normal file
View File

@ -0,0 +1,182 @@
package bolt
import (
"context"
"errors"
"fmt"
bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
"github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/reqlog"
)
var ErrRequestLogsBucketNotFound = errors.New("bolt: request logs bucket not found")
var reqLogsBucketName = []byte("request_logs")
func requestLogsBucket(tx *bolt.Tx, projectID string) (*bolt.Bucket, error) {
pb, err := projectBucket(tx, projectID)
if err != nil {
return nil, err
}
b := pb.Bucket(reqLogsBucketName)
if b == nil {
return nil, ErrRequestLogsBucketNotFound
}
return b, nil
}
func (db *Database) FindRequestLogs(ctx context.Context, projectID string, filterFn func(*reqlog.HttpRequestLog) (bool, error)) (reqLogs []*reqlog.HttpRequestLog, err error) {
tx, err := db.bolt.Begin(false)
if err != nil {
return nil, fmt.Errorf("bolt: failed to begin transaction: %w", err)
}
defer tx.Rollback()
b, err := requestLogsBucket(tx, projectID)
if err != nil {
return nil, fmt.Errorf("bolt: failed to get request logs bucket: %w", err)
}
err = b.ForEach(func(reqLogID, rawReqLog []byte) error {
var reqLog reqlog.HttpRequestLog
err = proto.Unmarshal(rawReqLog, &reqLog)
if err != nil {
return fmt.Errorf("failed to decode request log: %w", err)
}
if filterFn != nil {
match, err := filterFn(&reqLog)
if err != nil {
return fmt.Errorf("failed to filter request log: %w", err)
}
if !match {
return nil
}
}
reqLogs = append(reqLogs, &reqLog)
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to iterate over request logs: %w", err)
}
// Reverse items, so newest requests appear first.
for i, j := 0, len(reqLogs)-1; i < j; i, j = i+1, j-1 {
reqLogs[i], reqLogs[j] = reqLogs[j], reqLogs[i]
}
return reqLogs, nil
}
func (db *Database) FindRequestLogByID(ctx context.Context, projectID, reqLogID string) (*reqlog.HttpRequestLog, error) {
reqLog := &reqlog.HttpRequestLog{}
err := db.bolt.View(func(tx *bolt.Tx) error {
b, err := requestLogsBucket(tx, projectID)
if err != nil {
return fmt.Errorf("bolt: failed to get request logs bucket: %w", err)
}
rawReqLog := b.Get([]byte(reqLogID))
if rawReqLog == nil {
return reqlog.ErrRequestLogNotFound
}
err = proto.Unmarshal(rawReqLog, reqLog)
if err != nil {
return fmt.Errorf("failed to unmarshal request log: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to find request log by ID: %w", err)
}
return reqLog, nil
}
func (db *Database) StoreRequestLog(ctx context.Context, reqLog *reqlog.HttpRequestLog) error {
encReqLog, err := proto.Marshal(reqLog)
if err != nil {
return fmt.Errorf("bolt: failed to marshal request log: %w", err)
}
err = db.bolt.Update(func(txn *bolt.Tx) error {
b, err := requestLogsBucket(txn, reqLog.ProjectId)
if err != nil {
return fmt.Errorf("failed to get request logs bucket: %w", err)
}
err = b.Put([]byte(reqLog.Id), encReqLog)
if err != nil {
return fmt.Errorf("failed to put request log: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) StoreResponseLog(ctx context.Context, projectID, reqLogID string, resLog *http.Response) error {
err := db.bolt.Update(func(txn *bolt.Tx) error {
b, err := requestLogsBucket(txn, projectID)
if err != nil {
return fmt.Errorf("failed to get request logs bucket: %w", err)
}
encReqLog := b.Get([]byte(reqLogID))
if encReqLog == nil {
return reqlog.ErrRequestLogNotFound
}
var reqLog reqlog.HttpRequestLog
err = proto.Unmarshal(encReqLog, &reqLog)
if err != nil {
return fmt.Errorf("failed to decode request log: %w", err)
}
reqLog.Response = resLog
encReqLog, err = proto.Marshal(&reqLog)
if err != nil {
return fmt.Errorf("failed to encode request log: %w", err)
}
err = b.Put([]byte(reqLogID), encReqLog)
if err != nil {
return fmt.Errorf("failed to put request log: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) ClearRequestLogs(ctx context.Context, projectID string) error {
err := db.bolt.Update(func(txn *bolt.Tx) error {
pb, err := projectBucket(txn, projectID)
if err != nil {
return fmt.Errorf("failed to get project bucket: %w", err)
}
return pb.DeleteBucket(reqLogsBucketName)
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}

110
pkg/db/bolt/reqlog_test.go Normal file
View File

@ -0,0 +1,110 @@
package bolt_test
import (
"context"
"testing"
"github.com/oklog/ulid/v2"
"go.etcd.io/bbolt"
"github.com/dstotijn/hetty/pkg/db/bolt"
"github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/testutil"
)
func TestFindRequestLogs(t *testing.T) {
t.Parallel()
t.Run("returns request logs and related response logs", func(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := ulid.Make().String()
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
}
fixtures := []*reqlog.HttpRequestLog{
{
Id: ulid.Make().String(),
ProjectId: projectID,
Request: &http.Request{
Url: "https://example.com/foobar",
Method: http.Method_METHOD_POST,
Protocol: http.Protocol_PROTOCOL_HTTP11,
Headers: []*http.Header{
{Key: "X-Foo", Value: "baz"},
},
Body: []byte("foo"),
},
Response: &http.Response{
Status: "200 OK",
StatusCode: 200,
Headers: []*http.Header{
{Key: "X-Yolo", Value: "swag"},
},
Body: []byte("bar"),
},
},
{
Id: ulid.Make().String(),
ProjectId: projectID,
Request: &http.Request{
Url: "https://example.com/foo?bar=baz",
Method: http.Method_METHOD_GET,
Protocol: http.Protocol_PROTOCOL_HTTP11,
Headers: []*http.Header{
{Key: "X-Foo", Value: "baz"},
},
Body: []byte("foo"),
},
Response: &http.Response{
Status: "200 OK",
StatusCode: 200,
Headers: []*http.Header{
{Key: "X-Yolo", Value: "swag"},
},
Body: []byte("bar"),
},
},
}
// Store fixtures.
for _, reqLog := range fixtures {
err = db.StoreRequestLog(context.Background(), reqLog)
if err != nil {
t.Fatalf("unexpected error creating request log fixture: %v", err)
}
}
got, err := db.FindRequestLogs(context.Background(), projectID, nil)
if err != nil {
t.Fatalf("unexpected error finding request logs: %v", err)
}
// We expect the found request logs are *reversed*, e.g. newest first.
exp := make([]*reqlog.HttpRequestLog, len(fixtures))
for i, j := 0, len(fixtures)-1; i < j; i, j = i+1, j-1 {
exp[i], exp[j] = fixtures[j], fixtures[i]
}
testutil.ProtoSlicesDiff(t, "request logs not equal", exp, got)
})
}

153
pkg/db/bolt/sender.go Normal file
View File

@ -0,0 +1,153 @@
package bolt
import (
"context"
"errors"
"fmt"
bolt "go.etcd.io/bbolt"
"google.golang.org/protobuf/proto"
"github.com/dstotijn/hetty/pkg/sender"
)
var ErrSenderRequestsBucketNotFound = errors.New("bolt: sender requests bucket not found")
var senderReqsBucketName = []byte("sender_requests")
func senderReqsBucket(tx *bolt.Tx, projectID string) (*bolt.Bucket, error) {
pb, err := projectBucket(tx, projectID)
if err != nil {
return nil, err
}
b := pb.Bucket(senderReqsBucketName)
if b == nil {
return nil, ErrSenderRequestsBucketNotFound
}
return b, nil
}
func (db *Database) StoreSenderRequest(ctx context.Context, req *sender.Request) error {
rawReq, err := proto.Marshal(req)
if err != nil {
return fmt.Errorf("bolt: failed to marshal sender request: %w", err)
}
err = db.bolt.Update(func(tx *bolt.Tx) error {
senderReqsBucket, err := senderReqsBucket(tx, req.ProjectId)
if err != nil {
return fmt.Errorf("failed to get sender requests bucket: %w", err)
}
err = senderReqsBucket.Put([]byte(req.Id), rawReq)
if err != nil {
return fmt.Errorf("failed to put sender request: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}
func (db *Database) FindSenderRequestByID(ctx context.Context, projectID, senderReqID string) (req *sender.Request, err error) {
if projectID == "" {
return nil, sender.ErrProjectIDMustBeSet
}
err = db.bolt.View(func(tx *bolt.Tx) error {
senderReqsBucket, err := senderReqsBucket(tx, projectID)
if err != nil {
return fmt.Errorf("failed to get sender requests bucket: %w", err)
}
rawSenderReq := senderReqsBucket.Get([]byte(senderReqID))
if rawSenderReq == nil {
return sender.ErrRequestNotFound
}
req = &sender.Request{}
err = proto.Unmarshal(rawSenderReq, req)
if err != nil {
return fmt.Errorf("failed to unmarshal sender request: %w", err)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return req, nil
}
func (db *Database) FindSenderRequests(ctx context.Context, projectID string, filterFn func(req *sender.Request) (bool, error)) (reqs []*sender.Request, err error) {
tx, err := db.bolt.Begin(false)
if err != nil {
return nil, fmt.Errorf("bolt: failed to begin transaction: %w", err)
}
defer tx.Rollback()
b, err := senderReqsBucket(tx, projectID)
if err != nil {
return nil, fmt.Errorf("failed to get sender requests bucket: %w", err)
}
err = b.ForEach(func(senderReqID, rawSenderReq []byte) error {
req := &sender.Request{}
err = proto.Unmarshal(rawSenderReq, req)
if err != nil {
return fmt.Errorf("failed to unmarshal sender request: %w", err)
}
if filterFn != nil {
match, err := filterFn(req)
if err != nil {
return fmt.Errorf("failed to filter sender request: %w", err)
}
if !match {
return nil
}
}
reqs = append(reqs, req)
return nil
})
if err != nil {
return nil, fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
// Reverse items, so newest requests appear first.
for i, j := 0, len(reqs)-1; i < j; i, j = i+1, j-1 {
reqs[i], reqs[j] = reqs[j], reqs[i]
}
return reqs, nil
}
func (db *Database) DeleteSenderRequests(ctx context.Context, projectID string) error {
err := db.bolt.Update(func(tx *bolt.Tx) error {
senderReqsBucket, err := senderReqsBucket(tx, projectID)
if err != nil {
return fmt.Errorf("failed to get sender requests bucket: %w", err)
}
err = senderReqsBucket.DeleteBucket(senderReqsBucketName)
if err != nil {
return fmt.Errorf("failed to delete sender requests bucket: %w", err)
}
return nil
})
if err != nil {
return fmt.Errorf("bolt: failed to commit transaction: %w", err)
}
return nil
}

212
pkg/db/bolt/sender_test.go Normal file
View File

@ -0,0 +1,212 @@
package bolt_test
import (
"context"
"errors"
"net/url"
"testing"
"github.com/oklog/ulid/v2"
"go.etcd.io/bbolt"
"github.com/dstotijn/hetty/pkg/db/bolt"
"github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/sender"
"github.com/dstotijn/hetty/pkg/testutil"
)
var exampleURL = func() *url.URL {
u, err := url.Parse("https://example.com/foobar")
if err != nil {
panic(err)
}
return u
}()
func TestFindRequestByID(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
defer boltDB.Close()
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := "foobar-project-id"
reqID := "foobar-req-id"
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
}
// See: https://go.dev/blog/subtests#cleaning-up-after-a-group-of-parallel-tests
t.Run("group", func(t *testing.T) {
t.Run("sender request not found", func(t *testing.T) {
t.Parallel()
_, err := db.FindSenderRequestByID(context.Background(), projectID, reqID)
if !errors.Is(err, sender.ErrRequestNotFound) {
t.Fatalf("expected `sender.ErrRequestNotFound`, got: %v", err)
}
})
t.Run("sender request found", func(t *testing.T) {
t.Parallel()
exp := &sender.Request{
Id: "foobar-sender-req-id",
ProjectId: projectID,
SourceRequestLogId: "foobar-req-log-id",
HttpRequest: &http.Request{
Url: exampleURL.String(),
Method: http.Method_METHOD_GET,
Protocol: http.Protocol_PROTOCOL_HTTP20,
Headers: []*http.Header{
{
Key: "X-Foo",
Value: "bar",
},
},
Body: []byte("foo"),
},
HttpResponse: &http.Response{
Protocol: http.Protocol_PROTOCOL_HTTP20,
Status: "200 OK",
StatusCode: 200,
Headers: []*http.Header{
{
Key: "X-Yolo",
Value: "swag",
},
},
Body: []byte("bar"),
},
}
err := db.StoreSenderRequest(context.Background(), exp)
if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
got, err := db.FindSenderRequestByID(context.Background(), projectID, exp.Id)
if err != nil {
t.Fatalf("unexpected error (expected: nil, got: %v)", err)
}
testutil.ProtoDiff(t, "sender request not equal", exp, got, "id")
})
})
}
func TestFindSenderRequests(t *testing.T) {
t.Parallel()
t.Run("returns sender requests and related response logs", func(t *testing.T) {
t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
defer boltDB.Close()
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := "foobar-project-id"
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
Name: "foobar",
})
if err != nil {
t.Fatalf("unexpected error creating project (expected: nil, got: %v)", err)
}
fixtures := []*sender.Request{
{
Id: ulid.Make().String(),
ProjectId: projectID,
SourceRequestLogId: "foobar-req-log-id-1",
HttpRequest: &http.Request{
Url: exampleURL.String(),
Method: http.Method_METHOD_POST,
Protocol: http.Protocol_PROTOCOL_HTTP11,
Headers: []*http.Header{
{
Key: "X-Foo",
Value: "baz",
},
},
Body: []byte("foo"),
},
HttpResponse: &http.Response{
Protocol: http.Protocol_PROTOCOL_HTTP11,
Status: "200 OK",
StatusCode: 200,
Headers: []*http.Header{
{
Key: "X-Yolo",
Value: "swag",
},
},
Body: []byte("bar"),
},
},
{
Id: ulid.Make().String(),
ProjectId: projectID,
SourceRequestLogId: "foobar-req-log-id-2",
HttpRequest: &http.Request{
Url: exampleURL.String(),
Method: http.Method_METHOD_GET,
Protocol: http.Protocol_PROTOCOL_HTTP11,
Headers: []*http.Header{
{
Key: "X-Foo",
Value: "baz",
},
},
Body: []byte("foo"),
},
},
}
// Store fixtures.
for _, senderReq := range fixtures {
err = db.StoreSenderRequest(context.Background(), senderReq)
if err != nil {
t.Fatalf("unexpected error creating request log fixture: %v", err)
}
}
got, err := db.FindSenderRequests(context.Background(), projectID, nil)
if err != nil {
t.Fatalf("unexpected error finding sender requests: %v", err)
}
// We expect the found sender requests are *reversed*, e.g. newest first.
exp := make([]*sender.Request, len(fixtures))
for i, j := 0, len(fixtures)-1; i < j; i, j = i+1, j-1 {
exp[i], exp[j] = fixtures[j], fixtures[i]
}
testutil.ProtoSlicesDiff(t, "sender requests not equal", exp, got)
})
}

View File

@ -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
View 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)
}
})
}
}

75
pkg/filter/http.go Normal file
View File

@ -0,0 +1,75 @@
package filter
import (
"errors"
"fmt"
"github.com/dstotijn/hetty/pkg/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 _, header := range headers {
if strLiteral.Value == fmt.Sprintf("%v: %v", header.Key, header.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 _, header := range headers {
if strLiteral.Value == fmt.Sprintf("%v: %v", header.Key, header.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 _, header := range headers {
if re.Regexp.MatchString(fmt.Sprintf("%v: %v", header.Key, header.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 _, header := range headers {
if re.Regexp.MatchString(fmt.Sprintf("%v: %v", header.Key, header.Value)) {
return false, nil
}
}
return true, nil
default:
return false, fmt.Errorf("filter: unsupported operator %q", op.String())
}
}

View File

@ -1,4 +1,4 @@
package search package filter
import ( import (
"fmt" "fmt"

View File

@ -1,4 +1,4 @@
package search package filter
import "testing" import "testing"

View File

@ -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:

View File

@ -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",

79
pkg/http/http.go Normal file
View File

@ -0,0 +1,79 @@
package http
import (
"fmt"
"io"
nethttp "net/http"
"strconv"
"strings"
)
var ProtoMap = map[string]Protocol{
"HTTP/1.0": Protocol_PROTOCOL_HTTP10,
"HTTP/1.1": Protocol_PROTOCOL_HTTP11,
"HTTP/2.0": Protocol_PROTOCOL_HTTP20,
}
var MethodMap = map[string]Method{
"GET": Method_METHOD_GET,
"POST": Method_METHOD_POST,
"PUT": Method_METHOD_PUT,
"DELETE": Method_METHOD_DELETE,
"CONNECT": Method_METHOD_CONNECT,
"OPTIONS": Method_METHOD_OPTIONS,
"TRACE": Method_METHOD_TRACE,
"PATCH": Method_METHOD_PATCH,
}
func ParseHeader(header nethttp.Header) []*Header {
headers := []*Header{}
for key, values := range header {
for _, value := range values {
headers = append(headers, &Header{Key: key, Value: value})
}
}
return headers
}
var ResponseSearchKeyFns = map[string]func(rl *Response) string{
"res.proto": func(rl *Response) string { return rl.GetProtocol().String() },
"res.status": func(rl *Response) string { return rl.GetStatus() },
"res.statusCode": func(rl *Response) string { return strconv.Itoa(int(rl.GetStatusCode())) },
"res.statusReason": func(rl *Response) string {
statusReasonSubs := strings.SplitN(rl.GetStatus(), " ", 2)
if len(statusReasonSubs) != 2 {
return ""
}
return statusReasonSubs[1]
},
"res.body": func(rl *Response) string { return string(rl.GetBody()) },
}
func ParseHTTPResponse(res *nethttp.Response) (*Response, error) {
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("reqlog: could not read body: %w", err)
}
headers := []*Header{}
for k, v := range res.Header {
for _, vv := range v {
headers = append(headers, &Header{Key: k, Value: vv})
}
}
protocol, ok := ProtoMap[res.Proto]
if !ok {
return nil, fmt.Errorf("reqlog: invalid protocol %q", res.Proto)
}
return &Response{
Protocol: protocol,
Status: res.Status,
StatusCode: int32(res.StatusCode),
Headers: headers,
Body: body,
}, nil
}

476
pkg/http/http.pb.go Normal file
View File

@ -0,0 +1,476 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.3
// protoc (unknown)
// source: http/http.proto
package http
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Method int32
const (
Method_METHOD_UNSPECIFIED Method = 0
Method_METHOD_GET Method = 1
Method_METHOD_HEAD Method = 2
Method_METHOD_POST Method = 3
Method_METHOD_PUT Method = 4
Method_METHOD_DELETE Method = 5
Method_METHOD_CONNECT Method = 6
Method_METHOD_OPTIONS Method = 7
Method_METHOD_TRACE Method = 8
Method_METHOD_PATCH Method = 9
)
// Enum value maps for Method.
var (
Method_name = map[int32]string{
0: "METHOD_UNSPECIFIED",
1: "METHOD_GET",
2: "METHOD_HEAD",
3: "METHOD_POST",
4: "METHOD_PUT",
5: "METHOD_DELETE",
6: "METHOD_CONNECT",
7: "METHOD_OPTIONS",
8: "METHOD_TRACE",
9: "METHOD_PATCH",
}
Method_value = map[string]int32{
"METHOD_UNSPECIFIED": 0,
"METHOD_GET": 1,
"METHOD_HEAD": 2,
"METHOD_POST": 3,
"METHOD_PUT": 4,
"METHOD_DELETE": 5,
"METHOD_CONNECT": 6,
"METHOD_OPTIONS": 7,
"METHOD_TRACE": 8,
"METHOD_PATCH": 9,
}
)
func (x Method) Enum() *Method {
p := new(Method)
*p = x
return p
}
func (x Method) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Method) Descriptor() protoreflect.EnumDescriptor {
return file_http_http_proto_enumTypes[0].Descriptor()
}
func (Method) Type() protoreflect.EnumType {
return &file_http_http_proto_enumTypes[0]
}
func (x Method) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Method.Descriptor instead.
func (Method) EnumDescriptor() ([]byte, []int) {
return file_http_http_proto_rawDescGZIP(), []int{0}
}
type Protocol int32
const (
Protocol_PROTOCOL_UNSPECIFIED Protocol = 0
Protocol_PROTOCOL_HTTP10 Protocol = 1
Protocol_PROTOCOL_HTTP11 Protocol = 2
Protocol_PROTOCOL_HTTP20 Protocol = 3
)
// Enum value maps for Protocol.
var (
Protocol_name = map[int32]string{
0: "PROTOCOL_UNSPECIFIED",
1: "PROTOCOL_HTTP10",
2: "PROTOCOL_HTTP11",
3: "PROTOCOL_HTTP20",
}
Protocol_value = map[string]int32{
"PROTOCOL_UNSPECIFIED": 0,
"PROTOCOL_HTTP10": 1,
"PROTOCOL_HTTP11": 2,
"PROTOCOL_HTTP20": 3,
}
)
func (x Protocol) Enum() *Protocol {
p := new(Protocol)
*p = x
return p
}
func (x Protocol) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Protocol) Descriptor() protoreflect.EnumDescriptor {
return file_http_http_proto_enumTypes[1].Descriptor()
}
func (Protocol) Type() protoreflect.EnumType {
return &file_http_http_proto_enumTypes[1]
}
func (x Protocol) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Protocol.Descriptor instead.
func (Protocol) EnumDescriptor() ([]byte, []int) {
return file_http_http_proto_rawDescGZIP(), []int{1}
}
type Request struct {
state protoimpl.MessageState `protogen:"open.v1"`
Method Method `protobuf:"varint,1,opt,name=method,proto3,enum=hetty.http.v1.Method" json:"method,omitempty"`
Protocol Protocol `protobuf:"varint,2,opt,name=protocol,proto3,enum=hetty.http.v1.Protocol" json:"protocol,omitempty"`
Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
Headers []*Header `protobuf:"bytes,4,rep,name=headers,proto3" json:"headers,omitempty"`
Body []byte `protobuf:"bytes,5,opt,name=body,proto3" json:"body,omitempty"`
Response *Response `protobuf:"bytes,6,opt,name=response,proto3" json:"response,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Request) Reset() {
*x = Request{}
mi := &file_http_http_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Request) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Request) ProtoMessage() {}
func (x *Request) ProtoReflect() protoreflect.Message {
mi := &file_http_http_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Request.ProtoReflect.Descriptor instead.
func (*Request) Descriptor() ([]byte, []int) {
return file_http_http_proto_rawDescGZIP(), []int{0}
}
func (x *Request) GetMethod() Method {
if x != nil {
return x.Method
}
return Method_METHOD_UNSPECIFIED
}
func (x *Request) GetProtocol() Protocol {
if x != nil {
return x.Protocol
}
return Protocol_PROTOCOL_UNSPECIFIED
}
func (x *Request) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
func (x *Request) GetHeaders() []*Header {
if x != nil {
return x.Headers
}
return nil
}
func (x *Request) GetBody() []byte {
if x != nil {
return x.Body
}
return nil
}
func (x *Request) GetResponse() *Response {
if x != nil {
return x.Response
}
return nil
}
type Response struct {
state protoimpl.MessageState `protogen:"open.v1"`
Protocol Protocol `protobuf:"varint,1,opt,name=protocol,proto3,enum=hetty.http.v1.Protocol" json:"protocol,omitempty"`
Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
StatusCode int32 `protobuf:"varint,3,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"`
Headers []*Header `protobuf:"bytes,5,rep,name=headers,proto3" json:"headers,omitempty"`
Body []byte `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Response) Reset() {
*x = Response{}
mi := &file_http_http_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Response) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Response) ProtoMessage() {}
func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_http_http_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*Response) Descriptor() ([]byte, []int) {
return file_http_http_proto_rawDescGZIP(), []int{1}
}
func (x *Response) GetProtocol() Protocol {
if x != nil {
return x.Protocol
}
return Protocol_PROTOCOL_UNSPECIFIED
}
func (x *Response) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
func (x *Response) GetStatusCode() int32 {
if x != nil {
return x.StatusCode
}
return 0
}
func (x *Response) GetHeaders() []*Header {
if x != nil {
return x.Headers
}
return nil
}
func (x *Response) GetBody() []byte {
if x != nil {
return x.Body
}
return nil
}
type Header struct {
state protoimpl.MessageState `protogen:"open.v1"`
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Header) Reset() {
*x = Header{}
mi := &file_http_http_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Header) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Header) ProtoMessage() {}
func (x *Header) ProtoReflect() protoreflect.Message {
mi := &file_http_http_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Header.ProtoReflect.Descriptor instead.
func (*Header) Descriptor() ([]byte, []int) {
return file_http_http_proto_rawDescGZIP(), []int{2}
}
func (x *Header) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *Header) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
var File_http_http_proto protoreflect.FileDescriptor
var file_http_http_proto_rawDesc = []byte{
0x0a, 0x0f, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x12, 0x0d, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31,
0x22, 0xf9, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2d, 0x0a, 0x06,
0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x68,
0x65, 0x74, 0x74, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74,
0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x0a, 0x08, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e,
0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72,
0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75,
0x72, 0x6c, 0x12, 0x2f, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70,
0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x07, 0x68, 0x65, 0x61, 0x64,
0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28,
0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x12, 0x33, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x74, 0x74,
0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xbd, 0x01, 0x0a,
0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x08, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x68, 0x65,
0x74, 0x74, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x74,
0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x16,
0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x73, 0x74, 0x61,
0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x68, 0x65, 0x61, 0x64, 0x65,
0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79,
0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52,
0x07, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x6f, 0x64, 0x79,
0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x62, 0x6f, 0x64, 0x79, 0x22, 0x30, 0x0a, 0x06,
0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0xc1,
0x01, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x16, 0x0a, 0x12, 0x4d, 0x45, 0x54,
0x48, 0x4f, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10,
0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x47, 0x45, 0x54, 0x10,
0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x48, 0x45, 0x41, 0x44,
0x10, 0x02, 0x12, 0x0f, 0x0a, 0x0b, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x50, 0x4f, 0x53,
0x54, 0x10, 0x03, 0x12, 0x0e, 0x0a, 0x0a, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x50, 0x55,
0x54, 0x10, 0x04, 0x12, 0x11, 0x0a, 0x0d, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x44, 0x45,
0x4c, 0x45, 0x54, 0x45, 0x10, 0x05, 0x12, 0x12, 0x0a, 0x0e, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44,
0x5f, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x10, 0x06, 0x12, 0x12, 0x0a, 0x0e, 0x4d, 0x45,
0x54, 0x48, 0x4f, 0x44, 0x5f, 0x4f, 0x50, 0x54, 0x49, 0x4f, 0x4e, 0x53, 0x10, 0x07, 0x12, 0x10,
0x0a, 0x0c, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x08,
0x12, 0x10, 0x0a, 0x0c, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x50, 0x41, 0x54, 0x43, 0x48,
0x10, 0x09, 0x2a, 0x63, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x18,
0x0a, 0x14, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45,
0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x50, 0x52, 0x4f, 0x54,
0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x31, 0x30, 0x10, 0x01, 0x12, 0x13, 0x0a,
0x0f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x48, 0x54, 0x54, 0x50, 0x31, 0x31,
0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x50, 0x52, 0x4f, 0x54, 0x4f, 0x43, 0x4f, 0x4c, 0x5f, 0x48,
0x54, 0x54, 0x50, 0x32, 0x30, 0x10, 0x03, 0x42, 0x24, 0x5a, 0x22, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x73, 0x74, 0x6f, 0x74, 0x69, 0x6a, 0x6e, 0x2f, 0x68,
0x65, 0x74, 0x74, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x68, 0x74, 0x74, 0x70, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_http_http_proto_rawDescOnce sync.Once
file_http_http_proto_rawDescData = file_http_http_proto_rawDesc
)
func file_http_http_proto_rawDescGZIP() []byte {
file_http_http_proto_rawDescOnce.Do(func() {
file_http_http_proto_rawDescData = protoimpl.X.CompressGZIP(file_http_http_proto_rawDescData)
})
return file_http_http_proto_rawDescData
}
var file_http_http_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_http_http_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_http_http_proto_goTypes = []any{
(Method)(0), // 0: hetty.http.v1.Method
(Protocol)(0), // 1: hetty.http.v1.Protocol
(*Request)(nil), // 2: hetty.http.v1.Request
(*Response)(nil), // 3: hetty.http.v1.Response
(*Header)(nil), // 4: hetty.http.v1.Header
}
var file_http_http_proto_depIdxs = []int32{
0, // 0: hetty.http.v1.Request.method:type_name -> hetty.http.v1.Method
1, // 1: hetty.http.v1.Request.protocol:type_name -> hetty.http.v1.Protocol
4, // 2: hetty.http.v1.Request.headers:type_name -> hetty.http.v1.Header
3, // 3: hetty.http.v1.Request.response:type_name -> hetty.http.v1.Response
1, // 4: hetty.http.v1.Response.protocol:type_name -> hetty.http.v1.Protocol
4, // 5: hetty.http.v1.Response.headers:type_name -> hetty.http.v1.Header
6, // [6:6] is the sub-list for method output_type
6, // [6:6] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
}
func init() { file_http_http_proto_init() }
func file_http_http_proto_init() {
if File_http_http_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_http_http_proto_rawDesc,
NumEnums: 2,
NumMessages: 3,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_http_http_proto_goTypes,
DependencyIndexes: file_http_http_proto_depIdxs,
EnumInfos: file_http_http_proto_enumTypes,
MessageInfos: file_http_http_proto_msgTypes,
}.Build()
File_http_http_proto = out.File
file_http_http_proto_rawDesc = nil
file_http_http_proto_goTypes = nil
file_http_http_proto_depIdxs = nil
}

340
pkg/proj/proj.connect.go Normal file
View File

@ -0,0 +1,340 @@
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: proj/proj.proto
package proj
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// ProjectServiceName is the fully-qualified name of the ProjectService service.
ProjectServiceName = "hetty.proj.v1.ProjectService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// ProjectServiceCreateProjectProcedure is the fully-qualified name of the ProjectService's
// CreateProject RPC.
ProjectServiceCreateProjectProcedure = "/hetty.proj.v1.ProjectService/CreateProject"
// ProjectServiceOpenProjectProcedure is the fully-qualified name of the ProjectService's
// OpenProject RPC.
ProjectServiceOpenProjectProcedure = "/hetty.proj.v1.ProjectService/OpenProject"
// ProjectServiceCloseProjectProcedure is the fully-qualified name of the ProjectService's
// CloseProject RPC.
ProjectServiceCloseProjectProcedure = "/hetty.proj.v1.ProjectService/CloseProject"
// ProjectServiceDeleteProjectProcedure is the fully-qualified name of the ProjectService's
// DeleteProject RPC.
ProjectServiceDeleteProjectProcedure = "/hetty.proj.v1.ProjectService/DeleteProject"
// ProjectServiceGetActiveProjectProcedure is the fully-qualified name of the ProjectService's
// GetActiveProject RPC.
ProjectServiceGetActiveProjectProcedure = "/hetty.proj.v1.ProjectService/GetActiveProject"
// ProjectServiceListProjectsProcedure is the fully-qualified name of the ProjectService's
// ListProjects RPC.
ProjectServiceListProjectsProcedure = "/hetty.proj.v1.ProjectService/ListProjects"
// ProjectServiceUpdateInterceptSettingsProcedure is the fully-qualified name of the
// ProjectService's UpdateInterceptSettings RPC.
ProjectServiceUpdateInterceptSettingsProcedure = "/hetty.proj.v1.ProjectService/UpdateInterceptSettings"
// ProjectServiceSetScopeRulesProcedure is the fully-qualified name of the ProjectService's
// SetScopeRules RPC.
ProjectServiceSetScopeRulesProcedure = "/hetty.proj.v1.ProjectService/SetScopeRules"
// ProjectServiceSetRequestLogsFilterProcedure is the fully-qualified name of the ProjectService's
// SetRequestLogsFilter RPC.
ProjectServiceSetRequestLogsFilterProcedure = "/hetty.proj.v1.ProjectService/SetRequestLogsFilter"
)
// ProjectServiceClient is a client for the hetty.proj.v1.ProjectService service.
type ProjectServiceClient interface {
CreateProject(context.Context, *connect.Request[CreateProjectRequest]) (*connect.Response[CreateProjectResponse], error)
OpenProject(context.Context, *connect.Request[OpenProjectRequest]) (*connect.Response[OpenProjectResponse], error)
CloseProject(context.Context, *connect.Request[CloseProjectRequest]) (*connect.Response[CloseProjectResponse], error)
DeleteProject(context.Context, *connect.Request[DeleteProjectRequest]) (*connect.Response[DeleteProjectResponse], error)
GetActiveProject(context.Context, *connect.Request[GetActiveProjectRequest]) (*connect.Response[GetActiveProjectResponse], error)
ListProjects(context.Context, *connect.Request[ListProjectsRequest]) (*connect.Response[ListProjectsResponse], error)
UpdateInterceptSettings(context.Context, *connect.Request[UpdateInterceptSettingsRequest]) (*connect.Response[UpdateInterceptSettingsResponse], error)
SetScopeRules(context.Context, *connect.Request[SetScopeRulesRequest]) (*connect.Response[SetScopeRulesResponse], error)
SetRequestLogsFilter(context.Context, *connect.Request[SetRequestLogsFilterRequest]) (*connect.Response[SetRequestLogsFilterResponse], error)
}
// NewProjectServiceClient constructs a client for the hetty.proj.v1.ProjectService service. By
// default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses,
// and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the
// connect.WithGRPC() or connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewProjectServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ProjectServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
projectServiceMethods := File_proj_proj_proto.Services().ByName("ProjectService").Methods()
return &projectServiceClient{
createProject: connect.NewClient[CreateProjectRequest, CreateProjectResponse](
httpClient,
baseURL+ProjectServiceCreateProjectProcedure,
connect.WithSchema(projectServiceMethods.ByName("CreateProject")),
connect.WithClientOptions(opts...),
),
openProject: connect.NewClient[OpenProjectRequest, OpenProjectResponse](
httpClient,
baseURL+ProjectServiceOpenProjectProcedure,
connect.WithSchema(projectServiceMethods.ByName("OpenProject")),
connect.WithClientOptions(opts...),
),
closeProject: connect.NewClient[CloseProjectRequest, CloseProjectResponse](
httpClient,
baseURL+ProjectServiceCloseProjectProcedure,
connect.WithSchema(projectServiceMethods.ByName("CloseProject")),
connect.WithClientOptions(opts...),
),
deleteProject: connect.NewClient[DeleteProjectRequest, DeleteProjectResponse](
httpClient,
baseURL+ProjectServiceDeleteProjectProcedure,
connect.WithSchema(projectServiceMethods.ByName("DeleteProject")),
connect.WithClientOptions(opts...),
),
getActiveProject: connect.NewClient[GetActiveProjectRequest, GetActiveProjectResponse](
httpClient,
baseURL+ProjectServiceGetActiveProjectProcedure,
connect.WithSchema(projectServiceMethods.ByName("GetActiveProject")),
connect.WithClientOptions(opts...),
),
listProjects: connect.NewClient[ListProjectsRequest, ListProjectsResponse](
httpClient,
baseURL+ProjectServiceListProjectsProcedure,
connect.WithSchema(projectServiceMethods.ByName("ListProjects")),
connect.WithClientOptions(opts...),
),
updateInterceptSettings: connect.NewClient[UpdateInterceptSettingsRequest, UpdateInterceptSettingsResponse](
httpClient,
baseURL+ProjectServiceUpdateInterceptSettingsProcedure,
connect.WithSchema(projectServiceMethods.ByName("UpdateInterceptSettings")),
connect.WithClientOptions(opts...),
),
setScopeRules: connect.NewClient[SetScopeRulesRequest, SetScopeRulesResponse](
httpClient,
baseURL+ProjectServiceSetScopeRulesProcedure,
connect.WithSchema(projectServiceMethods.ByName("SetScopeRules")),
connect.WithClientOptions(opts...),
),
setRequestLogsFilter: connect.NewClient[SetRequestLogsFilterRequest, SetRequestLogsFilterResponse](
httpClient,
baseURL+ProjectServiceSetRequestLogsFilterProcedure,
connect.WithSchema(projectServiceMethods.ByName("SetRequestLogsFilter")),
connect.WithClientOptions(opts...),
),
}
}
// projectServiceClient implements ProjectServiceClient.
type projectServiceClient struct {
createProject *connect.Client[CreateProjectRequest, CreateProjectResponse]
openProject *connect.Client[OpenProjectRequest, OpenProjectResponse]
closeProject *connect.Client[CloseProjectRequest, CloseProjectResponse]
deleteProject *connect.Client[DeleteProjectRequest, DeleteProjectResponse]
getActiveProject *connect.Client[GetActiveProjectRequest, GetActiveProjectResponse]
listProjects *connect.Client[ListProjectsRequest, ListProjectsResponse]
updateInterceptSettings *connect.Client[UpdateInterceptSettingsRequest, UpdateInterceptSettingsResponse]
setScopeRules *connect.Client[SetScopeRulesRequest, SetScopeRulesResponse]
setRequestLogsFilter *connect.Client[SetRequestLogsFilterRequest, SetRequestLogsFilterResponse]
}
// CreateProject calls hetty.proj.v1.ProjectService.CreateProject.
func (c *projectServiceClient) CreateProject(ctx context.Context, req *connect.Request[CreateProjectRequest]) (*connect.Response[CreateProjectResponse], error) {
return c.createProject.CallUnary(ctx, req)
}
// OpenProject calls hetty.proj.v1.ProjectService.OpenProject.
func (c *projectServiceClient) OpenProject(ctx context.Context, req *connect.Request[OpenProjectRequest]) (*connect.Response[OpenProjectResponse], error) {
return c.openProject.CallUnary(ctx, req)
}
// CloseProject calls hetty.proj.v1.ProjectService.CloseProject.
func (c *projectServiceClient) CloseProject(ctx context.Context, req *connect.Request[CloseProjectRequest]) (*connect.Response[CloseProjectResponse], error) {
return c.closeProject.CallUnary(ctx, req)
}
// DeleteProject calls hetty.proj.v1.ProjectService.DeleteProject.
func (c *projectServiceClient) DeleteProject(ctx context.Context, req *connect.Request[DeleteProjectRequest]) (*connect.Response[DeleteProjectResponse], error) {
return c.deleteProject.CallUnary(ctx, req)
}
// GetActiveProject calls hetty.proj.v1.ProjectService.GetActiveProject.
func (c *projectServiceClient) GetActiveProject(ctx context.Context, req *connect.Request[GetActiveProjectRequest]) (*connect.Response[GetActiveProjectResponse], error) {
return c.getActiveProject.CallUnary(ctx, req)
}
// ListProjects calls hetty.proj.v1.ProjectService.ListProjects.
func (c *projectServiceClient) ListProjects(ctx context.Context, req *connect.Request[ListProjectsRequest]) (*connect.Response[ListProjectsResponse], error) {
return c.listProjects.CallUnary(ctx, req)
}
// UpdateInterceptSettings calls hetty.proj.v1.ProjectService.UpdateInterceptSettings.
func (c *projectServiceClient) UpdateInterceptSettings(ctx context.Context, req *connect.Request[UpdateInterceptSettingsRequest]) (*connect.Response[UpdateInterceptSettingsResponse], error) {
return c.updateInterceptSettings.CallUnary(ctx, req)
}
// SetScopeRules calls hetty.proj.v1.ProjectService.SetScopeRules.
func (c *projectServiceClient) SetScopeRules(ctx context.Context, req *connect.Request[SetScopeRulesRequest]) (*connect.Response[SetScopeRulesResponse], error) {
return c.setScopeRules.CallUnary(ctx, req)
}
// SetRequestLogsFilter calls hetty.proj.v1.ProjectService.SetRequestLogsFilter.
func (c *projectServiceClient) SetRequestLogsFilter(ctx context.Context, req *connect.Request[SetRequestLogsFilterRequest]) (*connect.Response[SetRequestLogsFilterResponse], error) {
return c.setRequestLogsFilter.CallUnary(ctx, req)
}
// ProjectServiceHandler is an implementation of the hetty.proj.v1.ProjectService service.
type ProjectServiceHandler interface {
CreateProject(context.Context, *connect.Request[CreateProjectRequest]) (*connect.Response[CreateProjectResponse], error)
OpenProject(context.Context, *connect.Request[OpenProjectRequest]) (*connect.Response[OpenProjectResponse], error)
CloseProject(context.Context, *connect.Request[CloseProjectRequest]) (*connect.Response[CloseProjectResponse], error)
DeleteProject(context.Context, *connect.Request[DeleteProjectRequest]) (*connect.Response[DeleteProjectResponse], error)
GetActiveProject(context.Context, *connect.Request[GetActiveProjectRequest]) (*connect.Response[GetActiveProjectResponse], error)
ListProjects(context.Context, *connect.Request[ListProjectsRequest]) (*connect.Response[ListProjectsResponse], error)
UpdateInterceptSettings(context.Context, *connect.Request[UpdateInterceptSettingsRequest]) (*connect.Response[UpdateInterceptSettingsResponse], error)
SetScopeRules(context.Context, *connect.Request[SetScopeRulesRequest]) (*connect.Response[SetScopeRulesResponse], error)
SetRequestLogsFilter(context.Context, *connect.Request[SetRequestLogsFilterRequest]) (*connect.Response[SetRequestLogsFilterResponse], error)
}
// NewProjectServiceHandler builds an HTTP handler from the service implementation. It returns the
// path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewProjectServiceHandler(svc ProjectServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
projectServiceMethods := File_proj_proj_proto.Services().ByName("ProjectService").Methods()
projectServiceCreateProjectHandler := connect.NewUnaryHandler(
ProjectServiceCreateProjectProcedure,
svc.CreateProject,
connect.WithSchema(projectServiceMethods.ByName("CreateProject")),
connect.WithHandlerOptions(opts...),
)
projectServiceOpenProjectHandler := connect.NewUnaryHandler(
ProjectServiceOpenProjectProcedure,
svc.OpenProject,
connect.WithSchema(projectServiceMethods.ByName("OpenProject")),
connect.WithHandlerOptions(opts...),
)
projectServiceCloseProjectHandler := connect.NewUnaryHandler(
ProjectServiceCloseProjectProcedure,
svc.CloseProject,
connect.WithSchema(projectServiceMethods.ByName("CloseProject")),
connect.WithHandlerOptions(opts...),
)
projectServiceDeleteProjectHandler := connect.NewUnaryHandler(
ProjectServiceDeleteProjectProcedure,
svc.DeleteProject,
connect.WithSchema(projectServiceMethods.ByName("DeleteProject")),
connect.WithHandlerOptions(opts...),
)
projectServiceGetActiveProjectHandler := connect.NewUnaryHandler(
ProjectServiceGetActiveProjectProcedure,
svc.GetActiveProject,
connect.WithSchema(projectServiceMethods.ByName("GetActiveProject")),
connect.WithHandlerOptions(opts...),
)
projectServiceListProjectsHandler := connect.NewUnaryHandler(
ProjectServiceListProjectsProcedure,
svc.ListProjects,
connect.WithSchema(projectServiceMethods.ByName("ListProjects")),
connect.WithHandlerOptions(opts...),
)
projectServiceUpdateInterceptSettingsHandler := connect.NewUnaryHandler(
ProjectServiceUpdateInterceptSettingsProcedure,
svc.UpdateInterceptSettings,
connect.WithSchema(projectServiceMethods.ByName("UpdateInterceptSettings")),
connect.WithHandlerOptions(opts...),
)
projectServiceSetScopeRulesHandler := connect.NewUnaryHandler(
ProjectServiceSetScopeRulesProcedure,
svc.SetScopeRules,
connect.WithSchema(projectServiceMethods.ByName("SetScopeRules")),
connect.WithHandlerOptions(opts...),
)
projectServiceSetRequestLogsFilterHandler := connect.NewUnaryHandler(
ProjectServiceSetRequestLogsFilterProcedure,
svc.SetRequestLogsFilter,
connect.WithSchema(projectServiceMethods.ByName("SetRequestLogsFilter")),
connect.WithHandlerOptions(opts...),
)
return "/hetty.proj.v1.ProjectService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case ProjectServiceCreateProjectProcedure:
projectServiceCreateProjectHandler.ServeHTTP(w, r)
case ProjectServiceOpenProjectProcedure:
projectServiceOpenProjectHandler.ServeHTTP(w, r)
case ProjectServiceCloseProjectProcedure:
projectServiceCloseProjectHandler.ServeHTTP(w, r)
case ProjectServiceDeleteProjectProcedure:
projectServiceDeleteProjectHandler.ServeHTTP(w, r)
case ProjectServiceGetActiveProjectProcedure:
projectServiceGetActiveProjectHandler.ServeHTTP(w, r)
case ProjectServiceListProjectsProcedure:
projectServiceListProjectsHandler.ServeHTTP(w, r)
case ProjectServiceUpdateInterceptSettingsProcedure:
projectServiceUpdateInterceptSettingsHandler.ServeHTTP(w, r)
case ProjectServiceSetScopeRulesProcedure:
projectServiceSetScopeRulesHandler.ServeHTTP(w, r)
case ProjectServiceSetRequestLogsFilterProcedure:
projectServiceSetRequestLogsFilterHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedProjectServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedProjectServiceHandler struct{}
func (UnimplementedProjectServiceHandler) CreateProject(context.Context, *connect.Request[CreateProjectRequest]) (*connect.Response[CreateProjectResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.CreateProject is not implemented"))
}
func (UnimplementedProjectServiceHandler) OpenProject(context.Context, *connect.Request[OpenProjectRequest]) (*connect.Response[OpenProjectResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.OpenProject is not implemented"))
}
func (UnimplementedProjectServiceHandler) CloseProject(context.Context, *connect.Request[CloseProjectRequest]) (*connect.Response[CloseProjectResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.CloseProject is not implemented"))
}
func (UnimplementedProjectServiceHandler) DeleteProject(context.Context, *connect.Request[DeleteProjectRequest]) (*connect.Response[DeleteProjectResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.DeleteProject is not implemented"))
}
func (UnimplementedProjectServiceHandler) GetActiveProject(context.Context, *connect.Request[GetActiveProjectRequest]) (*connect.Response[GetActiveProjectResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.GetActiveProject is not implemented"))
}
func (UnimplementedProjectServiceHandler) ListProjects(context.Context, *connect.Request[ListProjectsRequest]) (*connect.Response[ListProjectsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.ListProjects is not implemented"))
}
func (UnimplementedProjectServiceHandler) UpdateInterceptSettings(context.Context, *connect.Request[UpdateInterceptSettingsRequest]) (*connect.Response[UpdateInterceptSettingsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.UpdateInterceptSettings is not implemented"))
}
func (UnimplementedProjectServiceHandler) SetScopeRules(context.Context, *connect.Request[SetScopeRulesRequest]) (*connect.Response[SetScopeRulesResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.SetScopeRules is not implemented"))
}
func (UnimplementedProjectServiceHandler) SetRequestLogsFilter(context.Context, *connect.Request[SetRequestLogsFilterRequest]) (*connect.Response[SetRequestLogsFilterResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.proj.v1.ProjectService.SetRequestLogsFilter is not implemented"))
}

View File

@ -4,72 +4,44 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math/rand"
"regexp" "regexp"
"sync" "sync"
"time"
"github.com/oklog/ulid" connect "connectrpc.com/connect"
"github.com/oklog/ulid/v2"
"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"
) )
//nolint:gosec type Service struct {
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
// Service is used for managing projects.
type Service interface {
CreateProject(ctx context.Context, name string) (Project, error)
OpenProject(ctx context.Context, projectID ulid.ULID) (Project, error)
CloseProject() error
DeleteProject(ctx context.Context, projectID ulid.ULID) error
ActiveProject(ctx context.Context) (Project, error)
IsProjectActive(projectID ulid.ULID) bool
Projects(ctx context.Context) ([]Project, error)
Scope() *scope.Scope
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 repo Repository
interceptSvc *intercept.Service interceptSvc *intercept.Service
reqLogSvc reqlog.Service reqLogSvc *reqlog.Service
senderSvc sender.Service senderSvc *sender.Service
scope *scope.Scope scope *scope.Scope
activeProjectID ulid.ULID activeProjectID string
mu sync.RWMutex mu sync.RWMutex
} }
type Project struct {
ID ulid.ULID
Name string
Settings Settings
isActive bool
}
type Settings struct { 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
@ -87,14 +59,14 @@ var nameRegexp = regexp.MustCompile(`^[\w\d\s]+$`)
type Config struct { type Config struct {
Repository Repository Repository Repository
InterceptService *intercept.Service InterceptService *intercept.Service
ReqLogService reqlog.Service ReqLogService *reqlog.Service
SenderService sender.Service SenderService *sender.Service
Scope *scope.Scope Scope *scope.Scope
} }
// NewService returns a new Service. // NewService returns a new Service.
func NewService(cfg Config) (Service, error) { func NewService(cfg Config) (*Service, error) {
return &service{ return &Service{
repo: cfg.Repository, repo: cfg.Repository,
interceptSvc: cfg.InterceptService, interceptSvc: cfg.InterceptService,
reqLogSvc: cfg.ReqLogService, reqLogSvc: cfg.ReqLogService,
@ -103,216 +75,324 @@ func NewService(cfg Config) (Service, error) {
}, nil }, nil
} }
func (svc *service) CreateProject(ctx context.Context, name string) (Project, error) { func (svc *Service) CreateProject(ctx context.Context, req *connect.Request[CreateProjectRequest]) (*connect.Response[CreateProjectResponse], error) {
if !nameRegexp.MatchString(name) { if !nameRegexp.MatchString(req.Msg.Name) {
return Project{}, ErrInvalidName return nil, ErrInvalidName
} }
project := Project{ project := &Project{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), Id: ulid.Make().String(),
Name: name, Name: req.Msg.Name,
} }
err := svc.repo.UpsertProject(ctx, project) err := svc.repo.UpsertProject(ctx, project)
if err != nil { if err != nil {
return Project{}, fmt.Errorf("proj: could not create project: %w", err) return nil, fmt.Errorf("proj: could not create project: %w", err)
} }
return project, nil return &connect.Response[CreateProjectResponse]{
Msg: &CreateProjectResponse{
Project: project,
},
}, nil
} }
// CloseProject closes the currently open project (if there is one). // CloseProject closes the currently open project (if there is one).
func (svc *service) CloseProject() error { func (svc *Service) CloseProject(ctx context.Context, _ *connect.Request[CloseProjectRequest]) (*connect.Response[CloseProjectResponse], error) {
svc.mu.Lock() svc.mu.Lock()
defer svc.mu.Unlock() defer svc.mu.Unlock()
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 { if svc.activeProjectID == "" {
return nil return nil, connect.NewError(connect.CodeFailedPrecondition, ErrNoProject)
} }
svc.activeProjectID = ulid.ULID{} svc.activeProjectID = ""
svc.reqLogSvc.SetActiveProjectID(ulid.ULID{}) svc.reqLogSvc.SetActiveProjectID("")
svc.reqLogSvc.SetBypassOutOfScopeRequests(false) svc.reqLogSvc.SetBypassOutOfScopeRequests(false)
svc.reqLogSvc.SetFindReqsFilter(reqlog.FindRequestsFilter{}) svc.reqLogSvc.SetRequestLogsFilter(nil)
svc.interceptSvc.UpdateSettings(intercept.Settings{ svc.interceptSvc.UpdateSettings(intercept.Settings{
RequestsEnabled: false, RequestsEnabled: false,
ResponsesEnabled: false, ResponsesEnabled: false,
RequestFilter: nil, RequestFilter: nil,
ResponseFilter: nil, ResponseFilter: nil,
}) })
svc.senderSvc.SetActiveProjectID(ulid.ULID{}) svc.senderSvc.SetActiveProjectID("")
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
svc.scope.SetRules(nil) svc.scope.SetRules(nil)
return nil return &connect.Response[CloseProjectResponse]{}, nil
} }
// DeleteProject removes a project from the repository. // DeleteProject removes a project from the repository.
func (svc *service) DeleteProject(ctx context.Context, projectID ulid.ULID) error { func (svc *Service) DeleteProject(ctx context.Context, req *connect.Request[DeleteProjectRequest]) (*connect.Response[DeleteProjectResponse], error) {
if svc.activeProjectID.Compare(projectID) == 0 { if svc.activeProjectID == "" {
return fmt.Errorf("proj: project (%v) is active", projectID.String()) return nil, connect.NewError(connect.CodeFailedPrecondition, ErrNoProject)
} }
if err := svc.repo.DeleteProject(ctx, projectID); err != nil { if err := svc.repo.DeleteProject(ctx, req.Msg.ProjectId); err != nil {
return fmt.Errorf("proj: could not delete project: %w", err) return nil, fmt.Errorf("proj: could not delete project: %w", err)
} }
return nil return &connect.Response[DeleteProjectResponse]{}, nil
} }
// OpenProject sets a project as the currently active project. // OpenProject sets a project as the currently active project.
func (svc *service) OpenProject(ctx context.Context, projectID ulid.ULID) (Project, error) { func (svc *Service) OpenProject(ctx context.Context, req *connect.Request[OpenProjectRequest]) (*connect.Response[OpenProjectResponse], error) {
svc.mu.Lock() svc.mu.Lock()
defer svc.mu.Unlock() defer svc.mu.Unlock()
project, err := svc.repo.FindProjectByID(ctx, projectID) p, err := svc.repo.FindProjectByID(ctx, req.Msg.ProjectId)
if errors.Is(err, ErrProjectNotFound) {
return nil, connect.NewError(connect.CodeNotFound, ErrProjectNotFound)
}
if err != nil { if err != nil {
return Project{}, fmt.Errorf("proj: failed to get project: %w", err) return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to find project: %w", err))
} }
svc.activeProjectID = project.ID svc.activeProjectID = p.Id
interceptSettings := intercept.Settings{
RequestsEnabled: p.InterceptRequests,
ResponsesEnabled: p.InterceptResponses,
}
if p.InterceptRequestFilterExpr != "" {
expr, err := filter.ParseQuery(p.InterceptRequestFilterExpr)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to parse intercept request filter: %w", err))
}
interceptSettings.RequestFilter = expr
}
if p.InterceptResponseFilterExpr != "" {
expr, err := filter.ParseQuery(p.InterceptResponseFilterExpr)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to parse intercept response filter: %w", err))
}
interceptSettings.ResponseFilter = expr
}
// Request log settings. // Request log settings.
svc.reqLogSvc.SetFindReqsFilter(reqlog.FindRequestsFilter{ svc.reqLogSvc.SetActiveProjectID(p.Id)
ProjectID: project.ID, svc.reqLogSvc.SetBypassOutOfScopeRequests(p.ReqLogBypassOutOfScope)
OnlyInScope: project.Settings.ReqLogOnlyFindInScope, svc.reqLogSvc.SetRequestLogsFilter(p.ReqLogFilter)
SearchExpr: project.Settings.ReqLogSearchExpr,
})
svc.reqLogSvc.SetBypassOutOfScopeRequests(project.Settings.ReqLogBypassOutOfScope)
svc.reqLogSvc.SetActiveProjectID(project.ID)
// Intercept settings. // Intercept settings.
svc.interceptSvc.UpdateSettings(intercept.Settings{ svc.interceptSvc.UpdateSettings(interceptSettings)
RequestsEnabled: project.Settings.InterceptRequests,
ResponsesEnabled: project.Settings.InterceptResponses,
RequestFilter: project.Settings.InterceptRequestFilter,
ResponseFilter: project.Settings.InterceptResponseFilter,
})
// Sender settings. // Sender settings.
svc.senderSvc.SetActiveProjectID(project.ID) svc.senderSvc.SetActiveProjectID(p.Id)
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{ svc.senderSvc.SetRequestsFilter(&sender.RequestsFilter{
ProjectID: project.ID, OnlyInScope: p.SenderOnlyFindInScope,
OnlyInScope: project.Settings.SenderOnlyFindInScope, SearchExpr: p.SenderSearchExpr,
SearchExpr: project.Settings.SenderSearchExpr,
}) })
// Scope settings. // Scope settings.
svc.scope.SetRules(project.Settings.ScopeRules) scopeRules, err := p.ParseScopeRules()
return project, nil
}
func (svc *service) ActiveProject(ctx context.Context) (Project, error) {
activeProjectID := svc.activeProjectID
if activeProjectID.Compare(ulid.ULID{}) == 0 {
return Project{}, ErrNoProject
}
project, err := svc.repo.FindProjectByID(ctx, activeProjectID)
if err != nil { if err != nil {
return Project{}, fmt.Errorf("proj: failed to get active project: %w", err) return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse scope rules: %w", err))
}
svc.scope.SetRules(scopeRules)
p.IsActive = true
return &connect.Response[OpenProjectResponse]{
Msg: &OpenProjectResponse{
Project: p,
},
}, nil
}
func (svc *Service) GetActiveProject(ctx context.Context, _ *connect.Request[GetActiveProjectRequest]) (*connect.Response[GetActiveProjectResponse], error) {
project, err := svc.activeProject(ctx)
if errors.Is(err, ErrNoProject) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
} }
project.isActive = true project.IsActive = true
return &connect.Response[GetActiveProjectResponse]{
Msg: &GetActiveProjectResponse{
Project: project,
},
}, nil
}
func (svc *Service) activeProject(ctx context.Context) (*Project, error) {
if svc.activeProjectID == "" {
return nil, ErrNoProject
}
project, err := svc.repo.FindProjectByID(ctx, svc.activeProjectID)
if err != nil {
return nil, fmt.Errorf("proj: failed to get active project: %w", err)
}
return project, nil return project, nil
} }
func (svc *service) Projects(ctx context.Context) ([]Project, error) { func (svc *Service) ListProjects(ctx context.Context, _ *connect.Request[ListProjectsRequest]) (*connect.Response[ListProjectsResponse], error) {
projects, err := svc.repo.Projects(ctx) projects, err := svc.repo.Projects(ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("proj: could not get projects: %w", err) return nil, fmt.Errorf("proj: could not get projects: %w", err)
} }
return projects, nil for _, project := range projects {
if svc.IsProjectActive(project.Id) {
project.IsActive = true
}
}
return &connect.Response[ListProjectsResponse]{
Msg: &ListProjectsResponse{
Projects: projects,
},
}, nil
} }
func (svc *service) Scope() *scope.Scope { func (svc *Service) Scope() *scope.Scope {
return svc.scope return svc.scope
} }
func (svc *service) SetScopeRules(ctx context.Context, rules []scope.Rule) error { func (svc *Service) SetScopeRules(ctx context.Context, req *connect.Request[SetScopeRulesRequest]) (*connect.Response[SetScopeRulesResponse], error) {
project, err := svc.ActiveProject(ctx) p, err := svc.activeProject(ctx)
if errors.Is(err, ErrNoProject) {
return nil, connect.NewError(connect.CodeFailedPrecondition, err)
}
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
p.ScopeRules = req.Msg.Rules
err = svc.repo.UpsertProject(ctx, p)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to update project: %w", err))
}
scopeRules, err := p.ParseScopeRules()
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse scope rules: %w", err))
}
svc.scope.SetRules(scopeRules)
return &connect.Response[SetScopeRulesResponse]{}, nil
}
func (p *Project) ParseScopeRules() ([]scope.Rule, error) {
var err error
scopeRules := make([]scope.Rule, len(p.ScopeRules))
for i, rule := range p.ScopeRules {
scopeRules[i] = scope.Rule{}
if rule.UrlRegexp != "" {
scopeRules[i].URL, err = regexp.Compile(rule.UrlRegexp)
if err != nil {
return nil, fmt.Errorf("failed to parse scope rule's URL field: %w", err)
}
}
if rule.HeaderKeyRegexp != "" {
scopeRules[i].Header.Key, err = regexp.Compile(rule.HeaderKeyRegexp)
if err != nil {
return nil, fmt.Errorf("failed to parse scope rule's header key field: %w", err)
}
}
if rule.HeaderValueRegexp != "" {
scopeRules[i].Header.Value, err = regexp.Compile(rule.HeaderValueRegexp)
if err != nil {
return nil, fmt.Errorf("failed to parse scope rule's header value field: %w", err)
}
}
if rule.BodyRegexp != "" {
scopeRules[i].Body, err = regexp.Compile(rule.BodyRegexp)
if err != nil {
return nil, fmt.Errorf("failed to parse scope rule's body field: %w", err)
}
}
}
return scopeRules, nil
}
func (svc *Service) SetRequestLogsFilter(ctx context.Context, req *connect.Request[SetRequestLogsFilterRequest]) (*connect.Response[SetRequestLogsFilterResponse], error) {
project, err := svc.activeProject(ctx)
if errors.Is(err, ErrNoProject) {
return nil, connect.NewError(connect.CodeFailedPrecondition, err)
}
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
project.ReqLogFilter = req.Msg.Filter
err = svc.repo.UpsertProject(ctx, project)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to update project: %w", err))
}
svc.reqLogSvc.SetRequestLogsFilter(req.Msg.Filter)
return &connect.Response[SetRequestLogsFilterResponse]{}, nil
}
func (svc *Service) SetSenderRequestFindFilter(ctx context.Context, filter *sender.RequestsFilter) error {
project, err := svc.activeProject(ctx)
if err != nil { if err != nil {
return err return err
} }
project.Settings.ScopeRules = rules project.SenderOnlyFindInScope = filter.OnlyInScope
project.SenderSearchExpr = filter.SearchExpr
err = svc.repo.UpsertProject(ctx, project) err = svc.repo.UpsertProject(ctx, project)
if err != nil { if err != nil {
return fmt.Errorf("proj: failed to update project: %w", err) return fmt.Errorf("proj: failed to update project: %w", err)
} }
svc.scope.SetRules(rules) svc.senderSvc.SetRequestsFilter(filter)
return nil return nil
} }
func (svc *service) SetRequestLogFindFilter(ctx context.Context, filter reqlog.FindRequestsFilter) error { func (svc *Service) IsProjectActive(projectID string) bool {
project, err := svc.ActiveProject(ctx) return projectID == svc.activeProjectID
}
func (svc *Service) UpdateInterceptSettings(ctx context.Context, req *connect.Request[UpdateInterceptSettingsRequest]) (*connect.Response[UpdateInterceptSettingsResponse], error) {
project, err := svc.activeProject(ctx)
if err != nil { if err != nil {
return err return nil, err
} }
filter.ProjectID = project.ID project.InterceptRequests = req.Msg.RequestsEnabled
project.InterceptResponses = req.Msg.ResponsesEnabled
project.Settings.ReqLogOnlyFindInScope = filter.OnlyInScope project.InterceptRequestFilterExpr = req.Msg.RequestFilterExpr
project.Settings.ReqLogSearchExpr = filter.SearchExpr project.InterceptResponseFilterExpr = req.Msg.ResponseFilterExpr
err = svc.repo.UpsertProject(ctx, project) err = svc.repo.UpsertProject(ctx, project)
if err != nil { if err != nil {
return fmt.Errorf("proj: failed to update project: %w", err) return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("proj: failed to update project: %w", err))
} }
svc.reqLogSvc.SetFindReqsFilter(filter) reqFilterExpr, err := filter.ParseQuery(req.Msg.RequestFilterExpr)
return nil
}
func (svc *service) SetSenderRequestFindFilter(ctx context.Context, filter sender.FindRequestsFilter) error {
project, err := svc.ActiveProject(ctx)
if err != nil { if err != nil {
return err return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse intercept request filter: %w", err))
} }
filter.ProjectID = project.ID respFilterExpr, err := filter.ParseQuery(req.Msg.ResponseFilterExpr)
project.Settings.SenderOnlyFindInScope = filter.OnlyInScope
project.Settings.SenderSearchExpr = filter.SearchExpr
err = svc.repo.UpsertProject(ctx, project)
if err != nil { if err != nil {
return fmt.Errorf("proj: failed to update project: %w", err) return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("proj: failed to parse intercept response filter: %w", err))
} }
svc.senderSvc.SetFindReqsFilter(filter) svc.interceptSvc.UpdateSettings(intercept.Settings{
RequestsEnabled: req.Msg.RequestsEnabled,
ResponsesEnabled: req.Msg.ResponsesEnabled,
RequestFilter: reqFilterExpr,
ResponseFilter: respFilterExpr,
})
return nil return &connect.Response[UpdateInterceptSettingsResponse]{}, nil
}
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
} }

1181
pkg/proj/proj.pb.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,14 +2,12 @@ package proj
import ( import (
"context" "context"
"github.com/oklog/ulid"
) )
type Repository interface { type Repository interface {
FindProjectByID(ctx context.Context, id ulid.ULID) (Project, error) FindProjectByID(ctx context.Context, id string) (*Project, error)
UpsertProject(ctx context.Context, project Project) error UpsertProject(ctx context.Context, project *Project) error
DeleteProject(ctx context.Context, id ulid.ULID) error DeleteProject(ctx context.Context, id string) error
Projects(ctx context.Context) ([]Project, error) Projects(ctx context.Context) ([]*Project, error)
Close() error Close() error
} }

View File

@ -5,13 +5,13 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"github.com/dstotijn/hetty/pkg/filter"
httppb "github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/scope" "github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
) )
//nolint:unparam //nolint:unparam
@ -34,7 +34,7 @@ var reqFilterKeyFns = map[string]func(req *http.Request) (string, error){
return "", err return "", err
} }
req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) req.Body = io.NopCloser(bytes.NewBuffer(body))
return string(body), nil return string(body), nil
}, },
} }
@ -61,29 +61,29 @@ var resFilterKeyFns = map[string]func(res *http.Response) (string, error){
return "", err return "", err
} }
res.Body = ioutil.NopCloser(bytes.NewBuffer(body)) res.Body = io.NopCloser(bytes.NewBuffer(body))
return string(body), nil return string(body), nil
}, },
} }
// 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, httppb.ParseHeader(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 {
@ -247,7 +267,7 @@ func MatchRequestScope(req *http.Request, s *scope.Scope) (bool, error) {
return false, fmt.Errorf("failed to read request body: %w", err) return false, fmt.Errorf("failed to read request body: %w", err)
} }
req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) req.Body = io.NopCloser(bytes.NewBuffer(body))
if matches := rule.Body.Match(body); matches { if matches := rule.Body.Match(body); matches {
return true, nil return true, 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, httppb.ParseHeader(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 {

View File

@ -8,11 +8,11 @@ import (
"sort" "sort"
"sync" "sync"
"github.com/oklog/ulid" "github.com/oklog/ulid/v2"
"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.

View File

@ -3,23 +3,19 @@ package intercept_test
import ( import (
"context" "context"
"errors" "errors"
"math/rand"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"sync" "sync"
"testing" "testing"
"time" "time"
"github.com/oklog/ulid" "github.com/oklog/ulid/v2"
"go.uber.org/zap" "go.uber.org/zap"
"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"
) )
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
func TestRequestModifier(t *testing.T) { func TestRequestModifier(t *testing.T) {
t.Parallel() t.Parallel()
@ -33,7 +29,7 @@ func TestRequestModifier(t *testing.T) {
ResponsesEnabled: false, ResponsesEnabled: false,
}) })
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) reqID := ulid.Make()
err := svc.ModifyRequest(reqID, nil, nil) err := svc.ModifyRequest(reqID, nil, nil)
if !errors.Is(err, intercept.ErrRequestNotFound) { if !errors.Is(err, intercept.ErrRequestNotFound) {
@ -55,7 +51,7 @@ func TestRequestModifier(t *testing.T) {
defer cancel() defer cancel()
req := httptest.NewRequest("GET", "https://example.com/foo", nil) req := httptest.NewRequest("GET", "https://example.com/foo", nil)
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) reqID := ulid.Make()
*req = *req.WithContext(ctx) *req = *req.WithContext(ctx)
*req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID)) *req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID))
@ -82,7 +78,7 @@ func TestRequestModifier(t *testing.T) {
req := httptest.NewRequest("GET", "https://example.com/foo", nil) req := httptest.NewRequest("GET", "https://example.com/foo", nil)
req.Header.Set("X-Foo", "foo") req.Header.Set("X-Foo", "foo")
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) reqID := ulid.Make()
*req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID)) *req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID))
modReq := req.Clone(context.Background()) modReq := req.Clone(context.Background())
@ -143,7 +139,7 @@ func TestResponseModifier(t *testing.T) {
ResponsesEnabled: true, ResponsesEnabled: true,
}) })
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) reqID := ulid.Make()
err := svc.ModifyResponse(reqID, nil) err := svc.ModifyResponse(reqID, nil)
if !errors.Is(err, intercept.ErrRequestNotFound) { if !errors.Is(err, intercept.ErrRequestNotFound) {
@ -165,7 +161,7 @@ func TestResponseModifier(t *testing.T) {
defer cancel() defer cancel()
req := httptest.NewRequest("GET", "https://example.com/foo", nil) req := httptest.NewRequest("GET", "https://example.com/foo", nil)
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) reqID := ulid.Make()
*req = *req.WithContext(ctx) *req = *req.WithContext(ctx)
*req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID)) *req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID))
@ -212,7 +208,7 @@ func TestResponseModifier(t *testing.T) {
req := httptest.NewRequest("GET", "https://example.com/foo", nil) req := httptest.NewRequest("GET", "https://example.com/foo", nil)
req.Header.Set("X-Foo", "foo") req.Header.Set("X-Foo", "foo")
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) reqID := ulid.Make()
*req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID)) *req = *req.WithContext(proxy.WithRequestID(req.Context(), reqID))
res := &http.Response{ res := &http.Response{

View File

@ -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
} }

View File

@ -7,21 +7,17 @@ import (
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt" "fmt"
"math/rand"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"strings" "strings"
"time" "time"
"github.com/oklog/ulid" "github.com/oklog/ulid/v2"
"github.com/dstotijn/hetty/pkg/log" "github.com/dstotijn/hetty/pkg/log"
) )
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
type contextKey int type contextKey int
const reqIDKey contextKey = 0 const reqIDKey contextKey = 0
@ -95,7 +91,7 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) reqID := ulid.Make()
ctx := context.WithValue(r.Context(), reqIDKey, reqID) ctx := context.WithValue(r.Context(), reqIDKey, reqID)
*r = *r.WithContext(ctx) *r = *r.WithContext(ctx)

View File

@ -3,15 +3,13 @@ package reqlog
import ( import (
"context" "context"
"github.com/oklog/ulid" httppb "github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/scope"
) )
type Repository interface { type Repository interface {
FindRequestLogs(ctx context.Context, filter FindRequestsFilter, scope *scope.Scope) ([]RequestLog, error) FindRequestLogs(ctx context.Context, projectID string, filterFn func(*HttpRequestLog) (bool, error)) ([]*HttpRequestLog, error)
FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error) FindRequestLogByID(ctx context.Context, projectID, id string) (*HttpRequestLog, error)
StoreRequestLog(ctx context.Context, reqLog RequestLog) error StoreRequestLog(ctx context.Context, reqLog *HttpRequestLog) error
StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog ResponseLog) error StoreResponseLog(ctx context.Context, projectID, reqLogID string, resLog *httppb.Response) error
ClearRequestLogs(ctx context.Context, projectID ulid.ULID) error ClearRequestLogs(ctx context.Context, projectID string) error
} }

View File

@ -1,291 +0,0 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package reqlog_test
import (
"context"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/oklog/ulid"
"sync"
)
// Ensure, that RepoMock does implement reqlog.Repository.
// If this is not the case, regenerate this file with moq.
var _ reqlog.Repository = &RepoMock{}
// RepoMock is a mock implementation of reqlog.Repository.
//
// func TestSomethingThatUsesRepository(t *testing.T) {
//
// // make and configure a mocked reqlog.Repository
// mockedRepository := &RepoMock{
// ClearRequestLogsFunc: func(ctx context.Context, projectID ulid.ULID) error {
// panic("mock out the ClearRequestLogs method")
// },
// FindRequestLogByIDFunc: func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
// panic("mock out the FindRequestLogByID method")
// },
// FindRequestLogsFunc: func(ctx context.Context, filter reqlog.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]reqlog.RequestLog, error) {
// panic("mock out the FindRequestLogs method")
// },
// StoreRequestLogFunc: func(ctx context.Context, reqLog reqlog.RequestLog) error {
// panic("mock out the StoreRequestLog method")
// },
// StoreResponseLogFunc: func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
// panic("mock out the StoreResponseLog method")
// },
// }
//
// // use mockedRepository in code that requires reqlog.Repository
// // and then make assertions.
//
// }
type RepoMock struct {
// ClearRequestLogsFunc mocks the ClearRequestLogs method.
ClearRequestLogsFunc func(ctx context.Context, projectID ulid.ULID) error
// FindRequestLogByIDFunc mocks the FindRequestLogByID method.
FindRequestLogByIDFunc func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error)
// FindRequestLogsFunc mocks the FindRequestLogs method.
FindRequestLogsFunc func(ctx context.Context, filter reqlog.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]reqlog.RequestLog, error)
// StoreRequestLogFunc mocks the StoreRequestLog method.
StoreRequestLogFunc func(ctx context.Context, reqLog reqlog.RequestLog) error
// StoreResponseLogFunc mocks the StoreResponseLog method.
StoreResponseLogFunc func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error
// calls tracks calls to the methods.
calls struct {
// ClearRequestLogs holds details about calls to the ClearRequestLogs method.
ClearRequestLogs []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ProjectID is the projectID argument value.
ProjectID ulid.ULID
}
// FindRequestLogByID holds details about calls to the FindRequestLogByID method.
FindRequestLogByID []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ID is the id argument value.
ID ulid.ULID
}
// FindRequestLogs holds details about calls to the FindRequestLogs method.
FindRequestLogs []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Filter is the filter argument value.
Filter reqlog.FindRequestsFilter
// ScopeMoqParam is the scopeMoqParam argument value.
ScopeMoqParam *scope.Scope
}
// StoreRequestLog holds details about calls to the StoreRequestLog method.
StoreRequestLog []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ReqLog is the reqLog argument value.
ReqLog reqlog.RequestLog
}
// StoreResponseLog holds details about calls to the StoreResponseLog method.
StoreResponseLog []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ReqLogID is the reqLogID argument value.
ReqLogID ulid.ULID
// ResLog is the resLog argument value.
ResLog reqlog.ResponseLog
}
}
lockClearRequestLogs sync.RWMutex
lockFindRequestLogByID sync.RWMutex
lockFindRequestLogs sync.RWMutex
lockStoreRequestLog sync.RWMutex
lockStoreResponseLog sync.RWMutex
}
// ClearRequestLogs calls ClearRequestLogsFunc.
func (mock *RepoMock) ClearRequestLogs(ctx context.Context, projectID ulid.ULID) error {
if mock.ClearRequestLogsFunc == nil {
panic("RepoMock.ClearRequestLogsFunc: method is nil but Repository.ClearRequestLogs was just called")
}
callInfo := struct {
Ctx context.Context
ProjectID ulid.ULID
}{
Ctx: ctx,
ProjectID: projectID,
}
mock.lockClearRequestLogs.Lock()
mock.calls.ClearRequestLogs = append(mock.calls.ClearRequestLogs, callInfo)
mock.lockClearRequestLogs.Unlock()
return mock.ClearRequestLogsFunc(ctx, projectID)
}
// ClearRequestLogsCalls gets all the calls that were made to ClearRequestLogs.
// Check the length with:
// len(mockedRepository.ClearRequestLogsCalls())
func (mock *RepoMock) ClearRequestLogsCalls() []struct {
Ctx context.Context
ProjectID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ProjectID ulid.ULID
}
mock.lockClearRequestLogs.RLock()
calls = mock.calls.ClearRequestLogs
mock.lockClearRequestLogs.RUnlock()
return calls
}
// FindRequestLogByID calls FindRequestLogByIDFunc.
func (mock *RepoMock) FindRequestLogByID(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
if mock.FindRequestLogByIDFunc == nil {
panic("RepoMock.FindRequestLogByIDFunc: method is nil but Repository.FindRequestLogByID was just called")
}
callInfo := struct {
Ctx context.Context
ID ulid.ULID
}{
Ctx: ctx,
ID: id,
}
mock.lockFindRequestLogByID.Lock()
mock.calls.FindRequestLogByID = append(mock.calls.FindRequestLogByID, callInfo)
mock.lockFindRequestLogByID.Unlock()
return mock.FindRequestLogByIDFunc(ctx, id)
}
// FindRequestLogByIDCalls gets all the calls that were made to FindRequestLogByID.
// Check the length with:
// len(mockedRepository.FindRequestLogByIDCalls())
func (mock *RepoMock) FindRequestLogByIDCalls() []struct {
Ctx context.Context
ID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ID ulid.ULID
}
mock.lockFindRequestLogByID.RLock()
calls = mock.calls.FindRequestLogByID
mock.lockFindRequestLogByID.RUnlock()
return calls
}
// FindRequestLogs calls FindRequestLogsFunc.
func (mock *RepoMock) FindRequestLogs(ctx context.Context, filter reqlog.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]reqlog.RequestLog, error) {
if mock.FindRequestLogsFunc == nil {
panic("RepoMock.FindRequestLogsFunc: method is nil but Repository.FindRequestLogs was just called")
}
callInfo := struct {
Ctx context.Context
Filter reqlog.FindRequestsFilter
ScopeMoqParam *scope.Scope
}{
Ctx: ctx,
Filter: filter,
ScopeMoqParam: scopeMoqParam,
}
mock.lockFindRequestLogs.Lock()
mock.calls.FindRequestLogs = append(mock.calls.FindRequestLogs, callInfo)
mock.lockFindRequestLogs.Unlock()
return mock.FindRequestLogsFunc(ctx, filter, scopeMoqParam)
}
// FindRequestLogsCalls gets all the calls that were made to FindRequestLogs.
// Check the length with:
// len(mockedRepository.FindRequestLogsCalls())
func (mock *RepoMock) FindRequestLogsCalls() []struct {
Ctx context.Context
Filter reqlog.FindRequestsFilter
ScopeMoqParam *scope.Scope
} {
var calls []struct {
Ctx context.Context
Filter reqlog.FindRequestsFilter
ScopeMoqParam *scope.Scope
}
mock.lockFindRequestLogs.RLock()
calls = mock.calls.FindRequestLogs
mock.lockFindRequestLogs.RUnlock()
return calls
}
// StoreRequestLog calls StoreRequestLogFunc.
func (mock *RepoMock) StoreRequestLog(ctx context.Context, reqLog reqlog.RequestLog) error {
if mock.StoreRequestLogFunc == nil {
panic("RepoMock.StoreRequestLogFunc: method is nil but Repository.StoreRequestLog was just called")
}
callInfo := struct {
Ctx context.Context
ReqLog reqlog.RequestLog
}{
Ctx: ctx,
ReqLog: reqLog,
}
mock.lockStoreRequestLog.Lock()
mock.calls.StoreRequestLog = append(mock.calls.StoreRequestLog, callInfo)
mock.lockStoreRequestLog.Unlock()
return mock.StoreRequestLogFunc(ctx, reqLog)
}
// StoreRequestLogCalls gets all the calls that were made to StoreRequestLog.
// Check the length with:
// len(mockedRepository.StoreRequestLogCalls())
func (mock *RepoMock) StoreRequestLogCalls() []struct {
Ctx context.Context
ReqLog reqlog.RequestLog
} {
var calls []struct {
Ctx context.Context
ReqLog reqlog.RequestLog
}
mock.lockStoreRequestLog.RLock()
calls = mock.calls.StoreRequestLog
mock.lockStoreRequestLog.RUnlock()
return calls
}
// StoreResponseLog calls StoreResponseLogFunc.
func (mock *RepoMock) StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
if mock.StoreResponseLogFunc == nil {
panic("RepoMock.StoreResponseLogFunc: method is nil but Repository.StoreResponseLog was just called")
}
callInfo := struct {
Ctx context.Context
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
}{
Ctx: ctx,
ReqLogID: reqLogID,
ResLog: resLog,
}
mock.lockStoreResponseLog.Lock()
mock.calls.StoreResponseLog = append(mock.calls.StoreResponseLog, callInfo)
mock.lockStoreResponseLog.Unlock()
return mock.StoreResponseLogFunc(ctx, reqLogID, resLog)
}
// StoreResponseLogCalls gets all the calls that were made to StoreResponseLog.
// Check the length with:
// len(mockedRepository.StoreResponseLogCalls())
func (mock *RepoMock) StoreResponseLogCalls() []struct {
Ctx context.Context
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
} {
var calls []struct {
Ctx context.Context
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
}
mock.lockStoreResponseLog.RLock()
calls = mock.calls.StoreResponseLog
mock.lockStoreResponseLog.RUnlock()
return calls
}

View File

@ -0,0 +1,167 @@
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: reqlog/reqlog.proto
package reqlog
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// HttpRequestLogServiceName is the fully-qualified name of the HttpRequestLogService service.
HttpRequestLogServiceName = "hetty.reqlog.v1.HttpRequestLogService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// HttpRequestLogServiceGetHttpRequestLogProcedure is the fully-qualified name of the
// HttpRequestLogService's GetHttpRequestLog RPC.
HttpRequestLogServiceGetHttpRequestLogProcedure = "/hetty.reqlog.v1.HttpRequestLogService/GetHttpRequestLog"
// HttpRequestLogServiceListHttpRequestLogsProcedure is the fully-qualified name of the
// HttpRequestLogService's ListHttpRequestLogs RPC.
HttpRequestLogServiceListHttpRequestLogsProcedure = "/hetty.reqlog.v1.HttpRequestLogService/ListHttpRequestLogs"
// HttpRequestLogServiceClearHttpRequestLogsProcedure is the fully-qualified name of the
// HttpRequestLogService's ClearHttpRequestLogs RPC.
HttpRequestLogServiceClearHttpRequestLogsProcedure = "/hetty.reqlog.v1.HttpRequestLogService/ClearHttpRequestLogs"
)
// HttpRequestLogServiceClient is a client for the hetty.reqlog.v1.HttpRequestLogService service.
type HttpRequestLogServiceClient interface {
GetHttpRequestLog(context.Context, *connect.Request[GetHttpRequestLogRequest]) (*connect.Response[GetHttpRequestLogResponse], error)
ListHttpRequestLogs(context.Context, *connect.Request[ListHttpRequestLogsRequest]) (*connect.Response[ListHttpRequestLogsResponse], error)
ClearHttpRequestLogs(context.Context, *connect.Request[ClearHttpRequestLogsRequest]) (*connect.Response[ClearHttpRequestLogsResponse], error)
}
// NewHttpRequestLogServiceClient constructs a client for the hetty.reqlog.v1.HttpRequestLogService
// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
// the connect.WithGRPC() or connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewHttpRequestLogServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) HttpRequestLogServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
httpRequestLogServiceMethods := File_reqlog_reqlog_proto.Services().ByName("HttpRequestLogService").Methods()
return &httpRequestLogServiceClient{
getHttpRequestLog: connect.NewClient[GetHttpRequestLogRequest, GetHttpRequestLogResponse](
httpClient,
baseURL+HttpRequestLogServiceGetHttpRequestLogProcedure,
connect.WithSchema(httpRequestLogServiceMethods.ByName("GetHttpRequestLog")),
connect.WithClientOptions(opts...),
),
listHttpRequestLogs: connect.NewClient[ListHttpRequestLogsRequest, ListHttpRequestLogsResponse](
httpClient,
baseURL+HttpRequestLogServiceListHttpRequestLogsProcedure,
connect.WithSchema(httpRequestLogServiceMethods.ByName("ListHttpRequestLogs")),
connect.WithClientOptions(opts...),
),
clearHttpRequestLogs: connect.NewClient[ClearHttpRequestLogsRequest, ClearHttpRequestLogsResponse](
httpClient,
baseURL+HttpRequestLogServiceClearHttpRequestLogsProcedure,
connect.WithSchema(httpRequestLogServiceMethods.ByName("ClearHttpRequestLogs")),
connect.WithClientOptions(opts...),
),
}
}
// httpRequestLogServiceClient implements HttpRequestLogServiceClient.
type httpRequestLogServiceClient struct {
getHttpRequestLog *connect.Client[GetHttpRequestLogRequest, GetHttpRequestLogResponse]
listHttpRequestLogs *connect.Client[ListHttpRequestLogsRequest, ListHttpRequestLogsResponse]
clearHttpRequestLogs *connect.Client[ClearHttpRequestLogsRequest, ClearHttpRequestLogsResponse]
}
// GetHttpRequestLog calls hetty.reqlog.v1.HttpRequestLogService.GetHttpRequestLog.
func (c *httpRequestLogServiceClient) GetHttpRequestLog(ctx context.Context, req *connect.Request[GetHttpRequestLogRequest]) (*connect.Response[GetHttpRequestLogResponse], error) {
return c.getHttpRequestLog.CallUnary(ctx, req)
}
// ListHttpRequestLogs calls hetty.reqlog.v1.HttpRequestLogService.ListHttpRequestLogs.
func (c *httpRequestLogServiceClient) ListHttpRequestLogs(ctx context.Context, req *connect.Request[ListHttpRequestLogsRequest]) (*connect.Response[ListHttpRequestLogsResponse], error) {
return c.listHttpRequestLogs.CallUnary(ctx, req)
}
// ClearHttpRequestLogs calls hetty.reqlog.v1.HttpRequestLogService.ClearHttpRequestLogs.
func (c *httpRequestLogServiceClient) ClearHttpRequestLogs(ctx context.Context, req *connect.Request[ClearHttpRequestLogsRequest]) (*connect.Response[ClearHttpRequestLogsResponse], error) {
return c.clearHttpRequestLogs.CallUnary(ctx, req)
}
// HttpRequestLogServiceHandler is an implementation of the hetty.reqlog.v1.HttpRequestLogService
// service.
type HttpRequestLogServiceHandler interface {
GetHttpRequestLog(context.Context, *connect.Request[GetHttpRequestLogRequest]) (*connect.Response[GetHttpRequestLogResponse], error)
ListHttpRequestLogs(context.Context, *connect.Request[ListHttpRequestLogsRequest]) (*connect.Response[ListHttpRequestLogsResponse], error)
ClearHttpRequestLogs(context.Context, *connect.Request[ClearHttpRequestLogsRequest]) (*connect.Response[ClearHttpRequestLogsResponse], error)
}
// NewHttpRequestLogServiceHandler builds an HTTP handler from the service implementation. It
// returns the path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewHttpRequestLogServiceHandler(svc HttpRequestLogServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
httpRequestLogServiceMethods := File_reqlog_reqlog_proto.Services().ByName("HttpRequestLogService").Methods()
httpRequestLogServiceGetHttpRequestLogHandler := connect.NewUnaryHandler(
HttpRequestLogServiceGetHttpRequestLogProcedure,
svc.GetHttpRequestLog,
connect.WithSchema(httpRequestLogServiceMethods.ByName("GetHttpRequestLog")),
connect.WithHandlerOptions(opts...),
)
httpRequestLogServiceListHttpRequestLogsHandler := connect.NewUnaryHandler(
HttpRequestLogServiceListHttpRequestLogsProcedure,
svc.ListHttpRequestLogs,
connect.WithSchema(httpRequestLogServiceMethods.ByName("ListHttpRequestLogs")),
connect.WithHandlerOptions(opts...),
)
httpRequestLogServiceClearHttpRequestLogsHandler := connect.NewUnaryHandler(
HttpRequestLogServiceClearHttpRequestLogsProcedure,
svc.ClearHttpRequestLogs,
connect.WithSchema(httpRequestLogServiceMethods.ByName("ClearHttpRequestLogs")),
connect.WithHandlerOptions(opts...),
)
return "/hetty.reqlog.v1.HttpRequestLogService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case HttpRequestLogServiceGetHttpRequestLogProcedure:
httpRequestLogServiceGetHttpRequestLogHandler.ServeHTTP(w, r)
case HttpRequestLogServiceListHttpRequestLogsProcedure:
httpRequestLogServiceListHttpRequestLogsHandler.ServeHTTP(w, r)
case HttpRequestLogServiceClearHttpRequestLogsProcedure:
httpRequestLogServiceClearHttpRequestLogsHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedHttpRequestLogServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedHttpRequestLogServiceHandler struct{}
func (UnimplementedHttpRequestLogServiceHandler) GetHttpRequestLog(context.Context, *connect.Request[GetHttpRequestLogRequest]) (*connect.Response[GetHttpRequestLogResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.reqlog.v1.HttpRequestLogService.GetHttpRequestLog is not implemented"))
}
func (UnimplementedHttpRequestLogServiceHandler) ListHttpRequestLogs(context.Context, *connect.Request[ListHttpRequestLogsRequest]) (*connect.Response[ListHttpRequestLogsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.reqlog.v1.HttpRequestLogService.ListHttpRequestLogs is not implemented"))
}
func (UnimplementedHttpRequestLogServiceHandler) ClearHttpRequestLogs(context.Context, *connect.Request[ClearHttpRequestLogsRequest]) (*connect.Response[ClearHttpRequestLogsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("hetty.reqlog.v1.HttpRequestLogService.ClearHttpRequestLogs is not implemented"))
}

View File

@ -6,16 +6,16 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"net/url"
"github.com/oklog/ulid" "connectrpc.com/connect"
"github.com/oklog/ulid/v2"
"github.com/dstotijn/hetty/pkg/filter"
httppb "github.com/dstotijn/hetty/pkg/http"
"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
@ -26,68 +26,29 @@ const (
) )
var ( var (
ErrRequestNotFound = errors.New("reqlog: request not found") ErrRequestLogNotFound = errors.New("reqlog: request not found")
ErrProjectIDMustBeSet = errors.New("reqlog: project ID must be set") ErrProjectIDMustBeSet = errors.New("reqlog: project ID must be set")
) )
type RequestLog struct { type Service struct {
ID ulid.ULID
ProjectID ulid.ULID
URL *url.URL
Method string
Proto string
Header http.Header
Body []byte
Response *ResponseLog
}
type ResponseLog struct {
Proto string
StatusCode int
Status string
Header http.Header
Body []byte
}
type Service interface {
FindRequests(ctx context.Context) ([]RequestLog, error)
FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error)
ClearRequests(ctx context.Context, projectID ulid.ULID) error
RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc
ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc
SetActiveProjectID(id ulid.ULID)
ActiveProjectID() ulid.ULID
SetBypassOutOfScopeRequests(bool)
BypassOutOfScopeRequests() bool
SetFindReqsFilter(filter FindRequestsFilter)
FindReqsFilter() FindRequestsFilter
}
type service struct {
bypassOutOfScopeRequests bool bypassOutOfScopeRequests bool
findReqsFilter FindRequestsFilter reqsFilter *RequestLogsFilter
activeProjectID ulid.ULID activeProjectID string
scope *scope.Scope scope *scope.Scope
repo Repository repo Repository
logger log.Logger logger log.Logger
} }
type FindRequestsFilter struct {
ProjectID ulid.ULID
OnlyInScope bool
SearchExpr search.Expression
}
type Config struct { type Config struct {
ActiveProjectID string
Scope *scope.Scope Scope *scope.Scope
Repository Repository Repository Repository
Logger log.Logger Logger log.Logger
} }
func NewService(cfg Config) Service { func NewService(cfg Config) *Service {
s := &service{ s := &Service{
activeProjectID: cfg.ActiveProjectID,
repo: cfg.Repository, repo: cfg.Repository,
scope: cfg.Scope, scope: cfg.Scope,
logger: cfg.Logger, logger: cfg.Logger,
@ -100,28 +61,95 @@ func NewService(cfg Config) Service {
return s return s
} }
func (svc *service) FindRequests(ctx context.Context) ([]RequestLog, error) { func (svc *Service) ListHttpRequestLogs(ctx context.Context, req *connect.Request[ListHttpRequestLogsRequest]) (*connect.Response[ListHttpRequestLogsResponse], error) {
return svc.repo.FindRequestLogs(ctx, svc.findReqsFilter, svc.scope) projectID := svc.activeProjectID
if projectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
reqLogs, err := svc.repo.FindRequestLogs(ctx, projectID, svc.filterRequestLog)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("reqlog: failed to find request logs: %w", err))
}
return connect.NewResponse(&ListHttpRequestLogsResponse{
HttpRequestLogs: reqLogs,
}), nil
} }
func (svc *service) FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error) { func (svc *Service) filterRequestLog(reqLog *HttpRequestLog) (bool, error) {
return svc.repo.FindRequestLogByID(ctx, id) if svc.reqsFilter.GetOnlyInScope() && svc.scope != nil && !reqLog.MatchScope(svc.scope) {
return false, nil
}
var f filter.Expression
var err error
if expr := svc.reqsFilter.GetSearchExpr(); expr != "" {
f, err = filter.ParseQuery(expr)
if err != nil {
return false, fmt.Errorf("failed to parse search expression: %w", err)
}
}
if f == nil {
return true, nil
}
match, err := reqLog.Matches(f)
if err != nil {
return false, fmt.Errorf("failed to match search expression for request log (id: %v): %w", reqLog.Id, err)
}
return match, nil
} }
func (svc *service) ClearRequests(ctx context.Context, projectID ulid.ULID) error { func (svc *Service) FindRequestLogByID(ctx context.Context, id string) (*HttpRequestLog, error) {
return svc.repo.ClearRequestLogs(ctx, projectID) if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
return svc.repo.FindRequestLogByID(ctx, svc.activeProjectID, id)
} }
func (svc *service) storeResponse(ctx context.Context, reqLogID ulid.ULID, res *http.Response) error { // GetHttpRequestLog implements HttpRequestLogServiceHandler.
resLog, err := ParseHTTPResponse(res) func (svc *Service) GetHttpRequestLog(ctx context.Context, req *connect.Request[GetHttpRequestLogRequest]) (*connect.Response[GetHttpRequestLogResponse], error) {
id, err := ulid.Parse(req.Msg.Id)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
reqLog, err := svc.repo.FindRequestLogByID(ctx, svc.activeProjectID, id.String())
if errors.Is(err, ErrRequestLogNotFound) {
return nil, connect.NewError(connect.CodeNotFound, err)
}
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
return connect.NewResponse(&GetHttpRequestLogResponse{
HttpRequestLog: reqLog,
}), nil
}
func (svc *Service) ClearHttpRequestLogs(ctx context.Context, req *connect.Request[ClearHttpRequestLogsRequest]) (*connect.Response[ClearHttpRequestLogsResponse], error) {
err := svc.repo.ClearRequestLogs(ctx, svc.activeProjectID)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("reqlog: failed to clear request logs: %w", err))
}
return connect.NewResponse(&ClearHttpRequestLogsResponse{}), nil
}
func (svc *Service) storeResponse(ctx context.Context, reqLogID string, res *http.Response) error {
respb, err := httppb.ParseHTTPResponse(res)
if err != nil { if err != nil {
return err return err
} }
return svc.repo.StoreResponseLog(ctx, reqLogID, resLog) return svc.repo.StoreResponseLog(ctx, svc.activeProjectID, reqLogID, respb)
} }
func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc { func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
return func(req *http.Request) { return func(req *http.Request) {
next(req) next(req)
@ -133,19 +161,19 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
// TODO: Use io.LimitReader. // TODO: Use io.LimitReader.
var err error var err error
body, err = ioutil.ReadAll(req.Body) body, err = io.ReadAll(req.Body)
if err != nil { if err != nil {
svc.logger.Errorw("Failed to read request body for logging.", svc.logger.Errorw("Failed to read request body for logging.",
"error", err) "error", err)
return return
} }
req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) req.Body = io.NopCloser(bytes.NewBuffer(body))
clone.Body = ioutil.NopCloser(bytes.NewBuffer(body)) clone.Body = io.NopCloser(bytes.NewBuffer(body))
} }
// Bypass logging if no project is active. // Bypass logging if no project is active.
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 { if svc.activeProjectID == "" {
ctx := context.WithValue(req.Context(), LogBypassedKey, true) ctx := context.WithValue(req.Context(), LogBypassedKey, true)
*req = *req.WithContext(ctx) *req = *req.WithContext(ctx)
@ -173,14 +201,37 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
return return
} }
reqLog := RequestLog{ proto, ok := httppb.ProtoMap[clone.Proto]
ID: reqID, if !ok {
ProjectID: svc.activeProjectID, svc.logger.Errorw("Bypassed logging: request has an invalid protocol.",
Method: clone.Method, "proto", clone.Proto)
URL: clone.URL, return
Proto: clone.Proto, }
Header: clone.Header,
method, ok := httppb.MethodMap[clone.Method]
if !ok {
svc.logger.Errorw("Bypassed logging: request has an invalid method.",
"method", clone.Method)
return
}
headers := []*httppb.Header{}
for k, v := range clone.Header {
for _, vv := range v {
headers = append(headers, &httppb.Header{Key: k, Value: vv})
}
}
reqLog := &HttpRequestLog{
Id: reqID.String(),
ProjectId: svc.activeProjectID,
Request: &httppb.Request{
Url: clone.URL.String(),
Method: method,
Protocol: proto,
Headers: headers,
Body: body, Body: body,
},
} }
err := svc.repo.StoreRequestLog(req.Context(), reqLog) err := svc.repo.StoreRequestLog(req.Context(), reqLog)
@ -191,15 +242,15 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
} }
svc.logger.Debugw("Stored request log.", svc.logger.Debugw("Stored request log.",
"reqLogID", reqLog.ID.String(), "reqLogID", reqLog.Id,
"url", reqLog.URL.String()) "url", reqLog.Request.Url,
)
ctx := context.WithValue(req.Context(), ReqLogIDKey, reqLog.ID) ctx := context.WithValue(req.Context(), ReqLogIDKey, reqID)
*req = *req.WithContext(ctx) *req = *req.WithContext(ctx)
} }
} }
func (svc *service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc { func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
return func(res *http.Response) error { return func(res *http.Response) error {
if err := next(res); err != nil { if err := next(res); err != nil {
return err return err
@ -228,7 +279,7 @@ func (svc *service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
} }
go func() { go func() {
if err := svc.storeResponse(context.Background(), reqLogID, &clone); err != nil { if err := svc.storeResponse(context.Background(), reqLogID.String(), &clone); err != nil {
svc.logger.Errorw("Failed to store response log.", svc.logger.Errorw("Failed to store response log.",
"error", err) "error", err)
} else { } else {
@ -241,41 +292,22 @@ func (svc *service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
} }
} }
func (svc *service) SetActiveProjectID(id ulid.ULID) { func (svc *Service) SetActiveProjectID(id string) {
svc.activeProjectID = id svc.activeProjectID = id
} }
func (svc *service) ActiveProjectID() ulid.ULID { func (svc *Service) ActiveProjectID() string {
return svc.activeProjectID return svc.activeProjectID
} }
func (svc *service) SetFindReqsFilter(filter FindRequestsFilter) { func (svc *Service) SetRequestLogsFilter(filter *RequestLogsFilter) {
svc.findReqsFilter = filter svc.reqsFilter = filter
} }
func (svc *service) FindReqsFilter() FindRequestsFilter { func (svc *Service) SetBypassOutOfScopeRequests(bypass bool) {
return svc.findReqsFilter
}
func (svc *service) SetBypassOutOfScopeRequests(bypass bool) {
svc.bypassOutOfScopeRequests = bypass svc.bypassOutOfScopeRequests = bypass
} }
func (svc *service) BypassOutOfScopeRequests() bool { func (svc *Service) BypassOutOfScopeRequests() bool {
return svc.bypassOutOfScopeRequests return svc.bypassOutOfScopeRequests
} }
func ParseHTTPResponse(res *http.Response) (ResponseLog, error) {
body, err := io.ReadAll(res.Body)
if err != nil {
return ResponseLog{}, fmt.Errorf("reqlog: could not read body: %w", err)
}
return ResponseLog{
Proto: res.Proto,
StatusCode: res.StatusCode,
Status: res.Status,
Header: res.Header,
Body: body,
}, nil
}

533
pkg/reqlog/reqlog.pb.go Normal file
View File

@ -0,0 +1,533 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.3
// protoc (unknown)
// source: reqlog/reqlog.proto
package reqlog
import (
http "github.com/dstotijn/hetty/pkg/http"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type HttpRequestLog struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
ProjectId string `protobuf:"bytes,2,opt,name=project_id,json=projectId,proto3" json:"project_id,omitempty"`
RemoteIp string `protobuf:"bytes,3,opt,name=remote_ip,json=remoteIp,proto3" json:"remote_ip,omitempty"`
Request *http.Request `protobuf:"bytes,4,opt,name=request,proto3" json:"request,omitempty"`
Response *http.Response `protobuf:"bytes,5,opt,name=response,proto3" json:"response,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HttpRequestLog) Reset() {
*x = HttpRequestLog{}
mi := &file_reqlog_reqlog_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HttpRequestLog) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HttpRequestLog) ProtoMessage() {}
func (x *HttpRequestLog) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HttpRequestLog.ProtoReflect.Descriptor instead.
func (*HttpRequestLog) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{0}
}
func (x *HttpRequestLog) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *HttpRequestLog) GetProjectId() string {
if x != nil {
return x.ProjectId
}
return ""
}
func (x *HttpRequestLog) GetRemoteIp() string {
if x != nil {
return x.RemoteIp
}
return ""
}
func (x *HttpRequestLog) GetRequest() *http.Request {
if x != nil {
return x.Request
}
return nil
}
func (x *HttpRequestLog) GetResponse() *http.Response {
if x != nil {
return x.Response
}
return nil
}
type GetHttpRequestLogRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetHttpRequestLogRequest) Reset() {
*x = GetHttpRequestLogRequest{}
mi := &file_reqlog_reqlog_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetHttpRequestLogRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetHttpRequestLogRequest) ProtoMessage() {}
func (x *GetHttpRequestLogRequest) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetHttpRequestLogRequest.ProtoReflect.Descriptor instead.
func (*GetHttpRequestLogRequest) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{1}
}
func (x *GetHttpRequestLogRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type GetHttpRequestLogResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
HttpRequestLog *HttpRequestLog `protobuf:"bytes,1,opt,name=http_request_log,json=httpRequestLog,proto3" json:"http_request_log,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetHttpRequestLogResponse) Reset() {
*x = GetHttpRequestLogResponse{}
mi := &file_reqlog_reqlog_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetHttpRequestLogResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetHttpRequestLogResponse) ProtoMessage() {}
func (x *GetHttpRequestLogResponse) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetHttpRequestLogResponse.ProtoReflect.Descriptor instead.
func (*GetHttpRequestLogResponse) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{2}
}
func (x *GetHttpRequestLogResponse) GetHttpRequestLog() *HttpRequestLog {
if x != nil {
return x.HttpRequestLog
}
return nil
}
type ListHttpRequestLogsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListHttpRequestLogsRequest) Reset() {
*x = ListHttpRequestLogsRequest{}
mi := &file_reqlog_reqlog_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListHttpRequestLogsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListHttpRequestLogsRequest) ProtoMessage() {}
func (x *ListHttpRequestLogsRequest) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListHttpRequestLogsRequest.ProtoReflect.Descriptor instead.
func (*ListHttpRequestLogsRequest) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{3}
}
type ListHttpRequestLogsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
HttpRequestLogs []*HttpRequestLog `protobuf:"bytes,1,rep,name=http_request_logs,json=httpRequestLogs,proto3" json:"http_request_logs,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListHttpRequestLogsResponse) Reset() {
*x = ListHttpRequestLogsResponse{}
mi := &file_reqlog_reqlog_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListHttpRequestLogsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListHttpRequestLogsResponse) ProtoMessage() {}
func (x *ListHttpRequestLogsResponse) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListHttpRequestLogsResponse.ProtoReflect.Descriptor instead.
func (*ListHttpRequestLogsResponse) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{4}
}
func (x *ListHttpRequestLogsResponse) GetHttpRequestLogs() []*HttpRequestLog {
if x != nil {
return x.HttpRequestLogs
}
return nil
}
type RequestLogsFilter struct {
state protoimpl.MessageState `protogen:"open.v1"`
OnlyInScope bool `protobuf:"varint,1,opt,name=only_in_scope,json=onlyInScope,proto3" json:"only_in_scope,omitempty"`
SearchExpr string `protobuf:"bytes,2,opt,name=search_expr,json=searchExpr,proto3" json:"search_expr,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RequestLogsFilter) Reset() {
*x = RequestLogsFilter{}
mi := &file_reqlog_reqlog_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RequestLogsFilter) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RequestLogsFilter) ProtoMessage() {}
func (x *RequestLogsFilter) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RequestLogsFilter.ProtoReflect.Descriptor instead.
func (*RequestLogsFilter) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{5}
}
func (x *RequestLogsFilter) GetOnlyInScope() bool {
if x != nil {
return x.OnlyInScope
}
return false
}
func (x *RequestLogsFilter) GetSearchExpr() string {
if x != nil {
return x.SearchExpr
}
return ""
}
type ClearHttpRequestLogsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ClearHttpRequestLogsRequest) Reset() {
*x = ClearHttpRequestLogsRequest{}
mi := &file_reqlog_reqlog_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ClearHttpRequestLogsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ClearHttpRequestLogsRequest) ProtoMessage() {}
func (x *ClearHttpRequestLogsRequest) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ClearHttpRequestLogsRequest.ProtoReflect.Descriptor instead.
func (*ClearHttpRequestLogsRequest) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{6}
}
type ClearHttpRequestLogsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ClearHttpRequestLogsResponse) Reset() {
*x = ClearHttpRequestLogsResponse{}
mi := &file_reqlog_reqlog_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ClearHttpRequestLogsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ClearHttpRequestLogsResponse) ProtoMessage() {}
func (x *ClearHttpRequestLogsResponse) ProtoReflect() protoreflect.Message {
mi := &file_reqlog_reqlog_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ClearHttpRequestLogsResponse.ProtoReflect.Descriptor instead.
func (*ClearHttpRequestLogsResponse) Descriptor() ([]byte, []int) {
return file_reqlog_reqlog_proto_rawDescGZIP(), []int{7}
}
var File_reqlog_reqlog_proto protoreflect.FileDescriptor
var file_reqlog_reqlog_proto_rawDesc = []byte{
0x0a, 0x13, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2f, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72, 0x65, 0x71,
0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31, 0x1a, 0x0f, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x68, 0x74, 0x74,
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc3, 0x01, 0x0a, 0x0e, 0x48, 0x74, 0x74, 0x70,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72,
0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x6d,
0x6f, 0x74, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65,
0x6d, 0x6f, 0x74, 0x65, 0x49, 0x70, 0x12, 0x30, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e,
0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52,
0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x74,
0x74, 0x79, 0x2e, 0x68, 0x74, 0x74, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2a, 0x0a,
0x18, 0x47, 0x65, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c,
0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x66, 0x0a, 0x19, 0x47, 0x65, 0x74,
0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x49, 0x0a, 0x10, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x1f, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e,
0x76, 0x31, 0x2e, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f,
0x67, 0x52, 0x0e, 0x68, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f,
0x67, 0x22, 0x1c, 0x0a, 0x1a, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
0x6a, 0x0a, 0x1b, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b,
0x0a, 0x11, 0x68, 0x74, 0x74, 0x70, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x6c,
0x6f, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x68, 0x65, 0x74, 0x74,
0x79, 0x2e, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x74, 0x74, 0x70,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x52, 0x0f, 0x68, 0x74, 0x74, 0x70,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x22, 0x58, 0x0a, 0x11, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72,
0x12, 0x22, 0x0a, 0x0d, 0x6f, 0x6e, 0x6c, 0x79, 0x5f, 0x69, 0x6e, 0x5f, 0x73, 0x63, 0x6f, 0x70,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x6f, 0x6e, 0x6c, 0x79, 0x49, 0x6e, 0x53,
0x63, 0x6f, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x65,
0x78, 0x70, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x61, 0x72, 0x63,
0x68, 0x45, 0x78, 0x70, 0x72, 0x22, 0x1d, 0x0a, 0x1b, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x74,
0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x22, 0x1e, 0x0a, 0x1c, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x74, 0x74,
0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x32, 0xf0, 0x02, 0x0a, 0x15, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6c,
0x0a, 0x11, 0x47, 0x65, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x4c, 0x6f, 0x67, 0x12, 0x29, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72, 0x65, 0x71, 0x6c,
0x6f, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a,
0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31,
0x2e, 0x47, 0x65, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c,
0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x72, 0x0a, 0x13,
0x4c, 0x69, 0x73, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c,
0x6f, 0x67, 0x73, 0x12, 0x2b, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72, 0x65, 0x71, 0x6c,
0x6f, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x2c, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e,
0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
0x12, 0x75, 0x0a, 0x14, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x12, 0x2c, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79,
0x2e, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72,
0x48, 0x74, 0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x72,
0x65, 0x71, 0x6c, 0x6f, 0x67, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x48, 0x74,
0x74, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x6f, 0x67, 0x73, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x26, 0x5a, 0x24, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x73, 0x74, 0x6f, 0x74, 0x69, 0x6a, 0x6e, 0x2f, 0x68,
0x65, 0x74, 0x74, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x72, 0x65, 0x71, 0x6c, 0x6f, 0x67, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_reqlog_reqlog_proto_rawDescOnce sync.Once
file_reqlog_reqlog_proto_rawDescData = file_reqlog_reqlog_proto_rawDesc
)
func file_reqlog_reqlog_proto_rawDescGZIP() []byte {
file_reqlog_reqlog_proto_rawDescOnce.Do(func() {
file_reqlog_reqlog_proto_rawDescData = protoimpl.X.CompressGZIP(file_reqlog_reqlog_proto_rawDescData)
})
return file_reqlog_reqlog_proto_rawDescData
}
var file_reqlog_reqlog_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_reqlog_reqlog_proto_goTypes = []any{
(*HttpRequestLog)(nil), // 0: hetty.reqlog.v1.HttpRequestLog
(*GetHttpRequestLogRequest)(nil), // 1: hetty.reqlog.v1.GetHttpRequestLogRequest
(*GetHttpRequestLogResponse)(nil), // 2: hetty.reqlog.v1.GetHttpRequestLogResponse
(*ListHttpRequestLogsRequest)(nil), // 3: hetty.reqlog.v1.ListHttpRequestLogsRequest
(*ListHttpRequestLogsResponse)(nil), // 4: hetty.reqlog.v1.ListHttpRequestLogsResponse
(*RequestLogsFilter)(nil), // 5: hetty.reqlog.v1.RequestLogsFilter
(*ClearHttpRequestLogsRequest)(nil), // 6: hetty.reqlog.v1.ClearHttpRequestLogsRequest
(*ClearHttpRequestLogsResponse)(nil), // 7: hetty.reqlog.v1.ClearHttpRequestLogsResponse
(*http.Request)(nil), // 8: hetty.http.v1.Request
(*http.Response)(nil), // 9: hetty.http.v1.Response
}
var file_reqlog_reqlog_proto_depIdxs = []int32{
8, // 0: hetty.reqlog.v1.HttpRequestLog.request:type_name -> hetty.http.v1.Request
9, // 1: hetty.reqlog.v1.HttpRequestLog.response:type_name -> hetty.http.v1.Response
0, // 2: hetty.reqlog.v1.GetHttpRequestLogResponse.http_request_log:type_name -> hetty.reqlog.v1.HttpRequestLog
0, // 3: hetty.reqlog.v1.ListHttpRequestLogsResponse.http_request_logs:type_name -> hetty.reqlog.v1.HttpRequestLog
1, // 4: hetty.reqlog.v1.HttpRequestLogService.GetHttpRequestLog:input_type -> hetty.reqlog.v1.GetHttpRequestLogRequest
3, // 5: hetty.reqlog.v1.HttpRequestLogService.ListHttpRequestLogs:input_type -> hetty.reqlog.v1.ListHttpRequestLogsRequest
6, // 6: hetty.reqlog.v1.HttpRequestLogService.ClearHttpRequestLogs:input_type -> hetty.reqlog.v1.ClearHttpRequestLogsRequest
2, // 7: hetty.reqlog.v1.HttpRequestLogService.GetHttpRequestLog:output_type -> hetty.reqlog.v1.GetHttpRequestLogResponse
4, // 8: hetty.reqlog.v1.HttpRequestLogService.ListHttpRequestLogs:output_type -> hetty.reqlog.v1.ListHttpRequestLogsResponse
7, // 9: hetty.reqlog.v1.HttpRequestLogService.ClearHttpRequestLogs:output_type -> hetty.reqlog.v1.ClearHttpRequestLogsResponse
7, // [7:10] is the sub-list for method output_type
4, // [4:7] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_reqlog_reqlog_proto_init() }
func file_reqlog_reqlog_proto_init() {
if File_reqlog_reqlog_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_reqlog_reqlog_proto_rawDesc,
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_reqlog_reqlog_proto_goTypes,
DependencyIndexes: file_reqlog_reqlog_proto_depIdxs,
MessageInfos: file_reqlog_reqlog_proto_msgTypes,
}.Build()
File_reqlog_reqlog_proto = out.File
file_reqlog_reqlog_proto_rawDesc = nil
file_reqlog_reqlog_proto_goTypes = nil
file_reqlog_reqlog_proto_depIdxs = nil
}

View File

@ -1,86 +1,119 @@
package reqlog_test package reqlog_test
//go:generate go run github.com/matryer/moq -out repo_mock_test.go -pkg reqlog_test . Repository:RepoMock
import ( import (
"context" "context"
"io" "io"
"math/rand"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp" "github.com/oklog/ulid/v2"
"github.com/oklog/ulid" "go.etcd.io/bbolt"
"github.com/dstotijn/hetty/pkg/db/bolt"
httppb "github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/proxy" "github.com/dstotijn/hetty/pkg/proxy"
"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/testutil"
) )
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
//nolint:paralleltest //nolint:paralleltest
func TestRequestModifier(t *testing.T) { func TestRequestModifier(t *testing.T) {
repoMock := &RepoMock{ path := t.TempDir() + "bolt.db"
StoreRequestLogFunc: func(_ context.Context, _ reqlog.RequestLog) error { boltDB, err := bbolt.Open(path, 0o600, nil)
return nil if err != nil {
}, t.Fatalf("failed to open bolt database: %v", err)
} }
defer boltDB.Close()
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := ulid.Make().String()
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
}
svc := reqlog.NewService(reqlog.Config{ svc := reqlog.NewService(reqlog.Config{
Repository: repoMock, Repository: db,
Scope: &scope.Scope{}, Scope: &scope.Scope{},
}) })
svc.SetActiveProjectID(ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)) svc.SetActiveProjectID(projectID)
next := func(req *http.Request) { next := func(req *http.Request) {
req.Body = io.NopCloser(strings.NewReader("modified body")) req.Body = io.NopCloser(strings.NewReader("modified body"))
} }
reqModFn := svc.RequestModifier(next) reqModFn := svc.RequestModifier(next)
req := httptest.NewRequest("GET", "https://example.com/", strings.NewReader("bar")) req := httptest.NewRequest("GET", "https://example.com/", strings.NewReader("bar"))
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) req.Header.Add("X-Yolo", "swag")
reqID := ulid.Make()
req = req.WithContext(proxy.WithRequestID(req.Context(), reqID)) req = req.WithContext(proxy.WithRequestID(req.Context(), reqID))
reqModFn(req) reqModFn(req)
t.Run("request log was stored in repository", func(t *testing.T) { t.Run("request log was stored in repository", func(t *testing.T) {
gotCount := len(repoMock.StoreRequestLogCalls()) exp := &reqlog.HttpRequestLog{
if expCount := 1; expCount != gotCount { Id: reqID.String(),
t.Fatalf("incorrect `proj.Service.AddRequestLog` calls (expected: %v, got: %v)", expCount, gotCount) ProjectId: svc.ActiveProjectID(),
} Request: &httppb.Request{
Url: "https://example.com/",
exp := reqlog.RequestLog{ Method: httppb.Method_METHOD_GET,
ID: ulid.ULID{}, // Empty value Protocol: httppb.Protocol_PROTOCOL_HTTP11,
ProjectID: svc.ActiveProjectID(), Headers: []*httppb.Header{
Method: req.Method, {
URL: req.URL, Key: "X-Yolo",
Proto: req.Proto, Value: "swag",
Header: req.Header, },
},
Body: []byte("modified body"), Body: []byte("modified body"),
},
} }
got := repoMock.StoreRequestLogCalls()[0].ReqLog
got.ID = ulid.ULID{} // Override to empty value so we can compare against expected value.
if diff := cmp.Diff(exp, got); diff != "" { got, err := db.FindRequestLogByID(context.Background(), svc.ActiveProjectID(), reqID.String())
t.Fatalf("request log not equal (-exp, +got):\n%v", diff) if err != nil {
t.Fatalf("failed to find request by id: %v", err)
} }
testutil.ProtoDiff(t, "request log not equal", exp, got)
}) })
} }
//nolint:paralleltest
func TestResponseModifier(t *testing.T) { func TestResponseModifier(t *testing.T) {
repoMock := &RepoMock{ path := t.TempDir() + "bolt.db"
StoreResponseLogFunc: func(_ context.Context, _ ulid.ULID, _ reqlog.ResponseLog) error { boltDB, err := bbolt.Open(path, 0o600, nil)
return nil if err != nil {
}, t.Fatalf("failed to open bolt database: %v", err)
} }
svc := reqlog.NewService(reqlog.Config{ defer boltDB.Close()
Repository: repoMock,
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := "foobar-project-id"
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
}) })
svc.SetActiveProjectID(ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)) if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
}
svc := reqlog.NewService(reqlog.Config{
Repository: db,
})
svc.SetActiveProjectID(projectID)
next := func(res *http.Response) error { next := func(res *http.Response) error {
res.Body = io.NopCloser(strings.NewReader("modified body")) res.Body = io.NopCloser(strings.NewReader("modified body"))
@ -89,11 +122,22 @@ func TestResponseModifier(t *testing.T) {
resModFn := svc.ResponseModifier(next) resModFn := svc.ResponseModifier(next)
req := httptest.NewRequest("GET", "https://example.com/", strings.NewReader("bar")) req := httptest.NewRequest("GET", "https://example.com/", strings.NewReader("bar"))
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) reqLogID := ulid.Make()
req = req.WithContext(context.WithValue(req.Context(), reqlog.ReqLogIDKey, reqLogID)) req = req.WithContext(context.WithValue(req.Context(), reqlog.ReqLogIDKey, reqLogID))
err = db.StoreRequestLog(context.Background(), &reqlog.HttpRequestLog{
Id: reqLogID.String(),
ProjectId: projectID,
})
if err != nil {
t.Fatalf("failed to store request log: %v", err)
}
res := &http.Response{ res := &http.Response{
Request: req, Request: req,
Proto: "HTTP/1.1",
Status: "200 OK",
StatusCode: 200,
Body: io.NopCloser(strings.NewReader("bar")), Body: io.NopCloser(strings.NewReader("bar")),
} }
@ -101,27 +145,21 @@ func TestResponseModifier(t *testing.T) {
t.Fatalf("unexpected error (expected: nil, got: %v)", err) t.Fatalf("unexpected error (expected: nil, got: %v)", err)
} }
t.Run("request log was stored in repository", func(t *testing.T) {
// Dirty (but simple) wait for other goroutine to finish calling repository. // Dirty (but simple) wait for other goroutine to finish calling repository.
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
got := len(repoMock.StoreResponseLogCalls())
if exp := 1; exp != got { got, err := db.FindRequestLogByID(context.Background(), svc.ActiveProjectID(), reqLogID.String())
t.Fatalf("incorrect `proj.Service.AddResponseLog` calls (expected: %v, got: %v)", exp, got) if err != nil {
t.Fatalf("failed to find request by id: %v", err)
} }
t.Run("ran next modifier first, before calling repository", func(t *testing.T) { exp := &httppb.Response{
got := repoMock.StoreResponseLogCalls()[0].ResLog.Body Protocol: httppb.Protocol_PROTOCOL_HTTP11,
if exp := "modified body"; exp != string(got) { Status: "200 OK",
t.Fatalf("incorrect `ResponseLog.Body` value (expected: %v, got: %v)", exp, string(got)) StatusCode: 200,
Headers: []*httppb.Header{},
Body: []byte("modified body"),
} }
})
t.Run("called repository with request log id", func(t *testing.T) { testutil.ProtoDiff(t, "response not equal", exp, got.GetResponse())
got := repoMock.StoreResponseLogCalls()[0].ReqLogID
if exp := reqLogID; exp.Compare(got) != 0 {
t.Fatalf("incorrect `reqLogID` argument for `Repository.AddResponseLogCalls` (expected: %v, got: %v)",
exp.String(), got.String())
}
})
})
} }

View File

@ -3,55 +3,49 @@ package reqlog
import ( import (
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/oklog/ulid" "github.com/oklog/ulid/v2"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/http"
"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 *HttpRequestLog) string{
"req.id": func(rl RequestLog) string { return rl.ID.String() }, "req.id": func(rl *HttpRequestLog) string { return rl.GetId() },
"req.proto": func(rl RequestLog) string { return rl.Proto }, "req.proto": func(rl *HttpRequestLog) string { return rl.GetRequest().GetProtocol().String() },
"req.url": func(rl RequestLog) string { "req.url": func(rl *HttpRequestLog) string { return rl.GetRequest().GetUrl() },
if rl.URL == nil { "req.method": func(rl *HttpRequestLog) string { return rl.GetRequest().GetMethod().String() },
"req.body": func(rl *HttpRequestLog) string { return string(rl.GetRequest().GetBody()) },
"req.timestamp": func(rl *HttpRequestLog) string {
id, err := ulid.Parse(rl.GetId())
if err != nil {
return "" return ""
} }
return rl.URL.String() return ulid.Time(id.Time()).String()
}, },
"req.method": func(rl RequestLog) string { return rl.Method },
"req.body": func(rl RequestLog) string { return string(rl.Body) },
"req.timestamp": func(rl RequestLog) string { return ulid.Time(rl.ID.Time()).String() },
}
var ResLogSearchKeyFns = map[string]func(rl ResponseLog) string{
"res.proto": func(rl ResponseLog) string { return rl.Proto },
"res.statusCode": func(rl ResponseLog) string { return strconv.Itoa(rl.StatusCode) },
"res.statusReason": func(rl ResponseLog) string { return rl.Status },
"res.body": func(rl ResponseLog) string { return string(rl.Body) },
} }
// 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 *HttpRequestLog) 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 *HttpRequestLog) 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 +57,9 @@ func (reqLog RequestLog) matchPrefixExpr(expr search.PrefixExpression) (bool, er
} }
} }
func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, error) { func (reqLog *HttpRequestLog) 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 +71,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 +85,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.Request.Headers)
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.Headers)
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 +132,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:
@ -141,7 +153,7 @@ func (reqLog RequestLog) matchInfixExpr(expr search.InfixExpression) (bool, erro
} }
} }
func (reqLog RequestLog) getMappedStringLiteral(s string) string { func (reqLog *HttpRequestLog) getMappedStringLiteral(s string) string {
switch { switch {
case strings.HasPrefix(s, "req."): case strings.HasPrefix(s, "req."):
fn, ok := reqLogSearchKeyFns[s] fn, ok := reqLogSearchKeyFns[s]
@ -149,20 +161,25 @@ func (reqLog RequestLog) getMappedStringLiteral(s string) string {
return fn(reqLog) return fn(reqLog)
} }
case strings.HasPrefix(s, "res."): case strings.HasPrefix(s, "res."):
if reqLog.Response == nil { fn, ok := http.ResponseSearchKeyFns[s]
return ""
}
fn, ok := ResLogSearchKeyFns[s]
if ok { if ok {
return fn(*reqLog.Response) return fn(reqLog.GetResponse())
} }
} }
return s return s
} }
func (reqLog RequestLog) matchStringLiteral(strLiteral search.StringLiteral) (bool, error) { func (reqLog *HttpRequestLog) matchStringLiteral(strLiteral filter.StringLiteral) (bool, error) {
for _, header := range reqLog.GetRequest().GetHeaders() {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", header.Key, header.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)),
@ -172,10 +189,19 @@ func (reqLog RequestLog) matchStringLiteral(strLiteral search.StringLiteral) (bo
} }
} }
if reqLog.Response != nil { if res := reqLog.GetResponse(); res != nil {
for _, fn := range ResLogSearchKeyFns { for _, header := range res.Headers {
if strings.Contains( if strings.Contains(
strings.ToLower(fn(*reqLog.Response)), strings.ToLower(fmt.Sprintf("%v: %v", header.Key, header.Value)),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
}
for _, fn := range http.ResponseSearchKeyFns {
if strings.Contains(
strings.ToLower(fn(reqLog.GetResponse())),
strings.ToLower(strLiteral.Value), strings.ToLower(strLiteral.Value),
) { ) {
return true, nil return true, nil
@ -186,31 +212,27 @@ func (reqLog RequestLog) matchStringLiteral(strLiteral search.StringLiteral) (bo
return false, nil return false, nil
} }
func (reqLog RequestLog) MatchScope(s *scope.Scope) bool { func (reqLog *HttpRequestLog) MatchScope(s *scope.Scope) bool {
for _, rule := range s.Rules() { for _, rule := range s.Rules() {
if rule.URL != nil && reqLog.URL != nil { if rule.URL != nil {
if matches := rule.URL.MatchString(reqLog.URL.String()); matches { if matches := rule.URL.MatchString(reqLog.GetRequest().GetUrl()); matches {
return true return true
} }
} }
for key, values := range reqLog.Header { for _, header := range reqLog.GetRequest().GetHeaders() {
var keyMatches, valueMatches bool var keyMatches, valueMatches bool
if rule.Header.Key != nil { if matches := rule.Header.Key.MatchString(header.Key); matches {
if matches := rule.Header.Key.MatchString(key); matches {
keyMatches = true keyMatches = true
} }
}
if rule.Header.Value != nil { if rule.Header.Value != nil {
for _, value := range values { if matches := rule.Header.Value.MatchString(header.Value); matches {
if matches := rule.Header.Value.MatchString(value); matches {
valueMatches = true valueMatches = true
break break
} }
} }
}
// When only key or value is set, match on whatever is set. // When only key or value is set, match on whatever is set.
// When both are set, both must match. // When both are set, both must match.
switch { switch {
@ -224,7 +246,7 @@ func (reqLog RequestLog) MatchScope(s *scope.Scope) bool {
} }
if rule.Body != nil { if rule.Body != nil {
if matches := rule.Body.Match(reqLog.Body); matches { if matches := rule.Body.Match(reqLog.GetRequest().GetBody()); matches {
return true return true
} }
} }

View File

@ -3,8 +3,9 @@ package reqlog_test
import ( import (
"testing" "testing"
"github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/http"
"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) {
@ -13,106 +14,128 @@ func TestRequestLogMatch(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
query string query string
requestLog reqlog.RequestLog requestLog *reqlog.HttpRequestLog
expectedMatch bool expectedMatch bool
expectedError error expectedError error
}{ }{
{ {
name: "infix expression, equal operator, match", name: "infix expression, equal operator, match",
query: "req.body = foo", query: "req.body = foo",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("foo"), Body: []byte("foo"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, not equal operator, match", name: "infix expression, not equal operator, match",
query: "req.body != bar", query: "req.body != bar",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("foo"), Body: []byte("foo"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, greater than operator, match", name: "infix expression, greater than operator, match",
query: "req.body > a", query: "req.body > a",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("b"), Body: []byte("b"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, less than operator, match", name: "infix expression, less than operator, match",
query: "req.body < b", query: "req.body < b",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("a"), Body: []byte("a"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, greater than or equal operator, match greater than", name: "infix expression, greater than or equal operator, match greater than",
query: "req.body >= a", query: "req.body >= a",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("b"), Body: []byte("b"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, greater than or equal operator, match equal", name: "infix expression, greater than or equal operator, match equal",
query: "req.body >= a", query: "req.body >= a",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("a"), Body: []byte("a"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, less than or equal operator, match less than", name: "infix expression, less than or equal operator, match less than",
query: "req.body <= b", query: "req.body <= b",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("a"), Body: []byte("a"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, less than or equal operator, match equal", name: "infix expression, less than or equal operator, match equal",
query: "req.body <= b", query: "req.body <= b",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("b"), Body: []byte("b"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, regular expression operator, match", name: "infix expression, regular expression operator, match",
query: `req.body =~ "^foo(.*)$"`, query: `req.body =~ "^foo(.*)$"`,
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("foobar"), Body: []byte("foobar"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, negate regular expression operator, match", name: "infix expression, negate regular expression operator, match",
query: `req.body !~ "^foo(.*)$"`, query: `req.body !~ "^foo(.*)$"`,
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("xoobar"), Body: []byte("xoobar"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, and operator, match", name: "infix expression, and operator, match",
query: "req.body = bar AND res.body = yolo", query: "req.body = bar AND res.body = yolo",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("bar"), Body: []byte("bar"),
Response: &reqlog.ResponseLog{ },
Response: &http.Response{
Body: []byte("yolo"), Body: []byte("yolo"),
}, },
}, },
@ -122,9 +145,11 @@ func TestRequestLogMatch(t *testing.T) {
{ {
name: "infix expression, or operator, match", name: "infix expression, or operator, match",
query: "req.body = bar OR res.body = yolo", query: "req.body = bar OR res.body = yolo",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("foo"), Body: []byte("foo"),
Response: &reqlog.ResponseLog{ },
Response: &http.Response{
Body: []byte("yolo"), Body: []byte("yolo"),
}, },
}, },
@ -134,35 +159,41 @@ func TestRequestLogMatch(t *testing.T) {
{ {
name: "prefix expression, not operator, match", name: "prefix expression, not operator, match",
query: "NOT (req.body = bar)", query: "NOT (req.body = bar)",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("foo"), Body: []byte("foo"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "string literal expression, match in request log", name: "string literal expression, match in request log",
query: "foo", query: "foo",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("foo"), Body: []byte("foo"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "string literal expression, no match", name: "string literal expression, no match",
query: "foo", query: "foo",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Request: &http.Request{
Body: []byte("bar"), Body: []byte("bar"),
}, },
},
expectedMatch: false, expectedMatch: false,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "string literal expression, match in response log", name: "string literal expression, match in response log",
query: "foo", query: "foo",
requestLog: reqlog.RequestLog{ requestLog: &reqlog.HttpRequestLog{
Response: &reqlog.ResponseLog{ Response: &http.Response{
Body: []byte("foo"), Body: []byte("foo"),
}, },
}, },
@ -176,7 +207,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)

159
pkg/scope/scope.pb.go Normal file
View File

@ -0,0 +1,159 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.3
// protoc (unknown)
// source: scope/scope.proto
package scope
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type ScopeRule struct {
state protoimpl.MessageState `protogen:"open.v1"`
UrlRegexp string `protobuf:"bytes,1,opt,name=url_regexp,json=urlRegexp,proto3" json:"url_regexp,omitempty"`
HeaderKeyRegexp string `protobuf:"bytes,2,opt,name=header_key_regexp,json=headerKeyRegexp,proto3" json:"header_key_regexp,omitempty"`
HeaderValueRegexp string `protobuf:"bytes,3,opt,name=header_value_regexp,json=headerValueRegexp,proto3" json:"header_value_regexp,omitempty"`
BodyRegexp string `protobuf:"bytes,4,opt,name=body_regexp,json=bodyRegexp,proto3" json:"body_regexp,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ScopeRule) Reset() {
*x = ScopeRule{}
mi := &file_scope_scope_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ScopeRule) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ScopeRule) ProtoMessage() {}
func (x *ScopeRule) ProtoReflect() protoreflect.Message {
mi := &file_scope_scope_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ScopeRule.ProtoReflect.Descriptor instead.
func (*ScopeRule) Descriptor() ([]byte, []int) {
return file_scope_scope_proto_rawDescGZIP(), []int{0}
}
func (x *ScopeRule) GetUrlRegexp() string {
if x != nil {
return x.UrlRegexp
}
return ""
}
func (x *ScopeRule) GetHeaderKeyRegexp() string {
if x != nil {
return x.HeaderKeyRegexp
}
return ""
}
func (x *ScopeRule) GetHeaderValueRegexp() string {
if x != nil {
return x.HeaderValueRegexp
}
return ""
}
func (x *ScopeRule) GetBodyRegexp() string {
if x != nil {
return x.BodyRegexp
}
return ""
}
var File_scope_scope_proto protoreflect.FileDescriptor
var file_scope_scope_proto_rawDesc = []byte{
0x0a, 0x11, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x2f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65,
0x2e, 0x76, 0x31, 0x22, 0xa7, 0x01, 0x0a, 0x09, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x52, 0x75, 0x6c,
0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x72, 0x6c, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x75, 0x72, 0x6c, 0x52, 0x65, 0x67, 0x65, 0x78, 0x70,
0x12, 0x2a, 0x0a, 0x11, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x72,
0x65, 0x67, 0x65, 0x78, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x68, 0x65, 0x61,
0x64, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x67, 0x65, 0x78, 0x70, 0x12, 0x2e, 0x0a, 0x13,
0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x72, 0x65, 0x67,
0x65, 0x78, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x68, 0x65, 0x61, 0x64, 0x65,
0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x65, 0x67, 0x65, 0x78, 0x70, 0x12, 0x1f, 0x0a, 0x0b,
0x62, 0x6f, 0x64, 0x79, 0x5f, 0x72, 0x65, 0x67, 0x65, 0x78, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0a, 0x62, 0x6f, 0x64, 0x79, 0x52, 0x65, 0x67, 0x65, 0x78, 0x70, 0x42, 0x25, 0x5a,
0x23, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x73, 0x74, 0x6f,
0x74, 0x69, 0x6a, 0x6e, 0x2f, 0x68, 0x65, 0x74, 0x74, 0x79, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73,
0x63, 0x6f, 0x70, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_scope_scope_proto_rawDescOnce sync.Once
file_scope_scope_proto_rawDescData = file_scope_scope_proto_rawDesc
)
func file_scope_scope_proto_rawDescGZIP() []byte {
file_scope_scope_proto_rawDescOnce.Do(func() {
file_scope_scope_proto_rawDescData = protoimpl.X.CompressGZIP(file_scope_scope_proto_rawDescData)
})
return file_scope_scope_proto_rawDescData
}
var file_scope_scope_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_scope_scope_proto_goTypes = []any{
(*ScopeRule)(nil), // 0: hetty.scope.v1.ScopeRule
}
var file_scope_scope_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_scope_scope_proto_init() }
func file_scope_scope_proto_init() {
if File_scope_scope_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_scope_scope_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_scope_scope_proto_goTypes,
DependencyIndexes: file_scope_scope_proto_depIdxs,
MessageInfos: file_scope_scope_proto_msgTypes,
}.Build()
File_scope_scope_proto = out.File
file_scope_scope_proto_rawDesc = nil
file_scope_scope_proto_goTypes = nil
file_scope_scope_proto_depIdxs = nil
}

View File

@ -2,17 +2,11 @@ package sender
import ( import (
"context" "context"
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
) )
type Repository interface { type Repository interface {
FindSenderRequestByID(ctx context.Context, id ulid.ULID) (Request, error) FindSenderRequestByID(ctx context.Context, projectID, id string) (*Request, error)
FindSenderRequests(ctx context.Context, filter FindRequestsFilter, scope *scope.Scope) ([]Request, error) FindSenderRequests(ctx context.Context, projectID string, filterFn func(*Request) (bool, error)) ([]*Request, error)
StoreSenderRequest(ctx context.Context, req Request) error StoreSenderRequest(ctx context.Context, req *Request) error
StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error DeleteSenderRequests(ctx context.Context, projectID string) error
DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error
} }

View File

@ -1,292 +0,0 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package sender_test
import (
"context"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/sender"
"github.com/oklog/ulid"
"sync"
)
// Ensure, that RepoMock does implement sender.Repository.
// If this is not the case, regenerate this file with moq.
var _ sender.Repository = &RepoMock{}
// RepoMock is a mock implementation of sender.Repository.
//
// func TestSomethingThatUsesRepository(t *testing.T) {
//
// // make and configure a mocked sender.Repository
// mockedRepository := &RepoMock{
// DeleteSenderRequestsFunc: func(ctx context.Context, projectID ulid.ULID) error {
// panic("mock out the DeleteSenderRequests method")
// },
// FindSenderRequestByIDFunc: func(ctx context.Context, id ulid.ULID) (sender.Request, error) {
// panic("mock out the FindSenderRequestByID method")
// },
// FindSenderRequestsFunc: func(ctx context.Context, filter sender.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]sender.Request, error) {
// panic("mock out the FindSenderRequests method")
// },
// StoreResponseLogFunc: func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
// panic("mock out the StoreResponseLog method")
// },
// StoreSenderRequestFunc: func(ctx context.Context, req sender.Request) error {
// panic("mock out the StoreSenderRequest method")
// },
// }
//
// // use mockedRepository in code that requires sender.Repository
// // and then make assertions.
//
// }
type RepoMock struct {
// DeleteSenderRequestsFunc mocks the DeleteSenderRequests method.
DeleteSenderRequestsFunc func(ctx context.Context, projectID ulid.ULID) error
// FindSenderRequestByIDFunc mocks the FindSenderRequestByID method.
FindSenderRequestByIDFunc func(ctx context.Context, id ulid.ULID) (sender.Request, error)
// FindSenderRequestsFunc mocks the FindSenderRequests method.
FindSenderRequestsFunc func(ctx context.Context, filter sender.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]sender.Request, error)
// StoreResponseLogFunc mocks the StoreResponseLog method.
StoreResponseLogFunc func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error
// StoreSenderRequestFunc mocks the StoreSenderRequest method.
StoreSenderRequestFunc func(ctx context.Context, req sender.Request) error
// calls tracks calls to the methods.
calls struct {
// DeleteSenderRequests holds details about calls to the DeleteSenderRequests method.
DeleteSenderRequests []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ProjectID is the projectID argument value.
ProjectID ulid.ULID
}
// FindSenderRequestByID holds details about calls to the FindSenderRequestByID method.
FindSenderRequestByID []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ID is the id argument value.
ID ulid.ULID
}
// FindSenderRequests holds details about calls to the FindSenderRequests method.
FindSenderRequests []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Filter is the filter argument value.
Filter sender.FindRequestsFilter
// ScopeMoqParam is the scopeMoqParam argument value.
ScopeMoqParam *scope.Scope
}
// StoreResponseLog holds details about calls to the StoreResponseLog method.
StoreResponseLog []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ReqLogID is the reqLogID argument value.
ReqLogID ulid.ULID
// ResLog is the resLog argument value.
ResLog reqlog.ResponseLog
}
// StoreSenderRequest holds details about calls to the StoreSenderRequest method.
StoreSenderRequest []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// Req is the req argument value.
Req sender.Request
}
}
lockDeleteSenderRequests sync.RWMutex
lockFindSenderRequestByID sync.RWMutex
lockFindSenderRequests sync.RWMutex
lockStoreResponseLog sync.RWMutex
lockStoreSenderRequest sync.RWMutex
}
// DeleteSenderRequests calls DeleteSenderRequestsFunc.
func (mock *RepoMock) DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error {
if mock.DeleteSenderRequestsFunc == nil {
panic("RepoMock.DeleteSenderRequestsFunc: method is nil but Repository.DeleteSenderRequests was just called")
}
callInfo := struct {
Ctx context.Context
ProjectID ulid.ULID
}{
Ctx: ctx,
ProjectID: projectID,
}
mock.lockDeleteSenderRequests.Lock()
mock.calls.DeleteSenderRequests = append(mock.calls.DeleteSenderRequests, callInfo)
mock.lockDeleteSenderRequests.Unlock()
return mock.DeleteSenderRequestsFunc(ctx, projectID)
}
// DeleteSenderRequestsCalls gets all the calls that were made to DeleteSenderRequests.
// Check the length with:
// len(mockedRepository.DeleteSenderRequestsCalls())
func (mock *RepoMock) DeleteSenderRequestsCalls() []struct {
Ctx context.Context
ProjectID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ProjectID ulid.ULID
}
mock.lockDeleteSenderRequests.RLock()
calls = mock.calls.DeleteSenderRequests
mock.lockDeleteSenderRequests.RUnlock()
return calls
}
// FindSenderRequestByID calls FindSenderRequestByIDFunc.
func (mock *RepoMock) FindSenderRequestByID(ctx context.Context, id ulid.ULID) (sender.Request, error) {
if mock.FindSenderRequestByIDFunc == nil {
panic("RepoMock.FindSenderRequestByIDFunc: method is nil but Repository.FindSenderRequestByID was just called")
}
callInfo := struct {
Ctx context.Context
ID ulid.ULID
}{
Ctx: ctx,
ID: id,
}
mock.lockFindSenderRequestByID.Lock()
mock.calls.FindSenderRequestByID = append(mock.calls.FindSenderRequestByID, callInfo)
mock.lockFindSenderRequestByID.Unlock()
return mock.FindSenderRequestByIDFunc(ctx, id)
}
// FindSenderRequestByIDCalls gets all the calls that were made to FindSenderRequestByID.
// Check the length with:
// len(mockedRepository.FindSenderRequestByIDCalls())
func (mock *RepoMock) FindSenderRequestByIDCalls() []struct {
Ctx context.Context
ID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ID ulid.ULID
}
mock.lockFindSenderRequestByID.RLock()
calls = mock.calls.FindSenderRequestByID
mock.lockFindSenderRequestByID.RUnlock()
return calls
}
// FindSenderRequests calls FindSenderRequestsFunc.
func (mock *RepoMock) FindSenderRequests(ctx context.Context, filter sender.FindRequestsFilter, scopeMoqParam *scope.Scope) ([]sender.Request, error) {
if mock.FindSenderRequestsFunc == nil {
panic("RepoMock.FindSenderRequestsFunc: method is nil but Repository.FindSenderRequests was just called")
}
callInfo := struct {
Ctx context.Context
Filter sender.FindRequestsFilter
ScopeMoqParam *scope.Scope
}{
Ctx: ctx,
Filter: filter,
ScopeMoqParam: scopeMoqParam,
}
mock.lockFindSenderRequests.Lock()
mock.calls.FindSenderRequests = append(mock.calls.FindSenderRequests, callInfo)
mock.lockFindSenderRequests.Unlock()
return mock.FindSenderRequestsFunc(ctx, filter, scopeMoqParam)
}
// FindSenderRequestsCalls gets all the calls that were made to FindSenderRequests.
// Check the length with:
// len(mockedRepository.FindSenderRequestsCalls())
func (mock *RepoMock) FindSenderRequestsCalls() []struct {
Ctx context.Context
Filter sender.FindRequestsFilter
ScopeMoqParam *scope.Scope
} {
var calls []struct {
Ctx context.Context
Filter sender.FindRequestsFilter
ScopeMoqParam *scope.Scope
}
mock.lockFindSenderRequests.RLock()
calls = mock.calls.FindSenderRequests
mock.lockFindSenderRequests.RUnlock()
return calls
}
// StoreResponseLog calls StoreResponseLogFunc.
func (mock *RepoMock) StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
if mock.StoreResponseLogFunc == nil {
panic("RepoMock.StoreResponseLogFunc: method is nil but Repository.StoreResponseLog was just called")
}
callInfo := struct {
Ctx context.Context
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
}{
Ctx: ctx,
ReqLogID: reqLogID,
ResLog: resLog,
}
mock.lockStoreResponseLog.Lock()
mock.calls.StoreResponseLog = append(mock.calls.StoreResponseLog, callInfo)
mock.lockStoreResponseLog.Unlock()
return mock.StoreResponseLogFunc(ctx, reqLogID, resLog)
}
// StoreResponseLogCalls gets all the calls that were made to StoreResponseLog.
// Check the length with:
// len(mockedRepository.StoreResponseLogCalls())
func (mock *RepoMock) StoreResponseLogCalls() []struct {
Ctx context.Context
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
} {
var calls []struct {
Ctx context.Context
ReqLogID ulid.ULID
ResLog reqlog.ResponseLog
}
mock.lockStoreResponseLog.RLock()
calls = mock.calls.StoreResponseLog
mock.lockStoreResponseLog.RUnlock()
return calls
}
// StoreSenderRequest calls StoreSenderRequestFunc.
func (mock *RepoMock) StoreSenderRequest(ctx context.Context, req sender.Request) error {
if mock.StoreSenderRequestFunc == nil {
panic("RepoMock.StoreSenderRequestFunc: method is nil but Repository.StoreSenderRequest was just called")
}
callInfo := struct {
Ctx context.Context
Req sender.Request
}{
Ctx: ctx,
Req: req,
}
mock.lockStoreSenderRequest.Lock()
mock.calls.StoreSenderRequest = append(mock.calls.StoreSenderRequest, callInfo)
mock.lockStoreSenderRequest.Unlock()
return mock.StoreSenderRequestFunc(ctx, req)
}
// StoreSenderRequestCalls gets all the calls that were made to StoreSenderRequest.
// Check the length with:
// len(mockedRepository.StoreSenderRequestCalls())
func (mock *RepoMock) StoreSenderRequestCalls() []struct {
Ctx context.Context
Req sender.Request
} {
var calls []struct {
Ctx context.Context
Req sender.Request
}
mock.lockStoreSenderRequest.RLock()
calls = mock.calls.StoreSenderRequest
mock.lockStoreSenderRequest.RUnlock()
return calls
}

View File

@ -1,498 +0,0 @@
// Code generated by moq; DO NOT EDIT.
// github.com/matryer/moq
package sender_test
import (
"context"
"github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/oklog/ulid"
"sync"
)
// Ensure, that ReqLogServiceMock does implement reqlog.Service.
// If this is not the case, regenerate this file with moq.
var _ reqlog.Service = &ReqLogServiceMock{}
// ReqLogServiceMock is a mock implementation of reqlog.Service.
//
// func TestSomethingThatUsesService(t *testing.T) {
//
// // make and configure a mocked reqlog.Service
// mockedService := &ReqLogServiceMock{
// ActiveProjectIDFunc: func() ulid.ULID {
// panic("mock out the ActiveProjectID method")
// },
// BypassOutOfScopeRequestsFunc: func() bool {
// panic("mock out the BypassOutOfScopeRequests method")
// },
// ClearRequestsFunc: func(ctx context.Context, projectID ulid.ULID) error {
// panic("mock out the ClearRequests method")
// },
// FindReqsFilterFunc: func() reqlog.FindRequestsFilter {
// panic("mock out the FindReqsFilter method")
// },
// FindRequestLogByIDFunc: func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
// panic("mock out the FindRequestLogByID method")
// },
// FindRequestsFunc: func(ctx context.Context) ([]reqlog.RequestLog, error) {
// panic("mock out the FindRequests method")
// },
// RequestModifierFunc: func(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
// panic("mock out the RequestModifier method")
// },
// ResponseModifierFunc: func(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
// panic("mock out the ResponseModifier method")
// },
// SetActiveProjectIDFunc: func(id ulid.ULID) {
// panic("mock out the SetActiveProjectID method")
// },
// SetBypassOutOfScopeRequestsFunc: func(b bool) {
// panic("mock out the SetBypassOutOfScopeRequests method")
// },
// SetFindReqsFilterFunc: func(filter reqlog.FindRequestsFilter) {
// panic("mock out the SetFindReqsFilter method")
// },
// }
//
// // use mockedService in code that requires reqlog.Service
// // and then make assertions.
//
// }
type ReqLogServiceMock struct {
// ActiveProjectIDFunc mocks the ActiveProjectID method.
ActiveProjectIDFunc func() ulid.ULID
// BypassOutOfScopeRequestsFunc mocks the BypassOutOfScopeRequests method.
BypassOutOfScopeRequestsFunc func() bool
// ClearRequestsFunc mocks the ClearRequests method.
ClearRequestsFunc func(ctx context.Context, projectID ulid.ULID) error
// FindReqsFilterFunc mocks the FindReqsFilter method.
FindReqsFilterFunc func() reqlog.FindRequestsFilter
// FindRequestLogByIDFunc mocks the FindRequestLogByID method.
FindRequestLogByIDFunc func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error)
// FindRequestsFunc mocks the FindRequests method.
FindRequestsFunc func(ctx context.Context) ([]reqlog.RequestLog, error)
// RequestModifierFunc mocks the RequestModifier method.
RequestModifierFunc func(next proxy.RequestModifyFunc) proxy.RequestModifyFunc
// ResponseModifierFunc mocks the ResponseModifier method.
ResponseModifierFunc func(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc
// SetActiveProjectIDFunc mocks the SetActiveProjectID method.
SetActiveProjectIDFunc func(id ulid.ULID)
// SetBypassOutOfScopeRequestsFunc mocks the SetBypassOutOfScopeRequests method.
SetBypassOutOfScopeRequestsFunc func(b bool)
// SetFindReqsFilterFunc mocks the SetFindReqsFilter method.
SetFindReqsFilterFunc func(filter reqlog.FindRequestsFilter)
// calls tracks calls to the methods.
calls struct {
// ActiveProjectID holds details about calls to the ActiveProjectID method.
ActiveProjectID []struct {
}
// BypassOutOfScopeRequests holds details about calls to the BypassOutOfScopeRequests method.
BypassOutOfScopeRequests []struct {
}
// ClearRequests holds details about calls to the ClearRequests method.
ClearRequests []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ProjectID is the projectID argument value.
ProjectID ulid.ULID
}
// FindReqsFilter holds details about calls to the FindReqsFilter method.
FindReqsFilter []struct {
}
// FindRequestLogByID holds details about calls to the FindRequestLogByID method.
FindRequestLogByID []struct {
// Ctx is the ctx argument value.
Ctx context.Context
// ID is the id argument value.
ID ulid.ULID
}
// FindRequests holds details about calls to the FindRequests method.
FindRequests []struct {
// Ctx is the ctx argument value.
Ctx context.Context
}
// RequestModifier holds details about calls to the RequestModifier method.
RequestModifier []struct {
// Next is the next argument value.
Next proxy.RequestModifyFunc
}
// ResponseModifier holds details about calls to the ResponseModifier method.
ResponseModifier []struct {
// Next is the next argument value.
Next proxy.ResponseModifyFunc
}
// SetActiveProjectID holds details about calls to the SetActiveProjectID method.
SetActiveProjectID []struct {
// ID is the id argument value.
ID ulid.ULID
}
// SetBypassOutOfScopeRequests holds details about calls to the SetBypassOutOfScopeRequests method.
SetBypassOutOfScopeRequests []struct {
// B is the b argument value.
B bool
}
// SetFindReqsFilter holds details about calls to the SetFindReqsFilter method.
SetFindReqsFilter []struct {
// Filter is the filter argument value.
Filter reqlog.FindRequestsFilter
}
}
lockActiveProjectID sync.RWMutex
lockBypassOutOfScopeRequests sync.RWMutex
lockClearRequests sync.RWMutex
lockFindReqsFilter sync.RWMutex
lockFindRequestLogByID sync.RWMutex
lockFindRequests sync.RWMutex
lockRequestModifier sync.RWMutex
lockResponseModifier sync.RWMutex
lockSetActiveProjectID sync.RWMutex
lockSetBypassOutOfScopeRequests sync.RWMutex
lockSetFindReqsFilter sync.RWMutex
}
// ActiveProjectID calls ActiveProjectIDFunc.
func (mock *ReqLogServiceMock) ActiveProjectID() ulid.ULID {
if mock.ActiveProjectIDFunc == nil {
panic("ReqLogServiceMock.ActiveProjectIDFunc: method is nil but Service.ActiveProjectID was just called")
}
callInfo := struct {
}{}
mock.lockActiveProjectID.Lock()
mock.calls.ActiveProjectID = append(mock.calls.ActiveProjectID, callInfo)
mock.lockActiveProjectID.Unlock()
return mock.ActiveProjectIDFunc()
}
// ActiveProjectIDCalls gets all the calls that were made to ActiveProjectID.
// Check the length with:
// len(mockedService.ActiveProjectIDCalls())
func (mock *ReqLogServiceMock) ActiveProjectIDCalls() []struct {
} {
var calls []struct {
}
mock.lockActiveProjectID.RLock()
calls = mock.calls.ActiveProjectID
mock.lockActiveProjectID.RUnlock()
return calls
}
// BypassOutOfScopeRequests calls BypassOutOfScopeRequestsFunc.
func (mock *ReqLogServiceMock) BypassOutOfScopeRequests() bool {
if mock.BypassOutOfScopeRequestsFunc == nil {
panic("ReqLogServiceMock.BypassOutOfScopeRequestsFunc: method is nil but Service.BypassOutOfScopeRequests was just called")
}
callInfo := struct {
}{}
mock.lockBypassOutOfScopeRequests.Lock()
mock.calls.BypassOutOfScopeRequests = append(mock.calls.BypassOutOfScopeRequests, callInfo)
mock.lockBypassOutOfScopeRequests.Unlock()
return mock.BypassOutOfScopeRequestsFunc()
}
// BypassOutOfScopeRequestsCalls gets all the calls that were made to BypassOutOfScopeRequests.
// Check the length with:
// len(mockedService.BypassOutOfScopeRequestsCalls())
func (mock *ReqLogServiceMock) BypassOutOfScopeRequestsCalls() []struct {
} {
var calls []struct {
}
mock.lockBypassOutOfScopeRequests.RLock()
calls = mock.calls.BypassOutOfScopeRequests
mock.lockBypassOutOfScopeRequests.RUnlock()
return calls
}
// ClearRequests calls ClearRequestsFunc.
func (mock *ReqLogServiceMock) ClearRequests(ctx context.Context, projectID ulid.ULID) error {
if mock.ClearRequestsFunc == nil {
panic("ReqLogServiceMock.ClearRequestsFunc: method is nil but Service.ClearRequests was just called")
}
callInfo := struct {
Ctx context.Context
ProjectID ulid.ULID
}{
Ctx: ctx,
ProjectID: projectID,
}
mock.lockClearRequests.Lock()
mock.calls.ClearRequests = append(mock.calls.ClearRequests, callInfo)
mock.lockClearRequests.Unlock()
return mock.ClearRequestsFunc(ctx, projectID)
}
// ClearRequestsCalls gets all the calls that were made to ClearRequests.
// Check the length with:
// len(mockedService.ClearRequestsCalls())
func (mock *ReqLogServiceMock) ClearRequestsCalls() []struct {
Ctx context.Context
ProjectID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ProjectID ulid.ULID
}
mock.lockClearRequests.RLock()
calls = mock.calls.ClearRequests
mock.lockClearRequests.RUnlock()
return calls
}
// FindReqsFilter calls FindReqsFilterFunc.
func (mock *ReqLogServiceMock) FindReqsFilter() reqlog.FindRequestsFilter {
if mock.FindReqsFilterFunc == nil {
panic("ReqLogServiceMock.FindReqsFilterFunc: method is nil but Service.FindReqsFilter was just called")
}
callInfo := struct {
}{}
mock.lockFindReqsFilter.Lock()
mock.calls.FindReqsFilter = append(mock.calls.FindReqsFilter, callInfo)
mock.lockFindReqsFilter.Unlock()
return mock.FindReqsFilterFunc()
}
// FindReqsFilterCalls gets all the calls that were made to FindReqsFilter.
// Check the length with:
// len(mockedService.FindReqsFilterCalls())
func (mock *ReqLogServiceMock) FindReqsFilterCalls() []struct {
} {
var calls []struct {
}
mock.lockFindReqsFilter.RLock()
calls = mock.calls.FindReqsFilter
mock.lockFindReqsFilter.RUnlock()
return calls
}
// FindRequestLogByID calls FindRequestLogByIDFunc.
func (mock *ReqLogServiceMock) FindRequestLogByID(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) {
if mock.FindRequestLogByIDFunc == nil {
panic("ReqLogServiceMock.FindRequestLogByIDFunc: method is nil but Service.FindRequestLogByID was just called")
}
callInfo := struct {
Ctx context.Context
ID ulid.ULID
}{
Ctx: ctx,
ID: id,
}
mock.lockFindRequestLogByID.Lock()
mock.calls.FindRequestLogByID = append(mock.calls.FindRequestLogByID, callInfo)
mock.lockFindRequestLogByID.Unlock()
return mock.FindRequestLogByIDFunc(ctx, id)
}
// FindRequestLogByIDCalls gets all the calls that were made to FindRequestLogByID.
// Check the length with:
// len(mockedService.FindRequestLogByIDCalls())
func (mock *ReqLogServiceMock) FindRequestLogByIDCalls() []struct {
Ctx context.Context
ID ulid.ULID
} {
var calls []struct {
Ctx context.Context
ID ulid.ULID
}
mock.lockFindRequestLogByID.RLock()
calls = mock.calls.FindRequestLogByID
mock.lockFindRequestLogByID.RUnlock()
return calls
}
// FindRequests calls FindRequestsFunc.
func (mock *ReqLogServiceMock) FindRequests(ctx context.Context) ([]reqlog.RequestLog, error) {
if mock.FindRequestsFunc == nil {
panic("ReqLogServiceMock.FindRequestsFunc: method is nil but Service.FindRequests was just called")
}
callInfo := struct {
Ctx context.Context
}{
Ctx: ctx,
}
mock.lockFindRequests.Lock()
mock.calls.FindRequests = append(mock.calls.FindRequests, callInfo)
mock.lockFindRequests.Unlock()
return mock.FindRequestsFunc(ctx)
}
// FindRequestsCalls gets all the calls that were made to FindRequests.
// Check the length with:
// len(mockedService.FindRequestsCalls())
func (mock *ReqLogServiceMock) FindRequestsCalls() []struct {
Ctx context.Context
} {
var calls []struct {
Ctx context.Context
}
mock.lockFindRequests.RLock()
calls = mock.calls.FindRequests
mock.lockFindRequests.RUnlock()
return calls
}
// RequestModifier calls RequestModifierFunc.
func (mock *ReqLogServiceMock) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
if mock.RequestModifierFunc == nil {
panic("ReqLogServiceMock.RequestModifierFunc: method is nil but Service.RequestModifier was just called")
}
callInfo := struct {
Next proxy.RequestModifyFunc
}{
Next: next,
}
mock.lockRequestModifier.Lock()
mock.calls.RequestModifier = append(mock.calls.RequestModifier, callInfo)
mock.lockRequestModifier.Unlock()
return mock.RequestModifierFunc(next)
}
// RequestModifierCalls gets all the calls that were made to RequestModifier.
// Check the length with:
// len(mockedService.RequestModifierCalls())
func (mock *ReqLogServiceMock) RequestModifierCalls() []struct {
Next proxy.RequestModifyFunc
} {
var calls []struct {
Next proxy.RequestModifyFunc
}
mock.lockRequestModifier.RLock()
calls = mock.calls.RequestModifier
mock.lockRequestModifier.RUnlock()
return calls
}
// ResponseModifier calls ResponseModifierFunc.
func (mock *ReqLogServiceMock) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
if mock.ResponseModifierFunc == nil {
panic("ReqLogServiceMock.ResponseModifierFunc: method is nil but Service.ResponseModifier was just called")
}
callInfo := struct {
Next proxy.ResponseModifyFunc
}{
Next: next,
}
mock.lockResponseModifier.Lock()
mock.calls.ResponseModifier = append(mock.calls.ResponseModifier, callInfo)
mock.lockResponseModifier.Unlock()
return mock.ResponseModifierFunc(next)
}
// ResponseModifierCalls gets all the calls that were made to ResponseModifier.
// Check the length with:
// len(mockedService.ResponseModifierCalls())
func (mock *ReqLogServiceMock) ResponseModifierCalls() []struct {
Next proxy.ResponseModifyFunc
} {
var calls []struct {
Next proxy.ResponseModifyFunc
}
mock.lockResponseModifier.RLock()
calls = mock.calls.ResponseModifier
mock.lockResponseModifier.RUnlock()
return calls
}
// SetActiveProjectID calls SetActiveProjectIDFunc.
func (mock *ReqLogServiceMock) SetActiveProjectID(id ulid.ULID) {
if mock.SetActiveProjectIDFunc == nil {
panic("ReqLogServiceMock.SetActiveProjectIDFunc: method is nil but Service.SetActiveProjectID was just called")
}
callInfo := struct {
ID ulid.ULID
}{
ID: id,
}
mock.lockSetActiveProjectID.Lock()
mock.calls.SetActiveProjectID = append(mock.calls.SetActiveProjectID, callInfo)
mock.lockSetActiveProjectID.Unlock()
mock.SetActiveProjectIDFunc(id)
}
// SetActiveProjectIDCalls gets all the calls that were made to SetActiveProjectID.
// Check the length with:
// len(mockedService.SetActiveProjectIDCalls())
func (mock *ReqLogServiceMock) SetActiveProjectIDCalls() []struct {
ID ulid.ULID
} {
var calls []struct {
ID ulid.ULID
}
mock.lockSetActiveProjectID.RLock()
calls = mock.calls.SetActiveProjectID
mock.lockSetActiveProjectID.RUnlock()
return calls
}
// SetBypassOutOfScopeRequests calls SetBypassOutOfScopeRequestsFunc.
func (mock *ReqLogServiceMock) SetBypassOutOfScopeRequests(b bool) {
if mock.SetBypassOutOfScopeRequestsFunc == nil {
panic("ReqLogServiceMock.SetBypassOutOfScopeRequestsFunc: method is nil but Service.SetBypassOutOfScopeRequests was just called")
}
callInfo := struct {
B bool
}{
B: b,
}
mock.lockSetBypassOutOfScopeRequests.Lock()
mock.calls.SetBypassOutOfScopeRequests = append(mock.calls.SetBypassOutOfScopeRequests, callInfo)
mock.lockSetBypassOutOfScopeRequests.Unlock()
mock.SetBypassOutOfScopeRequestsFunc(b)
}
// SetBypassOutOfScopeRequestsCalls gets all the calls that were made to SetBypassOutOfScopeRequests.
// Check the length with:
// len(mockedService.SetBypassOutOfScopeRequestsCalls())
func (mock *ReqLogServiceMock) SetBypassOutOfScopeRequestsCalls() []struct {
B bool
} {
var calls []struct {
B bool
}
mock.lockSetBypassOutOfScopeRequests.RLock()
calls = mock.calls.SetBypassOutOfScopeRequests
mock.lockSetBypassOutOfScopeRequests.RUnlock()
return calls
}
// SetFindReqsFilter calls SetFindReqsFilterFunc.
func (mock *ReqLogServiceMock) SetFindReqsFilter(filter reqlog.FindRequestsFilter) {
if mock.SetFindReqsFilterFunc == nil {
panic("ReqLogServiceMock.SetFindReqsFilterFunc: method is nil but Service.SetFindReqsFilter was just called")
}
callInfo := struct {
Filter reqlog.FindRequestsFilter
}{
Filter: filter,
}
mock.lockSetFindReqsFilter.Lock()
mock.calls.SetFindReqsFilter = append(mock.calls.SetFindReqsFilter, callInfo)
mock.lockSetFindReqsFilter.Unlock()
mock.SetFindReqsFilterFunc(filter)
}
// SetFindReqsFilterCalls gets all the calls that were made to SetFindReqsFilter.
// Check the length with:
// len(mockedService.SetFindReqsFilterCalls())
func (mock *ReqLogServiceMock) SetFindReqsFilterCalls() []struct {
Filter reqlog.FindRequestsFilter
} {
var calls []struct {
Filter reqlog.FindRequestsFilter
}
mock.lockSetFindReqsFilter.RLock()
calls = mock.calls.SetFindReqsFilter
mock.lockSetFindReqsFilter.RUnlock()
return calls
}

View File

@ -5,46 +5,47 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/oklog/ulid" "github.com/oklog/ulid/v2"
"github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/http"
"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{
"req.id": func(req Request) string { return req.ID.String() }, "req.id": func(req *Request) string { return req.Id },
"req.proto": func(req Request) string { return req.Proto }, "req.proto": func(req *Request) string { return req.GetHttpRequest().GetProtocol().String() },
"req.url": func(req Request) string { "req.url": func(req *Request) string { return req.GetHttpRequest().GetUrl() },
if req.URL == nil { "req.method": func(req *Request) string { return req.GetHttpRequest().GetMethod().String() },
"req.body": func(req *Request) string { return string(req.GetHttpRequest().GetBody()) },
"req.timestamp": func(req *Request) string {
id, err := ulid.Parse(req.Id)
if err != nil {
return "" return ""
} }
return req.URL.String() return ulid.Time(id.Time()).String()
}, },
"req.method": func(req Request) string { return req.Method },
"req.body": func(req Request) string { return string(req.Body) },
"req.timestamp": func(req Request) string { return ulid.Time(req.ID.Time()).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 +57,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 +71,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 +85,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.GetHttpRequest().GetHeaders())
if err != nil {
return false, fmt.Errorf("failed to match request HTTP headers: %w", err)
}
return match, nil
}
if leftVal == "res.headers" && req.GetHttpResponse() != nil {
match, err := filter.MatchHTTPHeaders(expr.Operator, expr.Right, req.GetHttpResponse().GetHeaders())
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 +132,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:
@ -134,7 +153,7 @@ func (req Request) matchInfixExpr(expr search.InfixExpression) (bool, error) {
} }
} }
func (req Request) getMappedStringLiteral(s string) string { func (req *Request) getMappedStringLiteral(s string) string {
switch { switch {
case strings.HasPrefix(s, "req."): case strings.HasPrefix(s, "req."):
fn, ok := senderReqSearchKeyFns[s] fn, ok := senderReqSearchKeyFns[s]
@ -142,20 +161,25 @@ func (req Request) getMappedStringLiteral(s string) string {
return fn(req) return fn(req)
} }
case strings.HasPrefix(s, "res."): case strings.HasPrefix(s, "res."):
if req.Response == nil { fn, ok := http.ResponseSearchKeyFns[s]
return ""
}
fn, ok := reqlog.ResLogSearchKeyFns[s]
if ok { if ok {
return fn(*req.Response) return fn(req.GetHttpResponse())
} }
} }
return s return s
} }
func (req Request) matchStringLiteral(strLiteral search.StringLiteral) (bool, error) { func (req *Request) matchStringLiteral(strLiteral filter.StringLiteral) (bool, error) {
for _, header := range req.GetHttpRequest().GetHeaders() {
if strings.Contains(
strings.ToLower(fmt.Sprintf("%v: %v", header.Key, header.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)),
@ -165,43 +189,47 @@ func (req Request) matchStringLiteral(strLiteral search.StringLiteral) (bool, er
} }
} }
if req.Response != nil { for _, header := range req.GetHttpResponse().GetHeaders() {
for _, fn := range reqlog.ResLogSearchKeyFns {
if strings.Contains( if strings.Contains(
strings.ToLower(fn(*req.Response)), strings.ToLower(fmt.Sprintf("%v: %v", header.Key, header.Value)),
strings.ToLower(strLiteral.Value), strings.ToLower(strLiteral.Value),
) { ) {
return true, nil return true, nil
} }
} }
for _, fn := range http.ResponseSearchKeyFns {
if strings.Contains(
strings.ToLower(fn(req.GetHttpResponse())),
strings.ToLower(strLiteral.Value),
) {
return true, nil
}
} }
return false, nil return false, nil
} }
func (req Request) MatchScope(s *scope.Scope) bool { func (req *Request) MatchScope(s *scope.Scope) bool {
for _, rule := range s.Rules() { for _, rule := range s.Rules() {
if rule.URL != nil && req.URL != nil { if url := req.GetHttpRequest().GetUrl(); rule.URL != nil && url != "" {
if matches := rule.URL.MatchString(req.URL.String()); matches { if matches := rule.URL.MatchString(url); matches {
return true return true
} }
} }
for key, values := range req.Header { for _, headers := range req.GetHttpRequest().GetHeaders() {
var keyMatches, valueMatches bool var keyMatches, valueMatches bool
if rule.Header.Key != nil { if rule.Header.Key != nil {
if matches := rule.Header.Key.MatchString(key); matches { if matches := rule.Header.Key.MatchString(headers.Key); matches {
keyMatches = true keyMatches = true
} }
} }
if rule.Header.Value != nil { if rule.Header.Value != nil {
for _, value := range values { if matches := rule.Header.Value.MatchString(headers.Value); matches {
if matches := rule.Header.Value.MatchString(value); matches {
valueMatches = true valueMatches = true
break
}
} }
} }
// When only key or value is set, match on whatever is set. // When only key or value is set, match on whatever is set.
@ -217,7 +245,7 @@ func (req Request) MatchScope(s *scope.Scope) bool {
} }
if rule.Body != nil { if rule.Body != nil {
if matches := rule.Body.Match(req.Body); matches { if matches := rule.Body.Match(req.GetHttpRequest().GetBody()); matches {
return true return true
} }
} }

View File

@ -3,8 +3,8 @@ package sender_test
import ( import (
"testing" "testing"
"github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/filter"
"github.com/dstotijn/hetty/pkg/search" "github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/sender" "github.com/dstotijn/hetty/pkg/sender"
) )
@ -14,106 +14,128 @@ func TestRequestLogMatch(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
query string query string
senderReq sender.Request senderReq *sender.Request
expectedMatch bool expectedMatch bool
expectedError error expectedError error
}{ }{
{ {
name: "infix expression, equal operator, match", name: "infix expression, equal operator, match",
query: "req.body = foo", query: "req.body = foo",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("foo"), Body: []byte("foo"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, not equal operator, match", name: "infix expression, not equal operator, match",
query: "req.body != bar", query: "req.body != bar",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("foo"), Body: []byte("foo"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, greater than operator, match", name: "infix expression, greater than operator, match",
query: "req.body > a", query: "req.body > a",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("b"), Body: []byte("b"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, less than operator, match", name: "infix expression, less than operator, match",
query: "req.body < b", query: "req.body < b",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("a"), Body: []byte("a"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, greater than or equal operator, match greater than", name: "infix expression, greater than or equal operator, match greater than",
query: "req.body >= a", query: "req.body >= a",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("b"), Body: []byte("b"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, greater than or equal operator, match equal", name: "infix expression, greater than or equal operator, match equal",
query: "req.body >= a", query: "req.body >= a",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("a"), Body: []byte("a"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, less than or equal operator, match less than", name: "infix expression, less than or equal operator, match less than",
query: "req.body <= b", query: "req.body <= b",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("a"), Body: []byte("a"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, less than or equal operator, match equal", name: "infix expression, less than or equal operator, match equal",
query: "req.body <= b", query: "req.body <= b",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("b"), Body: []byte("b"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, regular expression operator, match", name: "infix expression, regular expression operator, match",
query: `req.body =~ "^foo(.*)$"`, query: `req.body =~ "^foo(.*)$"`,
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("foobar"), Body: []byte("foobar"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, negate regular expression operator, match", name: "infix expression, negate regular expression operator, match",
query: `req.body !~ "^foo(.*)$"`, query: `req.body !~ "^foo(.*)$"`,
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("xoobar"), Body: []byte("xoobar"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "infix expression, and operator, match", name: "infix expression, and operator, match",
query: "req.body = bar AND res.body = yolo", query: "req.body = bar AND res.body = yolo",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("bar"), Body: []byte("bar"),
Response: &reqlog.ResponseLog{ },
HttpResponse: &http.Response{
Body: []byte("yolo"), Body: []byte("yolo"),
}, },
}, },
@ -123,9 +145,11 @@ func TestRequestLogMatch(t *testing.T) {
{ {
name: "infix expression, or operator, match", name: "infix expression, or operator, match",
query: "req.body = bar OR res.body = yolo", query: "req.body = bar OR res.body = yolo",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("foo"), Body: []byte("foo"),
Response: &reqlog.ResponseLog{ },
HttpResponse: &http.Response{
Body: []byte("yolo"), Body: []byte("yolo"),
}, },
}, },
@ -135,35 +159,41 @@ func TestRequestLogMatch(t *testing.T) {
{ {
name: "prefix expression, not operator, match", name: "prefix expression, not operator, match",
query: "NOT (req.body = bar)", query: "NOT (req.body = bar)",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("foo"), Body: []byte("foo"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "string literal expression, match in request log", name: "string literal expression, match in request log",
query: "foo", query: "foo",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("foo"), Body: []byte("foo"),
}, },
},
expectedMatch: true, expectedMatch: true,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "string literal expression, no match", name: "string literal expression, no match",
query: "foo", query: "foo",
senderReq: sender.Request{ senderReq: &sender.Request{
HttpRequest: &http.Request{
Body: []byte("bar"), Body: []byte("bar"),
}, },
},
expectedMatch: false, expectedMatch: false,
expectedError: nil, expectedError: nil,
}, },
{ {
name: "string literal expression, match in response log", name: "string literal expression, match in response log",
query: "foo", query: "foo",
senderReq: sender.Request{ senderReq: &sender.Request{
Response: &reqlog.ResponseLog{ HttpResponse: &http.Response{
Body: []byte("foo"), Body: []byte("foo"),
}, },
}, },
@ -177,7 +207,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)

View File

@ -0,0 +1,311 @@
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: sender/sender.proto
package sender
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// SenderServiceName is the fully-qualified name of the SenderService service.
SenderServiceName = "sender.SenderService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// SenderServiceGetRequestByIDProcedure is the fully-qualified name of the SenderService's
// GetRequestByID RPC.
SenderServiceGetRequestByIDProcedure = "/sender.SenderService/GetRequestByID"
// SenderServiceListRequestsProcedure is the fully-qualified name of the SenderService's
// ListRequests RPC.
SenderServiceListRequestsProcedure = "/sender.SenderService/ListRequests"
// SenderServiceSetRequestsFilterProcedure is the fully-qualified name of the SenderService's
// SetRequestsFilter RPC.
SenderServiceSetRequestsFilterProcedure = "/sender.SenderService/SetRequestsFilter"
// SenderServiceGetRequestsFilterProcedure is the fully-qualified name of the SenderService's
// GetRequestsFilter RPC.
SenderServiceGetRequestsFilterProcedure = "/sender.SenderService/GetRequestsFilter"
// SenderServiceCreateOrUpdateRequestProcedure is the fully-qualified name of the SenderService's
// CreateOrUpdateRequest RPC.
SenderServiceCreateOrUpdateRequestProcedure = "/sender.SenderService/CreateOrUpdateRequest"
// SenderServiceCloneFromRequestLogProcedure is the fully-qualified name of the SenderService's
// CloneFromRequestLog RPC.
SenderServiceCloneFromRequestLogProcedure = "/sender.SenderService/CloneFromRequestLog"
// SenderServiceSendRequestProcedure is the fully-qualified name of the SenderService's SendRequest
// RPC.
SenderServiceSendRequestProcedure = "/sender.SenderService/SendRequest"
// SenderServiceDeleteRequestsProcedure is the fully-qualified name of the SenderService's
// DeleteRequests RPC.
SenderServiceDeleteRequestsProcedure = "/sender.SenderService/DeleteRequests"
)
// SenderServiceClient is a client for the sender.SenderService service.
type SenderServiceClient interface {
GetRequestByID(context.Context, *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error)
ListRequests(context.Context, *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error)
SetRequestsFilter(context.Context, *connect.Request[SetRequestsFilterRequest]) (*connect.Response[SetRequestsFilterResponse], error)
GetRequestsFilter(context.Context, *connect.Request[GetRequestsFilterRequest]) (*connect.Response[GetRequestsFilterResponse], error)
CreateOrUpdateRequest(context.Context, *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error)
CloneFromRequestLog(context.Context, *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error)
SendRequest(context.Context, *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error)
DeleteRequests(context.Context, *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error)
}
// NewSenderServiceClient constructs a client for the sender.SenderService service. By default, it
// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends
// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or
// connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewSenderServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SenderServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
senderServiceMethods := File_sender_sender_proto.Services().ByName("SenderService").Methods()
return &senderServiceClient{
getRequestByID: connect.NewClient[GetRequestByIDRequest, GetRequestByIDResponse](
httpClient,
baseURL+SenderServiceGetRequestByIDProcedure,
connect.WithSchema(senderServiceMethods.ByName("GetRequestByID")),
connect.WithClientOptions(opts...),
),
listRequests: connect.NewClient[ListRequestsRequest, ListRequestsResponse](
httpClient,
baseURL+SenderServiceListRequestsProcedure,
connect.WithSchema(senderServiceMethods.ByName("ListRequests")),
connect.WithClientOptions(opts...),
),
setRequestsFilter: connect.NewClient[SetRequestsFilterRequest, SetRequestsFilterResponse](
httpClient,
baseURL+SenderServiceSetRequestsFilterProcedure,
connect.WithSchema(senderServiceMethods.ByName("SetRequestsFilter")),
connect.WithClientOptions(opts...),
),
getRequestsFilter: connect.NewClient[GetRequestsFilterRequest, GetRequestsFilterResponse](
httpClient,
baseURL+SenderServiceGetRequestsFilterProcedure,
connect.WithSchema(senderServiceMethods.ByName("GetRequestsFilter")),
connect.WithClientOptions(opts...),
),
createOrUpdateRequest: connect.NewClient[CreateOrUpdateRequestRequest, CreateOrUpdateRequestResponse](
httpClient,
baseURL+SenderServiceCreateOrUpdateRequestProcedure,
connect.WithSchema(senderServiceMethods.ByName("CreateOrUpdateRequest")),
connect.WithClientOptions(opts...),
),
cloneFromRequestLog: connect.NewClient[CloneFromRequestLogRequest, CloneFromRequestLogResponse](
httpClient,
baseURL+SenderServiceCloneFromRequestLogProcedure,
connect.WithSchema(senderServiceMethods.ByName("CloneFromRequestLog")),
connect.WithClientOptions(opts...),
),
sendRequest: connect.NewClient[SendRequestRequest, SendRequestResponse](
httpClient,
baseURL+SenderServiceSendRequestProcedure,
connect.WithSchema(senderServiceMethods.ByName("SendRequest")),
connect.WithClientOptions(opts...),
),
deleteRequests: connect.NewClient[DeleteRequestsRequest, DeleteRequestsResponse](
httpClient,
baseURL+SenderServiceDeleteRequestsProcedure,
connect.WithSchema(senderServiceMethods.ByName("DeleteRequests")),
connect.WithClientOptions(opts...),
),
}
}
// senderServiceClient implements SenderServiceClient.
type senderServiceClient struct {
getRequestByID *connect.Client[GetRequestByIDRequest, GetRequestByIDResponse]
listRequests *connect.Client[ListRequestsRequest, ListRequestsResponse]
setRequestsFilter *connect.Client[SetRequestsFilterRequest, SetRequestsFilterResponse]
getRequestsFilter *connect.Client[GetRequestsFilterRequest, GetRequestsFilterResponse]
createOrUpdateRequest *connect.Client[CreateOrUpdateRequestRequest, CreateOrUpdateRequestResponse]
cloneFromRequestLog *connect.Client[CloneFromRequestLogRequest, CloneFromRequestLogResponse]
sendRequest *connect.Client[SendRequestRequest, SendRequestResponse]
deleteRequests *connect.Client[DeleteRequestsRequest, DeleteRequestsResponse]
}
// GetRequestByID calls sender.SenderService.GetRequestByID.
func (c *senderServiceClient) GetRequestByID(ctx context.Context, req *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error) {
return c.getRequestByID.CallUnary(ctx, req)
}
// ListRequests calls sender.SenderService.ListRequests.
func (c *senderServiceClient) ListRequests(ctx context.Context, req *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error) {
return c.listRequests.CallUnary(ctx, req)
}
// SetRequestsFilter calls sender.SenderService.SetRequestsFilter.
func (c *senderServiceClient) SetRequestsFilter(ctx context.Context, req *connect.Request[SetRequestsFilterRequest]) (*connect.Response[SetRequestsFilterResponse], error) {
return c.setRequestsFilter.CallUnary(ctx, req)
}
// GetRequestsFilter calls sender.SenderService.GetRequestsFilter.
func (c *senderServiceClient) GetRequestsFilter(ctx context.Context, req *connect.Request[GetRequestsFilterRequest]) (*connect.Response[GetRequestsFilterResponse], error) {
return c.getRequestsFilter.CallUnary(ctx, req)
}
// CreateOrUpdateRequest calls sender.SenderService.CreateOrUpdateRequest.
func (c *senderServiceClient) CreateOrUpdateRequest(ctx context.Context, req *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error) {
return c.createOrUpdateRequest.CallUnary(ctx, req)
}
// CloneFromRequestLog calls sender.SenderService.CloneFromRequestLog.
func (c *senderServiceClient) CloneFromRequestLog(ctx context.Context, req *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error) {
return c.cloneFromRequestLog.CallUnary(ctx, req)
}
// SendRequest calls sender.SenderService.SendRequest.
func (c *senderServiceClient) SendRequest(ctx context.Context, req *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error) {
return c.sendRequest.CallUnary(ctx, req)
}
// DeleteRequests calls sender.SenderService.DeleteRequests.
func (c *senderServiceClient) DeleteRequests(ctx context.Context, req *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error) {
return c.deleteRequests.CallUnary(ctx, req)
}
// SenderServiceHandler is an implementation of the sender.SenderService service.
type SenderServiceHandler interface {
GetRequestByID(context.Context, *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error)
ListRequests(context.Context, *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error)
SetRequestsFilter(context.Context, *connect.Request[SetRequestsFilterRequest]) (*connect.Response[SetRequestsFilterResponse], error)
GetRequestsFilter(context.Context, *connect.Request[GetRequestsFilterRequest]) (*connect.Response[GetRequestsFilterResponse], error)
CreateOrUpdateRequest(context.Context, *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error)
CloneFromRequestLog(context.Context, *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error)
SendRequest(context.Context, *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error)
DeleteRequests(context.Context, *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error)
}
// NewSenderServiceHandler builds an HTTP handler from the service implementation. It returns the
// path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewSenderServiceHandler(svc SenderServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
senderServiceMethods := File_sender_sender_proto.Services().ByName("SenderService").Methods()
senderServiceGetRequestByIDHandler := connect.NewUnaryHandler(
SenderServiceGetRequestByIDProcedure,
svc.GetRequestByID,
connect.WithSchema(senderServiceMethods.ByName("GetRequestByID")),
connect.WithHandlerOptions(opts...),
)
senderServiceListRequestsHandler := connect.NewUnaryHandler(
SenderServiceListRequestsProcedure,
svc.ListRequests,
connect.WithSchema(senderServiceMethods.ByName("ListRequests")),
connect.WithHandlerOptions(opts...),
)
senderServiceSetRequestsFilterHandler := connect.NewUnaryHandler(
SenderServiceSetRequestsFilterProcedure,
svc.SetRequestsFilter,
connect.WithSchema(senderServiceMethods.ByName("SetRequestsFilter")),
connect.WithHandlerOptions(opts...),
)
senderServiceGetRequestsFilterHandler := connect.NewUnaryHandler(
SenderServiceGetRequestsFilterProcedure,
svc.GetRequestsFilter,
connect.WithSchema(senderServiceMethods.ByName("GetRequestsFilter")),
connect.WithHandlerOptions(opts...),
)
senderServiceCreateOrUpdateRequestHandler := connect.NewUnaryHandler(
SenderServiceCreateOrUpdateRequestProcedure,
svc.CreateOrUpdateRequest,
connect.WithSchema(senderServiceMethods.ByName("CreateOrUpdateRequest")),
connect.WithHandlerOptions(opts...),
)
senderServiceCloneFromRequestLogHandler := connect.NewUnaryHandler(
SenderServiceCloneFromRequestLogProcedure,
svc.CloneFromRequestLog,
connect.WithSchema(senderServiceMethods.ByName("CloneFromRequestLog")),
connect.WithHandlerOptions(opts...),
)
senderServiceSendRequestHandler := connect.NewUnaryHandler(
SenderServiceSendRequestProcedure,
svc.SendRequest,
connect.WithSchema(senderServiceMethods.ByName("SendRequest")),
connect.WithHandlerOptions(opts...),
)
senderServiceDeleteRequestsHandler := connect.NewUnaryHandler(
SenderServiceDeleteRequestsProcedure,
svc.DeleteRequests,
connect.WithSchema(senderServiceMethods.ByName("DeleteRequests")),
connect.WithHandlerOptions(opts...),
)
return "/sender.SenderService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case SenderServiceGetRequestByIDProcedure:
senderServiceGetRequestByIDHandler.ServeHTTP(w, r)
case SenderServiceListRequestsProcedure:
senderServiceListRequestsHandler.ServeHTTP(w, r)
case SenderServiceSetRequestsFilterProcedure:
senderServiceSetRequestsFilterHandler.ServeHTTP(w, r)
case SenderServiceGetRequestsFilterProcedure:
senderServiceGetRequestsFilterHandler.ServeHTTP(w, r)
case SenderServiceCreateOrUpdateRequestProcedure:
senderServiceCreateOrUpdateRequestHandler.ServeHTTP(w, r)
case SenderServiceCloneFromRequestLogProcedure:
senderServiceCloneFromRequestLogHandler.ServeHTTP(w, r)
case SenderServiceSendRequestProcedure:
senderServiceSendRequestHandler.ServeHTTP(w, r)
case SenderServiceDeleteRequestsProcedure:
senderServiceDeleteRequestsHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedSenderServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedSenderServiceHandler struct{}
func (UnimplementedSenderServiceHandler) GetRequestByID(context.Context, *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.GetRequestByID is not implemented"))
}
func (UnimplementedSenderServiceHandler) ListRequests(context.Context, *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.ListRequests is not implemented"))
}
func (UnimplementedSenderServiceHandler) SetRequestsFilter(context.Context, *connect.Request[SetRequestsFilterRequest]) (*connect.Response[SetRequestsFilterResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.SetRequestsFilter is not implemented"))
}
func (UnimplementedSenderServiceHandler) GetRequestsFilter(context.Context, *connect.Request[GetRequestsFilterRequest]) (*connect.Response[GetRequestsFilterResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.GetRequestsFilter is not implemented"))
}
func (UnimplementedSenderServiceHandler) CreateOrUpdateRequest(context.Context, *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.CreateOrUpdateRequest is not implemented"))
}
func (UnimplementedSenderServiceHandler) CloneFromRequestLog(context.Context, *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.CloneFromRequestLog is not implemented"))
}
func (UnimplementedSenderServiceHandler) SendRequest(context.Context, *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.SendRequest is not implemented"))
}
func (UnimplementedSenderServiceHandler) DeleteRequests(context.Context, *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("sender.SenderService.DeleteRequests is not implemented"))
}

View File

@ -5,21 +5,19 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math/rand"
"net/http" "net/http"
"net/url"
"time" "time"
"github.com/oklog/ulid" connect "connectrpc.com/connect"
"github.com/oklog/ulid/v2"
"google.golang.org/protobuf/proto"
"github.com/dstotijn/hetty/pkg/filter"
httppb "github.com/dstotijn/hetty/pkg/http"
"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
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
var defaultHTTPClient = &http.Client{ var defaultHTTPClient = &http.Client{
Transport: &HTTPTransport{}, Transport: &HTTPTransport{},
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
@ -30,37 +28,19 @@ var (
ErrRequestNotFound = errors.New("sender: request not found") ErrRequestNotFound = errors.New("sender: request not found")
) )
type Service interface { type Service struct {
FindRequestByID(ctx context.Context, id ulid.ULID) (Request, error) activeProjectID string
FindRequests(ctx context.Context) ([]Request, error) reqsFilter *RequestsFilter
CreateOrUpdateRequest(ctx context.Context, req Request) (Request, error)
CloneFromRequestLog(ctx context.Context, reqLogID ulid.ULID) (Request, error)
DeleteRequests(ctx context.Context, projectID ulid.ULID) error
SendRequest(ctx context.Context, id ulid.ULID) (Request, error)
SetActiveProjectID(ulid.ULID)
SetFindReqsFilter(filter FindRequestsFilter)
FindReqsFilter() FindRequestsFilter
}
type service struct {
activeProjectID ulid.ULID
findReqsFilter FindRequestsFilter
scope *scope.Scope scope *scope.Scope
repo Repository repo Repository
reqLogSvc reqlog.Service reqLogSvc *reqlog.Service
httpClient *http.Client httpClient *http.Client
} }
type FindRequestsFilter struct {
ProjectID ulid.ULID
OnlyInScope bool
SearchExpr search.Expression
}
type Config struct { type Config struct {
Scope *scope.Scope Scope *scope.Scope
Repository Repository Repository Repository
ReqLogService reqlog.Service ReqLogService *reqlog.Service
HTTPClient *http.Client HTTPClient *http.Client
} }
@ -68,8 +48,8 @@ type SendError struct {
err error err error
} }
func NewService(cfg Config) Service { func NewService(cfg Config) *Service {
svc := &service{ svc := &Service{
repo: cfg.Repository, repo: cfg.Repository,
reqLogSvc: cfg.ReqLogService, reqLogSvc: cfg.ReqLogService,
httpClient: defaultHTTPClient, httpClient: defaultHTTPClient,
@ -83,163 +63,215 @@ func NewService(cfg Config) Service {
return svc return svc
} }
type Request struct { func (svc *Service) GetRequestByID(ctx context.Context, req *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error) {
ID ulid.ULID if svc.activeProjectID == "" {
ProjectID ulid.ULID return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
SourceRequestLogID ulid.ULID }
URL *url.URL senderReq, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, req.Msg.RequestId)
Method string
Proto string
Header http.Header
Body []byte
Response *reqlog.ResponseLog
}
func (svc *service) FindRequestByID(ctx context.Context, id ulid.ULID) (Request, error) {
req, err := svc.repo.FindSenderRequestByID(ctx, id)
if err != nil { if err != nil {
return Request{}, fmt.Errorf("sender: failed to find request: %w", err) return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("sender: failed to find request: %w", err))
} }
return req, nil return &connect.Response[GetRequestByIDResponse]{
Msg: &GetRequestByIDResponse{Request: senderReq},
}, nil
} }
func (svc *service) FindRequests(ctx context.Context) ([]Request, error) { func (svc *Service) ListRequests(ctx context.Context, req *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error) {
return svc.repo.FindSenderRequests(ctx, svc.findReqsFilter, svc.scope) reqs, err := svc.repo.FindSenderRequests(ctx, svc.activeProjectID, svc.filterRequest)
}
func (svc *service) CreateOrUpdateRequest(ctx context.Context, req Request) (Request, error) {
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 {
return Request{}, ErrProjectIDMustBeSet
}
if req.ID.Compare(ulid.ULID{}) == 0 {
req.ID = ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
}
req.ProjectID = svc.activeProjectID
if req.Method == "" {
req.Method = http.MethodGet
}
if req.Proto == "" {
req.Proto = HTTPProto20
}
if !isValidProto(req.Proto) {
return Request{}, fmt.Errorf("sender: unsupported HTTP protocol: %v", req.Proto)
}
err := svc.repo.StoreSenderRequest(ctx, req)
if err != nil { if err != nil {
return Request{}, fmt.Errorf("sender: failed to store request: %w", err) return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to find requests: %w", err))
} }
return req, nil return &connect.Response[ListRequestsResponse]{
Msg: &ListRequestsResponse{Requests: reqs},
}, nil
} }
func (svc *service) CloneFromRequestLog(ctx context.Context, reqLogID ulid.ULID) (Request, error) { func (svc *Service) filterRequest(req *Request) (bool, error) {
if svc.activeProjectID.Compare(ulid.ULID{}) == 0 { if svc.reqsFilter.OnlyInScope {
return Request{}, ErrProjectIDMustBeSet if svc.scope != nil && !req.MatchScope(svc.scope) {
return false, nil
}
} }
reqLog, err := svc.reqLogSvc.FindRequestLogByID(ctx, reqLogID) if svc.reqsFilter.SearchExpr == "" {
return true, nil
}
expr, err := filter.ParseQuery(svc.reqsFilter.SearchExpr)
if err != nil { if err != nil {
return Request{}, fmt.Errorf("sender: failed to find request log: %w", err) return false, fmt.Errorf("failed to parse search expression: %w", err)
} }
req := Request{ match, err := req.Matches(expr)
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: svc.activeProjectID,
SourceRequestLogID: reqLogID,
Method: reqLog.Method,
URL: reqLog.URL,
Proto: HTTPProto20, // Attempt HTTP/2.
Header: reqLog.Header,
Body: reqLog.Body,
}
err = svc.repo.StoreSenderRequest(ctx, req)
if err != nil { if err != nil {
return Request{}, fmt.Errorf("sender: failed to store request: %w", err) return false, fmt.Errorf("failed to match search expression for sender request (id: %v): %w",
req.Id, err,
)
} }
return req, nil return match, nil
} }
func (svc *service) SetFindReqsFilter(filter FindRequestsFilter) { func (svc *Service) CreateOrUpdateRequest(ctx context.Context, req *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error) {
svc.findReqsFilter = filter if svc.activeProjectID == "" {
} return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
func (svc *service) FindReqsFilter() FindRequestsFilter { r := proto.Clone(req.Msg.Request).(*Request)
return svc.findReqsFilter
}
func (svc *service) SendRequest(ctx context.Context, id ulid.ULID) (Request, error) { if r == nil {
req, err := svc.repo.FindSenderRequestByID(ctx, id) return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("sender: request is nil"))
}
if r.HttpRequest == nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("sender: request.http_request is nil"))
}
if r.Id == "" {
r.Id = ulid.Make().String()
}
r.ProjectId = svc.activeProjectID
if r.HttpRequest.Method == httppb.Method_METHOD_UNSPECIFIED {
r.HttpRequest.Method = httppb.Method_METHOD_GET
}
if r.HttpRequest.Protocol == httppb.Protocol_PROTOCOL_UNSPECIFIED {
r.HttpRequest.Protocol = httppb.Protocol_PROTOCOL_HTTP20
}
err := svc.repo.StoreSenderRequest(ctx, r)
if err != nil { if err != nil {
return Request{}, fmt.Errorf("sender: failed to find request: %w", err) return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store request: %w", err))
}
return &connect.Response[CreateOrUpdateRequestResponse]{
Msg: &CreateOrUpdateRequestResponse{
Request: r,
},
}, nil
}
func (svc *Service) CloneFromRequestLog(ctx context.Context, req *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error) {
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
reqLog, err := svc.reqLogSvc.FindRequestLogByID(ctx, req.Msg.RequestLogId)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to find request log: %w", err))
}
clonedReqLog := proto.Clone(reqLog).(*reqlog.HttpRequestLog)
senderReq := &Request{
Id: ulid.Make().String(),
ProjectId: svc.activeProjectID,
SourceRequestLogId: clonedReqLog.Id,
HttpRequest: clonedReqLog.Request,
}
err = svc.repo.StoreSenderRequest(ctx, senderReq)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store request: %w", err))
}
return &connect.Response[CloneFromRequestLogResponse]{Msg: &CloneFromRequestLogResponse{
Request: senderReq,
}}, nil
}
func (svc *Service) SetRequestsFilter(filter *RequestsFilter) {
svc.reqsFilter = filter
}
func (svc *Service) RequestsFilter() *RequestsFilter {
return svc.reqsFilter
}
func (svc *Service) SendRequest(ctx context.Context, connReq *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error) {
req, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, connReq.Msg.RequestId)
if err != nil {
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("sender: failed to find request: %w", err))
} }
httpReq, err := parseHTTPRequest(ctx, req) httpReq, err := parseHTTPRequest(ctx, req)
if err != nil { if err != nil {
return Request{}, fmt.Errorf("sender: failed to parse HTTP request: %w", err) return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to parse HTTP request: %w", err))
} }
resLog, err := svc.sendHTTPRequest(httpReq) httpRes, err := svc.sendHTTPRequest(httpReq)
if err != nil { if err != nil {
return Request{}, fmt.Errorf("sender: could not send HTTP request: %w", err) return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: could not send HTTP request: %w", err))
} }
err = svc.repo.StoreResponseLog(ctx, id, resLog) req.HttpResponse = httpRes
err = svc.repo.StoreSenderRequest(ctx, req)
if err != nil { if err != nil {
return Request{}, fmt.Errorf("sender: failed to store sender response log: %w", err) return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store sender response log: %w", err))
} }
req.Response = &resLog return &connect.Response[SendRequestResponse]{
Msg: &SendRequestResponse{
return req, nil Request: req,
},
}, nil
} }
func parseHTTPRequest(ctx context.Context, req Request) (*http.Request, error) { func parseHTTPRequest(ctx context.Context, req *Request) (*http.Request, error) {
ctx = context.WithValue(ctx, protoCtxKey{}, req.Proto) ctx = context.WithValue(ctx, protoCtxKey{}, req.GetHttpRequest().GetProtocol())
httpReq, err := http.NewRequestWithContext(ctx, req.Method, req.URL.String(), bytes.NewReader(req.Body)) httpReq, err := http.NewRequestWithContext(ctx,
req.GetHttpRequest().GetMethod().String(),
req.GetHttpRequest().GetUrl(),
bytes.NewReader(req.GetHttpRequest().GetBody()),
)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to construct HTTP request: %w", err) return nil, fmt.Errorf("failed to construct HTTP request: %w", err)
} }
if req.Header != nil { for _, header := range req.GetHttpRequest().GetHeaders() {
httpReq.Header = req.Header httpReq.Header.Add(header.Key, header.Value)
} }
return httpReq, nil return httpReq, nil
} }
func (svc *service) sendHTTPRequest(httpReq *http.Request) (reqlog.ResponseLog, error) { func (svc *Service) sendHTTPRequest(httpReq *http.Request) (*httppb.Response, error) {
res, err := svc.httpClient.Do(httpReq) res, err := svc.httpClient.Do(httpReq)
if err != nil { if err != nil {
return reqlog.ResponseLog{}, &SendError{err} return nil, &SendError{err}
} }
defer res.Body.Close() defer res.Body.Close()
resLog, err := reqlog.ParseHTTPResponse(res) resLog, err := httppb.ParseHTTPResponse(res)
if err != nil { if err != nil {
return reqlog.ResponseLog{}, fmt.Errorf("failed to parse http response: %w", err) return nil, fmt.Errorf("failed to parse http response: %w", err)
} }
return resLog, err return resLog, err
} }
func (svc *service) SetActiveProjectID(id ulid.ULID) { func (svc *Service) SetActiveProjectID(id string) {
svc.activeProjectID = id svc.activeProjectID = id
} }
func (svc *service) DeleteRequests(ctx context.Context, projectID ulid.ULID) error { func (svc *Service) DeleteRequests(ctx context.Context, req *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error) {
return svc.repo.DeleteSenderRequests(ctx, projectID) if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
err := svc.repo.DeleteSenderRequests(ctx, svc.activeProjectID)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to delete requests: %w", err))
}
return &connect.Response[DeleteRequestsResponse]{}, nil
} }
func (e SendError) Error() string { func (e SendError) Error() string {

1040
pkg/sender/sender.pb.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +1,27 @@
package sender_test package sender_test
//go:generate go run github.com/matryer/moq -out reqlog_mock_test.go -pkg sender_test ../reqlog Service:ReqLogServiceMock
//go:generate go run github.com/matryer/moq -out repo_mock_test.go -pkg sender_test . Repository:RepoMock
import ( import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math/rand" http "net/http"
"net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp" connect "connectrpc.com/connect"
"github.com/oklog/ulid" "go.etcd.io/bbolt"
"google.golang.org/protobuf/testing/protocmp"
"github.com/dstotijn/hetty/pkg/db/bolt"
httppb "github.com/dstotijn/hetty/pkg/http"
"github.com/dstotijn/hetty/pkg/proj"
"github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/sender" "github.com/dstotijn/hetty/pkg/sender"
"github.com/dstotijn/hetty/pkg/testutil"
"github.com/google/go-cmp/cmp"
) )
//nolint:gosec
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
var exampleURL = func() *url.URL {
u, err := url.Parse("https://example.com/foobar")
if err != nil {
panic(err)
}
return u
}()
func TestStoreRequest(t *testing.T) { func TestStoreRequest(t *testing.T) {
t.Parallel() t.Parallel()
@ -41,10 +30,16 @@ func TestStoreRequest(t *testing.T) {
svc := sender.NewService(sender.Config{}) svc := sender.NewService(sender.Config{})
_, err := svc.CreateOrUpdateRequest(context.Background(), sender.Request{ _, err := svc.CreateOrUpdateRequest(context.Background(), &connect.Request[sender.CreateOrUpdateRequestRequest]{
URL: exampleURL, Msg: &sender.CreateOrUpdateRequestRequest{
Method: http.MethodPost, Request: &sender.Request{
HttpRequest: &httppb.Request{
Url: "https://example.com/foobar",
Method: httppb.Method_METHOD_POST,
Body: []byte("foobar"), Body: []byte("foobar"),
},
},
},
}) })
if !errors.Is(err, sender.ErrProjectIDMustBeSet) { if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err) t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
@ -54,74 +49,86 @@ func TestStoreRequest(t *testing.T) {
t.Run("with active project", func(t *testing.T) { t.Run("with active project", func(t *testing.T) {
t.Parallel() t.Parallel()
repoMock := &RepoMock{ path := t.TempDir() + "bolt.db"
StoreSenderRequestFunc: func(ctx context.Context, req sender.Request) error { boltDB, err := bbolt.Open(path, 0o600, nil)
return nil if err != nil {
}, t.Fatalf("failed to open bolt database: %v", err)
} }
defer boltDB.Close()
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
svc := sender.NewService(sender.Config{ svc := sender.NewService(sender.Config{
Repository: repoMock, Repository: db,
}) })
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) projectID := "foobar-project-id"
svc.SetActiveProjectID(projectID) err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
exp := sender.Request{ Name: "foobar",
ProjectID: projectID, })
URL: exampleURL, if err != nil {
Method: http.MethodPost, t.Fatalf("unexpected error upserting project: %v", err)
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"bar"},
},
Body: []byte("foobar"),
} }
got, err := svc.CreateOrUpdateRequest(context.Background(), sender.Request{ svc.SetActiveProjectID(projectID)
URL: exampleURL,
Method: http.MethodPost, exp := &sender.Request{
Proto: "HTTP/1.1", ProjectId: projectID,
Header: http.Header{ HttpRequest: &httppb.Request{
"X-Foo": []string{"bar"}, Method: httppb.Method_METHOD_POST,
Protocol: httppb.Protocol_PROTOCOL_HTTP20,
Url: "https://example.com/foobar",
Headers: []*httppb.Header{
{Key: "X-Foo", Value: "bar"},
}, },
Body: []byte("foobar"), Body: []byte("foobar"),
},
}
createRes, err := svc.CreateOrUpdateRequest(context.Background(), &connect.Request[sender.CreateOrUpdateRequestRequest]{
Msg: &sender.CreateOrUpdateRequestRequest{
Request: exp,
},
}) })
if err != nil { if err != nil {
t.Fatalf("unexpected error storing request: %v", err) t.Fatalf("unexpected error storing request: %v", err)
} }
if got.ID.Compare(ulid.ULID{}) == 0 { if createRes.Msg.Request.Id == "" {
t.Fatal("expected request ID to be non-empty value") t.Fatal("expected request ID to be non-empty value")
} }
if len(repoMock.StoreSenderRequestCalls()) != 1 { testutil.ProtoDiff(t, "request not equal", exp, createRes.Msg.Request, "id")
t.Fatal("expected `svc.repo.StoreSenderRequest()` to have been called 1 time")
got, err := db.FindSenderRequestByID(context.Background(), projectID, createRes.Msg.Request.Id)
if err != nil {
t.Fatalf("failed to find request by ID: %v", err)
} }
if diff := cmp.Diff(got, repoMock.StoreSenderRequestCalls()[0].Req); diff != "" { testutil.ProtoDiff(t, "request not equal", exp, got, "id")
t.Fatalf("repo call arg not equal (-exp, +got):\n%v", diff)
}
// Reset ID to make comparison with expected easier.
got.ID = ulid.ULID{}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
}
}) })
} }
func TestCloneFromRequestLog(t *testing.T) { func TestCloneFromRequestLog(t *testing.T) {
t.Parallel() t.Parallel()
reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) reqLogID := "foobar-req-log-id"
t.Run("without active project", func(t *testing.T) { t.Run("without active project", func(t *testing.T) {
t.Parallel() t.Parallel()
svc := sender.NewService(sender.Config{}) svc := sender.NewService(sender.Config{})
_, err := svc.CloneFromRequestLog(context.Background(), reqLogID) _, err := svc.CloneFromRequestLog(context.Background(), &connect.Request[sender.CloneFromRequestLogRequest]{
Msg: &sender.CloneFromRequestLogRequest{
RequestLogId: reqLogID,
},
})
if !errors.Is(err, sender.ErrProjectIDMustBeSet) { if !errors.Is(err, sender.ErrProjectIDMustBeSet) {
t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err) t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err)
} }
@ -130,85 +137,103 @@ func TestCloneFromRequestLog(t *testing.T) {
t.Run("with active project", func(t *testing.T) { t.Run("with active project", func(t *testing.T) {
t.Parallel() t.Parallel()
reqLog := reqlog.RequestLog{ path := t.TempDir() + "bolt.db"
ID: reqLogID, boltDB, err := bbolt.Open(path, 0o600, nil)
ProjectID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), if err != nil {
URL: exampleURL, t.Fatalf("failed to open bolt database: %v", err)
Method: http.MethodPost, }
Proto: "HTTP/1.1", defer boltDB.Close()
Header: http.Header{
"X-Foo": []string{"bar"}, db, err := bolt.DatabaseFromBoltDB(boltDB)
}, if err != nil {
Body: []byte("foobar"), t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
projectID := "foobar-project-id"
err = db.UpsertProject(context.Background(), &proj.Project{
Id: projectID,
Name: "foobar",
})
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
} }
reqLogMock := &ReqLogServiceMock{ reqLog := &reqlog.HttpRequestLog{
FindRequestLogByIDFunc: func(ctx context.Context, id ulid.ULID) (reqlog.RequestLog, error) { Id: reqLogID,
return reqLog, nil ProjectId: projectID,
Request: &httppb.Request{
Url: "https://example.com/foobar",
Method: httppb.Method_METHOD_POST,
Body: []byte("foobar"),
Headers: []*httppb.Header{
{Key: "X-Foo", Value: "bar"},
},
},
Response: &httppb.Response{
Protocol: httppb.Protocol_PROTOCOL_HTTP20,
StatusCode: 200,
Status: "200 OK",
Body: []byte("foobar"),
}, },
} }
repoMock := &RepoMock{
StoreSenderRequestFunc: func(ctx context.Context, req sender.Request) error { if err := db.StoreRequestLog(context.Background(), reqLog); err != nil {
return nil t.Fatalf("failed to store request log: %v", err)
},
} }
svc := sender.NewService(sender.Config{ svc := sender.NewService(sender.Config{
ReqLogService: reqLogMock, ReqLogService: reqlog.NewService(reqlog.Config{
Repository: repoMock, ActiveProjectID: projectID,
Repository: db,
}),
Repository: db,
}) })
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
svc.SetActiveProjectID(projectID) svc.SetActiveProjectID(projectID)
exp := sender.Request{ exp := &sender.Request{
SourceRequestLogID: reqLogID, SourceRequestLogId: reqLogID,
ProjectID: projectID, ProjectId: projectID,
URL: exampleURL, HttpRequest: &httppb.Request{
Method: http.MethodPost, Url: "https://example.com/foobar",
Proto: sender.HTTPProto20, Method: httppb.Method_METHOD_POST,
Header: http.Header{
"X-Foo": []string{"bar"},
},
Body: []byte("foobar"), Body: []byte("foobar"),
Headers: []*httppb.Header{
{Key: "X-Foo", Value: "bar"},
},
},
} }
got, err := svc.CloneFromRequestLog(context.Background(), reqLogID) got, err := svc.CloneFromRequestLog(context.Background(), &connect.Request[sender.CloneFromRequestLogRequest]{
Msg: &sender.CloneFromRequestLogRequest{
RequestLogId: reqLogID,
},
})
if err != nil { if err != nil {
t.Fatalf("unexpected error cloning from request log: %v", err) t.Fatalf("unexpected error cloning from request log: %v", err)
} }
if len(reqLogMock.FindRequestLogByIDCalls()) != 1 { testutil.ProtoDiff(t, "request not equal", exp, got.Msg.Request, "id")
t.Fatal("expected `svc.reqLogSvc.FindRequestLogByID()` to have been called 1 time")
}
if got := reqLogMock.FindRequestLogByIDCalls()[0].ID; reqLogID.Compare(got) != 0 {
t.Fatalf("reqlog service call arg `id` not equal (expected: %q, got: %q)", reqLogID, got)
}
if got.ID.Compare(ulid.ULID{}) == 0 {
t.Fatal("expected request ID to be non-empty value")
}
if len(repoMock.StoreSenderRequestCalls()) != 1 {
t.Fatal("expected `svc.repo.StoreSenderRequest()` to have been called 1 time")
}
if diff := cmp.Diff(got, repoMock.StoreSenderRequestCalls()[0].Req); diff != "" {
t.Fatalf("repo call arg not equal (-exp, +got):\n%v", diff)
}
// Reset ID to make comparison with expected easier.
got.ID = ulid.ULID{}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("request not equal (-exp, +got):\n%v", diff)
}
}) })
} }
func TestSendRequest(t *testing.T) { func TestSendRequest(t *testing.T) {
t.Parallel() t.Parallel()
path := t.TempDir() + "bolt.db"
boltDB, err := bbolt.Open(path, 0o600, nil)
if err != nil {
t.Fatalf("failed to open bolt database: %v", err)
}
defer boltDB.Close()
db, err := bolt.DatabaseFromBoltDB(boltDB)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer db.Close()
date := time.Now().Format(http.TimeFormat) date := time.Now().Format(http.TimeFormat)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -218,72 +243,73 @@ func TestSendRequest(t *testing.T) {
})) }))
defer ts.Close() defer ts.Close()
tsURL, _ := url.Parse(ts.URL) projectID := "foobar-project-id"
err = db.UpsertProject(context.Background(), &proj.Project{
reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) Id: projectID,
req := sender.Request{ Name: "foobar",
ID: reqID,
ProjectID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
URL: tsURL,
Method: http.MethodPost,
Proto: "HTTP/1.1",
Header: http.Header{
"X-Foo": []string{"bar"},
},
Body: []byte("foobar"),
}
repoMock := &RepoMock{
FindSenderRequestByIDFunc: func(ctx context.Context, id ulid.ULID) (sender.Request, error) {
return req, nil
},
StoreResponseLogFunc: func(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error {
return nil
},
}
svc := sender.NewService(sender.Config{
Repository: repoMock,
}) })
if err != nil {
t.Fatalf("unexpected error upserting project: %v", err)
}
exp := reqlog.ResponseLog{ reqID := "foobar-req-id"
Proto: "HTTP/1.1", req := &sender.Request{
StatusCode: http.StatusOK, Id: reqID,
ProjectId: projectID,
HttpRequest: &httppb.Request{
Url: ts.URL,
Method: httppb.Method_METHOD_POST,
Body: []byte("foobar"),
Headers: []*httppb.Header{
{Key: "X-Foo", Value: "bar"},
},
},
}
if err := db.StoreSenderRequest(context.Background(), req); err != nil {
t.Fatalf("failed to store request: %v", err)
}
svc := sender.NewService(sender.Config{
ReqLogService: reqlog.NewService(reqlog.Config{
Repository: db,
}),
Repository: db,
})
svc.SetActiveProjectID(projectID)
exp := &httppb.Response{
Protocol: httppb.Protocol_PROTOCOL_HTTP11,
StatusCode: 200,
Status: "200 OK", Status: "200 OK",
Header: http.Header{ Headers: []*httppb.Header{
"Content-Length": []string{"3"}, {Key: "Date", Value: date},
"Content-Type": []string{"text/plain; charset=utf-8"}, {Key: "Foobar", Value: "baz"},
"Date": []string{date}, {Key: "Content-Length", Value: "3"},
"Foobar": []string{"baz"}, {Key: "Content-Type", Value: "text/plain; charset=utf-8"},
}, },
Body: []byte("baz"), Body: []byte("baz"),
} }
got, err := svc.SendRequest(context.Background(), reqID) got, err := svc.SendRequest(context.Background(), &connect.Request[sender.SendRequestRequest]{
Msg: &sender.SendRequestRequest{
RequestId: reqID,
},
})
if err != nil { if err != nil {
t.Fatalf("unexpected error sending request: %v", err) t.Fatalf("unexpected error sending request: %v", err)
} }
if len(repoMock.FindSenderRequestByIDCalls()) != 1 { opts := []cmp.Option{
t.Fatal("expected `svc.repo.FindSenderRequestByID()` to have been called 1 time") protocmp.Transform(),
protocmp.SortRepeated(func(a, b *httppb.Header) bool {
if a.Key != b.Key {
return a.Key < b.Key
} }
return a.Value < b.Value
if diff := cmp.Diff(reqID, repoMock.FindSenderRequestByIDCalls()[0].ID); diff != "" { }),
t.Fatalf("call arg `id` for `svc.repo.FindSenderRequestByID()` not equal (-exp, +got):\n%v", diff)
} }
if diff := cmp.Diff(exp, got.Msg.Request.HttpResponse, opts...); diff != "" {
if len(repoMock.StoreResponseLogCalls()) != 1 { t.Fatalf("response not equal (-exp, +got):\n%v", diff)
t.Fatal("expected `svc.repo.StoreResponseLog()` to have been called 1 time")
}
if diff := cmp.Diff(reqID, repoMock.StoreResponseLogCalls()[0].ReqLogID); diff != "" {
t.Fatalf("call arg `reqLogID` for `svc.repo.StoreResponseLog()` not equal (-exp, +got):\n%v", diff)
}
if diff := cmp.Diff(exp, repoMock.StoreResponseLogCalls()[0].ResLog); diff != "" {
t.Fatalf("call arg `resLog` for `svc.repo.StoreResponseLog()` not equal (-exp, +got):\n%v", diff)
}
if diff := cmp.Diff(repoMock.StoreResponseLogCalls()[0].ResLog, *got.Response); diff != "" {
t.Fatalf("returned response log value and persisted value not equal (-exp, +got):\n%v", diff)
} }
} }

View File

@ -45,7 +45,3 @@ func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return http.DefaultTransport.RoundTrip(req) return http.DefaultTransport.RoundTrip(req)
} }
func isValidProto(proto string) bool {
return proto == HTTPProto10 || proto == HTTPProto11 || proto == HTTPProto20
}

54
pkg/testutil/testutil.go Normal file
View File

@ -0,0 +1,54 @@
package testutil
import (
"testing"
"github.com/dstotijn/hetty/pkg/log"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/testing/protocmp"
)
func ProtoDiff[M proto.Message](t *testing.T, msg string, exp, got M, ignoreFields ...protoreflect.Name) {
t.Helper()
opts := []cmp.Option{
protocmp.Transform(),
protocmp.IgnoreFields(exp, ignoreFields...),
}
if diff := cmp.Diff(exp, got, opts...); diff != "" {
t.Fatalf("%v (-exp, +got):\n%v", msg, diff)
}
}
func ProtoSlicesDiff[M proto.Message](t *testing.T, msg string, exp, got []M, ignoreFields ...protoreflect.Name) {
t.Helper()
opts := []cmp.Option{
protocmp.Transform(),
}
if len(exp) > 0 {
opts = append(opts, protocmp.IgnoreFields(exp[0], ignoreFields...))
}
if diff := cmp.Diff(exp, got, opts...); diff != "" {
t.Fatalf("%v (-exp, +got):\n%v", msg, diff)
}
}
type testLogger struct {
log.NopLogger
t *testing.T
}
func (l *testLogger) Errorw(msg string, v ...interface{}) {
l.t.Helper()
l.t.Fatalf(msg+": %v", v...)
}
func NewLogger(t *testing.T) log.Logger {
t.Helper()
return &testLogger{t: t}
}

47
proto/http/http.proto Normal file
View File

@ -0,0 +1,47 @@
syntax = "proto3";
package hetty.http.v1;
option go_package = "github.com/dstotijn/hetty/pkg/http";
enum Method {
METHOD_UNSPECIFIED = 0;
METHOD_GET = 1;
METHOD_HEAD = 2;
METHOD_POST = 3;
METHOD_PUT = 4;
METHOD_DELETE = 5;
METHOD_CONNECT = 6;
METHOD_OPTIONS = 7;
METHOD_TRACE = 8;
METHOD_PATCH = 9;
}
enum Protocol {
PROTOCOL_UNSPECIFIED = 0;
PROTOCOL_HTTP10 = 1;
PROTOCOL_HTTP11 = 2;
PROTOCOL_HTTP20 = 3;
}
message Request {
Method method = 1;
Protocol protocol = 2;
string url = 3;
repeated Header headers = 4;
bytes body = 5;
Response response = 6;
}
message Response {
Protocol protocol = 1;
string status = 2;
int32 status_code = 3;
repeated Header headers = 5;
bytes body = 6;
}
message Header {
string key = 1;
string value = 2;
}

104
proto/proj/proj.proto Normal file
View File

@ -0,0 +1,104 @@
syntax = "proto3";
package hetty.proj.v1;
import "reqlog/reqlog.proto";
import "scope/scope.proto";
option go_package = "github.com/dstotijn/hetty/pkg/proj";
message Project {
string id = 1;
string name = 2;
bool is_active = 3;
// Request log settings
bool req_log_bypass_out_of_scope = 4;
// Request logs filter
hetty.reqlog.v1.RequestLogsFilter req_log_filter = 5;
// Intercept settings
bool intercept_requests = 6;
bool intercept_responses = 7;
string intercept_request_filter_expr = 8;
string intercept_response_filter_expr = 9;
// Sender settings
bool sender_only_find_in_scope = 10;
string sender_search_expr = 11;
// Scope settings
repeated hetty.scope.v1.ScopeRule scope_rules = 12;
}
message CreateProjectRequest {
string name = 1;
}
message CreateProjectResponse {
Project project = 1;
}
message OpenProjectRequest {
string project_id = 1;
}
message OpenProjectResponse {
Project project = 1;
}
message CloseProjectRequest {}
message CloseProjectResponse {}
message DeleteProjectRequest {
string project_id = 1;
}
message DeleteProjectResponse {}
message GetActiveProjectRequest {}
message GetActiveProjectResponse {
Project project = 1;
}
message ListProjectsRequest {}
message ListProjectsResponse {
repeated Project projects = 1;
}
message UpdateInterceptSettingsRequest {
bool requests_enabled = 1;
bool responses_enabled = 2;
string request_filter_expr = 3;
string response_filter_expr = 4;
}
message UpdateInterceptSettingsResponse {}
message SetScopeRulesRequest {
repeated hetty.scope.v1.ScopeRule rules = 1;
}
message SetScopeRulesResponse {}
message SetRequestLogsFilterRequest {
hetty.reqlog.v1.RequestLogsFilter filter = 1;
}
message SetRequestLogsFilterResponse {}
service ProjectService {
rpc CreateProject(CreateProjectRequest) returns (CreateProjectResponse) {}
rpc OpenProject(OpenProjectRequest) returns (OpenProjectResponse) {}
rpc CloseProject(CloseProjectRequest) returns (CloseProjectResponse) {}
rpc DeleteProject(DeleteProjectRequest) returns (DeleteProjectResponse) {}
rpc GetActiveProject(GetActiveProjectRequest) returns (GetActiveProjectResponse) {}
rpc ListProjects(ListProjectsRequest) returns (ListProjectsResponse) {}
rpc UpdateInterceptSettings(UpdateInterceptSettingsRequest) returns (UpdateInterceptSettingsResponse) {}
rpc SetScopeRules(SetScopeRulesRequest) returns (SetScopeRulesResponse) {}
rpc SetRequestLogsFilter(SetRequestLogsFilterRequest) returns (SetRequestLogsFilterResponse) {}
}

44
proto/reqlog/reqlog.proto Normal file
View File

@ -0,0 +1,44 @@
syntax = "proto3";
package hetty.reqlog.v1;
import "http/http.proto";
option go_package = "github.com/dstotijn/hetty/pkg/reqlog";
message HttpRequestLog {
string id = 1;
string project_id = 2;
string remote_ip = 3;
hetty.http.v1.Request request = 4;
hetty.http.v1.Response response = 5;
}
message GetHttpRequestLogRequest {
string id = 1;
}
message GetHttpRequestLogResponse {
HttpRequestLog http_request_log = 1;
}
message ListHttpRequestLogsRequest {}
message ListHttpRequestLogsResponse {
repeated HttpRequestLog http_request_logs = 1;
}
message RequestLogsFilter {
bool only_in_scope = 1;
string search_expr = 2;
}
message ClearHttpRequestLogsRequest {}
message ClearHttpRequestLogsResponse {}
service HttpRequestLogService {
rpc GetHttpRequestLog(GetHttpRequestLogRequest) returns (GetHttpRequestLogResponse) {}
rpc ListHttpRequestLogs(ListHttpRequestLogsRequest) returns (ListHttpRequestLogsResponse) {}
rpc ClearHttpRequestLogs(ClearHttpRequestLogsRequest) returns (ClearHttpRequestLogsResponse) {}
}

12
proto/scope/scope.proto Normal file
View File

@ -0,0 +1,12 @@
syntax = "proto3";
package hetty.scope.v1;
option go_package = "github.com/dstotijn/hetty/pkg/scope";
message ScopeRule {
string url_regexp = 1;
string header_key_regexp = 2;
string header_value_regexp = 3;
string body_regexp = 4;
}

85
proto/sender/sender.proto Normal file
View File

@ -0,0 +1,85 @@
syntax = "proto3";
package sender;
import "http/http.proto";
option go_package = "github.com/dstotijn/hetty/proto/sender";
message Request {
string id = 1;
string project_id = 2;
string source_request_log_id = 3;
hetty.http.v1.Request http_request = 4;
hetty.http.v1.Response http_response = 10;
}
message RequestsFilter {
bool only_in_scope = 1;
string search_expr = 2;
}
message GetRequestByIDRequest {
string request_id = 1;
}
message GetRequestByIDResponse {
Request request = 1;
}
message ListRequestsRequest {}
message ListRequestsResponse {
repeated Request requests = 1;
}
message CloneFromRequestLogRequest {
string request_log_id = 1;
}
message CloneFromRequestLogResponse {
Request request = 1;
}
message SendRequestRequest {
string request_id = 1;
}
message SendRequestResponse {
Request request = 1;
}
message DeleteRequestsRequest {}
message DeleteRequestsResponse {}
message CreateOrUpdateRequestRequest {
Request request = 1;
}
message CreateOrUpdateRequestResponse {
Request request = 1;
}
message SetRequestsFilterRequest {
RequestsFilter filter = 1;
}
message SetRequestsFilterResponse {}
message GetRequestsFilterRequest {}
message GetRequestsFilterResponse {
RequestsFilter filter = 1;
}
service SenderService {
rpc GetRequestByID(GetRequestByIDRequest) returns (GetRequestByIDResponse) {}
rpc ListRequests(ListRequestsRequest) returns (ListRequestsResponse) {}
rpc SetRequestsFilter(SetRequestsFilterRequest) returns (SetRequestsFilterResponse) {}
rpc GetRequestsFilter(GetRequestsFilterRequest) returns (GetRequestsFilterResponse) {}
rpc CreateOrUpdateRequest(CreateOrUpdateRequestRequest) returns (CreateOrUpdateRequestResponse) {}
rpc CloneFromRequestLog(CloneFromRequestLogRequest) returns (CloneFromRequestLogResponse) {}
rpc SendRequest(SendRequestRequest) returns (SendRequestResponse) {}
rpc DeleteRequests(DeleteRequestsRequest) returns (DeleteRequestsResponse) {}
}

View File

@ -1,9 +0,0 @@
//go:build tools
// +build tools
package tools
import (
_ "github.com/99designs/gqlgen"
_ "github.com/matryer/moq"
)