Formatting

This commit is contained in:
2023-12-19 12:35:00 +02:00
parent 2ef4b6a69a
commit 8154f62015
2 changed files with 262 additions and 138 deletions

98
lib.py
View File

@ -5,8 +5,9 @@ import yaml
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt='%d-%m-%Y %H:%M:%S') datefmt="%d-%m-%Y %H:%M:%S",
)
class ServerDict(TypedDict): class ServerDict(TypedDict):
@ -25,33 +26,40 @@ class ServerDict(TypedDict):
class Server: class Server:
def __init__(self, def __init__(
url: str, self,
cert: str, url: str,
comment: str, cert: str,
# read from config. not the same as real server id you can get from api comment: str,
local_server_id: 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.client = OutlineVPN(api_url=url, cert_sha256=cert)
self.data: ServerDict = { self.data: ServerDict = {
'local_server_id': local_server_id, "local_server_id": local_server_id,
'name': self.client.get_server_information()["name"], "name": self.client.get_server_information()["name"],
'url': url, "url": url,
'cert': cert, "cert": cert,
'comment': comment, "comment": comment,
'server_id': self.client.get_server_information()["serverId"], "server_id": self.client.get_server_information()["serverId"],
'metrics_enabled': self.client.get_server_information()["metricsEnabled"], "metrics_enabled": self.client.get_server_information()["metricsEnabled"],
'created_timestamp_ms': self.client.get_server_information()["createdTimestampMs"], "created_timestamp_ms": self.client.get_server_information()[
'version': self.client.get_server_information()["version"], "createdTimestampMs"
'port_for_new_access_keys': self.client.get_server_information()["portForNewAccessKeys"], ],
'hostname_for_access_keys': self.client.get_server_information()["hostnameForAccessKeys"], "version": self.client.get_server_information()["version"],
'keys': self.client.get_keys() "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"]}]') self.log = logging.getLogger(f'OutFleet.server[{self.data["name"]}]')
def info(self) -> ServerDict: def info(self) -> ServerDict:
return self.data return self.data
def check_client(self, name): def check_client(self, name):
# Looking for any users with provided name. len(result) != 1 is a problem. # Looking for any users with provided name. len(result) != 1 is a problem.
result = [] result = []
@ -60,7 +68,9 @@ class Server:
result.append(name) result.append(name)
self.log.info(f"check_client found client `{name}` config is correct.") self.log.info(f"check_client found client `{name}` config is correct.")
if len(result) != 1: 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 return False
else: else:
return True return True
@ -68,23 +78,47 @@ class Server:
def apply_config(self, config, CFG_PATH): def apply_config(self, config, CFG_PATH):
if config.get("name"): if config.get("name"):
self.client.set_server_name(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"): if config.get("metrics"):
self.client.set_metrics_status(True if config.get("metrics") == 'True' else False) self.client.set_metrics_status(
self.log.info("Changed %s metrics status to '%s'", self.data["server_id"], config.get("metrics")) 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"): 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.client.set_port_new_for_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")) 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"): if config.get("hostname_for_access_keys"):
self.client.set_hostname(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"): if config.get("comment"):
with open(CFG_PATH, "r") as file: with open(CFG_PATH, "r") as file:
config_file = yaml.safe_load(file) or {} 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: with open(CFG_PATH, "w") as file:
yaml.safe_dump(config_file, 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): def create_key(self, key_name):
self.log.info("New key created: %s", key_name) self.log.info("New key created: %s", key_name)
@ -96,4 +130,4 @@ class Server:
def delete_key(self, key_id): def delete_key(self, key_id):
self.log.info("Key %s deleted", key_id) self.log.info("Key %s deleted", key_id)
return self.client.delete_key(key_id) return self.client.delete_key(key_id)

302
main.py
View File

@ -10,33 +10,40 @@ from flask import Flask, render_template, request, url_for, redirect
from flask_cors import CORS from flask_cors import CORS
from lib import Server from lib import Server
logging.getLogger('werkzeug').setLevel(logging.ERROR) logging.getLogger("werkzeug").setLevel(logging.ERROR)
parser = argparse.ArgumentParser() 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() args = parser.parse_args()
CFG_PATH = args.config CFG_PATH = args.config
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt='%d-%m-%Y %H:%M:%S') datefmt="%d-%m-%Y %H:%M:%S",
)
log = logging.getLogger('OutFleet') log = logging.getLogger("OutFleet")
SERVERS = list() SERVERS = list()
CLIENTS = dict() CLIENTS = dict()
HOSTNAME = '' HOSTNAME = ""
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
def format_timestamp(ts): 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): def random_string(length=64):
letters = string.ascii_letters + string.digits 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(): def update_state():
@ -57,44 +64,59 @@ def update_state():
log.error(f"Couldn't create config. {exp}") log.error(f"Couldn't create config. {exp}")
if config: if config:
HOSTNAME = config.get('ui_hostname', 'my-own-SSL-ENABLED-domain.com') HOSTNAME = config.get("ui_hostname", "my-own-SSL-ENABLED-domain.com")
servers = config.get('servers', dict()) servers = config.get("servers", dict())
for local_server_id, server_config in servers.items(): for local_server_id, server_config in servers.items():
try: 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) 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: except Exception as e:
log.warning("Can't access server: %s - %s", server_config["url"], 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(): def index():
if request.method == 'GET': if request.method == "GET":
return render_template( return render_template(
'index.html', "index.html",
SERVERS=SERVERS, SERVERS=SERVERS,
nt=request.args.get('nt'), nt=request.args.get("nt"),
nl=request.args.get('nl'), nl=request.args.get("nl"),
selected_server=request.args.get('selected_server'), selected_server=request.args.get("selected_server"),
add_server=request.args.get('add_server', None), add_server=request.args.get("add_server", None),
format_timestamp=format_timestamp, format_timestamp=format_timestamp,
) )
elif request.method == 'POST': elif request.method == "POST":
server = request.form['server_id'] server = request.form["server_id"]
server = next((item for item in SERVERS if item.info()["server_id"] == server), None) server = next(
(item for item in SERVERS if item.info()["server_id"] == server), None
)
server.apply_config(request.form, CFG_PATH) server.apply_config(request.form, CFG_PATH)
update_state() update_state()
return redirect( 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: else:
return redirect( return redirect(url_for("index"))
url_for('index'))
@app.route('/clients', methods=['GET', 'POST']) @app.route("/clients", methods=["GET", "POST"])
def clients(): def clients():
# {% for server in SERVERS %} # {% for server in SERVERS %}
# {% for key in server.data["keys"] %} # {% for key in server.data["keys"] %}
@ -103,15 +125,15 @@ def clients():
# {% endif %} # {% endif %}
# {% endfor %} # {% endfor %}
# {% endfor %} # {% endfor %}
if request.method == 'GET': if request.method == "GET":
return render_template( return render_template(
'clients.html', "clients.html",
SERVERS=SERVERS, SERVERS=SERVERS,
CLIENTS=CLIENTS, CLIENTS=CLIENTS,
nt=request.args.get('nt'), nt=request.args.get("nt"),
nl=request.args.get('nl'), nl=request.args.get("nl"),
selected_client=request.args.get('selected_client'), selected_client=request.args.get("selected_client"),
add_client=request.args.get('add_client', None), add_client=request.args.get("add_client", None),
format_timestamp=format_timestamp, format_timestamp=format_timestamp,
dynamic_hostname=HOSTNAME, dynamic_hostname=HOSTNAME,
) )
@ -124,171 +146,239 @@ def clients():
# 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')))
@app.route('/add_server', methods=['POST']) @app.route("/add_server", methods=["POST"])
def add_server(): def add_server():
if request.method == 'POST': if request.method == "POST":
try: try:
with open(CFG_PATH, "r") as file: with open(CFG_PATH, "r") as file:
config = yaml.safe_load(file) or {} config = yaml.safe_load(file) or {}
servers = config.get('servers', dict()) servers = config.get("servers", dict())
local_server_id = uuid.uuid4() 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"]] = { servers[new_server.data["local_server_id"]] = {
'name': new_server.data["name"], "name": new_server.data["name"],
'url': new_server.data["url"], "url": new_server.data["url"],
'comment': new_server.data["comment"], "comment": new_server.data["comment"],
'cert': request.form['cert'] "cert": request.form["cert"],
} }
config["servers"] = servers config["servers"] = servers
with open(CFG_PATH, "w") as file: with open(CFG_PATH, "w") as file:
yaml.safe_dump(config, file) yaml.safe_dump(config, file)
log.info("Added server: %s", new_server.data["name"]) log.info("Added server: %s", new_server.data["name"])
update_state() 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: 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(): def add_client():
if request.method == 'POST': if request.method == "POST":
with open(CFG_PATH, "r") as file: with open(CFG_PATH, "r") as file:
config = yaml.safe_load(file) or {} config = yaml.safe_load(file) or {}
clients = config.get('clients', dict()) clients = config.get("clients", dict())
user_id = request.form.get('user_id', random_string()) user_id = request.form.get("user_id", random_string())
clients[user_id] = { clients[user_id] = {
'name': request.form.get('name'), "name": request.form.get("name"),
'comment': request.form.get('comment'), "comment": request.form.get("comment"),
'servers': request.form.getlist('servers') "servers": request.form.getlist("servers"),
} }
config["clients"] = clients config["clients"] = clients
with open(CFG_PATH, "w") as file: with open(CFG_PATH, "w") as file:
yaml.safe_dump(config, 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: for server in SERVERS:
if server.data["local_server_id"] in request.form.getlist('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) client = next(
(
item
for item in server.data["keys"]
if item.name == request.form.get("old_name")
),
None,
)
if client: if client:
if client.name == request.form.get('name'): if client.name == request.form.get("name"):
pass pass
else: else:
server.rename_key(client.key_id, request.form.get('name')) server.rename_key(client.key_id, request.form.get("name"))
log.info("Renaming key %s to %s on server %s", log.info(
request.form.get('old_name'), "Renaming key %s to %s on server %s",
request.form.get('name'), request.form.get("old_name"),
server.data["name"]) request.form.get("name"),
server.data["name"],
)
else: else:
server.create_key(request.form.get('name')) server.create_key(request.form.get("name"))
log.info("Creating key %s on server %s", request.form.get('name'), server.data["name"]) log.info(
"Creating key %s on server %s",
request.form.get("name"),
server.data["name"],
)
else: 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: if client:
server.delete_key(client.key_id) 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() 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: 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(): def del_client():
if request.method == 'POST': if request.method == "POST":
with open(CFG_PATH, "r") as file: with open(CFG_PATH, "r") as file:
config = yaml.safe_load(file) or {} config = yaml.safe_load(file) or {}
clients = config.get('clients', dict()) clients = config.get("clients", dict())
user_id = request.form.get('user_id') user_id = request.form.get("user_id")
if user_id in clients: if user_id in clients:
for server in SERVERS: 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: if client:
server.delete_key(client.key_id) server.delete_key(client.key_id)
config["clients"].pop(user_id) config["clients"].pop(user_id)
with open(CFG_PATH, "w") as file: with open(CFG_PATH, "w") as file:
yaml.safe_dump(config, 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() 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/<server_name>/<client_id>', methods=['GET'], strict_slashes=False) @app.route("/dynamic/<server_name>/<client_id>", methods=["GET"], strict_slashes=False)
def dynamic(server_name, client_id): def dynamic(server_name, client_id):
try: try:
client = next((keys for client, keys in CLIENTS.items() if client == client_id), None) client = next(
server = next((item for item in SERVERS if item.info()["name"] == server_name), None) (keys for client, keys in CLIENTS.items() if client == client_id), None
key = next((item for item in server.data["keys"] if item.name == client["name"]), 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 and client and key:
if server.data["local_server_id"] in client["servers"]: 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 { return {
"server": server.data["hostname_for_access_keys"], "server": server.data["hostname_for_access_keys"],
"server_port": key.port, "server_port": key.port,
"password": key.password, "password": key.password,
"method": key.method, "method": key.method,
"info": "Managed by OutFleet [github.com/house-of-vanity/OutFleet/]" "info": "Managed by OutFleet [github.com/house-of-vanity/OutFleet/]",
} }
else: 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" return "Hey buddy, i think you got the wrong door the leather-club is two blocks down"
except: except:
log.warning("Hack attempt! Client or server doesn't exist. SCAM") 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('/dynamic/', methods=['GET'], strict_slashes=False)
@app.route("/dynamic/", methods=["GET"], strict_slashes=False)
def _dynamic(): def _dynamic():
log.warning("Hack attempt! Client or server doesn't exist. SCAM") 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(): def sync():
if request.method == 'GET': if request.method == "GET":
try: try:
with open('sync.log', 'r') as file: with open("sync.log", "r") as file:
lines = file.readlines() lines = file.readlines()
except: except:
lines = [] lines = []
return render_template( return render_template(
'sync.html', "sync.html",
SERVERS=SERVERS, SERVERS=SERVERS,
CLIENTS=CLIENTS, CLIENTS=CLIENTS,
lines=lines, lines=lines,
) )
if request.method == 'POST': if request.method == "POST":
log = logging.getLogger('sync') log = logging.getLogger("sync")
file_handler = logging.FileHandler('sync.log') file_handler = logging.FileHandler("sync.log")
file_handler.setLevel(logging.DEBUG) 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) file_handler.setFormatter(formatter)
log.addHandler(file_handler) log.addHandler(file_handler)
server_hash = {} server_hash = {}
for server in SERVERS: 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(): for key, client in CLIENTS.items():
log.info(f"Sync client `{client['name']}`") log.info(f"Sync client `{client['name']}`")
for u_server_id in client['servers']: for u_server_id in client["servers"]:
if u_server_id in server_hash: if u_server_id in server_hash:
if not server_hash[u_server_id].check_client(client['name']): 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']}`") log.warning(
server_hash[u_server_id].create_key(client['name']) f"Client `{client['name']}` absent on `{server_hash[u_server_id].data['name']}`"
)
server_hash[u_server_id].create_key(client["name"])
else: 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: 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() update_state()
return redirect(url_for('sync')) return redirect(url_for("sync"))
if __name__ == "__main__":
if __name__ == '__main__':
update_state() update_state()
app.run(host='0.0.0.0') app.run(host="0.0.0.0")