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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core"
|
||||
"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 {
|
||||
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) {
|
||||
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) {
|
||||
@@ -28,6 +183,7 @@ func NewConfig(yamlFile string) (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
return &Config{
|
||||
I2PTunnel: tunnel,
|
||||
I2PTunnel: tunnel,
|
||||
configPath: yamlFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-i2p/go-i2ptunnel/webui/templates"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -16,10 +19,155 @@ type Controller struct {
|
||||
*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) {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user