From cf15664d3186e7829f31807025249a5f792a2d64 Mon Sep 17 00:00:00 2001 From: Craig Johnston Date: Tue, 3 Dec 2019 14:38:41 -0800 Subject: [PATCH] init functional --- .gitignore | 1 + .goreleaser.yml | 67 ++++++++++ Dockerfile | 15 +++ README.md | 14 +++ cmd/jwtpxy.go | 315 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 9 ++ go.sum | 110 +++++++++++++++++ 7 files changed, 531 insertions(+) create mode 100644 .gitignore create mode 100644 .goreleaser.yml create mode 100644 Dockerfile create mode 100644 cmd/jwtpxy.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..7f685ed --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,67 @@ +# Build customization +build: + # Path to main.go file. + # Default is `main.go` + main: ./cmd/jwtpxy.go + binary: jwtpxy + + env: + - CGO_ENABLED=0 + + # GOOS list to build in. + # For more info refer to https://golang.org/doc/install/source#environment + # Defaults are darwin and linux + goos: + - linux + + # GOARCH to build in. + # For more info refer to https://golang.org/doc/install/source#environment + # Defaults are 386 and amd64 + goarch: + - amd64 + + ldflags: -s -w -X main.Version={{.Version}} + +release: + # Repo in which the release will be created. + # Default is extracted from the origin remote URL. + github: + owner: txn2 + name: jwtpxy + + # If set to true, will not auto-publish the release. + # Default is false. + draft: false + + # If set to true, will mark the release as not ready for production. + # Default is false. + prerelease: false + + # You can change the name of the GitHub release. + # Default is `` + name_template: "{{.ProjectName}}-v{{.Version}} {{.Env.USER}}" + + # You can disable this pipe in order to not upload any artifacts to + # GitHub. + # Defaults to false. + disable: false + +dockers: + - + goos: linux + goarch: amd64 + goarm: '' + binaries: + - jwtpxy + dockerfile: Dockerfile + image_templates: + - "txn2/p3y:latest" + - "txn2/p3y:{{ .Tag }}" + - "txn2/p3y:v{{ .Tag }}" + - "txn2/p3y:v{{ .Major }}" + - "txn2/p3y:amd64-{{ .Tag }}" + - "txn2/p3y:amd64-v{{ .Major }}" + build_flag_templates: + - "--label=org.label-schema.schema-version=1.0" + - "--label=org.label-schema.version={{.Version}}" + - "--label=org.label-schema.name={{.ProjectName}}" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c7ba6de --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM alpine3.10 AS util + +RUN echo "nobody:x:65534:65534:Nobody:/:" > /etc_passwd + +FROM scratch + +ENV PATH=/bin + +COPY jwtpxy /bin/ +COPY --from=util /etc_passwd /etc/passwd + +WORKDIR / + +USER nobody +ENTRYPOINT ["/bin/jwtpxy"] \ No newline at end of file diff --git a/README.md b/README.md index 12a462a..75ed5d5 100644 --- a/README.md +++ b/README.md @@ -1 +1,15 @@ # jwtpxy + +## Development + +### Test Release + +```bash +goreleaser --skip-publish --rm-dist --skip-validate +``` + +### Release + +```bash +GITHUB_TOKEN=$GITHUB_TOKEN goreleaser --rm-dist +``` diff --git a/cmd/jwtpxy.go b/cmd/jwtpxy.go new file mode 100644 index 0000000..e23732b --- /dev/null +++ b/cmd/jwtpxy.go @@ -0,0 +1,315 @@ +package main + +import ( + "crypto/rsa" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strconv" + "strings" + "time" + + "github.com/dgrijalva/jwt-go" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.uber.org/zap" +) + +var ( + ipEnv = getEnv("IP", "127.0.0.1") + portEnv = getEnv("PORT", "8080") + metricsPortEnv = getEnv("METRICS_PORT", "2112") + readTimeoutEnv = getEnv("READ_TIMEOUT", "10") + writeTimeoutEnv = getEnv("WRITE_TIMEOUT", "10") + debugEnv = getEnv("DEBUG", "false") + keycloakEnv = getEnv("KEYCLOAK", "http://web-keycloak:8080/auth/realms/master") + backendEnv = getEnv("BACKEND", "http://api:80") +) + +var Version = "0.0.0" + +// Realm info from Keycloak +type RealmInfo struct { + Realm string `json:"realm"` + PublicKey string `json:"public_key"` + TokenService string `json:"token-service"` + AccountService string `json:"account-service"` + TokensNotBefore int `json:"tokens-not-before"` +} + +// JWTConfig +type JWTConfig struct { + PublicKey *rsa.PublicKey `json:"public_key"` +} + +// Proxy +type Proxy struct { + Target *url.URL + Proxy *httputil.ReverseProxy + Logger *zap.Logger + JWTConfig JWTConfig + Pmx *Pmx +} + +// Pmx +type Pmx struct { + Requests prometheus.Counter + Latency prometheus.Summary + AuthFails prometheus.Counter +} + +// handle requests +func (p *Proxy) handle(w http.ResponseWriter, r *http.Request) { + + p.Pmx.Requests.Inc() + + start := time.Now() + reqPath := r.URL.Path + reqMethod := r.Method + r.Host = p.Target.Host + + // get JWT token + authHeader := r.Header.Get("Authorization") + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + if tokenString == "" { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte("Missing token")) + return + } + + // process JWT + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + return p.JWTConfig.PublicKey, nil + }) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte(err.Error())) + return + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + // @TODO map claims to headers from config + r.Header.Add("From", claims["preferred_username"].(string)) + } else { + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte("Invalid token claims")) + return + } + + p.Proxy.ServeHTTP(w, r) + + end := time.Now() + latency := end.Sub(start) + p.Pmx.Latency.Observe(float64(latency)) + + p.Logger.Debug(reqPath, + zap.String("method", reqMethod), + zap.String("path", reqPath), + zap.String("time", end.Format(time.RFC3339)), + zap.Duration("latency", latency), + ) +} + +// use - middleware shim +func use(h http.HandlerFunc, middleware ...func(http.HandlerFunc) http.HandlerFunc) http.HandlerFunc { + for _, m := range middleware { + h = m(h) + } + + return h +} + +func main() { + readTimeoutInt, err := strconv.Atoi(readTimeoutEnv) + if err != nil { + fmt.Println("Parsing error, readTimeout must be an integer in seconds.") + os.Exit(1) + } + + writeTimeoutInt, err := strconv.Atoi(writeTimeoutEnv) + if err != nil { + fmt.Println("Parsing error, readTimeout must be an integer in seconds.") + os.Exit(1) + } + + var ( + ip = flag.String("ip", ipEnv, "Server IP address to bind to.") + port = flag.String("port", portEnv, "Server port.") + metricsPort = flag.String("metricsPort", metricsPortEnv, "Metrics port.") + readTimeout = flag.Int("readTimeout", readTimeoutInt, "HTTP read timeout") + writeTimeout = flag.Int("writeTimeout", writeTimeoutInt, "HTTP write timeout") + keycloak = flag.String("keycloak", keycloakEnv, "Keycloak realm info") + backend = flag.String("backend", backendEnv, "Backend service") + debug = flag.String("debug", debugEnv, "Debug log level") + ) + flag.Parse() + + // Logging + zapCfg := zap.NewProductionConfig() + + if *debug == "true" { + zapCfg.Level = zap.NewAtomicLevelAt(zap.DebugLevel) + } + + logger, err := zapCfg.Build() + if err != nil { + fmt.Printf("Cannot build logger: %s\n", err.Error()) + os.Exit(1) + } + + // HTTP Client for certificate retrieval + netTransport := &http.Transport{ + MaxIdleConnsPerHost: 10, + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 10 * time.Second, + } + + httpClient := &http.Client{ + Timeout: time.Second * 60, + Transport: netTransport, + } + + // Populate public certificate + req, err := http.NewRequest("GET", *keycloak, nil) + if err != nil { + logger.Error("Unable to create request to populate certificate", zap.Error(err)) + os.Exit(1) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := httpClient.Do(req) + if err != nil { + logger.Error("Unable to get realm information", zap.Error(err)) + os.Exit(1) + } + + if resp.StatusCode != http.StatusOK { + logger.Error("Keycloak returned non-200 response", zap.Int("code", resp.StatusCode)) + os.Exit(1) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + logger.Error("Unable read response body from Keycloak", zap.Error(err)) + os.Exit(1) + } + + realmInfo := RealmInfo{} + + err = json.Unmarshal(body, &realmInfo) + if err != nil { + logger.Error("Unable to unmarshal realm information", zap.Error(err)) + os.Exit(1) + } + + // make pem + pem := "-----BEGIN PUBLIC KEY-----\n" + realmInfo.PublicKey + "\n-----END PUBLIC KEY-----" + rsaPublicKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(pem)) + if err != nil { + logger.Error("Unable to ParseRSAPublicKeyFromPEM", zap.Error(err)) + os.Exit(1) + } + + // Backend Configuration + targetUrl, err := url.Parse(*backend) + if err != nil { + logger.Error("Unable to parse URL", zap.Error(err)) + os.Exit(1) + } + + pxy := httputil.NewSingleHostReverseProxy(targetUrl) + + pmx := &Pmx{ + Requests: promauto.NewCounter(prometheus.CounterOpts{ + Name: "jwtpxy_total_requests", + Help: "Total number of requests received.", + }), + + Latency: promauto.NewSummary(prometheus.SummaryOpts{ + Name: "jwtpxy_response_time", + Help: "Response latency.", + }), + } + + // JWTConfig + jwtConfig := JWTConfig{ + PublicKey: rsaPublicKey, + } + + // Proxy + proxy := &Proxy{ + Target: targetUrl, + Proxy: pxy, + Logger: logger, + JWTConfig: jwtConfig, + Pmx: pmx, + } + + mux := http.NewServeMux() + + // handlers / middleware + mux.HandleFunc("/", use(proxy.handle)) + + // HTTP Server + srv := &http.Server{ + Addr: *ip + ":" + *port, + Handler: mux, + ReadTimeout: time.Duration(*readTimeout) * time.Second, + WriteTimeout: time.Duration(*writeTimeout) * time.Second, + } + + // metrics server (run in go routine) + go func() { + http.Handle("/metrics", promhttp.Handler()) + + logger.Info("Starting jwtpxy Metrics Server", + zap.String("type", "metrics_startup"), + zap.String("port", *metricsPort), + zap.String("ip", *ip), + ) + + err = http.ListenAndServe(*ip+":"+*metricsPort, nil) + if err != nil { + logger.Fatal("Error Starting Score API Metrics Server", zap.Error(err)) + os.Exit(1) + } + }() + + logger.Info("Starting the jwtpxy Reverse Proxy", + zap.String("port", *port), + zap.String("ip", *ip), + zap.String("backend", *backend), + ) + + err = srv.ListenAndServe() + if err != nil { + fmt.Printf("Error starting Proxy: %s\n", err.Error()) + } + os.Exit(0) +} + +// getEnv gets an environment variable or sets a default if +// one does not exist. +func getEnv(key, fallback string) string { + value := os.Getenv(key) + if len(value) == 0 { + return fallback + } + + return value +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..444445c --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/txn2/jwtpxy + +go 1.13 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/prometheus/client_golang v1.2.1 // indirect + go.uber.org/zap v1.13.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5fa30d0 --- /dev/null +++ b/go.sum @@ -0,0 +1,110 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA= +github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.2.1 h1:JnMpQc6ppsNgw9QPAGF6Dod479itz7lvlsMzzNayLOI= +github.com/prometheus/client_golang v1.2.1/go.mod h1:XMU6Z2MjaRKVu/dC1qupJI9SiNkDYzz3xecMgSW/F+U= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=