Пример примитивного веб сервера на С++ с использованием OpenSSL

Давно хотел написать полезную статью и вот наконец нашёл подходящий информационный повод.

Речь в этой статье пойдёт о создании примитивного web сервера работающего по https протоколу. Мы на пишем серверную часть, а в качестве клиентской части будет выступать любой из браузеров.
В итоге мы получим максимально упрощённый пример примитивного web сервера который можно будет улучшать и затачивать под свои задачи.

Для начала нам потребуются файлы сертификатов. Процесс генерации сертификата прост.
$ openssl req -new -x509 -days 30 -keyout server.key -out server.pem

На вопрос «Enter PEM pass phrase:» отвечаем паролем, подтверждаем и запоминаем.
На вопрос «Common Name (eg, YOUR name) []:» отвечаем именем сайта, для которого создаем сертификат.
Все остальные ответы не особо важны.

После ответов в текущей появятся два новых файла — server.key и server.pem (ключ и сертификат, соответственно).

Для удобства снимаем пароль с ключа:
$ cp server.key server.key.orig
$ openssl rsa -in server.key.orig -out server.key
$ rm server.key.orig

Теперь код. Код основан на примерах с официального сайта OpenSSL, но только я его постарался упросить. Да и простое копирование и компиляция примера у меня по какой то из причин не работало.

Подключение библиотек затруднений вызвать не должно.
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netdb.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

#include <openssl/ssl.h>
#include <openssl/err.h>
 
extern BIO *bio_err;
BIO *bio_err=0;

#define PEM_FILE "server.pem"
#define KEY_FILE "server.key"
#define PORT	4333

Функция создания сокета тоже простая. Тем более не содержит пока ничего из OpenSSL.
/**
 * Создаёт самый обычный сокет
 */
int tcp_listen()
{
    int sock;
    struct sockaddr_in sin;
    int val=1;

    if((sock=socket(AF_INET,SOCK_STREAM,0))<0)
      printf("Couldn't make socket");

    memset(&sin,0,sizeof(sin));
    sin.sin_addr.s_addr=INADDR_ANY;
    sin.sin_family=AF_INET;
    sin.sin_port=htons(PORT);
    setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&val,sizeof(val));

    if(bind(sock,(struct sockaddr *)&sin,
      sizeof(sin))<0)
      printf("Couldn't bind");
    listen(sock,5);

    return(sock);
}

Функция инициализации ssl. Буду тщательно комментировать.
 /* Функция для обработки сигнала SIGPIPE который нам может послать ОС при попытки записи в закрытое соединение*/
static void sigpipe_handle(int x){}

SSL_CTX *initialize_ctx(const char *key_file,const char *pem_file)
{
    if(!bio_err)
    {
      /* Глобальная инициализация алгоритмов OpenSSL, без неё не обходится не один пример по OpenSSL */
      SSL_library_init();
      SSL_load_error_strings();

      /* An error write context */
      bio_err=BIO_new_fp(stderr,BIO_NOCLOSE);
    }

    /* Set up a SIGPIPE handler */
    /* Назначаем обработчик сигнала SIGPIPE который нам может послать ОС при попытки записи в закрытое соединение*/
    signal(SIGPIPE,sigpipe_handle);


    /* Create our context*/
    SSL_CTX* ctx=SSL_CTX_new(SSLv23_method());

    /* Load our keys and certificates*/
    if(!(SSL_CTX_use_certificate_file(ctx,pem_file,SSL_FILETYPE_PEM)))
    {
        printf("Не удалось загрузить файл сертификата\n");
    }

    if(!(SSL_CTX_use_PrivateKey_file(ctx, key_file,SSL_FILETYPE_PEM)))
    {
        printf("Не удалось загрузить файл ключей\n");
    }

    return ctx;
}

Функция initialize_ctx один раз в начале загружает наши файлы и создаёт контекст SSL_CTX. Он будет использоваться при создании отдельных соединений. Это позволяет не загружать файлы ключей для каждого нового соединения.

Теперь рассмотрим функцию работы с соединением.
  #define BUFSIZZ 4048
