Release 2.7.0: macOS GUI app with the same UI as Windows.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,8 +7,8 @@
|
|||||||
| OS | Артефакты | UI |
|
| OS | Артефакты | UI |
|
||||||
|----|----------|----|
|
|----|----------|----|
|
||||||
| Windows amd64 | `Navis.exe` | GUI (WebView2) |
|
| Windows amd64 | `Navis.exe` | GUI (WebView2) |
|
||||||
| macOS universal | `Navis.dmg` · `Navis.app.zip` | CLI / .app — **рекомендуется** |
|
| macOS universal | `Navis.dmg` · `Navis.app.zip` | **GUI** (как Windows) |
|
||||||
| macOS arm64 | `Navis` · `Navis.dmg` · `Navis.app.zip` | CLI / .app |
|
| macOS arm64 / amd64 | `Navis` · DMG · `Navis-cli` | GUI + CLI |
|
||||||
| Android | `Navis.apk` | GUI (VpnService) |
|
| Android | `Navis.apk` | GUI (VpnService) |
|
||||||
|
|
||||||
## Скачать macOS прямо с git
|
## Скачать macOS прямо с git
|
||||||
@@ -243,6 +243,9 @@ https://evilfox.win/
|
|||||||
|
|
||||||
Некоторые AV (Bkav, Microsoft Wacapew/Wacatac, Ikarus Trojan.WinGo.Agent, Google, Trapmine) часто помечают **любые** неподписанные Go-бинарники с сетью и сменой системного прокси.
|
Некоторые AV (Bkav, Microsoft Wacapew/Wacatac, Ikarus Trojan.WinGo.Agent, Google, Trapmine) часто помечают **любые** неподписанные Go-бинарники с сетью и сменой системного прокси.
|
||||||
|
|
||||||
|
В 2.7.0:
|
||||||
|
- GUI на macOS (тот же интерфейс, что на Windows): DMG → Navis.app открывает окно клиента; CLI лежит как `Navis-cli`.
|
||||||
|
|
||||||
В 2.6.2:
|
В 2.6.2:
|
||||||
- быстрое отключение AWG: принудительно закрываются прокси-соединения, UI больше не зависает.
|
- быстрое отключение AWG: принудительно закрываются прокси-соединения, UI больше не зависает.
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "win.evilfox.navis"
|
applicationId = "win.evilfox.navis"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 261
|
versionCode = 270
|
||||||
versionName = "2.6.1"
|
versionName = "2.7.0"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
|
||||||
|
|||||||
+12
-6
@@ -9,11 +9,15 @@ mkdir "dist\navis-release\darwin-universal" 2>nul
|
|||||||
set CGO_ENABLED=0
|
set CGO_ENABLED=0
|
||||||
set GOOS=darwin
|
set GOOS=darwin
|
||||||
set GOARCH=arm64
|
set GOARCH=arm64
|
||||||
go build -ldflags="-s -w" -o "dist\navis-release\darwin-arm64\Navis" .\cmd\vpnclient
|
go build -ldflags="-s -w" -o "dist\navis-release\darwin-arm64\Navis" .\cmd\vpnapp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
go build -ldflags="-s -w" -o "dist\navis-release\darwin-arm64\Navis-cli" .\cmd\vpnclient
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
set GOARCH=amd64
|
set GOARCH=amd64
|
||||||
go build -ldflags="-s -w" -o "dist\navis-release\darwin-amd64\Navis" .\cmd\vpnclient
|
go build -ldflags="-s -w" -o "dist\navis-release\darwin-amd64\Navis" .\cmd\vpnapp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
go build -ldflags="-s -w" -o "dist\navis-release\darwin-amd64\Navis-cli" .\cmd\vpnclient
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
set GOOS=
|
set GOOS=
|
||||||
@@ -24,18 +28,20 @@ go build -o "tools\mklipo\mklipo.exe" .\tools\mklipo
|
|||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
tools\mklipo\mklipo.exe "dist\navis-release\darwin-amd64\Navis" "dist\navis-release\darwin-arm64\Navis" "dist\navis-release\darwin-universal\Navis"
|
tools\mklipo\mklipo.exe "dist\navis-release\darwin-amd64\Navis" "dist\navis-release\darwin-arm64\Navis" "dist\navis-release\darwin-universal\Navis"
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
tools\mklipo\mklipo.exe "dist\navis-release\darwin-amd64\Navis-cli" "dist\navis-release\darwin-arm64\Navis-cli" "dist\navis-release\darwin-universal\Navis-cli"
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
go build -o "tools\packmac\packmac.exe" .\tools\packmac
|
go build -o "tools\packmac\packmac.exe" .\tools\packmac
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 2.6.2 -arch arm64
|
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 2.7.0 -arch arm64
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 2.6.2 -arch amd64
|
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 2.7.0 -arch amd64
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 2.6.2 -arch universal
|
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 2.7.0 -arch universal
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
echo Built:
|
echo Built Mac GUI + CLI:
|
||||||
dir "dist\navis-release\darwin-universal"
|
dir "dist\navis-release\darwin-universal"
|
||||||
dir "dist\navis-release\darwin-arm64"
|
dir "dist\navis-release\darwin-arm64"
|
||||||
dir "dist\navis-release\darwin-amd64"
|
dir "dist\navis-release\darwin-amd64"
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"vpnclient/internal/apphost"
|
||||||
|
"vpnclient/internal/config"
|
||||||
|
"vpnclient/internal/core"
|
||||||
|
"vpnclient/internal/update"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if update.MaybeFinishUpdate(os.Args[1:]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
update.CleanupStaleDownloads()
|
||||||
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
|
||||||
|
cfgPath, err := config.LocateConfig()
|
||||||
|
if err != nil {
|
||||||
|
fatalf("Не удалось найти путь конфига: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
|
||||||
|
if err := config.WriteExample(cfgPath); err != nil {
|
||||||
|
fatalf("Не удалось создать конфиг: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg, err := config.Load(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("Ошибка конфига %s: %v", cfgPath, err)
|
||||||
|
}
|
||||||
|
logBuf := &bytes.Buffer{}
|
||||||
|
mgr, err := core.NewManager(cfgPath, cfg, logBuf)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a := apphost.New(mgr, cfgPath, logBuf)
|
||||||
|
a.OpenURL = func(u string) error {
|
||||||
|
return exec.Command("open", u).Start()
|
||||||
|
}
|
||||||
|
a.OnAfterUpdate = func() {
|
||||||
|
go func() {
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, uiURL, err := apphost.ListenLocal()
|
||||||
|
if err != nil {
|
||||||
|
fatalf("listen: %v", err)
|
||||||
|
}
|
||||||
|
srv := &http.Server{Handler: a.Handler()}
|
||||||
|
go func() {
|
||||||
|
_ = srv.Serve(ln)
|
||||||
|
}()
|
||||||
|
go a.AutoCheckUpdate()
|
||||||
|
|
||||||
|
if err := openAppWindow(uiURL); err != nil {
|
||||||
|
log.Printf("open UI: %v — откройте вручную: %s", err, uiURL)
|
||||||
|
fmt.Println(uiURL)
|
||||||
|
} else {
|
||||||
|
log.Printf("Navis UI: %s", uiURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-sig
|
||||||
|
_ = mgr.Disconnect()
|
||||||
|
_ = srv.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func openAppWindow(uiURL string) error {
|
||||||
|
// Prefer Chromium-based --app window (looks like a desktop client).
|
||||||
|
for _, app := range []string{"Google Chrome", "Chromium", "Microsoft Edge", "Brave Browser", "Arc"} {
|
||||||
|
cmd := exec.Command("open", "-na", app, "--args", "--app="+uiURL, "--new-window")
|
||||||
|
if err := cmd.Start(); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: default browser
|
||||||
|
return exec.Command("open", uiURL).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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we keep a reference for potential future dock icon path helpers.
|
||||||
|
var _ = filepath.Separator
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !windows
|
//go:build !windows && !darwin
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Fprintln(os.Stderr, "Navis GUI (WebView2) is Windows-only.")
|
fmt.Fprintln(os.Stderr, "Navis GUI supports Windows (WebView2) and macOS.")
|
||||||
fmt.Fprintln(os.Stderr, "On macOS use the CLI binary: ./Navis connect | install-core | check-update")
|
fmt.Fprintln(os.Stderr, "On this OS use the CLI: ./Navis connect | install-core | check-update")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Vendored
+7
-7
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"version": "2.6.2",
|
"version": "2.7.0",
|
||||||
"notes": "Быстрое отключение AWG без зависания UI («приложение не отвечает»)",
|
"notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
|
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"windows-amd64": {
|
"windows-amd64": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
|
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
},
|
},
|
||||||
"darwin-arm64": {
|
"darwin-arm64": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
||||||
"sha256": "497cdf66b74e28061622debbbc6fa922ae64ae532e867dec876188af15e85278",
|
"sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
|
||||||
"os": "darwin",
|
"os": "darwin",
|
||||||
"arch": "arm64",
|
"arch": "arm64",
|
||||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
|
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
},
|
},
|
||||||
"darwin-amd64": {
|
"darwin-amd64": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
||||||
"sha256": "95ccb649116bd15cbcdddccdd57df77651bf5f41ec19a4c6a0e91f8139621df7",
|
"sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
|
||||||
"os": "darwin",
|
"os": "darwin",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
|
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
},
|
},
|
||||||
"darwin-universal": {
|
"darwin-universal": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
|
||||||
"sha256": "abdb4d5807145dfab7afa1fc9fd4d0382a4f5d9b4ce7621128b2b393c7f0c928",
|
"sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
|
||||||
"os": "darwin",
|
"os": "darwin",
|
||||||
"arch": "universal",
|
"arch": "universal",
|
||||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
|
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
|
||||||
|
|||||||
Vendored
+7
-7
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"version": "2.6.2",
|
"version": "2.7.0",
|
||||||
"notes": "Быстрое отключение AWG без зависания UI («приложение не отвечает»)",
|
"notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
|
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"windows-amd64": {
|
"windows-amd64": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
|
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
},
|
},
|
||||||
"darwin-arm64": {
|
"darwin-arm64": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
||||||
"sha256": "497cdf66b74e28061622debbbc6fa922ae64ae532e867dec876188af15e85278",
|
"sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
|
||||||
"os": "darwin",
|
"os": "darwin",
|
||||||
"arch": "arm64",
|
"arch": "arm64",
|
||||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
|
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
},
|
},
|
||||||
"darwin-amd64": {
|
"darwin-amd64": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
||||||
"sha256": "95ccb649116bd15cbcdddccdd57df77651bf5f41ec19a4c6a0e91f8139621df7",
|
"sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
|
||||||
"os": "darwin",
|
"os": "darwin",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
|
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
},
|
},
|
||||||
"darwin-universal": {
|
"darwin-universal": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
|
||||||
"sha256": "abdb4d5807145dfab7afa1fc9fd4d0382a4f5d9b4ce7621128b2b393c7f0c928",
|
"sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
|
||||||
"os": "darwin",
|
"os": "darwin",
|
||||||
"arch": "universal",
|
"arch": "universal",
|
||||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
|
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
|
||||||
|
|||||||
@@ -0,0 +1,488 @@
|
|||||||
|
package apphost
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"vpnclient/internal/appui"
|
||||||
|
"vpnclient/internal/config"
|
||||||
|
"vpnclient/internal/core"
|
||||||
|
"vpnclient/internal/netcheck"
|
||||||
|
"vpnclient/internal/protocols/awg"
|
||||||
|
"vpnclient/internal/protocols/hysteria2"
|
||||||
|
"vpnclient/internal/protocols/naive"
|
||||||
|
"vpnclient/internal/protocols/xray"
|
||||||
|
"vpnclient/internal/update"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App is the GUI controller shared by Windows WebView and macOS HTTP UI.
|
||||||
|
type App struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
Mgr *core.Manager
|
||||||
|
CfgPath string
|
||||||
|
LogBuf *bytes.Buffer
|
||||||
|
UpdateStatus update.Status
|
||||||
|
Pings []netcheck.Result
|
||||||
|
OpenURL func(string) error
|
||||||
|
OnAfterUpdate func()
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PingBestResult struct {
|
||||||
|
Pings []netcheck.Result `json:"pings"`
|
||||||
|
BestName string `json:"best_name,omitempty"`
|
||||||
|
BestMs int64 `json:"best_ms,omitempty"`
|
||||||
|
Selected bool `json:"selected"`
|
||||||
|
Connected bool `json:"connected"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(mgr *core.Manager, cfgPath string, logBuf *bytes.Buffer) *App {
|
||||||
|
return &App{
|
||||||
|
Mgr: mgr,
|
||||||
|
CfgPath: cfgPath,
|
||||||
|
LogBuf: logBuf,
|
||||||
|
UpdateStatus: update.Status{
|
||||||
|
Current: update.CurrentVersion,
|
||||||
|
ManifestURL: update.DefaultManifestURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
case config.ProtocolAWG:
|
||||||
|
if path, err := awg.ResolveBinary(a.Mgr.BinDir()); err == nil {
|
||||||
|
corePath, coreReady = path, true
|
||||||
|
}
|
||||||
|
case config.ProtocolVLESS, config.ProtocolVMess, config.ProtocolTrojan:
|
||||||
|
if path, err := xray.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 { return a.ConnectProfile("") }
|
||||||
|
|
||||||
|
func (a *App) ConnectProfile(name string) error {
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
st := a.Mgr.Status()
|
||||||
|
if st.Connected {
|
||||||
|
if name == "" || st.Profile == name {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := a.Mgr.Disconnect(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if name != "" {
|
||||||
|
if err := a.Mgr.SetActiveProfile(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
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) {
|
||||||
|
return a.runPings()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) PingBest(autoConnect bool) (PingBestResult, error) {
|
||||||
|
pings, err := a.runPings()
|
||||||
|
out := PingBestResult{Pings: pings}
|
||||||
|
if err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
best, ok := netcheck.BestOK(pings)
|
||||||
|
if !ok {
|
||||||
|
return out, fmt.Errorf("нет доступных серверов")
|
||||||
|
}
|
||||||
|
out.BestName = best.Name
|
||||||
|
out.BestMs = best.Ms
|
||||||
|
|
||||||
|
a.mu.Lock()
|
||||||
|
defer a.mu.Unlock()
|
||||||
|
st := a.Mgr.Status()
|
||||||
|
alreadyBest := st.Connected && st.Profile == best.Name
|
||||||
|
if st.Connected && !alreadyBest {
|
||||||
|
if err := a.Mgr.Disconnect(); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !alreadyBest {
|
||||||
|
if err := a.Mgr.SetActiveProfile(best.Name); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.Selected = true
|
||||||
|
if !autoConnect {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
if alreadyBest {
|
||||||
|
out.Connected = true
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
if p, err := a.Mgr.Config().ActiveProfile(); err != nil {
|
||||||
|
return out, err
|
||||||
|
} else if strings.TrimSpace(p.Proxy) == "" {
|
||||||
|
return out, fmt.Errorf("пустая ссылка у лучшего сервера")
|
||||||
|
}
|
||||||
|
if _, err := a.Mgr.EnsureCore(""); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := a.Mgr.Connect(ctx, ""); err != nil {
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
out.Connected = true
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) runPings() ([]netcheck.Result, error) {
|
||||||
|
a.mu.Lock()
|
||||||
|
list := a.Mgr.Profiles()
|
||||||
|
a.mu.Unlock()
|
||||||
|
|
||||||
|
targets := make([]netcheck.Target, 0, len(list))
|
||||||
|
for _, p := range list {
|
||||||
|
targets = append(targets, netcheck.Target{
|
||||||
|
Name: p.Name,
|
||||||
|
Protocol: config.Protocol(p.Protocol),
|
||||||
|
Proxy: p.Proxy,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
out := netcheck.PingAll(ctx, targets)
|
||||||
|
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() (string, 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()
|
||||||
|
if a.OnAfterUpdate != nil {
|
||||||
|
a.OnAfterUpdate()
|
||||||
|
} else {
|
||||||
|
go func() {
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
return "Устанавливается v" + latest + "… Navis перезапустится.", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) AutoCheckUpdate() {
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
st, err := a.CheckUpdate()
|
||||||
|
if err != nil || !st.Available {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(1500 * time.Millisecond)
|
||||||
|
_, _ = a.ApplyUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) OpenShopURL(raw string) error {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return fmt.Errorf("пустая ссылка")
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(raw)
|
||||||
|
switch {
|
||||||
|
case lower == "https://evilfox.win/" || lower == "https://evilfox.win" ||
|
||||||
|
lower == "http://evilfox.win/" || lower == "http://evilfox.win":
|
||||||
|
raw = "https://evilfox.win/"
|
||||||
|
case strings.HasPrefix(lower, "https://evilfox.win/") || strings.HasPrefix(lower, "http://evilfox.win/"):
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("разрешена только ссылка evilfox.win")
|
||||||
|
}
|
||||||
|
if a.OpenURL != nil {
|
||||||
|
return a.OpenURL(raw)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("openURL не настроен")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler serves embedded UI and JSON-RPC style /api/<method> endpoints.
|
||||||
|
func (a *App) Handler() http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
_, _ = io.WriteString(w, appui.IndexHTML)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/api/", a.handleAPI)
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) handleAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "POST only", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := strings.TrimPrefix(r.URL.Path, "/api/")
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20))
|
||||||
|
var req struct {
|
||||||
|
Args []json.RawMessage `json:"args"`
|
||||||
|
}
|
||||||
|
_ = json.Unmarshal(body, &req)
|
||||||
|
|
||||||
|
result, err := a.dispatch(name, req.Args)
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
if err != nil {
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"result": result})
|
||||||
|
}
|
||||||
|
|
||||||
|
func arg[T any](args []json.RawMessage, i int, def T) T {
|
||||||
|
if i >= len(args) {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
var v T
|
||||||
|
if err := json.Unmarshal(args[i], &v); err != nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) dispatch(name string, args []json.RawMessage) (any, error) {
|
||||||
|
switch name {
|
||||||
|
case "getState":
|
||||||
|
return a.GetState()
|
||||||
|
case "connect":
|
||||||
|
return nil, a.Connect()
|
||||||
|
case "disconnect":
|
||||||
|
return nil, a.Disconnect()
|
||||||
|
case "connectProfile":
|
||||||
|
return nil, a.ConnectProfile(arg(args, 0, ""))
|
||||||
|
case "saveProfile":
|
||||||
|
return nil, a.SaveProfile(arg(args, 0, ""), arg(args, 1, ""), arg(args, 2, true))
|
||||||
|
case "createProfile":
|
||||||
|
return nil, a.CreateProfile(arg(args, 0, ""), arg(args, 1, ""), arg(args, 2, true))
|
||||||
|
case "selectProfile":
|
||||||
|
return nil, a.SelectProfile(arg(args, 0, ""))
|
||||||
|
case "deleteProfile":
|
||||||
|
return nil, a.DeleteProfile(arg(args, 0, ""))
|
||||||
|
case "installCore":
|
||||||
|
return a.InstallCore()
|
||||||
|
case "openURL":
|
||||||
|
return nil, a.OpenShopURL(arg(args, 0, ""))
|
||||||
|
case "pingServers":
|
||||||
|
return a.PingServers()
|
||||||
|
case "pingBest":
|
||||||
|
return a.PingBest(arg(args, 0, false))
|
||||||
|
case "checkUpdate":
|
||||||
|
return a.CheckUpdate()
|
||||||
|
case "applyUpdate":
|
||||||
|
return a.ApplyUpdate()
|
||||||
|
case "saveHy2":
|
||||||
|
var opts core.Hy2Options
|
||||||
|
if len(args) > 0 {
|
||||||
|
_ = json.Unmarshal(args[0], &opts)
|
||||||
|
}
|
||||||
|
return nil, a.SaveHy2(opts)
|
||||||
|
case "importSubscription":
|
||||||
|
return a.ImportSubscription(arg(args, 0, ""))
|
||||||
|
case "quit":
|
||||||
|
go func() {
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
_ = a.Mgr.Disconnect()
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
return "ok", nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown method %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenLocal binds 127.0.0.1:0 and returns listener + URL.
|
||||||
|
func ListenLocal() (net.Listener, string, error) {
|
||||||
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("http://%s/", ln.Addr().String())
|
||||||
|
return ln, url, nil
|
||||||
|
}
|
||||||
@@ -725,7 +725,7 @@
|
|||||||
<div class="brand-wrap">
|
<div class="brand-wrap">
|
||||||
<div class="brand-row">
|
<div class="brand-row">
|
||||||
<h1 class="brand">Navis</h1>
|
<h1 class="brand">Navis</h1>
|
||||||
<span class="badge-ver">2.6.2</span>
|
<span class="badge-ver">2.7</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
|
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -866,6 +866,7 @@
|
|||||||
<button class="shop-link" id="shopLink" type="button">https://evilfox.win/</button>
|
<button class="shop-link" id="shopLink" type="button">https://evilfox.win/</button>
|
||||||
</section>
|
</section>
|
||||||
<p class="ver" id="verLabel">Navis</p>
|
<p class="ver" id="verLabel">Navis</p>
|
||||||
|
<button class="shop-link" id="quitBtn" type="button" hidden style="display:none;margin:8px auto 0;width:auto">Выйти</button>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div class="modal-backdrop" id="modal">
|
<div class="modal-backdrop" id="modal">
|
||||||
@@ -889,6 +890,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// HTTP bridge for macOS GUI (Windows WebView2 already binds these as natives).
|
||||||
|
(function () {
|
||||||
|
const methods = [
|
||||||
|
"getState","connect","disconnect","connectProfile","saveProfile","createProfile",
|
||||||
|
"selectProfile","deleteProfile","installCore","openURL","pingServers","pingBest",
|
||||||
|
"checkUpdate","applyUpdate","saveHy2","importSubscription","quit"
|
||||||
|
];
|
||||||
|
if (typeof window.getState === "function") return;
|
||||||
|
window.__navisHttp = true;
|
||||||
|
async function call(name, args) {
|
||||||
|
const r = await fetch("/api/" + name, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ args: args || [] })
|
||||||
|
});
|
||||||
|
const j = await r.json();
|
||||||
|
if (j && j.error) throw j.error;
|
||||||
|
return j ? j.result : null;
|
||||||
|
}
|
||||||
|
methods.forEach((name) => {
|
||||||
|
window[name] = function () {
|
||||||
|
return call(name, Array.prototype.slice.call(arguments));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})();
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
const meta = $("meta");
|
const meta = $("meta");
|
||||||
const btn = $("toggleBtn");
|
const btn = $("toggleBtn");
|
||||||
@@ -913,6 +939,17 @@
|
|||||||
const updateBanner = $("updateBanner");
|
const updateBanner = $("updateBanner");
|
||||||
const updateBtn = $("updateBtn");
|
const updateBtn = $("updateBtn");
|
||||||
const verLabel = $("verLabel");
|
const verLabel = $("verLabel");
|
||||||
|
const quitBtn = $("quitBtn");
|
||||||
|
if (window.__navisHttp && quitBtn) {
|
||||||
|
quitBtn.hidden = false;
|
||||||
|
quitBtn.style.display = "block";
|
||||||
|
quitBtn.addEventListener("click", () => {
|
||||||
|
withBusy(async () => {
|
||||||
|
setMeta("Выход…");
|
||||||
|
try { await quit(); } catch (_) {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
const subUrl = $("subUrl");
|
const subUrl = $("subUrl");
|
||||||
const subBtn = $("subBtn");
|
const subBtn = $("subBtn");
|
||||||
const hero = $("hero");
|
const hero = $("hero");
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// CurrentVersion is the shipped client version.
|
// CurrentVersion is the shipped client version.
|
||||||
const CurrentVersion = "2.6.2"
|
const CurrentVersion = "2.7.0"
|
||||||
|
|
||||||
// DefaultManifestURL is the update feed (hosted in the project git repo).
|
// 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"
|
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package update
|
||||||
|
|
||||||
|
// MaybeFinishUpdate is Windows-only (pending exe swap). No-op elsewhere.
|
||||||
|
func MaybeFinishUpdate(args []string) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
+12
-13
@@ -5,12 +5,8 @@ BASE="https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release"
|
|||||||
ARCH="$(uname -m)"
|
ARCH="$(uname -m)"
|
||||||
|
|
||||||
case "$ARCH" in
|
case "$ARCH" in
|
||||||
arm64)
|
arm64) SUBDIR="darwin-arm64" ;;
|
||||||
SUBDIR="darwin-arm64"
|
x86_64) SUBDIR="darwin-amd64" ;;
|
||||||
;;
|
|
||||||
x86_64)
|
|
||||||
SUBDIR="darwin-amd64"
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo "Неподдерживаемая архитектура: $ARCH" >&2
|
echo "Неподдерживаемая архитектура: $ARCH" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -21,14 +17,17 @@ DEST="${1:-$HOME/Downloads/Navis.dmg}"
|
|||||||
UNIVERSAL="$BASE/darwin-universal/Navis.dmg"
|
UNIVERSAL="$BASE/darwin-universal/Navis.dmg"
|
||||||
SPECIFIC="$BASE/$SUBDIR/Navis.dmg"
|
SPECIFIC="$BASE/$SUBDIR/Navis.dmg"
|
||||||
|
|
||||||
echo "Mac: $ARCH"
|
echo "Mac: $ARCH · ставим Navis GUI 2.7"
|
||||||
echo "Рекомендуется универсальный DMG (Intel + Apple Silicon)…"
|
echo "Скачиваю универсальный DMG…"
|
||||||
if curl -fL -o "$DEST" "$UNIVERSAL"; then
|
if ! curl -fL -o "$DEST" "$UNIVERSAL"; then
|
||||||
echo "Скачан: $DEST"
|
echo "Universal недоступен, скачиваю $SUBDIR…"
|
||||||
else
|
|
||||||
echo "Универсальный DMG недоступен, скачиваю $SUBDIR…"
|
|
||||||
curl -fL -o "$DEST" "$SPECIFIC"
|
curl -fL -o "$DEST" "$SPECIFIC"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
open "$DEST"
|
open "$DEST"
|
||||||
echo "Откройте диск и перетащите Navis.app в Программы."
|
echo ""
|
||||||
|
echo "1) Перетащите Navis.app в Программы"
|
||||||
|
echo "2) При блоке Gatekeeper:"
|
||||||
|
echo " xattr -dr com.apple.quarantine /Applications/Navis.app"
|
||||||
|
echo "3) Запуск: откройте Navis из Программ (откроется окно UI)"
|
||||||
|
echo " CLI рядом: Navis-cli (install-core / connect / check-update)"
|
||||||
|
|||||||
+7
-7
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"version": "2.6.2",
|
"version": "2.7.0",
|
||||||
"notes": "Быстрое отключение AWG без зависания UI («приложение не отвечает»)",
|
"notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
|
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"windows-amd64": {
|
"windows-amd64": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
|
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
},
|
},
|
||||||
"darwin-arm64": {
|
"darwin-arm64": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
||||||
"sha256": "497cdf66b74e28061622debbbc6fa922ae64ae532e867dec876188af15e85278",
|
"sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
|
||||||
"os": "darwin",
|
"os": "darwin",
|
||||||
"arch": "arm64",
|
"arch": "arm64",
|
||||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
|
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
},
|
},
|
||||||
"darwin-amd64": {
|
"darwin-amd64": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
||||||
"sha256": "95ccb649116bd15cbcdddccdd57df77651bf5f41ec19a4c6a0e91f8139621df7",
|
"sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
|
||||||
"os": "darwin",
|
"os": "darwin",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
|
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
},
|
},
|
||||||
"darwin-universal": {
|
"darwin-universal": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
|
||||||
"sha256": "abdb4d5807145dfab7afa1fc9fd4d0382a4f5d9b4ce7621128b2b393c7f0c928",
|
"sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
|
||||||
"os": "darwin",
|
"os": "darwin",
|
||||||
"arch": "universal",
|
"arch": "universal",
|
||||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
|
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis.dmg",
|
||||||
|
|||||||
@@ -368,17 +368,16 @@ func sanitizeVol(s string) string {
|
|||||||
func readmeText(version, arch string) string {
|
func readmeText(version, arch string) string {
|
||||||
switch arch {
|
switch arch {
|
||||||
case "universal":
|
case "universal":
|
||||||
return fmt.Sprintf(`Navis %s for macOS (универсальная сборка)
|
return fmt.Sprintf(`Navis %s for macOS (универсальная GUI-сборка)
|
||||||
|
|
||||||
Подходит для Apple Silicon (M1/M2/M3/M4) и Intel Mac.
|
Подходит для Apple Silicon (M1/M2/M3/M4) и Intel Mac.
|
||||||
|
|
||||||
1) Перетащите Navis.app в Программы (Applications)
|
1) Перетащите Navis.app в Программы (Applications)
|
||||||
2) При первом запуске, если macOS блокирует:
|
2) При блокировке:
|
||||||
xattr -dr com.apple.quarantine /Applications/Navis.app
|
xattr -dr com.apple.quarantine /Applications/Navis.app
|
||||||
3) В Терминале:
|
3) Запустите Navis — откроется окно как на Windows
|
||||||
/Applications/Navis.app/Contents/MacOS/Navis init
|
(Chrome/Edge/Brave в режиме приложения, иначе браузер)
|
||||||
/Applications/Navis.app/Contents/MacOS/Navis install-core
|
4) CLI (ядро/connect из терминала): рядом на git файл Navis-cli
|
||||||
/Applications/Navis.app/Contents/MacOS/Navis connect
|
|
||||||
|
|
||||||
Магазин: https://evilfox.win/
|
Магазин: https://evilfox.win/
|
||||||
`, version)
|
`, version)
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"FixedFileInfo": {
|
"FixedFileInfo": {
|
||||||
"FileVersion": { "Major": 2, "Minor": 6, "Patch": 2, "Build": 0 },
|
"FileVersion": { "Major": 2, "Minor": 7, "Patch": 0, "Build": 0 },
|
||||||
"ProductVersion": { "Major": 2, "Minor": 6, "Patch": 2, "Build": 0 },
|
"ProductVersion": { "Major": 2, "Minor": 7, "Patch": 0, "Build": 0 },
|
||||||
"FileFlagsMask": "3f",
|
"FileFlagsMask": "3f",
|
||||||
"FileFlags": "00",
|
"FileFlags": "00",
|
||||||
"FileOS": "40004",
|
"FileOS": "40004",
|
||||||
@@ -11,12 +11,12 @@
|
|||||||
"StringFileInfo": {
|
"StringFileInfo": {
|
||||||
"CompanyName": "EvilFox",
|
"CompanyName": "EvilFox",
|
||||||
"FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
"FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
||||||
"FileVersion": "2.6.2.0",
|
"FileVersion": "2.7.0.0",
|
||||||
"InternalName": "Navis",
|
"InternalName": "Navis",
|
||||||
"LegalCopyright": "Copyright (c) EvilFox",
|
"LegalCopyright": "Copyright (c) EvilFox",
|
||||||
"OriginalFilename": "Navis.exe",
|
"OriginalFilename": "Navis.exe",
|
||||||
"ProductName": "Navis",
|
"ProductName": "Navis",
|
||||||
"ProductVersion": "2.6.2.0",
|
"ProductVersion": "2.7.0.0",
|
||||||
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
||||||
},
|
},
|
||||||
"VarFileInfo": {
|
"VarFileInfo": {
|
||||||
|
|||||||
Reference in New Issue
Block a user