Release 4.0.0+1: rebrand Navis → EvilFox; centralized Remnawave auto-provisioning (panel credentials baked into binary); remove Remnawave API UI; Windows 4.0.0.1.
Keep dist/navis-release/ path and ship Navis.exe as a same-hash alias of EvilFox.exe so 3.x clients can still update from the Windows branch feed.
This commit is contained in:
@@ -6,6 +6,8 @@ bin/
|
|||||||
configs/config.json
|
configs/config.json
|
||||||
configs/remnawave-api.json
|
configs/remnawave-api.json
|
||||||
configs/account.json
|
configs/account.json
|
||||||
|
# Baked panel credentials (copied by build.bat; never commit)
|
||||||
|
internal/remnawave/embeddedcfg/remnawave-api.json
|
||||||
dist/navis-release/windows/configs/
|
dist/navis-release/windows/configs/
|
||||||
# Runtime-downloaded protocol cores + local packaging artifact (not release sources)
|
# Runtime-downloaded protocol cores + local packaging artifact (not release sources)
|
||||||
dist/navis-release/windows/bin/
|
dist/navis-release/windows/bin/
|
||||||
@@ -17,8 +19,11 @@ cmd/vpnapp/resource.syso
|
|||||||
navis-update.bat
|
navis-update.bat
|
||||||
Navis.exe.new
|
Navis.exe.new
|
||||||
Navis-pending.exe
|
Navis-pending.exe
|
||||||
|
EvilFox.exe.new
|
||||||
|
EvilFox-pending.exe
|
||||||
*.bak
|
*.bak
|
||||||
Navis.exe.bak
|
Navis.exe.bak
|
||||||
|
EvilFox.exe.bak
|
||||||
|
|
||||||
# MSIX staging / local packages
|
# MSIX staging / local packages
|
||||||
packaging/msix/_staging/
|
packaging/msix/_staging/
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
# Navis
|
# EvilFox
|
||||||
|
|
||||||
Клиент [NaiveProxy](https://github.com/klzgrad/naiveproxy) + [Hysteria 2](https://github.com/apernet/hysteria) + [AmneziaWG 2.0](https://docs.amnezia.org/documentation/amnezia-wg/) + [Xray](https://github.com/XTLS/Xray-core) (VLESS / VMess / Trojan) с профилями, пингом, URL-подпиской / **Remnawave API** и автообновлением.
|
Клиент [NaiveProxy](https://github.com/klzgrad/naiveproxy) + [Hysteria 2](https://github.com/apernet/hysteria) + [AmneziaWG 2.0](https://docs.amnezia.org/documentation/amnezia-wg/) + [Xray](https://github.com/XTLS/Xray-core) (VLESS / VMess / Trojan) с профилями, пингом, централизованной выдачей конфигов (Remnawave) и автообновлением.
|
||||||
|
|
||||||
|
> Ранее приложение называлось **Navis**. С 4.0.0 бинарник — `EvilFox.exe`. Каталог релиза `dist/navis-release/` и URL-путь сохранены, чтобы клиенты 3.x продолжали обновляться; в той же папке лежит и `Navis.exe` (копия `EvilFox.exe`) как совместимый алиас.
|
||||||
|
|
||||||
## Платформы
|
## Платформы
|
||||||
|
|
||||||
| OS | Артефакты | UI |
|
| OS | Артефакты | UI |
|
||||||
|----|----------|----|
|
|----|----------|----|
|
||||||
| Windows amd64 | `Navis.exe` | GUI (WebView2) |
|
| Windows amd64 | `EvilFox.exe` (+ `Navis.exe` alias) | GUI (WebView2) |
|
||||||
| macOS universal | `Navis.dmg` · `Navis.app.zip` | **GUI** (как Windows) |
|
| macOS universal | `Navis.dmg` · `Navis.app.zip` | **GUI** (как Windows) |
|
||||||
| macOS arm64 / amd64 | `Navis` · DMG · `Navis-cli` | GUI + CLI |
|
| macOS arm64 / amd64 | `Navis` · DMG · `Navis-cli` | GUI + CLI |
|
||||||
| Android | `Navis.apk` | GUI (VpnService) |
|
| Android | `Navis.apk` | GUI (VpnService) |
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ android {
|
|||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
// versionCode = major*1_000_000 + minor*10_000 + patch*100 + build
|
// versionCode = major*1_000_000 + minor*10_000 + patch*100 + build
|
||||||
versionCode = 3_100_001
|
versionCode = 4_000_001
|
||||||
versionName = "3.10.0+1"
|
versionName = "4.0.0+1"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ fun NavisScreen(
|
|||||||
.padding(18.dp)
|
.padding(18.dp)
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text("Navis", fontSize = 28.sp, fontWeight = FontWeight.Black, color = Ink)
|
Text("EvilFox", fontSize = 28.sp, fontWeight = FontWeight.Black, color = Ink)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Surface(color = Soft, shape = RoundedCornerShape(999.dp)) {
|
Surface(color = Soft, shape = RoundedCornerShape(999.dp)) {
|
||||||
Text("2.6.1", Modifier.padding(horizontal = 8.dp, vertical = 2.dp), fontSize = 12.sp, fontWeight = FontWeight.Bold, color = AccentDeep)
|
Text("2.6.1", Modifier.padding(horizontal = 8.dp, vertical = 2.dp), fontSize = 12.sp, fontWeight = FontWeight.Bold, color = AccentDeep)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Navis</string>
|
<string name="app_name">EvilFox</string>
|
||||||
<string name="vpn_notification_title">Navis VPN</string>
|
<string name="vpn_notification_title">EvilFox VPN</string>
|
||||||
<string name="vpn_notification_connected">Подключено</string>
|
<string name="vpn_notification_connected">Подключено</string>
|
||||||
<string name="vpn_notification_channel">VPN</string>
|
<string name="vpn_notification_channel">VPN</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -14,22 +14,37 @@ if errorlevel 1 (
|
|||||||
goversioninfo -64 -icon assets\navis.ico -manifest assets\app.manifest -o cmd\vpnapp\resource.syso versioninfo.json
|
goversioninfo -64 -icon assets\navis.ico -manifest assets\app.manifest -o cmd\vpnapp\resource.syso versioninfo.json
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
echo Building Navis GUI and CLI...
|
REM Bake Remnawave panel credentials into the binary (NordVPN-style centralized provisioning).
|
||||||
go build -ldflags="-H windowsgui -s -w" -trimpath -o Navis.exe ./cmd/vpnapp
|
REM Source of truth: configs\remnawave-api.json (gitignored). Never commit the embedded copy.
|
||||||
|
if exist "configs\remnawave-api.json" (
|
||||||
|
mkdir "internal\remnawave\embeddedcfg" 2>nul
|
||||||
|
copy /Y "configs\remnawave-api.json" "internal\remnawave\embeddedcfg\remnawave-api.json" >nul
|
||||||
|
echo Embedded panel credentials from configs\remnawave-api.json
|
||||||
|
) else (
|
||||||
|
echo WARNING: configs\remnawave-api.json missing — build will use external file / no auto-provision
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Building EvilFox GUI and CLI...
|
||||||
|
go build -ldflags="-H windowsgui -s -w" -trimpath -o EvilFox.exe ./cmd/vpnapp
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
go build -ldflags="-s -w" -trimpath -o vpnclient.exe ./cmd/vpnclient
|
go build -ldflags="-s -w" -trimpath -o evilfox-cli.exe ./cmd/vpnclient
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
mkdir "dist\navis-release\windows" 2>nul
|
mkdir "dist\navis-release\windows" 2>nul
|
||||||
copy /Y Navis.exe "dist\navis-release\windows\Navis.exe" >nul
|
copy /Y EvilFox.exe "dist\navis-release\windows\EvilFox.exe" >nul
|
||||||
copy /Y vpnclient.exe "dist\navis-release\windows\vpnclient.exe" >nul
|
REM Keep Navis.exe path for existing 3.x clients that fetch the old feed URL.
|
||||||
|
copy /Y EvilFox.exe "dist\navis-release\windows\Navis.exe" >nul
|
||||||
|
copy /Y evilfox-cli.exe "dist\navis-release\windows\evilfox-cli.exe" >nul
|
||||||
|
copy /Y evilfox-cli.exe "dist\navis-release\windows\vpnclient.exe" >nul
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Done:
|
echo Done:
|
||||||
echo Navis.exe
|
echo EvilFox.exe
|
||||||
echo vpnclient.exe
|
echo evilfox-cli.exe
|
||||||
echo dist\navis-release\windows\Navis.exe
|
echo dist\navis-release\windows\EvilFox.exe
|
||||||
|
echo dist\navis-release\windows\Navis.exe (compat alias for 3.x updater)
|
||||||
|
echo dist\navis-release\windows\evilfox-cli.exe
|
||||||
echo dist\navis-release\windows\vpnclient.exe
|
echo dist\navis-release\windows\vpnclient.exe
|
||||||
echo assets\navis.ico
|
echo assets\navis.ico
|
||||||
endlocal
|
endlocal
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func main() {
|
|||||||
log.Printf("open UI: %v — откройте вручную: %s", err, uiURL)
|
log.Printf("open UI: %v — откройте вручную: %s", err, uiURL)
|
||||||
fmt.Println(uiURL)
|
fmt.Println(uiURL)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Navis UI: %s", uiURL)
|
log.Printf("EvilFox UI: %s", uiURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
sig := make(chan os.Signal, 1)
|
sig := make(chan os.Signal, 1)
|
||||||
@@ -96,7 +96,7 @@ func openAppWindow(uiURL string) error {
|
|||||||
func fatalf(format string, args ...any) {
|
func fatalf(format string, args ...any) {
|
||||||
msg := fmt.Sprintf(format, args...)
|
msg := fmt.Sprintf(format, args...)
|
||||||
log.Println(msg)
|
log.Println(msg)
|
||||||
_ = exec.Command("osascript", "-e", fmt.Sprintf(`display alert "Navis" message %q`, msg)).Run()
|
_ = exec.Command("osascript", "-e", fmt.Sprintf(`display alert "EvilFox" message %q`, msg)).Run()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Fprintln(os.Stderr, "Navis GUI supports Windows (WebView2) and macOS.")
|
fmt.Fprintln(os.Stderr, "EvilFox GUI supports Windows (WebView2) and macOS.")
|
||||||
fmt.Fprintln(os.Stderr, "On this OS use the CLI: ./Navis connect | install-core | check-update")
|
fmt.Fprintln(os.Stderr, "On this OS use the CLI: ./evilfox-cli connect | install-core | check-update")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ func main() {
|
|||||||
AutoFocus: true,
|
AutoFocus: true,
|
||||||
DataPath: dataPath,
|
DataPath: dataPath,
|
||||||
WindowOptions: webview2.WindowOptions{
|
WindowOptions: webview2.WindowOptions{
|
||||||
Title: "Navis 2",
|
Title: "EvilFox",
|
||||||
Width: 920,
|
Width: 920,
|
||||||
Height: 680,
|
Height: 680,
|
||||||
Center: true,
|
Center: true,
|
||||||
@@ -160,12 +160,32 @@ func main() {
|
|||||||
mustBind(w, "getLogs", a.getLogs)
|
mustBind(w, "getLogs", a.getLogs)
|
||||||
|
|
||||||
go a.autoCheckUpdate()
|
go a.autoCheckUpdate()
|
||||||
|
go a.autoProvision()
|
||||||
mgr.StartAutoRenew(6 * time.Hour)
|
mgr.StartAutoRenew(6 * time.Hour)
|
||||||
|
|
||||||
w.SetHtml(appui.IndexHTML)
|
w.SetHtml(appui.IndexHTML)
|
||||||
w.Run()
|
w.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// autoProvision quietly issues a panel account + imports configs on first run
|
||||||
|
// (NordVPN-style centralized provisioning — users never edit the panel).
|
||||||
|
func (a *app) autoProvision() {
|
||||||
|
if !a.mgr.ProvisionConfigured() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
out, err := a.mgr.EnsureAutoProvision()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(a.logBuf, "auto-provision: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if out.Created {
|
||||||
|
fmt.Fprintf(a.logBuf, "auto-provision: создан %s · серверов %d\n", out.Username, out.Imported)
|
||||||
|
} else if out.Imported > 0 {
|
||||||
|
fmt.Fprintf(a.logBuf, "auto-provision: обновлены конфиги %s · серверов %d\n", out.Username, out.Imported)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mustBind(w webview2.WebView, name string, fn interface{}) {
|
func mustBind(w webview2.WebView, name string, fn interface{}) {
|
||||||
if err := w.Bind(name, fn); err != nil {
|
if err := w.Bind(name, fn); err != nil {
|
||||||
fatalDialog("bind %s: %v", name, err)
|
fatalDialog("bind %s: %v", name, err)
|
||||||
@@ -503,12 +523,12 @@ func (a *app) applyUpdate() (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
_ = a.mgr.Disconnect()
|
_ = a.mgr.Disconnect()
|
||||||
// Hard-exit so Windows unlocks Navis.exe; do not wait on webview.Terminate (can hang).
|
// Hard-exit so Windows unlocks EvilFox.exe; do not wait on webview.Terminate (can hang).
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(300 * time.Millisecond)
|
time.Sleep(300 * time.Millisecond)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}()
|
}()
|
||||||
return "Устанавливается v" + latest + "… Navis перезапустится.", nil
|
return "Устанавливается v" + latest + "… EvilFox перезапустится.", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *app) autoCheckUpdate() {
|
func (a *app) autoCheckUpdate() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
func messageBox(text string) {
|
func messageBox(text string) {
|
||||||
user32 := syscall.NewLazyDLL("user32.dll")
|
user32 := syscall.NewLazyDLL("user32.dll")
|
||||||
proc := user32.NewProc("MessageBoxW")
|
proc := user32.NewProc("MessageBoxW")
|
||||||
title, _ := syscall.UTF16PtrFromString("Navis")
|
title, _ := syscall.UTF16PtrFromString("EvilFox")
|
||||||
body, _ := syscall.UTF16PtrFromString(text)
|
body, _ := syscall.UTF16PtrFromString(text)
|
||||||
proc.Call(0, uintptr(unsafe.Pointer(body)), uintptr(unsafe.Pointer(title)), 0x10) // MB_ICONERROR
|
proc.Call(0, uintptr(unsafe.Pointer(body)), uintptr(unsafe.Pointer(title)), 0x10) // MB_ICONERROR
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,16 +59,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printUsage() {
|
func printUsage() {
|
||||||
fmt.Fprintf(os.Stderr, `Navis VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan) — Windows / macOS
|
fmt.Fprintf(os.Stderr, `EvilFox VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan) — Windows / macOS
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
navis init [-config configs/config.json]
|
evilfox-cli init [-config configs/config.json]
|
||||||
navis install-core [-config configs/config.json]
|
evilfox-cli install-core [-config configs/config.json]
|
||||||
navis set-proxy [-config configs/config.json] <share-or-proxy-uri>
|
evilfox-cli set-proxy [-config configs/config.json] <share-or-proxy-uri>
|
||||||
navis connect [-config configs/config.json] [-profile name] [-no-sysproxy] [-probe]
|
evilfox-cli connect [-config configs/config.json] [-profile name] [-no-sysproxy] [-probe]
|
||||||
navis probe [-config configs/config.json] [-profile name] [-url URL]
|
evilfox-cli probe [-config configs/config.json] [-profile name] [-url URL]
|
||||||
navis check-update
|
evilfox-cli check-update
|
||||||
navis apply-update
|
evilfox-cli apply-update
|
||||||
|
|
||||||
Share links: naive+https://… hysteria2://… vless://… vmess://… trojan://… awg .conf
|
Share links: naive+https://… hysteria2://… vless://… vmess://… trojan://… awg .conf
|
||||||
|
|
||||||
|
|||||||
Vendored
+6
-6
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"version": "3.10.0",
|
"version": "4.0.0",
|
||||||
"notes": "3.10.0+2: новый интерфейс с боковым меню (Главная / Профили / Настройки / Логи / О программе), аккаунт с продлением 50 ГБ / +30 дней и автопродлением, режим VPN на Windows (TUN, весь трафик, нужны права администратора и wintun.dll). Обновления теперь с git.de4ima.uk. Windows 3.10.0.2.",
|
"notes": "4.0.0+1: ребрендинг Navis → EvilFox; централизованная автовыдача конфигов (как NordVPN — панель вшита, пользователь только выбирает сервер и продлевает); из интерфейса убраны настройки Remnawave API. Windows 4.0.0.1. Клиенты 3.x обновляются по этому фиду; Navis.exe в релизе — совместимый алиас EvilFox.exe.",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/Navis.exe",
|
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
|
||||||
"sha256": "55ba57898ffc23ce1cb14172643513f70a2ae972c00181b2d3d889430bce1eb3",
|
"sha256": "869b37310526003143c5db8ea3ad6d4468173e9053cf585f06c162a861d5c49c",
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"windows-amd64": {
|
"windows-amd64": {
|
||||||
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/Navis.exe",
|
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
|
||||||
"sha256": "55ba57898ffc23ce1cb14172643513f70a2ae972c00181b2d3d889430bce1eb3",
|
"sha256": "869b37310526003143c5db8ea3ad6d4468173e9053cf585f06c162a861d5c49c",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+6
-6
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"version": "3.10.0",
|
"version": "4.0.0",
|
||||||
"notes": "3.10.0+2: новый интерфейс с боковым меню (Главная / Профили / Настройки / Логи / О программе), аккаунт с продлением 50 ГБ / +30 дней и автопродлением, режим VPN на Windows (TUN, весь трафик, нужны права администратора и wintun.dll). Обновления теперь с git.de4ima.uk. Windows 3.10.0.2.",
|
"notes": "4.0.0+1: ребрендинг Navis → EvilFox; централизованная автовыдача конфигов (как NordVPN — панель вшита, пользователь только выбирает сервер и продлевает); из интерфейса убраны настройки Remnawave API. Windows 4.0.0.1. Клиенты 3.x обновляются по этому фиду; Navis.exe в релизе — совместимый алиас EvilFox.exe.",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/Navis.exe",
|
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
|
||||||
"sha256": "55ba57898ffc23ce1cb14172643513f70a2ae972c00181b2d3d889430bce1eb3",
|
"sha256": "869b37310526003143c5db8ea3ad6d4468173e9053cf585f06c162a861d5c49c",
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"windows-amd64": {
|
"windows-amd64": {
|
||||||
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/Navis.exe",
|
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
|
||||||
"sha256": "55ba57898ffc23ce1cb14172643513f70a2ae972c00181b2d3d889430bce1eb3",
|
"sha256": "869b37310526003143c5db8ea3ad6d4468173e9053cf585f06c162a861d5c49c",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
},
|
},
|
||||||
|
|||||||
+13
-1
@@ -76,6 +76,18 @@ type PingBestResult struct {
|
|||||||
func New(mgr *core.Manager, cfgPath string, logBuf *bytes.Buffer) *App {
|
func New(mgr *core.Manager, cfgPath string, logBuf *bytes.Buffer) *App {
|
||||||
// Monthly plan upkeep: check at start and then periodically.
|
// Monthly plan upkeep: check at start and then periodically.
|
||||||
mgr.StartAutoRenew(6 * time.Hour)
|
mgr.StartAutoRenew(6 * time.Hour)
|
||||||
|
// NordVPN-style: silently issue a panel account + import configs.
|
||||||
|
go func() {
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
if !mgr.ProvisionConfigured() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if out, err := mgr.EnsureAutoProvision(); err != nil {
|
||||||
|
fmt.Fprintf(logBuf, "auto-provision: %v\n", err)
|
||||||
|
} else if out.Created {
|
||||||
|
fmt.Fprintf(logBuf, "auto-provision: создан %s · серверов %d\n", out.Username, out.Imported)
|
||||||
|
}
|
||||||
|
}()
|
||||||
return &App{
|
return &App{
|
||||||
Mgr: mgr,
|
Mgr: mgr,
|
||||||
CfgPath: cfgPath,
|
CfgPath: cfgPath,
|
||||||
@@ -410,7 +422,7 @@ func (a *App) ApplyUpdate() (string, error) {
|
|||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
return "Устанавливается v" + latest + "… Navis перезапустится.", nil
|
return "Устанавливается v" + latest + "… EvilFox перезапустится.", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) AutoCheckUpdate() {
|
func (a *App) AutoCheckUpdate() {
|
||||||
|
|||||||
+11
-139
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Navis</title>
|
<title>EvilFox</title>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
@@ -993,8 +993,8 @@
|
|||||||
<path d="M22.5 8.2l3.2-1.1-1.1 3.2-5.6 5.6 1.6 1.6 5.6-5.6 3.2-1.1-1.1 3.2" fill="#b8f0d8" opacity=".9"/>
|
<path d="M22.5 8.2l3.2-1.1-1.1 3.2-5.6 5.6 1.6 1.6 5.6-5.6 3.2-1.1-1.1 3.2" fill="#b8f0d8" opacity=".9"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="brand">Navis</h1>
|
<h1 class="brand">EvilFox</h1>
|
||||||
<span class="badge-ver" id="badgeVer">3.10.0</span>
|
<span class="badge-ver" id="badgeVer">4.0.0</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="nav-btn active" type="button" data-page="home">
|
<button class="nav-btn active" type="button" data-page="home">
|
||||||
@@ -1020,7 +1020,7 @@
|
|||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="side-foot">
|
<div class="side-foot">
|
||||||
<span class="ver" id="verLabel">Navis</span>
|
<span class="ver" id="verLabel">EvilFox</span>
|
||||||
<button type="button" class="theme-toggle" id="themeToggle" title="Тема" aria-label="Переключить тему">
|
<button type="button" class="theme-toggle" id="themeToggle" title="Тема" aria-label="Переключить тему">
|
||||||
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
<path d="M20.5 14.2A8.2 8.2 0 0 1 9.8 3.5 8.5 8.5 0 1 0 20.5 14.2z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
<path d="M20.5 14.2A8.2 8.2 0 0 1 9.8 3.5 8.5 8.5 0 1 0 20.5 14.2z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
||||||
@@ -1150,17 +1150,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<!-- Выдача доступа: скрыта, когда аккаунт уже привязан
|
|
||||||
(создание второго конфига отключено — платная подписка появится позже) -->
|
|
||||||
<div class="remna-box" id="provisionBox" hidden>
|
|
||||||
<div class="remna-title">Выдать доступ</div>
|
|
||||||
<p class="remna-hint">Создаёт пользователя в панели Remnawave (тариф из configs/remnawave-api.json, по умолчанию 50 ГБ / 30 дней), запоминает его и сразу импортирует конфиги. Повторно создать аккаунт нельзя — только продлевать.</p>
|
|
||||||
<div>
|
|
||||||
<label class="field" for="provUser">Пользователь</label>
|
|
||||||
<input id="provUser" type="text" placeholder="username / @telegram / email" spellcheck="false" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<button class="action primary" id="provBtn" type="button">Выдать доступ</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ============ Настройки ============ -->
|
<!-- ============ Настройки ============ -->
|
||||||
@@ -1173,7 +1162,7 @@
|
|||||||
<button type="button" id="modeProxyBtn">Режим прокси</button>
|
<button type="button" id="modeProxyBtn">Режим прокси</button>
|
||||||
<button type="button" id="modeVpnBtn">Режим VPN</button>
|
<button type="button" id="modeVpnBtn">Режим VPN</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="remna-hint" id="modeHint">Прокси — трафик идёт через локальный прокси (и системный прокси Windows). VPN — весь трафик, включая игры и приложения, через TUN-адаптер; нужны права администратора и wintun.dll рядом с Navis.exe. Меняется в отключённом состоянии.</p>
|
<p class="remna-hint" id="modeHint">Прокси — трафик идёт через локальный прокси (и системный прокси Windows). VPN — весь трафик, включая игры и приложения, через TUN-адаптер; нужны права администратора и wintun.dll рядом с EvilFox.exe. Меняется в отключённом состоянии.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -1260,33 +1249,6 @@
|
|||||||
<label class="check"><input id="hy2Lazy" type="checkbox" /> Lazy connect</label>
|
<label class="check"><input id="hy2Lazy" type="checkbox" /> Lazy connect</label>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details class="panel" id="rwPanel">
|
|
||||||
<summary>Remnawave API</summary>
|
|
||||||
<div class="remna-box" style="margin-top:8px;border:0;padding:0;background:transparent">
|
|
||||||
<p class="remna-hint">Панель → short UUID пользователя. Токен API — опционально (для /api/subscriptions/… и /users/…).</p>
|
|
||||||
<div>
|
|
||||||
<label class="field" for="rwBase">URL панели</label>
|
|
||||||
<input id="rwBase" type="text" placeholder="https://panel.example.com" spellcheck="false" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="field" for="rwShort">Short UUID</label>
|
|
||||||
<input id="rwShort" type="text" placeholder="abc12xyz" spellcheck="false" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="field" for="rwToken">API token</label>
|
|
||||||
<input id="rwToken" type="password" placeholder="Bearer из панели → API Tokens" spellcheck="false" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="field" for="rwCaddy">Caddy X-Api-Key</label>
|
|
||||||
<input id="rwCaddy" type="password" placeholder="если стоит auth у Caddy" spellcheck="false" autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<div class="sub-bar" style="margin-top:8px">
|
|
||||||
<button class="action secondary" id="rwSaveBtn" type="button" style="flex:1">Сохранить</button>
|
|
||||||
<button class="action primary" id="rwSyncBtn" type="button" style="flex:1">Синхронизировать</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details class="panel" id="toolsBox">
|
<details class="panel" id="toolsBox">
|
||||||
<summary>Сервис · cores, обновления</summary>
|
<summary>Сервис · cores, обновления</summary>
|
||||||
<div class="tools">
|
<div class="tools">
|
||||||
@@ -1317,9 +1279,9 @@
|
|||||||
<path d="M22.5 8.2l3.2-1.1-1.1 3.2-5.6 5.6 1.6 1.6 5.6-5.6 3.2-1.1-1.1 3.2" fill="#b8f0d8" opacity=".9"/>
|
<path d="M22.5 8.2l3.2-1.1-1.1 3.2-5.6 5.6 1.6 1.6 5.6-5.6 3.2-1.1-1.1 3.2" fill="#b8f0d8" opacity=".9"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2>Navis</h2>
|
<h2>EvilFox</h2>
|
||||||
<p id="aboutVer">версия…</p>
|
<p id="aboutVer">версия…</p>
|
||||||
<p style="margin-top:6px">VPN/прокси-клиент: Remnawave · Naive · Hysteria 2 · AWG · Xray</p>
|
<p style="margin-top:6px">VPN/прокси-клиент: Naive · Hysteria 2 · AWG · Xray</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="tools" style="margin-top:0">
|
<div class="tools" style="margin-top:0">
|
||||||
<button class="action secondary" id="aboutUpdBtn" type="button">Проверить обновление</button>
|
<button class="action secondary" id="aboutUpdBtn" type="button">Проверить обновление</button>
|
||||||
@@ -1498,15 +1460,6 @@
|
|||||||
}
|
}
|
||||||
const subUrl = $("subUrl");
|
const subUrl = $("subUrl");
|
||||||
const subBtn = $("subBtn");
|
const subBtn = $("subBtn");
|
||||||
const rwBase = $("rwBase");
|
|
||||||
const rwShort = $("rwShort");
|
|
||||||
const rwToken = $("rwToken");
|
|
||||||
const rwCaddy = $("rwCaddy");
|
|
||||||
const rwSaveBtn = $("rwSaveBtn");
|
|
||||||
const rwSyncBtn = $("rwSyncBtn");
|
|
||||||
const provisionBox = $("provisionBox");
|
|
||||||
const provUser = $("provUser");
|
|
||||||
const provBtn = $("provBtn");
|
|
||||||
const hero = $("hero");
|
const hero = $("hero");
|
||||||
const protoChip = $("protoChip");
|
const protoChip = $("protoChip");
|
||||||
const heroHint = $("heroHint");
|
const heroHint = $("heroHint");
|
||||||
@@ -1572,7 +1525,7 @@
|
|||||||
|
|
||||||
function markDirty() { dirty = true; }
|
function markDirty() { dirty = true; }
|
||||||
[
|
[
|
||||||
proxy, nameInput, sysproxy, subUrl, rwBase, rwShort, rwToken, rwCaddy,
|
proxy, nameInput, sysproxy, subUrl,
|
||||||
$("hy2Congestion"), $("hy2Bbr"), $("hy2Up"), $("hy2Down"),
|
$("hy2Congestion"), $("hy2Bbr"), $("hy2Up"), $("hy2Down"),
|
||||||
$("hy2Obfs"), $("hy2ObfsPass"), $("hy2Sni"), $("hy2Pin"), $("hy2Hop"),
|
$("hy2Obfs"), $("hy2ObfsPass"), $("hy2Sni"), $("hy2Pin"), $("hy2Hop"),
|
||||||
$("hy2Insecure"), $("hy2Fast"), $("hy2Lazy")
|
$("hy2Insecure"), $("hy2Fast"), $("hy2Lazy")
|
||||||
@@ -1681,7 +1634,7 @@
|
|||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
const empty = document.createElement("div");
|
const empty = document.createElement("div");
|
||||||
empty.className = "server-empty";
|
empty.className = "server-empty";
|
||||||
empty.textContent = "Нет серверов — добавьте профиль или подписку";
|
empty.textContent = "Нет серверов — подождите автовыдачу или добавьте подписку";
|
||||||
serverList.appendChild(empty);
|
serverList.appendChild(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1876,7 +1829,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderUpdate(u, version) {
|
function renderUpdate(u, version) {
|
||||||
const label = "Navis v" + (version || "?");
|
const label = "EvilFox v" + (version || "?");
|
||||||
verLabel.textContent = label;
|
verLabel.textContent = label;
|
||||||
if (aboutVer) aboutVer.textContent = "Версия " + (version || "?");
|
if (aboutVer) aboutVer.textContent = "Версия " + (version || "?");
|
||||||
if (badgeVer && version) {
|
if (badgeVer && version) {
|
||||||
@@ -1941,16 +1894,6 @@
|
|||||||
updateBtn.disabled = busy;
|
updateBtn.disabled = busy;
|
||||||
subBtn.disabled = busy;
|
subBtn.disabled = busy;
|
||||||
subUrl.disabled = busy;
|
subUrl.disabled = busy;
|
||||||
if (rwBase) rwBase.disabled = busy;
|
|
||||||
if (rwShort) rwShort.disabled = busy;
|
|
||||||
if (rwToken) rwToken.disabled = busy;
|
|
||||||
if (rwCaddy) rwCaddy.disabled = busy;
|
|
||||||
if (rwSaveBtn) rwSaveBtn.disabled = busy;
|
|
||||||
if (rwSyncBtn) rwSyncBtn.disabled = busy;
|
|
||||||
// Выдать доступ — только пока аккаунт НЕ привязан (второй не создаём).
|
|
||||||
if (provisionBox) provisionBox.hidden = !state.provision_ready || !!state.account;
|
|
||||||
if (provUser) provUser.disabled = busy;
|
|
||||||
if (provBtn) provBtn.disabled = busy;
|
|
||||||
if (renewBtn) renewBtn.disabled = busy || !state.provision_ready;
|
if (renewBtn) renewBtn.disabled = busy || !state.provision_ready;
|
||||||
if (modeProxyBtn) modeProxyBtn.disabled = busy || connected;
|
if (modeProxyBtn) modeProxyBtn.disabled = busy || connected;
|
||||||
if (modeVpnBtn) modeVpnBtn.disabled = busy || connected;
|
if (modeVpnBtn) modeVpnBtn.disabled = busy || connected;
|
||||||
@@ -1965,12 +1908,6 @@
|
|||||||
if (typeof state.subscription_url === "string" && !dirty) {
|
if (typeof state.subscription_url === "string" && !dirty) {
|
||||||
subUrl.value = state.subscription_url;
|
subUrl.value = state.subscription_url;
|
||||||
}
|
}
|
||||||
if (state.remnawave && !dirty) {
|
|
||||||
rwBase.value = state.remnawave.base_url || "";
|
|
||||||
rwShort.value = state.remnawave.short_uuid || "";
|
|
||||||
if (typeof state.remnawave.token === "string") rwToken.value = state.remnawave.token;
|
|
||||||
if (typeof state.remnawave.caddy_token === "string") rwCaddy.value = state.remnawave.caddy_token;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (syncForm || (!formHydrated && !dirty)) {
|
if (syncForm || (!formHydrated && !dirty)) {
|
||||||
const active = (state.profiles || []).find((p) => p.name === profile.value) || {};
|
const active = (state.profiles || []).find((p) => p.name === profile.value) || {};
|
||||||
@@ -2038,8 +1975,7 @@
|
|||||||
|
|
||||||
function paintButtonsLocked(locked) {
|
function paintButtonsLocked(locked) {
|
||||||
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, aboutUpdBtn,
|
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, aboutUpdBtn,
|
||||||
updateBtn, skipUpdateBtn, subBtn, subUrl, rwBase, rwShort, rwToken, rwCaddy, rwSaveBtn, rwSyncBtn,
|
updateBtn, skipUpdateBtn, subBtn, subUrl, renewBtn, modeProxyBtn, modeVpnBtn, logsRefreshBtn].forEach((b) => {
|
||||||
provUser, provBtn, renewBtn, modeProxyBtn, modeVpnBtn, logsRefreshBtn].forEach((b) => {
|
|
||||||
if (b) b.disabled = locked;
|
if (b) b.disabled = locked;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2166,15 +2102,6 @@
|
|||||||
return "Импортировано: " + r;
|
return "Импортировано: " + r;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readRemnawave() {
|
|
||||||
return {
|
|
||||||
base_url: (rwBase && rwBase.value || "").trim(),
|
|
||||||
short_uuid: (rwShort && rwShort.value || "").trim(),
|
|
||||||
token: (rwToken && rwToken.value || "").trim(),
|
|
||||||
caddy_token: (rwCaddy && rwCaddy.value || "").trim()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
subBtn.addEventListener("click", () => withBusy(async () => {
|
subBtn.addEventListener("click", () => withBusy(async () => {
|
||||||
try {
|
try {
|
||||||
await runImportSubscription();
|
await runImportSubscription();
|
||||||
@@ -2187,61 +2114,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (rwSaveBtn) {
|
|
||||||
rwSaveBtn.addEventListener("click", () => withBusy(async () => {
|
|
||||||
try {
|
|
||||||
await saveRemnawave(readRemnawave());
|
|
||||||
setMeta("Настройки Remnawave сохранены", "ok");
|
|
||||||
} catch (e) { setMeta(String(e), "err"); }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (rwSyncBtn) {
|
|
||||||
rwSyncBtn.addEventListener("click", () => withBusy(async () => {
|
|
||||||
try {
|
|
||||||
const s = readRemnawave();
|
|
||||||
if (!s.base_url || !s.short_uuid) {
|
|
||||||
setMeta("Укажите URL панели и short UUID", "err");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setMeta("Remnawave: загрузка конфигов…");
|
|
||||||
const r = await importRemnawave(s);
|
|
||||||
formHydrated = false;
|
|
||||||
dirty = false;
|
|
||||||
setMeta("Remnawave: " + importSummary(r).toLowerCase() + " · пинг…", "ok");
|
|
||||||
await runBest(!!autoBest.checked);
|
|
||||||
} catch (e) { setMeta(String(e), "err"); }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (provBtn) {
|
|
||||||
provBtn.addEventListener("click", () => withBusy(async () => {
|
|
||||||
try {
|
|
||||||
const u = (provUser.value || "").trim();
|
|
||||||
if (!u) {
|
|
||||||
setMeta("Укажите имя пользователя (username / @telegram / email)", "err");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setMeta("Выдача доступа через панель…");
|
|
||||||
const r = await provisionUser(u);
|
|
||||||
formHydrated = false;
|
|
||||||
dirty = false;
|
|
||||||
let msg = (r.created ? "Создан пользователь " : "Найден пользователь ") + r.username;
|
|
||||||
if (r.traffic_limit_bytes > 0) msg += " · " + fmtTraffic(r.traffic_limit_bytes);
|
|
||||||
if (r.expire_at) {
|
|
||||||
try { msg += " · до " + new Date(r.expire_at * 1000).toLocaleDateString("ru-RU"); } catch (_) {}
|
|
||||||
}
|
|
||||||
msg += " · импортировано серверов: " + (r.imported || 0);
|
|
||||||
setMeta(msg, "ok");
|
|
||||||
} catch (e) { setMeta(String(e), "err"); }
|
|
||||||
}));
|
|
||||||
provUser.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
provBtn.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (renewBtn) {
|
if (renewBtn) {
|
||||||
renewBtn.addEventListener("click", () => withBusy(async () => {
|
renewBtn.addEventListener("click", () => withBusy(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -298,10 +298,10 @@ func (m *Manager) SetMode(mode string) error {
|
|||||||
return fmt.Errorf("режим VPN пока доступен только на Windows")
|
return fmt.Errorf("режим VPN пока доступен только на Windows")
|
||||||
}
|
}
|
||||||
if !vpnmode.IsElevated() {
|
if !vpnmode.IsElevated() {
|
||||||
return fmt.Errorf("Запустите Navis от имени администратора для режима VPN")
|
return fmt.Errorf("Запустите EvilFox от имени администратора для режима VPN")
|
||||||
}
|
}
|
||||||
if !vpnmode.WintunAvailable() {
|
if !vpnmode.WintunAvailable() {
|
||||||
return fmt.Errorf("не найден wintun.dll — скачайте с wintun.net и положите рядом с Navis.exe")
|
return fmt.Errorf("не найден wintun.dll — скачайте с wintun.net и положите рядом с EvilFox.exe")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
@@ -700,9 +700,12 @@ type ProvisionOutcome struct {
|
|||||||
Skipped int `json:"skipped"`
|
Skipped int `json:"skipped"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProvisionConfigured reports whether configs/remnawave-api.json exists —
|
// ProvisionConfigured reports whether the Remnawave admin API is available
|
||||||
// the admin "Выдать доступ" panel is shown only in that case.
|
// (embedded credentials or configs/remnawave-api.json). Used for renew UI.
|
||||||
func (m *Manager) ProvisionConfigured() bool {
|
func (m *Manager) ProvisionConfigured() bool {
|
||||||
|
if remnawave.EmbeddedAdminConfig() != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
st, err := os.Stat(remnawave.AdminConfigPath(m.cfgPath))
|
st, err := os.Stat(remnawave.AdminConfigPath(m.cfgPath))
|
||||||
return err == nil && !st.IsDir()
|
return err == nil && !st.IsDir()
|
||||||
}
|
}
|
||||||
@@ -735,10 +738,46 @@ func (m *Manager) ProvisionAccess(username string) (ProvisionOutcome, error) {
|
|||||||
if acc := m.Account(); acc != nil {
|
if acc := m.Account(); acc != nil {
|
||||||
return ProvisionOutcome{}, fmt.Errorf("аккаунт уже привязан (%s) — новый создавать нельзя, используйте «Продлить»", acc.Username)
|
return ProvisionOutcome{}, fmt.Errorf("аккаунт уже привязан (%s) — новый создавать нельзя, используйте «Продлить»", acc.Username)
|
||||||
}
|
}
|
||||||
cfg, err := remnawave.LoadAdminConfig(remnawave.AdminConfigPath(m.cfgPath))
|
cfg, err := remnawave.ResolveAdminConfig(m.cfgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ProvisionOutcome{}, err
|
return ProvisionOutcome{}, err
|
||||||
}
|
}
|
||||||
|
return m.provisionWithConfig(cfg, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureAutoProvision silently creates a panel user (if none remembered yet)
|
||||||
|
// and imports its subscription configs. NordVPN-style: panel address / API
|
||||||
|
// token are embedded; the user never edits them. Safe to call on every start.
|
||||||
|
func (m *Manager) EnsureAutoProvision() (ProvisionOutcome, error) {
|
||||||
|
if acc := m.Account(); acc != nil {
|
||||||
|
// Already provisioned — refresh configs in the background if empty.
|
||||||
|
if len(m.Profiles()) == 0 && acc.SubscriptionURL != "" {
|
||||||
|
if imp, err := m.ImportSubscription(acc.SubscriptionURL); err == nil {
|
||||||
|
return ProvisionOutcome{
|
||||||
|
Username: acc.Username,
|
||||||
|
SubscriptionURL: acc.SubscriptionURL,
|
||||||
|
TrafficLimitBytes: acc.TrafficLimitBytes,
|
||||||
|
ExpireAt: acc.ExpireAt,
|
||||||
|
Imported: imp.Imported,
|
||||||
|
Skipped: imp.Skipped,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ProvisionOutcome{
|
||||||
|
Username: acc.Username,
|
||||||
|
SubscriptionURL: acc.SubscriptionURL,
|
||||||
|
TrafficLimitBytes: acc.TrafficLimitBytes,
|
||||||
|
ExpireAt: acc.ExpireAt,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
cfg, err := remnawave.ResolveAdminConfig(m.cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
return ProvisionOutcome{}, err
|
||||||
|
}
|
||||||
|
return m.provisionWithConfig(cfg, remnawave.GenerateUsername())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) provisionWithConfig(cfg *remnawave.AdminConfig, username string) (ProvisionOutcome, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
res, err := remnawave.ProvisionUser(ctx, cfg, username)
|
res, err := remnawave.ProvisionUser(ctx, cfg, username)
|
||||||
@@ -791,7 +830,7 @@ func (m *Manager) RenewAccount() (ProvisionOutcome, error) {
|
|||||||
if acc == nil {
|
if acc == nil {
|
||||||
return ProvisionOutcome{}, fmt.Errorf("нет привязанного аккаунта — сначала выдайте доступ")
|
return ProvisionOutcome{}, fmt.Errorf("нет привязанного аккаунта — сначала выдайте доступ")
|
||||||
}
|
}
|
||||||
cfg, err := remnawave.LoadAdminConfig(remnawave.AdminConfigPath(m.cfgPath))
|
cfg, err := remnawave.ResolveAdminConfig(m.cfgPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ProvisionOutcome{}, err
|
return ProvisionOutcome{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ func doGET(ctx context.Context, client *http.Client, s Settings, endpoint string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", "Navis/3.9 (Remnawave)")
|
req.Header.Set("User-Agent", "EvilFox/4.0 (Remnawave)")
|
||||||
req.Header.Set("Accept", "*/*")
|
req.Header.Set("Accept", "*/*")
|
||||||
if auth && s.Token != "" {
|
if auth && s.Token != "" {
|
||||||
req.Header.Set("Authorization", "Bearer "+s.Token)
|
req.Header.Set("Authorization", "Bearer "+s.Token)
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package remnawave
|
||||||
|
|
||||||
|
// Centralized (NordVPN-style) provisioning: the panel address and API token
|
||||||
|
// are baked into the binary at build time, so every user gets configs
|
||||||
|
// automatically and cannot change the panel.
|
||||||
|
//
|
||||||
|
// build.bat copies configs/remnawave-api.json into embeddedcfg/ before
|
||||||
|
// `go build`; the copy is gitignored so credentials never land in the repo.
|
||||||
|
// Without the embedded file the app falls back to the external
|
||||||
|
// configs/remnawave-api.json (developer mode).
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"embed"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed embeddedcfg
|
||||||
|
var embeddedCfgFS embed.FS
|
||||||
|
|
||||||
|
// EmbeddedAdminConfig returns the admin config baked into the binary,
|
||||||
|
// or nil when this build has no embedded credentials.
|
||||||
|
func EmbeddedAdminConfig() *AdminConfig {
|
||||||
|
data, err := embeddedCfgFS.ReadFile("embeddedcfg/remnawave-api.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cfg, err := parseAdminConfig(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateUsername makes a unique panel username for silent auto-provisioning
|
||||||
|
// (users never type anything): "fox_" + 10 hex chars, e.g. fox_3fa9c02b71.
|
||||||
|
func GenerateUsername() string {
|
||||||
|
var b [5]byte
|
||||||
|
if _, err := rand.Read(b[:]); err != nil {
|
||||||
|
// Keep within Remnawave's 6–34 char username limit.
|
||||||
|
s := fmt.Sprintf("fox_%x%x", os.Getpid()&0xffff, time.Now().UnixNano()&0xffffffff)
|
||||||
|
if len(s) > 34 {
|
||||||
|
s = s[:34]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return "fox_" + hex.EncodeToString(b[:])
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"_comment": "build.bat overwrites remnawave-api.json here from configs/ before go build. This placeholder keeps the embed directory non-empty for go build without credentials."
|
||||||
|
}
|
||||||
@@ -47,12 +47,26 @@ func AdminConfigPath(cfgPath string) string {
|
|||||||
return filepath.Join(filepath.Dir(cfgPath), "remnawave-api.json")
|
return filepath.Join(filepath.Dir(cfgPath), "remnawave-api.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveAdminConfig prefers the baked-in (embedded) panel credentials;
|
||||||
|
// falls back to configs/remnawave-api.json for local/developer builds.
|
||||||
|
func ResolveAdminConfig(cfgPath string) (*AdminConfig, error) {
|
||||||
|
if cfg := EmbeddedAdminConfig(); cfg != nil {
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
return LoadAdminConfig(AdminConfigPath(cfgPath))
|
||||||
|
}
|
||||||
|
|
||||||
// LoadAdminConfig reads and validates the admin API config file.
|
// LoadAdminConfig reads and validates the admin API config file.
|
||||||
func LoadAdminConfig(path string) (*AdminConfig, error) {
|
func LoadAdminConfig(path string) (*AdminConfig, error) {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("нет файла %s (скопируйте remnawave-api.example.json и вставьте ключ)", filepath.Base(path))
|
return nil, fmt.Errorf("нет файла %s (скопируйте remnawave-api.example.json и вставьте ключ)", filepath.Base(path))
|
||||||
}
|
}
|
||||||
|
return parseAdminConfig(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAdminConfig validates raw remnawave-api.json bytes (file or embedded).
|
||||||
|
func parseAdminConfig(data []byte) (*AdminConfig, error) {
|
||||||
var cfg AdminConfig
|
var cfg AdminConfig
|
||||||
if err := json.Unmarshal(bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF}), &cfg); err != nil {
|
if err := json.Unmarshal(bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF}), &cfg); err != nil {
|
||||||
return nil, fmt.Errorf("remnawave-api.json: некорректный JSON: %w", err)
|
return nil, fmt.Errorf("remnawave-api.json: некорректный JSON: %w", err)
|
||||||
@@ -252,7 +266,7 @@ func adminDo(ctx context.Context, client *http.Client, cfg *AdminConfig, method,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, "", err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", "Navis/3.10 (Remnawave provision)")
|
req.Header.Set("User-Agent", "EvilFox/4.0 (Remnawave provision)")
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
if body != nil {
|
if body != nil {
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ var (
|
|||||||
// for a generic UA, but return a Clash YAML config for Clash-like UAs — that
|
// for a generic UA, but return a Clash YAML config for Clash-like UAs — that
|
||||||
// YAML loses vless/trojan entries for us, so try the plain UA first.
|
// YAML loses vless/trojan entries for us, so try the plain UA first.
|
||||||
var userAgents = []string{
|
var userAgents = []string{
|
||||||
"Navis/3.9 (+https://evilfox.win)",
|
"EvilFox/4.0 (+https://evilfox.win)",
|
||||||
"ClashMeta/1.18.0",
|
"ClashMeta/1.18.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ import (
|
|||||||
|
|
||||||
// CurrentVersion is the product/semver used for update eligibility (feed "version").
|
// CurrentVersion is the product/semver used for update eligibility (feed "version").
|
||||||
// Keep major.minor.patch only — no build suffix here.
|
// Keep major.minor.patch only — no build suffix here.
|
||||||
const CurrentVersion = "3.10.0"
|
const CurrentVersion = "4.0.0"
|
||||||
|
|
||||||
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
|
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
|
||||||
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
|
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
|
||||||
const BuildNumber = 2
|
const BuildNumber = 1
|
||||||
|
|
||||||
// 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.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/update.json"
|
const DefaultManifestURL = "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/update.json"
|
||||||
@@ -287,7 +287,7 @@ func fetchManifest(ctx context.Context, manifestURL string) (Manifest, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Manifest{}, err
|
return Manifest{}, err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", "Navis/"+DisplayVersion())
|
req.Header.Set("User-Agent", "EvilFox/"+DisplayVersion())
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
client := &http.Client{Timeout: 20 * time.Second}
|
client := &http.Client{Timeout: 20 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
@@ -310,7 +310,7 @@ func downloadFile(ctx context.Context, url, dest string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", "Navis/"+DisplayVersion())
|
req.Header.Set("User-Agent", "EvilFox/"+DisplayVersion())
|
||||||
client := &http.Client{Timeout: 10 * time.Minute}
|
client := &http.Client{Timeout: 10 * time.Minute}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ import (
|
|||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// finishUpdateFlag must stay "--navis-finish-update" forever: older installed
|
||||||
|
// builds (3.x) launch the freshly downloaded binary with exactly this flag,
|
||||||
|
// so renaming it would break their update handoff.
|
||||||
const finishUpdateFlag = "--navis-finish-update"
|
const finishUpdateFlag = "--navis-finish-update"
|
||||||
|
|
||||||
// Apply downloads the new build and replaces the running Navis.exe after this process exits.
|
// Apply downloads the new build and replaces the running EvilFox.exe after this process exits.
|
||||||
func Apply(ctx context.Context, manifestURL string) (string, error) {
|
func Apply(ctx context.Context, manifestURL string) (string, error) {
|
||||||
if IsStorePackaged() {
|
if IsStorePackaged() {
|
||||||
return "", fmt.Errorf("обновления устанавливаются через Microsoft Store")
|
return "", fmt.Errorf("обновления устанавливаются через Microsoft Store")
|
||||||
@@ -27,7 +30,7 @@ func Apply(ctx context.Context, manifestURL string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
pending := filepath.Join(filepath.Dir(exe), "Navis-pending.exe")
|
pending := filepath.Join(filepath.Dir(exe), "EvilFox-pending.exe")
|
||||||
_ = os.Remove(pending)
|
_ = os.Remove(pending)
|
||||||
if err := os.Rename(tmp, pending); err != nil {
|
if err := os.Rename(tmp, pending); err != nil {
|
||||||
if err2 := copyFile(tmp, pending); err2 != nil {
|
if err2 := copyFile(tmp, pending); err2 != nil {
|
||||||
@@ -58,7 +61,7 @@ func Apply(ctx context.Context, manifestURL string) (string, error) {
|
|||||||
return latest, nil
|
return latest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MaybeFinishUpdate handles: Navis-pending.exe --navis-finish-update <pid> <targetExe>
|
// MaybeFinishUpdate handles: EvilFox-pending.exe --navis-finish-update <pid> <targetExe>
|
||||||
// Replaces targetExe in-place with this binary, then relaunches it.
|
// Replaces targetExe in-place with this binary, then relaunches it.
|
||||||
func MaybeFinishUpdate(args []string) bool {
|
func MaybeFinishUpdate(args []string) bool {
|
||||||
if len(args) < 3 || args[0] != finishUpdateFlag {
|
if len(args) < 3 || args[0] != finishUpdateFlag {
|
||||||
@@ -109,6 +112,7 @@ func CleanupStaleDownloads() {
|
|||||||
_ = os.Remove(filepath.Join(dir, base+".new"))
|
_ = os.Remove(filepath.Join(dir, base+".new"))
|
||||||
_ = os.Remove(filepath.Join(dir, "Navis-update.part"))
|
_ = os.Remove(filepath.Join(dir, "Navis-update.part"))
|
||||||
_ = os.Remove(filepath.Join(dir, ".navis-update-download"))
|
_ = os.Remove(filepath.Join(dir, ".navis-update-download"))
|
||||||
|
_ = os.Remove(filepath.Join(dir, "Navis-pending.exe"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func replaceExecutable(src, dst string) error {
|
func replaceExecutable(src, dst string) error {
|
||||||
|
|||||||
@@ -86,10 +86,10 @@ func Start(cfg Config) (*Session, error) {
|
|||||||
return nil, fmt.Errorf("режим VPN: нет локального SOCKS-прокси у текущего протокола")
|
return nil, fmt.Errorf("режим VPN: нет локального SOCKS-прокси у текущего протокола")
|
||||||
}
|
}
|
||||||
if !IsElevated() {
|
if !IsElevated() {
|
||||||
return nil, fmt.Errorf("Запустите Navis от имени администратора для режима VPN")
|
return nil, fmt.Errorf("Запустите EvilFox от имени администратора для режима VPN")
|
||||||
}
|
}
|
||||||
if !WintunAvailable() {
|
if !WintunAvailable() {
|
||||||
return nil, fmt.Errorf("не найден wintun.dll — скачайте с wintun.net и положите рядом с Navis.exe")
|
return nil, fmt.Errorf("не найден wintun.dll — скачайте с wintun.net и положите рядом с EvilFox.exe")
|
||||||
}
|
}
|
||||||
if len(cfg.DNSServers) == 0 {
|
if len(cfg.DNSServers) == 0 {
|
||||||
cfg.DNSServers = []string{"1.1.1.1", "8.8.8.8"}
|
cfg.DNSServers = []string{"1.1.1.1", "8.8.8.8"}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!--
|
||||||
Navis MSIX package manifest (Microsoft Store / sideload).
|
EvilFox MSIX package manifest (Microsoft Store / sideload).
|
||||||
|
|
||||||
WHY MSIX (not PWA):
|
WHY MSIX (not PWA):
|
||||||
Navis is a Win32 + WebView2 desktop VPN/proxy client (system proxy, local cores,
|
EvilFox is a Win32 + WebView2 desktop VPN/proxy client (system proxy, local cores,
|
||||||
Naive/Hy2/AWG/Xray). A PWA cannot replace those capabilities. Package the existing
|
Naive/Hy2/AWG/Xray). A PWA cannot replace those capabilities. Package the existing
|
||||||
GUI binary with Desktop Bridge / full-trust MSIX for Store distribution.
|
GUI binary with Desktop Bridge / full-trust MSIX for Store distribution.
|
||||||
|
|
||||||
IDENTITY PLACEHOLDERS — replace before Store submission:
|
IDENTITY PLACEHOLDERS — replace before Store submission:
|
||||||
Identity/@Name → Partner Center "Package/Identity/Name" (e.g. EvilFox.Navis)
|
Identity/@Name → Partner Center "Package/Identity/Name" (e.g. EvilFox.EvilFox)
|
||||||
Identity/@Publisher → exact Publisher CN from your Store certificate
|
Identity/@Publisher → exact Publisher CN from your Store certificate
|
||||||
(e.g. CN=EvilFox LLC or CN=A1B2C3D4-…)
|
(e.g. CN=EvilFox LLC or CN=A1B2C3D4-…)
|
||||||
Identity/@Version → Major.Minor.Build.Revision (all numeric; Revision often 0)
|
Identity/@Version → Major.Minor.Build.Revision (all numeric; Revision often 0)
|
||||||
@@ -31,15 +31,15 @@
|
|||||||
IgnorableNamespaces="uap rescap">
|
IgnorableNamespaces="uap rescap">
|
||||||
|
|
||||||
<Identity
|
<Identity
|
||||||
Name="EvilFox.Navis"
|
Name="EvilFox.EvilFox"
|
||||||
Publisher="CN=EvilFox"
|
Publisher="CN=EvilFox"
|
||||||
Version="3.10.0.2"
|
Version="4.0.0.1"
|
||||||
ProcessorArchitecture="x64" />
|
ProcessorArchitecture="x64" />
|
||||||
|
|
||||||
<Properties>
|
<Properties>
|
||||||
<DisplayName>Navis</DisplayName>
|
<DisplayName>EvilFox</DisplayName>
|
||||||
<PublisherDisplayName>EvilFox</PublisherDisplayName>
|
<PublisherDisplayName>EvilFox</PublisherDisplayName>
|
||||||
<Description>Navis — VPN/proxy client (Naive, Hysteria 2, AmneziaWG, Xray)</Description>
|
<Description>EvilFox — VPN/proxy client (Naive, Hysteria 2, AmneziaWG, Xray)</Description>
|
||||||
<Logo>Assets\StoreLogo.png</Logo>
|
<Logo>Assets\StoreLogo.png</Logo>
|
||||||
</Properties>
|
</Properties>
|
||||||
|
|
||||||
@@ -61,12 +61,12 @@
|
|||||||
|
|
||||||
<Applications>
|
<Applications>
|
||||||
<Application
|
<Application
|
||||||
Id="Navis"
|
Id="EvilFox"
|
||||||
Executable="Navis.exe"
|
Executable="EvilFox.exe"
|
||||||
EntryPoint="Windows.FullTrustApplication">
|
EntryPoint="Windows.FullTrustApplication">
|
||||||
<uap:VisualElements
|
<uap:VisualElements
|
||||||
DisplayName="Navis"
|
DisplayName="EvilFox"
|
||||||
Description="Navis VPN client"
|
Description="EvilFox VPN client"
|
||||||
BackgroundColor="transparent"
|
BackgroundColor="transparent"
|
||||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
Wide310x150Logo="Assets\Wide310x150Logo.png"
|
Wide310x150Logo="Assets\Wide310x150Logo.png"
|
||||||
Square71x71Logo="Assets\Square71x71Logo.png"
|
Square71x71Logo="Assets\Square71x71Logo.png"
|
||||||
Square310x310Logo="Assets\Square310x310Logo.png"
|
Square310x310Logo="Assets\Square310x310Logo.png"
|
||||||
ShortName="Navis" />
|
ShortName="EvilFox" />
|
||||||
<uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="#0c1613" />
|
<uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="#0c1613" />
|
||||||
</uap:VisualElements>
|
</uap:VisualElements>
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
+15
-15
@@ -1,24 +1,24 @@
|
|||||||
#Requires -Version 5.1
|
#Requires -Version 5.1
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Pack Navis.exe into an MSIX for Microsoft Store / sideload.
|
Pack EvilFox.exe into an MSIX for Microsoft Store / sideload.
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Builds Store assets, stages AppxManifest + Navis.exe, runs MakeAppx.
|
Builds Store assets, stages AppxManifest + EvilFox.exe, runs MakeAppx.
|
||||||
Prefers MSIX over PWA: Navis needs Win32/WebView2 VPN and system-proxy access.
|
Prefers MSIX over PWA: EvilFox needs Win32/WebView2 VPN and system-proxy access.
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\scripts\pack-msix.ps1
|
.\scripts\pack-msix.ps1
|
||||||
.\scripts\pack-msix.ps1 -Version 3.9.0.1 -Publisher "CN=EvilFox"
|
.\scripts\pack-msix.ps1 -Version 4.0.0.1 -Publisher "CN=EvilFox"
|
||||||
#>
|
#>
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$ExePath = "",
|
[string]$ExePath = "",
|
||||||
[string]$OutDir = "",
|
[string]$OutDir = "",
|
||||||
[string]$Version = "3.10.0.2",
|
[string]$Version = "4.0.0.1",
|
||||||
[string]$Name = "EvilFox.Navis",
|
[string]$Name = "EvilFox.EvilFox",
|
||||||
[string]$Publisher = "CN=EvilFox",
|
[string]$Publisher = "CN=EvilFox",
|
||||||
[string]$DisplayName = "Navis",
|
[string]$DisplayName = "EvilFox",
|
||||||
[string]$PublisherDisplayName = "EvilFox",
|
[string]$PublisherDisplayName = "EvilFox",
|
||||||
[string]$CertPath = "",
|
[string]$CertPath = "",
|
||||||
[string]$CertPassword = "",
|
[string]$CertPassword = "",
|
||||||
@@ -30,14 +30,14 @@ $Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
|||||||
Set-Location $Root
|
Set-Location $Root
|
||||||
|
|
||||||
if (-not $ExePath) {
|
if (-not $ExePath) {
|
||||||
$ExePath = Join-Path $Root "dist\navis-release\windows\Navis.exe"
|
$ExePath = Join-Path $Root "dist\navis-release\windows\EvilFox.exe"
|
||||||
}
|
}
|
||||||
if (-not $OutDir) {
|
if (-not $OutDir) {
|
||||||
$OutDir = Join-Path $Root "dist\navis-release\windows"
|
$OutDir = Join-Path $Root "dist\navis-release\windows"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath $ExePath)) {
|
if (-not (Test-Path -LiteralPath $ExePath)) {
|
||||||
throw "Navis.exe not found at '$ExePath'. Run build.bat first."
|
throw "EvilFox.exe not found at '$ExePath'. Run build.bat first."
|
||||||
}
|
}
|
||||||
|
|
||||||
function Find-SdkTool([string]$name) {
|
function Find-SdkTool([string]$name) {
|
||||||
@@ -74,22 +74,22 @@ New-Item -ItemType Directory -Path (Join-Path $Staging "Assets") | Out-Null
|
|||||||
|
|
||||||
$AssetsSrc = Join-Path $Root "packaging\msix\Assets"
|
$AssetsSrc = Join-Path $Root "packaging\msix\Assets"
|
||||||
Copy-Item -Path (Join-Path $AssetsSrc "*") -Destination (Join-Path $Staging "Assets") -Force
|
Copy-Item -Path (Join-Path $AssetsSrc "*") -Destination (Join-Path $Staging "Assets") -Force
|
||||||
Copy-Item -LiteralPath $ExePath -Destination (Join-Path $Staging "Navis.exe") -Force
|
Copy-Item -LiteralPath $ExePath -Destination (Join-Path $Staging "EvilFox.exe") -Force
|
||||||
|
|
||||||
$ManifestSrc = Join-Path $Root "packaging\msix\AppxManifest.xml"
|
$ManifestSrc = Join-Path $Root "packaging\msix\AppxManifest.xml"
|
||||||
$ManifestDst = Join-Path $Staging "AppxManifest.xml"
|
$ManifestDst = Join-Path $Staging "AppxManifest.xml"
|
||||||
$manifestText = Get-Content -LiteralPath $ManifestSrc -Raw -Encoding UTF8
|
$manifestText = Get-Content -LiteralPath $ManifestSrc -Raw -Encoding UTF8
|
||||||
# Prefer exact placeholder swaps so we never touch <?xml version=...?>.
|
# Prefer exact placeholder swaps so we never touch <?xml version=...?>.
|
||||||
$manifestText = $manifestText.Replace('Name="EvilFox.Navis"', "Name=`"$Name`"")
|
$manifestText = $manifestText.Replace('Name="EvilFox.EvilFox"', "Name=`"$Name`"")
|
||||||
$manifestText = $manifestText.Replace('Publisher="CN=EvilFox"', "Publisher=`"$Publisher`"")
|
$manifestText = $manifestText.Replace('Publisher="CN=EvilFox"', "Publisher=`"$Publisher`"")
|
||||||
$manifestText = $manifestText.Replace('Version="3.10.0.2"', "Version=`"$Version`"")
|
$manifestText = $manifestText.Replace('Version="4.0.0.1"', "Version=`"$Version`"")
|
||||||
$manifestText = $manifestText.Replace('<DisplayName>Navis</DisplayName>', "<DisplayName>$DisplayName</DisplayName>")
|
$manifestText = $manifestText.Replace('<DisplayName>EvilFox</DisplayName>', "<DisplayName>$DisplayName</DisplayName>")
|
||||||
$manifestText = $manifestText.Replace('<PublisherDisplayName>EvilFox</PublisherDisplayName>', "<PublisherDisplayName>$PublisherDisplayName</PublisherDisplayName>")
|
$manifestText = $manifestText.Replace('<PublisherDisplayName>EvilFox</PublisherDisplayName>', "<PublisherDisplayName>$PublisherDisplayName</PublisherDisplayName>")
|
||||||
$manifestText = $manifestText.Replace('DisplayName="Navis"', "DisplayName=`"$DisplayName`"")
|
$manifestText = $manifestText.Replace('DisplayName="EvilFox"', "DisplayName=`"$DisplayName`"")
|
||||||
[System.IO.File]::WriteAllText($ManifestDst, $manifestText, (New-Object System.Text.UTF8Encoding $false))
|
[System.IO.File]::WriteAllText($ManifestDst, $manifestText, (New-Object System.Text.UTF8Encoding $false))
|
||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
||||||
$MsixPath = Join-Path $OutDir ("Navis_" + $Version + ".msix")
|
$MsixPath = Join-Path $OutDir ("EvilFox_" + $Version + ".msix")
|
||||||
if (Test-Path $MsixPath) { Remove-Item -Force $MsixPath }
|
if (Test-Path $MsixPath) { Remove-Item -Force $MsixPath }
|
||||||
|
|
||||||
if (-not $MakeAppx) {
|
if (-not $MakeAppx) {
|
||||||
|
|||||||
+6
-6
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"version": "3.10.0",
|
"version": "4.0.0",
|
||||||
"notes": "3.10.0+2: новый интерфейс с боковым меню (Главная / Профили / Настройки / Логи / О программе), аккаунт с продлением 50 ГБ / +30 дней и автопродлением, режим VPN на Windows (TUN, весь трафик, нужны права администратора и wintun.dll). Обновления теперь с git.de4ima.uk. Windows 3.10.0.2.",
|
"notes": "4.0.0+1: ребрендинг Navis → EvilFox; централизованная автовыдача конфигов (как NordVPN — панель вшита, пользователь только выбирает сервер и продлевает); из интерфейса убраны настройки Remnawave API. Windows 4.0.0.1. Клиенты 3.x обновляются по этому фиду; Navis.exe в релизе — совместимый алиас EvilFox.exe.",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/Navis.exe",
|
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
|
||||||
"sha256": "55ba57898ffc23ce1cb14172643513f70a2ae972c00181b2d3d889430bce1eb3",
|
"sha256": "869b37310526003143c5db8ea3ad6d4468173e9053cf585f06c162a861d5c49c",
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"windows-amd64": {
|
"windows-amd64": {
|
||||||
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/Navis.exe",
|
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
|
||||||
"sha256": "55ba57898ffc23ce1cb14172643513f70a2ae972c00181b2d3d889430bce1eb3",
|
"sha256": "869b37310526003143c5db8ea3ad6d4468173e9053cf585f06c162a861d5c49c",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
},
|
},
|
||||||
|
|||||||
+8
-8
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"FixedFileInfo": {
|
"FixedFileInfo": {
|
||||||
"FileVersion": { "Major": 3, "Minor": 10, "Patch": 0, "Build": 2 },
|
"FileVersion": { "Major": 4, "Minor": 0, "Patch": 0, "Build": 1 },
|
||||||
"ProductVersion": { "Major": 3, "Minor": 10, "Patch": 0, "Build": 2 },
|
"ProductVersion": { "Major": 4, "Minor": 0, "Patch": 0, "Build": 1 },
|
||||||
"FileFlagsMask": "3f",
|
"FileFlagsMask": "3f",
|
||||||
"FileFlags": "00",
|
"FileFlags": "00",
|
||||||
"FileOS": "40004",
|
"FileOS": "40004",
|
||||||
@@ -10,13 +10,13 @@
|
|||||||
},
|
},
|
||||||
"StringFileInfo": {
|
"StringFileInfo": {
|
||||||
"CompanyName": "EvilFox",
|
"CompanyName": "EvilFox",
|
||||||
"FileDescription": "Navis 2 — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
"FileDescription": "EvilFox — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
||||||
"FileVersion": "3.10.0.2",
|
"FileVersion": "4.0.0.1",
|
||||||
"InternalName": "Navis",
|
"InternalName": "EvilFox",
|
||||||
"LegalCopyright": "Copyright (c) EvilFox",
|
"LegalCopyright": "Copyright (c) EvilFox",
|
||||||
"OriginalFilename": "Navis.exe",
|
"OriginalFilename": "EvilFox.exe",
|
||||||
"ProductName": "Navis",
|
"ProductName": "EvilFox",
|
||||||
"ProductVersion": "3.10.0.2",
|
"ProductVersion": "4.0.0.1",
|
||||||
"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