Derive WebVideo from BaseVideo instead of Video, so it won't include Push.
[pyTivo/wmcbrine/lucasnz.git] / httpserver.py
blob3817fb3cb6fb1b7da1f50bdcf3bd097f8d6fd5dc
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 def __init__(self, server_address, RequestHandlerClass):
39 self.containers = {}
40 self.stop = False
41 self.restart = False
42 self.logger = logging.getLogger('pyTivo')
43 BaseHTTPServer.HTTPServer.__init__(self, server_address,
44 RequestHandlerClass)
45 self.daemon_threads = True
47 def add_container(self, name, settings):
48 if name in self.containers or name == 'TiVoConnect':
49 raise "Container Name in use"
50 try:
51 self.containers[name] = settings
52 except KeyError:
53 self.logger.error('Unable to add container ' + name)
55 def reset(self):
56 self.containers.clear()
57 for section, settings in config.getShares():
58 self.add_container(section, settings)
60 def handle_error(self, request, client_address):
61 self.logger.exception('Exception during request from %s' %
62 (client_address,))
64 def set_beacon(self, beacon):
65 self.beacon = beacon
67 def set_service_status(self, status):
68 self.in_service = status
70 class TivoHTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
71 def __init__(self, request, client_address, server):
72 self.wbufsize = 0x10000
73 self.server_version = 'pyTivo/1.0'
74 self.sys_version = ''
75 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
76 client_address, server)
78 def address_string(self):
79 host, port = self.client_address[:2]
80 return host
82 def do_GET(self):
83 tsn = self.headers.getheader('TiVo_TCD_ID',
84 self.headers.getheader('tsn', ''))
85 if not self.authorize(tsn):
86 return
87 if tsn:
88 ip = self.address_string()
89 config.tivos[tsn] = ip
91 if not tsn in config.tivo_names or config.tivo_names[tsn] == tsn:
92 config.tivo_names[tsn] = self.server.beacon.get_name(ip)
94 if '?' in self.path:
95 path, opts = self.path.split('?', 1)
96 query = cgi.parse_qs(opts)
97 else:
98 path = self.path
99 query = {}
101 if path == '/TiVoConnect':
102 self.handle_query(query, tsn)
103 else:
104 ## Get File
105 splitpath = [x for x in unquote_plus(path).split('/') if x]
106 if splitpath:
107 self.handle_file(query, splitpath)
108 else:
109 ## Not a file not a TiVo command
110 self.infopage()
112 def do_POST(self):
113 tsn = self.headers.getheader('TiVo_TCD_ID',
114 self.headers.getheader('tsn', ''))
115 if not self.authorize(tsn):
116 return
117 ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
118 if ctype == 'multipart/form-data':
119 query = cgi.parse_multipart(self.rfile, pdict)
120 else:
121 length = int(self.headers.getheader('content-length'))
122 qs = self.rfile.read(length)
123 query = cgi.parse_qs(qs, keep_blank_values=1)
124 self.handle_query(query, tsn)
126 def handle_query(self, query, tsn):
127 mname = False
128 if 'Command' in query and len(query['Command']) >= 1:
130 command = query['Command'][0]
132 # If we are looking at the root container
133 if (command == 'QueryContainer' and
134 (not 'Container' in query or query['Container'][0] == '/')):
135 self.root_container()
136 return
138 if 'Container' in query:
139 # Dispatch to the container plugin
140 basepath = query['Container'][0].split('/')[0]
141 for name, container in config.getShares(tsn):
142 if basepath == name:
143 plugin = GetPlugin(container['type'])
144 if hasattr(plugin, command):
145 self.cname = name
146 self.container = container
147 method = getattr(plugin, command)
148 method(self, query)
149 return
150 else:
151 break
153 elif (command == 'QueryFormats' and 'SourceFormat' in query and
154 query['SourceFormat'][0].startswith('video')):
155 self.send_response(200)
156 self.send_header('Content-type', 'text/xml')
157 self.end_headers()
158 if config.hasTStivo(tsn):
159 self.wfile.write(VIDEO_FORMATS_TS)
160 else:
161 self.wfile.write(VIDEO_FORMATS)
162 return
164 elif command == 'FlushServer':
165 # Does nothing -- included for completeness
166 self.send_response(200)
167 self.end_headers()
168 return
170 # If we made it here it means we couldn't match the request to
171 # anything.
172 self.unsupported(query)
174 def handle_file(self, query, splitpath):
175 if '..' not in splitpath: # Protect against path exploits
176 ## Pass it off to a plugin?
177 for name, container in self.server.containers.items():
178 if splitpath[0] == name:
179 self.cname = name
180 self.container = container
181 base = os.path.normpath(container['path'])
182 path = os.path.join(base, *splitpath[1:])
183 plugin = GetPlugin(container['type'])
184 plugin.send_file(self, path, query)
185 return
187 ## Serve it from a "content" directory?
188 base = os.path.join(SCRIPTDIR, *splitpath[:-1])
189 path = os.path.join(base, 'content', splitpath[-1])
191 if os.path.isfile(path):
192 try:
193 handle = open(path, 'rb')
194 except:
195 self.send_error(404)
196 return
198 # Send the header
199 mime = mimetypes.guess_type(path)[0]
200 self.send_response(200)
201 if mime:
202 self.send_header('Content-type', mime)
203 self.send_header('Content-length', os.path.getsize(path))
204 self.end_headers()
206 # Send the body of the file
207 try:
208 shutil.copyfileobj(handle, self.wfile)
209 except:
210 pass
211 handle.close()
212 return
214 ## Give up
215 self.send_error(404)
217 def authorize(self, tsn=None):
218 # if allowed_clients is empty, we are completely open
219 allowed_clients = config.getAllowedClients()
220 if not allowed_clients or (tsn and config.isTsnInConfig(tsn)):
221 return True
222 client_ip = self.client_address[0]
223 for allowedip in allowed_clients:
224 if client_ip.startswith(allowedip):
225 return True
227 self.send_response(404)
228 self.send_header('Content-type', 'text/plain')
229 self.end_headers()
230 self.wfile.write("Unauthorized.")
231 return False
233 def log_message(self, format, *args):
234 self.server.logger.info("%s [%s] %s" % (self.address_string(),
235 self.log_date_time_string(), format%args))
237 def root_container(self):
238 tsn = self.headers.getheader('TiVo_TCD_ID', '')
239 tsnshares = config.getShares(tsn)
240 tsncontainers = []
241 for section, settings in tsnshares:
242 try:
243 mime = GetPlugin(settings['type']).CONTENT_TYPE
244 if mime.split('/')[1] in ('tivo-videos', 'tivo-music',
245 'tivo-photos'):
246 settings['content_type'] = mime
247 tsncontainers.append((section, settings))
248 except Exception, msg:
249 self.server.logger.error(section + ' - ' + str(msg))
250 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
251 'root_container.tmpl'),
252 filter=EncodeUnicode)
253 t.containers = tsncontainers
254 t.hostname = socket.gethostname()
255 t.escape = escape
256 t.quote = quote
257 self.send_response(200)
258 self.send_header('Content-type', 'text/xml')
259 self.end_headers()
260 self.wfile.write(t)
262 def infopage(self):
263 useragent = self.headers.getheader('User-Agent', '')
264 self.send_response(200)
265 self.send_header('Content-type', 'text/html; charset=utf-8')
266 self.end_headers()
267 if useragent.lower().find('mobile') > 0:
268 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
269 'info_page_mob.tmpl'),
270 filter=EncodeUnicode)
271 else:
272 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
273 'info_page.tmpl'),
274 filter=EncodeUnicode)
275 t.admin = ''
277 if config.get_server('tivo_mak') and config.get_server('togo_path'):
278 t.togo = 'Pull from TiVos:<br>'
279 else:
280 t.togo = ''
282 if (config.get_server('tivo_username') and
283 config.get_server('tivo_password')):
284 t.shares = 'Push from video shares:<br>'
285 else:
286 t.shares = ''
288 for section, settings in config.getShares():
289 plugin_type = settings.get('type')
290 if plugin_type == 'settings':
291 t.admin += ('<a href="/TiVoConnect?Command=Settings&amp;' +
292 'Container=' + quote(section) +
293 '">Web Configuration</a><br>')
294 elif plugin_type == 'togo' and t.togo:
295 for tsn in config.tivos:
296 if tsn:
297 t.togo += ('<a href="/TiVoConnect?' +
298 'Command=NPL&amp;Container=' + quote(section) +
299 '&amp;TiVo=' + config.tivos[tsn] + '">' +
300 escape(config.tivo_names[tsn]) + '</a><br>')
301 elif ( plugin_type == 'video' or plugin_type == 'dvdvideo' ) \
302 and t.shares:
303 t.shares += ('<a href="TiVoConnect?Command=' +
304 'QueryContainer&amp;Container=' +
305 quote(section) + '&Format=text/html">' +
306 section + '</a><br>')
308 self.wfile.write(t)
310 def unsupported(self, query):
311 message = UNSUP % '\n'.join(['<li>%s: %s</li>' % (escape(key),
312 escape(repr(value)))
313 for key, value in query.items()])
314 text = BASE_HTML % message
315 self.send_response(404)
316 self.send_header('Content-Type', 'text/html; charset=utf-8')
317 self.send_header('Content-Length', len(text))
318 self.end_headers()
319 self.wfile.write(text)
321 def redir(self, message, seconds=2):
322 url = self.headers.getheader('Referer')
323 if url:
324 message += RELOAD % (escape(url), seconds)
325 text = (BASE_HTML % message).encode('utf-8')
326 self.send_response(200)
327 self.send_header('Content-Type', 'text/html; charset=utf-8')
328 self.send_header('Content-Length', len(text))
329 self.send_header('Expires', '0')
330 if url:
331 self.send_header('Refresh', '%d; url=%s' % (seconds, url))
332 self.end_headers()
333 self.wfile.write(text)