Embedded HandbookApps
All sections › XXV. Mini-projects
STM32F407 · level intermediate · 23 min read
Open in the interactive handbook

198з. Web page and REST on STM32: lwIP httpd, SSI and CGI

In 198ж the sensor talked to the network over UDP with its own protocol: fast and compact, but it needs a program of its own on the computer. Here the device answers what is on every desk — a browser and curl. A page shows the uptime, the LED and the setpoint; a link turns the LED on; a program reads the state as JSON and changes the setpoint with a password-protected POST request.

All of this is done by httpd — a small web server that lives inside lwIP itself (src/apps/http). You do not need to write HTTP from scratch: the theory of requests and responses was given in sections 107.4 and 142.1, REST in 142.2, and the same REST on ESP32 in 156.5. Here we look at how it works on an MCU without an OS: where the pages are stored, how live numbers get into them, where the JSON comes from, how many browsers the device can handle, and why a Basic password is not yet protection.

Files are in _verify/web198i/: web_app.c (SSI, CGI, REST and POST — one file for the PC and the F407), lwipopts.h, the pages in fs/, host/ (the device on the PC behind the tap0 interface and the requests to it), f407/ (the board, the main loop, the port to the driver from 106а), web198i.py (run and build). Checked by _verify/web198i_check.py.

Sources: lwIP 2.1.2 from STM32CubeF4 1.28.0 — httpd.c, fs.c, httpd_opts.h, makefsdata.c, tcp.c; the ST example LwIP_HTTP_Server_Raw for the STM324xG-EVAL board (its lwipopts.h and pages) and the board description in the BSP (stm324xg_eval.h: LED1 on PG6). Everything comes from the CubeF4 package.

Terms used in the documentation

Term Meaning
httpd, lwIP HTTP server the web server that ships with lwIP
makefsdata, fsdata.c the program that turns a folder of pages into C arrays, and its output
SSI, server-side include substitution on the server: a tag in the page is replaced with a value
CGI handler a handler for an address with ?name=value parameters
custom files your own "files": the response is built at request time
Basic authentication a password in a request header, encoded in base64
TIME-WAIT the wait state after a TCP connection is closed
pcb (protocol control block) the state block of one TCP connection in lwIP

198з.1 What the device does and how httpd works

The device is an F407 with Ethernet from 106а and lwIP without an OS from 106в: one main loop takes frames from the DMA ring and runs the stack timers. On top of that sit httpd on port 80 and the file web_app.c — about two hundred lines. What it serves:

The path of a request in lwIP httpd: a browser or curl, lwIP TCP with 10 connections, request parsing with 188 bytes of state in the heap; an address with parameters goes to CGI, POST to its own handler with a Basic password check, GET to fs_open, which first asks the custom files /api with JSON in the heap, then fsdata in flash, 1419 bytes in size; an .shtml page goes through SSI substitution

The main thing about how httpd works: it has no file system. The pages are turned into C arrays in advance and sit in flash together with ready-made HTTP headers. On a request httpd finds the array by name and sends it in pieces straight from flash, without copying it to RAM. Only the connection state lives in the heap — 188 bytes on the F407 (the size of struct http_state with our lwipopts.h, as reported by armclang itself), plus 68 bytes for a page with SSI (struct http_ssi_state) and the insert buffer.

The key settings are in the same lwipopts.h; the memory is taken as in the ST example: a MEM_SIZE heap of 10 KB, MEMP_NUM_TCP_PCB 10, eight receive buffers of 1524 bytes.

#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 Pages in flash: makefsdata and fsdata

makefsdata is a C program from src/apps/http/makefsdata. You build it on the computer (gcc), run it on the fs/ folder and get fsdata_198i.c; httpd includes it through HTTPD_FSDATA_FILE. For each page the array has three parts: the name, the HTTP header and the file itself.

Page File, bytes Name Header Array in flash
/index.shtml 871 16 165 1052
/404.html 215 12 140 367

1419 bytes in total; together with the file table the object fs.o took 1459 bytes of flash. The header is three lines: HTTP/1.0 200 OK, Server: and Content-Type:. For .shtml, Expires and Pragma: no-cache are added to the type — the page is live and must not be cached. But .shtml has no Content-Length: nobody knows the length after substitution in advance, and the browser learns where the response ends when the connection closes.

For comparison, the pages of the ST example: seven files of 51263 bytes gave 52228 bytes of arrays, of which 32110 are three images; stm32.jpg alone weighs 26069. The rule is simple: do not put images into the device flash, do the styling with CSS inside the page itself.

makefsdata does have compression, but only when built with MAKEFS_SUPPORT_DEFLATE, and that needs miniz.c. CubeF4 1.28.0 does not contain this file, so the -defl switch is not available in this package; besides, the browser must understand Content-Encoding: deflate — the program's own help warns about this.


198з.3 SSI and CGI: live numbers and GET parameters

