--[[ Copyright (c) 2012-2013 Kaarle Ritvanen See LICENSE file for license details --]] local acf = require('acf') local mnode = acf.model.node local isinstance = acf.object.isinstance local json = require('json') local stringy = require('stringy') math.randomseed(os.time()) local save_req = os.execute('[ $(stat -f -c "%T" /) = tmpfs ]') -- TODO shared storage for sessions -- TODO expire stale sessions local sessions = {} return function(env) local method = env.REQUEST_METHOD local path = env.PATH_INFO local function wrap(code, headers, res, encode) if not headers then headers = {} end if res then local ctype if encode then ctype = 'application/json' res = json.encode(res) else ctype = 'text/plain' end headers['Content-Type'] = ctype else res = '' end return code, headers, coroutine.wrap( function() coroutine.yield(res) end ) end if path == '/' then if method ~= 'GET' then return wrap(405) end return wrap(301, {['Location']='/browser/'}) end local data local length = env.CONTENT_LENGTH and tonumber( env.CONTENT_LENGTH ) or 0 if length > 0 then local success success, data = pcall( json.decode, env.input:read(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.start_txn(parent_txn, true) local function fetch_user(name) user = name and txn:fetch('/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, ['X-ACF-Save-Required']=save_req and 1 or 0 } ) 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 if path == '/save' then if not save_req then return wrap(404) end if method ~= 'POST' then return wrap(405) end if os.execute('lbu commit') then wrap(204) end return wrap(500, nil, 'lbu commit failed') end local success, code, hdr, res = acf.call( function() if stringy.startswith(path, '/meta/') then if method ~= 'GET' then return 405 end return 200, nil, txn:meta(path:sub(6, -1)) end if stringy.startswith(path, '/config/') then path = path:sub(8, -1) local parent, name, res if path ~= '/' then parent = txn:fetch(acf.path.parent(path)) name = acf.path.name(path) end if method == 'GET' then local obj = txn:fetch(path) if type(obj) == 'function' then return 404 end if isinstance(obj, mnode.TreeNode) then if not mnode.has_permission(obj, user, 'read') then return 403 end local node = {} for k, v in mnode.pairs(obj) do local readable = true if isinstance(v, mnode.TreeNode) then readable = mnode.has_permission( v, user, 'read' ) v = mnode.path(v) end if readable then node[k] = v end end res = {data=node, meta=mnode.meta(obj)} elseif mnode.has_permission(parent, user, 'read') then res = {data=obj, meta=mnode.mmeta(parent, name)} else return 403 end return 200, nil, res end if method == 'POST' then local obj = txn:fetch(path) if acf.object.isinstance(obj, mnode.List) then if not mnode.has_permission(obj, user, 'create') then return 403 end local index if not acf.object.isinstance(obj, mnode.Set) then index = data.index data = data.data end mnode.insert(obj, data, index) elseif type(obj) == 'function' then if not mnode.has_permission(parent, user, name) then return 403 end res = obj(data) else return 405 end else local obj = parent[name] if obj ~= nil and not isinstance(obj, mnode.TreeNode) then obj = parent end if method == 'DELETE' then if obj == nil then return 404 end if not mnode.has_permission(obj, user, 'delete') then return 403 end parent[name] = nil elseif method == 'PUT' then if isinstance(parent, mnode.Set) then return 405 end local permission = 'modify' if obj == nil then obj = parent permission = 'create' end if not mnode.has_permission(obj, user, permission) then return 403 end parent[name] = data else return 405 end end txn:commit() return res == nil and 205 or 200, nil, res end if path == '/transaction' then 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.start_txn(parent_txn) return 204, {['X-ACF-Transaction-ID']=txn_id} end return 404 end ) if success then return wrap(code, hdr, res, true) end if code.conflict then return wrap(409, nil, code.conflict, true) end return wrap(422, nil, code, true) end