From 9dc4c4d44832bde2012c23bf39cb150085bc5d48 Mon Sep 17 00:00:00 2001
From: Gabriele <neothematrix@users.noreply.github.com>
Date: Wed, 1 May 2024 18:01:26 +0200
Subject: [PATCH 1/2] first 2fa test implementation

---
 noip-renew.py | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/noip-renew.py b/noip-renew.py
index 7f98edf..8035aac 100755
--- a/noip-renew.py
+++ b/noip-renew.py
@@ -21,6 +21,7 @@
 from selenium.webdriver.support import expected_conditions as EC
 from datetime import date
 from datetime import timedelta
+from pyotp import *
 import time
 import sys
 import os
@@ -28,6 +29,8 @@
 import base64
 import subprocess
 
+OTP_SECRET = ""
+
 class Logger:
     def __init__(self, level):
         self.level = 0 if level is None else level
@@ -89,6 +92,21 @@ def login(self):
         ele_pwd.send_keys(base64.b64decode(self.password).decode('utf-8'))
         ele_pwd.send_keys(Keys.ENTER)
         
+        try:
+            elem = WebDriverWait(self.browser, 10).until( EC.presence_of_element_located((By.ID, "verificationCode")))
+        except:
+            raise Exception("2FA verify page could not load")
+
+        if self.debug > 1:
+            self.browser.save_screenshot("debug-otp.png")
+        
+        self.logger.log("Sending OTP...")
+
+        ele_challenge = elem.find_element(By.NAME, "challenge_code")
+
+        ele_challenge.send_keys(TOTP(OTP_SECRET).now())
+        ele_challenge.send_keys(Keys.ENTER)
+
         # After Loggin browser loads my.noip.com page - give him some time to load
         # 'noip-cart' element is near the end of html, so html have been loaded
         try:

From fe7ed6c46497ab7d74f5f117296511ae91dc4666 Mon Sep 17 00:00:00 2001
From: Gabriele <neothematrix@users.noreply.github.com>
Date: Fri, 3 May 2024 01:37:32 +0200
Subject: [PATCH 2/2] adapting to 2fa required support

---
 Dockerfile    |  9 ++++----
 README.md     | 14 +++++++-----
 noip-renew.py | 61 +++++++++++++++++++++++++++++++++------------------
 noip-renew.sh |  5 +++--
 setup.sh      | 18 +++++++++++----
 5 files changed, 70 insertions(+), 37 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 5129d17..444b5d5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,19 +1,18 @@
 FROM debian
 LABEL maintainer="loblab"
 
-#ARG TZ=Asia/Shanghai
-#ARG APT_MIRROR=mirrors.163.com
 ARG DEBIAN_FRONTED=noninteractive
 ARG PYTHON=python3
 
-#RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
-#RUN sed -i "s/deb.debian.org/$APT_MIRROR/" /etc/apt/sources.list
+ENV CONTAINER=1
+
 RUN apt-get update && apt-get -y upgrade
 RUN apt-get -y install chromium-chromedriver || \
     apt-get -y install chromium-driver || \
     apt-get -y install chromedriver
 RUN apt-get -y install ${PYTHON}-pip
 RUN apt-get -y install ${PYTHON}-selenium
+RUN apt-get -y install ${PYTHON}-pyotp
 RUN apt-get -y install curl wget
 
 RUN mkdir -p /home/loblab && \
@@ -22,4 +21,4 @@ RUN mkdir -p /home/loblab && \
 USER loblab
 WORKDIR /home/loblab
 COPY /noip-renew.py /home/loblab/
