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
24 -- Decode an urlencoded string.
25 -- Returns the decoded value.
26 function urldecode( str, no_plus )
28 local function __chrdec( hex )
29 return string.char( tonumber( hex, 16 ) )
32 if type(str) == "string" then
34 str = str:gsub( "+", " " )
37 str = str:gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec )
44 -- Extract and split urlencoded data pairs, separated bei either "&" or ";" from given url.
45 -- Returns a table value with urldecoded values.
46 function urldecode_params( url, tbl )
48 local params = tbl or { }
51 url = url:gsub( "^.+%?([^?]+)", "%1" )
54 for pair in url:gmatch( "[^&;]+" ) do
57 local key = urldecode( pair:match("^([^=]+)") )
58 local val = urldecode( pair:match("^[^=]+=(.+)$") )
61 if type(key) == "string" and key:len() > 0 then
62 if type(val) ~= "string" then val = "" end
64 if not params[key] then
66 elseif type(params[key]) ~= "table" then
67 params[key] = { params[key], val }
69 table.insert( params[key], val )
78 -- Encode given string in urlencoded format.
79 -- Returns the encoded string.
80 function urlencode( str )
82 local function __chrenc( chr )
84 "%%%02x", string.byte( chr )
88 if type(str) == "string" then
90 "([^a-zA-Z0-9$_%-%.%+!*'(),])",
99 -- Encode given table to urlencoded string.
100 -- Returns the encoded string.
101 function urlencode_params( tbl )
104 for k, v in pairs(tbl) do
105 enc = enc .. ( enc and "&" or "" ) ..
106 urlencode(k) .. "=" ..
115 local function __initval( tbl, key )
116 local multival = ( key:sub( #key - 1, #key ) == "[]" )
119 if type(tbl[key]) == "table" then
120 table.insert( tbl[key], "" )
131 local function __appendval( tbl, key, multival, chunk )
133 tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk
135 tbl[key] = tbl[key] .. chunk
139 local function __finishval( tbl, key, multival, handler )
142 tbl[key][#tbl[key]] = handler( tbl[key][#tbl[key]] )
144 tbl[key] = handler( tbl[key] )
150 -- Table of our process states
151 local process_states = { }
153 -- Extract "magic", the first line of a http message.
154 -- Extracts the message type ("get", "post" or "response"), the requested uri
155 -- or the status code if the line descripes a http response.
156 process_states['magic'] = function( msg, chunk, err )
159 -- ignore empty lines before request
165 local method, uri, http_ver = chunk:match("^([A-Z]+) ([^ ]+) HTTP/([01]%.[019])$")
171 msg.request_method = method:lower()
172 msg.request_uri = uri
173 msg.http_version = tonumber( http_ver )
176 -- We're done, next state is header parsing
177 return true, function( chunk )
178 return process_states['headers']( msg, chunk )
184 local http_ver, code, message = chunk:match("^HTTP/([01]%.[019]) ([0-9]+) ([^\r\n]+)$")
189 msg.type = "response"
190 msg.status_code = code
191 msg.status_message = message
192 msg.http_version = tonumber( http_ver )
195 -- We're done, next state is header parsing
196 return true, function( chunk )
197 return process_states['headers']( msg, chunk )
204 return nil, "Invalid HTTP message magic"
208 -- Extract headers from given string.
209 process_states['headers'] = function( msg, chunk )
213 -- Look for a valid header format
214 local hdr, val = chunk:match( "^([A-Z][A-Za-z0-9%-_]+): +(.+)$" )
216 if type(hdr) == "string" and hdr:len() > 0 and
217 type(val) == "string" and val:len() > 0
219 msg.headers[hdr] = val
221 -- Valid header line, proceed
224 elseif #chunk == 0 then
225 -- Empty line, we won't accept data anymore
229 return nil, "Invalid HTTP header received"
232 return nil, "Unexpected EOF"
237 -- Find first MIME boundary
238 process_states['mime-init'] = function( msg, chunk, filecb )
241 if #chunk >= #msg.mime_boundary + 2 then
242 local boundary = chunk:sub( 1, #msg.mime_boundary + 4 )
244 if boundary == "--" .. msg.mime_boundary .. "\r\n" then
246 -- Store remaining data in buffer
247 msg._mimebuffer = chunk:sub( #msg.mime_boundary + 5, #chunk )
249 -- Switch to header processing state
250 return true, function( chunk )
251 return process_states['mime-headers']( msg, chunk, filecb )
254 return nil, "Invalid MIME boundary"
260 return nil, "Unexpected EOF"
265 -- Read MIME part headers
266 process_states['mime-headers'] = function( msg, chunk, filecb )
270 -- Combine look-behind buffer with current chunk
271 chunk = msg._mimebuffer .. chunk
273 if not msg._mimeheaders then
274 msg._mimeheaders = { }
277 local function __storehdr( k, v )
278 msg._mimeheaders[k] = v
282 -- Read all header lines
283 local ok, count = 1, 0
285 chunk, ok = chunk:gsub( "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n", __storehdr )
289 -- Headers processed, check for empty line
290 chunk, ok = chunk:gsub( "^\r\n", "" )
292 -- Store remaining buffer contents
293 msg._mimebuffer = chunk
298 -- When no Content-Type header is given assume text/plain
299 if not msg._mimeheaders['Content-Type'] then
300 msg._mimeheaders['Content-Type'] = 'text/plain'
303 -- Check Content-Disposition
304 if msg._mimeheaders['Content-Disposition'] then
305 -- Check for "form-data" token
306 if msg._mimeheaders['Content-Disposition']:match("^form%-data; ") then
307 -- Check for field name, filename
308 local field = msg._mimeheaders['Content-Disposition']:match('name="(.-)"')
309 local file = msg._mimeheaders['Content-Disposition']:match('filename="(.+)"$')
311 -- Is a file field and we have a callback
312 if file and filecb then
313 msg.params[field] = file
314 msg._mimecallback = function(chunk,eof)
318 headers = msg._mimeheaders
322 -- Treat as form field
324 local mv = __initval( msg.params, field )
325 msg._mimecallback = function(chunk,eof)
326 __appendval( msg.params, field, mv, chunk )
330 -- Header was valid, continue with mime-data
331 return true, function( chunk )
332 return process_states['mime-data']( msg, chunk, filecb )
335 -- Unknown Content-Disposition, abort
336 return nil, "Unexpected Content-Disposition MIME section header"
339 -- Content-Disposition is required, abort without
340 return nil, "Missing Content-Disposition MIME section header"
343 -- We parsed no headers yet and buffer is almost empty
344 elseif count > 0 or #chunk < 128 then
345 -- Keep feeding me with chunks
349 -- Buffer looks like garbage
350 return nil, "Malformed MIME section header"
352 return nil, "Unexpected EOF"
357 -- Read MIME part data
358 process_states['mime-data'] = function( msg, chunk, filecb )
362 -- Combine look-behind buffer with current chunk
363 local buffer = msg._mimebuffer .. chunk
365 -- Look for MIME boundary
366 local spos, epos = buffer:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
370 msg._mimecallback( buffer:sub( 1, spos - 1 ), true )
373 msg._mimebuffer = buffer:sub( epos + 1, #buffer )
375 -- Next state is mime-header processing
376 return true, function( chunk )
377 return process_states['mime-headers']( msg, chunk, filecb )
381 local spos, epos = buffer:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
385 msg._mimecallback( buffer:sub( 1, spos - 1 ), true )
387 -- We processed the final MIME boundary, cleanup
388 msg._mimebuffer = nil
389 msg._mimeheaders = nil
390 msg._mimecallback = nil
392 -- We won't accept data anymore
395 -- We're somewhere within a data section and our buffer is full
396 if #buffer > #chunk then
397 -- Flush buffered data
398 msg._mimecallback( buffer:sub( 1, #buffer - #chunk ), false )
401 msg._mimebuffer = buffer:sub( #buffer - #chunk + 1, #buffer )
403 -- Buffer is not full yet, append new data
405 msg._mimebuffer = buffer
413 return nil, "Unexpected EOF"
418 -- Init urldecoding stream
419 process_states['urldecode-init'] = function( msg, chunk, filecb )
423 -- Check for Content-Length
424 if msg.env.CONTENT_LENGTH then
425 msg.content_length = tonumber(msg.env.CONTENT_LENGTH)
427 if msg.content_length <= HTTP_MAX_CONTENT then
429 msg._urldecbuffer = chunk
430 msg._urldeclength = 0
432 -- Switch to urldecode-key state
433 return true, function(chunk)
434 return process_states['urldecode-key']( msg, chunk, filecb )
437 return nil, "Request exceeds maximum allowed size"
440 return nil, "Missing Content-Length header"
443 return nil, "Unexpected EOF"
448 -- Process urldecoding stream, read and validate parameter key
449 process_states['urldecode-key'] = function( msg, chunk, filecb )
452 -- Prevent oversized requests
453 if msg._urldeclength >= msg.content_length then
454 return nil, "Request exceeds maximum allowed size"
457 -- Combine look-behind buffer with current chunk
458 local buffer = msg._urldecbuffer .. chunk
459 local spos, epos = buffer:find("=")
464 -- Check that key doesn't exceed maximum allowed key length
465 if ( spos - 1 ) <= HTTP_URLENC_MAXKEYLEN then
466 local key = urldecode( buffer:sub( 1, spos - 1 ) )
469 msg._urldeclength = msg._urldeclength + epos
470 msg._urldecbuffer = buffer:sub( epos + 1, #buffer )
472 -- Use file callback or store values inside msg.params
474 msg._urldeccallback = function( chunk, eof )
475 filecb( field, chunk, eof )
478 local mv = __initval( msg.params, key )
479 msg._urldeccallback = function( chunk, eof )
480 __appendval( msg.params, key, mv, chunk )
482 -- FIXME: Use a filter
484 __finishval( msg.params, key, mv, urldecode )
489 -- Proceed with urldecode-value state
490 return true, function( chunk )
491 return process_states['urldecode-value']( msg, chunk, filecb )
494 return nil, "POST parameter exceeds maximum allowed length"
497 return nil, "POST data exceeds maximum allowed length"
500 return nil, "Unexpected EOF"
505 -- Process urldecoding stream, read parameter value
506 process_states['urldecode-value'] = function( msg, chunk, filecb )
510 -- Combine look-behind buffer with current chunk
511 local buffer = msg._urldecbuffer .. chunk
515 -- Compare processed length
516 if msg._urldeclength == msg.content_length then
518 msg._urldeclength = nil
519 msg._urldecbuffer = nil
520 msg._urldeccallback = nil
522 -- We won't accept data anymore
525 return nil, "Content-Length mismatch"
529 -- Check for end of value
530 local spos, epos = buffer:find("[&;]")
533 -- Flush buffer, send eof
534 msg._urldeccallback( buffer:sub( 1, spos - 1 ), true )
535 msg._urldecbuffer = buffer:sub( epos + 1, #buffer )
536 msg._urldeclength = msg._urldeclength + epos
538 -- Back to urldecode-key state
539 return true, function( chunk )
540 return process_states['urldecode-key']( msg, chunk, filecb )
543 -- We're somewhere within a data section and our buffer is full
544 if #buffer > #chunk then
545 -- Flush buffered data
546 msg._urldeccallback( buffer:sub( 1, #buffer - #chunk ), false )
549 msg._urldeclength = msg._urldeclength + #buffer - #chunk
550 msg._urldecbuffer = buffer:sub( #buffer - #chunk + 1, #buffer )
552 -- Buffer is not full yet, append new data
554 msg._urldecbuffer = buffer
562 msg._urldeccallback( "", true )
568 -- Creates a header source from a given socket
569 function header_source( sock )
570 return ltn12.source.simplify( function()
572 local chunk, err, part = sock:receive("*l")
576 if err ~= "timeout" then
578 and "Line exceeds maximum allowed length"
585 elseif chunk ~= nil then
588 chunk = chunk:gsub("\r$","")
596 -- Decode MIME encoded data.
597 function mimedecode_message_body( source, msg, filecb )
599 -- Find mime boundary
600 if msg and msg.env.CONTENT_TYPE then
602 local bound = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)")
605 msg.mime_boundary = bound
607 return nil, "No MIME boundary found or invalid content type given"
611 -- Create an initial LTN12 sink
612 -- The whole MIME parsing process is implemented as fancy sink, sinks replace themself
613 -- depending on current processing state (init, header, data). Return the initial state.
614 local sink = ltn12.sink.simplify(
616 return process_states['mime-init']( msg, chunk, filecb )
620 -- Create a throttling LTN12 source
621 -- Frequent state switching in the mime parsing process leads to unwanted buffer aggregation.
622 -- This source checks wheather there's still data in our internal read buffer and returns an
623 -- empty string if there's already enough data in the processing queue. If the internal buffer
624 -- runs empty we're calling the original source to get the next chunk of data.
625 local tsrc = function()
627 -- XXX: we schould propably keep the maximum buffer size in sync with
628 -- the blocksize of our original source... but doesn't really matter
629 if msg._mimebuffer ~= null and #msg._mimebuffer > 256 then
636 -- Pump input data...
639 local ok, err = ltn12.pump.step( tsrc, sink )
642 if not ok and err then
653 -- Decode urlencoded data.
654 function urldecode_message_body( source, msg )
656 -- Create an initial LTN12 sink
657 -- Return the initial state.
658 local sink = ltn12.sink.simplify(
660 return process_states['urldecode-init']( msg, chunk )
664 -- Create a throttling LTN12 source
665 -- See explaination in mimedecode_message_body().
666 local tsrc = function()
667 if msg._urldecbuffer ~= null and #msg._urldecbuffer > 0 then
674 -- Pump input data...
677 local ok, err = ltn12.pump.step( tsrc, sink )
680 if not ok and err then
691 -- Parse a http message header
692 function parse_message_header( source )
697 local sink = ltn12.sink.simplify(
699 return process_states['magic']( msg, chunk )
703 -- Pump input data...
707 ok, err = ltn12.pump.step( source, sink )
710 if not ok and err then
716 -- Process get parameters
717 if ( msg.request_method == "get" or msg.request_method == "post" ) and
718 msg.request_uri:match("?")
720 msg.params = urldecode_params( msg.request_uri )
725 -- Populate common environment variables
727 CONTENT_LENGTH = msg.headers['Content-Length'];
728 CONTENT_TYPE = msg.headers['Content-Type'];
729 REQUEST_METHOD = msg.request_method:upper();
730 REQUEST_URI = msg.request_uri;
731 SCRIPT_NAME = msg.request_uri:gsub("?.+$","");
732 SCRIPT_FILENAME = ""; -- XXX implement me
733 SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version)
736 -- Populate HTTP_* environment variables
737 for i, hdr in ipairs( {
748 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
749 local val = msg.headers[hdr]
760 -- Parse a http message body
761 function parse_message_body( source, msg, filecb )
762 -- Is it multipart/mime ?
763 if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
764 msg.env.CONTENT_TYPE:match("^multipart/form%-data")
767 return mimedecode_message_body( source, msg, filecb )
769 -- Is it application/x-www-form-urlencoded ?
770 elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
771 msg.env.CONTENT_TYPE == "application/x-www-form-urlencoded"
773 return urldecode_message_body( source, msg, filecb )
776 -- Unhandled encoding
777 -- If a file callback is given then feed it chunk by chunk, else
778 -- store whole buffer in message.content
783 -- If we have a file callback then feed it
784 if type(filecb) == "function" then
787 -- ... else append to .content
790 msg.content_length = 0
792 sink = function( chunk )
793 if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then
795 msg.content = msg.content .. chunk
796 msg.content_length = msg.content_length + #chunk
800 return nil, "POST data exceeds maximum allowed length"
807 local ok, err = ltn12.pump.step( source, sink )
809 if not ok and err then
821 [301] = "Moved Permanently",
822 [304] = "Not Modified",
823 [400] = "Bad Request",
826 [405] = "Method Not Allowed",
827 [411] = "Length Required",
828 [412] = "Precondition Failed",
829 [500] = "Internal Server Error",
830 [503] = "Server Unavailable",