diff --git a/README.md b/README.md index 5c2bd70..4487a30 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ | OS | Артефакты | UI | |----|----------|----| -| Windows amd64 | `Navis.exe` | GUI (WebView2) | -| macOS universal | `Navis.dmg` · `Navis.app.zip` | **GUI** (как Windows) | +| Windows amd64 | `Navis.exe` | GUI (glaze → WebView2 + HTTP UI) | +| macOS universal | `Navis.dmg` · `Navis.app.zip` | GUI (glaze → WKWebView + HTTP UI) | | macOS arm64 / amd64 | `Navis` · DMG · `Navis-cli` | GUI + CLI | | Android | `Navis.apk` | GUI (VpnService) | @@ -192,10 +192,14 @@ cd ~/Navis ## Сборка Windows +GUI — тот же HTML UI, что и на macOS: локальный HTTP API + окно [glaze](https://github.com/crgimenes/glaze) (на Windows под капотом Edge WebView2 Runtime). + ```bat build.bat ``` +Артефакты: `Navis-..exe`, `vpnclient-..exe` (и копии в `dist\navis-release\`). + или: ```bat diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b279f98..6c5368e 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -13,8 +13,8 @@ android { minSdk = 26 targetSdk = 35 // versionCode = major*1_000_000 + minor*10_000 + patch*100 + build - versionCode = 3080203 - versionName = "3.8.2+3" + versionCode = 3080204 + versionName = "3.8.2+4" vectorDrawables.useSupportLibrary = true ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") diff --git a/assets/navis.ico b/assets/navis.ico index a853848..5fffae7 100644 Binary files a/assets/navis.ico and b/assets/navis.ico differ diff --git a/build-macos.bat b/build-macos.bat index e74bea6..63ea3d3 100644 --- a/build-macos.bat +++ b/build-macos.bat @@ -1,7 +1,15 @@ @echo off -setlocal +setlocal EnableExtensions cd /d "%~dp0" +echo Bumping build number... +where py >nul 2>&1 && py -3 scripts\bump-build.py && goto :bumped +where python >nul 2>&1 && python scripts\bump-build.py && goto :bumped +where python3 >nul 2>&1 && python3 scripts\bump-build.py && goto :bumped +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\bump-build.ps1" +if errorlevel 1 exit /b 1 +:bumped + mkdir "dist\navis-release\darwin-arm64" 2>nul mkdir "dist\navis-release\darwin-amd64" 2>nul mkdir "dist\navis-release\darwin-universal" 2>nul @@ -34,11 +42,11 @@ if errorlevel 1 exit /b 1 go build -o "tools\packmac\packmac.exe" .\tools\packmac if errorlevel 1 exit /b 1 -tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 3.8.2 -build 3.8.2.3 -arch arm64 +tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 3.8.2 -build 3.8.2.4 -arch arm64 if errorlevel 1 exit /b 1 -tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 3.8.2 -build 3.8.2.3 -arch amd64 +tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 3.8.2 -build 3.8.2.4 -arch amd64 if errorlevel 1 exit /b 1 -tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 3.8.2 -build 3.8.2.3 -arch universal +tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 3.8.2 -build 3.8.2.4 -arch universal if errorlevel 1 exit /b 1 echo Built Mac GUI + CLI: diff --git a/build.bat b/build.bat index 9374f1e..24da7a7 100644 --- a/build.bat +++ b/build.bat @@ -1,7 +1,24 @@ @echo off -setlocal +setlocal EnableExtensions cd /d "%~dp0" +echo Bumping build number... +call :bump_build +if errorlevel 1 exit /b 1 + +REM Full version = CurrentVersion.BuildNumber from versioninfo.json (e.g. 3.8.2.4) +set "VER=" +for /f "usebackq delims=" %%v in (`powershell -NoProfile -Command "(Get-Content -Raw 'versioninfo.json' | ConvertFrom-Json).StringFileInfo.FileVersion"`) do set "VER=%%v" +if not defined VER ( + echo Failed to read FileVersion from versioninfo.json + exit /b 1 +) + +set "OUT_GUI=Navis-%VER%.exe" +set "OUT_CLI=vpnclient-%VER%.exe" +set "DIST=dist\navis-release" + +echo Building Navis %VER% for Windows... echo Generating Windows icon resources... go run ./tools/mkico if errorlevel 1 exit /b 1 @@ -14,16 +31,59 @@ if errorlevel 1 ( goversioninfo -64 -icon assets\navis.ico -manifest assets\app.manifest -o cmd\vpnapp\resource.syso versioninfo.json if errorlevel 1 exit /b 1 -echo Building Navis GUI and CLI... -go build -ldflags="-H windowsgui -s -w" -trimpath -o Navis.exe ./cmd/vpnapp +echo Building GUI -^> %OUT_GUI% +go build -ldflags="-H windowsgui -s -w" -trimpath -o "%OUT_GUI%" ./cmd/vpnapp if errorlevel 1 exit /b 1 -go build -ldflags="-s -w" -trimpath -o vpnclient.exe ./cmd/vpnclient +echo Building CLI -^> %OUT_CLI% +go build -ldflags="-s -w" -trimpath -o "%OUT_CLI%" ./cmd/vpnclient if errorlevel 1 exit /b 1 +mkdir "%DIST%" 2>nul +copy /Y "%OUT_GUI%" "%DIST%\%OUT_GUI%" >nul +if errorlevel 1 exit /b 1 +copy /Y "%OUT_CLI%" "%DIST%\%OUT_CLI%" >nul +if errorlevel 1 exit /b 1 +REM Stable names for the update feed / installers (best-effort if file is locked) +call :copy_alias "%OUT_GUI%" "%DIST%\Navis.exe" +call :copy_alias "%OUT_CLI%" "%DIST%\vpnclient.exe" + echo. echo Done: -echo Navis.exe -echo vpnclient.exe +echo %OUT_GUI% +echo %OUT_CLI% +echo %DIST%\%OUT_GUI% +echo %DIST%\%OUT_CLI% +echo %DIST%\Navis.exe (update alias) echo assets\navis.ico endlocal +exit /b 0 + +:bump_build +where py >nul 2>&1 +if not errorlevel 1 ( + py -3 scripts\bump-build.py + exit /b %ERRORLEVEL% +) +where python >nul 2>&1 +if not errorlevel 1 ( + python scripts\bump-build.py + exit /b %ERRORLEVEL% +) +where python3 >nul 2>&1 +if not errorlevel 1 ( + python3 scripts\bump-build.py + exit /b %ERRORLEVEL% +) +REM PowerShell fallback when Python is not installed +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\bump-build.ps1" +exit /b %ERRORLEVEL% + +:copy_alias +if exist "%~2" del /F /Q "%~2" >nul 2>&1 +copy /Y "%~1" "%~2" >nul 2>&1 +if errorlevel 1 ( + echo Warning: could not refresh %~2 ^(file in use^). Versioned build is still ready. + exit /b 0 +) +exit /b 0 diff --git a/cmd/vpnapp/main_darwin.go b/cmd/vpnapp/gui_desktop.go similarity index 83% rename from cmd/vpnapp/main_darwin.go rename to cmd/vpnapp/gui_desktop.go index 54a434f..9549f2d 100644 --- a/cmd/vpnapp/main_darwin.go +++ b/cmd/vpnapp/gui_desktop.go @@ -1,16 +1,13 @@ -//go:build darwin +//go:build windows || darwin package main import ( - "fmt" "log" "net/http" "os" - "os/exec" "os/signal" "runtime" - "syscall" "time" "github.com/crgimenes/glaze" @@ -28,6 +25,7 @@ func main() { return } update.CleanupStaleDownloads() + log.SetFlags(log.LstdFlags | log.Lshortfile) log.Printf("Navis %s · GOMAXPROCS=%d NumCPU=%d", update.DisplayVersion(), runtime.GOMAXPROCS(0), runtime.NumCPU()) @@ -44,6 +42,7 @@ func main() { if err != nil { fatalf("Ошибка конфига %s: %v", cfgPath, err) } + logBuf := logbuf.New(0) mgr, err := core.NewManager(cfgPath, cfg, logBuf) if err != nil { @@ -51,10 +50,9 @@ func main() { } a := apphost.New(mgr, cfgPath, logBuf) - a.OpenURL = func(u string) error { - return exec.Command("open", u).Start() - } + a.OpenURL = openExternal a.OnAfterUpdate = func() { + // Hard-exit so the OS unlocks the binary for replacement. go func() { time.Sleep(300 * time.Millisecond) _ = a.Disconnect() @@ -71,6 +69,7 @@ func main() { go func() { _ = srv.Serve(ln) }() + a.StartBackground() trayhost.Start("Navis", trayhost.Hooks{ Connect: func() { _ = a.Connect() }, @@ -86,12 +85,13 @@ func main() { w, err := glaze.New(false) if err != nil || w == nil { log.Printf("webview unavailable (%v); opening default browser", err) - if err2 := exec.Command("open", uiURL).Run(); err2 != nil { + if err2 := openExternal(uiURL); err2 != nil { + _ = srv.Close() fatalf("Не удалось открыть интерфейс:\n%v\n%v\n%s", err, err2, uiURL) } log.Printf("Navis UI: %s", uiURL) sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt, syscall.SIGTERM) + signal.Notify(sig, shutdownSignals()...) <-sig _ = mgr.Disconnect() _ = srv.Close() @@ -105,14 +105,8 @@ func main() { w.SetTitle("Navis") w.SetSize(500, 900, glaze.HintNone) + decorateWindow(w) w.Navigate(uiURL) log.Printf("Navis UI: %s", uiURL) w.Run() } - -func fatalf(format string, args ...any) { - msg := fmt.Sprintf(format, args...) - log.Println(msg) - _ = exec.Command("osascript", "-e", fmt.Sprintf(`display alert "Navis" message %q`, msg)).Run() - os.Exit(1) -} diff --git a/cmd/vpnapp/main_other.go b/cmd/vpnapp/main_other.go index 66ae7be..abddebb 100644 --- a/cmd/vpnapp/main_other.go +++ b/cmd/vpnapp/main_other.go @@ -8,7 +8,7 @@ import ( ) func main() { - fmt.Fprintln(os.Stderr, "Navis GUI supports Windows (WebView2) and macOS.") + fmt.Fprintln(os.Stderr, "Navis GUI supports Windows and macOS (glaze + local HTTP UI).") fmt.Fprintln(os.Stderr, "On this OS use the CLI: ./Navis connect | install-core | check-update") os.Exit(1) } diff --git a/cmd/vpnapp/main_windows.go b/cmd/vpnapp/main_windows.go deleted file mode 100644 index 8bab6c0..0000000 --- a/cmd/vpnapp/main_windows.go +++ /dev/null @@ -1,202 +0,0 @@ -//go:build windows - -package main - -import ( - "fmt" - "log" - "os" - "path/filepath" - "runtime" - "time" - "unsafe" - - "github.com/jchv/go-webview2" - "golang.org/x/sys/windows" - - "vpnclient/internal/apphost" - "vpnclient/internal/appui" - "vpnclient/internal/config" - "vpnclient/internal/core" - "vpnclient/internal/logbuf" - "vpnclient/internal/trayhost" - "vpnclient/internal/update" -) - -func main() { - if update.MaybeFinishUpdate(os.Args[1:]) { - return - } - update.CleanupStaleDownloads() - - log.SetFlags(log.LstdFlags | log.Lshortfile) - log.Printf("Navis %s · GOMAXPROCS=%d NumCPU=%d", update.DisplayVersion(), runtime.GOMAXPROCS(0), runtime.NumCPU()) - - 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 := logbuf.New(0) - mgr, err := core.NewManager(cfgPath, cfg, logBuf) - if err != nil { - fatalDialog("%v", err) - } - - a := apphost.New(mgr, cfgPath, logBuf) - a.OpenURL = shellOpen - a.OnAfterUpdate = func() { - // Hard-exit so Windows unlocks Navis.exe; do not wait on webview.Terminate. - go func() { - time.Sleep(300 * time.Millisecond) - _ = a.Disconnect() - os.Exit(0) - }() - } - - dataPath := filepath.Join(filepath.Dir(cfgPath), ".navis-webview") - _ = os.MkdirAll(dataPath, 0o755) - - w := webview2.NewWithOptions(webview2.WebViewOptions{ - Debug: false, - AutoFocus: true, - DataPath: dataPath, - WindowOptions: webview2.WindowOptions{ - Title: "Navis", - Width: 500, - Height: 900, - Center: true, - IconId: 1, - }, - }) - if w == nil { - fatalDialog("Не удалось создать окно WebView2.\nУстановите Microsoft Edge WebView2 Runtime.") - } - defer func() { - _ = mgr.Disconnect() - w.Destroy() - }() - - applyWindowIcon(w.Window()) - w.SetSize(500, 900, webview2.HintNone) - - mustBind(w, "getState", a.GetState) - mustBind(w, "getEditState", a.GetEditState) - mustBind(w, "connect", a.Connect) - mustBind(w, "disconnect", a.Disconnect) - mustBind(w, "connectProfile", a.ConnectProfile) - 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", a.OpenShopURL) - mustBind(w, "pingServers", a.PingServers) - mustBind(w, "pingBest", a.PingBest) - mustBind(w, "checkUpdate", a.CheckUpdate) - mustBind(w, "applyUpdate", a.ApplyUpdate) - mustBind(w, "saveHy2", a.SaveHy2) - mustBind(w, "importSubscription", a.ImportSubscription) - mustBind(w, "getLogs", a.GetLogs) - mustBind(w, "probeTunnel", a.ProbeTunnel) - mustBind(w, "savePrefs", a.SavePrefs) - - a.StartBackground() - trayhost.Start("Navis", trayhost.Hooks{ - Connect: func() { _ = a.Connect() }, - Disconnect: func() { _ = a.Disconnect() }, - Quit: func() { - _ = a.Disconnect() - os.Exit(0) - }, - IsUp: func() bool { return a.Mgr.Status().Connected }, - }) - - 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 shellOpen(raw string) error { - verb, err := windows.UTF16PtrFromString("open") - if err != nil { - return err - } - file, err := windows.UTF16PtrFromString(raw) - if err != nil { - return err - } - return windows.ShellExecute(0, verb, file, nil, nil, windows.SW_SHOWNORMAL) -} - -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) -} diff --git a/cmd/vpnapp/platform_darwin.go b/cmd/vpnapp/platform_darwin.go new file mode 100644 index 0000000..b8dc3fa --- /dev/null +++ b/cmd/vpnapp/platform_darwin.go @@ -0,0 +1,33 @@ +//go:build darwin + +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + "syscall" + + "github.com/crgimenes/glaze" +) + +func openExternal(raw string) error { + return exec.Command("open", raw).Start() +} + +func fatalf(format string, args ...any) { + msg := fmt.Sprintf(format, args...) + log.Println(msg) + _ = exec.Command("osascript", "-e", fmt.Sprintf(`display alert "Navis" message %q`, msg)).Run() + os.Exit(1) +} + +func shutdownSignals() []os.Signal { + return []os.Signal{os.Interrupt, syscall.SIGTERM} +} + +func decorateWindow(w glaze.WebView) { + // macOS Dock/app icon comes from the .app bundle; nothing to apply here. + _ = w +} diff --git a/cmd/vpnapp/platform_windows.go b/cmd/vpnapp/platform_windows.go new file mode 100644 index 0000000..2cfb7a9 --- /dev/null +++ b/cmd/vpnapp/platform_windows.go @@ -0,0 +1,92 @@ +//go:build windows + +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + "unsafe" + + "github.com/crgimenes/glaze" + "golang.org/x/sys/windows" +) + +func openExternal(raw string) error { + verb, err := windows.UTF16PtrFromString("open") + if err != nil { + return err + } + file, err := windows.UTF16PtrFromString(raw) + if err != nil { + return err + } + return windows.ShellExecute(0, verb, file, nil, nil, windows.SW_SHOWNORMAL) +} + +func fatalf(format string, args ...any) { + msg := fmt.Sprintf(format, args...) + log.Println(msg) + messageBox(msg) + os.Exit(1) +} + +func shutdownSignals() []os.Signal { + return []os.Signal{os.Interrupt} +} + +func decorateWindow(w glaze.WebView) { + if w == nil { + return + } + applyWindowIcon(w.Window()) +} + +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 "" +} diff --git a/dist/navis-release/Navis-3.8.2.4.exe b/dist/navis-release/Navis-3.8.2.4.exe new file mode 100644 index 0000000..45601a1 Binary files /dev/null and b/dist/navis-release/Navis-3.8.2.4.exe differ diff --git a/dist/navis-release/Navis.exe b/dist/navis-release/Navis.exe index b72117e..45601a1 100644 Binary files a/dist/navis-release/Navis.exe and b/dist/navis-release/Navis.exe differ diff --git a/dist/navis-release/update.json b/dist/navis-release/update.json index 6d1efe5..fda73b7 100644 --- a/dist/navis-release/update.json +++ b/dist/navis-release/update.json @@ -5,12 +5,12 @@ "os": "windows", "arch": "amd64", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", - "sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e", + "sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9", "mandatory": false, "platforms": { "windows-amd64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", - "sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e", + "sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9", "os": "windows", "arch": "amd64" }, diff --git a/dist/navis-release/vpnclient-3.8.2.4.exe b/dist/navis-release/vpnclient-3.8.2.4.exe new file mode 100644 index 0000000..459f29f Binary files /dev/null and b/dist/navis-release/vpnclient-3.8.2.4.exe differ diff --git a/dist/navis-release/vpnclient.exe b/dist/navis-release/vpnclient.exe index 00eb4d6..459f29f 100644 Binary files a/dist/navis-release/vpnclient.exe and b/dist/navis-release/vpnclient.exe differ diff --git a/dist/update.json b/dist/update.json index 6d1efe5..fda73b7 100644 --- a/dist/update.json +++ b/dist/update.json @@ -5,12 +5,12 @@ "os": "windows", "arch": "amd64", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", - "sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e", + "sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9", "mandatory": false, "platforms": { "windows-amd64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", - "sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e", + "sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9", "os": "windows", "arch": "amd64" }, diff --git a/go.mod b/go.mod index f896ae2..38781a2 100644 --- a/go.mod +++ b/go.mod @@ -4,21 +4,19 @@ go 1.26.5 require ( github.com/amnezia-vpn/amneziawg-go v0.2.19 + github.com/crgimenes/glaze v0.0.33 github.com/diskfs/go-diskfs v1.9.4 - github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808 + github.com/ebitengine/purego v0.10.1 golang.org/x/image v0.44.0 golang.org/x/sys v0.43.0 ) require ( github.com/anchore/go-lzo v0.1.0 // indirect - github.com/crgimenes/glaze v0.0.33 // indirect github.com/djherbis/times v1.6.0 // indirect - github.com/ebitengine/purego v0.10.1 // indirect github.com/elliotwutingfeng/asciiset v0.0.0-20260129054604-cfde2086bc57 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pkg/xattr v0.4.12 // indirect diff --git a/go.sum b/go.sum index 77ce956..ee5a5dc 100644 --- a/go.sum +++ b/go.sum @@ -22,10 +22,6 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808 h1:ftnsTqIUH57XQEF+PnXX9++nlHCzdkuB5zbWyMMruZo= -github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808/go.mod h1:rWifBlzkgrvd7zUqlfq91sWt3473OikgnglnIILx/Jo= -github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= -github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= @@ -48,8 +44,6 @@ golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= -golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= diff --git a/internal/apphost/handler_test.go b/internal/apphost/handler_test.go new file mode 100644 index 0000000..6ceb0fc --- /dev/null +++ b/internal/apphost/handler_test.go @@ -0,0 +1,68 @@ +package apphost + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "vpnclient/internal/config" + "vpnclient/internal/core" + "vpnclient/internal/logbuf" +) + +func TestHTTPBridgeGetState(t *testing.T) { + cfg := &config.Config{ + Active: "demo", + Profiles: []config.Profile{ + {Name: "demo", Protocol: config.ProtocolNaive, Proxy: "https://u:p@example.com"}, + }, + } + mgr, err := core.NewManager("", cfg, logbuf.New(0)) + if err != nil { + t.Fatal(err) + } + a := New(mgr, "", logbuf.New(0)) + a.APIToken = "test-token" + + body, _ := json.Marshal(map[string]any{"args": []any{}}) + req := httptest.NewRequest(http.MethodPost, "/api/getState", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Navis-Token", "test-token") + rr := httptest.NewRecorder() + a.Handler().ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("status %d body %s", rr.Code, rr.Body.String()) + } + var resp struct { + Result UIState `json:"result"` + Error string `json:"error"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatal(err) + } + if resp.Error != "" { + t.Fatalf("api error: %s", resp.Error) + } + if resp.Result.ActiveProfile != "demo" { + t.Fatalf("active_profile=%q", resp.Result.ActiveProfile) + } +} + +func TestHTTPBridgeRejectsBadToken(t *testing.T) { + cfg := &config.Config{} + mgr, err := core.NewManager("", cfg, logbuf.New(0)) + if err != nil { + t.Fatal(err) + } + a := New(mgr, "", logbuf.New(0)) + a.APIToken = "secret" + req := httptest.NewRequest(http.MethodPost, "/api/getState", bytes.NewReader([]byte(`{"args":[]}`))) + req.Header.Set("X-Navis-Token", "wrong") + rr := httptest.NewRecorder() + a.Handler().ServeHTTP(rr, req) + if rr.Code != http.StatusUnauthorized { + t.Fatalf("want 401, got %d", rr.Code) + } +} diff --git a/internal/appui/embed.go b/internal/appui/embed.go index 2b1b215..8463cd0 100644 --- a/internal/appui/embed.go +++ b/internal/appui/embed.go @@ -14,7 +14,7 @@ var appCSS string //go:embed app.js var appJS string -// IndexHTML is the full GUI document (CSS/JS inlined for WebView2 / glaze). +// IndexHTML is the full GUI document (CSS/JS inlined for glaze HTTP UI). var IndexHTML = buildIndexHTML() func buildIndexHTML() string { diff --git a/internal/update/update.go b/internal/update/update.go index 3af6017..deac06e 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -22,7 +22,7 @@ const CurrentVersion = "3.8.2" // BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part, // macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build. -const BuildNumber = 3 +const BuildNumber = 4 // DefaultManifestURL is the update feed (hosted in the project git repo). const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json" diff --git a/scripts/build-macos-arm64.sh b/scripts/build-macos-arm64.sh index f353299..99fe4ac 100755 --- a/scripts/build-macos-arm64.sh +++ b/scripts/build-macos-arm64.sh @@ -6,7 +6,7 @@ cd "$(dirname "$0")/.." # Prefer local toolchain if present. export PATH="$(pwd)/.tools/go/bin:/tmp/go-sdk/go/bin:/usr/local/go/bin:$PATH" -python3 scripts/sync-version.py +python3 scripts/bump-build.py VERSION="$(python3 - <<'PY' import re diff --git a/scripts/bump-build.ps1 b/scripts/bump-build.ps1 new file mode 100644 index 0000000..a1cb740 --- /dev/null +++ b/scripts/bump-build.ps1 @@ -0,0 +1,59 @@ +# Increment BuildNumber in update.go and sync version artifacts (no Python required). +$ErrorActionPreference = "Stop" +$Root = Split-Path -Parent $PSScriptRoot + +$updateGo = Join-Path $Root "internal\update\update.go" +$text = [IO.File]::ReadAllText($updateGo) +$m = [regex]::Match($text, 'const BuildNumber\s*=\s*(\d+)') +if (-not $m.Success) { throw "cannot find BuildNumber in update.go" } +$old = [int]$m.Groups[1].Value +$new = $old + 1 +$text2 = [regex]::Replace($text, 'const BuildNumber\s*=\s*\d+', "const BuildNumber = $new", 1) +[IO.File]::WriteAllText($updateGo, $text2) +Write-Host "BuildNumber: $old -> $new" + +$verM = [regex]::Match($text2, 'CurrentVersion\s*=\s*"([^"]+)"') +if (-not $verM.Success) { throw "cannot find CurrentVersion" } +$ver = $verM.Groups[1].Value +$parts = @($ver.Split('.') | ForEach-Object { [int]$_ }) +while ($parts.Count -lt 3) { $parts += 0 } +$major, $minor, $patch = $parts[0], $parts[1], $parts[2] +$full = "$ver.$new" +$display = "$ver+$new" +$versionCode = $major * 1000000 + $minor * 10000 + $patch * 100 + $new + +$viPath = Join-Path $Root "versioninfo.json" +$vi = [IO.File]::ReadAllText($viPath) +$vi = [regex]::Replace($vi, '("FileVersion"\s*:\s*\{\s*"Major"\s*:\s*)\d+', "`${1}$major", 1) +$vi = [regex]::Replace($vi, '("Minor"\s*:\s*)\d+(\s*,\s*"Patch")', "`${1}$minor`${2}", 1) +$vi = [regex]::Replace($vi, '("Patch"\s*:\s*)\d+(\s*,\s*"Build")', "`${1}$patch`${2}", 1) +$vi = [regex]::Replace($vi, '("Build"\s*:\s*)\d+', "`${1}$new", 2) +$vi = [regex]::Replace($vi, '("FileVersion"\s*:\s*")\d+\.\d+\.\d+\.\d+(")', "`${1}$full`${2}") +$vi = [regex]::Replace($vi, '("ProductVersion"\s*:\s*")\d+\.\d+\.\d+\.\d+(")', "`${1}$full`${2}") +[IO.File]::WriteAllText($viPath, $vi) +Write-Host "updated versioninfo.json -> $full" + +$batPath = Join-Path $Root "build-macos.bat" +$bat = [IO.File]::ReadAllText($batPath) +$bat2 = [regex]::Replace( + $bat, + '-version\s+\d+\.\d+\.\d+\s+-build\s+\d+\.\d+\.\d+\.\d+', + "-version $ver -build $full" +) +if ($bat2 -ne $bat) { + [IO.File]::WriteAllText($batPath, $bat2) + Write-Host "updated build-macos.bat -> $ver $full" +} + +$gradlePath = Join-Path $Root "android\app\build.gradle.kts" +if (Test-Path $gradlePath) { + $g = [IO.File]::ReadAllText($gradlePath) + $g2 = [regex]::Replace($g, 'versionCode\s*=\s*[\d_]+', "versionCode = $versionCode") + $g2 = [regex]::Replace($g2, 'versionName\s*=\s*"[^"]+"', "versionName = `"$display`"") + if ($g2 -ne $g) { + [IO.File]::WriteAllText($gradlePath, $g2) + Write-Host "updated android gradle -> $display $versionCode" + } +} + +Write-Host "bump complete: $full" diff --git a/scripts/bump-build.py b/scripts/bump-build.py new file mode 100644 index 0000000..eb648a3 --- /dev/null +++ b/scripts/bump-build.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Increment BuildNumber in update.go, then sync versioninfo / macOS bat / Android.""" +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +UPDATE_GO = ROOT / "internal" / "update" / "update.go" + + +def bump() -> int: + text = UPDATE_GO.read_text(encoding="utf-8") + m = re.search(r"const BuildNumber\s*=\s*(\d+)", text) + if not m: + raise SystemExit("cannot find BuildNumber in update.go") + n = int(m.group(1)) + 1 + text2, count = re.subn( + r"const BuildNumber\s*=\s*\d+", + f"const BuildNumber = {n}", + text, + count=1, + ) + if count != 1: + raise SystemExit("failed to rewrite BuildNumber") + UPDATE_GO.write_text(text2, encoding="utf-8") + print(f"BuildNumber: {m.group(1)} -> {n}", flush=True) + return n + + +def main() -> None: + bump() + sync = ROOT / "scripts" / "sync-version.py" + r = subprocess.run([sys.executable, str(sync)], cwd=ROOT, check=False) + if r.returncode != 0: + raise SystemExit(r.returncode) + + +if __name__ == "__main__": + main() diff --git a/server/update.json b/server/update.json index 6d1efe5..fda73b7 100644 --- a/server/update.json +++ b/server/update.json @@ -5,12 +5,12 @@ "os": "windows", "arch": "amd64", "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", - "sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e", + "sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9", "mandatory": false, "platforms": { "windows-amd64": { "url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe", - "sha256": "9a64a3b059c53eb818ff696d5f446c39f52e243508f92d05365dd06a05c57a1e", + "sha256": "d3c1ff19f14de16768680770391b9d5565e13508890f48f0c19e087ada94e9d9", "os": "windows", "arch": "amd64" }, diff --git a/versioninfo.json b/versioninfo.json index c824a9e..6a6e18c 100644 --- a/versioninfo.json +++ b/versioninfo.json @@ -4,13 +4,13 @@ "Major": 3, "Minor": 8, "Patch": 2, - "Build": 3 + "Build": 4 }, "ProductVersion": { "Major": 3, "Minor": 8, "Patch": 2, - "Build": 3 + "Build": 4 }, "FileFlagsMask": "3f", "FileFlags": "00", @@ -21,12 +21,12 @@ "StringFileInfo": { "CompanyName": "EvilFox", "FileDescription": "Navis — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)", - "FileVersion": "3.8.2.3", + "FileVersion": "3.8.2.4", "InternalName": "Navis", "LegalCopyright": "Copyright (c) EvilFox", "OriginalFilename": "Navis.exe", "ProductName": "Navis", - "ProductVersion": "3.8.2.3", + "ProductVersion": "3.8.2.4", "Comments": "Open-source VPN/proxy client. https://evilfox.win/" }, "VarFileInfo": {