9 from urllib
import unquote_plus
, quote
10 from xml
.sax
.saxutils
import escape
12 from Cheetah
.Template
import Template
14 from plugin
import GetPlugin
, EncodeUnicode
16 SCRIPTDIR
= os
.path
.dirname(__file__
)
18 VIDEO_FORMATS
= """<?xml version="1.0" encoding="utf-8"?>
20 <ContentType>video/x-tivo-mpeg</ContentType><Description/>
21 </Format></TiVoFormats>"""
23 class TivoHTTPServer(SocketServer
.ThreadingMixIn
, BaseHTTPServer
.HTTPServer
):
26 def __init__(self
, server_address
, RequestHandlerClass
):
27 BaseHTTPServer
.HTTPServer
.__init
__(self
, server_address
,
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"
36 self
.containers
[name
] = settings
38 self
.logger
.error('Unable to add container ' + name
)
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' %
49 def set_beacon(self
, 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]
63 tsn
= self
.headers
.getheader('TiVo_TCD_ID',
64 self
.headers
.getheader('tsn', ''))
65 if not self
.authorize(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
)
75 path
, opts
= self
.path
.split('?', 1)
76 query
= cgi
.parse_qs(opts
)
81 if path
== '/TiVoConnect':
82 self
.handle_query(query
, tsn
)
85 splitpath
= [x
for x
in unquote_plus(path
).split('/') if x
]
87 self
.handle_file(query
, splitpath
)
89 ## Not a file not a TiVo command
93 tsn
= self
.headers
.getheader('TiVo_TCD_ID',
94 self
.headers
.getheader('tsn', ''))
95 if not self
.authorize(tsn
):
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
)
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
):
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()
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
):
123 plugin
= GetPlugin(container
['type'])
124 if hasattr(plugin
, command
):
125 method
= getattr(plugin
, command
)
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')
136 self
.wfile
.write(VIDEO_FORMATS
)
139 elif command
== 'FlushServer':
140 # Does nothing -- included for completeness
141 self
.send_response(200)
145 # If we made it here it means we couldn't match the request to
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
)
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
167 handle
= open(path
, 'rb')
175 mime
= mimetypes
.guess_type(path
)[0]
176 self
.send_response(200)
178 self
.send_header('Content-type', mime
)
179 self
.send_header('Content-length', os
.path
.getsize(path
))
182 # Send the body of the file
183 self
.wfile
.write(text
)
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
)):
194 client_ip
= self
.client_address
[0]
195 for allowedip
in allowed_clients
:
196 if client_ip
.startswith(allowedip
):
199 self
.send_response(404)
200 self
.send_header('Content-type', 'text/plain')
202 self
.wfile
.write("Unauthorized.")
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
)
213 for section
, settings
in tsnshares
:
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()
227 self
.send_response(200)
228 self
.send_header('Content-type', 'text/xml')
233 self
.send_response(200)
234 self
.send_header('Content-type', 'text/html')
236 t
= Template(file=os
.path
.join(SCRIPTDIR
, 'templates',
240 if config
.get_server('tivo_mak') and config
.get_server('togo_path'):
241 t
.togo
= '<br>Pull from TiVos:<br>'
245 if (config
.get_server('tivo_username') and
246 config
.get_server('tivo_password')):
247 t
.shares
= '<br>Push from video shares:<br>'
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&' +
255 'Container=' + quote(section
) +
256 '">Web Configuration</a><br>')
257 elif plugin_type
== 'togo' and t
.togo
:
258 for tsn
in config
.tivos
:
260 t
.togo
+= ('<a href="/TiVoConnect?' +
261 'Command=NPL&Container=' + quote(section
) +
262 '&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&Container=' +
267 quote(section
) + '">' + section
+ '</a><br>')
271 def unsupported(self
, query
):
272 self
.send_response(404)
273 self
.send_header('Content-type', 'text/html')
275 t
= Template(file=os
.path
.join(SCRIPTDIR
, 'templates',