Салимóненко Дмитрий Александрович

Вычислительные Сети, Системы и Телекоммуникации

ЗАДАНИЕ 8: Простой прокси-сервер на ТСР-сокетах для LINUX (С/С++)




Введение

В этом задании рассмотрим разработку простого локального (т.е. расположенного на нашем же компьютере) прокси-сервера на ТСП сокетах. Как обычно, используем язык С/С++ под Linux.

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

Зачем это может быть нужно?

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

С другой стороны, можно будет посылать на сервер какие-то свои, индивидуальные запросы, такие, которые сам браузер сделать не сможет – для расширения функциональности открытой вебстраницы.

Есть также и иное назначение прокси-сервера: открывать страницы сайтов, которые по тем или иным причинам заблокированы в Вашей сети, но доступны из другой сети; также прокси-серверы используют, бывает, для того, чтобы подменить свой IP-адрес. Для этого потребуется разместить прокси-сервер уже не у себя на компьютере, а где-нибудь на хостинге в интернете. Ну, и запустить его, настроив соответствующим образом браузер. В данном задании мы этим заниматься не будем.

Правда, подобные вещи могут решаться, в частности, при помощи плагина к браузеру. Поэтому прокси-сервер, по сути, это лишь одна из возможностей модификации открываемых веб-страниц и запросов к веб-серверам. Также, он может являться инструментом контроля НТТР-трафика браузера. Итак, начнем разработку с самого простого варианта.


1. Вначале напишем клиент, который просто передаст вебсерверу заголовки браузера и выведет ответ в консоль

Сразу перейдем к делу и посмотрим программный код клиента (client.c) на языке С:

  1. #include <sys/types.h>
  2. #include <sys/socket.h>
  3. #include <stdlib.h>
  4. #include <stdio.h>
  5. #include <unistd.h>
  6. #include <fcntl.h>
  7. #include <sys/stat.h>
  8. #include <sys/types.h>
  9. #include <netinet/in.h>
  10. #include <arpa/inet.h>
  11. #include<netdb.h> //hostent
  12. #include <string.h>
  13. char message[] = "GET / HTTP/1.1\r\nHost: 4846d.ru\r\nUser-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nCache-Control: max-age=0\r\n\r\n";
  14. char buf1[1024];

  1. int fs, fr;
  2. int main()
  3. {
  4. int sock;
  5. struct sockaddr_in addr;
  6. sock = socket(AF_INET, SOCK_STREAM, 0);
  7. if(sock < 0)
  8. {
  9. perror("socket");
  10. exit(1);
  11. }

  1. //  Устанавливаем сокету параметр SO_REUSEADDR или SO_REUSEPORT, чтобы после прекращения работы программы порт не использовался системой и был готов для последующего использования. Полезно, когда программу нужно запустить вновь СРАЗУ после того, как она прекратила работу в предыдущий раз.
  2. int reuse = 1;
  3. if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)) < 0)
  4. perror("setsockopt(SO_REUSEADDR) failed");
  5. #ifdef SO_REUSEPORT
  6. if (setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuse, sizeof(reuse)) < 0)
  7. perror("setsockopt(SO_REUSEPORT) failed");
  8. #endif

  1. addr.sin_family = AF_INET;
  2. addr.sin_port = htons(80); //По умолчанию, сервер открывает, как правило, именно порт под номером 80 для службы НТТР. Да и браузер по умолчанию также обращается именно к этому порту при открытии страницы по протоколу НТТР.
  3. // addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // Это было раньше
  4. struct hostent* hostSITE = gethostbyname("4846d.ru");
  5. addr.sin_addr.s_addr = ((struct in_addr*)hostSITE->h_addr_list[0])->s_addr;

  1. if(connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0)
  2. {
  3. perror("connect");
  4. exit(2);
  5. }
  6. fs=send(sock, message, strlen(message)*sizeof(char), 0);
  7. printf ("\n");

  1. while((fr=recv(sock, buf1, sizeof(buf1), 0)) > 0)
  2. {printf("%s", buf1);
  3. printf ("\n!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"); // Это так, для ориентации, ибо вывод html-страницы в консоль будет иметь немалый объем, легко запутаться
  4. memset (buf1, 0, sizeof(buf1)); // Для чего это?
  5. }
  6. close(sock);
  7. printf ("\n");
  8. return 0;
  9. }

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

В строчке 13 задаются (пока в виде символьного массива) заголовки браузера, предназначенные для открытия веб-страницы 4846d.ru. Вы, конечно, вполне можете попробовать пооткрывать и другую какую-нибудь страницу, в том числе, и с другого сайта. Кроме того, у меня в Ubuntu установлен Firefox 63. Если у Вас будет ДРУГОЙ браузер, заголовки. которые он отправляет для открытия страницы, будут, скорее всего, ДРУГИМИ. Как же их получить?

Получаем заголовки браузера

Можно, конечно, скопировать из консоли браузера. Нажимаем правой кнопкой мыши где-нибудь на веб-странице, затем «Исследовать элемент», «Сеть» и обновляем страницу. После чего можно будет посмотреть заголовки и запроса, и ответа. Но, они там будут, как минимум, не в том порядке, в каком браузер их посылает серверу.

