webdav fixes

This commit is contained in:
timurgordon
2025-03-06 01:24:04 +01:00
parent b01e40da40
commit fe934bba36
14 changed files with 598 additions and 408 deletions

View File

@@ -34,7 +34,6 @@ pub fn new_app(args AppArgs) !&App {
app.use(handler: app.auth_middleware)
app.use(handler: middleware_log_request)
app.use(handler: middleware_log_response, after: true)
return app
}

View File

@@ -0,0 +1,196 @@
module webdav
import encoding.xml
import log
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.vfs
import freeflowuniverse.herolib.vfs.vfs_db
import os
import time
import veb
// PropfindRequest represents a parsed PROPFIND request
pub struct PropfindRequest {
pub:
typ PropfindType
props []string // Property names if typ is prop
depth int // Depth of the request (0, 1, or -1 for infinity)
xml_content string // Original XML content
}
// PropfindType represents the type of PROPFIND request
pub enum PropfindType {
allprop // Request all properties
propname // Request property names only
prop // Request specific properties
invalid // Invalid request
}
// parse_propfind_xml parses the XML body of a PROPFIND request
pub fn parse_propfind_xml(data string) !PropfindRequest {
if data.len == 0 {
// If no body is provided, default to allprop
return PropfindRequest{
typ: .allprop
depth: 0
xml_content: ''
}
}
doc := xml.XMLDocument.from_string(data) or {
return error('Failed to parse XML: ${err}')
}
root := doc.root
if root.name.to_lower() != 'propfind' && !root.name.ends_with(':propfind') {
return error('Invalid PROPFIND request: root element must be propfind')
}
mut typ := PropfindType.invalid
mut props := []string{}
// Check for allprop, propname, or prop elements
for child in root.children {
if child is xml.XMLNode {
node := child as xml.XMLNode
// Check for allprop
if node.name == 'allprop' || node.name == 'D:allprop' {
typ = .allprop
break
}
// Check for propname
if node.name == 'propname' || node.name == 'D:propname' {
typ = .propname
break
}
// Check for prop
if node.name == 'prop' || node.name == 'D:prop' {
typ = .prop
// Extract property names
for prop_child in node.children {
if prop_child is xml.XMLNode {
prop_node := prop_child as xml.XMLNode
props << prop_node.name
}
}
break
}
}
}
if typ == .invalid {
return error('Invalid PROPFIND request: missing prop, allprop, or propname element')
}
return PropfindRequest{
typ: typ
props: props
depth: 0
xml_content: data
}
}
// parse_depth parses the Depth header value
pub fn parse_depth(depth_str string) int {
if depth_str == 'infinity' {
return -1 // Use -1 to represent infinity
}
depth := depth_str.int()
// Only 0, 1, and infinity are valid values for Depth
if depth != 0 && depth != 1 {
// Invalid depth value, default to 0
log.warn('[WebDAV] Invalid Depth header value: ${depth_str}, defaulting to 0')
return 0
}
return depth
}
// returns the properties of a filesystem entry
fn get_properties(entry &vfs.FSEntry) []Property {
mut props := []Property{}
metadata := entry.get_metadata()
// Display name
props << DisplayName(metadata.name)
props << GetLastModified(format_iso8601(metadata.modified_time()))
props << GetContentType(if entry.is_dir() {'httpd/unix-directory'} else {get_file_content_type(entry.get_path())})
props << ResourceType(entry.is_dir())
// Content length (only for files)
if !entry.is_dir() {
props << GetContentLength(metadata.size.str())
}
// Creation date
props << CreationDate(format_iso8601(metadata.created_time()))
return props
}
// Response represents a WebDAV response for a resource
pub struct Response {
pub:
href string
found_props []Property
not_found_props []Property
}
fn (r Response) xml() string {
return '<D:response>\n<D:href>${r.href}</D:href>
<D:propstat>${r.found_props.xml()}<D:status>HTTP/1.1 200 OK</D:status></D:propstat>
<D:propstat>${r.not_found_props.xml()}<D:status>HTTP/1.1 404 Not Found</D:status></D:propstat>
</D:response>'
}
// generate_propfind_response generates a PROPFIND response XML string from Response structs
pub fn (r []Response) xml () string {
return '<?xml version="1.0" encoding="UTF-8"?>\n<D:multistatus xmlns:D="DAV:">
${r.map(it.xml()).join_lines()}\n</D:multistatus>'
}
fn get_file_content_type(path string) string {
ext := path.all_after_last('.')
content_type := if v := veb.mime_types[ext] {
v
} else {
'text/plain; charset=utf-8'
}
return content_type
}
// get_responses returns all properties for the given path and depth
fn (mut app App) get_responses(entry vfs.FSEntry, req PropfindRequest) ![]Response {
mut responses := []Response{}
path := if entry.is_dir() && entry.get_path() != '/' {
'${entry.get_path()}/'
} else {
entry.get_path()
}
// main entry response
responses << Response {
href: path
// not_found: entry.get_unfound_properties(req)
found_props: get_properties(entry)
}
if req.depth == 0 && entry.get_path() != '/' { return responses }
entries := app.vfs.dir_list(path) or {
log.error('Failed to list directory for ${path} ${err}')
return responses }
for e in entries {
responses << app.get_responses(e, PropfindRequest {
...req,
depth: if req.depth == 1 { 0 } else {-1}
})!
}
return responses
}

