pentool 0.1.0__py3-none-any.whl

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.
Files changed (133) hide show
  1. pentool/__init__.py +4 -0
  2. pentool/__main__.py +16 -0
  3. pentool/api/__init__.py +41 -0
  4. pentool/api/base_api.py +48 -0
  5. pentool/api/comparer_api.py +19 -0
  6. pentool/api/decoder_api.py +24 -0
  7. pentool/api/intruder_api.py +171 -0
  8. pentool/api/proxy_api.py +210 -0
  9. pentool/api/repeater_api.py +57 -0
  10. pentool/api/scanner_api.py +266 -0
  11. pentool/api/sequencer_api.py +17 -0
  12. pentool/api/spider_api.py +106 -0
  13. pentool/api/target_api.py +87 -0
  14. pentool/cli/__init__.py +1 -0
  15. pentool/cli/main.py +98 -0
  16. pentool/cli/project.py +88 -0
  17. pentool/cli/proxy.py +173 -0
  18. pentool/cli/scan.py +115 -0
  19. pentool/core/__init__.py +1 -0
  20. pentool/core/config.py +171 -0
  21. pentool/core/crash_reporter.py +75 -0
  22. pentool/core/database.py +139 -0
  23. pentool/core/event_bus.py +207 -0
  24. pentool/core/events.py +160 -0
  25. pentool/core/features.py +373 -0
  26. pentool/core/license.py +274 -0
  27. pentool/core/logging.py +54 -0
  28. pentool/core/plugin_manager.py +312 -0
  29. pentool/core/project.py +31 -0
  30. pentool/core/storage_interface.py +307 -0
  31. pentool/core/updater.py +96 -0
  32. pentool/core/utils.py +36 -0
  33. pentool/modules/__init__.py +1 -0
  34. pentool/modules/comparer.py +177 -0
  35. pentool/modules/decoder.py +281 -0
  36. pentool/modules/intruder.py +428 -0
  37. pentool/modules/intruder_turbo.py +176 -0
  38. pentool/modules/match_replace.py +142 -0
  39. pentool/modules/proxy.py +797 -0
  40. pentool/modules/repeater.py +195 -0
  41. pentool/modules/sequencer.py +492 -0
  42. pentool/modules/spider.py +679 -0
  43. pentool/modules/target.py +185 -0
  44. pentool/modules/websocket_handler.py +249 -0
  45. pentool/plugins/__init__.py +1 -0
  46. pentool/plugins/builtin/__init__.py +1 -0
  47. pentool/plugins/builtin/payloads_pro.py +381 -0
  48. pentool/plugins/builtin/reports_pro.py +436 -0
  49. pentool/plugins/builtin/scanner_pro.py +291 -0
  50. pentool/plugins/example_plugin.py +39 -0
  51. pentool/services/__init__.py +1 -0
  52. pentool/services/base_service.py +60 -0
  53. pentool/services/intruder_service.py +94 -0
  54. pentool/services/proxy_service.py +178 -0
  55. pentool/services/repeater_service.py +85 -0
  56. pentool/services/scan_service.py +330 -0
  57. pentool/storage/__init__.py +0 -0
  58. pentool/storage/http_storage.py +506 -0
  59. pentool/storage/large_body_handler.py +50 -0
  60. pentool/storage/lru_cache.py +45 -0
  61. pentool/t.py +213 -0
  62. pentool/tui/__init__.py +1 -0
  63. pentool/tui/app.py +1429 -0
  64. pentool/tui/constants.py +79 -0
  65. pentool/tui/dialogs/__init__.py +0 -0
  66. pentool/tui/dialogs/cert_dialog.py +81 -0
  67. pentool/tui/dialogs/file_selector.py +226 -0
  68. pentool/tui/dialogs/load_from_proxy.py +125 -0
  69. pentool/tui/dialogs/match_replace_dialog.py +229 -0
  70. pentool/tui/dialogs/project_dialog.py +82 -0
  71. pentool/tui/dialogs/scope_dialog.py +224 -0
  72. pentool/tui/messages.py +103 -0
  73. pentool/tui/mixins/__init__.py +0 -0
  74. pentool/tui/mixins/app_mixin.py +59 -0
  75. pentool/tui/mixins/dialog_cancel.py +21 -0
  76. pentool/tui/mixins/request_context_menu.py +247 -0
  77. pentool/tui/mixins/tab_rename.py +89 -0
  78. pentool/tui/screens/__init__.py +31 -0
  79. pentool/tui/screens/comparer/__init__.py +3 -0
  80. pentool/tui/screens/comparer/screen.py +229 -0
  81. pentool/tui/screens/dashboard/__init__.py +2 -0
  82. pentool/tui/screens/dashboard/live_dashboard.py +730 -0
  83. pentool/tui/screens/dashboard/screen.py +771 -0
  84. pentool/tui/screens/decoder/__init__.py +3 -0
  85. pentool/tui/screens/decoder/screen.py +306 -0
  86. pentool/tui/screens/extensions/__init__.py +3 -0
  87. pentool/tui/screens/extensions/screen.py +24 -0
  88. pentool/tui/screens/intruder/__init__.py +3 -0
  89. pentool/tui/screens/intruder/screen.py +1356 -0
  90. pentool/tui/screens/proxy/__init__.py +3 -0
  91. pentool/tui/screens/proxy/screen.py +1554 -0
  92. pentool/tui/screens/repeater/__init__.py +3 -0
  93. pentool/tui/screens/repeater/screen.py +621 -0
  94. pentool/tui/screens/scanner/__init__.py +3 -0
  95. pentool/tui/screens/scanner/screen.py +1956 -0
  96. pentool/tui/screens/sequencer/__init__.py +3 -0
  97. pentool/tui/screens/sequencer/screen.py +516 -0
  98. pentool/tui/screens/settings/__init__.py +5 -0
  99. pentool/tui/screens/settings/hotkeys.py +134 -0
  100. pentool/tui/screens/settings/screen.py +540 -0
  101. pentool/tui/screens/spider/__init__.py +3 -0
  102. pentool/tui/screens/spider/screen.py +380 -0
  103. pentool/tui/screens/target/__init__.py +4 -0
  104. pentool/tui/screens/target/screen.py +358 -0
  105. pentool/tui/screens/terminal/__init__.py +5 -0
  106. pentool/tui/screens/terminal/screen.py +179 -0
  107. pentool/tui/widgets/__init__.py +1 -0
  108. pentool/tui/widgets/context_menu.py +148 -0
  109. pentool/tui/widgets/filter_bar.py +204 -0
  110. pentool/tui/widgets/inspector_panel.py +111 -0
  111. pentool/tui/widgets/menu.py +86 -0
  112. pentool/tui/widgets/menu_bar.py +238 -0
  113. pentool/tui/widgets/module_tabs.py +68 -0
  114. pentool/tui/widgets/payload_drop_zone.py +65 -0
  115. pentool/tui/widgets/request_editor.py +495 -0
  116. pentool/tui/widgets/resize_handle.py +116 -0
  117. pentool/tui/widgets/search_bar.py +112 -0
  118. pentool/tui/widgets/statusbar.py +96 -0
  119. pentool/tui/widgets/toolbar_button.py +68 -0
  120. pentool/utils/__init__.py +1 -0
  121. pentool/utils/cert.py +314 -0
  122. pentool/utils/coder.py +170 -0
  123. pentool/utils/copy_as.py +250 -0
  124. pentool/utils/diff.py +82 -0
  125. pentool/utils/http_client.py +145 -0
  126. pentool/utils/parser.py +227 -0
  127. pentool/utils/terminal_check.py +31 -0
  128. pentool-0.1.0.dist-info/METADATA +231 -0
  129. pentool-0.1.0.dist-info/RECORD +133 -0
  130. pentool-0.1.0.dist-info/WHEEL +5 -0
  131. pentool-0.1.0.dist-info/entry_points.txt +2 -0
  132. pentool-0.1.0.dist-info/licenses/LICENSE +34 -0
  133. pentool-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,428 @@