SSI is a comment tag in the page: <!--#uptime-->. httpd parses only files with the extensions .shtml, .shtm, .ssi, .xml, .json, finds the tag, asks the handler and inserts the answer in place of the tag (LWIP_HTTPD_SSI_INCLUDE_TAG 0 — the tag itself does not go into the response).

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 вернёт «сколько хотел» */
}

The handler gets the tag's index in the table and a buffer of length LWIP_HTTPD_MAX_TAG_INSERT_LEN. By default that is 192 bytes for every connection serving an SSI page; 32 is enough for us. A tag name is at most LWIP_HTTPD_MAX_TAG_NAME_LEN long, 8 characters by default: a _Static_assert in web_app.c makes sure our tags fit into it. The beginning of ssi_handler in the armclang build (50 instructions in total):

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

The switch became a chain of comparisons of the tag index, and the call to board_uptime_s is not yet resolved (f7fffffe) — the linker will fill in the address.

CGI in httpd is not a separate program, as in 142.3, but a function. A table maps an address to a handler; httpd itself splits the string after ? into pairs (up to 16 of them, LWIP_HTTPD_MAX_CGI_PARAMETERS) and passes them as arrays of names and values:

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";              /* какую страницу отдать в ответ */
}

The handler returns the name of the page to send in response. So the "turn on" link on the page changes the LED and immediately shows the page with the new value. But this is GET, and GET by its meaning changes nothing (142.2): a browser may open such a link in advance, on its own. For commands that change state, we will use POST below.


198з.4 REST: custom files with JSON and a password-protected POST

A page from flash does not work for JSON: the response is new every time. For this there are "custom files" (LWIP_HTTPD_CUSTOM_FILES): fs_open first calls fs_open_custom, and if that one recognises the name, the response comes from the application.

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 builds the whole response — the status line, Content-Type: application/json, Cache-Control: no-store, Content-Length and the body — in a piece of heap and marks it FS_FILE_FLAGS_HEADER_INCLUDED. There is no data in flash (data == NULL), so httpd reads it through fs_read_custom, and at the end of the response calls fs_close_custom, which frees the memory. The error responses /api/e400 and /api/e401 are made the same way.

httpd supports POST with LWIP_HTTPD_SUPPORT_POST: the application provides three functions. httpd_post_begin gets the address, the headers and the body length and decides whether to accept the request; httpd_post_receive_data gets the body packet by packet; httpd_post_finished names the page for the response. httpd shows the request headers only in the first function — so the password must be checked there too:

    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 показывает только здесь */
    }

Checking the character after the password is necessary: without it a longer password that starts with ours would also pass. The form body is parsed "all or nothing": first all fields are checked, then they are written. setpoint=5000 is rejected entirely, even if a valid led=1 stands next to it. The response to a successful POST is a fresh /api/state: the program immediately sees what was written.

Why Basic is weak protection. The password goes in every request as plain text: base64 is not a cipher but a way of writing bytes as letters. Anyone who sees the network traffic turns the string YWRtaW46c2VjcmV0MTk4 from the header back into admin:secret198 with one command. There is no encryption: this device has no TLS (what is visible in it before encryption — 107.5), and httpd has it only on top of mbedTLS (HTTPD_ENABLE_HTTPS, 0 by default). There is no limit on attempts either — you can guess the password as long as you like. Conclusion: such a device belongs only in a closed shop-floor or lab network; do not expose it to the internet, either directly or through port forwarding on a router. The Basic password here protects against an accidental click, not against an attacker.


198з.5 Experiment on the PC: the same code behind the tap0 interface

QEMU has no F407 Ethernet, so the device is built on the PC: lwIP 2.1.2 without an OS, the same lwipopts.h, the same web_app.c, and instead of DMA the Linux kernel tap0 interface (as in 106в). Everything runs in the stm-sys:1 container: gcc builds makefsdata, the pages and the device (warnings in our own files — 0), and Python and curl talk to it at 192.168.7.2 like a browser.

Request Response What came back
GET / 200, 1000 bytes a 165-byte header from fsdata; SSI: выкл, 250, no <!--# tags in the response
curl http://192.168.7.2/ 200 835 bytes of body
GET /api/state 200, 141 bytes {"uptime":1,"led":0,"setpoint":250,"hits":3}, Content-Length: 45
GET /led.cgi?on=1 200, 998 bytes the page; a following /api/state gives "led":1
GET /nothing 404, 355 bytes 404.html
POST without a password and with a wrong one 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

The page file is 871 bytes, the response body is 835: tags like <!--#uptime--> are longer than the numbers that took their place. The response time on the PC is a median of 0.10 ms for /api/state and 0.11 ms for the page (200 requests in a row), but that is the speed of the stack and the Linux kernel on the computer. How fast the F407 answers at 100 Mbit/s was not measured here.


