diff --git a/lib.py b/lib.py index 1b3280e..e26daeb 100644 --- a/lib.py +++ b/lib.py @@ -5,8 +5,9 @@ import yaml logging.basicConfig( level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%d-%m-%Y %H:%M:%S') + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", +) class ServerDict(TypedDict): @@ -25,33 +26,40 @@ class ServerDict(TypedDict): class Server: - def __init__(self, - url: str, - cert: str, - comment: str, - # read from config. not the same as real server id you can get from api - local_server_id: str, - ): + def __init__( + self, + url: str, + cert: str, + comment: str, + # read from config. not the same as real server id you can get from api + local_server_id: str, + ): self.client = OutlineVPN(api_url=url, cert_sha256=cert) self.data: ServerDict = { - 'local_server_id': local_server_id, - 'name': self.client.get_server_information()["name"], - 'url': url, - 'cert': cert, - 'comment': comment, - 'server_id': self.client.get_server_information()["serverId"], - 'metrics_enabled': self.client.get_server_information()["metricsEnabled"], - 'created_timestamp_ms': self.client.get_server_information()["createdTimestampMs"], - 'version': self.client.get_server_information()["version"], - 'port_for_new_access_keys': self.client.get_server_information()["portForNewAccessKeys"], - 'hostname_for_access_keys': self.client.get_server_information()["hostnameForAccessKeys"], - 'keys': self.client.get_keys() + "local_server_id": local_server_id, + "name": self.client.get_server_information()["name"], + "url": url, + "cert": cert, + "comment": comment, + "server_id": self.client.get_server_information()["serverId"], + "metrics_enabled": self.client.get_server_information()["metricsEnabled"], + "created_timestamp_ms": self.client.get_server_information()[ + "createdTimestampMs" + ], + "version": self.client.get_server_information()["version"], + "port_for_new_access_keys": self.client.get_server_information()[ + "portForNewAccessKeys" + ], + "hostname_for_access_keys": self.client.get_server_information()[ + "hostnameForAccessKeys" + ], + "keys": self.client.get_keys(), } self.log = logging.getLogger(f'OutFleet.server[{self.data["name"]}]') def info(self) -> ServerDict: return self.data - + def check_client(self, name): # Looking for any users with provided name. len(result) != 1 is a problem. result = [] @@ -60,7 +68,9 @@ class Server: result.append(name) self.log.info(f"check_client found client `{name}` config is correct.") if len(result) != 1: - self.log.warning(f"check_client found client `{name}` inconsistent. Found {len(result)} keys.") + self.log.warning( + f"check_client found client `{name}` inconsistent. Found {len(result)} keys." + ) return False else: return True @@ -68,23 +78,47 @@ class Server: def apply_config(self, config, CFG_PATH): if config.get("name"): self.client.set_server_name(config.get("name")) - self.log.info("Changed %s name to '%s'", self.data["server_id"], config.get("name")) + self.log.info( + "Changed %s name to '%s'", self.data["server_id"], config.get("name") + ) if config.get("metrics"): - self.client.set_metrics_status(True if config.get("metrics") == 'True' else False) - self.log.info("Changed %s metrics status to '%s'", self.data["server_id"], config.get("metrics")) + self.client.set_metrics_status( + True if config.get("metrics") == "True" else False + ) + self.log.info( + "Changed %s metrics status to '%s'", + self.data["server_id"], + config.get("metrics"), + ) if config.get("port_for_new_access_keys"): - self.client.set_port_new_for_access_keys(int(config.get("port_for_new_access_keys"))) - self.log.info("Changed %s port_for_new_access_keys to '%s'", self.data["server_id"], config.get("port_for_new_access_keys")) + self.client.set_port_new_for_access_keys( + int(config.get("port_for_new_access_keys")) + ) + self.log.info( + "Changed %s port_for_new_access_keys to '%s'", + self.data["server_id"], + config.get("port_for_new_access_keys"), + ) if config.get("hostname_for_access_keys"): self.client.set_hostname(config.get("hostname_for_access_keys")) - self.log.info("Changed %s hostname_for_access_keys to '%s'", self.data["server_id"], config.get("hostname_for_access_keys")) + self.log.info( + "Changed %s hostname_for_access_keys to '%s'", + self.data["server_id"], + config.get("hostname_for_access_keys"), + ) if config.get("comment"): with open(CFG_PATH, "r") as file: config_file = yaml.safe_load(file) or {} - config_file["servers"][self.data['server_id']]['comment'] = config.get("comment") + config_file["servers"][self.data["server_id"]]["comment"] = config.get( + "comment" + ) with open(CFG_PATH, "w") as file: yaml.safe_dump(config_file, file) - self.log.info("Changed %s comment to '%s'", self.data["server_id"], config.get("comment")) + self.log.info( + "Changed %s comment to '%s'", + self.data["server_id"], + config.get("comment"), + ) def create_key(self, key_name): self.log.info("New key created: %s", key_name) @@ -96,4 +130,4 @@ class Server: def delete_key(self, key_id): self.log.info("Key %s deleted", key_id) - return self.client.delete_key(key_id) \ No newline at end of file + return self.client.delete_key(key_id) diff --git a/main.py b/main.py index 6142335..30352dc 100644 --- a/main.py +++ b/main.py @@ -10,33 +10,40 @@ from flask import Flask, render_template, request, url_for, redirect from flask_cors import CORS from lib import Server -logging.getLogger('werkzeug').setLevel(logging.ERROR) +logging.getLogger("werkzeug").setLevel(logging.ERROR) parser = argparse.ArgumentParser() -parser.add_argument("-c", "--config", default="/usr/local/etc/outfleet/config.yaml", help="Config file location") +parser.add_argument( + "-c", + "--config", + default="/usr/local/etc/outfleet/config.yaml", + help="Config file location", +) args = parser.parse_args() CFG_PATH = args.config logging.basicConfig( level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - datefmt='%d-%m-%Y %H:%M:%S') + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", +) -log = logging.getLogger('OutFleet') +log = logging.getLogger("OutFleet") SERVERS = list() CLIENTS = dict() -HOSTNAME = '' +HOSTNAME = "" app = Flask(__name__) CORS(app) + def format_timestamp(ts): - return datetime.fromtimestamp(ts // 1000).strftime('%Y-%m-%d %H:%M:%S') + return datetime.fromtimestamp(ts // 1000).strftime("%Y-%m-%d %H:%M:%S") def random_string(length=64): letters = string.ascii_letters + string.digits - return ''.join(random.choice(letters) for i in range(length)) + return "".join(random.choice(letters) for i in range(length)) def update_state(): @@ -57,44 +64,59 @@ def update_state(): log.error(f"Couldn't create config. {exp}") if config: - HOSTNAME = config.get('ui_hostname', 'my-own-SSL-ENABLED-domain.com') - servers = config.get('servers', dict()) + HOSTNAME = config.get("ui_hostname", "my-own-SSL-ENABLED-domain.com") + servers = config.get("servers", dict()) for local_server_id, server_config in servers.items(): try: - server = Server(url=server_config["url"], cert=server_config["cert"], comment=server_config["comment"], local_server_id=local_server_id) + server = Server( + url=server_config["url"], + cert=server_config["cert"], + comment=server_config["comment"], + local_server_id=local_server_id, + ) SERVERS.append(server) - log.info("Server state updated: %s, [%s]", server.info()["name"], local_server_id) + log.info( + "Server state updated: %s, [%s]", + server.info()["name"], + local_server_id, + ) except Exception as e: log.warning("Can't access server: %s - %s", server_config["url"], e) - CLIENTS = config.get('clients', dict()) + CLIENTS = config.get("clients", dict()) -@app.route('/', methods=['GET', 'POST']) +@app.route("/", methods=["GET", "POST"]) def index(): - if request.method == 'GET': + if request.method == "GET": return render_template( - 'index.html', + "index.html", SERVERS=SERVERS, - nt=request.args.get('nt'), - nl=request.args.get('nl'), - selected_server=request.args.get('selected_server'), - add_server=request.args.get('add_server', None), + nt=request.args.get("nt"), + nl=request.args.get("nl"), + selected_server=request.args.get("selected_server"), + add_server=request.args.get("add_server", None), format_timestamp=format_timestamp, ) - elif request.method == 'POST': - server = request.form['server_id'] - server = next((item for item in SERVERS if item.info()["server_id"] == server), None) + elif request.method == "POST": + server = request.form["server_id"] + server = next( + (item for item in SERVERS if item.info()["server_id"] == server), None + ) server.apply_config(request.form, CFG_PATH) update_state() return redirect( - url_for('index', nt="Updated Outline VPN Server", selected_server=request.args.get('selected_server'))) + url_for( + "index", + nt="Updated Outline VPN Server", + selected_server=request.args.get("selected_server"), + ) + ) else: - return redirect( - url_for('index')) + return redirect(url_for("index")) -@app.route('/clients', methods=['GET', 'POST']) +@app.route("/clients", methods=["GET", "POST"]) def clients(): # {% for server in SERVERS %} # {% for key in server.data["keys"] %} @@ -103,15 +125,15 @@ def clients(): # {% endif %} # {% endfor %} # {% endfor %} - if request.method == 'GET': + if request.method == "GET": return render_template( - 'clients.html', + "clients.html", SERVERS=SERVERS, CLIENTS=CLIENTS, - nt=request.args.get('nt'), - nl=request.args.get('nl'), - selected_client=request.args.get('selected_client'), - add_client=request.args.get('add_client', None), + nt=request.args.get("nt"), + nl=request.args.get("nl"), + selected_client=request.args.get("selected_client"), + add_client=request.args.get("add_client", None), format_timestamp=format_timestamp, dynamic_hostname=HOSTNAME, ) @@ -124,171 +146,239 @@ def clients(): # url_for('index', nt="Updated Outline VPN Server", selected_server=request.args.get('selected_server'))) -@app.route('/add_server', methods=['POST']) +@app.route("/add_server", methods=["POST"]) def add_server(): - if request.method == 'POST': + if request.method == "POST": try: with open(CFG_PATH, "r") as file: config = yaml.safe_load(file) or {} - servers = config.get('servers', dict()) + servers = config.get("servers", dict()) local_server_id = uuid.uuid4() - new_server = Server(url=request.form['url'], cert=request.form['cert'], comment=request.form['comment'], local_server_id=local_server_id) + new_server = Server( + url=request.form["url"], + cert=request.form["cert"], + comment=request.form["comment"], + local_server_id=local_server_id, + ) servers[new_server.data["local_server_id"]] = { - 'name': new_server.data["name"], - 'url': new_server.data["url"], - 'comment': new_server.data["comment"], - 'cert': request.form['cert'] + "name": new_server.data["name"], + "url": new_server.data["url"], + "comment": new_server.data["comment"], + "cert": request.form["cert"], } config["servers"] = servers with open(CFG_PATH, "w") as file: yaml.safe_dump(config, file) log.info("Added server: %s", new_server.data["name"]) update_state() - return redirect(url_for('index', nt="Added Outline VPN Server")) + return redirect(url_for("index", nt="Added Outline VPN Server")) except Exception as e: - return redirect(url_for('index', nt=f"Couldn't access Outline VPN Server: {e}", nl="error")) + return redirect( + url_for( + "index", nt=f"Couldn't access Outline VPN Server: {e}", nl="error" + ) + ) -@app.route('/add_client', methods=['POST']) +@app.route("/add_client", methods=["POST"]) def add_client(): - if request.method == 'POST': + if request.method == "POST": with open(CFG_PATH, "r") as file: config = yaml.safe_load(file) or {} - clients = config.get('clients', dict()) - user_id = request.form.get('user_id', random_string()) + clients = config.get("clients", dict()) + user_id = request.form.get("user_id", random_string()) clients[user_id] = { - 'name': request.form.get('name'), - 'comment': request.form.get('comment'), - 'servers': request.form.getlist('servers') + "name": request.form.get("name"), + "comment": request.form.get("comment"), + "servers": request.form.getlist("servers"), } config["clients"] = clients with open(CFG_PATH, "w") as file: yaml.safe_dump(config, file) - log.info("Client %s updated", request.form.get('name')) + log.info("Client %s updated", request.form.get("name")) for server in SERVERS: - if server.data["local_server_id"] in request.form.getlist('servers'): - client = next((item for item in server.data["keys"] if item.name == request.form.get('old_name')), None) + if server.data["local_server_id"] in request.form.getlist("servers"): + client = next( + ( + item + for item in server.data["keys"] + if item.name == request.form.get("old_name") + ), + None, + ) if client: - if client.name == request.form.get('name'): + if client.name == request.form.get("name"): pass else: - server.rename_key(client.key_id, request.form.get('name')) - log.info("Renaming key %s to %s on server %s", - request.form.get('old_name'), - request.form.get('name'), - server.data["name"]) + server.rename_key(client.key_id, request.form.get("name")) + log.info( + "Renaming key %s to %s on server %s", + request.form.get("old_name"), + request.form.get("name"), + server.data["name"], + ) else: - server.create_key(request.form.get('name')) - log.info("Creating key %s on server %s", request.form.get('name'), server.data["name"]) + server.create_key(request.form.get("name")) + log.info( + "Creating key %s on server %s", + request.form.get("name"), + server.data["name"], + ) else: - client = next((item for item in server.data["keys"] if item.name == request.form.get('old_name')), None) + client = next( + ( + item + for item in server.data["keys"] + if item.name == request.form.get("old_name") + ), + None, + ) if client: server.delete_key(client.key_id) - log.info("Deleting key %s on server %s", request.form.get('name'), server.data["name"]) + log.info( + "Deleting key %s on server %s", + request.form.get("name"), + server.data["name"], + ) update_state() - return redirect(url_for('clients', nt="Clients updated", selected_client=request.form.get('user_id'))) + return redirect( + url_for( + "clients", + nt="Clients updated", + selected_client=request.form.get("user_id"), + ) + ) else: - return redirect(url_for('clients')) + return redirect(url_for("clients")) -@app.route('/del_client', methods=['POST']) +@app.route("/del_client", methods=["POST"]) def del_client(): - if request.method == 'POST': + if request.method == "POST": with open(CFG_PATH, "r") as file: config = yaml.safe_load(file) or {} - clients = config.get('clients', dict()) - user_id = request.form.get('user_id') + clients = config.get("clients", dict()) + user_id = request.form.get("user_id") if user_id in clients: for server in SERVERS: - client = next((item for item in server.data["keys"] if item.name == request.form.get('name')), None) + client = next( + ( + item + for item in server.data["keys"] + if item.name == request.form.get("name") + ), + None, + ) if client: server.delete_key(client.key_id) config["clients"].pop(user_id) with open(CFG_PATH, "w") as file: yaml.safe_dump(config, file) - log.info("Deleting client %s", request.form.get('name')) + log.info("Deleting client %s", request.form.get("name")) update_state() - return redirect(url_for('clients', nt="User has been deleted")) + return redirect(url_for("clients", nt="User has been deleted")) -@app.route('/dynamic//', methods=['GET'], strict_slashes=False) +@app.route("/dynamic//", methods=["GET"], strict_slashes=False) def dynamic(server_name, client_id): try: - client = next((keys for client, keys in CLIENTS.items() if client == client_id), None) - server = next((item for item in SERVERS if item.info()["name"] == server_name), None) - key = next((item for item in server.data["keys"] if item.name == client["name"]), None) + client = next( + (keys for client, keys in CLIENTS.items() if client == client_id), None + ) + server = next( + (item for item in SERVERS if item.info()["name"] == server_name), None + ) + key = next( + (item for item in server.data["keys"] if item.name == client["name"]), None + ) if server and client and key: if server.data["local_server_id"] in client["servers"]: - log.info("Client %s wants ssconf for %s", client["name"], server.data["name"]) + log.info( + "Client %s wants ssconf for %s", client["name"], server.data["name"] + ) return { - "server": server.data["hostname_for_access_keys"], - "server_port": key.port, - "password": key.password, - "method": key.method, - "info": "Managed by OutFleet [github.com/house-of-vanity/OutFleet/]" + "server": server.data["hostname_for_access_keys"], + "server_port": key.port, + "password": key.password, + "method": key.method, + "info": "Managed by OutFleet [github.com/house-of-vanity/OutFleet/]", } else: - log.warning("Hack attempt! Client %s denied by ACL on %s", client["name"], server.data["name"]) + log.warning( + "Hack attempt! Client %s denied by ACL on %s", + client["name"], + server.data["name"], + ) return "Hey buddy, i think you got the wrong door the leather-club is two blocks down" except: log.warning("Hack attempt! Client or server doesn't exist. SCAM") return "Hey buddy, i think you got the wrong door the leather-club is two blocks down" - -@app.route('/dynamic/', methods=['GET'], strict_slashes=False) + +@app.route("/dynamic/", methods=["GET"], strict_slashes=False) def _dynamic(): log.warning("Hack attempt! Client or server doesn't exist. SCAM") - return "Hey buddy, i think you got the wrong door the leather-club is two blocks down" + return ( + "Hey buddy, i think you got the wrong door the leather-club is two blocks down" + ) -@app.route('/sync', methods=['GET', 'POST']) + +@app.route("/sync", methods=["GET", "POST"]) def sync(): - if request.method == 'GET': + if request.method == "GET": try: - with open('sync.log', 'r') as file: + with open("sync.log", "r") as file: lines = file.readlines() except: lines = [] return render_template( - 'sync.html', + "sync.html", SERVERS=SERVERS, CLIENTS=CLIENTS, lines=lines, ) - if request.method == 'POST': - log = logging.getLogger('sync') - file_handler = logging.FileHandler('sync.log') + if request.method == "POST": + log = logging.getLogger("sync") + file_handler = logging.FileHandler("sync.log") file_handler.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) file_handler.setFormatter(formatter) log.addHandler(file_handler) server_hash = {} for server in SERVERS: - server_hash[server.data['local_server_id']] = server + server_hash[server.data["local_server_id"]] = server for key, client in CLIENTS.items(): log.info(f"Sync client `{client['name']}`") - for u_server_id in client['servers']: - if u_server_id in server_hash: - if not server_hash[u_server_id].check_client(client['name']): - log.warning(f"Client `{client['name']}` absent on `{server_hash[u_server_id].data['name']}`") - server_hash[u_server_id].create_key(client['name']) + for u_server_id in client["servers"]: + if u_server_id in server_hash: + if not server_hash[u_server_id].check_client(client["name"]): + log.warning( + f"Client `{client['name']}` absent on `{server_hash[u_server_id].data['name']}`" + ) + server_hash[u_server_id].create_key(client["name"]) else: - log.info(f"Client `{client['name']}` presented on `{server_hash[u_server_id].data['name']}`") + log.info( + f"Client `{client['name']}` presented on `{server_hash[u_server_id].data['name']}`" + ) else: - log.info(f"Client `{client['name']}` incorrect server_id `{u_server_id}`") + log.info( + f"Client `{client['name']}` incorrect server_id `{u_server_id}`" + ) update_state() - return redirect(url_for('sync')) + return redirect(url_for("sync")) - -if __name__ == '__main__': +if __name__ == "__main__": update_state() - app.run(host='0.0.0.0') + app.run(host="0.0.0.0")