package vless import ( "archive/zip" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "runtime" "strings" "time" ) const xrayLatestReleaseAPI = "https://api.github.com/repos/XTLS/Xray-core/releases/latest" type xrayRelease struct { TagName string `json:"tag_name"` Assets []xrayReleaseAsset `json:"assets"` } type xrayReleaseAsset struct { Name string `json:"name"` BrowserDownloadURL string `json:"browser_download_url"` } func ensureXrayBinary(xrayDir string) (string, error) { exeName := "xray" if runtime.GOOS == "windows" { exeName = "xray.exe" } xrayPath := filepath.Join(xrayDir, exeName) if _, err := os.Stat(xrayPath); err == nil { return xrayPath, nil } if err := os.MkdirAll(xrayDir, 0755); err != nil { return "", fmt.Errorf("ошибка создания папки xray: %w", err) } asset, err := fetchLatestXrayAsset() if err != nil { return "", err } archivePath, err := downloadXrayArchive(asset.BrowserDownloadURL, xrayDir, asset.Name) if err != nil { return "", err } defer os.Remove(archivePath) if err := unzipXrayArchive(archivePath, xrayDir); err != nil { return "", err } if runtime.GOOS != "windows" { _ = os.Chmod(xrayPath, 0755) } if _, err := os.Stat(xrayPath); err != nil { return "", fmt.Errorf("xray не найден после распаковки в %s", xrayDir) } return xrayPath, nil } func fetchLatestXrayAsset() (*xrayReleaseAsset, error) { client := &http.Client{Timeout: 45 * time.Second} req, err := http.NewRequest(http.MethodGet, xrayLatestReleaseAPI, nil) if err != nil { return nil, fmt.Errorf("ошибка подготовки запроса релиза Xray: %w", err) } req.Header.Set("User-Agent", "go-vpn-client") req.Header.Set("Accept", "application/vnd.github+json") resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("ошибка получения релиза Xray: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) return nil, fmt.Errorf("ошибка API Xray: %d %s", resp.StatusCode, strings.TrimSpace(string(body))) } var release xrayRelease if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { return nil, fmt.Errorf("ошибка разбора релиза Xray: %w", err) } for _, asset := range release.Assets { if matchesXrayAsset(asset.Name) { return &asset, nil } } return nil, fmt.Errorf("не найден подходящий архив Xray для %s/%s в релизе %s", runtime.GOOS, runtime.GOARCH, release.TagName) } func matchesXrayAsset(name string) bool { lower := strings.ToLower(name) if !strings.HasSuffix(lower, ".zip") || strings.Contains(lower, ".dgst") { return false } osToken := map[string]string{ "windows": "windows", "linux": "linux", "darwin": "macos", }[runtime.GOOS] if osToken == "" || !strings.Contains(lower, osToken) { return false } archTokens := map[string][]string{ "amd64": {"64", "amd64", "x64"}, "386": {"32", "386", "x86"}, "arm64": {"arm64", "aarch64"}, "arm": {"arm32", "armv7", "arm"}, }[runtime.GOARCH] if len(archTokens) == 0 { return false } for _, token := range archTokens { if strings.Contains(lower, token) { return true } } return false } func downloadXrayArchive(downloadURL, xrayDir, name string) (string, error) { client := &http.Client{Timeout: 2 * time.Minute} req, err := http.NewRequest(http.MethodGet, downloadURL, nil) if err != nil { return "", fmt.Errorf("ошибка подготовки загрузки Xray: %w", err) } req.Header.Set("User-Agent", "go-vpn-client") resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("ошибка загрузки Xray: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("ошибка загрузки Xray: HTTP %d", resp.StatusCode) } archivePath := filepath.Join(xrayDir, name) file, err := os.Create(archivePath) if err != nil { return "", fmt.Errorf("ошибка создания архива Xray: %w", err) } defer file.Close() if _, err := io.Copy(file, resp.Body); err != nil { return "", fmt.Errorf("ошибка сохранения архива Xray: %w", err) } return archivePath, nil } func unzipXrayArchive(archivePath, targetDir string) error { reader, err := zip.OpenReader(archivePath) if err != nil { return fmt.Errorf("ошибка открытия архива Xray: %w", err) } defer reader.Close() for _, file := range reader.File { if err := extractZipEntry(file, targetDir); err != nil { return err } } return nil } func extractZipEntry(file *zip.File, targetDir string) error { cleanName := filepath.Clean(file.Name) targetPath := filepath.Join(targetDir, cleanName) absTargetDir, err := filepath.Abs(targetDir) if err != nil { return fmt.Errorf("ошибка определения папки Xray: %w", err) } absTargetPath, err := filepath.Abs(targetPath) if err != nil { return fmt.Errorf("ошибка определения пути Xray: %w", err) } if !strings.HasPrefix(absTargetPath, absTargetDir) { return fmt.Errorf("небезопасный путь в архиве Xray: %s", file.Name) } if file.FileInfo().IsDir() { return os.MkdirAll(absTargetPath, 0755) } if err := os.MkdirAll(filepath.Dir(absTargetPath), 0755); err != nil { return fmt.Errorf("ошибка создания папки Xray: %w", err) } in, err := file.Open() if err != nil { return fmt.Errorf("ошибка чтения файла из архива Xray: %w", err) } defer in.Close() out, err := os.OpenFile(absTargetPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, file.Mode()) if err != nil { return fmt.Errorf("ошибка создания файла Xray: %w", err) } defer out.Close() if _, err := io.Copy(out, in); err != nil { return fmt.Errorf("ошибка распаковки Xray: %w", err) } return nil }