Embedded HandbookПриложения
Все разделы › XXV. Мини-проекты
STM32F407 · уровень средний · 23 мин чтения
Открыть в интерактивном справочнике

198з. Веб-страница и REST на STM32: lwIP httpd, SSI и CGI

В 198ж датчик говорил с сетью по UDP своим протоколом: быстро и коротко, но для него нужна своя программа на компьютере. Здесь прибор отвечает тому, что есть на любом столе, — браузеру и curl. Страница показывает время работы, светодиод и уставку; ссылка включает светодиод; программа читает состояние в JSON и меняет уставку запросом POST под паролем.

Всё это умеет httpd — маленький веб-сервер, который лежит в самом lwIP (src/apps/http). Писать HTTP с нуля не нужно: теорию запроса и ответа дали разделы 107.4 и 142.1, REST — 142.2, тот же REST на ESP32 — 156.5. Здесь — как это устроено на МК без ОС: где лежат страницы, как в них попадают живые числа, откуда берётся JSON, сколько браузеров выдержит прибор и почему пароль Basic — это ещё не защита.

Файлы — _verify/web198i/: web_app.c (SSI, CGI, REST и POST — один файл для ПК и F407), lwipopts.h, страницы fs/, host/ (прибор на ПК за интерфейсом tap0 и запросы к нему), f407/ (плата, главный цикл, порт на драйвер из 106а), web198i.py (прогон и сборка). Сверяет _verify/web198i_check.py.

Опоры: исходники lwIP 2.1.2 из STM32CubeF4 1.28.0 — httpd.c, fs.c, httpd_opts.h, makefsdata.c, tcp.c; пример ST LwIP_HTTP_Server_Raw для платы STM324xG-EVAL (его lwipopts.h и страницы) и описание платы в BSP (stm324xg_eval.h: LED1 на PG6). Всё — из архива CubeF4.

Как это называется в документации

Оригинал Перевод
httpd, lwIP HTTP server веб-сервер из состава lwIP
makefsdata, fsdata.c программа, которая превращает папку страниц в массивы C, и её результат
SSI, server-side include подстановка на сервере: тег в странице меняется на значение
CGI handler обработчик адреса с параметрами ?имя=значение
custom files свои «файлы»: ответ собирается в момент запроса
Basic authentication пароль в заголовке запроса, закодированный base64
TIME-WAIT ожидание после закрытия соединения TCP
pcb (protocol control block) блок состояния одного соединения TCP в lwIP

198з.1 Что умеет прибор и как устроен httpd

Прибор — F407 с Ethernet из 106а и lwIP без ОС из 106в: один главный цикл забирает кадры из кольца DMA и крутит таймеры стека. Поверх — httpd на порту 80 и файл web_app.c — около двухсот строк. Что он отдаёт:

Путь запроса в httpd из lwIP: браузер или curl, TCP lwIP на 10 соединений, разбор запроса с состоянием 188 байт в куче; адрес с параметрами идёт в CGI, POST — в свой обработчик с проверкой пароля Basic, GET — в fs_open, который сначала спрашивает свои файлы /api с JSON в куче, потом fsdata во флеш размером 1419 байт; страница .shtml проходит подстановку SSI

Главное в устройстве httpd: у него нет файловой системы. Страницы заранее превращены в массивы C и лежат во флеш вместе с готовыми заголовками HTTP. На запрос httpd находит массив по имени и отдаёт его кусками прямо из флеш, не копируя в ОЗУ. В куче живёт только состояние соединения — 188 байт на F407 (размер struct http_state при нашем lwipopts.h, его назвал сам armclang), для страницы с SSI ещё 68 байт (struct http_ssi_state) и буфер вставки.

Ключевые настройки — в том же lwipopts.h, память взята как в примере ST: куча MEM_SIZE 10 КБ, MEMP_NUM_TCP_PCB 10, восемь буферов приёма по 1524 байта.

