e1f707c628ccbb98f1921e09b1656009648a3e31
[project/luci.git] / libs / lucittpd / luasrc / ttpd / 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 io = require "io"
19 local os = require "os"
20 local fs = require "luci.fs"
21 local util = require "luci.util"
22 local ltn12 = require "luci.ltn12"
23 local mod = require "luci.ttpd.module"
24 local srv = require "luci.ttpd.server"
25 local string = require "string"
26
27 local prot = require "luci.http.protocol"
28 local date = require "luci.http.protocol.date"
29 local mime = require "luci.http.protocol.mime"
30 local cond = require "luci.http.protocol.conditionals"
31
32 module "luci.ttpd.handler.file"
33
34 Simple = util.class(mod.Handler)
35 Response = mod.Response
36
37 function Simple.__init__(self, docroot, dirlist)
38         mod.Handler.__init__(self)
39         self.docroot = docroot
40         self.dirlist = dirlist and true or false
41 end
42
43 function Simple.parse_range(self, request, size)
44         if not request.headers.Range then
45                 return true
46         end
47
48         local from, to = request.headers.Range:match("bytes=([0-9]*)-([0-9]*)")
49         if not (from or to) then
50                 return true
51         end
52
53         from, to = tonumber(from), tonumber(to)
54         if not (from or to) then
55                 return true
56         elseif not from then
57                 from, to = size - to, size - 1
58         elseif not to then
59                 to = size - 1
60         end
61
62         -- Not satisfiable
63         if from >= size then
64                 return false
65         end
66
67         -- Normalize
68         if to >= size then
69                 to = size - 1
70         end
71
72         local range = "bytes " .. from .. "-" .. to .. "/" .. size
73         return from, (1 + to - from), range
74 end
75
76 function Simple.getfile(self, uri)
77         local file = self.docroot .. uri:gsub("%.%./+", "")
78         local stat = fs.stat(file)
79
80         return file, stat
81 end
82
83 function Simple.handle_get(self, request, sourcein, sinkerr)
84         local file, stat = self:getfile( prot.urldecode( request.env.PATH_INFO, true ) )
85
86         if stat then
87                 if stat.type == "regular" then
88
89                         -- Generate Entity Tag
90                         local etag = cond.mk_etag( stat )
91
92                         -- Check conditionals
93                         local ok, code, hdrs
94
95                         ok, code, hdrs = cond.if_modified_since( request, stat )
96                         if ok then
97                                 ok, code, hdrs = cond.if_match( request, stat )
98                                 if ok then
99                                         ok, code, hdrs = cond.if_unmodified_since( request, stat )
100                                         if ok then
101                                                 ok, code, hdrs = cond.if_none_match( request, stat )
102                                                 if ok then
103                                                         local f, err = io.open(file)
104
105                                                         if f then
106                                                                 local code = 200
107                                                                 local o, s, r = self:parse_range(request, stat.size)
108
109                                                                 if not o then
110                                                                         return self:failure(416, "Invalid Range")
111                                                                 end
112
113                                                                 local headers = {
114                                                                         ["Last-Modified"]  = date.to_http( stat.mtime ),
115                                                                         ["Content-Type"]   = mime.to_mime( file ),
116                                                                         ["ETag"]           = etag,
117                                                                         ["Accept-Ranges"]  = "bytes",
118                                                                 }
119
120                                                                 if o == true then
121                                                                         o = 0
122                                                                         s = stat.size
123                                                                 else
124                                                                         code = 206
125                                                                         headers["Content-Range"] = r
126                                                                 end
127
128                                                                 headers["Content-Length"] = s
129
130                                                                 -- Send Response
131                                                                 return Response(code, headers),
132                                                                         srv.IOResource(f, o, s)
133                                                         else
134                                                                 return self:failure( 403, err:gsub("^.+: ", "") )
135                                                         end
136                                                 else
137                                                         return Response( code, hdrs or { } )
138                                                 end
139                                         else
140                                                 return Response( code, hdrs or { } )
141                                         end
142                                 else
143                                         return Response( code, hdrs or { } )
144                                 end
145                         else
146                                 return Response( code, hdrs or { } )
147                         end
148
149                 elseif stat.type == "directory" then
150
151                         local ruri = request.request_uri:gsub("/$","")
152                         local duri = prot.urldecode( ruri, true )
153                         local root = self.docroot:gsub("/$","")
154
155                         -- check for index files
156                         local index_candidates = {
157                                 "index.html", "index.htm", "default.html", "default.htm",
158                                 "index.txt", "default.txt"
159                         }
160
161                         -- try to find an index file and redirect to it
162                         for i, candidate in ipairs( index_candidates ) do
163                                 local istat = fs.stat(
164                                         root .. "/" .. duri .. "/" .. candidate
165                                 )
166
167                                 if istat ~= nil and istat.type == "regular" then
168                                         return Response( 302, {
169                                                 ["Location"] = ruri .. "/" .. candidate
170                                         } )
171                                 end
172                         end
173
174
175                         local html = string.format(
176                                 '<?xml version="1.0" encoding="ISO-8859-15"?>\n' ..
177                                 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" '  ..
178                                         '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n' ..
179                                 '<html xmlns="http://www.w3.org/1999/xhtml" '                ..
180                                         'xml:lang="en" lang="en">\n'                             ..
181                                 '<head>\n'                                                   ..
182                                 '<title>Index of %s/</title>\n'                              ..
183                                 '<style type="text/css"><!--\n'                              ..
184                                         'body { background-color:#fbb034; color:#ffffff } '      ..
185                                         'li { border-bottom:1px dotted #CCCCCC; padding:3px } '  ..
186                                         'small { font-size:60%%; color:#ffffff } '               ..
187                                         'p { margin:0 }'                                         ..
188                                 '\n--></style></head><body><h1>Index of %s/</h1><hr /><ul>',
189                                         duri, duri
190                         )
191
192                         local entries = fs.dir( file )
193
194                         if type(entries) == "table" then
195                                 for i, e in util.spairs(
196                                         entries, function(a,b)
197                                                 if entries[a] == '..' then
198                                                         return true
199                                                 elseif entries[b] == '..' then
200                                                         return false
201                                                 else
202                                                         return ( entries[a] < entries[b] )
203                                                 end
204                                         end
205                                 ) do
206                                         if e ~= '.' and ( e == '..' or e:sub(1,1) ~= '.' ) then
207                                                 local estat = fs.stat( file .. "/" .. e )
208
209                                                 if estat.type == "directory" then
210                                                         html = html .. string.format(
211                                                                 '<li><p><a href="%s/%s/">%s/</a> '           ..
212                                                                 '<small>(directory)</small><br />'           ..
213                                                                 '<small>Changed: %s</small></li>',
214                                                                         ruri, prot.urlencode( e ), e,
215                                                                         date.to_http( estat.mtime )
216                                                         )
217                                                 else
218                                                         html = html .. string.format(
219                                                                 '<li><p><a href="%s/%s">%s</a> '             ..
220                                                                 '<small>(%s)</small><br />'                  ..
221                                                                 '<small>Size: %i Bytes | '                   ..
222                                                                         'Changed: %s</small></li>',
223                                                                         ruri, prot.urlencode( e ), e,
224                                                                         mime.to_mime( e ),
225                                                                         estat.size, date.to_http( estat.mtime )
226                                                         )
227                                                 end
228                                         end
229                                 end
230
231                                 html = html .. '</ul><hr /></body></html>'
232
233                                 return Response(
234                                         200, {
235                                                 ["Date"]         = date.to_http( os.time() );
236                                                 ["Content-Type"] = "text/html; charset=ISO-8859-15";
237                                         }
238                                 ), ltn12.source.string(html)
239                         else
240                                 return self:failure(403, "Permission denied")
241                         end
242                 else
243                         return self:failure(403, "Unable to transmit " .. stat.type .. " " .. file)
244                 end
245         else
246                 return self:failure(404, "No such file: " .. file)
247         end
248 end
249
250 function Simple.handle_head(self, ...)
251         return (self:handle_get(...))
252 end