├── admin
├── .gitignore
├── public
│ ├── favicon.ico
│ └── style.css
├── next-env.d.ts
├── src
│ ├── pages
│ │ ├── proxy
│ │ │ ├── logs
│ │ │ │ └── index.tsx
│ │ │ └── index.tsx
│ │ ├── sender
│ │ │ └── index.tsx
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ └── index.tsx
│ ├── components
│ │ ├── CenteredPaper.tsx
│ │ ├── reqlog
│ │ │ ├── HttpStatusCode.tsx
│ │ │ ├── Editor.tsx
│ │ │ ├── ResponseDetail.tsx
│ │ │ ├── LogsOverview.tsx
│ │ │ ├── LogDetail.tsx
│ │ │ ├── RequestDetail.tsx
│ │ │ ├── HttpHeadersTable.tsx
│ │ │ └── RequestList.tsx
│ │ └── Layout.tsx
│ └── lib
│ │ ├── theme.ts
│ │ └── graphql.ts
├── tsconfig.json
├── next.config.js
└── package.json
├── .gitignore
├── .github
└── FUNDING.yml
├── .dockerignore
├── modd.conf
├── Makefile
├── pkg
├── reqlog
│ ├── repo.go
│ └── reqlog.go
├── api
│ ├── schema.graphql
│ ├── models_gen.go
│ └── resolvers.go
├── proxy
│ ├── modify.go
│ ├── net.go
│ ├── proxy.go
│ └── cert.go
└── db
│ └── cayley
│ ├── bolt.go
│ └── cayley.go
├── go.mod
├── .goreleaser.yml
├── Dockerfile
├── LICENSE
├── gqlgen.yml
├── cmd
└── hetty
│ └── main.go
├── README.md
└── go.sum
/admin/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /.next
3 | /dist
4 |
5 | *.log*
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/rice-box.go
2 | dist
3 | hetty
4 | hetty.bolt
5 | *.test
6 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | patreon: dstotijn
4 |
--------------------------------------------------------------------------------
/admin/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hahwul/hetty/master/admin/public/favicon.ico
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/rice-box.go
2 | /admin/.env
3 | /admin/.next
4 | /admin/dist
5 | /admin/node_modules
--------------------------------------------------------------------------------
/admin/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/modd.conf:
--------------------------------------------------------------------------------
1 | @cert = $HOME/.ssh/hetty_cert.pem
2 | @key = $HOME/.ssh/hetty_key.pem
3 | @db = hetty.bolt
4 | @addr = :8080
5 |
6 | **/*.go {
7 | daemon +sigterm: go run ./cmd/hetty \
8 | -cert=@cert \
9 | -key=@key \
10 | -db=@db \
11 | -addr=@addr
12 | }
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | setup:
2 | go mod download
3 | go generate ./...
4 | .PHONY: setup
5 |
6 | embed:
7 | go install github.com/GeertJohan/go.rice/rice
8 | cd cmd/hetty && rice embed-go
9 | .PHONY: embed
10 |
11 | build: embed
12 | go build ./cmd/hetty
13 | .PHONY: build
14 |
15 | clean:
16 | rm -rf cmd/hetty/rice-box.go
17 | .PHONY: clean
18 |
19 | release:
20 | goreleaser -p 1
--------------------------------------------------------------------------------
/admin/src/pages/proxy/logs/index.tsx:
--------------------------------------------------------------------------------
1 | import LogsOverview from "../../../components/reqlog/LogsOverview";
2 | import Layout, { Page } from "../../../components/Layout";
3 |
4 | function ProxyLogs(): JSX.Element {
5 | return (
6 |
7 |
8 |
9 | );
10 | }
11 |
12 | export default ProxyLogs;
13 |
--------------------------------------------------------------------------------
/admin/src/pages/sender/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Typography } from "@material-ui/core";
2 |
3 | import Layout, { Page } from "../../components/Layout";
4 |
5 | function Index(): JSX.Element {
6 | return (
7 |
8 | Coming soon…
9 |
10 | );
11 | }
12 |
13 | export default Index;
14 |
--------------------------------------------------------------------------------
/pkg/reqlog/repo.go:
--------------------------------------------------------------------------------
1 | package reqlog
2 |
3 | import (
4 | "context"
5 |
6 | "github.com/google/uuid"
7 | )
8 |
9 | type Repository interface {
10 | FindAllRequestLogs(ctx context.Context) ([]Request, error)
11 | FindRequestLogByID(ctx context.Context, id uuid.UUID) (Request, error)
12 | AddRequestLog(ctx context.Context, reqLog Request) error
13 | AddResponseLog(ctx context.Context, resLog Response) error
14 | }
15 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/dstotijn/hetty
2 |
3 | go 1.15
4 |
5 | require (
6 | github.com/99designs/gqlgen v0.11.3
7 | github.com/GeertJohan/go.rice v1.0.0
8 | github.com/cayleygraph/cayley v0.7.7
9 | github.com/cayleygraph/quad v1.1.0
10 | github.com/google/uuid v1.1.2
11 | github.com/gorilla/mux v1.7.4
12 | github.com/hidal-go/hidalgo v0.0.0-20190814174001-42e03f3b5eaa
13 | github.com/vektah/gqlparser/v2 v2.0.1
14 | golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346 // indirect
15 | )
16 |
--------------------------------------------------------------------------------
/admin/src/components/CenteredPaper.tsx:
--------------------------------------------------------------------------------
1 | import { Paper } from "@material-ui/core";
2 |
3 | function CenteredPaper({
4 | children,
5 | }: {
6 | children: React.ReactNode;
7 | }): JSX.Element {
8 | return (
9 |
10 |
19 | {children}
20 |
21 |
22 | );
23 | }
24 |
25 | export default CenteredPaper;
26 |
--------------------------------------------------------------------------------
/admin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "strict": false,
12 | "forceConsistentCasingInFileNames": true,
13 | "noEmit": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "jsx": "preserve"
20 | },
21 | "include": [
22 | "next-env.d.ts",
23 | "**/*.ts",
24 | "**/*.tsx"
25 | ],
26 | "exclude": [
27 | "node_modules"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | before:
2 | hooks:
3 | - make clean
4 | - go mod download
5 | - go generate ./...
6 | builds:
7 | - main: ./cmd/hetty
8 | env:
9 | - CGO_ENABLED=0
10 | goos:
11 | - linux
12 | - windows
13 | - darwin
14 | hooks:
15 | pre: make embed
16 | archives:
17 | - replacements:
18 | darwin: Darwin
19 | linux: Linux
20 | windows: Windows
21 | 386: i386
22 | amd64: x86_64
23 | checksum:
24 | name_template: "checksums.txt"
25 | snapshot:
26 | name_template: "{{ .Tag }}-next"
27 | changelog:
28 | sort: asc
29 | filters:
30 | exclude:
31 | - "^docs:"
32 | - "^test:"
33 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG GO_VERSION=1.15
2 | ARG CGO_ENABLED=0
3 | ARG NODE_VERSION=14.11
4 |
5 | FROM golang:${GO_VERSION}-alpine AS go-builder
6 | WORKDIR /app
7 | COPY go.mod go.sum ./
8 | RUN go mod download
9 | COPY cmd ./cmd
10 | COPY pkg ./pkg
11 | RUN go build -o hetty ./cmd/hetty
12 |
13 | FROM node:${NODE_VERSION}-alpine AS node-builder
14 | WORKDIR /app
15 | COPY admin/package.json admin/yarn.lock ./
16 | RUN yarn install --frozen-lockfile
17 | COPY admin/ .
18 | ENV NEXT_TELEMETRY_DISABLED=1
19 | RUN yarn run export
20 |
21 | FROM alpine:3.12
22 | WORKDIR /app
23 | COPY --from=go-builder /app/hetty .
24 | COPY --from=node-builder /app/dist admin
25 |
26 | ENTRYPOINT ["./hetty", "-adminPath=./admin"]
27 |
28 | EXPOSE 80
--------------------------------------------------------------------------------
/pkg/api/schema.graphql:
--------------------------------------------------------------------------------
1 | type HttpRequestLog {
2 | id: ID!
3 | url: String!
4 | method: HttpMethod!
5 | proto: String!
6 | headers: [HttpHeader!]!
7 | body: String
8 | timestamp: Time!
9 | response: HttpResponseLog
10 | }
11 |
12 | type HttpResponseLog {
13 | requestId: ID!
14 | proto: String!
15 | status: String!
16 | statusCode: Int!
17 | body: String
18 | headers: [HttpHeader!]!
19 | }
20 |
21 | type HttpHeader {
22 | key: String!
23 | value: String!
24 | }
25 |
26 | type Query {
27 | httpRequestLog(id: ID!): HttpRequestLog
28 | httpRequestLogs: [HttpRequestLog!]!
29 | }
30 |
31 | enum HttpMethod {
32 | GET
33 | HEAD
34 | POST
35 | PUT
36 | DELETE
37 | CONNECT
38 | OPTIONS
39 | TRACE
40 | PATCH
41 | }
42 |
43 | scalar Time
44 |
--------------------------------------------------------------------------------
/admin/src/pages/proxy/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button, Typography } from "@material-ui/core";
3 | import ListIcon from "@material-ui/icons/List";
4 | import Link from "next/link";
5 |
6 | import Layout, { Page } from "../../components/Layout";
7 |
8 | function Index(): JSX.Element {
9 | return (
10 |
11 | Coming soon…
12 |
13 | }
19 | >
20 | View logs
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | export default Index;
28 |
--------------------------------------------------------------------------------
/admin/next.config.js:
--------------------------------------------------------------------------------
1 | const withCSS = require("@zeit/next-css");
2 | const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
3 |
4 | module.exports = withCSS({
5 | trailingSlash: true,
6 | async rewrites() {
7 | return [
8 | {
9 | source: "/api/:path/",
10 | destination: "http://localhost:8080/api/:path/",
11 | },
12 | ];
13 | },
14 | webpack: (config) => {
15 | config.module.rules.push({
16 | test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
17 | use: {
18 | loader: "url-loader",
19 | options: {
20 | limit: 100000,
21 | },
22 | },
23 | });
24 |
25 | config.plugins.push(
26 | new MonacoWebpackPlugin({
27 | languages: ["html", "json", "javascript"],
28 | filename: "static/[name].worker.js",
29 | })
30 | );
31 |
32 | return config;
33 | },
34 | });
35 |
--------------------------------------------------------------------------------
/pkg/proxy/modify.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import "net/http"
4 |
5 | var (
6 | nopReqModifier = func(req *http.Request) {}
7 | nopResModifier = func(res *http.Response) error { return nil }
8 | )
9 |
10 | // RequestModifyFunc defines a type for a function that can modify a HTTP
11 | // request before it's proxied.
12 | type RequestModifyFunc func(req *http.Request)
13 |
14 | // RequestModifyMiddleware defines a type for chaining request modifier
15 | // middleware.
16 | type RequestModifyMiddleware func(next RequestModifyFunc) RequestModifyFunc
17 |
18 | // ResponseModifyFunc defines a type for a function that can modify a HTTP
19 | // response before it's written back to the client.
20 | type ResponseModifyFunc func(res *http.Response) error
21 |
22 | // ResponseModifyMiddleware defines a type for chaining response modifier
23 | // middleware.
24 | type ResponseModifyMiddleware func(ResponseModifyFunc) ResponseModifyFunc
25 |
--------------------------------------------------------------------------------
/pkg/proxy/net.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "errors"
5 | "net"
6 | )
7 |
8 | var ErrAlreadyAccepted = errors.New("listener already accepted")
9 |
10 | // OnceListener implements net.Listener.
11 | //
12 | // Accepts a connection once and returns an error on subsequent
13 | // attempts.
14 | type OnceAcceptListener struct {
15 | c net.Conn
16 | }
17 |
18 | func (l *OnceAcceptListener) Accept() (net.Conn, error) {
19 | if l.c == nil {
20 | return nil, ErrAlreadyAccepted
21 | }
22 |
23 | c := l.c
24 | l.c = nil
25 |
26 | return c, nil
27 | }
28 |
29 | func (l *OnceAcceptListener) Close() error {
30 | return nil
31 | }
32 |
33 | func (l *OnceAcceptListener) Addr() net.Addr {
34 | return l.c.LocalAddr()
35 | }
36 |
37 | // ConnNotify embeds net.Conn and adds a channel field for notifying
38 | // that the connection was closed.
39 | type ConnNotify struct {
40 | net.Conn
41 | closed chan struct{}
42 | }
43 |
44 | func (c *ConnNotify) Close() {
45 | c.Conn.Close()
46 | c.closed <- struct{}{}
47 | }
48 |
--------------------------------------------------------------------------------
/admin/src/components/reqlog/HttpStatusCode.tsx:
--------------------------------------------------------------------------------
1 | import { Theme, withTheme } from "@material-ui/core";
2 | import { orange, red } from "@material-ui/core/colors";
3 | import FiberManualRecordIcon from "@material-ui/icons/FiberManualRecord";
4 |
5 | interface Props {
6 | status: number;
7 | theme: Theme;
8 | }
9 |
10 | function HttpStatusIcon({ status, theme }: Props): JSX.Element {
11 | const style = { marginTop: "-.25rem", verticalAlign: "middle" };
12 | switch (Math.floor(status / 100)) {
13 | case 2:
14 | case 3:
15 | return (
16 |
19 | );
20 | case 4:
21 | return (
22 |
23 | );
24 | case 5:
25 | return ;
26 | default:
27 | return ;
28 | }
29 | }
30 |
31 | export default withTheme(HttpStatusIcon);
32 |
--------------------------------------------------------------------------------
/admin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hetty-admin",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "export": "next build && next export -o dist"
10 | },
11 | "dependencies": {
12 | "@apollo/client": "^3.2.0",
13 | "@material-ui/core": "^4.11.0",
14 | "@material-ui/icons": "^4.9.1",
15 | "@material-ui/lab": "^4.0.0-alpha.56",
16 | "@zeit/next-css": "^1.0.1",
17 | "graphql": "^15.3.0",
18 | "monaco-editor": "^0.20.0",
19 | "monaco-editor-webpack-plugin": "^1.9.0",
20 | "next": "^9.5.3",
21 | "next-fonts": "^1.0.3",
22 | "react": "^16.13.1",
23 | "react-dom": "^16.13.1",
24 | "react-monaco-editor": "^0.34.0",
25 | "react-syntax-highlighter": "^13.5.3",
26 | "typescript": "^4.0.3"
27 | },
28 | "devDependencies": {
29 | "@types/node": "^14.11.1",
30 | "@types/react": "^16.9.49",
31 | "eslint": "^7.9.0",
32 | "eslint-config-prettier": "^6.11.0",
33 | "eslint-plugin-prettier": "^3.1.4",
34 | "prettier": "^2.1.2"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 David Stotijn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/admin/src/lib/theme.ts:
--------------------------------------------------------------------------------
1 | import { createMuiTheme } from "@material-ui/core/styles";
2 | import grey from "@material-ui/core/colors/grey";
3 | import teal from "@material-ui/core/colors/teal";
4 |
5 | const theme = createMuiTheme({
6 | palette: {
7 | type: "dark",
8 | primary: {
9 | main: grey[900],
10 | },
11 | secondary: {
12 | main: teal["A400"],
13 | },
14 | },
15 | typography: {
16 | h2: {
17 | fontFamily: "'JetBrains Mono', monospace",
18 | fontWeight: 600,
19 | },
20 | h3: {
21 | fontFamily: "'JetBrains Mono', monospace",
22 | fontWeight: 600,
23 | },
24 | h4: {
25 | fontFamily: "'JetBrains Mono', monospace",
26 | fontWeight: 600,
27 | },
28 | h5: {
29 | fontFamily: "'JetBrains Mono', monospace",
30 | fontWeight: 600,
31 | },
32 | h6: {
33 | fontFamily: "'JetBrains Mono', monospace",
34 | fontWeight: 600,
35 | },
36 | },
37 | overrides: {
38 | MuiTableCell: {
39 | stickyHeader: {
40 | backgroundColor: grey[900],
41 | },
42 | },
43 | },
44 | });
45 |
46 | export default theme;
47 |
--------------------------------------------------------------------------------
/admin/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { AppProps } from "next/app";
3 | import { ApolloProvider } from "@apollo/client";
4 | import Head from "next/head";
5 | import { ThemeProvider } from "@material-ui/core/styles";
6 | import CssBaseline from "@material-ui/core/CssBaseline";
7 |
8 | import theme from "../lib/theme";
9 | import { useApollo } from "../lib/graphql";
10 |
11 | function App({ Component, pageProps }: AppProps): JSX.Element {
12 | const apolloClient = useApollo(pageProps.initialApolloState);
13 |
14 | React.useEffect(() => {
15 | // Remove the server-side injected CSS.
16 | const jssStyles = document.querySelector("#jss-server-side");
17 | if (jssStyles) {
18 | jssStyles.parentElement.removeChild(jssStyles);
19 | }
20 | }, []);
21 |
22 | return (
23 |
24 |
25 | Hetty://
26 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | );
39 | }
40 |
41 | export default App;
42 |
--------------------------------------------------------------------------------
/pkg/db/cayley/bolt.go:
--------------------------------------------------------------------------------
1 | package cayley
2 |
3 | import (
4 | "os"
5 | "path/filepath"
6 |
7 | "github.com/cayleygraph/cayley/clog"
8 | "github.com/cayleygraph/cayley/graph"
9 | hkv "github.com/hidal-go/hidalgo/kv"
10 | "github.com/hidal-go/hidalgo/kv/bolt"
11 | )
12 |
13 | const Type = bolt.Name
14 |
15 | func boltFilePath(path, filename string) string {
16 | return filepath.Join(path, filename)
17 | }
18 |
19 | func boltCreate(path string, opt graph.Options) (hkv.KV, error) {
20 | filename, err := opt.StringKey("filename", "indexes.bolt")
21 | if err != nil {
22 | return nil, err
23 | }
24 |
25 | err = os.MkdirAll(path, 0700)
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | db, err := bolt.Open(boltFilePath(path, filename), nil)
31 | if err != nil {
32 | clog.Errorf("Error: couldn't create Bolt database: %v", err)
33 | return nil, err
34 | }
35 |
36 | return db, nil
37 | }
38 |
39 | func boltOpen(path string, opt graph.Options) (hkv.KV, error) {
40 | filename, err := opt.StringKey("filename", "indexes.bolt")
41 | if err != nil {
42 | return nil, err
43 | }
44 |
45 | db, err := bolt.Open(boltFilePath(path, filename), nil)
46 | if err != nil {
47 | clog.Errorf("Error, couldn't open! %v", err)
48 | return nil, err
49 | }
50 |
51 | bdb := db.DB()
52 | bdb.NoSync, err = opt.BoolKey("nosync", false)
53 | if err != nil {
54 | db.Close()
55 | return nil, err
56 | }
57 |
58 | bdb.NoGrowSync = bdb.NoSync
59 | if bdb.NoSync {
60 | clog.Infof("Running in nosync mode")
61 | }
62 |
63 | return db, nil
64 | }
65 |
--------------------------------------------------------------------------------
/admin/src/components/reqlog/Editor.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from "next/dynamic";
2 | const MonacoEditor = dynamic(import("react-monaco-editor"), { ssr: false });
3 |
4 | const monacoOptions = {
5 | readOnly: true,
6 | wordWrap: "on",
7 | minimap: {
8 | enabled: false,
9 | },
10 | };
11 |
12 | type language = "html" | "typescript" | "json";
13 |
14 | function editorDidMount() {
15 | return ((window as any).MonacoEnvironment.getWorkerUrl = (
16 | moduleId,
17 | label
18 | ) => {
19 | if (label === "json") return "/_next/static/json.worker.js";
20 | if (label === "html") return "/_next/static/html.worker.js";
21 | if (label === "javascript") return "/_next/static/ts.worker.js";
22 |
23 | return "/_next/static/editor.worker.js";
24 | });
25 | }
26 |
27 | function languageForContentType(contentType: string): language {
28 | switch (contentType) {
29 | case "text/html":
30 | return "html";
31 | case "application/json":
32 | case "application/json; charset=utf-8":
33 | return "json";
34 | case "application/javascript":
35 | case "application/javascript; charset=utf-8":
36 | return "typescript";
37 | default:
38 | return;
39 | }
40 | }
41 |
42 | interface Props {
43 | content: string;
44 | contentType: string;
45 | }
46 |
47 | function Editor({ content, contentType }: Props): JSX.Element {
48 | return (
49 |
57 | );
58 | }
59 |
60 | export default Editor;
61 |
--------------------------------------------------------------------------------
/admin/src/lib/graphql.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from "react";
2 | import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
3 | import { concatPagination } from "@apollo/client/utilities";
4 |
5 | let apolloClient;
6 |
7 | function createApolloClient() {
8 | return new ApolloClient({
9 | ssrMode: typeof window === "undefined",
10 | link: new HttpLink({
11 | uri: "/api/graphql/",
12 | }),
13 | cache: new InMemoryCache({
14 | typePolicies: {
15 | Query: {
16 | fields: {
17 | allPosts: concatPagination(),
18 | },
19 | },
20 | },
21 | }),
22 | });
23 | }
24 |
25 | export function initializeApollo(initialState = null) {
26 | const _apolloClient = apolloClient ?? createApolloClient();
27 |
28 | // If your page has Next.js data fetching methods that use Apollo Client, the initial state
29 | // gets hydrated here
30 | if (initialState) {
31 | // Get existing cache, loaded during client side data fetching
32 | const existingCache = _apolloClient.extract();
33 | // Restore the cache using the data passed from getStaticProps/getServerSideProps
34 | // combined with the existing cached data
35 | _apolloClient.cache.restore({ ...existingCache, ...initialState });
36 | }
37 | // For SSG and SSR always create a new Apollo Client
38 | if (typeof window === "undefined") return _apolloClient;
39 | // Create the Apollo Client once in the client
40 | if (!apolloClient) apolloClient = _apolloClient;
41 |
42 | return _apolloClient;
43 | }
44 |
45 | export function useApollo(initialState) {
46 | const store = useMemo(() => initializeApollo(initialState), [initialState]);
47 | return store;
48 | }
49 |
--------------------------------------------------------------------------------
/admin/src/components/reqlog/ResponseDetail.tsx:
--------------------------------------------------------------------------------
1 | import { Typography, Box, Divider } from "@material-ui/core";
2 |
3 | import HttpStatusIcon from "./HttpStatusCode";
4 | import Editor from "./Editor";
5 | import HttpHeadersTable from "./HttpHeadersTable";
6 |
7 | interface Props {
8 | response: {
9 | proto: string;
10 | statusCode: number;
11 | status: string;
12 | headers: Array<{ key: string; value: string }>;
13 | body?: string;
14 | };
15 | }
16 |
17 | function ResponseDetail({ response }: Props): JSX.Element {
18 | const contentType = response.headers.find(
19 | (header) => header.key === "Content-Type"
20 | )?.value;
21 | return (
22 |
23 |
24 |
29 | Response
30 |
31 |
35 | {" "}
36 |
37 |
42 | {response.proto}
43 |
44 | {" "}
45 | {response.status}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | {response.body && (
56 |
57 | )}
58 |
59 | );
60 | }
61 |
62 | export default ResponseDetail;
63 |
--------------------------------------------------------------------------------
/admin/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Document, { Html, Head, Main, NextScript } from "next/document";
3 | import { ServerStyleSheets } from "@material-ui/core/styles";
4 |
5 | import theme from "../lib/theme";
6 |
7 | export default class MyDocument extends Document {
8 | render() {
9 | return (
10 |
11 |
12 |
13 |
14 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 | }
31 |
32 | // `getInitialProps` belongs to `_document` (instead of `_app`),
33 | // it's compatible with server-side generation (SSG).
34 | MyDocument.getInitialProps = async (ctx) => {
35 | // Render app and page and get the context of the page with collected side effects.
36 | const sheets = new ServerStyleSheets();
37 | const originalRenderPage = ctx.renderPage;
38 |
39 | ctx.renderPage = () =>
40 | originalRenderPage({
41 | enhanceApp: (App) => (props) => sheets.collect(),
42 | });
43 |
44 | const initialProps = await Document.getInitialProps(ctx);
45 |
46 | return {
47 | ...initialProps,
48 | // Styles fragment is rendered after the app and page rendering finish.
49 | styles: [
50 | ...React.Children.toArray(initialProps.styles),
51 | sheets.getStyleElement(),
52 | ],
53 | };
54 | };
55 |
--------------------------------------------------------------------------------
/gqlgen.yml:
--------------------------------------------------------------------------------
1 | # Where are all the schema files located? globs are supported eg src/**/*.graphqls
2 | schema:
3 | - pkg/api/schema.graphql
4 |
5 | # Where should the generated server code go?
6 | exec:
7 | filename: pkg/api/generated.go
8 | package: api
9 |
10 | # Uncomment to enable federation
11 | # federation:
12 | # filename: graph/generated/federation.go
13 | # package: generated
14 |
15 | # Where should any generated models go?
16 | model:
17 | filename: pkg/api/models_gen.go
18 | package: api
19 |
20 | # Where should the resolver implementations go?
21 | resolver:
22 | layout: single-file
23 | filename: pkg/api/resolvers.go
24 | dir: pkg/api
25 | package: api
26 |
27 | # Optional: turn on use `gqlgen:"fieldName"` tags in your models
28 | # struct_tag: json
29 |
30 | # Optional: turn on to use []Thing instead of []*Thing
31 | omit_slice_element_pointers: true
32 | # Optional: set to speed up generation time by not performing a final validation pass.
33 | # skip_validation: true
34 |
35 | # gqlgen will search for any type names in the schema in these go packages
36 | # if they match it will use them, otherwise it will generate them.
37 | # autobind:
38 | # - "github.com/dstotijn/hetty/graph/model"
39 |
40 | # This section declares type mapping between the GraphQL and go type systems
41 | #
42 | # The first line in each type will be used as defaults for resolver arguments and
43 | # modelgen, the others will be allowed when binding to fields. Configure them to
44 | # your liking
45 | # models:
46 | # ID:
47 | # model:
48 | # - github.com/99designs/gqlgen/graphql.ID
49 | # - github.com/99designs/gqlgen/graphql.Int
50 | # - github.com/99designs/gqlgen/graphql.Int64
51 | # - github.com/99designs/gqlgen/graphql.Int32
52 | # Int:
53 | # model:
54 | # - github.com/99designs/gqlgen/graphql.Int
55 | # - github.com/99designs/gqlgen/graphql.Int64
56 | # - github.com/99designs/gqlgen/graphql.Int32
57 |
--------------------------------------------------------------------------------
/admin/src/components/reqlog/LogsOverview.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 | import { gql, useQuery } from "@apollo/client";
3 | import { useState } from "react";
4 | import { Box, Typography, CircularProgress } from "@material-ui/core";
5 | import Alert from "@material-ui/lab/Alert";
6 |
7 | import RequestList from "./RequestList";
8 | import LogDetail from "./LogDetail";
9 | import CenteredPaper from "../CenteredPaper";
10 |
11 | const HTTP_REQUEST_LOGS = gql`
12 | query HttpRequestLogs {
13 | httpRequestLogs {
14 | id
15 | method
16 | url
17 | timestamp
18 | response {
19 | status
20 | statusCode
21 | }
22 | }
23 | }
24 | `;
25 |
26 | function LogsOverview(): JSX.Element {
27 | const router = useRouter();
28 | const detailReqLogId = router.query.id as string;
29 | console.log(detailReqLogId);
30 |
31 | const { loading, error, data } = useQuery(HTTP_REQUEST_LOGS, {
32 | pollInterval: 1000,
33 | });
34 |
35 | const handleLogClick = (reqId: string) => {
36 | router.push("/proxy/logs?id=" + reqId, undefined, {
37 | shallow: false,
38 | });
39 | };
40 |
41 | if (loading) {
42 | return ;
43 | }
44 | if (error) {
45 | return Error fetching logs: {error.message};
46 | }
47 |
48 | const { httpRequestLogs: logs } = data;
49 |
50 | return (
51 |
52 |
53 |
58 |
59 |
60 | {detailReqLogId && }
61 | {logs.length !== 0 && !detailReqLogId && (
62 |
63 | Select a log entry…
64 |
65 | )}
66 |
67 |
68 | );
69 | }
70 |
71 | export default LogsOverview;
72 |
--------------------------------------------------------------------------------
/admin/src/components/reqlog/LogDetail.tsx:
--------------------------------------------------------------------------------
1 | import { gql, useQuery } from "@apollo/client";
2 | import { Box, Grid, Paper, CircularProgress } from "@material-ui/core";
3 |
4 | import ResponseDetail from "./ResponseDetail";
5 | import RequestDetail from "./RequestDetail";
6 | import Alert from "@material-ui/lab/Alert";
7 |
8 | const HTTP_REQUEST_LOG = gql`
9 | query HttpRequestLog($id: ID!) {
10 | httpRequestLog(id: $id) {
11 | id
12 | method
13 | url
14 | proto
15 | headers {
16 | key
17 | value
18 | }
19 | body
20 | response {
21 | proto
22 | headers {
23 | key
24 | value
25 | }
26 | status
27 | statusCode
28 | body
29 | }
30 | }
31 | }
32 | `;
33 |
34 | interface Props {
35 | requestId: string;
36 | }
37 |
38 | function LogDetail({ requestId: id }: Props): JSX.Element {
39 | const { loading, error, data } = useQuery(HTTP_REQUEST_LOG, {
40 | variables: { id },
41 | });
42 |
43 | if (loading) {
44 | return ;
45 | }
46 | if (error) {
47 | return (
48 |
49 | Error fetching logs details: {error.message}
50 |
51 | );
52 | }
53 |
54 | if (!data.httpRequestLog) {
55 | return (
56 |
57 | Request {id} was not found.
58 |
59 | );
60 | }
61 |
62 | const { method, url, proto, headers, body, response } = data.httpRequestLog;
63 |
64 | return (
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | {response && (
74 |
75 |
76 |
77 | )}
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | export default LogDetail;
85 |
--------------------------------------------------------------------------------
/admin/src/components/reqlog/RequestDetail.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Typography,
4 | Box,
5 | createStyles,
6 | makeStyles,
7 | Theme,
8 | Divider,
9 | } from "@material-ui/core";
10 |
11 | import HttpHeadersTable from "./HttpHeadersTable";
12 | import Editor from "./Editor";
13 |
14 | const useStyles = makeStyles((theme: Theme) =>
15 | createStyles({
16 | requestTitle: {
17 | width: "calc(100% - 80px)",
18 | fontSize: "1rem",
19 | wordBreak: "break-all",
20 | whiteSpace: "pre-wrap",
21 | },
22 | headersTable: {
23 | tableLayout: "fixed",
24 | width: "100%",
25 | },
26 | headerKeyCell: {
27 | verticalAlign: "top",
28 | width: "30%",
29 | fontWeight: "bold",
30 | },
31 | headerValueCell: {
32 | width: "70%",
33 | verticalAlign: "top",
34 | wordBreak: "break-all",
35 | whiteSpace: "pre-wrap",
36 | },
37 | })
38 | );
39 |
40 | interface Props {
41 | request: {
42 | method: string;
43 | url: string;
44 | proto: string;
45 | headers: Array<{ key: string; value: string }>;
46 | body?: string;
47 | };
48 | }
49 |
50 | function RequestDetail({ request }: Props): JSX.Element {
51 | const { method, url, proto, headers, body } = request;
52 | const classes = useStyles();
53 |
54 | const contentType = headers.find((header) => header.key === "Content-Type")
55 | ?.value;
56 | const parsedUrl = new URL(url);
57 |
58 | return (
59 |
60 |
61 |
66 | Request
67 |
68 |
69 | {method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)}{" "}
70 |
75 | {proto}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | {body &&
}
87 |
88 | );
89 | }
90 |
91 | export default RequestDetail;
92 |
--------------------------------------------------------------------------------
/pkg/api/models_gen.go:
--------------------------------------------------------------------------------
1 | // Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
2 |
3 | package api
4 |
5 | import (
6 | "fmt"
7 | "io"
8 | "strconv"
9 | "time"
10 | )
11 |
12 | type HTTPHeader struct {
13 | Key string `json:"key"`
14 | Value string `json:"value"`
15 | }
16 |
17 | type HTTPRequestLog struct {
18 | ID string `json:"id"`
19 | URL string `json:"url"`
20 | Method HTTPMethod `json:"method"`
21 | Proto string `json:"proto"`
22 | Headers []HTTPHeader `json:"headers"`
23 | Body *string `json:"body"`
24 | Timestamp time.Time `json:"timestamp"`
25 | Response *HTTPResponseLog `json:"response"`
26 | }
27 |
28 | type HTTPResponseLog struct {
29 | RequestID string `json:"requestId"`
30 | Proto string `json:"proto"`
31 | Status string `json:"status"`
32 | StatusCode int `json:"statusCode"`
33 | Body *string `json:"body"`
34 | Headers []HTTPHeader `json:"headers"`
35 | }
36 |
37 | type HTTPMethod string
38 |
39 | const (
40 | HTTPMethodGet HTTPMethod = "GET"
41 | HTTPMethodHead HTTPMethod = "HEAD"
42 | HTTPMethodPost HTTPMethod = "POST"
43 | HTTPMethodPut HTTPMethod = "PUT"
44 | HTTPMethodDelete HTTPMethod = "DELETE"
45 | HTTPMethodConnect HTTPMethod = "CONNECT"
46 | HTTPMethodOptions HTTPMethod = "OPTIONS"
47 | HTTPMethodTrace HTTPMethod = "TRACE"
48 | HTTPMethodPatch HTTPMethod = "PATCH"
49 | )
50 |
51 | var AllHTTPMethod = []HTTPMethod{
52 | HTTPMethodGet,
53 | HTTPMethodHead,
54 | HTTPMethodPost,
55 | HTTPMethodPut,
56 | HTTPMethodDelete,
57 | HTTPMethodConnect,
58 | HTTPMethodOptions,
59 | HTTPMethodTrace,
60 | HTTPMethodPatch,
61 | }
62 |
63 | func (e HTTPMethod) IsValid() bool {
64 | switch e {
65 | case HTTPMethodGet, HTTPMethodHead, HTTPMethodPost, HTTPMethodPut, HTTPMethodDelete, HTTPMethodConnect, HTTPMethodOptions, HTTPMethodTrace, HTTPMethodPatch:
66 | return true
67 | }
68 | return false
69 | }
70 |
71 | func (e HTTPMethod) String() string {
72 | return string(e)
73 | }
74 |
75 | func (e *HTTPMethod) UnmarshalGQL(v interface{}) error {
76 | str, ok := v.(string)
77 | if !ok {
78 | return fmt.Errorf("enums must be strings")
79 | }
80 |
81 | *e = HTTPMethod(str)
82 | if !e.IsValid() {
83 | return fmt.Errorf("%s is not a valid HttpMethod", str)
84 | }
85 | return nil
86 | }
87 |
88 | func (e HTTPMethod) MarshalGQL(w io.Writer) {
89 | fmt.Fprint(w, strconv.Quote(e.String()))
90 | }
91 |
--------------------------------------------------------------------------------
/admin/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Box,
3 | Button,
4 | createStyles,
5 | IconButton,
6 | makeStyles,
7 | Theme,
8 | Typography,
9 | } from "@material-ui/core";
10 | import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet";
11 | import SendIcon from "@material-ui/icons/Send";
12 | import Link from "next/link";
13 |
14 | import Layout, { Page } from "../components/Layout";
15 |
16 | const useStyles = makeStyles((theme: Theme) =>
17 | createStyles({
18 | titleHighlight: {
19 | color: theme.palette.secondary.main,
20 | },
21 | subtitle: {
22 | fontSize: "1.6rem",
23 | width: "60%",
24 | lineHeight: 2,
25 | marginBottom: theme.spacing(5),
26 | },
27 | button: {
28 | marginRight: theme.spacing(2),
29 | },
30 | })
31 | );
32 |
33 | function Index(): JSX.Element {
34 | const classes = useStyles();
35 | return (
36 |
37 |
38 |
39 |
40 | Hetty://
41 |
42 | The simple HTTP toolkit for security research.
43 |
44 |
45 |
46 | What if security testing was intuitive, powerful, and good looking?
47 | What if it was free, instead of $400 per year?{" "}
48 | Hetty is listening on{" "}
49 | :8080…
50 |
51 |
52 |
53 | }
60 | >
61 | Setup proxy
62 |
63 |
64 |
65 | }
72 | >
73 | Send HTTP requests
74 |
75 |
76 |
77 |
78 |
79 | );
80 | }
81 |
82 | export default Index;
83 |
--------------------------------------------------------------------------------
/pkg/api/resolvers.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | //go:generate go run github.com/99designs/gqlgen
4 |
5 | import (
6 | "context"
7 | "fmt"
8 |
9 | "github.com/google/uuid"
10 |
11 | "github.com/dstotijn/hetty/pkg/reqlog"
12 | )
13 |
14 | type Resolver struct {
15 | RequestLogService *reqlog.Service
16 | }
17 |
18 | type queryResolver struct{ *Resolver }
19 |
20 | func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }
21 |
22 | func (r *queryResolver) HTTPRequestLogs(ctx context.Context) ([]HTTPRequestLog, error) {
23 | reqs, err := r.RequestLogService.FindAllRequests(ctx)
24 | if err != nil {
25 | return nil, fmt.Errorf("could not query repository for requests: %v", err)
26 | }
27 | logs := make([]HTTPRequestLog, len(reqs))
28 |
29 | for i, req := range reqs {
30 | req, err := parseRequestLog(req)
31 | if err != nil {
32 | return nil, err
33 | }
34 | logs[i] = req
35 | }
36 |
37 | return logs, nil
38 | }
39 |
40 | func (r *queryResolver) HTTPRequestLog(ctx context.Context, id string) (*HTTPRequestLog, error) {
41 | reqLogID, err := uuid.Parse(id)
42 | if err != nil {
43 | return nil, fmt.Errorf("invalid id: %v", err)
44 | }
45 | log, err := r.RequestLogService.FindRequestLogByID(ctx, reqLogID)
46 | if err == reqlog.ErrRequestNotFound {
47 | return nil, nil
48 | }
49 | if err != nil {
50 | return nil, fmt.Errorf("could not get request by ID: %v", err)
51 | }
52 | req, err := parseRequestLog(log)
53 | if err != nil {
54 | return nil, err
55 | }
56 |
57 | return &req, nil
58 | }
59 |
60 | func parseRequestLog(req reqlog.Request) (HTTPRequestLog, error) {
61 | method := HTTPMethod(req.Request.Method)
62 | if !method.IsValid() {
63 | return HTTPRequestLog{}, fmt.Errorf("request has invalid method: %v", method)
64 | }
65 |
66 | log := HTTPRequestLog{
67 | ID: req.ID.String(),
68 | URL: req.Request.URL.String(),
69 | Proto: req.Request.Proto,
70 | Method: method,
71 | Timestamp: req.Timestamp,
72 | }
73 |
74 | if len(req.Body) > 0 {
75 | reqBody := string(req.Body)
76 | log.Body = &reqBody
77 | }
78 |
79 | if req.Request.Header != nil {
80 | log.Headers = make([]HTTPHeader, 0)
81 | for key, values := range req.Request.Header {
82 | for _, value := range values {
83 | log.Headers = append(log.Headers, HTTPHeader{
84 | Key: key,
85 | Value: value,
86 | })
87 | }
88 | }
89 | }
90 |
91 | if req.Response != nil {
92 | log.Response = &HTTPResponseLog{
93 | RequestID: req.ID.String(),
94 | Proto: req.Response.Response.Proto,
95 | Status: req.Response.Response.Status,
96 | StatusCode: req.Response.Response.StatusCode,
97 | }
98 | if len(req.Response.Body) > 0 {
99 | resBody := string(req.Response.Body)
100 | log.Response.Body = &resBody
101 | }
102 | if req.Response.Response.Header != nil {
103 | log.Response.Headers = make([]HTTPHeader, 0)
104 | for key, values := range req.Response.Response.Header {
105 | for _, value := range values {
106 | log.Response.Headers = append(log.Response.Headers, HTTPHeader{
107 | Key: key,
108 | Value: value,
109 | })
110 | }
111 | }
112 | }
113 | }
114 |
115 | return log, nil
116 | }
117 |
--------------------------------------------------------------------------------
/admin/public/style.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "JetBrains Mono";
3 | src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold-Italic.woff2")
4 | format("woff2"),
5 | url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Bold-Italic.woff")
6 | format("woff");
7 | font-weight: 700;
8 | font-style: italic;
9 | font-display: swap;
10 | }
11 |
12 | @font-face {
13 | font-family: "JetBrains Mono";
14 | src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold.woff2")
15 | format("woff2"),
16 | url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Bold.woff")
17 | format("woff");
18 | font-weight: 700;
19 | font-style: normal;
20 | font-display: swap;
21 | }
22 |
23 | @font-face {
24 | font-family: "JetBrains Mono";
25 | src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-ExtraBold-Italic.woff2")
26 | format("woff2"),
27 | url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-ExtraBold-Italic.woff")
28 | format("woff");
29 | font-weight: 800;
30 | font-style: italic;
31 | font-display: swap;
32 | }
33 |
34 | @font-face {
35 | font-family: "JetBrains Mono";
36 | src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-ExtraBold.woff2")
37 | format("woff2"),
38 | url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-ExtraBold.woff")
39 | format("woff");
40 | font-weight: 800;
41 | font-style: normal;
42 | font-display: swap;
43 | }
44 |
45 | @font-face {
46 | font-family: "JetBrains Mono";
47 | src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Italic.woff2")
48 | format("woff2"),
49 | url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Italic.woff")
50 | format("woff");
51 | font-weight: 400;
52 | font-style: italic;
53 | font-display: swap;
54 | }
55 |
56 | @font-face {
57 | font-family: "JetBrains Mono";
58 | src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Medium-Italic.woff2")
59 | format("woff2"),
60 | url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Medium-Italic.woff")
61 | format("woff");
62 | font-weight: 500;
63 | font-style: italic;
64 | font-display: swap;
65 | }
66 |
67 | @font-face {
68 | font-family: "JetBrains Mono";
69 | src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Medium.woff2")
70 | format("woff2"),
71 | url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Medium.woff")
72 | format("woff");
73 | font-weight: 500;
74 | font-style: normal;
75 | font-display: swap;
76 | }
77 |
78 | @font-face {
79 | font-family: "JetBrains Mono";
80 | src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff2")
81 | format("woff2"),
82 | url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Regular.woff")
83 | format("woff");
84 | font-weight: 400;
85 | font-style: normal;
86 | font-display: swap;
87 | }
88 |
89 | code {
90 | font-family: "JetBrains Mono", monospace;
91 | }
92 |
--------------------------------------------------------------------------------
/admin/src/components/reqlog/HttpHeadersTable.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | makeStyles,
3 | Theme,
4 | createStyles,
5 | Table,
6 | TableBody,
7 | TableCell,
8 | TableContainer,
9 | TableRow,
10 | Snackbar,
11 | } from "@material-ui/core";
12 | import { Alert } from "@material-ui/lab";
13 | import React, { useState } from "react";
14 |
15 | const useStyles = makeStyles((theme: Theme) => {
16 | const paddingX = 0;
17 | const paddingY = theme.spacing(1) / 3;
18 | const tableCell = {
19 | paddingLeft: paddingX,
20 | paddingRight: paddingX,
21 | paddingTop: paddingY,
22 | paddingBottom: paddingY,
23 | verticalAlign: "top",
24 | border: "none",
25 | whiteSpace: "nowrap" as any,
26 | overflow: "hidden",
27 | textOverflow: "ellipsis",
28 | "&:hover": {
29 | color: theme.palette.secondary.main,
30 | whiteSpace: "inherit" as any,
31 | overflow: "inherit",
32 | textOverflow: "inherit",
33 | cursor: "copy",
34 | },
35 | };
36 | return createStyles({
37 | root: {},
38 | table: {
39 | tableLayout: "fixed",
40 | width: "100%",
41 | },
42 | keyCell: {
43 | ...tableCell,
44 | paddingRight: theme.spacing(1),
45 | width: "40%",
46 | fontWeight: "bold",
47 | fontSize: ".75rem",
48 | },
49 | valueCell: {
50 | ...tableCell,
51 | width: "60%",
52 | border: "none",
53 | fontSize: ".75rem",
54 | },
55 | });
56 | });
57 |
58 | interface Props {
59 | headers: Array<{ key: string; value: string }>;
60 | }
61 |
62 | function HttpHeadersTable({ headers }: Props): JSX.Element {
63 | const classes = useStyles();
64 |
65 | const [open, setOpen] = useState(false);
66 |
67 | const handleClick = (e: React.MouseEvent) => {
68 | e.preventDefault();
69 |
70 | const r = document.createRange();
71 | r.selectNode(e.currentTarget);
72 | window.getSelection().removeAllRanges();
73 | window.getSelection().addRange(r);
74 | document.execCommand("copy");
75 | window.getSelection().removeAllRanges();
76 |
77 | setOpen(true);
78 | };
79 |
80 | const handleClose = (event?: React.SyntheticEvent, reason?: string) => {
81 | if (reason === "clickaway") {
82 | return;
83 | }
84 |
85 | setOpen(false);
86 | };
87 |
88 | return (
89 |
90 |
91 |
92 | Copied to clipboard.
93 |
94 |
95 |
96 |
97 |
98 | {headers.map(({ key, value }, index) => (
99 |
100 |
106 | {key}:
107 |
108 |
109 | {value}
110 |
111 |
112 | ))}
113 |
114 |
115 |
116 |
117 | );
118 | }
119 |
120 | export default HttpHeadersTable;
121 |
--------------------------------------------------------------------------------
/cmd/hetty/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 | "flag"
7 | "log"
8 | "net"
9 | "net/http"
10 | "os"
11 | "strings"
12 |
13 | rice "github.com/GeertJohan/go.rice"
14 | "github.com/dstotijn/hetty/pkg/api"
15 | "github.com/dstotijn/hetty/pkg/db/cayley"
16 | "github.com/dstotijn/hetty/pkg/proxy"
17 | "github.com/dstotijn/hetty/pkg/reqlog"
18 |
19 | "github.com/99designs/gqlgen/graphql/handler"
20 | "github.com/99designs/gqlgen/graphql/playground"
21 | "github.com/gorilla/mux"
22 | )
23 |
24 | var (
25 | caCertFile string
26 | caKeyFile string
27 | dbFile string
28 | addr string
29 | adminPath string
30 | )
31 |
32 | func main() {
33 | flag.StringVar(&caCertFile, "cert", "", "CA certificate file path")
34 | flag.StringVar(&caKeyFile, "key", "", "CA private key file path")
35 | flag.StringVar(&dbFile, "db", "hetty.db", "Database file path")
36 | flag.StringVar(&addr, "addr", ":80", "TCP address to listen on, in the form \"host:port\"")
37 | flag.StringVar(&adminPath, "adminPath", "", "File path to admin build")
38 | flag.Parse()
39 |
40 | tlsCA, err := tls.LoadX509KeyPair(caCertFile, caKeyFile)
41 | if err != nil {
42 | log.Fatalf("[FATAL] Could not load CA key pair: %v", err)
43 | }
44 |
45 | caCert, err := x509.ParseCertificate(tlsCA.Certificate[0])
46 | if err != nil {
47 | log.Fatalf("[FATAL] Could not parse CA: %v", err)
48 | }
49 |
50 | db, err := cayley.NewDatabase(dbFile)
51 | if err != nil {
52 | log.Fatalf("[FATAL] Could not initialize database: %v", err)
53 | }
54 | defer db.Close()
55 |
56 | reqLogService := reqlog.NewService(db)
57 |
58 | p, err := proxy.NewProxy(caCert, tlsCA.PrivateKey)
59 | if err != nil {
60 | log.Fatalf("[FATAL] Could not create Proxy: %v", err)
61 | }
62 |
63 | p.UseRequestModifier(reqLogService.RequestModifier)
64 | p.UseResponseModifier(reqLogService.ResponseModifier)
65 |
66 | var adminHandler http.Handler
67 | if adminPath == "" {
68 | // Used for embedding with `rice`.
69 | box, err := rice.FindBox("../../admin/dist")
70 | if err != nil {
71 | log.Fatalf("[FATAL] Could not find embedded admin resources: %v", err)
72 | }
73 | adminHandler = http.FileServer(box.HTTPBox())
74 | } else {
75 | adminHandler = http.FileServer(http.Dir(adminPath))
76 | }
77 |
78 | router := mux.NewRouter().SkipClean(true)
79 |
80 | adminRouter := router.MatcherFunc(func(req *http.Request, match *mux.RouteMatch) bool {
81 | hostname, _ := os.Hostname()
82 | host, _, _ := net.SplitHostPort(req.Host)
83 | return strings.EqualFold(host, hostname) || (req.Host == "hetty.proxy" || req.Host == "localhost:8080")
84 | }).Subrouter().StrictSlash(true)
85 |
86 | // GraphQL server.
87 | adminRouter.Path("/api/playground/").Handler(playground.Handler("GraphQL Playground", "/api/graphql/"))
88 | adminRouter.Path("/api/graphql/").Handler(handler.NewDefaultServer(api.NewExecutableSchema(api.Config{Resolvers: &api.Resolver{
89 | RequestLogService: reqLogService,
90 | }})))
91 |
92 | // Admin interface.
93 | adminRouter.PathPrefix("").Handler(adminHandler)
94 |
95 | // Fallback (default) is the Proxy handler.
96 | router.PathPrefix("").Handler(p)
97 |
98 | s := &http.Server{
99 | Addr: addr,
100 | Handler: router,
101 | TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}, // Disable HTTP/2
102 | }
103 |
104 | log.Printf("[INFO] Running server on %v ...", addr)
105 | err = s.ListenAndServe()
106 | if err != nil && err != http.ErrServerClosed {
107 | log.Fatalf("[FATAL] HTTP server closed: %v", err)
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/pkg/reqlog/reqlog.go:
--------------------------------------------------------------------------------
1 | package reqlog
2 |
3 | import (
4 | "bytes"
5 | "compress/gzip"
6 | "context"
7 | "errors"
8 | "fmt"
9 | "io/ioutil"
10 | "log"
11 | "net/http"
12 | "time"
13 |
14 | "github.com/dstotijn/hetty/pkg/proxy"
15 | "github.com/google/uuid"
16 | )
17 |
18 | var ErrRequestNotFound = errors.New("reqlog: request not found")
19 |
20 | type Request struct {
21 | ID uuid.UUID
22 | Request http.Request
23 | Body []byte
24 | Timestamp time.Time
25 | Response *Response
26 | }
27 |
28 | type Response struct {
29 | RequestID uuid.UUID
30 | Response http.Response
31 | Body []byte
32 | Timestamp time.Time
33 | }
34 |
35 | type Service struct {
36 | repo Repository
37 | }
38 |
39 | func NewService(repo Repository) *Service {
40 | return &Service{repo}
41 | }
42 |
43 | func (svc *Service) FindAllRequests(ctx context.Context) ([]Request, error) {
44 | return svc.repo.FindAllRequestLogs(ctx)
45 | }
46 |
47 | func (svc *Service) FindRequestLogByID(ctx context.Context, id uuid.UUID) (Request, error) {
48 | return svc.repo.FindRequestLogByID(ctx, id)
49 | }
50 |
51 | func (svc *Service) addRequest(ctx context.Context, reqID uuid.UUID, req http.Request, body []byte) error {
52 | reqLog := Request{
53 | ID: reqID,
54 | Request: req,
55 | Body: body,
56 | Timestamp: time.Now(),
57 | }
58 |
59 | return svc.repo.AddRequestLog(ctx, reqLog)
60 | }
61 |
62 | func (svc *Service) addResponse(ctx context.Context, reqID uuid.UUID, res http.Response, body []byte) error {
63 | if res.Header.Get("Content-Encoding") == "gzip" {
64 | gzipReader, err := gzip.NewReader(bytes.NewBuffer(body))
65 | if err != nil {
66 | return fmt.Errorf("reqlog: could not create gzip reader: %v", err)
67 | }
68 | defer gzipReader.Close()
69 | body, err = ioutil.ReadAll(gzipReader)
70 | if err != nil {
71 | return fmt.Errorf("reqlog: could not read gzipped response body: %v", err)
72 | }
73 | }
74 |
75 | resLog := Response{
76 | RequestID: reqID,
77 | Response: res,
78 | Body: body,
79 | Timestamp: time.Now(),
80 | }
81 |
82 | return svc.repo.AddResponseLog(ctx, resLog)
83 | }
84 |
85 | func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc {
86 | return func(req *http.Request) {
87 | next(req)
88 |
89 | clone := req.Clone(req.Context())
90 | var body []byte
91 | if req.Body != nil {
92 | // TODO: Use io.LimitReader.
93 | var err error
94 | body, err = ioutil.ReadAll(req.Body)
95 | if err != nil {
96 | log.Printf("[ERROR] Could not read request body for logging: %v", err)
97 | return
98 | }
99 | req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
100 | }
101 |
102 | reqID, _ := req.Context().Value(proxy.ReqIDKey).(uuid.UUID)
103 | if reqID == uuid.Nil {
104 | log.Println("[ERROR] Request is missing a related request ID")
105 | return
106 | }
107 |
108 | go func() {
109 | if err := svc.addRequest(context.Background(), reqID, *clone, body); err != nil {
110 | log.Printf("[ERROR] Could not store request log: %v", err)
111 | }
112 | }()
113 | }
114 | }
115 |
116 | func (svc *Service) ResponseModifier(next proxy.ResponseModifyFunc) proxy.ResponseModifyFunc {
117 | return func(res *http.Response) error {
118 | if err := next(res); err != nil {
119 | return err
120 | }
121 |
122 | reqID, _ := res.Request.Context().Value(proxy.ReqIDKey).(uuid.UUID)
123 | if reqID == uuid.Nil {
124 | return errors.New("reqlog: request is missing ID")
125 | }
126 |
127 | clone := *res
128 |
129 | // TODO: Use io.LimitReader.
130 | body, err := ioutil.ReadAll(res.Body)
131 | if err != nil {
132 | return fmt.Errorf("reqlog: could not read response body: %v", err)
133 | }
134 | res.Body = ioutil.NopCloser(bytes.NewBuffer(body))
135 |
136 | go func() {
137 | if err := svc.addResponse(res.Request.Context(), reqID, clone, body); err != nil {
138 | log.Printf("[ERROR] Could not store response log: %v", err)
139 | }
140 | }()
141 |
142 | return nil
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/admin/src/components/reqlog/RequestList.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | TableContainer,
3 | Paper,
4 | Table,
5 | TableHead,
6 | TableRow,
7 | TableCell,
8 | TableBody,
9 | Typography,
10 | Box,
11 | createStyles,
12 | makeStyles,
13 | Theme,
14 | withTheme,
15 | } from "@material-ui/core";
16 |
17 | import HttpStatusIcon from "./HttpStatusCode";
18 | import CenteredPaper from "../CenteredPaper";
19 |
20 | const useStyles = makeStyles((theme: Theme) =>
21 | createStyles({
22 | row: {
23 | "&:hover": {
24 | cursor: "pointer",
25 | },
26 | },
27 | /* Pseudo-class applied to the root element if `hover={true}`. */
28 | hover: {},
29 | })
30 | );
31 |
32 | interface Props {
33 | logs: Array;
34 | selectedReqLogId?: string;
35 | onLogClick(requestId: string): void;
36 | theme: Theme;
37 | }
38 |
39 | function RequestList({
40 | logs,
41 | onLogClick,
42 | selectedReqLogId,
43 | theme,
44 | }: Props): JSX.Element {
45 | return (
46 |
47 |
53 | {logs.length === 0 && (
54 |
55 |
56 | No logs found.
57 |
58 |
59 | )}
60 |
61 | );
62 | }
63 |
64 | interface RequestListTableProps {
65 | logs?: any;
66 | selectedReqLogId?: string;
67 | onLogClick(requestId: string): void;
68 | theme: Theme;
69 | }
70 |
71 | function RequestListTable({
72 | logs,
73 | selectedReqLogId,
74 | onLogClick,
75 | theme,
76 | }: RequestListTableProps): JSX.Element {
77 | const classes = useStyles();
78 | return (
79 |
86 |
87 |
88 |
89 | Method
90 | Origin
91 | Path
92 | Status
93 |
94 |
95 |
96 | {logs.map(({ id, method, url, response }) => {
97 | const { origin, pathname, search, hash } = new URL(url);
98 |
99 | const cellStyle = {
100 | whiteSpace: "nowrap",
101 | overflow: "hidden",
102 | textOverflow: "ellipsis",
103 | } as any;
104 |
105 | const rowStyle = {
106 | backgroundColor:
107 | id === selectedReqLogId && theme.palette.action.selected,
108 | };
109 |
110 | return (
111 | onLogClick(id)}
117 | >
118 |
119 | {method}
120 |
121 |
122 | {origin}
123 |
124 |
125 | {decodeURIComponent(pathname + search + hash)}
126 |
127 |
128 | {response && (
129 |
130 | {" "}
131 | {response.status}
132 |
133 | )}
134 |
135 |
136 | );
137 | })}
138 |
139 |
140 |
141 | );
142 | }
143 |
144 | export default withTheme(RequestList);
145 |
--------------------------------------------------------------------------------
/pkg/proxy/proxy.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "context"
5 | "crypto"
6 | "crypto/tls"
7 | "crypto/x509"
8 | "fmt"
9 | "log"
10 | "net"
11 | "net/http"
12 | "net/http/httputil"
13 |
14 | "github.com/google/uuid"
15 | )
16 |
17 | type contextKey int
18 |
19 | const ReqIDKey contextKey = 0
20 |
21 | // Proxy implements http.Handler and offers MITM behaviour for modifying
22 | // HTTP requests and responses.
23 | type Proxy struct {
24 | certConfig *CertConfig
25 | handler http.Handler
26 |
27 | // TODO: Add mutex for modifier funcs.
28 | reqModifiers []RequestModifyMiddleware
29 | resModifiers []ResponseModifyMiddleware
30 | }
31 |
32 | // NewProxy returns a new Proxy.
33 | func NewProxy(ca *x509.Certificate, key crypto.PrivateKey) (*Proxy, error) {
34 | certConfig, err := NewCertConfig(ca, key)
35 | if err != nil {
36 | return nil, err
37 | }
38 |
39 | p := &Proxy{
40 | certConfig: certConfig,
41 | reqModifiers: make([]RequestModifyMiddleware, 0),
42 | resModifiers: make([]ResponseModifyMiddleware, 0),
43 | }
44 |
45 | p.handler = &httputil.ReverseProxy{
46 | Director: p.modifyRequest,
47 | ModifyResponse: p.modifyResponse,
48 | ErrorHandler: errorHandler,
49 | }
50 |
51 | return p, nil
52 | }
53 |
54 | func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
55 | // Add a unique request ID, to be used for correlating responses to requests.
56 | reqID := uuid.New()
57 | ctx := context.WithValue(r.Context(), ReqIDKey, reqID)
58 | r = r.WithContext(ctx)
59 |
60 | if r.Method == http.MethodConnect {
61 | p.handleConnect(w, r)
62 | return
63 | }
64 |
65 | p.handler.ServeHTTP(w, r)
66 | }
67 |
68 | func (p *Proxy) UseRequestModifier(fn ...RequestModifyMiddleware) {
69 | p.reqModifiers = append(p.reqModifiers, fn...)
70 | }
71 |
72 | func (p *Proxy) UseResponseModifier(fn ...ResponseModifyMiddleware) {
73 | p.resModifiers = append(p.resModifiers, fn...)
74 | }
75 |
76 | func (p *Proxy) modifyRequest(r *http.Request) {
77 | // Fix r.URL for HTTPS requests after CONNECT.
78 | if r.URL.Scheme == "" {
79 | r.URL.Host = r.Host
80 | r.URL.Scheme = "https"
81 | }
82 |
83 | // Setting `X-Forwarded-For` to `nil` ensures that http.ReverseProxy doesn't
84 | // set this header.
85 | r.Header["X-Forwarded-For"] = nil
86 |
87 | fn := nopReqModifier
88 |
89 | for i := len(p.reqModifiers) - 1; i >= 0; i-- {
90 | fn = p.reqModifiers[i](fn)
91 | }
92 |
93 | fn(r)
94 | }
95 |
96 | func (p *Proxy) modifyResponse(res *http.Response) error {
97 | fn := nopResModifier
98 |
99 | for i := len(p.resModifiers) - 1; i >= 0; i-- {
100 | fn = p.resModifiers[i](fn)
101 | }
102 |
103 | return fn(res)
104 | }
105 |
106 | // handleConnect hijacks the incoming HTTP request and sets up an HTTP tunnel.
107 | // During the TLS handshake with the client, we use the proxy's CA config to
108 | // create a certificate on-the-fly.
109 | func (p *Proxy) handleConnect(w http.ResponseWriter, r *http.Request) {
110 | hj, ok := w.(http.Hijacker)
111 | if !ok {
112 | log.Printf("[ERROR] handleConnect: ResponseWriter is not a http.Hijacker (type: %T)", w)
113 | writeError(w, r, http.StatusServiceUnavailable)
114 | return
115 | }
116 |
117 | w.WriteHeader(http.StatusOK)
118 |
119 | clientConn, _, err := hj.Hijack()
120 | if err != nil {
121 | log.Printf("[ERROR] Hijacking client connection failed: %v", err)
122 | writeError(w, r, http.StatusServiceUnavailable)
123 | return
124 | }
125 | defer clientConn.Close()
126 |
127 | // Secure connection to client.
128 | clientConn, err = p.clientTLSConn(clientConn)
129 | if err != nil {
130 | log.Printf("[ERROR] Securing client connection failed: %v", err)
131 | return
132 | }
133 | clientConnNotify := ConnNotify{clientConn, make(chan struct{})}
134 |
135 | l := &OnceAcceptListener{clientConnNotify.Conn}
136 |
137 | err = http.Serve(l, p)
138 | if err != nil && err != ErrAlreadyAccepted {
139 | log.Printf("[ERROR] Serving HTTP request failed: %v", err)
140 | }
141 | <-clientConnNotify.closed
142 | }
143 |
144 | func (p *Proxy) clientTLSConn(conn net.Conn) (*tls.Conn, error) {
145 | tlsConfig := p.certConfig.TLSConfig()
146 |
147 | tlsConn := tls.Server(conn, tlsConfig)
148 | if err := tlsConn.Handshake(); err != nil {
149 | tlsConn.Close()
150 | return nil, fmt.Errorf("handshake error: %v", err)
151 | }
152 |
153 | return tlsConn, nil
154 | }
155 |
156 | func errorHandler(w http.ResponseWriter, r *http.Request, err error) {
157 | if err == context.Canceled {
158 | return
159 | }
160 | log.Printf("[ERROR]: Proxy error: %v", err)
161 | w.WriteHeader(http.StatusBadGateway)
162 | }
163 |
164 | func writeError(w http.ResponseWriter, r *http.Request, code int) {
165 | http.Error(w, http.StatusText(code), code)
166 | }
167 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | > Hetty is an HTTP toolkit for security research. It aims to become an open source
4 | > alternative to commercial software like Burp Suite Pro, with powerful features
5 | > tailored to the needs of the infosec and bug bounty community.
6 |
7 |
8 |
9 | ## Features/to do
10 |
11 | - [x] HTTP man-in-the-middle (MITM) proxy and GraphQL server.
12 | - [x] Web interface (Next.js) with proxy log viewer.
13 | - [ ] Add scope support to the proxy.
14 | - [ ] Full text search (with regex) in proxy log viewer.
15 | - [ ] Project management.
16 | - [ ] Sender module for sending manual HTTP requests, either from scratch or based
17 | off requests from the proxy log.
18 | - [ ] Attacker module for automated sending of HTTP requests. Leverage the concurrency
19 | features of Go and its `net/http` package to make it blazingly fast.
20 |
21 | ## Installation
22 |
23 | Hetty is packaged on GitHub as a single binary, with the web interface resources
24 | embedded.
25 |
26 | 👉 You can find downloads for Linux, macOS and Windows on the [releases page](https://github.com/dstotijn/hetty/releases).
27 |
28 | ### Alternatives:
29 |
30 | **Build from source**
31 |
32 | ```
33 | $ GO111MODULE=auto go get -u -v github.com/dstotijn/hetty/cmd/hetty
34 | ```
35 |
36 | Then export the Next.js frontend app:
37 |
38 | ```
39 | $ cd admin
40 | $ yarn install
41 | $ yarn export
42 | ```
43 |
44 | This will ensure a folder `./admin/dist` exists.
45 | Then, you can bundle the frontend app using `rice`.
46 | The easiest way to do this is via a supplied `Makefile` command in the root of
47 | the project:
48 |
49 | ```
50 | make build
51 | ```
52 |
53 | **Docker**
54 |
55 | Alternatively, you can run Hetty via Docker. See: [`dstotijn/hetty`](https://hub.docker.com/r/dstotijn/hetty)
56 | on Docker Hub.
57 |
58 | ```
59 | $ docker run \
60 | -v $HOME/.ssh/hetty_key.pem:/.ssh/hetty_key.pem \
61 | -v $HOME/.ssh/hetty_cert.pem:/.ssh/hetty_cert.pem \
62 | -v $HOME/.hetty/hetty.db:/app/hetty.db \
63 | -p 127.0.0.1:8080:80 \
64 | dstotijn/hetty -key /.ssh/hetty_key.pem -cert /.ssh/hetty_cert.pem -db hetty.db
65 | ```
66 |
67 | ## Usage
68 |
69 | Hetty is packaged as a single binary, with the web interface resources embedded.
70 | When the program is run, it listens by default on `:8080` and is accessible via
71 | http://localhost:8080. Depending on incoming HTTP requests, it either acts as a
72 | MITM proxy, or it serves the GraphQL API and web interface (Next.js).
73 |
74 | ```
75 | $ hetty -h
76 | Usage of hetty:
77 | -addr string
78 | TCP address to listen on, in the form "host:port" (default ":80")
79 | -adminPath string
80 | File path to admin build
81 | -cert string
82 | CA certificate file path
83 | -db string
84 | Database file path (default "hetty.db")
85 | -key string
86 | CA private key file path
87 | ```
88 |
89 | **Note:** There is no built-in in support yet for generating a CA certificate.
90 | This will be added really soon in an upcoming release. In the meantime, please
91 | use `openssl` (_TODO: add instructions_).
92 |
93 | ## Vision and roadmap
94 |
95 | The project has just gotten underway, and as such I haven’t had time yet to do a
96 | write-up on its mission and roadmap. A short summary/braindump:
97 |
98 | - Fast core/engine, built with Go, with a minimal memory footprint.
99 | - GraphQL server to interact with the backend.
100 | - Easy to use web interface, built with Next.js and Material UI.
101 | - Extensibility is top of mind. All modules are written as Go packages, to
102 | be used by the main `hetty` program, but also usable as libraries for other software.
103 | Aside from the GraphQL server, it should (eventually) be possible to also use
104 | it as a CLI tool.
105 | - Pluggable architecture for the MITM proxy and future modules, making it
106 | possible for hook into the core engine.
107 | - I’ve chosen [Cayley](https://cayley.io/) as the graph database (backed by
108 | BoltDB storage on disk) for now (not sure if it will work in the long run).
109 | The benefit is that Cayley (also written in Go)
110 | is embedded as a library. Because of this, the complete application is self contained
111 | in a single running binary.
112 | - Talk to the community, and focus on the features that the majority.
113 | Less features means less code to maintain.
114 |
115 | ## Status
116 |
117 | The project is currently under active development. Please star/follow and check
118 | back soon. 🤗
119 |
120 | ## Acknowledgements
121 |
122 | Thanks to the [Hacker101 community on Discord](https://discordapp.com/channels/514337135491416065)
123 | for all the encouragement to actually start building this thing!
124 |
125 | ## License
126 |
127 | [MIT](LICENSE)
128 |
129 | ---
130 |
131 | © 2020 David Stotijn — [Twitter](https://twitter.com/dstotijn), [Email](mailto:dstotijn@gmail.com)
132 |
--------------------------------------------------------------------------------
/pkg/proxy/cert.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "bytes"
5 | "crypto"
6 | "crypto/rand"
7 | "crypto/rsa"
8 | "crypto/sha1"
9 | "crypto/tls"
10 | "crypto/x509"
11 | "crypto/x509/pkix"
12 | "errors"
13 | "math/big"
14 | "net"
15 | "time"
16 | )
17 |
18 | // MaxSerialNumber is the upper boundary that is used to create unique serial
19 | // numbers for the certificate. This can be any unsigned integer up to 20
20 | // bytes (2^(8*20)-1).
21 | var MaxSerialNumber = big.NewInt(0).SetBytes(bytes.Repeat([]byte{255}, 20))
22 |
23 | // CertConfig is a set of configuration values that are used to build TLS configs
24 | // capable of MITM
25 | type CertConfig struct {
26 | ca *x509.Certificate
27 | caPriv crypto.PrivateKey
28 | priv *rsa.PrivateKey
29 | keyID []byte
30 | }
31 |
32 | // NewCertConfig creates a MITM config using the CA certificate and
33 | // private key to generate on-the-fly certificates.
34 | func NewCertConfig(ca *x509.Certificate, caPrivKey crypto.PrivateKey) (*CertConfig, error) {
35 | priv, err := rsa.GenerateKey(rand.Reader, 2048)
36 | if err != nil {
37 | return nil, err
38 | }
39 | pub := priv.Public()
40 |
41 | // Subject Key Identifier support for end entity certificate.
42 | // https://www.ietf.org/rfc/rfc3280.txt (section 4.2.1.2)
43 | pkixPubKey, err := x509.MarshalPKIXPublicKey(pub)
44 | if err != nil {
45 | return nil, err
46 | }
47 | h := sha1.New()
48 | h.Write(pkixPubKey)
49 | keyID := h.Sum(nil)
50 |
51 | return &CertConfig{
52 | ca: ca,
53 | caPriv: caPrivKey,
54 | priv: priv,
55 | keyID: keyID,
56 | }, nil
57 | }
58 |
59 | // NewCA creates a new CA certificate and associated private key.
60 | func NewCA(name, organization string, validity time.Duration) (*x509.Certificate, *rsa.PrivateKey, error) {
61 | priv, err := rsa.GenerateKey(rand.Reader, 2048)
62 | if err != nil {
63 | return nil, nil, err
64 | }
65 | pub := priv.Public()
66 |
67 | // Subject Key Identifier support for end entity certificate.
68 | // https://www.ietf.org/rfc/rfc3280.txt (section 4.2.1.2)
69 | pkixpub, err := x509.MarshalPKIXPublicKey(pub)
70 | if err != nil {
71 | return nil, nil, err
72 | }
73 | h := sha1.New()
74 | h.Write(pkixpub)
75 | keyID := h.Sum(nil)
76 |
77 | // TODO: keep a map of used serial numbers to avoid potentially reusing a
78 | // serial multiple times.
79 | serial, err := rand.Int(rand.Reader, MaxSerialNumber)
80 | if err != nil {
81 | return nil, nil, err
82 | }
83 |
84 | tmpl := &x509.Certificate{
85 | SerialNumber: serial,
86 | Subject: pkix.Name{
87 | CommonName: name,
88 | Organization: []string{organization},
89 | },
90 | SubjectKeyId: keyID,
91 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
92 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
93 | BasicConstraintsValid: true,
94 | NotBefore: time.Now().Add(-24 * time.Hour),
95 | NotAfter: time.Now().Add(24 * time.Hour),
96 | DNSNames: []string{name},
97 | IsCA: true,
98 | }
99 |
100 | raw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, priv)
101 | if err != nil {
102 | return nil, nil, err
103 | }
104 |
105 | // Parse certificate bytes so that we have a leaf certificate.
106 | x509c, err := x509.ParseCertificate(raw)
107 | if err != nil {
108 | return nil, nil, err
109 | }
110 |
111 | return x509c, priv, nil
112 | }
113 |
114 | // TLSConfig returns a *tls.Config that will generate certificates on-the-fly using
115 | // the SNI extension in the TLS ClientHello.
116 | func (c *CertConfig) TLSConfig() *tls.Config {
117 | return &tls.Config{
118 | GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
119 | if clientHello.ServerName == "" {
120 | return nil, errors.New("missing server name (SNI)")
121 | }
122 | return c.cert(clientHello.ServerName)
123 | },
124 | NextProtos: []string{"http/1.1"},
125 | }
126 | }
127 |
128 | func (c *CertConfig) cert(hostname string) (*tls.Certificate, error) {
129 | // Remove the port if it exists.
130 | host, _, err := net.SplitHostPort(hostname)
131 | if err == nil {
132 | hostname = host
133 | }
134 |
135 | serial, err := rand.Int(rand.Reader, MaxSerialNumber)
136 | if err != nil {
137 | return nil, err
138 | }
139 |
140 | tmpl := &x509.Certificate{
141 | SerialNumber: serial,
142 | Subject: pkix.Name{
143 | CommonName: hostname,
144 | Organization: []string{"Hetty"},
145 | },
146 | SubjectKeyId: c.keyID,
147 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
148 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
149 | BasicConstraintsValid: true,
150 | NotBefore: time.Now().Add(-24 * time.Hour),
151 | NotAfter: time.Now().Add(24 * time.Hour),
152 | }
153 |
154 | if ip := net.ParseIP(hostname); ip != nil {
155 | tmpl.IPAddresses = []net.IP{ip}
156 | } else {
157 | tmpl.DNSNames = []string{hostname}
158 | }
159 |
160 | raw, err := x509.CreateCertificate(rand.Reader, tmpl, c.ca, c.priv.Public(), c.caPriv)
161 | if err != nil {
162 | return nil, err
163 | }
164 |
165 | // Parse certificate bytes so that we have a leaf certificate.
166 | x509c, err := x509.ParseCertificate(raw)
167 | if err != nil {
168 | return nil, err
169 | }
170 |
171 | return &tls.Certificate{
172 | Certificate: [][]byte{raw, c.ca.Raw},
173 | PrivateKey: c.priv,
174 | Leaf: x509c,
175 | }, nil
176 | }
177 |
--------------------------------------------------------------------------------
/admin/src/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | makeStyles,
4 | Theme,
5 | createStyles,
6 | useTheme,
7 | AppBar,
8 | Toolbar,
9 | IconButton,
10 | Typography,
11 | Drawer,
12 | Divider,
13 | List,
14 | ListItem,
15 | ListItemIcon,
16 | ListItemText,
17 | Tooltip,
18 | } from "@material-ui/core";
19 | import Link from "next/link";
20 | import MenuIcon from "@material-ui/icons/Menu";
21 | import HomeIcon from "@material-ui/icons/Home";
22 | import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet";
23 | import SendIcon from "@material-ui/icons/Send";
24 | import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
25 | import ChevronRightIcon from "@material-ui/icons/ChevronRight";
26 | import clsx from "clsx";
27 |
28 | export enum Page {
29 | Home,
30 | ProxySetup,
31 | ProxyLogs,
32 | Sender,
33 | }
34 |
35 | const drawerWidth = 240;
36 |
37 | const useStyles = makeStyles((theme: Theme) =>
38 | createStyles({
39 | root: {
40 | display: "flex",
41 | width: "100%",
42 | },
43 | appBar: {
44 | zIndex: theme.zIndex.drawer + 1,
45 | transition: theme.transitions.create(["width", "margin"], {
46 | easing: theme.transitions.easing.sharp,
47 | duration: theme.transitions.duration.leavingScreen,
48 | }),
49 | },
50 | appBarShift: {
51 | marginLeft: drawerWidth,
52 | width: `calc(100% - ${drawerWidth}px)`,
53 | transition: theme.transitions.create(["width", "margin"], {
54 | easing: theme.transitions.easing.sharp,
55 | duration: theme.transitions.duration.enteringScreen,
56 | }),
57 | },
58 | menuButton: {
59 | marginRight: 28,
60 | },
61 | hide: {
62 | display: "none",
63 | },
64 | drawer: {
65 | width: drawerWidth,
66 | flexShrink: 0,
67 | whiteSpace: "nowrap",
68 | },
69 | drawerOpen: {
70 | width: drawerWidth,
71 | transition: theme.transitions.create("width", {
72 | easing: theme.transitions.easing.sharp,
73 | duration: theme.transitions.duration.enteringScreen,
74 | }),
75 | },
76 | drawerClose: {
77 | transition: theme.transitions.create("width", {
78 | easing: theme.transitions.easing.sharp,
79 | duration: theme.transitions.duration.leavingScreen,
80 | }),
81 | overflowX: "hidden",
82 | width: theme.spacing(7) + 1,
83 | [theme.breakpoints.up("sm")]: {
84 | width: theme.spacing(7) + 8,
85 | },
86 | },
87 | toolbar: {
88 | display: "flex",
89 | alignItems: "center",
90 | justifyContent: "flex-end",
91 | padding: theme.spacing(0, 1),
92 | // necessary for content to be below app bar
93 | ...theme.mixins.toolbar,
94 | },
95 | content: {
96 | flexGrow: 1,
97 | padding: theme.spacing(3),
98 | },
99 | listItem: {
100 | paddingLeft: 16,
101 | paddingRight: 16,
102 | [theme.breakpoints.up("sm")]: {
103 | paddingLeft: 20,
104 | paddingRight: 20,
105 | },
106 | },
107 | listItemIcon: {
108 | minWidth: 42,
109 | },
110 | titleHighlight: {
111 | color: theme.palette.secondary.main,
112 | marginRight: 4,
113 | },
114 | })
115 | );
116 |
117 | interface Props {
118 | children: React.ReactNode;
119 | title: string;
120 | page: Page;
121 | }
122 |
123 | export function Layout({ title, page, children }: Props): JSX.Element {
124 | const classes = useStyles();
125 | const theme = useTheme();
126 | const [open, setOpen] = React.useState(false);
127 |
128 | const handleDrawerOpen = () => {
129 | setOpen(true);
130 | };
131 |
132 | const handleDrawerClose = () => {
133 | setOpen(false);
134 | };
135 |
136 | return (
137 |
138 |
144 |
145 |
154 |
155 |
156 |
157 |
158 | Hetty://
159 |
160 | {title}
161 |
162 |
163 |
164 |
177 |
178 |
179 | {theme.direction === "rtl" ? (
180 |
181 | ) : (
182 |
183 | )}
184 |
185 |
186 |
187 |
188 |
189 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 | {children}
241 |
242 |
243 | );
244 | }
245 |
246 | export default Layout;
247 |
--------------------------------------------------------------------------------
/pkg/db/cayley/cayley.go:
--------------------------------------------------------------------------------
1 | package cayley
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "net/url"
9 | "path"
10 | "strings"
11 | "sync"
12 | "time"
13 |
14 | "github.com/cayleygraph/cayley"
15 | "github.com/cayleygraph/cayley/graph"
16 | "github.com/cayleygraph/cayley/graph/kv"
17 | "github.com/cayleygraph/cayley/schema"
18 | "github.com/cayleygraph/quad"
19 | "github.com/cayleygraph/quad/voc"
20 | "github.com/cayleygraph/quad/voc/rdf"
21 | "github.com/google/uuid"
22 |
23 | "github.com/dstotijn/hetty/pkg/reqlog"
24 | )
25 |
26 | type HTTPRequest struct {
27 | rdfType struct{} `quad:"@type > hy:HTTPRequest"`
28 | ID quad.IRI `quad:"@id"`
29 | Proto string `quad:"hy:proto"`
30 | URL string `quad:"hy:url"`
31 | Method string `quad:"hy:method"`
32 | Body string `quad:"hy:body,optional"`
33 | Headers []HTTPHeader `quad:"hy:header"`
34 | Timestamp time.Time `quad:"hy:timestamp"`
35 | Response *HTTPResponse `quad:"hy:request < *,optional"`
36 | }
37 |
38 | type HTTPResponse struct {
39 | rdfType struct{} `quad:"@type > hy:HTTPResponse"`
40 | RequestID quad.IRI `quad:"hy:request"`
41 | Proto string `quad:"hy:proto"`
42 | Status string `quad:"hy:status"`
43 | StatusCode int `quad:"hy:status_code"`
44 | Headers []HTTPHeader `quad:"hy:header"`
45 | Body string `quad:"hy:body,optional"`
46 | Timestamp time.Time `quad:"hy:timestamp"`
47 | }
48 |
49 | type HTTPHeader struct {
50 | rdfType struct{} `quad:"@type > hy:HTTPHeader"`
51 | Key string `quad:"hy:key"`
52 | Value string `quad:"hy:value,optional"`
53 | }
54 |
55 | type Database struct {
56 | store *cayley.Handle
57 | schema *schema.Config
58 | mu sync.Mutex
59 | }
60 |
61 | func init() {
62 | voc.RegisterPrefix("hy:", "https://hetty.xyz/")
63 | schema.RegisterType(quad.IRI("hy:HTTPRequest"), HTTPRequest{})
64 | schema.RegisterType(quad.IRI("hy:HTTPResponse"), HTTPResponse{})
65 | schema.RegisterType(quad.IRI("hy:HTTPHeader"), HTTPHeader{})
66 |
67 | kv.Register(Type, kv.Registration{
68 | NewFunc: boltOpen,
69 | InitFunc: boltCreate,
70 | IsPersistent: true,
71 | })
72 | }
73 |
74 | func NewDatabase(filename string) (*Database, error) {
75 | dir, file := path.Split(filename)
76 | if dir == "" {
77 | dir = "."
78 | }
79 | opts := graph.Options{
80 | "filename": file,
81 | }
82 |
83 | schemaCfg := schema.NewConfig()
84 | schemaCfg.GenerateID = func(_ interface{}) quad.Value {
85 | return quad.BNode(uuid.New().String())
86 | }
87 |
88 | // Initialize the database.
89 | err := graph.InitQuadStore("bolt", dir, opts)
90 | if err != nil && err != graph.ErrDatabaseExists {
91 | return nil, fmt.Errorf("cayley: could not initialize database: %v", err)
92 | }
93 |
94 | // Open the database.
95 | store, err := cayley.NewGraph("bolt", dir, opts)
96 | if err != nil {
97 | return nil, fmt.Errorf("cayley: could not open database: %v", err)
98 | }
99 |
100 | return &Database{
101 | store: store,
102 | schema: schemaCfg,
103 | }, nil
104 | }
105 |
106 | func (db *Database) Close() error {
107 | return db.store.Close()
108 | }
109 |
110 | func (db *Database) FindAllRequestLogs(ctx context.Context) ([]reqlog.Request, error) {
111 | db.mu.Lock()
112 | defer db.mu.Unlock()
113 |
114 | var reqLogs []reqlog.Request
115 | var reqs []HTTPRequest
116 |
117 | path := cayley.StartPath(db.store, quad.IRI("hy:HTTPRequest")).In(quad.IRI(rdf.Type))
118 | err := path.Iterate(ctx).EachValue(db.store, func(v quad.Value) {
119 | var req HTTPRequest
120 | if err := db.schema.LoadToDepth(ctx, db.store, &req, -1, v); err != nil {
121 | log.Printf("[ERROR] Could not load sub-graph for http requests: %v", err)
122 | return
123 | }
124 | reqs = append(reqs, req)
125 | })
126 | if err != nil {
127 | return nil, fmt.Errorf("cayley: could not iterate over http requests: %v", err)
128 | }
129 |
130 | for _, req := range reqs {
131 | reqLog, err := parseRequestQuads(req, nil)
132 | if err != nil {
133 | return nil, fmt.Errorf("cayley: could not parse request quads (id: %v): %v", req.ID, err)
134 | }
135 | reqLogs = append(reqLogs, reqLog)
136 | }
137 |
138 | // By default, all retrieved requests are ordered chronologically, oldest first.
139 | // Reverse the order, so newest logs are first.
140 | for i := len(reqLogs)/2 - 1; i >= 0; i-- {
141 | opp := len(reqLogs) - 1 - i
142 | reqLogs[i], reqLogs[opp] = reqLogs[opp], reqLogs[i]
143 | }
144 |
145 | return reqLogs, nil
146 | }
147 |
148 | func (db *Database) FindRequestLogByID(ctx context.Context, id uuid.UUID) (reqlog.Request, error) {
149 | db.mu.Lock()
150 | defer db.mu.Unlock()
151 |
152 | var req HTTPRequest
153 | err := db.schema.LoadTo(ctx, db.store, &req, iriFromUUID(id))
154 | if schema.IsNotFound(err) {
155 | return reqlog.Request{}, reqlog.ErrRequestNotFound
156 | }
157 | if err != nil {
158 | return reqlog.Request{}, fmt.Errorf("cayley: could not load value: %v", err)
159 | }
160 |
161 | reqLog, err := parseRequestQuads(req, nil)
162 | if err != nil {
163 | return reqlog.Request{}, fmt.Errorf("cayley: could not parse request log (id: %v): %v", req.ID, err)
164 | }
165 |
166 | return reqLog, nil
167 | }
168 |
169 | func (db *Database) AddRequestLog(ctx context.Context, reqLog reqlog.Request) error {
170 | db.mu.Lock()
171 | defer db.mu.Unlock()
172 |
173 | httpReq := HTTPRequest{
174 | ID: iriFromUUID(reqLog.ID),
175 | Proto: reqLog.Request.Proto,
176 | Method: reqLog.Request.Method,
177 | URL: reqLog.Request.URL.String(),
178 | Headers: httpHeadersSliceFromMap(reqLog.Request.Header),
179 | Body: string(reqLog.Body),
180 | Timestamp: reqLog.Timestamp,
181 | }
182 |
183 | tx := cayley.NewTransaction()
184 | qw := graph.NewTxWriter(tx, graph.Add)
185 |
186 | _, err := db.schema.WriteAsQuads(qw, httpReq)
187 | if err != nil {
188 | return fmt.Errorf("cayley: could not write quads: %v", err)
189 | }
190 |
191 | if err := db.store.ApplyTransaction(tx); err != nil {
192 | return fmt.Errorf("cayley: could not apply transaction: %v", err)
193 | }
194 |
195 | return nil
196 | }
197 |
198 | func (db *Database) AddResponseLog(ctx context.Context, resLog reqlog.Response) error {
199 | db.mu.Lock()
200 | defer db.mu.Unlock()
201 |
202 | httpRes := HTTPResponse{
203 | RequestID: iriFromUUID(resLog.RequestID),
204 | Proto: resLog.Response.Proto,
205 | Status: resLog.Response.Status,
206 | StatusCode: resLog.Response.StatusCode,
207 | Headers: httpHeadersSliceFromMap(resLog.Response.Header),
208 | Body: string(resLog.Body),
209 | Timestamp: resLog.Timestamp,
210 | }
211 |
212 | tx := cayley.NewTransaction()
213 | qw := graph.NewTxWriter(tx, graph.Add)
214 |
215 | _, err := db.schema.WriteAsQuads(qw, httpRes)
216 | if err != nil {
217 | return fmt.Errorf("cayley: could not write response quads: %v", err)
218 | }
219 |
220 | if err := db.store.ApplyTransaction(tx); err != nil {
221 | return fmt.Errorf("cayley: could not apply transaction: %v", err)
222 | }
223 |
224 | return nil
225 | }
226 |
227 | func iriFromUUID(id uuid.UUID) quad.IRI {
228 | return quad.IRI("hy:" + id.String()).Full().Short()
229 | }
230 |
231 | func uuidFromIRI(iri quad.IRI) (uuid.UUID, error) {
232 | iriString := iri.Short().String()
233 | stripped := strings.TrimRight(strings.TrimLeft(iriString, "")
234 | id, err := uuid.Parse(stripped)
235 | if err != nil {
236 | return uuid.Nil, err
237 | }
238 |
239 | return id, nil
240 | }
241 |
242 | func httpHeadersSliceFromMap(hm http.Header) []HTTPHeader {
243 | if hm == nil {
244 | return nil
245 | }
246 | var hs []HTTPHeader
247 | for key, values := range hm {
248 | for _, value := range values {
249 | hs = append(hs, HTTPHeader{Key: key, Value: value})
250 | }
251 | }
252 | return hs
253 | }
254 |
255 | func httpHeadersMapFromSlice(hs []HTTPHeader) http.Header {
256 | if hs == nil {
257 | return nil
258 | }
259 | hm := make(http.Header)
260 | for _, header := range hs {
261 | hm.Add(header.Key, header.Value)
262 | }
263 | return hm
264 | }
265 |
266 | func parseRequestQuads(req HTTPRequest, _ *HTTPResponse) (reqlog.Request, error) {
267 | reqID, err := uuidFromIRI(req.ID)
268 | if err != nil {
269 | return reqlog.Request{}, fmt.Errorf("cannot parse request id: %v", err)
270 | }
271 |
272 | u, err := url.Parse(req.URL)
273 | if err != nil {
274 | return reqlog.Request{}, fmt.Errorf("cannot parse request url: %v", err)
275 | }
276 |
277 | reqLog := reqlog.Request{
278 | ID: reqID,
279 | Request: http.Request{
280 | Method: req.Method,
281 | URL: u,
282 | Proto: req.Proto,
283 | Header: httpHeadersMapFromSlice(req.Headers),
284 | },
285 | Timestamp: req.Timestamp,
286 | }
287 | if req.Body != "" {
288 | reqLog.Body = []byte(reqLog.Body)
289 | }
290 |
291 | if req.Response != nil {
292 | reqLog.Response = &reqlog.Response{
293 | RequestID: reqID,
294 | Response: http.Response{
295 | Proto: req.Response.Proto,
296 | Status: req.Response.Status,
297 | StatusCode: req.Response.StatusCode,
298 | Header: httpHeadersMapFromSlice(req.Response.Headers),
299 | },
300 | }
301 | if req.Response.Body != "" {
302 | reqLog.Response.Body = []byte(req.Response.Body)
303 | }
304 | }
305 |
306 | return reqLog, nil
307 | }
308 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
4 | github.com/99designs/gqlgen v0.11.3 h1:oFSxl1DFS9X///uHV3y6CEfpcXWrDUxVblR4Xib2bs4=
5 | github.com/99designs/gqlgen v0.11.3/go.mod h1:RgX5GRRdDWNkh4pBrdzNpNPFVsdoUFY2+adM6nb1N+4=
6 | github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
7 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
8 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
9 | github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
10 | github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
11 | github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
12 | github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
13 | github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
14 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
15 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
16 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
17 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
18 | github.com/agnivade/levenshtein v1.0.1 h1:3oJU7J3FGFmyhn8KHjmVaZCN5hxTr7GxgRue+sxIXdQ=
19 | github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
20 | github.com/agnivade/levenshtein v1.0.3 h1:M5ZnqLOoZR8ygVq0FfkXsNOKzMCk0xRiow0R5+5VkQ0=
21 | github.com/agnivade/levenshtein v1.0.3/go.mod h1:4SFRZbbXWLF4MU1T9Qg0pGgH3Pjs+t6ie5efyrwRJXs=
22 | github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
23 | github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
24 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
25 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
26 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
27 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
28 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
29 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
30 | github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
31 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
32 | github.com/badgerodon/peg v0.0.0-20130729175151-9e5f7f4d07ca/go.mod h1:TWe0N2hv5qvpLHT+K16gYcGBllld4h65dQ/5CNuirmk=
33 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
34 | github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
35 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
36 | github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
37 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
38 | github.com/cayleygraph/cayley v0.7.7 h1:z+7xkAbg6bKiXJOtOkEG3zCm2K084sr/aGwFV7xcQNs=
39 | github.com/cayleygraph/cayley v0.7.7/go.mod h1:VUd+PInYf94/VY41ePeFtFyP99BAs953kFT4N+6F7Ko=
40 | github.com/cayleygraph/quad v1.1.0 h1:w1nXAmn+nz07+qlw89dke9LwWkYpeX+OcvfTvGQRBpM=
41 | github.com/cayleygraph/quad v1.1.0/go.mod h1:maWODEekEhrO0mdc9h5n/oP7cH1h/OTgqQ2qWbuI9M4=
42 | github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
43 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
44 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
45 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
46 | github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
47 | github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
48 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
49 | github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
50 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
51 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
52 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
53 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
54 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
55 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
56 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
57 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
58 | github.com/cznic/mathutil v0.0.0-20170313102836-1447ad269d64 h1:oad14P7M0/ZAPSMH1nl1vC8zdKVkA3kfHLO59z1l8Eg=
59 | github.com/cznic/mathutil v0.0.0-20170313102836-1447ad269d64/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
60 | github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
61 | github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
62 | github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY=
63 | github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
64 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
65 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
66 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
67 | github.com/dennwc/base v1.0.0 h1:xlBzvBNRvkQ1LFI/jom7rr0vZsvYDKtvMM6lIpjFb3M=
68 | github.com/dennwc/base v1.0.0/go.mod h1:zaTDIiAcg2oKW9XhjIaRc1kJVteCFXSSW6jwmCedUaI=
69 | github.com/dennwc/graphql v0.0.0-20180603144102-12cfed44bc5d/go.mod h1:lg9KQn0BgRCSCGNpcGvJp/0Ljf1Yxk8TZq9HSYc43fk=
70 | github.com/dgraph-io/badger v1.5.4/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ=
71 | github.com/dgraph-io/badger v1.5.5/go.mod h1:QgCntgIUPsjnp7cMLhUybJHb7iIoQWAHT6tF8ngCjWk=
72 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
73 | github.com/dgryski/go-farm v0.0.0-20190416075124-e1214b5e05dc/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
74 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
75 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
76 | github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c h1:TUuUh0Xgj97tLMNtWtNvI9mIV6isjEb9lBMNv+77IGM=
77 | github.com/dgryski/trifles v0.0.0-20190318185328-a8d75aae118c/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
78 | github.com/dlclark/regexp2 v1.1.4 h1:1udHhhGkIMplSrLeMJpPN7BHz1Iq2wVBUcb+3fxzhQM=
79 | github.com/dlclark/regexp2 v1.1.4/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
80 | github.com/docker/docker v0.7.3-0.20180412203414-a422774e593b/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
81 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
82 | github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
83 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
84 | github.com/dop251/goja v0.0.0-20190105122144-6d5bf35058fa h1:cA2OMt2CQ2yq2WhQw16mHv6ej9YY07H4pzfR/z/y+1Q=
85 | github.com/dop251/goja v0.0.0-20190105122144-6d5bf35058fa/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA=
86 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
87 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
88 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
89 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
90 | github.com/flimzy/diff v0.1.5/go.mod h1:lFJtC7SPsK0EroDmGTSrdtWKAxOk3rO+q+e04LL05Hs=
91 | github.com/flimzy/diff v0.1.6/go.mod h1:lFJtC7SPsK0EroDmGTSrdtWKAxOk3rO+q+e04LL05Hs=
92 | github.com/flimzy/kivik v1.8.1/go.mod h1:S2aPycbG0eDFll4wgXt9uacSNkXISPufutnc9sv+mdA=
93 | github.com/flimzy/testy v0.1.16/go.mod h1:3szguN8NXqgq9bt9Gu8TQVj698PJWmyx/VY1frwwKrM=
94 | github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
95 | github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
96 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
97 | github.com/fsouza/go-dockerclient v1.2.2/go.mod h1:KpcjM623fQYE9MZiTGzKhjfxXAV9wbyX2C1cyRHfhl0=
98 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
99 | github.com/go-chi/chi v3.3.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
100 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
101 | github.com/go-kivik/couchdb v1.8.1/go.mod h1:5XJRkAMpBlEVA4q0ktIZjUPYBjoBmRoiWvwUBzP3BOQ=
102 | github.com/go-kivik/kivik v1.8.1/go.mod h1:nIuJ8z4ikBrVUSk3Ua8NoDqYKULPNjuddjqRvlSUyyQ=
103 | github.com/go-kivik/kiviktest v1.1.2/go.mod h1:JdhVyzixoYhoIDUt6hRf1yAfYyaDa5/u9SDOindDkfQ=
104 | github.com/go-kivik/pouchdb v1.3.5/go.mod h1:U+siUrqLCVxeMU3QjQTYIC3/F/e6EUKm+o5buJb7vpw=
105 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
106 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
107 | github.com/go-sourcemap/sourcemap v2.1.2+incompatible h1:0b/xya7BKGhXuqFESKM4oIiRo9WOt2ebz7KxfreD6ug=
108 | github.com/go-sourcemap/sourcemap v2.1.2+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
109 | github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
110 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
111 | github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
112 | github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
113 | github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
114 | github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg=
115 | github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
116 | github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
117 | github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
118 | github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o=
119 | github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
120 | github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
121 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
122 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
123 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
124 | github.com/gogo/protobuf v1.3.0 h1:G8O7TerXerS4F6sx9OV7/nRfJdnXgHZu/S/7F2SN+UE=
125 | github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
126 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
127 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
128 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
129 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
130 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
131 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
132 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
133 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
134 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
135 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
136 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
137 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
138 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
139 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
140 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
141 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
142 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
143 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
144 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
145 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
146 | github.com/gopherjs/gopherjs v0.0.0-20190411002643-bd77b112433e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
147 | github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
148 | github.com/gopherjs/jsbuiltin v0.0.0-20180426082241-50091555e127/go.mod h1:7X1acUyFRf+oVFTU6SWw9mnb57Vxn+Nbh8iPbKg95hs=
149 | github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
150 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
151 | github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
152 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
153 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
154 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
155 | github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
156 | github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
157 | github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
158 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
159 | github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
160 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
161 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
162 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
163 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
164 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
165 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
166 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
167 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
168 | github.com/hidal-go/hidalgo v0.0.0-20190814174001-42e03f3b5eaa h1:hBE4LGxApbZiV/3YoEPv7uYlUMWOogG1hwtkpiU87zQ=
169 | github.com/hidal-go/hidalgo v0.0.0-20190814174001-42e03f3b5eaa/go.mod h1:bPkrxDlroXxigw8BMWTEPTv4W5/rQwNgg2BECXsgyX0=
170 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
171 | github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
172 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
173 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
174 | github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
175 | github.com/jackc/pgx v3.3.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
176 | github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
177 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
178 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
179 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
180 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
181 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
182 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
183 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
184 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
185 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
186 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
187 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
188 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
189 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
190 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
191 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
192 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
193 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
194 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
195 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
196 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
197 | github.com/linkeddata/gojsonld v0.0.0-20170418210642-4f5db6791326 h1:YP3lfXXYiQV5MKeUqVnxRP5uuMQTLPx+PGYm1UBoU98=
198 | github.com/linkeddata/gojsonld v0.0.0-20170418210642-4f5db6791326/go.mod h1:nfqkuSNlsk1bvti/oa7TThx4KmRMBmSxf3okHI9wp3E=
199 | github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
200 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
201 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
202 | github.com/mailru/easyjson v0.0.0-20180730094502-03f2033d19d5/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
203 | github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
204 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
205 | github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007 h1:reVOUXwnhsYv/8UqjvhrMOu5CNT9UapHFLbQ2JcXsmg=
206 | github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
207 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
208 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
209 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
210 | github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
211 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
212 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
213 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
214 | github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5nrJJVjbXiDZcVhOBSzKn3o9LgRLLMRNuru8=
215 | github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
216 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
217 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
218 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
219 | github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
220 | github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
221 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
222 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
223 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
224 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
225 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
226 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
227 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
228 | github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
229 | github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
230 | github.com/opencontainers/selinux v1.0.0/go.mod h1:+BLncwf63G4dgOzykXAxcmnFlUaOlkDdmw/CqsW6pjs=
231 | github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
232 | github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
233 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
234 | github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
235 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
236 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
237 | github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
238 | github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
239 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
240 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
241 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
242 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
243 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
244 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
245 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
246 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
247 | github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
248 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
249 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
250 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
251 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
252 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
253 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
254 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
255 | github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
256 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
257 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
258 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
259 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
260 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
261 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
262 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
263 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
264 | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
265 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
266 | github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
267 | github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
268 | github.com/rogpeppe/go-internal v1.5.0 h1:Usqs0/lDK/NqTkvrmKSwA/3XkZAs7ZAW/eLeQ2MVBTw=
269 | github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
270 | github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
271 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
272 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
273 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
274 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
275 | github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
276 | github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
277 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
278 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
279 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
280 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
281 | github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
282 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
283 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
284 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
285 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
286 | github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
287 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
288 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
289 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
290 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
291 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
292 | github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
293 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
294 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
295 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
296 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
297 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
298 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
299 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
300 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
301 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
302 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
303 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
304 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
305 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
306 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
307 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
308 | github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
309 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
310 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
311 | github.com/tylertreat/BoomFilters v0.0.0-20181028192813-611b3dbe80e8 h1:7X4KYG3guI2mPQGxm/ZNNsiu4BjKnef0KG0TblMC+Z8=
312 | github.com/tylertreat/BoomFilters v0.0.0-20181028192813-611b3dbe80e8/go.mod h1:OYRfF6eb5wY9VRFkXJH8FFBi3plw2v+giaIu7P054pM=
313 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
314 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
315 | github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
316 | github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
317 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
318 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
319 | github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
320 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
321 | github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg=
322 | github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
323 | github.com/vektah/gqlparser/v2 v2.0.1 h1:xgl5abVnsd4hkN9rk65OJID9bfcLSMuTaTcZj777q1o=
324 | github.com/vektah/gqlparser/v2 v2.0.1/go.mod h1:SyUiHgLATUR8BiYURfTirrTcGpcE+4XkV2se04Px1Ms=
325 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
326 | github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
327 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
328 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
329 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
330 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
331 | go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
332 | go.mongodb.org/mongo-driver v1.0.4/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
333 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
334 | go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
335 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
336 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
337 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
338 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
339 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
340 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
341 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
342 | golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
343 | golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
344 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
345 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
346 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
347 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
348 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
349 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
350 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
351 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
352 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
353 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
354 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
355 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
356 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
357 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
358 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
359 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
360 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
361 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
362 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
363 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
364 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
365 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
366 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
367 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
368 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
369 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
370 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
371 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
372 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
373 | golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
374 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
375 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
376 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
377 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
378 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
379 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
380 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
381 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
382 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
383 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
384 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
385 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
386 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
387 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
388 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
389 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
390 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
391 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
392 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
393 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
394 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
395 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
396 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
397 | golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
398 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
399 | golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
400 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
401 | golang.org/x/sys v0.0.0-20190614160838-b47fdc937951/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
402 | golang.org/x/sys v0.0.0-20191009170203-06d7bd2c5f4f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
403 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
404 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
405 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
406 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
407 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
408 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
409 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
410 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
411 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
412 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
413 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
414 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
415 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
416 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
417 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
418 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
419 | golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
420 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
421 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
422 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
423 | golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd h1:oMEQDWVXVNpceQoVd1JN3CQ7LYJJzs5qWqZIUcxXHHw=
424 | golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
425 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
426 | golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
427 | golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
428 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
429 | golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 h1:rjUrONFu4kLchcZTfp3/96bR8bW8dIa8uz3cR5n0cgM=
430 | golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
431 | golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346 h1:hzJjkvxUIF3bSt+v8N5tBQNx/605vszZJ+3XsIamzZo=
432 | golang.org/x/tools v0.0.0-20200925191224-5d1fdd8fa346/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
433 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
434 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
435 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
436 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
437 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
438 | google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
439 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
440 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
441 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
442 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
443 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
444 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
445 | google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
446 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
447 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
448 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
449 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
450 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
451 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
452 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
453 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
454 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
455 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
456 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
457 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
458 | gopkg.in/olivere/elastic.v5 v5.0.80/go.mod h1:uhHoB4o3bvX5sorxBU29rPcmBQdV2Qfg0FBrx5D6pV0=
459 | gopkg.in/olivere/elastic.v5 v5.0.81/go.mod h1:uhHoB4o3bvX5sorxBU29rPcmBQdV2Qfg0FBrx5D6pV0=
460 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
461 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
462 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
463 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
464 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
465 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
466 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
467 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
468 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
469 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
470 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
471 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
472 | sourcegraph.com/sourcegraph/appdash v0.0.0-20180110180208-2cc67fd64755/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
473 | sourcegraph.com/sourcegraph/appdash-data v0.0.0-20151005221446-73f23eafcf67/go.mod h1:L5q+DGLGOQFpo1snNEkLOJT2d1YTW66rWNzatr3He1k=
474 |
--------------------------------------------------------------------------------