--[[ Copyright (c) 2012-2013 Kaarle Ritvanen See LICENSE file for license details --]] require 'acf' local isinstance = acf.object.isinstance require 'json' require 'stringy' math.randomseed(os.time()) -- TODO shared storage for sessions -- TODO expire stale sessions local sessions = {} 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 session, user, txn_id if sid then session = sessions[sid] if not session then return wrap(401) end user = session.user txn_id = tonumber(env.HTTP_X_ACF_TRANSACTION_ID) end local parent_txn if txn_id then parent_txn = session.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 local sid repeat sid = math.floor(math.random() * 2^32) until not sessions[sid] sessions[sid] = {user=data.username, last_txn_id=0, txns={}} 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) local res if isinstance(obj, acf.model.node.TreeNode) then local node = {} for k, v in acf.model.node.pairs(obj) do node[k] = isinstance( v, acf.model.node.TreeNode ) and acf.model.node.path(v) or 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 if method == 'POST' then local obj = txn:search(path) if not acf.object.isinstance(obj, acf.model.set.Set) then -- TODO invoke model-specific actions return 405 end acf.model.set.add(obj, data) elseif 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 session.txns[txn_id] = nil return 204 end if method ~= 'POST' then return 405 end session.last_txn_id = session.last_txn_id + 1 local txn_id = session.last_txn_id session.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