Merge commit 'wmcbrine/master'
[pyTivo/TheBayer.git] / httpserver.py
blobe092fb68b35518df08e47dac2535df51d46bc772
1 import BaseHTTPServer
2 import SocketServer
3 import cgi
4 import logging
5 import mimetypes
6 import os
7 import re
8 import shutil
9 import socket
10 import time
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, GetPluginPath, EncodeUnicode
19 SCRIPTDIR = os.path.dirname(__file__)
21 VIDEO_FORMATS = """<?xml version="1.0" encoding="utf-8"?>
22 <TiVoFormats><Format>
23 <ContentType>video/x-tivo-mpeg</ContentType><Description/>
24 </Format></TiVoFormats>"""
26 RE_PLUGIN_CONTENT = re.compile( r"/plugin/([^/]+)/content/(.+)" )
28 class TivoHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
29 containers = {}
31 def __init__(self, server_address, RequestHandlerClass):
32 BaseHTTPServer.HTTPServer.__init__(self, server_address,
33 RequestHandlerClass)
34 self.daemon_threads = True
35 self.logger = logging.getLogger('pyTivo')
37 def add_container(self, name, settings):
38 if name in self.containers or name == 'TiVoConnect':
39 raise "Container Name in use"
40 try:
41 self.containers[name] = settings
42 except KeyError:
43 self.logger.error('Unable to add container ' + name)
45 def reset(self):
46 self.containers.clear()
47 for section, settings in config.getShares():
48 self.add_container(section, settings)
50 def handle_error(self, request, client_address):
51 self.logger.exception('Exception during request from %s' %
52 (client_address,))
54 def set_beacon(self, beacon):
55 self.beacon = beacon
57 class TivoHTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
58 def __init__(self, request, client_address, server):
59 self.wbufsize = 0x10000
60 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
61 client_address, server)
63 def address_string(self):
64 host, port = self.client_address[:2]
65 return host
67 def do_GET(self):
68 tsn = self.headers.getheader('TiVo_TCD_ID',
69 self.headers.getheader('tsn', ''))
70 if not self.authorize(tsn):
71 return
72 if tsn:
73 ip = self.address_string()
74 config.tivos[tsn] = ip
76 if not tsn in config.tivo_names or config.tivo_names[tsn] == tsn:
77 config.tivo_names[tsn] = self.server.beacon.get_name(ip)
79 if '?' in self.path:
80 path, opts = self.path.split('?', 1)
81 query = cgi.parse_qs(opts)
82 else:
83 path = self.path
84 query = {}
86 regm = RE_PLUGIN_CONTENT.match( path )
88 if path == '/TiVoConnect':
89 self.handle_query(query, tsn)
91 # Handle general plugin content requests of the form
92 # /plugin/<plugin type>/content/<file>
93 elif regm != None:
94 try:
95 # Protect ourself from path exploits
96 file_bits = regm.group(2).split( "/" )
97 for bit in file_bits:
98 if bit == "..":
99 raise
101 # Get the plugin path
102 plugin_path = GetPluginPath( regm.group(1) )
104 # Build up the actual path based on the plugin path
105 filen = os.path.join( plugin_path, "content", *file_bits )
107 # If it's not a file, then just error out
108 if not os.path.isfile( filen ):
109 raise
111 # Read in the full file
112 handle = open( filen, "rb" )
113 try:
114 text = handle.read()
115 handle.close()
116 except:
117 handle.close()
118 raise
120 # Send the header
121 self.send_response(200)
122 self.send_header( "Content-type", \
123 mimetypes.guess_type( filen ) )
124 self.send_header( "Content-length", \
125 os.path.getsize( filen ) )
126 self.end_headers()
128 # Send the body of the file
129 self.wfile.write( text )
130 self.wfile.flush()
131 return
133 except:
134 self.send_response(404)
135 self.end_headers()
136 self.wfile.write( "File not found" )
137 self.wfile.flush()
138 return
140 else:
141 ## Get File
142 path = unquote_plus(path)
143 basepath = unquote_plus(path).split('/')[1]
144 for name, container in self.server.containers.items():
145 if basepath == name:
146 path = os.path.join(os.path.normpath(container['path']),
147 os.path.normpath(path[len(name) + 2:]))
148 plugin = GetPlugin(container['type'])
149 plugin.send_file(self, path, query)
150 return
152 ## Not a file not a TiVo command
153 self.infopage()
155 def do_POST(self):
156 tsn = self.headers.getheader('TiVo_TCD_ID',
157 self.headers.getheader('tsn', ''))
158 if not self.authorize(tsn):
159 return
160 ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
161 if ctype == 'multipart/form-data':
162 query = cgi.parse_multipart(self.rfile, pdict)
163 else:
164 length = int(self.headers.getheader('content-length'))
165 qs = self.rfile.read(length)
166 query = cgi.parse_qs(qs, keep_blank_values=1)
167 self.handle_query(query, tsn)
169 def handle_query(self, query, tsn):
170 mname = False
171 if 'Command' in query and len(query['Command']) >= 1:
173 command = query['Command'][0]
175 # If we are looking at the root container
176 if (command == 'QueryContainer' and
177 (not 'Container' in query or query['Container'][0] == '/')):
178 self.root_container()
179 return
181 if 'Container' in query:
182 # Dispatch to the container plugin
183 basepath = query['Container'][0].split('/')[0]
184 for name, container in config.getShares(tsn):
185 if basepath == name:
186 plugin = GetPlugin(container['type'])
187 if hasattr(plugin, command):
188 method = getattr(plugin, command)
189 method(self, query)
190 return
191 else:
192 break
194 elif (command == 'QueryFormats' and 'SourceFormat' in query and
195 query['SourceFormat'][0].startswith('video')):
196 self.send_response(200)
197 self.send_header('Content-type', 'text/xml')
198 self.end_headers()
199 self.wfile.write(VIDEO_FORMATS)
200 return
202 elif command == 'FlushServer':
203 # Does nothing -- included for completeness
204 self.send_response(200)
205 self.end_headers()
206 return
208 # If we made it here it means we couldn't match the request to
209 # anything.
210 self.unsupported(query)
212 def handle_file(self, query, splitpath):
213 if '..' not in splitpath: # Protect against path exploits
214 ## Pass it off to a plugin?
215 for name, container in self.server.containers.items():
216 if splitpath[0] == name:
217 base = os.path.normpath(container['path'])
218 path = os.path.join(base, *splitpath[1:])
219 plugin = GetPlugin(container['type'])
220 plugin.send_file(self, path, query)
221 return
223 ## Serve it from a "content" directory?
224 base = os.path.join(SCRIPTDIR, *splitpath[:-1])
225 path = os.path.join(base, 'content', splitpath[-1])
227 if os.path.isfile(path):
228 try:
229 handle = open(path, 'rb')
230 except:
231 self.send_error(404)
232 return
234 # Send the header
235 mime = mimetypes.guess_type(path)[0]
236 self.send_response(200)
237 if mime:
238 self.send_header('Content-type', mime)
239 self.send_header('Content-length', os.path.getsize(path))
240 self.end_headers()
242 # Send the body of the file
243 try:
244 shutil.copyfileobj(handle, self.wfile)
245 except:
246 pass
247 handle.close()
248 return
250 ## Give up
251 self.send_error(404)
253 def authorize(self, tsn=None):
254 # if allowed_clients is empty, we are completely open
255 allowed_clients = config.getAllowedClients()
256 if not allowed_clients or (tsn and config.isTsnInConfig(tsn)):
257 return True
258 client_ip = self.client_address[0]
259 for allowedip in allowed_clients:
260 if client_ip.startswith(allowedip):
261 return True
263 self.send_response(404)
264 self.send_header('Content-type', 'text/plain')
265 self.end_headers()
266 self.wfile.write("Unauthorized.")
267 return False
269 def log_message(self, format, *args):
270 self.server.logger.info("%s [%s] %s" % (self.address_string(),
271 self.log_date_time_string(), format%args))
273 def root_container(self):
274 tsn = self.headers.getheader('TiVo_TCD_ID', '')
275 tsnshares = config.getShares(tsn)
276 tsncontainers = []
277 for section, settings in tsnshares:
278 try:
279 settings['content_type'] = \
280 GetPlugin(settings['type']).CONTENT_TYPE
281 tsncontainers.append((section, settings))
282 except Exception, msg:
283 self.server.logger.error(section + ' - ' + str(msg))
284 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
285 'root_container.tmpl'),
286 filter=EncodeUnicode)
287 t.containers = tsncontainers
288 t.hostname = socket.gethostname()
289 t.escape = escape
290 t.quote = quote
291 self.send_response(200)
292 self.send_header('Content-type', 'text/xml')
293 self.end_headers()
294 self.wfile.write(t)
296 def infopage(self):
297 self.send_response(200)
298 self.send_header('Content-type', 'text/html')
299 self.end_headers()
300 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
301 'info_page.tmpl'))
302 t.admin = ''
304 if config.get_server('tivo_mak') and config.get_server('togo_path'):
305 t.togo = '<br>Pull from TiVos:<br>'
306 else:
307 t.togo = ''
309 if (config.get_server('tivo_username') and
310 config.get_server('tivo_password')):
311 t.shares = '<br>Push from video shares:<br>'
312 else:
313 t.shares = ''
315 for section, settings in config.getShares():
316 plugin_type = settings.get('type')
317 if plugin_type == 'settings':
318 t.admin += ('<a href="/TiVoConnect?Command=Settings&amp;' +
319 'Container=' + quote(section) +
320 '">Web Configuration</a><br>')
321 elif plugin_type == 'togo' and t.togo:
322 for tsn in config.tivos:
323 if tsn:
324 t.togo += ('<a href="/TiVoConnect?' +
325 'Command=NPL&amp;Container=' + quote(section) +
326 '&amp;TiVo=' + config.tivos[tsn] + '">' +
327 escape(config.tivo_names[tsn]) + '</a><br>')
328 elif ( plugin_type == 'video' or plugin_type == 'dvdvideo' ) \
329 and t.shares:
330 t.shares += ('<a href="TiVoConnect?Command=' +
331 'QueryContainer&amp;Container=' +
332 quote(section) + '">' + section + '</a><br>')
334 self.wfile.write(t)
336 def unsupported(self, query):
337 self.send_response(404)
338 self.send_header('Content-type', 'text/html')
339 self.end_headers()
340 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
341 'unsupported.tmpl'))
342 t.query = query
343 self.wfile.write(t)