//go:build windows package main import ( "bytes" "context" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "sync" "syscall" "time" "unsafe" "github.com/jchv/go-webview2" "golang.org/x/sys/windows" "vpnclient/internal/appui" "vpnclient/internal/config" "vpnclient/internal/core" "vpnclient/internal/netcheck" "vpnclient/internal/protocols/hysteria2" "vpnclient/internal/protocols/naive" "vpnclient/internal/update" ) type app struct { mu sync.Mutex mgr *core.Manager cfgPath string logBuf *bytes.Buffer updateStatus update.Status pings []netcheck.Result webview webview2.WebView } type uiState struct { Connected bool `json:"connected"` Profile string `json:"profile,omitempty"` ActiveProfile string `json:"active_profile,omitempty"` Protocol string `json:"protocol,omitempty"` HTTPProxy string `json:"http_proxy,omitempty"` SOCKSProxy string `json:"socks_proxy,omitempty"` SystemProxy bool `json:"system_proxy"` Proxy string `json:"proxy"` CoreReady bool `json:"core_ready"` CorePath string `json:"core_path,omitempty"` ConfigPath string `json:"config_path"` Profiles []config.ProfileInfo `json:"profiles"` Version string `json:"version"` Update update.Status `json:"update"` Pings []netcheck.Result `json:"pings"` Subscription string `json:"subscription_url"` Hy2 core.Hy2Options `json:"hy2"` } func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) cfgPath, err := config.LocateConfig() if err != nil { fatalDialog("Не удалось найти путь конфига: %v", err) } if _, err := os.Stat(cfgPath); os.IsNotExist(err) { if err := config.WriteExample(cfgPath); err != nil { fatalDialog("Не удалось создать конфиг: %v", err) } } cfg, err := config.Load(cfgPath) if err != nil { fatalDialog("Ошибка конфига %s: %v", cfgPath, err) } logBuf := &bytes.Buffer{} mgr, err := core.NewManager(cfgPath, cfg, logBuf) if err != nil { fatalDialog("%v", err) } a := &app{ mgr: mgr, cfgPath: cfgPath, logBuf: logBuf, updateStatus: update.Status{ Current: update.CurrentVersion, ManifestURL: update.DefaultManifestURL, }, } dataPath := filepath.Join(filepath.Dir(cfgPath), ".navis-webview") _ = os.MkdirAll(dataPath, 0o755) go func() { _, _ = mgr.EnsureAllCores() }() w := webview2.NewWithOptions(webview2.WebViewOptions{ Debug: false, AutoFocus: true, DataPath: dataPath, WindowOptions: webview2.WindowOptions{ Title: "Navis", Width: 520, Height: 860, Center: true, IconId: 1, }, }) if w == nil { fatalDialog("Не удалось создать окно WebView2.\nУстановите Microsoft Edge WebView2 Runtime.") } a.webview = w defer func() { _ = a.mgr.Disconnect() w.Destroy() }() applyWindowIcon(w.Window()) w.SetSize(520, 860, webview2.HintNone) mustBind(w, "getState", a.getState) mustBind(w, "connect", a.connect) mustBind(w, "disconnect", a.disconnect) mustBind(w, "saveProfile", a.saveProfile) mustBind(w, "createProfile", a.createProfile) mustBind(w, "selectProfile", a.selectProfile) mustBind(w, "deleteProfile", a.deleteProfile) mustBind(w, "installCore", a.installCore) mustBind(w, "openURL", openURL) mustBind(w, "pingServers", a.pingServers) mustBind(w, "checkUpdate", a.checkUpdate) mustBind(w, "applyUpdate", a.applyUpdate) mustBind(w, "saveHy2", a.saveHy2) mustBind(w, "importSubscription", a.importSubscription) go a.autoCheckUpdate() w.SetHtml(appui.IndexHTML) w.Run() } func mustBind(w webview2.WebView, name string, fn interface{}) { if err := w.Bind(name, fn); err != nil { fatalDialog("bind %s: %v", name, err) } } func (a *app) getState() (uiState, error) { a.mu.Lock() defer a.mu.Unlock() st := a.mgr.Status() cfg := a.mgr.Config() proxy := "" active := cfg.Active if p, err := cfg.ActiveProfile(); err == nil { proxy = p.Proxy active = p.Name } corePath := "" coreReady := false if p, err := cfg.ActiveProfile(); err == nil { switch p.Protocol { case config.ProtocolHysteria2: if path, err := hysteria2.ResolveBinary(a.mgr.BinDir()); err == nil { corePath, coreReady = path, true } default: if path, err := naive.ResolveBinary(a.mgr.BinDir()); err == nil { corePath, coreReady = path, true } } } else if path, err := naive.ResolveBinary(a.mgr.BinDir()); err == nil { corePath, coreReady = path, true } out := uiState{ Connected: st.Connected, Profile: st.Profile, ActiveProfile: active, Protocol: string(st.Protocol), HTTPProxy: st.HTTPProxy, SOCKSProxy: st.SOCKSProxy, SystemProxy: cfg.SystemProxy, Proxy: proxy, CoreReady: coreReady, CorePath: corePath, ConfigPath: a.cfgPath, Profiles: cfg.ListProfiles(), Version: update.CurrentVersion, Update: a.updateStatus, Pings: append([]netcheck.Result(nil), a.pings...), Subscription: cfg.SubscriptionURL, Hy2: a.mgr.ActiveHy2Options(), } if out.Protocol == "" { if p, err := cfg.ActiveProfile(); err == nil { out.Protocol = string(p.Protocol) } } if st.Connected { out.SystemProxy = st.SystemProxy } return out, nil } func (a *app) saveProfile(name, proxy string, systemProxy bool) error { a.mu.Lock() defer a.mu.Unlock() name = strings.TrimSpace(name) if name == "" { return fmt.Errorf("укажите название профиля") } a.mgr.SetSystemProxy(systemProxy) return a.mgr.SaveActiveProfile(name, proxy) } func (a *app) createProfile(name, proxy string, systemProxy bool) error { a.mu.Lock() defer a.mu.Unlock() name = strings.TrimSpace(name) if name == "" { return fmt.Errorf("укажите название профиля") } a.mgr.SetSystemProxy(systemProxy) return a.mgr.SaveProfile(name, proxy) } func (a *app) selectProfile(name string) error { a.mu.Lock() defer a.mu.Unlock() return a.mgr.SetActiveProfile(name) } func (a *app) deleteProfile(name string) error { a.mu.Lock() defer a.mu.Unlock() return a.mgr.DeleteProfile(name) } func (a *app) connect() error { a.mu.Lock() defer a.mu.Unlock() if p, err := a.mgr.Config().ActiveProfile(); err != nil { return err } else if strings.TrimSpace(p.Proxy) == "" { return fmt.Errorf("сначала вставьте ссылку сервера") } if _, err := a.mgr.EnsureCore(""); err != nil { return err } ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) defer cancel() return a.mgr.Connect(ctx, "") } func (a *app) disconnect() error { a.mu.Lock() defer a.mu.Unlock() return a.mgr.Disconnect() } func (a *app) installCore() (string, error) { a.mu.Lock() defer a.mu.Unlock() paths, err := a.mgr.EnsureAllCores() if err != nil { return "", err } parts := make([]string, 0, len(paths)) for k, v := range paths { parts = append(parts, k+": "+v) } return strings.Join(parts, " | "), nil } func (a *app) saveHy2(opts core.Hy2Options) error { a.mu.Lock() defer a.mu.Unlock() return a.mgr.SaveHy2Options(opts) } func (a *app) importSubscription(rawURL string) (int, error) { return a.mgr.ImportSubscription(rawURL) } func (a *app) pingServers() ([]netcheck.Result, error) { a.mu.Lock() list := a.mgr.Profiles() a.mu.Unlock() ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() out := make([]netcheck.Result, 0, len(list)) for _, p := range list { proxy := p.Proxy if proxy == "" { out = append(out, netcheck.Result{Name: p.Name, Error: "нет ссылки сервера"}) continue } out = append(out, netcheck.PingProfile(ctx, p.Name, config.Protocol(p.Protocol), proxy)) } a.mu.Lock() a.pings = out a.mu.Unlock() return out, nil } func (a *app) checkUpdate() (update.Status, error) { ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second) defer cancel() st, err := update.Check(ctx, update.DefaultManifestURL) a.mu.Lock() if err != nil { st.Current = update.CurrentVersion st.ManifestURL = update.DefaultManifestURL st.Error = err.Error() } a.updateStatus = st a.mu.Unlock() return st, nil } func (a *app) applyUpdate() error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() latest, err := update.Apply(ctx, update.DefaultManifestURL) if err != nil { return err } _ = a.mgr.Disconnect() go func() { time.Sleep(400 * time.Millisecond) if a.webview != nil { a.webview.Terminate() } os.Exit(0) }() _ = latest return nil } func (a *app) autoCheckUpdate() { time.Sleep(2 * time.Second) _, _ = a.checkUpdate() ticker := time.NewTicker(6 * time.Hour) defer ticker.Stop() for range ticker.C { _, _ = a.checkUpdate() } } func openURL(raw string) error { raw = strings.TrimSpace(raw) switch raw { case "https://evilfox.win/", "https://evilfox.win", "http://evilfox.win/", "http://evilfox.win": raw = "https://evilfox.win/" default: return fmt.Errorf("разрешена только ссылка evilfox.win") } cmd := exec.Command("rundll32", "url.dll,FileProtocolHandler", raw) cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} return cmd.Start() } func applyWindowIcon(hwnd unsafe.Pointer) { ico := findIconPath() if ico == "" || hwnd == nil { return } pathPtr, err := windows.UTF16PtrFromString(ico) if err != nil { return } user32 := windows.NewLazySystemDLL("user32.dll") loadImage := user32.NewProc("LoadImageW") sendMessage := user32.NewProc("SendMessageW") const ( imageIcon = 1 lrLoadFromFile = 0x0010 lrDefaultSize = 0x0040 wmSetIcon = 0x0080 iconSmall = 0 iconBig = 1 ) h, _, _ := loadImage.Call(0, uintptr(unsafe.Pointer(pathPtr)), imageIcon, 0, 0, lrLoadFromFile|lrDefaultSize) if h == 0 { return } sendMessage.Call(uintptr(hwnd), wmSetIcon, iconSmall, h) sendMessage.Call(uintptr(hwnd), wmSetIcon, iconBig, h) } func findIconPath() string { candidates := []string{} if exe, err := os.Executable(); err == nil { dir := filepath.Dir(exe) candidates = append(candidates, filepath.Join(dir, "assets", "navis.ico"), filepath.Join(dir, "navis.ico"), ) } if cwd, err := os.Getwd(); err == nil { candidates = append(candidates, filepath.Join(cwd, "assets", "navis.ico")) } for _, c := range candidates { if st, err := os.Stat(c); err == nil && !st.IsDir() { return c } } return "" } func fatalDialog(format string, args ...interface{}) { msg := fmt.Sprintf(format, args...) log.Println(msg) messageBox(msg) os.Exit(1) }