Release 1.4.0: macOS CLI builds and multi-platform update channel.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,27 +1,71 @@
|
|||||||
# Navis
|
# Navis
|
||||||
|
|
||||||
Windows-клиент [NaiveProxy](https://github.com/klzgrad/naiveproxy) с профилями, пингом серверов и автообновлением.
|
Клиент [NaiveProxy](https://github.com/klzgrad/naiveproxy) + [Hysteria 2](https://github.com/apernet/hysteria) с профилями, пингом, URL-подпиской и автообновлением.
|
||||||
|
|
||||||
## Сборка
|
## Платформы
|
||||||
|
|
||||||
|
| OS | Артефакт | UI |
|
||||||
|
|----|----------|----|
|
||||||
|
| Windows amd64 | `dist/navis-release/Navis.exe` | GUI (WebView2) |
|
||||||
|
| macOS arm64 | `dist/navis-release/darwin-arm64/Navis` | CLI |
|
||||||
|
| macOS amd64 | `dist/navis-release/darwin-amd64/Navis` | CLI |
|
||||||
|
|
||||||
|
## Сборка Windows
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
build.bat
|
build.bat
|
||||||
```
|
```
|
||||||
|
|
||||||
Или:
|
или:
|
||||||
|
|
||||||
```bat
|
```bat
|
||||||
go build -ldflags="-H windowsgui -s -w" -o Navis.exe ./cmd/vpnapp
|
go build -ldflags="-H windowsgui -s -w" -o Navis.exe ./cmd/vpnapp
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Сборка macOS (кросс с Windows / Linux)
|
||||||
|
|
||||||
|
```bat
|
||||||
|
build-macos.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
или:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o dist/navis-release/darwin-arm64/Navis ./cmd/vpnclient
|
||||||
|
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o dist/navis-release/darwin-amd64/Navis ./cmd/vpnclient
|
||||||
|
```
|
||||||
|
|
||||||
|
На Mac:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x Navis
|
||||||
|
./Navis init
|
||||||
|
./Navis install-core
|
||||||
|
./Navis set-proxy 'hysteria2://…'
|
||||||
|
./Navis connect
|
||||||
|
```
|
||||||
|
|
||||||
|
Системный прокси на macOS выставляется через `networksetup` (HTTP + SOCKS).
|
||||||
|
|
||||||
## Обновления
|
## Обновления
|
||||||
|
|
||||||
Клиент читает манифест из этого репозитория:
|
Манифест: `https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json`
|
||||||
|
|
||||||
- Manifest: `https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json`
|
Формат (multi-platform):
|
||||||
- Binary: `https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe`
|
|
||||||
|
|
||||||
Чтобы выкатить релиз: соберите `Navis.exe`, положите в `dist/navis-release/`, обновите `dist/update.json` (version + sha256), сделайте commit и push в `main`.
|
```json
|
||||||
|
{
|
||||||
|
"version": "1.4.0",
|
||||||
|
"notes": "…",
|
||||||
|
"platforms": {
|
||||||
|
"windows-amd64": { "url": "…/Navis.exe", "sha256": "…" },
|
||||||
|
"darwin-arm64": { "url": "…/darwin-arm64/Navis", "sha256": "…" },
|
||||||
|
"darwin-amd64": { "url": "…/darwin-amd64/Navis", "sha256": "…" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Клиент сам выбирает ключ `GOOS-GOARCH`.
|
||||||
|
|
||||||
## Купить доступ
|
## Купить доступ
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
mkdir "dist\navis-release\darwin-arm64" 2>nul
|
||||||
|
mkdir "dist\navis-release\darwin-amd64" 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
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
|
set GOARCH=amd64
|
||||||
|
go build -ldflags="-s -w" -o "dist\navis-release\darwin-amd64\Navis" .\cmd\vpnclient
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
|
echo Built:
|
||||||
|
dir /b "dist\navis-release\darwin-arm64\Navis"
|
||||||
|
dir /b "dist\navis-release\darwin-amd64\Navis"
|
||||||
|
endlocal
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Fprintln(os.Stderr, "Navis GUI is Windows-only (WebView2). Use vpnclient CLI on this platform.")
|
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")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-11
@@ -15,6 +15,7 @@ import (
|
|||||||
"vpnclient/internal/core"
|
"vpnclient/internal/core"
|
||||||
"vpnclient/internal/protocols/hysteria2"
|
"vpnclient/internal/protocols/hysteria2"
|
||||||
"vpnclient/internal/protocols/naive"
|
"vpnclient/internal/protocols/naive"
|
||||||
|
"vpnclient/internal/update"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -43,6 +44,10 @@ func main() {
|
|||||||
os.Exit(runProbe(args))
|
os.Exit(runProbe(args))
|
||||||
case "set-proxy":
|
case "set-proxy":
|
||||||
os.Exit(runSetProxy(args))
|
os.Exit(runSetProxy(args))
|
||||||
|
case "check-update":
|
||||||
|
os.Exit(runCheckUpdate(args))
|
||||||
|
case "apply-update":
|
||||||
|
os.Exit(runApplyUpdate(args))
|
||||||
case "help", "-h", "--help":
|
case "help", "-h", "--help":
|
||||||
printUsage()
|
printUsage()
|
||||||
default:
|
default:
|
||||||
@@ -53,21 +58,21 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printUsage() {
|
func printUsage() {
|
||||||
fmt.Fprintf(os.Stderr, `VPN client (NaiveProxy) for Windows
|
fmt.Fprintf(os.Stderr, `Navis VPN client (NaiveProxy + Hysteria 2) — Windows / macOS
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
vpnclient init [-config configs/config.json]
|
navis init [-config configs/config.json]
|
||||||
vpnclient install-core [-config configs/config.json]
|
navis install-core [-config configs/config.json]
|
||||||
vpnclient set-proxy [-config configs/config.json] <share-or-proxy-uri>
|
navis set-proxy [-config configs/config.json] <share-or-proxy-uri>
|
||||||
vpnclient connect [-config configs/config.json] [-profile name] [-no-sysproxy] [-probe]
|
navis connect [-config configs/config.json] [-profile name] [-no-sysproxy] [-probe]
|
||||||
vpnclient probe [-config configs/config.json] [-profile name] [-url URL]
|
navis probe [-config configs/config.json] [-profile name] [-url URL]
|
||||||
|
navis check-update
|
||||||
|
navis apply-update
|
||||||
|
|
||||||
Share links like naive+https://user:pass@host:443#name are accepted.
|
Share links: naive+https://… hysteria2://… hy2://…
|
||||||
|
|
||||||
Architecture:
|
On macOS system proxy uses networksetup (HTTP + SOCKS). Local listens:
|
||||||
This client runs the official naive.exe from klzgrad/naiveproxy (Chromium
|
socks://127.0.0.1:1080 http://127.0.0.1:1081
|
||||||
network stack) and optionally sets the Windows system HTTP proxy so apps
|
|
||||||
use the tunnel. That is a real NaiveProxy session, not a simulation.
|
|
||||||
|
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
@@ -255,3 +260,38 @@ func runSetProxy(args []string) int {
|
|||||||
fmt.Println("proxy saved")
|
fmt.Println("proxy saved")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runCheckUpdate(args []string) int {
|
||||||
|
_ = args
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
st, err := update.Check(ctx, update.DefaultManifestURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "check-update: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fmt.Printf("current=%s platform=%s latest=%s available=%v\n", st.Current, st.Platform, st.Latest, st.Available)
|
||||||
|
if st.Notes != "" {
|
||||||
|
fmt.Println(st.Notes)
|
||||||
|
}
|
||||||
|
if st.Error != "" && !st.Available {
|
||||||
|
fmt.Fprintf(os.Stderr, "note: %s\n", st.Error)
|
||||||
|
}
|
||||||
|
if st.Available {
|
||||||
|
fmt.Printf("url=%s\n", st.URL)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func runApplyUpdate(args []string) int {
|
||||||
|
_ = args
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
latest, err := update.Apply(ctx, update.DefaultManifestURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "apply-update: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fmt.Printf("updating to %s — exit the app to finish\n", latest)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+24
-4
@@ -1,10 +1,30 @@
|
|||||||
{
|
{
|
||||||
"version": "1.3.1",
|
"version": "1.4.0",
|
||||||
"notes": "Фикс пинга Hysteria2 (UDP вместо TCP) и открытие ссылки evilfox.win",
|
"notes": "macOS CLI (arm64/amd64), multi-platform updates, networksetup proxy, naive tar.xz",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "88ddd38b0324979cae882c6cb0b8e4298c98b88547e72649587106425201e09e",
|
"sha256": "110ab8f5806f2635b39ec586d8218120d0041a82212715530ae8c3838078bf11",
|
||||||
"mandatory": false
|
"mandatory": false,
|
||||||
|
"platforms": {
|
||||||
|
"windows-amd64": {
|
||||||
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
|
"sha256": "110ab8f5806f2635b39ec586d8218120d0041a82212715530ae8c3838078bf11",
|
||||||
|
"os": "windows",
|
||||||
|
"arch": "amd64"
|
||||||
|
},
|
||||||
|
"darwin-arm64": {
|
||||||
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
||||||
|
"sha256": "4ce2b429fe4c8e8e1fdf776150003e42e4452e9ecf95cb55a714f84884717fd6",
|
||||||
|
"os": "darwin",
|
||||||
|
"arch": "arm64"
|
||||||
|
},
|
||||||
|
"darwin-amd64": {
|
||||||
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
||||||
|
"sha256": "a91bc03c7c68d896c42bb3520010e96191c12a0e95e2b9c2582dd2732637d9af",
|
||||||
|
"os": "darwin",
|
||||||
|
"arch": "amd64"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+24
-4
@@ -1,10 +1,30 @@
|
|||||||
{
|
{
|
||||||
"version": "1.3.1",
|
"version": "1.4.0",
|
||||||
"notes": "Фикс пинга Hysteria2 (UDP вместо TCP) и открытие ссылки evilfox.win",
|
"notes": "macOS CLI (arm64/amd64), multi-platform updates, networksetup proxy, naive tar.xz",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "88ddd38b0324979cae882c6cb0b8e4298c98b88547e72649587106425201e09e",
|
"sha256": "110ab8f5806f2635b39ec586d8218120d0041a82212715530ae8c3838078bf11",
|
||||||
"mandatory": false
|
"mandatory": false,
|
||||||
|
"platforms": {
|
||||||
|
"windows-amd64": {
|
||||||
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
|
"sha256": "110ab8f5806f2635b39ec586d8218120d0041a82212715530ae8c3838078bf11",
|
||||||
|
"os": "windows",
|
||||||
|
"arch": "amd64"
|
||||||
|
},
|
||||||
|
"darwin-arm64": {
|
||||||
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
||||||
|
"sha256": "4ce2b429fe4c8e8e1fdf776150003e42e4452e9ecf95cb55a714f84884717fd6",
|
||||||
|
"os": "darwin",
|
||||||
|
"arch": "arm64"
|
||||||
|
},
|
||||||
|
"darwin-amd64": {
|
||||||
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
||||||
|
"sha256": "a91bc03c7c68d896c42bb3520010e96191c12a0e95e2b9c2582dd2732637d9af",
|
||||||
|
"os": "darwin",
|
||||||
|
"arch": "amd64"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -81,8 +82,12 @@ func (m *Manager) Connect(ctx context.Context, profileName string) error {
|
|||||||
return fmt.Errorf("system_proxy requires an http:// listen address in the profile")
|
return fmt.Errorf("system_proxy requires an http:// listen address in the profile")
|
||||||
}
|
}
|
||||||
if err := m.sys.Enable(httpProxy); err != nil {
|
if err := m.sys.Enable(httpProxy); err != nil {
|
||||||
_ = engine.Stop()
|
if errors.Is(err, sysproxy.ErrUnsupported) {
|
||||||
return fmt.Errorf("enable system proxy: %w", err)
|
fmt.Fprintf(m.stderr, "system proxy unsupported on this OS; local SOCKS/HTTP still up\n")
|
||||||
|
} else {
|
||||||
|
_ = engine.Stop()
|
||||||
|
return fmt.Errorf("enable system proxy: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,14 +40,22 @@ func ResolveBinary(binDir string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func candidateNames() []string {
|
func candidateNames() []string {
|
||||||
if runtime.GOOS == "windows" {
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
return []string{
|
return []string{
|
||||||
"hysteria.exe",
|
"hysteria.exe",
|
||||||
"hysteria-windows-amd64.exe",
|
"hysteria-windows-amd64.exe",
|
||||||
"hysteria-windows-amd64-avx.exe",
|
"hysteria-windows-amd64-avx.exe",
|
||||||
}
|
}
|
||||||
|
case "darwin":
|
||||||
|
return []string{
|
||||||
|
"hysteria",
|
||||||
|
"hysteria-darwin-arm64",
|
||||||
|
"hysteria-darwin-amd64",
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return []string{"hysteria", "hysteria-linux-amd64", "hysteria-linux-arm64"}
|
||||||
}
|
}
|
||||||
return []string{"hysteria", "hysteria-linux-amd64"}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnsureBinary downloads official Hysteria 2 release if missing.
|
// EnsureBinary downloads official Hysteria 2 release if missing.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -54,13 +55,13 @@ func EnsureBinary(binDir string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
zipPath := filepath.Join(binDir, assetName)
|
archivePath := filepath.Join(binDir, assetName)
|
||||||
if err := downloadFile(zipPath, url); err != nil {
|
if err := downloadFile(archivePath, url); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer os.Remove(zipPath)
|
defer os.Remove(archivePath)
|
||||||
|
|
||||||
if err := unzipNaive(zipPath, binDir); err != nil {
|
if err := extractNaiveArchive(archivePath, binDir); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return ResolveBinary(binDir)
|
return ResolveBinary(binDir)
|
||||||
@@ -119,9 +120,6 @@ func findReleaseAsset(pattern string) (name, downloadURL string, err error) {
|
|||||||
}
|
}
|
||||||
for _, a := range rel.Assets {
|
for _, a := range rel.Assets {
|
||||||
if strings.HasPrefix(a.Name, prefix) && strings.HasSuffix(a.Name, suffix) {
|
if strings.HasPrefix(a.Name, prefix) && strings.HasSuffix(a.Name, suffix) {
|
||||||
if strings.Contains(a.Name, ".tar.") {
|
|
||||||
return "", "", fmt.Errorf("auto-extract of %s not implemented; download manually from %s", a.Name, a.BrowserDownloadURL)
|
|
||||||
}
|
|
||||||
return a.Name, a.BrowserDownloadURL, nil
|
return a.Name, a.BrowserDownloadURL, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +145,61 @@ func downloadFile(path, url string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractNaiveArchive(archivePath, destDir string) error {
|
||||||
|
lower := strings.ToLower(archivePath)
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(lower, ".zip"):
|
||||||
|
return unzipNaive(archivePath, destDir)
|
||||||
|
case strings.HasSuffix(lower, ".tar.xz"), strings.HasSuffix(lower, ".txz"):
|
||||||
|
return extractTarXz(archivePath, destDir)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported archive: %s", filepath.Base(archivePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractTarXz(archivePath, destDir string) error {
|
||||||
|
tmp, err := os.MkdirTemp(destDir, "naive-extract-*")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
|
||||||
|
cmd := exec.Command("tar", "-xJf", archivePath, "-C", tmp)
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("tar extract: %w (%s)", err, strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
|
||||||
|
want := "naive"
|
||||||
|
var found string
|
||||||
|
_ = filepath.Walk(tmp, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil || info == nil || info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if info.Name() == want {
|
||||||
|
found = path
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if found == "" {
|
||||||
|
return fmt.Errorf("archive does not contain %s", want)
|
||||||
|
}
|
||||||
|
dest := filepath.Join(destDir, want)
|
||||||
|
in, err := os.Open(found)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
if _, err := io.Copy(out, in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func unzipNaive(zipPath, destDir string) error {
|
func unzipNaive(zipPath, destDir string) error {
|
||||||
r, err := zip.OpenReader(zipPath)
|
r, err := zip.OpenReader(zipPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func Fetch(ctx context.Context, rawURL string) ([]Item, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", "Navis/1.3.1")
|
req.Header.Set("User-Agent", "Navis/1.4.0")
|
||||||
client := &http.Client{Timeout: 45 * time.Second}
|
client := &http.Client{Timeout: 45 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package sysproxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type darwinController struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
enabled bool
|
||||||
|
host string
|
||||||
|
httpPort string
|
||||||
|
socksPort string
|
||||||
|
services []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlatform() Controller { return &darwinController{} }
|
||||||
|
|
||||||
|
func (c *darwinController) Enable(httpHostPort string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
host, port, err := net.SplitHostPort(httpHostPort)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("sysproxy: bad address %q: %w", httpHostPort, err)
|
||||||
|
}
|
||||||
|
if host == "" || host == "0.0.0.0" {
|
||||||
|
host = "127.0.0.1"
|
||||||
|
}
|
||||||
|
socks := "1080"
|
||||||
|
if port == "1081" {
|
||||||
|
socks = "1080"
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := listNetworkServices()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(services) == 0 {
|
||||||
|
return fmt.Errorf("sysproxy: no network services")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, svc := range services {
|
||||||
|
if err := runNetworkSetup("-setwebproxy", svc, host, port); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runNetworkSetup("-setsecurewebproxy", svc, host, port); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runNetworkSetup("-setsocksfirewallproxy", svc, host, socks); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runNetworkSetup("-setwebproxystate", svc, "on"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runNetworkSetup("-setsecurewebproxystate", svc, "on"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runNetworkSetup("-setsocksfirewallproxystate", svc, "on"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.enabled = true
|
||||||
|
c.host = host
|
||||||
|
c.httpPort = port
|
||||||
|
c.socksPort = socks
|
||||||
|
c.services = services
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *darwinController) Disable() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
if !c.enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
services := c.services
|
||||||
|
if len(services) == 0 {
|
||||||
|
var err error
|
||||||
|
services, err = listNetworkServices()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var first error
|
||||||
|
for _, svc := range services {
|
||||||
|
for _, args := range [][]string{
|
||||||
|
{"-setwebproxystate", svc, "off"},
|
||||||
|
{"-setsecurewebproxystate", svc, "off"},
|
||||||
|
{"-setsocksfirewallproxystate", svc, "off"},
|
||||||
|
} {
|
||||||
|
if err := runNetworkSetup(args...); err != nil && first == nil {
|
||||||
|
first = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.enabled = false
|
||||||
|
c.services = nil
|
||||||
|
return first
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *darwinController) Enabled() bool {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func listNetworkServices() ([]string, error) {
|
||||||
|
out, err := exec.Command("networksetup", "-listallnetworkservices").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("networksetup: %w", err)
|
||||||
|
}
|
||||||
|
var services []string
|
||||||
|
sc := bufio.NewScanner(bytes.NewReader(out))
|
||||||
|
for sc.Scan() {
|
||||||
|
line := strings.TrimSpace(sc.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "An asterisk") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "*") {
|
||||||
|
continue // disabled service
|
||||||
|
}
|
||||||
|
services = append(services, line)
|
||||||
|
}
|
||||||
|
return services, sc.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNetworkSetup(args ...string) error {
|
||||||
|
cmd := exec.Command("networksetup", args...)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("networksetup %v: %w (%s)", args, err, strings.TrimSpace(string(out)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !windows
|
//go:build !windows && !darwin
|
||||||
|
|
||||||
package sysproxy
|
package sysproxy
|
||||||
|
|
||||||
|
|||||||
+116
-110
@@ -9,35 +9,48 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CurrentVersion is the shipped client version.
|
// CurrentVersion is the shipped client version.
|
||||||
const CurrentVersion = "1.3.1"
|
const CurrentVersion = "1.4.0"
|
||||||
|
|
||||||
// DefaultManifestURL is the update feed (hosted in the project git repo).
|
// DefaultManifestURL is the update feed (hosted in the project git repo).
|
||||||
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
|
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
|
||||||
|
|
||||||
// Manifest describes a remote release.
|
// PlatformAsset is one OS/arch binary in the feed.
|
||||||
type Manifest struct {
|
type PlatformAsset struct {
|
||||||
Version string `json:"version"`
|
URL string `json:"url"`
|
||||||
URL string `json:"url"`
|
SHA256 string `json:"sha256,omitempty"`
|
||||||
SHA256 string `json:"sha256,omitempty"`
|
OS string `json:"os,omitempty"`
|
||||||
Notes string `json:"notes,omitempty"`
|
Arch string `json:"arch,omitempty"`
|
||||||
Mandatory bool `json:"mandatory,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status is returned to the UI.
|
// Manifest describes a remote release (multi-platform + legacy single URL).
|
||||||
|
type Manifest struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
URL string `json:"url,omitempty"` // legacy windows-amd64
|
||||||
|
SHA256 string `json:"sha256,omitempty"`
|
||||||
|
Notes string `json:"notes,omitempty"`
|
||||||
|
Mandatory bool `json:"mandatory,omitempty"`
|
||||||
|
Platform string `json:"platform,omitempty"` // legacy
|
||||||
|
OS string `json:"os,omitempty"`
|
||||||
|
Arch string `json:"arch,omitempty"`
|
||||||
|
Platforms map[string]PlatformAsset `json:"platforms,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status is returned to the UI / CLI.
|
||||||
type Status struct {
|
type Status struct {
|
||||||
Current string `json:"current"`
|
Current string `json:"current"`
|
||||||
Latest string `json:"latest,omitempty"`
|
Latest string `json:"latest,omitempty"`
|
||||||
Notes string `json:"notes,omitempty"`
|
Notes string `json:"notes,omitempty"`
|
||||||
URL string `json:"url,omitempty"`
|
URL string `json:"url,omitempty"`
|
||||||
|
SHA256 string `json:"sha256,omitempty"`
|
||||||
|
Platform string `json:"platform,omitempty"`
|
||||||
Available bool `json:"available"`
|
Available bool `json:"available"`
|
||||||
Mandatory bool `json:"mandatory"`
|
Mandatory bool `json:"mandatory"`
|
||||||
Checking bool `json:"checking,omitempty"`
|
Checking bool `json:"checking,omitempty"`
|
||||||
@@ -45,42 +58,54 @@ type Status struct {
|
|||||||
ManifestURL string `json:"manifest_url"`
|
ManifestURL string `json:"manifest_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check fetches the manifest and compares versions.
|
// PlatformKey is "GOOS-GOARCH", e.g. windows-amd64, darwin-arm64.
|
||||||
|
func PlatformKey() string {
|
||||||
|
return runtime.GOOS + "-" + runtime.GOARCH
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetFor returns download URL + checksum for the given platform key.
|
||||||
|
func (m Manifest) AssetFor(key string) (url, sha string, ok bool) {
|
||||||
|
if key == "" {
|
||||||
|
key = PlatformKey()
|
||||||
|
}
|
||||||
|
if m.Platforms != nil {
|
||||||
|
if a, found := m.Platforms[key]; found && strings.TrimSpace(a.URL) != "" {
|
||||||
|
return strings.TrimSpace(a.URL), strings.TrimSpace(a.SHA256), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Legacy single-asset manifests are windows-amd64.
|
||||||
|
if (key == "windows-amd64" || key == "windows-386") && strings.TrimSpace(m.URL) != "" {
|
||||||
|
return strings.TrimSpace(m.URL), strings.TrimSpace(m.SHA256), true
|
||||||
|
}
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check fetches the manifest and compares versions for this platform.
|
||||||
func Check(ctx context.Context, manifestURL string) (Status, error) {
|
func Check(ctx context.Context, manifestURL string) (Status, error) {
|
||||||
if manifestURL == "" {
|
if manifestURL == "" {
|
||||||
manifestURL = DefaultManifestURL
|
manifestURL = DefaultManifestURL
|
||||||
}
|
}
|
||||||
st := Status{Current: CurrentVersion, ManifestURL: manifestURL}
|
key := PlatformKey()
|
||||||
|
st := Status{Current: CurrentVersion, ManifestURL: manifestURL, Platform: key}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, manifestURL, nil)
|
m, err := fetchManifest(ctx, manifestURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return st, err
|
|
||||||
}
|
|
||||||
req.Header.Set("User-Agent", "Navis/"+CurrentVersion)
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 20 * time.Second}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
st.Error = err.Error()
|
|
||||||
return st, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
err = fmt.Errorf("update feed HTTP %d", resp.StatusCode)
|
|
||||||
st.Error = err.Error()
|
|
||||||
return st, err
|
|
||||||
}
|
|
||||||
var m Manifest
|
|
||||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&m); err != nil {
|
|
||||||
st.Error = err.Error()
|
st.Error = err.Error()
|
||||||
return st, err
|
return st, err
|
||||||
}
|
}
|
||||||
st.Latest = strings.TrimSpace(m.Version)
|
st.Latest = strings.TrimSpace(m.Version)
|
||||||
st.Notes = m.Notes
|
st.Notes = m.Notes
|
||||||
st.URL = m.URL
|
|
||||||
st.Mandatory = m.Mandatory
|
st.Mandatory = m.Mandatory
|
||||||
st.Available = Compare(st.Latest, CurrentVersion) > 0 && m.URL != ""
|
|
||||||
|
url, sha, ok := m.AssetFor(key)
|
||||||
|
if !ok {
|
||||||
|
st.Error = fmt.Sprintf("нет сборки для %s", key)
|
||||||
|
st.Available = false
|
||||||
|
return st, nil
|
||||||
|
}
|
||||||
|
st.URL = url
|
||||||
|
st.SHA256 = sha
|
||||||
|
st.Available = Compare(st.Latest, CurrentVersion) > 0 && url != ""
|
||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +145,6 @@ func splitVer(v string) []int {
|
|||||||
out = append(out, 0)
|
out = append(out, 0)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// strip pre-release suffix: 1.2.3-beta
|
|
||||||
if i := strings.IndexAny(p, "-+"); i >= 0 {
|
if i := strings.IndexAny(p, "-+"); i >= 0 {
|
||||||
p = p[:i]
|
p = p[:i]
|
||||||
}
|
}
|
||||||
@@ -130,78 +154,6 @@ func splitVer(v string) []int {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply downloads the new exe and schedules replacement after exit.
|
|
||||||
func Apply(ctx context.Context, manifestURL string) (string, error) {
|
|
||||||
st, err := Check(ctx, manifestURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if !st.Available {
|
|
||||||
return "", fmt.Errorf("обновление не найдено")
|
|
||||||
}
|
|
||||||
if !allowedDownloadURL(st.URL) {
|
|
||||||
return "", fmt.Errorf("URL обновления должен быть на git.evilfox.cc или files.de4ima.uk")
|
|
||||||
}
|
|
||||||
|
|
||||||
exe, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
exe, err = filepath.Abs(exe)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
dir := filepath.Dir(exe)
|
|
||||||
tmp := filepath.Join(dir, "Navis.exe.new")
|
|
||||||
bat := filepath.Join(dir, "navis-update.bat")
|
|
||||||
|
|
||||||
if err := downloadFile(ctx, st.URL, tmp); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
man, err := fetchManifest(ctx, manifestURL)
|
|
||||||
if err == nil && man.SHA256 != "" {
|
|
||||||
sum, err := fileSHA256(tmp)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if !strings.EqualFold(sum, man.SHA256) {
|
|
||||||
_ = os.Remove(tmp)
|
|
||||||
return "", fmt.Errorf("checksum mismatch")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Important: do NOT self-delete the .bat at the end — that causes
|
|
||||||
// "Не удается найти пакетный файл" on paths with spaces (e.g. D:\vpn navi).
|
|
||||||
script := "@echo off\r\n" +
|
|
||||||
"setlocal EnableExtensions\r\n" +
|
|
||||||
"cd /d \"" + dir + "\"\r\n" +
|
|
||||||
"set \"EXE=" + exe + "\"\r\n" +
|
|
||||||
"set \"NEW=" + tmp + "\"\r\n" +
|
|
||||||
":wait\r\n" +
|
|
||||||
"ping -n 2 127.0.0.1 >nul\r\n" +
|
|
||||||
"del /f /q \"%EXE%\" >nul 2>&1\r\n" +
|
|
||||||
"if exist \"%EXE%\" goto wait\r\n" +
|
|
||||||
"move /y \"%NEW%\" \"%EXE%\" >nul\r\n" +
|
|
||||||
"if not exist \"%EXE%\" exit /b 1\r\n" +
|
|
||||||
"start \"\" \"%EXE%\"\r\n" +
|
|
||||||
"exit /b 0\r\n"
|
|
||||||
if err := os.WriteFile(bat, []byte(script), 0o755); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detached cmd so Apply returns while updater keeps running.
|
|
||||||
cmd := exec.Command("cmd.exe", "/C", bat)
|
|
||||||
cmd.Dir = dir
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
HideWindow: true,
|
|
||||||
CreationFlags: 0x00000008 | 0x00000200, // DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
|
|
||||||
}
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return "", fmt.Errorf("start updater: %w", err)
|
|
||||||
}
|
|
||||||
return st.Latest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func allowedDownloadURL(u string) bool {
|
func allowedDownloadURL(u string) bool {
|
||||||
u = strings.ToLower(strings.TrimSpace(u))
|
u = strings.ToLower(strings.TrimSpace(u))
|
||||||
return strings.HasPrefix(u, "https://git.evilfox.cc/") ||
|
return strings.HasPrefix(u, "https://git.evilfox.cc/") ||
|
||||||
@@ -217,15 +169,21 @@ func fetchManifest(ctx context.Context, manifestURL string) (Manifest, error) {
|
|||||||
return Manifest{}, err
|
return Manifest{}, err
|
||||||
}
|
}
|
||||||
req.Header.Set("User-Agent", "Navis/"+CurrentVersion)
|
req.Header.Set("User-Agent", "Navis/"+CurrentVersion)
|
||||||
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Manifest{}, err
|
return Manifest{}, err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return Manifest{}, fmt.Errorf("update feed HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
var m Manifest
|
var m Manifest
|
||||||
err = json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&m)
|
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&m); err != nil {
|
||||||
return m, err
|
return Manifest{}, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadFile(ctx context.Context, url, dest string) error {
|
func downloadFile(ctx context.Context, url, dest string) error {
|
||||||
@@ -264,3 +222,51 @@ func fileSHA256(path string) (string, error) {
|
|||||||
}
|
}
|
||||||
return hex.EncodeToString(h.Sum(nil)), nil
|
return hex.EncodeToString(h.Sum(nil)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareDownload(ctx context.Context, manifestURL string) (latest, url, sha, exePath, tmpPath string, err error) {
|
||||||
|
st, err := Check(ctx, manifestURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", err
|
||||||
|
}
|
||||||
|
if !st.Available {
|
||||||
|
if st.Error != "" {
|
||||||
|
return "", "", "", "", "", fmt.Errorf("%s", st.Error)
|
||||||
|
}
|
||||||
|
return "", "", "", "", "", fmt.Errorf("обновление не найдено")
|
||||||
|
}
|
||||||
|
if !allowedDownloadURL(st.URL) {
|
||||||
|
return "", "", "", "", "", fmt.Errorf("URL обновления должен быть на git.evilfox.cc или files.de4ima.uk")
|
||||||
|
}
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", err
|
||||||
|
}
|
||||||
|
exe, err = filepath.Abs(exe)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", "", "", "", err
|
||||||
|
}
|
||||||
|
tmp := exe + ".new"
|
||||||
|
if err := downloadFile(ctx, st.URL, tmp); err != nil {
|
||||||
|
return "", "", "", "", "", err
|
||||||
|
}
|
||||||
|
wantSHA := st.SHA256
|
||||||
|
if wantSHA == "" {
|
||||||
|
if man, err := fetchManifest(ctx, manifestURL); err == nil {
|
||||||
|
if _, sha2, ok := man.AssetFor(PlatformKey()); ok {
|
||||||
|
wantSHA = sha2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if wantSHA != "" {
|
||||||
|
sum, err := fileSHA256(tmp)
|
||||||
|
if err != nil {
|
||||||
|
_ = os.Remove(tmp)
|
||||||
|
return "", "", "", "", "", err
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(sum, wantSHA) {
|
||||||
|
_ = os.Remove(tmp)
|
||||||
|
return "", "", "", "", "", fmt.Errorf("checksum mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return st.Latest, st.URL, wantSHA, exe, tmp, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Apply downloads the new binary and schedules replacement after exit (macOS).
|
||||||
|
func Apply(ctx context.Context, manifestURL string) (string, error) {
|
||||||
|
latest, _, _, exe, tmp, err := prepareDownload(ctx, manifestURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
_ = os.Chmod(tmp, 0o755)
|
||||||
|
dir := filepath.Dir(exe)
|
||||||
|
scriptPath := filepath.Join(dir, "navis-update.sh")
|
||||||
|
pid := os.Getpid()
|
||||||
|
|
||||||
|
script := "#!/bin/bash\n" +
|
||||||
|
"set -e\n" +
|
||||||
|
"EXE=" + shellQuote(exe) + "\n" +
|
||||||
|
"NEW=" + shellQuote(tmp) + "\n" +
|
||||||
|
"PID=" + strconv.Itoa(pid) + "\n" +
|
||||||
|
"while kill -0 \"$PID\" 2>/dev/null; do sleep 0.4; done\n" +
|
||||||
|
"sleep 0.5\n" +
|
||||||
|
"mv -f \"$NEW\" \"$EXE\"\n" +
|
||||||
|
"chmod +x \"$EXE\"\n" +
|
||||||
|
"rm -f " + shellQuote(scriptPath) + "\n"
|
||||||
|
|
||||||
|
if err := os.WriteFile(scriptPath, []byte(script), 0o755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cmd := exec.Command("/bin/bash", scriptPath)
|
||||||
|
cmd.Dir = dir
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return "", fmt.Errorf("start updater: %w", err)
|
||||||
|
}
|
||||||
|
return latest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func shellQuote(s string) string {
|
||||||
|
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//go:build !windows && !darwin
|
||||||
|
|
||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Apply is not implemented on this OS yet.
|
||||||
|
func Apply(ctx context.Context, manifestURL string) (string, error) {
|
||||||
|
_ = ctx
|
||||||
|
_ = manifestURL
|
||||||
|
return "", fmt.Errorf("автообновление пока только для Windows и macOS")
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package update
|
package update
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestCompare(t *testing.T) {
|
func TestCompare(t *testing.T) {
|
||||||
if Compare("1.1.0", "1.0.0") <= 0 {
|
if Compare("1.1.0", "1.0.0") <= 0 {
|
||||||
@@ -13,3 +16,28 @@ func TestCompare(t *testing.T) {
|
|||||||
t.Fatal("expected equal")
|
t.Fatal("expected equal")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestManifestAssetFor(t *testing.T) {
|
||||||
|
m := Manifest{
|
||||||
|
URL: "https://git.evilfox.cc/legacy.exe",
|
||||||
|
SHA256: "abc",
|
||||||
|
Platforms: map[string]PlatformAsset{
|
||||||
|
"darwin-arm64": {
|
||||||
|
URL: "https://git.evilfox.cc/arm64/Navis",
|
||||||
|
SHA256: "def",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
u, s, ok := m.AssetFor("darwin-arm64")
|
||||||
|
if !ok || u == "" || s != "def" {
|
||||||
|
t.Fatalf("darwin: %v %q %q", ok, u, s)
|
||||||
|
}
|
||||||
|
u, s, ok = m.AssetFor("windows-amd64")
|
||||||
|
if !ok || !strings.Contains(u, "legacy") || s != "abc" {
|
||||||
|
t.Fatalf("windows legacy: %v %q %q", ok, u, s)
|
||||||
|
}
|
||||||
|
_, _, ok = m.AssetFor("linux-amd64")
|
||||||
|
if ok {
|
||||||
|
t.Fatal("expected missing linux")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Apply downloads the new exe and schedules replacement after exit (Windows).
|
||||||
|
func Apply(ctx context.Context, manifestURL string) (string, error) {
|
||||||
|
latest, _, _, exe, tmp, err := prepareDownload(ctx, manifestURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dir := filepath.Dir(exe)
|
||||||
|
bat := filepath.Join(dir, "navis-update.bat")
|
||||||
|
|
||||||
|
script := "@echo off\r\n" +
|
||||||
|
"setlocal EnableExtensions\r\n" +
|
||||||
|
"cd /d \"" + dir + "\"\r\n" +
|
||||||
|
"set \"EXE=" + exe + "\"\r\n" +
|
||||||
|
"set \"NEW=" + tmp + "\"\r\n" +
|
||||||
|
":wait\r\n" +
|
||||||
|
"ping -n 2 127.0.0.1 >nul\r\n" +
|
||||||
|
"del /f /q \"%EXE%\" >nul 2>&1\r\n" +
|
||||||
|
"if exist \"%EXE%\" goto wait\r\n" +
|
||||||
|
"move /y \"%NEW%\" \"%EXE%\" >nul\r\n" +
|
||||||
|
"if not exist \"%EXE%\" exit /b 1\r\n" +
|
||||||
|
"start \"\" \"%EXE%\"\r\n" +
|
||||||
|
"exit /b 0\r\n"
|
||||||
|
if err := os.WriteFile(bat, []byte(script), 0o755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("cmd.exe", "/C", bat)
|
||||||
|
cmd.Dir = dir
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: true,
|
||||||
|
CreationFlags: 0x00000008 | 0x00000200, // DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return "", fmt.Errorf("start updater: %w", err)
|
||||||
|
}
|
||||||
|
return latest, nil
|
||||||
|
}
|
||||||
+12
-6
@@ -2,17 +2,23 @@
|
|||||||
|
|
||||||
Обновления отдаются из git-репозитория:
|
Обновления отдаются из git-репозитория:
|
||||||
|
|
||||||
- `https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json`
|
- Manifest: `https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json`
|
||||||
- `https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe`
|
- Windows: `…/dist/navis-release/Navis.exe`
|
||||||
|
- macOS arm64: `…/dist/navis-release/darwin-arm64/Navis`
|
||||||
|
- macOS amd64: `…/dist/navis-release/darwin-amd64/Navis`
|
||||||
|
|
||||||
Пример `dist/update.json`:
|
Пример `dist/update.json` (multi-platform + legacy url для старых клиентов):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"version": "1.1.2",
|
"version": "1.4.0",
|
||||||
"notes": "Что нового",
|
"notes": "Что нового",
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "hex",
|
"sha256": "hex-windows",
|
||||||
"mandatory": false
|
"platforms": {
|
||||||
|
"windows-amd64": { "url": "…/Navis.exe", "sha256": "…" },
|
||||||
|
"darwin-arm64": { "url": "…/darwin-arm64/Navis", "sha256": "…" },
|
||||||
|
"darwin-amd64": { "url": "…/darwin-amd64/Navis", "sha256": "…" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
+24
-4
@@ -1,10 +1,30 @@
|
|||||||
{
|
{
|
||||||
"version": "1.3.1",
|
"version": "1.4.0",
|
||||||
"notes": "Фикс пинга Hysteria2 (UDP вместо TCP) и открытие ссылки evilfox.win",
|
"notes": "macOS CLI (arm64/amd64), multi-platform updates, networksetup proxy, naive tar.xz",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "88ddd38b0324979cae882c6cb0b8e4298c98b88547e72649587106425201e09e",
|
"sha256": "110ab8f5806f2635b39ec586d8218120d0041a82212715530ae8c3838078bf11",
|
||||||
"mandatory": false
|
"mandatory": false,
|
||||||
|
"platforms": {
|
||||||
|
"windows-amd64": {
|
||||||
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
|
"sha256": "110ab8f5806f2635b39ec586d8218120d0041a82212715530ae8c3838078bf11",
|
||||||
|
"os": "windows",
|
||||||
|
"arch": "amd64"
|
||||||
|
},
|
||||||
|
"darwin-arm64": {
|
||||||
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-arm64/Navis",
|
||||||
|
"sha256": "4ce2b429fe4c8e8e1fdf776150003e42e4452e9ecf95cb55a714f84884717fd6",
|
||||||
|
"os": "darwin",
|
||||||
|
"arch": "arm64"
|
||||||
|
},
|
||||||
|
"darwin-amd64": {
|
||||||
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/darwin-amd64/Navis",
|
||||||
|
"sha256": "a91bc03c7c68d896c42bb3520010e96191c12a0e95e2b9c2582dd2732637d9af",
|
||||||
|
"os": "darwin",
|
||||||
|
"arch": "amd64"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"FixedFileInfo": {
|
"FixedFileInfo": {
|
||||||
"FileVersion": { "Major": 1, "Minor": 3, "Patch": 1, "Build": 0 },
|
"FileVersion": { "Major": 1, "Minor": 4, "Patch": 0, "Build": 0 },
|
||||||
"ProductVersion": { "Major": 1, "Minor": 3, "Patch": 1, "Build": 0 },
|
"ProductVersion": { "Major": 1, "Minor": 4, "Patch": 0, "Build": 0 },
|
||||||
"FileFlagsMask": "3f",
|
"FileFlagsMask": "3f",
|
||||||
"FileFlags": "00",
|
"FileFlags": "00",
|
||||||
"FileOS": "40004",
|
"FileOS": "40004",
|
||||||
@@ -10,12 +10,12 @@
|
|||||||
},
|
},
|
||||||
"StringFileInfo": {
|
"StringFileInfo": {
|
||||||
"CompanyName": "Navis",
|
"CompanyName": "Navis",
|
||||||
"FileDescription": "Navis — NaiveProxy / Hysteria 2 client for Windows",
|
"FileDescription": "Navis — NaiveProxy / Hysteria 2 client for Windows and macOS",
|
||||||
"FileVersion": "1.3.1.0",
|
"FileVersion": "1.4.0.0",
|
||||||
"InternalName": "Navis",
|
"InternalName": "Navis",
|
||||||
"OriginalFilename": "Navis.exe",
|
"OriginalFilename": "Navis.exe",
|
||||||
"ProductName": "Navis",
|
"ProductName": "Navis",
|
||||||
"ProductVersion": "1.3.1.0"
|
"ProductVersion": "1.4.0.0"
|
||||||
},
|
},
|
||||||
"VarFileInfo": {
|
"VarFileInfo": {
|
||||||
"Translation": {
|
"Translation": {
|
||||||
|
|||||||
Reference in New Issue
Block a user