├── 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 | 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 | 63 | 64 | 65 | 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 | --------------------------------------------------------------------------------