feat: update external-ui
This commit is contained in:
parent
4407cc44f4
commit
c79c2f48f9
6 changed files with 75 additions and 53 deletions
|
@ -8,6 +8,8 @@ import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -277,6 +279,8 @@ type RawConfig struct {
|
||||||
ExternalController string `yaml:"external-controller"`
|
ExternalController string `yaml:"external-controller"`
|
||||||
ExternalControllerTLS string `yaml:"external-controller-tls"`
|
ExternalControllerTLS string `yaml:"external-controller-tls"`
|
||||||
ExternalUI string `yaml:"external-ui"`
|
ExternalUI string `yaml:"external-ui"`
|
||||||
|
ExternalUIURL string `yaml:"external-ui-url" json:"external-ui-url"`
|
||||||
|
ExternalUIName string `yaml:"external-ui-name" json:"external-ui-name"`
|
||||||
Secret string `yaml:"secret"`
|
Secret string `yaml:"secret"`
|
||||||
Interface string `yaml:"interface-name"`
|
Interface string `yaml:"interface-name"`
|
||||||
RoutingMark int `yaml:"routing-mark"`
|
RoutingMark int `yaml:"routing-mark"`
|
||||||
|
@ -569,7 +573,6 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseGeneral(cfg *RawConfig) (*General, error) {
|
func parseGeneral(cfg *RawConfig) (*General, error) {
|
||||||
externalUI := cfg.ExternalUI
|
|
||||||
geodata.SetLoader(cfg.GeodataLoader)
|
geodata.SetLoader(cfg.GeodataLoader)
|
||||||
C.GeoIpUrl = cfg.GeoXUrl.GeoIp
|
C.GeoIpUrl = cfg.GeoXUrl.GeoIp
|
||||||
C.GeoSiteUrl = cfg.GeoXUrl.GeoSite
|
C.GeoSiteUrl = cfg.GeoXUrl.GeoSite
|
||||||
|
@ -580,14 +583,34 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
|
||||||
N.KeepAliveInterval = time.Duration(cfg.KeepAliveInterval) * time.Second
|
N.KeepAliveInterval = time.Duration(cfg.KeepAliveInterval) * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugln("TCP Keep Alive Interval set %+v", N.KeepAliveInterval)
|
if cfg.ExternalUIURL != "" {
|
||||||
|
ExternalUIURL = cfg.ExternalUIURL
|
||||||
|
}
|
||||||
|
ExternalUIPath = cfg.ExternalUI
|
||||||
// checkout externalUI exist
|
// checkout externalUI exist
|
||||||
if externalUI != "" {
|
if ExternalUIPath != "" {
|
||||||
externalUI = C.Path.Resolve(externalUI)
|
ExternalUIPath = C.Path.Resolve(ExternalUIPath)
|
||||||
if _, err := os.Stat(externalUI); os.IsNotExist(err) {
|
if _, err := os.Stat(ExternalUIPath); os.IsNotExist(err) {
|
||||||
return nil, fmt.Errorf("external-ui: %s not exist", externalUI)
|
defaultUIpath := path.Join(C.Path.HomeDir(), "ui")
|
||||||
|
log.Warnln("external-ui: %s does not exist, creating folder in %s", ExternalUIPath, defaultUIpath)
|
||||||
|
if err := os.MkdirAll(defaultUIpath, os.ModePerm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ExternalUIPath = defaultUIpath
|
||||||
|
cfg.ExternalUI = defaultUIpath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// checkout UIpath/name exist
|
||||||
|
if cfg.ExternalUIName != "" {
|
||||||
|
ExternalUIName = cfg.ExternalUIName
|
||||||
|
ExternalUIFolder = filepath.Clean(path.Join(ExternalUIPath, cfg.ExternalUIName))
|
||||||
|
if _, err := os.Stat(ExternalUIPath); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(ExternalUIPath, os.ModePerm); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cfg.Tun.RedirectToTun = cfg.EBpf.RedirectToTun
|
cfg.Tun.RedirectToTun = cfg.EBpf.RedirectToTun
|
||||||
return &General{
|
return &General{
|
||||||
Inbound: Inbound{
|
Inbound: Inbound{
|
||||||
|
|
|
@ -9,112 +9,109 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const xdURL = "https://codeload.github.com/MetaCubeX/metacubexd/zip/refs/heads/gh-pages"
|
var (
|
||||||
|
ExternalUIURL string
|
||||||
|
ExternalUIPath string
|
||||||
|
ExternalUIFolder string
|
||||||
|
ExternalUIName string
|
||||||
|
)
|
||||||
|
|
||||||
var xdMutex sync.Mutex
|
var xdMutex sync.Mutex
|
||||||
|
|
||||||
func UpdateXD() error {
|
func UpdateUI() error {
|
||||||
xdMutex.Lock()
|
xdMutex.Lock()
|
||||||
defer xdMutex.Unlock()
|
defer xdMutex.Unlock()
|
||||||
|
|
||||||
err := cleanup(C.UIPath)
|
if ExternalUIPath == "" || ExternalUIFolder == "" || ExternalUIName == "" {
|
||||||
|
return fmt.Errorf("ExternalUI configure incomplete")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cleanup(ExternalUIFolder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("cleanup exist file error: %w", err)
|
return fmt.Errorf("cleanup exist file error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := downloadForBytes(xdURL)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't download XD file: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saved := path.Join(C.UIPath, "xd.zip")
|
data, err := downloadForBytes(ExternalUIURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't download file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
saved := path.Join(ExternalUIPath, "download.zip")
|
||||||
if saveFile(data, saved) != nil {
|
if saveFile(data, saved) != nil {
|
||||||
return fmt.Errorf("can't save XD zip file: %w", err)
|
return fmt.Errorf("can't save zip file: %w", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(saved)
|
defer os.Remove(saved)
|
||||||
|
|
||||||
err = unzip(saved, C.UIPath)
|
unzipFolder, err := unzip(saved, ExternalUIPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't extract XD zip file: %w", err)
|
return fmt.Errorf("can't extract zip file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Rename(path.Join(C.UIPath, "metacubexd-gh-pages"), path.Join(C.UIPath, "xd"))
|
err = os.Rename(unzipFolder, ExternalUIFolder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't rename folder: %w", err)
|
return fmt.Errorf("can't rename folder: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func unzip(src, dest string) error {
|
func unzip(src, dest string) (string, error) {
|
||||||
r, err := zip.OpenReader(src)
|
r, err := zip.OpenReader(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
var extractedFolder string
|
||||||
for _, f := range r.File {
|
for _, f := range r.File {
|
||||||
fpath := filepath.Join(dest, f.Name)
|
fpath := filepath.Join(dest, f.Name)
|
||||||
|
|
||||||
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
||||||
return fmt.Errorf("invalid file path: %s", fpath)
|
return "", fmt.Errorf("invalid file path: %s", fpath)
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.FileInfo().IsDir() {
|
if f.FileInfo().IsDir() {
|
||||||
os.MkdirAll(fpath, os.ModePerm)
|
os.MkdirAll(fpath, os.ModePerm)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
rc, err := f.Open()
|
rc, err := f.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.Copy(outFile, rc)
|
_, err = io.Copy(outFile, rc)
|
||||||
|
|
||||||
outFile.Close()
|
outFile.Close()
|
||||||
rc.Close()
|
rc.Close()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
|
}
|
||||||
|
if extractedFolder == "" {
|
||||||
|
extractedFolder = filepath.Dir(fpath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return extractedFolder, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanup(root string) error {
|
func cleanup(root string) error {
|
||||||
|
if _, err := os.Stat(root); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if path == root {
|
|
||||||
// skip root itself
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
if err := os.RemoveAll(path); err != nil {
|
if err := os.RemoveAll(path); err != nil {
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if err := os.Remove(path); err != nil {
|
if err := os.Remove(path); err != nil {
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -15,7 +15,6 @@ const Name = "clash"
|
||||||
var (
|
var (
|
||||||
GeositeName = "GeoSite.dat"
|
GeositeName = "GeoSite.dat"
|
||||||
GeoipName = "GeoIP.dat"
|
GeoipName = "GeoIP.dat"
|
||||||
UIPath = ""
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Path is used to get the configuration path
|
// Path is used to get the configuration path
|
||||||
|
|
|
@ -41,7 +41,11 @@ external-controller-tls: 0.0.0.0:9443 # RESTful API HTTPS 监听地址,需要
|
||||||
# secret: "123456" # `Authorization:Bearer ${secret}`
|
# secret: "123456" # `Authorization:Bearer ${secret}`
|
||||||
|
|
||||||
# tcp-concurrent: true # TCP 并发连接所有 IP, 将使用最快握手的 TCP
|
# tcp-concurrent: true # TCP 并发连接所有 IP, 将使用最快握手的 TCP
|
||||||
external-ui: /path/to/ui/folder # 配置 WEB UI 目录,使用 http://{{external-controller}}/ui 访问
|
|
||||||
|
# 配置 WEB UI 目录,使用 http://{{external-controller}}/ui 访问
|
||||||
|
external-ui: /path/to/ui/folder/
|
||||||
|
external-ui-name: xd
|
||||||
|
external-ui-url: "https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip"
|
||||||
|
|
||||||
# interface-name: en0 # 设置出口网卡
|
# interface-name: en0 # 设置出口网卡
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,6 @@ type Memory struct {
|
||||||
|
|
||||||
func SetUIPath(path string) {
|
func SetUIPath(path string) {
|
||||||
uiPath = C.Path.Resolve(path)
|
uiPath = C.Path.Resolve(path)
|
||||||
C.UIPath = uiPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(addr string, tlsAddr string, secret string,
|
func Start(addr string, tlsAddr string, secret string,
|
||||||
|
|
|
@ -16,7 +16,7 @@ import (
|
||||||
func upgradeRouter() http.Handler {
|
func upgradeRouter() http.Handler {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Post("/", upgrade)
|
r.Post("/", upgrade)
|
||||||
r.Post("/xd", updateXD)
|
r.Post("/ui", updateUI)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,8 +46,8 @@ func upgrade(w http.ResponseWriter, r *http.Request) {
|
||||||
go restartExecutable(execPath)
|
go restartExecutable(execPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateXD(w http.ResponseWriter, r *http.Request) {
|
func updateUI(w http.ResponseWriter, r *http.Request) {
|
||||||
err := config.UpdateXD()
|
err := config.UpdateUI()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnln("%s", err)
|
log.Warnln("%s", err)
|
||||||
render.Status(r, http.StatusInternalServerError)
|
render.Status(r, http.StatusInternalServerError)
|
||||||
|
|
Loading…
Reference in a new issue