From: Jo-Philipp Wich Date: Tue, 12 Oct 2010 05:22:38 +0000 (+0000) Subject: libs: merge libs/http into libs/web X-Git-Tag: 0.10.0~626 X-Git-Url: https://git.archive.openwrt.org/?a=commitdiff_plain;h=b6358aaea4e997d2d2466139e702c7951bd29d7b;p=project%2Fluci.git libs: merge libs/http into libs/web --- diff --git a/contrib/package/luci/Makefile b/contrib/package/luci/Makefile index d38a1ac18..70c76486a 100644 --- a/contrib/package/luci/Makefile +++ b/contrib/package/luci/Makefile @@ -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 index d8ef09f7a..000000000 --- a/libs/http/luasrc/http/protocol.lua +++ /dev/null @@ -1,690 +0,0 @@ ---[[ - -HTTP protocol implementation for LuCI -(c) 2008 Freifunk Leipzig / Jo-Philipp Wich - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -$Id$ - -]]-- - ---- 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 index 75e1f7b37..000000000 --- a/libs/http/luasrc/http/protocol/conditionals.lua +++ /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 - -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 index 83d11e2c2..000000000 --- a/libs/http/luasrc/http/protocol/date.lua +++ /dev/null @@ -1,115 +0,0 @@ ---[[ - -HTTP protocol implementation for LuCI - date handling -(c) 2008 Freifunk Leipzig / Jo-Philipp Wich - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -$Id$ - -]]-- - ---- 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 index c87816066..000000000 --- a/libs/http/luasrc/http/protocol/mime.lua +++ /dev/null @@ -1,99 +0,0 @@ ---[[ - -HTTP protocol implementation for LuCI - mime handling -(c) 2008 Freifunk Leipzig / Jo-Philipp Wich - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -$Id$ - -]]-- - ---- 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 index 000000000..d8ef09f7a --- /dev/null +++ b/libs/web/luasrc/http/protocol.lua @@ -0,0 +1,690 @@ +--[[ + +HTTP protocol implementation for LuCI +(c) 2008 Freifunk Leipzig / Jo-Philipp Wich + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +--- 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 index 000000000..75e1f7b37 --- /dev/null +++ b/libs/web/luasrc/http/protocol/conditionals.lua @@ -0,0 +1,153 @@ +--[[ + +HTTP protocol implementation for LuCI - RFC2616 / 14.19, 14.24 - 14.28 +(c) 2008 Freifunk Leipzig / Jo-Philipp Wich + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +--- 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 index 000000000..83d11e2c2 --- /dev/null +++ b/libs/web/luasrc/http/protocol/date.lua @@ -0,0 +1,115 @@ +--[[ + +HTTP protocol implementation for LuCI - date handling +(c) 2008 Freifunk Leipzig / Jo-Philipp Wich + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +--- 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 index 000000000..c87816066 --- /dev/null +++ b/libs/web/luasrc/http/protocol/mime.lua @@ -0,0 +1,99 @@ +--[[ + +HTTP protocol implementation for LuCI - mime handling +(c) 2008 Freifunk Leipzig / Jo-Philipp Wich + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +--- 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