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

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

ЗАДАНИЕ 9: Простой https-клиент (С++, LINUX)

Введение

↑ К содержанию ↑

В этом задании Вы можете потренироваться направлять сетевой запрос на какой-нибудь сайт по протоколу https. Ранее, например в задании 5. Вы уже пробовали делать сетевой запрос, но он осуществлялся в формате протокола HTTP. Сейчас же будет использоваться протокол HTTPS. Его отличие от НТТР состоит в том, что передаваемые данные зашифровываются при помощи того или иного алгоритма криптошифрования.

Итак, уточняю: по сути, как и ранее, будет использоваться тот самый, уже хорошо известный нам протокол ТСР, который в качестве передаваемого параметра (строки) будет содержать в себе заголовки запроса (нашей программы) или ответа (сервера) + тело его ответа (html-код отдаваемой сервером страницы). Только если при использовании HTTP все это передается в читаемом виде, то в случае HTTPS и заголовки, и тело ответа будут зашифрованы. Шифрование запроса (нашей программы) и дешифрование (ответа сервера) будет осуществлять операционная система с использованием библиотеки openssl. Она, как правило, входит в дистрибутивы Linux, в частности, в Ubuntu.

Исходный текст (код) программы на языке С++:

  1. // https.cpp
  2. // Компиляция: g++ -Wall -o https https.cpp -L/usr/lib -lssl -lcrypto -std=c++11
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. #include <sys/socket.h>
  6. #include <netdb.h>
  7. #include <netinet/in.h>
  8. #include <openssl/ssl.h>
  1. int main() {  
  2. char hostname[]="site.ru";
  3. int port = 443; // Если бы был протокол HTTP, использовался бы (как правило) порт 80
  1. SSL_library_init(); // Инициализируем библиотеку openssl
  2. OpenSSL_add_all_algorithms(); // Загружаем алгоритмы криптошифрования для этой библиотеки
  3. SSL_load_error_strings(); // Регистрируем сообщения об ошибках
  4. const SSL_METHOD *method = SSLv23_client_method(); // Создает новый метод для клиента
  5. SSL_CTX *ctx = SSL_CTX_new(method); // Создает новый контекст
  6. if (ctx == NULL)
  7. return -1;
  1. struct hostent *host;
  2. struct sockaddr_in addr;
  1. if ((host = gethostbyname(hostname)) == NULL)
  2. return -1;
  3. int sock = socket(PF_INET, SOCK_STREAM, 0); // Как обычно, будем использовать потоковый протокол транспортного уровня ТСР
  4. //  bzero(&addr, sizeof(addr)); // Можно использовать, для дополнительной уверенности, очистив структуру addr; но, необязательно
  5. addr.sin_family = AF_INET;
  6. addr.sin_port = htons(port);
  7. addr.sin_addr.s_addr = *(long *)(host->h_addr);
  8. if (connect(sock, (struct sockaddr*)&addr, sizeof addr))
  9. return -1;
  1. SSL *ssl = SSL_new(ctx); // Создает новое SSL-соединение
  2. SSL_set_fd(ssl, sock); // Прикрепляет дескриптор сокета
  3. if (SSL_connect(ssl) == -1) // Осуществляет соединение
  4. return -1;

  1. char msg[] = "GET https://site.ru HTTP/1.0\n\n";
  2. char buf[1024];
  3. char* buf_sum;
  4. buf_sum = (char*)malloc(sizeof(char) * 2000);
  5. int i=0;  
  1. SSL_write(ssl, msg, strlen(msg));
  1. FILE* fp;
  2. if((fp=fopen("file_tmp", "wb"))==NULL)
  3. { // Вначале уничтожаем этот файл (во избежание возможных остатков старого содержимого), если он есть, попутно проверяя, можно ли его создать и открыть
  4. printf("Ошибка при открытии файла.\n");
  5. exit(1);
  6. }
  7. fclose(fp);  
  8. // Записываем во временный файл  (туда запишется весь html-код страницы сервера, вызываемой по протоколу https
  9. fp=fopen("file_tmp", "ab"); // Записываем в файл с добавлением, каждый раз начиная с конца данных, записанных в предыдущий раз
  10. fputs("\nServer response from site:\n", fp);    
  1. long bytes_sum = 0, bytes;
  2. while ((bytes = SSL_read(ssl, buf, sizeof buf)) >0 ) {
  3. buf[bytes] = 0;
  4. printf(" strlen=%lu; ", strlen(buf_sum));
  1. if((buf_sum = (char*)realloc(buf_sum, strlen(buf_sum) + bytes +1))==NULL){ // Увеличиваем выделенную память на число байтов, полученных на этой итерации и добавляем еще 1 байт (для чего, кстати, добавлен этот байт?)
  2. perror("Allocation error2.");
  3. exit (0);
  4. }
  5. strncat(buf_sum, buf, bytes);
  6. i++;
  7. printf("%d ", i);
  8. fwrite(buf, strlen(buf), 1, fp); // Записываем в файл
  9. fflush(fp); // Принудительно делаем запись файла на жесткий диск - для надежности: если вдруг программа даст сбой и возникнет ошибка, в файле останутся хотя бы те байты, полученные на предыдущих итерациях. А иначе, если запись на жесткий диск не будет сделана до момента возникновения ошибки (сегментации), эти данные могут быть потеряны.
  10. }
  11. fclose(fp);  
  1. SSL_free(ssl);   // Освобождает SSL-соединение
  2. close(sock); // Закрывает сокет
  3. SSL_CTX_free(ctx); // Освобождает контекст
  1. printf("%s", buf_sum);
  2. free(buf_sum);
  3. printf("\nEnd.\n");
  4. return 0;
  5. }

