· 4 years ago · Jul 13, 2021, 09:38 AM
1package.path = '/www/api/?.lua;' .. package.path
2
3local socket = require('/var/run/lighttpd/fcgi-lua.socket')
4local json = require('luci.json')
5local uci = require('luci.model.uci')
6local log = require('restlog')
7
8module("restutil", package.seeall)
9
10local M = {}
11
12function M.starts_with(a_string, prefix, case_insensitive)
13 local target = string.sub(a_string, 1, string.len(prefix))
14 if case_insensitive then
15 target = string.lower(target)
16 end
17 return (target == prefix)
18end
19
20function M.split(str, delimiter)
21 local res = {}
22 string.gsub(str, '[^'..delimiter..']+', function(w) table.insert(res, w) end )
23 return res
24end
25
26-------------------------------------------------------------------------------
27-- http functions
28M.http = {
29 content_type = 'application/json',
30 methods = {
31 get = 'GET',
32 post = 'POST',
33 delete = 'DELETE'
34 },
35 codes = {
36 okay = {
37 value = 200,
38 text = 'Okay'
39 },
40 bad_request = {
41 value = 400,
42 text = 'Bad Request'
43 },
44 unauthorized = {
45 value = 401,
46 text = 'Unauthorized'
47 },
48 forbidden = {
49 value = 403,
50 text = 'Forbidden'
51 },
52 not_found = {
53 value = 404,
54 text = 'Not Found'
55 },
56 method_not_allowed = {
57 value = 405,
58 text = 'Method Not Allowed'
59 }
60 }
61}
62
63function M.urldecode(str, no_plus)
64 local function _chrdec(hex)
65 return string.char(tonumber(hex, 16))
66 end
67
68 if type(str) == 'string' then
69 if not no_plus then
70 str = str:gsub('+', ' ')
71 end
72
73 str = str:gsub('%%([a-fA-F0-9][a-fA-F0-9])', _chrdec)
74 end
75
76 return str
77end
78
79function M.urldecode_params(url, tbl)
80 local params = tbl or { }
81
82 if url:find('?') then
83 url = url:gsub('^.+%?([^?]+)', '%1')
84 end
85
86 for pair in url:gmatch('[^&;]+') do
87 local key = urldecode(pair:match('^([^=]+)'))
88 local val = urldecode(pair:match('^[^=]+=(.+)$'))
89
90 if type(key) == 'string' and key:len() > 0 then
91 if type(val) ~= 'string' then val = '' end
92
93 if not params[key] then
94 params[key] = val
95 elseif type(params[key]) ~= 'table' then
96 params[key] = { params[key], val }
97 else
98 table.insert(params[key], val)
99 end
100 end
101 end
102
103 return params
104end
105
106function M.get_post_data(env)
107 if env == nil or env.CONTENT_LENGTH == nil or type(env.CONTENT_LENGTH) ~= "string" then
108 return { }
109 end
110
111 local len = tonumber(env.CONTENT_LENGTH)
112 if len == nil or len <= 0 then
113 return { }
114 end
115
116 return json.decode(io.read(len))
117end
118
119function M.response(status, body)
120 if status ~= 200 and not body then
121 body = {
122 type = 'error',
123 status = status,
124 }
125 end
126
127 #uhttpd.send('Status: ' .. tostring(status) .. ' OK\r\n')
128 #uhttpd.send('Content-Type: application/json\r\n\r\n')
129 #uhttpd.send(json.encode(body))
130end
131
132-------------------------------------------------------------------------------
133-- ns api functions
134function M.ns_api(json_req, host, port)
135 local host = host or '127.0.0.1'
136 local port = port or 57000
137 local tcp = assert(socket.tcp())
138
139 tcp:connect(host, port)
140 local send_res = tcp:send(json_req)
141 if not send_res then
142 tcp:close()
143 return nil
144 end
145 tcp:shutdown('send')
146
147 local json_resp = tcp:receive('*a')
148 if not json_resp then
149 tcp:close()
150 return nil
151 end
152 tcp:close()
153
154 return json.decode(json_resp)
155end
156
157function M.ns_app_devices_api(act_id, params, msg_id)
158 local msg_id = msg_id or 1
159 local req = {
160 id = act_id,
161 params = params,
162 token = msg_id
163 }
164 local req_json = json.encode(req)
165 local resp = ns_api(req_json)
166 print(json.encode(resp))
167end
168
169-------------------------------------------------------------------------------
170-- token functions
171function M.validate_user(env)
172 local SCHEME = 'bearer'
173 local SCHEME_LEN = 6
174 local authorization = env.HTTP_AUTHORIZATION
175
176 if not M.starts_with(authorization, SCHEME, true) then
177 local code = M.http.codes.forbidden
178 M.response(code.value)
179 return
180 end
181
182 -- extra 1 for space
183 local token = string.sub(authorization, SCHEME_LEN + 2)
184
185 if M.token_exists(token) then
186 return token
187 else
188 return nil
189 end
190end
191
192function M.token_create_new(non_duplicate)
193 M.token_delete_expired()
194
195 local charset = {} do -- [0-9a-zA-Z]
196 for c = 48, 57 do table.insert(charset, string.char(c)) end
197 for c = 65, 90 do table.insert(charset, string.char(c)) end
198 for c = 97, 122 do table.insert(charset, string.char(c)) end
199 end
200
201 local function get_random_string(length)
202 if not length or length <= 0 then return '' end
203 math.randomseed(os.time())
204 return get_random_string(length - 1) .. charset[math.random(1, #charset)]
205 end
206
207 local token = get_random_string(32)
208
209 if non_duplicate then
210 while M.token_exists(token) do
211 token = get_random_string(32)
212 end
213 end
214
215 -- local expiration = os.time() + 1 * 60 * 60
216 local expiration = os.time() + 7 * 24 * 60 * 60
217
218 M.token_uci_commit(token, expiration)
219
220 return {
221 token = token,
222 expiration = expiration
223 }
224end
225
226function M.token_delete(token)
227 local cur = uci.cursor()
228
229 cur:delete('restapi', 'tokens', token)
230 cur:commit('restapi')
231end
232
233function M.token_exists(token)
234 local cur = uci.cursor()
235 local exists = false
236
237 tokens = cur:get_all('restapi', 'tokens')
238 if not tokens then
239 return false
240 end
241
242 for k, v in pairs(tokens) do
243 -- ignore option name starts with .
244 if not M.starts_with(k, '.') then
245 -- check if token exists
246 log.print(k .. v)
247 -- token expired. Take this chance to clean up all expired tokens
248 if k == token then
249 if os.time() > tonumber(v) then
250 M.token_delete_expired()
251 exists = false
252 else
253 exists = true
254 end
255 end
256 end
257 end
258
259 return exists
260end
261
262function M.token_delete_expired()
263 local cur = uci.cursor()
264 local exists = false
265
266 tokens = cur:get_all('restapi', 'tokens')
267 if not tokens then
268 return false
269 end
270
271 for k, v in pairs(tokens) do
272 -- ignore option name starts with .
273 if not M.starts_with(k, '.') then
274 -- remove expired tokens
275 if os.time() > tonumber(v) then
276 cur:delete('restapi', 'tokens', v)
277 end
278 end
279 end
280
281 cur:commit('restapi')
282end
283
284function M.token_uci_commit(token, expiration)
285 local cur = uci.cursor()
286
287 log.print('save restapi: token=' .. token .. ', expiration=' .. expiration)
288 local sec = cur:get('restapi', 'tokens')
289 if not sec then
290 cur:set('restapi', 'tokens', 'tokens')
291 end
292 cur:set('restapi', 'tokens', token, expiration)
293 cur:commit('restapi')
294end
295
296return M
297