Refactor web UI server setup in main.go and update default host in config

This commit is contained in:
Andy Oknen 2025-07-29 21:03:03 +00:00
parent 345d5b9cbd
commit 51a1a0a3d7
9 changed files with 146 additions and 55 deletions

View file

@ -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. // Set up the multicast module.
{ {
options := []multicast.SetupOption{} 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 //Windows service shutdown
minwinsvc.SetOnExit(func() { minwinsvc.SetOnExit(func() {
logger.Infof("Shutting down service ...") logger.Infof("Shutting down service ...")

View file

@ -96,7 +96,7 @@ func GenerateConfig() *NodeConfig {
cfg.WebUI = WebUIConfig{ cfg.WebUI = WebUIConfig{
Enable: false, Enable: false,
Port: 9000, Port: 9000,
Host: "", Host: "127.0.0.1",
} }
if err := cfg.postprocessConfig(); err != nil { if err := cfg.postprocessConfig(); err != nil {
panic(err) panic(err)

108
src/webui/README.md Normal file
View file

@ -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

View file

@ -164,11 +164,11 @@ func TestWebUIConfig_PortRanges(t *testing.T) {
if test.shouldWork { if test.shouldWork {
// Try to start briefly to see if port is valid // Try to start briefly to see if port is valid
go func() { go func() {
server.Start() _ = server.Start()
}() }()
// Quick cleanup // Quick cleanup
server.Stop() _ = server.Stop()
} }
}) })
} }
@ -236,9 +236,9 @@ func TestWebUIConfig_Integration(t *testing.T) {
// Test that server can start with this config // Test that server can start with this config
go func() { go func() {
server.Start() _ = server.Start()
}() }()
defer server.Stop() defer func() { _ = server.Stop() }()
// Verify server properties match config // Verify server properties match config
if server.listen != listenAddr { if server.listen != listenAddr {

View file

@ -47,7 +47,7 @@ func TestWebUIServer_HealthEndpointDetails(t *testing.T) {
}) })
mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
rw.Write([]byte("OK")) _, _ = rw.Write([]byte("OK"))
}) })
server := httptest.NewServer(mux) server := httptest.NewServer(mux)
@ -109,7 +109,7 @@ func TestWebUIServer_NonExistentEndpoint(t *testing.T) {
}) })
mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
rw.Write([]byte("OK")) _, _ = rw.Write([]byte("OK"))
}) })
server := httptest.NewServer(mux) server := httptest.NewServer(mux)
@ -158,7 +158,7 @@ func TestWebUIServer_ContentTypes(t *testing.T) {
}) })
mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
rw.Write([]byte("OK")) _, _ = rw.Write([]byte("OK"))
}) })
server := httptest.NewServer(mux) server := httptest.NewServer(mux)
@ -188,7 +188,7 @@ func TestWebUIServer_HeaderSecurity(t *testing.T) {
}) })
mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
rw.Write([]byte("OK")) _, _ = rw.Write([]byte("OK"))
}) })
server := httptest.NewServer(mux) server := httptest.NewServer(mux)
@ -227,7 +227,7 @@ func TestWebUIServer_ConcurrentRequests(t *testing.T) {
}) })
mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
rw.Write([]byte("OK")) _, _ = rw.Write([]byte("OK"))
}) })
server := httptest.NewServer(mux) server := httptest.NewServer(mux)

View file

@ -30,7 +30,7 @@ func TestWebUIServer_InvalidListenAddress(t *testing.T) {
// Start should fail for invalid addresses // Start should fail for invalid addresses
err := server.Start() err := server.Start()
if err == nil { 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) 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 // This should fail because port is already in use
err = server.Start() err = server.Start()
if err == nil { if err == nil {
server.Stop() _ = server.Stop()
t.Error("Expected Start() to fail when port is already in use") t.Error("Expected Start() to fail when port is already in use")
} }
} }
@ -126,7 +126,7 @@ func TestWebUIServer_StopTwice(t *testing.T) {
// Start server // Start server
go func() { go func() {
server.Start() _ = server.Start()
}() }()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@ -218,7 +218,7 @@ func TestWebUIServer_ContextCancellation(t *testing.T) {
// Start server // Start server
go func() { go func() {
server.Start() _ = server.Start()
}() }()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@ -255,7 +255,7 @@ func TestWebUIServer_EmptyListenAddress(t *testing.T) {
// This might fail when trying to start // This might fail when trying to start
err := server.Start() err := server.Start()
if err == nil { if err == nil {
server.Stop() _ = server.Stop()
t.Log("Note: Server started with empty listen address") t.Log("Note: Server started with empty listen address")
} else { } else {
t.Logf("Expected behavior: Start() failed with empty address: %v", err) 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) { mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
rw.Write([]byte("OK")) _, _ = rw.Write([]byte("OK"))
}) })
server := httptest.NewServer(mux) server := httptest.NewServer(mux)

View file

@ -35,7 +35,7 @@ func (w *WebUIServer) Start() error {
// Health check endpoint // Health check endpoint
mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
rw.Write([]byte("OK")) _, _ = rw.Write([]byte("OK"))
}) })
w.server = &http.Server{ w.server = &http.Server{

View file

@ -58,7 +58,7 @@ func serveFile(rw http.ResponseWriter, r *http.Request, log core.Logger) {
// Set headers and serve the file // Set headers and serve the file
rw.Header().Set("Content-Type", contentType) rw.Header().Set("Content-Type", contentType)
rw.Write(data) _, _ = rw.Write(data)
log.Debugf("Served file: %s (type: %s)", filePath, contentType) log.Debugf("Served file: %s (type: %s)", filePath, contentType)
} }

View file

@ -1,7 +1,6 @@
package webui package webui
import ( import (
"fmt"
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -23,22 +22,6 @@ func getTestAddress() string {
return "127.0.0.1:0" // Let OS assign available port 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) { func TestWebUIServer_Creation(t *testing.T) {
logger := createTestLogger() logger := createTestLogger()
listen := getTestAddress() listen := getTestAddress()
@ -123,7 +106,7 @@ func TestWebUIServer_HealthEndpoint(t *testing.T) {
}) })
mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusOK) rw.WriteHeader(http.StatusOK)
rw.Write([]byte("OK")) _, _ = rw.Write([]byte("OK"))
}) })
server := httptest.NewServer(mux) server := httptest.NewServer(mux)
@ -156,9 +139,9 @@ func TestWebUIServer_Timeouts(t *testing.T) {
// Start server // Start server
go func() { go func() {
server.Start() _ = server.Start()
}() }()
defer server.Stop() defer func() { _ = server.Stop() }()
// Wait for server to start // Wait for server to start
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)