Почему? Потому, что, к примеру, Firefox сортирует их по алфавиту. Чтобы пользователю было удобнее найти соответствующий заголовок. А передает их немного в другом порядке. Понятно, что веб-серверу это, как правило, без разницы; главное, чтобы нужные заголовки ВСЕ БЫЛИ, а уж в каком порядке – это уже детали (правда, некоторые веб-сервисы, типа Google, как раз обращают внимание на такие тонкости и по ним пытаются определить, из браузера ли делается запрос или он эмулирован). Но, все-таки, попытаемся притвориться браузером, чисто ради корректности, не более того. Для этого нам требуется где-то взять заголовки именно в том порядке, в котором их отправляет браузер. Для этого можно воспользоваться, скажем, заданием 5.

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

Следует скомпилировать приведенный в задании 5 код сервера (по ссылке), назвав его, к примеру, serv1.c (дальше будем называть эту программу локальным прокси-сервером). Затем, запустив его, в браузере откройте страницу

http://127.0.0.1:3425

После чего в браузере появятся заголовки его запроса (которые вернул обратно сервер), а также счетчик количества раз обновления страницы. Вот их-то мы и возьмем и скопируем с открытой в браузере веб-страницы, вставив в строчку 13 программы - клиента. Теперь осталось только вставить в нужных местах (где оканчивается очередной заголовок) переносы строк и переводы каретки, как требует спецификация протокола НТТР, т.е. символы \r\n. Не забудьте в конце вставить эти символы ДВАЖДЫ, так как необходимо послать веб-серверу пустую строку, чтобы он понял, что браузер более уже ничего посылать не будет и ждет ответа на свой запрос.

Примечание. По стандарту протокола HTTP полагается обозначать пустую строку в виде \r\n\r\n, т.е. двойной пропуск строки, что можно видеть, например, открыв какой-нибудь файл в программе Notepad++ (под Windows, где последовательность символов \r\n означает, в самом деле, перенос строки; их можно явно увидеть, включив режим отображения непечатаемых символов). Однако, в файлах Linux вместо \r\n переносы строк обозначаются как \n, в чем легко убедиться, открыв там какой-нибудь файл; даже при переносе его и открытии в Windows переносы строк так и останутся в виде \n. НО: используя команды синтаксического анализа текста на языке С/С++, тем не менее, следует указывать для обозначения переноса строки именно \r\n, а не \n! Т.е. параметры, соответствующие символам переноса строки, должны иметь значения \r\n. Парадокс, но это - так. Видимо, так сделали в целях полного соответствия правилам НТТР-протоколов.

Возможно, пояснения еще требуют строчки 37, 38. Ранее, когда работали с локальным клиентом, IP-адрес указывали в виде строчки 36 (сейчас она закомментирована). Теперь же нам требуется адресоваться к хостингу (сайту), расположенному на веб-сервере. Поэтому используем структуру hostSITE  типа hostent.

Ну, а далее, как обычно, устанавливаем соединение при помощи функции connect() и посылаем заголовки веб-серверу (строчка 44). Обратите внимание, что мы здесь используем порт 80 (см. строчку 35), а не 3425, как обычно. Потому, что нам требуется обратиться к СТАНДАРТНОМУ порту, на котором у веб-сервера запущены служба НТТР. Для интереса, попробуйте вместо 80-го порта указать какой-либо другой, например, 3425. После запуска клиента в таком случае в консоли увидите через некоторое время знакомое «Connection refused». Вспомните, что так бывало, когда Вы пытались запустить листинг-клиент при незапущенном листинг-сервере. Так и здесь: то ли порт 3425 не открыт на сервере, содержащем страницу 4846d.ru, то ли открыт, но по протоколу HTTP он не работает и потому с нагим клиентом взаимодействовать не желает.

Кстати, узнать, открыт ли соответствующий порт на сервере в интернете или нет, можно при помощи команды nmap.

Да, здесь – как раз тот самый случай, когда нам придется использовать стандартный номер порта, который меньше, чем 1024.

После отправки заголовков заходим в цикл (строчка 46), в котором считываем все то, что сервер присылает нам на ТСР-порт под номером 80. Если сообщение веб-сервера будет большим (а там ведь его заголовки ответа, пустая строка и затем – ВСЯ веб-страница), итерации в цикле повторятся соответствующее количество раз. В конце каждой итерации полученные от сервера символы выводим на экран в консоль. Когда весь ответ сервера будет прочитан, закрываем сокет (строчка 51), выводим пустую строку и, вроде бы, прекращаем работу. НО: на самом деле, не все так просто.

Дело в том, что ТСР-соединения (при помощи которых реализуется передача сообщений в формате протокола НТТР) являются, что называется, «живучими». Т.е. они сразу не закрываются. Поэтому и наш клиент тоже сразу не закроется, а будет находиться в состоянии ожидания. Чтобы было понятнее, о чем идет речь, предварительно запустите утилиту tcpdump в следующем виде:

sudo tcpdump port 80 –A

Опция –А дает нам возможность выводить в консоль не только заголовки протоколов IP, ТСР, но и данные, полученные с веб-сервера, в ASCII-виде.

В силу несовпадения кодировок (в Ubuntu – UTF-8, а на веб-странице 4846d.ruWindows-1251), русские буквы будут отображаться в нечитаемом виде.

После чего запустите наш клиент.

Будет видно, что в консоли клиента появились заголовки ответа веб-сервера, затем, через пустую строку, HTML-код веб-страницы http://4846d.ru. После чего клиент останется в ждущем режиме.

Тогда как в консоли с запущенной tcpdump будут происходить многократные тройные ТСР-рукопожатия, затем - передачи и подтверждения данных. Также там появятся куски содержимого скачиваемой веб-страницы (код HTML). Обратите внимание, что наш клиент, как ни странно, отправляет запросы с портов под номерами типа таких: 42385 и т.п. Тогда как для веб-сервера используется порт 80 (который в данном случае будет обозначен, как .http).

Это означает, что, на самом деле, для того, чтобы обеспечить передачу заголовков веб-серверу (при помощи функции send() ) и соответствующий прием ответа от него (при помощи функции recv() ), операционная система (на уровне ядра, как говорится, за кулисами) реализует несколько дополнительных ТСР-соединений, которые используются для тройных рукопожатий, а также для контроля передачи данных. Для этого используются временные ТСР-порты с высокими номерами. Номера выбираются случайным образом.

Иными словами, функция send() представляет собой, на самом деле, микроклиент (точнее, его часть, выполняющая запрос), не такой уж и низкоуровневый, как это могло бы показаться программистам на РНР, Java, C# и т.п. Точно также, функция recv() - это, на самом деле, микросервер (точнее, отвечающая часть микросервера). Который для выполнения своей задачи использует ряд соединений ТСР/UDP, при каждом из них происходит адресация, выполняется «тройное рукопожатие» и т.д. Эти соединения осуществляются и контролируются ядром операционной системы.

Наша программа – клиент, использует, всего-то навсего, лишь ДВЕ строчки - для отправки запроса и для получения ответа. Система же - много чего делает для выполнения этих «всего лишь двух строчек».

Настраиваем браузер для работы с прокси-сервером

Чтобы полноценно использовать прокси-сервер в работе, необходимо обеспечить взаимодействие браузера с ним. Это можно сделать разными путями, открывая веб-страницы:

  1. В виде http://localhost/4846d.ru,
  2. В виде http://4846d.ru:3425,
  3. С использованием прокси-настроек браузера.

Первый и второй пути (комбинированно) использовались в задании 5 (см. также выше). С этим может быть связана проблема, если открываемая веб-страница видоизменяет свое содержимое, например, при помощи AJAX-запросов. При которых, если не подключена CORS, будет применяться политика одного источника: одинаковые протокол, порт и домен. При нарушении хотя бы одного из этих требований запрос будет заблокирован браузером. К сожалению, так как AJAX-запрос выполняется средствами JS, будет проблематично переадресовать его на localhost и/или на другой порт (например, на порт 3425). Ибо в JS-коде, выполняющем AJAX, будут, скорее всего, прописаны совсем не эти домен и порт. Кстати, именно по этой причине многие прокси-серверы, имеющиеся в интернете, не дают возможность открытой веб-странице направлять AJAX-запросы. Ибо для их работоспособности (при использовании первого и второго путей) придется, по сути, писать собственный интерпретатор JS. А это задача - я Вам скажу...

По идее, можно, конечно, настроить переадресацию через iptables. Можно также добавить на открытую веб-страницы JS-код, который будет подменять нужным образом домен и порт AJAX-запроса. Также можно прислать от нашего клиента браузеру поддельный заголовок о разрешении технологии CORS. Кстати, это – достаточно интересная задача. Но, мы этим пока заниматься не будем, поступим проще: укажем браузеру принудительно соединяться с веб-серверами через локальный прокси-сервер. Для этого открываем в браузере

Настройки -> Параметры сети -> Настроить -> Ручная настройка прокси.

В НТТР-прокси указываем локальный IP-адрес: 127.0.0.1, порт: 3425. Ниже в «Не использовать прокси для», напротив, убираем запись, содержащую этот IP-адрес. Т.е. получится что-то примерно такое:

Настройка прокси для Firefox

После чего – запускаем клиент, затем – пытаемся открыть в браузере страницу ЛЮБОГО сайта (главное, чтобы он открывался по протоколу НТТР, а не HTTPS, ведь мы настроили браузер на работу через прокси-сервер только для HTTP). В окне страницы увидим заголовки, отправленные браузером. Например, для страницы http://citforum.ru/programming/unix/sockets/ вместо ее содержимого (как было обычно) увидим:

  • GET http://citforum.ru/programming/unix/sockets/ HTTP/1.1
  • Host: citforum.ru
  • User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0
  • Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  • Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3
  • Accept-Encoding: gzip, deflate
  • Referer: https://www.google.com/
  • Connection: keep-alive
  • Upgrade-Insecure-Requests: 1
  • Cache-Control: max-age=0

