mirror of
https://github.com/yggdrasil-network/yggdrasil-go.git
synced 2025-08-24 16:05:07 +03:00
Refactor web UI server setup in main.go and update default host in config
This commit is contained in:
parent
345d5b9cbd
commit
51a1a0a3d7
9 changed files with 146 additions and 55 deletions
|
@ -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 ...")
|
||||||
|
|
|
@ -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
108
src/webui/README.md
Normal 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
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue