libs/lucid-http: prepare external indexing and tree building in luci handler
[project/luci.git] / libs / lucid-http / luasrc / lucid / http / handler / file.lua
1 --[[
2
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>
6
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
10
11 http://www.apache.org/licenses/LICENSE-2.0
12
13 $Id$
14
15 ]]--
16
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"
25
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"
30
31 --- File system handler
32 -- @cstyle instance
33 module "luci.lucid.http.handler.file"
34
35 --- Create a simple file system handler.
36 -- @class function
37 -- @param name Name
38 -- @param docroot Physical Document Root
39 -- @param options Options
40 -- @return Simple file system handler object
41 Simple = util.class(srv.Handler)
42
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)
47
48 options = options or {}
49 self.dirlist = not options.noindex
50 self.error404 = options.error404
51 end
52
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
59 return true
60 end
61
62 local from, to = request.headers.Range:match("bytes=([0-9]*)-([0-9]*)")
63 if not (from or to) then
64 return true
65 end
66
67 from, to = tonumber(from), tonumber(to)
68 if not (from or to) then
69 return true
70 elseif not from then
71 from, to = size - to, size - 1
72 elseif not to then
73 to = size - 1
74 end
75
76 -- Not satisfiable
77 if from >= size then
78 return false
79 end
80
81 -- Normalize
82 if to >= size then
83 to = size - 1
84 end
85
86 local range = "bytes " .. from .. "-" .. to .. "/" .. size
87 return from, (1 + to - from), range
88 end
89
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)
96 end
97 local file = fs.realpath(self.docroot .. uri)
98 if not file or file:sub(1, #self.realdocroot) ~= self.realdocroot then
99 return uri
100 end
101 return file, fs.stat(file)
102 end
103
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))
109
110 if stat then
111 if stat.type == "reg" then
112
113 -- Generate Entity Tag
114 local etag = cond.mk_etag( stat )
115
116 -- Check conditionals
117 local ok, code, hdrs
118
119 ok, code, hdrs = cond.if_modified_since( request, stat )
120 if ok then
121 ok, code, hdrs = cond.if_match( request, stat )
122 if ok then
123 ok, code, hdrs = cond.if_unmodified_since( request, stat )
124 if ok then
125 ok, code, hdrs = cond.if_none_match( request, stat )
126 if ok then
127 local f, err = nixio.open(file)
128
129 if f then
130 local code = 200
131 local o, s, r = self:parse_range(request, stat.size)
132
133 if not o then
134 return self:failure(416, "Invalid Range")
135 end
136
137 local headers = {
138 ["Last-Modified"] = date.to_http( stat.mtime ),
139 ["Content-Type"] = mime.to_mime( file ),
140 ["ETag"] = etag,
141 ["Accept-Ranges"] = "bytes",
142 }
143
144 if o == true then
145 s = stat.size
146 else
147 code = 206
148 headers["Content-Range"] = r
149 f:seek(o)
150 end
151
152 headers["Content-Length"] = s
153
154 -- Send Response
155 return code, headers, srv.IOResource(f, s)
156 else
157 return self:failure( 403, err:gsub("^.+: ", "") )
158 end
159 else
160 return code, hdrs
161 end
162 else
163 return code, hdrs
164 end
165 else
166 return code, hdrs
167 end
168 else
169 return code, hdrs
170 end
171
172 elseif stat.type == "dir" then
173
174 local ruri = request.env.REQUEST_URI:gsub("/$", "")
175 local duri = prot.urldecode( ruri, true )
176 local root = self.docroot
177
178 -- check for index files
179 local index_candidates = {
180 "index.html", "index.htm", "default.html", "default.htm",
181 "index.txt", "default.txt"
182 }
183
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
188 )
189
190 if istat ~= nil and istat.type == "reg" then
191 return 302, { Location = ruri .. "/" .. candidate }
192 end
193 end
194
195
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' ..
202 '<head>\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 } ' ..
208 'p { margin:0 }' ..
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>',
213 duri, duri, ruri
214 )
215
216 local entries = fs.dir( file )
217
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 )
221
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 )
229 )
230 else
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,
237 mime.to_mime( e ),
238 estat.size, date.to_http( estat.mtime )
239 )
240 end
241 end
242
243 html = html .. '</ul><hr /><address>LuCId-HTTPd' ..
244 '</address></body></html>'
245
246 return 200, {
247 ["Date"] = date.to_http( os.time() );
248 ["Content-Type"] = "text/html; charset=utf-8";
249 }, ltn12.source.string(html)
250 else
251 return self:failure(403, "Permission denied")
252 end
253 else
254 return self:failure(403, "Unable to transmit " .. stat.type .. " " .. file)
255 end
256 else
257 if self.error404 then
258 return 302, { Location = self.error404 }
259 else
260 return self:failure(404, "No such file: " .. file)
261 end
262 end
263 end
264
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(...)
270 return stat, head
271 end