"""
Программа для проверки открытых портов.

Как работает:
1. Python запускает локальный веб-сервер.
2. В браузере открывается HTML-страница с формой.
3. Пользователь выбирает хост, диапазон портов, таймаут и количество потоков.
4. Python пробует подключиться к каждому порту через TCP.
5. На странице показываются только открытые порты и примерное имя сервиса.

Важно: проверяй только свои устройства, локальный компьютер или сеть,
для которой у тебя есть разрешение.
"""

from concurrent.futures import ThreadPoolExecutor, as_completed
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from socket import AF_INET, SOCK_STREAM, socket
from urllib.parse import parse_qs, urlparse
import html
import json
import socket as socket_module
import webbrowser


DEFAULT_HOST = "127.0.0.1"
DEFAULT_START_PORT = 1
DEFAULT_END_PORT = 65535
DEFAULT_TIMEOUT = 0.25
DEFAULT_WORKERS = 300


PAGE = """<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Проверка портов</title>
  <style>
    :root {
      color-scheme: light;
      font-family: Arial, sans-serif;
      background: #eef2f5;
      color: #1d2730;
    }

    body {
      margin: 0;
      min-height: 100vh;
    }

    main {
      width: min(980px, calc(100% - 32px));
      margin: 0 auto;
      padding: 32px 0;
    }

    h1 {
      margin: 0 0 18px;
      font-size: clamp(28px, 5vw, 44px);
      line-height: 1.05;
    }

    .panel {
      background: #ffffff;
      border: 1px solid #d8e0e6;
      border-radius: 8px;
      padding: 18px;
      box-shadow: 0 12px 30px rgba(20, 40, 60, 0.08);
    }

    .explanation {
      margin-bottom: 18px;
    }

    .explanation h2 {
      margin: 0 0 10px;
      font-size: 20px;
      line-height: 1.2;
    }

    .explanation p {
      margin: 0 0 10px;
      color: #485761;
      line-height: 1.5;
    }

    .explanation ol {
      margin: 0;
      padding-left: 21px;
      color: #485761;
      line-height: 1.55;
    }

    form {
      display: grid;
      grid-template-columns: 1.4fr repeat(4, minmax(92px, 1fr)) auto;
      gap: 12px;
      align-items: end;
    }

    label {
      display: grid;
      gap: 6px;
      font-size: 13px;
      color: #51606b;
      font-weight: 700;
    }

    input {
      width: 100%;
      box-sizing: border-box;
      border: 1px solid #bfccd6;
      border-radius: 6px;
      padding: 10px 11px;
      font: inherit;
      color: #16212a;
      background: #fbfdff;
    }

    input:focus {
      outline: 3px solid #b8ddff;
      border-color: #3985c6;
    }

    button {
      border: 0;
      border-radius: 6px;
      padding: 11px 18px;
      font: inherit;
      font-weight: 700;
      color: #ffffff;
      background: #146c94;
      cursor: pointer;
      min-height: 42px;
      white-space: nowrap;
    }

    button:hover {
      background: #0d587b;
    }

    button:disabled {
      cursor: wait;
      opacity: 0.7;
    }

    .status {
      margin-top: 16px;
      display: flex;
      gap: 12px;
      flex-wrap: wrap;
      align-items: center;
    }

    .badge {
      display: inline-flex;
      align-items: center;
      min-height: 28px;
      padding: 4px 9px;
      border-radius: 999px;
      background: #e6eef5;
      color: #26343f;
      font-weight: 700;
      font-size: 13px;
    }

    progress {
      width: 100%;
      height: 16px;
      margin-top: 16px;
      accent-color: #146c94;
    }

    .results {
      margin-top: 18px;
      overflow-x: auto;
    }

    table {
      width: 100%;
      border-collapse: collapse;
      background: #ffffff;
      border: 1px solid #d8e0e6;
      border-radius: 8px;
      overflow: hidden;
    }

    th, td {
      text-align: left;
      padding: 11px 12px;
      border-bottom: 1px solid #e5ebf0;
    }

    th {
      background: #f5f8fa;
      color: #44525d;
      font-size: 13px;
    }

    tr:last-child td {
      border-bottom: 0;
    }

    .open {
      color: #0d7a44;
      font-weight: 700;
    }

    .empty {
      padding: 18px;
      background: #ffffff;
      border: 1px solid #d8e0e6;
      border-radius: 8px;
      color: #52616d;
    }

    @media (max-width: 820px) {
      form {
        grid-template-columns: 1fr 1fr;
      }

      label:first-child,
      button {
        grid-column: 1 / -1;
      }
    }
  </style>
</head>
<body>
  <main>
    <h1>Проверка портов</h1>
    <section class="panel explanation">
      <h2>Пояснение</h2>
      <p>Эта программа запускает локальную веб-страницу и проверяет, какие TCP-порты открыты на выбранном хосте.</p>
      <ol>
        <li>Вводится адрес хоста, например 127.0.0.1.</li>
        <li>Задается диапазон портов от 1 до 65535.</li>
        <li>Python пробует подключиться к каждому порту.</li>
        <li>Если подключение получилось, порт считается открытым и появляется в таблице.</li>
      </ol>
    </section>
    <section class="panel">
      <form id="scan-form">
        <label>
          Хост
          <input id="host" name="host" value="127.0.0.1" autocomplete="off">
        </label>
        <label>
          От
          <input id="start" name="start" type="number" min="1" max="65535" value="1">
        </label>
        <label>
          До
          <input id="end" name="end" type="number" min="1" max="65535" value="65535">
        </label>
        <label>
          Таймаут
          <input id="timeout" name="timeout" type="number" min="0.05" max="5" step="0.05" value="0.25">
        </label>
        <label>
          Потоки
          <input id="workers" name="workers" type="number" min="1" max="1000" value="300">
        </label>
        <button id="scan-button" type="submit">Проверить</button>
      </form>
      <div class="status">
        <span class="badge" id="state">Готово</span>
        <span class="badge" id="count">Открытых портов: 0</span>
        <span class="badge" id="time">Время: 0 c</span>
      </div>
      <progress id="progress" value="0" max="100" hidden></progress>
    </section>
    <section class="results" id="results">
      <div class="empty">Запусти проверку, и здесь появятся открытые порты.</div>
    </section>
  </main>

  <script>
    const form = document.querySelector("#scan-form");
    const button = document.querySelector("#scan-button");
    const state = document.querySelector("#state");
    const count = document.querySelector("#count");
    const time = document.querySelector("#time");
    const progress = document.querySelector("#progress");
    const results = document.querySelector("#results");

    function renderPorts(data) {
      count.textContent = `Открытых портов: ${data.open_ports.length}`;
      time.textContent = `Время: ${data.elapsed_seconds} c`;

      if (!data.open_ports.length) {
        results.innerHTML = '<div class="empty">Открытые порты не найдены.</div>';
        return;
      }

      const rows = data.open_ports.map(item => `
        <tr>
          <td>${item.port}</td>
          <td class="open">Открыт</td>
          <td>${item.service || "неизвестно"}</td>
        </tr>
      `).join("");

      results.innerHTML = `
        <table>
          <thead>
            <tr>
              <th>Порт</th>
              <th>Статус</th>
              <th>Сервис</th>
            </tr>
          </thead>
          <tbody>${rows}</tbody>
        </table>
      `;
    }

    form.addEventListener("submit", async event => {
      event.preventDefault();
      const params = new URLSearchParams(new FormData(form));
      button.disabled = true;
      state.textContent = "Проверяю...";
      progress.hidden = false;
      progress.removeAttribute("value");
      results.innerHTML = '<div class="empty">Идет проверка портов. Полный диапазон может занять время.</div>';

      try {
        const response = await fetch(`/scan?${params.toString()}`);
        const data = await response.json();

        if (!response.ok || data.error) {
          throw new Error(data.error || "Ошибка проверки");
        }

        state.textContent = `Проверено: ${data.host}:${data.start_port}-${data.end_port}`;
        renderPorts(data);
      } catch (error) {
        state.textContent = "Ошибка";
        results.innerHTML = `<div class="empty">${error.message}</div>`;
      } finally {
        button.disabled = false;
        progress.hidden = true;
        progress.value = 0;
      }
    });
  </script>
</body>
</html>
"""


