├── .gitattributes ├── .gitignore ├── .travis.yml ├── Makefile ├── README.markdown ├── dist.ini ├── lib └── resty │ └── upload.lua ├── t ├── lib │ └── ljson.lua ├── sanity.t └── version.t └── valgrind.suppress /.gitattributes: -------------------------------------------------------------------------------- 1 | *.t linguist-language=Text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *~ 4 | go 5 | t/servroot/ 6 | reindex 7 | test.html 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: focal 3 | 4 | branches: 5 | only: 6 | - "master" 7 | 8 | os: linux 9 | 10 | language: c 11 | 12 | compiler: 13 | - gcc 14 | 15 | cache: 16 | directories: 17 | - download-cache 18 | 19 | env: 20 | global: 21 | - JOBS=3 22 | - NGX_BUILD_JOBS=$JOBS 23 | - LUAJIT_PREFIX=/opt/luajit21 24 | - LUAJIT_LIB=$LUAJIT_PREFIX/lib 25 | - LUAJIT_INC=$LUAJIT_PREFIX/include/luajit-2.1 26 | - LUA_INCLUDE_DIR=$LUAJIT_INC 27 | - LUA_CMODULE_DIR=/lib 28 | - OPENSSL_PREFIX=/opt/ssl 29 | - OPENSSL_LIB=$OPENSSL_PREFIX/lib 30 | - OPENSSL_INC=$OPENSSL_PREFIX/include 31 | - LD_LIBRARY_PATH=$LUAJIT_LIB:$LD_LIBRARY_PATH 32 | - TEST_NGINX_SLEEP=0.006 33 | matrix: 34 | - NGINX_VERSION=1.27.1 OPENSSL_VER=1.1.1w 35 | 36 | install: 37 | - if [ ! -d download-cache ]; then mkdir download-cache; fi 38 | - if [ ! -f download-cache/openssl-$OPENSSL_VER.tar.gz ]; then wget -O download-cache/openssl-$OPENSSL_VER.tar.gz https://www.openssl.org/source/openssl-$OPENSSL_VER.tar.gz; fi 39 | - sudo apt-get install -qq -y cpanminus axel 40 | - sudo cpanm --notest Test::Nginx > build.log 2>&1 || (cat build.log && exit 1) 41 | - wget http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz 42 | - git clone https://github.com/openresty/openresty.git ../openresty 43 | - git clone https://github.com/openresty/nginx-devel-utils.git 44 | - git clone https://github.com/simpl/ngx_devel_kit.git ../ndk-nginx-module 45 | - git clone https://github.com/openresty/lua-nginx-module.git ../lua-nginx-module 46 | - git clone https://github.com/openresty/lua-resty-core.git ../lua-resty-core 47 | - git clone https://github.com/openresty/lua-resty-lrucache.git ../lua-resty-lrucache 48 | - git clone https://github.com/openresty/no-pool-nginx.git ../no-pool-nginx 49 | - git clone -b v2.1-agentzh https://github.com/openresty/luajit2.git 50 | - git clone https://github.com/openresty/mockeagain.git 51 | 52 | script: 53 | - cd luajit2/ 54 | - make -j$JOBS CCDEBUG=-g Q= PREFIX=$LUAJIT_PREFIX CC=$CC XCFLAGS='-DLUA_USE_APICHECK -DLUA_USE_ASSERT' > build.log 2>&1 || (cat build.log && exit 1) 55 | - sudo make install PREFIX=$LUAJIT_PREFIX > build.log 2>&1 || (cat build.log && exit 1) 56 | - cd .. 57 | - tar zxf download-cache/openssl-$OPENSSL_VER.tar.gz 58 | - cd openssl-$OPENSSL_VER/ 59 | - ./config shared --prefix=$OPENSSL_PREFIX -DPURIFY > build.log 2>&1 || (cat build.log && exit 1) 60 | - make -j$JOBS > build.log 2>&1 || (cat build.log && exit 1) 61 | - sudo make PATH=$PATH install_sw > build.log 2>&1 || (cat build.log && exit 1) 62 | - cd ../mockeagain/ && make CC=$CC -j$JOBS && cd .. 63 | - export PATH=$PWD/work/nginx/sbin:$PWD/nginx-devel-utils:$PATH 64 | - export LD_PRELOAD=$PWD/mockeagain/mockeagain.so 65 | - export LD_LIBRARY_PATH=$PWD/mockeagain:$LD_LIBRARY_PATH 66 | - export TEST_NGINX_RESOLVER=8.8.4.4 67 | - export NGX_BUILD_CC=$CC 68 | - ngx-build $NGINX_VERSION --without-pcre2 --with-ipv6 --with-http_realip_module --with-http_ssl_module --with-cc-opt="-I$OPENSSL_INC" --with-ld-opt="-L$OPENSSL_LIB -Wl,-rpath,$OPENSSL_LIB" --add-module=../ndk-nginx-module --add-module=../lua-nginx-module --with-debug > build.log 2>&1 || (cat build.log && exit 1) 69 | - nginx -V 70 | - ldd `which nginx`|grep -E 'luajit|ssl|pcre' 71 | - prove -I. -r t 72 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OPENRESTY_PREFIX=/usr/local/openresty-debug 2 | 3 | PREFIX ?= /usr/local 4 | LUA_INCLUDE_DIR ?= $(PREFIX)/include 5 | LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION) 6 | INSTALL ?= install 7 | 8 | .PHONY: all test install 9 | 10 | all: ; 11 | 12 | install: all 13 | $(INSTALL) -d $(DESTDIR)/$(LUA_LIB_DIR)/resty 14 | $(INSTALL) lib/resty/*.lua $(DESTDIR)/$(LUA_LIB_DIR)/resty 15 | 16 | test: all 17 | PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r t 18 | 19 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Name 2 | ==== 3 | 4 | lua-resty-upload - Streaming reader and parser for HTTP file uploading based on ngx_lua cosocket 5 | 6 | Table of Contents 7 | ================= 8 | 9 | * [Name](#name) 10 | * [Status](#status) 11 | * [Description](#description) 12 | * [Synopsis](#synopsis) 13 | * [Author](#author) 14 | * [Copyright and License](#copyright-and-license) 15 | * [See Also](#see-also) 16 | 17 | Status 18 | ====== 19 | 20 | This library is considered production ready. 21 | 22 | Description 23 | =========== 24 | 25 | This Lua library is a streaming file uploading API for the ngx_lua nginx module: 26 | 27 | http://wiki.nginx.org/HttpLuaModule 28 | 29 | The multipart/form-data MIME type is supported. 30 | 31 | The API of this library just returns tokens one by one. The user just needs to call the `read` method repeatedly until a nil token type is returned. For each token returned from the `read` method, just check the first return value for the current token type. The token type can be `header`, `body`, and `part end`. Each `multipart/form-data` form field parsed consists of several `header` tokens holding each field header, several `body` tokens holding each body data chunk, and a `part end` flag indicating the field end. 32 | 33 | This is how streaming reading works. Even for giga bytes of file data input, the memory used in the lua land can be small and constant, as long as the user does not accumulate the input data chunks herself. 34 | 35 | This Lua library takes advantage of ngx_lua's cosocket API, which ensures 36 | 100% nonblocking behavior. 37 | 38 | Note that at least [ngx_lua 0.7.9](https://github.com/chaoslawful/lua-nginx-module/tags) or [OpenResty 1.2.4.14](http://openresty.org/#Download) is required. 39 | 40 | Synopsis 41 | ======== 42 | 43 | ```lua 44 | lua_package_path "/path/to/lua-resty-upload/lib/?.lua;;"; 45 | 46 | server { 47 | location /test { 48 | content_by_lua ' 49 | local upload = require "resty.upload" 50 | local cjson = require "cjson" 51 | 52 | local chunk_size = 5 -- should be set to 4096 or 8192 53 | -- for real-world settings 54 | 55 | local form, err = upload:new(chunk_size) 56 | if not form then 57 | ngx.log(ngx.ERR, "failed to new upload: ", err) 58 | ngx.exit(500) 59 | end 60 | 61 | form:set_timeout(1000) -- 1 sec 62 | 63 | while true do 64 | local typ, res, err = form:read() 65 | if not typ then 66 | ngx.say("failed to read: ", err) 67 | return 68 | end 69 | 70 | ngx.say("read: ", cjson.encode({typ, res})) 71 | 72 | if typ == "eof" then 73 | break 74 | end 75 | end 76 | 77 | local typ, res, err = form:read() 78 | ngx.say("read: ", cjson.encode({typ, res})) 79 | '; 80 | } 81 | } 82 | ``` 83 | 84 | A typical output of the /test location defined above is: 85 | 86 | read: ["header",["Content-Disposition","form-data; name=\"file1\"; filename=\"a.txt\"","Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""]] 87 | read: ["header",["Content-Type","text\/plain","Content-Type: text\/plain"]] 88 | read: ["body","Hello"] 89 | read: ["body",", wor"] 90 | read: ["body","ld"] 91 | read: ["part_end"] 92 | read: ["header",["Content-Disposition","form-data; name=\"test\"","Content-Disposition: form-data; name=\"test\""]] 93 | read: ["body","value"] 94 | read: ["body","\r\n"] 95 | read: ["part_end"] 96 | read: ["eof"] 97 | read: ["eof"] 98 | 99 | You can use the [lua-resty-string](https://github.com/agentzh/lua-resty-string) library to compute SHA-1 and MD5 digest of the file data incrementally. Here is such an example: 100 | 101 | ```lua 102 | local resty_sha1 = require "resty.sha1" 103 | local upload = require "resty.upload" 104 | 105 | local chunk_size = 4096 106 | local form = upload:new(chunk_size) 107 | local sha1 = resty_sha1:new() 108 | local file 109 | while true do 110 | local typ, res, err = form:read() 111 | 112 | if not typ then 113 | ngx.say("failed to read: ", err) 114 | return 115 | end 116 | 117 | if typ == "header" then 118 | local file_name = my_get_file_name(res) 119 | if file_name then 120 | file = io.open(file_name, "w+") 121 | if not file then 122 | ngx.say("failed to open file ", file_name) 123 | return 124 | end 125 | end 126 | 127 | elseif typ == "body" then 128 | if file then 129 | file:write(res) 130 | sha1:update(res) 131 | end 132 | 133 | elseif typ == "part_end" then 134 | file:close() 135 | file = nil 136 | local sha1_sum = sha1:final() 137 | sha1:reset() 138 | my_save_sha1_sum(sha1_sum) 139 | 140 | elseif typ == "eof" then 141 | break 142 | 143 | else 144 | -- do nothing 145 | end 146 | end 147 | ``` 148 | 149 | If you want to compute MD5 sums for the uploaded files, just use the 150 | resty.md5 module shipped by the [lua-resty-string](https://github.com/agentzh/lua-resty-string) library. It has 151 | a similar API as resty.sha1. 152 | 153 | For big file uploading, it is important not to buffer all the data in memory. 154 | That is, you should never accumulate data chunks either in a huge Lua string or 155 | in a huge Lua table. You must write the data chunk into files as soon as possible and 156 | throw away the data chunk immediately (to let the Lua GC free it up). 157 | 158 | Instead of writing the data chunk into files (as shown in the example above), 159 | you can also write the data chunks to upstream cosocket connections if you do 160 | not want to save the data on local file systems. 161 | 162 | [Back to TOC](#table-of-contents) 163 | 164 | Usage 165 | ===== 166 | 167 | ```lua 168 | local upload = require "resty.upload" 169 | local form, err = upload:new(self, chunk_size, max_line_size, preserve_body) 170 | ``` 171 | `chunk_size` defaults to 4096. It is the size used to read data from the socket. 172 | 173 | `max_line_size` defaults to 512. It is the size limit to read the chunked body header. 174 | 175 | By Default, `lua-resty-upload` will consume the request body. For proxy mode this means upstream will not see the body. When `preserve_body` is set to true, the request body will be preserved. Note that this option is not free. When enabled, it will double the memory usage of `resty.upload`. 176 | 177 | Author 178 | ====== 179 | 180 | Yichun "agentzh" Zhang (章亦春) , OpenResty Inc. 181 | 182 | [Back to TOC](#table-of-contents) 183 | 184 | Copyright and License 185 | ===================== 186 | 187 | This module is licensed under the BSD license. 188 | 189 | Copyright (C) 2012-2017, by Yichun "agentzh" Zhang (章亦春) , OpenResty Inc. 190 | 191 | All rights reserved. 192 | 193 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 194 | 195 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 196 | 197 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 198 | 199 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 200 | 201 | [Back to TOC](#table-of-contents) 202 | 203 | See Also 204 | ======== 205 | * the [ngx_lua module](http://wiki.nginx.org/HttpLuaModule) 206 | * the [lua-resty-string](https://github.com/agentzh/lua-resty-string) library 207 | * the [lua-resty-memcached](https://github.com/agentzh/lua-resty-memcached) library 208 | * the [lua-resty-redis](https://github.com/agentzh/lua-resty-redis) library 209 | * the [lua-resty-mysql](https://github.com/agentzh/lua-resty-mysql) library 210 | 211 | [Back to TOC](#table-of-contents) 212 | 213 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name=lua-resty-upload 2 | abstract=Streaming reader and parser for HTTP file uploading based on ngx_lua cosocket 3 | author=Yichun "agentzh" Zhang (agentzh) 4 | is_original=yes 5 | license=2bsd 6 | lib_dir=lib 7 | doc_dir=lib 8 | repo_link=https://github.com/openresty/lua-resty-upload 9 | main_module=lib/resty/upload.lua 10 | -------------------------------------------------------------------------------- /lib/resty/upload.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) Yichun Zhang (agentzh) 2 | 3 | 4 | -- local sub = string.sub 5 | local req_socket = ngx.req.socket 6 | local match = string.match 7 | local setmetatable = setmetatable 8 | local type = type 9 | local ngx_var = ngx.var 10 | local ngx_init_body = ngx.req.init_body 11 | local ngx_finish_body = ngx.req.finish_body 12 | local ngx_append_body = ngx.req.append_body 13 | -- local print = print 14 | 15 | 16 | local _M = { _VERSION = '0.11' } 17 | 18 | 19 | local CHUNK_SIZE = 4096 20 | local MAX_LINE_SIZE = 512 21 | 22 | local STATE_BEGIN = 1 23 | local STATE_READING_HEADER = 2 24 | local STATE_READING_BODY = 3 25 | local STATE_EOF = 4 26 | 27 | local mt = { __index = _M } 28 | 29 | local state_handlers 30 | 31 | local function wrapped_receiveuntil(self, until_str) 32 | local iter, err_outer = self:old_receiveuntil(until_str) 33 | if iter == nil then 34 | ngx_finish_body() 35 | end 36 | 37 | local function wrapped(size) 38 | local ret, err = iter(size) 39 | if ret then 40 | ngx_append_body(ret) 41 | end 42 | 43 | -- non-nil ret for call with no size or successful size call and nil ret 44 | if (not size and ret) or (size and not ret and not err) then 45 | ngx_append_body(until_str) 46 | end 47 | return ret, err 48 | end 49 | 50 | return wrapped, err_outer 51 | end 52 | 53 | 54 | local function wrapped_receive(self, arg) 55 | local ret, err, partial = self:old_receive(arg) 56 | if ret then 57 | ngx_append_body(ret) 58 | 59 | elseif partial then 60 | ngx_append_body(partial) 61 | end 62 | 63 | if ret == nil then 64 | ngx_finish_body() 65 | end 66 | 67 | return ret, err 68 | end 69 | 70 | 71 | local function req_socket_body_collector(sock) 72 | sock.old_receiveuntil = sock.receiveuntil 73 | sock.old_receive = sock.receive 74 | sock.receiveuntil = wrapped_receiveuntil 75 | sock.receive = wrapped_receive 76 | end 77 | 78 | 79 | local function get_boundary() 80 | local header = ngx_var.content_type 81 | if not header then 82 | return nil 83 | end 84 | 85 | if type(header) == "table" then 86 | header = header[1] 87 | end 88 | 89 | local m = match(header, ";%s*boundary=\"([^\"]+)\"") 90 | if m then 91 | return m 92 | end 93 | 94 | return match(header, ";%s*boundary=([^\",;]+)") 95 | end 96 | 97 | 98 | function _M.new(self, chunk_size, max_line_size, preserve_body) 99 | local boundary = get_boundary() 100 | 101 | -- print("boundary: ", boundary) 102 | 103 | if not boundary then 104 | return nil, "no boundary defined in Content-Type" 105 | end 106 | 107 | -- print('boundary: "', boundary, '"') 108 | 109 | local sock, err = req_socket() 110 | if not sock then 111 | return nil, err 112 | end 113 | 114 | if preserve_body then 115 | ngx_init_body(chunk_size) 116 | req_socket_body_collector(sock) 117 | end 118 | 119 | local read2boundary, err = sock:receiveuntil("--" .. boundary) 120 | if not read2boundary then 121 | return nil, err 122 | end 123 | 124 | local read_line, err = sock:receiveuntil("\r\n") 125 | if not read_line then 126 | return nil, err 127 | end 128 | 129 | return setmetatable({ 130 | sock = sock, 131 | size = chunk_size or CHUNK_SIZE, 132 | line_size = max_line_size or MAX_LINE_SIZE, 133 | read2boundary = read2boundary, 134 | read_line = read_line, 135 | boundary = boundary, 136 | state = STATE_BEGIN, 137 | preserve_body = preserve_body 138 | }, mt) 139 | end 140 | 141 | 142 | function _M.set_timeout(self, timeout) 143 | local sock = self.sock 144 | if not sock then 145 | return nil, "not initialized" 146 | end 147 | 148 | return sock:settimeout(timeout) 149 | end 150 | 151 | 152 | local function discard_line(self) 153 | local read_line = self.read_line 154 | 155 | local line, err = read_line(self.line_size) 156 | if not line then 157 | return nil, err 158 | end 159 | 160 | local dummy, err = read_line(1) 161 | if dummy then 162 | if self.preserve_body then 163 | ngx_finish_body() 164 | end 165 | 166 | return nil, "line too long: " .. line .. dummy .. "..." 167 | end 168 | 169 | if err then 170 | return nil, err 171 | end 172 | 173 | return 1 174 | end 175 | 176 | 177 | local function discard_rest(self) 178 | local sock = self.sock 179 | local size = self.size 180 | 181 | while true do 182 | local dummy, err = sock:receive(size) 183 | if err and err ~= 'closed' then 184 | return nil, err 185 | end 186 | 187 | if not dummy then 188 | return 1 189 | end 190 | end 191 | end 192 | 193 | 194 | local function read_body_part(self) 195 | local read2boundary = self.read2boundary 196 | 197 | local chunk, err = read2boundary(self.size) 198 | if err then 199 | return nil, nil, err 200 | end 201 | 202 | if not chunk then 203 | local sock = self.sock 204 | 205 | local data = sock:receive(2) 206 | if data == "--" then 207 | local ok, err = discard_rest(self) 208 | if not ok then 209 | return nil, nil, err 210 | end 211 | 212 | self.state = STATE_EOF 213 | return "part_end" 214 | end 215 | 216 | if data ~= "\r\n" then 217 | local ok, err = discard_line(self) 218 | if not ok then 219 | return nil, nil, err 220 | end 221 | end 222 | 223 | self.state = STATE_READING_HEADER 224 | return "part_end" 225 | end 226 | 227 | return "body", chunk 228 | end 229 | 230 | 231 | local function read_header(self) 232 | local read_line = self.read_line 233 | 234 | local line, err = read_line(self.line_size) 235 | if err then 236 | return nil, nil, err 237 | end 238 | 239 | local dummy, err = read_line(1) 240 | if dummy then 241 | if self.preserve_body then 242 | ngx_finish_body() 243 | end 244 | 245 | return nil, nil, "line too long: " .. line .. dummy .. "..." 246 | end 247 | 248 | if err then 249 | return nil, nil, err 250 | end 251 | 252 | -- print("read line: ", line) 253 | 254 | if line == "" then 255 | -- after the last header 256 | self.state = STATE_READING_BODY 257 | return read_body_part(self) 258 | end 259 | 260 | local key, value = match(line, "([^: \t]+)%s*:%s*(.+)") 261 | if not key then 262 | return 'header', line 263 | end 264 | 265 | return 'header', {key, value, line} 266 | end 267 | 268 | 269 | local function eof() 270 | return "eof", nil 271 | end 272 | 273 | 274 | function _M.read(self) 275 | -- local size = self.size 276 | 277 | local handler = state_handlers[self.state] 278 | if handler then 279 | return handler(self) 280 | end 281 | 282 | return nil, nil, "bad state: " .. self.state 283 | end 284 | 285 | 286 | local function read_preamble(self) 287 | local sock = self.sock 288 | if not sock then 289 | return nil, nil, "not initialized" 290 | end 291 | 292 | local size = self.size 293 | local read2boundary = self.read2boundary 294 | 295 | while true do 296 | local preamble = read2boundary(size) 297 | if not preamble then 298 | break 299 | end 300 | 301 | -- discard the preamble data chunk 302 | -- print("read preamble: ", preamble) 303 | end 304 | 305 | local ok, err = discard_line(self) 306 | if not ok then 307 | return nil, nil, err 308 | end 309 | 310 | local read2boundary, err = sock:receiveuntil("\r\n--" .. self.boundary) 311 | if not read2boundary then 312 | return nil, nil, err 313 | end 314 | 315 | self.read2boundary = read2boundary 316 | 317 | self.state = STATE_READING_HEADER 318 | return read_header(self) 319 | end 320 | 321 | 322 | state_handlers = { 323 | read_preamble, 324 | read_header, 325 | read_body_part, 326 | eof 327 | } 328 | 329 | 330 | return _M 331 | -------------------------------------------------------------------------------- /t/lib/ljson.lua: -------------------------------------------------------------------------------- 1 | local ngx_null = ngx.null 2 | local tostring = tostring 3 | local byte = string.byte 4 | local gsub = string.gsub 5 | local sort = table.sort 6 | local pairs = pairs 7 | local ipairs = ipairs 8 | local concat = table.concat 9 | 10 | local ok, new_tab = pcall(require, "table.new") 11 | if not ok then 12 | new_tab = function (narr, nrec) return {} end 13 | end 14 | 15 | local _M = {} 16 | 17 | local metachars = { 18 | ['\t'] = '\\t', 19 | ["\\"] = "\\\\", 20 | ['"'] = '\\"', 21 | ['\r'] = '\\r', 22 | ['\n'] = '\\n', 23 | } 24 | 25 | local function encode_str(s) 26 | -- XXX we will rewrite this when string.buffer is implemented 27 | -- in LuaJIT 2.1 because string.gsub cannot be JIT compiled. 28 | return gsub(s, '["\\\r\n\t]', metachars) 29 | end 30 | 31 | local function is_arr(t) 32 | local exp = 1 33 | for k, _ in pairs(t) do 34 | if k ~= exp then 35 | return nil 36 | end 37 | exp = exp + 1 38 | end 39 | return exp - 1 40 | end 41 | 42 | local encode 43 | 44 | encode = function (v) 45 | if v == nil or v == ngx_null then 46 | return "null" 47 | end 48 | 49 | local typ = type(v) 50 | if typ == 'string' then 51 | return '"' .. encode_str(v) .. '"' 52 | end 53 | 54 | if typ == 'number' or typ == 'boolean' then 55 | return tostring(v) 56 | end 57 | 58 | if typ == 'table' then 59 | local n = is_arr(v) 60 | if n then 61 | local bits = new_tab(n, 0) 62 | for i, elem in ipairs(v) do 63 | bits[i] = encode(elem) 64 | end 65 | return "[" .. concat(bits, ",") .. "]" 66 | end 67 | 68 | local keys = {} 69 | local i = 0 70 | for key, _ in pairs(v) do 71 | i = i + 1 72 | keys[i] = key 73 | end 74 | sort(keys) 75 | 76 | local bits = new_tab(0, i) 77 | i = 0 78 | for _, key in ipairs(keys) do 79 | i = i + 1 80 | bits[i] = encode(key) .. ":" .. encode(v[key]) 81 | end 82 | return "{" .. concat(bits, ",") .. "}" 83 | end 84 | 85 | return '"<' .. typ .. '>"' 86 | end 87 | _M.encode = encode 88 | 89 | return _M 90 | -------------------------------------------------------------------------------- /t/sanity.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket::Lua; 4 | use Cwd qw(cwd); 5 | 6 | repeat_each(2); 7 | 8 | plan tests => repeat_each() * (3 * blocks()); 9 | 10 | my $pwd = cwd(); 11 | 12 | our $HttpConfig = qq{ 13 | lua_package_path "$pwd/lib/?.lua;$pwd/t/lib/?.lua;;"; 14 | lua_package_cpath "/usr/local/openresty-debug/lualib/?.so;/usr/local/openresty/lualib/?.so;;"; 15 | }; 16 | 17 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 18 | 19 | no_long_string(); 20 | #no_diff(); 21 | 22 | run_tests(); 23 | 24 | __DATA__ 25 | 26 | === TEST 1: basic 27 | --- http_config eval: $::HttpConfig 28 | --- config 29 | location /t { 30 | content_by_lua ' 31 | local upload = require "resty.upload" 32 | local ljson = require "ljson" 33 | 34 | local form = upload:new(5) 35 | 36 | form:set_timeout(1000) -- 1 sec 37 | 38 | while true do 39 | local typ, res, err = form:read() 40 | if not typ then 41 | ngx.say("failed to read: ", err) 42 | return 43 | end 44 | 45 | ngx.say("read: ", ljson.encode({typ, res})) 46 | 47 | if typ == "eof" then 48 | break 49 | end 50 | end 51 | 52 | local typ, res, err = form:read() 53 | ngx.say("read: ", ljson.encode({typ, res})) 54 | '; 55 | } 56 | --- more_headers 57 | Content-Type: multipart/form-data; boundary=---------------------------820127721219505131303151179 58 | --- request eval 59 | qq{POST /t\n-----------------------------820127721219505131303151179\r 60 | Content-Disposition: form-data; name="file1"; filename="a.txt"\r 61 | Content-Type: text/plain\r 62 | \r 63 | Hello, world\r\n-----------------------------820127721219505131303151179\r 64 | Content-Disposition: form-data; name="test"\r 65 | \r 66 | value\r 67 | \r\n-----------------------------820127721219505131303151179--\r 68 | } 69 | --- response_body 70 | read: ["header",["Content-Disposition","form-data; name=\"file1\"; filename=\"a.txt\"","Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""]] 71 | read: ["header",["Content-Type","text/plain","Content-Type: text/plain"]] 72 | read: ["body","Hello"] 73 | read: ["body",", wor"] 74 | read: ["body","ld"] 75 | read: ["part_end"] 76 | read: ["header",["Content-Disposition","form-data; name=\"test\"","Content-Disposition: form-data; name=\"test\""]] 77 | read: ["body","value"] 78 | read: ["body","\r\n"] 79 | read: ["part_end"] 80 | read: ["eof"] 81 | read: ["eof"] 82 | --- no_error_log 83 | [error] 84 | 85 | 86 | 87 | === TEST 2: in-part header line too long 88 | --- http_config eval: $::HttpConfig 89 | --- config 90 | location /t { 91 | content_by_lua ' 92 | local upload = require "resty.upload" 93 | local ljson = require "ljson" 94 | 95 | local form = upload:new(5) 96 | 97 | form:set_timeout(1000) -- 1 sec 98 | 99 | while true do 100 | local typ, res, err = form:read() 101 | if not typ then 102 | ngx.say("failed to read: ", err) 103 | return 104 | end 105 | 106 | ngx.say("read: ", ljson.encode({typ, res})) 107 | 108 | if typ == "eof" then 109 | break 110 | end 111 | end 112 | 113 | local typ, res, err = form:read() 114 | ngx.say("read: ", ljson.encode({typ, res})) 115 | '; 116 | } 117 | --- more_headers 118 | Content-Type: multipart/form-data; boundary=---------------------------820127721219505131303151179 119 | --- request eval 120 | qq{POST /t\n-----------------------------820127721219505131303151179\r 121 | Content-Disposition: form-data; name="file1"; filename="a.txt"\r 122 | Content-Type: text/plain\r 123 | } . ("Hello, world" x 64) . qq{\r\n-----------------------------820127721219505131303151179\r 124 | Content-Disposition: form-data; name="test"\r 125 | \r 126 | value\r 127 | \r\n-----------------------------820127721219505131303151179--\r 128 | } 129 | --- response_body 130 | read: ["header",["Content-Disposition","form-data; name=\"file1\"; filename=\"a.txt\"","Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""]] 131 | read: ["header",["Content-Type","text/plain","Content-Type: text/plain"]] 132 | failed to read: line too long: Hello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, wo... 133 | --- no_error_log 134 | [error] 135 | 136 | 137 | 138 | === TEST 3: terminate line too long 139 | --- http_config eval: $::HttpConfig 140 | --- config 141 | location /t { 142 | content_by_lua ' 143 | local upload = require "resty.upload" 144 | local ljson = require "ljson" 145 | 146 | local form = upload:new(5) 147 | 148 | form:set_timeout(1000) -- 1 sec 149 | 150 | while true do 151 | local typ, res, err = form:read() 152 | if not typ then 153 | ngx.say("failed to read: ", err) 154 | return 155 | end 156 | 157 | ngx.say("read: ", ljson.encode({typ, res})) 158 | 159 | if typ == "eof" then 160 | break 161 | end 162 | end 163 | 164 | local typ, res, err = form:read() 165 | ngx.say("read: ", ljson.encode({typ, res})) 166 | '; 167 | } 168 | --- more_headers 169 | Content-Type: multipart/form-data; boundary=---------------------------820127721219505131303151179 170 | --- request eval 171 | qq{POST /t\n-----------------------------820127721219505131303151179\r 172 | Content-Disposition: form-data; name="file1"; filename="a.txt"\r 173 | Content-Type: text/plain\r 174 | \r 175 | Hello, world\r\n-----------------------------820127721219505131303151179\r 176 | Content-Disposition: form-data; name="test"\r 177 | \r 178 | value\r 179 | \r\n-----------------------------820127721219505131303151179} . ("a" x 1024) . qq{--\r 180 | } 181 | --- response_body 182 | read: ["header",["Content-Disposition","form-data; name=\"file1\"; filename=\"a.txt\"","Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""]] 183 | read: ["header",["Content-Type","text/plain","Content-Type: text/plain"]] 184 | read: ["body","Hello"] 185 | read: ["body",", wor"] 186 | read: ["body","ld"] 187 | read: ["part_end"] 188 | read: ["header",["Content-Disposition","form-data; name=\"test\"","Content-Disposition: form-data; name=\"test\""]] 189 | read: ["body","value"] 190 | read: ["body","\r\n"] 191 | failed to read: line too long: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... 192 | --- no_error_log 193 | [error] 194 | 195 | 196 | 197 | === TEST 4: example from RFC 1521 198 | --- http_config eval: $::HttpConfig 199 | --- config 200 | location /t { 201 | content_by_lua ' 202 | local upload = require "resty.upload" 203 | local ljson = require "ljson" 204 | 205 | local form = upload:new(20) 206 | 207 | form:set_timeout(1000) -- 1 sec 208 | 209 | while true do 210 | local typ, res, err = form:read() 211 | if not typ then 212 | ngx.say("failed to read: ", err) 213 | return 214 | end 215 | 216 | ngx.say("read: ", ljson.encode({typ, res})) 217 | 218 | if typ == "eof" then 219 | break 220 | end 221 | end 222 | 223 | local typ, res, err = form:read() 224 | ngx.say("read: ", ljson.encode({typ, res})) 225 | '; 226 | } 227 | --- more_headers 228 | content-TYPE: multipart/form-data; boundary="simple boundary" 229 | --- request eval 230 | qq{POST /t 231 | This is the preamble. It is to be ignored, though it 232 | is a handy place for mail composers to include an 233 | explanatory note to non-MIME conformant readers. 234 | --simple boundary\r 235 | \r 236 | This is implicitly typed plain ASCII text. 237 | It does NOT end with a linebreak.\r 238 | --simple boundary\r 239 | Content-type: text/plain; charset=us-ascii\r 240 | \r 241 | This is explicitly typed plain ASCII text. 242 | It DOES end with a linebreak. 243 | \r 244 | --simple boundary--\r 245 | This is the epilogue. It is also to be ignored. 246 | 247 | } 248 | --- response_body 249 | read: ["body","This is implicitly t"] 250 | read: ["body","yped plain ASCII tex"] 251 | read: ["body","t.\nIt does NOT end w"] 252 | read: ["body","ith a linebreak."] 253 | read: ["part_end"] 254 | read: ["header",["Content-type","text/plain; charset=us-ascii","Content-type: text/plain; charset=us-ascii"]] 255 | read: ["body","This is explicitly t"] 256 | read: ["body","yped plain ASCII tex"] 257 | read: ["body","t.\nIt DOES end with "] 258 | read: ["body","a linebreak.\n"] 259 | read: ["part_end"] 260 | read: ["eof"] 261 | read: ["eof"] 262 | --- no_error_log 263 | [error] 264 | 265 | 266 | 267 | === TEST 5: example from RFC 1521, no double quotes for the boundary value in the Content-Type response header 268 | --- http_config eval: $::HttpConfig 269 | --- config 270 | location /t { 271 | content_by_lua ' 272 | local upload = require "resty.upload" 273 | local ljson = require "ljson" 274 | 275 | local form = upload:new(20) 276 | 277 | form:set_timeout(1000) -- 1 sec 278 | 279 | while true do 280 | local typ, res, err = form:read() 281 | if not typ then 282 | ngx.say("failed to read: ", err) 283 | return 284 | end 285 | 286 | ngx.say("read: ", ljson.encode({typ, res})) 287 | 288 | if typ == "eof" then 289 | break 290 | end 291 | end 292 | 293 | local typ, res, err = form:read() 294 | ngx.say("read: ", ljson.encode({typ, res})) 295 | '; 296 | } 297 | --- more_headers 298 | Content-Type: multipart/form-data; boundary=simple boundary 299 | --- request eval 300 | qq{POST /t 301 | This is the preamble. It is to be ignored, though it 302 | is a handy place for mail composers to include an 303 | explanatory note to non-MIME conformant readers. 304 | --simple boundary\r 305 | \r 306 | This is implicitly typed plain ASCII text. 307 | It does NOT end with a linebreak.\r 308 | --simple boundary\r 309 | Content-type: text/plain; charset=us-ascii\r 310 | \r 311 | This is explicitly typed plain ASCII text. 312 | It DOES end with a linebreak. 313 | \r 314 | --simple boundary--\r 315 | This is the epilogue. It is also to be ignored. 316 | 317 | } 318 | --- response_body 319 | read: ["body","This is implicitly t"] 320 | read: ["body","yped plain ASCII tex"] 321 | read: ["body","t.\nIt does NOT end w"] 322 | read: ["body","ith a linebreak."] 323 | read: ["part_end"] 324 | read: ["header",["Content-type","text/plain; charset=us-ascii","Content-type: text/plain; charset=us-ascii"]] 325 | read: ["body","This is explicitly t"] 326 | read: ["body","yped plain ASCII tex"] 327 | read: ["body","t.\nIt DOES end with "] 328 | read: ["body","a linebreak.\n"] 329 | read: ["part_end"] 330 | read: ["eof"] 331 | read: ["eof"] 332 | --- no_error_log 333 | [error] 334 | 335 | 336 | 337 | === TEST 6: example from RFC 1521, using the default chunk size 338 | --- http_config eval: $::HttpConfig 339 | --- config 340 | location /t { 341 | content_by_lua ' 342 | local upload = require "resty.upload" 343 | local ljson = require "ljson" 344 | 345 | local form = upload:new() 346 | 347 | form:set_timeout(1000) -- 1 sec 348 | 349 | while true do 350 | local typ, res, err = form:read() 351 | if not typ then 352 | ngx.say("failed to read: ", err) 353 | return 354 | end 355 | 356 | ngx.say("read: ", ljson.encode({typ, res})) 357 | 358 | if typ == "eof" then 359 | break 360 | end 361 | end 362 | 363 | local typ, res, err = form:read() 364 | ngx.say("read: ", ljson.encode({typ, res})) 365 | '; 366 | } 367 | --- more_headers 368 | Content-Type: multipart/form-data; boundary=simple boundary 369 | --- request eval 370 | qq{POST /t 371 | This is the preamble. It is to be ignored, though it 372 | is a handy place for mail composers to include an 373 | explanatory note to non-MIME conformant readers. 374 | --simple boundary\r 375 | \r 376 | This is implicitly typed plain ASCII text. 377 | It does NOT end with a linebreak.\r 378 | --simple boundary\r 379 | Content-type: text/plain; charset=us-ascii\r 380 | \r 381 | This is explicitly typed plain ASCII text. 382 | It DOES end with a linebreak. 383 | \r 384 | --simple boundary--\r 385 | This is the epilogue. It is also to be ignored. 386 | 387 | } 388 | 389 | --- response_body 390 | read: ["body","This is implicitly typed plain ASCII text.\nIt does NOT end with a linebreak."] 391 | read: ["part_end"] 392 | read: ["header",["Content-type","text/plain; charset=us-ascii","Content-type: text/plain; charset=us-ascii"]] 393 | read: ["body","This is explicitly typed plain ASCII text.\nIt DOES end with a linebreak.\n"] 394 | read: ["part_end"] 395 | read: ["eof"] 396 | read: ["eof"] 397 | --- no_error_log 398 | [error] 399 | 400 | 401 | 402 | === TEST 7: github issue #2: cannot parse boundary - no space before parameter (w/o quotes) 403 | --- http_config eval: $::HttpConfig 404 | --- config 405 | location /t { 406 | content_by_lua ' 407 | local upload = require "resty.upload" 408 | local ljson = require "ljson" 409 | 410 | local form, err = upload:new(5) 411 | if not form then 412 | ngx.say("cannot get form: ", err) 413 | return 414 | end 415 | 416 | form:set_timeout(1000) -- 1 sec 417 | 418 | while true do 419 | local typ, res, err = form:read() 420 | if not typ then 421 | ngx.say("failed to read: ", err) 422 | return 423 | end 424 | 425 | ngx.say("read: ", ljson.encode({typ, res})) 426 | 427 | if typ == "eof" then 428 | break 429 | end 430 | end 431 | 432 | local typ, res, err = form:read() 433 | ngx.say("read: ", ljson.encode({typ, res})) 434 | '; 435 | } 436 | --- more_headers 437 | Content-Type: multipart/form-data;boundary=---------------------------820127721219505131303151179 438 | --- request eval 439 | qq{POST /t\n-----------------------------820127721219505131303151179\r 440 | Content-Disposition: form-data; name="file1"; filename="a.txt"\r 441 | Content-Type: text/plain\r 442 | \r 443 | Hello, world\r\n-----------------------------820127721219505131303151179\r 444 | Content-Disposition: form-data; name="test"\r 445 | \r 446 | value\r 447 | \r\n-----------------------------820127721219505131303151179--\r 448 | } 449 | --- response_body 450 | read: ["header",["Content-Disposition","form-data; name=\"file1\"; filename=\"a.txt\"","Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""]] 451 | read: ["header",["Content-Type","text/plain","Content-Type: text/plain"]] 452 | read: ["body","Hello"] 453 | read: ["body",", wor"] 454 | read: ["body","ld"] 455 | read: ["part_end"] 456 | read: ["header",["Content-Disposition","form-data; name=\"test\"","Content-Disposition: form-data; name=\"test\""]] 457 | read: ["body","value"] 458 | read: ["body","\r\n"] 459 | read: ["part_end"] 460 | read: ["eof"] 461 | read: ["eof"] 462 | --- no_error_log 463 | [error] 464 | 465 | 466 | 467 | === TEST 8: github issue #2: cannot parse boundary - no space before parameter (with quotes) 468 | --- http_config eval: $::HttpConfig 469 | --- config 470 | location /t { 471 | content_by_lua ' 472 | local upload = require "resty.upload" 473 | local ljson = require "ljson" 474 | 475 | local form, err = upload:new(5) 476 | if not form then 477 | ngx.say("cannot get form: ", err) 478 | return 479 | end 480 | 481 | form:set_timeout(1000) -- 1 sec 482 | 483 | while true do 484 | local typ, res, err = form:read() 485 | if not typ then 486 | ngx.say("failed to read: ", err) 487 | return 488 | end 489 | 490 | ngx.say("read: ", ljson.encode({typ, res})) 491 | 492 | if typ == "eof" then 493 | break 494 | end 495 | end 496 | 497 | local typ, res, err = form:read() 498 | ngx.say("read: ", ljson.encode({typ, res})) 499 | '; 500 | } 501 | --- more_headers 502 | Content-Type: multipart/form-data;boundary="---------------------------820127721219505131303151179" 503 | --- request eval 504 | qq{POST /t\n-----------------------------820127721219505131303151179\r 505 | Content-Disposition: form-data; name="file1"; filename="a.txt"\r 506 | Content-Type: text/plain\r 507 | \r 508 | Hello, world\r\n-----------------------------820127721219505131303151179\r 509 | Content-Disposition: form-data; name="test"\r 510 | \r 511 | value\r 512 | \r\n-----------------------------820127721219505131303151179--\r 513 | } 514 | --- response_body 515 | read: ["header",["Content-Disposition","form-data; name=\"file1\"; filename=\"a.txt\"","Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""]] 516 | read: ["header",["Content-Type","text/plain","Content-Type: text/plain"]] 517 | read: ["body","Hello"] 518 | read: ["body",", wor"] 519 | read: ["body","ld"] 520 | read: ["part_end"] 521 | read: ["header",["Content-Disposition","form-data; name=\"test\"","Content-Disposition: form-data; name=\"test\""]] 522 | read: ["body","value"] 523 | read: ["body","\r\n"] 524 | read: ["part_end"] 525 | read: ["eof"] 526 | read: ["eof"] 527 | --- no_error_log 528 | [error] 529 | 530 | 531 | 532 | === TEST 9: multiple Content-Type headers 533 | --- http_config eval: $::HttpConfig 534 | --- config 535 | location /t { 536 | content_by_lua ' 537 | local upload = require "resty.upload" 538 | local ljson = require "ljson" 539 | 540 | local form = upload:new(5) 541 | 542 | form:set_timeout(1000) -- 1 sec 543 | 544 | while true do 545 | local typ, res, err = form:read() 546 | if not typ then 547 | ngx.say("failed to read: ", err) 548 | return 549 | end 550 | 551 | ngx.say("read: ", ljson.encode({typ, res})) 552 | 553 | if typ == "eof" then 554 | break 555 | end 556 | end 557 | 558 | local typ, res, err = form:read() 559 | ngx.say("read: ", ljson.encode({typ, res})) 560 | '; 561 | } 562 | --- more_headers 563 | Content-Type: multipart/form-data; boundary=---------------------------820127721219505131303151179 564 | Content-Type: multipart/form-data; boundary=---------------------------820127721219505131303151179 565 | 566 | --- request eval 567 | qq{POST /t\n-----------------------------820127721219505131303151179\r 568 | Content-Disposition: form-data; name="file1"; filename="a.txt"\r 569 | Content-Type: text/plain\r 570 | \r 571 | Hello, world\r\n-----------------------------820127721219505131303151179\r 572 | Content-Disposition: form-data; name="test"\r 573 | \r 574 | value\r 575 | \r\n-----------------------------820127721219505131303151179--\r 576 | } 577 | --- response_body 578 | read: ["header",["Content-Disposition","form-data; name=\"file1\"; filename=\"a.txt\"","Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""]] 579 | read: ["header",["Content-Type","text/plain","Content-Type: text/plain"]] 580 | read: ["body","Hello"] 581 | read: ["body",", wor"] 582 | read: ["body","ld"] 583 | read: ["part_end"] 584 | read: ["header",["Content-Disposition","form-data; name=\"test\"","Content-Disposition: form-data; name=\"test\""]] 585 | read: ["body","value"] 586 | read: ["body","\r\n"] 587 | read: ["part_end"] 588 | read: ["eof"] 589 | read: ["eof"] 590 | --- no_error_log 591 | [error] 592 | 593 | 594 | 595 | === TEST 10: long in-part header line 596 | --- http_config eval: $::HttpConfig 597 | --- config 598 | location /t { 599 | content_by_lua ' 600 | local upload = require "resty.upload" 601 | local ljson = require "ljson" 602 | 603 | local form = upload:new(5, 1024) -- max_line_size = 1024 604 | 605 | form:set_timeout(1000) -- 1 sec 606 | 607 | while true do 608 | local typ, res, err = form:read() 609 | if not typ then 610 | ngx.say("failed to read: ", err) 611 | return 612 | end 613 | 614 | ngx.say("read: ", ljson.encode({typ, res})) 615 | 616 | if typ == "eof" then 617 | break 618 | end 619 | end 620 | 621 | local typ, res, err = form:read() 622 | ngx.say("read: ", ljson.encode({typ, res})) 623 | '; 624 | } 625 | --- more_headers 626 | Content-Type: multipart/form-data; boundary=---------------------------820127721219505131303151179 627 | --- request eval 628 | qq{POST /t\n-----------------------------820127721219505131303151179\r 629 | Content-Disposition: form-data; name="file1"; filename="a.txt"\r 630 | Content-Type: text/plain\r 631 | } . ("Hello, world" x 64) . qq{\r\n-----------------------------820127721219505131303151179\r 632 | Content-Disposition: form-data; name="test"\r 633 | \r 634 | value\r 635 | \r\n-----------------------------820127721219505131303151179--\r 636 | } 637 | --- response_body 638 | read: ["header",["Content-Disposition","form-data; name=\"file1\"; filename=\"a.txt\"","Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""]] 639 | read: ["header",["Content-Type","text/plain","Content-Type: text/plain"]] 640 | read: ["header","Hello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, worldHello, world"] 641 | read: ["header","-----------------------------820127721219505131303151179"] 642 | read: ["header",["Content-Disposition","form-data; name=\"test\"","Content-Disposition: form-data; name=\"test\""]] 643 | read: ["body","value"] 644 | read: ["body","\r\n"] 645 | read: ["part_end"] 646 | read: ["eof"] 647 | read: ["eof"] 648 | --- no_error_log 649 | [error] 650 | 651 | 652 | 653 | === TEST 11: body preserve off 654 | --- http_config eval: $::HttpConfig 655 | --- config 656 | location /t { 657 | content_by_lua ' 658 | local upload = require "resty.upload" 659 | local ljson = require "ljson" 660 | 661 | local form = upload:new(5) 662 | 663 | form:set_timeout(1000) -- 1 sec 664 | 665 | while true do 666 | local typ, res, err = form:read() 667 | if not typ then 668 | ngx.say("failed to read: ", err) 669 | return 670 | end 671 | 672 | ngx.say("read: ", ljson.encode({typ, res})) 673 | 674 | if typ == "eof" then 675 | break 676 | end 677 | end 678 | 679 | local typ, res, err = form:read() 680 | ngx.say("read: ", ljson.encode({typ, res})) 681 | 682 | ngx.say("remain body: ", ngx.req.get_body_data(), ",", ngx.req.get_body_file()) 683 | '; 684 | } 685 | --- more_headers 686 | Content-Type: multipart/form-data; boundary=---------------------------820127721219505131303151179 687 | --- request eval 688 | qq{POST /t\n-----------------------------820127721219505131303151179\r 689 | Content-Disposition: form-data; name="file1"; filename="a.txt"\r 690 | Content-Type: text/plain\r 691 | \r 692 | Hello, world\r\n-----------------------------820127721219505131303151179\r 693 | Content-Disposition: form-data; name="test"\r 694 | \r 695 | value\r 696 | \r\n-----------------------------820127721219505131303151179--\r 697 | } 698 | --- response_body 699 | read: ["header",["Content-Disposition","form-data; name=\"file1\"; filename=\"a.txt\"","Content-Disposition: form-data; name=\"file1\"; filename=\"a.txt\""]] 700 | read: ["header",["Content-Type","text/plain","Content-Type: text/plain"]] 701 | read: ["body","Hello"] 702 | read: ["body",", wor"] 703 | read: ["body","ld"] 704 | read: ["part_end"] 705 | read: ["header",["Content-Disposition","form-data; name=\"test\"","Content-Disposition: form-data; name=\"test\""]] 706 | read: ["body","value"] 707 | read: ["body","\r\n"] 708 | read: ["part_end"] 709 | read: ["eof"] 710 | read: ["eof"] 711 | remain body: nil,nil 712 | --- no_error_log 713 | [error] 714 | 715 | 716 | 717 | === TEST 12: body preserve on 718 | --- http_config eval: $::HttpConfig 719 | --- config 720 | location /t { 721 | content_by_lua ' 722 | local original_len = ngx.req.get_headers()["Content-Length"] 723 | local upload = require "resty.upload" 724 | 725 | local form = upload:new(5, nil, true) 726 | 727 | form:set_timeout(1000) -- 1 sec 728 | 729 | while true do 730 | local typ, res, err = form:read() 731 | if not typ then 732 | ngx.say("failed to read: ", err) 733 | return 734 | end 735 | 736 | if typ == "eof" then 737 | break 738 | end 739 | end 740 | 741 | local typ, res, err = form:read() 742 | 743 | local body = ngx.req.get_body_data() 744 | local new_len 745 | if body then 746 | new_len = #body 747 | else 748 | new_len = io.open(ngx.req.get_body_file(), "r"):seek("end") 749 | end 750 | ngx.say("content_length: ", original_len) 751 | ngx.say("remain body length: ", new_len) 752 | '; 753 | } 754 | --- more_headers 755 | Content-Type: multipart/form-data; boundary=---------------------------820127721219505131303151179 756 | --- request eval 757 | qq{POST /t\n-----------------------------820127721219505131303151179\r 758 | Content-Disposition: form-data; name="file1"; filename="a.txt"\r 759 | Content-Type: text/plain\r 760 | \r 761 | Hello, world\r\n-----------------------------820127721219505131303151179\r 762 | Content-Disposition: form-data; name="test"\r 763 | \r 764 | value\r 765 | \r\n-----------------------------820127721219505131303151179--\r 766 | } 767 | --- response_body 768 | content_length: 336 769 | remain body length: 336 770 | 771 | --- no_error_log 772 | [error] 773 | -------------------------------------------------------------------------------- /t/version.t: -------------------------------------------------------------------------------- 1 | # vim:set ft= ts=4 sw=4 et: 2 | 3 | use Test::Nginx::Socket::Lua; 4 | use Cwd qw(cwd); 5 | 6 | repeat_each(2); 7 | 8 | plan tests => repeat_each() * (3 * blocks()); 9 | 10 | my $pwd = cwd(); 11 | 12 | our $HttpConfig = qq{ 13 | lua_package_path "$pwd/lib/?.lua;;"; 14 | }; 15 | 16 | $ENV{TEST_NGINX_RESOLVER} = '8.8.8.8'; 17 | 18 | no_long_string(); 19 | #no_diff(); 20 | 21 | run_tests(); 22 | 23 | __DATA__ 24 | 25 | === TEST 1: basic 26 | --- http_config eval: $::HttpConfig 27 | --- config 28 | location /t { 29 | content_by_lua ' 30 | local upload = require "resty.upload" 31 | ngx.say(upload._VERSION) 32 | '; 33 | } 34 | --- request 35 | GET /t 36 | --- response_body_like chop 37 | ^\d+\.\d+$ 38 | --- no_error_log 39 | [error] 40 | 41 | -------------------------------------------------------------------------------- /valgrind.suppress: -------------------------------------------------------------------------------- 1 | { 2 | 3 | Memcheck:Param 4 | write(buf) 5 | fun:__write_nocancel 6 | fun:ngx_log_error_core 7 | fun:ngx_resolver_read_response 8 | } 9 | { 10 | 11 | Memcheck:Cond 12 | fun:ngx_sprintf_num 13 | fun:ngx_vslprintf 14 | fun:ngx_log_error_core 15 | fun:ngx_resolver_read_response 16 | fun:ngx_epoll_process_events 17 | fun:ngx_process_events_and_timers 18 | fun:ngx_single_process_cycle 19 | fun:main 20 | } 21 | { 22 | 23 | Memcheck:Addr1 24 | fun:ngx_vslprintf 25 | fun:ngx_snprintf 26 | fun:ngx_sock_ntop 27 | fun:ngx_event_accept 28 | } 29 | { 30 | 31 | Memcheck:Param 32 | write(buf) 33 | fun:__write_nocancel 34 | fun:ngx_log_error_core 35 | fun:ngx_resolver_read_response 36 | fun:ngx_event_process_posted 37 | fun:ngx_process_events_and_timers 38 | fun:ngx_single_process_cycle 39 | fun:main 40 | } 41 | { 42 | 43 | Memcheck:Cond 44 | fun:ngx_sprintf_num 45 | fun:ngx_vslprintf 46 | fun:ngx_log_error_core 47 | fun:ngx_resolver_read_response 48 | fun:ngx_event_process_posted 49 | fun:ngx_process_events_and_timers 50 | fun:ngx_single_process_cycle 51 | fun:main 52 | } 53 | { 54 | 55 | Memcheck:Leak 56 | fun:malloc 57 | fun:ngx_alloc 58 | obj:* 59 | } 60 | { 61 | 62 | exp-sgcheck:SorG 63 | fun:ngx_http_lua_ndk_set_var_get 64 | } 65 | { 66 | 67 | exp-sgcheck:SorG 68 | fun:ngx_http_variables_init_vars 69 | fun:ngx_http_block 70 | } 71 | { 72 | 73 | exp-sgcheck:SorG 74 | fun:ngx_conf_parse 75 | } 76 | { 77 | 78 | exp-sgcheck:SorG 79 | fun:ngx_vslprintf 80 | fun:ngx_log_error_core 81 | } 82 | { 83 | 84 | Memcheck:Leak 85 | fun:malloc 86 | fun:ngx_alloc 87 | fun:ngx_calloc 88 | fun:ngx_event_process_init 89 | } 90 | { 91 | 92 | Memcheck:Param 93 | epoll_ctl(event) 94 | fun:epoll_ctl 95 | } 96 | { 97 | 98 | Memcheck:Leak 99 | fun:malloc 100 | fun:ngx_alloc 101 | fun:ngx_event_process_init 102 | } 103 | { 104 | 105 | Memcheck:Cond 106 | fun:ngx_conf_flush_files 107 | fun:ngx_single_process_cycle 108 | } 109 | { 110 | 111 | Memcheck:Cond 112 | fun:memcpy 113 | fun:ngx_vslprintf 114 | fun:ngx_log_error_core 115 | fun:ngx_http_charset_header_filter 116 | } 117 | { 118 | 119 | Memcheck:Param 120 | socketcall.setsockopt(optval) 121 | fun:setsockopt 122 | fun:drizzle_state_connect 123 | } 124 | { 125 | 126 | Memcheck:Leak 127 | fun:malloc 128 | fun:ngx_alloc 129 | fun:ngx_pool_cleanup_add 130 | } 131 | { 132 | 133 | Memcheck:Cond 134 | fun:ngx_conf_flush_files 135 | fun:ngx_single_process_cycle 136 | fun:main 137 | } 138 | { 139 | 140 | Memcheck:Leak 141 | fun:malloc 142 | fun:ngx_alloc 143 | fun:ngx_palloc_large 144 | fun:ngx_palloc 145 | fun:ngx_array_push 146 | fun:ngx_http_get_variable_index 147 | fun:ngx_http_memc_add_variable 148 | fun:ngx_http_memc_init 149 | fun:ngx_http_block 150 | fun:ngx_conf_parse 151 | fun:ngx_init_cycle 152 | fun:main 153 | } 154 | { 155 | 156 | Memcheck:Leak 157 | fun:malloc 158 | fun:ngx_alloc 159 | fun:ngx_event_process_init 160 | fun:ngx_single_process_cycle 161 | fun:main 162 | } 163 | { 164 | 165 | Memcheck:Leak 166 | fun:malloc 167 | fun:ngx_alloc 168 | fun:ngx_crc32_table_init 169 | fun:main 170 | } 171 | { 172 | 173 | Memcheck:Leak 174 | fun:malloc 175 | fun:ngx_alloc 176 | fun:ngx_event_process_init 177 | fun:ngx_worker_process_init 178 | fun:ngx_worker_process_cycle 179 | fun:ngx_spawn_process 180 | fun:ngx_start_worker_processes 181 | fun:ngx_master_process_cycle 182 | fun:main 183 | } 184 | { 185 | 186 | Memcheck:Leak 187 | fun:malloc 188 | fun:ngx_alloc 189 | fun:ngx_palloc_large 190 | fun:ngx_palloc 191 | fun:ngx_pcalloc 192 | fun:ngx_hash_init 193 | fun:ngx_http_variables_init_vars 194 | fun:ngx_http_block 195 | fun:ngx_conf_parse 196 | fun:ngx_init_cycle 197 | fun:main 198 | } 199 | { 200 | 201 | Memcheck:Leak 202 | fun:malloc 203 | fun:ngx_alloc 204 | fun:ngx_palloc_large 205 | fun:ngx_palloc 206 | fun:ngx_pcalloc 207 | fun:ngx_http_upstream_drizzle_create_srv_conf 208 | fun:ngx_http_upstream 209 | fun:ngx_conf_parse 210 | fun:ngx_http_block 211 | fun:ngx_conf_parse 212 | fun:ngx_init_cycle 213 | fun:main 214 | } 215 | { 216 | 217 | Memcheck:Leak 218 | fun:malloc 219 | fun:ngx_alloc 220 | fun:ngx_palloc_large 221 | fun:ngx_palloc 222 | fun:ngx_pcalloc 223 | fun:ngx_hash_keys_array_init 224 | fun:ngx_http_variables_add_core_vars 225 | fun:ngx_http_core_preconfiguration 226 | fun:ngx_http_block 227 | fun:ngx_conf_parse 228 | fun:ngx_init_cycle 229 | fun:main 230 | } 231 | { 232 | 233 | Memcheck:Leak 234 | fun:malloc 235 | fun:ngx_alloc 236 | fun:ngx_palloc_large 237 | fun:ngx_palloc 238 | fun:ngx_array_push 239 | fun:ngx_hash_add_key 240 | fun:ngx_http_add_variable 241 | fun:ngx_http_echo_add_variables 242 | fun:ngx_http_echo_handler_init 243 | fun:ngx_http_block 244 | fun:ngx_conf_parse 245 | fun:ngx_init_cycle 246 | } 247 | { 248 | 249 | Memcheck:Leak 250 | fun:malloc 251 | fun:ngx_alloc 252 | fun:ngx_palloc_large 253 | fun:ngx_palloc 254 | fun:ngx_pcalloc 255 | fun:ngx_http_upstream_drizzle_create_srv_conf 256 | fun:ngx_http_core_server 257 | fun:ngx_conf_parse 258 | fun:ngx_http_block 259 | fun:ngx_conf_parse 260 | fun:ngx_init_cycle 261 | fun:main 262 | } 263 | { 264 | 265 | Memcheck:Leak 266 | fun:malloc 267 | fun:ngx_alloc 268 | fun:ngx_palloc_large 269 | fun:ngx_palloc 270 | fun:ngx_pcalloc 271 | fun:ngx_http_upstream_drizzle_create_srv_conf 272 | fun:ngx_http_block 273 | fun:ngx_conf_parse 274 | fun:ngx_init_cycle 275 | fun:main 276 | } 277 | { 278 | 279 | Memcheck:Leak 280 | fun:malloc 281 | fun:ngx_alloc 282 | fun:ngx_palloc_large 283 | fun:ngx_palloc 284 | fun:ngx_array_push 285 | fun:ngx_hash_add_key 286 | fun:ngx_http_variables_add_core_vars 287 | fun:ngx_http_core_preconfiguration 288 | fun:ngx_http_block 289 | fun:ngx_conf_parse 290 | fun:ngx_init_cycle 291 | fun:main 292 | } 293 | { 294 | 295 | Memcheck:Leak 296 | fun:malloc 297 | fun:ngx_alloc 298 | fun:ngx_palloc_large 299 | fun:ngx_palloc 300 | fun:ngx_pcalloc 301 | fun:ngx_init_cycle 302 | fun:main 303 | } 304 | { 305 | 306 | Memcheck:Leak 307 | fun:malloc 308 | fun:ngx_alloc 309 | fun:ngx_palloc_large 310 | fun:ngx_palloc 311 | fun:ngx_hash_init 312 | fun:ngx_http_upstream_init_main_conf 313 | fun:ngx_http_block 314 | fun:ngx_conf_parse 315 | fun:ngx_init_cycle 316 | fun:main 317 | } 318 | { 319 | 320 | Memcheck:Leak 321 | fun:malloc 322 | fun:ngx_alloc 323 | fun:ngx_palloc_large 324 | fun:ngx_palloc 325 | fun:ngx_pcalloc 326 | fun:ngx_http_drizzle_keepalive_init 327 | fun:ngx_http_upstream_drizzle_init 328 | fun:ngx_http_upstream_init_main_conf 329 | fun:ngx_http_block 330 | fun:ngx_conf_parse 331 | fun:ngx_init_cycle 332 | fun:main 333 | } 334 | { 335 | 336 | Memcheck:Leak 337 | fun:malloc 338 | fun:ngx_alloc 339 | fun:ngx_palloc_large 340 | fun:ngx_palloc 341 | fun:ngx_hash_init 342 | fun:ngx_http_variables_init_vars 343 | fun:ngx_http_block 344 | fun:ngx_conf_parse 345 | fun:ngx_init_cycle 346 | fun:main 347 | } 348 | { 349 | 350 | Memcheck:Cond 351 | fun:index 352 | fun:expand_dynamic_string_token 353 | fun:_dl_map_object 354 | fun:map_doit 355 | fun:_dl_catch_error 356 | fun:do_preload 357 | fun:dl_main 358 | fun:_dl_sysdep_start 359 | fun:_dl_start 360 | } 361 | { 362 | 363 | Memcheck:Leak 364 | match-leak-kinds: definite 365 | fun:malloc 366 | fun:ngx_alloc 367 | fun:ngx_set_environment 368 | fun:ngx_single_process_cycle 369 | } 370 | { 371 | 372 | Memcheck:Leak 373 | match-leak-kinds: definite 374 | fun:malloc 375 | fun:ngx_alloc 376 | fun:ngx_set_environment 377 | fun:ngx_worker_process_init 378 | fun:ngx_worker_process_cycle 379 | } 380 | --------------------------------------------------------------------------------