Package flumotion :: Package twisted :: Module rtsp
[hide private]

Source Code for Module flumotion.twisted.rtsp

  1  # -*- Mode: Python; test-case-name: flumotion.test.test_rtsp -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3  # 
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007 Fluendo, S.L. (www.fluendo.com). 
  6  # All rights reserved. 
  7   
  8  # This file may be distributed and/or modified under the terms of 
  9  # the GNU General Public License version 2 as published by 
 10  # the Free Software Foundation. 
 11  # This file is distributed without any warranty; without even the implied 
 12  # warranty of merchantability or fitness for a particular purpose. 
 13  # See "LICENSE.GPL" in the source distribution for more information. 
 14   
 15  # Licensees having purchased or holding a valid Flumotion Advanced 
 16  # Streaming Server license may use this file in accordance with the 
 17  # Flumotion Advanced Streaming Server Commercial License Agreement. 
 18  # See "LICENSE.Flumotion" in the source distribution for more information. 
 19   
 20  # Headers in this file shall remain intact. 
 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  from twisted.web import http 
 33  from twisted.web import server, resource 
 34  from twisted.internet import defer 
 35   
 36  from twisted.python import log, failure, reflect 
 37   
 38  try: 
 39      from twisted.protocols._c_urlarg import unquote 
 40  except ImportError: 
 41      from urllib import unquote 
 42   
 43  from flumotion.common import log as flog 
 44   
 45  __version__ = "$Rev: 6638 $" 
 46   
 47  SERVER_PROTOCOL = "RTSP/1.0" 
 48  # I can be overridden to add the version 
 49   
 50  SERVER_STRING = "Flumotion RTP" 
 51   
 52  # response codes 
 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      # 100 
