mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Compare commits
15 Commits
Author | SHA1 | Date | |
---|---|---|---|
29550ff43b | |||
7afc23b3ff | |||
6aa93b782e | |||
ed9a539ce3 | |||
857aa0c49e | |||
af26987601 | |||
ad26478043 | |||
ca0c085021 | |||
d438f93ee0 | |||
fa3f24eb70 | |||
f15438e10b | |||
bef52d956e | |||
8269af9478 | |||
c5f76e1f9a | |||
2ddf2a77e8 |
@ -12,6 +12,7 @@ linters:
|
||||
- test
|
||||
- unused
|
||||
disable:
|
||||
- dupl
|
||||
- exhaustive
|
||||
- exhaustivestruct
|
||||
- gochecknoglobals
|
||||
@ -21,9 +22,11 @@ linters:
|
||||
- gomnd
|
||||
- interfacer
|
||||
- maligned
|
||||
- nilnil
|
||||
- nlreturn
|
||||
- scopelint
|
||||
- testpackage
|
||||
- varnamelen
|
||||
- wrapcheck
|
||||
|
||||
linters-settings:
|
||||
@ -31,6 +34,8 @@ 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:
|
||||
|
@ -28,6 +28,20 @@ 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
263
README.md
@ -1,243 +1,148 @@
|
||||
<h1>
|
||||
<a href="https://github.com/dstotijn/hetty">
|
||||
<img src="https://hetty.xyz/assets/logo.png" width="293">
|
||||
</a>
|
||||
<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"/>
|
||||
</h1>
|
||||
|
||||
[](https://github.com/dstotijn/hetty/releases/latest)
|
||||
[](https://github.com/dstotijn/hetty/actions/workflows/build-test.yml)
|
||||

|
||||
[](https://github.com/dstotijn/hetty/blob/master/LICENSE)
|
||||
[](https://hetty.xyz/)
|
||||
[](https://github.com/dstotijn/hetty/releases/latest)
|
||||
[](https://github.com/dstotijn/hetty/actions/workflows/build-test.yml)
|
||||

|
||||
[](https://github.com/dstotijn/hetty/blob/master/LICENSE)
|
||||
[](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/assets/hetty_v0.2.0_header.png">
|
||||
<img src="https://hetty.xyz/img/hero.png" width="907" alt="Hetty proxy logs (screenshot)" />
|
||||
|
||||
## Features
|
||||
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
ℹ️ 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.
|
||||
👷♂️ Hetty is under active development. Check the <a
|
||||
href="https://github.com/dstotijn/hetty/projects/1">backlog</a> for the current
|
||||
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
|
||||
and web based admin interface.
|
||||
### Installation
|
||||
|
||||
### 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
|
||||
|
||||
#### 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
|
||||
```sh
|
||||
brew install hettysoft/tap/hetty
|
||||
```
|
||||
|
||||
### Docker
|
||||
#### Linux
|
||||
|
||||
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
|
||||
```sh
|
||||
sudo snap install hetty
|
||||
```
|
||||
|
||||
## Usage
|
||||
#### Windows
|
||||
|
||||
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
|
||||
```sh
|
||||
scoop bucket add hettysoft https://github.com/hettysoft/scoop.git
|
||||
scoop install hettysoft/hetty
|
||||
```
|
||||
|
||||
An overview of configuration flags:
|
||||
#### Other
|
||||
|
||||
```
|
||||
$ 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")
|
||||
```
|
||||
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)_.
|
||||
|
||||
You should see:
|
||||
### Usage
|
||||
|
||||
```
|
||||
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
|
||||
Once installed, start Hetty via:
|
||||
|
||||
```sh
|
||||
hetty
|
||||
```
|
||||
|
||||
You should now have a key and certificate located at `~/.hetty/hetty_key.pem` and
|
||||
`~/.hetty/hetty_cert.pem` respectively.
|
||||
💡 Read the [Getting started](https://hetty.xyz/docs/getting-started) doc for
|
||||
more details.
|
||||
|
||||
#### Generating CA certificates with OpenSSL
|
||||
|
||||
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`.
|
||||
To list all available options, run: `hetty --help`:
|
||||
|
||||
```
|
||||
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
|
||||
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.
|
||||
📖 [Read the docs](https://hetty.xyz/docs)
|
||||
|
||||
## 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 all the encouragement and feedback.
|
||||
- The font used in the logo and admin interface is [JetBrains Mono](https://www.jetbrains.com/lp/mono/).
|
||||
for the encouragement and early 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/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>
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](LICENSE)
|
||||
[MIT](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
© 2021 David Stotijn — [Twitter](https://twitter.com/dstotijn), [Email](mailto:dstotijn@gmail.com)
|
||||
© 2022 Hetty Software
|
||||
|
@ -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 { useState } from "react";
|
||||
|
||||
@ -10,6 +22,11 @@ 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;
|
||||
@ -17,7 +34,13 @@ export function RequestLogs(): JSX.Element {
|
||||
pollInterval: 1000,
|
||||
});
|
||||
|
||||
const [createSenderReqFromLog] = useCreateSenderRequestFromHttpRequestLogMutation({});
|
||||
const [createSenderReqFromLog] = useCreateSenderRequestFromHttpRequestLogMutation({
|
||||
onCompleted({ createSenderRequestFromHttpRequestLog }) {
|
||||
const { id } = createSenderRequestFromHttpRequestLog;
|
||||
setNewSenderReqId(id);
|
||||
setCopiedReqNotifOpen(true);
|
||||
},
|
||||
});
|
||||
|
||||
const [copyToSenderId, setCopyToSenderId] = useState("");
|
||||
const [Menu, handleContextMenu, handleContextMenuClose] = useContextMenu();
|
||||
@ -27,11 +50,6 @@ export function RequestLogs(): JSX.Element {
|
||||
variables: {
|
||||
id: copyToSenderId,
|
||||
},
|
||||
onCompleted({ createSenderRequestFromHttpRequestLog }) {
|
||||
const { id } = createSenderRequestFromHttpRequestLog;
|
||||
setNewSenderReqId(id);
|
||||
setCopiedReqNotifOpen(true);
|
||||
},
|
||||
});
|
||||
handleContextMenuClose();
|
||||
};
|
||||
@ -54,6 +72,26 @@ 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>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" height="100%">
|
||||
<Search />
|
||||
@ -77,6 +115,7 @@ export function RequestLogs(): JSX.Element {
|
||||
<RequestsTable
|
||||
requests={data?.httpRequestLogs || []}
|
||||
activeRowId={id}
|
||||
actionsCell={actionsCell}
|
||||
onRowClick={handleRowClick}
|
||||
onContextMenu={handleRowContextClick}
|
||||
/>
|
||||
|
@ -39,13 +39,15 @@ enum HttpMethod {
|
||||
}
|
||||
|
||||
enum HttpProto {
|
||||
Http1 = "HTTP/1.1",
|
||||
Http2 = "HTTP/2.0",
|
||||
Http10 = "HTTP/1.0",
|
||||
Http11 = "HTTP/1.1",
|
||||
Http20 = "HTTP/2.0",
|
||||
}
|
||||
|
||||
const httpProtoMap = new Map([
|
||||
[HttpProto.Http1, HttpProtocol.Http1],
|
||||
[HttpProto.Http2, HttpProtocol.Http2],
|
||||
[HttpProto.Http10, HttpProtocol.Http10],
|
||||
[HttpProto.Http11, HttpProtocol.Http11],
|
||||
[HttpProto.Http20, HttpProtocol.Http20],
|
||||
]);
|
||||
|
||||
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 [url, setURL] = useState("");
|
||||
const [proto, setProto] = useState(HttpProto.Http2);
|
||||
const [proto, setProto] = useState(HttpProto.Http20);
|
||||
const [queryParams, setQueryParams] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
||||
const [headers, setHeaders] = useState<KeyValuePair[]>([{ key: "", value: "" }]);
|
||||
const [body, setBody] = useState("");
|
||||
@ -154,7 +156,6 @@ function EditRequest(): JSX.Element {
|
||||
|
||||
const newHeaders = sortKeyValuePairs(senderRequest.headers || []);
|
||||
setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]);
|
||||
console.log(senderRequest.response);
|
||||
setResponse(senderRequest.response);
|
||||
},
|
||||
});
|
||||
|
@ -34,7 +34,6 @@ interface Props {
|
||||
}
|
||||
|
||||
function Editor({ content, contentType, monacoOptions, onChange }: Props): JSX.Element {
|
||||
console.log(content);
|
||||
return (
|
||||
<MonacoEditor
|
||||
language={languageForContentType(contentType)}
|
||||
|
@ -62,12 +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;
|
||||
}
|
||||
|
||||
export default function RequestsTable(props: Props): JSX.Element {
|
||||
const { requests, activeRowId, onRowClick, onContextMenu } = props;
|
||||
const { requests, activeRowId, actionsCell, onRowClick, onContextMenu } = props;
|
||||
|
||||
return (
|
||||
<TableContainer sx={{ overflowX: "initial" }}>
|
||||
@ -78,6 +79,7 @@ export default function RequestsTable(props: Props): JSX.Element {
|
||||
<TableCell>Origin</TableCell>
|
||||
<TableCell>Path</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
{actionsCell && <TableCell padding="checkbox"></TableCell>}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@ -104,6 +106,7 @@ export default function RequestsTable(props: Props): JSX.Element {
|
||||
<StatusTableCell>
|
||||
{response && <Status code={response.statusCode} reason={response.statusReason} />}
|
||||
</StatusTableCell>
|
||||
{actionsCell && actionsCell(id)}
|
||||
</RequestTableRow>
|
||||
);
|
||||
})}
|
||||
|
@ -12,9 +12,11 @@ type ResponseStatusProps = {
|
||||
|
||||
function mapProto(proto: HttpProtocol): string {
|
||||
switch (proto) {
|
||||
case HttpProtocol.Http1:
|
||||
case HttpProtocol.Http10:
|
||||
return "HTTP/1.0";
|
||||
case HttpProtocol.Http11:
|
||||
return "HTTP/1.1";
|
||||
case HttpProtocol.Http2:
|
||||
case HttpProtocol.Http20:
|
||||
return "HTTP/2.0";
|
||||
default:
|
||||
return proto;
|
||||
|
@ -62,8 +62,9 @@ export enum HttpMethod {
|
||||
}
|
||||
|
||||
export enum HttpProtocol {
|
||||
Http1 = 'HTTP1',
|
||||
Http2 = 'HTTP2'
|
||||
Http10 = 'HTTP10',
|
||||
Http11 = 'HTTP11',
|
||||
Http20 = 'HTTP20'
|
||||
}
|
||||
|
||||
export type HttpRequestLog = {
|
||||
|
212
cmd/hetty/cert.go
Normal file
212
cmd/hetty/cert.go
Normal file
@ -0,0 +1,212 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/smallstep/truststore"
|
||||
)
|
||||
|
||||
var certUsage = `
|
||||
Usage:
|
||||
hetty cert <subcommand> [flags]
|
||||
|
||||
Certificate management tools.
|
||||
|
||||
Options:
|
||||
--help, -h Output this usage text.
|
||||
|
||||
Subcommands:
|
||||
- install Installs a certificate to the system trust store, and
|
||||
(optionally) to the Firefox and Java trust stores.
|
||||
- uninstall Uninstalls a certificate from the system trust store, and
|
||||
(optionally) from the Firefox and Java trust stores.
|
||||
|
||||
Run ` + "`hetty cert <subcommand> --help`" + ` for subcommand specific usage instructions.
|
||||
|
||||
Visit https://hetty.xyz to learn more about Hetty.
|
||||
`
|
||||
|
||||
var certInstallUsage = `
|
||||
Usage:
|
||||
hetty cert install [flags]
|
||||
|
||||
Installs a certificate to the system trust store, and (optionally) to the Firefox
|
||||
and Java trust stores.
|
||||
|
||||
Options:
|
||||
--cert Path to certificate. (Default: "~/.hetty/hetty_cert.pem")
|
||||
--firefox Install certificate to Firefox trust store. (Default: false)
|
||||
--java Install certificate to Java trust store. (Default: false)
|
||||
--skip-system Skip installing certificate to system trust store (Default: false)
|
||||
--help, -h Output this usage text.
|
||||
|
||||
Visit https://hetty.xyz to learn more about Hetty.
|
||||
`
|
||||
|
||||
var certUninstallUsage = `
|
||||
Usage:
|
||||
hetty cert uninstall [flags]
|
||||
|
||||
Uninstalls a certificate from the system trust store, and (optionally) from the Firefox
|
||||
and Java trust stores.
|
||||
|
||||
Options:
|
||||
--cert Path to certificate. (Default: "~/.hetty/hetty_cert.pem")
|
||||
--firefox Uninstall certificate from Firefox trust store. (Default: false)
|
||||
--java Uninstall certificate from Java trust store. (Default: false)
|
||||
--skip-system Skip uninstalling certificate from system trust store (Default: false)
|
||||
--help, -h Output this usage text.
|
||||
|
||||
Visit https://hetty.xyz to learn more about Hetty.
|
||||
`
|
||||
|
||||
type CertInstallCommand struct {
|
||||
config *Config
|
||||
cert string
|
||||
firefox bool
|
||||
java bool
|
||||
skipSystem bool
|
||||
}
|
||||
|
||||
type CertUninstallCommand struct {
|
||||
config *Config
|
||||
cert string
|
||||
firefox bool
|
||||
java bool
|
||||
skipSystem bool
|
||||
}
|
||||
|
||||
func NewCertCommand(rootConfig *Config) *ffcli.Command {
|
||||
return &ffcli.Command{
|
||||
Name: "cert",
|
||||
Subcommands: []*ffcli.Command{
|
||||
NewCertInstallCommand(rootConfig),
|
||||
NewCertUninstallCommand(rootConfig),
|
||||
},
|
||||
Exec: func(context.Context, []string) error {
|
||||
return flag.ErrHelp
|
||||
},
|
||||
UsageFunc: func(*ffcli.Command) string {
|
||||
return certUsage
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewCertInstallCommand(rootConfig *Config) *ffcli.Command {
|
||||
cmd := CertInstallCommand{
|
||||
config: rootConfig,
|
||||
}
|
||||
fs := flag.NewFlagSet("hetty cert install", flag.ExitOnError)
|
||||
|
||||
fs.StringVar(&cmd.cert, "cert", "~/.hetty/hetty_cert.pem", "Path to certificate.")
|
||||
fs.BoolVar(&cmd.firefox, "firefox", false, "Install certificate to Firefox trust store. (Default: false)")
|
||||
fs.BoolVar(&cmd.java, "java", false, "Install certificate to Java trust store. (Default: false)")
|
||||
fs.BoolVar(&cmd.skipSystem, "skip-system", false, "Skip installing certificate to system trust store (Default: false)")
|
||||
|
||||
cmd.config.RegisterFlags(fs)
|
||||
|
||||
return &ffcli.Command{
|
||||
Name: "install",
|
||||
FlagSet: fs,
|
||||
Exec: cmd.Exec,
|
||||
UsageFunc: func(*ffcli.Command) string {
|
||||
return certInstallUsage
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *CertInstallCommand) Exec(_ context.Context, _ []string) error {
|
||||
caCertFile, err := homedir.Expand(cmd.cert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse certificate filepath: %w", err)
|
||||
}
|
||||
|
||||
opts := []truststore.Option{}
|
||||
|
||||
if cmd.skipSystem {
|
||||
opts = append(opts, truststore.WithNoSystem())
|
||||
}
|
||||
|
||||
if cmd.firefox {
|
||||
opts = append(opts, truststore.WithFirefox())
|
||||
}
|
||||
|
||||
if cmd.java {
|
||||
opts = append(opts, truststore.WithJava())
|
||||
}
|
||||
|
||||
if !cmd.skipSystem {
|
||||
cmd.config.logger.Info(
|
||||
"To install the certificate in the system trust store, you might be prompted for your password.")
|
||||
}
|
||||
|
||||
if err := truststore.InstallFile(caCertFile, opts...); err != nil {
|
||||
return fmt.Errorf("failed to install certificate: %w", err)
|
||||
}
|
||||
|
||||
cmd.config.logger.Info("Finished installing certificate.")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCertUninstallCommand(rootConfig *Config) *ffcli.Command {
|
||||
cmd := CertUninstallCommand{
|
||||
config: rootConfig,
|
||||
}
|
||||
fs := flag.NewFlagSet("hetty cert uninstall", flag.ExitOnError)
|
||||
|
||||
fs.StringVar(&cmd.cert, "cert", "~/.hetty/hetty_cert.pem", "Path to certificate.")
|
||||
fs.BoolVar(&cmd.firefox, "firefox", false, "Uninstall certificate from Firefox trust store. (Default: false)")
|
||||
fs.BoolVar(&cmd.java, "java", false, "Uninstall certificate from Java trust store. (Default: false)")
|
||||
fs.BoolVar(&cmd.skipSystem, "skip-system", false,
|
||||
"Skip uninstalling certificate from system trust store (Default: false)")
|
||||
|
||||
cmd.config.RegisterFlags(fs)
|
||||
|
||||
return &ffcli.Command{
|
||||
Name: "uninstall",
|
||||
FlagSet: fs,
|
||||
Exec: cmd.Exec,
|
||||
UsageFunc: func(*ffcli.Command) string {
|
||||
return certUninstallUsage
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *CertUninstallCommand) Exec(_ context.Context, _ []string) error {
|
||||
caCertFile, err := homedir.Expand(cmd.cert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse certificate filepath: %w", err)
|
||||
}
|
||||
|
||||
opts := []truststore.Option{}
|
||||
|
||||
if cmd.skipSystem {
|
||||
opts = append(opts, truststore.WithNoSystem())
|
||||
}
|
||||
|
||||
if cmd.firefox {
|
||||
opts = append(opts, truststore.WithFirefox())
|
||||
}
|
||||
|
||||
if cmd.java {
|
||||
opts = append(opts, truststore.WithJava())
|
||||
}
|
||||
|
||||
if !cmd.skipSystem {
|
||||
cmd.config.logger.Info(
|
||||
"To uninstall the certificate from the system trust store, you might be prompted for your password.")
|
||||
}
|
||||
|
||||
if err := truststore.UninstallFile(caCertFile, opts...); err != nil {
|
||||
return fmt.Errorf("failed to uninstall certificate: %w", err)
|
||||
}
|
||||
|
||||
cmd.config.logger.Info("Finished uninstalling certificate.")
|
||||
|
||||
return nil
|
||||
}
|
23
cmd/hetty/config.go
Normal file
23
cmd/hetty/config.go
Normal file
@ -0,0 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Config represents the global configuration shared amongst all commands.
|
||||
type Config struct {
|
||||
verbose bool
|
||||
jsonLogs bool
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// RegisterFlags registers the flag fields into the provided flag.FlagSet. This
|
||||
// helper function allows subcommands to register the root flags into their
|
||||
// flagsets, creating "global" flags that can be passed after any subcommand at
|
||||
// the commandline.
|
||||
func (cfg *Config) RegisterFlags(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&cfg.verbose, "verbose", false, "Enable verbose logging.")
|
||||
fs.BoolVar(&cfg.jsonLogs, "json", false, "Encode logs as JSON, instead of pretty/human readable output.")
|
||||
}
|
298
cmd/hetty/hetty.go
Normal file
298
cmd/hetty/hetty.go
Normal file
@ -0,0 +1,298 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
badgerdb "github.com/dgraph-io/badger/v3"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
|
||||
"github.com/dstotijn/hetty/pkg/api"
|
||||
"github.com/dstotijn/hetty/pkg/chrome"
|
||||
"github.com/dstotijn/hetty/pkg/db/badger"
|
||||
"github.com/dstotijn/hetty/pkg/proj"
|
||||
"github.com/dstotijn/hetty/pkg/proxy"
|
||||
"github.com/dstotijn/hetty/pkg/reqlog"
|
||||
"github.com/dstotijn/hetty/pkg/scope"
|
||||
"github.com/dstotijn/hetty/pkg/sender"
|
||||
)
|
||||
|
||||
var version = "0.0.0"
|
||||
|
||||
//go:embed admin
|
||||
//go:embed admin/_next/static
|
||||
//go:embed admin/_next/static/chunks/pages/*.js
|
||||
//go:embed admin/_next/static/*/*.js
|
||||
var adminContent embed.FS
|
||||
|
||||
var hettyUsage = `
|
||||
Usage:
|
||||
hetty [flags] [subcommand] [flags]
|
||||
|
||||
Runs an HTTP server with (MITM) proxy, GraphQL service, and a web based admin interface.
|
||||
|
||||
Options:
|
||||
--cert Path to root CA certificate. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_cert.pem")
|
||||
--key Path to root CA private key. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_key.pem")
|
||||
--db Database directory path. (Default: "~/.hetty/db")
|
||||
--addr TCP address for HTTP server to listen on, in the form \"host:port\". (Default: ":8080")
|
||||
--chrome Launch Chrome with proxy settings applied and certificate errors ignored. (Default: false)
|
||||
--verbose Enable verbose logging.
|
||||
--json Encode logs as JSON, instead of pretty/human readable output.
|
||||
--version, -v Output version.
|
||||
--help, -h Output this usage text.
|
||||
|
||||
Subcommands:
|
||||
- cert Certificate management
|
||||
|
||||
Run ` + "`hetty <subcommand> --help`" + ` for subcommand specific usage instructions.
|
||||
|
||||
Visit https://hetty.xyz to learn more about Hetty.
|
||||
`
|
||||
|
||||
type HettyCommand struct {
|
||||
config *Config
|
||||
|
||||
cert string
|
||||
key string
|
||||
db string
|
||||
addr string
|
||||
chrome bool
|
||||
version bool
|
||||
}
|
||||
|
||||
func NewHettyCommand() (*ffcli.Command, *Config) {
|
||||
cmd := HettyCommand{
|
||||
config: &Config{},
|
||||
}
|
||||
|
||||
fs := flag.NewFlagSet("hetty", flag.ExitOnError)
|
||||
|
||||
fs.StringVar(&cmd.cert, "cert", "~/.hetty/hetty_cert.pem",
|
||||
"Path to root CA certificate. Creates a new certificate if file doesn't exist.")
|
||||
fs.StringVar(&cmd.key, "key", "~/.hetty/hetty_key.pem",
|
||||
"Path to root CA private key. Creates a new private key if file doesn't exist.")
|
||||
fs.StringVar(&cmd.db, "db", "~/.hetty/db", "Database directory path.")
|
||||
fs.StringVar(&cmd.addr, "addr", ":8080", "TCP address to listen on, in the form \"host:port\".")
|
||||
fs.BoolVar(&cmd.chrome, "chrome", false, "Launch Chrome with proxy settings applied and certificate errors ignored.")
|
||||
fs.BoolVar(&cmd.version, "version", false, "Output version.")
|
||||
fs.BoolVar(&cmd.version, "v", false, "Output version.")
|
||||
|
||||
cmd.config.RegisterFlags(fs)
|
||||
|
||||
return &ffcli.Command{
|
||||
Name: "hetty",
|
||||
FlagSet: fs,
|
||||
Subcommands: []*ffcli.Command{
|
||||
NewCertCommand(cmd.config),
|
||||
},
|
||||
Exec: cmd.Exec,
|
||||
UsageFunc: func(*ffcli.Command) string {
|
||||
return hettyUsage
|
||||
},
|
||||
}, cmd.config
|
||||
}
|
||||
|
||||
func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error {
|
||||
ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
if cmd.version {
|
||||
fmt.Fprint(os.Stdout, version+"\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
mainLogger := cmd.config.logger.Named("main")
|
||||
|
||||
listenHost, listenPort, err := net.SplitHostPort(cmd.addr)
|
||||
if err != nil {
|
||||
mainLogger.Fatal("Failed to parse listening address.", zap.Error(err))
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("http://%v:%v", listenHost, listenPort)
|
||||
if listenHost == "" || listenHost == "0.0.0.0" || listenHost == "127.0.0.1" || listenHost == "::1" {
|
||||
url = fmt.Sprintf("http://localhost:%v", listenPort)
|
||||
}
|
||||
|
||||
// Expand `~` in filepaths.
|
||||
caCertFile, err := homedir.Expand(cmd.cert)
|
||||
if err != nil {
|
||||
cmd.config.logger.Fatal("Failed to parse CA certificate filepath.", zap.Error(err))
|
||||
}
|
||||
|
||||
caKeyFile, err := homedir.Expand(cmd.key)
|
||||
if err != nil {
|
||||
cmd.config.logger.Fatal("Failed to parse CA private key filepath.", zap.Error(err))
|
||||
}
|
||||
|
||||
dbPath, err := homedir.Expand(cmd.db)
|
||||
if err != nil {
|
||||
cmd.config.logger.Fatal("Failed to parse database path.", zap.Error(err))
|
||||
}
|
||||
|
||||
// Load existing CA certificate and key from disk, or generate and write
|
||||
// to disk if no files exist yet.
|
||||
caCert, caKey, err := proxy.LoadOrCreateCA(caKeyFile, caCertFile)
|
||||
if err != nil {
|
||||
cmd.config.logger.Fatal("Failed to load or create CA key pair.", zap.Error(err))
|
||||
}
|
||||
|
||||
// BadgerDB logs some verbose entries with `INFO` level, so unless
|
||||
// we're running in debug mode, bump the minimal level to `WARN`.
|
||||
dbLogger := cmd.config.logger.Named("badgerdb").WithOptions(zap.IncreaseLevel(zapcore.WarnLevel))
|
||||
|
||||
dbSugaredLogger := dbLogger.Sugar()
|
||||
|
||||
badger, err := badger.OpenDatabase(
|
||||
badgerdb.DefaultOptions(dbPath).WithLogger(badger.NewLogger(dbSugaredLogger)),
|
||||
)
|
||||
if err != nil {
|
||||
cmd.config.logger.Fatal("Failed to open database.", zap.Error(err))
|
||||
}
|
||||
defer badger.Close()
|
||||
|
||||
scope := &scope.Scope{}
|
||||
|
||||
reqLogService := reqlog.NewService(reqlog.Config{
|
||||
Scope: scope,
|
||||
Repository: badger,
|
||||
Logger: cmd.config.logger.Named("reqlog").Sugar(),
|
||||
})
|
||||
|
||||
senderService := sender.NewService(sender.Config{
|
||||
Repository: badger,
|
||||
ReqLogService: reqLogService,
|
||||
})
|
||||
|
||||
projService, err := proj.NewService(proj.Config{
|
||||
Repository: badger,
|
||||
ReqLogService: reqLogService,
|
||||
SenderService: senderService,
|
||||
Scope: scope,
|
||||
})
|
||||
if err != nil {
|
||||
cmd.config.logger.Fatal("Failed to create new projects service.", zap.Error(err))
|
||||
}
|
||||
|
||||
proxy, err := proxy.NewProxy(proxy.Config{
|
||||
CACert: caCert,
|
||||
CAKey: caKey,
|
||||
Logger: cmd.config.logger.Named("proxy").Sugar(),
|
||||
})
|
||||
if err != nil {
|
||||
cmd.config.logger.Fatal("Failed to create new proxy.", zap.Error(err))
|
||||
}
|
||||
|
||||
proxy.UseRequestModifier(reqLogService.RequestModifier)
|
||||
proxy.UseResponseModifier(reqLogService.ResponseModifier)
|
||||
|
||||
fsSub, err := fs.Sub(adminContent, "admin")
|
||||
if err != nil {
|
||||
cmd.config.logger.Fatal("Failed to construct file system subtree from admin dir.", zap.Error(err))
|
||||
}
|
||||
|
||||
adminHandler := http.FileServer(http.FS(fsSub))
|
||||
router := mux.NewRouter().SkipClean(true)
|
||||
adminRouter := router.MatcherFunc(func(req *http.Request, match *mux.RouteMatch) bool {
|
||||
hostname, _ := os.Hostname()
|
||||
host, _, _ := net.SplitHostPort(req.Host)
|
||||
|
||||
// Serve local admin routes when either:
|
||||
// - The `Host` is well-known, e.g. `hetty.proxy`, `localhost:[port]`
|
||||
// or the listen addr `[host]:[port]`.
|
||||
// - The request is not for TLS proxying (e.g. no `CONNECT`) and not
|
||||
// for proxying an external URL. E.g. Request-Line (RFC 7230, Section 3.1.1)
|
||||
// has no scheme.
|
||||
return strings.EqualFold(host, hostname) ||
|
||||
req.Host == "hetty.proxy" ||
|
||||
req.Host == fmt.Sprintf("%v:%v", "localhost", listenPort) ||
|
||||
req.Host == fmt.Sprintf("%v:%v", listenHost, listenPort) ||
|
||||
req.Method != http.MethodConnect && !strings.HasPrefix(req.RequestURI, "http://")
|
||||
}).Subrouter().StrictSlash(true)
|
||||
|
||||
// GraphQL server.
|
||||
gqlEndpoint := "/api/graphql/"
|
||||
adminRouter.Path(gqlEndpoint).Handler(api.HTTPHandler(&api.Resolver{
|
||||
ProjectService: projService,
|
||||
RequestLogService: reqLogService,
|
||||
SenderService: senderService,
|
||||
}, gqlEndpoint))
|
||||
|
||||
// Admin interface.
|
||||
adminRouter.PathPrefix("").Handler(adminHandler)
|
||||
|
||||
// Fallback (default) is the Proxy handler.
|
||||
router.PathPrefix("").Handler(proxy)
|
||||
|
||||
httpServer := &http.Server{
|
||||
Addr: cmd.addr,
|
||||
Handler: router,
|
||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}, // Disable HTTP/2
|
||||
ErrorLog: zap.NewStdLog(cmd.config.logger.Named("http")),
|
||||
}
|
||||
|
||||
go func() {
|
||||
mainLogger.Info(fmt.Sprintf("Hetty (v%v) is running on %v ...", version, cmd.addr))
|
||||
mainLogger.Info(fmt.Sprintf("\x1b[%dm%s\x1b[0m", uint8(32), "Get started at "+url))
|
||||
|
||||
err := httpServer.ListenAndServe()
|
||||
if err != http.ErrServerClosed {
|
||||
mainLogger.Fatal("HTTP server closed unexpected.", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
if cmd.chrome {
|
||||
ctx, cancel := chrome.NewExecAllocator(ctx, chrome.Config{
|
||||
ProxyServer: url,
|
||||
ProxyBypassHosts: []string{url},
|
||||
})
|
||||
defer cancel()
|
||||
|
||||
taskCtx, cancel := chromedp.NewContext(ctx)
|
||||
defer cancel()
|
||||
|
||||
err = chromedp.Run(taskCtx, chromedp.Navigate(url))
|
||||
|
||||
switch {
|
||||
case errors.Is(err, exec.ErrNotFound):
|
||||
mainLogger.Info("Chrome executable not found.")
|
||||
case err != nil:
|
||||
mainLogger.Error(fmt.Sprintf("Failed to navigate to %v.", url), zap.Error(err))
|
||||
default:
|
||||
mainLogger.Info("Launched Chrome.")
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for interrupt signal.
|
||||
<-ctx.Done()
|
||||
// Restore signal, allowing "force quit".
|
||||
stop()
|
||||
|
||||
mainLogger.Info("Shutting down HTTP server. Press Ctrl+C to force quit.")
|
||||
|
||||
// Note: We expect httpServer.Handler to handle timeouts, thus, we don't
|
||||
// need a context value with deadline here.
|
||||
//nolint:contextcheck
|
||||
err = httpServer.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to shutdown HTTP server: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,163 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
llog "log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"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"
|
||||
"github.com/dstotijn/hetty/pkg/log"
|
||||
)
|
||||
|
||||
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() {
|
||||
if err := run(); err != nil {
|
||||
log.Fatalf("[ERROR]: %v", err)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
flag.StringVar(&caCertFile, "cert", "~/.hetty/hetty_cert.pem",
|
||||
"CA certificate filepath. Creates a new CA certificate if file doesn't exist")
|
||||
flag.StringVar(&caKeyFile, "key", "~/.hetty/hetty_key.pem",
|
||||
"CA private key filepath. Creates a new CA private key if file doesn't exist")
|
||||
flag.StringVar(&dbPath, "db", "~/.hetty/db", "Database directory path")
|
||||
flag.StringVar(&addr, "addr", ":8080", "TCP address to listen on, in the form \"host:port\"")
|
||||
flag.Parse()
|
||||
|
||||
// Expand `~` in filepaths.
|
||||
caCertFile, err := homedir.Expand(caCertFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse CA certificate filepath: %w", err)
|
||||
}
|
||||
|
||||
caKeyFile, err := homedir.Expand(caKeyFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse CA private key filepath: %w", err)
|
||||
}
|
||||
|
||||
dbPath, err := homedir.Expand(dbPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse projects filepath: %w", err)
|
||||
}
|
||||
|
||||
// Load existing CA certificate and key from disk, or generate and write
|
||||
// to disk if no files exist yet.
|
||||
caCert, caKey, err := proxy.LoadOrCreateCA(caKeyFile, caCertFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create/load CA key pair: %w", err)
|
||||
}
|
||||
|
||||
badger, err := badger.OpenDatabase(badgerdb.DefaultOptions(dbPath))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open badger database: %w", err)
|
||||
}
|
||||
defer badger.Close()
|
||||
|
||||
scope := &scope.Scope{}
|
||||
|
||||
reqLogService := reqlog.NewService(reqlog.Config{
|
||||
Scope: scope,
|
||||
Repository: badger,
|
||||
})
|
||||
|
||||
senderService := sender.NewService(sender.Config{
|
||||
Repository: badger,
|
||||
ReqLogService: reqLogService,
|
||||
})
|
||||
|
||||
projService, err := proj.NewService(proj.Config{
|
||||
Repository: badger,
|
||||
ReqLogService: reqLogService,
|
||||
SenderService: senderService,
|
||||
Scope: scope,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create new project service: %w", err)
|
||||
}
|
||||
|
||||
p, err := proxy.NewProxy(caCert, caKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create proxy: %w", err)
|
||||
}
|
||||
|
||||
p.UseRequestModifier(reqLogService.RequestModifier)
|
||||
p.UseResponseModifier(reqLogService.ResponseModifier)
|
||||
|
||||
fsSub, err := fs.Sub(adminContent, "admin")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not prepare subtree file system: %w", err)
|
||||
}
|
||||
|
||||
adminHandler := http.FileServer(http.FS(fsSub))
|
||||
router := mux.NewRouter().SkipClean(true)
|
||||
adminRouter := router.MatcherFunc(func(req *http.Request, match *mux.RouteMatch) bool {
|
||||
hostname, _ := os.Hostname()
|
||||
host, _, _ := net.SplitHostPort(req.Host)
|
||||
return strings.EqualFold(host, hostname) || (req.Host == "hetty.proxy" || req.Host == "localhost:8080")
|
||||
}).Subrouter().StrictSlash(true)
|
||||
|
||||
// GraphQL server.
|
||||
adminRouter.Path("/api/playground/").Handler(playground.Handler("GraphQL Playground", "/api/graphql/"))
|
||||
adminRouter.Path("/api/graphql/").Handler(
|
||||
handler.NewDefaultServer(api.NewExecutableSchema(api.Config{Resolvers: &api.Resolver{
|
||||
ProjectService: projService,
|
||||
RequestLogService: reqLogService,
|
||||
SenderService: senderService,
|
||||
}})))
|
||||
|
||||
// Admin interface.
|
||||
adminRouter.PathPrefix("").Handler(adminHandler)
|
||||
|
||||
// Fallback (default) is the Proxy handler.
|
||||
router.PathPrefix("").Handler(p)
|
||||
|
||||
s := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: router,
|
||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}, // Disable HTTP/2
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Hetty (v%v) is running on %v ...", version, addr)
|
||||
|
||||
err = s.ListenAndServe()
|
||||
if err != nil && errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("http server closed unexpected: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
24
go.mod
24
go.mod
@ -4,22 +4,31 @@ go 1.17
|
||||
|
||||
require (
|
||||
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
|
||||
@ -28,7 +37,9 @@ 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
|
||||
@ -36,10 +47,13 @@ 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
|
||||
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
|
||||
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/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.4 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
|
||||
)
|
||||
|
68
go.sum
68
go.sum
@ -12,10 +12,18 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/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=
|
||||
@ -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/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=
|
||||
@ -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/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=
|
||||
@ -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/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=
|
||||
@ -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/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=
|
||||
@ -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/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=
|
||||
@ -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/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.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.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=
|
||||
@ -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.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=
|
||||
@ -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-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=
|
||||
@ -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-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=
|
||||
@ -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-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=
|
||||
@ -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-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-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-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=
|
||||
@ -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-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=
|
||||
@ -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.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=
|
||||
|
@ -897,8 +897,9 @@ enum HttpMethod {
|
||||
}
|
||||
|
||||
enum HttpProtocol {
|
||||
HTTP1
|
||||
HTTP2
|
||||
HTTP10
|
||||
HTTP11
|
||||
HTTP20
|
||||
}
|
||||
|
||||
scalar Time
|
||||
|
21
pkg/api/http.go
Normal file
21
pkg/api/http.go
Normal file
@ -0,0 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/playground"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func HTTPHandler(resolver *Resolver, gqlEndpoint string) http.Handler {
|
||||
router := mux.NewRouter().SkipClean(true)
|
||||
router.Methods("POST").Handler(
|
||||
handler.NewDefaultServer(NewExecutableSchema(Config{
|
||||
Resolvers: resolver,
|
||||
})),
|
||||
)
|
||||
router.Methods("GET").Handler(playground.Handler("GraphQL Playground", gqlEndpoint))
|
||||
|
||||
return router
|
||||
}
|
@ -186,18 +186,20 @@ func (e HTTPMethod) MarshalGQL(w io.Writer) {
|
||||
type HTTPProtocol string
|
||||
|
||||
const (
|
||||
HTTPProtocolHTTP1 HTTPProtocol = "HTTP1"
|
||||
HTTPProtocolHTTP2 HTTPProtocol = "HTTP2"
|
||||
HTTPProtocolHTTP10 HTTPProtocol = "HTTP10"
|
||||
HTTPProtocolHTTP11 HTTPProtocol = "HTTP11"
|
||||
HTTPProtocolHTTP20 HTTPProtocol = "HTTP20"
|
||||
)
|
||||
|
||||
var AllHTTPProtocol = []HTTPProtocol{
|
||||
HTTPProtocolHTTP1,
|
||||
HTTPProtocolHTTP2,
|
||||
HTTPProtocolHTTP10,
|
||||
HTTPProtocolHTTP11,
|
||||
HTTPProtocolHTTP20,
|
||||
}
|
||||
|
||||
func (e HTTPProtocol) IsValid() bool {
|
||||
switch e {
|
||||
case HTTPProtocolHTTP1, HTTPProtocolHTTP2:
|
||||
case HTTPProtocolHTTP10, HTTPProtocolHTTP11, HTTPProtocolHTTP20:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
@ -22,13 +22,15 @@ import (
|
||||
)
|
||||
|
||||
var httpProtocolMap = map[string]HTTPProtocol{
|
||||
sender.HTTPProto1: HTTPProtocolHTTP1,
|
||||
sender.HTTPProto2: HTTPProtocolHTTP2,
|
||||
sender.HTTPProto10: HTTPProtocolHTTP10,
|
||||
sender.HTTPProto11: HTTPProtocolHTTP11,
|
||||
sender.HTTPProto20: HTTPProtocolHTTP20,
|
||||
}
|
||||
|
||||
var revHTTPProtocolMap = map[HTTPProtocol]string{
|
||||
HTTPProtocolHTTP1: sender.HTTPProto1,
|
||||
HTTPProtocolHTTP2: sender.HTTPProto2,
|
||||
HTTPProtocolHTTP10: sender.HTTPProto10,
|
||||
HTTPProtocolHTTP11: sender.HTTPProto11,
|
||||
HTTPProtocolHTTP20: sender.HTTPProto20,
|
||||
}
|
||||
|
||||
type Resolver struct {
|
||||
@ -406,7 +408,10 @@ 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),
|
||||
@ -447,7 +452,10 @@ func (r *mutationResolver) CreateOrUpdateSenderRequest(ctx context.Context, inpu
|
||||
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)
|
||||
@ -471,10 +479,13 @@ func (r *mutationResolver) SendRequest(ctx context.Context, id ulid.ULID) (*Send
|
||||
|
||||
var sendErr *sender.SendError
|
||||
|
||||
//nolint:contextcheck
|
||||
req, err := r.SenderService.SendRequest(ctx2, id)
|
||||
if errors.Is(err, proj.ErrNoProject) {
|
||||
|
||||
switch {
|
||||
case errors.Is(err, proj.ErrNoProject):
|
||||
return nil, noActiveProjectErr(ctx)
|
||||
} else if errors.As(err, &sendErr) {
|
||||
case errors.As(err, &sendErr):
|
||||
return nil, &gqlerror.Error{
|
||||
Path: graphql.GetPath(ctx),
|
||||
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",
|
||||
},
|
||||
}
|
||||
} else if err != nil {
|
||||
case err != nil:
|
||||
return nil, fmt.Errorf("could not send request: %w", err)
|
||||
}
|
||||
|
||||
|
@ -157,8 +157,9 @@ enum HttpMethod {
|
||||
}
|
||||
|
||||
enum HttpProtocol {
|
||||
HTTP1
|
||||
HTTP2
|
||||
HTTP10
|
||||
HTTP11
|
||||
HTTP20
|
||||
}
|
||||
|
||||
scalar Time
|
||||
|
34
pkg/chrome/chrome.go
Normal file
34
pkg/chrome/chrome.go
Normal file
@ -0,0 +1,34 @@
|
||||
package chrome
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
var defaultOpts = []chromedp.ExecAllocatorOption{
|
||||
chromedp.NoFirstRun,
|
||||
chromedp.NoDefaultBrowserCheck,
|
||||
chromedp.IgnoreCertErrors,
|
||||
chromedp.Flag("test-type ", true), // This prevents the `ignore-certificate-errors` warning.
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ProxyServer string
|
||||
ProxyBypassHosts []string
|
||||
}
|
||||
|
||||
// NewExecAllocator returns a new context setup with a chromedp.ExecAllocator.
|
||||
// Its `context.Context` return value can be used to create subsequent contexts for interacting
|
||||
// with an allocated Chrome browser.
|
||||
func NewExecAllocator(ctx context.Context, cfg Config) (context.Context, context.CancelFunc) {
|
||||
proxyBypass := strings.Join(append([]string{"<-loopback"}, cfg.ProxyBypassHosts...), ";")
|
||||
//nolint:gocritic
|
||||
opts := append(defaultOpts,
|
||||
chromedp.ProxyServer(cfg.ProxyServer),
|
||||
chromedp.Flag("proxy-bypass-list", proxyBypass),
|
||||
)
|
||||
|
||||
return chromedp.NewExecAllocator(ctx, opts...)
|
||||
}
|
21
pkg/db/badger/logger.go
Normal file
21
pkg/db/badger/logger.go
Normal file
@ -0,0 +1,21 @@
|
||||
package badger
|
||||
|
||||
import (
|
||||
"github.com/dgraph-io/badger/v3"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Interface guard.
|
||||
var _ badger.Logger = (*Logger)(nil)
|
||||
|
||||
type Logger struct {
|
||||
*zap.SugaredLogger
|
||||
}
|
||||
|
||||
func NewLogger(l *zap.SugaredLogger) *Logger {
|
||||
return &Logger{l}
|
||||
}
|
||||
|
||||
func (l *Logger) Warningf(template string, args ...interface{}) {
|
||||
l.Warnf(template, args)
|
||||
}
|
@ -14,7 +14,11 @@ import (
|
||||
"github.com/dstotijn/hetty/pkg/scope"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@ -231,6 +235,7 @@ 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()
|
||||
|
||||
@ -238,7 +243,7 @@ func findRequestLogIDsByProjectID(txn *badger.Txn, projectID ulid.ULID) ([]ulid.
|
||||
|
||||
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)
|
||||
|
||||
var id ulid.ULID
|
||||
|
@ -46,7 +46,7 @@ func TestFindRequestLogs(t *testing.T) {
|
||||
|
||||
projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)
|
||||
|
||||
exp := []reqlog.RequestLog{
|
||||
fixtures := []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 exp {
|
||||
for _, reqLog := range fixtures {
|
||||
err = database.StoreRequestLog(context.Background(), reqLog)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
@ -63,7 +63,11 @@ 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
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ func TestFindRequestByID(t *testing.T) {
|
||||
|
||||
URL: exampleURL,
|
||||
Method: http.MethodGet,
|
||||
Proto: sender.HTTPProto2,
|
||||
Proto: sender.HTTPProto20,
|
||||
Header: http.Header{
|
||||
"X-Foo": []string{"bar"},
|
||||
},
|
||||
|
87
pkg/log/log.go
Normal file
87
pkg/log/log.go
Normal file
@ -0,0 +1,87 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
type Logger interface {
|
||||
Debugw(msg string, v ...interface{})
|
||||
Infow(msg string, v ...interface{})
|
||||
Errorw(msg string, v ...interface{})
|
||||
}
|
||||
|
||||
func NewZapLogger(verbose, jsonLogs bool) (*zap.Logger, error) {
|
||||
var config zap.Config
|
||||
|
||||
if verbose {
|
||||
config = zap.Config{
|
||||
Level: zap.NewAtomicLevelAt(zap.DebugLevel),
|
||||
Development: true,
|
||||
Encoding: "json",
|
||||
EncoderConfig: zapcore.EncoderConfig{
|
||||
TimeKey: "ts",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
FunctionKey: zapcore.OmitKey,
|
||||
MessageKey: "message",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||
EncodeTime: zapcore.RFC3339TimeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
},
|
||||
OutputPaths: []string{"stderr"},
|
||||
ErrorOutputPaths: []string{"stderr"},
|
||||
}
|
||||
} else {
|
||||
config = zap.NewProductionConfig()
|
||||
}
|
||||
|
||||
if !jsonLogs {
|
||||
config.Encoding = "console"
|
||||
config.EncoderConfig = zapcore.EncoderConfig{
|
||||
TimeKey: "T",
|
||||
LevelKey: "L",
|
||||
NameKey: "N",
|
||||
CallerKey: zapcore.OmitKey,
|
||||
FunctionKey: zapcore.OmitKey,
|
||||
MessageKey: "M",
|
||||
StacktraceKey: zapcore.OmitKey,
|
||||
ConsoleSeparator: " ",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.CapitalColorLevelEncoder,
|
||||
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||
enc.AppendString(t.Format("2006/01/02 15:04:05"))
|
||||
},
|
||||
EncodeName: func(loggerName string, enc zapcore.PrimitiveArrayEncoder) {
|
||||
// Print logger name in cyan (ANSI code 36).
|
||||
enc.AppendString(fmt.Sprintf("\x1b[%dm%s\x1b[0m", uint8(36), "["+loggerName+"]"))
|
||||
},
|
||||
EncodeDuration: zapcore.StringDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
|
||||
if verbose {
|
||||
config.EncoderConfig.CallerKey = "C"
|
||||
config.EncoderConfig.StacktraceKey = "S"
|
||||
}
|
||||
}
|
||||
|
||||
return config.Build()
|
||||
}
|
||||
|
||||
type NopLogger struct{}
|
||||
|
||||
func (nop NopLogger) Debugw(_ string, _ ...interface{}) {}
|
||||
func (nop NopLogger) Infow(_ string, _ ...interface{}) {}
|
||||
func (nop NopLogger) Errorw(_ string, _ ...interface{}) {}
|
||||
|
||||
func NewNopLogger() NopLogger {
|
||||
return NopLogger{}
|
||||
}
|
@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"sync"
|
||||
@ -21,11 +20,6 @@ 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)
|
||||
@ -39,8 +33,6 @@ 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 {
|
||||
@ -49,8 +41,6 @@ type service struct {
|
||||
senderSvc sender.Service
|
||||
scope *scope.Scope
|
||||
activeProjectID ulid.ULID
|
||||
onProjectOpenFns []OnProjectOpenFn
|
||||
onProjectCloseFns []OnProjectCloseFn
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
@ -126,8 +116,6 @@ func (svc *service) CloseProject() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
closedProjectID := svc.activeProjectID
|
||||
|
||||
svc.activeProjectID = ulid.ULID{}
|
||||
svc.reqLogSvc.SetActiveProjectID(ulid.ULID{})
|
||||
svc.reqLogSvc.SetBypassOutOfScopeRequests(false)
|
||||
@ -136,8 +124,6 @@ func (svc *service) CloseProject() error {
|
||||
svc.senderSvc.SetFindReqsFilter(sender.FindRequestsFilter{})
|
||||
svc.scope.SetRules(nil)
|
||||
|
||||
svc.emitProjectClosed(closedProjectID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -183,8 +169,6 @@ func (svc *service) OpenProject(ctx context.Context, projectID ulid.ULID) (Proje
|
||||
|
||||
svc.scope.SetRules(project.Settings.ScopeRules)
|
||||
|
||||
svc.emitProjectOpened()
|
||||
|
||||
return project, nil
|
||||
}
|
||||
|
||||
@ -217,36 +201,6 @@ 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 {
|
||||
|
@ -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, 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)
|
||||
}
|
||||
}
|
||||
@ -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, 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)
|
||||
}
|
||||
}
|
||||
@ -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, 0600)
|
||||
keyOut, err := os.OpenFile(caKeyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("proxy: could not open key file for writing: %w", err)
|
||||
}
|
||||
|
@ -7,10 +7,11 @@ import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
|
||||
"github.com/dstotijn/hetty/pkg/log"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
@ -22,15 +23,22 @@ 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(ca *x509.Certificate, key crypto.PrivateKey) (*Proxy, error) {
|
||||
certConfig, err := NewCertConfig(ca, key)
|
||||
func NewProxy(cfg Config) (*Proxy, error) {
|
||||
certConfig, err := NewCertConfig(cfg.CACert, cfg.CAKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -39,12 +47,17 @@ func NewProxy(ca *x509.Certificate, key crypto.PrivateKey) (*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: errorHandler,
|
||||
ErrorHandler: p.errorHandler,
|
||||
}
|
||||
|
||||
return p, nil
|
||||
@ -103,7 +116,8 @@ func (p *Proxy) modifyResponse(res *http.Response) error {
|
||||
func (p *Proxy) handleConnect(w http.ResponseWriter) {
|
||||
hj, ok := w.(http.Hijacker)
|
||||
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)
|
||||
|
||||
return
|
||||
@ -113,7 +127,8 @@ func (p *Proxy) handleConnect(w http.ResponseWriter) {
|
||||
|
||||
clientConn, _, err := hj.Hijack()
|
||||
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)
|
||||
|
||||
return
|
||||
@ -121,18 +136,22 @@ func (p *Proxy) handleConnect(w http.ResponseWriter) {
|
||||
defer clientConn.Close()
|
||||
|
||||
// Secure connection to client.
|
||||
clientConn, err = p.clientTLSConn(clientConn)
|
||||
tlsConn, err := p.clientTLSConn(clientConn)
|
||||
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
|
||||
}
|
||||
|
||||
clientConnNotify := ConnNotify{clientConn, make(chan struct{})}
|
||||
clientConnNotify := ConnNotify{tlsConn, make(chan struct{})}
|
||||
l := &OnceAcceptListener{clientConnNotify.Conn}
|
||||
|
||||
err = http.Serve(l, p)
|
||||
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
|
||||
@ -150,12 +169,13 @@ func (p *Proxy) clientTLSConn(conn net.Conn) (*tls.Conn, error) {
|
||||
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) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[ERROR]: Proxy error: %v", err)
|
||||
p.logger.Errorw("Failed to proxy request.",
|
||||
"error", err)
|
||||
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -16,6 +15,7 @@ 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,6 +74,7 @@ type service struct {
|
||||
activeProjectID ulid.ULID
|
||||
scope *scope.Scope
|
||||
repo Repository
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
type FindRequestsFilter struct {
|
||||
@ -85,13 +86,21 @@ type FindRequestsFilter struct {
|
||||
type Config struct {
|
||||
Scope *scope.Scope
|
||||
Repository Repository
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
func NewService(cfg Config) Service {
|
||||
return &service{
|
||||
s := &service{
|
||||
repo: cfg.Repository,
|
||||
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) {
|
||||
@ -129,7 +138,8 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
|
||||
|
||||
body, err = ioutil.ReadAll(req.Body)
|
||||
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
|
||||
}
|
||||
|
||||
@ -142,6 +152,9 @@ 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
|
||||
}
|
||||
|
||||
@ -151,6 +164,9 @@ 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
|
||||
}
|
||||
|
||||
@ -166,10 +182,15 @@ func (svc *service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestM
|
||||
|
||||
err := svc.repo.StoreRequestLog(req.Context(), reqLog)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Could not store request log: %v", err)
|
||||
svc.logger.Errorw("Failed to store request log.",
|
||||
"error", 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)
|
||||
}
|
||||
@ -203,7 +224,11 @@ func (svc *service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.Respon
|
||||
|
||||
go func() {
|
||||
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{}
|
||||
|
||||
//nolint:gosec
|
||||
if _, err := io.Copy(buf, gzipReader); err != nil {
|
||||
return ResponseLog{}, fmt.Errorf("reqlog: could not read gzipped response body: %w", err)
|
||||
}
|
||||
|
@ -117,7 +117,8 @@ func TestResponseModifier(t *testing.T) {
|
||||
t.Run("called repository with request log id", func(t *testing.T) {
|
||||
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())
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -126,7 +126,7 @@ func (svc *service) CreateOrUpdateRequest(ctx context.Context, req Request) (Req
|
||||
}
|
||||
|
||||
if req.Proto == "" {
|
||||
req.Proto = HTTPProto2
|
||||
req.Proto = HTTPProto20
|
||||
}
|
||||
|
||||
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: HTTPProto2, // Attempt HTTP/2.
|
||||
Proto: HTTPProto20, // Attempt HTTP/2.
|
||||
Header: reqLog.Header,
|
||||
Body: reqLog.Body,
|
||||
}
|
||||
|
@ -165,7 +165,7 @@ func TestCloneFromRequestLog(t *testing.T) {
|
||||
ProjectID: projectID,
|
||||
URL: exampleURL,
|
||||
Method: http.MethodPost,
|
||||
Proto: sender.HTTPProto2,
|
||||
Proto: sender.HTTPProto20,
|
||||
Header: http.Header{
|
||||
"X-Foo": []string{"bar"},
|
||||
},
|
||||
|
@ -12,8 +12,9 @@ type HTTPTransport struct{}
|
||||
type protoCtxKey struct{}
|
||||
|
||||
const (
|
||||
HTTPProto1 = "HTTP/1.1"
|
||||
HTTPProto2 = "HTTP/2.0"
|
||||
HTTPProto10 = "HTTP/1.0"
|
||||
HTTPProto11 = "HTTP/1.1"
|
||||
HTTPProto20 = "HTTP/2.0"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
proto, ok := req.Context().Value(protoCtxKey{}).(string)
|
||||
|
||||
if ok && proto == HTTPProto1 {
|
||||
if ok && proto == HTTPProto10 || proto == HTTPProto11 {
|
||||
return h1OnlyTransport.RoundTrip(req)
|
||||
}
|
||||
|
||||
@ -46,5 +47,5 @@ func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
}
|
||||
|
||||
func isValidProto(proto string) bool {
|
||||
return proto == HTTPProto1 || proto == HTTPProto2
|
||||
return proto == HTTPProto10 || proto == HTTPProto11 || proto == HTTPProto20
|
||||
}
|
||||
|
Reference in New Issue
Block a user