Files
OutFleet/main.py
Alexandr Bogomyakov eb9f199d57
Some checks failed
Docker hub build / docker (push) Has been cancelled
Add log
2024-10-26 11:27:51 +03:00

486 lines
17 KiB
Python
Executable File

import threading
import time
import yaml
import logging
from datetime import datetime
import random
import string
import argparse
import uuid
import bcrypt
import k8s
from flask import Flask, render_template, request, url_for, redirect
from flask_cors import CORS
from werkzeug.routing import BaseConverter
from lib import Server, write_config, get_config, args, lock
class DuplicateFilter(logging.Filter):
def filter(self, record):
# add other fields if you need more granular comparison, depends on your app
current_log = (record.module, record.levelno, record.msg)
if current_log != getattr(self, "last_log", None):
self.last_log = current_log
return True
return False
logging.getLogger("werkzeug").setLevel(logging.ERROR)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%d-%m-%Y %H:%M:%S",
)
log = logging.getLogger("OutFleet")
file_handler = logging.FileHandler("sync.log")
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
file_handler.setFormatter(formatter)
log.addHandler(file_handler)
duplicate_filter = DuplicateFilter()
log.addFilter(duplicate_filter)
CFG_PATH = args.config
NAMESPACE = k8s.NAMESPACE
SERVERS = list()
BROKEN_SERVERS = list()
CLIENTS = dict()
VERSION = '8.1'
SECRET_LINK_LENGTH = 8
SECRET_LINK_PREFIX = '$2b$12$'
SS_PREFIX = "\u0005\u00DC\u005F\u00E0\u0001\u0020"
HOSTNAME = ""
WRONG_DOOR = "Hey buddy, i think you got the wrong door the leather-club is two blocks down"
app = Flask(__name__)
CORS(app)
def format_timestamp(ts):
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))
def update_state(timer=40):
while True:
with lock:
global SERVERS
global CLIENTS
global BROKEN_SERVERS
global HOSTNAME
config = get_config()
if config:
HOSTNAME = config.get("ui_hostname", "my-own-SSL-ENABLED-domain.com")
servers = config.get("servers", dict())
_SERVERS = list()
for local_server_id, server_config in servers.items():
try:
server = Server(
url=server_config["url"],
cert=server_config["cert"],
comment=server_config.get("comment", ''),
local_server_id=local_server_id,
)
_SERVERS.append(server)
log.debug(
"Server state updated: %s, [%s]",
server.info()["name"],
local_server_id,
)
except Exception as e:
BROKEN_SERVERS.append({
"config": server_config,
"error": e,
"id": local_server_id
})
log.warning("Can't access server: %s - %s", server_config["url"], e)
SERVERS = _SERVERS
CLIENTS = config.get("clients", dict())
if timer == 0:
break
time.sleep(40)
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "GET":
#if request.args.get("broken") == True:
return render_template(
"index.html",
SERVERS=SERVERS,
VERSION=VERSION,
K8S_NAMESPACE=k8s.NAMESPACE,
BROKEN_SERVERS=BROKEN_SERVERS,
nt=request.args.get("nt"),
nl=request.args.get("nl"),
selected_server=request.args.get("selected_server"),
broken=request.args.get("broken", False),
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()["local_server_id"] == server), None
)
server.apply_config(request.form)
update_state(timer=0)
return redirect(
url_for(
"index",
nt="Updated Outline VPN Server",
selected_server=request.args.get("selected_server"),
)
)
else:
return redirect(url_for("index"))
@app.route("/clients", methods=["GET", "POST"])
def clients():
if request.method == "GET":
return render_template(
"clients.html",
SERVERS=SERVERS,
bcrypt=bcrypt,
CLIENTS=CLIENTS,
VERSION=VERSION,
SECRET_LINK_LENGTH=SECRET_LINK_LENGTH,
SECRET_LINK_PREFIX=SECRET_LINK_PREFIX,
K8S_NAMESPACE=k8s.NAMESPACE,
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,
)
@app.route("/add_server", methods=["POST"])
def add_server():
if request.method == "POST":
try:
config = get_config()
servers = config.get("servers", dict())
local_server_id = str(uuid.uuid4())
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"],
}
config["servers"] = servers
write_config(config)
log.info("Added server: %s", new_server.data["name"])
update_state(timer=0)
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"
)
)
@app.route("/del_server", methods=["POST"])
def del_server():
if request.method == "POST":
config = get_config()
local_server_id = request.form.get("local_server_id")
server_name = None
try:
server_name = config["servers"].pop(local_server_id)["name"]
except KeyError as e:
pass
for client_id, client_config in config["clients"].items():
try:
client_config["servers"].remove(local_server_id)
except ValueError as e:
pass
write_config(config)
log.info("Deleting server %s [%s]", server_name, request.form.get("local_server_id"))
update_state(timer=0)
return redirect(url_for("index", nt=f"Server {server_name} has been deleted"))
@app.route("/add_client", methods=["POST"])
def add_client():
if request.method == "POST":
config = get_config()
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"),
}
config["clients"] = clients
write_config(config)
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 client:
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"],
)
else:
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,
)
if client:
server.delete_key(client.key_id)
log.info(
"Deleting key %s on server %s",
request.form.get("name"),
server.data["name"],
)
update_state(timer=0)
return redirect(
url_for(
"clients",
nt="Clients updated",
selected_client=request.form.get("user_id"),
)
)
else:
return redirect(url_for("clients"))
@app.route("/del_client", methods=["POST"])
def del_client():
if request.method == "POST":
config = get_config()
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,
)
if client:
server.delete_key(client.key_id)
config["clients"].pop(user_id)
write_config(config)
log.info("Deleting client %s", request.form.get("name"))
update_state(timer=0)
return redirect(url_for("clients", nt="User has been deleted"))
def append_to_log(log_entry):
with open("access_log.log", "a") as log_file:
log_file.write(log_entry + "\n")
@app.route("/dynamic/<path:hash_secret>", methods=["GET"], strict_slashes=False)
def dynamic(hash_secret):
# Depricated scheme.
for server in SERVERS:
if hash_secret.startswith(server.data["name"]):
log.warning("Deprecated key request")
server_name = hash_secret.split('/')[0]
client_id = hash_secret.split('/')[1]
return dynamic_depticated(server_name, client_id, hash_secret)
try:
short_hash_server = hash_secret[0:SECRET_LINK_LENGTH]
short_hash_client = hash_secret[SECRET_LINK_LENGTH:SECRET_LINK_LENGTH * 2 ]
client_provided_secret = hash_secret[SECRET_LINK_LENGTH * 2:]
hash_server = None
hash_client = None
server = None
client = None
for _server in SERVERS:
if _server.data["local_server_id"][:SECRET_LINK_LENGTH] == short_hash_server:
hash_server = _server.data["local_server_id"]
server = _server
for client_id, values in CLIENTS.items():
if client_id[:SECRET_LINK_LENGTH] == short_hash_client:
hash_client = client_id
client = CLIENTS[client_id]
if server and client:
append_to_log(f"User: {client["name"]}. Server: {server.data['name']} client secret string: {hash_secret}")
client_shadowsocks_key = next(
(item for item in server.data["keys"] if item.key_id == client["name"]), None
)
secret_string = hash_server + hash_client
check_secret_hash = bcrypt.checkpw(
password=secret_string.encode('utf-8'),
hashed_password=f"{SECRET_LINK_PREFIX}{client_provided_secret}".encode('utf-8')
)
if check_secret_hash:
log.info(f"Client {client['name']} has been requested ssconf for {server.data['name']}. Bcrypt client hash {client_provided_secret[0:16]}...[FULL HASH SECURED]")
return {
"server": server.data["hostname_for_access_keys"],
"server_port": client_shadowsocks_key.port,
"password": client_shadowsocks_key.password,
"method": client_shadowsocks_key.method,
"prefix": SS_PREFIX,
"info": "Managed by OutFleet [github.com/house-of-vanity/OutFleet/]",
}
else:
log.warning(f"Hack attempt! Client secret does not match: {client_provided_secret}")
return WRONG_DOOR
else:
log.warning(f"Hack attempt! Client or server doesn't exist. payload: {hash_secret[0:200]}")
return WRONG_DOOR
except Exception as e:
log.error(f"Dynamic V2 parse error: {e}")
return WRONG_DOOR
def dynamic_depticated(server_name, client_id, hash_secret=""):
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.key_id == client["name"]), None
)
if server and client and key:
if server.data["local_server_id"] in client["servers"]:
log.info(
"Client %s has been requested ssconf for %s", client["name"], server.data["name"]
)
append_to_log(f"User: {client["name"]}. Server: {server.data['name']} client secret string: {hash_secret}")
return {
"server": server.data["hostname_for_access_keys"],
"server_port": key.port,
"password": key.password,
"method": key.method,
"prefix":SS_PREFIX,
"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"],
)
return WRONG_DOOR
except:
log.warning("Hack attempt! Client or server doesn't exist. SCAM")
return WRONG_DOOR
@app.route("/dynamic", methods=["GET"], strict_slashes=False)
def _dynamic():
log.warning("Hack attempt! Client or server doesn't exist. SCAM")
return WRONG_DOOR
@app.route("/sync", methods=["GET", "POST"])
def sync():
if request.method == "GET":
try:
with open("sync.log", "r") as file:
lines = file.readlines()
except:
lines = []
return render_template(
"sync.html",
SERVERS=SERVERS,
CLIENTS=CLIENTS,
lines=lines,
)
if request.method == "POST":
with lock:
if request.form.get("wipe") == 'all':
for server in SERVERS:
log.info("Wiping all keys on [%s]", server.data["name"])
for client in server.data['keys']:
server.delete_key(client.key_id)
server_hash = {}
with lock:
for server in SERVERS:
server_hash[server.data["local_server_id"]] = server
with lock:
for key, client in CLIENTS.items():
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']} already present on {server_hash[u_server_id].data['name']}"
)
else:
log.info(
f"Client {client['name']} incorrect server_id {u_server_id}"
)
update_state(timer=0)
return redirect(url_for("sync"))
if __name__ == "__main__":
update_state_thread = threading.Thread(target=update_state)
update_state_thread.start()
discovery_servers_thread = threading.Thread(target=k8s.discovery_servers)
discovery_servers_thread.start()
app.run(host="0.0.0.0")