mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-03 19:55:04 +03:00
6d9e836d61
UpdateXray stopped the core before downloading, which also stopped the Panel Traffic Outbound bridge the download needed on servers that reach GitHub only through it, leaving every client offline. The new core is now downloaded, staged and checked with run -test against the current config while the old one serves; only the rename and restart swap it (Windows still stops first). update.sh gets the bridge as XUI_UPDATE_PROXY, installs x-ui.sh from the release archive, and fetches Alpine's x-ui.rc before stopping x-ui.
208 lines
6.2 KiB
Go
208 lines
6.2 KiB
Go
package service
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
)
|
|
|
|
// fakeXrayCore handles only the command lines the panel uses to run a core. It exits 99
|
|
// on any other, so a changed invocation fails the test.
|
|
func fakeXrayCore(version string, acceptsConfig bool) string {
|
|
check := "exit 0"
|
|
if !acceptsConfig {
|
|
check = "echo 'Failed to start: fake core refuses this config'; exit 23"
|
|
}
|
|
return "#!/bin/sh\n" +
|
|
"case \"$*\" in\n" +
|
|
"-version) echo 'Xray " + version + " (fake)' ;;\n" +
|
|
"'run -test -c '*) [ -s \"$4\" ] || exit 98; " + check + " ;;\n" +
|
|
"'-c '*) exec sleep 60 ;;\n" +
|
|
"*) exit 99 ;;\n" +
|
|
"esac\n"
|
|
}
|
|
|
|
func xrayReleaseZip(t *testing.T, core string) []byte {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
zw := zip.NewWriter(&buf)
|
|
w, err := zw.Create("xray")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := w.Write([]byte(core)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := zw.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// xrayReleaseServer stands in for GitHub. It lists one version and serves asset with a
|
|
// matching .dgst, or a 404 when asset is nil.
|
|
func xrayReleaseServer(t *testing.T, version string, asset []byte) {
|
|
t.Helper()
|
|
sum := sha256.Sum256(asset)
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/releases", func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, `[{"tag_name":%q}]`, version)
|
|
})
|
|
mux.HandleFunc("/download/"+version+"/", func(w http.ResponseWriter, r *http.Request) {
|
|
switch {
|
|
case asset == nil:
|
|
http.NotFound(w, r)
|
|
case strings.HasSuffix(r.URL.Path, ".zip.dgst"):
|
|
fmt.Fprintf(w, "SHA2-256= %s\n", hex.EncodeToString(sum[:]))
|
|
case strings.HasSuffix(r.URL.Path, ".zip"):
|
|
_, _ = w.Write(asset)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
t.Cleanup(srv.Close)
|
|
|
|
releases, download := xrayReleasesURL, xrayDownloadURL
|
|
xrayReleasesURL, xrayDownloadURL = srv.URL+"/releases", srv.URL+"/download"
|
|
t.Cleanup(func() { xrayReleasesURL, xrayDownloadURL = releases, download })
|
|
}
|
|
|
|
// startFakeCore runs the installed core on the panel's own config, as RestartXray
|
|
// would, and stops whichever core is current when the test ends.
|
|
func startFakeCore(t *testing.T) *xray.Process {
|
|
t.Helper()
|
|
cfg, err := (&XrayService{}).GetXrayConfig()
|
|
if err != nil {
|
|
t.Fatalf("GetXrayConfig: %v", err)
|
|
}
|
|
p := xray.NewProcess(cfg)
|
|
if err := p.Start(); err != nil {
|
|
t.Fatalf("start fake core: %v", err)
|
|
}
|
|
restore := SetXrayProcessForTest(p)
|
|
t.Cleanup(func() {
|
|
if current := XrayProcess(); current != nil {
|
|
_ = current.Stop()
|
|
}
|
|
_ = p.Stop()
|
|
restore()
|
|
// restore() keeps the hold-back reason that a refused restart sets.
|
|
xrayState.holdBack("")
|
|
})
|
|
return p
|
|
}
|
|
|
|
func TestUpdateXray(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("the fake cores are shell scripts")
|
|
}
|
|
const current, next = "26.6.27", "26.7.1"
|
|
installed := fakeXrayCore(current, true)
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
candidate string // the release's core; empty makes the download 404
|
|
conflict bool // two inbounds on one port, added after the old core starts
|
|
wantErr string
|
|
wantBinary string
|
|
wantVersion string
|
|
}{
|
|
{
|
|
// The running core hosts the panel egress bridge. Stopping it first made the
|
|
// download bypass the bridge, and when that failed every client stayed offline.
|
|
name: "failed download leaves the running core alone",
|
|
wantErr: "unexpected HTTP 404",
|
|
wantBinary: installed,
|
|
wantVersion: current,
|
|
},
|
|
{
|
|
name: "core that refuses the config is not installed",
|
|
candidate: fakeXrayCore(next, false),
|
|
wantErr: "fake core refuses this config",
|
|
wantBinary: installed,
|
|
wantVersion: current,
|
|
},
|
|
{
|
|
name: "accepted core replaces the running one",
|
|
candidate: fakeXrayCore(next, true),
|
|
wantBinary: fakeXrayCore(next, true),
|
|
wantVersion: next,
|
|
},
|
|
{
|
|
// A refused restart keeps the old core running, but the new binary is
|
|
// already on disk, so the error has to say the update is installed.
|
|
name: "refused restart reports the installed core",
|
|
candidate: fakeXrayCore(next, true),
|
|
conflict: true,
|
|
wantErr: "is installed, but the restart failed: xray config refused",
|
|
wantBinary: fakeXrayCore(next, true),
|
|
wantVersion: current,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
dbDir := t.TempDir()
|
|
t.Setenv("XUI_DB_FOLDER", dbDir)
|
|
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
|
|
binFolder := t.TempDir()
|
|
t.Setenv("XUI_BIN_FOLDER", binFolder)
|
|
t.Setenv("XUI_LOG_FOLDER", t.TempDir())
|
|
|
|
binary := filepath.Join(binFolder, xray.GetBinaryName())
|
|
if err := os.WriteFile(binary, []byte(installed), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
running := startFakeCore(t)
|
|
if tc.conflict {
|
|
for _, tag := range []string{"first", "second"} {
|
|
seedInboundConflict(t, tag, "0.0.0.0", 4443, model.VLESS, `{"network":"tcp"}`, `{}`)
|
|
}
|
|
}
|
|
|
|
var asset []byte
|
|
if tc.candidate != "" {
|
|
asset = xrayReleaseZip(t, tc.candidate)
|
|
}
|
|
xrayReleaseServer(t, "v"+next, asset)
|
|
|
|
err := (&ServerService{}).UpdateXray("v" + next)
|
|
if tc.wantErr == "" && err != nil {
|
|
t.Fatalf("UpdateXray: %v", err)
|
|
}
|
|
if tc.wantErr != "" && (err == nil || !strings.Contains(err.Error(), tc.wantErr)) {
|
|
t.Fatalf("UpdateXray error = %v, want it to contain %q", err, tc.wantErr)
|
|
}
|
|
if tc.wantErr != "" && (XrayProcess() != running || !running.IsRunning()) {
|
|
t.Fatal("a failed update stopped or replaced the running core")
|
|
}
|
|
if got := XrayProcess().GetXrayVersion(); got != tc.wantVersion {
|
|
t.Fatalf("running core version = %q, want %q", got, tc.wantVersion)
|
|
}
|
|
got, err := os.ReadFile(binary)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != tc.wantBinary {
|
|
t.Fatalf("installed core:\n%s\nwant:\n%s", got, tc.wantBinary)
|
|
}
|
|
leftovers, err := filepath.Glob(filepath.Join(binFolder, ".*"))
|
|
if err != nil || len(leftovers) > 0 {
|
|
t.Fatalf("temp files left in the bin folder: %v (glob err %v)", leftovers, err)
|
|
}
|
|
})
|
|
}
|
|
}
|