luci/libs/lucid-http/luasrc/lucid/http/handler/file.lua
Jo-Philipp Wich 001e15a0db [PATCH] Explicit Cache-Control for lucid.http static content
The Chrome web browser revalidates every resource if no explicit Cache-Control or Expires HTTP/1.1 header is sent. This makes the page loads appear to take a long time on pages with a few external resources, adding 300-500ms 
per item. This includes the XHR json responses that set page images, like wireless signal indicators and the like-- the images are revalidated on every XHR response. As an example, the Network -> Interfaces page generates 16 
requests to the lucid http server:

    Main HTML
    cascade.css
    xhr.js
    tabbg.png
    cbi.js
    loading.gif
    ethernet_disabled.png
    reload.png
    reset.gif
    edit.gif
    remove.gif
    add.gif
    bridge.png
    vlan.png
    wifi.png
    iface_status 

Of those, 14 should be pulled from cache but they are all valdiated. The lucid server returns the correct 304 (Not Modified) responses but it delays the apparent page load time because of the backlog it creates at the http 
server.

I would suggest setting explicit cache control on all files returned by the lucid http directory dispatcher. The "Expires" header is reportedly more widely supported, however this relies on the clock on the OpenWrt? system 
being accurate, which may not be the case. The "Cache-Control: max-age=" allows the server to set a timeout in seconds. I've included a patch that sets revalidate interval to 1 year, which is the value recommended by google.
Reference:  http://code.google.com/speed/page-speed/docs/caching.html

Note this could create an issue if there are luci application which are generating files which change that are being served by the lucid http DirectoryPublisher?. I'm not sure if there is anyone doing that. If needed, this can 
probably be created as an option to the DirectoryPublisher? config stanza for each vhost.

Finally, this only affects the Google Chrome browser, as both IE9 and Firefox seem to have their own revalidation interval in the absence of explicit cache control which may be based on the last modified time of the resource. 
Even in Chrome, this change doesn't take effect until the item is re-served with a 200 HTTP response so Chrome's cache should be cleared after this patch is applied. The patch can be extended to include cache control on 304 
responses, but I'd not worry about cluttering the code with it because the problem will solve itself once chrome redownloads the resource.
2012-01-09 00:04:54 +00:00

272 lines
7.3 KiB
Lua

