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 — около двухсот строк. Что он отдаёт:
GET /— страницаindex.shtmlс четырьмя живыми значениями (SSI);GET /led.cgi?on=1— включить светодиод и вернуть ту же страницу (CGI);GET /api/state— состояние в JSON (свой «файл»);POST /api/setс теломled=1&setpoint=300— записать, только с паролем;- всё прочее —
404.html.
Главное в устройстве 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 Итоги
- httpd из lwIP 2.1.2 отдаёт страницы из флеш без копирования: две
страницы после
makefsdata— 1419 байт, заголовки HTTP уже внутри. - SSI подставляет значения в
.shtml, CGI разбирает до 16 параметров GET, свои файлы дают JSON, POST принимается тремя функциями приложения. - На ПК тот же
web_app.cответил на все запросы как задумано: 200, 401, 400, 404 — 11 видов запросов из таблицы 198з.5. - Предел — 10 соединений: 14 молчащих — принято 10, лишний запрос ждал 7,1 с; 60 коротких запросов разом — прошли все 60.
- Молчащие соединения httpd закрывает сам через 6–8 с (в опыте 7,6 с); закрытые 120 с сидят в TIME-WAIT, но стек отнимает их первыми.
- На F407: lwIP 21 492 байта флеш и 25 694 ОЗУ, httpd со страницами — 5723 и 1108; состояние соединения — 188 байт кучи.
- Basic — не защита: base64 обратим, TLS нет; прибор — только в закрытой сети.
Полные исходники
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: публикация, подписка и повторное подключение» — вместо того чтобы браузеры опрашивали прибор, прибор сам шлёт данные брокеру и держит одно соединение.