summaryrefslogtreecommitdiffstats
path: root/server.lua
blob: c6fe586bdc6773231bfbbf2f4d30f35a858e2d84 (plain)
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