def get_page_form( self, form_name: str, data: dict, controller_id: str ) -> typing.Tuple[fapi.ForisAjaxForm, typing.Callable[[dict], typing.Tuple[ "str", "str"]]]: """Returns appropriate foris form and handler to generate response """ form: fapi.ForisAjaxForm if form_name == "sub-form": form = handlers.SubordinatesEditForm(data) def prepare_message(results: dict) -> dict: if results["result"]: message = { "classes": ["success"], "text": _("Device '%(controller_id)s' was successfully updated." ) % dict(controller_id=data["controller_id"]), } else: message = { "classes": ["error"], "text": _("Failed to update subordinate '%(controller_id)s'.") % dict(controller_id=data["controller_id"]), } return message form.url = reverse("config_ajax_form", page_name="subordinates-setup", form_name="sub-form") return form, prepare_message elif form_name == "subsub-form": form = handlers.SubsubordinatesEditForm(data) def prepare_message(results: dict) -> dict: if results["result"]: message = { "classes": ["success"], "text": _("Subsubordinate '%(controller_id)s' was successfully updated." ) % dict(controller_id=data["controller_id"]), } else: message = { "classes": ["error"], "text": _("Failed to update subsubordinate '%(controller_id)s'." ) % dict(controller_id=data["controller_id"]), } return message form.url = reverse("config_ajax_form", page_name="subordinates-setup", form_name="subsub-form") return form, prepare_message raise bottle.HTTPError(404, "No form '%s' not found." % form_name)
def _action_check_registration(self): handler = RegistrationCheckHandler(bottle.request.POST.decode()) if not handler.save(): messages.warning(_("There were some errors in your input.")) return self.render(registration_check_form=handler.form) email = handler.data["email"] result = handler.form.callback_results kwargs = {} if not result["success"]: messages.error( _("An error ocurred when checking the registration: " "<br><pre>%(error)s</pre>" % dict(error=result["error"]))) return self.render() else: if result["status"] == "owned": messages.success( _("Registration for the entered email is valid. " "Now you can enable the data collection.")) collection_toggle_handler = CollectionToggleHandler( bottle.request.POST.decode()) kwargs[ 'collection_toggle_form'] = collection_toggle_handler.form elif result["status"] == "foreign": messages.warning( _('This router is currently assigned to a different email address. Please ' 'continue to the <a href="%(url)s">Turris website</a> and use the ' 'registration code <strong>%(reg_num)s</strong> for a re-assignment to your ' 'email address.') % dict(url=result["url"], reg_num=result["registration_number"])) bottle.redirect( reverse("config_page", page_name="data_collect") + "?" + urlencode({"email": email})) elif result["status"] == "free": messages.info( _('This email address is not registered yet. Please continue to the ' '<a href="%(url)s">Turris website</a> and use the registration code ' '<strong>%(reg_num)s</strong> to create a new account.') % dict(url=result["url"], reg_num=result["registration_number"])) bottle.redirect( reverse("config_page", page_name="data_collect") + "?" + urlencode({"email": email})) elif result["status"] == "not_found": messages.error( _('Router failed to authorize. Please try to validate our email later.' )) bottle.redirect( reverse("config_page", page_name="data_collect") + "?" + urlencode({"email": email})) return self.render(status=result["status"], registration_url=result["url"], reg_num=result["registration_number"], **kwargs)
def _action_save_notifications(self): if bottle.request.method != "POST": messages.error(_("Wrong HTTP method.")) bottle.redirect(reverse("config_page", page_name="maintenance")) handler = notifications.NotificationsHandler(bottle.request.POST.decode()) if handler.save(): messages.success(_("Configuration was successfully saved.")) bottle.redirect(reverse("config_page", page_name="maintenance")) messages.warning(_("There were some errors in your input.")) return super(MaintenanceConfigPage, self).render(notifications_form=handler.form)
def _action_save_notifications(self): if bottle.request.method != 'POST': messages.error(_("Wrong HTTP method.")) bottle.redirect(reverse("config_page", page_name="maintenance")) handler = notifications.NotificationsHandler(request.POST.decode()) if handler.save(): messages.success(_("Configuration was successfully saved.")) bottle.redirect(reverse("config_page", page_name="maintenance")) messages.warning(_("There were some errors in your input.")) return super(MaintenanceConfigPage, self).render(notifications_form=handler.form)
def _action_toggle_collecting(self): if bottle.request.method != 'POST': messages.error(_("Wrong HTTP method.")) bottle.redirect(reverse("config_page", page_name="data_collect")) handler = CollectionToggleHandler(bottle.request.POST.decode()) if handler.save(): messages.success(_("Configuration was successfully saved.")) bottle.redirect(reverse("config_page", page_name="data_collect")) messages.warning(_("There were some errors in your input.")) return super().render(collection_toggle_form=handler.form)
def _action_reset(self): if bottle.request.method != "POST": messages.error(_("Wrong HTTP method.")) bottle.redirect(reverse("config_page", page_name="wifi")) data = current_state.backend.perform("wifi", "reset") if "result" in data and data["result"] is True: messages.success(_("Wi-Fi reset was successful.")) else: messages.error(_("Failed to perform Wi-Fi reset.")) bottle.redirect(reverse("config_page", page_name="wifi"))
def ping(): res = bottle.response.copy(cls=bottle.HTTPResponse) res.content_type = 'application/json' next = bottle.request.GET.get('next', None) login_url = "%s://%s" % (bottle.request.urlparts.scheme, bottle.request.urlparts.netloc) login_url = "%s%s?next=%s" % (login_url, reverse("login"), next) if next else \ "%s%s" % (login_url, reverse("login")) res.body = json.dumps(dict(msg="pong", loginUrl=login_url)) res.status = 200 res.set_header('Access-Control-Allow-Origin', '*') res.set_header('Access-Control-Allow-Methods', 'GET, OPTIONS') res.set_header('Access-Control-Allow-Headers', 'Origin, Accept, Content-Type, X-Requested-With') raise res
def config_action_post(page_name, action): bottle.SimpleTemplate.defaults["active_config_page_key"] = page_name bottle.Jinja2Template.defaults["active_config_page_key"] = page_name ConfigPage = get_config_page(page_name) config_page = ConfigPage(request.POST.decode()) if request.is_xhr: if request.POST.pop("_update", None): # if update was requested, just render the page - otherwise handle actions as usual return config_page.render(is_xhr=True) # check if the button click wasn't any sub-action subaction = request.POST.pop("action", None) if subaction: return config_action_post(page_name, subaction) try: result = config_page.call_action(action) try: if not result: bottle.redirect(reverse("config_page", page_name=page_name)) except TypeError: # raised by Validator - could happen when the form is posted with wrong fields messages.error(_("Configuration could not be saved due to an internal error.")) logger.exception("Error when saving form.") logger.warning("Form not saved.") return result except ValueError: raise bottle.HTTPError(404, "Unknown action.")
def config_action_post(page_name, action): bottle.SimpleTemplate.defaults["active_config_page_key"] = page_name bottle.Jinja2Template.defaults["active_config_page_key"] = page_name ConfigPage = get_config_page(page_name) config_page = ConfigPage(request.POST.decode()) if request.is_xhr: if request.POST.pop("_update", None): # if update was requested, just render the page - otherwise handle actions as usual return config_page.render(is_xhr=True) # check if the button click wasn't any sub-action subaction = request.POST.pop("action", None) if subaction: return config_action_post(page_name, subaction) try: result = config_page.call_action(action) try: if not result: bottle.redirect(reverse("config_page", page_name=page_name)) except TypeError: # raised by Validator - could happen when the form is posted with wrong fields messages.error( _("Configuration could not be saved due to an internal error.") ) logger.exception("Error when saving form.") logger.warning("Form not saved.") return result except ValueError: raise bottle.HTTPError(404, "Unknown action.")
def _ajax_prepare_token(self): self._check_post() RemoteConfigPage.token_cleanup() form = self.get_token_id_form(bottle.request.POST.decode()) token_id = form.data.get("token_id") if not token_id: raise bottle.HTTPError(404, "id not found") name = form.data.get("name", token_id) res = current_state.backend.perform("remote", "get_token", {"id": form.data["token_id"]}) if res["status"] != "valid": raise bottle.HTTPError(404, "token not found") bottle.response.set_header("Content-Type", "application/json") new_uuid = uuid.uuid4() RemoteConfigPage.token_links[str(new_uuid)] = { "expiration": time.time() + RemoteConfigPage.TOKEN_LINK_EXPIRATION, "name": name, "token": base64.b64decode(res["token"]), } return { "url": reverse("config_insecure", page_name="remote", identifier=str(new_uuid)), "expires_in": RemoteConfigPage.TOKEN_LINK_EXPIRATION, }
def logout(): session = bottle.request.environ["foris.session"] if "user_authenticated" in session: session.load_anonymous() bottle.redirect(reverse("index"))
def get_page_form( self, form_name: str, data: dict, controller_id: str ) -> typing.Tuple[fapi.ForisAjaxForm, typing.Callable[[dict], typing.Tuple[ "str", "str"]]]: """Returns appropriate foris form and handler to generate response """ if form_name == "wifi-form": form = handlers.SubordinatesWifiEditForm( data, controller_id=controller_id, enable_guest=False) def prepare_message(results: dict) -> dict: if results["result"]: message = { "classes": ["success"], "text": _("Wi-Fi settings were successfully updated."), } else: message = { "classes": ["error"], "text": _("Failed to update Wi-Fi settings.") } return message form.url = reverse("config_ajax_form", page_name="subordinates-wifi", form_name="wifi-form") return form, prepare_message raise bottle.HTTPError(404, "No form '%s' not found." % form_name)
def _action_delete_ca(self): self._check_post() data = current_state.backend.perform("remote", "delete_ca") if data["result"]: messages.success(_("CA for remote access was sucessfully deleted.")) else: messages.error(_("Failed to delete CA for remote access.")) bottle.redirect(reverse("config_page", page_name="remote"))
def _redirect_to_default_location(): next_page = "notifications" # by default redirect to current guide step if current_state.guide.enabled: next_page = current_state.guide.current if current_state.guide.current else next_page bottle.redirect(reverse("config_page", page_name=next_page))
def ping(): res = bottle.response.copy(cls=bottle.HTTPResponse) res.content_type = "application/json" next = bottle.request.GET.get("next", None) login_url = "%s://%s" % (bottle.request.urlparts.scheme, bottle.request.urlparts.netloc) login_url = ( "%s%s?next=%s" % (login_url, reverse("login"), next) if next else "%s%s" % (login_url, reverse("login")) ) res.body = json.dumps(dict(msg="pong", loginUrl=login_url)) res.status = 200 res.set_header("Access-Control-Allow-Origin", "*") res.set_header("Access-Control-Allow-Methods", "GET, OPTIONS") res.set_header("Access-Control-Allow-Headers", "Origin, Accept, Content-Type, X-Requested-With") raise res
def _check_and_get_controller_id(self): if bottle.request.method != "POST": messages.error(_("Wrong HTTP method.")) bottle.redirect(reverse("config_page", page_name="remote")) form = self.get_controller_id_form(bottle.request.POST.decode()) if not form.data["controller_id"]: raise bottle.HTTPError(404, "controller_id not found") return form.data["controller_id"]
def _action_test_notifications(self): if bottle.request.method != 'POST': messages.error(_("Wrong HTTP method.")) bottle.redirect(reverse("config_page", page_name="maintenance")) data = current_state.backend.perform( "router_notifications", "create", { "msg": "_(This is a testing notification. Please ignore me.)", "severity": "news", "immediate": True, } ) if data["result"]: messages.success(_("Testing message was sent, please check your inbox.")) else: messages.error(_( "Sending of the testing message failed, your configuration is possibly wrong." )) bottle.redirect(reverse("config_page", page_name="maintenance"))
def reboot(): data = current_state.backend.perform("maintain", "reboot") if bottle.request.is_xhr: # return a list of ip addresses where to connect after reboot is performed res = bottle.response.copy(cls=bottle.HTTPResponse) res.content_type = 'application/json' res.body = json.dumps(data) res.status = 200 raise res else: bottle.redirect(reverse("/"))
def _action_test_notifications(self): if bottle.request.method != "POST": messages.error(_("Wrong HTTP method.")) bottle.redirect(reverse("config_page", page_name="maintenance")) data = current_state.backend.perform( "router_notifications", "create", { "msg": "_(This is a testing notification. Please ignore me.)", "severity": "news", "immediate": True, }, ) if data["result"]: messages.success(_("Testing message was sent, please check your inbox.")) else: messages.error( _("Sending of the testing message failed, your configuration is possibly wrong.") ) bottle.redirect(reverse("config_page", page_name="maintenance"))
def reboot(): data = current_state.backend.perform("maintain", "reboot") if bottle.request.is_xhr: # return a list of ip addresses where to connect after reboot is performed res = bottle.response.copy(cls=bottle.HTTPResponse) res.content_type = "application/json" res.body = json.dumps(data) res.status = 200 raise res else: bottle.redirect(reverse("/"))
def _action_generic(self, action): if bottle.request.method != "POST": messages.error(_("Wrong HTTP method.")) bottle.redirect(reverse("config_page", page_name="remote")) form = self.get_serial_form(bottle.request.POST.decode()) if not form.data["serial"]: raise bottle.HTTPError(404, "serial not found") res = current_state.backend.perform("netboot", action, {"serial": form.data["serial"]}) bottle.response.set_header("Content-Type", "application/json") return res
def login(next, session): if check_password(bottle.request.POST.get("password")): # re-generate session to prevent session fixation session.recreate() session["user_authenticated"] = True update_csrf_token(save_session=False) session.save() if next and is_safe_redirect(next, bottle.request.get_header("host")): bottle.redirect(next) else: bottle.redirect(reverse("index"))
def login(next, session): if check_password(bottle.request.POST.get("password")): # re-generate session to prevent session fixation session.recreate() session["user_authenticated"] = True update_csrf_token(save_session=False) session.save() if next and is_safe_redirect(next, bottle.request.get_header('host')): bottle.redirect(next) else: bottle.redirect(reverse("index"))
def change_lang(lang): """Change language of the interface. :param lang: language to set :raises: bottle.HTTPError if requested language is not installed """ if lang in translations: if set_current_language(lang): bottle.request.app.lang = lang backlink = bottle.request.GET.get("backlink") if backlink and is_safe_redirect(backlink, bottle.request.get_header("host")): bottle.redirect(backlink) bottle.redirect(reverse("index")) else: raise bottle.HTTPError(404, "Language '%s' is not available." % lang)
def config_page_get(page_name): # redirect in case that guide is not passed if current_state.guide.enabled and page_name not in current_state.guide.available_tabs: bottle.redirect(reverse("config_page", page_name=current_state.guide.current)) bottle.SimpleTemplate.defaults['active_config_page_key'] = page_name bottle.Jinja2Template.defaults['active_config_page_key'] = page_name ConfigPage = get_config_page(page_name) # test if page is enabled otherwise redirect to default if not ConfigPage.is_enabled() or not ConfigPage.is_visible(): _redirect_to_default_location() config_page = ConfigPage() return config_page.render(active_config_page_key=page_name)
def config_page_get(page_name): # redirect in case that guide is not passed if current_state.guide.enabled and page_name not in current_state.guide.available_tabs: bottle.redirect(reverse("config_page", page_name=current_state.guide.current)) bottle.SimpleTemplate.defaults["active_config_page_key"] = page_name bottle.Jinja2Template.defaults["active_config_page_key"] = page_name ConfigPage = get_config_page(page_name) # test if page is enabled otherwise redirect to default if not ConfigPage.is_enabled() or not ConfigPage.is_visible(): _redirect_to_default_location() config_page = ConfigPage() return config_page.render(active_config_page_key=page_name)
def change_lang(lang): """Change language of the interface. :param lang: language to set :raises: bottle.HTTPError if requested language is not installed """ if lang in translations: if set_current_language(lang): bottle.request.app.lang = lang backlink = bottle.request.GET.get('backlink') if backlink and is_safe_redirect(backlink, bottle.request.get_header('host')): bottle.redirect(backlink) bottle.redirect(reverse("index")) else: raise bottle.HTTPError(404, "Language '%s' is not available." % lang)
def _check_post(self): if bottle.request.method != "POST": messages.error(_("Wrong HTTP method.")) bottle.redirect(reverse("config_page", page_name="remote"))
def _action_generate_ca(self): self._check_post() messages.info(_("Starting to generate CA for remote access.")) current_state.backend.perform("remote", "generate_ca") # don't need to handle async_id (should influence all clients) bottle.redirect(reverse("config_page", page_name="remote"))
def reset_guide(): current_state.backend.perform("web", "reset_guide") bottle.redirect(reverse("/"))
def leave_guide(): current_state.backend.perform("web", "update_guide", {"enabled": False}) bottle.redirect(reverse("/"))
def login_redirect(): next_url = bottle.request.GET.get("next") if next_url and is_safe_redirect(next_url, bottle.request.get_header("host")): bottle.redirect(next_url) bottle.redirect(reverse("config_index"))
def _prepare_device_fields(self, section, device, form_data, last=False): HINTS = { 'password': _("WPA2 pre-shared key, that is required to connect to the " "network. Minimum length is 8 characters.") } def prefixed(name): return WifiHandler.prefixed(device["id"], name) # get corresponding band bands = [ e for e in device["available_bands"] if e["hwmode"] == form_data["hwmode"] ] if not bands: # wrong hwmode selected pick the first one from available band = device["available_bands"][0] form_data["hwmode"] = device["available_bands"][0]["hwmode"] else: band = bands[0] wifi_main = section.add_section( name=prefixed("set_wifi"), title=None, ) wifi_main.add_field( Checkbox, name=prefixed("device_enabled"), label=_("Enable Wi-Fi %s") % (device["id"] + 1), default=True, ) wifi_main.add_field(Textbox, name=prefixed("ssid"), label=_("SSID"), required=True, validators=validators.ByteLenRange( 1, 32)).requires(prefixed("device_enabled"), True) wifi_main.add_field( Checkbox, name=prefixed("ssid_hidden"), label=_("Hide SSID"), default=False, hint= _("If set, network is not visible when scanning for available networks." )).requires(prefixed("device_enabled"), True) wifi_main.add_field( Radio, name=prefixed("hwmode"), label=_("Wi-Fi mode"), args=[ e for e in (("11g", "2.4 GHz (g)"), ("11a", "5 GHz (a)")) if e[0] in [b["hwmode"] for b in device["available_bands"]] ], hint=_( "The 2.4 GHz band is more widely supported by clients, but " "tends to have more interference. The 5 GHz band is a newer" " standard and may not be supported by all your devices. It " "usually has less interference, but the signal does not " "carry so well indoors.")).requires(prefixed("device_enabled"), True) htmodes = ( ("NOHT", _("Disabled")), ("HT20", _("802.11n - 20 MHz wide channel")), ("HT40", _("802.11n - 40 MHz wide channel")), ("VHT20", _("802.11ac - 20 MHz wide channel")), ("VHT40", _("802.11ac - 40 MHz wide channel")), ("VHT80", _("802.11ac - 80 MHz wide channel")), ) wifi_main.add_field( Dropdown, name=prefixed("htmode"), label=_("802.11n/ac mode"), args=[e for e in htmodes if e[0] in band["available_htmodes"]], hint= _("Change this to adjust 802.11n/ac mode of operation. 802.11n with 40 MHz wide " "channels can yield higher throughput but can cause more interference in the " "network. If you don't know what to choose, use the default option with 20 MHz " "wide channel." )).requires(prefixed("device_enabled"), True).requires( prefixed("hwmode"), lambda val: val in ("11g", "11a") ) # this req is added to rerender htmodes when hwmode changes channels = [("0", _("auto"))] + [ (str(e["number"]), ("%d (%d MHz%s)" % (e["number"], e["frequency"], ", DFS" if e["radar"] else ""))) for e in band["available_channels"] ] wifi_main.add_field( Dropdown, name=prefixed("channel"), label=_("Network channel"), default="0", args=channels, ).requires(prefixed("device_enabled"), True).requires( prefixed("hwmode"), lambda val: val in ("11g", "11a") ) # this req is added to rerender channel list when hwmode changes wifi_main.add_field(PasswordWithHide, name=prefixed("password"), label=_("Network password"), required=True, validators=validators.ByteLenRange(8, 63), hint=HINTS['password']).requires( prefixed("device_enabled"), True) if current_state.app == "config": # Guest wi-fi part guest_section = wifi_main.add_section( name=prefixed("set_guest_wifi"), title=_("Guest Wi-Fi"), description=_("Set guest Wi-Fi here.")) guest_section.add_field( Checkbox, name=prefixed("guest_enabled"), label=_("Enable guest Wi-Fi"), default=False, hint= _("Enables Wi-Fi for guests, which is separated from LAN network. Devices " "connected to this network are allowed to access the internet, but aren't " "allowed to access other devices and the configuration interface of the " "router. Parameters of the guest network can be set in <a href='%(url)s'>the " "Guest network tab</a>. ") % dict(url=reverse("config_page", page_name="guest"))).requires( prefixed("device_enabled"), True) guest_section.add_field( Textbox, name=prefixed("guest_ssid"), label=_("SSID for guests"), required=True, validators=validators.ByteLenRange(1, 32), ).requires(prefixed("guest_enabled"), True) guest_section.add_field( PasswordWithHide, name=prefixed("guest_password"), label=_("Password for guests"), required=True, default="", validators=validators.ByteLenRange(8, 63), hint=HINTS['password'], ).requires(prefixed("guest_enabled"), True) # Horizontal line separating wi-fi cards if not last: wifi_main.add_field(HorizontalLine, name=prefixed("wifi-separator"), class_="wifi-separator").requires( prefixed("device_enabled"), True)
def get_form(self): lan_form = fapi.ForisForm( "lan", self.data, filter=create_config_filter("dhcp", "network", "firewall", "sqm")) lan_main = lan_form.add_section( name="set_lan", title=_(self.userfriendly_title), description=_("This section contains settings for the local network (LAN). The provided" " defaults are suitable for most networks. <br><strong>Note:</strong> If " "you change the router IP address, all computers in LAN, probably " "including the one you are using now, will need to obtain a <strong>new " "IP address</strong> which does <strong>not</strong> happen <strong>" "immediately</strong>. It is recommended to disconnect and reconnect all " "LAN cables after submitting your changes to force the update. The next " "page will not load until you obtain a new IP from DHCP (if DHCP enabled)" " and you might need to <strong>refresh the page</strong> in your " "browser.") ) lan_main.add_field(Textbox, name="lan_ipaddr", label=_("Router IP address"), nuci_path="uci.network.lan.ipaddr", validators=validators.IPv4(), hint=_("Router's IP address in inner network. Also defines the range of " "assigned IP addresses.")) lan_main.add_field(Checkbox, name="dhcp_enabled", label=_("Enable DHCP"), nuci_path="uci.dhcp.lan.ignore", nuci_preproc=lambda val: not bool(int(val.value)), default=True, hint=_("Enable this option to automatically assign IP addresses to " "the devices connected to the router.")) lan_main.add_field(Textbox, name="dhcp_min", label=_("DHCP start"), nuci_path="uci.dhcp.lan.start")\ .requires("dhcp_enabled", True) lan_main.add_field(Textbox, name="dhcp_max", label=_("DHCP max leases"), nuci_path="uci.dhcp.lan.limit")\ .requires("dhcp_enabled", True) guest_network_section = lan_form.add_section( name="guest_network", title=_("Guest network"), ) guest_network_section.add_field( Checkbox, name="guest_network_enabled", label=_("Enable guest network"), default=False, hint=_( "Guest network is used for <a href='%(url)s'>guest Wi-Fi</a>. It is separated " "from your ordinary LAN network. Devices connected to this network are allowed " "to access the internet, but are not allowed to access other devices and " "the configuration interface of the router." ) % dict(url=reverse("config_page", page_name="wifi")), nuci_preproc=guest_network_enabled, ) guest_network_section.add_field( Textbox, name="guest_network_subnet", label=_("Guest network"), nuci_preproc=generate_network_preprocessor( "uci.network.guest_turris.ipaddr", "uci.network.guest_turris.netmask", DEFAULT_GUEST_NETWORK, DEFAULT_GUEST_MASK, ), validators=[validators.IPv4Prefix()], hint=_( "You need to set the IP range for your guest network. It is necessary that " "the range is different than ranges on your other networks (LAN, WAN, VPN, etc.)." ), ).requires("guest_network_enabled", True) guest_network_section.add_field( Checkbox, name="guest_network_shapping", label=_("Guest Lan QoS"), nuci_preproc=parse_uci_bool, nuci_path="uci.sqm.guest_limit_turris.enabled", hint=_( "This option enables you to set a bandwidth limit for the guest network, " "so that your main network doesn't get slowed-down by it." ), ).requires("guest_network_enabled", True) guest_network_section.add_field( Number, name="guest_network_download", label=_("Download (kb/s)"), validators=[validators.PositiveInteger()], hint=_( "Download speed in guest network (in kilobits per second)." ), default=1024, nuci_path="uci.sqm.guest_limit_turris.upload", ).requires("guest_network_shapping", True) guest_network_section.add_field( Number, name="guest_network_upload", label=_("Upload (kb/s)"), validators=[validators.PositiveInteger()], hint=_( "Upload speed in guest network (in kilobits per second)." ), default=1024, nuci_path="uci.sqm.guest_limit_turris.download", ).requires("guest_network_shapping", True) def lan_form_cb(data): uci = Uci() config = Config("dhcp") uci.add(config) dhcp = Section("lan", "dhcp") config.add(dhcp) # FIXME: this would overwrite any unrelated DHCP options the user might have set. # Maybe we should get the current values, scan them and remove selectively the ones # with 6 in front of them? Or have some support for higher level of stuff in nuci. options = List("dhcp_option") options.add(Value(0, "6," + data['lan_ipaddr'])) dhcp.add_replace(options) network = Config("network") uci.add(network) interface = Section("lan", "interface") network.add(interface) interface.add(Option("ipaddr", data['lan_ipaddr'])) if data['dhcp_enabled']: dhcp.add(Option("ignore", "0")) dhcp.add(Option("start", data['dhcp_min'])) dhcp.add(Option("limit", data['dhcp_max'])) else: dhcp.add(Option("ignore", "1")) # qos data qos = {'enabled': False} if 'guest_network_shapping' in data and data['guest_network_shapping']: qos['enabled'] = True qos['download'] = data['guest_network_download'] qos['upload'] = data['guest_network_upload'] # update guest network configs guest_enabled = data.get("guest_network_enabled") guest_network_subnet = data.get("guest_network_subnet") if guest_network_subnet: network, prefix = data.get("guest_network_subnet").split("/") else: network, prefix = DEFAULT_GUEST_NETWORK, DEFAULT_GUEST_PREFIX # disable guest wifi when guest network is not enabled data = client.get(filter=wifi_filter()) card_count = 0 while data.find_child("uci.wireless.@wifi-device[%d]" % card_count): card_count += 1 if not guest_enabled and card_count > 0: wireless = uci.add(Config("wireless")) for i in range(card_count): guest_iface = wireless.add(Section("guest_iface_%d" % i, "wifi-iface")) guest_iface.add(Option("disabled", "1")) guest_interfaces = ["guest_turris_%d" % e for e in range(card_count)] LanHandler.prepare_guest_configs( uci, guest_enabled, network, prefix, guest_interfaces, qos) return "edit_config", uci lan_form.add_callback(lan_form_cb) return lan_form
def get_form(self): data = {} data["guest_enabled"] = self.backend_data["enabled"] data["guest_ipaddr"] = self.backend_data["ip"] data["guest_netmask"] = self.backend_data["netmask"] data["guest_dhcp_enabled"] = self.backend_data["dhcp"]["enabled"] data["guest_dhcp_start"] = self.backend_data["dhcp"]["start"] data["guest_dhcp_limit"] = self.backend_data["dhcp"]["limit"] data["guest_dhcp_leasetime"] = self.backend_data["dhcp"]["lease_time"] // 60 // 60 data["guest_qos_enabled"] = self.backend_data["qos"]["enabled"] data["guest_qos_download"] = self.backend_data["qos"]["download"] data["guest_qos_upload"] = self.backend_data["qos"]["upload"] if self.data: # Update from post data.update(self.data) guest_form = fapi.ForisForm( "guest", data, validators=[ validators.DhcpRangeValidator( "guest_netmask", "guest_dhcp_start", "guest_dhcp_limit", gettext( "<strong>DHCP start</strong> and <strong>DHCP max leases</strong> " "does not fit into <strong>Guest network netmask</strong>!" ), [ lambda data: not data["guest_enabled"], lambda data: not data["guest_dhcp_enabled"], ], ) ], ) guest_network_section = guest_form.add_section( name="guest_network", title=_(self.userfriendly_title), description=_( "Guest network is used for <a href='%(url)s'>guest Wi-Fi</a>. It is separated " "from your ordinary LAN. Devices connected to this network are allowed " "to access the internet, but are not allowed to access the configuration " "interface of the this device nor the devices in LAN." ) % dict(url=reverse("config_page", page_name="wifi")), ) guest_network_section.add_field( Checkbox, name="guest_enabled", label=_("Enable guest network"), default=False ) guest_network_section.add_field( Textbox, name="guest_ipaddr", label=_("Router IP in guest network"), default=DEFAULT_GUEST_IP, validators=validators.IPv4(), hint=_( "Router's IP address in the guest network. It is necessary that " "the guest network IPs are different from other networks " "(LAN, WAN, VPN, etc.)." ), ).requires("guest_enabled", True) guest_network_section.add_field( Textbox, name="guest_netmask", label=_("Guest network netmask"), default=DEFAULT_GUEST_MASK, validators=validators.IPv4Netmask(), hint=_("Network mask of the guest network."), ).requires("guest_enabled", True) guest_network_section.add_field( Checkbox, name="guest_dhcp_enabled", label=_("Enable DHCP"), preproc=lambda val: bool(int(val)), default=True, hint=_( "Enable this option to automatically assign IP addresses to " "the devices connected to the router." ), ).requires("guest_enabled", True) guest_network_section.add_field( Textbox, name="guest_dhcp_start", label=_("DHCP start") ).requires("guest_dhcp_enabled", True) guest_network_section.add_field( Textbox, name="guest_dhcp_limit", label=_("DHCP max leases") ).requires("guest_dhcp_enabled", True) guest_network_section.add_field( Textbox, name="guest_dhcp_leasetime", label=_("Lease time (hours)"), validators=[validators.InRange(1, 7 * 24)], ).requires("guest_dhcp_enabled", True) guest_network_section.add_field( Checkbox, name="guest_qos_enabled", label=_("Guest Lan QoS"), hint=_( "This option enables you to set a bandwidth limit for the guest network, " "so that your main network doesn't get slowed-down by it." ), ).requires("guest_enabled", True) guest_network_section.add_field( Number, name="guest_qos_download", label=_("Download (kb/s)"), validators=[validators.PositiveInteger()], hint=_("Download speed in guest network (in kilobits per second)."), default=1024, ).requires("guest_qos_enabled", True) guest_network_section.add_field( Number, name="guest_qos_upload", label=_("Upload (kb/s)"), validators=[validators.PositiveInteger()], hint=_("Upload speed in guest network (in kilobits per second)."), default=1024, ).requires("guest_qos_enabled", True) def guest_form_cb(data): if data["guest_enabled"]: msg = { "enabled": data["guest_enabled"], "ip": data["guest_ipaddr"], "netmask": data["guest_netmask"], "dhcp": {"enabled": data["guest_dhcp_enabled"]}, "qos": {"enabled": data["guest_qos_enabled"]}, } if data["guest_dhcp_enabled"]: msg["dhcp"]["start"] = int(data["guest_dhcp_start"]) msg["dhcp"]["limit"] = int(data["guest_dhcp_limit"]) msg["dhcp"]["lease_time"] = int(data["guest_dhcp_leasetime"]) * 60 * 60 if data["guest_qos_enabled"]: msg["qos"]["download"] = int(data["guest_qos_download"]) msg["qos"]["upload"] = int(data["guest_qos_upload"]) else: msg = {"enabled": False} res = current_state.backend.perform("guest", "update_settings", msg) return "save_result", res # store {"result": ...} to be used later... guest_form.add_callback(guest_form_cb) return guest_form
def _prepare_device_fields(self, section, device, form_data, last=False): HINTS = { "password": _( "WPA2 pre-shared key, that is required to connect to the " "network. Minimum length is 8 characters." ) } def prefixed(name): return WifiEditForm.prefixed(device["id"], name) # get corresponding band bands = [e for e in device["available_bands"] if e["hwmode"] == form_data["hwmode"]] if not bands: # wrong hwmode selected pick the first one from available band = device["available_bands"][0] form_data["hwmode"] = device["available_bands"][0]["hwmode"] else: band = bands[0] wifi_main = section.add_section(name=prefixed("set_wifi"), title=None) wifi_main.add_field( Checkbox, name=prefixed("device_enabled"), label=_("Enable Wi-Fi %s") % (device["id"] + 1), default=True, ) wifi_main.add_field( Textbox, name=prefixed("ssid"), label=_("SSID"), required=True, validators=validators.ByteLenRange(1, 32), ).requires(prefixed("device_enabled"), True) wifi_main.add_field( Checkbox, name=prefixed("ssid_hidden"), label=_("Hide SSID"), default=False, hint=_("If set, network is not visible when scanning for available networks."), ).requires(prefixed("device_enabled"), True) wifi_main.add_field( Radio, name=prefixed("hwmode"), label=_("Wi-Fi mode"), args=[ e for e in (("11g", "2.4 GHz (g)"), ("11a", "5 GHz (a)")) if e[0] in [b["hwmode"] for b in device["available_bands"]] ], hint=_( "The 2.4 GHz band is more widely supported by clients, but " "tends to have more interference. The 5 GHz band is a newer" " standard and may not be supported by all your devices. It " "usually has less interference, but the signal does not " "carry so well indoors." ), ).requires(prefixed("device_enabled"), True) htmodes = ( ("NOHT", _("Disabled")), ("HT20", _("802.11n - 20 MHz wide channel")), ("HT40", _("802.11n - 40 MHz wide channel")), ("VHT20", _("802.11ac - 20 MHz wide channel")), ("VHT40", _("802.11ac - 40 MHz wide channel")), ("VHT80", _("802.11ac - 80 MHz wide channel")), ) wifi_main.add_field( Dropdown, name=prefixed("htmode"), label=_("802.11n/ac mode"), args=[e for e in htmodes if e[0] in band["available_htmodes"]], hint=_( "Change this to adjust 802.11n/ac mode of operation. 802.11n with 40 MHz wide " "channels can yield higher throughput but can cause more interference in the " "network. If you don't know what to choose, use the default option with 20 MHz " "wide channel." ), ).requires(prefixed("device_enabled"), True).requires( prefixed("hwmode"), lambda val: val in ("11g", "11a") ) # this req is added to rerender htmodes when hwmode changes channels = [("0", _("auto"))] + [ ( str(e["number"]), ("%d (%d MHz%s)" % (e["number"], e["frequency"], ", DFS" if e["radar"] else "")), ) for e in band["available_channels"] ] wifi_main.add_field( Dropdown, name=prefixed("channel"), label=_("Network channel"), default="0", args=channels, ).requires(prefixed("device_enabled"), True).requires( prefixed("hwmode"), lambda val: val in ("11g", "11a") ) # this req is added to rerender channel list when hwmode changes wifi_main.add_field( PasswordWithHide, name=prefixed("password"), label=_("Network password"), required=True, validators=validators.ByteLenRange(8, 63), hint=HINTS["password"], ).requires(prefixed("device_enabled"), True) if current_state.app == "config" and self.enable_guest: # Guest wi-fi part guest_section = wifi_main.add_section( name=prefixed("set_guest_wifi"), title=_("Guest Wi-Fi"), description=_("Set guest Wi-Fi here."), ) guest_section.add_field( Checkbox, name=prefixed("guest_enabled"), label=_("Enable guest Wi-Fi"), default=False, hint=_( "Enables Wi-Fi for guests, which is separated from LAN network. Devices " "connected to this network are allowed to access the internet, but aren't " "allowed to access other devices and the configuration interface of the " "router. Parameters of the guest network can be set in <a href='%(url)s'>the " "Guest network tab</a>." ) % dict(url=reverse("config_page", page_name="guest")), ).requires(prefixed("device_enabled"), True) guest_section.add_field( Textbox, name=prefixed("guest_ssid"), label=_("SSID for guests"), required=True, validators=validators.ByteLenRange(1, 32), ).requires(prefixed("guest_enabled"), True) guest_section.add_field( PasswordWithHide, name=prefixed("guest_password"), label=_("Password for guests"), required=True, default="", validators=validators.ByteLenRange(8, 63), hint=HINTS["password"], ).requires(prefixed("guest_enabled"), True) # Horizontal line separating wi-fi cards if not last: wifi_main.add_field( HorizontalLine, name=prefixed("wifi-separator"), class_="wifi-separator" ).requires(prefixed("device_enabled"), True)
def get_form(self): data = {} data["guest_enabled"] = self.backend_data["enabled"] data["guest_ipaddr"] = self.backend_data["ip"] data["guest_netmask"] = self.backend_data["netmask"] data["guest_dhcp_enabled"] = self.backend_data["dhcp"]["enabled"] data["guest_dhcp_start"] = self.backend_data["dhcp"]["start"] data["guest_dhcp_limit"] = self.backend_data["dhcp"]["limit"] data["guest_dhcp_leasetime"] = self.backend_data["dhcp"]["lease_time"] // 60 // 60 data["guest_qos_enabled"] = self.backend_data["qos"]["enabled"] data["guest_qos_download"] = self.backend_data["qos"]["download"] data["guest_qos_upload"] = self.backend_data["qos"]["upload"] if self.data: # Update from post data.update(self.data) guest_form = fapi.ForisForm( "guest", data, validators=[ validators.DhcpRangeValidator( "guest_netmask", "guest_dhcp_start", "guest_dhcp_limit", gettext( "<strong>DHCP start</strong> and <strong>DHCP max leases</strong> " "does not fit into <strong>Guest network netmask</strong>!" ), [ lambda data: not data.get("guest_enabled"), lambda data: not data.get("guest_dhcp_enabled"), ], ), validators.DhcpRangeRouterIpValidator( "guest_ipaddr", "guest_netmask", "guest_dhcp_start", "guest_dhcp_limit", gettext( "<strong>Router IP</strong> should not be within DHCP range " "defined by <strong>DHCP start</strong> and <strong>DHCP max leases " "</strong>" ), [ lambda data: not data.get("guest_dhcp_enabled"), lambda data: not data.get("guest_enabled"), ], ), ], ) guest_network_section = guest_form.add_section( name="guest_network", title=_(self.userfriendly_title), description=_( "Guest network is used for <a href='%(url)s'>guest Wi-Fi</a>. It is separated " "from your ordinary LAN. Devices connected to this network are allowed " "to access the internet, but are not allowed to access the configuration " "interface of this device nor the devices in LAN." ) % dict(url=reverse("config_page", page_name="wifi")), ) guest_network_section.add_field( Checkbox, name="guest_enabled", label=_("Enable guest network"), default=False ) guest_network_section.add_field( Textbox, name="guest_ipaddr", label=_("Router IP in guest network"), default=DEFAULT_GUEST_IP, validators=validators.IPv4(), hint=_( "Router's IP address in the guest network. It is necessary that " "the guest network IPs are different from other networks " "(LAN, WAN, VPN, etc.)." ), ).requires("guest_enabled", True) guest_network_section.add_field( Textbox, name="guest_netmask", label=_("Guest network netmask"), default=DEFAULT_GUEST_MASK, validators=validators.IPv4Netmask(), hint=_("Network mask of the guest network."), ).requires("guest_enabled", True) guest_network_section.add_field( Checkbox, name="guest_dhcp_enabled", label=_("Enable DHCP"), preproc=lambda val: bool(int(val)), default=True, hint=_( "Enable this option to automatically assign IP addresses to " "the devices connected to the router." ), ).requires("guest_enabled", True) guest_network_section.add_field( Textbox, name="guest_dhcp_start", label=_("DHCP start") ).requires("guest_dhcp_enabled", True) guest_network_section.add_field( Textbox, name="guest_dhcp_limit", label=_("DHCP max leases") ).requires("guest_dhcp_enabled", True) guest_network_section.add_field( Textbox, name="guest_dhcp_leasetime", label=_("Lease time (hours)"), validators=[validators.InRange(1, 7 * 24)], ).requires("guest_dhcp_enabled", True) guest_network_section.add_field( Checkbox, name="guest_qos_enabled", label=_("Guest Lan QoS"), hint=_( "This option enables you to set a bandwidth limit for the guest network, " "so that your main network doesn't get slowed-down by it." ), ).requires("guest_enabled", True) guest_network_section.add_field( Number, name="guest_qos_download", label=_("Download (kb/s)"), validators=[validators.PositiveInteger()], hint=_("Download speed in guest network (in kilobits per second)."), default=1024, ).requires("guest_qos_enabled", True) guest_network_section.add_field( Number, name="guest_qos_upload", label=_("Upload (kb/s)"), validators=[validators.PositiveInteger()], hint=_("Upload speed in guest network (in kilobits per second)."), default=1024, ).requires("guest_qos_enabled", True) def guest_form_cb(data): if data["guest_enabled"]: msg = { "enabled": data["guest_enabled"], "ip": data["guest_ipaddr"], "netmask": data["guest_netmask"], "dhcp": {"enabled": data["guest_dhcp_enabled"]}, "qos": {"enabled": data["guest_qos_enabled"]}, } if data["guest_dhcp_enabled"]: msg["dhcp"]["start"] = int(data["guest_dhcp_start"]) msg["dhcp"]["limit"] = int(data["guest_dhcp_limit"]) msg["dhcp"]["lease_time"] = int(data["guest_dhcp_leasetime"]) * 60 * 60 if data["guest_qos_enabled"]: msg["qos"]["download"] = int(data["guest_qos_download"]) msg["qos"]["upload"] = int(data["guest_qos_upload"]) else: msg = {"enabled": False} res = current_state.backend.perform("guest", "update_settings", msg) return "save_result", res # store {"result": ...} to be used later... guest_form.add_callback(guest_form_cb) return guest_form
def leave_guide(): current_state.backend.perform("web", "update_guide", { "enabled": False, }) bottle.redirect(reverse("/"))
def _add_wifi_section(self, wifi_section, wifi_card, radio_to_iface, post_data, nuci_data, last=False): HINTS = { 'password': _("WPA2 pre-shared key, that is required to connect to the " "network. Minimum length is 8 characters.") } radio_index = int(wifi_card['name'][3:]) iface_index = radio_to_iface.get('radio%s' % radio_index) if iface_index is None: # Interface is not present in the wireless config - skip this radio return None def prefixed_name(name): return "radio%s-%s" % (radio_index, name) wifi_main = wifi_section.add_section( name=prefixed_name("set_wifi"), title=None, ) wifi_main.add_field(Hidden, name=prefixed_name("iface_section"), nuci_path="uci.wireless.@wifi-iface[%s]" % iface_index, nuci_preproc=lambda val: val.name) # Use numbering starting from one. In rare cases, it may happen that the first radio # is not radio0, or that there's a gap between radio numbers, but it should not happen # on most of the setups. wifi_main.add_field(Checkbox, name=prefixed_name("wifi_enabled"), label=_("Enable Wi-Fi %s") % (radio_index + 1), default=True, nuci_path="uci.wireless.@wifi-iface[%s].disabled" % iface_index, nuci_preproc=lambda val: not bool(int(val.value))) wifi_main.add_field( Textbox, name=prefixed_name("ssid"), label=_("SSID"), nuci_path="uci.wireless.@wifi-iface[%s].ssid" % iface_index, required=True, validators=validators.ByteLenRange(1, 32)).requires( prefixed_name("wifi_enabled"), True) wifi_main.add_field( Checkbox, name=prefixed_name("ssid_hidden"), label=_("Hide SSID"), default=False, nuci_path="uci.wireless.@wifi-iface[%s].hidden" % iface_index, hint= _("If set, network is not visible when scanning for available networks." )).requires(prefixed_name("wifi_enabled"), True) channels_2g4, channels_5g = self._get_channels(wifi_card) is_dual_band = False # hwmode choice for dual band devices if len(channels_2g4) > 1 and len(channels_5g) > 1: is_dual_band = True wifi_main.add_field( Radio, name=prefixed_name("hwmode"), label=_("Wi-Fi mode"), default="11g", args=(("11g", "2.4 GHz (g)"), ("11a", "5 GHz (a)")), nuci_path="uci.wireless.radio%s.hwmode" % radio_index, nuci_preproc=lambda x: x.value.replace("n", "" ), # old configs used # 11ng/11na hint= _("The 2.4 GHz band is more widely supported by clients, but " "tends to have more interference. The 5 GHz band is a newer" " standard and may not be supported by all your devices. It " "usually has less interference, but the signal does not " "carry so well indoors.")).requires( prefixed_name("wifi_enabled"), True) htmodes = (("NOHT", _("Disabled")), ("HT20", _("802.11n - 20 MHz wide channel")), ("HT40", _("802.11n - 40 MHz wide channel"))) # Allow VHT modes only if the card has the capabilities and 5 GHz band is selected hwmode = self._get_value(post_data, nuci_data, "radio%s-hwmode" % radio_index, "uci.wireless.radio%s.hwmode" % radio_index) allow_vht = wifi_card['vht-capabilities'] and hwmode == "11a" if allow_vht: htmodes += ( ("VHT20", _("802.11ac - 20 MHz wide channel")), ("VHT40", _("802.11ac - 40 MHz wide channel")), ("VHT80", _("802.11ac - 80 MHz wide channel")), ) wifi_main.add_field( Dropdown, name=prefixed_name("htmode"), label=_("802.11n/ac mode"), args=htmodes, nuci_path="uci.wireless.radio%s.htmode" % radio_index, hint= _("Change this to adjust 802.11n/ac mode of operation. 802.11n with 40 MHz wide " "channels can yield higher throughput but can cause more interference in the " "network. If you don't know what to choose, use the default option with 20 MHz " "wide channel.")).requires(prefixed_name("wifi_enabled"), True) # 2.4 GHz channels if len(channels_2g4) > 1: field_2g4 = wifi_main.add_field( Dropdown, name=prefixed_name("channel2g4"), label=_("Network channel"), default=channels_2g4[0][0], args=channels_2g4, nuci_path="uci.wireless.radio%s.channel" % radio_index).requires(prefixed_name("wifi_enabled"), True) if is_dual_band: field_2g4.requires(prefixed_name("hwmode"), "11g") # 5 GHz channels if len(channels_5g) > 1: field_5g = wifi_main.add_field( Dropdown, name=prefixed_name("channel5g"), label=_("Network channel"), default=channels_5g[0][0], args=channels_5g, nuci_path="uci.wireless.radio%s.channel" % radio_index).requires(prefixed_name("wifi_enabled"), True) if is_dual_band: field_5g.requires(prefixed_name("hwmode"), "11a") wifi_main.add_field( Password, name=prefixed_name("key"), label=_("Network password"), nuci_path="uci.wireless.@wifi-iface[%s].key" % iface_index, required=True, validators=validators.ByteLenRange(8, 63), hint=HINTS['password']).requires(prefixed_name("wifi_enabled"), True) # Guest wi-fi part guest_section = wifi_main.add_section( name=prefixed_name("set_guest_wifi"), title=_("Guest Wi-Fi"), description=_("Set guest Wi-Fi here.")) guest_section.add_field( Checkbox, name=prefixed_name("guest_enabled"), label=_("Enable guest Wi-Fi"), default=False, nuci_path="uci.wireless.guest_iface_%s.disabled" % iface_index, nuci_preproc=lambda value: not parse_uci_bool(value), hint= _("Enables Wi-Fi for guests, which is separated from LAN network. Devices " "connected to this network are allowed to access the internet, but aren't " "allowed to access other devices and the configuration interface of the router. " "Parameters of the guest network can be set in <a href='%(url)s'>the LAN tab</a>. " ) % dict(url=reverse("config_page", page_name="lan"))).requires( prefixed_name("wifi_enabled"), True) default_ssid = self._get_value( post_data, nuci_data, prefixed_name("ssid"), "uci.wireless.@wifi-iface[%s].ssid" % iface_index, "Turris", ) default_guest_ssid = self._get_value( post_data, nuci_data, prefixed_name("guest_ssid"), "uci.wireless.guest_iface_%s.ssid" % iface_index, "%s-guest" % default_ssid) guest_section.add_field(Textbox, name=prefixed_name("guest_ssid"), label=_("SSID for guests"), nuci_path="uci.wireless.guest_iface_%s.ssid" % iface_index, required=True, validators=validators.ByteLenRange(1, 32), default=default_guest_ssid).requires( prefixed_name("guest_enabled"), True) guest_section.add_field( Password, name=prefixed_name("guest_key"), label=_("Password for guests"), nuci_path="uci.wireless.guest_iface_%s.key" % iface_index, required=True, default="", validators=validators.ByteLenRange(8, 63), hint=HINTS['password'], ).requires(prefixed_name("guest_enabled"), True) # Horizontal line separating wi-fi cards if not last: wifi_main.add_field(HorizontalLine, name=prefixed_name("wifi-separator"), class_="wifi-separator").requires( prefixed_name("wifi_enabled"), True)