Skip to content

Commit 1cc26ed

Browse files
authored
Add TSGI Layer Between Router and Server (#81)
Changes done: 1. decompose the code into modular pieces 2. decouple router and server parts 3. introduce nginx as possible Web Server via TSGI adapter 4. enable router tests on NGINX Note: this change breaks connection stealing.
1 parent 3669ac5 commit 1cc26ed

File tree

16 files changed

+2054
-1279
lines changed

16 files changed

+2054
-1279
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,14 @@ end
209209
is in the lower case, all headers joined together into a single string.
210210
* `req.peer` - a Lua table with information about the remote peer
211211
(like `socket:peer()`).
212+
**NOTE**: when router is being used with
213+
nginx adapter, `req.peer` contains information on iproto connection with
214+
nginx, not the original HTTP user-agent.
212215
* `tostring(req)` - returns a string representation of the request.
213216
* `req:request_line()` - returns the request body.
214217
* `req:read(delimiter|chunk|{delimiter = x, chunk = x}, timeout)` - reads the
215-
raw request body as a stream (see `socket:read()`).
218+
raw request body as a stream (see `socket:read()`). **NOTE**: when using
219+
NGINX TSGI adapter, only `req:read(chunk)` is available.
216220
* `req:json()` - returns a Lua table from a JSON request.
217221
* `req:post_param(name)` - returns a single POST request a parameter value.
218222
If `name` is `nil`, returns all parameters as a Lua table.

http/nginx_server/init.lua

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
local tsgi = require('http.tsgi')
2+
3+
require('checks')
4+
local json = require('json')
5+
local log = require('log')
6+
7+
local KEY_BODY = 'tsgi.http.nginx_server.body'
8+
9+
local function noop() end
10+
11+
local function convert_headername(name)
12+
return 'HEADER_' .. string.upper(name)
13+
end
14+
15+
local function tsgi_input_read(self, n)
16+
checks('table', '?number') -- luacheck: ignore
17+
18+
local start = self._pos
19+
local last
20+
21+
if n ~= nil then
22+
last = start + n
23+
self._pos = last
24+
else
25+
last = #self._env[KEY_BODY]
26+
self._pos = last
27+
end
28+
29+
return self._env[KEY_BODY]:sub(start, last)
30+
end
31+
32+
local function tsgi_input_rewind(self)
33+
self._pos = 0
34+
end
35+
36+
local function make_env(server, req)
37+
-- NGINX Tarantool Upstream `parse_query` option must NOT be set.
38+
local uriparts = string.split(req.uri, '?') -- luacheck: ignore
39+
local path_info, query_string = uriparts[1], uriparts[2]
40+
41+
local body = ''
42+
if type(req.body) == 'string' then
43+
body = json.decode(req.body).params
44+
end
45+
46+
local hostport = box.session.peer(box.session.id()) -- luacheck: ignore
47+
local hostport_parts = string.split(hostport, ':') -- luacheck: ignore
48+
local peer_host, peer_port = hostport_parts[1], tonumber(hostport_parts[2])
49+
50+
local env = {
51+
['tsgi.version'] = '1',
52+
['tsgi.url_scheme'] = 'http', -- no support for https
53+
['tsgi.input'] = {
54+
_pos = 0, -- last unread char in body
55+
read = tsgi_input_read,
56+
rewind = tsgi_input_rewind,
57+
},
58+
['tsgi.errors'] = {
59+
write = noop,
60+
flush = noop,
61+
},
62+
['tsgi.hijack'] = nil, -- no support for hijack with nginx
63+
['REQUEST_METHOD'] = string.upper(req.method),
64+
['SERVER_NAME'] = server.host,
65+
['SERVER_PORT'] = server.port,
66+
['PATH_INFO'] = path_info,
67+
['QUERY_STRING'] = query_string,
68+
['SERVER_PROTOCOL'] = req.proto,
69+
[tsgi.KEY_PEER] = {
70+
host = peer_host,
71+
port = peer_port,
72+
family = 'AF_INET',
73+
type = 'SOCK_STREAM',
74+
protocol = 'tcp',
75+
},
76+
77+
[KEY_BODY] = body, -- http body string; used in `tsgi_input_read`
78+
}
79+
80+
-- Pass through `env` to env['tsgi.*']:read() functions
81+
env['tsgi.input']._env = env
82+
env['tsgi.errors']._env = env
83+
84+
for name, value in pairs(req.headers) do
85+
env[convert_headername(name)] = value
86+
end
87+
88+
-- SCRIPT_NAME is a virtual location of your app.
89+
--
90+
-- Imagine you want to serve your HTTP API under prefix /test
91+
-- and later move it to /.
92+
--
93+
-- Instead of rewriting endpoints to your application, you do:
94+
--
95+
-- location /test/ {
96+
-- proxy_pass http://127.0.0.1:8001/test/;
97+
-- proxy_redirect http://127.0.0.1:8001/test/ http://$host/test/;
98+
-- proxy_set_header SCRIPT_NAME /test;
99+
-- }
100+
--
101+
-- Application source code is not touched.
102+
env['SCRIPT_NAME'] = env['HTTP_SCRIPT_NAME'] or ''
103+
env['HTTP_SCRIPT_NAME'] = nil
104+
105+
return env
106+
end
107+
108+
local function generic_entrypoint(server, req, ...) -- luacheck: ignore
109+
local env = make_env(server, req, ...)
110+
111+
local ok, resp = pcall(server.router, env)
112+
113+
local status = resp.status or 200
114+
local headers = resp.headers or {}
115+
local body = resp.body or ''
116+
117+
if not ok then
118+
status = 500
119+
headers = {}
120+
local trace = debug.traceback()
121+
122+
-- TODO: copypaste
123+
-- TODO: env could be changed. we need to save a copy of it
124+
log.error('unhandled error: %s\n%s\nrequest:\n%s',
125+
tostring(resp), trace, tsgi.serialize_request(env))
126+
127+
if server.display_errors then
128+
body =
129+
"Unhandled error: " .. tostring(resp) .. "\n"
130+
.. trace .. "\n\n"
131+
.. "\n\nRequest:\n"
132+
.. tsgi.serialize_request(env)
133+
else
134+
body = "Internal Error"
135+
end
136+
end
137+
138+
-- handle iterable body
139+
local gen, param, state
140+
141+
if type(body) == 'function' then
142+
-- Generating function
143+
gen = body
144+
elseif type(body) == 'table' and body.gen then
145+
-- Iterator
146+
gen, param, state = body.gen, body.param, body.state
147+
end
148+
149+
if gen ~= nil then
150+
body = ''
151+
for _, part in gen, param, state do
152+
body = body .. tostring(part)
153+
end
154+
end
155+
156+
return status, headers, body
157+
end
158+
159+
local function ngxserver_set_router(self, router)
160+
checks('table', 'function') -- luacheck: ignore
161+
162+
self.router = router
163+
end
164+
165+
local function ngxserver_start(self)
166+
checks('table') -- luacheck: ignore
167+
168+
rawset(_G, self.tnt_method, function(...)
169+
return generic_entrypoint(self, ...)
170+
end)
171+
end
172+
173+
local function ngxserver_stop(self)
174+
checks('table') -- luacheck: ignore
175+
176+
rawset(_G, self.tnt_method, nil)
177+
end
178+
179+
local function new(opts)
180+
checks({ -- luacheck: ignore
181+
host = 'string',
182+
port = 'number',
183+
tnt_method = 'string',
184+
display_errors = '?boolean',
185+
log_errors = '?boolean',
186+
log_requests = '?boolean',
187+
})
188+
189+
local self = {
190+
host = opts.host,
191+
port = opts.port,
192+
tnt_method = opts.tnt_method,
193+
display_errors = opts.display_errors or true,
194+
log_errors = opts.log_errors or true,
195+
log_requests = opts.log_requests or true,
196+
197+
set_router = ngxserver_set_router,
198+
start = ngxserver_start,
199+
stop = ngxserver_stop,
200+
}
201+
return self
202+
end
203+
204+
return {
205+
new = new,
206+
}

0 commit comments

Comments
 (0)