build: split into luci and luci-addons packages
[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                                                                         ["Cache-Control"]  = "max-age=29030400",
139                                                                         ["Last-Modified"]  = date.to_http( stat.mtime ),
140                                                                         ["Content-Type"]   = mime.to_mime( file ),
141                                                                         ["ETag"]           = etag,
142                                                                         ["Accept-Ranges"]  = "bytes",
143                                                                 }
144
145                                                                 if o == true then
146                                                                         s = stat.size
147                                                                 else
148                                                                         code = 206
149                                                                         headers["Content-Range"] = r
150                                                                         f:seek(o)
151                                                                 end
152                                                                 
153                                                                 headers["Content-Length"] = s
154
155                                                                 -- Send Response
156                                                                 return code, headers, srv.IOResource(f, s)
157                                                         else
158                                                                 return self:failure( 403, err:gsub("^.+: ", "") )
159                                                         end
160                                                 else
161                                                         return code, hdrs
162                                                 end
163                                         else
164                                                 return code, hdrs
165                                         end
166                                 else
167                                         return code, hdrs
168                                 end
169                         else
170                                 return code, hdrs
171                         end
172
173                 elseif stat.type == "dir" then
174
175                         local ruri = request.env.REQUEST_URI:gsub("/$", "")
176                         local duri = prot.urldecode( ruri, true )
177                         local root = self.docroot
178
179                         -- check for index files
180                         local index_candidates = {
181                                 "index.html", "index.htm", "default.html", "default.htm",
182                                 "index.txt", "default.txt"
183                         }
184
185                         -- try to find an index file and redirect to it
186                         for i, candidate in ipairs( index_candidates ) do
187                                 local istat = fs.stat(
188                                         root .. "/" .. duri .. "/" .. candidate
189                                 )
190
191                                 if istat ~= nil and istat.type == "reg" then
192                                         return 302, { Location = ruri .. "/" .. candidate }
193                                 end
194                         end
195
196
197                         local html = string.format(
198                                 '<?xml version="1.0" encoding="utf-8"?>\n' ..
199                                 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" '     ..
200                                         '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n'..
201                                 '<html xmlns="http://www.w3.org/1999/xhtml" '                           ..
202                                         'xml:lang="en" lang="en">\n'                                                    ..
203                                 '<head>\n'                                                                                                      ..
204                                 '<title>Index of %s/</title>\n'                                                         ..
205                                 '<style type="text/css">\n'                                                                     ..
206                                         'body { color:#000000 } '                                                               ..
207                                         'li { border-bottom:1px dotted #CCCCCC; padding:3px } ' ..
208                                         'small { font-size:60%%; color:#333333 } '                              ..
209                                         'p { margin:0 }'                                                                                ..
210                                         '\n</style></head><body><h1>Index of %s/</h1><hr /><ul>'..
211                                         '<li><p><a href="%s/../">../</a> '                                              ..
212                                         '<small>(parent directory)</small><br />'                               ..
213                                         '<small></small></li>',
214                                         duri, duri, ruri 
215                         )
216
217                         local entries = fs.dir( file )
218
219                         if type(entries) == "function" then
220                                 for i, e in util.vspairs(nixio.util.consume(entries)) do
221                                         local estat = fs.stat( file .. "/" .. e )
222
223                                         if estat.type == "dir" then
224                                                 html = html .. string.format(
225                                                         '<li><p><a href="%s/%s/">%s/</a> '           ..
226                                                         '<small>(directory)</small><br />'           ..
227                                                         '<small>Changed: %s</small></li>',
228                                                                 ruri, prot.urlencode( e ), e,
229                                                                 date.to_http( estat.mtime )
230                                                 )
231                                         else
232                                                 html = html .. string.format(
233                                                         '<li><p><a href="%s/%s">%s</a> '             ..
234                                                         '<small>(%s)</small><br />'                  ..
235                                                         '<small>Size: %i Bytes | '                   ..
236                                                                 'Changed: %s</small></li>',
237                                                                 ruri, prot.urlencode( e ), e,
238                                                                 mime.to_mime( e ),
239                                                                 estat.size, date.to_http( estat.mtime )
240                                                 )
241                                         end
242                                 end
243
244                                 html = html .. '</ul><hr /><address>LuCId-HTTPd' .. 
245                                         '</address></body></html>'
246
247                                 return 200, {
248                                                 ["Date"]         = date.to_http( os.time() );
249                                                 ["Content-Type"] = "text/html; charset=utf-8";
250                                         }, ltn12.source.string(html)
251                         else
252                                 return self:failure(403, "Permission denied")
253                         end
254                 else
255                         return self:failure(403, "Unable to transmit " .. stat.type .. " " .. file)
256                 end
257         else
258                 if self.error404 then
259                         return 302, { Location = self.error404 }
260                 else
261                         return self:failure(404, "No such file: " .. file)
262                 end
263         end
264 end
265
266 --- Handle a HEAD request.
267 -- @param request Request object
268 -- @return status code, header table, response source
269 function Simple.handle_HEAD(self, ...)
270         local stat, head = self:handle_GET(...)
271         return stat, head
272 end