123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- -- Copyright 2008 Freifunk Leipzig / Jo-Philipp Wich <jow@openwrt.org>
- -- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
- -- Licensed to the public under the Apache License 2.0.
- -- This class contains several functions useful for http message- and content
- -- decoding and to retrive form data from raw http messages.
- module("gluon.web.http.protocol", package.seeall)
- HTTP_MAX_CONTENT = 1024*8 -- 8 kB maximum content size
- local function pump(src, snk)
- while true do
- local chunk, src_err = src()
- local ret, snk_err = snk(chunk, src_err)
- if not (chunk and ret) then
- local err = src_err or snk_err
- if err then
- return nil, err
- else
- return true
- end
- end
- end
- end
- function urlencode(s)
- return (string.gsub(s, '[^a-zA-Z0-9%-_%.~]',
- function(c)
- local ret = ''
- for i = 1, string.len(c) do
- ret = ret .. string.format('%%%02X', string.byte(c, i, i))
- end
- return ret
- end
- ))
- end
- -- the "+" sign to " " - and return the decoded string.
- function urldecode(str, no_plus)
- local function chrdec(hex)
- return string.char(tonumber(hex, 16))
- end
- if type(str) == "string" then
- if not no_plus then
- str = str:gsub("+", " ")
- end
- str = str:gsub("%%(%x%x)", chrdec)
- end
- return str
- end
- local function initval(tbl, key)
- if not tbl[key] then
- tbl[key] = {}
- end
- table.insert(tbl[key], "")
- end
- local function appendval(tbl, key, chunk)
- local t = tbl[key]
- t[#t] = t[#t] .. chunk
- end
- -- from given url or string. Returns a table with urldecoded values.
- -- Simple parameters are stored as string values associated with the parameter
- -- name within the table. Parameters with multiple values are stored as array
- -- containing the corresponding values.
- function urldecode_params(url)
- local params = {}
- if url:find("?") then
- url = url:gsub("^.+%?([^?]+)", "%1")
- end
- for pair in url:gmatch("[^&;]+") do
- -- find key and value
- local key = urldecode(pair:match("^([^=]+)"))
- local val = urldecode(pair:match("^[^=]+=(.+)$"))
- -- store
- if key and key:len() > 0 then
- initval(params, key)
- if val then
- appendval(params, key, val)
- end
- end
- end
- return params
- end
- -- Content-Type. Stores all extracted data associated with its parameter name
- -- in the params table withing the given message object. Multiple parameter
- -- values are stored as tables, ordinary ones as strings.
- -- If an optional file callback function is given then it is feeded with the
- -- file contents chunk by chunk and only the extracted file name is stored
- -- within the params table. The callback function will be called subsequently
- -- with three arguments:
- -- o Table containing decoded (name, file) and raw (headers) mime header data
- -- o String value containing a chunk of the file data
- -- o Boolean which indicates wheather the current chunk is the last one (eof)
- function mimedecode_message_body(src, msg, filecb)
- if msg and msg.env.CONTENT_TYPE then
- msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$")
- end
- if not msg.mime_boundary then
- return nil, "Invalid Content-Type found"
- end
- local tlen = 0
- local inhdr = false
- local field = nil
- local store = nil
- local lchunk = nil
- local function parse_headers(chunk, field)
- local stat
- repeat
- chunk, stat = chunk:gsub(
- "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n",
- function(k,v)
- field.headers[k] = v
- return ""
- end
- )
- until stat == 0
- chunk, stat = chunk:gsub("^\r\n","")
- -- End of headers
- if stat > 0 then
- if field.headers["Content-Disposition"] then
- if field.headers["Content-Disposition"]:match("^form%-data; ") then
- field.name = field.headers["Content-Disposition"]:match('name="(.-)"')
- field.file = field.headers["Content-Disposition"]:match('filename="(.+)"$')
- end
- end
- if not field.headers["Content-Type"] then
- field.headers["Content-Type"] = "text/plain"
- end
- if field.name then
- initval(msg.params, field.name)
- if field.file then
- appendval(msg.params, field.name, field.file)
- store = filecb
- else
- store = function(hdr, buf, eof)
- appendval(msg.params, field.name, buf)
- end
- end
- else
- store = nil
- end
- return chunk, true
- end
- return chunk, false
- end
- local function snk(chunk)
- tlen = tlen + (chunk and #chunk or 0)
- if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
- return nil, "Message body size exceeds Content-Length"
- end
- if chunk and not lchunk then
- lchunk = "\r\n" .. chunk
- elseif lchunk then
- local data = lchunk .. (chunk or "")
- local spos, epos, found
- repeat
- spos, epos = data:find("\r\n--" .. msg.mime_boundary .. "\r\n", 1, true)
- if not spos then
- spos, epos = data:find("\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true)
- end
- if spos then
- local predata = data:sub(1, spos - 1)
- if inhdr then
- predata, eof = parse_headers(predata, field)
- if not eof then
- return nil, "Invalid MIME section header"
- elseif not field.name then
- return nil, "Invalid Content-Disposition header"
- end
- end
- if store then
- store(field, predata, true)
- end
- field = { headers = { } }
- found = true
- data, eof = parse_headers(data:sub(epos + 1, #data), field)
- inhdr = not eof
- end
- until not spos
- if found then
- -- We found at least some boundary. Save
- -- the unparsed remaining data for the
- -- next chunk.
- lchunk, data = data, nil
- else
- -- There was a complete chunk without a boundary. Parse it as headers or
- -- append it as data, depending on our current state.
- if inhdr then
- lchunk, eof = parse_headers(data, field)
- inhdr = not eof
- else
- -- We're inside data, so append the data. Note that we only append
- -- lchunk, not all of data, since there is a chance that chunk
- -- contains half a boundary. Assuming that each chunk is at least the
- -- boundary in size, this should prevent problems
- if store then
- store(field, lchunk, false)
- end
- lchunk, chunk = chunk, nil
- end
- end
- end
- return true
- end
- return pump(src, snk)
- end
- -- This function will examine the Content-Type within the given message object
- -- to select the appropriate content decoder.
- -- Currently only the multipart/form-data mime type is supported.
- function parse_message_body(src, msg, filecb)
- if not (msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE) then
- return
- end
- if msg.env.CONTENT_TYPE:match("^multipart/form%-data") then
- return mimedecode_message_body(src, msg, filecb)
- end
- end
|