#define LWIP_HTTPD_CGI              1       /* GET /led.cgi?on=1 */
#define LWIP_HTTPD_SSI              1       /* <!--#uptime--> в .shtml */
#define LWIP_HTTPD_SSI_INCLUDE_TAG  0       /* сам тег в страницу не пускать */
#define LWIP_HTTPD_MAX_TAG_INSERT_LEN 32    /* буфер вставки SSI — в куче на каждое соединение */
#define LWIP_HTTPD_SUPPORT_POST     1       /* POST /api/set */
#define LWIP_HTTPD_CUSTOM_FILES     1       /* /api/state собирается на лету */
#define LWIP_HTTPD_DYNAMIC_FILE_READ 1      /* ответ на лету копируется в пакеты */

198з.2 Страницы во флеш: makefsdata и fsdata

makefsdata — программа на C из src/apps/http/makefsdata. Её собирают на компьютере (gcc), запускают на папке fs/ и получают fsdata_198i.c; httpd включает его через HTTPD_FSDATA_FILE. Для каждой страницы в массиве три части: имя, заголовок HTTP и сам файл.

Страница Файл, байт Имя Заголовок Массив во флеш
/index.shtml 871 16 165 1052
/404.html 215 12 140 367

Итого 1419 байт; вместе с таблицей файлов объект fs.o занял во флеш 1459 байт. Заголовок — три строки: HTTP/1.0 200 OK, Server: и Content-Type:. Для .shtml к типу добавлены Expires и Pragma: no-cache — страница живая, кэшировать её нельзя. А вот Content-Length у .shtml нет: длину после подстановки заранее не знает никто, и конец ответа браузер узнаёт по закрытию соединения.

Для сравнения — страницы примера ST: семь файлов на 51 263 байта дали 52 228 байт массивов, из них 32 110 — три картинки, одна stm32.jpg весит 26 069. Правило простое: картинки во флеш прибора не класть, оформление — стилями в самой странице.

Сжатие у makefsdata есть, но только при сборке с MAKEFS_SUPPORT_DEFLATE, а для него нужен miniz.c. В CubeF4 1.28.0 этого файла нет, так что ключ -defl в этой поставке недоступен; к тому же браузер должен понимать Content-Encoding: deflate — об этом предупреждает сама справка программы.


198з.3 SSI и CGI: живые числа и параметры GET

SSI — это тег-комментарий в странице: <!--#uptime-->. httpd разбирает только файлы с расширениями .shtml, .shtm, .ssi, .xml, .json, находит тег, спрашивает обработчик и вставляет ответ вместо тега (LWIP_HTTPD_SSI_INCLUDE_TAG 0 — сам тег в ответ не попадает).

static const char *ssi_tags[] = { "uptime", "led", "setp", "hits" };

static u16_t ssi_handler(int idx, char *ins, int len)
{
    int n;
    switch (idx) {
    case 0:  n = snprintf(ins, (size_t)len, "%lu", (unsigned long)board_uptime_s()); break;
    case 1:  n = snprintf(ins, (size_t)len, "%s", board_led_get() ? "вкл" : "выкл"); break;
    case 2:  n = snprintf(ins, (size_t)len, "%d", setpoint); break;
    default: n = snprintf(ins, (size_t)len, "%lu", (unsigned long)++hits); break;
    }
    return (u16_t)(n < 0 ? 0 : LWIP_MIN(n, len - 1));  /* snprintf вернёт «сколько хотел» */
}

Обработчик получает номер тега в таблице и буфер длиной LWIP_HTTPD_MAX_TAG_INSERT_LEN. По умолчанию это 192 байта на каждое соединение со страницей SSI; нам хватает 32. Имя тега — не длиннее LWIP_HTTPD_MAX_TAG_NAME_LEN, по умолчанию 8 символов: _Static_assert в web_app.c следит, чтобы наши теги в него влезали. Начало ssi_handler в сборке armclang (всего 50 команд):

