Engine 0.0.4
Game engine in lua
Loading...
Searching...
No Matches
http_ginga.lua
Go to the documentation of this file.
1--! @file src/lib/protocol/http_ginga.lua
2--! @short HTTP ginga module
3--!
4--! @li @b specification: https://www.rfc-editor.org/rfc/rfc2616
5--!
6--! @par Compare
7--! | feature/support | this module | manoel campos |
8--! | :- | :-: | :-: |
9--! | Samsung TVs | yes | no |
10--! | Redirect 3xx | yes | no |
11--! | HTTPS protocol | handler error | no |
12--! | Timeout request | yes | no |
13--! | DNS Resolving | yes | no |
14--! | multi-request | yes, event loop | yes, corotines |
15--!
16--! @note @b Samsung Tvs have connections blocked when host is not cached in DNS
17--!
18--! @par Finite State Machine
19--! @startuml
20--! hide empty description
21--! state 0 as "no requests"
22--! state 1 as "DNS resolving": first request
23--! state 2 as "DNS disabled"
24--! state 3 as "DNS Idle"
25--! state 4 as "DNS resolving"
26--!
27--! [*] -> 0
28--! 0 -> 1
29--! 1 -> 2
30--! 1 -> 3
31--! 3 -> 4
32--! 4 -> 3
33--! 2 -> [*]
34--! @enduml
35--!
36--! @par Contexts
37--! @startjson
38--! {
39--! "resolve": "8.8.8.8",
40--! "by_dns": {
41--! "google.com": "8.8.8.8"
42--! },
43--! "by_host": {
44--! "8.8.8.8": [
45--! {
46--! "url": "google.com/search?q=pudim.com.br"
47--! }
48--! ],
49--! "pudim.com.br": [
50--! {
51--! "url": "pudim.com.br/"
52--! }
53--! ],
54--! "example.com": [
55--! {
56--! "url": "example.com/foo"
57--! },
58--! {
59--! "url": "example.com/bar"
60--! }
61--! ]
62--! },
63--! "by_connection": {
64--! "connector.id.1" : {
65--! "url": "example.com/zig"
66--! },
67--! "connector.id.2" : {
68--! "url": "example.com/zag"
69--! }
70--! }
71--! }
72--! @endjson
73local http_util = require('src/lib/util/http')
74local lua_util = require('src/lib/util/lua')
75
76--! @cond
77local function http_connect(self)
78 local params = http_util.url_search_param(self.param_list, self.param_dict)
79 local request, cleanup = http_util.create_request(self.method, self.p_uri..params)
80 .add_imutable_header('Host', self.p_host)
81 .add_imutable_header('Cache-Control', 'max-age=0')
82 .add_mutable_header('Accept', '*/*')
83 .add_mutable_header('Accept-Charset', 'utf-8', lua_util.has_support_utf8())
84 .add_mutable_header('Accept-Charset', 'windows-1252, cp1252')
85 .add_mutable_header('User-Agent', 'Mozilla/4.0 (compatible; MSIE 4.0; Windows 95; Win 9x 4.90)')
86 .add_custom_headers(self.header_list, self.header_dict)
87 .add_imutable_header('Content-Length', tostring(#self.body_content), #self.body_content > 0)
88 .add_imutable_header('Connection', 'close')
89 .add_body_content(self.body_content)
90 .to_http_protocol()
91
92 event.post({
93 class = 'tcp',
94 type = 'data',
95 connection = self.evt.connection,
96 value = request,
97 })
98
99 cleanup()
100end
101--! @endcond
102
103--! @cond
104local function http_connect_dns(self)
105 if self.p_host == self.evt.host then
106 self.application.internal.http.dns_state = 2
107 else
108 self.application.internal.http.context.dns(self)
109 self.application.internal.http.dns_state = 3
110 end
111 event.post({
112 class = 'tcp',
113 type = 'disconnect',
114 connection = self.evt.connection,
115 })
116end
117--! @endcond
118
119--! @cond
120local function http_headers(self)
121 self.p_header = self.p_data:sub(1, self.p_header_pos -1)
122 self.p_status = tonumber(self.p_header:match('^HTTP/%d.%d (%d+) %w*'))
123 self.p_content_size = tonumber(self.p_header:match('Content%-Length: (%d+)') or 0)
124end
125--! @endcond
126
127--! @cond
128local function http_redirect(self)
129 local protocol, location = self.p_header:match('Location: (%w+)://([^%s]*)')
130 local url, uri = (location or self.url):match('^([^/]+)(.*)$')
131 local host, port_str = url:match("^(.-):?(%d*)$")
132 local redirects = self.p_redirects + 1
133 local port = tonumber(port_str and #port_str > 0 and port_str or 80)
134
135 if protocol == 'https' and self.p_host == url then
136 self.evt.error = 'HTTPS is not supported!'
137 self.application.internal.http.callbacks.http_error(self)
138 elseif self.p_redirects > 5 then
139 self.evt.error = 'Too Many Redirects!'
140 self.application.internal.http.callbacks.http_error(self)
141 else
142 local index = #self.application.internal.http.queue + 1
143
144 event.post({
145 class = 'tcp',
146 type = 'disconnect',
147 connection = self.evt.connection,
148 })
149
150 self.application.internal.http.context.remove(self.evt)
151 self.application.internal.http.callbacks.http_clear(self)
153 self.p_url = url
154 self.p_uri = uri or '/'
155 self.p_ip = host
156 self.p_host = host
157 self.p_port = port
158 self.p_data = ''
159 self.p_redirects = redirects
160
161 self.application.internal.http.queue[index] = self
162 end
163end
164--! @endcond
165
166--! @cond
167local function http_error(self)
168 self.set('ok', false)
169 self.set('error', self.evt and self.evt.error or 'unknown error')
170 self.resolve()
171end
172--! @endcond
173
174--! @cond
175local function http_data_fast(self)
176 local evt = self.evt
177 local status = self.evt.value:match('^HTTP/%d.%d (%d+) %w*')
178 if not status then
179 self.evt = {error = self.evt.value}
180 self.application.internal.http.callbacks.http_error(self)
181 else
182 self.p_status = tonumber(status)
183 self.application.internal.http.callbacks.http_resolve(self)
184 end
185 event.post({
186 class = 'tcp',
187 type = 'disconnect',
188 connection = evt.connection,
189 })
190end
191--! @endcond
192
193--! @cond
194local function http_data(self)
195 self.p_data = self.p_data..self.evt.value
196
197 if not self.p_header_pos then
198 self.p_header_pos = self.p_data:find('\r\n\r\n')
199 if self.p_header_pos then
200 self.application.internal.http.callbacks.http_headers(self)
201 end
202 if http_util.is_redirect(self.p_status) then
203 self.application.internal.http.callbacks.http_redirect(self)
204 return
205 end
206 end
207
208 if self.p_header_pos and (#self.p_data - self.p_header_pos) >= self.p_content_size then
209 local evt = self.evt
210 self.application.internal.http.context.remove(self.evt)
211 self.application.internal.http.callbacks.http_resolve(self)
212 event.post({
213 class = 'tcp',
214 type = 'disconnect',
215 connection = evt.connection,
216 })
217 end
218end
219--! @endcond
220
221--! @cond
222local function http_resolve(self)
223 local body = ''
224 if self.speed ~= '_fast' then
225 body = self.p_data:sub(self.p_header_pos + 4, #self.p_data)
226 end
227 self.set('ok', http_util.is_ok(self.p_status))
228 self.set('status', self.p_status)
229 self.set('body', body)
230 self.application.internal.http.callbacks.http_clear(self)
231 self.resolve()
232end
233--! @endcond
234
235--! @cond
236local function http_clear(self)
237 self.evt = nil
238 self.p_url = nil
239 self.p_uri = nil
240 self.p_host = nil
241 self.p_port = nil
242 self.p_status = nil
243 self.p_content_size = nil
244 self.p_header = nil
245 self.p_header_pos = nil
246 self.p_data = nil
247 self.p_redirects = nil
248end
249--! @endcond
250
251--! @short create request
252local function http_handler(self)
253 local protocol, location = self.url:match('(%w*)://?(.*)')
254 local url, uri = (location or self.url):match('^([^/]+)(.*)$')
255 local host, port_str = url:match("^([%w%.]+)([:0-9]*)$")
256 local port = tonumber((port_str and #port_str > 0 and port_str:sub(2, #port_str)) or 80)
257
258 self.p_url = url
259 self.p_uri = uri or '/'
260 self.p_ip = host
261 self.p_host = host
262 self.p_port = port
263 self.p_data = ''
264 self.p_redirects = 0
265
266 if protocol ~= 'http' and location then
267 self.evt = { error = 'HTTPS is not supported!' }
268 self.application.internal.http.callbacks.http_error(self)
269 else
270 local index = #self.application.internal.http.queue + 1
271 self.application.internal.http.queue[index] = self
272 self.promise()
273 end
274end
275
276--! @cond
277local function context_dns(self, contexts)
278 if self.p_host and self.p_ip and self.p_host ~= self.p_ip then
279 contexts.by_dns[self.p_host] = self.p_ip
280 return true
281 elseif contexts.by_dns[self.p_host] then
282 self.p_ip = contexts.by_dns[self.p_host]
283 return true
284 else
285 contexts.dns_resolve = self
286 return false
287 end
288end
289--! @endcond
290
291--! @cond
292local function context_push(self, contexts)
293 local host = self.p_ip
294 if not contexts.by_host[host] then
295 contexts.by_host[host] = {}
296 end
297 local index = #contexts.by_host[host] + 1
298 contexts.by_host[host][index] = self
299end
300--! @endcond
301
302--! @cond
303local function context_pull(evt, contexts)
304 local self = nil
305 local connection = evt.connection
306 local host = (evt.host and contexts.by_dns[evt.dns]) or evt.host
307 local index = host and contexts.by_host[host] and #contexts.by_host[host]
308
309 if host and contexts.dns_resolve then
310 self = {
311 speed = '_dns',
312 type = 'connect',
313 application = contexts.dns_resolve.application,
314 p_host = contexts.dns_resolve.p_host,
315 p_ip = host
316 }
317 contexts.dns_resolve = nil
318 elseif evt.type == 'connect' and host and index and contexts.by_host[host][index] then
319 self = contexts.by_host[host][index]
320 if connection then
321 contexts.by_connection[connection] = self
322 contexts.by_host[host][index] = nil
323 end
324 elseif connection and contexts.by_connection[connection] then
325 self = contexts.by_connection[connection]
326 end
327
328 if self then
329 self.evt = evt
330 if evt.error then
331 self.speed='_error'
332 if connection and contexts.by_connection[connection] then
333 contexts.by_connection[connection] = nil
334 end
335 if host and index and contexts.by_host[host][index] then
336 contexts.by_host[host][index] = nil
337 end
338 end
339 end
340
341 return self
342end
343--! @endcond
344
345--! @cond
346local function context_remove(evt, contexts)
347 local connection = evt.connection
348
349 if connection then
350 contexts.by_connection[connection] = nil
351 end
352end
353--! @endcond
354
355--! @short dequeue request
356--! @param [in] std
357--! @param [in] game
358--! @param [in, out] application
359--! @brief This code may seem confusing, but it was the simplest I thought,
360--! analyze the finite state machine to understand better.
361local function fixed_loop(std, game, application)
362 local state = application.internal.http.dns_state
363 local index = #application.internal.http.queue
364 while index >= 1 and state ~= 1 and state ~= 4 do
365 local self = application.internal.http.queue[index]
366
367 if state == 0 then
368 self.application.internal.http.context.dns(self)
369 state = 1
370 elseif state == 2 then
371 self.application.internal.http.context.push(self)
372 application.internal.http.queue[index] = nil
373 elseif state == 3 then
374 if self.application.internal.http.context.dns(self) then
375 application.internal.http.context.push(self)
376 application.internal.http.queue[index] = nil
377 else
378 state = 4
379 end
380 end
381
382 event.post({
383 class = 'tcp',
384 type = 'connect',
385 host = self.p_ip,
386 port = self.p_port
387 })
388
389 application.internal.http.dns_state = state
390 index = index - 1
391 end
392end
393
394--! @short resolve request
395local function event_loop(std, game, application, evt)
396 if evt.class ~= 'tcp' then return end
397
398 local self = application.internal.http.context.pull(evt)
399
400 local value = tostring(evt.value)
401 local debug = evt.type..' '..tostring(evt.host)..' '..tostring(evt.connection)..' '..value:sub(1, (value:find('\n') or 30) - 2)
402
403 if self then
404 local index = 'http_'..self.evt.type..self.speed
405 application.internal.http.callbacks[index](self)
406 end
407end
408
409local function install(std, game, application)
410 local contexts = {
411 by_dns={},
412 by_host={},
413 by_connection={}
414 }
415
416 application.internal = application.internal or {}
417 application.internal.event_loop = application.internal.event_loop or {}
418 application.internal.fixed_loop = application.internal.fixed_loop or {}
419 application.internal.http = {}
420 application.internal.http.dns_state = 0
421 application.internal.http.queue = {}
422 application.internal.http.context = {
423 dns = function(self) return context_dns(self, contexts) end,
424 push = function(self) context_push(self, contexts) end,
425 pull = function(evt) return context_pull(evt, contexts) end,
426 remove = function (evt) context_remove(evt, contexts) end
427 }
428
429 application.internal.http.callbacks = {
430 -- dns
431 http_connect_dns=http_connect_dns,
432 -- error
433 http_connect_error=http_error,
434 http_data_error=http_error,
435 -- fast
436 http_connect_fast=http_connect,
437 http_data_fast=http_data_fast,
438 -- http
439 http_connect=http_connect,
440 http_data=http_data,
441 -- extra
442 http_redirect=http_redirect,
443 http_headers=http_headers,
444 http_resolve=http_resolve,
445 http_error=http_error,
446 http_clear=http_clear,
447 }
448
449 application.internal.event_loop[#application.internal.event_loop + 1] = function (evt)
450 event_loop(std, game, application, evt)
451 end
452
453 application.internal.fixed_loop[#application.internal.fixed_loop + 1] = function ()
454 fixed_loop(std, game, application)
455 end
456
457 return http_handler
458end
459
460local P = {
461 handler = http_handler,
462 install = install
463}
464
465return P
local function file(self, file)
local function install(std, game, application)
local lua_util
local function http_handler(self)
create request
local function event_loop(std, game, application, evt)
resolve request
local http_util
local function require(std, game, application)
local application
Definition main.lua:16
local game
Definition main.lua:17
local std
Definition main.lua:18
local function fixed_loop()