#!/usr/bin/env python3
# ----------------------------------------------------------------------------------------------------
# Proxy2 - IPFS Server с поддержкой создания статических снэпшотов (HTML + ресурсы через IPFS)
# Работает в двух режимах:
# 1. Обычный прокси (как раньше) - для обычного трафика
# 2. Режим снэпшота - по специальному заголовку X-Snapshot: true

# ----------------------------------------------------------------------------------------------------
# Этот скрипт Proxy 2 совместно с Proxy 1 создаёт IPFS канал, использующий не блокируемые пиринговые сети.
# Т.к. через IPFS крайне тяжело передаётся динамический контент (например, сайты, генерируемые JavaScript), то в Proxy 2 добавлена технология
# для скачивания динамической веб страницы и генерации её статической формы (снэпшота), которая и отправляется на Proxy 1 (пользователю) через IPFS
# ----------------------------------------------------------------------------------------------------


import socket
import threading
import binascii
import subprocess
import time
import re
import json
import hashlib
from urllib.parse import urljoin, urlparse, quote
from datetime import datetime

# ==================== КОНФИГУРАЦИЯ ====================
LISTEN_PORT = 8222
EXTERNAL_IP = "192.168.1.160"  # IP вашей VMware

# Режимы работы: True = использовать рендеринг снэпшотов, False = обычный прокси
ENABLE_SNAPSHOT_MODE = True
# Максимальный размер ресурса в байтах (50 MB)
MAX_RESOURCE_SIZE = 50 * 1024 * 1024
# Таймаут загрузки страницы в секундах
PAGE_LOAD_TIMEOUT = 30

HEX_PREFIX = b"GET /wiki/ HTTP/1.1\r\nHost: ru.euwiki.ru\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\nAccept: text/html\r\nAccept-Language: ru-RU,ru;q=0.9\r\nAccept-Encoding: gzip, deflate, br\r\nConnection: keep-alive\r\n\r\n"
SEPARATOR = b"\n---HEX---\n"

# IPFS клиент будет инициализирован позже
ipfs_client = None

# ==================== IPFS ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ====================

def init_ipfs_client():
    """Инициализирует IPFS клиент"""
    global ipfs_client
    try:
        import ipfshttpclient
        # Пробуем подключиться к локальному IPFS демону
        ipfs_client = ipfshttpclient.connect('/dns/localhost/tcp/5001/http')
        print(f"[?] IPFS client connected successfully")
        return True
    except ImportError:
        print(f"[!] ipfshttpclient not installed. Run: pip install ipfshttpclient")
        return False
    except Exception as e:
        print(f"[!] Failed to connect to IPFS: {e}")
        return False

def upload_to_ipfs(data: bytes, filename: str = None) -> str:
    """Загружает данные в IPFS и возвращает CID"""
    global ipfs_client
    if ipfs_client is None:
        return None
    
    try:
        # Пробуем добавить как файл с именем (для лучшей дедупликации)
        if filename:
            res = ipfs_client.add_bytes(data, name=filename)
        else:
            res = ipfs_client.add_bytes(data)
        
        # Возвращаем CID
        if isinstance(res, dict):
            return res['Hash']
        return str(res)
    except Exception as e:
        print(f"[!] Failed to upload to IPFS: {e}")
        return None

def download_from_ipfs(cid: str) -> bytes:
    """Скачивает данные из IPFS по CID"""
    global ipfs_client
    if ipfs_client is None:
        return None
    
    try:
        return ipfs_client.cat(cid)
    except Exception as e:
        print(f"[!] Failed to download from IPFS: {e}")
        return None

# ==================== РЕНДЕРИНГ САЙТОВ (Playwright) ====================

