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