--[[ Copyright (c) 2012-2014 Kaarle Ritvanen See LICENSE file for license details --]] local aconf = require('aconf') local mbin = aconf.model.binary local mnode = aconf.model.node local isinstance = aconf.object.isinstance local json = require('cjson') local posix = require('posix') local stringy = require('stringy') math.randomseed(os.time()) local save_req = os.execute('[ $(stat -f -c "%T" /) = tmpfs ]') -- TODO shared storage for sessions local sessions = {} return function(env) for sid, session in pairs(sessions) do if session.expires < os.time() then sessions[sid] = nil end end 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) posix.close(uwsgi.connection_fd()) aconf.commit() 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 session, user, txn_id function reset_session_expiry() session.expires = os.time() + 600 end local sid = tonumber(env.HTTP_X_ACONF_AUTH_TOKEN) if sid then session = sessions[sid] if not session then return wrap(401) end reset_session_expiry() user = session.user txn_id = tonumber(env.HTTP_X_ACONF_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 function new_txn(defer_validation) return aconf.start_txn{ allow_commit_defer=true, defer_validation=defer_validation, parent=parent_txn } end local txn = new_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] session = {user=data.username, last_txn_id=0, txns={}} reset_session_expiry() sessions[sid] = session return wrap( 204, { ['X-AConf-Auth-Token']=sid, ['X-AConf-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 = aconf.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(aconf.path.parent(path)) name = aconf.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) elseif isinstance(v, mbin.Data) then v = v.path end if readable then node[k] = v end end res = {data=node, meta=mnode.meta(obj)} elseif mnode.has_permission(parent, user, 'read') then if isinstance(obj, mbin.Data) then obj = obj:encode() end 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 isinstance(obj, mnode.List) then if not mnode.has_permission(obj, user, 'create') then return 403 end local index if not 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] = new_txn() return 204, {['X-AConf-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