DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
webdav.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 webdav.cpp
6 * @brief WebDAV server core (RFC 4918): method classification, header parsing,
7 * and the 207 Multi-Status XML builder. Pure - no sockets, no filesystem.
8 */
9
12
13#if DETWS_ENABLE_WEBDAV
14
15#include <string.h>
16
17WebDavMethod webdav_method(const char *m)
18{
19 if (!m)
20 return WebDavMethod::DAV_M_UNSUPPORTED;
21 if (!strcmp(m, "OPTIONS"))
22 return WebDavMethod::DAV_M_OPTIONS;
23 if (!strcmp(m, "GET"))
24 return WebDavMethod::DAV_M_GET;
25 if (!strcmp(m, "HEAD"))
26 return WebDavMethod::DAV_M_HEAD;
27 if (!strcmp(m, "PUT"))
28 return WebDavMethod::DAV_M_PUT;
29 if (!strcmp(m, "DELETE"))
30 return WebDavMethod::DAV_M_DELETE;
31 if (!strcmp(m, "PROPFIND"))
32 return WebDavMethod::DAV_M_PROPFIND;
33 if (!strcmp(m, "PROPPATCH"))
34 return WebDavMethod::DAV_M_PROPPATCH;
35 if (!strcmp(m, "MKCOL"))
36 return WebDavMethod::DAV_M_MKCOL;
37 if (!strcmp(m, "COPY"))
38 return WebDavMethod::DAV_M_COPY;
39 if (!strcmp(m, "MOVE"))
40 return WebDavMethod::DAV_M_MOVE;
41 if (!strcmp(m, "LOCK"))
42 return WebDavMethod::DAV_M_LOCK;
43 if (!strcmp(m, "UNLOCK"))
44 return WebDavMethod::DAV_M_UNLOCK;
45 return WebDavMethod::DAV_M_UNSUPPORTED;
46}
47
48int webdav_depth(const char *depth_hdr, int dflt)
49{
50 if (!depth_hdr || !depth_hdr[0])
51 return dflt;
52 if (!strcmp(depth_hdr, "0"))
53 return 0;
54 if (!strcmp(depth_hdr, "1"))
55 return 1;
56 if (!strcmp(depth_hdr, "infinity"))
57 return DAV_DEPTH_INFINITY;
58 return dflt;
59}
60
61// Append a NUL-terminated string if it fits; returns false (leaving *len and the
62// NUL terminator intact) when it would overflow.
63static bool app(char *buf, size_t cap, size_t *len, const char *s)
64{
65 size_t n = strnlen(s, cap + 1);
66 if (*len + n + 1 > cap)
67 return false;
68 memcpy(buf + *len, s, n);
69 *len += n;
70 buf[*len] = '\0';
71 return true;
72}
73
74size_t webdav_xml_escape(char *dst, size_t cap, const char *src)
75{
76 size_t o = 0;
77 if (cap == 0)
78 return 0;
79 for (const char *p = src; *p; p++)
80 {
81 const char *rep = nullptr;
82 switch (*p)
83 {
84 case '&':
85 rep = "&amp;";
86 break;
87 case '<':
88 rep = "&lt;";
89 break;
90 case '>':
91 rep = "&gt;";
92 break;
93 case '"':
94 rep = "&quot;";
95 break;
96 case '\'':
97 rep = "&apos;";
98 break;
99 default:
100 break;
101 }
102 if (rep)
103 {
104 size_t rn = strnlen(rep, cap + 1);
105 if (o + rn + 1 > cap)
106 break;
107 memcpy(dst + o, rep, rn);
108 o += rn;
109 }
110 else
111 {
112 if (o + 1 + 1 > cap)
113 break;
114 dst[o++] = *p;
115 }
116 }
117 dst[o] = '\0';
118 return o;
119}
120
121bool webdav_dest_path(const char *destination, char *out, size_t cap)
122{
123 if (!destination || !out || cap == 0)
124 return false;
125
126 // Skip an absolute-URI scheme + authority: after "://", advance to the first
127 // '/' (the path). An abs-path value ("/p/q") is used as-is.
128 const char *p = destination;
129 const char *scheme = strstr(destination, "://");
130 if (scheme)
131 {
132 p = scheme + 3;
133 while (*p && *p != '/')
134 p++;
135 if (*p != '/')
136 return false; // authority with no path
137 }
138 else if (*p != '/')
139 {
140 return false; // not an absolute path
141 }
142
143 // Percent-decode into out. A while loop so the %XX case can consume its two
144 // extra hex digits without mutating a for-loop counter.
145 size_t o = 0;
146 while (*p)
147 {
148 char c = *p;
149 if (c == '%')
150 {
151 int hi = det_hex_val(p[1]);
152 int lo = (hi >= 0) ? det_hex_val(p[2]) : -1;
153 if (hi < 0 || lo < 0)
154 return false; // malformed escape
155 c = (char)((hi << 4) | lo);
156 p += 2;
157 }
158 if (o + 1 >= cap)
159 return false; // no room for char + NUL
160 out[o++] = c;
161 p++;
162 }
163 out[o] = '\0';
164 return true;
165}
166
167size_t webdav_ms_begin(char *buf, size_t cap, size_t len)
168{
169 app(buf, cap, &len, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus xmlns:D=\"DAV:\">\n");
170 return len;
171}
172
173size_t webdav_ms_entry(char *buf, size_t cap, size_t len, const char *href, bool is_collection, uint32_t size,
174 const char *rfc1123_mtime, const char *content_type)
175{
176 // Build the whole <response> in a temp first so the append is atomic: a
177 // partial element is never left in the document when the buffer fills.
178 char tmp[512];
179 size_t t = 0;
180 char esc[256];
181
182 webdav_xml_escape(esc, sizeof(esc), href);
183 // The href block is at most 26 + esc(<=255) + 62 == 343 bytes, and adding the collection
184 // marker + resourcetype close reaches <=376 - all well within tmp[512], so these three
185 // atomic-append guards cannot fire (esc is capped by its own 256-byte buffer above).
186 if (!app(tmp, sizeof(tmp), &t, " <D:response>\n <D:href>") || !app(tmp, sizeof(tmp), &t, esc) ||
187 !app(tmp, sizeof(tmp), &t, "</D:href>\n <D:propstat>\n <D:prop>\n <D:resourcetype>"))
188 return len; // GCOVR_EXCL_LINE unreachable: href block <=343 < tmp[512] (see above)
189
190 if (is_collection)
191 {
192 if (!app(tmp, sizeof(tmp), &t, "<D:collection/>"))
193 return len; // GCOVR_EXCL_LINE unreachable: <=358 < tmp[512] (see above)
194 }
195 if (!app(tmp, sizeof(tmp), &t, "</D:resourcetype>\n"))
196 return len; // GCOVR_EXCL_LINE unreachable: <=376 < tmp[512] (see above)
197
198 if (!is_collection)
199 {
200 char num[24];
201 unsigned long s = (unsigned long)size;
202 // minimal itoa to avoid pulling in snprintf in the pure core
203 char rev[24];
204 int rn = 0;
205 do
206 {
207 rev[rn++] = (char)('0' + (int)(s % 10));
208 s /= 10;
209 } while (s && rn < (int)sizeof(rev));
210 int ni = 0;
211 while (rn > 0)
212 num[ni++] = rev[--rn];
213 num[ni] = '\0';
214 if (!app(tmp, sizeof(tmp), &t, " <D:getcontentlength>") || !app(tmp, sizeof(tmp), &t, num) ||
215 !app(tmp, sizeof(tmp), &t, "</D:getcontentlength>\n"))
216 return len;
217 if (content_type && content_type[0])
218 {
219 if (!app(tmp, sizeof(tmp), &t, " <D:getcontenttype>") || !app(tmp, sizeof(tmp), &t, content_type) ||
220 !app(tmp, sizeof(tmp), &t, "</D:getcontenttype>\n"))
221 return len;
222 }
223 }
224
225 if (rfc1123_mtime && rfc1123_mtime[0])
226 {
227 if (!app(tmp, sizeof(tmp), &t, " <D:getlastmodified>") || !app(tmp, sizeof(tmp), &t, rfc1123_mtime) ||
228 !app(tmp, sizeof(tmp), &t, "</D:getlastmodified>\n"))
229 return len;
230 }
231
232 if (!app(tmp, sizeof(tmp), &t,
233 " </D:prop>\n <D:status>HTTP/1.1 200 OK</D:status>\n"
234 " </D:propstat>\n </D:response>\n"))
235 return len;
236
237 // Atomic commit: app() appends the finished element only if it fits and leaves
238 // len unchanged on no-room, so the caller sees an unchanged len and stops adding.
239 app(buf, cap, &len, tmp);
240 return len;
241}
242
243size_t webdav_ms_end(char *buf, size_t cap, size_t len)
244{
245 app(buf, cap, &len, "</D:multistatus>\n");
246 return len;
247}
248
249// True for a byte that ends an XML element name (whitespace, '/', '>').
250static bool name_end_char(char c)
251{
252 return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '/' || c == '>';
253}
254
255size_t webdav_proppatch_ms(char *buf, size_t cap, const char *href, const char *body, size_t body_len)
256{
257 size_t len = 0;
258 if (cap)
259 buf[0] = '\0'; // always a valid C-string, even if nothing below fits
260 char esc[256];
261 webdav_xml_escape(esc, sizeof(esc), href);
262 if (!app(buf, cap, &len,
263 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus xmlns:D=\"DAV:\">\n"
264 " <D:response>\n <D:href>") ||
265 !app(buf, cap, &len, esc) || !app(buf, cap, &len, "</D:href>\n <D:propstat>\n <D:prop>\n"))
266 return 0;
267
268 // Walk the request and echo every element that sits directly inside a <prop>
269 // (across all <set>/<remove> blocks) as a self-closed element. The wrappers
270 // (propertyupdate / set / remove / prop) are skipped; only the properties are
271 // reflected, each refused 403 below.
272 int emitted = 0;
273 bool in_prop = false;
274 size_t i = 0;
275 while (i < body_len && emitted < DETWS_WEBDAV_MAX_PROPS)
276 {
277 if (body[i] != '<')
278 {
279 i++;
280 continue;
281 }
282 size_t start = i + 1;
283 if (start < body_len && (body[start] == '?' || body[start] == '!'))
284 {
285 while (i < body_len && body[i] != '>') // skip PI / comment / declaration
286 i++;
287 i++;
288 continue;
289 }
290 bool closing = (start < body_len && body[start] == '/');
291 if (closing)
292 start++;
293 size_t end = start;
294 while (end < body_len && body[end] != '>')
295 end++;
296 if (end >= body_len)
297 break; // unterminated tag
298 bool self_closed = (end > start && body[end - 1] == '/');
299 size_t name_end = start;
300 while (name_end < end && !name_end_char(body[name_end]))
301 name_end++;
302 size_t local = start; // local name = after the last ':' in the qualified name
303 for (size_t k = start; k < name_end; k++)
304 if (body[k] == ':')
305 local = k + 1;
306 bool is_prop = (name_end - local) == 4 && !strncmp(&body[local], "prop", 4);
307
308 if (closing)
309 {
310 if (is_prop)
311 in_prop = false;
312 i = end + 1;
313 continue;
314 }
315 if (is_prop)
316 {
317 if (!self_closed) // <prop> opens a block; <prop/> is an empty block
318 in_prop = true;
319 i = end + 1;
320 continue;
321 }
322 if (in_prop)
323 {
324 // Echo this property element self-closed. Copy the open-tag content
325 // (name + its own xmlns/attrs), dropping a trailing '/' and trailing
326 // whitespace; reject a span containing '<' so nothing is injected.
327 size_t copy_end = self_closed ? end - 1 : end;
328 while (copy_end > start && (body[copy_end - 1] == ' ' || body[copy_end - 1] == '\t' ||
329 body[copy_end - 1] == '\r' || body[copy_end - 1] == '\n'))
330 copy_end--;
331 bool ok = copy_end > start;
332 for (size_t k = start; k < copy_end && ok; k++)
333 if (body[k] == '<')
334 ok = false;
335 char tag[256];
336 size_t tl = copy_end - start;
337 if (ok && tl < sizeof(tag))
338 {
339 memcpy(tag, &body[start], tl);
340 tag[tl] = '\0';
341 if (app(buf, cap, &len, " <") && app(buf, cap, &len, tag) && app(buf, cap, &len, "/>\n"))
342 emitted++;
343 }
344 if (!self_closed)
345 {
346 // Skip the property's value up to its close tag (no nesting expected).
347 size_t j = end + 1;
348 while (j + 1 < body_len && !(body[j] == '<' && body[j + 1] == '/'))
349 j++;
350 while (j < body_len && body[j] != '>')
351 j++;
352 i = (j < body_len) ? j + 1 : body_len;
353 }
354 else
355 i = end + 1;
356 continue;
357 }
358 i = end + 1;
359 }
360
361 if (!app(buf, cap, &len,
362 " </D:prop>\n <D:status>HTTP/1.1 403 Forbidden</D:status>\n"
363 " </D:propstat>\n </D:response>\n</D:multistatus>\n"))
364 return 0;
365 return len;
366}
367
368#endif // DETWS_ENABLE_WEBDAV
#define DETWS_WEBDAV_MAX_PROPS
Maximum properties echoed in a WebDAV PROPPATCH 207 response (bounds the response).
Tiny no-stdlib hex encode / decode / nibble helpers (one shared copy).
int det_hex_val(char c)
Hex digit -> 0..15, or -1 if c is not a hex digit (either case).
Definition hex.h:31
WebDAV server core (RFC 4918): method classification, header parsing, and the 207 Multi-Status XML bu...