LIMB
Вход Регистрация

тема

Ищем таргеты из выгрузки живых портов

mike_vazovski · · 7 ответов · 142 просмотров

Привет братва, на связи майк


Давно меня тут не было, накопился интересный опыт которым я хотел бы поделиться.
Сегодня пойдет речь про самый древний способ идентификации живых целей для последующей отработки.
К сожалению сканер, который добыл мне 72 млн строк формата страна(folder) -> port-№.txt я где-то похерил, так как работал не с абузов а с простых серверов до их бана, бекапа не было.

За основу возьмем следуюющую схему пробития внешнего контура:
  1. Определение целей по географическому признаку. В помощь классификатор CIDR - сюда дуй от туда х*й
  2. Мы работаем по ipv4, поэтому берем тереиториальный признак + ipv4 соответственно
  3. По РУ, нашим друзьям  и СНГ не работаем, дело чести
  4. Момент с массканом пропускаем, в следующих статья постараюсь переписать код. В кратце - как по мне это самый эффективный сканер который может быстро и пол подсетям детектить статус того что порт открыт.
  5. Получив выгрузку из открытых портов мы к каждой строке приклеиваем уникальный отпечаток, проще говоря, ищем в http запросе по ключевым словам (Это как раз тема данной статьи)

Не удалось прогрузить медиа



Это выгрузка из masscan по топ 10 открытым портам широкоизвестных в узких кругах Fortinet, из них 36 млн - 443 порт, вторая часть 10443, 8443 и так далее.

Если мы сразу пустим в обработку 72 млн строк, то: 
  1. Высокая вероятность что мы найдем не то что нужно, потомучто большинство систем на голый порт редирект на админ панель авторизации не делают
  2. Более высокая, я бы сказал 100-процентное палево, шум без конкретики, самый первый хостинг провайдер у которого вы арендовали вычислительную линукс мощность сочтут это за DOS
  3. Даже если вы найдете http 200 то не факт что это то что вам нужно
  4. Рекурсивно обойти 100+  каталогов, в каждом из которых потенциально 10-30 txt файлов
  5. Цикл for на языке Bash или Python? в теории да, на практике нет. Вопрос про ЛСИО - ложные сетевые информационные объекты которые находятся в сканируемых подсетях провайдера на всех этапах сканирования. Прямых доказательств нет, но с точки зрения работы сетевого трафика в каждой 10-й подсети есть пидоры, которые на уровне сокета держат твое соединение и не отпускают, истощая вычислительную мощность твоей машины и не давая моему циклу for работать. Простая математика, 5 процентов из 72 млн это 3,6 млн ip адресов, умножь на 10 секунд, если мы говорим про то, что мы долбим цель на другом конце глобуса  - 240 часов только на простой и ожидание, не считая сбоев сканера на уровне сокета. Дальше думайте сами. 
Вот примерно как это выглядит, вызываем линуховую утилиту nethogs. Картина маслом, либо не сбрасывается соединение, либо удерживается

Не удалось прогрузить медиа


По умолчанию мы говорим что это потенциальные цели на которых мы можем поймать, склеить и пульнуть http://ip:port/{remote/login}, где /remote/login - то что мы приклеиваем к каждой строке из нашей выгрузки и ловим ответ http 200 OK, парсим по ключевым словам и если это истина, то записываем это в чанк txt в каталог ./output по заданному классификатору рабочих директорий.
Вот как это выглядело как я только начал в это вникать:

Python:
import os
import re
import requests
import warnings
import concurrent.futures
import time
from urllib.parse import quote
from datetime import datetime
from pathlib import Path

# --- ИМПОРТ И ПРЕДУПРЕЖДЕНИЙ ---
from urllib3.exceptions import InsecureRequestWarning
warnings.filterwarnings("ignore", category=InsecureRequestWarning)

# --- Конфигурация ---
INPUT_DIR = "./input"  # Каталог с входными файлами
OUTPUT_DIR = "./output" # Каталог для результатов (только 200 OK)
OUTPUT_FULL_DIR = "./output_full" # Каталог для подробных логов (только ошибки)
OUTPUT_DONT_WORKED_DIR = "./output_dont_worked" # Каталог для всех обработанных URL
FORTINET_PATH = "/remote/login"  # Путь для проверки
TIMEOUT = 10  # Увеличим таймаут до 10 секунд, если сервера медленные
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 Safari/537.36"
THREADS = 25 # Уменьшено количество потоков
CHUNK_SIZE = 100 # Уменьшено количество URL в одном чанке

# Цвета для вывода в терминал (работает в терминалах, поддерживающих ANSI)
COLOR_RED = "\033[91m"
COLOR_GREEN = "\033[92m"
COLOR_BLUE = "\033[94m"
COLOR_YELLOW = "\033[93m"
COLOR_CYAN = "\033[96m"
COLOR_RESET = "\033[0m"

def log_failure_details(full_log_file_path, target_url, start_time, end_time, status_code_or_error, response_text_summary=""):
    """Записывает подробную информацию о НЕУДАЧНОМ запросе в общий лог-файл для страны."""
    with open(full_log_file_path, 'a', encoding='utf-8') as f:
        f.write(f"--- Failed Request Log ---\n")
        f.write(f"Target URL: {target_url}\n")
        f.write(f"Start Time: {datetime.fromtimestamp(start_time).strftime('%Y-%m-%d %H:%M:%S.%f')}\n")
        f.write(f"End Time: {datetime.fromtimestamp(end_time).strftime('%Y-%m-%d %H:%M:%S.%f')}\n")
        f.write(f"Duration: {end_time - start_time:.2f}s\n")
        f.write(f"Error/Status: {status_code_or_error}\n")
        if response_text_summary:
            f.write(f"Response Summary (first 200 chars): {response_text_summary[:200]}\n")
        f.write(f"Result: FAILED\n")
        f.write(f"-------------------------\n\n")

def process_single_url(args):
    """Обрабатывает один URL. Функция, выполняемая в потоке."""
    extracted_url, full_log_file_path = args
    target_url = extracted_url + FORTINET_PATH
    start_time = time.time()

    try:
        response = requests.get(
            target_url,
            headers={'User-Agent': USER_AGENT},
            verify=False,
            timeout=TIMEOUT
        )
        request_time = time.time() - start_time

        if response.status_code == 200:
            print(f"    {COLOR_GREEN}[+] 200 OK за {request_time:.2f}s для: {target_url}{COLOR_RESET}")
            return extracted_url, True, response.status_code
        else:
            print(f"    {COLOR_RED}[-] Код {response.status_code} за {request_time:.2f}s для: {target_url}{COLOR_RESET}")
            response_summary = response.text if response.text else "[No response body]"
            log_failure_details(full_log_file_path, target_url, start_time, time.time(), response.status_code, response_summary)
            return extracted_url, False, response.status_code

    except requests.exceptions.Timeout:
        error_msg = f"Timeout after {TIMEOUT}s"
        print(f"    {COLOR_RED}[ОШИБКА ТАЙМАУТ] {target_url} ({error_msg}){COLOR_RESET}")
        log_failure_details(full_log_file_path, target_url, start_time, time.time(), error_msg)
        return extracted_url, False, error_msg

    except requests.exceptions.RequestException as e:
        error_msg = str(e)
        print(f"    {COLOR_RED}[ОШИБКА ЗАПРОСА] {target_url}: {error_msg}{COLOR_RESET}")
        log_failure_details(full_log_file_path, target_url, start_time, time.time(), error_msg)
        return extracted_url, False, error_msg

    except Exception as e:
        error_msg = str(e)
        print(f"    {COLOR_RED}[НЕИЗВЕСТНАЯ ОШИБКА] {target_url}: {error_msg}{COLOR_RESET}")
        log_failure_details(full_log_file_path, target_url, start_time, time.time(), error_msg)
        return extracted_url, False, error_msg

