1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import string
23 import time
24
25
26 HAS_MP4SEEK = False
27 try:
28 import mp4seek.async
29 HAS_MP4SEEK = True
30 except ImportError:
31 pass
32
33 from twisted.web import resource, server, http
34 from twisted.web import error as weberror, static
35 from twisted.internet import defer, reactor, error, abstract
36 from twisted.cred import credentials
37 from twisted.python.failure import Failure
38
39 from flumotion.configure import configure
40 from flumotion.component import component
41 from flumotion.common import log, messages, errors, netutils
42 from flumotion.component.component import moods
43 from flumotion.component.misc.porter import porterclient
44 from flumotion.component.misc.httpserver import fileprovider
45 from flumotion.component.base import http as httpbase
46 from flumotion.twisted import fdserver
47
48 __version__ = "$Rev: 8510 $"
49
50 LOG_CATEGORY = "httpserver"
51
52 try:
53 resource.ErrorPage
54 errorpage = resource
55 except AttributeError:
56 errorpage = weberror
57
58
60 """
61 Web error for invalid requests
62 """
63
64 - def __init__(self, message="Invalid request format"):
67
68
70 """
71 Web error for internal failures
72 """
73
74 - def __init__(self, message="The server failed to complete the request"):
77
78
80 """
81 Web error for when the request cannot be served.
82 """
83
84 - def __init__(self, message="The server is currently unable to handle "
85 "the request due to a temporary overloading "
86 "or maintenance of the server"):
89
90
91 -class File(resource.Resource, log.Loggable):
92 """
93 this file is inspired by/adapted from twisted.web.static
94 """
95
96 logCategory = LOG_CATEGORY
97
98 defaultType = "application/octet-stream"
99
100 childNotFound = errorpage.NoResource("File not found.")
101 forbiddenerrorpage = errorpage.ForbiddenResource("Access forbidden")
102 badRequest = BadRequest()
103 internalServerError = InternalServerError()
104 serviceUnavailable = ServiceUnavailableError()
105
106 - def __init__(self, path, httpauth,
107 mimeToResource=None,
108 rateController=None,
109 requestModifiers=None,
110 metadataProvider=None):
111 resource.Resource.__init__(self)
112
113 self._path = path
114 self._httpauth = httpauth
115
116 self._mimeToResource = mimeToResource or {}
117 self._rateController = rateController
118 self._metadataProvider = metadataProvider
119 self._requestModifiers = requestModifiers or []
120 self._factory = MimedFileFactory(httpauth, self._mimeToResource,
121 rateController=rateController,
122 metadataProvider=metadataProvider,
123 requestModifiers=requestModifiers)
124
141
143 """
144 The request gets rendered by asking the httpauth object for
145 authentication, which returns a deferred.
146 This deferred will callback when the request gets authenticated.
147 """
148
149
150 self.debug('[fd %5d] (ts %f) incoming request %r',
151 request.transport.fileno(), time.time(), request)
152
153
154
155
156
157 request.setHeader('Server', 'Flumotion/%s' % configure.version)
158 request.setHeader('Connection', 'close')
159
160 d = self._httpauth.startAuthentication(request)
161 d.addCallbacks(self._requestAuthenticated, self._authenticationFailed,
162 callbackArgs=(request, ), errbackArgs=(request, ))
163
164 return server.NOT_DONE_YET
165
171
180
211
234
245
247 self.debug("Rendering file %s", self._path)
248
249
250
251
252
253
254 request.setHeader('Server', 'Flumotion/%s' % configure.version)
255 request.setHeader('Connection', 'close')
256
257
258
259 if not self._path.mimeType == 'application/pdf':
260 request.setHeader('Accept-Ranges', 'bytes')
261
262 if request.setLastModified(provider.getmtime()) is http.CACHED:
263 return ''
264
265 contentType = provider.mimeType or self.defaultType
266
267 if contentType:
268 self.debug('File content type: %r' % contentType)
269 request.setHeader('content-type', contentType)
270
271 fileSize = provider.getsize()
272
273 first = 0
274 last = fileSize - 1
275
276 requestRange = request.getHeader('range')
277 if requestRange is not None:
278
279
280
281 self.log('range request, %r', requestRange)
282 rangeKeyValue = string.split(requestRange, '=')
283 if len(rangeKeyValue) != 2:
284 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
285 return ''
286
287 if rangeKeyValue[0] != 'bytes':
288 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
289 return ''
290
291
292 ranges = rangeKeyValue[1].split(',')[0]
293 l = ranges.split('-')
294 if len(l) != 2:
295 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
296 return ''
297
298 start, end = l
299
300 if start:
301
302 first = int(start)
303 if end:
304 last = min(int(end), last)
305 elif end:
306
307 count = int(end)
308
309 if count > fileSize:
310 count = fileSize
311 first = fileSize - count
312 else:
313
314 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
315 return ''
316
317
318 if first:
319
320
321 self.debug("Request for range \"%s\" of file, seeking to "
322 "%d of total file size %d", ranges, first, fileSize)
323 provider.seek(first)
324
325
326
327 request.setResponseCode(http.PARTIAL_CONTENT)
328 request.setHeader('Content-Range', "bytes %d-%d/%d" %
329 (first, last, fileSize))
330
331 request.setResponseRange(first, last, fileSize)
332 d = defer.maybeDeferred(self.do_prepareBody,
333 request, provider, first, last)
334
335 def dispatchMethod(header, request):
336 if request.method == 'HEAD':
337
338
339 return ''
340 return self._startRequest(request, header, provider, first, last)
341
342 d.addCallback(dispatchMethod, request)
343
344 return d
345
346 - def _startRequest(self, request, header, provider, first, last):
347
348 for modifier in self._requestModifiers:
349 modifier.modify(request)
350
351
352 self.debug('[fd %5d] (ts %f) started request %r',
353 request.transport.fileno(), time.time(), request)
354
355 if self._metadataProvider:
356 self.log("Retrieving metadata using %r", self._metadataProvider)
357
358
359 d = self._metadataProvider.getMetadata(request.path)
360 else:
361 d = defer.succeed(None)
362
363 def metadataError(failure):
364 self.warning('Error retrieving metadata for file %s'
365 ' using plug %r. %r',
366 request.path,
367 self._metadataProvider,
368 failure.value)
369
370 d.addErrback(metadataError)
371 d.addCallback(self._configureTransfer, request, header,
372 provider, first, last)
373
374 return d
375
407
408 d.addCallback(attachProxy, provider, header, first, last)
409
410 return d
411
412 - def do_prepareBody(self, request, provider, first, last):
413 """
414 I am called before the body of the response gets written,
415 and after generic header setting has been done.
416
417 I set Content-Length.
418
419 Override me to send additional headers, or to prefix the body
420 with data headers.
421
422 I can return a Deferred, that should fire with a string header. That
423 header will be written to the request.
424 """
425 request.setHeader("Content-Length", str(last - first + 1))
426 return ''
427
428
430 """
431 I create File subclasses based on the mime type of the given path.
432 """
433
434 logCategory = LOG_CATEGORY
435
436 defaultType = "application/octet-stream"
437
438 - def __init__(self, httpauth,
439 mimeToResource=None,
440 rateController=None,
441 requestModifiers=None,
442 metadataProvider=None):
443 self._httpauth = httpauth
444 self._mimeToResource = mimeToResource or {}
445 self._rateController = rateController
446 self._requestModifiers = requestModifiers
447 self._metadataProvider = metadataProvider
448
450 """
451 Creates and returns an instance of a File subclass based
452 on the mime type of the given path.
453 """
454 mimeType = path.mimeType or self.defaultType
455 self.debug("Create %s file for %s", mimeType, path)
456 klazz = self._mimeToResource.get(mimeType, File)
457 return klazz(path, self._httpauth,
458 mimeToResource=self._mimeToResource,
459 rateController=self._rateController,
460 requestModifiers=self._requestModifiers,
461 metadataProvider=self._metadataProvider)
462
463
465 """
466 I am a File resource for FLV files.
467 I can handle requests with a 'start' GET parameter.
468 This parameter represents the byte offset from where to start.
469 If it is non-zero, I will output an FLV header so the result is
470 playable.
471 """
472 header = 'FLV\x01\x01\000\000\000\x09\000\000\000\x09'
473
474 - def do_prepareBody(self, request, provider, first, last):
475 self.log('do_prepareBody for FLV')
476 length = last - first + 1
477 ret = ''
478
479
480
481
482 try:
483 start = int(request.args.get('start', ['0'])[0])
484 except ValueError:
485 start = 0
486
487 if request.getHeader('range') is None and start:
488 self.debug('Start %d passed, seeking', start)
489 provider.seek(start)
490 length = last - start + 1 + len(self.header)
491 ret = self.header
492
493 request.setHeader("Content-Length", str(length))
494
495 return ret
496
497
499 """
500 I am a File resource for MP4 files.
501 If I have a library for manipulating MP4 files available, I can handle
502 requests with a 'start' GET parameter, Without the library, I ignore this
503 parameter.
504 The 'start' parameter represents the time offset from where to start, in
505 seconds. If it is non-zero, I will seek inside the file to the sample with
506 that time, and prepend the content with rebuilt MP4 tables, to make the
507 output playable.
508 """
509
510 - def do_prepareBody(self, request, provider, first, last):
511 self.log('do_prepareBody for MP4')
512 length = last - first + 1
513 ret = ''
514
515
516
517 try:
518 start = float(request.args.get('start', ['0'])[0])
519 except ValueError:
520 start = 0
521
522 if request.getHeader('range') is None and start and HAS_MP4SEEK:
523 self.debug('Start %f passed, seeking', start)
524 provider.seek(0)
525 d = self._split_file(provider, start)
526
527 def seekAndSetContentLength(header_and_offset):
528 header, offset = header_and_offset
529
530
531 length = last - offset + 1 + header.tell()
532 provider.seek(offset)
533 request.setHeader("Content-Length", str(length))
534 header.seek(0)
535 return header.read()
536
537 def seekingFailed(failure):
538
539 self.warning("Seeking in MP4 file %s failed: %s", provider,
540 log.getFailureMessage(failure))
541 provider.seek(0)
542 request.setHeader('Content-Length', str(length))
543 return ret
544
545 d.addCallback(seekAndSetContentLength)
546 d.addErrback(seekingFailed)
547 return d
548 else:
549 request.setHeader('Content-Length', str(length))
550 return defer.succeed(ret)
551
553 d = defer.Deferred()
554
555 def read_some_data(how_much, from_where):
556 if how_much:
557 provider.seek(from_where)
558 read_d = provider.read(how_much)
559 read_d.addCallback(splitter.feed)
560 read_d.addErrback(d.errback)
561 else:
562 d.callback(splitter.result())
563
564 splitter = mp4seek.async.Splitter(start)
565 splitter.start(read_some_data)
566
567 return d
568
569
571 """
572 A class to represent the transfer of a file over the network.
573 """
574
575 logCategory = LOG_CATEGORY
576
577 consumer = None
578
579 - def __init__(self, provider, size, consumer):
580 """
581 @param provider: an asynchronous file provider
582 @type provider: L{fileprovider.File}
583 @param size: file position to which file should be read
584 @type size: int
585 @param consumer: consumer to receive the data
586 @type consumer: L{twisted.internet.interfaces.IFinishableConsumer}
587 """
588 self.provider = provider
589 self.size = size
590 self.consumer = consumer
591 self.written = self.provider.tell()
592 self.bytesWritten = 0
593 self._pending = None
594 self._again = False
595 self._finished = False
596 self.debug("Calling registerProducer on %r", consumer)
597 consumer.registerProducer(self, 0)
598
603
606
608 self.debug('Stop producing from %s at %d/%d bytes',
609 self.provider, self.provider.tell(), self.size)
610
611
612
613 self._terminate()
614
616 if self._pending:
617
618 self._again = True
619 return
620 self._again = False
621 d = self.provider.read(min(abstract.FileDescriptor.bufferSize,
622 self.size - self.written))
623 self._pending = d
624 d.addCallbacks(self._cbGotData, self._ebReadFailed)
625
627 self._pending = None
628
629
630
631
632 if self._finished:
633 return
634
635 if data:
636
637
638 self._writeToConsumer(data)
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653 if self._finished:
654 return
655
656 if self.provider.tell() == self.size:
657 self.debug('Written entire file of %d bytes from %s',
658 self.size, self.provider)
659 self._terminate()
660 elif self._again:
661
662 self._produce()
663
673
680
693