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