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: 8008 $"
49
50 LOG_CATEGORY = "httpserver"
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
73 -class File(resource.Resource, log.Loggable):
74 """
75 this file is inspired by/adapted from twisted.web.static
76 """
77
78 logCategory = LOG_CATEGORY
79
80 defaultType = "application/octet-stream"
81
82 childNotFound = weberror.NoResource("File not found.")
83 forbiddenResource = weberror.ForbiddenResource("Access forbidden")
84 badRequest = BadRequest()
85 internalServerError = InternalServerError()
86
87 - def __init__(self, path, httpauth,
88 mimeToResource=None,
89 rateController=None,
90 requestModifiers=None,
91 metadataProvider=None):
92 resource.Resource.__init__(self)
93
94 self._path = path
95 self._httpauth = httpauth
96
97 self._mimeToResource = mimeToResource or {}
98 self._rateController = rateController
99 self._metadataProvider = metadataProvider
100 self._requestModifiers = requestModifiers or []
101 self._factory = MimedFileFactory(httpauth, self._mimeToResource,
102 rateController=rateController,
103 metadataProvider=metadataProvider,
104 requestModifiers=requestModifiers)
105
122
139
145
154
173
175
176
177 self.debug('[fd %5d] (ts %f) authenticated request %r',
178 request.transport.fileno(), time.time(), request)
179
180
181
182
183
184
185 try:
186 self.debug("Opening file %s", self._path)
187 provider = self._path.open()
188 except fileprovider.NotFoundError:
189 self.debug("Could not find resource %s", self._path)
190 return self.childNotFound.render(request)
191 except fileprovider.CannotOpenError:
192 self.debug("%s is a directory, can't be GET", self._path)
193 return self.childNotFound.render(request)
194 except fileprovider.AccessError:
195 return self.forbiddenResource.render(request)
196
197
198
199
200
201
202 request.setHeader('Server', 'Flumotion/%s' % configure.version)
203 request.setHeader('Connection', 'close')
204
205
206
207 if not self._path.path.endswith('.pdf'):
208 request.setHeader('Accept-Ranges', 'bytes')
209
210 if request.setLastModified(provider.getmtime()) is http.CACHED:
211 return ''
212
213 contentType = provider.mimeType or self.defaultType
214
215 if contentType:
216 self.debug('File content type: %r' % contentType)
217 request.setHeader('content-type', contentType)
218
219 fileSize = provider.getsize()
220
221 first = 0
222 last = fileSize - 1
223
224 requestRange = request.getHeader('range')
225 if requestRange is not None:
226
227
228
229 self.log('range request, %r', requestRange)
230 rangeKeyValue = string.split(requestRange, '=')
231 if len(rangeKeyValue) != 2:
232 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
233 return ''
234
235 if rangeKeyValue[0] != 'bytes':
236 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
237 return ''
238
239
240 ranges = rangeKeyValue[1].split(',')[0]
241 l = ranges.split('-')
242 if len(l) != 2:
243 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
244 return ''
245
246 start, end = l
247
248 if start:
249
250 first = int(start)
251 if end:
252 last = min(int(end), last)
253 elif end:
254
255 count = int(end)
256
257 if count > fileSize:
258 count = fileSize
259 first = fileSize - count
260 else:
261
262 request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE)
263 return ''
264
265
266 if first:
267
268
269 self.debug("Request for range \"%s\" of file, seeking to "
270 "%d of total file size %d", ranges, first, fileSize)
271 provider.seek(first)
272
273
274
275 request.setResponseCode(http.PARTIAL_CONTENT)
276 request.setHeader('Content-Range', "bytes %d-%d/%d" %
277 (first, last, fileSize))
278
279 request.setResponseRange(first, last, fileSize)
280 d = defer.maybeDeferred(self.do_prepareBody,
281 request, provider, first, last)
282
283 def dispatchMethod(header, request):
284 if request.method == 'HEAD':
285
286
287 return ''
288 return self._startRequest(request, header, provider, first, last)
289
290 d.addCallback(dispatchMethod, request)
291
292 return d
293
294 - def _startRequest(self, request, header, provider, first, last):
295
296 for modifier in self._requestModifiers:
297 modifier.modify(request)
298
299
300 self.debug('[fd %5d] (ts %f) started request %r',
301 request.transport.fileno(), time.time(), request)
302
303 if self._metadataProvider:
304 self.log("Retrieving metadata using %r", self._metadataProvider)
305 d = self._metadataProvider.getMetadata(self._path.path)
306 else:
307 d = defer.succeed(None)
308
309 def metadataError(failure):
310 self.warning('Error retrieving metadata for file %s'
311 ' using plug %r. %r',
312 self._path.path,
313 self._metadataProvider,
314 failure.value)
315
316 d.addErrback(metadataError)
317 d.addCallback(self._configureTransfer, request, header,
318 provider, first, last)
319
320 return d
321
353
354 d.addCallback(attachProxy, provider, header, first, last)
355
356 return d
357
358 - def do_prepareBody(self, request, provider, first, last):
359 """
360 I am called before the body of the response gets written,
361 and after generic header setting has been done.
362
363 I set Content-Length.
364
365 Override me to send additional headers, or to prefix the body
366 with data headers.
367
368 I can return a Deferred, that should fire with a string header. That
369 header will be written to the request.
370 """
371 request.setHeader("Content-Length", str(last - first + 1))
372 return ''
373
374
376 """
377 I create File subclasses based on the mime type of the given path.
378 """
379
380 logCategory = LOG_CATEGORY
381
382 defaultType = "application/octet-stream"
383
384 - def __init__(self, httpauth,
385 mimeToResource=None,
386 rateController=None,
387 requestModifiers=None,
388 metadataProvider=None):
389 self._httpauth = httpauth
390 self._mimeToResource = mimeToResource or {}
391 self._rateController = rateController
392 self._requestModifiers = requestModifiers
393 self._metadataProvider = metadataProvider
394
396 """
397 Creates and returns an instance of a File subclass based
398 on the mime type of the given path.
399 """
400 mimeType = path.mimeType or self.defaultType
401 self.debug("Create %s file for %s", mimeType, path)
402 klazz = self._mimeToResource.get(mimeType, File)
403 return klazz(path, self._httpauth,
404 mimeToResource=self._mimeToResource,
405 rateController=self._rateController,
406 requestModifiers=self._requestModifiers,
407 metadataProvider=self._metadataProvider)
408
409
411 """
412 I am a File resource for FLV files.
413 I can handle requests with a 'start' GET parameter.
414 This parameter represents the byte offset from where to start.
415 If it is non-zero, I will output an FLV header so the result is
416 playable.
417 """
418 header = 'FLV\x01\x01\000\000\000\x09\000\000\000\x09'
419
420 - def do_prepareBody(self, request, provider, first, last):
421 self.log('do_prepareBody for FLV')
422 length = last - first + 1
423 ret = ''
424
425
426
427
428 try:
429 start = int(request.args.get('start', ['0'])[0])
430 except ValueError:
431 start = 0
432
433 if request.getHeader('range') is None and start:
434 self.debug('Start %d passed, seeking', start)
435 provider.seek(start)
436 length = last - start + 1 + len(self.header)
437 ret = self.header
438
439 request.setHeader("Content-Length", str(length))
440
441 return ret
442
443
445 """
446 I am a File resource for MP4 files.
447 If I have a library for manipulating MP4 files available, I can handle
448 requests with a 'start' GET parameter, Without the library, I ignore this
449 parameter.
450 The 'start' parameter represents the time offset from where to start, in
451 seconds. If it is non-zero, I will seek inside the file to the sample with
452 that time, and prepend the content with rebuilt MP4 tables, to make the
453 output playable.
454 """
455
456 - def do_prepareBody(self, request, provider, first, last):
457 self.log('do_prepareBody for MP4')
458 length = last - first + 1
459 ret = ''
460
461
462
463 try:
464 start = float(request.args.get('start', ['0'])[0])
465 except ValueError:
466 start = 0
467
468 if request.getHeader('range') is None and start and HAS_MP4SEEK:
469 self.debug('Start %f passed, seeking', start)
470 provider.seek(0)
471 d = self._split_file(provider, start)
472
473 def seekAndSetContentLength(header_and_offset):
474 header, offset = header_and_offset
475
476
477 length = last - offset + 1 + header.tell()
478 provider.seek(offset)
479 request.setHeader("Content-Length", str(length))
480 header.seek(0)
481 return header.read()
482
483 def seekingFailed(failure):
484
485 self.warning("Seeking in MP4 file %s failed: %s", provider,
486 log.getFailureMessage(failure))
487 provider.seek(0)
488 request.setHeader('Content-Length', str(length))
489 return ret
490
491 d.addCallback(seekAndSetContentLength)
492 d.addErrback(seekingFailed)
493 return d
494 else:
495 request.setHeader('Content-Length', str(length))
496 return defer.succeed(ret)
497
499 d = defer.Deferred()
500
501 def read_some_data(how_much, from_where):
502 if how_much:
503 provider.seek(from_where)
504 read_d = provider.read(how_much)
505 read_d.addCallback(splitter.feed)
506 read_d.addErrback(d.errback)
507 else:
508 d.callback(splitter.result())
509
510 splitter = mp4seek.async.Splitter(start)
511 splitter.start(read_some_data)
512
513 return d
514
515
517 """
518 A class to represent the transfer of a file over the network.
519 """
520
521 logCategory = LOG_CATEGORY
522
523 consumer = None
524
525 - def __init__(self, provider, size, consumer):
526 """
527 @param provider: an asynchronous file provider
528 @type provider: L{fileprovider.File}
529 @param size: file position to which file should be read
530 @type size: int
531 @param consumer: consumer to receive the data
532 @type consumer: L{twisted.internet.interfaces.IFinishableConsumer}
533 """
534 self.provider = provider
535 self.size = size
536 self.consumer = consumer
537 self.written = self.provider.tell()
538 self.bytesWritten = 0
539 self._pending = None
540 self._again = False
541 self._finished = False
542 self.debug("Calling registerProducer on %r", consumer)
543 consumer.registerProducer(self, 0)
544
549
552
554 self.debug('Stop producing from %s at %d/%d bytes',
555 self.provider, self.provider.tell(), self.size)
556
557
558
559 self._terminate()
560
562 if self._pending:
563
564 self._again = True
565 return
566 self._again = False
567 d = self.provider.read(min(abstract.FileDescriptor.bufferSize,
568 self.size - self.written))
569 self._pending = d
570 d.addCallbacks(self._cbGotData, self._ebReadFailed)
571
573 self._pending = None
574
575
576
577
578 if self._finished:
579 return
580
581 if data:
582
583
584 self._writeToConsumer(data)
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599 if self._finished:
600 return
601
602 if self.provider.tell() == self.size:
603 self.debug('Written entire file of %d bytes from %s',
604 self.size, self.provider)
605 self._terminate()
606 elif self._again:
607
608 self._produce()
609
615
622
630