implement webdav server in veb

This commit is contained in:
timurgordon
2025-02-20 19:04:07 +03:00
parent 9160e95e4a
commit 6b0cf48292
7 changed files with 149 additions and 238 deletions

View File

@@ -1,24 +1,25 @@
module webdav module webdav
import vweb import veb
import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.vfs.vfscore import freeflowuniverse.herolib.vfs.vfscore
@[heap] pub struct App {
struct App { veb.Middleware[Context]
vweb.Context server_port int
user_db map[string]string @[required]
pub mut: pub mut:
lock_manager LockManager lock_manager LockManager
vfs vfscore.VFSImplementation @[vweb_global] user_db map[string]string @[required]
server_port int vfs vfscore.VFSImplementation @[veb_global]
middlewares map[string][]vweb.Middleware }
pub struct Context {
veb.Context
} }
@[params] @[params]
pub struct AppArgs { pub struct AppArgs {
pub mut: pub mut:
server_port int = 8080
user_db map[string]string @[required] user_db map[string]string @[required]
vfs vfscore.VFSImplementation vfs vfscore.VFSImplementation
} }
@@ -26,43 +27,29 @@ pub mut:
pub fn new_app(args AppArgs) !&App { pub fn new_app(args AppArgs) !&App {
mut app := &App{ mut app := &App{
user_db: args.user_db.clone() user_db: args.user_db.clone()
server_port: args.server_port
vfs: args.vfs vfs: args.vfs
} }
app.middlewares['/'] << logging_middleware // register middlewares for all routes
app.middlewares['/'] << app.auth_middleware app.use(handler: logging_middleware)
app.use(handler: unsafe{app.auth_middleware})
return app return app
} }
@[params] @[params]
pub struct RunArgs { pub struct RunParams {
pub mut: pub mut:
port int = 8088
background bool background bool
} }
pub fn (mut app App) run(args RunArgs) { pub fn (mut app App) run(params RunParams) {
console.print_green('Running the server on port: ${app.server_port}') console.print_green('Running the server on port: ${app.server_port}')
if params.background {
if args.background { spawn veb.run[App, Context](mut app, params.port)
spawn vweb.run(app, app.server_port)
} else { } else {
vweb.run(app, app.server_port) veb.run[App, Context](mut app, params.port)
} }
} }
pub fn (mut app App) not_found() vweb.Result {
app.set_status(404, 'Not Found')
return app.text('Not Found')
}
pub fn (mut app App) server_error() vweb.Result {
app.set_status(500, 'Inernal Server Error')
return app.text('Internal Server Error')
}
pub fn (mut app App) bad_request(message string) vweb.Result {
app.set_status(400, 'Bad Request')
return app.text(message)
}

View File

@@ -1,43 +0,0 @@
module webdav
import vweb
import encoding.base64
fn (mut app App) auth_middleware(mut ctx vweb.Context) bool {
auth_header := ctx.get_header('Authorization')
if auth_header == '' {
ctx.set_status(401, 'Unauthorized')
ctx.add_header('WWW-Authenticate', 'Basic realm="WebDAV Server"')
ctx.send_response_to_client('', '')
return false
}
if !auth_header.starts_with('Basic ') {
ctx.set_status(401, 'Unauthorized')
ctx.add_header('WWW-Authenticate', 'Basic realm="WebDAV Server"')
ctx.send_response_to_client('', '')
return false
}
auth_decoded := base64.decode_str(auth_header[6..])
split_credentials := auth_decoded.split(':')
if split_credentials.len != 2 {
ctx.set_status(401, 'Unauthorized')
ctx.add_header('WWW-Authenticate', 'Basic realm="WebDAV Server"')
ctx.send_response_to_client('', '')
return false
}
username := split_credentials[0]
hashed_pass := split_credentials[1]
if app.user_db[username] != hashed_pass {
ctx.set_status(401, 'Unauthorized')
ctx.add_header('WWW-Authenticate', 'Basic realm="WebDAV Server"')
ctx.send_response_to_client('', '')
return false
}
return true
}

View File

