├── Makefile ├── README.md ├── lib └── resty │ ├── binutil.lua │ ├── fastcgi-http.lua │ └── fastcgi.lua └── t ├── .gitignore ├── 01-sanity.t └── 02-protocol.t /Makefile: -------------------------------------------------------------------------------- 1 | OPENRESTY_PREFIX=/usr/local/openresty 2 | 3 | PREFIX ?= /usr/local 4 | LUA_INCLUDE_DIR ?= $(PREFIX)/include 5 | LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) 6 | INSTALL ?= install 7 | TEST_FILE ?= t 8 | 9 | .PHONY: all test install 10 | 11 | all: ; 12 | 13 | install: all 14 | $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/resty/fastcgi 15 | 16 | test: all 17 | PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r $(TEST_FILE) 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-resty-fcgi 2 | 3 | Lua FastCGI client driver for ngx_lua based on the cosocket API. 4 | 5 | # Table of Contents 6 | 7 | * [Status](#status) 8 | * [Overview](#overview) 9 | * [fastcgi](#fastcgi) 10 | * [new](#new) 11 | * [connect](#connect) 12 | * [request_simple](#request_simple) 13 | * [request](#request) 14 | 15 | # Status 16 | 17 | Experimental, API may change without warning. 18 | 19 | Requires ngx_lua > 0.9.5 20 | 21 | # Overview 22 | 23 | Require the resty.fastcgi module in init_by_lua. 24 | 25 | Create an instance of the `fastcgi` class in your content_by_lua. 26 | 27 | Call the `connect` method with a socket path or hostname:port combination to connect. 28 | 29 | Use the `request_simple` method to make a basic FastCGI request which returns a result object containing http body and headers, or nil, err. 30 | 31 | ```lua 32 | init_by_lua ' 33 | fcgi = require("resty.fastcgi") 34 | '; 35 | 36 | server { 37 | root /var/www; 38 | 39 | location / { 40 | 41 | content_by_lua ' 42 | local fcgic = fcgi.new() 43 | 44 | fcgic:set_timeout(2000) 45 | fcgic:connect("127.0.0.1",9000) 46 | 47 | ngx.req.read_body() 48 | 49 | fcgic:set_timeout(60000) 50 | 51 | local res, err = fcgic:request_simple({ 52 | fastcgi_params = { 53 | DOCUMENT_ROOT = ngx.var.document_root, 54 | SCRIPT_FILENAME = ngx.var.document_root .. "/index.php", 55 | SCRIPT_NAME = "/", 56 | REQUEST_METHOD = ngx.var.request_method, 57 | CONTENT_TYPE = ngx.var.content_type, 58 | CONTENT_LENGTH = ngx.var.content_length, 59 | REQUEST_URI = ngx.var.request_uri, 60 | QUERY_STRING = ngx.var.args, 61 | SERVER_PROTOCOL = ngx.var.server_protocol, 62 | GATEWAY_INTERFACE = "CGI/1.1", 63 | SERVER_SOFTWARE = "lua-resty-fastcgi", 64 | REMOTE_ADDR = ngx.var.remote_addr, 65 | REMOTE_PORT = ngx.var.remote_port, 66 | SERVER_ADDR = ngx.var.server_addr, 67 | SERVER_PORT = ngx.var.server_port, 68 | SERVER_NAME = ngx.var.server_name 69 | }, 70 | headers = ngx.req.get_headers(), 71 | body = ngx.req.get_body_data(), 72 | }) 73 | 74 | if not res then 75 | ngx.status = 500 76 | ngx.log(ngx.ERR,"Error making FCGI request: ",err) 77 | ngx.exit(500) 78 | else 79 | for k,v in pairs(res.headers) do 80 | ngx.header[k] = v 81 | end 82 | ngx.status = res.status 83 | ngx.say(res.body) 84 | end 85 | 86 | fcgic:close() 87 | '; 88 | } 89 | } 90 | 91 | ``` 92 | 93 | # fastcgi 94 | 95 | ### new 96 | `syntax: fcgi_client = fcgi.new()` 97 | 98 | Returns a new fastcgi object. 99 | 100 | ### connect 101 | `syntax: ok, err = fcgi_client:connect(host or sockpath[,port])` 102 | 103 | Attempts to connect to the FastCGI server details given. 104 | 105 | 106 | ```lua 107 | fcgi_class = require('resty.fastcgi') 108 | local fcgi_client = fcgi_class.new() 109 | 110 | local ok, err = fcgi_client:connect("127.0.0.1",9000) 111 | 112 | if not ok then 113 | ngx.log(ngx.ERR, err) 114 | ngx.status = 500 115 | return ngx.exit(ngx.status) 116 | end 117 | 118 | ngx.log(ngx.info, 'Connected to ' .. err.host.host .. ':' .. err.host.port) 119 | ``` 120 | 121 | ### request_simple 122 | `syntax: res, err = fcgi_client:request_simple({params...})` 123 | 124 | Makes a FCGI request to the connected socket using the details given in params. 125 | 126 | Returns a result object containing HTTP body and headers. Internally this uses the streaming API. 127 | 128 | e.g. 129 | ```lua 130 | local params = { 131 | fastcgi_params = { 132 | DOCUMENT_ROOT = ngx.var.document_root, 133 | SCRIPT_FILENAME = ngx.var.document_root .. "/index.php", 134 | SCRIPT_NAME = "/", 135 | REQUEST_METHOD = ngx.var.request_method, 136 | CONTENT_TYPE = ngx.var.content_type, 137 | CONTENT_LENGTH = ngx.var.content_length, 138 | REQUEST_URI = ngx.var.request_uri, 139 | QUERY_STRING = ngx.var.args, 140 | SERVER_PROTOCOL = ngx.var.server_protocol, 141 | GATEWAY_INTERFACE = "CGI/1.1", 142 | SERVER_SOFTWARE = "lua-resty-fastcgi", 143 | REMOTE_ADDR = ngx.var.remote_addr, 144 | REMOTE_PORT = ngx.var.remote_port, 145 | SERVER_ADDR = ngx.var.server_addr, 146 | SERVER_PORT = ngx.var.server_port, 147 | SERVER_NAME = ngx.var.server_name 148 | }, 149 | headers = ngx.req.get_headers(), 150 | body = ngx.req.get_body_data(), 151 | } 152 | 153 | res, err = fcgi_client:request_simple(params) 154 | 155 | if not res then 156 | ngx.log(ngx.ERR, err) 157 | ngx.status = 500 158 | return ngx.exit(ngx.status) 159 | end 160 | 161 | local res_headers = res.headers 162 | local res_body = res.body 163 | ``` 164 | 165 | ### request 166 | `syntax: res, err = fcgi_client:request({params...})` 167 | 168 | Makes a FCGI request to the connected socket using the details given in params. 169 | 170 | Returns number of bytes written to socket or nil, err. This method is intended to be used with the response streaming functions. 171 | 172 | e.g. 173 | ```lua 174 | local params = { 175 | fastcgi_params = { 176 | DOCUMENT_ROOT = ngx.var.document_root, 177 | SCRIPT_FILENAME = ngx.var.document_root .. "/index.php", 178 | SCRIPT_NAME = "/", 179 | REQUEST_METHOD = ngx.var.request_method, 180 | CONTENT_TYPE = ngx.var.content_type, 181 | CONTENT_LENGTH = ngx.var.content_length, 182 | REQUEST_URI = ngx.var.request_uri, 183 | QUERY_STRING = ngx.var.args, 184 | SERVER_PROTOCOL = ngx.var.server_protocol, 185 | GATEWAY_INTERFACE = "CGI/1.1", 186 | SERVER_SOFTWARE = "lua-resty-fastcgi", 187 | REMOTE_ADDR = ngx.var.remote_addr, 188 | REMOTE_PORT = ngx.var.remote_port, 189 | SERVER_ADDR = ngx.var.server_addr, 190 | SERVER_PORT = ngx.var.server_port, 191 | SERVER_NAME = ngx.var.server_name 192 | }, 193 | headers = ngx.req.get_headers(), 194 | body = ngx.req.get_body_data(), 195 | } 196 | 197 | res, err = fcgi_client:request(params) 198 | 199 | if not res then 200 | ngx.log(ngx.ERR, err) 201 | ngx.status = 500 202 | return ngx.exit(ngx.status) 203 | end 204 | 205 | local body_reader = fcgic:get_response_reader() 206 | local chunk, err 207 | repeat 208 | chunk, err = body_reader(32768) 209 | 210 | if err then 211 | return nil, err, tbl_concat(chunks) 212 | end 213 | 214 | if chunk then 215 | -- Parse stdout here for e.g. HTTP headers 216 | ngx.print(chunk.stdout) 217 | end 218 | until not chunk 219 | 220 | ``` 221 | 222 | ## TODO 223 | * Streaming request support 224 | * Better testing, including testing streaming functionality 225 | 226 | -------------------------------------------------------------------------------- /lib/resty/binutil.lua: -------------------------------------------------------------------------------- 1 | local ffi = require 'ffi' 2 | 3 | local str_byte = string.byte 4 | local str_char = string.char 5 | local bit_lshift = bit.lshift 6 | local bit_rshift = bit.rshift 7 | local bit_band = bit.band 8 | local tbl_concat = table.concat 9 | local ipairs = ipairs 10 | local ffi_new = ffi.new 11 | 12 | local _M = {} 13 | 14 | -- Number to binary. Converts a lua number into binary string with bytes (8 max) 15 | function _M.ntob(num,bytes) 16 | bytes = bytes or 1 17 | 18 | local str = "" 19 | 20 | -- Mask high bit 21 | local mask = bit_lshift(0xff,(bytes-1)*8) 22 | 23 | for i=1, bytes do 24 | -- Isolate current bit by anding it with mask, then shift it bytes-i right 25 | -- This puts it into byte '0'. 26 | local val = bit_rshift(bit_band(num,mask),(bytes-i)*8) 27 | -- Pass it to str_char and append to string 28 | str = str .. str_char(val) 29 | -- Shift the mask 1 byte to the left and repeat 30 | mask = bit_rshift(mask,8) 31 | end 32 | return str 33 | end 34 | 35 | 36 | function _M.bton(str) 37 | local num = 0 38 | local bytes = {str_byte(str,1,#str)} 39 | 40 | for _, byte in ipairs(bytes) do 41 | num = bit_lshift(num,8) + bit_band(byte,0xff) 42 | end 43 | return num 44 | end 45 | 46 | return _M -------------------------------------------------------------------------------- /lib/resty/fastcgi-http.lua: -------------------------------------------------------------------------------- 1 | local fcgi = require 'resty.fastcgi' 2 | 3 | local ngx_var = ngx.var 4 | local ngx_re_gsub = ngx.re.gsub 5 | local ngx_re_gmatch = ngx.re.gmatch 6 | local ngx_re_match = ngx.re.match 7 | local ngx_re_find = ngx.re.find 8 | local ngx_log = ngx.log 9 | local ngx_DEBUG = ngx.DEBUG 10 | local ngx_ERR = ngx.ERR 11 | 12 | local ngx_req = ngx.req 13 | local ngx_req_socket = ngx_req.socket 14 | local ngx_req_get_headers = ngx_req.get_headers 15 | 16 | local str_char = string.char 17 | local str_byte = string.byte 18 | local str_rep = string.rep 19 | local str_lower = string.lower 20 | local str_upper = string.upper 21 | local str_sub = string.sub 22 | 23 | 24 | local tbl_concat = table.concat 25 | 26 | local co_yield = coroutine.yield 27 | local co_create = coroutine.create 28 | local co_status = coroutine.status 29 | local co_resume = coroutine.resume 30 | 31 | 32 | local FCGI_HIDE_HEADERS = { 33 | ["Status"] = true, 34 | ["X-Accel-Expires"] = true, 35 | ["X-Accel-Redirect"] = true, 36 | ["X-Accel-Limit-Rate"] = true, 37 | ["X-Accel-Buffering"] = true, 38 | ["X-Accel-Charset"] = true, 39 | } 40 | 41 | -- Reimplemented coroutine.wrap, returning "nil, err" if the coroutine cannot 42 | -- be resumed. This protects user code from inifite loops when doing things like 43 | -- repeat 44 | -- local chunk, err = res.body_reader() 45 | -- if chunk then -- <-- This could be a string msg in the core wrap function. 46 | -- ... 47 | -- end 48 | -- until not chunk 49 | local co_wrap = function(func) 50 | local co = co_create(func) 51 | if not co then 52 | return nil, "could not create coroutine" 53 | else 54 | return function(...) 55 | if co_status(co) == "suspended" then 56 | return select(2, co_resume(co, ...)) 57 | else 58 | return nil, "can't resume a " .. co_status(co) .. " coroutine" 59 | end 60 | end 61 | end 62 | end 63 | 64 | 65 | local _M = { 66 | _VERSION = '0.01', 67 | } 68 | 69 | 70 | local mt = { __index = _M } 71 | 72 | 73 | local function _should_receive_body(method, code) 74 | if method == "HEAD" then return nil end 75 | if code == 204 or code == 304 then return nil end 76 | if code >= 100 and code < 200 then return nil end 77 | return true 78 | end 79 | 80 | 81 | local function _hide_headers(headers) 82 | for _,v in ipairs(FCGI_HIDE_HEADERS) do 83 | headers[v] = nil 84 | end 85 | return headers 86 | end 87 | 88 | 89 | local function _parse_headers(str) 90 | 91 | -- Only look in the first header_buffer_len bytes 92 | local header_buffer_len = 1024 93 | local header_buffer = str_sub(str,1,header_buffer_len) 94 | local found, header_boundary 95 | 96 | -- Find header boundary 97 | found, header_boundary, err = ngx_re_find(header_buffer,"\\r?\\n\\r?\\n","jo") 98 | 99 | -- If we can't find the header boundary then return an error 100 | if not found then 101 | ngx_log(ngx_ERR,"Unable to find end of HTTP header in first ",header_buffer_len," bytes - aborting") 102 | return nil, "Error reading HTTP header" 103 | end 104 | 105 | local http_headers = {} 106 | 107 | for line in ngx_re_gmatch(str_sub(header_buffer,1,header_boundary),"[^\r\n]+","jo") do 108 | for header_pairs in ngx_re_gmatch(line[0], "([\\w\\-]+)\\s*:\\s*(.+)","jo") do 109 | local header_name = header_pairs[1] 110 | local header_value = header_pairs[2] 111 | if not FCGI_HIDE_HEADERS[header_name] then 112 | if http_headers[header_name] then 113 | http_headers[header_name] = http_headers[header_name] .. ", " .. tostring(header_value) 114 | else 115 | http_headers[header_name] = tostring(header_value) 116 | end 117 | end 118 | end 119 | end 120 | 121 | return http_headers, str_sub(str,header_boundary+1) 122 | end 123 | 124 | 125 | local function _chunked_body_reader(sock, default_chunk_size) 126 | return co_wrap(function(max_chunk_size) 127 | local max_chunk_size = max_chunk_size or default_chunk_size 128 | local remaining = 0 129 | local length 130 | 131 | repeat 132 | -- If we still have data on this chunk 133 | if max_chunk_size and remaining > 0 then 134 | 135 | if remaining > max_chunk_size then 136 | -- Consume up to max_chunk_size 137 | length = max_chunk_size 138 | remaining = remaining - max_chunk_size 139 | else 140 | -- Consume all remaining 141 | length = remaining 142 | remaining = 0 143 | end 144 | else -- This is a fresh chunk 145 | 146 | -- Receive the chunk size 147 | local str, err = sock:receive("*l") 148 | if not str then 149 | co_yield(nil, err) 150 | end 151 | 152 | length = tonumber(str, 16) 153 | 154 | if not length then 155 | co_yield(nil, "unable to read chunksize") 156 | end 157 | 158 | if max_chunk_size and length > max_chunk_size then 159 | -- Consume up to max_chunk_size 160 | remaining = length - max_chunk_size 161 | length = max_chunk_size 162 | end 163 | end 164 | 165 | if length > 0 then 166 | local str, err = sock:receive(length) 167 | if not str then 168 | co_yield(nil, err) 169 | end 170 | 171 | max_chunk_size = co_yield(str) or default_chunk_size 172 | 173 | -- If we're finished with this chunk, read the carriage return. 174 | if remaining == 0 then 175 | sock:receive(2) -- read \r\n 176 | end 177 | else 178 | -- Read the last (zero length) chunk's carriage return 179 | sock:receive(2) -- read \r\n 180 | end 181 | 182 | until length == 0 183 | end) 184 | end 185 | 186 | 187 | local function _body_reader(sock, content_length, default_chunk_size) 188 | return co_wrap(function(max_chunk_size) 189 | local max_chunk_size = max_chunk_size or default_chunk_size 190 | 191 | if not content_length and max_chunk_size then 192 | -- We have no length, but wish to stream. 193 | -- HTTP 1.0 with no length will close connection, so read chunks to the end. 194 | repeat 195 | local str, err, partial = sock:receive(max_chunk_size) 196 | if not str and err == "closed" then 197 | max_chunk_size = co_yield(partial, err) or default_chunk_size 198 | end 199 | 200 | max_chunk_size = co_yield(str) or default_chunk_size 201 | until not str 202 | 203 | elseif not content_length then 204 | -- We have no length but don't wish to stream. 205 | -- HTTP 1.0 with no length will close connection, so read to the end. 206 | co_yield(sock:receive("*a")) 207 | 208 | elseif not max_chunk_size then 209 | -- We have a length and potentially keep-alive, but want everything. 210 | co_yield(sock:receive(content_length)) 211 | 212 | else 213 | -- We have a length and potentially a keep-alive, and wish to stream 214 | -- the response. 215 | local received = 0 216 | repeat 217 | local length = max_chunk_size 218 | if received + length > content_length then 219 | length = content_length - received 220 | end 221 | 222 | if length > 0 then 223 | local str, err = sock:receive(length) 224 | if not str then 225 | max_chunk_size = co_yield(nil, err) or default_chunk_size 226 | end 227 | received = received + length 228 | 229 | max_chunk_size = co_yield(str) or default_chunk_size 230 | end 231 | 232 | until length == 0 233 | end 234 | end) 235 | end 236 | 237 | 238 | function _M.new(_) 239 | local self = { 240 | fcgi = fcgi.new(), 241 | stdout_buffer = "", 242 | } 243 | 244 | return setmetatable(self, mt) 245 | end 246 | 247 | 248 | function _M.set_timeout(self, timeout) 249 | local fcgi = self.fcgi 250 | return fcgi.sock:settimeout(timeout) 251 | end 252 | 253 | 254 | function _M.connect(self, ...) 255 | local fcgi = self.fcgi 256 | return fcgi.sock:connect(...) 257 | end 258 | 259 | 260 | function _M.set_keepalive(self, ...) 261 | local fcgi = self.fcgi 262 | return fcgi.sock:setkeepalive(...) 263 | end 264 | 265 | 266 | function _M.get_reused_times(self) 267 | local fcgi = self.fcgi 268 | return fcgi.sock:getreusedtimes() 269 | end 270 | 271 | 272 | function _M.close(self) 273 | local fcgi = self.fcgi 274 | return fcgi.sock:close() 275 | end 276 | 277 | function _M.get_response_reader(self) 278 | local response_reader = self.fcgi:get_response_reader() 279 | 280 | return function(chunk_size) 281 | local chunk_size = chunk_size or 65536 282 | local data, err 283 | local buffer = self.stdout_buffer 284 | -- If we have buffered data then return from the buffer 285 | if buffer then 286 | local return_data 287 | if chunk_size > #buffer then 288 | return_data = buffer 289 | self.stdout_buffer = nil 290 | else 291 | return_data = str_sub(buffer,1,chunk_size) 292 | self.stdout_buffer = str_sub(buffer,chunk_size+1) 293 | end 294 | 295 | return return_data 296 | 297 | -- Otherwise simply return from the fcgi response reader 298 | else 299 | data, err = response_reader(chunk_size) 300 | if data then 301 | if data.stderr ~= nil then 302 | ngx_log(ngx_ERR,"FastCGI Stderr: ",data.stderr) 303 | end 304 | return data.stdout or "" 305 | else 306 | return nil, err 307 | end 308 | end 309 | 310 | end 311 | 312 | end 313 | 314 | 315 | function _M.request(self,params) 316 | local fcgi = self.fcgi 317 | local sock = fcgi.sock 318 | local headers = params.headers or {} 319 | local body = params.body 320 | local user_params = params.fastcgi or {} 321 | 322 | local request_method = user_params.request_method or ngx_var.request_method 323 | local script_name = user_params.script_name or ngx_re_gsub(user_params.request_uri or ngx.var.request_uri, "\\?.*", "","jo") 324 | 325 | -- Set default headers if we can 326 | if type(body) == 'string' and not headers["Content-Length"] then 327 | headers["Content-Length"] = #body 328 | end 329 | if not headers["Host"] then 330 | headers["Host"] = self.host 331 | end 332 | if params.version == 1.0 and not headers["Connection"] then 333 | headers["Connection"] = "Keep-Alive" 334 | end 335 | 336 | local fcgi_params = { 337 | SCRIPT_NAME = script_name, 338 | SCRIPT_FILENAME = user_params.script_filename or "index.php", 339 | DOCUMENT_ROOT = user_params.document_root or ngx_var.document_root, 340 | REQUEST_METHOD = request_method, 341 | CONTENT_TYPE = user_params.content_type or ngx_var.content_type, 342 | CONTENT_LENGTH = headers["Content-Length"] or ngx_var.content_length, 343 | REQUEST_URI = user_params.request_uri or ngx_var.request_uri, 344 | DOCUMENT_URI = script_name, 345 | QUERY_STRING = user_params.args or (ngx_var.args or ""), 346 | SERVER_PROTOCOL = user_params.server_protocol or ngx_var.server_protocol, 347 | GATEWAY_INTERFACE = "CGI/1.1", 348 | SERVER_SOFTWARE = "lua-resty-fastcgi", 349 | REMOTE_ADDR = ngx_var.remote_addr, 350 | REMOTE_PORT = ngx_var.remote_port, 351 | SERVER_ADDR = ngx_var.server_addr, 352 | SERVER_PORT = ngx_var.server_port, 353 | SERVER_NAME = ngx_var.server_name or "host", 354 | } 355 | 356 | local https_var = user_params.https or ngx_var.https 357 | if https_var ~= '' then 358 | fcgi_params['HTTPS'] = https_var 359 | end 360 | 361 | for k,v in pairs(headers) do 362 | local clean_header = ngx_re_gsub(str_upper(k),"-","_","jo") 363 | fcgi_params["HTTP_" .. clean_header] = v 364 | end 365 | 366 | local res, err, chunk 367 | 368 | res, err = fcgi:request({ 369 | params = fcgi_params, 370 | stdin = body, 371 | }) 372 | 373 | if not res then 374 | return nil, err 375 | end 376 | 377 | local body_reader = self:get_response_reader() 378 | local have_http_headers = false 379 | local res = {status = nil, headers = nil, has_body = false, body_reader = body_reader} 380 | 381 | -- Read chunks off the network until we get the first stdout chunk. 382 | -- Buffer remaining stdout data and log any Stderr info to nginx error log 383 | repeat 384 | chunk, err, partial = body_reader() 385 | 386 | if err then 387 | return nil, err 388 | end 389 | 390 | -- We can't have stderr and stdout in the same chunk 391 | if not have_http_headers and #chunk > 0 then 392 | http_headers,remaining_stdout = _parse_headers(chunk) 393 | 394 | if not http_headers then 395 | res.status = 500 396 | return 397 | end 398 | 399 | self.stdout_buffer = tbl_concat({self.stdout_buffer or "",remaining_stdout}) 400 | 401 | res.headers = http_headers 402 | 403 | local status_header = http_headers['Status'] 404 | 405 | -- If FCGI response contained a status header, then assume that status 406 | if status_header then 407 | res.status = tonumber(str_sub(status_header, 1, 3)) 408 | 409 | -- If a HTTP location is given but no HTTP status, this is a redirect 410 | elseif http_headers['Location'] then 411 | res.status = 302 412 | 413 | -- Otherwise assume this request was OK and return 200 414 | else 415 | res.status = 200 416 | end 417 | 418 | res.has_body = _should_receive_body(request_method,res.status) 419 | 420 | return res 421 | end 422 | until not chunk 423 | 424 | return res 425 | end 426 | 427 | function _M.get_client_body_reader(self, chunksize) 428 | local chunksize = chunksize or 65536 429 | local sock, err = ngx_req_socket() 430 | 431 | if not sock then 432 | if err == "no body" then 433 | return nil 434 | else 435 | return nil, err 436 | end 437 | end 438 | 439 | local headers = ngx_req_get_headers() 440 | local length = headers["Content-Length"] 441 | local encoding = headers["Transfer-Encoding"] 442 | if length then 443 | return _body_reader(sock, tonumber(length), chunksize) 444 | elseif str_lower(encoding) == 'chunked' then 445 | -- Not yet supported by ngx_lua but should just work... 446 | return _chunked_body_reader(sock, chunksize) 447 | else 448 | return nil, "Unknown transfer encoding" 449 | end 450 | end 451 | 452 | return _M 453 | -------------------------------------------------------------------------------- /lib/resty/fastcgi.lua: -------------------------------------------------------------------------------- 1 | local binutil = require 'resty.binutil' 2 | local ntob = binutil.ntob 3 | local bton = binutil.bton 4 | 5 | local bit_band = bit.band 6 | 7 | local ngx_socket_tcp = ngx.socket.tcp 8 | local ngx_log = ngx.log 9 | local ngx_DEBUG = ngx.DEBUG 10 | local ngx_ERR = ngx.ERR 11 | 12 | local str_char = string.char 13 | local str_sub = string.sub 14 | 15 | local tbl_concat = table.concat 16 | local pairs = pairs 17 | local ipairs = ipairs 18 | 19 | 20 | local _M = { 21 | _VERSION = '0.01', 22 | } 23 | 24 | local mt = { __index = _M } 25 | 26 | 27 | local FCGI_HEADER_LEN = 0x08 28 | local FCGI_VERSION_1 = 0x01 29 | local FCGI_BEGIN_REQUEST = 0x01 30 | local FCGI_ABORT_REQUEST = 0x02 31 | local FCGI_END_REQUEST = 0x03 32 | local FCGI_PARAMS = 0x04 33 | local FCGI_STDIN = 0x05 34 | local FCGI_STDOUT = 0x06 35 | local FCGI_STDERR = 0x07 36 | local FCGI_DATA = 0x08 37 | local FCGI_GET_VALUES = 0x09 38 | local FCGI_GET_VALUES_RESULT = 0x10 39 | local FCGI_UNKNOWN_TYPE = 0x11 40 | local FCGI_MAXTYPE = 0x11 41 | local FCGI_PARAM_HIGH_BIT = 2147483648 42 | local FCGI_BODY_MAX_LENGTH = 32768 43 | local FCGI_KEEP_CONN = 0x01 44 | local FCGI_NO_KEEP_CONN = 0x00 45 | local FCGI_NULL_REQUEST_ID = 0x00 46 | local FCGI_RESPONDER = 0x01 47 | local FCGI_AUTHORIZER = 0x02 48 | local FCGI_FILTER = 0x03 49 | 50 | 51 | local FCGI_HEADER_FORMAT = { 52 | {"version",1,FCGI_VERSION_1}, 53 | {"type",1,nil}, 54 | {"request_id",2,1}, 55 | {"content_length",2,0}, 56 | {"padding_length",1,0}, 57 | {"reserved",1,0} 58 | } 59 | 60 | 61 | local FCGI_BEGIN_REQ_FORMAT = { 62 | {"role",2,FCGI_RESPONDER}, 63 | {"flags",1,0}, 64 | {"reserved",5,0} 65 | } 66 | 67 | 68 | local FCGI_END_REQ_FORMAT = { 69 | {"status",4,nil}, 70 | {"protocolStatus",1,nil}, 71 | {"reserved",3,nil} 72 | } 73 | 74 | 75 | local FCGI_PADDING_BYTES = { 76 | str_char(0), 77 | str_char(0,0), 78 | str_char(0,0,0), 79 | str_char(0,0,0,0), 80 | str_char(0,0,0,0,0), 81 | str_char(0,0,0,0,0,0), 82 | str_char(0,0,0,0,0,0,0), 83 | } 84 | 85 | 86 | local function _pack(format,params) 87 | local bytes = "" 88 | 89 | for index, field in ipairs(format) do 90 | local fieldname = field[1] 91 | local fieldlength = field[2] 92 | local defaulval = field[3] 93 | 94 | if params[fieldname] == nil then 95 | bytes = bytes .. ntob(defaulval,fieldlength) 96 | else 97 | bytes = bytes .. ntob(params[fieldname],fieldlength) 98 | end 99 | end 100 | 101 | return bytes 102 | end 103 | 104 | 105 | local function _pack_header(params) 106 | local align = 8 107 | params.padding_length = bit_band(-(params.content_length or 0),align - 1) 108 | return _pack(FCGI_HEADER_FORMAT,params), params.padding_length 109 | end 110 | 111 | 112 | local function _unpack(format,str) 113 | -- If we received nil, return nil 114 | if not str then 115 | return nil 116 | end 117 | 118 | local res, idx = {}, 1 119 | 120 | -- Extract bytes based on format. Convert back to number and place in res rable 121 | for _, field in ipairs(format) do 122 | res[field[1]] = bton(str_sub(str,idx,idx + field[2] - 1)) 123 | idx = idx + field[2] 124 | end 125 | 126 | return res 127 | end 128 | 129 | 130 | local FCGI_PREPACKED = { 131 | end_params = _pack_header({ 132 | type = FCGI_PARAMS, 133 | }), 134 | begin_request = _pack_header({ 135 | type = FCGI_BEGIN_REQUEST, 136 | request_id = 1, 137 | content_length = FCGI_HEADER_LEN, 138 | }) .. _pack(FCGI_BEGIN_REQ_FORMAT,{ 139 | role = FCGI_RESPONDER, 140 | flags = 1, 141 | }), 142 | abort_request = _pack_header({ 143 | type = FCGI_ABORT_REQUEST, 144 | }), 145 | empty_stdin = _pack_header({ 146 | type = FCGI_STDIN, 147 | content_length = 0, 148 | }), 149 | } 150 | 151 | 152 | local function _pad(bytes) 153 | if bytes == 0 then 154 | return "" 155 | else 156 | return FCGI_PADDING_BYTES[bytes] 157 | end 158 | end 159 | 160 | 161 | function _M.new(_) 162 | local sock, err = ngx_socket_tcp() 163 | if not sock then 164 | return nil, err 165 | end 166 | 167 | local self = { 168 | sock = sock, 169 | keepalives = false, 170 | } 171 | 172 | return setmetatable(self, mt) 173 | end 174 | 175 | 176 | function _M.set_timeout(self, timeout) 177 | local sock = self.sock 178 | if not sock then 179 | return nil, "not initialized" 180 | end 181 | 182 | return sock:settimeout(timeout) 183 | end 184 | 185 | 186 | function _M.connect(self, ...) 187 | local sock = self.sock 188 | if not sock then 189 | return nil, "not initialized" 190 | end 191 | 192 | self.host = select(1, ...) 193 | 194 | return sock:connect(...) 195 | end 196 | 197 | 198 | function _M.set_keepalive(self, ...) 199 | local sock = self.sock 200 | if not sock then 201 | return nil, "not initialized" 202 | end 203 | 204 | return sock:setkeepalive(...) 205 | end 206 | 207 | 208 | function _M.get_reused_times(self) 209 | local sock = self.sock 210 | if not sock then 211 | return nil, "not initialized" 212 | end 213 | 214 | return sock:getreusedtimes() 215 | end 216 | 217 | 218 | function _M.close(self) 219 | local sock = self.sock 220 | if not sock then 221 | return nil, "not initialized" 222 | end 223 | 224 | return sock:close() 225 | end 226 | 227 | 228 | local function _format_params(params) 229 | local new_params, idx = {}, 1 230 | 231 | -- Iterate over each param 232 | for key,value in pairs(params) do 233 | key = tostring(key) 234 | value = tostring(value) 235 | 236 | local keylen = #key 237 | local valuelen = #value 238 | 239 | -- If length of field is longer than 127, we represent 240 | -- it as 4 bytes with high bit set to 1 (+2147483648 241 | -- or FCGI_PARAM_HIGH_BIT) 242 | 243 | local keylen_b, valuelen_b 244 | 245 | if keylen <= 127 then 246 | keylen_b = ntob(keylen) 247 | else 248 | keylen_b = ntob(keylen + FCGI_PARAM_HIGH_BIT,4) 249 | end 250 | 251 | if valuelen <= 127 then 252 | valuelen_b = ntob(valuelen) 253 | else 254 | valuelen_b = ntob(valuelen + FCGI_PARAM_HIGH_BIT,4) 255 | end 256 | 257 | new_params[idx] = tbl_concat({ 258 | keylen_b, 259 | valuelen_b, 260 | key, 261 | value, 262 | }) 263 | 264 | idx = idx + 1 265 | end 266 | 267 | local new_params_str = tbl_concat(new_params) 268 | 269 | local start_params, padding = _pack_header({ 270 | type = FCGI_PARAMS, 271 | content_length = #new_params_str 272 | }) 273 | 274 | return tbl_concat({ start_params, new_params_str, _pad(padding), FCGI_PREPACKED.end_params }) 275 | end 276 | 277 | 278 | local function _format_stdin(stdin) 279 | local chunk_length 280 | local to_send = {} 281 | local stdin_chunk = {"","",""} 282 | local header = "" 283 | local padding, idx = 0, 1 284 | local stdin_length = #stdin 285 | 286 | -- We could potentially need to send more than one records' worth of data, so 287 | -- loop to format. 288 | repeat 289 | -- While we still have stdin data, build up STDIN record in chunks 290 | if stdin_length > FCGI_BODY_MAX_LENGTH then 291 | chunk_length = FCGI_BODY_MAX_LENGTH 292 | else 293 | chunk_length = stdin_length 294 | end 295 | 296 | header, padding = _pack_header({ 297 | type = FCGI_STDIN, 298 | content_length = chunk_length, 299 | }) 300 | 301 | stdin_chunk[1] = header 302 | stdin_chunk[2] = str_sub(stdin,1,chunk_length) 303 | stdin_chunk[3] = _pad(padding) 304 | 305 | to_send[idx] = tbl_concat(stdin_chunk) 306 | stdin = str_sub(stdin,chunk_length+1) 307 | stdin_length = stdin_length - chunk_length 308 | idx = idx + 1 309 | until stdin_length == 0 310 | 311 | return tbl_concat(to_send) 312 | end 313 | 314 | 315 | local function _send_stdin(sock,stdin) 316 | 317 | local ok, bytes, err, chunk, partial 318 | 319 | if type(stdin) == 'function' then 320 | repeat 321 | chunk, err, partial = stdin(FCGI_BODY_MAX_LENGTH) 322 | 323 | -- If the iterator returns nil, then we have no more stdin 324 | -- Send an empty stdin record to signify the end of the request 325 | if chunk then 326 | ngx_log(ngx_DEBUG,"Request body reader yielded ",#chunk," bytes of data - sending") 327 | ok,err = sock:send(_format_stdin(chunk)) 328 | if not ok then 329 | ngx_log(ngx_DEBUG,"Unable to send ",#chunk," bytes of stdin: ",err) 330 | return nil, err 331 | end 332 | -- Otherwise iterator errored, return 333 | elseif err ~= nil then 334 | ngx_log(ngx_DEBUG,"Request body reader yielded an error: ",err) 335 | return nil, err, partial 336 | end 337 | until chunk == nil 338 | elseif stdin ~= nil then 339 | ngx_log(ngx_DEBUG,"Sending ",#stdin," bytes of read data") 340 | bytes, err = sock:send(_format_stdin(stdin)) 341 | 342 | if not bytes then 343 | return nil, err 344 | end 345 | end 346 | 347 | -- Send empty stdin record to signify end 348 | bytes, err = sock:send(FCGI_PREPACKED.empty_stdin) 349 | 350 | if not bytes then 351 | return nil, err 352 | end 353 | 354 | return true, nil 355 | end 356 | 357 | 358 | function _M.get_response_reader(self) 359 | local sock = self.sock 360 | local record_type = nil 361 | local content_length = 0 362 | local padding_length = 0 363 | 364 | return function(chunk_size) 365 | -- 65536 is the maximum content length of a FCGI record 366 | local chunk_size = chunk_size or 65536 367 | local res = { stdout = nil, stderr = nil} 368 | local data, err, partial, header_bytes, bytes_to_read 369 | 370 | -- If we don't have a length of data to read yet, attempt to read a record header 371 | if not record_type then 372 | ngx_log(ngx_DEBUG,"Attempting to grab next FCGI record") 373 | local header_bytes, err = sock:receive(FCGI_HEADER_LEN) 374 | local header = _unpack(FCGI_HEADER_FORMAT,header_bytes) 375 | 376 | if not header then 377 | return nil, err or "Unable to parse FCGI record header" 378 | end 379 | 380 | record_type = header.type 381 | content_length = header.content_length 382 | padding_length = header.padding_length 383 | 384 | ngx_log(ngx_DEBUG,"New content length is ",content_length," padding ",padding_length) 385 | 386 | -- If we've reached the end of the request, return nil 387 | if record_type == FCGI_END_REQUEST then 388 | ngx_log(ngx_DEBUG,"Attempting to read end request") 389 | read_bytes, err, partial = sock:receive(content_length) 390 | 391 | if not read_bytes or partial then 392 | return nil, err or "Unable to parse FCGI end request body" 393 | end 394 | 395 | return nil -- TODO: Return end request format correctly without breaking 396 | end 397 | end 398 | 399 | -- Calculate maximum readable buffer size 400 | if chunk_size >= content_length then 401 | bytes_to_read = content_length 402 | else 403 | bytes_to_read = chunk_size 404 | end 405 | 406 | -- If we have any bytes to read, read these now 407 | if bytes_to_read > 0 then 408 | data, err, partial = sock:receive(bytes_to_read) 409 | 410 | if not data then 411 | return nil, err or "Unable to retrieve request body", partial 412 | end 413 | 414 | -- Reduce content_length by the amount that we've read so far 415 | content_length = content_length - bytes_to_read 416 | ngx_log(ngx_DEBUG,"Reducing content length by ", bytes_to_read," bytes to ",content_length) 417 | end 418 | 419 | -- Place received data into correct result attribute based on record type 420 | if record_type == FCGI_STDOUT then 421 | res.stdout = data 422 | elseif record_type == FCGI_STDERR then 423 | res.stderr = data 424 | else 425 | return nil, err or "Attempted to receive an unknown FCGI record" 426 | end 427 | 428 | -- If we've read all of the data that we have 'available' in this record, then start again 429 | -- by attempting to parse another record the next time this function is called. 430 | if content_length == 0 then 431 | -- Read and discard padding data 432 | _ = sock:receive(padding_length) 433 | ngx_log(ngx_DEBUG,"Resetting record type") 434 | record_type = nil 435 | end 436 | 437 | return res, nil 438 | end 439 | 440 | end 441 | 442 | 443 | function _M.request(self,parameters) 444 | local sock = self.sock 445 | local stdin = parameters.stdin 446 | local params = parameters.params 447 | 448 | -- Build request 449 | local req = { 450 | FCGI_PREPACKED.begin_request, -- Generate start of request 451 | _format_params(params), -- Generate params (HTTP / FCGI headers) 452 | } 453 | 454 | -- Send request 455 | local bytes_sent, err, partial = sock:send(req) 456 | if not bytes_sent then 457 | return nil, "Failed to send request, " .. err, partial 458 | end 459 | 460 | -- Send body if any 461 | local ok, err, partial = _send_stdin(sock, stdin) 462 | if not ok then 463 | return nil, "Failed to send stdin, " .. (err or "Unkown error"), partial 464 | end 465 | 466 | return true, nil 467 | end 468 | 469 | return _M -------------------------------------------------------------------------------- /t/.gitignore: -------------------------------------------------------------------------------- 1 | servroot/ 2 | -------------------------------------------------------------------------------- /t/01-sanity.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | plan tests => repeat_each() * (8); 7 | 8 | my $pwd = cwd(); 9 | 10 | our $HttpConfig = qq{ 11 | lua_package_path "$pwd/lib/?.lua;;"; 12 | error_log logs/error.log debug; 13 | 14 | init_by_lua ' 15 | fcgi = require "resty.fastcgi" 16 | binutil = require "resty.binutil" 17 | '; 18 | }; 19 | 20 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 21 | 22 | no_long_string(); 23 | #no_diff(); 24 | 25 | run_tests(); 26 | 27 | __DATA__ 28 | === TEST 1: binutil.ntob correctly encodes 1, 2 and 4 byte numbers. 29 | --- http_config eval: $::HttpConfig 30 | --- config 31 | location = /a { 32 | content_by_lua ' 33 | local test = { 34 | {binutil.ntob(41,1),string.char(41)}, 35 | {binutil.ntob(41,2),string.char(0,41)}, 36 | {binutil.ntob(41,4),string.char(0,0,0,41)}, 37 | {binutil.ntob(125,1),string.char(125)}, 38 | {binutil.ntob(65410,2),string.char(255,130)}, 39 | {binutil.ntob(2342349,4),string.char(0,35,189,205)}, 40 | {binutil.ntob(549583953,4),string.char(32,193,252,81)} 41 | } 42 | 43 | for _, pair in ipairs(test) do 44 | if pair[1] ~= pair[2] then 45 | ngx.status = 500 46 | ngx.say("ERR") 47 | ngx.exit(500) 48 | end 49 | end 50 | 51 | ngx.say("OK") 52 | '; 53 | } 54 | --- request 55 | GET /a 56 | --- response_body 57 | OK 58 | --- no_error_log 59 | [error] 60 | [warn] 61 | 62 | === TEST 2: binutil.bton correctly decodes 1, 2 and 4 byte numbers. 63 | --- http_config eval: $::HttpConfig 64 | --- config 65 | location = /a { 66 | content_by_lua ' 67 | local test = { 68 | {binutil.bton(string.char(41)),41}, 69 | {binutil.bton(string.char(0,41)),41}, 70 | {binutil.bton(string.char(0,0,0,41)),41}, 71 | {binutil.bton(string.char(125)),125}, 72 | {binutil.bton(string.char(255,130)),65410}, 73 | {binutil.bton(string.char(0,35,189,205)),2342349}, 74 | {binutil.bton(string.char(32,193,252,81)),549583953}, 75 | } 76 | 77 | for _, pair in ipairs(test) do 78 | if pair[1] ~= pair[2] then 79 | ngx.status = 500 80 | ngx.say("ERR") 81 | ngx.exit(500) 82 | end 83 | end 84 | 85 | ngx.say("OK") 86 | '; 87 | } 88 | --- request 89 | GET /a 90 | --- response_body 91 | OK 92 | --- no_error_log 93 | [error] 94 | [warn] -------------------------------------------------------------------------------- /t/02-protocol.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket; 4 | use Cwd qw(cwd); 5 | 6 | no warnings "experimental::lexical_subs"; 7 | use feature 'lexical_subs'; 8 | 9 | plan tests => repeat_each() * (12); 10 | 11 | my $pwd = cwd(); 12 | 13 | our $HttpConfig = qq{ 14 | lua_package_path "$pwd/lib/?.lua;;"; 15 | error_log logs/error.log debug; 16 | 17 | init_by_lua ' 18 | fcgi = require "resty.fastcgi" 19 | '; 20 | }; 21 | 22 | 23 | our sub pack_padding { 24 | my ($paddinglength) = @_; 25 | return pack("x[$paddinglength]"); 26 | } 27 | 28 | 29 | our sub pack_fcgi_header { 30 | my ($version,$rectype,$reqid,$contentlength,$paddinglength) = @_; 31 | return pack("CCnnCx",$version,$rectype,$reqid,$contentlength,$paddinglength); 32 | } 33 | 34 | 35 | our sub calculate_padding_length { 36 | my ($contentLength) = @_; 37 | my $align = 8; 38 | return (-$contentLength) & ($align - 1); 39 | } 40 | 41 | 42 | our sub pack_fcgi_record { 43 | my($recordType,$content) = @_; 44 | my $contentLength = length $content; 45 | my $paddingLength = calculate_padding_length($contentLength); 46 | 47 | return pack_fcgi_header(1,$recordType,1,$contentLength,$paddingLength) . $content . pack_padding($paddingLength) 48 | } 49 | 50 | 51 | our sub pack_fcgi_begin_request { 52 | my ($role,$flags) = @_; 53 | my $reqBody = pack("nCx[5]",$role,$flags); 54 | return pack_fcgi_record(1,$reqBody); 55 | } 56 | 57 | 58 | our sub pack_fcgi_params { 59 | my(%params) = @_; 60 | my $paramStr = ""; 61 | 62 | while(my ($key, $value) = each %params ) { 63 | my $keylen = length $key; 64 | my $valuelen = length $value; 65 | if($keylen <= 127) { 66 | if($valuelen <= 127) { 67 | $paramStr .= pack("CCA[$keylen]A[$valuelen]",$keylen,$valuelen,$key,$value); 68 | } else { 69 | $paramStr .= pack("CNA[$keylen]A[$valuelen]",$keylen,$valuelen + 2147483648,$key,$value); 70 | } 71 | } else { 72 | if($valuelen <= 127) { 73 | $paramStr .= pack("NCA[$keylen]A[$valuelen]",$keylen + 2147483648,$valuelen,$key,$value); 74 | } else { 75 | $paramStr .= pack("NNA[$keylen]A[$valuelen]",$keylen + 2147483648,$valuelen + 2147483648,$key,$value); 76 | } 77 | } 78 | } 79 | return pack_fcgi_record(4,$paramStr) . pack_fcgi_record(4,"") 80 | } 81 | 82 | 83 | our sub pack_fcgi_stdin { 84 | my ($content) = @_; 85 | my $n = 32768; 86 | my @records = unpack("a$n" x ((length($content)/$n)) . "a*", $content); 87 | 88 | my $out = ""; 89 | foreach my $item (@records) { 90 | $out .= pack_fcgi_record(5,$item); 91 | } 92 | return $out . pack_fcgi_record(5,""); 93 | } 94 | 95 | 96 | our sub pack_fcgi_stdout { 97 | my ($content) = @_; 98 | my $n = 32768; 99 | my @records = unpack("a$n" x ((length($content)/$n)) . "a*", $content); 100 | 101 | my $out = ""; 102 | foreach my $item (@records) { 103 | $out .= pack_fcgi_record(6,$item); 104 | } 105 | return $out . pack_fcgi_record(6,""); 106 | } 107 | 108 | 109 | our sub pack_fcgi_stderr { 110 | my ($content) = @_; 111 | my $n = 32768; 112 | my @records = unpack("a$n" x ((length($content)/$n)) . "a*", $content); 113 | 114 | my $out = ""; 115 | foreach my $item (@records) { 116 | $out .= pack_fcgi_record(7,$item); 117 | } 118 | return $out . pack_fcgi_record(7,""); 119 | } 120 | 121 | 122 | our sub pack_fcgi_end_request { 123 | my ($appStatus,$protoStatus) = @_; 124 | my $reqBody = pack("NCx[3]",$appStatus,$protoStatus); 125 | return pack_fcgi_record(3,$reqBody); 126 | } 127 | 128 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 129 | 130 | no_long_string(); 131 | #no_diff(); 132 | 133 | run_tests(); 134 | 135 | __DATA__ 136 | === TEST 1: Validate short request / response 137 | --- http_config eval: $::HttpConfig 138 | --- config 139 | location = /a { 140 | content_by_lua ' 141 | 142 | local fcgic = fcgi.new() 143 | 144 | fcgic:set_timeout(2000) 145 | fcgic:connect("127.0.0.1",31498) 146 | 147 | fcgic:set_timeout(60000) 148 | 149 | local res, err = fcgic:request({ 150 | params = { 151 | PARAM_1 = "val1", 152 | PARAM_2 = "val2", 153 | }, 154 | stdin = "TEST 1", 155 | }) 156 | 157 | if res then -- We sent the request successfully 158 | local reader = fcgic:get_response_reader() 159 | local stdout, stderr = "","" 160 | 161 | repeat 162 | chunk, err = reader() 163 | 164 | if chunk then 165 | if chunk.stdout then 166 | 167 | stdout = stdout .. chunk.stdout 168 | end 169 | if chunk.stderr then 170 | stderr = stderr .. chunk.stderr 171 | end 172 | elseif err then 173 | ngx.status = 501 174 | ngx.say("ERR") 175 | ngx.exit(501) 176 | end 177 | until not chunk 178 | 179 | fcgic:close() 180 | 181 | if stdout ~= "TEST STDOUT 1" or #stderr > 0 then 182 | ngx.status = 501 183 | ngx.say("ERR") 184 | ngx.exit(501) 185 | else 186 | ngx.say("OK") 187 | ngx.status = 200 188 | end 189 | else 190 | ngx.status = 502 191 | ngx.say("ERR") 192 | ngx.exit(502) 193 | end 194 | 195 | 196 | '; 197 | } 198 | --- request 199 | GET /a 200 | --- response_body 201 | OK 202 | --- tcp_listen: 31498 203 | --- tcp_reply_delay: 1000ms 204 | --- tcp_query_len: 88 205 | --- tcp_reply eval 206 | return ::pack_fcgi_stdout("TEST STDOUT 1") . ::pack_fcgi_end_request(0,0) 207 | --- tcp_query eval 208 | my(%params) = ('PARAM_1' => 'val1','PARAM_2' => 'val2'); 209 | return ::pack_fcgi_begin_request(1,1) . ::pack_fcgi_params(%params) . ::pack_fcgi_stdin("TEST 1") 210 | 211 | 212 | === TEST 2: Validate long request headers 213 | --- http_config eval: $::HttpConfig 214 | --- config 215 | location = /a { 216 | content_by_lua ' 217 | 218 | local fcgic = fcgi.new() 219 | 220 | fcgic:set_timeout(2000) 221 | fcgic:connect("127.0.0.1",31498) 222 | 223 | fcgic:set_timeout(60000) 224 | 225 | local res, err = fcgic:request({ 226 | params = { 227 | FOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBAR = "val1", 228 | val1 = "FOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBAR", 229 | 230 | }, 231 | stdin = "TEST 2", 232 | }) 233 | 234 | if res then -- We sent the request successfully 235 | local reader = fcgic:get_response_reader() 236 | local stdout, stderr = "","" 237 | 238 | repeat 239 | chunk, err = reader() 240 | 241 | if chunk then 242 | if chunk.stdout then 243 | 244 | stdout = stdout .. chunk.stdout 245 | end 246 | if chunk.stderr then 247 | stderr = stderr .. chunk.stderr 248 | end 249 | elseif err then 250 | ngx.status = 501 251 | ngx.say("ERR") 252 | ngx.exit(501) 253 | end 254 | until not chunk 255 | 256 | fcgic:close() 257 | 258 | if stdout ~= "TEST STDOUT 2" or #stderr > 0 then 259 | ngx.status = 501 260 | ngx.say("ERR") 261 | ngx.exit(501) 262 | else 263 | ngx.status = 200 264 | ngx.say("OK") 265 | end 266 | else 267 | ngx.status = 502 268 | ngx.say("ERR") 269 | ngx.exit(502) 270 | end 271 | 272 | 273 | '; 274 | } 275 | --- request 276 | GET /a 277 | --- response_body 278 | OK 279 | --- tcp_listen: 31498 280 | --- tcp_reply_delay: 1000ms 281 | --- tcp_query_len: 344 282 | --- tcp_reply eval 283 | return ::pack_fcgi_stdout("TEST STDOUT 2") . ::pack_fcgi_end_request(0,0) 284 | --- tcp_query eval 285 | my(%params) = ( 286 | 'FOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBAR' => 'val1', 287 | 'val1' => 'FOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBARFOOBAR', 288 | ); 289 | return ::pack_fcgi_begin_request(1,1) . ::pack_fcgi_params(%params) . ::pack_fcgi_stdin("TEST 2") 290 | 291 | 292 | === TEST 3: Validate long request / response body 293 | --- http_config eval: $::HttpConfig 294 | --- config 295 | location = /a { 296 | content_by_lua ' 297 | 298 | local fcgic = fcgi.new() 299 | 300 | fcgic:set_timeout(2000) 301 | fcgic:connect("127.0.0.1",31498) 302 | 303 | fcgic:set_timeout(60000) 304 | 305 | local bodycontent = string.rep("FOOBARRABOOF",6000) 306 | local res, err = fcgic:request({ 307 | params = { 308 | PARAM_1 = "val1", 309 | PARAM_2 = "val2", 310 | }, 311 | stdin = bodycontent, 312 | }) 313 | 314 | if res then -- We sent the request successfully 315 | local reader = fcgic:get_response_reader() 316 | local stdout, stderr = "","" 317 | 318 | repeat 319 | chunk, err = reader() 320 | 321 | if chunk then 322 | if chunk.stdout then 323 | 324 | stdout = stdout .. chunk.stdout 325 | end 326 | if chunk.stderr then 327 | stderr = stderr .. chunk.stderr 328 | end 329 | elseif err then 330 | ngx.status = 501 331 | ngx.say("ERR") 332 | ngx.exit(501) 333 | end 334 | until not chunk 335 | 336 | fcgic:close() 337 | 338 | if stdout ~= bodycontent or #stderr > 0 then 339 | ngx.status = 501 340 | ngx.say("ERR") 341 | ngx.exit(501) 342 | else 343 | ngx.status = 200 344 | ngx.say("OK") 345 | end 346 | else 347 | ngx.status = 502 348 | ngx.say("ERR") 349 | ngx.exit(502) 350 | end 351 | 352 | 353 | '; 354 | } 355 | --- request 356 | GET /a 357 | --- response_body 358 | OK 359 | --- tcp_listen: 31498 360 | --- tcp_reply_delay: 1000ms 361 | --- tcp_query_len: 72096 362 | --- tcp_reply eval 363 | return ::pack_fcgi_stdout("FOOBARRABOOF" x 6000) . ::pack_fcgi_end_request(0,0) 364 | --- tcp_query eval 365 | my(%params) = ('PARAM_1' => 'val1','PARAM_2' => 'val2'); 366 | return ::pack_fcgi_begin_request(1,1) . ::pack_fcgi_params(%params) . ::pack_fcgi_stdin("FOOBARRABOOF" x 6000) 367 | --------------------------------------------------------------------------------