Don't need the special XSL() function anymore.
[pyTivo/TheBayer.git] / httpserver.py
blob27b2f2af2abb0f48e3ed999850e6efd93ed3406e
1 import BaseHTTPServer
2 import SocketServer
3 import cgi
4 import logging
5 import mimetypes
6 import os
7 import socket
8 import time
9 from urllib import unquote_plus, quote
10 from xml.sax.saxutils import escape
12 from Cheetah.Template import Template
13 import config
14 from plugin import GetPlugin, EncodeUnicode
16 SCRIPTDIR = os.path.dirname(__file__)
18 VIDEO_FORMATS = """<?xml version="1.0" encoding="utf-8"?>
19 <TiVoFormats><Format>
20 <ContentType>video/x-tivo-mpeg</ContentType><Description/>
21 </Format></TiVoFormats>"""
23 class TivoHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
24 containers = {}
26 def __init__(self, server_address, RequestHandlerClass):
27 BaseHTTPServer.HTTPServer.__init__(self, server_address,
28 RequestHandlerClass)
29 self.daemon_threads = True
30 self.logger = logging.getLogger('pyTivo')
32 def add_container(self, name, settings):
33 if name in self.containers or name == 'TiVoConnect':
34 raise "Container Name in use"
35 try:
36 self.containers[name] = settings
37 except KeyError:
38 self.logger.error('Unable to add container ' + name)
40 def reset(self):
41 self.containers.clear()
42 for section, settings in config.getShares():
43 self.add_container(section, settings)
45 def handle_error(self, request, client_address):
46 self.logger.exception('Exception during request from %s' %
47 (client_address,))
49 def set_beacon(self, beacon):
50 self.beacon = beacon
52 class TivoHTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
53 def __init__(self, request, client_address, server):
54 self.wbufsize = 0x10000
55 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
56 client_address, server)
58 def address_string(self):
59 host, port = self.client_address[:2]
60 return host
62 def do_GET(self):
63 tsn = self.headers.getheader('TiVo_TCD_ID',
64 self.headers.getheader('tsn', ''))
65 if not self.authorize(tsn):
66 return
67 if tsn:
68 ip = self.address_string()
69 config.tivos[tsn] = ip
71 if not tsn in config.tivo_names or config.tivo_names[tsn] == tsn:
72 config.tivo_names[tsn] = self.server.beacon.get_name(ip)
74 if '?' in self.path:
75 path, opts = self.path.split('?', 1)
76 query = cgi.parse_qs(opts)
77 else:
78 path = self.path
79 query = {}
81 if path == '/TiVoConnect':
82 self.handle_query(query, tsn)
83 else:
84 ## Get File
85 splitpath = [x for x in unquote_plus(path).split('/') if x]
86 if splitpath:
87 self.handle_file(query, splitpath)
88 else:
89 ## Not a file not a TiVo command
90 self.infopage()
92 def do_POST(self):
93 tsn = self.headers.getheader('TiVo_TCD_ID',
94 self.headers.getheader('tsn', ''))
95 if not self.authorize(tsn):
96 return
97 ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
98 if ctype == 'multipart/form-data':
99 query = cgi.parse_multipart(self.rfile, pdict)
100 else:
101 length = int(self.headers.getheader('content-length'))
102 qs = self.rfile.read(length)
103 query = cgi.parse_qs(qs, keep_blank_values=1)
104 self.handle_query(query, tsn)
106 def handle_query(self, query, tsn):
107 mname = False
108 if 'Command' in query and len(query['Command']) >= 1:
110 command = query['Command'][0]
112 # If we are looking at the root container
113 if (command == 'QueryContainer' and
114 (not 'Container' in query or query['Container'][0] == '/')):
115 self.root_container()
116 return
118 if 'Container' in query:
119 # Dispatch to the container plugin
120 basepath = query['Container'][0].split('/')[0]
121 for name, container in config.getShares(tsn):
122 if basepath == name:
123 plugin = GetPlugin(container['type'])
124 if hasattr(plugin, command):
125 method = getattr(plugin, command)
126 method(self, query)
127 return
128 else:
129 break
131 elif (command == 'QueryFormats' and 'SourceFormat' in query and
132 query['SourceFormat'][0].startswith('video')):
133 self.send_response(200)
134 self.send_header('Content-type', 'text/xml')
135 self.end_headers()
136 self.wfile.write(VIDEO_FORMATS)
137 return
139 elif command == 'FlushServer':
140 # Does nothing -- included for completeness
141 self.send_response(200)
142 self.end_headers()
143 return
145 # If we made it here it means we couldn't match the request to
146 # anything.
147 self.unsupported(query)
149 def handle_file(self, query, splitpath):
150 if '..' not in splitpath: # Protect against path exploits
151 ## Pass it off to a plugin?
152 for name, container in self.server.containers.items():
153 if splitpath[0] == name:
154 base = os.path.normpath(container['path'])
155 path = os.path.join(base, *splitpath[1:])
156 plugin = GetPlugin(container['type'])
157 plugin.send_file(self, path, query)
158 return
160 ## Serve it from a "content" directory?
161 base = os.path.join(SCRIPTDIR, *splitpath[:-1])
162 path = os.path.join(base, 'content', splitpath[-1])
164 if os.path.isfile(path):
165 # Read in the full file
166 try:
167 handle = open(path, 'rb')
168 text = handle.read()
169 handle.close()
170 except:
171 self.send_error(404)
172 return
174 # Send the header
175 mime = mimetypes.guess_type(path)[0]
176 self.send_response(200)
177 if mime:
178 self.send_header('Content-type', mime)
179 self.send_header('Content-length', os.path.getsize(path))
180 self.end_headers()
182 # Send the body of the file
183 self.wfile.write(text)
184 return
186 ## Give up
187 self.send_error(404)
189 def authorize(self, tsn=None):
190 # if allowed_clients is empty, we are completely open
191 allowed_clients = config.getAllowedClients()
192 if not allowed_clients or (tsn and config.isTsnInConfig(tsn)):
193 return True
194 client_ip = self.client_address[0]
195 for allowedip in allowed_clients:
196 if client_ip.startswith(allowedip):
197 return True
199 self.send_response(404)
200 self.send_header('Content-type', 'text/plain')
201 self.end_headers()
202 self.wfile.write("Unauthorized.")
203 return False
205 def log_message(self, format, *args):
206 self.server.logger.info("%s [%s] %s" % (self.address_string(),
207 self.log_date_time_string(), format%args))
209 def root_container(self):
210 tsn = self.headers.getheader('TiVo_TCD_ID', '')
211 tsnshares = config.getShares(tsn)
212 tsncontainers = []
213 for section, settings in tsnshares:
214 try:
215 settings['content_type'] = \
216 GetPlugin(settings['type']).CONTENT_TYPE
217 tsncontainers.append((section, settings))
218 except Exception, msg:
219 self.server.logger.error(section + ' - ' + str(msg))
220 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
221 'root_container.tmpl'),
222 filter=EncodeUnicode)
223 t.containers = tsncontainers
224 t.hostname = socket.gethostname()
225 t.escape = escape
226 t.quote = quote
227 self.send_response(200)
228 self.send_header('Content-type', 'text/xml')
229 self.end_headers()
230 self.wfile.write(t)
232 def infopage(self):
233 self.send_response(200)
234 self.send_header('Content-type', 'text/html')
235 self.end_headers()
236 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
237 'info_page.tmpl'))
238 t.admin = ''
240 if config.get_server('tivo_mak') and config.get_server('togo_path'):
241 t.togo = '<br>Pull from TiVos:<br>'
242 else:
243 t.togo = ''
245 if (config.get_server('tivo_username') and
246 config.get_server('tivo_password')):
247 t.shares = '<br>Push from video shares:<br>'
248 else:
249 t.shares = ''
251 for section, settings in config.getShares():
252 plugin_type = settings.get('type')
253 if plugin_type == 'settings':
254 t.admin += ('<a href="/TiVoConnect?Command=Settings&amp;' +
255 'Container=' + quote(section) +
256 '">Web Configuration</a><br>')
257 elif plugin_type == 'togo' and t.togo:
258 for tsn in config.tivos:
259 if tsn:
260 t.togo += ('<a href="/TiVoConnect?' +
261 'Command=NPL&amp;Container=' + quote(section) +
262 '&amp;TiVo=' + config.tivos[tsn] + '">' +
263 config.tivo_names[tsn] + '</a><br>')
264 elif plugin_type == 'video' and t.shares:
265 t.shares += ('<a href="TiVoConnect?Command=' +
266 'QueryContainer&amp;Container=' +
267 quote(section) + '">' + section + '</a><br>')
269 self.wfile.write(t)
271 def unsupported(self, query):
272 self.send_response(404)
273 self.send_header('Content-type', 'text/html')
274 self.end_headers()
275 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
276 'unsupported.tmpl'))
277 t.query = query
278 self.wfile.write(t)