Skip to content

Commit c764f9c

Browse files
Patrick Ammannpamapa
Patrick Ammann
authored andcommitted
feat: add fritzboxfilter
1 parent 0ca83da commit c764f9c

File tree

1 file changed

+267
-0
lines changed

1 file changed

+267
-0
lines changed

tools/fritzboxfilter.py

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
#!/usr/bin/env python3
2+
3+
# python-fritzbox - Automate the Fritz!Box with python
4+
# Copyright (C) 2015-2024 Patrick Ammann <[email protected]>
5+
#
6+
# This program is free software; you can redistribute it and/or
7+
# modify it under the terms of the GNU General Public License
8+
# as published by the Free Software Foundation; either version 3
9+
# of the License, or (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program; if not, write to the Free Software
18+
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19+
#
20+
21+
import sys
22+
import re
23+
import argparse
24+
import traceback
25+
import logging
26+
import lxml.html
27+
import json
28+
29+
# pip install fritzconnection
30+
from fritzconnection import FritzConnection
31+
32+
URL_DATA = "/data.lua"
33+
34+
35+
# inspired by https://github.com/flopp/fritz-switch-profiles
36+
# tested with FRITZ!Box 5590 using FRITZ!OS:7.58
37+
class FritzboxKidProfile(object):
38+
def __init__(self, name: str, id: str) -> None:
39+
self.name = name
40+
self.id = id
41+
def __repr__(self) -> str:
42+
return "%s (id=%s)" % (self.name, self.id)
43+
44+
45+
class FritzboxDevice(object):
46+
def __init__(self, name: str) -> None:
47+
self.name = name
48+
self.network_ids = []
49+
self.filter_ids = []
50+
def __repr__(self) -> str:
51+
return "%s (network_ids=[%s],filter_ids=[%s])" % (self.name, ",".join(self.network_ids), ",".join(self.filter_ids))
52+
53+
54+
class FritzboxFilter(object):
55+
def __init__(self, fc) -> None:
56+
self._fc = fc
57+
self._logger = logging.getLogger()
58+
59+
self._fc.http_interface._set_sid_from_box()
60+
self._sid = self._fc.http_interface.sid
61+
self._url_data = "%s/%s" % (self._fc.address, URL_DATA)
62+
63+
self.devices = self._get_devices()
64+
self.profiles = self._get_profiles()
65+
66+
def _get_devices(self) -> list[FritzboxDevice]:
67+
self._logger.debug("_get_devices...")
68+
ret: list[FritzboxDevice] = []
69+
70+
# by network
71+
data = {"xhr": "1", "sid": self._sid, "lang": "de", "page": "netDev", "xhrId": "cleanup", "useajax": "1", "no_sidrenew": ""}
72+
r = self._fc.session.post(self._url_data, data=data)
73+
j = json.loads(r.text)
74+
data = j["data"]
75+
for d in data["active"] + data["passive"]:
76+
# merge by name
77+
for r in ret:
78+
if r.name == d["name"]:
79+
device = r
80+
break
81+
else:
82+
device = FritzboxDevice(d["name"])
83+
ret.append(device)
84+
device.network_ids.append(d["UID"])
85+
86+
# by filter
87+
data = {"xhr": 1, "sid": self._sid, "page": "kidLis"}
88+
r = self._fc.session.post(self._url_data, data=data)
89+
html = lxml.html.fromstring(r.text)
90+
for row in html.xpath('//table[@id="uiDevices"]/tr'):
91+
device_name = row.xpath('td[@class="name"]/span/text()')
92+
if not device_name:
93+
continue
94+
device_name = device_name[0]
95+
device_uid = row.xpath('td[@class="block"]/a/@data-uid')
96+
if not device_uid:
97+
continue
98+
device_uid = device_uid[0]
99+
# merge by name
100+
for r in ret:
101+
if r.name == device_name:
102+
device = r
103+
break
104+
else:
105+
device = FritzboxDevice(device_name)
106+
ret.append(device)
107+
device.filter_ids.append(device_uid)
108+
109+
return ret
110+
111+
def get_device_by_name(self, name):
112+
for device in self.devices:
113+
if device.name == name:
114+
return device
115+
return None
116+
117+
def get_device_details(self, device_lan_id: str):
118+
self._logger.debug("get_device_details...")
119+
data = {"xhr": 1, "sid": self._sid, "lang": "de", "page": "edit_device", "xhrId": "all", "backToPage": "netDev", "dev": device_lan_id}
120+
r = self._fc.session.post(self._url_data, data=data)
121+
j = json.loads(r.text)
122+
kisi = j["data"]["vars"]["dev"]["netAccess"]["kisi"]
123+
profiles = kisi["profiles"]
124+
profile_selected = self._get_profile_by_id(profiles["selected"])
125+
return {"profile_selected": profile_selected}
126+
127+
def _get_profiles(self) -> list[FritzboxKidProfile]:
128+
self._logger.debug("_get_profiles...")
129+
data = {"xhr": 1, "sid": self._sid, "page": "kidPro"}
130+
r = self._fc.session.post(self._url_data, data=data)
131+
html = lxml.html.fromstring(r.text)
132+
ret: list[FritzboxKidProfile] = []
133+
for row in html.xpath('//table[@id="uiProfileList"]/tr'):
134+
profile_name = row.xpath('td[@class="name"]/span/text()')
135+
if not profile_name:
136+
continue
137+
profile_name = profile_name[0]
138+
profile_id = row.xpath('td[@class="btncolumn"]/button[@name="edit"]/@value')[0]
139+
ret.append(FritzboxKidProfile(profile_name, profile_id))
140+
return ret
141+
142+
def _get_profile_by_id(self, id):
143+
for profile in self.profiles:
144+
if profile.id == id:
145+
return profile
146+
return None
147+
148+
def get_profile_by_name(self, name):
149+
for profile in self.profiles:
150+
if profile.name == name:
151+
return profile
152+
return None
153+
154+
def get_profile_details(self, profile: FritzboxKidProfile):
155+
self._logger.debug("get_profile_details...")
156+
data = {"xhr": 1, "sid": self._sid, "edit": profile.id, "back_to_page": "kidPro", "page": "kids_profileedit"}
157+
r = self._fc.session.post(self._url_data, data=data)
158+
html = lxml.html.fromstring(r.text)
159+
assigned_devices = []
160+
for row in html.xpath('//h4[@id="uiUserlistAnchor"][1]/following-sibling::div[@class="formular"]/table/tr'):
161+
device_name = row.xpath('td/text()')[0]
162+
device = self.get_device_by_name(device_name)
163+
assigned_devices.append(device)
164+
assigned_devices = list(dict.fromkeys(assigned_devices))
165+
return {"assigned_devices": assigned_devices}
166+
167+
def set_profiles(self, device_profiles: list[list[str]]):
168+
self._logger.debug("set_profile...")
169+
data = {"xhr": 1, "sid": self._sid, "apply": "", "oldpage": "/internet/kids_userlist.lua"}
170+
updates = 0
171+
for device_name, profile_name in device_profiles:
172+
device = self.get_device_by_name(device_name)
173+
if not device:
174+
self._logger.error("device '%s' not found" % device_name)
175+
continue
176+
profile = self.get_profile_by_name(profile_name)
177+
if not profile:
178+
self._logger.error("profile '%s' not found" % profile_name)
179+
continue
180+
self._logger.info("set device(s) '%s' to profile '%s'" % (device.name, profile.name))
181+
for device_id in device.filter_ids:
182+
updates += 1
183+
data["profile:%s" % device_id] = profile.id
184+
if updates != 0:
185+
self._fc.session.post(self._url_data, data=data)
186+
187+
188+
def parse_argument_kv(s: str):
189+
if not re.match("^[^=]+=[^=]+$", s):
190+
raise argparse.ArgumentTypeError("Invalid format: '%s'." % s)
191+
return s.split("=")
192+
193+
194+
#
195+
# main
196+
#
197+
if __name__ == "__main__":
198+
parser = argparse.ArgumentParser(description="Manipulate internet filter")
199+
parser.add_argument("--hostname", default="https://fritz.box",
200+
help="Hostname")
201+
parser.add_argument("--username", type=str,
202+
help="Login username. If not set the environment FRITZ_USERNAME is used.")
203+
parser.add_argument("--password", type=str,
204+
help="Login password. If not set the environment FRITZ_PASSWORD is used.")
205+
# action
206+
action = parser.add_mutually_exclusive_group(required=True)
207+
action.add_argument("--list", action="store_true",
208+
help="List all available devices and profiles")
209+
action.add_argument("--list-device", dest="list_device", type=str,
210+
help="List device(s) by name")
211+
action.add_argument("--list-profile", dest="list_profile", type=str,
212+
help="List profile by name")
213+
action.add_argument("--device-profiles", dest="device_profiles", nargs="*", metavar="DEVICE_NAME=PROFILE_NAME", type=parse_argument_kv,
214+
help="Set device to profile by name. E.g. DeviceName1=ProfileName1")
215+
# others
216+
parser.add_argument('--debug', action='store_true')
217+
args = parser.parse_args()
218+
219+
# logging
220+
h1 = logging.StreamHandler(sys.stdout)
221+
h1.setLevel(logging.DEBUG)
222+
h1.addFilter(lambda record: record.levelno <= logging.INFO)
223+
h2 = logging.StreamHandler()
224+
h2.setLevel(logging.WARNING)
225+
logging.basicConfig(level=logging.INFO, handlers=[h1, h2])
226+
logging.getLogger("urllib3.connectionpool").setLevel(logging.WARN)
227+
logging.getLogger("fritzconnection").setLevel(logging.WARN)
228+
if args.debug:
229+
logging.getLogger().setLevel(logging.DEBUG)
230+
231+
try:
232+
fc = FritzConnection(address=args.hostname, use_tls=True, user=args.username, password=args.password)
233+
filter = FritzboxFilter(fc)
234+
if args.list:
235+
print("\nAvailable devices:")
236+
for d in filter.devices:
237+
print(d)
238+
print("\nAvailable profiles:")
239+
for p in filter.profiles:
240+
print(p)
241+
elif args.list_device:
242+
d = filter.get_device_by_name(args.list_device)
243+
if d:
244+
print("\nDevice %s:" % d.name)
245+
for device_lan_id in d.network_ids:
246+
details = filter.get_device_details(device_lan_id)
247+
print(" - id=%s" % device_lan_id)
248+
print(" - profile_selected=%s" % details["profile_selected"])
249+
else:
250+
print("Device %s not found" % args.list_device)
251+
elif args.list_profile:
252+
p = filter.get_profile_by_name(args.list_profile)
253+
if p:
254+
details = filter.get_profile_details(p)
255+
print("\nProfile %s:" % p.name)
256+
print(" - id=%s" % p.id)
257+
print(" - assigned_devices")
258+
for d in details["assigned_devices"]:
259+
print(" - %s" % d)
260+
else:
261+
print("Profile %s not found" % args.list_profile)
262+
elif args.device_profiles:
263+
filter.set_profiles(args.device_profiles)
264+
except Exception as ex:
265+
logging.error(ex)
266+
logging.debug(traceback.format_exc())
267+
sys.exit(-2)

0 commit comments

Comments
 (0)