No need to step through the loop.
[pyTivo/TheBayer.git] / httpserver.py
blob9e6a7579aa1c3c13a0c9c01ea33dd87ee374a8d3
1 import BaseHTTPServer
2 import SocketServer
3 import cgi
4 import logging
5 import mimetypes
6 import os
7 import re
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, GetPluginPath, 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 RE_PLUGIN_CONTENT = re.compile(r'/plugin/([^/]+)/content/(.+)')
26 class TivoHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
27 containers = {}
29 def __init__(self, server_address, RequestHandlerClass):
30 BaseHTTPServer.HTTPServer.__init__(self, server_address,
31 RequestHandlerClass)
32 self.daemon_threads = True
33 self.logger = logging.getLogger('pyTivo')
35 def add_container(self, name, settings):
36 if name in self.containers or name == 'TiVoConnect':
37 raise "Container Name in use"
38 try:
39 self.containers[name] = settings
40 except KeyError:
41 self.logger.error('Unable to add container ' + name)
43 def reset(self):
44 self.containers.clear()
45 for section, settings in config.getShares():
46 self.add_container(section, settings)
48 def handle_error(self, request, client_address):
49 self.logger.exception('Exception during request from %s' %
50 (client_address,))
52 def set_beacon(self, beacon):
53 self.beacon = beacon
55 class TivoHTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
56 def __init__(self, request, client_address, server):
57 self.wbufsize = 0x10000
58 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
59 client_address, server)
61 def address_string(self):
62 host, port = self.client_address[:2]
63 return host
65 def do_GET(self):
66 tsn = self.headers.getheader('TiVo_TCD_ID',
67 self.headers.getheader('tsn', ''))
68 if not self.authorize(tsn):
69 return
70 if tsn:
71 ip = self.address_string()
72 config.tivos[tsn] = ip
74 if not tsn in config.tivo_names or config.tivo_names[tsn] == tsn:
75 config.tivo_names[tsn] = self.server.beacon.get_name(ip)
77 if '?' in self.path:
78 path, opts = self.path.split('?', 1)
79 query = cgi.parse_qs(opts)
80 else:
81 path = self.path
82 query = {}
84 if path == '/TiVoConnect':
85 self.handle_query(query, tsn)
86 else:
87 ## Get File
88 path = unquote_plus(path)
89 basepath = path.split('/')[1]
90 for name, container in self.server.containers.items():
91 if basepath == name:
92 path = os.path.join(os.path.normpath(container['path']),
93 os.path.normpath(path[len(name) + 2:]))
94 plugin = GetPlugin(container['type'])
95 plugin.send_file(self, path, query)
96 return
98 regm = RE_PLUGIN_CONTENT.match(path)
99 if regm != None:
100 self.handle_plugin_content(regm)
102 ## Not a file not a TiVo command
103 self.infopage()
105 def do_POST(self):
106 tsn = self.headers.getheader('TiVo_TCD_ID',
107 self.headers.getheader('tsn', ''))
108 if not self.authorize(tsn):
109 return
110 ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
111 if ctype == 'multipart/form-data':
112 query = cgi.parse_multipart(self.rfile, pdict)
113 else:
114 length = int(self.headers.getheader('content-length'))
115 qs = self.rfile.read(length)
116 query = cgi.parse_qs(qs, keep_blank_values=1)
117 self.handle_query(query, tsn)
119 def handle_query(self, query, tsn):
120 mname = False
121 if 'Command' in query and len(query['Command']) >= 1:
123 command = query['Command'][0]
125 # If we are looking at the root container
126 if (command == 'QueryContainer' and
127 (not 'Container' in query or query['Container'][0] == '/')):
128 self.root_container()
129 return
131 if 'Container' in query:
132 # Dispatch to the container plugin
133 basepath = query['Container'][0].split('/')[0]
134 for name, container in config.getShares(tsn):
135 if basepath == name:
136 plugin = GetPlugin(container['type'])
137 if hasattr(plugin, command):
138 method = getattr(plugin, command)
139 method(self, query)
140 return
141 else:
142 break
144 elif (command == 'QueryFormats' and 'SourceFormat' in query and
145 query['SourceFormat'][0].startswith('video')):
146 self.send_response(200)
147 self.send_header('Content-type', 'text/xml')
148 self.end_headers()
149 self.wfile.write(VIDEO_FORMATS)
150 return
152 elif command == 'FlushServer':
153 # Does nothing -- included for completeness
154 self.send_response(200)
155 self.end_headers()
156 return
158 # If we made it here it means we couldn't match the request to
159 # anything.
160 self.unsupported(query)
162 def handle_plugin_content(self, regm):
163 # Handle general plugin content requests of the form
164 # /plugin/<plugin type>/content/<file>
165 try:
166 # Protect ourself from path exploits
167 file_bits = regm.group(2).split('/')
168 if '..' in file_bits:
169 raise
171 # Get the plugin path
172 plugin_path = GetPluginPath(regm.group(1))
174 # Build up the actual path based on the plugin path
175 filen = os.path.join(plugin_path, 'content', *file_bits)
177 # If it's not a file, then just error out
178 if not os.path.isfile(filen):
179 raise
181 # Read in the full file
182 handle = open(filen, 'rb')
183 try:
184 text = handle.read()
185 handle.close()
186 except:
187 handle.close()
188 raise
190 # Send the header
191 mime = mimetypes.guess_type(filen)[0]
192 self.send_response(200)
193 if mime:
194 self.send_header('Content-type', mime)
195 self.send_header('Content-length', os.path.getsize(filen))
196 self.end_headers()
198 # Send the body of the file
199 self.wfile.write(text)
200 except:
201 self.send_response(404)
202 self.end_headers()
203 self.wfile.write('File not found')
205 def authorize(self, tsn=None):
206 # if allowed_clients is empty, we are completely open
207 allowed_clients = config.getAllowedClients()
208 if not allowed_clients or (tsn and config.isTsnInConfig(tsn)):
209 return True
210 client_ip = self.client_address[0]
211 for allowedip in allowed_clients:
212 if client_ip.startswith(allowedip):
213 return True
215 self.send_response(404)
216 self.send_header('Content-type', 'text/plain')
217 self.end_headers()
218 self.wfile.write("Unauthorized.")
219 return False
221 def log_message(self, format, *args):
222 self.server.logger.info("%s [%s] %s" % (self.address_string(),
223 self.log_date_time_string(), format%args))
225 def root_container(self):
226 tsn = self.headers.getheader('TiVo_TCD_ID', '')
227 tsnshares = config.getShares(tsn)
228 tsncontainers = []
229 for section, settings in tsnshares:
230 try:
231 settings['content_type'] = \
232 GetPlugin(settings['type']).CONTENT_TYPE
233 tsncontainers.append((section, settings))
234 except Exception, msg:
235 self.server.logger.error(section + ' - ' + str(msg))
236 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
237 'root_container.tmpl'),
238 filter=EncodeUnicode)
239 t.containers = tsncontainers
240 t.hostname = socket.gethostname()
241 t.escape = escape
242 t.quote = quote
243 self.send_response(200)
244 self.send_header('Content-type', 'text/xml')
245 self.end_headers()
246 self.wfile.write(t)
248 def infopage(self):
249 self.send_response(200)
250 self.send_header('Content-type', 'text/html')
251 self.end_headers()
252 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
253 'info_page.tmpl'))
254 t.admin = ''
256 if config.get_server('tivo_mak') and config.get_server('togo_path'):
257 t.togo = '<br>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 = '<br>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 config.tivo_names[tsn] + '</a><br>')
280 elif plugin_type == 'video' and t.shares:
281 t.shares += ('<a href="TiVoConnect?Command=' +
282 'QueryContainer&amp;Container=' +
283 quote(section) + '">' + section + '</a><br>')
285 self.wfile.write(t)
287 def unsupported(self, query):
288 self.send_response(404)
289 self.send_header('Content-type', 'text/html')
290 self.end_headers()
291 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
292 'unsupported.tmpl'))
293 t.query = query
294 self.wfile.write(t)