37#if PC_ENABLE_OTA || PC_ENABLE_UPLOAD
38#error "PC_ENABLE_WEBDAV cannot be combined with PC_ENABLE_OTA or PC_ENABLE_UPLOAD: the parser's \
39streaming-body sink is a single global hook, so whichever registers last silently disables the others."
53static const size_t PC_WEBDAV_MIN_ENTRY_BYTES = 192;
55 "PC_WEBDAV_BUF_SIZE is large enough to hold PC_WEBDAV_MAX_ENTRIES entries: raise "
56 "PC_WEBDAV_MAX_ENTRIES or lower PC_WEBDAV_BUF_SIZE so the buffer bound stays the "
57 "binding one (see the PROPFIND listing loop).");
65static DavBufCtx s_dav;
72static bool dav_join(
const char *root,
const char *sub,
char *out,
size_t cap)
75 bool root_slash = (rlen > 0 && root[rlen - 1] ==
'/');
76 if (root_slash && sub[0] ==
'/')
80 bool sub_slash = (sub[0] ==
'/');
81 const char *sep = (root_slash || sub_slash) ?
"" :
"/";
82 pc_sb sb_out = {out, cap, 0,
true};
90 return wn > 0 && wn < (int)cap;
95static const char *dav_basename(
const char *name)
97 const char *slash = strrchr(name,
'/');
100 return slash ? slash + 1 : name;
105static bool dav_rm_recursive(fs::FS &fsys,
const char *path,
int depth)
111 fs::File d = fsys.open(path,
"r");
116 if (!d.isDirectory())
119 return fsys.remove(path);
123 fs::File c = d.openNextFile();
129 pc_sb sb_cp = {cp,
sizeof(cp), 0,
true};
132 pc_sb_put(&sb_cp, dav_basename(c.name()));
141 if (wn <= 0 || wn >= (
int)
sizeof(cp))
147 if (!dav_rm_recursive(fsys, cp, depth + 1))
153 d = fsys.open(path,
"r");
165 return fsys.rmdir(path);
173static bool dav_copy_recursive(fs::FS &fsys,
const char *src,
const char *dst,
int depth)
180 fs::File s = fsys.open(src,
"r");
185 if (!s.isDirectory())
187 fs::File d = fsys.open(dst,
"w");
195 while ((cn = s.read(cbuf,
sizeof(cbuf))) > 0)
205 if (!fsys.mkdir(dst))
210 for (
int idx = 0;; idx++)
212 fs::File d = fsys.open(src,
"r");
223 for (
int i = 0; i <= idx; i++)
225 c = d.openNextFile();
237 pc_sb sb_base = {base,
sizeof(base), 0,
true};
238 pc_sb_put(&sb_base, dav_basename(c.name()));
248 pc_sb sb_sp = {sp,
sizeof(sp), 0,
true};
253 pc_sb sb_dp = {dp,
sizeof(dp), 0,
true};
263 if (wn1 <= 0 || wn1 >= (
int)
sizeof(sp) || wn2 <= 0 || wn2 >= (
int)
sizeof(dp))
268 if (!dav_copy_recursive(fsys, sp, dp, depth + 1))
281static int dav_resolve_path(
const Route *r,
const char *reqpath,
char *out,
size_t cap)
286 if (plen > 0 && r->
path[plen - 1] ==
'*')
294 const char *sub = (strnlen(reqpath,
MAX_PATH_LEN) >= plen) ? reqpath + plen :
"";
296 if (strstr(sub,
".."))
300 const char *root = r->static_root ? r->static_root :
"";
301 if (!dav_join(root, sub, out, cap))
305 size_t fpl = strnlen(out, cap);
306 if (fpl > 1 && out[fpl - 1] ==
'/')
313#if PC_ENABLE_STREAM_BODY
332 PC *stream_srv =
nullptr;
335static DavPutCtx s_davput;
343static DavLockCtx s_dav_lock;
347static bool dav_write_blocked(
HttpReq *req,
const char *path)
350 char tok[PC_DAV_LOCK_TOKEN_MAX];
351 const char *presented = (if_hdr && pc_dav_if_token(if_hdr, tok,
sizeof(tok))) ? tok : nullptr;
352 return !pc_dav_lock_can_write(&s_dav_lock.table, path, presented);
356static bool dav_body_has(
HttpReq *req,
const char *needle)
358 return strstr((
const char *)req->
body, needle) !=
nullptr;
362static bool dav_coded_url_token(
const char *coded,
char *out,
size_t cap)
364 const char *lt = strchr(coded,
'<');
369 const char *gt = strchr(lt,
'>');
374 size_t n = (size_t)(gt - lt - 1);
379 memcpy(out, lt + 1, n);
387bool PC::dav_put_begin_tramp(
HttpReq *req)
389 return s_davput.stream_srv && s_davput.stream_srv->dav_stream_put_begin(req);
391void PC::dav_put_data_tramp(
HttpReq *req,
const uint8_t *data,
size_t len)
393 if (s_davput.stream_srv)
395 s_davput.stream_srv->dav_stream_put_data(req, data, len);
399void PC::dav_put_abort_tramp(
HttpReq *req)
403 uint8_t slot = (uint8_t)(req -
http_pool);
408 if (slot <
MAX_CONNS && s_davput.put[slot].active)
410 s_davput.put[slot].file.close();
411 s_davput.put[slot].active =
false;
416bool PC::dav_stream_put_begin(
HttpReq *req)
418 if (strcmp(req->
method,
"PUT") != 0)
422 uint8_t slot = (uint8_t)(req -
http_pool);
423 for (uint8_t i = 0; i < _route_count; i++)
425 Route *r = &_routes[i];
452 if (dav_resolve_path(r, req->
path, fs_path,
sizeof(fs_path)) != 0)
456 DavPut *d = &s_davput.put[slot];
461 if (dav_write_blocked(req, req->
path))
468 d->existed = r->static_fs->exists(fs_path);
469 d->file = r->static_fs->open(fs_path,
"w");
483void PC::dav_stream_put_data(
HttpReq *req,
const uint8_t *data,
size_t len)
485 uint8_t slot = (uint8_t)(req -
http_pool);
495 DavPut *d = &s_davput.put[slot];
496 if (d->active && !d->error)
498 if (d->file.write(data, len) != len)
510void PC::dav(
const char *url_prefix, fs::FS &file_sys,
const char *fs_root)
516 Route *r = &_routes[_route_count++];
520 if (n > 0 && url_prefix[n - 1] ==
'*')
522 pc_sb sb_pat = {pat,
sizeof(pat), 0,
true};
531 pc_sb sb_pat2 = {pat,
sizeof(pat), 0,
true};
540 r->
type = RouteType::ROUTE_DAV;
542 r->static_fs = &file_sys;
543 r->static_root = fs_root;
545#if PC_ENABLE_STREAM_BODY
547 s_davput.stream_srv =
this;
548 http_parser_set_stream_hooks(dav_put_begin_tramp, dav_put_data_tramp, dav_put_abort_tramp);
552void PC::dav_send_status(uint8_t slot_id,
int code,
const char *extra_headers)
554 if (!pc_conn_active(slot_id))
560 const char *cl = pc_resp_conn_hdr(slot_id, &keep);
564 pc_sb sb_header = {header,
sizeof(header), 0,
true};
570 pc_sb_put(&sb_header, extra_headers ? extra_headers :
"");
571 pc_sb_put(&sb_header,
"Content-Length: 0\r\n");
577 pc_resp_end(slot_id, code, 0, keep);
580bool PC::try_serve_dav(uint8_t slot_id,
HttpReq *req)
582 for (uint8_t i = 0; i < _route_count; i++)
584 Route *r = &_routes[i];
603 serve_dav_request(slot_id, req, r);
609void PC::serve_dav_request(uint8_t slot_id,
HttpReq *req,
const Route *r)
615 dav_send_status(slot_id, 404,
"");
619 fs::FS &fsys = *r->static_fs;
622 int rc = dav_resolve_path(r, req->
path, fs_path,
sizeof(fs_path));
625 dav_send_status(slot_id, rc,
"");
632 if (plen > 0 && r->
path[plen - 1] ==
'*')
636 const char *root = r->static_root ? r->static_root :
"";
640 uint32_t dav_now_s = (uint32_t)(
pc_millis() / 1000u);
641 pc_dav_lock_sweep(&s_dav_lock.table, dav_now_s);
643 switch (pc_webdav_method(req->
method))
645 case WebDavMethod::DAV_M_OPTIONS:
648 "OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK");
653 case WebDavMethod::DAV_M_GET:
654 case WebDavMethod::DAV_M_HEAD: {
655 fs::File f = fsys.open(fs_path,
"r");
658 dav_send_status(slot_id, 404,
"");
661 bool isdir = f.isDirectory();
665 dav_send_status(slot_id, 405,
"");
668 serve_file_internal(slot_id, pc_webdav_method(req->
method) == WebDavMethod::DAV_M_HEAD, fsys, fs_path,
673 case WebDavMethod::DAV_M_PUT: {
674#if PC_ENABLE_STREAM_BODY
675 if (req->body_streaming)
678 DavPut *d = &s_davput.put[slot_id];
682 dav_send_status(slot_id, 423,
"");
692 dav_send_status(slot_id, 409,
"");
697 dav_send_status(slot_id, 507,
"");
700 dav_send_status(slot_id, d->existed ? 204 : 201,
"");
705 if (dav_write_blocked(req, req->
path))
707 dav_send_status(slot_id, 423,
"");
710 bool existed = fsys.exists(fs_path);
711 fs::File f = fsys.open(fs_path,
"w");
714 dav_send_status(slot_id, 409,
"");
729 dav_send_status(slot_id, existed ? 204 : 201,
"");
733 case WebDavMethod::DAV_M_DELETE: {
734 if (dav_write_blocked(req, req->
path))
736 dav_send_status(slot_id, 423,
"");
739 if (!fsys.exists(fs_path))
741 dav_send_status(slot_id, 404,
"");
744 dav_send_status(slot_id, dav_rm_recursive(fsys, fs_path, 0) ? 204 : 403,
"");
748 case WebDavMethod::DAV_M_MKCOL:
749 if (dav_write_blocked(req, req->
path))
751 dav_send_status(slot_id, 423,
"");
754 if (fsys.exists(fs_path))
756 dav_send_status(slot_id, 405,
"");
759 dav_send_status(slot_id, fsys.mkdir(fs_path) ? 201 : 409,
"");
762 case WebDavMethod::DAV_M_COPY:
763 case WebDavMethod::DAV_M_MOVE: {
766 if (!dest_hdr || !pc_webdav_dest_path(dest_hdr, dest_url,
sizeof(dest_url)))
768 dav_send_status(slot_id, 400,
"");
772 if (strncmp(dest_url, r->
path, plen) != 0)
774 dav_send_status(slot_id, 502,
"");
777 const char *dest_sub = dest_url + plen;
778 if (strstr(dest_sub,
".."))
780 dav_send_status(slot_id, 403,
"");
785 bool is_move = pc_webdav_method(req->
method) == WebDavMethod::DAV_M_MOVE;
786 if (dav_write_blocked(req, dest_url) || (is_move && dav_write_blocked(req, req->
path)))
788 dav_send_status(slot_id, 423,
"");
792 if (!dav_join(root, dest_sub, dest_fs,
sizeof(dest_fs)))
794 dav_send_status(slot_id, 414,
"");
797 size_t dpl = strnlen(dest_fs,
sizeof(dest_fs));
798 if (dpl > 1 && dest_fs[dpl - 1] ==
'/')
800 dest_fs[dpl - 1] =
'\0';
804 bool overwrite = !(ow && (ow[0] ==
'F' || ow[0] ==
'f'));
805 bool dest_exists = fsys.exists(dest_fs);
806 if (dest_exists && !overwrite)
808 dav_send_status(slot_id, 412,
"");
812 if (pc_webdav_method(req->
method) == WebDavMethod::DAV_M_MOVE)
816 dav_rm_recursive(fsys, dest_fs, 0);
818 bool ok = fsys.rename(fs_path, dest_fs);
819 dav_send_status(slot_id, ok ? (dest_exists ? 204 : 201) : 409,
"");
826 fs::File src = fsys.open(fs_path,
"r");
829 dav_send_status(slot_id, 404,
"");
832 bool src_is_dir = src.isDirectory();
836 bool shallow = depth_h && depth_h[0] ==
'0';
840 dav_rm_recursive(fsys, dest_fs, 0);
844 if (src_is_dir && shallow)
846 ok = fsys.mkdir(dest_fs);
850 ok = dav_copy_recursive(fsys, fs_path, dest_fs, 0);
852 dav_send_status(slot_id, ok ? (dest_exists ? 204 : 201) : 409,
"");
856 case WebDavMethod::DAV_M_LOCK: {
857 const uint32_t timeout_s = 3600;
858 uint32_t expiry_s = dav_now_s + timeout_s;
863 char iftok[PC_DAV_LOCK_TOKEN_MAX];
864 const DavLock *lk =
nullptr;
865 if (if_hdr && pc_dav_if_token(if_hdr, iftok,
sizeof(iftok)))
867 lk = pc_dav_lock_refresh(&s_dav_lock.table, iftok, expiry_s);
870 char token[PC_DAV_LOCK_TOKEN_MAX];
871 bool shared, depth_inf;
874 pc_sb sb_token = {token,
sizeof(token), 0,
true};
880 shared = !lk->exclusive;
881 depth_inf = lk->depth_infinity;
887 shared = req->
body_len && dav_body_has(req,
"shared");
888 depth_inf = pc_webdav_depth(
http_get_header(req,
"Depth"), PC_DAV_DEPTH_INFINITY) != 0;
889 unsigned long tok = (
unsigned long)millis();
891 tok ^= (
unsigned long)esp_random();
893 pc_sb sb_token2 = {token,
sizeof(token), 0,
true};
894 pc_sb_put(&sb_token2,
"opaquelocktoken:");
895 pc_sb_hex(&sb_token2, (uint64_t)(tok), 8);
901 if (!pc_dav_lock_acquire(&s_dav_lock.table, req->
path, token, !shared, depth_inf, expiry_s))
903 dav_send_status(slot_id, 423,
"");
907 pc_sb sb_buf = {s_dav.buf,
sizeof(s_dav.buf), 0,
true};
910 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:prop "
911 "xmlns:D=\"DAV:\"><D:lockdiscovery><D:activelock><D:locktype><D:write/></D:locktype><D:lockscope><D:");
912 pc_sb_put(&sb_buf, shared ?
"shared" :
"exclusive");
913 pc_sb_put(&sb_buf,
"/></D:lockscope><D:depth>");
914 pc_sb_put(&sb_buf, depth_inf ?
"infinity" :
"0");
915 pc_sb_put(&sb_buf,
"</D:depth><D:timeout>Second-");
916 pc_sb_u32(&sb_buf, (uint32_t)((
unsigned long)timeout_s));
917 pc_sb_put(&sb_buf,
"</D:timeout><D:locktoken><D:href>");
919 pc_sb_put(&sb_buf,
"</D:href></D:locktoken></D:activelock></D:lockdiscovery></D:prop>\n");
926 pc_sb sb_lt = {lt,
sizeof(lt), 0,
true};
935 send(slot_id, 200,
"application/xml; charset=utf-8", s_dav.buf);
939 case WebDavMethod::DAV_M_UNLOCK: {
942 char token[PC_DAV_LOCK_TOKEN_MAX];
943 if (!lt || !dav_coded_url_token(lt, token,
sizeof(token)) || !pc_dav_lock_release(&s_dav_lock.table, token))
945 dav_send_status(slot_id, 409,
"");
948 dav_send_status(slot_id, 204,
"");
952 case WebDavMethod::DAV_M_PROPFIND: {
953 fs::File f = fsys.open(fs_path,
"r");
956 dav_send_status(slot_id, 404,
"");
959 bool isdir = f.isDirectory();
960 uint32_t fsize = (uint32_t)f.size();
961 time_t mtime = f.getLastWrite();
969 if (depth == PC_DAV_DEPTH_INFINITY)
972 static const char body[] =
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n"
973 "<D:error xmlns:D=\"DAV:\"><D:propfind-finite-depth/></D:error>\r\n";
974 send(slot_id, 403,
"application/xml", body);
980 pc_sb sb_self_href = {self_href,
sizeof(self_href), 0,
true};
986 size_t sl = strnlen(self_href,
sizeof(self_href));
991 if (isdir && (sl == 0 || self_href[sl - 1] !=
'/'))
993 if (sl + 1 <
sizeof(self_href))
995 self_href[sl++] =
'/';
996 self_href[sl] =
'\0';
1001 size_t cap =
sizeof(s_dav.buf), len = 0;
1002 len = pc_webdav_ms_begin(s_dav.buf, cap, len);
1005 len = pc_webdav_ms_entry(s_dav.buf, cap, len, self_href, isdir, fsize, mt, isdir ?
"" :
mime_type(fs_path));
1007 if (isdir && depth >= 1)
1012 fs::File c = f.openNextFile();
1027 const char *base = dav_basename(c.name());
1028 bool cdir = c.isDirectory();
1029 uint32_t csize = (uint32_t)c.size();
1030 time_t cmt = c.getLastWrite();
1032 pc_sb sb_chref = {chref,
sizeof(chref), 0,
true};
1043 size_t before = len;
1044 len = pc_webdav_ms_entry(s_dav.buf, cap, len, chref, cdir, csize, cmtbuf, cdir ?
"" :
mime_type(base));
1053 len = pc_webdav_ms_end(s_dav.buf, cap, len);
1054 send(slot_id, 207,
"application/xml; charset=utf-8", s_dav.buf);
1058 case WebDavMethod::DAV_M_PROPPATCH: {
1062 if (!fsys.exists(fs_path))
1064 dav_send_status(slot_id, 404,
"");
1068 pc_webdav_proppatch_ms(s_dav.buf,
sizeof(s_dav.buf), req->
path, (
const char *)req->
body, req->
body_len);
1076 dav_send_status(slot_id, 507,
"");
1080 send(slot_id, 207,
"application/xml; charset=utf-8", s_dav.buf);
1084 case WebDavMethod::DAV_M_UNSUPPORTED:
1088 "Allow: OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK\r\n");
Single-port HTTP server with deterministic, zero-allocation execution.
static const char * mime_type(const char *path)
Guess a Content-Type from a path's file extension.
void send_empty(uint8_t slot_id, int code)
Send a headers-only HTTP response and close the connection.
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.
Pluggable monotonic clock for all library timing.
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
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.
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.
#define RESP_HDR_BUF_SIZE
Stack buffer for HTTP response header lines in send() / send_empty() / send_unauth() / serve_file().
#define PC_WEBDAV_BUF_SIZE
Buffer (BSS) for a WebDAV 207 Multi-Status response, in bytes (see PC_ENABLE_WEBDAV).
#define FILE_CHUNK_SIZE
Bytes read from the filesystem and passed to tcp_write() per loop().
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define PC_WEBDAV_MAX_ENTRIES
Maximum children listed in a WebDAV Depth-1 PROPFIND (bounds the response).
@ PC_IFACE_ANY
Unknown / no filter (matches any interface).
Library-private declarations shared between protocore.cpp and the src/server/*.cpp request-handler tr...
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.
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
void pc_sb_hex(pc_sb *b, uint64_t v, unsigned min_digits)
Append v as lowercase hex, zero-padded to at least min_digits (printf "%0Nx").
void pc_sb_i64(pc_sb *b, int64_t v)
Append v as signed decimal (64-bit), with a leading '-' when negative.
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
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).
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.
char method[PC_METHOD_BUF_SIZE]
HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).
Internal route entry stored in the routing table.
RouteType type
HTTP, WS, or SSE.
pc_iface iface_filter
Interface gate; pc_iface::PC_IFACE_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.
Bump-append target; ok latches false once an append would overflow cap.
bool pc_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...