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 )
28 local function __chrdec( hex )
29 return string.char( tonumber( hex, 16 ) )
32 if type(str) == "string" then
33 str = str:gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec )
40 -- Extract and split urlencoded data pairs, separated bei either "&" or ";" from given url.
41 -- Returns a table value with urldecoded values.
42 function urldecode_params( url, tbl )
44 local params = tbl or { }
47 url = url:gsub( "^.+%?([^?]+)", "%1" )
50 for pair in url:gmatch( "[^&;]+" ) do
53 local key = urldecode( pair:match("^([^=]+)") )
54 local val = urldecode( pair:match("^[^=]+=(.+)$") )
57 if type(key) == "string" and key:len() > 0 then
58 if type(val) ~= "string" then val = "" end
60 if not params[key] then
62 elseif type(params[key]) ~= "table" then
63 params[key] = { params[key], val }
65 table.insert( params[key], val )
74 -- Encode given string in urlencoded format.
75 -- Returns the encoded string.
76 function urlencode( str )
78 local function __chrenc( chr )
80 "%%%02x", string.byte( chr )
84 if type(str) == "string" then
86 "([^a-zA-Z0-9$_%-%.%+!*'(),])",
95 -- Encode given table to urlencoded string.
96 -- Returns the encoded string.
97 function urlencode_params( tbl )
100 for k, v in pairs(tbl) do
101 enc = enc .. ( enc and "&" or "" ) ..
102 urlencode(k) .. "=" ..
110 -- Table of our process states
111 local process_states = { }
113 -- Extract "magic", the first line of a http message.
114 -- Extracts the message type ("get", "post" or "response"), the requested uri
115 -- or the status code if the line descripes a http response.
116 process_states['magic'] = function( msg, chunk, err )
119 -- ignore empty lines before request
125 local method, uri, http_ver = chunk:match("^([A-Z]+) ([^ ]+) HTTP/([01]%.[019])$")
131 msg.request_method = method:lower()
132 msg.request_uri = uri
133 msg.http_version = tonumber( http_ver )
136 -- We're done, next state is header parsing
137 return true, function( chunk )
138 return process_states['headers']( msg, chunk )
144 local http_ver, code, message = chunk:match("^HTTP/([01]%.[019]) ([0-9]+) ([^\r\n]+)$")
149 msg.type = "response"
150 msg.status_code = code
151 msg.status_message = message
152 msg.http_version = tonumber( http_ver )
155 -- We're done, next state is header parsing
156 return true, function( chunk )
157 return process_states['headers']( msg, chunk )
164 return nil, "Invalid HTTP message magic"
168 -- Extract headers from given string.
169 process_states['headers'] = function( msg, chunk )
173 -- Look for a valid header format
174 local hdr, val = chunk:match( "^([A-Z][A-Za-z0-9%-_]+): +(.+)$" )
176 if type(hdr) == "string" and hdr:len() > 0 and
177 type(val) == "string" and val:len() > 0
179 msg.headers[hdr] = val
181 -- Valid header line, proceed
184 elseif #chunk == 0 then
185 -- Empty line, we won't accept data anymore
189 return nil, "Invalid HTTP header received"
192 return nil, "Unexpected EOF"
197 -- Find first MIME boundary
198 process_states['mime-init'] = function( msg, chunk, filecb )
201 if #chunk >= #msg.mime_boundary + 2 then
202 local boundary = chunk:sub( 1, #msg.mime_boundary + 4 )
204 if boundary == "--" .. msg.mime_boundary .. "\r\n" then
206 -- Store remaining data in buffer
207 msg._mimebuffer = chunk:sub( #msg.mime_boundary + 5, #chunk )
209 -- Switch to header processing state
210 return true, function( chunk )
211 return process_states['mime-headers']( msg, chunk, filecb )
214 return nil, "Invalid MIME boundary"
220 return nil, "Unexpected EOF"
225 -- Read MIME part headers
226 process_states['mime-headers'] = function( msg, chunk, filecb )
230 -- Combine look-behind buffer with current chunk
231 chunk = msg._mimebuffer .. chunk
233 if not msg._mimeheaders then
234 msg._mimeheaders = { }
237 local function __storehdr( k, v )
238 msg._mimeheaders[k] = v
242 -- Read all header lines
243 local ok, count = 1, 0
245 chunk, ok = chunk:gsub( "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n", __storehdr )
249 -- Headers processed, check for empty line
250 chunk, ok = chunk:gsub( "^\r\n", "" )
252 -- Store remaining buffer contents
253 msg._mimebuffer = chunk
258 -- When no Content-Type header is given assume text/plain
259 if not msg._mimeheaders['Content-Type'] then
260 msg._mimeheaders['Content-Type'] = 'text/plain'
263 -- Check Content-Disposition
264 if msg._mimeheaders['Content-Disposition'] then
265 -- Check for "form-data" token
266 if msg._mimeheaders['Content-Disposition']:match("^form%-data; ") then
267 -- Check for field name, filename
268 local field = msg._mimeheaders['Content-Disposition']:match('name="(.-)"')
269 local file = msg._mimeheaders['Content-Disposition']:match('filename="(.+)"$')
271 -- Is a file field and we have a callback
272 if file and filecb then
273 msg.params[field] = file
274 msg._mimecallback = function(chunk,eof)
278 headers = msg._mimeheaders
282 -- Treat as form field
284 msg.params[field] = ""
285 msg._mimecallback = function(chunk,eof)
286 msg.params[field] = msg.params[field] .. chunk
290 -- Header was valid, continue with mime-data
291 return true, function( chunk )
292 return process_states['mime-data']( msg, chunk, filecb )
295 -- Unknown Content-Disposition, abort
296 return nil, "Unexpected Content-Disposition MIME section header"
299 -- Content-Disposition is required, abort without
300 return nil, "Missing Content-Disposition MIME section header"
303 -- We parsed no headers yet and buffer is almost empty
304 elseif count > 0 or #chunk < 128 then
305 -- Keep feeding me with chunks
309 -- Buffer looks like garbage
310 return nil, "Malformed MIME section header"
312 return nil, "Unexpected EOF"
317 -- Read MIME part data
318 process_states['mime-data'] = function( msg, chunk, filecb )
322 -- Combine look-behind buffer with current chunk
323 local buffer = msg._mimebuffer .. chunk
325 -- Look for MIME boundary
326 local spos, epos = buffer:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true )
330 msg._mimecallback( buffer:sub( 1, spos - 1 ), true )
333 msg._mimebuffer = buffer:sub( epos + 1, #buffer )
335 -- Next state is mime-header processing
336 return true, function( chunk )
337 return process_states['mime-headers']( msg, chunk, filecb )
341 local spos, epos = buffer:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true )
345 msg._mimecallback( buffer:sub( 1, spos - 1 ), true )
347 -- We processed the final MIME boundary, cleanup
348 msg._mimebuffer = nil
349 msg._mimeheaders = nil
350 msg._mimecallback = nil
352 -- We won't accept data anymore
355 -- We're somewhere within a data section and our buffer is full
356 if #buffer > #chunk then
357 -- Flush buffered data
358 msg._mimecallback( buffer:sub( 1, #buffer - #chunk ), false )
361 msg._mimebuffer = buffer:sub( #buffer - #chunk + 1, #buffer )
363 -- Buffer is not full yet, append new data
365 msg._mimebuffer = buffer
373 return nil, "Unexpected EOF"
378 -- Init urldecoding stream
379 process_states['urldecode-init'] = function( msg, chunk, filecb )
383 -- Check for Content-Length
384 if msg.env.CONTENT_LENGTH then
385 msg.content_length = tonumber(msg.env.CONTENT_LENGTH)
387 if msg.content_length <= HTTP_MAX_CONTENT then
389 msg._urldecbuffer = chunk
390 msg._urldeclength = 0
392 -- Switch to urldecode-key state
393 return true, function(chunk)
394 return process_states['urldecode-key']( msg, chunk, filecb )
397 return nil, "Request exceeds maximum allowed size"
400 return nil, "Missing Content-Length header"
403 return nil, "Unexpected EOF"
408 -- Process urldecoding stream, read and validate parameter key
409 process_states['urldecode-key'] = function( msg, chunk, filecb )
412 -- Prevent oversized requests
413 if msg._urldeclength >= msg.content_length then
414 return nil, "Request exceeds maximum allowed size"
417 -- Combine look-behind buffer with current chunk
418 local buffer = msg._urldecbuffer .. chunk
419 local spos, epos = buffer:find("=")
424 -- Check that key doesn't exceed maximum allowed key length
425 if ( spos - 1 ) <= HTTP_URLENC_MAXKEYLEN then
426 local key = urldecode( buffer:sub( 1, spos - 1 ) )
430 msg._urldeclength = msg._urldeclength + epos
431 msg._urldecbuffer = buffer:sub( epos + 1, #buffer )
433 -- Use file callback or store values inside msg.params
435 msg._urldeccallback = function( chunk, eof )
436 filecb( field, chunk, eof )
439 msg._urldeccallback = function( chunk, eof )
440 msg.params[key] = msg.params[key] .. chunk
442 -- FIXME: Use a filter
444 msg.params[key] = urldecode( msg.params[key] )
449 -- Proceed with urldecode-value state
450 return true, function( chunk )
451 return process_states['urldecode-value']( msg, chunk, filecb )
454 return nil, "POST parameter exceeds maximum allowed length"
457 return nil, "POST data exceeds maximum allowed length"
460 return nil, "Unexpected EOF"
465 -- Process urldecoding stream, read parameter value
466 process_states['urldecode-value'] = function( msg, chunk, filecb )
470 -- Combine look-behind buffer with current chunk
471 local buffer = msg._urldecbuffer .. chunk
475 -- Compare processed length
476 if msg._urldeclength == msg.content_length then
478 msg._urldeclength = nil
479 msg._urldecbuffer = nil
480 msg._urldeccallback = nil
482 -- We won't accept data anymore
485 return nil, "Content-Length mismatch"
489 -- Check for end of value
490 local spos, epos = buffer:find("[&;]")
493 -- Flush buffer, send eof
494 msg._urldeccallback( buffer:sub( 1, spos - 1 ), true )
495 msg._urldecbuffer = buffer:sub( epos + 1, #buffer )
496 msg._urldeclength = msg._urldeclength + epos
498 -- Back to urldecode-key state
499 return true, function( chunk )
500 return process_states['urldecode-key']( msg, chunk, filecb )
503 -- We're somewhere within a data section and our buffer is full
504 if #buffer > #chunk then
505 -- Flush buffered data
506 msg._urldeccallback( buffer:sub( 1, #buffer - #chunk ), false )
509 msg._urldeclength = msg._urldeclength + #buffer - #chunk
510 msg._urldecbuffer = buffer:sub( #buffer - #chunk + 1, #buffer )
512 -- Buffer is not full yet, append new data
514 msg._urldecbuffer = buffer
522 msg._urldeccallback( "", true )
528 -- Creates a header source from a given socket
529 function header_source( sock )
530 return ltn12.source.simplify( function()
532 local chunk, err, part = sock:receive("*l")
536 if err ~= "timeout" then
538 and "Line exceeds maximum allowed length"
545 elseif chunk ~= nil then
548 chunk = chunk:gsub("\r$","")
556 -- Decode MIME encoded data.
557 function mimedecode_message_body( source, msg, filecb )
559 -- Find mime boundary
560 if msg and msg.env.CONTENT_TYPE then
562 local bound = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)")
565 msg.mime_boundary = bound
567 return nil, "No MIME boundary found or invalid content type given"
571 -- Create an initial LTN12 sink
572 -- The whole MIME parsing process is implemented as fancy sink, sinks replace themself
573 -- depending on current processing state (init, header, data). Return the initial state.
574 local sink = ltn12.sink.simplify(
576 return process_states['mime-init']( msg, chunk, filecb )
580 -- Create a throttling LTN12 source
581 -- Frequent state switching in the mime parsing process leads to unwanted buffer aggregation.
582 -- This source checks wheather there's still data in our internal read buffer and returns an
583 -- empty string if there's already enough data in the processing queue. If the internal buffer
584 -- runs empty we're calling the original source to get the next chunk of data.
585 local tsrc = function()
587 -- XXX: we schould propably keep the maximum buffer size in sync with
588 -- the blocksize of our original source... but doesn't really matter
589 if msg._mimebuffer ~= null and #msg._mimebuffer > 256 then
596 -- Pump input data...
599 local ok, err = ltn12.pump.step( tsrc, sink )
602 if not ok and err then
613 -- Decode urlencoded data.
614 function urldecode_message_body( source, msg )
616 -- Create an initial LTN12 sink
617 -- Return the initial state.
618 local sink = ltn12.sink.simplify(
620 return process_states['urldecode-init']( msg, chunk )
624 -- Create a throttling LTN12 source
625 -- See explaination in mimedecode_message_body().
626 local tsrc = function()
627 if msg._urldecbuffer ~= null and #msg._urldecbuffer > 0 then
634 -- Pump input data...
637 local ok, err = ltn12.pump.step( tsrc, sink )
640 if not ok and err then
651 -- Parse a http message header
652 function parse_message_header( source )
657 local sink = ltn12.sink.simplify(
659 return process_states['magic']( msg, chunk )
663 -- Pump input data...
667 ok, err = ltn12.pump.step( source, sink )
670 if not ok and err then
676 -- Process get parameters
677 if ( msg.request_method == "get" or msg.request_method == "post" ) and
678 msg.request_uri:match("?")
680 msg.params = urldecode_params( msg.request_uri )
685 -- Populate common environment variables
687 CONTENT_LENGTH = msg.headers['Content-Length'];
688 CONTENT_TYPE = msg.headers['Content-Type'];
689 REQUEST_METHOD = msg.request_method:upper();
690 REQUEST_URI = msg.request_uri;
691 SCRIPT_NAME = msg.request_uri:gsub("?.+$","");
692 SCRIPT_FILENAME = ""; -- XXX implement me
693 SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version)
696 -- Populate HTTP_* environment variables
697 for i, hdr in ipairs( {
708 local var = 'HTTP_' .. hdr:upper():gsub("%-","_")
709 local val = msg.headers[hdr]
720 -- Parse a http message body
721 function parse_message_body( source, msg, filecb )
722 -- Is it multipart/mime ?
723 if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
724 msg.env.CONTENT_TYPE:match("^multipart/form%-data")
727 return mimedecode_message_body( source, msg, filecb )
729 -- Is it application/x-www-form-urlencoded ?
730 elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and
731 msg.env.CONTENT_TYPE == "application/x-www-form-urlencoded"
733 return urldecode_message_body( source, msg, filecb )
736 -- Unhandled encoding
737 -- If a file callback is given then feed it chunk by chunk, else
738 -- store whole buffer in message.content
743 -- If we have a file callback then feed it
744 if type(filecb) == "function" then
747 -- ... else append to .content
750 msg.content_length = 0
752 sink = function( chunk )
753 if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then
755 msg.content = msg.content .. chunk
756 msg.content_length = msg.content_length + #chunk
760 return nil, "POST data exceeds maximum allowed length"
767 local ok, err = ltn12.pump.step( source, sink )
769 if not ok and err then
781 [301] = "Moved Permanently",
782 [304] = "Not Modified",
783 [400] = "Bad Request",
786 [405] = "Method Not Allowed",
787 [411] = "Length Required",
788 [412] = "Precondition Failed",
789 [500] = "Internal Server Error",
790 [503] = "Server Unavailable",