feat(gui): add GUI (Test) implementation with documentation and admin support
- Add GUI (Test) module with Fyne-based interface (internal/gui/gui.go, internal/gui/server.go) - Add CLI monitoring capability (internal/cli/monitor.go) - Add main_cli.go entry point for CLI-only builds - Add comprehensive documentation suite covering setup, build, quick start, and changelog - Add admin manifest (admin.manifest) for Windows UAC elevation support - Add rsrc.syso.json configuration for resource embedding - Update .gitignore to exclude build scripts (*.bat, *.sh) - Update main.go and cli.go to support dual GUI (Test)/CLI modes - Update README.md with new project information - Enables users to build and run both GUI (Test)and CLI versions with proper admin privileges on Windows
This commit is contained in:
437
internal/gui/server.go
Normal file
437
internal/gui/server.go
Normal file
@@ -0,0 +1,437 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"vpn-client/internal/config"
|
||||
"vpn-client/internal/subscription"
|
||||
"vpn-client/internal/vless"
|
||||
"vpn-client/internal/vpn"
|
||||
"vpn-client/internal/wireguard"
|
||||
)
|
||||
|
||||
// Запуск HTTP сервера
|
||||
func startServer() string {
|
||||
addr := "127.0.0.1:8765"
|
||||
|
||||
http.HandleFunc("/", handleIndex)
|
||||
http.HandleFunc("/api/status", handleStatus)
|
||||
http.HandleFunc("/api/wireguard/list", handleWireGuardList)
|
||||
http.HandleFunc("/api/wireguard/add", handleWireGuardAdd)
|
||||
http.HandleFunc("/api/wireguard/delete", handleWireGuardDelete)
|
||||
http.HandleFunc("/api/wireguard/connect", handleWireGuardConnect)
|
||||
http.HandleFunc("/api/vless/list", handleVLESSList)
|
||||
http.HandleFunc("/api/vless/add", handleVLESSAdd)
|
||||
http.HandleFunc("/api/vless/delete", handleVLESSDelete)
|
||||
http.HandleFunc("/api/vless/connect", handleVLESSConnect)
|
||||
http.HandleFunc("/api/vless/test", handleVLESSTest)
|
||||
http.HandleFunc("/api/subscriptions/list", handleSubscriptionsList)
|
||||
http.HandleFunc("/api/subscriptions/add", handleSubscriptionsAdd)
|
||||
http.HandleFunc("/api/subscriptions/delete", handleSubscriptionsDelete)
|
||||
http.HandleFunc("/api/subscriptions/update", handleSubscriptionsUpdate)
|
||||
http.HandleFunc("/api/subscriptions/show", handleSubscriptionsShow)
|
||||
http.HandleFunc("/api/subscriptions/test", handleSubscriptionsTest)
|
||||
http.HandleFunc("/api/disconnect", handleDisconnect)
|
||||
|
||||
go http.ListenAndServe(addr, nil)
|
||||
|
||||
return "http://" + addr
|
||||
}
|
||||
|
||||
// API handlers
|
||||
func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := vpn.GetStatus()
|
||||
|
||||
response := map[string]interface{}{
|
||||
"connected": false,
|
||||
"info": "",
|
||||
}
|
||||
|
||||
if err == nil && state.Connected {
|
||||
response["connected"] = true
|
||||
|
||||
info := fmt.Sprintf("Конфиг: %s | Тип: %s", state.ConfigName, state.ConfigType)
|
||||
|
||||
if state.StartTime != "" {
|
||||
startTime, err := time.Parse(time.RFC3339, state.StartTime)
|
||||
if err == nil {
|
||||
duration := time.Since(startTime)
|
||||
hours := int(duration.Hours())
|
||||
minutes := int(duration.Minutes()) % 60
|
||||
seconds := int(duration.Seconds()) % 60
|
||||
info += fmt.Sprintf(" | Время: %02d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
if state.ConfigType == "vless" {
|
||||
info += " | Прокси: 127.0.0.1:10808"
|
||||
} else if state.ConfigType == "wireguard" {
|
||||
stats, err := wireguard.GetStats(state.Interface)
|
||||
if err == nil {
|
||||
info += fmt.Sprintf(" | ↓%s ↑%s", stats["rx"], stats["tx"])
|
||||
}
|
||||
}
|
||||
|
||||
response["info"] = info
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func handleWireGuardList(w http.ResponseWriter, r *http.Request) {
|
||||
configs, _ := config.LoadConfigs()
|
||||
json.NewEncoder(w).Encode(configs.WireGuard)
|
||||
}
|
||||
|
||||
func handleWireGuardAdd(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Config string `json:"config"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
err := wireguard.AddConfig(req.Name, req.Config)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": err == nil,
|
||||
"message": func() string {
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return "Конфиг добавлен"
|
||||
}(),
|
||||
})
|
||||
}
|
||||
|
||||
func handleWireGuardDelete(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
configs, _ := config.LoadConfigs()
|
||||
if req.Index >= 0 && req.Index < len(configs.WireGuard) {
|
||||
name := configs.WireGuard[req.Index].Name
|
||||
wireguard.DeleteConfig(name)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func handleWireGuardConnect(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
configs, _ := config.LoadConfigs()
|
||||
if req.Index >= 0 && req.Index < len(configs.WireGuard) {
|
||||
name := configs.WireGuard[req.Index].Name
|
||||
err := wireguard.Connect(name, config.LogsDir)
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": err == nil,
|
||||
"message": func() string {
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return "Подключено к " + name
|
||||
}(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "Конфиг не найден",
|
||||
})
|
||||
}
|
||||
|
||||
func handleVLESSList(w http.ResponseWriter, r *http.Request) {
|
||||
configs, _ := config.LoadConfigs()
|
||||
json.NewEncoder(w).Encode(configs.VLESS)
|
||||
}
|
||||
|
||||
func handleVLESSAdd(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
configs, err := config.LoadConfigs()
|
||||
if err == nil {
|
||||
configs.VLESS = append(configs.VLESS, config.VLESSConfig{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
Protocol: "VLESS",
|
||||
})
|
||||
config.SaveConfigs(configs)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func handleVLESSDelete(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
configs, _ := config.LoadConfigs()
|
||||
if req.Index >= 0 && req.Index < len(configs.VLESS) {
|
||||
configs.VLESS = append(configs.VLESS[:req.Index], configs.VLESS[req.Index+1:]...)
|
||||
config.SaveConfigs(configs)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func handleVLESSConnect(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
configs, _ := config.LoadConfigs()
|
||||
if req.Index >= 0 && req.Index < len(configs.VLESS) {
|
||||
name := configs.VLESS[req.Index].Name
|
||||
err := vless.Connect(name, config.LogsDir, config.XrayDir)
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": err == nil,
|
||||
"message": func() string {
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return "Подключено к " + name + "\n\nSOCKS5 прокси: 127.0.0.1:10808"
|
||||
}(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "Конфиг не найден",
|
||||
})
|
||||
}
|
||||
|
||||
func handleVLESSTest(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
configs, _ := config.LoadConfigs()
|
||||
if req.Index >= 0 && req.Index < len(configs.VLESS) {
|
||||
cfg := configs.VLESS[req.Index]
|
||||
success, ping, err := vless.PingServer(cfg.URL, 5*time.Second)
|
||||
|
||||
if err != nil || !success {
|
||||
msg := "Сервер недоступен"
|
||||
if err != nil {
|
||||
msg += "\n\nОшибка: " + err.Error()
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": msg})
|
||||
return
|
||||
}
|
||||
|
||||
quality := "Плохо"
|
||||
if ping < 50 {
|
||||
quality = "Отлично"
|
||||
} else if ping < 100 {
|
||||
quality = "Хорошо"
|
||||
} else if ping < 200 {
|
||||
quality = "Средне"
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("✓ Сервер доступен\n\nПинг: %.2f мс\nКачество: %s", ping, quality)
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": msg})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "Конфиг не найден"})
|
||||
}
|
||||
|
||||
func handleSubscriptionsList(w http.ResponseWriter, r *http.Request) {
|
||||
subs, _ := config.LoadSubscriptions()
|
||||
json.NewEncoder(w).Encode(subs.Subscriptions)
|
||||
}
|
||||
|
||||
func handleSubscriptionsAdd(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
subs, err := config.LoadSubscriptions()
|
||||
if err == nil {
|
||||
subs.Subscriptions = append(subs.Subscriptions, config.Subscription{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
})
|
||||
config.SaveSubscriptions(subs)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func handleSubscriptionsDelete(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
subs, _ := config.LoadSubscriptions()
|
||||
if req.Index >= 0 && req.Index < len(subs.Subscriptions) {
|
||||
subs.Subscriptions = append(subs.Subscriptions[:req.Index], subs.Subscriptions[req.Index+1:]...)
|
||||
config.SaveSubscriptions(subs)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func handleSubscriptionsUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
subs, _ := config.LoadSubscriptions()
|
||||
if req.Index >= 0 && req.Index < len(subs.Subscriptions) {
|
||||
name := subs.Subscriptions[req.Index].Name
|
||||
err := subscription.UpdateSubscription(name, config.LogsDir)
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": err == nil,
|
||||
"message": func() string {
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return "Конфиги обновлены из подписки"
|
||||
}(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "Подписка не найдена",
|
||||
})
|
||||
}
|
||||
|
||||
func handleSubscriptionsShow(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
subs, _ := config.LoadSubscriptions()
|
||||
if req.Index >= 0 && req.Index < len(subs.Subscriptions) {
|
||||
subName := subs.Subscriptions[req.Index].Name
|
||||
|
||||
configs, _ := config.LoadConfigs()
|
||||
var subConfigs []config.VLESSConfig
|
||||
for _, cfg := range configs.VLESS {
|
||||
if cfg.Subscription == subName {
|
||||
subConfigs = append(subConfigs, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(subConfigs) == 0 {
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"message": "Нет конфигов из этой подписки.\n\nСначала обновите конфиги из подписки.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("Конфиги из '%s':\n\n", subName)
|
||||
for i, cfg := range subConfigs {
|
||||
protocol := cfg.Protocol
|
||||
if protocol == "" {
|
||||
protocol = "Unknown"
|
||||
}
|
||||
msg += fmt.Sprintf("%d. [%s] %s\n", i+1, protocol, cfg.Name)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": msg})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "Подписка не найдена"})
|
||||
}
|
||||
|
||||
func handleSubscriptionsTest(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Index int `json:"index"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
subs, _ := config.LoadSubscriptions()
|
||||
if req.Index >= 0 && req.Index < len(subs.Subscriptions) {
|
||||
subName := subs.Subscriptions[req.Index].Name
|
||||
|
||||
configs, _ := config.LoadConfigs()
|
||||
var subConfigs []config.VLESSConfig
|
||||
for _, cfg := range configs.VLESS {
|
||||
if cfg.Subscription == subName {
|
||||
subConfigs = append(subConfigs, cfg)
|
||||
}
|
||||
}
|
||||
|
||||
if len(subConfigs) == 0 {
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "Нет конфигов из этой подписки"})
|
||||
return
|
||||
}
|
||||
|
||||
// Тестирование (упрощенная версия - первые 10)
|
||||
msg := fmt.Sprintf("Тестирование серверов из '%s'...\n\n", subName)
|
||||
tested := 0
|
||||
for i, cfg := range subConfigs {
|
||||
if i >= 10 {
|
||||
break
|
||||
}
|
||||
tested++
|
||||
success, ping, _ := vless.PingServer(cfg.URL, 3*time.Second)
|
||||
if success {
|
||||
msg += fmt.Sprintf("✓ %s - %.2f мс\n", cfg.Name, ping)
|
||||
} else {
|
||||
msg += fmt.Sprintf("✗ %s - недоступен\n", cfg.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(subConfigs) > 10 {
|
||||
msg += fmt.Sprintf("\n(Показано %d из %d серверов)", tested, len(subConfigs))
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": msg})
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "Подписка не найдена"})
|
||||
}
|
||||
|
||||
func handleDisconnect(w http.ResponseWriter, r *http.Request) {
|
||||
state, err := vpn.GetStatus()
|
||||
if err != nil || !state.Connected {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "VPN не подключен",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = vpn.Disconnect(config.LogsDir)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"success": err == nil,
|
||||
"message": func() string {
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return "Отключено от VPN"
|
||||
}(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user