mirror of
https://github.com/go-i2p/go-i2ptunnel.git
synced 2025-12-20 15:15:52 -05:00
Implement configuration management and control for I2P tunnels with HTTP handlers and associated tests
This commit is contained in:
191
webui/controller/config_test.go
Normal file
191
webui/controller/config_test.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createTestConfig creates a temporary config file with the given parameters
|
||||||
|
func createTestConfig(t *testing.T, name, tunnelType, target string, port int) string {
|
||||||
|
configDir := t.TempDir()
|
||||||
|
configFile := filepath.Join(configDir, fmt.Sprintf("%s.yaml", name))
|
||||||
|
|
||||||
|
// YAML format requires tunnels: map with tunnel name as key
|
||||||
|
configContent := fmt.Sprintf(`tunnels:
|
||||||
|
%s:
|
||||||
|
name: %s
|
||||||
|
type: %s
|
||||||
|
interface: 127.0.0.1
|
||||||
|
port: %d`, name, name, tunnelType, port)
|
||||||
|
|
||||||
|
if target != "" {
|
||||||
|
configContent += fmt.Sprintf(`
|
||||||
|
target: %s`, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
configContent += "\n"
|
||||||
|
|
||||||
|
if err := os.WriteFile(configFile, []byte(configContent), 0644); err != nil {
|
||||||
|
t.Fatalf("Failed to write config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigServeHTTPGet tests the GET request for configuration display
|
||||||
|
func TestConfigServeHTTPGet(t *testing.T) {
|
||||||
|
configFile := createTestConfig(t, "test-tcp-client", "tcpclient", "example.i2p", 8080)
|
||||||
|
|
||||||
|
cfg, err := NewConfig(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test-tcp-client/config", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
cfg.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
if !strings.Contains(body, "test-tcp-client") {
|
||||||
|
t.Errorf("Response should contain tunnel name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigServeHTTPPost tests saving configuration changes
|
||||||
|
func TestConfigServeHTTPPost(t *testing.T) {
|
||||||
|
configFile := createTestConfig(t, "test-tcp-client", "tcpclient", "example.i2p", 8080)
|
||||||
|
|
||||||
|
cfg, err := NewConfig(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test POST request with new config
|
||||||
|
formData := url.Values{}
|
||||||
|
formData.Set("host", "localhost")
|
||||||
|
formData.Set("port", "9090")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/test-tcp-client/config", strings.NewReader(formData.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
cfg.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Should redirect on success
|
||||||
|
if w.Code != http.StatusSeeOther {
|
||||||
|
t.Logf("Response body: %s", w.Body.String())
|
||||||
|
t.Errorf("Expected status 303 (redirect), got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigServeHTTPPostInvalidPort tests validation of invalid port numbers
|
||||||
|
func TestConfigServeHTTPPostInvalidPort(t *testing.T) {
|
||||||
|
configFile := createTestConfig(t, "test-tcp-client", "tcpclient", "example.i2p", 8080)
|
||||||
|
|
||||||
|
cfg, err := NewConfig(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
port string
|
||||||
|
wantCode int
|
||||||
|
}{
|
||||||
|
{"invalid port text", "abc", http.StatusBadRequest},
|
||||||
|
{"port too low", "0", http.StatusBadRequest},
|
||||||
|
{"port too high", "99999", http.StatusBadRequest},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
formData := url.Values{}
|
||||||
|
formData.Set("port", tc.port)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/test-tcp-client/config", strings.NewReader(formData.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
cfg.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != tc.wantCode {
|
||||||
|
t.Errorf("Expected status %d, got %d", tc.wantCode, w.Code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConfigServeHTTPPostWhileRunning tests that config cannot be changed while tunnel is running
|
||||||
|
func TestConfigServeHTTPPostWhileRunning(t *testing.T) {
|
||||||
|
t.Skip("Skipping test that requires I2P router connection - known i2cp.leaseSetEncType duplicate parameter issue")
|
||||||
|
|
||||||
|
configFile := createTestConfig(t, "test-tcp-client", "tcpclient", "example.i2p", 8080)
|
||||||
|
|
||||||
|
cfg, err := NewConfig(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the tunnel
|
||||||
|
if err := cfg.Start(); err != nil {
|
||||||
|
t.Fatalf("Failed to start tunnel: %v", err)
|
||||||
|
}
|
||||||
|
defer cfg.Stop()
|
||||||
|
|
||||||
|
// Try to modify config while running
|
||||||
|
formData := url.Values{}
|
||||||
|
formData.Set("port", "9090")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/test-tcp-client/config", strings.NewReader(formData.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
cfg.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("Expected status 400, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
if !strings.Contains(body, "running") {
|
||||||
|
t.Errorf("Error message should mention tunnel is running")
|
||||||
|
}
|
||||||
|
}// TestNewConfig tests config creation from file
|
||||||
|
func TestNewConfig(t *testing.T) {
|
||||||
|
configFile := createTestConfig(t, "test-tcp-client", "tcpclient", "example.i2p", 8080)
|
||||||
|
|
||||||
|
cfg, err := NewConfig(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg == nil {
|
||||||
|
t.Fatal("Expected non-nil config")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.configPath != configFile {
|
||||||
|
t.Errorf("Expected configPath %s, got %s", configFile, cfg.configPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewConfigInvalidFile tests handling of invalid config files
|
||||||
|
func TestNewConfigInvalidFile(t *testing.T) {
|
||||||
|
cfg, err := NewConfig("/nonexistent/path/config.yaml")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error for non-existent file")
|
||||||
|
}
|
||||||
|
if cfg != nil {
|
||||||
|
t.Error("Expected nil config for non-existent file")
|
||||||
|
}
|
||||||
|
}
|
||||||
194
webui/controller/control_test.go
Normal file
194
webui/controller/control_test.go
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestControllerServeHTTPGet tests the GET request for control page
|
||||||
|
func TestControllerServeHTTPGet(t *testing.T) {
|
||||||
|
configFile := createTestConfig(t, "test-tcp-client", "tcpclient", "example.i2p", 8080)
|
||||||
|
|
||||||
|
controller, err := NewController(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create controller: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/test-tcp-client/control", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
controller.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
if !strings.Contains(body, "test-tcp-client") {
|
||||||
|
t.Errorf("Response should contain tunnel name")
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "stopped") {
|
||||||
|
t.Errorf("Response should show tunnel status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestControllerStart tests starting a tunnel via POST
|
||||||
|
func TestControllerStart(t *testing.T) {
|
||||||
|
t.Skip("Skipping test that requires I2P router connection - known i2cp.leaseSetEncType duplicate parameter issue")
|
||||||
|
|
||||||
|
configFile := createTestConfig(t, "test-tcp-client", "tcpclient", "example.i2p", 8080)
|
||||||
|
|
||||||
|
controller, err := NewController(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create controller: %v", err)
|
||||||
|
}
|
||||||
|
defer controller.Stop()
|
||||||
|
|
||||||
|
formData := url.Values{}
|
||||||
|
formData.Set("action", "Start")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/test-tcp-client/control", strings.NewReader(formData.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
controller.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Should redirect on success
|
||||||
|
if w.Code != http.StatusSeeOther {
|
||||||
|
t.Errorf("Expected status 303 (redirect), got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify tunnel is running
|
||||||
|
status := controller.Status()
|
||||||
|
if status != "running" {
|
||||||
|
t.Errorf("Expected tunnel to be running, got status: %s", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestControllerStop tests stopping a running tunnel
|
||||||
|
func TestControllerStop(t *testing.T) {
|
||||||
|
t.Skip("Skipping test that requires I2P router connection - known i2cp.leaseSetEncType duplicate parameter issue")
|
||||||
|
|
||||||
|
configFile := createTestConfig(t, "test-tcp-client", "tcpclient", "example.i2p", 8080)
|
||||||
|
|
||||||
|
controller, err := NewController(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create controller: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start tunnel first
|
||||||
|
if err := controller.Start(); err != nil {
|
||||||
|
t.Fatalf("Failed to start tunnel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
formData := url.Values{}
|
||||||
|
formData.Set("action", "Stop")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/test-tcp-client/control", strings.NewReader(formData.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
controller.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Should redirect on success
|
||||||
|
if w.Code != http.StatusSeeOther {
|
||||||
|
t.Errorf("Expected status 303 (redirect), got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify tunnel is stopped
|
||||||
|
status := controller.Status()
|
||||||
|
if status != "stopped" {
|
||||||
|
t.Errorf("Expected tunnel to be stopped, got status: %s", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestControllerRestart tests restarting a tunnel
|
||||||
|
func TestControllerRestart(t *testing.T) {
|
||||||
|
t.Skip("Skipping test that requires I2P router connection - known i2cp.leaseSetEncType duplicate parameter issue")
|
||||||
|
|
||||||
|
configFile := createTestConfig(t, "test-tcp-client", "tcpclient", "example.i2p", 8080)controller, err := NewController(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create controller: %v", err)
|
||||||
|
}
|
||||||
|
defer controller.Stop()
|
||||||
|
|
||||||
|
// Start tunnel first
|
||||||
|
if err := controller.Start(); err != nil {
|
||||||
|
t.Fatalf("Failed to start tunnel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
formData := url.Values{}
|
||||||
|
formData.Set("action", "Restart")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/test-tcp-client/control", strings.NewReader(formData.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
controller.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Should redirect on success
|
||||||
|
if w.Code != http.StatusSeeOther {
|
||||||
|
t.Errorf("Expected status 303 (redirect), got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify tunnel is running after restart
|
||||||
|
status := controller.Status()
|
||||||
|
if status != "running" {
|
||||||
|
t.Errorf("Expected tunnel to be running after restart, got status: %s", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestControllerInvalidAction tests handling of invalid actions
|
||||||
|
func TestControllerInvalidAction(t *testing.T) {
|
||||||
|
configFile := createTestConfig(t, "test-tcp-client", "tcpclient", "example.i2p", 8080)
|
||||||
|
|
||||||
|
controller, err := NewController(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create controller: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
formData := url.Values{}
|
||||||
|
formData.Set("action", "InvalidAction")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/test-tcp-client/control", strings.NewReader(formData.Encode()))
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
controller.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusBadRequest {
|
||||||
|
t.Errorf("Expected status 400, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
if !strings.Contains(body, "Unknown action") {
|
||||||
|
t.Errorf("Error message should mention unknown action")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMiniServeHTTP tests the mini control widget rendering
|
||||||
|
func TestMiniServeHTTP(t *testing.T) {
|
||||||
|
configFile := createTestConfig(t, "test-tcp-client", "tcpclient", "example.i2p", 8080)
|
||||||
|
|
||||||
|
controller, err := NewController(configFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create controller: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/home", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
controller.MiniServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
if !strings.Contains(body, "test-tcp-client") {
|
||||||
|
t.Errorf("Response should contain tunnel name")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core"
|
i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core"
|
||||||
"github.com/go-i2p/go-i2ptunnel/lib/loader"
|
"github.com/go-i2p/go-i2ptunnel/lib/loader"
|
||||||
|
"github.com/go-i2p/go-i2ptunnel/webui/templates"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,9 +23,158 @@ It uses ../templates/i2ptunnelconfig.html as an HTML template
|
|||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
i2ptunnel.I2PTunnel
|
i2ptunnel.I2PTunnel
|
||||||
|
configPath string // Store config file path for persistence
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfigData holds data for rendering the configuration template
|
||||||
|
type ConfigData struct {
|
||||||
|
Name string
|
||||||
|
ID string
|
||||||
|
Type string
|
||||||
|
Target string
|
||||||
|
Options map[string]string
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP handles both GET (display config form) and POST (save config)
|
||||||
func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
c.handleGetConfig(w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
c.handlePostConfig(w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetConfig displays the configuration form with current settings
|
||||||
|
func (c *Config) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := ConfigData{
|
||||||
|
Name: c.Name(),
|
||||||
|
ID: c.ID(),
|
||||||
|
Type: c.Type(),
|
||||||
|
Target: c.Target(),
|
||||||
|
Options: c.Options(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := templates.I2PTunnelConfigTemplate.Execute(w, data); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Template error: %v", err), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePostConfig processes configuration changes and persists them to disk
|
||||||
|
func (c *Config) handlePostConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
c.renderConfigWithError(w, fmt.Sprintf("Failed to parse form: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that tunnel is not running before applying config changes
|
||||||
|
if c.Status() == i2ptunnel.I2PTunnelStatusRunning {
|
||||||
|
c.renderConfigWithError(w, "Cannot modify configuration while tunnel is running. Stop the tunnel first.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build new options map from form data
|
||||||
|
newOptions := make(map[string]string)
|
||||||
|
for key := range r.Form {
|
||||||
|
// Skip non-option fields
|
||||||
|
if key == "name" || key == "id" || key == "type" || key == "destination" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newOptions[key] = r.FormValue(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate port if provided
|
||||||
|
if portStr := r.FormValue("port"); portStr != "" {
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
c.renderConfigWithError(w, "Invalid port number")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if port < 1 || port > 65535 {
|
||||||
|
c.renderConfigWithError(w, "Port must be between 1 and 65535")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newOptions["port"] = portStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate host if provided
|
||||||
|
if host := r.FormValue("host"); host != "" {
|
||||||
|
newOptions["host"] = host
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply new options to tunnel
|
||||||
|
if err := c.SetOptions(newOptions); err != nil {
|
||||||
|
c.renderConfigWithError(w, fmt.Sprintf("Failed to apply options: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist configuration to disk if config path is available
|
||||||
|
if c.configPath != "" {
|
||||||
|
if err := c.saveConfig(); err != nil {
|
||||||
|
c.renderConfigWithError(w, fmt.Sprintf("Configuration applied but failed to save to disk: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to control page after successful save
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("/%s/control", c.ID()), http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderConfigWithError displays the config form with an error message
|
||||||
|
func (c *Config) renderConfigWithError(w http.ResponseWriter, errMsg string) {
|
||||||
|
data := ConfigData{
|
||||||
|
Name: c.Name(),
|
||||||
|
ID: c.ID(),
|
||||||
|
Type: c.Type(),
|
||||||
|
Target: c.Target(),
|
||||||
|
Options: c.Options(),
|
||||||
|
Error: errMsg,
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates.I2PTunnelConfigTemplate.Execute(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveConfig persists the current tunnel configuration to disk in YAML format
|
||||||
|
func (c *Config) saveConfig() error {
|
||||||
|
// Create config structure matching loader expectations
|
||||||
|
config := map[string]interface{}{
|
||||||
|
"type": c.Type(),
|
||||||
|
"name": c.Name(),
|
||||||
|
"id": c.ID(),
|
||||||
|
"options": c.Options(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if target := c.Target(); target != "" {
|
||||||
|
config["target"] = target
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal to YAML
|
||||||
|
data, err := yaml.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
dir := filepath.Dir(c.configPath)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to file atomically using temp file + rename
|
||||||
|
tempPath := c.configPath + ".tmp"
|
||||||
|
if err := os.WriteFile(tempPath, data, 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write temp config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(tempPath, c.configPath); err != nil {
|
||||||
|
os.Remove(tempPath) // Clean up temp file
|
||||||
|
return fmt.Errorf("failed to save config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig(yamlFile string) (*Config, error) {
|
func NewConfig(yamlFile string) (*Config, error) {
|
||||||
@@ -29,5 +184,6 @@ func NewConfig(yamlFile string) (*Config, error) {
|
|||||||
}
|
}
|
||||||
return &Config{
|
return &Config{
|
||||||
I2PTunnel: tunnel,
|
I2PTunnel: tunnel,
|
||||||
|
configPath: yamlFile,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-i2p/go-i2ptunnel/webui/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -16,10 +19,155 @@ type Controller struct {
|
|||||||
*Config
|
*Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
// ControlData holds data for rendering control templates
|
||||||
|
type ControlData struct {
|
||||||
|
Name string
|
||||||
|
ID string
|
||||||
|
Type string
|
||||||
|
Status string
|
||||||
|
LocalAddress string
|
||||||
|
Address string
|
||||||
|
Target string
|
||||||
|
Options map[string]string
|
||||||
|
Error string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServeHTTP handles the full control page (GET displays info, POST handles actions)
|
||||||
|
func (c *Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
c.handleGetControl(w, r)
|
||||||
|
case http.MethodPost:
|
||||||
|
c.handlePostControl(w, r)
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiniServeHTTP renders the mini control widget for the dashboard
|
||||||
func (c *Controller) MiniServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (c *Controller) MiniServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := c.buildControlData()
|
||||||
|
|
||||||
|
if err := templates.I2PTunnelMiniControlTemplate.Execute(w, data); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Template error: %v", err), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetControl displays the full control page with tunnel status
|
||||||
|
func (c *Controller) handleGetControl(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := c.buildControlData()
|
||||||
|
|
||||||
|
if err := templates.I2PTunnelControlTemplate.Execute(w, data); err != nil {
|
||||||
|
http.Error(w, fmt.Sprintf("Template error: %v", err), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePostControl processes control actions (Start, Stop, Restart)
|
||||||
|
func (c *Controller) handlePostControl(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
c.renderControlWithError(w, fmt.Sprintf("Failed to parse form: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action := r.FormValue("action")
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "Start":
|
||||||
|
err = c.handleStart()
|
||||||
|
case "Stop":
|
||||||
|
err = c.handleStop()
|
||||||
|
case "Restart":
|
||||||
|
err = c.handleRestart()
|
||||||
|
default:
|
||||||
|
c.renderControlWithError(w, fmt.Sprintf("Unknown action: %s", action))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.renderControlWithError(w, fmt.Sprintf("Action '%s' failed: %v", action, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect back to control page to show updated status
|
||||||
|
http.Redirect(w, r, r.URL.Path, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleStart starts the tunnel if it's not already running
|
||||||
|
func (c *Controller) handleStart() error {
|
||||||
|
status := c.Status()
|
||||||
|
if status == "running" || status == "starting" {
|
||||||
|
return fmt.Errorf("tunnel is already %s", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start tunnel: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleStop stops the tunnel if it's running
|
||||||
|
func (c *Controller) handleStop() error {
|
||||||
|
status := c.Status()
|
||||||
|
if status == "stopped" || status == "stopping" {
|
||||||
|
return fmt.Errorf("tunnel is already %s", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Stop(); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop tunnel: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRestart stops and then starts the tunnel
|
||||||
|
func (c *Controller) handleRestart() error {
|
||||||
|
// Stop tunnel if running
|
||||||
|
if c.Status() == "running" {
|
||||||
|
if err := c.Stop(); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop tunnel during restart: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start tunnel
|
||||||
|
if err := c.Start(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start tunnel during restart: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildControlData creates the data structure for control templates
|
||||||
|
func (c *Controller) buildControlData() ControlData {
|
||||||
|
localAddr, _ := c.LocalAddress()
|
||||||
|
|
||||||
|
data := ControlData{
|
||||||
|
Name: c.Name(),
|
||||||
|
ID: c.ID(),
|
||||||
|
Type: c.Type(),
|
||||||
|
Status: string(c.Status()),
|
||||||
|
LocalAddress: localAddr,
|
||||||
|
Address: c.Address(),
|
||||||
|
Target: c.Target(),
|
||||||
|
Options: c.Options(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include error message if tunnel has an error
|
||||||
|
if err := c.Error(); err != nil {
|
||||||
|
data.Error = err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderControlWithError displays the control page with an error message
|
||||||
|
func (c *Controller) renderControlWithError(w http.ResponseWriter, errMsg string) {
|
||||||
|
data := c.buildControlData()
|
||||||
|
data.Error = errMsg
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
templates.I2PTunnelControlTemplate.Execute(w, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewController(yamlFile string) (*Controller, error) {
|
func NewController(yamlFile string) (*Controller, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user