Compare commits

...

7 Commits

Author SHA1 Message Date
Pavel Loginov 35bd366f55
Merge pull request #425 from e345/patch-1
Update action.py : Redirect Docker command output to prevent Python hang
2025-09-23 09:44:28 +03:00
e345 d7c07b75b1
Merge pull request #1 from e345/e345-patch-1
Update action.py : Redirect Docker command output to prevent Python hang
2025-09-22 15:42:31 +08:00
e345 b25ce00e12
Update action.py : Redirect Docker command output to prevent Python hang
When executing Docker commands via Python, such as `docker kill -s HUP <container>`, the process may hang because the command produces output that blocks the Python subprocess.  

This change appends `> /dev/null` to Docker commands to discard any standard output, ensuring that Python can execute the command without waiting indefinitely.  

Systemd commands remain unchanged, as they do not exhibit the same blocking behavior.
2025-09-22 15:41:50 +08:00
Pavel Loginov 90050a9bd0
Merge pull request #424 from e345/patch-1
Update action.py: correct docker kill signal option case
2025-09-22 09:07:08 +03:00
e345 6b7cfd1e86
Update action.py: correct docker kill signal option case
Previously, when handling the reload action, the code used `docker kill -S HUP`.
However, the Docker CLI only supports the lowercase `-s` option.
Using `-S` results in an error and prevents the signal from being sent to the container process.

This change:
- Corrects `kill -S HUP` to `kill -s HUP`
- Ensures that in Docker mode, the reload action can properly send the SIGHUP signal
- Keeps the systemd branch logic unchanged
2025-09-22 13:53:50 +08:00
Aidaho 3042fd7f24 v8.2.3: Add hotkeys info link in config template and refactor time_range handling in metrics module
- Added an informational link about hotkeys to the config.html template for improved usability.
- Refactored time_range logic in metric queries to use integers instead of strings for better consistency and readability.
2025-09-06 09:50:36 +03:00
Aidaho dccc2b3e20 v8.2.3: Add backend header hiding option and improve UDP listener updates
- Introduced `hide_backend_headers` option in Nginx configuration for enhanced security.
- Updated models, templates, and JavaScript to support `hide_backend_headers`.
- Improved UDP listener rendering logic by dynamically fetching and syncing listeners via AJAX.
- Added new database migration to update version to `8.2.3`.
2025-08-26 09:24:20 +03:00
18 changed files with 122 additions and 44 deletions

View File

@ -52,28 +52,26 @@ def select_metrics(serv, service, **kwargs):
else:
model = Metrics
# Get time range from kwargs
time_range = kwargs.get('time_range', '30') # Default to 30 minutes if not specified
time_range = kwargs.get('time_range', '30')
# Create a base query
query = model.select().where(model.serv == serv)
# Add time-based filtering
now = datetime.utcnow()
if time_range == '1':
if time_range == 1:
# Last 1 minute
query = query.where(model.date >= now - timedelta(minutes=1))
elif time_range == '60':
elif time_range == 60:
# Last 60 minutes
query = query.where(model.date >= now - timedelta(minutes=60))
elif time_range == '180':
elif time_range == 180:
# Last 180 minutes
query = query.where(model.date >= now - timedelta(minutes=180))
elif time_range == '360':
elif time_range == 360:
# Last 360 minutes
query = query.where(model.date >= now - timedelta(minutes=360))
elif time_range == '720':
elif time_range == 720:
# Last 720 minutes
query = query.where(model.date >= now - timedelta(minutes=720))
else:
@ -84,14 +82,14 @@ def select_metrics(serv, service, **kwargs):
query = query.order_by(model.date.asc())
# For longer time ranges, we can sample the data to reduce the number of points
# This is similar to the original SQL's "group by `date` div X" or "rowid % X = 0"
if mysql_enable == '1' and time_range not in ('1', '30'):
if mysql_enable == '1' and time_range not in (1, 30):
# For MySQL, we can use the SQL function to sample data
from peewee import fn
sampling_rates = {
'60': 100,
'180': 200,
'360': 300,
'720': 500
60: 100,
180: 200,
360: 300,
720: 500
}
if time_range in sampling_rates:
# Group by date div X to reduce data points
@ -99,15 +97,15 @@ def select_metrics(serv, service, **kwargs):
(model.serv == serv) &
(model.date >= now - timedelta(minutes=int(time_range)))
).group_by(fn.DIV(model.date, sampling_rates[time_range])).order_by(model.date.asc())
elif not mysql_enable == '1' and time_range not in ('1', '30'):
elif not mysql_enable == '1' and time_range not in (1, 30):
# For SQLite, we need to fetch all data and then sample it in Python
# This is less efficient but maintains compatibility
results = list(query.dicts())
sampling_rates = {
'60': 2,
'180': 5,
'360': 7,
'720': 9
60: 2,
180: 5,
360: 7,
720: 9
}
if time_range in sampling_rates:
# Sample data by taking every Nth row

