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
, GetPluginPath
, 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 RE_PLUGIN_CONTENT
= re
.compile(r
'/plugin/([^/]+)/content/(.+)')
26 class TivoHTTPServer(SocketServer
.ThreadingMixIn
, BaseHTTPServer
.HTTPServer
):
29 def __init__(self
, server_address
, RequestHandlerClass
):
30 BaseHTTPServer
.HTTPServer
.__init
__(self
, server_address
,
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"
39 self
.containers
[name
] = settings
41 self
.logger
.error('Unable to add container ' + name
)
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' %
52 def set_beacon(self
, 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]
66 tsn
= self
.headers
.getheader('TiVo_TCD_ID',
67 self
.headers
.getheader('tsn', ''))
68 if not self
.authorize(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
)
78 path
, opts
= self
.path
.split('?', 1)
79 query
= cgi
.parse_qs(opts
)
84 regm
= RE_PLUGIN_CONTENT
.match(path
)
86 if path
== '/TiVoConnect':
87 self
.handle_query(query
, tsn
)
89 self
.handle_plugin_content(regm
)
92 path
= unquote_plus(path
)
93 basepath
= path
.split('/')[1]
94 for name
, container
in self
.server
.containers
.items():
96 path
= os
.path
.join(os
.path
.normpath(container
['path']),
97 os
.path
.normpath(path
[len(name
) + 2:]))
98 plugin
= GetPlugin(container
['type'])
99 plugin
.send_file(self
, path
, query
)
102 ## Not a file not a TiVo command
106 tsn
= self
.headers
.getheader('TiVo_TCD_ID',
107 self
.headers
.getheader('tsn', ''))
108 if not self
.authorize(tsn
):
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
)
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
):
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()
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
):
136 plugin
= GetPlugin(container
['type'])
137 if hasattr(plugin
, command
):
138 method
= getattr(plugin
, command
)
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')
149 self
.wfile
.write(VIDEO_FORMATS
)
152 elif command
== 'FlushServer':
153 # Does nothing -- included for completeness
154 self
.send_response(200)
158 # If we made it here it means we couldn't match the request to
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>
166 # Protect ourself from path exploits
167 file_bits
= regm
.group(2).split('/')
168 for bit
in file_bits
:
172 # Get the plugin path
173 plugin_path
= GetPluginPath(regm
.group(1))
175 # Build up the actual path based on the plugin path
176 filen
= os
.path
.join(plugin_path
, 'content', *file_bits
)
178 # If it's not a file, then just error out
179 if not os
.path
.isfile(filen
):
182 # Read in the full file
183 handle
= open(filen
, 'rb')
192 mime
= mimetypes
.guess_type(filen
)[0]
193 self
.send_response(200)
195 self
.send_header('Content-type', mime
)
196 self
.send_header('Content-length', os
.path
.getsize(filen
))
199 # Send the body of the file
200 self
.wfile
.write(text
)
202 self
.send_response(404)
204 self
.wfile
.write('File not found')
206 def authorize(self
, tsn
=None):
207 # if allowed_clients is empty, we are completely open
208 allowed_clients
= config
.getAllowedClients()
209 if not allowed_clients
or (tsn
and config
.isTsnInConfig(tsn
)):
211 client_ip
= self
.client_address
[0]
212 for allowedip
in allowed_clients
:
213 if client_ip
.startswith(allowedip
):
216 self
.send_response(404)
217 self
.send_header('Content-type', 'text/plain')
219 self
.wfile
.write("Unauthorized.")
222 def log_message(self
, format
, *args
):
223 self
.server
.logger
.info("%s [%s] %s" % (self
.address_string(),
224 self
.log_date_time_string(), format
%args
))
226 def root_container(self
):
227 tsn
= self
.headers
.getheader('TiVo_TCD_ID', '')
228 tsnshares
= config
.getShares(tsn
)
230 for section
, settings
in tsnshares
:
232 settings
['content_type'] = \
233 GetPlugin(settings
['type']).CONTENT_TYPE
234 tsncontainers
.append((section
, settings
))
235 except Exception, msg
:
236 self
.server
.logger
.error(section
+ ' - ' + str(msg
))
237 t
= Template(file=os
.path
.join(SCRIPTDIR
, 'templates',
238 'root_container.tmpl'),
239 filter=EncodeUnicode
)
240 t
.containers
= tsncontainers
241 t
.hostname
= socket
.gethostname()
244 self
.send_response(200)
245 self
.send_header('Content-type', 'text/xml')
250 self
.send_response(200)
251 self
.send_header('Content-type', 'text/html')
253 t
= Template(file=os
.path
.join(SCRIPTDIR
, 'templates',
257 if config
.get_server('tivo_mak') and config
.get_server('togo_path'):
258 t
.togo
= '<br>Pull from TiVos:<br>'
262 if (config
.get_server('tivo_username') and
263 config
.get_server('tivo_password')):
264 t
.shares
= '<br>Push from video shares:<br>'
268 for section
, settings
in config
.getShares():
269 plugin_type
= settings
.get('type')
270 if plugin_type
== 'settings':
271 t
.admin
+= ('<a href="/TiVoConnect?Command=Settings&' +
272 'Container=' + quote(section
) +
273 '">Web Configuration</a><br>')
274 elif plugin_type
== 'togo' and t
.togo
:
275 for tsn
in config
.tivos
:
277 t
.togo
+= ('<a href="/TiVoConnect?' +
278 'Command=NPL&Container=' + quote(section
) +
279 '&TiVo=' + config
.tivos
[tsn
] + '">' +
280 config
.tivo_names
[tsn
] + '</a><br>')
281 elif plugin_type
== 'video' and t
.shares
:
282 t
.shares
+= ('<a href="TiVoConnect?Command=' +
283 'QueryContainer&Container=' +
284 quote(section
) + '">' + section
+ '</a><br>')
288 def unsupported(self
, query
):
289 self
.send_response(404)
290 self
.send_header('Content-type', 'text/html')
292 t
= Template(file=os
.path
.join(SCRIPTDIR
, 'templates',