12 from urllib
import unquote_plus
, quote
13 from xml
.sax
.saxutils
import escape
15 from Cheetah
.Template
import Template
17 from plugin
import GetPlugin
, GetPluginPath
, EncodeUnicode
19 SCRIPTDIR
= os
.path
.dirname(__file__
)
21 VIDEO_FORMATS
= """<?xml version="1.0" encoding="utf-8"?>
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
):
31 def __init__(self
, server_address
, RequestHandlerClass
):
32 BaseHTTPServer
.HTTPServer
.__init
__(self
, server_address
,
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"
41 self
.containers
[name
] = settings
43 self
.logger
.error('Unable to add container ' + name
)
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' %
54 def set_beacon(self
, 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]
68 tsn
= self
.headers
.getheader('TiVo_TCD_ID',
69 self
.headers
.getheader('tsn', ''))
70 if not self
.authorize(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
)
80 path
, opts
= self
.path
.split('?', 1)
81 query
= cgi
.parse_qs(opts
)
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>
95 # Protect ourself from path exploits
96 file_bits
= regm
.group(2).split( "/" )
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
):
111 # Read in the full file
112 handle
= open( filen
, "rb" )
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
) )
128 # Send the body of the file
129 self
.wfile
.write( text
)
134 self
.send_response(404)
136 self
.wfile
.write( "File not found" )
142 path
= unquote_plus(path
)
143 basepath
= unquote_plus(path
).split('/')[1]
144 for name
, container
in self
.server
.containers
.items():
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
)
152 ## Not a file not a TiVo command
156 tsn
= self
.headers
.getheader('TiVo_TCD_ID',
157 self
.headers
.getheader('tsn', ''))
158 if not self
.authorize(tsn
):
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
)
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
):
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()
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
):
186 plugin
= GetPlugin(container
['type'])
187 if hasattr(plugin
, command
):
188 method
= getattr(plugin
, command
)
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')
199 self
.wfile
.write(VIDEO_FORMATS
)
202 elif command
== 'FlushServer':
203 # Does nothing -- included for completeness
204 self
.send_response(200)
208 # If we made it here it means we couldn't match the request to
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
)
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
):
229 handle
= open(path
, 'rb')
235 mime
= mimetypes
.guess_type(path
)[0]
236 self
.send_response(200)
238 self
.send_header('Content-type', mime
)
239 self
.send_header('Content-length', os
.path
.getsize(path
))
242 # Send the body of the file
244 shutil
.copyfileobj(handle
, self
.wfile
)
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
)):
258 client_ip
= self
.client_address
[0]
259 for allowedip
in allowed_clients
:
260 if client_ip
.startswith(allowedip
):
263 self
.send_response(404)
264 self
.send_header('Content-type', 'text/plain')
266 self
.wfile
.write("Unauthorized.")
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
)
277 for section
, settings
in tsnshares
:
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()
291 self
.send_response(200)
292 self
.send_header('Content-type', 'text/xml')
297 self
.send_response(200)
298 self
.send_header('Content-type', 'text/html')
300 t
= Template(file=os
.path
.join(SCRIPTDIR
, 'templates',
304 if config
.get_server('tivo_mak') and config
.get_server('togo_path'):
305 t
.togo
= '<br>Pull from TiVos:<br>'
309 if (config
.get_server('tivo_username') and
310 config
.get_server('tivo_password')):
311 t
.shares
= '<br>Push from video shares:<br>'
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&' +
319 'Container=' + quote(section
) +
320 '">Web Configuration</a><br>')
321 elif plugin_type
== 'togo' and t
.togo
:
322 for tsn
in config
.tivos
:
324 t
.togo
+= ('<a href="/TiVoConnect?' +
325 'Command=NPL&Container=' + quote(section
) +
326 '&TiVo=' + config
.tivos
[tsn
] + '">' +
327 escape(config
.tivo_names
[tsn
]) + '</a><br>')
328 elif ( plugin_type
== 'video' or plugin_type
== 'dvdvideo' ) \
330 t
.shares
+= ('<a href="TiVoConnect?Command=' +
331 'QueryContainer&Container=' +
332 quote(section
) + '">' + section
+ '</a><br>')
336 def unsupported(self
, query
):
337 self
.send_response(404)
338 self
.send_header('Content-type', 'text/html')
340 t
= Template(file=os
.path
.join(SCRIPTDIR
, 'templates',