package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) const ( ipv4URL = "https://4.ident.me/" ipv6URL = "https://6.ident.me/" ipHistoryPath = "history.json" checkInterval = 30 * time.Second maxRetries = 3 retryDelay = 10 * time.Second ) type IPRecord struct { Timestamp string `json:"timestamp"` IPv4 string `json:"ipv4"` IPv6 string `json:"ipv6"` } func getPublicIPs() (string, string, error) { var ipv4, ipv6 string var err error for attempt := 1; attempt <= maxRetries; attempt++ { ipv4, ipv6, err = fetchIPs() if err == nil { return ipv4, ipv6, nil } fmt.Printf("Attempt %d: Error fetching IPs: %v\n", attempt, err) time.Sleep(retryDelay) } return "", "", err } func fetchIPs() (string, string, error) { ipv4, err := fetchIP(ipv4URL) if err != nil { return "", "", err } ipv6, err := fetchIP(ipv6URL) if err != nil { fmt.Println("Warning: Could not fetch IPv6 address:", err) } return ipv4, ipv6, nil } func fetchIP(url string) (string, error) { resp, err := http.Get(url) if err != nil { return "", err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", err } return strings.TrimSpace(string(body)), nil } func readHistory() ([]IPRecord, error) { file, err := os.Open(ipHistoryPath) if os.IsNotExist(err) { return []IPRecord{}, nil } else if err != nil { return nil, err } defer file.Close() fileInfo, err := file.Stat() if err != nil { return nil, err } // Check if the file is empty if fileInfo.Size() == 0 { return []IPRecord{}, nil } var history []IPRecord err = json.NewDecoder(file).Decode(&history) if err != nil { return nil, err } return history, nil } func writeHistory(history []IPRecord) error { file, err := os.Create(ipHistoryPath) if err != nil { return err } defer file.Close() encoder := json.NewEncoder(file) encoder.SetIndent("", " ") // Pretty-print with indentation err = encoder.Encode(history) if err != nil { return err } return nil } func trackIP(ipChan <-chan IPRecord) { var lastLoggedIP IPRecord hasNotifiedUpToDate := false for ipRecord := range ipChan { history, err := readHistory() if err != nil { fmt.Println("Error reading IP history:", err) continue } if len(history) > 0 { lastLoggedIP = history[0] } // Check if IPs are the same as the last logged one if ipRecord.IPv4 == lastLoggedIP.IPv4 && ipRecord.IPv6 == lastLoggedIP.IPv6 { if !hasNotifiedUpToDate { fmt.Println("🤷 IPs are already up to date") hasNotifiedUpToDate = true } continue } // Reset the notification flag when IP changes hasNotifiedUpToDate = false ipRecord.Timestamp = time.Now().Format("2006-01-02 15:04:05") history = append([]IPRecord{ipRecord}, history...) if err := writeHistory(history); err != nil { fmt.Println("Error writing IP history:", err) continue } fmt.Printf("📄 New IP logged: {Timestamp: %s, IPv4: %s, IPv6: %s}\n", ipRecord.Timestamp, ipRecord.IPv4, ipRecord.IPv6) } } func serveWeb() { http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("web")))) http.HandleFunc("/history", func(w http.ResponseWriter, r *http.Request) { history, err := readHistory() if err != nil { http.Error(w, "Error reading IP history", http.StatusInternalServerError) return } jsonData, err := json.Marshal(history) if err != nil { http.Error(w, "Error converting history to JSON", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(jsonData) }) fmt.Println("Starting server on :8080") http.ListenAndServe(":8080", nil) } func main() { ipChan := make(chan IPRecord) go func() { for { ipv4, ipv6, err := getPublicIPs() if err != nil { fmt.Println("Error fetching public IPs:", err) } else { ipChan <- IPRecord{IPv4: ipv4, IPv6: ipv6} } time.Sleep(checkInterval) } }() go trackIP(ipChan) serveWeb() }