github.com/xxf098/lite-proxy@v0.15.1-0.20230422081941-12c69f323218/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/xxf098/lite-proxy/config" 24 "github.com/xxf098/lite-proxy/download" 25 "github.com/xxf098/lite-proxy/request" 26 "github.com/xxf098/lite-proxy/utils" 27 "github.com/xxf098/lite-proxy/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 }