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=valueparameterscustom 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:
GET /— the pageindex.shtmlwith four live values (SSI);GET /led.cgi?on=1— turn the LED on and return the same page (CGI);GET /api/state— the state as JSON (a custom "file");POST /api/setwith the bodyled=1&setpoint=300— write, only with the password;- everything else —
404.html.
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
- httpd from lwIP 2.1.2 serves pages from flash without copying: two
pages after
makefsdatatake 1419 bytes, with the HTTP headers already inside. - SSI substitutes values into
.shtml, CGI parses up to 16 GET parameters, custom files provide JSON, and POST is accepted by three application functions. - On the PC the same
web_app.canswered all requests as intended: 200, 401, 400, 404 — 11 kinds of requests from the table in 198з.5. - The limit is 10 connections: of 14 silent ones 10 were accepted, the extra request waited 7.1 s; 60 short requests at once — all 60 went through.
- httpd closes silent connections itself after 6–8 s (7.6 s in the experiment); closed ones sit in TIME-WAIT for 120 s, but the stack takes them first.
- On the F407: lwIP takes 21492 bytes of flash and 25694 of RAM, httpd with the pages 5723 and 1108; the connection state is 188 bytes of heap.
- Basic is not protection: base64 is reversible and there is no TLS; the device belongs only in a closed network.
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.