Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5198eba

Browse files
committed
maybe fixed now?
1 parent fa831af commit 5198eba

1 file changed

Lines changed: 166 additions & 88 deletions

File tree

helpers_fritz.py

Lines changed: 166 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,114 +1,192 @@
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
54

65
"""
76
Being lazy with globals because you probably don't have more than one in your LAN
87
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
1210
1311
CONSIDER: fetching things beyond transfers.
14-
"""
1512
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
1916
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
7022
23+
"""
7124

25+
_fritz_sid = None
26+
_fritz_lastfetched = 0
27+
_fritz_lastdata = None
7228

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
84106

85107

86-
def fritz_fetch():
108+
109+
def fritz_fetch(verbose=False):
87110
" Fetches ul/dl graph data "
88111
global _fritz_sid, _fritz_lastfetched, _fritz_lastdata
112+
if verbose:
113+
print( "FETCH data")
89114

90115
td = time.time() - _fritz_lastfetched
91116
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
92117
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:
101122
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
106179
_fritz_lastfetched = time.time()
107180
_fritz_lastdata = jd
108-
#pprint.pprint( jd )
181+
if verbose:
182+
pprint.pprint( jd )
109183
return jd
110184

111185

112186
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

Comments
 (0)