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-19 18:43:49 +09:00
|
|
|
import headscale, helper, pytz, os, yaml
|
2023-02-19 20:20:55 +09:00
|
|
|
from flask import Markup, render_template, Flask, logging
|
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-19 19:58:56 +09:00
|
|
|
app = Flask(__name__)
|
2023-02-19 20:20:55 +09:00
|
|
|
LOG = logging.create_logger(app)
|
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():
|
|
|
|
|
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-15 19:54:48 +09:00
|
|
|
try: config_file = open("/etc/headscale/config.yml", "r")
|
|
|
|
|
except: config_file = open("/etc/headscale/config.yaml", "r")
|
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
|
|
|
|
|
|
|
|
# Get all routes:
|
|
|
|
|
routes = headscale.get_routes(url,api_key)
|
|
|
|
|
total_routes = len(routes["routes"])
|
|
|
|
|
enabled_routes = 0
|
|
|
|
|
for route in routes["routes"]:
|
|
|
|
|
if route["enabled"] and route['advertised']:
|
|
|
|
|
enabled_routes += 1
|
|
|
|
|
|
|
|
|
|
# Get a count of all enabled exit routes
|
|
|
|
|
exits_count = 0
|
|
|
|
|
exits_enabled_count = 0
|
|
|
|
|
for route in routes["routes"]:
|
|
|
|
|
if route['advertised']:
|
|
|
|
|
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
|
|
|
|
|
user_count = 0
|
|
|
|
|
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:
|
|
|
|
|
if "server" in config_yaml["derp"] and config_yaml["derp"]["server"]["enabled"] == "True":
|
|
|
|
|
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 = """
|
|
|
|
|
<ul class=collection with-header">
|
|
|
|
|
<li class="collection-header"><h4>Server Statistics</h4></li>
|
|
|
|
|
<li class="collection-item"><div>Machines Added <div class="secondary-content">"""+ str(machines_count) +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Users Added <div class="secondary-content">"""+ str(user_count) +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Usable Preauth Keys <div class="secondary-content">"""+ str(usable_keys_count) +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Enabled/Total Routes <div class="secondary-content">"""+ str(enabled_routes) +"""/"""+str(total_routes) +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Enabled/Total Exits <div class="secondary-content">"""+ str(exits_enabled_count) +"""/"""+str(exits_count)+"""</div></div></li>
|
|
|
|
|
</ul>
|
|
|
|
|
"""
|
|
|
|
|
general_content = """
|
|
|
|
|
<ul class=collection with-header">
|
|
|
|
|
<li class="collection-header"><h4>General Information</h4></li>
|
|
|
|
|
<li class="collection-item"><div>IP Prefixes <div class="secondary-content">"""+ ip_prefixes +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Server URL <div class="secondary-content">"""+ server_url +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Updates Disabled <div class="secondary-content">"""+ disable_check_updates +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Ephemeral Node Inactivity Timeout <div class="secondary-content">"""+ ephemeral_node_inactivity_timeout +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Node Update Check Interval <div class="secondary-content">"""+ node_update_check_interval +"""</div></div></li>
|
|
|
|
|
</ul>
|
2023-02-06 04:58:09 +00:00
|
|
|
"""
|
2023-02-23 14:29:35 +09:00
|
|
|
oidc_content = """
|
|
|
|
|
<ul class=collection with-header">
|
|
|
|
|
<li class="collection-header"><h4>Headscale OIDC Information</h4></li>
|
|
|
|
|
<li class="collection-item"><div>Issuer <div class="secondary-content">"""+ issuer +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Client ID <div class="secondary-content">"""+ client_id +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Scope <div class="secondary-content">"""+ scope +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Use OIDC Token Expiry <div class="secondary-content">"""+ use_expiry_from_token +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Expiry <div class="secondary-content">"""+ expiry +"""</div></div></li>
|
|
|
|
|
</ul>
|
|
|
|
|
"""
|
|
|
|
|
derp_content = """
|
|
|
|
|
<ul class=collection with-header">
|
|
|
|
|
<li class="collection-header"><h4>Embedded DERP Information</h4></li>
|
|
|
|
|
<li class="collection-item"><div>Issuer <div class="secondary-content">"""+ issuer +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Client ID <div class="secondary-content">"""+ client_id +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Scope <div class="secondary-content">"""+ scope +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Use OIDC Token Expiry <div class="secondary-content">"""+ use_expiry_from_token +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Expiry <div class="secondary-content">"""+ expiry +"""</div></div></li>
|
|
|
|
|
</ul>
|
|
|
|
|
"""
|
|
|
|
|
oidc_content = """
|
|
|
|
|
<ul class=collection with-header">
|
|
|
|
|
<li class="collection-header"><h4>Embedded DERP Information</h4></li>
|
|
|
|
|
<li class="collection-item"><div>Issuer <div class="secondary-content">"""+ issuer +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Client ID <div class="secondary-content">"""+ client_id +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Scope <div class="secondary-content">"""+ scope +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Use OIDC Token Expiry <div class="secondary-content">"""+ use_expiry_from_token +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Expiry <div class="secondary-content">"""+ expiry +"""</div></div></li>
|
|
|
|
|
</ul>
|
|
|
|
|
"""
|
|
|
|
|
dns_content = """
|
|
|
|
|
<ul class=collection with-header">
|
|
|
|
|
<li class="collection-header"><h4>Embedded DERP Information</h4></li>
|
|
|
|
|
<li class="collection-item"><div>DNS Nameservers <div class="secondary-content">"""+ nameservers +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>MagicDNS <div class="secondary-content">"""+ magic_dns +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Search Domains <div class="secondary-content">"""+ domains +"""</div></div></li>
|
|
|
|
|
<li class="collection-item"><div>Base Domain <div class="secondary-content">"""+ base_domain +"""</div></div></li>
|
|
|
|
|
</ul>
|
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-02-23 14:43:50 +09:00
|
|
|
if "derp" not in config_yaml:
|
|
|
|
|
oidc_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 14:43:50 +09:00
|
|
|
if config_yaml["derp"]["server"]["enabled"] == "False":
|
|
|
|
|
oidc_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-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
|
|
|
|
|
|
|
|
|
|
content = "<br><div class='row'>" + overview_content + general_content + derp_content + oidc_content + dns_content + "</div>"
|
|
|
|
|
return Markup(content)
|
|
|
|
|
|
|
|
|
|
def thread_machine_content(machine, machine_content, idx):
|
|
|
|
|
# machine = passed in machine information
|
|
|
|
|
# content = place to write the content
|
|
|
|
|
|
|
|
|
|
url = headscale.get_url()
|
|
|
|
|
api_key = headscale.get_api_key()
|
|
|
|
|
|
|
|
|
|
# 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())
|
|
|
|
|
|
|
|
|
|
# Get the machines routes
|
|
|
|
|
pulled_routes = headscale.get_machine_routes(url, api_key, machine["id"])
|
|
|
|
|
routes = ""
|
|
|
|
|
|
|
|
|
|
# Test if the machine is an exit node:
|
|
|
|
|
exit_node = False
|
|
|
|
|
# If the LENGTH of "routes" is NULL/0, there are no routes, enabled or disabled:
|
|
|
|
|
if len(pulled_routes["routes"]) > 0:
|
|
|
|
|
advertised_and_enabled = False
|
|
|
|
|
advertised_route = False
|
|
|
|
|
# First, check if there are any routes that are both enabled and advertised
|
|
|
|
|
for route in pulled_routes["routes"]:
|
|
|
|
|
if route ["advertised"] and route["enabled"]:
|
|
|
|
|
advertised_and_enabled = True
|
|
|
|
|
if route["advertised"]:
|
|
|
|
|
advertised_route = True
|
|
|
|
|
if advertised_and_enabled or advertised_route:
|
|
|
|
|
routes = """
|
|
|
|
|
<li class="collection-item avatar">
|
|
|
|
|
<i class="material-icons circle">directions</i>
|
|
|
|
|
<span class="title">Routes</span>
|
|
|
|
|
<p><div>
|
|
|
|
|
"""
|
|
|
|
|
for route in pulled_routes["routes"]:
|
2023-02-17 22:41:41 +09:00
|
|
|
# LOG.warning("Route: ["+str(route['machine']['name'])+"] id: "+str(route['id'])+" / prefix: "+str(route['prefix'])+" enabled?: "+str(route['enabled']))
|
2023-02-06 04:58:09 +00:00
|
|
|
# Check if the route is enabled:
|
|
|
|
|
route_enabled = "red"
|
|
|
|
|
route_tooltip = 'enable'
|
|
|
|
|
if route["enabled"]:
|
|
|
|
|
route_enabled = "green"
|
|
|
|
|
route_tooltip = 'disable'
|
|
|
|
|
if route["prefix"] == "0.0.0.0/0" or route["prefix"] == "::/0" and str(route["enabled"]) == "True":
|
|
|
|
|
exit_node = True
|
|
|
|
|
routes = routes+"""
|
|
|
|
|
<p
|
|
|
|
|
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>
|
|
|
|
|
"""
|
|
|
|
|
routes = routes+"</div></p></li>"
|
|
|
|
|
|
|
|
|
|
# Get machine tags
|
|
|
|
|
tag_array = ""
|
|
|
|
|
for tag in machine["forcedTags"]: tag_array = tag_array+"{tag: '"+tag[4:]+"'}, "
|
|
|
|
|
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-02-22 17:58:18 +09:00
|
|
|
expiry_parse = parser.parse(machine["expiry"])
|
|
|
|
|
expiry_local = expiry_parse.astimezone(timezone)
|
2023-02-22 21:34:35 +09:00
|
|
|
expiry_delta = expiry_local - local_time
|
|
|
|
|
expiry_print = helper.pretty_print_duration(expiry_delta, "expiry")
|
2023-02-22 22:43:09 +09:00
|
|
|
|
2023-02-22 22:42:39 +09:00
|
|
|
if str(expiry_local.strftime('%Y')) in ("0001", "9999", "0000"):
|
2023-02-22 22:26:06 +09:00
|
|
|
expiry_time = "No expiration date."
|
2023-02-22 22:42:39 +09:00
|
|
|
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)+")"
|
2023-02-22 22:20:36 +09:00
|
|
|
else:
|
2023-02-22 22:26:06 +09:00
|
|
|
expiry_time = str(expiry_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+" "+str(timezone)+" ("+str(expiry_print)+")"
|
2023-02-22 22:30:32 +09:00
|
|
|
LOG.error("Machine: "+machine["name"]+" expires: "+str(expiry_local.strftime('%Y'))+" / "+str(expiry_delta.days))
|
|
|
|
|
|
2023-02-22 23:14:13 +09:00
|
|
|
expiring_soon = True if int(expiry_delta.days) < 14 and int(expiry_delta.days) > 0 else False
|
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"
|
|
|
|
|
|
|
|
|
|
# Set the status badge color:
|
|
|
|
|
text_color = helper.text_color_duration(last_seen_delta)
|
|
|
|
|
# Set the user badge color:
|
|
|
|
|
user_color = helper.get_color(int(machine["user"]["id"]))
|
|
|
|
|
|
|
|
|
|
# Generate the various badges:
|
|
|
|
|
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-02-06 04:58:09 +00:00
|
|
|
exit_node_badge = "" if not exit_node else "<span class='badge grey white-text text-lighten-4 tooltipped' data-position='left' data-tooltip='This machine has an enabled exit route.'>Exit Node</span>"
|
2023-02-22 22:26:06 +09:00
|
|
|
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),
|
|
|
|
|
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-02-17 22:41:41 +09:00
|
|
|
LOG.warning("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():
|
|
|
|
|
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-22 18:35:24 +09:00
|
|
|
LOG.error("Appending iterable: "+str(i))
|
2023-02-06 04:58:09 +00:00
|
|
|
iterable.append(i)
|
|
|
|
|
# Flask-Executor Method:
|
2023-02-17 22:41:41 +09:00
|
|
|
LOG.warning("Starting futures")
|
2023-02-06 04:58:09 +00:00
|
|
|
futures = [executor.submit(thread_machine_content, machines_list["machines"][idx], machine_content, idx) for idx in iterable]
|
|
|
|
|
# Wait for the executor to finish all jobs:
|
|
|
|
|
wait(futures, return_when=ALL_COMPLETED)
|
2023-02-17 22:41:41 +09:00
|
|
|
LOG.warning("Finished futures")
|
2023-02-06 04:58:09 +00:00
|
|
|
|
|
|
|
|
# DEBUG: Do in a forloop:
|
|
|
|
|
# for idx in iterable: thread_machine_content(machines_list["machines"][idx], machine_content, idx)
|
|
|
|
|
|
|
|
|
|
# Sort the content by machine_id:
|
|
|
|
|
sorted_machines = {key: val for key, val in sorted(machine_content.items(), key = lambda ele: ele[0])}
|
|
|
|
|
|
|
|
|
|
content = "<div class='u-flex u-justify-space-evenly u-flex-wrap u-gap-1'>"
|
|
|
|
|
# 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])
|
|
|
|
|
# content = content+str(sorted_machines[index])
|
|
|
|
|
|
|
|
|
|
content = content+"</div>"
|
|
|
|
|
|
|
|
|
|
return Markup(content)
|
|
|
|
|
|
|
|
|
|
# Render the cards for the Users page:
|
|
|
|
|
def render_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)
|
|
|
|
|
|
|
|
|
|
content = "<div class='u-flex u-justify-space-evenly u-flex-wrap u-gap-1'>"
|
|
|
|
|
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)
|
|
|
|
|
)
|
|
|
|
|
content = content+"</div>"
|
|
|
|
|
return Markup(content)
|
|
|
|
|
|
|
|
|
|
# Builds the preauth key table for the User page
|
|
|
|
|
def build_preauth_key_table(user_name):
|
|
|
|
|
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'
|
|
|
|
|
onclick="load_modal_add_preauth_key('"""+user_name+"""')"
|
|
|
|
|
>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-23 08:17:09 +09:00
|
|
|
html_payload = """
|
|
|
|
|
<!-- 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
|
|
|
"""
|
|
|
|
|
return Markup(html_payload)
|