DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
multipart.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file multipart.cpp
6 * @brief In-place multipart/form-data parser implementation.
7 */
8
9#include "multipart.h"
10#include <string.h>
11
12// Length-bounded, binary-safe forward search for needle[0..nlen) within hay[0..hlen).
13// Unlike strstr, it does not stop at a NUL, so a body containing NUL bytes scans correctly.
14static char *mem_find(char *hay, size_t hlen, const char *needle, size_t nlen)
15{
16 if (nlen == 0 || nlen > hlen)
17 return nullptr;
18 for (size_t i = 0; i + nlen <= hlen; i++)
19 if (memcmp(hay + i, needle, nlen) == 0)
20 return hay + i;
21 return nullptr;
22}
23
24// Extract parameter value: search for `key="<value>"` inside `src`.
25// If found, null-terminates in-place and returns pointer to the value.
26// Returns nullptr if not found.
27static char *extract_quoted_param(char *src, const char *key)
28{
29 char *p = strstr(src, key);
30 if (!p)
31 return nullptr;
32 constexpr size_t key_max = 32; // param keys are short literals ("name=", "filename=")
33 p += strnlen(key, key_max);
34 if (*p != '"')
35 return nullptr;
36 p++; // skip opening quote
37 char *end = strchr(p, '"');
38 if (!end)
39 return nullptr;
40 *end = '\0';
41 return p;
42}
43
45{
46 mp->part_count = 0;
47
48 const char *ct = http_get_header(req, "Content-Type");
49 if (!ct)
50 return false;
51
52 // Extract boundary value (may be quoted or unquoted)
53 const char *bsearch = strstr(ct, "boundary=");
54 if (!bsearch)
55 return false;
56 bsearch += 9;
57 if (*bsearch == '"')
58 bsearch++;
59
60 char bval[MAX_BOUNDARY_LEN + 1];
61 size_t blen = 0;
62 while (*bsearch && *bsearch != '"' && *bsearch != ';' && *bsearch != ' ' && blen < MAX_BOUNDARY_LEN)
63 bval[blen++] = *bsearch++;
64 bval[blen] = '\0';
65
66 if (blen == 0)
67 return false;
68
69 // Delimiter is "--" + boundary
70 char delim[MAX_BOUNDARY_LEN + 3];
71 delim[0] = '-';
72 delim[1] = '-';
73 memcpy(delim + 2, bval, blen + 1); // includes null
74 size_t dlen = blen + 2;
75
76 char *body = (char *)req->body;
77 char *end = body + req->body_len; // length-bounded scanning: NUL bytes in a binary part are fine
78
79 // A part's data ends at the full "\r\n--boundary" delimiter (RFC 2046): matching only the
80 // "--boundary" bytes would false-truncate a binary part that happens to contain them.
81 char ddelim[MAX_BOUNDARY_LEN + 5];
82 ddelim[0] = '\r';
83 ddelim[1] = '\n';
84 memcpy(ddelim + 2, delim, dlen); // "--boundary" (dlen bytes, no NUL)
85 size_t ddlen = dlen + 2;
86
87 // Find the first delimiter ("--boundary"; a leading CRLF / preamble is optional here).
88 char *pos = mem_find(body, (size_t)(end - body), delim, dlen);
89 if (!pos)
90 return false;
91 pos += dlen;
92 if (pos + 2 <= end && pos[0] == '\r' && pos[1] == '\n')
93 pos += 2;
94
96 {
97 // "--" immediately after the delimiter marks the terminating boundary.
98 if (pos + 2 <= end && pos[0] == '-' && pos[1] == '-')
99 break;
100
101 MultipartPart *part = &mp->parts[mp->part_count];
102 part->name = nullptr;
103 part->filename = nullptr;
104 part->type = nullptr;
105 part->data = nullptr;
106 part->data_len = 0;
107
108 // Parse the per-part headers (text) until the blank line.
109 for (;;)
110 {
111 if (pos + 2 <= end && pos[0] == '\r' && pos[1] == '\n')
112 {
113 pos += 2; // blank line → start of data
114 break;
115 }
116
117 char *line_end = mem_find(pos, (size_t)(end - pos), "\r\n", 2);
118 if (!line_end)
119 return false;
120
121 *line_end = '\0'; // null-terminate header line
122
123 if (strncasecmp(pos, "Content-Disposition:", 20) == 0)
124 {
125 char *v = pos + 20;
126 while (*v == ' ')
127 v++;
128 // Extract filename before name: filename= appears after name= in the
129 // header, so extracting it first avoids corrupting name='s search
130 // when extract_quoted_param null-terminates the value in-place.
131 part->filename = extract_quoted_param(v, "filename=");
132 part->name = extract_quoted_param(v, "name=");
133 }
134 else if (strncasecmp(pos, "Content-Type:", 13) == 0)
135 {
136 char *v = pos + 13;
137 while (*v == ' ')
138 v++;
139 part->type = v;
140 }
141
142 pos = line_end + 2; // next line (skip '\0' + '\n')
143 }
144
145 // Data runs from pos until the next "\r\n--boundary" (binary-safe, length-bounded).
146 char *next = mem_find(pos, (size_t)(end - pos), ddelim, ddlen);
147 if (!next)
148 return false;
149
150 part->data = pos;
151 part->data_len = (size_t)(next - pos);
152 *next = '\0'; // terminate at the CRLF so a text part is still usable as a C-string
153
154 mp->part_count++;
155
156 pos = next + ddlen; // past "\r\n--boundary"
157 if (pos + 2 <= end && pos[0] == '\r' && pos[1] == '\n')
158 pos += 2;
159 }
160
161 return mp->part_count > 0;
162}
163
164const char *multipart_get_field(const Multipart *mp, const char *field)
165{
166 for (int i = 0; i < mp->part_count; i++)
167 {
168 if (mp->parts[i].name && strcmp(mp->parts[i].name, field) == 0)
169 return mp->parts[i].data;
170 }
171 return nullptr;
172}
#define MAX_BOUNDARY_LEN
Maximum MIME boundary length (RFC 2046 allows up to 70 characters).
#define MAX_MULTIPART_PARTS
Maximum simultaneously parsed multipart parts per request.
const char * http_get_header(const HttpReq *req, const char *key)
Look up a header value by name (case-insensitive).
const char * multipart_get_field(const Multipart *mp, const char *field)
Look up a field value across all parsed parts by name.
bool multipart_parse(HttpReq *req, Multipart *mp)
Parse the body of req as multipart/form-data.
Definition multipart.cpp:44
In-place multipart/form-data parser (RFC 7578).
Fully-parsed HTTP/1.1 request.
uint8_t body[BODY_BUF_SIZE+1]
Stored body bytes, always null-terminated.
size_t body_len
Bytes stored in body[] (≤ BODY_BUF_SIZE).
One parsed part from a multipart body.
Definition multipart.h:47
const char * data
Part body (null-terminated in-place).
Definition multipart.h:51
const char * type
Content-Type of this part, or nullptr.
Definition multipart.h:50
size_t data_len
Part body length in bytes (not counting the null).
Definition multipart.h:52
const char * name
Form field name from Content-Disposition, or nullptr.
Definition multipart.h:48
const char * filename
Upload filename from Content-Disposition, or nullptr.
Definition multipart.h:49
Container for all parsed parts of a multipart body.
Definition multipart.h:59
MultipartPart parts[MAX_MULTIPART_PARTS]
Parsed parts.
Definition multipart.h:60
int part_count
Number of valid entries in parts[].
Definition multipart.h:61