-ENTRYPOINT ["python3", "/home/loblab/noip-renew.py"]
+ENTRYPOINT ["python3", "/home/loblab/noip-renew.py"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 1d15734..b7810da 100644
--- a/README.md
+++ b/README.md
@@ -40,12 +40,16 @@ grep -h Confirmed *.log | grep -v ": 0" | sort
 ```
 ## Usage with Docker
 
-For docker users, run the following:
+For docker users you need to define the following ENV variables:
+
+NOIP_USERNAME = '<your username>'
+NOIP_PASSWORD = '<your password (plain not base64 encoded)>'
+NOIP_2FA_SECRET_KEY = '<your 2FA secret key that appeared when you setup 2FA>'
+NOIP_DEBUG = <optional, defaults to 1>
+
+so you can run the following:
 ```sh
-my_username='add username here'
-my_password='add base64 encoded password here'
-debug_lvl=2
-echo -e "$(crontab -l)"$'\n'"12  3  *  *  1,3,5  docker run --rm --network host moebiuss/noip-renew ${my_username} ${my_password} ${debug_lvl}" | crontab -
+echo -e "$(crontab -l)"$'\n'"12  3  *  *  1,3,5  docker run --rm --network host -e NOIP_USERNAME='<your_username>' -e NOIP_PASSWORD='<your_password>' -e NOIP_2FA_SECRET_KEY='<your 2fa secret key>' -e NOIP_DEBUG=2 moebiuss/noip-renew" | crontab -
 ```
 NOTE: with newer versions of ChromeDriver (>v99) you might need to increase the shm size of the container otherwise ChromeDriver will crash and throw an exception. To do it, you can just add the "--shm-size="512m" flag to the docker run command.
 
diff --git a/noip-renew.py b/noip-renew.py
index 8035aac..a2577fc 100755
--- a/noip-renew.py
+++ b/noip-renew.py
@@ -29,7 +29,8 @@
 import base64
 import subprocess
 
-OTP_SECRET = ""
+VERSION = "2.0.0"
+DOCKER = False
 
 class Logger:
     def __init__(self, level):
@@ -48,10 +49,12 @@ class Robot:
     LOGIN_URL = "https://www.noip.com/login"
     HOST_URL = "https://my.noip.com/dynamic-dns"
 
-    def __init__(self, username, password, debug):
+    def __init__(self, username, password, totp_secret, debug, docker):
         self.debug = debug
+        self.docker = docker
         self.username = username
         self.password = password
+        self.totp_secret = totp_secret
         self.browser = self.init_browser()
         self.logger = Logger(debug)
 
@@ -89,7 +92,12 @@ def login(self):
         ele_pwd = elem.find_element(By.NAME, "password")
         
         ele_usr.send_keys(self.username)
-        ele_pwd.send_keys(base64.b64decode(self.password).decode('utf-8'))
+
+        # If running on docker, password is not base64 encoded
+        if self.docker:
+            ele_pwd.send_keys(self.password)
+        else:
+            ele_pwd.send_keys(base64.b64decode(self.password).decode('utf-8'))
         ele_pwd.send_keys(Keys.ENTER)
         
         try:
@@ -104,7 +112,7 @@ def login(self):
 
         ele_challenge = elem.find_element(By.NAME, "challenge_code")
 
-        ele_challenge.send_keys(TOTP(OTP_SECRET).now())
+        ele_challenge.send_keys(TOTP(self.totp_secret).now())
         ele_challenge.send_keys(Keys.ENTER)
 
         # After Loggin browser loads my.noip.com page - give him some time to load
@@ -147,10 +155,11 @@ def update_hosts(self):
         today = date.today() + timedelta(days=nr)
         day = str(today.day)
         month = str(today.month)
-        try:
-            subprocess.call(['/usr/local/bin/noip-renew-skd.sh', day, month, "True"])
-        except (FileNotFoundError,PermissionError):
-            self.logger.log(f"noip-renew-skd.sh missing or not executable, skipping crontab configuration")
+        if not self.docker:
+            try:
+                subprocess.call(['/usr/local/bin/noip-renew-skd.sh', day, month, "True"])
+            except (FileNotFoundError,PermissionError):
+                self.logger.log(f"noip-renew-skd.sh missing or not executable, skipping crontab configuration")
         return True
 
     def open_hosts_page(self):
@@ -208,8 +217,7 @@ def get_hosts(self):
 
     def run(self):
         rc = 0
-        version = "1.7.1"
-        self.logger.log(f"No-IP renew script version {version}")
+        self.logger.log(f"No-IP renew script version {VERSION}")
         self.logger.log(f"Debug level: {self.debug}")
         try:
             self.login()
@@ -218,10 +226,11 @@ def run(self):
         except Exception as e:
             self.logger.log(str(e))
             self.browser.save_screenshot("exception.png")
-            try:
-                subprocess.call(['/usr/local/bin/noip-renew-skd.sh', "*", "*", "False"])
-            except (FileNotFoundError,PermissionError):
-                self.logger.log(f"noip-renew-skd.sh missing or not executable, skipping crontab configuration")
+            if not self.docker:
+                try:
+                    subprocess.call(['/usr/local/bin/noip-renew-skd.sh', "*", "*", "False"])
+                except (FileNotFoundError,PermissionError):
+                    self.logger.log(f"noip-renew-skd.sh missing or not executable, skipping crontab configuration")
             rc = 2
         finally:
             self.browser.quit()
@@ -229,23 +238,33 @@ def run(self):
 
 
 def main(argv=None):
-    noip_username, noip_password, debug,  = get_args_values(argv)
-    return (Robot(noip_username, noip_password, debug)).run()
+    # check if we're running on docker
+    DOCKER = os.environ.get("CONTAINER", "").lower() in ("yes", "y", "on", "true", "1")
+    if DOCKER:
+        print("Running inside docker container")
+        noip_username = os.environ.get('NOIP_USERNAME')
+        noip_password = os.environ.get('NOIP_PASSWORD')
+        noip_totp = os.environ.get('NOIP_2FA_SECRET_KEY')
+        debug = int(os.environ.get('NOIP_DEBUG', 1))
+    else:
+        noip_username, noip_password, noip_totp, debug = get_args_values(argv)
+    return (Robot(noip_username, noip_password, noip_totp, debug, DOCKER)).run()
 
 
 def get_args_values(argv):
     if argv is None:
         argv = sys.argv
-    if len(argv) < 3:
-        print(f"Usage: {argv[0]} <noip_username> <noip_password> [<debug-level>] ")
+    if len(argv) < 4:
+        print(f"Usage: {argv[0]} <noip_username> <noip_base64encoded_password> <2FA_secret_key> [<debug-level>] ")
         sys.exit(1)
 
     noip_username = argv[1]
     noip_password = argv[2]
+    noip_totp = argv[3]
     debug = 1
-    if len(argv) > 3:
-        debug = int(argv[3])
-    return noip_username, noip_password, debug
+    if len(argv) > 4:
+        debug = int(argv[4])
+    return noip_username, noip_password, noip_totp, debug
 
 
 if __name__ == "__main__":
diff --git a/noip-renew.sh b/noip-renew.sh
index d0001b9..ce9b9d9 100755
--- a/noip-renew.sh
+++ b/noip-renew.sh
@@ -2,13 +2,14 @@
 
 USERNAME=""
 PASSWORD=""
+TOTP_SECRET=""
 
 LOGDIR=$1
 PROGDIR=$(dirname -- $0)
 
 if [ -z "$LOGDIR" ]; then
-    $PROGDIR/noip-renew.py "$USERNAME" "$PASSWORD" 2
+    $PROGDIR/noip-renew.py "$USERNAME" "$PASSWORD" "$TOTP_SECRET" 2
 else
     cd $LOGDIR
-    $PROGDIR/noip-renew.py "$USERNAME" "$PASSWORD" 0 >> $USERNAME.log
+    $PROGDIR/noip-renew.py "$USERNAME" "$PASSWORD" "$TOTP_SECRET" 0 >> $USERNAME.log
 fi
diff --git a/setup.sh b/setup.sh
index f8fa294..975ae37 100755
--- a/setup.sh
+++ b/setup.sh
@@ -35,9 +35,7 @@ function install() {
             install_debian
             ;;
     esac
-    # Debian9 package 'python-selenium' does not work with chromedriver,
-    # Install from pip, which is newer
-    $SUDO $PYTHON -m pip install selenium
+
     if [ "$PYTHON35" = true ]; then
         $SUDO $PYTHON -m pip install future-fstrings
     fi
@@ -47,7 +45,9 @@ function install_arch(){
     $SUDO pacman -Qi cronie > /dev/null ||  $SUDO pacman -S cronie
     $SUDO pacman -Qi python > /dev/null ||  $SUDO pacman -S python
     $SUDO pacman -Qi python-pip > /dev/null ||  $SUDO pacman -S python-pip
+    $SUDO pacman -Qi python-pyotp > /dev/null ||  $SUDO pacman -S python-pyotp
     $SUDO pacman -Qi chromium > /dev/null || $SUDO pacman -S chromium
+    $SUDO $PYTHON -m pip install selenium
 }
 
 function install_debian(){
@@ -88,6 +88,13 @@ function install_debian(){
         $SUDO apt -y install chromium # Update Chromium Browser or script won't work.
         
         $SUDO apt -y install $PYTHON-pip
+        $SUDO apt -y install $PYTHON-pyotp
+
+	if [[ "$PYV" -gt "36" ]]; then
+		$SUDO apt -y install $PYTHON-selenium
+	else
+		$SUDO $PYTHON -m pip install selenium
+	fi
 }
 
 function deploy() {
@@ -121,15 +128,18 @@ function deploy() {
 }
 
 function noip() {
-    echo "Enter your No-IP Account details..."
+    echo "Enter your No-IP Account details...make sure you enabled 2fa authentication and saved the 2fa secret key"
     read -p 'Username: ' uservar
     read -sp 'Password: ' passvar
+    echo
+    read -sp '2FA Secret Key: ' totpsecret
 
     passvar=`echo -n $passvar | base64`
     echo
 
     $SUDO sed -i 's/USERNAME=".*"/USERNAME="'$uservar'"/1' $INSTEXE
     $SUDO sed -i 's/PASSWORD=".*"/PASSWORD="'$passvar'"/1' $INSTEXE
+    $SUDO sed -i 's/TOTP_SECRET=".*"/TOTP_SECRET="'$totpsecret'"/1' $INSTEXE
 
     read -p 'Do you want randomized cronjob? (y/n): ' rcron
     if [ "${rcron^^}" = "Y" ]