static int http_serve(SSL *ssl,int s)
{
    char buf[BUFSIZZ];
    int r = 0;
    int e;
   
    bzero(buf,BUFSIZZ-1); // Очистка буфера
    r=SSL_read(ssl,buf,BUFSIZZ-1); // Чтение данных
    if(r<0) // Если r < 0 то произошла ошибка
    {
        e = SSL_get_error(ssl,r);
    }
    printf("[Длина принятого текста %d, Error:%d]%s\n", r, e, buf);
   
    /* Запись ответа */
    r = SSL_write(ssl,"HTTP/1.0 200 OK\r\nServer: EKRServer\r\n\r\nServer test page\r\n",strlen("HTTP/1.0 200 OK\r\nServer: EKRServer\r\n\r\nServer test page\r\n"));
    if(r<=0)  // Если r < 0 то произошла ошибка
    {
        printf("Write error %d\n",r);
    }
    else
    {
        printf("Write ok %d\n",r);
     }
    
    /* Закрытие соединения */
    shutdown(s,1);
    SSL_shutdown(ssl);

    SSL_free(ssl);
    close(s);

    return(0);
}

Думаю тут всё очевидно.
И последний этап, соединяем всё вместе.
int main(int argc, char *argv[])
{
    int sock,s;
    SSL_CTX *ctx;
    SSL *ssl;
    int r;

    // Build our SSL context 
    ctx=initialize_ctx(KEY_FILE,PEM_FILE);

    sock=tcp_listen();
    printf("\n");

    while(1)
    {
        if((s=accept(sock,0,0))<0)
        {
            printf("Problem accepting\n");
        }
        else
        {
            printf("Accepting %d\n",s);
        }

        ssl=SSL_new(ctx);
        SSL_set_fd(ssl, s);
        r=SSL_accept(ssl);
        if( r < 0 )
        {
            printf("SSL accept error %d\n",r);
            printf("SSL accept error code %d\n",SSL_get_error(ssl,r) );
            exit(0);
        }
        else
        {
            printf("SSL accept %d\n",r);
        }

        http_serve(ssl,s);
        printf("\n");
    }

    SSL_CTX_free(ctx);
}

Из кода я постарался убрать всё то без чего будет работать. Но не смотря на это, как я и ожидал, кода получилось больше чем текста. Но я очень надеюсь что кому то эта статья поможет.
Я всё писал под Ubuntu, возможно при компиляции в windows придётся внести незначительные правки.

В довесок добавлю ссылку на примеры по OpenSSL
Share post

Comments 12

    –2
    А зачем?
      0
      web-панель администрирования для корпоративного софта с защищенным подключением по локальной сети?
        +2
          0
          Да, им и пользуюсь. Только вот Хабра — это концентрат клевости и мануальности, а не ссылочности и «зачем»ности ))
            –1
            Что клевого в велосипедах? (не в смысле средства передвижения)
              +2
              А мне порой нравятся велики, потому что они позволяют понять как можно ту или иную вещь запустить и потом уже использовать в своих проектах.
                0
                Чтобы понять, как работает велосипед, вам обязательно собирать свой с нуля?
                  +2
                  Всегда есть альтернатива не пользоваться им, или я не понимаю вашего недовольства.

                  Я, например, предпочел бы собрать велосипед, если это не займет много времени, чем начинать гуглить мануалы как сбилдить Tomcat и ставить повсюду точки останова.

                  Чем меньше уровень вхождения в новый API, тем легче с ним начинать работать.

                  Я, когда смотрел что такое node.js, был очень доволен, когда увидел небольшой пример примитивного веб сервера в несколько строк. Это как HelloWorld, если вы ешё помните такое. Мне, как новичку, это было бы гораздо интереснее.
                    0
                    Чем вам не угодили родные примеры?
                      +1
                      я вижу, вы не любите неродныемного примеров. Кстати, на офф. сайте две очевидные ссылки с корнями HowTo и Cookbook у меня, увы, не открываются :(.
              +1
              > концентрат клевости и мануальности
              Это вы так хотите (все хотят), но мануальности и клевости на Хабре действительно маловато, больше идиотизма и бесполезности.
        +1
        Я для похожих целей использовал Mongoose: code.google.com/p/mongoose/,
        УПС. Только заметил, линк уже дали :)

        Only users with full accounts can post comments. Log in, please.