Skip to content

Commit

Permalink
Add TSGI Layer Between Router and Server (#81)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ASverdlov authored May 18, 2019
1 parent 3669ac5 commit 1cc26ed
Show file tree
Hide file tree
Showing 16 changed files with 2,054 additions and 1,279 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,14 @@ end
is in the lower case, all headers joined together into a single string.
* `req.peer` - a Lua table with information about the remote peer
(like `socket:peer()`).
**NOTE**: when router is being used with
nginx adapter, `req.peer` contains information on iproto connection with
nginx, not the original HTTP user-agent.
* `tostring(req)` - returns a string representation of the request.
* `req:request_line()` - returns the request body.
* `req:read(delimiter|chunk|{delimiter = x, chunk = x}, timeout)` - reads the
raw request body as a stream (see `socket:read()`).
raw request body as a stream (see `socket:read()`). **NOTE**: when using
NGINX TSGI adapter, only `req:read(chunk)` is available.
* `req:json()` - returns a Lua table from a JSON request.
* `req:post_param(name)` - returns a single POST request a parameter value.
If `name` is `nil`, returns all parameters as a Lua table.
Expand Down
206 changes: 206 additions & 0 deletions http/nginx_server/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
local tsgi = require('http.tsgi')

require('checks')
local json = require('json')
local log = require('log')

local KEY_BODY = 'tsgi.http.nginx_server.body'

local function noop() end

local function convert_headername(name)
return 'HEADER_' .. string.upper(name)
end

local function tsgi_input_read(self, n)
checks('table', '?number') -- luacheck: ignore

local start = self._pos
local last

if n ~= nil then
last = start + n
self._pos = last
else
last = #self._env[KEY_BODY]
self._pos = last
end

return self._env[KEY_BODY]:sub(start, last)
end

local function tsgi_input_rewind(self)
self._pos = 0
end

local function make_env(server, req)
-- NGINX Tarantool Upstream `parse_query` option must NOT be set.
local uriparts = string.split(req.uri, '?') -- luacheck: ignore
local path_info, query_string = uriparts[1], uriparts[2]

local body = ''
if type(req.body) == 'string' then
body = json.decode(req.body).params
end

local hostport = box.session.peer(box.session.id()) -- luacheck: ignore
local hostport_parts = string.split(hostport, ':') -- luacheck: ignore
local peer_host, peer_port = hostport_parts[1], tonumber(hostport_parts[2])

local env = {
['tsgi.version'] = '1',
['tsgi.url_scheme'] = 'http', -- no support for https
['tsgi.input'] = {
_pos = 0, -- last unread char in body
read = tsgi_input_read,
rewind = tsgi_input_rewind,
},
['tsgi.errors'] = {
write = noop,
flush = noop,
},
['tsgi.hijack'] = nil, -- no support for hijack with nginx
['REQUEST_METHOD'] = string.upper(req.method),
['SERVER_NAME'] = server.host,
['SERVER_PORT'] = server.port,
['PATH_INFO'] = path_info,
['QUERY_STRING'] = query_string,
['SERVER_PROTOCOL'] = req.proto,
[tsgi.KEY_PEER] = {
host = peer_host,
port = peer_port,
family = 'AF_INET',
type = 'SOCK_STREAM',
protocol = 'tcp',
},

[KEY_BODY] = body, -- http body string; used in `tsgi_input_read`
}

-- Pass through `env` to env['tsgi.*']:read() functions
env['tsgi.input']._env = env
env['tsgi.errors']._env = env

for name, value in pairs(req.headers) do
env[convert_headername(name)] = value
end

-- SCRIPT_NAME is a virtual location of your app.
--
-- Imagine you want to serve your HTTP API under prefix /test
-- and later move it to /.
--
-- Instead of rewriting endpoints to your application, you do:
--
-- location /test/ {
-- proxy_pass http://127.0.0.1:8001/test/;
-- proxy_redirect http://127.0.0.1:8001/test/ http://$host/test/;
-- proxy_set_header SCRIPT_NAME /test;
-- }
--
-- Application source code is not touched.
env['SCRIPT_NAME'] = env['HTTP_SCRIPT_NAME'] or ''
env['HTTP_SCRIPT_NAME'] = nil

return env
end

local function generic_entrypoint(server, req, ...) -- luacheck: ignore
local env = make_env(server, req, ...)

local ok, resp = pcall(server.router, env)

local status = resp.status or 200
local headers = resp.headers or {}
local body = resp.body or ''

if not ok then
status = 500
headers = {}
local trace = debug.traceback()

-- TODO: copypaste
-- TODO: env could be changed. we need to save a copy of it
log.error('unhandled error: %s\n%s\nrequest:\n%s',
tostring(resp), trace, tsgi.serialize_request(env))

if server.display_errors then
body =
"Unhandled error: " .. tostring(resp) .. "\n"
.. trace .. "\n\n"
.. "\n\nRequest:\n"
.. tsgi.serialize_request(env)
else
body = "Internal Error"
end
end

-- handle iterable body
local gen, param, state

if type(body) == 'function' then
-- Generating function
gen = body
elseif type(body) == 'table' and body.gen then
-- Iterator
gen, param, state = body.gen, body.param, body.state
end

if gen ~= nil then
body = ''
for _, part in gen, param, state do
body = body .. tostring(part)
end
end

return status, headers, body
end

local function ngxserver_set_router(self, router)
checks('table', 'function') -- luacheck: ignore

self.router = router
end

local function ngxserver_start(self)
checks('table') -- luacheck: ignore

rawset(_G, self.tnt_method, function(...)
return generic_entrypoint(self, ...)
end)
end

local function ngxserver_stop(self)
checks('table') -- luacheck: ignore

rawset(_G, self.tnt_method, nil)
end

local function new(opts)
checks({ -- luacheck: ignore
host = 'string',
port = 'number',
tnt_method = 'string',
display_errors = '?boolean',
log_errors = '?boolean',
log_requests = '?boolean',
})

local self = {
host = opts.host,
port = opts.port,
tnt_method = opts.tnt_method,
display_errors = opts.display_errors or true,
log_errors = opts.log_errors or true,
log_requests = opts.log_requests or true,

set_router = ngxserver_set_router,
start = ngxserver_start,
stop = ngxserver_stop,
}
return self
end

return {
new = new,
}
Loading

0 comments on commit 1cc26ed

Please sign in to comment.