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: 7162 $" 
 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   
161 -class RTSPError(Exception):
162 """An exception with the RTSP status code and a str as arguments"""
163 164
165 -class RTSPRequest(http.Request, flog.Loggable):
166 logCategory = 'request' 167 code = OK 168 code_message = RESPONSES[OK] 169 host = None 170 port = None 171
172 - def delHeader(self, key):
173 if key.lower() in self.headers.keys(): 174 del self.headers[key.lower()]
175 176 # base method override 177 178 # copied from HTTP since we have our own set of RESPONSES 179
180 - def setResponseCode(self, code, message=None):
181 """ 182 Set the RTSP response code. 183 """ 184 self.code = code 185 if message: 186 self.code_message = message 187 else: 188 self.code_message = RESPONSES.get(code, "Unknown Status")
189
190 - def process(self):
191 # First check that we have a valid request. 192 if self.clientproto != SERVER_PROTOCOL: 193 e = ErrorResource(BAD_REQUEST) 194 self.render(e) 195 return 196 197 # process the request and render the resource or give a failure 198 first = "%s %s %s" % (self.method, self.path, SERVER_PROTOCOL) 199 self.debug('incoming request: %s' % first) 200 201 lines = [] 202 for key, value in self.received_headers.items(): 203 lines.append("%s: %s" % (key, value)) 204 205 self.debug('incoming headers:\n%s\n' % "\n".join(lines)) 206 207 #self.debug('user-agent: %s' % self.received_headers.get('user-agent', 208 # '[Unknown]')) 209 #self.debug('clientid: %s' % self.received_headers.get('clientid', 210 # '[Unknown]')) 211 212 # don't store site locally; we can't be sure every request has gone 213 # through our customized handlers 214 site = self.channel.site 215 ip = self.getClientIP() 216 site.logRequest(ip, first, lines) 217 218 if not self._processPath(): 219 return 220 221 try: 222 if self.path == "*": 223 resrc = site.resource 224 else: 225 resrc = site.getResourceFor(self) 226 self.debug("RTSPRequest.process(): got resource %r" % resrc) 227 try: 228 self.render(resrc) 229 except server.UnsupportedMethod: 230 e = ErrorResource(OPTION_NOT_SUPPORTED) 231 self.setHeader('Allow', ",".join(resrc.allowedMethods)) 232 self.render(e) 233 except RTSPError, e: 234 er = ErrorResource(e.args[0]) 235 self.render(er) 236 except Exception, e: 237 self.warning('failed to process %s: %s' % 238 (lines and lines[0] or "[No headers]", 239 flog.getExceptionMessage(e))) 240 self.processingFailed(failure.Failure())
241
242 - def _processPath(self):
243 # process self.path into components; return whether or not it worked 244 self.log("path %s" % self.path) 245 246 self.prepath = [] # used in getResourceFor 247 248 # check Request-URI; RFC 2326 6.1 says it's "*" or absolute URI 249 if self.path == '*': 250 self.log('Request-URI is *') 251 return True 252 253 # match the host:port 254 matcher = re.compile('rtspu?://([^/]*)') 255 m = matcher.match(self.path) 256 hostport = None 257 if m: 258 hostport = m.expand('\\1') 259 260 if not hostport: 261 # malformed Request-URI; 400 seems like a likely reply ? 262 self.log('Absolute rtsp URL required: %s' % self.path) 263 self.render(ErrorResource(BAD_REQUEST, 264 "Malformed Request-URI %s" % self.path)) 265 return False 266 267 # get the rest after hostport starting with '/' 268 rest = self.path.split(hostport)[1] 269 self.host = hostport 270 if ':' in hostport: 271 chunks = hostport.split(':') 272 self.host = chunks[0] 273 self.port = int(chunks[1]) 274 # if we got fed crap, they're in other chunks, and we ignore them 275 276 self.postpath = map(unquote, rest.split('/')) 277 self.log( 278 'split up self.path in host %s, port %r, pre %r and post %r' % ( 279 self.host, self.port, self.prepath, self.postpath)) 280 return True
281
282 - def processingFailed(self, reason):
283 self.warningFailure(reason) 284 # FIXME: disable tracebacks until we can reliably disable them 285 if not True: # self.site or self.site.displayTracebacks: 286 self.debug('sending traceback to client') 287 import traceback 288 tb = sys.exc_info()[2] 289 text = "".join(traceback.format_exception( 290 reason.type, reason.value, tb)) 291 else: 292 text = "RTSP server failed to process your request.\n" 293 294 self.setResponseCode(INTERNAL_SERVER_ERROR) 295 self.setHeader('Content-Type', "text/plain") 296 self.setHeader('Content-Length', str(len(text))) 297 self.write(text) 298 self.finish() 299 return reason
300
301 - def _error(self, code, *lines):
302 self.setResponseCode(code) 303 self.setHeader('content-type', "text/plain") 304 body = "\n".join(lines) 305 return body
306
307 - def render(self, resrc):
308 self.log('%r.render(%r)' % (resrc, self)) 309 result = resrc.render(self) 310 self.log('%r.render(%r) returned result %r' % (resrc, self, result)) 311 if isinstance(result, defer.Deferred): 312 result.addCallback(self._renderCallback, resrc) 313 result.addErrback(self._renderErrback, resrc) 314 else: 315 self._renderCallback(result, resrc)
316 317 # TODO: Refactor this and renderCallback to be cleaner and share code. 318
319 - def _renderErrback(self, failure, resrc):
320 body = self._error(INTERNAL_SERVER_ERROR, 321 "Request failed: %r" % failure) 322 self.setHeader('Content-Length', str(len(body))) 323 lines = [] 324 for key, value in self.headers.items(): 325 lines.append("%s: %s" % (key, value)) 326 327 self.channel.site.logReply(self.code, self.code_message, lines, body) 328 329 self.write(body) 330 self.finish()
331
332 - def _renderCallback(self, result, resrc):
333 body = result 334 if type(body) is not types.StringType: 335 self.warning('request did not return a string but %r' % 336 type(body)) 337 body = self._error(INTERNAL_SERVER_ERROR, 338 "Request did not return a string", 339 "Request: " + reflect.safe_repr(self), 340 "Resource: " + reflect.safe_repr(resrc), 341 "Value: " + reflect.safe_repr(body)) 342 self.setHeader('Content-Length', str(len(body))) 343 344 lines = [] 345 for key, value in self.headers.items(): 346 lines.append("%s: %s" % (key, value)) 347 # FIXME: debug response code 348 self.debug('responding to %s %s with %s (%d)' % ( 349 self.method, self.path, self.code_message, self.code)) 350 self.debug('outgoing headers:\n%s\n' % "\n".join(lines)) 351 if body: 352 self.debug('body:\n%s\n' % body) 353 self.log('RTSPRequest._renderCallback(): outgoing response:\n%s\n' % 354 "\n".join(lines)) 355 self.log("\n".join(lines)) 356 self.log("\n") 357 self.log(body) 358 359 self.channel.site.logReply(self.code, self.code_message, lines, body) 360 361 self.write(body) 362 self.finish()
363 364 # RTSP keeps the initial request alive, pinging it regularly. 365 # for now we just keep it persistent for ever 366 367
368 -class RTSPChannel(http.HTTPChannel):
369 370 requestFactory = RTSPRequest 371
372 - def checkPersistence(self, request, version):
373 if version == SERVER_PROTOCOL: 374 return 1 375 log.err('version %s not handled' % version) 376 return 0
377 378 #class RTSPFactory(http.HTTPFactory): 379 # protocol = RTSPChannel 380 # timeout = 60 381 382
383 -class RTSPSite(server.Site):
384 """ 385 I am a ServerFactory that can be used in 386 L{twisted.internet.interfaces.IReactorTCP}'s .listenTCP 387 Create me with an L{RTSPResource} object. 388 """ 389 protocol = RTSPChannel 390 requestFactory = RTSPRequest 391
392 - def logRequest(self, ip, requestLine, headerLines):
393 pass
394
395 - def logReply(self, code, message, headerLines, body):
396 pass
397 398
399 -class RTSPResource(resource.Resource, flog.Loggable):
400 """ 401 I am a base class for all RTSP Resource classes. 402 403 @type allowedMethods: tuple 404 @ivar allowedMethods: a tuple of allowed methods that can be invoked 405 on this resource. 406 """ 407 408 logCategory = 'resource' 409 allowedMethods = ['OPTIONS'] 410
411 - def getChild(self, path, request):
412 return NoResource() 413 # use WithDefault so static children have a chance too 414 self.log( 415 'RTSPResource.getChild(%r, %s, <request>), pre %r, post %r' % ( 416 self, path, request.prepath, request.postpath)) 417 res = resource.Resource.getChild(self, path, request) 418 self.log('RTSPResource.getChild(%r, %s, <request>) returns %r' % ( 419 self, path, res)) 420 return res
421
422 - def getChildWithDefault(self, path, request):
423 self.log( 424 'RTSPResource.getChildWithDefault(%r, %s, <request>), pre %r, ' 425 'post %r' % ( 426 self, path, request.prepath, request.postpath)) 427 self.log('children: %r' % self.children.keys()) 428 res = resource.Resource.getChildWithDefault(self, path, request) 429 self.log( 430 'RTSPResource.getChildWithDefault(%r, %s, <request>) ' 431 'returns %r' % ( 432 self, path, res)) 433 return res
434 435 # FIXME: remove 436
437 - def noputChild(self, path, r):
438 self.log('RTSPResource.putChild(%r, %s, %r)' % (self, path, r)) 439 return resource.Resource.putChild(self, path, r)
440 441 # needs to be done for ALL responses 442 # see 12.17 CSeq and H14.19 Date 443
444 - def render_startCSeqDate(self, request, method):
445 """ 446 Set CSeq and Date on response to given request. 447 This should be done even for errors. 448 """ 449 self.log('render_startCSeqDate, method %r' % method) 450 cseq = request.getHeader('CSeq') 451 # RFC says clients MUST have CSeq field, but we're lenient 452 # in what we accept and assume 0 if not specified 453 if cseq == None: 454 cseq = 0 455 request.setHeader('CSeq', cseq) 456 request.setHeader('Date', http.datetimeToString())
457
458 - def render_start(self, request, method):
459 ip = request.getClientIP() 460 self.log('RTSPResource.render_start(): client from %s requests %s' % ( 461 ip, method)) 462 self.log('RTSPResource.render_start(): uri %r' % request.path) 463 464 self.render_startCSeqDate(request, method) 465 request.setHeader('Server', SERVER_STRING) 466 request.delHeader('Content-Type') 467 468 # tests for 3gpp 469 request.setHeader('Last-Modified', http.datetimeToString()) 470 request.setHeader('Cache-Control', 'must-revalidate') 471 #request.setHeader('x-Accept-Retransmit', 'our-revalidate') 472 #request.setHeader('x-Accept-Dynamic-Rate', '1') 473 #request.setHeader('Content-Base', 'rtsp://core.fluendo.com/test.3gpp') 474 #request.setHeader('Via', 'RTSP/1.0 288f9c2a') 475 476 # hacks for Real 477 if 'Real' in request.received_headers.get('user-agent', ''): 478 self.debug('Detected Real client, sending specific headers') 479 # request.setHeader('Public', 'OPTIONS, DESCRIBE, ANNOUNCE, PLAY, 480 # SETUP, GET_PARAMETER, SET_PARAMETER, TEARDOWN') 481 # Public seems to be the same as allowed-methods, and real clients 482 # seem to respect SET_PARAMETER not listed here 483 request.setHeader( 484 'Public', 485 'OPTIONS, DESCRIBE, ANNOUNCE, PLAY, SETUP, TEARDOWN') 486 # without a RealChallenge1, clients don't even go past OPTIONS 487 request.setHeader('RealChallenge1', 488 '28d49444034696e1d523f2819b8dcf4c')
489 #request.setHeader('StatsMask', '3') 490
491 - def render_GET(self, request):
492 # the Resource.get_HEAD refers to this -- pacify pychecker 493 raise NotImplementedError
494 495
496 -class ErrorResource(RTSPResource):
497
498 - def __init__(self, code, *lines):
499 resource.Resource.__init__(self) 500 self.code = code 501 self.body = "" 502 if lines != (None, ): 503 self.body = "\n".join(lines) + "\n\n" 504 505 # HACK! 506 if not hasattr(self, 'method'): 507 self.method = 'GET'
508
509 - def render(self, request):
510 request.clientproto = SERVER_PROTOCOL 511 self.render_startCSeqDate(request, request.method) 512 request.setResponseCode(self.code) 513 if self.body: 514 request.setHeader('content-type', "text/plain") 515 return self.body
516
517 - def render_GET(self, request):
518 # the Resource.get_HEAD refers to this -- pacify pychecker 519 raise NotImplementedError
520
521 - def getChild(self, chname, request):
522 return self
523 524
525 -class NoResource(ErrorResource):
526
527 - def __init__(self, message=None):
529