--[[ Copyright (c) 2012-2016 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 update = require('aconf.util').update 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) local function wrap(code, headers, res, encode) headers = update( headers, { ['Cache-Control']='no-cache, no-store, must-revalidate', Pragma='no-cache', Expires='0' } ) 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 local success, code, headers, res, encode = xpcall( function() local session local function log_action(txn, params, s) params.user = mnode.name((s or session).user) mnode.insert(txn:fetch('/aaa/action-log', true), params) end local function log_session_event(action, session) local txn = aconf.start_txn() log_action(txn, {action=action}, session) txn:commit() end for sid, session in pairs(sessions) do if session.expires < os.time() then sessions[sid] = nil log_session_event('expire', session) end end local method = env.REQUEST_METHOD local path = env.PATH_INFO if path == '/' then if method ~= 'GET' then return 405 end return 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 400, nil, 'Request not in JSON format' end end 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 401 end reset_session_expiry() end if path == '/login' then if method == 'POST' then if not data.username or not data.password then return 401 end local user = aconf.start_txn():fetch('/aaa/users')[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=user, last_txn_id=0, txns={}} reset_session_expiry() sessions[sid] = session log_session_event('login') return 204, { ['X-AConf-Auth-Token']=sid, ['X-AConf-Save-Required']=save_req and 1 or 0 } end return 401 end if not session then return 401 end if method == 'DELETE' then sessions[sid] = nil log_session_event('logout') return 204 end return 405 end if not session then return 401 end if path == '/save' then if not save_req then return 404 end if method ~= 'POST' then return 405 end if os.execute('lbu commit') then return 204 end return 500, nil, 'lbu commit failed' end local success, code, hdr, res = aconf.call( function() local txn_id = tonumber(env.HTTP_X_ACONF_TRANSACTION_ID) local parent_txn if txn_id then parent_txn = session.txns[txn_id] if not parent_txn then return 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, user=session.user } end local txn = new_txn(true) 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 local node = {} for k, v in pairs(obj) do local readable = true if isinstance(v, mnode.TreeNode) then v = mnode.path(v) elseif isinstance(v, mbin.Data) then v = v.path end node[k] = v end res = {data=node, meta=mnode.meta(obj)} else if isinstance(obj, mbin.Data) then obj = obj:encode() end res = {data=obj, meta=mnode.mmeta(parent, name)} end return 200, nil, res end local jdata = json.encode(data) local function log_obj_action(action) log_action(txn, {action=action, path=path, data=jdata}) end if method == 'POST' then local obj = txn:fetch(path) if isinstance(obj, mnode.List) then local index if not isinstance(obj, mnode.Set) then index = data.index data = data.data end mnode.insert(obj, data, index) log_obj_action('insert') elseif type(obj) == 'function' then res = obj(data) log_obj_action('invoke') else return 405 end else if method == 'DELETE' then if parent[name] == nil then return 404 end parent[name] = nil log_obj_action('delete') elseif method == 'PUT' then if isinstance(parent, mnode.Set) then return 405 end parent[name] = data log_obj_action('set') 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 code, hdr, res, true end if code.forbidden then return 403, nil, code.forbidden, true end if code.conflict then return 409, nil, code.conflict, true end return 422, nil, code, true end, function(err) return err..'\n'..debug.traceback() end ) if success then return wrap(code, headers, res, encode) end print(code) return wrap(500, nil, 'Internal server error') end