ee36df7ac6e13498a32970f8c7938a4f5959159b
[vlc.git] / share / luaintf / 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  * no_error_detail: If set, do not print the Lua error message when generating
27                     a page fails.
28  * no_index: If set, don't build directory indexes
29 --]==========================================================================]
30
31
32 require "httpd"
33 require "acl"
34 require "common"
35
36 vlc.msg.err("Lua HTTP interface")
37
38 open_tag = "<?vlc"
39 close_tag = "?>"
40
41 function escape(s)
42     return (string.gsub(s,"([%^%$%%%.%[%]%*%+%-%?])","%%%1"))
43 end
44
45 function process_raw(filename)
46     local input = io.open(filename):read("*a")
47     -- find the longest [===[ or ]=====] type sequence and make sure that
48     -- we use one that's longer.
49     local str="X"
50     for str2 in string.gmatch(input,"[%[%]]=*[%[%]]") do
51         if #str < #str2 then str = str2 end
52     end
53     str=string.rep("=",#str-1)
54
55     --[[ FIXME:
56     <?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.
57     --]]
58     local code0 = string.gsub(input,escape(close_tag)," print(["..str.."[")
59     local code1 = string.gsub(code0,escape(open_tag),"]"..str.."]) ")
60     local code = "print(["..str.."["..code1.."]"..str.."])"
61     --[[ Uncomment to debug
62     if string.match(filename,"vlm_cmd.xml$") then
63     io.write(code)
64     io.write("\n")
65     end
66     --]]
67     return assert(loadstring(code,filename))
68 end
69 function process(filename)
70     vlc.msg.dbg("Loading `"..filename.."'")
71     local mtime = 0 -- vlc.fd.stat(filename).modification_time
72     local func = false -- process_raw(filename)
73     return function(...)
74         local new_mtime = vlc.fd.stat(filename).modification_time
75         if new_mtime ~= mtime then
76             -- Re-read the file if it changed
77             vlc.msg.dbg("Reloading `"..filename.."'")
78             func = process_raw(filename)
79             mtime = new_mtime
80         end
81         return func(...)
82     end
83 end
84
85
86 function callback_error(path,url,msg)
87     local url = url or "&lt;page unknown&gt;"
88     return  [[<html xmlns="http://www.w3.org/1999/xhtml">
89 <head>
90 <title>Error loading ]]..url..[[</title>
91 </head>
92 <body>
93 <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>
94 <p>
95 <a href="http://www.videolan.org/">VideoLAN</a><br/>
96 <a href="http://www.lua.org/manual/5.1/">Lua 5.1 Reference Manual</a>
97 </p>
98 </body>
99 </html>]]
100 end
101
102 function dirlisting(url,listing)
103     local list = {}
104     for _,f in ipairs(listing) do
105         if not string.match(f,"^%.") then
106             table.insert(list,"<li><a href='"..f.."'>"..f.."</a></li>")
107         end
108     end
109     list = table.concat(list)
110     local function callback()
111         return [[<html xmlns="http://www.w3.org/1999/xhtml">
112 <head>
113 <title>Directory listing ]]..url..[[</title>
114 </head>
115 <body>
116 <h1>Directory listing ]]..url..[[</h1><ul>]]..list..[[</ul>
117 </body>
118 </html>]]
119     end
120     return h:file_new(url,"text/html",nil,nil,nil,callback,nil)
121 end
122
123 function file(h,path,url,acl_,mime)
124     local generate_page = process(path)
125     local callback = function(data,request)
126         -- FIXME: I'm sure that we could define a real sandbox
127         -- redefine print
128         local page = {}
129         local function pageprint(...)
130             for i=1,select("#",...) do
131                 if i== 1 then
132                     table.insert(page,tostring(select(i,...)))
133                 else
134                     table.insert(page," "..tostring(select(i,...)))
135                 end
136             end
137         end
138         _G._GET = parse_url_request(request)
139         local oldprint = print
140         print = pageprint
141         local ok, msg = pcall(generate_page)
142         -- reset
143         print = oldprint
144         if not ok then
145             return callback_error(path,url,msg)
146         end
147         return table.concat(page)
148     end
149     return h:file_new(url or path,mime,nil,nil,acl_,callback,nil)
150 end
151
152 function rawfile(h,path,url,acl_)
153     local filename = path
154     vlc.msg.dbg("Loading `"..filename.."'")
155     local mtime = 0 -- vlc.fd.stat(filename).modification_time
156     local page = false -- io.open(filename):read("*a")
157     local callback = function(data,request)
158         local new_mtime = vlc.fd.stat(filename).modification_time
159         if mtime ~= new_mtime then
160             -- Re-read the file if it changed
161             vlc.msg.dbg("Reloading `"..filename.."'")
162             page = io.open(filename):read("*a")
163             mtime = new_mtime
164         end
165         return page
166     end
167     return h:file_new(url or path,nil,nil,nil,acl_,callback,nil)
168 end
169
170 function parse_url_request(request)
171     if not request then return {} end
172     t = {}
173     for k,v in string.gmatch(request,"([^=&]+)=?([^=&]*)") do
174         local k_ = vlc.decode_uri(k)
175         local v_ = vlc.decode_uri(v)
176         t[k_]=v_
177     end
178     return t
179 end
180
181 local function find_datadir(name)
182     local list = vlc.datadir_list(name)
183     for _, l in ipairs(list) do
184         local s = vlc.fd.stat(l)
185         if s then
186             return l
187         end
188     end
189     error("Unable to find the `"..name.."' directory.")
190 end
191 http_dir = find_datadir("http-lua")
192
193 do
194     local oldpath = package.path
195     package.path = http_dir.."/?.lua"
196     local ok, err = pcall(require,"custom")
197     if not ok then
198         vlc.msg.warn("Couldn't load "..http_dir.."/custom.lua")
199     else
200         vlc.msg.dbg("Loaded "..http_dir.."/custom.lua")
201     end
202     package.path = oldpath
203 end
204 local files = {}
205 local mimes = {
206     txt = "text/plain",
207     html = "text/html",
208     xml = "text/xml",
209     js = "text/javascript",
210     png = "image/png",
211     ico = "image/x-icon",
212 }
213 local function load_dir(dir,root,parent_acl)
214     local root = root or "/"
215     local has_index = false
216     if string.match(dir,"/old") then
217         return
218     end
219     local my_acl = parent_acl
220     do
221         local af = dir.."/.hosts"
222         local s = vlc.fd.stat(af)
223         if s and s.type == "file" then
224             -- We found an acl
225             my_acl = acl.new(false)
226             my_acl:load_file(af)
227         end
228     end
229     local d = vlc.fd.opendir(dir)
230     for _,f in ipairs(d) do
231         if not string.match(f,"^%.") then
232             local s = vlc.fd.stat(dir.."/"..f)
233             if s.type == "file" then
234                 local url
235                 if f == "index.html" then
236                     url = root
237                     has_index = true
238                 else
239                     url = root..f
240                 end
241                 local ext = string.match(f,"%.([^%.]-)$")
242                 local mime = mimes[ext]
243                 -- print(url,mime)
244                 if mime and string.match(mime,"^text/") then
245                     table.insert(files,file(h,dir.."/"..f,url,my_acl and my_acl.private,mime))
246                 else
247                     table.insert(files,rawfile(h,dir.."/"..f,url,my_acl and my_acl.private))
248                 end
249             elseif s.type == "dir" then
250                 if f == "dialogs" then -- FIXME
251                 else
252                     load_dir(dir.."/"..f,root..f.."/",my_acl)
253                 end
254             end
255         end
256     end
257     if not has_index and not config.no_index then
258         -- print("Adding index for", root)
259         table.insert(files,dirlisting(root,d,my_acl and my_acl.private))
260     end
261 end
262
263 h = httpd.new("localhost",8080)
264 load_dir( http_dir )
265
266 while not die do die = vlc.lock_and_wait() end -- everything happens in callbacks
267
268 -- FIXME: We shouldn't need to do this ourselves.
269 for i=1,#files do
270     getmetatable(files[i]).__gc(files[i])
271     files[i] = nil
272 end