def render_page_with_playwright(url: str) -> dict:
    """
    Рендерит страницу через Playwright, сохраняет все ресурсы
    Возвращает: {
        'html': str,  # HTML с заменёнными ссылками на IPFS
        'resources': dict,  # маппинг url -> cid
        'success': bool,
        'error': str
    }
    """
    try:
        from playwright.sync_api import sync_playwright
    except ImportError:
        print(f"[!] Playwright not installed. Run: pip install playwright && playwright install chromium")
        return {'success': False, 'error': 'Playwright not installed'}

    resources = {
        'images': {},
        'css': {},
        'fonts': {},
        'other': {}
    }
    
    def handle_response(response):
        """Обработчик ответов - сохраняет все ресурсы"""
        try:
            # Определяем тип ресурса
            resource_type = response.request.resource_type
            
            # Сохраняем только нужные типы
            if resource_type in ['image', 'stylesheet', 'font']:
                url = response.url
                
                # Пропускаем data: URI и слишком большие файлы
                if url.startswith('data:'):
                    return
                
                # Получаем тело ответа
                body = None
                try:
                    body = response.body()
                except Exception:
                    return
                
                if body and len(body) < MAX_RESOURCE_SIZE:
                    # Загружаем в IPFS
                    cid = upload_to_ipfs(body, filename=hashlib.md5(url.encode()).hexdigest())
                    if cid:
                        if resource_type == 'image':
                            resources['images'][url] = cid
                        elif resource_type == 'stylesheet':
                            resources['css'][url] = cid
                        elif resource_type == 'font':
                            resources['fonts'][url] = cid
                        else:
                            resources['other'][url] = cid
        except Exception as e:
            # Тихая обработка ошибок
            pass
    
    try:
        with sync_playwright() as p:
            # Запускаем браузер с маскировкой ботов
            browser = p.chromium.launch(
                headless=True,
                args=[
                    '--disable-blink-features=AutomationControlled',
                    '--disable-dev-shm-usage',
                    '--no-sandbox',
                    '--disable-setuid-sandbox'
                ]
            )
            
            # Создаём контекст с маскировкой
            context = browser.new_context(
                viewport={'width': 1920, 'height': 1080},
                user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
            )
            
            page = context.new_page()
            
            # Добавляем обработчик ответов
            page.on('response', handle_response)
            
            # Переходим на страницу
            print(f"[*] Rendering: {url}")
            page.goto(url, wait_until='networkidle', timeout=PAGE_LOAD_TIMEOUT * 1000)
            
            # Прокручиваем страницу для загрузки lazy-элементов
            page.evaluate('window.scrollTo(0, document.body.scrollHeight)')
            time.sleep(2)
            
            # Получаем HTML
            html = page.content()
            
            # Закрываем браузер
            browser.close()
            
            # Подменяем ссылки в HTML
            modified_html = replace_urls_with_ipfs(html, url, resources)
            
            return {
                'success': True,
                'html': modified_html,
                'resources': resources,
                'original_url': url,
                'timestamp': datetime.now().isoformat()
            }
            
    except Exception as e:
        print(f"[!] Playwright rendering error: {e}")
        return {'success': False, 'error': str(e)}

def replace_urls_with_ipfs(html: str, base_url: str, resources: dict) -> str:
    """Заменяет ссылки на ресурсы в HTML на ссылки через IPFS"""
    from bs4 import BeautifulSoup
    
    soup = BeautifulSoup(html, 'html.parser')
    
    # Заменяем картинки
    for img in soup.find_all('img'):
        src = img.get('src')
        if src and not src.startswith('data:'):
            absolute_url = urljoin(base_url, src)
            if absolute_url in resources.get('images', {}):
                cid = resources['images'][absolute_url]
                img['src'] = f"/ipfs/{cid}"
                # Добавляем атрибут для указания оригинального URL
                img['data-original-url'] = absolute_url
    
    # Заменяем CSS
    for link in soup.find_all('link', rel='stylesheet'):
        href = link.get('href')
        if href:
            absolute_url = urljoin(base_url, href)
            if absolute_url in resources.get('css', {}):
                cid = resources['css'][absolute_url]
                link['href'] = f"/ipfs/{cid}"
    
    # Заменяем шрифты в стилях
    for style in soup.find_all('style'):
        if style.string:
            # Простая замена URL на IPFS в inline стилях (можно расширить)
            for font_url, cid in resources.get('fonts', {}).items():
                style.string = style.string.replace(font_url, f"/ipfs/{cid}")
    
    # Удаляем все скрипты
    for script in soup.find_all('script'):
        script.decompose()
    
    # Удаляем атрибуты-обработчики событий
    for tag in soup.find_all():
        attrs_to_remove = [attr for attr in tag.attrs if attr.startswith('on')]
        for attr in attrs_to_remove:
            del tag[attr]
    
    # Добавляем мета-тег о том, что это снэпшот
    meta = soup.new_tag('meta', name='generator', content='IPFS-HTTP-Proxy-Snapshot')
    if soup.head:
        soup.head.insert(0, meta)
    else:
        soup.insert(0, meta)
    
    return str(soup)

def create_snapshot_response(url: str) -> bytes:
    """
    Создаёт ответ для снэпшота
    Возвращает закодированные данные для отправки через IPFS
    """
    print(f"[*] Creating snapshot for: {url}")
    
    # Рендерим страницу
    result = render_page_with_playwright(url)
    
    if result['success']:
        # Формируем JSON ответ
        response = {
            'type': 'snapshot',
            'original_url': result['original_url'],
            'html': result['html'],
            'resources': result['resources'],
            'timestamp': result['timestamp'],
            'status': 'ok'
        }
        
        response_json = json.dumps(response, ensure_ascii=False)
        response_data = response_json.encode('utf-8')
        
        # Добавляем специальный заголовок для идентификации снэпшота
        http_response = f"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nX-Snapshot: true\r\nContent-Length: {len(response_data)}\r\n\r\n".encode('utf-8') + response_data
        
        return http_response
    else:
        # В случае ошибки возвращаем обычный ответ
        error_msg = json.dumps({
            'type': 'error',
            'error': result.get('error', 'Unknown error'),
            'original_url': url
        })
        http_response = f"HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\n\r\n{error_msg}".encode('utf-8')
        return http_response