View File

@ -0,0 +1,22 @@
from playhouse.migrate import *
from app.modules.db.db_model import connect, Version
migrator = connect(get_migrator=1)
def up():
"""Apply the migration."""
try:
Version.update(version='8.2.3').execute()
except Exception as e:
print(f"Error updating version: {str(e)}")
raise e
def down():
"""Roll back the migration."""
try:
Version.update(version='8.2.2').execute()
except Exception as e:
print(f"Error rolling back migration: {str(e)}")
raise e

View File

@ -56,8 +56,8 @@ def get_action_command(service: str, action: str, server_id: int) -> str:
if is_docker == '1':
container_name = sql.get_setting(f'{service}_container_name')
if action == 'reload':
action = 'kill -S HUP'
commands = f"sudo docker {action} {container_name}"
action = 'kill -s HUP'
commands = f"sudo docker {action} {container_name} > /dev/null"
else:
service_name = service_common.get_correct_service_name(service, server_id)
commands = f"sudo systemctl {action} {service_name}"

View File

@ -135,7 +135,7 @@ location {{ location.location }} {
gzip_proxied any;
{% endif %}
{% if nginx_proxy.security.hide_backend_headers %}
{% if config.security.hide_backend_headers %}
# Hide backend headers
proxy_hide_header X-Powered-By;
proxy_hide_header Server;

View File

@ -9,7 +9,6 @@ nginx_proxy:
# Security configurations
security:
hide_backend_headers: true # Hide backend server headers (e.g., X-Powered-By)
hsts_max_age: 15768000 # HSTS duration in seconds (6 months)
content_security_policy: "default-src 'self'" # Content Security Policy rules

View File

@ -237,7 +237,14 @@ function getNginxFormData($form, form_name) {
let header = {action, name, value};
headers.push(header);
});
let location_config = {location, proxy_connect_timeout, proxy_read_timeout, proxy_send_timeout, headers, upstream};
let location_config = {
location,
proxy_connect_timeout,
proxy_read_timeout,
proxy_send_timeout,
headers,
upstream
};
indexed_array['locations'].push(location_config)
} else if (n['name'] === 'ssl_offloading') {
if ($('input[name="ssl_offloading"]').is(':checked')) {
@ -261,7 +268,7 @@ function getNginxFormData($form, form_name) {
indexed_array[n['name']] = n['value'];
}
});
$('#name_alias_div p').each(function (){
$('#name_alias_div p').each(function () {
let name = $(this).children("input[name='name_alias']").val();
if (name === undefined || name === '') {
return;
@ -270,14 +277,22 @@ function getNginxFormData($form, form_name) {
});
let hide_server_tokens = false;
let security_headers = false;
let hide_backend_headers = false;
if ($('#hide_server_tokens').is(':checked')) {
hide_server_tokens = true;
}
if ($('#security_headers').is(':checked')) {
security_headers = true;
}
indexed_array['security'] = {'hide_server_tokens': hide_server_tokens, 'security_headers': security_headers};
$('#'+form_name+' span[name="add_servers"] p').each(function (){
if ($('#hide_backend_headers').is(':checked')) {
hide_backend_headers = true;
}
indexed_array['security'] = {
'hide_server_tokens': hide_server_tokens,
'security_headers': security_headers,
'hide_backend_headers': hide_backend_headers
};
$('#' + form_name + ' span[name="add_servers"] p').each(function () {
let server = $(this).children("input[name='servers']").val();
if (server === undefined || server === '') {
return;
@ -290,7 +305,8 @@ function getNginxFormData($form, form_name) {
});
let elementsForDelete = [
'servers', 'server_port', 'max_fails', 'fail_timeout', 'proxy_connect_timeout', 'proxy_read_timeout', 'proxy_send_timeout',
'headers_res', 'header_name', 'header_value', 'upstream', 'server', 'name_alias', 'hide_server_tokens', 'security_headers'
'headers_res', 'header_name', 'header_value', 'upstream', 'server', 'name_alias', 'hide_server_tokens', 'security_headers',
'hide_backend_headers'
]
for (let element of elementsForDelete) {
delete indexed_array[element]

View File

@ -826,6 +826,11 @@ function openNginxSection(section) {
} else {
$('#security_headers').prop("checked", false);
}
if (data.security.hide_backend_headers) {
$('#hide_backend_headers').prop("checked", true);
} else {
$('#hide_backend_headers').prop("checked", false);
}
}
if (data.hsts) {
$('#hsts').prop("checked", true);

View File

@ -615,3 +615,33 @@ function checkUdpBackendStatus(listener_id, backend_ip) {
}
});
}
function getUdpListeners() {
$.ajax({
url: api_prefix + "/udp/listeners",
success: function (data) {
let clusters = document.querySelectorAll('.up-pannel > [id^="listener-"]');
let clusterNumbers = Array.from(clusters).map(div =>
parseInt(div.id.replace('listener-', ''), 10)
);
if (data.status === 'failed') {
toastr.error(data.error);
return;
}
if (data.length > 0) {
const dataIds = data.map(item => item.id);
const idsToDelete = clusterNumbers.filter(id => !dataIds.includes(id));
for (const [key, value] of Object.entries(data)) {
if (!clusterNumbers.includes(value.id)) {
getUDPListener(value.id, true);
} else {
getUDPListener(value.id, false);
}
}
for (let id of idsToDelete) {
$('#listener-' + id).remove();
}
}
}
});
}

View File

@ -88,6 +88,7 @@
<button type="submit" value="reload" name="save" class="btn btn-default">{{lang.phrases.save_and_reload}}</button>
{% if service != 'keepalived' %}
<div class="alert alert-info" style="margin-left: -0px;"><b>{{lang.words.note|title()}}:</b> {{lang.phrases.master_slave}}</div>
<div class="alert alert-info" style="margin-left: -0px;">Read more about <a href="https://roxy-wi.org/description/hotkeys" target="_blank">Hotkeys</a></div>
{% endif %}
</p>
</form>

View File

@ -120,6 +120,7 @@ Timeouts: Timeouts (proxy_connect_timeout).
{% set enable_sec_headers = lang.words.enable|title() + ' ' + lang.words.security + ' ' + lang.words.headers %}
<p>{{ checkbox('hide_server_tokens', title=lang.add_page.desc.hide_server_tokens, desc=lang.phrases.hide_server_tokens, checked='checked') }}</p>
<p>{{ checkbox('security_headers', title=lang.add_page.desc.security_headers, desc=lang.phrases.security_headers, checked='checked') }}</p>
<p>{{ checkbox('hide_backend_headers', title=lang.add_page.desc.hide_backend_headers, desc=lang.phrases.hide_backend_headers, checked='checked') }}</p>
</div>
</td>
</tr>

View File

@ -345,6 +345,7 @@
"check_interval_title": "Check interval. In seconds.",
"hide_server_tokens": "Hide server tokens",
"security_headers": "Enable security headers",
"hide_backend_headers": "Hide backend server headers",
}
%}
{% set roles = {
@ -358,6 +359,7 @@
"desc": {
"security_headers": "Enable security headers (X-Content-Type-Options, etc.)",
"hide_server_tokens": "Hide Nginx version in headers",
"hide_backend_headers": "Hide backend server headers (e.g., X-Powered-By)",
"port_check": "A basic TCP-layer health check tries to connect to the server's TCP port. The check is valid when the server answers with a SYN/ACK packet.",
"maxconn": "The total number of connections allowed, process-wide. This stops the process from accepting too many connections at once, which safeguards it from running out of memory.",
"server_template": "Create the list of servers from the template",

View File

@ -332,6 +332,7 @@
"check_interval_title": "Intervalo de comprobación. En segundos.",
"hide_server_tokens": "Ocultar tokens del servidor",
"security_headers": "Habilitar encabezados de seguridad",
"hide_backend_headers": "Ocultar los encabezados del servidor backend",
}
%}
{% set roles = {
@ -345,6 +346,7 @@
"desc": {
"security_headers": "Habilitar encabezados de seguridad (X-Content-Type-Options, etc.)",
"hide_server_tokens": "Ocultar la versión de Nginx en los encabezados",
"hide_backend_headers": "Ocultar los encabezados del servidor backend (por ejemplo, X-Powered-By)",
"port_check": "Una verificación básica a nivel TCP intenta conectarse al puerto TCP del servidor. La verificación es válida cuando el servidor responde con un paquete SYN/ACK.",
"maxconn": "Número total de conexiones permitidas a nivel de proceso. Esto evita aceptar demasiadas conexiones a la vez, protegiendo de quedarse sin memoria.",
"server_template": "Crear la lista de servidores desde la plantilla",

View File

@ -332,6 +332,7 @@
"check_interval_title": "Vérifiez l'intervalle. En secondes.",
"hide_server_tokens": "Masquer les jetons du serveur",
"security_headers": "Activer les en-têtes de sécurité",
"hide_backend_headers": "Masquer les en-têtes du serveur principal",
}
%}
{% set roles = {
@ -345,6 +346,7 @@
"desc": {
"security_headers": "Activer les en-têtes de sécurité (X-Content-Type-Options, etc.)",
"hide_server_tokens": "Masquer la version Nginx dans les en-têtes",
"hide_backend_headers": "Masquer les en-têtes du serveur principal (par exemple, X-Powered-By)",
"port_check": "Un contrôle de base de la couche TCP tente de se connecter au port TCP du serveur. Le contrôle est valide lorsque le serveur répond par un paquet SYN/ACK.",
"maxconn": "Le nombre total de connexions autorisées, à l\'échelle du processus. Cela empêche le processus d\'accepter trop de connexions à la fois, ce qui lui évite de manquer de mémoire.",
"server_template": "Créez la liste des serveurs à partir du modèle",

View File

@ -332,6 +332,7 @@
"check_interval_title": "Verifique o intervalo. Em segundos.",
"hide_server_tokens": "Ocultar tokens do servidor",
"security_headers": "Habilitar cabeçalhos de segurança",
"hide_backend_headers": "Ocultar cabeçalhos do servidor de backend",
}
%}
{% set roles = {
@ -345,6 +346,7 @@
"desc": {
"security_headers": "Habilitar cabeçalhos de segurança (X-Content-Type-Options, etc.)",
"hide_server_tokens": "Ocultar versão do Nginx nos cabeçalhos",
"hide_backend_headers": "Ocultar cabeçalhos do servidor de backend (por exemplo, X-Powered-By)",
"port_check": "a verificação de integridade básica do nivel TCP tenta se conectar à porta TCP do servidor. A verificação é válida quando o servidor responde com um pacote SYN/ACK",
"maxconn": "número total de conexões permitidas. Isso impede que o processo aceite muitas conexões de uma só vez, o que o protege de ficar sem memória.",
"server_template": "Criar uma lista de servidores de um modelo",

View File

@ -345,6 +345,7 @@
"check_interval_title": "Интервал проверки. В секундах.",
"hide_server_tokens": "Скрыть токены сервера",
"security_headers": "Включить заголовки безопасности",
"hide_backend_headers": "Скрыть заголовки внутреннего сервера",
}
%}
{% set roles = {
@ -358,6 +359,7 @@
"desc": {
"security_headers": "Включить заголовки безопасности (X-Content-Type-Options и т. д.)",
"hide_server_tokens": "Скрыть версию Nginx в заголовках",
"hide_backend_headers": "Скрыть заголовки внутреннего сервера (например, X-Powered-By)",
"port_check": "Базовая проверка работоспособности на уровне TCP пытается подключиться к TCP-порту сервера. Проверка действительна, когда сервер отвечает пакетом SYN/ACK.",
"maxconn": "Общее количество разрешенных подключений для всего процесса. Это не позволяет процессу одновременно принимать слишком много подключений, что защищает его от нехватки памяти.",
"server_template": "Создать список серверов из шаблона",

View File

@ -332,6 +332,7 @@
"check_interval_title": "检查间隔(以秒为单位)。",
"hide_server_tokens": "隐藏服务器令牌",
"security_headers": "启用安全标头",
"hide_backend_headers": "隐藏后端服务器标头",
}
%}
{% set roles = {
@ -345,6 +346,7 @@
"desc": {
"security_headers": "启用安全标头X-Content-Type-Options 等)",
"hide_server_tokens": "在标头中隐藏 Nginx 版本",
"hide_backend_headers": "隐藏后端服务器标头(例如 X-Powered-By",
"port_check": "基本的 TCP 层健康检查尝试连接到服务器的 TCP 端口。当服务器返回 SYN/ACK 数据包时,检查被认为是成功的。",
"maxconn": "进程允许的总连接数。这可以防止进程接受过多连接,从而防止内存耗尽。",
"server_template": "根据模板创建服务器列表。",

View File

@ -22,11 +22,7 @@
{% if g.user_params['role'] <= 3 %}
<div class="add-button add-button-big" title="{{lang.words.create|title()}} UDP {{ lang.words.listener }}" onclick="createUDPListener();">+ {{lang.words.create|title()}} UDP {{ lang.words.listener }}</div>
{% endif %}
<div class="up-pannel" class="sortable">
{% for listener in listeners %}
<div id="listener-{{listener.id}}" class="div-server-hapwi animated-background"></div>
{% endfor %}
</div>
<div class="up-pannel" class="sortable"></div>
<div id="create-udp-step-1" style="display: none;">
<table class="overview" id="create-udp-step-1-overview"
title="{{lang.words.create|title()}} UDP {{ lang.words.listener }}"
@ -179,9 +175,8 @@
<p><span class="ui-icon ui-icon-alert" style="float:left; margin:3px 12px 20px 0;"></span>{{lang.phrases.are_you_sure}}</p>
</div>
<script>
{% for listener in listeners %}
getUDPListener('{{listener.id}}');
{% endfor %}
getUdpListeners();
setInterval(getUdpListeners, 60000);
</script>
{% endif %}
{% endblock %}

View File

@ -105,7 +105,6 @@ class UDPListener(MethodView):
else:
if not listener_id:
kwargs = {
'listeners': udp_sql.select_listeners(g.user_params['group_id']),
'lang': g.user_params['lang'],
'clusters': ha_sql.select_clusters(g.user_params['group_id']),
'is_needed_tool': common.is_tool('ansible'),