bunkerweb 1.4.0

This commit is contained in:
bunkerity
2022-06-03 17:24:14 +02:00
parent 3a078326c5
commit a9f886804a
5245 changed files with 1432051 additions and 27894 deletions

View File

@@ -1,55 +1,135 @@
import json, re, sys
import sys, json, glob, re, os, traceback
sys.path.append("/opt/bunkerweb/utils")
from logger import log
class Configurator :
def __init__(self) :
self.__settings = {}
self.__variables = {}
def __init__(self, settings, core, plugins, variables) :
self.__settings = self.__load_settings(settings)
self.__core = self.__load_plugins(core)
self.__plugins = self.__load_plugins(plugins)
self.__variables = self.__load_variables(variables)
self.__multisite = False
if "MULTISITE" in self.__variables and self.__variables["MULTISITE"] == "yes" :
self.__multisite = True
self.__servers = self.__map_servers()
def __map_servers(self) :
if not self.__multisite or not "SERVER_NAME" in self.__variables :
return {}
servers = {}
for server_name in self.__variables["SERVER_NAME"].split(" ") :
if not re.search(self.__settings["SERVER_NAME"]["regex"], server_name) :
log("GENERATOR", "⚠️", "Ignoring server name " + server_name + " because regex is not valid")
continue
names = [server_name]
if server_name + "_SERVER_NAME" in self.__variables :
if not re.search(self.__settings["SERVER_NAME"]["regex"], self.__variables[server_name + "_SERVER_NAME"]) :
log("GENERATOR", "⚠️", "Ignoring " + server_name + "_SERVER_NAME because regex is not valid")
else :
names = self.__variables[server_name + "_SERVER_NAME"].split(" ")
servers[server_name] = names
return servers
def __load_settings(self, path) :
with open(path) as f :
return json.loads(f.read())
def __load_plugins(self, path) :
plugins = {}
files = glob.glob(path + "/*/plugin.json")
for file in files :
try :
with open(file) as f :
plugins.update(json.loads(f.read())["settings"])
except :
log("GENERATOR", "", "Exception while loading JSON from " + file + " :")
print(traceback.format_exc())
return plugins
def __load_variables(self, path) :
variables = {}
with open(path) as f :
lines = f.readlines()
for line in lines :
line = line.strip()
if line.startswith("#") or line == "" or not "=" in line :
continue
var = line.split("=")[0]
value = line[len(var)+1:]
variables[var] = value
return variables
def load_settings(self, path) :
with open(path, "r") as f :
data = json.loads(f.read())
for cat in data :
for param in data[cat]["params"] :
if param["type"] == "multiple" :
real_params = param["params"]
else :
real_params = [param]
for real_param in real_params :
self.__settings[real_param["env"]] = real_param
self.__settings[real_param["env"]]["category"] = cat
def get_config(self) :
config = {}
# Extract default settings
default_settings = [self.__settings, self.__core, self.__plugins]
for settings in default_settings :
for setting, data in settings.items() :
config[setting] = data["default"]
# Override with variables
for variable, value in self.__variables.items() :
ret, err = self.__check_var(variable)
if ret :
config[variable] = value
else :
log("GENERATOR", "⚠️", "Ignoring variable " + variable + " : " + err)
# Expand variables to each sites if MULTISITE=yes and if not present
if config["MULTISITE"] == "yes" :
for server_name in config["SERVER_NAME"].split(" ") :
if server_name == "" :
continue
for settings in default_settings :
for setting, data in settings.items() :
if data["context"] == "global" :
continue
key = server_name + "_" + setting
if setting == "SERVER_NAME" :
if key not in config :
config[key] = server_name
continue
if key not in config :
config[key] = config[setting]
return config
def load_variables(self, vars, multisite_only=False) :
for var, value in vars.items() :
check, reason = self.__check_var(var, value)
if check :
self.__variables[var] = value
else :
print("ignoring " + var + "=" + value + " (" + reason + ")", file=sys.stderr)
def get_config(self) :
config = {}
for setting in self.__settings :
config[setting] = self.__settings[setting]["default"]
for variable, value in self.__variables.items() :
config[variable] = value
return config
def __check_var(self, var, value, multisite_only=False) :
real_var = ""
if var in self.__settings :
real_var = var
elif var[len(var.split("_")[0])+1:] in self.__settings :
real_var = var[len(var.split("_")[0])+1:]
elif re.search("\\_\d+$", var) and ("_".join(var.split("_")[:-1]) in self.__settings or "_".join(var.split("_")[:-1][1:]) in self.__settings) :
if "_".join(var.split("_")[:-1]) in self.__settings :
real_var = "_".join(var.split("_")[:-1])
else :
real_var = "_".join(var.split("_")[:-1][1:])
if real_var == "" :
return False, "doesn't exist"
elif not re.search(self.__settings[real_var]["regex"], value) :
return False, "doesn't match regex : " + self.__settings[real_var]["regex"]
elif multisite_only and self.__settings[real_var]["context"] != "multisite" :
return False, "not at multisite context"
return True, ""
def __check_var(self, variable) :
value = self.__variables[variable]
# MULTISITE=no
if not self.__multisite :
where, real_var = self.__find_var(variable)
if not where :
return False, "variable name " + variable + " doesn't exist"
if not "regex" in where[real_var] :
return False, "missing regex for variable " + variable
if not re.search(where[real_var]["regex"], value) :
return False, "value " + value + " doesn't match regex " + where[real_var]["regex"]
return True, "ok"
# MULTISITE=yes
prefixed, real_var = self.__var_is_prefixed(variable)
where, real_var = self.__find_var(real_var)
if not where :
return False, "variable name " + variable + " doesn't exist"
if prefixed and where[real_var]["context"] != "multisite" :
return False, "context of " + variable + " isn't multisite"
if not re.search(where[real_var]["regex"], value) :
return False, "value " + value + " doesn't match regex " + where[real_var]["regex"]
return True, "ok"
def __find_var(self, variable) :
targets = [self.__settings, self.__core, self.__plugins]
for target in targets :
if variable in target :
return target, variable
for real_var, settings in target.items() :
if "multiple" in settings and re.search("^" + real_var + "_" + "[0-9]+$", variable) :
return target, real_var
return False, variable
def __var_is_prefixed(self, variable) :
for server in self.__servers :
if variable.startswith(server + "_") :
return True, variable.replace(server + "_", "", 1)
return False, variable

