- 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
438 lines
12 KiB
Go
438 lines
12 KiB
Go
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"
|
||
}(),
|
||
})
|
||
}
|