package client import ( "errors" "io" "net/http" "os" "path/filepath" "strconv" "strings" "time" "velvettear/godyn/config" "velvettear/godyn/log" "velvettear/godyn/notifications" ) var wanIp string var ipFile string // exported function(s) func Run() { timestamp := time.Now().UnixMilli() log.Debug("starting godyn in client mode...") if !config.HasValidDyndnsConfig() { log.Fatal("encountered an error starting godyn in client mode", "missing parameters for the dyndns provider") } interval := strconv.Itoa(config.Interval()) notifications.Send(notifications.Notification{ Title: "godyn started", Message: "interval: " + interval + " seconds\n" + "ip-provider: " + config.IpProviderUrl(), Emoji: "white_circle", }) readIpFile() for config.Interval() > 0 { log.Info("waiting " + interval + " seconds before next remote/wan ip query and dyndns update...") time.Sleep(time.Duration(config.Interval()) * time.Second) updateDyndns() } log.DebugTimed("client mode finished", timestamp) } // unexported function(s) func readIpFile() { ipFile = filepath.Join(os.TempDir(), ".ipfile") log.Debug("using file '" + ipFile + "' to store ip address") timestamp := time.Now().UnixMilli() bytes, error := os.ReadFile(ipFile) if error != nil { if errors.Is(error, os.ErrNotExist) { return } log.ErrorTimed("encountered an error reading from the '.ipfile'", timestamp, "location: '"+ipFile+"'", error.Error()) } wanIp = string(bytes) log.DebugTimed("read latest remote/wan ip from '.ipfile'", timestamp, "value: '"+wanIp+"'", "location: '"+ipFile+"'") } func writeIpFile(value string) { timestamp := time.Now().UnixMilli() error := os.WriteFile(ipFile, []byte(value), 0644) if error != nil { log.ErrorTimed("encountered an error writing the '.ipfile'", timestamp, "value: '"+value+"'", "location: '"+ipFile+"'", error.Error()) return } log.DebugTimed("wrote latest remote/wan ip to '.ipfile'", timestamp, "value: '"+wanIp+"'", "location: '"+ipFile+"'") } func getWANIp() (string, error) { timestamp := time.Now().UnixMilli() var ip string providerUrl := config.IpProviderUrl() response, error := http.Get(providerUrl) if error != nil { return ip, error } body, error := io.ReadAll(response.Body) if error != nil { return ip, error } ip = strings.TrimSpace(string(body)) log.InfoTimed("got remote/wan ip '"+ip+"'", timestamp, "ip provider: '"+providerUrl+"'") return ip, nil } func buildDyndnsdUrl(wanIp string) string { return config.DyndnsUrl() + "?hostname=" + config.DyndnsHostname() + "&myip=" + wanIp } func updateDyndns() { timestamp := time.Now().UnixMilli() tmpIp, error := getWANIp() if error != nil { log.ErrorTimed("encountered an error getting the remote/wan ip", timestamp, "ip provider: '"+config.IpProviderUrl()+"'", error.Error()) } if len(tmpIp) <= 0 { log.ErrorTimed("will not update dyndns because remote/wan ip is empty", timestamp) } if tmpIp == wanIp { log.InfoTimed("nothing to do - remote/wan ip has not changed", timestamp, "ip: "+wanIp) return } dyndnsUrl := buildDyndnsdUrl(tmpIp) request, error := http.NewRequest("GET", dyndnsUrl, nil) if error != nil { msg := "encountered an error creating the http request to update dyndns" log.ErrorTimed(msg, timestamp, error.Error()) notifications.Send(notifications.Notification{ Title: "godyn", Message: msg + "\n" + error.Error(), Priority: notifications.PRIORITY_MAX, Emoji: "red_circle", }) return } request.SetBasicAuth(config.DyndnsUsername(), config.DyndnsPassword()) response, error := http.DefaultClient.Do(request) if error != nil { msg := "encountered an error updating dyndns" log.ErrorTimed(msg, timestamp, "url: '"+dyndnsUrl+"'", error.Error()) notifications.Send(notifications.Notification{ Title: "godyn", Message: msg + "\n" + error.Error(), Priority: notifications.PRIORITY_MAX, Emoji: "red_circle", }) return } bytes, error := io.ReadAll(response.Body) if error != nil { msg := "encountered an error reading the response from the dyndns provider" log.ErrorTimed(msg, timestamp, error.Error()) notifications.Send(notifications.Notification{ Title: "godyn", Message: msg + "\n" + error.Error(), Priority: notifications.PRIORITY_MAX, Emoji: "red_circle", }) return } body := strings.ToLower(strings.TrimSpace(string(bytes))) if !strings.Contains(body, tmpIp) { statuscode := strconv.Itoa(response.StatusCode) msg := "probably encountered an error updating dyndns" log.WarningTimed(msg, timestamp, "status code: "+statuscode+"'", "response: '"+body+"'") notifications.Send(notifications.Notification{ Message: msg + "\n" + "status code: " + statuscode + "\n" + "response: '" + body + "'", Priority: notifications.PRIORITY_MAX, Emoji: "red_circle", }) return } notification := notifications.Notification{} var msg string if strings.Contains(body, "abuse") { msg = "dyndns was not updated because remote/wan ip did not change and the dyndns provider reported an abusive update" log.WarningTimed(msg, timestamp, "response: '"+body+"'") notification.Priority = notifications.PRIORITY_HIGH notification.Emoji = "orange_circle" } else if strings.Contains(body, "nochg") { msg = "dyndns was not updated because remote/wan ip did not change" log.WarningTimed(msg, timestamp, "response: '"+body+"'") notification.Emoji = "yellow_circle" } else { msg = "dyndns was updated successfully" log.InfoTimed(msg, timestamp, "response: '"+strings.TrimSpace(string(bytes))+"'") msg += "\n" + "ip-address: " + tmpIp notification.Priority = notifications.PRIORITY_MAX notification.Emoji = "green_circle" } notification.Message = msg notifications.Send(notification) wanIp = tmpIp writeIpFile(wanIp) }