22
gen/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM python:3.10-alpine
COPY . /opt/bunkerweb
RUN addgroup -g 101 nginx && \
adduser -h /opt/bunkerweb -g nginx -s /bin/sh -G nginx -D -H -u 101 nginx && \
chown -R root:nginx /opt && \
find /opt -type f -exec chmod 0740 {} \; && \
find /opt -type d -exec chmod 0750 {} \; && \
chmod 750 /opt/bunkerweb/gen/main.py && \
pip3 install -r /opt/bunkerweb/gen/requirements.txt && \
mkdir /etc/nginx /opt/bunkerweb/plugins && \
chown root:nginx /etc/nginx /opt/bunkerweb/plugins && \
chmod 770 /etc/nginx /opt/bunkerweb/plugins
WORKDIR /opt/bunkerweb/gen
USER nginx:nginx
VOLUME /etc/nginx /opt/bunkerweb/plugins
ENTRYPOINT ["python3", "/opt/bunkerweb/gen/main.py"]

View File

@@ -1,122 +1,135 @@
import jinja2, glob, os, pathlib, copy, crypt, random, string
import jinja2, glob, importlib, os, pathlib, copy, string, random
class Templator :
def __init__(self, config, input_path, output_path, target_path) :
self.__config_global = copy.deepcopy(config)
if config["MULTISITE"] == "yes" and config["SERVER_NAME"] != "" :
self.__config_sites = {}
for server_name in config["SERVER_NAME"].split(" ") :
self.__config_sites[server_name] = {}
for k, v in config.items() :
if k.startswith(server_name + "_") :
self.__config_sites[server_name][k.replace(server_name + "_", "", 1)] = v
del self.__config_global[k]
self.__input_path = input_path
self.__output_path = output_path
self.__target_path = target_path
if not self.__target_path.endswith("/") :
self.__target_path += "/"
self.__template_env = jinja2.Environment(loader=jinja2.FileSystemLoader(searchpath=self.__input_path), lstrip_blocks=True, trim_blocks=True)
def __init__(self, templates, core, plugins, output, target, config) :
self.__templates = templates
self.__core = core
self.__plugins = plugins
self.__output = output
if not self.__output.endswith("/") :
self.__output += "/"
self.__target = target
if not self.__target.endswith("/") :
self.__target += "/"
self.__config = config
self.__jinja_env = self.__load_jinja_env()
def render_global(self) :
return self.__render("global")
def render(self) :
self.__render_global()
servers = [self.__config["SERVER_NAME"]]
if self.__config["MULTISITE"] == "yes" :
servers = self.__config["SERVER_NAME"].split(" ")
for server in servers :
self.__render_server(server)
def render_site(self, server_name=None, first_server=None) :
if server_name is None :
server_name = self.__config_global["SERVER_NAME"]
if first_server is None :
first_server = self.__config_global["SERVER_NAME"].split(" ")[0]
return self.__render("site", server_name, first_server)
def __load_jinja_env(self) :
searchpath = [self.__templates]
for subpath in glob.glob(self.__core + "/*") + glob.glob(self.__plugins + "/*") :
if os.path.isdir(subpath) :
searchpath.append(subpath + "/confs")
return jinja2.Environment(loader=jinja2.FileSystemLoader(searchpath=searchpath), lstrip_blocks=True, trim_blocks=True)
def __prepare_config(self, type, server_name=None, first_server=None) :
real_config = copy.deepcopy(self.__config_global)
if type == "site" and self.__config_global["MULTISITE"] == "yes" :
site_config = copy.deepcopy(self.__config_sites[first_server])
real_config.update(site_config)
elif type == "global" and self.__config_global["MULTISITE"] == "yes" and self.__config_global["SERVER_NAME"] != "" :
for k, v in self.__config_sites.items() :
for k2, v2 in v.items() :
real_config[k + "_" + k2] = v2
if not server_name is None :
real_config["SERVER_NAME"] = server_name
if not first_server is None :
real_config["FIRST_SERVER"] = first_server
real_config["NGINX_PREFIX"] = self.__target_path
if self.__config_global["MULTISITE"] == "yes" and type == "site" :
real_config["NGINX_PREFIX"] += first_server + "/"
if not real_config["ROOT_FOLDER"].endswith("/" + first_server) :
real_config["ROOT_FOLDER"] += "/" + first_server
if real_config["ROOT_SITE_SUBFOLDER"] != "" :
real_config["ROOT_FOLDER"] += "/" + real_config["ROOT_SITE_SUBFOLDER"]
return real_config
def __find_templates(self, contexts) :
templates = []
for template in self.__jinja_env.list_templates() :
if "global" in contexts and "/" not in template :
templates.append(template)
continue
for context in contexts :
if template.startswith(context + "/") :
templates.append(template)
return templates
def __filter_var(self, variable) :
filters = ["FIRST_SERVER", "NGINX_PREFIX"]
for filter in filters :
if variable == filter or variable.endswith("_" + filter) :
return True
return False
def __write_config(self, subpath=None, config=None) :
real_path = self.__output
if subpath != None :
real_path += subpath + "/"
real_path += "variables.env"
real_config = self.__config
if config != None :
real_config = config
pathlib.Path(os.path.dirname(real_path)).mkdir(parents=True, exist_ok=True)
with open(real_path, "w") as f :
for k, v in real_config.items() :
f.write(k + "=" + v + "\n")
def __save_config(self, type, config) :
first_servers = config["SERVER_NAME"].split(" ")
data = ""
for variable, value in config.items() :
if self.__filter_var(variable) :
continue
add = True
if type == "global" :
for first_server in first_servers :
if variable.startswith(first_server + "_") :
add = False
break
if add :
data += variable + "=" + value + "\n"
file = self.__output_path
if type == "global" :
file += "/global.env"
elif config["MULTISITE"] == "yes" :
file += "/" + config["FIRST_SERVER"] + "/site.env"
else :
file += "/site.env"
with open(file, "w") as f :
f.write(data)
def __render_global(self) :
self.__write_config()
templates = self.__find_templates(["global", "http", "stream", "default-server-http"])
for template in templates :
self.__render_template(template)
def __render(self, type, server_name=None, first_server=None) :
real_config = self.__prepare_config(type, server_name, first_server)
for filename in glob.iglob(self.__input_path + "/" + type + "**/**", recursive=True) :
if not os.path.isfile(filename) :
continue
relative_filename = filename.replace(self.__input_path, "").replace(type + "/", "")
template = self.__template_env.get_template(type + "/" + relative_filename)
template.globals["has_value"] = Templator.has_value
template.globals["sha512_crypt"] = Templator.sha512_crypt
template.globals["is_custom_conf"] = Templator.is_custom_conf
template.globals["random"] = Templator.random
output = template.render(real_config, all=real_config)
if real_config["MULTISITE"] == "yes" and type == "site" :
relative_filename = real_config["FIRST_SERVER"] + "/" + relative_filename
if "/" in relative_filename :
directory = relative_filename.replace(relative_filename.split("/")[-1], "")
pathlib.Path(self.__output_path + "/" + directory).mkdir(parents=True, exist_ok=True)
with open(self.__output_path + "/" + relative_filename, "w") as f :
f.write(output)
self.__save_config(type, real_config)
def __render_server(self, server) :
templates = self.__find_templates(["modsec", "modsec-crs", "server-http", "server-stream"])
if self.__config["MULTISITE"] == "yes" :
config = copy.deepcopy(self.__config)
for variable, value in self.__config.items() :
if variable.startswith(server + "_") :
config[variable.replace(server + "_", "", 1)] = value
self.__write_config(subpath=server, config=config)
for template in templates :
subpath = None
config = None
name = None
if self.__config["MULTISITE"] == "yes" :
subpath = server
config = copy.deepcopy(self.__config)
for variable, value in self.__config.items() :
if variable.startswith(server + "_") :
config[variable.replace(server + "_", "", 1)] = value
config["NGINX_PREFIX"] = self.__target + server + "/"
server_key = server + "_SERVER_NAME"
if server_key not in self.__config :
config["SERVER_NAME"] = server
root_confs = ["server.conf", "access-lua.conf", "init-lua.conf", "log-lua.conf"]
for root_conf in root_confs :
if template.endswith("/" + root_conf) :
name = os.path.basename(template)
break
self.__render_template(template, subpath=subpath, config=config, name=name)
@jinja2.contextfunction
def has_value(context, name, value) :
for k, v in context.items() :
if (k == name or k.endswith("_" + name)) and v == value :
return True
return False
def __render_template(self, template, subpath=None, config=None, name=None) :
# Get real config and output folder in case it's a server config and we are in multisite mode
real_config = copy.deepcopy(self.__config)
if config :
real_config = copy.deepcopy(config)
real_config["all"] = copy.deepcopy(real_config)
real_config["import"] = importlib.import_module
real_config["is_custom_conf"] = Templator.is_custom_conf
real_config["has_variable"] = Templator.has_variable
real_config["random"] = Templator.random
real_config["read_lines"] = Templator.read_lines
real_output = self.__output
if subpath :
real_output += "/" + subpath + "/"
real_name = template
if name :
real_name = name
jinja_template = self.__jinja_env.get_template(template)
pathlib.Path(os.path.dirname(real_output + real_name)).mkdir(parents=True, exist_ok=True)
with open(real_output + real_name, "w") as f :
f.write(jinja_template.render(real_config))
def sha512_crypt(password) :
return crypt.crypt(password, crypt.mksalt(crypt.METHOD_SHA512))
def is_custom_conf(folder) :
for filename in glob.iglob(folder + "/*.conf") :
return True
return False
def random(number) :
return "".join(random.choices(string.ascii_uppercase + string.ascii_lowercase + string.digits, k=number))
def is_custom_conf(path) :
return glob.glob(path + "/*.conf")
def has_variable(all_vars, variable, value) :
if variable in all_vars and all_vars[variable] == value :
return True
if all_vars["MULTISITE"] == "yes" :
for server_name in all_vars["SERVER_NAME"].split(" ") :
if server_name + "_" + variable in all_vars and all_vars[server_name + "_" + variable] == value :
return True
return False
def random(nb) :
characters = string.ascii_letters + string.digits
return "".join(random.choice(characters) for i in range(nb))
def read_lines(file) :
try :
with open(file, "r") as f :
return f.readlines()
except :
return []

