23#if DETWS_ENABLE_WEBDAV
36static DavBufCtx s_dav;
43static bool dav_join(
const char *root,
const char *sub,
char *out,
size_t cap)
46 bool root_slash = (rlen > 0 && root[rlen - 1] ==
'/');
47 if (root_slash && sub[0] ==
'/')
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;
57static const char *dav_basename(
const char *name)
59 const char *slash = strrchr(name,
'/');
60 return slash ? slash + 1 : name;
65static bool dav_rm_recursive(fs::FS &fsys,
const char *path,
int depth)
69 fs::File d = fsys.open(path,
"r");
75 return fsys.remove(path);
79 fs::File c = d.openNextFile();
83 int wn = snprintf(cp,
sizeof(cp),
"%s/%s", path, dav_basename(c.name()));
85 if (wn <= 0 || wn >= (
int)
sizeof(cp))
90 if (!dav_rm_recursive(fsys, cp, depth + 1))
96 d = fsys.open(path,
"r");
101 return fsys.rmdir(path);
109static bool dav_copy_recursive(fs::FS &fsys,
const char *src,
const char *dst,
int depth)
114 fs::File s = fsys.open(src,
"r");
117 if (!s.isDirectory())
119 fs::File d = fsys.open(dst,
"w");
127 while ((cn = s.read(cbuf,
sizeof(cbuf))) > 0)
135 if (!fsys.mkdir(dst))
138 for (
int idx = 0;; idx++)
140 fs::File d = fsys.open(src,
"r");
144 for (
int i = 0; i <= idx; i++)
146 c = d.openNextFile();
156 snprintf(base,
sizeof(base),
"%s", dav_basename(c.name()));
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))
166 if (!dav_copy_recursive(fsys, sp, dp, depth + 1))
177static int dav_resolve_path(
const Route *r,
const char *reqpath,
char *out,
size_t cap)
180 if (plen > 0 && r->
path[plen - 1] ==
'*')
182 const char *sub = (strnlen(reqpath,
MAX_PATH_LEN) >= plen) ? reqpath + plen :
"";
183 if (strstr(sub,
".."))
185 const char *root = r->static_root ? r->static_root :
"";
186 if (!dav_join(root, sub, out, cap))
188 size_t fpl = strnlen(out, cap);
189 if (fpl > 1 && out[fpl - 1] ==
'/')
194#if DETWS_ENABLE_STREAM_BODY
215static DavPutCtx s_davput;
217bool DetWebServer::dav_put_begin_tramp(
HttpReq *req)
219 return s_davput.stream_srv && s_davput.stream_srv->dav_stream_put_begin(req);
221void DetWebServer::dav_put_data_tramp(
HttpReq *req,
const uint8_t *data,
size_t len)
223 if (s_davput.stream_srv)
224 s_davput.stream_srv->dav_stream_put_data(req, data, len);
226void DetWebServer::dav_put_abort_tramp(
HttpReq *req)
230 uint8_t slot = (uint8_t)(req -
http_pool);
231 if (slot <
MAX_CONNS && s_davput.put[slot].active)
233 s_davput.put[slot].file.close();
234 s_davput.put[slot].active =
false;
238bool DetWebServer::dav_stream_put_begin(
HttpReq *req)
240 if (strcmp(req->
method,
"PUT") != 0)
242 uint8_t slot = (uint8_t)(req -
http_pool);
243 for (uint8_t i = 0; i < _route_count; i++)
245 Route *r = &_routes[i];
258 if (dav_resolve_path(r, req->
path, fs_path,
sizeof(fs_path)) != 0)
260 DavPut *d = &s_davput.put[slot];
264 d->existed = r->static_fs->exists(fs_path);
265 d->file = r->static_fs->open(fs_path,
"w");
275void DetWebServer::dav_stream_put_data(
HttpReq *req,
const uint8_t *data,
size_t len)
277 uint8_t slot = (uint8_t)(req -
http_pool);
282 DavPut *d = &s_davput.put[slot];
283 if (d->active && !d->error)
285 if (d->file.write(data, len) != len)
293void DetWebServer::dav(
const char *url_prefix, fs::FS &file_sys,
const char *fs_root)
297 Route *r = &_routes[_route_count++];
301 if (n > 0 && url_prefix[n - 1] ==
'*')
302 snprintf(pat,
sizeof(pat),
"%s", url_prefix);
304 snprintf(pat,
sizeof(pat),
"%s*", url_prefix);
306 r->
type = RouteType::ROUTE_DAV;
308 r->static_fs = &file_sys;
309 r->static_root = fs_root;
311#if 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);
318void DetWebServer::dav_send_status(uint8_t slot_id,
int code,
const char *extra_headers)
320 if (!det_conn_active(slot_id))
326 const char *cl = resp_conn_hdr(slot_id, &keep);
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);
331 resp_end(slot_id, code, 0, keep);
334bool DetWebServer::try_serve_dav(uint8_t slot_id,
HttpReq *req)
336 for (uint8_t i = 0; i < _route_count; i++)
338 Route *r = &_routes[i];
345 serve_dav_request(slot_id, req, r);
351void DetWebServer::serve_dav_request(uint8_t slot_id,
HttpReq *req,
const Route *r)
357 dav_send_status(slot_id, 404,
"");
361 fs::FS &fsys = *r->static_fs;
364 int rc = dav_resolve_path(r, req->
path, fs_path,
sizeof(fs_path));
367 dav_send_status(slot_id, rc,
"");
373 if (plen > 0 && r->
path[plen - 1] ==
'*')
375 const char *root = r->static_root ? r->static_root :
"";
377 switch (webdav_method(req->
method))
379 case WebDavMethod::DAV_M_OPTIONS:
382 "OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK");
387 case WebDavMethod::DAV_M_GET:
388 case WebDavMethod::DAV_M_HEAD: {
389 fs::File f = fsys.open(fs_path,
"r");
392 dav_send_status(slot_id, 404,
"");
395 bool isdir = f.isDirectory();
399 dav_send_status(slot_id, 405,
"");
402 serve_file_internal(slot_id, webdav_method(req->
method) == WebDavMethod::DAV_M_HEAD, fsys, fs_path,
407 case WebDavMethod::DAV_M_PUT: {
408#if DETWS_ENABLE_STREAM_BODY
409 if (req->body_streaming)
412 DavPut *d = &s_davput.put[slot_id];
420 dav_send_status(slot_id, 409,
"");
425 dav_send_status(slot_id, 507,
"");
428 dav_send_status(slot_id, d->existed ? 204 : 201,
"");
433 bool existed = fsys.exists(fs_path);
434 fs::File f = fsys.open(fs_path,
"w");
437 dav_send_status(slot_id, 409,
"");
446 dav_send_status(slot_id, existed ? 204 : 201,
"");
450 case WebDavMethod::DAV_M_DELETE: {
451 if (!fsys.exists(fs_path))
453 dav_send_status(slot_id, 404,
"");
456 dav_send_status(slot_id, dav_rm_recursive(fsys, fs_path, 0) ? 204 : 403,
"");
460 case WebDavMethod::DAV_M_MKCOL:
461 if (fsys.exists(fs_path))
463 dav_send_status(slot_id, 405,
"");
466 dav_send_status(slot_id, fsys.mkdir(fs_path) ? 201 : 409,
"");
469 case WebDavMethod::DAV_M_COPY:
470 case WebDavMethod::DAV_M_MOVE: {
473 if (!dest_hdr || !webdav_dest_path(dest_hdr, dest_url,
sizeof(dest_url)))
475 dav_send_status(slot_id, 400,
"");
479 if (strncmp(dest_url, r->
path, plen) != 0)
481 dav_send_status(slot_id, 502,
"");
484 const char *dest_sub = dest_url + plen;
485 if (strstr(dest_sub,
".."))
487 dav_send_status(slot_id, 403,
"");
491 if (!dav_join(root, dest_sub, dest_fs,
sizeof(dest_fs)))
493 dav_send_status(slot_id, 414,
"");
496 size_t dpl = strnlen(dest_fs,
sizeof(dest_fs));
497 if (dpl > 1 && dest_fs[dpl - 1] ==
'/')
498 dest_fs[dpl - 1] =
'\0';
501 bool overwrite = !(ow && (ow[0] ==
'F' || ow[0] ==
'f'));
502 bool dest_exists = fsys.exists(dest_fs);
503 if (dest_exists && !overwrite)
505 dav_send_status(slot_id, 412,
"");
509 if (webdav_method(req->
method) == WebDavMethod::DAV_M_MOVE)
512 dav_rm_recursive(fsys, dest_fs, 0);
513 bool ok = fsys.rename(fs_path, dest_fs);
514 dav_send_status(slot_id, ok ? (dest_exists ? 204 : 201) : 409,
"");
521 fs::File src = fsys.open(fs_path,
"r");
524 dav_send_status(slot_id, 404,
"");
527 bool src_is_dir = src.isDirectory();
531 bool shallow = depth_h && depth_h[0] ==
'0';
534 dav_rm_recursive(fsys, dest_fs, 0);
537 if (src_is_dir && shallow)
538 ok = fsys.mkdir(dest_fs);
540 ok = dav_copy_recursive(fsys, fs_path, dest_fs, 0);
541 dav_send_status(slot_id, ok ? (dest_exists ? 204 : 201) : 409,
"");
545 case WebDavMethod::DAV_M_LOCK: {
547 unsigned long tok = (
unsigned long)millis();
549 tok ^= (
unsigned long)esp_random();
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",
564 snprintf(lt,
sizeof(lt),
"<%s>", token);
566 send(slot_id, 200,
"application/xml; charset=utf-8", s_dav.buf);
570 case WebDavMethod::DAV_M_UNLOCK:
571 dav_send_status(slot_id, 204,
"");
574 case WebDavMethod::DAV_M_PROPFIND: {
575 fs::File f = fsys.open(fs_path,
"r");
578 dav_send_status(slot_id, 404,
"");
581 bool isdir = f.isDirectory();
582 uint32_t fsize = (uint32_t)f.size();
583 time_t mtime = f.getLastWrite();
591 if (depth == DAV_DEPTH_INFINITY)
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);
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] !=
'/'))
606 if (sl + 1 <
sizeof(self_href))
608 self_href[sl++] =
'/';
609 self_href[sl] =
'\0';
613 size_t cap =
sizeof(s_dav.buf), len = 0;
614 len = webdav_ms_begin(s_dav.buf, cap, len);
617 len = webdav_ms_entry(s_dav.buf, cap, len, self_href, isdir, fsize, mt, isdir ?
"" :
mime_type(fs_path));
619 if (isdir && depth >= 1)
624 fs::File c = f.openNextFile();
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();
637 snprintf(chref,
sizeof(chref),
"%s%s%s", self_href, base, cdir ?
"/" :
"");
642 len = webdav_ms_entry(s_dav.buf, cap, len, chref, cdir, csize, cmtbuf, cdir ?
"" :
mime_type(base));
649 len = webdav_ms_end(s_dav.buf, cap, len);
650 send(slot_id, 207,
"application/xml; charset=utf-8", s_dav.buf);
654 case WebDavMethod::DAV_M_PROPPATCH: {
658 if (!fsys.exists(fs_path))
660 dav_send_status(slot_id, 404,
"");
663 size_t n = webdav_proppatch_ms(s_dav.buf,
sizeof(s_dav.buf), req->
path, (
const char *)req->
body, req->
body_len);
666 dav_send_status(slot_id, 507,
"");
669 send(slot_id, 207,
"application/xml; charset=utf-8", s_dav.buf);
673 case WebDavMethod::DAV_M_UNSUPPORTED:
677 "Allow: OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK\r\n");
@ 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.
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.
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.
void fill_route_base(Route *r, const char *path)
Register a route in the route table.
const char * status_text(int code)
Convert an HTTP status code to its standard reason phrase.
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.
RouteType type
HTTP, WS, or SSE.
DetIface iface_filter
Interface gate; DetIface::DETIFACE_ANY (0) = match any interface.
HttpMethod method
HTTP method (RouteType::ROUTE_HTTP only).
bool is_active
false for unused table slots.
bool is_wildcard
true when path ends with *.
char path[MAX_PATH_LEN]
Null-terminated path pattern.
bool det_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
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...