%PDF- %PDF-
| Direktori : /lib/calibre/calibre/srv/tests/ |
| Current File : //lib/calibre/calibre/srv/tests/http.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import hashlib, zlib, string, time, os
from io import BytesIO
from tempfile import NamedTemporaryFile
from calibre import guess_type
from calibre.srv.tests.base import BaseTest, TestServer
from calibre.srv.utils import eintr_retry_call
from calibre.utils.monotonic import monotonic
from polyglot.builtins import iteritems
from polyglot import http_client
is_ci = os.environ.get('CI', '').lower() == 'true'
class TestHTTP(BaseTest):
def test_header_parsing(self): # {{{
'Test parsing of HTTP headers'
from calibre.srv.http_request import HTTPHeaderParser
def test(name, *lines, **kwargs):
p = HTTPHeaderParser()
p.push(*lines)
self.assertTrue(p.finished)
self.assertSetEqual(set(p.hdict.items()), {(k.replace('_', '-').title(), v) for k, v in iteritems(kwargs)}, name + ' failed')
test('Continuation line parsing',
b'a: one',
b'b: two',
b' 2',
b'\t3',
b'c:three',
b'\r\n', a='one', b='two 2 3', c='three')
test('Non-ascii headers parsing',
'a:mūs\r'.encode(), b'\r\n', a='mūs')
test('Comma-separated parsing',
b'Accept-Encoding: one',
b'accept-Encoding: two',
b'\r\n', accept_encoding='one, two')
def parse(*lines):
lines = list(lines)
lines.append(b'\r\n')
self.assertRaises(ValueError, HTTPHeaderParser().push, *lines)
parse('Connection:mūs\r\n'.encode('utf-16'))
parse(b'Connection\r\n')
parse(b'Connection:a\r\n', b'\r\n')
parse(b' Connection:a\n')
parse(b':a\n')
# }}}
def test_accept_encoding(self): # {{{
'Test parsing of Accept-Encoding'
from calibre.srv.http_response import acceptable_encoding
def test(name, val, ans, allowed={'gzip'}):
self.ae(acceptable_encoding(val, allowed), ans, name + ' failed')
test('Empty field', '', None)
test('Simple', 'gzip', 'gzip')
test('Case insensitive', 'GZIp', 'gzip')
test('Multiple', 'gzip, identity', 'gzip')
test('Priority', '1;q=0.5, 2;q=0.75, 3;q=1.0', '3', {'1', '2', '3'})
# }}}
def test_accept_language(self): # {{{
'Test parsing of Accept-Language'
from calibre.srv.http_response import preferred_lang
def test(name, val, ans):
self.ae(preferred_lang(val, lambda x:(True, x, None)), ans, name + ' failed')
test('Empty field', '', 'en')
test('Simple', 'de', 'de')
test('Case insensitive', 'Es', 'es')
test('Multiple', 'fr, es', 'fr')
test('Priority', 'en;q=0.1, de;q=0.7, fr;q=0.5', 'de')
try:
self.do_accept_language()
except Exception:
# this test is flaky on the Linux CI machines
time.sleep(3)
self.do_accept_language()
def do_accept_language(self):
from calibre.utils.localization import get_translator
def handler(data):
return data.lang_code + data._('Unknown')
with TestServer(handler, timeout=100) as server:
conn = server.connect()
def test(al, q):
conn.request('GET', '/', headers={'Accept-Language': al})
r = conn.getresponse()
self.ae(r.status, http_client.OK)
q += get_translator(q)[-1].gettext('Unknown')
self.ae(r.read(), q.encode('utf-8'))
test('en', 'en')
test('eng', 'en')
test('es', 'es')
# }}}
def test_range_parsing(self): # {{{
'Test parsing of Range header'
from calibre.srv.http_response import get_ranges
def test(val, *args):
pval = get_ranges(val, 100)
if len(args) == 1 and args[0] is None:
self.assertIsNone(pval, val)
else:
self.assertListEqual([tuple(x) for x in pval], list(args), val)
test('crap', None)
test('crap=', None)
test('crap=1', None)
test('crap=1-2', None)
test('bytes=a-2')
test('bytes=0-99', (0, 99, 100))
test('bytes=0-0,-1', (0, 0, 1), (99, 99, 1))
test('bytes=-5', (95, 99, 5))
test('bytes=95-', (95, 99, 5))
test('bytes=-200', (0, 99, 100))
# }}}
def test_http_basic(self): # {{{
'Test basic HTTP protocol conformance'
try:
self.do_http_basic()
except Exception:
# this test is a little flaky on the windows CI machine.
time.sleep(3)
self.do_http_basic()
def do_http_basic(self):
from calibre.srv.errors import HTTPNotFound, HTTPRedirect
body = 'Requested resource not found'
def handler(data):
raise HTTPNotFound(body)
def raw_send(conn, raw):
conn.send(raw)
conn._HTTPConnection__state = http_client._CS_REQ_SENT
return conn.getresponse()
base_timeout = 50 if is_ci else 10
with TestServer(handler, timeout=base_timeout, max_header_line_size=100./1024, max_request_body_size=100./(1024*1024)) as server:
conn = server.connect()
r = raw_send(conn, b'hello\n')
self.ae(r.status, http_client.BAD_REQUEST)
self.ae(r.read(), b'HTTP requires CRLF line terminators')
r = raw_send(conn, b'\r\nGET /index.html HTTP/1.1\r\n\r\n')
self.ae(r.status, http_client.NOT_FOUND), self.ae(r.read(), b'Requested resource not found')
r = raw_send(conn, b'\r\n\r\nGET /index.html HTTP/1.1\r\n\r\n')
self.ae(r.status, http_client.BAD_REQUEST)
self.ae(r.read(), b'Multiple leading empty lines not allowed')
r = raw_send(conn, b'hello world\r\n')
self.ae(r.status, http_client.BAD_REQUEST)
self.ae(r.read(), b'Malformed Request-Line')
r = raw_send(conn, b'x' * 200)
self.ae(r.status, http_client.BAD_REQUEST)
self.ae(r.read(), b'')
r = raw_send(conn, b'XXX /index.html HTTP/1.1\r\n\r\n')
self.ae(r.status, http_client.BAD_REQUEST), self.ae(r.read(), b'Unknown HTTP method')
# Test 404
conn.request('HEAD', '/moose')
r = conn.getresponse()
self.ae(r.status, http_client.NOT_FOUND)
self.assertIsNotNone(r.getheader('Date', None))
self.ae(r.getheader('Content-Length'), str(len(body)))
self.ae(r.getheader('Content-Type'), 'text/plain; charset=UTF-8')
self.ae(len(r.getheaders()), 3)
self.ae(r.read(), b'')
conn.request('GET', '/choose')
r = conn.getresponse()
self.ae(r.status, http_client.NOT_FOUND)
self.ae(r.read(), b'Requested resource not found')
# Test 500
server.change_handler(lambda data:1/0)
conn = server.connect()
conn.request('GET', '/test/')
r = conn.getresponse()
self.ae(r.status, http_client.INTERNAL_SERVER_ERROR)
# Test 301
def handler(data):
raise HTTPRedirect('/somewhere-else')
server.change_handler(handler)
conn = server.connect()
conn.request('GET', '/')
r = conn.getresponse()
self.ae(r.status, http_client.MOVED_PERMANENTLY)
self.ae(r.getheader('Location'), '/somewhere-else')
self.ae(b'', r.read())
server.change_handler(lambda data:data.path[0] + data.read().decode('ascii'))
conn = server.connect(timeout=base_timeout * 5)
# Test simple GET
conn.request('GET', '/test/')
r = conn.getresponse()
self.ae(r.status, http_client.OK)
self.ae(r.read(), b'test')
# Test TRACE
lines = ['TRACE /xxx HTTP/1.1', 'Test: value', 'Xyz: abc, def', '', '']
r = raw_send(conn, ('\r\n'.join(lines)).encode('ascii'))
self.ae(r.status, http_client.OK)
self.ae(r.read().decode('utf-8'), '\n'.join(lines[:-2]))
# Test POST with simple body
conn.request('POST', '/test', 'body')
r = conn.getresponse()
self.ae(r.status, http_client.OK)
self.ae(r.read(), b'testbody')
# Test POST with chunked transfer encoding
conn.request('POST', '/test', headers={'Transfer-Encoding': 'chunked'})
conn.send(b'4\r\nbody\r\na\r\n1234567890\r\n0\r\n\r\n')
r = conn.getresponse()
self.ae(r.status, http_client.OK)
self.ae(r.read(), b'testbody1234567890')
conn.request('GET', '/test' + ('a' * 200))
r = conn.getresponse()
self.ae(r.status, http_client.BAD_REQUEST)
conn = server.connect()
conn.request('GET', '/test', ('a' * 200))
r = conn.getresponse()
self.ae(r.status, http_client.REQUEST_ENTITY_TOO_LARGE)
conn = server.connect()
conn.request('POST', '/test', headers={'Transfer-Encoding': 'chunked'})
conn.send(b'x\r\nbody\r\n0\r\n\r\n')
r = conn.getresponse()
self.ae(r.status, http_client.BAD_REQUEST)
self.assertIn(b'not a valid chunk size', r.read())
conn.request('POST', '/test', headers={'Transfer-Encoding': 'chunked'})
conn.send(b'4\r\nbody\r\n200\r\n\r\n')
r = conn.getresponse()
self.ae(r.status, http_client.REQUEST_ENTITY_TOO_LARGE)
conn.request('POST', '/test', body='a'*200)
r = conn.getresponse()
self.ae(r.status, http_client.REQUEST_ENTITY_TOO_LARGE)
conn = server.connect()
conn.request('POST', '/test', headers={'Transfer-Encoding': 'chunked'})
conn.send(b'3\r\nbody\r\n0\r\n\r\n')
r = conn.getresponse()
self.ae(r.status, http_client.BAD_REQUEST), self.ae(r.read(), b'Chunk does not have trailing CRLF')
conn = server.connect(timeout=base_timeout * 5)
conn.request('POST', '/test', headers={'Transfer-Encoding': 'chunked'})
conn.send(b'30\r\nbody\r\n0\r\n\r\n')
r = conn.getresponse()
self.ae(r.status, http_client.REQUEST_TIMEOUT)
self.assertIn(b'', r.read())
conn = server.connect()
# Test closing
server.loop.opts.timeout = 1000 # ensure socket is not closed because of timeout
conn.request('GET', '/close', headers={'Connection':'close'})
r = conn.getresponse()
self.ae(r.status, 200), self.ae(r.read(), b'close')
server.loop.wakeup()
num = 10
while num and server.loop.num_active_connections != 0:
time.sleep(0.01)
num -= 1
self.ae(server.loop.num_active_connections, 0)
self.assertIsNone(conn.sock)
# Test timeout
server.loop.opts.timeout = 10
conn = server.connect(timeout=100)
conn.request('GET', '/something')
r = conn.getresponse()
self.ae(r.status, 200), self.ae(r.read(), b'something')
self.assertIn(b'Request Timeout', eintr_retry_call(conn.sock.recv, 500))
# }}}
def test_http_response(self): # {{{
'Test HTTP protocol responses'
from calibre.srv.http_response import parse_multipart_byterange
def handler(conn):
return conn.generate_static_output('test', lambda : ''.join(conn.path))
with NamedTemporaryFile(suffix='test.epub') as f, open(P('localization/locales.zip'), 'rb') as lf, \
TestServer(handler, timeout=100, compress_min_size=0) as server:
fdata = (string.ascii_letters * 100).encode('ascii')
f.write(fdata), f.seek(0)
# Test ETag
conn = server.connect()
conn.request('GET', '/an_etagged_path')
r = conn.getresponse()
self.ae(r.status, http_client.OK), self.ae(r.read(), b'an_etagged_path')
etag = r.getheader('ETag')
self.ae(etag, '"%s"' % hashlib.sha1(b'an_etagged_path').hexdigest())
conn.request('GET', '/an_etagged_path', headers={'If-None-Match':etag})
r = conn.getresponse()
self.ae(r.status, http_client.NOT_MODIFIED)
self.ae(r.read(), b'')
# Test gzip
raw = b'a'*20000
server.change_handler(lambda conn: raw)
conn = server.connect()
conn.request('GET', '/an_etagged_path', headers={'Accept-Encoding':'gzip'})
r = conn.getresponse()
self.ae(str(len(raw)), r.getheader('Calibre-Uncompressed-Length'))
self.ae(r.status, http_client.OK), self.ae(zlib.decompress(r.read(), 16+zlib.MAX_WBITS), raw)
# Test dynamic etagged content
num_calls = [0]
def edfunc():
num_calls[0] += 1
return b'data'
server.change_handler(lambda conn:conn.etagged_dynamic_response("xxx", edfunc))
conn = server.connect()
conn.request('GET', '/an_etagged_path')
r = conn.getresponse()
self.ae(r.status, http_client.OK), self.ae(r.read(), b'data')
etag = r.getheader('ETag')
self.ae(etag, '"xxx"')
self.ae(r.getheader('Content-Length'), '4')
conn.request('GET', '/an_etagged_path', headers={'If-None-Match':etag})
r = conn.getresponse()
self.ae(r.status, http_client.NOT_MODIFIED)
self.ae(r.read(), b'')
self.ae(num_calls[0], 1)
# Test getting a filesystem file
for use_sendfile in (True, False):
server.change_handler(lambda conn: f)
server.loop.opts.use_sendfile = use_sendfile
conn = server.connect()
conn.request('GET', '/test')
r = conn.getresponse()
etag = str(r.getheader('ETag'))
self.assertTrue(etag)
self.ae(r.getheader('Content-Type'), guess_type(f.name)[0])
self.ae(str(r.getheader('Accept-Ranges')), 'bytes')
self.ae(int(r.getheader('Content-Length')), len(fdata))
self.ae(r.status, http_client.OK), self.ae(r.read(), fdata)
conn.request('GET', '/test', headers={'Range':'bytes=2-25'})
r = conn.getresponse()
self.ae(r.status, http_client.PARTIAL_CONTENT)
self.ae(str(r.getheader('Accept-Ranges')), 'bytes')
self.ae(str(r.getheader('Content-Range')), 'bytes 2-25/%d' % len(fdata))
self.ae(int(r.getheader('Content-Length')), 24)
self.ae(r.read(), fdata[2:26])
conn.request('GET', '/test', headers={'Range':'bytes=100000-'})
r = conn.getresponse()
self.ae(r.status, http_client.REQUESTED_RANGE_NOT_SATISFIABLE)
self.ae(str(r.getheader('Content-Range')), 'bytes */%d' % len(fdata))
conn.request('GET', '/test', headers={'Range':'bytes=25-50', 'If-Range':etag})
r = conn.getresponse()
self.ae(r.status, http_client.PARTIAL_CONTENT), self.ae(r.read(), fdata[25:51])
self.ae(int(r.getheader('Content-Length')), 26)
conn.request('GET', '/test', headers={'Range':'bytes=0-1000000'})
r = conn.getresponse()
self.ae(r.status, http_client.PARTIAL_CONTENT), self.ae(r.read(), fdata)
conn.request('GET', '/test', headers={'Range':'bytes=25-50', 'If-Range':'"nomatch"'})
r = conn.getresponse()
self.ae(r.status, http_client.OK), self.ae(r.read(), fdata)
self.assertFalse(r.getheader('Content-Range'))
self.ae(int(r.getheader('Content-Length')), len(fdata))
conn.request('GET', '/test', headers={'Range':'bytes=0-25,26-50'})
r = conn.getresponse()
self.ae(r.status, http_client.PARTIAL_CONTENT)
clen = int(r.getheader('Content-Length'))
data = r.read()
self.ae(clen, len(data))
buf = BytesIO(data)
self.ae(parse_multipart_byterange(buf, r.getheader('Content-Type')), [(0, fdata[:26]), (26, fdata[26:51])])
# Test sending of larger file
start_time = monotonic()
lf.seek(0)
data = lf.read()
server.change_handler(lambda conn: lf)
conn = server.connect(timeout=100)
conn.request('GET', '/test')
r = conn.getresponse()
self.ae(r.status, http_client.OK)
rdata = r.read()
self.ae(len(data), len(rdata))
self.ae(hashlib.sha1(data).hexdigest(), hashlib.sha1(rdata).hexdigest())
self.ae(data, rdata)
time_taken = monotonic() - start_time
self.assertLess(time_taken, 1, 'Large file transfer took too long')
# }}}
def test_static_generation(self): # {{{
'Test static generation'
nums = list(map(str, range(10)))
def handler(conn):
return conn.generate_static_output('test', nums.pop)
with TestServer(handler) as server:
conn = server.connect()
conn.request('GET', '/an_etagged_path')
r = conn.getresponse()
data = r.read()
for i in range(5):
conn.request('GET', '/an_etagged_path')
r = conn.getresponse()
self.assertEqual(data, r.read())
# }}}