From: Jo-Philipp Wich Date: Thu, 19 Jun 2008 02:53:09 +0000 (+0000) Subject: * luci/libs: moved http.protocol from libs/web to libs/httpd, rewrote http.protocol... X-Git-Tag: 0.8.0~795 X-Git-Url: http://git.lede-project.org./?a=commitdiff_plain;h=f712a1f2c284bfecff775ceff60c0e7a182ed114;p=project%2Fluci.git * luci/libs: moved http.protocol from libs/web to libs/httpd, rewrote http.protocol to rely on LTN12 chains --- diff --git a/libs/httpd/luasrc/http/protocol.lua b/libs/httpd/luasrc/http/protocol.lua new file mode 100644 index 0000000000..01d3128b25 --- /dev/null +++ b/libs/httpd/luasrc/http/protocol.lua @@ -0,0 +1,754 @@ +--[[ + +HTTP protocol implementation for LuCI +(c) 2008 Freifunk Leipzig / Jo-Philipp Wich + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +module("luci.http.protocol", package.seeall) + +require("ltn12") +require("luci.util") + +HTTP_MAX_CONTENT = 1024*4 -- 4 kB maximum content size +HTTP_URLENC_MAXKEYLEN = 1024 -- maximum allowd size of urlencoded parameter names + + +-- Decode an urlencoded string. +-- Returns the decoded value. +function urldecode( str ) + + local function __chrdec( hex ) + return string.char( tonumber( hex, 16 ) ) + end + + if type(str) == "string" then + str = str:gsub( "+", " " ):gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec ) + end + + return str +end + + +-- Extract and split urlencoded data pairs, separated bei either "&" or ";" from given url. +-- Returns a table value with urldecoded values. +function urldecode_params( url, tbl ) + + local params = tbl or { } + + if url:find("?") then + url = url:gsub( "^.+%?([^?]+)", "%1" ) + end + + for i, pair in ipairs(luci.util.split( url, "[&;]+", nil, true )) do + + -- find key and value + local key = urldecode( pair:match("^([^=]+)") ) + local val = urldecode( pair:match("^[^=]+=(.+)$") ) + + -- store + if type(key) == "string" and key:len() > 0 then + if type(val) ~= "string" then val = "" end + + if not params[key] then + params[key] = val + elseif type(params[key]) ~= "table" then + params[key] = { params[key], val } + else + table.insert( params[key], val ) + end + end + end + + return params +end + + +-- Encode given string in urlencoded format. +-- Returns the encoded string. +function urlencode( str ) + + local function __chrenc( chr ) + return string.format( + "%%%02x", string.byte( chr ) + ) + end + + if type(str) == "string" then + str = str:gsub( + "([^a-zA-Z0-9$_%-%.+!*'(),])", + __chrenc + ) + end + + return str +end + + +-- Encode given table to urlencoded string. +-- Returns the encoded string. +function urlencode_params( tbl ) + local enc = "" + + for k, v in pairs(tbl) do + enc = enc .. ( enc and "&" or "" ) .. + urlencode(k) .. "=" .. + urlencode(v) + end + + return enc +end + + +-- Table of our process states +local process_states = { } + +-- Extract "magic", the first line of a http message. +-- Extracts the message type ("get", "post" or "response"), the requested uri +-- or the status code if the line descripes a http response. +process_states['magic'] = function( msg, chunk ) + + if chunk ~= nil then + + -- Is it a request? + local method, uri, http_ver = chunk:match("^([A-Z]+) ([^ ]+) HTTP/([01]%.[019])$") + + -- Yup, it is + if method then + + msg.type = "request" + msg.request_method = method:lower() + msg.request_uri = uri + msg.http_version = http_ver + msg.headers = { } + + -- We're done, next state is header parsing + return true, function( chunk ) + return process_states['headers']( msg, chunk ) + end + + -- Is it a response? + else + + local http_ver, code, message = chunk:match("^HTTP/([01]%.[019]) ([0-9]+) ([^\r\n]+)$") + + -- Is a response + if code then + + msg.type = "response" + msg.status_code = code + msg.status_message = message + msg.http_version = http_ver + msg.headers = { } + + -- We're done, next state is header parsing + return true, function( chunk ) + return process_states['headers']( msg, chunk ) + end + end + end + end + + -- Can't handle it + return nil, "Invalid HTTP message magic" +end + + +-- Extract headers from given string. +process_states['headers'] = function( msg, chunk ) + + if chunk ~= nil then + + -- Look for a valid header format + local hdr, val = chunk:match( "^([A-Z][A-Za-z0-9%-_]+): +(.+)$" ) + + if type(hdr) == "string" and hdr:len() > 0 and + type(val) == "string" and val:len() > 0 + then + msg.headers[hdr] = val + + -- Valid header line, proceed + return true, nil + + elseif #chunk == 0 then + -- Empty line, we won't accept data anymore + return false, nil + else + -- Junk data + return nil, "Invalid HTTP header received" + end + else + return nil, "Unexpected EOF" + end +end + + +-- Find first MIME boundary +process_states['mime-init'] = function( msg, chunk, filecb ) + + if chunk ~= nil then + if #chunk >= #msg.mime_boundary + 2 then + local boundary = chunk:sub( 1, #msg.mime_boundary + 4 ) + + if boundary == "--" .. msg.mime_boundary .. "\r\n" then + + -- Store remaining data in buffer + msg._mimebuffer = chunk:sub( #msg.mime_boundary + 5, #chunk ) + + -- Switch to header processing state + return true, function( chunk ) + return process_states['mime-headers']( msg, chunk, filecb ) + end + else + return nil, "Invalid MIME boundary" + end + else + return true + end + else + return nil, "Unexpected EOF" + end +end + + +-- Read MIME part headers +process_states['mime-headers'] = function( msg, chunk, filecb ) + + if chunk ~= nil then + + -- Combine look-behind buffer with current chunk + chunk = msg._mimebuffer .. chunk + + if not msg._mimeheaders then + msg._mimeheaders = { } + end + + local function __storehdr( k, v ) + msg._mimeheaders[k] = v + return "" + end + + -- Read all header lines + local ok, count = 1, 0 + while ok > 0 do + chunk, ok = chunk:gsub( "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n", __storehdr ) + count = count + ok + end + + -- Headers processed, check for empty line + chunk, ok = chunk:gsub( "^\r\n", "" ) + + -- Store remaining buffer contents + msg._mimebuffer = chunk + + -- End of headers + if ok > 0 then + + -- When no Content-Type header is given assume text/plain + if not msg._mimeheaders['Content-Type'] then + msg._mimeheaders['Content-Type'] = 'text/plain' + end + + -- Check Content-Disposition + if msg._mimeheaders['Content-Disposition'] then + -- Check for "form-data" token + if msg._mimeheaders['Content-Disposition']:match("^form%-data; ") then + -- Check for field name, filename + local field = msg._mimeheaders['Content-Disposition']:match('name="(.-)"') + local file = msg._mimeheaders['Content-Disposition']:match('filename="(.+)"$') + + -- Is a file field and we have a callback + if file and filecb then + msg.params[field] = file + msg._mimecallback = function(chunk,eof) + filecb( { + name = field; + file = file; + headers = msg._mimeheaders + }, chunk, eof ) + end + + -- Treat as form field + else + msg.params[field] = "" + msg._mimecallback = function(chunk,eof) + msg.params[field] = msg.params[field] .. chunk + end + end + + -- Header was valid, continue with mime-data + return true, function( chunk ) + return process_states['mime-data']( msg, chunk, filecb ) + end + else + -- Unknown Content-Disposition, abort + return nil, "Unexpected Content-Disposition MIME section header" + end + else + -- Content-Disposition is required, abort without + return nil, "Missing Content-Disposition MIME section header" + end + + -- We parsed no headers yet and buffer is almost empty + elseif count > 0 or #chunk < 128 then + -- Keep feeding me with chunks + return true, nil + end + + -- Buffer looks like garbage + return nil, "Malformed MIME section header" + else + return nil, "Unexpected EOF" + end +end + + +-- Read MIME part data +process_states['mime-data'] = function( msg, chunk, filecb ) + + if chunk ~= nil then + + -- Combine look-behind buffer with current chunk + local buffer = msg._mimebuffer .. chunk + + -- Look for MIME boundary + local spos, epos = buffer:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true ) + + if spos then + -- Content data + msg._mimecallback( buffer:sub( 1, spos - 1 ), true ) + + -- Store remainder + msg._mimebuffer = buffer:sub( epos + 1, #buffer ) + + -- Next state is mime-header processing + return true, function( chunk ) + return process_states['mime-headers']( msg, chunk, filecb ) + end + else + -- Look for EOF? + local spos, epos = buffer:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true ) + + if spos then + -- Content data + msg._mimecallback( buffer:sub( 1, spos - 1 ), true ) + + -- We processed the final MIME boundary, cleanup + msg._mimebuffer = nil + msg._mimeheaders = nil + msg._mimecallback = nil + + -- We won't accept data anymore + return false + else + -- We're somewhere within a data section and our buffer is full + if #buffer > #chunk then + -- Flush buffered data + msg._mimecallback( buffer:sub( 1, #buffer - #chunk ), false ) + + -- Store new data + msg._mimebuffer = buffer:sub( #buffer - #chunk + 1, #buffer ) + + -- Buffer is not full yet, append new data + else + msg._mimebuffer = buffer + end + + -- Keep feeding me + return true + end + end + else + return nil, "Unexpected EOF" + end +end + + +-- Init urldecoding stream +process_states['urldecode-init'] = function( msg, chunk, filecb ) + + if chunk ~= nil then + + -- Check for Content-Length + if msg.headers['Content-Length'] then + msg.content_length = tonumber(msg.headers['Content-Length']) + + if msg.content_length <= HTTP_MAX_CONTENT then + -- Initialize buffer + msg._urldecbuffer = chunk + msg._urldeclength = 0 + + -- Switch to urldecode-key state + return true, function(chunk) + return process_states['urldecode-key']( msg, chunk, filecb ) + end + else + return nil, "Request exceeds maximum allowed size" + end + else + return nil, "Missing Content-Length header" + end + else + return nil, "Unexpected EOF" + end +end + + +-- Process urldecoding stream, read and validate parameter key +process_states['urldecode-key'] = function( msg, chunk, filecb ) + + if chunk ~= nil then + + -- Prevent oversized requests + if msg._urldeclength >= msg.content_length then + return nil, "Request exceeds maximum allowed size" + end + + -- Combine look-behind buffer with current chunk + local buffer = msg._urldecbuffer .. chunk + local spos, epos = buffer:find("=") + + -- Found param + if spos then + + -- Check that key doesn't exceed maximum allowed key length + if ( spos - 1 ) <= HTTP_URLENC_MAXKEYLEN then + local key = urldecode( buffer:sub( 1, spos - 1 ) ) + + -- Prepare buffers + msg.params[key] = "" + msg._urldeclength = msg._urldeclength + epos + msg._urldecbuffer = buffer:sub( epos + 1, #buffer ) + + -- Use file callback or store values inside msg.params + if filecb then + msg._urldeccallback = function( chunk, eof ) + filecb( field, chunk, eof ) + end + else + msg._urldeccallback = function( chunk, eof ) + msg.params[key] = msg.params[key] .. chunk + end + end + + -- Proceed with urldecode-value state + return true, function( chunk ) + return process_states['urldecode-value']( msg, chunk, filecb ) + end + else + return nil, "POST parameter exceeds maximum allowed length" + end + else + return nil, "POST data exceeds maximum allowed length" + end + else + return nil, "Unexpected EOF" + end +end + + +-- Process urldecoding stream, read parameter value +process_states['urldecode-value'] = function( msg, chunk, filecb ) + + if chunk ~= nil then + + -- Combine look-behind buffer with current chunk + local buffer = msg._urldecbuffer .. chunk + + -- Check for EOF + if #buffer == 0 then + -- Compare processed length + if msg._urldeclength == msg.content_length then + -- Cleanup + msg._urldeclength = nil + msg._urldecbuffer = nil + msg._urldeccallback = nil + + -- We won't accept data anymore + return false + else + return nil, "Content-Length mismatch" + end + end + + -- Check for end of value + local spos, epos = buffer:find("[&;]") + if spos then + + -- Flush buffer, send eof + msg._urldeccallback( buffer:sub( 1, spos - 1 ), true ) + msg._urldecbuffer = buffer:sub( epos + 1, #buffer ) + msg._urldeclength = msg._urldeclength + epos + + -- Back to urldecode-key state + return true, function( chunk ) + return process_states['urldecode-key']( msg, chunk, filecb ) + end + else + -- We're somewhere within a data section and our buffer is full + if #buffer > #chunk then + -- Flush buffered data + msg._urldeccallback( buffer:sub( 1, #buffer - #chunk ), false ) + + -- Store new data + msg._urldeclength = msg._urldeclength + #buffer - #chunk + msg._urldecbuffer = buffer:sub( #buffer - #chunk + 1, #buffer ) + + -- Buffer is not full yet, append new data + else + msg._urldecbuffer = buffer + end + + -- Keep feeding me + return true + end + else + return nil, "Unexpected EOF" + end +end + + +-- Decode MIME encoded data. +function mimedecode_message_body( source, msg, filecb ) + + -- Find mime boundary + if msg and msg.headers['Content-Type'] then + + local bound = msg.headers['Content-Type']:match("^multipart/form%-data; boundary=(.+)") + + if bound then + msg.mime_boundary = bound + else + return nil, "No MIME boundary found or invalid content type given" + end + end + + -- Create an initial LTN12 sink + -- The whole MIME parsing process is implemented as fancy sink, sinks replace themself + -- depending on current processing state (init, header, data). Return the initial state. + local sink = ltn12.sink.simplify( + function( chunk ) + return process_states['mime-init']( msg, chunk, filecb ) + end + ) + + -- Create a throttling LTN12 source + -- Frequent state switching in the mime parsing process leads to unwanted buffer aggregation. + -- This source checks wheather there's still data in our internal read buffer and returns an + -- empty string if there's already enough data in the processing queue. If the internal buffer + -- runs empty we're calling the original source to get the next chunk of data. + local tsrc = function() + + -- XXX: we schould propably keep the maximum buffer size in sync with + -- the blocksize of our original source... but doesn't really matter + if msg._mimebuffer ~= null and #msg._mimebuffer > 256 then + return "" + else + return source() + end + end + + -- Pump input data... + while true do + -- get data + local ok, err = ltn12.pump.step( tsrc, sink ) + + -- error + if not ok and err then + return nil, err + + -- eof + elseif not ok then + return true + end + end +end + + +-- Decode urlencoded data. +function urldecode_message_body( source, msg ) + + -- Create an initial LTN12 sink + -- Return the initial state. + local sink = ltn12.sink.simplify( + function( chunk ) + return process_states['urldecode-init']( msg, chunk ) + end + ) + + -- Create a throttling LTN12 source + -- See explaination in mimedecode_message_body(). + local tsrc = function() + if msg._urldecbuffer ~= null and #msg._urldecbuffer > 0 then + return "" + else + return source() + end + end + + -- Pump input data... + while true do + -- get data + local ok, err = ltn12.pump.step( tsrc, sink ) + + -- step + if not ok and err then + return nil, err + + -- eof + elseif not ok then + return true + end + end +end + + +-- Parse a http message +function parse_message( data, filecb ) + + local reader = _linereader( data, HTTP_MAX_READBUF ) + local message = parse_message_header( reader ) + + if message then + parse_message_body( reader, message, filecb ) + end + + return message +end + + +-- Parse a http message header +function parse_message_header( source ) + + local ok = true + local msg = { } + + local sink = ltn12.sink.simplify( + function( chunk ) + return process_states['magic']( msg, chunk ) + end + ) + + -- Pump input data... + while ok do + + -- get data + ok, err = ltn12.pump.step( source, sink ) + + -- error + if not ok and err then + return nil, err + + -- eof + elseif not ok then + + -- Process get parameters + if ( msg.request_method == "get" or msg.request_method == "post" ) and + msg.request_uri:match("?") + then + msg.params = urldecode_params( msg.request_uri ) + else + msg.params = { } + end + + -- Populate common environment variables + msg.env = { + CONTENT_LENGTH = msg.headers['Content-Length']; + CONTENT_TYPE = msg.headers['Content-Type']; + REQUEST_METHOD = msg.request_method:upper(); + REQUEST_URI = msg.request_uri; + SCRIPT_NAME = msg.request_uri:gsub("?.+$",""); + SCRIPT_FILENAME = "" -- XXX implement me + } + + -- Populate HTTP_* environment variables + for i, hdr in ipairs( { + 'Accept', + 'Accept-Charset', + 'Accept-Encoding', + 'Accept-Language', + 'Connection', + 'Cookie', + 'Host', + 'Referer', + 'User-Agent', + } ) do + local var = 'HTTP_' .. hdr:upper():gsub("%-","_") + local val = msg.headers[hdr] + + msg.env[var] = val + end + end + end + + return msg +end + + +-- Parse a http message body +function parse_message_body( source, msg, filecb ) + + -- Is it multipart/mime ? + if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and + msg.env.CONTENT_TYPE:match("^multipart/form%-data") + then + + return mimedecode_message_body( source, msg, filecb ) + + -- Is it application/x-www-form-urlencoded ? + elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and + msg.env.CONTENT_TYPE == "application/x-www-form-urlencoded" + then + + return urldecode_message_body( source, msg, filecb ) + + -- Unhandled encoding + -- If a file callback is given then feed it line by line, else + -- store whole buffer in message.content + else + + local sink + local length = 0 + + -- If we have a file callback then feed it + if type(filecb) == "function" then + sink = filecb + + -- ... else append to .content + else + msg.content = "" + msg.content_length = 0 + + sink = function( chunk ) + if ( msg.content_length ) + #chunk <= HTTP_MAX_CONTENT then + + msg.content = msg.content .. chunk + msg.content_length = msg.content_length + #chunk + + return true + else + return nil, "POST data exceeds maximum allowed length" + end + end + end + + -- Pump data... + while true do + local ok, err = ltn12.pump.step( source, sink ) + + if not ok and err then + return nil, err + elseif not err then + return true + end + end + end +end diff --git a/libs/web/luasrc/http/protocol.lua b/libs/web/luasrc/http/protocol.lua deleted file mode 100644 index 6901291b9a..0000000000 --- a/libs/web/luasrc/http/protocol.lua +++ /dev/null @@ -1,572 +0,0 @@ ---[[ - -HTTP protocol implementation for LuCI -(c) 2008 Freifunk Leipzig / Jo-Philipp Wich - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -$Id$ - -]]-- - -module("luci.http.protocol", package.seeall) - -require("luci.util") - - -HTTP_MAX_CONTENT = 1024^2 -- 1 MB maximum content size -HTTP_MAX_READBUF = 1024 -- 1 kB read buffer size - -HTTP_DEFAULT_CTYPE = "text/html" -- default content type -HTTP_DEFAULT_VERSION = "1.0" -- HTTP default version - - --- Decode an urlencoded string. --- Returns the decoded value. -function urldecode( str ) - - local function __chrdec( hex ) - return string.char( tonumber( hex, 16 ) ) - end - - if type(str) == "string" then - str = str:gsub( "+", " " ):gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec ) - end - - return str -end - - --- Extract and split urlencoded data pairs, separated bei either "&" or ";" from given url. --- Returns a table value with urldecoded values. -function urldecode_params( url ) - - local params = { } - - if url:find("?") then - url = url:gsub( "^.+%?([^?]+)", "%1" ) - end - - for i, pair in ipairs(luci.util.split( url, "[&;]+", nil, true )) do - - -- find key and value - local key = urldecode( pair:match("^([^=]+)") ) - local val = urldecode( pair:match("^[^=]+=(.+)$") ) - - -- store - if type(key) == "string" and key:len() > 0 then - if type(val) ~= "string" then val = "" end - - if not params[key] then - params[key] = val - elseif type(params[key]) ~= "table" then - params[key] = { params[key], val } - else - table.insert( params[key], val ) - end - end - end - - return params -end - - --- Encode given string in urlencoded format. --- Returns the encoded string. -function urlencode( str ) - - local function __chrenc( chr ) - return string.format( - "%%%02x", string.byte( chr ) - ) - end - - if type(str) == "string" then - str = str:gsub( - "([^a-zA-Z0-9$_%-%.+!*'(),])", - __chrenc - ) - end - - return str -end - - --- Encode given table to urlencoded string. --- Returns the encoded string. -function urlencode_params( tbl ) - local enc = "" - - for k, v in pairs(tbl) do - enc = enc .. ( enc and "&" or "" ) .. - urlencode(k) .. "=" .. - urlencode(v) - end - - return enc -end - - --- Decode MIME encoded data. --- Returns a table with decoded values. -function mimedecode( data, boundary, filecb ) - - local params = { } - - -- create a line reader - local reader = _linereader( data, HTTP_MAX_READBUF ) - - -- state variables - local in_part = false - local in_file = false - local in_fbeg = false - local in_size = true - - local filename - local buffer - local field - local clen = 0 - - -- try to read all mime parts - for line, eol in reader do - - -- update content length - clen = clen + line:len() - - if clen >= HTTP_MAX_CONTENT then - in_size = false - end - - -- when no boundary is given, try to find it - if not boundary then - boundary = line:match("^%-%-([^\r\n]+)\r?\n$") - end - - -- Got a valid boundary line or reached max allowed size. - if ( boundary and line:sub(1,2) == "--" and line:len() > #boundary + 2 and - line:sub( 3, 2 + #boundary ) == boundary ) or not in_size - then - -- Flush the data of the previous mime part. - -- When field and/or buffer are set to nil we should discard - -- the previous section entirely due to format violations. - if type(field) == "string" and field:len() > 0 and - type(buffer) == "string" - then - -- According to the rfc the \r\n preceeding a boundary - -- is assumed to be part of the boundary itself. - -- Since we are reading line by line here, this crlf - -- is part of the last line of our section content, - -- so strip it before storing the buffer. - buffer = buffer:gsub("\r?\n$","") - - -- If we're in a file part and a file callback has been provided - -- then do a final call and send eof. - if in_file and type(filecb) == "function" then - filecb( field, filename, buffer, true ) - params[field] = filename - - -- Store buffer. - else - params[field] = buffer - end - end - - -- Reset vars - buffer = "" - filename = nil - field = nil - in_file = false - - -- Abort here if we reached maximum allowed size - if not in_size then break end - - -- Do we got the last boundary? - if line:len() > #boundary + 4 and - line:sub( #boundary + 2, #boundary + 4 ) == "--" - then - -- No more processing - in_part = false - - -- It's a middle boundary - else - - -- Read headers - local hlen, headers = extract_headers( reader ) - - -- Check for valid headers - if headers['Content-Disposition'] then - - -- Got no content type header, assume content-type "text/plain" - if not headers['Content-Type'] then - headers['Content-Type'] = 'text/plain' - end - - -- Find field name - local hdrvals = luci.util.split( - headers['Content-Disposition'], '; ' - ) - - -- Valid form data part? - if hdrvals[1] == "form-data" and hdrvals[2]:match("^name=") then - - -- Store field identifier - field = hdrvals[2]:match('^name="(.+)"$') - - -- Do we got a file upload field? - if #hdrvals == 3 and hdrvals[3]:match("^filename=") then - in_file = true - if_fbeg = true - filename = hdrvals[3]:match('^filename="(.+)"$') - end - - -- Entering next part processing - in_part = true - end - end - end - - -- Processing content - elseif in_part then - - -- XXX: Would be really good to switch from line based to - -- buffered reading here. - - - -- If we're in a file part and a file callback has been provided - -- then call the callback and reset the buffer. - if in_file and type(filecb) == "function" then - - -- If we're not processing the first chunk, then call - if not in_fbeg then - filecb( field, filename, buffer, false ) - buffer = "" - - -- Clear in_fbeg flag after first run - else - in_fbeg = false - end - end - - -- Append date to buffer - buffer = buffer .. line - end - end - - return params -end - - --- Extract "magic", the first line of a http message. --- Returns the message type ("get", "post" or "response"), the requested uri --- if it is a valid http request or the status code if the line descripes a --- http response. For requests the third parameter is nil, for responses it --- contains the human readable status description. -function extract_magic( reader ) - - for line in reader do - -- Is it a request? - local method, uri = line:match("^([A-Z]+) ([^ ]+) HTTP/[01]%.[019]\r?\n$") - - -- Yup, it is - if method then - return method:lower(), uri, nil - - -- Is it a response? - else - local code, message = line:match("^HTTP/[01]%.[019] ([0-9]+) ([^\r\n]+)\r?\n$") - - -- Is a response - if code then - return "response", code + 0, message - - -- Can't handle it - else - return nil - end - end - end -end - - --- Extract headers from given string. --- Returns a table of extracted headers and the remainder of the parsed data. -function extract_headers( reader, tbl ) - - local headers = tbl or { } - local count = 0 - - -- Iterate line by line - for line in reader do - - -- Look for a valid header format - local hdr, val = line:match( "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r?\n$" ) - - if type(hdr) == "string" and hdr:len() > 0 and - type(val) == "string" and val:len() > 0 - then - count = count + line:len() - headers[hdr] = val - - elseif line:match("^\r?\n$") then - - return count + line:len(), headers - - else - -- junk data, don't add length - return count, headers - end - end - - return count, headers -end - - --- Parse a http message -function parse_message( data, filecb ) - - local reader = _linereader( data, HTTP_MAX_READBUF ) - local message = parse_message_header( reader ) - - if message then - parse_message_body( reader, message, filecb ) - end - - return message -end - - --- Parse a http message header -function parse_message_header( data ) - - -- Create a line reader - local reader = _linereader( data, HTTP_MAX_READBUF ) - local message = { } - - -- Try to extract magic - local method, arg1, arg2 = extract_magic( reader ) - - -- Does it looks like a valid message? - if method then - - message.request_method = method - message.status_code = arg2 and arg1 or 200 - message.status_message = arg2 or nil - message.request_uri = arg2 and nil or arg1 - - if method == "response" then - message.type = "response" - else - message.type = "request" - end - - -- Parse headers? - local hlen, hdrs = extract_headers( reader ) - - -- Valid headers? - if hlen > 2 and type(hdrs) == "table" then - - message.headers = hdrs - - -- Process get parameters - if ( method == "get" or method == "post" ) and - message.request_uri:match("?") - then - message.params = urldecode_params( message.request_uri ) - else - message.params = { } - end - - -- Populate common environment variables - message.env = { - CONTENT_LENGTH = hdrs['Content-Length']; - CONTENT_TYPE = hdrs['Content-Type']; - REQUEST_METHOD = message.request_method; - REQUEST_URI = message.request_uri; - SCRIPT_NAME = message.request_uri:gsub("?.+$",""); - SCRIPT_FILENAME = "" -- XXX implement me - } - - -- Populate HTTP_* environment variables - for i, hdr in ipairs( { - 'Accept', - 'Accept-Charset', - 'Accept-Encoding', - 'Accept-Language', - 'Connection', - 'Cookie', - 'Host', - 'Referer', - 'User-Agent', - } ) do - local var = 'HTTP_' .. hdr:upper():gsub("%-","_") - local val = hdrs[hdr] - - message.env[var] = val - end - - - return message - end - end -end - - --- Parse a http message body -function parse_message_body( reader, message, filecb ) - - if type(message) == "table" then - local env = message.env - - local clen = ( env.CONTENT_LENGTH or HTTP_MAX_CONTENT ) + 0 - - -- Process post method - if env.REQUEST_METHOD:lower() == "post" and env.CONTENT_TYPE then - - -- Is it multipart/form-data ? - if env.CONTENT_TYPE:match("^multipart/form%-data") then - - -- Read multipart/mime data - for k, v in pairs( mimedecode( - reader, - env.CONTENT_TYPE:match("boundary=(.+)"), - filecb - ) ) do - message.params[k] = v - end - - -- Is it x-www-form-urlencoded? - elseif env.CONTENT_TYPE:match('^application/x%-www%-form%-urlencoded') then - - -- Read post data - local post_data = "" - - for chunk, eol in reader do - - post_data = post_data .. chunk - - -- Abort on eol or if maximum allowed size or content length is reached - if eol or #post_data >= HTTP_MAX_CONTENT or #post_data > clen then - break - end - end - - -- Parse params - for k, v in pairs( urldecode_params( post_data ) ) do - message.params[k] = v - end - - -- Unhandled encoding - -- If a file callback is given then feed it line by line, else - -- store whole buffer in message.content - else - - local len = 0 - - for chunk in reader do - - len = len + #chunk - - -- We have a callback, feed it. - if type(filecb) == "function" then - - filecb( "_post", nil, chunk, false ) - - -- Append to .content buffer. - else - message.content = - type(message.content) == "string" - and message.content .. chunk - or chunk - end - - -- Abort if maximum allowed size or content length is reached - if len >= HTTP_MAX_CONTENT or len >= clen then - break - end - end - - -- Send eof to callback - if type(filecb) == "function" then - filecb( "_post", nil, "", true ) - end - end - end - end -end - - --- Wrap given object into a line read iterator -function _linereader( obj, bufsz ) - - bufsz = ( bufsz and bufsz >= 256 ) and bufsz or 256 - - local __read = function() return nil end - local __eof = function(x) return type(x) ~= "string" or #x == 0 end - - local _pos = 1 - local _buf = "" - local _eof = nil - - -- object is string - if type(obj) == "string" then - - __read = function() return obj:sub( _pos, _pos + bufsz - #_buf - 1 ) end - - -- object implements a receive() or read() function - elseif (type(obj) == "userdata" or type(obj) == "table") and ( type(obj.receive) == "function" or type(obj.read) == "function" ) then - - if type(obj.read) == "function" then - __read = function() return obj:read( bufsz - #_buf ) end - else - __read = function() return obj:receive( bufsz - #_buf ) end - end - - -- object is a function - elseif type(obj) == "function" then - - return obj - - -- no usable data type - else - - -- dummy iterator - return __read - end - - - -- generic block to line algorithm - return function() - if not _eof then - local buffer = __read() - - if __eof( buffer ) then - buffer = "" - end - - _pos = _pos + #buffer - buffer = _buf .. buffer - - local crlf, endpos = buffer:find("\r?\n") - - - if crlf then - _buf = buffer:sub( endpos + 1, #buffer ) - return buffer:sub( 1, endpos ), true - else - -- check for eof - _eof = __eof( buffer ) - - -- clear overflow buffer - _buf = "" - - return buffer, false - end - else - return nil - end - end -end