Files
kanhole/client/gui/app.go
T

255 lines
6.1 KiB
Go

//go:build frpc_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"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/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("frpc 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()
}