def process_chunk(all_extracted_urls, output_file_path, dont_worked_file_path, full_log_file_path):
    """Обрабатывает один чанк URL, используя ThreadPoolExecutor."""
    if not all_extracted_urls:
        return

    successful_urls = set()

    with concurrent.futures.ThreadPoolExecutor(max_workers=THREADS) as executor:
        task_args = [(url, full_log_file_path) for url in all_extracted_urls]
        futures = {executor.submit(process_single_url, arg): arg[0] for arg in task_args}

        for future in concurrent.futures.as_completed(futures):
            extracted_url, success, error_details = future.result()
            if success:
                successful_urls.add(extracted_url)
                with open(output_file_path, 'a', encoding='utf-8') as f_out:
                    f_out.write(extracted_url + "\n")
            with open(dont_worked_file_path, 'a', encoding='utf-8') as f_dont_work:
                f_dont_work.write(extracted_url + "\n")

    print(f"  {COLOR_BLUE}[ЧАНК] Обработано: {len(all_extracted_urls)}, Найдено 200 OK: {len(successful_urls)}.{COLOR_RESET}")


def process_input_file(input_file_path, output_file_path, dont_worked_file_path, full_log_file_path):
    """Обрабатывает один входной файл ЧАНК за ЧАНКОМ, разбирая длинные строки на подстроки."""
    print(f"{COLOR_BLUE}[ИНФО] Начата обработка файла: {input_file_path}{COLOR_RESET}")

    with open(output_file_path, 'w', encoding='utf-8') as f_out:
        pass
    with open(dont_worked_file_path, 'w', encoding='utf-8') as f_dont_work:
        pass

    all_extracted_urls_chunk = []
    try:
        with open(input_file_path, 'r', encoding='utf-8') as f_in:
            for line_num, line in enumerate(f_in, start=1):
                line_content = line.strip()
                if not line_content:
                    continue

                print(f"  {COLOR_BLUE}[ЛОГ] Обработка строки {line_num} из {input_file_path} (длина {len(line_content)}).{COLOR_RESET}")

                # --- Ключевое изменение: разбор строки на подстроки ---
                # Регулярное выражение для поиска https://<IP>:<PORT>
                url_pattern = r'https://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+'
                
                # Используем re.finditer для поиска всех совпадений в строке по одному
                # Это позволяет избежать создания большого списка сразу
                for match in re.finditer(url_pattern, line_content):
                    extracted_url = match.group() # Получаем найденный URL
                    
                    # Добавляем его в текущий чанк
                    all_extracted_urls_chunk.append(extracted_url)

                    # Если чанк заполнен, обрабатываем его
                    if len(all_extracted_urls_chunk) >= CHUNK_SIZE:
                        print(f"  {COLOR_CYAN}[ЧАНК] Заполнен (размер {CHUNK_SIZE}). Начинаю обработку...{COLOR_RESET}")
                        process_chunk(all_extracted_urls_chunk, output_file_path, dont_worked_file_path, full_log_file_path)
                        all_extracted_urls_chunk = [] # Очищаем чанк

    except FileNotFoundError:
        print(f"{COLOR_RED}[ОШИБКА] Файл {input_file_path} не найден.{COLOR_RESET}")
        return False
    except Exception as e:
        print(f"{COLOR_RED}[ОШИБКА] Ошибка чтения файла {input_file_path}: {e}{COLOR_RESET}")
        return False

    # Обрабатываем оставшиеся URL, если они есть
    if all_extracted_urls_chunk:
        print(f"  {COLOR_CYAN}[ЧАНК] Финальный (размер {len(all_extracted_urls_chunk)}). Начинаю обработку...{COLOR_RESET}")
        process_chunk(all_extracted_urls_chunk, output_file_path, dont_worked_file_path, full_log_file_path)

    print(f"{COLOR_BLUE}[ИНФО] Завершена обработка {input_file_path} (все чанки).{COLOR_RESET}")
    return True


def main():
    """Основная функция сканера Fortinet."""
    print(f"{COLOR_CYAN}--- Старт сканирования каталога {INPUT_DIR} (многопоточный, чанки, разбор строки, удаление после обработки) ---{COLOR_RESET}")

    if not os.path.isdir(INPUT_DIR):
        print(f"{COLOR_RED}[ОШИБКА] Каталог {INPUT_DIR} не найден.{COLOR_RESET}")
        return

    input_file_paths = []
    for country_dir_name in os.listdir(INPUT_DIR):
        country_input_path = os.path.join(INPUT_DIR, country_dir_name)

        if not os.path.isdir(country_input_path) or len(country_dir_name) != 2:
            continue

        print(f"{COLOR_BLUE}[ИНФО] Обрабатываем подкаталог: {country_input_path}{COLOR_RESET}")

        country_full_log_path = os.path.join(OUTPUT_FULL_DIR, country_dir_name)
        country_output_path = os.path.join(OUTPUT_DIR, country_dir_name)
        country_dont_worked_path = os.path.join(OUTPUT_DONT_WORKED_DIR, country_dir_name)

        os.makedirs(country_full_log_path, exist_ok=True)
        os.makedirs(country_output_path, exist_ok=True)
        os.makedirs(country_dont_worked_path, exist_ok=True)

        full_log_file_path = os.path.join(country_full_log_path, "full.log")

        for filename in os.listdir(country_input_path):
            if not filename.lower().endswith('.txt'):
                 continue

            input_file_path = os.path.join(country_input_path, filename)
            input_file_paths.append(input_file_path)

    print(f"{COLOR_BLUE}[ИНФО] Найдено {len(input_file_paths)} файлов для обработки.{COLOR_RESET}")

    for input_file_path in input_file_paths:
        path_parts = Path(input_file_path).parts
        if len(path_parts) < 3 or path_parts[-3] != 'input':
            print(f"{COLOR_YELLOW}[ПРЕДУПРЕЖДЕНИЕ] Неверный путь к файлу: {input_file_path}{COLOR_RESET}")
            continue

        country_code = path_parts[-2]
        filename = os.path.basename(input_file_path)
        output_file_path = os.path.join(OUTPUT_DIR, country_code, filename)
        dont_worked_file_path = os.path.join(OUTPUT_DONT_WORKED_DIR, country_code, filename)

        success = process_input_file(input_file_path, output_file_path, dont_worked_file_path, os.path.join(OUTPUT_FULL_DIR, country_code, "full.log"))

        if success:
            try:
                os.remove(input_file_path)
                print(f"{COLOR_GREEN}[+] Файл успешно обработан и удалён: {input_file_path}{COLOR_RESET}")
            except OSError as e:
                print(f"{COLOR_RED}[-] Ошибка при удалении файла {input_file_path}: {e}{COLOR_RESET}")
        else:
            print(f"{COLOR_YELLOW}[-] Файл НЕ был удалён из-за ошибки при обработке: {input_file_path}{COLOR_RESET}")

    print(f"\n{COLOR_CYAN}--- Сканирование завершено ---{COLOR_RESET}")


