Files
headscale-webui/helper.py

276 lines
11 KiB
Python
Raw Normal View History

2023-02-20 17:23:05 +09:00
# pylint: disable=wrong-import-order
2023-02-21 10:16:50 +09:00
import os, headscale, requests
2023-02-17 22:41:41 +09:00
from flask import Flask
from flask.logging import create_logger
2023-02-06 04:58:09 +00:00
2023-02-15 19:20:44 +09:00
app = Flask(__name__)
2023-02-17 22:41:41 +09:00
LOG = create_logger(app)
2023-02-15 19:02:14 +09:00
2023-02-22 21:34:35 +09:00
def pretty_print_duration(duration, delta_type=""):
2023-02-17 22:24:58 +09:00
""" Prints a duration in human-readable formats """
2023-02-22 21:34:35 +09:00
if delta_type == "expiry":
days, seconds = duration.days, duration.seconds
hours = (days * 24 + seconds // 3600)
mins = (seconds % 3600) // 60
secs = seconds % 60
2023-02-22 18:59:52 +09:00
if days > 0: return str(days ) + " days ago" if days > 1 else str(days ) + " day ago"
if hours > 0: return str(hours) + " hours ago" if hours > 1 else str(hours) + " hour ago"
if mins > 0: return str(mins ) + " minutes ago" if mins > 1 else str(mins ) + " minute ago"
2023-02-22 21:34:35 +09:00
return str(secs ) + " seconds ago" if secs >= 1 or secs == 0 else str(secs ) + " second ago"
2023-02-22 18:59:52 +09:00
days, seconds = abs(int(duration.days)), abs(int(duration.seconds))
hours = (days * 24 + seconds // 3600)
mins = (seconds % 3600) // 60
secs = seconds % 60
2023-02-22 21:34:35 +09:00
if days > 0: return "in "+ str(days ) + " days" if days > 1 else str(days ) + " day"
2023-02-22 18:59:52 +09:00
if hours > 0: return "in "+ str(hours) + " hours" if hours > 1 else str(hours) + " hour"
if mins > 0: return "in "+ str(mins ) + " minutes" if mins > 1 else str(mins ) + " minute"
2023-02-22 21:34:35 +09:00
return "in "+ str(secs ) + " seconds" if secs >= 1 or secs == 0 else str(secs ) + " second"
2023-02-06 04:58:09 +00:00
def text_color_duration(duration):
2023-02-17 22:24:58 +09:00
""" Prints a color based on duratioin (imported as seconds) """
2023-02-06 04:58:09 +00:00
days, seconds = duration.days, duration.seconds
hours = (days * 24 + seconds // 3600)
mins = ((seconds % 3600) // 60)
secs = (seconds % 60)
2023-02-17 22:24:58 +09:00
if days > 30: return "grey-text "
if days > 14: return "red-text text-darken-2 "
if days > 5: return "deep-orange-text text-lighten-1"
if days > 1: return "deep-orange-text text-lighten-1"
if hours > 12: return "orange-text "
if hours > 1: return "orange-text text-lighten-2"
if hours == 1: return "yellow-text "
if mins > 15: return "yellow-text text-lighten-2"
if mins > 5: return "green-text text-lighten-3"
if secs > 30: return "green-text text-lighten-2"
return "green-text "
2023-02-06 04:58:09 +00:00
2023-02-16 20:17:36 +09:00
def key_check():
2023-02-17 22:24:58 +09:00
""" Checks the validity of a Headsclae API key and renews it if it's nearing expiration """
2023-02-06 04:58:09 +00:00
api_key = headscale.get_api_key()
url = headscale.get_url()
2023-02-17 22:24:58 +09:00
# Test the API key. If the test fails, return a failure.
2023-02-06 04:58:09 +00:00
# AKA, if headscale returns Unauthorized, fail:
status = headscale.test_api_key(url, api_key)
if status != 200: return False
else:
# Check if the key needs to be renewed
2023-02-17 22:24:58 +09:00
headscale.renew_api_key(url, api_key)
2023-02-06 04:58:09 +00:00
return True
2023-02-17 22:24:58 +09:00
def get_color(import_id, item_type = ""):
""" Sets colors for users/namespaces """
2023-02-06 04:58:09 +00:00
# Define the colors... Seems like a good number to start with
2023-02-17 22:24:58 +09:00
if item_type == "text":
2023-02-06 04:58:09 +00:00
colors = [
"red-text text-lighten-1",
"teal-text text-lighten-1",
"blue-text text-lighten-1",
"blue-grey-text text-lighten-1",
"indigo-text text-lighten-2",
"green-text text-lighten-1",
"deep-orange-text text-lighten-1",
"yellow-text text-lighten-2",
"purple-text text-lighten-2",
"indigo-text text-lighten-2",
"brown-text text-lighten-1",
"grey-text text-lighten-1",
]
2023-02-17 22:24:58 +09:00
index = import_id % len(colors)
2023-02-06 04:58:09 +00:00
return colors[index]
2023-02-17 22:24:58 +09:00
colors = [
"red lighten-1",
"teal lighten-1",
"blue lighten-1",
"blue-grey lighten-1",
"indigo lighten-2",
"green lighten-1",
"deep-orange lighten-1",
"yellow lighten-2",
"purple lighten-2",
"indigo lighten-2",
"brown lighten-1",
"grey lighten-1",
]
index = import_id % len(colors)
return colors[index]
2023-02-20 17:23:05 +09:00
def format_message(error_type, title, message):
2023-02-17 22:24:58 +09:00
""" Defines a generic 'collection' as error/warning/info messages """
2023-02-15 12:31:22 +09:00
content = """
<ul class="collection">
<li class="collection-item avatar">
"""
2023-02-17 22:24:58 +09:00
match error_type.lower():
2023-02-15 12:31:22 +09:00
case "warning":
icon = """<i class="material-icons circle yellow">priority_high</i>"""
2023-02-15 13:31:28 +09:00
title = """<span class="title">Warning - """+title+"""</span>"""
2023-02-15 12:31:22 +09:00
case "success":
icon = """<i class="material-icons circle green">check</i>"""
2023-02-15 13:31:28 +09:00
title = """<span class="title">Success - """+title+"""</span>"""
2023-02-15 12:31:22 +09:00
case "error":
icon = """<i class="material-icons circle red">warning</i>"""
2023-02-15 13:31:28 +09:00
title = """<span class="title">Error - """+title+"""</span>"""
2023-02-15 12:31:22 +09:00
case "information":
icon = """<i class="material-icons circle grey">help</i>"""
2023-02-15 13:31:28 +09:00
title = """<span class="title">Information - """+title+"""</span>"""
2023-02-15 12:31:22 +09:00
2023-02-17 22:24:58 +09:00
content = content+icon+title+message
2023-02-15 12:31:22 +09:00
content = content+"""
</li>
</ul>
"""
2023-02-15 20:33:25 +09:00
2023-02-15 12:31:22 +09:00
return content
2023-02-16 21:48:05 +09:00
def access_checks():
2023-02-17 22:24:58 +09:00
""" Checks various items before each page load to ensure permissions are correct """
2023-02-15 12:31:22 +09:00
url = headscale.get_url()
2023-02-17 22:24:58 +09:00
# Return an error message if things fail.
2023-02-15 12:31:22 +09:00
# Return a formatted error message for EACH fail.
2023-02-15 20:58:27 +09:00
checks_passed = True # Default to true. Set to false when any checks fail.
data_readable = False # Checks R permissions of /data
data_writable = False # Checks W permissions of /data
2023-02-17 22:24:58 +09:00
data_executable = False # Execute on directories allows file access
2023-02-15 20:58:27 +09:00
file_readable = False # Checks R permissions of /data/key.txt
file_writable = False # Checks W permissions of /data/key.txt
file_exists = False # Checks if /data/key.txt exists
config_readable = False # Checks if the headscale configuration file is readable
2023-02-15 12:31:22 +09:00
2023-02-15 20:58:27 +09:00
# Check 1: Check: the Headscale server is reachable:
2023-02-15 15:50:15 +09:00
server_reachable = False
2023-02-15 13:36:05 +09:00
response = requests.get(str(url)+"/health")
2023-02-15 20:13:27 +09:00
if response.status_code == 200:
server_reachable = True
2023-02-17 22:24:58 +09:00
else:
2023-02-15 20:27:30 +09:00
checks_passed = False
2023-02-17 22:41:41 +09:00
LOG.error("Headscale URL: Response 200: FAILED")
2023-02-15 20:27:30 +09:00
2023-02-15 20:58:27 +09:00
# Check: /data is rwx for 1000:1000:
if os.access('/data/', os.R_OK): data_readable = True
2023-02-15 20:27:30 +09:00
else:
2023-02-17 22:41:41 +09:00
LOG.error("/data READ: FAILED")
2023-02-15 20:27:30 +09:00
checks_passed = False
2023-02-15 20:58:27 +09:00
if os.access('/data/', os.W_OK): data_writable = True
2023-02-15 20:27:30 +09:00
else:
2023-02-17 22:41:41 +09:00
LOG.error("/data WRITE: FAILED")
2023-02-15 20:27:30 +09:00
checks_passed = False
2023-02-15 20:58:27 +09:00
if os.access('/data/', os.X_OK): data_executable = True
2023-02-15 20:45:51 +09:00
else:
2023-02-17 22:41:41 +09:00
LOG.error("/data EXEC: FAILED")
2023-02-15 20:45:51 +09:00
checks_passed = False
2023-02-15 14:43:38 +09:00
2023-02-15 20:58:27 +09:00
# Check: /data/key.txt exists and is rw:
2023-02-17 22:24:58 +09:00
if os.access('/data/key.txt', os.F_OK):
2023-02-15 20:33:47 +09:00
file_exists = True
2023-02-15 20:58:27 +09:00
if os.access('/data/key.txt', os.R_OK): file_readable = True
2023-02-15 20:27:30 +09:00
else:
2023-02-17 22:41:41 +09:00
LOG.error("/data/key.txt READ: FAILED")
2023-02-15 20:27:30 +09:00
checks_passed = False
2023-02-15 20:58:27 +09:00
if os.access('/data/key.txt', os.W_OK): file_writable = True
2023-02-15 20:27:30 +09:00
else:
2023-02-17 22:41:41 +09:00
LOG.error("/data/key.txt WRITE: FAILED")
2023-02-15 20:27:30 +09:00
checks_passed = False
2023-02-17 22:41:41 +09:00
else: LOG.error("/data/key.txt EXIST: FAILED - NO ERROR")
2023-02-15 20:58:27 +09:00
# Check: /etc/headscale/config.yaml is readable:
if os.access('/etc/headscale/config.yaml', os.R_OK): config_readable = True
elif os.access('/etc/headscale/config.yml', os.R_OK): config_readable = True
2023-02-17 22:24:58 +09:00
else:
2023-02-17 22:41:41 +09:00
LOG.error("/etc/headscale/config.y(a)ml: READ: FAILED")
2023-02-15 20:27:30 +09:00
checks_passed = False
2023-02-15 19:53:46 +09:00
2023-02-17 22:24:58 +09:00
if checks_passed:
2023-02-17 22:41:41 +09:00
LOG.error("All startup checks passed.")
2023-02-16 21:39:54 +09:00
return "Pass"
2023-02-15 12:31:22 +09:00
2023-02-17 22:24:58 +09:00
message_html = ""
2023-02-15 12:31:22 +09:00
# Generate the message:
2023-02-15 14:43:38 +09:00
if not server_reachable:
2023-02-17 22:41:41 +09:00
LOG.error("Server is unreachable")
2023-02-15 12:31:22 +09:00
message = """
2023-02-17 22:24:58 +09:00
<p>Your headscale server is either unreachable or not properly configured.
Please ensure your configuration is correct (Check for 200 status on
2023-02-15 13:51:14 +09:00
"""+url+"""/api/v1 failed. Response: """+str(response.status_code)+""".)</p>
2023-02-15 12:31:22 +09:00
"""
2023-02-15 20:33:25 +09:00
2023-02-20 17:23:05 +09:00
message_html += format_message("Error", "Headscale unreachable", message)
2023-02-15 19:53:46 +09:00
if not config_readable:
2023-02-17 22:41:41 +09:00
LOG.error("Headscale configuration is not readable")
2023-02-15 19:53:46 +09:00
message = """
2023-02-17 22:24:58 +09:00
<p>/etc/headscale/config.yaml not readable. Please ensure your
headscale configuration file resides in /etc/headscale and
2023-02-15 19:53:46 +09:00
is named "config.yaml" or "config.yml"</p>
"""
2023-02-15 20:33:25 +09:00
2023-02-20 17:23:05 +09:00
message_html += format_message("Error", "/etc/headscale/config.yaml not readable", message)
2023-02-15 19:53:46 +09:00
2023-02-15 14:43:38 +09:00
if not data_writable:
2023-02-17 22:41:41 +09:00
LOG.error("/data folder is not writable")
2023-02-15 12:31:22 +09:00
message = """
2023-02-17 22:24:58 +09:00
<p>/data is not writable. Please ensure your
permissions are correct. /data mount should be writable
2023-02-15 14:43:38 +09:00
by UID/GID 1000:1000.</p>
2023-02-15 12:31:22 +09:00
"""
2023-02-15 20:33:25 +09:00
2023-02-20 17:23:05 +09:00
message_html += format_message("Error", "/data not writable", message)
2023-02-15 20:14:42 +09:00
2023-02-15 14:43:38 +09:00
if not data_readable:
2023-02-17 22:41:41 +09:00
LOG.error("/data folder is not readable")
2023-02-15 14:43:38 +09:00
message = """
2023-02-17 22:24:58 +09:00
<p>/data is not readable. Please ensure your
permissions are correct. /data mount should be readable
2023-02-15 14:43:38 +09:00
by UID/GID 1000:1000.</p>
"""
2023-02-15 20:33:25 +09:00
2023-02-20 17:23:05 +09:00
message_html += format_message("Error", "/data not readable", message)
2023-02-15 14:43:38 +09:00
2023-02-15 20:45:51 +09:00
if not data_executable:
2023-02-17 22:41:41 +09:00
LOG.error("/data folder is not readable")
2023-02-15 20:45:51 +09:00
message = """
2023-02-17 22:24:58 +09:00
<p>/data is not executable. Please ensure your
permissions are correct. /data mount should be readable
2023-02-15 20:45:51 +09:00
by UID/GID 1000:1000. (chown 1000:1000 /path/to/data && chmod -R 755 /path/to/data)</p>
"""
2023-02-20 17:23:05 +09:00
message_html += format_message("Error", "/data not executable", message)
2023-02-15 20:45:51 +09:00
2023-02-15 20:14:42 +09:00
2023-02-17 22:24:58 +09:00
if file_exists:
# If it doesn't exist, we assume the user hasn't created it yet.
# Just redirect to the settings page to enter an API Key
2023-02-15 16:16:02 +09:00
if not file_writable:
2023-02-17 22:41:41 +09:00
LOG.error("/data/key.txt is not writable")
2023-02-15 16:16:02 +09:00
message = """
2023-02-17 22:24:58 +09:00
<p>/data/key.txt is not writable. Please ensure your
permissions are correct. /data mount should be writable
2023-02-15 16:16:02 +09:00
by UID/GID 1000:1000.</p>
"""
2023-02-15 20:33:25 +09:00
2023-02-20 17:23:05 +09:00
message_html += format_message("Error", "/data/key.txt not writable", message)
2023-02-15 20:14:42 +09:00
2023-02-15 16:16:02 +09:00
if not file_readable:
2023-02-17 22:41:41 +09:00
LOG.error("/data/key.txt is not readable")
2023-02-15 16:16:02 +09:00
message = """
2023-02-17 22:24:58 +09:00
<p>/data/key.txt is not readable. Please ensure your
permissions are correct. /data mount should be readable
2023-02-15 16:16:02 +09:00
by UID/GID 1000:1000.</p>
"""
2023-02-15 20:33:25 +09:00
2023-02-20 17:23:05 +09:00
message_html += format_message("Error", "/data/key.txt not readable", message)
2023-02-15 20:14:42 +09:00
2023-02-17 22:24:58 +09:00
return message_html
2023-02-16 20:17:36 +09:00
2023-02-16 21:30:49 +09:00
def load_checks():
2023-02-17 22:24:58 +09:00
""" Bundles all the checks into a single function to call easier """
2023-02-16 20:17:36 +09:00
# General error checks. See the function for more info:
2023-02-16 21:57:46 +09:00
if access_checks() != "Pass": return 'error_page'
2023-02-16 20:17:36 +09:00
# If the API key fails, redirect to the settings page:
2023-02-17 22:24:58 +09:00
if not key_check(): return 'settings_page'
return "Pass"