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  }