diff --git a/.gitignore b/.gitignore index a7cf351..906386c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ config.toml .idea/ -*.json -gin-bin \ No newline at end of file +gapps.json +*-bin +api/swagger +pkg/api \ No newline at end of file diff --git a/.golangci.toml b/.golangci.toml new file mode 100644 index 0000000..fe11127 --- /dev/null +++ b/.golangci.toml @@ -0,0 +1,10 @@ +[linters] +enable = [ + "gofmt", + "goimports", + "whitespace", + "wsl" +] + +[linters-settings.goimports] +local-prefixes = "github.com/cthit/gotify" \ No newline at end of file diff --git a/DESIGN.md b/DESIGN.md index 4f4e692..4c7bcac 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,22 +1,11 @@ # Design rationale for gotify ## Project structure -The root package/directory only contains the domain types and logic. This will probably be limited to notification service interfaces and notification types. This package may not depend on any other package in this repo. - -cmd contains the main package and takes care of meta sutch as configuration and binding all other packages together, This package may depend on any other package - -All other packages represent a functionality or dependency and may only depend on the root package as well as external packages. - -See [Project structure in go](https://medium.com/@benbjohnson/structuring-applications-in-go-3b04be4ff091) for further explanation. +See [Project structure in go](https://github.com/golang-standards/project-layout) for further explanation. ## API Structure One api endpoint for every notification type. See readme for existing api endpoints -A post request to an endpoint with the matching jason notification type should send a notification and on success return the sent notification in json. - -## Dependency injection - -The web package has some weird dependency injection. - -For now, take a look at it until you understand it. It looks like it does for a good reason. \ No newline at end of file +A post request to an endpoint with the matching json notification type should send a notification and on +success return the sent notification in json. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 5d7833f..e836ac3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,31 @@ +# Dockerfile for protobuf generation +FROM znly/protoc:0.4.0 AS protocGenerator +MAINTAINER digIT + +RUN apk update +RUN apk upgrade +RUN apk add --update git + +# Add standard certificates +RUN apk add ca-certificates && rm -rf /var/cache/apk/* + +# Add proto imports +RUN mkdir -p /src/github.com/grpc-ecosystem +WORKDIR /src/github.com/grpc-ecosystem +RUN git clone https://github.com/grpc-ecosystem/grpc-gateway.git + +# create dir +RUN mkdir /app +WORKDIR /app + +ENTRYPOINT ["/bin/sh"] + +FROM protocGenerator AS protocGen + +COPY . /app + +RUN ./scripts/protoc-gen.sh + # Dockerfile for gotify production FROM golang:alpine AS buildStage MAINTAINER digIT @@ -8,16 +36,15 @@ RUN apk upgrade RUN apk add --update git # Copy sources -RUN mkdir -p $GOPATH/src/github.com/cthit/gotify -COPY . $GOPATH/src/github.com/cthit/gotify -WORKDIR $GOPATH/src/github.com/cthit/gotify/cmd +RUN mkdir /app +COPY --from=protocGen /app /app +WORKDIR /app/cmd/gotify # Grab dependencies -RUN go get -d -v ./... +RUN go mod download # build binary -RUN go install -v -RUN mkdir /app && mv $GOPATH/bin/cmd /app/gotify +RUN go build ########################## # PRODUCTION STAGE # @@ -34,7 +61,7 @@ RUN adduser -S -G app -s /bin/bash app USER app:app # Copy binary -COPY --from=buildStage /app/gotify /app/gotify +COPY --from=buildStage /app/cmd/gotify/gotify /app/gotify # Set good defaults WORKDIR /app diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e7ff74a --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +.PHONY: setup +setup: gen-setup gen + go mod download + +.PHONY: gen-setup +gen-setup: + docker build --target protocGenerator -t gotify-protoc-generator . + +.PHONY: gen +gen: + docker run -v `pwd`:/app gotify-protoc-generator ./scripts/protoc-gen.sh + +.PHONY: build +build: gen + go build -o gotify-bin ./cmd/gotify + +.PHONY: run +run: gen + go run ./cmd/gotify + +.PHONY: dev +dev: + docker-compose up + +.PHONY: clean +clean: + rm -rf pkg/api + git restore api/swagger + +.PHONY: lint +lint: + golangci-lint run + +.PHONY: lint-fix +lint-fix: + golangci-lint run --fix + +.PHONY: lint-docker +lint-docker: + docker run --rm -v `pwd`:/app -w /app golangci/golangci-lint:v1.31.0-alpine golangci-lint run + +.PHONY: test +test: + go test ./... \ No newline at end of file diff --git a/README.md b/README.md index 7a3a139..e366b66 100644 --- a/README.md +++ b/README.md @@ -8,20 +8,18 @@ Currently supports the following notification types: ## Usage How to use the running application -All request must inclue a header with the preshared key. - -`Authorization`: `pre-shared: your...key` - ### Mail POST `/mail` Json Request: -``` +```json5 { - "to": "....", - "from": "....", - "subject": "....", - "body": "...." + "to": "...", + "from": "...", // (optional) + "reply_to": "...", // (optional) + "content_type": "...", // (optional) + "subject": "...", + "body": "..." } ``` @@ -30,29 +28,19 @@ Steps to run the application. this include configuration and key files at the moment ### Config -The application can be configured through a config file or environment variables. Environment variables take precedence. - -#### config.toml -config.toml can reside in your working directory, `/etc/gotify/` or `$HOME/.gotify/` - -``` -port = "8080" -pre-shared-key = "......" -debug-mode = false -mock-mode = false - -[google-mail] - keyfile = "gapps.json" - admin-mail = "admin@example.ex" -``` -See [Environment Variables](#environment-variables) for config explanation +The application is configured through environment variables. #### Environment Variables -* `GOTIFY_PORT`: Port for the web service, defaults to `8080` (string) -* `GOTIFY_PRE-SHARED-KEY`*: Random string used by other apps to authenticate -* `GOTIFY_DEBUG-MODE`: Bool indicating debug mode defaults to `false` -* `GOTIFY_GOOGLE-MAIL.KEYFILE`: the file described in [Google config file](#google-config-file) defaults to `gapps.json` -* `GOTIFY_GOOGLE-MAIL.ADMIN-MAIL`*: The google administrator email. +* `GOTIFY_WEB_PORT`: Port for the web service, defaults to `8080` (string) +* `GOTIFY_RPC_PORT`: Port for the rpc service, defaults to `8090` (string) +* `GOTIFY_DEBUG_MODE`: Bool indicating debug mode defaults to `false` +* `GOTIFY_GOOGLE_MAIL_KEYFILE`: the file described in [Google config file](#google-config-file) defaults +to `gapps.json` +* `GOTIFY_MAIL_DEFAULT_FROM`: Default `from` address in the mail, defaults to `admin@chalmers.it` +* `GOTIFY_MAIL_DEFAULT_REPLY_TO`: Default `reply_to` address in the mail, defaults to `no-reply@chalmers.it` +* `GOTIFY_MAIL_DEFAULT_CONTENT_TYPE`: Default `content_type` in mail, default so `text/html; charset=ISO-8859-1` +* `GOTIFY_MOCK_MODE`: Enable mock mode, defaults to `false` +* `GOTIFY_ENVIRONMENT`: (`test` | `production` | `development`), defaults to `development` ### Google config file This file (gapps.json by default config) should be placed in the working directory @@ -63,6 +51,7 @@ This file (gapps.json by default config) should be placed in the working directo Go to [Google developer console](https://console.developers.google.com) to retrieve this file * go to credentials +* create a project for this app if you don't already have one * create new service account för this app * use the downloaded file @@ -74,45 +63,18 @@ You must also allow mail api calls: * use api scope `https://www.googleapis.com/auth/gmail.send` ## Development -You can either set this project up manually or with a simple docker compose setup. The manual setup is recommended if you'll be doing extensive development. - -Please referer to the software design document before starting development: `DESIGN.md` - -See issues for suggested features. -### Manual -Make sure you have golang installed and you `$GOPATH` setup. -1. Follow the steps in [Setup](#setup) and enable debug mode. -2. Grab all dependencies by standing in the project root and run `go get -d ./...` -3. You find the main file in `cmd/main.go` -4. Go to http://localhost:8080 - -Use gin for hot reloading. -1. Grab it with `go get github.com/codegangsta/gin` -2. Run gotify with `gin -d cmd -a 8080 run main.go` -3. Go to http://localhost:3000 - -### Docker Compose -1. Get a [Google key file](#google-config-file). -2. Run `docker-compose up --build` -3. Go to http://localhost:8080 - -You can install additional dependencies without restarting the container by running `docker exec gotify_web_1 go get ...`, gotify_web_1 is the name of the container and ... is the dependency. - -### As mock -1. Set the `pre-shared-key` config/environment variable. -2. Set the `mock-mode` config/environment variable to true -3. Enjoy - -Example docker-compose entry for mock service: +To start a dockerized development environment with hot-reloading: +```bash +$ make dev ``` -services: - ... - gotify: - image: cthit/gotify:latest - environment: - GOTIFY_PRE-SHARED-KEY: "123abc" - GOTIFY_MOCK-MODE: "true" +To start a non-dockerized development environment: +```bash +$ make run ``` -Other services would then be able to reach this service on `http://gotify:8080/...` with `123abc` as the preshared key \ No newline at end of file +Please referer to the software design document before starting development: `DESIGN.md` + +### As mock +1. Set the `mock-mode` config/environment variable to true +2. Enjoy diff --git a/api/proto/v1/mail.proto b/api/proto/v1/mail.proto new file mode 100644 index 0000000..8f3acd6 --- /dev/null +++ b/api/proto/v1/mail.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package gotify; + +import "google/api/annotations.proto"; +import "protoc-gen-swagger/options/annotations.proto"; + +option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = { + host: "localhost:8080" + schemes: HTTP; +}; + +service Mailer { + rpc SendMail(Mail) returns (Mail) { + option(google.api.http) = { + post: "/mail" + body: "*" + }; + } +} + +message Mail { + string to = 1; + string from = 2; + string reply_to = 3; + string subject = 4; + string content_type = 5; + string body = 6; +} \ No newline at end of file diff --git a/api/swagger/v1/apidocs.swagger.json b/api/swagger/v1/apidocs.swagger.json new file mode 100644 index 0000000..81921ef --- /dev/null +++ b/api/swagger/v1/apidocs.swagger.json @@ -0,0 +1 @@ +PLACEHOLDER FILE \ No newline at end of file diff --git a/cmd/config.go b/cmd/config.go deleted file mode 100644 index bcc0a14..0000000 --- a/cmd/config.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "github.com/spf13/viper" -) - -func loadConfig() error { - viper.SetDefault("port", "8080") - viper.SetDefault("debug-mode", false) - viper.SetDefault("google-mail.keyfile", "gapps.json") - viper.SetDefault("mock-mode", false) - - viper.SetEnvPrefix("gotify") - viper.AutomaticEnv() - - viper.SetConfigName("config") // name of config file (without extension) - viper.AddConfigPath("/etc/gotify/") // path to look for the config file in - viper.AddConfigPath("$HOME/.gotify/") // call multiple times to add many search paths - viper.AddConfigPath(".") // optionally look for config in the working directory - - err := viper.ReadInConfig() // Find and read the config file - return err - -} diff --git a/cmd/gotify/main.go b/cmd/gotify/main.go new file mode 100644 index 0000000..5d57462 --- /dev/null +++ b/cmd/gotify/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "os" + + "github.com/cthit/gotify/internal/app" +) + +func main() { + err := app.Start() + if err != nil { + fmt.Printf("Crash: %v\n", err) + os.Exit(1) + } + + os.Exit(0) +} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index b252f7f..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,60 +0,0 @@ -package main - -import ( - "fmt" - "github.com/cthit/gotify" - "github.com/cthit/gotify/google_mail" - "github.com/cthit/gotify/mock" - "github.com/cthit/gotify/web" - "github.com/spf13/viper" - "log" - "net/http" -) - -func init() { - err := loadConfig() - if err != nil { - fmt.Println("Failed to load config.") - } else { - fmt.Println("Loaded config.") - - } -} - -func main() { - fmt.Printf("Debug mode is set to: %t \n", viper.GetBool("debug-mode")) - fmt.Printf("Mock mode is set to: %t \n", viper.GetBool("mock-mode")) - - fmt.Printf("Setting up services...") - - var mailServiceCreator func() gotify.MailService - var err error - - if !viper.GetBool("mock-mode") { - mailServiceCreator, err = google_mail.NewGoogleMailServiceCreator( - viper.GetString("google-mail.keyfile"), - viper.GetString("google-mail.admin-mail"), - viper.GetBool("debug-mode"), - ) - if err != nil { - panic(err) - } - } else { - mailServiceCreator, _ = mock.NewMockServiceCreator() - } - - preSharedKey := viper.GetString("pre-shared-key") - - fmt.Printf("Done! \n") - - fmt.Printf("Serving application on port %s \n", viper.GetString("port")) - log.Fatal( - http.ListenAndServe(":"+viper.GetString("port"), - web.Router( - preSharedKey, - mailServiceCreator, - viper.GetBool("debug-mode"), - ), - ), - ) -} diff --git a/dev.Dockerfile b/dev.Dockerfile index c783e65..0efeea3 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -1,5 +1,5 @@ # Dockerfile for gotify development -FROM golang:alpine +FROM golang:alpine as dev MAINTAINER digIT # Install git @@ -7,14 +7,41 @@ RUN apk update RUN apk upgrade RUN apk add --update git -RUN mkdir -p $GOPATH/bin && \ - go get github.com/codegangsta/gin +RUN go get -u github.com/cespare/reflex # Add standard certificates RUN apk add ca-certificates && rm -rf /var/cache/apk/* # create dir -RUN mkdir -p /go/src/github.com/cthit/gotify -WORKDIR $GOPATH/src/github.com/cthit/gotify +RUN mkdir /app +WORKDIR /app + +RUN which reflex + +CMD reflex -r '\.go$' -s -- go run ./cmd/gotify + +# Dockerfile for protobuf generation +FROM znly/protoc:0.4.0 as dev_gen +MAINTAINER digIT + +RUN apk update +RUN apk upgrade +RUN apk add --update git + +# Add standard certificates +RUN apk add ca-certificates && rm -rf /var/cache/apk/* + +# Add proto imports +RUN mkdir -p /src/github.com/grpc-ecosystem +WORKDIR /src/github.com/grpc-ecosystem +RUN git clone https://github.com/grpc-ecosystem/grpc-gateway.git + +COPY --from=dev /go/bin/reflex /bin/reflex + +# create dir +RUN mkdir /app +WORKDIR /app + + +ENTRYPOINT ["/bin/sh"] -CMD go get -d -v ./... && gin -d cmd -a 8080 run main.go diff --git a/docker-compose.yml b/docker-compose.yml index 7e8f32b..2b7f228 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,33 @@ -version: '3' +version: '3.4' services: - web: + dev: build: dockerfile: dev.Dockerfile context: ./ + target: dev ports: - - "8080:3000" + - "8080:8080" + - "8090:8090" volumes: - - .:/go/src/github.com/cthit/gotify + - .:/app environment: - GOTIFY_PRE-SHARED-KEY: "123abc" - GOTIFY_DEBUG-MODE: "true" - GOTIFY_GOOGLE-MAIL.KEYFILE: "gapps.json" - GOTIFY_GOOGLE-MAIL.ADMIN-MAIL: "admin@chalmers.it" \ No newline at end of file + GOTIFY_DEBUG_MODE: "true" + GOTIFY_MOCK_MODE: "true" + GOTIFY_GOOGLE_MAIL_KEYFILE: "gapps.json" + dev-gen: + build: + dockerfile: dev.Dockerfile + context: ./ + target: dev_gen + volumes: + - .:/app + command: ./scripts/proto-watcher.sh + swagger: + image: swaggerapi/swagger-ui + ports: + - "8000:8000" + environment: + PORT: "8000" + SWAGGER_JSON: "/app/v1/apidocs.swagger.json" + volumes: + - ./api/swagger:/app \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..af0ab03 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/cthit/gotify + +go 1.14 + +require ( + github.com/golang/protobuf v1.3.4 + github.com/grpc-ecosystem/grpc-gateway v1.9.0 + github.com/pkg/errors v0.8.0 + github.com/spf13/viper v1.5.0 + github.com/stretchr/testify v1.4.0 // indirect + golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914 + golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect + google.golang.org/api v0.14.0 + google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 + google.golang.org/grpc v1.27.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ab1327b --- /dev/null +++ b/go.sum @@ -0,0 +1,224 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +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/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-kit/kit v0.8.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/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +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/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0 h1:bM6ZAFZmc/wPFaRDi0d5L7hGEZEx/2u+Tmr2evNHDiI= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +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 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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 v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +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/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.5.0 h1:GpsTwfsQ27oS/Aha/6d1oD7tpKIqWnOA6tgOX9HHkt4= +github.com/spf13/viper v1.5.0/go.mod h1:AkYRkVJF8TkSG/xet6PzXX+l39KhhXa2pdqVSxnTcn4= +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 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914 h1:MlY3mEfbnWGmUi4rtHOtNnnnN4UJRGSyLPx+DXA5Sq4= +golang.org/x/net v0.0.0-20191119073136-fc4aabc6c914/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/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-20190507160741-ecd444e8653b h1:ag/x1USPSsqHud38I9BAC88qdNLDHHtQ4mlgQIZPPNA= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.14.0 h1:uMf5uLi4eQMRrMKhCplNik4U4H8Z6C1br3zOtAa/aDE= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/google_mail/google_service.go b/google_mail/google_service.go deleted file mode 100644 index 7b07737..0000000 --- a/google_mail/google_service.go +++ /dev/null @@ -1,78 +0,0 @@ -package google_mail - -import ( - "google.golang.org/api/gmail/v1" // Imports as gmail - - "golang.org/x/net/context" - "golang.org/x/oauth2/google" - - "encoding/base64" - "io/ioutil" - - "github.com/cthit/gotify" -) - -type googleService struct { - mailService *gmail.Service - adminMail string - debug bool -} - -func NewGoogleMailServiceCreator(keyPath string, adminMail string, debug bool) (func() gotify.MailService, error) { - - jsonKey, err := ioutil.ReadFile(keyPath) - if err != nil { - return nil, err - } - - // Parse jsonKey - config, err := google.JWTConfigFromJSON(jsonKey, gmail.GmailSendScope) - if err != nil { - return nil, err - } - - // Why do I need this?? - config.Subject = adminMail - - // Create a http client - client := config.Client(context.Background()) - - mailService, err := gmail.New(client) - if err != nil { - return nil, err - } - - gs := &googleService{ - mailService: mailService, - adminMail: adminMail, - debug: debug, - } - if err != nil { - return nil, err - } - - return func() gotify.MailService { - return gs - }, nil -} - -func (g *googleService) SendMail(mail gotify.Mail) (gotify.Mail, error) { - - mail.From = g.adminMail - - msgRaw := "From: " + mail.From + "\r\n" + - "To: " + mail.To + "\r\n" + - "Subject: " + mail.Subject + "\r\n\r\n" + - mail.Body + "\r\n" - - msg := &gmail.Message{ - Raw: base64.RawURLEncoding.EncodeToString([]byte(msgRaw)), - } - _, err := g.mailService.Users.Messages.Send(mail.From, msg).Do() - - return mail, err -} - -func (g *googleService) Destroy() error { - return nil -} diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..3d925b7 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,64 @@ +package app + +import ( + "github.com/cthit/gotify/internal/app/config" + "github.com/cthit/gotify/internal/app/grpc" + "github.com/cthit/gotify/pkg/mail" + "github.com/cthit/gotify/pkg/mail/gmail" + "github.com/cthit/gotify/pkg/mail/mock" + + "fmt" +) + +func Start() error { + c, err := config.LoadConfig() + if err != nil { + fmt.Println("Failed to load config.") + return err + } else { + fmt.Println("Loaded config.") + } + + fmt.Printf("Debug mode is set to: %t \n", c.Debug()) + fmt.Printf("Mock mode is set to: %t \n", c.Mock()) + fmt.Printf("Environment is set to: %s \n", c.Environment()) + + fmt.Printf("Setting up services...") + + var mailService mail.Service + + if !c.Mock() { + mailService, err = gmail.NewService( + c.GmailKeyfile(), + c.Debug(), + ) + if err != nil { + return err + } + } else { + mailService, _ = mock.NewService() + } + + mailService = mail.NewService(mailService, c.MailDefaultFrom(), c.MailDefaultReplyTo(), c.MailDefaultContentType()) + + fmt.Printf("Done! \n") + + fmt.Printf("Serving application on port %s \n", c.WebPort()) + fmt.Printf("Serving rpc on port %s \n", c.RPCPort()) + + server, err := grpc.NewServer( + c.RPCPort(), + c.WebPort(), + c.Environment(), + c.Debug(), + mailService, + ) + if err != nil { + fmt.Println("Failed to create webserver.") + return err + } + + server.Start() + + return nil +} diff --git a/internal/app/config/config.go b/internal/app/config/config.go new file mode 100644 index 0000000..39b5340 --- /dev/null +++ b/internal/app/config/config.go @@ -0,0 +1,84 @@ +package config + +import ( + "fmt" + "strings" + + "github.com/spf13/viper" +) + +type Config struct{} + +func LoadConfig() (*Config, error) { + viper.SetDefault("web-port", "8080") + viper.SetDefault("rpc-port", "8090") + viper.SetDefault("debug-mode", false) + viper.SetDefault("google-mail.keyfile", "gapps.json") + viper.SetDefault("mail.default-from", "admin@chalmers.it") + viper.SetDefault("mail.default-reply-to", "no-reply@chalmers.it") + viper.SetDefault("mail.default-content-type", "text/html; charset=ISO-8859-1") + viper.SetDefault("mock-mode", false) + viper.SetDefault("environment", EnvDevelopment) + + viper.SetEnvPrefix("gotify") + viper.AutomaticEnv() + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) + + viper.SetConfigName("config") + viper.AddConfigPath("/etc/gotify/") + viper.AddConfigPath(".") + + err := viper.ReadInConfig() + if err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { //nolint:gosimple + fmt.Println("Failed to read config from file") + } else { + return &Config{}, err + } + } + + return &Config{}, nil +} + +func (*Config) WebPort() string { + return viper.GetString("web-port") +} + +func (*Config) Debug() bool { + return viper.GetBool("debug-mode") +} + +func (*Config) RPCPort() string { + return viper.GetString("rpc-port") +} + +func (*Config) Mock() bool { + return viper.GetBool("mock-mode") +} + +func (*Config) Environment() string { + switch viper.GetString("environment") { + case EnvTest: + return EnvTest + case EnvProduction: + return EnvProduction + default: + return EnvDevelopment + } +} + +func (*Config) GmailKeyfile() string { + return viper.GetString("google-mail.keyfile") +} + +func (*Config) MailDefaultFrom() string { + return viper.GetString("mail.default-from") +} + +func (*Config) MailDefaultReplyTo() string { + return viper.GetString("mail.default-reply-to") +} + +func (*Config) MailDefaultContentType() string { + return viper.GetString("mail.default-content-type") +} diff --git a/internal/app/config/environment.go b/internal/app/config/environment.go new file mode 100644 index 0000000..6967771 --- /dev/null +++ b/internal/app/config/environment.go @@ -0,0 +1,7 @@ +package config + +const ( + EnvProduction = "production" + EnvDevelopment = "development" + EnvTest = "test" +) diff --git a/internal/app/grpc/cors.go b/internal/app/grpc/cors.go new file mode 100644 index 0000000..4f629c5 --- /dev/null +++ b/internal/app/grpc/cors.go @@ -0,0 +1,22 @@ +package grpc + +import ( + "net/http" +) + +// allowCORS allows Cross Origin Resoruce Sharing from any origin. +func allowCORS(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + if origin != "" { + w.Header().Set("Access-Control-Allow-Origin", origin) + + if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" { + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization") + w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE") + return + } + } + h.ServeHTTP(w, r) + }) +} diff --git a/internal/app/grpc/errors.go b/internal/app/grpc/errors.go new file mode 100644 index 0000000..7c9c26b --- /dev/null +++ b/internal/app/grpc/errors.go @@ -0,0 +1,22 @@ +package grpc + +import ( + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type grpcError struct { + error + status codes.Code +} + +func (ge grpcError) GRPCStatus() *status.Status { + return status.New(ge.status, ge.Error()) +} + +func WithErrorStatus(err error, status codes.Code) grpcError { + return grpcError{ + error: err, + status: status, + } +} \ No newline at end of file diff --git a/internal/app/grpc/mail.go b/internal/app/grpc/mail.go new file mode 100644 index 0000000..b46fc23 --- /dev/null +++ b/internal/app/grpc/mail.go @@ -0,0 +1,42 @@ +package grpc + +import ( + "context" + + "google.golang.org/grpc/codes" + + gotify "github.com/cthit/gotify/pkg/api/v1" + "github.com/cthit/gotify/pkg/mail" + + "github.com/pkg/errors" +) + +func (s *Server) SendMail(_ context.Context, in *gotify.Mail) (*gotify.Mail, error) { + m := mail.Mail{ + To: in.To, + From: in.From, + ReplyTo: in.ReplyTo, + Subject: in.Subject, + ContentType: in.ContentType, + Body: in.Body, + } + + err := mail.Validate(m) + if err != nil { + return nil, WithErrorStatus(err, codes.InvalidArgument) + } + + m, err = s.mailService.SendMail(m) + if err != nil { + return nil, WithErrorStatus(errors.Wrap(err, "failed to send mail"), codes.Internal) + } + + return &gotify.Mail{ + To: m.To, + From: m.From, + ReplyTo: m.ReplyTo, + Subject: m.Subject, + ContentType: m.ContentType, + Body: m.Body, + }, nil +} diff --git a/internal/app/grpc/server.go b/internal/app/grpc/server.go new file mode 100644 index 0000000..4181291 --- /dev/null +++ b/internal/app/grpc/server.go @@ -0,0 +1,87 @@ +package grpc + +import ( + "context" + "fmt" + "net" + "net/http" + "sync" + + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "google.golang.org/grpc" + + "github.com/cthit/gotify/internal/app/config" + gotify "github.com/cthit/gotify/pkg/api/v1" + "github.com/cthit/gotify/pkg/mail" +) + +type Server struct { + rpcPort, webPort string + debug bool + env string + mailService mail.Service + wg sync.WaitGroup +} + +func NewServer(rpcPort, webPort string, env string, debug bool, mailService mail.Service) (*Server, error) { + return &Server{ + rpcPort: rpcPort, + webPort: webPort, + debug: debug, + env: env, + mailService: mailService, + }, nil +} + +func (s *Server) Start() { + s.wg.Add(1) + + go func() { + err := s.startGRPC() + fmt.Println(err) + s.wg.Done() + }() + + s.wg.Add(1) + + go func() { + err := s.startREST() + fmt.Println(err) + s.wg.Done() + }() + + s.wg.Wait() +} + +func (s *Server) startGRPC() error { + lis, err := net.Listen("tcp", ":"+s.rpcPort) + if err != nil { + return err + } + + grpcServer := grpc.NewServer() + + gotify.RegisterMailerServer(grpcServer, s) + + return grpcServer.Serve(lis) +} + +func (s *Server) startREST() error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var mux http.Handler = runtime.NewServeMux() + + opts := []grpc.DialOption{grpc.WithInsecure()} + + err := gotify.RegisterMailerHandlerFromEndpoint(ctx, mux.(*runtime.ServeMux), "localhost:"+s.rpcPort, opts) + if err != nil { + return err + } + + if s.env == config.EnvDevelopment { + mux = allowCORS(mux) + } + + return http.ListenAndServe(":"+s.webPort, mux) +} diff --git a/internal/app/tools/tools.go b/internal/app/tools/tools.go new file mode 100644 index 0000000..4283b20 --- /dev/null +++ b/internal/app/tools/tools.go @@ -0,0 +1,10 @@ +// +build tools + +package tools + +// Makes sure the right packages are imported +import ( +_ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway" +_ "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger" +_ "github.com/golang/protobuf/protoc-gen-go" +) \ No newline at end of file diff --git a/internal/validation/strings.go b/internal/validation/strings.go new file mode 100644 index 0000000..ab21936 --- /dev/null +++ b/internal/validation/strings.go @@ -0,0 +1,64 @@ +package validation + +import ( + "errors" + "regexp" + "strings" +) + +var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + +func OrString(errFuncs ...func(string) error) func(string) error { + return func(s string) error { + errs := make([]error, len(errFuncs)) + for i, errFunc := range errFuncs { + errs[i] = errFunc(s) + } + + return Or(errs...) + } +} + +func AndString(errFuncs ...func(string) error) func(string) error { + return func(s string) error { + errs := make([]error, len(errFuncs)) + for i, errFunc := range errFuncs { + errs[i] = errFunc(s) + } + + return And(errs...) + } +} + +func FieldString(name string, value string, errFuncs ...func(string) error) error { + errs := make([]error, len(errFuncs)) + for i, errFunc := range errFuncs { + errs[i] = errFunc(value) + } + + return Field(name, errs...) +} + +func IsEmpty(s string) error { + if strings.TrimSpace(s) == "" { + return nil + } + + return errors.New("should be empty") +} + +func IsNotEmpty(s string) error { + if strings.TrimSpace(s) != "" { + return nil + } + + return errors.New("should not be empty") +} + +func IsEmail(s string) error { + if emailRegexp.MatchString(s) { + return nil + } + + return errors.New("should be an email address") +} \ No newline at end of file diff --git a/internal/validation/strings_test.go b/internal/validation/strings_test.go new file mode 100644 index 0000000..d77d0cf --- /dev/null +++ b/internal/validation/strings_test.go @@ -0,0 +1,326 @@ +package validation + +import ( + "testing" + + "github.com/pkg/errors" +) + +func willFail(s string) error { + return errors.New("always fails") +} +func willPass(s string) error { + return nil +} + +var stringAndTests = []struct { + name string + errFuncs []func(string) error + wantErr bool +}{ + { + name: "no ok", + errFuncs: []func(string) error{ + willFail, + willFail, + willFail, + }, + wantErr: true, + }, + { + name: "fail first", + errFuncs: []func(string) error{ + willFail, + willPass, + willPass, + }, + wantErr: true, + }, + { + name: "fail last", + errFuncs: []func(string) error{ + willPass, + willPass, + willFail, + }, + wantErr: true, + }, + { + name: "fail middle", + errFuncs: []func(string) error{ + willPass, + willFail, + willPass, + }, + wantErr: true, + }, + { + name: "two ok ", + errFuncs: []func(string) error{ + willFail, + willPass, + willPass, + }, + wantErr: true, + }, + { + name: "all ok ", + errFuncs: []func(string) error{ + willPass, + willPass, + willPass, + }, + wantErr: false, + }, + { + name: "one fail ", + errFuncs: []func(string) error{ + willFail, + }, + wantErr: true, + }, + { + name: "one ok ", + errFuncs: []func(string) error{ + willPass, + }, + wantErr: false, + }, + { + name: "none", + errFuncs: []func(string) error{}, + wantErr: false, + }, +} + +func TestAndString(t *testing.T) { + for _, tt := range stringAndTests { + t.Run(tt.name, func(t *testing.T) { + if err := AndString(tt.errFuncs...)(""); (err != nil) != tt.wantErr { + t.Errorf("AndString()() = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestOrString(t *testing.T) { + tests := []struct { + name string + errFuncs []func(string) error + wantErr bool + }{ + { + name: "no ok", + errFuncs: []func(string) error{ + willFail, + willFail, + willFail, + }, + wantErr: true, + }, + { + name: "ok first", + errFuncs: []func(string) error{ + willPass, + willFail, + willFail, + }, + wantErr: false, + }, + { + name: "ok last", + errFuncs: []func(string) error{ + willFail, + willFail, + willPass, + }, + wantErr: false, + }, + { + name: "ok middle", + errFuncs: []func(string) error{ + willFail, + willPass, + willFail, + }, + wantErr: false, + }, + { + name: "two ok ", + errFuncs: []func(string) error{ + willFail, + willPass, + willPass, + }, + wantErr: false, + }, + { + name: "all ok ", + errFuncs: []func(string) error{ + willPass, + willPass, + willPass, + }, + wantErr: false, + }, + { + name: "one fail ", + errFuncs: []func(string) error{ + willFail, + }, + wantErr: true, + }, + { + name: "one ok ", + errFuncs: []func(string) error{ + willPass, + }, + wantErr: false, + }, + { + name: "none", + errFuncs: []func(string) error{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := OrString(tt.errFuncs...)(""); (err != nil) != tt.wantErr { + t.Errorf("OrString()() = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestFieldString(t *testing.T) { + for _, tt := range stringAndTests { + t.Run(tt.name, func(t *testing.T) { + if err := FieldString("test", "value", tt.errFuncs...); (err != nil) != tt.wantErr { + t.Errorf("FieldString() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestIsEmail(t *testing.T) { + tests := []struct { + name string + s string + wantErr bool + }{ + { + name: "none", + s: "", + wantErr: true, + }, + { + name: "spaces", + s: " ", + wantErr: true, + }, + { + name: "no at", + s: "asdjhfb.com", + wantErr: true, + }, + { + name: "no dot", + s: "aaa@asdjhfb,com", + wantErr: true, + }, + { + name: "no beginning", + s: "@asdjhfb.com", + wantErr: true, + }, + { + name: "no end", + s: "aaa@asdjhfb.", + wantErr: true, + }, + { + name: "no middle", + s: "aaa@.asdjhfb", + wantErr: true, + }, + { + name: "valid", + s: "aaa@asdjhfb.com", + wantErr: false, + }, + { + name: "valid with dots", + s: "aa.aa.bb.cc@as.dj.h.fb.com", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := IsEmail(tt.s); (err != nil) != tt.wantErr { + t.Errorf("IsEmail() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestIsEmpty(t *testing.T) { + tests := []struct { + name string + s string + wantErr bool + }{ + { + name: "empty", + s: "", + wantErr: false, + }, + { + name: "not empty", + s: "a", + wantErr: true, + }, + { + name: "spaces", + s: " ", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := IsEmpty(tt.s); (err != nil) != tt.wantErr { + t.Errorf("IsEmpty() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestIsNotEmpty(t *testing.T) { + tests := []struct { + name string + s string + wantErr bool + }{ + { + name: "empty", + s: "", + wantErr: true, + }, + { + name: "not empty", + s: "a", + wantErr: false, + }, + { + name: "spaces", + s: " ", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := IsNotEmpty(tt.s); (err != nil) != tt.wantErr { + t.Errorf("IsNotEmpty() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} \ No newline at end of file diff --git a/internal/validation/validation.go b/internal/validation/validation.go new file mode 100644 index 0000000..e5bc079 --- /dev/null +++ b/internal/validation/validation.go @@ -0,0 +1,49 @@ +package validation + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" +) + +func Or(errs ...error) error { + var messages []string + + for _, err := range errs { + if err != nil { + messages = append(messages, err.Error()) + } else { + return nil + } + } + + return fmt.Errorf("should satisfy at least one of (%s)", strings.Join(messages, ", ")) +} + +func And(errs ...error) error { + var messages []string + + for _, err := range errs { + if err != nil { + messages = append(messages, err.Error()) + } + } + + if len(messages) == 0 { + return nil + } else if len(messages) == 1 { + return errors.New(messages[0]) + } + + return fmt.Errorf("should satisfy all of (%s)", strings.Join(messages, ", ")) +} + +func Field(name string, errs ...error) error { + err := And(errs...) + if err == nil { + return nil + } + + return errors.Wrapf(err, "field '%s' failed validation", name) +} \ No newline at end of file diff --git a/internal/validation/validation_test.go b/internal/validation/validation_test.go new file mode 100644 index 0000000..48564a0 --- /dev/null +++ b/internal/validation/validation_test.go @@ -0,0 +1,198 @@ +package validation + +import ( + "testing" + + "github.com/pkg/errors" +) + +var e = errors.New("") + +var andTests = []struct { + name string + errs []error + wantErr bool +}{ + { + name: "no ok", + errs: []error{ + e, + e, + e, + }, + wantErr: true, + }, + { + name: "fail first", + errs: []error{ + e, + nil, + nil, + }, + wantErr: true, + }, + { + name: "fail last", + errs: []error{ + nil, + nil, + e, + }, + wantErr: true, + }, + { + name: "fail middle", + errs: []error{ + nil, + e, + nil, + }, + wantErr: true, + }, + { + name: "two ok ", + errs: []error{ + e, + nil, + nil, + }, + wantErr: true, + }, + { + name: "all ok ", + errs: []error{ + nil, + nil, + nil, + }, + wantErr: false, + }, + { + name: "one fail ", + errs: []error{ + e, + }, + wantErr: true, + }, + { + name: "one ok ", + errs: []error{ + nil, + }, + wantErr: false, + }, + { + name: "none", + errs: []error{}, + wantErr: false, + }, +} + +func TestAnd(t *testing.T) { + for _, tt := range andTests { + t.Run(tt.name, func(t *testing.T) { + if err := And(tt.errs...); (err != nil) != tt.wantErr { + t.Errorf("And() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestField(t *testing.T) { + for _, tt := range andTests { + t.Run(tt.name, func(t *testing.T) { + if err := Field("", tt.errs...); (err != nil) != tt.wantErr { + t.Errorf("Field() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestOr(t *testing.T) { + tests := []struct { + name string + errs []error + wantErr bool + }{ + { + name: "no ok", + errs: []error{ + e, + e, + e, + }, + wantErr: true, + }, + { + name: "ok first", + errs: []error{ + nil, + e, + e, + }, + wantErr: false, + }, + { + name: "ok last", + errs: []error{ + e, + e, + nil, + }, + wantErr: false, + }, + { + name: "ok middle", + errs: []error{ + e, + nil, + e, + }, + wantErr: false, + }, + { + name: "two ok ", + errs: []error{ + e, + nil, + nil, + }, + wantErr: false, + }, + { + name: "all ok ", + errs: []error{ + nil, + nil, + nil, + }, + wantErr: false, + }, + { + name: "one fail ", + errs: []error{ + e, + }, + wantErr: true, + }, + { + name: "one ok ", + errs: []error{ + nil, + }, + wantErr: false, + }, + { + name: "none", + errs: []error{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := Or(tt.errs...); (err != nil) != tt.wantErr { + t.Errorf("Or() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} \ No newline at end of file diff --git a/mail.go b/mail.go deleted file mode 100644 index 7cf0d81..0000000 --- a/mail.go +++ /dev/null @@ -1,8 +0,0 @@ -package gotify - -type Mail struct { - To string `json:"to"` - From string `json:"from"` - Subject string `json:"subject"` - Body string `json:"body"` -} diff --git a/mock/mock_service.go b/mock/mock_service.go deleted file mode 100644 index 3d0d6f3..0000000 --- a/mock/mock_service.go +++ /dev/null @@ -1,26 +0,0 @@ -package mock - -import ( - "fmt" - "github.com/cthit/gotify" -) - -type mockService struct { -} - -func NewMockServiceCreator() (func() gotify.MailService, error) { - return func() gotify.MailService { - return &mockService{} - }, nil -} - -func (g *mockService) SendMail(mail gotify.Mail) (gotify.Mail, error) { - - fmt.Printf("Sending mail:\n %#v \n", mail) - - return mail, nil -} - -func (g *mockService) Destroy() error { - return nil -} diff --git a/pkg/mail/defaults_wrapper.go b/pkg/mail/defaults_wrapper.go new file mode 100644 index 0000000..d0fb435 --- /dev/null +++ b/pkg/mail/defaults_wrapper.go @@ -0,0 +1,39 @@ +package mail + +import "strings" + +type DefaultsWrapper struct { + ms Service + mailDefaultFromAddress string + mailDefaultReplyToAddress string + mailDefaultContentType string +} + +func NewService(ms Service, mailDefaultFromAddress, mailDefaultReplyToAddress, mailDefaultContentType string) *DefaultsWrapper { + return &DefaultsWrapper{ + ms: ms, + mailDefaultFromAddress: mailDefaultFromAddress, + mailDefaultReplyToAddress: mailDefaultReplyToAddress, + mailDefaultContentType: mailDefaultContentType, + } +} + +func (w DefaultsWrapper) SendMail(mail Mail) (Mail, error) { + if strings.TrimSpace(mail.From) == "" { + mail.From = w.mailDefaultFromAddress + } + + if strings.TrimSpace(mail.ReplyTo) == "" { + mail.ReplyTo = w.mailDefaultReplyToAddress + } + + if strings.TrimSpace(mail.ContentType) == "" { + mail.ContentType = w.mailDefaultContentType + } + + return w.ms.SendMail(mail) +} + +func (w DefaultsWrapper) Destroy() error { + return w.ms.Destroy() +} diff --git a/pkg/mail/gmail/service.go b/pkg/mail/gmail/service.go new file mode 100644 index 0000000..4055668 --- /dev/null +++ b/pkg/mail/gmail/service.go @@ -0,0 +1,87 @@ +package gmail + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "strings" + + "golang.org/x/net/context" + "golang.org/x/oauth2/google" + "golang.org/x/oauth2/jwt" + "google.golang.org/api/gmail/v1" + "google.golang.org/api/option" + + "github.com/cthit/gotify/pkg/mail" +) + +const googleInvalidEmailErrorMessage = `Response: { + "error": "invalid_grant", + "error_description": "Invalid email or User ID" +}` + +type googleService struct { + config jwt.Config + debug bool +} + +func (g *googleService) mailService(from string) (*gmail.Service, error) { + // make sure to not edit the original config + c := g.config + c.Subject = from + + return gmail.NewService(context.Background(), option.WithScopes(gmail.GmailSendScope), option.WithTokenSource(c.TokenSource(context.TODO()))) +} + +func NewService(keyPath string, debug bool) (mail.Service, error) { + jsonKey, err := ioutil.ReadFile(keyPath) + if err != nil { + return nil, err + } + + // Parse jsonKey + config, err := google.JWTConfigFromJSON(jsonKey, gmail.GmailSendScope) + if err != nil { + return nil, err + } + + gs := &googleService{ + config: *config, + debug: debug, + } + + return gs, err +} + +func (g *googleService) SendMail(m mail.Mail) (mail.Mail, error) { + mailService, err := g.mailService(m.From) + if err != nil { + return m, err + } + + msgRaw := "From: " + m.From + "\r\n" + + "To: " + m.To + "\r\n" + + "Reply-To: " + m.ReplyTo + "\r\n" + + "Content-Type: " + m.ContentType + "\r\n" + + "Subject: " + mail.EncodeHeader(m.Subject) + "\r\n\r\n" + + m.Body + "\r\n" + + msg := &gmail.Message{ + Raw: base64.RawURLEncoding.EncodeToString([]byte(msgRaw)), + } + + _, err = mailService.Users.Messages.Send(m.From, msg).Context(context.Background()).Do() + if err != nil { + if strings.Contains(err.Error(), googleInvalidEmailErrorMessage) { + return m, fmt.Errorf("Invalid from email, email must exists") + } + + return m, err + } + + return m, nil +} + +func (g *googleService) Destroy() error { + return nil +} diff --git a/pkg/mail/mail.go b/pkg/mail/mail.go new file mode 100644 index 0000000..eadb6cd --- /dev/null +++ b/pkg/mail/mail.go @@ -0,0 +1,10 @@ +package mail + +type Mail struct { + To string `json:"to"` + From string `json:"from"` + ReplyTo string `json:"reply_to"` + Subject string `json:"subject"` + ContentType string `json:"content_type"` + Body string `json:"body"` +} diff --git a/pkg/mail/mock/service.go b/pkg/mail/mock/service.go new file mode 100644 index 0000000..9dfe707 --- /dev/null +++ b/pkg/mail/mock/service.go @@ -0,0 +1,24 @@ +package mock + +import ( + "fmt" + + "github.com/cthit/gotify/pkg/mail" +) + +type mockService struct { +} + +func NewService() (mail.Service, error) { + return &mockService{}, nil +} + +func (g *mockService) SendMail(mail mail.Mail) (mail.Mail, error) { + fmt.Printf("Sending mail:\n %#v \n", mail) + + return mail, nil +} + +func (g *mockService) Destroy() error { + return nil +} diff --git a/mail_service.go b/pkg/mail/service.go similarity index 67% rename from mail_service.go rename to pkg/mail/service.go index 4dfcbc2..d4c0b48 100644 --- a/mail_service.go +++ b/pkg/mail/service.go @@ -1,6 +1,6 @@ -package gotify +package mail -type MailService interface { +type Service interface { SendMail(mail Mail) (Mail, error) // Returns the actually sent email Destroy() error } diff --git a/pkg/mail/util.go b/pkg/mail/util.go new file mode 100644 index 0000000..3a13e4d --- /dev/null +++ b/pkg/mail/util.go @@ -0,0 +1,7 @@ +package mail + +import "encoding/base64" + +func EncodeHeader(header string) string { + return "=?utf-8?B?" + base64.RawURLEncoding.EncodeToString([]byte(header)) + "?=" +} diff --git a/pkg/mail/validation.go b/pkg/mail/validation.go new file mode 100644 index 0000000..9d46e0e --- /dev/null +++ b/pkg/mail/validation.go @@ -0,0 +1,52 @@ +package mail + +import ( + "github.com/pkg/errors" + + "github.com/cthit/gotify/internal/validation" +) + +func Validate(mail Mail) error { + err := validation.And( + validation.FieldString( + "to", + mail.To, + validation.IsEmail, + ), + validation.FieldString( + "from", + mail.From, + validation.OrString( + validation.IsEmail, + validation.IsEmpty, + ), + ), + validation.FieldString( + "reply_to", + mail.ReplyTo, + validation.OrString( + validation.IsEmail, + validation.IsEmpty, + ), + ), + validation.FieldString( + "subject", + mail.Subject, + validation.IsNotEmpty, + ), + validation.FieldString( + "content_type", + mail.ContentType, + ), + validation.FieldString( + "body", + mail.Body, + validation.IsNotEmpty, + ), + ) + if err != nil { + return errors.Wrap(err, "validation failed") + } + + return nil +} diff --git a/pkg/mail/validation_test.go b/pkg/mail/validation_test.go new file mode 100644 index 0000000..d78ea50 --- /dev/null +++ b/pkg/mail/validation_test.go @@ -0,0 +1,98 @@ +package mail + +import "testing" + +func TestValidate(t *testing.T) { + tests := []struct { + name string + mail Mail + wantErr bool + }{ + { + name: "empty mail", + mail: Mail{}, + wantErr: true, + }, + { + name: "missing to", + mail: Mail{ + Subject: "not empty", + Body: "not empty", + }, + wantErr: true, + }, + { + name: "invalid to", + mail: Mail{ + To: "qwerty@abgc,com", + Subject: "not empty", + Body: "not empty", + }, + wantErr: true, + }, + { + name: "no optional fields", + mail: Mail{ + To: "qwerty@abgc.com", + Subject: "not empty", + Body: "not empty", + }, + wantErr: false, + }, + { + name: "no subject", + mail: Mail{ + To: "qwerty@abgc.com", + Body: "not empty", + }, + wantErr: true, + }, + { + name: "no body", + mail: Mail{ + To: "qwerty@abgc.com", + Subject: "not empty", + }, + wantErr: true, + }, + { + name: "invalid from", + mail: Mail{ + From: "qwerty@abgc,com", + To: "qwerty@abgc.com", + Subject: "not empty", + Body: "not empty", + }, + wantErr: true, + }, + { + name: "invalid reply-to", + mail: Mail{ + ReplyTo: "qwerty@abgc,com", + To: "qwerty@abgc.com", + Subject: "not empty", + Body: "not empty", + }, + wantErr: true, + }, + { + name: "all fields", + mail: Mail{ + To: "qwerty@abgc.com", + From: "qwerty@abgc.com", + ReplyTo: "qwerty@abgc.com", + Subject: "not empty", + ContentType: "some content type", + Body: "not empty", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := Validate(tt.mail); (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} \ No newline at end of file diff --git a/scripts/proto-watcher.sh b/scripts/proto-watcher.sh new file mode 100644 index 0000000..612652d --- /dev/null +++ b/scripts/proto-watcher.sh @@ -0,0 +1,3 @@ +#!/bin/sh +(./scripts/protoc-gen.sh && echo "Generated!" ) || true +reflex -r '\.proto$' -- sh -c './scripts/protoc-gen.sh && echo "Generated!"' \ No newline at end of file diff --git a/scripts/protoc-gen.sh b/scripts/protoc-gen.sh new file mode 100755 index 0000000..de67264 --- /dev/null +++ b/scripts/protoc-gen.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +function generate_for_for_version { + PROTOFILES=api/proto/$1/* + for f in $PROTOFILES; do + mkdir -p api/swagger/$1 + mkdir -p pkg/api/$1 + protoc --proto_path=api/proto/$1 \ + -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/ \ + --go_out=plugins=grpc:pkg/api/$1 \ + $(basename $f) + protoc --proto_path=api/proto/$1 \ + -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/ \ + --grpc-gateway_out=logtostderr=true:pkg/api/$1 \ + $(basename $f) + protoc --proto_path=api/proto/$1 \ + -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \ + -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/ \ + --swagger_out=allow_merge=true,logtostderr=true:api/swagger/$1 \ + $(basename $f) + done +} + +for d in $(find api/proto/* -type d); do + generate_for_for_version $(basename $d) +done diff --git a/web/auth.go b/web/auth.go deleted file mode 100644 index f6af08a..0000000 --- a/web/auth.go +++ /dev/null @@ -1,15 +0,0 @@ -package web - -import ( - "fmt" - "github.com/gocraft/web" - "net/http" -) - -func (c *Context) Auth(rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { - if req.Header.Get("Authorization") == fmt.Sprintf("pre-shared: %s", c.AuthKey) { - next(rw, req) - } else { - rw.WriteHeader(http.StatusUnauthorized) - } -} diff --git a/web/mail.go b/web/mail.go deleted file mode 100644 index 58adbd5..0000000 --- a/web/mail.go +++ /dev/null @@ -1,50 +0,0 @@ -package web - -import ( - "encoding/json" - "github.com/cthit/gotify" - "github.com/gocraft/web" - "io/ioutil" - "net/http" -) - -func (c *Context) SendMail(rw web.ResponseWriter, req *web.Request) { - var mail gotify.Mail - - // Read request body - body, err := ioutil.ReadAll(req.Body) - req.Body.Close() - if err != nil { - c.printError(err) - rw.WriteHeader(http.StatusBadRequest) - return - } - - // Parse json email - err = json.Unmarshal(body, &mail) - if err != nil { - c.printError(err) - rw.WriteHeader(http.StatusBadRequest) - return - } - - // Send email - mail, err = c.MailService.SendMail(mail) - if err != nil { - c.printError(err) - rw.WriteHeader(http.StatusInternalServerError) - return - } - - // Build json email - data, err := json.Marshal(mail) - if err != nil { - c.printError(err) - rw.WriteHeader(http.StatusInternalServerError) - return - } - - // Return the sent email - rw.WriteHeader(http.StatusOK) - rw.Write(data) -} diff --git a/web/router.go b/web/router.go deleted file mode 100644 index 0061a59..0000000 --- a/web/router.go +++ /dev/null @@ -1,55 +0,0 @@ -package web - -import ( - "github.com/cthit/gotify" - "github.com/gocraft/web" - "net/http" -) - -type Context struct { - MailService gotify.MailService - AuthKey string - Debug bool -} - -func Router(authKey string, mailServiceCreator func() gotify.MailService, debug bool) http.Handler { - - router := web.NewWithPrefix( - Context{}, - "") - - router.Middleware(web.LoggerMiddleware) - if debug { - router.Middleware(web.ShowErrorsMiddleware) - } - - router.Middleware(setDebugMode(debug)) - router.Middleware(setMailServiceProvider(mailServiceCreator)) - router.Middleware(setAuthKey(authKey)) - router.Middleware((*Context).Auth) - - router.Post("/mail", (*Context).SendMail) - return router -} - -func setMailServiceProvider(mailServiceProvider func() gotify.MailService) func(*Context, web.ResponseWriter, *web.Request, web.NextMiddlewareFunc) { - return func(c *Context, rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { - c.MailService = mailServiceProvider() - next(rw, req) - c.MailService.Destroy() - } -} - -func setAuthKey(authKey string) func(*Context, web.ResponseWriter, *web.Request, web.NextMiddlewareFunc) { - return func(c *Context, rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { - c.AuthKey = authKey - next(rw, req) - } -} - -func setDebugMode(debug bool) func(*Context, web.ResponseWriter, *web.Request, web.NextMiddlewareFunc) { - return func(c *Context, rw web.ResponseWriter, req *web.Request, next web.NextMiddlewareFunc) { - c.Debug = debug - next(rw, req) - } -} diff --git a/web/util.go b/web/util.go deleted file mode 100644 index cb859a0..0000000 --- a/web/util.go +++ /dev/null @@ -1,11 +0,0 @@ -package web - -import "fmt" - -func (c *Context) printError(err error) { - if c.Debug { - fmt.Println(err) - } else { - fmt.Println("An error occurred, turn on debug mode for more info") - } -}