{"repo": "anjomro/headscale-webui", "files": {"renderer.py": {"7ab508f071fe9040ea9133020601197ffb8e4d5d": {"t": 1785595757.0604842, "n": 25065, "h": "9b4a89290f8434d9", "c": "import headscale, helper, json, logging, sys, pytz, os, time, yaml\nfrom flask import Markup, render_template, Flask\nfrom datetime import datetime, timedelta, date\nfrom dateutil import parser\n\n# Threading to speed things up\nfrom concurrent.futures import wait, ALL_COMPLETED\nfrom flask_executor import Executor\n\napp = Flask(__name__)\nexecutor = Executor(app)\n\nlog = logging.getLogger('server.renderer')\n\ndef render_overview():\n url = headscale.get_url()\n api_key = headscale.get_api_key()\n\n timezone = pytz.timezone(os.environ[\"TZ\"] if os.environ[\"TZ\"] else \"UTC\")\n local_time = timezone.localize(datetime.now())\n \n # Overview page will just read static information from the config file and display it\n # Open the config.yaml and parse it.\n config_file = open(\"/etc/headscale/config.yaml\", \"r\")\n config_yaml = yaml.safe_load(config_file)\n\n # Get and display the following information:\n # Overview of the server's machines, users, preauth keys, API key expiration, server version\n \n # Get all machines:\n machines_count = 0\n machines = headscale.get_machines(url, api_key)\n for machine in machines[\"machines\"]:\n machines_count += 1\n\n # Get all routes:\n routes = headscale.get_routes(url,api_key)\n total_routes = len(routes[\"routes\"])\n enabled_routes = 0\n for route in routes[\"routes\"]:\n if route[\"enabled\"] and route['advertised']: \n enabled_routes += 1\n\n # Get a count of all enabled exit routes\n exits_count = 0\n exits_enabled_count = 0\n for route in routes[\"routes\"]:\n if route['advertised']:\n if route[\"prefix\"] == \"0.0.0.0/0\" or route[\"prefix\"] == \"::/0\":\n exits_count +=1\n if route[\"enabled\"]:\n exits_enabled_count += 1\n\n # Get User and PreAuth Key counts\n user_count = 0\n usable_keys_count = 0\n users = headscale.get_users(url, api_key)\n for user in users[\"users\"]:\n user_count +=1\n preauth_keys = headscale.get_preauth_keys(url, api_key, user[\"name\"])\n for key in preauth_keys[\"preAuthKeys\"]:\n expiration_parse = parser.parse(key[\"expiration\"])\n key_expired = True if expiration_parse < local_time else False\n if key[\"reusable\"] and not key_expired: usable_keys_count += 1\n if not key[\"reusable\"] and not key[\"used\"] and not key_expired: usable_keys_count += 1\n\n overview_content = \"\"\"\n
\n
\n
\n Stats\n

\n \n \n \n \n \n \n
Machines Added \"\"\"+ str(machines_count) +\"\"\"
Users \"\"\"+ str(user_count) +\"\"\"
Usable PreAuth Keys \"\"\"+ str(usable_keys_count) +\"\"\"
Enabled/Total Routes \"\"\"+ str(enabled_routes) +\"\"\"/\"\"\"+str(total_routes)+\"\"\"
Enabled/Total Exits \"\"\"+ str(exits_enabled_count) +\"\"\"/\"\"\"+str(exits_count)+\"\"\"
\n

\n
\n
\n
\n \"\"\"\n # Overview of general configs from the YAML\n general_content = \"\"\"\n
\n
\n
\n General\n

\n \n \n \n \n \n \n
IP Prefixes \"\"\"+str(config_yaml[\"ip_prefixes\"]) +\"\"\"
Server URL \"\"\"+str(config_yaml[\"server_url\"]) +\"\"\"
Updates Disabled? \"\"\"+str(config_yaml[\"disable_check_updates\"]) +\"\"\"
Ephemeral Node Timeout \"\"\"+str(config_yaml[\"ephemeral_node_inactivity_timeout\"]) +\"\"\"
Node Update Check Interval \"\"\"+str(config_yaml[\"node_update_check_interval\"]) +\"\"\"
\n

\n
\n
\n
\n \"\"\"\n\n # Whether OIDC is configured\n if config_yaml[\"oidc\"]:\n oidc_content = \"\"\"\n
\n
\n
\n OIDC\n

\n \n \n \n \n \n \n
Issuer \"\"\"+str(config_yaml[\"oidc\"][\"issuer\"]) +\"\"\"
Client ID \"\"\"+str(config_yaml[\"oidc\"][\"client_id\"]) +\"\"\"
Scope \"\"\"+str(config_yaml[\"oidc\"][\"scope\"]) +\"\"\"
Allowed Domains \"\"\"+str(config_yaml[\"oidc\"][\"allowed_domains\"]) +\"\"\"
Strip Email Domain \"\"\"+str(config_yaml[\"oidc\"][\"strip_email_domain\"])+\"\"\"
\n

\n
\n
\n
\n \"\"\"\n\n if config_yaml[\"derp\"][\"server\"]:\n derp_content = \"\"\"\n
\n
\n
\n Built-in DERP\n

\n \n \n \n \n \n \n
Enabled \"\"\"+str(config_yaml[\"derp\"][\"server\"][\"enabled\"]) +\"\"\"
Region ID \"\"\"+str(config_yaml[\"derp\"][\"server\"][\"region_id\"]) +\"\"\"
Region Code \"\"\"+str(config_yaml[\"derp\"][\"server\"][\"region_code\"]) +\"\"\"
Region Name \"\"\"+str(config_yaml[\"derp\"][\"server\"][\"region_name\"]) +\"\"\"
STUN Address \"\"\"+str(config_yaml[\"derp\"][\"server\"][\"stun_listen_addr\"]) +\"\"\"
\n

\n
\n
\n
\n \"\"\"\n\n # Whether there are custom DERP servers\n # If there are custom DERP servers, get the file location from the config file. Assume mapping is the same.\n # Whether the built-in DERP server is enabled \n # The IP prefixes\n # The DNS config\n if config_yaml[\"dns_config\"]:\n dns_content = \"\"\"\n
\n
\n
\n DNS\n

\n \n \n \n \n \n \n
Nameservers \"\"\"+str(config_yaml[\"dns_config\"][\"nameservers\"])+\"\"\"
MagicDNS \"\"\"+str(config_yaml[\"dns_config\"][\"magic_dns\"]) +\"\"\"
Domains \"\"\"+str(config_yaml[\"dns_config\"][\"domains\"]) +\"\"\"
Base Domain \"\"\"+str(config_yaml[\"dns_config\"][\"base_domain\"])+\"\"\"

\n

\n
\n
\n
\n \"\"\"\n if config_yaml[\"derp\"][\"paths\"]: pass\n # # open the path:\n # derp_file = \n # config_file = open(\"/etc/headscale/config.yaml\", \"r\")\n # config_yaml = yaml.safe_load(config_file)\n # The ACME config, if not empty\n # Whether updates are running\n # Whether metrics are enabled (and their listen addr)\n # The log level\n # What kind of Database is being used to drive headscale\n\n content = \"
\" + overview_content + general_content + derp_content + oidc_content + dns_content + \"
\"\n return Markup(content)\n\ndef thread_machine_content(machine, machine_content, idx):\n # machine = passed in machine information\n # content = place to write the content\n\n url = headscale.get_url()\n api_key = headscale.get_api_key()\n\n # Set the current timezone and local time\n timezone = pytz.timezone(os.environ[\"TZ\"] if os.environ[\"TZ\"] else \"UTC\")\n local_time = timezone.localize(datetime.now())\n\n # Get the machines routes\n pulled_routes = headscale.get_machine_routes(url, api_key, machine[\"id\"])\n routes = \"\"\n\n# New format to parse:\n# New JSON endpoint requires a machine_id and a route_id to toggle.\n# Pass the following info:\n# 1. Machine ID\n# 2. Route ID (NEW REQ)\n# 3. Route State (NEW REQ) - This would be the JSON key of pulled_routes[\"routes\"][index][enabled]\n# {\n# \"routes\": [\n# { \n# \"id\": \"1\",\n# \"prefix\": \"0.0.0.0/0\",\n# \"advertised\": true,\n# \"enabled\": true,\n# },\n\n # Test if the machine is an exit node:\n exit_node = False\n # If the LENGTH of \"routes\" is NULL/0, there are no routes, enabled or disabled:\n if len(pulled_routes[\"routes\"]) > 0:\n advertised_and_enabled = False\n advertised_route = False\n # First, check if there are any routes that are both enabled and advertised\n for route in pulled_routes[\"routes\"]:\n if route [\"advertised\"] and route[\"enabled\"]: \n advertised_and_enabled = True\n if route[\"advertised\"]:\n advertised_route = True\n if advertised_and_enabled or advertised_route:\n routes = \"\"\"\n
  • \n directions\n Routes\n

    \n \"\"\"\n for route in pulled_routes[\"routes\"]:\n # log.info(\"Route: [\"+str(route['machine']['name'])+\"] id: \"+str(route['id'])+\" / prefix: \"+str(route['prefix'])+\" enabled?: \"+str(route['enabled']))\n # Check if the route is enabled:\n route_enabled = \"red\"\n route_tooltip = 'enable'\n if route[\"enabled\"]:\n route_enabled = \"green\"\n route_tooltip = 'disable'\n if route[\"prefix\"] == \"0.0.0.0/0\" or route[\"prefix\"] == \"::/0\" and str(route[\"enabled\"]) == \"True\":\n exit_node = True\n routes = routes+\"\"\"\n

    \n \"\"\"+route['prefix']+\"\"\"\n

    \n \"\"\"\n routes = routes+\"

  • \"\n# This entire thing will probably need to change after the new API endpoint change.\n# Old code for v1.17.0\n# # Test if the machine is an exit node:\n# exit_node = False\n# # If there are ANY advertised routes, print them:\n# if len(pulled_routes[\"routes\"][\"advertisedRoutes\"]) > 0:\n# routes = \"\"\"\n#
  • \n# directions\n# Routes\n#

    \n# \"\"\"\n# for advertised_route in pulled_routes[\"routes\"][\"advertisedRoutes\"]:\n# # Check if the route has been enabled. Red for False, Green for True \n# route_enabled = \"red\"\n# route_tooltip = 'enable'\n# for enabled_route in pulled_routes[\"routes\"][\"enabledRoutes\"]:\n# if advertised_route == enabled_route: \n# route_enabled = \"green\"\n# route_tooltip = 'disable'\n# if (advertised_route == \"0.0.0.0/0\" or advertised_route == \"::/0\") and route_enabled == \"green\": \n# exit_node = True\n# routes = routes+\"\"\"\n#

    \n# \"\"\"+advertised_route+\"\"\"\n#

    \n# \"\"\"\n# routes = routes+\"

  • \"\n\n # Get machine tags\n tag_array = \"\"\n for tag in machine[\"forcedTags\"]: tag_array = tag_array+\"{tag: '\"+tag[4:]+\"'}, \"\n tags = \"\"\"\n
  • \n label\n Tags\n

    \n
  • \n \n \"\"\"\n\n # Get the machine IP's\n machine_ips = \"\"\n\n # Format the dates for easy readability\n last_seen_parse = parser.parse(machine[\"lastSeen\"])\n last_seen_local = last_seen_parse.astimezone(timezone)\n last_seen_delta = local_time - last_seen_local\n last_seen_print = helper.pretty_print_duration(last_seen_delta)\n last_seen_time = str(last_seen_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+\" \"+str(timezone)+\" (\"+str(last_seen_print)+\")\"\n \n last_update_parse = local_time if machine[\"lastSuccessfulUpdate\"] is None else parser.parse(machine[\"lastSuccessfulUpdate\"])\n last_update_local = last_update_parse.astimezone(timezone)\n last_update_delta = local_time - last_update_local\n last_update_print = helper.pretty_print_duration(last_update_delta)\n last_update_time = str(last_update_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+\" \"+str(timezone)+\" (\"+str(last_update_print)+\")\"\n\n created_parse = parser.parse(machine[\"createdAt\"])\n created_local = created_parse.astimezone(timezone)\n created_delta = local_time - created_local\n created_print = helper.pretty_print_duration(created_delta)\n created_time = str(created_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+\" \"+str(timezone)+\" (\"+str(created_print)+\")\"\n\n # Get the first 10 characters of the PreAuth Key:\n if machine[\"preAuthKey\"]:\n preauth_key = str(machine[\"preAuthKey\"][\"key\"])[0:10]\n else: preauth_key = \"None\"\n\n # Set the status badge color:\n text_color = helper.text_color_duration(last_seen_delta)\n # Set the user badge color:\n user_color = helper.get_color(int(machine[\"user\"][\"id\"]))\n\n # Generate the various badges:\n status_badge = \"fiber_manual_record\"\n user_badge = \"\"+machine[\"user\"][\"name\"]+\"\"\n exit_node_badge = \"\" if not exit_node else \"Exit Node\"\n\n\n machine_content[idx] = (str(render_template(\n 'machines_card.html', \n given_name = machine[\"givenName\"],\n machine_id = machine[\"id\"],\n hostname = machine[\"name\"],\n ns_name = machine[\"user\"][\"name\"],\n ns_id = machine[\"user\"][\"id\"],\n ns_created = machine[\"user\"][\"createdAt\"],\n last_seen = str(last_seen_print),\n last_update = str(last_update_print),\n machine_ips = Markup(machine_ips),\n advertised_routes = Markup(routes),\n exit_node_badge = Markup(exit_node_badge),\n status_badge = Markup(status_badge),\n user_badge = Markup(user_badge),\n last_update_time = str(last_update_time),\n last_seen_time = str(last_seen_time),\n created_time = str(created_time),\n preauth_key = str(preauth_key),\n machine_tags = Markup(tags),\n )))\n log.info(\"Finished thread for machine \"+machine[\"givenName\"]+\" index \"+str(idx))\n\n# Render the cards for the machines page:\ndef render_machines_cards():\n url = headscale.get_url()\n api_key = headscale.get_api_key()\n machines_list = headscale.get_machines(url, api_key)\n\n #########################################\n # Thread this entire thing. \n numThreads = len(machines_list[\"machines\"])\n iterable = []\n machine_content = {}\n for i in range (0, numThreads):\n log.info(\"Appending iterable: \"+str(i))\n iterable.append(i)\n # Flask-Executor Method:\n log.info(\"Starting futures\")\n futures = [executor.submit(thread_machine_content, machines_list[\"machines\"][idx], machine_content, idx) for idx in iterable]\n # Wait for the executor to finish all jobs:\n wait(futures, return_when=ALL_COMPLETED)\n log.info(\"Finished futures\")\n\n # DEBUG: Do in a forloop:\n # for idx in iterable: thread_machine_content(machines_list[\"machines\"][idx], machine_content, idx)\n\n # Sort the content by machine_id:\n sorted_machines = {key: val for key, val in sorted(machine_content.items(), key = lambda ele: ele[0])}\n\n content = \"
    \"\n # Print the content\n\n for index in range(0, numThreads):\n content = content+str(sorted_machines[index])\n # content = content+str(sorted_machines[index])\n\n content = content+\"
    \"\n\n return Markup(content)\n\n# Render the cards for the Users page:\ndef render_users_cards():\n url = headscale.get_url()\n api_key = headscale.get_api_key()\n user_list = headscale.get_users(url, api_key)\n\n # Set the current timezone and local time\n timezone = pytz.timezone(os.environ[\"TZ\"] if os.environ[\"TZ\"] else \"UTC\")\n local_time = timezone.localize(datetime.now())\n\n content = \"
    \"\n for user in user_list[\"users\"]:\n # Get all preAuth Keys in the user, only display if one exists:\n preauth_keys_collection = build_preauth_key_table(user[\"name\"])\n\n # Set the user badge color:\n user_color = helper.get_color(int(user[\"id\"]), \"text\")\n\n # Generate the various badges:\n status_badge = \"fiber_manual_record\"\n\n content = content + render_template(\n 'users_card.html', \n status_badge = Markup(status_badge),\n user_name = user[\"name\"],\n user_id = user[\"id\"],\n preauth_keys_collection = Markup(preauth_keys_collection)\n ) \n content = content+\"
    \"\n return Markup(content)\n\n# Builds the preauth key table for the User page\ndef build_preauth_key_table(user_name):\n url = headscale.get_url()\n api_key = headscale.get_api_key()\n\n preauth_keys = headscale.get_preauth_keys(url, api_key, user_name)\n preauth_keys_collection = \"\"\"
  • \n Toggle Expired\n Add PreAuth Key\n vpn_key\n PreAuth Keys\n \"\"\"\n if len(preauth_keys[\"preAuthKeys\"]) == 0: preauth_keys_collection += \"

    No keys defined for this user

    \"\n if len(preauth_keys[\"preAuthKeys\"]) > 0:\n preauth_keys_collection += \"\"\"\n \n \n \n \n \n \n \n \n \n \n \n \n \"\"\"\n for key in preauth_keys[\"preAuthKeys\"]:\n # Get the key expiration date and compare it to now to check if it's expired:\n # Set the current timezone and local time\n timezone = pytz.timezone(os.environ[\"TZ\"] if os.environ[\"TZ\"] else \"UTC\")\n local_time = timezone.localize(datetime.now())\n expiration_parse = parser.parse(key[\"expiration\"])\n key_expired = True if expiration_parse < local_time else False\n expiration_time = str(expiration_parse.strftime('%A %m/%d/%Y, %H:%M:%S'))+\" \"+str(timezone)\n\n # Class for the javascript function to look for to toggle the hide function\n hide_expired = \"expired-row\" if key_expired else \"\"\n\n key_usable = False\n if key[\"reusable\"] and not key_expired: key_usable = True\n if not key[\"reusable\"] and not key[\"used\"] and not key_expired: key_usable = True\n\n tooltip_expired = \"Expiration: \"+expiration_time\n\n btn_reusable = \"fiber_manual_record\" if key[\"reusable\"] else \"\"\n btn_ephemeral = \"fiber_manual_record\" if key[\"ephemeral\"] else \"\"\n btn_used = \"fiber_manual_record\" if key[\"used\"] else \"\"\n btn_usable = \"fiber_manual_record\" if key_usable else \"\"\n\n # Other buttons:\n btn_delete = \"Expire\" if not key_expired else \"\"\n tooltip_data = \"Expiration: \"+expiration_time\n\n # TR ID will look like \"1-albert-tr\"\n preauth_keys_collection = preauth_keys_collection+\"\"\"\n \n \n \n \n \n \n \n \n \n \"\"\"\n\n preauth_keys_collection = preauth_keys_collection+\"\"\"
    IDKey
    Reusable
    Used
    Ephemeral
    Usable
    Actions
    \"\"\"+str(key[\"id\"])+\"\"\"\"\"\"+str(key[\"key\"])+\"\"\"
    \"\"\"+btn_reusable+\"\"\"
    \"\"\"+btn_used+\"\"\"
    \"\"\"+btn_ephemeral+\"\"\"
    \"\"\"+btn_usable+\"\"\"
    \"\"\"+btn_delete+\"\"\"
    \n
  • \n \"\"\"\n return preauth_keys_collection"}}}}