Как стать автором
Поиск
Написать публикацию
Обновить

Пишем «Hello, World» Telegram бота на Си

Время на прочтение7 мин
Количество просмотров62K
Привет всем, не знаю зачем это надо, но может кому пригодится…

Дисклеймер: Я ни в коем случае не являюсь профессиональным Си программистом.

Что нам понадобится:

1. Любой компьютер на Linux, Ubuntu, Centos, MacOS… с доступом к порту 443 или 8443 из интернета.
2. Любой Си компилятор
3. Бибилиотеки openssl, libssl-dev («apt-get install openssl libssl-dev» в терминале, для Ubuntu)

Итак, приступим…

Первое что нужно сделать — это создать бота у отца всех ботов @BotFather, опустим все подробности и предположим что с этой задачей все справились и получили токен, что-то вроде:
373288854:AAHHT77v5_ZNEMус4bfnРЩo6dxiMeeEwgwJ

Далее создадим ssl сертификат, для установки WebHook. Команда выглядит примерно так:

openssl req -newkey rsa:2048 -sha256 -nodes -keyout private.key -x509 -days 365 -out public.pem

Упакуем ключ и публичный сертификат в один файл:

cat private.key public.pem > cert.pem

Устанавливаем WebHook:

curl -F"url=https://ВАШ_IP:ПОРТ(либо 443, либо 8443)/ЛЮБОЙ_URI(можно и без него, я буду использовать токен)/" -F"certificate=@public.pem" https://api.telegram.org/botТОКЕН/setWebhook/

Должен прийти JSON ответ что-то типа success:true..., если нет то проверьте все и попробуйте еще раз.

Приступаем к самому интересному:

Создаем файл main.c и открываем его в любом редакторе. Включаем необходимые библиотеки:

#include <stdio.h>
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <unistd.h>
#include <openssl/err.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <resolv.h>
#include <netdb.h>

Функция инициализации сокета:

int InitializeSocket(int port) {
    int sd = socket(AF_INET, SOCK_STREAM, 0); 
    if (sd < 0) exit(-1);
    struct sockaddr_in s_addr;
    s_addr.sin_family = AF_INET;
    s_addr.sin_addr.s_addr = INADDR_ANY;
    s_addr.sin_port = htons(port);
    if (bind(sd, (struct sockaddr *)&s_addr, sizeof(s_addr)) < 0) {
        printf("Binding Error!\n");
        exit(-3);
    }
    return sd;
}

Включаем SSL/TLS:

SSL_CTX * InitializeSSL(char[] certificate) {
    OpenSSL_add_all_algorithms();
    SSL_load_error_strings();
    SSL_library_init();
    SSL_CTX * sslctx = SSL_CTX_new(TLSv1_2_server_method());
    if (SSL_CTX_use_certificate_file(sslctx, certificate , SSL_FILETYPE_PEM) <= 0) {
        exit(-2);
    }
    if (SSL_CTX_use_PrivateKey_file(sslctx,  certificate, SSL_FILETYPE_PEM) <= 0) {
        exit(-2);
    }
    if (!SSL_CTX_check_private_key(sslctx)) {
       exit(-2);
    }
    return sslctx;
}

Собственно сам main():

