#!/usr/bin/env python3
# -*- coding: utf-8 -*-
### *|--------------------------------|* ###
### *| NO-IP Account-Renew Script 1.0 |* ###
### *| (c) by DarkStarXxX @ DEB |* ###
### *|--------------------------------|* ###
# Modifiziert von MegaV0lt @ DEB
# - Benötigt Python 3
# - Umstellung auf 'mechanicalsoup.StatefulBrowser()'
# - Prüfung, ob Konfig-Datei existiert
# - Umstellung auf *.ini konfiguration
# - Konfiguration (*.ini) kann als Parameter übergeben werden
# - Automatisches Eintragen des Verifizierungs-Codes
VERSION=250206 # JJMMDD
# Modifiziert von SLASH @ DEB
# - Skript nutzt nun python3 direkt (#!/usr/bin/env python3)
# - Update für eMail-imports
# Modifiziert von bl0w @DEB
# Wichtiger Hinweis: Externe Konfiguration
# Die Login-Daten für No-IP und eMail können aus einer externen Datei, welche in
# der Variable "CONFIG_FILE" definiert werden muss gelesen werden.
# Bitte dort die Daten eintragen
CONFIG_FILE = 'NOIP_Account_Renew.ini' # Im gleichen Verzeichnis wie das Skript!
#CONFIG_FILE = '' # Kann abgeschaltet werden, in dem man CONFIG_FILE = '' setzt.
# Die Konfiguration kann dem Skript auch als Parameter übergeben werden:
# NOIP-Account-Renew MyIni.ini
# Oder die Variablen hier ausfüllen!
USERNAME = '' # No-IP Benutzername
PASSWORD = '' # No-IP Passwort
FROMADRESS = '' # eMail-Sender / Benutzername
TOADRESS = '' # eMail-Empfänger
SMTPSERVER = '' # SMTP-Serveradresse ("" zum deaktivieren)
SMTPPORT = '' # SMTP-Port (Z. B. 25)
SMTPPASS = '' # Server verlangt Autentification ("" zum deaktivieren)
IMAPSERVER = '' # IMAP-Serveradresse ("" zum deaktivieren)
IMAPPORT = '' # IMAP-Port (Z. B. 993)
IMAPPASS = '' # IMAP-Passwort ("" zum deaktivieren)
# Vorgaben
LOG_FILE = '' # Kein Log, wenn nächste Zeile auskommentiert ist
#LOG_FILE = '/var/log/NOIP_renew.log' # Dateiname wird auch für die eMail verwendet
MAIL_SUBJECT = 'NO-IP Account Updater' # Betreff der Status eMail
MAIL_BODY = 'NO-IP Account-Renew #{VERSION} hat Ihr NO-IP Konto für weitere 30 Tage aktualisiert.\n\n' # Text in der eMail
HOST_URL = 'https://www.noip.com/members/dns/'
RESULT_STR = []
DEBUG = False # Debug-Modus
import platform # Python-Version
import importlib # Konfiguration
import mechanicalsoup # Browser
import time # Zeit
import ssl # SSL
import re # Regex
import imaplib # eMail
import email # eMail
if hasattr(ssl, '_create_unverified_context'):
ssl._create_default_https_context = ssl._create_unverified_context
# Funktion: update_host() - Für jeden Host "Modify" klicken
def update_host(str_host, browser):
global RESULT_STR
host_id = after(str_host, '=') # Alle Zeichen nach dem '='
browser.select_form(nr=00) ; browser.submit_selected()
response = browser.get_current_page()
update_status = None
if response.find('Update will be applied') == 'None' : # Prüfen, ob es geklappt hat
update_status = '[FEHLER]'
else:
update_status = '[OK]'
if LOG_FILE:
ts = time.strftime("%d.%m.%Y %H:%M:%S") # Zeitstempel für das Log (19.10.2016 12:51:30)
f1 = open(LOG_FILE, 'a+')
print(f'{ts}: Update für Host-ID: {host_id} {update_status}', file=f1) # Ausgabe in das Log
f1.close()
print(f'Update für Host-ID: …{host_id[-2:]} {update_status}') # Ausgabe auf der Konsole (und cron)
RESULT_STR.append('…' + host_id[-2:] + ' {update_status}') # Für die eMail
return
# Funktion: after() - Liefert Zeichenkette nach Zeichen a
def after(value, a):
pos_a = value.rfind(a) # Position des gesuchten Zeichens
if pos_a == -1: return value # Nicht gefunden: Ganzer Wert zurück!
adjusted_pos_a = pos_a + len(a) # Alles nach dem gefundenen Zeichen
if adjusted_pos_a >= len(value): return ''
return value[adjusted_pos_a:]
# Funktion: get_verification_code() - Liefert den Verifizierungscode aus der eMail
def get_verification_code(from_address, imap_server, imap_pass, timeout=30, debug=False):
start_time = time.time()
while True:
time.sleep(5) # 5 Sekunden warten
try:
mail = imaplib.IMAP4_SSL(imap_server)
mail.login(from_address, imap_pass)
mail.select('inbox') # Posteingang
status, messages = mail.search(None, '(HEADER Subject "No-IP Verification Code:")')
if status == 'OK' and messages[0]:
latest_id = messages[0].split()[-1]
_, data = mail.fetch(latest_id, '(RFC822)')
message = email.message_from_bytes(data[0][1])
verification_code = message['subject'].split()[-1]
mail.close(); mail.logout()
return verification_code
except Exception as e:
if debug: print(f'Fehler: {e}')
if time.time() - start_time > timeout:
raise TimeoutError('FEHLER: Keine eMail mit Verifications Code gefunden!')
###* Start *###
print(f'NO-IP Account-Renew #{VERSION} (Python: {platform.python_version()})')
# Wenn eine *.ini als Parameter übergeben wurde, diese verwenden
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('filename', nargs='?', action='store')
args = parser.parse_args()
if args.filename is not None: CONFIG_FILE = args.filename
if CONFIG_FILE:
from pathlib import Path
my_conf = Path(CONFIG_FILE)
if my_conf.is_file(): # Datei vorhanden?
print('Verwende Konfiguration aus:', my_conf)
import configparser
config = configparser.ConfigParser(interpolation=None)
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config.read_file(f)
USERNAME = config['NOIP']['USERNAME'] ; PASSWORD = config['NOIP']['PASSWORD']
FROMADRESS = config['MAIL']['FROMADRESS'] ; TOADRESS = config['MAIL']['TOADRESS']
SMTPSERVER = config['MAIL']['SMTPSERVER'] ; SMTPPORT = config['MAIL']['SMTPPORT']
SMTPPASS = config['MAIL']['SMTPPASS']
IMAPSERVER = config['MAIL']['IMAPSERVER'] ; IMAPPORT = config['MAIL']['IMAPPORT']
IMAPPASS = config['MAIL']['IMAPPASS']
else: print(f'FEHLER: Konfiguration {my_conf} nicht gefunden!') ; quit()
# Prüfen, ob benötigte Werte konfiguriert sind
if not USERNAME: print('FEHLER: USERNAME ist nicht konfiguriert!') ; quit()
if not PASSWORD: print('FEHLER: PASSWORD ist nicht konfiguriert!') ; quit()
if not IMAPSERVER: print('FEHLER: IMAPSERVER ist nicht konfiguriert!') ; quit()
if not IMAPPASS: print('FEHLER: IMAPPASS ist nicht konfiguriert!') ; quit()
if not FROMADRESS: print('FEHLER: FROMADRESS ist nicht konfiguriert!') ; quit()
# Browser Optionen
browser = mechanicalsoup.StatefulBrowser(
soup_config={'features': 'lxml'}, # Verwenden des lxml HTML parser
raise_on_404=True,
user_agent='Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.2228.0 Safari/537.36',
)
# Seite öffnen (noip.com)
print('Log-In… ', end='') # Kein Zeilenumbruch
browser.open(HOST_URL)
if DEBUG:
response = browser.get_current_page()
with open('response_host_url.html', 'w', encoding='utf-8') as f:
f.write(str(response))
# Login mit Benutzername und Passwort
browser.select_form(nr=2)
browser['username'] = USERNAME
browser['password'] = PASSWORD
browser.submit_selected()
# Login OK?
response = browser.get_current_page()
if DEBUG:
with open('response_login.html', 'w', encoding='utf-8') as f:
f.write(str(response))
verification_code = None
if str(response).find(USERNAME.lower()) == -1:
if DEBUG: print(f'Benutzername {USERNAME.lower()} nicht gefunden!')
if str(response).find('Check your email for a verification code') == -1:
if DEBUG: print('Formular für Verifications Code nicht gefunden!')
print('FEHLER: Login nicht erfolgreich!')
quit()
else:
print('Warte auf Verifications Code eMail…')
verification_code = get_verification_code(FROMADRESS, IMAPSERVER, IMAPPASS, 30, debug=DEBUG)
# Rückgabe ist der Code oder ein Timeout Error
#if not verification_code:
# print('FEHLER: Verifications Code nicht gefunden!')
# quit()
else:
print('OK!')
# Verifications Code eingeben
if verification_code is not None:
browser.select_form('#challenge_form')
browser["challenge_code"] = verification_code
# Checkbox 'Trust this device' aktivieren
page = browser.get_current_page()
checkbox = page.find('input', {'id': 'trust_device_checkbox'})
if checkbox:
checkbox['checked'] = 'checked'
browser.submit_selected()
# Login nach Code eingabe OK?
response = browser.get_current_page()
if DEBUG:
with open('response_after_code_entry.html', 'w', encoding='utf-8') as f:
f.write(str(response))
if str(response).find(USERNAME.lower()) == -1:
print('FEHLER: Login nach Code eingabe nicht erfolgreich!')
quit()
else:
print('Eingabe des Verifications Code erfolgreich!')
# Links in Array speichern (Modify)
mylink = [] # Leeres Array
for link in browser.links(link_text='Modify'):
mylink += [link]
# Anzeigen wie viele Links gefunden wurden
if not mylink:
print('FEHLER: Keine Hosts gefunden!')
quit()
else:
print(f'Gefundene Hosts: {len(mylink)}. Starte Update…')
# Links klicken und Updaten
for link in mylink:
target = link.attrs['href']
browser.open_relative(target) # Zur Seite gehen
update_host(target, browser) # Host updaten
###* SMTP Abschnitt *###
if not SMTPSERVER: quit() # Nur wenn SMTPSERVER gesetzt ist
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
msg = MIMEMultipart()
msg['From'] = FROMADRESS ; msg['To'] = TOADRESS
msg['Subject'] = MAIL_SUBJECT
body = MAIL_BODY
for result in RESULT_STR:
body = str(body) + str(result) + '\n'
msg.attach(MIMEText(body, 'plain'))
if LOG_FILE:
filename = after(LOG_FILE, '/') # Dateiname ohne Pfad
attachment = open(LOG_FILE, 'rb')
part = MIMEBase('application', 'octet-stream')
part.set_payload((attachment).read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename= %s' % filename)
msg.attach(part)
# Beispiel: Server Addresse und TCP Port: '192.168.1.1', 25
server = smtplib.SMTP(SMTPSERVER, SMTPPORT)
server.starttls()
# Server verlangt Autentification (Nur wenn SMTPPASS gesetzt ist)
if SMTPPASS: server.login(FROMADRESS, SMTPPASS)
text = msg.as_string()
server.sendmail(FROMADRESS, TOADRESS, text)
server.quit()