@@ -3,227 +3,181 @@ module webdav
import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.ui.console
import encoding.xml import encoding.xml
import net.urllib import net.urllib
import vweb import veb
@['/:path...'; options] @['/:path...'; options]
fn (mut app App) options(path string) vweb.Result { pub fn (app &App) options(mut ctx Context, path string) veb.Result {
app.set_status(200, 'OK') ctx.res.set_status(.ok)
app.add_header('DAV', '1,2') ctx.res.header.add_custom('dav', '1,2') or {return ctx.server_error(err.msg())}
app.add_header('Allow', 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE') ctx.res.header.add(.allow, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
app.add_header('MS-Author-Via', 'DAV') ctx.res.header.add_custom('MS-Author-Via', 'DAV') or {return ctx.server_error(err.msg())}
app.add_header('Access-Control-Allow-Origin', '*') ctx.res.header.add(.access_control_allow_origin, '*')
app.add_header('Access-Control-Allow-Methods', 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE') ctx.res.header.add(.access_control_allow_methods, 'OPTIONS, PROPFIND, MKCOL, GET, HEAD, POST, PUT, DELETE, COPY, MOVE')
app.add_header('Access-Control-Allow-Headers', 'Authorization, Content-Type') ctx.res.header.add(.access_control_allow_headers, 'Authorization, Content-Type')
app.send_response_to_client('text/plain', '') return ctx.text('')
return vweb.not_found()
} }
@['/:path...'; LOCK] @['/:path...'; lock]
fn (mut app App) lock_handler(path string) vweb.Result { pub fn (mut app App) lock_handler(mut ctx Context, path string) veb.Result {
// Not yet working resource := ctx.req.url
// TODO: Test with multiple clients owner := ctx.get_custom_header('owner') or {return ctx.server_error(err.msg())}
resource := app.req.url
owner := app.get_header('Owner')
if owner.len == 0 { if owner.len == 0 {
app.set_status(400, 'Bad Request') ctx.res.set_status(.bad_request)
return app.text('Owner header is required.') return ctx.text('Owner header is required.')
} }
depth := if app.get_header('Depth').len > 0 { app.get_header('Depth').int() } else { 0 } depth := ctx.get_custom_header('Depth') or { '0' }.int()
timeout := if app.get_header('Timeout').len > 0 { app.get_header('Timeout').int() } else { 3600 } timeout := ctx.get_custom_header('Timeout') or { '3600' }.int()
token := app.lock_manager.lock(resource, owner, depth, timeout) or { token := app.lock_manager.lock(resource, owner, depth, timeout) or {
app.set_status(423, 'Locked') ctx.res.set_status(.locked)
return app.text('Resource is already locked.') return ctx.text('Resource is already locked.')
} }
app.set_status(200, 'OK') ctx.res.set_status(.ok)
app.add_header('Lock-Token', token) ctx.res.header.add_custom('Lock-Token', token) or {return ctx.server_error(err.msg())}
return app.text('Lock granted with token: ${token}') return ctx.text('Lock granted with token: ${token}')
} }
@['/:path...'; UNLOCK] @['/:path...'; unlock]
fn (mut app App) unlock_handler(path string) vweb.Result { pub fn (mut app App) unlock_handler(mut ctx Context, path string) veb.Result {
// Not yet working resource := ctx.req.url
// TODO: Test with multiple clients token := ctx.get_custom_header('Lock-Token') or {return ctx.server_error(err.msg())}
resource := app.req.url
token := app.get_header('Lock-Token')
if token.len == 0 { if token.len == 0 {
console.print_stderr('Unlock failed: `Lock-Token` header required.') console.print_stderr('Unlock failed: `Lock-Token` header required.')
app.set_status(400, 'Bad Request') ctx.res.set_status(.bad_request)
return app.text('Lock failed: `Owner` header missing.') return ctx.text('Lock failed: `Owner` header missing.')
} }
if app.lock_manager.unlock_with_token(resource, token) { if app.lock_manager.unlock_with_token(resource, token) {
app.set_status(204, 'No Content') ctx.res.set_status(.no_content)
return app.text('Lock successfully released') return ctx.text('Lock successfully released')
} }
console.print_stderr('Resource is not locked or token mismatch.') console.print_stderr('Resource is not locked or token mismatch.')
app.set_status(409, 'Conflict') ctx.res.set_status(.conflict)
return app.text('Resource is not locked or token mismatch') return ctx.text('Resource is not locked or token mismatch')
} }
@['/:path...'; get] @['/:path...'; get]
fn (mut app App) get_file(path string) vweb.Result { pub fn (mut app App) get_file(mut ctx Context, path string) veb.Result {
if !app.vfs.exists(path) { if !app.vfs.exists(path) {
return app.not_found() return ctx.not_found()
} }
fs_entry := app.vfs.get(path) or { fs_entry := app.vfs.get(path) or {
console.print_stderr('failed to get FS Entry ${path}: ${err}') console.print_stderr('failed to get FS Entry ${path}: ${err}')
return app.server_error() return ctx.server_error(err.msg())
} }
file_data := app.vfs.file_read(fs_entry.get_path()) or { return app.server_error() } file_data := app.vfs.file_read(fs_entry.get_path()) or { return ctx.server_error(err.msg()) }
ext := fs_entry.get_metadata().name.all_after_last('.') ext := fs_entry.get_metadata().name.all_after_last('.')
content_type := if v := vweb.mime_types[ext] { content_type := veb.mime_types[ext] or { 'text/plain' }
v
} else {
'text/plain'
}
app.set_status(200, 'Ok') ctx.res.set_status(.ok)
app.send_response_to_client(content_type, file_data.str()) return ctx.text(file_data.str())
return vweb.not_found() // this is for returning a dummy result
} }
@['/:path...'; delete] @['/:path...'; delete]
fn (mut app App) delete(path string) vweb.Result { pub fn (mut app App) delete(mut ctx Context, path string) veb.Result {
if !app.vfs.exists(path) { if !app.vfs.exists(path) {
return app.not_found() return ctx.not_found()
} }
fs_entry := app.vfs.get(path) or { fs_entry := app.vfs.get(path) or {
console.print_stderr('failed to get FS Entry ${path}: ${err}') console.print_stderr('failed to get FS Entry ${path}: ${err}')
return app.server_error() return ctx.server_error(err.msg())
} }
if fs_entry.is_dir() { if fs_entry.is_dir() {
console.print_debug('deleting directory: ${path}') console.print_debug('deleting directory: ${path}')
app.vfs.dir_delete(path) or { return app.server_error() } app.vfs.dir_delete(path) or { return ctx.server_error(err.msg()) }
} }
if fs_entry.is_file() { if fs_entry.is_file() {
console.print_debug('deleting file: ${path}') console.print_debug('deleting file: ${path}')
app.vfs.file_delete(path) or { return app.server_error() } app.vfs.file_delete(path) or { return ctx.server_error(err.msg()) }
} }
if fs_entry.is_symlink() { ctx.res.set_status(.no_content)
console.print_debug('deleting symlink: ${path}') return ctx.text('entry ${path} is deleted')
app.vfs.link_delete(path) or { return app.server_error() }
}
console.print_debug('entry: ${path} is deleted')
app.set_status(204, 'No Content')
return app.text('entry ${path} is deleted')
} }
// @['/:path...'; put]
// fn (mut app App) create_or_update(path string) vweb.Result {
// fs_entry := app.vfs.get(path) or {
// console.print_stderr('failed to get FS Entry ${path}: ${err}')
// return app.server_error()
// }
// mut p := pathlib.get(app.root_dir.path + path)
// if p.is_dir() {
// console.print_stderr('Cannot PUT to a directory: ${p.path}')
// app.set_status(405, 'Method Not Allowed')
// return app.text('HTTP 405: Method Not Allowed')
// }
// file_data := app.req.data
// p = pathlib.get_file(path: p.path, create: true) or {
// console.print_stderr('failed to get file ${p.path}: ${err}')
// return app.server_error()
// }
// p.write(file_data) or {
// console.print_stderr('failed to write file data ${p.path}: ${err}')
// return app.server_error()
// }
// app.set_status(200, 'Successfully saved file: ${p.path}')
// return app.text('HTTP 200: Successfully saved file: ${p.path}')
// }
@['/:path...'; copy] @['/:path...'; copy]
fn (mut app App) copy(path string) vweb.Result { pub fn (mut app App) copy(mut ctx Context, path string) veb.Result {
if !app.vfs.exists(path) { if !app.vfs.exists(path) {
return app.not_found() return ctx.not_found()
} }
destination := app.get_header('Destination') destination := ctx.req.header.get_custom('Destination') or {
return ctx.server_error(err.msg())
}
destination_url := urllib.parse(destination) or { destination_url := urllib.parse(destination) or {
return app.bad_request('Invalid Destination ${destination}: ${err}') ctx.res.set_status(.bad_request)
return ctx.text('Invalid Destination ${destination}: ${err}')
} }
destination_path_str := destination_url.path destination_path_str := destination_url.path
app.vfs.get(path) or {
console.print_stderr('failed to get FS Entry ${path}: ${err}')
return app.server_error()
}
app.vfs.copy(path, destination_path_str) or { app.vfs.copy(path, destination_path_str) or {
console.print_stderr('failed to copy: ${err}') console.print_stderr('failed to copy: ${err}')
return app.server_error() return ctx.server_error(err.msg())
} }
app.set_status(200, 'Successfully copied entry: ${path}') ctx.res.set_status(.ok)
return app.text('HTTP 200: Successfully copied entry: ${path}') return ctx.text('HTTP 200: Successfully copied entry: ${path}')
} }
@['/:path...'; move] @['/:path...'; move]
fn (mut app App) move(path string) vweb.Result { pub fn (mut app App) move(mut ctx Context, path string) veb.Result {
if !app.vfs.exists(path) { if !app.vfs.exists(path) {
return app.not_found() return ctx.not_found()
} }
destination := app.get_header('Destination') destination := ctx.req.header.get_custom('Destination') or {
return ctx.server_error(err.msg())
}
destination_url := urllib.parse(destination) or { destination_url := urllib.parse(destination) or {
return app.bad_request('Invalid Destination ${destination}: ${err}') ctx.res.set_status(.bad_request)
return ctx.text('Invalid Destination ${destination}: ${err}')
} }
destination_path_str := destination_url.path destination_path_str := destination_url.path
app.vfs.move(path, destination_path_str) or { app.vfs.move(path, destination_path_str) or {
console.print_stderr('failed to move: ${err}') console.print_stderr('failed to move: ${err}')
return app.server_error() return ctx.server_error(err.msg())
} }
app.set_status(200, 'Successfully moved entry: ${path}') ctx.res.set_status(.ok)
return app.text('HTTP 200: Successfully moved entry: ${path}') return ctx.text('HTTP 200: Successfully copied entry: ${path}')
} }
@['/:path...'; mkcol] @['/:path...'; mkcol]
fn (mut app App) mkcol(path string) vweb.Result { pub fn (mut app App) mkcol(mut ctx Context, path string) veb.Result {
if app.vfs.exists(path) { if app.vfs.exists(path) {
return app.bad_request('Another collection exists at ${path}') ctx.res.set_status(.bad_request)
return ctx.text('Another collection exists at ${path}')
} }
app.vfs.dir_create(path) or { app.vfs.dir_create(path) or {
console.print_stderr('failed to create directory ${path}: ${err}') console.print_stderr('failed to create directory ${path}: ${err}')
return app.server_error() return ctx.server_error(err.msg())
} }
app.set_status(201, 'Created') ctx.res.set_status(.created)
return app.text('HTTP 201: Created') return ctx.text('HTTP 201: Created')
} }
@['/:path...'; propfind] @['/:path...'; propfind]
fn (mut app App) propfind(path string) vweb.Result { fn (mut app App) propfind(mut ctx Context, path string) veb.Result {
println('path: ${path}')
if !app.vfs.exists(path) { if !app.vfs.exists(path) {
return app.not_found() return ctx.not_found()
} }
depth := app.get_header('Depth').int() depth := ctx.req.header.get_custom('Depth') or {'0'}.int()
responses := app.get_responses(path, depth) or { responses := app.get_responses(path, depth) or {
console.print_stderr('failed to get responses: ${err}') console.print_stderr('failed to get responses: ${err}')
return app.server_error() return ctx.server_error(err.msg())
} }
doc := xml.XMLDocument{ doc := xml.XMLDocument{
@@ -239,36 +193,7 @@ fn (mut app App) propfind(path string) vweb.Result {
res := '<?xml version="1.0" encoding="UTF-8"?>${doc.pretty_str('').split('\n')[1..].join('')}' res := '<?xml version="1.0" encoding="UTF-8"?>${doc.pretty_str('').split('\n')[1..].join('')}'
// println('res: ${res}') // println('res: ${res}')
app.set_status(207, 'Multi-Status') ctx.res.set_status(.multi_status)
app.send_response_to_client('application/xml', res) return ctx.send_response_to_client('application/xml', res)
return vweb.not_found() // return veb.not_found()
} }
fn (mut app App) generate_resource_response(path string) string {
mut response := ''
response += app.generate_element('response', 2)
response += app.generate_element('href', 4)
response += app.generate_element('/href', 4)
response += app.generate_element('/response', 2)
return response
}
fn (mut app App) generate_element(element string, space_cnt int) string {
mut spaces := ''
for i := 0; i < space_cnt; i++ {
spaces += ' '
}
return '${spaces}<${element}>\n'
}
// TODO: implement
// @['/'; proppatch]
// fn (mut app App) prop_patch() vweb.Result {
// }
// TODO: implement, now it's used with PUT
// @['/'; post]
// fn (mut app App) post() vweb.Result {
// }

View File

@@ -0,0 +1,46 @@
module webdav
import encoding.base64
fn (mut app App) auth_middleware(mut ctx Context) bool {
auth_header := ctx.get_header(.authorization) or {
ctx.res.set_status(.unauthorized)
ctx.send_response_to_client('', 'Authorization header not found in request.')
return false
}
if auth_header == '' {
ctx.res.set_status(.unauthorized)
ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"')
ctx.send_response_to_client('', '')
return false
}
if !auth_header.starts_with('Basic ') {
ctx.res.set_status(.unauthorized)
ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"')
ctx.send_response_to_client('', '')
return false
}
auth_decoded := base64.decode_str(auth_header[6..])
split_credentials := auth_decoded.split(':')
if split_credentials.len != 2 {
ctx.res.set_status(.unauthorized)
ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"')
ctx.send_response_to_client('', '')
return false
}
username := split_credentials[0]
hashed_pass := split_credentials[1]
if app.user_db[username] != hashed_pass {
ctx.res.set_status(.unauthorized)
ctx.res.header.add(.www_authenticate, 'Basic realm="WebDAV Server"')
ctx.send_response_to_client('', '')
return false
}
return true
}

View File

@@ -3,7 +3,7 @@ module webdav
import vweb import vweb
import freeflowuniverse.herolib.ui.console import freeflowuniverse.herolib.ui.console
fn logging_middleware(mut ctx vweb.Context) bool { fn logging_middleware(mut ctx Context) bool {
console.print_green('=== New Request ===') console.print_green('=== New Request ===')
console.print_green('Method: ${ctx.req.method.str()}') console.print_green('Method: ${ctx.req.method.str()}')
console.print_green('Path: ${ctx.req.url}') console.print_green('Path: ${ctx.req.url}')

View File

@@ -154,12 +154,10 @@ fn (mut app App) get_responses(path string, depth int) ![]xml.XMLNodeContents {
if os.is_dir(path) { if os.is_dir(path) {
mut dir := pathlib.get_dir(path: path) or { mut dir := pathlib.get_dir(path: path) or {
app.set_status(500, 'failed to get directory ${path}: ${err}')
return error('failed to get directory ${path}: ${err}') return error('failed to get directory ${path}: ${err}')
} }
entries := dir.list(recursive: false) or { entries := dir.list(recursive: false) or {
app.set_status(500, 'failed to list directory ${path}: ${err}')
return error('failed to list directory ${path}: ${err}') return error('failed to list directory ${path}: ${err}')
} }

View File

@@ -7,9 +7,7 @@ import encoding.base64
import rand import rand
fn test_run() { fn test_run() {
root_dir := '/tmp/webdav'
mut app := new_app( mut app := new_app(
root_dir: root_dir
user_db: { user_db: {
'mario': '123' 'mario': '123'
} }