0x00000000: b5b0 .. PUSH {r4,r5,r7,lr}
0x00000002: 4614 .F MOV r4,r2
0x00000004: 2802 .( CMP r0,#2
0x00000006: 460d .F MOV r5,r1
0x00000008: d00f .. BEQ {pc}+0x22 ; 0x2a
0x0000000a: 2801 .( CMP r0,#1
0x0000000c: d004 .. BEQ {pc}+0xc ; 0x18
0x0000000e: b998 .. CBNZ r0,{pc}+0x2a ; 0x38
0x00000010: f7fffffe .... BL board_uptime_s

switch стал цепочкой сравнений номера тега, вызов board_uptime_s ещё не привязан (f7fffffe) — адрес подставит компоновщик.

CGI в httpd — не отдельная программа, как в 142.3, а функция. Таблица связывает адрес с обработчиком; httpd сам режет строку после ? на пары (до 16 штук, LWIP_HTTPD_MAX_CGI_PARAMETERS) и отдаёт их массивами имён и значений:

static const char *cgi_led(int idx, int n, char *par[], char *val[])
{
    (void)idx;
    for (int i = 0; i < n; i++)
        if (strcmp(par[i], "on") == 0 && val[i] != NULL)
            board_led_set(val[i][0] == '1');
    return "/index.shtml";              /* какую страницу отдать в ответ */
}

Обработчик возвращает имя страницы для ответа. Так ссылка «включить» на странице меняет светодиод и сразу показывает страницу с новым значением. Но это GET, а GET по смыслу ничего не меняет (142.2): браузер может открыть такую ссылку заранее, сам по себе. Для команд, которые меняют состояние, дальше будет POST.


198з.4 REST: свои файлы с JSON и POST под паролем

Для JSON страница из флеш не подходит: ответ каждый раз новый. На это есть «свои файлы» (LWIP_HTTPD_CUSTOM_FILES): fs_open сначала зовёт fs_open_custom, и если тот узнал имя — ответ берётся у приложения.

int fs_open_custom(struct fs_file *file, const char *name)
{
    char body[128];
    if (strcmp(name, "/api/state") == 0) {
        snprintf(body, sizeof body, "{\"uptime\":%lu,\"led\":%d,\"setpoint\":%d,\"hits\":%lu}\n",
                 (unsigned long)board_uptime_s(), board_led_get(), setpoint, (unsigned long)++hits);
        return reply(file, "200 OK", "", body);
    }

reply собирает ответ целиком — строка статуса, Content-Type: application/json, Cache-Control: no-store, Content-Length и тело — в кусок кучи и помечает его FS_FILE_FLAGS_HEADER_INCLUDED. Данных во флеш нет (data == NULL), поэтому httpd читает их через fs_read_custom, а по концу ответа зовёт fs_close_custom, который возвращает память. Так же сделаны ответы-ошибки /api/e400 и /api/e401.

POST httpd поддерживает при LWIP_HTTPD_SUPPORT_POST: приложение даёт три функции. httpd_post_begin получает адрес, заголовки и длину тела и решает, принять ли запрос; httpd_post_receive_data получает тело пакетами; httpd_post_finished называет страницу для ответа. Заголовки запроса httpd показывает только в первой функции — значит, и пароль проверять там:

    static const char auth[] = "Authorization: Basic " WEB_BASIC_B64;
    const char *a = lwip_strnstr(req, auth, req_len);
    (void)auto_wnd;
    if (a == NULL || (a[sizeof auth - 1] != '\r' && a[sizeof auth - 1] != '\0')) {
        snprintf(resp_uri, resp_len, "/api/e401");
        return ERR_VAL;                 /* заголовки запроса httpd показывает только здесь */
    }

Проверка символа после пароля нужна: без неё прошёл бы и более длинный пароль, который начинается с нашего. Тело формы разбирается «всё или ничего»: сначала проверка всех полей, потом запись. setpoint=5000 отвергается целиком, даже если рядом стоит верный led=1. Ответ на удачный POST — свежий /api/state: программа сразу видит, что записалось.

Почему Basic — слабая защита. Пароль идёт в каждом запросе открытым текстом: base64 — это не шифр, а запись байтов буквами. Строку YWRtaW46c2VjcmV0MTk4 из заголовка любой, кто видит трафик в сети, превращает обратно в admin:secret198 одной командой. Шифрования нет: TLS в этом приборе нет (что видно в нём до шифрования — 107.5), а у httpd он есть только поверх mbedTLS (HTTPD_ENABLE_HTTPS, по умолчанию 0). Нет и ограничения попыток — подбирать пароль можно сколько угодно. Вывод: такой прибор — только в закрытой сети цеха или лаборатории, в интернет его не выставлять ни напрямую, ни пробросом порта на роутере. Пароль Basic здесь защищает от случайного нажатия, а не от злоумышленника.


198з.5 Опыт на ПК: тот же код за интерфейсом tap0

Ethernet F407 в QEMU нет, поэтому прибор собран на ПК: lwIP 2.1.2 без ОС, тот же lwipopts.h, тот же web_app.c, а вместо DMA — интерфейс tap0 ядра Linux (как в 106в). Всё идёт в контейнере stm-sys:1: gcc собирает makefsdata, страницы и прибор (предупреждений в своих файлах — 0), Python и curl ходят к нему на 192.168.7.2 как браузер.

Запрос Ответ Что пришло
GET / 200, 1000 байт заголовок 165 байт из fsdata; SSI: выкл, 250, тегов <!--# в ответе нет
curl http://192.168.7.2/ 200 835 байт тела
GET /api/state 200, 141 байт {"uptime":1,"led":0,"setpoint":250,"hits":3}, Content-Length: 45
GET /led.cgi?on=1 200, 998 байт страница; следом /api/state даёт "led":1
GET /nothing 404, 355 байт 404.html
POST без пароля и с чужим 401 {"error":"login required"}
POST setpoint=300 200 "setpoint":300
POST setpoint=5000 400 {"error":"out of range"}
POST led=abc 400 expected led=0\|1 and/or setpoint=0..1000
POST led=0&setpoint=700 200 "led":0,"setpoint":700
curl -u admin:secret198 -d setpoint=450 200 "setpoint":450

Файл страницы — 871 байт, тело ответа — 835: теги вроде <!--#uptime--> длиннее чисел, которые встали на их место. Время ответа на ПК — медиана 0,10 мс на /api/state и 0,11 мс на страницу (200 запросов подряд), но это скорость стека и ядра Linux на компьютере. Сколько отвечает F407 на 100 Мбит/с, здесь не мерили.


198з.6 Сколько соединений и что будет при 10 браузерах

Потолок задаёт MEMP_NUM_TCP_PCB — 10 блоков соединений TCP (слушающий порт 80 берёт блок из отдельного пула MEMP_NUM_TCP_PCB_LISTEN). Опыт первый: открыть 14 соединений и молчать, как делает браузер, который соединяется «про запас». Прибор принял 10 из 14; четыре лишних запроса на соединение (SYN) стек отбросил, счётчик tcp.memerr показал 4. На ПК 10 молчащих соединений заняли в куче 3772 байта из 10 240; на F407 состояние короче — 188 байт на соединение.

Молчащих httpd закрывает сам: опрос раз в HTTPD_POLL_INTERVAL 4 тика по 0,5 с, после HTTPD_MAX_RETRIES 4 опросов без дела соединение закрыто — 6–8 с в зависимости от фазы таймера. В опыте все 10 закрылись через 7,6 с. Запрос сверх предела, начатый на 2,3 с, соединился через 7,1 с: клиент повторяет SYN всё реже, и очередной повтор застал уже освободившиеся блоки. Для пользователя это страница, которая думает семь секунд.

Опыт второй — живые запросы: 10 «браузеров» по одному соединению, потом по шесть сразу — 60 одновременных GET /. Прошли все 10 из 10 и 60 из 60, вторая пачка — за 0,02 с на ПК. Почему не упёрлись в 10: каждый запрос обслуживается за доли миллисекунды, а закрытые соединения уходят в TIME-WAIT. Он длится 2 × TCP_MSL = 120 с, и после серии запросов все 10 блоков заняты именно им (счётчик: time-wait 10). Но блок в TIME-WAIT стек отнимает первым, когда нужен новый (tcp_alloc → tcp_kill_timewait), так что отказов не было.

Вывод: прибор держит сколько угодно коротких запросов и ровно 10 висящих соединений. Опасны не десять браузеров, а соединения, которые открыты и молчат. Живое соединение новое не вытеснит: httpd ставит своим соединениям наименьший приоритет TCP_PRIO_MIN, а tcp_kill_prio убивает только тех, чей приоритет ниже нового. Если панелей много — пусть они реже опрашивают /api/state (наша страница спрашивает раз в 2 с), а постоянный поток данных лучше отдать MQTT (198и).

Память на F407 (armclang, -Os, NO_SYS=1, наш lwipopts.h):

Часть Флеш, байт ОЗУ, байт
lwIP: ядро, IPv4, ARP, TCP, UDP 21 492 25 694
httpd и fs с fsdata 5723 1108
весь образ с драйвером 106а и приложением 32 544 37 916

В ОЗУ lwIP почти всё — куча MEM_SIZE (объект mem.o — 10 271 байт) и пулы memp (memp.o — 15 055 байт, из них больше всего буферы приёма). Сам web_app.c — 1382 байта кода и 316 байт констант. Своих файлов шесть, собраны с -Wall -Wextra, предупреждений 0.


198з.7 Грабли

После оборванного POST прибор до перезагрузки отвечает busy. Если клиент оборвал POST сбросом (RST), httpd освобождает соединение через http_err и не зовёт httpd_post_finished. Приложение, которое помнит «сейчас идёт POST», остаётся занятым навсегда. В опыте POST, оборванный на середине тела, давал busy сразу и через 5 с. Лечение в web_app.c: метка времени и отказ от старой занятости через 10 с (POST_STALE_MS) — через 11 с запрос прошёл.

Тег SSI не заменился, в браузере пусто. Имя тега длиннее LWIP_HTTPD_MAX_TAG_NAME_LEN (по умолчанию 8) httpd не узнаёт и отдаёт как есть, а браузер прячет комментарий. Держать имена короткими и проверять предел _Static_assert.

Значение SSI обрезано или страница испорчена. snprintf возвращает не сколько записал, а сколько хотел. Если вернуть это httpd, он вставит мусор за концом буфера. Возвращать не больше len - 1, как в ssi_handler.

Одиннадцатый браузер «висит» семь секунд. Все 10 блоков TCP заняты молчащими соединениями; новый SYN отброшен, клиент ждёт повтора. Больше MEMP_NUM_TCP_PCB — больше ОЗУ; меньше HTTPD_MAX_RETRIES — быстрее закрываются молчащие.

Кириллица на странице — кракозябры. makefsdata пишет Content-Type: text/html без кодировки. Нужна строка <meta charset="utf-8"> в самой странице.

Правка страницы не видна. Страницы вшиты в прошивку: после правки fs/ надо заново запустить makefsdata и пересобрать. Проверка раздела сверяет сохранённый fsdata_198i.c со свежесгенерированным.

GET меняет состояние. Ссылка /led.cgi?on=1 удобна, но браузер или антивирус может открыть её сам. Всё, что меняет прибор, — через POST с паролем.

Пароль «защищает», а в сети его видно. Basic без TLS — открытый текст. Прибор — только в закрытой сети, не в интернете.


198з.8 Итоги

Полные исходники

lwipopts.h — настройки lwIP для веб-страницы и REST.

/* 198з: настройки lwIP 2.1.2 для веб-страницы и REST. Один файл на F407 (armclang,
   -DWEB_ON_F407) и на ПК (gcc, tap). Память — как в примере ST LwIP_HTTP_Server_Raw
   для STM324xG_EVAL: куча 10 КБ, 10 TCP-соединений, 8 буферов приёма по 1524 байта. */
#ifndef LWIPOPTS_H
#define LWIPOPTS_H

#define NO_SYS                      1       /* без ОС: один цикл, обратные вызовы */
#define SYS_LIGHTWEIGHT_PROT        0
#define LWIP_NETCONN                0
#define LWIP_SOCKET                 0

#define MEM_ALIGNMENT               4
#define MEM_SIZE                    (10 * 1024)   /* куча: состояние HTTP, копии SSI, буфер JSON */
#define MEMP_NUM_PBUF               10            /* ссылки на страницы во флеш без копирования */
#define MEMP_NUM_TCP_PCB            10            /* одновременных TCP-соединений — потолок httpd */
#define MEMP_NUM_TCP_PCB_LISTEN     2
#define MEMP_NUM_TCP_SEG            16
#define MEMP_NUM_UDP_PCB            2
#define PBUF_POOL_SIZE              8
#define PBUF_POOL_BUFSIZE           1524

#define LWIP_ARP                    1
#define LWIP_ETHERNET               1
#define LWIP_ICMP                   1
#define LWIP_UDP                    1
#define LWIP_TCP                    1
#define LWIP_DHCP                   0             /* адрес статический: стенд без DHCP */
#define TCP_MSS                     1460
#define TCP_SND_BUF                 (4 * TCP_MSS)
#define TCP_SND_QUEUELEN            (2 * TCP_SND_BUF / TCP_MSS)
#define TCP_WND                     (2 * TCP_MSS)
#define TCP_QUEUE_OOSEQ             0

#ifdef WEB_ON_F407                          /* суммы считает MAC (бит IPCO, раздел 106а) */
#define CHECKSUM_GEN_IP             0
#define CHECKSUM_GEN_UDP            0
#define CHECKSUM_GEN_TCP            0
#define CHECKSUM_GEN_ICMP           0
#define CHECKSUM_CHECK_IP           0
#define CHECKSUM_CHECK_UDP          0
#define CHECKSUM_CHECK_TCP          0
#define LWIP_STATS                  0
#else                                       /* на ПК — счётчики для отчёта прогона */
#define LWIP_STATS                  1
#define LWIP_STATS_DISPLAY          1       /* нужно ради имён пулов в сводке */
#endif

/* ---- httpd ---- */
#define LWIP_HTTPD_CGI              1       /* GET /led.cgi?on=1 */
#define LWIP_HTTPD_SSI              1       /* <!--#uptime--> в .shtml */
#define LWIP_HTTPD_SSI_INCLUDE_TAG  0       /* сам тег в страницу не пускать */
#define LWIP_HTTPD_MAX_TAG_INSERT_LEN 32    /* буфер вставки SSI — в куче на каждое соединение */
#define LWIP_HTTPD_SUPPORT_POST     1       /* POST /api/set */
#define LWIP_HTTPD_CUSTOM_FILES     1       /* /api/state собирается на лету */
#define LWIP_HTTPD_DYNAMIC_FILE_READ 1      /* ответ на лету копируется в пакеты */
#define HTTPD_FSDATA_FILE           "fsdata_198i.c"

#endif

web_app.c — веб-приложение поверх httpd: SSI, CGI, REST с JSON и POST под паролем.

/* 198з: веб-приложение поверх httpd из lwIP 2.1.2.
   SSI — значения в странице, CGI — параметры GET, свои «файлы» /api/... — REST с JSON,
   POST /api/set — запись уставки под паролем Basic. Один и тот же файл собирается
   для F407 (armclang) и для ПК (gcc, прогон через tap). */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "lwip/apps/httpd.h"
#include "lwip/apps/fs.h"
#include "lwip/def.h"
#include "lwip/mem.h"
#include "lwip/pbuf.h"
#include "lwip/sys.h"
#include "board.h"
#include "web_app.h"

_Static_assert(LWIP_HTTPD_SUPPORT_POST && LWIP_HTTPD_CUSTOM_FILES && LWIP_HTTPD_DYNAMIC_FILE_READ,
               "нужны POST, свои файлы и чтение на лету");
_Static_assert(LWIP_HTTPD_MAX_TAG_NAME_LEN >= 6, "тег uptime — 6 букв; длиннее предела тег молча не находится");
_Static_assert(LWIP_HTTPD_SSI_INCLUDE_TAG == 0, "сам тег <!--#...--> в ответ не пускать");
_Static_assert(sizeof(WEB_BASIC_B64) - 1 == 20, "base64 от 15 байт — 20 символов");

static int setpoint = 250;              /* уставка, 0…1000 */
static uint32_t hits;                   /* сколько раз прочитали состояние */
static const char *post_err = "";

/* ---- SSI: <!--#uptime--> и др. в index.shtml ---- */
static const char *ssi_tags[] = { "uptime", "led", "setp", "hits" };

static u16_t ssi_handler(int idx, char *ins, int len)
{
    int n;
    switch (idx) {
    case 0:  n = snprintf(ins, (size_t)len, "%lu", (unsigned long)board_uptime_s()); break;
    case 1:  n = snprintf(ins, (size_t)len, "%s", board_led_get() ? "вкл" : "выкл"); break;
    case 2:  n = snprintf(ins, (size_t)len, "%d", setpoint); break;
    default: n = snprintf(ins, (size_t)len, "%lu", (unsigned long)++hits); break;
    }
    return (u16_t)(n < 0 ? 0 : LWIP_MIN(n, len - 1));  /* snprintf вернёт «сколько хотел» */
}

/* ---- CGI: GET /led.cgi?on=1 ---- */
static const char *cgi_led(int idx, int n, char *par[], char *val[])
{
    (void)idx;
    for (int i = 0; i < n; i++)
        if (strcmp(par[i], "on") == 0 && val[i] != NULL)
            board_led_set(val[i][0] == '1');
    return "/index.shtml";              /* какую страницу отдать в ответ */
}

static const tCGI cgis[] = { { "/led.cgi", cgi_led } };

/* ---- свои «файлы»: ответ целиком, с заголовками, собирается при открытии ---- */
static int reply(struct fs_file *file, const char *status, const char *extra, const char *body)
{
    char tmp[256];
    int n = snprintf(tmp, sizeof tmp, "HTTP/1.0 %s\r\nContent-Type: application/json\r\n"
                     "Cache-Control: no-store\r\n%sContent-Length: %u\r\n\r\n%s",
                     status, extra, (unsigned)strlen(body), body);
    if (n <= 0 || n >= (int)sizeof tmp)
        return 0;
    char *p = (char *)mem_malloc((mem_size_t)n);
    if (p == NULL)
        return 0;                       /* куча кончилась — httpd ответит 404 */
    memcpy(p, tmp, (size_t)n);
    memset(file, 0, sizeof *file);
    file->len = n;                      /* data == NULL: httpd читает через fs_read_custom */
    file->pextension = p;
    file->flags = FS_FILE_FLAGS_HEADER_INCLUDED;
    return 1;
}

int fs_open_custom(struct fs_file *file, const char *name)
{
    char body[128];
    if (strcmp(name, "/api/state") == 0) {
        snprintf(body, sizeof body, "{\"uptime\":%lu,\"led\":%d,\"setpoint\":%d,\"hits\":%lu}\n",
                 (unsigned long)board_uptime_s(), board_led_get(), setpoint, (unsigned long)++hits);
        return reply(file, "200 OK", "", body);
    }
    if (strcmp(name, "/api/e400") == 0) {
        snprintf(body, sizeof body, "{\"error\":\"%s\"}\n", post_err);
        return reply(file, "400 Bad Request", "", body);
    }
    if (strcmp(name, "/api/e401") == 0)
        return reply(file, "401 Unauthorized", "WWW-Authenticate: Basic realm=\"device\"\r\n",
                     "{\"error\":\"login required\"}\n");
    return 0;                           /* не наш — искать в fsdata */
}

int fs_read_custom(struct fs_file *file, char *buffer, int count)
{
    int left = file->len - file->index;
    if (left <= 0)
        return FS_READ_EOF;
    if (count > left)
        count = left;
    memcpy(buffer, (const char *)file->pextension + file->index, (size_t)count);
    file->index += count;
    return count;
}

void fs_close_custom(struct fs_file *file)
{
    mem_free(file->pextension);
    file->pextension = NULL;
}

/* ---- POST /api/set: тело "led=1&setpoint=300" ---- */
#define POST_STALE_MS 10000u            /* обрыв по RST: httpd не зовёт post_finished */
static void *post_conn;                 /* один POST за раз — прибору хватает */
static u32_t post_t0;
static char post_buf[64];
static u16_t post_len;

static int form_int(const char *s, const char *key, long *out)
{
    size_t k = strlen(key);
    for (const char *p = s; p != NULL; p = strchr(p, '&') ? strchr(p, '&') + 1 : NULL) {
        if (strncmp(p, key, k) == 0 && p[k] == '=') {
            char *end;
            long v = strtol(p + k + 1, &end, 10);
            if (end == p + k + 1 || (*end != '&' && *end != '\0'))
                return -1;              /* не число */
            *out = v;
            return 1;
        }
    }
    return 0;
}

static int apply_form(const char *s)
{
    long led = -1, sp = -1;
    int a = form_int(s, "led", &led), b = form_int(s, "setpoint", &sp);
    if (a < 0 || b < 0 || (a == 0 && b == 0)) {
        post_err = "expected led=0|1 and/or setpoint=0..1000";
        return 0;
    }
    if ((a && led != 0 && led != 1) || (b && (sp < 0 || sp > 1000))) {
        post_err = "out of range";
        return 0;                       /* всё или ничего: сначала проверка, потом запись */
    }
    if (a)
        board_led_set((int)led);
    if (b)
        setpoint = (int)sp;
    return 1;
}

err_t httpd_post_begin(void *conn, const char *uri, const char *req, u16_t req_len, int content_len,
                       char *resp_uri, u16_t resp_len, u8_t *auto_wnd)
{
    static const char auth[] = "Authorization: Basic " WEB_BASIC_B64;
    const char *a = lwip_strnstr(req, auth, req_len);
    (void)auto_wnd;
    if (a == NULL || (a[sizeof auth - 1] != '\r' && a[sizeof auth - 1] != '\0')) {
        snprintf(resp_uri, resp_len, "/api/e401");
        return ERR_VAL;                 /* заголовки запроса httpd показывает только здесь */
    }
    if (post_conn != NULL && sys_now() - post_t0 > POST_STALE_MS)
        post_conn = NULL;               /* прошлый POST оборван — занятость снять */
    if (strcmp(uri, "/api/set") != 0 || post_conn != NULL || content_len <= 0
        || content_len >= (int)sizeof post_buf) {
        post_err = post_conn != NULL ? "busy" : "bad uri or body length";
        snprintf(resp_uri, resp_len, "/api/e400");
        return ERR_VAL;
    }
    post_conn = conn;
    post_t0 = sys_now();
    post_len = 0;
    return ERR_OK;
}

err_t httpd_post_receive_data(void *conn, struct pbuf *p)
{
    if (conn == post_conn)
        post_len += pbuf_copy_partial(p, post_buf + post_len, (u16_t)(sizeof post_buf - 1u - post_len), 0);
    pbuf_free(p);                       /* пакет освобождает приложение */
    return ERR_OK;
}

void httpd_post_finished(void *conn, char *resp_uri, u16_t resp_len)
{
    int ok = 0;
    if (conn == post_conn) {
        post_buf[post_len] = '\0';
        post_conn = NULL;
        ok = apply_form(post_buf);
    }
    snprintf(resp_uri, resp_len, ok ? "/api/state" : "/api/e400");  /* ответ — свежее состояние */
}

void web_app_init(void)
{
    httpd_init();
    http_set_ssi_handler(ssi_handler, ssi_tags, (int)LWIP_ARRAYSIZE(ssi_tags));
    http_set_cgi_handlers(cgis, (int)LWIP_ARRAYSIZE(cgis));
}

Дальше: раздел 198и «MQTT-клиент на STM32: публикация, подписка и повторное подключение» — вместо того чтобы браузеры опрашивали прибор, прибор сам шлёт данные брокеру и держит одно соединение.

Открыть в интерактивном справочнике