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"
458 local field = { headers = { } }
463 local function snk( chunk )
465 if chunk and not lchunk then
466 lchunk = "\r\n" .. chunk
469 local data = lchunk .. ( chunk or "" )
470 local spos, epos, found
473 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
476 spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
481 local predata = data:sub( 1, spos - 1 )
484 predata, eof = parse_headers( predata, field )
487 return nil, "Invalid MIME section header"
490 if not field.name then
491 return nil, "Invalid Content-Disposition header"
496 store( field.headers, predata, true )
500 field = { headers = { } }
501 found = found or true
503 data, eof = parse_headers( data:sub( epos + 1, #data ), field )
507 if field.file and filecb then
508 msg.params[field.name] = field.file
511 __initval( msg.params, field.name )
513 store = function( hdr, buf, eof )
514 __appendval( msg.params, field.name, buf )
524 lchunk = data:sub( #data - 78 + 1, #data )
525 data = data:sub( 1, #data - 78 )
527 store( field.headers, data )
529 lchunk, data = data, nil
533 lchunk, eof = parse_headers( data, field )
536 store( field.headers, lchunk )
537 lchunk, chunk = chunk, nil
545 return luci.ltn12.pump.all( src, snk )
549 -- Decode urlencoded data.
550 function urldecode_message_body( source, msg )
552 -- Create an initial LTN12 sink
553 -- Return the initial state.
554 local sink = ltn12.sink.simplify(
556 return process_states['urldecode-init']( msg, chunk )
560 -- Create a throttling LTN12 source
561 -- See explaination in mimedecode_message_body().
562 local tsrc = function()
563 if msg._urldecbuffer ~= nil and #msg._urldecbuffer > 0 then
570 -- Pump input data...
573 local ok, err = ltn12.pump.step( tsrc, sink )
576 if not ok and err then
587 -- Parse a http message header
588 function parse_message_header( source )
593 local sink = ltn12.sink.simplify(
595 return process_states['magic']( msg, chunk )
599 -- Pump input data...
603 ok, err = ltn12.pump.step( source, sink )
606 if not ok and err then
612 -- Process get parameters
613 if ( msg.request_method == "get" or msg.request_method == "post" ) and
614 msg.request_uri:match("?")
616 msg.params = urldecode_params( msg.request_uri )
621 -- Populate common environment variables
623 CONTENT_LENGTH = msg.headers['Content-Length'];
624 CONTENT_TYPE = msg.headers['Content-Type'];
625 REQUEST_METHOD = msg.request_method:upper();
626 REQUEST_URI = msg.request_uri;
627 SCRIPT_NAME = msg.request_uri:gsub("?.+$","");
628 SCRIPT_FILENAME = ""; -- XXX implement me
629 SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version)
632 -- Populate HTTP_* environment variables
633 for i, hdr in ipairs( {
644 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
645 local val = msg.headers[hdr]
656 -- Parse a http message body
657 function parse_message_body( source, msg, filecb )
658 -- Is it multipart/mime ?
659 if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
660 msg.env.CONTENT_TYPE:match("^multipart/form%-data")
663 return mimedecode_message_body( source, msg, filecb )
665 -- Is it application/x-www-form-urlencoded ?
666 elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
667 msg.env.CONTENT_TYPE == "application/x-www-form-urlencoded"
669 return urldecode_message_body( source, msg, filecb )
672 -- Unhandled encoding
673 -- If a file callback is given then feed it chunk by chunk, else
674 -- store whole buffer in message.content
679 -- If we have a file callback then feed it
680 if type(filecb) == "function" then
683 -- ... else append to .content
686 msg.content_length = 0
688 sink = function( chunk )
689 if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then
691 msg.content = msg.content .. chunk
692 msg.content_length = msg.content_length + #chunk
696 return nil, "POST data exceeds maximum allowed length"
703 local ok, err = ltn12.pump.step( source, sink )
705 if not ok and err then
717 [301] = "Moved Permanently",
718 [304] = "Not Modified",
719 [400] = "Bad Request",
722 [405] = "Method Not Allowed",
723 [411] = "Length Required",
724 [412] = "Precondition Failed",
725 [500] = "Internal Server Error",
726 [503] = "Server Unavailable",