198з.6 How many connections, and what happens with 10 browsers

The ceiling is set by MEMP_NUM_TCP_PCB — 10 TCP connection blocks (the listening port 80 takes a block from the separate pool MEMP_NUM_TCP_PCB_LISTEN). The first experiment: open 14 connections and stay silent, as a browser does when it connects "just in case". The device accepted 10 of 14; the stack dropped the four extra connection requests (SYN), and the tcp.memerr counter showed 4. On the PC 10 silent connections took 3772 bytes of the 10240-byte heap; on the F407 the state is shorter — 188 bytes per connection.

httpd closes silent connections itself: it polls every HTTPD_POLL_INTERVAL of 4 ticks of 0.5 s, and after HTTPD_MAX_RETRIES of 4 idle polls the connection is closed — 6–8 s depending on the timer phase. In the experiment all 10 closed after 7.6 s. A request beyond the limit, started at 2.3 s, connected after 7.1 s: the client repeats the SYN less and less often, and the next retry found blocks that had already been freed. For the user this is a page that thinks for seven seconds.

The second experiment is live requests: 10 "browsers" with one connection each, then six at a time — 60 simultaneous GET /. All went through: 10 of 10 and 60 of 60, the second batch in 0.02 s on the PC. Why we did not hit the limit of 10: each request is served in a fraction of a millisecond, and closed connections go into TIME-WAIT. It lasts 2 × TCP_MSL = 120 s, and after a series of requests all 10 blocks are occupied by exactly that (counter: time-wait 10). But the stack takes a block in TIME-WAIT first when it needs a new one (tcp_alloc → tcp_kill_timewait), so there were no refusals.

Conclusion: the device handles any number of short requests and exactly 10 hanging connections. The danger is not ten browsers but connections that are open and silent. A live connection will not push a new one out: httpd gives its connections the lowest priority TCP_PRIO_MIN, and tcp_kill_prio kills only those whose priority is lower than the new one. If there are many dashboards, let them poll /api/state less often (our page asks once every 2 s), and a continuous data stream is better handed to MQTT (198и).

Memory on the F407 (armclang, -Os, NO_SYS=1, our lwipopts.h):

Part Flash, bytes RAM, bytes
lwIP: core, IPv4, ARP, TCP, UDP 21492 25694
httpd and fs with fsdata 5723 1108
whole image with the 106а driver and the application 32544 37916

In RAM, lwIP is almost all the MEM_SIZE heap (the object mem.o — 10271 bytes) and the memp pools (memp.o — 15055 bytes, most of it the receive buffers). web_app.c itself is 1382 bytes of code and 316 bytes of constants. There are six files of our own, built with -Wall -Wextra, 0 warnings.


198з.7 Pitfalls

After an aborted POST the device answers busy until reset. If the client aborted a POST with a reset (RST), httpd frees the connection through http_err and does not call httpd_post_finished. An application that remembers "a POST is in progress" stays busy forever. In the experiment a POST aborted in the middle of the body gave busy immediately and after 5 s. The fix in web_app.c: a timestamp and dropping a stale busy state after 10 s (POST_STALE_MS) — after 11 s the request went through.

The SSI tag was not replaced, the browser shows nothing. httpd does not recognise a tag name longer than LWIP_HTTPD_MAX_TAG_NAME_LEN (8 by default) and sends it as is, and the browser hides the comment. Keep the names short and check the limit with _Static_assert.

The SSI value is truncated or the page is corrupted. snprintf returns not how much it wrote but how much it wanted to. If you return that to httpd, it will insert garbage from past the end of the buffer. Return at most len - 1, as in ssi_handler.

The eleventh browser "hangs" for seven seconds. All 10 TCP blocks are taken by silent connections; the new SYN is dropped, and the client waits for a retry. A larger MEMP_NUM_TCP_PCB means more RAM; a smaller HTTPD_MAX_RETRIES means silent connections are closed sooner.

Cyrillic text on the page turns into mojibake. makefsdata writes Content-Type: text/html without a charset. You need a <meta charset="utf-8"> line in the page itself.

A change to a page does not show up. The pages are built into the firmware: after editing fs/ you have to run makefsdata again and rebuild. The section check compares the saved fsdata_198i.c with a freshly generated one.

GET changes state. The link /led.cgi?on=1 is convenient, but a browser or an antivirus may open it on its own. Everything that changes the device goes through POST with a password.

The password "protects", but it is visible on the network. Basic without TLS is plain text. The device belongs only in a closed network, not on the internet.


198з.8 Summary

Full source code

lwipopts.h — lwIP settings for the web page and 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 — the web application on top of httpd: SSI, CGI, REST with JSON and a password-protected 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));
}

Next: section 198и "An MQTT client on the STM32: publish, subscribe and reconnect" — instead of browsers polling the device, the device itself sends data to a broker and keeps a single connection.

Open in the interactive handbook