Т.е. локальный прокси-сервер (serv1.c), отвечая браузеру, вернул ему отправленные им (браузером) заголовки. Они же, кстати, появятся и в консоли, в которой запущен прокси-сервер. Выглядеть все это будет примерно так:

Результат запроса прокси-сервера

Обратите внимание, что URL запроса – http://www.4846d.ru, тогда как удаленный IP-адрес – 127.0.0.1:3425. Т.е. браузер считает, что страница 4846d.ru находится на локальном адресе и http-служба (протокол) открыла на прослушивание по этому адресу порт под номером 3425.

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

Отметим также, что настройки прокси, сделанные в браузере, очевидно, не повлияют на работу всех остальных программ (в том числе, и программы client.c). Поэтому они будут способны посылать запросы в сеть интернет, как обычно.

2. Выполняем прокси-сервер из кодов клиента и сервера

Итак, у нас с Вами получилось, с одной стороны, послать запрос программой клиентом на сервер и получить ответ; с другой стороны, получилось получить ответ заголовки, отсылаемые браузером – в целях соединения с веб-сервером. Осталось только соединить обе программы в одно целое и получить прокси-сервер. Вот что у меня получилось:

  1. // Виртуальный прокси-сервер: посредник между браузером и веб-сайтами по протоколу НТТР  
  2. #include <iostream>
  3. #include <regex>
  4. #include <cstring>
  5. #include <sys/types.h>
  6. #include <sys/socket.h>
  7. #include <stdlib.h>
  8. #include <stdio.h>
  9. #include <unistd.h>
  10. //#include <fcntl.h>
  11. #include <sys/stat.h>
  12. #include <sys/types.h>
  13. #include <netinet/in.h>
  14. #include <arpa/inet.h>
  15. #include<netdb.h>  //hostent
  16. //#include <string.h>

  1. using namespace std;

  1. // char message[] = "Server iteration: ";
  2. // char message1[] = "GET /test.html HTTP/1.1\r\nHost: 4846d.ru\r\nUser-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3\r\nAccept-Encoding: gzip, deflate\r\nConnection: close\r\nUpgrade-Insecure-Requests: 1\r\nCache-Control: max-age=0\r\n\r\n";
  3. unsigned int sz;
  4. char *client(char*, char*);

  1. int main()
  2. {
  3. int sock, listener;
  4. struct sockaddr_in addr;
  5. char buf[2048];
  6. int bytes_read;
  7. listener = socket(AF_INET, SOCK_STREAM, 0);
  8. if(listener < 0)
  9. {
  10. perror("socket");
  11. exit(1);
  12. }

  1. //  Устанавливаем сокету параметр SO_REUSEADDR или SO_REUSEPORT, чтобы после прекращения работы программы порт не использовался системой и был готов для последующего использования. Полезно, когда программу нужно запустить вновь СРАЗУ после того, как она прекратила работу в предыдущий раз.
  2. int reuse = 1;
  3. if (setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)) < 0)
  4. perror("setsockopt(SO_REUSEADDR) failed");
  5. #ifdef SO_REUSEPORT
  6. if (setsockopt(listener, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuse, sizeof(reuse)) < 0)
  7. perror("setsockopt(SO_REUSEPORT) failed");
  8. #endif

  1. addr.sin_family = AF_INET;
  2. addr.sin_port = htons(3425);
  3. addr.sin_addr.s_addr = htonl(INADDR_ANY);
  4. if(bind(listener, (struct sockaddr *)&addr, sizeof(addr)) < 0)
  5. {
  6. perror("bind");
  7. exit(2);
  8. }
  9. listen(listener, 1);

  1. // Создаем новый временный файл  
  2. FILE* fp; FILE* fp1;
  3. if((fp=fopen("file_tmp", "wb"))==NULL)
  4. { // Вначале уничтожаем этот файл (во избежание возможных остатков старого содержимого), если он есть, попутно проверяя, можно ли его создать и открыть
  5. printf("Ошибка при открытии файла.\n");
  6. exit(1);
  7. }
  8. fclose(fp);
  9. // Создаем еще один новый временный файл
  10. if((fp1=fopen("file1_tmp", "wb"))==NULL)
  11. {
  12. printf("Ошибка при открытии файла1.\n");
  13. exit(1);
  14. }
  15. fclose(fp1);

  1. int i =0;   // Счетчик итераций прокси-сервера
  2. char *buf_changed; char *buf_from_site;

  1. while(1)
  2. {i++;
  3. sz=sizeof(addr);
  4. sock = accept(listener, (struct sockaddr *)&addr, &sz );
  5. printf ("\nБраузерный запрос %d:\n", i);
  6. if(sock < 0) {perror("accept"); exit(3);}

  1. buf_changed = (char*)malloc(sizeof(char) * 10000); // Это опасно!
  2. buf_from_site = (char*)malloc(sizeof(char) * 1000000); // Это опасно!

  1. bytes_read = recv(sock, buf, sizeof(buf), 0);
  2. if(bytes_read <= 0)
  3. {printf("bytes_read= %d ;\n while(1) was broken\n", bytes_read);
  4. break;}

  1. printf("buf (headers from Brauzer):\n%s \n\n", buf);

  1. // Подменяем заголовок, полученный от браузера: Connection: keep-alive -> Connection: close
  2. string str = "", ex, str_to_change;
  3. regex header("Connection:\\s*keep-alive"); // Что такое и для чего \\s*   ?

  1. str_to_change = "Connection: close";
  2. ex = regex_replace(buf, header, str_to_change);  // Подменяем заголовок
  3. ex = ex.string::substr(0, (int)ex.find("\r\n\r\n")+4); // Берем только полезную часть сообщения, отбрасываем вспомогательные байты. Кстати, если компилировать при помощи не g++, а gcc, этих вспомогательных байтов не будет (правда, с gcc не получится, т.к. используем регулярные выражения и функцию find() ). Почему?... открытый вопрос. Равно как и то, что это за байты. Похоже, их добавляет веб-сервер в конец каждого фрагмента (чанка - chunk) сообщения. Но, почему это делается только если программа скомпилирована на g++? Непонятно.
  4. buf_changed = (char*)ex.c_str(); // Преобразуем в char (т.к. С++)
  5. printf("buf_changed (headers to web-server):\n__________\n%s \n__________\n", buf_changed);

  1. fp=fopen("file_tmp", "ab"); // Записываем в файлы с добавлением, каждый раз начиная с конца данных, записанных в предыдущий раз
  2. fp1=fopen("file1_tmp", "ab");   // Что означают параметры “ab” ?
  3. fputs("\nChanged headers from brauzer (buf_changed):\n", fp);
  4. fputs("\nChanged headers from brauzer (buf_changed):\n", fp1);

  1. fwrite(buf_changed, strlen(buf_changed), 1, fp);
  2. fwrite(buf_changed, strlen(buf_changed), 1, fp1);
  3. fclose(fp);
  4. fclose(fp1);

  1. string host_str; // = "4846d.ru";
  2. // Ищем в заголовках сообщения, которое будем отсылать веб-серверу, имя хоста
  3. regex host_reg("Host:\\s*(.*?)\\r\\n");
  4. std::cmatch cm;
  5. std::regex_search(buf_changed, cm, host_reg);
  6. // Удаляем из найденного фрагмента символы Host: и максимальное количество пробелов
  7. regex host_reg1("Host:\\s*");
  8. string cm_str = cm[0] ; // Берем самое первое совпадение (ибо последующие совпадения могут оказаться не в заголовках, а в теле запроса, к примеру)
  9. char* cm_char = (char*)cm_str.c_str();
  10. host_str = regex_replace(cm_char, host_reg1, ""); // Здесь и удаляем

  1. cm_char =  (char*)host_str.c_str();
  2. regex host_reg2("\\r|\\n");  
  3. host_str = regex_replace(cm_char, host_reg2, "");  // А здесь удаляем перенос(ы) строк

  1. char* host = (char*)host_str.c_str(); // Имя хоста (извлеченное из заголовков, посылаемых веб-серверу), например, 4846d.ru

  1. // ***********************************************  
  2. // Посылаем заголовки веб-серверу и считываем то, что он вернет в ответ
  3. buf_from_site = client(buf_changed, host); // Функция client определена ниже

  1. // Отсылаем то, что получили от веб-сервера, браузеру
  2. send(sock, buf_from_site, strlen(buf_from_site), 0);

  1. // free(buf_changed);
  2. memset (buf, 0, sizeof(buf));

  1. close(sock);
  2. printf ("sosk  is closed.");  
  3. } // Конец "бесконечного" цикла

  1. printf ("free: buf_changed, buf_from_site... \n");  
  2. free(buf_changed);
  3. free(buf_from_site);
  4. printf ("Stop.\n");
  5. return 0;
  6. }

  1. // ******************************************************
  2. char *client(char *message1, char* host) {
  3. // char message1[] = "GET / HTTP/1.1\r\nHost: 4846d.ru\r\nUser-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3\r\nAccept-Encoding: gzip, deflate\r\nConnection: close\r\nUpgrade-Insecure-Requests: 1\r\nCache-Control: max-age=0\r\n\r\n";
  4. char buf1[1024];
  5. memset(buf1, 0, sizeof(buf1));

  1. int fs, fr=0;
  2. int sock1;
  3. struct sockaddr_in addr;
  4. sock1 = socket(AF_INET, SOCK_STREAM, 0);
  5. if(sock1 < 0)
  6. {
  7. perror("socket");
  8. exit(1);
  9. }

  1. //  Устанавливаем сокету параметр SO_REUSEADDR или SO_REUSEPORT, чтобы после прекращения работы программы порт не использовался системой и был готов для последующего использования. Полезно, когда программу нужно запустить вновь СРАЗУ после того, как она прекратила работу в предыдущий раз.
  2. int reuse = 1;
  3. if (setsockopt(sock1, SOL_SOCKET, SO_REUSEADDR, (const char*)&reuse, sizeof(reuse)) < 0)
  4. perror("setsockopt(SO_REUSEADDR) failed");
  5. #ifdef SO_REUSEPORT
  6. if (setsockopt(sock1, SOL_SOCKET, SO_REUSEPORT, (const char*)&reuse, sizeof(reuse)) < 0)
  7. perror("setsockopt(SO_REUSEPORT) failed");
  8. #endif  

  1. addr.sin_family = AF_INET;
  2. addr.sin_port = htons(80); //По умолчанию, сервер открывает, как правило, именно порт под номером 80 для службы НТТР. Да и браузер по умолчанию также обращается именно к этому порту при открытии страницы по протоколу НТТР.
  3. //addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

  1. // string host_str = "4846d.ru";  
  2. // host =  (char*)host_str.c_str();

  1. struct hostent* hostSITE = gethostbyname(host);
  2. addr.sin_addr.s_addr = ((struct in_addr*)hostSITE->h_addr_list[0])->s_addr;

  1. if(connect(sock1, (struct sockaddr *)&addr, sizeof(addr)) < 0)
  2. {
  3. perror("connect");
  4. exit(2);
  5. }
  6. fs=send(sock1, message1, strlen(message1)*sizeof(char), 0);
  7. printf (" BEGIN client: \n\n");
  8. // char buf_all[1000000];
  9. char *buf_all;
  10. buf_all = (char*)malloc(1000000); // Выделяем память в куче (с некоторым запасом, это НЕоптимально), чтобы при выходе из этой функции возвращаемое ею значение не терялось. Если для buf_all не выделить динамическую память (если вместо этого, к примеру, инициализировать массив buf_all[100000]), она будет, в качестве локальной переменной, содержаться в стеке. При возврате из функции значение этой переменной  может быть потеряно (по истечению времени жизни локальных переменных).
  11. //  char* buf_new;
  12. //  buf_new = (char*)malloc(1000000);

  1. FILE* fp; FILE* fp1;
  2. // Записываем во временный файл  
  3. fp=fopen("file_tmp", "ab"); // Записываем в файл с добавлением, каждый раз начиная с конца данных, записанных в предыдущий раз
  4. fputs("\nServer response from site:\n", fp);
  5. fp1=fopen("file1_tmp", "ab");
  6. fputs("\nServer response from site:\n", fp1);

  1. size_t fn=1;  

  1. while((fr=recv(sock1, buf1, sizeof(buf1), 0)) > 0) // Почему нельзя сделать >=0 ?
  2. { //  strcpy(buf_new, buf1);
  3. buf_all = strncat(buf_all, buf1, fr); // Опасная функция! Т.к. переменная buf_all может неконтролируемо увеличиться!
  4. printf("fr=%d  strlen(buf1)=%lu  strlen(buf_all)=%lu \n", fr, strlen(buf1), strlen(buf_all));

  1. fn=fn+fr; // Счетчик числа полученных от веб-сервера байтов при очередном запросе
  2. fwrite(buf1, fr, 1, fp1);
  3. memset(buf1, 0, sizeof(buf1));
  4. }

  1. printf("Finally:\nfr=%d  strlen(buf1)=%lu  strlen(buf_all)=%lu \n", fr, strlen(buf1), strlen(buf_all));

  1. fwrite(buf_all, fn, 1, fp);  // А теперь записываем весь ответ сервера целиком. Проверьте файлы file_tmp  и  file1_tmp - они могут различаться, в частности, при получении изображений. Почему?
  2. fclose(fp);
  3. fclose(fp1);

  1. close(sock1);
  2. printf (" FINISH client \n________________________________\n");
  3. return buf_all;
  4. }

