|
1 | | -import time, urllib, urllib2, hashlib, pprint, re, sys |
2 | | -import urllib, urllib2, socket, httplib |
3 | | - |
4 | | -import json |
| 1 | +import time, hashlib, re, urllib, json, bs4, pprint |
| 2 | +import xml.etree.ElementTree as ET |
| 3 | +import requests |
5 | 4 |
|
6 | 5 | """ |
7 | 6 | Being lazy with globals because you probably don't have more than one in your LAN |
8 | 7 |
|
9 | | - Note that login takes a little time. The _fritz_sid keeps the login token so if you can |
10 | | - keep the interpreter running you can get fster fetches |
11 | | -
|
| 8 | + Note that login takes a little time. The _fritz_sid keeps the login token so that |
| 9 | + if you can keep the interpreter running you can get faster fetches |
12 | 10 |
|
13 | 11 | CONSIDER: fetching things beyond transfers. |
14 | | -""" |
15 | 12 |
|
16 | | -_fritz_sid = None |
17 | | -_fritz_lastfetched = 0 |
18 | | -_fritz_lastdata = None |
| 13 | + So, there are two variants. |
| 14 | + The older one is MD5-based, |
| 15 | + the newer one (?version=2) is SHA256-based |
19 | 16 |
|
20 | | -IP = '192.168.178.1' |
21 | | -username = '' |
22 | | -password = 'change_me' |
23 | | - |
24 | | - |
25 | | -def urlfetch(url, data=None, headers=None, raise_as_none=False, return_reqresp=False): |
26 | | - """ Returns: |
27 | | - - if return_reqresp==False (default), returns the data at an URL |
28 | | - - if return_reqresp==True, returns the request and response objects (can be useful for streams) |
29 | | -
|
30 | | - data: May be |
31 | | - - a dict |
32 | | - - a sequence of tuples (will be encoded), |
33 | | - - a string (not altered - you often want to have used urllib.urlencode) |
34 | | - When you use this parameter, the request becomes a POST instead of the default GET |
35 | | - (and seems to force <tt>Content-type: application/x-www-form-urlencoded</tt>) |
36 | | - headers: dict of additional headers (each is add_header()'d) |
37 | | - raise_as_none: In cases where you want to treat common connection failures as 'try again later', |
38 | | - using True here can save a bunch of your own typing in error catching |
39 | | - """ |
40 | | - try: |
41 | | - if type(data) in (tuple, dict): |
42 | | - data=urllib.urlencode(data) |
43 | | - req = urllib2.Request(url, data=data) |
44 | | - if headers!=None: |
45 | | - for k in headers: |
46 | | - vv = headers[k] |
47 | | - if type(vv) in (list,tuple): |
48 | | - for v in vv: |
49 | | - req.add_header(k,v) |
50 | | - else: # assume single string. TODO: consider unicode |
51 | | - req.add_header(k,vv) |
52 | | - response = urllib2.urlopen(req, timeout=60) |
53 | | - if return_reqresp: |
54 | | - return req,response |
55 | | - else: |
56 | | - data = response.read() |
57 | | - return data |
58 | | - except (socket.timeout), e: |
59 | | - if raise_as_none: |
60 | | - sys.stderr.write( 'Timeout fetching %r\n'%url ) |
61 | | - return None |
62 | | - else: |
63 | | - raise |
64 | | - except (socket.error, urllib2.URLError, httplib.HTTPException), e: |
65 | | - if raise_as_none: |
66 | | - #print 'Networking problem, %s: %s'%(e.__class__, str(e)) |
67 | | - return None |
68 | | - else: |
69 | | - raise |
| 17 | + I'm still confused as to why the older version broke, |
| 18 | + because it seems requests to login_sid.lua default to the older style |
| 19 | + |
| 20 | +
|
| 21 | + https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AVM_Technical_Note_-_Session_ID_english_2021-05-03.pdf |
70 | 22 |
|
| 23 | +""" |
71 | 24 |
|
| 25 | +_fritz_sid = None |
| 26 | +_fritz_lastfetched = 0 |
| 27 | +_fritz_lastdata = None |
72 | 28 |
|
73 | | -def fritz_login(): |
74 | | - data = urlfetch('http://%s/login_sid.lua'%(IP,)) |
75 | | - m = re.search('<Challenge>([0-9a-f]+)</Challenge>', data) |
76 | | - challenge = m.groups()[0] |
77 | | - m5h = hashlib.md5() |
78 | | - hashstr = '%s-%s'%(challenge, password) |
79 | | - m5h.update(hashstr.encode('utf_16_le')) |
80 | | - response = m5h.hexdigest() |
81 | | - data = urlfetch('http://%s/login_sid.lua'%(IP,), {'response':'%s-%s'%(challenge, response)}) |
82 | | - m = re.search('<SID>([0-9a-f]+)</SID>', data) |
83 | | - return m.groups()[0] |
| 29 | +#as str, not bytestr |
| 30 | +IP = '192.168.178.1' |
| 31 | +username = 'changeme' # depending on the fritzbox age, this may be empty, or effectively decided for you and you can fish it out of the regular HTML login page |
| 32 | +password = 'changeme' |
| 33 | + |
| 34 | + |
| 35 | + |
| 36 | +def fritz_login(version=1, verbose=False): |
| 37 | + ''' TODO: Make version 2 work, |
| 38 | + TODO: review for clarity with all the encodings |
| 39 | + CONSIDER: fetch username from first request, so we don't have to set it (but check how that worked on older versions) |
| 40 | +
|
| 41 | + Returns |
| 42 | + None if the fritzbox signals that we're being blocked for a while |
| 43 | + False if the login seems to be wrong |
| 44 | + a string as a valid session identifier |
| 45 | + ''' |
| 46 | + if version==2: |
| 47 | + url = 'http://%s/login_sid.lua?version=2'%(IP,) |
| 48 | + else: |
| 49 | + url = 'http://%s/login_sid.lua'%(IP,) |
| 50 | + |
| 51 | + if verbose: |
| 52 | + print( "FIRST" ) |
| 53 | + r = requests.get( url ) |
| 54 | + if verbose: |
| 55 | + print(' fetched: ',r.text) |
| 56 | + root = ET.fromstring( r.text ) |
| 57 | + challenge = root.find('Challenge').text.encode('ascii') |
| 58 | + |
| 59 | + blocktime = root.find('BlockTime').text |
| 60 | + if int(blocktime)>0: |
| 61 | + print('Blocktime = %s'%blocktime) |
| 62 | + return None |
| 63 | + |
| 64 | + if verbose: |
| 65 | + print(' challenge: %r'%challenge) |
| 66 | + |
| 67 | + # note: challenge and password are bytestrings, response should be str |
| 68 | + if challenge.startswith( b'2$'): ## version 2 |
| 69 | + # this is not correct, and I don't yet understand why |
| 70 | + _, iter1, salt1, iter2, salt2 = challenge.split(b"$") |
| 71 | + iter1 = int(iter1) |
| 72 | + salt1_b = bytes.fromhex(salt1.decode('ascii')) # apparently fromhex wants str, not bytes. Maybe there's a slightly cleaner way? |
| 73 | + iter2 = int(iter2) |
| 74 | + salt2_b = bytes.fromhex(salt2.decode('ascii')) |
| 75 | + if verbose: |
| 76 | + print(' iter1=%r, salt1=%r, iter2=%r, salt2=%r'%(iter1, salt1, iter2, salt2) ) |
| 77 | + hash1 = hashlib.pbkdf2_hmac(hash_name="sha256", password=password.encode('utf8'), salt=salt1, iterations=iter1) |
| 78 | + hash2 = hashlib.pbkdf2_hmac(hash_name="sha256", password=hash1, salt=salt2, iterations=iter2) |
| 79 | + response = '%s$%s'%(salt2.decode('ascii'), hash2.hex()) |
| 80 | + |
| 81 | + else: ## version 1 |
| 82 | + m5h = hashlib.md5() |
| 83 | + hashstr = '%s-%s'%(challenge.decode('ascii'), password) |
| 84 | + hashstr = hashstr.encode('utf_16_le') # or maybe decode utf8 if our password contains any? |
| 85 | + m5h.update( hashstr ) |
| 86 | + digest = m5h.hexdigest() # a str |
| 87 | + response = '%s-%s'%(challenge.decode('ascii'), digest) |
| 88 | + |
| 89 | + data = {'response':response, 'username':username} |
| 90 | + if verbose: |
| 91 | + print(' response request data: %r'%data) |
| 92 | + r = requests.post( url, data=data ) |
| 93 | + |
| 94 | + secondresp = r.text |
| 95 | + if verbose: |
| 96 | + print( 'SECOND' ) |
| 97 | + print( ' fetched', secondresp ) |
| 98 | + m = re.search('<SID>([0-9a-f]+)</SID>', secondresp) |
| 99 | + sid = m.groups()[0] |
| 100 | + #print( " SID", sid ) |
| 101 | + if sid == '0000000000000000': |
| 102 | + return False |
| 103 | + #raise ValueError('Login failed (SID is %r)'%sid) |
| 104 | + else: |
| 105 | + return sid |
84 | 106 |
|
85 | 107 |
|
86 | | -def fritz_fetch(): |
| 108 | + |
| 109 | +def fritz_fetch(verbose=False): |
87 | 110 | " Fetches ul/dl graph data " |
88 | 111 | global _fritz_sid, _fritz_lastfetched, _fritz_lastdata |
| 112 | + if verbose: |
| 113 | + print( "FETCH data") |
89 | 114 |
|
90 | 115 | td = time.time() - _fritz_lastfetched |
91 | 116 | if td < 5.0 and _fritz_lastdata!=None: # if our last fetch was less than 5 seconds ago, we're not going to get a new answer |
92 | 117 | return _fritz_lastdata |
93 | | - |
94 | | - try: |
95 | | - fetchurl = 'http://%s/internet/inetstat_monitor.lua?sid=%s&myXhr=1&action=get_graphic&useajax=1&xhr=1'%(IP, _fritz_sid) |
96 | | - data = urlfetch(fetchurl) |
97 | | - except urllib2.HTTPError, e: |
98 | | - if e.code==403: |
99 | | - #print "Forbidden, tryin to log in for new SID" |
100 | | - _fritz_sid = fritz_login() |
| 118 | + |
| 119 | + |
| 120 | + # Previously this fetch would fail with a 403, now it only redirects you |
| 121 | + if 0: |
101 | 122 | fetchurl = 'http://%s/internet/inetstat_monitor.lua?sid=%s&myXhr=1&action=get_graphic&useajax=1&xhr=1'%(IP, _fritz_sid) |
102 | | - #print fetchurl |
103 | | - data = urlfetch(fetchurl) |
104 | | - |
105 | | - jd = json.loads( data )[0]# [0]: assume it's one main interface |
| 123 | + resp = requests.get( fetchurl, allow_redirects = False ) |
| 124 | + else: |
| 125 | + fetchurl = 'http://%s/data.lua'%IP |
| 126 | + resp = requests.post(fetchurl, data={ |
| 127 | + 'xhr':'1', |
| 128 | + 'sid':_fritz_sid, |
| 129 | + 'lang':'en', |
| 130 | + 'page':'netMoni', |
| 131 | + 'xhrId':'updateGraphs', |
| 132 | + 'useajax':'1', |
| 133 | + 'no_sidrenew':'', |
| 134 | + }, allow_redirects=False) # or it'd send a 303 to the front page that we follow |
| 135 | + |
| 136 | + |
| 137 | + if resp.status_code != 200: |
| 138 | + |
| 139 | + if verbose: |
| 140 | + print( " Status code %r, trying to log in for new SID"%resp.status_code ) |
| 141 | + _fritz_sid = fritz_login(verbose=verbose) |
| 142 | + |
| 143 | + if _fritz_sid in (None,False): |
| 144 | + raise ValueError( "Could not log in, either wrong auth or being blocked for some reason" ) |
| 145 | + else: |
| 146 | + if verbose: |
| 147 | + print("Logged in, SID = %s"%_fritz_sid) |
| 148 | + |
| 149 | + if 0: |
| 150 | + fetchurl = 'http://%s/internet/inetstat_monitor.lua?sid=%s&myXhr=1&action=get_graphic&useajax=1&xhr=1'%(IP, _fritz_sid) |
| 151 | + if verbose: |
| 152 | + print( " try 2: %r"% fetchurl ) |
| 153 | + resp = requests.get(fetchurl) |
| 154 | + else: |
| 155 | + fetchurl = 'http://%s/data.lua'%IP |
| 156 | + resp = requests.post(fetchurl, data={ |
| 157 | + 'xhr':'1', |
| 158 | + 'sid':_fritz_sid, |
| 159 | + 'lang':'en', |
| 160 | + 'page':'netMoni', |
| 161 | + 'xhrId':'updateGraphs', |
| 162 | + 'useajax':'1', |
| 163 | + 'no_sidrenew':'', |
| 164 | + }, allow_redirects=False) |
| 165 | + |
| 166 | + #print( resp.status_code ) |
| 167 | + #print( resp.text ) |
| 168 | + |
| 169 | + # implied else, or assume the last-minute login worked: assume (200 OK, JSON fetched fine) |
| 170 | + # TODO: slightly better testing |
| 171 | + |
| 172 | + data = resp.text |
| 173 | + |
| 174 | + if verbose: |
| 175 | + print( repr(data) ) |
| 176 | + |
| 177 | + |
| 178 | + jd = json.loads( data ) #[0]# [0]: assume it's one main interface |
106 | 179 | _fritz_lastfetched = time.time() |
107 | 180 | _fritz_lastdata = jd |
108 | | - #pprint.pprint( jd ) |
| 181 | + if verbose: |
| 182 | + pprint.pprint( jd ) |
109 | 183 | return jd |
110 | 184 |
|
111 | 185 |
|
112 | 186 | if __name__ == '__main__': |
113 | | - import pprint |
114 | | - pprint.pprint( fritz_fetch() ) |
| 187 | + SID = fritz_login() |
| 188 | + #print( 'SID:', SID ) |
| 189 | + |
| 190 | + while True: |
| 191 | + print( 'data', fritz_fetch() ) |
| 192 | + time.sleep( 2.5 ) |
0 commit comments