1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
--[[
Copyright (c) 2012-2013 Kaarle Ritvanen
See LICENSE file for license details
--]]
require 'acf'
local isinstance = acf.object.isinstance
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 = {}
-- TODO implement transactions as threads or store their state in
-- shared storage
local last_txn_id = 0
local txns = {}
-- TODO expire stale sessions and transactions
return function(env)
local method = env.REQUEST_METHOD
local path = env.REQUEST_URI
local data
if env.CONTENT_LENGTH then
data = json.decode(env.input:read(env.CONTENT_LENGTH))
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
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
else txn = acf.transaction.start() end
local function fetch_user(name)
user = name and txn:search('/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
last_sid = last_sid + 1
local sid = last_sid
sessions[sid] = data.username
return 200, {['X-ACF-Auth-Token']=sid}
end
return 401
end
if not user then return 401 end
if method == 'DELETE' then
sessions[sid] = nil
return 200
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
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 200
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
return 404
end
|