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 (RFC 4918) filesystem-backed request handler for DetWebServer.
7 *
8 * Split out of dwserver.cpp (single-purpose server files). The pure WebDAV core - method
9 * classification, the 207 Multi-Status XML builder, header parsing - lives in
10 * services/webdav/webdav.{h,cpp} and is host-tested; this file is the DetWebServer glue that
11 * needs a real filesystem (PROPFIND/PUT/COPY/MOVE/... over an fs::FS subtree). WEBDAV requires
12 * FILE_SERVING (enforced in ServerConfig.h), so the file-serving helpers it borrows are always
13 * present. Behaviour is identical to the pre-split code - a pure move.
14 */
15
16#include "services/webdav/webdav.h" // the pure WebDAV core
17#include "dwserver.h"
18#include "network_drivers/transport/tcp.h" // conn_pool
21#include <string.h>
22
23#if DETWS_ENABLE_WEBDAV
24// ---------------------------------------------------------------------------
25// WebDAV (RFC 4918) - filesystem-backed request handling. The pure core
26// (method classification + 207 XML builder + header parsing) lives in
27// services/webdav/webdav.{h,cpp} and is host-tested; this part needs a real FS.
28// ---------------------------------------------------------------------------
29
30// WebDAV response scratch, owned by one instance (internal linkage): the 207 Multi-Status build
31// buffer (BSS). One named owner, unreachable from any other translation unit.
32struct DavBufCtx
33{
34 char buf[DETWS_WEBDAV_BUF_SIZE];
35};
36static DavBufCtx s_dav;
37
38// http_rfc1123() lives in the FILE_SERVING section above (WEBDAV requires
39// FILE_SERVING), shared by both; used here for getlastmodified / creationdate.
40
41// Join an FS root and a subpath into @p out (mirrors serve_static_request's
42// separator handling). Returns false on overflow.
43static bool dav_join(const char *root, const char *sub, char *out, size_t cap)
44{
45 size_t rlen = strnlen(root, MAX_PATH_LEN);
46 bool root_slash = (rlen > 0 && root[rlen - 1] == '/');
47 if (root_slash && sub[0] == '/')
48 sub++;
49 bool sub_slash = (sub[0] == '/');
50 const char *sep = (root_slash || sub_slash) ? "" : "/";
51 int wn = snprintf(out, cap, "%s%s%s", root, sep, sub);
52 return wn > 0 && wn < (int)cap;
53}
54
55// The basename of an FS entry name (cores differ: name() may be a full path or a
56// bare name).
57static const char *dav_basename(const char *name)
58{
59 const char *slash = strrchr(name, '/');
60 return slash ? slash + 1 : name;
61}
62
63// Recursively delete a file or directory tree (bounded depth). Re-opens the
64// directory after each child removal so iteration is never mutated underneath us.
65static bool dav_rm_recursive(fs::FS &fsys, const char *path, int depth)
66{
67 if (depth > 8)
68 return false; // refuse pathologically deep trees rather than overflow the stack
69 fs::File d = fsys.open(path, "r");
70 if (!d)
71 return false;
72 if (!d.isDirectory())
73 {
74 d.close();
75 return fsys.remove(path);
76 }
77 for (;;)
78 {
79 fs::File c = d.openNextFile();
80 if (!c)
81 break;
82 char cp[256];
83 int wn = snprintf(cp, sizeof(cp), "%s/%s", path, dav_basename(c.name()));
84 c.close();
85 if (wn <= 0 || wn >= (int)sizeof(cp))
86 {
87 d.close();
88 return false;
89 }
90 if (!dav_rm_recursive(fsys, cp, depth + 1))
91 {
92 d.close();
93 return false;
94 }
95 d.close();
96 d = fsys.open(path, "r"); // reset the directory cursor after the deletion
97 if (!d)
98 return false;
99 }
100 d.close();
101 return fsys.rmdir(path);
102}
103
104// Recursively copy a file or directory tree from @p src to @p dst (bounded depth).
105// Unlike dav_rm_recursive we cannot re-open + take the first child each step (the
106// source is not consumed, so that would loop forever); instead we re-open and skip
107// to child #idx, which is safe even if a core invalidates an open dir handle across
108// the writes the copy makes to the destination tree.
109static bool dav_copy_recursive(fs::FS &fsys, const char *src, const char *dst, int depth)
110{
111 if (depth > 8)
112 return false; // refuse pathologically deep trees rather than overflow the stack
113
114 fs::File s = fsys.open(src, "r");
115 if (!s)
116 return false;
117 if (!s.isDirectory())
118 {
119 fs::File d = fsys.open(dst, "w");
120 if (!d)
121 {
122 s.close();
123 return false;
124 }
125 uint8_t cbuf[FILE_CHUNK_SIZE];
126 size_t cn;
127 while ((cn = s.read(cbuf, sizeof(cbuf))) > 0)
128 d.write(cbuf, cn);
129 s.close();
130 d.close();
131 return true;
132 }
133 s.close();
134
135 if (!fsys.mkdir(dst)) // create the destination collection (caller cleared any existing dst)
136 return false;
137
138 for (int idx = 0;; idx++)
139 {
140 fs::File d = fsys.open(src, "r");
141 if (!d)
142 return false;
143 fs::File c;
144 for (int i = 0; i <= idx; i++)
145 {
146 c = d.openNextFile();
147 if (!c)
148 break;
149 }
150 if (!c)
151 {
152 d.close();
153 break; // no child at this index - done
154 }
155 char base[128];
156 snprintf(base, sizeof(base), "%s", dav_basename(c.name()));
157 c.close();
158 d.close();
159
160 char sp[256];
161 char dp[256];
162 int wn1 = snprintf(sp, sizeof(sp), "%s/%s", src, base);
163 int wn2 = snprintf(dp, sizeof(dp), "%s/%s", dst, base);
164 if (wn1 <= 0 || wn1 >= (int)sizeof(sp) || wn2 <= 0 || wn2 >= (int)sizeof(dp))
165 return false;
166 if (!dav_copy_recursive(fsys, sp, dp, depth + 1))
167 return false;
168 }
169 return true;
170}
171
172// Map a WebDAV request path to its on-disk path under the mount @p r. Strips the
173// mount prefix, rejects traversal, joins onto the FS root, and drops a trailing
174// '/'. Returns 0 on success, else the HTTP error code (403 traversal, 414 too
175// long) - the single source of truth for the path check, shared by the request
176// handler and the streaming-PUT begin hook.
177static int dav_resolve_path(const Route *r, const char *reqpath, char *out, size_t cap)
178{
179 size_t plen = strnlen(r->path, MAX_PATH_LEN);
180 if (plen > 0 && r->path[plen - 1] == '*')
181 plen--;
182 const char *sub = (strnlen(reqpath, MAX_PATH_LEN) >= plen) ? reqpath + plen : "";
183 if (strstr(sub, ".."))
184 return 403;
185 const char *root = r->static_root ? r->static_root : "";
186 if (!dav_join(root, sub, out, cap))
187 return 414;
188 size_t fpl = strnlen(out, cap);
189 if (fpl > 1 && out[fpl - 1] == '/')
190 out[fpl - 1] = '\0';
191 return 0;
192}
193
194#if DETWS_ENABLE_STREAM_BODY
195// Per-connection streaming-PUT state for WebDAV: each slot streams its body to its
196// own file, so concurrent PUTs never clobber one another, and a transfer is never
197// bounded by BODY_BUF_SIZE. Indexed by the request's slot (req - http_pool).
198struct DavPut
199{
200 fs::File file; ///< destination file for this slot's PUT.
201 bool active; ///< file opened for the current PUT.
202 bool error; ///< a write (or the open) failed.
203 bool existed; ///< target existed before this PUT (204 vs 201).
204 size_t written; ///< bytes written so far.
205};
206
207// WebDAV streaming-PUT state, owned by one instance (internal linkage): the serving instance
208// and the per-slot destination-file state (each slot streams to its own file, so concurrent
209// PUTs never clobber one another). One named owner, unreachable from any other TU.
210struct DavPutCtx
211{
212 DetWebServer *stream_srv = nullptr;
213 DavPut put[MAX_CONNS];
214};
215static DavPutCtx s_davput;
216
217bool DetWebServer::dav_put_begin_tramp(HttpReq *req)
218{
219 return s_davput.stream_srv && s_davput.stream_srv->dav_stream_put_begin(req);
220}
221void DetWebServer::dav_put_data_tramp(HttpReq *req, const uint8_t *data, size_t len)
222{
223 if (s_davput.stream_srv)
224 s_davput.stream_srv->dav_stream_put_data(req, data, len);
225}
226void DetWebServer::dav_put_abort_tramp(HttpReq *req)
227{
228 // The PUT was torn down before the handler ran: close the half-written file so
229 // the handle is not leaked (a leak eventually exhausts LittleFS's open slots).
230 uint8_t slot = (uint8_t)(req - http_pool);
231 if (slot < MAX_CONNS && s_davput.put[slot].active)
232 {
233 s_davput.put[slot].file.close();
234 s_davput.put[slot].active = false;
235 }
236}
237
238bool DetWebServer::dav_stream_put_begin(HttpReq *req)
239{
240 if (strcmp(req->method, "PUT") != 0)
241 return false;
242 uint8_t slot = (uint8_t)(req - http_pool);
243 for (uint8_t i = 0; i < _route_count; i++)
244 {
245 Route *r = &_routes[i];
246 if (!r->is_active || r->type != RouteType::ROUTE_DAV)
247 continue;
248 if (!path_matches(r->path, r->is_wildcard, req->path))
249 continue;
250 if (r->iface_filter != DetIface::DETIFACE_ANY && r->iface_filter != det_conn_iface(slot))
251 continue;
252 // GCOVR_EXCL_START a RouteType::ROUTE_DAV route always carries static_fs (set in dav()); this null-guard
253 // cannot fire
254 if (!r->static_fs)
255 return false;
256 // GCOVR_EXCL_STOP
257 char fs_path[256];
258 if (dav_resolve_path(r, req->path, fs_path, sizeof(fs_path)) != 0)
259 return false; // traversal / too long - let it buffer; the handler answers 403/414
260 DavPut *d = &s_davput.put[slot];
261 d->active = false;
262 d->error = false;
263 d->written = 0;
264 d->existed = r->static_fs->exists(fs_path);
265 d->file = r->static_fs->open(fs_path, "w");
266 if (d->file)
267 d->active = true;
268 else
269 d->error = true;
270 return true; // stream regardless so the body is consumed and the handler replies
271 }
272 return false;
273}
274
275void DetWebServer::dav_stream_put_data(HttpReq *req, const uint8_t *data, size_t len)
276{
277 uint8_t slot = (uint8_t)(req - http_pool);
278 // GCOVR_EXCL_START req is always one of the http_pool slots, so slot < MAX_CONNS; the bound cannot fire
279 if (slot >= MAX_CONNS)
280 return;
281 // GCOVR_EXCL_STOP
282 DavPut *d = &s_davput.put[slot];
283 if (d->active && !d->error)
284 {
285 if (d->file.write(data, len) != len)
286 d->error = true;
287 else
288 d->written += len;
289 }
290}
291#endif // DETWS_ENABLE_STREAM_BODY
292
293void DetWebServer::dav(const char *url_prefix, fs::FS &file_sys, const char *fs_root)
294{
295 if (_route_count >= MAX_ROUTES)
296 return;
297 Route *r = &_routes[_route_count++];
298
299 char pat[MAX_PATH_LEN];
300 size_t n = strnlen(url_prefix, MAX_PATH_LEN);
301 if (n > 0 && url_prefix[n - 1] == '*')
302 snprintf(pat, sizeof(pat), "%s", url_prefix);
303 else
304 snprintf(pat, sizeof(pat), "%s*", url_prefix);
305 fill_route_base(r, pat);
306 r->type = RouteType::ROUTE_DAV;
307 r->method = HttpMethod::HTTP_GET; // unused: WebDAV dispatch keys off the raw method token
308 r->static_fs = &file_sys;
309 r->static_root = fs_root;
310
311#if DETWS_ENABLE_STREAM_BODY
312 // Stream PUT bodies straight to the file (one global sink; see DETWS_ENABLE_STREAM_BODY).
313 s_davput.stream_srv = this;
314 http_parser_set_stream_hooks(dav_put_begin_tramp, dav_put_data_tramp, dav_put_abort_tramp);
315#endif
316}
317
318void DetWebServer::dav_send_status(uint8_t slot_id, int code, const char *extra_headers)
319{
320 if (!det_conn_active(slot_id))
321 {
322 http_reset(slot_id);
323 return;
324 }
325 bool keep;
326 const char *cl = resp_conn_hdr(slot_id, &keep);
327 char header[RESP_HDR_BUF_SIZE];
328 int hlen = snprintf(header, sizeof(header), "HTTP/1.1 %d %s\r\n%sContent-Length: 0\r\n%s\r\n", code,
329 status_text(code), extra_headers ? extra_headers : "", cl);
330 det_conn_send(slot_id, header, (u16_t)hlen);
331 resp_end(slot_id, code, 0, keep);
332}
333
334bool DetWebServer::try_serve_dav(uint8_t slot_id, HttpReq *req)
335{
336 for (uint8_t i = 0; i < _route_count; i++)
337 {
338 Route *r = &_routes[i];
339 if (!r->is_active || r->type != RouteType::ROUTE_DAV)
340 continue;
341 if (!path_matches(r->path, r->is_wildcard, req->path))
342 continue;
343 if (r->iface_filter != DetIface::DETIFACE_ANY && r->iface_filter != det_conn_iface(slot_id))
344 continue;
345 serve_dav_request(slot_id, req, r);
346 return true;
347 }
348 return false;
349}
350
351void DetWebServer::serve_dav_request(uint8_t slot_id, HttpReq *req, const Route *r)
352{
353 // GCOVR_EXCL_START a RouteType::ROUTE_DAV route always carries static_fs (set in dav()); this null-guard cannot
354 // fire
355 if (!r->static_fs)
356 {
357 dav_send_status(slot_id, 404, "");
358 return;
359 }
360 // GCOVR_EXCL_STOP
361 fs::FS &fsys = *r->static_fs;
362
363 char fs_path[256];
364 int rc = dav_resolve_path(r, req->path, fs_path, sizeof(fs_path));
365 if (rc != 0)
366 {
367 dav_send_status(slot_id, rc, ""); // 403 traversal / 414 too long
368 return;
369 }
370
371 // Mount-prefix length and FS root, used by COPY/MOVE to resolve the Destination.
372 size_t plen = strnlen(r->path, MAX_PATH_LEN);
373 if (plen > 0 && r->path[plen - 1] == '*')
374 plen--;
375 const char *root = r->static_root ? r->static_root : "";
376
377 switch (webdav_method(req->method))
378 {
379 case WebDavMethod::DAV_M_OPTIONS:
380 add_response_header(slot_id, "DAV", "1, 2");
381 add_response_header(slot_id, "Allow",
382 "OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK");
383 add_response_header(slot_id, "MS-Author-Via", "DAV");
384 send_empty(slot_id, 200);
385 return;
386
387 case WebDavMethod::DAV_M_GET:
388 case WebDavMethod::DAV_M_HEAD: {
389 fs::File f = fsys.open(fs_path, "r");
390 if (!f)
391 {
392 dav_send_status(slot_id, 404, "");
393 return;
394 }
395 bool isdir = f.isDirectory();
396 f.close();
397 if (isdir)
398 {
399 dav_send_status(slot_id, 405, ""); // GET on a collection is not a download
400 return;
401 }
402 serve_file_internal(slot_id, webdav_method(req->method) == WebDavMethod::DAV_M_HEAD, fsys, fs_path,
403 mime_type(fs_path), nullptr);
404 return;
405 }
406
407 case WebDavMethod::DAV_M_PUT: {
408#if DETWS_ENABLE_STREAM_BODY
409 if (req->body_streaming)
410 {
411 // The body was written to this slot's file as it arrived (dav_stream_put_*).
412 DavPut *d = &s_davput.put[slot_id];
413 if (d->active)
414 {
415 d->file.close();
416 d->active = false; // closed here: the abort hook must not double-close
417 }
418 else
419 {
420 dav_send_status(slot_id, 409, ""); // parent missing / not writable
421 return;
422 }
423 if (d->error)
424 {
425 dav_send_status(slot_id, 507, ""); // a write failed (e.g. disk full)
426 return;
427 }
428 dav_send_status(slot_id, d->existed ? 204 : 201, "");
429 return;
430 }
431#endif
432 // Buffered fallback (streaming disabled): body bounded by BODY_BUF_SIZE.
433 bool existed = fsys.exists(fs_path);
434 fs::File f = fsys.open(fs_path, "w");
435 if (!f)
436 {
437 dav_send_status(slot_id, 409, ""); // parent missing / not writable
438 return;
439 }
440 // Only an empty CL:0 PUT reaches this buffered path (a bodied PUT to a DAV route streams, or
441 // bails before this switch since stream_begin's decline reasons also fail the top-level
442 // resolve), so body_len is always 0 here and the write never runs.
443 if (req->body_len)
444 f.write(req->body, req->body_len); // GCOVR_EXCL_LINE unreachable: body_len always 0 (see above)
445 f.close();
446 dav_send_status(slot_id, existed ? 204 : 201, "");
447 return;
448 }
449
450 case WebDavMethod::DAV_M_DELETE: {
451 if (!fsys.exists(fs_path))
452 {
453 dav_send_status(slot_id, 404, "");
454 return;
455 }
456 dav_send_status(slot_id, dav_rm_recursive(fsys, fs_path, 0) ? 204 : 403, "");
457 return;
458 }
459
460 case WebDavMethod::DAV_M_MKCOL:
461 if (fsys.exists(fs_path))
462 {
463 dav_send_status(slot_id, 405, ""); // already exists
464 return;
465 }
466 dav_send_status(slot_id, fsys.mkdir(fs_path) ? 201 : 409, "");
467 return;
468
469 case WebDavMethod::DAV_M_COPY:
470 case WebDavMethod::DAV_M_MOVE: {
471 const char *dest_hdr = http_get_header(req, "Destination");
472 char dest_url[256];
473 if (!dest_hdr || !webdav_dest_path(dest_hdr, dest_url, sizeof(dest_url)))
474 {
475 dav_send_status(slot_id, 400, "");
476 return;
477 }
478 // The destination must live under this same mount.
479 if (strncmp(dest_url, r->path, plen) != 0)
480 {
481 dav_send_status(slot_id, 502, "");
482 return;
483 }
484 const char *dest_sub = dest_url + plen;
485 if (strstr(dest_sub, ".."))
486 {
487 dav_send_status(slot_id, 403, "");
488 return;
489 }
490 char dest_fs[256];
491 if (!dav_join(root, dest_sub, dest_fs, sizeof(dest_fs)))
492 {
493 dav_send_status(slot_id, 414, "");
494 return;
495 }
496 size_t dpl = strnlen(dest_fs, sizeof(dest_fs));
497 if (dpl > 1 && dest_fs[dpl - 1] == '/')
498 dest_fs[dpl - 1] = '\0';
499
500 const char *ow = http_get_header(req, "Overwrite");
501 bool overwrite = !(ow && (ow[0] == 'F' || ow[0] == 'f'));
502 bool dest_exists = fsys.exists(dest_fs);
503 if (dest_exists && !overwrite)
504 {
505 dav_send_status(slot_id, 412, "");
506 return;
507 }
508
509 if (webdav_method(req->method) == WebDavMethod::DAV_M_MOVE)
510 {
511 if (dest_exists)
512 dav_rm_recursive(fsys, dest_fs, 0); // replace
513 bool ok = fsys.rename(fs_path, dest_fs);
514 dav_send_status(slot_id, ok ? (dest_exists ? 204 : 201) : 409, "");
515 return;
516 }
517
518 // COPY: a file or a whole collection (RFC 4918 9.8). Depth applies to a
519 // collection source: "0" copies just the collection itself, "infinity"
520 // (the default, also when absent) copies the entire tree.
521 fs::File src = fsys.open(fs_path, "r");
522 if (!src)
523 {
524 dav_send_status(slot_id, 404, "");
525 return;
526 }
527 bool src_is_dir = src.isDirectory();
528 src.close();
529
530 const char *depth_h = http_get_header(req, "Depth");
531 bool shallow = depth_h && depth_h[0] == '0'; // Depth: 0
532
533 if (dest_exists)
534 dav_rm_recursive(fsys, dest_fs, 0); // overwrite: clear the target first
535
536 bool ok;
537 if (src_is_dir && shallow)
538 ok = fsys.mkdir(dest_fs); // collection, Depth:0 - just the collection, no members
539 else
540 ok = dav_copy_recursive(fsys, fs_path, dest_fs, 0);
541 dav_send_status(slot_id, ok ? (dest_exists ? 204 : 201) : 409, "");
542 return;
543 }
544
545 case WebDavMethod::DAV_M_LOCK: {
546 // Advisory lock: issue a synthetic exclusive-write token (NOT enforced).
547 unsigned long tok = (unsigned long)millis();
548#ifdef ARDUINO
549 tok ^= (unsigned long)esp_random();
550#endif
551 char token[48];
552 snprintf(token, sizeof(token), "opaquelocktoken:%08lx-detws", tok);
553 snprintf(s_dav.buf, sizeof(s_dav.buf),
554 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
555 "<D:prop xmlns:D=\"DAV:\"><D:lockdiscovery><D:activelock>"
556 "<D:locktype><D:write/></D:locktype>"
557 "<D:lockscope><D:exclusive/></D:lockscope>"
558 "<D:depth>infinity</D:depth><D:timeout>Second-3600</D:timeout>"
559 "<D:locktoken><D:href>%s</D:href></D:locktoken>"
560 "</D:activelock></D:lockdiscovery></D:prop>\n",
561 token);
562 // RFC 4918 §10.5: Lock-Token uses a Coded-URL (angle-bracketed).
563 char lt[64];
564 snprintf(lt, sizeof(lt), "<%s>", token);
565 add_response_header(slot_id, "Lock-Token", lt);
566 send(slot_id, 200, "application/xml; charset=utf-8", s_dav.buf);
567 return;
568 }
569
570 case WebDavMethod::DAV_M_UNLOCK:
571 dav_send_status(slot_id, 204, ""); // advisory: nothing to release
572 return;
573
574 case WebDavMethod::DAV_M_PROPFIND: {
575 fs::File f = fsys.open(fs_path, "r");
576 if (!f)
577 {
578 dav_send_status(slot_id, 404, "");
579 return;
580 }
581 bool isdir = f.isDirectory();
582 uint32_t fsize = (uint32_t)f.size();
583 time_t mtime = f.getLastWrite();
584
585 int depth = webdav_depth(http_get_header(req, "Depth"), 1);
586
587 // RFC 4918 9.1.1: this server lists at most one level, so a Depth: infinity
588 // PROPFIND is rejected with 403 + the propfind-finite-depth precondition rather
589 // than silently returning a partial (one-level) 207 the client would read as
590 // complete. Clients wanting a listing use Depth: 0 or 1.
591 if (depth == DAV_DEPTH_INFINITY)
592 {
593 f.close();
594 static const char body[] = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"
595 "<D:error xmlns:D=\"DAV:\"><D:propfind-finite-depth/></D:error>\r\n";
596 send(slot_id, 403, "application/xml", body);
597 return;
598 }
599
600 // Self href: the request path, with a trailing '/' for a collection.
601 char self_href[MAX_PATH_LEN + 2];
602 snprintf(self_href, sizeof(self_href), "%s", req->path);
603 size_t sl = strnlen(self_href, sizeof(self_href));
604 if (isdir && (sl == 0 || self_href[sl - 1] != '/'))
605 {
606 if (sl + 1 < sizeof(self_href))
607 {
608 self_href[sl++] = '/';
609 self_href[sl] = '\0';
610 }
611 }
612
613 size_t cap = sizeof(s_dav.buf), len = 0;
614 len = webdav_ms_begin(s_dav.buf, cap, len);
615 char mt[40];
616 http_rfc1123(mtime, mt, sizeof(mt));
617 len = webdav_ms_entry(s_dav.buf, cap, len, self_href, isdir, fsize, mt, isdir ? "" : mime_type(fs_path));
618
619 if (isdir && depth >= 1)
620 {
621 int count = 0;
622 for (;;)
623 {
624 fs::File c = f.openNextFile();
625 if (!c)
626 break;
627 if (count >= DETWS_WEBDAV_MAX_ENTRIES)
628 {
629 c.close(); // GCOVR_EXCL_LINE a 2048B DETWS_WEBDAV_BUF_SIZE fills at ~8 entries, well before
630 break; // GCOVR_EXCL_LINE MAX_ENTRIES(32), so the buffer-full break preempts this cap
631 }
632 const char *base = dav_basename(c.name());
633 bool cdir = c.isDirectory();
634 uint32_t csize = (uint32_t)c.size();
635 time_t cmt = c.getLastWrite();
636 char chref[MAX_PATH_LEN + 80];
637 snprintf(chref, sizeof(chref), "%s%s%s", self_href, base, cdir ? "/" : "");
638 char cmtbuf[40];
639 http_rfc1123(cmt, cmtbuf, sizeof(cmtbuf));
640 c.close();
641 size_t before = len;
642 len = webdav_ms_entry(s_dav.buf, cap, len, chref, cdir, csize, cmtbuf, cdir ? "" : mime_type(base));
643 if (len == before)
644 break; // buffer full - stop listing
645 count++;
646 }
647 }
648 f.close();
649 len = webdav_ms_end(s_dav.buf, cap, len);
650 send(slot_id, 207, "application/xml; charset=utf-8", s_dav.buf);
651 return;
652 }
653
654 case WebDavMethod::DAV_M_PROPPATCH: {
655 // Read-only properties (no dead-property store): answer 207 with each
656 // requested property refused 403, rather than 405 - keeps Explorer/Finder,
657 // which PROPPATCH a timestamp right after a PUT, from erroring.
658 if (!fsys.exists(fs_path))
659 {
660 dav_send_status(slot_id, 404, "");
661 return;
662 }
663 size_t n = webdav_proppatch_ms(s_dav.buf, sizeof(s_dav.buf), req->path, (const char *)req->body, req->body_len);
664 if (!n)
665 {
666 dav_send_status(slot_id, 507, ""); // Insufficient Storage: response did not fit the buffer
667 return;
668 }
669 send(slot_id, 207, "application/xml; charset=utf-8", s_dav.buf);
670 return;
671 }
672
673 case WebDavMethod::DAV_M_UNSUPPORTED:
674 default:
675 dav_send_status(
676 slot_id, 405,
677 "Allow: OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK\r\n");
678 return;
679 }
680}
681#endif // DETWS_ENABLE_WEBDAV
@ DETIFACE_ANY
Unknown / no filter (matches any interface).
#define DETWS_WEBDAV_BUF_SIZE
Buffer (BSS) for a WebDAV 207 Multi-Status response, in bytes (see DETWS_ENABLE_WEBDAV).
#define RESP_HDR_BUF_SIZE
Stack buffer for HTTP response header lines in send() / send_empty() / send_unauth() / serve_file().
#define FILE_CHUNK_SIZE
Bytes read from the filesystem and passed to tcp_write() per loop().
#define DETWS_WEBDAV_MAX_ENTRIES
Maximum children listed in a WebDAV Depth-1 PROPFIND (bounds the response).
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define MAX_CONNS
Maximum simultaneous TCP connections (fixed static pool; ~3.95 KB of internal RAM per slot).
#define MAX_ROUTES
Maximum simultaneously registered routes.
Single-port HTTP server with deterministic, zero-allocation execution.
Definition dwserver.h:348
void add_response_header(uint8_t slot_id, const char *name, const char *value)
Queue a custom response header for the next send on this slot.
Definition response.cpp:276
void send(uint8_t slot_id, int code, const char *content_type, const char *payload)
Send an HTTP response with a body and close the connection.
void send_empty(uint8_t slot_id, int code)
Send a headers-only HTTP response and close the connection.
static const char * mime_type(const char *path)
Guess a Content-Type from a path's file extension.
Definition response.cpp:317
void fill_route_base(Route *r, const char *path)
Register a route in the route table.
Definition dwserver.cpp:747
const char * status_text(int code)
Convert an HTTP status code to its standard reason phrase.
Definition dwserver.cpp:112
Layer 7 (Application) - public HTTP routing API.
@ HTTP_GET
Safe, idempotent read.
Library-private declarations shared between dwserver.cpp and the src/server/*.cpp request-handler tra...
void http_rfc1123(time_t t, char *out, size_t cap)
Format t as an RFC 1123 GMT date into out (cap bytes); out is emptied for t <= 0.
const char * http_get_header(const HttpReq *req, const char *key)
Look up a header value by name (case-insensitive).
HttpReq http_pool[CONN_POOL_SLOTS]
Pool of parser contexts, one per connection-pool slot (incl. reserved dispatch slots).
Shared HTTP Content-Type ("MIME") string constants (one source of truth).
void http_reset(uint8_t slot_id)
Reset the HTTP parser for a connection slot.
Fully-parsed HTTP/1.1 request.
char method[DETWS_METHOD_BUF_SIZE]
HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).
uint8_t body[BODY_BUF_SIZE+1]
Stored body bytes, always null-terminated.
size_t body_len
Bytes stored in body[] (≤ BODY_BUF_SIZE).
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.
Internal route entry stored in the routing table.
Definition dwserver.h:244
RouteType type
HTTP, WS, or SSE.
Definition dwserver.h:246
DetIface iface_filter
Interface gate; DetIface::DETIFACE_ANY (0) = match any interface.
Definition dwserver.h:277
HttpMethod method
HTTP method (RouteType::ROUTE_HTTP only).
Definition dwserver.h:247
bool is_active
false for unused table slots.
Definition dwserver.h:273
bool is_wildcard
true when path ends with *.
Definition dwserver.h:274
char path[MAX_PATH_LEN]
Null-terminated path pattern.
Definition dwserver.h:245
bool det_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:362
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
WebDAV server core (RFC 4918): method classification, header parsing, and the 207 Multi-Status XML bu...