--[[ HTTP server implementation for LuCI - file handler (c) 2008 Steven Barth (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$ ]]-- 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 = { ["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( '\n' .. '\n'.. '\n' .. '\n' .. 'Index of %s/\n' .. '

Index of %s/



LuCId-HTTPd' .. '
' 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