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*4 -- 4 kB maximum content size
21 HTTP_URLENC_MAXKEYLEN = 1024 -- maximum allowd size of urlencoded parameter names
22 TSRC_BLOCKSIZE = 2048 -- target block size for throttling sources
25 -- Decode an urlencoded string.
26 -- Returns the decoded value.
27 function urldecode( str, no_plus )
29 local function __chrdec( hex )
30 return string.char( tonumber( hex, 16 ) )
33 if type(str) == "string" then
35 str = str:gsub( "+", " " )
38 str = str:gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec )
45 -- Extract and split urlencoded data pairs, separated bei either "&" or ";" from given url.
46 -- Returns a table value with urldecoded values.
47 function urldecode_params( url, tbl )
49 local params = tbl or { }
52 url = url:gsub( "^.+%?([^?]+)", "%1" )
55 for pair in url:gmatch( "[^&;]+" ) do
58 local key = urldecode( pair:match("^([^=]+)") )
59 local val = urldecode( pair:match("^[^=]+=(.+)$") )
62 if type(key) == "string" and key:len() > 0 then
63 if type(val) ~= "string" then val = "" end
65 if not params[key] then
67 elseif type(params[key]) ~= "table" then
68 params[key] = { params[key], val }
70 table.insert( params[key], val )
79 -- Encode given string in urlencoded format.
80 -- Returns the encoded string.
81 function urlencode( str )
83 local function __chrenc( chr )
85 "%%%02x", string.byte( chr )
89 if type(str) == "string" then
91 "([^a-zA-Z0-9$_%-%.%+!*'(),])",
100 -- Encode given table to urlencoded string.
101 -- Returns the encoded string.
102 function urlencode_params( tbl )
105 for k, v in pairs(tbl) do
106 enc = enc .. ( enc and "&" or "" ) ..
107 urlencode(k) .. "=" ..
116 local function __initval( tbl, key )
117 if tbl[key] == nil then
119 elseif type(tbl[key]) == "string" then
120 tbl[key] = { tbl[key], "" }
122 table.insert( tbl[key], "" )
126 local function __appendval( tbl, key, chunk )
127 if type(tbl[key]) == "table" then
128 tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk
130 tbl[key] = tbl[key] .. chunk
134 local function __finishval( tbl, key, handler )
136 if type(tbl[key]) == "table" then
137 tbl[key][#tbl[key]] = handler( tbl[key][#tbl[key]] )
139 tbl[key] = handler( tbl[key] )
145 -- Table of our process states
146 local process_states = { }
148 -- Extract "magic", the first line of a http message.
149 -- Extracts the message type ("get", "post" or "response"), the requested uri
150 -- or the status code if the line descripes a http response.
151 process_states['magic'] = function( msg, chunk, err )
154 -- ignore empty lines before request
160 local method, uri, http_ver = chunk:match("^([A-Z]+) ([^ ]+) HTTP/([01]%.[019])$")
166 msg.request_method = method:lower()
167 msg.request_uri = uri
168 msg.http_version = tonumber( http_ver )
171 -- We're done, next state is header parsing
172 return true, function( chunk )
173 return process_states['headers']( msg, chunk )
179 local http_ver, code, message = chunk:match("^HTTP/([01]%.[019]) ([0-9]+) ([^\r\n]+)$")
184 msg.type = "response"
185 msg.status_code = code
186 msg.status_message = message
187 msg.http_version = tonumber( http_ver )
190 -- We're done, next state is header parsing
191 return true, function( chunk )
192 return process_states['headers']( msg, chunk )
199 return nil, "Invalid HTTP message magic"
203 -- Extract headers from given string.
204 process_states['headers'] = function( msg, chunk )
208 -- Look for a valid header format
209 local hdr, val = chunk:match( "^([A-Z][A-Za-z0-9%-_]+): +(.+)$" )
211 if type(hdr) == "string" and hdr:len() > 0 and
212 type(val) == "string" and val:len() > 0
214 msg.headers[hdr] = val
216 -- Valid header line, proceed
219 elseif #chunk == 0 then
220 -- Empty line, we won't accept data anymore
224 return nil, "Invalid HTTP header received"
227 return nil, "Unexpected EOF"
232 -- Init urldecoding stream
233 process_states['urldecode-init'] = function( msg, chunk, filecb )
237 -- Check for Content-Length
238 if msg.env.CONTENT_LENGTH then
239 msg.content_length = tonumber(msg.env.CONTENT_LENGTH)
241 if msg.content_length <= HTTP_MAX_CONTENT then
243 msg._urldecbuffer = chunk
244 msg._urldeclength = 0
246 -- Switch to urldecode-key state
247 return true, function(chunk)
248 return process_states['urldecode-key']( msg, chunk, filecb )
251 return nil, "Request exceeds maximum allowed size"
254 return nil, "Missing Content-Length header"
257 return nil, "Unexpected EOF"
262 -- Process urldecoding stream, read and validate parameter key
263 process_states['urldecode-key'] = function( msg, chunk, filecb )
266 -- Prevent oversized requests
267 if msg._urldeclength >= msg.content_length then
268 return nil, "Request exceeds maximum allowed size"
271 -- Combine look-behind buffer with current chunk
272 local buffer = msg._urldecbuffer .. chunk
273 local spos, epos = buffer:find("=")
278 -- Check that key doesn't exceed maximum allowed key length
279 if ( spos - 1 ) <= HTTP_URLENC_MAXKEYLEN then
280 local key = urldecode( buffer:sub( 1, spos - 1 ) )
283 msg._urldeclength = msg._urldeclength + epos
284 msg._urldecbuffer = buffer:sub( epos + 1, #buffer )
286 -- Use file callback or store values inside msg.params
288 msg._urldeccallback = function( chunk, eof )
289 filecb( field, chunk, eof )
292 __initval( msg.params, key )
294 msg._urldeccallback = function( chunk, eof )
295 __appendval( msg.params, key, chunk )
297 -- FIXME: Use a filter
299 __finishval( msg.params, key, urldecode )
304 -- Proceed with urldecode-value state
305 return true, function( chunk )
306 return process_states['urldecode-value']( msg, chunk, filecb )
309 return nil, "POST parameter exceeds maximum allowed length"
312 return nil, "POST data exceeds maximum allowed length"
315 return nil, "Unexpected EOF"
320 -- Process urldecoding stream, read parameter value
321 process_states['urldecode-value'] = function( msg, chunk, filecb )
325 -- Combine look-behind buffer with current chunk
326 local buffer = msg._urldecbuffer .. chunk
330 -- Compare processed length
331 if msg._urldeclength == msg.content_length then
333 msg._urldeclength = nil
334 msg._urldecbuffer = nil
335 msg._urldeccallback = nil
337 -- We won't accept data anymore
340 return nil, "Content-Length mismatch"
344 -- Check for end of value
345 local spos, epos = buffer:find("[&;]")
348 -- Flush buffer, send eof
349 msg._urldeccallback( buffer:sub( 1, spos - 1 ), true )
350 msg._urldecbuffer = buffer:sub( epos + 1, #buffer )
351 msg._urldeclength = msg._urldeclength + epos
353 -- Back to urldecode-key state
354 return true, function( chunk )
355 return process_states['urldecode-key']( msg, chunk, filecb )
358 -- We're somewhere within a data section and our buffer is full
359 if #buffer > #chunk then
360 -- Flush buffered data
361 msg._urldeccallback( buffer:sub( 1, #buffer - #chunk ), false )
364 msg._urldeclength = msg._urldeclength + #buffer - #chunk
365 msg._urldecbuffer = buffer:sub( #buffer - #chunk + 1, #buffer )
367 -- Buffer is not full yet, append new data
369 msg._urldecbuffer = buffer
377 msg._urldeccallback( "", true )
383 -- Creates a header source from a given socket
384 function header_source( sock )
385 return ltn12.source.simplify( function()
387 local chunk, err, part = sock:receive("*l")
391 if err ~= "timeout" then
393 and "Line exceeds maximum allowed length"
400 elseif chunk ~= nil then
403 chunk = chunk:gsub("\r$","")
411 -- Decode MIME encoded data.
412 function mimedecode_message_body( src, msg, filecb )
414 if msg and msg.env.CONTENT_TYPE then
415 msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$")
418 if not msg.mime_boundary then
419 return nil, "Invalid Content-Type found"
423 local function parse_headers( chunk, field )
427 chunk, stat = chunk:gsub(
428 "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n",
436 chunk, stat = chunk:gsub("^\r\n","")
440 if field.headers["Content-Disposition"] then
441 if field.headers["Content-Disposition"]:match("^form%-data; ") then
442 field.name = field.headers["Content-Disposition"]:match('name="(.-)"')
443 field.file = field.headers["Content-Disposition"]:match('filename="(.+)"$')
447 if not field.headers["Content-Type"] then
448 field.headers["Content-Type"] = "text/plain"
464 local function snk( chunk )
466 tlen = tlen + ( chunk and #chunk or 0 )
468 if msg.env.CONTENT_LENGTH and tlen > msg.env.CONTENT_LENGTH then
469 return nil, "Message body size exceeds Content-Length"
472 if chunk and not lchunk then
473 lchunk = "\r\n" .. chunk
476 local data = lchunk .. ( chunk or "" )
477 local spos, epos, found
480 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
483 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
488 local predata = data:sub( 1, spos - 1 )
491 predata, eof = parse_headers( predata, field )
494 return nil, "Invalid MIME section header"
497 if not field.name then
498 return nil, "Invalid Content-Disposition header"
503 store( field.headers, predata, true )
507 field = { headers = { } }
508 found = found or true
510 data, eof = parse_headers( data:sub( epos + 1, #data ), field )
514 if field.file and filecb then
515 msg.params[field.name] = field.file
518 __initval( msg.params, field.name )
520 store = function( hdr, buf, eof )
521 __appendval( msg.params, field.name, buf )
531 lchunk = data:sub( #data - 78 + 1, #data )
532 data = data:sub( 1, #data - 78 )
534 if store and field and field.name then
535 store( field.headers, data )
537 return nil, "Invalid MIME section header"
540 lchunk, data = data, nil
544 lchunk, eof = parse_headers( data, field )
547 store( field.headers, lchunk )
548 lchunk, chunk = chunk, nil
556 return luci.ltn12.pump.all( src, snk )
560 -- Decode urlencoded data.
561 function urldecode_message_body( source, msg )
563 -- Create an initial LTN12 sink
564 -- Return the initial state.
565 local sink = ltn12.sink.simplify(
567 return process_states['urldecode-init']( msg, chunk )
571 -- Create a throttling LTN12 source
572 -- See explaination in mimedecode_message_body().
573 local tsrc = function()
574 if msg._urldecbuffer ~= nil and #msg._urldecbuffer > 0 then
581 -- Pump input data...
584 local ok, err = ltn12.pump.step( tsrc, sink )
587 if not ok and err then
598 -- Parse a http message header
599 function parse_message_header( source )
604 local sink = ltn12.sink.simplify(
606 return process_states['magic']( msg, chunk )
610 -- Pump input data...
614 ok, err = ltn12.pump.step( source, sink )
617 if not ok and err then
623 -- Process get parameters
624 if ( msg.request_method == "get" or msg.request_method == "post" ) and
625 msg.request_uri:match("?")
627 msg.params = urldecode_params( msg.request_uri )
632 -- Populate common environment variables
634 CONTENT_LENGTH = tonumber(msg.headers['Content-Length']);
635 CONTENT_TYPE = msg.headers['Content-Type'];
636 REQUEST_METHOD = msg.request_method:upper();
637 REQUEST_URI = msg.request_uri;
638 SCRIPT_NAME = msg.request_uri:gsub("?.+$","");
639 SCRIPT_FILENAME = ""; -- XXX implement me
640 SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version)
643 -- Populate HTTP_* environment variables
644 for i, hdr in ipairs( {
655 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
656 local val = msg.headers[hdr]
667 -- Parse a http message body
668 function parse_message_body( source, msg, filecb )
669 -- Is it multipart/mime ?
670 if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
671 msg.env.CONTENT_TYPE:match("^multipart/form%-data")
674 return mimedecode_message_body( source, msg, filecb )
676 -- Is it application/x-www-form-urlencoded ?
677 elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
678 msg.env.CONTENT_TYPE == "application/x-www-form-urlencoded"
680 return urldecode_message_body( source, msg, filecb )
683 -- Unhandled encoding
684 -- If a file callback is given then feed it chunk by chunk, else
685 -- store whole buffer in message.content
690 -- If we have a file callback then feed it
691 if type(filecb) == "function" then
694 -- ... else append to .content
697 msg.content_length = 0
699 sink = function( chunk )
700 if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then
702 msg.content = msg.content .. chunk
703 msg.content_length = msg.content_length + #chunk
707 return nil, "POST data exceeds maximum allowed length"
714 local ok, err = ltn12.pump.step( source, sink )
716 if not ok and err then
728 [301] = "Moved Permanently",
729 [304] = "Not Modified",
730 [400] = "Bad Request",
733 [405] = "Method Not Allowed",
734 [411] = "Length Required",
735 [412] = "Precondition Failed",
736 [500] = "Internal Server Error",
737 [503] = "Server Unavailable",