Примечание 1. С библиотеками, возможно, перебор, я особо не проверял. По идее, надо бы лучше отказатся от использования регулярных выражений (regex), тогда все будет работать, скажем так, побыстрее, ибо размер исполняемого файла снизится раз в 100. Правда, если потребуется многочисленная замена (подмена) много чего на открываемой через прокси-сервер вебстранице, вот тогда регулярные выражения существенно сэкономят время разработки. Впрочем, если таких замен будет еще больше, тогда все же лучше забыть о них (в С/С++) и написать свою универсальную функцию. Быстрее будет работать.

Примечание 2.  По идее, эту программу, с некоторыми корректировками, сделанными в целях безопасности, вполне можно использовать не только у себя на локальном компьютере, но и на сервере – в целях организации личного прокси-сервера. В целях тестирования (только) это возможно, нужен лишь хостинг, который разрешит ее запуск. Ну, и браузер необходимо будет настроить с учетом IP-адреса, по которому будет располагаться эта программа. Кроме того, см. Примечания 3, 4.

Примечание 3. Эта программа вполне рабочая, НО абсолютно не оптимизированная и опасная для реального использования на практике, так как подвержена переполнению памяти. Если попадется нехороший сервер, он нам может на ТСР-соединение отправить ТАКУЮ (большую и каверзную) строку, что возникнет переполнение памяти (например, в куче) и программа завершит работу с ошибкой сегментации. После чего удаленный компьютер на вебсервере может получить контроль над нашим компьютером. Поэтому, если возникнет желание воспользоваться программой для реализации собственного прокси-сервера, потребуется ее оптимизация, по крайней мере, в плане защищенности. Без этого я запрещаю использовать эту программу на практике, за исключением, разве что, аккуратного тестирования в виртуальной безопасной среде.

