Fixes for bugs at the end of a transfer.
[pyTivo/TheBayer.git] / httpserver.py
blobffc7862f204c4a43d8c6210c1ebc6d9b8d68d66a
1 import BaseHTTPServer
2 import SocketServer
3 import cgi
4 import logging
5 import os
6 import re
7 import socket
8 import time
9 import mimetypes
11 from urllib import unquote_plus, quote
12 from xml.sax.saxutils import escape
14 from Cheetah.Template import Template
15 import config
16 from plugin import GetPlugin, GetPluginPath, EncodeUnicode
18 SCRIPTDIR = os.path.dirname(__file__)
20 VIDEO_FORMATS = """<?xml version="1.0" encoding="utf-8"?>
21 <TiVoFormats><Format>
22 <ContentType>video/x-tivo-mpeg</ContentType><Description/>
23 </Format></TiVoFormats>"""
25 RE_PLUGIN_CONTENT = re.compile( r"/plugin/([^/]+)/content/(.+)" )
27 class TivoHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
28 containers = {}
30 def __init__(self, server_address, RequestHandlerClass):
31 BaseHTTPServer.HTTPServer.__init__(self, server_address,
32 RequestHandlerClass)
33 self.daemon_threads = True
34 self.logger = logging.getLogger('pyTivo')
36 def add_container(self, name, settings):
37 if name in self.containers or name == 'TiVoConnect':
38 raise "Container Name in use"
39 try:
40 self.containers[name] = settings
41 except KeyError:
42 self.logger.error('Unable to add container ' + name)
44 def reset(self):
45 self.containers.clear()
46 for section, settings in config.getShares():
47 self.add_container(section, settings)
49 def handle_error(self, request, client_address):
50 self.logger.exception('Exception during request from %s' %
51 (client_address,))
53 def set_beacon(self, beacon):
54 self.beacon = beacon
56 class TivoHTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
57 def __init__(self, request, client_address, server):
58 self.wbufsize = 0x10000
59 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
60 client_address, server)
62 def address_string(self):
63 host, port = self.client_address[:2]
64 return host
66 def do_GET(self):
67 tsn = self.headers.getheader('TiVo_TCD_ID',
68 self.headers.getheader('tsn', ''))
69 if not self.authorize(tsn):
70 return
71 if tsn:
72 ip = self.address_string()
73 config.tivos[tsn] = ip
75 if not tsn in config.tivo_names or config.tivo_names[tsn] == tsn:
76 config.tivo_names[tsn] = self.server.beacon.get_name(ip)
78 if '?' in self.path:
79 path, opts = self.path.split('?', 1)
80 query = cgi.parse_qs(opts)
81 else:
82 path = self.path
83 query = {}
85 regm = RE_PLUGIN_CONTENT.match( path )
87 if path == '/TiVoConnect':
88 self.handle_query(query, tsn)
90 # Handle general plugin content requests of the form
91 # /plugin/<plugin type>/content/<file>
92 elif regm != None:
93 try:
94 # Protect ourself from path exploits
95 file_bits = regm.group(2).split( "/" )
96 for bit in file_bits:
97 if bit == "..":
98 raise
100 # Get the plugin path
101 plugin_path = GetPluginPath( regm.group(1) )
103 # Build up the actual path based on the plugin path
104 filen = os.path.join( plugin_path, "content", *file_bits )
106 # If it's not a file, then just error out
107 if not os.path.isfile( filen ):
108 raise
110 # Read in the full file
111 handle = open( filen, "rb" )
112 try:
113 text = handle.read()
114 handle.close()
115 except:
116 handle.close()
117 raise
119 # Send the header
120 self.send_response(200)
121 self.send_header( "Content-type", \
122 mimetypes.guess_type( filen ) )
123 self.send_header( "Content-length", \
124 os.path.getsize( filen ) )
125 self.end_headers()
127 # Send the body of the file
128 self.wfile.write( text )
129 self.wfile.flush()
130 return
132 except:
133 self.send_response(404)
134 self.end_headers()
135 self.wfile.write( "File not found" )
136 self.wfile.flush()
137 return
139 else:
140 ## Get File
141 path = unquote_plus(path)
142 basepath = path.split('/')[1]
143 for name, container in self.server.containers.items():
144 if basepath == name:
145 path = os.path.join(os.path.normpath(container['path']),
146 os.path.normpath(path[len(name) + 2:]))
147 plugin = GetPlugin(container['type'])
148 plugin.send_file(self, path, query)
149 return
151 ## Not a file not a TiVo command
152 self.infopage()
154 def do_POST(self):
155 tsn = self.headers.getheader('TiVo_TCD_ID',
156 self.headers.getheader('tsn', ''))
157 if not self.authorize(tsn):
158 return
159 ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
160 if ctype == 'multipart/form-data':
161 query = cgi.parse_multipart(self.rfile, pdict)
162 else:
163 length = int(self.headers.getheader('content-length'))
164 qs = self.rfile.read(length)
165 query = cgi.parse_qs(qs, keep_blank_values=1)
166 self.handle_query(query, tsn)
168 def handle_query(self, query, tsn):
169 mname = False
170 if 'Command' in query and len(query['Command']) >= 1:
172 command = query['Command'][0]
174 # If we are looking at the root container
175 if (command == 'QueryContainer' and
176 (not 'Container' in query or query['Container'][0] == '/')):
177 self.root_container()
178 return
180 if 'Container' in query:
181 # Dispatch to the container plugin
182 basepath = query['Container'][0].split('/')[0]
183 for name, container in config.getShares(tsn):
184 if basepath == name:
185 plugin = GetPlugin(container['type'])
186 if hasattr(plugin, command):
187 method = getattr(plugin, command)
188 method(self, query)
189 return
190 else:
191 break
193 elif (command == 'QueryFormats' and 'SourceFormat' in query and
194 query['SourceFormat'][0].startswith('video')):
195 self.send_response(200)
196 self.send_header('Content-type', 'text/xml')
197 self.end_headers()
198 self.wfile.write(VIDEO_FORMATS)
199 return
201 elif command == 'FlushServer':
202 # Does nothing -- included for completeness
203 self.send_response(200)
204 self.end_headers()
205 return
207 # If we made it here it means we couldn't match the request to
208 # anything.
209 self.unsupported(query)
211 def authorize(self, tsn=None):
212 # if allowed_clients is empty, we are completely open
213 allowed_clients = config.getAllowedClients()
214 if not allowed_clients or (tsn and config.isTsnInConfig(tsn)):
215 return True
216 client_ip = self.client_address[0]
217 for allowedip in allowed_clients:
218 if client_ip.startswith(allowedip):
219 return True
221 self.send_response(404)
222 self.send_header('Content-type', 'text/plain')
223 self.end_headers()
224 self.wfile.write("Unauthorized.")
225 return False
227 def log_message(self, format, *args):
228 self.server.logger.info("%s [%s] %s" % (self.address_string(),
229 self.log_date_time_string(), format%args))
231 def root_container(self):
232 tsn = self.headers.getheader('TiVo_TCD_ID', '')
233 tsnshares = config.getShares(tsn)
234 tsncontainers = {}
235 for section, settings in tsnshares:
236 try:
237 settings['content_type'] = \
238 GetPlugin(settings['type']).CONTENT_TYPE
239 tsncontainers[section] = settings
240 except Exception, msg:
241 self.server.logger.error(section + ' - ' + str(msg))
242 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
243 'root_container.tmpl'),
244 filter=EncodeUnicode)
245 t.containers = tsncontainers
246 t.hostname = socket.gethostname()
247 t.escape = escape
248 t.quote = quote
249 self.send_response(200)
250 self.send_header('Content-type', 'text/xml')
251 self.end_headers()
252 self.wfile.write(t)
254 def infopage(self):
255 self.send_response(200)
256 self.send_header('Content-type', 'text/html')
257 self.end_headers()
258 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
259 'info_page.tmpl'))
260 shares = dict(config.getShares())
261 t.admin = ''
263 if config.get_server('tivo_mak') and config.get_server('togo_path'):
264 t.togo = '<br>Pull from TiVos:<br>'
265 else:
266 t.togo = ''
268 if (config.get_server('tivo_username') and
269 config.get_server('tivo_password')):
270 t.shares = '<br>Push from video shares:<br>'
271 else:
272 t.shares = ''
274 for section in shares:
275 plugin_type = shares[section].get('type')
276 if plugin_type == 'settings':
277 t.admin += ('<a href="/TiVoConnect?Command=Settings&amp;' +
278 'Container=' + quote(section) +
279 '">Web Configuration</a><br>')
280 elif plugin_type == 'togo' and t.togo:
281 for tsn in config.tivos:
282 if tsn:
283 t.togo += ('<a href="/TiVoConnect?' +
284 'Command=NPL&amp;Container=' + quote(section) +
285 '&amp;TiVo=' + config.tivos[tsn] + '">' +
286 config.tivo_names[tsn] + '</a><br>')
287 elif ( plugin_type == 'video' or plugin_type == 'dvdvideo' ) \
288 and t.shares:
289 t.shares += ('<a href="TiVoConnect?Command=' +
290 'QueryContainer&amp;Container=' +
291 quote(section) + '">' + section + '</a><br>')
293 self.wfile.write(t)
295 def unsupported(self, query):
296 self.send_response(404)
297 self.send_header('Content-type', 'text/html')
298 self.end_headers()
299 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
300 'unsupported.tmpl'))
301 t.query = query
302 self.wfile.write(t)