1
+ """Intruder — модуль автоматизированных атак с подстановкой payload'ов."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import csv
7
+ import itertools
8
+ import re
9
+ import uuid
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timezone
12
+ from enum import Enum
13
+ from pathlib import Path
14
+ from typing import AsyncIterator, Callable, Iterator
15
+
16
+ from pentool.core.logging import get_logger
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class AttackType(str, Enum):
22
+ SNIPER = "sniper"
23
+ BATTERING_RAM = "battering_ram"
24
+ PITCHFORK = "pitchfork"
25
+ CLUSTER_BOMB = "cluster_bomb"
26
+
27
+
28
+ @dataclass
29
+ class IntruderResult:
30
+ id: str
31
+ attack_id: str
32
+ request_number: int
33
+ payload_values: list[str] # значения по каждой позиции
34
+ request_raw: str
35
+ response_status: int | None
36
+ response_length: int | None
37
+ response_time_ms: int | None
38
+ error: str | None
39
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
40
+
41
+
42
+ @dataclass
43
+ class IntruderConfig:
44
+ template: str # raw HTTP с маркерами §payload§
45
+ attack_type: AttackType
46
+ payload_sets: list[list[str]] # один список на каждую позицию
47
+ threads: int = 10
48
+ delay_ms: int = 0
49
+ follow_redirects: bool = False
50
+ timeout: int = 30
51
+
52
+
53
+ def parse_markers(template: str) -> tuple[str, list[tuple[int, int]]]:
54
+ """Найти §маркеры§ в шаблоне.
55
+
56
+ Returns:
57
+ (clean_template, positions) где positions — список (start, end)
58
+ в clean_template для каждой позиции.
59
+ """
60
+ positions: list[tuple[int, int]] = []
61
+ result = []
62
+ i = 0
63
+ offset = 0 # смещение из-за удалённых §
64
+
65
+ while i < len(template):
66
+ if template[i] == "§":
67
+ # Ищем закрывающий §
68
+ j = template.find("§", i + 1)
69
+ if j == -1:
70
+ # Нет закрывающего — оставляем как есть
71
+ result.append(template[i])
72
+ i += 1
73
+ continue
74
+ # Вырезаем содержимое маркера
75
+ inner = template[i + 1 : j]
76
+ start = len(result)
77
+ result.append(inner)
78
+ end = len(result)
79
+ positions.append((start, end))
80
+ i = j + 1
81
+ else:
82
+ result.append(template[i])
83
+ i += 1
84
+
85
+ return "".join(result), positions
86
+
87
+
88
+ def _find_marker_positions(template: str) -> list[tuple[int, int]]:
89
+ """Найти позиции §...§ пар в строке. Возвращает (start_§, end_§)."""
90
+ positions = []
91
+ i = 0
92
+ while i < len(template):
93
+ start = template.find("§", i)
94
+ if start == -1:
95
+ break
96
+ end = template.find("§", start + 1)
97
+ if end == -1:
98
+ break
99
+ positions.append((start, end + 1)) # включая оба §
100
+ i = end + 1
101
+ return positions
102
+
103
+
104
+ def substitute_payload(template: str, payload_values: list[str]) -> str:
105
+ """Подставить payload'ы в §маркеры§ шаблона.
106
+
107
+ Заменяет маркеры слева направо. Если payload_values меньше чем маркеров —
108
+ оставшиеся маркеры заменяются пустой строкой.
109
+ """
110
+ result = template
111
+ idx = 0
112
+ while True:
113
+ start = result.find("§")
114
+ if start == -1:
115
+ break
116
+ end = result.find("§", start + 1)
117
+ if end == -1:
118
+ break
119
+ value = payload_values[idx] if idx < len(payload_values) else ""
120
+ result = result[:start] + value + result[end + 1:]
121
+ idx += 1
122
+ return result
123
+
124
+
125
+ def count_markers(template: str) -> int:
126
+ """Подсчитать количество §маркеров§ в шаблоне."""
127
+ count = 0
128
+ i = 0
129
+ while True:
130
+ start = template.find("§", i)
131
+ if start == -1:
132
+ break
133
+ end = template.find("§", start + 1)
134
+ if end == -1:
135
+ break
136
+ count += 1
137
+ i = end + 1
138
+ return count
139
+
140
+
141
+ def extract_marker_defaults(template: str) -> list[str]:
142
+ """Извлечь оригинальные значения из §маркеров§ шаблона.
143
+
144
+ Например: 'user=§admin§&pass=§secret§' → ['admin', 'secret']
145
+ """
146
+ defaults = []
147
+ i = 0
148
+ while i < len(template):
149
+ start = template.find("§", i)
150
+ if start == -1:
151
+ break
152
+ end = template.find("§", start + 1)
153
+ if end == -1:
154
+ break
155
+ defaults.append(template[start + 1:end])
156
+ i = end + 1
157
+ return defaults
158
+
159
+
160
+ def load_payloads_from_file(path: str) -> list[str]:
161
+ result = []
162
+ try:
163
+ text = Path(path).read_text(encoding="utf-8", errors="replace")
164
+ for line in text.splitlines():
165
+ stripped = line.strip()
166
+ if stripped and not stripped.startswith("#"):
167
+ result.append(stripped)
168
+ except Exception:
169
+ pass
170
+ return result
171
+
172
+
173
+ def generate_numeric_payloads(start: int, end: int, step: int = 1) -> list[str]:
174
+ """Числовой диапазон [start, end) с шагом step."""
175
+ return [str(n) for n in range(start, end, step)]
176
+
177
+
178
+ def generate_char_payloads(charset: str, min_len: int, max_len: int) -> list[str]:
179
+ """Перебор строк по набору символов заданной длины.
180
+
181
+ Осторожно: может быть очень большим. Для min_len > max_len возвращает [].
182
+ """
183
+ result = []
184
+ for length in range(min_len, max_len + 1):
185
+ for combo in itertools.product(charset, repeat=length):
186
+ result.append("".join(combo))
187
+ return result
188
+
189
+
190
+ def process_payload(payload: str, operations: list[str]) -> str:
191
+ """Применить операции из utils/coder.py к payload.
192
+
193
+ Операции применяются последовательно (pipeline).
194
+ Неизвестные операции — игнорируются.
195
+ """
196
+ from pentool.utils.coder import OPERATIONS
197
+ result = payload
198
+ for op in operations:
199
+ func = OPERATIONS.get(op)
200
+ if func is not None:
201
+ try:
202
+ result = func(result)
203
+ except Exception:
204
+ pass
205
+ return result
206
+
207
+
208
+ class IntruderAttack:
209
+ """Выполняет intruder-атаку заданного типа."""
210
+
211
+ def __init__(
212
+ self,
213
+ config: IntruderConfig,
214
+ db_path: str | None = None,
215
+ http_client=None,
216
+ ) -> None:
217
+ self._config = config
218
+ self._db_path = db_path
219
+ self._http_client = http_client
220
+ self._attack_id = str(uuid.uuid4())
221
+ self._is_running = False
222
+ self._paused = False
223
+ self._stopped = False
224
+ self._done = 0
225
+ self._total = 0
226
+ self._results: list[IntruderResult] = []
227
+ self._pause_event = asyncio.Event()
228
+ self._pause_event.set() # не на паузе изначально
229
+
230
+ @property
231
+ def attack_id(self) -> str:
232
+ return self._attack_id
233
+
234
+ @property
235
+ def is_running(self) -> bool:
236
+ return self._is_running
237
+
238
+ @property
239
+ def total_requests(self) -> int:
240
+ return self._total
241
+
242
+ @property
243
+ def progress(self) -> tuple[int, int]:
244
+ return self._done, self._total
245
+
246
+ @property
247
+ def results(self) -> list[IntruderResult]:
248
+ return list(self._results)
249
+
250
+ def _iter_sniper(self) -> Iterator[list[str]]:
251
+ """Один набор payload'ов, по одной позиции за раз.
252
+ N_positions × M запросов.
253
+ Незатрагиваемые позиции сохраняют оригинальное значение из шаблона.
254
+ """
255
+ n_positions = count_markers(self._config.template)
256
+ defaults = extract_marker_defaults(self._config.template)
257
+ # Дополняем defaults до n_positions пустыми строками на случай рассинхрона
258
+ while len(defaults) < n_positions:
259
+ defaults.append("")
260
+ # Берём только первый payload set для всех позиций
261
+ payloads = self._config.payload_sets[0] if self._config.payload_sets else []
262
+ for pos_idx in range(n_positions):
263
+ for payload in payloads:
264
+ # В позиции pos_idx подставляем payload, в остальных — оригинал
265
+ values = list(defaults)
266
+ values[pos_idx] = payload
267
+ yield values
268
+
269
+ def _iter_battering_ram(self) -> Iterator[list[str]]:
270
+ """Один payload во все позиции одновременно. M запросов."""
271
+ n_positions = count_markers(self._config.template)
272
+ payloads = self._config.payload_sets[0] if self._config.payload_sets else []
273
+ for payload in payloads:
274
+ yield [payload] * n_positions
275
+
276
+ def _iter_pitchfork(self) -> Iterator[list[str]]:
277
+ """Параллельно берёт по одному из каждого набора (zip). min(M) запросов."""
278
+ sets = self._config.payload_sets
279
+ if not sets:
280
+ return
281
+ for combo in zip(*sets):
282
+ yield list(combo)
283
+
284
+ def _iter_cluster_bomb(self) -> Iterator[list[str]]:
285
+ """Декартово произведение всех наборов. M1×M2×... запросов."""
286
+ sets = self._config.payload_sets
287
+ if not sets:
288
+ return
289
+ for combo in itertools.product(*sets):
290
+ yield list(combo)
291
+
292
+ def _get_iterator(self) -> Iterator[list[str]]:
293
+ t = self._config.attack_type
294
+ if t == AttackType.SNIPER:
295
+ return self._iter_sniper()
296
+ elif t == AttackType.BATTERING_RAM:
297
+ return self._iter_battering_ram()
298
+ elif t == AttackType.PITCHFORK:
299
+ return self._iter_pitchfork()
300
+ elif t == AttackType.CLUSTER_BOMB:
301
+ return self._iter_cluster_bomb()
302
+ return iter([])
303
+
304
+ def _count_total(self) -> int:
305
+ """Подсчитать общее количество запросов (без реального выполнения)."""
306
+ return sum(1 for _ in self._get_iterator())
307
+
308
+ async def run(
309
+ self,
310
+ on_result: Callable[[IntruderResult], None],
311
+ on_progress: Callable[[int, int], None],
312
+ ) -> None:
313
+ self._is_running = True
314
+ self._stopped = False
315
+ self._done = 0
316
+ self._results = []
317
+
318
+ combinations = list(self._get_iterator())
319
+ self._total = len(combinations)
320
+ logger.info("INTRUDER: run() started, total=%d, type=%s, threads=%d", self._total, self._config.attack_type, self._config.threads)
321
+ on_progress(0, self._total)
322
+
323
+ sem = asyncio.Semaphore(self._config.threads)
324
+
325
+ async def _run_one(req_num: int, payload_values: list[str]) -> None:
326
+ if self._stopped:
327
+ return
328
+ # Ожидаем снятия паузы
329
+ await self._pause_event.wait()
330
+ if self._stopped:
331
+ return
332
+
333
+ async with sem:
334
+ if self._stopped:
335
+ return
336
+
337
+ # Подставляем payload'ы
338
+ request_raw = substitute_payload(self._config.template, payload_values)
339
+
340
+ # Задержка
341
+ if self._config.delay_ms > 0:
342
+ await asyncio.sleep(self._config.delay_ms / 1000.0)
343
+
344
+ result = await self._send_request(req_num, payload_values, request_raw)
345
+ self._results.append(result)
346
+ on_result(result)
347
+ self._done += 1
348
+ on_progress(self._done, self._total)
349
+
350
+ tasks = [
351
+ asyncio.create_task(_run_one(i + 1, vals))
352
+ for i, vals in enumerate(combinations)
353
+ ]
354
+ if tasks:
355
+ await asyncio.gather(*tasks, return_exceptions=True)
356
+
357
+ self._is_running = False
358
+
359
+ async def _send_request(
360
+ self,
361
+ req_num: int,
362
+ payload_values: list[str],
363
+ request_raw: str,
364
+ ) -> IntruderResult:
365
+ import time
366
+ t0 = time.monotonic()
367
+ status = None
368
+ length = None
369
+ error_msg = None
370
+
371
+ try:
372
+ from pentool.utils.parser import parse_http_request
373
+ from pentool.utils.http_client import HTTPClient
374
+
375
+ req = parse_http_request(request_raw)
376
+ async with HTTPClient(timeout=self._config.timeout) as client:
377
+ resp = await client.send(req)
378
+ status = resp.status
379
+ body = resp.body if isinstance(resp.body, (bytes, str)) else b""
380
+ length = len(body) if isinstance(body, bytes) else len(body.encode("utf-8", errors="replace"))
381
+ except Exception as exc:
382
+ error_msg = str(exc)
383
+ logger.warning("INTRUDER: _send_request #%d error: %s", req_num, exc)
384
+
385
+ elapsed_ms = int((time.monotonic() - t0) * 1000)
386
+ logger.debug("INTRUDER: #%d payload=%s status=%s length=%s time=%dms", req_num, payload_values, status, length, elapsed_ms)
387
+
388
+ return IntruderResult(
389
+ id=str(uuid.uuid4()),
390
+ attack_id=self._attack_id,
391
+ request_number=req_num,
392
+ payload_values=payload_values,
393
+ request_raw=request_raw,
394
+ response_status=status,
395
+ response_length=length,
396
+ response_time_ms=elapsed_ms,
397
+ error=error_msg,
398
+ )
399
+
400
+ async def pause(self) -> None:
401
+ self._paused = True
402
+ self._pause_event.clear()
403
+
404
+ async def resume(self) -> None:
405
+ self._paused = False
406
+ self._pause_event.set()
407
+
408
+ async def stop(self) -> None:
409
+ self._stopped = True
410
+ self._pause_event.set() # разблокировать ожидающие
411
+ self._is_running = False
412
+
413
+ def export_csv(self, path: str) -> None:
414
+ with open(path, "w", newline="", encoding="utf-8") as f:
415
+ writer = csv.writer(f)
416
+ writer.writerow([
417
+ "#", "Payloads", "Status", "Length", "Time(ms)", "Error", "Timestamp"
418
+ ])
419
+ for r in self._results:
420
+ writer.writerow([
421
+ r.request_number,
422
+ " | ".join(r.payload_values),
423
+ r.response_status or "",
424
+ r.response_length or "",
425
+ r.response_time_ms or "",
426
+ r.error or "",
427
+ r.timestamp.isoformat(),
428
+ ])
@@ -0,0 +1,176 @@
1
+ """Turbo Mode для Intruder — оптимизированная версия с connection pooling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ from typing import Callable
8
+
9
+ import aiohttp
10
+
11
+ from pentool.modules.intruder import IntruderResult, IntruderConfig, substitute_payload
12
+ from pentool.core.logging import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class TurboIntruderAttack:
18
+ """Turbo Mode для Intruder — 10x быстрее через connection pooling."""
19
+
20
+ def __init__(self, config: IntruderConfig) -> None:
21
+ self._config = config
22
+ self._is_running = False
23
+ self._stopped = False
24
+ self._done = 0
25
+ self._total = 0
26
+ self._results: list[IntruderResult] = []
27
+ self._session: aiohttp.ClientSession | None = None
28
+
29
+ async def run(
30
+ self,
31
+ on_result: Callable[[IntruderResult], None],
32
+ on_progress: Callable[[int, int], None],
33
+ ) -> None:
34
+ from pentool.modules.intruder import IntruderAttack
35
+
36
+ # Используем базовый класс для генерации комбинаций
37
+ base_attack = IntruderAttack(self._config)
38
+ combinations = list(base_attack._get_iterator())
39
+ self._total = len(combinations)
40
+
41
+ logger.info("TURBO INTRUDER: started, total=%d, threads=%d (Turbo Mode)",
42
+ self._total, self._config.threads)
43
+
44
+ self._is_running = True
45
+ self._stopped = False
46
+ self._done = 0
47
+ self._results = []
48
+
49
+ on_progress(0, self._total)
50
+
51
+ # Создаём connection pool
52
+ connector = aiohttp.TCPConnector(
53
+ limit=self._config.threads * 2, # Больше соединений для Keep-Alive
54
+ limit_per_host=self._config.threads,
55
+ ttl_dns_cache=300,
56
+ enable_cleanup_closed=True,
57
+ )
58
+
59
+ timeout = aiohttp.ClientTimeout(total=self._config.timeout)
60
+
61
+ async with aiohttp.ClientSession(
62
+ connector=connector,
63
+ timeout=timeout,
64
+ headers={"Connection": "keep-alive"}, # Keep-Alive!
65
+ ) as session:
66
+ self._session = session
67
+
68
+ # Используем семафор для контроля параллелизма
69
+ sem = asyncio.Semaphore(self._config.threads)
70
+
71
+ async def _run_one(req_num: int, payload_values: list[str]) -> None:
72
+ if self._stopped:
73
+ return
74
+
75
+ async with sem:
76
+ if self._stopped:
77
+ return
78
+
79
+ request_raw = substitute_payload(self._config.template, payload_values)
80
+
81
+ # Задержка
82
+ if self._config.delay_ms > 0:
83
+ await asyncio.sleep(self._config.delay_ms / 1000.0)
84
+
85
+ result = await self._send_request_turbo(req_num, payload_values, request_raw)
86
+ self._results.append(result)
87
+ on_result(result)
88
+ self._done += 1
89
+ on_progress(self._done, self._total)
90
+
91
+ # Создаём все задачи
92
+ tasks = [
93
+ asyncio.create_task(_run_one(i + 1, vals))
94
+ for i, vals in enumerate(combinations)
95
+ ]
96
+
97
+ if tasks:
98
+ await asyncio.gather(*tasks, return_exceptions=True)
99
+
100
+ self._is_running = False
101
+ self._session = None
102
+
103
+ async def _send_request_turbo(
104
+ self,
105
+ req_num: int,
106
+ payload_values: list[str],
107
+ request_raw: str,
108
+ ) -> IntruderResult:
109
+ import uuid
110
+ from pentool.utils.parser import parse_http_request
111
+
112
+ t0 = time.monotonic()
113
+ status = None
114
+ length = None
115
+ error_msg = None
116
+
117
+ try:
118
+ req = parse_http_request(request_raw)
119
+
120
+ # Формируем URL
121
+ scheme = "https" if req.port == 443 else "http"
122
+ url = f"{scheme}://{req.host}:{req.port}{req.path}"
123
+
124
+ # Отправляем через aiohttp (Keep-Alive автоматом)
125
+ async with self._session.request(
126
+ method=req.method,
127
+ url=url,
128
+ headers=dict(req.headers),
129
+ data=req.body.encode("utf-8") if req.body else None,
130
+ allow_redirects=self._config.follow_redirects,
131
+ ) as response:
132
+ status = response.status
133
+ body = await response.read()
134
+ length = len(body)
135
+
136
+ except Exception as exc:
137
+ error_msg = str(exc)
138
+ logger.debug("TURBO: request #%d error: %s", req_num, exc)
139
+
140
+ elapsed_ms = int((time.monotonic() - t0) * 1000)
141
+
142
+ return IntruderResult(
143
+ id=str(uuid.uuid4()),
144
+ attack_id="turbo",
145
+ request_number=req_num,
146
+ payload_values=payload_values,
147
+ request_raw=request_raw,
148
+ response_status=status,
149
+ response_length=length,
150
+ response_time_ms=elapsed_ms,
151
+ error=error_msg,
152
+ )
153
+
154
+ async def stop(self) -> None:
155
+ self._stopped = True
156
+ self._is_running = False
157
+
158
+ def get_results(self) -> list[IntruderResult]:
159
+ return self._results
160
+
161
+ @property
162
+ def is_running(self) -> bool:
163
+ """Проверить, запущена ли атака."""
164
+ return self._is_running
165
+
166
+ @property
167
+ def attack_id(self) -> str:
168
+ return "turbo"
169
+
170
+ @property
171
+ def results(self) -> list[IntruderResult]:
172
+ return self._results
173
+
174
+ @property
175
+ def progress(self) -> tuple[int, int]:
176
+ return (self._done, self._total)