3 HTTP server implementation for LuCI - file handler
4 (c) 2008 Steven Barth <steven@midlink.org>
5 (c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net>
7 Licensed under the Apache License, Version 2.0 (the "License");
8 you may not use this file except in compliance with the License.
9 You may obtain a copy of the License at
11 http://www.apache.org/licenses/LICENSE-2.0
17 local ipairs, type, tonumber = ipairs, type, tonumber
18 local os = require "os"
19 local nixio = require "nixio", require "nixio.util"
20 local fs = require "nixio.fs"
21 local util = require "luci.util"
22 local ltn12 = require "luci.ltn12"
23 local srv = require "luci.lucid.http.server"
24 local string = require "string"
26 local prot = require "luci.http.protocol"
27 local date = require "luci.http.protocol.date"
28 local mime = require "luci.http.protocol.mime"
29 local cond = require "luci.http.protocol.conditionals"
31 module "luci.lucid.http.handler.file"
33 Simple = util.class(srv.Handler)
35 function Simple.__init__(self, name, docroot, options)
36 srv.Handler.__init__(self, name)
37 self.docroot = docroot
38 self.realdocroot = fs.realpath(self.docroot)
40 options = options or {}
41 self.dirlist = not options.noindex
42 self.error404 = options.error404
45 function Simple.parse_range(self, request, size)
46 if not request.headers.Range then
50 local from, to = request.headers.Range:match("bytes=([0-9]*)-([0-9]*)")
51 if not (from or to) then
55 from, to = tonumber(from), tonumber(to)
56 if not (from or to) then
59 from, to = size - to, size - 1
74 local range = "bytes " .. from .. "-" .. to .. "/" .. size
75 return from, (1 + to - from), range
78 function Simple.getfile(self, uri)
79 if not self.realdocroot then
80 self.realdocroot = fs.realpath(self.docroot)
82 local file = fs.realpath(self.docroot .. uri)
83 if not file or file:sub(1, #self.realdocroot) ~= self.realdocroot then
86 return file, fs.stat(file)
89 function Simple.handle_GET(self, request)
90 local file, stat = self:getfile(prot.urldecode(request.env.PATH_INFO, true))
93 if stat.type == "reg" then
95 -- Generate Entity Tag
96 local etag = cond.mk_etag( stat )
101 ok, code, hdrs = cond.if_modified_since( request, stat )
103 ok, code, hdrs = cond.if_match( request, stat )
105 ok, code, hdrs = cond.if_unmodified_since( request, stat )
107 ok, code, hdrs = cond.if_none_match( request, stat )
109 local f, err = nixio.open(file)
113 local o, s, r = self:parse_range(request, stat.size)
116 return self:failure(416, "Invalid Range")
120 ["Last-Modified"] = date.to_http( stat.mtime ),
121 ["Content-Type"] = mime.to_mime( file ),
123 ["Accept-Ranges"] = "bytes",
130 headers["Content-Range"] = r
134 headers["Content-Length"] = s
137 return code, headers, srv.IOResource(f, s)
139 return self:failure( 403, err:gsub("^.+: ", "") )
154 elseif stat.type == "dir" then
156 local ruri = request.env.REQUEST_URI:gsub("/$", "")
157 local duri = prot.urldecode( ruri, true )
158 local root = self.docroot
160 -- check for index files
161 local index_candidates = {
162 "index.html", "index.htm", "default.html", "default.htm",
163 "index.txt", "default.txt"
166 -- try to find an index file and redirect to it
167 for i, candidate in ipairs( index_candidates ) do
168 local istat = fs.stat(
169 root .. "/" .. duri .. "/" .. candidate
172 if istat ~= nil and istat.type == "reg" then
173 return 302, { Location = ruri .. "/" .. candidate }
178 local html = string.format(
179 '<?xml version="1.0" encoding="utf-8"?>\n' ..
180 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' ..
181 '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'..
182 '<html xmlns="http://www.w3.org/1999/xhtml" ' ..
183 'xml:lang="en" lang="en">\n' ..
185 '<title>Index of %s/</title>\n' ..
186 '<style type="text/css">\n' ..
187 'body { color:#000000 } ' ..
188 'li { border-bottom:1px dotted #CCCCCC; padding:3px } ' ..
189 'small { font-size:60%%; color:#333333 } ' ..
191 '\n</style></head><body><h1>Index of %s/</h1><hr /><ul>'..
192 '<li><p><a href="%s/../">../</a> ' ..
193 '<small>(parent directory)</small><br />' ..
194 '<small></small></li>',
198 local entries = fs.dir( file )
200 if type(entries) == "function" then
201 for i, e in util.vspairs(nixio.util.consume(entries)) do
202 local estat = fs.stat( file .. "/" .. e )
204 if estat.type == "dir" then
205 html = html .. string.format(
206 '<li><p><a href="%s/%s/">%s/</a> ' ..
207 '<small>(directory)</small><br />' ..
208 '<small>Changed: %s</small></li>',
209 ruri, prot.urlencode( e ), e,
210 date.to_http( estat.mtime )
213 html = html .. string.format(
214 '<li><p><a href="%s/%s">%s</a> ' ..
215 '<small>(%s)</small><br />' ..
216 '<small>Size: %i Bytes | ' ..
217 'Changed: %s</small></li>',
218 ruri, prot.urlencode( e ), e,
220 estat.size, date.to_http( estat.mtime )
225 html = html .. '</ul><hr /><address>LuCId-HTTPd' ..
226 '</address></body></html>'
229 ["Date"] = date.to_http( os.time() );
230 ["Content-Type"] = "text/html; charset=utf-8";
231 }, ltn12.source.string(html)
233 return self:failure(403, "Permission denied")
236 return self:failure(403, "Unable to transmit " .. stat.type .. " " .. file)
239 if self.error404 then
240 return 302, { Location = self.error404 }
242 return self:failure(404, "No such file: " .. file)
247 function Simple.handle_HEAD(self, ...)
248 local stat, head = self:handle_GET(...)