1 -- Copyright 2008 Freifunk Leipzig / Jo-Philipp Wich <jow@openwrt.org>
2 -- Licensed to the public under the Apache License 2.0.
4 -- This class contains several functions useful for http message- and content
5 -- decoding and to retrive form data from raw http messages.
6 module("luci.http.protocol", package.seeall)
8 local ltn12 = require("luci.ltn12")
9 local util = require("luci.util")
11 HTTP_MAX_CONTENT = 1024*8 -- 8 kB maximum content size
13 -- from given url or string. Returns a table with urldecoded values.
14 -- Simple parameters are stored as string values associated with the parameter
15 -- name within the table. Parameters with multiple values are stored as array
16 -- containing the corresponding values.
17 function urldecode_params( url, tbl )
19 local params = tbl or { }
22 url = url:gsub( "^.+%?([^?]+)", "%1" )
25 for pair in url:gmatch( "[^&;]+" ) do
28 local key = util.urldecode( pair:match("^([^=]+)") )
29 local val = util.urldecode( pair:match("^[^=]+=(.+)$") )
32 if type(key) == "string" and key:len() > 0 then
33 if type(val) ~= "string" then val = "" end
35 if not params[key] then
37 elseif type(params[key]) ~= "table" then
38 params[key] = { params[key], val }
40 table.insert( params[key], val )
48 -- separated by "&". Tables are encoded as parameters with multiple values by
49 -- repeating the parameter name with each value.
50 function urlencode_params( tbl )
53 for k, v in pairs(tbl) do
54 if type(v) == "table" then
55 for i, v2 in ipairs(v) do
56 enc = enc .. ( #enc > 0 and "&" or "" ) ..
57 util.urlencode(k) .. "=" .. util.urlencode(v2)
60 enc = enc .. ( #enc > 0 and "&" or "" ) ..
61 util.urlencode(k) .. "=" .. util.urlencode(v)
68 -- (Internal function)
69 -- Initialize given parameter and coerce string into table when the parameter
71 local function __initval( tbl, key )
72 if tbl[key] == nil then
74 elseif type(tbl[key]) == "string" then
75 tbl[key] = { tbl[key], "" }
77 table.insert( tbl[key], "" )
81 -- (Internal function)
82 -- Initialize given file parameter.
83 local function __initfileval( tbl, key, filename, fd )
84 if tbl[key] == nil then
85 tbl[key] = { file=filename, fd=fd, name=key, "" }
87 table.insert( tbl[key], "" )
91 -- (Internal function)
92 -- Append given data to given parameter, either by extending the string value
93 -- or by appending it to the last string in the parameter's value table.
94 local function __appendval( tbl, key, chunk )
95 if type(tbl[key]) == "table" then
96 tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk
98 tbl[key] = tbl[key] .. chunk
102 -- (Internal function)
103 -- Finish the value of given parameter, either by transforming the string value
104 -- or - in the case of multi value parameters - the last element in the
105 -- associated values table.
106 local function __finishval( tbl, key, handler )
108 if type(tbl[key]) == "table" then
109 tbl[key][#tbl[key]] = handler( tbl[key][#tbl[key]] )
111 tbl[key] = handler( tbl[key] )
117 -- Table of our process states
118 local process_states = { }
120 -- Extract "magic", the first line of a http message.
121 -- Extracts the message type ("get", "post" or "response"), the requested uri
122 -- or the status code if the line descripes a http response.
123 process_states['magic'] = function( msg, chunk, err )
126 -- ignore empty lines before request
132 local method, uri, http_ver = chunk:match("^([A-Z]+) ([^ ]+) HTTP/([01]%.[019])$")
138 msg.request_method = method:lower()
139 msg.request_uri = uri
140 msg.http_version = tonumber( http_ver )
143 -- We're done, next state is header parsing
144 return true, function( chunk )
145 return process_states['headers']( msg, chunk )
151 local http_ver, code, message = chunk:match("^HTTP/([01]%.[019]) ([0-9]+) ([^\r\n]+)$")
156 msg.type = "response"
157 msg.status_code = code
158 msg.status_message = message
159 msg.http_version = tonumber( http_ver )
162 -- We're done, next state is header parsing
163 return true, function( chunk )
164 return process_states['headers']( msg, chunk )
171 return nil, "Invalid HTTP message magic"
175 -- Extract headers from given string.
176 process_states['headers'] = function( msg, chunk )
180 -- Look for a valid header format
181 local hdr, val = chunk:match( "^([A-Za-z][A-Za-z0-9%-_]+): +(.+)$" )
183 if type(hdr) == "string" and hdr:len() > 0 and
184 type(val) == "string" and val:len() > 0
186 msg.headers[hdr] = val
188 -- Valid header line, proceed
191 elseif #chunk == 0 then
192 -- Empty line, we won't accept data anymore
196 return nil, "Invalid HTTP header received"
199 return nil, "Unexpected EOF"
204 -- data line by line with the trailing \r\n stripped of.
205 function header_source( sock )
206 return ltn12.source.simplify( function()
208 local chunk, err, part = sock:receive("*l")
212 if err ~= "timeout" then
214 and "Line exceeds maximum allowed length"
221 elseif chunk ~= nil then
224 chunk = chunk:gsub("\r$","")
231 -- Content-Type. Stores all extracted data associated with its parameter name
232 -- in the params table within the given message object. Multiple parameter
233 -- values are stored as tables, ordinary ones as strings.
234 -- If an optional file callback function is given then it is feeded with the
235 -- file contents chunk by chunk and only the extracted file name is stored
236 -- within the params table. The callback function will be called subsequently
237 -- with three arguments:
238 -- o Table containing decoded (name, file) and raw (headers) mime header data
239 -- o String value containing a chunk of the file data
240 -- o Boolean which indicates wheather the current chunk is the last one (eof)
241 function mimedecode_message_body( src, msg, filecb )
243 if msg and msg.env.CONTENT_TYPE then
244 msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$")
247 if not msg.mime_boundary then
248 return nil, "Invalid Content-Type found"
258 local function parse_headers( chunk, field )
262 chunk, stat = chunk:gsub(
263 "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n",
271 chunk, stat = chunk:gsub("^\r\n","")
275 if field.headers["Content-Disposition"] then
276 if field.headers["Content-Disposition"]:match("^form%-data; ") then
277 field.name = field.headers["Content-Disposition"]:match('name="(.-)"')
278 field.file = field.headers["Content-Disposition"]:match('filename="(.+)"$')
282 if not field.headers["Content-Type"] then
283 field.headers["Content-Type"] = "text/plain"
286 if field.name and field.file and filecb then
287 __initval( msg.params, field.name )
288 __appendval( msg.params, field.name, field.file )
291 elseif field.name and field.file then
292 local nxf = require "nixio"
293 local fd = nxf.mkstemp(field.name)
294 __initfileval ( msg.params, field.name, field.file, fd )
296 store = function(hdr, buf, eof)
303 store = function( hdr, buf, eof )
304 __appendval( msg.params, field.name, buf )
307 elseif field.name then
308 __initval( msg.params, field.name )
310 store = function( hdr, buf, eof )
311 __appendval( msg.params, field.name, buf )
323 local function snk( chunk )
325 tlen = tlen + ( chunk and #chunk or 0 )
327 if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
328 return nil, "Message body size exceeds Content-Length"
331 if chunk and not lchunk then
332 lchunk = "\r\n" .. chunk
335 local data = lchunk .. ( chunk or "" )
336 local spos, epos, found
339 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
342 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
347 local predata = data:sub( 1, spos - 1 )
350 predata, eof = parse_headers( predata, field )
353 return nil, "Invalid MIME section header"
354 elseif not field.name then
355 return nil, "Invalid Content-Disposition header"
360 store( field, predata, true )
364 field = { headers = { } }
365 found = found or true
367 data, eof = parse_headers( data:sub( epos + 1, #data ), field )
373 -- We found at least some boundary. Save
374 -- the unparsed remaining data for the
376 lchunk, data = data, nil
378 -- There was a complete chunk without a boundary. Parse it as headers or
379 -- append it as data, depending on our current state.
381 lchunk, eof = parse_headers( data, field )
384 -- We're inside data, so append the data. Note that we only append
385 -- lchunk, not all of data, since there is a chance that chunk
386 -- contains half a boundary. Assuming that each chunk is at least the
387 -- boundary in size, this should prevent problems
388 store( field, lchunk, false )
389 lchunk, chunk = chunk, nil
397 return ltn12.pump.all( src, snk )
400 -- Content-Type. Stores all extracted data associated with its parameter name
401 -- in the params table within the given message object. Multiple parameter
402 -- values are stored as tables, ordinary ones as strings.
403 function urldecode_message_body( src, msg )
408 local function snk( chunk )
410 tlen = tlen + ( chunk and #chunk or 0 )
412 if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
413 return nil, "Message body size exceeds Content-Length"
414 elseif tlen > HTTP_MAX_CONTENT then
415 return nil, "Message body size exceeds maximum allowed length"
418 if not lchunk and chunk then
422 local data = lchunk .. ( chunk or "&" )
426 spos, epos = data:find("^.-[;&]")
429 local pair = data:sub( spos, epos - 1 )
430 local key = pair:match("^(.-)=")
431 local val = pair:match("=([^%s]*)%s*$")
433 if key and #key > 0 then
434 __initval( msg.params, key )
435 __appendval( msg.params, key, val )
436 __finishval( msg.params, key, urldecode )
439 data = data:sub( epos + 1, #data )
449 return ltn12.pump.all( src, snk )
452 -- version, message headers and resulting CGI environment variables from the
453 -- given ltn12 source.
454 function parse_message_header( src )
459 local sink = ltn12.sink.simplify(
461 return process_states['magic']( msg, chunk )
465 -- Pump input data...
469 ok, err = ltn12.pump.step( src, sink )
472 if not ok and err then
478 -- Process get parameters
479 if ( msg.request_method == "get" or msg.request_method == "post" ) and
480 msg.request_uri:match("?")
482 msg.params = urldecode_params( msg.request_uri )
487 -- Populate common environment variables
489 CONTENT_LENGTH = msg.headers['Content-Length'];
490 CONTENT_TYPE = msg.headers['Content-Type'] or msg.headers['Content-type'];
491 REQUEST_METHOD = msg.request_method:upper();
492 REQUEST_URI = msg.request_uri;
493 SCRIPT_NAME = msg.request_uri:gsub("?.+$","");
494 SCRIPT_FILENAME = ""; -- XXX implement me
495 SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version);
496 QUERY_STRING = msg.request_uri:match("?")
497 and msg.request_uri:gsub("^.+?","") or ""
500 -- Populate HTTP_* environment variables
501 for i, hdr in ipairs( {
512 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
513 local val = msg.headers[hdr]
523 -- This function will examine the Content-Type within the given message object
524 -- to select the appropriate content decoder.
525 -- Currently the application/x-www-urlencoded and application/form-data
526 -- mime types are supported. If the encountered content encoding can't be
527 -- handled then the whole message body will be stored unaltered as "content"
528 -- property within the given message object.
529 function parse_message_body( src, msg, filecb )
530 -- Is it multipart/mime ?
531 if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
532 msg.env.CONTENT_TYPE:match("^multipart/form%-data")
535 return mimedecode_message_body( src, msg, filecb )
537 -- Is it application/x-www-form-urlencoded ?
538 elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
539 msg.env.CONTENT_TYPE:match("^application/x%-www%-form%-urlencoded")
541 return urldecode_message_body( src, msg, filecb )
544 -- Unhandled encoding
545 -- If a file callback is given then feed it chunk by chunk, else
546 -- store whole buffer in message.content
551 -- If we have a file callback then feed it
552 if type(filecb) == "function" then
555 encoding = msg.env.CONTENT_TYPE
557 sink = function( chunk )
559 return filecb(meta, chunk, false)
561 return filecb(meta, nil, true)
564 -- ... else append to .content
567 msg.content_length = 0
569 sink = function( chunk )
571 if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then
572 msg.content = msg.content .. chunk
573 msg.content_length = msg.content_length + #chunk
576 return nil, "POST data exceeds maximum allowed length"
585 local ok, err = ltn12.pump.step( src, sink )
587 if not ok and err then
589 elseif not ok then -- eof
600 [206] = "Partial Content",
601 [301] = "Moved Permanently",
603 [304] = "Not Modified",
604 [400] = "Bad Request",
607 [405] = "Method Not Allowed",
608 [408] = "Request Time-out",
609 [411] = "Length Required",
610 [412] = "Precondition Failed",
611 [416] = "Requested range not satisfiable",
612 [500] = "Internal Server Error",
613 [503] = "Server Unavailable",