diff options
Diffstat (limited to 'server.lua')
-rw-r--r-- | server.lua | 508 |
1 files changed, 252 insertions, 256 deletions
@@ -22,269 +22,265 @@ 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 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 + + local success, code, headers, res, encode = xpcall( + function() + 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 + + 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 + + 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 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 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 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 + 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 204, { + ['X-AConf-Auth-Token']=sid, + ['X-AConf-Save-Required']=save_req and 1 or 0 + } + end + return 401 + end + + if not user then return 401 end + + if method == 'DELETE' then + sessions[sid] = nil + return 204 + end + + return 405 + end + + if not user 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() + 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 + 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 method == 'GET' then + local obj = txn:fetch(path) - if type(obj) == 'function' then return 404 end + 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 + 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 + 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 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)} + 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 + 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 + 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 + 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.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 |