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)
18 local ltn12 = require("luci.ltn12")
20 HTTP_MAX_CONTENT = 1024*8 -- 8 kB maximum content size
22 -- Decode an urlencoded string.
23 -- Returns the decoded value.
24 function urldecode( str, no_plus )
26 local function __chrdec( hex )
27 return string.char( tonumber( hex, 16 ) )
30 if type(str) == "string" then
32 str = str:gsub( "+", " " )
35 str = str:gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec )
42 -- Extract and split urlencoded data pairs, separated bei either "&" or ";" from given url.
43 -- Returns a table value with urldecoded values.
44 function urldecode_params( url, tbl )
46 local params = tbl or { }
49 url = url:gsub( "^.+%?([^?]+)", "%1" )
52 for pair in url:gmatch( "[^&;]+" ) do
55 local key = urldecode( pair:match("^([^=]+)") )
56 local val = urldecode( pair:match("^[^=]+=(.+)$") )
59 if type(key) == "string" and key:len() > 0 then
60 if type(val) ~= "string" then val = "" end
62 if not params[key] then
64 elseif type(params[key]) ~= "table" then
65 params[key] = { params[key], val }
67 table.insert( params[key], val )
76 -- Encode given string in urlencoded format.
77 -- Returns the encoded string.
78 function urlencode( str )
80 local function __chrenc( chr )
82 "%%%02x", string.byte( chr )
86 if type(str) == "string" then
88 "([^a-zA-Z0-9$_%-%.%+!*'(),])",
97 -- Encode given table to urlencoded string.
98 -- Returns the encoded string.
99 function urlencode_params( tbl )
102 for k, v in pairs(tbl) do
103 enc = enc .. ( enc and "&" or "" ) ..
104 urlencode(k) .. "=" ..
113 local function __initval( tbl, key )
114 if tbl[key] == nil then
116 elseif type(tbl[key]) == "string" then
117 tbl[key] = { tbl[key], "" }
119 table.insert( tbl[key], "" )
123 local function __appendval( tbl, key, chunk )
124 if type(tbl[key]) == "table" then
125 tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk
127 tbl[key] = tbl[key] .. chunk
131 local function __finishval( tbl, key, handler )
133 if type(tbl[key]) == "table" then
134 tbl[key][#tbl[key]] = handler( tbl[key][#tbl[key]] )
136 tbl[key] = handler( tbl[key] )
142 -- Table of our process states
143 local process_states = { }
145 -- Extract "magic", the first line of a http message.
146 -- Extracts the message type ("get", "post" or "response"), the requested uri
147 -- or the status code if the line descripes a http response.
148 process_states['magic'] = function( msg, chunk, err )
151 -- ignore empty lines before request
157 local method, uri, http_ver = chunk:match("^([A-Z]+) ([^ ]+) HTTP/([01]%.[019])$")
163 msg.request_method = method:lower()
164 msg.request_uri = uri
165 msg.http_version = tonumber( http_ver )
168 -- We're done, next state is header parsing
169 return true, function( chunk )
170 return process_states['headers']( msg, chunk )
176 local http_ver, code, message = chunk:match("^HTTP/([01]%.[019]) ([0-9]+) ([^\r\n]+)$")
181 msg.type = "response"
182 msg.status_code = code
183 msg.status_message = message
184 msg.http_version = tonumber( http_ver )
187 -- We're done, next state is header parsing
188 return true, function( chunk )
189 return process_states['headers']( msg, chunk )
196 return nil, "Invalid HTTP message magic"
200 -- Extract headers from given string.
201 process_states['headers'] = function( msg, chunk )
205 -- Look for a valid header format
206 local hdr, val = chunk:match( "^([A-Z][A-Za-z0-9%-_]+): +(.+)$" )
208 if type(hdr) == "string" and hdr:len() > 0 and
209 type(val) == "string" and val:len() > 0
211 msg.headers[hdr] = val
213 -- Valid header line, proceed
216 elseif #chunk == 0 then
217 -- Empty line, we won't accept data anymore
221 return nil, "Invalid HTTP header received"
224 return nil, "Unexpected EOF"
229 -- Creates a header source from a given socket
230 function header_source( sock )
231 return ltn12.source.simplify( function()
233 local chunk, err, part = sock:receive("*l")
237 if err ~= "timeout" then
239 and "Line exceeds maximum allowed length"
246 elseif chunk ~= nil then
249 chunk = chunk:gsub("\r$","")
257 -- Decode MIME encoded data.
258 function mimedecode_message_body( src, msg, filecb )
260 if msg and msg.env.CONTENT_TYPE then
261 msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$")
264 if not msg.mime_boundary then
265 return nil, "Invalid Content-Type found"
269 local function parse_headers( chunk, field )
273 chunk, stat = chunk:gsub(
274 "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n",
282 chunk, stat = chunk:gsub("^\r\n","")
286 if field.headers["Content-Disposition"] then
287 if field.headers["Content-Disposition"]:match("^form%-data; ") then
288 field.name = field.headers["Content-Disposition"]:match('name="(.-)"')
289 field.file = field.headers["Content-Disposition"]:match('filename="(.+)"$')
293 if not field.headers["Content-Type"] then
294 field.headers["Content-Type"] = "text/plain"
310 local function snk( chunk )
312 tlen = tlen + ( chunk and #chunk or 0 )
314 if msg.env.CONTENT_LENGTH and tlen > msg.env.CONTENT_LENGTH then
315 return nil, "Message body size exceeds Content-Length"
318 if chunk and not lchunk then
319 lchunk = "\r\n" .. chunk
322 local data = lchunk .. ( chunk or "" )
323 local spos, epos, found
326 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
329 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
334 local predata = data:sub( 1, spos - 1 )
337 predata, eof = parse_headers( predata, field )
340 return nil, "Invalid MIME section header"
343 if not field.name then
344 return nil, "Invalid Content-Disposition header"
349 store( field.headers, predata, true )
353 field = { headers = { } }
354 found = found or true
356 data, eof = parse_headers( data:sub( epos + 1, #data ), field )
360 if field.file and filecb then
361 msg.params[field.name] = field.file
364 __initval( msg.params, field.name )
366 store = function( hdr, buf, eof )
367 __appendval( msg.params, field.name, buf )
377 lchunk = data:sub( #data - 78 + 1, #data )
378 data = data:sub( 1, #data - 78 )
380 if store and field and field.name then
381 store( field.headers, data, false )
383 return nil, "Invalid MIME section header"
386 lchunk, data = data, nil
390 lchunk, eof = parse_headers( data, field )
393 store( field.headers, lchunk, false )
394 lchunk, chunk = chunk, nil
402 return luci.ltn12.pump.all( src, snk )
406 -- Decode urlencoded data.
407 function urldecode_message_body( src, msg )
412 local function snk( chunk )
414 tlen = tlen + ( chunk and #chunk or 0 )
416 if msg.env.CONTENT_LENGTH and tlen > msg.env.CONTENT_LENGTH then
417 return nil, "Message body size exceeds Content-Length"
418 elseif tlen > HTTP_MAX_CONTENT then
419 return nil, "Message body size exceeds maximum allowed length"
422 if not lchunk and chunk then
426 local data = lchunk .. ( chunk or "&" )
430 spos, epos = data:find("^.-[;&]")
433 local pair = data:sub( spos, epos - 1 )
434 local key = pair:match("^(.-)=")
435 local val = pair:match("=(.*)$")
437 if key and #key > 0 then
438 __initval( msg.params, key )
439 __appendval( msg.params, key, val )
440 __finishval( msg.params, key, urldecode )
443 data = data:sub( epos + 1, #data )
453 return luci.ltn12.pump.all( src, snk )
457 -- Parse a http message header
458 function parse_message_header( source )
463 local sink = ltn12.sink.simplify(
465 return process_states['magic']( msg, chunk )
469 -- Pump input data...
473 ok, err = ltn12.pump.step( source, sink )
476 if not ok and err then
482 -- Process get parameters
483 if ( msg.request_method == "get" or msg.request_method == "post" ) and
484 msg.request_uri:match("?")
486 msg.params = urldecode_params( msg.request_uri )
491 -- Populate common environment variables
493 CONTENT_LENGTH = tonumber(msg.headers['Content-Length']);
494 CONTENT_TYPE = msg.headers['Content-Type'];
495 REQUEST_METHOD = msg.request_method:upper();
496 REQUEST_URI = msg.request_uri;
497 SCRIPT_NAME = msg.request_uri:gsub("?.+$","");
498 SCRIPT_FILENAME = ""; -- XXX implement me
499 SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version)
502 -- Populate HTTP_* environment variables
503 for i, hdr in ipairs( {
514 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
515 local val = msg.headers[hdr]
526 -- Parse a http message body
527 function parse_message_body( source, msg, filecb )
528 -- Is it multipart/mime ?
529 if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
530 msg.env.CONTENT_TYPE:match("^multipart/form%-data")
533 return mimedecode_message_body( source, msg, filecb )
535 -- Is it application/x-www-form-urlencoded ?
536 elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
537 msg.env.CONTENT_TYPE == "application/x-www-form-urlencoded"
539 return urldecode_message_body( source, msg, filecb )
542 -- Unhandled encoding
543 -- If a file callback is given then feed it chunk by chunk, else
544 -- store whole buffer in message.content
549 -- If we have a file callback then feed it
550 if type(filecb) == "function" then
553 -- ... else append to .content
556 msg.content_length = 0
558 sink = function( chunk )
559 if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then
561 msg.content = msg.content .. chunk
562 msg.content_length = msg.content_length + #chunk
566 return nil, "POST data exceeds maximum allowed length"
573 local ok, err = ltn12.pump.step( source, sink )
575 if not ok and err then
587 [301] = "Moved Permanently",
588 [304] = "Not Modified",
589 [400] = "Bad Request",
592 [405] = "Method Not Allowed",
593 [411] = "Length Required",
594 [412] = "Precondition Failed",
595 [500] = "Internal Server Error",
596 [503] = "Server Unavailable",