diff --git a/docs/de/userlogin.png b/docs/de/userlogin.png new file mode 100644 index 00000000..0ab0cd71 Binary files /dev/null and b/docs/de/userlogin.png differ diff --git a/docs/de/userpermissions.png b/docs/de/userpermissions.png new file mode 100644 index 00000000..fe3d7790 Binary files /dev/null and b/docs/de/userpermissions.png differ diff --git a/lib/fritz.py b/lib/fritz.py new file mode 100644 index 00000000..6fd6f13a --- /dev/null +++ b/lib/fritz.py @@ -0,0 +1,120 @@ +import xml.etree.ElementTree as ET +import urllib.parse +import urllib.request +import time +import hashlib +import sys + +""" +Example code Python3 +#!/usr/bin/env python3 +# vim: expandtab sw=4 ts=4 + +FRITZ!OS WebGUI Login +Get a sid (session ID) via PBKDF2 based challenge response algorithm. Fallback to MD5 if FRITZ!OS has no PBKDF2 support. +AVM 2020-09-25 +""" + +LOGIN_SID_ROUTE = "/login_sid.lua?version=2" + + +class LoginState: + def __init__(self, challenge: str, blocktime: int): + self.challenge = challenge + self.blocktime = blocktime + self.is_pbkdf2 = challenge.startswith("2$") + + +def get_sid(box_url: str, username: str, password: str) -> str: + """ Get a sid by solving the PBKDF2 (or MD5) challenge-response process. """ + try: + state = get_login_state(box_url) + except Exception as ex: + raise Exception("failed to get challenge") from ex + if state.is_pbkdf2: + print("PBKDF2 supported") + challenge_response = calculate_pbkdf2_response( + state.challenge, password) + else: + print("Falling back to MD5") + challenge_response = calculate_md5_response(state.challenge, password) + if state.blocktime > 0: + print(f"Waiting for {state.blocktime} seconds...") + time.sleep(state.blocktime) + try: + sid = send_response(box_url, username, challenge_response) + except Exception as ex: + raise Exception("failed to login") from ex + if sid == "0000000000000000": + raise Exception("wrong username or password") + return sid + + +def get_login_state(box_url: str) -> LoginState: + """ Get login state from FRITZ!Box using login_sid.lua?version=2 """ + url = box_url + LOGIN_SID_ROUTE + http_response = urllib.request.urlopen(url) + xml = ET.fromstring(http_response.read()) + # print(f"xml: {xml}") + challenge = xml.find("Challenge").text + blocktime = int(xml.find("BlockTime").text) + return LoginState(challenge, blocktime) + + +def calculate_pbkdf2_response(challenge: str, password: str) -> str: + """ Calculate the response for a given challenge via PBKDF2 """ + challenge_parts = challenge.split("$") + # Extract all necessary values encoded into the challenge + iter1 = int(challenge_parts[1]) + salt1 = bytes.fromhex(challenge_parts[2]) + iter2 = int(challenge_parts[3]) + salt2 = bytes.fromhex(challenge_parts[4]) + # Hash twice, once with static salt... + # Once with dynamic salt. + hash1 = hashlib.pbkdf2_hmac("sha256", password.encode(), salt1, iter1) + hash2 = hashlib.pbkdf2_hmac("sha256", hash1, salt2, iter2) + return f"{challenge_parts[4]}${hash2.hex()}" + + +def calculate_md5_response(challenge: str, password: str) -> str: + """ Calculate the response for a challenge using legacy MD5 """ + response = challenge + "-" + password + # the legacy response needs utf_16_le encoding + response = response.encode("utf_16_le") + md5_sum = hashlib.md5() + md5_sum.update(response) + response = challenge + "-" + md5_sum.hexdigest() + return response + + +def send_response(box_url: str, username: str, challenge_response: str) -> str: + """ Send the response and return the parsed sid. raises an Exception on error """ + # Build response params + post_data_dict = {"username": username, "response": challenge_response} + post_data = urllib.parse.urlencode(post_data_dict).encode() + headers = {"Content-Type": "application/x-www-form-urlencoded"} + url = box_url + LOGIN_SID_ROUTE + # Send response + http_request = urllib.request.Request(url, post_data, headers) + http_response = urllib.request.urlopen(http_request) + # Parse SID from resulting XML. + xml = ET.fromstring(http_response.read()) + return xml.find("SID").text + + +def main(): + if len(sys.argv) < 4: + print( + f"Usage: {sys.argv[0]} http://fritz.box user pass" + ) + exit(1) + url = sys.argv[1] + username = sys.argv[2] + password = sys.argv[3] + sid = get_sid(url, username, password) + print(f"Successful login for user: {username}") + print(f"sid: {sid}") + + +if __name__ == "__main__": + main() diff --git a/main.js b/main.js index a2d5424b..396f92ad 100644 --- a/main.js +++ b/main.js @@ -51,8 +51,8 @@ var fritzTimeout; 512 = ON_OFF 513 = LEVEL_CTRL 514 = COLOR_CTRL -516 = ? detected with blinds -517 = ? detected with blinds +516 = ? detected with blinds, different alert? +517 = ? detected with blinds, different alerttimestamp 772 = SIMPLE_BUTTON 1024 = SUOTA-Update */ @@ -1734,6 +1734,7 @@ function main() { }, native: {} }); + adapter.setState(typ + newId + '.blindsopen', { val: false, ack: true }); //Button auf Ausgangszustand adapter.setObjectNotExists(typ + newId + '.blindsclose', { type: 'state', common: { @@ -1746,6 +1747,7 @@ function main() { }, native: {} }); + adapter.setState(typ + newId + '.blindsclose', { val: false, ack: true }); //Button auf Ausgangszustand adapter.setObjectNotExists(typ + newId + '.blindsstop', { type: 'state', common: { @@ -1758,6 +1760,7 @@ function main() { }, native: {} }); + adapter.setState(typ + newId + '.blindsstop', { val: false, ack: true }); //Button auf Ausgangszustand } function createDevices() { diff --git a/test/lib/fritzserver.js b/test/lib/fritzserver.js index a558a83b..065df0d7 100644 --- a/test/lib/fritzserver.js +++ b/test/lib/fritzserver.js @@ -56,7 +56,7 @@ function handleHttpRequest(request, response) { '0' ); response.end(); - } else if (request.url == '/login_sid.lua?version=2') { + } else if (request.url == '/login_sid.lua?version=2' && request.method == 'GET') { //check the URL of the current request response.writeHead(200, { 'Content-Type': 'application/xml' }); response.write( @@ -85,7 +85,7 @@ function handleHttpRequest(request, response) { '0Dial2App2HomeAuto2BoxAdmin2Phone2NAS2' ); response.end(); - } else if (request.url == '/login_sid.lua?version=2?username=admin&response=' + challengeResponse) { + } else if (request.url == '/login_sid.lua?version=2' && request.method == 'POST') { //check the URL of the current request response.writeHead(200, { 'Content-Type': 'application/xml' }); response.write(