# ==================== ОСНОВНАЯ ПРОКСИ-ЛОГИКА ====================

def parse_request(data: bytes) -> tuple:
    """Парсит HTTP-запрос, возвращает (host, port, is_connect, is_snapshot, target_url)"""
    try:
        text = data.decode('ascii', errors='ignore')
        
        # Проверяем, является ли запрос запросом снэпшота (через специальный путь)
        match = re.search(r'GET\s+/\?snapshot=(.+?)\s+HTTP', text, re.IGNORECASE)
        if match and ENABLE_SNAPSHOT_MODE:
            target_url = match.group(1)
            return (None, None, False, True, target_url)
        
        # Обычный CONNECT запрос
        match = re.search(r'CONNECT\s+([^\s:]+)(?::(\d+))?', text, re.IGNORECASE)
        if match:
            host = match.group(1)
            port = int(match.group(2)) if match.group(2) else 443
            return (host, port, True, False, None)
        
        # Обычный HTTP запрос
        match = re.search(r'Host:\s*([^\r\n]+)', text, re.IGNORECASE)
        if match:
            host = match.group(1).strip()
            return (host, 80, False, False, None)
        
        return (None, None, False, False, None)
    except:
        return (None, None, False, False, None)

def extract_hex_data(data: bytes) -> bytes:
    """Извлекает и декодирует HEX данные из пакета с префиксом"""
    if data.startswith(HEX_PREFIX):
        data = data[len(HEX_PREFIX) + len(SEPARATOR):]
    
    # Декодируем все HEX строки
    result = b""
    lines = data.split(b'\n')
    for line in lines:
        line = line.strip()
        if line:
            try:
                result += binascii.unhexlify(line)
            except:
                pass
    return result

def encode_data(data: bytes) -> bytes:
    """Кодирует данные в HEX с префиксом"""
    hex_data = binascii.hexlify(data) + b"\n"
    return HEX_PREFIX + SEPARATOR + hex_data

def forward_data(src, dst, is_from_client=True):
    """Пересылает данные из src в dst с декодированием/кодированием"""
    try:
        while True:
            chunk = src.recv(8192)
            if not chunk:
                break
            
            if is_from_client:
                # Данные от клиента (с префиксом) > интернет (без префикса)
                decoded = extract_hex_data(chunk)
                if decoded:
                    dst.send(decoded)
            else:
                # Данные от интернета (без префикса) > клиент (с префиксом)
                encoded = encode_data(chunk)
                dst.send(encoded)
    except:
        pass

def handle_tunnel(client_sock, addr):
    """Обрабатывает P2P-соединение от IPFS"""
    internet_sock = None
    try:
        # Получаем первый запрос
        data = client_sock.recv(8192)
        if not data:
            return
        
        print(f"[*] {addr} Received {len(data)} bytes")
        
        # Извлекаем и декодируем запрос
        request = extract_hex_data(data)
        if not request:
            print(f"[!] {addr} Failed to decode request")
            return
        
        # Парсим запрос
        host, port, is_connect, is_snapshot, target_url = parse_request(request)
        
        # Обработка снэпшота
        if is_snapshot and target_url:
            print(f"[*] {addr} SNAPSHOT_REQUEST > {target_url}")
            response = create_snapshot_response(target_url)
            # Отправляем напрямую (уже закодированный ответ)
            if not response.startswith(HEX_PREFIX):
                response = encode_data(response)
            client_sock.send(response)
            return
        
        if not host:
            print(f"[!] {addr} Failed to parse request")
            return
        
        print(f"[*] {addr} REQUEST > {host}:{port} (CONNECT={is_connect})")
        
        # Подключаемся к целевому серверу
        internet_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        internet_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
        if EXTERNAL_IP:
            internet_sock.bind((EXTERNAL_IP, 0))
        internet_sock.settimeout(30)
        internet_sock.connect((host, port))
        internet_sock.settimeout(None)
        
        # Если это CONNECT, отправляем ответ браузеру
        if is_connect:
            response = b"HTTP/1.1 200 Connection established\r\n\r\n"
            encoded_response = encode_data(response)
            client_sock.send(encoded_response)
            print(f"[*] {addr} Sent 200 CONNECT response")
            
            # Пересылаем данные в обе стороны
            t1 = threading.Thread(target=forward_data, args=(client_sock, internet_sock, True))
            t2 = threading.Thread(target=forward_data, args=(internet_sock, client_sock, False))
            t1.daemon = True
            t2.daemon = True
            t1.start()
            t2.start()
            
            t1.join()
            t2.join()
        else:
            # Обычный HTTP запрос
            internet_sock.send(request)
            
            # Получаем ответ и отправляем клиенту
            response = b""
            while True:
                chunk = internet_sock.recv(8192)
                if not chunk:
                    break
                response += chunk
            
            encoded_response = encode_data(response)
            client_sock.send(encoded_response)
        
    except Exception as e:
        print(f"[!] {addr} Error: {e}")
    finally:
        try:
            client_sock.close()
        except:
            pass
        try:
            if internet_sock:
                internet_sock.close()
        except:
            pass

