10 from urllib
import unquote_plus
, quote
11 from xml
.sax
.saxutils
import escape
13 from Cheetah
.Template
import Template
15 from plugin
import GetPlugin
, EncodeUnicode
17 SCRIPTDIR
= os
.path
.dirname(__file__
)
19 VIDEO_FORMATS
= """<?xml version="1.0" encoding="utf-8"?>
21 <ContentType>video/x-tivo-mpeg</ContentType><Description/>
22 </Format></TiVoFormats>"""
24 BASE_HTML
= """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
25 "http://www.w3.org/TR/html4/strict.dtd">
26 <html> <head><title>pyTivo</title></head> <body> %s </body> </html>"""
28 RELOAD
= '<p>The <a href="%s">page</a> will reload in %d seconds.</p>'
29 UNSUP
= '<h3>Unsupported Command</h3> <p>Query:</p> <ul>%s</ul>'
31 class TivoHTTPServer(SocketServer
.ThreadingMixIn
, BaseHTTPServer
.HTTPServer
):
34 def __init__(self
, server_address
, RequestHandlerClass
):
35 BaseHTTPServer
.HTTPServer
.__init
__(self
, server_address
,
37 self
.daemon_threads
= True
38 self
.logger
= logging
.getLogger('pyTivo')
40 def add_container(self
, name
, settings
):
41 if name
in self
.containers
or name
== 'TiVoConnect':
42 raise "Container Name in use"
44 self
.containers
[name
] = settings
46 self
.logger
.error('Unable to add container ' + name
)
49 self
.containers
.clear()
50 for section
, settings
in config
.getShares():
51 self
.add_container(section
, settings
)
53 def handle_error(self
, request
, client_address
):
54 self
.logger
.exception('Exception during request from %s' %
57 def set_beacon(self
, beacon
):
60 class TivoHTTPHandler(BaseHTTPServer
.BaseHTTPRequestHandler
):
61 def __init__(self
, request
, client_address
, server
):
62 self
.wbufsize
= 0x10000
63 BaseHTTPServer
.BaseHTTPRequestHandler
.__init
__(self
, request
,
64 client_address
, server
)
66 def address_string(self
):
67 host
, port
= self
.client_address
[:2]
71 tsn
= self
.headers
.getheader('TiVo_TCD_ID',
72 self
.headers
.getheader('tsn', ''))
73 if not self
.authorize(tsn
):
76 ip
= self
.address_string()
77 config
.tivos
[tsn
] = ip
79 if not tsn
in config
.tivo_names
or config
.tivo_names
[tsn
] == tsn
:
80 config
.tivo_names
[tsn
] = self
.server
.beacon
.get_name(ip
)
83 path
, opts
= self
.path
.split('?', 1)
84 query
= cgi
.parse_qs(opts
)
89 if path
== '/TiVoConnect':
90 self
.handle_query(query
, tsn
)
93 splitpath
= [x
for x
in unquote_plus(path
).split('/') if x
]
95 self
.handle_file(query
, splitpath
)
97 ## Not a file not a TiVo command
101 tsn
= self
.headers
.getheader('TiVo_TCD_ID',
102 self
.headers
.getheader('tsn', ''))
103 if not self
.authorize(tsn
):
105 ctype
, pdict
= cgi
.parse_header(self
.headers
.getheader('content-type'))
106 if ctype
== 'multipart/form-data':
107 query
= cgi
.parse_multipart(self
.rfile
, pdict
)
109 length
= int(self
.headers
.getheader('content-length'))
110 qs
= self
.rfile
.read(length
)
111 query
= cgi
.parse_qs(qs
, keep_blank_values
=1)
112 self
.handle_query(query
, tsn
)
114 def handle_query(self
, query
, tsn
):
116 if 'Command' in query
and len(query
['Command']) >= 1:
118 command
= query
['Command'][0]
120 # If we are looking at the root container
121 if (command
== 'QueryContainer' and
122 (not 'Container' in query
or query
['Container'][0] == '/')):
123 self
.root_container()
126 if 'Container' in query
:
127 # Dispatch to the container plugin
128 basepath
= query
['Container'][0].split('/')[0]
129 for name
, container
in config
.getShares(tsn
):
131 plugin
= GetPlugin(container
['type'])
132 if hasattr(plugin
, command
):
133 method
= getattr(plugin
, command
)
139 elif (command
== 'QueryFormats' and 'SourceFormat' in query
and
140 query
['SourceFormat'][0].startswith('video')):
141 self
.send_response(200)
142 self
.send_header('Content-type', 'text/xml')
144 self
.wfile
.write(VIDEO_FORMATS
)
147 elif command
== 'FlushServer':
148 # Does nothing -- included for completeness
149 self
.send_response(200)
153 # If we made it here it means we couldn't match the request to
155 self
.unsupported(query
)
157 def handle_file(self
, query
, splitpath
):
158 if '..' not in splitpath
: # Protect against path exploits
159 ## Pass it off to a plugin?
160 for name
, container
in self
.server
.containers
.items():
161 if splitpath
[0] == name
:
162 base
= os
.path
.normpath(container
['path'])
163 path
= os
.path
.join(base
, *splitpath
[1:])
164 plugin
= GetPlugin(container
['type'])
165 plugin
.send_file(self
, path
, query
)
168 ## Serve it from a "content" directory?
169 base
= os
.path
.join(SCRIPTDIR
, *splitpath
[:-1])
170 path
= os
.path
.join(base
, 'content', splitpath
[-1])
172 if os
.path
.isfile(path
):
174 handle
= open(path
, 'rb')
180 mime
= mimetypes
.guess_type(path
)[0]
181 self
.send_response(200)
183 self
.send_header('Content-type', mime
)
184 self
.send_header('Content-length', os
.path
.getsize(path
))
187 # Send the body of the file
189 shutil
.copyfileobj(handle
, self
.wfile
)
198 def authorize(self
, tsn
=None):
199 # if allowed_clients is empty, we are completely open
200 allowed_clients
= config
.getAllowedClients()
201 if not allowed_clients
or (tsn
and config
.isTsnInConfig(tsn
)):
203 client_ip
= self
.client_address
[0]
204 for allowedip
in allowed_clients
:
205 if client_ip
.startswith(allowedip
):
208 self
.send_response(404)
209 self
.send_header('Content-type', 'text/plain')
211 self
.wfile
.write("Unauthorized.")
214 def log_message(self
, format
, *args
):
215 self
.server
.logger
.info("%s [%s] %s" % (self
.address_string(),
216 self
.log_date_time_string(), format
%args
))
218 def root_container(self
):
219 tsn
= self
.headers
.getheader('TiVo_TCD_ID', '')
220 tsnshares
= config
.getShares(tsn
)
222 for section
, settings
in tsnshares
:
224 settings
['content_type'] = \
225 GetPlugin(settings
['type']).CONTENT_TYPE
226 tsncontainers
.append((section
, settings
))
227 except Exception, msg
:
228 self
.server
.logger
.error(section
+ ' - ' + str(msg
))
229 t
= Template(file=os
.path
.join(SCRIPTDIR
, 'templates',
230 'root_container.tmpl'),
231 filter=EncodeUnicode
)
232 t
.containers
= tsncontainers
233 t
.hostname
= socket
.gethostname()
236 self
.send_response(200)
237 self
.send_header('Content-type', 'text/xml')
242 useragent
= self
.headers
.getheader('User-Agent', '')
243 self
.send_response(200)
244 self
.send_header('Content-type', 'text/html; charset=utf-8')
246 if useragent
.lower().find('mobile') > 0:
247 t
= Template(file=os
.path
.join(SCRIPTDIR
, 'templates',
248 'info_page_mob.tmpl'),
249 filter=EncodeUnicode
)
251 t
= Template(file=os
.path
.join(SCRIPTDIR
, 'templates',
253 filter=EncodeUnicode
)
256 if config
.get_server('tivo_mak') and config
.get_server('togo_path'):
257 t
.togo
= 'Pull from TiVos:<br>'
261 if (config
.get_server('tivo_username') and
262 config
.get_server('tivo_password')):
263 t
.shares
= 'Push from video shares:<br>'
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&' +
271 'Container=' + quote(section
) +
272 '">Web Configuration</a><br>')
273 elif plugin_type
== 'togo' and t
.togo
:
274 for tsn
in config
.tivos
:
276 t
.togo
+= ('<a href="/TiVoConnect?' +
277 'Command=NPL&Container=' + quote(section
) +
278 '&TiVo=' + config
.tivos
[tsn
] + '">' +
279 escape(config
.tivo_names
[tsn
]) + '</a><br>')
280 elif ( plugin_type
== 'video' or plugin_type
== 'dvdvideo' ) \
282 t
.shares
+= ('<a href="TiVoConnect?Command=' +
283 'QueryContainer&Container=' +
284 quote(section
) + '">' + section
+ '</a><br>')
288 def unsupported(self
, query
):
289 message
= UNSUP
% '\n'.join(['<li>%s: %s</li>' % (escape(key
),
291 for key
, value
in query
.items()])
292 text
= BASE_HTML
% message
293 self
.send_response(404)
294 self
.send_header('Content-Type', 'text/html; charset=utf-8')
295 self
.send_header('Content-Length', len(text
))
297 self
.wfile
.write(text
)
299 def redir(self
, message
, seconds
=2):
300 url
= self
.headers
.getheader('Referer')
302 message
+= RELOAD
% (escape(url
), seconds
)
303 text
= (BASE_HTML
% message
).encode('utf-8')
304 self
.send_response(200)
305 self
.send_header('Content-Type', 'text/html; charset=utf-8')
306 self
.send_header('Content-Length', len(text
))
307 self
.send_header('Expires', '0')
309 self
.send_header('Refresh', '%d; url=%s' % (seconds
, url
))
311 self
.wfile
.write(text
)