Compare commits

..

1 Commits

Author SHA1 Message Date
b41fe29850 Add "Copy to Sender" table action 2022-02-26 08:51:36 +01:00
35 changed files with 495 additions and 1115 deletions

View File

@ -12,7 +12,6 @@ linters:
- test
- unused
disable:
- dupl
- exhaustive
- exhaustivestruct
- gochecknoglobals
@ -22,11 +21,9 @@ linters:
- gomnd
- interfacer
- maligned
- nilnil
- nlreturn
- scopelint
- testpackage
- varnamelen
- wrapcheck
linters-settings:
@ -34,8 +31,6 @@ linters-settings:
local-prefixes: github.com/dstotijn/hetty
godot:
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:
exclude-rules:

View File

@ -28,20 +28,6 @@ archives:
- goos: windows
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:
name_template: "checksums.txt"

263
README.md
View File

@ -1,148 +1,243 @@
<h1>
<img src="https://hetty.xyz/img/hetty_light.svg#gh-light-mode-only" width="240"/>
<img src="https://hetty.xyz/img/hetty_dark.svg#gh-dark-mode-only" width="240"/>
<a href="https://github.com/dstotijn/hetty">
<img src="https://hetty.xyz/assets/logo.png" width="293">
</a>
</h1>
[![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&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=25ae8f)
[![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-25ae8f)](https://hetty.xyz/)
[![Latest GitHub release](https://img.shields.io/github/v/release/dstotijn/hetty?color=18BA91&style=flat-square)](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)
![GitHub download count](https://img.shields.io/github/downloads/dstotijn/hetty/total?color=18BA91&style=flat-square)
[![GitHub](https://img.shields.io/github/license/dstotijn/hetty?color=18BA91&style=flat-square)](https://github.com/dstotijn/hetty/blob/master/LICENSE)
[![Documentation](https://img.shields.io/badge/hetty-docs-18BA91?style=flat-square)](https://hetty.xyz/)
**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
features tailored to the needs of the infosec and bug bounty community.
<img src="https://hetty.xyz/img/hero.png" width="907" alt="Hetty proxy logs (screenshot)" />
<img src="https://hetty.xyz/assets/hetty_v0.2.0_header.png">
## Features
- Machine-in-the-middle (MITM) HTTP proxy, with logs and advanced search
- HTTP client for manually creating/editing requests, and replay proxied requests
- Scope support, to help keep work organized
- Easy-to-use web based admin interface
- Project based database storage, to help keep work organized
- Man-in-the-middle (MITM) HTTP/1.1 proxy with logs
- Project based database storage (BadgerDB)
- Scope support
- Headless management API using GraphQL
- Embedded web interface (Next.js)
👷‍♂ Hetty is under active development. Check the <a
href="https://github.com/dstotijn/hetty/projects/1">backlog</a> for the current
status.
Hetty is in early development. Additional features are planned
for a `v1.0` release. Please see the <a href="https://github.com/dstotijn/hetty/projects/1">backlog</a>
for details.
📣 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!
## Documentation
## Getting started
📖 [Read the docs.](https://hetty.xyz/)
💡 The [Getting started](https://hetty.xyz/docs/getting-started) doc has more
detailed install and usage instructions.
## Installation
### Installation
Hetty compiles to a self-contained binary, with an embedded BadgerDB database
and web based admin interface.
The quickest way to install and update Hetty is via a package manager:
### Install pre-built release (recommended)
#### macOS
👉 Downloads for Linux, macOS and Windows are available on the [releases page](https://github.com/dstotijn/hetty/releases).
```sh
brew install hettysoft/tap/hetty
### Build from source
#### 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
```
#### Linux
### Docker
```sh
sudo snap install hetty
A Docker image is available on Docker Hub: [`dstotijn/hetty`](https://hub.docker.com/r/dstotijn/hetty).
For persistent storage of CA certificates and projects database, mount a volume:
```
$ mkdir -p $HOME/.hetty
$ docker run -v $HOME/.hetty:/root/.hetty -p 8080:8080 dstotijn/hetty
```
#### Windows
## Usage
```sh
scoop bucket add hettysoft https://github.com/hettysoft/scoop.git
scoop install hettysoft/hetty
When Hetty is run, by default it listens on `:8080` and is accessible via
http://localhost:8080. Depending on incoming HTTP requests, it either acts as a
MITM proxy, or it serves the API and web interface.
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
```
#### Other
An overview of configuration flags:
Alternatively, you can [download the latest release from
GitHub](https://github.com/dstotijn/hetty/releases/latest) for your OS and
architecture, and move the binary to a directory in your `$PATH`. If your OS is
not available for one of the package managers or not listed in the GitHub
releases, you can compile from source _(link coming soon)_ or use a Docker image
_(link coming soon)_.
```
$ hetty -h
Usage of ./hetty:
-addr string
TCP address to listen on, in the form "host:port" (default ":8080")
-adminPath string
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")
```
### Usage
You should see:
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
hetty
```
💡 Read the [Getting started](https://hetty.xyz/docs/getting-started) doc for
more details.
You should now have a key and certificate located at `~/.hetty/hetty_key.pem` and
`~/.hetty/hetty_cert.pem` respectively.
To list all available options, run: `hetty --help`:
#### Generating CA certificates with OpenSSL
```
$ hetty --help
You can start off by generating a new key and CA certificate which will both expire
after a month.
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.
```sh
mkdir ~/.hetty
openssl req -newkey rsa:2048 -new -nodes -x509 -days 31 -keyout ~/.hetty/hetty_key.pem -out ~/.hetty/hetty_cert.pem
```
## Documentation
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`.
📖 [Read the docs](https://hetty.xyz/docs)
```
hetty -key key.pem -cert cert.pem
```
### Trusting the CA certificate
In order for your browser to allow traffic to the local Hetty proxy, you may need
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
Use [issues](https://github.com/dstotijn/hetty/issues) for bug reports and
feature requests, and
[discussions](https://github.com/dstotijn/hetty/discussions) for questions and
troubleshooting.
feature requests, and [discussions](https://github.com/dstotijn/hetty/discussions)
for questions and troubleshooting.
## Community
💬 [Join the Hetty Discord server](https://discord.gg/3HVsj5pTFP)
💬 [Join the Hetty Discord server](https://discord.gg/3HVsj5pTFP).
## Contributing
Want to contribute? Great! Please check the [Contribution
Guidelines](CONTRIBUTING.md) for details.
Want to contribute? Great! Please check the [Contribution Guidelines](CONTRIBUTING.md)
for details.
## Acknowledgements
- Thanks to the [Hacker101 community on Discord](https://www.hacker101.com/discord)
for the encouragement and early feedback.
- The font used in the logo and admin interface is [JetBrains
Mono](https://www.jetbrains.com/lp/mono/).
for all the encouragement and feedback.
- The font used in the logo and admin interface is [JetBrains Mono](https://www.jetbrains.com/lp/mono/).
## Sponsors
<a href="https://www.tines.com/?utm_source=oss&utm_medium=sponsorship&utm_campaign=hetty">
<img src="https://hetty.xyz/img/tines-sponsorship-badge.png" width="140" alt="Sponsored by Tines">
<img src="https://hetty.xyz/assets/tines-sponsorship-badge.png" width="140" alt="Sponsored by Tines">
</a>
## License
[MIT](LICENSE)
[MIT License](LICENSE)
© 2022 Hetty Software
---
© 2021 David Stotijn — [Twitter](https://twitter.com/dstotijn), [Email](mailto:dstotijn@gmail.com)

View File

@ -1,16 +1,5 @@
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import {
Alert,
Box,
IconButton,
Link,
MenuItem,
Snackbar,
styled,
TableCell,
TableCellProps,
Tooltip,
} from "@mui/material";
import { ContentCopy } from "@mui/icons-material";
import { Alert, Box, IconButton, Link, MenuItem, Snackbar, Tooltip } from "@mui/material";
import { useRouter } from "next/router";
import { useState } from "react";
@ -22,11 +11,6 @@ import SplitPane from "lib/components/SplitPane";
import useContextMenu from "lib/components/useContextMenu";
import { useCreateSenderRequestFromHttpRequestLogMutation, useHttpRequestLogsQuery } from "lib/graphql/generated";
const ActionsTableCell = styled(TableCell)<TableCellProps>(() => ({
paddingTop: 0,
paddingBottom: 0,
}));
export function RequestLogs(): JSX.Element {
const router = useRouter();
const id = router.query.id as string | undefined;
@ -72,24 +56,21 @@ export function RequestLogs(): JSX.Element {
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>
const handleCopyToSenderActionClick = (id: string) => {
setCopyToSenderId(id);
createSenderReqFromLog({
variables: {
id,
},
});
};
const rowActions = (id: string): JSX.Element => (
<Tooltip title="Copy to Sender">
<IconButton size="small" onClick={() => handleCopyToSenderActionClick(id)}>
<ContentCopy fontSize="inherit" />
</IconButton>
</Tooltip>
);
return (
@ -115,9 +96,9 @@ export function RequestLogs(): JSX.Element {
<RequestsTable
requests={data?.httpRequestLogs || []}
activeRowId={id}
actionsCell={actionsCell}
onRowClick={handleRowClick}
onContextMenu={handleRowContextClick}
rowActions={rowActions}
/>
</Box>
</Box>

View File

@ -39,15 +39,13 @@ enum HttpMethod {
}
enum HttpProto {
Http10 = "HTTP/1.0",
Http11 = "HTTP/1.1",
Http20 = "HTTP/2.0",
Http1 = "HTTP/1.1",
Http2 = "HTTP/2.0",
}
const httpProtoMap = new Map([
[HttpProto.Http10, HttpProtocol.Http10],
[HttpProto.Http11, HttpProtocol.Http11],
[HttpProto.Http20, HttpProtocol.Http20],
[HttpProto.Http1, HttpProtocol.Http1],
[HttpProto.Http2, HttpProtocol.Http2],
]);
function updateKeyPairItem(key: string, value: string, idx: number, items: KeyValuePair[]): KeyValuePair[] {
@ -94,7 +92,7 @@ function EditRequest(): JSX.Element {
const [method, setMethod] = useState(HttpMethod.Get);
const [url, setURL] = useState("");
const [proto, setProto] = useState(HttpProto.Http20);
const [proto, setProto] = useState(HttpProto.Http2);
const [queryParams, setQueryParams] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
const [headers, setHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
const [body, setBody] = useState("");

View File

@ -9,6 +9,7 @@ import {
Table,
TableBody,
TableCell,
TableCellProps,
TableContainer,
TableHead,
TableRow,
@ -73,6 +74,19 @@ export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTab
setCopyConfOpen(false);
};
const baseCellStyle = {
"&:hover": {
cursor: "copy",
},
};
const KeyTableCell = styled(TableCell)<TableCellProps>(() => (!onChange ? baseCellStyle : {}));
const ValueTableCell = styled(TableCell)<TableCellProps>(() => ({
...(!onChange && baseCellStyle),
width: "60%",
wordBreak: "break-all",
}));
return (
<div>
<Snackbar open={copyConfOpen} autoHideDuration={3000} onClose={handleCopyConfClose}>
@ -104,19 +118,12 @@ export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTab
>
{items.map(({ key, value }, idx) => (
<StyledTableRow key={idx} hover>
<TableCell
<KeyTableCell
component="th"
scope="row"
onClick={(e) => {
!onChange && handleCellClick(e);
}}
sx={{
...(!onChange && {
"&:hover": {
cursor: "copy",
},
}),
}}
>
{!onChange && <span>{key}</span>}
{onChange && (
@ -130,20 +137,11 @@ export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTab
}}
/>
)}
</TableCell>
<TableCell
</KeyTableCell>
<ValueTableCell
onClick={(e) => {
!onChange && handleCellClick(e);
}}
sx={{
width: "60%",
wordBreak: "break-all",
...(!onChange && {
"&:hover": {
cursor: "copy",
},
}),
}}
>
{!onChange && value}
{onChange && (
@ -157,7 +155,7 @@ export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTab
}}
/>
)}
</TableCell>
</ValueTableCell>
{onDelete && (
<TableCell>
<div className="delete-button">

View File

@ -62,13 +62,13 @@ interface HttpResponse {
interface Props {
requests: HttpRequest[];
activeRowId?: string;
actionsCell?: (id: string) => JSX.Element;
onRowClick?: (id: string) => void;
onContextMenu?: (e: React.MouseEvent, id: string) => void;
rowActions?: (id: string) => JSX.Element;
}
export default function RequestsTable(props: Props): JSX.Element {
const { requests, activeRowId, actionsCell, onRowClick, onContextMenu } = props;
const { requests, activeRowId, onRowClick, onContextMenu, rowActions } = props;
return (
<TableContainer sx={{ overflowX: "initial" }}>
@ -79,7 +79,7 @@ export default function RequestsTable(props: Props): JSX.Element {
<TableCell>Origin</TableCell>
<TableCell>Path</TableCell>
<TableCell>Status</TableCell>
{actionsCell && <TableCell padding="checkbox"></TableCell>}
{rowActions && <TableCell padding="checkbox" />}
</TableRow>
</TableHead>
<TableBody>
@ -106,7 +106,7 @@ export default function RequestsTable(props: Props): JSX.Element {
<StatusTableCell>
{response && <Status code={response.statusCode} reason={response.statusReason} />}
</StatusTableCell>
{actionsCell && actionsCell(id)}
{rowActions && <TableCell sx={{ py: 0 }}>{rowActions(id)}</TableCell>}
</RequestTableRow>
);
})}

View File

@ -12,11 +12,9 @@ type ResponseStatusProps = {
function mapProto(proto: HttpProtocol): string {
switch (proto) {
case HttpProtocol.Http10:
return "HTTP/1.0";
case HttpProtocol.Http11:
case HttpProtocol.Http1:
return "HTTP/1.1";
case HttpProtocol.Http20:
case HttpProtocol.Http2:
return "HTTP/2.0";
default:
return proto;

View File

@ -62,9 +62,8 @@ export enum HttpMethod {
}
export enum HttpProtocol {
Http10 = 'HTTP10',
Http11 = 'HTTP11',
Http20 = 'HTTP20'
Http1 = 'HTTP1',
Http2 = 'HTTP2'
}
export type HttpRequestLog = {

View File

@ -1,212 +0,0 @@
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
}

View File

@ -1,23 +0,0 @@
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.")
}

View File

@ -1,298 +0,0 @@
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,35 +1,163 @@
package main
import (
"context"
"crypto/tls"
"embed"
"errors"
"flag"
llog "log"
"fmt"
"io/fs"
"log"
"net"
"net/http"
"os"
"strings"
"go.uber.org/zap"
"github.com/99designs/gqlgen/graphql/handler"
"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/log"
"github.com/dstotijn/hetty/pkg/api"
"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() {
hettyCmd, cfg := NewHettyCommand()
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))
if err := run(); err != nil {
log.Fatalf("[ERROR]: %v", 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,31 +4,22 @@ go 1.17
require (
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/google/go-cmp v0.5.6
github.com/gorilla/mux v1.7.4
github.com/matryer/moq v0.2.5
github.com/mitchellh/go-homedir v1.1.0
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
go.uber.org/zap v1.21.0
)
require (
github.com/agnivade/levenshtein v1.1.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.1 // indirect
github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect
@ -37,9 +28,7 @@ require (
github.com/google/flatbuffers v1.12.1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.12.3 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
@ -47,13 +36,10 @@ require (
github.com/urfave/cli/v2 v2.1.1 // indirect
github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e // indirect
go.opencensus.io v0.22.5 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/mod v0.4.2 // 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/mod v0.3.0 // indirect
golang.org/x/net v0.0.0-20201021035429-f5854403a974 // indirect
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/tools v0.0.0-20210106214847-113979e3529a // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
gopkg.in/yaml.v2 v2.2.4 // indirect
)

68
go.sum
View File

@ -12,18 +12,10 @@ 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/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf h1:1omDWNUsWxn2HpiMiMuyRmzjl9uG7RP3IE6GTlpgJWU=
github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf/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/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=
@ -46,12 +38,6 @@ 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/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/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/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
@ -81,9 +67,6 @@ 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/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/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/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU=
@ -95,8 +78,6 @@ 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/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/matryer/moq v0.2.5 h1:BGQISyhl7Gc9W/gMYmAJONh9mT6AYeyeTjNupNPknMs=
github.com/matryer/moq v0.2.5/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE=
@ -112,12 +93,7 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5 h1:1SoBaSPudixRecmlHXb/GxmaD3fLMtHIDN13QujwQuc=
github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
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.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -134,8 +110,6 @@ 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/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/smallstep/truststore v0.11.0 h1:JUTkQ4oHr40jHTS/A2t0usEhteMWG+45CDD2iJA/dIk=
github.com/smallstep/truststore v0.11.0/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
@ -148,10 +122,8 @@ 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/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
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/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
@ -163,17 +135,8 @@ 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.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -182,11 +145,9 @@ 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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/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.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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -195,9 +156,8 @@ 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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
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/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=
@ -206,7 +166,6 @@ 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-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -216,14 +175,8 @@ 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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -235,9 +188,8 @@ 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-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ=
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -250,19 +202,11 @@ 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.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 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.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.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=
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-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=

View File

@ -897,9 +897,8 @@ enum HttpMethod {
}
enum HttpProtocol {
HTTP10
HTTP11
HTTP20
HTTP1
HTTP2
}
scalar Time

View File

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

View File

@ -186,20 +186,18 @@ func (e HTTPMethod) MarshalGQL(w io.Writer) {
type HTTPProtocol string
const (
HTTPProtocolHTTP10 HTTPProtocol = "HTTP10"
HTTPProtocolHTTP11 HTTPProtocol = "HTTP11"
HTTPProtocolHTTP20 HTTPProtocol = "HTTP20"
HTTPProtocolHTTP1 HTTPProtocol = "HTTP1"
HTTPProtocolHTTP2 HTTPProtocol = "HTTP2"
)
var AllHTTPProtocol = []HTTPProtocol{
HTTPProtocolHTTP10,
HTTPProtocolHTTP11,
HTTPProtocolHTTP20,
HTTPProtocolHTTP1,
HTTPProtocolHTTP2,
}
func (e HTTPProtocol) IsValid() bool {
switch e {
case HTTPProtocolHTTP10, HTTPProtocolHTTP11, HTTPProtocolHTTP20:
case HTTPProtocolHTTP1, HTTPProtocolHTTP2:
return true
}
return false

View File

@ -22,15 +22,13 @@ import (
)
var httpProtocolMap = map[string]HTTPProtocol{
sender.HTTPProto10: HTTPProtocolHTTP10,
sender.HTTPProto11: HTTPProtocolHTTP11,
sender.HTTPProto20: HTTPProtocolHTTP20,
sender.HTTPProto1: HTTPProtocolHTTP1,
sender.HTTPProto2: HTTPProtocolHTTP2,
}
var revHTTPProtocolMap = map[HTTPProtocol]string{
HTTPProtocolHTTP10: sender.HTTPProto10,
HTTPProtocolHTTP11: sender.HTTPProto11,
HTTPProtocolHTTP20: sender.HTTPProto20,
HTTPProtocolHTTP1: sender.HTTPProto1,
HTTPProtocolHTTP2: sender.HTTPProto2,
}
type Resolver struct {
@ -408,10 +406,7 @@ func (r *mutationResolver) SetSenderRequestFilter(
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{
URL: input.URL,
Header: make(http.Header),
@ -452,10 +447,7 @@ func (r *mutationResolver) CreateOrUpdateSenderRequest(
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)
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
@ -479,13 +471,10 @@ func (r *mutationResolver) SendRequest(ctx context.Context, id ulid.ULID) (*Send
var sendErr *sender.SendError
//nolint:contextcheck
req, err := r.SenderService.SendRequest(ctx2, id)
switch {
case errors.Is(err, proj.ErrNoProject):
if errors.Is(err, proj.ErrNoProject) {
return nil, noActiveProjectErr(ctx)
case errors.As(err, &sendErr):
} else if errors.As(err, &sendErr) {
return nil, &gqlerror.Error{
Path: graphql.GetPath(ctx),
Message: fmt.Sprintf("Sending request failed: %v", sendErr.Unwrap()),
@ -493,7 +482,7 @@ func (r *mutationResolver) SendRequest(ctx context.Context, id ulid.ULID) (*Send
"code": "send_request_failed",
},
}
case err != nil:
} else if err != nil {
return nil, fmt.Errorf("could not send request: %w", err)
}

View File

@ -157,9 +157,8 @@ enum HttpMethod {
}
enum HttpProtocol {
HTTP10
HTTP11
HTTP20
HTTP1
HTTP2
}
scalar Time

View File

@ -1,34 +0,0 @@
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...)
}

View File

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

View File

@ -14,11 +14,7 @@ import (
"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 {
return nil, reqlog.ErrProjectIDMustBeSet
}
@ -235,7 +231,6 @@ func findRequestLogIDsByProjectID(txn *badger.Txn, projectID ulid.ULID) ([]ulid.
reqLogIDs := make([]ulid.ULID, 0)
opts := badger.DefaultIteratorOptions
opts.PrefetchValues = false
opts.Reverse = true
iterator := txn.NewIterator(opts)
defer iterator.Close()
@ -243,7 +238,7 @@ func findRequestLogIDsByProjectID(txn *badger.Txn, projectID ulid.ULID) ([]ulid.
prefix := entryKey(reqLogPrefix, reqLogProjectIDIndex, projectID[:])
for iterator.Seek(append(prefix, 255)); iterator.ValidForPrefix(prefix); iterator.Next() {
for iterator.Seek(prefix); iterator.ValidForPrefix(prefix); iterator.Next() {
projectIndexKey = iterator.Item().KeyCopy(projectIndexKey)
var id ulid.ULID

View File

@ -46,7 +46,7 @@ func TestFindRequestLogs(t *testing.T) {
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
fixtures := []reqlog.RequestLog{
exp := []reqlog.RequestLog{
{
ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy),
ProjectID: projectID,
@ -80,7 +80,7 @@ func TestFindRequestLogs(t *testing.T) {
}
// Store fixtures.
for _, reqLog := range fixtures {
for _, reqLog := range exp {
err = database.StoreRequestLog(context.Background(), reqLog)
if err != nil {
t.Fatalf("unexpected error creating request log fixture: %v", err)
@ -103,12 +103,6 @@ func TestFindRequestLogs(t *testing.T) {
t.Fatalf("unexpected error finding request logs: %v", err)
}
// We expect the found request logs are *reversed*, e.g. newest first.
exp := make([]reqlog.RequestLog, len(fixtures))
for i, j := 0, len(fixtures)-1; i < j; i, j = i+1, j-1 {
exp[i], exp[j] = fixtures[j], fixtures[i]
}
if diff := cmp.Diff(exp, got); diff != "" {
t.Fatalf("request logs not equal (-exp, +got):\n%v", diff)
}

View File

@ -63,11 +63,7 @@ func (db *Database) FindSenderRequestByID(ctx context.Context, senderReqID ulid.
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 {
return nil, sender.ErrProjectIDMustBeSet
}

View File

@ -60,7 +60,7 @@ func TestFindRequestByID(t *testing.T) {
URL: exampleURL,
Method: http.MethodGet,
Proto: sender.HTTPProto20,
Proto: sender.HTTPProto2,
Header: http.Header{
"X-Foo": []string{"bar"},
},

View File

@ -1,87 +0,0 @@
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,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"log"
"math/rand"
"regexp"
"sync"
@ -20,6 +21,11 @@ import (
//nolint:gosec
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.
type Service interface {
CreateProject(ctx context.Context, name string) (Project, error)
@ -33,15 +39,19 @@ type Service interface {
SetScopeRules(ctx context.Context, rules []scope.Rule) error
SetRequestLogFindFilter(ctx context.Context, filter reqlog.FindRequestsFilter) error
SetSenderRequestFindFilter(ctx context.Context, filter sender.FindRequestsFilter) error
OnProjectOpen(fn OnProjectOpenFn)
OnProjectClose(fn OnProjectCloseFn)
}
type service struct {
repo Repository
reqLogSvc reqlog.Service
senderSvc sender.Service
scope *scope.Scope
activeProjectID ulid.ULID
mu sync.RWMutex
repo Repository
reqLogSvc reqlog.Service
senderSvc sender.Service
scope *scope.Scope
activeProjectID ulid.ULID
onProjectOpenFns []OnProjectOpenFn
onProjectCloseFns []OnProjectCloseFn
mu sync.RWMutex
}
type Project struct {
@ -116,6 +126,8 @@ func (svc *service) CloseProject() error {
return nil
}
closedProjectID := svc.activeProjectID
svc.activeProjectID = ulid.ULID{}
svc.reqLogSvc.SetActiveProjectID(ulid.ULID{})
svc.reqLogSvc.SetBypassOutOfScopeRequests(false)
@ -124,6 +136,8 @@ func (svc *service) CloseProject() error {
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
svc.scope.SetRules(nil)
svc.emitProjectClosed(closedProjectID)
return nil
}
@ -169,6 +183,8 @@ func (svc *service) OpenProject(ctx context.Context, projectID ulid.ULID) (Proje
svc.scope.SetRules(project.Settings.ScopeRules)
svc.emitProjectOpened()
return project, nil
}
@ -201,6 +217,36 @@ func (svc *service) Scope() *scope.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 {
project, err := svc.ActiveProject(ctx)
if err != nil {

View File

@ -88,7 +88,7 @@ func LoadOrCreateCA(caKeyFile, caCertFile string) (*x509.Certificate, *rsa.Priva
keyDir, _ := filepath.Split(caKeyFile)
if keyDir != "" {
if _, err := os.Stat(keyDir); os.IsNotExist(err) {
if err := os.MkdirAll(keyDir, 0o755); err != nil {
if err := os.MkdirAll(keyDir, 0755); err != nil {
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)
if keyDir != "" {
if _, err := os.Stat("keyDir"); os.IsNotExist(err) {
if err := os.MkdirAll(keyDir, 0o755); err != nil {
if err := os.MkdirAll(keyDir, 0755); err != nil {
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)
}
keyOut, err := os.OpenFile(caKeyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
keyOut, err := os.OpenFile(caKeyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return nil, nil, fmt.Errorf("proxy: could not open key file for writing: %w", err)
}

View File

@ -7,11 +7,10 @@ import (
"crypto/x509"
"errors"
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"github.com/dstotijn/hetty/pkg/log"
)
type contextKey int
@ -23,22 +22,15 @@ const ReqLogIDKey contextKey = 0
type Proxy struct {
certConfig *CertConfig
handler http.Handler
logger log.Logger
// TODO: Add mutex for modifier funcs.
reqModifiers []RequestModifyMiddleware
resModifiers []ResponseModifyMiddleware
}
type Config struct {
CACert *x509.Certificate
CAKey crypto.PrivateKey
Logger log.Logger
}
// NewProxy returns a new Proxy.
func NewProxy(cfg Config) (*Proxy, error) {
certConfig, err := NewCertConfig(cfg.CACert, cfg.CAKey)
func NewProxy(ca *x509.Certificate, key crypto.PrivateKey) (*Proxy, error) {
certConfig, err := NewCertConfig(ca, key)
if err != nil {
return nil, err
}
@ -47,17 +39,12 @@ func NewProxy(cfg Config) (*Proxy, error) {
certConfig: certConfig,
reqModifiers: make([]RequestModifyMiddleware, 0),
resModifiers: make([]ResponseModifyMiddleware, 0),
logger: cfg.Logger,
}
if p.logger == nil {
p.logger = log.NewNopLogger()
}
p.handler = &httputil.ReverseProxy{
Director: p.modifyRequest,
ModifyResponse: p.modifyResponse,
ErrorHandler: p.errorHandler,
ErrorHandler: errorHandler,
}
return p, nil
@ -116,8 +103,7 @@ func (p *Proxy) modifyResponse(res *http.Response) error {
func (p *Proxy) handleConnect(w http.ResponseWriter) {
hj, ok := w.(http.Hijacker)
if !ok {
p.logger.Errorw("ResponseWriter is not a http.Hijacker.",
"type", fmt.Sprintf("%T", w))
log.Printf("[ERROR] handleConnect: ResponseWriter is not a http.Hijacker (type: %T)", w)
writeError(w, http.StatusServiceUnavailable)
return
@ -127,8 +113,7 @@ func (p *Proxy) handleConnect(w http.ResponseWriter) {
clientConn, _, err := hj.Hijack()
if err != nil {
p.logger.Errorw("Hijacking client connection failed.",
"error", err)
log.Printf("[ERROR] Hijacking client connection failed: %v", err)
writeError(w, http.StatusServiceUnavailable)
return
@ -136,22 +121,18 @@ func (p *Proxy) handleConnect(w http.ResponseWriter) {
defer clientConn.Close()
// Secure connection to client.
tlsConn, err := p.clientTLSConn(clientConn)
clientConn, err = p.clientTLSConn(clientConn)
if err != nil {
p.logger.Errorw("Securing client connection failed.",
"error", err,
"remoteAddr", clientConn.RemoteAddr().String())
log.Printf("[ERROR] Securing client connection failed: %v", err)
return
}
clientConnNotify := ConnNotify{tlsConn, make(chan struct{})}
clientConnNotify := ConnNotify{clientConn, make(chan struct{})}
l := &OnceAcceptListener{clientConnNotify.Conn}
err = http.Serve(l, p)
if err != nil && !errors.Is(err, ErrAlreadyAccepted) {
p.logger.Errorw("Serving HTTP request failed.",
"error", err)
log.Printf("[ERROR] Serving HTTP request failed: %v", err)
}
<-clientConnNotify.closed
@ -169,13 +150,12 @@ func (p *Proxy) clientTLSConn(conn net.Conn) (*tls.Conn, error) {
return tlsConn, nil
}
func (p *Proxy) errorHandler(w http.ResponseWriter, r *http.Request, err error) {
func errorHandler(w http.ResponseWriter, r *http.Request, err error) {
if errors.Is(err, context.Canceled) {
return
}
p.logger.Errorw("Failed to proxy request.",
"error", err)
log.Printf("[ERROR]: Proxy error: %v", err)
w.WriteHeader(http.StatusBadGateway)
}

View File

@ -8,6 +8,7 @@ import (
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/url"
@ -15,7 +16,6 @@ import (
"github.com/oklog/ulid"
"github.com/dstotijn/hetty/pkg/log"
"github.com/dstotijn/hetty/pkg/proxy"
"github.com/dstotijn/hetty/pkg/scope"
"github.com/dstotijn/hetty/pkg/search"
@ -74,7 +74,6 @@ type service struct {
activeProjectID ulid.ULID
scope *scope.Scope
repo Repository
logger log.Logger
}
type FindRequestsFilter struct {
@ -86,21 +85,13 @@ type FindRequestsFilter struct {
type Config struct {
Scope *scope.Scope
Repository Repository
Logger log.Logger
}
func NewService(cfg Config) Service {
s := &service{
repo: cfg.Repository,
scope: cfg.Scope,
logger: cfg.Logger,
return &service{
repo: cfg.Repository,
scope: cfg.Scope,
}
if s.logger == nil {
s.logger = log.NewNopLogger()
}
return s
}
func (svc *service) FindRequests(ctx context.Context) ([]RequestLog, error) {
@ -138,8 +129,7 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
body, err = ioutil.ReadAll(req.Body)
if err != nil {
svc.logger.Errorw("Failed to read request body for logging.",
"error", err)
log.Printf("[ERROR] Could not read request body for logging: %v", err)
return
}
@ -152,9 +142,6 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
*req = *req.WithContext(ctx)
svc.logger.Debugw("Bypassed logging: no active project.",
"url", req.URL.String())
return
}
@ -164,9 +151,6 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
ctx := context.WithValue(req.Context(), LogBypassedKey, true)
*req = *req.WithContext(ctx)
svc.logger.Debugw("Bypassed logging: request doesn't match any scope rules.",
"url", req.URL.String())
return
}
@ -182,15 +166,10 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
err := svc.repo.StoreRequestLog(req.Context(), reqLog)
if err != nil {
svc.logger.Errorw("Failed to store request log.",
"error", err)
log.Printf("[ERROR] Could not store request log: %v", err)
return
}
svc.logger.Debugw("Stored request log.",
"reqLogID", reqLog.ID.String(),
"url", reqLog.URL.String())
ctx := context.WithValue(req.Context(), proxy.ReqLogIDKey, reqLog.ID)
*req = *req.WithContext(ctx)
}
@ -224,11 +203,7 @@ func (svc *service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
go func() {
if err := svc.storeResponse(context.Background(), reqLogID, &clone); err != nil {
svc.logger.Errorw("Failed to store response log.",
"error", err)
} else {
svc.logger.Debugw("Stored response log.",
"reqLogID", reqLogID.String())
log.Printf("[ERROR] Could not store response log: %v", err)
}
}()
@ -270,7 +245,6 @@ func ParseHTTPResponse(res *http.Response) (ResponseLog, error) {
buf := &bytes.Buffer{}
//nolint:gosec
if _, err := io.Copy(buf, gzipReader); err != nil {
return ResponseLog{}, fmt.Errorf("reqlog: could not read gzipped response body: %w", err)
}

View File

@ -117,8 +117,7 @@ func TestResponseModifier(t *testing.T) {
t.Run("called repository with request log id", func(t *testing.T) {
got := repoMock.StoreResponseLogCalls()[0].ReqLogID
if exp := reqLogID; exp.Compare(got) != 0 {
t.Fatalf("incorrect `reqLogID` argument for `Repository.AddResponseLogCalls` (expected: %v, got: %v)",
exp.String(), got.String())
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 == "" {
req.Proto = HTTPProto20
req.Proto = HTTPProto2
}
if !isValidProto(req.Proto) {
@ -157,7 +157,7 @@ func (svc *service) CloneFromRequestLog(ctx context.Context, reqLogID ulid.ULID)
SourceRequestLogID: reqLogID,
Method: reqLog.Method,
URL: reqLog.URL,
Proto: HTTPProto20, // Attempt HTTP/2.
Proto: HTTPProto2, // Attempt HTTP/2.
Header: reqLog.Header,
Body: reqLog.Body,
}

View File

@ -165,7 +165,7 @@ func TestCloneFromRequestLog(t *testing.T) {
ProjectID: projectID,
URL: exampleURL,
Method: http.MethodPost,
Proto: sender.HTTPProto20,
Proto: sender.HTTPProto2,
Header: http.Header{
"X-Foo": []string{"bar"},
},

View File

@ -12,9 +12,8 @@ type HTTPTransport struct{}
type protoCtxKey struct{}
const (
HTTPProto10 = "HTTP/1.0"
HTTPProto11 = "HTTP/1.1"
HTTPProto20 = "HTTP/2.0"
HTTPProto1 = "HTTP/1.1"
HTTPProto2 = "HTTP/2.0"
)
// h1OnlyTransport mimics `http.DefaultTransport`, but with HTTP/2 disabled.
@ -39,7 +38,7 @@ var h1OnlyTransport = &http.Transport{
func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
proto, ok := req.Context().Value(protoCtxKey{}).(string)
if ok && proto == HTTPProto10 || proto == HTTPProto11 {
if ok && proto == HTTPProto1 {
return h1OnlyTransport.RoundTrip(req)
}
@ -47,5 +46,5 @@ func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
}
func isValidProto(proto string) bool {
return proto == HTTPProto10 || proto == HTTPProto11 || proto == HTTPProto20
return proto == HTTPProto1 || proto == HTTPProto2
}