refinements

This commit is contained in:
eyedeekay
2025-02-08 21:48:59 -05:00
parent c4e8885653
commit a8d467fc10
16 changed files with 477 additions and 1 deletions

2
go.mod
View File

@@ -5,7 +5,7 @@ go 1.23.5
require ( require (
github.com/go-i2p/go-connfilter v0.0.0-20250205023438-0f2b889a80f6 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-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/go-limit v0.0.0-20250203203118-210616857c15
github.com/go-i2p/i2pkeys v0.33.92 github.com/go-i2p/i2pkeys v0.33.92
github.com/go-i2p/onramp v0.33.92 github.com/go-i2p/onramp v0.33.92

2
go.sum
View File

@@ -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-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 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-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 h1:ASjMbwlepoDQfrhv+H2B5ICBPJU5ES1JzmOxzPDx3YQ=
github.com/go-i2p/go-limit v0.0.0-20250203203118-210616857c15/go.mod h1:4jjmVRhvKj47sQ6B6wdDhN1IrEZunE6KwkYLQx/BeVE= 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= github.com/go-i2p/i2pkeys v0.0.0-20241108200332-e4f5ccdff8c4/go.mod h1:m5TlHjPZrU5KbTd7Lr+I2rljyC6aJ88HdkeMQXV0U0E=

View File

@@ -1,5 +1,7 @@
package i2ptunnel package i2ptunnel
import "strings"
type I2PTunnelStatus string type I2PTunnelStatus string
const ( const (
@@ -24,6 +26,8 @@ type I2PTunnel interface {
Stop() error Stop() error
// Get the tunnel's name // Get the tunnel's name
Name() string Name() string
// Get the tunnel's ID
ID() string
// Get the tunnel's type // Get the tunnel's type
Type() string Type() string
// Get the tunnel's I2P address // Get the tunnel's I2P address
@@ -34,6 +38,8 @@ type I2PTunnel interface {
Options() map[string]string Options() map[string]string
// Set the tunnel's options // Set the tunnel's options
SetOptions(map[string]string) error SetOptions(map[string]string) error
// Load the tunnel config
LoadConfig(path string) error
// Get the tunnel's status // Get the tunnel's status
Status() I2PTunnelStatus Status() I2PTunnelStatus
// Get the tunnel's error message // Get the tunnel's error message
@@ -41,3 +47,20 @@ type I2PTunnel interface {
// Get the tunnel's local host:port // Get the tunnel's local host:port
LocalAddress() (string, error) 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
}

View File

@@ -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 ""
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

0
webui/css/style.css Normal file
View File

0
webui/js/script.js Normal file
View File

View File

@@ -0,0 +1,2 @@
</body>
</html>

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<style>
.mini-tunnel {
border: 1px solid #ccc;
padding: 10px;
margin: 5px;
display: inline-block;
min-width: 200px;
}
.status-running { color: green; }
.status-stopped { color: red; }
.status-starting { color: orange; }
.status-stopping { color: orange; }
.status-failed { color: darkred; }
.status-unknown { color: gray; }
</style>
</head>

View File

@@ -0,0 +1,98 @@
<h1>Configure Tunnel: {{.Name}}</h1>
<form method="POST" action="/{{.ID}}/config">
<fieldset>
<legend>Basic Settings</legend>
<div class="form-group">
<label>Name <span class="required">*</span></label>
<input type="text" name="name" value="{{.Name}}" required>
<div class="help-text">Helpful identifier for this tunnel</div>
<label>ID <span class="required">*</span></label>
<input type="text" name="id" value="{{.ID}}" required>
<div class="help-text">Unique identifier for this tunnel</div>
</div>
<div class="form-group">
<label>Type <span class="required">*</span></label>
<select name="type" required>
<!-- Server Types -->
<optgroup label="Server Tunnels">
<option value="tcpserver" {{if eq .Type "tcpserver"}}selected{{end}}>TCP Server</option>
<option value="httpserver" {{if eq .Type "httpserver"}}selected{{end}}>HTTP Server</option>
<option value="ircserver" {{if eq .Type "ircserver"}}selected{{end}}>IRC Server</option>
<option value="udpserver" {{if eq .Type "udpserver"}}selected{{end}}>UDP Server</option>
</optgroup>
<!-- Client Types -->
<optgroup label="Client Tunnels">
<option value="tcpclient" {{if eq .Type "tcpclient"}}selected{{end}}>TCP Client</option>
<option value="udpclient" {{if eq .Type "udpclient"}}selected{{end}}>UDP Client</option>
<option value="ircclient" {{if eq .Type "ircclient"}}selected{{end}}>IRC Client</option>
</optgroup>
<!-- Proxy Types -->
<optgroup label="Proxy Tunnels">
<option value="httpclient" {{if eq .Type "httpclient"}}selected{{end}}>HTTP Proxy</option>
<option value="socks" {{if eq .Type "socks"}}selected{{end}}>SOCKS Proxy</option>
</optgroup>
<!-- Special Types -->
<!--<optgroup label="Special">
<option value="tunclient" {{if eq .Type "tunclient"}}selected{{end}}>TUN Device</option>
</optgroup>-->
</select>
</div>
</fieldset>
<fieldset>
<legend>Network Settings</legend>
<div class="form-group">
<label>Local Host</label>
<input type="text" name="host" value="{{.Options.host}}" placeholder="127.0.0.1">
</div>
<div class="form-group">
<label>Local Port <span class="required">*</span></label>
<input type="number" name="port" value="{{.Options.port}}" required>
</div>
{{if eq .Type "client"}}
<div class="form-group">
<label>Target Destination <span class="required">*</span></label>
<input type="text" name="destination" value="{{.Target}}">
<div class="help-text">I2P destination address</div>
</div>
{{end}}
</fieldset>
<fieldset>
<legend>I2P Options</legend>
<div class="form-group">
<label>Keys File</label>
<input type="text" name="keys" value="{{.Options.keys}}">
<div class="help-text">Path to keyfile (leave empty for session-length keys)</div>
</div>
<div class="form-group">
<label>Signature Type</label>
<select name="sigtype">
<option value="ED25519" {{if eq .Options.sigtype "ED25519"}}selected{{end}}>ED25519</option>
<option value="ECDSA" {{if eq .Options.sigtype "ECDSA"}}selected{{end}}>ECDSA</option>
</select>
</div>
</fieldset>
{{if .Error}}
<div class="error">
{{.Error}}
</div>
{{end}}
<div class="form-actions">
<button type="submit">Save Changes</button>
<a href="/{{.ID}}/config">Cancel</a>
</div>
</form>

View File

@@ -0,0 +1,69 @@
<h1>Tunnel Control: {{.Name}}</h1>
<div class="tunnel-info">
<h2>Tunnel Information</h2>
<table>
<tr>
<td>Name:</td>
<td>{{.Name}}</td>
</tr>
<tr>
<td>Type:</td>
<td>{{.Type}}</td>
</tr>
<tr>
<td>Status:</td>
<td class="status-{{.Status}}">{{.Status}}</td>
</tr>
<tr>
<td>Local Address:</td>
<td>{{.LocalAddress}}</td>
</tr>
<tr>
<td>I2P Address:</td>
<td>{{.Address}}</td>
</tr>
{{if .Target}}
<tr>
<td>Target:</td>
<td>{{.Target}}</td>
</tr>
{{end}}
</table>
</div>
<div class="tunnel-controls">
<h2>Controls</h2>
<form method="POST">
{{if eq .Status "running"}}
<input type="submit" name="action" value="Stop" />
{{else if eq .Status "stopped"}}
<input type="submit" name="action" value="Start" />
{{end}}
<input type="submit" name="action" value="Restart" />
</form>
</div>
{{if .Error}}
<div class="tunnel-errors">
<h2>Error Messages</h2>
<pre>{{.Error}}</pre>
</div>
{{end}}
<div class="tunnel-options">
<h2>Tunnel Options</h2>
<form method="POST" action="config">
{{range $key, $value := .Options}}
<div>
<label for="{{$key}}">{{$key}}:</label>
<input type="text" id="{{$key}}" name="{{$key}}" value="{{$value}}" />
</div>
{{end}}
<input type="submit" value="Update Options" />
</form>
</div>
<footer>
<p><a href="/home">Return to Tunnel List</a></p>
</footer>

View File

@@ -0,0 +1,4 @@
<div class="add-tunnel">
<h3>Add New Tunnel</h3>
<a href="/new" class="button">Create Tunnel</a>
</div>

View File

@@ -0,0 +1,24 @@
<div class="mini-tunnel">
<h3>
<a href="/tunnel/{{.Name}}">{{.Name}}</a>
<small>({{.Type}})</small>
</h3>
<div class="status-{{.Status}}">
Status: {{.Status}}
</div>
<form method="POST" style="margin-top: 5px;">
{{if eq .Status "running"}}
<input type="submit" name="action" value="Stop" />
{{else if eq .Status "stopped"}}
<input type="submit" name="action" value="Start" />
{{end}}
</form>
{{if .Error}}
<div style="color: red; font-size: 0.8em;">
Error: {{.Error}}
</div>
{{end}}
</div>

View File

@@ -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