2023-02-15 19:20:44 +09:00
|
|
|
import sys, pytz, os, headscale, requests
|
|
|
|
|
from datetime import datetime, timedelta, date
|
|
|
|
|
from dateutil import parser
|
|
|
|
|
from flask import Flask
|
2023-02-06 04:58:09 +00:00
|
|
|
|
2023-02-15 19:20:44 +09:00
|
|
|
app = Flask(__name__)
|
2023-02-15 19:02:14 +09:00
|
|
|
|
2023-02-06 04:58:09 +00:00
|
|
|
def pretty_print_duration(duration):
|
|
|
|
|
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"
|
|
|
|
|
elif hours > 0: return str(hours) + " hours ago" if hours > 1 else str(hours) + " hour ago"
|
|
|
|
|
elif mins > 0: return str(mins ) + " minutes ago" if mins > 1 else str(mins ) + " minute ago"
|
|
|
|
|
else: return str(secs ) + " seconds ago" if secs >= 1 or secs == 0 else str(secs ) + " second ago"
|
|
|
|
|
|
|
|
|
|
def text_color_duration(duration):
|
|
|
|
|
days, seconds = duration.days, duration.seconds
|
|
|
|
|
hours = (days * 24 + seconds // 3600)
|
|
|
|
|
mins = ((seconds % 3600) // 60)
|
|
|
|
|
secs = (seconds % 60)
|
|
|
|
|
if days > 30: return "grey-text "
|
|
|
|
|
elif days > 14: return "red-text text-darken-2 "
|
|
|
|
|
elif days > 5: return "deep-orange-text text-lighten-1"
|
|
|
|
|
elif days > 1: return "deep-orange-text text-lighten-1"
|
|
|
|
|
elif hours > 12: return "orange-text "
|
|
|
|
|
elif hours > 1: return "orange-text text-lighten-2"
|
|
|
|
|
elif hours == 1: return "yellow-text "
|
|
|
|
|
elif mins > 15: return "yellow-text text-lighten-2"
|
|
|
|
|
elif mins > 5: return "green-text text-lighten-3"
|
|
|
|
|
elif secs > 30: return "green-text text-lighten-2"
|
|
|
|
|
else: return "green-text "
|
|
|
|
|
|
|
|
|
|
def key_test():
|
|
|
|
|
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(id, type = ""):
|
|
|
|
|
# Define the colors... Seems like a good number to start with
|
|
|
|
|
if 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 = id % len(colors)
|
|
|
|
|
return colors[index]
|
|
|
|
|
else:
|
|
|
|
|
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 = id % len(colors)
|
2023-02-15 12:31:22 +09:00
|
|
|
return colors[index]
|
|
|
|
|
|
2023-02-15 13:20:08 +09:00
|
|
|
def format_error_message(type, title, message):
|
2023-02-15 12:31:22 +09:00
|
|
|
content = """
|
|
|
|
|
<ul class="collection">
|
|
|
|
|
<li class="collection-item avatar">
|
|
|
|
|
"""
|
|
|
|
|
match type.lower():
|
|
|
|
|
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
|
|
|
|
|
|
|
|
content = content+icon+title+message
|
|
|
|
|
content = content+"""
|
|
|
|
|
</li>
|
|
|
|
|
</ul>
|
|
|
|
|
"""
|
|
|
|
|
return content
|
|
|
|
|
|
|
|
|
|
def startup_checks():
|
|
|
|
|
url = headscale.get_url()
|
|
|
|
|
|
|
|
|
|
# Return an error message if things fail.
|
|
|
|
|
# Return a formatted error message for EACH fail.
|
2023-02-15 12:46:52 +09:00
|
|
|
checks_passed = True
|
2023-02-15 12:31:22 +09:00
|
|
|
|
|
|
|
|
# Check 1: See if 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:
|
|
|
|
|
app.logger.warning("server response 200: PASS")
|
|
|
|
|
server_reachable = True
|
2023-02-15 14:45:51 +09:00
|
|
|
else: checks_passed = False
|
2023-02-15 12:31:22 +09:00
|
|
|
|
2023-02-15 14:43:38 +09:00
|
|
|
# Check 2 and 3: See if /data/ is rw:
|
2023-02-15 16:27:08 +09:00
|
|
|
data_readable = False
|
|
|
|
|
data_writable = False
|
2023-02-15 20:13:27 +09:00
|
|
|
if os.access('/data/', os.R_OK):
|
|
|
|
|
app.logger.warning("/data READ: PASS")
|
|
|
|
|
data_readable = True
|
2023-02-15 16:27:08 +09:00
|
|
|
else: checks_passed = False
|
2023-02-15 20:13:27 +09:00
|
|
|
if os.access('/data/', os.W_OK):
|
|
|
|
|
app.logger.warning("/data WRITE: PASS")
|
|
|
|
|
data_writable = True
|
2023-02-15 16:27:08 +09:00
|
|
|
else: checks_passed = False
|
2023-02-15 14:43:38 +09:00
|
|
|
|
2023-02-15 18:51:07 +09:00
|
|
|
# Check 4/5/6: See if /data/key.txt exists and is rw:
|
2023-02-15 14:43:38 +09:00
|
|
|
file_readable = False
|
|
|
|
|
file_writable = False
|
2023-02-15 16:16:02 +09:00
|
|
|
file_exists = False
|
|
|
|
|
if os.access('/data/key.txt', os.F_OK):
|
2023-02-15 20:13:27 +09:00
|
|
|
app.logger.warning("/data/key.txt EXIST: PASS")
|
2023-02-15 16:16:02 +09:00
|
|
|
file_exists: True
|
2023-02-15 20:13:27 +09:00
|
|
|
if os.access('/data/key.txt', os.R_OK):
|
|
|
|
|
app.logger.warning("/data/key.txt READ: PASS")
|
|
|
|
|
file_readable = True
|
2023-02-15 16:16:02 +09:00
|
|
|
else: checks_passed = False
|
2023-02-15 20:13:27 +09:00
|
|
|
if os.access('/data/key.txt', os.W_OK):
|
|
|
|
|
app.logger.warning("/data/key.txt WRITE: PASS")
|
|
|
|
|
file_writable = True
|
2023-02-15 16:16:02 +09:00
|
|
|
else: checks_passed = False
|
2023-02-15 14:12:15 +09:00
|
|
|
|
2023-02-15 19:53:46 +09:00
|
|
|
# Check 7: See if /etc/headscale/config.yaml is readable:
|
|
|
|
|
config_readable = False
|
2023-02-15 20:13:27 +09:00
|
|
|
if os.access('/etc/headscale/config.yaml', os.R_OK):
|
|
|
|
|
app.logger.warning("/etc/headscale/config.yaml: READ: PASS")
|
|
|
|
|
config_readable = True
|
|
|
|
|
if os.access('/etc/headscale/config.yml', os.R_OK):
|
|
|
|
|
app.logger.warning("/etc/headscale/config.yml: READ: PASS")
|
|
|
|
|
config_readable = True
|
2023-02-15 19:53:46 +09:00
|
|
|
|
2023-02-15 19:40:18 +09:00
|
|
|
if checks_passed: return "Pass"
|
2023-02-15 12:31:22 +09:00
|
|
|
|
|
|
|
|
messageHTML = ""
|
|
|
|
|
# Generate the message:
|
2023-02-15 14:43:38 +09:00
|
|
|
if not server_reachable:
|
2023-02-15 19:20:44 +09:00
|
|
|
app.logger.error("Server is unreachable")
|
2023-02-15 12:31:22 +09:00
|
|
|
message = """
|
|
|
|
|
<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
|
|
|
"""
|
|
|
|
|
messageHTML += format_error_message("Error", "Headscale unreachable", message)
|
2023-02-15 19:53:46 +09:00
|
|
|
|
|
|
|
|
if not config_readable:
|
|
|
|
|
app.logger.error("Headscale configuration is not readable")
|
|
|
|
|
message = """
|
|
|
|
|
<p>/etc/headscale/config.yaml not readable. Please ensure your
|
|
|
|
|
headscale configuration file resides in /etc/headscale and
|
|
|
|
|
is named "config.yaml" or "config.yml"</p>
|
|
|
|
|
"""
|
|
|
|
|
messageHTML += format_error_message("Error", "/etc/headscale/config.yaml not readable", message)
|
|
|
|
|
|
2023-02-15 14:43:38 +09:00
|
|
|
if not data_writable:
|
2023-02-15 19:20:44 +09:00
|
|
|
app.logger.error("/data folder is not writable")
|
2023-02-15 12:31:22 +09:00
|
|
|
message = """
|
2023-02-15 18:51:07 +09:00
|
|
|
<p>/data is not writable. Please ensure your
|
2023-02-15 12:31:22 +09:00
|
|
|
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
|
|
|
"""
|
|
|
|
|
messageHTML += format_error_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-15 19:53:46 +09:00
|
|
|
app.logger.error("/data folder is not readable")
|
2023-02-15 14:43:38 +09:00
|
|
|
message = """
|
2023-02-15 18:51:07 +09:00
|
|
|
<p>/data is not readable. Please ensure your
|
2023-02-15 14:43:38 +09:00
|
|
|
permissions are correct. /data mount should be readable
|
|
|
|
|
by UID/GID 1000:1000.</p>
|
|
|
|
|
"""
|
|
|
|
|
messageHTML += format_error_message("Error", "/data not readable", message)
|
|
|
|
|
|
2023-02-15 20:14:42 +09:00
|
|
|
|
2023-02-15 18:51:07 +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-15 19:20:44 +09:00
|
|
|
app.logger.error("/data/key.txt is not writable")
|
2023-02-15 16:16:02 +09:00
|
|
|
message = """
|
|
|
|
|
<p>/data/key.txt is not writable. Please ensure your
|
|
|
|
|
permissions are correct. /data mount should be writable
|
|
|
|
|
by UID/GID 1000:1000.</p>
|
|
|
|
|
"""
|
|
|
|
|
messageHTML += format_error_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-15 19:53:46 +09:00
|
|
|
app.logger.error("/data/key.txt is not readable")
|
2023-02-15 16:16:02 +09:00
|
|
|
message = """
|
|
|
|
|
<p>/data/key.txt is not readable. Please ensure your
|
|
|
|
|
permissions are correct. /data mount should be readable
|
|
|
|
|
by UID/GID 1000:1000.</p>
|
|
|
|
|
"""
|
|
|
|
|
messageHTML += format_error_message("Error", "/data/key.txt not readable", message)
|
2023-02-15 20:14:42 +09:00
|
|
|
|
2023-02-15 12:31:22 +09:00
|
|
|
return messageHTML
|