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"
275 local function parse_headers( chunk, field )
279 chunk, stat = chunk:gsub(
280 "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n",
288 chunk, stat = chunk:gsub("^\r\n","")
292 if field.headers["Content-Disposition"] then
293 if field.headers["Content-Disposition"]:match("^form%-data; ") then
294 field.name = field.headers["Content-Disposition"]:match('name="(.-)"')
295 field.file = field.headers["Content-Disposition"]:match('filename="(.+)"$')
299 if not field.headers["Content-Type"] then
300 field.headers["Content-Type"] = "text/plain"
303 if field.name and field.file and filecb then
304 __initval( msg.params, field.name )
305 __appendval( msg.params, field.name, field.file )
308 elseif field.name then
309 __initval( msg.params, field.name )
311 store = function( hdr, buf, eof )
312 __appendval( msg.params, field.name, buf )
324 local function snk( chunk )
326 tlen = tlen + ( chunk and #chunk or 0 )
328 if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
329 return nil, "Message body size exceeds Content-Length"
332 if chunk and not lchunk then
333 lchunk = "\r\n" .. chunk
336 local data = lchunk .. ( chunk or "" )
337 local spos, epos, found
340 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
343 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
348 local predata = data:sub( 1, spos - 1 )
351 predata, eof = parse_headers( predata, field )
354 return nil, "Invalid MIME section header"
355 elseif not field.name then
356 return nil, "Invalid Content-Disposition header"
361 store( field.headers, predata, true )
365 field = { headers = { } }
366 found = found or true
368 data, eof = parse_headers( data:sub( epos + 1, #data ), field )
375 lchunk = data:sub( #data - 78 + 1, #data )
376 data = data:sub( 1, #data - 78 )
379 store( field.headers, data, false )
381 return nil, "Invalid MIME section header"
384 lchunk, data = data, nil
388 lchunk, eof = parse_headers( data, field )
391 store( field.headers, lchunk, false )
392 lchunk, chunk = chunk, nil
400 return ltn12.pump.all( src, snk )
404 -- Decode urlencoded data.
405 function urldecode_message_body( src, msg )
410 local function snk( chunk )
412 tlen = tlen + ( chunk and #chunk or 0 )
414 if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
415 return nil, "Message body size exceeds Content-Length"
416 elseif tlen > HTTP_MAX_CONTENT then
417 return nil, "Message body size exceeds maximum allowed length"
420 if not lchunk and chunk then
424 local data = lchunk .. ( chunk or "&" )
428 spos, epos = data:find("^.-[;&]")
431 local pair = data:sub( spos, epos - 1 )
432 local key = pair:match("^(.-)=")
433 local val = pair:match("=(.*)$")
435 if key and #key > 0 then
436 __initval( msg.params, key )
437 __appendval( msg.params, key, val )
438 __finishval( msg.params, key, urldecode )
441 data = data:sub( epos + 1, #data )
451 return ltn12.pump.all( src, snk )
455 -- Parse a http message header
456 function parse_message_header( source )
461 local sink = ltn12.sink.simplify(
463 return process_states['magic']( msg, chunk )
467 -- Pump input data...
471 ok, err = ltn12.pump.step( source, sink )
474 if not ok and err then
480 -- Process get parameters
481 if ( msg.request_method == "get" or msg.request_method == "post" ) and
482 msg.request_uri:match("?")
484 msg.params = urldecode_params( msg.request_uri )
489 -- Populate common environment variables
491 CONTENT_LENGTH = msg.headers['Content-Length'];
492 CONTENT_TYPE = msg.headers['Content-Type'];
493 REQUEST_METHOD = msg.request_method:upper();
494 REQUEST_URI = msg.request_uri;
495 SCRIPT_NAME = msg.request_uri:gsub("?.+$","");
496 SCRIPT_FILENAME = ""; -- XXX implement me
497 SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version)
500 -- Populate HTTP_* environment variables
501 for i, hdr in ipairs( {
512 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
513 local val = msg.headers[hdr]
524 -- Parse a http message body
525 function parse_message_body( source, msg, filecb )
526 -- Is it multipart/mime ?
527 if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
528 msg.env.CONTENT_TYPE:match("^multipart/form%-data")
531 return mimedecode_message_body( source, msg, filecb )
533 -- Is it application/x-www-form-urlencoded ?
534 elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
535 msg.env.CONTENT_TYPE == "application/x-www-form-urlencoded"
537 return urldecode_message_body( source, msg, filecb )
540 -- Unhandled encoding
541 -- If a file callback is given then feed it chunk by chunk, else
542 -- store whole buffer in message.content
547 -- If we have a file callback then feed it
548 if type(filecb) == "function" then
551 -- ... else append to .content
554 msg.content_length = 0
556 sink = function( chunk )
557 if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then
559 msg.content = msg.content .. chunk
560 msg.content_length = msg.content_length + #chunk
564 return nil, "POST data exceeds maximum allowed length"
571 local ok, err = ltn12.pump.step( source, sink )
573 if not ok and err then
585 [301] = "Moved Permanently",
586 [304] = "Not Modified",
587 [400] = "Bad Request",
590 [405] = "Method Not Allowed",
591 [411] = "Length Required",
592 [412] = "Precondition Failed",
593 [500] = "Internal Server Error",
594 [503] = "Server Unavailable",