int main() {
    SSL_CTX * sslctx = InitializeSSL("cert.pem"); //Созданный нами файл из приватного ключа и публичного сертификата
    int sd = InitializeSocket(8443);  //Порт который вы указали при установке WebHook
    listen(sd, 5); //Слушаем подключения на созданном сокете
    while (1) { //Запускаем бесконечный цикл
        int client = accept(sd, NULL, NULL) //функция accept ждет новое подключение, в качестве параметров принимает сокет, указатель на структуру sockaddr, и указатель на размер этой структуры и записывает туда данные подключения, так как нам необязательно знать подробности подключения отправим NULL, функция возвращает сетевой дескриптор. 
        SSL * ssl = SSL_new(sslctx); //Cоздаем ssl дескриптор
        SSL_set_fd(ssl, client); //Переключаем обычный дескриптор на защищенный 
        if (SSL_accept(ssl) <= 0) { //Пытаемся принять подключение, если ошибка то закрываем соединение и возвращаемся к началу цикла
            SSL_clear(ssl);
            close(newsd);
            continue;
        }
        //Для увеличения производительности будем использовать fork() и обрабатывать соединение в дочернем процессе, а родительский процесс вернем к ожиданию новых подключений
     int pid = fork();
        if (pid != 0) { //Если это родитель, то закрываем подключение и возвращаемся к началу цикла
            SSL_clear(ssl);
            close(newsd);
            continue;
        }
    //Дальнейшие действия будут происходить в дочернем процессе
   //Опишу их дальше после некоторых пояснений....
    exit(0); //Завершаем дочерний процесс
    }
}

Так как Telegram использует HTTP протокол поясню некоторые моменты:

Любой HTTP запрос состоит из заголовков отделенных между собой "\r\n", и тела отделенного от заголовков "\r\n\r\n", может быть пустым, но разделитель "\r\n\r\n" присутствует всегда. Запросы от Telegram будут приходить методом POST, тело будет в формате JSON.

Пример запроса похожего на Telegram:

POST /(URI указанный при установке WebHook) HTTP/1.1\r\n
....Неважные для нас поля заголовков
Content-Type: application/json\r\n (Тип данных в теле)
Content-Length: 256\r\n (Размер тела в байтах, целое число)
..../r/n/r/n
Json тело

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

Для этого подготовим короткий HTTP response:

HTTP/1.1 200 OK\r\n
Connection: close\r\n\r\n

Этих двух полей достаточно что бы сказать серверу Telegram что все нормально, ответ 200 и можно закрывать соединение

Продолжаем писать программу. Внутри цикла после создания дочернего процесса…

char[] response = "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n"; //Наш HTTP response
char header[1024];
bzero(header,1024); //Выделили массив для записи в него заголовков запроса и на всякий случай занулили там все записи.
int s = 0;
int n = 0;
while (strcmp(header + s - strlen("\r\n\r\n"), "\r\n\r\n") != 0) { //strcmp Сравнивает две строки и если они равны возвращает 0, в нашем случае сравниваем последние strlen("\r\n\r\n") байт с "\r\n\r\n", то есть ищем конец заголовка
    n = SSL_read(ssl,header+s,1); //Считываем данные по одному байту в header + s, s - общее кол-во считанных байт
    s += n; //n - кол-во считанных байт за раз
}
//Все, заголовки считаны, теперь нам надо проверить метод, uri, content-type и вытащить content-length запроса.
if (strstr(header,"POST /(URI указанный при установке WebHook) HTTP/1.1\r\n") == NULL) { //Ищем вхождение строки POST .... в header, если его нет то возвращается NULL, значит пришел неверный запрос, закрываем подключение и завершаем дочерний процесс
            SSL_clear(ssl);
            close(client);
            exit(0);
}
//Также проверим тип данных, должен быть application/json;
       if (strstr(header, "Content-Type: application/json") == NULL) {
            SSL_clear(ssl);
            close(client);
            exit(0);
        }
//Если все нормально, то узнаем размер тела
int len = atoi(strstr(header, "Content-Length: ") + strlen("Content-Length: ")); //strstr возвращает указатель не первое вхождение указанной строки, то есть на "Content-Length: ", а кол-во байт записано дальше после этой строки, поэтому прибавляем длину строки "Content-Length: " и приводим строку к типу int функцией atoi(char *);

