finish netlib.http.http1 refactor

This commit is contained in:
Maximilian Hils 2015-09-16 00:04:23 +02:00
parent 11e7f476bd
commit a077d8877d
17 changed files with 639 additions and 727 deletions

View File

@ -1,7 +1,9 @@
from .models import Request, Response, Headers, CONTENT_MISSING
from .models import Request, Response, Headers
from .models import HDR_FORM_MULTIPART, HDR_FORM_URLENCODED, CONTENT_MISSING
from . import http1, http2
__all__ = [
"Request", "Response", "Headers", "CONTENT_MISSING"
"Request", "Response", "Headers",
"HDR_FORM_MULTIPART", "HDR_FORM_URLENCODED", "CONTENT_MISSING",
"http1", "http2"
]

View File

@ -1,7 +1,7 @@
from .read import (
read_request, read_request_head,
read_response, read_response_head,
read_message_body, read_message_body_chunked,
read_body,
connection_close,
expected_http_body_size,
)
@ -14,7 +14,7 @@ from .assemble import (
__all__ = [
"read_request", "read_request_head",
"read_response", "read_response_head",
"read_message_body", "read_message_body_chunked",
"read_body",
"connection_close",
"expected_http_body_size",
"assemble_request", "assemble_request_head",

View File

@ -31,8 +31,6 @@ def assemble_response_head(response):
return b"%s\r\n%s\r\n" % (first_line, headers)
def _assemble_request_line(request, form=None):
if form is None:
form = request.form_out
@ -50,7 +48,7 @@ def _assemble_request_line(request, form=None):
request.httpversion
)
elif form == "absolute":
return b"%s %s://%s:%s%s %s" % (
return b"%s %s://%s:%d%s %s" % (
request.method,
request.scheme,
request.host,
@ -78,11 +76,11 @@ def _assemble_request_headers(request):
if request.body or request.body == b"":
headers[b"Content-Length"] = str(len(request.body)).encode("ascii")
return str(headers)
return bytes(headers)
def _assemble_response_line(response):
return b"%s %s %s" % (
return b"%s %d %s" % (
response.httpversion,
response.status_code,
response.msg,

View File

@ -7,12 +7,13 @@ from ... import utils
from ...exceptions import HttpReadDisconnect, HttpSyntaxException, HttpException
from .. import Request, Response, Headers
ALPN_PROTO_HTTP1 = 'http/1.1'
ALPN_PROTO_HTTP1 = b'http/1.1'
def read_request(rfile, body_size_limit=None):
request = read_request_head(rfile)
request.body = read_message_body(rfile, request, limit=body_size_limit)
expected_body_size = expected_http_body_size(request)
request.body = b"".join(read_body(rfile, expected_body_size, limit=body_size_limit))
request.timestamp_end = time.time()
return request
@ -23,15 +24,14 @@ def read_request_head(rfile):
Args:
rfile: The input stream
body_size_limit (bool): Maximum body size
Returns:
The HTTP request object
The HTTP request object (without body)
Raises:
HttpReadDisconnect: If no bytes can be read from rfile.
HttpSyntaxException: If the input is invalid.
HttpException: A different error occured.
HttpReadDisconnect: No bytes can be read from rfile.
HttpSyntaxException: The input is malformed HTTP.
HttpException: Any other error occured.
"""
timestamp_start = time.time()
if hasattr(rfile, "reset_timestamps"):
@ -51,12 +51,28 @@ def read_request_head(rfile):
def read_response(rfile, request, body_size_limit=None):
response = read_response_head(rfile)
response.body = read_message_body(rfile, request, response, body_size_limit)
expected_body_size = expected_http_body_size(request, response)
response.body = b"".join(read_body(rfile, expected_body_size, body_size_limit))
response.timestamp_end = time.time()
return response
def read_response_head(rfile):
"""
Parse an HTTP response head (response line + headers) from an input stream
Args:
rfile: The input stream
Returns:
The HTTP request object (without body)
Raises:
HttpReadDisconnect: No bytes can be read from rfile.
HttpSyntaxException: The input is malformed HTTP.
HttpException: Any other error occured.
"""
timestamp_start = time.time()
if hasattr(rfile, "reset_timestamps"):
rfile.reset_timestamps()
@ -68,50 +84,33 @@ def read_response_head(rfile):
# more accurate timestamp_start
timestamp_start = rfile.first_byte_timestamp
return Response(
http_version,
status_code,
message,
headers,
None,
timestamp_start
)
return Response(http_version, status_code, message, headers, None, timestamp_start)
def read_message_body(*args, **kwargs):
chunks = read_message_body_chunked(*args, **kwargs)
return b"".join(chunks)
def read_message_body_chunked(rfile, request, response=None, limit=None, max_chunk_size=None):
def read_body(rfile, expected_size, limit=None, max_chunk_size=4096):
"""
Read an HTTP message body:
Read an HTTP message body
Args:
If a request body should be read, only request should be passed.
If a response body should be read, both request and response should be passed.
rfile: The input stream
expected_size: The expected body size (see :py:meth:`expected_body_size`)
limit: Maximum body size
max_chunk_size: Maximium chunk size that gets yielded
Returns:
A generator that yields byte chunks of the content.
Raises:
HttpException
"""
if not response:
headers = request.headers
response_code = None
is_request = True
else:
headers = response.headers
response_code = response.status_code
is_request = False
HttpException, if an error occurs
Caveats:
max_chunk_size is not considered if the transfer encoding is chunked.
"""
if not limit or limit < 0:
limit = sys.maxsize
if not max_chunk_size:
max_chunk_size = limit
expected_size = expected_http_body_size(
headers, is_request, request.method, response_code
)
if expected_size is None:
for x in _read_chunked(rfile, limit):
yield x
@ -125,6 +124,8 @@ def read_message_body_chunked(rfile, request, response=None, limit=None, max_chu
while bytes_left:
chunk_size = min(bytes_left, max_chunk_size)
content = rfile.read(chunk_size)
if len(content) < chunk_size:
raise HttpException("Unexpected EOF")
yield content
bytes_left -= chunk_size
else:
@ -148,10 +149,10 @@ def connection_close(http_version, headers):
"""
# At first, check if we have an explicit Connection header.
if b"connection" in headers:
toks = utils.get_header_tokens(headers, "connection")
if b"close" in toks:
tokens = utils.get_header_tokens(headers, "connection")
if b"close" in tokens:
return True
elif b"keep-alive" in toks:
elif b"keep-alive" in tokens:
return False
# If we don't have a Connection header, HTTP 1.1 connections are assumed to
@ -159,37 +160,41 @@ def connection_close(http_version, headers):
return http_version != (1, 1)
def expected_http_body_size(
headers,
is_request,
request_method,
response_code,
):
def expected_http_body_size(request, response=False):
"""
Returns the expected body length:
- a positive integer, if the size is known in advance
- None, if the size in unknown in advance (chunked encoding)
- -1, if all data should be read until end of stream.
Returns:
The expected body length:
- a positive integer, if the size is known in advance
- None, if the size in unknown in advance (chunked encoding)
- -1, if all data should be read until end of stream.
Raises:
HttpSyntaxException, if the content length header is invalid
"""
# Determine response size according to
# http://tools.ietf.org/html/rfc7230#section-3.3
if request_method:
request_method = request_method.upper()
if not response:
headers = request.headers
response_code = None
is_request = True
else:
headers = response.headers
response_code = response.status_code
is_request = False
is_empty_response = (not is_request and (
request_method == b"HEAD" or
100 <= response_code <= 199 or
(response_code == 200 and request_method == b"CONNECT") or
response_code in (204, 304)
))
if is_request:
if headers.get(b"expect", b"").lower() == b"100-continue":
return 0
else:
if request.method.upper() == b"HEAD":
return 0
if 100 <= response_code <= 199:
return 0
if response_code == 200 and request.method.upper() == b"CONNECT":
return 0
if response_code in (204, 304):
return 0
if is_empty_response:
return 0
if is_request and headers.get(b"expect", b"").lower() == b"100-continue":
return 0
if b"chunked" in headers.get(b"transfer-encoding", b"").lower():
return None
if b"content-length" in headers:
@ -212,18 +217,22 @@ def _get_first_line(rfile):
line = rfile.readline()
if not line:
raise HttpReadDisconnect()
return line
line = line.strip()
try:
line.decode("ascii")
except ValueError:
raise HttpSyntaxException("Non-ascii characters in first line: {}".format(line))
return line.strip()
def _read_request_line(rfile):
line = _get_first_line(rfile)
try:
method, path, http_version = line.strip().split(b" ")
method, path, http_version = line.split(b" ")
if path == b"*" or path.startswith(b"/"):
form = "relative"
path.decode("ascii") # should not raise a ValueError
scheme, host, port = None, None, None
elif method == b"CONNECT":
form = "authority"
@ -233,6 +242,7 @@ def _read_request_line(rfile):
form = "absolute"
scheme, host, port, path = utils.parse_url(path)
_check_http_version(http_version)
except ValueError:
raise HttpSyntaxException("Bad HTTP request line: {}".format(line))
@ -253,7 +263,7 @@ def _parse_authority_form(hostport):
if not utils.is_valid_host(host) or not utils.is_valid_port(port):
raise ValueError()
except ValueError:
raise ValueError("Invalid host specification: {}".format(hostport))
raise HttpSyntaxException("Invalid host specification: {}".format(hostport))
return host, port
@ -263,7 +273,7 @@ def _read_response_line(rfile):
try:
parts = line.strip().split(b" ")
parts = line.split(b" ", 2)
if len(parts) == 2: # handle missing message gracefully
parts.append(b"")
@ -278,7 +288,7 @@ def _read_response_line(rfile):
def _check_http_version(http_version):
if not re.match(rb"^HTTP/\d\.\d$", http_version):
if not re.match(br"^HTTP/\d\.\d$", http_version):
raise HttpSyntaxException("Unknown HTTP version: {}".format(http_version))
@ -313,7 +323,7 @@ def _read_headers(rfile):
return Headers(ret)
def _read_chunked(rfile, limit):
def _read_chunked(rfile, limit=sys.maxsize):
"""
Read a HTTP body with chunked transfer encoding.

View File

@ -4,7 +4,7 @@ import time
from hpack.hpack import Encoder, Decoder
from netlib import http, utils
from netlib.http import semantics
from netlib.http import models as semantics
from . import frame
@ -15,7 +15,7 @@ class TCPHandler(object):
self.wfile = wfile
class HTTP2Protocol(semantics.ProtocolMixin):
class HTTP2Protocol(object):
ERROR_CODES = utils.BiDi(
NO_ERROR=0x0,

View File

@ -1,12 +1,31 @@
import sys
from __future__ import absolute_import, print_function, division
import struct
from hpack.hpack import Encoder, Decoder
from .. import utils
from ...utils import BiDi
from ...exceptions import HttpSyntaxException
class FrameSizeError(Exception):
pass
ERROR_CODES = BiDi(
NO_ERROR=0x0,
PROTOCOL_ERROR=0x1,
INTERNAL_ERROR=0x2,
FLOW_CONTROL_ERROR=0x3,
SETTINGS_TIMEOUT=0x4,
STREAM_CLOSED=0x5,
FRAME_SIZE_ERROR=0x6,
REFUSED_STREAM=0x7,
CANCEL=0x8,
COMPRESSION_ERROR=0x9,
CONNECT_ERROR=0xa,
ENHANCE_YOUR_CALM=0xb,
INADEQUATE_SECURITY=0xc,
HTTP_1_1_REQUIRED=0xd
)
CLIENT_CONNECTION_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
ALPN_PROTO_H2 = b'h2'
class Frame(object):
@ -30,7 +49,9 @@ class Frame(object):
length=0,
flags=FLAG_NO_FLAGS,
stream_id=0x0):
valid_flags = reduce(lambda x, y: x | y, self.VALID_FLAGS, 0x0)
valid_flags = 0
for flag in self.VALID_FLAGS:
valid_flags |= flag
if flags | valid_flags != valid_flags:
raise ValueError('invalid flags detected.')
@ -61,7 +82,7 @@ class Frame(object):
SettingsFrame.SETTINGS.SETTINGS_MAX_FRAME_SIZE]
if length > max_frame_size:
raise FrameSizeError(
raise HttpSyntaxException(
"Frame size exceeded: %d, but only %d allowed." % (
length, max_frame_size))
@ -80,7 +101,7 @@ class Frame(object):
stream_id = fields[4]
if raw_header[:4] == b'HTTP': # pragma no cover
print >> sys.stderr, "WARNING: This looks like an HTTP/1 connection!"
raise HttpSyntaxException("Expected HTTP2 Frame, got HTTP/1 connection")
cls._check_frame_size(length, state)
@ -339,7 +360,7 @@ class SettingsFrame(Frame):
TYPE = 0x4
VALID_FLAGS = [Frame.FLAG_ACK]
SETTINGS = utils.BiDi(
SETTINGS = BiDi(
SETTINGS_HEADER_TABLE_SIZE=0x1,
SETTINGS_ENABLE_PUSH=0x2,
SETTINGS_MAX_CONCURRENT_STREAMS=0x3,
@ -366,7 +387,7 @@ class SettingsFrame(Frame):
def from_bytes(cls, state, length, flags, stream_id, payload):
f = cls(state=state, length=length, flags=flags, stream_id=stream_id)
for i in xrange(0, len(payload), 6):
for i in range(0, len(payload), 6):
identifier, value = struct.unpack("!HL", payload[i:i + 6])
f.settings[identifier] = value

View File

@ -474,7 +474,6 @@ class Response(object):
msg=None,
headers=None,
body=None,
sslinfo=None,
timestamp_start=None,
timestamp_end=None,
):
@ -487,7 +486,6 @@ class Response(object):
self.msg = msg
self.headers = headers
self.body = body
self.sslinfo = sslinfo
self.timestamp_start = timestamp_start
self.timestamp_end = timestamp_end

View File

@ -7,13 +7,15 @@ from contextlib import contextmanager
import six
import sys
from netlib import tcp, utils, http
from . import utils
from .http import Request, Response, Headers
def treader(bytes):
"""
Construct a tcp.Read object from bytes.
"""
from . import tcp # TODO: move to top once cryptography is on Python 3.5
fp = BytesIO(bytes)
return tcp.Reader(fp)
@ -91,55 +93,39 @@ class RaisesContext(object):
test_data = utils.Data(__name__)
def treq(content="content", scheme="http", host="address", port=22):
def treq(**kwargs):
"""
@return: libmproxy.protocol.http.HTTPRequest
Returns:
netlib.http.Request
"""
headers = http.Headers()
headers["header"] = "qvalue"
req = http.Request(
"relative",
"GET",
scheme,
host,
port,
"/path",
(1, 1),
headers,
content,
None,
None,
default = dict(
form_in="relative",
method=b"GET",
scheme=b"http",
host=b"address",
port=22,
path=b"/path",
httpversion=b"HTTP/1.1",
headers=Headers(header=b"qvalue"),
body=b"content"
)
return req
default.update(kwargs)
return Request(**default)
def treq_absolute(content="content"):
def tresp(**kwargs):
"""
@return: libmproxy.protocol.http.HTTPRequest
Returns:
netlib.http.Response
"""
r = treq(content)
r.form_in = r.form_out = "absolute"
r.host = "address"
r.port = 22
r.scheme = "http"
return r
def tresp(content="message"):
"""
@return: libmproxy.protocol.http.HTTPResponse
"""
headers = http.Headers()
headers["header_response"] = "svalue"
resp = http.semantics.Response(
(1, 1),
200,
"OK",
headers,
content,
default = dict(
httpversion=b"HTTP/1.1",
status_code=200,
msg=b"OK",
headers=Headers(header_response=b"svalue"),
body=b"message",
timestamp_start=time.time(),
timestamp_end=time.time(),
timestamp_end=time.time()
)
return resp
default.update(kwargs)
return Response(**default)

View File

@ -40,9 +40,9 @@ def clean_bin(s, keep_spacing=True):
)
else:
if keep_spacing:
keep = b"\n\r\t"
keep = (9, 10, 13) # \t, \n, \r,
else:
keep = b""
keep = ()
return b"".join(
six.int2byte(ch) if (31 < ch < 127 or ch in keep) else b"."
for ch in six.iterbytes(s)
@ -251,7 +251,7 @@ def hostport(scheme, host, port):
if (port, scheme) in [(80, "http"), (443, "https")]:
return host
else:
return b"%s:%s" % (host, port)
return b"%s:%d" % (host, port)
def unparse_url(scheme, host, port, path=""):

View File

@ -0,0 +1,91 @@
from __future__ import absolute_import, print_function, division
from netlib.exceptions import HttpException
from netlib.http import CONTENT_MISSING, Headers
from netlib.http.http1.assemble import (
assemble_request, assemble_request_head, assemble_response,
assemble_response_head, _assemble_request_line, _assemble_request_headers,
_assemble_response_headers
)
from netlib.tutils import treq, raises, tresp
def test_assemble_request():
c = assemble_request(treq()) == (
b"GET /path HTTP/1.1\r\n"
b"header: qvalue\r\n"
b"Host: address:22\r\n"
b"Content-Length: 7\r\n"
b"\r\n"
b"content"
)
with raises(HttpException):
assemble_request(treq(body=CONTENT_MISSING))
def test_assemble_request_head():
c = assemble_request_head(treq())
assert b"GET" in c
assert b"qvalue" in c
assert b"content" not in c
def test_assemble_response():
c = assemble_response(tresp()) == (
b"HTTP/1.1 200 OK\r\n"
b"header-response: svalue\r\n"
b"Content-Length: 7\r\n"
b"\r\n"
b"message"
)
with raises(HttpException):
assemble_response(tresp(body=CONTENT_MISSING))
def test_assemble_response_head():
c = assemble_response_head(tresp())
assert b"200" in c
assert b"svalue" in c
assert b"message" not in c
def test_assemble_request_line():
assert _assemble_request_line(treq()) == b"GET /path HTTP/1.1"
authority_request = treq(method=b"CONNECT", form_in="authority")
assert _assemble_request_line(authority_request) == b"CONNECT address:22 HTTP/1.1"
absolute_request = treq(form_in="absolute")
assert _assemble_request_line(absolute_request) == b"GET http://address:22/path HTTP/1.1"
with raises(RuntimeError):
_assemble_request_line(treq(), "invalid_form")
def test_assemble_request_headers():
# https://github.com/mitmproxy/mitmproxy/issues/186
r = treq(body=b"")
r.headers[b"Transfer-Encoding"] = b"chunked"
c = _assemble_request_headers(r)
assert b"Content-Length" in c
assert b"Transfer-Encoding" not in c
assert b"Host" in _assemble_request_headers(treq(headers=Headers()))
assert b"Proxy-Connection" not in _assemble_request_headers(
treq(headers=Headers(Proxy_Connection="42"))
)
def test_assemble_response_headers():
# https://github.com/mitmproxy/mitmproxy/issues/186
r = tresp(body=b"")
r.headers["Transfer-Encoding"] = b"chunked"
c = _assemble_response_headers(r)
assert b"Content-Length" in c
assert b"Transfer-Encoding" not in c
assert b"Proxy-Connection" not in _assemble_response_headers(
tresp(headers=Headers(Proxy_Connection=b"42"))
)

View File

@ -1,466 +0,0 @@
from io import BytesIO
import textwrap
from http.http1.protocol import _parse_authority_form
from netlib.exceptions import HttpSyntaxException, HttpReadDisconnect, HttpException
from netlib import http, tcp, tutils
from netlib.http import semantics, Headers
from netlib.http.http1 import HTTP1Protocol, read_message_body, read_request, \
read_message_body_chunked, expected_http_body_size
from ... import tservers
class NoContentLengthHTTPHandler(tcp.BaseHandler):
def handle(self):
self.wfile.write("HTTP/1.1 200 OK\r\n\r\nbar\r\n\r\n")
self.wfile.flush()
def mock_protocol(data=''):
rfile = BytesIO(data)
wfile = BytesIO()
return HTTP1Protocol(rfile=rfile, wfile=wfile)
def match_http_string(data):
return textwrap.dedent(data).strip().replace('\n', '\r\n')
def test_stripped_chunked_encoding_no_content():
"""
https://github.com/mitmproxy/mitmproxy/issues/186
"""
r = tutils.treq(content="")
r.headers["Transfer-Encoding"] = "chunked"
assert "Content-Length" in mock_protocol()._assemble_request_headers(r)
r = tutils.tresp(content="")
r.headers["Transfer-Encoding"] = "chunked"
assert "Content-Length" in mock_protocol()._assemble_response_headers(r)
def test_read_chunked():
req = tutils.treq(None)
req.headers["Transfer-Encoding"] = "chunked"
data = b"1\r\na\r\n0\r\n"
with tutils.raises(HttpSyntaxException):
read_message_body(BytesIO(data), req)
data = b"1\r\na\r\n0\r\n\r\n"
assert read_message_body(BytesIO(data), req) == b"a"
data = b"\r\n\r\n1\r\na\r\n1\r\nb\r\n0\r\n\r\n"
assert read_message_body(BytesIO(data), req) == b"ab"
data = b"\r\n"
with tutils.raises("closed prematurely"):
read_message_body(BytesIO(data), req)
data = b"1\r\nfoo"
with tutils.raises("malformed chunked body"):
read_message_body(BytesIO(data), req)
data = b"foo\r\nfoo"
with tutils.raises(HttpSyntaxException):
read_message_body(BytesIO(data), req)
data = b"5\r\naaaaa\r\n0\r\n\r\n"
with tutils.raises("too large"):
read_message_body(BytesIO(data), req, limit=2)
def test_connection_close():
headers = Headers()
assert HTTP1Protocol.connection_close((1, 0), headers)
assert not HTTP1Protocol.connection_close((1, 1), headers)
headers["connection"] = "keep-alive"
assert not HTTP1Protocol.connection_close((1, 1), headers)
headers["connection"] = "close"
assert HTTP1Protocol.connection_close((1, 1), headers)
def test_read_http_body_request():
headers = Headers()
data = "testing"
assert mock_protocol(data).read_http_body(headers, None, "GET", None, True) == ""
def test_read_http_body_response():
headers = Headers()
data = "testing"
assert mock_protocol(data).read_http_body(headers, None, "GET", 200, False) == "testing"
def test_read_http_body():
# test default case
headers = Headers()
headers["content-length"] = "7"
data = "testing"
assert mock_protocol(data).read_http_body(headers, None, "GET", 200, False) == "testing"
# test content length: invalid header
headers["content-length"] = "foo"
data = "testing"
tutils.raises(
http.HttpError,
mock_protocol(data).read_http_body,
headers, None, "GET", 200, False
)
# test content length: invalid header #2
headers["content-length"] = "-1"
data = "testing"
tutils.raises(
http.HttpError,
mock_protocol(data).read_http_body,
headers, None, "GET", 200, False
)
# test content length: content length > actual content
headers["content-length"] = "5"
data = "testing"
tutils.raises(
http.HttpError,
mock_protocol(data).read_http_body,
headers, 4, "GET", 200, False
)
# test content length: content length < actual content
data = "testing"
assert len(mock_protocol(data).read_http_body(headers, None, "GET", 200, False)) == 5
# test no content length: limit > actual content
headers = Headers()
data = "testing"
assert len(mock_protocol(data).read_http_body(headers, 100, "GET", 200, False)) == 7
# test no content length: limit < actual content
data = "testing"
tutils.raises(
http.HttpError,
mock_protocol(data).read_http_body,
headers, 4, "GET", 200, False
)
# test chunked
headers = Headers()
headers["transfer-encoding"] = "chunked"
data = "5\r\naaaaa\r\n0\r\n\r\n"
assert mock_protocol(data).read_http_body(headers, 100, "GET", 200, False) == "aaaaa"
def test_expected_http_body_size():
# gibber in the content-length field
headers = Headers(content_length="foo")
with tutils.raises(HttpSyntaxException):
expected_http_body_size(headers, False, "GET", 200) is None
# negative number in the content-length field
headers = Headers(content_length="-7")
with tutils.raises(HttpSyntaxException):
expected_http_body_size(headers, False, "GET", 200) is None
# explicit length
headers = Headers(content_length="5")
assert expected_http_body_size(headers, False, "GET", 200) == 5
# no length
headers = Headers()
assert expected_http_body_size(headers, False, "GET", 200) == -1
# no length request
headers = Headers()
assert expected_http_body_size(headers, True, "GET", None) == 0
# expect header
headers = Headers(content_length="5", expect="100-continue")
assert expected_http_body_size(headers, True, "GET", None) == 0
def test_parse_init_connect():
assert _parse_authority_form(b"CONNECT host.com:443 HTTP/1.0")
tutils.raises(ValueError,_parse_authority_form, b"\0host.com:443")
tutils.raises(ValueError,_parse_authority_form, b"host.com:444444")
tutils.raises(ValueError,_parse_authority_form, b"CONNECT host.com443 HTTP/1.0")
tutils.raises(ValueError,_parse_authority_form, b"CONNECT host.com:foo HTTP/1.0")
def test_parse_init_proxy():
u = b"GET http://foo.com:8888/test HTTP/1.1"
m, s, h, po, pa, httpversion = HTTP1Protocol._parse_absolute_form(u)
assert m == "GET"
assert s == "http"
assert h == "foo.com"
assert po == 8888
assert pa == "/test"
assert httpversion == (1, 1)
u = "G\xfeET http://foo.com:8888/test HTTP/1.1"
assert not HTTP1Protocol._parse_absolute_form(u)
with tutils.raises(ValueError):
assert not HTTP1Protocol._parse_absolute_form("invalid")
with tutils.raises(ValueError):
assert not HTTP1Protocol._parse_absolute_form("GET invalid HTTP/1.1")
with tutils.raises(ValueError):
assert not HTTP1Protocol._parse_absolute_form("GET http://foo.com:8888/test foo/1.1")
def test_parse_init_http():
u = "GET /test HTTP/1.1"
m, u, httpversion = HTTP1Protocol._parse_init_http(u)
assert m == "GET"
assert u == "/test"
assert httpversion == (1, 1)
u = "G\xfeET /test HTTP/1.1"
assert not HTTP1Protocol._parse_init_http(u)
assert not HTTP1Protocol._parse_init_http("invalid")
assert not HTTP1Protocol._parse_init_http("GET invalid HTTP/1.1")
assert not HTTP1Protocol._parse_init_http("GET /test foo/1.1")
assert not HTTP1Protocol._parse_init_http("GET /test\xc0 HTTP/1.1")
class TestReadHeaders:
def _read(self, data, verbatim=False):
if not verbatim:
data = textwrap.dedent(data)
data = data.strip()
return mock_protocol(data).read_headers()
def test_read_simple(self):
data = """
Header: one
Header2: two
\r\n
"""
headers = self._read(data)
assert headers.fields == [["Header", "one"], ["Header2", "two"]]
def test_read_multi(self):
data = """
Header: one
Header: two
\r\n
"""
headers = self._read(data)
assert headers.fields == [["Header", "one"], ["Header", "two"]]
def test_read_continued(self):
data = """
Header: one
\ttwo
Header2: three
\r\n
"""
headers = self._read(data)
assert headers.fields == [["Header", "one\r\n two"], ["Header2", "three"]]
def test_read_continued_err(self):
data = "\tfoo: bar\r\n"
assert self._read(data, True) is None
def test_read_err(self):
data = """
foo
"""
assert self._read(data) is None
class TestReadRequest(object):
def tst(self, data, **kwargs):
return mock_protocol(data).read_request(**kwargs)
def test_invalid(self):
tutils.raises(
"bad http request",
self.tst,
"xxx"
)
tutils.raises(
"bad http request line",
self.tst,
"get /\xff HTTP/1.1"
)
tutils.raises(
"invalid headers",
self.tst,
"get / HTTP/1.1\r\nfoo"
)
tutils.raises(
HttpReadDisconnect,
self.tst,
"\r\n"
)
def test_asterisk_form_in(self):
v = self.tst("OPTIONS * HTTP/1.1")
assert v.form_in == "relative"
assert v.method == "OPTIONS"
def test_absolute_form_in(self):
tutils.raises(
"Bad HTTP request line",
self.tst,
"GET oops-no-protocol.com HTTP/1.1"
)
v = self.tst("GET http://address:22/ HTTP/1.1")
assert v.form_in == "absolute"
assert v.port == 22
assert v.host == "address"
assert v.scheme == "http"
def test_connect(self):
tutils.raises(
"Bad HTTP request line",
self.tst,
"CONNECT oops-no-port.com HTTP/1.1"
)
v = self.tst("CONNECT foo.com:443 HTTP/1.1")
assert v.form_in == "authority"
assert v.method == "CONNECT"
assert v.port == 443
assert v.host == "foo.com"
def test_expect(self):
data = (
b"GET / HTTP/1.1\r\n"
b"Content-Length: 3\r\n"
b"Expect: 100-continue\r\n"
b"\r\n"
b"foobar"
)
rfile = BytesIO(data)
r = read_request(rfile)
assert r.body == b""
assert rfile.read(-1) == b"foobar"
class TestReadResponse(object):
def tst(self, data, method, body_size_limit, include_body=True):
data = textwrap.dedent(data)
return mock_protocol(data).read_response(
method, body_size_limit, include_body=include_body
)
def test_errors(self):
tutils.raises("server disconnect", self.tst, "", "GET", None)
tutils.raises("invalid server response", self.tst, "foo", "GET", None)
def test_simple(self):
data = """
HTTP/1.1 200
"""
assert self.tst(data, "GET", None) == http.Response(
(1, 1), 200, '', Headers(), ''
)
def test_simple_message(self):
data = """
HTTP/1.1 200 OK
"""
assert self.tst(data, "GET", None) == http.Response(
(1, 1), 200, 'OK', Headers(), ''
)
def test_invalid_http_version(self):
data = """
HTTP/x 200 OK
"""
tutils.raises("invalid http version", self.tst, data, "GET", None)
def test_invalid_status_code(self):
data = """
HTTP/1.1 xx OK
"""
tutils.raises("invalid server response", self.tst, data, "GET", None)
def test_valid_with_continue(self):
data = """
HTTP/1.1 100 CONTINUE
HTTP/1.1 200 OK
"""
assert self.tst(data, "GET", None) == http.Response(
(1, 1), 100, 'CONTINUE', Headers(), ''
)
def test_simple_body(self):
data = """
HTTP/1.1 200 OK
Content-Length: 3
foo
"""
assert self.tst(data, "GET", None).body == 'foo'
assert self.tst(data, "HEAD", None).body == ''
def test_invalid_headers(self):
data = """
HTTP/1.1 200 OK
\tContent-Length: 3
foo
"""
tutils.raises("invalid headers", self.tst, data, "GET", None)
def test_without_body(self):
data = """
HTTP/1.1 200 OK
Content-Length: 3
foo
"""
assert self.tst(data, "GET", None, include_body=False).body is None
class TestReadResponseNoContentLength(tservers.ServerTestBase):
handler = NoContentLengthHTTPHandler
def test_no_content_length(self):
c = tcp.TCPClient(("127.0.0.1", self.port))
c.connect()
resp = HTTP1Protocol(c).read_response("GET", None)
assert resp.body == "bar\r\n\r\n"
class TestAssembleRequest(object):
def test_simple(self):
req = tutils.treq()
b = HTTP1Protocol().assemble_request(req)
assert b == match_http_string("""
GET /path HTTP/1.1
header: qvalue
Host: address:22
Content-Length: 7
content""")
def test_body_missing(self):
req = tutils.treq(content=semantics.CONTENT_MISSING)
tutils.raises(http.HttpError, HTTP1Protocol().assemble_request, req)
def test_not_a_request(self):
tutils.raises(AssertionError, HTTP1Protocol().assemble_request, 'foo')
class TestAssembleResponse(object):
def test_simple(self):
resp = tutils.tresp()
b = HTTP1Protocol().assemble_response(resp)
assert b == match_http_string("""
HTTP/1.1 200 OK
header_response: svalue
Content-Length: 7
message""")
def test_body_missing(self):
resp = tutils.tresp(content=semantics.CONTENT_MISSING)
tutils.raises(http.HttpError, HTTP1Protocol().assemble_response, resp)
def test_not_a_request(self):
tutils.raises(AssertionError, HTTP1Protocol().assemble_response, 'foo')

View File

@ -0,0 +1,313 @@
from __future__ import absolute_import, print_function, division
from io import BytesIO
import textwrap
from mock import Mock
from netlib.exceptions import HttpException, HttpSyntaxException, HttpReadDisconnect
from netlib.http import Headers
from netlib.http.http1.read import (
read_request, read_response, read_request_head,
read_response_head, read_body, connection_close, expected_http_body_size, _get_first_line,
_read_request_line, _parse_authority_form, _read_response_line, _check_http_version,
_read_headers, _read_chunked
)
from netlib.tutils import treq, tresp, raises
def test_read_request():
rfile = BytesIO(b"GET / HTTP/1.1\r\n\r\nskip")
r = read_request(rfile)
assert r.method == b"GET"
assert r.body == b""
assert r.timestamp_end
assert rfile.read() == b"skip"
def test_read_request_head():
rfile = BytesIO(
b"GET / HTTP/1.1\r\n"
b"Content-Length: 4\r\n"
b"\r\n"
b"skip"
)
rfile.reset_timestamps = Mock()
rfile.first_byte_timestamp = 42
r = read_request_head(rfile)
assert r.method == b"GET"
assert r.headers["Content-Length"] == b"4"
assert r.body is None
assert rfile.reset_timestamps.called
assert r.timestamp_start == 42
assert rfile.read() == b"skip"
def test_read_response():
req = treq()
rfile = BytesIO(b"HTTP/1.1 418 I'm a teapot\r\n\r\nbody")
r = read_response(rfile, req)
assert r.status_code == 418
assert r.body == b"body"
assert r.timestamp_end
def test_read_response_head():
rfile = BytesIO(
b"HTTP/1.1 418 I'm a teapot\r\n"
b"Content-Length: 4\r\n"
b"\r\n"
b"skip"
)
rfile.reset_timestamps = Mock()
rfile.first_byte_timestamp = 42
r = read_response_head(rfile)
assert r.status_code == 418
assert r.headers["Content-Length"] == b"4"
assert r.body is None
assert rfile.reset_timestamps.called
assert r.timestamp_start == 42
assert rfile.read() == b"skip"
class TestReadBody(object):
def test_chunked(self):
rfile = BytesIO(b"3\r\nfoo\r\n0\r\n\r\nbar")
body = b"".join(read_body(rfile, None))
assert body == b"foo"
assert rfile.read() == b"bar"
def test_known_size(self):
rfile = BytesIO(b"foobar")
body = b"".join(read_body(rfile, 3))
assert body == b"foo"
assert rfile.read() == b"bar"
def test_known_size_limit(self):
rfile = BytesIO(b"foobar")
with raises(HttpException):
b"".join(read_body(rfile, 3, 2))
def test_known_size_too_short(self):
rfile = BytesIO(b"foo")
with raises(HttpException):
b"".join(read_body(rfile, 6))
def test_unknown_size(self):
rfile = BytesIO(b"foobar")
body = b"".join(read_body(rfile, -1))
assert body == b"foobar"
def test_unknown_size_limit(self):
rfile = BytesIO(b"foobar")
with raises(HttpException):
b"".join(read_body(rfile, -1, 3))
def test_connection_close():
headers = Headers()
assert connection_close((1, 0), headers)
assert not connection_close((1, 1), headers)
headers["connection"] = "keep-alive"
assert not connection_close((1, 1), headers)
headers["connection"] = "close"
assert connection_close((1, 1), headers)
def test_expected_http_body_size():
# Expect: 100-continue
assert expected_http_body_size(
treq(headers=Headers(expect=b"100-continue", content_length=b"42"))
) == 0
# http://tools.ietf.org/html/rfc7230#section-3.3
assert expected_http_body_size(
treq(method=b"HEAD"),
tresp(headers=Headers(content_length=b"42"))
) == 0
assert expected_http_body_size(
treq(method=b"CONNECT"),
tresp()
) == 0
for code in (100, 204, 304):
assert expected_http_body_size(
treq(),
tresp(status_code=code)
) == 0
# chunked
assert expected_http_body_size(
treq(headers=Headers(transfer_encoding=b"chunked")),
) is None
# explicit length
for l in (b"foo", b"-7"):
with raises(HttpSyntaxException):
expected_http_body_size(
treq(headers=Headers(content_length=l))
)
assert expected_http_body_size(
treq(headers=Headers(content_length=b"42"))
) == 42
# no length
assert expected_http_body_size(
treq()
) == 0
assert expected_http_body_size(
treq(), tresp()
) == -1
def test_get_first_line():
rfile = BytesIO(b"foo\r\nbar")
assert _get_first_line(rfile) == b"foo"
rfile = BytesIO(b"\r\nfoo\r\nbar")
assert _get_first_line(rfile) == b"foo"
with raises(HttpReadDisconnect):
rfile = BytesIO(b"")
_get_first_line(rfile)
with raises(HttpSyntaxException):
rfile = BytesIO(b"GET /\xff HTTP/1.1")
_get_first_line(rfile)
def test_read_request_line():
def t(b):
return _read_request_line(BytesIO(b))
assert (t(b"GET / HTTP/1.1") ==
("relative", b"GET", None, None, None, b"/", b"HTTP/1.1"))
assert (t(b"OPTIONS * HTTP/1.1") ==
("relative", b"OPTIONS", None, None, None, b"*", b"HTTP/1.1"))
assert (t(b"CONNECT foo:42 HTTP/1.1") ==
("authority", b"CONNECT", None, b"foo", 42, None, b"HTTP/1.1"))
assert (t(b"GET http://foo:42/bar HTTP/1.1") ==
("absolute", b"GET", b"http", b"foo", 42, b"/bar", b"HTTP/1.1"))
with raises(HttpSyntaxException):
t(b"GET / WTF/1.1")
with raises(HttpSyntaxException):
t(b"this is not http")
def test_parse_authority_form():
assert _parse_authority_form(b"foo:42") == (b"foo", 42)
with raises(HttpSyntaxException):
_parse_authority_form(b"foo")
with raises(HttpSyntaxException):
_parse_authority_form(b"foo:bar")
with raises(HttpSyntaxException):
_parse_authority_form(b"foo:99999999")
with raises(HttpSyntaxException):
_parse_authority_form(b"f\x00oo:80")
def test_read_response_line():
def t(b):
return _read_response_line(BytesIO(b))
assert t(b"HTTP/1.1 200 OK") == (b"HTTP/1.1", 200, b"OK")
assert t(b"HTTP/1.1 200") == (b"HTTP/1.1", 200, b"")
with raises(HttpSyntaxException):
assert t(b"HTTP/1.1")
with raises(HttpSyntaxException):
t(b"HTTP/1.1 OK OK")
with raises(HttpSyntaxException):
t(b"WTF/1.1 200 OK")
def test_check_http_version():
_check_http_version(b"HTTP/0.9")
_check_http_version(b"HTTP/1.0")
_check_http_version(b"HTTP/1.1")
_check_http_version(b"HTTP/2.0")
with raises(HttpSyntaxException):
_check_http_version(b"WTF/1.0")
with raises(HttpSyntaxException):
_check_http_version(b"HTTP/1.10")
with raises(HttpSyntaxException):
_check_http_version(b"HTTP/1.b")
class TestReadHeaders(object):
@staticmethod
def _read(data):
return _read_headers(BytesIO(data))
def test_read_simple(self):
data = (
b"Header: one\r\n"
b"Header2: two\r\n"
b"\r\n"
)
headers = self._read(data)
assert headers.fields == [[b"Header", b"one"], [b"Header2", b"two"]]
def test_read_multi(self):
data = (
b"Header: one\r\n"
b"Header: two\r\n"
b"\r\n"
)
headers = self._read(data)
assert headers.fields == [[b"Header", b"one"], [b"Header", b"two"]]
def test_read_continued(self):
data = (
b"Header: one\r\n"
b"\ttwo\r\n"
b"Header2: three\r\n"
b"\r\n"
)
headers = self._read(data)
assert headers.fields == [[b"Header", b"one\r\n two"], [b"Header2", b"three"]]
def test_read_continued_err(self):
data = b"\tfoo: bar\r\n"
with raises(HttpSyntaxException):
self._read(data)
def test_read_err(self):
data = b"foo"
with raises(HttpSyntaxException):
self._read(data)
def test_read_chunked():
req = treq(body=None)
req.headers["Transfer-Encoding"] = "chunked"
data = b"1\r\na\r\n0\r\n"
with raises(HttpSyntaxException):
b"".join(_read_chunked(BytesIO(data)))
data = b"1\r\na\r\n0\r\n\r\n"
assert b"".join(_read_chunked(BytesIO(data))) == b"a"
data = b"\r\n\r\n1\r\na\r\n1\r\nb\r\n0\r\n\r\n"
assert b"".join(_read_chunked(BytesIO(data))) == b"ab"
data = b"\r\n"
with raises("closed prematurely"):
b"".join(_read_chunked(BytesIO(data)))
data = b"1\r\nfoo"
with raises("malformed chunked body"):
b"".join(_read_chunked(BytesIO(data)))
data = b"foo\r\nfoo"
with raises(HttpSyntaxException):
b"".join(_read_chunked(BytesIO(data)))
data = b"5\r\naaaaa\r\n0\r\n\r\n"
with raises("too large"):
b"".join(_read_chunked(BytesIO(data), limit=2))

View File

@ -39,7 +39,7 @@ def test_too_large_frames():
flags=Frame.FLAG_END_STREAM,
stream_id=0x1234567,
payload='foobar' * 3000)
tutils.raises(FrameSizeError, f.to_bytes)
tutils.raises(HttpSyntaxException, f.to_bytes)
def test_data_frame_to_bytes():

View File

@ -2,21 +2,21 @@ import OpenSSL
import mock
from netlib import tcp, http, tutils
from netlib.http import http2, Headers
from netlib.http.http2 import HTTP2Protocol
from netlib.http import Headers
from netlib.http.http2.connections import HTTP2Protocol, TCPHandler
from netlib.http.http2.frame import *
from ... import tservers
class TestTCPHandlerWrapper:
def test_wrapped(self):
h = http2.TCPHandler(rfile='foo', wfile='bar')
h = TCPHandler(rfile='foo', wfile='bar')
p = HTTP2Protocol(h)
assert p.tcp_handler.rfile == 'foo'
assert p.tcp_handler.wfile == 'bar'
def test_direct(self):
p = HTTP2Protocol(rfile='foo', wfile='bar')
assert isinstance(p.tcp_handler, http2.TCPHandler)
assert isinstance(p.tcp_handler, TCPHandler)
assert p.tcp_handler.rfile == 'foo'
assert p.tcp_handler.wfile == 'bar'
@ -32,8 +32,8 @@ class EchoHandler(tcp.BaseHandler):
class TestProtocol:
@mock.patch("netlib.http.http2.HTTP2Protocol.perform_server_connection_preface")
@mock.patch("netlib.http.http2.HTTP2Protocol.perform_client_connection_preface")
@mock.patch("netlib.http.http2.connections.HTTP2Protocol.perform_server_connection_preface")
@mock.patch("netlib.http.http2.connections.HTTP2Protocol.perform_client_connection_preface")
def test_perform_connection_preface(self, mock_client_method, mock_server_method):
protocol = HTTP2Protocol(is_server=False)
protocol.connection_preface_performed = True
@ -46,8 +46,8 @@ class TestProtocol:
assert mock_client_method.called
assert not mock_server_method.called
@mock.patch("netlib.http.http2.HTTP2Protocol.perform_server_connection_preface")
@mock.patch("netlib.http.http2.HTTP2Protocol.perform_client_connection_preface")
@mock.patch("netlib.http.http2.connections.HTTP2Protocol.perform_server_connection_preface")
@mock.patch("netlib.http.http2.connections.HTTP2Protocol.perform_client_connection_preface")
def test_perform_connection_preface_server(self, mock_client_method, mock_server_method):
protocol = HTTP2Protocol(is_server=True)
protocol.connection_preface_performed = True

View File

@ -1,6 +0,0 @@
from netlib.http.exceptions import *
class TestHttpError:
def test_simple(self):
e = HttpError(404, "Not found")
assert str(e)

View File

@ -1,32 +1,11 @@
import mock
from netlib import http
from netlib import odict
from netlib import tutils
from netlib import utils
from netlib.http import semantics
from netlib.http.semantics import CONTENT_MISSING
from netlib.odict import ODict, ODictCaseless
from netlib.http import Request, Response, Headers, CONTENT_MISSING, HDR_FORM_URLENCODED, \
HDR_FORM_MULTIPART
class TestProtocolMixin(object):
@mock.patch("netlib.http.semantics.ProtocolMixin.assemble_response")
@mock.patch("netlib.http.semantics.ProtocolMixin.assemble_request")
def test_assemble_request(self, mock_request_method, mock_response_method):
p = semantics.ProtocolMixin()
p.assemble(tutils.treq())
assert mock_request_method.called
assert not mock_response_method.called
@mock.patch("netlib.http.semantics.ProtocolMixin.assemble_response")
@mock.patch("netlib.http.semantics.ProtocolMixin.assemble_request")
def test_assemble_response(self, mock_request_method, mock_response_method):
p = semantics.ProtocolMixin()
p.assemble(tutils.tresp())
assert not mock_request_method.called
assert mock_response_method.called
def test_assemble_foo(self):
p = semantics.ProtocolMixin()
tutils.raises(ValueError, p.assemble, 'foo')
class TestRequest(object):
def test_repr(self):
@ -34,7 +13,7 @@ class TestRequest(object):
assert repr(r)
def test_headers(self):
tutils.raises(AssertionError, semantics.Request,
tutils.raises(AssertionError, Request,
'form_in',
'method',
'scheme',
@ -45,7 +24,7 @@ class TestRequest(object):
'foobar',
)
req = semantics.Request(
req = Request(
'form_in',
'method',
'scheme',
@ -54,7 +33,7 @@ class TestRequest(object):
'path',
(1, 1),
)
assert isinstance(req.headers, http.Headers)
assert isinstance(req.headers, Headers)
def test_equal(self):
a = tutils.treq()
@ -66,13 +45,6 @@ class TestRequest(object):
assert not 'foo' == a
assert not 'foo' == b
def test_legacy_first_line(self):
req = tutils.treq()
assert req.legacy_first_line('relative') == "GET /path HTTP/1.1"
assert req.legacy_first_line('authority') == "GET address:22 HTTP/1.1"
assert req.legacy_first_line('absolute') == "GET http://address:22/path HTTP/1.1"
tutils.raises(http.HttpError, req.legacy_first_line, 'foobar')
def test_anticache(self):
req = tutils.treq()
@ -103,44 +75,44 @@ class TestRequest(object):
def test_get_form(self):
req = tutils.treq()
assert req.get_form() == odict.ODict()
assert req.get_form() == ODict()
@mock.patch("netlib.http.semantics.Request.get_form_multipart")
@mock.patch("netlib.http.semantics.Request.get_form_urlencoded")
@mock.patch("netlib.http.Request.get_form_multipart")
@mock.patch("netlib.http.Request.get_form_urlencoded")
def test_get_form_with_url_encoded(self, mock_method_urlencoded, mock_method_multipart):
req = tutils.treq()
assert req.get_form() == odict.ODict()
assert req.get_form() == ODict()
req = tutils.treq()
req.body = "foobar"
req.headers["Content-Type"] = semantics.HDR_FORM_URLENCODED
req.headers["Content-Type"] = HDR_FORM_URLENCODED
req.get_form()
assert req.get_form_urlencoded.called
assert not req.get_form_multipart.called
@mock.patch("netlib.http.semantics.Request.get_form_multipart")
@mock.patch("netlib.http.semantics.Request.get_form_urlencoded")
@mock.patch("netlib.http.Request.get_form_multipart")
@mock.patch("netlib.http.Request.get_form_urlencoded")
def test_get_form_with_multipart(self, mock_method_urlencoded, mock_method_multipart):
req = tutils.treq()
req.body = "foobar"
req.headers["Content-Type"] = semantics.HDR_FORM_MULTIPART
req.headers["Content-Type"] = HDR_FORM_MULTIPART
req.get_form()
assert not req.get_form_urlencoded.called
assert req.get_form_multipart.called
def test_get_form_urlencoded(self):
req = tutils.treq("foobar")
assert req.get_form_urlencoded() == odict.ODict()
req = tutils.treq(body="foobar")
assert req.get_form_urlencoded() == ODict()
req.headers["Content-Type"] = semantics.HDR_FORM_URLENCODED
assert req.get_form_urlencoded() == odict.ODict(utils.urldecode(req.body))
req.headers["Content-Type"] = HDR_FORM_URLENCODED
assert req.get_form_urlencoded() == ODict(utils.urldecode(req.body))
def test_get_form_multipart(self):
req = tutils.treq("foobar")
assert req.get_form_multipart() == odict.ODict()
req = tutils.treq(body="foobar")
assert req.get_form_multipart() == ODict()
req.headers["Content-Type"] = semantics.HDR_FORM_MULTIPART
assert req.get_form_multipart() == odict.ODict(
req.headers["Content-Type"] = HDR_FORM_MULTIPART
assert req.get_form_multipart() == ODict(
utils.multipartdecode(
req.headers,
req.body
@ -149,8 +121,8 @@ class TestRequest(object):
def test_set_form_urlencoded(self):
req = tutils.treq()
req.set_form_urlencoded(odict.ODict([('foo', 'bar'), ('rab', 'oof')]))
assert req.headers["Content-Type"] == semantics.HDR_FORM_URLENCODED
req.set_form_urlencoded(ODict([('foo', 'bar'), ('rab', 'oof')]))
assert req.headers["Content-Type"] == HDR_FORM_URLENCODED
assert req.body
def test_get_path_components(self):
@ -172,7 +144,7 @@ class TestRequest(object):
def test_set_query(self):
req = tutils.treq()
req.set_query(odict.ODict([]))
req.set_query(ODict([]))
def test_pretty_host(self):
r = tutils.treq()
@ -203,21 +175,21 @@ class TestRequest(object):
assert req.pretty_url(False) == "http://address:22/path"
def test_get_cookies_none(self):
headers = http.Headers()
headers = Headers()
r = tutils.treq()
r.headers = headers
assert len(r.get_cookies()) == 0
def test_get_cookies_single(self):
r = tutils.treq()
r.headers = http.Headers(cookie="cookiename=cookievalue")
r.headers = Headers(cookie="cookiename=cookievalue")
result = r.get_cookies()
assert len(result) == 1
assert result['cookiename'] == ['cookievalue']
def test_get_cookies_double(self):
r = tutils.treq()
r.headers = http.Headers(cookie="cookiename=cookievalue;othercookiename=othercookievalue")
r.headers = Headers(cookie="cookiename=cookievalue;othercookiename=othercookievalue")
result = r.get_cookies()
assert len(result) == 2
assert result['cookiename'] == ['cookievalue']
@ -225,7 +197,7 @@ class TestRequest(object):
def test_get_cookies_withequalsign(self):
r = tutils.treq()
r.headers = http.Headers(cookie="cookiename=coo=kievalue;othercookiename=othercookievalue")
r.headers = Headers(cookie="cookiename=coo=kievalue;othercookiename=othercookievalue")
result = r.get_cookies()
assert len(result) == 2
assert result['cookiename'] == ['coo=kievalue']
@ -233,14 +205,14 @@ class TestRequest(object):
def test_set_cookies(self):
r = tutils.treq()
r.headers = http.Headers(cookie="cookiename=cookievalue")
r.headers = Headers(cookie="cookiename=cookievalue")
result = r.get_cookies()
result["cookiename"] = ["foo"]
r.set_cookies(result)
assert r.get_cookies()["cookiename"] == ["foo"]
def test_set_url(self):
r = tutils.treq_absolute()
r = tutils.treq(form_in="absolute")
r.url = "https://otheraddress:42/ORLY"
assert r.scheme == "https"
assert r.host == "otheraddress"
@ -332,24 +304,19 @@ class TestRequest(object):
# "Host: address\r\n"
# "Content-Length: 0\r\n\r\n")
class TestEmptyRequest(object):
def test_init(self):
req = semantics.EmptyRequest()
assert req
class TestResponse(object):
def test_headers(self):
tutils.raises(AssertionError, semantics.Response,
tutils.raises(AssertionError, Response,
(1, 1),
200,
headers='foobar',
)
resp = semantics.Response(
resp = Response(
(1, 1),
200,
)
assert isinstance(resp.headers, http.Headers)
assert isinstance(resp.headers, Headers)
def test_equal(self):
a = tutils.tresp()
@ -366,24 +333,24 @@ class TestResponse(object):
assert "unknown content type" in repr(r)
r.headers["content-type"] = "foo"
assert "foo" in repr(r)
assert repr(tutils.tresp(content=CONTENT_MISSING))
assert repr(tutils.tresp(body=CONTENT_MISSING))
def test_get_cookies_none(self):
resp = tutils.tresp()
resp.headers = http.Headers()
resp.headers = Headers()
assert not resp.get_cookies()
def test_get_cookies_simple(self):
resp = tutils.tresp()
resp.headers = http.Headers(set_cookie="cookiename=cookievalue")
resp.headers = Headers(set_cookie="cookiename=cookievalue")
result = resp.get_cookies()
assert len(result) == 1
assert "cookiename" in result
assert result["cookiename"][0] == ["cookievalue", odict.ODict()]
assert result["cookiename"][0] == ["cookievalue", ODict()]
def test_get_cookies_with_parameters(self):
resp = tutils.tresp()
resp.headers = http.Headers(set_cookie="cookiename=cookievalue;domain=example.com;expires=Wed Oct 21 16:29:41 2015;path=/; HttpOnly")
resp.headers = Headers(set_cookie="cookiename=cookievalue;domain=example.com;expires=Wed Oct 21 16:29:41 2015;path=/; HttpOnly")
result = resp.get_cookies()
assert len(result) == 1
assert "cookiename" in result
@ -397,7 +364,7 @@ class TestResponse(object):
def test_get_cookies_no_value(self):
resp = tutils.tresp()
resp.headers = http.Headers(set_cookie="cookiename=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/")
resp.headers = Headers(set_cookie="cookiename=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/")
result = resp.get_cookies()
assert len(result) == 1
assert "cookiename" in result
@ -406,31 +373,31 @@ class TestResponse(object):
def test_get_cookies_twocookies(self):
resp = tutils.tresp()
resp.headers = http.Headers([
resp.headers = Headers([
["Set-Cookie", "cookiename=cookievalue"],
["Set-Cookie", "othercookie=othervalue"]
])
result = resp.get_cookies()
assert len(result) == 2
assert "cookiename" in result
assert result["cookiename"][0] == ["cookievalue", odict.ODict()]
assert result["cookiename"][0] == ["cookievalue", ODict()]
assert "othercookie" in result
assert result["othercookie"][0] == ["othervalue", odict.ODict()]
assert result["othercookie"][0] == ["othervalue", ODict()]
def test_set_cookies(self):
resp = tutils.tresp()
v = resp.get_cookies()
v.add("foo", ["bar", odict.ODictCaseless()])
v.add("foo", ["bar", ODictCaseless()])
resp.set_cookies(v)
v = resp.get_cookies()
assert len(v) == 1
assert v["foo"] == [["bar", odict.ODictCaseless()]]
assert v["foo"] == [["bar", ODictCaseless()]]
class TestHeaders(object):
def _2host(self):
return semantics.Headers(
return Headers(
[
["Host", "example.com"],
["host", "example.org"]
@ -438,25 +405,25 @@ class TestHeaders(object):
)
def test_init(self):
headers = semantics.Headers()
headers = Headers()
assert len(headers) == 0
headers = semantics.Headers([["Host", "example.com"]])
headers = Headers([["Host", "example.com"]])
assert len(headers) == 1
assert headers["Host"] == "example.com"
headers = semantics.Headers(Host="example.com")
headers = Headers(Host="example.com")
assert len(headers) == 1
assert headers["Host"] == "example.com"
headers = semantics.Headers(
headers = Headers(
[["Host", "invalid"]],
Host="example.com"
)
assert len(headers) == 1
assert headers["Host"] == "example.com"
headers = semantics.Headers(
headers = Headers(
[["Host", "invalid"], ["Accept", "text/plain"]],
Host="example.com"
)
@ -465,7 +432,7 @@ class TestHeaders(object):
assert headers["Accept"] == "text/plain"
def test_getitem(self):
headers = semantics.Headers(Host="example.com")
headers = Headers(Host="example.com")
assert headers["Host"] == "example.com"
assert headers["host"] == "example.com"
tutils.raises(KeyError, headers.__getitem__, "Accept")
@ -474,17 +441,17 @@ class TestHeaders(object):
assert headers["Host"] == "example.com, example.org"
def test_str(self):
headers = semantics.Headers(Host="example.com")
headers = Headers(Host="example.com")
assert bytes(headers) == "Host: example.com\r\n"
headers = semantics.Headers([
headers = Headers([
["Host", "example.com"],
["Accept", "text/plain"]
])
assert str(headers) == "Host: example.com\r\nAccept: text/plain\r\n"
def test_setitem(self):
headers = semantics.Headers()
headers = Headers()
headers["Host"] = "example.com"
assert "Host" in headers
assert "host" in headers
@ -507,7 +474,7 @@ class TestHeaders(object):
assert "Host" in headers
def test_delitem(self):
headers = semantics.Headers(Host="example.com")
headers = Headers(Host="example.com")
assert len(headers) == 1
del headers["host"]
assert len(headers) == 0
@ -523,7 +490,7 @@ class TestHeaders(object):
assert len(headers) == 0
def test_keys(self):
headers = semantics.Headers(Host="example.com")
headers = Headers(Host="example.com")
assert len(headers.keys()) == 1
assert headers.keys()[0] == "Host"
@ -532,13 +499,13 @@ class TestHeaders(object):
assert headers.keys()[0] == "Host"
def test_eq_ne(self):
headers1 = semantics.Headers(Host="example.com")
headers2 = semantics.Headers(host="example.com")
headers1 = Headers(Host="example.com")
headers2 = Headers(host="example.com")
assert not (headers1 == headers2)
assert headers1 != headers2
headers1 = semantics.Headers(Host="example.com")
headers2 = semantics.Headers(Host="example.com")
headers1 = Headers(Host="example.com")
headers2 = Headers(Host="example.com")
assert headers1 == headers2
assert not (headers1 != headers2)
@ -550,7 +517,7 @@ class TestHeaders(object):
assert headers.get_all("accept") == []
def test_set_all(self):
headers = semantics.Headers(Host="example.com")
headers = Headers(Host="example.com")
headers.set_all("Accept", ["text/plain"])
assert len(headers) == 2
assert "accept" in headers
@ -565,9 +532,9 @@ class TestHeaders(object):
def test_state(self):
headers = self._2host()
assert len(headers.get_state()) == 2
assert headers == semantics.Headers.from_state(headers.get_state())
assert headers == Headers.from_state(headers.get_state())
headers2 = semantics.Headers()
headers2 = Headers()
assert headers != headers2
headers2.load_state(headers.get_state())
assert headers == headers2

View File

@ -1,11 +1,13 @@
import os
from nose.tools import raises
from netlib.http.http1 import read_response, read_request
from netlib import tcp, tutils, websockets, http
from netlib.http import status_codes
from netlib.http.exceptions import *
from netlib.http.http1 import HTTP1Protocol
from netlib.tutils import treq
from netlib.exceptions import *
from .. import tservers
@ -34,9 +36,8 @@ class WebSocketsEchoHandler(tcp.BaseHandler):
frame.to_file(self.wfile)
def handshake(self):
http1_protocol = HTTP1Protocol(self)
req = http1_protocol.read_request()
req = read_request(self.rfile)
key = self.protocol.check_client_handshake(req.headers)
preamble = 'HTTP/1.1 101 %s' % status_codes.RESPONSES.get(101)
@ -61,8 +62,6 @@ class WebSocketsClient(tcp.TCPClient):
def connect(self):
super(WebSocketsClient, self).connect()
http1_protocol = HTTP1Protocol(self)
preamble = 'GET / HTTP/1.1'
self.wfile.write(preamble + "\r\n")
headers = self.protocol.client_handshake_headers()
@ -70,7 +69,7 @@ class WebSocketsClient(tcp.TCPClient):
self.wfile.write(str(headers) + "\r\n")
self.wfile.flush()
resp = http1_protocol.read_response("GET", None)
resp = read_response(self.rfile, treq(method="GET"))
server_nonce = self.protocol.check_server_handshake(resp.headers)
if not server_nonce == self.protocol.create_server_nonce(
@ -158,9 +157,8 @@ class TestWebSockets(tservers.ServerTestBase):
class BadHandshakeHandler(WebSocketsEchoHandler):
def handshake(self):
http1_protocol = HTTP1Protocol(self)
client_hs = http1_protocol.read_request()
client_hs = read_request(self.rfile)
self.protocol.check_client_handshake(client_hs.headers)
preamble = 'HTTP/1.1 101 %s' % status_codes.RESPONSES.get(101)