--[[
HTTP server implementation for LuCI - file handler
(c) 2008 Steven Barth <steven@midlink.org>
(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$
]]--
local ipairs, type, tonumber = ipairs, type, tonumber
local os = require "os"
local nixio = require "nixio", require "nixio.util"
local fs = require "nixio.fs"
local util = require "luci.util"
local ltn12 = require "luci.ltn12"
local srv = require "luci.lucid.http.server"
local string = require "string"
local prot = require "luci.http.protocol"
local date = require "luci.http.protocol.date"
local mime = require "luci.http.protocol.mime"
local cond = require "luci.http.protocol.conditionals"
--- File system handler
-- @cstyle instance
module "luci.lucid.http.handler.file"
--- Create a simple file system handler.
-- @class function
-- @param name Name
-- @param docroot Physical Document Root
-- @param options Options
-- @return Simple file system handler object
Simple = util.class(srv.Handler)
function Simple.__init__(self, name, docroot, options)
srv.Handler.__init__(self, name)
self.docroot = docroot
self.realdocroot = fs.realpath(self.docroot)
options = options or {}
self.dirlist = not options.noindex
self.error404 = options.error404
end
--- Parse a range request.
-- @param request Request object
-- @param size File size
-- @return offset, length, range header or boolean status
function Simple.parse_range(self, request, size)
if not request.headers.Range then
return true
end
local from, to = request.headers.Range:match("bytes=([0-9]*)-([0-9]*)")
if not (from or to) then
return true
end
from, to = tonumber(from), tonumber(to)
if not (from or to) then
return true
elseif not from then
from, to = size - to, size - 1
elseif not to then
to = size - 1
end
-- Not satisfiable
if from >= size then
return false
end
-- Normalize
if to >= size then
to = size - 1
end
local range = "bytes " .. from .. "-" .. to .. "/" .. size
return from, (1 + to - from), range
end
--- Translate path and return file information.
-- @param uri Request URI
-- @return physical file path, file information
function Simple.getfile(self, uri)
if not self.realdocroot then
self.realdocroot = fs.realpath(self.docroot)
end
local file = fs.realpath(self.docroot .. uri)
if not file or file:sub(1, #self.realdocroot) ~= self.realdocroot then
return uri
end
return file, fs.stat(file)
end
--- Handle a GET request.
-- @param request Request object
-- @return status code, header table, response source
function Simple.handle_GET(self, request)
local file, stat = self:getfile(prot.urldecode(request.env.PATH_INFO, true))
if stat then
if stat.type == "reg" then
-- Generate Entity Tag
local etag = cond.mk_etag( stat )
-- Check conditionals
local ok, code, hdrs
ok, code, hdrs = cond.if_modified_since( request, stat )
if ok then
ok, code, hdrs = cond.if_match( request, stat )
if ok then
ok, code, hdrs = cond.if_unmodified_since( request, stat )
if ok then
ok, code, hdrs = cond.if_none_match( request, stat )
if ok then
local f, err = nixio.open(file)
if f then
local code = 200
local o, s, r = self:parse_range(request, stat.size)
if not o then
return self:failure(416, "Invalid Range")
end
local headers = {
["Cache-Control"] = "max-age=29030400",
["Last-Modified"] = date.to_http( stat.mtime ),
["Content-Type"] = mime.to_mime( file ),
["ETag"] = etag,
["Accept-Ranges"] = "bytes",
}
if o == true then
s = stat.size
else
code = 206
headers["Content-Range"] = r
f:seek(o)
end
headers["Content-Length"] = s
-- Send Response
return code, headers, srv.IOResource(f, s)
else
return self:failure( 403, err:gsub("^.+: ", "") )
end
else
return code, hdrs
end
else
return code, hdrs
end
else
return code, hdrs
end
else
return code, hdrs
end
elseif stat.type == "dir" then
local ruri = request.env.REQUEST_URI:gsub("/$", "")
local duri = prot.urldecode( ruri, true )
local root = self.docroot
-- check for index files
local index_candidates = {
"index.html", "index.htm", "default.html", "default.htm",
"index.txt", "default.txt"
}
-- try to find an index file and redirect to it
for i, candidate in ipairs( index_candidates ) do
local istat = fs.stat(
root .. "/" .. duri .. "/" .. candidate
)
if istat ~= nil and istat.type == "reg" then
return 302, { Location = ruri .. "/" .. candidate }
end
end
local html = string.format(
'<?xml version="1.0" encoding="utf-8"?>\n' ..
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' ..
'"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'..
'<html xmlns="http://www.w3.org/1999/xhtml" ' ..
'xml:lang="en" lang="en">\n' ..
'<head>\n' ..
'<title>Index of %s/</title>\n' ..
'<style type="text/css">\n' ..
'body { color:#000000 } ' ..
'li { border-bottom:1px dotted #CCCCCC; padding:3px } ' ..
'small { font-size:60%%; color:#333333 } ' ..
'p { margin:0 }' ..
'\n</style></head><body><h1>Index of %s/</h1><hr /><ul>'..
'<li><p><a href="%s/../">../</a> ' ..
'<small>(parent directory)</small><br />' ..
'<small></small></li>',
duri, duri, ruri
)
local entries = fs.dir( file )
if type(entries) == "function" then
for i, e in util.vspairs(nixio.util.consume(entries)) do
local estat = fs.stat( file .. "/" .. e )
if estat.type == "dir" then
html = html .. string.format(
'<li><p><a href="%s/%s/">%s/</a> ' ..
'<small>(directory)</small><br />' ..
'<small>Changed: %s</small></li>',
ruri, prot.urlencode( e ), e,
date.to_http( estat.mtime )
)
else
html = html .. string.format(
'<li><p><a href="%s/%s">%s</a> ' ..
'<small>(%s)</small><br />' ..
'<small>Size: %i Bytes | ' ..
'Changed: %s</small></li>',
ruri, prot.urlencode( e ), e,
mime.to_mime( e ),
estat.size, date.to_http( estat.mtime )
)
end
end
html = html .. '</ul><hr /><address>LuCId-HTTPd' ..
'</address></body></html>'
return 200, {
["Date"] = date.to_http( os.time() );
["Content-Type"] = "text/html; charset=utf-8";
}, ltn12.source.string(html)
else
return self:failure(403, "Permission denied")
end
else
return self:failure(403, "Unable to transmit " .. stat.type .. " " .. file)
end
else
if self.error404 then
return 302, { Location = self.error404 }
else
return self:failure(404, "No such file: " .. file)
end
end
end
--- Handle a HEAD request.
-- @param request Request object
-- @return status code, header table, response source
function Simple.handle_HEAD(self, ...)
local stat, head = self:handle_GET(...)
return stat, head
end