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