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 |
|
||||
|----|----------|----|
|
||||
| Windows amd64 | `Navis.exe` | GUI (WebView2) |
|
||||
| macOS universal | `Navis.dmg` · `Navis.app.zip` | CLI / .app — **рекомендуется** |
|
||||
| macOS arm64 | `Navis` · `Navis.dmg` · `Navis.app.zip` | CLI / .app |
|
||||
| macOS universal | `Navis.dmg` · `Navis.app.zip` | **GUI** (как Windows) |
|
||||
| macOS arm64 / amd64 | `Navis` · DMG · `Navis-cli` | GUI + CLI |
|
||||
| Android | `Navis.apk` | GUI (VpnService) |
|
||||
|
||||
## Скачать macOS прямо с git
|
||||
@@ -243,6 +243,9 @@ https://evilfox.win/
|
||||
|
||||
Некоторые 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:
|
||||
- быстрое отключение AWG: принудительно закрываются прокси-соединения, UI больше не зависает.
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "win.evilfox.navis"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 261
|
||||
versionName = "2.6.1"
|
||||
versionCode = 270
|
||||
versionName = "2.7.0"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
ndk {
|
||||
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 GOOS=darwin
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
set GOOS=
|
||||
@@ -24,18 +28,20 @@ go build -o "tools\mklipo\mklipo.exe" .\tools\mklipo
|
||||
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"
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
echo Built:
|
||||
echo Built Mac GUI + CLI:
|
||||
dir "dist\navis-release\darwin-universal"
|
||||
dir "dist\navis-release\darwin-arm64"
|
||||
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
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Fprintln(os.Stderr, "Navis GUI (WebView2) is Windows-only.")
|
||||
fmt.Fprintln(os.Stderr, "On macOS use the CLI binary: ./Navis connect | install-core | check-update")
|
||||
fmt.Fprintln(os.Stderr, "Navis GUI supports Windows (WebView2) and macOS.")
|
||||
fmt.Fprintln(os.Stderr, "On this OS use the CLI: ./Navis connect | install-core | check-update")
|
||||
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",
|
||||
"notes": "Быстрое отключение AWG без зависания UI («приложение не отвечает»)",
|
||||
"version": "2.7.0",
|
||||
"notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
|
||||
"platform": "windows-amd64",
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
|
||||
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
|
||||
"mandatory": false,
|
||||
"platforms": {
|
||||
"windows-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
|
||||
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
},
|
||||
"darwin-arm64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
||||
"sha256": "497cdf66b74e28061622debbbc6fa922ae64ae532e867dec876188af15e85278",
|
||||
"sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
|
||||
"os": "darwin",
|
||||
"arch": "arm64",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"darwin-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
||||
"sha256": "95ccb649116bd15cbcdddccdd57df77651bf5f41ec19a4c6a0e91f8139621df7",
|
||||
"sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
|
||||
"os": "darwin",
|
||||
"arch": "amd64",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"darwin-universal": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
|
||||
"sha256": "abdb4d5807145dfab7afa1fc9fd4d0382a4f5d9b4ce7621128b2b393c7f0c928",
|
||||
"sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
|
||||
"os": "darwin",
|
||||
"arch": "universal",
|
||||
"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",
|
||||
"notes": "Быстрое отключение AWG без зависания UI («приложение не отвечает»)",
|
||||
"version": "2.7.0",
|
||||
"notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
|
||||
"platform": "windows-amd64",
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
|
||||
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
|
||||
"mandatory": false,
|
||||
"platforms": {
|
||||
"windows-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
|
||||
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
},
|
||||
"darwin-arm64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
||||
"sha256": "497cdf66b74e28061622debbbc6fa922ae64ae532e867dec876188af15e85278",
|
||||
"sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
|
||||
"os": "darwin",
|
||||
"arch": "arm64",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"darwin-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
||||
"sha256": "95ccb649116bd15cbcdddccdd57df77651bf5f41ec19a4c6a0e91f8139621df7",
|
||||
"sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
|
||||
"os": "darwin",
|
||||
"arch": "amd64",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"darwin-universal": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
|
||||
"sha256": "abdb4d5807145dfab7afa1fc9fd4d0382a4f5d9b4ce7621128b2b393c7f0c928",
|
||||
"sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
|
||||
"os": "darwin",
|
||||
"arch": "universal",
|
||||
"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-row">
|
||||
<h1 class="brand">Navis</h1>
|
||||
<span class="badge-ver">2.6.2</span>
|
||||
<span class="badge-ver">2.7</span>
|
||||
</div>
|
||||
<p class="tagline">Быстрый клиент · Naive · Hy2 · AWG · Xray</p>
|
||||
</div>
|
||||
@@ -866,6 +866,7 @@
|
||||
<button class="shop-link" id="shopLink" type="button">https://evilfox.win/</button>
|
||||
</section>
|
||||
<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>
|
||||
|
||||
<div class="modal-backdrop" id="modal">
|
||||
@@ -889,6 +890,31 @@
|
||||
</div>
|
||||
|
||||
<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 meta = $("meta");
|
||||
const btn = $("toggleBtn");
|
||||
@@ -913,6 +939,17 @@
|
||||
const updateBanner = $("updateBanner");
|
||||
const updateBtn = $("updateBtn");
|
||||
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 subBtn = $("subBtn");
|
||||
const hero = $("hero");
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
// 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).
|
||||
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)"
|
||||
|
||||
case "$ARCH" in
|
||||
arm64)
|
||||
SUBDIR="darwin-arm64"
|
||||
;;
|
||||
x86_64)
|
||||
SUBDIR="darwin-amd64"
|
||||
;;
|
||||
arm64) SUBDIR="darwin-arm64" ;;
|
||||
x86_64) SUBDIR="darwin-amd64" ;;
|
||||
*)
|
||||
echo "Неподдерживаемая архитектура: $ARCH" >&2
|
||||
exit 1
|
||||
@@ -21,14 +17,17 @@ DEST="${1:-$HOME/Downloads/Navis.dmg}"
|
||||
UNIVERSAL="$BASE/darwin-universal/Navis.dmg"
|
||||
SPECIFIC="$BASE/$SUBDIR/Navis.dmg"
|
||||
|
||||
echo "Mac: $ARCH"
|
||||
echo "Рекомендуется универсальный DMG (Intel + Apple Silicon)…"
|
||||
if curl -fL -o "$DEST" "$UNIVERSAL"; then
|
||||
echo "Скачан: $DEST"
|
||||
else
|
||||
echo "Универсальный DMG недоступен, скачиваю $SUBDIR…"
|
||||
echo "Mac: $ARCH · ставим Navis GUI 2.7"
|
||||
echo "Скачиваю универсальный DMG…"
|
||||
if ! curl -fL -o "$DEST" "$UNIVERSAL"; then
|
||||
echo "Universal недоступен, скачиваю $SUBDIR…"
|
||||
curl -fL -o "$DEST" "$SPECIFIC"
|
||||
fi
|
||||
|
||||
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",
|
||||
"notes": "Быстрое отключение AWG без зависания UI («приложение не отвечает»)",
|
||||
"version": "2.7.0",
|
||||
"notes": "GUI на macOS (как на Windows). Откройте Navis.app — окно клиента. CLI: Navis-cli",
|
||||
"platform": "windows-amd64",
|
||||
"os": "windows",
|
||||
"arch": "amd64",
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
|
||||
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
|
||||
"mandatory": false,
|
||||
"platforms": {
|
||||
"windows-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||
"sha256": "14fe9afb7978433a811662175ed21e9f65e66420c105ef943ca6f11aeeada072",
|
||||
"sha256": "60392f389c9ed48b11124ce440d787ab7b051e7593833f15172e1c09434245dd",
|
||||
"os": "windows",
|
||||
"arch": "amd64"
|
||||
},
|
||||
"darwin-arm64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
||||
"sha256": "497cdf66b74e28061622debbbc6fa922ae64ae532e867dec876188af15e85278",
|
||||
"sha256": "dd15b856bed836b82f2892e64db4db71771a947f3a392ce7d9f9e7f1b59edf1d",
|
||||
"os": "darwin",
|
||||
"arch": "arm64",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis.dmg",
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"darwin-amd64": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
||||
"sha256": "95ccb649116bd15cbcdddccdd57df77651bf5f41ec19a4c6a0e91f8139621df7",
|
||||
"sha256": "e71d7c0080e9acdcaa2355e4dd8fa3ccec8d4ed24646e27dbc1a814b0a4019cc",
|
||||
"os": "darwin",
|
||||
"arch": "amd64",
|
||||
"dmg_url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis.dmg",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"darwin-universal": {
|
||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-universal/Navis",
|
||||
"sha256": "abdb4d5807145dfab7afa1fc9fd4d0382a4f5d9b4ce7621128b2b393c7f0c928",
|
||||
"sha256": "6b58907bb81583cf1730b34012cce97e7f758cf3eb12e630c971d0232cce5262",
|
||||
"os": "darwin",
|
||||
"arch": "universal",
|
||||
"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 {
|
||||
switch arch {
|
||||
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.
|
||||
|
||||
1) Перетащите Navis.app в Программы (Applications)
|
||||
2) При первом запуске, если macOS блокирует:
|
||||
2) При блокировке:
|
||||
xattr -dr com.apple.quarantine /Applications/Navis.app
|
||||
3) В Терминале:
|
||||
/Applications/Navis.app/Contents/MacOS/Navis init
|
||||
/Applications/Navis.app/Contents/MacOS/Navis install-core
|
||||
/Applications/Navis.app/Contents/MacOS/Navis connect
|
||||
3) Запустите Navis — откроется окно как на Windows
|
||||
(Chrome/Edge/Brave в режиме приложения, иначе браузер)
|
||||
4) CLI (ядро/connect из терминала): рядом на git файл Navis-cli
|
||||
|
||||
Магазин: https://evilfox.win/
|
||||
`, version)
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"FixedFileInfo": {
|
||||
"FileVersion": { "Major": 2, "Minor": 6, "Patch": 2, "Build": 0 },
|
||||
"ProductVersion": { "Major": 2, "Minor": 6, "Patch": 2, "Build": 0 },
|
||||
"FileVersion": { "Major": 2, "Minor": 7, "Patch": 0, "Build": 0 },
|
||||
"ProductVersion": { "Major": 2, "Minor": 7, "Patch": 0, "Build": 0 },
|
||||
"FileFlagsMask": "3f",
|
||||
"FileFlags": "00",
|
||||
"FileOS": "40004",
|
||||
@@ -11,12 +11,12 @@
|
||||
"StringFileInfo": {
|
||||
"CompanyName": "EvilFox",
|
||||
"FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
||||
"FileVersion": "2.6.2.0",
|
||||
"FileVersion": "2.7.0.0",
|
||||
"InternalName": "Navis",
|
||||
"LegalCopyright": "Copyright (c) EvilFox",
|
||||
"OriginalFilename": "Navis.exe",
|
||||
"ProductName": "Navis",
|
||||
"ProductVersion": "2.6.2.0",
|
||||
"ProductVersion": "2.7.0.0",
|
||||
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
Reference in New Issue
Block a user