- System-wide proxy: automatic Windows proxy configuration for all apps - DNS leak protection: force all DNS queries through VPN - Config encryption: AES-256-GCM encryption for all config files - File protection: strict access permissions for config directory - Leak detection: built-in security check system - Kill Switch: temporarily disabled (will be improved in next version) Security features: ✓ Automatic system proxy setup ✓ DNS leak protection (optional) ✓ AES-256-GCM config encryption ✓ File and directory protection ✓ Security leak checker ⚠ Kill Switch disabled (caused internet blocking issues) Emergency recovery scripts included: - ОТКЛЮЧИТЬ_KILLSWITCH.bat - EMERGENCY_FIX_INTERNET.bat - ЕСЛИ_СЛОМАЛСЯ_ИНТЕРНЕТ.txt Documentation: - Markdown/SECURITY_GUIDE.md - full security guide - БЕЗОПАСНОСТЬ_БЫСТРЫЙ_СТАРТ.md - quick start guide - CHANGELOG_SECURITY.md - detailed changelog
454 lines
12 KiB
Go
454 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"
|
||
)
|
||
|
||
// handleIndex обрабатывает главную страницу
|
||
func handleIndex(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||
fmt.Fprintf(w, `<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>VPN Client</title>
|
||
<meta charset="utf-8">
|
||
</head>
|
||
<body>
|
||
<h1>VPN Client GUI</h1>
|
||
<p>GUI интерфейс в разработке. Используйте CLI режим: <code>vpn-client-gui.exe --cli</code></p>
|
||
</body>
|
||
</html>`)
|
||
}
|
||
|
||
// Запуск 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)
|
||
|
||
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"
|
||
}(),
|
||
})
|
||
}
|