Merge pull request #1003 from bunkerity/dev

Merge branch "dev" into branch "staging"
This commit is contained in:
Théophile Diot 2024-03-22 18:15:41 +00:00 committed by GitHub
commit 7f7792d258
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 334 additions and 436 deletions

View file

@ -153,6 +153,47 @@ Here is the list of related settings :
Full Let's Encrypt automation is fully working with stream mode as long as you open the `80/tcp` port from the outside. Please note that you will need to use the `LISTEN_STREAM_PORT_SSL` setting in order to choose your listening SSL/TLS port.
### Let's Encrypt DNS <img src='/assets/img/pro-icon.svg' alt='crow pro icon' height='32px' width='32px'> (PRO)
STREAM support :white_check_mark:
The Let's Encrypt DNS plugin facilitates the automatic creation, renewal, and configuration of Let's Encrypt certificates using DNS challenges. This plugin offers seamless integration with various DNS providers for streamlined certificate management.
- Automatic creation and renewal of Let's Encrypt certificates
- Integration with DNS providers for DNS challenges
- Generate wildcard certificates
- Configuration options for customization and flexibility
Settings of the Let's Encrypt DNS plugin :
| Setting | Default | Context | Multiple | Description |
| ---------------------------------- | --------- | --------- | -------- | --------------------------------------------------------------------------------------- |
| `AUTO_LETS_ENCRYPT_DNS` | `no` | multisite | no | Set to `yes` to enable automatic certificate creation and renewal using DNS challenges. |
| `LETS_ENCRYPT_DNS_EMAIL` | | multisite | no | Email address for Let's Encrypt notifications. |
| `USE_LETS_ENCRYPT_DNS_STAGING` | `no` | multisite | no | Set to `yes` to use Let's Encrypt staging server. |
| `LETS_ENCRYPT_DNS_PROVIDER` | | multisite | no | DNS provider for Let's Encrypt DNS challenges. |
| `USE_LETS_ENCRYPT_DNS_WILDCARD` | `no` | multisite | no | Set to `yes` to automatically generate wildcard domains in certificates. |
| `LETS_ENCRYPT_DNS_PROPAGATION` | `default` | multisite | no | Time in seconds to wait for DNS propagation. |
| `LETS_ENCRYPT_DNS_CREDENTIAL_ITEM` | | multisite | yes | Credential item for Let's Encrypt DNS provider that contains required credentials. |
Info :
- The `LETS_ENCRYPT_DNS_CREDENTIAL_ITEM` setting is a multiple setting and can be used to set multiple items for the DNS provider. The items will be saved as a cache file and Certbot will read the credentials from it.
- If no `LETS_ENCRYPT_DNS_PROPAGATION` setting is set, the provider's default propagation time will be used.
Available DNS Providers :
| Provider | Description | Mandatory Settings | Link(s) |
| -------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `cloudflare` | Cloudflare DNS provider | `dns_cloudflare_api_token` | [Documentation](https://certbot-dns-cloudflare.readthedocs.io/en/stable/) |
| `digitalocean` | DigitalOcean DNS provider | `dns_digitalocean_token` | [Documentation](https://certbot-dns-digitalocean.readthedocs.io/en/stable/) |
| `google` | Google Cloud DNS provider | `project_id`, `private_key_id`, `private_key`, `client_email`, `client_email`, `client_x509_cert_url` | [Documentation](https://certbot-dns-google.readthedocs.io/en/stable/) |
| `linode` | Linode DNS provider | `dns_linode_key` | [Documentation](https://certbot-dns-linode.readthedocs.io/en/stable/) |
| `ovh` | OVH DNS provider | `dns_ovh_application_key`, `dns_ovh_application_secret`, `dns_ovh_consumer_key` | [Documentation](https://certbot-dns-ovh.readthedocs.io/en/stable/) |
| `rfc2136` | RFC 2136 DNS provider | `dns_rfc2136_server`, `dns_rfc2136_name`, `dns_rfc2136_secret` | [Documentation](https://certbot-dns-rfc2136.readthedocs.io/en/stable/) |
| `route53` | Amazon Route 53 DNS provider | `aws_access_key_id`, `aws_secret_access_key` | [Documentation](https://certbot-dns-route53.readthedocs.io/en/stable/) |
| `scaleway` | Scaleway DNS provider | `dns_scaleway_application_token` | [Documentation](https://github.com/vanonox/certbot-dns-scaleway/blob/main/README.rst) |
### Custom certificate
STREAM support :white_check_mark:
@ -165,7 +206,6 @@ If you want to use your own certificates, here is the list of related settings :
| `CUSTOM_SSL_CERT` | | multisite | no | Full path of the certificate or bundle file (must be readable by the scheduler). |
| `CUSTOM_SSL_KEY` | | multisite | no | Full path of the key file (must be readable by the scheduler). |
When `USE_CUSTOM_SSL` is set to `yes`, BunkerWeb will check every day if the custom certificate specified in `CUSTOM_SSL_CERT` is modified and will reload NGINX if that's the case.
When using stream mode, you will need to use the `LISTEN_STREAM_PORT_SSL` setting in order to choose your listening SSL/TLS port.
@ -505,96 +545,71 @@ You can deploy complex authentication (e.g. SSO), by using the auth request sett
## Monitoring and reporting
Monitoring and reporting means that you are kept informed of the slightest problem and can react as quickly as possible.
### Monitoring <img src='/assets/img/pro-icon.svg' alt='crow pro icon' height='32px' width='32px'> (PRO)
### Reporting
TODO
<div style="display:flex; align-items:center">
### Prometheus exporter <img src='/assets/img/pro-icon.svg' alt='crow pro icon' height='32px' width='32px'> (PRO)
<h3 data-custom-header id="reporting">Reporting</h3>
The Prometheus exporter plugin adds a [Prometheus exporter](https://prometheus.io/docs/instrumenting/exporters/) on your BunkerWeb instance(s). When enabled, you can configure your Prometheus instance(s) to scrape a specific endpoint on Bunkerweb and gather internal metrics.
<svg style="height:1.25rem; width:1.25rem; margin-top: 0.70rem; margin-left: 0.5rem"
viewBox="0 0 48 46"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path style="fill:#eab308" d="M43.218 28.2327L43.6765 23.971C43.921 21.6973 44.0825 20.1957 43.9557 19.2497L44 19.25C46.071 19.25 47.75 17.5711 47.75 15.5C47.75 13.4289 46.071 11.75 44 11.75C41.929 11.75 40.25 13.4289 40.25 15.5C40.25 16.4366 40.5935 17.2931 41.1613 17.9503C40.346 18.4535 39.2805 19.515 37.6763 21.1128C36.4405 22.3438 35.8225 22.9593 35.1333 23.0548C34.7513 23.1075 34.3622 23.0532 34.0095 22.898C33.373 22.6175 32.9485 21.8567 32.0997 20.335L27.6262 12.3135C27.1025 11.3747 26.6642 10.5889 26.2692 9.95662C27.89 9.12967 29 7.44445 29 5.5C29 2.73857 26.7615 0.5 24 0.5C21.2385 0.5 19 2.73857 19 5.5C19 7.44445 20.11 9.12967 21.7308 9.95662C21.3358 10.589 20.8975 11.3746 20.3738 12.3135L15.9002 20.335C15.0514 21.8567 14.627 22.6175 13.9905 22.898C13.6379 23.0532 13.2487 23.1075 12.8668 23.0548C12.1774 22.9593 11.5595 22.3438 10.3238 21.1128C8.71968 19.515 7.6539 18.4535 6.83882 17.9503C7.4066 17.2931 7.75 16.4366 7.75 15.5C7.75 13.4289 6.07107 11.75 4 11.75C1.92893 11.75 0.25 13.4289 0.25 15.5C0.25 17.5711 1.92893 19.25 4 19.25L4.04428 19.2497C3.91755 20.1957 4.07905 21.6973 4.32362 23.971L4.782 28.2327C5.03645 30.5982 5.24802 32.849 5.50717 34.875H42.4928C42.752 32.849 42.9635 30.5982 43.218 28.2327Z" fill="#1C274C" />
<path style="fill:#eab308" d="M21.2803 45.5H26.7198C33.8098 45.5 37.3545 45.5 39.7198 43.383C40.7523 42.4588 41.4057 40.793 41.8775 38.625H6.1224C6.59413 40.793 7.24783 42.4588 8.2802 43.383C10.6454 45.5 14.1903 45.5 21.2803 45.5Z" fill="#1C274C" />
</svg>
</div>
We also provide a [Grafana dashboard](https://grafana.com/grafana/dashboards/20755) that you can import into your own instance and connect to your own Prometheus datasource.
!!! warning "Used of cache data"
**Please note that the use of Prometheus exporter plugin requires to enable the Monitoring plugin (`USE_MONITORING=yes`)**
A comparison is made every hour with the cached data. If BunkerWeb no longer has access to the cache, the data to be compared will be reset.
List of features :
#### Types of reporting
- Prometheus exporter providing internal BunkerWeb metrics
- Dedicated and configurable port, listen IP and URL
- Whitelist IP/network for maximum security
Pro reporting plugin gives you two types of reports :
List of settings :
- **regular report**: you can define a period of time, and you'll get a regular report showing the percentage change in data between the previous report and this one, and also key points about your BunkerWeb state.
| Setting | Default |Context|Multiple| Description |
|------------------------------|-----------------------------------------------------|-------|--------|------------------------------------------------------------------------|
|`USE_PROMETHEUS_EXPORTER` |`no` |global |no |Enable the Prometheus export. |
|`PROMETHEUS_EXPORTER_IP` |`0.0.0.0` |global |no |Listening IP of the Prometheus exporter. |
|`PROMETHEUS_EXPORTER_PORT` |`9113` |global |no |Listening port of the Prometheus exporter. |
|`PROMETHEUS_EXPORTER_URL` |`/metrics` |global |no |HTTP URL of the Prometheus exporter. |
|`PROMETHEUS_EXPORTER_ALLOW_IP`|`127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16`|global |no |List of IP/networks allowed to contact the Prometheus exporter endpoint.|
- **alerts**: every hour, an analysis of the metrics will be carried out, and you can set a threshold for the percentage change in the data. If this threshold is reached, you will receive an alert.
### Reporting <img src='/assets/img/pro-icon.svg' alt='crow pro icon' height='32px' width='32px'> (PRO)
!!! info "Example"
The Reporting plugin provides a comprehensive solution for regular reporting of important data from BunkerWeb, including global statistics, attacks, bans, requests, reasons, and AS information. It offers a wide range of features, including automatic report creation, customization options, and seamless integration with monitoring pro plugin. With the Reporting plugin, you can easily generate and manage reports to monitor the performance and security of your application.
After one hour, if I go from 300 requests blocked to more than 600 after one hour : in case I have set a threshold of +100%, I'll be alerted.
List of features :
#### Get reporting
- Regular reporting of important data from BunkerWeb, including global statistics, attacks, bans, requests, reasons, and AS information.
- Integration with Monitoring Pro plugin for seamless integration and enhanced reporting capabilities.
- Support for webhooks (classic, Discord, and Slack) for real-time notifications.
- Support for SMTP for email notifications.
- Configuration options for customization and flexibility.
To receive alerts or regular reports, you can use :
List of settings :
**1) webhook**
| Setting | Default | Context | Description |
| ------------------------------ | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `USE_REPORTING_SMTP` | `no` | `global` | Enable sending the report via email. |
| `USE_REPORTING_WEBHOOK` | `no` | `global` | Enable sending the report via webhook. |
| `REPORTING_SCHEDULE` | `weekly` | `global` | The frequency at which reports are sent. |
| `REPORTING_WEBHOOK_URLS` | | `global` | List of webhook URLs to receive the report in Markdown (separated by spaces). |
| `REPORTING_SMTP_EMAILS` | | `global` | List of email addresses to receive the report in HTML format (separated by spaces). |
| `REPORTING_SMTP_HOST` | | `global` | The host server used for SMTP sending. |
| `REPORTING_SMTP_PORT` | `465` | `global` | The port used for SMTP. Please note that there are different standards depending on the type of connection (SSL = 465, TLS = 587). |
| `REPORTING_SMTP_FROM_EMAIL` | | `global` | The email address used as the sender. Note that 2FA must be disabled for this email address. |
| `REPORTING_SMTP_FROM_USER` | | `global` | The user authentication value for sending via the from email address. |
| `REPORTING_SMTP_FROM_PASSWORD` | | `global` | The password authentication value for sending via the from email address. |
| `REPORTING_SMTP_SSL` | `SSL` | `global` | Determine whether or not to use a secure connection for SMTP. |
We are supporting multiple webhooks :
**Warning:**
- **API** : we will send a JSON of type `{"message" : markdownReport }`.
- **Discord**
- **Slack**
- This plugins requires the Monitoring Pro plugin to be installed and enabled with the `USE_MONITORING` setting set to `yes`.
!!! info "Specific webhook"
**Info:**
We listen to our customers, so if you need to make the plugin compatible with a particular webhook, don't hesitate to contact us to discuss it together.
**2) SMTP**
You can also use the SMTP protocol. You will need to set the various parameters (user auth, password auth, host...).
You need to **pay attention** using SMTP:
- Make sure that the address used to send the **message does not end up in the spam folder**.
- The address used must **not have double authentication** to work.
### Prometheus exporter
<div style="display:flex; align-items:center">
<h3 data-custom-header id="prometheus-exporter">Prometheus exporter</h3>
<svg style="height:1.25rem; width:1.25rem; margin-top: 0.70rem; margin-left: 0.5rem"
viewBox="0 0 48 46"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path style="fill:#eab308" d="M43.218 28.2327L43.6765 23.971C43.921 21.6973 44.0825 20.1957 43.9557 19.2497L44 19.25C46.071 19.25 47.75 17.5711 47.75 15.5C47.75 13.4289 46.071 11.75 44 11.75C41.929 11.75 40.25 13.4289 40.25 15.5C40.25 16.4366 40.5935 17.2931 41.1613 17.9503C40.346 18.4535 39.2805 19.515 37.6763 21.1128C36.4405 22.3438 35.8225 22.9593 35.1333 23.0548C34.7513 23.1075 34.3622 23.0532 34.0095 22.898C33.373 22.6175 32.9485 21.8567 32.0997 20.335L27.6262 12.3135C27.1025 11.3747 26.6642 10.5889 26.2692 9.95662C27.89 9.12967 29 7.44445 29 5.5C29 2.73857 26.7615 0.5 24 0.5C21.2385 0.5 19 2.73857 19 5.5C19 7.44445 20.11 9.12967 21.7308 9.95662C21.3358 10.589 20.8975 11.3746 20.3738 12.3135L15.9002 20.335C15.0514 21.8567 14.627 22.6175 13.9905 22.898C13.6379 23.0532 13.2487 23.1075 12.8668 23.0548C12.1774 22.9593 11.5595 22.3438 10.3238 21.1128C8.71968 19.515 7.6539 18.4535 6.83882 17.9503C7.4066 17.2931 7.75 16.4366 7.75 15.5C7.75 13.4289 6.07107 11.75 4 11.75C1.92893 11.75 0.25 13.4289 0.25 15.5C0.25 17.5711 1.92893 19.25 4 19.25L4.04428 19.2497C3.91755 20.1957 4.07905 21.6973 4.32362 23.971L4.782 28.2327C5.03645 30.5982 5.24802 32.849 5.50717 34.875H42.4928C42.752 32.849 42.9635 30.5982 43.218 28.2327Z" fill="#1C274C" />
<path style="fill:#eab308" d="M21.2803 45.5H26.7198C33.8098 45.5 37.3545 45.5 39.7198 43.383C40.7523 42.4588 41.4057 40.793 41.8775 38.625H6.1224C6.59413 40.793 7.24783 42.4588 8.2802 43.383C10.6454 45.5 14.1903 45.5 21.2803 45.5Z" fill="#1C274C" />
</svg>
</div>
TO DO
### Pro metrics
<div style="display:flex; align-items:center">
<h3 data-custom-header id="pro-metrics">Pro metrics</h3>
<svg style="height:1.25rem; width:1.25rem; margin-top: 0.70rem; margin-left: 0.5rem"
viewBox="0 0 48 46"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path style="fill:#eab308" d="M43.218 28.2327L43.6765 23.971C43.921 21.6973 44.0825 20.1957 43.9557 19.2497L44 19.25C46.071 19.25 47.75 17.5711 47.75 15.5C47.75 13.4289 46.071 11.75 44 11.75C41.929 11.75 40.25 13.4289 40.25 15.5C40.25 16.4366 40.5935 17.2931 41.1613 17.9503C40.346 18.4535 39.2805 19.515 37.6763 21.1128C36.4405 22.3438 35.8225 22.9593 35.1333 23.0548C34.7513 23.1075 34.3622 23.0532 34.0095 22.898C33.373 22.6175 32.9485 21.8567 32.0997 20.335L27.6262 12.3135C27.1025 11.3747 26.6642 10.5889 26.2692 9.95662C27.89 9.12967 29 7.44445 29 5.5C29 2.73857 26.7615 0.5 24 0.5C21.2385 0.5 19 2.73857 19 5.5C19 7.44445 20.11 9.12967 21.7308 9.95662C21.3358 10.589 20.8975 11.3746 20.3738 12.3135L15.9002 20.335C15.0514 21.8567 14.627 22.6175 13.9905 22.898C13.6379 23.0532 13.2487 23.1075 12.8668 23.0548C12.1774 22.9593 11.5595 22.3438 10.3238 21.1128C8.71968 19.515 7.6539 18.4535 6.83882 17.9503C7.4066 17.2931 7.75 16.4366 7.75 15.5C7.75 13.4289 6.07107 11.75 4 11.75C1.92893 11.75 0.25 13.4289 0.25 15.5C0.25 17.5711 1.92893 19.25 4 19.25L4.04428 19.2497C3.91755 20.1957 4.07905 21.6973 4.32362 23.971L4.782 28.2327C5.03645 30.5982 5.24802 32.849 5.50717 34.875H42.4928C42.752 32.849 42.9635 30.5982 43.218 28.2327Z" fill="#1C274C" />
<path style="fill:#eab308" d="M21.2803 45.5H26.7198C33.8098 45.5 37.3545 45.5 39.7198 43.383C40.7523 42.4588 41.4057 40.793 41.8775 38.625H6.1224C6.59413 40.793 7.24783 42.4588 8.2802 43.383C10.6454 45.5 14.1903 45.5 21.2803 45.5Z" fill="#1C274C" />
</svg>
</div>
TO DO
- If `USE_REPORTING_SMTP` is set to `yes`, the setting `REPORTING_SMTP_EMAILS` must be set.
- If `USE_REPORTING_WEBHOOK` is set to `yes`, the setting `REPORTING_WEBHOOK_URLS` must be set.
- Accepted values for `REPORTING_SCHEDULE` are `daily`, `weekly`and `monthly`.
- If no `REPORTING_SMTP_FROM_USER` and `REPORTING_SMTP_FROM_PASSWORD` are set, the plugin will try to send the email without authentication.
- If `REPORTING_SMTP_FROM_USER` isn't set but `REPORTING_SMTP_FROM_PASSWORD` is set, the plugin will use the `REPORTING_SMTP_FROM_EMAIL` as the username.
- If the job fails, the plugin will retry sending the report in the next execution.

View file

@ -251,6 +251,7 @@ try:
if not plugin_nbr:
LOGGER.info("All Pro plugins are up to date")
db.set_pro_metadata(metadata | {"last_pro_check": current_date})
sys_exit(0)
pro_plugins = []

View file

@ -962,7 +962,7 @@ class Database:
def save_custom_configs(
self,
custom_configs: List[Dict[str, Tuple[str, List[str]]]],
custom_configs: List[Dict[str, Union[str, bytes, Tuple[str, List[str]]]]],
method: str,
changed: Optional[bool] = True,
) -> str:
@ -975,59 +975,57 @@ class Database:
to_put = []
endl = "\n"
for custom_config in custom_configs:
config = {
"data": custom_config["value"].encode("utf-8") if isinstance(custom_config["value"], str) else custom_config["value"],
"method": method,
if method != "ui":
config = {
"data": custom_config["value"],
"method": method,
}
assert isinstance(custom_config["exploded"], tuple) and len(custom_config["exploded"]) == 3, "Invalid exploded custom config"
if custom_config["exploded"][0]:
if not session.query(Services).with_entities(Services.id).filter_by(id=custom_config["exploded"][0]).first():
message += f"{endl if message else ''}Service {custom_config['exploded'][0]} not found, please check your config"
config.update(
{
"service_id": custom_config["exploded"][0],
"type": custom_config["exploded"][1],
"name": custom_config["exploded"][2],
}
)
else:
config.update(
{
"type": custom_config["exploded"][1],
"name": custom_config["exploded"][2],
}
)
custom_config = config
custom_config["type"] = custom_config["type"].replace("-", "_").lower() # type: ignore
custom_config["data"] = custom_config["data"].encode("utf-8") if isinstance(custom_config["data"], str) else custom_config["data"]
custom_config["checksum"] = sha256(custom_config["data"]).hexdigest() # type: ignore
service_id = custom_config.get("service_id", None) or None
filters = {
"type": custom_config["type"],
"name": custom_config["name"],
}
config["checksum"] = sha256(config["data"]).hexdigest()
if custom_config["exploded"][0]:
if not session.query(Services).with_entities(Services.id).filter_by(id=custom_config["exploded"][0]).first():
message += f"{endl if message else ''}Service {custom_config['exploded'][0]} not found, please check your config"
if service_id:
filters["service_id"] = service_id
config.update(
{
"service_id": custom_config["exploded"][0],
"type": custom_config["exploded"][1].replace("-", "_").lower(),
"name": custom_config["exploded"][2],
}
)
else:
config.update(
{
"type": custom_config["exploded"][1].replace("-", "_").lower(),
"name": custom_config["exploded"][2],
}
)
custom_conf = (
session.query(Custom_configs)
.with_entities(Custom_configs.checksum, Custom_configs.method)
.filter_by(
service_id=config.get("service_id", None),
type=config["type"],
name=config["name"],
)
.first()
)
custom_conf = session.query(Custom_configs).with_entities(Custom_configs.checksum, Custom_configs.method).filter_by(**filters).first()
if not custom_conf:
to_put.append(Custom_configs(**config))
elif config["checksum"] != custom_conf.checksum and method in (
custom_conf.method,
"autoconf",
):
session.query(Custom_configs).filter(
Custom_configs.service_id == config.get("service_id", None),
Custom_configs.type == config["type"],
Custom_configs.name == config["name"],
).update(
{
Custom_configs.data: config["data"],
Custom_configs.checksum: config["checksum"],
}
| ({Custom_configs.method: "autoconf"} if method == "autoconf" else {})
)
to_put.append(Custom_configs(**custom_config))
elif custom_config["checksum"] != custom_conf.checksum and method in (custom_conf.method, "autoconf"):
custom_conf.data = custom_config["data"]
custom_conf.checksum = custom_config["checksum"]
if method == "autoconf":
custom_conf.method = method
if changed:
with suppress(ProgrammingError, OperationalError):
metadata = session.query(Metadata).get(1)

View file

@ -2,14 +2,13 @@
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from inspect import getsourcefile
from io import BytesIO
from logging import Logger
from os import getenv
from os.path import sep
from pathlib import Path
from shutil import rmtree
from sys import _getframe
from sys import argv
from tarfile import open as tar_open
from threading import Lock
from traceback import format_exc
@ -28,11 +27,16 @@ EXPIRE_TIME = {
class Job:
def __init__(self, logger: Optional[Logger] = None, db=None, *, job_name: str = "", deprecated: bool = False):
source_file = getsourcefile(_getframe(1))
if not argv:
raise ValueError("argv could not be determined.")
source_file = argv[0]
if source_file is None:
raise ValueError("source_file could not be determined.")
elif not logger and not db:
raise ValueError("Either logger or db must be provided.")
source_path = Path(source_file)
self.job_path = Path(sep, "var", "cache", "bunkerweb", source_path.parent.parent.name)
self.job_name = job_name or source_path.name.replace(".py", "")

View file

@ -7,7 +7,7 @@ from glob import glob
from hashlib import sha256
from io import BytesIO
from json import load as json_load
from os import _exit, chmod, environ, getenv, getpid, listdir, sep, walk
from os import _exit, environ, getenv, getpid, listdir, sep, walk
from os.path import basename, dirname, join, normpath
from pathlib import Path
from shutil import copy, rmtree
@ -148,9 +148,8 @@ def generate_external_plugins(plugins: List[Dict[str, Any]], *, original_path: U
tar.extractall(original_path)
tmp_path.unlink(missing_ok=True)
for job_file in glob(join(str(tmp_path.parent), "jobs", "*")):
st = Path(job_file).stat()
chmod(job_file, st.st_mode | S_IEXEC)
for job_file in original_path.joinpath(plugin["id"], "jobs").glob("*"):
job_file.chmod(job_file.stat().st_mode | S_IEXEC)
except BaseException as e:
logger.error(f"Error while generating {'pro ' if pro else ''}external plugins \"{plugin['name']}\": {e}")

View file

@ -195,7 +195,7 @@ try:
DEBUG=True,
INSTANCES=Instances(docker_client, kubernetes_client, INTEGRATION),
CONFIG=Config(db),
CONFIGFILES=ConfigFiles(app.logger, db),
CONFIGFILES=ConfigFiles(),
WTF_CSRF_SSL_STRICT=False,
USER=USER,
SEND_FILE_MAX_AGE_DEFAULT=86400,
@ -205,6 +205,7 @@ try:
DARK_MODE=False,
CURRENT_TOTP_TOKEN=None,
SCRIPT_NONCE=sha256(urandom(32)).hexdigest(),
DB=db,
)
except FileNotFoundError as e:
app.logger.error(repr(e), e.filename)
@ -255,14 +256,8 @@ def manage_bunkerweb(method: str, *args, operation: str = "reloads", is_draft: b
app.config["TO_FLASH"].append({"content": operation, "type": "success"})
if (was_draft != is_draft or not is_draft) and (moved or deleted):
changes = ["config", "custom_configs"]
error = app.config["CONFIGFILES"].save_configs(check_changes=False)
if error:
app.config["TO_FLASH"].append({"content": error, "type": "error"})
changes.pop()
# update changes in db
ret = db.checked_changes(changes, value=True)
ret = db.checked_changes(["config", "custom_configs"], value=True)
if ret:
app.logger.error(f"Couldn't set the changes to checked in the database: {ret}")
app.config["TO_FLASH"].append(
@ -1069,6 +1064,8 @@ def global_config():
@app.route("/configs", methods=["GET", "POST"])
@login_required
def configs():
db_configs = db.get_custom_configs()
if request.method == "POST":
operation = ""
@ -1080,74 +1077,92 @@ def configs():
"edit",
"delete",
):
return redirect_flash_error("Missing operation parameter on /configs.", "configs", True)
return redirect_flash_error("Operation parameter is invalid on /configs.", "configs", True)
# Check variables
variables = deepcopy(request.form.to_dict())
del variables["csrf_token"]
if variables["type"] != "file":
return redirect_flash_error("Invalid type parameter on /configs.", "configs", True)
operation = app.config["CONFIGFILES"].check_path(variables["path"])
if operation:
return redirect_flash_error(operation, "configs", True)
old_name = variables.get("old_name", "").replace(".conf", "")
name = variables.get("name", old_name).replace(".conf", "")
path_exploded = variables["path"].split(sep)
service_id = (path_exploded[5] if len(path_exploded) > 6 else None) or None
root_dir = path_exploded[4].replace("-", "_").lower()
if not old_name and not name:
return redirect_flash_error("Missing name parameter on /configs.", "configs", True)
index = -1
for i, db_config in enumerate(db_configs):
if db_config["type"] == root_dir and db_config["name"] == name and db_config["service_id"] == service_id:
if request.form["operation"] == "new":
return redirect_flash_error(f"Config {name} already exists{f' for service {service_id}' if service_id else ''}", "configs", True)
elif db_config["method"] not in ("ui", "manual"):
return redirect_flash_error(
f"Can't edit config {name}{f' for service {service_id}' if service_id else ''} because it was not created by the UI or manually",
"configs",
True,
)
index = i
break
# New or edit a config
if request.form["operation"] in ("new", "edit"):
if not app.config["CONFIGFILES"].check_name(variables["name"]):
if not app.config["CONFIGFILES"].check_name(name):
return redirect_flash_error(
f"Invalid {variables['type']} name. (Can only contain numbers, letters, underscores, dots and hyphens (min 4 characters and max 64))",
"configs",
True,
)
if variables["type"] == "file":
variables["name"] = f"{variables['name']}.conf"
content = BeautifulSoup(variables["content"], "html.parser").get_text()
if "old_name" in variables:
variables["old_name"] = f"{variables['old_name']}.conf"
if request.form["operation"] == "new":
db_configs.append({"type": root_dir, "name": name, "service_id": service_id, "data": content, "method": "ui"})
operation = f"Created config {name}{f' for service {service_id}' if service_id else ''}"
elif request.form["operation"] == "edit":
if index == -1:
return redirect_flash_error(
f"Can't edit config {name}{f' for service {service_id}' if service_id else ''} because it doesn't exist", "configs", True
)
variables["content"] = BeautifulSoup(variables["content"], "html.parser").get_text()
if old_name != name:
db_configs[index]["name"] = name
elif db_configs[index]["data"] == content:
return redirect_flash_error(
f"Config {name} was not edited because no values were changed{f' for service {service_id}' if service_id else ''}",
"configs",
True,
)
error = False
if request.form["operation"] == "new" and variables["type"] == "folder":
operation, error = app.config["CONFIGFILES"].create_folder(variables["path"], variables["name"])
if request.form["operation"] == "new" and variables["type"] == "file":
operation, error = app.config["CONFIGFILES"].create_file(variables["path"], variables["name"], variables["content"])
if request.form["operation"] == "edit" and variables["type"] == "file":
operation, error = app.config["CONFIGFILES"].edit_file(
variables["path"],
variables["name"],
variables.get("old_name", variables["name"]),
variables["content"],
)
if request.form["operation"] == "edit" and variables["type"] == "folder":
operation, error = app.config["CONFIGFILES"].edit_folder(
variables["path"],
variables["name"],
variables.get("old_name", variables["name"]),
)
if error:
return redirect_flash_error(operation, "configs", True)
db_configs[index]["data"] = content
operation = f"Edited config {name}{f' for service {service_id}' if service_id else ''}"
# Delete a config
if request.form["operation"] == "delete":
operation, error = app.config["CONFIGFILES"].delete_path(variables["path"])
elif request.form["operation"] == "delete":
if index == -1:
return redirect_flash_error(
f"Can't delete config {name}{f' for service {service_id}' if service_id else ''} because it doesn't exist", "configs", True
)
if error:
return redirect_flash_error(operation, "configs", True)
del db_configs[index]
operation = f"Deleted config {name}{f' for service {service_id}' if service_id else ''}"
error = db.save_custom_configs([config for config in db_configs if config["method"] == "ui"], "ui")
if error:
app.logger.error(f"Could not save custom configs: {error}")
return redirect_flash_error("Couldn't save custom configs", "configs", True)
flash(operation)
error = app.config["CONFIGFILES"].save_configs()
if error:
return redirect_flash_error("Couldn't save custom configs to disk", "configs", True)
return redirect(url_for("loading", next=url_for("configs")))
return render_template(
@ -1155,7 +1170,7 @@ def configs():
folders=[
path_to_dict(
join(sep, "etc", "bunkerweb", "configs"),
db_data=db.get_custom_configs(),
db_data=db_configs,
services=app.config["CONFIG"].get_config(methods=False).get("SERVER_NAME", "").split(" "),
)
],
@ -1182,7 +1197,7 @@ def plugins():
if variables["type"] in ("core", "pro"):
return redirect_flash_error(f"Can't delete {variables['type']} plugin {variables['name']}", "plugins", True)
plugins = app.config["CONFIG"].get_plugins(_type="external")
plugins = app.config["CONFIG"].get_plugins(_type="external", with_data=True)
for x, plugin in enumerate(plugins):
if plugin["id"] == variables["name"]:
del plugins[x]
@ -1384,12 +1399,12 @@ def plugins():
args=("plugins",),
).start()
# Remove tmp folder
if tmp_ui_path.exists():
rmtree(str(tmp_ui_path), ignore_errors=True)
return redirect(url_for("loading", next=url_for("plugins"), message="Reloading plugins"))
# Remove tmp folder
if tmp_ui_path.is_dir():
rmtree(tmp_ui_path, ignore_errors=True)
plugins = app.config["CONFIG"].get_plugins()
plugins_internal = 0
plugins_external = 0

View file

@ -1,84 +1,18 @@
#!/usr/bin/env python3
from glob import glob
from os import listdir, replace, sep, walk
from os.path import basename, dirname, join
from os import sep
from os.path import join
from pathlib import Path
from re import compile as re_compile
from shutil import rmtree, move as shutil_move
from typing import Any, Dict, List, Tuple
from utils import path_to_dict
def generate_custom_configs(
custom_configs: List[Dict[str, Any]],
*,
original_path: Path = Path(sep, "etc", "bunkerweb", "configs"),
):
original_path.mkdir(parents=True, exist_ok=True)
for custom_config in custom_configs:
tmp_path = original_path.joinpath(custom_config["type"].replace("_", "-"))
if custom_config["service_id"]:
tmp_path = tmp_path.joinpath(custom_config["service_id"])
tmp_path = tmp_path.joinpath(f"{custom_config['name']}.conf")
tmp_path.parent.mkdir(parents=True, exist_ok=True)
tmp_path.write_bytes(custom_config["data"])
class ConfigFiles:
def __init__(self, logger, db):
def __init__(self):
self.__name_regex = re_compile(r"^[\w.-]{4,64}$")
self.__root_dirs = [child["name"] for child in path_to_dict(join(sep, "etc", "bunkerweb", "configs"))["children"]]
self.__file_creation_blacklist = ["http", "stream"]
self.__logger = logger
self.__db = db
if not Path(sep, "usr", "sbin", "nginx").is_file():
custom_configs = self.__db.get_custom_configs()
if custom_configs:
self.__logger.info("Refreshing custom configs ...")
# Remove old custom configs files
for file in glob(join(sep, "etc", "bunkerweb", "configs", "*", "*")):
file = Path(file)
if file.is_symlink() or file.is_file():
file.unlink()
elif file.is_dir():
rmtree(str(file), ignore_errors=True)
generate_custom_configs(custom_configs)
self.__logger.info("Custom configs refreshed successfully")
def save_configs(self, *, check_changes: bool = True) -> str:
custom_configs = []
configs_path = join(sep, "etc", "bunkerweb", "configs")
root_dirs = listdir(configs_path)
for root, dirs, files in walk(configs_path):
if files or (dirs and basename(root) not in root_dirs):
path_exploded = root.split("/")
for file in files:
# root_dirs is index 4 on path exploded
# in case this is a service config, index 5 is the service id and index 6 is the config name
# else index 5 is the config name
service_id = path_exploded[5] if len(path_exploded) >= 6 else None
root_dir = path_exploded[4]
path_result = (service_id, root_dir, file.replace(".conf", ""))
with open(join(root, file), "r", encoding="utf-8") as f:
custom_configs.append(
{
"value": f.read(),
"exploded": path_result,
}
)
print("custom config", custom_configs, flush=True)
err = self.__db.save_custom_configs(custom_configs, "ui", changed=check_changes)
if err:
self.__logger.error(f"Could not save custom configs: {err}")
return "Couldn't save custom configs to database"
return ""
def check_name(self, name: str) -> bool:
return self.__name_regex.match(name) is not None
@ -104,88 +38,3 @@ class ConfigFiles:
return f"{join(root_path, root_dir, '/'.join(dirs.split('/')[0:-x]))} doesn't exist"
return ""
def delete_path(self, path: str) -> Tuple[str, int]:
try:
path: Path = Path(path)
if path.is_file():
path.unlink()
elif path.is_dir():
rmtree(path, ignore_errors=False)
else:
path = Path(f"{path}.conf")
if path.is_file():
path.unlink()
else:
rmtree(path, ignore_errors=False)
except OSError:
return f"Could not delete {path}", 1
return f"{path} was successfully deleted", 0
def create_folder(self, path: str, name: str) -> Tuple[str, int]:
folder_path = join(path, name) if not path.endswith(name) else path
try:
Path(folder_path).mkdir(parents=True)
except OSError:
return f"Could not create {folder_path}", 1
return f"The folder {folder_path} was successfully created", 0
def create_file(self, path: str, name: str, content: str) -> Tuple[str, int]:
file_path = Path(path, name)
file_path.parent.mkdir(exist_ok=True)
file_path.write_text(content, encoding="utf-8")
return f"The file {file_path} was successfully created", 0
def edit_folder(self, path: str, name: str, old_name: str) -> Tuple[str, int]:
new_folder_path = join(dirname(path), name)
old_folder_path = join(dirname(path), old_name)
if old_folder_path == new_folder_path:
return (
f"{old_folder_path} was not renamed because the name didn't change",
0,
)
try:
shutil_move(old_folder_path, new_folder_path)
except OSError:
return f"Could not move {old_folder_path}", 1
return (
f"The folder {old_folder_path} was successfully renamed to {new_folder_path}",
0,
)
def edit_file(self, path: str, name: str, old_name: str, content: str) -> Tuple[str, int]:
new_path = join(dirname(path), name)
old_path = join(dirname(path), old_name)
try:
file_content = Path(old_path).read_text(encoding="utf-8")
except FileNotFoundError:
return f"Could not find {old_path}", 1
if old_path == new_path and file_content == content:
return (
f"{old_path} was not edited because the content and the name didn't change",
0,
)
elif file_content == content:
try:
replace(path, new_path)
return f"{old_path} was successfully renamed to {new_path}", 0
except OSError:
return f"Could not rename {old_path} into {new_path}", 1
elif old_path == new_path:
new_path = old_path
else:
try:
Path(old_path).unlink()
except OSError:
return f"Could not remove {old_path}", 1
Path(new_path).write_text(content, encoding="utf-8")
return f"The file {old_path} was successfully edited", 0

File diff suppressed because one or more lines are too long

View file

@ -128,8 +128,26 @@ class SwitchTabForm {
}
}
class FormatExpire {
constructor() {
this.init();
}
init() {
window.addEventListener("DOMContentLoaded", () => {
const expireEl = document.querySelector("[data-expire]");
if (!expireEl) return;
expireEl.textContent = expireEl.textContent
.replaceAll("-", "/")
.split(" ")[0];
});
}
}
const setPWBtn = new PwBtn();
const setSubmit = new SubmitAccount();
const setTabs = new Tabs();
const setPopover = new Popover();
const setSwitchTabForm = new SwitchTabForm();
const setFormatExpire = new FormatExpire();

View file

@ -67,6 +67,9 @@ module.exports = {
"focus:bg-emerald-500/80",
"col-span-12",
"w-full",
"text-yellow-500",
"text-green-500",
"text-red-500",
],
presets: [],

View file

@ -57,7 +57,8 @@
</div>
{% set pro_info = {
"message" : "Pro version" if is_pro_version else "Pro version but exceeding services" if pro_status == "active" and pro_overlapped else "Pro version is expired" if pro_status == "expired" else "Pro version suspended" if pro_status == "suspended" else "You are using free version",
"link_message" : "All features available" if is_pro_version else "Awaiting compliance" if pro_status == "active" and pro_overlapped else "Renew license" if pro_status == "expired" else "Talk to team" if pro_status == "suspended" else "Upgrade to pro",
"link_message" : "All features available" if is_pro_version else "Awaiting compliance" if pro_status == "active" and pro_overlapped else "Renew license" if pro_status == "expired" else "Suspended license" if pro_status == "suspended" else "Upgrade to pro",
"link_message_color" : "text-green-500" if is_pro_version else "text-yellow-500" if pro_status == "active" and pro_overlapped else "text-yellow-500" if pro_status == "expired" else "text-red-500" if pro_status == "suspended" else "text-yellow-500",
"icon" : "pro" if is_pro_version else "free"
} %}
<div data-tab-item="pro"
@ -65,7 +66,7 @@
<div class="col-span-12">
<h5 class="text-xl my-1 text-center transition duration-300 ease-in-out font-bold m-0 mb-4 dark:text-gray-200">PRO</h5>
<div class="flex justify-center items-center">
<p class="mb-0 mr-2 dark:text-gray-300">{{ pro_info['message'] }}</p>
<p class="mb-0 mr-2 dark:text-gray-300 font-semibold">{{ pro_info['message'] }}</p>
<div role="img"
aria-label="pro"
class="dark:brightness-90 inline-block w-8 h-8 text-center rounded-circle bg-yellow-500">
@ -91,18 +92,18 @@
</div>
{% if pro_info['link_message'] %}
<div class="flex justify-center mt-2">
<a class="text-center font-semibold text-yellow-500 underline"
<a class="text-center font-semibold {{ pro_info['link_message_color'] }} underline"
target="_blank"
href="https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui#pro">{{ pro_info['link_message'] }}</a>
</div>
{% endif %}
{% if is_pro_version %}
<div class="mt-2 flex flex-col justify-center items-center">
{% if pro_expire %}
<p class="my-2 mr-2 dark:text-gray-300 text-center">Your license is valid until {{ pro_expire }}</p>
{% endif %}
{% if pro_services %}
<p class="my-2 mr-2 dark:text-gray-300 text-center">You can handle {{ pro_services }} services</p>
<p class="my-2 mr-2 dark:text-gray-300 text-center font-bold">{{ pro_services }} services allowed</p>
{% endif %}
{% if pro_expire %}
<p class="my-2 mr-2 dark:text-gray-300 text-center">License expired : <span data-expire class="font-bold">{{ pro_expire }}</span></p>
{% endif %}
</div>
{% endif %}

View file

@ -15,7 +15,7 @@
<!-- end float button-->
<!-- left sidebar -->
<aside data-sidebar-menu
class="transition-all mt-[4.5rem] fixed flex inset-y-0 flex-wrap justify-between w-full p-0 my-4 overflow-y-auto antialiased duration-200 -translate-x-full bg-white border-0 shadow-xl dark:shadow-none dark:bg-slate-850 dark:brightness-110 max-w-64 z-[1000] xl:ml-6 rounded-2xl xl:left-0 xl:translate-x-0"
class="transition-all mt-[4.5rem] fixed flex flex-col justify-between inset-y-0 max-h-screen w-full p-0 my-4 antialiased duration-200 -translate-x-full bg-white border-0 shadow-xl dark:shadow-none dark:bg-slate-850 dark:brightness-110 max-w-64 z-[1000] xl:ml-6 rounded-2xl xl:left-0 xl:translate-x-0"
aria-hidden="true"
id="sidebar-menu">
<!-- close btn-->
@ -31,32 +31,35 @@
</button>
<!-- close btn-->
<!-- top sidebar -->
<div class="w-full">
<div class="mt-6">
<!-- logo and version -->
<div class="h-19">
<div class="w-full">
<a aria-label="link to home"
class="flex justify-center px-8 py-6 m-0 text-sm whitespace-nowrap dark:text-white text-slate-700"
href="{% if current_endpoint == 'home' %}#{% else %}loading?next={{ url_for("home") }}{% endif %}">
class="flex justify-center px-8 m-0 text-sm whitespace-nowrap dark:text-white text-slate-700"
href="{% if current_endpoint == 'home' %}#{% else %}loading?next={{ url_for("home") }}{% endif %}">
<img src="images/logo-menu-2.png"
class="hidden dark:inline w-28 sm:w-36 transition-all duration-200 h-8 sm:h-10"
alt="main logo" />
class="hidden dark:inline w-28 sm:w-36 transition-all duration-200 h-8 sm:h-10"
alt="main logo" />
<img src="images/logo-menu.png"
class="dark:hidden inline w-28 sm:w-36 transition-all duration-200 h-8 sm:h-10"
alt="main logo" />
class="dark:hidden inline w-28 sm:w-36 transition-all duration-200 h-8 sm:h-10"
alt="main logo" />
</a>
</div>
<div class="w-full px-1">
<div class="mt-2 w-full px-1">
<h1 class="mb-0.5 tracking-normal text-primary text-center text-lg break-words whitespace-normal dark:text-gray-300">
{{ username }}
</h1>
<a class="block underline mb-2 text-gray-600 dark:text-gray-400 text-sm text-center hover:brightness-90"
href="{% if current_endpoint == 'account' %}#{% else %}loading?next={{ url_for("account") }}{% endif %}">manage account
href="{% if current_endpoint == 'account' %}#{% else %}loading?next={{ url_for("account") }}{% endif %}">manage account
</a>
</div>
<hr class="h-px mt-0 bg-transparent bg-gradient-to-r from-transparent via-black/40 to-transparent dark:bg-gradient-to-r dark:from-transparent dark:via-white dark:to-transparent" />
<!-- end logo version -->
</div>
<!-- list items -->
<div class="items-center block w-auto max-h-screen overflow-auto h-sidenav grow basis-full">
<div class="items-center block w-auto overflow-auto grow basis-full">
<!-- default anchor -->
<ul class="flex flex-col pl-0 mb-0">
{% set paths = [
@ -197,58 +200,50 @@
<!-- end loop paths-->
</ul>
<!-- end default anchor -->
<!-- plugin list -->
<div>
<ul>
<li class="w-full mt-4">
<h6 class="pl-6 ml-2 text-xs font-bold leading-tight uppercase dark:text-gray-400 dark:opacity-100 opacity-60">
PLUGINS PAGE
</h6>
</li>
{% for plugin in plugins %}
{% if plugin['page'] and plugin['type'] != "pro" %}
<li class="mt-0.5 w-full">
<a class="dark:hover:bg-primary/20 hover:bg-primary/5 hover:rounded-lg dark:text-gray-200 py-1 text-sm ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap px-4 transition"
href="{{ request.url_root }}plugins/{{ plugin['id'] }}">
<div class="mr-2 flex flex-wrap items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5">
<svg class="fill-gray-500 h-5 w-5 relative"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512">
<path d="M0 64C0 28.7 28.7 0 64 0H224V128c0 17.7 14.3 32 32 32H384V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64zm384 64H256V0L384 128z" />
</svg>
</div>
<span class="ml-1 duration-300 opacity-100 pointer-events-none ease">{{ plugin['name'] }}</span>
</a>
</li>
{% endif %}
{% if plugin['page'] and plugin['type'] == "pro" %}
<li class="mt-0.5 w-full">
<a {% if not is_pro_version %}target="_blank" rel="noopener"{% endif %} class="dark:hover:bg-primary/20 hover:bg-primary/5 hover:rounded-lg dark:text-gray-200 py-1 text-sm ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap px-4 transition" href="{% if not is_pro_version %}https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui#pro{% else %}javascript:void(0){% endif %}"
<div class="mr-2 flex flex-wrap items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5">
<svg class="h-5 w-5 dark:brightness-90 relative"
viewBox="0 0 48 46"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path class="fill-yellow-500" d="M43.218 28.2327L43.6765 23.971C43.921 21.6973 44.0825 20.1957 43.9557 19.2497L44 19.25C46.071 19.25 47.75 17.5711 47.75 15.5C47.75 13.4289 46.071 11.75 44 11.75C41.929 11.75 40.25 13.4289 40.25 15.5C40.25 16.4366 40.5935 17.2931 41.1613 17.9503C40.346 18.4535 39.2805 19.515 37.6763 21.1128C36.4405 22.3438 35.8225 22.9593 35.1333 23.0548C34.7513 23.1075 34.3622 23.0532 34.0095 22.898C33.373 22.6175 32.9485 21.8567 32.0997 20.335L27.6262 12.3135C27.1025 11.3747 26.6642 10.5889 26.2692 9.95662C27.89 9.12967 29 7.44445 29 5.5C29 2.73857 26.7615 0.5 24 0.5C21.2385 0.5 19 2.73857 19 5.5C19 7.44445 20.11 9.12967 21.7308 9.95662C21.3358 10.589 20.8975 11.3746 20.3738 12.3135L15.9002 20.335C15.0514 21.8567 14.627 22.6175 13.9905 22.898C13.6379 23.0532 13.2487 23.1075 12.8668 23.0548C12.1774 22.9593 11.5595 22.3438 10.3238 21.1128C8.71968 19.515 7.6539 18.4535 6.83882 17.9503C7.4066 17.2931 7.75 16.4366 7.75 15.5C7.75 13.4289 6.07107 11.75 4 11.75C1.92893 11.75 0.25 13.4289 0.25 15.5C0.25 17.5711 1.92893 19.25 4 19.25L4.04428 19.2497C3.91755 20.1957 4.07905 21.6973 4.32362 23.971L4.782 28.2327C5.03645 30.5982 5.24802 32.849 5.50717 34.875H42.4928C42.752 32.849 42.9635 30.5982 43.218 28.2327Z" fill="#1C274C" />
<path class="fill-yellow-500" d="M21.2803 45.5H26.7198C33.8098 45.5 37.3545 45.5 39.7198 43.383C40.7523 42.4588 41.4057 40.793 41.8775 38.625H6.1224C6.59413 40.793 7.24783 42.4588 8.2802 43.383C10.6454 45.5 14.1903 45.5 21.2803 45.5Z" fill="#1C274C" />
</svg>
</div>
<span class="ml-1 duration-300 {% if not is_pro_version %}opacity-80 dark:opacity-60{% endif %} pointer-events-none ease">{{ plugin['name'] }}</span>
</a>
</li>
{% endif %}
{% endfor %}
</ul>
<!-- end plugin list -->
</div>
<ul>
<li class="w-full mt-4">
<h6 class="pl-6 ml-2 text-xs font-bold leading-tight uppercase dark:text-gray-400 dark:opacity-100 opacity-60">
PLUGINS PAGE
</h6>
</li>
{% for plugin in plugins %}
{% if plugin['page'] %}
<li class="mt-0.5 w-full">
<a class="dark:hover:bg-primary/20 hover:bg-primary/5 hover:rounded-lg dark:text-gray-200 py-1 text-sm ease-nav-brand my-0 mx-2 flex items-center whitespace-nowrap px-4 transition"
href="{{ request.url_root }}plugins/{{ plugin['id'] }}">
<div class="mr-2 flex flex-wrap items-center justify-center rounded-lg bg-center stroke-0 text-center p-1 xl:p-1.5">
{% if plugin['type'] != "pro" %}
<svg class="fill-gray-500 h-5 w-5 relative"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 384 512">
<path d="M0 64C0 28.7 28.7 0 64 0H224V128c0 17.7 14.3 32 32 32H384V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64zm384 64H256V0L384 128z" />
</svg>
{% endif %}
{% if plugin['type'] == "pro" %}
<svg class="h-5 w-5 dark:brightness-90 relative"
viewBox="0 0 48 46"
fill="none"
xmlns="http://www.w3.org/2000/svg">
<path class="fill-yellow-500" d="M43.218 28.2327L43.6765 23.971C43.921 21.6973 44.0825 20.1957 43.9557 19.2497L44 19.25C46.071 19.25 47.75 17.5711 47.75 15.5C47.75 13.4289 46.071 11.75 44 11.75C41.929 11.75 40.25 13.4289 40.25 15.5C40.25 16.4366 40.5935 17.2931 41.1613 17.9503C40.346 18.4535 39.2805 19.515 37.6763 21.1128C36.4405 22.3438 35.8225 22.9593 35.1333 23.0548C34.7513 23.1075 34.3622 23.0532 34.0095 22.898C33.373 22.6175 32.9485 21.8567 32.0997 20.335L27.6262 12.3135C27.1025 11.3747 26.6642 10.5889 26.2692 9.95662C27.89 9.12967 29 7.44445 29 5.5C29 2.73857 26.7615 0.5 24 0.5C21.2385 0.5 19 2.73857 19 5.5C19 7.44445 20.11 9.12967 21.7308 9.95662C21.3358 10.589 20.8975 11.3746 20.3738 12.3135L15.9002 20.335C15.0514 21.8567 14.627 22.6175 13.9905 22.898C13.6379 23.0532 13.2487 23.1075 12.8668 23.0548C12.1774 22.9593 11.5595 22.3438 10.3238 21.1128C8.71968 19.515 7.6539 18.4535 6.83882 17.9503C7.4066 17.2931 7.75 16.4366 7.75 15.5C7.75 13.4289 6.07107 11.75 4 11.75C1.92893 11.75 0.25 13.4289 0.25 15.5C0.25 17.5711 1.92893 19.25 4 19.25L4.04428 19.2497C3.91755 20.1957 4.07905 21.6973 4.32362 23.971L4.782 28.2327C5.03645 30.5982 5.24802 32.849 5.50717 34.875H42.4928C42.752 32.849 42.9635 30.5982 43.218 28.2327Z" fill="#1C274C" />
<path class="fill-yellow-500" d="M21.2803 45.5H26.7198C33.8098 45.5 37.3545 45.5 39.7198 43.383C40.7523 42.4588 41.4057 40.793 41.8775 38.625H6.1224C6.59413 40.793 7.24783 42.4588 8.2802 43.383C10.6454 45.5 14.1903 45.5 21.2803 45.5Z" fill="#1C274C" />
</svg>
{% endif %}
</div>
<span class="ml-1 duration-300 opacity-100 pointer-events-none ease">{{ plugin['name'] }}</span>
</a>
</li>
{% endif %}
{% endfor %}
</ul>
<!-- end plugin list -->
</div>
<!-- end top sidebar -->
<!-- end list items -->
</div>
<!-- end top sidebar -->
<!-- bottom sidebar -->
<div class="w-full flex flex-col justify-end m-4">
<!-- bottom sidebar -->
<div class="flex flex-col justify-end mx-4 mt-2 mb-4">
<!-- dark/light mode -->
<div class="min-h-6 my-4 ml-12 flex justify-start">
<div class="min-h-6 ml-12 my-4 flex justify-start">
<input type="hidden"
id="csrf_token"
name="csrf_token"

View file

@ -7,7 +7,7 @@
{"name" : "TOTAL PLUGINS", "data" : plugins|length|string},
{"name" : "INTERNAL PLUGINS", "data" : plugins_count_internal|string},
{"name" : "EXTERNAL PLUGINS", "data" : plugins_count_external|string},
{"name" : "PRO PLUGINS", "data" : plugins_count_pro|string},
{"name" : "PRO PLUGINS", "data" : plugins_count_pro|string if is_pro_version else plugins_count_pro|string + ' (preview)'},
] %}
<div class="h-fit p-4 col-span-12 md:col-span-5 2xl:col-span-4 relative min-w-0 break-words bg-white shadow-xl dark:bg-slate-850 dark:shadow-dark-xl rounded-2xl bg-clip-border">
<h5 class="col-span-12 mb-4 font-bold dark:text-white/90">INFO</h5>
@ -154,7 +154,7 @@
<div data-plugins-list class="grid grid-cols-12 gap-3">
{% for plugin in plugins %}
<div data-plugins-type="{{ plugin['type'] }}"
class="py-3 min-h-12 relative col-span-12 sm:col-span-6 2xl:col-span-4 3xl:col-span-3 p-1 flex justify-between items-center transition rounded {% if plugin['type'] != 'pro' or plugin['type'] == 'pro' and is_pro_version %} bg-gray-100 hover:bg-gray-300 dark:bg-slate-700 dark:hover:bg-slate-800 {% else %} bg-gray-300 dark:bg-gray-800 {% endif %}">
class="py-3 min-h-12 relative col-span-12 sm:col-span-6 2xl:col-span-4 3xl:col-span-3 p-1 flex justify-between items-center transition rounded {% if plugin['type'] != 'pro' or plugin['type'] == 'pro' and is_pro_version %} bg-gray-100 hover:bg-gray-300 dark:bg-slate-700 dark:hover:bg-slate-800 {% else %} cursor-not-allowed bg-gray-300 dark:bg-gray-800 {% endif %}">
<p data-plugins-content
class="{% if plugin['type'] == 'pro' and not is_pro_version %} opacity-80 dark:opacity-60 {% endif %} ml-3 mr-2 break-words mb-0 transition duration-300 ease-in-out text-left text-sm md:text-base text-slate-700 dark:text-gray-200">
{{ plugin['name'] }}

View file

@ -12,7 +12,7 @@
</p>
{% endif %}
<div class="flex justify-start items-center">
<h5 class="transition duration-300 ease-in-out ml-2 font-bold text-md uppercase dark:text-white/90 mb-0">
<h5 class="transition duration-300 ease-in-out ml-2 mr-1 font-bold text-md uppercase dark:text-white/90 mb-0">
{{ plugin['name'] }} <span>{{ plugin['version'] }}</span>
</h5>
{% if plugin['page'] %}
@ -32,7 +32,7 @@
aria-label="pro plugin"
class="hover:-translate-y-px mx-1 -translate-y-0.5 ml-1"
href="{% if not is_pro_version %}https://panel.bunkerweb.io/?utm_campaign=self&utm_source=ui#pro{% else %}javascript:void(0){% endif %}">
<svg class="h-6 w-6 dark:brightness-90"
<svg class="h-5.5 w-5.5 dark:brightness-90"
viewBox="0 0 48 46"
fill="none"
xmlns="http://www.w3.org/2000/svg">
@ -42,7 +42,7 @@
</a>
{% endif %}
</div>
<div class="transition duration-300 ease-in-out dark:opacity-90 ml-2 ">
<div class="max-w-[650px] transition duration-300 ease-in-out dark:opacity-90 ml-2 ">
<p class="text-sm dark:text-gray-300 mb-1">{{ plugin['description'] }}</p>
</div>
</div>

View file

@ -14,7 +14,7 @@ exit_code = 0
try:
log_info("Navigating to the bans page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[9]/a", "bans")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[9]/a", "bans")
try:
safe_get_element(DRIVER, By.XPATH, "/html/body/main/div/div[2]/div/h5", error=True)

View file

@ -10,7 +10,7 @@ exit_code = 0
try:
log_info("Navigating to the cache page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[7]/a", "cache")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[7]/a", "cache")
log_info('Trying to open "jobs/asn.mmdb" cache file ...')

View file

@ -16,7 +16,7 @@ exit_code = 0
try:
log_info("Navigating to the services page to create a new service ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[4]/a", "services")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[4]/a", "services")
assert_button_click(DRIVER, "//button[@data-services-action='new']")
@ -35,7 +35,7 @@ try:
wait_for_service("app1.example.com")
log_info("Navigating to the configs page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[5]/a", "configs")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[5]/a", "configs")
log_info("Trying to create a new config ...")
@ -63,7 +63,7 @@ location /hello {
if TEST_TYPE == "linux":
wait_for_service()
assert_alert_message(DRIVER, "was successfully created")
assert_alert_message(DRIVER, "Created")
sleep(10)
@ -107,17 +107,17 @@ location /hello {
assert_button_click(DRIVER, "//button[@data-configs-setting-select-dropdown-btn='withconf' and @value='true']")
is_server_http_folder_hidden = DRIVER.execute_script(
f"""return document.querySelector("[data-configs-element='server-http']").classList.contains("hidden")"""
"""return document.querySelector("[data-configs-element='server-http']").classList.contains("hidden")"""
)
if is_server_http_folder_hidden:
log_error(f"Server http folder should be visible.")
log_error("Server http folder should be visible.")
exit(1)
is_http_folder_hidden = DRIVER.execute_script(f"""return document.querySelector("[data-configs-element='http']").classList.contains("hidden")""")
is_http_folder_hidden = DRIVER.execute_script("""return document.querySelector("[data-configs-element='http']").classList.contains("hidden")""")
if not is_http_folder_hidden:
log_error(f"Http folder should be hidden.")
log_error("Http folder should be hidden.")
exit(1)
# Reset
@ -132,11 +132,11 @@ location /hello {
assert_button_click(DRIVER, "//div[@data-configs-element='http' and @data-_type='folder']")
is_app1_example_com_folder_hidden = DRIVER.execute_script(
f"""return document.querySelector("[data-configs-element='app1.example.com']").classList.contains("hidden")"""
"""return document.querySelector("[data-configs-element='app1.example.com']").classList.contains("hidden")"""
)
if not is_app1_example_com_folder_hidden:
log_error(f"app1.example.com folder should be hidden.")
log_error("app1.example.com folder should be hidden.")
exit(1)
assert_button_click(DRIVER, "//button[@data-configs-setting-select='globalconf']")
@ -162,7 +162,7 @@ location /hello {
if TEST_TYPE == "linux":
wait_for_service()
assert_alert_message(DRIVER, "was successfully deleted")
assert_alert_message(DRIVER, "Deleted")
sleep(10)
@ -199,7 +199,7 @@ location /hello {
if TEST_TYPE == "linux":
wait_for_service()
assert_alert_message(DRIVER, "was successfully created")
assert_alert_message(DRIVER, "Created")
sleep(10)
@ -228,7 +228,7 @@ location /hello {
log_info("The config has been created only for the app1.example.com service, trying to delete the service to see if the config gets deleted ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[4]/a", "services")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[4]/a", "services")
assert_button_click(DRIVER, "//button[@data-services-action='delete' and @data-services-name='app1.example.com']")
@ -239,7 +239,7 @@ location /hello {
log_info("The service has been deleted, checking if the config has been deleted as well ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[5]/a", "configs")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[5]/a", "configs")
assert_button_click(DRIVER, "//div[@data-configs-element='server-http' and @data-_type='folder']")

View file

@ -12,7 +12,7 @@ exit_code = 0
try:
log_info("Navigating to the global config page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[3]/a", "global config")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[3]/a", "global config")
log_info("Trying filters ...")

View file

@ -11,7 +11,7 @@ exit_code = 0
try:
log_info("Navigating to the instances page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[2]/a", "instances")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[2]/a", "instances")
no_errors = True
retries = 0

View file

@ -14,7 +14,7 @@ exit_code = 0
try:
log_info("Navigating to the jobs page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[10]/a", "jobs")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[10]/a", "jobs")
log_info("Trying to filter jobs ...")

View file

@ -14,7 +14,7 @@ exit_code = 0
try:
log_info("Navigating to the logs page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[11]/a", "logs")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[11]/a", "logs")
log_info("Trying filters ...")

View file

@ -17,7 +17,7 @@ exit_code = 0
try:
log_info("Navigating to the plugins page to create a new service ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[6]/a", "plugins")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[6]/a", "plugins")
for _ in range(5):
get(f"http://www.example.com{UI_URL}/?id=/etc/passwd")

View file

@ -14,7 +14,7 @@ exit_code = 0
try:
log_info("Navigating to the reports page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[8]/a", "reports")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[8]/a", "reports")
with suppress(TimeoutException):
safe_get_element(DRIVER, By.XPATH, "/html/body/main/div/div/div/h5", error=True)

View file

@ -19,7 +19,7 @@ exit_code = 0
try:
log_info("Navigating to the services page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[4]/a", "services")
access_page(DRIVER, "/html/body/aside[1]/div[2]/ul[1]/li[4]/a", "services")
log_info("Check if default www.example.com service is here ...")