diff --git a/go.mod b/go.mod index 9d745a1..a38fe72 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/githubnemo/CompileDaemon v1.2.1 github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/jsonp v0.0.0-20170809160916-b971022286e2 + github.com/go-chi/render v1.0.1 github.com/gobuffalo/packr/v2 v2.8.0 github.com/gogo/gateway v1.1.0 github.com/golang/protobuf v1.4.2 @@ -17,7 +18,6 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 github.com/grpc-ecosystem/grpc-gateway v1.14.6 github.com/karrick/godirwalk v1.15.6 // indirect - github.com/moogar0880/problems v0.1.1 github.com/oklog/run v1.1.1-0.20200508094559-c7096881717e github.com/peterbourgon/ff/v3 v3.0.0 github.com/rs/cors v1.7.0 diff --git a/go.sum b/go.sum index 8571707..6866114 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyN github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/jsonp v0.0.0-20170809160916-b971022286e2 h1:2c3FgELeVI+A0/GZ5xOVCshaAhP87//dwmYvOUJUkeo= github.com/go-chi/jsonp v0.0.0-20170809160916-b971022286e2/go.mod h1:GDGxNyZ4xc0cql8PZjNrAWIan7yy89rRAMMcHojj/f4= +github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= +github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= 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= @@ -147,8 +149,6 @@ github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/moogar0880/problems v0.1.1 h1:bktLhq8NDG/czU2ZziYNigBFksx13RaYe5AVdNmHDT4= -github.com/moogar0880/problems v0.1.1/go.mod h1:5Dxrk2sD7BfBAgnOzQ1yaTiuCYdGPUh49L8Vhfky62c= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/run v1.1.1-0.20200508094559-c7096881717e h1:bxQ+jj+8fdl9112bovUjD/14jj/uboMqjyVoFkqrdGg= github.com/oklog/run v1.1.1-0.20200508094559-c7096881717e/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= diff --git a/pkg/sgtm/auth.go b/pkg/sgtm/auth.go index 14077bb..f8c5357 100644 --- a/pkg/sgtm/auth.go +++ b/pkg/sgtm/auth.go @@ -5,22 +5,16 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "io/ioutil" "net/http" - "github.com/moogar0880/problems" "go.uber.org/zap" "golang.org/x/oauth2" - "moul.io/godev" -) - -var ( - invalidStateProblem = problems.NewDetailedProblem(http.StatusBadRequest, "invalid state") - codeExchangeProblem = problems.NewDetailedProblem(http.StatusInternalServerError, "oauth code exchange") - // internalProblem = problems.NewDetailedProblem(http.StatusInternalServerError, "internal problem") ) const ( oauthTokenCookie = "oauth-token" + // sessionError ) func (svc *Service) httpAuthLogin(w http.ResponseWriter, r *http.Request) { @@ -45,8 +39,7 @@ func (svc *Service) httpAuthCallback(w http.ResponseWriter, r *http.Request) { got := r.URL.Query().Get("state") expected := svc.authGenerateState(r) if expected != got { - svc.logger.Warn("invalid oauth2 state", zap.String("expected", expected), zap.String("got", got)) - problems.StatusProblemHandler(invalidStateProblem)(w, r) + svc.errRender(w, r, fmt.Errorf("invalid oauth2 state"), http.StatusBadRequest) return } } @@ -58,8 +51,7 @@ func (svc *Service) httpAuthCallback(w http.ResponseWriter, r *http.Request) { var err error token, err = conf.Exchange(context.Background(), code) if err != nil { - svc.logger.Warn("code exchange failed", zap.Error(err)) - problems.StatusProblemHandler(codeExchangeProblem)(w, r) + svc.errRender(w, r, err, http.StatusUnprocessableEntity) return } cookie := http.Cookie{ @@ -70,31 +62,27 @@ func (svc *Service) httpAuthCallback(w http.ResponseWriter, r *http.Request) { Path: "/", //Domain: r.Host, } - fmt.Println(godev.PrettyJSON(cookie)) + svc.logger.Debug("set user cookie", zap.Any("cookie", cookie)) http.SetCookie(w, &cookie) http.Redirect(w, r, "/", http.StatusTemporaryRedirect) } // FIXME: configure jwt, embed username, email, create account if not exists, set roles in jwt - /* - // get user's info - { - res, err := conf.Client(context.Background(), token).Get("https://discordapp.com/api/v6/users/@me") - if err != nil { - svc.logger.Warn("init discord client", zap.Error(err)) - problems.StatusProblemHandler(internalProblem)(w, r) - return - } - defer res.Body.Close() - body, err := ioutil.ReadAll(res.Body) - if err != nil { - svc.logger.Warn("init discord client", zap.Error(err)) - problems.StatusProblemHandler(internalProblem)(w, r) - return - } - _, _ = w.Write(body) + // get user's info + { + res, err := conf.Client(context.Background(), token).Get("https://discordapp.com/api/v6/users/@me") + if err != nil { + svc.errRender(w, r, err, http.StatusUnprocessableEntity) + return } - */ + defer res.Body.Close() + body, err := ioutil.ReadAll(res.Body) + if err != nil { + svc.errRender(w, r, err, http.StatusUnprocessableEntity) + return + } + _, _ = w.Write(body) + } } func (svc *Service) authConfigFromReq(r *http.Request) *oauth2.Config { diff --git a/pkg/sgtm/driver_server.go b/pkg/sgtm/driver_server.go index 7543cb0..54be7c2 100644 --- a/pkg/sgtm/driver_server.go +++ b/pkg/sgtm/driver_server.go @@ -17,6 +17,7 @@ import ( "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/go-chi/jsonp" + "github.com/go-chi/render" packr "github.com/gobuffalo/packr/v2" "github.com/gogo/gateway" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" @@ -147,6 +148,7 @@ func (svc *Service) httpServer() (*http.Server, error) { r.Use(middleware.Timeout(svc.opts.ServerRequestTimeout)) r.Use(middleware.RealIP) r.Use(middleware.RequestID) + r.Use(render.SetContentType(render.ContentTypeJSON)) runtimeMux := runtime.NewServeMux( runtime.WithMarshalerOption(runtime.MIMEWildcard, &gateway.JSONPb{ @@ -189,12 +191,24 @@ func (svc *Service) httpServer() (*http.Server, error) { Date time.Time OAuthToken string }{ - Title: "SGTM!", + Title: "SGTM", Date: time.Now(), } if cookie, err := r.Cookie(oauthTokenCookie); err == nil { data.OAuthToken = cookie.Value } + if svc.opts.DevMode { + src, err := box.FindString("index.tmpl.html") + if err != nil { + svc.errRender(w, r, err, http.StatusInternalServerError) + return + } + tmpl, err = template.New("index").Parse(src) + if err != nil { + svc.errRender(w, r, err, http.StatusInternalServerError) + return + } + } if err := tmpl.Execute(w, data); err != nil { svc.logger.Warn("failed to reply", zap.Error(err)) } diff --git a/pkg/sgtm/errors.go b/pkg/sgtm/errors.go new file mode 100644 index 0000000..5f815ac --- /dev/null +++ b/pkg/sgtm/errors.go @@ -0,0 +1,43 @@ +package sgtm + +import ( + "net/http" + + "github.com/go-chi/render" + "go.uber.org/zap" +) + +func (svc *Service) errRender(w http.ResponseWriter, r *http.Request, err error, status int) { + if status == 0 { + status = http.StatusUnprocessableEntity + } + renderer := &errResponse{ + Type: "about:blank", + Title: http.StatusText(status), + Status: status, + Detail: err.Error(), + Instance: "", + } + svc.logger.Warn( + "user error", + zap.String("title", renderer.Title), + zap.Error(err), + ) + if err := render.Render(w, r, renderer); err != nil { + svc.logger.Warn("cannot render error", zap.Error(err)) + } +} + +// based on github.com/moogar0880/problems.DefaultProblem +type errResponse struct { + Type string `json:"type"` + Title string `json:"title"` + Status int `json:"status,omitempty"` + Detail string `json:"detail,omitempty"` + Instance string `json:"instance,omitempty"` +} + +func (e *errResponse) Render(w http.ResponseWriter, r *http.Request) error { + render.Status(r, e.Status) + return nil +} diff --git a/rules.mk b/rules.mk index 23ffc2f..19f0767 100644 --- a/rules.mk +++ b/rules.mk @@ -72,14 +72,26 @@ GO_INSTALL_OPTS ?= GO_TEST_OPTS ?= -test.timeout=30s GOMOD_DIR ?= . GOCOVERAGE_FILE ?= ./coverage.txt +GOTESTJSON_FILE ?= ./go-test.json +GOBUILDLOG_FILE ?= ./go-build.log +GOINSTALLLOG_FILE ?= ./go-install.log ifdef GOBINS .PHONY: go.install go.install: +ifeq ($(CI),true) + @rm -f /tmp/goinstall.log + @set -e; for dir in $(GOBINS); do ( set -xe; \ + cd $$dir; \ + $(GO) install -v $(GO_INSTALL_OPTS) .; \ + ); done 2>&1 | tee $(GOINSTALLLOG_FILE) + +else @set -e; for dir in $(GOBINS); do ( set -xe; \ cd $$dir; \ $(GO) install $(GO_INSTALL_OPTS) .; \ ); done +endif INSTALL_STEPS += go.install .PHONY: go.release @@ -92,15 +104,29 @@ endif .PHONY: go.unittest go.unittest: +ifeq ($(CI),true) + @echo "mode: atomic" > /tmp/gocoverage + @rm -f $(GOTESTJSON_FILE) + @set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do (set -e; (set -euf pipefail; \ + cd $$dir; \ + ($(GO) test ./... $(GO_TEST_OPTS) -cover -coverprofile=/tmp/profile.out -covermode=atomic -race -json | tee -a $(GOTESTJSON_FILE) 3>&1 1>&2 2>&3 | tee -a $(GOBUILDLOG_FILE); \ + ); \ + if [ -f /tmp/profile.out ]; then \ + cat /tmp/profile.out | sed "/mode: atomic/d" >> /tmp/gocoverage; \ + rm -f /tmp/profile.out; \ + fi)); done + @mv /tmp/gocoverage $(GOCOVERAGE_FILE) +else @echo "mode: atomic" > /tmp/gocoverage @set -e; for dir in `find $(GOMOD_DIR) -type f -name "go.mod" | grep -v /vendor/ | sed 's@/[^/]*$$@@' | sort | uniq`; do (set -e; (set -xe; \ cd $$dir; \ - $(GO) test $(GO_TEST_OPTS) -cover -coverprofile=/tmp/profile.out -covermode=atomic -race ./...); \ + $(GO) test ./... $(GO_TEST_OPTS) -cover -coverprofile=/tmp/profile.out -covermode=atomic -race); \ if [ -f /tmp/profile.out ]; then \ cat /tmp/profile.out | sed "/mode: atomic/d" >> /tmp/gocoverage; \ rm -f /tmp/profile.out; \ fi); done @mv /tmp/gocoverage $(GOCOVERAGE_FILE) +endif .PHONY: go.checkdoc go.checkdoc: