Skip to content

Commit

Permalink
clash api: download clash-dashboard if external-ui directory is empty
Browse files Browse the repository at this point in the history
  • Loading branch information
nekohasekai committed Apr 22, 2023
1 parent f98cfdf commit eb57cbc
Show file tree
Hide file tree
Showing 10 changed files with 380 additions and 13 deletions.
114 changes: 114 additions & 0 deletions common/badversion/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package badversion

import (
"strconv"
"strings"

F "github.com/sagernet/sing/common/format"
)

type Version struct {
Major int
Minor int
Patch int
PreReleaseIdentifier string
PreReleaseVersion int
}

func (v Version) After(anotherVersion Version) bool {
if v.Major > anotherVersion.Major {
return true
} else if v.Major < anotherVersion.Major {
return false
}
if v.Minor > anotherVersion.Minor {
return true
} else if v.Minor < anotherVersion.Minor {
return false
}
if v.Patch > anotherVersion.Patch {
return true
} else if v.Patch < anotherVersion.Patch {
return false
}
if v.PreReleaseIdentifier == "" && anotherVersion.PreReleaseIdentifier != "" {
return true
} else if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier == "" {
return false
}
if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier != "" {
if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
return true
} else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" {
return false
}
if v.PreReleaseVersion > anotherVersion.PreReleaseVersion {
return true
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
return false
}
}
return false
}

func (v Version) String() string {
version := F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
if v.PreReleaseIdentifier != "" {
version = F.ToString(version, "-", v.PreReleaseIdentifier, ".", v.PreReleaseVersion)
}
return version
}

func (v Version) BadString() string {
version := F.ToString(v.Major, ".", v.Minor)
if v.Patch > 0 {
version = F.ToString(version, ".", v.Patch)
}
if v.PreReleaseIdentifier != "" {
version = F.ToString(version, "-", v.PreReleaseIdentifier)
if v.PreReleaseVersion > 0 {
version = F.ToString(version, v.PreReleaseVersion)
}
}
return version
}

func Parse(versionName string) (version Version) {
if strings.HasPrefix(versionName, "v") {
versionName = versionName[1:]
}
if strings.Contains(versionName, "-") {
parts := strings.Split(versionName, "-")
versionName = parts[0]
identifier := parts[1]
if strings.Contains(identifier, ".") {
identifierParts := strings.Split(identifier, ".")
version.PreReleaseIdentifier = identifierParts[0]
if len(identifierParts) >= 2 {
version.PreReleaseVersion, _ = strconv.Atoi(identifierParts[1])
}
} else {
if strings.HasPrefix(identifier, "alpha") {
version.PreReleaseIdentifier = "alpha"
version.PreReleaseVersion, _ = strconv.Atoi(identifier[5:])
} else if strings.HasPrefix(identifier, "beta") {
version.PreReleaseIdentifier = "beta"
version.PreReleaseVersion, _ = strconv.Atoi(identifier[4:])
} else {
version.PreReleaseIdentifier = identifier
}
}
}
versionElements := strings.Split(versionName, ".")
versionLen := len(versionElements)
if versionLen >= 1 {
version.Major, _ = strconv.Atoi(versionElements[0])
}
if versionLen >= 2 {
version.Minor, _ = strconv.Atoi(versionElements[1])
}
if versionLen >= 3 {
version.Patch, _ = strconv.Atoi(versionElements[2])
}
return
}
17 changes: 17 additions & 0 deletions common/badversion/version_json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package badversion

import "github.com/sagernet/sing-box/common/json"

func (v Version) MarshalJSON() ([]byte, error) {
return json.Marshal(v.String())
}

func (v *Version) UnmarshalJSON(data []byte) error {
var version string
err := json.Unmarshal(data, &version)
if err != nil {
return err
}
*v = Parse(version)
return nil
}
18 changes: 18 additions & 0 deletions common/badversion/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package badversion

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestCompareVersion(t *testing.T) {
t.Parallel()
require.Equal(t, "1.3.0-beta.1", Parse("v1.3.0-beta1").String())
require.Equal(t, "1.3-beta1", Parse("v1.3.0-beta.1").BadString())
require.True(t, Parse("1.3.0").After(Parse("1.3-beta1")))
require.True(t, Parse("1.3.0").After(Parse("1.3.0-beta1")))
require.True(t, Parse("1.3.0-beta1").After(Parse("1.3.0-alpha1")))
require.True(t, Parse("1.3.1").After(Parse("1.3.0")))
require.True(t, Parse("1.4").After(Parse("1.3")))
}
12 changes: 12 additions & 0 deletions constant/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const dirName = "sing-box"