19
lib/dav/webdav/errors.v Normal file
View File

@@ -0,0 +1,19 @@
module webdav
import net.http
import veb
import log
struct WebDAVError {
pub:
status http.Status
message string
tag string
}
pub fn (mut ctx Context) error(err WebDAVError) veb.Result {
message := if err.message != '' {err.message } else {err.status.str().replace('_', ' ').title()}
log.error('[WebDAV] ${message}')
ctx.res.set_status(err.status)
return ctx.send_response_to_client('application/xml', $tmpl('./templates/error_response.xml'))
}

View File

@@ -3,15 +3,6 @@ module webdav
import time
import rand
struct Lock {
resource string
owner string
token string
depth int // 0 for a single resource, 1 for recursive
timeout int // in seconds
created_at time.Time
}
struct LockManager {
mut:
locks map[string]Lock
@@ -27,7 +18,7 @@ pub:
// lock attempts to lock a resource for a specific owner
// Returns a LockResult with the lock token and whether it's a new lock
// Returns an error if the resource is already locked by a different owner
pub fn (mut lm LockManager) lock(resource string, owner string, depth int, timeout int) !LockResult {
pub fn (mut lm LockManager) lock(resource string, owner string, depth int, timeout int) !Lock {
if resource in lm.locks {
// Check if the lock is still valid
existing_lock := lm.locks[resource]
@@ -35,18 +26,15 @@ pub fn (mut lm LockManager) lock(resource string, owner string, depth int, timeo
// Resource is already locked
if existing_lock.owner == owner {
// Same owner, refresh the lock
lm.locks[resource] = Lock{
refreshed_lock := Lock {...existing_lock,
resource: resource
owner: owner
token: existing_lock.token
depth: depth
timeout: timeout
created_at: time.now()
}
return LockResult{
token: existing_lock.token
is_new_lock: false
}
lm.locks[resource] = refreshed_lock
return refreshed_lock
} else {
// Different owner, return an error
return error('Resource is already locked by a different owner')
@@ -57,19 +45,16 @@ pub fn (mut lm LockManager) lock(resource string, owner string, depth int, timeo
}
// Generate a new lock token
token := rand.uuid_v4()
lm.locks[resource] = Lock{
new_lock := Lock{
resource: resource
owner: owner
token: token
token: rand.uuid_v4()
depth: depth
timeout: timeout
created_at: time.now()
}
return LockResult{
token: token
is_new_lock: true
}
lm.locks[resource] = new_lock
return new_lock
}
pub fn (mut lm LockManager) unlock(resource string) bool {
@@ -107,9 +92,7 @@ pub fn (lm LockManager) get_lock(resource string) ?Lock {
}
pub fn (mut lm LockManager) unlock_with_token(resource string, token string) bool {
println('debugzoA ${resource} ${lm.locks}')
if resource in lm.locks {
println('debugzoB ${token} ${lm.locks[resource]}')
lock_ := lm.locks[resource]
if lock_.token == token {
lm.locks.delete(resource)
@@ -119,7 +102,7 @@ pub fn (mut lm LockManager) unlock_with_token(resource string, token string) boo
return false
}
fn (mut lm LockManager) lock_recursive(resource string, owner string, depth int, timeout int) !LockResult {
fn (mut lm LockManager) lock_recursive(resource string, owner string, depth int, timeout int) !Lock {
if depth == 0 {
return lm.lock(resource, owner, depth, timeout)
}

View File

@@ -10,128 +10,19 @@ import strings
@['/:path...'; options]
pub fn (app &App) options(mut ctx Context, path string) veb.Result {
ctx.res.set_status(.ok)
ctx.res.header.add_custom('dav', '1,2') or { return ctx.server_error(err.msg()) }
ctx.res.header.add(.allow, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
ctx.res.header.add_custom('MS-Author-Via', 'DAV') or { return ctx.server_error(err.msg()) }
ctx.res.header.add(.access_control_allow_origin, '*')
ctx.res.header.add(.access_control_allow_methods, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
ctx.res.header.add(.access_control_allow_headers, 'Authorization, Content-Type')
ctx.res.header.add(.content_length, '0')
return ctx.text('')
}
// parse_lock_xml parses the XML data from a WebDAV LOCK request
// and extracts the lock parameters (scope, type, owner)
fn parse_lock_xml(xml_data string) !LockInfo {
mut lock_info := LockInfo{
scope: 'exclusive' // default values
lock_type: 'write'
owner: ''
}
// Parse the XML document
doc := xml.XMLDocument.from_string(xml_data) or {
return error('Failed to parse XML: ${err}')
}
// Get the root element (lockinfo)
root := doc.root
// Handle namespace prefixes (D:) in element names
// WebDAV uses namespaces, so we need to check for both prefixed and non-prefixed names
// Extract lockscope
for child in root.children {
if child is xml.XMLNode {
node := child as xml.XMLNode
// Check for lockscope (with or without namespace prefix)
if node.name == 'lockscope' || node.name == 'D:lockscope' {
for scope_child in node.children {
if scope_child is xml.XMLNode {
scope_node := scope_child as xml.XMLNode
if scope_node.name == 'exclusive' || scope_node.name == 'D:exclusive' {
lock_info.scope = 'exclusive'
} else if scope_node.name == 'shared' || scope_node.name == 'D:shared' {
lock_info.scope = 'shared'
}
}
}
}
// Check for locktype (with or without namespace prefix)
if node.name == 'locktype' || node.name == 'D:locktype' {
for type_child in node.children {
if type_child is xml.XMLNode {
type_node := type_child as xml.XMLNode
if type_node.name == 'write' || type_node.name == 'D:write' {
lock_info.lock_type = 'write'
}
}
}
}
// Check for owner (with or without namespace prefix)
if node.name == 'owner' || node.name == 'D:owner' {
for owner_child in node.children {
if owner_child is xml.XMLNode {
owner_node := owner_child as xml.XMLNode
if owner_node.name == 'href' || owner_node.name == 'D:href' {
for href_content in owner_node.children {
if href_content is string {
lock_info.owner = (href_content as string).trim_space()
break
}
}
}
} else if owner_child is string {
// Some clients might include owner text directly
lock_info.owner = (owner_child as string).trim_space()
}
}
}
}
}
// If owner is still empty, try to extract it from any text content in the owner node
if lock_info.owner.len == 0 {
for child in root.children {
if child is xml.XMLNode {
node := child as xml.XMLNode
if node.name == 'owner' || node.name == 'D:owner' {
for content in node.children {
if content is string {
lock_info.owner = (content as string).trim_space()
break
}
}
}
}
}
}
// Use a default owner if none was found
if lock_info.owner.len == 0 {
lock_info.owner = 'unknown-client'
}
// Debug output
println('Parsed lock info: scope=${lock_info.scope}, type=${lock_info.lock_type}, owner=${lock_info.owner}')
return lock_info
}
// LockInfo holds the parsed information from a WebDAV LOCK request
struct LockInfo {
pub mut:
scope string // 'exclusive' or 'shared'
lock_type string // typically 'write'
owner string // owner identifier
ctx.set_custom_header('DAV', '1,2') or { return ctx.server_error(err.msg()) }
ctx.set_header(.allow, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
// ctx.set_header(.connection, 'close')
ctx.set_custom_header('MS-Author-Via', 'DAV') or { return ctx.server_error(err.msg()) }
ctx.set_header(.access_control_allow_origin, '*')
ctx.set_header(.access_control_allow_methods, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
ctx.set_header(.access_control_allow_headers, 'Authorization, Content-Type')
ctx.set_header(.content_length, '0')
return ctx.ok('')
}
@['/:path...'; lock]
pub fn (mut app App) lock_handler(mut ctx Context, path string) veb.Result {
pub fn (mut app App) lock(mut ctx Context, path string) veb.Result {
resource := ctx.req.url
// Parse lock information from XML body instead of headers
@@ -161,12 +52,7 @@ pub fn (mut app App) lock_handler(mut ctx Context, path string) veb.Result {
// Resource is locked by a different owner
// Return a 423 Locked status with information about the existing lock
ctx.res.set_status(.locked)
// Create a response with information about the existing lock
lock_discovery_response := create_lock_discovery_response(existing_lock)
ctx.res.header.add(.content_type, 'application/xml; charset="utf-8"')
return ctx.text(lock_discovery_response)
return ctx.send_response_to_client('application/xml', existing_lock.xml())
}
}
@@ -177,70 +63,14 @@ pub fn (mut app App) lock_handler(mut ctx Context, path string) veb.Result {
return ctx.text('Resource is already locked by a different owner.')
}
ctx.res.set_status(.ok)
ctx.res.header.add_custom('Lock-Token', '${lock_result.token}') or { return ctx.server_error(err.msg()) }
ctx.set_custom_header('Lock-Token', '${lock_result.token}') or { return ctx.server_error(err.msg()) }
// Create a proper WebDAV lock response
lock_response := create_lock_response(lock_result.token, lock_info, resource, timeout)
println('debugzo4444 ${lock_response}')
return ctx.send_response_to_client('application/xml', lock_response)
}
// create_lock_discovery_response generates an XML response with information about an existing lock
fn create_lock_discovery_response(lock_ Lock) string {
mut sb := strings.new_builder(500)
sb.write_string('<?xml version="1.0" encoding="utf-8"?>\n')
// sb.write_string('<D:prop xmlns:D="DAV:">\n')
// sb.write_string(' <D:lockdiscovery>\n')
sb.write_string(' <D:activelock>\n')
sb.write_string(' <D:locktype><D:write/></D:locktype>\n')
sb.write_string(' <D:lockscope><D:exclusive/></D:lockscope>\n')
sb.write_string(' <D:depth>${lock_.depth}</D:depth>\n')
sb.write_string(' <D:owner>\n')
sb.write_string(' <D:href>${lock_.owner}</D:href>\n')
sb.write_string(' </D:owner>\n')
sb.write_string(' <D:timeout>Second-${lock_.timeout}</D:timeout>\n')
sb.write_string(' <D:locktoken>\n')
sb.write_string(' <D:href>${lock_.token}</D:href>\n')
sb.write_string(' </D:locktoken>\n')
sb.write_string(' <D:lockroot>\n')
sb.write_string(' <D:href>${lock_.resource}</D:href>\n')
sb.write_string(' </D:lockroot>\n')
sb.write_string(' </D:activelock>\n')
// sb.write_string(' </D:lockdiscovery>\n')
// sb.write_string('</D:prop>\n')
return sb.str()
}
// create_lock_response generates the XML response for a successful lock request
fn create_lock_response(token string, lock_info LockInfo, resource string, timeout int) string {
mut sb := strings.new_builder(500)
sb.write_string('<?xml version="1.0" encoding="utf-8"?>\n')
sb.write_string('<D:prop xmlns:D="DAV:">\n')
sb.write_string(' <D:lockdiscovery xmlns:D="DAV:">\n')
sb.write_string(' <D:activelock>\n')
sb.write_string(' <D:locktype><D:${lock_info.lock_type}/></D:locktype>\n')
sb.write_string(' <D:lockscope><D:${lock_info.scope}/></D:lockscope>\n')
sb.write_string(' <D:depth>infinity</D:depth>\n')
sb.write_string(' <D:owner>\n')
sb.write_string(' <D:href>${lock_info.owner}</D:href>\n')
sb.write_string(' </D:owner>\n')
sb.write_string(' <D:timeout>Second-${timeout}</D:timeout>\n')
sb.write_string(' <D:locktoken>\n')
sb.write_string(' <D:href>${token}</D:href>\n')
sb.write_string(' </D:locktoken>\n')
sb.write_string(' <D:lockroot>\n')
sb.write_string(' <D:href>${resource}</D:href>\n')
sb.write_string(' </D:lockroot>\n')
sb.write_string(' </D:activelock>\n')
sb.write_string(' </D:lockdiscovery>\n')
sb.write_string('</D:prop>\n')
return sb.str()
return ctx.send_response_to_client('application/xml', lock_result.xml())
}
@['/:path...'; unlock]
pub fn (mut app App) unlock_handler(mut ctx Context, path string) veb.Result {
pub fn (mut app App) unlock(mut ctx Context, path string) veb.Result {
resource := ctx.req.url
token_ := ctx.get_custom_header('Lock-Token') or { return ctx.server_error(err.msg()) }
token := token_.trim_string_left('<').trim_string_right('>')
@@ -250,7 +80,6 @@ pub fn (mut app App) unlock_handler(mut ctx Context, path string) veb.Result {
return ctx.text('Lock failed: `Owner` header missing.')
}
println('debugzoZ ${token}')
if app.lock_manager.unlock_with_token(resource, token) {
ctx.res.set_status(.no_content)
return ctx.text('Lock successfully released')
@@ -263,16 +92,16 @@ pub fn (mut app App) unlock_handler(mut ctx Context, path string) veb.Result {
@['/:path...'; get]
pub fn (mut app App) get_file(mut ctx Context, path string) veb.Result {
log.info('[WebDAV] Getting file ${path}')
if !app.vfs.exists(path) {
return ctx.not_found()
}
fs_entry := app.vfs.get(path) or {
console.print_stderr('failed to get FS Entry ${path}: ${err}')
log.error('[WebDAV] failed to get FS Entry ${path}: ${err}')
return ctx.server_error(err.msg())
}
println('debugzone-- ${fs_entry.get_path()} >> ${path}')
file_data := app.vfs.file_read(path) or { return ctx.server_error(err.msg()) }
ext := fs_entry.get_metadata().name.all_after_last('.')
@@ -284,7 +113,13 @@ pub fn (mut app App) get_file(mut ctx Context, path string) veb.Result {
@[head]
pub fn (app &App) index(mut ctx Context) veb.Result {
ctx.res.header.add(.content_length, '0')
ctx.set_custom_header('DAV', '1,2') or { return ctx.server_error(err.msg()) }
ctx.set_header(.allow, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
ctx.set_custom_header('MS-Author-Via', 'DAV') or { return ctx.server_error(err.msg()) }
ctx.set_header(.access_control_allow_origin, '*')
ctx.set_header(.access_control_allow_methods, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
ctx.set_header(.access_control_allow_headers, 'Authorization, Content-Type')
ctx.set_header(.content_length, '0')
return ctx.ok('')
}
@@ -296,32 +131,26 @@ pub fn (mut app App) exists(mut ctx Context, path string) veb.Result {
}
// Add necessary WebDAV headers
ctx.res.header.add(.authorization, 'Basic') // Indicates Basic auth usage
ctx.res.header.add_custom('DAV', '1, 2') or {
// ctx.set_header(.authorization, 'Basic') // Indicates Basic auth usage
ctx.set_custom_header('dav', '1, 2') or {
return ctx.server_error('Failed to set DAV header: ${err}')
}
ctx.res.header.add_custom('Etag', 'abc123xyz') or {
return ctx.server_error('Failed to set ETag header: ${err}')
}
ctx.res.header.add(.content_length, '0') // HEAD request, so no body
ctx.res.header.add(.date, time.now().as_utc().format()) // Correct UTC date format
// ctx.res.header.add(.content_type, 'application/xml') // XML is common for WebDAV metadata
ctx.res.header.add_custom('Allow', 'OPTIONS, GET, HEAD, PROPFIND, PROPPATCH, MKCOL, PUT, DELETE, COPY, MOVE, LOCK, UNLOCK') or {
ctx.set_header(.content_length, '0') // HEAD request, so no body
// ctx.set_header(.date, time.now().as_utc().format_rfc1123()) // Correct UTC date format
// ctx.set_header(.content_type, 'application/xml') // XML is common for WebDAV metadata
ctx.set_custom_header('Allow', 'OPTIONS, GET, HEAD, PROPFIND, PROPPATCH, MKCOL, PUT, DELETE, COPY, MOVE, LOCK, UNLOCK') or {
return ctx.server_error('Failed to set Allow header: ${err}')
}
ctx.res.header.add(.accept_ranges, 'bytes') // Allows range-based file downloads
ctx.res.header.add_custom('Cache-Control', 'no-cache, no-store, must-revalidate') or {
ctx.set_header(.accept_ranges, 'bytes') // Allows range-based file downloads
ctx.set_custom_header('Cache-Control', 'no-cache, no-store, must-revalidate') or {
return ctx.server_error('Failed to set Cache-Control header: ${err}')
}
ctx.res.header.add_custom('Last-Modified', time.now().as_utc().format()) or {
ctx.set_custom_header('Last-Modified', time.now().as_utc().format()) or {
return ctx.server_error('Failed to set Last-Modified header: ${err}')
}
ctx.res.set_status(.ok)
ctx.res.set_version(.v1_1)
// Debugging output (can be removed in production)
println('HEAD response: ${ctx.res}')
return ctx.ok('')
}
@@ -336,6 +165,14 @@ pub fn (mut app App) delete(mut ctx Context, path string) veb.Result {
return ctx.server_error(err.msg())
}
// Check if the resource is locked
if app.lock_manager.is_locked(ctx.req.url) {
// Resource is locked, return a 207 Multi-Status response with a 423 Locked status
ctx.res.set_status(.multi_status)
return ctx.send_response_to_client('application/xml', $tmpl('./templates/delete_response.xml'))
}
// If not locked, proceed with deletion
if fs_entry.is_dir() {
console.print_debug('deleting directory: ${path}')
app.vfs.dir_delete(path) or { return ctx.server_error(err.msg()) }
@@ -346,8 +183,8 @@ pub fn (mut app App) delete(mut ctx Context, path string) veb.Result {
app.vfs.file_delete(path) or { return ctx.server_error(err.msg()) }
}
ctx.res.set_status(.no_content)
return ctx.text('entry ${path} is deleted')
// Return success response
return ctx.no_content()
}
@['/:path...'; copy]
@@ -418,28 +255,52 @@ pub fn (mut app App) mkcol(mut ctx Context, path string) veb.Result {
@['/:path...'; propfind]
fn (mut app App) propfind(mut ctx Context, path string) veb.Result {
log.info('[WebDAV] ${@FN} ${path}')
// Check if resource exists
if !app.vfs.exists(path) {
return ctx.not_found()
return ctx.error(
status: .not_found
message: 'Path ${path} does not exist'
tag: 'resource-must-be-null'
)
}
// Parse Depth header
depth_str := ctx.req.header.get_custom('Depth') or { '0' }
depth := parse_depth(depth_str)
// Parse PROPFIND request
propfind_req := parse_propfind_xml(ctx.req.data) or {
return ctx.error(WebDAVError{
status: .bad_request
message: 'Failed to parse PROPFIND XML: ${err}'
tag: 'propfind-parse-error'
})
}
depth := ctx.req.header.get_custom('Depth') or { '0' }.int()
responses := app.get_responses(path, depth) or {
console.print_stderr('failed to get responses: ${err}')
return ctx.server_error(err.msg())
log.debug('[WebDAV] ${@FN} Propfind Request: ${propfind_req.typ} ${propfind_req.depth}')
// Check if resource is locked
if app.lock_manager.is_locked(ctx.req.url) {
// If the resource is locked, we should still return properties
// but we might need to indicate the lock status in the response
// This is handled in the property generation
log.info('[WebDAV] Resource is locked: ${ctx.req.url}')
}
doc := xml.XMLDocument{
root: xml.XMLNode{
name: 'D:multistatus'
children: responses
attributes: {
'xmlns:D': 'DAV:'
}
}
entry := app.vfs.get(path) or {return ctx.server_error('entry not found ${err}')}
responses := app.get_responses(entry, propfind_req) or {
return ctx.server_error('Failed to get entry properties ${err}')
}
res := '<?xml version="1.0" encoding="UTF-8"?>${doc.pretty_str('').split('\n')[1..].join('')}'
println('debugzo ${responses}')
// Create multistatus response using the responses
ctx.res.set_status(.multi_status)
return ctx.send_response_to_client('application/xml', res)
// return veb.not_found()
return ctx.send_response_to_client('application/xml', responses.xml())
}
@['/:path...'; put]
@@ -454,12 +315,15 @@ fn (mut app App) create_or_update(mut ctx Context, path string) veb.Result {
} else {
return ctx.server_error('failed to get FS Entry ${path}: ${err.msg()}')
}
data := ctx.req.data.bytes()
app.vfs.file_write(path, data) or { return ctx.server_error(err.msg()) }
return ctx.ok('HTTP 200: Successfully saved file: ${path}')
// return ctx.ok('HTTP 200: Successfully saved file: ${path}')
} else {
data := ctx.req.data.bytes()
app.vfs.file_create(path) or { return ctx.server_error(err.msg()) }
}
if ctx.req.data.len > 0 {
data := ctx.req.data.bytes()
app.vfs.file_write(path, data) or { return ctx.server_error(err.msg()) }
return ctx.ok('HTTP 200: Successfully wrote file: ${path}')
}
return ctx.ok('HTTP 200: Successfully created file: ${path}')
}

View File

@@ -10,7 +10,6 @@ fn (app &App) auth_middleware(mut ctx Context) bool {
ctx.send_response_to_client('text', 'unauthorized')
return false
}
if auth_header == '' {
ctx.res.set_status(.unauthorized)
ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"')
@@ -24,7 +23,6 @@ fn (app &App) auth_middleware(mut ctx Context) bool {
ctx.send_response_to_client('text', 'unauthorized')
return false
}
auth_decoded := base64.decode_str(auth_header[6..])
split_credentials := auth_decoded.split(':')
if split_credentials.len != 2 {
@@ -38,12 +36,14 @@ fn (app &App) auth_middleware(mut ctx Context) bool {
if user := app.user_db[username] {
if user != hashed_pass {
ctx.res.set_status(.unauthorized)
ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"')
ctx.send_response_to_client('text', 'unauthorized')
return false
}
return true
}
ctx.res.set_status(.unauthorized)
ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"')
ctx.send_response_to_client('text', 'unauthorized')
return false
}

View File

@@ -0,0 +1,11 @@
module webdav
import freeflowuniverse.herolib.ui.console
import log
// fn add_dav_headers(mut ctx Context) bool {
// ctx.set_custom_header('dav', '1,2') or { return ctx.server_error(err.msg()) }
// ctx.set_header(.allow, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
// ctx.set_custom_header('MS-Author-Via', 'DAV') or { return ctx.server_error(err.msg()) }
// return true
// }

View File

@@ -4,14 +4,12 @@ import freeflowuniverse.herolib.ui.console
import log
fn middleware_log_request(mut ctx Context) bool {
log.debug('[WebDAV] New Request: Method: ${ctx.req.method.str()} Path: ${ctx.req.url}')
log.debug('[WebDAV] New Request: Method: ${ctx.req.method.str()} Path: ${ctx.req.url}')
log.debug('[WebDAV] Request: ${ctx.req.method.str()} ${ctx.req.url}')
return true
}
fn middleware_log_response(mut ctx Context) bool {
log.debug('[WebDAV] Response: Method: ${ctx.req.method.str()} Path: ${ctx.req.url}')
log.debug('[WebDAV] Response: ${ctx.res}')
log.debug('[WebDAV] Response: ${ctx.req.url} ${ctx.res.status()}')
return true
}

122
lib/dav/webdav/model_lock.v Normal file
View File

@@ -0,0 +1,122 @@
module webdav
import encoding.xml
import time
pub struct Lock {
pub mut:
resource string
owner string
token string
depth int // 0 for a single resource, 1 for recursive
timeout int // in seconds
created_at time.Time
lock_type string // typically 'write'
scope string // 'exclusive' or 'shared'
}
fn (l Lock) xml() string {
return $tmpl('./templates/lock_response.xml')
}
// parse_lock_xml parses the XML data from a WebDAV LOCK request
// and extracts the lock parameters (scope, type, owner)
fn parse_lock_xml(xml_data string) !Lock {
mut lock_info := Lock{
scope: 'exclusive' // default values
lock_type: 'write'
owner: ''
}
// Parse the XML document
doc := xml.XMLDocument.from_string(xml_data) or {
return error('Failed to parse XML: ${err}')
}
// Get the root element (lockinfo)
root := doc.root
// Handle namespace prefixes (D:) in element names
// WebDAV uses namespaces, so we need to check for both prefixed and non-prefixed names
// Extract lockscope
for child in root.children {
if child is xml.XMLNode {
node := child as xml.XMLNode
// Check for lockscope (with or without namespace prefix)
if node.name == 'lockscope' || node.name == 'D:lockscope' {
for scope_child in node.children {
if scope_child is xml.XMLNode {
scope_node := scope_child as xml.XMLNode
if scope_node.name == 'exclusive' || scope_node.name == 'D:exclusive' {
lock_info.scope = 'exclusive'
} else if scope_node.name == 'shared' || scope_node.name == 'D:shared' {
lock_info.scope = 'shared'
}
}
}
}
// Check for locktype (with or without namespace prefix)
if node.name == 'locktype' || node.name == 'D:locktype' {
for type_child in node.children {
if type_child is xml.XMLNode {
type_node := type_child as xml.XMLNode
if type_node.name == 'write' || type_node.name == 'D:write' {
lock_info.lock_type = 'write'
}
}
}
}
// Check for owner (with or without namespace prefix)
if node.name == 'owner' || node.name == 'D:owner' {
for owner_child in node.children {
if owner_child is xml.XMLNode {
owner_node := owner_child as xml.XMLNode
if owner_node.name == 'href' || owner_node.name == 'D:href' {
for href_content in owner_node.children {
if href_content is string {
lock_info.owner = (href_content as string).trim_space()
break
}
}
}
} else if owner_child is string {
// Some clients might include owner text directly
lock_info.owner = (owner_child as string).trim_space()
}
}
}
}
}
// If owner is still empty, try to extract it from any text content in the owner node
if lock_info.owner.len == 0 {
for child in root.children {
if child is xml.XMLNode {
node := child as xml.XMLNode
if node.name == 'owner' || node.name == 'D:owner' {
for content in node.children {
if content is string {
lock_info.owner = (content as string).trim_space()
break
}
}
}
}
}
}
// Use a default owner if none was found
if lock_info.owner.len == 0 {
lock_info.owner = 'unknown-client'
}
// Debug output
// println('Parsed lock info: scope=${lock_info.scope}, type=${lock_info.lock_type}, owner=${lock_info.owner}')
return lock_info
}

View File

@@ -0,0 +1,112 @@
module webdav
import encoding.xml
import log
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.vfs
import os
import time
import veb
// Property represents a WebDAV property
pub interface Property {
xml() string
xml_name() string
}
type DisplayName = string
type GetLastModified = string
type GetContentType = string
type GetContentLength = string
type ResourceType = bool
type CreationDate = string
type SupportedLock = string
type LockDiscovery = string
fn (p []Property) xml() string {
return '<D:propstat>
<D:prop>${p.map(it.xml()).join_lines()}</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>'
}
fn (p DisplayName) xml() string {
return '<D:displayname>${p}</D:displayname>'
}
fn (p DisplayName) xml_name() string {
return '<displayname/>'
}
fn (p GetLastModified) xml() string {
return '<D:getlastmodified>${p}</D:getlastmodified>'
}
fn (p GetLastModified) xml_name() string {
return '<getlastmodified/>'
}
fn (p GetContentType) xml() string {
return '<D:getcontenttype>${p}</D:getcontenttype>'
}
fn (p GetContentType) xml_name() string {
return '<getcontenttype/>'
}
fn (p GetContentLength) xml() string {
return '<D:getcontentlength>${p}</D:getcontentlength>'
}
fn (p GetContentLength) xml_name() string {
return '<getcontentlength/>'
}
fn (p ResourceType) xml() string {
return if p {
'<D:resourcetype><D:collection/></D:resourcetype>'
} else {
'<D:resourcetype/>'
}
}
fn (p ResourceType) xml_name() string {
return '<resourcetype/>'
}
fn (p CreationDate) xml() string {
return '<D:creationdate>${p}</D:creationdate>'
}
fn (p CreationDate) xml_name() string {
return '<creationdate/>'
}
fn (p SupportedLock) xml() string {
return '<D:supportedlock>
<D:lockentry>
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
<D:lockentry>
<D:lockscope><D:shared/></D:lockscope>
<D:locktype><D:write/></D:locktype>
</D:lockentry>
</D:supportedlock>'
}
fn (p SupportedLock) xml_name() string {
return '<supportedlock/>'
}
fn (p LockDiscovery) xml() string {
return '<D:lockdiscovery>${p}</D:lockdiscovery>'
}
fn (p LockDiscovery) xml_name() string {
return '<lockdiscovery/>'
}
fn format_iso8601(t time.Time) string {
return '${t.year:04d}-${t.month:02d}-${t.day:02d}T${t.hour:02d}:${t.minute:02d}:${t.second:02d}Z'
}

View File

@@ -1,150 +0,0 @@
module webdav
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.vfs
import encoding.xml
import os
import time
import veb
fn generate_response_element(entry vfs.FSEntry) !xml.XMLNode {
path := if entry.is_dir() && entry.get_path() != '/' {
'${entry.get_path()}/'
} else {
entry.get_path()
}
return xml.XMLNode{
name: 'D:response'
children: [
xml.XMLNode{
name: 'D:href'
children: [path]
},
generate_propstat_element(entry)!,
]
}
}
const xml_ok_status = xml.XMLNode{
name: 'D:status'
children: ['HTTP/1.1 200 OK']
}
const xml_500_status = xml.XMLNode{
name: 'D:status'
children: ['HTTP/1.1 500 Internal Server Error']
}
fn generate_propstat_element(entry vfs.FSEntry) !xml.XMLNode {
prop := generate_prop_element(entry) or {
// TODO: status should be according to returned error
return xml.XMLNode{
name: 'D:propstat'
children: [xml_500_status]
}
}
return xml.XMLNode{
name: 'D:propstat'
children: [prop, xml_ok_status]
}
}
fn generate_prop_element(entry vfs.FSEntry) !xml.XMLNode {
metadata := entry.get_metadata()
display_name := xml.XMLNode{
name: 'D:displayname'
children: ['${metadata.name}']
}
content_length := if entry.is_dir() { 0 } else { metadata.size }
get_content_length := xml.XMLNode{
name: 'D:getcontentlength'
children: ['${content_length}']
}
creation_date := xml.XMLNode{
name: 'D:creationdate'
children: ['${format_iso8601(metadata.created_time())}']
}
get_last_mod := xml.XMLNode{
name: 'D:getlastmodified'
children: ['${format_iso8601(metadata.modified_time())}']
}
content_type := match entry.is_dir() {
true {
'httpd/unix-directory'
}
false {
get_file_content_type(entry.get_path())
}
}
get_content_type := xml.XMLNode{
name: 'D:getcontenttype'
children: ['${content_type}']
}
mut get_resource_type_children := []xml.XMLNodeContents{}
if entry.is_dir() {
get_resource_type_children << xml.XMLNode{
name: 'D:collection xmlns:D="DAV:"'
}
}
get_resource_type := xml.XMLNode{
name: 'D:resourcetype'
children: get_resource_type_children
}
mut nodes := []xml.XMLNodeContents{}
nodes << display_name
nodes << get_last_mod
nodes << get_content_type
nodes << get_resource_type
if !entry.is_dir() {
nodes << get_content_length
}
nodes << creation_date
mut res := xml.XMLNode{
name: 'D:prop'
children: nodes.clone()
}
return res
}
fn get_file_content_type(path string) string {
ext := path.all_after_last('.')
content_type := if v := veb.mime_types[ext] {
v
} else {
'text/plain; charset=utf-8'
}
return content_type
}
fn format_iso8601(t time.Time) string {
return '${t.year:04d}-${t.month:02d}-${t.day:02d}T${t.hour:02d}:${t.minute:02d}:${t.second:02d}Z'
}
fn (mut app App) get_responses(path string, depth int) ![]xml.XMLNodeContents {
if depth == 0 {
entry := app.vfs.get(path)!
return [generate_response_element(entry)!]
}
mut responses := []xml.XMLNodeContents{}
entries := app.vfs.dir_list(path) or { return responses }
for e in entries {
responses << generate_response_element(e)!
}
return responses
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>@{ctx.req.url}</d:href>
<d:status>HTTP/1.1 423 Locked</d:status>
<d:error><d:lock-token-submitted/></d:error>
</d:response>
</d:multistatus>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8" ?>
<d:multistatus xmlns:d="DAV:">
<d:response>
<d:href>@{ctx.req.url}</d:href>
<d:status>HTTP/1.1 @{err.status.int()} @{message}</d:status>
<d:error><d:@{err.tag}/></d:error>
</d:response>
</d:multistatus>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8" ?>
<D:prop xmlns:D="DAV:">
<D:lockdiscovery xmlns:D="DAV:">
<D:activelock>
<D:locktype><D:@{l.lock_type}/></D:locktype>
<D:lockscope><D:@{l.scope}/></D:lockscope>
<D:depth>@{l.depth}</D:depth>
<D:owner>
<D:href>@{l.owner}</D:href>
</D:owner>
<D:timeout>Second-@{l.timeout}</D:timeout>
<D:locktoken>
<D:href>@{l.token}</D:href>
</D:locktoken>
<D:lockroot>
<D:href>@{l.resource}</D:href>
</D:lockroot>
</D:activelock>
</D:lockdiscovery>
</D:prop>