├── .gitignore
├── README.markdown
├── dist.ini
├── lib
└── resty
│ └── post.lua
└── t
└── post.t
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.swo
3 | *~
4 | go
5 | t/servroot/
6 | reindex
7 | *.t_
8 | tags
9 | .hg
10 | .hgignore
11 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | lua-resty-post
2 | ==============
3 |
4 | Openresty utility for HTTP post
5 |
6 | Table of Contents
7 | =================
8 | * [Status](#status)
9 | * [Description](#description)
10 | * [Installation](#installation)
11 | * [How to use](#how-to-use)
12 | * [Copyright and License](#copyright-and-license)
13 | * [See Also](#see-also)
14 |
15 | Status
16 | ======
17 |
18 | This library beta tested and used in production.
19 |
20 | Description
21 | ===========
22 |
23 | This library processed HTTP using [lua-resty-upload](https://github.com/openresty/lua-resty-upload) which very fast and low memory used, it handles multiple type of HTTP POST and converted into lua table:
24 | * application/x-www-form-urlencoded
25 | * application/json
26 | * multipart/form-data
27 | * [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
28 | * [File Upload](#file-upload)
29 | * [Array Input](#array-input)
30 |
31 | [Back to TOC](#table-of-contents)
32 |
33 | Installation
34 | ============
35 |
36 | * Download or clone this repo
37 | * copy or link to openresty/lualib/resty/ or to any your lua_package_path
38 |
39 | [Back to TOC](#table-of-contents)
40 |
41 | How to use
42 | ==========
43 |
44 | ```lua
45 | local resty_post = require 'resty.post'
46 | local post = resty_post:new()
47 | local m = post:read()
48 | -- return table with all form value and file
49 | ```
50 |
51 | [Back to TOC](#table-of-contents)
52 |
53 | File Upload
54 | ===========
55 |
56 | * Support multiple file upload
57 | * Files info are stored in files property using field name as key
58 | ```lua
59 | {
60 | files = {
61 | file1 = { -- input name
62 | name = "a.txt",
63 | type = "text/plain",
64 | size = 10240,
65 | tmp_name = 1454551131.5459
66 | },
67 | file2 = {
68 | name = "b.png",
69 | type = "image/png",
70 | size = 20480,
71 | tmp_name = 1454553275.6401
72 | }
73 | }
74 | ```
75 |
76 | * Define path for files upload or default to logs directory (follow ngx.config.prefix)
77 | * Default file will be saved to tmp name (require moving action to destination)
78 | ``` lua
79 | local resty_post = require "resty.post"
80 | local post = resty_post:new({
81 | path = "/my/path", -- path upload file will be saved
82 | chunk_size = 10240, -- default 8192
83 | no_tmp = true, -- if set original name will uses or generate random name
84 | name = function(name, field) -- overide name with user defined function
85 | return name.."_"..field
86 | end
87 | })
88 | post:read()
89 | ```
90 |
91 |
92 | Array Input
93 | ===========
94 |
95 | Support multiple input of similar name
96 | --------------------------------------
97 | It is useful for thing like HTML input checkboxes or select in multiple mode
98 | ```html
99 |
100 |
101 |
106 | ```
107 | converted into
108 | ``` lua
109 | {
110 | check_multi = { 1, 2 },
111 | select_multi = { 1, 2 }
112 | }
113 | ```
114 | When checked one similar with [ngx.req.get_post_args](https://github.com/openresty/lua-nginx-module#ngxreqget_post_args)
115 | ``` lua
116 | {
117 | check_multi = 2,
118 | select_multi = 1
119 | }
120 | ```
121 |
122 |
123 | Support array input with name
124 | --------------------------------------
125 | This is like supporting input which mimic class and property, which can be uses to handle dynamic input
126 | support PHP style (dynamic language) and ASP.NET MVC binding style (static language which uses class)
127 | ```html
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | ```
145 | converted into
146 | ```lua
147 | {
148 | name = {
149 | "Bar",
150 | "Foo"
151 | },
152 | user = {
153 | title = "Mr.",
154 | name = "Foo Bar"
155 | },
156 | users = {
157 | {
158 | title = "Mr.",
159 | name = "John Do"
160 | },
161 | {
162 | title = "Ms.",
163 | name = "Jane Do"
164 | }
165 | }
166 | }
167 | ```
168 |
169 | [Back to TOC](#table-of-contents)
170 |
171 | Copyright and License
172 | =====================
173 |
174 | This module is licensed under the BSD license.
175 |
176 | Copyright (C) 2015, by Anton Heryanto Hasan.
177 |
178 | All rights reserved.
179 |
180 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
181 |
182 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
183 |
184 | * 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.
185 |
186 | 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.
187 |
188 | [Back to TOC](#table-of-contents)
189 |
190 | See Also
191 | ========
192 | * [lua-resty-stack](https://github.com/antonheryanto/lua-resty-stack)
193 | * [lua-resty-upload](https://github.com/openresty/lua-resty-upload)
194 | * [lua-nginx-module](https://github.com/openresty/lua-nginx-module)
195 |
196 | [Back to TOC](#table-of-contents)
197 |
--------------------------------------------------------------------------------
/dist.ini:
--------------------------------------------------------------------------------
1 | name = lua-resty-post
2 | abstract = Openresty utility for parsing HTTP POST data
3 | author = Anton Heryanto
4 | is_original = yes
5 | license = 2bsd
6 | lib_dir = lib
7 | doc_dir = lib
8 | repo_link = https://github.com/antonheryanto/lua-resty-post
9 | main_module = lib/resty/post.lua
10 |
--------------------------------------------------------------------------------
/lib/resty/post.lua:
--------------------------------------------------------------------------------
1 | -- Copyright (C) Anton heryanto.
2 |
3 | local cjson = require "cjson"
4 | local upload = require "resty.upload"
5 | local table_new_ok, new_tab = pcall(require, "table.new")
6 |
7 |
8 | local open = io.open
9 | local sub = string.sub
10 | local find = string.find
11 | local byte = string.byte
12 | local lower = string.lower
13 | local type = type
14 | local tonumber = tonumber
15 | local setmetatable = setmetatable
16 | local random = math.random
17 | local re_find = ngx.re.find
18 | local read_body = ngx.req.read_body
19 | local get_post_args = ngx.req.get_post_args
20 | local var = ngx.var
21 | local log = ngx.log
22 | local WARN = ngx.WARN
23 | local prefix = ngx.config.prefix()..'logs/'
24 | local now = ngx.now
25 |
26 |
27 | if not table_new_ok then
28 | new_tab = function(narr, nrec) return {} end
29 | end
30 |
31 |
32 | local _M = new_tab(0, 3)
33 | local mt = { __index = _M }
34 | _M.VERSION = '0.2.3'
35 |
36 |
37 | local function tmp()
38 | return now() + random()
39 | end
40 |
41 |
42 | local function original(name)
43 | return name
44 | end
45 |
46 |
47 | function _M.new(self, opts)
48 | local ot = type(opts)
49 | opts = ot == 'string' and {path = opts} or ot == 'table' and opts or {}
50 | opts.path = type(opts.path) == 'string' and opts.path or prefix
51 | opts.chunk_size = tonumber(opts.chunk_size, 10) or 8192
52 | opts.name = type(opts.name) == 'function' and opts.name or opts.no_tmp
53 | and original or tmp
54 | return setmetatable(opts, mt)
55 | end
56 |
57 |
58 | local function decode_disposition(self, data)
59 | local needle = 'filename="'
60 | local needle_len = 10 -- #needle
61 | local name_pos = 18 -- #'form-data; name="'
62 | local last_quote_pos = #data - 1
63 | local filename_pos = find(data, needle)
64 |
65 | if not filename_pos then
66 | return sub(data,name_pos,last_quote_pos)
67 | end
68 |
69 | local field = sub(data,name_pos,filename_pos - 4)
70 | local name = sub(data,filename_pos + needle_len, last_quote_pos)
71 | if not name or name == '' then
72 | return
73 | end
74 |
75 | local fn = self.name
76 | local path = self.path
77 | local tmp_name = fn(name, field)
78 | local filename = path .. tmp_name
79 | local handler = open(filename, 'w+b')
80 |
81 | if not handler then
82 | log(WARN, 'failed to open file ', filename)
83 | end
84 |
85 | return field, name, handler, tmp_name
86 | end
87 |
88 |
89 | local function multipart(self)
90 | local chunk_size = self.chunk_size
91 | local form, e = upload:new(chunk_size)
92 | if not form then
93 | log(WARN, 'failed to new upload: ', e)
94 | return
95 | end
96 |
97 | local m = { files = {} }
98 | local files = {}
99 | local handler, key, value
100 | while true do
101 | local ctype, res, er = form:read()
102 |
103 | if not ctype then
104 | log(WARN, 'failed to read: ', er)
105 | return
106 | end
107 |
108 | if ctype == 'header' then
109 | local header, data = res[1], res[2]
110 |
111 | if lower(header or '') == 'content-disposition' then
112 | local tmp_name
113 | key, value, handler, tmp_name = decode_disposition(self, data)
114 |
115 | if handler then
116 | files[key] = { name = value, tmp_name = tmp_name }
117 | end
118 | end
119 |
120 | if handler and lower(header or '') == 'content-type' then
121 | files[key].type = data
122 | end
123 | end
124 |
125 | if ctype == 'body' then
126 | if handler then
127 | handler:write(res)
128 | elseif res ~= '' then
129 | value = value and value .. res or res
130 | end
131 | end
132 |
133 | if ctype == 'part_end' then
134 | if handler then
135 | files[key].size = handler:seek('end')
136 | handler:close()
137 | if m.files[key] then
138 | local nf = #m.files[key]
139 | if nf > 0 then
140 | m.files[key][nf + 1] = files[key]
141 | else
142 | m.files[key] = { m.files[key], files[key] }
143 | end
144 | else
145 | m.files[key] = files[key]
146 | end
147 |
148 | elseif key then
149 | -- handle array input, checkboxes
150 | -- handle one dimension array input
151 | -- name[0]
152 | -- user.name and user[name]
153 | -- user[0].name and user[0][name]
154 | -- TODO [0].name ?
155 | -- FIXME track mk
156 | local from, to = re_find(key, '(\\[\\w+\\])|(\\.)','jo')
157 | if from then
158 | -- check 46(.)
159 | local index = byte(key, from) == 46 and '' or
160 | sub(key, from + 1, to - 1)
161 | local name = sub(key, 0, from - 1)
162 | local field
163 | if #key == to then -- parse input[name]
164 | local ix = tonumber(index, 10)
165 | field = ix and ix + 1 or index
166 | index = ''
167 | else
168 | -- parse input[index].field or input[index][field]
169 | local ns = index == '' and 1 or 2
170 | local ne = #key
171 | if index ~= '' and byte(key, to + 1) ~= 46 then
172 | ne = ne - 1
173 | end
174 | field = sub(key, to + ns, ne)
175 | index = index == '' and index or (index + 1)
176 | end
177 |
178 | if type(m[name]) ~= 'table' then
179 | m[name] = {}
180 | end
181 |
182 | if index ~= '' and type(m[name][index]) ~= 'table' then
183 | m[name][index] = {}
184 | end
185 |
186 | if index ~= '' and m[name][index] then
187 | m[name][index][field] = value -- input[0].name
188 | else
189 | m[name][field] = value
190 | end
191 |
192 | elseif m[key] then
193 | local mk = m[key]
194 | if type(mk) == 'table' then
195 | m[key][#mk + 1] = value
196 | else
197 | m[key] = { mk, value }
198 | end
199 | else
200 | m[key] = value
201 | end
202 | key = nil
203 | value = nil
204 | end
205 | end
206 |
207 | if ctype == 'eof' then break end
208 |
209 | end
210 | return m
211 | end
212 |
213 |
214 | -- proses post based on content type
215 | function _M.read(self)
216 | local ctype = var.content_type
217 |
218 | if ctype and find(ctype, 'multipart') then
219 | return multipart(self)
220 | end
221 |
222 | read_body()
223 |
224 | if ctype and find(ctype, 'json') then
225 | local body = var.request_body
226 | return body and cjson.decode(body) or {}
227 | end
228 |
229 | return get_post_args()
230 | end
231 |
232 |
233 | return _M
234 |
--------------------------------------------------------------------------------
/t/post.t:
--------------------------------------------------------------------------------
1 | use Test::Nginx::Socket::Lua;
2 | use Cwd qw(cwd);
3 |
4 | repeat_each(2);
5 |
6 | plan tests => repeat_each() * (blocks() * 2);
7 |
8 | my $pwd = cwd();
9 |
10 | our $HttpConfig = <<"_EOC_";
11 | lua_package_path "$pwd/lib/?.lua;;";
12 | _EOC_
13 |
14 | no_long_string();
15 | no_diff();
16 | run_tests();
17 |
18 | __DATA__
19 |
20 | === TEST 1: simple post
21 | --- http_config eval: $::HttpConfig
22 | --- config
23 | location /t {
24 | content_by_lua_block {
25 | local cjson = require 'cjson'
26 | local post = require 'resty.post':new()
27 | local m = post:read()
28 | ngx.say(cjson.encode(m))
29 | }
30 | }
31 | --- request
32 | POST /t
33 | a=3&b=4&c
34 | --- response_body
35 | {"b":"4","a":"3","c":true}
36 |
37 |
38 | === TEST 2: array post
39 | --- http_config eval: $::HttpConfig
40 | --- config
41 | location /t {
42 | content_by_lua_block {
43 | local cjson = require 'cjson'
44 | local post = require 'resty.post':new()
45 | local m = post:read()
46 | ngx.say(cjson.encode(m))
47 | }
48 | }
49 | --- request
50 | POST /t
51 | a=1&a=2&b=1&c=1&c=2
52 | --- response_body
53 | {"b":"1","a":["1","2"],"c":["1","2"]}
54 |
55 |
56 | === TEST 3: json
57 | --- http_config eval: $::HttpConfig
58 | --- config
59 | location /t {
60 | content_by_lua_block {
61 | local cjson = require "cjson"
62 | local post = require "resty.post":new()
63 | local m = post:read()
64 | ngx.say(cjson.encode(m))
65 | }
66 | }
67 | --- more_headers
68 | Content-Type: application/json
69 | --- request
70 | POST /t
71 | {"a":3,"b":4,"c":true}
72 | --- response_body
73 | {"b":4,"a":3,"c":true}
74 | --- error_log
75 |
76 |
77 | === TEST 4: post with formdata file
78 | --- http_config eval: $::HttpConfig
79 | --- config
80 | location /t {
81 | content_by_lua_block {
82 | local cjson = require 'cjson'
83 | local post = require 'resty.post':new()
84 | local m = post:read()
85 | m.files.file1.tmp_name = nil
86 | ngx.say(cjson.encode(m))
87 | }
88 | }
89 | --- more_headers
90 | Content-Type: multipart/form-data; boundary=---------------------------820127721219505131303151179
91 | --- request eval
92 | qq{POST /t\n-----------------------------820127721219505131303151179\r
93 | Content-Disposition: form-data; name="file1"; filename="a.txt"\r
94 | Content-Type: text/plain\r
95 | \r
96 | Hello, world\r\n-----------------------------820127721219505131303151179\r
97 | Content-Disposition: form-data; name="test"\r
98 | \r
99 | value\r
100 | \r\n-----------------------------820127721219505131303151179--\r
101 | }
102 | --- response_body
103 | {"files":{"file1":{"type":"text\/plain","size":12,"name":"a.txt"}},"test":"value\r\n"}
104 |
105 |
106 | === TEST 5: post with formdata array input
107 | --- http_config eval: $::HttpConfig
108 | --- config
109 | location /t {
110 | content_by_lua_block {
111 | local cjson = require 'cjson'
112 | local post = require 'resty.post':new()
113 | local m = post:read()
114 | m.files = nil
115 | ngx.say(cjson.encode(m.name))
116 | ngx.say(cjson.encode(m.user))
117 | ngx.say(cjson.encode(m.friend))
118 | }
119 | }
120 | --- more_headers
121 | Content-Type: multipart/form-data; boundary=---------------------------820127721219505131303151179
122 | --- request eval
123 | qq{POST /t\n-----------------------------820127721219505131303151179\r
124 | Content-Disposition: form-data; name="name[1]"\r
125 | \r
126 | Foo\r
127 | -----------------------------820127721219505131303151179\r
128 | Content-Disposition: form-data; name="name[0]"\r
129 | \r
130 | Bar\r
131 | -----------------------------820127721219505131303151179\r
132 | Content-Disposition: form-data; name="user.name"\r
133 | \r
134 | Foo Bar\r
135 | -----------------------------820127721219505131303151179\r
136 | Content-Disposition: form-data; name="user[title]"\r
137 | \r
138 | Mr.\r
139 | -----------------------------820127721219505131303151179\r
140 | Content-Disposition: form-data; name="friend[0].title"\r
141 | \r
142 | Ms.\r
143 | -----------------------------820127721219505131303151179\r
144 | Content-Disposition: form-data; name="friend[0].name"\r
145 | \r
146 | Jane Doo\r
147 | -----------------------------820127721219505131303151179\r
148 | Content-Disposition: form-data; name="friend[1][title]"\r
149 | \r
150 | Mr.\r
151 | -----------------------------820127721219505131303151179\r
152 | Content-Disposition: form-data; name="friend[1][name]"\r
153 | \r
154 | John Doo\r
155 | -----------------------------820127721219505131303151179--\r
156 | }
157 | --- response_body
158 | ["Bar","Foo"]
159 | {"name":"Foo Bar","title":"Mr."}
160 | [{"title":"Ms.","name":"Jane Doo"},{"title":"Mr.","name":"John Doo"}]
161 |
162 |
--------------------------------------------------------------------------------