--[[ Copyright (c) 2012-2013 Kaarle Ritvanen See LICENSE file for license details --]] require 'acf' local isinstance = acf.object.isinstance require 'json' require 'stringy' -- TODO shared storage for login sessions local last_sid = 0 local sessions = {} -- TODO implement transactions as threads or store their state in -- shared storage local last_txn_id = 0 local txns = {} -- TODO expire stale sessions and transactions return function(env) local method = env.REQUEST_METHOD local path = env.REQUEST_URI local function wrap(code, headers, res) if not headers then headers = {} end local ctype if type(res) == 'table' then ctype = 'application/json' res = json.encode(res) else ctype = 'text/plain' if not res then res = '' end end headers['Content-Type'] = ctype return code, headers, coroutine.wrap( function() coroutine.yield(res) end ) end local data if env.CONTENT_LENGTH then local success success, data = pcall( json.decode, env.input:read(env.CONTENT_LENGTH) ) if not success then return wrap(400, nil, 'Request not in JSON format') end end local sid = tonumber(env.HTTP_X_ACF_AUTH_TOKEN) local user, txn_id if sid then user = sessions[sid] if not user then return wrap(401) end txn_id = tonumber(env.HTTP_X_ACF_TRANSACTION_ID) end local parent_txn if txn_id then parent_txn = txns[txn_id] if not parent_txn then return wrap(400, nil, 'Invalid transaction ID') end end local txn = acf.transaction.start(parent_txn, true) local function fetch_user(name) user = name and txn:search('/auth/users')[name] end if user then fetch_user(user) if not user then return wrap(401) end end if path == '/login' then if method == 'POST' then if not data.username or not data.password then return wrap(401) end fetch_user(data.username) if user and user:check_password(data.password) then last_sid = last_sid + 1 local sid = last_sid sessions[sid] = data.username return wrap(204, {['X-ACF-Auth-Token']=sid}) end return wrap(401) end if not user then return wrap(401) end if method == 'DELETE' then sessions[sid] = nil return wrap(204) end return wrap(405) end if not user then return wrap(401) end local success, code, hdr, res = acf.call( function() if stringy.startswith(path, '/config/') then path = string.sub(path, 8, -1) local parent, name if path ~= '/' then parent = txn:search(acf.path.parent(path)) name = acf.path.name(path) end if method == 'GET' then local obj = txn:search(path) if obj == nil then return 404 end local res if isinstance(obj, acf.model.node.TreeNode) then local node = {} for _, k in ipairs(acf.model.node.members(obj)) do local v = obj[k] if isinstance(v, acf.model.node.TreeNode) then v = acf.model.node.path(v) end node[k] = v end res = {data=node, meta=acf.model.node.meta(obj)} else res = { data=obj, meta=acf.model.node.mmeta(parent, name) } end return 200, nil, res end -- TODO implement POST for invoking object-specific actions if method == 'DELETE' then parent[name] = nil elseif method == 'PUT' then parent[name] = data else return 405 end txn:commit() return 205 end if path == '/' then if method == 'GET' then return 301, {['Location']='/browser/'} end if ({DELETE=true, PUT=true})[method] then if not txn_id then return 405 end if method == 'PUT' then parent_txn:commit() end txns[txn_id] = nil return 204 end if method ~= 'POST' then return 405 end last_txn_id = last_txn_id + 1 local txn_id = last_txn_id txns[txn_id] = acf.transaction.start(parent_txn) return 204, {['X-ACF-Transaction-ID']=txn_id} end return 404 end ) if success then return wrap(code, hdr, res) end if code.conflict then return wrap(409, nil, code.conflict) end return wrap(422, nil, code) end