Fix for parent folder link and other tweaks wmcbrine applied to
[pyTivo/wmcbrine/lucasnz.git] / httpserver.py
blob313824254988590db5783a0e8895479bf9c83287
1 import BaseHTTPServer
2 import SocketServer
3 import cgi
4 import logging
5 import mimetypes
6 import os
7 import shutil
8 import socket
9 import time
10 from urllib import unquote_plus, quote
11 from xml.sax.saxutils import escape
13 from Cheetah.Template import Template
14 import config
15 from plugin import GetPlugin, EncodeUnicode
17 SCRIPTDIR = os.path.dirname(__file__)
19 VIDEO_FORMATS = """<?xml version="1.0" encoding="utf-8"?>
20 <TiVoFormats>
21 <Format><ContentType>video/x-tivo-mpeg</ContentType><Description/></Format>
22 </TiVoFormats>"""
24 VIDEO_FORMATS_TS = """<?xml version="1.0" encoding="utf-8"?>
25 <TiVoFormats>
26 <Format><ContentType>video/x-tivo-mpeg</ContentType><Description/></Format>
27 <Format><ContentType>video/x-tivo-mpeg-ts</ContentType><Description/></Format>
28 </TiVoFormats>"""
30 BASE_HTML = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
31 "http://www.w3.org/TR/html4/strict.dtd">
32 <html> <head><title>pyTivo</title></head> <body> %s </body> </html>"""
34 RELOAD = '<p>The <a href="%s">page</a> will reload in %d seconds.</p>'
35 UNSUP = '<h3>Unsupported Command</h3> <p>Query:</p> <ul>%s</ul>'
37 class TivoHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
38 containers = {}
40 def __init__(self, server_address, RequestHandlerClass):
41 BaseHTTPServer.HTTPServer.__init__(self, server_address,
42 RequestHandlerClass)
43 self.daemon_threads = True
44 self.logger = logging.getLogger('pyTivo')
46 def add_container(self, name, settings):
47 if name in self.containers or name == 'TiVoConnect':
48 raise "Container Name in use"
49 try:
50 self.containers[name] = settings
51 except KeyError:
52 self.logger.error('Unable to add container ' + name)
54 def reset(self):
55 self.containers.clear()
56 for section, settings in config.getShares():
57 self.add_container(section, settings)
59 def handle_error(self, request, client_address):
60 self.logger.exception('Exception during request from %s' %
61 (client_address,))
63 def set_beacon(self, beacon):
64 self.beacon = beacon
66 class TivoHTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
67 def __init__(self, request, client_address, server):
68 self.wbufsize = 0x10000
69 self.server_version = 'pyTivo/1.0'
70 self.sys_version = ''
71 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
72 client_address, server)
74 def address_string(self):
75 host, port = self.client_address[:2]
76 return host
78 def do_GET(self):
79 tsn = self.headers.getheader('TiVo_TCD_ID',
80 self.headers.getheader('tsn', ''))
81 if not self.authorize(tsn):
82 return
83 if tsn:
84 ip = self.address_string()
85 config.tivos[tsn] = ip
87 if not tsn in config.tivo_names or config.tivo_names[tsn] == tsn:
88 config.tivo_names[tsn] = self.server.beacon.get_name(ip)
90 if '?' in self.path:
91 path, opts = self.path.split('?', 1)
92 query = cgi.parse_qs(opts)
93 else:
94 path = self.path
95 query = {}
97 if path == '/TiVoConnect':
98 self.handle_query(query, tsn)
99 else:
100 ## Get File
101 splitpath = [x for x in unquote_plus(path).split('/') if x]
102 if splitpath:
103 self.handle_file(query, splitpath)
104 else:
105 ## Not a file not a TiVo command
106 self.infopage()
108 def do_POST(self):
109 tsn = self.headers.getheader('TiVo_TCD_ID',
110 self.headers.getheader('tsn', ''))
111 if not self.authorize(tsn):
112 return
113 ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
114 if ctype == 'multipart/form-data':
115 query = cgi.parse_multipart(self.rfile, pdict)
116 else:
117 length = int(self.headers.getheader('content-length'))
118 qs = self.rfile.read(length)
119 query = cgi.parse_qs(qs, keep_blank_values=1)
120 self.handle_query(query, tsn)
122 def handle_query(self, query, tsn):
123 mname = False
124 if 'Command' in query and len(query['Command']) >= 1:
126 command = query['Command'][0]
128 # If we are looking at the root container
129 if (command == 'QueryContainer' and
130 (not 'Container' in query or query['Container'][0] == '/')):
131 self.root_container()
132 return
134 if 'Container' in query:
135 # Dispatch to the container plugin
136 basepath = query['Container'][0].split('/')[0]
137 for name, container in config.getShares(tsn):
138 if basepath == name:
139 plugin = GetPlugin(container['type'])
140 if hasattr(plugin, command):
141 method = getattr(plugin, command)
142 method(self, query)
143 return
144 else:
145 break
147 elif (command == 'QueryFormats' and 'SourceFormat' in query and
148 query['SourceFormat'][0].startswith('video')):
149 self.send_response(200)
150 self.send_header('Content-type', 'text/xml')
151 self.end_headers()
152 if config.hasTStivo(tsn):
153 self.wfile.write(VIDEO_FORMATS_TS)
154 else:
155 self.wfile.write(VIDEO_FORMATS)
156 return
158 elif command == 'FlushServer':
159 # Does nothing -- included for completeness
160 self.send_response(200)
161 self.end_headers()
162 return
164 # If we made it here it means we couldn't match the request to
165 # anything.
166 self.unsupported(query)
168 def handle_file(self, query, splitpath):
169 if '..' not in splitpath: # Protect against path exploits
170 ## Pass it off to a plugin?
171 for name, container in self.server.containers.items():
172 if splitpath[0] == name:
173 base = os.path.normpath(container['path'])
174 path = os.path.join(base, *splitpath[1:])
175 plugin = GetPlugin(container['type'])
176 plugin.send_file(self, path, query)
177 return
179 ## Serve it from a "content" directory?
180 base = os.path.join(SCRIPTDIR, *splitpath[:-1])
181 path = os.path.join(base, 'content', splitpath[-1])
183 if os.path.isfile(path):
184 try:
185 handle = open(path, 'rb')
186 except:
187 self.send_error(404)
188 return
190 # Send the header
191 mime = mimetypes.guess_type(path)[0]
192 self.send_response(200)
193 if mime:
194 self.send_header('Content-type', mime)
195 self.send_header('Content-length', os.path.getsize(path))
196 self.end_headers()
198 # Send the body of the file
199 try:
200 shutil.copyfileobj(handle, self.wfile)
201 except:
202 pass
203 handle.close()
204 return
206 ## Give up
207 self.send_error(404)
209 def authorize(self, tsn=None):
210 # if allowed_clients is empty, we are completely open
211 allowed_clients = config.getAllowedClients()
212 if not allowed_clients or (tsn and config.isTsnInConfig(tsn)):
213 return True
214 client_ip = self.client_address[0]
215 for allowedip in allowed_clients:
216 if client_ip.startswith(allowedip):
217 return True
219 self.send_response(404)
220 self.send_header('Content-type', 'text/plain')
221 self.end_headers()
222 self.wfile.write("Unauthorized.")
223 return False
225 def log_message(self, format, *args):
226 self.server.logger.info("%s [%s] %s" % (self.address_string(),
227 self.log_date_time_string(), format%args))
229 def root_container(self):
230 tsn = self.headers.getheader('TiVo_TCD_ID', '')
231 tsnshares = config.getShares(tsn)
232 tsncontainers = []
233 for section, settings in tsnshares:
234 try:
235 settings['content_type'] = \
236 GetPlugin(settings['type']).CONTENT_TYPE
237 tsncontainers.append((section, settings))
238 except Exception, msg:
239 self.server.logger.error(section + ' - ' + str(msg))
240 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
241 'root_container.tmpl'),
242 filter=EncodeUnicode)
243 t.containers = tsncontainers
244 t.hostname = socket.gethostname()
245 t.escape = escape
246 t.quote = quote
247 self.send_response(200)
248 self.send_header('Content-type', 'text/xml')
249 self.end_headers()
250 self.wfile.write(t)
252 def infopage(self):
253 useragent = self.headers.getheader('User-Agent', '')
254 self.send_response(200)
255 self.send_header('Content-type', 'text/html; charset=utf-8')
256 self.end_headers()
257 if useragent.lower().find('mobile') > 0:
258 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
259 'info_page_mob.tmpl'),
260 filter=EncodeUnicode)
261 else:
262 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
263 'info_page.tmpl'),
264 filter=EncodeUnicode)
265 t.admin = ''
267 if config.get_server('tivo_mak') and config.get_server('togo_path'):
268 t.togo = 'Pull from TiVos:<br>'
269 else:
270 t.togo = ''
272 if (config.get_server('tivo_username') and
273 config.get_server('tivo_password')):
274 t.shares = 'Push from video shares:<br>'
275 else:
276 t.shares = ''
278 for section, settings in config.getShares():
279 plugin_type = settings.get('type')
280 if plugin_type == 'settings':
281 t.admin += ('<a href="/TiVoConnect?Command=Settings&amp;' +
282 'Container=' + quote(section) +
283 '">Web Configuration</a><br>')
284 elif plugin_type == 'togo' and t.togo:
285 for tsn in config.tivos:
286 if tsn:
287 t.togo += ('<a href="/TiVoConnect?' +
288 'Command=NPL&amp;Container=' + quote(section) +
289 '&amp;TiVo=' + config.tivos[tsn] + '">' +
290 escape(config.tivo_names[tsn]) + '</a><br>')
291 elif ( plugin_type == 'video' or plugin_type == 'dvdvideo' ) \
292 and t.shares:
293 t.shares += ('<a href="TiVoConnect?Command=' +
294 'QueryContainer&amp;Container=' +
295 quote(section) + '&Format=text/html">' +
296 section + '</a><br>')
298 self.wfile.write(t)
300 def unsupported(self, query):
301 message = UNSUP % '\n'.join(['<li>%s: %s</li>' % (escape(key),
302 escape(repr(value)))
303 for key, value in query.items()])
304 text = BASE_HTML % message
305 self.send_response(404)
306 self.send_header('Content-Type', 'text/html; charset=utf-8')
307 self.send_header('Content-Length', len(text))
308 self.end_headers()
309 self.wfile.write(text)
311 def redir(self, message, seconds=2):
312 url = self.headers.getheader('Referer')
313 if url:
314 message += RELOAD % (escape(url), seconds)
315 text = (BASE_HTML % message).encode('utf-8')
316 self.send_response(200)
317 self.send_header('Content-Type', 'text/html; charset=utf-8')
318 self.send_header('Content-Length', len(text))
319 self.send_header('Expires', '0')
320 if url:
321 self.send_header('Refresh', '%d; url=%s' % (seconds, url))
322 self.end_headers()
323 self.wfile.write(text)