Files
headscale-webui/server.py

419 lines
16 KiB
Python
Raw Normal View History

2023-02-20 17:23:05 +09:00
# pylint: disable=wrong-import-order
2023-02-21 20:59:05 +09:00
import headscale, helper, json, os, pytz, renderer, secrets, urllib
2023-02-21 21:50:36 +09:00
from werkzeug import wrappers
2023-02-21 20:59:05 +09:00
from functools import wraps
2023-02-20 20:08:39 +09:00
from flask import Flask, Markup, redirect, render_template, request, url_for, logging
2023-02-15 19:32:27 +09:00
from dateutil import parser
2023-02-17 19:02:29 +09:00
from flask_executor import Executor
2023-02-06 04:58:09 +00:00
2023-02-21 20:59:05 +09:00
from werkzeug.middleware.proxy_fix import ProxyFix
2023-02-06 04:58:09 +00:00
# Global vars
# Colors: https://materializecss.com/color.html
2023-02-20 21:07:32 +09:00
COLOR = os.environ["COLOR"].replace('"', '')
COLOR_NAV = COLOR+" darken-1"
COLOR_BTN = COLOR+" darken-3"
2023-02-20 19:39:36 +09:00
DEBUG_STATE = True
2023-02-20 21:49:46 +09:00
AUTH_TYPE = os.environ["AUTH_TYPE"].replace('"', '').lower()
2023-02-17 19:58:50 +09:00
STATIC_URL_PATH = "/static"
2023-02-06 04:58:09 +00:00
2023-02-19 19:58:56 +09:00
# Initiate the Flask application:
app = Flask(__name__, static_url_path=STATIC_URL_PATH)
2023-02-21 21:03:02 +09:00
LOG = logging.create_logger(app)
executor = Executor(app)
2023-02-21 20:59:05 +09:00
app.wsgi_app = ProxyFix(
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
)
2023-02-19 19:58:56 +09:00
2023-02-20 14:39:09 +09:00
########################################################################################
2023-02-20 22:06:20 +09:00
# Set Authentication type. Currently "OIDC" and "BASIC"
2023-02-20 14:39:09 +09:00
########################################################################################
2023-02-20 21:49:46 +09:00
if AUTH_TYPE == "oidc":
2023-02-20 20:13:31 +09:00
# Currently using: flask-providers-oidc - https://pypi.org/project/flask-providers-oidc/
2023-02-20 20:08:39 +09:00
#
# https://gist.github.com/thomasdarimont/145dc9aa857b831ff2eff221b79d179a/
# https://www.authelia.com/integration/openid-connect/introduction/
# https://github.com/steinarvk/flask_oidc_demo
2023-02-19 19:58:56 +09:00
LOG.error("Loading OIDC libraries and configuring app...")
2023-02-19 20:25:33 +09:00
2023-02-19 19:58:56 +09:00
DOMAIN_NAME = os.environ["DOMAIN_NAME"]
BASE_PATH = os.environ["SCRIPT_NAME"] if os.environ["SCRIPT_NAME"] != "/" else ""
OIDC_ISSUER = os.environ["OIDC_ISSUER"].replace('"','')
OIDC_SECRET = os.environ["OIDC_CLIENT_SECRET"]
OIDC_CLIENT_ID = os.environ["OIDC_CLIENT_ID"]
2023-02-20 20:08:39 +09:00
# Construct client_secrets.json:
2023-02-20 20:19:42 +09:00
client_secrets = """{
2023-02-20 20:08:39 +09:00
"web": {
2023-02-20 20:19:42 +09:00
"issuer": \""""+OIDC_ISSUER+"""",
2023-02-20 20:08:39 +09:00
"auth_uri": \""""+OIDC_ISSUER+"""/api/oidc/authorization",
"client_id": \""""+OIDC_CLIENT_ID+"""",
"client_secret": \""""+OIDC_SECRET+"""",
2023-02-20 20:36:22 +09:00
"redirect_uris": [
2023-02-21 20:51:03 +09:00
\""""+DOMAIN_NAME+BASE_PATH+"""/oidc_callback",
"https://headscale.sysctl.io/admin/oidc_callback"
2023-02-20 20:08:39 +09:00
],
"userinfo_uri": \""""+OIDC_ISSUER+"""/api/oidc/userinfo",
"token_uri": \""""+OIDC_ISSUER+"""/api/oidc/token",
"token_introspection_uri": \""""+OIDC_ISSUER+"""/api/oidc/introspection"
2023-02-19 19:58:56 +09:00
}
2023-02-20 20:08:39 +09:00
}
"""
2023-02-21 16:12:42 +09:00
2023-02-20 20:08:39 +09:00
with open("/app/instance/secrets.json", "w+") as secrets_json:
secrets_json.write(client_secrets)
2023-02-21 20:51:03 +09:00
LOG.debug("Client Secrets: ")
with open("/app/instance/secrets.json", "r+") as secrets_json:
2023-02-21 20:54:07 +09:00
LOG.debug(secrets_json.read())
2023-02-20 20:08:39 +09:00
app.config.update({
'SECRET_KEY': secrets.token_urlsafe(32),
'TESTING': DEBUG_STATE,
'DEBUG': DEBUG_STATE,
'OIDC_CLIENT_SECRETS': '/app/instance/secrets.json',
2023-02-20 20:36:22 +09:00
'OIDC_ID_TOKEN_COOKIE_SECURE': True,
2023-02-20 20:08:39 +09:00
'OIDC_REQUIRE_VERIFIED_EMAIL': False,
'OIDC_USER_INFO_ENABLED': True,
'OIDC_OPENID_REALM': 'Headscale-WebUI',
2023-02-20 20:36:22 +09:00
'OIDC_SCOPES': ['openid', 'profile', 'email'],
2023-02-20 20:08:39 +09:00
'OIDC_INTROSPECTION_AUTH_METHOD': 'client_secret_post'
})
2023-02-20 20:16:50 +09:00
from flask_oidc import OpenIDConnect
2023-02-20 20:08:39 +09:00
oidc = OpenIDConnect(app)
2023-02-19 15:25:08 +09:00
2023-02-20 21:49:46 +09:00
elif AUTH_TYPE == "basic":
2023-02-16 20:17:36 +09:00
# https://flask-basicauth.readthedocs.io/en/latest/
2023-02-19 19:58:56 +09:00
LOG.error("Loading basic auth libraries and configuring app...")
2023-02-16 20:44:33 +09:00
from flask_basicauth import BasicAuth
2023-02-19 19:58:56 +09:00
2023-02-16 20:44:33 +09:00
app.config['BASIC_AUTH_USERNAME'] = os.environ["BASIC_AUTH_USER"].replace('"', '')
app.config['BASIC_AUTH_PASSWORD'] = os.environ["BASIC_AUTH_PASS"]
2023-02-16 21:39:54 +09:00
app.config['BASIC_AUTH_FORCE'] = True
2023-02-16 20:44:33 +09:00
basic_auth = BasicAuth(app)
2023-02-21 20:44:38 +09:00
# Make a fake decorator for oidc.require_login
2023-02-22 09:06:48 +09:00
class oidc(object):
2023-02-22 09:00:08 +09:00
def require_login(self, view_func):
@wraps(view_func)
def decorated(*args, **kwargs): # Do nothing
return view_func(*args, **kwargs)
return decorated
2023-02-21 20:43:50 +09:00
else:
# Make a fake decorator for oidc.require_login
2023-02-22 09:06:48 +09:00
class oidc(object):
2023-02-22 09:00:08 +09:00
def require_login(self, view_func):
@wraps(view_func)
def decorated(*args, **kwargs): # Do nothing
return view_func(*args, **kwargs)
return decorated
2023-02-21 14:43:50 +09:00
########################################################################################
2023-02-21 14:56:23 +09:00
# Set Authentication type - Dynamically load function decorators
2023-02-21 20:24:09 +09:00
# https://stackoverflow.com/questions/17256602/assertionerror-view-function-mapping-is-overwriting-an-existing-endpoint-functi
2023-02-21 14:43:50 +09:00
########################################################################################
2023-02-21 20:43:50 +09:00
#def enable_oidc(func):
# LOG.error("in enable_oidc")
# def wrapper(*args, **kwargs):
# LOG.error("in enable_oidc-wrapper wrapper")
# if AUTH_TYPE == "oidc":
# oidc.require_login(func)
# LOG.error("Applied oidc.require_login to func "+str(func))
# return func(*args, **kwargs)
# LOG.error ("Returning wrapper")
# wrapper.__name__ = func.__name__
# return wrapper
2023-02-13 20:56:31 +09:00
########################################################################################
# / pages - User-facing pages
########################################################################################
2023-02-21 20:20:17 +09:00
@app.route('/', endpoint='overview_page')
@app.route('/overview', endpoint='overview_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def overview_page():
2023-02-16 20:17:36 +09:00
# Some basic sanity checks:
2023-02-16 21:39:54 +09:00
pass_checks = str(helper.load_checks())
2023-02-17 16:55:46 +09:00
if pass_checks != "Pass": return redirect(url_for(pass_checks))
2023-02-06 04:58:09 +00:00
return render_template('overview.html',
render_page = renderer.render_overview(),
COLOR_NAV = COLOR_NAV,
COLOR_BTN = COLOR_BTN
)
2023-02-21 20:20:17 +09:00
@app.route('/machines', methods=('GET', 'POST'), endpoint='machines_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def machines_page():
2023-02-16 20:17:36 +09:00
# Some basic sanity checks:
2023-02-16 21:39:54 +09:00
pass_checks = str(helper.load_checks())
2023-02-17 16:55:46 +09:00
if pass_checks != "Pass": return redirect(url_for(pass_checks))
2023-02-15 12:31:22 +09:00
2023-02-06 04:58:09 +00:00
cards = renderer.render_machines_cards()
return render_template('machines.html',
cards = cards,
headscale_server = headscale.get_url(),
COLOR_NAV = COLOR_NAV,
COLOR_BTN = COLOR_BTN
)
2023-02-21 20:20:17 +09:00
@app.route('/users', methods=('GET', 'POST'), endpoint='users_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def users_page():
2023-02-16 20:17:36 +09:00
# Some basic sanity checks:
2023-02-16 21:39:54 +09:00
pass_checks = str(helper.load_checks())
2023-02-17 16:55:46 +09:00
if pass_checks != "Pass": return redirect(url_for(pass_checks))
2023-02-06 04:58:09 +00:00
cards = renderer.render_users_cards()
return render_template('users.html',
cards = cards,
headscale_server = headscale.get_url(),
COLOR_NAV = COLOR_NAV,
COLOR_BTN = COLOR_BTN
)
2023-02-21 20:20:17 +09:00
@app.route('/settings', methods=('GET', 'POST'), endpoint='settings_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def settings_page():
2023-02-16 20:17:36 +09:00
# Some basic sanity checks:
2023-02-16 21:39:54 +09:00
pass_checks = str(helper.load_checks())
2023-02-17 16:55:46 +09:00
if pass_checks != "Pass": return redirect(url_for(pass_checks))
2023-02-06 04:58:09 +00:00
2023-02-16 20:17:36 +09:00
return render_template('settings.html',
url = headscale.get_url(),
2023-02-15 12:31:22 +09:00
COLOR_NAV = COLOR_NAV,
COLOR_BTN = COLOR_BTN,
2023-02-17 19:30:29 +09:00
BUILD_DATE = os.environ["BUILD_DATE"],
APP_VERSION = os.environ["APP_VERSION"],
GIT_COMMIT = os.environ["GIT_COMMIT"],
GIT_BRANCH = os.environ["GIT_BRANCH"],
HS_VERSION = os.environ["HS_VERSION"]
2023-02-15 12:31:22 +09:00
)
2023-02-21 20:20:17 +09:00
@app.route('/error', endpoint='error_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-15 12:31:22 +09:00
def error_page():
2023-02-16 21:42:54 +09:00
if helper.access_checks() == "Pass":
2023-02-15 14:29:14 +09:00
return redirect(url_for('overview_page'))
2023-02-15 12:31:22 +09:00
return render_template('error.html',
2023-02-16 21:42:54 +09:00
ERROR_MESSAGE = Markup(helper.access_checks())
2023-02-15 12:31:22 +09:00
)
2023-02-06 04:58:09 +00:00
########################################################################################
# /api pages
########################################################################################
########################################################################################
# Headscale API Key Endpoints
########################################################################################
2023-02-21 20:20:17 +09:00
@app.route('/api/test_key', methods=('GET', 'POST'), endpoint='test_key_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def test_key_page():
api_key = headscale.get_api_key()
url = headscale.get_url()
# Test the API key. If the test fails, return a failure.
status = headscale.test_api_key(url, api_key)
if status != 200: return "Unauthenticated"
renewed = headscale.renew_api_key(url, api_key)
2023-02-17 22:41:41 +09:00
LOG.warning("The below statement will be TRUE if the key has been renewed, ")
LOG.warning("or DOES NOT need renewal. False in all other cases")
LOG.warning("Renewed: "+str(renewed))
2023-02-06 04:58:09 +00:00
# The key works, let's renew it if it needs it. If it does, re-read the api_key from the file:
if renewed: api_key = headscale.get_api_key()
key_info = headscale.get_api_key_info(url, api_key)
# Set the current timezone and local time
timezone = pytz.timezone(os.environ["TZ"] if os.environ["TZ"] else "UTC")
2023-02-06 04:58:09 +00:00
# Format the dates for easy readability
expiration_parse = parser.parse(key_info['expiration'])
expiration_local = expiration_parse.astimezone(timezone)
expiration_time = str(expiration_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+" "+str(timezone)
creation_parse = parser.parse(key_info['createdAt'])
creation_local = creation_parse.astimezone(timezone)
creation_time = str(creation_local.strftime('%A %m/%d/%Y, %H:%M:%S'))+" "+str(timezone)
key_info['expiration'] = expiration_time
key_info['createdAt'] = creation_time
message = json.dumps(key_info)
return message
2023-02-21 20:20:17 +09:00
@app.route('/api/save_key', methods=['POST'], endpoint='save_key_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def save_key_page():
json_response = request.get_json()
api_key = json_response['api_key']
url = headscale.get_url()
file_written = headscale.set_api_key(api_key)
message = ''
if file_written:
# Re-read the file and get the new API key and test it
api_key = headscale.get_api_key()
test_status = headscale.test_api_key(url, api_key)
if test_status == 200:
key_info = headscale.get_api_key_info(url, api_key)
expiration = key_info['expiration']
message = "Key: '"+api_key+"', Expiration: "+expiration
# If the key was saved successfully, test it:
return "Key saved and tested: "+message
else: return "Key failed testing. Check your key"
else: return "Key did not save properly. Check logs"
########################################################################################
# Machine API Endpoints
########################################################################################
2023-02-21 20:20:17 +09:00
@app.route('/api/update_route', methods=['POST'], endpoint='update_route_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def update_route_page():
json_response = request.get_json()
route_id = json_response['route_id']
url = headscale.get_url()
api_key = headscale.get_api_key()
current_state = json_response['current_state']
return headscale.update_route(url, api_key, route_id, current_state)
2023-02-21 20:20:17 +09:00
@app.route('/api/machine_information', methods=['POST'], endpoint='machine_information_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def machine_information_page():
json_response = request.get_json()
machine_id = json_response['id']
url = headscale.get_url()
api_key = headscale.get_api_key()
return headscale.get_machine_info(url, api_key, machine_id)
2023-02-21 20:20:17 +09:00
@app.route('/api/delete_machine', methods=['POST'], endpoint='delete_machine_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def delete_machine_page():
json_response = request.get_json()
machine_id = json_response['id']
url = headscale.get_url()
api_key = headscale.get_api_key()
return headscale.delete_machine(url, api_key, machine_id)
2023-02-21 20:20:17 +09:00
@app.route('/api/rename_machine', methods=['POST'], endpoint='rename_machine_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def rename_machine_page():
json_response = request.get_json()
machine_id = json_response['id']
new_name = json_response['new_name']
url = headscale.get_url()
api_key = headscale.get_api_key()
return headscale.rename_machine(url, api_key, machine_id, new_name)
2023-02-21 20:20:17 +09:00
@app.route('/api/move_user', methods=['POST'], endpoint='move_user_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def move_user_page():
json_response = request.get_json()
machine_id = json_response['id']
2023-02-21 20:17:31 +09:00
new_user = json_response['new_user']
2023-02-06 04:58:09 +00:00
url = headscale.get_url()
api_key = headscale.get_api_key()
return headscale.move_user(url, api_key, machine_id, new_user)
2023-02-21 20:20:17 +09:00
@app.route('/api/set_machine_tags', methods=['POST'], endpoint='set_machine_tags')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def set_machine_tags():
json_response = request.get_json()
machine_id = json_response['id']
machine_tags = json_response['tags_list']
url = headscale.get_url()
api_key = headscale.get_api_key()
return headscale.set_machine_tags(url, api_key, machine_id, machine_tags)
2023-02-21 20:20:17 +09:00
@app.route('/api/register_machine', methods=['POST'], endpoint='register_machine')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def register_machine():
json_response = request.get_json()
machine_key = json_response['key']
2023-02-21 20:17:31 +09:00
user = json_response['user']
2023-02-06 04:58:09 +00:00
url = headscale.get_url()
api_key = headscale.get_api_key()
return str(headscale.register_machine(url, api_key, machine_key, user))
########################################################################################
# User API Endpoints
########################################################################################
2023-02-21 20:20:17 +09:00
@app.route('/api/rename_user', methods=['POST'], endpoint='rename_user_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def rename_user_page():
json_response = request.get_json()
old_name = json_response['old_name']
new_name = json_response['new_name']
url = headscale.get_url()
api_key = headscale.get_api_key()
return headscale.rename_user(url, api_key, old_name, new_name)
2023-02-21 20:20:17 +09:00
@app.route('/api/add_user', methods=['POST'], endpoint='add_user')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def add_user():
json_response = json.dumps(request.get_json())
url = headscale.get_url()
api_key = headscale.get_api_key()
return headscale.add_user(url, api_key, json_response)
2023-02-21 20:20:17 +09:00
@app.route('/api/delete_user', methods=['POST'], endpoint='delete_user')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def delete_user():
json_response = request.get_json()
2023-02-21 20:17:31 +09:00
user_name = json_response['name']
2023-02-06 04:58:09 +00:00
url = headscale.get_url()
api_key = headscale.get_api_key()
return headscale.delete_user(url, api_key, user_name)
2023-02-21 20:20:17 +09:00
@app.route('/api/get_users', methods=['POST'], endpoint='get_users_page')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def get_users_page():
url = headscale.get_url()
api_key = headscale.get_api_key()
return headscale.get_users(url, api_key)
########################################################################################
# Pre-Auth Key API Endpoints
########################################################################################
2023-02-21 20:20:17 +09:00
@app.route('/api/add_preauth_key', methods=['POST'], endpoint='add_preauth_key')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def add_preauth_key():
json_response = json.dumps(request.get_json())
url = headscale.get_url()
api_key = headscale.get_api_key()
return headscale.add_preauth_key(url, api_key, json_response)
2023-02-21 20:20:17 +09:00
@app.route('/api/expire_preauth_key', methods=['POST'], endpoint='expire_preauth_key')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def expire_preauth_key():
json_response = json.dumps(request.get_json())
url = headscale.get_url()
api_key = headscale.get_api_key()
return headscale.expire_preauth_key(url, api_key, json_response)
2023-02-21 20:20:17 +09:00
@app.route('/api/build_preauthkey_table', methods=['POST'], endpoint='build_preauth_key_table')
2023-02-21 20:43:50 +09:00
@oidc.require_login
2023-02-06 04:58:09 +00:00
def build_preauth_key_table():
json_response = request.get_json()
user_name = json_response['name']
return renderer.build_preauth_key_table(user_name)
########################################################################################
# Main thread
########################################################################################
2023-02-17 17:37:05 +09:00
if __name__ == '__main__':
2023-02-17 17:01:23 +09:00
app.run(host="0.0.0.0", debug=DEBUG_STATE)