GSoC Commit #1: LuCId + HTTP-Server
[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 module "luci.lucid.http.handler.file"
32
33 Simple = util.class(srv.Handler)
34
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)
39
40         options = options or {}
41         self.dirlist = not options.noindex
42         self.error404 = options.error404
43 end
44
45 function Simple.parse_range(self, request, size)
46         if not request.headers.Range then
47                 return true
48         end
49
50         local from, to = request.headers.Range:match("bytes=([0-9]*)-([0-9]*)")
51         if not (from or to) then
52                 return true
53         end
54
55         from, to = tonumber(from), tonumber(to)
56         if not (from or to) then
57                 return true
58         elseif not from then
59                 from, to = size - to, size - 1
60         elseif not to then
61                 to = size - 1
62         end
63
64         -- Not satisfiable
65         if from >= size then
66                 return false
67         end
68
69         -- Normalize
70         if to >= size then
71                 to = size - 1
72         end
73
74         local range = "bytes " .. from .. "-" .. to .. "/" .. size
75         return from, (1 + to - from), range
76 end
77
78 function Simple.getfile(self, uri)
79         if not self.realdocroot then
80                 self.realdocroot = fs.realpath(self.docroot)
81         end
82         local file = fs.realpath(self.docroot .. uri)
83         if not file or file:sub(1, #self.realdocroot) ~= self.realdocroot then
84                 return uri
85         end
86         return file, fs.stat(file)
87 end
88
89 function Simple.handle_GET(self, request)
90         local file, stat = self:getfile(prot.urldecode(request.env.PATH_INFO, true))
91
92         if stat then
93                 if stat.type == "reg" then
94
95                         -- Generate Entity Tag
96                         local etag = cond.mk_etag( stat )
97
98                         -- Check conditionals
99                         local ok, code, hdrs
100
101                         ok, code, hdrs = cond.if_modified_since( request, stat )
102                         if ok then
103                                 ok, code, hdrs = cond.if_match( request, stat )
104                                 if ok then
105                                         ok, code, hdrs = cond.if_unmodified_since( request, stat )
106                                         if ok then
107                                                 ok, code, hdrs = cond.if_none_match( request, stat )
108                                                 if ok then
109                                                         local f, err = nixio.open(file)
110
111                                                         if f then
112                                                                 local code = 200
113                                                                 local o, s, r = self:parse_range(request, stat.size)
114
115                                                                 if not o then
116                                                                         return self:failure(416, "Invalid Range")
117                                                                 end
118
119                                                                 local headers = {
120                                                                         ["Last-Modified"]  = date.to_http( stat.mtime ),
121                                                                         ["Content-Type"]   = mime.to_mime( file ),
122                                                                         ["ETag"]           = etag,
123                                                                         ["Accept-Ranges"]  = "bytes",
124                                                                 }
125
126                                                                 if o == true then
127                                                                         s = stat.size
128                                                                 else
129                                                                         code = 206
130                                                                         headers["Content-Range"] = r
131                                                                         f:seek(o)
132                                                                 end
133                                                                 
134                                                                 headers["Content-Length"] = s
135
136                                                                 -- Send Response
137                                                                 return code, headers, srv.IOResource(f, s)
138                                                         else
139                                                                 return self:failure( 403, err:gsub("^.+: ", "") )
140                                                         end
141                                                 else
142                                                         return code, hdrs
143                                                 end
144                                         else
145                                                 return code, hdrs
146                                         end
147                                 else
148                                         return code, hdrs
149                                 end
150                         else
151                                 return code, hdrs
152                         end
153
154                 elseif stat.type == "dir" then
155
156                         local ruri = request.env.REQUEST_URI:gsub("/$", "")
157                         local duri = prot.urldecode( ruri, true )
158                         local root = self.docroot
159
160                         -- check for index files
161                         local index_candidates = {
162                                 "index.html", "index.htm", "default.html", "default.htm",
163                                 "index.txt", "default.txt"
164                         }
165
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
170                                 )
171
172                                 if istat ~= nil and istat.type == "reg" then
173                                         return 302, { Location = ruri .. "/" .. candidate }
174                                 end
175                         end
176
177
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'                                                    ..
184                                 '<head>\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 } '                              ..
190                                         'p { margin:0 }'                                                                                ..
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>',
195                                         duri, duri, ruri 
196                         )
197
198                         local entries = fs.dir( file )
199
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 )
203
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 )
211                                                 )
212                                         else
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,
219                                                                 mime.to_mime( e ),
220                                                                 estat.size, date.to_http( estat.mtime )
221                                                 )
222                                         end
223                                 end
224
225                                 html = html .. '</ul><hr /><address>LuCId-HTTPd' .. 
226                                         '</address></body></html>'
227
228                                 return 200, {
229                                                 ["Date"]         = date.to_http( os.time() );
230                                                 ["Content-Type"] = "text/html; charset=utf-8";
231                                         }, ltn12.source.string(html)
232                         else
233                                 return self:failure(403, "Permission denied")
234                         end
235                 else
236                         return self:failure(403, "Unable to transmit " .. stat.type .. " " .. file)
237                 end
238         else
239                 if self.error404 then
240                         return 302, { Location = self.error404 }
241                 else
242                         return self:failure(404, "No such file: " .. file)
243                 end
244         end
245 end
246
247 function Simple.handle_HEAD(self, ...)
248         local stat, head = self:handle_GET(...)
249         return stat, head
250 end