diff --git a/main.go b/main.go index 69848ab..1ba50d3 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "github.com/caarlos0/env/v6" "github.com/cloudflare/cloudflare-go/v4" @@ -12,11 +13,14 @@ import ( "io" "log" "net/http" + "regexp" "time" ) -type IPResponse struct { - IPAddress string `json:"YourFuckingIPAddress"` +var ipPattern = `^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$` + +type WTFIPResponse struct { + IP string `json:"YourFuckingIPAddress"` Location string `json:"YourFuckingLocation"` Hostname string `json:"YourFuckingHostname"` ISP string `json:"YourFuckingISP"` @@ -26,10 +30,111 @@ type IPResponse struct { CountryCode string `json:"YourFuckingCountryCode"` } +type IpifyResponse struct { + IP string `json:"ip"` +} + +type IpInfoResponse struct { + IP string `json:"ip"` + Hostname string `json:"hostname"` + City string `json:"city"` + Region string `json:"region"` + Country string `json:"country"` + Loc string `json:"loc"` + Org string `json:"org"` + Postal string `json:"postal"` + Timezone string `json:"timezone"` + Readme string `json:"readme"` +} +type Response struct { + IPAddress string +} + +type ResponseInterface interface { + getIP() Response +} + +func (r *WTFIPResponse) getIP() Response { + return Response{IPAddress: r.IP} +} +func (r *IpInfoResponse) getIP() Response { + return Response{IPAddress: r.IP} +} +func (r *IpifyResponse) getIP() Response { + return Response{IPAddress: r.IP} +} + +func NewIpifyProvider() *IpProvider { + return &IpProvider{ + url: "https://api.ipify.org?format=json", + resp: new(IpifyResponse), + client: http.Client{ + Timeout: 5 * time.Second, + }, + } +} + +func NewIpInfoProvider() *IpProvider { + return &IpProvider{ + url: "https://ipinfo.io/json", + resp: new(IpInfoResponse), + client: http.Client{ + Timeout: 5 * time.Second, + }, + } +} + +func NewWTFProvider() *IpProvider { + return &IpProvider{ + url: "https://myip.wtf/json", + resp: new(WTFIPResponse), + client: http.Client{ + Timeout: 5 * time.Second, + }, + } +} + +type IpProvider struct { + url string + resp interface{} + client http.Client +} + +func (p *IpProvider) get() *Response { + + body, err := p.client.Get(p.url) + if err != nil { + return nil + } + defer body.Body.Close() + + data, err := io.ReadAll(body.Body) + if err != nil { + return nil + } + + err = json.Unmarshal(data, &p.resp) + if err != nil { + return nil + } + + resp := p.resp.(ResponseInterface).getIP() + pattern := regexp.MustCompile(ipPattern) + + if !pattern.MatchString(resp.IPAddress) { + return nil + } + + return &resp +} + type Config struct { CloudflareZoneID string `env:"CLOUDFLARE_ZONE_ID"` CloudFlareAPIKey string `env:"CLOUDFLARE_API_KEY"` CloudflareEmail string `env:"CLOUDFLARE_EMAIL"` + IpProviders []*IpProvider + Client *cloudflare.Client + Timeout time.Duration } func main() { @@ -40,7 +145,9 @@ func main() { return } - config := &Config{} + config := &Config{ + Timeout: 5 * time.Second, + } err = env.Parse(config) if err != nil { log.Fatal("Failed to load env file") @@ -52,81 +159,107 @@ func main() { return } - client := cloudflare.NewClient( + config.Client = cloudflare.NewClient( option.WithAPIKey(config.CloudFlareAPIKey), option.WithAPIEmail(config.CloudflareEmail), ) - ExistingIP, err := getIP() - if err != nil { - fmt.Println(err) - return - } + config.IpProviders = append( + config.IpProviders, + NewWTFProvider(), + ) + config.IpProviders = append( + config.IpProviders, + NewIpifyProvider(), + ) + config.IpProviders = append( + config.IpProviders, + NewIpInfoProvider(), + ) + + ExistingIP, err := GetIPAddress(config) + + fmt.Printf("The current IP address is: %s\n", ExistingIP) err = updateDNSRecords( - client, - config.CloudflareZoneID, - ExistingIP.IPAddress, + config, + ExistingIP, ) if err != nil { fmt.Println(err) } - fmt.Println("Your public IP address is:", ExistingIP.IPAddress) - do: for { - CurrentIP, err := getIP() + CurrentIP, err := GetIPAddress(config) if err != nil { fmt.Println(err) continue do } - if ExistingIP.IPAddress != CurrentIP.IPAddress { - ExistingIP = CurrentIP - fmt.Println( - "Your public IP address has updated:", - ExistingIP.IPAddress, - ) - - err := updateDNSRecords( - client, - config.CloudflareZoneID, - CurrentIP.IPAddress, - ) - if err != nil { - fmt.Println(err) - break - } - - } else { - fmt.Println("Your public IP address has not changed.") + if ExistingIP == CurrentIP { + time.Sleep(config.Timeout) + continue do } - time.Sleep(10 * time.Second) + ExistingIP = CurrentIP + fmt.Printf( + "Your IP address has changed: %s\n", + CurrentIP, + ) + + err = updateDNSRecords( + config, + CurrentIP, + ) + if err != nil { + fmt.Println(err) + } + + time.Sleep(config.Timeout) continue do } } -func getIP() (*IPResponse, error) { +func GetIPAddress(config *Config) (string, error) { - client := &http.Client{ - Timeout: time.Second * 5, - } - var ip IPResponse - resp, err := client.Get("https://myip.wtf/json") - if err != nil { - return nil, err - } - defer resp.Body.Close() + responses := []*Response{} - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err + for _, provider := range config.IpProviders { + resp := provider.get() + responses = append(responses, resp) } - err = json.Unmarshal(data, &ip) - return &ip, err + // Count the occurrences of each IPAddress + counts := map[string]int{} + for _, r := range responses { + if r != nil { + IP := r.IPAddress + if count, ok := counts[IP]; ok { + counts[IP] = count + 1 + } else { + counts[IP] = 1 + } + } + } + + // Find the IPAddress that repeats the most + maxCount := 0 + CurrentIP := "" + for IP, count := range counts { + if count > maxCount { + maxCount = count + CurrentIP = IP + } + } + + pattern := regexp.MustCompile(ipPattern) + + if !pattern.MatchString(CurrentIP) { + return "", errors.New("Did not find a valid IP") + } + + return CurrentIP, nil } func getDNSRecords( @@ -154,17 +287,23 @@ func getDNSRecords( } func updateDNSRecords( - client *cloudflare.Client, - zoneID string, + config *Config, ipAddress string, ) error { - records, err := getDNSRecords(client, zoneID) + records, err := getDNSRecords( + config.Client, + config.CloudflareZoneID, + ) if err != nil { return err } for _, record := range *records { + if record.Content == ipAddress { + continue + } + NewDNS := dns.ARecordParam{ Comment: cloudflare.F(record.Comment), Content: cloudflare.F(ipAddress), @@ -174,9 +313,9 @@ func updateDNSRecords( Type: cloudflare.F(dns.ARecordTypeA), } - _, err = client.DNS.Records.Edit( + _, err = config.Client.DNS.Records.Edit( context.TODO(), record.ID, dns.RecordEditParams{ - ZoneID: cloudflare.F(zoneID), + ZoneID: cloudflare.F(config.CloudflareZoneID), Record: NewDNS, }, )