--[[ Copyright (c) 2012-2013 Kaarle Ritvanen See LICENSE file for license details --]] require 'acf' local isinstance = acf.object.isinstance require 'json' require 'stringy' local function handle(txn, method, path, data) 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 local function f() coroutine.yield(json.encode(res)) end return 200, {['Content-Type']='application/json'}, coroutine.wrap(f) end if method == 'DELETE' then parent[name] = nil elseif method == 'PUT' then parent[name] = data -- TODO implement POST for invoking object-specific actions else return 405 end return 200 end -- 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 data if env.CONTENT_LENGTH then data = json.decode(env.input:read(env.CONTENT_LENGTH)) 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 401 end txn_id = tonumber(env.HTTP_X_ACF_TRANSACTION_ID) end local txn if txn_id then txn = txns[txn_id] if not txn then return 404 end else txn = acf.transaction.start() end 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 401 end end if path == '/login' then if method == 'POST' then if not data.username or not data.password then return 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 200, {['X-ACF-Auth-Token']=sid} end return 401 end if not user then return 401 end if method == 'DELETE' then sessions[sid] = nil return 200 end return 405 end if not user then return 401 end if stringy.startswith(path, '/config/') then -- TODO catch and forward relevant errors to the client local code, hdr, body = handle(txn, method, string.sub(path, 8, -1), data) if not txn_id and method ~= 'GET' and code == 200 then txn:commit() end return code, hdr, body end if path == '/' then if method == 'GET' then return 301, {['Location']='/browser/'} end if not ({DELETE=true, POST=true})[method] then return 405 end if txn_id then if method == 'POST' then txn:commit() end txns[txn_id] = nil return 200 end if method == 'DELETE' then return 405 end last_txn_id = last_txn_id + 1 local txn_id = last_txn_id txns[txn_id] = txn return 200, {['X-ACF-Transaction-ID']=txn_id} end return 404 end