#!/bin/9lua
-- webdav ver. 1.3 (2014/10/20)
-- need Lua-5.2
-- coded by Kenar (Kenji arisawa)
package.path = "/usr/local/lib/lua/5.2/?.lua"
-- "?" is same as "*" in shell syntax
gen = require("gen")
dir = require("dir")
xml = require("xml")
dav = require("dav")
basename = gen.basename
test = dir.test
lsdir = dir.lsdir
isdir = dir.isdir
sub = string.sub
gsub = string.gsub
match = string.match
find = string.find
format = string.format
function res_options(header)
-- under the compile option -DWIN2000DAV in making Pegasus, OPTIONS never reach here.
header['Status'] = '200 OK'
if locksupport then
header["Allow"] = "OPTIONS,GET,HEAD,PUT,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK"
header["DAV"] = "1,2"
else
header["Allow"] = "OPTIONS,GET,HEAD,PUT,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE"
header["DAV"] = "1"
end
header["Content-Type"] = "text/plain"
return header,body
end
function res_get(header)
header["Status"] = nil
header["X-CGI-Pass"] = target -- Pegasus special
header["ETag"] = etag
header["Content-Type"] = nil
header["Content-Length"] = nil
return header,body
end
function res_head(header)
header["Status"] = nil
header["X-CGI-Pass"] = target -- Pegasus special
header['ETag'] = etag
header["Content-Type"] = "text/plain"
return header,body
end
function locked_xmlmsg(path)
local msg = format([[
%s
]],path)
return msg
end
function res_put(header)
-- we need not support PUT for Collection -- rfc2518
-- PUT for collection is the work of client
if contentRange and contentRange ~= "" then
header['Status'] = '501 Not Implemented'
return header,body
end
if dav.unlock1(target,if_tokens) == false then
header['Status'] = '423 Locked'
body = locked_xmlmsg(target)
return header,body
end
local g = test("-e", target)
local f = io.open(target,"w+")
if f == nil then
if test("-d",basename(target)) then
header['Status'] = '403 Permission Denied'
else
--[[ rfc4918
A PUT that would result in the creation of a resource without an
appropriately scoped parent collection MUST fail with a 409 (Conflict).
]]
header['Status'] = '409 Conflict'
end
else
f:write(content) -- content in the request
f:close()
if g == true then
header['Status'] = '204 No Content'
else
header['Status'] = '201 Created'
header['Location'] = htarget
-- f = p9.dirstat(target)
-- Apache/2.2.9 does not return new ETag for PUT method
-- header['ETag'] = dav.mketag(f)
end
end
return header,body
end
function res_delete(header)
-- the existence of the target is already confirmed
if path == "" or path == "/" then
header['Status'] = '403 Permission Denied'
return header,body
end
if dav.unlock1(target,if_tokens) == false then
header['Status'] = '423 Locked'
return header,body
end
if test("-d",target) and dav.unlock(target,if_tokens) == false then
header['Status'] = '423 Locked'
body = locked_xmlmsg(target)
return header,body
-- we should return graceful message in XML format
end
if not dir.removable(target) then
header['Status'] = '403 Permission Denied'
else
dav.rmlock(target)
dav.remove(target)
header['Status'] = '204 No Content'
end
return header,body
end
function res_mkcol(header)
-- the existence of the target is not confirmed
local s
if content and content ~= "" then
header['Status'] = '501 Not Implemented'
elseif test("-e",target) then
header['Status'] = '405 Method Not Allowed'
elseif not test("-e",basename(target)) then
header['Status'] = '409 Conflict'
else
s = p9.mkdir(target)
if s then
header['Status'] = '201 Created'
else
header['Status'] = '403 Forbidden'
end
header['Location'] = htarget
end
return header,body
end
function res_copy(header)
-- print(target,destpath)
if depth and depth ~= '0' and depth ~= 'infinity' then
header['Status'] = '400 Bad Request'
return header,body
end
if not destpath then
header['Status'] = '400 Bad Request'
return header,body
end
if target == destpath then
header['Status'] = "403 Forbidden"
return header,body
end
if dav.unlock1(target,if_tokens) == false then
header['Status'] = '423 Locked'
return header,body
end
if test("-d",target) and dav.unlock(target,if_tokens) == false then
header['Status'] = '423 Locked'
body = locked_xmlmsg(target)
return header,body
-- we should return graceful message in XML format
end
--[[
according to rfc2518, overwrite == "T" is default for COPY and MOVE.
If the overwrite header is not included in a COPY or MOVE request
then the resource MUST treat the request as if it has an overwrite
header of value "T"
dreadful specification!
]]
if overwrite == nil then
overwrite = "T"
end
-- the existence of the target is already confirmed
if test("-e",destpath) == false then
dav.copy(target,destpath)
header['Status'] = '201 Created'
header['Location'] = htarget
elseif test("-w",destpath) == false then
header['Status'] = '403 Permission Denied'
else
if overwrite == "F" then
header['Status'] = '412 Precondifion Failed'
else
if dir.removable(destpath) == false then
header['Status'] = '403 Permission Denied'
return header,body
end
dav.remove(destpath)
dav.copy(target,destpath)
header['Status']='204 No Content'
end
end
return header,body
end
function res_move(header)
-- equiv. copy followed by delete
-- make dir destpath and move the tree under source to the destpath
-- we should confirm the source is removable
-- for overwrite, look the comment above
if destpath == nil then
header['Status'] = '400 Bad Request'
return header,body
end
if depth and depth ~= 'infinity' then
header['Status']='400 Bad Request'
return header,body
end
if target == destpath then
header['Status']="403 Forbidden"
return header,body
end
if dav.unlock1(target,if_tokens) == false then
header['Status'] = '423 Locked'
return header,body
end
if test("-d",target) and dav.unlock(target,if_tokens) == false then
header['Status'] = '423 Locked'
body = locked_xmlmsg(target)
return header,body
-- we should return graceful message in XML format
end
if overwrite == nil then
overwrite = "T"
end
if not dir.removable(target) then
header['Status'] = '403 Permission Denied'
return header,body
end
-- the existence of the target is already confirmed
if test("-e",destpath) == false then
dav.move(target,destpath)
header['Status']='201 Created'
header['Location'] = destination
return header,body
end
-- then we have something in destination
if overwrite == "F" then
header['Status'] = '412 Precondifion Failed'
return header,body
end
if not dir.removable(destpath) then
header['Status'] = '403 Permission Denied'
return header,body
end
dav.remove(destpath)
dav.rmlock(target)
dav.move(target,destpath)
header['Status']='204 No Content'
header['Location'] = destination
return header,body
end
function res_propfind(header)
local namelist
--[[
rfc2518:
A client may submit a Depth header with a value of "0", "1", or
"infinity" with a PROPFIND on a collection resource with internal
member URIs. DAV compliant servers MUST support the "0", "1" and
"infinity" behaviors. By default, the PROPFIND method without a Depth
header MUST act as if a "Depth: infinity" header was included.
rfc4918:
In practice, support for infinite-depth
requests MAY be disabled, due to the performance and security
concerns associated with this behavior. Servers SHOULD treat a
request without a Depth header as if a "Depth: infinity" header was
included.
]]
local t={["0"]=1,["1"]=1,["infinity"]=1}
local list
if depth and t[depth] == nil then
header["Status"]="400 Bad Request"
return header,body
end
-- the existence of the target is already confirmed
header["Status"] = "207 Multi-Status"
header["Content-Type"] = "text/xml"
-- apache returns ETag header
header["ETag"] = etag
-- list = lsdir(docroot..path,tonumber(depth)) -- need not protect against depth==nil
list = lsdir(target,tonumber(depth)) -- need not protect against depth==nil
-- list is a set of absolute path; we remove prepending target part
-- assume target="/doc/foo" and list={[1]="/doc/foo/bar",...}
-- then we want to have: namelist={[1]="bar",...}
-- list is a table; hide lines that have "._.locktokens"
namelist = {}
for i,v in ipairs(list) do
v = sub(v,#target + 1)
v = gsub(v,"^/","")
if not match(v,"%._%.locktokens") then
table.insert(namelist,v)
end
end
-- then if the target is a file, we will get namelist={[1]=""}
-- we should hide OSX special files from non-OSX client
if user_agent and (not match(user_agent,"Darwin")) then
local nl = {}
for i,v in ipairs(namelist) do
if not match(v,"%._") then
table.insert(nl,v)
end
end
namelist = nl
end
-- dprint(namelist)
-- for i,v in ipairs(namelist) do print(i,v) end -- DEBUG
body = dav.mkresponse(target,htarget,namelist,reqhash)
return header,body
end
function res_proppatch(header)
header["Status"] = "501 Not Implemented"
return header,body
end
--[[ a typical case for lock request:
http://www.apple.com/webdav_fs/
--]]
function res_lock(header)
local s,u,l,v,n,t,locktype,lockscope,owner, lockroot,lockinfo
if depth and depth ~= "0" and depth ~= "infinity" then
header["Status"] = "400 Bad Request"
return header,body
end
t = xml.pullout(reqhash)
if t["lockinfo locktype"] ~= "write" then
header["Status"] = "501 Not Implemented"
return header,body
end
if t["lockinfo lockscope"] ~= "exclusive" and
t["lockinfo lockscope"] ~= "shared" then
header["Status"] = "501 Not Implemented"
return header,body
end
if not test("-e", target) then
header["Status"] = "412 Precondition Failed"
return header,body
end
locktype = t["lockinfo locktype"]
lockscope = t["lockinfo lockscope"]
owner = t["lockinfo owner href"]
owner = xml.encode(owner)
lockroot = htarget
lockinfo = {
["locktype"] = locktype,
["lockscope"] = lockscope,
["owner"] = owner,
["depth"] = depth,
["lockroot"] = lockroot,
}
newtoken = dav.lock(target,http_if,lockinfo)
if newtoken == nil then
header["Status"] = "423 Locked"
body = locked_xmlmsg(target)
return header,body
end
header["Status"] = "200 OK"
body = [[
<%s/>
<%s/>
%s
%s
Second-%s
%s
%s
]]
body=format(body,locktype,lockscope,depth,owner,timeout,newtoken,lockroot)
return header,body
end
function res_unlock(header)
local s
-- dprint(target,locktoken)
s = dav.unlock1(target,locktoken)
if s == true then
header["Status"] = "204 No Content"
return header,body
end
if s == nil then
header["Status"] = "409 Conflict"
else -- s == false
heade["Status"] = "400 Bad Request"
end
return header,body
end
function flush(header,body)
if body == nil then
body = ""
end
header["Content-Length"] = #body
for k,v in pairs(header) do
io.write(k..": "..v.."\r\n")
end
io.write("\r\n"..body)
io.flush()
end
function logit(f,header,body)
if f == nil then
return
end
if body == nil then
body = ""
end
if header == nil then
f:write("#DEBUG: "..body.."\n")
f:flush()
return
end
header["Content-Length"] = #body
f:write("------response------\n")
for k,v in pairs(header) do
f:write(k..": "..v.."\n")
end
if method ~= "GET" then
f:write("\n"..body.."\n")
end
f:flush()
end
-- main routine start up from here --
-- LOG = io.open("/log/webdav.log","a") -- uncomment if you need log
locksupport = true
-- maxtimeout = 86400 -- max lock timeout, one day
maxtimeout = 3600 -- max lock timeout, one hour
docroot = "/doc" -- a fixed value if Pegasus. -Kenar-
-- NOTE: headers that begin with HTTP_ come from HTTP request header
user = os.getenv("user")
host = os.getenv("HTTP_HOST")
query = os.getenv("QUERY_STRING")
method = os.getenv("REQUEST_METHOD")
user_agent = os.getenv("HTTP_USER_AGENT")
connection = os.getenv("HTTP_CONNECTION") -- "Keep-Alive" or "Close"
depth = os.getenv("HTTP_DEPTH")
if depth == "" then -- make simple
depth = nil
end
timeout = nil
t = os.getenv("HTTP_TIMEOUT") -- example: "Second-600", "Infinite",
if t then
timeout = match(t,"Second-([0-9]+)")
end
if timeout == nil or timeout > maxtimeout then
timeout = maxtimeout
end
range = os.getenv("HTTP_RANGE")
ifModifiedSince = os.getenv("HTTP_IF_MODIFIED_SINCE")
ifRange = os.getenv("HTTP_IF_RANGE")
contentRange = os.getenv("HTTP_CONTENT_RANGE")
locktoken = os.getenv("HTTP_LOCK_TOKEN")
-- This locktoken is of the form: "" where "<(.)>" is true lock token
-- we must strip "<" and ">"
if locktoken then
locktoken = match(locktoken,"<([^>]+)>")
end
overwrite = os.getenv("HTTP_OVERWRITE")
port = os.getenv("SERVER_PORT")
scheme = os.getenv("HTTP_SCHEME")
if scheme == "http" and port == "80" then
stem = "http://" .. host
elseif scheme == "https" and port == "443" then
stem = "https://" .. host
else
stem = scheme .. "://" .. host .. ":"..port
end
requser = os.getenv('REQUEST_USER')
if requser ~= nil and #requser ~= 0 then
stem = stem .. "/~" .. requser
end
davdir = "/"
rootdir = docroot
path = os.getenv("request") -- path part in "http://host/dav/somewhere", i.e., "/dav/somewhere"
-- check some integrity for the request
path = gsub(path,"/+","/") -- replace contiguous "/" to single "/" altough this is done in Pegasus
path = gsub(path,"/$", "") -- remove trailing "/" if present
-- target is already urldecoded by Peagsus
target = docroot .. path -- real target in view of the server
htarget = stem .. dav.urlencode(path) -- URL oriented target in view of the client
header = {
["Status"] = "500 Internal Server Error",
["MS-Author-Via"] = "DAV",
["ETag"] = "",
["Connection"] = "keep-alive",
["Content-Type"] = "text/xml",
["Content-Length"] = 0
}
if #arg > 0 then -- Lua does't count arg[0] for #arg
davdir = match(path,"^(/[^/]+)")
-- this is /dav if request is "/dav/somewher
rootdir = arg[1]
m = match(path,"^/[^/]+(.*)")
-- matching part is, for example, "/somewhere" if request is "/dev/somewhere"
-- or "" if request is "/dav"
target = rootdir .. m -- then, "/doc/private/somewhere" for example
else
header["Status"] = "500 Internal Errort: lua arguments"
flush(header,"")
logit(LOG,header,"")
os.exit()
end
http_if = os.getenv('HTTP_IF')
-- Example of "If:" header
-- MOVE /container/ HTTP/1.1
-- Destination: http://www.example.com/othercontainer/
-- If: ()
-- ()
--
-- COPY /~fielding/index.html HTTP/1.1
-- Destination: http://www.example.com/users/f/fielding/index.html
-- If:
-- ()
--
-- I think the syntax and the semantics of "If:" is unnecessarily complicated
-- so I implement only the following form:
-- If: ([^(]*%([^<]*<([^>]+)>[^)]*%))+
-- and I will use these tokens to pass through the locked gate without removing the lock
-- If one of if_tokens matches the locked token then we pass through.
-- Note that we neglect "Not", which might make a problem.
if_tokens = {}
if http_if then
gsub(http_if,"%([^<]*<([^>]+)>[^)]*%)",function(a)
if_tokens[a] = true
end)
end
destination = os.getenv('HTTP_DESTINATION')
if destination then
local dest
dest = dav.urldecode(destination) -- need this one
dest = gsub(dest,"(/)$","") -- remove trailing "/" if present
destpath = sub(dest,#stem+1)
m = match(destpath,davdir.."(.*)")
if m then
if sub(m,1,1) ~= "/" then
m = "/"..m
end
destpath = rootdir..m
end
end
contentlength=os.getenv("CONTENT_LENGTH")
if contentlength ~= nil then
contentlength=tonumber(contentlength)
end
if contentlength == nil then -- need not for webdav but we support this one
content = io.read("*a") -- body part in the request
elseif contentlength == 0 then
content = ""
else
content = io.read(contentlength) -- body part in the request
if method ~= "PUT" then
s = xml.collect(content)
t = xml.namepath(s)
-- we simply discard label part in name
reqhash = {}
for k,v in pairs(t) do
k = gsub(k,"%w+:","") -- remove name label
if sub(k, #k, #k) ~= " " then -- skip attribute value
reqhash[k] = v
end
end
end
end
if LOG then -- print all env name and the value
LOG:write( "----------request: "..os.date().."-------------\n")
--[[
local d = p9.readdir("/env")
local f,s
for k,v in pairs(d) do
f=io.open("/env/"..k)
s=f:read("*a")
LOG:write(format("%s=%s\n",k,s))
f:close()
end
--]]
--[[
for k,v in pairs(reqhash) do
LOG:write(k..":"..v.."\n")
end
--]]
s = os.getenv("HTTP_HEADER")
if method ~= "PUT" then
LOG:write(s..content.."\n")
else
LOG:write(s.."....".."("..contentlength..")...\n\n")
end
LOG:flush()
end
-- we can confirm the existence of the target for some methods
-- don't put MKCOL and PUT here.
if locksupport then
method_tar = "OPTIONS|GET|HEAD|PROPFIND|COPY|MOVE|DELETE|PROPPATCH|LOCK|UNLOCK"
else
method_tar = "OPTIONS|GET|HEAD|PROPFIND|COPY|MOVE|DELETE|PROPPATCH"
end
if match(method_tar,method) then
f = p9.dirstat(target)
if f == nil then
header["Status"] = "404 Not Found"
flush(header,"")
logit(LOG,header,"")
os.exit()
end
etag = dav.mketag(f)
end
resfuns = {
["OPTIONS"] = res_options,
["GET"] = res_get,
["HEAD"] = res_head,
["PUT"] = res_put,
["DELETE"] = res_delete,
["MKCOL"] = res_mkcol,
["COPY"] = res_copy,
["MOVE"] = res_move,
["PROPFIND"] = res_propfind,
["PROPPATCH"] = res_proppatch,
["LOCK"] = res_lock,
["UNLOCK"] = res_unlock
}
f = resfuns[method]
if f then
-- header,body = f(header)
st,h,body = pcall(f,header) -- call f(header) in protected mode. look lua manual.
if st == false then
header["Connection"] = "close"
body = '\n'..h.."\n"
else
header = h
if connection then
header["Connection"] = connection
end
end
-- other input are: target,depth,...
else
header["Status"] = "501 Not Implemented: "..method
end
flush(header,body)
logit(LOG,header,body)