From df34f8a144e3dd62f191b95f7254815642450bc4 Mon Sep 17 00:00:00 2001 From: Tomas Date: Tue, 10 Sep 2024 23:31:54 +0200 Subject: [PATCH 1/2] Attempt to fix issue when IP cannot be fetched. Added debug log --- Dockerfile | 4 +-- README.md | 47 +++++++++++++++++++++++++++++ container-entrypoint.sh | 4 +-- docker-build.sh | 2 +- logger.go | 59 +++++++++++++++++++++++++----------- main.go | 32 +++++++++++++++++--- model.go | 2 +- updaterecord.go | 67 ++++++++++++++++++++++++++++++----------- 8 files changed, 171 insertions(+), 46 deletions(-) diff --git a/Dockerfile b/Dockerfile index f79fb50..8f7a6de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.23.1-alpine3.20 as build +FROM golang:1.23.1-alpine3.20 AS build ARG OS ARG ARCH COPY . /build/ @@ -19,4 +19,4 @@ RUN apk update && apk --no-cache add bash && addgroup -g ${gid} ${group} && addu RUN chown ncddns:ncddns /app/ncddns && chmod +x /app/ncddns && \ chown ncddns:ncddns /app/container-entrypoint.sh && chmod +x /app/container-entrypoint.sh USER ncddns -ENTRYPOINT [ "/app/container-entrypoint.sh"] \ No newline at end of file +ENTRYPOINT [ "/app/container-entrypoint.sh"] diff --git a/README.md b/README.md index 595caec..4ababc7 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ Here, `NC_PASS` is your Dynamic DDNS password which is generated from Namecheap. +You can also use an additional optional env variable: + +`CUSTOM_IPCHECK_URL` is an optional variable where you can specify any URL that is used to get the current IP. The program uses HTTP GET method without additional parameters and expects a valid JSON with `ip` field. + * Check the log ``` @@ -57,6 +61,49 @@ docker start server.example.com # To start after its stopped docker rm server.example.com -f # To remove ``` +### Alternative Configuration using Docker Compose + +If you prefer to configure and manage your Docker containers using Docker Compose, follow the steps outlined below. This approach simplifies the configuration and management by allowing you to define all necessary parameters in a `docker-compose.yml` file. + +1. Ensure that Docker and Docker Compose are installed on your server. + +2. Create a `docker-compose.yml` file with the following content. Replace the placeholder values with your actual configuration: + + ```yaml + services: + ddns: + image: linuxshots/namecheap-ddns + container_name: server.example.com + environment: + - NC_HOST=server + - NC_DOMAIN=example.com + - NC_PASS=DynamicDDNSPa2w0rd + restart: unless-stopped + ``` + +3. Run the following command in the directory containing your `docker-compose.yml` file to start the container: + + ```sh + docker-compose up -d + ``` + +4. Check the logs to ensure the service is running correctly: + + ```sh + docker-compose logs ddns + ``` + +5. To manage the container (stop, start, remove), you can use Docker Compose commands: + + ```sh + docker-compose stop ddns # To stop the service + docker-compose start ddns # To start the service again after it’s stopped + docker-compose down # To stop and remove the container + ``` + +By using Docker Compose, you simplify the deployment process and gain better control over your Docker services. This method is particularly useful for managing multiple containers and configurations in a unified manner. + + ## Build your own image To build your own image diff --git a/container-entrypoint.sh b/container-entrypoint.sh index a022bdf..af7ebba 100644 --- a/container-entrypoint.sh +++ b/container-entrypoint.sh @@ -3,9 +3,9 @@ set -e if [ "$NC_HOST" == "" -o "$NC_DOMAIN" == "" -o "$NC_PASS" == "" ]; then - echo "ERROR NC_HOST, NC_DOMAIN and GD_PASS are mandatory." + echo "ERROR NC_HOST, NC_DOMAIN and NC_PASS are mandatory." echo "Use --env with docker run to pass these environment variables." exit 1 fi -/app/ncddns --host="$NC_HOST" --domain="$NC_DOMAIN" --password="$NC_PASS" \ No newline at end of file +/app/ncddns --host="$NC_HOST" --domain="$NC_DOMAIN" --password="$NC_PASS" --custom-ipcheck-url="$CUSTOM_IPCHECK_URL" --log-level="$LOG_LEVEL" \ No newline at end of file diff --git a/docker-build.sh b/docker-build.sh index 2b16c13..3e332d4 100644 --- a/docker-build.sh +++ b/docker-build.sh @@ -3,7 +3,7 @@ if [ $# -ne 1 ]; then echo "Exactly one argument required" echo "bash docker-build.sh VERSION" - echo " e.g. bash docker-build.sh 1.3.0" + echo " e.g. bash docker-build.sh 1.4.0" exit 1 fi diff --git a/logger.go b/logger.go index 6911288..25c92c5 100644 --- a/logger.go +++ b/logger.go @@ -6,31 +6,54 @@ import ( "os" ) +// LogLevel constants const ( - ErrorLog string = "ERROR" + DebugLog string = "DEBUG" InformationLog string = "INFO" - WarningLog string = "WARN" + WarningLog string = "WARN" + ErrorLog string = "ERROR" ) -func DDNSLogger(logType, host, domain, message string) { - - var ( - StdoutInfoLogger *log.Logger - StdoutWarningLogger *log.Logger - StdoutErrorLogger *log.Logger - ) +var ( + logLevel string + StdoutInfoLogger *log.Logger + StdoutWarningLogger *log.Logger + StdoutErrorLogger *log.Logger + StdoutDebugLogger *log.Logger + logLevelPriority map[string]int +) +func init() { + // Initialize loggers StdoutInfoLogger = log.New(os.Stdout, "INFO ", log.Ldate|log.Ltime) StdoutWarningLogger = log.New(os.Stdout, "WARNING ", log.Ldate|log.Ltime) StdoutErrorLogger = log.New(os.Stdout, "ERROR ", log.Ldate|log.Ltime) - - if logType == "INFO" { - StdoutInfoLogger.Println(host+"."+domain, message) - } else if logType == "WARN" { - StdoutWarningLogger.Println(host+"."+domain, message) - } else if logType == "ERROR" { - StdoutErrorLogger.Println(host+"."+domain, message) - } else { - fmt.Println(host+"."+domain, message) + StdoutDebugLogger = log.New(os.Stdout, "DEBUG ", log.Ldate|log.Ltime) + + // Initialize log level priorities + logLevelPriority = map[string]int{ + DebugLog: 1, + InformationLog: 2, + WarningLog: 3, + ErrorLog: 4, } } + +func DDNSLogger(logType, host, domain, message string) { + // Ensure logType maps to a valid priority + if logLevelPriority[logType] >= logLevelPriority[logLevel] { + logMessage := fmt.Sprintf("%s.%s %s", host, domain, message) + switch logType { + case DebugLog: + StdoutDebugLogger.Println(logMessage) + case InformationLog: + StdoutInfoLogger.Println(logMessage) + case WarningLog: + StdoutWarningLogger.Println(logMessage) + case ErrorLog: + StdoutErrorLogger.Println(logMessage) + default: + fmt.Println(logMessage) + } + } +} \ No newline at end of file diff --git a/main.go b/main.go index 29c8718..ef939a1 100644 --- a/main.go +++ b/main.go @@ -13,17 +13,39 @@ func main() { domain := flag.String("domain", "", "Domain name e.g. example.com") host := flag.String("host", "", "Subdomain or hostname e.g. www") password := flag.String("password", "", "Dynamic DNS Password from Namecheap") - // iPCacheTimeOutSeconds := flag.Int("ip-cache-timeout", 86400, "IP cache timeout in seconds.") + custom_ipcheck_url := flag.String("custom-ipcheck-url", "", "Custom IP check URL. Script always falls back to default ones") + logLevelFlag := flag.String("log-level", InformationLog, "Log level (DEBUG, INFO, WARN, ERROR) - default is INFO") flag.Parse() + + // Set log level based on flag + logLevel = *logLevelFlag + + if logLevel == "" { + logLevel = InformationLog + } + + // Validate log level + validLogLevels := map[string]bool{ + DebugLog: true, + InformationLog: true, + WarningLog: true, + ErrorLog: true, + } + + if !validLogLevels[logLevel] { + fmt.Println("ERROR: Invalid log level. Supported values are DEBUG, INFO, WARN, ERROR") + os.Exit(1) + } + if *domain == "" || *host == "" || *password == "" { - fmt.Println("ERROR domain, host and Dynamic DDNS password are mandatory") + fmt.Println("ERROR: domain, host and Dynamic DDNS password are mandatory") fmt.Printf("\nUsage of %s:\n", os.Args[1]) flag.PrintDefaults() os.Exit(1) } - pubIp, err := getPubIP() + pubIp, err := getPubIP(*custom_ipcheck_url) if err != nil { DDNSLogger(ErrorLog, *host, *domain, err.Error()) } else { @@ -35,5 +57,5 @@ func main() { } } - updateRecord(*domain, *host, *password) -} + updateRecord(*domain, *host, *password, *custom_ipcheck_url) +} \ No newline at end of file diff --git a/model.go b/model.go index 0122abc..0e34f95 100644 --- a/model.go +++ b/model.go @@ -15,7 +15,7 @@ func (err *CustomError) Error() string { } var ( - version string = "1.3.0-go1.23" + version string = "1.4.0-go1.23" daemon_poll_time time.Duration = 1 * time.Minute // Time in minute gitrepo string = "https://github.com/navilg/namecheap-ddns-docker" httpTimeout time.Duration = 30 * time.Second diff --git a/updaterecord.go b/updaterecord.go index 2a254cf..516380c 100644 --- a/updaterecord.go +++ b/updaterecord.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "encoding/json" "encoding/xml" "errors" @@ -13,7 +14,7 @@ import ( "time" ) -func updateRecord(domain, host, password string) { +func updateRecord(domain, host, password, custom_ipcheck_url string) { DDNSLogger(InformationLog, "", "", "Started daemon process") @@ -27,16 +28,17 @@ func updateRecord(domain, host, password string) { return case <-ticker.C: - pubIp, err := getPubIP() + pubIp, err := getPubIP(custom_ipcheck_url) if err != nil { DDNSLogger(ErrorLog, host, domain, err.Error()) + continue // Move to the next tick } currentIp := os.Getenv("NC_PUB_IP") lastIpUpdatedStr := os.Getenv("NC_PUB_IP_TIME") var lastIpUpdatedDuration float64 - fmt.Println(lastIpUpdatedStr) + //fmt.Println(lastIpUpdatedStr) lastIpUpdated, err := time.Parse("2006-01-02 15:04:05", lastIpUpdatedStr) if err != nil { DDNSLogger(WarningLog, host, domain, "Not able to fetch last IP updated time. "+err.Error()) @@ -87,43 +89,74 @@ func updateRecord(domain, host, password string) { done <- true } -func getPubIP() (string, error) { +func fetchIPFromURL(url, sourceName string) (*http.Response, error) { + apiclient := &http.Client{Timeout: httpTimeout} + + response, err := apiclient.Get(url) + if err != nil { + DDNSLogger(DebugLog, "", "", "IP could not be fetched from "+sourceName+" - error: "+err.Error()) + return nil, err + } + DDNSLogger(DebugLog, "", "", "IP fetched from "+sourceName) + return response, nil +} + +func getPubIP(custom_ipcheck_url string) (string, error) { type GetIPBody struct { IP string `json:"ip"` } var ipbody GetIPBody + var response *http.Response + var err error - apiclient := &http.Client{Timeout: httpTimeout} + if custom_ipcheck_url != "" { + response, err = fetchIPFromURL(custom_ipcheck_url, "custom URL") + if err == nil { + goto ParseResponse + } + } - response, err := apiclient.Get("https://api.ipify.org?format=json") + response, err = fetchIPFromURL("https://ipinfo.io/json", "ipinfo.io") if err != nil { - response, err = apiclient.Get("https://ipinfo.io/json") + response, err = fetchIPFromURL("https://api.ipify.org/?format=json", "ipify.org") if err != nil { - return "", nil + return "", &CustomError{ErrorCode: -1, Err: errors.New("IP could not be fetched from either endpoint")} } } +ParseResponse: defer response.Body.Close() - bodyBytes, err := io.ReadAll(response.Body) + + rawResponse, err := io.ReadAll(response.Body) if err != nil { - // fmt.Println(err.Error()) - return "", &CustomError{ErrorCode: response.StatusCode, Err: errors.New("IP could not be fetched." + err.Error())} + return "", &CustomError{ErrorCode: response.StatusCode, Err: errors.New("IP could not be fetched: " + err.Error())} + } + response.Body = io.NopCloser(bytes.NewBuffer(rawResponse)) + + // Convert headers to a string + headersString := "" + for key, values := range response.Header { + headersString += key + ": " + fmt.Sprintf("%v", values) + "\n" } - err = json.Unmarshal(bodyBytes, &ipbody) + // Print raw response headers and body + DDNSLogger(DebugLog, "", "", "Response headers:\n"+headersString) + DDNSLogger(DebugLog, "", "", "Response body: "+string(rawResponse)) + + err = json.Unmarshal(rawResponse, &ipbody) if err != nil { - // fmt.Println(err.Error()) - return "", &CustomError{ErrorCode: response.StatusCode, Err: errors.New("IP could not be fetched." + err.Error())} + return "", &CustomError{ErrorCode: response.StatusCode, Err: errors.New("IP could not be fetched: " + err.Error())} } if ipbody.IP == "" { return "", &CustomError{ErrorCode: response.StatusCode, Err: errors.New("IP could not be fetched. Empty IP value detected")} } - return ipbody.IP, nil + DDNSLogger(DebugLog, "", "", "IP fetched: "+ipbody.IP) + return ipbody.IP, nil } func setDNSRecord(host, domain, password, pubIp string) error { @@ -133,8 +166,8 @@ func setDNSRecord(host, domain, password, pubIp string) error { } type InterfaceResponse struct { - ErrorCount int `xml:"ErrCount"` - Errors InterfaceError `xml:"errors"` + ErrorCount int `xml:"ErrCount"` + Errors InterfaceError `xml:"errors"` } var interfaceResponse InterfaceResponse From 14e593d561f739beda7371bb9f4b74d55fddba0e Mon Sep 17 00:00:00 2001 From: Tomas Date: Wed, 11 Sep 2024 01:12:10 +0200 Subject: [PATCH 2/2] Improve README.md. Fix indentation --- README.md | 4 +++- logger.go | 18 +++++++++--------- main.go | 6 +++--- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4ababc7..e1d7a9b 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,12 @@ Here, `NC_PASS` is your Dynamic DDNS password which is generated from Namecheap. -You can also use an additional optional env variable: +You can also use an additional optional env variables: `CUSTOM_IPCHECK_URL` is an optional variable where you can specify any URL that is used to get the current IP. The program uses HTTP GET method without additional parameters and expects a valid JSON with `ip` field. +`LOG_LEVEL` can be DEBUG, INFO, WARN, ERROR. Default is INFO + * Check the log ``` diff --git a/logger.go b/logger.go index 25c92c5..913fe37 100644 --- a/logger.go +++ b/logger.go @@ -8,19 +8,19 @@ import ( // LogLevel constants const ( - DebugLog string = "DEBUG" + DebugLog string = "DEBUG" InformationLog string = "INFO" - WarningLog string = "WARN" - ErrorLog string = "ERROR" + WarningLog string = "WARN" + ErrorLog string = "ERROR" ) var ( - logLevel string - StdoutInfoLogger *log.Logger + logLevel string + StdoutInfoLogger *log.Logger StdoutWarningLogger *log.Logger StdoutErrorLogger *log.Logger StdoutDebugLogger *log.Logger - logLevelPriority map[string]int + logLevelPriority map[string]int ) func init() { @@ -32,10 +32,10 @@ func init() { // Initialize log level priorities logLevelPriority = map[string]int{ - DebugLog: 1, + DebugLog: 1, InformationLog: 2, - WarningLog: 3, - ErrorLog: 4, + WarningLog: 3, + ErrorLog: 4, } } diff --git a/main.go b/main.go index ef939a1..75b10ea 100644 --- a/main.go +++ b/main.go @@ -27,10 +27,10 @@ func main() { // Validate log level validLogLevels := map[string]bool{ - DebugLog: true, + DebugLog: true, InformationLog: true, - WarningLog: true, - ErrorLog: true, + WarningLog: true, + ErrorLog: true, } if !validLogLevels[logLevel] {