Soft merge branch 'dev' into branch '1.6'

This commit is contained in:
Théophile Diot 2024-05-24 12:34:47 +01:00
commit 9e7d824d82
No known key found for this signature in database
GPG key ID: 248FEA4BAE400D06
8 changed files with 108 additions and 59 deletions

View file

@ -752,6 +752,16 @@ When your BunkerWeb instance has upgraded to the PRO version, you will see your
### Username / Password
!!! tip "Overriding admin credentials from environment variables"
If you want to override the admin credentials from environment variables, you can set the following variables :
- `OVERRIDE_ADMIN_CREDS` : set it to `yes` to enable the override even if the admin credentials are already set (default is `no`)
- `ADMIN_USERNAME` : username to access the web UI
- `ADMIN_PASSWORD` : password to access the web UI
The web UI will use these variables to authenticate you.
!!! warning "Lost password/username"
In case you forgot your UI credentials, you can reset them from the CLI following [the steps described in the troubleshooting section](troubleshooting.md#web-ui).

View file

@ -1,4 +1,13 @@
{% if UI_HOST != "" and not has_variable(all, "USE_UI", "yes") +%}
access_by_lua_block {
local ngx_var = ngx.var
local scheme = ngx_var.scheme
local http_host = ngx_var.http_host
local request_uri = ngx_var.request_uri
if scheme == "http" and http_host ~= nil and http_host ~= "" and request_uri and request_uri ~= "" then
return ngx.redirect("https://" .. http_host .. request_uri, ngx.HTTP_MOVED_PERMANENTLY)
end
}
location /setup {
etag off;
add_header Last-Modified "";

View file

@ -454,7 +454,7 @@ class Database:
# Rename the old tables
db_version_id = db_version.replace(".", "_")
for table_name in metadata.tables.keys():
if table_name not in Base.metadata.tables:
if table_name in Base.metadata.tables:
with self.__db_session() as session:
if inspector.has_table(f"{table_name}_{db_version_id}"):
self.logger.warning(f'Table "{table_name}" already exists, dropping it to make room for the new one')

View file

@ -9,7 +9,7 @@ from os.path import sep
from pathlib import Path
from shutil import rmtree
from sys import argv
from tarfile import open as tar_open
from tarfile import TarFile, open as tar_open
from threading import Lock
from traceback import format_exc
from typing import Any, Dict, Literal, Optional, Tuple, Union
@ -78,15 +78,22 @@ class Job:
rmtree(extract_path, ignore_errors=True)
extract_path.mkdir(parents=True, exist_ok=True)
with tar_open(fileobj=BytesIO(job_cache_file["data"]), mode="r:gz") as tar:
assert isinstance(tar, TarFile)
try:
tar.extractall(extract_path, filter="fully_trusted")
except TypeError:
tar.extractall(extract_path)
for member in tar.getmembers():
try:
tar.extract(member, path=extract_path)
except Exception as e:
self.logger.error(f"Error extracting {member.name}: {e}")
except Exception as e:
self.logger.error(f"Error extracting tar file: {e}")
self.logger.debug(f"Restored cache directory {extract_path}")
continue
elif job_cache_file["job_name"] != job_name:
continue
cache_path.parent.mkdir(parents=True, exist_ok=True)
cache_path.write_bytes(job_cache_file["data"])
self.logger.debug(f"Restored cache file {job_cache_file['file_name']}")
except BaseException as e:
self.logger.error(f"Exception while restoring cache file {job_cache_file['file_name']} :\n{e}")
ret = False
@ -203,7 +210,7 @@ class Job:
tgz.add(dir_path, arcname=".")
content.seek(0, 0)
return self.cache_file(file_name, content.read(), job_name=job_name, service_id=service_id)
return self.cache_file(file_name, content.getvalue(), job_name=job_name, service_id=service_id)
def del_cache(self, name: str, *, job_name: str = "", service_id: str = "") -> Tuple[bool, str]:
"""Delete cache file from database and local cache file."""

View file

@ -178,11 +178,16 @@ def generate_custom_configs(configs: List[Dict[str, Any]], *, original_path: Uni
LOGGER.error("Sending custom configs failed, configuration will not work as expected...")
def generate_external_plugins(plugins: List[Dict[str, Any]], *, original_path: Union[Path, str] = EXTERNAL_PLUGINS_PATH):
def generate_external_plugins(plugins: Optional[List[Dict[str, Any]]], *, original_path: Union[Path, str] = EXTERNAL_PLUGINS_PATH):
if not isinstance(original_path, Path):
original_path = Path(original_path)
pro = "pro" in original_path.parts
if not plugins:
assert SCHEDULER is not None
plugins = SCHEDULER.db.get_plugins(_type="pro" if pro else "external", with_data=True)
assert plugins is not None, "Couldn't get plugins from database"
# Remove old external/pro plugins files
LOGGER.info(f"Removing old/changed {'pro ' if pro else ''}external plugins files ...")
ignored_plugins = set()
@ -486,6 +491,8 @@ if __name__ == "__main__":
# Instantiate scheduler environment
SCHEDULER.env = env
threads = []
SCHEDULER.apis = []
for db_instance in SCHEDULER.db.get_instances():
SCHEDULER.apis.append(API(f"http://{db_instance['hostname']}:{db_instance['port']}", db_instance["server_name"]))
@ -494,43 +501,43 @@ if __name__ == "__main__":
LOGGER.info("Scheduler started ...")
# Checking if any custom config has been created by the user
LOGGER.info("Checking if there are any changes in custom configs ...")
custom_configs = []
db_configs = SCHEDULER.db.get_custom_configs()
changes = False
for file in CUSTOM_CONFIGS_PATH.rglob("*.conf"):
if len(file.parts) > len(CUSTOM_CONFIGS_PATH.parts) + 3:
LOGGER.warning(f"Custom config file {file} is not in the correct path, skipping ...")
def check_configs_changes():
# Checking if any custom config has been created by the user
LOGGER.info("Checking if there are any changes in custom configs ...")
custom_configs = []
db_configs = SCHEDULER.db.get_custom_configs()
changes = False
for file in CUSTOM_CONFIGS_PATH.rglob("*.conf"):
if len(file.parts) > len(CUSTOM_CONFIGS_PATH.parts) + 3:
LOGGER.warning(f"Custom config file {file} is not in the correct path, skipping ...")
content = file.read_text(encoding="utf-8")
service_id = file.parent.name if file.parent.name not in CUSTOM_CONFIGS_DIRS else None
config_type = file.parent.parent.name if service_id else file.parent.name
content = file.read_text(encoding="utf-8")
service_id = file.parent.name if file.parent.name not in CUSTOM_CONFIGS_DIRS else None
config_type = file.parent.parent.name if service_id else file.parent.name
saving = True
in_db = False
for db_conf in db_configs:
if db_conf["service_id"] == service_id and db_conf["name"] == file.stem:
in_db = True
saving = True
in_db = False
for db_conf in db_configs:
if db_conf["service_id"] == service_id and db_conf["name"] == file.stem:
in_db = True
if not in_db and content.startswith("# CREATED BY ENV"):
saving = False
changes = True
if not in_db and content.startswith("# CREATED BY ENV"):
saving = False
changes = True
if saving:
custom_configs.append({"value": content, "exploded": (service_id, config_type, file.stem)})
if saving:
custom_configs.append({"value": content, "exploded": (service_id, config_type, file.stem)})
changes = changes or {hash(dict_to_frozenset(d)) for d in custom_configs} != {hash(dict_to_frozenset(d)) for d in db_configs}
changes = changes or {hash(dict_to_frozenset(d)) for d in custom_configs} != {hash(dict_to_frozenset(d)) for d in db_configs}
if changes:
err = SCHEDULER.db.save_custom_configs(custom_configs, "manual")
if err:
LOGGER.error(f"Couldn't save some manually created custom configs to database: {err}")
if changes:
err = SCHEDULER.db.save_custom_configs(custom_configs, "manual")
if err:
LOGGER.error(f"Couldn't save some manually created custom configs to database: {err}")
if (scheduler_first_start and db_configs) or changes:
generate_custom_configs(SCHEDULER.db.get_custom_configs())
del custom_configs, db_configs
threads.append(Thread(target=check_configs_changes))
def check_plugin_changes(_type: Literal["external", "pro"] = "external"):
# Check if any external or pro plugin has been added by the user
@ -581,11 +588,15 @@ if __name__ == "__main__":
if err:
LOGGER.error(f"Couldn't save some manually added {_type} plugins to database: {err}")
if (scheduler_first_start and db_plugins) or changes:
generate_external_plugins(SCHEDULER.db.get_plugins(_type=_type, with_data=True), original_path=plugin_path)
generate_external_plugins(SCHEDULER.db.get_plugins(_type=_type, with_data=True), original_path=plugin_path)
check_plugin_changes("external")
check_plugin_changes("pro")
threads.extend([Thread(target=check_plugin_changes, args=("external",)), Thread(target=check_plugin_changes, args=("pro",))])
for thread in threads:
thread.start()
for thread in threads:
thread.join()
LOGGER.info("Running plugins download jobs ...")
@ -598,10 +609,18 @@ if __name__ == "__main__":
db_metadata = SCHEDULER.db.get_metadata()
if db_metadata["pro_plugins_changed"] or db_metadata["external_plugins_changed"]:
threads.clear()
if db_metadata["pro_plugins_changed"]:
generate_external_plugins(SCHEDULER.db.get_plugins(_type="pro", with_data=True), original_path=PRO_PLUGINS_PATH)
threads.append(Thread(target=generate_external_plugins, args=(None,), kwargs={"original_path": PRO_PLUGINS_PATH}))
if db_metadata["external_plugins_changed"]:
generate_external_plugins(SCHEDULER.db.get_plugins(_type="external", with_data=True))
threads.append(Thread(target=generate_external_plugins, args=(None,)))
for thread in threads:
thread.start()
for thread in threads:
thread.join()
# run the config saver to save potential ignored external plugins settings
LOGGER.info("Running config saver to save potential ignored external plugins settings ...")
@ -630,7 +649,6 @@ if __name__ == "__main__":
CONFIG_NEED_GENERATION = True
RUN_JOBS_ONCE = True
CHANGES = []
threads = []
def send_nginx_configs():
LOGGER.info(f"Sending {join(sep, 'etc', 'nginx')} folder ...")

View file

@ -76,21 +76,29 @@ def on_starting(server):
USER = User(**USER)
if getenv("ADMIN_USERNAME") or getenv("ADMIN_PASSWORD"):
if USER.method == "manual":
override_admin_creds = getenv("OVERRIDE_ADMIN_CREDS", "no").lower() == "yes"
if USER.method == "manual" or override_admin_creds:
updated = False
if getenv("ADMIN_USERNAME", "") and USER.get_id() != getenv("ADMIN_USERNAME", ""):
USER.id = getenv("ADMIN_USERNAME", "")
updated = True
if getenv("ADMIN_PASSWORD", "") and not USER.check_password(getenv("ADMIN_PASSWORD", "")):
USER.update_password(getenv("ADMIN_PASSWORD", ""))
updated = True
if not USER_PASSWORD_RX.match(getenv("ADMIN_PASSWORD", "")):
LOGGER.warning(
"The admin password is not strong enough. It must contain at least 8 characters, including at least 1 uppercase letter, 1 lowercase letter, 1 number and 1 special character (#@?!$%^&*-). It will not be updated."
)
else:
USER.update_password(getenv("ADMIN_PASSWORD", ""))
updated = True
if updated:
ret = db.update_ui_user(USER.get_id(), USER.password_hash, USER.is_two_factor_enabled, USER.secret_token)
if ret:
LOGGER.error(f"Couldn't update the admin user in the database: {ret}")
exit(1)
LOGGER.info("The admin user was updated successfully")
if override_admin_creds:
LOGGER.warning("Overriding the admin user credentials, as the OVERRIDE_ADMIN_CREDS environment variable is set to 'yes'.")
err = db.update_ui_user(USER.get_id(), USER.password_hash, USER.is_two_factor_enabled, USER.secret_token, method="manual")
if err:
LOGGER.error(f"Couldn't update the admin user in the database: {err}")
else:
LOGGER.info("The admin user was updated successfully")
else:
LOGGER.warning("The admin user wasn't created manually. You can't change it from the environment variables.")
elif getenv("ADMIN_USERNAME") and getenv("ADMIN_PASSWORD"):

View file

@ -568,6 +568,7 @@ def setup():
"REVERSE_PROXY_HOST": request.form["ui_host"],
"REVERSE_PROXY_URL": request.form["ui_url"] or "/",
"AUTO_LETS_ENCRYPT": request.form.get("auto_lets_encrypt", "no"),
"GENERATE_SELF_SIGNED_SSL": "yes" if request.form.get("auto_lets_encrypt", "no") == "no" else "no",
"INTERCEPTED_ERROR_CODES": "400 404 405 413 429 500 501 502 503 504",
"MAX_CLIENT_SIZE": "50m",
},

View file

@ -320,7 +320,7 @@
Your BunkerWeb UI final URL will be
</h5>
<p class="family-text text-center text-sm md:text-base break-words w-full px-4"
data-resume>http://</p>
data-resume>https://</p>
</div>
<div class="col-span-12 flex justify-center">
<button tabindex="2"
@ -370,7 +370,7 @@
e.preventDefault();
this.updateCheck("unknown");
// get resume
const api = `http://${this.servInp.value}/setup/check`;
const api = `${location.protocol}://${this.servInp.value}/setup/check`;
fetch(api)
.then((res) => {
this.updateCheck("success");
@ -437,14 +437,12 @@
}
updateResume() {
this.servInp.value = this.servInp.value.replace('https://', '').replace('http://', '');
this.servInp.value = this.servInp.value.replace('https://', '');
if (!this.urlInp.value.startsWith("/")) {
this.urlInp.value = "/" + this.urlInp.value;
}
this.urlInp.value = this.urlInp.value.replace("//", "/");
this.resumeEl.textContent = `http${
this.sslCheck.getAttribute("data-checked") === "true" ? "s" : ""
}://${this.servInp.value}${this.urlInp.value}`;
this.resumeEl.textContent = `https://${this.servInp.value}${this.urlInp.value}`;
}
}
@ -600,9 +598,7 @@
// send form and wait for response
let api = `http${
this.sslCheck.getAttribute("data-checked") === "true" ? "s" : ""
}://${this.servInp.value}${this.urlInp.value}`;
let api = `https://${this.servInp.value}${this.urlInp.value}`;
if (!api.endsWith("/")) {
api = `${api}/`;
}