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 --- File system handler
33 module "luci.lucid.http.handler.file"
35 --- Create a simple file system handler.
38 -- @param docroot Physical Document Root
39 -- @param options Options
40 -- @return Simple file system handler object
41 Simple = util.class(srv.Handler)
43 function Simple.__init__(self, name, docroot, options)
44 srv.Handler.__init__(self, name)
45 self.docroot = docroot
46 self.realdocroot = fs.realpath(self.docroot)
48 options = options or {}
49 self.dirlist = not options.noindex
50 self.error404 = options.error404
53 --- Parse a range request.
54 -- @param request Request object
55 -- @param size File size
56 -- @return offset, length, range header or boolean status
57 function Simple.parse_range(self, request, size)
58 if not request.headers.Range then
62 local from, to = request.headers.Range:match("bytes=([0-9]*)-([0-9]*)")
63 if not (from or to) then
67 from, to = tonumber(from), tonumber(to)
68 if not (from or to) then
71 from, to = size - to, size - 1
86 local range = "bytes " .. from .. "-" .. to .. "/" .. size
87 return from, (1 + to - from), range
90 --- Translate path and return file information.
91 -- @param uri Request URI
92 -- @return physical file path, file information
93 function Simple.getfile(self, uri)
94 if not self.realdocroot then
95 self.realdocroot = fs.realpath(self.docroot)
97 local file = fs.realpath(self.docroot .. uri)
98 if not file or file:sub(1, #self.realdocroot) ~= self.realdocroot then
101 return file, fs.stat(file)
104 --- Handle a GET request.
105 -- @param request Request object
106 -- @return status code, header table, response source
107 function Simple.handle_GET(self, request)
108 local file, stat = self:getfile(prot.urldecode(request.env.PATH_INFO, true))
111 if stat.type == "reg" then
113 -- Generate Entity Tag
114 local etag = cond.mk_etag( stat )
116 -- Check conditionals
119 ok, code, hdrs = cond.if_modified_since( request, stat )
121 ok, code, hdrs = cond.if_match( request, stat )
123 ok, code, hdrs = cond.if_unmodified_since( request, stat )
125 ok, code, hdrs = cond.if_none_match( request, stat )
127 local f, err = nixio.open(file)
131 local o, s, r = self:parse_range(request, stat.size)
134 return self:failure(416, "Invalid Range")
138 ["Last-Modified"] = date.to_http( stat.mtime ),
139 ["Content-Type"] = mime.to_mime( file ),
141 ["Accept-Ranges"] = "bytes",
148 headers["Content-Range"] = r
152 headers["Content-Length"] = s
155 return code, headers, srv.IOResource(f, s)
157 return self:failure( 403, err:gsub("^.+: ", "") )
172 elseif stat.type == "dir" then
174 local ruri = request.env.REQUEST_URI:gsub("/$", "")
175 local duri = prot.urldecode( ruri, true )
176 local root = self.docroot
178 -- check for index files
179 local index_candidates = {
180 "index.html", "index.htm", "default.html", "default.htm",
181 "index.txt", "default.txt"
184 -- try to find an index file and redirect to it
185 for i, candidate in ipairs( index_candidates ) do
186 local istat = fs.stat(
187 root .. "/" .. duri .. "/" .. candidate
190 if istat ~= nil and istat.type == "reg" then
191 return 302, { Location = ruri .. "/" .. candidate }
196 local html = string.format(
197 '<?xml version="1.0" encoding="utf-8"?>\n' ..
198 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' ..
199 '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'..
200 '<html xmlns="http://www.w3.org/1999/xhtml" ' ..
201 'xml:lang="en" lang="en">\n' ..
203 '<title>Index of %s/</title>\n' ..
204 '<style type="text/css">\n' ..
205 'body { color:#000000 } ' ..
206 'li { border-bottom:1px dotted #CCCCCC; padding:3px } ' ..
207 'small { font-size:60%%; color:#333333 } ' ..
209 '\n</style></head><body><h1>Index of %s/</h1><hr /><ul>'..
210 '<li><p><a href="%s/../">../</a> ' ..
211 '<small>(parent directory)</small><br />' ..
212 '<small></small></li>',
216 local entries = fs.dir( file )
218 if type(entries) == "function" then
219 for i, e in util.vspairs(nixio.util.consume(entries)) do
220 local estat = fs.stat( file .. "/" .. e )
222 if estat.type == "dir" then
223 html = html .. string.format(
224 '<li><p><a href="%s/%s/">%s/</a> ' ..
225 '<small>(directory)</small><br />' ..
226 '<small>Changed: %s</small></li>',
227 ruri, prot.urlencode( e ), e,
228 date.to_http( estat.mtime )
231 html = html .. string.format(
232 '<li><p><a href="%s/%s">%s</a> ' ..
233 '<small>(%s)</small><br />' ..
234 '<small>Size: %i Bytes | ' ..
235 'Changed: %s</small></li>',
236 ruri, prot.urlencode( e ), e,
238 estat.size, date.to_http( estat.mtime )
243 html = html .. '</ul><hr /><address>LuCId-HTTPd' ..
244 '</address></body></html>'
247 ["Date"] = date.to_http( os.time() );
248 ["Content-Type"] = "text/html; charset=utf-8";
249 }, ltn12.source.string(html)
251 return self:failure(403, "Permission denied")
254 return self:failure(403, "Unable to transmit " .. stat.type .. " " .. file)
257 if self.error404 then
258 return 302, { Location = self.error404 }
260 return self:failure(404, "No such file: " .. file)
265 --- Handle a HEAD request.
266 -- @param request Request object
267 -- @return status code, header table, response source
268 function Simple.handle_HEAD(self, ...)
269 local stat, head = self:handle_GET(...)