Merge pull request #477 from bunkerity/ui

Merge branch "ui" into branch "dev"
This commit is contained in:
Théophile Diot 2023-05-20 17:55:22 -04:00 committed by GitHub
commit 99f8f69fa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 286 additions and 289 deletions

View File

@ -8,14 +8,18 @@ RUN mkdir -p /usr/share/bunkerweb/deps && \
cat /tmp/req/requirements.txt /tmp/req/requirements.txt.1 > /usr/share/bunkerweb/deps/requirements.txt && \
rm -rf /tmp/req
# Install dependencies
RUN apk add --no-cache --virtual .build-deps g++ gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev openssl-dev cargo postgresql-dev && \
# Install python dependencies
RUN apk add --no-cache --virtual .build-deps g++ gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev openssl-dev cargo postgresql-dev
# Install python requirements
RUN export MAKEFLAGS="-j$(nproc)" && \
pip install --no-cache-dir --upgrade pip && \
pip install wheel && \
pip install --no-cache-dir --upgrade wheel && \
mkdir -p /usr/share/bunkerweb/deps/python && \
export MAKEFLAGS="-j$(nproc)" && \
pip install --no-cache-dir --require-hashes --target /usr/share/bunkerweb/deps/python -r /usr/share/bunkerweb/deps/requirements.txt && \
apk del .build-deps
pip install --no-cache-dir --require-hashes --target /usr/share/bunkerweb/deps/python -r /usr/share/bunkerweb/deps/requirements.txt
# Remove build dependencies
RUN apk del .build-deps
# Copy files
# can't exclude specific files/dir from . so we are copying everything by hand

View File

@ -725,7 +725,7 @@ class Database:
config[setting.id] = (
default
if methods is False
else {"value": default, "method": "default"}
else {"value": default, "global": True, "method": "default"}
)
global_values = (
@ -750,6 +750,7 @@ class Database:
if methods is False
else {
"value": global_value.value,
"global": True,
"method": global_value.method,
}
)
@ -798,13 +799,16 @@ class Database:
if methods is False
else {
"value": service_setting.value,
"global": False,
"method": service_setting.method,
}
)
servers = " ".join(service.id for service in session.query(Services).all())
config["SERVER_NAME"] = (
servers if methods is False else {"value": servers, "method": "default"}
servers
if methods is False
else {"value": servers, "global": True, "method": "default"}
)
return config
@ -852,7 +856,7 @@ class Database:
tmp_config.pop(key)
else:
tmp_config[key] = (
{"value": value["value"], "method": "default"}
{"value": value["value"], "global": True, "method": "default"}
if methods is True
else value
)

View File

@ -9,15 +9,18 @@ RUN mkdir -p /usr/share/bunkerweb/deps && \
cat /tmp/req/requirements.txt /tmp/req/requirements.txt.1 /tmp/req/requirements.txt.2 > /usr/share/bunkerweb/deps/requirements.txt && \
rm -rf /tmp/req
# Install python dependencies
RUN apk add --no-cache --virtual .build-deps g++ gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev openssl-dev cargo postgresql-dev
# Install python requirements
RUN apk add --no-cache --virtual .build-deps g++ gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev openssl-dev cargo postgresql-dev && \
RUN export MAKEFLAGS="-j$(nproc)" && \
pip install --no-cache-dir --upgrade pip && \
pip install wheel && \
pip install --no-cache-dir --upgrade wheel && \
mkdir -p /usr/share/bunkerweb/deps/python && \
export MAKEFLAGS="-j$(nproc)" && \
pip install --no-cache-dir --require-hashes --target /usr/share/bunkerweb/deps/python -r /usr/share/bunkerweb/deps/requirements.txt && \
pip install --no-cache-dir gunicorn && \
apk del .build-deps
pip install --no-cache-dir --require-hashes --target /usr/share/bunkerweb/deps/python -r /usr/share/bunkerweb/deps/requirements.txt
# Remove build dependencies
RUN apk del .build-deps
# Copy files
# can't exclude specific files/dir from . so we are copying everything by hand

