github.com/laof/lite-speed-test@v0.0.0-20230930011949-1f39b7037845/web/profile.go (about)

     1  package web
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"encoding/base64"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"log"
    14  	"net/http"
    15  	"net/url"
    16  	"os"
    17  	"regexp"
    18  	"strconv"
    19  	"strings"
    20  	"sync"
    21  	"time"
    22  
    23  	"github.com/laof/lite-speed-test/config"
    24  	"github.com/laof/lite-speed-test/download"
    25  	"github.com/laof/lite-speed-test/request"
    26  	"github.com/laof/lite-speed-test/utils"
    27  	"github.com/laof/lite-speed-test/web/render"
    28  )
    29  
    30  var (
    31  	ErrInvalidData = errors.New("invalid data")
    32  	regProfile     = regexp.MustCompile(`((?i)vmess://(\S+?)@(\S+?):([0-9]{2,5})/([?#][^\s]+))|((?i)vmess://[a-zA-Z0-9+_/=-]+([?#][^\s]+)?)|((?i)ssr://[a-zA-Z0-9+_/=-]+)|((?i)(vless|ss|trojan)://(\S+?)@(\S+?):([0-9]{2,5})/?([?#][^\s]+))|((?i)(ss)://[a-zA-Z0-9+_/=-]+([?#][^\s]+))`)
    33  )
    34  
    35  const (
    36  	PIC_BASE64 = iota
    37  	PIC_PATH
    38  	PIC_NONE
    39  	JSON_OUTPUT
    40  	TEXT_OUTPUT
    41  )
    42  
    43  type PAESE_TYPE int
    44  
    45  const (
    46  	PARSE_ANY PAESE_TYPE = iota
    47  	PARSE_URL
    48  	PARSE_FILE
    49  	PARSE_BASE64
    50  	PARSE_CLASH
    51  	PARSE_PROFILE
    52  )
    53  
    54  // support proxy
    55  // concurrency setting
    56  // as subscription server
    57  // profiles filter
    58  // clash to vmess local subscription
    59  func getSubscriptionLinks(link string) ([]string, error) {
    60  	c := http.Client{
    61  		Timeout: 20 * time.Second,
    62  	}
    63  	resp, err := c.Get(link)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	defer resp.Body.Close()
    68  	if isYamlFile(link) {
    69  		return scanClashProxies(resp.Body, true)
    70  	}
    71  	data, err := io.ReadAll(resp.Body)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  	dataStr := string(data)
    76  	msg, err := utils.DecodeB64(dataStr)
    77  	if err != nil {
    78  		if strings.Contains(dataStr, "proxies:") {
    79  			return parseClash(dataStr)
    80  		} else if strings.Contains(dataStr, "vmess://") ||
    81  			strings.Contains(dataStr, "trojan://") ||
    82  			strings.Contains(dataStr, "ssr://") ||
    83  			strings.Contains(dataStr, "ss://") {
    84  			return parseProfiles(dataStr)
    85  		} else {
    86  			return []string{}, err
    87  		}
    88  	}
    89  	return ParseLinks(msg)
    90  }
    91  
    92  type parseFunc func(string) ([]string, error)
    93  
    94  type ParseOption struct {
    95  	Type PAESE_TYPE
    96  }
    97  
    98  // api
    99  func ParseLinks(message string) ([]string, error) {
   100  	opt := ParseOption{Type: PARSE_ANY}
   101  	return ParseLinksWithOption(message, opt)
   102  }
   103  
   104  // api
   105  func ParseLinksWithOption(message string, opt ParseOption) ([]string, error) {
   106  	// matched, err := regexp.MatchString(`^(?:https?:\/\/)(?:[^@\/\n]+@)?(?:www\.)?([^:\/\n]+)`, message)
   107  	if opt.Type == PARSE_URL || utils.IsUrl(message) {
   108  		log.Println(message)
   109  		return getSubscriptionLinks(message)
   110  	}
   111  	// check is file path
   112  	if opt.Type == PARSE_FILE || utils.IsFilePath(message) {
   113  		return parseFile(message)
   114  	}
   115  	if opt.Type == PARSE_BASE64 {
   116  		return parseBase64(message)
   117  	}
   118  	if opt.Type == PARSE_CLASH {
   119  		return parseClash(message)
   120  	}
   121  	if opt.Type == PARSE_PROFILE {
   122  		return parseProfiles(message)
   123  	}
   124  	var links []string
   125  	var err error
   126  	for _, fn := range []parseFunc{parseProfiles, parseBase64, parseClash, parseFile} {
   127  		links, err = fn(message)
   128  		if err == nil && len(links) > 0 {
   129  			break
   130  		}
   131  	}
   132  	return links, err
   133  }
   134  
   135  func parseProfiles(data string) ([]string, error) {
   136  	// encodeed url
   137  	links := strings.Split(data, "\n")
   138  	if len(links) > 1 {
   139  		for i, link := range links {
   140  			if l, err := url.Parse(link); err == nil {
   141  				if query, err := url.QueryUnescape(l.RawQuery); err == nil && query == l.RawQuery {
   142  					links[i] = l.String()
   143  				}
   144  			}
   145  		}
   146  		data = strings.Join(links, "\n")
   147  	}
   148  	// reg := regexp.MustCompile(`((?i)vmess://(\S+?)@(\S+?):([0-9]{2,5})/([?#][^\s]+))|((?i)vmess://[a-zA-Z0-9+_/=-]+([?#][^\s]+)?)|((?i)ssr://[a-zA-Z0-9+_/=-]+)|((?i)(vless|ss|trojan)://(\S+?)@(\S+?):([0-9]{2,5})([?#][^\s]+))|((?i)(ss)://[a-zA-Z0-9+_/=-]+([?#][^\s]+))`)
   149  	matches := regProfile.FindAllStringSubmatch(data, -1)
   150  	linksLen, matchesLen := len(links), len(matches)
   151  	if linksLen < matchesLen {
   152  		links = make([]string, matchesLen)
   153  	} else if linksLen > matchesLen {
   154  		links = links[:len(matches)]
   155  	}
   156  	for index, match := range matches {
   157  		link := match[0]
   158  		if config.RegShadowrocketVmess.MatchString(link) {
   159  			if l, err := config.ShadowrocketLinkToVmessLink(link); err == nil {
   160  				link = l
   161  			}
   162  		}
   163  		links[index] = link
   164  	}
   165  	return links, nil
   166  }
   167  
   168  func parseBase64(data string) ([]string, error) {
   169  	msg, err := utils.DecodeB64(data)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	return parseProfiles(msg)
   174  }
   175  
   176  func parseClash(data string) ([]string, error) {
   177  	cc, err := config.ParseClash(utils.UnsafeGetBytes(data))
   178  	if err != nil {
   179  		return parseClashProxies(data)
   180  	}
   181  	return cc.Proxies, nil
   182  }
   183  
   184  // split to new line
   185  func parseClashProxies(input string) ([]string, error) {
   186  
   187  	if !strings.Contains(input, "{") {
   188  		return []string{}, nil
   189  	}
   190  	return scanClashProxies(strings.NewReader(input), true)
   191  }
   192  
   193  func scanClashProxies(r io.Reader, greedy bool) ([]string, error) {
   194  	proxiesStart := false
   195  	var data []byte
   196  	scanner := bufio.NewScanner(r)
   197  	for scanner.Scan() {
   198  		b := scanner.Bytes()
   199  		trimLine := strings.TrimSpace(string(b))
   200  		if trimLine == "proxy-groups:" || trimLine == "rules:" || trimLine == "Proxy Group:" {
   201  			break
   202  		}
   203  		if !proxiesStart && (trimLine == "proxies:" || trimLine == "Proxy:") {
   204  			proxiesStart = true
   205  			b = []byte("proxies:")
   206  		}
   207  		if proxiesStart {
   208  			if _, err := config.ParseBaseProxy(trimLine); err != nil {
   209  				continue
   210  			}
   211  			data = append(data, b...)
   212  			data = append(data, byte('\n'))
   213  		}
   214  	}
   215  	// fmt.Println(string(data))
   216  	return parseClashByte(data)
   217  }
   218  
   219  func parseClashFileByLine(filepath string) ([]string, error) {
   220  	file, err := os.Open(filepath)
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  	defer file.Close()
   225  	return scanClashProxies(file, false)
   226  }
   227  
   228  func parseClashByte(data []byte) ([]string, error) {
   229  	cc, err := config.ParseClash(data)
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  	return cc.Proxies, nil
   234  }
   235  
   236  func parseFile(filepath string) ([]string, error) {
   237  	filepath = strings.TrimSpace(filepath)
   238  	if _, err := os.Stat(filepath); err != nil {
   239  		return nil, err
   240  	}
   241  	// clash
   242  	if isYamlFile(filepath) {
   243  		return parseClashFileByLine(filepath)
   244  	}
   245  	data, err := ioutil.ReadFile(filepath)
   246  	if err != nil {
   247  		return nil, err
   248  	}
   249  
   250  	links, err := parseBase64(string(data))
   251  	if err != nil && len(data) > 2048 {
   252  		preview := string(data[:2048])
   253  		if strings.Contains(preview, "proxies:") {
   254  			return scanClashProxies(bytes.NewReader(data), true)
   255  		}
   256  		if strings.Contains(preview, "vmess://") ||
   257  			strings.Contains(preview, "trojan://") ||
   258  			strings.Contains(preview, "ssr://") ||
   259  			strings.Contains(preview, "ss://") {
   260  			return parseProfiles(string(data))
   261  		}
   262  	}
   263  	return links, err
   264  }
   265  
   266  func parseOptions(message string) (*ProfileTestOptions, error) {
   267  	opts := strings.Split(message, "^")
   268  	if len(opts) < 7 {
   269  		return nil, ErrInvalidData
   270  	}
   271  	groupName := opts[0]
   272  	if groupName == "?empty?" || groupName == "" {
   273  		groupName = "Default"
   274  	}
   275  	concurrency, err := strconv.Atoi(opts[5])
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  	if concurrency < 1 {
   280  		concurrency = 1
   281  	}
   282  	timeout, err := strconv.Atoi(opts[6])
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  	if timeout < 20 {
   287  		timeout = 20
   288  	}
   289  	testOpt := &ProfileTestOptions{
   290  		GroupName:     groupName,
   291  		SpeedTestMode: opts[1],
   292  		PingMethod:    opts[2],
   293  		SortMethod:    opts[3],
   294  		Concurrency:   concurrency,
   295  		TestMode:      ALLTEST,
   296  		Timeout:       time.Duration(timeout) * time.Second,
   297  	}
   298  	return testOpt, nil
   299  }
   300  
   301  const (
   302  	SpeedOnly = "speedonly"
   303  	PingOnly  = "pingonly"
   304  	ALLTEST   = iota
   305  	RETEST
   306  )
   307  
   308  type ProfileTestOptions struct {
   309  	GroupName       string        `json:"group"`
   310  	SpeedTestMode   string        `json:"speedtestMode"` // speedonly pingonly all
   311  	PingMethod      string        `json:"pingMethod"`    // googleping
   312  	SortMethod      string        `json:"sortMethod"`    // speed rspeed ping rping
   313  	Concurrency     int           `json:"concurrency"`
   314  	TestMode        int           `json:"testMode"` // 2: ALLTEST 3: RETEST
   315  	TestIDs         []int         `json:"testids"`
   316  	Timeout         time.Duration `json:"timeout"`
   317  	Links           []string      `json:"links"`
   318  	Subscription    string        `json:"subscription"`
   319  	Language        string        `json:"language"`
   320  	FontSize        int           `json:"fontSize"`
   321  	Theme           string        `json:"theme"`
   322  	Unique          bool          `json:"unique"`
   323  	GeneratePicMode int           `json:"generatePicMode"` // 0: base64 1:pic path 2: no pic 3: json @deprecated use outputMode
   324  	OutputMode      int           `json:"outputMode"`
   325  }
   326  
   327  type JSONOutput struct {
   328  	Nodes        []render.Node      `json:"nodes"`
   329  	Options      ProfileTestOptions `json:"options"`
   330  	Traffic      int64              `json:"traffic"`
   331  	Duration     string             `json:"duration"`
   332  	SuccessCount int                `json:"successCount"`
   333  	LinksCount   int                `json:"linksCount"`
   334  }
   335  
   336  func parseMessage(message []byte) ([]string, *ProfileTestOptions, error) {
   337  	options := &ProfileTestOptions{}
   338  	err := json.Unmarshal(message, options)
   339  	if err != nil {
   340  		return nil, nil, err
   341  	}
   342  	options.Timeout = time.Duration(int(options.Timeout)) * time.Second
   343  	if options.GroupName == "?empty?" || options.GroupName == "" {
   344  		options.GroupName = "Default"
   345  	}
   346  	if options.Timeout < 8 {
   347  		options.Timeout = 8
   348  	}
   349  	if options.Concurrency < 1 {
   350  		options.Concurrency = 1
   351  	}
   352  	if options.TestMode == RETEST {
   353  		return options.Links, options, nil
   354  	}
   355  	options.TestMode = ALLTEST
   356  	links, err := ParseLinks(options.Subscription)
   357  	if err != nil {
   358  		return nil, nil, err
   359  	}
   360  	return links, options, nil
   361  }
   362  
   363  func parseRetestMessage(message []byte) ([]string, *ProfileTestOptions, error) {
   364  	options := &ProfileTestOptions{}
   365  	err := json.Unmarshal(message, options)
   366  	if err != nil {
   367  		return nil, nil, err
   368  	}
   369  	if options.TestMode != RETEST {
   370  		return nil, nil, errors.New("not retest mode")
   371  	}
   372  	options.TestMode = RETEST
   373  	options.Timeout = time.Duration(int(options.Timeout)) * time.Second
   374  	if options.GroupName == "?empty?" || options.GroupName == "" {
   375  		options.GroupName = "Default"
   376  	}
   377  	if options.Timeout < 20 {
   378  		options.Timeout = 20
   379  	}
   380  	if options.Concurrency < 1 {
   381  		options.Concurrency = 1
   382  	}
   383  	return options.Links, options, nil
   384  }
   385  
   386  type MessageWriter interface {
   387  	WriteMessage(messageType int, data []byte) error
   388  }
   389  
   390  type OutputMessageWriter struct {
   391  }
   392  
   393  func (p *OutputMessageWriter) WriteMessage(messageType int, data []byte) error {
   394  	log.Println(string(data))
   395  	return nil
   396  }
   397  
   398  type EmptyMessageWriter struct {
   399  }
   400  
   401  func (w *EmptyMessageWriter) WriteMessage(messageType int, data []byte) error {
   402  	return nil
   403  }
   404  
   405  type ProfileTest struct {
   406  	Writer      MessageWriter
   407  	Options     *ProfileTestOptions
   408  	MessageType int
   409  	Links       []string
   410  	mu          sync.Mutex
   411  	wg          sync.WaitGroup // wait for all to finish
   412  }
   413  
   414  func (p *ProfileTest) WriteMessage(data []byte) error {
   415  	var err error
   416  	if p.Writer != nil {
   417  		p.mu.Lock()
   418  		err = p.Writer.WriteMessage(p.MessageType, data)
   419  		p.mu.Unlock()
   420  	}
   421  	return err
   422  }
   423  
   424  func (p *ProfileTest) WriteString(data string) error {
   425  	b := []byte(data)
   426  	return p.WriteMessage(b)
   427  }
   428  
   429  // api
   430  // render.Node contain the final test result
   431  func (p *ProfileTest) TestAll(ctx context.Context, trafficChan chan<- int64) (chan render.Node, error) {
   432  	links := p.Links
   433  	linksCount := len(links)
   434  	if linksCount < 1 {
   435  		return nil, fmt.Errorf("profile not found")
   436  	}
   437  	nodeChan := make(chan render.Node, linksCount)
   438  	go func(context.Context) {
   439  		guard := make(chan int, p.Options.Concurrency)
   440  		for i := range links {
   441  			p.wg.Add(1)
   442  			id := i
   443  			link := links[i]
   444  			select {
   445  			case guard <- i:
   446  				go func(id int, link string, c <-chan int, nodeChan chan<- render.Node) {
   447  					p.testOne(ctx, id, link, nodeChan, trafficChan)
   448  					<-c
   449  				}(id, link, guard, nodeChan)
   450  			case <-ctx.Done():
   451  				return
   452  			}
   453  		}
   454  		// p.wg.Wait()
   455  		// if trafficChan != nil {
   456  		// 	close(trafficChan)
   457  		// }
   458  	}(ctx)
   459  	return nodeChan, nil
   460  }
   461  
   462  func (p *ProfileTest) testAll(ctx context.Context) (render.Nodes, error) {
   463  	linksCount := len(p.Links)
   464  	if linksCount < 1 {
   465  		p.WriteString(SPEEDTEST_ERROR_NONODES)
   466  		return nil, fmt.Errorf("no profile found")
   467  	}
   468  	start := time.Now()
   469  	p.WriteMessage(getMsgByte(-1, "started"))
   470  	// for i := range p.Links {
   471  	// 	p.WriteMessage(gotserverMsg(i, p.Links[i], p.Options.GroupName))
   472  	// }
   473  	step := 9
   474  	if linksCount > 200 {
   475  		step = linksCount / 20
   476  		if step > 50 {
   477  			step = 50
   478  		}
   479  	}
   480  	for i := 0; i < linksCount; {
   481  		end := i + step
   482  		if end > linksCount {
   483  			end = linksCount
   484  		}
   485  		links := p.Links[i:end]
   486  		msg := gotserversMsg(i, links, p.Options.GroupName)
   487  		p.WriteMessage(msg)
   488  		i += step
   489  	}
   490  	guard := make(chan int, p.Options.Concurrency)
   491  	nodeChan := make(chan render.Node, linksCount)
   492  
   493  	nodes := make(render.Nodes, linksCount)
   494  	for i := range p.Links {
   495  		p.wg.Add(1)
   496  		id := i
   497  		link := ""
   498  		if len(p.Options.TestIDs) > 0 && len(p.Options.Links) > 0 {
   499  			id = p.Options.TestIDs[i]
   500  			link = p.Options.Links[i]
   501  		}
   502  		select {
   503  		case guard <- i:
   504  			go func(id int, link string, c <-chan int, nodeChan chan<- render.Node) {
   505  				p.testOne(ctx, id, link, nodeChan, nil)
   506  				_ = p.WriteMessage(getMsgByte(id, "endone"))
   507  				<-c
   508  			}(id, link, guard, nodeChan)
   509  		case <-ctx.Done():
   510  			return nil, nil
   511  		}
   512  	}
   513  	p.wg.Wait()
   514  	p.WriteMessage(getMsgByte(-1, "eof"))
   515  	duration := FormatDuration(time.Since(start))
   516  	// draw png
   517  	successCount := 0
   518  	var traffic int64 = 0
   519  	for i := 0; i < linksCount; i++ {
   520  		node := <-nodeChan
   521  		node.Link = p.Links[node.Id]
   522  		nodes[node.Id] = node
   523  		traffic += node.Traffic
   524  		if node.IsOk {
   525  			successCount += 1
   526  		}
   527  	}
   528  	close(nodeChan)
   529  
   530  	if p.Options.OutputMode == PIC_NONE {
   531  		return nodes, nil
   532  	}
   533  
   534  	// sort nodes
   535  	nodes.Sort(p.Options.SortMethod)
   536  	// save json
   537  	if p.Options.OutputMode == JSON_OUTPUT {
   538  		p.saveJSON(nodes, traffic, duration, successCount, linksCount)
   539  	} else if p.Options.OutputMode == TEXT_OUTPUT {
   540  		p.saveText(nodes)
   541  	} else {
   542  		// render the result to pic
   543  		p.renderPic(nodes, traffic, duration, successCount, linksCount)
   544  	}
   545  	return nodes, nil
   546  }
   547  
   548  func (p *ProfileTest) renderPic(nodes render.Nodes, traffic int64, duration string, successCount int, linksCount int) error {
   549  	fontPath := "WenQuanYiMicroHei-01.ttf"
   550  	options := render.NewTableOptions(40, 30, 0.5, 0.5, p.Options.FontSize, 0.5, fontPath, p.Options.Language, p.Options.Theme, "Asia/Shanghai", FontBytes)
   551  	table, err := render.NewTableWithOption(nodes, &options)
   552  	if err != nil {
   553  		return err
   554  	}
   555  	// msg := fmt.Sprintf("Total Traffic : %s. Total Time : %s. Working Nodes: [%d/%d]", download.ByteCountIECTrim(traffic), duration, successCount, linksCount)
   556  	msg := table.FormatTraffic(download.ByteCountIECTrim(traffic), duration, fmt.Sprintf("%d/%d", successCount, linksCount))
   557  	if p.Options.OutputMode == PIC_PATH {
   558  		table.Draw("out.png", msg)
   559  		p.WriteMessage(getMsgByte(-1, "picdata", "out.png"))
   560  		return nil
   561  	}
   562  	if picdata, err := table.EncodeB64(msg); err == nil {
   563  		p.WriteMessage(getMsgByte(-1, "picdata", picdata))
   564  	}
   565  	return nil
   566  }
   567  
   568  func (p *ProfileTest) saveJSON(nodes render.Nodes, traffic int64, duration string, successCount int, linksCount int) error {
   569  	jsonOutput := JSONOutput{
   570  		Nodes:        nodes,
   571  		Options:      *p.Options,
   572  		Traffic:      traffic,
   573  		Duration:     duration,
   574  		SuccessCount: successCount,
   575  		LinksCount:   linksCount,
   576  	}
   577  	data, err := json.MarshalIndent(&jsonOutput, "", "\t")
   578  	if err != nil {
   579  		return err
   580  	}
   581  	return ioutil.WriteFile("output.json", data, 0644)
   582  }
   583  
   584  func (p *ProfileTest) saveText(nodes render.Nodes) error {
   585  	var links []string
   586  	for _, node := range nodes {
   587  		if node.Ping != "0" || node.AvgSpeed > 0 || node.MaxSpeed > 0 {
   588  			links = append(links, node.Link)
   589  		}
   590  	}
   591  	data := []byte(strings.Join(links, "\n"))
   592  	return ioutil.WriteFile("output.txt", data, 0644)
   593  }
   594  
   595  func (p *ProfileTest) testOne(ctx context.Context, index int, link string, nodeChan chan<- render.Node, trafficChan chan<- int64) error {
   596  	// panic
   597  	defer p.wg.Done()
   598  	if link == "" {
   599  		link = p.Links[index]
   600  		link = strings.SplitN(link, "^", 2)[0]
   601  	}
   602  	cfg, err := config.Link2Config(link)
   603  	if err != nil {
   604  		return err
   605  	}
   606  	remarks := cfg.Remarks
   607  	if err != nil || remarks == "" {
   608  		remarks = fmt.Sprintf("Profile %d", index)
   609  	}
   610  	protocol := cfg.Protocol
   611  	if (cfg.Protocol == "vmess" || cfg.Protocol == "trojan") && cfg.Net != "" {
   612  		protocol = fmt.Sprintf("%s/%s", cfg.Protocol, cfg.Net)
   613  	}
   614  	elapse, err := p.pingLink(index, link)
   615  	log.Printf("%d %s elapse: %dms", index, remarks, elapse)
   616  	if err != nil {
   617  		node := render.Node{
   618  			Id:       index,
   619  			Group:    p.Options.GroupName,
   620  			Remarks:  remarks,
   621  			Protocol: protocol,
   622  			Ping:     fmt.Sprintf("%d", elapse),
   623  			AvgSpeed: 0,
   624  			MaxSpeed: 0,
   625  			IsOk:     elapse > 0,
   626  		}
   627  		nodeChan <- node
   628  		return err
   629  	}
   630  	err = p.WriteMessage(getMsgByte(index, "startspeed"))
   631  	ch := make(chan int64, 1)
   632  	startCh := make(chan time.Time, 1)
   633  	defer close(ch)
   634  	go func(ch <-chan int64, startChan <-chan time.Time) {
   635  		var max int64
   636  		var sum int64
   637  		var avg int64
   638  		start := time.Now()
   639  	Loop:
   640  		for {
   641  			select {
   642  			case speed, ok := <-ch:
   643  				if !ok || speed < 0 {
   644  					break Loop
   645  				}
   646  				sum += speed
   647  				duration := float64(time.Since(start)/time.Millisecond) / float64(1000)
   648  				avg = int64(float64(sum) / duration)
   649  				if max < speed {
   650  					max = speed
   651  				}
   652  				log.Printf("%d %s recv: %s", index, remarks, download.ByteCountIEC(speed))
   653  				err = p.WriteMessage(getMsgByte(index, "gotspeed", avg, max, speed))
   654  				if trafficChan != nil {
   655  					trafficChan <- speed
   656  				}
   657  			case s := <-startChan:
   658  				start = s
   659  			case <-ctx.Done():
   660  				log.Printf("index %d done!", index)
   661  				break Loop
   662  			}
   663  		}
   664  		node := render.Node{
   665  			Id:       index,
   666  			Group:    p.Options.GroupName,
   667  			Remarks:  remarks,
   668  			Protocol: protocol,
   669  			Ping:     fmt.Sprintf("%d", elapse),
   670  			AvgSpeed: avg,
   671  			MaxSpeed: max,
   672  			IsOk:     true,
   673  			Traffic:  sum,
   674  		}
   675  		nodeChan <- node
   676  	}(ch, startCh)
   677  	speed, err := download.Download(link, p.Options.Timeout, p.Options.Timeout, ch, startCh)
   678  	// speed, err := download.DownloadRange(link, 2, p.Options.Timeout, p.Options.Timeout, ch, startCh)
   679  	if speed < 1 {
   680  		p.WriteMessage(getMsgByte(index, "gotspeed", -1, -1, 0))
   681  	}
   682  	return err
   683  }
   684  
   685  func (p *ProfileTest) pingLink(index int, link string) (int64, error) {
   686  	if p.Options.SpeedTestMode == SpeedOnly {
   687  		return 0, nil
   688  	}
   689  	if link == "" {
   690  		link = p.Links[index]
   691  	}
   692  	p.WriteMessage(getMsgByte(index, "startping"))
   693  	elapse, err := request.PingLink(link, 2)
   694  	p.WriteMessage(getMsgByte(index, "gotping", elapse))
   695  	if elapse < 1 {
   696  		p.WriteMessage(getMsgByte(index, "gotspeed", -1, -1, 0))
   697  		return 0, err
   698  	}
   699  	if p.Options.SpeedTestMode == PingOnly {
   700  		p.WriteMessage(getMsgByte(index, "gotspeed", -1, -1, 0))
   701  		return elapse, errors.New(PingOnly)
   702  	}
   703  	return elapse, err
   704  }
   705  
   706  func FormatDuration(duration time.Duration) string {
   707  	h := duration / time.Hour
   708  	duration -= h * time.Hour
   709  	m := duration / time.Minute
   710  	duration -= m * time.Minute
   711  	s := duration / time.Second
   712  	if h > 0 {
   713  		return fmt.Sprintf("%dh %dm %ds", h, m, s)
   714  	}
   715  	return fmt.Sprintf("%dm %ds", m, s)
   716  }
   717  
   718  func png2base64(path string) (string, error) {
   719  	bytes, err := ioutil.ReadFile(path)
   720  	if err != nil {
   721  		return "", err
   722  	}
   723  	return "data:image/png;base64," + base64.StdEncoding.EncodeToString(bytes), nil
   724  }
   725  
   726  func isYamlFile(filePath string) bool {
   727  	return strings.HasSuffix(filePath, ".yaml") || strings.HasSuffix(filePath, ".yml")
   728  }
   729  
   730  // api
   731  func PeekClash(input string, n int) ([]string, error) {
   732  	scanner := bufio.NewScanner(strings.NewReader(input))
   733  	proxiesStart := false
   734  	data := []byte{}
   735  	linkCount := 0
   736  	for scanner.Scan() {
   737  		b := scanner.Bytes()
   738  		trimLine := strings.TrimSpace(string(b))
   739  		if trimLine == "proxy-groups:" || trimLine == "rules:" || trimLine == "Proxy Group:" {
   740  			break
   741  		}
   742  		if proxiesStart {
   743  			if _, err := config.ParseBaseProxy(trimLine); err != nil {
   744  				continue
   745  			}
   746  			if strings.HasPrefix(trimLine, "-") {
   747  				if linkCount >= n {
   748  					break
   749  				}
   750  				linkCount += 1
   751  			}
   752  			data = append(data, b...)
   753  			data = append(data, byte('\n'))
   754  			continue
   755  		}
   756  		if !proxiesStart && (trimLine == "proxies:" || trimLine == "Proxy:") {
   757  			proxiesStart = true
   758  			b = []byte("proxies:")
   759  		}
   760  		data = append(data, b...)
   761  		data = append(data, byte('\n'))
   762  	}
   763  	// fmt.Println(string(data))
   764  	links, err := parseClashByte(data)
   765  	if err != nil || len(links) < 1 {
   766  		return []string{}, err
   767  	}
   768  	endIndex := n
   769  	if endIndex > len(links) {
   770  		endIndex = len(links)
   771  	}
   772  	return links[:endIndex], nil
   773  }