152
gen/main.py Executable file → Normal file
View File

@@ -1,84 +1,92 @@
#!/usr/bin/python3
#!/usr/bin/env python3
import argparse, os, sys, shutil, glob
import argparse, os, sys, shutil, glob, traceback
import sys
sys.path.append("/opt/bunkerweb/deps/python")
sys.path.append("/opt/bunkerweb/gen")
sys.path.append("/opt/bunkerweb/utils")
from logger import log
import utils
from Configurator import Configurator
from Templator import Templator
if __name__ == "__main__" :
# Parse arguments
parser = argparse.ArgumentParser(description="bunkerized-nginx config generator v1.0")
parser.add_argument("--settings", default="/opt/settings.json", type=str, help="path to the files containing the default settings")
parser.add_argument("--templates", default="/opt/confs", type=str, help="directory containing the templates files")
parser.add_argument("--output", default="/etc/nginx", type=str, help="where to write the rendered files")
parser.add_argument("--target", default="/etc/nginx", type=str, help="where nginx will search for configurations files")
parser.add_argument("--variables", default="/opt/variables.env", type=str, help="path to the file containing environment variables")
args = parser.parse_args()
try :
# Check existences and permissions
if not os.path.exists(args.settings) :
print("[!] Missing settings file : " + args.settings)
sys.exit(1)
if not os.access(args.settings, os.R_OK) :
print("[!] Can't read settings file : " + args.settings)
sys.exit(2)
if not os.path.isdir(args.templates) :
print("[!] Missing templates directory : " + args.templates)
sys.exit(1)
if not os.access(args.templates, os.R_OK | os.X_OK) :
print("[!] Can't read the templates directory : " + args.templates)
sys.exit(2)
if not os.path.isdir(args.output) :
print("[!] Missing output directory : " + args.output)
sys.exit(1)
if not os.access(args.output, os.W_OK | os.X_OK) :
print("[!] Can't write to the templates directory : " + args.output)
sys.exit(2)
if not os.path.exists(args.variables) :
print("[!] Missing variables file : " + args.variables)
sys.exit(1)
if not os.access(args.variables, os.R_OK) :
print("[!] Can't read variables file : " + args.variables)
sys.exit(2)
# Parse arguments
parser = argparse.ArgumentParser(description="BunkerWeb config generator")
parser.add_argument("--settings", default="/opt/bunkerweb/settings.json", type=str, help="file containing the main settings")
parser.add_argument("--templates", default="/opt/bunkerweb/confs", type=str, help="directory containing the main template files")
parser.add_argument("--core", default="/opt/bunkerweb/core", type=str, help="directory containing the core plugins")
parser.add_argument("--plugins", default="/opt/bunkerweb/plugins", type=str, help="directory containing the external plugins")
parser.add_argument("--output", default="/etc/nginx", type=str, help="where to write the rendered files")
parser.add_argument("--target", default="/etc/nginx", type=str, help="where nginx will search for configurations files")
parser.add_argument("--variables", default="/opt/bunkerweb/variables.env", type=str, help="path to the file containing environment variables")
args = parser.parse_args()
# Compute the final config
configurator = Configurator()
configurator.load_settings(args.settings)
variables = utils.load_variables(args.variables)
configurator.load_variables(variables)
config = configurator.get_config()
log("GENERATOR", "", "Generator started ...")
log("GENERATOR", "", "Settings : " + args.settings)
log("GENERATOR", "", "Templates : " + args.templates)
log("GENERATOR", "", "Core : " + args.core)
log("GENERATOR", "", "Plugins : " + args.plugins)
log("GENERATOR", "", "Output : " + args.output)
log("GENERATOR", "", "Target : " + args.target)
log("GENERATOR", "", "Variables : " + args.variables)
# Remove old files
files = glob.glob(args.output + "/*")
for file in files :
if (file.endswith(".conf") or file.endswith(".env")) and os.path.isfile(file) and not os.path.islink(file) :
os.remove(file)
elif os.path.isdir(file) and not os.path.islink(file) :
shutil.rmtree(file, ignore_errors=False)
# Check existences and permissions
log("GENERATOR", "", "Checking arguments ...")
files = [args.settings, args.variables]
paths_rx = [args.core, args.plugins, args.templates]
paths_rwx = [args.output]
for file in files :
if not os.path.exists(file) :
log("GENERATOR", "", "Missing file : " + file)
sys.exit(1)
if not os.access(file, os.R_OK) :
log("GENERATOR", "", "Can't read file : " + file)
sys.exit(1)
for path in paths_rx + paths_rwx :
if not os.path.isdir(path) :
log("GENERATOR", "", "Missing directory : " + path)
sys.exit(1)
if not os.access(path, os.R_OK | os.X_OK) :
log("GENERATOR", "", "Missing RX rights on directory : " + path)
sys.exit(1)
for path in paths_rwx :
if not os.access(path, os.W_OK) :
log("GENERATOR", "", "Missing W rights on directory : " + path)
sys.exit(1)
# Generate the files from templates and config
templator = Templator(config, args.templates, args.output, args.target)
templator.render_global()
if config["MULTISITE"] == "no" :
templator.render_site()
elif config["SERVER_NAME"] != "" :
# Compute a dict of first_server: [list of server_name]
map_servers = {}
for server_name in config["SERVER_NAME"].split(" ") :
if server_name + "_SERVER_NAME" in config :
map_servers[server_name] = config[server_name + "_SERVER_NAME"].split(" ")
for server_name in config["SERVER_NAME"].split(" ") :
if server_name in map_servers :
continue
for first_server, servers in map_servers.items() :
if server_name in servers :
continue
map_servers[server_name] = [server_name]
for first_server, servers in map_servers.items() :
templator.render_site(" ".join(servers), first_server)
# Compute the config
log("GENERATOR", "", "Computing config ...")
configurator = Configurator(args.settings, args.core, args.plugins, args.variables)
config = configurator.get_config()
# We're done
print("[*] Generation done !")
sys.exit(0)
# Remove old files
log("GENERATOR", "", "Removing old files ...")
files = glob.glob(args.output + "/*")
for file in files :
if os.path.islink(file) :
os.unlink(file)
elif os.path.isfile(file) :
os.remove(file)
elif os.path.isdir(file) :
shutil.rmtree(file, ignore_errors=False)
# Render the templates
log("GENERATOR", "", "Rendering templates ...")
templator = Templator(args.templates, args.core, args.plugins, args.output, args.target, config)
templator.render()
# We're done
log("GENERATOR", "", "Generator successfully executed !")
sys.exit(0)
except SystemExit as e :
sys.exit(e)
except :
log("GENERATOR", "", "Exception while executing generator : " + traceback.format_exc())
sys.exit(1)