View File

@ -9,14 +9,18 @@ RUN mkdir -p /usr/share/bunkerweb/deps && \
cat /tmp/req/requirements.txt /tmp/req/requirements.txt.1 /tmp/req/requirements.txt.2 > /usr/share/bunkerweb/deps/requirements.txt && \
rm -rf /tmp/req
# Install python dependencies
RUN apk add --no-cache --virtual .build-deps g++ gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev openssl-dev cargo postgresql-dev
# Install python requirements
RUN apk add --no-cache --virtual .build-deps g++ gcc musl-dev jpeg-dev zlib-dev libffi-dev cairo-dev pango-dev gdk-pixbuf-dev openssl-dev cargo file make postgresql-dev && \
RUN export MAKEFLAGS="-j$(nproc)" && \
pip install --no-cache-dir --upgrade pip && \
pip install wheel && \
pip install --no-cache-dir --upgrade wheel && \
mkdir -p /usr/share/bunkerweb/deps/python && \
export MAKEFLAGS="-j$(nproc)" && \
pip install --no-cache-dir --require-hashes --target /usr/share/bunkerweb/deps/python -r /usr/share/bunkerweb/deps/requirements.txt && \
apk del .build-deps
pip install --no-cache-dir --require-hashes --target /usr/share/bunkerweb/deps/python -r /usr/share/bunkerweb/deps/requirements.txt
# Remove build dependencies
RUN apk del .build-deps
# Copy files
# can't exclude specific files/dir from . so we are copying everything by hand

View File

@ -98,62 +98,19 @@ class Config:
def get_plugins(
self, *, external: bool = False, with_data: bool = False
) -> List[dict]:
plugins = []
for foldername in list(iglob("/etc/bunkerweb/plugins/*")) + (
list(iglob("/usr/share/bunkerweb/core/*") if not external else [])
):
content = listdir(foldername)
if "plugin.json" not in content:
continue
with open(f"{foldername}/plugin.json", "r") as f:
plugin = json_load(f)
plugin.update(
{
"page": False,
"external": foldername.startswith("/etc/bunkerweb/plugins"),
}
)
plugin["method"] = "ui" if plugin["external"] else "manual"
if "ui" in content:
if "template.html" in listdir(f"{foldername}/ui"):
plugin["page"] = True
if with_data:
plugin_content = BytesIO()
with tar_open(
fileobj=plugin_content, mode="w:gz", compresslevel=9
) as tar:
tar.add(foldername, arcname=basename(foldername), recursive=True)
plugin_content.seek(0)
value = plugin_content.getvalue()
plugin["data"] = value
plugin["checksum"] = sha256(value).hexdigest()
plugins.append(plugin)
plugins = self.__db.get_plugins(external=external, with_data=with_data)
plugins.sort(key=lambda x: x["name"])
with open("/usr/share/bunkerweb/settings.json", "r") as f:
plugins.insert(
0,
{
"id": "general",
"name": "General",
"description": "The general settings for the server",
"version": "0.1",
"stream": "partial",
"external": False,
"method": "manual",
"page": False,
"settings": json_load(f),
},
)
general_plugin = None
for plugin in plugins.copy():
if plugin["id"] == "general":
general_plugin = plugin
plugins.remove(plugin)
break
if general_plugin:
plugins.insert(0, general_plugin)
return plugins

View File

