--[[ 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 io = require "io" local os = require "os" local fs = require "luci.fs" local util = require "luci.util" local ltn12 = require "luci.ltn12" local mod = require "luci.ttpd.module" local srv = require "luci.ttpd.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" module "luci.ttpd.handler.file" Simple = util.class(mod.Handler) Response = mod.Response function Simple.__init__(self, docroot, dirlist) mod.Handler.__init__(self) self.docroot = docroot self.dirlist = dirlist and true or false end 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 function Simple.getfile(self, uri) local file = self.docroot .. uri:gsub("%.%./+", "") local stat = fs.stat(file) return file, stat end function Simple.handle_get(self, request, sourcein, sinkerr) local file, stat = self:getfile( prot.urldecode( request.env.PATH_INFO, true ) ) if stat then if stat.type == "regular" 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 = io.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 = { ["Last-Modified"] = date.to_http( stat.mtime ), ["Content-Type"] = mime.to_mime( file ), ["ETag"] = etag, ["Accept-Ranges"] = "bytes", } if o == true then o = 0 s = stat.size else code = 206 headers["Content-Range"] = r end headers["Content-Length"] = s -- Send Response return Response(code, headers), srv.IOResource(f, o, s) else return self:failure( 403, err:gsub("^.+: ", "") ) end else return Response( code, hdrs or { } ) end else return Response( code, hdrs or { } ) end else return Response( code, hdrs or { } ) end else return Response( code, hdrs or { } ) end elseif stat.type == "directory" then local ruri = request.request_uri:gsub("/$","") local duri = prot.urldecode( ruri, true ) local root = self.docroot:gsub("/$","") -- 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 == "regular" then return Response( 302, { ["Location"] = ruri .. "/" .. candidate } ) end end local html = string.format( '<?xml version="1.0" encoding="ISO-8859-15"?>\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 { background-color:#fbb034; color:#ffffff } ' .. 'li { border-bottom:1px dotted #CCCCCC; padding:3px } ' .. 'small { font-size:60%%; color:#ffffff } ' .. 'p { margin:0 }' .. '\n--></style></head><body><h1>Index of %s/</h1><hr /><ul>', duri, duri ) local entries = fs.dir( file ) if type(entries) == "table" then for i, e in util.spairs( entries, function(a,b) if entries[a] == '..' then return true elseif entries[b] == '..' then return false else return ( entries[a] < entries[b] ) end end ) do if e ~= '.' and ( e == '..' or e:sub(1,1) ~= '.' ) then local estat = fs.stat( file .. "/" .. e ) if estat.type == "directory" 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 end html = html .. '</ul><hr /></body></html>' return Response( 200, { ["Date"] = date.to_http( os.time() ); ["Content-Type"] = "text/html; charset=ISO-8859-15"; } ), 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 return self:failure(404, "No such file: " .. file) end end function Simple.handle_head(self, ...) return (self:handle_get(...)) end