nginx-lens 0.1.3__tar.gz → 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/PKG-INFO +1 -1
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/analyzer/duplicates.py +6 -4
- nginx_lens-0.1.5/commands/analyze.py +130 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/commands/cli.py +4 -4
- nginx_lens-0.1.5/commands/graph.py +75 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/commands/logs.py +1 -2
- nginx_lens-0.1.5/commands/route.py +49 -0
- nginx_lens-0.1.5/commands/syntax.py +73 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/nginx_lens.egg-info/PKG-INFO +1 -1
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/setup.py +1 -1
- nginx_lens-0.1.3/commands/analyze.py +0 -74
- nginx_lens-0.1.3/commands/graph.py +0 -26
- nginx_lens-0.1.3/commands/route.py +0 -33
- nginx_lens-0.1.3/commands/syntax.py +0 -63
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/analyzer/__init__.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/analyzer/base.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/analyzer/conflicts.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/analyzer/dead_locations.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/analyzer/diff.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/analyzer/empty_blocks.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/analyzer/include.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/analyzer/rewrite.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/analyzer/route.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/analyzer/unused.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/analyzer/warnings.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/commands/__init__.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/commands/diff.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/commands/health.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/commands/include.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/commands/tree.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/exporter/__init__.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/exporter/graph.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/exporter/html.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/exporter/markdown.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/nginx_lens.egg-info/SOURCES.txt +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/nginx_lens.egg-info/dependency_links.txt +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/nginx_lens.egg-info/entry_points.txt +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/nginx_lens.egg-info/requires.txt +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/nginx_lens.egg-info/top_level.txt +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/parser/__init__.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/parser/nginx_parser.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/pyproject.toml +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/setup.cfg +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/tests/test_conflicts.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/tests/test_duplicates.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/tests/test_empty_blocks.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/tests/test_health.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/tests/test_parser.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/upstream_checker/__init__.py +0 -0
- {nginx_lens-0.1.3 → nginx_lens-0.1.5}/upstream_checker/checker.py +0 -0
|
@@ -3,15 +3,16 @@ from typing import List, Dict, Any
|
|
|
3
3
|
|
|
4
4
|
def find_duplicate_directives(tree) -> List[Dict[str, Any]]:
|
|
5
5
|
"""
|
|
6
|
-
Находит дублирующиеся директивы внутри одного
|
|
7
|
-
Возвращает список: [{block, directive, count}]
|
|
6
|
+
Находит дублирующиеся директивы внутри одного блока (без вложенности).
|
|
7
|
+
Возвращает список: [{block, directive, count, location}]
|
|
8
8
|
"""
|
|
9
9
|
analyzer = Analyzer(tree)
|
|
10
10
|
duplicates = []
|
|
11
11
|
for d, parent in analyzer.walk():
|
|
12
12
|
if 'directives' in d:
|
|
13
|
+
# Считаем только прямые дочерние директивы (без вложенных блоков)
|
|
13
14
|
seen = {}
|
|
14
|
-
for sub
|
|
15
|
+
for sub in d['directives']:
|
|
15
16
|
if 'directive' in sub:
|
|
16
17
|
key = (sub['directive'], str(sub.get('args')))
|
|
17
18
|
seen[key] = seen.get(key, 0) + 1
|
|
@@ -21,6 +22,7 @@ def find_duplicate_directives(tree) -> List[Dict[str, Any]]:
|
|
|
21
22
|
'block': d,
|
|
22
23
|
'directive': directive,
|
|
23
24
|
'args': args,
|
|
24
|
-
'count': count
|
|
25
|
+
'count': count,
|
|
26
|
+
'location': d.get('arg')
|
|
25
27
|
})
|
|
26
28
|
return duplicates
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from analyzer.conflicts import find_location_conflicts, find_listen_servername_conflicts
|
|
5
|
+
from analyzer.duplicates import find_duplicate_directives
|
|
6
|
+
from analyzer.empty_blocks import find_empty_blocks
|
|
7
|
+
from analyzer.warnings import find_warnings
|
|
8
|
+
from analyzer.unused import find_unused_variables
|
|
9
|
+
from parser.nginx_parser import parse_nginx_config
|
|
10
|
+
from analyzer.rewrite import find_rewrite_issues
|
|
11
|
+
from analyzer.dead_locations import find_dead_locations
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
# Карта советов и критичности для issue_type
|
|
17
|
+
ISSUE_META = {
|
|
18
|
+
'location_conflict': ("Возможное пересечение location. Это не всегда ошибка: порядок и типы location могут быть корректны. Проверьте, что порядок и типы location соответствуют вашим ожиданиям. Если всё ок — игнорируйте предупреждение.", "medium"),
|
|
19
|
+
'duplicate_directive': ("Оставьте только одну директиву с нужным значением в этом блоке.", "medium"),
|
|
20
|
+
'empty_block': ("Удалите или заполните пустой блок.", "low"),
|
|
21
|
+
'proxy_pass_no_scheme': ("Добавьте http:// или https:// в proxy_pass.", "medium"),
|
|
22
|
+
'autoindex_on': ("Отключите autoindex, если не требуется публикация файлов.", "medium"),
|
|
23
|
+
'if_block': ("Избегайте if внутри location, используйте map/try_files.", "medium"),
|
|
24
|
+
'server_tokens_on': ("Отключите server_tokens для безопасности.", "low"),
|
|
25
|
+
'ssl_missing': ("Укажите путь к SSL-сертификату/ключу.", "high"),
|
|
26
|
+
'ssl_protocols_weak': ("Отключите устаревшие протоколы TLS.", "high"),
|
|
27
|
+
'ssl_ciphers_weak': ("Используйте современные шифры.", "high"),
|
|
28
|
+
'listen_443_no_ssl': ("Добавьте ssl к listen 443.", "high"),
|
|
29
|
+
'listen_443_no_http2': ("Добавьте http2 к listen 443 для производительности.", "low"),
|
|
30
|
+
'no_limit_req_conn': ("Добавьте limit_req/limit_conn для защиты от DDoS.", "medium"),
|
|
31
|
+
'missing_security_header': ("Добавьте security-заголовок.", "medium"),
|
|
32
|
+
'deprecated': ("Замените устаревшую директиву.", "medium"),
|
|
33
|
+
'limit_too_small': ("Увеличьте лимит до рекомендуемого значения.", "medium"),
|
|
34
|
+
'limit_too_large': ("Уменьшите лимит до разумного значения.", "medium"),
|
|
35
|
+
'unused_variable': ("Удалите неиспользуемую переменную.", "low"),
|
|
36
|
+
'listen_servername_conflict': ("Измените listen/server_name для устранения конфликта.", "high"),
|
|
37
|
+
'rewrite_cycle': ("Проверьте rewrite на циклические правила.", "high"),
|
|
38
|
+
'rewrite_conflict': ("Проверьте порядок и уникальность rewrite.", "medium"),
|
|
39
|
+
'rewrite_no_flag': ("Добавьте last/break/redirect/permanent к rewrite.", "low"),
|
|
40
|
+
'dead_location': ("Удалите неиспользуемый location или используйте его.", "low"),
|
|
41
|
+
}
|
|
42
|
+
SEVERITY_COLOR = {"high": "red", "medium": "orange3", "low": "yellow"}
|
|
43
|
+
|
|
44
|
+
def analyze(config_path: str = typer.Argument(..., help="Путь к nginx.conf")):
|
|
45
|
+
"""
|
|
46
|
+
Анализирует конфигурацию Nginx на типовые проблемы и best practices.
|
|
47
|
+
|
|
48
|
+
Показывает:
|
|
49
|
+
- Конфликты location-ов
|
|
50
|
+
- Дублирующиеся директивы
|
|
51
|
+
- Пустые блоки
|
|
52
|
+
- Потенциальные проблемы (proxy_pass без схемы, autoindex on, if, server_tokens on, SSL, лимиты, deprecated и др.)
|
|
53
|
+
- Неиспользуемые переменные
|
|
54
|
+
- Конфликты listen/server_name
|
|
55
|
+
- Проблемы с rewrite
|
|
56
|
+
- Мертвые location-ы
|
|
57
|
+
|
|
58
|
+
Пример:
|
|
59
|
+
nginx-lens analyze /etc/nginx/nginx.conf
|
|
60
|
+
"""
|
|
61
|
+
tree = parse_nginx_config(config_path)
|
|
62
|
+
conflicts = find_location_conflicts(tree)
|
|
63
|
+
dups = find_duplicate_directives(tree)
|
|
64
|
+
empties = find_empty_blocks(tree)
|
|
65
|
+
warnings = find_warnings(tree)
|
|
66
|
+
unused_vars = find_unused_variables(tree)
|
|
67
|
+
listen_conflicts = find_listen_servername_conflicts(tree)
|
|
68
|
+
rewrite_issues = find_rewrite_issues(tree)
|
|
69
|
+
dead_locations = find_dead_locations(tree)
|
|
70
|
+
|
|
71
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
72
|
+
table.add_column("issue_type")
|
|
73
|
+
table.add_column("issue_description")
|
|
74
|
+
table.add_column("solution")
|
|
75
|
+
|
|
76
|
+
def add_row(issue_type, desc):
|
|
77
|
+
solution, severity = ISSUE_META.get(issue_type, ("", "low"))
|
|
78
|
+
color = SEVERITY_COLOR.get(severity, "yellow")
|
|
79
|
+
table.add_row(f"[{color}]{issue_type}[/{color}]", desc, f"[{color}]{solution}[/{color}]")
|
|
80
|
+
|
|
81
|
+
for c in conflicts:
|
|
82
|
+
add_row("location_conflict", f"server: {c['server'].get('arg', '')} location: {c['location1']} ↔ {c['location2']}")
|
|
83
|
+
for d in dups:
|
|
84
|
+
loc = d.get('location')
|
|
85
|
+
add_row("duplicate_directive", f"{d['directive']} ({d['args']}) — {d['count']} раз в блоке {d['block'].get('block', d['block'])}{' location: '+str(loc) if loc else ''}")
|
|
86
|
+
for e in empties:
|
|
87
|
+
add_row("empty_block", f"{e['block']} {e['arg'] or ''}")
|
|
88
|
+
for w in warnings:
|
|
89
|
+
t = w['type']
|
|
90
|
+
if t == 'proxy_pass_no_scheme':
|
|
91
|
+
add_row(t, f"proxy_pass без схемы: {w['value']}")
|
|
92
|
+
elif t == 'autoindex_on':
|
|
93
|
+
add_row(t, f"autoindex on в блоке {w['context'].get('block','')}")
|
|
94
|
+
elif t == 'if_block':
|
|
95
|
+
add_row(t, f"директива if внутри блока {w['context'].get('block','')}")
|
|
96
|
+
elif t == 'server_tokens_on':
|
|
97
|
+
add_row(t, f"server_tokens on в блоке {w['context'].get('block','')}")
|
|
98
|
+
elif t == 'ssl_missing':
|
|
99
|
+
add_row(t, f"{w['directive']} не указан")
|
|
100
|
+
elif t == 'ssl_protocols_weak':
|
|
101
|
+
add_row(t, f"ssl_protocols содержит устаревшие протоколы: {w['value']}")
|
|
102
|
+
elif t == 'ssl_ciphers_weak':
|
|
103
|
+
add_row(t, f"ssl_ciphers содержит слабые шифры: {w['value']}")
|
|
104
|
+
elif t == 'listen_443_no_ssl':
|
|
105
|
+
add_row(t, f"listen без ssl: {w['value']}")
|
|
106
|
+
elif t == 'listen_443_no_http2':
|
|
107
|
+
add_row(t, f"listen 443 без http2: {w['value']}")
|
|
108
|
+
elif t == 'no_limit_req_conn':
|
|
109
|
+
add_row(t, f"server без limit_req/limit_conn")
|
|
110
|
+
elif t == 'missing_security_header':
|
|
111
|
+
add_row(t, f"отсутствует security header: {w['value']}")
|
|
112
|
+
elif t == 'deprecated':
|
|
113
|
+
add_row(t, f"устаревшая директива: {w['directive']} — {w['value']}")
|
|
114
|
+
elif t == 'limit_too_small':
|
|
115
|
+
add_row(t, f"слишком маленькое значение: {w['directive']} = {w['value']}")
|
|
116
|
+
elif t == 'limit_too_large':
|
|
117
|
+
add_row(t, f"слишком большое значение: {w['directive']} = {w['value']}")
|
|
118
|
+
for v in unused_vars:
|
|
119
|
+
add_row("unused_variable", v['name'])
|
|
120
|
+
for c in listen_conflicts:
|
|
121
|
+
add_row("listen_servername_conflict", f"server1: {c['server1'].get('arg','')} server2: {c['server2'].get('arg','')} listen: {','.join(c['listen'])} server_name: {','.join(c['server_name'])}")
|
|
122
|
+
for r in rewrite_issues:
|
|
123
|
+
add_row(r['type'], r['value'])
|
|
124
|
+
for l in dead_locations:
|
|
125
|
+
add_row("dead_location", f"server: {l['server'].get('arg','')} location: {l['location'].get('arg','')}")
|
|
126
|
+
|
|
127
|
+
if table.row_count == 0:
|
|
128
|
+
console.print("[green]Проблем не найдено[/green]")
|
|
129
|
+
else:
|
|
130
|
+
console.print(table)
|
|
@@ -7,8 +7,8 @@ from commands.diff import diff
|
|
|
7
7
|
from commands.route import route
|
|
8
8
|
from commands.include import include_tree
|
|
9
9
|
from commands.graph import graph
|
|
10
|
-
from commands.logs import
|
|
11
|
-
from commands.syntax import
|
|
10
|
+
from commands.logs import logs
|
|
11
|
+
from commands.syntax import syntax
|
|
12
12
|
|
|
13
13
|
app = typer.Typer(help="nginx-lens — анализ и диагностика конфигураций Nginx")
|
|
14
14
|
console = Console()
|
|
@@ -20,8 +20,8 @@ app.command()(diff)
|
|
|
20
20
|
app.command()(route)
|
|
21
21
|
app.command()(include_tree)
|
|
22
22
|
app.command()(graph)
|
|
23
|
-
app.
|
|
24
|
-
app.
|
|
23
|
+
app.command()(logs)
|
|
24
|
+
app.command()(syntax)
|
|
25
25
|
|
|
26
26
|
if __name__ == "__main__":
|
|
27
27
|
app()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from parser.nginx_parser import parse_nginx_config
|
|
4
|
+
from exporter.graph import tree_to_dot, tree_to_mermaid
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
app = typer.Typer()
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
def graph(
|
|
11
|
+
config_path: str = typer.Argument(..., help="Путь к nginx.conf")
|
|
12
|
+
):
|
|
13
|
+
"""
|
|
14
|
+
Показывает все возможные маршруты nginx в виде цепочек server → location → proxy_pass → upstream → server.
|
|
15
|
+
|
|
16
|
+
Пример:
|
|
17
|
+
nginx-lens graph /etc/nginx/nginx.conf
|
|
18
|
+
"""
|
|
19
|
+
tree = parse_nginx_config(config_path)
|
|
20
|
+
routes = []
|
|
21
|
+
# Для каждого server/location строим маршрут
|
|
22
|
+
def walk(d, chain, upstreams):
|
|
23
|
+
if d.get('block') == 'server':
|
|
24
|
+
srv = d.get('arg','') or '[no arg]'
|
|
25
|
+
for sub in d.get('directives', []):
|
|
26
|
+
walk(sub, chain + [('server', srv)], upstreams)
|
|
27
|
+
elif d.get('block') == 'location':
|
|
28
|
+
loc = d.get('arg','')
|
|
29
|
+
for sub in d.get('directives', []):
|
|
30
|
+
walk(sub, chain + [('location', loc)], upstreams)
|
|
31
|
+
elif d.get('directive') == 'proxy_pass':
|
|
32
|
+
val = d.get('args','')
|
|
33
|
+
# ищем, есть ли такой upstream
|
|
34
|
+
up_name = None
|
|
35
|
+
if val.startswith('http://') or val.startswith('https://'):
|
|
36
|
+
up = val.split('://',1)[1].split('/',1)[0]
|
|
37
|
+
if up in upstreams:
|
|
38
|
+
up_name = up
|
|
39
|
+
if up_name:
|
|
40
|
+
for srv in upstreams[up_name]:
|
|
41
|
+
routes.append(chain + [('proxy_pass', val), ('upstream', up_name), ('upstream_server', srv)])
|
|
42
|
+
else:
|
|
43
|
+
routes.append(chain + [('proxy_pass', val)])
|
|
44
|
+
elif d.get('upstream'):
|
|
45
|
+
# собираем upstream-ы
|
|
46
|
+
upstreams[d['upstream']] = d.get('servers',[])
|
|
47
|
+
# рекурсивно по всем директивам
|
|
48
|
+
for sub in d.get('directives', []):
|
|
49
|
+
walk(sub, chain, upstreams)
|
|
50
|
+
# Собираем upstream-ы
|
|
51
|
+
upstreams = {}
|
|
52
|
+
for d in tree.directives:
|
|
53
|
+
if d.get('upstream'):
|
|
54
|
+
upstreams[d['upstream']] = d.get('servers',[])
|
|
55
|
+
# Строим маршруты
|
|
56
|
+
for d in tree.directives:
|
|
57
|
+
walk(d, [], upstreams)
|
|
58
|
+
if not routes:
|
|
59
|
+
console.print("[yellow]Не найдено ни одного маршрута[/yellow]")
|
|
60
|
+
return
|
|
61
|
+
# Красивый вывод
|
|
62
|
+
for route in routes:
|
|
63
|
+
t = Text()
|
|
64
|
+
for i, (typ, val) in enumerate(route):
|
|
65
|
+
if typ == 'server':
|
|
66
|
+
t.append(f"server: {val}", style="bold blue")
|
|
67
|
+
elif typ == 'location':
|
|
68
|
+
t.append(f" -> location: {val}", style="yellow")
|
|
69
|
+
elif typ == 'proxy_pass':
|
|
70
|
+
t.append(f" -> proxy_pass: {val}", style="green")
|
|
71
|
+
elif typ == 'upstream':
|
|
72
|
+
t.append(f" -> upstream: {val}", style="magenta")
|
|
73
|
+
elif typ == 'upstream_server':
|
|
74
|
+
t.append(f" -> server: {val}", style="grey50")
|
|
75
|
+
console.print(t)
|
|
@@ -4,12 +4,11 @@ from rich.table import Table
|
|
|
4
4
|
import re
|
|
5
5
|
from collections import Counter, defaultdict
|
|
6
6
|
|
|
7
|
-
app = typer.Typer()
|
|
7
|
+
app = typer.Typer(help="Анализ access.log/error.log: топ-статусы, пути, IP, User-Agent, ошибки.")
|
|
8
8
|
console = Console()
|
|
9
9
|
|
|
10
10
|
log_line_re = re.compile(r'(?P<ip>\S+) \S+ \S+ \[(?P<time>[^\]]+)\] "(?P<method>\S+) (?P<path>\S+) [^\"]+" (?P<status>\d{3})')
|
|
11
11
|
|
|
12
|
-
@app.command()
|
|
13
12
|
def logs(
|
|
14
13
|
log_path: str = typer.Argument(..., help="Путь к access.log или error.log"),
|
|
15
14
|
top: int = typer.Option(10, help="Сколько топ-значений выводить")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from analyzer.route import find_route
|
|
5
|
+
from parser.nginx_parser import parse_nginx_config
|
|
6
|
+
import glob
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
app = typer.Typer()
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
def route(
|
|
13
|
+
config_path: str = typer.Argument(None, help="Путь к nginx.conf (если не указан — поиск по всем .conf в /etc/nginx)", show_default=False),
|
|
14
|
+
url: str = typer.Argument(..., help="URL для маршрутизации (например, http://host/path)")
|
|
15
|
+
):
|
|
16
|
+
"""
|
|
17
|
+
Показывает, какой server/location обслуживает указанный URL.
|
|
18
|
+
|
|
19
|
+
Пример:
|
|
20
|
+
nginx-lens route /etc/nginx/nginx.conf http://example.com/api/v1
|
|
21
|
+
nginx-lens route http://example.com/api/v1
|
|
22
|
+
"""
|
|
23
|
+
configs = []
|
|
24
|
+
if config_path:
|
|
25
|
+
configs = [config_path]
|
|
26
|
+
else:
|
|
27
|
+
configs = glob.glob("/etc/nginx/**/*.conf", recursive=True)
|
|
28
|
+
if not configs:
|
|
29
|
+
console.print(Panel("Не найдено ни одного .conf файла в /etc/nginx", style="red"))
|
|
30
|
+
return
|
|
31
|
+
for conf in configs:
|
|
32
|
+
try:
|
|
33
|
+
tree = parse_nginx_config(conf)
|
|
34
|
+
except Exception as e:
|
|
35
|
+
continue # пропускаем битые/невалидные
|
|
36
|
+
res = find_route(tree, url)
|
|
37
|
+
if res:
|
|
38
|
+
server = res['server']
|
|
39
|
+
location = res['location']
|
|
40
|
+
proxy_pass = res['proxy_pass']
|
|
41
|
+
text = f"[bold]Config:[/bold] {conf}\n"
|
|
42
|
+
text += f"[bold]Server:[/bold] {server.get('arg','') or '[no arg]'}\n"
|
|
43
|
+
if location:
|
|
44
|
+
text += f"[bold]Location:[/bold] {location.get('arg','')}\n"
|
|
45
|
+
if proxy_pass:
|
|
46
|
+
text += f"[bold]proxy_pass:[/bold] {proxy_pass}\n"
|
|
47
|
+
console.print(Panel(text, title="Route", style="green"))
|
|
48
|
+
return
|
|
49
|
+
console.print(Panel(f"Ни один ваш конфиг в /etc/nginx не обрабатывает этот URL", style="red"))
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
import subprocess
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(help="Проверка синтаксиса nginx-конфига через nginx -t с подсветкой ошибок.")
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
ERRORS_RE = re.compile(r'in (.+?):(\d+)(?:\s*\n)?(.+?)(?=\nin |$)', re.DOTALL)
|
|
12
|
+
|
|
13
|
+
def syntax(
|
|
14
|
+
config_path: str = typer.Option(None, "-c", "--config", help="Путь к кастомному nginx.conf"),
|
|
15
|
+
nginx_path: str = typer.Option("nginx", help="Путь к бинарю nginx (по умолчанию 'nginx')")
|
|
16
|
+
):
|
|
17
|
+
"""
|
|
18
|
+
Проверяет синтаксис nginx-конфига через nginx -t.
|
|
19
|
+
|
|
20
|
+
В случае ошибки показывает место в виде таблицы с контекстом.
|
|
21
|
+
|
|
22
|
+
Пример:
|
|
23
|
+
nginx-lens syntax -c ./mynginx.conf
|
|
24
|
+
nginx-lens syntax
|
|
25
|
+
"""
|
|
26
|
+
if not config_path:
|
|
27
|
+
candidates = [
|
|
28
|
+
"/etc/nginx/nginx.conf",
|
|
29
|
+
"/usr/local/etc/nginx/nginx.conf",
|
|
30
|
+
"./nginx.conf"
|
|
31
|
+
]
|
|
32
|
+
config_path = next((p for p in candidates if os.path.isfile(p)), None)
|
|
33
|
+
if not config_path:
|
|
34
|
+
console.print("[red]Не удалось найти nginx.conf. Укажите путь через -c.[/red]")
|
|
35
|
+
return
|
|
36
|
+
cmd = [nginx_path, "-t", "-c", os.path.abspath(config_path)]
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
39
|
+
if result.returncode == 0:
|
|
40
|
+
console.print("[green]Синтаксис nginx-конфига корректен[/green]")
|
|
41
|
+
return
|
|
42
|
+
else:
|
|
43
|
+
console.print("[red]Ошибка синтаксиса![/red]")
|
|
44
|
+
console.print(result.stdout)
|
|
45
|
+
console.print(result.stderr)
|
|
46
|
+
# Парсим все ошибки
|
|
47
|
+
err = result.stderr or result.stdout
|
|
48
|
+
errors = list(ERRORS_RE.finditer(err))
|
|
49
|
+
if not errors:
|
|
50
|
+
console.print("[red]Не удалось определить файл и строку ошибки[/red]")
|
|
51
|
+
return
|
|
52
|
+
table = Table(title="Ошибки синтаксиса", show_header=True, header_style="bold red")
|
|
53
|
+
table.add_column("config_file")
|
|
54
|
+
table.add_column("issue_message")
|
|
55
|
+
table.add_column("context")
|
|
56
|
+
for m in errors:
|
|
57
|
+
file, line, msg = m.group(1), int(m.group(2)), m.group(3).strip().split('\n')[0]
|
|
58
|
+
# Читаем контекст
|
|
59
|
+
context_lines = []
|
|
60
|
+
try:
|
|
61
|
+
with open(file) as f:
|
|
62
|
+
lines = f.readlines()
|
|
63
|
+
start = max(0, line-3)
|
|
64
|
+
end = min(len(lines), line+2)
|
|
65
|
+
for i in range(start, end):
|
|
66
|
+
mark = "->" if i+1 == line else " "
|
|
67
|
+
context_lines.append(f"{mark} {lines[i].rstrip()}")
|
|
68
|
+
except Exception:
|
|
69
|
+
context_lines = []
|
|
70
|
+
table.add_row(file, msg, '\n'.join(context_lines))
|
|
71
|
+
console.print(table)
|
|
72
|
+
except FileNotFoundError:
|
|
73
|
+
console.print(f"[red]Не найден бинарь nginx: {nginx_path}[/red]")
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="nginx-lens",
|
|
5
|
-
version="0.1.
|
|
5
|
+
version="0.1.5",
|
|
6
6
|
description="CLI-инструмент для анализа, визуализации и диагностики конфигураций Nginx",
|
|
7
7
|
author="Daniil Astrouski",
|
|
8
8
|
author_email="shelovesuastra@gmail.com",
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich.console import Console
|
|
3
|
-
from rich.table import Table
|
|
4
|
-
from analyzer.conflicts import find_location_conflicts, find_listen_servername_conflicts
|
|
5
|
-
from analyzer.duplicates import find_duplicate_directives
|
|
6
|
-
from analyzer.empty_blocks import find_empty_blocks
|
|
7
|
-
from analyzer.warnings import find_warnings
|
|
8
|
-
from analyzer.unused import find_unused_variables
|
|
9
|
-
from parser.nginx_parser import parse_nginx_config
|
|
10
|
-
from analyzer.rewrite import find_rewrite_issues
|
|
11
|
-
from analyzer.dead_locations import find_dead_locations
|
|
12
|
-
|
|
13
|
-
app = typer.Typer()
|
|
14
|
-
console = Console()
|
|
15
|
-
|
|
16
|
-
def analyze(config_path: str = typer.Argument(..., help="Путь к nginx.conf")):
|
|
17
|
-
"""
|
|
18
|
-
Анализирует конфигурацию Nginx на типовые проблемы и best practices.
|
|
19
|
-
|
|
20
|
-
Показывает:
|
|
21
|
-
- Конфликты location-ов
|
|
22
|
-
- Дублирующиеся директивы
|
|
23
|
-
- Пустые блоки
|
|
24
|
-
- Потенциальные проблемы (proxy_pass без схемы, autoindex on, if, server_tokens on, SSL, лимиты, deprecated и др.)
|
|
25
|
-
- Неиспользуемые переменные
|
|
26
|
-
- Конфликты listen/server_name
|
|
27
|
-
- Проблемы с rewrite
|
|
28
|
-
- Мертвые location-ы
|
|
29
|
-
|
|
30
|
-
Пример:
|
|
31
|
-
nginx-lens analyze /etc/nginx/nginx.conf
|
|
32
|
-
"""
|
|
33
|
-
tree = parse_nginx_config(config_path)
|
|
34
|
-
conflicts = find_location_conflicts(tree)
|
|
35
|
-
dups = find_duplicate_directives(tree)
|
|
36
|
-
empties = find_empty_blocks(tree)
|
|
37
|
-
warnings = find_warnings(tree)
|
|
38
|
-
unused_vars = find_unused_variables(tree)
|
|
39
|
-
listen_conflicts = find_listen_servername_conflicts(tree)
|
|
40
|
-
rewrite_issues = find_rewrite_issues(tree)
|
|
41
|
-
dead_locations = find_dead_locations(tree)
|
|
42
|
-
|
|
43
|
-
table = Table(show_header=True, header_style="bold blue")
|
|
44
|
-
table.add_column("issue_type")
|
|
45
|
-
table.add_column("issue_description")
|
|
46
|
-
|
|
47
|
-
for c in conflicts:
|
|
48
|
-
table.add_row("location_conflict", f"server: {c['server'].get('arg', '')} location: {c['location1']} ↔ {c['location2']}")
|
|
49
|
-
for d in dups:
|
|
50
|
-
table.add_row("duplicate_directive", f"{d['directive']} ({d['args']}) — {d['count']} раз в блоке {d['block'].get('block', d['block'])}")
|
|
51
|
-
for e in empties:
|
|
52
|
-
table.add_row("empty_block", f"{e['block']} {e['arg'] or ''}")
|
|
53
|
-
for w in warnings:
|
|
54
|
-
if w['type'] == 'proxy_pass_no_scheme':
|
|
55
|
-
table.add_row("proxy_pass_no_scheme", f"proxy_pass без схемы: {w['value']}")
|
|
56
|
-
elif w['type'] == 'autoindex_on':
|
|
57
|
-
table.add_row("autoindex_on", f"autoindex on в блоке {w['context'].get('block','')}")
|
|
58
|
-
elif w['type'] == 'if_block':
|
|
59
|
-
table.add_row("if_block", f"директива if внутри блока {w['context'].get('block','')}")
|
|
60
|
-
elif w['type'] == 'server_tokens_on':
|
|
61
|
-
table.add_row("server_tokens_on", f"server_tokens on в блоке {w['context'].get('block','')}")
|
|
62
|
-
for v in unused_vars:
|
|
63
|
-
table.add_row("unused_variable", v['name'])
|
|
64
|
-
for c in listen_conflicts:
|
|
65
|
-
table.add_row("listen_servername_conflict", f"server1: {c['server1'].get('arg','')} server2: {c['server2'].get('arg','')} listen: {','.join(c['listen'])} server_name: {','.join(c['server_name'])}")
|
|
66
|
-
for r in rewrite_issues:
|
|
67
|
-
table.add_row(r['type'], r['value'])
|
|
68
|
-
for l in dead_locations:
|
|
69
|
-
table.add_row("dead_location", f"server: {l['server'].get('arg','')} location: {l['location'].get('arg','')}")
|
|
70
|
-
|
|
71
|
-
if table.row_count == 0:
|
|
72
|
-
console.print("[green]Проблем не найдено[/green]")
|
|
73
|
-
else:
|
|
74
|
-
console.print(table)
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich.console import Console
|
|
3
|
-
from parser.nginx_parser import parse_nginx_config
|
|
4
|
-
from exporter.graph import tree_to_dot, tree_to_mermaid
|
|
5
|
-
|
|
6
|
-
app = typer.Typer()
|
|
7
|
-
console = Console()
|
|
8
|
-
|
|
9
|
-
def graph(
|
|
10
|
-
config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
|
|
11
|
-
format: str = typer.Option("dot", help="Формат: dot или mermaid")
|
|
12
|
-
):
|
|
13
|
-
"""
|
|
14
|
-
Генерирует схему маршрутизации nginx (dot/mermaid).
|
|
15
|
-
|
|
16
|
-
Пример:
|
|
17
|
-
nginx-lens graph /etc/nginx/nginx.conf --format dot
|
|
18
|
-
nginx-lens graph /etc/nginx/nginx.conf --format mermaid
|
|
19
|
-
"""
|
|
20
|
-
tree = parse_nginx_config(config_path)
|
|
21
|
-
if format == "dot":
|
|
22
|
-
console.print(tree_to_dot(tree.directives))
|
|
23
|
-
elif format == "mermaid":
|
|
24
|
-
console.print(tree_to_mermaid(tree.directives))
|
|
25
|
-
else:
|
|
26
|
-
console.print("[red]Неизвестный формат: выберите dot или mermaid[/red]")
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich.console import Console
|
|
3
|
-
from rich.panel import Panel
|
|
4
|
-
from analyzer.route import find_route
|
|
5
|
-
from parser.nginx_parser import parse_nginx_config
|
|
6
|
-
|
|
7
|
-
app = typer.Typer()
|
|
8
|
-
console = Console()
|
|
9
|
-
|
|
10
|
-
def route(
|
|
11
|
-
config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
|
|
12
|
-
url: str = typer.Argument(..., help="URL для маршрутизации (например, http://host/path)")
|
|
13
|
-
):
|
|
14
|
-
"""
|
|
15
|
-
Показывает, какой server/location обслуживает указанный URL.
|
|
16
|
-
|
|
17
|
-
Пример:
|
|
18
|
-
nginx-lens route /etc/nginx/nginx.conf http://example.com/api/v1
|
|
19
|
-
"""
|
|
20
|
-
tree = parse_nginx_config(config_path)
|
|
21
|
-
res = find_route(tree, url)
|
|
22
|
-
if not res:
|
|
23
|
-
console.print(Panel(f"Не найден подходящий server для {url}", style="red"))
|
|
24
|
-
return
|
|
25
|
-
server = res['server']
|
|
26
|
-
location = res['location']
|
|
27
|
-
proxy_pass = res['proxy_pass']
|
|
28
|
-
text = f"[bold]Server:[/bold] {server.get('arg','') or '[no arg]'}\n"
|
|
29
|
-
if location:
|
|
30
|
-
text += f"[bold]Location:[/bold] {location.get('arg','')}\n"
|
|
31
|
-
if proxy_pass:
|
|
32
|
-
text += f"[bold]proxy_pass:[/bold] {proxy_pass}\n"
|
|
33
|
-
console.print(Panel(text, title="Route", style="green"))
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
from rich.console import Console
|
|
3
|
-
from rich.table import Table
|
|
4
|
-
import subprocess
|
|
5
|
-
import os
|
|
6
|
-
import re
|
|
7
|
-
|
|
8
|
-
app = typer.Typer()
|
|
9
|
-
console = Console()
|
|
10
|
-
|
|
11
|
-
ERROR_RE = re.compile(r'in (.+?):(\d+)')
|
|
12
|
-
|
|
13
|
-
@app.command()
|
|
14
|
-
def syntax(
|
|
15
|
-
config_path: str = typer.Argument(..., help="Путь к nginx.conf"),
|
|
16
|
-
nginx_path: str = typer.Option("nginx", help="Путь к бинарю nginx (по умолчанию 'nginx')")
|
|
17
|
-
):
|
|
18
|
-
"""
|
|
19
|
-
Проверяет синтаксис nginx-конфига через nginx -t.
|
|
20
|
-
|
|
21
|
-
В случае ошибки показывает место в виде таблицы с контекстом.
|
|
22
|
-
|
|
23
|
-
Пример:
|
|
24
|
-
nginx-lens syntax /etc/nginx/nginx.conf
|
|
25
|
-
nginx-lens syntax /etc/nginx/nginx.conf --nginx-path /usr/local/sbin/nginx
|
|
26
|
-
"""
|
|
27
|
-
cmd = [nginx_path, "-t", "-c", os.path.abspath(config_path)]
|
|
28
|
-
try:
|
|
29
|
-
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
30
|
-
if result.returncode == 0:
|
|
31
|
-
console.print("[green]Синтаксис nginx-конфига корректен[/green]")
|
|
32
|
-
else:
|
|
33
|
-
console.print("[red]Ошибка синтаксиса![/red]")
|
|
34
|
-
console.print(result.stdout)
|
|
35
|
-
console.print(result.stderr)
|
|
36
|
-
# Парсим ошибку
|
|
37
|
-
err = result.stderr or result.stdout
|
|
38
|
-
m = ERROR_RE.search(err)
|
|
39
|
-
if m:
|
|
40
|
-
file, line = m.group(1), int(m.group(2))
|
|
41
|
-
msg = err.strip().split('\n')[-1]
|
|
42
|
-
# Читаем контекст
|
|
43
|
-
context = []
|
|
44
|
-
try:
|
|
45
|
-
with open(file) as f:
|
|
46
|
-
lines = f.readlines()
|
|
47
|
-
start = max(0, line-3)
|
|
48
|
-
end = min(len(lines), line+2)
|
|
49
|
-
for i in range(start, end):
|
|
50
|
-
mark = "->" if i+1 == line else " "
|
|
51
|
-
context.append((str(i+1), mark, lines[i].rstrip()))
|
|
52
|
-
except Exception:
|
|
53
|
-
context = []
|
|
54
|
-
table = Table(title="Ошибка синтаксиса", show_header=True, header_style="bold red")
|
|
55
|
-
table.add_column("File")
|
|
56
|
-
table.add_column("Line")
|
|
57
|
-
table.add_column("Message")
|
|
58
|
-
table.add_column("Context")
|
|
59
|
-
for ln, mark, code in context:
|
|
60
|
-
table.add_row(file, ln, msg if mark == "->" else "", f"{mark} {code}")
|
|
61
|
-
console.print(table)
|
|
62
|
-
except FileNotFoundError:
|
|
63
|
-
console.print(f"[red]Не найден бинарь nginx: {nginx_path}[/red]")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|