-
Notifications
You must be signed in to change notification settings - Fork 7
/
api32.lua
372 lines (289 loc) · 10.1 KB
/
api32.lua
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
--[[
Title : Library for easy way of creating HTTP JSON Api Service for ESP32
Author : Alija Bobija
Author-Website : https://abobija.com
GitHub Repo : https://github.com/abobija/api32
Dependencies:
- sjson
- encoder
]]
local Api32 = {}
local function str_starts_with(haystack, needle)
return haystack:sub(1, #needle) == needle
end
local function str_ends_with(str, ending)
return ending == "" or str:sub(-#ending) == ending
end
local function str_split(inputstr, sep)
if sep == nil then sep = "%s" end
local result = {}
for str in inputstr:gmatch("([^"..sep.."]+)") do table.insert(result, str) end
return result
end
local function json_parse(json_str)
local ok
local result
ok, result = pcall(sjson.decode, json_str)
if ok then return result end
return nil
end
local function json_stringify(table)
local ok
local json
ok, json = pcall(sjson.encode, table)
if ok then return json end
return nil
end
local function get_http_header_value(hname, hlines)
for _, hline in pairs(hlines) do
if str_starts_with(hline:lower(), hname:lower()) then
local colon_index = hline:find(':')
if colon_index ~= nil then
return hline:sub(colon_index + 2)
end
end
end
return nil
end
local function get_auth_from_http_header(hlines)
local auth_line = get_http_header_value('Authorization', hlines)
if auth_line == nil then return nil end
local parts = str_split(auth_line)
if #parts == 2 and parts[1]:lower() == 'basic' then
local key = parts[2]
parts = nil
local ok
local decoded_key
ok, decoded_key = pcall(encoder.fromBase64, key)
key = nil
if ok then
parts = str_split(decoded_key, ':')
decoded_key = nil
if #parts == 2 then
return {
user = parts[1],
pwd = parts[2]
}
end
end
end
return nil
end
local function parse_http_header(request, params)
local options = {
parse_auth = false
}
if params ~= nil then
if params.parse_auth ~= nil then options.parse_auth = params.parse_auth end
end
local hlines = str_split(request, "\r\n")
if #hlines > 0 then
local hline1_parts = str_split(hlines[1])
if #hline1_parts == 3 and hline1_parts[3] == 'HTTP/1.1' then
local result = {
method = hline1_parts[1],
path = hline1_parts[2],
std = hline1_parts[3]
}
hline1_parts = nil
result.content_length = get_http_header_value('Content-Length', hlines)
if options.parse_auth then
result.auth = get_auth_from_http_header(hlines)
end
hlines = nil
if result.content_length ~= nil then
result.content_length = tonumber(result.content_length)
end
return result
end
end
return nil
end
-- Extends one level only
local function extend(tbl, with)
if with ~= nil then
for k, v in pairs(with) do
if tbl[k] == nil then
tbl[k] = with[k]
end
end
end
return tbl
end
Api32.create = function(conf)
local self = extend({
http_body_min = 10,
http_body_max = 512,
port = 80,
auth = nil
}, conf)
local endpoints = {}
self.on = function(method, path, handler)
table.insert(endpoints, {
method = method,
path = path,
handler = handler
})
return self
end
self.on_get = function(path, handler)
return self.on('GET', path, handler)
end
self.on_post = function(path, handler)
return self.on('POST', path, handler)
end
local get_endpoint = function(method, path)
for _, ep in pairs(endpoints) do
if ep.method == method and ep.path == path then return ep end
end
return nil
end
local srv = net.createServer(net.TCP, 30)
local sending = false
local http_header = nil
local http_req_body_buffer = nil
local function stop_rec()
sending = false
http_header = nil
http_req_body_buffer = nil
end
local is_authorized = function()
return self.auth == nil or (
http_header ~= nil
and http_header.auth ~= nil
and self.auth.user == http_header.auth.user
and self.auth.pwd == http_header.auth.pwd
)
end
local function parse_http_request(sck)
local res = {}
local stream_file = nil
local send = function(_sck)
sending = true
local close_socket = false
if #res > 0 then
_sck:send(table.remove(res, 1))
elseif stream_file ~= nil then
local line = stream_file:readline()
if line ~= nil then
_sck:send(line)
else
stream_file:close()
stream_file = nil
close_socket = true
end
else
close_socket = true
end
if close_socket then
sending = false
_sck:close()
res = nil
end
end
sck:on('sent', send)
local response_status = '200 OK'
local content_type = 'application/json'
local response_body = nil
res[1] = 'HTTP/1.1 '
res[2] = 'Content-Type: CTYPE; charset=UTF-8\r\n'
if http_header == nil then
response_status = '400 Bad Request'
else
if not is_authorized() then
response_status = '401 Unauthorized'
res[#res + 1] = 'WWW-Authenticate: Basic realm="User Visible Realm", charset="UTF-8"\r\n'
else
local ep = get_endpoint(http_header.method, http_header.path)
if ep == nil then
response_status = '404 Not Found'
else
http_header = nil
if type(ep.handler) == 'function' then -- custom handler
local jreq = json_parse(http_req_body_buffer)
http_req_body_buffer = nil
local jres = ep.handler(jreq)
jreq = nil
response_body = json_stringify(jres)
jres = nil
elseif type(ep.handler) == 'string' then -- static file
-- ep.handler is filename in this case
http_req_body_buffer = nil
if not file.exists(ep.handler) then
response_status = '404 Not Found'
else
if str_ends_with(ep.handler, 'html') then
content_type = 'text/html'
end
stream_file = file.open(ep.handler)
end
end
end
end
end
res[1] = res[1] .. response_status .. "\r\n"
res[2] = res[2]:gsub('CTYPE', content_type)
res[#res + 1] = "\r\n"
if response_body ~= nil then
res[#res + 1] = response_body
response_body = nil
end
stop_rec()
send(sck)
end
local on_receive = function(sck, data)
if sending then return end
if http_header == nil then
local eof_head = data:find("\r\n\r\n")
local head_data = nil
if eof_head ~= nil then
head_data = data:sub(1, eof_head - 1)
http_req_body_buffer = data:sub(eof_head + 4)
end
data = nil
if head_data ~= nil then
http_header = parse_http_header(head_data, {
parse_auth = self.auth ~= nil
})
head_data = nil
end
if http_header ~= nil then
if http_header.content_length == nil
or http_header.content_length < self.http_body_min
or http_header.content_length > self.http_body_max then
-- It seems like request body is too short, too big or does not exist at all.
-- Parse request immediatelly
return parse_http_request(sck)
end
else
-- Received some data which does not represent the http header.
-- Let's parse it anyway because error 400 shoud be sent back to the client
return parse_http_request(sck)
end
end
if data ~= nil and http_header ~= nil then
-- Buffering request body
if http_req_body_buffer == nil then
http_req_body_buffer = data
else
http_req_body_buffer = http_req_body_buffer .. data
end
end
-- Check if body has received
if http_req_body_buffer ~= nil then
local http_body_len = http_req_body_buffer:len()
if (http_header.content_length ~= nil and http_body_len >= http_header.content_length)
or http_body_len >= self.http_body_max then
-- Received enough bytes of request body.
return parse_http_request(sck)
end
end
end
srv:listen(self.port, function(conn)
stop_rec()
conn:on('receive', on_receive)
conn:on('disconnection', stop_rec)
end)
return self
end
return Api32