def main():
    print("=" * 60)
    print("PROXY2 (Server) - IPFS P2P Mode WITH SNAPSHOT SUPPORT")
    print("=" * 60)
    print(f"[*] External IP: {EXTERNAL_IP}")
    print(f"[*] Listening port: {LISTEN_PORT}")
    print(f"[*] Snapshot mode: {'ENABLED' if ENABLE_SNAPSHOT_MODE else 'DISABLED'}")
    
    # Инициализируем IPFS клиент
    if not init_ipfs_client():
        print("[!] Continuing without IPFS client (will use direct URLs)")
    
    # Запускаем ipfs p2p listen
    cmd = ['ipfs', 'p2p', 'listen', '/x/http-proxy/1.0.0', f'/ip4/127.0.0.1/tcp/{LISTEN_PORT}']
    print(f"[*] Starting: {' '.join(cmd)}")
    listener_process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    time.sleep(2)
    
    # Создаём TCP сервер для приёма соединений от IPFS
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(('127.0.0.1', LISTEN_PORT))
    server.listen(100)
    print(f"[*] Waiting for P2P connections...")
    
    try:
        while True:
            client, addr = server.accept()
            client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
            print(f"[+] P2P connection from {addr}")
            t = threading.Thread(target=handle_tunnel, args=(client, addr))
            t.daemon = True
            t.start()
    except KeyboardInterrupt:
        print("\n[*] Shutting down...")
    finally:
        server.close()
        listener_process.terminate()

if __name__ == "__main__":
    main()

# (c) by Valery Shmelev (Deutsche: Valery Shmeleff)



# --------------------------------------------------------------------------------------------------------------
#  Как использовать
#  1. Установка зависимостей на Ubuntu

# Установка pip и пакетов
#  sudo apt update
#  sudo apt install python3-pip python3-venv -y

# Установка Python библиотек
#  pip3 install playwright beautifulsoup4 ipfshttpclient

# Установка браузера для Playwright
#  playwright install chromium

# Проверка IPFS (должен быть запущен)
#  ipfs daemon  # в отдельном терминале, если ещё не запущен

# --------------------------------------------------------------------------------------------------------------

# Запуск Proxy 2 с поддержкой снэпшотов

# python3 proxy2_ipfs_snapshot.py

# --------------------------------------------------------------------------------------------------------------

# Как запросить снэпшот через IPFS
# Со стороны Proxy 1 (на ПК пользователя) нужно отправить запрос:

# GET /?snapshot=https://news.ru/article/123 HTTP/1.1
# Host: any

# --------------------------------------------------------------------------------------------------------------

# Proxy 2:

# Рендерит страницу через Playwright
# Сохраняет все картинки, CSS, шрифты в IPFS
# Подменяет ссылки на /ipfs/{cid}
# Возвращает JSON с HTML и маппингом ресурсов

# --------------------------------------------------------------------------------------------------------------

# На ПК пользователя (Proxy 1)

# Браузер (Chrome/Firefox) 
#     v (настроен прокси: localhost:8081)
# Proxy 1 (ваш, на Python) 
#     v (передаёт через IPFS канал)
# Proxy 2 (на хостинге) - который мы только что дописали

# --------------------------------------------------------------------------------------------------------------

# Как пользователь запрашивает снэпшот?
# Способ 1: Через адресную строку (самый простой)

# Пользователь просто вводит в браузере URL с специальным параметром:

# text
# http://any-site.com/?snapshot=https://news.ru/article/123
# Или если Proxy 1 пропускает любые хосты:

# text
# http://proxy1.local/?snapshot=https://news.ru/article/123

# --------------------------------------------------------------------------------------------------------------

# Почему это работает:

# Браузер отправляет GET-запрос на any-site.com (или proxy1.local)

# Proxy 1 перехватывает этот запрос (так как браузер настроен на его использование)

# Proxy 1 передаёт запрос через IPFS в Proxy 2

# Proxy 2 видит параметр ?snapshot=... и создаёт снэпшот

# --------------------------------------------------------------------------------------------------------------

https://chat.deepseek.com/share/umw9kzmere5cbfk317










