if __name__ == "__main__":
    main()



 Структура и конфигурация​

INPUT_DIR = "./input"           # Входные файлы по странам: ./input/US/file.txt
OUTPUT_DIR = "./output"         # Только успешные (200 OK)
OUTPUT_FULL_DIR = "./output_full"  # Детальные логи ошибок
OUTPUT_DONT_WORKED_DIR = "./output_dont_worked"  # Все обработанные URL
FORTINET_PATH = "/remote/login" # Проверяемый эндпоинт
TIMEOUT = 10                    # Таймаут запроса в секундах
THREADS = 25                    # Количество потоков
CHUNK_SIZE = 100                # URL в одном пакете обработки

Ожидаемая структура каталогов​

./input/
├── US/
│   ├── file1.txt
│   └── file2.txt
├── DE/
│   └── targets.txt
└── ...

./output/          # → сюда пишутся только 200 OK
./output_full/     # → детальные логи ошибок
./output_dont_worked/  # → все проверенные URL


Ключевые функции​

log_failure_details(...)
Записывает в full.log подробную информацию о неудачных запросах:
  • Целевой URL
  • Время начала/окончания и длительность
  • Код статуса или текст ошибки
  • Первые 200 символов ответа (если есть)

process_single_url(args)
Записывает в full.log подробную информацию о неудачных запросах:
  • Целевой URL
  • Время начала/окончания и длительность
  • Код статуса или текст ошибки
  • Первые 200 символов ответа (если есть)

process_chunk(...)

  • Создаёт ThreadPoolExecutor(max_workers=THREADS)
  • Распределяет URL чанка между потоками
  • По мере завершения потоков:
    • Успешные → записывает в output_file_path
    • Все → записывает в dont_worked_file_path
  • Выводит статистику по чанку

process_input_file(...) - чтение и парсинг​

 main() - оркестрация​

Визуализация потока данных​

