class NetworksHandler(BaseConfigHandler): """ Networks settings handler """ userfriendly_title = gettext("Network interfaces") def load_backend_data(self): data = current_state.backend.perform("networks", "get_settings") self.backend_data = data def __init__(self, *args, **kwargs): self.load_backend_data() super(NetworksHandler, self).__init__(*args, **kwargs) def get_form(self): data = copy.deepcopy(self.backend_data) if self.data: # Update from post data.update(self.data) networks_form = fapi.ForisForm("networks", self.data) ports_section = networks_form.add_section( name="set_ports", title=_(self.userfriendly_title)) checkboxes = [] for kind in ["wan", "lan", "guest", "none"]: checkboxes += [(e["id"], e["id"]) for e in self.backend_data["networks"][kind]] ports_section.add_field(MultiCheckbox, name="wan", args=checkboxes, multifield=True) ports_section.add_field(MultiCheckbox, name="lan", args=checkboxes, multifield=True) ports_section.add_field(MultiCheckbox, name="guest", args=checkboxes, multifield=True) ports_section.add_field(MultiCheckbox, name="none", args=checkboxes, multifield=True) ports_section.add_field(Checkbox, name="ssh_on_wan", default=False, required=False) ports_section.add_field(Checkbox, name="http_on_wan", default=False, required=False) ports_section.add_field(Checkbox, name="https_on_wan", default=False, required=False) def networks_form_cb(data): wan = data.get("wan", []) lan = data.get("lan", []) guest = data.get("guest", []) none = data.get("none", []) ssh_on_wan = bool(data.get("ssh_on_wan", "0")) http_on_wan = bool(data.get("http_on_wan", "0")) https_on_wan = bool(data.get("https_on_wan", "0")) result = current_state.backend.perform( "networks", "update_settings", { "firewall": { "ssh_on_wan": ssh_on_wan, "http_on_wan": http_on_wan, "https_on_wan": https_on_wan }, "networks": {"lan": lan, "wan": wan, "guest": guest, "none": none} } ) return "save_result", result networks_form.add_callback(networks_form_cb) return networks_form
class SamplePluginConfigHandler(BaseConfigHandler): # gettext() triggers lazy_translated text # it is also used for detecting translations during foris_make_messages cmd userfriendly_title = gettext("Sample") def get_form(self): data = current_state.backend.perform("sample", "get_slices") if self.data: # Update from post (used when the form is updated via ajax) data.update(self.data) form = fapi.ForisForm("sample", data) section = form.add_section( name="main_section", title=self.userfriendly_title, ) # _() translates the string immediatelly # it is also used for detecting translations during foris_make_messages cmd section.add_field(Number, name="slices", label=_("Number of slices"), required=True, validators=validators.InRange(2, 15)) def form_cb(data): res = current_state.backend.perform( "sample", "set_slices", {"slices": int(data["slices"])}) return "save_result", res # store {"result": ...} to be used in SamplePluginPage save() method form.add_callback(form_cb) return form
class StoragePluginPage(ConfigPageMixin, StoragePluginConfigHandler): slug = "storage" menu_order = 60 template = "storage/storage" template_type = "jinja2" userfriendly_title = gettext("Storage") def render(self, **kwargs): kwargs['settings'] = current_state.backend.perform("storage", "get_settings") kwargs['settings']['old_device_name'] = \ kwargs['settings']["old_device"].replace("/dev/", "") available_modules = current_state.backend.perform("introspect", "list_modules") if "nextcloud" in available_modules["modules"]: nextcloud_data = current_state.backend.perform("nextcloud", "get_status") kwargs["settings"]["nextcloud_installed"] = nextcloud_data["nextcloud_installed"] kwargs["settings"]["nextcloud_configured"] = nextcloud_data["nextcloud_configured"] kwargs["settings"]["nextcloud_configuring"] = nextcloud_data["nextcloud_configuring"] drives = current_state.backend.perform("storage", "get_drives")["drives"] kwargs['drives'] = sorted(drives, key=lambda d: d['dev']) return super(StoragePluginPage, self).render(**kwargs) def call_ajax_action(self, action): if action == 'get_settings': if bottle.request.method != 'GET': raise bottle.HTTPError(404, "Wrong http method (only GET is allowed.") data = current_state.backend.perform("storage", "get_settings") return data raise ValueError("Unknown AJAX action.")
class CollectionToggleHandler(BaseConfigHandler): userfriendly_title = gettext("Data collection") def get_form(self): data = current_state.backend.perform("data_collect", "get") if self.data and "enable" in self.data: data["enable"] = self.data["enable"] else: data["enable"] = data["agreed"] form = fapi.ForisForm("enable_collection", data) section = form.add_section( name="collection_toggle", title=_(self.userfriendly_title), ) section.add_field(Checkbox, name="enable", label=_("Enable data collection"), preproc=lambda val: bool(int(val))) def form_cb(data): data = current_state.backend.perform("data_collect", "set", {"agreed": data["enable"]}) return "save_result", data # store {"result": ...} to be used later... form.add_callback(form_cb) return form
class WifiHandler(BaseConfigHandler): userfriendly_title = gettext("Wi-Fi") def get_form(self): ajax_form = WifiEditForm(self.data) return ajax_form.foris_form
class SsbackupsPluginConfigHandler(BaseConfigHandler): userfriendly_title = gettext("Cloud Backups") def get_form(self): form = fapi.ForisForm("create_and_upload", self.data) section = form.add_section( name="passwords", title=self.userfriendly_title, ) section.add_field( Password, name="password", label=_("Password"), required=True, validators=validators.LenRange(6, 128) ) section.add_field( Password, name="password_validation", label=_("Password (repeat)"), required=True, validators=validators.EqualTo( "password", "password_validation", _("Passwords are not equal.") ) ) def form_cb(data): res = current_state.backend.perform( "ssbackups", "set_password", {"password": base64.b64encode(data["password"].encode()).decode()} ) return "save_result", res # store {"result": ...} to be used later... form.add_callback(form_cb) return form
class ProfileHandler(BaseConfigHandler): """ Profile settings handler """ userfriendly_title = gettext("Guide workflow") def __init__(self, *args, **kwargs): self.load_backend_data() super(ProfileHandler, self).__init__(*args, **kwargs) def load_backend_data(self): self.backend_data = current_state.backend.perform("web", "get_guide") def get_form(self): data = {"workflow": self.backend_data["current_workflow"]} if self.data: data.update(self.data) profile_form = fapi.ForisForm("profile", data) main = profile_form.add_section(name="set_profile", title=_(self.userfriendly_title)) main.add_field(Hidden, name="workflow", value=self.backend_data["current_workflow"]) def profile_form_cb(data): result = current_state.backend.perform( "web", "update_guide", { "enabled": True, "workflow": data["workflow"], } ) return "save_result", result profile_form.add_callback(profile_form_cb) return profile_form
class StoragePluginConfigHandler(BaseConfigHandler): userfriendly_title = gettext("storage") def get_form(self): form = fapi.ForisForm("storage", []) form.add_section(name="set_srv", title=self.userfriendly_title) return form
class NotificationsConfigPage(ConfigPageMixin): slug = "notifications" menu_order = 9 template = "config/notifications" userfriendly_title = gettext("Notifications") template_type = "jinja2" def render(self, **kwargs): notifications = current_state.backend.perform( "router_notifications", "list", {"lang": current_state.language})["notifications"] # show only non displayed notifications kwargs["notifications"] = [ e for e in notifications if not e["displayed"] ] return super(NotificationsConfigPage, self).render(**kwargs) def _action_dismiss_notifications(self): notification_ids = bottle.request.POST.getall("notification_ids[]") response = current_state.backend.perform("router_notifications", "mark_as_displayed", {"ids": notification_ids}) return response["result"], notification_ids def call_ajax_action(self, action): if action == "dismiss-notifications": bottle.response.set_header("Content-Type", "application/json") res = self._action_dismiss_notifications() if res[0]: return {"success": True, "displayedIDs": res[1]} else: return {"success": False} elif action == "list": notifications = current_state.backend.perform( "router_notifications", "list", {"lang": current_state.language})["notifications"] return bottle.template( "_notifications.html.j2", notifications=[e for e in notifications if not e["displayed"]], template_adapter=bottle.Jinja2Template, ) raise ValueError("Unknown AJAX action.") @classmethod def get_menu_tag(cls): return { "show": True if current_state.notification_count else False, "hint": _("Number of notifications"), "text": "%d" % current_state.notification_count, }
class AboutConfigPage(ConfigPageMixin): slug = "about" menu_order = 99 template = "config/about" template_type = "jinja2" userfriendly_title = gettext("About") def render(self, **kwargs): data = current_state.backend.perform("about", "get") # process dates etc return self.default_template(data=data, **kwargs)
class NetmetrPluginConfigHandler(BaseConfigHandler): userfriendly_title = gettext("netmetr") def get_form(self): data = current_state.backend.perform("netmetr", "get_settings") # init hours for i in range(24): data["hour_to_run_%d" % i] = False # update the enabled for e in data["hours_to_run"]: data["hour_to_run_%d" % e] = True if self.data: # Update from post data.update(self.data) form = fapi.ForisForm("netmetr", data) main = form.add_section(name="set_netmetr", title=_(self.userfriendly_title)) autostart_section = main.add_section(name="autostart", title=_("Autostart")) autostart_section.add_field( Checkbox, name="autostart_enabled", label=_("Autostart enabled"), preproc=lambda val: bool(int(val)), hint= _("Measuring will start about selected hour (time is no exact for load distribution)" )) hours = main.add_section(name="hours", title=_("Autostart times")) for i in range(24): hours.add_field( Checkbox, name="hour_to_run_%d" % i, label="%02d:00" % i, preproc=lambda val: bool(int(val)), ).requires("autostart_enabled", True) def form_cb(data): msg = {"autostart_enabled": data["autostart_enabled"]} if data["autostart_enabled"]: msg["hours_to_run"] = [] for i in range(24): name = "hour_to_run_%d" % i if name in data and data[name]: msg["hours_to_run"].append(i) current_state.backend.perform("netmetr", "update_settings", msg) messages.success(_("Netmetr settings were updated.")) return "none", None form.add_callback(form_cb) return form
class GuideFinishedHandler(BaseConfigHandler): userfriendly_title = gettext("Guide Finished") def get_form(self): finished_form = fapi.ForisForm("guide_finished", {}) finished_form.add_section(name="guide_finished", title=_(self.userfriendly_title)) def guide_finished_cb(data): res = current_state.backend.perform("web", "update_guide", {"enabled": False}) return "save_result", res # store {"result": ...} to be used later... finished_form.add_callback(guide_finished_cb) return finished_form
class NetbootConfigHandler(BaseConfigHandler): STATE_ACCEPTED = gettext("accepted") STATE_INCOMMING = gettext("incomming") userfriendly_title = gettext("Netboot") def get_form(self): form = fapi.ForisForm("netboot", {}) form.add_section(name="main_section", title=self.userfriendly_title) def form_cb(data): return "save_result", {} form.add_callback(form_cb) return form def get_serial_form(self, data=None): generate_serial_form = fapi.ForisForm("serial_form", data) serial_section = generate_serial_form.add_section("serial_section", title=None) serial_section.add_field( Textbox, name="serial", label=" ", required=True, validators=[validators.MacAddress()] ) return generate_serial_form
class MaintenanceHandler(BaseConfigHandler): userfriendly_title = gettext("Maintenance") def get_form(self): maintenance_form = fapi.ForisForm("maintenance", self.data) maintenance_main = maintenance_form.add_section( name="restore_backup", title=_(self.userfriendly_title) ) maintenance_main.add_field(File, name="backup_file", label=_("Backup file"), required=True) def maintenance_form_cb(data): data = current_state.backend.perform( "maintain", "restore_backup", {"backup": base64.b64encode(data["backup_file"].file.read()).decode("utf-8")}, ) return "save_result", {"result": data["result"]} maintenance_form.add_callback(maintenance_form_cb) return maintenance_form
class GuideFinishedHandler(BaseConfigHandler): userfriendly_title = gettext("Guide Finished") def get_form(self): finished_form = fapi.ForisForm("guide_finished", {}) finished_form.add_section( name="guide_finished", title=_(self.userfriendly_title), description=_( "Congratulations you've successfully reached the end of this guide. " "Once you leave this guide you'll be granted access to the " "full configuration interface of this device.")) def guide_finished_cb(data): res = current_state.backend.perform("web", "update_guide", { "enabled": False, }) return "save_result", res # store {"result": ...} to be used later... finished_form.add_callback(guide_finished_cb) return finished_form
class RegistrationCheckHandler(BaseConfigHandler): """ Handler for checking of the registration status and assignment to a queried email address. """ userfriendly_title = gettext("Data collection") def get_form(self): form = fapi.ForisForm("registration_check", self.data) main_section = form.add_section(name="check_email", title=_(self.userfriendly_title)) main_section.add_field(Email, name="email", label=_("Email")) def form_cb(data): data = current_state.backend.perform( "data_collect", "get_registered", { "email": data.get("email"), "language": current_state.language }) error = None registration_number = None url = None if data["status"] == "unknown": error = _("Failed to query the server.") elif data["status"] == "not_valid": error = _("Failed to verify the router's registration.") elif data["status"] in ["free", "foreign"]: url = data["url"] registration_number = data["registration_number"] return "save_result", { 'success': data["status"] not in ["unknown", "not_valid"], 'status': data["status"], 'error': error, 'url': url, 'registration_number': registration_number, } form.add_callback(form_cb) return form
class SubordinatesJoinedPage(JoinedPages): userfriendly_title = gettext("Managed devices") slug = "subordinates" no_url = True subpages: typing.Iterable[typing.Type["ConfigPageMixin"]] = [ SubordinatesSetupPage, SubordinatesWifiPage, SubordinatesNetbootPage, ] @classmethod def is_visible(cls): if current_state.backend.name != "mqtt": return False return ConfigPageMixin.is_visible_static(cls) @classmethod def is_enabled(cls): if current_state.backend.name != "mqtt": return False return ConfigPageMixin.is_enabled_static(cls)
class TLSConfigHandler(BaseConfigHandler): userfriendly_title = gettext("Access tokens") def get_form(self): tls_form = ForisForm("tls", self.data, filter=ca_filter) maintenance_main = tls_form.add_section(name="tls_client", title=_( self.userfriendly_title)) maintenance_main.add_field( Textbox, name="client_name", label=_("Client name"), required=True, hint= _("The display name for the client. It must be shorter than 64 characters " "and must contain only alphanumeric characters, dots, dashes and " "underscores."), validators=[ RegExp(_("Client name is invalid."), client_name_regexp), LenRange(1, 63) ]) def maintenance_form_cb(data): client_name = data['client_name'] if new_client(client_name): messages.success( _("Request for creating a new client \"%s\" was succesfully submitted. " "Client token should be available for download in a minute." ) % client_name) else: messages.error( _("An error occurred when creating client \"%s\".") % client_name) return "none", None tls_form.add_callback(maintenance_form_cb) return tls_form
class WanHandler(BaseConfigHandler): userfriendly_title = gettext("WAN") def __init__(self, *args, **kwargs): # Do not display "none" options for WAN protocol if hide_no_wan is True self.hide_no_wan = kwargs.pop("hide_no_wan", False) self.status_data = current_state.backend.perform( "wan", "get_wan_status") self.backend_data = current_state.backend.perform( "wan", "get_settings") super(WanHandler, self).__init__(*args, **kwargs) @staticmethod def _convert_backend_data_to_form_data(data): res = {} # WAN # Convert none (initial setup) to dhcp (default) res["proto"] = ("dhcp" if data["wan_settings"]["wan_type"] == "none" else data["wan_settings"]["wan_type"]) if res["proto"] == "dhcp": res["hostname"] = data["wan_settings"].get("wan_dhcp", {}).get("hostname", "") elif res["proto"] == "static": res["ipaddr"] = data["wan_settings"]["wan_static"]["ip"] res["netmask"] = data["wan_settings"]["wan_static"]["netmask"] res["gateway"] = data["wan_settings"]["wan_static"]["gateway"] res["ipv4_dns1"] = data["wan_settings"]["wan_static"].get( "dns1", "") res["ipv4_dns2"] = data["wan_settings"]["wan_static"].get( "dns2", "") elif res["proto"] == "pppoe": res["username"] = data["wan_settings"]["wan_pppoe"]["username"] res["password"] = data["wan_settings"]["wan_pppoe"]["password"] # WAN6 res["wan6_proto"] = data["wan6_settings"]["wan6_type"] if res["wan6_proto"] == "static": res["ip6addr"] = data["wan6_settings"]["wan6_static"]["ip"] res["ip6prefix"] = data["wan6_settings"]["wan6_static"]["network"] res["ip6gw"] = data["wan6_settings"]["wan6_static"]["gateway"] res["ipv6_dns1"] = data["wan6_settings"]["wan6_static"].get( "dns1", "") res["ipv6_dns2"] = data["wan6_settings"]["wan6_static"].get( "dns2", "") elif res["wan6_proto"] == "dhcpv6": res["ip6duid"] = data["wan6_settings"]["wan6_dhcpv6"]["duid"] elif res["wan6_proto"] == "6to4": res["6to4_ipaddr"] = data["wan6_settings"]["wan6_6to4"][ "ipv4_address"] elif res["wan6_proto"] == "6in4": res["6in4_mtu"] = data["wan6_settings"]["wan6_6in4"]["mtu"] res["6in4_server_ipv4"] = data["wan6_settings"]["wan6_6in4"][ "server_ipv4"] res["6in4_ipv6_prefix"] = data["wan6_settings"]["wan6_6in4"][ "ipv6_prefix"] res["6in4_dynamic_enabled"] = data["wan6_settings"]["wan6_6in4"][ "dynamic_ipv4"]["enabled"] if res["6in4_dynamic_enabled"]: res["6in4_tunnel_id"] = data["wan6_settings"]["wan6_6in4"][ "dynamic_ipv4"]["tunnel_id"] res["6in4_username"] = data["wan6_settings"]["wan6_6in4"][ "dynamic_ipv4"]["username"] res["6in4_key"] = data["wan6_settings"]["wan6_6in4"][ "dynamic_ipv4"]["password_or_key"] # MAC res["custom_mac"] = data["mac_settings"]["custom_mac_enabled"] res["macaddr"] = data["mac_settings"].get("custom_mac", "") return res @staticmethod def _convert_form_data_to_backend_data(data): res = {"wan_settings": {}, "wan6_settings": {}, "mac_settings": {}} # WAN res["wan_settings"]["wan_type"] = data["proto"] if data["proto"] == "dhcp": hostname = data.get("hostname", False) res["wan_settings"]["wan_dhcp"] = { "hostname": hostname } if hostname else {} elif data["proto"] == "static": res["wan_settings"]["wan_static"] = {} res["wan_settings"]["wan_static"]["ip"] = data["ipaddr"] res["wan_settings"]["wan_static"]["netmask"] = data["netmask"] res["wan_settings"]["wan_static"]["gateway"] = data["gateway"] dns1 = data.get("ipv4_dns1", None) dns2 = data.get("ipv4_dns2", None) res["wan_settings"]["wan_static"].update( {k: v for k, v in { "dns1": dns1, "dns2": dns2 }.items() if v}) elif data["proto"] == "pppoe": res["wan_settings"]["wan_pppoe"] = {} res["wan_settings"]["wan_pppoe"]["username"] = data["username"] res["wan_settings"]["wan_pppoe"]["password"] = data["password"] # WAN6 res["wan6_settings"]["wan6_type"] = data["wan6_proto"] if data["wan6_proto"] == "static": res["wan6_settings"]["wan6_static"] = {} res["wan6_settings"]["wan6_static"]["ip"] = data["ip6addr"] res["wan6_settings"]["wan6_static"]["network"] = data["ip6prefix"] res["wan6_settings"]["wan6_static"]["gateway"] = data["ip6gw"] dns1 = data.get("ipv6_dns1", None) dns2 = data.get("ipv6_dns2", None) res["wan6_settings"]["wan6_static"].update( {k: v for k, v in { "dns1": dns1, "dns2": dns2 }.items() if v}) if data["wan6_proto"] == "dhcpv6": res["wan6_settings"]["wan6_dhcpv6"] = { "duid": data.get("ip6duid", "") } if data["wan6_proto"] == "6to4": res["wan6_settings"]["wan6_6to4"] = { "ipv4_address": data.get("6to4_ipaddr", "") } if data["wan6_proto"] == "6in4": dynamic = {"enabled": data.get("6in4_dynamic_enabled", False)} if dynamic["enabled"]: dynamic["tunnel_id"] = data.get("6in4_tunnel_id") dynamic["username"] = data.get("6in4_username") dynamic["password_or_key"] = data.get("6in4_key") res["wan6_settings"]["wan6_6in4"] = { "mtu": int(data.get("6in4_mtu")), "ipv6_prefix": data.get("6in4_ipv6_prefix"), "server_ipv4": data.get("6in4_server_ipv4"), "dynamic_ipv4": dynamic, } # MAC res["mac_settings"] = ({ "custom_mac_enabled": True, "custom_mac": data["macaddr"] } if "custom_mac" in data and data["custom_mac"] else { "custom_mac_enabled": False }) return res def get_form(self): data = WanHandler._convert_backend_data_to_form_data(self.backend_data) if self.data: # Update from post data.update(self.data) # WAN wan_form = fapi.ForisForm("wan", data) wan_main = wan_form.add_section( name="set_wan", title=_(self.userfriendly_title), description= _("Here you specify your WAN port settings. Usually, you can leave these " "options untouched unless instructed otherwise by your internet service " "provider. Also, in case there is a cable or DSL modem connecting your " "router to the network, it is usually not necessary to change this " "setting."), ) WAN_DHCP = "dhcp" WAN_STATIC = "static" WAN_PPPOE = "pppoe" WAN_OPTIONS = ( (WAN_DHCP, _("DHCP (automatic configuration)")), (WAN_STATIC, _("Static IP address (manual configuration)")), (WAN_PPPOE, _("PPPoE (for DSL bridges, Modem Turris, etc.)")), ) WAN6_NONE = "none" WAN6_DHCP = "dhcpv6" WAN6_STATIC = "static" WAN6_6TO4 = "6to4" WAN6_6IN4 = "6in4" WAN6_OPTIONS = ( (WAN6_DHCP, _("DHCPv6 (automatic configuration)")), (WAN6_STATIC, _("Static IP address (manual configuration)")), (WAN6_6TO4, _("6to4 (public IPv4 address required)")), (WAN6_6IN4, _("6in4 (public IPv4 address required)")), ) if not self.hide_no_wan: WAN6_OPTIONS = ((WAN6_NONE, _("Disable IPv6")), ) + WAN6_OPTIONS # protocol wan_main.add_field(Dropdown, name="proto", label=_("IPv4 protocol"), args=WAN_OPTIONS, default=WAN_DHCP) # static ipv4 wan_main.add_field( Textbox, name="ipaddr", label=_("IP address"), required=True, validators=validators.IPv4(), ).requires("proto", WAN_STATIC) wan_main.add_field( Textbox, name="netmask", label=_("Network mask"), required=True, validators=validators.IPv4Netmask(), ).requires("proto", WAN_STATIC) wan_main.add_field(Textbox, name="gateway", label=_("Gateway"), required=True, validators=validators.IPv4()).requires( "proto", WAN_STATIC) wan_main.add_field( Textbox, name="hostname", label=_("DHCP hostname"), validators=validators.Domain(), hint=_("Hostname which will be provided to DHCP server."), ).requires("proto", WAN_DHCP) # DNS servers wan_main.add_field( Textbox, name="ipv4_dns1", label=_("DNS server 1 (IPv4)"), validators=validators.IPv4(), hint=_("DNS server address is not required as the built-in " "DNS resolver is capable of working without it."), ).requires("proto", WAN_STATIC) wan_main.add_field( Textbox, name="ipv4_dns2", label=_("DNS server 2 (IPv4)"), validators=validators.IPv4(), hint=_("DNS server address is not required as the built-in " "DNS resolver is capable of working without it."), ).requires("proto", WAN_STATIC) # xDSL settings wan_main.add_field( Textbox, name="username", label=_("PAP/CHAP username"), required=True, ).requires("proto", WAN_PPPOE) wan_main.add_field( PasswordWithHide, name="password", label=_("PAP/CHAP password"), required=True, ).requires("proto", WAN_PPPOE) # IPv6 configuration wan_main.add_field( Dropdown, name="wan6_proto", label=_("IPv6 protocol"), args=WAN6_OPTIONS, default=WAN6_NONE, ) wan_main.add_field( Textbox, name="ip6addr", label=_("IPv6 address"), validators=validators.IPv6Prefix(), required=True, hint=_("IPv6 address and prefix length for WAN interface, " "e.g. 2001:db8:be13:37da::1/64"), ).requires("wan6_proto", WAN6_STATIC) wan_main.add_field( Textbox, name="ip6gw", label=_("IPv6 gateway"), validators=validators.IPv6(), required=True, ).requires("wan6_proto", WAN6_STATIC) wan_main.add_field( Textbox, name="ip6prefix", label=_("IPv6 prefix"), validators=validators.IPv6Prefix(), hint=_("Address range for local network, " "e.g. 2001:db8:be13:37da::/64"), ).requires("wan6_proto", WAN6_STATIC) # DNS servers wan_main.add_field( Textbox, name="ipv6_dns1", label=_("DNS server 1 (IPv6)"), validators=validators.IPv6(), hint=_("DNS server address is not required as the built-in " "DNS resolver is capable of working without it."), ).requires("wan6_proto", WAN6_STATIC) wan_main.add_field( Textbox, name="ipv6_dns2", label=_("DNS server 2 (IPv6)"), validators=validators.IPv6(), hint=_("DNS server address is not required as the built-in " "DNS resolver is capable of working without it."), ).requires("wan6_proto", WAN6_STATIC) wan_main.add_field( Textbox, name="ip6duid", label=_("Custom DUID"), validators=validators.Duid(), placeholder=self.status_data["last_seen_duid"], hint=_("DUID which will be provided to the DHCPv6 server."), ).requires("wan6_proto", WAN6_DHCP) wan_main.add_field( Textbox, name="6to4_ipaddr", label=_("Public IPv4"), validators=validators.IPv4(), hint= _("In order to use 6to4 protocol, you might need to specify your public IPv4 " "address manually (e.g. when your WAN interface has a private address which " "is mapped to public IP)."), placeholder=_("use autodetection"), required=False, ).requires("wan6_proto", WAN6_6TO4) wan_main.add_field( Textbox, name="6in4_server_ipv4", label=_("Provider IPv4"), validators=validators.IPv4(), hint= _("This address will be used as a endpoint of the tunnel on the provider's side." ), required=True, ).requires("wan6_proto", WAN6_6IN4) wan_main.add_field( Textbox, name="6in4_ipv6_prefix", label=_("Routed IPv6 prefix"), validators=validators.IPv6Prefix(), hint=_("IPv6 addresses which will be routed to your network."), required=True, ).requires("wan6_proto", WAN6_6IN4) wan_main.add_field( Number, name="6in4_mtu", label=_("MTU"), validators=validators.InRange(1280, 1500), hint=_("Maximum Transmission Unit in the tunnel (in bytes)."), required=True, default="1480", ).requires("wan6_proto", WAN6_6IN4) wan_main.add_field( Checkbox, name="6in4_dynamic_enabled", label=_("Dynamic IPv4 handling"), hint=_( "Some tunnel providers allow you to have public dynamic IPv4. " "Note that you need to fill in some extra fields to make it work." ), default=False, ).requires("wan6_proto", WAN6_6IN4) wan_main.add_field( Textbox, name="6in4_tunnel_id", label=_("Tunnel ID"), validators=validators.NotEmpty(), hint=_( "ID of your tunnel which was assigned to you by the provider." ), required=True, ).requires("6in4_dynamic_enabled", True) wan_main.add_field( Textbox, name="6in4_username", label=_("Username"), validators=validators.NotEmpty(), hint= _("Username which will be used to provide credentials to your tunnel provider." ), required=True, ).requires("6in4_dynamic_enabled", True) wan_main.add_field( Textbox, name="6in4_key", label=_("Key"), validators=validators.NotEmpty(), hint= _("Key which will be used to provide credentials to your tunnel provider." ), required=True, ).requires("6in4_dynamic_enabled", True) # custom MAC wan_main.add_field( Checkbox, name="custom_mac", label=_("Custom MAC address"), hint=_( "Useful in cases, when a specific MAC address is required by " "your internet service provider."), ) wan_main.add_field( Textbox, name="macaddr", label=_("MAC address"), validators=validators.MacAddress(), required=True, hint=_( "Colon is used as a separator, for example 00:11:22:33:44:55"), ).requires("custom_mac", True) def wan_form_cb(data): backend_data = WanHandler._convert_form_data_to_backend_data(data) res = current_state.backend.perform("wan", "update_settings", backend_data) return "save_result", res # store {"result": ...} to be used later... wan_form.add_callback(wan_form_cb) return wan_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.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 get_form(self): data = {} data["mode"] = self.backend_data["mode"] data["router_ip"] = self.backend_data["mode_managed"]["router_ip"] data["router_netmask"] = self.backend_data["mode_managed"]["netmask"] data["router_dhcp_enabled"] = self.backend_data["mode_managed"][ "dhcp"]["enabled"] data["router_dhcp_start"] = self.backend_data["mode_managed"]["dhcp"][ "start"] data["router_dhcp_limit"] = self.backend_data["mode_managed"]["dhcp"][ "limit"] data["router_dhcp_leasetime"] = self.backend_data["mode_managed"]["dhcp"]["lease_time"] \ // (60 * 60) data["client_proto_4"] = self.backend_data["mode_unmanaged"][ "lan_type"] data["client_ip_4"] = self.backend_data["mode_unmanaged"][ "lan_static"]["ip"] data["client_netmask_4"] = self.backend_data["mode_unmanaged"][ "lan_static"]["netmask"] data["client_gateway_4"] = self.backend_data["mode_unmanaged"][ "lan_static"]["gateway"] dns1 = self.backend_data["mode_unmanaged"]["lan_static"].get("dns1") if dns1: data["client_dns1_4"] = dns1 dns2 = self.backend_data["mode_unmanaged"]["lan_static"].get("dns2") if dns2: data["client_dns2_4"] = dns2 data["client_hostname_4"] = self.backend_data["mode_unmanaged"][ "lan_dhcp"].get("hostname", "") if self.data: # Update from post data.update(self.data) lan_form = fapi.ForisForm( "lan", data, validators=[ validators.DhcpRangeValidator( 'router_netmask', 'router_dhcp_start', 'router_dhcp_limit', gettext( "<strong>DHCP start</strong> and <strong>DHCP max leases</strong> " "does not fit into <strong>Network netmask</strong>!"), [ lambda data: data['mode'] != 'managed', lambda data: not data['router_dhcp_enabled'], ]) ]) 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( Dropdown, name="mode", label=_("LAN mode"), args=[ ("managed", _("Router")), ("unmanaged", _("Computer")), ], hint= _("Router mode means that this devices manages the LAN " "(acts as a router, can assing IP addresses, ...). " "Computer mode means that this device acts as a client in this network. " "It acts in a similar way as WAN, but it has opened ports for configuration " "interface and other services. "), default="managed", ) # managed options lan_main.add_field( Textbox, name="router_ip", label=_("Router IP address"), validators=validators.IPv4(), hint=_("Router's IP address in the inner network.")).requires( "mode", "managed") lan_main.add_field( Textbox, name="router_netmask", label=_("Network netmask"), validators=validators.IPv4Netmask(), hint=_("Network mask of the inner network.")).requires( "mode", "managed") lan_main.add_field( Checkbox, name="router_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( "mode", "managed") lan_main.add_field( Number, name="router_dhcp_start", label=_("DHCP start"), ).requires("router_dhcp_enabled", True) lan_main.add_field( Number, name="router_dhcp_limit", label=_("DHCP max leases"), ).requires("router_dhcp_enabled", True) lan_main.add_field(Number, name="router_dhcp_leasetime", label=_("Lease time (hours)"), validators=[validators.InRange(1, 7 * 24) ]).requires("router_dhcp_enabled", True) # unmanaged options LAN_DHCP = "dhcp" LAN_STATIC = "static" LAN_NONE = "none" LAN_OPTIONS = ( (LAN_DHCP, _("DHCP (automatic configuration)")), (LAN_STATIC, _("Static IP address (manual configuration)")), (LAN_NONE, _("Don't connect this device to LAN")), ) lan_main.add_field(Dropdown, name="client_proto_4", label=_("IPv4 protocol"), args=LAN_OPTIONS, default=LAN_DHCP).requires("mode", "unmanaged") # unmanaged static lan_main.add_field(Textbox, name="client_ip_4", label=_("IPv4 address"), required=True, validators=validators.IPv4()).requires( "client_proto_4", LAN_STATIC) lan_main.add_field(Textbox, name="client_netmask_4", label=_("Network mask"), required=True, validators=validators.IPv4Netmask()).requires( "client_proto_4", LAN_STATIC) lan_main.add_field( Textbox, name="client_gateway_4", label=_("Gateway"), required=True, validators=validators.IPv4(), ).requires("client_proto_4", LAN_STATIC) lan_main.add_field( Textbox, name="client_dns1_4", label=_("DNS server 1 (IPv4)"), validators=validators.IPv4(), hint=_("DNS server address is not required as the built-in " "DNS resolver is capable of working without it.")).requires( "client_proto_4", LAN_STATIC) lan_main.add_field( Textbox, name="client_dns2_4", label=_("DNS server 2 (IPv4)"), validators=validators.IPv4(), hint=_("DNS server address is not required as the built-in " "DNS resolver is capable of working without it.")).requires( "client_proto_4", LAN_STATIC) # unamanaged dhcp lan_main.add_field( Textbox, name="client_hostname_4", label=_("DHCP hostname"), validators=validators.Domain(), hint=_( "Hostname which will be provided to DHCP server.")).requires( "client_proto_4", LAN_DHCP) def lan_form_cb(data): msg = {"mode": data["mode"]} if msg["mode"] == "managed": dhcp = { "enabled": data["router_dhcp_enabled"], } if dhcp["enabled"]: dhcp["start"] = int(data["router_dhcp_start"]) dhcp["limit"] = int(data["router_dhcp_limit"]) dhcp["lease_time"] = int( data.get("router_dhcp_leasetime", 12)) * 60 * 60 msg["mode_managed"] = { "router_ip": data["router_ip"], "netmask": data["router_netmask"], "dhcp": dhcp, } elif data["mode"] == "unmanaged": msg["mode_unmanaged"] = { "lan_type": data["client_proto_4"], } if data["client_proto_4"] == "static": msg["mode_unmanaged"]["lan_static"] = { "ip": data["client_ip_4"], "netmask": data["client_netmask_4"], "gateway": data["client_gateway_4"], } dns1 = data.get("client_dns1_4") if dns1: msg["mode_unmanaged"]["lan_static"]["dns1"] = dns1 dns2 = data.get("client_dns2_4") if dns2: msg["mode_unmanaged"]["lan_static"]["dns2"] = dns2 elif data["client_proto_4"] == "dhcp": hostname = data.get("client_hostname_4") msg["mode_unmanaged"]["lan_dhcp"] = { "hostname": hostname } if hostname else {} res = current_state.backend.perform("lan", "update_settings", msg) return "save_result", res # store {"result": ...} to be used later... 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
class RemoteHandler(BaseConfigHandler): # Translate status obtained via get_status CLIENT_STATUS_VALID = gettext("valid") CLIENT_STATUS_REVOKED = gettext("revoked") CLIENT_STATUS_EXPIRED = gettext("expired") CLIENT_STATUS_GENERATING = gettext("generating") CLIENT_STATUS_LOST = gettext("lost") TRANSLATION_MAP = { "valid": CLIENT_STATUS_VALID, "revoked": CLIENT_STATUS_REVOKED, "expired": CLIENT_STATUS_EXPIRED, "generating": CLIENT_STATUS_GENERATING, "lost": CLIENT_STATUS_LOST, } userfriendly_title = gettext("Remote Access") def __init__(self, *args, **kwargs): self.backend_data = current_state.backend.perform( "remote", "get_settings") super().__init__(*args, **kwargs) def get_form(self): data = { "enabled": self.backend_data["enabled"], "port": self.backend_data["port"], "wan_access": self.backend_data["wan_access"], } if self.data: # Update from post data.update(self.data) form = fapi.ForisForm("remote", data) config_section = form.add_section(name="set_remote", title=_(self.userfriendly_title)) config_section.add_field( Checkbox, name="enabled", label=_("Enable remote access"), ) config_section.add_field( Checkbox, name="wan_access", label=_("Accessible via WAN"), hint= _("If this option is check the device in the WAN network will be able to connect " "to the configuration interface. Otherwise only devices on LAN will be able to " "access the configuration interface."), ).requires("enabled", True) config_section.add_field( Number, name="port", label=_("Port"), hint=_("A port which will be opened for the remote configuration " "of this device."), validator=[InRange(1, 2**16 - 1)], default=11884, ).requires("enabled", True) def form_callback(data): msg = {"enabled": data['enabled']} if msg["enabled"]: msg["port"] = int(data["port"]) msg["wan_access"] = data['wan_access'] res = current_state.backend.perform("remote", "update_settings", msg) res['enabled'] = msg['enabled'] return "save_result", res # store {"result": ...} to be used later... form.add_callback(form_callback) return form def get_generate_token_form(self, data=None): generate_token_form = fapi.ForisForm("generate_remote_token", data) token_section = generate_token_form.add_section("generate_token", title=None) token_section.add_field( Textbox, name="name", label=_("Token name"), required=True, hint= _("The display name for the token. It must be shorter than 64 characters " "and must contain only alphanumeric characters, dots, dashes and " "underscores."), validators=[ RegExp(_("Token name is invalid."), r'[a-zA-Z0-9_.-]+'), LenRange(1, 63) ]) return generate_token_form def get_token_id_form(self, data=None): token_id_form = fapi.ForisForm("token_id_form", data) token_section = token_id_form.add_section("token_id_section", title=None) token_section.add_field(Textbox, name="token_id", label="", required=True, validators=[ RegExp(_("Token id is invalid."), r'([a-zA-Z0-9][a-zA-Z0-9])+') ]) token_section.add_field( Textbox, name="name", label=_("Token name"), required=False, validators=[ RegExp(_("Token name is invalid."), r'[a-zA-Z0-9_.-]+'), LenRange(1, 63) ], ) return token_id_form
class WifiHandler(BaseConfigHandler): userfriendly_title = gettext("Wi-Fi") @staticmethod def prefixed(index, name): return "radio%d-%s" % (index, name) def _backend_data_to_form_data(self, backend_data): form_data = {} for device in backend_data["devices"]: def prefixed(name): return WifiHandler.prefixed(device["id"], name) form_data[prefixed("device_enabled")] = device["enabled"] form_data[prefixed("ssid")] = device["SSID"] form_data[prefixed("ssid_hidden")] = device["hidden"] form_data[prefixed("hwmode")] = device["hwmode"] form_data[prefixed("htmode")] = device["htmode"] form_data[prefixed("channel")] = str(device["channel"]) form_data[prefixed("password")] = device["password"] form_data[prefixed( "guest_enabled")] = device["guest_wifi"]["enabled"] form_data[prefixed("guest_ssid")] = device["guest_wifi"]["SSID"] form_data[prefixed( "guest_password")] = device["guest_wifi"]["password"] return form_data 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 _form_data_to_backend_data(self, form_data, device_ids): res = [] for dev_id in device_ids: def prefixed(name): return WifiHandler.prefixed(dev_id, name) dev_rec = {"id": dev_id} dev_rec["enabled"] = form_data[prefixed("device_enabled")] if dev_rec["enabled"]: dev_rec["SSID"] = form_data[prefixed("ssid")] dev_rec["hidden"] = form_data[prefixed("ssid_hidden")] dev_rec["hwmode"] = form_data[prefixed("hwmode")] dev_rec["htmode"] = form_data[prefixed("htmode")] dev_rec["channel"] = int(form_data[prefixed("channel")]) dev_rec["guest_wifi"] = {} dev_rec["guest_wifi"]["enabled"] = form_data.get( prefixed("guest_enabled"), False) dev_rec["password"] = form_data[prefixed("password")] if dev_rec["guest_wifi"]["enabled"]: dev_rec["guest_wifi"]["SSID"] = form_data[prefixed( "guest_ssid")] dev_rec["guest_wifi"]["password"] = form_data[prefixed( "guest_password")] res.append(dev_rec) return {"devices": res} def get_form(self): backend_data = current_state.backend.perform("wifi", "get_settings") form_data = self._backend_data_to_form_data(backend_data) if self.data: form_data.update(self.data) wifi_form = fapi.ForisForm("wifi", form_data) wifi_form.add_section( name="wifi", title=_(self.userfriendly_title), description= _("If you want to use your router as a Wi-Fi access point, enable Wi-Fi " "here and fill in an SSID (the name of the access point) and a " "corresponding password. You can then set up your mobile devices, " "using the QR code available within the form.")) # Add wifi section wifi_section = wifi_form.add_section( name="wifi_settings", title=_("Wi-Fi settings"), ) for idx, device in enumerate(backend_data["devices"]): prefix = WifiHandler.prefixed(device["id"], "") device_form_data = { k[len(prefix):]: v for k, v in form_data.items() if k.startswith(prefix) } # prefix removed self._prepare_device_fields( wifi_section, device, device_form_data, len(backend_data["devices"]) - 1 == idx) def form_cb(data): update_data = self._form_data_to_backend_data( data, [e["id"] for e in backend_data["devices"]]) res = current_state.backend.perform("wifi", "update_settings", update_data) return "save_result", res # store {"result": ...} to be used later... wifi_form.add_callback(form_cb) return wifi_form
def get_form(self): data = {} data["mode"] = self.backend_data["mode"] data["router_ip"] = self.backend_data["mode_managed"]["router_ip"] data["router_netmask"] = self.backend_data["mode_managed"]["netmask"] data["router_dhcp_enabled"] = self.backend_data["mode_managed"]["dhcp"]["enabled"] data["router_dhcp_start"] = self.backend_data["mode_managed"]["dhcp"]["start"] data["router_dhcp_limit"] = self.backend_data["mode_managed"]["dhcp"]["limit"] data["router_dhcp_leasetime"] = self.backend_data["mode_managed"]["dhcp"]["lease_time"] // ( 60 * 60 ) data["client_proto_4"] = self.backend_data["mode_unmanaged"]["lan_type"] data["client_ip_4"] = self.backend_data["mode_unmanaged"]["lan_static"]["ip"] data["client_netmask_4"] = self.backend_data["mode_unmanaged"]["lan_static"]["netmask"] data["client_gateway_4"] = self.backend_data["mode_unmanaged"]["lan_static"]["gateway"] dns1 = self.backend_data["mode_unmanaged"]["lan_static"].get("dns1") if dns1: data["client_dns1_4"] = dns1 dns2 = self.backend_data["mode_unmanaged"]["lan_static"].get("dns2") if dns2: data["client_dns2_4"] = dns2 data["client_hostname_4"] = self.backend_data["mode_unmanaged"]["lan_dhcp"].get( "hostname", "" ) if self.data: # Update from post data.update(self.data) lan_form = fapi.ForisForm( "lan", data, validators=[ validators.DhcpRangeValidator( "router_netmask", "router_dhcp_start", "router_dhcp_limit", gettext( "<strong>DHCP start</strong> and <strong>DHCP max leases</strong> " "does not fit into <strong>Network netmask</strong>!" ), [ lambda data: data["mode"] != "managed", lambda data: not data["router_dhcp_enabled"], ], ) ], ) 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( Dropdown, name="mode", label=_("LAN mode"), args=[("managed", _("Router")), ("unmanaged", _("Computer"))], hint=_( "Router mode means that this devices manages the LAN " "(acts as a router, can assing IP addresses, ...). " "Computer mode means that this device acts as a client in this network. " "It acts in a similar way as WAN, but it has opened ports for configuration " "interface and other services." ), default="managed", ) # managed options lan_main.add_field( Textbox, name="router_ip", label=_("Router IP address"), validators=validators.IPv4(), hint=_("Router's IP address in the inner network."), ).requires("mode", "managed") lan_main.add_field( Textbox, name="router_netmask", label=_("Network netmask"), validators=validators.IPv4Netmask(), hint=_("Network mask of the inner network."), ).requires("mode", "managed") lan_main.add_field( Checkbox, name="router_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("mode", "managed") lan_main.add_field(Number, name="router_dhcp_start", label=_("DHCP start")).requires( "router_dhcp_enabled", True ) lan_main.add_field(Number, name="router_dhcp_limit", label=_("DHCP max leases")).requires( "router_dhcp_enabled", True ) lan_main.add_field( Number, name="router_dhcp_leasetime", label=_("Lease time (hours)"), validators=[validators.InRange(1, 7 * 24)], ).requires("router_dhcp_enabled", True) # unmanaged options LAN_DHCP = "dhcp" LAN_STATIC = "static" LAN_NONE = "none" LAN_OPTIONS = ( (LAN_DHCP, _("DHCP (automatic configuration)")), (LAN_STATIC, _("Static IP address (manual configuration)")), (LAN_NONE, _("Don't connect this device to LAN")), ) lan_main.add_field( Dropdown, name="client_proto_4", label=_("IPv4 protocol"), args=LAN_OPTIONS, default=LAN_DHCP, ).requires("mode", "unmanaged") # unmanaged static lan_main.add_field( Textbox, name="client_ip_4", label=_("IPv4 address"), required=True, validators=validators.IPv4(), ).requires("client_proto_4", LAN_STATIC) lan_main.add_field( Textbox, name="client_netmask_4", label=_("Network mask"), required=True, validators=validators.IPv4Netmask(), ).requires("client_proto_4", LAN_STATIC) lan_main.add_field( Textbox, name="client_gateway_4", label=_("Gateway"), required=True, validators=validators.IPv4(), ).requires("client_proto_4", LAN_STATIC) lan_main.add_field( Textbox, name="client_dns1_4", label=_("DNS server 1 (IPv4)"), validators=validators.IPv4(), hint=_( "DNS server address is not required as the built-in " "DNS resolver is capable of working without it." ), ).requires("client_proto_4", LAN_STATIC) lan_main.add_field( Textbox, name="client_dns2_4", label=_("DNS server 2 (IPv4)"), validators=validators.IPv4(), hint=_( "DNS server address is not required as the built-in " "DNS resolver is capable of working without it." ), ).requires("client_proto_4", LAN_STATIC) # unamanaged dhcp lan_main.add_field( Textbox, name="client_hostname_4", label=_("DHCP hostname"), validators=validators.Domain(), hint=_("Hostname which will be provided to DHCP server."), ).requires("client_proto_4", LAN_DHCP) def lan_form_cb(data): msg = {"mode": data["mode"]} if msg["mode"] == "managed": dhcp = {"enabled": data["router_dhcp_enabled"]} if dhcp["enabled"]: dhcp["start"] = int(data["router_dhcp_start"]) dhcp["limit"] = int(data["router_dhcp_limit"]) dhcp["lease_time"] = int(data.get("router_dhcp_leasetime", 12)) * 60 * 60 msg["mode_managed"] = { "router_ip": data["router_ip"], "netmask": data["router_netmask"], "dhcp": dhcp, } elif data["mode"] == "unmanaged": msg["mode_unmanaged"] = {"lan_type": data["client_proto_4"]} if data["client_proto_4"] == "static": msg["mode_unmanaged"]["lan_static"] = { "ip": data["client_ip_4"], "netmask": data["client_netmask_4"], "gateway": data["client_gateway_4"], } dns1 = data.get("client_dns1_4") if dns1: msg["mode_unmanaged"]["lan_static"]["dns1"] = dns1 dns2 = data.get("client_dns2_4") if dns2: msg["mode_unmanaged"]["lan_static"]["dns2"] = dns2 elif data["client_proto_4"] == "dhcp": hostname = data.get("client_hostname_4") msg["mode_unmanaged"]["lan_dhcp"] = {"hostname": hostname} if hostname else {} res = current_state.backend.perform("lan", "update_settings", msg) return "save_result", res # store {"result": ...} to be used later... lan_form.add_callback(lan_form_cb) return lan_form
class UpdaterHandler(BaseConfigHandler): userfriendly_title = gettext("Updater") APPROVAL_NO = "off" APPROVAL_TIMEOUT = "delayed" APPROVAL_NEEDED = "on" APPROVAL_DEFAULT = APPROVAL_NO APPROVAL_DEFAULT_DELAY = 1.0 def __init__(self, *args, **kwargs): super(UpdaterHandler, self).__init__(*args, **kwargs) # Check whether updater is supposed to be always on and store the reason why self.always_on_reasons: typing.List[str] = [] for entry_point in pkg_resources.iter_entry_points( "updater_always_on"): logger.info("Processing 'updater_always_on' for '%s' plugin", entry_point.name) reason: typing.Option[str] = entry_point.load()() if reason: self.always_on_reasons.append(reason) self.backend_data = current_state.backend.perform( "updater", "get_settings", {"lang": current_state.language}) # store setting required for rendering self.current_approval = self.backend_data["approval"] # update can be in 3 states: True, False, None # None means that it is not set in this case we want to prefill True self.updater_enabled = False if self.backend_data[ "enabled"] is False else True self.approval_setting_status = self.backend_data["approval_settings"][ "status"] self.approval_setting_delay = self.backend_data[ "approval_settings"].get("delay", self.APPROVAL_DEFAULT_DELAY) def get_form(self): data = copy.deepcopy(self.backend_data) data["enabled"] = "0" if data["enabled"] is False else "1" data["approval_status"] = data["approval_settings"]["status"] if "delay" in data["approval_settings"]: data["approval_delay"] = "%.1f" % ( data["approval_settings"]["delay"] / 24.0) for userlist in [e for e in data["user_lists"] if not e["hidden"]]: data["install_%s" % userlist["name"]] = userlist["enabled"] for lang in data["languages"]: data["language_%s" % lang["code"]] = lang["enabled"] if self.data: # Update from post data.update(self.data) self.updater_enabled = True if data["enabled"] == "1" else False self.approval_setting_status = data["approval_status"] self.approval_setting_delay = data.get("approval_delay", self.APPROVAL_DEFAULT_DELAY) form = fapi.ForisForm("updater", data) main_section = form.add_section( name="main", title=_(self.userfriendly_title), description=_("Updater is a service that keeps all TurrisOS " "software up to date. Apart from the standard " "installation, you can optionally select bundles of " "additional software that'd be installed on the " "router. This software can be selected from the " "following list. " "Please note that only software that is part of " "TurrisOS or that has been installed from a package " "list is maintained by Updater. Software that has " "been installed manually or using opkg is not " "affected."), ) main_section.add_field( Radio, name="enabled", label=_("I agree"), default="1", args=( ("1", _("Use automatic updates (recommended)")), ("0", _("Turn automatic updates off")), ), ) approval_section = main_section.add_section( name="approvals", title=_("Update approvals")) approval_section.add_field( RadioSingle, name=UpdaterHandler.APPROVAL_NO, group="approval_status", label=_("Automatic installation"), hint=_("Updates will be installed without user's intervention."), default=data["approval_status"], ) approval_section.add_field( RadioSingle, name=UpdaterHandler.APPROVAL_TIMEOUT, group="approval_status", label=_("Delayed updates"), hint=_("Updates will be installed with an adjustable delay. " "You can also approve them manually."), default=data["approval_status"], ) approval_section.add_field( Textbox, name="approval_delay", validators=[validators.FloatRange(0.1, 31.0)], default=UpdaterHandler.APPROVAL_DEFAULT_DELAY, required=True, ).requires(UpdaterHandler.APPROVAL_TIMEOUT, UpdaterHandler.APPROVAL_TIMEOUT).requires( UpdaterHandler.APPROVAL_NO, UpdaterHandler.APPROVAL_TIMEOUT).requires( UpdaterHandler.APPROVAL_NEEDED, UpdaterHandler.APPROVAL_TIMEOUT) approval_section.add_field( RadioSingle, name=UpdaterHandler.APPROVAL_NEEDED, group="approval_status", label=_("Update approval needed"), hint= _("You have to approve the updates, otherwise they won't be installed." ), default=data["approval_status"], ) package_lists_main = main_section.add_section( name="select_package_lists", title=None) for userlist in [e for e in data["user_lists"] if not e["hidden"]]: package_lists_main.add_field( Checkbox, name="install_%s" % userlist["name"], label=userlist["title"], hint=userlist["msg"], ).requires("enabled", "1") language_lists_main = main_section.add_section( name="select_languages", title=_( "If you want to use other language than English you can select it from the " "following list:"), ) for lang in data["languages"]: language_lists_main.add_field(Checkbox, name="language_%s" % lang["code"], label=lang["code"].upper()) if self.backend_data["approval"]["present"]: # field for hidden approval current_approval_section = main_section.add_section( name="current_approval", title="") current_approval_section.add_field( Hidden, name="approval-id", default=self.backend_data["approval"]["hash"]) # this will be filled according to action main_section.add_field(Hidden, name="target") def form_cb(data): data["enabled"] = True if data["enabled"] == "1" else False if data["enabled"] and data["target"] == "save": if data[self.APPROVAL_NEEDED] == self.APPROVAL_NEEDED: data["approval_settings"] = { "status": self.APPROVAL_NEEDED } elif data[self.APPROVAL_TIMEOUT] == self.APPROVAL_TIMEOUT: data["approval_settings"] = { "status": self.APPROVAL_TIMEOUT } data["approval_settings"]["delay"] = int( float(data["approval_delay"]) * 24) elif data[self.APPROVAL_NO] == self.APPROVAL_NO: data["approval_settings"] = {"status": self.APPROVAL_NO} if self.always_on_reasons: # don't disable updater when there are reasons data["enabled"] = True languages = [ k[9:] for k, v in data.items() if v and k.startswith("language_") ] user_lists = [ k[8:] for k, v in data.items() if v and k.startswith("install_") ] # merge with enabled hidden user lists user_lists += [ e["name"] for e in self.backend_data["user_lists"] if e["hidden"] and e["enabled"] ] res = current_state.backend.perform( "updater", "update_settings", { "enabled": True, "approval_settings": data["approval_settings"], "user_lists": user_lists, "languages": languages, }, ) elif data["enabled"] and data["target"] in ["grant", "deny"]: res = current_state.backend.perform( "updater", "resolve_approval", { "hash": data["approval-id"], "solution": data["target"] }, ) else: res = current_state.backend.perform("updater", "update_settings", {"enabled": False}) res["target"] = data["target"] res["enabled"] = data["enabled"] return "save_result", res form.add_callback(form_cb) return form
class NotificationsHandler(BaseConfigHandler): userfriendly_title = gettext("Notifications") def get_form(self): data = current_state.backend.perform("router_notifications", "get_settings") data["enable_smtp"] = data["emails"]["enabled"] data["use_turris_smtp"] = "1" if data["emails"][ "smtp_type"] == "turris" else "0" data["to"] = " ".join(data["emails"]["common"]["to"]) data["sender_name"] = data["emails"]["smtp_turris"]["sender_name"] data["severity"] = data["emails"]["common"]["severity_filter"] data["news"] = data["emails"]["common"]["send_news"] data["from"] = data["emails"]["smtp_custom"]["from"] data["server"] = data["emails"]["smtp_custom"]["host"] data["port"] = data["emails"]["smtp_custom"]["port"] data["security"] = data["emails"]["smtp_custom"]["security"] data["username"] = data["emails"]["smtp_custom"]["username"] data["password"] = data["emails"]["smtp_custom"]["password"] data["delay"] = str(data["reboots"]["delay"]) data["reboot_time"] = data["reboots"]["time"] if self.data: # Update from post data.update(self.data) notifications_form = fapi.ForisForm("notifications", data) notifications = notifications_form.add_section( name="notifications", title=_("Notifications settings")) # notifications settings notifications.add_field(Checkbox, name="enable_smtp", label=_("Enable notifications"), default=False) notifications.add_field( Radio, name="use_turris_smtp", label=_("SMTP provider"), default="0", args=(("1", _("Turris")), ("0", _("Custom"))), hint= _('If you set SMTP provider to "Turris", the servers provided to members of the ' "Turris project would be used. These servers do not require any additional " 'settings. If you want to set your own SMTP server, please select "Custom" ' "and enter required settings."), ).requires("enable_smtp", True) notifications.add_field( Textbox, name="to", label=_("Recipient's email"), hint= _("Email address of recipient. Separate multiple addresses by spaces." ), required=True, validators=[ validators.RegExp( _("Doesn't contain a list of emails separated by spaces"), r"^([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+ *)( +[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+ *)*$", ) ], ).requires("enable_smtp", True) # sender's name for CZ.NIC SMTP only notifications.add_field( Textbox, name="sender_name", label=_("Sender's name"), hint=_("Name of the sender - will be used as a part of the " 'sender\'s email address before the "at" sign.'), validators=[ validators.RegExp( _("Sender's name can contain only alphanumeric characters, dots " "and underscores."), r"^[0-9a-zA-Z_\.-]+$", ) ], required=True, ).requires("enable_smtp", True).requires("use_turris_smtp", "1") SEVERITY_OPTIONS = ( (1, _("Reboot is required")), (2, _("Reboot or attention is required")), (3, _("Reboot or attention is required or update was installed")), ) notifications.add_field(Dropdown, name="severity", label=_("Importance"), args=SEVERITY_OPTIONS, default=1).requires("enable_smtp", True) notifications.add_field( Checkbox, name="news", label=_("Send news"), hint=_("Send emails about new features."), default=True, ).requires("enable_smtp", True) # SMTP settings (custom server) smtp = notifications_form.add_section(name="smtp", title=_("SMTP settings")) smtp.add_field( Email, name="from", label=_("Sender address (From)"), hint=_("This is the address notifications are send from."), required=True, placeholder="*****@*****.**", ).requires("enable_smtp", True).requires("use_turris_smtp", "0") smtp.add_field(Textbox, name="server", label=_("Server address"), placeholder="example.com").requires( "enable_smtp", True).requires("use_turris_smtp", "0") smtp.add_field( Number, name="port", label=_("Server port"), validators=[validators.PositiveInteger()], required=True, ).requires("enable_smtp", True).requires("use_turris_smtp", "0") SECURITY_OPTIONS = (("none", _("None")), ("ssl", _("SSL/TLS")), ("starttls", _("STARTTLS"))) smtp.add_field(Dropdown, name="security", label=_("Security"), args=SECURITY_OPTIONS, default="none").requires("enable_smtp", True).requires( "use_turris_smtp", "0") smtp.add_field(Textbox, name="username", label=_("Username")).requires( "enable_smtp", True).requires("use_turris_smtp", "0") smtp.add_field(Password, name="password", label=_("Password")).requires( "enable_smtp", True).requires("use_turris_smtp", "0") # reboot time reboot = notifications_form.add_section( name="reboot", title=_("Automatic restarts after software update")) reboot.add_field( Number, name="delay", label=_("Delay (days)"), hint=_( "Number of days that must pass between receiving the request " "for restart and the automatic restart itself."), validators=[ validators.PositiveInteger(), validators.InRange(0, 10) ], required=True, ) reboot.add_field( Time, name="reboot_time", label=_("Reboot time"), hint=_("Time of day of automatic reboot in HH:MM format."), validators=[validators.Time()], required=True, ) def notifications_form_cb(data): msg = { "reboots": { "delay": int(data["delay"]), "time": data["reboot_time"] }, "emails": { "enabled": data["enable_smtp"] }, } if data["enable_smtp"]: msg["emails"]["smtp_type"] = ( "turris" if data["use_turris_smtp"] == "1" else "custom") msg["emails"]["common"] = { "to": [e for e in data["to"].split(" ") if e], "severity_filter": int(data["severity"]), "send_news": data["news"], } if msg["emails"]["smtp_type"] == "turris": msg["emails"]["smtp_turris"] = { "sender_name": data["sender_name"] } elif msg["emails"]["smtp_type"] == "custom": msg["emails"]["smtp_custom"] = { "from": data["from"], "host": data["server"], "port": int(data["port"]), "security": data["security"], "username": data["username"], "password": data["password"], } res = current_state.backend.perform("router_notifications", "update_settings", msg) return "save_result", res # store {"result": ...} to be used later... notifications_form.add_callback(notifications_form_cb) return notifications_form
class DNSHandler(BaseConfigHandler): """ DNS-related settings """ userfriendly_title = gettext("DNS") def get_form(self): data = current_state.backend.perform("dns", "get_settings") available_forwarders = [[e["name"], e["description"]] for e in data["available_forwarders"]] data["dnssec_disabled"] = not data["dnssec_enabled"] if self.data: # Update from post data.update(self.data) data["dnssec_enabled"] = not self.data.get("dnssec_disabled", False) dns_form = fapi.ForisForm("dns", data) dns_main = dns_form.add_section(name="set_dns", title=_(self.userfriendly_title)) dns_main.add_field( Checkbox, name="forwarding_enabled", label=_("Use forwarding"), preproc=lambda val: bool(int(val)), ) available_forwarders = sorted(available_forwarders, key=lambda x: x[0]) # fill in text for forwarder (first with "" name) available_forwarders[0][1] = _("Use provider's DNS resolver") dns_main.add_field(Dropdown, name="forwarder", label=_("DNS Forwarder"), args=available_forwarders).requires( "forwarding_enabled", True) dns_main.add_field( Checkbox, name="dnssec_disabled", label=_("Disable DNSSEC"), preproc=lambda val: bool(int(val)), default=False, ) dns_main.add_field( Checkbox, name="dns_from_dhcp_enabled", label=_("Enable DHCP clients in DNS"), hint=_("This will enable your DNS resolver to place DHCP client's " "names among the local DNS records."), preproc=lambda val: bool(int(val)), default=False, ) dns_main.add_field( Textbox, name="dns_from_dhcp_domain", label=_("Domain of DHCP clients in DNS"), hint=_( 'This domain will be used as suffix. E.g. The result for client "android-123" ' 'and domain "my.lan" will be "android-123.my.lan".'), validators=[validators.Domain()], ).requires("dns_from_dhcp_enabled", True) def dns_form_cb(data): msg = { "dnssec_enabled": not data.get("dnssec_disabled", False), "forwarding_enabled": data["forwarding_enabled"], "dns_from_dhcp_enabled": data["dns_from_dhcp_enabled"], } if "dns_from_dhcp_domain" in data: msg["dns_from_dhcp_domain"] = data["dns_from_dhcp_domain"] if data["forwarding_enabled"]: msg["forwarder"] = data.get("forwarder", "") res = current_state.backend.perform("dns", "update_settings", msg) return "save_result", res # store {"result": ...} to be used later... dns_form.add_callback(dns_form_cb) return dns_form
class UpdaterHandler(BaseConfigHandler): userfriendly_title = gettext("Updater") APPROVAL_NO = "off" APPROVAL_TIMEOUT = "delayed" APPROVAL_NEEDED = "on" APPROVAL_DEFAULT = APPROVAL_NO APPROVAL_DEFAULT_DELAY = 24 def __init__(self, *args, **kwargs): super(UpdaterHandler, self).__init__(*args, **kwargs) agreed = current_state.backend.perform( "data_collect", "get", raise_exception_on_failure=False) self.agreed_collect = False if agreed is None else agreed["agreed"] self.backend_data = current_state.backend.perform( "updater", "get_settings", {"lang": current_state.language}) # store setting required for rendering self.current_approval = self.backend_data["approval"] self.updater_enabled = self.backend_data["enabled"] self.approval_setting_status = self.backend_data["approval_settings"][ "status"] self.approval_setting_delay = self.backend_data[ "approval_settings"].get("delay", self.APPROVAL_DEFAULT_DELAY) def get_form(self): data = copy.deepcopy(self.backend_data) data["enabled"] = "1" if data["enabled"] else "0" data["approval_status"] = data["approval_settings"]["status"] if "delay" in data["approval_settings"]: data["approval_delay"] = data["approval_settings"]["delay"] for userlist in [e for e in data['user_lists'] if not e["hidden"]]: data["install_%s" % userlist["name"]] = userlist["enabled"] for lang in data["languages"]: data["language_%s" % lang["code"]] = lang["enabled"] if self.data: # Update from post data.update(self.data) self.updater_enabled = True if data["enabled"] == "1" else False self.approval_setting_status = data["approval_status"] self.approval_setting_delay = data.get("approval_delay", self.APPROVAL_DEFAULT_DELAY) form = fapi.ForisForm("updater", data) main_section = form.add_section( name="main", title=_(self.userfriendly_title), description=_("Updater is a service that keeps all TurrisOS " "software up to date. Apart from the standard " "installation, you can optionally select bundles of " "additional software that'd be installed on the " "router. This software can be selected from the " "following list. " "Please note that only software that is part of " "TurrisOS or that has been installed from a package " "list is maintained by Updater. Software that has " "been installed manually or using opkg is not " "affected.")) main_section.add_field( Radio, name="enabled", label=_("I agree"), default="1", args=(("1", _("Use automatic updates (recommended)")), ("0", _("Turn automatic updates off"))), ) approval_section = main_section.add_section( name="approvals", title=_("Update approvals")) approval_section.add_field( RadioSingle, name=UpdaterHandler.APPROVAL_NO, group="approval_status", label=_("Automatic installation"), hint=_("Updates will be installed without user's intervention."), default=data["approval_status"], ) approval_section.add_field( RadioSingle, name=UpdaterHandler.APPROVAL_TIMEOUT, group="approval_status", label=_("Delayed updates"), hint=_("Updates will be installed with an adjustable delay. " "You can also approve them manually."), default=data["approval_status"], ) approval_section.add_field( Number, name="approval_delay", validators=[validators.InRange(1, 24 * 7)], default=UpdaterHandler.APPROVAL_DEFAULT_DELAY, min=1, max=24 * 7, required=True, ).requires(UpdaterHandler.APPROVAL_TIMEOUT, UpdaterHandler.APPROVAL_TIMEOUT).requires( UpdaterHandler.APPROVAL_NO, UpdaterHandler.APPROVAL_TIMEOUT).requires( UpdaterHandler.APPROVAL_NEEDED, UpdaterHandler.APPROVAL_TIMEOUT) approval_section.add_field( RadioSingle, name=UpdaterHandler.APPROVAL_NEEDED, group="approval_status", label=_("Update approval needed"), hint= _("You have to approve the updates, otherwise they won't be installed." ), default=data["approval_status"], ) package_lists_main = main_section.add_section( name="select_package_lists", title=None, ) for userlist in [e for e in data['user_lists'] if not e["hidden"]]: package_lists_main.add_field(Checkbox, name="install_%s" % userlist["name"], label=userlist["title"], hint=userlist["msg"]).requires( "enabled", "1") language_lists_main = main_section.add_section( name="select_languages", title=_( "If you want to use other language than English you can select it from the " "following list:")) for lang in data["languages"]: language_lists_main.add_field(Checkbox, name="language_%s" % lang["code"], label=lang["code"].upper()) if self.backend_data["approval"]["present"]: # field for hidden approval current_approval_section = main_section.add_section( name="current_approval", title="") current_approval_section.add_field( Hidden, name="approval-id", default=self.backend_data["approval"]["hash"]) # this will be filled according to action main_section.add_field(Hidden, name="target") def form_cb(data): data["enabled"] = True if data["enabled"] == "1" else False if data["enabled"] and data["target"] == "save": if data[self.APPROVAL_NEEDED] == self.APPROVAL_NEEDED: data["approval_settings"] = { "status": self.APPROVAL_NEEDED } elif data[self.APPROVAL_TIMEOUT] == self.APPROVAL_TIMEOUT: data["approval_settings"] = { "status": self.APPROVAL_TIMEOUT } data["approval_settings"]["delay"] = int( data["approval_delay"]) elif data[self.APPROVAL_NO] == self.APPROVAL_NO: data["approval_settings"] = {"status": self.APPROVAL_NO} if self.agreed_collect: data["enabled"] = True languages = [ k[9:] for k, v in data.items() if v and k.startswith("language_") ] user_lists = [ k[8:] for k, v in data.items() if v and k.startswith("install_") ] # merge with enabled hidden user lists user_lists += [ e["name"] for e in self.backend_data["user_lists"] if e["hidden"] and e["enabled"] ] res = current_state.backend.perform( "updater", "update_settings", { "enabled": True, "approval_settings": data["approval_settings"], "user_lists": user_lists, "languages": languages, }) elif data["enabled"] and data["target"] in ["grant", "deny"]: res = current_state.backend.perform( "updater", "resolve_approval", { "hash": data["approval-id"], "solution": data["target"] }) else: res = current_state.backend.perform("updater", "update_settings", { "enabled": False, }) res["target"] = data["target"] return "save_result", res form.add_callback(form_cb) return form
class PasswordHandler(BaseConfigHandler): """ Setting the password """ userfriendly_title = gettext("Password") def __init__(self, *args, **kwargs): self.change = kwargs.pop("change", False) super(PasswordHandler, self).__init__(*args, **kwargs) def get_form(self): # form definitions pw_form = fapi.ForisForm("password", self.data) pw_main = pw_form.add_section(name="passwords", title=_(self.userfriendly_title)) if self.change: pw_main.add_field(Password, name="old_password", label=_("Current Foris password")) label_pass1 = _("New password") label_pass2 = _("New password (repeat)") else: label_pass1 = _("Password") label_pass2 = _("Password (repeat)") pw_foris = pw_form.add_section( name="foris_pw", title=_("for Foris web interface"), description=_("Set your password for this administration " "interface. The password must be at least 6 " "characters long.")) pw_foris.add_field(Password, name="password", label=label_pass1, required=True, validators=validators.LenRange(6, 128)) pw_foris.add_field(Password, name="password_validation", label=label_pass2, required=True, validators=validators.EqualTo( "password", "password_validation", _("Passwords are not equal."))) system_pw = pw_form.add_section( name="system_pw", title=_("for Advanced administration"), description= _("In order to access the advanced configuration options which are " "not available here, you must set the root user's password. The advanced " "configuration options can be managed either through the " "<a href=\"//%(host)s/%(path)s\">LuCI web interface</a> " "or via SSH.") % { 'host': bottle.request.get_header('host'), 'path': 'cgi-bin/luci' }) SYSTEM_PW_SKIP = 'skip' SYSTEM_PW_SAME = 'same' SYSTEM_PW_CUSTOM = 'custom' SYSTEM_PW_OPTIONS = ( (SYSTEM_PW_SKIP, _("Don't set this password")), (SYSTEM_PW_SAME, _("Use Foris password")), (SYSTEM_PW_CUSTOM, _("Use other password")), ) system_pw.add_field( Dropdown, name="set_system_pw", label=_("Advanced administration"), hint=_( "Same password would be used for accessing this administration " "interface, for root user in LuCI web interface and for SSH " "login. Use a strong password! (If you choose not to set the " "password for advanced configuration here, you will have the " "option to do so later. Until then, the root account will be " "blocked.)"), args=SYSTEM_PW_OPTIONS, default=SYSTEM_PW_SKIP, ) system_pw.add_field(Password, name="system_password", label=_("Password"), required=True, validators=validators.LenRange(6, 128)).requires( "set_system_pw", SYSTEM_PW_CUSTOM) system_pw.add_field(Password, name="system_password_validation", label=_("New password (repeat)"), required=True, validators=validators.EqualTo( "system_password", "system_password_validation", _("Passwords are not equal."))).requires( "set_system_pw", SYSTEM_PW_CUSTOM) def pw_form_cb(data): if self.change: if not check_password(data['old_password']): return "save_result", {'wrong_old_password': True} encoded_password = base64.b64encode( data["password"].encode("utf-8")).decode("utf-8") result = current_state.backend.perform("password", "set", { "password": encoded_password, "type": "foris" })["result"] res = {"foris_password_no_error": result} if data['set_system_pw'] == SYSTEM_PW_SAME: result = current_state.backend.perform("password", "set", { "password": encoded_password, "type": "system" })["result"] res["system_password_no_error"] = result elif data['set_system_pw'] == SYSTEM_PW_CUSTOM: encoded_pw = base64.b64encode( data["system_password"].encode("utf-8")).decode("utf-8") result = current_state.backend.perform("password", "set", { "password": encoded_pw, "type": "system" })["result"] res["system_password_no_error"] = result return "save_result", res pw_form.add_callback(pw_form_cb) return pw_form
class DataCollectPluginPage(ConfigPageMixin, DataCollectPluginConfigHandler): userfriendly_title = gettext("Data collection") slug = "data_collect" menu_order = 80 template = "data_collect/data_collect" template_type = "jinja2" SENDING_STATUS_TRANSLATION = { 'online': gettext("Online"), 'offline': gettext("Offline"), 'unknown': gettext("Unknown status"), } def save(self, *args, **kwargs): super().save(no_messages=True, *args, **kwargs) result = self.form.callback_results.get('result', False) if result: messages.success(_("Configuration was successfully saved.")) else: messages.error( _("Failed to update emulated services. Note that you might need to wait till " "ucollect is properly installed.")) return result def _prepare_render_args(self, args): args['PLUGIN_NAME'] = DataCollectPlugin.PLUGIN_NAME args['PLUGIN_STYLES'] = DataCollectPlugin.PLUGIN_STYLES args['PLUGIN_STATIC_SCRIPTS'] = DataCollectPlugin.PLUGIN_STATIC_SCRIPTS args[ 'PLUGIN_DYNAMIC_SCRIPTS'] = DataCollectPlugin.PLUGIN_DYNAMIC_SCRIPTS def render(self, **kwargs): self._prepare_render_args(kwargs) status = kwargs.pop("status", None) updater_data = current_state.backend.perform("updater", "get_enabled") kwargs['updater_disabled'] = not updater_data["enabled"] if updater_data["enabled"]: collect_data = current_state.backend.perform("data_collect", "get") firewall_status = collect_data["firewall_status"] firewall_status["seconds_ago"] = int(time.time() - firewall_status["last_check"]) firewall_status["datetime"] = datetime.fromtimestamp( firewall_status["last_check"]) firewall_status["state_trans"] = self.SENDING_STATUS_TRANSLATION[ firewall_status["state"]] ucollect_status = collect_data["ucollect_status"] ucollect_status["seconds_ago"] = int(time.time() - ucollect_status["last_check"]) ucollect_status["datetime"] = datetime.fromtimestamp( ucollect_status["last_check"]) ucollect_status["state_trans"] = self.SENDING_STATUS_TRANSLATION[ ucollect_status["state"]] kwargs["ucollect_status"] = ucollect_status kwargs["firewall_status"] = firewall_status if collect_data["agreed"]: handler = CollectionToggleHandler(bottle.request.POST.decode()) kwargs['collection_toggle_form'] = handler.form kwargs['agreed'] = collect_data["agreed"] else: email = bottle.request.POST.decode().get( "email", bottle.request.GET.decode().get("email", "")) handler = RegistrationCheckHandler({"email": email}) kwargs['registration_check_form'] = handler.form return self.default_template(form=self.form, title=self.userfriendly_title, description=None, status=status, **kwargs) 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_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 call_action(self, action): if action == "check_registration": return self._action_check_registration() elif action == "toggle_collecting": return self._action_toggle_collecting() raise bottle.HTTPError(404, "Unknown action")
class UnifiedTimeHandler(BaseConfigHandler): """ Setting of the region information and time """ userfriendly_title = gettext("Region and time") def __init__(self, *args, **kwargs): self.backend_data = current_state.backend.perform( "time", "get_settings") super().__init__(*args, **kwargs) def get_form(self): data = copy.deepcopy(self.backend_data) data["zonename"] = "%s/%s" % (data["region"], data["city"]) data["how_to_set_time"] = data["time_settings"]["how_to_set_time"] formatted_date = datetime.strptime( data["time_settings"]["time"], "%Y-%m-%dT%H:%M:%S.%f").strftime("%Y-%m-%d %H:%M:%S") data["time"] = formatted_date data["ntp_time"] = formatted_date if self.data: # update from post data.update(self.data) if bottle.request.is_xhr: # xhr won't update the settings, so use current time to update it data["time"] = formatted_date data["ntp_time"] = formatted_date region_and_time_form = fapi.ForisForm("region_and_time", data) # section just for common description main_section = region_and_time_form.add_section( name="region_and_time", title=_(self.userfriendly_title), description= _("It is important for your device to have the correct time set. " "If your device's time is delayed, the procedure of SSL certificate verification " "might not work correctly.")) # region section region_section = main_section.add_section( name="timezone", title=_("Region settings"), description=_( "Please select the timezone the router is being operated in. " "Correct setting is required to display the right time and for related functions." )) lang = current_state.language def construct_args(items, translation_function=_, key_getter=lambda x: x): """ Helper function that builds args for country/timezone dropdowns. If there's only one item, dropdown should contain only that item. Otherwise the list of items should be prepended by an empty value. :param items: list of filtered TZ data :param translation_function: function that returns displayed choice from TZ data :param key_getter: :return: list of args """ args = localized_sorted( ((key_getter(x), translation_function(x)) for x in items), lang=lang, key=lambda x: x[1]) if len(args) > 1: return [(None, "-" * 16)] + args return args regions = localized_sorted(((x, _(x)) for x in tzinfo.regions), lang=lang, key=lambda x: x[1]) region_section.add_field(Dropdown, name="region", label=_("Continent or ocean"), required=True, args=regions) # Get region and offer available countries region = region_and_time_form.current_data.get('region') countries = construct_args(tzinfo.countries_in_region(region), lambda x: _(tzinfo.countries[x])) region_section.add_field( Dropdown, name="country", label=_("Country"), required=True, default=tzinfo.get_country_for_tz(data["zonename"]), args=countries, ).requires("region") # Get country and offer available timezones country = region_and_time_form.current_data.get( "country", countries[0][0]) # It's possible that data contain country from the previous request, # in that case fall back to the first item in list of available countries if country not in (x[0] for x in countries): country = countries[0][0] timezones = construct_args(tzinfo.timezones_in_region_and_country( region, country), translation_function=lambda x: _(x[2]), key_getter=lambda x: x[0]) # Offer timezones - but only if a country is selected and is not None (ensured by the # requires() method) region_section.add_field(Dropdown, name="zonename", label=_("Timezone"), required=True, default=data["zonename"], args=timezones).requires( "country", lambda x: country and x is not None) # time section time_section = main_section.add_section( name="time", title=_("Time settings"), description= _("Time should be up-to-date otherwise DNS and other services might not work properly." )) time_section.add_field( Dropdown, name="how_to_set_time", label=_("How to set time"), description=_( "Choose method to store current time into the router."), default="ntp", args=( ("ntp", _("via ntp")), ("manual", _("manually")), )) time_section.add_field( Textbox, name="time", validators=validators.Datetime(), label=_("Time"), hint=_("Time in YYYY-MM-DD HH:MM:SS format."), ).requires("how_to_set_time", "manual") time_section.add_field( Textbox, name="ntp_time", label=_("Time"), ).requires("how_to_set_time", "ntp") def region_form_cb(data): region, city = data["zonename"].split("/") msg = { "city": city, "region": region, "timezone": tzinfo.get_zoneinfo_for_tz(data["zonename"]), "time_settings": { "how_to_set_time": data["how_to_set_time"], } } if data["how_to_set_time"] == "manual": msg["time_settings"]["time"] = datetime.strptime( data["time"], "%Y-%m-%d %H:%M:%S").replace(microsecond=1).isoformat() res = current_state.backend.perform("time", "update_settings", msg) return "save_result", res # store {"result": ...} to be used later... region_and_time_form.add_callback(region_form_cb) return region_and_time_form