Строчки, необходимые для реализации SSL-шифрования, выделены жирным шрифтом.

Компилировать программу следует так:

g++ -Wall -o https https.cpp -L/usr/lib -lssl -lcrypto -std=c++11

Вместо site.ru следует использовать имя какого-нибудь сайта, работающего по протоколу HTTPS.

Примечание. Такие сайты, как yandex.ru, google.com и некоторые другие, по всей видимости, требуют, чтобы запрос был корректным и содержал ВСЕ необходимые протоколы; поэтому при попытке доступа при помощи этой программы они выдадут что-то типа ошибки доступа. Поэтому для тестирования программы следует использовать другие сайты или сформировать полностью корректный GET-запрос.

Как видим, добавились опции -lssl и -lcrypto, необходимые для подключения библиотеки openssl. Кроме того, указан путь к этим модулям, так как по умолчанию компилятор не ищет библиотеки в каталоге usr/lib.

Как видим, программа весьма похожа на предыдущие сетевые программы, в которых использовались сокеты. Жирным шрифтом выделены места, которые добавлены для реализации криптошифрования на основе библиотеки openssl.

Что здесь нового?

Вначале задается порт номер 443. Это порт, который на подавляющем большинстве серверов в интернете открыт на прослушивание для соединений по протоколу HTTPS. Затем инициализируется библиотека openssl, загружаются алгоритмы криптошифрования и т.д. (строчки 12-18).

После этого, как обычно, создается ТСР-сокет, задаются его параметры и делается попытка установления соединения (строчки 23-29).

А вот далее (это также является новым) создается SSL-соединение (строчки 30-33).

В строчке 34, как обычно, задается строка GET-запроса. Конечно, такой «запрос» не является полным, его лучше бы дополнить недостающими заголовками. Откуда их взять? Наиболее простой способ - из заголовков запроса браузера к соответствующему сайту, как это делалось, к примеру, в задании 5.

Так как некоторые HTTPS-сайты отдают достаточно большой объем html-кода, вывод нашей программы в консоль может быть весьма объемным; поэтому целесообразно записать все, что получено от сервера, в файл (под названием file_tmp), что делается в строчках 61-62.

Также новыми являются строчки 65, 67, которые осуществляют завершение работы с библиотекой openssl.

Выводы

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

Примечание: Некоторые особо современные протоколы SSL-криптошифрования могут отсутствовать в старой версии библиотеки openssl. В этом случае придется обновить эту биллиотеку и/или саму операционную систему.


Самостоятельная разработка

↑ К содержанию ↑
  1. Изучите, скомпилируйте предложенный выше код, предварительно выбрав вместо site.ru какое-нибудь другие доменное имя.
  2. Весьма полезным будет просмотр зашифрованного трафика, который поступает в сеть от нашей программы (при осуществлении GET-запроса) и который поступает из сети от сервера. Для этого следует использовать утилиту tcpdump, которую нужно запускать с опцией -A.
  3. Сравните этот трафик с «обычным» НЕзашифрованным трафиком, который получается, например, в результате взаимодействия браузера и прокси-сервера Можете попробовать расшифровать https-трафик. После чего Вам станет более понятным назначение протокола HTTPS.

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