mirror of
https://github.com/go-i2p/go-i2ptunnel.git
synced 2025-12-20 15:15:52 -05:00
190 lines
5.1 KiB
Go
190 lines
5.1 KiB
Go
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"
|
|
)
|
|
|
|
/**
|
|
Config is an I2P Tunnel configuration GUI for a single I2P Tunnel.
|
|
It manages a single i2ptunnel.I2PTunnel, and can accept any implementation of that interface.
|
|
It has no specific behaviors for any tunnel type.
|
|
It presents a simple categorial list of I2PTunnel options.
|
|
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) {
|
|
tunnel, err := loader.Load(yamlFile, "localhost:7656")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Config{
|
|
I2PTunnel: tunnel,
|
|
configPath: yamlFile,
|
|
}, nil
|
|
}
|