diff --git a/go.mod b/go.mod index 25972ad..aaa9040 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.5 require ( github.com/go-i2p/go-connfilter v0.0.0-20250205023438-0f2b889a80f6 github.com/go-i2p/go-forward v0.0.0-20250202052226-ee8a43dcb664 - github.com/go-i2p/go-i2ptunnel-config v0.0.0-20250208035926-cff0b0758eda + github.com/go-i2p/go-i2ptunnel-config v0.0.0-20250209024146-bb43a7caaf9f github.com/go-i2p/go-limit v0.0.0-20250203203118-210616857c15 github.com/go-i2p/i2pkeys v0.33.92 github.com/go-i2p/onramp v0.33.92 diff --git a/go.sum b/go.sum index dac84d7..524f51a 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/go-i2p/go-i2ptunnel-config v0.0.0-20250203061220-6b5e19741c47 h1:3F1v github.com/go-i2p/go-i2ptunnel-config v0.0.0-20250203061220-6b5e19741c47/go.mod h1:u8CgiYIfehSFpoVWNe1up6TO4sasPpRUHxZw7W2e4sM= github.com/go-i2p/go-i2ptunnel-config v0.0.0-20250208035926-cff0b0758eda h1:I5z+lG0tk6TB/GY1wEZLVJZer8kuA9KCG0IdrJWGghQ= github.com/go-i2p/go-i2ptunnel-config v0.0.0-20250208035926-cff0b0758eda/go.mod h1:u8CgiYIfehSFpoVWNe1up6TO4sasPpRUHxZw7W2e4sM= +github.com/go-i2p/go-i2ptunnel-config v0.0.0-20250209024146-bb43a7caaf9f h1:sLuEjwk/1NfH7krFMdyyqeM43IpkcaYBxv4pt6k8l3I= +github.com/go-i2p/go-i2ptunnel-config v0.0.0-20250209024146-bb43a7caaf9f/go.mod h1:u8CgiYIfehSFpoVWNe1up6TO4sasPpRUHxZw7W2e4sM= github.com/go-i2p/go-limit v0.0.0-20250203203118-210616857c15 h1:ASjMbwlepoDQfrhv+H2B5ICBPJU5ES1JzmOxzPDx3YQ= github.com/go-i2p/go-limit v0.0.0-20250203203118-210616857c15/go.mod h1:4jjmVRhvKj47sQ6B6wdDhN1IrEZunE6KwkYLQx/BeVE= github.com/go-i2p/i2pkeys v0.0.0-20241108200332-e4f5ccdff8c4/go.mod h1:m5TlHjPZrU5KbTd7Lr+I2rljyC6aJ88HdkeMQXV0U0E= diff --git a/lib/core/types.go b/lib/core/types.go index 4bb7f76..a54eb41 100644 --- a/lib/core/types.go +++ b/lib/core/types.go @@ -1,5 +1,7 @@ package i2ptunnel +import "strings" + type I2PTunnelStatus string const ( @@ -24,6 +26,8 @@ type I2PTunnel interface { Stop() error // Get the tunnel's name Name() string + // Get the tunnel's ID + ID() string // Get the tunnel's type Type() string // Get the tunnel's I2P address @@ -34,6 +38,8 @@ type I2PTunnel interface { Options() map[string]string // Set the tunnel's options SetOptions(map[string]string) error + // Load the tunnel config + LoadConfig(path string) error // Get the tunnel's status Status() I2PTunnelStatus // Get the tunnel's error message @@ -41,3 +47,20 @@ type I2PTunnel interface { // Get the tunnel's local host:port LocalAddress() (string, error) } + +// Clean the name to form an ID +// change newlines to + +// change tabs to _ +// change spaces to - +// erase foreslashes +func Clean(name string) string { + // change newlines to + + // change tabs to _ + // change spaces to - + // erase foreslashes + clean := strings.ReplaceAll(name, "\n", "+") + clean = strings.ReplaceAll(clean, "\t", "_") + clean = strings.ReplaceAll(clean, " ", "-") + clean = strings.ReplaceAll(clean, "/", "") + return clean +} diff --git a/webui/controller/fromhome.go b/webui/controller/fromhome.go new file mode 100644 index 0000000..43ac7db --- /dev/null +++ b/webui/controller/fromhome.go @@ -0,0 +1,36 @@ +package controller + +import ( + "net/http" + "path/filepath" + + i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core" +) + +func handler(r *http.Request) string { + if r != nil { + dir, file := filepath.Split(r.URL.Path) + if dir == "/" { + if file == "home" { + return "group" + } + } else { + if file == "config" { + return "config" + } else if file == "control" { + return "control" + } + } + } + return "group" +} + +func tunnel(r *http.Request) string { + if r != nil { + dir, file := filepath.Split(r.URL.Path) + if file == "config" || file == "control" { + return i2ptunnel.Clean(dir) + } + } + return "" +} diff --git a/webui/controller/i2ptunnelconfig.go b/webui/controller/i2ptunnelconfig.go new file mode 100644 index 0000000..5b63e44 --- /dev/null +++ b/webui/controller/i2ptunnelconfig.go @@ -0,0 +1,32 @@ +package controller + +import ( + "net/http" + + i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core" +) + +/** +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 +} + +func (c *Config) ServeHTTP(w http.ResponseWriter, r *http.Request) { + +} + +func NewConfig(yamlFile string) (*Config, error) { + c := &Config{} + err := c.LoadConfig(yamlFile) + if err != nil { + return nil, err + } + return c, nil +} diff --git a/webui/controller/i2ptunnelcontrol.go b/webui/controller/i2ptunnelcontrol.go new file mode 100644 index 0000000..75c1a9e --- /dev/null +++ b/webui/controller/i2ptunnelcontrol.go @@ -0,0 +1,36 @@ +package controller + +import ( + "net/http" +) + +/* +* +Controller is a tunnel controller 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 uses ../templates/i2ptunnelcontrol.html as an HTML template for the control page. +It uses ../templates/i2ptunnelminicontrol.html as an HTML template for the home page. +*/ +type Controller struct { + *Config +} + +func (c *Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) { + +} + +func (c *Controller) MiniServeHTTP(w http.ResponseWriter, r *http.Request) { + +} + +func NewController(yamlFile string) (*Controller, error) { + cfg, err := NewConfig(yamlFile) + if err != nil { + return nil, err + } + c := &Controller{ + Config: cfg, + } + return c, err +} diff --git a/webui/controller/i2ptunnelgroup.go b/webui/controller/i2ptunnelgroup.go new file mode 100644 index 0000000..c35f537 --- /dev/null +++ b/webui/controller/i2ptunnelgroup.go @@ -0,0 +1,87 @@ +package controller + +import ( + "net/http" + "os" + "path/filepath" + "strings" + + i2ptunnel "github.com/go-i2p/go-i2ptunnel/lib/core" + templates "github.com/go-i2p/go-i2ptunnel/webui/templates" +) + +type ControllerGroup struct { + I2PTunnels []Controller +} + +func (cg *ControllerGroup) ServeHTTP(w http.ResponseWriter, r *http.Request) { + cg.HandleHTMLHeader(r, w) + defer cg.HandleHTMLFooter(r, w) + switch handler(r) { + case "group": + cg.HandleGroup(r, w) + case "control": + for _, controller := range cg.I2PTunnels { + if i2ptunnel.Clean(controller.Name()) == tunnel(r) { + controller.ServeHTTP(w, r) + return + } + } + cg.HandleError(r, w) + case "config": + for _, controller := range cg.I2PTunnels { + if i2ptunnel.Clean(controller.Name()) == tunnel(r) { + controller.Config.ServeHTTP(w, r) + return + } + } + cg.HandleError(r, w) + default: + cg.HandleGroup(r, w) + } +} + +func (cg *ControllerGroup) HandleHTMLHeader(r *http.Request, w http.ResponseWriter) { + templates.HeaderTemplate.Execute(w, nil) +} + +func (cg *ControllerGroup) HandleHTMLFooter(r *http.Request, w http.ResponseWriter) { + templates.FooterTemplate.Execute(w, nil) +} + +func (cg *ControllerGroup) HandleGroup(r *http.Request, w http.ResponseWriter) { + for _, controller := range cg.I2PTunnels { + if i2ptunnel.Clean(controller.Name()) == tunnel(r) { + controller.MiniServeHTTP(w, r) + } + } +} + +func (cg *ControllerGroup) HandleError(r *http.Request, w http.ResponseWriter) { + r.Form = nil + // just redirect back to /home + http.Redirect(w, r, "/home", 302) +} + +func NewControllerGroup(directory string) (*ControllerGroup, error) { + files, err := os.ReadDir(directory) + if err != nil { + return nil, err + } + + group := &ControllerGroup{ + I2PTunnels: make([]Controller, 0), + } + + for _, file := range files { + if !file.IsDir() && strings.HasSuffix(file.Name(), ".yaml") { + controller, err := NewController(filepath.Join(directory, file.Name())) + if err != nil { + return nil, err + } + group.I2PTunnels = append(group.I2PTunnels, *controller) + } + } + + return group, nil +} diff --git a/webui/css/style.css b/webui/css/style.css new file mode 100644 index 0000000..e69de29 diff --git a/webui/js/script.js b/webui/js/script.js new file mode 100644 index 0000000..e69de29 diff --git a/webui/templates/footer.html b/webui/templates/footer.html new file mode 100644 index 0000000..691287b --- /dev/null +++ b/webui/templates/footer.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/webui/templates/header.html b/webui/templates/header.html new file mode 100644 index 0000000..24197a4 --- /dev/null +++ b/webui/templates/header.html @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/webui/templates/i2ptunnelconfig.html b/webui/templates/i2ptunnelconfig.html new file mode 100644 index 0000000..b55e89b --- /dev/null +++ b/webui/templates/i2ptunnelconfig.html @@ -0,0 +1,98 @@ +

Configure Tunnel: {{.Name}}

+ +
+
+ Basic Settings + +
+ + +
Helpful identifier for this tunnel
+ + +
Unique identifier for this tunnel
+
+ +
+ + +
+
+ +
+ Network Settings + +
+ + +
+ +
+ + +
+ + {{if eq .Type "client"}} +
+ + +
I2P destination address
+
+ {{end}} +
+ +
+ I2P Options + +
+ + +
Path to keyfile (leave empty for session-length keys)
+
+ +
+ + +
+
+ + {{if .Error}} +
+ {{.Error}} +
+ {{end}} + +
+ + Cancel +
+
\ No newline at end of file diff --git a/webui/templates/i2ptunnelcontrol.html b/webui/templates/i2ptunnelcontrol.html new file mode 100644 index 0000000..cfef779 --- /dev/null +++ b/webui/templates/i2ptunnelcontrol.html @@ -0,0 +1,69 @@ +

Tunnel Control: {{.Name}}

+ +
+

Tunnel Information

+ + + + + + + + + + + + + + + + + + + + + + {{if .Target}} + + + + + {{end}} +
Name:{{.Name}}
Type:{{.Type}}
Status:{{.Status}}
Local Address:{{.LocalAddress}}
I2P Address:{{.Address}}
Target:{{.Target}}
+
+ +
+

Controls

+
+ {{if eq .Status "running"}} + + {{else if eq .Status "stopped"}} + + {{end}} + +
+
+ +{{if .Error}} +
+

Error Messages

+
{{.Error}}
+
+{{end}} + +
+

Tunnel Options

+
+ {{range $key, $value := .Options}} +
+ + +
+ {{end}} + +
+
+ + diff --git a/webui/templates/i2ptunnelgroup.html b/webui/templates/i2ptunnelgroup.html new file mode 100644 index 0000000..07521a5 --- /dev/null +++ b/webui/templates/i2ptunnelgroup.html @@ -0,0 +1,4 @@ +
+

Add New Tunnel

+ Create Tunnel +
\ No newline at end of file diff --git a/webui/templates/i2ptunnelminicontrol.html b/webui/templates/i2ptunnelminicontrol.html new file mode 100644 index 0000000..558111a --- /dev/null +++ b/webui/templates/i2ptunnelminicontrol.html @@ -0,0 +1,24 @@ +
+

+ {{.Name}} + ({{.Type}}) +

+ +
+ Status: {{.Status}} +
+ +
+ {{if eq .Status "running"}} + + {{else if eq .Status "stopped"}} + + {{end}} +
+ + {{if .Error}} +
+ Error: {{.Error}} +
+ {{end}} +
\ No newline at end of file diff --git a/webui/templates/templates.go b/webui/templates/templates.go new file mode 100644 index 0000000..9a0a09f --- /dev/null +++ b/webui/templates/templates.go @@ -0,0 +1,44 @@ +package templates + +import ( + "embed" + html "html/template" +) + +// embeds the header.html template +// +//go:embed header.html +var BytesHeaderTemplate []byte +var HeaderTemplate, _ = html.New("header").Parse(string(BytesHeaderTemplate)) + +// embeds the i2ptunnelconfig.html template +// +//go:embed i2ptunnelconfig.html +var BytesI2PTunnelConfigTemplate []byte +var I2PTunnelConfigTemplate, _ = html.New("i2ptunnelconfig").Parse(string(BytesI2PTunnelConfigTemplate)) + +// embeds the i2ptunnelcontrol.html template +// +//go:embed i2ptunnelcontrol.html +var BytesI2PTunnelControlTemplate []byte +var I2PTunnelControlTemplate, _ = html.New("i2ptunnelcontrol").Parse(string(BytesI2PTunnelControlTemplate)) + +// embeds the i2ptunnelminicontrol.html template +// +//go:embed i2ptunnelminicontrol.html +var BytesI2PTunnelMiniControlTemplate []byte +var I2PTunnelMiniControlTemplate, _ = html.New("i2ptunnelminicontrol").Parse(string(BytesI2PTunnelMiniControlTemplate)) + +// embeds the i2ptunnelgroup.html template +// +//go:embed i2ptunnelgroup.html +var BytesI2PTunnelGroupTemplate []byte +var I2PTunnelGroupTemplate, _ = html.New("i2ptunnelgroup").Parse(string(BytesI2PTunnelGroupTemplate)) + +// embeds the footer.html template +// +//go:embed footer.html +var BytesFooterTemplate []byte +var FooterTemplate, _ = html.New("footer").Parse(string(BytesFooterTemplate)) + +var efs embed.FS