255 lines
6.1 KiB
Go
255 lines
6.1 KiB
Go
//go:build kanholec_gui
|
|
|
|
package gui
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/app"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
|
|
"kanhole/pkg/config"
|
|
v1 "kanhole/pkg/config/v1"
|
|
"kanhole/pkg/util/log"
|
|
)
|
|
|
|
type App struct {
|
|
fyneApp fyne.App
|
|
window fyne.Window
|
|
|
|
serverURL *widget.Entry
|
|
authToken *widget.Entry
|
|
clientKey *widget.Entry
|
|
statusLabel *widget.Label
|
|
configView *widget.Entry
|
|
proxyList *widget.List
|
|
startBtn *widget.Button
|
|
stopBtn *widget.Button
|
|
connectBtn *widget.Button
|
|
|
|
mu sync.Mutex
|
|
running bool
|
|
configData string
|
|
}
|
|
|
|
func New() *App {
|
|
a := &App{}
|
|
a.fyneApp = app.New()
|
|
a.window = a.fyneApp.NewWindow("kanholec GUI")
|
|
a.window.Resize(fyne.NewSize(850, 620))
|
|
a.window.SetMaster()
|
|
a.setupUI()
|
|
return a
|
|
}
|
|
|
|
func (a *App) saveSettings() {
|
|
p := a.fyneApp.Preferences()
|
|
p.SetString("server_url", a.serverURL.Text)
|
|
}
|
|
|
|
func (a *App) loadSettings() {
|
|
p := a.fyneApp.Preferences()
|
|
if url := p.StringWithFallback("server_url", "http://localhost:7500"); url != "" {
|
|
a.serverURL.SetText(url)
|
|
}
|
|
}
|
|
|
|
func (a *App) setupUI() {
|
|
a.serverURL = widget.NewEntry()
|
|
a.serverURL.SetText("http://localhost:7500")
|
|
a.serverURL.PlaceHolder = "http://frps-server:7500"
|
|
|
|
a.authToken = widget.NewEntry()
|
|
a.authToken.PlaceHolder = "One-time token"
|
|
|
|
a.clientKey = widget.NewEntry()
|
|
a.clientKey.PlaceHolder = "Client key (from server)"
|
|
|
|
a.statusLabel = widget.NewLabel("Not connected")
|
|
a.statusLabel.Importance = widget.MediumImportance
|
|
|
|
a.configView = widget.NewMultiLineEntry()
|
|
a.configView.SetMinRowsVisible(8)
|
|
a.configView.Disable()
|
|
|
|
a.proxyList = widget.NewList(
|
|
func() int { return 0 },
|
|
func() fyne.CanvasObject { return widget.NewLabel("") },
|
|
func(id widget.ListItemID, o fyne.CanvasObject) {},
|
|
)
|
|
|
|
a.connectBtn = widget.NewButtonWithIcon("Connect", theme.NavigateNextIcon(), a.onConnect)
|
|
a.startBtn = widget.NewButtonWithIcon("Start", theme.MediaPlayIcon(), a.onStart)
|
|
a.startBtn.Disable()
|
|
a.stopBtn = widget.NewButtonWithIcon("Stop", theme.MediaStopIcon(), a.onStop)
|
|
a.stopBtn.Disable()
|
|
|
|
// Settings form
|
|
settingsForm := widget.NewForm(
|
|
widget.NewFormItem("Server URL", a.serverURL),
|
|
)
|
|
settingsForm.OnSubmit = func() { a.saveSettings() }
|
|
settingsBtn := widget.NewButtonWithIcon("Save Settings", theme.DocumentSaveIcon(), a.saveSettings)
|
|
|
|
settingsTab := container.NewVBox(
|
|
widget.NewLabelWithStyle("Connection Settings", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
settingsForm,
|
|
settingsBtn,
|
|
layout.NewSpacer(),
|
|
)
|
|
|
|
// Auth form
|
|
authForm := widget.NewForm(
|
|
widget.NewFormItem("Server URL", a.serverURL),
|
|
widget.NewFormItem("One-Time Token", a.authToken),
|
|
widget.NewFormItem("Client Key", a.clientKey),
|
|
)
|
|
|
|
statusLine := container.NewHBox(
|
|
widget.NewLabelWithStyle("Status:", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
a.statusLabel,
|
|
layout.NewSpacer(),
|
|
a.connectBtn,
|
|
)
|
|
|
|
controls := container.NewHBox(
|
|
a.startBtn,
|
|
a.stopBtn,
|
|
)
|
|
|
|
connectTab := container.NewVBox(
|
|
widget.NewLabelWithStyle("frp Client GUI", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
|
authForm,
|
|
statusLine,
|
|
controls,
|
|
container.NewBorder(
|
|
widget.NewLabelWithStyle("Proxies", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
nil, nil, nil,
|
|
a.proxyList,
|
|
),
|
|
)
|
|
|
|
// Config tab
|
|
configTab := container.NewBorder(
|
|
widget.NewLabelWithStyle("Configuration (read-only)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
|
nil, nil, nil,
|
|
a.configView,
|
|
)
|
|
|
|
tabs := container.NewAppTabs(
|
|
container.NewTabItemWithIcon("Connect", theme.ComputerIcon(), connectTab),
|
|
container.NewTabItemWithIcon("Config", theme.DocumentIcon(), configTab),
|
|
container.NewTabItemWithIcon("Settings", theme.SettingsIcon(), settingsTab),
|
|
)
|
|
tabs.SelectIndex(0)
|
|
|
|
a.window.SetContent(tabs)
|
|
a.loadSettings()
|
|
}
|
|
|
|
func (a *App) onConnect() {
|
|
url := strings.TrimRight(a.serverURL.Text, "/")
|
|
token := a.authToken.Text
|
|
key := a.clientKey.Text
|
|
a.saveSettings()
|
|
|
|
if token == "" && key == "" {
|
|
a.statusLabel.SetText("Error: provide token or client key")
|
|
return
|
|
}
|
|
|
|
if token != "" {
|
|
apiURL := url + "/admin/api/client/auth"
|
|
body := fmt.Sprintf(`{"token":"%s"}`, token)
|
|
resp, err := http.Post(apiURL, "application/json", strings.NewReader(body))
|
|
if err != nil {
|
|
a.statusLabel.SetText(fmt.Sprintf("Error: %v", err))
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
respBody, _ := io.ReadAll(resp.Body)
|
|
a.statusLabel.SetText(fmt.Sprintf("Auth failed (%d): %s", resp.StatusCode, string(respBody)))
|
|
return
|
|
}
|
|
|
|
configData, _ := io.ReadAll(resp.Body)
|
|
a.configData = string(configData)
|
|
a.configView.SetText(a.configData)
|
|
a.statusLabel.SetText("Connected (token auth)")
|
|
a.startBtn.Enable()
|
|
a.connectBtn.SetText("Reconnect")
|
|
} else if key != "" {
|
|
apiURL := fmt.Sprintf("%s/admin/api/frpc/proxy-config/%s", url, key)
|
|
resp, err := http.Get(apiURL)
|
|
if err != nil {
|
|
a.statusLabel.SetText(fmt.Sprintf("Error: %v", err))
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
a.statusLabel.SetText(fmt.Sprintf("Config fetch failed (%d)", resp.StatusCode))
|
|
return
|
|
}
|
|
|
|
configData, _ := io.ReadAll(resp.Body)
|
|
a.configData = string(configData)
|
|
a.configView.SetText(a.configData)
|
|
a.statusLabel.SetText("Connected (key auth)")
|
|
a.startBtn.Enable()
|
|
a.connectBtn.SetText("Reconnect")
|
|
}
|
|
}
|
|
|
|
func (a *App) onStart() {
|
|
if a.configData == "" {
|
|
a.statusLabel.SetText("Error: no config loaded")
|
|
return
|
|
}
|
|
|
|
var cfg v1.ClientConfig
|
|
if err := config.LoadConfigure([]byte(a.configData), &cfg, false, "toml"); err != nil {
|
|
dialog.ShowError(fmt.Errorf("invalid config: %w", err), a.window)
|
|
return
|
|
}
|
|
|
|
a.mu.Lock()
|
|
a.running = true
|
|
a.mu.Unlock()
|
|
|
|
a.startBtn.Disable()
|
|
a.stopBtn.Enable()
|
|
a.statusLabel.SetText("Running")
|
|
}
|
|
|
|
func (a *App) onStop() {
|
|
a.mu.Lock()
|
|
a.running = false
|
|
a.mu.Unlock()
|
|
|
|
a.startBtn.Enable()
|
|
a.stopBtn.Disable()
|
|
a.statusLabel.SetText("Stopped")
|
|
}
|
|
|
|
func (a *App) Run() {
|
|
a.window.ShowAndRun()
|
|
}
|
|
|
|
func Run() {
|
|
gui := New()
|
|
log.Infof("starting frpc GUI")
|
|
gui.Run()
|
|
}
|