1
0

server_resources.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. package clashapi
  2. import (
  3. "archive/zip"
  4. "context"
  5. "crypto/tls"
  6. "io"
  7. "net"
  8. "net/http"
  9. "os"
  10. "path/filepath"
  11. "strings"
  12. "github.com/sagernet/sing-box/adapter"
  13. C "github.com/sagernet/sing-box/constant"
  14. "github.com/sagernet/sing/common"
  15. E "github.com/sagernet/sing/common/exceptions"
  16. M "github.com/sagernet/sing/common/metadata"
  17. "github.com/sagernet/sing/common/ntp"
  18. "github.com/sagernet/sing/service/filemanager"
  19. )
  20. func (s *Server) checkAndDownloadExternalUI() {
  21. if s.externalUI == "" {
  22. return
  23. }
  24. entries, err := os.ReadDir(s.externalUI)
  25. if err != nil {
  26. os.MkdirAll(s.externalUI, 0o755)
  27. }
  28. if len(entries) == 0 {
  29. err = s.downloadExternalUI()
  30. if err != nil {
  31. s.logger.Error("download external ui error: ", err)
  32. }
  33. }
  34. }
  35. func (s *Server) downloadExternalUI() error {
  36. var downloadURL string
  37. if s.externalUIDownloadURL != "" {
  38. downloadURL = s.externalUIDownloadURL
  39. } else {
  40. downloadURL = "https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip"
  41. }
  42. var detour adapter.Outbound
  43. if s.externalUIDownloadDetour != "" {
  44. outbound, loaded := s.outbound.Outbound(s.externalUIDownloadDetour)
  45. if !loaded {
  46. return E.New("detour outbound not found: ", s.externalUIDownloadDetour)
  47. }
  48. detour = outbound
  49. } else {
  50. outbound := s.outbound.Default()
  51. detour = outbound
  52. }
  53. s.logger.Info("downloading external ui using outbound/", detour.Type(), "[", detour.Tag(), "]")
  54. httpClient := &http.Client{
  55. Transport: &http.Transport{
  56. ForceAttemptHTTP2: true,
  57. TLSHandshakeTimeout: C.TCPTimeout,
  58. DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
  59. return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
  60. },
  61. TLSClientConfig: &tls.Config{
  62. Time: ntp.TimeFuncFromContext(s.ctx),
  63. RootCAs: adapter.RootPoolFromContext(s.ctx),
  64. },
  65. },
  66. }
  67. defer httpClient.CloseIdleConnections()
  68. response, err := httpClient.Get(downloadURL)
  69. if err != nil {
  70. return err
  71. }
  72. defer response.Body.Close()
  73. if response.StatusCode != http.StatusOK {
  74. return E.New("download external ui failed: ", response.Status)
  75. }
  76. err = s.downloadZIP(response.Body, s.externalUI)
  77. if err != nil {
  78. removeAllInDirectory(s.externalUI)
  79. }
  80. return err
  81. }
  82. func (s *Server) downloadZIP(body io.Reader, output string) error {
  83. tempFile, err := filemanager.CreateTemp(s.ctx, "external-ui.zip")
  84. if err != nil {
  85. return err
  86. }
  87. defer os.Remove(tempFile.Name())
  88. _, err = io.Copy(tempFile, body)
  89. tempFile.Close()
  90. if err != nil {
  91. return err
  92. }
  93. reader, err := zip.OpenReader(tempFile.Name())
  94. if err != nil {
  95. return err
  96. }
  97. defer reader.Close()
  98. trimDir := zipIsInSingleDirectory(reader.File)
  99. for _, file := range reader.File {
  100. if file.FileInfo().IsDir() {
  101. continue
  102. }
  103. pathElements := strings.Split(file.Name, "/")
  104. if trimDir {
  105. pathElements = pathElements[1:]
  106. }
  107. saveDirectory := output
  108. if len(pathElements) > 1 {
  109. saveDirectory = filepath.Join(saveDirectory, filepath.Join(pathElements[:len(pathElements)-1]...))
  110. }
  111. err = os.MkdirAll(saveDirectory, 0o755)
  112. if err != nil {
  113. return err
  114. }
  115. savePath := filepath.Join(saveDirectory, pathElements[len(pathElements)-1])
  116. err = downloadZIPEntry(s.ctx, file, savePath)
  117. if err != nil {
  118. return err
  119. }
  120. }
  121. return nil
  122. }
  123. func downloadZIPEntry(ctx context.Context, zipFile *zip.File, savePath string) error {
  124. saveFile, err := filemanager.Create(ctx, savePath)
  125. if err != nil {
  126. return err
  127. }
  128. defer saveFile.Close()
  129. reader, err := zipFile.Open()
  130. if err != nil {
  131. return err
  132. }
  133. defer reader.Close()
  134. return common.Error(io.Copy(saveFile, reader))
  135. }
  136. func removeAllInDirectory(directory string) {
  137. dirEntries, err := os.ReadDir(directory)
  138. if err != nil {
  139. return
  140. }
  141. for _, dirEntry := range dirEntries {
  142. os.RemoveAll(filepath.Join(directory, dirEntry.Name()))
  143. }
  144. }
  145. func zipIsInSingleDirectory(files []*zip.File) bool {
  146. var singleDirectory string
  147. for _, file := range files {
  148. if file.FileInfo().IsDir() {
  149. continue
  150. }
  151. pathElements := strings.Split(file.Name, "/")
  152. if len(pathElements) == 0 {
  153. return false
  154. }
  155. if singleDirectory == "" {
  156. singleDirectory = pathElements[0]
  157. } else if singleDirectory != pathElements[0] {
  158. return false
  159. }
  160. }
  161. return true
  162. }