Compare commits

...

15 Commits

35 changed files with 1104 additions and 465 deletions

View File

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

View File

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

@ -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>
[![Latest GitHub release](https://img.shields.io/github/v/release/dstotijn/hetty?color=18BA91&style=flat-square)](https://github.com/dstotijn/hetty/releases/latest) [![Latest GitHub release](https://img.shields.io/github/v/release/dstotijn/hetty?color=25ae8f)](https://github.com/dstotijn/hetty/releases/latest)
[![Build Status](https://img.shields.io/endpoint.svg?url=https://actions-badge.atrox.dev/dstotijn/hetty/badge&style=flat-square&label=build+%26+test&logo=none&color=18BA91)](https://github.com/dstotijn/hetty/actions/workflows/build-test.yml) [![Build Status](https://img.shields.io/endpoint.svg?url=https://actions-badge.atrox.dev/dstotijn/hetty/badge&label=build&logo=none&color=25ae8f)](https://github.com/dstotijn/hetty/actions/workflows/build-test.yml)
![GitHub download count](https://img.shields.io/github/downloads/dstotijn/hetty/total?color=18BA91&style=flat-square) ![GitHub download count](https://img.shields.io/github/downloads/dstotijn/hetty/total?color=25ae8f)
[![GitHub](https://img.shields.io/github/license/dstotijn/hetty?color=18BA91&style=flat-square)](https://github.com/dstotijn/hetty/blob/master/LICENSE) [![GitHub](https://img.shields.io/github/license/dstotijn/hetty?color=25ae8f)](https://github.com/dstotijn/hetty/blob/master/LICENSE)
[![Documentation](https://img.shields.io/badge/hetty-docs-18BA91?style=flat-square)](https://hetty.xyz/) [![Documentation](https://img.shields.io/badge/hetty-docs-25ae8f)](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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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