package updater import ( "bytes" "context" "errors" "fmt" "io" "net/http" "net/url" "strings" "github.com/qdm12/gluetun/internal/provider/common" ) type apiServer struct { country string city string hostname string } var ( ErrDataMalformed = errors.New("data is malformed") ) const apiURL = "https://support.fastestvpn.com/wp-admin/admin-ajax.php" // The API URL and requests are shamelessly taken from network operations // done on the page https://support.fastestvpn.com/vpn-servers/ func fetchAPIServers(ctx context.Context, client *http.Client, protocol string) ( servers []apiServer, err error) { form := url.Values{ "action": []string{"vpn_servers"}, "protocol": []string{protocol}, } body := strings.NewReader(form.Encode()) request, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, body) if err != nil { return nil, fmt.Errorf("creating request: %w", err) } request.Header.Set("Content-Type", "application/x-www-form-urlencoded") // request.Header.Set("User-Agent", "curl/8.9.0") // request.Header.Set("Accept", "*/*") response, err := client.Do(request) if err != nil { return nil, fmt.Errorf("sending request: %w", err) } if response.StatusCode != http.StatusOK { _ = response.Body.Close() return nil, fmt.Errorf("%w: %d", common.ErrHTTPStatusCodeNotOK, response.StatusCode) } data, err := io.ReadAll(response.Body) if err != nil { _ = response.Body.Close() return nil, fmt.Errorf("reading response body: %w", err) } err = response.Body.Close() if err != nil { return nil, fmt.Errorf("closing response body: %w", err) } const usualMaxNumber = 100 servers = make([]apiServer, 0, usualMaxNumber) for { trBlock := getNextTRBlock(data) if trBlock == nil { break } data = data[len(trBlock):] var server apiServer const numberOfTDBlocks = 3 for i := range numberOfTDBlocks { tdBlock := getNextTDBlock(trBlock) if tdBlock == nil { return nil, fmt.Errorf("%w: expected 3 blocks in block %q", ErrDataMalformed, string(trBlock)) } trBlock = trBlock[len(tdBlock):] const startToken, endToken = "", "" tdBlockData := string(tdBlock[len(startToken) : len(tdBlock)-len(endToken)]) const countryIndex, cityIndex, hostnameIndex = 0, 1, 2 switch i { case countryIndex: server.country = tdBlockData case cityIndex: server.city = tdBlockData case hostnameIndex: server.hostname = tdBlockData } } servers = append(servers, server) } return servers, nil } func getNextTRBlock(data []byte) (trBlock []byte) { const startToken, endToken = "", "" return getNextBlock(data, startToken, endToken) } func getNextTDBlock(data []byte) (tdBlock []byte) { const startToken, endToken = "", "" return getNextBlock(data, startToken, endToken) } func getNextBlock(data []byte, startToken, endToken string) (nextBlock []byte) { i := bytes.Index(data, []byte(startToken)) if i == -1 { return nil } nextBlock = data[i:] i = bytes.Index(nextBlock[len(startToken):], []byte(endToken)) if i == -1 { return nil } nextBlock = nextBlock[:i+len(startToken)+len(endToken)] return nextBlock }