def clamp_int(value, default, minimum, maximum):
    try:
        number = int(value)
    except (TypeError, ValueError):
        return default
    return max(minimum, min(maximum, number))


def clamp_float(value, default, minimum, maximum):
    try:
        number = float(value)
    except (TypeError, ValueError):
        return default
    return max(minimum, min(maximum, number))


def service_name(port):
    try:
        return socket_module.getservbyport(port)
    except OSError:
        return ""


def check_port(host, port, timeout):
    with socket(AF_INET, SOCK_STREAM) as sock:
        sock.settimeout(timeout)
        result = sock.connect_ex((host, port))
        if result == 0:
            return {"port": port, "service": service_name(port)}
    return None


def scan_ports(host, start_port, end_port, timeout, workers):
    open_ports = []
    ports = range(start_port, end_port + 1)

    with ThreadPoolExecutor(max_workers=workers) as executor:
        futures = [executor.submit(check_port, host, port, timeout) for port in ports]
        for future in as_completed(futures):
            result = future.result()
            if result:
                open_ports.append(result)

    return sorted(open_ports, key=lambda item: item["port"])


class PortScannerHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed_url = urlparse(self.path)

        if parsed_url.path == "/":
            self.send_html(PAGE)
            return

        if parsed_url.path == "/scan":
            self.handle_scan(parsed_url.query)
            return

        self.send_error(404, "Страница не найдена")

    def handle_scan(self, query):
        params = parse_qs(query)
        host = params.get("host", [DEFAULT_HOST])[0].strip() or DEFAULT_HOST
        host = html.escape(host, quote=True)
        start_port = clamp_int(params.get("start", [DEFAULT_START_PORT])[0], DEFAULT_START_PORT, 1, 65535)
        end_port = clamp_int(params.get("end", [DEFAULT_END_PORT])[0], DEFAULT_END_PORT, 1, 65535)
        timeout = clamp_float(params.get("timeout", [DEFAULT_TIMEOUT])[0], DEFAULT_TIMEOUT, 0.05, 5.0)
        workers = clamp_int(params.get("workers", [DEFAULT_WORKERS])[0], DEFAULT_WORKERS, 1, 1000)

        if start_port > end_port:
            start_port, end_port = end_port, start_port

        try:
            socket_module.gethostbyname(host)
        except OSError:
            self.send_json({"error": "Не удалось найти такой хост."}, status=400)
            return

        from time import perf_counter

        started = perf_counter()
        try:
            open_ports = scan_ports(host, start_port, end_port, timeout, workers)
        except OSError as error:
            self.send_json({"error": str(error)}, status=400)
            return

        elapsed = round(perf_counter() - started, 2)
        self.send_json(
            {
                "host": host,
                "start_port": start_port,
                "end_port": end_port,
                "timeout": timeout,
                "workers": workers,
                "elapsed_seconds": elapsed,
                "open_ports": open_ports,
            }
        )

    def send_html(self, content):
        data = content.encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def send_json(self, content, status=200):
        data = json.dumps(content, ensure_ascii=False).encode("utf-8")
        self.send_response(status)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)

    def log_message(self, format, *args):
        return


def find_free_port(start=8000, end=8100):
    for port in range(start, end + 1):
        try:
            server = ThreadingHTTPServer(("127.0.0.1", port), PortScannerHandler)
            return server, port
        except OSError:
            continue
    raise OSError("Не удалось найти свободный порт для веб-страницы.")


if __name__ == "__main__":
    httpd, port = find_free_port()
    url = f"http://127.0.0.1:{port}"
    print(f"Открываю страницу: {url}")
    print("Закрыть сервер: Ctrl+C")
    webbrowser.open(url)
    httpd.serve_forever()
