libs: merge libs/http into libs/web
authorJo-Philipp Wich <jow@openwrt.org>
Tue, 12 Oct 2010 05:22:38 +0000 (05:22 +0000)
committerJo-Philipp Wich <jow@openwrt.org>
Tue, 12 Oct 2010 05:22:38 +0000 (05:22 +0000)
contrib/package/luci/Makefile
libs/http/luasrc/http/protocol.lua [deleted file]
libs/http/luasrc/http/protocol/conditionals.lua [deleted file]
libs/http/luasrc/http/protocol/date.lua [deleted file]
libs/http/luasrc/http/protocol/mime.lua [deleted file]
libs/web/luasrc/http/protocol.lua [new file with mode: 0644]
libs/web/luasrc/http/protocol/conditionals.lua [new file with mode: 0644]
libs/web/luasrc/http/protocol/date.lua [new file with mode: 0644]
libs/web/luasrc/http/protocol/mime.lua [new file with mode: 0644]

index d38a1ac..70c7648 100644 (file)
@@ -179,20 +179,10 @@ define Package/luci-fastindex/install
 endef
 
 
-define Package/luci-http
-  $(call Package/luci/libtemplate)
-  TITLE:=HTTP Protocol implementation
-endef
-
-define Package/luci-http/install
-       $(call Package/luci/install/template,$(1),libs/http)
-endef
-
-
 define Package/luci-httpclient
   $(call Package/luci/libtemplate)
   TITLE:=HTTP(S) client library
-  DEPENDS+=+luci-http +luci-nixio
+  DEPENDS+=+luci-web +luci-nixio
 endef
 
 define Package/luci-httpclient/install
@@ -244,7 +234,7 @@ endef
 define Package/luci-lucid
   $(call Package/luci/webservertemplate)
   TITLE:=LuCId Full-Stack Webserver
-  DEPENDS+=+luci-nixio +luci-http +luci-px5g
+  DEPENDS+=+luci-nixio +luci-web +luci-px5g
 endef
 
 define Package/luci-lucid/install
@@ -325,7 +315,7 @@ endef
 
 define Package/luci-web
   $(call Package/luci/libtemplate)
-  DEPENDS+=+luci-http +luci-sys +luci-nixio +luci-uci \
+  DEPENDS+=+luci-sys +luci-nixio +luci-uci \
        +luci-sgi-cgi +luci-lmo
   TITLE:=MVC Webframework
   $(call Config,luci.main.lang,string,auto,Default Language)
@@ -1061,9 +1051,6 @@ endif
 ifneq ($(CONFIG_PACKAGE_luci-fastindex),)
        PKG_SELECTED_MODULES+=libs/fastindex
 endif
-ifneq ($(CONFIG_PACKAGE_luci-http),)
-       PKG_SELECTED_MODULES+=libs/http
-endif
 ifneq ($(CONFIG_PACKAGE_luci-httpclient),)
        PKG_SELECTED_MODULES+=libs/httpclient
 endif
@@ -1297,7 +1284,6 @@ MAKE_FLAGS += \
 
 $(eval $(call BuildPackage,luci-core))
 $(eval $(call BuildPackage,luci-fastindex))
-$(eval $(call BuildPackage,luci-http))
 $(eval $(call BuildPackage,luci-httpclient))
 $(eval $(call BuildPackage,luci-ipkg))
 $(eval $(call BuildPackage,luci-json))
