#!/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)