#!/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([[
<?xml version="1.0" encoding="utf-8" ?>
<error xmlns="DAV:">
<lock-token-submitted>
<href>%s</href>
</lock-token-submitted>
</error>
]],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:
<?xml version="1.0" encoding="utf-8"?>
<D:lockinfo xmlns:D="DAV:">
<D:lockscope><D:exclusive/></D:lockscope>
<D:locktype><D:write/></D:locktype>
<D:owner>
<D:href>http://www.apple.com/webdav_fs/</D:href>
</D:owner>
</D:lockinfo>
--]]

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 = [[
<?xml version="1.0" encoding="utf-8"?>
<prop xmlns="DAV:">
 <lockdiscovery>
  <activelock>
   <locktype><%s/></locktype>
   <lockscope><%s/></lockscope>
   <depth>%s</depth>
   <owner><href>%s</href></owner>
   <timeout>Second-%s</timeout>
   <locktoken><href>%s</href></locktoken>
   <lockroot><href>%s</href></lockroot>
  </activelock>
 </lockdiscovery>
</prop>
]]
  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: "<some-lock-token>" 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: (<urn:uuid:fe184f2e-6eec-41d0-c765-01adc56e6bb4>)
--       (<urn:uuid:e454f3f3-acdc-452a-56c7-00a5c91e4b77>)
--
--   COPY /~fielding/index.html HTTP/1.1
--   Destination: http://www.example.com/users/f/fielding/index.html
--   If: <http://www.example.com/users/f/fielding/index.html>
--       (<urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6>)
--
--   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 = '<?xml version="1.0" encoding="utf-8" ?>\n<error>'..h.."</error>\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)