CSS for the ToGo page; comments in template instead of final page.
[pyTivo/TheBayer.git] / httpserver.py
bloba42a3e6d81b7bcf7081d59ebc7a0d639a3daae57
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 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 path = unquote_plus(path)
86 basepath = path.split('/')[1]
87 for name, container in self.server.containers.items():
88 if basepath == name:
89 path = os.path.join(os.path.normpath(container['path']),
90 os.path.normpath(path[len(name) + 2:]))
91 plugin = GetPlugin(container['type'])
92 plugin.send_file(self, path, query)
93 return
95 ## Not a file not a TiVo command
96 self.infopage()
98 def do_POST(self):
99 tsn = self.headers.getheader('TiVo_TCD_ID',
100 self.headers.getheader('tsn', ''))
101 if not self.authorize(tsn):
102 return
103 ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
104 if ctype == 'multipart/form-data':
105 query = cgi.parse_multipart(self.rfile, pdict)
106 else:
107 length = int(self.headers.getheader('content-length'))
108 qs = self.rfile.read(length)
109 query = cgi.parse_qs(qs, keep_blank_values=1)
110 self.handle_query(query, tsn)
112 def handle_query(self, query, tsn):
113 mname = False
114 if 'Command' in query and len(query['Command']) >= 1:
116 command = query['Command'][0]
118 # If we are looking at the root container
119 if (command == 'QueryContainer' and
120 (not 'Container' in query or query['Container'][0] == '/')):
121 self.root_container()
122 return
124 if 'Container' in query:
125 # Dispatch to the container plugin
126 basepath = query['Container'][0].split('/')[0]
127 for name, container in config.getShares(tsn):
128 if basepath == name:
129 plugin = GetPlugin(container['type'])
130 if hasattr(plugin, command):
131 method = getattr(plugin, command)
132 method(self, query)
133 return
134 else:
135 break
137 elif (command == 'QueryFormats' and 'SourceFormat' in query and
138 query['SourceFormat'][0].startswith('video')):
139 self.send_response(200)
140 self.send_header('Content-type', 'text/xml')
141 self.end_headers()
142 self.wfile.write(VIDEO_FORMATS)
143 return
145 elif command == 'FlushServer':
146 # Does nothing -- included for completeness
147 self.send_response(200)
148 self.end_headers()
149 return
151 # If we made it here it means we couldn't match the request to
152 # anything.
153 self.unsupported(query)
155 def authorize(self, tsn=None):
156 # if allowed_clients is empty, we are completely open
157 allowed_clients = config.getAllowedClients()
158 if not allowed_clients or (tsn and config.isTsnInConfig(tsn)):
159 return True
160 client_ip = self.client_address[0]
161 for allowedip in allowed_clients:
162 if client_ip.startswith(allowedip):
163 return True
165 self.send_response(404)
166 self.send_header('Content-type', 'text/plain')
167 self.end_headers()
168 self.wfile.write("Unauthorized.")
169 return False
171 def log_message(self, format, *args):
172 self.server.logger.info("%s [%s] %s" % (self.address_string(),
173 self.log_date_time_string(), format%args))
175 def root_container(self):
176 tsn = self.headers.getheader('TiVo_TCD_ID', '')
177 tsnshares = config.getShares(tsn)
178 tsncontainers = {}
179 for section, settings in tsnshares:
180 try:
181 settings['content_type'] = \
182 GetPlugin(settings['type']).CONTENT_TYPE
183 tsncontainers[section] = settings
184 except Exception, msg:
185 self.server.logger.error(section + ' - ' + str(msg))
186 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
187 'root_container.tmpl'),
188 filter=EncodeUnicode)
189 t.containers = tsncontainers
190 t.hostname = socket.gethostname()
191 t.escape = escape
192 t.quote = quote
193 self.send_response(200)
194 self.send_header('Content-type', 'text/xml')
195 self.end_headers()
196 self.wfile.write(t)
198 def infopage(self):
199 self.send_response(200)
200 self.send_header('Content-type', 'text/html')
201 self.end_headers()
202 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
203 'info_page.tmpl'))
204 shares = dict(config.getShares())
205 t.admin = ''
207 if config.get_server('tivo_mak') and config.get_server('togo_path'):
208 t.togo = '<br>Pull from TiVos:<br>'
209 else:
210 t.togo = ''
212 if (config.get_server('tivo_username') and
213 config.get_server('tivo_password')):
214 t.shares = '<br>Push from video shares:<br>'
215 else:
216 t.shares = ''
218 for section in shares:
219 plugin_type = shares[section].get('type')
220 if plugin_type == 'settings':
221 t.admin += ('<a href="/TiVoConnect?Command=Settings&amp;' +
222 'Container=' + quote(section) +
223 '">Web Configuration</a><br>')
224 elif plugin_type == 'togo' and t.togo:
225 for tsn in config.tivos:
226 if tsn:
227 t.togo += ('<a href="/TiVoConnect?' +
228 'Command=NPL&amp;Container=' + quote(section) +
229 '&amp;TiVo=' + config.tivos[tsn] + '">' +
230 config.tivo_names[tsn] + '</a><br>')
231 elif plugin_type == 'video' and t.shares:
232 t.shares += ('<a href="TiVoConnect?Command=' +
233 'QueryContainer&amp;Container=' +
234 quote(section) + '">' + section + '</a><br>')
236 self.wfile.write(t)
238 def unsupported(self, query):
239 self.send_response(404)
240 self.send_header('Content-type', 'text/html')
241 self.end_headers()
242 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
243 'unsupported.tmpl'))
244 t.query = query
245 self.wfile.write(t)