Skip to content

Add TSGI Layer Between Router and Server #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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