var (
basePath string
tempPath string
resourcePaths []string
)

Expand All @@ -22,10 +23,21 @@ func BasePath(name string) string {
return filepath.Join(basePath, name)
}

func CreateTemp(pattern string) (*os.File, error) {
if tempPath == "" {
tempPath = os.TempDir()
}
return os.CreateTemp(tempPath, pattern)
}

func SetBasePath(path string) {
basePath = path
}

func SetTempPath(path string) {
tempPath = path
}

func FindPath(name string) (string, bool) {
name = os.ExpandEnv(name)
if rw.FileExists(name) {
Expand Down
14 changes: 14 additions & 0 deletions docs/configuration/experimental/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "folder",
"external_ui_download_url": "",
"external_ui_download_detour": "",
"secret": "",
"default_mode": "rule",
"store_selected": false,
Expand Down Expand Up @@ -53,6 +55,18 @@ A relative path to the configuration directory or an absolute path to a
directory in which you put some static web resource. sing-box will then
serve it at `http://{{external-controller}}/ui`.

#### external_ui_download_url

ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty.

`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty.

#### external_ui_download_detour

The tag of the outbound to download the external UI.

Default outbound will be used if empty.

#### secret

Secret for the RESTful API (optional)
Expand Down
14 changes: 14 additions & 0 deletions docs/configuration/experimental/index.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "folder",
"external_ui_download_url": "",
"external_ui_download_detour": "",
"secret": "",
"default_mode": "rule",
"store_selected": false,
Expand Down Expand Up @@ -51,6 +53,18 @@ RESTful web API 监听地址。如果为空,则禁用 Clash API。

到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。

#### external_ui_download_url

静态网页资源的 ZIP 下载 URL,如果指定的 `external_ui` 目录为空,将使用。

默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`

#### external_ui_download_detour

用于下载静态网页资源的出站的标签。

如果为空,将使用默认出站。

#### secret

RESTful API 的密钥(可选)
Expand Down
20 changes: 14 additions & 6 deletions experimental/clashapi/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ type Server struct {
storeFakeIP bool
cacheFilePath string
cacheFile adapter.ClashCacheFile

externalUI string
externalUIDownloadURL string
externalUIDownloadDetour string
}

func NewServer(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
Expand All @@ -59,11 +63,13 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
Addr: options.ExternalController,
Handler: chiRouter,
},
trafficManager: trafficManager,
urlTestHistory: urltest.NewHistoryStorage(),
mode: strings.ToLower(options.DefaultMode),
storeSelected: options.StoreSelected,
storeFakeIP: options.StoreFakeIP,
trafficManager: trafficManager,
urlTestHistory: urltest.NewHistoryStorage(),
mode: strings.ToLower(options.DefaultMode),
storeSelected: options.StoreSelected,
storeFakeIP: options.StoreFakeIP,
externalUIDownloadURL: options.ExternalUIDownloadURL,
externalUIDownloadDetour: options.ExternalUIDownloadDetour,
}
if server.mode == "" {
server.mode = "rule"
Expand Down Expand Up @@ -105,8 +111,9 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
r.Mount("/dns", dnsRouter(router))
})
if options.ExternalUI != "" {
server.externalUI = C.BasePath(os.ExpandEnv(options.ExternalUI))
chiRouter.Group(func(r chi.Router) {
fs := http.StripPrefix("/ui", http.FileServer(http.Dir(C.BasePath(os.ExpandEnv(options.ExternalUI)))))
fs := http.StripPrefix("/ui", http.FileServer(http.Dir(server.externalUI)))
r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP)
r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) {
fs.ServeHTTP(w, r)
Expand All @@ -128,6 +135,7 @@ func (s *Server) PreStart() error {
}

func (s *Server) Start() error {
s.checkAndDownloadExternalUI()
listener, err := net.Listen("tcp", s.httpServer.Addr)
if err != nil {
return E.Cause(err, "external controller listen error")
Expand Down

2 comments on commit eb57cbc

@fxzxmicah

This comment was marked as spam.

@fxzxmicah

This comment was marked as spam.

Please sign in to comment.