mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
29550ff43b | |||
7afc23b3ff | |||
6aa93b782e | |||
ed9a539ce3 | |||
857aa0c49e | |||
af26987601 | |||
ad26478043 | |||
ca0c085021 | |||
d438f93ee0 | |||
fa3f24eb70 | |||
f15438e10b | |||
bef52d956e | |||
8269af9478 | |||
c5f76e1f9a | |||
2ddf2a77e8 |
@ -12,6 +12,7 @@ linters:
|
|||||||
- test
|
- test
|
||||||
- unused
|
- unused
|
||||||
disable:
|
disable:
|
||||||
|
- dupl
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- exhaustivestruct
|
- exhaustivestruct
|
||||||
- gochecknoglobals
|
- gochecknoglobals
|
||||||
@ -21,9 +22,11 @@ linters:
|
|||||||
- gomnd
|
- gomnd
|
||||||
- interfacer
|
- interfacer
|
||||||
- maligned
|
- maligned
|
||||||
|
- nilnil
|
||||||
- nlreturn
|
- nlreturn
|
||||||
- scopelint
|
- scopelint
|
||||||
- testpackage
|
- testpackage
|
||||||
|
- varnamelen
|
||||||
- wrapcheck
|
- wrapcheck
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
@ -31,6 +34,8 @@ linters-settings:
|
|||||||
local-prefixes: github.com/dstotijn/hetty
|
local-prefixes: github.com/dstotijn/hetty
|
||||||
godot:
|
godot:
|
||||||
capital: true
|
capital: true
|
||||||
|
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"
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
|
@ -28,6 +28,20 @@ archives:
|
|||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
format: zip
|
||||||
|
|
||||||
|
brews:
|
||||||
|
- tap:
|
||||||
|
owner: hettysoft
|
||||||
|
name: homebrew-tap
|
||||||
|
folder: Formula
|
||||||
|
homepage: https://hetty.xyz
|
||||||
|
description: An HTTP toolkit for security research.
|
||||||
|
license: MIT
|
||||||
|
commit_author:
|
||||||
|
name: David Stotijn
|
||||||
|
email: dstotijn@gmail.com
|
||||||
|
test: |
|
||||||
|
system "#{bin}/hetty -v"
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: "checksums.txt"
|
name_template: "checksums.txt"
|
||||||
|
|
||||||
|
263
README.md
263
README.md
@ -1,243 +1,148 @@
|
|||||||
<h1>
|
<h1>
|
||||||
<a href="https://github.com/dstotijn/hetty">
|
<img src="https://hetty.xyz/img/hetty_light.svg#gh-light-mode-only" width="240"/>
|
||||||
<img src="https://hetty.xyz/assets/logo.png" width="293">
|
<img src="https://hetty.xyz/img/hetty_dark.svg#gh-dark-mode-only" width="240"/>
|
||||||
</a>
|
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
[](https://github.com/dstotijn/hetty/releases/latest)
|
[](https://github.com/dstotijn/hetty/releases/latest)
|
||||||
[](https://github.com/dstotijn/hetty/actions/workflows/build-test.yml)
|
[](https://github.com/dstotijn/hetty/actions/workflows/build-test.yml)
|
||||||

|

|
||||||
[](https://github.com/dstotijn/hetty/blob/master/LICENSE)
|
[](https://github.com/dstotijn/hetty/blob/master/LICENSE)
|
||||||
[](https://hetty.xyz/)
|
[](https://hetty.xyz/)
|
||||||
|
|
||||||
**Hetty** is an HTTP toolkit for security research. It aims to become an open
|
**Hetty** is an HTTP toolkit for security research. It aims to become an open
|
||||||
source alternative to commercial software like Burp Suite Pro, with powerful
|
source alternative to commercial software like Burp Suite Pro, with powerful
|
||||||
features tailored to the needs of the infosec and bug bounty community.
|
features tailored to the needs of the infosec and bug bounty community.
|
||||||
|
|
||||||
<img src="https://hetty.xyz/assets/hetty_v0.2.0_header.png">
|
<img src="https://hetty.xyz/img/hero.png" width="907" alt="Hetty proxy logs (screenshot)" />
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Man-in-the-middle (MITM) HTTP/1.1 proxy with logs
|
- Machine-in-the-middle (MITM) HTTP proxy, with logs and advanced search
|
||||||
- Project based database storage (BadgerDB)
|
- HTTP client for manually creating/editing requests, and replay proxied requests
|
||||||
- Scope support
|
- Scope support, to help keep work organized
|
||||||
- Headless management API using GraphQL
|
- Easy-to-use web based admin interface
|
||||||
- Embedded web interface (Next.js)
|
- Project based database storage, to help keep work organized
|
||||||
|
|
||||||
ℹ️ Hetty is in early development. Additional features are planned
|
👷♂️ Hetty is under active development. Check the <a
|
||||||
for a `v1.0` release. Please see the <a href="https://github.com/dstotijn/hetty/projects/1">backlog</a>
|
href="https://github.com/dstotijn/hetty/projects/1">backlog</a> for the current
|
||||||
for details.
|
status.
|
||||||
|
|
||||||
## Documentation
|
📣 Are you pen testing professionaly in a team? I would love to hear your
|
||||||
|
thoughts on tooling via [this 5 minute
|
||||||
|
survey](https://forms.gle/36jtgNc3TJ2imi5A8). Thank you!
|
||||||
|
|
||||||
📖 [Read the docs.](https://hetty.xyz/)
|
## Getting started
|
||||||
|
|
||||||
## Installation
|
💡 The [Getting started](https://hetty.xyz/docs/getting-started) doc has more
|
||||||
|
detailed install and usage instructions.
|
||||||
|
|
||||||
Hetty compiles to a self-contained binary, with an embedded BadgerDB database
|
### Installation
|
||||||
and web based admin interface.
|
|
||||||
|
|
||||||
### Install pre-built release (recommended)
|
The quickest way to install and update Hetty is via a package manager:
|
||||||
|
|
||||||
👉 Downloads for Linux, macOS and Windows are available on the [releases page](https://github.com/dstotijn/hetty/releases).
|
#### macOS
|
||||||
|
|
||||||
### Build from source
|
```sh
|
||||||
|
brew install hettysoft/tap/hetty
|
||||||
#### Prerequisites
|
|
||||||
|
|
||||||
- [Go 1.16](https://golang.org/)
|
|
||||||
- [Yarn](https://yarnpkg.com/)
|
|
||||||
|
|
||||||
When building from source, the static resources for the admin interface
|
|
||||||
(Next.js) need to be generated via [Yarn](https://yarnpkg.com/). The generated
|
|
||||||
files will be embedded (using the [embed](https://golang.org/pkg/embed/)
|
|
||||||
package) when you use the `build` Makefile target.
|
|
||||||
|
|
||||||
Clone the repository and use the `build` make target to create a binary:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ git clone git@github.com:dstotijn/hetty.git
|
|
||||||
$ cd hetty
|
|
||||||
$ make build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker
|
#### Linux
|
||||||
|
|
||||||
A Docker image is available on Docker Hub: [`dstotijn/hetty`](https://hub.docker.com/r/dstotijn/hetty).
|
```sh
|
||||||
For persistent storage of CA certificates and projects database, mount a volume:
|
sudo snap install hetty
|
||||||
|
|
||||||
```
|
|
||||||
$ mkdir -p $HOME/.hetty
|
|
||||||
$ docker run -v $HOME/.hetty:/root/.hetty -p 8080:8080 dstotijn/hetty
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
#### Windows
|
||||||
|
|
||||||
When Hetty is run, by default it listens on `:8080` and is accessible via
|
```sh
|
||||||
http://localhost:8080. Depending on incoming HTTP requests, it either acts as a
|
scoop bucket add hettysoft https://github.com/hettysoft/scoop.git
|
||||||
MITM proxy, or it serves the API and web interface.
|
scoop install hettysoft/hetty
|
||||||
|
|
||||||
By default, the projects database files and CA certificates are stored in a `.hetty`
|
|
||||||
directory under the user's home directory (`$HOME` on Linux/macOS, `%USERPROFILE%`
|
|
||||||
on Windows).
|
|
||||||
|
|
||||||
To start, ensure `hetty` (downloaded from a release, or manually built) is in your
|
|
||||||
`$PATH` and run:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ hetty
|
|
||||||
```
|
```
|
||||||
|
|
||||||
An overview of configuration flags:
|
#### Other
|
||||||
|
|
||||||
```
|
Alternatively, you can [download the latest release from
|
||||||
$ hetty -h
|
GitHub](https://github.com/dstotijn/hetty/releases/latest) for your OS and
|
||||||
Usage of ./hetty:
|
architecture, and move the binary to a directory in your `$PATH`. If your OS is
|
||||||
-addr string
|
not available for one of the package managers or not listed in the GitHub
|
||||||
TCP address to listen on, in the form "host:port" (default ":8080")
|
releases, you can compile from source _(link coming soon)_ or use a Docker image
|
||||||
-adminPath string
|
_(link coming soon)_.
|
||||||
File path to admin build
|
|
||||||
-cert string
|
|
||||||
CA certificate filepath. Creates a new CA certificate if file doesn't exist (default "~/.hetty/hetty_cert.pem")
|
|
||||||
-key string
|
|
||||||
CA private key filepath. Creates a new CA private key if file doesn't exist (default "~/.hetty/hetty_key.pem")
|
|
||||||
-db string
|
|
||||||
Database directory path (default "~/.hetty/db")
|
|
||||||
```
|
|
||||||
|
|
||||||
You should see:
|
### Usage
|
||||||
|
|
||||||
```
|
Once installed, start Hetty via:
|
||||||
2022/01/26 10:34:24 [INFO] Hetty (v0.3.2) is running on :8080 ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, visit [http://localhost:8080](http://localhost:8080) to get started.
|
|
||||||
|
|
||||||
ℹ️ Detailed documentation is under development and will be available soon.
|
|
||||||
|
|
||||||
## Certificate Setup and Installation
|
|
||||||
|
|
||||||
In order for Hetty to proxy requests going to HTTPS endpoints, a root CA certificate for
|
|
||||||
Hetty will need to be set up. Furthermore, the CA certificate may need to be
|
|
||||||
installed to the host for them to be trusted by your browser. The following steps
|
|
||||||
will cover how you can generate your certificate, provide them to hetty, and how
|
|
||||||
you can install them in your local CA store.
|
|
||||||
|
|
||||||
⚠️ _This process was done on a Linux machine but should_
|
|
||||||
_provide guidance on Windows and macOS as well._
|
|
||||||
|
|
||||||
### Generating CA certificates
|
|
||||||
|
|
||||||
You can generate a CA keypair two different ways. The first is bundled directly
|
|
||||||
with Hetty, and simplifies the process immensely. The alternative is using OpenSSL
|
|
||||||
to generate them, which provides more control over expiration time and cryptography
|
|
||||||
used, but requires you install the OpenSSL tooling. The first is suggested for any
|
|
||||||
beginners trying to get started.
|
|
||||||
|
|
||||||
#### Generating CA certificates with hetty
|
|
||||||
|
|
||||||
Hetty will generate the default key and certificate on its own if none are supplied
|
|
||||||
or found in `~/.hetty/` when first running the CLI. To generate a default key and
|
|
||||||
certificate with hetty, simply run the command with no arguments
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
hetty
|
hetty
|
||||||
```
|
```
|
||||||
|
|
||||||
You should now have a key and certificate located at `~/.hetty/hetty_key.pem` and
|
💡 Read the [Getting started](https://hetty.xyz/docs/getting-started) doc for
|
||||||
`~/.hetty/hetty_cert.pem` respectively.
|
more details.
|
||||||
|
|
||||||
#### Generating CA certificates with OpenSSL
|
To list all available options, run: `hetty --help`:
|
||||||
|
|
||||||
You can start off by generating a new key and CA certificate which will both expire
|
|
||||||
after a month.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
mkdir ~/.hetty
|
|
||||||
openssl req -newkey rsa:2048 -new -nodes -x509 -days 31 -keyout ~/.hetty/hetty_key.pem -out ~/.hetty/hetty_cert.pem
|
|
||||||
```
|
|
||||||
|
|
||||||
The default location which `hetty` will check for the key and CA certificate is under
|
|
||||||
`~/.hetty/`, at `hetty_key.pem` and `hetty_cert.pem` respectively. You can move them
|
|
||||||
here and `hetty` will detect them automatically. Otherwise, you can specify the
|
|
||||||
location of these as arguments to `hetty`.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
hetty -key key.pem -cert cert.pem
|
$ hetty --help
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
hetty [flags] [subcommand] [flags]
|
||||||
|
|
||||||
|
Runs an HTTP server with (MITM) proxy, GraphQL service, and a web based admin interface.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--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")
|
||||||
|
--db Database directory path. (Default: "~/.hetty/db")
|
||||||
|
--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)
|
||||||
|
--verbose Enable verbose logging.
|
||||||
|
--json Encode logs as JSON, instead of pretty/human readable output.
|
||||||
|
--version, -v Output version.
|
||||||
|
--help, -h Output this usage text.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
- cert Certificate management
|
||||||
|
|
||||||
|
Run `hetty <subcommand> --help` for subcommand specific usage instructions.
|
||||||
|
|
||||||
|
Visit https://hetty.xyz to learn more about Hetty.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Trusting the CA certificate
|
## Documentation
|
||||||
|
|
||||||
In order for your browser to allow traffic to the local Hetty proxy, you may need
|
📖 [Read the docs](https://hetty.xyz/docs)
|
||||||
to install these certificates to your local CA store.
|
|
||||||
|
|
||||||
On Ubuntu, you can update your local CA store with the certificate by running the
|
|
||||||
following commands:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
sudo cp ~/.hetty/hetty_cert.pem /usr/local/share/ca-certificates/hetty.crt
|
|
||||||
sudo update-ca-certificates
|
|
||||||
```
|
|
||||||
|
|
||||||
On Windows, you would add your certificate by using the Certificate Manager. You
|
|
||||||
can launch that by running the command:
|
|
||||||
|
|
||||||
```batch
|
|
||||||
certmgr.msc
|
|
||||||
```
|
|
||||||
|
|
||||||
On macOS, you can add your certificate by using the Keychain Access program. This
|
|
||||||
can be found under `Application/Utilities/Keychain Access.app`. After opening this,
|
|
||||||
drag the certificate into the app. Next, open the certificate in the app, enter the
|
|
||||||
_Trust_ section, and under _When using this certificate_ select _Always Trust_.
|
|
||||||
|
|
||||||
_Note: Various Linux distributions may require other steps or commands for updating_
|
|
||||||
_their certificate authority. See the documentation relevant to your distribution for_
|
|
||||||
_more information on how to update the system to trust your self-signed certificate._
|
|
||||||
|
|
||||||
## Vision and roadmap
|
|
||||||
|
|
||||||
- Fast core/engine, built with Go, with a minimal memory footprint.
|
|
||||||
- Easy to use admin interface, built with Next.js and Material UI.
|
|
||||||
- Headless management, via GraphQL API.
|
|
||||||
- Extensibility is top of mind. All modules are written as Go packages, to
|
|
||||||
be used by Hetty, but also as libraries by other software.
|
|
||||||
- Pluggable architecture for MITM proxy, projects, scope. It should be possible.
|
|
||||||
to build a plugin system in the (near) future.
|
|
||||||
- Based on feedback and real-world usage of pentesters and bug bounty hunters.
|
|
||||||
- Aim for a relatively small core feature set that the majority of security researchers need.
|
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
Use [issues](https://github.com/dstotijn/hetty/issues) for bug reports and
|
Use [issues](https://github.com/dstotijn/hetty/issues) for bug reports and
|
||||||
feature requests, and [discussions](https://github.com/dstotijn/hetty/discussions)
|
feature requests, and
|
||||||
for questions and troubleshooting.
|
[discussions](https://github.com/dstotijn/hetty/discussions) for questions and
|
||||||
|
troubleshooting.
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
💬 [Join the Hetty Discord server](https://discord.gg/3HVsj5pTFP).
|
💬 [Join the Hetty Discord server](https://discord.gg/3HVsj5pTFP)
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Want to contribute? Great! Please check the [Contribution Guidelines](CONTRIBUTING.md)
|
Want to contribute? Great! Please check the [Contribution
|
||||||
for details.
|
Guidelines](CONTRIBUTING.md) for details.
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
- Thanks to the [Hacker101 community on Discord](https://www.hacker101.com/discord)
|
- Thanks to the [Hacker101 community on Discord](https://www.hacker101.com/discord)
|
||||||
for all the encouragement and feedback.
|
for the encouragement and early feedback.
|
||||||
- The font used in the logo and admin interface is [JetBrains Mono](https://www.jetbrains.com/lp/mono/).
|
- The font used in the logo and admin interface is [JetBrains
|
||||||
|
Mono](https://www.jetbrains.com/lp/mono/).
|
||||||
|
|
||||||
## Sponsors
|
## Sponsors
|
||||||
|
|
||||||
<a href="https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=hetty">
|
<a href="https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=hetty">
|
||||||
<img src="https://hetty.xyz/assets/tines-sponsorship-badge.png" width="140" alt="Sponsored by Tines">
|
<img src="https://hetty.xyz/img/tines-sponsorship-badge.png" width="140" alt="Sponsored by Tines">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT License](LICENSE)
|
[MIT](LICENSE)
|
||||||
|
|
||||||
---
|
© 2022 Hetty Software
|
||||||
|
|
||||||
© 2021 David Stotijn — [Twitter](https://twitter.com/dstotijn), [Email](mailto:dstotijn@gmail.com)
|
|
||||||
|
@ -1,4 +1,16 @@
|
|||||||
import { Alert, Box, Link, MenuItem, Snackbar } from "@mui/material";
|
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Box,
|
||||||
|
IconButton,
|
||||||
|
Link,
|
||||||
|
MenuItem,
|
||||||
|
Snackbar,
|
||||||
|
styled,
|
||||||
|
TableCell,
|
||||||
|
TableCellProps,
|
||||||
|
Tooltip,
|
||||||
|
} from "@mui/material";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
@ -10,6 +22,11 @@ import SplitPane from "lib/components/SplitPane";
|
|||||||
import useContextMenu from "lib/components/useContextMenu";
|
import useContextMenu from "lib/components/useContextMenu";
|
||||||
import { useCreateSenderRequestFromHttpRequestLogMutation, useHttpRequestLogsQuery } from "lib/graphql/generated";
|
import { useCreateSenderRequestFromHttpRequestLogMutation, useHttpRequestLogsQuery } from "lib/graphql/generated";
|
||||||
|
|
||||||
|
const ActionsTableCell = styled(TableCell)<TableCellProps>(() => ({
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
export function RequestLogs(): JSX.Element {
|
export function RequestLogs(): JSX.Element {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const id = router.query.id as string | undefined;
|
const id = router.query.id as string | undefined;
|
||||||
@ -17,7 +34,13 @@ export function RequestLogs(): JSX.Element {
|
|||||||
pollInterval: 1000,
|
pollInterval: 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [createSenderReqFromLog] = useCreateSenderRequestFromHttpRequestLogMutation({});
|
const [createSenderReqFromLog] = useCreateSenderRequestFromHttpRequestLogMutation({
|
||||||
|
onCompleted({ createSenderRequestFromHttpRequestLog }) {
|
||||||
|
const { id } = createSenderRequestFromHttpRequestLog;
|
||||||
|
setNewSenderReqId(id);
|
||||||
|
setCopiedReqNotifOpen(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const [copyToSenderId, setCopyToSenderId] = useState("");
|
const [copyToSenderId, setCopyToSenderId] = useState("");
|
||||||
const [Menu, handleContextMenu, handleContextMenuClose] = useContextMenu();
|
const [Menu, handleContextMenu, handleContextMenuClose] = useContextMenu();
|
||||||
@ -27,11 +50,6 @@ export function RequestLogs(): JSX.Element {
|
|||||||
variables: {
|
variables: {
|
||||||
id: copyToSenderId,
|
id: copyToSenderId,
|
||||||
},
|
},
|
||||||
onCompleted({ createSenderRequestFromHttpRequestLog }) {
|
|
||||||
const { id } = createSenderRequestFromHttpRequestLog;
|
|
||||||
setNewSenderReqId(id);
|
|
||||||
setCopiedReqNotifOpen(true);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
handleContextMenuClose();
|
handleContextMenuClose();
|
||||||
};
|
};
|
||||||
@ -54,6 +72,26 @@ export function RequestLogs(): JSX.Element {
|
|||||||
handleContextMenu(e);
|
handleContextMenu(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const actionsCell = (id: string) => (
|
||||||
|
<ActionsTableCell>
|
||||||
|
<Tooltip title="Copy to Sender">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => {
|
||||||
|
setCopyToSenderId(id);
|
||||||
|
createSenderReqFromLog({
|
||||||
|
variables: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ContentCopyIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ActionsTableCell>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flexDirection="column" height="100%">
|
<Box display="flex" flexDirection="column" height="100%">
|
||||||
<Search />
|
<Search />
|
||||||
@ -77,6 +115,7 @@ export function RequestLogs(): JSX.Element {
|
|||||||
<RequestsTable
|
<RequestsTable
|
||||||
requests={data?.httpRequestLogs || []}
|
requests={data?.httpRequestLogs || []}
|
||||||
activeRowId={id}
|
activeRowId={id}
|
||||||
|
actionsCell={actionsCell}
|
||||||
onRowClick={handleRowClick}
|
onRowClick={handleRowClick}
|
||||||
onContextMenu={handleRowContextClick}
|
onContextMenu={handleRowContextClick}
|
||||||
/>
|
/>
|
||||||
|
@ -39,13 +39,15 @@ enum HttpMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum HttpProto {
|
enum HttpProto {
|
||||||
Http1 = "HTTP/1.1",
|
Http10 = "HTTP/1.0",
|
||||||
Http2 = "HTTP/2.0",
|
Http11 = "HTTP/1.1",
|
||||||
|
Http20 = "HTTP/2.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
const httpProtoMap = new Map([
|
const httpProtoMap = new Map([
|
||||||
[HttpProto.Http1, HttpProtocol.Http1],
|
[HttpProto.Http10, HttpProtocol.Http10],
|
||||||
[HttpProto.Http2, HttpProtocol.Http2],
|
[HttpProto.Http11, HttpProtocol.Http11],
|
||||||
|
[HttpProto.Http20, HttpProtocol.Http20],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function updateKeyPairItem(key: string, value: string, idx: number, items: KeyValuePair[]): KeyValuePair[] {
|
function updateKeyPairItem(key: string, value: string, idx: number, items: KeyValuePair[]): KeyValuePair[] {
|
||||||
@ -92,7 +94,7 @@ function EditRequest(): JSX.Element {
|
|||||||
|
|
||||||
const [method, setMethod] = useState(HttpMethod.Get);
|
const [method, setMethod] = useState(HttpMethod.Get);
|
||||||
const [url, setURL] = useState("");
|
const [url, setURL] = useState("");
|
||||||
const [proto, setProto] = useState(HttpProto.Http2);
|
const [proto, setProto] = useState(HttpProto.Http20);
|
||||||
const [queryParams, setQueryParams] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
const [queryParams, setQueryParams] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
||||||
const [headers, setHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
const [headers, setHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
||||||
const [body, setBody] = useState("");
|
const [body, setBody] = useState("");
|
||||||
@ -154,7 +156,6 @@ function EditRequest(): JSX.Element {
|
|||||||
|
|
||||||
const newHeaders = sortKeyValuePairs(senderRequest.headers || []);
|
const newHeaders = sortKeyValuePairs(senderRequest.headers || []);
|
||||||
setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
||||||
console.log(senderRequest.response);
|
|
||||||
setResponse(senderRequest.response);
|
setResponse(senderRequest.response);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -34,7 +34,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Editor({ content, contentType, monacoOptions, onChange }: Props): JSX.Element {
|
function Editor({ content, contentType, monacoOptions, onChange }: Props): JSX.Element {
|
||||||
console.log(content);
|
|
||||||
return (
|
return (
|
||||||
<MonacoEditor
|
<MonacoEditor
|
||||||
language={languageForContentType(contentType)}
|
language={languageForContentType(contentType)}
|
||||||
|
@ -62,12 +62,13 @@ interface HttpResponse {
|
|||||||
interface Props {
|
interface Props {
|
||||||
requests: HttpRequest[];
|
requests: HttpRequest[];
|
||||||
activeRowId?: string;
|
activeRowId?: string;
|
||||||
|
actionsCell?: (id: string) => JSX.Element;
|
||||||
onRowClick?: (id: string) => void;
|
onRowClick?: (id: string) => void;
|
||||||
onContextMenu?: (e: React.MouseEvent, id: string) => void;
|
onContextMenu?: (e: React.MouseEvent, id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RequestsTable(props: Props): JSX.Element {
|
export default function RequestsTable(props: Props): JSX.Element {
|
||||||
const { requests, activeRowId, onRowClick, onContextMenu } = props;
|
const { requests, activeRowId, actionsCell, onRowClick, onContextMenu } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer sx={{ overflowX: "initial" }}>
|
<TableContainer sx={{ overflowX: "initial" }}>
|
||||||
@ -78,6 +79,7 @@ export default function RequestsTable(props: Props): JSX.Element {
|
|||||||
<TableCell>Origin</TableCell>
|
<TableCell>Origin</TableCell>
|
||||||
<TableCell>Path</TableCell>
|
<TableCell>Path</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>Status</TableCell>
|
||||||
|
{actionsCell && <TableCell padding="checkbox"></TableCell>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@ -104,6 +106,7 @@ export default function RequestsTable(props: Props): JSX.Element {
|
|||||||
<StatusTableCell>
|
<StatusTableCell>
|
||||||
{response && <Status code={response.statusCode} reason={response.statusReason} />}
|
{response && <Status code={response.statusCode} reason={response.statusReason} />}
|
||||||
</StatusTableCell>
|
</StatusTableCell>
|
||||||
|
{actionsCell && actionsCell(id)}
|
||||||
</RequestTableRow>
|
</RequestTableRow>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -12,9 +12,11 @@ type ResponseStatusProps = {
|
|||||||
|
|
||||||
function mapProto(proto: HttpProtocol): string {
|
function mapProto(proto: HttpProtocol): string {
|
||||||
switch (proto) {
|
switch (proto) {
|
||||||
case HttpProtocol.Http1:
|
case HttpProtocol.Http10:
|
||||||
|
return "HTTP/1.0";
|
||||||
|
case HttpProtocol.Http11:
|
||||||
return "HTTP/1.1";
|
return "HTTP/1.1";
|
||||||
case HttpProtocol.Http2:
|
case HttpProtocol.Http20:
|
||||||
return "HTTP/2.0";
|
return "HTTP/2.0";
|
||||||
default:
|
default:
|
||||||
return proto;
|
return proto;
|
||||||
|
@ -62,8 +62,9 @@ export enum HttpMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum HttpProtocol {
|
export enum HttpProtocol {
|
||||||
Http1 = 'HTTP1',
|
Http10 = 'HTTP10',
|
||||||
Http2 = 'HTTP2'
|
Http11 = 'HTTP11',
|
||||||
|
Http20 = 'HTTP20'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HttpRequestLog = {
|
export type HttpRequestLog = {
|
||||||
|
212
cmd/hetty/cert.go
Normal file
212
cmd/hetty/cert.go
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"github.com/smallstep/truststore"
|
||||||
|
)
|
||||||
|
|
||||||
|
var certUsage = `
|
||||||
|
Usage:
|
||||||
|
hetty cert <subcommand> [flags]
|
||||||
|
|
||||||
|
Certificate management tools.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--help, -h Output this usage text.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
- install Installs a certificate to the system trust store, and
|
||||||
|
(optionally) to the Firefox and Java trust stores.
|
||||||
|
- uninstall Uninstalls a certificate from the system trust store, and
|
||||||
|
(optionally) from the Firefox and Java trust stores.
|
||||||
|
|
||||||
|
Run ` + "`hetty cert <subcommand> --help`" + ` for subcommand specific usage instructions.
|
||||||
|
|
||||||
|
Visit https://hetty.xyz to learn more about Hetty.
|
||||||
|
`
|
||||||
|
|
||||||
|
var certInstallUsage = `
|
||||||
|
Usage:
|
||||||
|
hetty cert install [flags]
|
||||||
|
|
||||||
|
Installs a certificate to the system trust store, and (optionally) to the Firefox
|
||||||
|
and Java trust stores.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--cert Path to certificate. (Default: "~/.hetty/hetty_cert.pem")
|
||||||
|
--firefox Install certificate to Firefox trust store. (Default: false)
|
||||||
|
--java Install certificate to Java trust store. (Default: false)
|
||||||
|
--skip-system Skip installing certificate to system trust store (Default: false)
|
||||||
|
--help, -h Output this usage text.
|
||||||
|
|
||||||
|
Visit https://hetty.xyz to learn more about Hetty.
|
||||||
|
`
|
||||||
|
|
||||||
|
var certUninstallUsage = `
|
||||||
|
Usage:
|
||||||
|
hetty cert uninstall [flags]
|
||||||
|
|
||||||
|
Uninstalls a certificate from the system trust store, and (optionally) from the Firefox
|
||||||
|
and Java trust stores.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--cert Path to certificate. (Default: "~/.hetty/hetty_cert.pem")
|
||||||
|
--firefox Uninstall certificate from Firefox trust store. (Default: false)
|
||||||
|
--java Uninstall certificate from Java trust store. (Default: false)
|
||||||
|
--skip-system Skip uninstalling certificate from system trust store (Default: false)
|
||||||
|
--help, -h Output this usage text.
|
||||||
|
|
||||||
|
Visit https://hetty.xyz to learn more about Hetty.
|
||||||
|
`
|
||||||
|
|
||||||
|
type CertInstallCommand struct {
|
||||||
|
config *Config
|
||||||
|
cert string
|
||||||
|
firefox bool
|
||||||
|
java bool
|
||||||
|
skipSystem bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type CertUninstallCommand struct {
|
||||||
|
config *Config
|
||||||
|
cert string
|
||||||
|
firefox bool
|
||||||
|
java bool
|
||||||
|
skipSystem bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCertCommand(rootConfig *Config) *ffcli.Command {
|
||||||
|
return &ffcli.Command{
|
||||||
|
Name: "cert",
|
||||||
|
Subcommands: []*ffcli.Command{
|
||||||
|
NewCertInstallCommand(rootConfig),
|
||||||
|
NewCertUninstallCommand(rootConfig),
|
||||||
|
},
|
||||||
|
Exec: func(context.Context, []string) error {
|
||||||
|
return flag.ErrHelp
|
||||||
|
},
|
||||||
|
UsageFunc: func(*ffcli.Command) string {
|
||||||
|
return certUsage
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCertInstallCommand(rootConfig *Config) *ffcli.Command {
|
||||||
|
cmd := CertInstallCommand{
|
||||||
|
config: rootConfig,
|
||||||
|
}
|
||||||
|
fs := flag.NewFlagSet("hetty cert install", flag.ExitOnError)
|
||||||
|
|
||||||
|
fs.StringVar(&cmd.cert, "cert", "~/.hetty/hetty_cert.pem", "Path to certificate.")
|
||||||
|
fs.BoolVar(&cmd.firefox, "firefox", false, "Install certificate to Firefox trust store. (Default: false)")
|
||||||
|
fs.BoolVar(&cmd.java, "java", false, "Install certificate to Java trust store. (Default: false)")
|
||||||
|
fs.BoolVar(&cmd.skipSystem, "skip-system", false, "Skip installing certificate to system trust store (Default: false)")
|
||||||
|
|
||||||
|
cmd.config.RegisterFlags(fs)
|
||||||
|
|
||||||
|
return &ffcli.Command{
|
||||||
|
Name: "install",
|
||||||
|
FlagSet: fs,
|
||||||
|
Exec: cmd.Exec,
|
||||||
|
UsageFunc: func(*ffcli.Command) string {
|
||||||
|
return certInstallUsage
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *CertInstallCommand) Exec(_ context.Context, _ []string) error {
|
||||||
|
caCertFile, err := homedir.Expand(cmd.cert)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse certificate filepath: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := []truststore.Option{}
|
||||||
|
|
||||||
|
if cmd.skipSystem {
|
||||||
|
opts = append(opts, truststore.WithNoSystem())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.firefox {
|
||||||
|
opts = append(opts, truststore.WithFirefox())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.java {
|
||||||
|
opts = append(opts, truststore.WithJava())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmd.skipSystem {
|
||||||
|
cmd.config.logger.Info(
|
||||||
|
"To install the certificate in the system trust store, you might be prompted for your password.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := truststore.InstallFile(caCertFile, opts...); err != nil {
|
||||||
|
return fmt.Errorf("failed to install certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.config.logger.Info("Finished installing certificate.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCertUninstallCommand(rootConfig *Config) *ffcli.Command {
|
||||||
|
cmd := CertUninstallCommand{
|
||||||
|
config: rootConfig,
|
||||||
|
}
|
||||||
|
fs := flag.NewFlagSet("hetty cert uninstall", flag.ExitOnError)
|
||||||
|
|
||||||
|
fs.StringVar(&cmd.cert, "cert", "~/.hetty/hetty_cert.pem", "Path to certificate.")
|
||||||
|
fs.BoolVar(&cmd.firefox, "firefox", false, "Uninstall certificate from Firefox trust store. (Default: false)")
|
||||||
|
fs.BoolVar(&cmd.java, "java", false, "Uninstall certificate from Java trust store. (Default: false)")
|
||||||
|
fs.BoolVar(&cmd.skipSystem, "skip-system", false,
|
||||||
|
"Skip uninstalling certificate from system trust store (Default: false)")
|
||||||
|
|
||||||
|
cmd.config.RegisterFlags(fs)
|
||||||
|
|
||||||
|
return &ffcli.Command{
|
||||||
|
Name: "uninstall",
|
||||||
|
FlagSet: fs,
|
||||||
|
Exec: cmd.Exec,
|
||||||
|
UsageFunc: func(*ffcli.Command) string {
|
||||||
|
return certUninstallUsage
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *CertUninstallCommand) Exec(_ context.Context, _ []string) error {
|
||||||
|
caCertFile, err := homedir.Expand(cmd.cert)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse certificate filepath: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := []truststore.Option{}
|
||||||
|
|
||||||
|
if cmd.skipSystem {
|
||||||
|
opts = append(opts, truststore.WithNoSystem())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.firefox {
|
||||||
|
opts = append(opts, truststore.WithFirefox())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.java {
|
||||||
|
opts = append(opts, truststore.WithJava())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmd.skipSystem {
|
||||||
|
cmd.config.logger.Info(
|
||||||
|
"To uninstall the certificate from the system trust store, you might be prompted for your password.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := truststore.UninstallFile(caCertFile, opts...); err != nil {
|
||||||
|
return fmt.Errorf("failed to uninstall certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.config.logger.Info("Finished uninstalling certificate.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
23
cmd/hetty/config.go
Normal file
23
cmd/hetty/config.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the global configuration shared amongst all commands.
|
||||||
|
type Config struct {
|
||||||
|
verbose bool
|
||||||
|
jsonLogs bool
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterFlags registers the flag fields into the provided flag.FlagSet. This
|
||||||
|
// helper function allows subcommands to register the root flags into their
|
||||||
|
// flagsets, creating "global" flags that can be passed after any subcommand at
|
||||||
|
// the commandline.
|
||||||
|
func (cfg *Config) RegisterFlags(fs *flag.FlagSet) {
|
||||||
|
fs.BoolVar(&cfg.verbose, "verbose", false, "Enable verbose logging.")
|
||||||
|
fs.BoolVar(&cfg.jsonLogs, "json", false, "Encode logs as JSON, instead of pretty/human readable output.")
|
||||||
|
}
|
298
cmd/hetty/hetty.go
Normal file
298
cmd/hetty/hetty.go
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"embed"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
badgerdb "github.com/dgraph-io/badger/v3"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"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/db/badger"
|
||||||
|
"github.com/dstotijn/hetty/pkg/proj"
|
||||||
|
"github.com/dstotijn/hetty/pkg/proxy"
|
||||||
|
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||||
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
|
"github.com/dstotijn/hetty/pkg/sender"
|
||||||
|
)
|
||||||
|
|
||||||
|
var version = "0.0.0"
|
||||||
|
|
||||||
|
//go:embed admin
|
||||||
|
//go:embed admin/_next/static
|
||||||
|
//go:embed admin/_next/static/chunks/pages/*.js
|
||||||
|
//go:embed admin/_next/static/*/*.js
|
||||||
|
var adminContent embed.FS
|
||||||
|
|
||||||
|
var hettyUsage = `
|
||||||
|
Usage:
|
||||||
|
hetty [flags] [subcommand] [flags]
|
||||||
|
|
||||||
|
Runs an HTTP server with (MITM) proxy, GraphQL service, and a web based admin interface.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--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")
|
||||||
|
--db Database directory path. (Default: "~/.hetty/db")
|
||||||
|
--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)
|
||||||
|
--verbose Enable verbose logging.
|
||||||
|
--json Encode logs as JSON, instead of pretty/human readable output.
|
||||||
|
--version, -v Output version.
|
||||||
|
--help, -h Output this usage text.
|
||||||
|
|
||||||
|
Subcommands:
|
||||||
|
- cert Certificate management
|
||||||
|
|
||||||
|
Run ` + "`hetty <subcommand> --help`" + ` for subcommand specific usage instructions.
|
||||||
|
|
||||||
|
Visit https://hetty.xyz to learn more about Hetty.
|
||||||
|
`
|
||||||
|
|
||||||
|
type HettyCommand struct {
|
||||||
|
config *Config
|
||||||
|
|
||||||
|
cert string
|
||||||
|
key string
|
||||||
|
db string
|
||||||
|
addr string
|
||||||
|
chrome bool
|
||||||
|
version bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHettyCommand() (*ffcli.Command, *Config) {
|
||||||
|
cmd := HettyCommand{
|
||||||
|
config: &Config{},
|
||||||
|
}
|
||||||
|
|
||||||
|
fs := flag.NewFlagSet("hetty", flag.ExitOnError)
|
||||||
|
|
||||||
|
fs.StringVar(&cmd.cert, "cert", "~/.hetty/hetty_cert.pem",
|
||||||
|
"Path to root CA certificate. Creates a new certificate if file doesn't exist.")
|
||||||
|
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.")
|
||||||
|
fs.StringVar(&cmd.db, "db", "~/.hetty/db", "Database directory path.")
|
||||||
|
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.version, "version", false, "Output version.")
|
||||||
|
fs.BoolVar(&cmd.version, "v", false, "Output version.")
|
||||||
|
|
||||||
|
cmd.config.RegisterFlags(fs)
|
||||||
|
|
||||||
|
return &ffcli.Command{
|
||||||
|
Name: "hetty",
|
||||||
|
FlagSet: fs,
|
||||||
|
Subcommands: []*ffcli.Command{
|
||||||
|
NewCertCommand(cmd.config),
|
||||||
|
},
|
||||||
|
Exec: cmd.Exec,
|
||||||
|
UsageFunc: func(*ffcli.Command) string {
|
||||||
|
return hettyUsage
|
||||||
|
},
|
||||||
|
}, cmd.config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error {
|
||||||
|
ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
if cmd.version {
|
||||||
|
fmt.Fprint(os.Stdout, version+"\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mainLogger := cmd.config.logger.Named("main")
|
||||||
|
|
||||||
|
listenHost, listenPort, err := net.SplitHostPort(cmd.addr)
|
||||||
|
if err != nil {
|
||||||
|
mainLogger.Fatal("Failed to parse listening address.", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("http://%v:%v", listenHost, listenPort)
|
||||||
|
if listenHost == "" || listenHost == "0.0.0.0" || listenHost == "127.0.0.1" || listenHost == "::1" {
|
||||||
|
url = fmt.Sprintf("http://localhost:%v", listenPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand `~` in filepaths.
|
||||||
|
caCertFile, err := homedir.Expand(cmd.cert)
|
||||||
|
if err != nil {
|
||||||
|
cmd.config.logger.Fatal("Failed to parse CA certificate filepath.", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
caKeyFile, err := homedir.Expand(cmd.key)
|
||||||
|
if err != nil {
|
||||||
|
cmd.config.logger.Fatal("Failed to parse CA private key filepath.", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath, err := homedir.Expand(cmd.db)
|
||||||
|
if err != nil {
|
||||||
|
cmd.config.logger.Fatal("Failed to parse database path.", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load existing CA certificate and key from disk, or generate and write
|
||||||
|
// to disk if no files exist yet.
|
||||||
|
caCert, caKey, err := proxy.LoadOrCreateCA(caKeyFile, caCertFile)
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
// we're running in debug mode, bump the minimal level to `WARN`.
|
||||||
|
dbLogger := cmd.config.logger.Named("badgerdb").WithOptions(zap.IncreaseLevel(zapcore.WarnLevel))
|
||||||
|
|
||||||
|
dbSugaredLogger := dbLogger.Sugar()
|
||||||
|
|
||||||
|
badger, err := badger.OpenDatabase(
|
||||||
|
badgerdb.DefaultOptions(dbPath).WithLogger(badger.NewLogger(dbSugaredLogger)),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
cmd.config.logger.Fatal("Failed to open database.", zap.Error(err))
|
||||||
|
}
|
||||||
|
defer badger.Close()
|
||||||
|
|
||||||
|
scope := &scope.Scope{}
|
||||||
|
|
||||||
|
reqLogService := reqlog.NewService(reqlog.Config{
|
||||||
|
Scope: scope,
|
||||||
|
Repository: badger,
|
||||||
|
Logger: cmd.config.logger.Named("reqlog").Sugar(),
|
||||||
|
})
|
||||||
|
|
||||||
|
senderService := sender.NewService(sender.Config{
|
||||||
|
Repository: badger,
|
||||||
|
ReqLogService: reqLogService,
|
||||||
|
})
|
||||||
|
|
||||||
|
projService, err := proj.NewService(proj.Config{
|
||||||
|
Repository: badger,
|
||||||
|
ReqLogService: reqLogService,
|
||||||
|
SenderService: senderService,
|
||||||
|
Scope: scope,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
cmd.config.logger.Fatal("Failed to create new projects service.", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy, err := proxy.NewProxy(proxy.Config{
|
||||||
|
CACert: caCert,
|
||||||
|
CAKey: caKey,
|
||||||
|
Logger: cmd.config.logger.Named("proxy").Sugar(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
cmd.config.logger.Fatal("Failed to create new proxy.", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.UseRequestModifier(reqLogService.RequestModifier)
|
||||||
|
proxy.UseResponseModifier(reqLogService.ResponseModifier)
|
||||||
|
|
||||||
|
fsSub, err := fs.Sub(adminContent, "admin")
|
||||||
|
if err != nil {
|
||||||
|
cmd.config.logger.Fatal("Failed to construct file system subtree from admin dir.", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
adminHandler := http.FileServer(http.FS(fsSub))
|
||||||
|
router := mux.NewRouter().SkipClean(true)
|
||||||
|
adminRouter := router.MatcherFunc(func(req *http.Request, match *mux.RouteMatch) bool {
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
host, _, _ := net.SplitHostPort(req.Host)
|
||||||
|
|
||||||
|
// Serve local admin routes when either:
|
||||||
|
// - The `Host` is well-known, e.g. `hetty.proxy`, `localhost:[port]`
|
||||||
|
// or the listen addr `[host]:[port]`.
|
||||||
|
// - The request is not for TLS proxying (e.g. no `CONNECT`) and not
|
||||||
|
// for proxying an external URL. E.g. Request-Line (RFC 7230, Section 3.1.1)
|
||||||
|
// has no scheme.
|
||||||
|
return strings.EqualFold(host, hostname) ||
|
||||||
|
req.Host == "hetty.proxy" ||
|
||||||
|
req.Host == fmt.Sprintf("%v:%v", "localhost", listenPort) ||
|
||||||
|
req.Host == fmt.Sprintf("%v:%v", listenHost, listenPort) ||
|
||||||
|
req.Method != http.MethodConnect && !strings.HasPrefix(req.RequestURI, "http://")
|
||||||
|
}).Subrouter().StrictSlash(true)
|
||||||
|
|
||||||
|
// GraphQL server.
|
||||||
|
gqlEndpoint := "/api/graphql/"
|
||||||
|
adminRouter.Path(gqlEndpoint).Handler(api.HTTPHandler(&api.Resolver{
|
||||||
|
ProjectService: projService,
|
||||||
|
RequestLogService: reqLogService,
|
||||||
|
SenderService: senderService,
|
||||||
|
}, gqlEndpoint))
|
||||||
|
|
||||||
|
// Admin interface.
|
||||||
|
adminRouter.PathPrefix("").Handler(adminHandler)
|
||||||
|
|
||||||
|
// Fallback (default) is the Proxy handler.
|
||||||
|
router.PathPrefix("").Handler(proxy)
|
||||||
|
|
||||||
|
httpServer := &http.Server{
|
||||||
|
Addr: cmd.addr,
|
||||||
|
Handler: router,
|
||||||
|
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}, // Disable HTTP/2
|
||||||
|
ErrorLog: zap.NewStdLog(cmd.config.logger.Named("http")),
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
mainLogger.Info(fmt.Sprintf("Hetty (v%v) is running on %v ...", version, cmd.addr))
|
||||||
|
mainLogger.Info(fmt.Sprintf("\x1b[%dm%s\x1b[0m", uint8(32), "Get started at "+url))
|
||||||
|
|
||||||
|
err := httpServer.ListenAndServe()
|
||||||
|
if err != http.ErrServerClosed {
|
||||||
|
mainLogger.Fatal("HTTP server closed unexpected.", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if cmd.chrome {
|
||||||
|
ctx, cancel := chrome.NewExecAllocator(ctx, chrome.Config{
|
||||||
|
ProxyServer: url,
|
||||||
|
ProxyBypassHosts: []string{url},
|
||||||
|
})
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
taskCtx, cancel := chromedp.NewContext(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = chromedp.Run(taskCtx, chromedp.Navigate(url))
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, exec.ErrNotFound):
|
||||||
|
mainLogger.Info("Chrome executable not found.")
|
||||||
|
case err != nil:
|
||||||
|
mainLogger.Error(fmt.Sprintf("Failed to navigate to %v.", url), zap.Error(err))
|
||||||
|
default:
|
||||||
|
mainLogger.Info("Launched Chrome.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for interrupt signal.
|
||||||
|
<-ctx.Done()
|
||||||
|
// Restore signal, allowing "force quit".
|
||||||
|
stop()
|
||||||
|
|
||||||
|
mainLogger.Info("Shutting down HTTP server. Press Ctrl+C to force quit.")
|
||||||
|
|
||||||
|
// Note: We expect httpServer.Handler to handle timeouts, thus, we don't
|
||||||
|
// need a context value with deadline here.
|
||||||
|
//nolint:contextcheck
|
||||||
|
err = httpServer.Shutdown(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to shutdown HTTP server: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
@ -1,163 +1,35 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"context"
|
||||||
"embed"
|
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
llog "log"
|
||||||
"io/fs"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/99designs/gqlgen/graphql/handler"
|
"go.uber.org/zap"
|
||||||
"github.com/99designs/gqlgen/graphql/playground"
|
|
||||||
badgerdb "github.com/dgraph-io/badger/v3"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/mitchellh/go-homedir"
|
|
||||||
|
|
||||||
"github.com/dstotijn/hetty/pkg/api"
|
"github.com/dstotijn/hetty/pkg/log"
|
||||||
"github.com/dstotijn/hetty/pkg/db/badger"
|
|
||||||
"github.com/dstotijn/hetty/pkg/proj"
|
|
||||||
"github.com/dstotijn/hetty/pkg/proxy"
|
|
||||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
|
||||||
"github.com/dstotijn/hetty/pkg/scope"
|
|
||||||
"github.com/dstotijn/hetty/pkg/sender"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "0.0.0"
|
|
||||||
|
|
||||||
// Flag variables.
|
|
||||||
var (
|
|
||||||
caCertFile string
|
|
||||||
caKeyFile string
|
|
||||||
dbPath string
|
|
||||||
addr string
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed admin
|
|
||||||
//go:embed admin/_next/static
|
|
||||||
//go:embed admin/_next/static/chunks/pages/*.js
|
|
||||||
//go:embed admin/_next/static/*/*.js
|
|
||||||
var adminContent embed.FS
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := run(); err != nil {
|
hettyCmd, cfg := NewHettyCommand()
|
||||||
log.Fatalf("[ERROR]: %v", err)
|
|
||||||
|
if err := hettyCmd.Parse(os.Args[1:]); err != nil {
|
||||||
|
llog.Fatalf("Failed to parse command line arguments: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err := log.NewZapLogger(cfg.verbose, cfg.jsonLogs)
|
||||||
|
if err != nil {
|
||||||
|
llog.Fatal(err)
|
||||||
|
}
|
||||||
|
//nolint:errcheck
|
||||||
|
defer logger.Sync()
|
||||||
|
|
||||||
|
cfg.logger = logger
|
||||||
|
|
||||||
|
err = hettyCmd.Run(context.Background())
|
||||||
|
if err != nil && !errors.Is(err, flag.ErrHelp) {
|
||||||
|
logger.Fatal("Command failed.", zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func run() error {
|
|
||||||
flag.StringVar(&caCertFile, "cert", "~/.hetty/hetty_cert.pem",
|
|
||||||
"CA certificate filepath. Creates a new CA certificate if file doesn't exist")
|
|
||||||
flag.StringVar(&caKeyFile, "key", "~/.hetty/hetty_key.pem",
|
|
||||||
"CA private key filepath. Creates a new CA private key if file doesn't exist")
|
|
||||||
flag.StringVar(&dbPath, "db", "~/.hetty/db", "Database directory path")
|
|
||||||
flag.StringVar(&addr, "addr", ":8080", "TCP address to listen on, in the form \"host:port\"")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
// Expand `~` in filepaths.
|
|
||||||
caCertFile, err := homedir.Expand(caCertFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not parse CA certificate filepath: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
caKeyFile, err := homedir.Expand(caKeyFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not parse CA private key filepath: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dbPath, err := homedir.Expand(dbPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not parse projects filepath: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load existing CA certificate and key from disk, or generate and write
|
|
||||||
// to disk if no files exist yet.
|
|
||||||
caCert, caKey, err := proxy.LoadOrCreateCA(caKeyFile, caCertFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not create/load CA key pair: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
badger, err := badger.OpenDatabase(badgerdb.DefaultOptions(dbPath))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not open badger database: %w", err)
|
|
||||||
}
|
|
||||||
defer badger.Close()
|
|
||||||
|
|
||||||
scope := &scope.Scope{}
|
|
||||||
|
|
||||||
reqLogService := reqlog.NewService(reqlog.Config{
|
|
||||||
Scope: scope,
|
|
||||||
Repository: badger,
|
|
||||||
})
|
|
||||||
|
|
||||||
senderService := sender.NewService(sender.Config{
|
|
||||||
Repository: badger,
|
|
||||||
ReqLogService: reqLogService,
|
|
||||||
})
|
|
||||||
|
|
||||||
projService, err := proj.NewService(proj.Config{
|
|
||||||
Repository: badger,
|
|
||||||
ReqLogService: reqLogService,
|
|
||||||
SenderService: senderService,
|
|
||||||
Scope: scope,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not create new project service: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := proxy.NewProxy(caCert, caKey)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not create proxy: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.UseRequestModifier(reqLogService.RequestModifier)
|
|
||||||
p.UseResponseModifier(reqLogService.ResponseModifier)
|
|
||||||
|
|
||||||
fsSub, err := fs.Sub(adminContent, "admin")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not prepare subtree file system: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
adminHandler := http.FileServer(http.FS(fsSub))
|
|
||||||
router := mux.NewRouter().SkipClean(true)
|
|
||||||
adminRouter := router.MatcherFunc(func(req *http.Request, match *mux.RouteMatch) bool {
|
|
||||||
hostname, _ := os.Hostname()
|
|
||||||
host, _, _ := net.SplitHostPort(req.Host)
|
|
||||||
return strings.EqualFold(host, hostname) || (req.Host == "hetty.proxy" || req.Host == "localhost:8080")
|
|
||||||
}).Subrouter().StrictSlash(true)
|
|
||||||
|
|
||||||
// GraphQL server.
|
|
||||||
adminRouter.Path("/api/playground/").Handler(playground.Handler("GraphQL Playground", "/api/graphql/"))
|
|
||||||
adminRouter.Path("/api/graphql/").Handler(
|
|
||||||
handler.NewDefaultServer(api.NewExecutableSchema(api.Config{Resolvers: &api.Resolver{
|
|
||||||
ProjectService: projService,
|
|
||||||
RequestLogService: reqLogService,
|
|
||||||
SenderService: senderService,
|
|
||||||
}})))
|
|
||||||
|
|
||||||
// Admin interface.
|
|
||||||
adminRouter.PathPrefix("").Handler(adminHandler)
|
|
||||||
|
|
||||||
// Fallback (default) is the Proxy handler.
|
|
||||||
router.PathPrefix("").Handler(p)
|
|
||||||
|
|
||||||
s := &http.Server{
|
|
||||||
Addr: addr,
|
|
||||||
Handler: router,
|
|
||||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}, // Disable HTTP/2
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("[INFO] Hetty (v%v) is running on %v ...", version, addr)
|
|
||||||
|
|
||||||
err = s.ListenAndServe()
|
|
||||||
if err != nil && errors.Is(err, http.ErrServerClosed) {
|
|
||||||
return fmt.Errorf("http server closed unexpected: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
24
go.mod
24
go.mod
@ -4,22 +4,31 @@ go 1.17
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/gqlgen v0.14.0
|
github.com/99designs/gqlgen v0.14.0
|
||||||
|
github.com/chromedp/chromedp v0.7.8
|
||||||
github.com/dgraph-io/badger/v3 v3.2103.2
|
github.com/dgraph-io/badger/v3 v3.2103.2
|
||||||
github.com/google/go-cmp v0.5.6
|
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/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 v1.3.1
|
||||||
|
github.com/peterbourgon/ff/v3 v3.1.2
|
||||||
|
github.com/smallstep/truststore v0.11.0
|
||||||
github.com/vektah/gqlparser/v2 v2.2.0
|
github.com/vektah/gqlparser/v2 v2.2.0
|
||||||
|
go.uber.org/zap v1.21.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/agnivade/levenshtein v1.1.0 // indirect
|
github.com/agnivade/levenshtein v1.1.0 // indirect
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf // indirect
|
||||||
|
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
|
||||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
|
github.com/gobwas/ws v1.1.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
|
||||||
@ -28,7 +37,9 @@ require (
|
|||||||
github.com/google/flatbuffers v1.12.1 // indirect
|
github.com/google/flatbuffers v1.12.1 // indirect
|
||||||
github.com/gorilla/websocket v1.4.2 // indirect
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
github.com/hashicorp/golang-lru v0.5.1 // indirect
|
github.com/hashicorp/golang-lru v0.5.1 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.12.3 // indirect
|
github.com/klauspost/compress v1.12.3 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.1.2 // 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/russross/blackfriday/v2 v2.0.1 // indirect
|
||||||
@ -36,10 +47,13 @@ require (
|
|||||||
github.com/urfave/cli/v2 v2.1.1 // indirect
|
github.com/urfave/cli/v2 v2.1.1 // indirect
|
||||||
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e // indirect
|
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e // indirect
|
||||||
go.opencensus.io v0.22.5 // indirect
|
go.opencensus.io v0.22.5 // indirect
|
||||||
golang.org/x/mod v0.3.0 // indirect
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
|
go.uber.org/multierr v1.6.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
|
golang.org/x/mod v0.4.2 // indirect
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a // 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
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.2.4 // indirect
|
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||||
|
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
|
||||||
)
|
)
|
||||||
|
68
go.sum
68
go.sum
@ -12,10 +12,18 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo
|
|||||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
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/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/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/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
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 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 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U=
|
||||||
|
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/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||||
|
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
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/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-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
@ -38,6 +46,12 @@ github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8
|
|||||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
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/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/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/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
|
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/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
|
||||||
|
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||||
@ -67,6 +81,9 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+
|
|||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
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/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/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||||
|
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/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
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/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 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU=
|
||||||
@ -78,6 +95,8 @@ 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/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/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/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
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 h1:BGQISyhl7Gc9W/gMYmAJONh9mT6AYeyeTjNupNPknMs=
|
||||||
github.com/matryer/moq v0.2.5/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE=
|
github.com/matryer/moq v0.2.5/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE=
|
||||||
@ -93,7 +112,12 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
|||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
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/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
|
||||||
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
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/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
|
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/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
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=
|
||||||
@ -110,6 +134,8 @@ github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJ
|
|||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
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/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/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/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM=
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
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 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
@ -122,8 +148,10 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
|
|||||||
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.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
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.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/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||||
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
|
||||||
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||||
@ -135,8 +163,17 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:
|
|||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.2.1/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=
|
||||||
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
|
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
|
||||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
|
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||||
|
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/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||||
|
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/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
|
||||||
|
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-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=
|
||||||
@ -145,9 +182,11 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
|
|||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
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-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-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/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
|
||||||
golang.org/x/mod v0.3.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/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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-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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@ -156,8 +195,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||||||
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-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-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@ -166,6 +206,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-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-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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@ -175,8 +216,14 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
|
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-20210124154548-22da62e12c0c/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-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/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/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=
|
||||||
@ -188,8 +235,9 @@ golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBn
|
|||||||
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-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-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ=
|
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
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/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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@ -202,11 +250,19 @@ google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRn
|
|||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
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/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.2/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 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
|
||||||
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/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-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||||
|
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=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
|
||||||
|
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 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=
|
sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
|
||||||
|
@ -897,8 +897,9 @@ enum HttpMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum HttpProtocol {
|
enum HttpProtocol {
|
||||||
HTTP1
|
HTTP10
|
||||||
HTTP2
|
HTTP11
|
||||||
|
HTTP20
|
||||||
}
|
}
|
||||||
|
|
||||||
scalar Time
|
scalar Time
|
||||||
|
21
pkg/api/http.go
Normal file
21
pkg/api/http.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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
|
||||||
|
}
|
@ -186,18 +186,20 @@ func (e HTTPMethod) MarshalGQL(w io.Writer) {
|
|||||||
type HTTPProtocol string
|
type HTTPProtocol string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
HTTPProtocolHTTP1 HTTPProtocol = "HTTP1"
|
HTTPProtocolHTTP10 HTTPProtocol = "HTTP10"
|
||||||
HTTPProtocolHTTP2 HTTPProtocol = "HTTP2"
|
HTTPProtocolHTTP11 HTTPProtocol = "HTTP11"
|
||||||
|
HTTPProtocolHTTP20 HTTPProtocol = "HTTP20"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AllHTTPProtocol = []HTTPProtocol{
|
var AllHTTPProtocol = []HTTPProtocol{
|
||||||
HTTPProtocolHTTP1,
|
HTTPProtocolHTTP10,
|
||||||
HTTPProtocolHTTP2,
|
HTTPProtocolHTTP11,
|
||||||
|
HTTPProtocolHTTP20,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e HTTPProtocol) IsValid() bool {
|
func (e HTTPProtocol) IsValid() bool {
|
||||||
switch e {
|
switch e {
|
||||||
case HTTPProtocolHTTP1, HTTPProtocolHTTP2:
|
case HTTPProtocolHTTP10, HTTPProtocolHTTP11, HTTPProtocolHTTP20:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -22,13 +22,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var httpProtocolMap = map[string]HTTPProtocol{
|
var httpProtocolMap = map[string]HTTPProtocol{
|
||||||
sender.HTTPProto1: HTTPProtocolHTTP1,
|
sender.HTTPProto10: HTTPProtocolHTTP10,
|
||||||
sender.HTTPProto2: HTTPProtocolHTTP2,
|
sender.HTTPProto11: HTTPProtocolHTTP11,
|
||||||
|
sender.HTTPProto20: HTTPProtocolHTTP20,
|
||||||
}
|
}
|
||||||
|
|
||||||
var revHTTPProtocolMap = map[HTTPProtocol]string{
|
var revHTTPProtocolMap = map[HTTPProtocol]string{
|
||||||
HTTPProtocolHTTP1: sender.HTTPProto1,
|
HTTPProtocolHTTP10: sender.HTTPProto10,
|
||||||
HTTPProtocolHTTP2: sender.HTTPProto2,
|
HTTPProtocolHTTP11: sender.HTTPProto11,
|
||||||
|
HTTPProtocolHTTP20: sender.HTTPProto20,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
@ -406,7 +408,10 @@ func (r *mutationResolver) SetSenderRequestFilter(
|
|||||||
return findReqFilterToSenderReqFilter(filter), nil
|
return findReqFilterToSenderReqFilter(filter), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) CreateOrUpdateSenderRequest(ctx context.Context, input SenderRequestInput) (*SenderRequest, error) {
|
func (r *mutationResolver) CreateOrUpdateSenderRequest(
|
||||||
|
ctx context.Context,
|
||||||
|
input SenderRequestInput,
|
||||||
|
) (*SenderRequest, error) {
|
||||||
req := sender.Request{
|
req := sender.Request{
|
||||||
URL: input.URL,
|
URL: input.URL,
|
||||||
Header: make(http.Header),
|
Header: make(http.Header),
|
||||||
@ -447,7 +452,10 @@ func (r *mutationResolver) CreateOrUpdateSenderRequest(ctx context.Context, inpu
|
|||||||
return &senderReq, nil
|
return &senderReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) CreateSenderRequestFromHTTPRequestLog(ctx context.Context, id ulid.ULID) (*SenderRequest, error) {
|
func (r *mutationResolver) CreateSenderRequestFromHTTPRequestLog(
|
||||||
|
ctx context.Context,
|
||||||
|
id ulid.ULID,
|
||||||
|
) (*SenderRequest, error) {
|
||||||
req, err := r.SenderService.CloneFromRequestLog(ctx, id)
|
req, err := r.SenderService.CloneFromRequestLog(ctx, id)
|
||||||
if errors.Is(err, proj.ErrNoProject) {
|
if errors.Is(err, proj.ErrNoProject) {
|
||||||
return nil, noActiveProjectErr(ctx)
|
return nil, noActiveProjectErr(ctx)
|
||||||
@ -471,10 +479,13 @@ func (r *mutationResolver) SendRequest(ctx context.Context, id ulid.ULID) (*Send
|
|||||||
|
|
||||||
var sendErr *sender.SendError
|
var sendErr *sender.SendError
|
||||||
|
|
||||||
|
//nolint:contextcheck
|
||||||
req, err := r.SenderService.SendRequest(ctx2, id)
|
req, err := r.SenderService.SendRequest(ctx2, id)
|
||||||
if errors.Is(err, proj.ErrNoProject) {
|
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, proj.ErrNoProject):
|
||||||
return nil, noActiveProjectErr(ctx)
|
return nil, noActiveProjectErr(ctx)
|
||||||
} else if errors.As(err, &sendErr) {
|
case errors.As(err, &sendErr):
|
||||||
return nil, &gqlerror.Error{
|
return nil, &gqlerror.Error{
|
||||||
Path: graphql.GetPath(ctx),
|
Path: graphql.GetPath(ctx),
|
||||||
Message: fmt.Sprintf("Sending request failed: %v", sendErr.Unwrap()),
|
Message: fmt.Sprintf("Sending request failed: %v", sendErr.Unwrap()),
|
||||||
@ -482,7 +493,7 @@ func (r *mutationResolver) SendRequest(ctx context.Context, id ulid.ULID) (*Send
|
|||||||
"code": "send_request_failed",
|
"code": "send_request_failed",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
case err != nil:
|
||||||
return nil, fmt.Errorf("could not send request: %w", err)
|
return nil, fmt.Errorf("could not send request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,8 +157,9 @@ enum HttpMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum HttpProtocol {
|
enum HttpProtocol {
|
||||||
HTTP1
|
HTTP10
|
||||||
HTTP2
|
HTTP11
|
||||||
|
HTTP20
|
||||||
}
|
}
|
||||||
|
|
||||||
scalar Time
|
scalar Time
|
||||||
|
34
pkg/chrome/chrome.go
Normal file
34
pkg/chrome/chrome.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package chrome
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultOpts = []chromedp.ExecAllocatorOption{
|
||||||
|
chromedp.NoFirstRun,
|
||||||
|
chromedp.NoDefaultBrowserCheck,
|
||||||
|
chromedp.IgnoreCertErrors,
|
||||||
|
chromedp.Flag("test-type ", true), // This prevents the `ignore-certificate-errors` warning.
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ProxyServer string
|
||||||
|
ProxyBypassHosts []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExecAllocator returns a new context setup with a chromedp.ExecAllocator.
|
||||||
|
// Its `context.Context` return value can be used to create subsequent contexts for interacting
|
||||||
|
// with an allocated Chrome browser.
|
||||||
|
func NewExecAllocator(ctx context.Context, cfg Config) (context.Context, context.CancelFunc) {
|
||||||
|
proxyBypass := strings.Join(append([]string{"<-loopback"}, cfg.ProxyBypassHosts...), ";")
|
||||||
|
//nolint:gocritic
|
||||||
|
opts := append(defaultOpts,
|
||||||
|
chromedp.ProxyServer(cfg.ProxyServer),
|
||||||
|
chromedp.Flag("proxy-bypass-list", proxyBypass),
|
||||||
|
)
|
||||||
|
|
||||||
|
return chromedp.NewExecAllocator(ctx, opts...)
|
||||||
|
}
|
21
pkg/db/badger/logger.go
Normal file
21
pkg/db/badger/logger.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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)
|
||||||
|
}
|
@ -14,7 +14,11 @@ import (
|
|||||||
"github.com/dstotijn/hetty/pkg/scope"
|
"github.com/dstotijn/hetty/pkg/scope"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (db *Database) FindRequestLogs(ctx context.Context, filter reqlog.FindRequestsFilter, scope *scope.Scope) ([]reqlog.RequestLog, error) {
|
func (db *Database) FindRequestLogs(
|
||||||
|
ctx context.Context,
|
||||||
|
filter reqlog.FindRequestsFilter,
|
||||||
|
scope *scope.Scope) ([]reqlog.RequestLog, error,
|
||||||
|
) {
|
||||||
if filter.ProjectID.Compare(ulid.ULID{}) == 0 {
|
if filter.ProjectID.Compare(ulid.ULID{}) == 0 {
|
||||||
return nil, reqlog.ErrProjectIDMustBeSet
|
return nil, reqlog.ErrProjectIDMustBeSet
|
||||||
}
|
}
|
||||||
@ -231,6 +235,7 @@ func findRequestLogIDsByProjectID(txn *badger.Txn, projectID ulid.ULID) ([]ulid.
|
|||||||
reqLogIDs := make([]ulid.ULID, 0)
|
reqLogIDs := make([]ulid.ULID, 0)
|
||||||
opts := badger.DefaultIteratorOptions
|
opts := badger.DefaultIteratorOptions
|
||||||
opts.PrefetchValues = false
|
opts.PrefetchValues = false
|
||||||
|
opts.Reverse = true
|
||||||
iterator := txn.NewIterator(opts)
|
iterator := txn.NewIterator(opts)
|
||||||
defer iterator.Close()
|
defer iterator.Close()
|
||||||
|
|
||||||
@ -238,7 +243,7 @@ func findRequestLogIDsByProjectID(txn *badger.Txn, projectID ulid.ULID) ([]ulid.
|
|||||||
|
|
||||||
prefix := entryKey(reqLogPrefix, reqLogProjectIDIndex, projectID[:])
|
prefix := entryKey(reqLogPrefix, reqLogProjectIDIndex, projectID[:])
|
||||||
|
|
||||||
for iterator.Seek(prefix); iterator.ValidForPrefix(prefix); iterator.Next() {
|
for iterator.Seek(append(prefix, 255)); iterator.ValidForPrefix(prefix); iterator.Next() {
|
||||||
projectIndexKey = iterator.Item().KeyCopy(projectIndexKey)
|
projectIndexKey = iterator.Item().KeyCopy(projectIndexKey)
|
||||||
|
|
||||||
var id ulid.ULID
|
var id ulid.ULID
|
||||||
|
@ -46,7 +46,7 @@ func TestFindRequestLogs(t *testing.T) {
|
|||||||
|
|
||||||
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||||
|
|
||||||
exp := []reqlog.RequestLog{
|
fixtures := []reqlog.RequestLog{
|
||||||
{
|
{
|
||||||
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
@ -80,7 +80,7 @@ func TestFindRequestLogs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store fixtures.
|
// Store fixtures.
|
||||||
for _, reqLog := range exp {
|
for _, reqLog := range fixtures {
|
||||||
err = database.StoreRequestLog(context.Background(), reqLog)
|
err = database.StoreRequestLog(context.Background(), reqLog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error creating request log fixture: %v", err)
|
t.Fatalf("unexpected error creating request log fixture: %v", err)
|
||||||
@ -103,6 +103,12 @@ func TestFindRequestLogs(t *testing.T) {
|
|||||||
t.Fatalf("unexpected error finding request logs: %v", err)
|
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 != "" {
|
if diff := cmp.Diff(exp, got); diff != "" {
|
||||||
t.Fatalf("request logs not equal (-exp, +got):\n%v", diff)
|
t.Fatalf("request logs not equal (-exp, +got):\n%v", diff)
|
||||||
}
|
}
|
||||||
|
@ -63,7 +63,11 @@ func (db *Database) FindSenderRequestByID(ctx context.Context, senderReqID ulid.
|
|||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *Database) FindSenderRequests(ctx context.Context, filter sender.FindRequestsFilter, scope *scope.Scope) ([]sender.Request, error) {
|
func (db *Database) FindSenderRequests(
|
||||||
|
ctx context.Context,
|
||||||
|
filter sender.FindRequestsFilter,
|
||||||
|
scope *scope.Scope,
|
||||||
|
) ([]sender.Request, error) {
|
||||||
if filter.ProjectID.Compare(ulid.ULID{}) == 0 {
|
if filter.ProjectID.Compare(ulid.ULID{}) == 0 {
|
||||||
return nil, sender.ErrProjectIDMustBeSet
|
return nil, sender.ErrProjectIDMustBeSet
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ func TestFindRequestByID(t *testing.T) {
|
|||||||
|
|
||||||
URL: exampleURL,
|
URL: exampleURL,
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
Proto: sender.HTTPProto2,
|
Proto: sender.HTTPProto20,
|
||||||
Header: http.Header{
|
Header: http.Header{
|
||||||
"X-Foo": []string{"bar"},
|
"X-Foo": []string{"bar"},
|
||||||
},
|
},
|
||||||
|
87
pkg/log/log.go
Normal file
87
pkg/log/log.go
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Debugw(msg string, v ...interface{})
|
||||||
|
Infow(msg string, v ...interface{})
|
||||||
|
Errorw(msg string, v ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewZapLogger(verbose, jsonLogs bool) (*zap.Logger, error) {
|
||||||
|
var config zap.Config
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
config = zap.Config{
|
||||||
|
Level: zap.NewAtomicLevelAt(zap.DebugLevel),
|
||||||
|
Development: true,
|
||||||
|
Encoding: "json",
|
||||||
|
EncoderConfig: zapcore.EncoderConfig{
|
||||||
|
TimeKey: "ts",
|
||||||
|
LevelKey: "level",
|
||||||
|
NameKey: "logger",
|
||||||
|
CallerKey: "caller",
|
||||||
|
FunctionKey: zapcore.OmitKey,
|
||||||
|
MessageKey: "message",
|
||||||
|
StacktraceKey: "stacktrace",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||||
|
EncodeTime: zapcore.RFC3339TimeEncoder,
|
||||||
|
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||||
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
|
},
|
||||||
|
OutputPaths: []string{"stderr"},
|
||||||
|
ErrorOutputPaths: []string{"stderr"},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config = zap.NewProductionConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !jsonLogs {
|
||||||
|
config.Encoding = "console"
|
||||||
|
config.EncoderConfig = zapcore.EncoderConfig{
|
||||||
|
TimeKey: "T",
|
||||||
|
LevelKey: "L",
|
||||||
|
NameKey: "N",
|
||||||
|
CallerKey: zapcore.OmitKey,
|
||||||
|
FunctionKey: zapcore.OmitKey,
|
||||||
|
MessageKey: "M",
|
||||||
|
StacktraceKey: zapcore.OmitKey,
|
||||||
|
ConsoleSeparator: " ",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.CapitalColorLevelEncoder,
|
||||||
|
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||||
|
enc.AppendString(t.Format("2006/01/02 15:04:05"))
|
||||||
|
},
|
||||||
|
EncodeName: func(loggerName string, enc zapcore.PrimitiveArrayEncoder) {
|
||||||
|
// Print logger name in cyan (ANSI code 36).
|
||||||
|
enc.AppendString(fmt.Sprintf("\x1b[%dm%s\x1b[0m", uint8(36), "["+loggerName+"]"))
|
||||||
|
},
|
||||||
|
EncodeDuration: zapcore.StringDurationEncoder,
|
||||||
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
|
}
|
||||||
|
|
||||||
|
if verbose {
|
||||||
|
config.EncoderConfig.CallerKey = "C"
|
||||||
|
config.EncoderConfig.StacktraceKey = "S"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.Build()
|
||||||
|
}
|
||||||
|
|
||||||
|
type NopLogger struct{}
|
||||||
|
|
||||||
|
func (nop NopLogger) Debugw(_ string, _ ...interface{}) {}
|
||||||
|
func (nop NopLogger) Infow(_ string, _ ...interface{}) {}
|
||||||
|
func (nop NopLogger) Errorw(_ string, _ ...interface{}) {}
|
||||||
|
|
||||||
|
func NewNopLogger() NopLogger {
|
||||||
|
return NopLogger{}
|
||||||
|
}
|
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
"sync"
|
||||||
@ -21,11 +20,6 @@ import (
|
|||||||
//nolint:gosec
|
//nolint:gosec
|
||||||
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
|
var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
type (
|
|
||||||
OnProjectOpenFn func(projectID ulid.ULID) error
|
|
||||||
OnProjectCloseFn func(projectID ulid.ULID) error
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service is used for managing projects.
|
// Service is used for managing projects.
|
||||||
type Service interface {
|
type Service interface {
|
||||||
CreateProject(ctx context.Context, name string) (Project, error)
|
CreateProject(ctx context.Context, name string) (Project, error)
|
||||||
@ -39,8 +33,6 @@ type Service interface {
|
|||||||
SetScopeRules(ctx context.Context, rules []scope.Rule) error
|
SetScopeRules(ctx context.Context, rules []scope.Rule) error
|
||||||
SetRequestLogFindFilter(ctx context.Context, filter reqlog.FindRequestsFilter) error
|
SetRequestLogFindFilter(ctx context.Context, filter reqlog.FindRequestsFilter) error
|
||||||
SetSenderRequestFindFilter(ctx context.Context, filter sender.FindRequestsFilter) error
|
SetSenderRequestFindFilter(ctx context.Context, filter sender.FindRequestsFilter) error
|
||||||
OnProjectOpen(fn OnProjectOpenFn)
|
|
||||||
OnProjectClose(fn OnProjectCloseFn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type service struct {
|
type service struct {
|
||||||
@ -49,8 +41,6 @@ type service struct {
|
|||||||
senderSvc sender.Service
|
senderSvc sender.Service
|
||||||
scope *scope.Scope
|
scope *scope.Scope
|
||||||
activeProjectID ulid.ULID
|
activeProjectID ulid.ULID
|
||||||
onProjectOpenFns []OnProjectOpenFn
|
|
||||||
onProjectCloseFns []OnProjectCloseFn
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,8 +116,6 @@ func (svc *service) CloseProject() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
closedProjectID := svc.activeProjectID
|
|
||||||
|
|
||||||
svc.activeProjectID = ulid.ULID{}
|
svc.activeProjectID = ulid.ULID{}
|
||||||
svc.reqLogSvc.SetActiveProjectID(ulid.ULID{})
|
svc.reqLogSvc.SetActiveProjectID(ulid.ULID{})
|
||||||
svc.reqLogSvc.SetBypassOutOfScopeRequests(false)
|
svc.reqLogSvc.SetBypassOutOfScopeRequests(false)
|
||||||
@ -136,8 +124,6 @@ func (svc *service) CloseProject() error {
|
|||||||
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
|
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
|
||||||
svc.scope.SetRules(nil)
|
svc.scope.SetRules(nil)
|
||||||
|
|
||||||
svc.emitProjectClosed(closedProjectID)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,8 +169,6 @@ func (svc *service) OpenProject(ctx context.Context, projectID ulid.ULID) (Proje
|
|||||||
|
|
||||||
svc.scope.SetRules(project.Settings.ScopeRules)
|
svc.scope.SetRules(project.Settings.ScopeRules)
|
||||||
|
|
||||||
svc.emitProjectOpened()
|
|
||||||
|
|
||||||
return project, nil
|
return project, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,36 +201,6 @@ func (svc *service) Scope() *scope.Scope {
|
|||||||
return svc.scope
|
return svc.scope
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *service) OnProjectOpen(fn OnProjectOpenFn) {
|
|
||||||
svc.mu.Lock()
|
|
||||||
defer svc.mu.Unlock()
|
|
||||||
|
|
||||||
svc.onProjectOpenFns = append(svc.onProjectOpenFns, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *service) OnProjectClose(fn OnProjectCloseFn) {
|
|
||||||
svc.mu.Lock()
|
|
||||||
defer svc.mu.Unlock()
|
|
||||||
|
|
||||||
svc.onProjectCloseFns = append(svc.onProjectCloseFns, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *service) emitProjectOpened() {
|
|
||||||
for _, fn := range svc.onProjectOpenFns {
|
|
||||||
if err := fn(svc.activeProjectID); err != nil {
|
|
||||||
log.Printf("[ERROR] Could not execute onProjectOpen function: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *service) emitProjectClosed(projectID ulid.ULID) {
|
|
||||||
for _, fn := range svc.onProjectCloseFns {
|
|
||||||
if err := fn(projectID); err != nil {
|
|
||||||
log.Printf("[ERROR] Could not execute onProjectClose function: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *service) SetScopeRules(ctx context.Context, rules []scope.Rule) error {
|
func (svc *service) SetScopeRules(ctx context.Context, rules []scope.Rule) error {
|
||||||
project, err := svc.ActiveProject(ctx)
|
project, err := svc.ActiveProject(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -88,7 +88,7 @@ func LoadOrCreateCA(caKeyFile, caCertFile string) (*x509.Certificate, *rsa.Priva
|
|||||||
keyDir, _ := filepath.Split(caKeyFile)
|
keyDir, _ := filepath.Split(caKeyFile)
|
||||||
if keyDir != "" {
|
if keyDir != "" {
|
||||||
if _, err := os.Stat(keyDir); os.IsNotExist(err) {
|
if _, err := os.Stat(keyDir); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(keyDir, 0755); err != nil {
|
if err := os.MkdirAll(keyDir, 0o755); err != nil {
|
||||||
return nil, nil, fmt.Errorf("proxy: could not create directory for CA key: %w", err)
|
return nil, nil, fmt.Errorf("proxy: could not create directory for CA key: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,7 +97,7 @@ func LoadOrCreateCA(caKeyFile, caCertFile string) (*x509.Certificate, *rsa.Priva
|
|||||||
keyDir, _ = filepath.Split(caCertFile)
|
keyDir, _ = filepath.Split(caCertFile)
|
||||||
if keyDir != "" {
|
if keyDir != "" {
|
||||||
if _, err := os.Stat("keyDir"); os.IsNotExist(err) {
|
if _, err := os.Stat("keyDir"); os.IsNotExist(err) {
|
||||||
if err := os.MkdirAll(keyDir, 0755); err != nil {
|
if err := os.MkdirAll(keyDir, 0o755); err != nil {
|
||||||
return nil, nil, fmt.Errorf("proxy: could not create directory for CA cert: %w", err)
|
return nil, nil, fmt.Errorf("proxy: could not create directory for CA cert: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,7 +115,7 @@ func LoadOrCreateCA(caKeyFile, caCertFile string) (*x509.Certificate, *rsa.Priva
|
|||||||
return nil, nil, fmt.Errorf("proxy: could not open cert file for writing: %w", err)
|
return nil, nil, fmt.Errorf("proxy: could not open cert file for writing: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
keyOut, err := os.OpenFile(caKeyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
keyOut, err := os.OpenFile(caKeyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("proxy: could not open key file for writing: %w", err)
|
return nil, nil, fmt.Errorf("proxy: could not open key file for writing: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,11 @@ import (
|
|||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
|
|
||||||
|
"github.com/dstotijn/hetty/pkg/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey int
|
type contextKey int
|
||||||
@ -22,15 +23,22 @@ const ReqLogIDKey contextKey = 0
|
|||||||
type Proxy struct {
|
type Proxy struct {
|
||||||
certConfig *CertConfig
|
certConfig *CertConfig
|
||||||
handler http.Handler
|
handler http.Handler
|
||||||
|
logger log.Logger
|
||||||
|
|
||||||
// TODO: Add mutex for modifier funcs.
|
// TODO: Add mutex for modifier funcs.
|
||||||
reqModifiers []RequestModifyMiddleware
|
reqModifiers []RequestModifyMiddleware
|
||||||
resModifiers []ResponseModifyMiddleware
|
resModifiers []ResponseModifyMiddleware
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
CACert *x509.Certificate
|
||||||
|
CAKey crypto.PrivateKey
|
||||||
|
Logger log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
// NewProxy returns a new Proxy.
|
// NewProxy returns a new Proxy.
|
||||||
func NewProxy(ca *x509.Certificate, key crypto.PrivateKey) (*Proxy, error) {
|
func NewProxy(cfg Config) (*Proxy, error) {
|
||||||
certConfig, err := NewCertConfig(ca, key)
|
certConfig, err := NewCertConfig(cfg.CACert, cfg.CAKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -39,12 +47,17 @@ func NewProxy(ca *x509.Certificate, key crypto.PrivateKey) (*Proxy, error) {
|
|||||||
certConfig: certConfig,
|
certConfig: certConfig,
|
||||||
reqModifiers: make([]RequestModifyMiddleware, 0),
|
reqModifiers: make([]RequestModifyMiddleware, 0),
|
||||||
resModifiers: make([]ResponseModifyMiddleware, 0),
|
resModifiers: make([]ResponseModifyMiddleware, 0),
|
||||||
|
logger: cfg.Logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.logger == nil {
|
||||||
|
p.logger = log.NewNopLogger()
|
||||||
}
|
}
|
||||||
|
|
||||||
p.handler = &httputil.ReverseProxy{
|
p.handler = &httputil.ReverseProxy{
|
||||||
Director: p.modifyRequest,
|
Director: p.modifyRequest,
|
||||||
ModifyResponse: p.modifyResponse,
|
ModifyResponse: p.modifyResponse,
|
||||||
ErrorHandler: errorHandler,
|
ErrorHandler: p.errorHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
return p, nil
|
return p, nil
|
||||||
@ -103,7 +116,8 @@ func (p *Proxy) modifyResponse(res *http.Response) error {
|
|||||||
func (p *Proxy) handleConnect(w http.ResponseWriter) {
|
func (p *Proxy) handleConnect(w http.ResponseWriter) {
|
||||||
hj, ok := w.(http.Hijacker)
|
hj, ok := w.(http.Hijacker)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("[ERROR] handleConnect: ResponseWriter is not a http.Hijacker (type: %T)", w)
|
p.logger.Errorw("ResponseWriter is not a http.Hijacker.",
|
||||||
|
"type", fmt.Sprintf("%T", w))
|
||||||
writeError(w, http.StatusServiceUnavailable)
|
writeError(w, http.StatusServiceUnavailable)
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -113,7 +127,8 @@ func (p *Proxy) handleConnect(w http.ResponseWriter) {
|
|||||||
|
|
||||||
clientConn, _, err := hj.Hijack()
|
clientConn, _, err := hj.Hijack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Hijacking client connection failed: %v", err)
|
p.logger.Errorw("Hijacking client connection failed.",
|
||||||
|
"error", err)
|
||||||
writeError(w, http.StatusServiceUnavailable)
|
writeError(w, http.StatusServiceUnavailable)
|
||||||
|
|
||||||
return
|
return
|
||||||
@ -121,18 +136,22 @@ func (p *Proxy) handleConnect(w http.ResponseWriter) {
|
|||||||
defer clientConn.Close()
|
defer clientConn.Close()
|
||||||
|
|
||||||
// Secure connection to client.
|
// Secure connection to client.
|
||||||
clientConn, err = p.clientTLSConn(clientConn)
|
tlsConn, err := p.clientTLSConn(clientConn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Securing client connection failed: %v", err)
|
p.logger.Errorw("Securing client connection failed.",
|
||||||
|
"error", err,
|
||||||
|
"remoteAddr", clientConn.RemoteAddr().String())
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clientConnNotify := ConnNotify{clientConn, make(chan struct{})}
|
clientConnNotify := ConnNotify{tlsConn, make(chan struct{})}
|
||||||
l := &OnceAcceptListener{clientConnNotify.Conn}
|
l := &OnceAcceptListener{clientConnNotify.Conn}
|
||||||
|
|
||||||
err = http.Serve(l, p)
|
err = http.Serve(l, p)
|
||||||
if err != nil && !errors.Is(err, ErrAlreadyAccepted) {
|
if err != nil && !errors.Is(err, ErrAlreadyAccepted) {
|
||||||
log.Printf("[ERROR] Serving HTTP request failed: %v", err)
|
p.logger.Errorw("Serving HTTP request failed.",
|
||||||
|
"error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
<-clientConnNotify.closed
|
<-clientConnNotify.closed
|
||||||
@ -150,12 +169,13 @@ func (p *Proxy) clientTLSConn(conn net.Conn) (*tls.Conn, error) {
|
|||||||
return tlsConn, nil
|
return tlsConn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorHandler(w http.ResponseWriter, r *http.Request, err error) {
|
func (p *Proxy) errorHandler(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[ERROR]: Proxy error: %v", err)
|
p.logger.Errorw("Failed to proxy request.",
|
||||||
|
"error", err)
|
||||||
|
|
||||||
w.WriteHeader(http.StatusBadGateway)
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -16,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/oklog/ulid"
|
"github.com/oklog/ulid"
|
||||||
|
|
||||||
|
"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"
|
"github.com/dstotijn/hetty/pkg/search"
|
||||||
@ -74,6 +74,7 @@ type service struct {
|
|||||||
activeProjectID ulid.ULID
|
activeProjectID ulid.ULID
|
||||||
scope *scope.Scope
|
scope *scope.Scope
|
||||||
repo Repository
|
repo Repository
|
||||||
|
logger log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindRequestsFilter struct {
|
type FindRequestsFilter struct {
|
||||||
@ -85,13 +86,21 @@ type FindRequestsFilter struct {
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Scope *scope.Scope
|
Scope *scope.Scope
|
||||||
Repository Repository
|
Repository Repository
|
||||||
|
Logger log.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(cfg Config) Service {
|
func NewService(cfg Config) Service {
|
||||||
return &service{
|
s := &service{
|
||||||
repo: cfg.Repository,
|
repo: cfg.Repository,
|
||||||
scope: cfg.Scope,
|
scope: cfg.Scope,
|
||||||
|
logger: cfg.Logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.logger == nil {
|
||||||
|
s.logger = log.NewNopLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *service) FindRequests(ctx context.Context) ([]RequestLog, error) {
|
func (svc *service) FindRequests(ctx context.Context) ([]RequestLog, error) {
|
||||||
@ -129,7 +138,8 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
|
|||||||
|
|
||||||
body, err = ioutil.ReadAll(req.Body)
|
body, err = ioutil.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Could not read request body for logging: %v", err)
|
svc.logger.Errorw("Failed to read request body for logging.",
|
||||||
|
"error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,6 +152,9 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
|
|||||||
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
|
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
|
||||||
*req = *req.WithContext(ctx)
|
*req = *req.WithContext(ctx)
|
||||||
|
|
||||||
|
svc.logger.Debugw("Bypassed logging: no active project.",
|
||||||
|
"url", req.URL.String())
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +164,9 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
|
|||||||
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
|
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
|
||||||
*req = *req.WithContext(ctx)
|
*req = *req.WithContext(ctx)
|
||||||
|
|
||||||
|
svc.logger.Debugw("Bypassed logging: request doesn't match any scope rules.",
|
||||||
|
"url", req.URL.String())
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,10 +182,15 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
|
|||||||
|
|
||||||
err := svc.repo.StoreRequestLog(req.Context(), reqLog)
|
err := svc.repo.StoreRequestLog(req.Context(), reqLog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] Could not store request log: %v", err)
|
svc.logger.Errorw("Failed to store request log.",
|
||||||
|
"error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
svc.logger.Debugw("Stored request log.",
|
||||||
|
"reqLogID", reqLog.ID.String(),
|
||||||
|
"url", reqLog.URL.String())
|
||||||
|
|
||||||
ctx := context.WithValue(req.Context(), proxy.ReqLogIDKey, reqLog.ID)
|
ctx := context.WithValue(req.Context(), proxy.ReqLogIDKey, reqLog.ID)
|
||||||
*req = *req.WithContext(ctx)
|
*req = *req.WithContext(ctx)
|
||||||
}
|
}
|
||||||
@ -203,7 +224,11 @@ 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, &clone); err != nil {
|
||||||
log.Printf("[ERROR] Could not store response log: %v", err)
|
svc.logger.Errorw("Failed to store response log.",
|
||||||
|
"error", err)
|
||||||
|
} else {
|
||||||
|
svc.logger.Debugw("Stored response log.",
|
||||||
|
"reqLogID", reqLogID.String())
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -245,6 +270,7 @@ func ParseHTTPResponse(res *http.Response) (ResponseLog, error) {
|
|||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
//nolint:gosec
|
||||||
if _, err := io.Copy(buf, gzipReader); err != nil {
|
if _, err := io.Copy(buf, gzipReader); err != nil {
|
||||||
return ResponseLog{}, fmt.Errorf("reqlog: could not read gzipped response body: %w", err)
|
return ResponseLog{}, fmt.Errorf("reqlog: could not read gzipped response body: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,8 @@ func TestResponseModifier(t *testing.T) {
|
|||||||
t.Run("called repository with request log id", func(t *testing.T) {
|
t.Run("called repository with request log id", func(t *testing.T) {
|
||||||
got := repoMock.StoreResponseLogCalls()[0].ReqLogID
|
got := repoMock.StoreResponseLogCalls()[0].ReqLogID
|
||||||
if exp := reqLogID; exp.Compare(got) != 0 {
|
if exp := reqLogID; exp.Compare(got) != 0 {
|
||||||
t.Fatalf("incorrect `reqLogID` argument for `Repository.AddResponseLogCalls` (expected: %v, got: %v)", exp.String(), got.String())
|
t.Fatalf("incorrect `reqLogID` argument for `Repository.AddResponseLogCalls` (expected: %v, got: %v)",
|
||||||
|
exp.String(), got.String())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -126,7 +126,7 @@ func (svc *service) CreateOrUpdateRequest(ctx context.Context, req Request) (Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Proto == "" {
|
if req.Proto == "" {
|
||||||
req.Proto = HTTPProto2
|
req.Proto = HTTPProto20
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isValidProto(req.Proto) {
|
if !isValidProto(req.Proto) {
|
||||||
@ -157,7 +157,7 @@ func (svc *service) CloneFromRequestLog(ctx context.Context, reqLogID ulid.ULID)
|
|||||||
SourceRequestLogID: reqLogID,
|
SourceRequestLogID: reqLogID,
|
||||||
Method: reqLog.Method,
|
Method: reqLog.Method,
|
||||||
URL: reqLog.URL,
|
URL: reqLog.URL,
|
||||||
Proto: HTTPProto2, // Attempt HTTP/2.
|
Proto: HTTPProto20, // Attempt HTTP/2.
|
||||||
Header: reqLog.Header,
|
Header: reqLog.Header,
|
||||||
Body: reqLog.Body,
|
Body: reqLog.Body,
|
||||||
}
|
}
|
||||||
|
@ -165,7 +165,7 @@ func TestCloneFromRequestLog(t *testing.T) {
|
|||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
URL: exampleURL,
|
URL: exampleURL,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Proto: sender.HTTPProto2,
|
Proto: sender.HTTPProto20,
|
||||||
Header: http.Header{
|
Header: http.Header{
|
||||||
"X-Foo": []string{"bar"},
|
"X-Foo": []string{"bar"},
|
||||||
},
|
},
|
||||||
|
@ -12,8 +12,9 @@ type HTTPTransport struct{}
|
|||||||
type protoCtxKey struct{}
|
type protoCtxKey struct{}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
HTTPProto1 = "HTTP/1.1"
|
HTTPProto10 = "HTTP/1.0"
|
||||||
HTTPProto2 = "HTTP/2.0"
|
HTTPProto11 = "HTTP/1.1"
|
||||||
|
HTTPProto20 = "HTTP/2.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
// h1OnlyTransport mimics `http.DefaultTransport`, but with HTTP/2 disabled.
|
// h1OnlyTransport mimics `http.DefaultTransport`, but with HTTP/2 disabled.
|
||||||
@ -38,7 +39,7 @@ var h1OnlyTransport = &http.Transport{
|
|||||||
func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
proto, ok := req.Context().Value(protoCtxKey{}).(string)
|
proto, ok := req.Context().Value(protoCtxKey{}).(string)
|
||||||
|
|
||||||
if ok && proto == HTTPProto1 {
|
if ok && proto == HTTPProto10 || proto == HTTPProto11 {
|
||||||
return h1OnlyTransport.RoundTrip(req)
|
return h1OnlyTransport.RoundTrip(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,5 +47,5 @@ func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isValidProto(proto string) bool {
|
func isValidProto(proto string) bool {
|
||||||
return proto == HTTPProto1 || proto == HTTPProto2
|
return proto == HTTPProto10 || proto == HTTPProto11 || proto == HTTPProto20
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user