mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Add Date header and support If-None-Match
This commit is contained in:
parent
42dd04b854
commit
45b290ad5b
@ -17,3 +17,11 @@ class MaxSizeExceeded(Exception):
|
|||||||
|
|
||||||
def __init__(self, prefix, size, limit):
|
def __init__(self, prefix, size, limit):
|
||||||
Exception.__init__(self, prefix + (' %d > maximum %d' % (size, limit)))
|
Exception.__init__(self, prefix + (' %d > maximum %d' % (size, limit)))
|
||||||
|
|
||||||
|
class HTTP404(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class IfNoneMatch(Exception):
|
||||||
|
def __init__(self, etag=None):
|
||||||
|
Exception.__init__(self, '')
|
||||||
|
self.etag = etag
|
||||||
|
@ -13,9 +13,10 @@ from urllib import unquote
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from calibre import as_unicode
|
from calibre import as_unicode
|
||||||
from calibre.srv.errors import MaxSizeExceeded, NonHTTPConnRequest
|
from calibre.srv.errors import (
|
||||||
|
MaxSizeExceeded, NonHTTPConnRequest, HTTP404, IfNoneMatch)
|
||||||
from calibre.srv.respond import finalize_output, generate_static_output
|
from calibre.srv.respond import finalize_output, generate_static_output
|
||||||
from calibre.srv.utils import MultiDict
|
from calibre.srv.utils import MultiDict, http_date
|
||||||
|
|
||||||
HTTP1 = 'HTTP/1.0'
|
HTTP1 = 'HTTP/1.0'
|
||||||
HTTP11 = 'HTTP/1.1'
|
HTTP11 = 'HTTP/1.1'
|
||||||
@ -436,7 +437,7 @@ class HTTPPair(object):
|
|||||||
self.flushed_write(msg.encode('ascii'))
|
self.flushed_write(msg.encode('ascii'))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def simple_response(self, status_code, msg=""):
|
def simple_response(self, status_code, msg="", read_remaining_input=False):
|
||||||
abort = status_code in (httplib.REQUEST_ENTITY_TOO_LARGE, httplib.REQUEST_URI_TOO_LONG)
|
abort = status_code in (httplib.REQUEST_ENTITY_TOO_LARGE, httplib.REQUEST_URI_TOO_LONG)
|
||||||
if abort:
|
if abort:
|
||||||
self.close_connection = True
|
self.close_connection = True
|
||||||
@ -448,13 +449,32 @@ class HTTPPair(object):
|
|||||||
buf = [
|
buf = [
|
||||||
'%s %d %s' % (self.reponse_protocol, status_code, httplib.responses[status_code]),
|
'%s %d %s' % (self.reponse_protocol, status_code, httplib.responses[status_code]),
|
||||||
"Content-Length: %s" % len(msg),
|
"Content-Length: %s" % len(msg),
|
||||||
"Content-Type: text/plain; charset=UTF-8"
|
"Content-Type: text/plain; charset=UTF-8",
|
||||||
|
"Date: " + http_date(),
|
||||||
]
|
]
|
||||||
if abort and self.reponse_protocol is HTTP11:
|
if abort and self.reponse_protocol is HTTP11:
|
||||||
buf.append("Connection: close")
|
buf.append("Connection: close")
|
||||||
buf.append('')
|
buf.append('')
|
||||||
buf = [(x + '\r\n').encode('ascii') for x in buf]
|
buf = [(x + '\r\n').encode('ascii') for x in buf]
|
||||||
buf.append(msg)
|
buf.append(msg)
|
||||||
|
if read_remaining_input:
|
||||||
|
self.input_reader.read()
|
||||||
|
self.flushed_write(b''.join(buf))
|
||||||
|
|
||||||
|
def send_not_modified(self, etag=None):
|
||||||
|
buf = [
|
||||||
|
'%s %d %s' % (self.reponse_protocol, httplib.NOT_MODIFIED, httplib.responses[httplib.NOT_MODIFIED]),
|
||||||
|
"Content-Length: 0",
|
||||||
|
"Date: " + http_date(),
|
||||||
|
]
|
||||||
|
if etag is not None:
|
||||||
|
buf.append('ETag: ' + etag)
|
||||||
|
for header in ('Expires', 'Cache-Control', 'Vary'):
|
||||||
|
val = self.outheaders.get(header)
|
||||||
|
if val:
|
||||||
|
buf.append(header + ': ' + val)
|
||||||
|
buf.append('')
|
||||||
|
buf = [(x + '\r\n').encode('ascii') for x in buf]
|
||||||
self.flushed_write(b''.join(buf))
|
self.flushed_write(b''.join(buf))
|
||||||
|
|
||||||
def flushed_write(self, data):
|
def flushed_write(self, data):
|
||||||
@ -475,13 +495,24 @@ class HTTPPair(object):
|
|||||||
else:
|
else:
|
||||||
self.input_reader = FixedSizeReader(self.conn.socket_file, self.request_content_length)
|
self.input_reader = FixedSizeReader(self.conn.socket_file, self.request_content_length)
|
||||||
|
|
||||||
output = self.handle_request(self)
|
try:
|
||||||
if self.status_code is None:
|
output = self.handle_request(self)
|
||||||
raise Exception('Request handler did not set status_code')
|
except HTTP404 as e:
|
||||||
|
self.simple_response(httplib.NOT_FOUND, e.message, read_remaining_input=True)
|
||||||
|
return
|
||||||
# Read and discard any remaining body from the HTTP request
|
# Read and discard any remaining body from the HTTP request
|
||||||
self.input_reader.read()
|
self.input_reader.read()
|
||||||
|
if self.status_code is None:
|
||||||
|
raise Exception('Request handler did not set status_code')
|
||||||
|
|
||||||
self.status_code, output = finalize_output(output, self.inheaders, self.outheaders, self.status_code)
|
try:
|
||||||
|
self.status_code, output = finalize_output(output, self.inheaders, self.outheaders, self.status_code, self.response_protocol is HTTP1, self.method)
|
||||||
|
except IfNoneMatch as e:
|
||||||
|
if self.method in ('GET', 'HEAD'):
|
||||||
|
self.send_not_modified(e.etag)
|
||||||
|
else:
|
||||||
|
self.simple_response(httplib.PRECONDITION_FAILED)
|
||||||
|
return
|
||||||
|
|
||||||
self.send_headers()
|
self.send_headers()
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import os, hashlib, shutil, httplib, zlib, struct, time
|
|||||||
from io import DEFAULT_BUFFER_SIZE, BytesIO
|
from io import DEFAULT_BUFFER_SIZE, BytesIO
|
||||||
|
|
||||||
from calibre import force_unicode
|
from calibre import force_unicode
|
||||||
|
from calibre.srv.errors import IfNoneMatch
|
||||||
|
|
||||||
def acceptable_encoding(val, allowed=frozenset({'gzip'})):
|
def acceptable_encoding(val, allowed=frozenset({'gzip'})):
|
||||||
def enc(x):
|
def enc(x):
|
||||||
@ -75,7 +76,7 @@ class FileSystemOutputFile(object):
|
|||||||
pos = output.tell()
|
pos = output.tell()
|
||||||
output.seek(0, os.SEEK_END)
|
output.seek(0, os.SEEK_END)
|
||||||
self.content_length = output.tell() - pos
|
self.content_length = output.tell() - pos
|
||||||
self.etag = hashlib.sha1(force_unicode(output.name or '') + str(os.fstat(output.fileno()).st_mtime)).hexdigest()
|
self.etag = hashlib.sha1(type('')(os.fstat(output.fileno()).st_mtime) + force_unicode(output.name or '')).hexdigest()
|
||||||
output.seek(pos)
|
output.seek(pos)
|
||||||
self.accept_ranges = True
|
self.accept_ranges = True
|
||||||
|
|
||||||
@ -141,7 +142,10 @@ def generate_static_output(cache, gso_lock, name, generator):
|
|||||||
ans = cache[name] = StaticGeneratedOutput(generator())
|
ans = cache[name] = StaticGeneratedOutput(generator())
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def finalize_output(output, inheaders, outheaders, status_code):
|
def parse_if_none_match(val):
|
||||||
|
return {x.strip() for x in val.split(',')}
|
||||||
|
|
||||||
|
def finalize_output(output, inheaders, outheaders, status_code, is_http1, method):
|
||||||
ct = outheaders.get('Content-Type', '')
|
ct = outheaders.get('Content-Type', '')
|
||||||
compressible = not ct or ct.startswith('text/') or ct.startswith('image/svg') or ct.startswith('application/json')
|
compressible = not ct or ct.startswith('text/') or ct.startswith('image/svg') or ct.startswith('application/json')
|
||||||
if isinstance(output, file):
|
if isinstance(output, file):
|
||||||
@ -153,20 +157,29 @@ def finalize_output(output, inheaders, outheaders, status_code):
|
|||||||
else:
|
else:
|
||||||
output = GeneratedOutput(output, outheaders)
|
output = GeneratedOutput(output, outheaders)
|
||||||
compressible = (status_code == httplib.OK and compressible and output.content_length > 1024 and
|
compressible = (status_code == httplib.OK and compressible and output.content_length > 1024 and
|
||||||
acceptable_encoding(inheaders.get('Accept-Encoding', '')))
|
acceptable_encoding(inheaders.get('Accept-Encoding', '')) and not is_http1)
|
||||||
accept_ranges = not compressible and output.accept_ranges is not None and status_code == httplib.OK
|
accept_ranges = (not compressible and output.accept_ranges is not None and status_code == httplib.OK and
|
||||||
|
not is_http1)
|
||||||
|
ranges = None
|
||||||
|
|
||||||
for header in 'Accept-Ranges Content-Encoding Transfer-Encoding ETag'.split():
|
for header in ('Accept-Ranges', 'Content-Encoding', 'Transfer-Encoding', 'ETag', 'Content-Length'):
|
||||||
outheaders.pop('header', all=True)
|
outheaders.pop('header', all=True)
|
||||||
|
|
||||||
# TODO: If-None-Match, Ranges, If-Range
|
none_match = parse_if_none_match(inheaders.get('If-None-Match', ''))
|
||||||
|
matched = '*' in none_match or (output.etag and output.etag in none_match)
|
||||||
|
if matched:
|
||||||
|
raise IfNoneMatch(output.etag)
|
||||||
|
|
||||||
if output.etag:
|
# TODO: Ranges, If-Range
|
||||||
|
|
||||||
|
if output.etag and method in ('GET', 'HEAD'):
|
||||||
outheaders.set('ETag', output.etag, replace=True)
|
outheaders.set('ETag', output.etag, replace=True)
|
||||||
if accept_ranges:
|
if accept_ranges:
|
||||||
outheaders.set('Accept-Ranges', 'bytes', replace=True)
|
outheaders.set('Accept-Ranges', 'bytes', replace=True)
|
||||||
elif compressible:
|
elif compressible:
|
||||||
outheaders.set('Content-Encoding', 'gzip', replace=True)
|
outheaders.set('Content-Encoding', 'gzip', replace=True)
|
||||||
|
if output.content_length is not None and not compressible and not ranges:
|
||||||
|
outheaders.set('Content-Length', '%d' % output.content_length, replace=True)
|
||||||
|
|
||||||
if compressible or output.content_length is None:
|
if compressible or output.content_length is None:
|
||||||
outheaders.set('Transfer-Encoding', 'chunked', replace=True)
|
outheaders.set('Transfer-Encoding', 'chunked', replace=True)
|
||||||
|
@ -8,8 +8,12 @@ __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
from urlparse import parse_qs
|
from urlparse import parse_qs
|
||||||
import repr as reprlib
|
import repr as reprlib
|
||||||
|
from email.utils import formatdate
|
||||||
|
|
||||||
class MultiDict(dict):
|
def http_date(timeval=None):
|
||||||
|
return type('')(formatdate(timeval=timeval, usegmt=True))
|
||||||
|
|
||||||
|
class MultiDict(dict): # {{{
|
||||||
|
|
||||||
def __setitem__(self, key, val):
|
def __setitem__(self, key, val):
|
||||||
vals = dict.get(self, key, [])
|
vals = dict.get(self, key, [])
|
||||||
@ -75,3 +79,4 @@ class MultiDict(dict):
|
|||||||
|
|
||||||
def pretty(self, leading_whitespace=''):
|
def pretty(self, leading_whitespace=''):
|
||||||
return leading_whitespace + ('\n' + leading_whitespace).join('%s: %s' % (k, v) for k, v in self.items())
|
return leading_whitespace + ('\n' + leading_whitespace).join('%s: %s' % (k, v) for k, v in self.items())
|
||||||
|
# }}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user