diff --git a/app/options.go b/app/options.go index 93ff36cd3..4260988d1 100644 --- a/app/options.go +++ b/app/options.go @@ -6,20 +6,21 @@ import ( type ( Options struct { - ActionLog options.ActionLogOpt - SMTP options.SMTPOpt - Auth options.AuthOpt - HTTPClient options.HTTPClientOpt - DB options.DBOpt - Upgrade options.UpgradeOpt - Provision options.ProvisionOpt - Sentry options.SentryOpt - ObjStore options.ObjectStoreOpt - Corredor options.CorredorOpt - Monitor options.MonitorOpt - WaitFor options.WaitForOpt - HTTPServer options.HTTPServerOpt - Websocket options.WebsocketOpt + Environment options.EnvironmentOpt + ActionLog options.ActionLogOpt + SMTP options.SMTPOpt + Auth options.AuthOpt + HTTPClient options.HTTPClientOpt + DB options.DBOpt + Upgrade options.UpgradeOpt + Provision options.ProvisionOpt + Sentry options.SentryOpt + ObjStore options.ObjectStoreOpt + Corredor options.CorredorOpt + Monitor options.MonitorOpt + WaitFor options.WaitForOpt + HTTPServer options.HTTPServerOpt + Websocket options.WebsocketOpt } ) @@ -29,20 +30,23 @@ func NewOptions(prefix ...string) *Options { p = prefix[0] } + // @todo remover prefixes on opt constructors and + // pass in EnvironmentOpt so we can have environment-dependant defaults return &Options{ - ActionLog: *options.ActionLog(), - Auth: *options.Auth(), - SMTP: *options.SMTP(p), - HTTPClient: *options.HttpClient(p), - DB: *options.DB(p), - Upgrade: *options.Upgrade(p), - Provision: *options.Provision(p), - Sentry: *options.Sentry(p), - ObjStore: *options.ObjectStore(p), - Corredor: *options.Corredor(), - Monitor: *options.Monitor(p), - WaitFor: *options.WaitFor(p), - HTTPServer: *options.HTTP(p), - Websocket: *options.Websocket(p), + Environment: *options.Environment(), + ActionLog: *options.ActionLog(), + Auth: *options.Auth(), + SMTP: *options.SMTP(p), + HTTPClient: *options.HttpClient(p), + DB: *options.DB(p), + Upgrade: *options.Upgrade(p), + Provision: *options.Provision(p), + Sentry: *options.Sentry(p), + ObjStore: *options.ObjectStore(p), + Corredor: *options.Corredor(), + Monitor: *options.Monitor(p), + WaitFor: *options.WaitFor(p), + HTTPServer: *options.HTTP(p), + Websocket: *options.Websocket(p), } } diff --git a/app/servers.go b/app/servers.go index 4ee5b8322..5c9b4541c 100644 --- a/app/servers.go +++ b/app/servers.go @@ -18,7 +18,7 @@ func (app *CortezaApp) Serve(ctx context.Context) (err error) { { // @todo refactor wait-for out of HTTP API server. - app.HttpServer = server.New(app.Log, app.Opt.HTTPServer, app.Opt.WaitFor) + app.HttpServer = server.New(app.Log, app.Opt.Environment, app.Opt.HTTPServer, app.Opt.WaitFor) app.HttpServer.MountRoutes(app.mountHttpRoutes) wg.Add(1) diff --git a/pkg/api/debug.go b/pkg/api/debug.go index 3c1615a61..f4147481f 100644 --- a/pkg/api/debug.go +++ b/pkg/api/debug.go @@ -9,20 +9,16 @@ import ( type ctxKeyDebug struct{} // Packs remote address to context -func DebugToContext(next http.Handler) http.Handler { - if true { - // debug disabled - return next +func DebugToContext(production bool) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + next.ServeHTTP(w, req.WithContext(context.WithValue(req.Context(), ctxKeyDebug{}, !production))) + }) } - - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - next.ServeHTTP(w, req.WithContext(context.WithValue(req.Context(), ctxKeyDebug{}, true))) - }) } // DebugFromContext returns remote IP address from context func DebugFromContext(ctx context.Context) bool { - return true debug, ok := ctx.Value(ctxKeyDebug{}).(bool) return ok && debug } diff --git a/pkg/api/response.go b/pkg/api/response.go index 43841dd54..1b5e1c46f 100644 --- a/pkg/api/response.go +++ b/pkg/api/response.go @@ -96,7 +96,7 @@ func encode(w http.ResponseWriter, r *http.Request, payload interface{}) { _ = err.Apply(errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP")) } - errors.ServeHTTP(w, r, err, DebugFromContext(r.Context())) + errors.ServeHTTP(w, r, err, !DebugFromContext(r.Context())) } w.Header().Set("Content-Type", "application/json") diff --git a/pkg/api/server/middleware.go b/pkg/api/server/middleware.go index b6fe314da..864e6ae3d 100644 --- a/pkg/api/server/middleware.go +++ b/pkg/api/server/middleware.go @@ -2,24 +2,22 @@ package server import ( "github.com/cortezaproject/corteza-server/pkg/api" + "github.com/cortezaproject/corteza-server/pkg/logger" + "github.com/getsentry/sentry-go/http" + "github.com/go-chi/chi/middleware" + "go.uber.org/zap" "net/http" "os" "runtime/debug" - - "github.com/go-chi/chi/middleware" - "go.uber.org/zap" - - "github.com/getsentry/sentry-go/http" - - "github.com/cortezaproject/corteza-server/pkg/logger" ) -func BaseMiddleware(log *zap.Logger) []func(http.Handler) http.Handler { +func BaseMiddleware(isProduction bool, log *zap.Logger) []func(http.Handler) http.Handler { return []func(http.Handler) http.Handler{ handleCORS, middleware.RealIP, api.RemoteAddrToContext, middleware.RequestID, + api.DebugToContext(isProduction), contextLogger(log), } } diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index 27616385a..d07fc0462 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -16,19 +16,22 @@ import ( type ( server struct { - log *zap.Logger - httpOpt options.HTTPServerOpt - waitForOpt options.WaitForOpt - endpoints []func(r chi.Router) + log *zap.Logger + httpOpt options.HTTPServerOpt + waitForOpt options.WaitForOpt + environmentOpt options.EnvironmentOpt + endpoints []func(r chi.Router) } ) -func New(log *zap.Logger, httpOpt options.HTTPServerOpt, waitForOpt options.WaitForOpt) *server { +func New(log *zap.Logger, envOpt options.EnvironmentOpt, httpOpt options.HTTPServerOpt, waitForOpt options.WaitForOpt) *server { return &server{ - endpoints: make([]func(r chi.Router), 0), - log: log.Named("http"), - httpOpt: httpOpt, - waitForOpt: waitForOpt, + endpoints: make([]func(r chi.Router), 0), + log: log.Named("http"), + + environmentOpt: envOpt, + httpOpt: httpOpt, + waitForOpt: waitForOpt, } } @@ -48,7 +51,7 @@ func (s server) Serve(ctx context.Context) { router := chi.NewRouter() // Base middleware, CORS, RealIP, RequestID, context-logger - router.Use(BaseMiddleware(s.log)...) + router.Use(BaseMiddleware(s.environmentOpt.IsProduction(), s.log)...) router.Group(func(r chi.Router) { s.bindMiscRoutes(r) diff --git a/pkg/errors/http.go b/pkg/errors/http.go index e75371628..cf457430b 100644 --- a/pkg/errors/http.go +++ b/pkg/errors/http.go @@ -12,7 +12,7 @@ import ( // ServeHTTP Prepares and encodes given error for HTTP transport // // mask arg hides extra/debug info -func ServeHTTP(w http.ResponseWriter, r *http.Request, err error, debug bool) { +func ServeHTTP(w http.ResponseWriter, r *http.Request, err error, mask bool) { var ( // Very naive approach on parsing accept headers acceptsJson = strings.Contains(r.Header.Get("accept"), "application/json") @@ -31,12 +31,7 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, err error, debug bool) { w.WriteHeader(code) - if !debug { - // trim error details when not debugging - err = fmt.Errorf(err.Error()) - } - - if debug && !acceptsJson { + if !mask && !acceptsJson { // Prettify error for plain text debug output w.Header().Set("Content-Type", "plain/text") writeHttpPlain(w, err) @@ -46,7 +41,7 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, err error, debug bool) { } w.Header().Set("Content-Type", "application/json") - writeHttpJSON(w, err) + writeHttpJSON(w, err, mask) } func writeHttpPlain(w io.Writer, err error) { @@ -97,14 +92,20 @@ func writeHttpPlain(w io.Writer, err error) { } -func writeHttpJSON(w io.Writer, err error) { +func writeHttpJSON(w io.Writer, err error, mask bool) { var ( wrap = struct { Error interface{} `json:"error"` }{} ) - if c, is := err.(*Error); is { + if se, is := err.(interface{ Safe() bool }); !is || !se.Safe() { + // trim error details when not debugging or error is not safe + err = fmt.Errorf(err.Error()) + } + + if c, is := err.(json.Marshaler); is { + // take advantage of JSON marshaller on error wrap.Error = c } else { wrap.Error = map[string]string{"message": err.Error()} diff --git a/pkg/errors/http_test.go b/pkg/errors/http_test.go index e31e22a43..082be489a 100644 --- a/pkg/errors/http_test.go +++ b/pkg/errors/http_test.go @@ -11,12 +11,10 @@ func ExampleSimpleErrorAsText() { // Output: // Error: dummy error // -------------------------------------------------------------------------------- - // Note: you are seeing this because system is running in development mode - // and HTTP request is made without "Accept: .../json" headers } func ExampleSimpleErrorAsJson() { - writeHttpJSON(os.Stdout, fmt.Errorf("dummy error")) + writeHttpJSON(os.Stdout, fmt.Errorf("dummy error"), true) // Output: // {"error":{"message":"dummy error"}} @@ -31,14 +29,12 @@ func ExampleErrorAsText() { // -------------------------------------------------------------------------------- // a: b // -------------------------------------------------------------------------------- - // Note: you are seeing this because system is running in development mode - // and HTTP request is made without "Accept: .../json" headers } func ExampleErrorAsJson() { err := New(0, "dummy error", Meta("a", "b"), Meta(&Error{}, "nope")) err.stack = nil // will not test the stack as file path & line numbers might change - writeHttpJSON(os.Stdout, err) + writeHttpJSON(os.Stdout, err, false) // Output: // {"error":{"message":"dummy error","meta":{"a":"b"}}} diff --git a/pkg/options/environment.go b/pkg/options/environment.go new file mode 100644 index 000000000..39bb4d8f0 --- /dev/null +++ b/pkg/options/environment.go @@ -0,0 +1,32 @@ +package options + +import "strings" + +type ( + EnvironmentOpt struct { + Environment string `env:"ENVIRONMENT"` + } +) + +func Environment() (o *EnvironmentOpt) { + o = &EnvironmentOpt{ + Environment: "production", + } + + fill(o) + + return +} + +func (e EnvironmentOpt) IsDevelopment() bool { + return strings.HasPrefix(e.Environment, "dev") +} + +func (e EnvironmentOpt) IsTest() bool { + return strings.HasPrefix(e.Environment, "test") +} + +func (e EnvironmentOpt) IsProduction() bool { + return !e.IsDevelopment() && + !e.IsTest() +} diff --git a/tests/compose/main_test.go b/tests/compose/main_test.go index fd4fb58f5..5ef86df18 100644 --- a/tests/compose/main_test.go +++ b/tests/compose/main_test.go @@ -68,7 +68,7 @@ func InitTestApp() { if r == nil { r = chi.NewRouter() - r.Use(server.BaseMiddleware(logger.Default())...) + r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) rest.MountRoutes(r) } diff --git a/tests/messaging/main_test.go b/tests/messaging/main_test.go index 209bcee29..50d04e6a2 100644 --- a/tests/messaging/main_test.go +++ b/tests/messaging/main_test.go @@ -65,7 +65,7 @@ func InitTestApp() { if r == nil { r = chi.NewRouter() - r.Use(server.BaseMiddleware(logger.Default())...) + r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) rest.MountRoutes(r) } diff --git a/tests/system/main_test.go b/tests/system/main_test.go index d5ab214a7..03fc280b3 100644 --- a/tests/system/main_test.go +++ b/tests/system/main_test.go @@ -83,7 +83,7 @@ func InitTestApp() { if r == nil { r = chi.NewRouter() - r.Use(server.BaseMiddleware(logger.Default())...) + r.Use(server.BaseMiddleware(false, logger.Default())...) helpers.BindAuthMiddleware(r) rest.MountRoutes(r) }