ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
webdav.h
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.h
6 * @brief WebDAV server core (RFC 4918): method classification, header parsing,
7 * and the 207 Multi-Status XML builder.
8 *
9 * Mirrors the CoAP/SNMP split: this header declares the pure, host-testable core
10 * (no sockets, no filesystem - unit-tested in env:native_webdav). The
11 * filesystem-backed request handling (PROPFIND directory walk, PUT/MKCOL/DELETE/
12 * COPY/MOVE, GET via the file-serving path) lives in PC::serve_dav_*
13 * and runs only on a build with a real Arduino FS.
14 *
15 * Scope: class 1 (PROPFIND Depth 0/1, PROPPATCH, PUT, DELETE, MKCOL, COPY, MOVE)
16 * plus OPTIONS and class 2 LOCK/UNLOCK, now enforced by a small lock table (see
17 * the lock manager below): a locked resource rejects a write that does not present
18 * the matching token in its If header (423 Locked). PROPPATCH is answered 207 with every requested property refused 403
19 * (read-only live properties, no dead-property store). The filesystem-backed
20 * handler streams a PUT body straight to the file (PC's stream-body
21 * hook), so uploads are not bounded by BODY_BUF_SIZE.
22 */
23
24#ifndef PROTOCORE_WEBDAV_H
25#define PROTOCORE_WEBDAV_H
26
27#include "protocore_config.h"
28#include <stddef.h>
29#include <stdint.h>
30
31#if PC_ENABLE_WEBDAV
32
33/** @brief WebDAV request methods recognized by the server. */
34enum class WebDavMethod : uint8_t
35{
36 DAV_M_OPTIONS,
37 DAV_M_GET,
38 DAV_M_HEAD,
39 DAV_M_PUT,
40 DAV_M_DELETE,
41 DAV_M_PROPFIND,
42 DAV_M_PROPPATCH,
43 DAV_M_MKCOL,
44 DAV_M_COPY,
45 DAV_M_MOVE,
46 DAV_M_LOCK,
47 DAV_M_UNLOCK,
48 DAV_M_UNSUPPORTED ///< Anything else - answered 405 Method Not Allowed.
49};
50
51/** @brief "infinity" Depth value (RFC 4918 §10.2). */
52/** @brief Depth: infinity sentinel (a lone constant). */
53#define PC_DAV_DEPTH_INFINITY 0x7fffffff
54
55/** @brief Classify an HTTP method token (e.g. "PROPFIND") into a WebDavMethod. */
56WebDavMethod pc_webdav_method(const char *m);
57
58/**
59 * @brief Parse a Depth header value ("0", "1", or "infinity").
60 * @return 0, 1, or PC_DAV_DEPTH_INFINITY; @p dflt when @p depth_hdr is null/empty.
61 */
62int pc_webdav_depth(const char *depth_hdr, int dflt);
63
64/**
65 * @brief XML-escape @p src into @p dst (`&`, `<`, `>`, `"`, `'`).
66 * @return length written (NUL-terminated; truncated to fit @p cap).
67 */
68size_t pc_webdav_xml_escape(char *dst, size_t cap, const char *src);
69
70/**
71 * @brief Extract and percent-decode the path of a Destination header.
72 *
73 * Accepts an absolute URI ("http://host/p/q") or an absolute path ("/p/q"); the
74 * scheme + authority are skipped and `%xx` escapes are decoded into @p out.
75 * @return false on overflow or a malformed value.
76 */
77bool pc_webdav_dest_path(const char *destination, char *out, size_t cap);
78
79// 207 Multi-Status incremental builder. Each call appends to a buffer already
80// holding @p len bytes and returns the new length, never exceeding @p cap. A
81// return value equal to @p len means the fragment did not fit (the caller should
82// stop adding entries and close the document).
83
84/** @brief Write the XML prolog and the open <multistatus> element. */
85size_t pc_webdav_ms_begin(char *buf, size_t cap, size_t len);
86
87/**
88 * @brief Append one <response> describing a resource.
89 *
90 * @param href the resource's URL path (XML-escaped here).
91 * @param is_collection true for a directory (emits <collection/>).
92 * @param size content length (files only).
93 * @param rfc1123_mtime Last-Modified string, or "" to omit.
94 * @param content_type MIME type (files only), or "" to omit.
95 */
96size_t pc_webdav_ms_entry(char *buf, size_t cap, size_t len, const char *href, bool is_collection, uint32_t size,
97 const char *rfc1123_mtime, const char *content_type);
98
99/** @brief Close the <multistatus> element. */
100size_t pc_webdav_ms_end(char *buf, size_t cap, size_t len);
101
102/**
103 * @brief Build a complete 207 Multi-Status body answering a PROPPATCH.
104 *
105 * The server has no dead-property store and its live properties are read-only, so
106 * every requested property is refused with 403 Forbidden. The property elements
107 * are echoed verbatim (self-closed) from the request @p body so the client sees
108 * exactly which properties it asked for, namespace declarations intact; a property
109 * whose tag would contain a stray '<' is skipped (no XML injection). Up to
110 * PC_WEBDAV_MAX_PROPS properties are echoed.
111 *
112 * @param buf destination buffer (whole document, NUL-terminated).
113 * @param cap buffer capacity.
114 * @param href the resource path (XML-escaped here).
115 * @param body the PROPPATCH request body (not required to be NUL-terminated).
116 * @param body_len length of @p body.
117 * @return bytes written, or 0 if the document did not fit @p cap.
118 */
119size_t pc_webdav_proppatch_ms(char *buf, size_t cap, const char *href, const char *body, size_t body_len);
120
121// ── lock manager (RFC 4918 §6-7, class 2) ──────────────────────────────────────────────────────
122//
123// A small fixed lock table that makes LOCK/UNLOCK enforceable rather than advisory: a locked resource
124// rejects a write (PUT / DELETE / MKCOL / MOVE / PROPPATCH) unless the request presents the matching lock
125// token in its If header. This is the pure, host-testable core - the handler supplies the token (its own
126// RNG) and wires the checks into the mutating methods.
127
128/** @brief Maximum concurrent locks (fixed - a small structural bound, not a per-board tunable). */
129#define PC_DAV_LOCK_MAX 8
130/** @brief Maximum locked-path length, including the NUL. */
131#define PC_DAV_LOCK_PATH_MAX 128
132/** @brief Maximum lock-token length, including the NUL (e.g. "opaquelocktoken:xxxxxxxx-pc"). */
133#define PC_DAV_LOCK_TOKEN_MAX 48
134
135/** @brief One active lock (RFC 4918 §6.4). */
136struct DavLock
137{
138 char path[PC_DAV_LOCK_PATH_MAX]; ///< the locked resource path (trailing slash normalized off)
139 char token[PC_DAV_LOCK_TOKEN_MAX]; ///< the lock token (an opaquelocktoken URI)
140 bool exclusive; ///< exclusive-write (true) or shared (false)
141 bool depth_infinity; ///< the lock covers the whole subtree (Depth: infinity) vs just the resource
142 bool active; ///< false = free slot
143 uint32_t expiry_s; ///< monotonic second the lock expires (0 = no timeout); swept by _sweep
144};
145
146/** @brief The server-global lock table (one instance, not per-connection). */
147struct DavLockTable
148{
149 DavLock locks[PC_DAV_LOCK_MAX];
150};
151
152/** @brief Reset a lock table (no locks held). */
153void pc_dav_lock_init(DavLockTable *t);
154
155/**
156 * @brief Acquire a lock on @p path with the caller-supplied @p token (RFC 4918 §6-7).
157 *
158 * Fails when a conflicting lock already covers @p path or its subtree - an exclusive request conflicts
159 * with any overlapping lock, a shared request only with an overlapping exclusive one - or when the table
160 * is full / an argument is bad.
161 * @param expiry_s the monotonic second the lock expires (0 = no timeout); @ref pc_dav_lock_sweep drops
162 * a lock once now reaches it. The clock stays out of this pure core - the caller passes
163 * its own now + timeout.
164 * @return the stored lock on success, nullptr on conflict / full.
165 */
166const DavLock *pc_dav_lock_acquire(DavLockTable *t, const char *path, const char *token, bool exclusive,
167 bool depth_infinity, uint32_t expiry_s);
168
169/**
170 * @brief Expire and drop every lock whose timeout has passed (RFC 4918 §6.6). Call before a lock query so
171 * stale locks never gate a write. A lock with @c expiry_s == 0 never expires.
172 * @param now_s the caller's current monotonic second.
173 * @return the number of locks dropped.
174 */
175size_t pc_dav_lock_sweep(DavLockTable *t, uint32_t now_s);
176
177/**
178 * @brief Refresh a held lock's timeout to @p new_expiry_s, keyed by @p token (a LOCK refresh, RFC 4918
179 * §9.10.2). @return the refreshed lock, or nullptr if no live lock has that token.
180 */
181const DavLock *pc_dav_lock_refresh(DavLockTable *t, const char *token, uint32_t new_expiry_s);
182
183/**
184 * @brief Find a lock covering @p path: one on @p path itself, or a Depth-infinity lock on an ancestor.
185 * @return the covering lock, or nullptr if @p path is unlocked.
186 */
187const DavLock *pc_dav_lock_find(const DavLockTable *t, const char *path);
188
189/** @brief Release the lock whose token equals @p token (UNLOCK). @return true if one was removed. */
190bool pc_dav_lock_release(DavLockTable *t, const char *token);
191
192/**
193 * @brief May a write to @p path proceed given the token the request presented (RFC 4918 §7)?
194 *
195 * Allowed when no lock covers @p path, or when @p presented_token (from the If header, may be nullptr)
196 * matches a covering lock. A locked resource with no / wrong token is denied (the handler answers 423).
197 */
198bool pc_dav_lock_can_write(const DavLockTable *t, const char *path, const char *presented_token);
199
200/**
201 * @brief Extract the first lock token from an If header value (RFC 4918 §10.4).
202 *
203 * Handles the tagged (`<res> (<token>)`) and untagged (`(<token>)`) list forms and a `Not` condition by
204 * taking the first Coded-URL inside the first condition list. @return true and fills @p out on success.
205 */
206bool pc_dav_if_token(const char *if_header, char *out, size_t cap);
207
208#endif // PC_ENABLE_WEBDAV
209
210#endif // PROTOCORE_WEBDAV_H
User-facing configuration for ProtoCore.