Примечание 4. Этот прокси-сервер способен работать только по протоколу НТТР. Если Вам потребуется другой протокол, например, HTTPS, необходимо будет доработать его. Дело в том, что при соединении по протоколу HTTPS потребуется, как минимум, шифрование сообщений, подтверждение цифровой подписи HTTPS-протокола, который применяется тем или иным сервером. Без этого соединение не состоится. А для этой цели необходимо будет использовать специальную библиотеку (например, openSSL). Может быть, я рассмотрю этот момент позже. Если сделать корректировки, обеспечивающие безопасность, реализовать шифрование SSL (для связи по протоколу HTTPS), то получится более-менее полноценный прокси-сервер.

С учетом вышесказанного, скомпилируйте программу в стандарте С++11:

g++ -o serv1 serv1.c -std=c++11

Ну, и запустите. Программа должна оставаться в режиме ожидания (если только браузер вдруг не вздумает отправлять какой-нибудь НТТР-трафик – тогда его заголовки будут показываться в консоли).

Скрыть пояснение
Например, я иногда встречал ситуацию, когда браузер Firefox 63, при отладке прокси-сервера, отправлял и такой заголовок:
Host: safebrowsing.googleapis.com:443

После чего в браузере (я использовал Firefox 63) откройте, например, страницу http://4846d.ru. Можно непосредственно ввести этот адрес в строке запроса браузера, можно перейти посылке откуда-нибудь или т.п., разницы нет. И – вот что увидим:

