2023-02-20 17:23:05 +09:00
# pylint: disable=line-too-long, wrong-import-order
2023-02-17 22:41:41 +09:00
2023-02-28 14:26:46 +09:00
import headscale , helper , pytz , os , yaml , logging
2023-02-28 13:29:31 +09:00
from flask import Flask , Markup , render_template
2023-02-16 10:41:46 +09:00
from datetime import datetime
from dateutil import parser
2023-02-19 20:20:55 +09:00
from concurrent . futures import ALL_COMPLETED , wait
2023-02-16 10:41:46 +09:00
from flask_executor import Executor
2023-02-06 04:58:09 +00:00
2023-02-28 14:24:46 +09:00
LOG_LEVEL = os . environ [ " LOG_LEVEL " ] . replace ( ' " ' , ' ' ) . upper ( )
# Initiate the Flask application and logging:
2023-02-27 22:21:51 +09:00
app = Flask ( __name__ , static_url_path = " /static " )
2023-02-28 14:24:46 +09:00
match LOG_LEVEL :
case " DEBUG " : app . logger . setLevel ( logging . DEBUG )
case " INFO " : app . logger . setLevel ( logging . INFO )
case " WARNING " : app . logger . setLevel ( logging . WARNING )
case " ERROR " : app . logger . setLevel ( logging . ERROR )
case " CRITICAL " : app . logger . setLevel ( logging . CRITICAL )
2023-02-06 04:58:09 +00:00
executor = Executor ( app )
2023-02-19 18:43:49 +09:00
2023-02-06 04:58:09 +00:00
def render_overview ( ) :
2023-02-28 12:43:09 +09:00
app . logger . info ( " Rendering the Overview page " )
2023-02-06 04:58:09 +00:00
url = headscale . get_url ( )
api_key = headscale . get_api_key ( )
timezone = pytz . timezone ( os . environ [ " TZ " ] if os . environ [ " TZ " ] else " UTC " )
local_time = timezone . localize ( datetime . now ( ) )
# Overview page will just read static information from the config file and display it
# Open the config.yaml and parse it.
2023-02-15 19:53:46 +09:00
config_file = " "
2023-02-28 12:43:09 +09:00
try :
config_file = open ( " /etc/headscale/config.yml " , " r " )
app . logger . info ( " Opening /etc/headscale/config.yml " )
except :
config_file = open ( " /etc/headscale/config.yaml " , " r " )
app . logger . info ( " Opening /etc/headscale/config.yaml " )
2023-02-06 04:58:09 +00:00
config_yaml = yaml . safe_load ( config_file )
# Get and display the following information:
# Overview of the server's machines, users, preauth keys, API key expiration, server version
# Get all machines:
machines = headscale . get_machines ( url , api_key )
2023-02-20 17:23:05 +09:00
machines_count = len ( machines [ " machines " ] )
2023-02-06 04:58:09 +00:00
2023-03-03 13:05:15 +09:00
# Need to check if routes are attached to an active machine:
# ISSUE: https://github.com/iFargle/headscale-webui/issues/36
# ISSUE: https://github.com/juanfont/headscale/issues/1228
2023-02-06 04:58:09 +00:00
# Get all routes:
routes = headscale . get_routes ( url , api_key )
2023-03-03 13:23:10 +09:00
total_routes = 0
2023-03-03 13:09:51 +09:00
for route in routes [ " routes " ] :
if int ( route [ ' machine ' ] [ ' id ' ] ) != 0 :
total_routes + = 1
2023-02-06 04:58:09 +00:00
enabled_routes = 0
for route in routes [ " routes " ] :
2023-03-03 13:09:51 +09:00
if route [ " enabled " ] and route [ ' advertised ' ] and int ( route [ ' machine ' ] [ ' id ' ] ) != 0 :
2023-02-06 04:58:09 +00:00
enabled_routes + = 1
# Get a count of all enabled exit routes
exits_count = 0
exits_enabled_count = 0
for route in routes [ " routes " ] :
2023-03-03 13:09:51 +09:00
if route [ ' advertised ' ] and int ( route [ ' machine ' ] [ ' id ' ] ) != 0 :
2023-02-06 04:58:09 +00:00
if route [ " prefix " ] == " 0.0.0.0/0 " or route [ " prefix " ] == " ::/0 " :
exits_count + = 1
if route [ " enabled " ] :
exits_enabled_count + = 1
# Get User and PreAuth Key counts
2023-03-03 13:05:15 +09:00
user_count = 0
2023-02-06 04:58:09 +00:00
usable_keys_count = 0
users = headscale . get_users ( url , api_key )
for user in users [ " users " ] :
user_count + = 1
preauth_keys = headscale . get_preauth_keys ( url , api_key , user [ " name " ] )
for key in preauth_keys [ " preAuthKeys " ] :
expiration_parse = parser . parse ( key [ " expiration " ] )
key_expired = True if expiration_parse < local_time else False
if key [ " reusable " ] and not key_expired : usable_keys_count + = 1
if not key [ " reusable " ] and not key [ " used " ] and not key_expired : usable_keys_count + = 1
2023-02-23 14:29:35 +09:00
# General Content variables:
2023-02-23 14:35:45 +09:00
ip_prefixes , server_url , disable_check_updates , ephemeral_node_inactivity_timeout , node_update_check_interval = " N/A " , " N/A " , " N/A " , " N/A " , " N/A "
2023-02-23 14:29:35 +09:00
if " ip_prefixes " in config_yaml : ip_prefixes = str ( config_yaml [ " ip_prefixes " ] )
if " server_url " in config_yaml : server_url = str ( config_yaml [ " server_url " ] )
if " disable_check_updates " in config_yaml : disable_check_updates = str ( config_yaml [ " disable_check_updates " ] )
if " ephemeral_node_inactivity_timeout " in config_yaml : ephemeral_node_inactivity_timeout = str ( config_yaml [ " ephemeral_node_inactivity_timeout " ] )
if " node_update_check_interval " in config_yaml : node_update_check_interval = str ( config_yaml [ " node_update_check_interval " ] )
# OIDC Content variables:
2023-02-23 14:35:45 +09:00
issuer , client_id , scope , use_expiry_from_token , expiry = " N/A " , " N/A " , " N/A " , " N/A " , " N/A "
2023-02-23 14:29:35 +09:00
if " oidc " in config_yaml :
if " issuer " in config_yaml [ " oidc " ] : issuer = str ( config_yaml [ " oidc " ] [ " issuer " ] )
if " client_id " in config_yaml [ " oidc " ] : client_id = str ( config_yaml [ " oidc " ] [ " client_id " ] )
if " scope " in config_yaml [ " oidc " ] : scope = str ( config_yaml [ " oidc " ] [ " scope " ] )
if " use_expiry_from_token " in config_yaml [ " oidc " ] : use_expiry_from_token = str ( config_yaml [ " oidc " ] [ " use_expiry_from_token " ] )
if " expiry " in config_yaml [ " oidc " ] : expiry = str ( config_yaml [ " oidc " ] [ " expiry " ] )
# Embedded DERP server information.
2023-02-23 14:35:45 +09:00
enabled , region_id , region_code , region_name , stun_listen_addr = " N/A " , " N/A " , " N/A " , " N/A " , " N/A "
2023-02-23 14:29:35 +09:00
if " derp " in config_yaml :
2023-03-21 11:19:17 +09:00
if " server " in config_yaml [ " derp " ] and config_yaml [ " derp " ] [ " server " ] [ " enabled " ] :
2023-02-23 14:29:35 +09:00
if " enabled " in config_yaml [ " derp " ] [ " server " ] : enabled = str ( config_yaml [ " derp " ] [ " server " ] [ " enabled " ] )
if " region_id " in config_yaml [ " derp " ] [ " server " ] : region_id = str ( config_yaml [ " derp " ] [ " server " ] [ " region_id " ] )
if " region_code " in config_yaml [ " derp " ] [ " server " ] : region_code = str ( config_yaml [ " derp " ] [ " server " ] [ " region_code " ] )
if " region_name " in config_yaml [ " derp " ] [ " server " ] : region_name = str ( config_yaml [ " derp " ] [ " server " ] [ " region_name " ] )
if " stun_listen_addr " in config_yaml [ " derp " ] [ " server " ] : stun_listen_addr = str ( config_yaml [ " derp " ] [ " server " ] [ " stun_listen_addr " ] )
2023-02-23 14:35:45 +09:00
nameservers , magic_dns , domains , base_domain = " N/A " , " N/A " , " N/A " , " N/A "
2023-02-23 14:29:35 +09:00
if " dns_config " in config_yaml :
if " nameservers " in config_yaml [ " dns_config " ] : nameservers = str ( config_yaml [ " dns_config " ] [ " nameservers " ] )
if " magic_dns " in config_yaml [ " dns_config " ] : magic_dns = str ( config_yaml [ " dns_config " ] [ " magic_dns " ] )
if " domains " in config_yaml [ " dns_config " ] : domains = str ( config_yaml [ " dns_config " ] [ " domains " ] )
if " base_domain " in config_yaml [ " dns_config " ] : base_domain = str ( config_yaml [ " dns_config " ] [ " base_domain " ] )
# Start putting the content together
overview_content = """
2023-02-23 15:00:16 +09:00
< div class = " row " >
2023-02-23 18:05:56 +09:00
< div class = " col s1 " > < / div >
< div class = " col s10 " >
2023-02-23 15:07:58 +09:00
< ul class = " collection with-header z-depth-1 " >
2023-02-23 14:56:57 +09:00
< li class = " collection-header " > < h4 > Server Statistics < / h4 > < / li >
< li class = " collection-item " > < div > Machines Added < div class = " secondary-content overview-page " > """ + str(machines_count) + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Users Added < div class = " secondary-content overview-page " > """ + str(user_count) + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Usable Preauth Keys < div class = " secondary-content overview-page " > """ + str(usable_keys_count) + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Enabled / Total Routes < div class = " secondary-content overview-page " > """ + str(enabled_routes) + """ / """ +str(total_routes) + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Enabled / Total Exits < div class = " secondary-content overview-page " > """ + str(exits_enabled_count) + """ / """ +str(exits_count)+ """ < / div > < / div > < / li >
< / ul >
< / div >
2023-02-23 18:05:56 +09:00
< div class = " col s1 " > < / div >
2023-02-23 15:00:16 +09:00
< / div >
2023-02-23 14:29:35 +09:00
"""
general_content = """
2023-02-23 15:00:16 +09:00
< div class = " row " >
2023-02-23 18:05:56 +09:00
< div class = " col s1 " > < / div >
< div class = " col s10 " >
2023-02-23 15:07:58 +09:00
< ul class = " collection with-header z-depth-1 " >
2023-02-23 15:20:43 +09:00
< li class = " collection-header " > < h4 > General < / h4 > < / li >
2023-02-23 14:56:57 +09:00
< li class = " collection-item " > < div > IP Prefixes < div class = " secondary-content overview-page " > """ + ip_prefixes + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Server URL < div class = " secondary-content overview-page " > """ + server_url + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Updates Disabled < div class = " secondary-content overview-page " > """ + disable_check_updates + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Ephemeral Node Inactivity Timeout < div class = " secondary-content overview-page " > """ + ephemeral_node_inactivity_timeout + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Node Update Check Interval < div class = " secondary-content overview-page " > """ + node_update_check_interval + """ < / div > < / div > < / li >
< / ul >
< / div >
2023-02-23 18:05:56 +09:00
< div class = " col s1 " > < / div >
2023-02-23 15:00:16 +09:00
< / div >
2023-02-06 04:58:09 +00:00
"""
2023-02-23 14:29:35 +09:00
oidc_content = """
2023-02-23 15:00:16 +09:00
< div class = " row " >
2023-02-23 18:05:56 +09:00
< div class = " col s1 " > < / div >
< div class = " col s10 " >
2023-02-23 15:07:58 +09:00
< ul class = " collection with-header z-depth-1 " >
2023-02-23 15:20:43 +09:00
< li class = " collection-header " > < h4 > Headscale OIDC < / h4 > < / li >
2023-02-23 14:56:57 +09:00
< li class = " collection-item " > < div > Issuer < div class = " secondary-content overview-page " > """ + issuer + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Client ID < div class = " secondary-content overview-page " > """ + client_id + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Scope < div class = " secondary-content overview-page " > """ + scope + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Use OIDC Token Expiry < div class = " secondary-content overview-page " > """ + use_expiry_from_token + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Expiry < div class = " secondary-content overview-page " > """ + expiry + """ < / div > < / div > < / li >
< / ul >
< / div >
2023-02-23 18:05:56 +09:00
< div class = " col s1 " > < / div >
2023-02-23 15:00:16 +09:00
< / div >
2023-02-23 14:29:35 +09:00
"""
derp_content = """
2023-02-23 15:00:16 +09:00
< div class = " row " >
2023-02-23 18:05:56 +09:00
< div class = " col s1 " > < / div >
< div class = " col s10 " >
2023-02-23 15:07:58 +09:00
< ul class = " collection with-header z-depth-1 " >
2023-02-23 15:20:43 +09:00
< li class = " collection-header " > < h4 > Embedded DERP < / h4 > < / li >
< li class = " collection-item " > < div > Enabled < div class = " secondary-content overview-page " > """ + enabled + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Region ID < div class = " secondary-content overview-page " > """ + region_id + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Region Code < div class = " secondary-content overview-page " > """ + region_code + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Region Name < div class = " secondary-content overview-page " > """ + region_name + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > STUN Address < div class = " secondary-content overview-page " > """ + stun_listen_addr + """ < / div > < / div > < / li >
2023-02-23 14:56:57 +09:00
< / ul >
< / div >
2023-02-23 18:05:56 +09:00
< div class = " col s1 " > < / div >
2023-02-23 15:00:16 +09:00
< / div >
2023-02-23 14:29:35 +09:00
"""
dns_content = """
2023-02-23 15:00:16 +09:00
< div class = " row " >
2023-02-23 18:05:56 +09:00
< div class = " col s1 " > < / div >
< div class = " col s10 " >
2023-02-23 15:07:58 +09:00
< ul class = " collection with-header z-depth-1 " >
2023-02-23 15:20:43 +09:00
< li class = " collection-header " > < h4 > DNS < / h4 > < / li >
2023-02-23 14:56:57 +09:00
< li class = " collection-item " > < div > DNS Nameservers < div class = " secondary-content overview-page " > """ + nameservers + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > MagicDNS < div class = " secondary-content overview-page " > """ + magic_dns + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Search Domains < div class = " secondary-content overview-page " > """ + domains + """ < / div > < / div > < / li >
< li class = " collection-item " > < div > Base Domain < div class = " secondary-content overview-page " > """ + base_domain + """ < / div > < / div > < / li >
< / ul >
< / div >
2023-02-23 18:05:56 +09:00
< div class = " col s1 " > < / div >
2023-02-23 15:00:16 +09:00
< / div >
2023-02-06 04:58:09 +00:00
"""
2023-02-23 14:29:35 +09:00
# Remove content that isn't needed:
# Remove OIDC if it isn't available:
if " oidc " not in config_yaml : oidc_content = " "
# Remove DERP if it isn't available or isn't enabled
2023-03-22 19:17:53 +09:00
if " derp " not in config_yaml : derp_content = " "
2023-02-15 19:53:46 +09:00
if " derp " in config_yaml :
2023-02-23 14:46:43 +09:00
if " server " in config_yaml [ " derp " ] :
2023-02-23 15:20:43 +09:00
if str ( config_yaml [ " derp " ] [ " server " ] [ " enabled " ] ) == " False " :
derp_content = " "
2023-02-06 04:58:09 +00:00
2023-02-15 19:53:46 +09:00
# TODO:
2023-02-06 04:58:09 +00:00
# Whether there are custom DERP servers
# If there are custom DERP servers, get the file location from the config file. Assume mapping is the same.
# Whether the built-in DERP server is enabled
# The IP prefixes
# The DNS config
2023-02-23 15:20:43 +09:00
2023-02-15 21:34:14 +09:00
if config_yaml [ " derp " ] [ " paths " ] : pass
2023-02-06 04:58:09 +00:00
# # open the path:
# derp_file =
# config_file = open("/etc/headscale/config.yaml", "r")
# config_yaml = yaml.safe_load(config_file)
# The ACME config, if not empty
# Whether updates are running
# Whether metrics are enabled (and their listen addr)
# The log level
# What kind of Database is being used to drive headscale
2023-02-23 14:52:34 +09:00
content = " <br> " + overview_content + general_content + derp_content + oidc_content + dns_content + " "
2023-02-06 04:58:09 +00:00
return Markup ( content )
2023-03-22 13:58:51 +09:00
def thread_machine_content ( machine , machine_content , idx , all_routes ) :
2023-02-06 04:58:09 +00:00
# machine = passed in machine information
# content = place to write the content
2023-03-17 20:38:04 +09:00
app . logger . debug ( " Machine Information " )
app . logger . debug ( str ( machine ) )
2023-02-06 04:58:09 +00:00
url = headscale . get_url ( )
api_key = headscale . get_api_key ( )
# Set the current timezone and local time
2023-03-17 20:38:04 +09:00
timezone = pytz . timezone ( os . environ [ " TZ " ] if os . environ [ " TZ " ] else " UTC " )
local_time = timezone . localize ( datetime . now ( ) )
2023-02-06 04:58:09 +00:00
# Get the machines routes
pulled_routes = headscale . get_machine_routes ( url , api_key , machine [ " id " ] )
routes = " "
# Test if the machine is an exit node:
2023-03-22 19:17:53 +09:00
exit_route_found = False
2023-03-22 20:56:30 +09:00
exit_route_enabled = False
2023-03-22 21:32:34 +09:00
# If the device has enabled Failover routes (High Availability routes)
2023-03-22 21:49:22 +09:00
ha_enabled = False
2023-03-22 21:32:34 +09:00
2023-03-22 18:57:56 +09:00
# If the length of "routes" is NULL/0, there are no routes, enabled or disabled:
2023-02-06 04:58:09 +00:00
if len ( pulled_routes [ " routes " ] ) > 0 :
2023-03-22 19:17:53 +09:00
advertised_routes = False
2023-03-22 13:16:32 +09:00
2023-02-06 04:58:09 +00:00
# First, check if there are any routes that are both enabled and advertised
2023-03-22 19:17:53 +09:00
# If that is true, we will output the collection-item for routes. Otherwise, it will not be displayed.
2023-02-06 04:58:09 +00:00
for route in pulled_routes [ " routes " ] :
2023-03-22 19:17:53 +09:00
if route [ " advertised " ] :
advertised_routes = True
if advertised_routes :
2023-02-06 04:58:09 +00:00
routes = """
< li class = " collection-item avatar " >
< i class = " material-icons circle " > directions < / i >
< span class = " title " > Routes < / span >
2023-03-22 19:57:03 +09:00
< p >
2023-02-06 04:58:09 +00:00
"""
2023-03-22 19:17:53 +09:00
# app.logger.debug("Pulled Routes Dump: "+str(pulled_routes))
# app.logger.debug("All Routes Dump: "+str(all_routes))
2023-03-22 14:07:03 +09:00
# Find all exits and put their ID's into the exit_routes array
2023-03-22 14:31:30 +09:00
exit_routes = [ ]
2023-03-22 14:37:58 +09:00
exit_enabled_color = " red "
2023-03-22 14:54:45 +09:00
exit_tooltip = " enable "
2023-03-22 19:42:18 +09:00
exit_route_enabled = False
2023-03-22 20:35:11 +09:00
failover_pair_prefixes = [ ]
2023-03-22 14:31:30 +09:00
2023-03-22 14:13:42 +09:00
for route in pulled_routes [ " routes " ] :
2023-03-22 14:31:30 +09:00
if route [ " prefix " ] == " 0.0.0.0/0 " or route [ " prefix " ] == " ::/0 " :
exit_routes . append ( route [ " id " ] )
2023-03-22 19:17:53 +09:00
exit_route_found = True
2023-03-22 14:31:30 +09:00
# Test if it is enabled:
if route [ " enabled " ] :
exit_enabled_color = " green "
exit_tooltip = ' disable '
2023-03-22 19:42:18 +09:00
exit_route_enabled = True
2023-03-22 19:53:09 +09:00
app . logger . debug ( " Found exit route ID ' s: " + str ( exit_routes ) )
2023-03-22 20:00:40 +09:00
app . logger . debug ( " Exit Route Information: ID: %s | Enabled: %s | exit_route_enabled: %s / Found: %s " , str ( route [ " id " ] ) , str ( route [ " enabled " ] ) , str ( exit_route_enabled ) , str ( exit_route_found ) )
2023-03-22 14:31:30 +09:00
# Print the button for the Exit routes:
2023-03-22 19:17:53 +09:00
if exit_route_found :
2023-03-22 19:36:37 +09:00
routes = routes + """ <p
2023-03-22 19:17:53 +09:00
class = ' waves-effect waves-light btn-small " " " +exit_enabled_color+ " " " lighten-2 tooltipped '
data - position = ' top ' data - tooltip = ' Click to " " " +exit_tooltip+ " " " '
id = ' " " " +machine[ " id " ]+ " " " -exit '
2023-03-22 19:42:18 +09:00
onclick = " toggle_exit( " " " + exit_routes [ 0 ] + """ , """ + exit_routes [ 1 ] + """ , ' """ + machine [ " id " ] + """ -exit ' , ' """ + str ( exit_route_enabled ) + """ ' ) " >
2023-03-22 19:17:53 +09:00
Exit Route
< / p >
"""
2023-03-22 14:07:03 +09:00
2023-03-22 20:56:30 +09:00
# Check if the route has another enabled identical route.
2023-02-06 04:58:09 +00:00
for route in pulled_routes [ " routes " ] :
2023-03-22 20:42:20 +09:00
for route_info in all_routes [ " routes " ] :
2023-03-22 21:22:45 +09:00
if str ( route_info [ " prefix " ] ) == str ( route [ " prefix " ] ) and ( route [ " prefix " ] != " 0.0.0.0/0 " and route [ " prefix " ] != " ::/0 " ) :
2023-03-22 20:35:11 +09:00
if route_info [ " id " ] != route [ " id " ] :
2023-03-22 21:49:22 +09:00
ha_enabled = False
app . logger . info ( " HA pair found: %s " , str ( route [ " prefix " ] ) )
2023-03-23 08:22:38 +09:00
failover_pair_prefixes . append ( str ( route [ " prefix " ] ) )
2023-03-22 21:49:22 +09:00
if route [ " enabled " ] :
ha_enabled = True
2023-03-22 21:07:22 +09:00
if route [ " prefix " ] != " 0.0.0.0/0 " and route [ " prefix " ] != " ::/0 " and route [ " prefix " ] in failover_pair_prefixes :
2023-03-22 20:35:11 +09:00
route_enabled = " red "
route_tooltip = ' enable '
2023-03-23 07:59:36 +09:00
color_index = failover_pair_prefixes . index ( str ( route [ " prefix " ] ) )
2023-03-23 07:44:13 +09:00
route_enabled_color = helper . get_color ( color_index , " failover " )
2023-03-22 20:35:11 +09:00
if route [ " enabled " ] :
2023-03-22 21:07:22 +09:00
color_index = failover_pair_prefixes . index ( str ( route [ " prefix " ] ) )
route_enabled = helper . get_color ( color_index , " failover " )
2023-03-22 20:35:11 +09:00
route_tooltip = ' disable '
routes = routes + """ <p
class = ' waves-effect waves-light btn-small " " " +route_enabled+ " " " lighten-2 tooltipped '
data - position = ' top ' data - tooltip = ' Click to " " " +route_tooltip+ " " " (Failover Pair) '
id = ' " " " +route[ ' id ' ]+ " " " '
2023-03-23 07:44:13 +09:00
onclick = " toggle_failover_route( " " " + route [ ' id ' ] + """ , ' """ + str ( route [ ' enabled ' ] ) + """ ' , ' """ + str ( route_enabled_color ) + """ ' ) " >
2023-03-22 20:35:11 +09:00
""" +route[ ' prefix ' ]+ """
< / p >
"""
# Get the remaining routes:
for route in pulled_routes [ " routes " ] :
# Get the remaining routes - No exits or failover pairs
if route [ " prefix " ] != " 0.0.0.0/0 " and route [ " prefix " ] != " ::/0 " and route [ " prefix " ] not in failover_pair_prefixes :
2023-03-22 18:52:48 +09:00
app . logger . debug ( " Route: [ " + str ( route [ ' machine ' ] [ ' name ' ] ) + " ] id: " + str ( route [ ' id ' ] ) + " / prefix: " + str ( route [ ' prefix ' ] ) + " enabled?: " + str ( route [ ' enabled ' ] ) )
route_enabled = " red "
route_tooltip = ' enable '
if route [ " enabled " ] :
route_enabled = " green "
route_tooltip = ' disable '
2023-03-22 19:36:37 +09:00
routes = routes + """ <p
2023-03-22 13:58:51 +09:00
class = ' waves-effect waves-light btn-small " " " +route_enabled+ " " " lighten-2 tooltipped '
data - position = ' top ' data - tooltip = ' Click to " " " +route_tooltip+ " " " '
id = ' " " " +route[ ' id ' ]+ " " " '
onclick = " toggle_route( " " " + route [ ' id ' ] + """ , ' """ + str ( route [ ' enabled ' ] ) + """ ' ) " >
""" +route[ ' prefix ' ]+ """
< / p >
"""
2023-03-22 19:57:03 +09:00
routes = routes + " </p></li> "
2023-02-06 04:58:09 +00:00
# Get machine tags
tag_array = " "
2023-03-22 12:08:03 +09:00
for tag in machine [ " forcedTags " ] :
tag_array = tag_array + " { tag: ' " + tag [ 4 : ] + " ' }, "
2023-02-06 04:58:09 +00:00
tags = """
< li class = " collection-item avatar " >
< i class = " material-icons circle tooltipped " data - position = " right " data - tooltip = " Spaces will be replaced with a dash (-) upon page refresh " > label < / i >
< span class = " title " > Tags < / span >
< p > < div style = ' margin: 0px ' class = ' chips ' id = ' " " " +machine[ " id " ]+ " " " -tags ' > < / div > < / p >
< / li >
< script >
window . addEventListener ( ' load ' ,
function ( ) {
var instances = M . Chips . init (
document . getElementById ( ' " " " +machine[ ' id ' ]+ " " " -tags ' ) , ( {
data : [ """ +tag_array+ """ ] ,
onChipDelete ( ) { delete_chip ( """ +machine[ " id " ]+ """ , this . chipsData ) } ,
onChipAdd ( ) { add_chip ( """ +machine[ " id " ]+ """ , this . chipsData ) }
} )
) ;
} , false
2023-02-17 22:41:41 +09:00
)
2023-02-06 04:58:09 +00:00
< / script >
"""
# Get the machine IP's
machine_ips = " <ul> "
2023-02-20 17:23:05 +09:00
for ip_address in machine [ " ipAddresses " ] :
machine_ips = machine_ips + " <li> " + ip_address + " </li> "
2023-02-06 04:58:09 +00:00
machine_ips = machine_ips + " </ul> "
# Format the dates for easy readability
last_seen_parse = parser . parse ( machine [ " lastSeen " ] )
last_seen_local = last_seen_parse . astimezone ( timezone )
last_seen_delta = local_time - last_seen_local
last_seen_print = helper . pretty_print_duration ( last_seen_delta )
last_seen_time = str ( last_seen_local . strftime ( ' % A % m/ %d / % Y, % H: % M: % S ' ) ) + " " + str ( timezone ) + " ( " + str ( last_seen_print ) + " ) "
last_update_parse = local_time if machine [ " lastSuccessfulUpdate " ] is None else parser . parse ( machine [ " lastSuccessfulUpdate " ] )
last_update_local = last_update_parse . astimezone ( timezone )
last_update_delta = local_time - last_update_local
last_update_print = helper . pretty_print_duration ( last_update_delta )
last_update_time = str ( last_update_local . strftime ( ' % A % m/ %d / % Y, % H: % M: % S ' ) ) + " " + str ( timezone ) + " ( " + str ( last_update_print ) + " ) "
created_parse = parser . parse ( machine [ " createdAt " ] )
created_local = created_parse . astimezone ( timezone )
created_delta = local_time - created_local
created_print = helper . pretty_print_duration ( created_delta )
created_time = str ( created_local . strftime ( ' % A % m/ %d / % Y, % H: % M: % S ' ) ) + " " + str ( timezone ) + " ( " + str ( created_print ) + " ) "
2023-03-17 20:56:03 +09:00
# If there is no expiration date, we don't need to do any calculations:
if machine [ " expiry " ] != " 0001-01-01T00:00:00Z " :
expiry_parse = parser . parse ( machine [ " expiry " ] )
expiry_local = expiry_parse . astimezone ( timezone )
expiry_delta = expiry_local - local_time
expiry_print = helper . pretty_print_duration ( expiry_delta , " expiry " )
if str ( expiry_local . strftime ( ' % Y ' ) ) in ( " 0001 " , " 9999 " , " 0000 " ) :
expiry_time = " No expiration date. "
elif int ( expiry_local . strftime ( ' % Y ' ) ) > int ( expiry_local . strftime ( ' % Y ' ) ) + 2 :
expiry_time = str ( expiry_local . strftime ( ' % m/ % Y ' ) ) + " " + str ( timezone ) + " ( " + str ( expiry_print ) + " ) "
else :
expiry_time = str ( expiry_local . strftime ( ' % A % m/ %d / % Y, % H: % M: % S ' ) ) + " " + str ( timezone ) + " ( " + str ( expiry_print ) + " ) "
2023-03-17 21:05:47 +09:00
expiring_soon = True if int ( expiry_delta . days ) < 14 and int ( expiry_delta . days ) > 0 else False
2023-03-17 21:00:05 +09:00
app . logger . debug ( " Machine: " + machine [ " name " ] + " expires: " + str ( expiry_local . strftime ( ' % Y ' ) ) + " / " + str ( expiry_delta . days ) )
2023-03-17 20:56:03 +09:00
else :
2023-02-22 22:26:06 +09:00
expiry_time = " No expiration date. "
2023-03-17 21:10:16 +09:00
expiring_soon = False
2023-03-17 21:00:05 +09:00
app . logger . debug ( " Machine: " + machine [ " name " ] + " has no expiration date " )
2023-03-17 20:56:03 +09:00
2023-02-22 22:30:32 +09:00
2023-02-06 04:58:09 +00:00
# Get the first 10 characters of the PreAuth Key:
if machine [ " preAuthKey " ] :
preauth_key = str ( machine [ " preAuthKey " ] [ " key " ] ) [ 0 : 10 ]
else : preauth_key = " None "
2023-03-22 18:52:48 +09:00
# Set the status and user badge color:
2023-02-06 04:58:09 +00:00
text_color = helper . text_color_duration ( last_seen_delta )
user_color = helper . get_color ( int ( machine [ " user " ] [ " id " ] ) )
# Generate the various badges:
2023-03-22 18:57:43 +09:00
status_badge = " <i class= ' material-icons left tooltipped " + text_color + " ' data-position= ' top ' data-tooltip= ' Last Seen: " + last_seen_print + " ' id= ' " + machine [ " id " ] + " -status ' >fiber_manual_record</i> "
2023-02-22 22:26:06 +09:00
user_badge = " <span class= ' badge ipinfo " + user_color + " white-text hide-on-small-only ' id= ' " + machine [ " id " ] + " -ns-badge ' > " + machine [ " user " ] [ " name " ] + " </span> "
2023-03-22 21:39:04 +09:00
exit_node_badge = " " if not exit_route_enabled else " <span class= ' badge grey white-text text-lighten-4 tooltipped ' data-position= ' left ' data-tooltip= ' This machine has an enabled exit route. ' >Exit</span> "
2023-03-22 21:49:22 +09:00
ha_route_badge = " " if not ha_enabled else " <span class= ' badge blue-grey white-text text-lighten-4 tooltipped ' data-position= ' left ' data-tooltip= ' This machine has an enabled HA route. ' >HA</span> "
expiration_badge = " " if not expiring_soon else " <span class= ' badge red white-text text-lighten-4 tooltipped ' data-position= ' left ' data-tooltip= ' This machine expires soon. ' >Expiring!</span> "
2023-02-06 04:58:09 +00:00
machine_content [ idx ] = ( str ( render_template (
' machines_card.html ' ,
given_name = machine [ " givenName " ] ,
machine_id = machine [ " id " ] ,
hostname = machine [ " name " ] ,
ns_name = machine [ " user " ] [ " name " ] ,
ns_id = machine [ " user " ] [ " id " ] ,
ns_created = machine [ " user " ] [ " createdAt " ] ,
last_seen = str ( last_seen_print ) ,
last_update = str ( last_update_print ) ,
machine_ips = Markup ( machine_ips ) ,
advertised_routes = Markup ( routes ) ,
exit_node_badge = Markup ( exit_node_badge ) ,
2023-03-22 21:32:34 +09:00
ha_route_badge = Markup ( ha_route_badge ) ,
2023-02-06 04:58:09 +00:00
status_badge = Markup ( status_badge ) ,
2023-02-22 18:02:47 +09:00
user_badge = Markup ( user_badge ) ,
2023-02-06 04:58:09 +00:00
last_update_time = str ( last_update_time ) ,
last_seen_time = str ( last_seen_time ) ,
created_time = str ( created_time ) ,
2023-02-22 18:02:47 +09:00
expiry_time = str ( expiry_time ) ,
2023-02-06 04:58:09 +00:00
preauth_key = str ( preauth_key ) ,
2023-02-22 22:26:06 +09:00
expiration_badge = Markup ( expiration_badge ) ,
2023-02-06 04:58:09 +00:00
machine_tags = Markup ( tags ) ,
2023-03-22 12:08:03 +09:00
taglist = machine [ " forcedTags " ]
2023-02-06 04:58:09 +00:00
) ) )
2023-02-27 22:57:38 +09:00
app . logger . info ( " Finished thread for machine " + machine [ " givenName " ] + " index " + str ( idx ) )
2023-02-06 04:58:09 +00:00
# Render the cards for the machines page:
def render_machines_cards ( ) :
2023-02-28 12:43:09 +09:00
app . logger . info ( " Rendering machine cards " )
2023-02-06 04:58:09 +00:00
url = headscale . get_url ( )
api_key = headscale . get_api_key ( )
machines_list = headscale . get_machines ( url , api_key )
#########################################
# Thread this entire thing.
2023-02-20 17:23:05 +09:00
num_threads = len ( machines_list [ " machines " ] )
2023-02-06 04:58:09 +00:00
iterable = [ ]
machine_content = { }
2023-02-20 17:23:05 +09:00
for i in range ( 0 , num_threads ) :
2023-02-27 22:57:38 +09:00
app . logger . debug ( " Appending iterable: " + str ( i ) )
2023-02-06 04:58:09 +00:00
iterable . append ( i )
# Flask-Executor Method:
2023-03-22 13:58:51 +09:00
# Get all routes
all_routes = headscale . get_routes ( url , api_key )
app . logger . debug ( " All found routes " )
app . logger . debug ( str ( all_routes ) )
2023-03-16 08:24:19 +09:00
if LOG_LEVEL == " DEBUG " :
# DEBUG: Do in a forloop:
2023-03-22 13:58:51 +09:00
for idx in iterable : thread_machine_content ( machines_list [ " machines " ] [ idx ] , machine_content , idx , all_routes )
2023-03-16 08:24:19 +09:00
else :
app . logger . info ( " Starting futures " )
2023-03-22 13:58:51 +09:00
futures = [ executor . submit ( thread_machine_content , machines_list [ " machines " ] [ idx ] , machine_content , idx , all_routes ) for idx in iterable ]
2023-03-16 08:24:19 +09:00
# Wait for the executor to finish all jobs:
wait ( futures , return_when = ALL_COMPLETED )
app . logger . info ( " Finished futures " )
2023-02-06 04:58:09 +00:00
# Sort the content by machine_id:
sorted_machines = { key : val for key , val in sorted ( machine_content . items ( ) , key = lambda ele : ele [ 0 ] ) }
2023-03-21 18:29:51 +09:00
content = " <ul class= ' collapsible expandable ' > "
2023-02-06 04:58:09 +00:00
# Print the content
2023-02-20 17:23:05 +09:00
for index in range ( 0 , num_threads ) :
2023-02-06 04:58:09 +00:00
content = content + str ( sorted_machines [ index ] )
2023-03-21 18:07:08 +09:00
content = content + " </ul> "
2023-02-06 04:58:09 +00:00
return Markup ( content )
# Render the cards for the Users page:
def render_users_cards ( ) :
2023-02-28 12:43:09 +09:00
app . logger . info ( " Rendering Users cards " )
2023-02-16 00:06:31 +09:00
url = headscale . get_url ( )
api_key = headscale . get_api_key ( )
2023-02-06 04:58:09 +00:00
user_list = headscale . get_users ( url , api_key )
2023-03-21 18:29:51 +09:00
content = " <ul class= ' collapsible expandable ' > "
2023-02-06 04:58:09 +00:00
for user in user_list [ " users " ] :
# Get all preAuth Keys in the user, only display if one exists:
preauth_keys_collection = build_preauth_key_table ( user [ " name " ] )
# Set the user badge color:
user_color = helper . get_color ( int ( user [ " id " ] ) , " text " )
# Generate the various badges:
status_badge = " <i class= ' material-icons left " + user_color + " ' id= ' " + user [ " id " ] + " -status ' >fiber_manual_record</i> "
content = content + render_template (
' users_card.html ' ,
status_badge = Markup ( status_badge ) ,
2023-02-22 22:01:18 +09:00
user_name = user [ " name " ] ,
user_id = user [ " id " ] ,
2023-02-06 04:58:09 +00:00
preauth_keys_collection = Markup ( preauth_keys_collection )
)
2023-03-21 18:17:07 +09:00
content = content + " </ul> "
2023-02-06 04:58:09 +00:00
return Markup ( content )
# Builds the preauth key table for the User page
def build_preauth_key_table ( user_name ) :
2023-02-28 12:43:09 +09:00
app . logger . info ( " Building the PreAuth key table for User: %s " , str ( user_name ) )
2023-02-06 04:58:09 +00:00
url = headscale . get_url ( )
api_key = headscale . get_api_key ( )
preauth_keys = headscale . get_preauth_keys ( url , api_key , user_name )
preauth_keys_collection = """ <li class= " collection-item avatar " >
< span
class = ' badge grey lighten-2 btn-small '
onclick = ' toggle_expired() '
> Toggle Expired < / span >
< span
href = " #card_modal "
class = ' badge grey lighten-2 btn-small modal-trigger '
2023-03-04 10:52:03 +09:00
onclick = " load_modal_add_preauth_key( ' " " " + user_name + """ ' ) "
2023-02-06 04:58:09 +00:00
> Add PreAuth Key < / span >
< i class = " material-icons circle " > vpn_key < / i >
< span class = " title " > PreAuth Keys < / span >
"""
if len ( preauth_keys [ " preAuthKeys " ] ) == 0 : preauth_keys_collection + = " <p>No keys defined for this user</p> "
if len ( preauth_keys [ " preAuthKeys " ] ) > 0 :
preauth_keys_collection + = """
< table class = " responsive-table striped " id = ' " " " +user_name+ " " " -preauthkey-table ' >
< thead >
< tr >
< td > ID < / td >
2023-02-10 14:01:31 +09:00
< td class = ' tooltipped ' data - tooltip = ' Click an Auth Key Prefix to copy it to the clipboard ' > Key Prefix < / td >
2023-02-06 04:58:09 +00:00
< td > < center > Reusable < / center > < / td >
< td > < center > Used < / center > < / td >
< td > < center > Ephemeral < / center > < / td >
< td > < center > Usable < / center > < / td >
< td > < center > Actions < / center > < / td >
< / tr >
< / thead >
"""
for key in preauth_keys [ " preAuthKeys " ] :
# Get the key expiration date and compare it to now to check if it's expired:
# Set the current timezone and local time
timezone = pytz . timezone ( os . environ [ " TZ " ] if os . environ [ " TZ " ] else " UTC " )
local_time = timezone . localize ( datetime . now ( ) )
expiration_parse = parser . parse ( key [ " expiration " ] )
key_expired = True if expiration_parse < local_time else False
expiration_time = str ( expiration_parse . strftime ( ' % A % m/ %d / % Y, % H: % M: % S ' ) ) + " " + str ( timezone )
key_usable = False
if key [ " reusable " ] and not key_expired : key_usable = True
if not key [ " reusable " ] and not key [ " used " ] and not key_expired : key_usable = True
2023-02-10 12:20:50 +09:00
# Class for the javascript function to look for to toggle the hide function
hide_expired = " expired-row " if not key_usable else " "
2023-02-06 04:58:09 +00:00
2023-02-22 22:01:18 +09:00
btn_reusable = " <i class= ' pulse material-icons tiny blue-text text-darken-1 ' >fiber_manual_record</i> " if key [ " reusable " ] else " "
btn_ephemeral = " <i class= ' pulse material-icons tiny red-text text-darken-1 ' >fiber_manual_record</i> " if key [ " ephemeral " ] else " "
btn_used = " <i class= ' pulse material-icons tiny yellow-text text-darken-1 ' >fiber_manual_record</i> " if key [ " used " ] else " "
btn_usable = " <i class= ' pulse material-icons tiny green-text text-darken-1 ' >fiber_manual_record</i> " if key_usable else " "
2023-02-06 04:58:09 +00:00
# Other buttons:
2023-02-22 22:01:18 +09:00
btn_delete = " <span href= ' #card_modal ' data-tooltip= ' Expire this PreAuth Key ' class= ' btn-small modal-trigger badge tooltipped white-text red ' onclick= ' load_modal_expire_preauth_key( \" " + user_name + " \" , \" " + str ( key [ " key " ] ) + " \" ) ' >Expire</span> " if key_usable else " "
tooltip_data = " Expiration: " + expiration_time
2023-02-06 04:58:09 +00:00
# TR ID will look like "1-albert-tr"
preauth_keys_collection = preauth_keys_collection + """
< tr id = ' " " " +key[ " id " ]+ " " " - " " " +user_name+ " " " -tr ' class = ' " " " +hide_expired+ " " " ' >
2023-02-10 14:22:23 +09:00
< td > """ +str(key[ " id " ])+ """ < / td >
2023-02-10 14:25:03 +09:00
< td onclick = copy_preauth_key ( ' " " " +str(key[ " key " ])+ " " " ' ) class = ' tooltipped ' data - tooltip = ' " " " +tooltip_data+ " " " ' > """ +str(key[ " key " ])[0:10]+ """ < / td >
2023-02-06 04:58:09 +00:00
< td > < center > """ +btn_reusable+ """ < / center > < / td >
< td > < center > """ +btn_used+ """ < / center > < / td >
< td > < center > """ +btn_ephemeral+ """ < / center > < / td >
< td > < center > """ +btn_usable+ """ < / center > < / td >
< td > < center > """ +btn_delete+ """ < / center > < / td >
< / tr >
"""
preauth_keys_collection = preauth_keys_collection + """ </table>
< / li >
"""
2023-02-17 20:58:58 +09:00
return preauth_keys_collection
2023-02-22 17:00:36 +09:00
def oidc_nav_dropdown ( user_name , email_address , name ) :
2023-02-28 12:43:09 +09:00
app . logger . info ( " OIDC is enabled. Building the OIDC nav dropdown " )
2023-02-23 08:17:09 +09:00
html_payload = """
2023-03-21 14:51:30 +09:00
< ! - - OIDC Dropdown Structure - - >
2023-02-23 10:41:53 +09:00
< ul id = " dropdown1 " class = " dropdown-content dropdown-oidc " >
2023-02-23 11:14:54 +09:00
< ul class = " collection dropdown-oidc-collection " >
2023-02-23 12:03:43 +09:00
< li class = " collection-item dropdown-oidc-avatar avatar " >
2023-02-23 10:41:53 +09:00
< i class = " material-icons circle " > email < / i >
2023-02-23 12:03:43 +09:00
< span class = " dropdown-oidc-title title " > Email < / span >
2023-02-23 10:41:53 +09:00
< p > """ +email_address+ """ < / p >
< / li >
2023-02-23 12:03:43 +09:00
< li class = " collection-item dropdown-oidc-avatar avatar " >
2023-02-23 10:41:53 +09:00
< i class = " material-icons circle " > person_outline < / i >
2023-02-23 12:03:43 +09:00
< span class = " dropdown-oidc-title title " > Username < / span >
2023-02-23 10:41:53 +09:00
< p > """ +user_name+ """ < / p >
< / li >
< / ul >
< li class = " divider " > < / li >
2023-02-23 08:17:09 +09:00
< li > < a href = " logout " > < i class = " material-icons left " > exit_to_app < / i > Logout < / a > < / li >
< / ul >
< li >
< a class = " dropdown-trigger " href = " #! " data - target = " dropdown1 " >
""" +name+ """ < i class = " material-icons right " > account_circle < / i >
< / a >
< / li >
"""
2023-02-20 17:23:05 +09:00
return Markup ( html_payload )
2023-02-22 16:04:10 +09:00
2023-02-22 17:00:36 +09:00
def oidc_nav_mobile ( user_name , email_address , name ) :
2023-02-22 16:04:10 +09:00
# https://materializecss.github.io/materialize/sidenav.html
html_payload = """
2023-02-22 18:12:41 +09:00
< li > < hr > < a href = " logout " > < i class = " material-icons left " > exit_to_app < / i > Logout < / a > < / li >
2023-02-22 16:04:10 +09:00
"""
2023-03-21 13:57:17 +09:00
return Markup ( html_payload )
2023-03-21 14:38:09 +09:00
def render_search ( ) :
html_payload = """
2023-03-21 20:23:53 +09:00
< li role = " menu-item " class = " tooltipped " data - position = " bottom " data - tooltip = " Search " onclick = " show_search() " >
< a href = " # " > < i class = " material-icons " > search < / i > < / a >
< / li >
2023-03-21 14:38:09 +09:00
"""
return Markup ( html_payload )