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