105      CONTINUE:                   "Continue", 
106   
107      # 200 
108      OK:                         "OK", 
109      CREATED:                    "Created", 
110      LOW_STORAGE:                "Low on Storage Space", 
111   
112      # 300 
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      # 400 
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      # 500 
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   
160 -class RTSPError(Exception):
161 """An exception with the RTSP status code and a str as arguments"""
162
163 -class RTSPRequest(http.Request, flog.Loggable):
164 logCategory = 'request' 165 code = OK 166 code_message = RESPONSES[OK] 167 host = None 168 port = None 169
170 - def delHeader(self, key):
171 if key.lower() in self.headers.keys(): 172 del self.headers[key.lower()]
173 174 # base method override 175 176 # copied from HTTP since we have our own set of RESPONSES
177 - def setResponseCode(self, code, message=None):
178 """ 179 Set the RTSP response code. 180 """ 181 self.code = code 182 if message: 183 self.code_message = message 184 else: 185 self.code_message = RESPONSES.get(code, "Unknown Status")
186
187 - def process(self):
188 # First check that we have a valid request. 189 if self.clientproto != SERVER_PROTOCOL: 190 e = ErrorResource(BAD_REQUEST) 191 self.render(e) 192 return 193 194 # process the request and render the resource or give a failure 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 #self.debug('user-agent: %s' % self.received_headers.get('user-agent', 205 # '[Unknown]')) 206 #self.debug('clientid: %s' % self.received_headers.get('clientid', 207 # '[Unknown]')) 208 209 # don't store site locally; we can't be sure every request has gone 210 # through our customized handlers 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 and lines[0] or "[No headers]", 236 flog.getExceptionMessage(e))) 237 self.processingFailed(failure.Failure())
238
239 - def _processPath(self):
240 # process self.path into components; return whether or not it worked 241 self.log("path %s" % self.path) 242 243 self.prepath = [] # used in getResourceFor 244 245 # check Request-URI; RFC 2326 6.1 says it's "*" or absolute URI 246 if self.path == '*': 247 self.log('Request-URI is *') 248 return True 249 250 # match the host:port 251 matcher = re.compile('rtspu?://([^/]*)') 252 m = matcher.match(self.path) 253 hostport = None 254 if m: 255 hostport = m.expand('\\1') 256 257 if not hostport: 258 # malformed Request-URI; 400 seems like a likely reply ? 259 self.log('Absolute rtsp URL required: %s' % self.path) 260 self.render(ErrorResource(BAD_REQUEST, 261 "Malformed Request-URI %s" % self.path)) 262 return False 263 264 # get the rest after hostport starting with '/' 265 rest = self.path.split(hostport)[1] 266 self.host = hostport 267 if ':' in hostport: 268 chunks = hostport.split(':') 269 self.host = chunks[0] 270 self.port = int(chunks[1]) 271 # if we got fed crap, they're in other chunks, and we ignore them 272 273 self.postpath = map(unquote, rest.split('/')) 274 self.log('split up self.path in host %s, port %r, pre %r and post %r' % ( 275 self.host, self.port, self.prepath, self.postpath)) 276 return True
277
278 - def processingFailed(self, reason):
279 self.warningFailure(reason) 280 # FIXME: disable tracebacks until we can reliably disable them 281 if not True: # self.site or self.site.displayTracebacks: 282 self.debug('sending traceback to client') 283 import traceback 284 tb = sys.exc_info()[2] 285 text = "".join(traceback.format_exception( 286 reason.type, reason.value, tb)) 287 else: 288 text = "RTSP server failed to process your request.\n" 289 290 self.setResponseCode(INTERNAL_SERVER_ERROR) 291 self.setHeader('Content-Type', "text/plain") 292 self.setHeader('Content-Length', str(len(text))) 293 self.write(text) 294 self.finish() 295 return reason
296
297 - def _error(self, code, *lines):
298 self.setResponseCode(code) 299 self.setHeader('content-type', "text/plain") 300 body = "\n".join(lines) 301 return body
302
303 - def render(self, resrc):
304 self.log('%r.render(%r)' % (resrc, self)) 305 result = resrc.render(self) 306 self.log('%r.render(%r) returned result %r' % (resrc, self, result)) 307 if isinstance(result, defer.Deferred): 308 result.addCallback(self._renderCallback, resrc) 309 result.addErrback(self._renderErrback, resrc) 310 else: 311 self._renderCallback(result, resrc)
312 313 # TODO: Refactor this and renderCallback to be cleaner and share code.
314 - def _renderErrback(self, failure, resrc):
315 body = self._error(INTERNAL_SERVER_ERROR, 316 "Request failed: %r" % failure) 317 self.setHeader('Content-Length', str(len(body))) 318 lines = [] 319 for key, value in self.headers.items(): 320 lines.append("%s: %s" % (key, value)) 321 322 self.channel.site.logReply(self.code, self.code_message, lines, body) 323 324 self.write(body) 325 self.finish()
326
327 - def _renderCallback(self, result, resrc):
328 body = result 329 if type(body) is not types.StringType: 330 self.warning('request did not return a string but %r' % 331 type(body)) 332 body = self._error(INTERNAL_SERVER_ERROR, 333 "Request did not return a string", 334 "Request: " + reflect.safe_repr(self), 335 "Resource: " + reflect.safe_repr(resrc), 336 "Value: " + reflect.safe_repr(body)) 337 self.setHeader('Content-Length', str(len(body))) 338 339 lines = [] 340 for key, value in self.headers.items(): 341 lines.append("%s: %s" % (key, value)) 342 # FIXME: debug response code 343 self.debug('responding to %s %s with %s (%d)' % ( 344 self.method, self.path, self.code_message, self.code)) 345 self.debug('outgoing headers:\n%s\n' % "\n".join(lines)) 346 if body: 347 self.debug('body:\n%s\n' % body) 348 self.log('RTSPRequest._renderCallback(): outgoing response:\n%s\n' % 349 "\n".join(lines)) 350 self.log("\n".join(lines)) 351 self.log("\n") 352 self.log(body) 353 354 self.channel.site.logReply(self.code, self.code_message, lines, body) 355 356 self.write(body) 357 self.finish()
358 359 # RTSP keeps the initial request alive, pinging it regularly. 360 # for now we just keep it persistent for ever
361 -class RTSPChannel(http.HTTPChannel):
362 363 requestFactory = RTSPRequest 364
365 - def checkPersistence(self, request, version):
366 if version == SERVER_PROTOCOL: 367 return 1 368 log.err('version %s not handled' % version) 369 return 0
370 371 #class RTSPFactory(http.HTTPFactory): 372 # protocol = RTSPChannel 373 # timeout = 60 374
375 -class RTSPSite(server.Site):
376 """ 377 I am a ServerFactory that can be used in 378 L{twisted.internet.interfaces.IReactorTCP}'s .listenTCP 379 Create me with an L{RTSPResource} object. 380 """ 381 protocol = RTSPChannel 382 requestFactory = RTSPRequest 383
384 - def logRequest(self, ip, requestLine, headerLines):
385 pass
386 - def logReply(self, code, message, headerLines, body):
387 pass
388
389 -class RTSPResource(resource.Resource, flog.Loggable):
390 """ 391 I am a base class for all RTSP Resource classes. 392 393 @type allowedMethods: tuple 394 @ivar allowedMethods: a tuple of allowed methods that can be invoked 395 on this resource. 396 """ 397 398 logCategory = 'resource' 399 allowedMethods = ['OPTIONS'] 400
401 - def getChild(self, path, request):
402 return NoResource() 403 # use WithDefault so static children have a chance too 404 self.log('RTSPResource.getChild(%r, %s, <request>), pre %r, post %r' % ( 405 self, path, request.prepath, request.postpath)) 406 res = resource.Resource.getChild(self, path, request) 407 self.log('RTSPResource.getChild(%r, %s, <request>) returns %r' % ( 408 self, path, res)) 409 return res
410
411 - def getChildWithDefault(self, path, request):
412 self.log('RTSPResource.getChildWithDefault(%r, %s, <request>), pre %r, post %r' % ( 413 self, path, request.prepath, request.postpath)) 414 self.log('children: %r' % self.children.keys()) 415 res = resource.Resource.getChildWithDefault(self, path, request) 416 self.log('RTSPResource.getChildWithDefault(%r, %s, <request>) returns %r' % ( 417 self, path, res)) 418 return res
419 420 # FIXME: remove
421 - def noputChild(self, path, r):
422 self.log('RTSPResource.putChild(%r, %s, %r)' % (self, path, r)) 423 return resource.Resource.putChild(self, path, r)
424 425 # needs to be done for ALL responses 426 # see 12.17 CSeq and H14.19 Date
427 - def render_startCSeqDate(self, request, method):
428 """ 429 Set CSeq and Date on response to given request. 430 This should be done even for errors. 431 """ 432 self.log('render_startCSeqDate, method %r' % method) 433 cseq = request.getHeader('CSeq') 434 # RFC says clients MUST have CSeq field, but we're lenient 435 # in what we accept and assume 0 if not specified 436 if cseq == None: 437 cseq = 0 438 request.setHeader('CSeq', cseq) 439 request.setHeader('Date', http.datetimeToString())
440
441 - def render_start(self, request, method):
442 ip = request.getClientIP() 443 self.log('RTSPResource.render_start(): client from %s requests %s' % ( 444 ip, method)) 445 self.log('RTSPResource.render_start(): uri %r' % request.path) 446 447 self.render_startCSeqDate(request, method) 448 request.setHeader('Server', SERVER_STRING) 449 request.delHeader('Content-Type') 450 451 # tests for 3gpp 452 request.setHeader('Last-Modified', http.datetimeToString()) 453 request.setHeader('Cache-Control', 'must-revalidate') 454 #request.setHeader('x-Accept-Retransmit', 'our-revalidate') 455 #request.setHeader('x-Accept-Dynamic-Rate', '1') 456 #request.setHeader('Content-Base', 'rtsp://core.fluendo.com/test.3gpp') 457 #request.setHeader('Via', 'RTSP/1.0 288f9c2a') 458 459 # hacks for Real 460 if 'Real' in request.received_headers.get('user-agent', ''): 461 self.debug('Detected Real client, sending specific headers') 462 # request.setHeader('Public', 'OPTIONS, DESCRIBE, ANNOUNCE, PLAY, SETUP, GET_PARAMETER, SET_PARAMETER, TEARDOWN') 463 # Public seems to be the same as allowed-methods, and real clients 464 # seem to respect SET_PARAMETER not listed here 465 request.setHeader('Public', 'OPTIONS, DESCRIBE, ANNOUNCE, PLAY, SETUP, TEARDOWN') 466 # without a RealChallenge1, clients don't even go past OPTIONS 467 request.setHeader('RealChallenge1', '28d49444034696e1d523f2819b8dcf4c')
468 #request.setHeader('StatsMask', '3') 469
470 - def render_GET(self, request):
471 # the Resource.get_HEAD refers to this -- pacify pychecker 472 raise NotImplementedError
473
474 -class ErrorResource(RTSPResource):
475 - def __init__(self, code, *lines):
476 resource.Resource.__init__(self) 477 self.code = code 478 self.body = "" 479 if lines != (None,): 480 self.body = "\n".join(lines) + "\n\n" 481 482 # HACK! 483 if not hasattr(self, 'method'): 484 self.method = 'GET'
485
486 - def render(self, request):
487 request.clientproto = SERVER_PROTOCOL 488 self.render_startCSeqDate(request, request.method) 489 request.setResponseCode(self.code) 490 if self.body: 491 request.setHeader('content-type', "text/plain") 492 return self.body
493
494 - def render_GET(self, request):
495 # the Resource.get_HEAD refers to this -- pacify pychecker 496 raise NotImplementedError
497
498 - def getChild(self, chname, request):
499 return self
500
501 -class NoResource(ErrorResource):
502 - def __init__(self, message=None):
504