@ -169,33 +169,32 @@ class ServiceModal {
}
//SET METHOD
this.setDisabled(inp, defaultMethod);
this.setDisabledDefault(inp, defaultMethod);
});
const selects = this.modal.querySelectorAll("select");
selects.forEach((select) => {
const defaultMethod = "default";
const defaultVal = select.getAttribute("data-default-value") || "";
document
.querySelector(
`[data-setting-select=${select.getAttribute(
"data-setting-select-default"
)}]`
)
.removeAttribute("disabled");
const defaultMethod = select.getAttribute("data-default-method");
const defaultVal = select.getAttribute("data-default-value");
//click the custom select dropdown to update select value
select.parentElement
.querySelector(
`button[data-setting-select-dropdown-btn][value='${defaultVal}']`
)
.click();
this.setDisabled(select, defaultMethod);
//set state to custom visible el
const btnCustom = document.querySelector(
`[data-setting-select=${select.getAttribute(
"data-setting-select-default"
)}]`
);
this.setDisabledDefault(btnCustom, defaultMethod);
});
}
setDisabled(inp, method) {
setDisabledDefault(inp, method) {
if (method === "ui" || method === "default") {
inp.removeAttribute("disabled");
} else {
@ -279,6 +278,7 @@ class ServiceModal {
//change format to match id
const value = data["value"];
const method = data["method"];
const global = data["global"];
try {
const inps = this.modal.querySelectorAll(`[name='${key}']`);
@ -326,12 +326,22 @@ class ServiceModal {
}
//check disabled/enabled after setting values and methods
this.setDisabled(inp, method);
this.setDisabledServ(inp, method, global);
});
} catch (err) {}
}
}
setDisabledServ(inp, method, global) {
if (global) return inp.removeAttribute("disabled");
if (method === "ui" || method === "default") {
inp.removeAttribute("disabled");
} else {
inp.setAttribute("disabled", "");
}
}
//UTILS
toggleModal() {
this.modal.classList.toggle("hidden");
@ -366,7 +376,7 @@ class Multiple {
});
this.container.addEventListener("click", (e) => {
//edit button
//edit service button
try {
if (
e.target.closest("button").getAttribute("data-services-action") ===
@ -387,7 +397,7 @@ class Multiple {
this.setMultipleToDOM(sortMultiples);
}
} catch (err) {}
//new button
//new service button
try {
if (
e.target.closest("button").getAttribute("data-services-action") ===
@ -439,8 +449,10 @@ class Multiple {
);
//clone schema to create a group with new num
const schemaClone = schema.cloneNode(true);
//add special attribut for disabled logic
this.changeCloneSuffix(schemaClone, setNum);
this.setDisabled();
//set disabled / enabled state
this.setDisabledMultNew(schemaClone);
this.showClone(schema, schemaClone);
//insert new group before first one
//show all groups
@ -576,6 +588,7 @@ class Multiple {
multiples[key] = {
value: data["value"],
method: data["method"],
global: data["global"],
};
}
}
@ -609,16 +622,18 @@ class Multiple {
const settingContainer = schemaCtnrClone.querySelector(
`[data-setting-container="${name}"]`
);
//replace input info
this.setSetting(data["value"], data["method"], settingContainer);
//replace input info and disabled state
this.setSetting(
data["value"],
data["method"],
data["global"],
settingContainer
);
}
//send schema clone to DOM and show it
this.showClone(schemaCtnr, schemaCtnrClone);
}
}
//disabled after update values and method
this.setDisabled();
}
changeCloneSuffix(schemaCtnrClone, suffix) {
@ -677,7 +692,7 @@ class Multiple {
});
}
setSetting(value, method, settingContainer) {
setSetting(value, method, global, settingContainer) {
//update input
try {
const inps = settingContainer.querySelectorAll("input");
@ -707,27 +722,29 @@ class Multiple {
inp.value = value;
inp.setAttribute("data-method", method);
}
this.setDisabledMultServ(inp, method, global);
});
} catch (err) {}
//update select
try {
const select = settingContainer.querySelector("select");
const name = select.getAttribute("data-services-setting-select-default");
const selTxt = document.querySelector(
`[data-services-setting-select-text='${name}']`
select.setAttribute("data-method", method);
//click the custom select dropdown btn vavlue to update select value
select.parentElement
.querySelector(
`button[data-setting-select-dropdown-btn][value='${defaultVal}']`
)
.click();
//set state to custom visible el
const btnCustom = document.querySelector(
`[data-setting-select=${select.getAttribute(
"data-setting-select-default"
)}]`
);
selTxt.textContent = value;
selTxt.setAttribute("value", value);
const options = select.options;
for (let i = 0; i < options.length; i++) {
const option = options[i];
option.value === value
? option.setAttribute("selected")
: option.removeAttribute("selected");
}
select.setAttribute("data-method", method);
this.setDisabledMultServ(btnCustom, method, global);
} catch (err) {}
}
@ -737,48 +754,55 @@ class Multiple {
schemaCtnrClone.classList.add("grid");
}
setDisabled() {
const multipleCtnr = document.querySelectorAll(
"[data-services-settings-multiple]"
);
multipleCtnr.forEach((container) => {
const settings = container.querySelectorAll("[data-setting-container]");
//global value isn't check at this point
setDisabledMultNew(container) {
const settings = container.querySelectorAll("[data-setting-container]");
settings.forEach((setting) => {
//replace input info
try {
const inps = setting.querySelectorAll("input");
inps.forEach((inp) => {
const method = inp.getAttribute("data-method") || "default";
if (method === "ui" || method === "default") {
inp.removeAttribute("disabled");
} else {
inp.setAttribute("disabled", "");
}
});
} catch (err) {}
//or select
try {
const selects = setting.querySelectorAll("select");
selects.forEach((select) => {
const method = select.getAttribute("data-method") || "default";
const name = select.getAttribute(
"data-services-setting-select-default"
);
const selDOM = document.querySelector(
`button[data-services-setting-select='${name}']`
);
if (method === "ui" || method === "default") {
selDOM.removeAttribute("disabled", "");
} else {
selDOM.setAttribute("disabled", "");
}
});
} catch (err) {}
});
settings.forEach((setting) => {
//replace input info
try {
const inps = setting.querySelectorAll("input");
inps.forEach((inp) => {
const method = inp.getAttribute("data-default-method");
if (method === "ui" || method === "default") {
inp.removeAttribute("disabled");
} else {
inp.setAttribute("disabled", "");
}
});
} catch (err) {}
//or select
try {
const selects = setting.querySelectorAll("select");
selects.forEach((select) => {
const method = select.getAttribute("data-default-method");
const name = select.getAttribute(
"data-services-setting-select-default"
);
const selDOM = document.querySelector(
`button[data-services-setting-select='${name}']`
);
if (method === "ui" || method === "default") {
selDOM.removeAttribute("disabled", "");
} else {
selDOM.setAttribute("disabled", "");
}
});
} catch (err) {}
});
}
//for already existing services multiples
//global is check
setDisabledMultServ(inp, method, global) {
if (global) return inp.removeAttribute("disabled");
if (method === "ui" || method === "default") {
inp.removeAttribute("disabled");
} else {
inp.setAttribute("disabled", "");
}
}
//UTILS
getSuffixNumOrFalse(name) {

View File

@ -18,19 +18,18 @@ COPY requirements.txt .
RUN pip install --no-cache -r requirements.txt
RUN touch test.txt && \
zip -r test.zip test.txt && \
zip test.zip test.txt && \
rm test.txt
RUN echo '{ \
"id": "discord", \
"order": 999, \
"name": "Discord", \
"description": "Send alerts to a Discord channel (using webhooks).", \
"version": "0.1", \
"stream": "no", \
"settings": {} \
}' > plugin.json && \
zip -r discord.zip plugin.json && \
zip discord.zip plugin.json && \
rm plugin.json
# Clean up

View File

@ -21,156 +21,155 @@ from selenium.common.exceptions import (
TimeoutException,
)
try:
ready = False
retries = 0
while not ready:
with suppress(RequestException):
status_code = get("http://www.example.com/admin").status_code
ready = False
retries = 0
while not ready:
with suppress(RequestException):
status_code = get("http://www.example.com/admin").status_code
if status_code > 500:
print("An error occurred with the server, exiting ...", flush=True)
exit(1)
ready = status_code < 400
if retries > 10:
print("UI took too long to be ready, exiting ...", flush=True)
exit(1)
elif not ready:
retries += 1
print("Waiting for UI to be ready, retrying in 5s ...", flush=True)
sleep(5)
print("UI is ready, starting tests ...", flush=True)
firefox_options = Options()
if "geckodriver" not in listdir(Path.cwd()):
firefox_options.add_argument("--headless")
print("Starting Firefox ...", flush=True)
def safe_get_element(
driver, by: By, _id: str, *, multiple: bool = False, error: bool = False
) -> Union[WebElement, List[WebElement]]:
try:
return WebDriverWait(driver, 4).until(
EC.presence_of_element_located((by, _id))
if not multiple
else EC.presence_of_all_elements_located((by, _id))
)
except TimeoutException as e:
if error:
raise e
print(
f'Element searched by {by}: "{_id}" not found, exiting ...', flush=True
)
if status_code > 500:
print("An error occurred with the server, exiting ...", flush=True)
exit(1)
def assert_button_click(driver, button: Union[str, WebElement]):
clicked = False
while not clicked:
with suppress(ElementClickInterceptedException):
if isinstance(button, str):
button = safe_get_element(driver, By.XPATH, button)
ready = status_code < 400
sleep(0.5)
if retries > 10:
print("UI took too long to be ready, exiting ...", flush=True)
exit(1)
elif not ready:
retries += 1
print("Waiting for UI to be ready, retrying in 5s ...", flush=True)
sleep(5)
button.click()
clicked = True
print("UI is ready, starting tests ...", flush=True)
def assert_alert_message(driver, message: str):
safe_get_element(driver, By.XPATH, "//button[@data-flash-sidebar-open='']")
firefox_options = Options()
if "geckodriver" not in listdir(Path.cwd()):
firefox_options.add_argument("--headless")
sleep(0.3)
print("Starting Firefox ...", flush=True)
assert_button_click(driver, "//button[@data-flash-sidebar-open='']")
error = False
while True:
try:
alerts = safe_get_element(
driver,
By.XPATH,
"//aside[@data-flash-sidebar='']/div[2]/div",
multiple=True,
error=True,
)
break
except TimeoutException:
if error:
print("Messages list not found, exiting ...", flush=True)
exit(1)
error = True
driver.refresh()
is_in = False
for alert in alerts:
if message in alert.text:
is_in = True
break
if not is_in:
print(
f'Message "{message}" not found in one of the messages in the list, exiting ...',
flush=True,
)
exit(1)
print(
f'Message "{message}" found in one of the messages in the list', flush=True
def safe_get_element(
driver, by: By, _id: str, *, multiple: bool = False, error: bool = False
) -> Union[WebElement, List[WebElement]]:
try:
return WebDriverWait(driver, 4).until(
EC.presence_of_element_located((by, _id))
if not multiple
else EC.presence_of_all_elements_located((by, _id))
)
except TimeoutException as e:
if error:
raise e
assert_button_click(
driver, "//aside[@data-flash-sidebar='']/*[local-name() = 'svg']"
)
print(f'Element searched by {by}: "{_id}" not found, exiting ...', flush=True)
exit(1)
def access_page(
driver,
driver_wait: WebDriverWait,
button: Union[str, WebElement],
name: str,
message: bool = True,
*,
retries: int = 0,
):
assert_button_click(driver, button)
def assert_button_click(driver, button: Union[str, WebElement]):
clicked = False
while not clicked:
with suppress(ElementClickInterceptedException):
if isinstance(button, str):
button = safe_get_element(driver, By.XPATH, button)
sleep(0.5)
button.click()
clicked = True
def assert_alert_message(driver, message: str):
safe_get_element(driver, By.XPATH, "//button[@data-flash-sidebar-open='']")
sleep(0.3)
assert_button_click(driver, "//button[@data-flash-sidebar-open='']")
error = False
while True:
try:
title = driver_wait.until(
EC.presence_of_element_located(
(By.XPATH, "/html/body/div/header/div/nav/h6")
)
alerts = safe_get_element(
driver,
By.XPATH,
"//aside[@data-flash-sidebar='']/div[2]/div",
multiple=True,
error=True,
)
if title.text != name.replace(" ", "_").title():
print(f"Didn't get redirected to {name} page, exiting ...", flush=True)
exit(1)
break
except TimeoutException:
if retries < 3 and driver.current_url.split("/")[-1].startswith("/loading"):
sleep(2)
access_page(
driver, driver_wait, button, name, message, retries=retries + 1
)
if error:
print("Messages list not found, exiting ...", flush=True)
exit(1)
error = True
driver.refresh()
print(f"{name.title()} page didn't load in time, exiting ...", flush=True)
exit(1)
is_in = False
for alert in alerts:
if message in alert.text:
is_in = True
break
if message:
print(
f"{name.title()} page loaded successfully",
flush=True,
if not is_in:
print(
f'Message "{message}" not found in one of the messages in the list, exiting ...',
flush=True,
)
exit(1)
print(f'Message "{message}" found in one of the messages in the list', flush=True)
assert_button_click(
driver, "//aside[@data-flash-sidebar='']/*[local-name() = 'svg']"
)
def access_page(
driver,
driver_wait: WebDriverWait,
button: Union[str, WebElement],
name: str,
message: bool = True,
*,
retries: int = 0,
):
assert_button_click(driver, button)
try:
title = driver_wait.until(
EC.presence_of_element_located(
(By.XPATH, "/html/body/div/header/div/nav/h6")
)
)
with webdriver.Firefox(
service=Service(
executable_path="./geckodriver"
if "geckodriver" in listdir(Path.cwd())
else "/usr/local/bin/geckodriver"
),
options=firefox_options,
) as driver:
if title.text != name.replace(" ", "_").title():
print(f"Didn't get redirected to {name} page, exiting ...", flush=True)
exit(1)
except TimeoutException:
if retries < 3 and driver.current_url.split("/")[-1].startswith("/loading"):
sleep(2)
access_page(driver, driver_wait, button, name, message, retries=retries + 1)
print(f"{name.title()} page didn't load in time, exiting ...", flush=True)
exit(1)
if message:
print(
f"{name.title()} page loaded successfully",
flush=True,
)
with webdriver.Firefox(
service=Service(
executable_path="./geckodriver"
if "geckodriver" in listdir(Path.cwd())
else "/usr/local/bin/geckodriver"
),
options=firefox_options,
) as driver:
try:
driver.delete_all_cookies()
driver.maximize_window()
driver_wait = WebDriverWait(driver, 30)
@ -901,6 +900,8 @@ try:
driver, By.XPATH, "//input[@type='file' and @name='file']"
).send_keys(join(Path.cwd(), "test.zip"))
sleep(2)
access_page(
driver,
driver_wait,
@ -909,8 +910,6 @@ try:
False,
)
sleep(2)
print(
"The bad plugin has been rejected, trying to add a good plugin ...",
flush=True,
@ -920,6 +919,8 @@ try:
driver, By.XPATH, "//input[@type='file' and @name='file']"
).send_keys(join(Path.cwd(), "discord.zip"))
sleep(2)
access_page(
driver,
driver_wait,
@ -1300,10 +1301,11 @@ try:
exit(1)
print("Successfully logged out, tests are done", flush=True)
except SystemExit:
exit(1)
except:
print(f"Something went wrong, exiting ...\n{format_exc()}", flush=True)
exit(1)
except SystemExit:
exit(1)
except:
print(f"Something went wrong, exiting ...\n{format_exc()}", flush=True)
driver.save_screenshot("error.png")
exit(1)
exit(0)