Ответ прокси-сервера

Браузер успешно загрузил, путем GET-запросов, все ресурсы, которые имеются на странице. Вначале он загрузил сам код html-страницы. Затем, проводя синтаксический ее анализ, он обнаруживал там ссылки для загрузки дополнительных ресурсов, в частности, JS-скриптов и CSS-файлов. Для этого он каждый раз инициировал ТСР-соединение (которое, согласно его прокси-настройкам, направлялось на IP-адрес 127.0.0.1:3425, попадая к нашей программе, так как она уже к этому времени открыла слушающий сокет на ТСР-порту 3425). Наш прокси-сервер, в свою очередь, принимал это соединение и выводил в консоль заголовки, полученные от браузера: buf (headers from brauzer).

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

Connection: keep-alive.

Однако, заголовок-то этот попадет, вначале, нашему прокси-серверу. Который, соответственно, установит постоянное соединение. При этом цикл приема сообщение (строчки 161…168) не будет прерываться до тех пор, пока веб-сервер не разорвет соединение или пока не пришлет пустое сообщение (т.е. пока функция recv() в строчке 161 не получит нулевое или отрицательное значение). Однако, пока действует постоянное ТСР-соединение, веб-сервер, скорее всего, не будет этого делать.

Именно для этой цели мы в программе подменяем этот заголовок на

Connection: close

Это дает команду веб-серверу – разорвать соединение сразу после того, как будет передано ВСЁ сообщение. Поэтому для реализации последующих запросов браузер будет вынужден вновь устанавливать соединения (т.е. для каждого скачивания ресурса проходить процесс «тройного рукопожатия», а уже потом давать серверу запрос именно на скачивание конкретного ресурса).

Да, это очень неэкономично, в чем Вы, видимо, уже убедились, скачивая страницу через проски-сервер: скорость загрузки страницы ниже, чем обычно, раз в 3…5. Зато, программа является концептуально простой. Можно, разумеется, использовать здесь, например, неблокирующие сокеты (тогда возможно использование постоянного ТСР-соединения), но, это усложнит программу, хотя и сделает ее ближе к реальным схемам прокси-серверов.

Факт подмены заголовка виден на рисунке (выделено белым цветом). Именно для этого в программе использованы регулярные выражения (см. строчки 3, 84…86). Остальные заголовки неизменны, хотя, при желании, можно также легко изменить и их, перед тем, как передать веб-серверу – никто – ни браузер, ни веб-сервер ничего не заметят. Собственно, на то он и есть – прокси-сервер, чтобы давать возможность подобных замен (или подмен – как правильно-то).

Пояснения по программе прокси-сервера

Основная часть программы Вам, наверное, уже очевидна, если Вы внимательно отнеслись к предыдущим заданиям. Остановимся на некоторых не сразу очевидных аспектах. В программе создаются два файла с именами file_tmp и file1_tmp. Они создаются заново (см. строчки 52…66) при каждом очередном запросе (происходящем уже ПОСЛЕ того, как в очередной раз успешно произойдет «тройное рукопожатие») браузера к нашему прокси-серверу, т.е. при каждом скачивании того или иного ресурса (html-кода, JS-кода, CSS и т.д.). Т.е. в обоих файлах будут хранится, своего рода, логи запроса, где будут присутствовать:

  • заголовки браузера, отосланные прокси-сервером на веб-сервер,
  • заголовки веб-сервера, полученные прокси-сервером и переданные браузеру,
  • содержимое ответов веб-сервера (т.е. содержимое скачиваемого ресурса).

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

