diff --git a/.gitignore b/.gitignore index 1b1813923..6d398564a 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ cmd/test/ localdev/ .vscode/settings.json +.zed/ diff --git a/go.mod b/go.mod index b762ac7c5..2ff9d23c9 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/hashicorp/go-retryablehttp v0.7.7 github.com/juju/ratelimit v1.0.2 github.com/labstack/echo/v4 v4.12.0 + github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.20.0 github.com/prometheus/client_model v0.6.1 github.com/prometheus/common v0.55.0 diff --git a/internal/web/server.go b/internal/web/server.go index 18673a1e9..4efca1eab 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -12,6 +12,7 @@ import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/zap" @@ -24,6 +25,14 @@ import ( //go:embed templates/*.html var templatesFS embed.FS +const ( + metricsPath = "/metrics/" + indexPath = "/" + connectionsPath = "/connections/" + rulesPath = "/rules/" + apiPrefix = "/api/v1" +) + type Server struct { glue.Reloader glue.HealthChecker @@ -50,15 +59,53 @@ func NewServer( healthChecker glue.HealthChecker, connMgr cmgr.Cmgr, ) (*Server, error) { + if err := validateConfig(cfg); err != nil { + return nil, errors.Wrap(err, "invalid configuration") + } + l := zap.S().Named("web") - templates := template.Must(template.ParseFS(templatesFS, "templates/*.html")) - for _, temp := range templates.Templates() { - l.Debug("template name: ", temp.Name()) - } e := NewEchoServer() + if err := setupMiddleware(e, cfg, l); err != nil { + return nil, errors.Wrap(err, "failed to setup middleware") + } + + if err := setupTemplates(e, l); err != nil { + return nil, errors.Wrap(err, "failed to setup templates") + } + + if err := setupMetrics(cfg); err != nil { + return nil, errors.Wrap(err, "failed to setup metrics") + } + + s := &Server{ + Reloader: relayReloader, + HealthChecker: healthChecker, + + e: e, + l: l, + cfg: cfg, + connMgr: connMgr, + addr: net.JoinHostPort(cfg.WebHost, fmt.Sprintf("%d", cfg.WebPort)), + } + + setupRoutes(s) + + return s, nil +} + +func validateConfig(cfg *config.Config) error { + // Add validation logic here + if cfg.WebPort <= 0 || cfg.WebPort > 65535 { + return errors.New("invalid web port") + } + // Add more validations as needed + return nil +} + +func setupMiddleware(e *echo.Echo, cfg *config.Config, l *zap.SugaredLogger) error { e.Use(NginxLogMiddleware(l)) - e.Renderer = &echoTemplate{templates: templates} + if cfg.WebToken != "" { e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{ KeyLookup: "query:token", @@ -70,7 +117,6 @@ func NewServer( if cfg.WebAuthUser != "" && cfg.WebAuthPass != "" { e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) { - // Be careful to use constant time comparison to prevent timing attacks if subtle.ConstantTimeCompare([]byte(username), []byte(cfg.WebAuthUser)) == 1 && subtle.ConstantTimeCompare([]byte(password), []byte(cfg.WebAuthPass)) == 1 { return true, nil @@ -79,38 +125,51 @@ func NewServer( })) } + return nil +} + +func setupTemplates(e *echo.Echo, l *zap.SugaredLogger) error { + funcMap := template.FuncMap{ + "sub": func(a, b int) int { return a - b }, + "add": func(a, b int) int { return a + b }, + } + tmpl, err := template.New("").Funcs(funcMap).ParseFS(templatesFS, "templates/*.html") + if err != nil { + return errors.Wrap(err, "failed to parse templates") + } + templates := template.Must(tmpl, nil) + for _, temp := range templates.Templates() { + l.Debug("template name: ", temp.Name()) + } + e.Renderer = &echoTemplate{templates: templates} + return nil +} + +func setupMetrics(cfg *config.Config) error { if err := metrics.RegisterEhcoMetrics(cfg); err != nil { - return nil, err + return errors.Wrap(err, "failed to register Ehco metrics") } if err := metrics.RegisterNodeExporterMetrics(cfg); err != nil { - return nil, err + return errors.Wrap(err, "failed to register Node Exporter metrics") } - s := &Server{ - Reloader: relayReloader, - HealthChecker: healthChecker, + return nil +} - e: e, - l: l, - cfg: cfg, - connMgr: connMgr, - addr: net.JoinHostPort(cfg.WebHost, fmt.Sprintf("%d", cfg.WebPort)), - } +func setupRoutes(s *Server) { + e := s.e - // register handler - e.GET("/metrics/", echo.WrapHandler(promhttp.Handler())) + e.GET(metricsPath, echo.WrapHandler(promhttp.Handler())) e.GET("/debug/pprof/*", echo.WrapHandler(http.DefaultServeMux)) - e.GET("/", s.index) - e.GET("/connections/", s.ListConnections) - e.GET("/rules/", s.ListRules) + e.GET(indexPath, s.index) + e.GET(connectionsPath, s.ListConnections) + e.GET(rulesPath, s.ListRules) - // api group - api := e.Group("/api/v1") + api := e.Group(apiPrefix) api.GET("/config/", s.CurrentConfig) api.POST("/config/reload/", s.HandleReload) api.GET("/health_check/", s.HandleHealthCheck) api.GET("/node_metrics/", s.GetNodeMetrics) - return s, nil } func (s *Server) Start() error { diff --git a/internal/web/templates/_head.html b/internal/web/templates/_head.html new file mode 100644 index 000000000..0e30406b1 --- /dev/null +++ b/internal/web/templates/_head.html @@ -0,0 +1,13 @@ + + Ehco Web + + + + + + + + + + + diff --git a/internal/web/templates/metrics.html b/internal/web/templates/_metrics.html similarity index 100% rename from internal/web/templates/metrics.html rename to internal/web/templates/_metrics.html diff --git a/internal/web/templates/_navbar.html b/internal/web/templates/_navbar.html new file mode 100644 index 000000000..fba5d64d3 --- /dev/null +++ b/internal/web/templates/_navbar.html @@ -0,0 +1,58 @@ + +
diff --git a/internal/web/templates/connection.html b/internal/web/templates/connection.html index 91bd4adc0..a932ae525 100644 --- a/internal/web/templates/connection.html +++ b/internal/web/templates/connection.html @@ -1,30 +1,27 @@ - - - - - - - Connections - + {{template "_head.html" .}} + {{ template "_navbar.html" . }}
-

ALL Connections: {{.AllCount}}

+

Connections

+

Total: {{.AllCount}}

+ + {{if gt (len .ConnectionList) 0}}
- +
@@ -37,39 +34,70 @@

ALL Connections: {{.AllCount}}

{{range .ConnectionList}} - - - - - + + + + + {{end}}
Label
{{.RelayLabel}}{{.ConnType}}{{.GetFlow}}{{.Stats}}{{.GetTime}}{{.RelayLabel}}{{.ConnType}}{{.GetFlow}}{{.Stats}}{{.GetTime}}
{{else}} -
-

No connections available.

+
+

No {{.ConnType}} connections available.

{{end}} + + {{if gt .TotalPage 1}} + {{end}}
+ + + + diff --git a/internal/web/templates/index.html b/internal/web/templates/index.html index 4bb3a19e9..58b7e60ea 100644 --- a/internal/web/templates/index.html +++ b/internal/web/templates/index.html @@ -1,127 +1,65 @@ - - Ehco Web - - - - - - - - - - - + {{template "_head.html" .}} + + {{ template "_navbar.html" . }} - -
-
-
-

Ehco Relay

-
-
- -
-
-

Build Info

-
-
-
-
    -
  • Version: {{.Version}}
  • -
  • GitBranch: {{.GitBranch}}
  • -
  • GitRevision: {{.GitRevision}}
  • -
  • BuildTime: {{.BuildTime}}
  • -
  • StartTime: {{.StartTime}}
  • -
-
-
-
- -
-
-
- -
- -
-
-

Quick Links({{ .Cfg.NodeLabel }})

-
- -
-
-
- - {{template "metrics.html" .}} -
+
+
+
+
+
+

Build Info

+
+
+
+
    +
  • Version: {{.Version}}
  • +
  • GitBranch: {{.GitBranch}}
  • +
  • GitRevision: {{.GitRevision}}
  • +
  • BuildTime: {{.BuildTime}}
  • +
  • StartTime: {{.StartTime}}
  • +
+
+
+ +
+
+
+
+ + {{template "_metrics.html" .}} +
- -
- -
-
+ +
+ +
- - + + diff --git a/internal/web/templates/rule_list.html b/internal/web/templates/rule_list.html index 3881f5620..bebf6e18c 100644 --- a/internal/web/templates/rule_list.html +++ b/internal/web/templates/rule_list.html @@ -1,18 +1,11 @@ - + - - - - - - - - Rules - + {{template "_head.html" .}} + {{ template "_navbar.html" . }}
-

Rules

+

Rules

@@ -60,7 +53,7 @@

Rules

response.msg + // Use 'msg' as per Go struct ' (Latency: ' + response.latency + // Ensure this matches the Go struct field name - 'ms)' + 'ms)', ); } else { // If error code is not 0, show error message