diff --git a/libs/http/luasrc/http/protocol.lua b/libs/http/luasrc/http/protocol.lua
deleted file mode 100644 (file)
index d8ef09f..0000000
+++ /dev/null
@@ -1,690 +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$
-
-]]--
-
---- LuCI http protocol class.
--- This class contains several functions useful for http message- and content
--- decoding and to retrive form data from raw http messages.
-module("luci.http.protocol", package.seeall)
-
-local ltn12 = require("luci.ltn12")
-
-HTTP_MAX_CONTENT      = 1024*8         -- 8 kB maximum content size
-
---- Decode an urlencoded string - optionally without decoding
--- the "+" sign to " " - and return the decoded string.
--- @param str          Input string in x-www-urlencoded format
--- @param no_plus      Don't decode "+" signs to spaces
--- @return                     The decoded string
--- @see                                urlencode
-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( "%%([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 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.
--- @param url  The url or string which contains x-www-urlencoded form data
--- @param tbl  Use the given table for storing values (optional)
--- @return             Table containing the urldecoded parameters
--- @see                        urlencode_params
-function urldecode_params( url, tbl )
-
-       local params = tbl or { }
-
-       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 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 to x-www-urlencoded format.
--- @param str  String to encode
--- @return             String containing the encoded data
--- @see                        urldecode
-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 each key-value-pair in given table to x-www-urlencoded format,
--- separated by "&". Tables are encoded as parameters with multiple values by
--- repeating the parameter name with each value.
--- @param tbl  Table with the values
--- @return             String containing encoded values
--- @see                        urldecode_params
-function urlencode_params( tbl )
-       local enc = ""
-
-       for k, v in pairs(tbl) do
-               if type(v) == "table" then
-                       for i, v2 in ipairs(v) do
-                               enc = enc .. ( #enc > 0 and "&" or "" ) ..
-                                       urlencode(k) .. "=" .. urlencode(v2)
-                       end
-               else
-                       enc = enc .. ( #enc > 0 and "&" or "" ) ..
-                               urlencode(k) .. "=" .. urlencode(v)
-               end
-       end
-
-       return enc
-end
-
--- (Internal function)
--- Initialize given parameter and coerce string into table when the parameter
--- already exists.
--- @param tbl  Table where parameter should be created
--- @param key  Parameter name
--- @return             Always nil
-local function __initval( tbl, key )
-       if tbl[key] == nil then
-               tbl[key] = ""
-       elseif type(tbl[key]) == "string" then
-               tbl[key] = { tbl[key], "" }
-       else
-               table.insert( tbl[key], "" )
-       end
-end
-
--- (Internal function)
--- Append given data to given parameter, either by extending the string value
--- or by appending it to the last string in the parameter's value table.
--- @param tbl  Table containing the previously initialized parameter value
--- @param key  Parameter name
--- @param chunk        String containing the data to append
--- @return             Always nil
--- @see                        __initval
-local function __appendval( tbl, key, chunk )
-       if type(tbl[key]) == "table" then
-               tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk
-       else
-               tbl[key] = tbl[key] .. chunk
-       end
-end
-
--- (Internal function)
--- Finish the value of given parameter, either by transforming the string value
--- or - in the case of multi value parameters - the last element in the
--- associated values table.
--- @param tbl          Table containing the previously initialized parameter value
--- @param key          Parameter name
--- @param handler      Function which transforms the parameter value
--- @return                     Always nil
--- @see                                __initval
--- @see                                __appendval
-local function __finishval( tbl, key, handler )
-       if handler then
-               if type(tbl[key]) == "table" then
-                       tbl[key][#tbl[key]] = handler( tbl[key][#tbl[key]] )
-               else
-                       tbl[key] = handler( tbl[key] )
-               end
-       end
-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, err )
-
-       if chunk ~= nil then
-               -- ignore empty lines before request
-               if #chunk == 0 then
-                       return true, nil
-               end
-
-               -- 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   = tonumber( 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   = tonumber( 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-Za-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
-
-
---- Creates a ltn12 source from the given socket. The source will return it's
--- data line by line with the trailing \r\n stripped of.
--- @param sock Readable network socket
--- @return             Ltn12 source function
-function header_source( sock )
-       return ltn12.source.simplify( function()
-
-               local chunk, err, part = sock:receive("*l")
-
-               -- Line too long
-               if chunk == nil then
-                       if err ~= "timeout" then
-                               return nil, part
-                                       and "Line exceeds maximum allowed length"
-                                       or  "Unexpected EOF"
-                       else
-                               return nil, err
-                       end
-
-               -- Line ok
-               elseif chunk ~= nil then
-
-                       -- Strip trailing CR
-                       chunk = chunk:gsub("\r$","")
-
-                       return chunk, nil
-               end
-       end )
-end
-
---- Decode a mime encoded http message body with multipart/form-data
--- 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)
--- @param src          Ltn12 source function
--- @param msg          HTTP message object
--- @param filecb       File callback function (optional)
--- @return                     Value indicating successful operation (not nil means "ok")
--- @return                     String containing the error if unsuccessful
--- @see                                parse_message_header
-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 and field.file and filecb then
-                               __initval( msg.params, field.name )
-                               __appendval( msg.params, field.name, field.file )
-
-                               store = filecb
-                       elseif field.name then
-                               __initval( msg.params, field.name )
-
-                               store = function( hdr, buf, eof )
-                                       __appendval( msg.params, field.name, buf )
-                               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 = found or true
-
-                                       data, eof = parse_headers( data:sub( epos + 1, #data ), field )
-                                       inhdr = not eof
-                               end
-                       until not spos
-
-                       if found then
-                               if #data > 78 then
-                                       lchunk = data:sub( #data - 78 + 1, #data )
-                                       data   = data:sub( 1, #data - 78 )
-
-                                       if store then
-                                               store( field, data, false )
-                                       else
-                                               return nil, "Invalid MIME section header"
-                                       end
-                               else
-                                       lchunk, data = data, nil
-                               end
-                       else
-                               if inhdr then
-                                       lchunk, eof = parse_headers( data, field )
-                                       inhdr = not eof
-                               else
-                                       store( field, lchunk, false )
-                                       lchunk, chunk = chunk, nil
-                               end
-                       end
-               end
-
-               return true
-       end
-
-       return ltn12.pump.all( src, snk )
-end
-
---- Decode an urlencoded http message body with application/x-www-urlencoded
--- 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.
--- @param src  Ltn12 source function
--- @param msg  HTTP message object
--- @return             Value indicating successful operation (not nil means "ok")
--- @return             String containing the error if unsuccessful
--- @see                        parse_message_header
-function urldecode_message_body( src, msg )
-
-       local tlen   = 0
-       local lchunk = nil
-
-       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"
-               elseif tlen > HTTP_MAX_CONTENT then
-                       return nil, "Message body size exceeds maximum allowed length"
-               end
-
-               if not lchunk and chunk then
-                       lchunk = chunk
-
-               elseif lchunk then
-                       local data = lchunk .. ( chunk or "&" )
-                       local spos, epos
-
-                       repeat
-                               spos, epos = data:find("^.-[;&]")
-
-                               if spos then
-                                       local pair = data:sub( spos, epos - 1 )
-                                       local key  = pair:match("^(.-)=")
-                                       local val  = pair:match("=([^%s]*)%s*$")
-
-                                       if key and #key > 0 then
-                                               __initval( msg.params, key )
-                                               __appendval( msg.params, key, val )
-                                               __finishval( msg.params, key, urldecode )
-                                       end
-
-                                       data = data:sub( epos + 1, #data )
-                               end
-                       until not spos
-
-                       lchunk = data
-               end
-
-               return true
-       end
-
-       return ltn12.pump.all( src, snk )
-end
-
---- Try to extract an http message header including information like protocol
--- version, message headers and resulting CGI environment variables from the
--- given ltn12 source.
--- @param src  Ltn12 source function
--- @return             HTTP message object
--- @see                        parse_message_body
-function parse_message_header( src )
-
-       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( src, 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'] or 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
-                               SERVER_PROTOCOL   = "HTTP/" .. string.format("%.1f", msg.http_version);
-                               QUERY_STRING      = msg.request_uri:match("?")
-                                       and msg.request_uri:gsub("^.+?","") or ""
-                       }
-
-                       -- 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
-
---- Try to extract and decode a http message body from the given ltn12 source.
--- This function will examine the Content-Type within the given message object
--- to select the appropriate content decoder.
--- Currently the application/x-www-urlencoded and application/form-data
--- mime types are supported. If the encountered content encoding can't be
--- handled then the whole message body will be stored unaltered as "content"
--- property within the given message object.
--- @param src          Ltn12 source function
--- @param msg          HTTP message object
--- @param filecb       File data callback (optional, see mimedecode_message_body())
--- @return                     Value indicating successful operation (not nil means "ok")
--- @return                     String containing the error if unsuccessful
--- @see                                parse_message_header
-function parse_message_body( src, 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( src, 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:match("^application/x%-www%-form%-urlencoded")
-       then
-               return urldecode_message_body( src, msg, filecb )
-
-
-       -- Unhandled encoding
-       -- If a file callback is given then feed it chunk by chunk, else
-       -- store whole buffer in message.content
-       else
-
-               local sink
-
-               -- 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, err )
-                               if chunk then
-                                       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
-                               return true
-                       end
-               end
-
-               -- Pump data...
-               while true do
-                       local ok, err = ltn12.pump.step( src, sink )
-
-                       if not ok and err then
-                               return nil, err
-                       elseif not err then
-                               return true
-                       end
-               end
-
-               return true
-       end
-end
-
---- Table containing human readable messages for several http status codes.
--- @class table
-statusmsg = {
-       [200] = "OK",
-       [206] = "Partial Content",
-       [301] = "Moved Permanently",
-       [302] = "Found",
-       [304] = "Not Modified",
-       [400] = "Bad Request",
-       [403] = "Forbidden",
-       [404] = "Not Found",
-       [405] = "Method Not Allowed",
-       [408] = "Request Time-out",
-       [411] = "Length Required",
-       [412] = "Precondition Failed",
-       [416] = "Requested range not satisfiable",
-       [500] = "Internal Server Error",
-       [503] = "Server Unavailable",
-}
diff --git a/libs/http/luasrc/http/protocol/conditionals.lua b/libs/http/luasrc/http/protocol/conditionals.lua
deleted file mode 100644 (file)
index 75e1f7b..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
---[[
-
-HTTP protocol implementation for LuCI - RFC2616 / 14.19, 14.24 - 14.28
-(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$
-
-]]--
-
---- LuCI http protocol implementation - HTTP/1.1 bits.
--- This class provides basic ETag handling and implements most of the
--- conditional HTTP/1.1 headers specified in RFC2616 Sct. 14.24 - 14.28 .
-module("luci.http.protocol.conditionals", package.seeall)
-
-local date = require("luci.http.protocol.date")
-
-
---- Implement 14.19 / ETag.
--- @param stat A file.stat structure
--- @return             String containing the generated tag suitable for ETag headers
-function mk_etag( stat )
-       if stat ~= nil then
-               return string.format( '"%x-%x-%x"', stat.ino, stat.size, stat.mtime )
-       end
-end
-
---- 14.24 / If-Match
--- Test whether the given message object contains an "If-Match" header and
--- compare it against the given stat object.
--- @param req  HTTP request message object
--- @param stat A file.stat object
--- @return             Boolean indicating whether the precondition is ok
--- @return             Alternative status code if the precondition failed
-function if_match( req, stat )
-       local h    = req.headers
-       local etag = mk_etag( stat )
-
-       -- Check for matching resource
-       if type(h['If-Match']) == "string" then
-               for ent in h['If-Match']:gmatch("([^, ]+)") do
-                       if ( ent == '*' or ent == etag ) and stat ~= nil then
-                               return true
-                       end
-               end
-
-               return false, 412
-       end
-
-       return true
-end
-
---- 14.25 / If-Modified-Since
--- Test whether the given message object contains an "If-Modified-Since" header
--- and compare it against the given stat object.
--- @param req  HTTP request message object
--- @param stat A file.stat object
--- @return             Boolean indicating whether the precondition is ok
--- @return             Alternative status code if the precondition failed
--- @return             Table containing extra HTTP headers if the precondition failed
-function if_modified_since( req, stat )
-       local h = req.headers
-
-       -- Compare mtimes
-       if type(h['If-Modified-Since']) == "string" then
-               local since = date.to_unix( h['If-Modified-Since'] )
-
-               if stat == nil or since < stat.mtime then
-                       return true
-               end
-
-               return false, 304, {
-                       ["ETag"]          = mk_etag( stat );
-                       ["Date"]          = date.to_http( os.time() );
-                       ["Last-Modified"] = date.to_http( stat.mtime )
-               }
-       end
-
-       return true
-end
-
---- 14.26 / If-None-Match
--- Test whether the given message object contains an "If-None-Match" header and
--- compare it against the given stat object.
--- @param req  HTTP request message object
--- @param stat A file.stat object
--- @return             Boolean indicating whether the precondition is ok
--- @return             Alternative status code if the precondition failed
--- @return             Table containing extra HTTP headers if the precondition failed
-function if_none_match( req, stat )
-       local h      = req.headers
-       local etag   = mk_etag( stat )
-       local method = req.env and req.env.REQUEST_METHOD or "GET"
-
-       -- Check for matching resource
-       if type(h['If-None-Match']) == "string" then
-               for ent in h['If-None-Match']:gmatch("([^, ]+)") do
-                       if ( ent == '*' or ent == etag ) and stat ~= nil then
-                               if method == "GET" or method == "HEAD" then
-                                       return false, 304, {
-                                               ["ETag"]          = etag;
-                                               ["Date"]          = date.to_http( os.time() );
-                                               ["Last-Modified"] = date.to_http( stat.mtime )
-                                       }
-                               else
-                                       return false, 412
-                               end
-                       end
-               end
-       end
-
-       return true
-end
-
---- 14.27 / If-Range
--- The If-Range header is currently not implemented due to the lack of general
--- byte range stuff in luci.http.protocol . This function will always return
--- false, 412 to indicate a failed precondition.
--- @param req  HTTP request message object
--- @param stat A file.stat object
--- @return             Boolean indicating whether the precondition is ok
--- @return             Alternative status code if the precondition failed
-function if_range( req, stat )
-       -- Sorry, no subranges (yet)
-       return false, 412
-end
-
---- 14.28 / If-Unmodified-Since
--- Test whether the given message object contains an "If-Unmodified-Since"
--- header and compare it against the given stat object.
--- @param req  HTTP request message object
--- @param stat A file.stat object
--- @return             Boolean indicating whether the precondition is ok
--- @return             Alternative status code if the precondition failed
-function if_unmodified_since( req, stat )
-       local h = req.headers
-
-       -- Compare mtimes
-       if type(h['If-Unmodified-Since']) == "string" then
-               local since = date.to_unix( h['If-Unmodified-Since'] )
-
-               if stat ~= nil and since <= stat.mtime then
-                       return false, 412
-               end
-       end
-
-       return true
-end
diff --git a/libs/http/luasrc/http/protocol/date.lua b/libs/http/luasrc/http/protocol/date.lua
deleted file mode 100644 (file)
index 83d11e2..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
---[[
-
-HTTP protocol implementation for LuCI - date handling
-(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$
-
-]]--
-
---- LuCI http protocol implementation - date helper class.
--- This class contains functions to parse, compare and format http dates.
-module("luci.http.protocol.date", package.seeall)
-
-require("luci.sys.zoneinfo")
-
-
-MONTHS = {
-       "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
-       "Sep", "Oct", "Nov", "Dec"
-}
-
---- Return the time offset in seconds between the UTC and given time zone.
--- @param tz   Symbolic or numeric timezone specifier
--- @return             Time offset to UTC in seconds
-function tz_offset(tz)
-
-       if type(tz) == "string" then
-
-               -- check for a numeric identifier
-               local s, v = tz:match("([%+%-])([0-9]+)")
-               if s == '+' then s = 1 else s = -1 end
-               if v then v = tonumber(v) end
-
-               if s and v then
-                       return s * 60 * ( math.floor( v / 100 ) * 60 + ( v % 100 ) )
-
-               -- lookup symbolic tz
-               elseif luci.sys.zoneinfo.OFFSET[tz:lower()] then
-                       return luci.sys.zoneinfo.OFFSET[tz:lower()]
-               end
-
-       end
-
-       -- bad luck
-       return 0
-end
-
---- Parse given HTTP date string and convert it to unix epoch time.
--- @param data String containing the date
--- @return             Unix epoch time
-function to_unix(date)
-
-       local wd, day, mon, yr, hr, min, sec, tz = date:match(
-               "([A-Z][a-z][a-z]), ([0-9]+) " ..
-               "([A-Z][a-z][a-z]) ([0-9]+) " ..
-               "([0-9]+):([0-9]+):([0-9]+) " ..
-               "([A-Z0-9%+%-]+)"
-       )
-
-       if day and mon and yr and hr and min and sec then
-               -- find month
-               local month = 1
-               for i = 1, 12 do
-                       if MONTHS[i] == mon then
-                               month = i
-                               break
-                       end
-               end
-
-               -- convert to epoch time
-               return tz_offset(tz) + os.time( {
-                       year  = yr,
-                       month = month,
-                       day   = day,
-                       hour  = hr,
-                       min   = min,
-                       sec   = sec
-               } )
-       end
-
-       return 0
-end
-
---- Convert the given unix epoch time to valid HTTP date string.
--- @param time Unix epoch time
--- @return             String containing the formatted date
-function to_http(time)
-       return os.date( "%a, %d %b %Y %H:%M:%S GMT", time )
-end
-
---- Compare two dates which can either be unix epoch times or HTTP date strings.
--- @param d1   The first date or epoch time to compare
--- @param d2   The first date or epoch time to compare
--- @return             -1  -  if d1 is lower then d2
--- @return             0   -  if both dates are equal
--- @return             1   -  if d1 is higher then d2
-function compare(d1, d2)
-
-       if d1:match("[^0-9]") then d1 = to_unix(d1) end
-       if d2:match("[^0-9]") then d2 = to_unix(d2) end
-
-       if d1 == d2 then
-               return 0
-       elseif d1 < d2 then
-               return -1
-       else
-               return 1
-       end
-end
diff --git a/libs/http/luasrc/http/protocol/mime.lua b/libs/http/luasrc/http/protocol/mime.lua
deleted file mode 100644 (file)
index c878160..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
---[[
-
-HTTP protocol implementation for LuCI - mime handling
-(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$
-
-]]--
-
---- LuCI http protocol implementation - mime helper class.
--- This class provides functions to guess mime types from file extensions and
--- vice versa.
-module("luci.http.protocol.mime", package.seeall)
-
-require("luci.util")
-
---- MIME mapping table containg extension - mimetype relations.
--- @class table
-MIME_TYPES = {
-    ["txt"]   = "text/plain";
-    ["js"]    = "text/javascript";
-    ["css"]   = "text/css";
-    ["htm"]   = "text/html";
-    ["html"]  = "text/html";
-    ["patch"] = "text/x-patch";
-    ["c"]     = "text/x-csrc";
-    ["h"]     = "text/x-chdr";
-    ["o"]     = "text/x-object";
-    ["ko"]    = "text/x-object";
-
-    ["bmp"]   = "image/bmp";
-    ["gif"]   = "image/gif";
-    ["png"]   = "image/png";
-    ["jpg"]   = "image/jpeg";
-    ["jpeg"]  = "image/jpeg";
-    ["svg"]   = "image/svg+xml";
-
-    ["zip"]   = "application/zip";
-    ["pdf"]   = "application/pdf";
-    ["xml"]   = "application/xml";
-    ["xsl"]   = "application/xml";
-    ["doc"]   = "application/msword";
-    ["ppt"]   = "application/vnd.ms-powerpoint";
-    ["xls"]   = "application/vnd.ms-excel";
-    ["odt"]   = "application/vnd.oasis.opendocument.text";
-    ["odp"]   = "application/vnd.oasis.opendocument.presentation";
-    ["pl"]    = "application/x-perl";
-    ["sh"]    = "application/x-shellscript";
-    ["php"]   = "application/x-php";
-    ["deb"]   = "application/x-deb";
-    ["iso"]   = "application/x-cd-image";
-    ["tgz"]   = "application/x-compressed-tar";
-
-    ["mp3"]   = "audio/mpeg";
-    ["ogg"]   = "audio/x-vorbis+ogg";
-    ["wav"]   = "audio/x-wav";
-
-    ["mpg"]   = "video/mpeg";
-    ["mpeg"]  = "video/mpeg";
-    ["avi"]   = "video/x-msvideo";
-}
-
---- Extract extension from a filename and return corresponding mime-type or
--- "application/octet-stream" if the extension is unknown.
--- @param filename     The filename for which the mime type is guessed
--- @return                     String containign the determined mime type
-function to_mime(filename)
-       if type(filename) == "string" then
-               local ext = filename:match("[^%.]+$")
-
-               if ext and MIME_TYPES[ext:lower()] then
-                       return MIME_TYPES[ext:lower()]
-               end
-       end
-
-       return "application/octet-stream"
-end
-
---- Return corresponding extension for a given mime type or nil if the
--- given mime-type is unknown.
--- @param mimetype     The mimetype to retrieve the extension from
--- @return                     String with the extension or nil for unknown type
-function to_ext(mimetype)
-       if type(mimetype) == "string" then
-               for ext, type in luci.util.kspairs( MIME_TYPES ) do
-                       if type == mimetype then
-                               return ext
-                       end
-               end
-       end
-
-       return nil
-end
diff --git a/libs/web/luasrc/http/protocol.lua b/libs/web/luasrc/http/protocol.lua
new file mode 100644 (file)
index 0000000..d8ef09f
--- /dev/null
@@ -0,0 +1,690 @@
+--[[
+
+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$
+
+]]--
+
+--- LuCI http protocol class.
+-- This class contains several functions useful for http message- and content
+-- decoding and to retrive form data from raw http messages.
+module("luci.http.protocol", package.seeall)
+
+local ltn12 = require("luci.ltn12")
+
+HTTP_MAX_CONTENT      = 1024*8         -- 8 kB maximum content size
+
+--- Decode an urlencoded string - optionally without decoding
+-- the "+" sign to " " - and return the decoded string.
+-- @param str          Input string in x-www-urlencoded format
+-- @param no_plus      Don't decode "+" signs to spaces
+-- @return                     The decoded string
+-- @see                                urlencode
+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( "%%([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 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.
+-- @param url  The url or string which contains x-www-urlencoded form data
+-- @param tbl  Use the given table for storing values (optional)
+-- @return             Table containing the urldecoded parameters
+-- @see                        urlencode_params
+function urldecode_params( url, tbl )
+
+       local params = tbl or { }
+
+       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 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 to x-www-urlencoded format.
+-- @param str  String to encode
+-- @return             String containing the encoded data
+-- @see                        urldecode
+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 each key-value-pair in given table to x-www-urlencoded format,
+-- separated by "&". Tables are encoded as parameters with multiple values by
+-- repeating the parameter name with each value.
+-- @param tbl  Table with the values
+-- @return             String containing encoded values
+-- @see                        urldecode_params
+function urlencode_params( tbl )
+       local enc = ""
+
+       for k, v in pairs(tbl) do
+               if type(v) == "table" then
+                       for i, v2 in ipairs(v) do
+                               enc = enc .. ( #enc > 0 and "&" or "" ) ..
+                                       urlencode(k) .. "=" .. urlencode(v2)
+                       end
+               else
+                       enc = enc .. ( #enc > 0 and "&" or "" ) ..
+                               urlencode(k) .. "=" .. urlencode(v)
+               end
+       end
+
+       return enc
+end
+
+-- (Internal function)
+-- Initialize given parameter and coerce string into table when the parameter
+-- already exists.
+-- @param tbl  Table where parameter should be created
+-- @param key  Parameter name
+-- @return             Always nil
+local function __initval( tbl, key )
+       if tbl[key] == nil then
+               tbl[key] = ""
+       elseif type(tbl[key]) == "string" then
+               tbl[key] = { tbl[key], "" }
+       else
+               table.insert( tbl[key], "" )
+       end
+end
+
+-- (Internal function)
+-- Append given data to given parameter, either by extending the string value
+-- or by appending it to the last string in the parameter's value table.
+-- @param tbl  Table containing the previously initialized parameter value
+-- @param key  Parameter name
+-- @param chunk        String containing the data to append
+-- @return             Always nil
+-- @see                        __initval
+local function __appendval( tbl, key, chunk )
+       if type(tbl[key]) == "table" then
+               tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk
+       else
+               tbl[key] = tbl[key] .. chunk
+       end
+end
+
+-- (Internal function)
+-- Finish the value of given parameter, either by transforming the string value
+-- or - in the case of multi value parameters - the last element in the
+-- associated values table.
+-- @param tbl          Table containing the previously initialized parameter value
+-- @param key          Parameter name
+-- @param handler      Function which transforms the parameter value
+-- @return                     Always nil
+-- @see                                __initval
+-- @see                                __appendval
+local function __finishval( tbl, key, handler )
+       if handler then
+               if type(tbl[key]) == "table" then
+                       tbl[key][#tbl[key]] = handler( tbl[key][#tbl[key]] )
+               else
+                       tbl[key] = handler( tbl[key] )
+               end
+       end
+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, err )
+
+       if chunk ~= nil then
+               -- ignore empty lines before request
+               if #chunk == 0 then
+                       return true, nil
+               end
+
+               -- 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   = tonumber( 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   = tonumber( 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-Za-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
+
+
+--- Creates a ltn12 source from the given socket. The source will return it's
+-- data line by line with the trailing \r\n stripped of.
+-- @param sock Readable network socket
+-- @return             Ltn12 source function
+function header_source( sock )
+       return ltn12.source.simplify( function()
+
+               local chunk, err, part = sock:receive("*l")
+
+               -- Line too long
+               if chunk == nil then
+                       if err ~= "timeout" then
+                               return nil, part
+                                       and "Line exceeds maximum allowed length"
+                                       or  "Unexpected EOF"
+                       else
+                               return nil, err
+                       end
+
+               -- Line ok
+               elseif chunk ~= nil then
+
+                       -- Strip trailing CR
+                       chunk = chunk:gsub("\r$","")
+
+                       return chunk, nil
+               end
+       end )
+end
+
+--- Decode a mime encoded http message body with multipart/form-data
+-- 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)
+-- @param src          Ltn12 source function
+-- @param msg          HTTP message object
+-- @param filecb       File callback function (optional)
+-- @return                     Value indicating successful operation (not nil means "ok")
+-- @return                     String containing the error if unsuccessful
+-- @see                                parse_message_header
+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 and field.file and filecb then
+                               __initval( msg.params, field.name )
+                               __appendval( msg.params, field.name, field.file )
+
+                               store = filecb
+                       elseif field.name then
+                               __initval( msg.params, field.name )
+
+                               store = function( hdr, buf, eof )
+                                       __appendval( msg.params, field.name, buf )
+                               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 = found or true
+
+                                       data, eof = parse_headers( data:sub( epos + 1, #data ), field )
+                                       inhdr = not eof
+                               end
+                       until not spos
+
+                       if found then
+                               if #data > 78 then
+                                       lchunk = data:sub( #data - 78 + 1, #data )
+                                       data   = data:sub( 1, #data - 78 )
+
+                                       if store then
+                                               store( field, data, false )
+                                       else
+                                               return nil, "Invalid MIME section header"
+                                       end
+                               else
+                                       lchunk, data = data, nil
+                               end
+                       else
+                               if inhdr then
+                                       lchunk, eof = parse_headers( data, field )
+                                       inhdr = not eof
+                               else
+                                       store( field, lchunk, false )
+                                       lchunk, chunk = chunk, nil
+                               end
+                       end
+               end
+
+               return true
+       end
+
+       return ltn12.pump.all( src, snk )
+end
+
+--- Decode an urlencoded http message body with application/x-www-urlencoded
+-- 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.
+-- @param src  Ltn12 source function
+-- @param msg  HTTP message object
+-- @return             Value indicating successful operation (not nil means "ok")
+-- @return             String containing the error if unsuccessful
+-- @see                        parse_message_header
+function urldecode_message_body( src, msg )
+
+       local tlen   = 0
+       local lchunk = nil
+
+       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"
+               elseif tlen > HTTP_MAX_CONTENT then
+                       return nil, "Message body size exceeds maximum allowed length"
+               end
+
+               if not lchunk and chunk then
+                       lchunk = chunk
+
+               elseif lchunk then
+                       local data = lchunk .. ( chunk or "&" )
+                       local spos, epos
+
+                       repeat
+                               spos, epos = data:find("^.-[;&]")
+
+                               if spos then
+                                       local pair = data:sub( spos, epos - 1 )
+                                       local key  = pair:match("^(.-)=")
+                                       local val  = pair:match("=([^%s]*)%s*$")
+
+                                       if key and #key > 0 then
+                                               __initval( msg.params, key )
+                                               __appendval( msg.params, key, val )
+                                               __finishval( msg.params, key, urldecode )
+                                       end
+
+                                       data = data:sub( epos + 1, #data )
+                               end
+                       until not spos
+
+                       lchunk = data
+               end
+
+               return true
+       end
+
+       return ltn12.pump.all( src, snk )
+end
+
+--- Try to extract an http message header including information like protocol
+-- version, message headers and resulting CGI environment variables from the
+-- given ltn12 source.
+-- @param src  Ltn12 source function
+-- @return             HTTP message object
+-- @see                        parse_message_body
+function parse_message_header( src )
+
+       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( src, 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'] or 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
+                               SERVER_PROTOCOL   = "HTTP/" .. string.format("%.1f", msg.http_version);
+                               QUERY_STRING      = msg.request_uri:match("?")
+                                       and msg.request_uri:gsub("^.+?","") or ""
+                       }
+
+                       -- 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
+
+--- Try to extract and decode a http message body from the given ltn12 source.
+-- This function will examine the Content-Type within the given message object
+-- to select the appropriate content decoder.
+-- Currently the application/x-www-urlencoded and application/form-data
+-- mime types are supported. If the encountered content encoding can't be
+-- handled then the whole message body will be stored unaltered as "content"
+-- property within the given message object.
+-- @param src          Ltn12 source function
+-- @param msg          HTTP message object
+-- @param filecb       File data callback (optional, see mimedecode_message_body())
+-- @return                     Value indicating successful operation (not nil means "ok")
+-- @return                     String containing the error if unsuccessful
+-- @see                                parse_message_header
+function parse_message_body( src, 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( src, 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:match("^application/x%-www%-form%-urlencoded")
+       then
+               return urldecode_message_body( src, msg, filecb )
+
+
+       -- Unhandled encoding
+       -- If a file callback is given then feed it chunk by chunk, else
+       -- store whole buffer in message.content
+       else
+
+               local sink
+
+               -- 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, err )
+                               if chunk then
+                                       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
+                               return true
+                       end
+               end
+
+               -- Pump data...
+               while true do
+                       local ok, err = ltn12.pump.step( src, sink )
+
+                       if not ok and err then
+                               return nil, err
+                       elseif not err then
+                               return true
+                       end
+               end
+
+               return true
+       end
+end
+
+--- Table containing human readable messages for several http status codes.
+-- @class table
+statusmsg = {
+       [200] = "OK",
+       [206] = "Partial Content",
+       [301] = "Moved Permanently",
+       [302] = "Found",
+       [304] = "Not Modified",
+       [400] = "Bad Request",
+       [403] = "Forbidden",
+       [404] = "Not Found",
+       [405] = "Method Not Allowed",
+       [408] = "Request Time-out",
+       [411] = "Length Required",
+       [412] = "Precondition Failed",
+       [416] = "Requested range not satisfiable",
+       [500] = "Internal Server Error",
+       [503] = "Server Unavailable",
+}
diff --git a/libs/web/luasrc/http/protocol/conditionals.lua b/libs/web/luasrc/http/protocol/conditionals.lua
new file mode 100644 (file)
index 0000000..75e1f7b
--- /dev/null
@@ -0,0 +1,153 @@
+--[[
+
+HTTP protocol implementation for LuCI - RFC2616 / 14.19, 14.24 - 14.28
+(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$
+
+]]--
+
+--- LuCI http protocol implementation - HTTP/1.1 bits.
+-- This class provides basic ETag handling and implements most of the
+-- conditional HTTP/1.1 headers specified in RFC2616 Sct. 14.24 - 14.28 .
+module("luci.http.protocol.conditionals", package.seeall)
+
+local date = require("luci.http.protocol.date")
+
+
+--- Implement 14.19 / ETag.
+-- @param stat A file.stat structure
+-- @return             String containing the generated tag suitable for ETag headers
+function mk_etag( stat )
+       if stat ~= nil then
+               return string.format( '"%x-%x-%x"', stat.ino, stat.size, stat.mtime )
+       end
+end
+
+--- 14.24 / If-Match
+-- Test whether the given message object contains an "If-Match" header and
+-- compare it against the given stat object.
+-- @param req  HTTP request message object
+-- @param stat A file.stat object
+-- @return             Boolean indicating whether the precondition is ok
+-- @return             Alternative status code if the precondition failed
+function if_match( req, stat )
+       local h    = req.headers
+       local etag = mk_etag( stat )
+
+       -- Check for matching resource
+       if type(h['If-Match']) == "string" then
+               for ent in h['If-Match']:gmatch("([^, ]+)") do
+                       if ( ent == '*' or ent == etag ) and stat ~= nil then
+                               return true
+                       end
+               end
+
+               return false, 412
+       end
+
+       return true
+end
+
+--- 14.25 / If-Modified-Since
+-- Test whether the given message object contains an "If-Modified-Since" header
+-- and compare it against the given stat object.
+-- @param req  HTTP request message object
+-- @param stat A file.stat object
+-- @return             Boolean indicating whether the precondition is ok
+-- @return             Alternative status code if the precondition failed
+-- @return             Table containing extra HTTP headers if the precondition failed
+function if_modified_since( req, stat )
+       local h = req.headers
+
+       -- Compare mtimes
+       if type(h['If-Modified-Since']) == "string" then
+               local since = date.to_unix( h['If-Modified-Since'] )
+
+               if stat == nil or since < stat.mtime then
+                       return true
+               end
+
+               return false, 304, {
+                       ["ETag"]          = mk_etag( stat );
+                       ["Date"]          = date.to_http( os.time() );
+                       ["Last-Modified"] = date.to_http( stat.mtime )
+               }
+       end
+
+       return true
+end
+
+--- 14.26 / If-None-Match
+-- Test whether the given message object contains an "If-None-Match" header and
+-- compare it against the given stat object.
+-- @param req  HTTP request message object
+-- @param stat A file.stat object
+-- @return             Boolean indicating whether the precondition is ok
+-- @return             Alternative status code if the precondition failed
+-- @return             Table containing extra HTTP headers if the precondition failed
+function if_none_match( req, stat )
+       local h      = req.headers
+       local etag   = mk_etag( stat )
+       local method = req.env and req.env.REQUEST_METHOD or "GET"
+
+       -- Check for matching resource
+       if type(h['If-None-Match']) == "string" then
+               for ent in h['If-None-Match']:gmatch("([^, ]+)") do
+                       if ( ent == '*' or ent == etag ) and stat ~= nil then
+                               if method == "GET" or method == "HEAD" then
+                                       return false, 304, {
+                                               ["ETag"]          = etag;
+                                               ["Date"]          = date.to_http( os.time() );
+                                               ["Last-Modified"] = date.to_http( stat.mtime )
+                                       }
+                               else
+                                       return false, 412
+                               end
+                       end
+               end
+       end
+
+       return true
+end
+
+--- 14.27 / If-Range
+-- The If-Range header is currently not implemented due to the lack of general
+-- byte range stuff in luci.http.protocol . This function will always return
+-- false, 412 to indicate a failed precondition.
+-- @param req  HTTP request message object
+-- @param stat A file.stat object
+-- @return             Boolean indicating whether the precondition is ok
+-- @return             Alternative status code if the precondition failed
+function if_range( req, stat )
+       -- Sorry, no subranges (yet)
+       return false, 412
+end
+
+--- 14.28 / If-Unmodified-Since
+-- Test whether the given message object contains an "If-Unmodified-Since"
+-- header and compare it against the given stat object.
+-- @param req  HTTP request message object
+-- @param stat A file.stat object
+-- @return             Boolean indicating whether the precondition is ok
+-- @return             Alternative status code if the precondition failed
+function if_unmodified_since( req, stat )
+       local h = req.headers
+
+       -- Compare mtimes
+       if type(h['If-Unmodified-Since']) == "string" then
+               local since = date.to_unix( h['If-Unmodified-Since'] )
+
+               if stat ~= nil and since <= stat.mtime then
+                       return false, 412
+               end
+       end
+
+       return true
+end
diff --git a/libs/web/luasrc/http/protocol/date.lua b/libs/web/luasrc/http/protocol/date.lua
new file mode 100644 (file)
index 0000000..83d11e2
--- /dev/null
@@ -0,0 +1,115 @@
+--[[
+
+HTTP protocol implementation for LuCI - date handling
+(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$
+
+]]--
+
+--- LuCI http protocol implementation - date helper class.
+-- This class contains functions to parse, compare and format http dates.
+module("luci.http.protocol.date", package.seeall)
+
+require("luci.sys.zoneinfo")
+
+
+MONTHS = {
+       "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
+       "Sep", "Oct", "Nov", "Dec"
+}
+
+--- Return the time offset in seconds between the UTC and given time zone.
+-- @param tz   Symbolic or numeric timezone specifier
+-- @return             Time offset to UTC in seconds
+function tz_offset(tz)
+
+       if type(tz) == "string" then
+
+               -- check for a numeric identifier
+               local s, v = tz:match("([%+%-])([0-9]+)")
+               if s == '+' then s = 1 else s = -1 end
+               if v then v = tonumber(v) end
+
+               if s and v then
+                       return s * 60 * ( math.floor( v / 100 ) * 60 + ( v % 100 ) )
+
+               -- lookup symbolic tz
+               elseif luci.sys.zoneinfo.OFFSET[tz:lower()] then
+                       return luci.sys.zoneinfo.OFFSET[tz:lower()]
+               end
+
+       end
+
+       -- bad luck
+       return 0
+end
+
+--- Parse given HTTP date string and convert it to unix epoch time.
+-- @param data String containing the date
+-- @return             Unix epoch time
+function to_unix(date)
+
+       local wd, day, mon, yr, hr, min, sec, tz = date:match(
+               "([A-Z][a-z][a-z]), ([0-9]+) " ..
+               "([A-Z][a-z][a-z]) ([0-9]+) " ..
+               "([0-9]+):([0-9]+):([0-9]+) " ..
+               "([A-Z0-9%+%-]+)"
+       )
+
+       if day and mon and yr and hr and min and sec then
+               -- find month
+               local month = 1
+               for i = 1, 12 do
+                       if MONTHS[i] == mon then
+                               month = i
+                               break
+                       end
+               end
+
+               -- convert to epoch time
+               return tz_offset(tz) + os.time( {
+                       year  = yr,
+                       month = month,
+                       day   = day,
+                       hour  = hr,
+                       min   = min,
+                       sec   = sec
+               } )
+       end
+
+       return 0
+end
+
+--- Convert the given unix epoch time to valid HTTP date string.
+-- @param time Unix epoch time
+-- @return             String containing the formatted date
+function to_http(time)
+       return os.date( "%a, %d %b %Y %H:%M:%S GMT", time )
+end
+
+--- Compare two dates which can either be unix epoch times or HTTP date strings.
+-- @param d1   The first date or epoch time to compare
+-- @param d2   The first date or epoch time to compare
+-- @return             -1  -  if d1 is lower then d2
+-- @return             0   -  if both dates are equal
+-- @return             1   -  if d1 is higher then d2
+function compare(d1, d2)
+
+       if d1:match("[^0-9]") then d1 = to_unix(d1) end
+       if d2:match("[^0-9]") then d2 = to_unix(d2) end
+
+       if d1 == d2 then
+               return 0
+       elseif d1 < d2 then
+               return -1
+       else
+               return 1
+       end
+end
diff --git a/libs/web/luasrc/http/protocol/mime.lua b/libs/web/luasrc/http/protocol/mime.lua
new file mode 100644 (file)
index 0000000..c878160
--- /dev/null
@@ -0,0 +1,99 @@
+--[[
+
+HTTP protocol implementation for LuCI - mime handling
+(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$
+
+]]--
+
+--- LuCI http protocol implementation - mime helper class.
+-- This class provides functions to guess mime types from file extensions and
+-- vice versa.
+module("luci.http.protocol.mime", package.seeall)
+
+require("luci.util")
+
+--- MIME mapping table containg extension - mimetype relations.
+-- @class table
+MIME_TYPES = {
+    ["txt"]   = "text/plain";
+    ["js"]    = "text/javascript";
+    ["css"]   = "text/css";
+    ["htm"]   = "text/html";
+    ["html"]  = "text/html";
+    ["patch"] = "text/x-patch";
+    ["c"]     = "text/x-csrc";
+    ["h"]     = "text/x-chdr";
+    ["o"]     = "text/x-object";
+    ["ko"]    = "text/x-object";
+
+    ["bmp"]   = "image/bmp";
+    ["gif"]   = "image/gif";
+    ["png"]   = "image/png";
+    ["jpg"]   = "image/jpeg";
+    ["jpeg"]  = "image/jpeg";
+    ["svg"]   = "image/svg+xml";
+
+    ["zip"]   = "application/zip";
+    ["pdf"]   = "application/pdf";
+    ["xml"]   = "application/xml";
+    ["xsl"]   = "application/xml";
+    ["doc"]   = "application/msword";
+    ["ppt"]   = "application/vnd.ms-powerpoint";
+    ["xls"]   = "application/vnd.ms-excel";
+    ["odt"]   = "application/vnd.oasis.opendocument.text";
+    ["odp"]   = "application/vnd.oasis.opendocument.presentation";
+    ["pl"]    = "application/x-perl";
+    ["sh"]    = "application/x-shellscript";
+    ["php"]   = "application/x-php";
+    ["deb"]   = "application/x-deb";
+    ["iso"]   = "application/x-cd-image";
+    ["tgz"]   = "application/x-compressed-tar";
+
+    ["mp3"]   = "audio/mpeg";
+    ["ogg"]   = "audio/x-vorbis+ogg";
+    ["wav"]   = "audio/x-wav";
+
+    ["mpg"]   = "video/mpeg";
+    ["mpeg"]  = "video/mpeg";
+    ["avi"]   = "video/x-msvideo";
+}
+
+--- Extract extension from a filename and return corresponding mime-type or
+-- "application/octet-stream" if the extension is unknown.
+-- @param filename     The filename for which the mime type is guessed
+-- @return                     String containign the determined mime type
+function to_mime(filename)
+       if type(filename) == "string" then
+               local ext = filename:match("[^%.]+$")
+
+               if ext and MIME_TYPES[ext:lower()] then
+                       return MIME_TYPES[ext:lower()]
+               end
+       end
+
+       return "application/octet-stream"
+end
+
+--- Return corresponding extension for a given mime type or nil if the
+-- given mime-type is unknown.
+-- @param mimetype     The mimetype to retrieve the extension from
+-- @return                     String with the extension or nil for unknown type
+function to_ext(mimetype)
+       if type(mimetype) == "string" then
+               for ext, type in luci.util.kspairs( MIME_TYPES ) do
+                       if type == mimetype then
+                               return ext
+                       end
+               end
+       end
+
+       return nil
+end