feat: ent ORM, admin UI, client auth, Fyne GUI, Windows/MSI packaging
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
//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()
|
||||
}
|
||||
Reference in New Issue
Block a user