Moving server files to ./server
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Debug context
|
||||
type ctxKeyDebug struct{}
|
||||
|
||||
// Packs remote address to context
|
||||
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)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// DebugFromContext returns remote IP address from context
|
||||
func DebugFromContext(ctx context.Context) bool {
|
||||
debug, ok := ctx.Value(ctxKeyDebug{}).(bool)
|
||||
return ok && debug
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Key to use when setting the request ID.
|
||||
type ctxKeyRemoteAddr int
|
||||
|
||||
// RemoteAddrKey is the key that holds th unique request ID in a request context.
|
||||
const remoteAddrKey ctxKeyRemoteAddr = 0
|
||||
|
||||
// Packs remote address to context
|
||||
func RemoteAddrToContext(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
next.ServeHTTP(w, req.WithContext(context.WithValue(req.Context(), remoteAddrKey, req.RemoteAddr)))
|
||||
})
|
||||
}
|
||||
|
||||
// RemoteAddrFromContext returns remote IP address from context
|
||||
func RemoteAddrFromContext(ctx context.Context) string {
|
||||
v := ctx.Value(remoteAddrKey)
|
||||
if str, ok := v.(string); ok {
|
||||
return str
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package api
|
||||
|
||||
// This code is modified version from
|
||||
// https://github.com/titpetric/factory/tree/master/resputil
|
||||
//
|
||||
// Parts of the code are rewritten to allow greater flexibility
|
||||
// but the general logic stays the same for now
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza-server/pkg/errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type (
|
||||
successWrap struct {
|
||||
Success struct {
|
||||
Message string `json:"message"`
|
||||
} `json:"success"`
|
||||
}
|
||||
|
||||
CallFrame struct {
|
||||
Function string `json:"function"`
|
||||
File string `json:"file"`
|
||||
Line int `json:"line"`
|
||||
}
|
||||
|
||||
ErrorPayload struct {
|
||||
Message string `json:"message"`
|
||||
Context map[string]interface{} `json:"context,omitempty"`
|
||||
Callstack []*CallFrame `json:"callstack,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
// Success returns a structured success message for API responses
|
||||
func Success(success ...string) *successWrap {
|
||||
response := &successWrap{}
|
||||
response.Success.Message = "OK"
|
||||
if len(success) > 0 {
|
||||
response.Success.Message = success[0]
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// OK returns the default Success message
|
||||
func OK() *successWrap {
|
||||
return Success()
|
||||
}
|
||||
|
||||
// Writes response, according to type
|
||||
//
|
||||
// Primarily this function encodes given payload (directly or indirectly) as compact JSON
|
||||
//
|
||||
// In some specific scenarios, when:
|
||||
// - debug mode is enabled,
|
||||
// - no explicit accept header with /json mime-type is sent
|
||||
// and,
|
||||
//
|
||||
// if payload is an error:
|
||||
// it outputs formatted error with extended info
|
||||
//
|
||||
// if payload is non-error:
|
||||
// it outputs formatted and indented JSON
|
||||
//
|
||||
func encode(w http.ResponseWriter, r *http.Request, payload interface{}) {
|
||||
var (
|
||||
err error
|
||||
enc = json.NewEncoder(w)
|
||||
)
|
||||
|
||||
switch c := payload.(type) {
|
||||
|
||||
case error:
|
||||
err = c
|
||||
|
||||
case *successWrap:
|
||||
// main key is "success"
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
if err = enc.Encode(c); err != nil {
|
||||
err = fmt.Errorf("failed to encode response: %w", err)
|
||||
}
|
||||
|
||||
default:
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
// main key is "response"
|
||||
aux := struct {
|
||||
Response interface{} `json:"response"`
|
||||
}{c}
|
||||
if err = enc.Encode(aux); err != nil {
|
||||
err = fmt.Errorf("failed to encode response: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err, is := err.(*errors.Error); is {
|
||||
// trim out the base stack we don't care about...
|
||||
_ = err.Apply(errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"))
|
||||
}
|
||||
|
||||
errors.ServeHTTP(w, r, err, !DebugFromContext(r.Context()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Send handles first non-nil (and non-empty) payload and encodes it or it's results (when fn)
|
||||
//
|
||||
// See encode() for details
|
||||
func Send(w http.ResponseWriter, r *http.Request, rr ...interface{}) {
|
||||
for _, rsp := range rr {
|
||||
switch c := rsp.(type) {
|
||||
case nil:
|
||||
// this will match a nil error
|
||||
continue
|
||||
|
||||
case *successWrap:
|
||||
encode(w, r, c)
|
||||
|
||||
case func(http.ResponseWriter, *http.Request):
|
||||
c(w, r)
|
||||
|
||||
case func() ([]byte, error):
|
||||
result, err := c()
|
||||
Send(w, r, err, result)
|
||||
|
||||
case func() (interface{}, error):
|
||||
result, err := c()
|
||||
Send(w, r, err, result)
|
||||
|
||||
case func() error:
|
||||
err := c()
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
encode(w, r, err)
|
||||
|
||||
case error:
|
||||
encode(w, r, c)
|
||||
|
||||
case []byte:
|
||||
if len(c) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, err := w.Write(c); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
|
||||
case string:
|
||||
if c == "" {
|
||||
continue
|
||||
}
|
||||
encode(w, r, c)
|
||||
|
||||
case bool:
|
||||
if !c {
|
||||
continue
|
||||
}
|
||||
encode(w, r, c)
|
||||
|
||||
default:
|
||||
encode(w, r, c)
|
||||
}
|
||||
|
||||
// Exit on the first output...
|
||||
return
|
||||
}
|
||||
|
||||
encode(w, r, false)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"io/ioutil"
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
func TestTests(t *testing.T) {
|
||||
testResponse := func(output interface{}) string {
|
||||
w := httptest.NewRecorder()
|
||||
r := &http.Request{Header: http.Header{}}
|
||||
r.Header.Add("accept", "application/json")
|
||||
Send(w, r, output)
|
||||
body, _ := ioutil.ReadAll(w.Result().Body)
|
||||
return string(body)
|
||||
}
|
||||
|
||||
var cc = []struct {
|
||||
name string
|
||||
inp interface{}
|
||||
out string
|
||||
}{
|
||||
{"nil", nil, `{"response":false}`},
|
||||
{"bool true", true, `{"response":true}`},
|
||||
{"bool false", false, `{"response":false}`},
|
||||
{"string empty", "", `{"response":false}`},
|
||||
{"string", "string", `{"response":"string"}`},
|
||||
{"int zero", 0, `{"response":0}`},
|
||||
{"int non-zero", 1337, `{"response":1337}`},
|
||||
{"int sub-zero", -1, `{"response":-1}`},
|
||||
{"error nil", func() error { return nil }, `{"response":false}`},
|
||||
{"error", func() error { return fmt.Errorf("error response") }, `{"error":{"message":"error response"}}`},
|
||||
{"value + error", func() (interface{}, error) { return "string response", fmt.Errorf("error response") }, `{"error":{"message":"error response"}}`},
|
||||
{"empty value + error", func() (interface{}, error) { return "", fmt.Errorf("error response") }, `{"error":{"message":"error response"}}`},
|
||||
{"value + empty error", func() (interface{}, error) { return "string response", nil }, `{"response":"string response"}`},
|
||||
{"success default", Success(), `{"success":{"message":"OK"}}`},
|
||||
{"ok", OK(), `{"success":{"message":"OK"}}`},
|
||||
{"success custom", Success("string"), `{"success":{"message":"string"}}`},
|
||||
{"error stdlib", fmt.Errorf("string"), `{"error":{"message":"string"}}`},
|
||||
{"error stdlib nil", func() interface{} { return func() error { return nil }() }(), `{"response":false}`},
|
||||
{"func json nil", func() ([]byte, error) { return json.Marshal(nil) }, `null`},
|
||||
{"func json false", func() ([]byte, error) { return json.Marshal(false) }, `false`},
|
||||
{"func json 0", func() ([]byte, error) { return json.Marshal(0) }, `0`},
|
||||
{"func json empty string", func() ([]byte, error) { return json.Marshal("") }, `""`},
|
||||
{"func writer/req", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("foo")) }, `foo`},
|
||||
{"custom struct", struct {
|
||||
Name string `json:"name"`
|
||||
}{"Corteza"}, `{"response":{"name":"Corteza"}}`},
|
||||
}
|
||||
|
||||
for _, c := range cc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := strings.TrimSpace(testResponse(c.inp))
|
||||
if got != strings.TrimSpace(c.out) {
|
||||
t.Errorf("got %#v, expected %#v", got, c.out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/cors"
|
||||
)
|
||||
|
||||
// Sets up default CORS rules to use as a middleware
|
||||
func handleCORS(next http.Handler) http.Handler {
|
||||
return cors.New(cors.Options{
|
||||
AllowedOrigins: []string{
|
||||
"http://*",
|
||||
"https://*",
|
||||
},
|
||||
AllowedMethods: []string{
|
||||
http.MethodHead,
|
||||
http.MethodGet,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
},
|
||||
AllowedHeaders: []string{
|
||||
"Accept",
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"X-CSRF-ID",
|
||||
},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 300, // Maximum value not ignored by any of major browsers
|
||||
}).Handler(next)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/corredor"
|
||||
"github.com/cortezaproject/corteza-server/pkg/eventbus"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func debugRoutes(r chi.Routes) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
var printRoutes func(chi.Routes, string)
|
||||
|
||||
printRoutes = func(r chi.Routes, pfix string) {
|
||||
routes := r.Routes()
|
||||
for _, route := range routes {
|
||||
if route.SubRoutes != nil && len(route.SubRoutes.Routes()) > 0 {
|
||||
printRoutes(route.SubRoutes, pfix+route.Pattern[:len(route.Pattern)-2])
|
||||
} else {
|
||||
if route.Handlers["*"] != nil {
|
||||
fmt.Fprintf(w, "%-8s %-80s\n", "*", pfix+route.Pattern)
|
||||
continue
|
||||
}
|
||||
|
||||
for method := range route.Handlers {
|
||||
fmt.Fprintf(w, "%-8s %-80s\n", method, pfix+route.Pattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printRoutes(r, "")
|
||||
}
|
||||
}
|
||||
|
||||
func debugEventbus() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
spew.Fdump(w, eventbus.Service().Debug())
|
||||
}
|
||||
}
|
||||
|
||||
func debugCorredor() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
spew.Fdump(w, corredor.Service().Debug())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
type (
|
||||
// demux (demultiplexer) routes request to one of the underlying routers
|
||||
// according to current state
|
||||
demux struct {
|
||||
state *atomic.Uint32
|
||||
routers map[uint32]chi.Router
|
||||
}
|
||||
)
|
||||
|
||||
var _ http.Handler = &demux{}
|
||||
|
||||
func Demux(state uint32, r chi.Router) *demux {
|
||||
return &demux{
|
||||
state: atomic.NewUint32(state),
|
||||
routers: map[uint32]chi.Router{state: r},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *demux) State(s uint32) {
|
||||
d.state.Store(s)
|
||||
}
|
||||
|
||||
func (d *demux) Router(s uint32, r chi.Router) {
|
||||
d.routers[s] = r
|
||||
}
|
||||
|
||||
func (d *demux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer panicRecovery(r.Context(), w)
|
||||
|
||||
var (
|
||||
state = d.state.Load()
|
||||
router, exists = d.routers[state]
|
||||
)
|
||||
|
||||
if !exists {
|
||||
_, _ = fmt.Fprintf(w, "unconfigured request demultiplexor state (%d)", state)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
router.ServeHTTP(w, r)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/assets"
|
||||
"github.com/cortezaproject/corteza-server/pkg/api"
|
||||
"github.com/cortezaproject/corteza-server/pkg/auth"
|
||||
"github.com/cortezaproject/corteza-server/pkg/errors"
|
||||
"github.com/cortezaproject/corteza-server/pkg/healthcheck"
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
"github.com/cortezaproject/corteza-server/pkg/options"
|
||||
"github.com/cortezaproject/corteza-server/pkg/version"
|
||||
"github.com/cortezaproject/corteza-server/webconsole"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// routes used when server is in waiting mode
|
||||
func waitingRoutes(log *zap.Logger, httpOpt options.HttpServerOpt) (r chi.Router) {
|
||||
r = chi.NewRouter()
|
||||
r.Use(handleCORS)
|
||||
|
||||
mountServiceHandlers(r, log, httpOpt, waiting)
|
||||
|
||||
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
// For non GET requests, return 503 (service unavailable)
|
||||
errors.ServeHTTPWithCode(w, r,
|
||||
http.StatusServiceUnavailable,
|
||||
fmt.Errorf("corteza server initializing"),
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Refresh the page in 15 seconds
|
||||
w.Header().Set("Refresh", "15; url=/")
|
||||
_, _ = fmt.Fprint(w, "Corteza server initializing\n\n")
|
||||
if httpOpt.EnableHealthcheckRoute {
|
||||
healthcheck.Defaults().Run(r.Context()).WriteTo(w)
|
||||
}
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// routes used when server in shutdown mode
|
||||
func shutdownRoutes() (r chi.Router) {
|
||||
r = chi.NewRouter()
|
||||
|
||||
r.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
// For non GET requests, return 503 (service unavailable)
|
||||
errors.ServeHTTPWithCode(w, r,
|
||||
http.StatusServiceUnavailable,
|
||||
fmt.Errorf("corteza server shutting down"),
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
r.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Refresh the page in 15 seconds
|
||||
w.Header().Set("Refresh", "15; url=/")
|
||||
_, _ = fmt.Fprint(w, "corteza server shutting down")
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// routes used when in active mode
|
||||
func activeRoutes(log *zap.Logger, mountable []func(r chi.Router), opts *options.Options) (r chi.Router) {
|
||||
r = chi.NewRouter()
|
||||
r.Use(handleCORS)
|
||||
|
||||
httpOpt := opts.HTTPServer
|
||||
authOpt := opts.Auth
|
||||
envOpt := opts.Environment
|
||||
|
||||
r.Route("/"+strings.TrimPrefix(httpOpt.BaseUrl, "/"), func(r chi.Router) {
|
||||
// Reports error to Sentry if enabled
|
||||
if httpOpt.EnablePanicReporting {
|
||||
r.Use(sentryMiddleware())
|
||||
}
|
||||
|
||||
if httpOpt.EnableMetrics {
|
||||
// Metrics tracking middleware
|
||||
r.Use(metricsMiddleware(httpOpt.MetricsServiceLabel))
|
||||
}
|
||||
|
||||
// Handle panic (sets 500 server error headers)
|
||||
//r.Use(handlePanic)
|
||||
|
||||
// Base middleware, CORS, RealIP, RequestID, context-logger
|
||||
r.Use(BaseMiddleware(envOpt.IsProduction(), log)...)
|
||||
|
||||
// Logging request if enabled
|
||||
if httpOpt.LogRequest {
|
||||
r.Use(LogRequest)
|
||||
}
|
||||
|
||||
// Logging response if enabled
|
||||
if httpOpt.LogResponse {
|
||||
r.Use(LogResponse)
|
||||
}
|
||||
|
||||
// Verifies JWT in headers, cookies, ...
|
||||
r.Use(auth.HttpTokenVerifier)
|
||||
|
||||
for _, mount := range mountable {
|
||||
mount(r)
|
||||
}
|
||||
|
||||
if httpOpt.EnableMetrics {
|
||||
metricsMount(r, httpOpt.MetricsUsername, httpOpt.MetricsPassword)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
if httpOpt.BaseUrl != "/" {
|
||||
r.Handle("/", http.RedirectHandler(httpOpt.BaseUrl, http.StatusTemporaryRedirect))
|
||||
}
|
||||
|
||||
mountServiceHandlers(r, log, httpOpt, active)
|
||||
|
||||
r.HandleFunc(httpOpt.ApiBaseUrl, handleStaticPages(log, httpOpt, authOpt, "api-landing.html"))
|
||||
r.HandleFunc(httpOpt.ApiBaseUrl+"/", handleStaticPages(log, httpOpt, authOpt, "api-landing.html"))
|
||||
r.NotFound(handleStaticPages(log, httpOpt, authOpt, "api-404.html"))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func mountServiceHandlers(r chi.Router, log *zap.Logger, opt options.HttpServerOpt, state uint32) {
|
||||
if opt.WebConsoleEnabled {
|
||||
path := "/console"
|
||||
log.Info("web console enabled (HTTP_SERVER_WEB_CONSOLE_ENABLED=true): " + path)
|
||||
r.Route(path, func(r chi.Router) {
|
||||
if len(opt.WebConsolePassword) > 0 {
|
||||
credentials := map[string]string{
|
||||
opt.WebConsoleUsername: opt.WebConsolePassword,
|
||||
}
|
||||
r.Use(middleware.BasicAuth("web-console", credentials))
|
||||
} else {
|
||||
// warn only in waiting state to avoid repeated log messages
|
||||
if state == waiting {
|
||||
// warn the user regardless of what environment Corteza is running in.
|
||||
log.Warn("SECURITY RISK: web console is enabled and unprotected, set " +
|
||||
"HTTP_SERVER_WEB_CONSOLE_USERNAME, HTTP_SERVER_WEB_CONSOLE_PASSWORD " +
|
||||
"if not running in development environment!")
|
||||
}
|
||||
}
|
||||
|
||||
webconsole.Mount(r)
|
||||
mountDebugLogViewer(r, log)
|
||||
|
||||
// redirect from /console to /console/ui/
|
||||
r.Mount("/", http.RedirectHandler(path+"/ui", http.StatusTemporaryRedirect))
|
||||
})
|
||||
}
|
||||
|
||||
if opt.EnableDebugRoute {
|
||||
mountDebugHandler(r, log)
|
||||
}
|
||||
|
||||
if opt.EnableVersionRoute {
|
||||
mountVersionHandler(r, log, opt.BaseUrl)
|
||||
}
|
||||
|
||||
if opt.EnableHealthcheckRoute {
|
||||
mountHealthCheckHandler(r, log, opt.BaseUrl)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// @todo move all these routes under /console and
|
||||
// output JSON instead of plain raw text
|
||||
func mountDebugHandler(r chi.Router, log *zap.Logger) {
|
||||
log.Debug("route debugger enabled: /__routes")
|
||||
r.Get("/__routes", debugRoutes(r))
|
||||
|
||||
log.Debug("profiler enabled: /debug/pprof")
|
||||
r.Mount("/debug", middleware.Profiler())
|
||||
|
||||
log.Debug("eventbus handlers debug enabled: /__eventbus")
|
||||
r.Get("/__eventbus", debugEventbus())
|
||||
|
||||
log.Debug("corredor service debug enabled: /__corredor")
|
||||
r.Get("/__corredor", debugCorredor())
|
||||
}
|
||||
|
||||
func mountVersionHandler(r chi.Router, log *zap.Logger, basePath string) {
|
||||
var (
|
||||
dPath = "/version"
|
||||
sPath = path.Join(basePath, dPath)
|
||||
handler = func(w http.ResponseWriter, r *http.Request) {
|
||||
api.Send(w, r, struct {
|
||||
BuildTime string `json:"buildTime"`
|
||||
Version string `json:"version"`
|
||||
}{version.BuildTime, version.Version})
|
||||
}
|
||||
)
|
||||
|
||||
r.Get(dPath, handler)
|
||||
log.Debug("version route enabled: " + dPath)
|
||||
|
||||
if dPath != sPath {
|
||||
r.Get(sPath, handler)
|
||||
log.Debug("version route enabled: " + sPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func mountHealthCheckHandler(r chi.Router, log *zap.Logger, basePath string) {
|
||||
// default & sub path for health-check endpoint
|
||||
var (
|
||||
dPath = "/healthcheck"
|
||||
sPath = path.Join(basePath, dPath)
|
||||
)
|
||||
|
||||
r.Get(dPath, healthcheck.HttpHandler())
|
||||
log.Debug("health check route enabled: " + sPath)
|
||||
|
||||
if dPath != sPath {
|
||||
r.Get(sPath, healthcheck.HttpHandler())
|
||||
log.Debug("health check route enabled: " + sPath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func mountDebugLogViewer(r chi.Router, log *zap.Logger) {
|
||||
var (
|
||||
path = "/server-log-feed"
|
||||
)
|
||||
|
||||
r.Get(path+".json", func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
after int = 0
|
||||
limit int = 100
|
||||
err error
|
||||
q = r.URL.Query()
|
||||
)
|
||||
|
||||
if aux := q.Get("after"); len(aux) > 0 {
|
||||
after, err = strconv.Atoi(aux)
|
||||
if err != nil {
|
||||
errors.ProperlyServeHTTP(w, r, errors.InvalidData("invalid value format for after: %v", err), false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if aux := q.Get("limit"); len(aux) > 0 {
|
||||
limit, err = strconv.Atoi(aux)
|
||||
if err != nil {
|
||||
errors.ProperlyServeHTTP(w, r, errors.InvalidData("invalid value format for limit: %v", err), false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = logger.WriteLogBuffer(w, after, limit)
|
||||
})
|
||||
}
|
||||
|
||||
func handleStaticPages(log *zap.Logger, hOpt options.HttpServerOpt, aOpt options.AuthOpt, file string) http.HandlerFunc {
|
||||
// "good-enough" for now, plan to move to templates when
|
||||
// merging with auth
|
||||
const linkTpl = `<a class="btn btn-light font-weight-bold text-dark m-2" href="%s">%s</a>`
|
||||
var (
|
||||
links = make([]string, 0)
|
||||
buf []byte
|
||||
|
||||
placeholder = []byte("<!-- links -->")
|
||||
)
|
||||
|
||||
links = append(links, fmt.Sprintf(linkTpl, aOpt.BaseURL, "Login"))
|
||||
|
||||
if hOpt.ApiEnabled {
|
||||
links = append(links, fmt.Sprintf(linkTpl, "https://docs.cortezaproject.org/", "Documentation"))
|
||||
}
|
||||
|
||||
if hOpt.WebConsoleEnabled {
|
||||
links = append(links, fmt.Sprintf(linkTpl, "/console", "Console"))
|
||||
}
|
||||
|
||||
page, err := assets.Files(log, hOpt.AssetsPath).Open(file)
|
||||
if err != nil {
|
||||
log.Warn("could not open static page", zap.String("file", file), zap.Error(err))
|
||||
}
|
||||
|
||||
buf, err = io.ReadAll(page)
|
||||
if err != nil {
|
||||
log.Warn("could not prepare static page", zap.String("file", file), zap.Error(err))
|
||||
}
|
||||
|
||||
buf = bytes.ReplaceAll(buf, placeholder, []byte(strings.Join(links, "")))
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if page == nil {
|
||||
// fallback to default 404 handler
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write(buf)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// contextLogger middleware binds logger to request's context.
|
||||
//
|
||||
// This allows us to use logger from context (with requestID)
|
||||
// inside our (generated) handlers and controllers
|
||||
func contextLogger(log *zap.Logger) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
var requestID = middleware.GetReqID(req.Context())
|
||||
|
||||
w.Header().Add("X-Request-Id", requestID)
|
||||
|
||||
req = req.WithContext(logger.ContextWithValue(
|
||||
req.Context(),
|
||||
log.With(zap.String("requestID", requestID)).Named("rest"),
|
||||
))
|
||||
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// LogRequest middleware logs request details
|
||||
//
|
||||
// It uses logger from context, see contextLogger()
|
||||
func LogRequest(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
var remote = req.RemoteAddr
|
||||
if l := strings.LastIndex(remote, ":"); l > -1 {
|
||||
remote = remote[:l]
|
||||
}
|
||||
|
||||
logger.ContextValue(req.Context()).Info(
|
||||
"HTTP request "+req.Method+" "+req.URL.Path,
|
||||
zap.String("method", req.Method),
|
||||
zap.String("path", req.URL.Path),
|
||||
zap.Int64("size", req.ContentLength),
|
||||
zap.String("remote", remote),
|
||||
)
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
// LogResponse middleware logs response details
|
||||
//
|
||||
// It uses logger from context, see contextLogger()
|
||||
func LogResponse(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
wrapped := middleware.NewWrapResponseWriter(w, req.ProtoMajor)
|
||||
t := time.Now()
|
||||
|
||||
defer func() {
|
||||
logger.ContextValue(req.Context()).Info(
|
||||
"HTTP response "+req.Method+" "+req.URL.Path,
|
||||
zap.String("method", req.Method),
|
||||
zap.String("path", req.URL.Path),
|
||||
zap.Int("status", wrapped.Status()),
|
||||
zap.Int("size", wrapped.BytesWritten()),
|
||||
zap.Float64("duration", time.Since(t).Seconds()),
|
||||
)
|
||||
}()
|
||||
|
||||
next.ServeHTTP(wrapped, req)
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/99designs/basicauth-go"
|
||||
"net/http"
|
||||
|
||||
"github.com/766b/chi-prometheus"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// MetricsMiddleware is the request logger that provides metrics to prometheus
|
||||
func metricsMiddleware(name string) func(http.Handler) http.Handler {
|
||||
return chiprometheus.NewMiddleware(name)
|
||||
}
|
||||
|
||||
func metricsMount(r chi.Router, username, password string) {
|
||||
r.Route("/metrics", func(r chi.Router) {
|
||||
r.Use(basicauth.New("Metrics", map[string][]string{
|
||||
username: {password},
|
||||
}))
|
||||
r.Handle("/", promhttp.Handler())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/api"
|
||||
"github.com/cortezaproject/corteza-server/pkg/locale"
|
||||
"github.com/cortezaproject/corteza-server/pkg/logger"
|
||||
"github.com/getsentry/sentry-go/http"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func BaseMiddleware(isProduction bool, log *zap.Logger) []func(http.Handler) http.Handler {
|
||||
return []func(http.Handler) http.Handler{
|
||||
handleCORS,
|
||||
locale.DetectLanguage(locale.Global()),
|
||||
middleware.RealIP,
|
||||
api.RemoteAddrToContext,
|
||||
middleware.RequestID,
|
||||
api.DebugToContext(isProduction),
|
||||
contextLogger(log),
|
||||
}
|
||||
}
|
||||
|
||||
func sentryMiddleware() func(http.Handler) http.Handler {
|
||||
return sentryhttp.New(sentryhttp.Options{
|
||||
Repanic: true,
|
||||
}).Handle
|
||||
}
|
||||
|
||||
func panicRecovery(ctx context.Context, w http.ResponseWriter) {
|
||||
if err := recover(); err != nil {
|
||||
|
||||
if _, has := os.LookupEnv("LOG_DEBUG"); has {
|
||||
println("================================================================================")
|
||||
fmt.Printf("%v\n", err)
|
||||
println("--------------------------------------------------------------------------------")
|
||||
debug.PrintStack()
|
||||
println("================================================================================")
|
||||
} else {
|
||||
log := logger.ContextValue(ctx)
|
||||
if err, ok := err.(error); ok {
|
||||
log = log.With(zap.Error(err))
|
||||
} else {
|
||||
log = log.With(zap.Any("recover-value", err))
|
||||
}
|
||||
log.Debug("crashed on http request", zap.ByteString("stack", debug.Stack()))
|
||||
}
|
||||
|
||||
w.WriteHeader(500)
|
||||
|
||||
if _, has := os.LookupEnv("DEBUG_DUMP_STACK_IN_RESPONSE"); has {
|
||||
// Provide nice call stack on endpoint when
|
||||
// we crash
|
||||
_, _ = w.Write(debug.Stack())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/options"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
server struct {
|
||||
log *zap.Logger
|
||||
opts *options.Options
|
||||
endpoints []func(r chi.Router)
|
||||
|
||||
// last error
|
||||
err error
|
||||
|
||||
demux *demux
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
waiting uint32 = iota
|
||||
active
|
||||
shutdown
|
||||
)
|
||||
|
||||
// New initializes new HTTP server with special powers
|
||||
// Server is started as early as possible and with a special request handler
|
||||
// that demultiplexes request to one of the configured routers according to the server state.
|
||||
//
|
||||
// Waiting state
|
||||
// This is initial state with the ofllowing route handlers:
|
||||
// - /version
|
||||
// - /healthcheck
|
||||
|
||||
func New(log *zap.Logger, opts *options.Options) *server {
|
||||
s := &server{
|
||||
endpoints: make([]func(r chi.Router), 0),
|
||||
log: log.Named("http"),
|
||||
|
||||
opts: opts,
|
||||
}
|
||||
|
||||
s.demux = Demux(waiting, waitingRoutes(s.log.Named("waiting"), s.opts.HTTPServer))
|
||||
s.demux.Router(shutdown, shutdownRoutes())
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *server) LastError() error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
func Test(o *options.Options) error {
|
||||
listener, err := net.Listen("tcp", o.HTTPServer.Addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = listener.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Activate reconfigures server to use active routes
|
||||
func (s *server) Activate(mm ...func(chi.Router)) {
|
||||
s.demux.Router(active, activeRoutes(s.log, mm, s.opts))
|
||||
|
||||
s.log.Debug("entering active state")
|
||||
s.demux.State(active)
|
||||
}
|
||||
|
||||
// Shutdown reconfigures server to use shutdown routes
|
||||
func (s *server) Shutdown() {
|
||||
s.log.Debug("entering shutdown state")
|
||||
s.demux.State(shutdown)
|
||||
}
|
||||
|
||||
func (s server) Serve(ctx context.Context) {
|
||||
var (
|
||||
listener net.Listener
|
||||
)
|
||||
|
||||
s.log.Info(
|
||||
"starting HTTP server",
|
||||
|
||||
zap.String("path-prefix", s.opts.HTTPServer.BaseUrl),
|
||||
zap.String("address", s.opts.HTTPServer.Addr),
|
||||
)
|
||||
|
||||
listener, s.err = net.Listen("tcp", s.opts.HTTPServer.Addr)
|
||||
if s.err != nil {
|
||||
s.log.Error("cannot start server", zap.Error(s.err))
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
srv := http.Server{
|
||||
Handler: s.demux,
|
||||
|
||||
// use root context as server's base context and as a basis for
|
||||
// context for all requests
|
||||
// this enables us to send cancellation down to every request
|
||||
BaseContext: func(listener net.Listener) context.Context { return ctx },
|
||||
}
|
||||
s.err = srv.Serve(listener)
|
||||
}()
|
||||
<-ctx.Done()
|
||||
|
||||
if s.err == nil {
|
||||
s.err = ctx.Err()
|
||||
if s.err == context.Canceled {
|
||||
s.err = nil
|
||||
}
|
||||
}
|
||||
|
||||
s.log.Info("HTTP server stopped", zap.Error(s.err))
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// WaitFor sets up a simple status page, delays execution and probes services
|
||||
func (s server) WaitFor(ctx context.Context) {
|
||||
var (
|
||||
opt = s.opts.WaitFor
|
||||
services = opt.GetServices()
|
||||
)
|
||||
|
||||
if len(services) == 0 && opt.Delay == 0 {
|
||||
// Nothing to do here..
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
log = s.log.Named("wait-for")
|
||||
depChan = make(chan struct{})
|
||||
wg sync.WaitGroup
|
||||
serviceAddr string
|
||||
serviceURL *url.URL
|
||||
err error
|
||||
)
|
||||
|
||||
// Setup a simple HTTP server that will inform the impatient users
|
||||
listener, err := net.Listen("tcp", s.opts.HTTPServer.Addr)
|
||||
if err != nil {
|
||||
s.log.Error("cannot start server", zap.Error(err))
|
||||
os.Exit(1)
|
||||
}
|
||||
defer listener.Close()
|
||||
go func() {
|
||||
router := chi.NewRouter()
|
||||
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusPreconditionFailed)
|
||||
w.Write([]byte("waiting for services..."))
|
||||
})
|
||||
_ = http.Serve(listener, router)
|
||||
}()
|
||||
|
||||
if opt.Delay > 0 {
|
||||
s.log.Info("delaying", zap.Duration("delay", opt.Delay))
|
||||
|
||||
// First delay execution
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debug("canceled")
|
||||
return
|
||||
case <-time.After(opt.Delay):
|
||||
// all good...
|
||||
}
|
||||
}
|
||||
|
||||
if len(services) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("waiting for services", zap.Strings("services", services))
|
||||
// Probe services
|
||||
wg.Add(len(services))
|
||||
go func() {
|
||||
|
||||
for _, service := range services {
|
||||
slog := log.With(zap.String("service", service))
|
||||
|
||||
go func(ctx context.Context, service string) {
|
||||
defer wg.Done()
|
||||
|
||||
if serviceAddr, serviceURL, err = s.resolveService(service); err != nil {
|
||||
log.Error("could not resolve service", zap.Error(err))
|
||||
}
|
||||
|
||||
for {
|
||||
ctx, cancelFn := context.WithTimeout(ctx, opt.ServicesProbeTimeout)
|
||||
defer cancelFn()
|
||||
|
||||
if serviceURL == nil {
|
||||
if err = s.probeService(ctx, serviceAddr); err != nil {
|
||||
slog.Warn("service probe failed", zap.Error(err))
|
||||
time.Sleep(opt.ServicesProbeInterval)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if err = s.probeServiceURL(ctx, serviceURL); err != nil {
|
||||
slog.Warn("service URL probe failed", zap.Error(err))
|
||||
time.Sleep(opt.ServicesProbeInterval)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
slog.Debug("service ready")
|
||||
return
|
||||
}
|
||||
}(ctx, service)
|
||||
}
|
||||
wg.Wait()
|
||||
close(depChan)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debug("canceled")
|
||||
return
|
||||
case <-depChan: // services are ready
|
||||
log.Debug("all services ready")
|
||||
return
|
||||
case <-time.After(opt.ServicesTimeout):
|
||||
log.Debug("services not ready")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (s server) resolveService(service string) (addr string, u *url.URL, err error) {
|
||||
addr = service
|
||||
|
||||
if strings.Contains(addr, "://") {
|
||||
// Is service an URL?
|
||||
u, err = url.Parse(addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
addr = u.Host
|
||||
|
||||
if u.Port() == "" {
|
||||
if u.Scheme == "https" {
|
||||
addr += ":443"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default to port 80
|
||||
if !strings.Contains(addr, ":") {
|
||||
addr += ":80"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s server) probeService(ctx context.Context, addr string) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dialer := net.Dialer{}
|
||||
_, err = dialer.DialContext(ctx, "tcp", addr)
|
||||
return
|
||||
}
|
||||
|
||||
func (s server) probeServiceURL(ctx context.Context, u *url.URL) error {
|
||||
req, err := http.NewRequest("GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to assemble service request")
|
||||
}
|
||||
|
||||
rsp, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "service URL request failed")
|
||||
}
|
||||
|
||||
defer rsp.Body.Close()
|
||||
if rsp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.Errorf("service responded with unexpected status '%s'", rsp.Status)
|
||||
}
|
||||
Reference in New Issue
Block a user