3 HTTP protocol implementation for LuCI
4 (c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net>
6 Licensed under the Apache License, Version 2.0 (the "License");
7 you may not use this file except in compliance with the License.
8 You may obtain a copy of the License at
10 http://www.apache.org/licenses/LICENSE-2.0
16 module("luci.http.protocol", package.seeall)
22 HTTP_MAX_CONTENT = 1048576 -- 1 MB
23 HTTP_DEFAULT_CTYPE = "text/html" -- default content type
24 HTTP_DEFAULT_VERSION = "1.0" -- HTTP default version
27 -- Decode an urlencoded string.
28 -- Returns the decoded value.
29 function urldecode( str )
31 local function __chrdec( hex )
32 return string.char( luci.bits.Hex2Dec( hex ) )
35 if type(str) == "string" then
36 str = str:gsub( "+", " " ):gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec )
43 -- Extract and split urlencoded data pairs, separated bei either "&" or ";" from given url.
44 -- Returns a table value with urldecoded values.
45 function urldecode_params( url )
50 url = url:gsub( "^.+%?([^?]+)", "%1" )
53 for i, pair in ipairs(luci.util.split( url, "[&;]+", nil, true )) do
56 local key = urldecode( pair:match("^([^=]+)") )
57 local val = urldecode( pair:match("^[^=]+=(.+)$") )
60 if type(key) == "string" and key:len() > 0 then
61 if type(val) ~= "string" then val = "" end
63 if not params[key] then
65 elseif type(params[key]) ~= "table" then
66 params[key] = { params[key], val }
68 table.insert( params[key], val )
77 -- Encode given string in urlencoded format.
78 -- Returns the encoded string.
79 function urlencode( str )
81 local function __chrenc( chr )
83 "%%%02x", string.byte( chr )
87 if type(str) == "string" then
89 "([^a-zA-Z0-9$_%-%.+!*'(),])",
98 -- Encode given table to urlencoded string.
99 -- Returns the encoded string.
100 function urlencode_params( tbl )
103 for k, v in pairs(tbl) do
104 enc = enc .. ( enc and "&" or "" ) ..
105 urlencode(k) .. "=" ..
113 -- Decode MIME encoded data.
114 -- Returns a table with decoded values.
115 function mimedecode( data, boundary, filecb )
119 -- create a line reader
120 local reader = _linereader( data )
123 local in_part = false
124 local in_file = false
125 local in_fbeg = false
134 -- try to read all mime parts
135 for line in reader do
137 -- update content length
138 clen = clen + line:len()
140 if clen >= HTTP_MAX_CONTENT then
144 -- when no boundary is given, try to find it
146 boundary = line:match("^%-%-([^\r\n]+)\r?\n$")
149 -- Got a valid boundary line or reached max allowed size.
150 if ( boundary and line:sub(1,2) == "--" and line:len() > #boundary + 2 and
151 line:sub( 3, 2 + #boundary ) == boundary ) or not in_size
153 -- Flush the data of the previous mime part.
154 -- When field and/or buffer are set to nil we should discard
155 -- the previous section entirely due to format violations.
156 if type(field) == "string" and field:len() > 0 and
157 type(buffer) == "string"
159 -- According to the rfc the \r\n preceeding a boundary
160 -- is assumed to be part of the boundary itself.
161 -- Since we are reading line by line here, this crlf
162 -- is part of the last line of our section content,
163 -- so strip it before storing the buffer.
164 buffer = buffer:gsub("\r?\n$","")
166 -- If we're in a file part and a file callback has been provided
167 -- then do a final call and send eof.
168 if in_file and type(filecb) == "function" then
169 filecb( field, filename, buffer, true )
170 params[field] = filename
174 params[field] = buffer
184 -- Abort here if we reached maximum allowed size
185 if not in_size then break end
187 -- Do we got the last boundary?
188 if line:len() > #boundary + 4 and
189 line:sub( #boundary + 2, #boundary + 4 ) == "--"
191 -- No more processing
194 -- It's a middle boundary
198 local hlen, headers = extract_headers( reader )
200 -- Check for valid headers
201 if headers['Content-Disposition'] then
203 -- Got no content type header, assume content-type "text/plain"
204 if not headers['Content-Type'] then
205 headers['Content-Type'] = 'text/plain'
209 local hdrvals = luci.util.split(
210 headers['Content-Disposition'], '; '
213 -- Valid form data part?
214 if hdrvals[1] == "form-data" and hdrvals[2]:match("^name=") then
216 -- Store field identifier
217 field = hdrvals[2]:match('^name="(.+)"$')
219 -- Do we got a file upload field?
220 if #hdrvals == 3 and hdrvals[3]:match("^filename=") then
223 filename = hdrvals[3]:match('^filename="(.+)"$')
226 -- Entering next part processing
232 -- Processing content
235 -- XXX: Would be really good to switch from line based to
236 -- buffered reading here.
239 -- If we're in a file part and a file callback has been provided
240 -- then call the callback and reset the buffer.
241 if in_file and type(filecb) == "function" then
243 -- If we're not processing the first chunk, then call
245 filecb( field, filename, buffer, false )
248 -- Clear in_fbeg flag after first run
254 -- Append date to buffer
255 buffer = buffer .. line
263 -- Extract "magic", the first line of a http message.
264 -- Returns the message type ("get", "post" or "response"), the requested uri
265 -- if it is a valid http request or the status code if the line descripes a
266 -- http response. For requests the third parameter is nil, for responses it
267 -- contains the human readable status description.
268 function extract_magic( reader )
270 for line in reader do
272 local method, uri = line:match("^([A-Z]+) ([^ ]+) HTTP/[01]%.[019]\r?\n$")
276 return method:lower(), uri, nil
280 local code, message = line:match("^HTTP/[01]%.[019] ([0-9]+) ([^\r\n]+)\r?\n$")
284 return "response", code + 0, message
295 -- Extract headers from given string.
296 -- Returns a table of extracted headers and the remainder of the parsed data.
297 function extract_headers( reader, tbl )
299 local headers = tbl or { }
302 -- Iterate line by line
303 for line in reader do
305 -- Look for a valid header format
306 local hdr, val = line:match( "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r?\n$" )
308 if type(hdr) == "string" and hdr:len() > 0 and
309 type(val) == "string" and val:len() > 0
311 count = count + line:len()
314 elseif line:match("^\r?\n$") then
316 return count + line:len(), headers
319 -- junk data, don't add length
320 return count, headers
324 return count, headers
328 -- Parse a http message
329 function parse_message( data, filecb )
331 -- Create a line reader
332 local reader = _linereader( data )
335 -- Try to extract magic
336 local method, arg1, arg2 = extract_magic( reader )
338 -- Does it looks like a valid message?
341 message.request_method = method
342 message.status = arg2 and arg1 or 0
343 message.request_uri = arg2 and nil or arg1
345 if method == "response" then
346 message.type = "response"
348 message.type = "request"
352 local hlen, hdrs = extract_headers( reader )
355 if hlen > 2 and type(hdrs) == "table" then
357 message.headers = hdrs
360 local clen = ( hdrs['Content-Length'] or HTTP_MAX_CONTENT ) + 0
362 -- Process get parameters
363 if method == "get" or method == "post" then
364 message.params = urldecode_params( message.request_uri )
367 -- Process post method
368 if method == "post" and hdrs['Content-Type'] then
370 -- Is it multipart/form-data ?
371 if hdrs['Content-Type']:match("^multipart/form%-data") then
372 for k, v in pairs( mimedecode(
374 hdrs['Content-Type']:match("boundary=(.+)"),
377 message.params[k] = v
380 -- Is it x-www-urlencoded?
381 elseif hdrs['Content-Type'] == 'application/x-www-urlencoded' then
383 -- XXX: readline isn't the best solution here
384 for chunk in reader do
385 for k, v in pairs( urldecode_params( chunk ) ) do
386 message.params[k] = v
389 -- XXX: unreliable (undefined line length)
390 if clen + chunk:len() >= HTTP_MAX_CONTENT then
394 clen = clen + chunk:len()
397 -- Unhandled encoding
398 -- If a file callback is given then feed it line by line, else
399 -- store whole buffer in message.content
402 for chunk in reader do
404 -- We have a callback, feed it.
405 if type(filecb) == "function" then
407 filecb( "_post", nil, chunk, false )
409 -- Append to .content buffer.
412 type(message.content) == "string"
413 and message.content .. chunk
418 if clen + chunk:len() >= HTTP_MAX_CONTENT then
422 clen = clen + chunk:len()
425 -- Send eof to callback
426 if type(filecb) == "function" then
427 filecb( "_post", nil, "", true )
432 -- Populate common environment variables
434 CONTENT_LENGTH = hdrs['Content-Length'];
435 CONTENT_TYPE = hdrs['Content-Type'];
436 REQUEST_METHOD = message.request_method;
437 REQUEST_URI = message.request_uri;
438 SCRIPT_NAME = message.request_uri:gsub("?.+$","");
439 SCRIPT_FILENAME = "" -- XXX implement me
442 -- Populate HTTP_* environment variables
443 for i, hdr in ipairs( {
454 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
455 local val = hdrs[hdr]
457 message.env[var] = val
466 function _linereader( obj )
469 if type(obj) == "string" then
471 return obj:gmatch( "[^\r\n]*\r?\n" )
473 -- object is a function
474 elseif type(obj) == "function" then
478 -- object is a table and implements a readline() function
479 elseif type(obj) == "table" and type(obj.readline) == "function" then
483 -- object is a table and has a lines property
484 elseif type(obj) == "table" and obj.lines then
486 -- decide wheather to use "lines" as function or table
487 local _lns = ( type(obj.lines) == "function" ) and obj.lines() or obj.lines
491 if _pos <= #_lns then
497 -- no usable data type