summaryrefslogtreecommitdiffstats
path: root/server.lua
diff options
context:
space:
mode:
Diffstat (limited to 'server.lua')
-rw-r--r--server.lua200
1 files changed, 115 insertions, 85 deletions
diff --git a/server.lua b/server.lua
index c6fe586..cbbc82f 100644
--- a/server.lua
+++ b/server.lua
@@ -10,50 +10,6 @@ require 'json'
require 'stringy'
-local function handle(txn, method, path, data)
- 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)
- if obj == nil then return 404 end
-
- local res
-
- if isinstance(obj, acf.model.node.TreeNode) then
- local node = {}
- for _, k in ipairs(acf.model.node.members(obj)) do
- local v = obj[k]
- if isinstance(v, acf.model.node.TreeNode) then
- v = acf.model.node.path(v)
- end
- node[k] = v
- end
- res = {data=node, meta=acf.model.node.meta(obj)}
-
- else res = {data=obj, meta=acf.model.node.mmeta(parent, name)} end
-
- local function f() coroutine.yield(json.encode(res)) end
- return 200, {['Content-Type']='application/json'}, coroutine.wrap(f)
- end
-
- if method == 'DELETE' then
- parent[name] = nil
-
- elseif method == 'PUT' then
- parent[name] = data
-
- -- TODO implement POST for invoking object-specific actions
-
- else return 405 end
-
- return 200
-end
-
-
-- TODO shared storage for login sessions
local last_sid = 0
local sessions = {}
@@ -70,23 +26,50 @@ 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
- data = json.decode(env.input:read(env.CONTENT_LENGTH))
+ 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 user, txn_id
if sid then
user = sessions[sid]
- if not user then return 401 end
+ if not user then return wrap(401) end
txn_id = tonumber(env.HTTP_X_ACF_TRANSACTION_ID)
end
local txn
if txn_id then
txn = txns[txn_id]
- if not txn then return 404 end
+ if not txn then
+ return wrap(400, nil, 'Invalid transaction ID')
+ end
else txn = acf.transaction.start() end
local function fetch_user(name)
@@ -94,66 +77,113 @@ return function(env)
end
if user then
fetch_user(user)
- if not user then return 401 end
+ 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 401 end
+ 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
last_sid = last_sid + 1
local sid = last_sid
sessions[sid] = data.username
- return 200, {['X-ACF-Auth-Token']=sid}
+ return wrap(204, {['X-ACF-Auth-Token']=sid})
end
- return 401
+ return wrap(401)
end
- if not user then return 401 end
+ if not user then return wrap(401) end
if method == 'DELETE' then
sessions[sid] = nil
- return 200
+ return wrap(204)
end
- return 405
- end
-
- if not user then return 401 end
-
- if stringy.startswith(path, '/config/') then
- -- TODO catch and forward relevant errors to the client
- local code, hdr, body = handle(txn,
- method,
- string.sub(path, 8, -1),
- data)
- if not txn_id and method ~= 'GET' and code == 200 then
- txn:commit()
- end
- return code, hdr, body
+ return wrap(405)
end
- if path == '/' then
- if method == 'GET' then return 301, {['Location']='/browser/'} 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)
+ if obj == nil then return 404 end
+
+ local res
+
+ if isinstance(obj, acf.model.node.TreeNode) then
+ local node = {}
+ for _, k in ipairs(acf.model.node.members(obj)) do
+ local v = obj[k]
+ if isinstance(v, acf.model.node.TreeNode) then
+ v = acf.model.node.path(v)
+ end
+ node[k] = 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
+
+ -- TODO implement POST for invoking object-specific actions
+ if method == 'DELETE' then parent[name] = nil
+ elseif method == 'PUT' then parent[name] = data
+ else return 405 end
+
+ if not txn_id then txn:commit() end
+ return 205
+ end
- if not ({DELETE=true, POST=true})[method] then
- return 405
- end
+ if path == '/' then
+ if method == 'GET' then
+ return 301, {['Location']='/browser/'}
+ end
+
+ if not ({DELETE=true, POST=true})[method] then
+ return 405
+ end
+
+ if txn_id then
+ if method == 'POST' then txn:commit() end
+ txns[txn_id] = nil
+ return 204
+ end
+
+ if method == 'DELETE' then return 405 end
+
+ last_txn_id = last_txn_id + 1
+ local txn_id = last_txn_id
+ txns[txn_id] = txn
+ return 204, {['X-ACF-Transaction-ID']=txn_id}
+ end
- if txn_id then
- if method == 'POST' then txn:commit() end
- txns[txn_id] = nil
- return 200
+ return 404
end
+ )
- if method == 'DELETE' then return 405 end
-
- last_txn_id = last_txn_id + 1
- local txn_id = last_txn_id
- txns[txn_id] = txn
- return 200, {['X-ACF-Transaction-ID']=txn_id}
- end
+ if success then return wrap(code, hdr, res) end
- return 404
+ if code.conflict then return wrap(409, nil, code.conflict) end
+
+ return wrap(422, nil, code)
end