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   
 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  # 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[0], flog.getExceptionMessage(e))) 236 self.processingFailed(failure.Failure())
237
238 - def _processPath(self):
239 # process self.path into components; return whether or not it worked 240 self.log("path %s" % self.path) 241 242 self.prepath = [] # used in getResourceFor 243 244 # check Request-URI; RFC 2326 6.1 says it's "*" or absolute URI 245 if self.path == '*': 246 self.log('Request-URI is *') 247 return True 248 249 # match the host:port 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 # malformed Request-URI; 400 seems like a likely reply ? 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 # get the rest after hostport starting with '/' 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 # if we got fed crap, they're in other chunks, and we ignore them 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
277 - def processingFailed(self, reason):
278 self.warningFailure(reason) 279 # FIXME: disable tracebacks until we can reliably disable them 280 if not True: # self.site or self.site.displayTracebacks: 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
302 - def render(self, resrc):
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 # web uses NOT_DONE_YET as a return value of render, maybe we should 307 # do that instead of doing deferreds 308 if isinstance(result, defer.Deferred): 309 result.addCallback(self._renderCallback, resrc) 310 else: 311 self._renderCallback(result, resrc)
312 313
314 - def _renderCallback(self, result, resrc):
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 # FIXME: debug response code 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 # RTSP keeps the initial request alive, pinging it regularly. 347 # for now we just keep it persistent for ever
348 -class RTSPChannel(http.HTTPChannel):
349 350 requestFactory = RTSPRequest 351
352 - def checkPersistence(self, request, version):
353 if version == SERVER_PROTOCOL: 354 return 1 355 log.err('version %s not handled' % version) 356 return 0
357 358 #class RTSPFactory(http.HTTPFactory): 359 # protocol = RTSPChannel 360 # timeout = 60 361
362 -class RTSPSite(server.Site):
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):
372 pass
373 - def logReply(self, code, message, headerLines, body):
374 pass
375
376 -class RTSPResource(resource.Resource, flog.Loggable):
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
388 - def getChild(self, path, request):
389 return NoResource() 390 # use WithDefault so static children have a chance too 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
398 - def getChildWithDefault(self, path, request):
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 # FIXME: remove
408 - def noputChild(self, path, r):
409 self.log('RTSPResource.putChild(%r, %s, %r)' % (self, path, r)) 410 return resource.Resource.putChild(self, path, r)
411 412 # needs to be done for ALL responses 413 # see 12.17 CSeq and H14.19 Date
414 - def render_startCSeqDate(self, request, method):
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 # RFC says clients MUST have CSeq field, but we're lenient 421 # in what we accept and assume 0 if not specified 422 if cseq == None: 423 cseq = 0 424 request.setHeader('CSeq', cseq) 425 request.setHeader('Date', http.datetimeToString())
426
427 - def render_start(self, request, method):
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 # tests for 3gpp 438 request.setHeader('Last-Modified', http.datetimeToString()) 439 request.setHeader('Cache-Control', 'must-revalidate') 440 #request.setHeader('x-Accept-Retransmit', 'our-revalidate') 441 #request.setHeader('x-Accept-Dynamic-Rate', '1') 442 #request.setHeader('Content-Base', 'rtsp://core.fluendo.com/test.3gpp') 443 #request.setHeader('Via', 'RTSP/1.0 288f9c2a') 444 445 # hacks for Real 446 if 'Real' in request.received_headers.get('user-agent', ''): 447 self.debug('Detected Real client, sending specific headers') 448 # request.setHeader('Public', 'OPTIONS, DESCRIBE, ANNOUNCE, PLAY, SETUP, GET_PARAMETER, SET_PARAMETER, TEARDOWN') 449 # Public seems to be the same as allowed-methods, and real clients 450 # seem to respect SET_PARAMETER not listed here 451 request.setHeader('Public', 'OPTIONS, DESCRIBE, ANNOUNCE, PLAY, SETUP, TEARDOWN') 452 # without a RealChallenge1, clients don't even go past OPTIONS 453 request.setHeader('RealChallenge1', '28d49444034696e1d523f2819b8dcf4c')
454 #request.setHeader('StatsMask', '3') 455
456 - def render_GET(self, request):
457 # the Resource.get_HEAD refers to this -- pacify pychecker 458 raise NotImplementedError
459
460 -class ErrorResource(RTSPResource):
461 - def __init__(self, code, *lines):
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 # HACK! 469 if not hasattr(self, 'method'): 470 self.method = 'GET'
471
472 - def render(self, request):
473 request.clientproto = SERVER_PROTOCOL 474 self.render_startCSeqDate(request, request.method) 475 request.setResponseCode(self.code) 476 if self.body: 477 request.setHeader('content-type', "text/plain") 478 return self.body
479
480 - def render_GET(self, request):
481 # the Resource.get_HEAD refers to this -- pacify pychecker 482 raise NotImplementedError
483
484 - def getChild(self, chname, request):
485 return self
486
487 -class NoResource(ErrorResource):
488 - def __init__(self, message=None):
490