...this, too.
[pyTivo/wmcbrine.git] / httpserver.py
blob64c4b7412998310a9312cc4afa57b300ee0b13c1
1 import BaseHTTPServer
2 import SocketServer
3 import cgi
4 import gzip
5 import logging
6 import mimetypes
7 import os
8 import shutil
9 import socket
10 import time
11 from cStringIO import StringIO
12 from urllib import unquote_plus, quote
13 from xml.sax.saxutils import escape
15 from Cheetah.Template import Template
16 import config
17 from plugin import GetPlugin, EncodeUnicode
19 SCRIPTDIR = os.path.dirname(__file__)
21 SERVER_INFO = """<?xml version="1.0" encoding="utf-8"?>
22 <TiVoServer>
23 <Version>1.6</Version>
24 <InternalName>pyTivo</InternalName>
25 <InternalVersion>1.0</InternalVersion>
26 <Organization>pyTivo Developers</Organization>
27 <Comment>http://pytivo.sf.net/</Comment>
28 </TiVoServer>"""
30 VIDEO_FORMATS = """<?xml version="1.0" encoding="utf-8"?>
31 <TiVoFormats>
32 <Format><ContentType>video/x-tivo-mpeg</ContentType><Description/></Format>
33 </TiVoFormats>"""
35 VIDEO_FORMATS_TS = """<?xml version="1.0" encoding="utf-8"?>
36 <TiVoFormats>
37 <Format><ContentType>video/x-tivo-mpeg</ContentType><Description/></Format>
38 <Format><ContentType>video/x-tivo-mpeg-ts</ContentType><Description/></Format>
39 </TiVoFormats>"""
41 BASE_HTML = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
42 "http://www.w3.org/TR/html4/strict.dtd">
43 <html> <head><title>pyTivo</title></head> <body> %s </body> </html>"""
45 RELOAD = '<p>The <a href="%s">page</a> will reload in %d seconds.</p>'
46 UNSUP = '<h3>Unsupported Command</h3> <p>Query:</p> <ul>%s</ul>'
48 class TivoHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
49 def __init__(self, server_address, RequestHandlerClass):
50 self.containers = {}
51 self.stop = False
52 self.restart = False
53 self.logger = logging.getLogger('pyTivo')
54 BaseHTTPServer.HTTPServer.__init__(self, server_address,
55 RequestHandlerClass)
56 self.daemon_threads = True
58 def add_container(self, name, settings):
59 if name in self.containers or name == 'TiVoConnect':
60 raise "Container Name in use"
61 try:
62 self.containers[name] = settings
63 except KeyError:
64 self.logger.error('Unable to add container ' + name)
66 def reset(self):
67 self.containers.clear()
68 for section, settings in config.getShares():
69 self.add_container(section, settings)
71 def handle_error(self, request, client_address):
72 self.logger.exception('Exception during request from %s' %
73 (client_address,))
75 def set_beacon(self, beacon):
76 self.beacon = beacon
78 def set_service_status(self, status):
79 self.in_service = status
81 class TivoHTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
82 def __init__(self, request, client_address, server):
83 self.wbufsize = 0x10000
84 self.server_version = 'pyTivo/1.0'
85 self.protocol_version = 'HTTP/1.1'
86 self.sys_version = ''
87 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
88 client_address, server)
90 def address_string(self):
91 host, port = self.client_address[:2]
92 return host
94 def version_string(self):
95 """ Override version_string() so it doesn't include the Python
96 version.
98 """
99 return self.server_version
101 def do_GET(self):
102 tsn = self.headers.getheader('TiVo_TCD_ID',
103 self.headers.getheader('tsn', ''))
104 if not self.authorize(tsn):
105 return
106 if tsn:
107 ip = self.address_string()
108 config.tivos[tsn] = ip
110 if not tsn in config.tivo_names or config.tivo_names[tsn] == tsn:
111 config.tivo_names[tsn] = self.server.beacon.get_name(ip)
113 if '?' in self.path:
114 path, opts = self.path.split('?', 1)
115 query = cgi.parse_qs(opts)
116 else:
117 path = self.path
118 query = {}
120 if path == '/TiVoConnect':
121 self.handle_query(query, tsn)
122 else:
123 ## Get File
124 splitpath = [x for x in unquote_plus(path).split('/') if x]
125 if splitpath:
126 self.handle_file(query, splitpath)
127 else:
128 ## Not a file not a TiVo command
129 self.infopage()
131 def do_POST(self):
132 tsn = self.headers.getheader('TiVo_TCD_ID',
133 self.headers.getheader('tsn', ''))
134 if not self.authorize(tsn):
135 return
136 ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
137 if ctype == 'multipart/form-data':
138 query = cgi.parse_multipart(self.rfile, pdict)
139 else:
140 length = int(self.headers.getheader('content-length'))
141 qs = self.rfile.read(length)
142 query = cgi.parse_qs(qs, keep_blank_values=1)
143 self.handle_query(query, tsn)
145 def do_command(self, query, command, target, tsn):
146 for name, container in config.getShares(tsn):
147 if target == name:
148 plugin = GetPlugin(container['type'])
149 if hasattr(plugin, command):
150 self.cname = name
151 self.container = container
152 method = getattr(plugin, command)
153 method(self, query)
154 return True
155 else:
156 break
157 return False
159 def handle_query(self, query, tsn):
160 mname = False
161 if 'Command' in query and len(query['Command']) >= 1:
163 command = query['Command'][0]
165 # If we are looking at the root container
166 if (command == 'QueryContainer' and
167 (not 'Container' in query or query['Container'][0] == '/')):
168 self.root_container()
169 return
171 if 'Container' in query:
172 # Dispatch to the container plugin
173 basepath = query['Container'][0].split('/')[0]
174 if self.do_command(query, command, basepath, tsn):
175 return
177 elif command == 'QueryItem':
178 path = query.get('Url', [''])[0]
179 splitpath = [x for x in unquote_plus(path).split('/') if x]
180 if splitpath and not '..' in splitpath:
181 if self.do_command(query, command, splitpath[0], tsn):
182 return
184 elif (command == 'QueryFormats' and 'SourceFormat' in query and
185 query['SourceFormat'][0].startswith('video')):
186 if config.is_ts_capable(tsn):
187 self.send_xml(VIDEO_FORMATS_TS)
188 else:
189 self.send_xml(VIDEO_FORMATS)
190 return
192 elif command == 'QueryServer':
193 self.send_xml(SERVER_INFO)
194 return
196 elif command in ('FlushServer', 'ResetServer'):
197 # Does nothing -- included for completeness
198 self.send_response(200)
199 self.end_headers()
200 return
202 # If we made it here it means we couldn't match the request to
203 # anything.
204 self.unsupported(query)
206 def handle_file(self, query, splitpath):
207 if '..' not in splitpath: # Protect against path exploits
208 ## Pass it off to a plugin?
209 for name, container in self.server.containers.items():
210 if splitpath[0] == name:
211 self.cname = name
212 self.container = container
213 base = os.path.normpath(container['path'])
214 path = os.path.join(base, *splitpath[1:])
215 plugin = GetPlugin(container['type'])
216 plugin.send_file(self, path, query)
217 return
219 ## Serve it from a "content" directory?
220 base = os.path.join(SCRIPTDIR, *splitpath[:-1])
221 path = os.path.join(base, 'content', splitpath[-1])
223 if os.path.isfile(path):
224 try:
225 handle = open(path, 'rb')
226 except:
227 self.send_error(404)
228 return
230 # Send the header
231 mime = mimetypes.guess_type(path)[0]
232 self.send_response(200)
233 if mime:
234 self.send_header('Content-type', mime)
235 self.send_header('Content-length', os.path.getsize(path))
236 self.end_headers()
238 # Send the body of the file
239 try:
240 shutil.copyfileobj(handle, self.wfile)
241 except:
242 pass
243 handle.close()
244 return
246 ## Give up
247 self.send_error(404)
249 def authorize(self, tsn=None):
250 # if allowed_clients is empty, we are completely open
251 allowed_clients = config.getAllowedClients()
252 if not allowed_clients or (tsn and config.isTsnInConfig(tsn)):
253 return True
254 client_ip = self.client_address[0]
255 for allowedip in allowed_clients:
256 if client_ip.startswith(allowedip):
257 return True
259 self.send_fixed('Unauthorized.', 'text/plain', 403)
260 return False
262 def log_message(self, format, *args):
263 self.server.logger.info("%s [%s] %s" % (self.address_string(),
264 self.log_date_time_string(), format%args))
266 def send_fixed(self, page, mime, code=200, refresh=''):
267 squeeze = (len(page) > 256 and mime.startswith('text') and
268 'gzip' in self.headers.getheader('Accept-Encoding', ''))
269 if squeeze:
270 out = StringIO()
271 gzip.GzipFile(mode='wb', fileobj=out).write(page)
272 page = out.getvalue()
273 out.close()
274 self.send_response(code)
275 self.send_header('Content-Type', mime)
276 self.send_header('Content-Length', len(page))
277 if squeeze:
278 self.send_header('Content-Encoding', 'gzip')
279 self.send_header('Expires', '0')
280 if refresh:
281 self.send_header('Refresh', refresh)
282 self.end_headers()
283 self.wfile.write(page)
285 def send_xml(self, page):
286 self.send_fixed(page, 'text/xml')
288 def send_html(self, page, code=200, refresh=''):
289 self.send_fixed(page, 'text/html; charset=utf-8', code, refresh)
291 def root_container(self):
292 tsn = self.headers.getheader('TiVo_TCD_ID', '')
293 tsnshares = config.getShares(tsn)
294 tsncontainers = []
295 for section, settings in tsnshares:
296 try:
297 mime = GetPlugin(settings['type']).CONTENT_TYPE
298 if mime.split('/')[1] in ('tivo-videos', 'tivo-music',
299 'tivo-photos'):
300 settings['content_type'] = mime
301 tsncontainers.append((section, settings))
302 except Exception, msg:
303 self.server.logger.error(section + ' - ' + str(msg))
304 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
305 'root_container.tmpl'),
306 filter=EncodeUnicode)
307 if self.server.beacon.bd:
308 t.renamed = self.server.beacon.bd.renamed
309 else:
310 t.renamed = {}
311 t.containers = tsncontainers
312 t.hostname = socket.gethostname()
313 t.escape = escape
314 t.quote = quote
315 self.send_xml(str(t))
317 def infopage(self):
318 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
319 'info_page.tmpl'),
320 filter=EncodeUnicode)
321 t.admin = ''
323 if config.get_server('tivo_mak') and config.get_server('togo_path'):
324 t.togo = '<br>Pull from TiVos:<br>'
325 else:
326 t.togo = ''
328 if (config.get_server('tivo_username') and
329 config.get_server('tivo_password')):
330 t.shares = '<br>Push from video shares:<br>'
331 else:
332 t.shares = ''
334 for section, settings in config.getShares():
335 plugin_type = settings.get('type')
336 if plugin_type == 'settings':
337 t.admin += ('<a href="/TiVoConnect?Command=Settings&amp;' +
338 'Container=' + quote(section) +
339 '">Web Configuration</a><br>')
340 elif plugin_type == 'togo' and t.togo:
341 for tsn in config.tivos:
342 if tsn:
343 t.togo += ('<a href="/TiVoConnect?' +
344 'Command=NPL&amp;Container=' + quote(section) +
345 '&amp;TiVo=' + config.tivos[tsn] + '">' +
346 escape(config.tivo_names[tsn]) + '</a><br>')
347 elif plugin_type and t.shares:
348 plugin = GetPlugin(plugin_type)
349 if hasattr(plugin, 'Push'):
350 t.shares += ('<a href="/TiVoConnect?Command=' +
351 'QueryContainer&amp;Container=' +
352 quote(section) + '&Format=text/html">' +
353 section + '</a><br>')
355 self.send_html(str(t))
357 def unsupported(self, query):
358 message = UNSUP % '\n'.join(['<li>%s: %s</li>' % (escape(key),
359 escape(repr(value)))
360 for key, value in query.items()])
361 text = BASE_HTML % message
362 self.send_html(text, code=404)
364 def redir(self, message, seconds=2):
365 url = self.headers.getheader('Referer')
366 if url:
367 message += RELOAD % (escape(url), seconds)
368 refresh = '%d; url=%s' % (seconds, url)
369 else:
370 refresh = ''
371 text = (BASE_HTML % message).encode('utf-8')
372 self.send_html(text, refresh=refresh)