View File

@@ -1 +0,0 @@
jinja2

View File

@@ -1 +0,0 @@
./main.py --settings /opt/work/bunkerized-nginx/settings.json --templates /opt/work/bunkerized-nginx/confs --output /tmp/debug --variables /tmp/variables.env

View File

@@ -1,12 +1,32 @@
def load_variables(path) :
variables = {}
with open(path) as f :
lines = f.read().splitlines()
for line in lines :
if line.startswith("#") or line == "" or not "=" in line :
continue
var = line.split("=")[0]
value = line[len(var)+1:]
variables[var] = value
return variables
import os, stat
def has_permissions(path, need_permissions) :
uid = os.geteuid()
gid = os.getegid()
statinfo = os.stat(path)
permissions = {"R": False, "W": False, "X": False}
if statinfo.st_uid == uid :
if statinfo.st_mode & stat.S_IRUSR :
permissions["R"] = True
if statinfo.st_mode & stat.S_IWUSR :
permissions["W"] = True
if statinfo.st_mode & stat.S_IXUSR :
permissions["X"] = True
if statinfo.st_uid == gid :
if statinfo.st_mode & stat.S_IRGRP :
permissions["R"] = True
if statinfo.st_mode & stat.S_IWGRP :
permissions["W"] = True
if statinfo.st_mode & stat.S_IXGRP :
permissions["X"] = True
if statinfo.st_mode & stat.S_IROTH :
permissions["R"] = True
if statinfo.st_mode & stat.S_IWOTH :
permissions["W"] = True
if statinfo.st_mode & stat.S_IXOTH :
permissions["X"] = True
list_permissions = [permission for permission in need_permissions]
for need_permission in list_permissions :
if not permissions[need_permission] :
return False
return True