Однако, запись в эти файлы происходит немного по-разному. В file_tmp запись производится в том числе, в рамках каждой итерации цикла в строчках 161…168. Тогда как в файл file1_tmp запись производится сразу «оптом», в частности, командой в строчке 170, уже после окончания работы цикла. А предварительная запись строчками 92…95 производится одинаково в обоих файлах. Указанное различие обуславливает, определенно, разные логи в файлах, в частности, когда речь идет о загружаемых изображениях (favicon.ico). Проанализируйте этот момент – почему так происходит.

Программа состоит из основной части (main() ) и одной функции (client() ). Функция предназначена для отправки заголовков браузера веб-серверу (строчка 147) и получения от него ответа (строчка 161). Так как ответ может быть достаточно большим, несмотря на фрагментированность (заголовок Transfer-Encoding: chunked), поэтому целесообразно производить считывание в цикле. Повторимся, если бы мы не подменили заголовок Connection: keep-alive на Connection: close, даже после того, как веб-сервер окончил передачу очередного сообщения (т.е. передал бы все его фрагменты – чанки), цикл в строчке 161 так и оставался бы в ждущем режиме (именно так и обстоит дело в программе client.c, см. выше). Ибо использован обычный, т.е. блокирующий сокет. А вследствие подмены, как только передача сообщения прекращена, веб-сервер посылает нам признак окончания сообщения и цикл в строчке 161 прекращается.

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

Примечание. Следует ОБЯЗАТЕЛЬНО разобраться с необходимостью выделения именно динамической памяти (в куче) для переменных buf_all и buf_from_site. Вроде бы, первая-то из них используется в качестве локальной переменной в функции и передается по значению. Будет ли работать программа, если НЕ использовать динамическое выделение памяти этой переменной, да еще за пределами функции (т.е. в основной программе)? Если будет, то какие проблемы могут возникать? Ведь в языках типа РНР, Javascript мы бы так точно не делали – передали бы из функции значение переменной, да и все. А здесь «зачем-то» такие вот сложности…

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

После чего осталось лишь передать браузеру то, что мы получили в конце очередного запроса к веб-серверу, что и делается в строчке 102. При этом браузер считает, что все данные пришли в соответствии с его ожиданиями (которые он обозначил в своих заголовках). Однако, сервер вернул заголовок Connection: close (в ответ на подмененный нами соответствующий заголовок), что означает, что после получения очередного запроса браузеру, если он захочет скачать с сервера что-то еще, потребуется вновь устанавливать ТСР-соединение. Вот, наверное, и все.

Наконец, посмотрим, для примера, заголовки первого запроса, в процессе которого скачивался код-html с веб-сервера:

Заголовки запроса и ответа

Видны заголовки ответа (вверху) и запроса (внизу). Видим также, что браузер предложил Connection: keep-alive, однако веб-сервер вернул Connection: close (кстати, это – не нарушает правили HTTP-протокола). В качестве удаленного адреса фигурирует 127.0.0.1:3425. Иными словами, именно этот IP-адрес соответствует доменному имени и URL запроса, в связи с прокси-настройками браузера.


Задание для самостоятельной разработки
  1. Освойте работу прокси-сервера (только, еще раз, аккуратнее, чтобы не занести к себе на компьютер вирус и чтобы не создать проблем себе или кому-либо),
  2. Разбейте (мысленно) программу на отдельные функциональные блоки, проанализируйте работу каждого из них,
  3. В требующихся местах ликвидируйте возможности возникновения опасных ситуаций (ведущих к переполнению памяти). Это может быть достигнуто, в частности, путем проверки размера строк, посылаемых браузером и приходящих от сервера, т.е. путем обработки ситуаций, как ошибочных в случае, если размер строки превысит максимально установленное.
  4. Попробуйте применить неблокирующиеся сокеты вместо обычных (блокирующих). Этим Вы сделаете возможным существование установленного ТСР-соединения между браузером, прокси-сервером и веб-сервером даже после того, как вся веб-страница будет скачана, вместе со всеми подключенными к ней ресурсами. Это снизит время отправки AJAX-запроса и, соответственно, повысит скорость получения ответа. Также снизится и время загрузки страницы. Но, программа может усложниться в связи с тем, что при открытии веб-страницы запросы могут делаться на разные домены. В таких случаях использоваться один и тот же неблокирующийся сокет, открытый по протоколу TCP и адресованный на другой домен, не получится. Для обращения к ресурсам с других доменов, при условии использования ТСР-соединения в режиме keep-alive, придется создавать новые неблокирующиеся сокеты.

С уважением, Салимоненко Д.А.