commit 61eadfcb0da5a8157e38994c3183e87fb2865df2 Author: Konstantin Date: Mon May 25 16:45:01 2026 +0300 first commit diff --git a/.README.md.swp b/.README.md.swp new file mode 100644 index 0000000..76ec65c Binary files /dev/null and b/.README.md.swp differ diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..4543785 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,23 @@ +kind: pipeline +type: docker +name: build-and-push + +steps: + - name: build-image + image: plugins/docker + settings: + registry: git.sedletskiy.ru + repo: git.sedletskiy.ru/kosechok/testmintools + tags: + - latest + - ${DRONE_COMMIT_SHA:0:8} + username: + from_secret: REGISTRY_USER + password: + from_secret: REGISTRY_PASSWORD + +trigger: + branch: + - master + event: + - push \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6be9526 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.10-slim + +RUN python -m pip install --upgrade pip +RUN pip install Flask + +# Установка curl, nano, ping и traceroute +RUN apt-get update && apt-get install -y curl nano iputils-ping traceroute +RUN mkdir /app +COPY ./network_tools.py /app +WORKDIR /app + +CMD ["python", "network_tools.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..08e62b5 --- /dev/null +++ b/README.md @@ -0,0 +1,222 @@ +# 🛠️ testmontools + +Лёгкий HTTP-сервис для сетевой диагностики. Оборачивает `ping`, `traceroute` и `curl` в REST API — удобно для мониторинга, дебага и автоматизации. + +--- + +## 📋 Содержание + +- [Требования](#требования) +- [Запуск](#запуск) +- [API Reference](#api-reference) + - [POST /testmontools/ping](#post-testmontoolsping) + - [POST /testmontools/traceroute](#post-testmontoolstraceroute) + - [POST /testmontools/curl](#post-testmontoolscurl) + - [POST /testmontools/all](#post-testmontoolsall) +- [Формат входных данных](#формат-входных-данных) +- [Примеры](#примеры) + +--- + +## Требования + +- Python 3.8+ +- Flask +- Утилиты `ping`, `traceroute`, `curl` в системе + +```bash +pip install flask +``` + +--- + +## Запуск + +```bash +python app.py +``` + +Сервер стартует на `http://0.0.0.0:7088`. + +--- + +## API Reference + +Все эндпоинты принимают `Content-Type: application/json` и возвращают JSON. + +### Формат входных данных + +В теле каждого запроса передаётся одно из полей: + +| Поле | Тип | Описание | +|--------|--------|--------------------------------------------| +| `url` | string | Полный URL (`https://example.com/path`) | +| `host` | string | Хост или IP (`example.com`, `8.8.8.8`) | + +Если передан URL, хост извлекается автоматически там, где нужен именно он (ping, traceroute). + +--- + +### POST /testmontools/ping + +Запускает `ping -c 4` до указанного хоста. + +**Запрос** +```json +{ "host": "example.com" } +``` + +**Ответ** +```json +{ + "input": "example.com", + "resolved_host": "example.com", + "stdout": "PING example.com (93.184.216.34) ...", + "stderr": "", + "returncode": 0 +} +``` + +--- + +### POST /testmontools/traceroute + +Запускает `traceroute` до указанного хоста. + +**Запрос** +```json +{ "host": "8.8.8.8" } +``` + +**Ответ** +```json +{ + "input": "8.8.8.8", + "resolved_host": "8.8.8.8", + "stdout": "traceroute to 8.8.8.8 (8.8.8.8), 30 hops max ...", + "stderr": "", + "returncode": 0 +} +``` + +--- + +### POST /testmontools/curl + +Выполняет `curl` к указанному URL с подробными тайминговыми метриками. + +**Запрос** +```json +{ "url": "https://example.com" } +``` + +Дополнительные параметры: + +| Поле | Тип | Описание | +|-----------|---------|-----------------------------------------------| +| `headers` | boolean | Если `true` — запрашивает только заголовки (`-I`) | + +**Ответ** +```json +{ + "input": "https://example.com", + "url": "https://example.com", + "timing": { + "dns_lookup": 0.012, + "tcp_connect": 0.034, + "tls_handshake":0.098, + "pre_transfer": 0.099, + "first_byte": 0.215, + "total": 0.216, + "size_bytes": 1256, + "speed_bps": 5814, + "http_code": 200 + }, + "trace": "* Connected to example.com ...", + "body": "...", + "returncode": 0 +} +``` + +**Тайминговые поля** + +| Поле | Описание | +|-----------------|---------------------------------------| +| `dns_lookup` | Время разрешения DNS (сек) | +| `tcp_connect` | Время установки TCP-соединения (сек) | +| `tls_handshake` | Время TLS-хендшейка (сек) | +| `pre_transfer` | Время до начала передачи (сек) | +| `first_byte` | Время до первого байта ответа (сек) | +| `total` | Общее время запроса (сек) | +| `size_bytes` | Размер скачанного тела (байт) | +| `speed_bps` | Скорость загрузки (байт/сек) | +| `http_code` | HTTP-код ответа | + +--- + +### POST /testmontools/all + +Запускает `ping`, `traceroute` и `curl` **параллельно** и возвращает сводный результат. + +**Запрос** +```json +{ "url": "https://example.com" } +``` + +**Ответ** +```json +{ + "input": "https://example.com", + "ping": { ... }, + "traceroute": { ... }, + "curl": { ... } +} +``` + +Структура каждого вложенного объекта совпадает с ответами соответствующих отдельных эндпоинтов. + +--- + +## Примеры + +**curl из терминала** + +```bash +# Ping +curl -s -X POST http://localhost:7088/testmontools/ping \ + -H 'Content-Type: application/json' \ + -d '{"host": "google.com"}' | jq + +# Только заголовки +curl -s -X POST http://localhost:7088/testmontools/curl \ + -H 'Content-Type: application/json' \ + -d '{"url": "https://google.com", "headers": true}' | jq + +# Полная диагностика +curl -s -X POST http://localhost:7088/testmontools/all \ + -H 'Content-Type: application/json' \ + -d '{"url": "https://google.com"}' | jq +``` + +**Python** + +```python +import requests + +r = requests.post( + "http://localhost:7088/testmontools/all", + json={"url": "https://example.com"} +) +data = r.json() +print(data["curl"]["timing"]["first_byte"]) +``` + +--- + +## Коды ответов + +| HTTP-код | Описание | +|----------|-------------------------------------| +| `200` | Успешно, результат в теле ответа | +| `400` | Не передан обязательный параметр | + diff --git a/network_tools.py b/network_tools.py new file mode 100644 index 0000000..3dc5afc --- /dev/null +++ b/network_tools.py @@ -0,0 +1,171 @@ +from flask import Flask, request, jsonify +from urllib.parse import urlparse +from concurrent.futures import ThreadPoolExecutor +import subprocess +import re + +app = Flask(__name__) + +CURL_TIMING_FORMAT = ( + "dns_lookup=%(time_namelookup)s\n" + "tcp_connect=%(time_connect)s\n" + "tls_handshake=%(time_appconnect)s\n" + "pre_transfer=%(time_pretransfer)s\n" + "first_byte=%(time_starttransfer)s\n" + "total=%(time_total)s\n" + "size_bytes=%(size_download)s\n" + "speed_bps=%(speed_download)s\n" + "http_code=%(http_code)s\n" +).replace('%', '%') + +CURL_NOISE = re.compile(r'[\{\}] \[\d+ bytes data\]|\[[\d\s]+ bytes data\]|\r') + + +def extract_host(value: str) -> str: + if value.startswith(('http://', 'https://')): + return urlparse(value).hostname or value + return value + + +def parse_input(data: dict): + raw = data.get('url') or data.get('host') + if not raw: + return None, None + return raw, extract_host(raw) + + +def parse_curl_timing(text: str) -> dict: + timing = {} + for line in text.splitlines(): + if '=' in line: + key, _, val = line.partition('=') + try: + timing[key.strip()] = float(val.strip()) + except ValueError: + timing[key.strip()] = val.strip() + return timing + + +def clean_curl_output(text: str) -> str: + return '\n'.join( + line for line in text.splitlines() + if not CURL_NOISE.search(line) + ) + + +def run_ping(host: str) -> dict: + result = subprocess.run( + ['ping', '-c', '4', host], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + return { + 'resolved_host': host, + 'stdout': result.stdout, + 'stderr': result.stderr, + 'returncode': result.returncode + } + + +def run_traceroute(host: str) -> dict: + result = subprocess.run( + ['traceroute', host], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) + return { + 'resolved_host': host, + 'stdout': result.stdout, + 'stderr': result.stderr, + 'returncode': result.returncode + } + + + +def run_curl(url: str, headers_only: bool = False) -> dict: + args = [ + 'curl', '-sS', '-v', + '-w', '\n===TIMING===\n' + CURL_TIMING_FORMAT, + '--max-time', '30', + ] + + if headers_only: + args.append('-I') + + args.append(url) + + result = subprocess.run( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # Разделяем тело и timing + parts = result.stdout.split('===TIMING===') + + body = parts[0].strip() + timing_text = parts[1] if len(parts) > 1 else "" + + return { + 'url': url, + 'timing': parse_curl_timing(timing_text), + 'trace': clean_curl_output(result.stderr), + 'returncode': result.returncode, + 'body': body + } + + +# ── Endpoints ──────────────────────────────────────────────────────────────── + +@app.route('/testmontools/ping', methods=['POST']) +def ping(): + data = request.get_json() + raw, host = parse_input(data or {}) + if not host: + return jsonify({'error': 'Field "url" or "host" is required'}), 400 + return jsonify({'input': raw, **run_ping(host)}) + + +@app.route('/testmontools/traceroute', methods=['POST']) +def traceroute(): + data = request.get_json() + raw, host = parse_input(data or {}) + if not host: + return jsonify({'error': 'Field "url" or "host" is required'}), 400 + return jsonify({'input': raw, **run_traceroute(host)}) + + +@app.route('/testmontools/curl', methods=['POST']) +def curl(): + data = request.get_json() + raw, _ = parse_input(data or {}) + if not raw: + return jsonify({'error': 'Field "url" or "host" is required'}), 400 + url = raw if raw.startswith(('http://', 'https://')) else f'https://{raw}' + return jsonify({'input': raw, **run_curl(url, headers_only=bool(data.get('headers')))}) + + +@app.route('/testmontools/all', methods=['POST']) +def check_all(): + data = request.get_json() + raw, host = parse_input(data or {}) + if not raw or not host: + return jsonify({'error': 'Field "url" or "host" is required'}), 400 + + url = raw if raw.startswith(('http://', 'https://')) else f'https://{raw}' + + # Запускаем ping и traceroute параллельно, curl — отдельно + with ThreadPoolExecutor(max_workers=3) as pool: + f_ping = pool.submit(run_ping, host) + f_traceroute = pool.submit(run_traceroute, host) + f_curl = pool.submit(run_curl, url) + + return jsonify({ + 'input': raw, + 'curl': f_curl.result(), + 'ping': f_ping.result(), + 'traceroute': f_traceroute.result(), + }) + + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=7088, debug=False)