1#!/usr/local/bin/python3.8
2# vim:fileencoding=utf-8
3
4
5__license__ = 'GPL v3'
6__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
7
8import errno
9import hashlib
10import os
11import struct
12import uuid
13from collections import namedtuple
14from functools import wraps
15from io import DEFAULT_BUFFER_SIZE, BytesIO
16from itertools import chain, repeat
17from operator import itemgetter
18
19from calibre import force_unicode, guess_type
20from calibre.constants import __version__
21from calibre.srv.errors import HTTPSimpleResponse
22from calibre.srv.http_request import HTTPRequest, read_headers
23from calibre.srv.loop import WRITE
24from calibre.srv.utils import (
25    HTTP1, HTTP11, Cookie, MultiDict, fast_now_strftime, get_translator_for_lang,
26    http_date, socket_errors_socket_closed, sort_q_values
27)
28from calibre.utils.monotonic import monotonic
29from calibre.utils.speedups import ReadOnlyFileBuffer
30from polyglot import http_client, reprlib
31from polyglot.builtins import (
32    error_message, iteritems, itervalues, reraise, string_or_bytes
33)
34
35Range = namedtuple('Range', 'start stop size')
36MULTIPART_SEPARATOR = uuid.uuid4().hex
37if isinstance(MULTIPART_SEPARATOR, bytes):
38    MULTIPART_SEPARATOR = MULTIPART_SEPARATOR.decode('ascii')
39COMPRESSIBLE_TYPES = {'application/json', 'application/javascript', 'application/xml', 'application/oebps-package+xml'}
40import zlib
41from itertools import zip_longest
42
43
44def file_metadata(fileobj):
45    try:
46        fd = fileobj.fileno()
47        return os.fstat(fd)
48    except Exception:
49        pass
50
51
52def header_list_to_file(buf):  # {{{
53    buf.append('')
54    return ReadOnlyFileBuffer(b''.join((x + '\r\n').encode('ascii') for x in buf))
55# }}}
56
57
58def parse_multipart_byterange(buf, content_type):  # {{{
59    sep = (content_type.rsplit('=', 1)[-1]).encode('utf-8')
60    ans = []
61
62    def parse_part():
63        line = buf.readline()
64        if not line:
65            raise ValueError('Premature end of message')
66        if not line.startswith(b'--' + sep):
67            raise ValueError('Malformed start of multipart message: %s' % reprlib.repr(line))
68        if line.endswith(b'--'):
69            return None
70        headers = read_headers(buf.readline)
71        cr = headers.get('Content-Range')
72        if not cr:
73            raise ValueError('Missing Content-Range header in sub-part')
74        if not cr.startswith('bytes '):
75            raise ValueError('Malformed Content-Range header in sub-part, no prefix')
76        try:
77            start, stop = map(lambda x: int(x.strip()), cr.partition(' ')[-1].partition('/')[0].partition('-')[::2])
78        except Exception:
79            raise ValueError('Malformed Content-Range header in sub-part, failed to parse byte range')
80        content_length = stop - start + 1
81        ret = buf.read(content_length)
82        if len(ret) != content_length:
83            raise ValueError('Malformed sub-part, length of body not equal to length specified in Content-Range')
84        buf.readline()
85        return (start, ret)
86    while True:
87        data = parse_part()
88        if data is None:
89            break
90        ans.append(data)
91    return ans
92# }}}
93
94
95def parse_if_none_match(val):  # {{{
96    return {x.strip() for x in val.split(',')}
97# }}}
98
99
100def acceptable_encoding(val, allowed=frozenset({'gzip'})):  # {{{
101    for x in sort_q_values(val):
102        x = x.lower()
103        if x in allowed:
104            return x
105# }}}
106
107
108def preferred_lang(val, get_translator_for_lang):  # {{{
109    for x in sort_q_values(val):
110        x = x.lower()
111        found, lang, translator = get_translator_for_lang(x)
112        if found:
113            return x
114    return 'en'
115# }}}
116
117
118def get_ranges(headervalue, content_length):  # {{{
119    ''' Return a list of ranges from the Range header. If this function returns
120    an empty list, it indicates no valid range was found. '''
121    if not headervalue:
122        return None
123
124    result = []
125    try:
126        bytesunit, byteranges = headervalue.split("=", 1)
127    except Exception:
128        return None
129    if bytesunit.strip() != 'bytes':
130        return None
131
132    for brange in byteranges.split(","):
133        start, stop = (x.strip() for x in brange.split("-", 1))
134        if start:
135            if not stop:
136                stop = content_length - 1
137            try:
138                start, stop = int(start), int(stop)
139            except Exception:
140                continue
141            if start >= content_length:
142                continue
143            if stop < start:
144                continue
145            stop = min(stop, content_length - 1)
146            result.append(Range(start, stop, stop - start + 1))
147        elif stop:
148            # Negative subscript (last N bytes)
149            try:
150                stop = int(stop)
151            except Exception:
152                continue
153            if stop > content_length:
154                result.append(Range(0, content_length-1, content_length))
155            else:
156                result.append(Range(content_length - stop, content_length - 1, stop))
157
158    return result
159# }}}
160
161# gzip transfer encoding  {{{
162
163
164def gzip_prefix():
165    # See http://www.gzip.org/zlib/rfc-gzip.html
166    return b''.join((
167        b'\x1f\x8b',       # ID1 and ID2: gzip marker
168        b'\x08',           # CM: compression method
169        b'\x00',           # FLG: none set
170        # MTIME: 4 bytes, set to zero so as not to leak timezone information
171        b'\0\0\0\0',
172        b'\x02',           # XFL: max compression, slowest algo
173        b'\xff',           # OS: unknown
174    ))
175
176
177def compress_readable_output(src_file, compress_level=6):
178    crc = zlib.crc32(b"")
179    size = 0
180    zobj = zlib.compressobj(compress_level,
181                            zlib.DEFLATED, -zlib.MAX_WBITS,
182                            zlib.DEF_MEM_LEVEL, zlib.Z_DEFAULT_STRATEGY)
183    prefix_written = False
184    while True:
185        data = src_file.read(DEFAULT_BUFFER_SIZE)
186        if not data:
187            break
188        size += len(data)
189        crc = zlib.crc32(data, crc)
190        data = zobj.compress(data)
191        if not prefix_written:
192            prefix_written = True
193            data = gzip_prefix() + data
194        yield data
195    yield zobj.flush() + struct.pack(b"<L", crc & 0xffffffff) + struct.pack(b"<L", size)
196# }}}
197
198
199def get_range_parts(ranges, content_type, content_length):  # {{{
200
201    def part(r):
202        ans = ['--%s' % MULTIPART_SEPARATOR, 'Content-Range: bytes %d-%d/%d' % (r.start, r.stop, content_length)]
203        if content_type:
204            ans.append('Content-Type: %s' % content_type)
205        ans.append('')
206        return ('\r\n'.join(ans)).encode('ascii')
207    return list(map(part, ranges)) + [('--%s--' % MULTIPART_SEPARATOR).encode('ascii')]
208# }}}
209
210
211class ETaggedFile:  # {{{
212
213    def __init__(self, output, etag):
214        self.output, self.etag = output, etag
215
216    def fileno(self):
217        return self.output.fileno()
218# }}}
219
220
221class RequestData:  # {{{
222
223    cookies = {}
224    username = None
225
226    def __init__(self, method, path, query, inheaders, request_body_file, outheaders, response_protocol,
227                 static_cache, opts, remote_addr, remote_port, is_trusted_ip, translator_cache,
228                 tdir, forwarded_for, request_original_uri=None):
229
230        (self.method, self.path, self.query, self.inheaders, self.request_body_file, self.outheaders,
231         self.response_protocol, self.static_cache, self.translator_cache) = (
232            method, path, query, inheaders, request_body_file, outheaders,
233            response_protocol, static_cache, translator_cache
234        )
235
236        self.remote_addr, self.remote_port, self.is_trusted_ip = remote_addr, remote_port, is_trusted_ip
237        self.forwarded_for = forwarded_for
238        self.request_original_uri = request_original_uri
239        self.opts = opts
240        self.status_code = http_client.OK
241        self.outcookie = Cookie()
242        self.lang_code = self.gettext_func = self.ngettext_func = None
243        self.set_translator(self.get_preferred_language())
244        self.tdir = tdir
245
246    def generate_static_output(self, name, generator, content_type='text/html; charset=UTF-8'):
247        ans = self.static_cache.get(name)
248        if ans is None:
249            ans = self.static_cache[name] = StaticOutput(generator())
250        ct = self.outheaders.get('Content-Type')
251        if not ct:
252            self.outheaders.set('Content-Type', content_type, replace_all=True)
253        return ans
254
255    def filesystem_file_with_custom_etag(self, output, *etag_parts):
256        etag = hashlib.sha1()
257        for i in etag_parts:
258            etag.update(str(i).encode('utf-8'))
259        return ETaggedFile(output, etag.hexdigest())
260
261    def filesystem_file_with_constant_etag(self, output, etag_as_hexencoded_string):
262        return ETaggedFile(output, etag_as_hexencoded_string)
263
264    def etagged_dynamic_response(self, etag, func, content_type='text/html; charset=UTF-8'):
265        ' A response that is generated only if the etag does not match '
266        ct = self.outheaders.get('Content-Type')
267        if not ct:
268            self.outheaders.set('Content-Type', content_type, replace_all=True)
269        if not etag.endswith('"'):
270            etag = '"%s"' % etag
271        return ETaggedDynamicOutput(func, etag)
272
273    def read(self, size=-1):
274        return self.request_body_file.read(size)
275
276    def peek(self, size=-1):
277        pos = self.request_body_file.tell()
278        try:
279            return self.read(size)
280        finally:
281            self.request_body_file.seek(pos)
282
283    def get_translator(self, bcp_47_code):
284        return get_translator_for_lang(self.translator_cache, bcp_47_code)
285
286    def get_preferred_language(self):
287        return preferred_lang(self.inheaders.get('Accept-Language'), self.get_translator)
288
289    def _(self, text):
290        return self.gettext_func(text)
291
292    def ngettext(self, singular, plural, n):
293        return self.ngettext_func(singular, plural, n)
294
295    def set_translator(self, lang_code):
296        if lang_code != self.lang_code:
297            found, lang, t = self.get_translator(lang_code)
298            self.lang_code = lang
299            self.gettext_func = t.gettext
300            self.ngettext_func = t.ngettext
301# }}}
302
303
304class ReadableOutput:
305
306    def __init__(self, output, etag=None, content_length=None):
307        self.src_file = output
308        if content_length is None:
309            self.src_file.seek(0, os.SEEK_END)
310            self.content_length = self.src_file.tell()
311        else:
312            self.content_length = content_length
313        self.etag = etag
314        self.accept_ranges = True
315        self.use_sendfile = False
316        self.src_file.seek(0)
317
318
319def filesystem_file_output(output, outheaders, stat_result):
320    etag = getattr(output, 'etag', None)
321    if etag is None:
322        oname = output.name or ''
323        if not isinstance(oname, string_or_bytes):
324            oname = str(oname)
325        etag = hashlib.sha1((str(stat_result.st_mtime) + force_unicode(oname)).encode('utf-8')).hexdigest()
326    else:
327        output = output.output
328    etag = '"%s"' % etag
329    self = ReadableOutput(output, etag=etag, content_length=stat_result.st_size)
330    self.name = output.name
331    self.use_sendfile = True
332    return self
333
334
335def dynamic_output(output, outheaders, etag=None):
336    if isinstance(output, bytes):
337        data = output
338    else:
339        data = output.encode('utf-8')
340        ct = outheaders.get('Content-Type')
341        if not ct:
342            outheaders.set('Content-Type', 'text/plain; charset=UTF-8', replace_all=True)
343    ans = ReadableOutput(ReadOnlyFileBuffer(data), etag=etag)
344    ans.accept_ranges = False
345    return ans
346
347
348class ETaggedDynamicOutput:
349
350    def __init__(self, func, etag):
351        self.func, self.etag = func, etag
352
353    def __call__(self):
354        return self.func()
355
356
357class GeneratedOutput:
358
359    def __init__(self, output, etag=None):
360        self.output = output
361        self.content_length = None
362        self.etag = etag
363        self.accept_ranges = False
364
365
366class StaticOutput:
367
368    def __init__(self, data):
369        if isinstance(data, str):
370            data = data.encode('utf-8')
371        self.data = data
372        self.etag = '"%s"' % hashlib.sha1(data).hexdigest()
373        self.content_length = len(data)
374
375
376class HTTPConnection(HTTPRequest):
377
378    use_sendfile = False
379
380    def write(self, buf, end=None):
381        pos = buf.tell()
382        if end is None:
383            buf.seek(0, os.SEEK_END)
384            end = buf.tell()
385            buf.seek(pos)
386        limit = end - pos
387        if limit <= 0:
388            return True
389        if self.use_sendfile and not isinstance(buf, (BytesIO, ReadOnlyFileBuffer)):
390            limit = min(limit, 2 ** 30)
391            try:
392                sent = os.sendfile(self.socket.fileno(), buf.fileno(), pos, limit)
393            except OSError as e:
394                if e.errno in socket_errors_socket_closed:
395                    self.ready = self.use_sendfile = False
396                    return False
397                if e.errno in (errno.EAGAIN, errno.EINTR):
398                    return False
399                raise
400            finally:
401                self.last_activity = monotonic()
402            if sent == 0:
403                # Something bad happened, was the file modified on disk by
404                # another process?
405                self.use_sendfile = self.ready = False
406                raise OSError('sendfile() failed to write any bytes to the socket')
407        else:
408            data = buf.read(min(limit, self.send_bufsize))
409            sent = self.send(data)
410        buf.seek(pos + sent)
411        return buf.tell() >= end
412
413    def simple_response(self, status_code, msg='', close_after_response=True, extra_headers=None):
414        if self.response_protocol is HTTP1:
415            # HTTP/1.0 has no 413/414/303 codes
416            status_code = {
417                http_client.REQUEST_ENTITY_TOO_LARGE:http_client.BAD_REQUEST,
418                http_client.REQUEST_URI_TOO_LONG:http_client.BAD_REQUEST,
419                http_client.SEE_OTHER:http_client.FOUND
420            }.get(status_code, status_code)
421
422        self.close_after_response = close_after_response
423        msg = msg.encode('utf-8')
424        ct = 'http' if self.method == 'TRACE' else 'plain'
425        buf = [
426            '%s %d %s' % (self.response_protocol, status_code, http_client.responses[status_code]),
427            "Content-Length: %s" % len(msg),
428            "Content-Type: text/%s; charset=UTF-8" % ct,
429            "Date: " + http_date(),
430        ]
431        if self.close_after_response and self.response_protocol is HTTP11:
432            buf.append("Connection: close")
433        if extra_headers is not None:
434            for h, v in iteritems(extra_headers):
435                buf.append('%s: %s' % (h, v))
436        buf.append('')
437        buf = [(x + '\r\n').encode('ascii') for x in buf]
438        if self.method != 'HEAD':
439            buf.append(msg)
440        response_data = b''.join(buf)
441        self.log_access(status_code=status_code, response_size=len(response_data))
442        self.response_ready(ReadOnlyFileBuffer(response_data))
443
444    def prepare_response(self, inheaders, request_body_file):
445        if self.method == 'TRACE':
446            msg = force_unicode(self.request_line, 'utf-8') + '\n' + inheaders.pretty()
447            return self.simple_response(http_client.OK, msg, close_after_response=False)
448        request_body_file.seek(0)
449        outheaders = MultiDict()
450        data = RequestData(
451            self.method, self.path, self.query, inheaders, request_body_file,
452            outheaders, self.response_protocol, self.static_cache, self.opts,
453            self.remote_addr, self.remote_port, self.is_trusted_ip,
454            self.translator_cache, self.tdir, self.forwarded_for, self.request_original_uri
455        )
456        self.queue_job(self.run_request_handler, data)
457
458    def run_request_handler(self, data):
459        result = self.request_handler(data)
460        return data, result
461
462    def send_range_not_satisfiable(self, content_length):
463        buf = [
464            '%s %d %s' % (
465                self.response_protocol,
466                http_client.REQUESTED_RANGE_NOT_SATISFIABLE,
467                http_client.responses[http_client.REQUESTED_RANGE_NOT_SATISFIABLE]),
468            "Date: " + http_date(),
469            "Content-Range: bytes */%d" % content_length,
470        ]
471        response_data = header_list_to_file(buf)
472        self.log_access(status_code=http_client.REQUESTED_RANGE_NOT_SATISFIABLE, response_size=response_data.sz)
473        self.response_ready(response_data)
474
475    def send_not_modified(self, etag=None):
476        buf = [
477            '%s %d %s' % (self.response_protocol, http_client.NOT_MODIFIED, http_client.responses[http_client.NOT_MODIFIED]),
478            "Content-Length: 0",
479            "Date: " + http_date(),
480        ]
481        if etag is not None:
482            buf.append('ETag: ' + etag)
483        response_data = header_list_to_file(buf)
484        self.log_access(status_code=http_client.NOT_MODIFIED, response_size=response_data.sz)
485        self.response_ready(response_data)
486
487    def report_busy(self):
488        self.simple_response(http_client.SERVICE_UNAVAILABLE)
489
490    def job_done(self, ok, result):
491        if not ok:
492            etype, e, tb = result
493            if isinstance(e, HTTPSimpleResponse):
494                eh = {}
495                if e.location:
496                    eh['Location'] = e.location
497                if e.authenticate:
498                    eh['WWW-Authenticate'] = e.authenticate
499                if e.log:
500                    self.log.warn(e.log)
501                return self.simple_response(e.http_code, msg=error_message(e) or '', close_after_response=e.close_connection, extra_headers=eh)
502            reraise(etype, e, tb)
503
504        data, output = result
505        output = self.finalize_output(output, data, self.method is HTTP1)
506        if output is None:
507            return
508        outheaders = data.outheaders
509
510        outheaders.set('Date', http_date(), replace_all=True)
511        outheaders.set('Server', 'calibre %s' % __version__, replace_all=True)
512        keep_alive = not self.close_after_response and self.opts.timeout > 0
513        if keep_alive:
514            outheaders.set('Keep-Alive', 'timeout=%d' % int(self.opts.timeout))
515        if 'Connection' not in outheaders:
516            if self.response_protocol is HTTP11:
517                if self.close_after_response:
518                    outheaders.set('Connection', 'close')
519            else:
520                if not self.close_after_response:
521                    outheaders.set('Connection', 'Keep-Alive')
522
523        ct = outheaders.get('Content-Type', '')
524        if ct.startswith('text/') and 'charset=' not in ct:
525            outheaders.set('Content-Type', ct + '; charset=UTF-8', replace_all=True)
526
527        buf = [HTTP11 + (' %d ' % data.status_code) + http_client.responses[data.status_code]]
528        for header, value in sorted(iteritems(outheaders), key=itemgetter(0)):
529            buf.append('%s: %s' % (header, value))
530        for morsel in itervalues(data.outcookie):
531            morsel['version'] = '1'
532            x = morsel.output()
533            if isinstance(x, bytes):
534                x = x.decode('ascii')
535            buf.append(x)
536        buf.append('')
537        response_data = ReadOnlyFileBuffer(b''.join((x + '\r\n').encode('ascii') for x in buf))
538        if self.access_log is not None:
539            sz = outheaders.get('Content-Length')
540            if sz is not None:
541                sz = int(sz) + response_data.sz
542            self.log_access(status_code=data.status_code, response_size=sz, username=data.username)
543        self.response_ready(response_data, output=output)
544
545    def log_access(self, status_code, response_size=None, username=None):
546        if self.access_log is None:
547            return
548        if not self.opts.log_not_found and status_code == http_client.NOT_FOUND:
549            return
550        ff = self.forwarded_for
551        if ff:
552            ff = '[%s] ' % ff
553        line = '%s port-%s %s%s %s "%s" %s %s' % (
554            self.remote_addr, self.remote_port, ff or '', username or '-',
555            fast_now_strftime('%d/%b/%Y:%H:%M:%S %z'),
556            force_unicode(self.request_line or '', 'utf-8'),
557            status_code, ('-' if response_size is None else response_size))
558        self.access_log(line)
559
560    def response_ready(self, header_file, output=None):
561        self.response_started = True
562        self.optimize_for_sending_packet()
563        self.use_sendfile = False
564        self.set_state(WRITE, self.write_response_headers, header_file, output)
565
566    def write_response_headers(self, buf, output, event):
567        if self.write(buf):
568            self.write_response_body(output)
569
570    def write_response_body(self, output):
571        if output is None or self.method == 'HEAD':
572            self.reset_state()
573            return
574        if isinstance(output, ReadableOutput):
575            self.use_sendfile = output.use_sendfile and self.opts.use_sendfile and hasattr(os, 'sendfile') and self.ssl_context is None
576            # sendfile() does not work with SSL sockets since encryption has to
577            # be done in userspace
578            if output.ranges is not None:
579                if isinstance(output.ranges, Range):
580                    r = output.ranges
581                    output.src_file.seek(r.start)
582                    self.set_state(WRITE, self.write_buf, output.src_file, end=r.stop + 1)
583                else:
584                    self.set_state(WRITE, self.write_ranges, output.src_file, output.ranges, first=True)
585            else:
586                self.set_state(WRITE, self.write_buf, output.src_file)
587        elif isinstance(output, GeneratedOutput):
588            self.set_state(WRITE, self.write_iter, chain(output.output, repeat(None, 1)))
589        else:
590            raise TypeError('Unknown output type: %r' % output)
591
592    def write_buf(self, buf, event, end=None):
593        if self.write(buf, end=end):
594            self.reset_state()
595
596    def write_ranges(self, buf, ranges, event, first=False):
597        r, range_part = next(ranges)
598        if r is None:
599            # EOF range part
600            self.set_state(WRITE, self.write_buf, ReadOnlyFileBuffer(b'\r\n' + range_part))
601        else:
602            buf.seek(r.start)
603            self.set_state(WRITE, self.write_range_part, ReadOnlyFileBuffer((b'' if first else b'\r\n') + range_part + b'\r\n'), buf, r.stop + 1, ranges)
604
605    def write_range_part(self, part_buf, buf, end, ranges, event):
606        if self.write(part_buf):
607            self.set_state(WRITE, self.write_range, buf, end, ranges)
608
609    def write_range(self, buf, end, ranges, event):
610        if self.write(buf, end=end):
611            self.set_state(WRITE, self.write_ranges, buf, ranges)
612
613    def write_iter(self, output, event):
614        chunk = next(output)
615        if chunk is None:
616            self.set_state(WRITE, self.write_chunk, ReadOnlyFileBuffer(b'0\r\n\r\n'), output, last=True)
617        else:
618            if chunk:
619                if not isinstance(chunk, bytes):
620                    chunk = chunk.encode('utf-8')
621                chunk = ('%X\r\n' % len(chunk)).encode('ascii') + chunk + b'\r\n'
622                self.set_state(WRITE, self.write_chunk, ReadOnlyFileBuffer(chunk), output)
623            else:
624                # Empty chunk, ignore it
625                self.write_iter(output, event)
626
627    def write_chunk(self, buf, output, event, last=False):
628        if self.write(buf):
629            if last:
630                self.reset_state()
631            else:
632                self.set_state(WRITE, self.write_iter, output)
633
634    def reset_state(self):
635        ready = not self.close_after_response
636        self.end_send_optimization()
637        self.connection_ready()
638        self.ready = ready
639
640    def report_unhandled_exception(self, e, formatted_traceback):
641        self.simple_response(http_client.INTERNAL_SERVER_ERROR)
642
643    def finalize_output(self, output, request, is_http1):
644        none_match = parse_if_none_match(request.inheaders.get('If-None-Match', ''))
645        if isinstance(output, ETaggedDynamicOutput):
646            matched = '*' in none_match or (output.etag and output.etag in none_match)
647            if matched:
648                if self.method in ('GET', 'HEAD'):
649                    self.send_not_modified(output.etag)
650                else:
651                    self.simple_response(http_client.PRECONDITION_FAILED)
652                return
653
654        opts = self.opts
655        outheaders = request.outheaders
656        stat_result = file_metadata(output)
657        if stat_result is not None:
658            output = filesystem_file_output(output, outheaders, stat_result)
659            if 'Content-Type' not in outheaders:
660                output_name = output.name
661                if not isinstance(output_name, string_or_bytes):
662                    output_name = str(output_name)
663                mt = guess_type(output_name)[0]
664                if mt:
665                    if mt in {'text/plain', 'text/html', 'application/javascript', 'text/css'}:
666                        mt += '; charset=UTF-8'
667                    outheaders['Content-Type'] = mt
668                else:
669                    outheaders['Content-Type'] = 'application/octet-stream'
670        elif isinstance(output, string_or_bytes):
671            output = dynamic_output(output, outheaders)
672        elif hasattr(output, 'read'):
673            output = ReadableOutput(output)
674        elif isinstance(output, StaticOutput):
675            output = ReadableOutput(ReadOnlyFileBuffer(output.data), etag=output.etag, content_length=output.content_length)
676        elif isinstance(output, ETaggedDynamicOutput):
677            output = dynamic_output(output(), outheaders, etag=output.etag)
678        else:
679            output = GeneratedOutput(output)
680        ct = outheaders.get('Content-Type', '').partition(';')[0]
681        compressible = (not ct or ct.startswith('text/') or ct.startswith('image/svg') or
682                        ct.partition(';')[0] in COMPRESSIBLE_TYPES)
683        compressible = (compressible and request.status_code == http_client.OK and
684                        (opts.compress_min_size > -1 and output.content_length >= opts.compress_min_size) and
685                        acceptable_encoding(request.inheaders.get('Accept-Encoding', '')) and not is_http1)
686        accept_ranges = (not compressible and output.accept_ranges is not None and request.status_code == http_client.OK and
687                        not is_http1)
688        ranges = get_ranges(request.inheaders.get('Range'), output.content_length) if output.accept_ranges and self.method in ('GET', 'HEAD') else None
689        if_range = (request.inheaders.get('If-Range') or '').strip()
690        if if_range and if_range != output.etag:
691            ranges = None
692        if ranges is not None and not ranges:
693            return self.send_range_not_satisfiable(output.content_length)
694
695        for header in ('Accept-Ranges', 'Content-Encoding', 'Transfer-Encoding', 'ETag', 'Content-Length'):
696            outheaders.pop(header, all=True)
697
698        matched = '*' in none_match or (output.etag and output.etag in none_match)
699        if matched:
700            if self.method in ('GET', 'HEAD'):
701                self.send_not_modified(output.etag)
702            else:
703                self.simple_response(http_client.PRECONDITION_FAILED)
704            return
705
706        output.ranges = None
707
708        if output.etag and self.method in ('GET', 'HEAD'):
709            outheaders.set('ETag', output.etag, replace_all=True)
710        if accept_ranges:
711            outheaders.set('Accept-Ranges', 'bytes', replace_all=True)
712        if compressible and not ranges:
713            outheaders.set('Content-Encoding', 'gzip', replace_all=True)
714            if getattr(output, 'content_length', None):
715                outheaders.set('Calibre-Uncompressed-Length', '%d' % output.content_length)
716            output = GeneratedOutput(compress_readable_output(output.src_file), etag=output.etag)
717        if output.content_length is not None and not compressible and not ranges:
718            outheaders.set('Content-Length', '%d' % output.content_length, replace_all=True)
719
720        if compressible or output.content_length is None:
721            outheaders.set('Transfer-Encoding', 'chunked', replace_all=True)
722
723        if ranges:
724            if len(ranges) == 1:
725                r = ranges[0]
726                outheaders.set('Content-Length', '%d' % r.size, replace_all=True)
727                outheaders.set('Content-Range', 'bytes %d-%d/%d' % (r.start, r.stop, output.content_length), replace_all=True)
728                output.ranges = r
729            else:
730                range_parts = get_range_parts(ranges, outheaders.get('Content-Type'), output.content_length)
731                size = sum(map(len, range_parts)) + sum(r.size + 4 for r in ranges)
732                outheaders.set('Content-Length', '%d' % size, replace_all=True)
733                outheaders.set('Content-Type', 'multipart/byteranges; boundary=' + MULTIPART_SEPARATOR, replace_all=True)
734                output.ranges = zip_longest(ranges, range_parts)
735            request.status_code = http_client.PARTIAL_CONTENT
736        return output
737
738
739def create_http_handler(handler=None, websocket_handler=None):
740    from calibre.srv.web_socket import WebSocketConnection
741    static_cache = {}
742    translator_cache = {}
743    if handler is None:
744        def dummy_http_handler(data):
745            return 'Hello'
746        handler = dummy_http_handler
747
748    @wraps(handler)
749    def wrapper(*args, **kwargs):
750        ans = WebSocketConnection(*args, **kwargs)
751        ans.request_handler = handler
752        ans.websocket_handler = websocket_handler
753        ans.static_cache = static_cache
754        ans.translator_cache = translator_cache
755        return ans
756    return wrapper
757