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