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

в 13:58, , рубрики: c++, HTTPS, openssl, SSL, Веб-разработка, Песочница, С++, метки: , , ,

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

Речь в этой статье пойдёт о создании примитивного 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]%sn", r, e, buf);
   
    /* Запись ответа */
    r = SSL_write(ssl,"HTTP/1.0 200 OKrnServer: EKRServerrnrnServer test pagern",strlen("HTTP/1.0 200 OKrnServer: EKRServerrnrnServer test pagern"));
    if(r<=0)  // Если r < 0 то произошла ошибка
    {
        printf("Write error %dn",r);
    }
    else
    {
        printf("Write ok %dn",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 acceptingn");
        }
        else
        {
            printf("Accepting %dn",s);
        }

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

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

    SSL_CTX_free(ctx);
}

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

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

Автор: Levhav

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js