package api import ( "errors" "fmt" "maps" "net/http" "net/url" "regexp" "slices" "strings" ) type Provider string const ( Cloudflare Provider = "cloudflare" IfConfigCo Provider = "ifconfigco" IPInfo Provider = "ipinfo" IP2Location Provider = "ip2location" ) const echoipPrefix = "echoip#" type NameToken struct { Name string Token string } func New(nameTokenPairs []NameToken, client *http.Client) ( fetchers []Fetcher, err error, ) { fetchers = make([]Fetcher, len(nameTokenPairs)) for i, nameTokenPair := range nameTokenPairs { provider, err := ParseProvider(nameTokenPair.Name) if err != nil { return nil, fmt.Errorf("parsing API name: %w", err) } switch { case provider == Cloudflare: fetchers[i] = newCloudflare(client) case provider == IfConfigCo: const ifConfigCoURL = "https://ifconfig.co" fetchers[i] = newEchoip(client, ifConfigCoURL) case provider == IPInfo: fetchers[i] = newIPInfo(client, nameTokenPair.Token) case provider == IP2Location: fetchers[i] = newIP2Location(client, nameTokenPair.Token) case strings.HasPrefix(string(provider), echoipPrefix): url := strings.TrimPrefix(string(provider), echoipPrefix) fetchers[i] = newEchoip(client, url) default: panic("provider not valid: " + provider) } } return fetchers, nil } var regexEchoipURL = regexp.MustCompile(`^http(s|):\/\/.+$`) var ErrProviderNotValid = errors.New("API name is not valid") func ParseProvider(s string) (provider Provider, err error) { possibleProviders := []Provider{ Cloudflare, IfConfigCo, IP2Location, IPInfo, } stringToProvider := make(map[string]Provider, len(possibleProviders)) for _, provider := range possibleProviders { stringToProvider[string(provider)] = provider } provider, ok := stringToProvider[strings.ToLower(s)] if ok { return provider, nil } customPrefixToURLRegex := map[string]*regexp.Regexp{ echoipPrefix: regexEchoipURL, } for prefix, urlRegex := range customPrefixToURLRegex { match, err := checkCustomURL(s, prefix, urlRegex) if !match { continue } else if err != nil { return "", err } return Provider(s), nil } providerStrings := make([]string, 0, len(stringToProvider)+len(customPrefixToURLRegex)) for _, providerString := range slices.Sorted(maps.Keys(stringToProvider)) { providerStrings = append(providerStrings, `"`+providerString+`"`) } for _, prefix := range slices.Sorted(maps.Keys(customPrefixToURLRegex)) { providerStrings = append(providerStrings, "a custom "+prefix+" url") } return "", fmt.Errorf(`%w: %q can only be %s`, ErrProviderNotValid, s, orStrings(providerStrings)) } var ErrCustomURLNotValid = errors.New("custom URL is not valid") func checkCustomURL(s, prefix string, regex *regexp.Regexp) (match bool, err error) { if !strings.HasPrefix(s, prefix) { return false, nil } s = strings.TrimPrefix(s, prefix) _, err = url.Parse(s) if err != nil { return true, fmt.Errorf("%s %w: %w", prefix, ErrCustomURLNotValid, err) } if regex.MatchString(s) { return true, nil } return true, fmt.Errorf("%s %w: %q does not match regular expression: %s", prefix, ErrCustomURLNotValid, s, regex) } func orStrings(strings []string) (result string) { return joinStrings(strings, "or") } func joinStrings(strings []string, lastJoin string) (result string) { if len(strings) == 0 { return "" } result = strings[0] for i := 1; i < len(strings); i++ { if i < len(strings)-1 { result += ", " + strings[i] } else { result += " " + lastJoin + " " + strings[i] } } return result }