Skip to content

Commit

Permalink
Merge pull request #61 from neothematrix/2fa
Browse files Browse the repository at this point in the history
2fa implementation
  • Loading branch information
neothematrix authored May 2, 2024
2 parents 3d8440d + fe7ed6c commit 06aa540
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 35 deletions.
9 changes: 4 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 && \
Expand All @@ -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"]
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
75 changes: 56 additions & 19 deletions noip-renew.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@
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
import re
import base64
import subprocess

VERSION = "2.0.0"
DOCKER = False

class Logger:
def __init__(self, level):
self.level = 0 if level is None else level
Expand All @@ -45,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)

Expand Down Expand Up @@ -86,9 +92,29 @@ 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:
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(self.totp_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:
Expand Down Expand Up @@ -129,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):
Expand Down Expand Up @@ -190,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()
Expand All @@ -200,34 +226,45 @@ 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()
return rc


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__":
Expand Down
5 changes: 3 additions & 2 deletions noip-renew.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 14 additions & 4 deletions setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(){
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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" ]
Expand Down

0 comments on commit 06aa540

Please sign in to comment.