┌─────────────────┐
│  ./input/US/*.txt                   │
└────────┬────────┘
                      ▼
┌─────────────────┐
│  Чтение строки                  │
│  Regex-парсинг                 │
│  Извлечение URL              │
└────────┬────────┘
                       ▼
┌─────────────────┐
│  Накопление в                   │
│  чанк (100 URL)                 │
└────────┬────────┘
                       ▼
┌─────────────────┐
│  ThreadPool (25)│
│  • GET /remote/login
│  • Обработка ответов
└────────┬────────┘
                      ▼
           ┌────┴────┐
           ▼                      ▼
┌────────┐ ┌────────────┐
│200 OK т      │ │Не 200/Ошибка     │
│→ output      │ │→ output_full│      
│                  │ │→ output_dont_worked│
└────────┘ └────────────┘


Сильные стороны:

  • :white_check_mark: Модульная структура, читаемый код
  • :white_check_mark: Многопоточность с контролем нагрузки (чанки + THREADS)
  • :white_check_mark: Детальное логирование ошибок
  • :white_check_mark: Поддержка пакетной обработки по странам
  • :white_check_mark: Автоматическая очистка обработанных файлов
Слабые стороны:
  • :x: Отсутствие устойчивости к прерываниям (Ctrl+C)
  • :x: Жёсткие настройки таймаута и потоков
  • :x: Нет дедупликации и проверки валидности IP

Спойлер: факт
Могу сказать честно - полная хуйня - работает медленно, зависает и работает нестабильно. Однако очень полезно это все с точки зрения того, что на больших входных данных можно это все диагностировать и выявлять те самые костыли, о которых вам не расскажет ни один GPT - сокеты, ЛСИО и неидеальность раьботы очень маленьких сетевых шестеренок.


Второй итерацией был следующий код, в нем изменены и доработаны существенные недостатки
Вот собственно и он

Python:
import os
import re
import requests
import warnings
import concurrent.futures
import time
import random
from urllib.parse import quote
from datetime import datetime
from pathlib import Path
import sys
import queue
from threading import Thread, Event

# --- ИМПОРТ И ПРЕДУПРЕЖДЕНИЙ ---
from urllib3.exceptions import InsecureRequestWarning
warnings.filterwarnings("ignore", category=InsecureRequestWarning)

# --- Конфигурация ---
INPUT_DIR = "./input"
OUTPUT_DIR = "./output"
OUTPUT_DONT_WORKED_DIR = "./output_dont_worked"
FORTINET_PATH = "/remote/login"
TIMEOUT = 10
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.120 Safari/537.36"
THREADS = 200
CHUNK_SIZE = 1000
CHUNK_DELAY = 1
REQUEST_DELAY_MEAN = 0.1
REQUEST_DELAY_VARIANCE = 0.05
WRITE_BUFFER_SIZE = 500
WRITE_BUFFER_TIMEOUT = 6

# --- Автоматическое определение портов ---
# Цвета для вывода в терминал (работает в терминалах, поддерживающих ANSI)
COLOR_RED = "\033[91m"
COLOR_GREEN = "\033[92m"
COLOR_BLUE = "\033[94m"
COLOR_YELLOW = "\033[93m"
COLOR_CYAN = "\033[96m"
COLOR_RESET = "\033[0m"

print(f"{COLOR_CYAN}--- Сканирование каталога {INPUT_DIR} для определения портов ---{COLOR_RESET}")
discovered_ports = set()
for item in os.listdir(INPUT_DIR):
    item_path = os.path.join(INPUT_DIR, item)
    if os.path.isdir(item_path) and len(item) == 2:
        try:
            files_in_subdir = os.listdir(item_path)
        except OSError as e:
            print(f"  {COLOR_RED}[ОШИБКА] Не удалось прочитать подкаталог '{item}': {e}{COLOR_RESET}")
            continue

        for filename in files_in_subdir:
            if filename.lower().endswith('.txt'):
                match = re.match(r'^(\d+)-', filename)
                if match:
                    port = int(match.group(1))
                    discovered_ports.add(port)

PORTS_TO_PROCESS = sorted(list(discovered_ports))
print(f"{COLOR_BLUE}[ИНФО] Обнаружено {len(PORTS_TO_PROCESS)} уникальных портов для обработки: {PORTS_TO_PROCESS}{COLOR_RESET}")

from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def worker_thread(url_queue, result_queue, stop_event, stats):
    session = requests.Session()
    retry_strategy = Retry(
        total=3,
        backoff_factor=0.3,
        status_forcelist=[429, 500, 502, 503, 504],
    )
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("http://", adapter)
    session.mount("https://", adapter)

    while not stop_event.is_set():
        try:
            extracted_url = url_queue.get(timeout=1)
        except queue.Empty:
            continue

        if extracted_url is None:
            url_queue.task_done()
            break

        target_url = extracted_url + FORTINET_PATH
        time.sleep(random.uniform(REQUEST_DELAY_MEAN - REQUEST_DELAY_VARIANCE, REQUEST_DELAY_MEAN + REQUEST_DELAY_VARIANCE))

        start_time = time.time()

        try:
            response = session.get(
                target_url,
                headers={'User-Agent': USER_AGENT},
                verify=False,
                timeout=TIMEOUT
            )
            request_time = time.time() - start_time

            if response.status_code == 200:
                print(f"    {COLOR_GREEN}[+] 200 OK за {request_time:.2f}s для: {target_url}{COLOR_RESET}")
                stats['success'] += 1
                result_queue.put(('success', extracted_url))
            else:
                print(f"    {COLOR_RED}[-] Код {response.status_code} за {request_time:.2f}s для: {target_url}{COLOR_RESET}")
                stats['failed'] += 1
                result_queue.put(('failed', extracted_url))

        except requests.exceptions.Timeout:
            error_msg = f"Timeout after {TIMEOUT}s"
            print(f"    {COLOR_RED}[ОШИБКА ТАЙМАУТ] {target_url} ({error_msg}){COLOR_RESET}")
            stats['failed'] += 1
            result_queue.put(('failed', extracted_url))

        except requests.exceptions.RequestException as e:
            error_msg = str(e)
            print(f"    {COLOR_RED}[ОШИБКА ЗАПРОСА] {target_url}: {error_msg}{COLOR_RESET}")
            stats['failed'] += 1
            result_queue.put(('failed', extracted_url))

        except Exception as e:
            error_msg = str(e)
            print(f"    {COLOR_RED}[НЕИЗВЕСТНАЯ ОШИБКА в worker] {target_url}: {error_msg}{COLOR_RESET}")
            stats['failed'] += 1
            result_queue.put(('failed', extracted_url))

        finally:
            url_queue.task_done()

    session.close()
    print(f"  {COLOR_BLUE}[WORKER] Поток завершает работу.{COLOR_RESET}")


def write_results_thread(result_queue, output_file_path, dont_worked_file_path, stop_event, stats):
    output_buffer = []
    dont_worked_buffer = []

    while not stop_event.is_set():
        try:
            res_type, url = result_queue.get(timeout=WRITE_BUFFER_TIMEOUT)
        except queue.Empty:
            if stop_event.is_set():
                break
            continue

        if res_type is None and url is None:
            break

        if res_type == 'success':
            output_buffer.append(url)
            # --- Информационное сообщение о добавлении в буфер для output ---
            print(f"      {COLOR_BLUE}[ЛОГ] URL '{url}' добавлен в буфер для записи в output.{COLOR_RESET}")
        elif res_type == 'failed':
            dont_worked_buffer.append(url)
            # --- Информационное сообщение о добавлении в буфер для output_dont_worked ---
            print(f"      {COLOR_BLUE}[ЛОГ] URL '{url}' добавлен в буфер для записи в output_dont_worked.{COLOR_RESET}")

        if len(output_buffer) >= WRITE_BUFFER_SIZE or len(dont_worked_buffer) >= WRITE_BUFFER_SIZE:
            flush_buffers(output_buffer, dont_worked_buffer, output_file_path, dont_worked_file_path, stats)

        result_queue.task_done()

    if output_buffer or dont_worked_buffer:
        flush_buffers(output_buffer, dont_worked_buffer, output_file_path, dont_worked_file_path, stats)

    print(f"  {COLOR_BLUE}[WRITER] Поток завершает работу.{COLOR_RESET}")


def flush_buffers(output_buf, dont_worked_buf, output_path, dont_worked_path, stats):
    if output_buf:
        try:
            with open(output_path, 'a', encoding='utf-8') as f_out:
                f_out.write('\n'.join(output_buf) + '\n')
            stats['written_success'] += len(output_buf)
            # --- Информационное сообщение о записи в output ---
            print(f"      {COLOR_GREEN}[ЗАПИСЬ] {len(output_buf)} URL записано в {output_path}.{COLOR_RESET}")
        except OSError as e:
            print(f"{COLOR_RED}[ОШИБКА ЗАПИСИ OUTPUT] {output_path}: {e}{COLOR_RESET}")
            stats['write_errors'] += len(output_buf)
        output_buf.clear()

    if dont_worked_buf:
        try:
            with open(dont_worked_path, 'a', encoding='utf-8') as f_dont_work:
                f_dont_work.write('\n'.join(dont_worked_buf) + '\n')
            stats['written_failed'] += len(dont_worked_buf)
            # --- Информационное сообщение о записи в output_dont_worked ---
            print(f"      {COLOR_GREEN}[ЗАПИСЬ] {len(dont_worked_buf)} URL записано в {dont_worked_path}.{COLOR_RESET}")
        except OSError as e:
            print(f"{COLOR_RED}[ОШИБКА ЗАПИСИ DONT_WORKED] {dont_worked_path}: {e}{COLOR_RESET}")
            stats['write_errors'] += len(dont_worked_buf)
        dont_worked_buf.clear()


def process_input_file(input_file_path, output_file_path, dont_worked_file_path):
    print(f"{COLOR_BLUE}[ИНФО] Начата обработка файла: {input_file_path}{COLOR_RESET}")

    MAX_QUEUE_SIZE = CHUNK_SIZE
    url_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
    result_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
    stop_event = Event()
    stats = {'success': 0, 'failed': 0, 'written_success': 0, 'written_failed': 0, 'write_errors': 0}

    writer_thread_instance = Thread(
        target=write_results_thread,
        args=(result_queue, output_file_path, dont_worked_file_path, stop_event, stats)
    )
    writer_thread_instance.start()

    workers = []
    for _ in range(THREADS):
        t = Thread(
            target=worker_thread,
            args=(url_queue, result_queue, stop_event, stats)
        )
        t.start()
        workers.append(t)

    processing_success = False
    try:
        with open(output_file_path, 'w', encoding='utf-8') as f_out:
            pass
        with open(dont_worked_file_path, 'w', encoding='utf-8') as f_dont_work:
            pass

        print(f"  {COLOR_BLUE}[ЛОГ] Файлы вывода созданы/очищены.{COLOR_RESET}")

        with open(input_file_path, 'r', encoding='utf-8') as f_in:
            for line_num, line in enumerate(f_in, start=1):
                line_content = line.strip()
                if not line_content:
                    continue

                print(f"  {COLOR_BLUE}[ЛОГ] Обработка строки {line_num} из {input_file_path}.{COLOR_RESET}")

                url_pattern = r'https://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)'
                for match in re.finditer(url_pattern, line_content):
                    ip, port = match.groups()
                    extracted_url = f"https://{ip}:{port}"
                    url_queue.put(extracted_url)

                    if url_queue.qsize() >= CHUNK_SIZE:
                        print(f"  {COLOR_CYAN}[ЧАНК] Очередь заполнена (размер >= {CHUNK_SIZE}). Начинаю обработку...{COLOR_RESET}")
                        time.sleep(CHUNK_DELAY)

    except FileNotFoundError:
        print(f"{COLOR_RED}[ОШИБКА] Файл {input_file_path} не найден.{COLOR_RESET}")
        processing_success = False
    except Exception as e:
        print(f"{COLOR_RED}[ФАТАЛЬНАЯ ОШИБКА] Критическая ошибка при обработке {input_file_path}: {e}{COLOR_RESET}")
        processing_success = False
    finally:
        for _ in workers:
            url_queue.put(None)

        for t in workers:
            t.join(timeout=10)

        print(f"  {COLOR_BLUE}[ЛОГ] Все потоки обработки завершены. Ожидание завершения записи...{COLOR_RESET}")

        result_queue.put((None, None))

        writer_thread_instance.join(timeout=10)

        print(f"  {COLOR_BLUE}[ЛОГ] Поток записи завершен.{COLOR_RESET}")

        if stats['write_errors'] > 0:
             print(f"{COLOR_RED}[ОШИБКА] Обнаружены ошибки записи ({stats['write_errors']} шт.). Входной файл НЕ будет удален.{COLOR_RESET}")
             processing_success = False
        else:
            print(f"{COLOR_BLUE}[ЛОГ] Обработка завершена успешно для {input_file_path}. Статистика: Успехи: {stats['success']}, Неуспехи: {stats['failed']}, Записано: {stats['written_success']} OK, {stats['written_failed']} Не_ОК.{COLOR_RESET}")
            processing_success = True

        stop_event.set()

    return processing_success


def main():
    print(f"{COLOR_CYAN}--- Старт сканирования каталога {INPUT_DIR} (многопоточный, чанки, очереди, удаление после обработки, фильтрация по портам) ---{COLOR_RESET}")

    if not os.path.isdir(INPUT_DIR):
        print(f"{COLOR_RED}[ОШИБКА] Каталог {INPUT_DIR} не найден.{COLOR_RESET}")
        return

    country_dirs = []
    for item in os.listdir(INPUT_DIR):
        item_path = os.path.join(INPUT_DIR, item)
        if os.path.isdir(item_path) and len(item) == 2:
            try:
                files_in_subdir = os.listdir(item_path)
            except OSError as e:
                print(f"  {COLOR_RED}[ОШИБКА] Не удалось прочитать подкаталог '{item}': {e}{COLOR_RESET}")
                continue

            found_matching_file = False
            for filename in files_in_subdir:
                 if filename.lower().endswith('.txt'):
                     match = re.match(r'^(\d+)-', filename)
                     if match:
                         port = int(match.group(1))
                         if port in PORTS_TO_PROCESS:
                             found_matching_file = True
                             break

            if found_matching_file:
                country_dirs.append(item)
                print(f"  {COLOR_BLUE}[ЛОГ] Подкаталог '{item}' добавлен.{COLOR_RESET}")
            else:
                print(f"  {COLOR_YELLOW}[ЛОГ] Подкаталог '{item}' пропущен.{COLOR_RESET}")

    country_dirs.sort()

    if not country_dirs:
        print(f"{COLOR_YELLOW}[ИНФО] В каталоге {INPUT_DIR} не найдено подкаталогов стран по портам.{COLOR_RESET}")
        return

    print(f"{COLOR_BLUE}[ИНФО] Найдено {len(country_dirs)} подкаталогов стран.{COLOR_RESET}")

    # --- Создаем список файлов для обработки ---
    input_file_paths = []
    for country_code in country_dirs:
        country_input_path = os.path.join(INPUT_DIR, country_code)
        if not os.path.exists(country_input_path):
            continue

        try:
            files_in_dir = os.listdir(country_input_path)
        except OSError as e:
            print(f"  {COLOR_RED}[ОШИБКА] Не удалось прочитать подкаталог '{country_code}': {e}{COLOR_RESET}")
            continue

        for filename in files_in_dir:
            if not filename.lower().endswith('.txt'):
                continue

            file_matches_port = False
            for port in PORTS_TO_PROCESS:
                if filename.startswith(f"{port}-"):
                    file_matches_port = True
                    break

            if file_matches_port:
                input_file_path = os.path.join(country_input_path, filename)
                input_file_paths.append(input_file_path)
                # print(f"  {COLOR_BLUE}[ЛОГ] Файл добавлен: {input_file_path}{COLOR_RESET}") # Может быть много, закомментировано

    if not input_file_paths:
        print(f"{COLOR_YELLOW}[ИНФО] Не найдено ни одного файла для обработки.{COLOR_RESET}")
        return

    total_files = len(input_file_paths)
    print(f"{COLOR_BLUE}[ИНФО] Найдено {total_files} файлов для обработки.{COLOR_RESET}")

    # --- Обработка аргументов командной строки ---
    selected_indices = []
    if len(sys.argv) > 1:
        arg = sys.argv[1].strip().lower()

        if arg == 'all':
            selected_indices = list(range(total_files))
            print(f"{COLOR_BLUE}[ИНФО] Выбраны все {total_files} файлов.{COLOR_RESET}")

        elif '-' in arg:
            try:
                range_parts = arg.split('-')
                if len(range_parts) != 2:
                    raise ValueError("Неверный формат диапазона.")
                start_num = int(range_parts[0])
                end_num = int(range_parts[1])
                if start_num < 1 or end_num > total_files or start_num > end_num:
                    raise ValueError("Неверный диапазон.")
                selected_indices = list(range(start_num - 1, end_num))
                print(f"{COLOR_BLUE}[ИНФО] Выбран диапазон: файлы {start_num} - {end_num} ({len(selected_indices)} шт.).{COLOR_RESET}")
            except ValueError as e:
                print(f"{COLOR_RED}[ОШИБКА] {e} Используйте формат 'N-M', где 1 <= N <= M <= {total_files}.{COLOR_RESET}")
                return

        else: # Одиночное число
            try:
                single_num = int(arg)
                if single_num < 1 or single_num > total_files:
                    raise ValueError(f"Номер файла {single_num} вне диапазона. Должен быть от 1 до {total_files}.")
                selected_indices = [single_num - 1]
                print(f"{COLOR_BLUE}[ИНФО] Выбран файл № {single_num} ('{os.path.basename(input_file_paths[selected_indices[0]])}').{COLOR_RESET}")
            except ValueError as e:
                print(f"{COLOR_RED}[ОШИБКА] {e}{COLOR_RESET}")
                return
    else: # Интерактивный режим
        print(f"{COLOR_BLUE}Список файлов для обработки:{COLOR_RESET}")
        for i, file_path in enumerate(input_file_paths, start=1):
            print(f"  {i}. {file_path}")

        while True:
            try:
                user_input = input(f"{COLOR_BLUE}Введите номер файла или диапазон (например, '5' или '10-20'), или 'all': {COLOR_RESET}").strip()
                if not user_input:
                    continue

                if user_input.lower() == 'all':
                    selected_indices = list(range(total_files))
                    break

                if '-' in user_input:
                    range_parts = user_input.split('-')
                    if len(range_parts) != 2:
                        print(f"{COLOR_RED}[!] Неверный формат диапазона.{COLOR_RESET}")
                        continue
                    start_num = int(range_parts[0])
                    end_num = int(range_parts[1])
                    if start_num < 1 or end_num > total_files or start_num > end_num:
                        print(f"{COLOR_RED}[!] Неверный диапазон. Убедитесь, что 1 <= N <= M <= {total_files}.{COLOR_RESET}")
                        continue
                    selected_indices = list(range(start_num - 1, end_num))
                    break
                else:
                    single_num = int(user_input)
                    if single_num < 1 or single_num > total_files:
                        print(f"{COLOR_RED}[!] Неверный номер. Убедитесь, что 1 <= N <= {total_files}.{COLOR_RESET}")
                        continue
                    selected_indices = [single_num - 1]
                    break
            except ValueError:
                print(f"{COLOR_RED}[!] Пожалуйста, введите корректное число или диапазон.{COLOR_RESET}")
            except KeyboardInterrupt:
                print(f"\n{COLOR_YELLOW}--- Работа скрипта прервана ---{COLOR_RESET}")
                return

    # --- Обработка выбранных файлов ---
    print(f"{COLOR_BLUE}[ЛОГ] Начинается обработка {len(selected_indices)} выбранных файлов.{COLOR_RESET}")
    for idx in selected_indices:
        if idx < 0 or idx >= total_files:
             print(f"{COLOR_RED}[ОШИБКА] Индекс {idx} вне диапазона списка файлов (0 - {total_files-1}).{COLOR_RESET}")
             continue

        input_file_path = input_file_paths[idx]
        print(f"{COLOR_BLUE}[ЛОГ] Обработка индекса {idx}, файл: {input_file_path}{COLOR_RESET}")

        path_parts = Path(input_file_path).parts
        if len(path_parts) < 3 or path_parts[-3] != 'input':
            print(f"{COLOR_YELLOW}[ПРЕДУПРЕЖДЕНИЕ] Неверный путь: {input_file_path}{COLOR_RESET}")
            continue

        country_code = path_parts[-2]
        filename = os.path.basename(input_file_path)
        output_file_path = os.path.join(OUTPUT_DIR, country_code, filename)
        dont_worked_file_path = os.path.join(OUTPUT_DONT_WORKED_DIR, country_code, filename)

        os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
        os.makedirs(os.path.dirname(dont_worked_file_path), exist_ok=True)

        try:
            success = process_input_file(input_file_path, output_file_path, dont_worked_file_path)
        except Exception as e:
             print(f"{COLOR_RED}[ФАТАЛЬНАЯ ОШИБКА ВНУТРИ process_input_file] {input_file_path}: {e}{COLOR_RESET}")
             success = False

        if success:
            try:
                os.remove(input_file_path)
                # --- Информационное сообщение об удалении файла из input ---
                print(f"{COLOR_GREEN}[+] Файл успешно обработан и УДАЛЁН из input: {input_file_path}{COLOR_RESET}")
            except OSError as e:
                print(f"{COLOR_RED}[-] Ошибка при удалении файла {input_file_path}: {e}{COLOR_RESET}")
        else:
            print(f"{COLOR_YELLOW}[-] Файл НЕ был удалён: {input_file_path}{COLOR_RESET}")

    print(f"\n{COLOR_CYAN}--- Сканирование завершено ---{COLOR_RESET}")


if __name__ == "__main__":
    main()



Что у нас тут

Общий поток выполнения (Main Flow)​

[main()] 
   │
   ▼
[1] Сканирование ./input → авто-детект портов из имён файлов (строки 45-60)
   │
   ▼
[2] Фильтрация подкаталогов стран и сбор списка файлов (строки 240-290)
   │
   ▼
[3] Интерактивный/CLI выбор файлов для обработки (строки 295-345)
   │
   ▼
[4] Для каждого выбранного файла → process_input_file() (строки 350-380)
   │
   ▼
[5] process_input_file():
   │   ├─▶ Создание очередей: url_queue, result_queue (строки 150-155)
   │   ├─▶ Запуск writer-потока (строки 157-162)
   │   ├─▶ Запуск N worker-потоков (строки 164-172)
   │   ├─▶ Чтение файла → парсинг URL → put в url_queue (строки 185-210)
   │   ├─▶ Отправка "poison pill" (None) для остановки воркеров (строки 215-218)
   │   ├─▶ Ожидание завершения всех потоков (строки 220-228)
   │   └─▶ Проверка статистики → удаление исходного файла при успехе (строки 230-245)
   │
   ▼
[6] worker_thread():
   │   ├─▶ Получение URL из url_queue (строки 75-82)
   │   ├─▶ Random delay (строка 84)
   │   ├─▶ GET-запрос с retry-логикой (строки 86-100)
   │   ├─▶ Классификация ответа → put в result_queue (строки 101-125)
   │   └─▶ task_done() (строка 128)
   │
   ▼
[7] write_results_thread():
   │   ├─▶ Получение результата из result_queue (строки 135-145)
   │   ├─▶ Буферизация в output_buffer / dont_worked_buffer (строки 147-155)
   │   ├─▶ При заполнении буфера → flush_buffers() (строки 157-160)
   │   └─▶ Финальная запись при остановке (строки 162-165)


КомпонентФункцияСсылка на кодurl_queueОчередь URL для обработки (Producer → Consumer)строки 150, 75-82result_queueОчередь результатов (Worker → Writer)строки 151, 101-125stop_eventФлаг корректной остановки потоковстроки 152, 73, 133worker_thread()Извлечение URL, HTTP-запрос, классификациястроки 65-129write_results_thread()Буферизация и асинхронная запись на дискстроки 131-168flush_buffers()Пакетная запись буферов в файлыстроки 170-195process_input_file()Оркестрация: чтение → парсинг → распределение → сборстроки 147-247


 Механизм остановки (Graceful Shutdown)​


# 1. Сигнал "стоп" для воркеров через poison pill
for _ in workers:
    url_queue.put(None)  # строка 216

# 2. Ожидание завершения воркеров с таймаутом
for t in workers:
    t.join(timeout=10)   # строка 219

# 3. Сигнал "стоп" для writer-потока
result_queue.put((None, None))  # строка 223

# 4. Ожидание writer
writer_thread_instance.join(timeout=10)  # строка 226

# 5. Установка флага stop_event (дополнительная страховка)
stop_event.set()  # строка 245

​


 Построчный разбор ключевых блоков кода​


 Блок 1: Авто-детект портов (строки 45-60)​

45  discovered_ports = set()
46  for item in os.listdir(INPUT_DIR):
47      item_path = os.path.join(INPUT_DIR, item)
48      if os.path.isdir(item_path) and len(item) == 2:  # фильтр по коду страны
49          try:
50              files_in_subdir = os.listdir(item_path)
51          except OSError as e:
52              print(f"  {COLOR_RED}[ОШИБКА] ... {e}{COLOR_RESET}")
53              continue
54          for filename in files_in_subdir:
55              if filename.lower().endswith('.txt'):
56                  match = re.match(r'^(\d+)-', filename)  # парсинг "443-..."
57                  if match:
58                      port = int(match.group(1))
59                      discovered_ports.add(port)
60  PORTS_TO_PROCESS = sorted(list(discovered_ports))
Умный парсинг имён файлов вида 443-germany.txt позволяет автоматически определять, какие порты сканировать, без правки конфига. Использование set() гарантирует уникальность портов.


Блок 2: Настройка retry-логики (строки 62-68)​

62  from requests.adapters import HTTPAdapter
63  from urllib3.util.retry import Retry
64  
65  def worker_thread(url_queue, result_queue, stop_event, stats):
66      session = requests.Session()
67      retry_strategy = Retry(
68          total=3,                      # макс. 3 попытки
69          backoff_factor=0.3,           # задержка: 0.3 → 0.9 → 2.7 сек
70          status_forcelist=[429,500,502,503,504],  # коды для ретрая
71      )
72      adapter = HTTPAdapter(max_retries=retry_strategy)
73      session.mount("http://", adapter)
74      session.mount("https://", adapter)
Retry из urllib3 автоматически повторяет запросы при временных сбоях. backoff_factor=0.3 реализует экспоненциальную задержку, что снижает нагрузку на целевые сервера.


 Блок 3: Worker-поток — получение и обработка URL (строки 75-128)​

75  while not stop_event.is_set():
76      try:
77          extracted_url = url_queue.get(timeout=1)  # блокирующее получение
78      except queue.Empty:
79          continue
80  
81      if extracted_url is None:  # poison pill → выход из цикла
82          url_queue.task_done()
83          break
84  
85      target_url = extracted_url + FORTINET_PATH
86      time.sleep(random.uniform(0.05, 0.15))  # REQUEST_DELAY_MEAN ± VARIANCE
87  
88      start_time = time.time()
89      try:
90          response = session.get(target_url, headers={'User-Agent': USER_AGENT},
91                               verify=False, timeout=TIMEOUT)
92          request_time = time.time() - start_time
93  
94          if response.status_code == 200:
95              print(f"    {COLOR_GREEN}[+] 200 OK ...")
96              stats['success'] += 1
97              result_queue.put(('success', extracted_url))
98          else:
99              print(f"    {COLOR_RED}[-] Код {response.status_code} ...")
100             stats['failed'] += 1
101             result_queue.put(('failed', extracted_url))
102     except requests.exceptions.Timeout:
103         ...  # логирование + stats['failed'] += 1 + put в result_queue
104     except requests.exceptions.RequestException as e:
105         ...  # аналогично
106     except Exception as e:
107         ...  # catch-all для неизвестных ошибок
108     finally:
109         url_queue.task_done()  # обязательный вызов для queue.join()
110  
111  session.close()  # освобождение ресурсов
  • queue.get(timeout=1) предотвращает вечную блокировку потока.
  • random.uniform() добавляет джиттер для обхода WAF.
  • Все исключения перехватываются → статистика не теряется.
  • task_done() критичен для корректной работы queue.join().



Блок 4: Writer-поток — буферизация записи (строки 131-168)​

131 def write_results_thread(result_queue, output_file_path, dont_worked_file_path, stop_event, stats):
132     output_buffer = []
133     dont_worked_buffer = []
134  
135     while not stop_event.is_set():
136         try:
137             res_type, url = result_queue.get(timeout=WRITE_BUFFER_TIMEOUT)  # 6 сек
138         except queue.Empty:
139             if stop_event.is_set():
140                 break
141             continue
142  
143         if res_type is None and url is None:  # сигнал остановки
144             break
145  
146         if res_type == 'success':
147             output_buffer.append(url)
148         elif res_type == 'failed':
149             dont_worked_buffer.append(url)
150  
151         # Триггер записи: буфер заполнен ИЛИ таймаут
152         if len(output_buffer) >= WRITE_BUFFER_SIZE or len(dont_worked_buffer) >= WRITE_BUFFER_SIZE:
153             flush_buffers(output_buffer, dont_worked_buffer, output_file_path, dont_worked_file_path, stats)
154  
155         result_queue.task_done()
156  
157     # Финальная запись оставшихся данных
158     if output_buffer or dont_worked_buffer:
159         flush_buffers(output_buffer, dont_worked_buffer, output_file_path, dont_worked_file_path, stats)
Двойной триггер записи (по размеру буфера или по таймауту) гарантирует, что данные не "зависнут" в памяти при низкой нагрузке. task_done() позволяет отслеживать завершение обработки очереди.

Блок 5: flush_buffers — атомарная запись на диск (строки 170-195)​

170 def flush_buffers(output_buf, dont_worked_buf, output_path, dont_worked_path, stats):
171     if output_buf:
172         try:
173             with open(output_path, 'a', encoding='utf-8') as f_out:
174                 f_out.write('\n'.join(output_buf) + '\n')  # одна операция write()
175             stats['written_success'] += len(output_buf)
176         except OSError as e:
177             print(f"{COLOR_RED}[ОШИБКА ЗАПИСИ OUTPUT] ...")
178             stats['write_errors'] += len(output_buf)
179         output_buf.clear()  # очистка буфера после успешной/неуспешной записи
180  
181     if dont_worked_buf:
182         try:
183             with open(dont_worked_path, 'a', encoding='utf-8') as f_dont_work:
184                 f_dont_work.write('\n'.join(dont_worked_buf) + '\n')
185             stats['written_failed'] += len(dont_worked_buf)
186         except OSError as e:
187             print(f"{COLOR_RED}[ОШИБКА ЗАПИСИ DONT_WORKED] ...")
188             stats['write_errors'] += len(dont_worked_buf)
189         dont_worked_buf.clear()
  • Запись пачкой ('\n'.join(...)) снижает количество системных вызовов.
  • Очистка буфера (clear()) выполняется в любом случае — это предотвращает дублирование при повторной записи.
  • Статистика write_errors позволяет отследить проблемы с диском.


Блок 6: process_input_file — оркестрация (строки 147-247)​

147 def process_input_file(input_file_path, output_file_path, dont_worked_file_path):
148     # Инициализация очередей и статистики
149     url_queue = queue.Queue(maxsize=CHUNK_SIZE)  # 1000
150     result_queue = queue.Queue(maxsize=CHUNK_SIZE)
151     stop_event = Event()
152     stats = {'success': 0, 'failed': 0, 'written_success': 0, 'written_failed': 0, 'write_errors': 0}
153  
154     # Запуск потоков
155     writer_thread_instance = Thread(target=write_results_thread, args=(...))
156     writer_thread_instance.start()
157     workers = [Thread(target=worker_thread, args=(...)) for _ in range(THREADS)]  # 200 потоков
158     for t in workers: t.start()
159  
160     # Чтение и парсинг входного файла
161     with open(input_file_path, 'r', encoding='utf-8') as f_in:
162         for line_num, line in enumerate(f_in, start=1):
163             line_content = line.strip()
164             if not line_content: continue
165             url_pattern = r'https://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)'
166             for match in re.finditer(url_pattern, line_content):
167                 ip, port = match.groups()
168                 extracted_url = f"https://{ip}:{port}"
169                 url_queue.put(extracted_url)  # блокирующее добавление
170                 if url_queue.qsize() >= CHUNK_SIZE:
171                     time.sleep(CHUNK_DELAY)  # пауза 1 сек между чанками
172  
173     # Корректная остановка потоков
174     for _ in workers: url_queue.put(None)  # poison pill
175     for t in workers: t.join(timeout=10)
176     result_queue.put((None, None))  # сигнал writer'у
177     writer_thread_instance.join(timeout=10)
178  
179     # Проверка результатов и удаление исходного файла
180     if stats['write_errors'] > 0:
181         processing_success = False  # файл НЕ удаляется
182     else:
183         processing_success = True
184         os.remove(input_file_path)  # только при успехе
185  
186     stop_event.set()
187     return processing_success
  • url_queue.put() блокируется, если очередь полна → естественный бэкпрессинг.
  • CHUNK_DELAY предотвращает "шторм" запросов при переходе между чанками.
  • Удаление файла только при отсутствии ошибок записи — защита от потери данных.


Блок 7: main() — выбор файлов и запуск (строки 249-380)​

249 # CLI-аргументы: all / N / N-M
250 if len(sys.argv) > 1:
251     arg = sys.argv[1].strip().lower()
252     if arg == 'all':
253         selected_indices = list(range(total_files))
254     elif '-' in arg:  # диапазон "10-20"
255         start_num, end_num = map(int, arg.split('-'))
256         selected_indices = list(range(start_num-1, end_num))
257     else:  # одиночный номер
258         selected_indices = [int(arg)-1]
259 else:
260     # Интерактивный режим с валидацией ввода
261     while True:
262         user_input = input("Введите номер файла или диапазон...")
263         # ... парсинг и валидация ...
264         if valid: break
265  
266 # Обработка выбранных файлов
267 for idx in selected_indices:
268     input_file_path = input_file_paths[idx]
269     # ... формирование путей output ...
270     success = process_input_file(...)
271     if success:
272         os.remove(input_file_path)  # удаление при успехе
Гибкая система выбора файлов позволяет использовать скрипт как в интерактивном режиме (отладка), так и в автоматизированных пайплайнах (через аргументы).

  


                            
 
АспектПочему это хорошоАрхитектура Producer-ConsumerЧёткое разделение ответственности: генерация → обработка →  запись. Легко масштабировать и тестировать по частям.Retry-логикаАвтоматическое восстановление при временных сбоях сети/сервера  снижает ложные негативы.Буферизация записиСнижение I/O-нагрузки на диск, повышение производительности  при больших объёмах данных.Авто-детект портовНе требуется править конфиг при добавлении новых портов —  скрипт адаптивен.Детальная статистикаПозволяет точно диагностировать, где происходят потери: сеть,  обработка или диск.Защита от потери данныхФайл удаляется только при успешной записи всех  результатов.


В качестве пруфа о том, что это работает предоставляю форумчанам маленькую часть выгрузки, которая была получена путем отработки данного кода.

Итак, идем к завершению
Что вам может дать этот код?

  1. Детализация детектов систем на больших диапазонах
  2. Понимание подводных камней которые вы можете встретить на своем пути
  3. Вишенка на торте - (Самое пиздатое) - поменяйте искомый URL и у вас полноценный сканер 
  4. Есть еще более пиздатая версия сканера, с которым сервер отлетел за 3 дня, кнопка бабло.


С вами Был майк, изучайте информационную безопасность, линукс и компьютерные сети.
Люблю, целую, Пух!
​
👍7 😁1 😢1
Ответmike_vazovski

Привет братва, на связи майк Давно меня тут не было, накопился интересный опыт которым я хотел бы поделиться. Сегодня пойдет речь про самый древний способ идентификации живых целей для последующей отработки. К сожалению…

это вайбкодинг. Зачем подавать на оплату?)
👍1
Ответmike_vazovski

Могу сказать честно - полная хуйня - работает медленно, зависает и работает нестабильно.

смысла нет постить такое.
Если бы ты написал 0 мол, вод код, цели такие-то как улучшить было бы и честно, и полезно.
а но да, бесплатно.
👍3
Код не работает, но спасибо
👍1
Ответlisa99

смысла нет постить такое. Если бы ты написал 0 мол, вод код, цели такие-то как улучшить было бы и честно, и полезно. а но да, бесплатно.

К критике отношусь спокойно, но я написал особенности,  где и с чем столкнулся и как улучшил. Крайнюю редакцию турбо сканера не стал постить, материал предоставил. Пошли в лс и сама все глянешь? (я за диалог
)
👍3
Ответdfecrf392

Код не работает, но спасибо

нужно правильно загрузить входную структуру каталога чтобы он в пул на обработку взял
👍2
Ответmike_vazovski

Крайнюю редакцию турбо сканера не стал постить, материал предоставил. Пошли в лс и сама все глянешь? (я за диалог )

почему не стал выкладывать софт с лучшей, оптимизированной версией - не вопрос

да я понимаю общий посыл статьи -  понабирать через авто-кодинг от ИИ-шки и маскана сырые базы, потом рекурсивно их обходить,
наверное просмотривая детальней баннеры+
дедупликация и фильтрация неинтересных целей на тех же портах 443+8443 что и Форты.

Такой пайплайн абсолютно под силу локальному  qwen 2.5.
Другое дело - ценность такой бд - о чем тебе какой-то профи уже писал в другом месте)
👍1
Ответlisa99

почему не стал выкладывать софт с лучшей, оптимизированной версией - не вопрос да я понимаю общий посыл статьи - понабирать через авто-кодинг от ИИ-шки и маскана сырые базы, потом рекурсивно их обходить, наверное…

Пайплайн подобного рода без фиксов(вишенка на торт от автора) - в целом штука заезжанная и уже пущенная в обиход всеми, кому только не лень. Что называется, вот вашш-сс мануальчик Бастера, сударь. 
Это если вкатываться в тему конкретно и с нуля, имея только реги на нескольких форумах и желание учиться. Тогда увидишь это по кучам однообразных мануалов, где будут фигурировать шодан, зумай и все вариации приготовления такого блюда, как масскан.

Если же ты пент(уже), то вся эта суть со сканами по цыдрам, перегонкой, граббингом баннеров и определением потенциального меда для тебя и так переходит в категорию ясно-понятных практически с самого начала. Прописные истины в формате "можно разведать пассивно через крупных серых агрегаторов вроде выгрузок шодана и зумая" и "можно разведать активно, прикинув диапазоны и пробежавшись по ним со своей впски с помощью масскана и софта, его находки валидирующего" подаваться на оплату не могут изначально.

Если бы это был формат подачи, где исправлялись бы основные лажа-моменты(вроде статей, которые на эту тему уже были на других площадках) с тем же массканом и узкими местами в среднестатистическом пайплайне, или учили бы пользоваться более эффективными инструментами/юзать то, что уже есть, более эффективно, - не вопрос, что называется.
👍2

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.