char body[len+2]; 
bzero(body, len+2); //Создаем массив для тела, на этот раз мы точно знаем сколько байт нам понадобится, но создаем с запасом, дабы не оказалось что в памяти сразу после нашей строки что-то записано
 n = 0;
 s = 0;
 while (len - s > 0) { //Так как мы четко знаем сколько данных нам надо считать просто считываем пока не считаем нужное кол-во
    n = SSL_read(ssl, request + s, len - s); //Конечно можно было считать целиком все данные, но бывают случаи при плохом соединении, за раз все данные не считываеются, и функция SSL_read возвращает кол-во считанных байт
    s += n;
}
//На этом получение данных окончено, отправим наш http response и закроем соединение SSL_write(ssl, response, (int)strlen(response));
SSL_clear(ssl);
SSL_free(ssl);
close(client);
//Так как у нас "Hello, World" бот то мы будем просто отвечать на любое сообщение "Hello, World!", но нам нужно знать кому отправлять сообщение для это из тела запросы надо вытащить параметр chat_id
int chat_id = atoi(strstr("\"chat_id\":") + strlen("\"chat_id\":")); //То же самое что и с Content-Length
//Осталось только отправить сообщение, для этого лучше создадим отдельную функцию SendMessage
char msg[] = "Hello, World!";
SendMessage(chat_id, msg); //Описание функции далее

Для отправки запросов нам почти так же понадобится инициализировать сокет и ssl, но в отличие от получения запросов, мы не будем ждать подключения а просто сразу будем отправлять данные:

void SendMessage(int chat_id, char[] msg) {
    int port = 443;
    char host[] =  "api.telegram.org"; //Адрес и порт всегда одинаковые
   //Создадим шаблон HTTP запроса для отправки сообщения, в виде форматированной строки
    char header[] = "POST /bot352115436:AAEAIEPeKdR2-SS7p9jGeksQljkNa9_Smo0/sendMessage HTTP/1.1\r\nHost: files.ctrl.uz\r\nContent-Type: application/json\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s";
    //Шаблон тела для отправки сообщения
    char tpl[] = "{\"chat_id\":%d,\"text\":\"%s\"}";
    char body[strlen(tpl)+strlen(msg)+16];
    bzero(body, strlen(tpl)+strlen(msg)+16);
    sprintf(body,tpl,chat_id,msg); //Как printf, только печатаем в char[] 
    char request[strlen(header)+strlen(body)+4];
    bzero(request,strlen(header)+strlen(body)+4);
    sprintf(request, header, strlen(body), body);
   //Подготовили наш запрос, теперь создаем подключение
    struct hostent *server; 
    struct sockaddr_in serv_addr;
    int sd;
    sd = socket(AF_INET, SOCK_STREAM, 0);
    if (sd < 0) exit(-5);
    server = gethostbyname(host); //Данная функция получает ip и еще некоторые данные по url
    if (server == NULL) exit(-6); 
    bzero(&serv_addr, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(portno);
    memcpy(&serv_addr.sin_addr.s_addr,server->h_addr,server->h_length); 
    if (connect(sd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0) { exit(-6);}
    SSL_CTX * sslctx = SSL_CTX_new(TLSv1_client_method());
    SSL * cSSL = SSL_new(sslctx);
    SSL_set_fd(cSSL, sfd);
    SSL_connect(cSSL); 
    SSL_write(cSSL,request,(int)strlen(request)); //Отправляем наш запрос, в идеале его надо отправлять так же как мы считывали данные, то есть с проверкой на кол-во отправленных байт
    char str[1024];
    SSL_read(cSSL, str, 1024); //Считываем ответ и закрываем соединение
    SSL_clear(cSSL);
    SSL_CTX_free(sslctx);
    close(sd);
}

На этом, в принципе все. Сохраняем файл в одной папке с сертификатом, компилируем любым компилятором и запускаем:

clang main.c -o bot -lcrypto -lssl 
./bot

Конец!

Надеюсь статья будет кому-то полезной.
Теги:
Хабы:
Всего голосов 40: ↑27 и ↓13+14
Комментарии33

Публикации

Ближайшие события