1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """
23 RTSP - Real Time Streaming Protocol.
24
25 See RFC 2326, and its Robin, RFC 2068.
26 """
27
28 import sys
29 import re
30 import types
31
32
33 from twisted.web import server, resource, util
34 from twisted.internet import reactor, defer
35
36 from twisted.python import log, failure, reflect
37
38 from flumotion.twisted import http
39
40 try:
41 from twisted.protocols._c_urlarg import unquote
42 except ImportError:
43 from urllib import unquote
44
45 from flumotion.common import log as flog
46
47 SERVER_PROTOCOL = "RTSP/1.0"
48
49
50 SERVER_STRING = "Flumotion RTP"
51
52
53 CONTINUE = 100
54
55 OK = 200
56 CREATED = 201
57 LOW_STORAGE = 250
58
59 MULTIPLE_CHOICE = 300
60 MOVED_PERMANENTLY = 301
61 MOVED_TEMPORARILY = 302
62 SEE_OTHER = 303
63 NOT_MODIFIED = 304
64 USE_PROXY = 305
65
66 BAD_REQUEST = 400
67 UNAUTHORIZED = 401
68 PAYMENT_REQUIRED = 402
69 FORBIDDEN = 403
70 NOT_FOUND = 404
71 NOT_ALLOWED = 405
72 NOT_ACCEPTABLE = 406
73 PROXY_AUTH_REQUIRED = 407
74 REQUEST_TIMEOUT = 408
75 GONE = 410
76 LENGTH_REQUIRED = 411
77 PRECONDITION_FAILED = 412
78 REQUEST_ENTITY_TOO_LARGE = 413
79 REQUEST_URI_TOO_LONG = 414
80 UNSUPPORTED_MEDIA_TYPE = 415
81
82 PARAMETER_NOT_UNDERSTOOD = 451
83 CONFERENCE_NOT_FOUND = 452
84 NOT_ENOUGH_BANDWIDTH = 453
85 SESSION_NOT_FOUND = 454
86 METHOD_INVALID_STATE = 455
87 HEADER_FIELD_INVALID = 456
88 INVALID_RANGE = 457
89 PARAMETER_READ_ONLY = 458
90 AGGREGATE_NOT_ALLOWED = 459
91 AGGREGATE_ONLY_ALLOWED = 460
92 UNSUPPORTED_TRANSPORT = 461
93 DESTINATION_UNREACHABLE = 462
94
95 INTERNAL_SERVER_ERROR = 500
96 NOT_IMPLEMENTED = 501
97 BAD_GATEWAY = 502
98 SERVICE_UNAVAILABLE = 503
99 GATEWAY_TIMEOUT = 504
100 RTSP_VERSION_NOT_SUPPORTED = 505
101 OPTION_NOT_SUPPORTED = 551
102
103 RESPONSES = {
104
105 CONTINUE: "Continue",
106
107
108 OK: "OK",
109 CREATED: "Created",
110 LOW_STORAGE: "Low on Storage Space",
111
112
113 MULTIPLE_CHOICE: "Multiple Choices",
114 MOVED_PERMANENTLY: "Moved Permanently",
115 MOVED_TEMPORARILY: "Moved Temporarily",
116 SEE_OTHER: "See Other",
117 NOT_MODIFIED: "Not Modified",
118 USE_PROXY: "Use Proxy",
119
120
121 BAD_REQUEST: "Bad Request",
122 UNAUTHORIZED: "Unauthorized",
123 PAYMENT_REQUIRED: "Payment Required",
124 FORBIDDEN: "Forbidden",
125 NOT_FOUND: "Not Found",
126 NOT_ALLOWED: "Method Not Allowed",
127 NOT_ACCEPTABLE: "Not Acceptable",
128 PROXY_AUTH_REQUIRED: "Proxy Authentication Required",
129 REQUEST_TIMEOUT: "Request Time-out",
130 GONE: "Gone",
131 LENGTH_REQUIRED: "Length Required",
132 PRECONDITION_FAILED: "Precondition Failed",
133 REQUEST_ENTITY_TOO_LARGE: "Request Entity Too Large",
134 REQUEST_URI_TOO_LONG: "Request-URI Too Large",
135 UNSUPPORTED_MEDIA_TYPE: "Unsupported Media Type",
136
137 PARAMETER_NOT_UNDERSTOOD: "Parameter Not Understood",
138 CONFERENCE_NOT_FOUND: "Conference Not Found",
139 NOT_ENOUGH_BANDWIDTH: "Not Enough Bandwidth",
140 SESSION_NOT_FOUND: "Session Not Found",
141 METHOD_INVALID_STATE: "Method Not Valid In This State",
142 HEADER_FIELD_INVALID: "Header Field Not Valid for Resource",
143 INVALID_RANGE: "Invalid Range",
144 PARAMETER_READ_ONLY: "Parameter is Read-Only",
145 AGGREGATE_NOT_ALLOWED: "Aggregate operation not allowed",
146 AGGREGATE_ONLY_ALLOWED: "Only aggregate operation allowed",
147 UNSUPPORTED_TRANSPORT: "Unsupported transport",
148 DESTINATION_UNREACHABLE: "Destination unreachable",
149
150
151 INTERNAL_SERVER_ERROR: "Internal Server Error",
152 NOT_IMPLEMENTED: "Not Implemented",
153 BAD_GATEWAY: "Bad Gateway",
154 SERVICE_UNAVAILABLE: "Service Unavailable",
155 GATEWAY_TIMEOUT: "Gateway Time-out",
156 RTSP_VERSION_NOT_SUPPORTED: "RTSP Version not supported",
157 OPTION_NOT_SUPPORTED: "Option not supported",
158 }
159
161 """An exception with the RTSP status code and a str as arguments"""
162
164 logCategory = 'request'
165 code = OK
166 code_message = RESPONSES[OK]
167 host = None
168 port = None
169
171 if key.lower() in self.headers.keys():
172 del self.headers[key.lower()]
173
174
175
176
186
188
189 if self.clientproto != SERVER_PROTOCOL:
190 e = ErrorResource(BAD_REQUEST)
191 self.render(e)
192 return
193
194
195 first = "%s %s %s" % (self.method, self.path, SERVER_PROTOCOL)
196 self.debug('incoming request: %s' % first)
197
198 lines = []
199 for key, value in self.received_headers.items():
200 lines.append("%s: %s" % (key, value))
201
202 self.debug('incoming headers:\n%s\n' % "\n".join(lines))
203
204
205
206
207
208
209
210
211 site = self.channel.site
212 ip = self.getClientIP()
213 site.logRequest(ip, first, lines)
214
215 if not self._processPath():
216 return
217
218 try:
219 if self.path == "*":
220 resrc = site.resource
221 else:
222 resrc = site.getResourceFor(self)
223 self.debug("RTSPRequest.process(): got resource %r" % resrc)
224 try:
225 self.render(resrc)
226 except server.UnsupportedMethod:
227 e = ErrorResource(OPTION_NOT_SUPPORTED)
228 self.setHeader('Allow', ",".join(resrc.allowedMethods))
229 self.render(e)
230 except RTSPError, e:
231 er = ErrorResource(e.args[0])
232 self.render(er)
233 except Exception, e:
234 self.warning('failed to process %s: %s' %
235 (lines[0], flog.getExceptionMessage(e)))
236 self.processingFailed(failure.Failure())
237
239
240 self.log("path %s" % self.path)
241
242 self.prepath = []
243
244
245 if self.path == '*':
246 self.log('Request-URI is *')
247 return True
248
249
250 matcher = re.compile('rtspu?://([^/]*)')
251 m = matcher.match(self.path)
252 hostport = None
253 if m:
254 hostport = m.expand('\\1')
255
256 if not hostport:
257
258 self.log('Absolute rtsp URL required: %s' % self.path)
259 self.render(ErrorResource(BAD_REQUEST,
260 "Malformed Request-URI %s" % self.path))
261 return False
262
263
264 rest = self.path.split(hostport)[1]
265 self.host = hostport
266 if ':' in hostport:
267 chunks = hostport.split(':')
268 self.host = chunks[0]
269 self.port = int(chunks[1])
270
271
272 self.postpath = map(unquote, rest.split('/'))
273 self.log('split up self.path in host %s, port %r, pre %r and post %r' % (
274 self.host, self.port, self.prepath, self.postpath))
275 return True
276
278 self.warningFailure(reason)
279
280 if not True:
281 self.debug('sending traceback to client')
282 import traceback
283 tb = sys.exc_info()[2]
284 text = "".join(traceback.format_exception(
285 reason.type, reason.value, tb))
286 else:
287 text = "RTSP server failed to process your request.\n"
288
289 self.setResponseCode(INTERNAL_SERVER_ERROR)
290 self.setHeader('Content-Type', "text/plain")
291 self.setHeader('Content-Length', str(len(text)))
292 self.write(text)
293 self.finish()
294 return reason
295
296 - def _error(self, code, *lines):
297 self.setResponseCode(code)
298 self.setHeader('content-type', "text/plain")
299 body = "\n".join(lines)
300 return body
301
303 self.log('%r.render(%r)' % (resrc, self))
304 result = resrc.render(self)
305 self.log('%r.render(%r) returned result %r' % (resrc, self, result))
306
307
308 if isinstance(result, defer.Deferred):
309 result.addCallback(self._renderCallback, resrc)
310 else:
311 self._renderCallback(result, resrc)
312
313
315 body = result
316 if type(body) is not types.StringType:
317 self.warning('request did not return a string but %r' %
318 type(body))
319 body = self._error(INTERNAL_SERVER_ERROR,
320 "Request did not return a string",
321 "Request: " + reflect.safe_repr(self),
322 "Resource: " + reflect.safe_repr(resrc),
323 "Value: " + reflect.safe_repr(body))
324 self.setHeader('Content-Length', str(len(body)))
325
326 lines = []
327 for key, value in self.headers.items():
328 lines.append("%s: %s" % (key, value))
329
330 self.debug('responding to %s %s with %s (%d)' % (
331 self.method, self.path, self.code_message, self.code))
332 self.debug('outgoing headers:\n%s\n' % "\n".join(lines))
333 if body:
334 self.debug('body:\n%s\n' % body)
335 self.log('RTSPRequest._renderCallback(): outgoing response:\n%s\n' %
336 "\n".join(lines))
337 self.log("\n".join(lines))
338 self.log("\n")
339 self.log(body)
340
341 self.channel.site.logReply(self.code, self.code_message, lines, body)
342
343 self.write(body)
344 self.finish()
345
346
347
357
358
359
360
361
363 """
364 I am a ServerFactory that can be used in
365 L{twisted.internet.interfaces.IReactorTCP.listenTCP}
366 Create me with an L{RTSPSiteResource} object.
367 """
368 protocol = RTSPChannel
369 requestFactory = RTSPRequest
370
371 - def logRequest(self, ip, requestLine, headerLines):
373 - def logReply(self, code, message, headerLines, body):
375
377 """
378 I am a base class for all RTSP Resource classes.
379
380 @type allowedMethods: tuple
381 @ivar allowedMethods: a tuple of allowed methods that can be invoked
382 on this resource.
383 """
384
385 logCategory = 'resource'
386 allowedMethods = ['OPTIONS']
387
389 return NoResource()
390
391 self.log('RTSPResource.getChild(%r, %s, <request>), pre %r, post %r' % (
392 self, path, request.prepath, request.postpath))
393 res = resource.Resource.getChild(self, path, request)
394 self.log('RTSPResource.getChild(%r, %s, <request>) returns %r' % (
395 self, path, res))
396 return res
397
399 self.log('RTSPResource.getChildWithDefault(%r, %s, <request>), pre %r, post %r' % (
400 self, path, request.prepath, request.postpath))
401 self.log('children: %r' % self.children.keys())
402 res = resource.Resource.getChildWithDefault(self, path, request)
403 self.log('RTSPResource.getChildWithDefault(%r, %s, <request>) returns %r' % (
404 self, path, res))
405 return res
406
407
409 self.log('RTSPResource.putChild(%r, %s, %r)' % (self, path, r))
410 return resource.Resource.putChild(self, path, r)
411
412
413
415 """
416 Set CSeq and Date on response to given request.
417 This should be done even for errors.
418 """
419 cseq = request.getHeader('CSeq')
420
421
422 if cseq == None:
423 cseq = 0
424 request.setHeader('CSeq', cseq)
425 request.setHeader('Date', http.datetimeToString())
426
428 ip = request.getClientIP()
429 self.log('RTSPResource.render_start(): client from %s requests %s' % (
430 ip, method))
431 self.log('RTSPResource.render_start(): uri %r' % request.path)
432
433 self.render_startCSeqDate(request, method)
434 request.setHeader('Server', SERVER_STRING)
435 request.delHeader('Content-Type')
436
437
438 request.setHeader('Last-Modified', http.datetimeToString())
439 request.setHeader('Cache-Control', 'must-revalidate')
440
441
442
443
444
445
446 if 'Real' in request.received_headers.get('user-agent', ''):
447 self.debug('Detected Real client, sending specific headers')
448
449
450
451 request.setHeader('Public', 'OPTIONS, DESCRIBE, ANNOUNCE, PLAY, SETUP, TEARDOWN')
452
453 request.setHeader('RealChallenge1', '28d49444034696e1d523f2819b8dcf4c')
454
455
457
458 raise NotImplementedError
459
462 resource.Resource.__init__(self)
463 self.code = code
464 self.body = ""
465 if lines != (None, ):
466 self.body = "\n".join(lines) + "\n\n"
467
468
469 if not hasattr(self, 'method'):
470 self.method = 'GET'
471
479
481
482 raise NotImplementedError
483
486
490