# pylint: disable=wrong-import-order import os, headscale, requests from flask import Flask from flask.logging import create_logger app = Flask(__name__) LOG = create_logger(app) def pretty_print_duration(duration): """ Prints a duration in human-readable formats """ if int(duration.seconds) > 0: days, seconds = duration.days, duration.seconds hours = (days * 24 + seconds // 3600) mins = (seconds % 3600) // 60 secs = seconds % 60 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" return str(secs ) + " seconds ago" if secs >= 1 or secs == 0 else str(secs ) + " second ago" days, seconds = abs(int(duration.days)), abs(int(duration.seconds)) hours = (days * 24 + seconds // 3600) mins = (seconds % 3600) // 60 secs = seconds % 60 if days > 0: return "in "+str(days ) + " days" if days > 1 else str(days ) + " day" 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" return "in "+ str(secs ) + " seconds" if secs >= 1 or secs == 0 else str(secs ) + " second" def text_color_duration(duration): """ Prints a color based on duratioin (imported as seconds) """ days, seconds = duration.days, duration.seconds hours = (days * 24 + seconds // 3600) mins = ((seconds % 3600) // 60) secs = (seconds % 60) 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 " def key_check(): """ Checks the validity of a Headsclae API key and renews it if it's nearing expiration """ api_key = headscale.get_api_key() url = headscale.get_url() # Test the API key. If the test fails, return a failure. # 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 headscale.renew_api_key(url, api_key) return True def get_color(import_id, item_type = ""): """ Sets colors for users/namespaces """ # Define the colors... Seems like a good number to start with if item_type == "text": 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", ] index = import_id % len(colors) return colors[index] 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] def format_message(error_type, title, message): """ Defines a generic 'collection' as error/warning/info messages """ content = """
Your headscale server is either unreachable or not properly configured. Please ensure your configuration is correct (Check for 200 status on """+url+"""/api/v1 failed. Response: """+str(response.status_code)+""".)
""" message_html += format_message("Error", "Headscale unreachable", message) if not config_readable: LOG.error("Headscale configuration is not readable") message = """/etc/headscale/config.yaml not readable. Please ensure your headscale configuration file resides in /etc/headscale and is named "config.yaml" or "config.yml"
""" message_html += format_message("Error", "/etc/headscale/config.yaml not readable", message) if not data_writable: LOG.error("/data folder is not writable") message = """/data is not writable. Please ensure your permissions are correct. /data mount should be writable by UID/GID 1000:1000.
""" message_html += format_message("Error", "/data not writable", message) if not data_readable: LOG.error("/data folder is not readable") message = """/data is not readable. Please ensure your permissions are correct. /data mount should be readable by UID/GID 1000:1000.
""" message_html += format_message("Error", "/data not readable", message) if not data_executable: LOG.error("/data folder is not readable") message = """/data is not executable. Please ensure your permissions are correct. /data mount should be readable by UID/GID 1000:1000. (chown 1000:1000 /path/to/data && chmod -R 755 /path/to/data)
""" message_html += format_message("Error", "/data not executable", message) 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 if not file_writable: LOG.error("/data/key.txt is not writable") message = """/data/key.txt is not writable. Please ensure your permissions are correct. /data mount should be writable by UID/GID 1000:1000.
""" message_html += format_message("Error", "/data/key.txt not writable", message) if not file_readable: LOG.error("/data/key.txt is not readable") message = """/data/key.txt is not readable. Please ensure your permissions are correct. /data mount should be readable by UID/GID 1000:1000.
""" message_html += format_message("Error", "/data/key.txt not readable", message) return message_html def load_checks(): """ Bundles all the checks into a single function to call easier """ # General error checks. See the function for more info: if access_checks() != "Pass": return 'error_page' # If the API key fails, redirect to the settings page: if not key_check(): return 'settings_page' return "Pass"