|
| 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