* luci/libs: moved http.protocol from libs/web to libs/httpd, rewrote http.protocol to rely on LTN12 chains

This commit is contained in:
Jo-Philipp Wich 2008-06-19 02:53:09 +00:00
parent 304ce583c7
commit f712a1f2c2
2 changed files with 754 additions and 572 deletions

View file

@ -0,0 +1,754 @@
--[[
HTTP protocol implementation for LuCI
(c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net>
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

View file

@ -1,572 +0,0 @@
--[[
HTTP protocol implementation for LuCI
(c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net>
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