dde018e88765c841f8d51acf9884b6bf3751f772
[vlc.git] / share / lua / intf / http.lua
1 --[==========================================================================[
2  http.lua: HTTP interface module for VLC
3 --[==========================================================================[
4  Copyright (C) 2007 the VideoLAN team
5  $Id$
6
7  Authors: Antoine Cellerier <dionoea at videolan dot org>
8
9  This program is free software; you can redistribute it and/or modify
10  it under the terms of the GNU General Public License as published by
11  the Free Software Foundation; either version 2 of the License, or
12  (at your option) any later version.
13
14  This program is distributed in the hope that it will be useful,
15  but WITHOUT ANY WARRANTY; without even the implied warranty of
16  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  GNU General Public License for more details.
18
19  You should have received a copy of the GNU General Public License
20  along with this program; if not, write to the Free Software
21  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
22 --]==========================================================================]
23
24 --[==========================================================================[
25 Configuration options:
26  * host: A host to listen on.
27  * dir: Directory to use as the http interface's root.
28  * no_error_detail: If set, do not print the Lua error message when generating
29                     a page fails.
30  * no_index: If set, don't build directory indexes
31 --]==========================================================================]
32
33
34 require "httpd"
35 require "acl"
36 require "common"
37
38 vlc.msg.info("Lua HTTP interface")
39
40 open_tag = "<?vlc"
41 close_tag = "?>"
42
43 function escape(s)
44     return (string.gsub(s,"([%^%$%%%.%[%]%*%+%-%?])","%%%1"))
45 end
46
47 function process_raw(filename)
48     local input = io.open(filename):read("*a")
49     -- find the longest [===[ or ]=====] type sequence and make sure that
50     -- we use one that's longer.
51     local str="X"
52     for str2 in string.gmatch(input,"[%[%]]=*[%[%]]") do
53         if #str < #str2 then str = str2 end
54     end
55     str=string.rep("=",#str-1)
56
57     --[[ FIXME:
58     <?xml version="1.0" encoding="charset" standalone="yes" ?> is still a problem. The closing '?>' needs to be printed using '?<?vlc print ">" ?>' to prevent a parse error.
59     --]]
60     local code0 = string.gsub(input,escape(close_tag)," print(["..str.."[")
61     local code1 = string.gsub(code0,escape(open_tag),"]"..str.."]) ")
62     local code = "print(["..str.."["..code1.."]"..str.."])"
63     --[[ Uncomment to debug
64     if string.match(filename,"vlm_cmd.xml$") then
65     io.write(code)
66     io.write("\n")
67     end
68     --]]
69     return assert(loadstring(code,filename))
70 end
71 function process(filename)
72     local mtime = 0    -- vlc.fd.stat(filename).modification_time
73     local func = false -- process_raw(filename)
74     return function(...)
75         local new_mtime = vlc.fd.stat(filename).modification_time
76         if new_mtime ~= mtime then
77             -- Re-read the file if it changed
78             if mtime == 0 then
79                 vlc.msg.dbg("Loading `"..filename.."'")
80             else
81                 vlc.msg.dbg("Reloading `"..filename.."'")
82             end
83             func = process_raw(filename)
84             mtime = new_mtime
85         end
86         return func(...)
87     end
88 end
89
90
91 function callback_error(path,url,msg)
92     local url = url or "&lt;page unknown&gt;"
93     return  [[<html xmlns="http://www.w3.org/1999/xhtml">
94 <head>
95 <title>Error loading ]]..url..[[</title>
96 </head>
97 <body>
98 <h1>Error loading ]]..url..[[</h1><pre>]]..(config.no_error_detail and "Remove configuration option `no_error_detail' on the server to get more information." or tostring(msg))..[[</pre>
99 <p>
100 <a href="http://www.videolan.org/">VideoLAN</a><br/>
101 <a href="http://www.lua.org/manual/5.1/">Lua 5.1 Reference Manual</a>
102 </p>
103 </body>
104 </html>]]
105 end
106
107 function dirlisting(url,listing,acl_)
108     local list = {}
109     for _,f in ipairs(listing) do
110         if not string.match(f,"^%.") then
111             table.insert(list,"<li><a href='"..f.."'>"..f.."</a></li>")
112         end
113     end
114     list = table.concat(list)
115     local function callback()
116         return [[<html xmlns="http://www.w3.org/1999/xhtml">
117 <head>
118 <title>Directory listing ]]..url..[[</title>
119 </head>
120 <body>
121 <h1>Directory listing ]]..url..[[</h1><ul>]]..list..[[</ul>
122 </body>
123 </html>]]
124     end
125     return h:file_new(url,"text/html",nil,nil,acl_,callback,nil)
126 end
127
128 function file(h,path,url,acl_,mime)
129     local generate_page = process(path)
130     local callback = function(data,request)
131         -- FIXME: I'm sure that we could define a real sandbox
132         -- redefine print
133         local page = {}
134         local function pageprint(...)
135             for i=1,select("#",...) do
136                 if i== 1 then
137                     table.insert(page,tostring(select(i,...)))
138                 else
139                     table.insert(page," "..tostring(select(i,...)))
140                 end
141             end
142         end
143         _G._GET = parse_url_request(request)
144         local oldprint = print
145         print = pageprint
146         local ok, msg = pcall(generate_page)
147         -- reset
148         print = oldprint
149         if not ok then
150             return callback_error(path,url,msg)
151         end
152         return table.concat(page)
153     end
154     return h:file_new(url or path,mime,nil,nil,acl_,callback,nil)
155 end
156
157 function rawfile(h,path,url,acl_)
158     local filename = path
159     local mtime = 0    -- vlc.fd.stat(filename).modification_time
160     local page = false -- io.open(filename):read("*a")
161     local callback = function(data,request)
162         local new_mtime = vlc.fd.stat(filename).modification_time
163         if mtime ~= new_mtime then
164             -- Re-read the file if it changed
165             if mtime == 0 then
166                 vlc.msg.dbg("Loading `"..filename.."'")
167             else
168                 vlc.msg.dbg("Reloading `"..filename.."'")
169             end
170             page = io.open(filename):read("*a")
171             mtime = new_mtime
172         end
173         return page
174     end
175     return h:file_new(url or path,nil,nil,nil,acl_,callback,nil)
176 end
177
178 function parse_url_request(request)
179     if not request then return {} end
180     t = {}
181     for k,v in string.gmatch(request,"([^=&]+)=?([^=&]*)") do
182         local k_ = vlc.decode_uri(k)
183         local v_ = vlc.decode_uri(v)
184         if t[k_] ~= nil then
185             local t2
186             if type(t[k_]) ~= "table" then
187                 t2 = {}
188                 table.insert(t2,t[k_])
189                 t[k_] = t2
190             else
191                 t2 = t[k_]
192             end
193             table.insert(t2,v_)
194         else
195             t[k_] = v_
196         end
197     end
198     return t
199 end
200
201 local function find_datadir(name)
202     local list = vlc.datadir_list(name)
203     for _, l in ipairs(list) do
204         local s = vlc.fd.stat(l)
205         if s then
206             return l
207         end
208     end
209     error("Unable to find the `"..name.."' directory.")
210 end
211 http_dir = config.dir or find_datadir("http")
212
213 do
214     local oldpath = package.path
215     package.path = http_dir.."/?.lua"
216     local ok, err = pcall(require,"custom")
217     if not ok then
218         vlc.msg.warn("Couldn't load "..http_dir.."/custom.lua")
219     else
220         vlc.msg.dbg("Loaded "..http_dir.."/custom.lua")
221     end
222     package.path = oldpath
223 end
224 local files = {}
225 local mimes = {
226     txt = "text/plain",
227     html = "text/html",
228     xml = "text/xml",
229     js = "text/javascript",
230     css = "text/css",
231     png = "image/png",
232     ico = "image/x-icon",
233 }
234 local function load_dir(dir,root,parent_acl)
235     local root = root or "/"
236     local has_index = false
237     local my_acl = parent_acl
238     do
239         local af = dir.."/.hosts"
240         local s = vlc.fd.stat(af)
241         if s and s.type == "file" then
242             -- We found an acl
243             my_acl = acl.new(false)
244             my_acl:load_file(af)
245         end
246     end
247     local d = vlc.fd.opendir(dir)
248     for _,f in ipairs(d) do
249         if not string.match(f,"^%.") then
250             local s = vlc.fd.stat(dir.."/"..f)
251             if s.type == "file" then
252                 local url
253                 if f == "index.html" then
254                     url = root
255                     has_index = true
256                 else
257                     url = root..f
258                 end
259                 local ext = string.match(f,"%.([^%.]-)$")
260                 local mime = mimes[ext]
261                 -- print(url,mime)
262                 if mime and string.match(mime,"^text/") then
263                     table.insert(files,file(h,dir.."/"..f,url,my_acl and my_acl.private,mime))
264                 else
265                     table.insert(files,rawfile(h,dir.."/"..f,url,my_acl and my_acl.private))
266                 end
267             elseif s.type == "dir" then
268                 load_dir(dir.."/"..f,root..f.."/",my_acl)
269             end
270         end
271     end
272     if not has_index and not config.no_index then
273         -- print("Adding index for", root)
274         table.insert(files,dirlisting(root,d,my_acl and my_acl.private))
275     end
276 end
277
278 local u = vlc.net.url_parse( config.host or "localhost:8080" )
279 h = httpd.new(u.host,u.port)
280 load_dir( http_dir )
281
282 while not die do die = vlc.lock_and_wait() end -- everything happens in callbacks
283
284 -- FIXME: We shouldn't need to do this ourselves.
285 for i=1,#files do
286     getmetatable(files[i]).__gc(files[i])
287     files[i] = nil
288 end