169 lines
4.4 KiB
Go
169 lines
4.4 KiB
Go
package xray
|
|
|
|
import (
|
|
"archive/zip"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"vpnclient/internal/coredl"
|
|
)
|
|
|
|
const githubAPILatest = "https://api.github.com/repos/XTLS/Xray-core/releases/latest"
|
|
|
|
// ResolveBinary finds xray executable.
|
|
func ResolveBinary(binDir string) (string, error) {
|
|
for _, name := range candidateNames() {
|
|
candidates := []string{
|
|
filepath.Join(binDir, name),
|
|
filepath.Join(binDir, "xray", name),
|
|
}
|
|
if exe, err := os.Executable(); err == nil {
|
|
dir := filepath.Dir(exe)
|
|
candidates = append(candidates, filepath.Join(dir, name), filepath.Join(dir, "bin", name))
|
|
}
|
|
for _, c := range candidates {
|
|
if st, err := os.Stat(c); err == nil && !st.IsDir() {
|
|
return c, nil
|
|
}
|
|
}
|
|
}
|
|
return "", fmt.Errorf("xray binary not found in %s; run install-core", binDir)
|
|
}
|
|
|
|
func candidateNames() []string {
|
|
if runtime.GOOS == "windows" {
|
|
return []string{"xray.exe", "Xray.exe"}
|
|
}
|
|
return []string{"xray", "Xray"}
|
|
}
|
|
|
|
// EnsureBinary downloads official Xray-core release if missing.
|
|
func EnsureBinary(binDir string) (string, error) {
|
|
if path, err := ResolveBinary(binDir); err == nil {
|
|
return path, nil
|
|
}
|
|
if err := os.MkdirAll(binDir, 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
want, err := releaseZipName()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
fmt.Fprintf(os.Stderr, "downloading official Xray-core (%s)...\n", want)
|
|
asset, all, err := findReleaseAsset(want)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sum, err := coredl.ResolveSHA(asset, all)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
zipPath := filepath.Join(binDir, asset.Name+".download")
|
|
_ = os.Remove(zipPath)
|
|
if err := coredl.DownloadAndVerify(zipPath, asset.BrowserDownloadURL, sum); err != nil {
|
|
return "", err
|
|
}
|
|
defer os.Remove(zipPath)
|
|
|
|
destName := "xray"
|
|
if runtime.GOOS == "windows" {
|
|
destName = "xray.exe"
|
|
}
|
|
dest := filepath.Join(binDir, destName)
|
|
if err := extractNamedFromZip(zipPath, destName, dest); err != nil {
|
|
if err2 := extractXrayFromZip(zipPath, dest); err2 != nil {
|
|
return "", fmt.Errorf("%v; %w", err, err2)
|
|
}
|
|
}
|
|
_ = os.Chmod(dest, 0o755)
|
|
if runtime.GOOS == "darwin" {
|
|
_ = exec.Command("xattr", "-dr", "com.apple.quarantine", dest).Run()
|
|
}
|
|
return ResolveBinary(binDir)
|
|
}
|
|
|
|
func releaseZipName() (string, error) {
|
|
switch {
|
|
case runtime.GOOS == "windows" && runtime.GOARCH == "amd64":
|
|
return "Xray-windows-64.zip", nil
|
|
case runtime.GOOS == "windows" && runtime.GOARCH == "arm64":
|
|
return "Xray-windows-arm64-v8a.zip", nil
|
|
case runtime.GOOS == "darwin" && runtime.GOARCH == "amd64":
|
|
return "Xray-macos-64.zip", nil
|
|
case runtime.GOOS == "darwin" && runtime.GOARCH == "arm64":
|
|
return "Xray-macos-arm64-v8a.zip", nil
|
|
case runtime.GOOS == "linux" && runtime.GOARCH == "amd64":
|
|
return "Xray-linux-64.zip", nil
|
|
case runtime.GOOS == "linux" && runtime.GOARCH == "arm64":
|
|
return "Xray-linux-arm64-v8a.zip", nil
|
|
default:
|
|
return "", fmt.Errorf("unsupported platform %s/%s for xray auto-download", runtime.GOOS, runtime.GOARCH)
|
|
}
|
|
}
|
|
|
|
func findReleaseAsset(exactName string) (coredl.ReleaseAsset, []coredl.ReleaseAsset, error) {
|
|
_, assets, err := coredl.FetchLatestAssets(githubAPILatest)
|
|
if err != nil {
|
|
return coredl.ReleaseAsset{}, nil, err
|
|
}
|
|
for _, a := range assets {
|
|
if a.Name == exactName || strings.EqualFold(a.Name, exactName) {
|
|
return a, assets, nil
|
|
}
|
|
}
|
|
return coredl.ReleaseAsset{}, assets, fmt.Errorf("no asset %s in xray release", exactName)
|
|
}
|
|
|
|
func extractNamedFromZip(zipPath, wantName, dest string) error {
|
|
r, err := zip.OpenReader(zipPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
wantBase := strings.ToLower(filepath.Base(wantName))
|
|
for _, f := range r.File {
|
|
base := strings.ToLower(filepath.Base(f.Name))
|
|
if base != wantBase {
|
|
continue
|
|
}
|
|
return writeZipFile(f, dest)
|
|
}
|
|
return fmt.Errorf("%s not found in zip", wantName)
|
|
}
|
|
|
|
func extractXrayFromZip(zipPath, dest string) error {
|
|
r, err := zip.OpenReader(zipPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
for _, f := range r.File {
|
|
base := strings.ToLower(filepath.Base(f.Name))
|
|
if base == "xray" || base == "xray.exe" {
|
|
return writeZipFile(f, dest)
|
|
}
|
|
}
|
|
return fmt.Errorf("xray binary not found in archive")
|
|
}
|
|
|
|
func writeZipFile(f *zip.File, dest string) error {
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rc.Close()
|
|
_ = os.Remove(dest)
|
|
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
_, err = io.Copy(out, io.LimitReader(rc, 80<<20))
|
|
return err
|
|
}
|