From 51a1a0a3d71428369ec139e77d819e36d548768d Mon Sep 17 00:00:00 2001 From: Andy Oknen Date: Tue, 29 Jul 2025 21:03:03 +0000 Subject: [PATCH] Refactor web UI server setup in main.go and update default host in config --- cmd/yggdrasil/main.go | 34 +++++----- src/config/config.go | 2 +- src/webui/README.md | 108 +++++++++++++++++++++++++++++++ src/webui/config_test.go | 8 +-- src/webui/endpoints_test.go | 10 +-- src/webui/error_handling_test.go | 12 ++-- src/webui/server.go | 2 +- src/webui/server_prod.go | 2 +- src/webui/server_test.go | 23 +------ 9 files changed, 146 insertions(+), 55 deletions(-) create mode 100644 src/webui/README.md diff --git a/cmd/yggdrasil/main.go b/cmd/yggdrasil/main.go index 4ad0753e..5f183ca3 100644 --- a/cmd/yggdrasil/main.go +++ b/cmd/yggdrasil/main.go @@ -264,23 +264,6 @@ func main() { } } - // Set up the web UI server if enabled in config. - if cfg.WebUI.Enable { - var listenAddr string - if cfg.WebUI.Host == "" { - listenAddr = fmt.Sprintf(":%d", cfg.WebUI.Port) - } else { - listenAddr = fmt.Sprintf("%s:%d", cfg.WebUI.Host, cfg.WebUI.Port) - } - - n.webui = webui.Server(listenAddr, logger) - go func() { - if err := n.webui.Start(); err != nil { - logger.Errorf("WebUI server error: %v", err) - } - }() - } - // Set up the multicast module. { options := []multicast.SetupOption{} @@ -316,6 +299,23 @@ func main() { } } + // Set up the web UI server if enabled in config. + if cfg.WebUI.Enable { + var listenAddr string + if cfg.WebUI.Host == "" { + listenAddr = fmt.Sprintf(":%d", cfg.WebUI.Port) + } else { + listenAddr = fmt.Sprintf("%s:%d", cfg.WebUI.Host, cfg.WebUI.Port) + } + + n.webui = webui.Server(listenAddr, logger) + go func() { + if err := n.webui.Start(); err != nil { + logger.Errorf("WebUI server error: %v", err) + } + }() + } + //Windows service shutdown minwinsvc.SetOnExit(func() { logger.Infof("Shutting down service ...") diff --git a/src/config/config.go b/src/config/config.go index ad6e9adf..ac2b3ec3 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -96,7 +96,7 @@ func GenerateConfig() *NodeConfig { cfg.WebUI = WebUIConfig{ Enable: false, Port: 9000, - Host: "", + Host: "127.0.0.1", } if err := cfg.postprocessConfig(); err != nil { panic(err) diff --git a/src/webui/README.md b/src/webui/README.md new file mode 100644 index 00000000..e430837e --- /dev/null +++ b/src/webui/README.md @@ -0,0 +1,108 @@ +# WebUI Module + +This module provides a web interface for managing Yggdrasil node through a browser. + +## Features + +- ✅ HTTP web server with static files +- ✅ Health check endpoint (`/health`) +- ✅ Development and production build modes +- ✅ Automatic binding to Yggdrasil IPv6 address +- ✅ IPv4 and IPv6 support +- ✅ Path traversal attack protection + +## Configuration + +In the Yggdrasil configuration file: + +```json +{ + "WebUI": { + "Enable": true, + "Port": 9000, + "Host": "", + "BindYgg": false + } +} +``` + +### Configuration parameters: + +- **`Enable`** - enable/disable WebUI +- **`Port`** - port for web interface (default 9000) +- **`Host`** - IP address to bind to (empty means all interfaces) +- **`BindYgg`** - automatically bind to Yggdrasil IPv6 address + +## Usage + +### Standard mode + +```go +server := webui.Server("127.0.0.1:9000", logger) +``` + +### With core access + +```go +server := webui.ServerWithCore("127.0.0.1:9000", logger, coreInstance) +``` + +### Automatic Yggdrasil address binding + +```go +server := webui.ServerForYggdrasil(9000, logger, coreInstance) +// Automatically binds to [yggdrasil_ipv6]:9000 +``` + +### Starting the server + +```go +go func() { + if err := server.Start(); err != nil { + logger.Errorf("WebUI server error: %v", err) + } +}() + +// To stop +server.Stop() +``` + +## Endpoints + +- **`/`** - main page (index.html) +- **`/health`** - health check (returns "OK") +- **`/static/*`** - static files (CSS, JS, images) + +## Build modes + +### Development mode (`-tags debug`) +- Files loaded from disk from `src/webui/static/` +- File changes available without rebuild + +### Production mode (default) +- Files embedded in binary +- Faster loading, smaller deployment size + +## Security + +- Path traversal attack protection +- Configured HTTP timeouts +- Header size limits +- File MIME type validation + +## Testing + +The module includes a comprehensive test suite: + +```bash +cd src/webui +go test -v +``` + +Tests cover: +- Server creation and management +- HTTP endpoints +- Static files (dev and prod modes) +- Error handling +- Configuration +- Yggdrasil IPv6 binding \ No newline at end of file diff --git a/src/webui/config_test.go b/src/webui/config_test.go index 7a4aa36a..647b5d9c 100644 --- a/src/webui/config_test.go +++ b/src/webui/config_test.go @@ -164,11 +164,11 @@ func TestWebUIConfig_PortRanges(t *testing.T) { if test.shouldWork { // Try to start briefly to see if port is valid go func() { - server.Start() + _ = server.Start() }() // Quick cleanup - server.Stop() + _ = server.Stop() } }) } @@ -236,9 +236,9 @@ func TestWebUIConfig_Integration(t *testing.T) { // Test that server can start with this config go func() { - server.Start() + _ = server.Start() }() - defer server.Stop() + defer func() { _ = server.Stop() }() // Verify server properties match config if server.listen != listenAddr { diff --git a/src/webui/endpoints_test.go b/src/webui/endpoints_test.go index 652e4e14..6916ee2d 100644 --- a/src/webui/endpoints_test.go +++ b/src/webui/endpoints_test.go @@ -47,7 +47,7 @@ func TestWebUIServer_HealthEndpointDetails(t *testing.T) { }) mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusOK) - rw.Write([]byte("OK")) + _, _ = rw.Write([]byte("OK")) }) server := httptest.NewServer(mux) @@ -109,7 +109,7 @@ func TestWebUIServer_NonExistentEndpoint(t *testing.T) { }) mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusOK) - rw.Write([]byte("OK")) + _, _ = rw.Write([]byte("OK")) }) server := httptest.NewServer(mux) @@ -158,7 +158,7 @@ func TestWebUIServer_ContentTypes(t *testing.T) { }) mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusOK) - rw.Write([]byte("OK")) + _, _ = rw.Write([]byte("OK")) }) server := httptest.NewServer(mux) @@ -188,7 +188,7 @@ func TestWebUIServer_HeaderSecurity(t *testing.T) { }) mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusOK) - rw.Write([]byte("OK")) + _, _ = rw.Write([]byte("OK")) }) server := httptest.NewServer(mux) @@ -227,7 +227,7 @@ func TestWebUIServer_ConcurrentRequests(t *testing.T) { }) mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusOK) - rw.Write([]byte("OK")) + _, _ = rw.Write([]byte("OK")) }) server := httptest.NewServer(mux) diff --git a/src/webui/error_handling_test.go b/src/webui/error_handling_test.go index bc57eed0..cb0adff2 100644 --- a/src/webui/error_handling_test.go +++ b/src/webui/error_handling_test.go @@ -30,7 +30,7 @@ func TestWebUIServer_InvalidListenAddress(t *testing.T) { // Start should fail for invalid addresses err := server.Start() if err == nil { - server.Stop() // Clean up if it somehow started + _ = server.Stop() // Clean up if it somehow started t.Errorf("Expected Start() to fail for invalid address %s", addr) } }) @@ -56,7 +56,7 @@ func TestWebUIServer_PortAlreadyInUse(t *testing.T) { // This should fail because port is already in use err = server.Start() if err == nil { - server.Stop() + _ = server.Stop() t.Error("Expected Start() to fail when port is already in use") } } @@ -126,7 +126,7 @@ func TestWebUIServer_StopTwice(t *testing.T) { // Start server go func() { - server.Start() + _ = server.Start() }() time.Sleep(100 * time.Millisecond) @@ -218,7 +218,7 @@ func TestWebUIServer_ContextCancellation(t *testing.T) { // Start server go func() { - server.Start() + _ = server.Start() }() time.Sleep(100 * time.Millisecond) @@ -255,7 +255,7 @@ func TestWebUIServer_EmptyListenAddress(t *testing.T) { // This might fail when trying to start err := server.Start() if err == nil { - server.Stop() + _ = server.Stop() t.Log("Note: Server started with empty listen address") } else { t.Logf("Expected behavior: Start() failed with empty address: %v", err) @@ -308,7 +308,7 @@ func TestWebUIServer_LargeNumberOfRequests(t *testing.T) { }) mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusOK) - rw.Write([]byte("OK")) + _, _ = rw.Write([]byte("OK")) }) server := httptest.NewServer(mux) diff --git a/src/webui/server.go b/src/webui/server.go index ae82fd50..130b8102 100644 --- a/src/webui/server.go +++ b/src/webui/server.go @@ -35,7 +35,7 @@ func (w *WebUIServer) Start() error { // Health check endpoint mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusOK) - rw.Write([]byte("OK")) + _, _ = rw.Write([]byte("OK")) }) w.server = &http.Server{ diff --git a/src/webui/server_prod.go b/src/webui/server_prod.go index f87dc153..116805dc 100644 --- a/src/webui/server_prod.go +++ b/src/webui/server_prod.go @@ -58,7 +58,7 @@ func serveFile(rw http.ResponseWriter, r *http.Request, log core.Logger) { // Set headers and serve the file rw.Header().Set("Content-Type", contentType) - rw.Write(data) + _, _ = rw.Write(data) log.Debugf("Served file: %s (type: %s)", filePath, contentType) } diff --git a/src/webui/server_test.go b/src/webui/server_test.go index 14c85613..42cb9835 100644 --- a/src/webui/server_test.go +++ b/src/webui/server_test.go @@ -1,7 +1,6 @@ package webui import ( - "fmt" "io" "net/http" "net/http/httptest" @@ -23,22 +22,6 @@ func getTestAddress() string { return "127.0.0.1:0" // Let OS assign available port } -// Helper function to wait for server to be ready -func waitForServer(url string, timeout time.Duration) error { - client := &http.Client{Timeout: 1 * time.Second} - deadline := time.Now().Add(timeout) - - for time.Now().Before(deadline) { - resp, err := client.Get(url) - if err == nil { - resp.Body.Close() - return nil - } - time.Sleep(50 * time.Millisecond) - } - return fmt.Errorf("server not ready within timeout") -} - func TestWebUIServer_Creation(t *testing.T) { logger := createTestLogger() listen := getTestAddress() @@ -123,7 +106,7 @@ func TestWebUIServer_HealthEndpoint(t *testing.T) { }) mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusOK) - rw.Write([]byte("OK")) + _, _ = rw.Write([]byte("OK")) }) server := httptest.NewServer(mux) @@ -156,9 +139,9 @@ func TestWebUIServer_Timeouts(t *testing.T) { // Start server go func() { - server.Start() + _ = server.Start() }() - defer server.Stop() + defer func() { _ = server.Stop() }() // Wait for server to start time.Sleep(200 * time.Millisecond)