Сайт Романа ПарпалакаБлог20230622

Http-прокси на PHP

22 июня 2023 года, 01:23

Обычно в постах о программировании я пишу об успешных подходах и находках. В этот раз расскажу об идее, которая на практике не заработала.

В прошлый раз я рассказывал, что если у вас есть свой виртуальный сервер, вы можете не возиться с VPN, а отправить трафик из браузера через ssh-туннель. Всё что нужно сделать — выполнить однострочную команду, которая запустит локальный SOCKS5-прокси, и прописать параметры этого локального прокси в браузере.

Я задумался, можно ли провернуть такой же трюк без своего виртуального сервера. Стал смотреть в сторону виртуальных (shared) хостингов, в частности бесплатных или предоставляющих бесплатный тестовый период. На hostings.info нашел бесплатный хостинг с доступом по SSH. Трюк с ssh-туннелем не удался. Видимо, админы озаботились этой проблемой и запретили процессу SSH исходящий трафик через файервол или как-нибудь еще.

Я стал думать дальше и решил попробовать другой вариант. На сервере хостера запущен PHP. Я могу подключиться к нему через обычный 80-й или 443-й порт, а сам PHP будет ходить по нужным внешним хостам. Так работают HTTP-прокси. Каких-то готовых решений быстро не смог нагуглить и спросил решение у ChatGPT. Он с третьей попытки предложил простенький скрипт, который я взял за основу и доработал:

<?php

require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\ServerRequest;

// Создание экземпляра клиента Guzzle
$client = new Client();

// Обработка входящего запроса
$request = ServerRequest::fromGlobals();

// Получение URL-адреса запрашиваемого сайта
$url = $request->getUri();
$url
    // Я собирался ходить на https-сайты, поэтому подменил протокол и порт
    ->withScheme('https')
    ->withPort(443)
    // Подменяем хост (видимо, тут и происходит обработка протокола http-прокси)
    ->withHost($request->getHeaderLine('host'))
    ->withQuery($request->getUri()->getQuery())
;

// Создание прокси-запроса
$proxyRequest = new Request(
    $request->getMethod(),
    $url,
    $request->getHeaders(),
    $request->getBody(),
    $request->getProtocolVersion()
);

// Отправка прокси-запроса и получение ответа
$response = $client->send($proxyRequest, [
    'stream'          => true,
    'verify'          => false,
    'allow_redirects' => false, // Коды редиректов отправляем назад в браузер
]);

// Передача заголовков ответа клиенту
foreach ($response->getHeaders() as $name => $values) {
    foreach ($values as $value) {
        header(sprintf('%s: %s', $name, $value), false);
    }
}

// Передача тела ответа клиенту
echo $response->getBody();

Чтобы этот скрипт завести, нужно сохранить его в файл со произвольным редким названием, например, q7e6r53t.php, и установить через composer библиотеку guzzle. Кроме того, в nginx в настройку хоста надо добавить следующее:

server {
    listen       8082;
    server_name  localhost;
    root         /mnt/c/git/proxy;

    location / {
        try_files $uri $uri/ /q7e6r53t.php$is_args$args;
    }

    location ~ \q7e6r53t.php$ {
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
        fastcgi_index index.php;
        include fastcgi.conf;
    }
}

В таком варианте прокси заработал локально, даже с авторизацией и куками. Самый большой недостаток этого подхода в том, что нельзя проксировать https-трафик. При обращении по https выполняются запросы CONNECT, которые до скрипта не доходят, так как nginx отвечает ошибкой 400:

127.0.0.1 - - [18/Jun/2023:12:56:47 +0300] "CONNECT www.google.com:443 HTTP/1.1" 400 166 "-" "-"

И даже если вдруг представить, что такое возможно, это был бы man in the middle. Поэтому в браузере приходится набирать адрес сайта с http, а скрипт подменяет протокол на https. Если в PHP нет нужных сертификатов, подключиться к сайту будет невозможно. Для отключения проверки сертификатов я добавил флаг 'verify' => false. Конечно, это несекьюрно, но тут и так трафик передается хостеру в открытом виде, так что держать ворота запертыми в открытом поле смысла нет :)

На практике у хостера этот скрипт не заработал. В браузере отображалась страница ошибки Apache о неправильно сконфигурированном хосте. Очевидно, Apache настроен так, чтобы не позволять так просто делать http-прокси. Возможный выход — написать расширение для браузера, которое отправляло бы запросы через другой протокол. Скажем, оно оборачивало бы все запросы в обычный POST-запрос, в теле которого передавались бы параметры оригинального запроса (url, method, headers, body). Но это потребовало бы больше времени, чем я готов был выделить на это исследование, и свою изначальную задачу я решил обходным путем.

В ходе лабораторной работы мы написали простейший скрипт http-прокси на PHP, добились его работы в локальном окружении, но на виртуальном хостинге он не заработал. В принципе, такой подход всё еще можно использовать в собственном или контролируемом окружении для подмены IP-адресов и прочих задач, связанных с парсингом сайтов.

Поделиться

Эксперименты и использование ChatGPT Ctrl Задача о педантичном пассажире

Читайте также

Комментарии

#1. 23 ноября 2023 года, 01:05. пишет:
Использую github.com/PHProxy/phproxy — даже старый нетскейп через него по интернету ходит. А для взятия одиночных страниц однокомандный php-шник — «<?php echo file_get_contents($_GET['q']); ?>». Например для старых версий Rebol`а, которые не понимают новый ssl получается простая функция «reads: func [ url ] [ read append http://www.pochinoksergey.ru/sslv3/proxy.php?q= url]» и далее простой комадой "reads https://parpalak.com/blog/2023/06/22/http_ … n_php"; получаем требуемое содержимое. Разумеется можно и без функций, например "read http://www.pochinoksergey.ru/sslv3/proxy.p … n_php";

Оставьте свой комментарий


Формулы на латехе: $$f(x) = x^2-\sqrt{x}$$ превратится в $$f(x) = x^2-\sqrt{x}$$.
Выделение текста: [i]курсивом[/i] или [b]жирным[/b].
Цитату оформляйте так: [q = имя автора]цитата[/q] или [q]еще цитата[/q].
Других команд или HTML-тегов здесь нет.

Записи