aboutsummaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/__init__.py9
-rw-r--r--test/mitmproxy/__init__.py9
-rw-r--r--test/mitmproxy/completion/aaa (renamed from test/completion/aaa)0
-rw-r--r--test/mitmproxy/completion/aab (renamed from test/completion/aab)0
-rw-r--r--test/mitmproxy/completion/aac (renamed from test/completion/aac)0
-rw-r--r--test/mitmproxy/data/1.css (renamed from test/data/1.css)0
-rw-r--r--test/mitmproxy/data/amf01 (renamed from test/data/amf01)bin432 -> 432 bytes
-rw-r--r--test/mitmproxy/data/amf02 (renamed from test/data/amf02)bin286 -> 286 bytes
-rw-r--r--test/mitmproxy/data/amf03 (renamed from test/data/amf03)bin33691 -> 33691 bytes
-rw-r--r--test/mitmproxy/data/clientcert/.gitignore (renamed from test/data/clientcert/.gitignore)0
-rw-r--r--test/mitmproxy/data/clientcert/127.0.0.1.pem (renamed from test/data/clientcert/127.0.0.1.pem)0
-rw-r--r--test/mitmproxy/data/clientcert/client.cnf (renamed from test/data/clientcert/client.cnf)0
-rw-r--r--test/mitmproxy/data/clientcert/client.pem (renamed from test/data/clientcert/client.pem)0
-rw-r--r--[-rwxr-xr-x]test/mitmproxy/data/clientcert/make (renamed from test/data/clientcert/make)0
-rw-r--r--test/mitmproxy/data/confdir/mitmproxy-ca-cert.cer (renamed from test/data/confdir/mitmproxy-ca-cert.cer)0
-rw-r--r--test/mitmproxy/data/confdir/mitmproxy-ca-cert.p12 (renamed from test/data/confdir/mitmproxy-ca-cert.p12)bin1689 -> 1689 bytes
-rw-r--r--test/mitmproxy/data/confdir/mitmproxy-ca-cert.pem (renamed from test/data/confdir/mitmproxy-ca-cert.pem)0
-rw-r--r--test/mitmproxy/data/confdir/mitmproxy-ca.pem (renamed from test/data/confdir/mitmproxy-ca.pem)0
-rw-r--r--test/mitmproxy/data/dercert (renamed from test/data/dercert)bin1838 -> 1838 bytes
-rw-r--r--test/mitmproxy/data/dumpfile-012 (renamed from test/data/dumpfile-012)0
-rw-r--r--test/mitmproxy/data/dumpfile-013 (renamed from test/data/dumpfile-013)0
-rw-r--r--test/mitmproxy/data/htpasswd (renamed from test/data/htpasswd)0
-rw-r--r--test/mitmproxy/data/htpasswd.invalid (renamed from test/data/htpasswd.invalid)0
-rw-r--r--test/mitmproxy/data/image-err1.jpg (renamed from test/data/image-err1.jpg)bin82674 -> 82674 bytes
-rw-r--r--test/mitmproxy/data/image.gif (renamed from test/data/image.gif)bin2398 -> 2398 bytes
-rw-r--r--test/mitmproxy/data/image.ico (renamed from test/data/image.ico)bin11502 -> 11502 bytes
-rw-r--r--test/mitmproxy/data/image.jpg (renamed from test/data/image.jpg)bin1568 -> 1568 bytes
-rw-r--r--test/mitmproxy/data/image.png (renamed from test/data/image.png)bin9311 -> 9311 bytes
-rw-r--r--test/mitmproxy/data/no_common_name.pem (renamed from test/data/no_common_name.pem)0
-rw-r--r--test/mitmproxy/data/pf01 (renamed from test/data/pf01)0
-rw-r--r--test/mitmproxy/data/pf02 (renamed from test/data/pf02)0
-rw-r--r--test/mitmproxy/data/protobuf01 (renamed from test/data/protobuf01)0
-rw-r--r--test/mitmproxy/data/replace (renamed from test/data/replace)0
-rw-r--r--test/mitmproxy/data/testkey.pem (renamed from test/data/testkey.pem)0
-rw-r--r--test/mitmproxy/data/trusted-cadir/8117bdb9.0 (renamed from test/data/trusted-cadir/8117bdb9.0)0
-rw-r--r--test/mitmproxy/data/trusted-cadir/9d45e6a9.0 (renamed from test/data/trusted-cadir/9d45e6a9.0)0
-rw-r--r--test/mitmproxy/data/trusted-cadir/trusted-ca.pem (renamed from test/data/trusted-cadir/trusted-ca.pem)0
-rw-r--r--test/mitmproxy/data/trusted-server.crt (renamed from test/data/trusted-server.crt)0
-rw-r--r--test/mitmproxy/data/untrusted-server.crt (renamed from test/data/untrusted-server.crt)0
-rw-r--r--test/mitmproxy/fuzzing/.env (renamed from test/fuzzing/.env)0
-rw-r--r--test/mitmproxy/fuzzing/README (renamed from test/fuzzing/README)0
-rw-r--r--test/mitmproxy/fuzzing/client_patterns (renamed from test/fuzzing/client_patterns)0
-rw-r--r--[-rwxr-xr-x]test/mitmproxy/fuzzing/go_proxy (renamed from test/fuzzing/go_proxy)0
-rw-r--r--test/mitmproxy/fuzzing/reverse_patterns (renamed from test/fuzzing/reverse_patterns)0
-rw-r--r--test/mitmproxy/fuzzing/straight_stream (renamed from test/fuzzing/straight_stream)0
-rw-r--r--test/mitmproxy/fuzzing/straight_stream_patterns (renamed from test/fuzzing/straight_stream_patterns)0
-rw-r--r--test/mitmproxy/fuzzing/straight_stream_ssl (renamed from test/fuzzing/straight_stream_ssl)0
-rw-r--r--test/mitmproxy/mock_urwid.py (renamed from test/mock_urwid.py)0
-rw-r--r--test/mitmproxy/scripts/a.py (renamed from test/scripts/a.py)0
-rw-r--r--test/mitmproxy/scripts/a_helper.py (renamed from test/scripts/a_helper.py)0
-rw-r--r--test/mitmproxy/scripts/all.py (renamed from test/scripts/all.py)0
-rw-r--r--test/mitmproxy/scripts/concurrent_decorator.py (renamed from test/scripts/concurrent_decorator.py)0
-rw-r--r--test/mitmproxy/scripts/concurrent_decorator_err.py (renamed from test/scripts/concurrent_decorator_err.py)0
-rw-r--r--test/mitmproxy/scripts/duplicate_flow.py (renamed from test/scripts/duplicate_flow.py)0
-rw-r--r--test/mitmproxy/scripts/loaderr.py (renamed from test/scripts/loaderr.py)0
-rw-r--r--test/mitmproxy/scripts/reqerr.py (renamed from test/scripts/reqerr.py)0
-rw-r--r--test/mitmproxy/scripts/starterr.py (renamed from test/scripts/starterr.py)0
-rw-r--r--test/mitmproxy/scripts/stream_modify.py (renamed from test/scripts/stream_modify.py)0
-rw-r--r--test/mitmproxy/scripts/syntaxerr.py (renamed from test/scripts/syntaxerr.py)0
-rw-r--r--test/mitmproxy/scripts/tcp_stream_modify.py (renamed from test/scripts/tcp_stream_modify.py)0
-rw-r--r--test/mitmproxy/scripts/unloaderr.py (renamed from test/scripts/unloaderr.py)0
-rw-r--r--test/mitmproxy/test_app.py (renamed from test/test_app.py)0
-rw-r--r--test/mitmproxy/test_cmdline.py (renamed from test/test_cmdline.py)0
-rw-r--r--test/mitmproxy/test_console.py (renamed from test/test_console.py)0
-rw-r--r--test/mitmproxy/test_console_common.py (renamed from test/test_console_common.py)0
-rw-r--r--test/mitmproxy/test_console_help.py (renamed from test/test_console_help.py)0
-rw-r--r--test/mitmproxy/test_console_palettes.py (renamed from test/test_console_palettes.py)0
-rw-r--r--test/mitmproxy/test_console_pathedit.py (renamed from test/test_console_pathedit.py)0
-rw-r--r--test/mitmproxy/test_contentview.py (renamed from test/test_contentview.py)0
-rw-r--r--test/mitmproxy/test_controller.py (renamed from test/test_controller.py)0
-rw-r--r--test/mitmproxy/test_custom_contentview.py (renamed from test/test_custom_contentview.py)0
-rw-r--r--test/mitmproxy/test_dump.py (renamed from test/test_dump.py)0
-rw-r--r--test/mitmproxy/test_examples.py (renamed from test/test_examples.py)0
-rw-r--r--test/mitmproxy/test_filt.py (renamed from test/test_filt.py)0
-rw-r--r--test/mitmproxy/test_flow.py (renamed from test/test_flow.py)0
-rw-r--r--test/mitmproxy/test_flow_export.py (renamed from test/test_flow_export.py)0
-rw-r--r--test/mitmproxy/test_flow_format_compat.py (renamed from test/test_flow_format_compat.py)0
-rw-r--r--test/mitmproxy/test_fuzzing.py (renamed from test/test_fuzzing.py)0
-rw-r--r--test/mitmproxy/test_platform_pf.py (renamed from test/test_platform_pf.py)0
-rw-r--r--test/mitmproxy/test_protocol_http1.py (renamed from test/test_protocol_http1.py)0
-rw-r--r--test/mitmproxy/test_protocol_http2.py (renamed from test/test_protocol_http2.py)2
-rw-r--r--test/mitmproxy/test_proxy.py (renamed from test/test_proxy.py)0
-rw-r--r--test/mitmproxy/test_script.py (renamed from test/test_script.py)0
-rw-r--r--test/mitmproxy/test_server.py (renamed from test/test_server.py)0
-rw-r--r--test/mitmproxy/test_utils.py (renamed from test/test_utils.py)0
-rw-r--r--test/mitmproxy/tools/1024example (renamed from test/tools/1024example)0
-rw-r--r--test/mitmproxy/tools/ab.exe (renamed from test/tools/ab.exe)bin82944 -> 82944 bytes
-rw-r--r--test/mitmproxy/tools/bench.py (renamed from test/tools/bench.py)0
-rw-r--r--test/mitmproxy/tools/benchtool.py (renamed from test/tools/benchtool.py)0
-rw-r--r--[-rwxr-xr-x]test/mitmproxy/tools/getcert (renamed from test/tools/getcert)0
-rw-r--r--test/mitmproxy/tools/inspect_dumpfile.py (renamed from test/tools/inspect_dumpfile.py)0
-rw-r--r--test/mitmproxy/tools/memoryleak.py (renamed from test/tools/memoryleak.py)0
-rw-r--r--test/mitmproxy/tools/passive_close.py (renamed from test/tools/passive_close.py)0
-rw-r--r--[-rwxr-xr-x]test/mitmproxy/tools/testpatt (renamed from test/tools/testpatt)0
-rw-r--r--test/mitmproxy/tservers.py (renamed from test/tservers.py)0
-rw-r--r--test/mitmproxy/tutils.py (renamed from test/tutils.py)0
-rw-r--r--test/netlib/__init__.py0
-rw-r--r--test/netlib/data/clientcert/.gitignore3
-rw-r--r--test/netlib/data/clientcert/client.cnf5
-rw-r--r--test/netlib/data/clientcert/client.pem42
-rw-r--r--test/netlib/data/clientcert/make8
-rw-r--r--test/netlib/data/dercertbin0 -> 1838 bytes
-rw-r--r--test/netlib/data/dhparam.pem13
-rw-r--r--test/netlib/data/htpasswd1
-rw-r--r--test/netlib/data/server.crt14
-rw-r--r--test/netlib/data/server.key15
-rw-r--r--test/netlib/data/text_cert145
-rw-r--r--test/netlib/data/text_cert_239
-rw-r--r--test/netlib/data/text_cert_weird131
-rw-r--r--test/netlib/data/verificationcerts/9da13359.021
-rw-r--r--test/netlib/data/verificationcerts/generate.py68
-rw-r--r--test/netlib/data/verificationcerts/self-signed.crt19
-rw-r--r--test/netlib/data/verificationcerts/self-signed.key27
-rw-r--r--test/netlib/data/verificationcerts/trusted-leaf.crt18
-rw-r--r--test/netlib/data/verificationcerts/trusted-leaf.key27
-rw-r--r--test/netlib/data/verificationcerts/trusted-root.crt21
-rw-r--r--test/netlib/data/verificationcerts/trusted-root.key27
-rw-r--r--test/netlib/data/verificationcerts/trusted-root.srl1
-rw-r--r--test/netlib/http/__init__.py0
-rw-r--r--test/netlib/http/http1/__init__.py0
-rw-r--r--test/netlib/http/http1/test_assemble.py102
-rw-r--r--test/netlib/http/http1/test_read.py333
-rw-r--r--test/netlib/http/http2/__init__.py0
-rw-r--r--test/netlib/http/http2/test_connections.py540
-rw-r--r--test/netlib/http/test_authentication.py122
-rw-r--r--test/netlib/http/test_cookies.py218
-rw-r--r--test/netlib/http/test_headers.py152
-rw-r--r--test/netlib/http/test_message.py153
-rw-r--r--test/netlib/http/test_request.py238
-rw-r--r--test/netlib/http/test_response.py102
-rw-r--r--test/netlib/http/test_status_codes.py6
-rw-r--r--test/netlib/http/test_user_agents.py6
-rw-r--r--test/netlib/test_certutils.py155
-rw-r--r--test/netlib/test_encoding.py37
-rw-r--r--test/netlib/test_imports.py1
-rw-r--r--test/netlib/test_odict.py153
-rw-r--r--test/netlib/test_socks.py149
-rw-r--r--test/netlib/test_tcp.py795
-rw-r--r--test/netlib/test_utils.py141
-rw-r--r--test/netlib/test_version_check.py38
-rw-r--r--test/netlib/test_wsgi.py106
-rw-r--r--test/netlib/tools/getcertnames27
-rw-r--r--test/netlib/websockets/__init__.py0
-rw-r--r--test/netlib/websockets/test_websockets.py266
-rw-r--r--test/pathod/data/clientcert/.gitignore3
-rw-r--r--test/pathod/data/clientcert/client.cnf5
-rw-r--r--test/pathod/data/clientcert/client.pem42
-rw-r--r--test/pathod/data/clientcert/make8
-rw-r--r--test/pathod/data/file1
-rw-r--r--test/pathod/data/request1
-rw-r--r--test/pathod/data/response1
-rw-r--r--test/pathod/data/testkey.pem68
-rw-r--r--test/pathod/scripts/generate.sh17
-rw-r--r--test/pathod/scripts/openssl.cnf39
-rw-r--r--test/pathod/test_app.py85
-rw-r--r--test/pathod/test_language_actions.py135
-rw-r--r--test/pathod/test_language_base.py352
-rw-r--r--test/pathod/test_language_generators.py42
-rw-r--r--test/pathod/test_language_http.py358
-rw-r--r--test/pathod/test_language_http2.py233
-rw-r--r--test/pathod/test_language_websocket.py142
-rw-r--r--test/pathod/test_language_writer.py91
-rw-r--r--test/pathod/test_log.py25
-rw-r--r--test/pathod/test_pathoc.py310
-rw-r--r--test/pathod/test_pathoc_cmdline.py59
-rw-r--r--test/pathod/test_pathod.py289
-rw-r--r--test/pathod/test_pathod_cmdline.py85
-rw-r--r--test/pathod/test_test.py45
-rw-r--r--test/pathod/test_utils.py39
-rw-r--r--test/pathod/tutils.py128
170 files changed, 6998 insertions, 10 deletions
diff --git a/test/__init__.py b/test/__init__.py
index 61d03152..e69de29b 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -1,9 +0,0 @@
-from __future__ import (print_function, absolute_import, division)
-
-# Silence third-party modules
-import logging
-logging.getLogger("hyper").setLevel(logging.WARNING)
-logging.getLogger("requests").setLevel(logging.WARNING)
-logging.getLogger("passlib").setLevel(logging.WARNING)
-logging.getLogger("PIL").setLevel(logging.WARNING)
-logging.getLogger("tornado").setLevel(logging.WARNING)
diff --git a/test/mitmproxy/__init__.py b/test/mitmproxy/__init__.py
new file mode 100644
index 00000000..61d03152
--- /dev/null
+++ b/test/mitmproxy/__init__.py
@@ -0,0 +1,9 @@
+from __future__ import (print_function, absolute_import, division)
+
+# Silence third-party modules
+import logging
+logging.getLogger("hyper").setLevel(logging.WARNING)
+logging.getLogger("requests").setLevel(logging.WARNING)
+logging.getLogger("passlib").setLevel(logging.WARNING)
+logging.getLogger("PIL").setLevel(logging.WARNING)
+logging.getLogger("tornado").setLevel(logging.WARNING)
diff --git a/test/completion/aaa b/test/mitmproxy/completion/aaa
index e69de29b..e69de29b 100644
--- a/test/completion/aaa
+++ b/test/mitmproxy/completion/aaa
diff --git a/test/completion/aab b/test/mitmproxy/completion/aab
index e69de29b..e69de29b 100644
--- a/test/completion/aab
+++ b/test/mitmproxy/completion/aab
diff --git a/test/completion/aac b/test/mitmproxy/completion/aac
index e69de29b..e69de29b 100644
--- a/test/completion/aac
+++ b/test/mitmproxy/completion/aac
diff --git a/test/data/1.css b/test/mitmproxy/data/1.css
index 33387ca7..33387ca7 100644
--- a/test/data/1.css
+++ b/test/mitmproxy/data/1.css
diff --git a/test/data/amf01 b/test/mitmproxy/data/amf01
index c8fc261d..c8fc261d 100644
--- a/test/data/amf01
+++ b/test/mitmproxy/data/amf01
Binary files differ
diff --git a/test/data/amf02 b/test/mitmproxy/data/amf02
index ba69f130..ba69f130 100644
--- a/test/data/amf02
+++ b/test/mitmproxy/data/amf02
Binary files differ
diff --git a/test/data/amf03 b/test/mitmproxy/data/amf03
index d9fa736a..d9fa736a 100644
--- a/test/data/amf03
+++ b/test/mitmproxy/data/amf03
Binary files differ
diff --git a/test/data/clientcert/.gitignore b/test/mitmproxy/data/clientcert/.gitignore
index 07bc53d2..07bc53d2 100644
--- a/test/data/clientcert/.gitignore
+++ b/test/mitmproxy/data/clientcert/.gitignore
diff --git a/test/data/clientcert/127.0.0.1.pem b/test/mitmproxy/data/clientcert/127.0.0.1.pem
index d7093b76..d7093b76 100644
--- a/test/data/clientcert/127.0.0.1.pem
+++ b/test/mitmproxy/data/clientcert/127.0.0.1.pem
diff --git a/test/data/clientcert/client.cnf b/test/mitmproxy/data/clientcert/client.cnf
index 5046a944..5046a944 100644
--- a/test/data/clientcert/client.cnf
+++ b/test/mitmproxy/data/clientcert/client.cnf
diff --git a/test/data/clientcert/client.pem b/test/mitmproxy/data/clientcert/client.pem
index 322e07e0..322e07e0 100644
--- a/test/data/clientcert/client.pem
+++ b/test/mitmproxy/data/clientcert/client.pem
diff --git a/test/data/clientcert/make b/test/mitmproxy/data/clientcert/make
index e829952d..e829952d 100755..100644
--- a/test/data/clientcert/make
+++ b/test/mitmproxy/data/clientcert/make
diff --git a/test/data/confdir/mitmproxy-ca-cert.cer b/test/mitmproxy/data/confdir/mitmproxy-ca-cert.cer
index cc7f8f19..cc7f8f19 100644
--- a/test/data/confdir/mitmproxy-ca-cert.cer
+++ b/test/mitmproxy/data/confdir/mitmproxy-ca-cert.cer
diff --git a/test/data/confdir/mitmproxy-ca-cert.p12 b/test/mitmproxy/data/confdir/mitmproxy-ca-cert.p12
index d4cec0d4..d4cec0d4 100644
--- a/test/data/confdir/mitmproxy-ca-cert.p12
+++ b/test/mitmproxy/data/confdir/mitmproxy-ca-cert.p12
Binary files differ
diff --git a/test/data/confdir/mitmproxy-ca-cert.pem b/test/mitmproxy/data/confdir/mitmproxy-ca-cert.pem
index cc7f8f19..cc7f8f19 100644
--- a/test/data/confdir/mitmproxy-ca-cert.pem
+++ b/test/mitmproxy/data/confdir/mitmproxy-ca-cert.pem
diff --git a/test/data/confdir/mitmproxy-ca.pem b/test/mitmproxy/data/confdir/mitmproxy-ca.pem
index 2a2343a6..2a2343a6 100644
--- a/test/data/confdir/mitmproxy-ca.pem
+++ b/test/mitmproxy/data/confdir/mitmproxy-ca.pem
diff --git a/test/data/dercert b/test/mitmproxy/data/dercert
index 370252af..370252af 100644
--- a/test/data/dercert
+++ b/test/mitmproxy/data/dercert
Binary files differ
diff --git a/test/data/dumpfile-012 b/test/mitmproxy/data/dumpfile-012
index 49c2350d..49c2350d 100644
--- a/test/data/dumpfile-012
+++ b/test/mitmproxy/data/dumpfile-012
diff --git a/test/data/dumpfile-013 b/test/mitmproxy/data/dumpfile-013
index ede06f23..ede06f23 100644
--- a/test/data/dumpfile-013
+++ b/test/mitmproxy/data/dumpfile-013
diff --git a/test/data/htpasswd b/test/mitmproxy/data/htpasswd
index 54c95b8c..54c95b8c 100644
--- a/test/data/htpasswd
+++ b/test/mitmproxy/data/htpasswd
diff --git a/test/data/htpasswd.invalid b/test/mitmproxy/data/htpasswd.invalid
index 257cc564..257cc564 100644
--- a/test/data/htpasswd.invalid
+++ b/test/mitmproxy/data/htpasswd.invalid
diff --git a/test/data/image-err1.jpg b/test/mitmproxy/data/image-err1.jpg
index 1b251e6e..1b251e6e 100644
--- a/test/data/image-err1.jpg
+++ b/test/mitmproxy/data/image-err1.jpg
Binary files differ
diff --git a/test/data/image.gif b/test/mitmproxy/data/image.gif
index 91c53284..91c53284 100644
--- a/test/data/image.gif
+++ b/test/mitmproxy/data/image.gif
Binary files differ
diff --git a/test/data/image.ico b/test/mitmproxy/data/image.ico
index 4a8421dc..4a8421dc 100644
--- a/test/data/image.ico
+++ b/test/mitmproxy/data/image.ico
Binary files differ
diff --git a/test/data/image.jpg b/test/mitmproxy/data/image.jpg
index 6cbe081e..6cbe081e 100644
--- a/test/data/image.jpg
+++ b/test/mitmproxy/data/image.jpg
Binary files differ
diff --git a/test/data/image.png b/test/mitmproxy/data/image.png
index 33143e77..33143e77 100644
--- a/test/data/image.png
+++ b/test/mitmproxy/data/image.png
Binary files differ
diff --git a/test/data/no_common_name.pem b/test/mitmproxy/data/no_common_name.pem
index fc271a0e..fc271a0e 100644
--- a/test/data/no_common_name.pem
+++ b/test/mitmproxy/data/no_common_name.pem
diff --git a/test/data/pf01 b/test/mitmproxy/data/pf01
index 3139a289..3139a289 100644
--- a/test/data/pf01
+++ b/test/mitmproxy/data/pf01
diff --git a/test/data/pf02 b/test/mitmproxy/data/pf02
index e4dc18b3..e4dc18b3 100644
--- a/test/data/pf02
+++ b/test/mitmproxy/data/pf02
diff --git a/test/data/protobuf01 b/test/mitmproxy/data/protobuf01
index fbfdbff3..fbfdbff3 100644
--- a/test/data/protobuf01
+++ b/test/mitmproxy/data/protobuf01
diff --git a/test/data/replace b/test/mitmproxy/data/replace
index ad8e760a..ad8e760a 100644
--- a/test/data/replace
+++ b/test/mitmproxy/data/replace
diff --git a/test/data/testkey.pem b/test/mitmproxy/data/testkey.pem
index af8d9d8f..af8d9d8f 100644
--- a/test/data/testkey.pem
+++ b/test/mitmproxy/data/testkey.pem
diff --git a/test/data/trusted-cadir/8117bdb9.0 b/test/mitmproxy/data/trusted-cadir/8117bdb9.0
index ae78b546..ae78b546 100644
--- a/test/data/trusted-cadir/8117bdb9.0
+++ b/test/mitmproxy/data/trusted-cadir/8117bdb9.0
diff --git a/test/data/trusted-cadir/9d45e6a9.0 b/test/mitmproxy/data/trusted-cadir/9d45e6a9.0
index ae78b546..ae78b546 100644
--- a/test/data/trusted-cadir/9d45e6a9.0
+++ b/test/mitmproxy/data/trusted-cadir/9d45e6a9.0
diff --git a/test/data/trusted-cadir/trusted-ca.pem b/test/mitmproxy/data/trusted-cadir/trusted-ca.pem
index ae78b546..ae78b546 100644
--- a/test/data/trusted-cadir/trusted-ca.pem
+++ b/test/mitmproxy/data/trusted-cadir/trusted-ca.pem
diff --git a/test/data/trusted-server.crt b/test/mitmproxy/data/trusted-server.crt
index 76f8559a..76f8559a 100644
--- a/test/data/trusted-server.crt
+++ b/test/mitmproxy/data/trusted-server.crt
diff --git a/test/data/untrusted-server.crt b/test/mitmproxy/data/untrusted-server.crt
index 62e58601..62e58601 100644
--- a/test/data/untrusted-server.crt
+++ b/test/mitmproxy/data/untrusted-server.crt
diff --git a/test/fuzzing/.env b/test/mitmproxy/fuzzing/.env
index 82ae6a8d..82ae6a8d 100644
--- a/test/fuzzing/.env
+++ b/test/mitmproxy/fuzzing/.env
diff --git a/test/fuzzing/README b/test/mitmproxy/fuzzing/README
index 2760506f..2760506f 100644
--- a/test/fuzzing/README
+++ b/test/mitmproxy/fuzzing/README
diff --git a/test/fuzzing/client_patterns b/test/mitmproxy/fuzzing/client_patterns
index 83457b6f..83457b6f 100644
--- a/test/fuzzing/client_patterns
+++ b/test/mitmproxy/fuzzing/client_patterns
diff --git a/test/fuzzing/go_proxy b/test/mitmproxy/fuzzing/go_proxy
index ea29400f..ea29400f 100755..100644
--- a/test/fuzzing/go_proxy
+++ b/test/mitmproxy/fuzzing/go_proxy
diff --git a/test/fuzzing/reverse_patterns b/test/mitmproxy/fuzzing/reverse_patterns
index 8d1d76a2..8d1d76a2 100644
--- a/test/fuzzing/reverse_patterns
+++ b/test/mitmproxy/fuzzing/reverse_patterns
diff --git a/test/fuzzing/straight_stream b/test/mitmproxy/fuzzing/straight_stream
index 41e2a6e1..41e2a6e1 100644
--- a/test/fuzzing/straight_stream
+++ b/test/mitmproxy/fuzzing/straight_stream
diff --git a/test/fuzzing/straight_stream_patterns b/test/mitmproxy/fuzzing/straight_stream_patterns
index 93a066e6..93a066e6 100644
--- a/test/fuzzing/straight_stream_patterns
+++ b/test/mitmproxy/fuzzing/straight_stream_patterns
diff --git a/test/fuzzing/straight_stream_ssl b/test/mitmproxy/fuzzing/straight_stream_ssl
index 708ff0b3..708ff0b3 100644
--- a/test/fuzzing/straight_stream_ssl
+++ b/test/mitmproxy/fuzzing/straight_stream_ssl
diff --git a/test/mock_urwid.py b/test/mitmproxy/mock_urwid.py
index 191210bf..191210bf 100644
--- a/test/mock_urwid.py
+++ b/test/mitmproxy/mock_urwid.py
diff --git a/test/scripts/a.py b/test/mitmproxy/scripts/a.py
index d4272ac8..d4272ac8 100644
--- a/test/scripts/a.py
+++ b/test/mitmproxy/scripts/a.py
diff --git a/test/scripts/a_helper.py b/test/mitmproxy/scripts/a_helper.py
index e1f1c649..e1f1c649 100644
--- a/test/scripts/a_helper.py
+++ b/test/mitmproxy/scripts/a_helper.py
diff --git a/test/scripts/all.py b/test/mitmproxy/scripts/all.py
index dad2aade..dad2aade 100644
--- a/test/scripts/all.py
+++ b/test/mitmproxy/scripts/all.py
diff --git a/test/scripts/concurrent_decorator.py b/test/mitmproxy/scripts/concurrent_decorator.py
index f6feda1d..f6feda1d 100644
--- a/test/scripts/concurrent_decorator.py
+++ b/test/mitmproxy/scripts/concurrent_decorator.py
diff --git a/test/scripts/concurrent_decorator_err.py b/test/mitmproxy/scripts/concurrent_decorator_err.py
index 00fd8dad..00fd8dad 100644
--- a/test/scripts/concurrent_decorator_err.py
+++ b/test/mitmproxy/scripts/concurrent_decorator_err.py
diff --git a/test/scripts/duplicate_flow.py b/test/mitmproxy/scripts/duplicate_flow.py
index e13af786..e13af786 100644
--- a/test/scripts/duplicate_flow.py
+++ b/test/mitmproxy/scripts/duplicate_flow.py
diff --git a/test/scripts/loaderr.py b/test/mitmproxy/scripts/loaderr.py
index 8dc4d56d..8dc4d56d 100644
--- a/test/scripts/loaderr.py
+++ b/test/mitmproxy/scripts/loaderr.py
diff --git a/test/scripts/reqerr.py b/test/mitmproxy/scripts/reqerr.py
index e7c503a8..e7c503a8 100644
--- a/test/scripts/reqerr.py
+++ b/test/mitmproxy/scripts/reqerr.py
diff --git a/test/scripts/starterr.py b/test/mitmproxy/scripts/starterr.py
index b217bdfe..b217bdfe 100644
--- a/test/scripts/starterr.py
+++ b/test/mitmproxy/scripts/starterr.py
diff --git a/test/scripts/stream_modify.py b/test/mitmproxy/scripts/stream_modify.py
index e26d83f1..e26d83f1 100644
--- a/test/scripts/stream_modify.py
+++ b/test/mitmproxy/scripts/stream_modify.py
diff --git a/test/scripts/syntaxerr.py b/test/mitmproxy/scripts/syntaxerr.py
index 219d6b84..219d6b84 100644
--- a/test/scripts/syntaxerr.py
+++ b/test/mitmproxy/scripts/syntaxerr.py
diff --git a/test/scripts/tcp_stream_modify.py b/test/mitmproxy/scripts/tcp_stream_modify.py
index 93b0d5c8..93b0d5c8 100644
--- a/test/scripts/tcp_stream_modify.py
+++ b/test/mitmproxy/scripts/tcp_stream_modify.py
diff --git a/test/scripts/unloaderr.py b/test/mitmproxy/scripts/unloaderr.py
index fba02734..fba02734 100644
--- a/test/scripts/unloaderr.py
+++ b/test/mitmproxy/scripts/unloaderr.py
diff --git a/test/test_app.py b/test/mitmproxy/test_app.py
index 577a70a5..577a70a5 100644
--- a/test/test_app.py
+++ b/test/mitmproxy/test_app.py
diff --git a/test/test_cmdline.py b/test/mitmproxy/test_cmdline.py
index 9b3317aa..9b3317aa 100644
--- a/test/test_cmdline.py
+++ b/test/mitmproxy/test_cmdline.py
diff --git a/test/test_console.py b/test/mitmproxy/test_console.py
index e64ed44a..e64ed44a 100644
--- a/test/test_console.py
+++ b/test/mitmproxy/test_console.py
diff --git a/test/test_console_common.py b/test/mitmproxy/test_console_common.py
index deba5f6c..deba5f6c 100644
--- a/test/test_console_common.py
+++ b/test/mitmproxy/test_console_common.py
diff --git a/test/test_console_help.py b/test/mitmproxy/test_console_help.py
index f1a71faf..f1a71faf 100644
--- a/test/test_console_help.py
+++ b/test/mitmproxy/test_console_help.py
diff --git a/test/test_console_palettes.py b/test/mitmproxy/test_console_palettes.py
index ac33f83d..ac33f83d 100644
--- a/test/test_console_palettes.py
+++ b/test/mitmproxy/test_console_palettes.py
diff --git a/test/test_console_pathedit.py b/test/mitmproxy/test_console_pathedit.py
index 940351f5..940351f5 100644
--- a/test/test_console_pathedit.py
+++ b/test/mitmproxy/test_console_pathedit.py
diff --git a/test/test_contentview.py b/test/mitmproxy/test_contentview.py
index af80d63a..af80d63a 100644
--- a/test/test_contentview.py
+++ b/test/mitmproxy/test_contentview.py
diff --git a/test/test_controller.py b/test/mitmproxy/test_controller.py
index ffc7d433..ffc7d433 100644
--- a/test/test_controller.py
+++ b/test/mitmproxy/test_controller.py
diff --git a/test/test_custom_contentview.py b/test/mitmproxy/test_custom_contentview.py
index adc4109b..adc4109b 100644
--- a/test/test_custom_contentview.py
+++ b/test/mitmproxy/test_custom_contentview.py
diff --git a/test/test_dump.py b/test/mitmproxy/test_dump.py
index dbd0c653..dbd0c653 100644
--- a/test/test_dump.py
+++ b/test/mitmproxy/test_dump.py
diff --git a/test/test_examples.py b/test/mitmproxy/test_examples.py
index bb7c596b..bb7c596b 100644
--- a/test/test_examples.py
+++ b/test/mitmproxy/test_examples.py
diff --git a/test/test_filt.py b/test/mitmproxy/test_filt.py
index e6873c7d..e6873c7d 100644
--- a/test/test_filt.py
+++ b/test/mitmproxy/test_filt.py
diff --git a/test/test_flow.py b/test/mitmproxy/test_flow.py
index b122489f..b122489f 100644
--- a/test/test_flow.py
+++ b/test/mitmproxy/test_flow.py
diff --git a/test/test_flow_export.py b/test/mitmproxy/test_flow_export.py
index e5e9c0a3..e5e9c0a3 100644
--- a/test/test_flow_export.py
+++ b/test/mitmproxy/test_flow_export.py
diff --git a/test/test_flow_format_compat.py b/test/mitmproxy/test_flow_format_compat.py
index 232f5473..232f5473 100644
--- a/test/test_flow_format_compat.py
+++ b/test/mitmproxy/test_flow_format_compat.py
diff --git a/test/test_fuzzing.py b/test/mitmproxy/test_fuzzing.py
index cec64f58..cec64f58 100644
--- a/test/test_fuzzing.py
+++ b/test/mitmproxy/test_fuzzing.py
diff --git a/test/test_platform_pf.py b/test/mitmproxy/test_platform_pf.py
index 8994ee0d..8994ee0d 100644
--- a/test/test_platform_pf.py
+++ b/test/mitmproxy/test_platform_pf.py
diff --git a/test/test_protocol_http1.py b/test/mitmproxy/test_protocol_http1.py
index 13e0eabe..13e0eabe 100644
--- a/test/test_protocol_http1.py
+++ b/test/mitmproxy/test_protocol_http1.py
diff --git a/test/test_protocol_http2.py b/test/mitmproxy/test_protocol_http2.py
index c2c736af..6d3bb43f 100644
--- a/test/test_protocol_http2.py
+++ b/test/mitmproxy/test_protocol_http2.py
@@ -26,7 +26,7 @@ import h2
from . import tservers
requires_alpn = pytest.mark.skipif(
- not OpenSSL._util.lib.Cryptography_HAS_ALPN,
+ not netlib.tcp.HAS_ALPN,
reason="requires OpenSSL with ALPN support")
diff --git a/test/test_proxy.py b/test/mitmproxy/test_proxy.py
index 27ae70a8..27ae70a8 100644
--- a/test/test_proxy.py
+++ b/test/mitmproxy/test_proxy.py
diff --git a/test/test_script.py b/test/mitmproxy/test_script.py
index f3a6499e..f3a6499e 100644
--- a/test/test_script.py
+++ b/test/mitmproxy/test_script.py
diff --git a/test/test_server.py b/test/mitmproxy/test_server.py
index 1b7e6966..1b7e6966 100644
--- a/test/test_server.py
+++ b/test/mitmproxy/test_server.py
diff --git a/test/test_utils.py b/test/mitmproxy/test_utils.py
index 17bf3dbf..17bf3dbf 100644
--- a/test/test_utils.py
+++ b/test/mitmproxy/test_utils.py
diff --git a/test/tools/1024example b/test/mitmproxy/tools/1024example
index 78af7ed0..78af7ed0 100644
--- a/test/tools/1024example
+++ b/test/mitmproxy/tools/1024example
diff --git a/test/tools/ab.exe b/test/mitmproxy/tools/ab.exe
index d68ed0f3..d68ed0f3 100644
--- a/test/tools/ab.exe
+++ b/test/mitmproxy/tools/ab.exe
Binary files differ
diff --git a/test/tools/bench.py b/test/mitmproxy/tools/bench.py
index 8127d083..8127d083 100644
--- a/test/tools/bench.py
+++ b/test/mitmproxy/tools/bench.py
diff --git a/test/tools/benchtool.py b/test/mitmproxy/tools/benchtool.py
index a1d80697..a1d80697 100644
--- a/test/tools/benchtool.py
+++ b/test/mitmproxy/tools/benchtool.py
diff --git a/test/tools/getcert b/test/mitmproxy/tools/getcert
index 3bd2bec8..3bd2bec8 100755..100644
--- a/test/tools/getcert
+++ b/test/mitmproxy/tools/getcert
diff --git a/test/tools/inspect_dumpfile.py b/test/mitmproxy/tools/inspect_dumpfile.py
index d15e9e8a..d15e9e8a 100644
--- a/test/tools/inspect_dumpfile.py
+++ b/test/mitmproxy/tools/inspect_dumpfile.py
diff --git a/test/tools/memoryleak.py b/test/mitmproxy/tools/memoryleak.py
index 259309a6..259309a6 100644
--- a/test/tools/memoryleak.py
+++ b/test/mitmproxy/tools/memoryleak.py
diff --git a/test/tools/passive_close.py b/test/mitmproxy/tools/passive_close.py
index 5b1bd451..5b1bd451 100644
--- a/test/tools/passive_close.py
+++ b/test/mitmproxy/tools/passive_close.py
diff --git a/test/tools/testpatt b/test/mitmproxy/tools/testpatt
index b41011c0..b41011c0 100755..100644
--- a/test/tools/testpatt
+++ b/test/mitmproxy/tools/testpatt
diff --git a/test/tservers.py b/test/mitmproxy/tservers.py
index dbc9f7d0..dbc9f7d0 100644
--- a/test/tservers.py
+++ b/test/mitmproxy/tservers.py
diff --git a/test/tutils.py b/test/mitmproxy/tutils.py
index 2ce0884d..2ce0884d 100644
--- a/test/tutils.py
+++ b/test/mitmproxy/tutils.py
diff --git a/test/netlib/__init__.py b/test/netlib/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/netlib/__init__.py
diff --git a/test/netlib/data/clientcert/.gitignore b/test/netlib/data/clientcert/.gitignore
new file mode 100644
index 00000000..07bc53d2
--- /dev/null
+++ b/test/netlib/data/clientcert/.gitignore
@@ -0,0 +1,3 @@
+client.crt
+client.key
+client.req
diff --git a/test/netlib/data/clientcert/client.cnf b/test/netlib/data/clientcert/client.cnf
new file mode 100644
index 00000000..5046a944
--- /dev/null
+++ b/test/netlib/data/clientcert/client.cnf
@@ -0,0 +1,5 @@
+[ ssl_client ]
+basicConstraints = CA:FALSE
+nsCertType = client
+keyUsage = digitalSignature, keyEncipherment
+extendedKeyUsage = clientAuth
diff --git a/test/netlib/data/clientcert/client.pem b/test/netlib/data/clientcert/client.pem
new file mode 100644
index 00000000..4927bca2
--- /dev/null
+++ b/test/netlib/data/clientcert/client.pem
@@ -0,0 +1,42 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAzCpoRjSTfIN24kkNap/GYmP9zVWj0Gk8R5BB/PvvN0OB1Zk0
+EEYPsWCcuhEdK0ehiDZX030doF0DOncKKa6mop/d0x2o+ts42peDhZM6JNUrm6d+
+ZWQVtio33mpp77UMhR093vaA+ExDnmE26kBTVijJ1+fRAVDXG/cmQINEri91Kk/G
+3YJ5e45UrohGI5seBZ4vV0xbHtmczFRhYFlGOvYsoIe4Lvz/eFS2pIrTIpYQ2VM/
+SQQl+JFy+NlQRsWG2NrxtKOzMnnDE7YN4I3z5D5eZFo1EtwZ48LNCeSwrEOdfuzP
+G5q5qbs5KpE/x85H9umuRwSCIArbMwBYV8a8JwIDAQABAoIBAFE3FV/IDltbmHEP
+iky93hbJm+6QgKepFReKpRVTyqb7LaygUvueQyPWQMIriKTsy675nxo8DQr7tQsO
+y3YlSZgra/xNMikIB6e82c7K8DgyrDQw/rCqjZB3Xt4VCqsWJDLXnQMSn98lx0g7
+d7Lbf8soUpKWXqfdVpSDTi4fibSX6kshXyfSTpcz4AdoncEpViUfU1xkEEmZrjT8
+1GcCsDC41xdNmzCpqRuZX7DKSFRoB+0hUzsC1oiqM7FD5kixonRd4F5PbRXImIzt
+6YCsT2okxTA04jX7yByis7LlOLTlkmLtKQYuc3erOFvwx89s4vW+AeFei+GGNitn
+tHfSwbECgYEA7SzV+nN62hAERHlg8cEQT4TxnsWvbronYWcc/ev44eHSPDWL5tPi
+GHfSbW6YAq5Wa0I9jMWfXyhOYEC3MZTC5EEeLOB71qVrTwcy/sY66rOrcgjFI76Q
+5JFHQ4wy3SWU50KxE0oWJO9LIowprG+pW1vzqC3VF0T7q0FqESrY4LUCgYEA3F7Z
+80ndnCUlooJAb+Hfotv7peFf1o6+m1PTRcz1lLnVt5R5lXj86kn+tXEpYZo1RiGR
+2rE2N0seeznWCooakHcsBN7/qmFIhhooJNF7yW+JP2I4P2UV5+tJ+8bcs/voUkQD
+1x+rGOuMn8nvHBd2+Vharft8eGL2mgooPVI2XusCgYEAlMZpO3+w8pTVeHaDP2MR
+7i/AuQ3cbCLNjSX3Y7jgGCFllWspZRRIYXzYPNkA9b2SbBnTLjjRLgnEkFBIGgvs
+7O2EFjaCuDRvydUEQhjq4ErwIsopj7B8h0QyZcbOKTbn3uFQ3n68wVJx2Sv/ADHT
+FIHrp/WIE96r19Niy34LKXkCgYB2W59VsuOKnMz01l5DeR5C+0HSWxS9SReIl2IO
+yEFSKullWyJeLIgyUaGy0990430feKI8whcrZXYumuah7IDN/KOwzhCk8vEfzWao
+N7bzfqtJVrh9HA7C7DVlO+6H4JFrtcoWPZUIomJ549w/yz6EN3ckoMC+a/Ck1TW9
+ka1QFwKBgQCywG6TrZz0UmOjyLQZ+8Q4uvZklSW5NAKBkNnyuQ2kd5rzyYgMPE8C
+Er8T88fdVIKvkhDyHhwcI7n58xE5Gr7wkwsrk/Hbd9/ZB2GgAPY3cATskK1v1McU
+YeX38CU0fUS4aoy26hWQXkViB47IGQ3jWo3ZCtzIJl8DI9/RsBWTnw==
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIICYDCCAckCAQEwDQYJKoZIhvcNAQEFBQAwKDESMBAGA1UEAxMJbWl0bXByb3h5
+MRIwEAYDVQQKEwltaXRtcHJveHkwHhcNMTMwMTIwMDEwODEzWhcNMTUxMDE3MDEw
+ODEzWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UE
+ChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAzCpoRjSTfIN24kkNap/GYmP9zVWj0Gk8R5BB/PvvN0OB1Zk0
+EEYPsWCcuhEdK0ehiDZX030doF0DOncKKa6mop/d0x2o+ts42peDhZM6JNUrm6d+
+ZWQVtio33mpp77UMhR093vaA+ExDnmE26kBTVijJ1+fRAVDXG/cmQINEri91Kk/G
+3YJ5e45UrohGI5seBZ4vV0xbHtmczFRhYFlGOvYsoIe4Lvz/eFS2pIrTIpYQ2VM/
+SQQl+JFy+NlQRsWG2NrxtKOzMnnDE7YN4I3z5D5eZFo1EtwZ48LNCeSwrEOdfuzP
+G5q5qbs5KpE/x85H9umuRwSCIArbMwBYV8a8JwIDAQABMA0GCSqGSIb3DQEBBQUA
+A4GBAFvI+cd47B85PQ970n2dU/PlA2/Hb1ldrrXh2guR4hX6vYx/uuk5yRI/n0Rd
+KOXJ3czO0bd2Fpe3ZoNpkW0pOSDej/Q+58ScuJd0gWCT/Sh1eRk6ZdC0kusOuWoY
+bPOPMkG45LPgUMFOnZEsfJP6P5mZIxlbCvSMFC25nPHWlct7
+-----END CERTIFICATE-----
diff --git a/test/netlib/data/clientcert/make b/test/netlib/data/clientcert/make
new file mode 100644
index 00000000..d1caea81
--- /dev/null
+++ b/test/netlib/data/clientcert/make
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+openssl genrsa -out client.key 2048
+openssl req -key client.key -new -out client.req
+openssl x509 -req -days 365 -in client.req -signkey client.key -out client.crt -extfile client.cnf -extensions ssl_client
+openssl x509 -req -days 1000 -in client.req -CA ~/.mitmproxy/mitmproxy-ca.pem -CAkey ~/.mitmproxy/mitmproxy-ca.pem -set_serial 00001 -out client.crt -extensions ssl_client
+cat client.key client.crt > client.pem
+openssl x509 -text -noout -in client.pem
diff --git a/test/netlib/data/dercert b/test/netlib/data/dercert
new file mode 100644
index 00000000..370252af
--- /dev/null
+++ b/test/netlib/data/dercert
Binary files differ
diff --git a/test/netlib/data/dhparam.pem b/test/netlib/data/dhparam.pem
new file mode 100644
index 00000000..afb41672
--- /dev/null
+++ b/test/netlib/data/dhparam.pem
@@ -0,0 +1,13 @@
+-----BEGIN DH PARAMETERS-----
+MIICCAKCAgEAyT6LzpwVFS3gryIo29J5icvgxCnCebcdSe/NHMkD8dKJf8suFCg3
+O2+dguLakSVif/t6dhImxInJk230HmfC8q93hdcg/j8rLGJYDKu3ik6H//BAHKIv
+j5O9yjU3rXCfmVJQic2Nne39sg3CreAepEts2TvYHhVv3TEAzEqCtOuTjgDv0ntJ
+Gwpj+BJBRQGG9NvprX1YGJ7WOFBP/hWU7d6tgvE6Xa7T/u9QIKpYHMIkcN/l3ZFB
+chZEqVlyrcngtSXCROTPcDOQ6Q8QzhaBJS+Z6rcsd7X+haiQqvoFcmaJ08Ks6LQC
+ZIL2EtYJw8V8z7C0igVEBIADZBI6OTbuuhDwRw//zU1uq52Oc48CIZlGxTYG/Evq
+o9EWAXUYVzWkDSTeBH1r4z/qLPE2cnhtMxbFxuvK53jGB0emy2y1Ei6IhKshJ5qX
+IB/aE7SSHyQ3MDHHkCmQJCsOd4Mo26YX61NZ+n501XjqpCBQ2+DfZCBh8Va2wDyv
+A2Ryg9SUz8j0AXViRNMJgJrr446yro/FuJZwnQcO3WQnXeqSBnURqKjmqkeFP+d8
+6mk2tqJaY507lRNqtGlLnj7f5RNoBFJDCLBNurVgfvq9TCVWKDIFD4vZRjCrnl6I
+rD693XKIHUCWOjMh1if6omGXKHH40QuME2gNa50+YPn1iYDl88uDbbMCAQI=
+-----END DH PARAMETERS-----
diff --git a/test/netlib/data/htpasswd b/test/netlib/data/htpasswd
new file mode 100644
index 00000000..54c95b8c
--- /dev/null
+++ b/test/netlib/data/htpasswd
@@ -0,0 +1 @@
+test:$apr1$/LkYxy3x$WI4.YbiJlu537jLGEW2eu1
diff --git a/test/netlib/data/server.crt b/test/netlib/data/server.crt
new file mode 100644
index 00000000..68f61bac
--- /dev/null
+++ b/test/netlib/data/server.crt
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICOzCCAaQCCQDC7f5GsEpo9jANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJO
+WjEOMAwGA1UECBMFT3RhZ28xEDAOBgNVBAcTB0R1bmVkaW4xDzANBgNVBAoTBm5l
+dGxpYjEPMA0GA1UECxMGbmV0bGliMQ8wDQYDVQQDEwZuZXRsaWIwHhcNMTIwNjI0
+MjI0MTU0WhcNMjIwNjIyMjI0MTU0WjBiMQswCQYDVQQGEwJOWjEOMAwGA1UECBMF
+T3RhZ28xEDAOBgNVBAcTB0R1bmVkaW4xDzANBgNVBAoTBm5ldGxpYjEPMA0GA1UE
+CxMGbmV0bGliMQ8wDQYDVQQDEwZuZXRsaWIwgZ8wDQYJKoZIhvcNAQEBBQADgY0A
+MIGJAoGBALJSVEl9y3QUSYuXTH0UjBOPQgS0nHmNWej9hjqnA0KWvEnGY+c6yQeP
+/rmwswlKw1iVV5o8kRK9Wej88YWQl/hl/xruyeJgGic0+yqY/FcueZxRudwBcWu2
+7+46aEftwLLRF0GwHZxX/HwWME+TcCXGpXGSG2qs921M4iVeBn5hAgMBAAEwDQYJ
+KoZIhvcNAQEFBQADgYEAODZCihEv2yr8zmmQZDrfqg2ChxAoOXWF5+W2F/0LAUBf
+2bHP+K4XE6BJWmadX1xKngj7SWrhmmTDp1gBAvXURoDaScOkB1iOCOHoIyalscTR
+0FvSHKqFF8fgSlfqS6eYaSbXU3zQolvwP+URzIVnGDqgQCWPtjMqLD3Kd5tuwos=
+-----END CERTIFICATE-----
diff --git a/test/netlib/data/server.key b/test/netlib/data/server.key
new file mode 100644
index 00000000..b1b658ab
--- /dev/null
+++ b/test/netlib/data/server.key
@@ -0,0 +1,15 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQCyUlRJfct0FEmLl0x9FIwTj0IEtJx5jVno/YY6pwNClrxJxmPn
+OskHj/65sLMJSsNYlVeaPJESvVno/PGFkJf4Zf8a7sniYBonNPsqmPxXLnmcUbnc
+AXFrtu/uOmhH7cCy0RdBsB2cV/x8FjBPk3AlxqVxkhtqrPdtTOIlXgZ+YQIDAQAB
+AoGAQEpGcSiVTYhy64zk2sOprPOdTa0ALSK1I7cjycmk90D5KXAJXLho+f0ETVZT
+dioqO6m8J7NmamcyHznyqcDzyNRqD2hEBDGVRJWmpOjIER/JwWLNNbpeVjsMHV8I
+40P5rZMOhBPYlwECSC5NtMwaN472fyGNNze8u37IZKiER/ECQQDe1iY5AG3CgkP3
+tEZB3Vtzcn4PoOr3Utyn1YER34lPqAmeAsWUhmAVEfR3N1HDe1VFD9s2BidhBn1a
+/Bgqxz4DAkEAzNw0m+uO0WkD7aEYRBW7SbXCX+3xsbVToIWC1jXFG+XDzSWn++c1
+DMXEElzEJxPDA+FzQUvRTml4P92bTAbGywJAS9H7wWtm7Ubbj33UZfbGdhqfz/uF
+109naufXedhgZS0c0JnK1oV+Tc0FLEczV9swIUaK5O/lGDtYDcw3AN84NwJBAIw5
+/1jrOOtm8uVp6+5O4dBmthJsEZEPCZtLSG/Qhoe+EvUN3Zq0fL+tb7USAsKs6ERz
+wizj9PWzhDhTPMYhrVkCQGIponZHx6VqiFyLgYUH9+gDTjBhYyI+6yMTYzcRweyL
+9Suc2NkS3X2Lp+wCjvVZdwGtStp6Vo8z02b3giIsAIY=
+-----END RSA PRIVATE KEY-----
diff --git a/test/netlib/data/text_cert b/test/netlib/data/text_cert
new file mode 100644
index 00000000..36ca33b9
--- /dev/null
+++ b/test/netlib/data/text_cert
@@ -0,0 +1,145 @@
+-----BEGIN CERTIFICATE-----
+MIIadTCCGd6gAwIBAgIGR09PUAFtMA0GCSqGSIb3DQEBBQUAMEYxCzAJBgNVBAYT
+AlVTMRMwEQYDVQQKEwpHb29nbGUgSW5jMSIwIAYDVQQDExlHb29nbGUgSW50ZXJu
+ZXQgQXV0aG9yaXR5MB4XDTEyMDExNzEyNTUwNFoXDTEzMDExNzEyNTUwNFowTDEL
+MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEzARBgNVBAoTCkdvb2ds
+ZSBJbmMxEzARBgNVBAMTCmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0A
+MIGJAoGBALofcxR2fud5cyFIeld9pj2vGB5GH0y9tmAYa5t33xbJguKKX/el3tXA
+KMNiT1SZzu8ELJ1Ey0GcBAgHA9jVPQd0LGdbEtNIxjblAsWAD/FZlSt8X87h7C5w
+2JSefOani0qgQqU6sTdsaCUGZ+Eu7D0lBfT5/Vnl2vV+zI3YmDlpAgMBAAGjghhm
+MIIYYjAdBgNVHQ4EFgQUL3+JeC/oL9jZhTp3F550LautzV8wHwYDVR0jBBgwFoAU
+v8Aw6/VDET5nup6R+/xq2uNrEiQwWwYDVR0fBFQwUjBQoE6gTIZKaHR0cDovL3d3
+dy5nc3RhdGljLmNvbS9Hb29nbGVJbnRlcm5ldEF1dGhvcml0eS9Hb29nbGVJbnRl
+cm5ldEF1dGhvcml0eS5jcmwwZgYIKwYBBQUHAQEEWjBYMFYGCCsGAQUFBzAChkpo
+dHRwOi8vd3d3LmdzdGF0aWMuY29tL0dvb2dsZUludGVybmV0QXV0aG9yaXR5L0dv
+b2dsZUludGVybmV0QXV0aG9yaXR5LmNydDCCF1kGA1UdEQSCF1AwghdMggpnb29n
+bGUuY29tggwqLmdvb2dsZS5jb22CCyouZ29vZ2xlLmFjggsqLmdvb2dsZS5hZIIL
+Ki5nb29nbGUuYWWCCyouZ29vZ2xlLmFmggsqLmdvb2dsZS5hZ4ILKi5nb29nbGUu
+YW2CCyouZ29vZ2xlLmFzggsqLmdvb2dsZS5hdIILKi5nb29nbGUuYXqCCyouZ29v
+Z2xlLmJhggsqLmdvb2dsZS5iZYILKi5nb29nbGUuYmaCCyouZ29vZ2xlLmJnggsq
+Lmdvb2dsZS5iaYILKi5nb29nbGUuYmqCCyouZ29vZ2xlLmJzggsqLmdvb2dsZS5i
+eYILKi5nb29nbGUuY2GCDCouZ29vZ2xlLmNhdIILKi5nb29nbGUuY2OCCyouZ29v
+Z2xlLmNkggsqLmdvb2dsZS5jZoILKi5nb29nbGUuY2eCCyouZ29vZ2xlLmNoggsq
+Lmdvb2dsZS5jaYILKi5nb29nbGUuY2yCCyouZ29vZ2xlLmNtggsqLmdvb2dsZS5j
+boIOKi5nb29nbGUuY28uYW+CDiouZ29vZ2xlLmNvLmJ3gg4qLmdvb2dsZS5jby5j
+a4IOKi5nb29nbGUuY28uY3KCDiouZ29vZ2xlLmNvLmh1gg4qLmdvb2dsZS5jby5p
+ZIIOKi5nb29nbGUuY28uaWyCDiouZ29vZ2xlLmNvLmltgg4qLmdvb2dsZS5jby5p
+boIOKi5nb29nbGUuY28uamWCDiouZ29vZ2xlLmNvLmpwgg4qLmdvb2dsZS5jby5r
+ZYIOKi5nb29nbGUuY28ua3KCDiouZ29vZ2xlLmNvLmxzgg4qLmdvb2dsZS5jby5t
+YYIOKi5nb29nbGUuY28ubXqCDiouZ29vZ2xlLmNvLm56gg4qLmdvb2dsZS5jby50
+aIIOKi5nb29nbGUuY28udHqCDiouZ29vZ2xlLmNvLnVngg4qLmdvb2dsZS5jby51
+a4IOKi5nb29nbGUuY28udXqCDiouZ29vZ2xlLmNvLnZlgg4qLmdvb2dsZS5jby52
+aYIOKi5nb29nbGUuY28uemGCDiouZ29vZ2xlLmNvLnptgg4qLmdvb2dsZS5jby56
+d4IPKi5nb29nbGUuY29tLmFmgg8qLmdvb2dsZS5jb20uYWeCDyouZ29vZ2xlLmNv
+bS5haYIPKi5nb29nbGUuY29tLmFygg8qLmdvb2dsZS5jb20uYXWCDyouZ29vZ2xl
+LmNvbS5iZIIPKi5nb29nbGUuY29tLmJogg8qLmdvb2dsZS5jb20uYm6CDyouZ29v
+Z2xlLmNvbS5ib4IPKi5nb29nbGUuY29tLmJygg8qLmdvb2dsZS5jb20uYnmCDyou
+Z29vZ2xlLmNvbS5ieoIPKi5nb29nbGUuY29tLmNugg8qLmdvb2dsZS5jb20uY2+C
+DyouZ29vZ2xlLmNvbS5jdYIPKi5nb29nbGUuY29tLmN5gg8qLmdvb2dsZS5jb20u
+ZG+CDyouZ29vZ2xlLmNvbS5lY4IPKi5nb29nbGUuY29tLmVngg8qLmdvb2dsZS5j
+b20uZXSCDyouZ29vZ2xlLmNvbS5maoIPKi5nb29nbGUuY29tLmdlgg8qLmdvb2ds
+ZS5jb20uZ2iCDyouZ29vZ2xlLmNvbS5naYIPKi5nb29nbGUuY29tLmdygg8qLmdv
+b2dsZS5jb20uZ3SCDyouZ29vZ2xlLmNvbS5oa4IPKi5nb29nbGUuY29tLmlxgg8q
+Lmdvb2dsZS5jb20uam2CDyouZ29vZ2xlLmNvbS5qb4IPKi5nb29nbGUuY29tLmto
+gg8qLmdvb2dsZS5jb20ua3eCDyouZ29vZ2xlLmNvbS5sYoIPKi5nb29nbGUuY29t
+Lmx5gg8qLmdvb2dsZS5jb20ubXSCDyouZ29vZ2xlLmNvbS5teIIPKi5nb29nbGUu
+Y29tLm15gg8qLmdvb2dsZS5jb20ubmGCDyouZ29vZ2xlLmNvbS5uZoIPKi5nb29n
+bGUuY29tLm5ngg8qLmdvb2dsZS5jb20ubmmCDyouZ29vZ2xlLmNvbS5ucIIPKi5n
+b29nbGUuY29tLm5ygg8qLmdvb2dsZS5jb20ub22CDyouZ29vZ2xlLmNvbS5wYYIP
+Ki5nb29nbGUuY29tLnBlgg8qLmdvb2dsZS5jb20ucGiCDyouZ29vZ2xlLmNvbS5w
+a4IPKi5nb29nbGUuY29tLnBsgg8qLmdvb2dsZS5jb20ucHKCDyouZ29vZ2xlLmNv
+bS5weYIPKi5nb29nbGUuY29tLnFhgg8qLmdvb2dsZS5jb20ucnWCDyouZ29vZ2xl
+LmNvbS5zYYIPKi5nb29nbGUuY29tLnNigg8qLmdvb2dsZS5jb20uc2eCDyouZ29v
+Z2xlLmNvbS5zbIIPKi5nb29nbGUuY29tLnN2gg8qLmdvb2dsZS5jb20udGqCDyou
+Z29vZ2xlLmNvbS50boIPKi5nb29nbGUuY29tLnRygg8qLmdvb2dsZS5jb20udHeC
+DyouZ29vZ2xlLmNvbS51YYIPKi5nb29nbGUuY29tLnV5gg8qLmdvb2dsZS5jb20u
+dmOCDyouZ29vZ2xlLmNvbS52ZYIPKi5nb29nbGUuY29tLnZuggsqLmdvb2dsZS5j
+doILKi5nb29nbGUuY3qCCyouZ29vZ2xlLmRlggsqLmdvb2dsZS5kaoILKi5nb29n
+bGUuZGuCCyouZ29vZ2xlLmRtggsqLmdvb2dsZS5keoILKi5nb29nbGUuZWWCCyou
+Z29vZ2xlLmVzggsqLmdvb2dsZS5maYILKi5nb29nbGUuZm2CCyouZ29vZ2xlLmZy
+ggsqLmdvb2dsZS5nYYILKi5nb29nbGUuZ2WCCyouZ29vZ2xlLmdnggsqLmdvb2ds
+ZS5nbIILKi5nb29nbGUuZ22CCyouZ29vZ2xlLmdwggsqLmdvb2dsZS5ncoILKi5n
+b29nbGUuZ3mCCyouZ29vZ2xlLmhrggsqLmdvb2dsZS5oboILKi5nb29nbGUuaHKC
+CyouZ29vZ2xlLmh0ggsqLmdvb2dsZS5odYILKi5nb29nbGUuaWWCCyouZ29vZ2xl
+Lmltgg0qLmdvb2dsZS5pbmZvggsqLmdvb2dsZS5pcYILKi5nb29nbGUuaXOCCyou
+Z29vZ2xlLml0gg4qLmdvb2dsZS5pdC5hb4ILKi5nb29nbGUuamWCCyouZ29vZ2xl
+Lmpvgg0qLmdvb2dsZS5qb2JzggsqLmdvb2dsZS5qcIILKi5nb29nbGUua2eCCyou
+Z29vZ2xlLmtpggsqLmdvb2dsZS5reoILKi5nb29nbGUubGGCCyouZ29vZ2xlLmxp
+ggsqLmdvb2dsZS5sa4ILKi5nb29nbGUubHSCCyouZ29vZ2xlLmx1ggsqLmdvb2ds
+ZS5sdoILKi5nb29nbGUubWSCCyouZ29vZ2xlLm1lggsqLmdvb2dsZS5tZ4ILKi5n
+b29nbGUubWuCCyouZ29vZ2xlLm1sggsqLmdvb2dsZS5tboILKi5nb29nbGUubXOC
+CyouZ29vZ2xlLm11ggsqLmdvb2dsZS5tdoILKi5nb29nbGUubXeCCyouZ29vZ2xl
+Lm5lgg4qLmdvb2dsZS5uZS5qcIIMKi5nb29nbGUubmV0ggsqLmdvb2dsZS5ubIIL
+Ki5nb29nbGUubm+CCyouZ29vZ2xlLm5yggsqLmdvb2dsZS5udYIPKi5nb29nbGUu
+b2ZmLmFpggsqLmdvb2dsZS5wa4ILKi5nb29nbGUucGyCCyouZ29vZ2xlLnBuggsq
+Lmdvb2dsZS5wc4ILKi5nb29nbGUucHSCCyouZ29vZ2xlLnJvggsqLmdvb2dsZS5y
+c4ILKi5nb29nbGUucnWCCyouZ29vZ2xlLnJ3ggsqLmdvb2dsZS5zY4ILKi5nb29n
+bGUuc2WCCyouZ29vZ2xlLnNoggsqLmdvb2dsZS5zaYILKi5nb29nbGUuc2uCCyou
+Z29vZ2xlLnNtggsqLmdvb2dsZS5zboILKi5nb29nbGUuc2+CCyouZ29vZ2xlLnN0
+ggsqLmdvb2dsZS50ZIILKi5nb29nbGUudGeCCyouZ29vZ2xlLnRrggsqLmdvb2ds
+ZS50bIILKi5nb29nbGUudG2CCyouZ29vZ2xlLnRuggsqLmdvb2dsZS50b4ILKi5n
+b29nbGUudHCCCyouZ29vZ2xlLnR0ggsqLmdvb2dsZS51c4ILKi5nb29nbGUudXqC
+CyouZ29vZ2xlLnZnggsqLmdvb2dsZS52dYILKi5nb29nbGUud3OCCWdvb2dsZS5h
+Y4IJZ29vZ2xlLmFkgglnb29nbGUuYWWCCWdvb2dsZS5hZoIJZ29vZ2xlLmFnggln
+b29nbGUuYW2CCWdvb2dsZS5hc4IJZ29vZ2xlLmF0gglnb29nbGUuYXqCCWdvb2ds
+ZS5iYYIJZ29vZ2xlLmJlgglnb29nbGUuYmaCCWdvb2dsZS5iZ4IJZ29vZ2xlLmJp
+gglnb29nbGUuYmqCCWdvb2dsZS5ic4IJZ29vZ2xlLmJ5gglnb29nbGUuY2GCCmdv
+b2dsZS5jYXSCCWdvb2dsZS5jY4IJZ29vZ2xlLmNkgglnb29nbGUuY2aCCWdvb2ds
+ZS5jZ4IJZ29vZ2xlLmNogglnb29nbGUuY2mCCWdvb2dsZS5jbIIJZ29vZ2xlLmNt
+gglnb29nbGUuY26CDGdvb2dsZS5jby5hb4IMZ29vZ2xlLmNvLmJ3ggxnb29nbGUu
+Y28uY2uCDGdvb2dsZS5jby5jcoIMZ29vZ2xlLmNvLmh1ggxnb29nbGUuY28uaWSC
+DGdvb2dsZS5jby5pbIIMZ29vZ2xlLmNvLmltggxnb29nbGUuY28uaW6CDGdvb2ds
+ZS5jby5qZYIMZ29vZ2xlLmNvLmpwggxnb29nbGUuY28ua2WCDGdvb2dsZS5jby5r
+coIMZ29vZ2xlLmNvLmxzggxnb29nbGUuY28ubWGCDGdvb2dsZS5jby5teoIMZ29v
+Z2xlLmNvLm56ggxnb29nbGUuY28udGiCDGdvb2dsZS5jby50eoIMZ29vZ2xlLmNv
+LnVnggxnb29nbGUuY28udWuCDGdvb2dsZS5jby51eoIMZ29vZ2xlLmNvLnZlggxn
+b29nbGUuY28udmmCDGdvb2dsZS5jby56YYIMZ29vZ2xlLmNvLnptggxnb29nbGUu
+Y28ueneCDWdvb2dsZS5jb20uYWaCDWdvb2dsZS5jb20uYWeCDWdvb2dsZS5jb20u
+YWmCDWdvb2dsZS5jb20uYXKCDWdvb2dsZS5jb20uYXWCDWdvb2dsZS5jb20uYmSC
+DWdvb2dsZS5jb20uYmiCDWdvb2dsZS5jb20uYm6CDWdvb2dsZS5jb20uYm+CDWdv
+b2dsZS5jb20uYnKCDWdvb2dsZS5jb20uYnmCDWdvb2dsZS5jb20uYnqCDWdvb2ds
+ZS5jb20uY26CDWdvb2dsZS5jb20uY2+CDWdvb2dsZS5jb20uY3WCDWdvb2dsZS5j
+b20uY3mCDWdvb2dsZS5jb20uZG+CDWdvb2dsZS5jb20uZWOCDWdvb2dsZS5jb20u
+ZWeCDWdvb2dsZS5jb20uZXSCDWdvb2dsZS5jb20uZmqCDWdvb2dsZS5jb20uZ2WC
+DWdvb2dsZS5jb20uZ2iCDWdvb2dsZS5jb20uZ2mCDWdvb2dsZS5jb20uZ3KCDWdv
+b2dsZS5jb20uZ3SCDWdvb2dsZS5jb20uaGuCDWdvb2dsZS5jb20uaXGCDWdvb2ds
+ZS5jb20uam2CDWdvb2dsZS5jb20uam+CDWdvb2dsZS5jb20ua2iCDWdvb2dsZS5j
+b20ua3eCDWdvb2dsZS5jb20ubGKCDWdvb2dsZS5jb20ubHmCDWdvb2dsZS5jb20u
+bXSCDWdvb2dsZS5jb20ubXiCDWdvb2dsZS5jb20ubXmCDWdvb2dsZS5jb20ubmGC
+DWdvb2dsZS5jb20ubmaCDWdvb2dsZS5jb20ubmeCDWdvb2dsZS5jb20ubmmCDWdv
+b2dsZS5jb20ubnCCDWdvb2dsZS5jb20ubnKCDWdvb2dsZS5jb20ub22CDWdvb2ds
+ZS5jb20ucGGCDWdvb2dsZS5jb20ucGWCDWdvb2dsZS5jb20ucGiCDWdvb2dsZS5j
+b20ucGuCDWdvb2dsZS5jb20ucGyCDWdvb2dsZS5jb20ucHKCDWdvb2dsZS5jb20u
+cHmCDWdvb2dsZS5jb20ucWGCDWdvb2dsZS5jb20ucnWCDWdvb2dsZS5jb20uc2GC
+DWdvb2dsZS5jb20uc2KCDWdvb2dsZS5jb20uc2eCDWdvb2dsZS5jb20uc2yCDWdv
+b2dsZS5jb20uc3aCDWdvb2dsZS5jb20udGqCDWdvb2dsZS5jb20udG6CDWdvb2ds
+ZS5jb20udHKCDWdvb2dsZS5jb20udHeCDWdvb2dsZS5jb20udWGCDWdvb2dsZS5j
+b20udXmCDWdvb2dsZS5jb20udmOCDWdvb2dsZS5jb20udmWCDWdvb2dsZS5jb20u
+dm6CCWdvb2dsZS5jdoIJZ29vZ2xlLmN6gglnb29nbGUuZGWCCWdvb2dsZS5kaoIJ
+Z29vZ2xlLmRrgglnb29nbGUuZG2CCWdvb2dsZS5keoIJZ29vZ2xlLmVlgglnb29n
+bGUuZXOCCWdvb2dsZS5maYIJZ29vZ2xlLmZtgglnb29nbGUuZnKCCWdvb2dsZS5n
+YYIJZ29vZ2xlLmdlgglnb29nbGUuZ2eCCWdvb2dsZS5nbIIJZ29vZ2xlLmdtggln
+b29nbGUuZ3CCCWdvb2dsZS5ncoIJZ29vZ2xlLmd5gglnb29nbGUuaGuCCWdvb2ds
+ZS5oboIJZ29vZ2xlLmhygglnb29nbGUuaHSCCWdvb2dsZS5odYIJZ29vZ2xlLmll
+gglnb29nbGUuaW2CC2dvb2dsZS5pbmZvgglnb29nbGUuaXGCCWdvb2dsZS5pc4IJ
+Z29vZ2xlLml0ggxnb29nbGUuaXQuYW+CCWdvb2dsZS5qZYIJZ29vZ2xlLmpvggtn
+b29nbGUuam9ic4IJZ29vZ2xlLmpwgglnb29nbGUua2eCCWdvb2dsZS5raYIJZ29v
+Z2xlLmt6gglnb29nbGUubGGCCWdvb2dsZS5saYIJZ29vZ2xlLmxrgglnb29nbGUu
+bHSCCWdvb2dsZS5sdYIJZ29vZ2xlLmx2gglnb29nbGUubWSCCWdvb2dsZS5tZYIJ
+Z29vZ2xlLm1ngglnb29nbGUubWuCCWdvb2dsZS5tbIIJZ29vZ2xlLm1ugglnb29n
+bGUubXOCCWdvb2dsZS5tdYIJZ29vZ2xlLm12gglnb29nbGUubXeCCWdvb2dsZS5u
+ZYIMZ29vZ2xlLm5lLmpwggpnb29nbGUubmV0gglnb29nbGUubmyCCWdvb2dsZS5u
+b4IJZ29vZ2xlLm5ygglnb29nbGUubnWCDWdvb2dsZS5vZmYuYWmCCWdvb2dsZS5w
+a4IJZ29vZ2xlLnBsgglnb29nbGUucG6CCWdvb2dsZS5wc4IJZ29vZ2xlLnB0ggln
+b29nbGUucm+CCWdvb2dsZS5yc4IJZ29vZ2xlLnJ1gglnb29nbGUucneCCWdvb2ds
+ZS5zY4IJZ29vZ2xlLnNlgglnb29nbGUuc2iCCWdvb2dsZS5zaYIJZ29vZ2xlLnNr
+gglnb29nbGUuc22CCWdvb2dsZS5zboIJZ29vZ2xlLnNvgglnb29nbGUuc3SCCWdv
+b2dsZS50ZIIJZ29vZ2xlLnRngglnb29nbGUudGuCCWdvb2dsZS50bIIJZ29vZ2xl
+LnRtgglnb29nbGUudG6CCWdvb2dsZS50b4IJZ29vZ2xlLnRwgglnb29nbGUudHSC
+CWdvb2dsZS51c4IJZ29vZ2xlLnV6gglnb29nbGUudmeCCWdvb2dsZS52dYIJZ29v
+Z2xlLndzMA0GCSqGSIb3DQEBBQUAA4GBAJmZ9RyqpUzrP0UcJnHXoLu/AjIEsIvZ
+Y9hq/9bLry8InfmvERYHr4hNetkOYlW0FeDZtCpWxdPUgJjmWgKAK6j0goOFavTV
+GptkL8gha4p1QUsdLkd36/cvBXeBYSle787veo46N1k4V6Uv2gaDVkre786CNsHv
+Q6MYZ5ClQ+kS
+-----END CERTIFICATE-----
+
diff --git a/test/netlib/data/text_cert_2 b/test/netlib/data/text_cert_2
new file mode 100644
index 00000000..ffe8faae
--- /dev/null
+++ b/test/netlib/data/text_cert_2
@@ -0,0 +1,39 @@
+-----BEGIN CERTIFICATE-----
+MIIGujCCBaKgAwIBAgIDAQlEMA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ
+TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0
+YWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg
+MSBQcmltYXJ5IEludGVybWVkaWF0ZSBTZXJ2ZXIgQ0EwHhcNMTAwMTExMTkyNzM2
+WhcNMTEwMTEyMDkxNDU1WjCBtDEgMB4GA1UEDRMXMTI2ODMyLU1DeExzWTZUbjFn
+bTdvOTAxCzAJBgNVBAYTAk5aMR4wHAYDVQQKExVQZXJzb25hIE5vdCBWYWxpZGF0
+ZWQxKTAnBgNVBAsTIFN0YXJ0Q29tIEZyZWUgQ2VydGlmaWNhdGUgTWVtYmVyMRgw
+FgYDVQQDEw93d3cuaW5vZGUuY28ubnoxHjAcBgkqhkiG9w0BCQEWD2ppbUBpbm9k
+ZS5jby5uejCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL6ghWlGhqg+
+V0P58R3SvLRiO9OrdekDxzmQbKwQcc05frnF5Z9vT6ga7YOuXVeXxhYCAo0nr6KI
++y/Lx+QHvP5W0nKbs+svzUQErq2ZZFwhh1e1LbVccrNwkHUzKOq0TTaVdU4k8kDQ
+zzYF9tTZb+G5Hv1BJjpwYwe8P4cAiPJPrFFOKTySzHqiYsXlx+vR1l1e3zKavhd+
+LVSoLWWXb13yKODq6vnuiHjUJXl8CfVlBhoGotXU4JR5cbuGoW/8+rkwEdX+YoCv
+VCqgdx9IkRFB6uWfN6ocUiFvhA0eknO+ewuVfRLiIaSDB8pNyUWVqu4ngFWtWO1O
+YZg0I/32BkcCAwEAAaOCAvkwggL1MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgOoMBMG
+A1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBQfaL2Rj6r8iRlBTgppgE7ZZ5WT
+UzAfBgNVHSMEGDAWgBTrQjTQmLCrn/Qbawj3zGQu7w4sRTAnBgNVHREEIDAegg93
+d3cuaW5vZGUuY28ubnqCC2lub2RlLmNvLm56MIIBQgYDVR0gBIIBOTCCATUwggEx
+BgsrBgEEAYG1NwECATCCASAwLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRz
+c2wuY29tL3BvbGljeS5wZGYwNAYIKwYBBQUHAgEWKGh0dHA6Ly93d3cuc3RhcnRz
+c2wuY29tL2ludGVybWVkaWF0ZS5wZGYwgbcGCCsGAQUFBwICMIGqMBQWDVN0YXJ0
+Q29tIEx0ZC4wAwIBARqBkUxpbWl0ZWQgTGlhYmlsaXR5LCBzZWUgc2VjdGlvbiAq
+TGVnYWwgTGltaXRhdGlvbnMqIG9mIHRoZSBTdGFydENvbSBDZXJ0aWZpY2F0aW9u
+IEF1dGhvcml0eSBQb2xpY3kgYXZhaWxhYmxlIGF0IGh0dHA6Ly93d3cuc3RhcnRz
+c2wuY29tL3BvbGljeS5wZGYwYQYDVR0fBFowWDAqoCigJoYkaHR0cDovL3d3dy5z
+dGFydHNzbC5jb20vY3J0MS1jcmwuY3JsMCqgKKAmhiRodHRwOi8vY3JsLnN0YXJ0
+c3NsLmNvbS9jcnQxLWNybC5jcmwwgY4GCCsGAQUFBwEBBIGBMH8wOQYIKwYBBQUH
+MAGGLWh0dHA6Ly9vY3NwLnN0YXJ0c3NsLmNvbS9zdWIvY2xhc3MxL3NlcnZlci9j
+YTBCBggrBgEFBQcwAoY2aHR0cDovL3d3dy5zdGFydHNzbC5jb20vY2VydHMvc3Vi
+LmNsYXNzMS5zZXJ2ZXIuY2EuY3J0MCMGA1UdEgQcMBqGGGh0dHA6Ly93d3cuc3Rh
+cnRzc2wuY29tLzANBgkqhkiG9w0BAQUFAAOCAQEAivWID0KT8q1EzWzy+BecsFry
+hQhuLFfAsPkHqpNd9OfkRStGBuJlLX+9DQ9TzjqutdY2buNBuDn71buZK+Y5fmjr
+28rAT6+WMd+KnCl5WLT5IOS6Z9s3cec5TFQbmOGlepSS9Q6Ts9KsXOHHQvDkQeDq
+OV2UqdgXIAyFm5efSL9JXPXntRausNu2s8F2B2rRJe4jPfnUy2LvY8OW1YvjUA++
+vpdWRdfUbJQp55mRfaYMPRnyUm30lAI27QaxgQPFOqDeZUm5llb5eFG/B3f87uhg
++Y1oEykbEvZrIFN4hithioQ0tb+57FKkkG2sW3uemNiQw2qrEo/GAMb1cI50Rg==
+-----END CERTIFICATE-----
+
diff --git a/test/netlib/data/text_cert_weird1 b/test/netlib/data/text_cert_weird1
new file mode 100644
index 00000000..72b09dcb
--- /dev/null
+++ b/test/netlib/data/text_cert_weird1
@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIFNDCCBBygAwIBAgIEDFJFNzANBgkqhkiG9w0BAQUFADCBjDELMAkGA1UEBhMC
+REUxHjAcBgNVBAoTFVVuaXZlcnNpdGFldCBNdWVuc3RlcjE6MDgGA1UEAxMxWmVy
+dGlmaXppZXJ1bmdzc3RlbGxlIFVuaXZlcnNpdGFldCBNdWVuc3RlciAtIEcwMjEh
+MB8GCSqGSIb3DQEJARYSY2FAdW5pLW11ZW5zdGVyLmRlMB4XDTA4MDUyMDEyNDQy
+NFoXDTEzMDUxOTEyNDQyNFowezELMAkGA1UEBhMCREUxHjAcBgNVBAoTFVVuaXZl
+cnNpdGFldCBNdWVuc3RlcjEuMCwGA1UECxMlWmVudHJ1bSBmdWVyIEluZm9ybWF0
+aW9uc3ZlcmFyYmVpdHVuZzEcMBoGA1UEAxMTd3d3LnVuaS1tdWVuc3Rlci5kZTCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMM0WlCj0ew+tyZ1GurBOqFn
+AlChKk4S1F9oDzvp3FwOON4H8YFET7p9ZnoWtkfXSlGNMjekqy67dFlLt1sLusSo
+tjNdaOrDLYmnGEgnYAT0RFBvErzIybJoD/Vu3NXyhes+L94R9mEMCwYXmSvG51H9
+c5CvguXBofMchDLCM/U6AYpwu3sST5orV3S1Rsa9sndj8sKJAcw195PYwl6EiEBb
+M36ltDBlTYEUAg3Z+VSzB09J3U4vSvguVkDCz+szZh5RG3xlN9mlNfzhf4lHrNgV
+0BRbKypa5Uuf81wbMcMMqTxKq+A9ysObpn9J3pNUym+Tn2oqHzGgvwZYB4tzXqUC
+AwEAAaOCAawwggGoMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBMGA1UdJQQMMAoG
+CCsGAQUFBwMBMB0GA1UdDgQWBBQ3RFo8awewUTq5TpOFf3jOCEKihzAfBgNVHSME
+GDAWgBS+nlGiyZJ8u2CL5rBoZHdaUhmhADAjBgNVHREEHDAagRh3d3dhZG1pbkB1
+bmktbXVlbnN0ZXIuZGUwewYDVR0fBHQwcjA3oDWgM4YxaHR0cDovL2NkcDEucGNh
+LmRmbi5kZS93d3UtY2EvcHViL2NybC9nX2NhY3JsLmNybDA3oDWgM4YxaHR0cDov
+L2NkcDIucGNhLmRmbi5kZS93d3UtY2EvcHViL2NybC9nX2NhY3JsLmNybDCBlgYI
+KwYBBQUHAQEEgYkwgYYwQQYIKwYBBQUHMAKGNWh0dHA6Ly9jZHAxLnBjYS5kZm4u
+ZGUvd3d1LWNhL3B1Yi9jYWNlcnQvZ19jYWNlcnQuY3J0MEEGCCsGAQUFBzAChjVo
+dHRwOi8vY2RwMi5wY2EuZGZuLmRlL3d3dS1jYS9wdWIvY2FjZXJ0L2dfY2FjZXJ0
+LmNydDANBgkqhkiG9w0BAQUFAAOCAQEAFfNpagtcKUSDKss7TcqjYn99FQ4FtWjE
+pGmzYL2zX2wsdCGoVQlGkieL9slbQVEUAnBuqM1LPzUNNe9kZpOPV3Rdhq4y8vyS
+xkx3G1v5aGxfPUe8KM8yKIOHRqYefNronHJM0fw7KyjQ73xgbIEgkW+kNXaMLcrb
+EPC36O2Zna8GP9FQxJRLgcfQCcYdRKGVn0EtRSkz2ym5Rbh/hrmJBbbC2yJGGMI0
+Vu5A9piK0EZPekZIUmhMQynD9QcMfWhTEFr7YZfx9ktxKDW4spnu7YrgICfZNcCm
+tfxmnEAFt6a47u9P0w9lpY8+Sx9MNFfTePym+HP4TYha9bIBes+XnA==
+-----END CERTIFICATE-----
+
diff --git a/test/netlib/data/verificationcerts/9da13359.0 b/test/netlib/data/verificationcerts/9da13359.0
new file mode 100644
index 00000000..b22e4d20
--- /dev/null
+++ b/test/netlib/data/verificationcerts/9da13359.0
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDXTCCAkWgAwIBAgIJAPAfPQGCV/Z4MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTUxMTAxMTY0ODAxWhcNMTgwODIxMTY0ODAxWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEArp8LD34JhKCwcQbwIYQMg4+eCgLVN8fwB7+/qOfJbArPs0djFBN+F7c6
+HGvMr24BKUk5u8pn4dPtNurm/vPC8ovNGmcXz62BQJpcMX2veVdRsF7yNwhNacNJ
+Arq+70zNMwYBznx0XUxMF6j6nVFf3AW6SU04ylT4Mp3SY/BUUDAdfl1eRo0mPLNS
+8rpsN+8YBw1Q7SCuBRVqpOgVIsL88svgQUSOlzvMZPBpG/cmB3BNKNrltwb5iFEI
+1jAV7uSj5IcIuNO/246kfsDVPTFMJIzav/CUoidd5UNw+SoFDlzh8sA7L1Bm7D1/
+3KHYSKswGsSR3kynAl10w/SJKDtn8wIDAQABo1AwTjAdBgNVHQ4EFgQUgOcrtxBX
+LxbpnOT65d+vpfyWUkgwHwYDVR0jBBgwFoAUgOcrtxBXLxbpnOT65d+vpfyWUkgw
+DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAEE9bFmUCA+6cvESKPoi2
+TGSpV652d0xd2U66LpEXeiWRJFLz8YGgoJCx3QFGBscJDXxrLxrBBBV/tCpEqypo
+pYIqsawH7M66jpOr83Us3M8JC2eFBZJocMpXxdytWqHik5VKZNx6VQFT8bS7+yVC
+VoUKePhlgcg+pmo41qjqieBNKRMh/1tXS77DI1lgO5wZLVrLXcdqWuDpmaQOKJeq
+G/nxytCW/YJA7bFn/8Gjy8DYypJSeeaKu7o3P3+ONJHdIMHb+MdcheDBS9AOFSeo
+xI0D5EbO9F873O77l7nbD7B0X34HFN0nGczC4poexIpbDFG3hAPekwZ5KC6VwJLc
+1Q==
+-----END CERTIFICATE-----
diff --git a/test/netlib/data/verificationcerts/generate.py b/test/netlib/data/verificationcerts/generate.py
new file mode 100644
index 00000000..9203abbb
--- /dev/null
+++ b/test/netlib/data/verificationcerts/generate.py
@@ -0,0 +1,68 @@
+"""
+Generate SSL test certificates.
+"""
+import subprocess
+import shlex
+import os
+import shutil
+
+
+ROOT_CA = "trusted-root"
+SUBJECT = "/CN=example.mitmproxy.org/"
+
+
+def do(args):
+ print("> %s" % args)
+ args = shlex.split(args)
+ output = subprocess.check_output(args)
+ return output
+
+
+def genrsa(cert):
+ do("openssl genrsa -out {cert}.key 2048".format(cert=cert))
+
+
+def sign(cert):
+ do("openssl x509 -req -in {cert}.csr "
+ "-CA {root_ca}.crt "
+ "-CAkey {root_ca}.key "
+ "-CAcreateserial "
+ "-days 1024 "
+ "-out {cert}.crt".format(root_ca=ROOT_CA, cert=cert)
+ )
+
+
+def mkcert(cert, args):
+ genrsa(cert)
+ do("openssl req -new -nodes -batch "
+ "-key {cert}.key "
+ "{args} "
+ "-out {cert}.csr".format(cert=cert, args=args)
+ )
+ sign(cert)
+ os.remove("{cert}.csr".format(cert=cert))
+
+
+# create trusted root CA
+genrsa("trusted-root")
+do("openssl req -x509 -new -nodes -batch "
+ "-key trusted-root.key "
+ "-days 1024 "
+ "-out trusted-root.crt"
+ )
+h = do("openssl x509 -hash -noout -in trusted-root.crt").decode("ascii").strip()
+shutil.copyfile("trusted-root.crt", "{}.0".format(h))
+
+# create trusted leaf cert.
+mkcert("trusted-leaf", "-subj {}".format(SUBJECT))
+
+# create self-signed cert
+genrsa("self-signed")
+do("openssl req -x509 -new -nodes -batch "
+ "-key self-signed.key "
+ "-subj {} "
+ "-days 1024 "
+ "-out self-signed.crt".format(SUBJECT)
+ )
+
+
diff --git a/test/netlib/data/verificationcerts/self-signed.crt b/test/netlib/data/verificationcerts/self-signed.crt
new file mode 100644
index 00000000..dce2a7e0
--- /dev/null
+++ b/test/netlib/data/verificationcerts/self-signed.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDEzCCAfugAwIBAgIJAJ945xt1FRsfMA0GCSqGSIb3DQEBCwUAMCAxHjAcBgNV
+BAMMFWV4YW1wbGUubWl0bXByb3h5Lm9yZzAeFw0xNTExMDExNjQ4MDJaFw0xODA4
+MjExNjQ4MDJaMCAxHjAcBgNVBAMMFWV4YW1wbGUubWl0bXByb3h5Lm9yZzCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALFxyzPfjgIghOMMnJlW80yB84xC
+nJtko3tuyOdozgTCyha2W+NdIKPNZJtWrzN4P0B5PlozCDwfcSYffLs0WZs8LRWv
+BfZX8+oX+14qQjKFsiqgO65cTLP3qlPySYPJQQ37vOP1Y5Yf8nQq2mwQdC18hLtT
+QOANG6OFoSplpBLsYF+QeoMgqCTa6hrl/5GLmQoDRTjXkv3Sj379AUDMybuBqccm
+q5EIqCrE4+xJ8JywJclAVn2YP14baiFrrYCsYYg4sS1Od6xFj+xtpLe7My3AYjB9
+/aeHd8vDiob0cqOW1TFwhqgJKuErfFyg8lZ2hJmStJKyfofWuY/gl/vnvX0CAwEA
+AaNQME4wHQYDVR0OBBYEFB8d32zK8eqZIoKw4jXzYzhw4amPMB8GA1UdIwQYMBaA
+FB8d32zK8eqZIoKw4jXzYzhw4amPMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEL
+BQADggEBAJmo2oKv1OEjZ0Q4yELO6BAnHAkmBKpW+zmLyQa8idxtLVkI9uXk3iqY
+GWugkmcUZCTVFRWv/QXQQSex+00IY3x2rdHbtuZwcyKiz2u8WEmfW1rOIwBaFJ1i
+v7+SA2aZs6vepN2sE56X54c/YbwQooaKZtOb+djWXYMJrc/Ezj0J7oQIJTptYV8v
+/3216yCHRp/KCL7yTLtiw25xKuXNu/gkcd8wZOY9rS2qMUD897MJF0MvgJoauRBd
+d4XEYCNKkrIRmfqrkiRQfAZpvpoutH6NCk7KuQYcI0BlOHlsnHHcs/w72EEqHwFq
+x6476tW/t8GJDZVD74+pNBcLifXxArE=
+-----END CERTIFICATE-----
diff --git a/test/netlib/data/verificationcerts/self-signed.key b/test/netlib/data/verificationcerts/self-signed.key
new file mode 100644
index 00000000..71a6ad6a
--- /dev/null
+++ b/test/netlib/data/verificationcerts/self-signed.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAsXHLM9+OAiCE4wycmVbzTIHzjEKcm2Sje27I52jOBMLKFrZb
+410go81km1avM3g/QHk+WjMIPB9xJh98uzRZmzwtFa8F9lfz6hf7XipCMoWyKqA7
+rlxMs/eqU/JJg8lBDfu84/Vjlh/ydCrabBB0LXyEu1NA4A0bo4WhKmWkEuxgX5B6
+gyCoJNrqGuX/kYuZCgNFONeS/dKPfv0BQMzJu4GpxyarkQioKsTj7EnwnLAlyUBW
+fZg/XhtqIWutgKxhiDixLU53rEWP7G2kt7szLcBiMH39p4d3y8OKhvRyo5bVMXCG
+qAkq4St8XKDyVnaEmZK0krJ+h9a5j+CX++e9fQIDAQABAoIBAQCT+FvGbych2PJX
+0D2KlXqgE0IAdc/YuYymstSwPLKIP9N8KyfnKtK8Jdw+uYOyfRTp8/EuEJ5OXL3j
+V6CRD++lRwIlseVb7y5EySjh9oVrUhgn+aSrGucPsHkGNeZeEmbAfWugARLBrvRl
+MRMhyHrJL6wT9jIEZInmy9mA3G99IuFW3rS8UR1Yu7zyvhtjvop1xg/wfEUu24Ty
+PvMfnwaDcZHCz2tmu2KJvaxSBAG3FKmAqeMvk1Gt5m2keKgw03M+EX0LrM8ybWqn
+VwB8tnSyMBLVFLIXMpIiSfpji10+p9fdKFMRF++D6qVwyoxPiIq+yEJapxXiqLea
+mkhtJW91AoGBAOvIb7bZvH4wYvi6txs2pygF3ZMjqg/fycnplrmYMrjeeDeeN4v1
+h/5tkN9TeTkHRaN3L7v49NEUDhDyuopLTNfWpYdv63U/BVzvgMm/guacTYkx9whB
+OvQ2YekR/WKg7kuyrTZidTDz+mjU+1b8JaWGjiDc6vFwxZA7uWicaGGHAoGBAMCo
+y/2AwFGwCR+5bET1nTTyxok6iKo4k6R/7DJe4Bq8VLifoyX3zDlGG/33KN3xVqBU
+xnT9gkii1lfX2U+4iM+GOSPl0nG0hOEqEH+vFHszpHybDeNez3FEyIbgOzg6u7sV
+NOy+P94L5EMQVEmWp5g6Vm3k9kr92Bd9UacKQPnbAoGAMN8KyMu41i8RVJze9zUM
+0K7mjmkGBuRL3x4br7xsRwVVxbF1sfzig0oSjTewGLH5LTi3HC8uD2gowjqNj7yr
+4NEM3lXEaDj305uRBkA70bD0IUvJ+FwM7DGZecXQz3Cr8+TFIlCmGc94R+Jddlot
+M3IAY69mw0SsroiylYxV1mECgYAcSGtx8rXJCDO+sYTgdsI2ZLGasbogax/ZlWIC
+XwU9R4qUc/MKft8/RTiUxvT76BMUhH2B7Tl0GlunF6vyVR/Yf1biGzoSsTKUr40u
+gXBbSdCK7mRSjbecZEGf80keTxkCNPHJE4DiwxImej41c2V1JpNLnMI/bhaMFDyp
+bgrt4wKBgHFzZgAgM1v07F038tAkIBGrYLukY1ZFBaZoGZ9xHfy/EmLJM3HCHLO5
+8wszMGhMTe2+39EeChwgj0kFaq1YnDiucU74BC57KR1tD59y7l6UnsQXTm4/32j8
+Or6i8GekBibCb97DzzOU0ZK//fNhHTXpDDXsYt5lJUWSmgW+S9Qp
+-----END RSA PRIVATE KEY-----
diff --git a/test/netlib/data/verificationcerts/trusted-leaf.crt b/test/netlib/data/verificationcerts/trusted-leaf.crt
new file mode 100644
index 00000000..6a92de92
--- /dev/null
+++ b/test/netlib/data/verificationcerts/trusted-leaf.crt
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC4TCCAckCCQCj6D9oVylb8jANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
+VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
+cyBQdHkgTHRkMB4XDTE1MTEwMTE2NDgwMloXDTE4MDgyMTE2NDgwMlowIDEeMBwG
+A1UEAwwVZXhhbXBsZS5taXRtcHJveHkub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAy/L5JYHS7QFhSIsjmd6bJTgs2rdqEn6tsmPBVZKZ7SqCAVjW
+hPpEu7Q23akmU6Zm9Fp/vENc3jzxQLlEKhrv7eWmFYSOrCYtbJOz3RQorlwjjfdY
+LlNQh1wYUXQX3PN3r3dyYtt5vTtXKc8+aP4M4vX7qlbW+4j4LrQfmPjS0XOdYpu3
+wh+i1ZMIhZye3hpCjwnpjTf7/ff45ZFxtkoi1uzEC/+swr1RSvamY8Foe12Re17Z
+5ij8ZB0NIdoSk1tDkY3sJ8iNi35+qartl0UYeG9IUXRwDRrPsEKpF4RxY1+X2bdZ
+r6PKb/E4CA5JlMvS5SVmrvxjCVqTQBmTjXfxqwIDAQABMA0GCSqGSIb3DQEBCwUA
+A4IBAQBmpSZJrTDvzSlo6P7P7x1LoETzHyVjwgPeqGYw6ndGXeJMN9rhhsFvRsiB
+I/aHh58MIlSjti7paikDAoFHB3dBvFHR+JUa/ailWEbcZReWRSE3lV6wFiN3G3lU
+OyofR7MKnPW7bv8hSqOLqP1mbupXuQFB5M6vPLRwg5VgiCHI/XBiTvzMamzvNAR3
+UHHZtsJkRqzogYm6K9YJaga7jteSx2nNo+ujLwrxeXsLChTyFMJGnVkp5IyKeNfc
+qwlzNncb3y+4KnUdNkPEtuydgAxAfuyXufiFBYRcUWbQ5/9ycgF7131ySaj9f/Y2
+kMsv2jg+soKvwwVYCABsk1KSHtfz
+-----END CERTIFICATE-----
diff --git a/test/netlib/data/verificationcerts/trusted-leaf.key b/test/netlib/data/verificationcerts/trusted-leaf.key
new file mode 100644
index 00000000..783ebf1c
--- /dev/null
+++ b/test/netlib/data/verificationcerts/trusted-leaf.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAy/L5JYHS7QFhSIsjmd6bJTgs2rdqEn6tsmPBVZKZ7SqCAVjW
+hPpEu7Q23akmU6Zm9Fp/vENc3jzxQLlEKhrv7eWmFYSOrCYtbJOz3RQorlwjjfdY
+LlNQh1wYUXQX3PN3r3dyYtt5vTtXKc8+aP4M4vX7qlbW+4j4LrQfmPjS0XOdYpu3
+wh+i1ZMIhZye3hpCjwnpjTf7/ff45ZFxtkoi1uzEC/+swr1RSvamY8Foe12Re17Z
+5ij8ZB0NIdoSk1tDkY3sJ8iNi35+qartl0UYeG9IUXRwDRrPsEKpF4RxY1+X2bdZ
+r6PKb/E4CA5JlMvS5SVmrvxjCVqTQBmTjXfxqwIDAQABAoIBAQC956DWq+wbhA1x
+3x1nSUBth8E8Z0z9q7dRRFHhvIBXth0X5ADcEa2umj/8ZmSpv2heX2ZRhugSh+yc
+t+YgzrRacFwV7ThsU6A4WdBBK2Q19tWke4xAlpOFdtut/Mu7kXkAidiY9ISHD5o5
+9B/I48ZcD3AnTHUiAogV9OL3LbogDD4HasLt4mWkbq8U2thdjxMIvxdg36olJEuo
+iAZrAUCPZEXuU89BtvPLUYioe9n90nzkyneGNS0SHxotlEc9ZYK9VTsivtXJb4wB
+ptDMCp+TH3tjo8BTGnbnoZEybgyyOEd0UTzxK4DlxnvRVWexFY6NXwPFhIxKlB0Y
+Bg8NkAkBAoGBAOiRnmbC5QkqrKrTkLx3fghIHPqgEXPPYgHLSuY3UjTlMb3APXpq
+vzQnlCn3QuSse/1fWnQj+9vLVbx1XNgKjzk7dQhn5IUY+mGN4lLmoSnTebxvSQ43
+VAgTYjST9JFmJ3wK4KkWDsEsVao8LAx0h5JEQXUTT5xZpFA2MLztYbgfAoGBAOB/
+MvhLMAwlx8+m/zXMEPLk/KOd2dVZ4q5se8bAT/GiGsi8JUcPnCk140ZZabJqryAp
+JFzUHIjfVsS9ejAfocDk1JeIm7Uus4um6fQEKIPMBxI/M/UAwYCXAG9ULXqilbO3
+pTdeeuraVKrTu1Z4ea6x4du1JWKcyDfYfsHepcT1AoGBAM2fskV5G7e3G2MOG3IG
+1E/OMpEE5WlXenfLnjVdxDkwS4JRbgnGR7d9JurTyzkTp6ylmfwFtLDoXq15ttTs
+wSUBBMCh2tIy+201XV2eu++XIpMQca84C/v352RFTH8hqtdpZqkY74KsCDGzcd6x
+SQxxfM5efIzoVPb2crEX0MZRAoGAQ2EqFSfL9flo7UQ8GRN0itJ7mUgJV2WxCZT5
+2X9i/y0eSN1feuKOhjfsTPMNLEWk5kwy48GuBs6xpj8Qa10zGUgVHp4bzdeEgAfK
+9DhDSLt1694YZBKkAUpRERj8xXAC6nvWFLZAwjhhbRw7gAqMywgMt/q4i85usYRD
+F0ESE/kCgYBbc083PcLmlHbkn/d1i4IcLI6wFk+tZYIEVYDid7xDOgZOBcOTTyYB
+BrDzNqbKNexKRt7QHVlwR+VOGMdN5P0hf7oH3SMW23OxBKoQe8pUSGF9a4DjCS1v
+vCXMekifb9kIhhUWaG71L8+MaOzNBVAmk1+3NzPZgV/YxHjAWWhGHQ==
+-----END RSA PRIVATE KEY-----
diff --git a/test/netlib/data/verificationcerts/trusted-root.crt b/test/netlib/data/verificationcerts/trusted-root.crt
new file mode 100644
index 00000000..b22e4d20
--- /dev/null
+++ b/test/netlib/data/verificationcerts/trusted-root.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDXTCCAkWgAwIBAgIJAPAfPQGCV/Z4MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMTUxMTAxMTY0ODAxWhcNMTgwODIxMTY0ODAxWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEArp8LD34JhKCwcQbwIYQMg4+eCgLVN8fwB7+/qOfJbArPs0djFBN+F7c6
+HGvMr24BKUk5u8pn4dPtNurm/vPC8ovNGmcXz62BQJpcMX2veVdRsF7yNwhNacNJ
+Arq+70zNMwYBznx0XUxMF6j6nVFf3AW6SU04ylT4Mp3SY/BUUDAdfl1eRo0mPLNS
+8rpsN+8YBw1Q7SCuBRVqpOgVIsL88svgQUSOlzvMZPBpG/cmB3BNKNrltwb5iFEI
+1jAV7uSj5IcIuNO/246kfsDVPTFMJIzav/CUoidd5UNw+SoFDlzh8sA7L1Bm7D1/
+3KHYSKswGsSR3kynAl10w/SJKDtn8wIDAQABo1AwTjAdBgNVHQ4EFgQUgOcrtxBX
+LxbpnOT65d+vpfyWUkgwHwYDVR0jBBgwFoAUgOcrtxBXLxbpnOT65d+vpfyWUkgw
+DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAEE9bFmUCA+6cvESKPoi2
+TGSpV652d0xd2U66LpEXeiWRJFLz8YGgoJCx3QFGBscJDXxrLxrBBBV/tCpEqypo
+pYIqsawH7M66jpOr83Us3M8JC2eFBZJocMpXxdytWqHik5VKZNx6VQFT8bS7+yVC
+VoUKePhlgcg+pmo41qjqieBNKRMh/1tXS77DI1lgO5wZLVrLXcdqWuDpmaQOKJeq
+G/nxytCW/YJA7bFn/8Gjy8DYypJSeeaKu7o3P3+ONJHdIMHb+MdcheDBS9AOFSeo
+xI0D5EbO9F873O77l7nbD7B0X34HFN0nGczC4poexIpbDFG3hAPekwZ5KC6VwJLc
+1Q==
+-----END CERTIFICATE-----
diff --git a/test/netlib/data/verificationcerts/trusted-root.key b/test/netlib/data/verificationcerts/trusted-root.key
new file mode 100644
index 00000000..05483f77
--- /dev/null
+++ b/test/netlib/data/verificationcerts/trusted-root.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEArp8LD34JhKCwcQbwIYQMg4+eCgLVN8fwB7+/qOfJbArPs0dj
+FBN+F7c6HGvMr24BKUk5u8pn4dPtNurm/vPC8ovNGmcXz62BQJpcMX2veVdRsF7y
+NwhNacNJArq+70zNMwYBznx0XUxMF6j6nVFf3AW6SU04ylT4Mp3SY/BUUDAdfl1e
+Ro0mPLNS8rpsN+8YBw1Q7SCuBRVqpOgVIsL88svgQUSOlzvMZPBpG/cmB3BNKNrl
+twb5iFEI1jAV7uSj5IcIuNO/246kfsDVPTFMJIzav/CUoidd5UNw+SoFDlzh8sA7
+L1Bm7D1/3KHYSKswGsSR3kynAl10w/SJKDtn8wIDAQABAoIBAFgMzjDzpqz/sbhs
+fS0JPp4gDtqRbx3/bSMbJvNuXPxjvzNxLZ5z7cLbmyu1l7Jlz6QXzkrI1vTiPdzR
+OcUY+RYANF252iHYJTKEIzS5YX/X7dL3LT9eqlpIJEqCC8Dygw3VW5fY3Xwl+sB7
+blNhMuro4HQRwi8UBUrQlcPa7Ui5BBi323Q6en+VjYctkqpJHzNKPSqPTbsdLaK+
+B0XuXxFatM09rmeRKZCL71Lk1T8N/l0hqEzej7zxgVD7vG/x1kMFN4T3yCmXCbPa
+izGHYr1EBHglm4qMNWveXCZiVJ+wmwCjdjqvggyHiZFXE2N0OCrWPhxQPdqFf5y7
+bUO9U2ECgYEA6GM1UzRnbVpjb20ezFy7dU7rlWM0nHBfG27M3bcXh4HnPpnvKp0/
+8a1WFi4kkRywrNXx8hFEd43vTbdObLpVXScXRKiY3MHmFk4k4hbWuTpmumCubQZO
+AWlX6TE0HRKn1wQahgpQcxcWaDN2xJJmRQ1zVmlnNkT48/4kFgRxyykCgYEAwF08
+ngrF35oYoU/x+KKq2NXGeNUzoZMj568dE1oWW0ZFpqCi+DGT+hAbG3yUOBSaPqy9
+zn1obGo0YRlrayvtebz118kG7a/rzY02VcAPlT/GpEhvkZlXTwEK17zRJc1nJrfP
+39QAZWZsaOru9NRIg/8HcdG3JPR2MhRD/De9GbsCgYAaiZnBUq6s8jGAu/lUZRKT
+JtwIRzfu1XZG77Q9bXcmZlM99t41A5gVxTGbftF2MMyMMDJc7lPfQzocqd4u1GiD
+Jr+le4tZSls4GNxlZS5IIL8ycW/5y0qFJr5/RrsoxsSb7UAKJothWTWZ2Karc/xx
+zkNpjsfWjrHPSypbyU4lYQKBgFh1R5/BgnatjO/5LGNSok/uFkOQfxqo6BTtYOh6
+P9efO/5A1lBdtBeE+oIsSphzWO7DTtE6uB9Kw2V3Y/83hw+5RjABoG8Cu+OdMURD
+eqb+WeFH8g45Pn31E8Bbcq34g5u5YR0jhz8Z13ZzuojZabNRPmIntxmGVSf4S78a
+/plrAoGBANMHNng2lyr03nqnHrOM6NXD+60af0YR/YJ+2d/H40RnXxGJ4DXn7F00
+a4vJFPa97uq+xpd0HE+TE+NIrOdVDXPePD2qzBzMTsctGtj30vLzojMOT+Yf/nvO
+WxTL5Q8GruJz2Dn0awSZO2z/3A8S1rmpuVZ/jT5NtRrvOSY6hmxF
+-----END RSA PRIVATE KEY-----
diff --git a/test/netlib/data/verificationcerts/trusted-root.srl b/test/netlib/data/verificationcerts/trusted-root.srl
new file mode 100644
index 00000000..4ad962ba
--- /dev/null
+++ b/test/netlib/data/verificationcerts/trusted-root.srl
@@ -0,0 +1 @@
+A3E83F6857295BF2
diff --git a/test/netlib/http/__init__.py b/test/netlib/http/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/netlib/http/__init__.py
diff --git a/test/netlib/http/http1/__init__.py b/test/netlib/http/http1/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/netlib/http/http1/__init__.py
diff --git a/test/netlib/http/http1/test_assemble.py b/test/netlib/http/http1/test_assemble.py
new file mode 100644
index 00000000..31a62438
--- /dev/null
+++ b/test/netlib/http/http1/test_assemble.py
@@ -0,0 +1,102 @@
+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,
+ assemble_body)
+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(content=CONTENT_MISSING))
+
+
+def test_assemble_request_head():
+ c = assemble_request_head(treq(content="foo"))
+ assert b"GET" in c
+ assert b"qvalue" in c
+ assert b"content-length" in c
+ assert b"foo" 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(content=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_body():
+ c = list(assemble_body(Headers(), [b"body"]))
+ assert c == [b"body"]
+
+ c = list(assemble_body(Headers(transfer_encoding="chunked"), [b"123456789a", b""]))
+ assert c == [b"a\r\n123456789a\r\n", b"0\r\n\r\n"]
+
+ c = list(assemble_body(Headers(transfer_encoding="chunked"), [b"123456789a"]))
+ assert c == [b"a\r\n123456789a\r\n", b"0\r\n\r\n"]
+
+
+def test_assemble_request_line():
+ assert _assemble_request_line(treq().data) == b"GET /path HTTP/1.1"
+
+ authority_request = treq(method=b"CONNECT", first_line_format="authority").data
+ assert _assemble_request_line(authority_request) == b"CONNECT address:22 HTTP/1.1"
+
+ absolute_request = treq(first_line_format="absolute").data
+ assert _assemble_request_line(absolute_request) == b"GET http://address:22/path HTTP/1.1"
+
+ with raises(RuntimeError):
+ _assemble_request_line(treq(first_line_format="invalid_form").data)
+
+
+def test_assemble_request_headers():
+ # https://github.com/mitmproxy/mitmproxy/issues/186
+ r = treq(content=b"")
+ r.headers["Transfer-Encoding"] = "chunked"
+ c = _assemble_request_headers(r.data)
+ assert b"Transfer-Encoding" in c
+
+
+def test_assemble_request_headers_host_header():
+ r = treq()
+ r.headers = Headers()
+ c = _assemble_request_headers(r.data)
+ assert b"host" in c
+
+ r.host = None
+ c = _assemble_request_headers(r.data)
+ assert b"host" not in c
+
+
+def test_assemble_response_headers():
+ # https://github.com/mitmproxy/mitmproxy/issues/186
+ r = tresp(content=b"")
+ r.headers["Transfer-Encoding"] = "chunked"
+ c = _assemble_response_headers(r)
+ assert b"Transfer-Encoding" in c
diff --git a/test/netlib/http/http1/test_read.py b/test/netlib/http/http1/test_read.py
new file mode 100644
index 00000000..90234070
--- /dev/null
+++ b/test/netlib/http/http1/test_read.py
@@ -0,0 +1,333 @@
+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, TcpDisconnect
+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 == "GET"
+ assert r.content == 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 == "GET"
+ assert r.headers["Content-Length"] == "4"
+ assert r.content 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.content == 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"] == "4"
+ assert r.content 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_max_chunk_size(self):
+ rfile = BytesIO(b"123456")
+ assert list(read_body(rfile, -1, max_chunk_size=None)) == [b"123456"]
+ rfile = BytesIO(b"123456")
+ assert list(read_body(rfile, -1, max_chunk_size=1)) == [b"1", b"2", b"3", b"4", b"5", b"6"]
+
+def test_connection_close():
+ headers = Headers()
+ assert connection_close(b"HTTP/1.0", headers)
+ assert not connection_close(b"HTTP/1.1", headers)
+
+ headers["connection"] = "keep-alive"
+ assert not connection_close(b"HTTP/1.1", headers)
+
+ headers["connection"] = "close"
+ assert connection_close(b"HTTP/1.1", headers)
+
+ headers["connection"] = "foobar"
+ assert connection_close(b"HTTP/1.0", headers)
+ assert not connection_close(b"HTTP/1.1", headers)
+
+def test_expected_http_body_size():
+ # Expect: 100-continue
+ assert expected_http_body_size(
+ treq(headers=Headers(expect="100-continue", content_length="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="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="chunked")),
+ ) is None
+
+ # explicit length
+ for val in (b"foo", b"-7"):
+ with raises(HttpSyntaxException):
+ expected_http_body_size(
+ treq(headers=Headers(content_length=val))
+ )
+ assert expected_http_body_size(
+ treq(headers=Headers(content_length="42"))
+ ) == 42
+
+ # no length
+ assert expected_http_body_size(
+ treq(headers=Headers())
+ ) == 0
+ assert expected_http_body_size(
+ treq(headers=Headers()), tresp(headers=Headers())
+ ) == -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(HttpReadDisconnect):
+ rfile = Mock()
+ rfile.readline.side_effect = TcpDisconnect
+ _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")
+ with raises(HttpReadDisconnect):
+ t(b"")
+
+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"")
+
+ # https://github.com/mitmproxy/mitmproxy/issues/784
+ assert t(b"HTTP/1.1 200 Non-Autoris\xc3\xa9") == (b"HTTP/1.1", 200, b"Non-Autoris\xc3\xa9")
+
+ 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")
+ with raises(HttpReadDisconnect):
+ t(b"")
+
+
+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_empty_name(self):
+ data = b":foo"
+ with raises(HttpSyntaxException):
+ self._read(data)
+
+ def test_read_empty_value(self):
+ data = b"bar:"
+ headers = self._read(data)
+ assert headers.fields == [[b"bar", b""]]
+
+def test_read_chunked():
+ req = treq(content=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))
diff --git a/test/netlib/http/http2/__init__.py b/test/netlib/http/http2/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/netlib/http/http2/__init__.py
diff --git a/test/netlib/http/http2/test_connections.py b/test/netlib/http/http2/test_connections.py
new file mode 100644
index 00000000..8be127e4
--- /dev/null
+++ b/test/netlib/http/http2/test_connections.py
@@ -0,0 +1,540 @@
+import OpenSSL
+import mock
+import codecs
+
+from hyperframe.frame import *
+
+from netlib import tcp, http, utils, tservers
+from netlib.tutils import raises
+from netlib.exceptions import TcpDisconnect
+from netlib.http.http2.connections import HTTP2Protocol, TCPHandler
+
+
+class TestTCPHandlerWrapper:
+ def test_wrapped(self):
+ 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, TCPHandler)
+ assert p.tcp_handler.rfile == 'foo'
+ assert p.tcp_handler.wfile == 'bar'
+
+
+class EchoHandler(tcp.BaseHandler):
+ sni = None
+
+ def handle(self):
+ while True:
+ v = self.rfile.safe_read(1)
+ self.wfile.write(v)
+ self.wfile.flush()
+
+
+class TestProtocol:
+ @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
+
+ protocol.perform_connection_preface()
+ assert not mock_client_method.called
+ assert not mock_server_method.called
+
+ protocol.perform_connection_preface(force=True)
+ assert mock_client_method.called
+ assert not mock_server_method.called
+
+ @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
+
+ protocol.perform_connection_preface()
+ assert not mock_client_method.called
+ assert not mock_server_method.called
+
+ protocol.perform_connection_preface(force=True)
+ assert not mock_client_method.called
+ assert mock_server_method.called
+
+
+class TestCheckALPNMatch(tservers.ServerTestBase):
+ handler = EchoHandler
+ ssl = dict(
+ alpn_select=b'h2',
+ )
+
+ if tcp.HAS_ALPN:
+
+ def test_check_alpn(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl(alpn_protos=[b'h2'])
+ protocol = HTTP2Protocol(c)
+ assert protocol.check_alpn()
+
+
+class TestCheckALPNMismatch(tservers.ServerTestBase):
+ handler = EchoHandler
+ ssl = dict(
+ alpn_select=None,
+ )
+
+ if tcp.HAS_ALPN:
+
+ def test_check_alpn(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl(alpn_protos=[b'h2'])
+ protocol = HTTP2Protocol(c)
+ with raises(NotImplementedError):
+ protocol.check_alpn()
+
+
+class TestPerformServerConnectionPreface(tservers.ServerTestBase):
+ class handler(tcp.BaseHandler):
+
+ def handle(self):
+ # send magic
+ self.wfile.write(codecs.decode('505249202a20485454502f322e300d0a0d0a534d0d0a0d0a', 'hex_codec'))
+ self.wfile.flush()
+
+ # send empty settings frame
+ self.wfile.write(codecs.decode('000000040000000000', 'hex_codec'))
+ self.wfile.flush()
+
+ # check empty settings frame
+ raw = utils.http2_read_raw_frame(self.rfile)
+ assert raw == codecs.decode('00000c040000000000000200000000000300000001', 'hex_codec')
+
+ # check settings acknowledgement
+ raw = utils.http2_read_raw_frame(self.rfile)
+ assert raw == codecs.decode('000000040100000000', 'hex_codec')
+
+ # send settings acknowledgement
+ self.wfile.write(codecs.decode('000000040100000000', 'hex_codec'))
+ self.wfile.flush()
+
+ def test_perform_server_connection_preface(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ protocol = HTTP2Protocol(c)
+
+ assert not protocol.connection_preface_performed
+ protocol.perform_server_connection_preface()
+ assert protocol.connection_preface_performed
+
+ with raises(TcpDisconnect):
+ protocol.perform_server_connection_preface(force=True)
+
+
+class TestPerformClientConnectionPreface(tservers.ServerTestBase):
+ class handler(tcp.BaseHandler):
+
+ def handle(self):
+ # check magic
+ assert self.rfile.read(24) == HTTP2Protocol.CLIENT_CONNECTION_PREFACE
+
+ # check empty settings frame
+ assert self.rfile.read(9) ==\
+ codecs.decode('000000040000000000', 'hex_codec')
+
+ # send empty settings frame
+ self.wfile.write(codecs.decode('000000040000000000', 'hex_codec'))
+ self.wfile.flush()
+
+ # check settings acknowledgement
+ assert self.rfile.read(9) == \
+ codecs.decode('000000040100000000', 'hex_codec')
+
+ # send settings acknowledgement
+ self.wfile.write(codecs.decode('000000040100000000', 'hex_codec'))
+ self.wfile.flush()
+
+ def test_perform_client_connection_preface(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ protocol = HTTP2Protocol(c)
+
+ assert not protocol.connection_preface_performed
+ protocol.perform_client_connection_preface()
+ assert protocol.connection_preface_performed
+
+
+class TestClientStreamIds(object):
+ c = tcp.TCPClient(("127.0.0.1", 0))
+ protocol = HTTP2Protocol(c)
+
+ def test_client_stream_ids(self):
+ assert self.protocol.current_stream_id is None
+ assert self.protocol._next_stream_id() == 1
+ assert self.protocol.current_stream_id == 1
+ assert self.protocol._next_stream_id() == 3
+ assert self.protocol.current_stream_id == 3
+ assert self.protocol._next_stream_id() == 5
+ assert self.protocol.current_stream_id == 5
+
+
+class TestServerStreamIds(object):
+ c = tcp.TCPClient(("127.0.0.1", 0))
+ protocol = HTTP2Protocol(c, is_server=True)
+
+ def test_server_stream_ids(self):
+ assert self.protocol.current_stream_id is None
+ assert self.protocol._next_stream_id() == 2
+ assert self.protocol.current_stream_id == 2
+ assert self.protocol._next_stream_id() == 4
+ assert self.protocol.current_stream_id == 4
+ assert self.protocol._next_stream_id() == 6
+ assert self.protocol.current_stream_id == 6
+
+
+class TestApplySettings(tservers.ServerTestBase):
+ class handler(tcp.BaseHandler):
+ def handle(self):
+ # check settings acknowledgement
+ assert self.rfile.read(9) == codecs.decode('000000040100000000', 'hex_codec')
+ self.wfile.write("OK")
+ self.wfile.flush()
+ self.rfile.safe_read(9) # just to keep the connection alive a bit longer
+
+ ssl = True
+
+ def test_apply_settings(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ protocol = HTTP2Protocol(c)
+
+ protocol._apply_settings({
+ SettingsFrame.ENABLE_PUSH: 'foo',
+ SettingsFrame.MAX_CONCURRENT_STREAMS: 'bar',
+ SettingsFrame.INITIAL_WINDOW_SIZE: 'deadbeef',
+ })
+
+ assert c.rfile.safe_read(2) == b"OK"
+
+ assert protocol.http2_settings[
+ SettingsFrame.ENABLE_PUSH] == 'foo'
+ assert protocol.http2_settings[
+ SettingsFrame.MAX_CONCURRENT_STREAMS] == 'bar'
+ assert protocol.http2_settings[
+ SettingsFrame.INITIAL_WINDOW_SIZE] == 'deadbeef'
+
+
+class TestCreateHeaders(object):
+ c = tcp.TCPClient(("127.0.0.1", 0))
+
+ def test_create_headers(self):
+ headers = http.Headers([
+ (b':method', b'GET'),
+ (b':path', b'index.html'),
+ (b':scheme', b'https'),
+ (b'foo', b'bar')])
+
+ bytes = HTTP2Protocol(self.c)._create_headers(
+ headers, 1, end_stream=True)
+ assert b''.join(bytes) ==\
+ codecs.decode('000014010500000001824488355217caf3a69a3f87408294e7838c767f', 'hex_codec')
+
+ bytes = HTTP2Protocol(self.c)._create_headers(
+ headers, 1, end_stream=False)
+ assert b''.join(bytes) ==\
+ codecs.decode('000014010400000001824488355217caf3a69a3f87408294e7838c767f', 'hex_codec')
+
+ def test_create_headers_multiple_frames(self):
+ headers = http.Headers([
+ (b':method', b'GET'),
+ (b':path', b'/'),
+ (b':scheme', b'https'),
+ (b'foo', b'bar'),
+ (b'server', b'version')])
+
+ protocol = HTTP2Protocol(self.c)
+ protocol.http2_settings[SettingsFrame.MAX_FRAME_SIZE] = 8
+ bytes = protocol._create_headers(headers, 1, end_stream=True)
+ assert len(bytes) == 3
+ assert bytes[0] == codecs.decode('000008010100000001828487408294e783', 'hex_codec')
+ assert bytes[1] == codecs.decode('0000080900000000018c767f7685ee5b10', 'hex_codec')
+ assert bytes[2] == codecs.decode('00000209040000000163d5', 'hex_codec')
+
+
+class TestCreateBody(object):
+ c = tcp.TCPClient(("127.0.0.1", 0))
+
+ def test_create_body_empty(self):
+ protocol = HTTP2Protocol(self.c)
+ bytes = protocol._create_body(b'', 1)
+ assert b''.join(bytes) == b''
+
+ def test_create_body_single_frame(self):
+ protocol = HTTP2Protocol(self.c)
+ bytes = protocol._create_body(b'foobar', 1)
+ assert b''.join(bytes) == codecs.decode('000006000100000001666f6f626172', 'hex_codec')
+
+ def test_create_body_multiple_frames(self):
+ protocol = HTTP2Protocol(self.c)
+ protocol.http2_settings[SettingsFrame.MAX_FRAME_SIZE] = 5
+ bytes = protocol._create_body(b'foobarmehm42', 1)
+ assert len(bytes) == 3
+ assert bytes[0] == codecs.decode('000005000000000001666f6f6261', 'hex_codec')
+ assert bytes[1] == codecs.decode('000005000000000001726d65686d', 'hex_codec')
+ assert bytes[2] == codecs.decode('0000020001000000013432', 'hex_codec')
+
+
+class TestReadRequest(tservers.ServerTestBase):
+ class handler(tcp.BaseHandler):
+
+ def handle(self):
+ self.wfile.write(
+ codecs.decode('000003010400000001828487', 'hex_codec'))
+ self.wfile.write(
+ codecs.decode('000006000100000001666f6f626172', 'hex_codec'))
+ self.wfile.flush()
+ self.rfile.safe_read(9) # just to keep the connection alive a bit longer
+
+ ssl = True
+
+ def test_read_request(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ protocol = HTTP2Protocol(c, is_server=True)
+ protocol.connection_preface_performed = True
+
+ req = protocol.read_request(NotImplemented)
+
+ assert req.stream_id
+ assert req.headers.fields == [[b':method', b'GET'], [b':path', b'/'], [b':scheme', b'https']]
+ assert req.content == b'foobar'
+
+
+class TestReadRequestRelative(tservers.ServerTestBase):
+ class handler(tcp.BaseHandler):
+ def handle(self):
+ self.wfile.write(
+ codecs.decode('00000c0105000000014287d5af7e4d5a777f4481f9', 'hex_codec'))
+ self.wfile.flush()
+
+ ssl = True
+
+ def test_asterisk_form_in(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ protocol = HTTP2Protocol(c, is_server=True)
+ protocol.connection_preface_performed = True
+
+ req = protocol.read_request(NotImplemented)
+
+ assert req.form_in == "relative"
+ assert req.method == "OPTIONS"
+ assert req.path == "*"
+
+
+class TestReadRequestAbsolute(tservers.ServerTestBase):
+ class handler(tcp.BaseHandler):
+ def handle(self):
+ self.wfile.write(
+ codecs.decode('00001901050000000182448d9d29aee30c0e492c2a1170426366871c92585422e085', 'hex_codec'))
+ self.wfile.flush()
+
+ ssl = True
+
+ def test_absolute_form_in(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ protocol = HTTP2Protocol(c, is_server=True)
+ protocol.connection_preface_performed = True
+
+ req = protocol.read_request(NotImplemented)
+
+ assert req.form_in == "absolute"
+ assert req.scheme == "http"
+ assert req.host == "address"
+ assert req.port == 22
+
+
+class TestReadRequestConnect(tservers.ServerTestBase):
+ class handler(tcp.BaseHandler):
+ def handle(self):
+ self.wfile.write(
+ codecs.decode('00001b0105000000014287bdab4e9c17b7ff44871c92585422e08541871c92585422e085', 'hex_codec'))
+ self.wfile.write(
+ codecs.decode('00001d0105000000014287bdab4e9c17b7ff44882f91d35d055c87a741882f91d35d055c87a7', 'hex_codec'))
+ self.wfile.flush()
+
+ ssl = True
+
+ def test_connect(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ protocol = HTTP2Protocol(c, is_server=True)
+ protocol.connection_preface_performed = True
+
+ req = protocol.read_request(NotImplemented)
+ assert req.form_in == "authority"
+ assert req.method == "CONNECT"
+ assert req.host == "address"
+ assert req.port == 22
+
+ req = protocol.read_request(NotImplemented)
+ assert req.form_in == "authority"
+ assert req.method == "CONNECT"
+ assert req.host == "example.com"
+ assert req.port == 443
+
+
+class TestReadResponse(tservers.ServerTestBase):
+ class handler(tcp.BaseHandler):
+ def handle(self):
+ self.wfile.write(
+ codecs.decode('00000801040000002a88628594e78c767f', 'hex_codec'))
+ self.wfile.write(
+ codecs.decode('00000600010000002a666f6f626172', 'hex_codec'))
+ self.wfile.flush()
+ self.rfile.safe_read(9) # just to keep the connection alive a bit longer
+
+ ssl = True
+
+ def test_read_response(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ protocol = HTTP2Protocol(c)
+ protocol.connection_preface_performed = True
+
+ resp = protocol.read_response(NotImplemented, stream_id=42)
+
+ assert resp.http_version == "HTTP/2.0"
+ assert resp.status_code == 200
+ assert resp.msg == ''
+ assert resp.headers.fields == [[b':status', b'200'], [b'etag', b'foobar']]
+ assert resp.content == b'foobar'
+ assert resp.timestamp_end
+
+
+class TestReadEmptyResponse(tservers.ServerTestBase):
+ class handler(tcp.BaseHandler):
+ def handle(self):
+ self.wfile.write(
+ codecs.decode('00000801050000002a88628594e78c767f', 'hex_codec'))
+ self.wfile.flush()
+
+ ssl = True
+
+ def test_read_empty_response(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ protocol = HTTP2Protocol(c)
+ protocol.connection_preface_performed = True
+
+ resp = protocol.read_response(NotImplemented, stream_id=42)
+
+ assert resp.stream_id == 42
+ assert resp.http_version == "HTTP/2.0"
+ assert resp.status_code == 200
+ assert resp.msg == ''
+ assert resp.headers.fields == [[b':status', b'200'], [b'etag', b'foobar']]
+ assert resp.content == b''
+
+
+class TestAssembleRequest(object):
+ c = tcp.TCPClient(("127.0.0.1", 0))
+
+ def test_request_simple(self):
+ bytes = HTTP2Protocol(self.c).assemble_request(http.Request(
+ b'',
+ b'GET',
+ b'https',
+ b'',
+ b'',
+ b'/',
+ b"HTTP/2.0",
+ None,
+ None,
+ ))
+ assert len(bytes) == 1
+ assert bytes[0] == codecs.decode('00000d0105000000018284874188089d5c0b8170dc07', 'hex_codec')
+
+ def test_request_with_stream_id(self):
+ req = http.Request(
+ b'',
+ b'GET',
+ b'https',
+ b'',
+ b'',
+ b'/',
+ b"HTTP/2.0",
+ None,
+ None,
+ )
+ req.stream_id = 0x42
+ bytes = HTTP2Protocol(self.c).assemble_request(req)
+ assert len(bytes) == 1
+ assert bytes[0] == codecs.decode('00000d0105000000428284874188089d5c0b8170dc07', 'hex_codec')
+
+ def test_request_with_body(self):
+ bytes = HTTP2Protocol(self.c).assemble_request(http.Request(
+ b'',
+ b'GET',
+ b'https',
+ b'',
+ b'',
+ b'/',
+ b"HTTP/2.0",
+ http.Headers([(b'foo', b'bar')]),
+ b'foobar',
+ ))
+ assert len(bytes) == 2
+ assert bytes[0] ==\
+ codecs.decode('0000150104000000018284874188089d5c0b8170dc07408294e7838c767f', 'hex_codec')
+ assert bytes[1] ==\
+ codecs.decode('000006000100000001666f6f626172', 'hex_codec')
+
+
+class TestAssembleResponse(object):
+ c = tcp.TCPClient(("127.0.0.1", 0))
+
+ def test_simple(self):
+ bytes = HTTP2Protocol(self.c, is_server=True).assemble_response(http.Response(
+ b"HTTP/2.0",
+ 200,
+ ))
+ assert len(bytes) == 1
+ assert bytes[0] ==\
+ codecs.decode('00000101050000000288', 'hex_codec')
+
+ def test_with_stream_id(self):
+ resp = http.Response(
+ b"HTTP/2.0",
+ 200,
+ )
+ resp.stream_id = 0x42
+ bytes = HTTP2Protocol(self.c, is_server=True).assemble_response(resp)
+ assert len(bytes) == 1
+ assert bytes[0] ==\
+ codecs.decode('00000101050000004288', 'hex_codec')
+
+ def test_with_body(self):
+ bytes = HTTP2Protocol(self.c, is_server=True).assemble_response(http.Response(
+ b"HTTP/2.0",
+ 200,
+ b'',
+ http.Headers(foo=b"bar"),
+ b'foobar'
+ ))
+ assert len(bytes) == 2
+ assert bytes[0] ==\
+ codecs.decode('00000901040000000288408294e7838c767f', 'hex_codec')
+ assert bytes[1] ==\
+ codecs.decode('000006000100000002666f6f626172', 'hex_codec')
diff --git a/test/netlib/http/test_authentication.py b/test/netlib/http/test_authentication.py
new file mode 100644
index 00000000..1df7cd9c
--- /dev/null
+++ b/test/netlib/http/test_authentication.py
@@ -0,0 +1,122 @@
+import binascii
+
+from netlib import tutils
+from netlib.http import authentication, Headers
+
+
+def test_parse_http_basic_auth():
+ vals = ("basic", "foo", "bar")
+ assert authentication.parse_http_basic_auth(
+ authentication.assemble_http_basic_auth(*vals)
+ ) == vals
+ assert not authentication.parse_http_basic_auth("")
+ assert not authentication.parse_http_basic_auth("foo bar")
+ v = "basic " + binascii.b2a_base64(b"foo").decode("ascii")
+ assert not authentication.parse_http_basic_auth(v)
+
+
+class TestPassManNonAnon:
+
+ def test_simple(self):
+ p = authentication.PassManNonAnon()
+ assert not p.test("", "")
+ assert p.test("user", "")
+
+
+class TestPassManHtpasswd:
+
+ def test_file_errors(self):
+ tutils.raises(
+ "malformed htpasswd file",
+ authentication.PassManHtpasswd,
+ tutils.test_data.path("data/server.crt"))
+
+ def test_simple(self):
+ pm = authentication.PassManHtpasswd(tutils.test_data.path("data/htpasswd"))
+
+ vals = ("basic", "test", "test")
+ authentication.assemble_http_basic_auth(*vals)
+ assert pm.test("test", "test")
+ assert not pm.test("test", "foo")
+ assert not pm.test("foo", "test")
+ assert not pm.test("test", "")
+ assert not pm.test("", "")
+
+
+class TestPassManSingleUser:
+
+ def test_simple(self):
+ pm = authentication.PassManSingleUser("test", "test")
+ assert pm.test("test", "test")
+ assert not pm.test("test", "foo")
+ assert not pm.test("foo", "test")
+
+
+class TestNullProxyAuth:
+
+ def test_simple(self):
+ na = authentication.NullProxyAuth(authentication.PassManNonAnon())
+ assert not na.auth_challenge_headers()
+ assert na.authenticate("foo")
+ na.clean({})
+
+
+class TestBasicProxyAuth:
+
+ def test_simple(self):
+ ba = authentication.BasicProxyAuth(authentication.PassManNonAnon(), "test")
+ headers = Headers()
+ assert ba.auth_challenge_headers()
+ assert not ba.authenticate(headers)
+
+ def test_authenticate_clean(self):
+ ba = authentication.BasicProxyAuth(authentication.PassManNonAnon(), "test")
+
+ headers = Headers()
+ vals = ("basic", "foo", "bar")
+ headers[ba.AUTH_HEADER] = authentication.assemble_http_basic_auth(*vals)
+ assert ba.authenticate(headers)
+
+ ba.clean(headers)
+ assert not ba.AUTH_HEADER in headers
+
+ headers[ba.AUTH_HEADER] = ""
+ assert not ba.authenticate(headers)
+
+ headers[ba.AUTH_HEADER] = "foo"
+ assert not ba.authenticate(headers)
+
+ vals = ("foo", "foo", "bar")
+ headers[ba.AUTH_HEADER] = authentication.assemble_http_basic_auth(*vals)
+ assert not ba.authenticate(headers)
+
+ ba = authentication.BasicProxyAuth(authentication.PassMan(), "test")
+ vals = ("basic", "foo", "bar")
+ headers[ba.AUTH_HEADER] = authentication.assemble_http_basic_auth(*vals)
+ assert not ba.authenticate(headers)
+
+
+class Bunch:
+ pass
+
+
+class TestAuthAction:
+
+ def test_nonanonymous(self):
+ m = Bunch()
+ aa = authentication.NonanonymousAuthAction(None, "authenticator")
+ aa(None, m, None, None)
+ assert m.authenticator
+
+ def test_singleuser(self):
+ m = Bunch()
+ aa = authentication.SingleuserAuthAction(None, "authenticator")
+ aa(None, m, "foo:bar", None)
+ assert m.authenticator
+ tutils.raises("invalid", aa, None, m, "foo", None)
+
+ def test_httppasswd(self):
+ m = Bunch()
+ aa = authentication.HtpasswdAuthAction(None, "authenticator")
+ aa(None, m, tutils.test_data.path("data/htpasswd"), None)
+ assert m.authenticator
diff --git a/test/netlib/http/test_cookies.py b/test/netlib/http/test_cookies.py
new file mode 100644
index 00000000..34bb64f2
--- /dev/null
+++ b/test/netlib/http/test_cookies.py
@@ -0,0 +1,218 @@
+from netlib.http import cookies
+
+
+def test_read_token():
+ tokens = [
+ [("foo", 0), ("foo", 3)],
+ [("foo", 1), ("oo", 3)],
+ [(" foo", 1), ("foo", 4)],
+ [(" foo;", 1), ("foo", 4)],
+ [(" foo=", 1), ("foo", 4)],
+ [(" foo=bar", 1), ("foo", 4)],
+ ]
+ for q, a in tokens:
+ assert cookies._read_token(*q) == a
+
+
+def test_read_quoted_string():
+ tokens = [
+ [('"foo" x', 0), ("foo", 5)],
+ [('"f\oo" x', 0), ("foo", 6)],
+ [(r'"f\\o" x', 0), (r"f\o", 6)],
+ [(r'"f\\" x', 0), (r"f" + '\\', 5)],
+ [('"fo\\\"" x', 0), ("fo\"", 6)],
+ [('"foo" x', 7), ("", 8)],
+ ]
+ for q, a in tokens:
+ assert cookies._read_quoted_string(*q) == a
+
+
+def test_read_pairs():
+ vals = [
+ [
+ "one",
+ [["one", None]]
+ ],
+ [
+ "one=two",
+ [["one", "two"]]
+ ],
+ [
+ "one=",
+ [["one", ""]]
+ ],
+ [
+ 'one="two"',
+ [["one", "two"]]
+ ],
+ [
+ 'one="two"; three=four',
+ [["one", "two"], ["three", "four"]]
+ ],
+ [
+ 'one="two"; three=four; five',
+ [["one", "two"], ["three", "four"], ["five", None]]
+ ],
+ [
+ 'one="\\"two"; three=four',
+ [["one", '"two'], ["three", "four"]]
+ ],
+ ]
+ for s, lst in vals:
+ ret, off = cookies._read_pairs(s)
+ assert ret == lst
+
+
+def test_pairs_roundtrips():
+ pairs = [
+ [
+ "",
+ []
+ ],
+ [
+ "one=uno",
+ [["one", "uno"]]
+ ],
+ [
+ "one",
+ [["one", None]]
+ ],
+ [
+ "one=uno; two=due",
+ [["one", "uno"], ["two", "due"]]
+ ],
+ [
+ 'one="uno"; two="\due"',
+ [["one", "uno"], ["two", "due"]]
+ ],
+ [
+ 'one="un\\"o"',
+ [["one", 'un"o']]
+ ],
+ [
+ 'one="uno,due"',
+ [["one", 'uno,due']]
+ ],
+ [
+ "one=uno; two; three=tre",
+ [["one", "uno"], ["two", None], ["three", "tre"]]
+ ],
+ [
+ "_lvs2=zHai1+Hq+Tc2vmc2r4GAbdOI5Jopg3EwsdUT9g=; "
+ "_rcc2=53VdltWl+Ov6ordflA==;",
+ [
+ ["_lvs2", "zHai1+Hq+Tc2vmc2r4GAbdOI5Jopg3EwsdUT9g="],
+ ["_rcc2", "53VdltWl+Ov6ordflA=="]
+ ]
+ ]
+ ]
+ for s, lst in pairs:
+ ret, off = cookies._read_pairs(s)
+ assert ret == lst
+ s2 = cookies._format_pairs(lst)
+ ret, off = cookies._read_pairs(s2)
+ assert ret == lst
+
+
+def test_cookie_roundtrips():
+ pairs = [
+ [
+ "one=uno",
+ [["one", "uno"]]
+ ],
+ [
+ "one=uno; two=due",
+ [["one", "uno"], ["two", "due"]]
+ ],
+ ]
+ for s, lst in pairs:
+ ret = cookies.parse_cookie_header(s)
+ assert ret.lst == lst
+ s2 = cookies.format_cookie_header(ret)
+ ret = cookies.parse_cookie_header(s2)
+ assert ret.lst == lst
+
+
+def test_parse_set_cookie_pairs():
+ pairs = [
+ [
+ "one=uno",
+ [
+ ["one", "uno"]
+ ]
+ ],
+ [
+ "one=un\x20",
+ [
+ ["one", "un\x20"]
+ ]
+ ],
+ [
+ "one=uno; foo",
+ [
+ ["one", "uno"],
+ ["foo", None]
+ ]
+ ],
+ [
+ "mun=1.390.f60; "
+ "expires=sun, 11-oct-2015 12:38:31 gmt; path=/; "
+ "domain=b.aol.com",
+ [
+ ["mun", "1.390.f60"],
+ ["expires", "sun, 11-oct-2015 12:38:31 gmt"],
+ ["path", "/"],
+ ["domain", "b.aol.com"]
+ ]
+ ],
+ [
+ r'rpb=190%3d1%2616726%3d1%2634832%3d1%2634874%3d1; '
+ 'domain=.rubiconproject.com; '
+ 'expires=mon, 11-may-2015 21:54:57 gmt; '
+ 'path=/',
+ [
+ ['rpb', r'190%3d1%2616726%3d1%2634832%3d1%2634874%3d1'],
+ ['domain', '.rubiconproject.com'],
+ ['expires', 'mon, 11-may-2015 21:54:57 gmt'],
+ ['path', '/']
+ ]
+ ],
+ ]
+ for s, lst in pairs:
+ ret = cookies._parse_set_cookie_pairs(s)
+ assert ret == lst
+ s2 = cookies._format_set_cookie_pairs(ret)
+ ret2 = cookies._parse_set_cookie_pairs(s2)
+ assert ret2 == lst
+
+
+def test_parse_set_cookie_header():
+ vals = [
+ [
+ "", None
+ ],
+ [
+ ";", None
+ ],
+ [
+ "one=uno",
+ ("one", "uno", [])
+ ],
+ [
+ "one=uno; foo=bar",
+ ("one", "uno", [["foo", "bar"]])
+ ]
+ ]
+ for s, expected in vals:
+ ret = cookies.parse_set_cookie_header(s)
+ if expected:
+ assert ret[0] == expected[0]
+ assert ret[1] == expected[1]
+ assert ret[2].lst == expected[2]
+ s2 = cookies.format_set_cookie_header(*ret)
+ ret2 = cookies.parse_set_cookie_header(s2)
+ assert ret2[0] == expected[0]
+ assert ret2[1] == expected[1]
+ assert ret2[2].lst == expected[2]
+ else:
+ assert ret is None
diff --git a/test/netlib/http/test_headers.py b/test/netlib/http/test_headers.py
new file mode 100644
index 00000000..d50fee3e
--- /dev/null
+++ b/test/netlib/http/test_headers.py
@@ -0,0 +1,152 @@
+from netlib.http import Headers
+from netlib.tutils import raises
+
+
+class TestHeaders(object):
+ def _2host(self):
+ return Headers(
+ [
+ [b"Host", b"example.com"],
+ [b"host", b"example.org"]
+ ]
+ )
+
+ def test_init(self):
+ headers = Headers()
+ assert len(headers) == 0
+
+ headers = Headers([[b"Host", b"example.com"]])
+ assert len(headers) == 1
+ assert headers["Host"] == "example.com"
+
+ headers = Headers(Host="example.com")
+ assert len(headers) == 1
+ assert headers["Host"] == "example.com"
+
+ headers = Headers(
+ [[b"Host", b"invalid"]],
+ Host="example.com"
+ )
+ assert len(headers) == 1
+ assert headers["Host"] == "example.com"
+
+ headers = Headers(
+ [[b"Host", b"invalid"], [b"Accept", b"text/plain"]],
+ Host="example.com"
+ )
+ assert len(headers) == 2
+ assert headers["Host"] == "example.com"
+ assert headers["Accept"] == "text/plain"
+
+ with raises(ValueError):
+ Headers([[b"Host", u"not-bytes"]])
+
+ def test_getitem(self):
+ headers = Headers(Host="example.com")
+ assert headers["Host"] == "example.com"
+ assert headers["host"] == "example.com"
+ with raises(KeyError):
+ _ = headers["Accept"]
+
+ headers = self._2host()
+ assert headers["Host"] == "example.com, example.org"
+
+ def test_str(self):
+ headers = Headers(Host="example.com")
+ assert bytes(headers) == b"Host: example.com\r\n"
+
+ headers = Headers([
+ [b"Host", b"example.com"],
+ [b"Accept", b"text/plain"]
+ ])
+ assert bytes(headers) == b"Host: example.com\r\nAccept: text/plain\r\n"
+
+ headers = Headers()
+ assert bytes(headers) == b""
+
+ def test_setitem(self):
+ headers = Headers()
+ headers["Host"] = "example.com"
+ assert "Host" in headers
+ assert "host" in headers
+ assert headers["Host"] == "example.com"
+
+ headers["host"] = "example.org"
+ assert "Host" in headers
+ assert "host" in headers
+ assert headers["Host"] == "example.org"
+
+ headers["accept"] = "text/plain"
+ assert len(headers) == 2
+ assert "Accept" in headers
+ assert "Host" in headers
+
+ headers = self._2host()
+ assert len(headers.fields) == 2
+ headers["Host"] = "example.com"
+ assert len(headers.fields) == 1
+ assert "Host" in headers
+
+ def test_delitem(self):
+ headers = Headers(Host="example.com")
+ assert len(headers) == 1
+ del headers["host"]
+ assert len(headers) == 0
+ try:
+ del headers["host"]
+ except KeyError:
+ assert True
+ else:
+ assert False
+
+ headers = self._2host()
+ del headers["Host"]
+ assert len(headers) == 0
+
+ def test_keys(self):
+ headers = Headers(Host="example.com")
+ assert list(headers.keys()) == ["Host"]
+
+ headers = self._2host()
+ assert list(headers.keys()) == ["Host"]
+
+ def test_eq_ne(self):
+ headers1 = Headers(Host="example.com")
+ headers2 = Headers(host="example.com")
+ assert not (headers1 == headers2)
+ assert headers1 != headers2
+
+ headers1 = Headers(Host="example.com")
+ headers2 = Headers(Host="example.com")
+ assert headers1 == headers2
+ assert not (headers1 != headers2)
+
+ assert headers1 != 42
+
+ def test_get_all(self):
+ headers = self._2host()
+ assert headers.get_all("host") == ["example.com", "example.org"]
+ assert headers.get_all("accept") == []
+
+ def test_set_all(self):
+ headers = Headers(Host="example.com")
+ headers.set_all("Accept", ["text/plain"])
+ assert len(headers) == 2
+ assert "accept" in headers
+
+ headers = self._2host()
+ headers.set_all("Host", ["example.org"])
+ assert headers["host"] == "example.org"
+
+ headers.set_all("Host", ["example.org", "example.net"])
+ assert headers["host"] == "example.org, example.net"
+
+ def test_state(self):
+ headers = self._2host()
+ assert len(headers.get_state()) == 2
+ assert headers == Headers.from_state(headers.get_state())
+
+ headers2 = Headers()
+ assert headers != headers2
+ headers2.set_state(headers.get_state())
+ assert headers == headers2
diff --git a/test/netlib/http/test_message.py b/test/netlib/http/test_message.py
new file mode 100644
index 00000000..4b1f4630
--- /dev/null
+++ b/test/netlib/http/test_message.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, print_function, division
+
+from netlib.http import decoded, Headers
+from netlib.tutils import tresp, raises
+
+
+def _test_passthrough_attr(message, attr):
+ assert getattr(message, attr) == getattr(message.data, attr)
+ setattr(message, attr, "foo")
+ assert getattr(message.data, attr) == "foo"
+
+
+def _test_decoded_attr(message, attr):
+ assert getattr(message, attr) == getattr(message.data, attr).decode("utf8")
+ # Set str, get raw bytes
+ setattr(message, attr, "foo")
+ assert getattr(message.data, attr) == b"foo"
+ # Set raw bytes, get decoded
+ setattr(message.data, attr, b"BAR") # use uppercase so that we can also cover request.method
+ assert getattr(message, attr) == "BAR"
+ # Set bytes, get raw bytes
+ setattr(message, attr, b"baz")
+ assert getattr(message.data, attr) == b"baz"
+
+ # Set UTF8
+ setattr(message, attr, "Non-Autorisé")
+ assert getattr(message.data, attr) == b"Non-Autoris\xc3\xa9"
+ # Don't fail on garbage
+ setattr(message.data, attr, b"FOO\xFF\x00BAR")
+ assert getattr(message, attr).startswith("FOO")
+ assert getattr(message, attr).endswith("BAR")
+ # foo.bar = foo.bar should not cause any side effects.
+ d = getattr(message, attr)
+ setattr(message, attr, d)
+ assert getattr(message.data, attr) == b"FOO\xFF\x00BAR"
+
+
+class TestMessageData(object):
+ def test_eq_ne(self):
+ data = tresp(timestamp_start=42, timestamp_end=42).data
+ same = tresp(timestamp_start=42, timestamp_end=42).data
+ assert data == same
+ assert not data != same
+
+ other = tresp(content=b"foo").data
+ assert not data == other
+ assert data != other
+
+ assert data != 0
+
+
+class TestMessage(object):
+
+ def test_init(self):
+ resp = tresp()
+ assert resp.data
+
+ def test_eq_ne(self):
+ resp = tresp(timestamp_start=42, timestamp_end=42)
+ same = tresp(timestamp_start=42, timestamp_end=42)
+ assert resp == same
+ assert not resp != same
+
+ other = tresp(timestamp_start=0, timestamp_end=0)
+ assert not resp == other
+ assert resp != other
+
+ assert resp != 0
+
+ def test_content_length_update(self):
+ resp = tresp()
+ resp.content = b"foo"
+ assert resp.data.content == b"foo"
+ assert resp.headers["content-length"] == "3"
+ resp.content = b""
+ assert resp.data.content == b""
+ assert resp.headers["content-length"] == "0"
+
+ def test_content_basic(self):
+ _test_passthrough_attr(tresp(), "content")
+
+ def test_headers(self):
+ _test_passthrough_attr(tresp(), "headers")
+
+ def test_timestamp_start(self):
+ _test_passthrough_attr(tresp(), "timestamp_start")
+
+ def test_timestamp_end(self):
+ _test_passthrough_attr(tresp(), "timestamp_end")
+
+ def teste_http_version(self):
+ _test_decoded_attr(tresp(), "http_version")
+
+
+class TestDecodedDecorator(object):
+
+ def test_simple(self):
+ r = tresp()
+ assert r.content == b"message"
+ assert "content-encoding" not in r.headers
+ assert r.encode("gzip")
+
+ assert r.headers["content-encoding"]
+ assert r.content != b"message"
+ with decoded(r):
+ assert "content-encoding" not in r.headers
+ assert r.content == b"message"
+ assert r.headers["content-encoding"]
+ assert r.content != b"message"
+
+ def test_modify(self):
+ r = tresp()
+ assert "content-encoding" not in r.headers
+ assert r.encode("gzip")
+
+ with decoded(r):
+ r.content = b"foo"
+
+ assert r.content != b"foo"
+ r.decode()
+ assert r.content == b"foo"
+
+ def test_unknown_ce(self):
+ r = tresp()
+ r.headers["content-encoding"] = "zopfli"
+ r.content = b"foo"
+ with decoded(r):
+ assert r.headers["content-encoding"]
+ assert r.content == b"foo"
+ assert r.headers["content-encoding"]
+ assert r.content == b"foo"
+
+ def test_cannot_decode(self):
+ r = tresp()
+ assert r.encode("gzip")
+ r.content = b"foo"
+ with decoded(r):
+ assert r.headers["content-encoding"]
+ assert r.content == b"foo"
+ assert r.headers["content-encoding"]
+ assert r.content != b"foo"
+ r.decode()
+ assert r.content == b"foo"
+
+ def test_cannot_encode(self):
+ r = tresp()
+ assert r.encode("gzip")
+ with decoded(r):
+ r.content = None
+
+ assert "content-encoding" not in r.headers
+ assert r.content is None
diff --git a/test/netlib/http/test_request.py b/test/netlib/http/test_request.py
new file mode 100644
index 00000000..900b2cd1
--- /dev/null
+++ b/test/netlib/http/test_request.py
@@ -0,0 +1,238 @@
+# -*- coding: utf-8 -*-
+from __future__ import absolute_import, print_function, division
+
+import six
+
+from netlib import utils
+from netlib.http import Headers
+from netlib.odict import ODict
+from netlib.tutils import treq, raises
+from .test_message import _test_decoded_attr, _test_passthrough_attr
+
+
+class TestRequestData(object):
+ def test_init(self):
+ with raises(ValueError if six.PY2 else TypeError):
+ treq(headers="foobar")
+
+ assert isinstance(treq(headers=None).headers, Headers)
+
+
+class TestRequestCore(object):
+ """
+ Tests for builtins and the attributes that are directly proxied from the data structure
+ """
+ def test_repr(self):
+ request = treq()
+ assert repr(request) == "Request(GET address:22/path)"
+ request.host = None
+ assert repr(request) == "Request(GET /path)"
+
+ def test_first_line_format(self):
+ _test_passthrough_attr(treq(), "first_line_format")
+
+ def test_method(self):
+ _test_decoded_attr(treq(), "method")
+
+ def test_scheme(self):
+ _test_decoded_attr(treq(), "scheme")
+
+ def test_port(self):
+ _test_passthrough_attr(treq(), "port")
+
+ def test_path(self):
+ _test_decoded_attr(treq(), "path")
+
+ def test_host(self):
+ if six.PY2:
+ from unittest import SkipTest
+ raise SkipTest()
+
+ request = treq()
+ assert request.host == request.data.host.decode("idna")
+
+ # Test IDNA encoding
+ # Set str, get raw bytes
+ request.host = "ídna.example"
+ assert request.data.host == b"xn--dna-qma.example"
+ # Set raw bytes, get decoded
+ request.data.host = b"xn--idn-gla.example"
+ assert request.host == "idná.example"
+ # Set bytes, get raw bytes
+ request.host = b"xn--dn-qia9b.example"
+ assert request.data.host == b"xn--dn-qia9b.example"
+ # IDNA encoding is not bijective
+ request.host = "fußball"
+ assert request.host == "fussball"
+
+ # Don't fail on garbage
+ request.data.host = b"foo\xFF\x00bar"
+ assert request.host.startswith("foo")
+ assert request.host.endswith("bar")
+ # foo.bar = foo.bar should not cause any side effects.
+ d = request.host
+ request.host = d
+ assert request.data.host == b"foo\xFF\x00bar"
+
+ def test_host_header_update(self):
+ request = treq()
+ assert "host" not in request.headers
+ request.host = "example.com"
+ assert "host" not in request.headers
+
+ request.headers["Host"] = "foo"
+ request.host = "example.org"
+ assert request.headers["Host"] == "example.org"
+
+
+class TestRequestUtils(object):
+ """
+ Tests for additional convenience methods.
+ """
+ def test_url(self):
+ request = treq()
+ assert request.url == "http://address:22/path"
+
+ request.url = "https://otheraddress:42/foo"
+ assert request.scheme == "https"
+ assert request.host == "otheraddress"
+ assert request.port == 42
+ assert request.path == "/foo"
+
+ with raises(ValueError):
+ request.url = "not-a-url"
+
+ def test_pretty_host(self):
+ request = treq()
+ assert request.pretty_host == "address"
+ assert request.host == "address"
+ request.headers["host"] = "other"
+ assert request.pretty_host == "other"
+ assert request.host == "address"
+ request.host = None
+ assert request.pretty_host is None
+ assert request.host is None
+
+ # Invalid IDNA
+ request.headers["host"] = ".disqus.com"
+ assert request.pretty_host == ".disqus.com"
+
+ def test_pretty_url(self):
+ request = treq()
+ assert request.url == "http://address:22/path"
+ assert request.pretty_url == "http://address:22/path"
+ request.headers["host"] = "other"
+ assert request.pretty_url == "http://other:22/path"
+
+ def test_pretty_url_authority(self):
+ request = treq(first_line_format="authority")
+ assert request.pretty_url == "address:22"
+
+ def test_get_query(self):
+ request = treq()
+ assert request.query is None
+
+ request.url = "http://localhost:80/foo?bar=42"
+ assert request.query.lst == [("bar", "42")]
+
+ def test_set_query(self):
+ request = treq()
+ request.query = ODict([])
+
+ def test_get_cookies_none(self):
+ request = treq()
+ request.headers = Headers()
+ assert len(request.cookies) == 0
+
+ def test_get_cookies_single(self):
+ request = treq()
+ request.headers = Headers(cookie="cookiename=cookievalue")
+ result = request.cookies
+ assert len(result) == 1
+ assert result['cookiename'] == ['cookievalue']
+
+ def test_get_cookies_double(self):
+ request = treq()
+ request.headers = Headers(cookie="cookiename=cookievalue;othercookiename=othercookievalue")
+ result = request.cookies
+ assert len(result) == 2
+ assert result['cookiename'] == ['cookievalue']
+ assert result['othercookiename'] == ['othercookievalue']
+
+ def test_get_cookies_withequalsign(self):
+ request = treq()
+ request.headers = Headers(cookie="cookiename=coo=kievalue;othercookiename=othercookievalue")
+ result = request.cookies
+ assert len(result) == 2
+ assert result['cookiename'] == ['coo=kievalue']
+ assert result['othercookiename'] == ['othercookievalue']
+
+ def test_set_cookies(self):
+ request = treq()
+ request.headers = Headers(cookie="cookiename=cookievalue")
+ result = request.cookies
+ result["cookiename"] = ["foo"]
+ request.cookies = result
+ assert request.cookies["cookiename"] == ["foo"]
+
+ def test_get_path_components(self):
+ request = treq(path=b"/foo/bar")
+ assert request.path_components == ["foo", "bar"]
+
+ def test_set_path_components(self):
+ request = treq()
+ request.path_components = ["foo", "baz"]
+ assert request.path == "/foo/baz"
+ request.path_components = []
+ assert request.path == "/"
+
+ def test_anticache(self):
+ request = treq()
+ request.headers["If-Modified-Since"] = "foo"
+ request.headers["If-None-Match"] = "bar"
+ request.anticache()
+ assert "If-Modified-Since" not in request.headers
+ assert "If-None-Match" not in request.headers
+
+ def test_anticomp(self):
+ request = treq()
+ request.headers["Accept-Encoding"] = "foobar"
+ request.anticomp()
+ assert request.headers["Accept-Encoding"] == "identity"
+
+ def test_constrain_encoding(self):
+ request = treq()
+
+ h = request.headers.copy()
+ request.constrain_encoding() # no-op if there is no accept_encoding header.
+ assert request.headers == h
+
+ request.headers["Accept-Encoding"] = "identity, gzip, foo"
+ request.constrain_encoding()
+ assert "foo" not in request.headers["Accept-Encoding"]
+ assert "gzip" in request.headers["Accept-Encoding"]
+
+ def test_get_urlencoded_form(self):
+ request = treq(content="foobar")
+ assert request.urlencoded_form is None
+
+ request.headers["Content-Type"] = "application/x-www-form-urlencoded"
+ assert request.urlencoded_form == ODict(utils.urldecode(request.content))
+
+ def test_set_urlencoded_form(self):
+ request = treq()
+ request.urlencoded_form = ODict([('foo', 'bar'), ('rab', 'oof')])
+ assert request.headers["Content-Type"] == "application/x-www-form-urlencoded"
+ assert request.content
+
+ def test_get_multipart_form(self):
+ request = treq(content="foobar")
+ assert request.multipart_form is None
+
+ request.headers["Content-Type"] = "multipart/form-data"
+ assert request.multipart_form == ODict(
+ utils.multipartdecode(
+ request.headers,
+ request.content
+ )
+ )
diff --git a/test/netlib/http/test_response.py b/test/netlib/http/test_response.py
new file mode 100644
index 00000000..14588000
--- /dev/null
+++ b/test/netlib/http/test_response.py
@@ -0,0 +1,102 @@
+from __future__ import absolute_import, print_function, division
+
+import six
+
+from netlib.http import Headers
+from netlib.odict import ODict, ODictCaseless
+from netlib.tutils import raises, tresp
+from .test_message import _test_passthrough_attr, _test_decoded_attr
+
+
+class TestResponseData(object):
+ def test_init(self):
+ with raises(ValueError if six.PY2 else TypeError):
+ tresp(headers="foobar")
+
+ assert isinstance(tresp(headers=None).headers, Headers)
+
+
+class TestResponseCore(object):
+ """
+ Tests for builtins and the attributes that are directly proxied from the data structure
+ """
+ def test_repr(self):
+ response = tresp()
+ assert repr(response) == "Response(200 OK, unknown content type, 7B)"
+ response.content = None
+ assert repr(response) == "Response(200 OK, no content)"
+
+ def test_status_code(self):
+ _test_passthrough_attr(tresp(), "status_code")
+
+ def test_reason(self):
+ _test_decoded_attr(tresp(), "reason")
+
+
+class TestResponseUtils(object):
+ """
+ Tests for additional convenience methods.
+ """
+ def test_get_cookies_none(self):
+ resp = tresp()
+ resp.headers = Headers()
+ assert not resp.cookies
+
+ def test_get_cookies_empty(self):
+ resp = tresp()
+ resp.headers = Headers(set_cookie="")
+ assert not resp.cookies
+
+ def test_get_cookies_simple(self):
+ resp = tresp()
+ resp.headers = Headers(set_cookie="cookiename=cookievalue")
+ result = resp.cookies
+ assert len(result) == 1
+ assert "cookiename" in result
+ assert result["cookiename"][0] == ["cookievalue", ODict()]
+
+ def test_get_cookies_with_parameters(self):
+ resp = tresp()
+ resp.headers = Headers(set_cookie="cookiename=cookievalue;domain=example.com;expires=Wed Oct 21 16:29:41 2015;path=/; HttpOnly")
+ result = resp.cookies
+ assert len(result) == 1
+ assert "cookiename" in result
+ assert result["cookiename"][0][0] == "cookievalue"
+ attrs = result["cookiename"][0][1]
+ assert len(attrs) == 4
+ assert attrs["domain"] == ["example.com"]
+ assert attrs["expires"] == ["Wed Oct 21 16:29:41 2015"]
+ assert attrs["path"] == ["/"]
+ assert attrs["httponly"] == [None]
+
+ def test_get_cookies_no_value(self):
+ resp = tresp()
+ resp.headers = Headers(set_cookie="cookiename=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/")
+ result = resp.cookies
+ assert len(result) == 1
+ assert "cookiename" in result
+ assert result["cookiename"][0][0] == ""
+ assert len(result["cookiename"][0][1]) == 2
+
+ def test_get_cookies_twocookies(self):
+ resp = tresp()
+ resp.headers = Headers([
+ [b"Set-Cookie", b"cookiename=cookievalue"],
+ [b"Set-Cookie", b"othercookie=othervalue"]
+ ])
+ result = resp.cookies
+ assert len(result) == 2
+ assert "cookiename" in result
+ assert result["cookiename"][0] == ["cookievalue", ODict()]
+ assert "othercookie" in result
+ assert result["othercookie"][0] == ["othervalue", ODict()]
+
+ def test_set_cookies(self):
+ resp = tresp()
+ v = resp.cookies
+ v.add("foo", ["bar", ODictCaseless()])
+ resp.set_cookies(v)
+
+ v = resp.cookies
+ assert len(v) == 1
+ assert v["foo"] == [["bar", ODictCaseless()]]
diff --git a/test/netlib/http/test_status_codes.py b/test/netlib/http/test_status_codes.py
new file mode 100644
index 00000000..9fea6b70
--- /dev/null
+++ b/test/netlib/http/test_status_codes.py
@@ -0,0 +1,6 @@
+from netlib.http import status_codes
+
+
+def test_simple():
+ assert status_codes.IM_A_TEAPOT == 418
+ assert status_codes.RESPONSES[418] == "I'm a teapot"
diff --git a/test/netlib/http/test_user_agents.py b/test/netlib/http/test_user_agents.py
new file mode 100644
index 00000000..0bf1bba7
--- /dev/null
+++ b/test/netlib/http/test_user_agents.py
@@ -0,0 +1,6 @@
+from netlib.http import user_agents
+
+
+def test_get_shortcut():
+ assert user_agents.get_by_shortcut("c")[0] == "chrome"
+ assert not user_agents.get_by_shortcut("_")
diff --git a/test/netlib/test_certutils.py b/test/netlib/test_certutils.py
new file mode 100644
index 00000000..027dcc93
--- /dev/null
+++ b/test/netlib/test_certutils.py
@@ -0,0 +1,155 @@
+import os
+from netlib import certutils, tutils
+
+# class TestDNTree:
+# def test_simple(self):
+# d = certutils.DNTree()
+# d.add("foo.com", "foo")
+# d.add("bar.com", "bar")
+# assert d.get("foo.com") == "foo"
+# assert d.get("bar.com") == "bar"
+# assert not d.get("oink.com")
+# assert not d.get("oink")
+# assert not d.get("")
+# assert not d.get("oink.oink")
+#
+# d.add("*.match.org", "match")
+# assert not d.get("match.org")
+# assert d.get("foo.match.org") == "match"
+# assert d.get("foo.foo.match.org") == "match"
+#
+# def test_wildcard(self):
+# d = certutils.DNTree()
+# d.add("foo.com", "foo")
+# assert not d.get("*.foo.com")
+# d.add("*.foo.com", "wild")
+#
+# d = certutils.DNTree()
+# d.add("*", "foo")
+# assert d.get("foo.com") == "foo"
+# assert d.get("*.foo.com") == "foo"
+# assert d.get("com") == "foo"
+
+
+class TestCertStore:
+
+ def test_create_explicit(self):
+ with tutils.tmpdir() as d:
+ ca = certutils.CertStore.from_store(d, "test")
+ assert ca.get_cert(b"foo", [])
+
+ ca2 = certutils.CertStore.from_store(d, "test")
+ assert ca2.get_cert(b"foo", [])
+
+ assert ca.default_ca.get_serial_number() == ca2.default_ca.get_serial_number()
+
+ def test_create_no_common_name(self):
+ with tutils.tmpdir() as d:
+ ca = certutils.CertStore.from_store(d, "test")
+ assert ca.get_cert(None, [])[0].cn is None
+
+ def test_create_tmp(self):
+ with tutils.tmpdir() as d:
+ ca = certutils.CertStore.from_store(d, "test")
+ assert ca.get_cert(b"foo.com", [])
+ assert ca.get_cert(b"foo.com", [])
+ assert ca.get_cert(b"*.foo.com", [])
+
+ r = ca.get_cert(b"*.foo.com", [])
+ assert r[1] == ca.default_privatekey
+
+ def test_sans(self):
+ with tutils.tmpdir() as d:
+ ca = certutils.CertStore.from_store(d, "test")
+ c1 = ca.get_cert(b"foo.com", [b"*.bar.com"])
+ ca.get_cert(b"foo.bar.com", [])
+ # assert c1 == c2
+ c3 = ca.get_cert(b"bar.com", [])
+ assert not c1 == c3
+
+ def test_sans_change(self):
+ with tutils.tmpdir() as d:
+ ca = certutils.CertStore.from_store(d, "test")
+ ca.get_cert(b"foo.com", [b"*.bar.com"])
+ cert, key, chain_file = ca.get_cert(b"foo.bar.com", [b"*.baz.com"])
+ assert b"*.baz.com" in cert.altnames
+
+ def test_overrides(self):
+ with tutils.tmpdir() as d:
+ ca1 = certutils.CertStore.from_store(os.path.join(d, "ca1"), "test")
+ ca2 = certutils.CertStore.from_store(os.path.join(d, "ca2"), "test")
+ assert not ca1.default_ca.get_serial_number(
+ ) == ca2.default_ca.get_serial_number()
+
+ dc = ca2.get_cert(b"foo.com", [b"sans.example.com"])
+ dcp = os.path.join(d, "dc")
+ f = open(dcp, "wb")
+ f.write(dc[0].to_pem())
+ f.close()
+ ca1.add_cert_file(b"foo.com", dcp)
+
+ ret = ca1.get_cert(b"foo.com", [])
+ assert ret[0].serial == dc[0].serial
+
+
+class TestDummyCert:
+
+ def test_with_ca(self):
+ with tutils.tmpdir() as d:
+ ca = certutils.CertStore.from_store(d, "test")
+ r = certutils.dummy_cert(
+ ca.default_privatekey,
+ ca.default_ca,
+ b"foo.com",
+ [b"one.com", b"two.com", b"*.three.com"]
+ )
+ assert r.cn == b"foo.com"
+
+ r = certutils.dummy_cert(
+ ca.default_privatekey,
+ ca.default_ca,
+ None,
+ []
+ )
+ assert r.cn is None
+
+
+class TestSSLCert:
+
+ def test_simple(self):
+ with open(tutils.test_data.path("data/text_cert"), "rb") as f:
+ d = f.read()
+ c1 = certutils.SSLCert.from_pem(d)
+ assert c1.cn == b"google.com"
+ assert len(c1.altnames) == 436
+
+ with open(tutils.test_data.path("data/text_cert_2"), "rb") as f:
+ d = f.read()
+ c2 = certutils.SSLCert.from_pem(d)
+ assert c2.cn == b"www.inode.co.nz"
+ assert len(c2.altnames) == 2
+ assert c2.digest("sha1")
+ assert c2.notbefore
+ assert c2.notafter
+ assert c2.subject
+ assert c2.keyinfo == ("RSA", 2048)
+ assert c2.serial
+ assert c2.issuer
+ assert c2.to_pem()
+ assert c2.has_expired is not None
+
+ assert not c1 == c2
+ assert c1 != c2
+
+ def test_err_broken_sans(self):
+ with open(tutils.test_data.path("data/text_cert_weird1"), "rb") as f:
+ d = f.read()
+ c = certutils.SSLCert.from_pem(d)
+ # This breaks unless we ignore a decoding error.
+ assert c.altnames is not None
+
+ def test_der(self):
+ with open(tutils.test_data.path("data/dercert"), "rb") as f:
+ d = f.read()
+ s = certutils.SSLCert.from_der(d)
+ assert s.cn
diff --git a/test/netlib/test_encoding.py b/test/netlib/test_encoding.py
new file mode 100644
index 00000000..0ff1aad1
--- /dev/null
+++ b/test/netlib/test_encoding.py
@@ -0,0 +1,37 @@
+from netlib import encoding
+
+
+def test_identity():
+ assert b"string" == encoding.decode("identity", b"string")
+ assert b"string" == encoding.encode("identity", b"string")
+ assert not encoding.encode("nonexistent", b"string")
+ assert not encoding.decode("nonexistent encoding", b"string")
+
+
+def test_gzip():
+ assert b"string" == encoding.decode(
+ "gzip",
+ encoding.encode(
+ "gzip",
+ b"string"
+ )
+ )
+ assert encoding.decode("gzip", b"bogus") is None
+
+
+def test_deflate():
+ assert b"string" == encoding.decode(
+ "deflate",
+ encoding.encode(
+ "deflate",
+ b"string"
+ )
+ )
+ assert b"string" == encoding.decode(
+ "deflate",
+ encoding.encode(
+ "deflate",
+ b"string"
+ )[2:-4]
+ )
+ assert encoding.decode("deflate", b"bogus") is None
diff --git a/test/netlib/test_imports.py b/test/netlib/test_imports.py
new file mode 100644
index 00000000..b88ef26d
--- /dev/null
+++ b/test/netlib/test_imports.py
@@ -0,0 +1 @@
+# These are actually tests!
diff --git a/test/netlib/test_odict.py b/test/netlib/test_odict.py
new file mode 100644
index 00000000..f0985ef6
--- /dev/null
+++ b/test/netlib/test_odict.py
@@ -0,0 +1,153 @@
+from netlib import odict, tutils
+
+
+class TestODict(object):
+
+ def test_repr(self):
+ h = odict.ODict()
+ h["one"] = ["two"]
+ assert repr(h)
+
+ def test_str_err(self):
+ h = odict.ODict()
+ with tutils.raises(ValueError):
+ h["key"] = u"foo"
+ with tutils.raises(ValueError):
+ h["key"] = b"foo"
+
+ def test_getset_state(self):
+ od = odict.ODict()
+ od.add("foo", 1)
+ od.add("foo", 2)
+ od.add("bar", 3)
+ state = od.get_state()
+ nd = odict.ODict.from_state(state)
+ assert nd == od
+ b = odict.ODict()
+ b.set_state(state)
+ assert b == od
+
+ def test_in_any(self):
+ od = odict.ODict()
+ od["one"] = ["atwoa", "athreea"]
+ assert od.in_any("one", "two")
+ assert od.in_any("one", "three")
+ assert not od.in_any("one", "four")
+ assert not od.in_any("nonexistent", "foo")
+ assert not od.in_any("one", "TWO")
+ assert od.in_any("one", "TWO", True)
+
+ def test_iter(self):
+ od = odict.ODict()
+ assert not [i for i in od]
+ od.add("foo", 1)
+ assert [i for i in od]
+
+ def test_keys(self):
+ od = odict.ODict()
+ assert not od.keys()
+ od.add("foo", 1)
+ assert od.keys() == ["foo"]
+ od.add("foo", 2)
+ assert od.keys() == ["foo"]
+ od.add("bar", 2)
+ assert len(od.keys()) == 2
+
+ def test_copy(self):
+ od = odict.ODict()
+ od.add("foo", 1)
+ od.add("foo", 2)
+ od.add("bar", 3)
+ assert od == od.copy()
+ assert not od != od.copy()
+
+ def test_del(self):
+ od = odict.ODict()
+ od.add("foo", 1)
+ od.add("Foo", 2)
+ od.add("bar", 3)
+ del od["foo"]
+ assert len(od.lst) == 2
+
+ def test_replace(self):
+ od = odict.ODict()
+ od.add("one", "two")
+ od.add("two", "one")
+ assert od.replace("one", "vun") == 2
+ assert od.lst == [
+ ["vun", "two"],
+ ["two", "vun"],
+ ]
+
+ def test_get(self):
+ od = odict.ODict()
+ od.add("one", "two")
+ assert od.get("one") == ["two"]
+ assert od.get("two") is None
+
+ def test_get_first(self):
+ od = odict.ODict()
+ od.add("one", "two")
+ od.add("one", "three")
+ assert od.get_first("one") == "two"
+ assert od.get_first("two") is None
+
+ def test_extend(self):
+ a = odict.ODict([["a", "b"], ["c", "d"]])
+ b = odict.ODict([["a", "b"], ["e", "f"]])
+ a.extend(b)
+ assert len(a) == 4
+ assert a["a"] == ["b", "b"]
+
+
+class TestODictCaseless(object):
+
+ def test_override(self):
+ o = odict.ODictCaseless()
+ o.add('T', 'application/x-www-form-urlencoded; charset=UTF-8')
+ o["T"] = ["foo"]
+ assert o["T"] == ["foo"]
+
+ def test_case_preservation(self):
+ od = odict.ODictCaseless()
+ od["Foo"] = ["1"]
+ assert "foo" in od
+ assert od.items()[0][0] == "Foo"
+ assert od.get("foo") == ["1"]
+ assert od.get("foo", [""]) == ["1"]
+ assert od.get("Foo", [""]) == ["1"]
+ assert od.get("xx", "yy") == "yy"
+
+ def test_del(self):
+ od = odict.ODictCaseless()
+ od.add("foo", 1)
+ od.add("Foo", 2)
+ od.add("bar", 3)
+ del od["foo"]
+ assert len(od) == 1
+
+ def test_keys(self):
+ od = odict.ODictCaseless()
+ assert not od.keys()
+ od.add("foo", 1)
+ assert od.keys() == ["foo"]
+ od.add("Foo", 2)
+ assert od.keys() == ["foo"]
+ od.add("bar", 2)
+ assert len(od.keys()) == 2
+
+ def test_add_order(self):
+ od = odict.ODict(
+ [
+ ["one", "uno"],
+ ["two", "due"],
+ ["three", "tre"],
+ ]
+ )
+ od["two"] = ["foo", "bar"]
+ assert od.lst == [
+ ["one", "uno"],
+ ["two", "foo"],
+ ["three", "tre"],
+ ["two", "bar"],
+ ]
diff --git a/test/netlib/test_socks.py b/test/netlib/test_socks.py
new file mode 100644
index 00000000..d95dee41
--- /dev/null
+++ b/test/netlib/test_socks.py
@@ -0,0 +1,149 @@
+import ipaddress
+from io import BytesIO
+import socket
+from netlib import socks, tcp, tutils
+
+
+def test_client_greeting():
+ raw = tutils.treader(b"\x05\x02\x00\xBE\xEF")
+ out = BytesIO()
+ msg = socks.ClientGreeting.from_file(raw)
+ msg.assert_socks5()
+ msg.to_file(out)
+
+ assert out.getvalue() == raw.getvalue()[:-1]
+ assert msg.ver == 5
+ assert len(msg.methods) == 2
+ assert 0xBE in msg.methods
+ assert 0xEF not in msg.methods
+
+
+def test_client_greeting_assert_socks5():
+ raw = tutils.treader(b"\x00\x00")
+ msg = socks.ClientGreeting.from_file(raw)
+ tutils.raises(socks.SocksError, msg.assert_socks5)
+
+ raw = tutils.treader(b"HTTP/1.1 200 OK" + b" " * 100)
+ msg = socks.ClientGreeting.from_file(raw)
+ try:
+ msg.assert_socks5()
+ except socks.SocksError as e:
+ assert "Invalid SOCKS version" in str(e)
+ assert "HTTP" not in str(e)
+ else:
+ assert False
+
+ raw = tutils.treader(b"GET / HTTP/1.1" + b" " * 100)
+ msg = socks.ClientGreeting.from_file(raw)
+ try:
+ msg.assert_socks5()
+ except socks.SocksError as e:
+ assert "Invalid SOCKS version" in str(e)
+ assert "HTTP" in str(e)
+ else:
+ assert False
+
+ raw = tutils.treader(b"XX")
+ tutils.raises(
+ socks.SocksError,
+ socks.ClientGreeting.from_file,
+ raw,
+ fail_early=True)
+
+
+def test_server_greeting():
+ raw = tutils.treader(b"\x05\x02")
+ out = BytesIO()
+ msg = socks.ServerGreeting.from_file(raw)
+ msg.assert_socks5()
+ msg.to_file(out)
+
+ assert out.getvalue() == raw.getvalue()
+ assert msg.ver == 5
+ assert msg.method == 0x02
+
+
+def test_server_greeting_assert_socks5():
+ raw = tutils.treader(b"HTTP/1.1 200 OK" + b" " * 100)
+ msg = socks.ServerGreeting.from_file(raw)
+ try:
+ msg.assert_socks5()
+ except socks.SocksError as e:
+ assert "Invalid SOCKS version" in str(e)
+ assert "HTTP" in str(e)
+ else:
+ assert False
+
+ raw = tutils.treader(b"GET / HTTP/1.1" + b" " * 100)
+ msg = socks.ServerGreeting.from_file(raw)
+ try:
+ msg.assert_socks5()
+ except socks.SocksError as e:
+ assert "Invalid SOCKS version" in str(e)
+ assert "HTTP" not in str(e)
+ else:
+ assert False
+
+
+def test_message():
+ raw = tutils.treader(b"\x05\x01\x00\x03\x0bexample.com\xDE\xAD\xBE\xEF")
+ out = BytesIO()
+ msg = socks.Message.from_file(raw)
+ msg.assert_socks5()
+ assert raw.read(2) == b"\xBE\xEF"
+ msg.to_file(out)
+
+ assert out.getvalue() == raw.getvalue()[:-2]
+ assert msg.ver == 5
+ assert msg.msg == 0x01
+ assert msg.atyp == 0x03
+ assert msg.addr == ("example.com", 0xDEAD)
+
+
+def test_message_assert_socks5():
+ raw = tutils.treader(b"\xEE\x01\x00\x03\x0bexample.com\xDE\xAD\xBE\xEF")
+ msg = socks.Message.from_file(raw)
+ tutils.raises(socks.SocksError, msg.assert_socks5)
+
+
+def test_message_ipv4():
+ # Test ATYP=0x01 (IPV4)
+ raw = tutils.treader(b"\x05\x01\x00\x01\x7f\x00\x00\x01\xDE\xAD\xBE\xEF")
+ out = BytesIO()
+ msg = socks.Message.from_file(raw)
+ left = raw.read(2)
+ assert left == b"\xBE\xEF"
+ msg.to_file(out)
+
+ assert out.getvalue() == raw.getvalue()[:-2]
+ assert msg.addr == ("127.0.0.1", 0xDEAD)
+
+
+def test_message_ipv6():
+ # Test ATYP=0x04 (IPV6)
+ ipv6_addr = u"2001:db8:85a3:8d3:1319:8a2e:370:7344"
+
+ raw = tutils.treader(
+ b"\x05\x01\x00\x04" +
+ ipaddress.IPv6Address(ipv6_addr).packed +
+ b"\xDE\xAD\xBE\xEF")
+ out = BytesIO()
+ msg = socks.Message.from_file(raw)
+ assert raw.read(2) == b"\xBE\xEF"
+ msg.to_file(out)
+
+ assert out.getvalue() == raw.getvalue()[:-2]
+ assert msg.addr.host == ipv6_addr
+
+
+def test_message_invalid_rsv():
+ raw = tutils.treader(b"\x05\x01\xFF\x01\x7f\x00\x00\x01\xDE\xAD\xBE\xEF")
+ tutils.raises(socks.SocksError, socks.Message.from_file, raw)
+
+
+def test_message_unknown_atyp():
+ raw = tutils.treader(b"\x05\x02\x00\x02\x7f\x00\x00\x01\xDE\xAD\xBE\xEF")
+ tutils.raises(socks.SocksError, socks.Message.from_file, raw)
+
+ m = socks.Message(5, 1, 0x02, tcp.Address(("example.com", 5050)))
+ tutils.raises(socks.SocksError, m.to_file, BytesIO())
diff --git a/test/netlib/test_tcp.py b/test/netlib/test_tcp.py
new file mode 100644
index 00000000..8ae3aa51
--- /dev/null
+++ b/test/netlib/test_tcp.py
@@ -0,0 +1,795 @@
+from io import BytesIO
+from six.moves import queue
+import time
+import socket
+import random
+import os
+import threading
+import mock
+
+from OpenSSL import SSL
+import OpenSSL
+
+from netlib import tcp, certutils, tutils, tservers
+from netlib.exceptions import InvalidCertificateException, TcpReadIncomplete, TlsException, \
+ TcpTimeout, TcpDisconnect, TcpException, NetlibException
+
+
+class EchoHandler(tcp.BaseHandler):
+ sni = None
+
+ def handle_sni(self, connection):
+ self.sni = connection.get_servername()
+
+ def handle(self):
+ v = self.rfile.readline()
+ self.wfile.write(v)
+ self.wfile.flush()
+
+
+class ClientCipherListHandler(tcp.BaseHandler):
+ sni = None
+
+ def handle(self):
+ self.wfile.write("%s" % self.connection.get_cipher_list())
+ self.wfile.flush()
+
+
+class HangHandler(tcp.BaseHandler):
+
+ def handle(self):
+ while True:
+ time.sleep(1)
+
+
+class ALPNHandler(tcp.BaseHandler):
+ sni = None
+
+ def handle(self):
+ alp = self.get_alpn_proto_negotiated()
+ if alp:
+ self.wfile.write(alp)
+ else:
+ self.wfile.write(b"NONE")
+ self.wfile.flush()
+
+
+class TestServer(tservers.ServerTestBase):
+ handler = EchoHandler
+
+ def test_echo(self):
+ testval = b"echo!\n"
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.wfile.write(testval)
+ c.wfile.flush()
+ assert c.rfile.readline() == testval
+
+ def test_thread_start_error(self):
+ with mock.patch.object(threading.Thread, "start", side_effect=threading.ThreadError("nonewthread")) as m:
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ assert not c.rfile.read(1)
+ assert m.called
+ assert "nonewthread" in self.q.get_nowait()
+ self.test_echo()
+
+
+class TestServerBind(tservers.ServerTestBase):
+
+ class handler(tcp.BaseHandler):
+
+ def handle(self):
+ self.wfile.write(str(self.connection.getpeername()).encode())
+ self.wfile.flush()
+
+ def test_bind(self):
+ """ Test to bind to a given random port. Try again if the random port turned out to be blocked. """
+ for i in range(20):
+ random_port = random.randrange(1024, 65535)
+ try:
+ c = tcp.TCPClient(
+ ("127.0.0.1", self.port), source_address=(
+ "127.0.0.1", random_port))
+ c.connect()
+ assert c.rfile.readline() == str(("127.0.0.1", random_port)).encode()
+ return
+ except TcpException: # port probably already in use
+ pass
+
+
+class TestServerIPv6(tservers.ServerTestBase):
+ handler = EchoHandler
+ addr = tcp.Address(("localhost", 0), use_ipv6=True)
+
+ def test_echo(self):
+ testval = b"echo!\n"
+ c = tcp.TCPClient(tcp.Address(("::1", self.port), use_ipv6=True))
+ c.connect()
+ c.wfile.write(testval)
+ c.wfile.flush()
+ assert c.rfile.readline() == testval
+
+
+class TestEcho(tservers.ServerTestBase):
+ handler = EchoHandler
+
+ def test_echo(self):
+ testval = b"echo!\n"
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.wfile.write(testval)
+ c.wfile.flush()
+ assert c.rfile.readline() == testval
+
+
+class HardDisconnectHandler(tcp.BaseHandler):
+
+ def handle(self):
+ self.connection.close()
+
+
+class TestFinishFail(tservers.ServerTestBase):
+
+ """
+ This tests a difficult-to-trigger exception in the .finish() method of
+ the handler.
+ """
+ handler = EchoHandler
+
+ def test_disconnect_in_finish(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.wfile.write(b"foo\n")
+ c.wfile.flush = mock.Mock(side_effect=TcpDisconnect)
+ c.finish()
+
+
+class TestServerSSL(tservers.ServerTestBase):
+ handler = EchoHandler
+ ssl = dict(
+ cipher_list="AES256-SHA",
+ chain_file=tutils.test_data.path("data/server.crt")
+ )
+
+ def test_echo(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl(sni=b"foo.com", options=SSL.OP_ALL)
+ testval = b"echo!\n"
+ c.wfile.write(testval)
+ c.wfile.flush()
+ assert c.rfile.readline() == testval
+
+ def test_get_current_cipher(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ assert not c.get_current_cipher()
+ c.convert_to_ssl(sni=b"foo.com")
+ ret = c.get_current_cipher()
+ assert ret
+ assert "AES" in ret[0]
+
+
+class TestSSLv3Only(tservers.ServerTestBase):
+ handler = EchoHandler
+ ssl = dict(
+ request_client_cert=False,
+ v3_only=True
+ )
+
+ def test_failure(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ tutils.raises(TlsException, c.convert_to_ssl, sni=b"foo.com")
+
+
+class TestSSLUpstreamCertVerificationWBadServerCert(tservers.ServerTestBase):
+ handler = EchoHandler
+
+ ssl = dict(
+ cert=tutils.test_data.path("data/verificationcerts/self-signed.crt"),
+ key=tutils.test_data.path("data/verificationcerts/self-signed.key")
+ )
+
+ def test_mode_default_should_pass(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+
+ c.convert_to_ssl()
+
+ # Verification errors should be saved even if connection isn't aborted
+ # aborted
+ assert c.ssl_verification_error is not None
+
+ testval = b"echo!\n"
+ c.wfile.write(testval)
+ c.wfile.flush()
+ assert c.rfile.readline() == testval
+
+ def test_mode_none_should_pass(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+
+ c.convert_to_ssl(verify_options=SSL.VERIFY_NONE)
+
+ # Verification errors should be saved even if connection isn't aborted
+ assert c.ssl_verification_error is not None
+
+ testval = b"echo!\n"
+ c.wfile.write(testval)
+ c.wfile.flush()
+ assert c.rfile.readline() == testval
+
+ def test_mode_strict_should_fail(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+
+ with tutils.raises(InvalidCertificateException):
+ c.convert_to_ssl(
+ sni=b"example.mitmproxy.org",
+ verify_options=SSL.VERIFY_PEER,
+ ca_pemfile=tutils.test_data.path("data/verificationcerts/trusted-root.crt")
+ )
+
+ assert c.ssl_verification_error is not None
+
+ # Unknown issuing certificate authority for first certificate
+ assert c.ssl_verification_error['errno'] == 18
+ assert c.ssl_verification_error['depth'] == 0
+
+
+class TestSSLUpstreamCertVerificationWBadHostname(tservers.ServerTestBase):
+ handler = EchoHandler
+
+ ssl = dict(
+ cert=tutils.test_data.path("data/verificationcerts/trusted-leaf.crt"),
+ key=tutils.test_data.path("data/verificationcerts/trusted-leaf.key")
+ )
+
+ def test_should_fail_without_sni(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+
+ with tutils.raises(TlsException):
+ c.convert_to_ssl(
+ verify_options=SSL.VERIFY_PEER,
+ ca_pemfile=tutils.test_data.path("data/verificationcerts/trusted-root.crt")
+ )
+
+ def test_should_fail(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+
+ with tutils.raises(InvalidCertificateException):
+ c.convert_to_ssl(
+ sni=b"mitmproxy.org",
+ verify_options=SSL.VERIFY_PEER,
+ ca_pemfile=tutils.test_data.path("data/verificationcerts/trusted-root.crt")
+ )
+
+ assert c.ssl_verification_error is not None
+
+
+class TestSSLUpstreamCertVerificationWValidCertChain(tservers.ServerTestBase):
+ handler = EchoHandler
+
+ ssl = dict(
+ cert=tutils.test_data.path("data/verificationcerts/trusted-leaf.crt"),
+ key=tutils.test_data.path("data/verificationcerts/trusted-leaf.key")
+ )
+
+ def test_mode_strict_w_pemfile_should_pass(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+
+ c.convert_to_ssl(
+ sni=b"example.mitmproxy.org",
+ verify_options=SSL.VERIFY_PEER,
+ ca_pemfile=tutils.test_data.path("data/verificationcerts/trusted-root.crt")
+ )
+
+ assert c.ssl_verification_error is None
+
+ testval = b"echo!\n"
+ c.wfile.write(testval)
+ c.wfile.flush()
+ assert c.rfile.readline() == testval
+
+ def test_mode_strict_w_cadir_should_pass(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+
+ c.convert_to_ssl(
+ sni=b"example.mitmproxy.org",
+ verify_options=SSL.VERIFY_PEER,
+ ca_path=tutils.test_data.path("data/verificationcerts/")
+ )
+
+ assert c.ssl_verification_error is None
+
+ testval = b"echo!\n"
+ c.wfile.write(testval)
+ c.wfile.flush()
+ assert c.rfile.readline() == testval
+
+
+class TestSSLClientCert(tservers.ServerTestBase):
+
+ class handler(tcp.BaseHandler):
+ sni = None
+
+ def handle_sni(self, connection):
+ self.sni = connection.get_servername()
+
+ def handle(self):
+ self.wfile.write(b"%d\n" % self.clientcert.serial)
+ self.wfile.flush()
+
+ ssl = dict(
+ request_client_cert=True,
+ v3_only=False
+ )
+
+ def test_clientcert(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl(
+ cert=tutils.test_data.path("data/clientcert/client.pem"))
+ assert c.rfile.readline().strip() == b"1"
+
+ def test_clientcert_err(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ tutils.raises(
+ TlsException,
+ c.convert_to_ssl,
+ cert=tutils.test_data.path("data/clientcert/make")
+ )
+
+
+class TestSNI(tservers.ServerTestBase):
+
+ class handler(tcp.BaseHandler):
+ sni = None
+
+ def handle_sni(self, connection):
+ self.sni = connection.get_servername()
+
+ def handle(self):
+ self.wfile.write(self.sni)
+ self.wfile.flush()
+
+ ssl = True
+
+ def test_echo(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl(sni=b"foo.com")
+ assert c.sni == b"foo.com"
+ assert c.rfile.readline() == b"foo.com"
+
+
+class TestServerCipherList(tservers.ServerTestBase):
+ handler = ClientCipherListHandler
+ ssl = dict(
+ cipher_list='RC4-SHA'
+ )
+
+ def test_echo(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl(sni=b"foo.com")
+ assert c.rfile.readline() == b"['RC4-SHA']"
+
+
+class TestServerCurrentCipher(tservers.ServerTestBase):
+
+ class handler(tcp.BaseHandler):
+ sni = None
+
+ def handle(self):
+ self.wfile.write(str(self.get_current_cipher()).encode())
+ self.wfile.flush()
+
+ ssl = dict(
+ cipher_list='RC4-SHA'
+ )
+
+ def test_echo(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl(sni=b"foo.com")
+ assert b"RC4-SHA" in c.rfile.readline()
+
+
+class TestServerCipherListError(tservers.ServerTestBase):
+ handler = ClientCipherListHandler
+ ssl = dict(
+ cipher_list='bogus'
+ )
+
+ def test_echo(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ tutils.raises("handshake error", c.convert_to_ssl, sni=b"foo.com")
+
+
+class TestClientCipherListError(tservers.ServerTestBase):
+ handler = ClientCipherListHandler
+ ssl = dict(
+ cipher_list='RC4-SHA'
+ )
+
+ def test_echo(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ tutils.raises(
+ "cipher specification",
+ c.convert_to_ssl,
+ sni=b"foo.com",
+ cipher_list="bogus")
+
+
+class TestSSLDisconnect(tservers.ServerTestBase):
+
+ class handler(tcp.BaseHandler):
+
+ def handle(self):
+ self.finish()
+
+ ssl = True
+
+ def test_echo(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ # Excercise SSL.ZeroReturnError
+ c.rfile.read(10)
+ c.close()
+ tutils.raises(TcpDisconnect, c.wfile.write, b"foo")
+ tutils.raises(queue.Empty, self.q.get_nowait)
+
+
+class TestSSLHardDisconnect(tservers.ServerTestBase):
+ handler = HardDisconnectHandler
+ ssl = True
+
+ def test_echo(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ # Exercise SSL.SysCallError
+ c.rfile.read(10)
+ c.close()
+ tutils.raises(TcpDisconnect, c.wfile.write, b"foo")
+
+
+class TestDisconnect(tservers.ServerTestBase):
+
+ def test_echo(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.rfile.read(10)
+ c.wfile.write(b"foo")
+ c.close()
+ c.close()
+
+
+class TestServerTimeOut(tservers.ServerTestBase):
+
+ class handler(tcp.BaseHandler):
+
+ def handle(self):
+ self.timeout = False
+ self.settimeout(0.01)
+ try:
+ self.rfile.read(10)
+ except TcpTimeout:
+ self.timeout = True
+
+ def test_timeout(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ time.sleep(0.3)
+ assert self.last_handler.timeout
+
+
+class TestTimeOut(tservers.ServerTestBase):
+ handler = HangHandler
+
+ def test_timeout(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.settimeout(0.1)
+ assert c.gettimeout() == 0.1
+ tutils.raises(TcpTimeout, c.rfile.read, 10)
+
+
+class TestALPNClient(tservers.ServerTestBase):
+ handler = ALPNHandler
+ ssl = dict(
+ alpn_select=b"bar"
+ )
+
+ if tcp.HAS_ALPN:
+ def test_alpn(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl(alpn_protos=[b"foo", b"bar", b"fasel"])
+ assert c.get_alpn_proto_negotiated() == b"bar"
+ assert c.rfile.readline().strip() == b"bar"
+
+ def test_no_alpn(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ assert c.get_alpn_proto_negotiated() == b""
+ assert c.rfile.readline().strip() == b"NONE"
+
+ else:
+ def test_none_alpn(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl(alpn_protos=[b"foo", b"bar", b"fasel"])
+ assert c.get_alpn_proto_negotiated() == b""
+ assert c.rfile.readline() == b"NONE"
+
+
+class TestNoSSLNoALPNClient(tservers.ServerTestBase):
+ handler = ALPNHandler
+
+ def test_no_ssl_no_alpn(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ assert c.get_alpn_proto_negotiated() == b""
+ assert c.rfile.readline().strip() == b"NONE"
+
+
+class TestSSLTimeOut(tservers.ServerTestBase):
+ handler = HangHandler
+ ssl = True
+
+ def test_timeout_client(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ c.settimeout(0.1)
+ tutils.raises(TcpTimeout, c.rfile.read, 10)
+
+
+class TestDHParams(tservers.ServerTestBase):
+ handler = HangHandler
+ ssl = dict(
+ dhparams=certutils.CertStore.load_dhparam(
+ tutils.test_data.path("data/dhparam.pem"),
+ ),
+ cipher_list="DHE-RSA-AES256-SHA"
+ )
+
+ def test_dhparams(self):
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ ret = c.get_current_cipher()
+ assert ret[0] == "DHE-RSA-AES256-SHA"
+
+ def test_create_dhparams(self):
+ with tutils.tmpdir() as d:
+ filename = os.path.join(d, "dhparam.pem")
+ certutils.CertStore.load_dhparam(filename)
+ assert os.path.exists(filename)
+
+
+class TestTCPClient:
+
+ def test_conerr(self):
+ c = tcp.TCPClient(("127.0.0.1", 0))
+ tutils.raises(TcpException, c.connect)
+
+
+class TestFileLike:
+
+ def test_blocksize(self):
+ s = BytesIO(b"1234567890abcdefghijklmnopqrstuvwxyz")
+ s = tcp.Reader(s)
+ s.BLOCKSIZE = 2
+ assert s.read(1) == b"1"
+ assert s.read(2) == b"23"
+ assert s.read(3) == b"456"
+ assert s.read(4) == b"7890"
+ d = s.read(-1)
+ assert d.startswith(b"abc") and d.endswith(b"xyz")
+
+ def test_wrap(self):
+ s = BytesIO(b"foobar\nfoobar")
+ s.flush()
+ s = tcp.Reader(s)
+ assert s.readline() == b"foobar\n"
+ assert s.readline() == b"foobar"
+ # Test __getattr__
+ assert s.isatty
+
+ def test_limit(self):
+ s = BytesIO(b"foobar\nfoobar")
+ s = tcp.Reader(s)
+ assert s.readline(3) == b"foo"
+
+ def test_limitless(self):
+ s = BytesIO(b"f" * (50 * 1024))
+ s = tcp.Reader(s)
+ ret = s.read(-1)
+ assert len(ret) == 50 * 1024
+
+ def test_readlog(self):
+ s = BytesIO(b"foobar\nfoobar")
+ s = tcp.Reader(s)
+ assert not s.is_logging()
+ s.start_log()
+ assert s.is_logging()
+ s.readline()
+ assert s.get_log() == b"foobar\n"
+ s.read(1)
+ assert s.get_log() == b"foobar\nf"
+ s.start_log()
+ assert s.get_log() == b""
+ s.read(1)
+ assert s.get_log() == b"o"
+ s.stop_log()
+ tutils.raises(ValueError, s.get_log)
+
+ def test_writelog(self):
+ s = BytesIO()
+ s = tcp.Writer(s)
+ s.start_log()
+ assert s.is_logging()
+ s.write(b"x")
+ assert s.get_log() == b"x"
+ s.write(b"x")
+ assert s.get_log() == b"xx"
+
+ def test_writer_flush_error(self):
+ s = BytesIO()
+ s = tcp.Writer(s)
+ o = mock.MagicMock()
+ o.flush = mock.MagicMock(side_effect=socket.error)
+ s.o = o
+ tutils.raises(TcpDisconnect, s.flush)
+
+ def test_reader_read_error(self):
+ s = BytesIO(b"foobar\nfoobar")
+ s = tcp.Reader(s)
+ o = mock.MagicMock()
+ o.read = mock.MagicMock(side_effect=socket.error)
+ s.o = o
+ tutils.raises(TcpDisconnect, s.read, 10)
+
+ def test_reset_timestamps(self):
+ s = BytesIO(b"foobar\nfoobar")
+ s = tcp.Reader(s)
+ s.first_byte_timestamp = 500
+ s.reset_timestamps()
+ assert not s.first_byte_timestamp
+
+ def test_first_byte_timestamp_updated_on_read(self):
+ s = BytesIO(b"foobar\nfoobar")
+ s = tcp.Reader(s)
+ s.read(1)
+ assert s.first_byte_timestamp
+ expected = s.first_byte_timestamp
+ s.read(5)
+ assert s.first_byte_timestamp == expected
+
+ def test_first_byte_timestamp_updated_on_readline(self):
+ s = BytesIO(b"foobar\nfoobar\nfoobar")
+ s = tcp.Reader(s)
+ s.readline()
+ assert s.first_byte_timestamp
+ expected = s.first_byte_timestamp
+ s.readline()
+ assert s.first_byte_timestamp == expected
+
+ def test_read_ssl_error(self):
+ s = mock.MagicMock()
+ s.read = mock.MagicMock(side_effect=SSL.Error())
+ s = tcp.Reader(s)
+ tutils.raises(TlsException, s.read, 1)
+
+ def test_read_syscall_ssl_error(self):
+ s = mock.MagicMock()
+ s.read = mock.MagicMock(side_effect=SSL.SysCallError())
+ s = tcp.Reader(s)
+ tutils.raises(TlsException, s.read, 1)
+
+ def test_reader_readline_disconnect(self):
+ o = mock.MagicMock()
+ o.read = mock.MagicMock(side_effect=socket.error)
+ s = tcp.Reader(o)
+ tutils.raises(TcpDisconnect, s.readline, 10)
+
+ def test_reader_incomplete_error(self):
+ s = BytesIO(b"foobar")
+ s = tcp.Reader(s)
+ tutils.raises(TcpReadIncomplete, s.safe_read, 10)
+
+
+class TestPeek(tservers.ServerTestBase):
+ handler = EchoHandler
+
+ def _connect(self, c):
+ c.connect()
+
+ def test_peek(self):
+ testval = b"peek!\n"
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ self._connect(c)
+ c.wfile.write(testval)
+ c.wfile.flush()
+
+ assert c.rfile.peek(4) == b"peek"
+ assert c.rfile.peek(6) == b"peek!\n"
+ assert c.rfile.readline() == testval
+
+ c.close()
+ with tutils.raises(NetlibException):
+ if c.rfile.peek(1) == b"":
+ # Workaround for Python 2 on Unix:
+ # Peeking a closed connection does not raise an exception here.
+ raise NetlibException()
+
+
+class TestPeekSSL(TestPeek):
+ ssl = True
+
+ def _connect(self, c):
+ c.connect()
+ c.convert_to_ssl()
+
+
+class TestAddress:
+
+ def test_simple(self):
+ a = tcp.Address("localhost", True)
+ assert a.use_ipv6
+ b = tcp.Address("foo.com", True)
+ assert not a == b
+ assert str(b) == str(tuple("foo.com"))
+ c = tcp.Address("localhost", True)
+ assert a == c
+ assert not a != c
+ assert repr(a)
+
+
+class TestSSLKeyLogger(tservers.ServerTestBase):
+ handler = EchoHandler
+ ssl = dict(
+ cipher_list="AES256-SHA"
+ )
+
+ def test_log(self):
+ testval = b"echo!\n"
+ _logfun = tcp.log_ssl_key
+
+ with tutils.tmpdir() as d:
+ logfile = os.path.join(d, "foo", "bar", "logfile")
+ tcp.log_ssl_key = tcp.SSLKeyLogger(logfile)
+
+ c = tcp.TCPClient(("127.0.0.1", self.port))
+ c.connect()
+ c.convert_to_ssl()
+ c.wfile.write(testval)
+ c.wfile.flush()
+ assert c.rfile.readline() == testval
+ c.finish()
+
+ tcp.log_ssl_key.close()
+ with open(logfile, "rb") as f:
+ assert f.read().count(b"CLIENT_RANDOM") == 2
+
+ tcp.log_ssl_key = _logfun
+
+ def test_create_logfun(self):
+ assert isinstance(
+ tcp.SSLKeyLogger.create_logfun("test"),
+ tcp.SSLKeyLogger)
+ assert not tcp.SSLKeyLogger.create_logfun(False)
diff --git a/test/netlib/test_utils.py b/test/netlib/test_utils.py
new file mode 100644
index 00000000..b096e5bc
--- /dev/null
+++ b/test/netlib/test_utils.py
@@ -0,0 +1,141 @@
+from netlib import utils, tutils
+from netlib.http import Headers
+
+def test_bidi():
+ b = utils.BiDi(a=1, b=2)
+ assert b.a == 1
+ assert b.get_name(1) == "a"
+ assert b.get_name(5) is None
+ tutils.raises(AttributeError, getattr, b, "c")
+ tutils.raises(ValueError, utils.BiDi, one=1, two=1)
+
+
+def test_hexdump():
+ assert list(utils.hexdump(b"one\0" * 10))
+
+
+def test_clean_bin():
+ assert utils.clean_bin(b"one") == b"one"
+ assert utils.clean_bin(b"\00ne") == b".ne"
+ assert utils.clean_bin(b"\nne") == b"\nne"
+ assert utils.clean_bin(b"\nne", False) == b".ne"
+ assert utils.clean_bin(u"\u2605".encode("utf8")) == b"..."
+
+ assert utils.clean_bin(u"one") == u"one"
+ assert utils.clean_bin(u"\00ne") == u".ne"
+ assert utils.clean_bin(u"\nne") == u"\nne"
+ assert utils.clean_bin(u"\nne", False) == u".ne"
+ assert utils.clean_bin(u"\u2605") == u"\u2605"
+
+
+def test_pretty_size():
+ assert utils.pretty_size(100) == "100B"
+ assert utils.pretty_size(1024) == "1kB"
+ assert utils.pretty_size(1024 + (1024 / 2.0)) == "1.5kB"
+ assert utils.pretty_size(1024 * 1024) == "1MB"
+
+
+def test_parse_url():
+ with tutils.raises(ValueError):
+ utils.parse_url("")
+
+ s, h, po, pa = utils.parse_url(b"http://foo.com:8888/test")
+ assert s == b"http"
+ assert h == b"foo.com"
+ assert po == 8888
+ assert pa == b"/test"
+
+ s, h, po, pa = utils.parse_url("http://foo/bar")
+ assert s == b"http"
+ assert h == b"foo"
+ assert po == 80
+ assert pa == b"/bar"
+
+ s, h, po, pa = utils.parse_url(b"http://user:pass@foo/bar")
+ assert s == b"http"
+ assert h == b"foo"
+ assert po == 80
+ assert pa == b"/bar"
+
+ s, h, po, pa = utils.parse_url(b"http://foo")
+ assert pa == b"/"
+
+ s, h, po, pa = utils.parse_url(b"https://foo")
+ assert po == 443
+
+ with tutils.raises(ValueError):
+ utils.parse_url(b"https://foo:bar")
+
+ # Invalid IDNA
+ with tutils.raises(ValueError):
+ utils.parse_url("http://\xfafoo")
+ # Invalid PATH
+ with tutils.raises(ValueError):
+ utils.parse_url("http:/\xc6/localhost:56121")
+ # Null byte in host
+ with tutils.raises(ValueError):
+ utils.parse_url("http://foo\0")
+ # Port out of range
+ _, _, port, _ = utils.parse_url("http://foo:999999")
+ assert port == 80
+ # Invalid IPv6 URL - see http://www.ietf.org/rfc/rfc2732.txt
+ with tutils.raises(ValueError):
+ utils.parse_url('http://lo[calhost')
+
+
+def test_unparse_url():
+ assert utils.unparse_url("http", "foo.com", 99, "") == "http://foo.com:99"
+ assert utils.unparse_url("http", "foo.com", 80, "/bar") == "http://foo.com/bar"
+ assert utils.unparse_url("https", "foo.com", 80, "") == "https://foo.com:80"
+ assert utils.unparse_url("https", "foo.com", 443, "") == "https://foo.com"
+
+
+def test_urlencode():
+ assert utils.urlencode([('foo', 'bar')])
+
+
+def test_urldecode():
+ s = "one=two&three=four"
+ assert len(utils.urldecode(s)) == 2
+
+
+def test_get_header_tokens():
+ headers = Headers()
+ assert utils.get_header_tokens(headers, "foo") == []
+ headers["foo"] = "bar"
+ assert utils.get_header_tokens(headers, "foo") == ["bar"]
+ headers["foo"] = "bar, voing"
+ assert utils.get_header_tokens(headers, "foo") == ["bar", "voing"]
+ headers.set_all("foo", ["bar, voing", "oink"])
+ assert utils.get_header_tokens(headers, "foo") == ["bar", "voing", "oink"]
+
+
+def test_multipartdecode():
+ boundary = 'somefancyboundary'
+ headers = Headers(
+ content_type='multipart/form-data; boundary=' + boundary
+ )
+ content = (
+ "--{0}\n"
+ "Content-Disposition: form-data; name=\"field1\"\n\n"
+ "value1\n"
+ "--{0}\n"
+ "Content-Disposition: form-data; name=\"field2\"\n\n"
+ "value2\n"
+ "--{0}--".format(boundary).encode()
+ )
+
+ form = utils.multipartdecode(headers, content)
+
+ assert len(form) == 2
+ assert form[0] == (b"field1", b"value1")
+ assert form[1] == (b"field2", b"value2")
+
+
+def test_parse_content_type():
+ p = utils.parse_content_type
+ assert p("text/html") == ("text", "html", {})
+ assert p("text") is None
+
+ v = p("text/html; charset=UTF-8")
+ assert v == ('text', 'html', {'charset': 'UTF-8'})
diff --git a/test/netlib/test_version_check.py b/test/netlib/test_version_check.py
new file mode 100644
index 00000000..ec2396fe
--- /dev/null
+++ b/test/netlib/test_version_check.py
@@ -0,0 +1,38 @@
+from io import StringIO
+import mock
+from netlib import version_check, version
+
+
+@mock.patch("sys.exit")
+def test_check_mitmproxy_version(sexit):
+ fp = StringIO()
+ version_check.check_mitmproxy_version(version.IVERSION, fp=fp)
+ assert not fp.getvalue()
+ assert not sexit.called
+
+ b = (version.IVERSION[0] - 1, version.IVERSION[1])
+ version_check.check_mitmproxy_version(b, fp=fp)
+ assert fp.getvalue()
+ assert sexit.called
+
+
+@mock.patch("sys.exit")
+def test_check_pyopenssl_version(sexit):
+ fp = StringIO()
+ version_check.check_pyopenssl_version(fp=fp)
+ assert not fp.getvalue()
+ assert not sexit.called
+
+ version_check.check_pyopenssl_version((9999,), fp=fp)
+ assert "outdated" in fp.getvalue()
+ assert sexit.called
+
+
+@mock.patch("sys.exit")
+@mock.patch("OpenSSL.__version__")
+def test_unparseable_pyopenssl_version(version, sexit):
+ version.split.return_value = ["foo", "bar"]
+ fp = StringIO()
+ version_check.check_pyopenssl_version(fp=fp)
+ assert "Cannot parse" in fp.getvalue()
+ assert not sexit.called
diff --git a/test/netlib/test_wsgi.py b/test/netlib/test_wsgi.py
new file mode 100644
index 00000000..8c782b27
--- /dev/null
+++ b/test/netlib/test_wsgi.py
@@ -0,0 +1,106 @@
+from io import BytesIO
+import sys
+from netlib import wsgi
+from netlib.http import Headers
+
+
+def tflow():
+ headers = Headers(test=b"value")
+ req = wsgi.Request("http", "GET", "/", "HTTP/1.1", headers, "")
+ return wsgi.Flow(("127.0.0.1", 8888), req)
+
+
+class ExampleApp:
+
+ def __init__(self):
+ self.called = False
+
+ def __call__(self, environ, start_response):
+ self.called = True
+ status = '200 OK'
+ response_headers = [('Content-type', 'text/plain')]
+ start_response(status, response_headers)
+ return [b'Hello', b' world!\n']
+
+
+class TestWSGI:
+
+ def test_make_environ(self):
+ w = wsgi.WSGIAdaptor(None, "foo", 80, "version")
+ tf = tflow()
+ assert w.make_environ(tf, None)
+
+ tf.request.path = "/foo?bar=voing"
+ r = w.make_environ(tf, None)
+ assert r["QUERY_STRING"] == "bar=voing"
+
+ def test_serve(self):
+ ta = ExampleApp()
+ w = wsgi.WSGIAdaptor(ta, "foo", 80, "version")
+ f = tflow()
+ f.request.host = "foo"
+ f.request.port = 80
+
+ wfile = BytesIO()
+ err = w.serve(f, wfile)
+ assert ta.called
+ assert not err
+
+ val = wfile.getvalue()
+ assert b"Hello world" in val
+ assert b"Server:" in val
+
+ def _serve(self, app):
+ w = wsgi.WSGIAdaptor(app, "foo", 80, "version")
+ f = tflow()
+ f.request.host = "foo"
+ f.request.port = 80
+ wfile = BytesIO()
+ w.serve(f, wfile)
+ return wfile.getvalue()
+
+ def test_serve_empty_body(self):
+ def app(environ, start_response):
+ status = '200 OK'
+ response_headers = [('Foo', 'bar')]
+ start_response(status, response_headers)
+ return []
+ assert self._serve(app)
+
+ def test_serve_double_start(self):
+ def app(environ, start_response):
+ try:
+ raise ValueError("foo")
+ except:
+ sys.exc_info()
+ status = '200 OK'
+ response_headers = [('Content-type', 'text/plain')]
+ start_response(status, response_headers)
+ start_response(status, response_headers)
+ assert b"Internal Server Error" in self._serve(app)
+
+ def test_serve_single_err(self):
+ def app(environ, start_response):
+ try:
+ raise ValueError("foo")
+ except:
+ ei = sys.exc_info()
+ status = '200 OK'
+ response_headers = [('Content-type', 'text/plain')]
+ start_response(status, response_headers, ei)
+ yield b""
+ assert b"Internal Server Error" in self._serve(app)
+
+ def test_serve_double_err(self):
+ def app(environ, start_response):
+ try:
+ raise ValueError("foo")
+ except:
+ ei = sys.exc_info()
+ status = '200 OK'
+ response_headers = [('Content-type', 'text/plain')]
+ start_response(status, response_headers)
+ yield b"aaa"
+ start_response(status, response_headers, ei)
+ yield b"bbb"
+ assert b"Internal Server Error" in self._serve(app)
diff --git a/test/netlib/tools/getcertnames b/test/netlib/tools/getcertnames
new file mode 100644
index 00000000..e33619f7
--- /dev/null
+++ b/test/netlib/tools/getcertnames
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+import sys
+sys.path.insert(0, "../../")
+from netlib import tcp
+
+
+def get_remote_cert(host, port, sni):
+ c = tcp.TCPClient((host, port))
+ c.connect()
+ c.convert_to_ssl(sni=sni)
+ return c.cert
+
+if len(sys.argv) > 2:
+ port = int(sys.argv[2])
+else:
+ port = 443
+if len(sys.argv) > 3:
+ sni = sys.argv[3]
+else:
+ sni = None
+
+cert = get_remote_cert(sys.argv[1], port, sni)
+print "CN:", cert.cn
+if cert.altnames:
+ print "SANs:",
+ for i in cert.altnames:
+ print "\t", i
diff --git a/test/netlib/websockets/__init__.py b/test/netlib/websockets/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/test/netlib/websockets/__init__.py
diff --git a/test/netlib/websockets/test_websockets.py b/test/netlib/websockets/test_websockets.py
new file mode 100644
index 00000000..d53f0d83
--- /dev/null
+++ b/test/netlib/websockets/test_websockets.py
@@ -0,0 +1,266 @@
+import os
+
+from netlib.http.http1 import read_response, read_request
+
+from netlib import tcp, websockets, http, tutils, tservers
+from netlib.http import status_codes
+from netlib.tutils import treq
+
+from netlib.exceptions import *
+
+
+class WebSocketsEchoHandler(tcp.BaseHandler):
+
+ def __init__(self, connection, address, server):
+ super(WebSocketsEchoHandler, self).__init__(
+ connection, address, server
+ )
+ self.protocol = websockets.WebsocketsProtocol()
+ self.handshake_done = False
+
+ def handle(self):
+ while True:
+ if not self.handshake_done:
+ self.handshake()
+ else:
+ self.read_next_message()
+
+ def read_next_message(self):
+ frame = websockets.Frame.from_file(self.rfile)
+ self.on_message(frame.payload)
+
+ def send_message(self, message):
+ frame = websockets.Frame.default(message, from_client=False)
+ frame.to_file(self.wfile)
+
+ def handshake(self):
+
+ req = read_request(self.rfile)
+ key = self.protocol.check_client_handshake(req.headers)
+
+ preamble = 'HTTP/1.1 101 %s' % status_codes.RESPONSES.get(101)
+ self.wfile.write(preamble.encode() + b"\r\n")
+ headers = self.protocol.server_handshake_headers(key)
+ self.wfile.write(str(headers) + "\r\n")
+ self.wfile.flush()
+ self.handshake_done = True
+
+ def on_message(self, message):
+ if message is not None:
+ self.send_message(message)
+
+
+class WebSocketsClient(tcp.TCPClient):
+
+ def __init__(self, address, source_address=None):
+ super(WebSocketsClient, self).__init__(address, source_address)
+ self.protocol = websockets.WebsocketsProtocol()
+ self.client_nonce = None
+
+ def connect(self):
+ super(WebSocketsClient, self).connect()
+
+ preamble = b'GET / HTTP/1.1'
+ self.wfile.write(preamble + b"\r\n")
+ headers = self.protocol.client_handshake_headers()
+ self.client_nonce = headers["sec-websocket-key"].encode("ascii")
+ self.wfile.write(bytes(headers) + b"\r\n")
+ self.wfile.flush()
+
+ resp = read_response(self.rfile, treq(method=b"GET"))
+ server_nonce = self.protocol.check_server_handshake(resp.headers)
+
+ if not server_nonce == self.protocol.create_server_nonce(self.client_nonce):
+ self.close()
+
+ def read_next_message(self):
+ return websockets.Frame.from_file(self.rfile).payload
+
+ def send_message(self, message):
+ frame = websockets.Frame.default(message, from_client=True)
+ frame.to_file(self.wfile)
+
+
+class TestWebSockets(tservers.ServerTestBase):
+ handler = WebSocketsEchoHandler
+
+ def __init__(self):
+ self.protocol = websockets.WebsocketsProtocol()
+
+ def random_bytes(self, n=100):
+ return os.urandom(n)
+
+ def echo(self, msg):
+ client = WebSocketsClient(("127.0.0.1", self.port))
+ client.connect()
+ client.send_message(msg)
+ response = client.read_next_message()
+ assert response == msg
+
+ def test_simple_echo(self):
+ self.echo(b"hello I'm the client")
+
+ def test_frame_sizes(self):
+ # length can fit in the the 7 bit payload length
+ small_msg = self.random_bytes(100)
+ # 50kb, sligthly larger than can fit in a 7 bit int
+ medium_msg = self.random_bytes(50000)
+ # 150kb, slightly larger than can fit in a 16 bit int
+ large_msg = self.random_bytes(150000)
+
+ self.echo(small_msg)
+ self.echo(medium_msg)
+ self.echo(large_msg)
+
+ def test_default_builder(self):
+ """
+ default builder should always generate valid frames
+ """
+ msg = self.random_bytes()
+ client_frame = websockets.Frame.default(msg, from_client=True)
+ server_frame = websockets.Frame.default(msg, from_client=False)
+
+ def test_serialization_bijection(self):
+ """
+ Ensure that various frame types can be serialized/deserialized back
+ and forth between to_bytes() and from_bytes()
+ """
+ for is_client in [True, False]:
+ for num_bytes in [100, 50000, 150000]:
+ frame = websockets.Frame.default(
+ self.random_bytes(num_bytes), is_client
+ )
+ frame2 = websockets.Frame.from_bytes(
+ frame.to_bytes()
+ )
+ assert frame == frame2
+
+ bytes = b'\x81\x03cba'
+ assert websockets.Frame.from_bytes(bytes).to_bytes() == bytes
+
+ def test_check_server_handshake(self):
+ headers = self.protocol.server_handshake_headers("key")
+ assert self.protocol.check_server_handshake(headers)
+ headers["Upgrade"] = "not_websocket"
+ assert not self.protocol.check_server_handshake(headers)
+
+ def test_check_client_handshake(self):
+ headers = self.protocol.client_handshake_headers("key")
+ assert self.protocol.check_client_handshake(headers) == "key"
+ headers["Upgrade"] = "not_websocket"
+ assert not self.protocol.check_client_handshake(headers)
+
+
+class BadHandshakeHandler(WebSocketsEchoHandler):
+
+ def handshake(self):
+
+ client_hs = read_request(self.rfile)
+ self.protocol.check_client_handshake(client_hs.headers)
+
+ preamble = 'HTTP/1.1 101 %s\r\n' % status_codes.RESPONSES.get(101)
+ self.wfile.write(preamble.encode())
+ headers = self.protocol.server_handshake_headers(b"malformed key")
+ self.wfile.write(bytes(headers) + b"\r\n")
+ self.wfile.flush()
+ self.handshake_done = True
+
+
+class TestBadHandshake(tservers.ServerTestBase):
+
+ """
+ Ensure that the client disconnects if the server handshake is malformed
+ """
+ handler = BadHandshakeHandler
+
+ def test(self):
+ with tutils.raises(TcpDisconnect):
+ client = WebSocketsClient(("127.0.0.1", self.port))
+ client.connect()
+ client.send_message(b"hello")
+
+
+class TestFrameHeader:
+
+ def test_roundtrip(self):
+ def round(*args, **kwargs):
+ f = websockets.FrameHeader(*args, **kwargs)
+ f2 = websockets.FrameHeader.from_file(tutils.treader(bytes(f)))
+ assert f == f2
+ round()
+ round(fin=1)
+ round(rsv1=1)
+ round(rsv2=1)
+ round(rsv3=1)
+ round(payload_length=1)
+ round(payload_length=100)
+ round(payload_length=1000)
+ round(payload_length=10000)
+ round(opcode=websockets.OPCODE.PING)
+ round(masking_key=b"test")
+
+ def test_human_readable(self):
+ f = websockets.FrameHeader(
+ masking_key=b"test",
+ fin=True,
+ payload_length=10
+ )
+ assert repr(f)
+ f = websockets.FrameHeader()
+ assert repr(f)
+
+ def test_funky(self):
+ f = websockets.FrameHeader(masking_key=b"test", mask=False)
+ raw = bytes(f)
+ f2 = websockets.FrameHeader.from_file(tutils.treader(raw))
+ assert not f2.mask
+
+ def test_violations(self):
+ tutils.raises("opcode", websockets.FrameHeader, opcode=17)
+ tutils.raises("masking key", websockets.FrameHeader, masking_key=b"x")
+
+ def test_automask(self):
+ f = websockets.FrameHeader(mask=True)
+ assert f.masking_key
+
+ f = websockets.FrameHeader(masking_key=b"foob")
+ assert f.mask
+
+ f = websockets.FrameHeader(masking_key=b"foob", mask=0)
+ assert not f.mask
+ assert f.masking_key
+
+
+class TestFrame:
+
+ def test_roundtrip(self):
+ def round(*args, **kwargs):
+ f = websockets.Frame(*args, **kwargs)
+ raw = bytes(f)
+ f2 = websockets.Frame.from_file(tutils.treader(raw))
+ assert f == f2
+ round(b"test")
+ round(b"test", fin=1)
+ round(b"test", rsv1=1)
+ round(b"test", opcode=websockets.OPCODE.PING)
+ round(b"test", masking_key=b"test")
+
+ def test_human_readable(self):
+ f = websockets.Frame()
+ assert repr(f)
+
+
+def test_masker():
+ tests = [
+ [b"a"],
+ [b"four"],
+ [b"fourf"],
+ [b"fourfive"],
+ [b"a", b"aasdfasdfa", b"asdf"],
+ [b"a" * 50, b"aasdfasdfa", b"asdf"],
+ ]
+ for i in tests:
+ m = websockets.Masker(b"abcd")
+ data = b"".join([m(t) for t in i])
+ data2 = websockets.Masker(b"abcd")(data)
+ assert data2 == b"".join(i)
diff --git a/test/pathod/data/clientcert/.gitignore b/test/pathod/data/clientcert/.gitignore
new file mode 100644
index 00000000..07bc53d2
--- /dev/null
+++ b/test/pathod/data/clientcert/.gitignore
@@ -0,0 +1,3 @@
+client.crt
+client.key
+client.req
diff --git a/test/pathod/data/clientcert/client.cnf b/test/pathod/data/clientcert/client.cnf
new file mode 100644
index 00000000..5046a944
--- /dev/null
+++ b/test/pathod/data/clientcert/client.cnf
@@ -0,0 +1,5 @@
+[ ssl_client ]
+basicConstraints = CA:FALSE
+nsCertType = client
+keyUsage = digitalSignature, keyEncipherment
+extendedKeyUsage = clientAuth
diff --git a/test/pathod/data/clientcert/client.pem b/test/pathod/data/clientcert/client.pem
new file mode 100644
index 00000000..4927bca2
--- /dev/null
+++ b/test/pathod/data/clientcert/client.pem
@@ -0,0 +1,42 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAzCpoRjSTfIN24kkNap/GYmP9zVWj0Gk8R5BB/PvvN0OB1Zk0
+EEYPsWCcuhEdK0ehiDZX030doF0DOncKKa6mop/d0x2o+ts42peDhZM6JNUrm6d+
+ZWQVtio33mpp77UMhR093vaA+ExDnmE26kBTVijJ1+fRAVDXG/cmQINEri91Kk/G
+3YJ5e45UrohGI5seBZ4vV0xbHtmczFRhYFlGOvYsoIe4Lvz/eFS2pIrTIpYQ2VM/
+SQQl+JFy+NlQRsWG2NrxtKOzMnnDE7YN4I3z5D5eZFo1EtwZ48LNCeSwrEOdfuzP
+G5q5qbs5KpE/x85H9umuRwSCIArbMwBYV8a8JwIDAQABAoIBAFE3FV/IDltbmHEP
+iky93hbJm+6QgKepFReKpRVTyqb7LaygUvueQyPWQMIriKTsy675nxo8DQr7tQsO
+y3YlSZgra/xNMikIB6e82c7K8DgyrDQw/rCqjZB3Xt4VCqsWJDLXnQMSn98lx0g7
+d7Lbf8soUpKWXqfdVpSDTi4fibSX6kshXyfSTpcz4AdoncEpViUfU1xkEEmZrjT8
+1GcCsDC41xdNmzCpqRuZX7DKSFRoB+0hUzsC1oiqM7FD5kixonRd4F5PbRXImIzt
+6YCsT2okxTA04jX7yByis7LlOLTlkmLtKQYuc3erOFvwx89s4vW+AeFei+GGNitn
+tHfSwbECgYEA7SzV+nN62hAERHlg8cEQT4TxnsWvbronYWcc/ev44eHSPDWL5tPi
+GHfSbW6YAq5Wa0I9jMWfXyhOYEC3MZTC5EEeLOB71qVrTwcy/sY66rOrcgjFI76Q
+5JFHQ4wy3SWU50KxE0oWJO9LIowprG+pW1vzqC3VF0T7q0FqESrY4LUCgYEA3F7Z
+80ndnCUlooJAb+Hfotv7peFf1o6+m1PTRcz1lLnVt5R5lXj86kn+tXEpYZo1RiGR
+2rE2N0seeznWCooakHcsBN7/qmFIhhooJNF7yW+JP2I4P2UV5+tJ+8bcs/voUkQD
+1x+rGOuMn8nvHBd2+Vharft8eGL2mgooPVI2XusCgYEAlMZpO3+w8pTVeHaDP2MR
+7i/AuQ3cbCLNjSX3Y7jgGCFllWspZRRIYXzYPNkA9b2SbBnTLjjRLgnEkFBIGgvs
+7O2EFjaCuDRvydUEQhjq4ErwIsopj7B8h0QyZcbOKTbn3uFQ3n68wVJx2Sv/ADHT
+FIHrp/WIE96r19Niy34LKXkCgYB2W59VsuOKnMz01l5DeR5C+0HSWxS9SReIl2IO
+yEFSKullWyJeLIgyUaGy0990430feKI8whcrZXYumuah7IDN/KOwzhCk8vEfzWao
+N7bzfqtJVrh9HA7C7DVlO+6H4JFrtcoWPZUIomJ549w/yz6EN3ckoMC+a/Ck1TW9
+ka1QFwKBgQCywG6TrZz0UmOjyLQZ+8Q4uvZklSW5NAKBkNnyuQ2kd5rzyYgMPE8C
+Er8T88fdVIKvkhDyHhwcI7n58xE5Gr7wkwsrk/Hbd9/ZB2GgAPY3cATskK1v1McU
+YeX38CU0fUS4aoy26hWQXkViB47IGQ3jWo3ZCtzIJl8DI9/RsBWTnw==
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIICYDCCAckCAQEwDQYJKoZIhvcNAQEFBQAwKDESMBAGA1UEAxMJbWl0bXByb3h5
+MRIwEAYDVQQKEwltaXRtcHJveHkwHhcNMTMwMTIwMDEwODEzWhcNMTUxMDE3MDEw
+ODEzWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UE
+ChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAzCpoRjSTfIN24kkNap/GYmP9zVWj0Gk8R5BB/PvvN0OB1Zk0
+EEYPsWCcuhEdK0ehiDZX030doF0DOncKKa6mop/d0x2o+ts42peDhZM6JNUrm6d+
+ZWQVtio33mpp77UMhR093vaA+ExDnmE26kBTVijJ1+fRAVDXG/cmQINEri91Kk/G
+3YJ5e45UrohGI5seBZ4vV0xbHtmczFRhYFlGOvYsoIe4Lvz/eFS2pIrTIpYQ2VM/
+SQQl+JFy+NlQRsWG2NrxtKOzMnnDE7YN4I3z5D5eZFo1EtwZ48LNCeSwrEOdfuzP
+G5q5qbs5KpE/x85H9umuRwSCIArbMwBYV8a8JwIDAQABMA0GCSqGSIb3DQEBBQUA
+A4GBAFvI+cd47B85PQ970n2dU/PlA2/Hb1ldrrXh2guR4hX6vYx/uuk5yRI/n0Rd
+KOXJ3czO0bd2Fpe3ZoNpkW0pOSDej/Q+58ScuJd0gWCT/Sh1eRk6ZdC0kusOuWoY
+bPOPMkG45LPgUMFOnZEsfJP6P5mZIxlbCvSMFC25nPHWlct7
+-----END CERTIFICATE-----
diff --git a/test/pathod/data/clientcert/make b/test/pathod/data/clientcert/make
new file mode 100644
index 00000000..d1caea81
--- /dev/null
+++ b/test/pathod/data/clientcert/make
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+openssl genrsa -out client.key 2048
+openssl req -key client.key -new -out client.req
+openssl x509 -req -days 365 -in client.req -signkey client.key -out client.crt -extfile client.cnf -extensions ssl_client
+openssl x509 -req -days 1000 -in client.req -CA ~/.mitmproxy/mitmproxy-ca.pem -CAkey ~/.mitmproxy/mitmproxy-ca.pem -set_serial 00001 -out client.crt -extensions ssl_client
+cat client.key client.crt > client.pem
+openssl x509 -text -noout -in client.pem
diff --git a/test/pathod/data/file b/test/pathod/data/file
new file mode 100644
index 00000000..26918572
--- /dev/null
+++ b/test/pathod/data/file
@@ -0,0 +1 @@
+testfile
diff --git a/test/pathod/data/request b/test/pathod/data/request
new file mode 100644
index 00000000..c4c90e76
--- /dev/null
+++ b/test/pathod/data/request
@@ -0,0 +1 @@
+get:/foo
diff --git a/test/pathod/data/response b/test/pathod/data/response
new file mode 100644
index 00000000..8f897c85
--- /dev/null
+++ b/test/pathod/data/response
@@ -0,0 +1 @@
+202
diff --git a/test/pathod/data/testkey.pem b/test/pathod/data/testkey.pem
new file mode 100644
index 00000000..b804bd4c
--- /dev/null
+++ b/test/pathod/data/testkey.pem
@@ -0,0 +1,68 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIG5QIBAAKCAYEAwvtKxoZvBV2AxPAkCx8PXbuE7KeqK9bBvk8x+JchPMdf/KZj
+sdu2v6Gm8Hi053i7ZGxouFvonJxHAiK6cwk9OYQwa9fbOFf2mgWKEBO4fbCH93tW
+DCTdWVxFyNViAvxGHlJs3/IU03pIG29AgUnhRW8pGbabAfx8emcOZJZ3ykEuimaC
+4s7mRwdc63GXnbcjTtRkrJsBATI+xvPwuR2+4daX7sPCf0kel3bN2jMpwXfvk/Ww
+kJ2BIEeZCg0qIvyMjH9qrUirUnsmQnpPln0CGBbQEBsW9yMfGoFdREiMYls5jZeq
+NxjWNv1RTRIm/4RjMwyxnoTA9eDS9wwO2NnJS4vfXAnUTP4BYx8Pe4ZMA2Gm6YrC
+ysT6YA1xdHNpcuHXClxwmPj/cm8Z5kIg5clbNIK60ts9yFr/Ao3KPPYJ2GBv8/Oe
+ApPBJuubews+/9/13Ew/SJ1t2u28+sPbgXUG8dC2n4vWTvJwKf6Duqxgnm82zdzj
+SZoXRQsP984qiN7NAgMBAAECggGBALB6rqWdzCL5DLI0AQun40qdjaR95UKksNvF
+5p7we379nl2ZZKb5DSHJ+MWzG1pfJo2wqeAkIBiQQp0mPcgdVrMWeJVD3QHUbDng
+RaRjlRr+izJvCeUYANj+8ZLjwECfgf+z7yOLg1oeVeGvAp2C90jXYkYJx6c2lpxb
+ZuWYY3hHIw7V1iXfywIDIhFg0TBJMMYK68xmx7QDfFqrNPj4eWsDxqSvvv1iezPw
+rkWPBX49RjWPrW5XgSZsZ5J3c+oS1rZmIY7EAgopTWB/3wJjZR1Idz/9l9LIWlBP
+6zVC27CIZzSEeGguqNVeyzJ0TPWh5idYNRmSZr6eTUF0245LNO/gqvWKgRSNIZko
+HoBa2F1AvCiB67S1kxjwS5y3VkudZE4jkgGKcC2Ws/9QmOZ0HAsjI8VAMp2hj6iN
+0HdPMTNtsLgbhKyXsoZuW4YmwfSTPxGi2gvcI7GUozpTz84n1cOneJnz1ygx6Uru
+v8DpQg+VX6xTy4X6AK1F8OYNMZ/jaQKBwQDv30NevQStnGbTmcSr+0fd4rmWFklK
+V6B2X7zWynVpSGvCWkqVSp3mG6aiZItAltVMRL/9LT6zIheyClyd+vXIjR2+W210
+XMxrvz7A8qCXkvB2dyEhrMdCfZ7p8+kf+eD2c/Mnxb7VpmDfHYLx30JeQoBwjrwU
+Eul+dE1P+r8bWBaLTjlsipTya74yItWWAToXAo+s1BXBtXhEsLoe4FghlC0u724d
+ucjDaeICdLcerApdvg6Q6p4kVHaoF6ka6I8CgcEA0Bdc05ery9gLC6CclV+BhA5Q
+dfDq2P7qhc7e1ipwNRrQo2gy5HhgOkTL3dJWc+8rV6CBP/JfchnsW40tDOnPCTLT
+gg3n7vv3RHrtncApXuhIFR+B5xjohTPBzxRUMiAOre2d0F5b6eBXFjptf/1i2tQ+
+qdqJoyOGOZP0hKVslGIfz+CKc6WEkIqX7c91Msdr5myeaWDI5TsurfuKRBH395T3
+BMAi6oinAAEb1rdySenLO2A/0kVmBVlTpaN3TNjjAoHBAMvS4uQ1qSv8okNbfgrF
+UqPwa9JkzZImM2tinovFLU9xAl/7aTTCWrmU9Vs4JDuV71kHcjwnngeJCKl4tIpp
+HUB06Lk/5xnhYLKNpz087cjeSwXe5IBA2HBfXhFd+NH6+nVwwUUieq4A2n+8C/CK
+zVJbH9iE8Lv99fpFyQwU/R63EzD8Hz9j4ny7oLnpb6QvFrVGr98jt/kJwlBb+0sR
+RtIBnwMq4F7R5w5lgm6jzpZ5ibVuMeJh+k7Ulp7uu/rpcQKBwQDE3sWIvf7f7PaO
+OpbJz0CmYjCHVLWrNIlGrPAv6Jid9U+cuXEkrCpGFl5V77CxIH59+bEuga0BMztl
+ZkxP4khoqHhom6VpeWJ3nGGAFJRPYS0JJvTsYalilBPxSYdaoO+iZ6MdxpfozcE2
+m3KLW3uSEqlyYvpCqNJNWQhGEoeGXstADWyPevHPGgAhElwL/ZW8u9inU9Tc4sAI
+BGnMer+BsaJ+ERU3lK+Clony+z2aZiFLfIUE93lM6DT2CZBN2QcCgcAVk4L0bfA6
+HFnP/ZWNlnYWpOVFKcq57PX+J5/k7Tf34e2cYM2P0eqYggWZbzVd8qoCOQCHrAx0
+aZSSvEyKAVvzRNeqbm1oXaMojksMnrSX5henHjPbZlr1EmM7+zMnSTMkfVOx/6g1
+97sASej31XdOAgKCBJGymrwvYrCLW+P5cHqd+D8v/PvfpRIQM54p5ixRt3EYZvtR
+zGrzsr0OGyOLZtj1DB0a3kvajAAOCl3TawJSzviKo2mwc+/xj28MCQM=
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIE4TCCA0mgAwIBAgIJALONCAWZxPhUMA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV
+BAYTAk5aMQ4wDAYDVQQIDAVPdGFnbzEPMA0GA1UECgwGUGF0aG9kMREwDwYDVQQD
+DAh0ZXN0LmNvbTAeFw0xNTA0MTgyMjA0NTNaFw00MjA5MDIyMjA0NTNaMEExCzAJ
+BgNVBAYTAk5aMQ4wDAYDVQQIDAVPdGFnbzEPMA0GA1UECgwGUGF0aG9kMREwDwYD
+VQQDDAh0ZXN0LmNvbTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAML7
+SsaGbwVdgMTwJAsfD127hOynqivWwb5PMfiXITzHX/ymY7Hbtr+hpvB4tOd4u2Rs
+aLhb6JycRwIiunMJPTmEMGvX2zhX9poFihATuH2wh/d7Vgwk3VlcRcjVYgL8Rh5S
+bN/yFNN6SBtvQIFJ4UVvKRm2mwH8fHpnDmSWd8pBLopmguLO5kcHXOtxl523I07U
+ZKybAQEyPsbz8LkdvuHWl+7Dwn9JHpd2zdozKcF375P1sJCdgSBHmQoNKiL8jIx/
+aq1Iq1J7JkJ6T5Z9AhgW0BAbFvcjHxqBXURIjGJbOY2XqjcY1jb9UU0SJv+EYzMM
+sZ6EwPXg0vcMDtjZyUuL31wJ1Ez+AWMfD3uGTANhpumKwsrE+mANcXRzaXLh1wpc
+cJj4/3JvGeZCIOXJWzSCutLbPcha/wKNyjz2Cdhgb/PzngKTwSbrm3sLPv/f9dxM
+P0idbdrtvPrD24F1BvHQtp+L1k7ycCn+g7qsYJ5vNs3c40maF0ULD/fOKojezQID
+AQABo4HbMIHYMAsGA1UdDwQEAwIFoDAdBgNVHQ4EFgQUbEgfTauEqEP/bnBtby1K
+bihJvcswcQYDVR0jBGowaIAUbEgfTauEqEP/bnBtby1KbihJvcuhRaRDMEExCzAJ
+BgNVBAYTAk5aMQ4wDAYDVQQIDAVPdGFnbzEPMA0GA1UECgwGUGF0aG9kMREwDwYD
+VQQDDAh0ZXN0LmNvbYIJALONCAWZxPhUMAwGA1UdEwQFMAMBAf8wKQYDVR0RBCIw
+IIIIdGVzdC5jb22CCXRlc3QyLmNvbYIJdGVzdDMuY29tMA0GCSqGSIb3DQEBCwUA
+A4IBgQBcTedXtUb91DxQRtg73iomz7cQ4niZntUBW8iE5rpoA7prtQNGHMCbHwaX
+tbWFkzBmL5JTBWvd/6AQ2LtiB3rYB3W/iRhbpsNJ501xaoOguPEQ9720Ph8TEveM
+208gNzGsEOcNALwyXj2y9M19NGu9zMa8eu1Tc3IsQaVaGKHx8XZn5HTNUx8EdcwI
+Z/Ji9ETDCL7+e5INv0tqfFSazWaQUwxM4IzPMkKTYRcMuN/6eog609k9r9pp32Ut
+rKlzc6GIkAlgJJ0Wkoz1V46DmJNJdJG7eLu/mtsB85j6hytIQeWTf1fll5YnMZLF
+HgNZtfYn8Q0oTdBQ0ZOaZeQCfZ8emYBdLJf2YB83uGRMjQ1FoeIxzQqiRq8WHRdb
+9Q45i0DINMnNp0DbLMA4numZ7wT9SQb6sql9eUyuCNDw7nGIWTHUNfLtU1Er3h1d
+icJuApx9+//UN/pGh0yTXb3fZbiI4IehRmkpnIWonIAwaVGm6JZU04wiIn8CuBho
+/qQdlS8=
+-----END CERTIFICATE-----
diff --git a/test/pathod/scripts/generate.sh b/test/pathod/scripts/generate.sh
new file mode 100644
index 00000000..eec3077d
--- /dev/null
+++ b/test/pathod/scripts/generate.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+if [ ! -f ./private.key ]
+then
+ openssl genrsa -out private.key 3072
+fi
+openssl req \
+ -batch \
+ -new -x509 \
+ -key private.key \
+ -sha256 \
+ -out cert.pem \
+ -days 9999 \
+ -config ./openssl.cnf
+openssl x509 -in cert.pem -text -noout
+cat ./private.key ./cert.pem > testcert.pem
+rm ./private.key ./cert.pem
diff --git a/test/pathod/scripts/openssl.cnf b/test/pathod/scripts/openssl.cnf
new file mode 100644
index 00000000..5c890354
--- /dev/null
+++ b/test/pathod/scripts/openssl.cnf
@@ -0,0 +1,39 @@
+[ req ]
+default_bits = 1024
+default_keyfile = privkey.pem
+distinguished_name = req_distinguished_name
+x509_extensions = v3_ca
+
+[ req_distinguished_name ]
+countryName = Country Name (2 letter code)
+countryName_default = NZ
+countryName_min = 2
+countryName_max = 2
+stateOrProvinceName = State or Province Name (full name)
+stateOrProvinceName_default = Otago
+localityName = Locality Name (eg, city)
+0.organizationName = Organization Name (eg, company)
+0.organizationName_default = Pathod
+commonName = Common Name (e.g. server FQDN or YOUR name)
+commonName_default = test.com
+commonName_max = 64
+
+[ v3_req ]
+
+basicConstraints = CA:FALSE
+keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+
+[ v3_ca ]
+
+keyUsage = digitalSignature, keyEncipherment
+subjectKeyIdentifier=hash
+authorityKeyIdentifier=keyid:always,issuer:always
+basicConstraints = CA:true
+subjectAltName = @alternate_names
+
+
+[ alternate_names ]
+
+DNS.1 = test.com
+DNS.2 = test2.com
+DNS.3 = test3.com
diff --git a/test/pathod/test_app.py b/test/pathod/test_app.py
new file mode 100644
index 00000000..4536db8e
--- /dev/null
+++ b/test/pathod/test_app.py
@@ -0,0 +1,85 @@
+import tutils
+
+
+class TestApp(tutils.DaemonTests):
+ SSL = False
+
+ def test_index(self):
+ r = self.getpath("/")
+ assert r.status_code == 200
+ assert r.content
+
+ def test_about(self):
+ r = self.getpath("/about")
+ assert r.ok
+
+ def test_download(self):
+ r = self.getpath("/download")
+ assert r.ok
+
+ def test_docs(self):
+ assert self.getpath("/docs/pathod").status_code == 200
+ assert self.getpath("/docs/pathoc").status_code == 200
+ assert self.getpath("/docs/language").status_code == 200
+ assert self.getpath("/docs/libpathod").status_code == 200
+ assert self.getpath("/docs/test").status_code == 200
+
+ def test_log(self):
+ assert self.getpath("/log").status_code == 200
+ assert self.get("200:da").status_code == 200
+ id = self.d.log()[0]["id"]
+ assert self.getpath("/log").status_code == 200
+ assert self.getpath("/log/%s" % id).status_code == 200
+ assert self.getpath("/log/9999999").status_code == 404
+
+ def test_log_binary(self):
+ assert self.get("200:h@10b=@10b:da")
+
+ def test_response_preview(self):
+ r = self.getpath("/response_preview", params=dict(spec="200"))
+ assert r.status_code == 200
+ assert 'Response' in r.content
+
+ r = self.getpath("/response_preview", params=dict(spec="foo"))
+ assert r.status_code == 200
+ assert 'Error' in r.content
+
+ r = self.getpath("/response_preview", params=dict(spec="200:b@100m"))
+ assert r.status_code == 200
+ assert "too large" in r.content
+
+ r = self.getpath("/response_preview", params=dict(spec="200:b@5k"))
+ assert r.status_code == 200
+ assert 'Response' in r.content
+
+ r = self.getpath(
+ "/response_preview",
+ params=dict(
+ spec="200:b<nonexistent"))
+ assert r.status_code == 200
+ assert 'File access denied' in r.content
+
+ r = self.getpath("/response_preview", params=dict(spec="200:b<file"))
+ assert r.status_code == 200
+ assert 'testfile' in r.content
+
+ def test_request_preview(self):
+ r = self.getpath("/request_preview", params=dict(spec="get:/"))
+ assert r.status_code == 200
+ assert 'Request' in r.content
+
+ r = self.getpath("/request_preview", params=dict(spec="foo"))
+ assert r.status_code == 200
+ assert 'Error' in r.content
+
+ r = self.getpath("/request_preview", params=dict(spec="get:/:b@100m"))
+ assert r.status_code == 200
+ assert "too large" in r.content
+
+ r = self.getpath("/request_preview", params=dict(spec="get:/:b@5k"))
+ assert r.status_code == 200
+ assert 'Request' in r.content
+
+ r = self.getpath("/request_preview", params=dict(spec=""))
+ assert r.status_code == 200
+ assert 'empty spec' in r.content
diff --git a/test/pathod/test_language_actions.py b/test/pathod/test_language_actions.py
new file mode 100644
index 00000000..755f0d85
--- /dev/null
+++ b/test/pathod/test_language_actions.py
@@ -0,0 +1,135 @@
+import cStringIO
+
+from libpathod.language import actions
+from libpathod import language
+
+
+def parse_request(s):
+ return language.parse_pathoc(s).next()
+
+
+def test_unique_name():
+ assert not actions.PauseAt(0, "f").unique_name
+ assert actions.DisconnectAt(0).unique_name
+
+
+class TestDisconnects:
+
+ def test_parse_pathod(self):
+ a = language.parse_pathod("400:d0").next().actions[0]
+ assert a.spec() == "d0"
+ a = language.parse_pathod("400:dr").next().actions[0]
+ assert a.spec() == "dr"
+
+ def test_at(self):
+ e = actions.DisconnectAt.expr()
+ v = e.parseString("d0")[0]
+ assert isinstance(v, actions.DisconnectAt)
+ assert v.offset == 0
+
+ v = e.parseString("d100")[0]
+ assert v.offset == 100
+
+ e = actions.DisconnectAt.expr()
+ v = e.parseString("dr")[0]
+ assert v.offset == "r"
+
+ def test_spec(self):
+ assert actions.DisconnectAt("r").spec() == "dr"
+ assert actions.DisconnectAt(10).spec() == "d10"
+
+
+class TestInject:
+
+ def test_parse_pathod(self):
+ a = language.parse_pathod("400:ir,@100").next().actions[0]
+ assert a.offset == "r"
+ assert a.value.datatype == "bytes"
+ assert a.value.usize == 100
+
+ a = language.parse_pathod("400:ia,@100").next().actions[0]
+ assert a.offset == "a"
+
+ def test_at(self):
+ e = actions.InjectAt.expr()
+ v = e.parseString("i0,'foo'")[0]
+ assert v.value.val == "foo"
+ assert v.offset == 0
+ assert isinstance(v, actions.InjectAt)
+
+ v = e.parseString("ir,'foo'")[0]
+ assert v.offset == "r"
+
+ def test_serve(self):
+ s = cStringIO.StringIO()
+ r = language.parse_pathod("400:i0,'foo'").next()
+ assert language.serve(r, s, {})
+
+ def test_spec(self):
+ e = actions.InjectAt.expr()
+ v = e.parseString("i0,'foo'")[0]
+ assert v.spec() == 'i0,"foo"'
+
+ def test_spec(self):
+ e = actions.InjectAt.expr()
+ v = e.parseString("i0,@100")[0]
+ v2 = v.freeze({})
+ v3 = v2.freeze({})
+ assert v2.value.val == v3.value.val
+
+
+class TestPauses:
+
+ def test_parse_pathod(self):
+ e = actions.PauseAt.expr()
+ v = e.parseString("p10,10")[0]
+ assert v.seconds == 10
+ assert v.offset == 10
+
+ v = e.parseString("p10,f")[0]
+ assert v.seconds == "f"
+
+ v = e.parseString("pr,f")[0]
+ assert v.offset == "r"
+
+ v = e.parseString("pa,f")[0]
+ assert v.offset == "a"
+
+ def test_request(self):
+ r = language.parse_pathod('400:p10,10').next()
+ assert r.actions[0].spec() == "p10,10"
+
+ def test_spec(self):
+ assert actions.PauseAt("r", 5).spec() == "pr,5"
+ assert actions.PauseAt(0, 5).spec() == "p0,5"
+ assert actions.PauseAt(0, "f").spec() == "p0,f"
+
+ def test_freeze(self):
+ l = actions.PauseAt("r", 5)
+ assert l.freeze({}).spec() == l.spec()
+
+
+class Test_Action:
+
+ def test_cmp(self):
+ a = actions.DisconnectAt(0)
+ b = actions.DisconnectAt(1)
+ c = actions.DisconnectAt(0)
+ assert a < b
+ assert a == c
+ l = sorted([b, a])
+ assert l[0].offset == 0
+
+ def test_resolve(self):
+ r = parse_request('GET:"/foo"')
+ e = actions.DisconnectAt("r")
+ ret = e.resolve({}, r)
+ assert isinstance(ret.offset, int)
+
+ def test_repr(self):
+ e = actions.DisconnectAt("r")
+ assert repr(e)
+
+ def test_freeze(self):
+ l = actions.DisconnectAt(5)
+ assert l.freeze({}).spec() == l.spec()
diff --git a/test/pathod/test_language_base.py b/test/pathod/test_language_base.py
new file mode 100644
index 00000000..b18ee5b2
--- /dev/null
+++ b/test/pathod/test_language_base.py
@@ -0,0 +1,352 @@
+import os
+from libpathod import language
+from libpathod.language import base, exceptions
+import tutils
+
+
+def parse_request(s):
+ return language.parse_pathoc(s).next()
+
+
+def test_times():
+ reqs = list(language.parse_pathoc("get:/:x5"))
+ assert len(reqs) == 5
+ assert not reqs[0].times
+
+
+def test_caseless_literal():
+ class CL(base.CaselessLiteral):
+ TOK = "foo"
+ v = CL("foo")
+ assert v.expr()
+ assert v.values(language.Settings())
+
+
+class TestTokValueNakedLiteral:
+
+ def test_expr(self):
+ v = base.TokValueNakedLiteral("foo")
+ assert v.expr()
+
+ def test_spec(self):
+ v = base.TokValueNakedLiteral("foo")
+ assert v.spec() == repr(v) == "foo"
+
+ v = base.TokValueNakedLiteral("f\x00oo")
+ assert v.spec() == repr(v) == r"f\x00oo"
+
+
+class TestTokValueLiteral:
+
+ def test_espr(self):
+ v = base.TokValueLiteral("foo")
+ assert v.expr()
+ assert v.val == "foo"
+
+ v = base.TokValueLiteral("foo\n")
+ assert v.expr()
+ assert v.val == "foo\n"
+ assert repr(v)
+
+ def test_spec(self):
+ v = base.TokValueLiteral("foo")
+ assert v.spec() == r"'foo'"
+
+ v = base.TokValueLiteral("f\x00oo")
+ assert v.spec() == repr(v) == r"'f\x00oo'"
+
+ v = base.TokValueLiteral("\"")
+ assert v.spec() == repr(v) == '\'"\''
+
+ def roundtrip(self, spec):
+ e = base.TokValueLiteral.expr()
+ v = base.TokValueLiteral(spec)
+ v2 = e.parseString(v.spec())
+ assert v.val == v2[0].val
+ assert v.spec() == v2[0].spec()
+
+ def test_roundtrip(self):
+ self.roundtrip("'")
+ self.roundtrip('\'')
+ self.roundtrip("a")
+ self.roundtrip("\"")
+ # self.roundtrip("\\")
+ self.roundtrip("200:b'foo':i23,'\\''")
+ self.roundtrip("\a")
+
+
+class TestTokValueGenerate:
+
+ def test_basic(self):
+ v = base.TokValue.parseString("@10b")[0]
+ assert v.usize == 10
+ assert v.unit == "b"
+ assert v.bytes() == 10
+ v = base.TokValue.parseString("@10")[0]
+ assert v.unit == "b"
+ v = base.TokValue.parseString("@10k")[0]
+ assert v.bytes() == 10240
+ v = base.TokValue.parseString("@10g")[0]
+ assert v.bytes() == 1024 ** 3 * 10
+
+ v = base.TokValue.parseString("@10g,digits")[0]
+ assert v.datatype == "digits"
+ g = v.get_generator({})
+ assert g[:100]
+
+ v = base.TokValue.parseString("@10,digits")[0]
+ assert v.unit == "b"
+ assert v.datatype == "digits"
+
+ def test_spec(self):
+ v = base.TokValueGenerate(1, "b", "bytes")
+ assert v.spec() == repr(v) == "@1"
+
+ v = base.TokValueGenerate(1, "k", "bytes")
+ assert v.spec() == repr(v) == "@1k"
+
+ v = base.TokValueGenerate(1, "k", "ascii")
+ assert v.spec() == repr(v) == "@1k,ascii"
+
+ v = base.TokValueGenerate(1, "b", "ascii")
+ assert v.spec() == repr(v) == "@1,ascii"
+
+ def test_freeze(self):
+ v = base.TokValueGenerate(100, "b", "ascii")
+ f = v.freeze(language.Settings())
+ assert len(f.val) == 100
+
+
+class TestTokValueFile:
+
+ def test_file_value(self):
+ v = base.TokValue.parseString("<'one two'")[0]
+ assert str(v)
+ assert v.path == "one two"
+
+ v = base.TokValue.parseString("<path")[0]
+ assert v.path == "path"
+
+ def test_access_control(self):
+ v = base.TokValue.parseString("<path")[0]
+ with tutils.tmpdir() as t:
+ p = os.path.join(t, "path")
+ with open(p, "wb") as f:
+ f.write("x" * 10000)
+
+ assert v.get_generator(language.Settings(staticdir=t))
+
+ v = base.TokValue.parseString("<path2")[0]
+ tutils.raises(
+ exceptions.FileAccessDenied,
+ v.get_generator,
+ language.Settings(staticdir=t)
+ )
+ tutils.raises(
+ "access disabled",
+ v.get_generator,
+ language.Settings()
+ )
+
+ v = base.TokValue.parseString("</outside")[0]
+ tutils.raises(
+ "outside",
+ v.get_generator,
+ language.Settings(staticdir=t)
+ )
+
+ def test_spec(self):
+ v = base.TokValue.parseString("<'one two'")[0]
+ v2 = base.TokValue.parseString(v.spec())[0]
+ assert v2.path == "one two"
+
+ def test_freeze(self):
+ v = base.TokValue.parseString("<'one two'")[0]
+ v2 = v.freeze({})
+ assert v2.path == v.path
+
+
+class TestMisc:
+
+ def test_generators(self):
+ v = base.TokValue.parseString("'val'")[0]
+ g = v.get_generator({})
+ assert g[:] == "val"
+
+ def test_value(self):
+ assert base.TokValue.parseString("'val'")[0].val == "val"
+ assert base.TokValue.parseString('"val"')[0].val == "val"
+ assert base.TokValue.parseString('"\'val\'"')[0].val == "'val'"
+
+ def test_value(self):
+ class TT(base.Value):
+ preamble = "m"
+ e = TT.expr()
+ v = e.parseString("m'msg'")[0]
+ assert v.value.val == "msg"
+
+ s = v.spec()
+ assert s == e.parseString(s)[0].spec()
+
+ v = e.parseString("m@100")[0]
+ v2 = v.freeze({})
+ v3 = v2.freeze({})
+ assert v2.value.val == v3.value.val
+
+ def test_fixedlengthvalue(self):
+ class TT(base.FixedLengthValue):
+ preamble = "m"
+ length = 4
+
+ e = TT.expr()
+ assert e.parseString("m@4")
+ tutils.raises("invalid value length", e.parseString, "m@100")
+ tutils.raises("invalid value length", e.parseString, "m@1")
+
+ with tutils.tmpdir() as t:
+ p = os.path.join(t, "path")
+ s = base.Settings(staticdir=t)
+ with open(p, "wb") as f:
+ f.write("a" * 20)
+ v = e.parseString("m<path")[0]
+ tutils.raises("invalid value length", v.values, s)
+
+ p = os.path.join(t, "path")
+ with open(p, "wb") as f:
+ f.write("a" * 4)
+ v = e.parseString("m<path")[0]
+ assert v.values(s)
+
+
+class TKeyValue(base.KeyValue):
+ preamble = "h"
+
+ def values(self, settings):
+ return [
+ self.key.get_generator(settings),
+ ": ",
+ self.value.get_generator(settings),
+ "\r\n",
+ ]
+
+
+class TestKeyValue:
+
+ def test_simple(self):
+ e = TKeyValue.expr()
+ v = e.parseString("h'foo'='bar'")[0]
+ assert v.key.val == "foo"
+ assert v.value.val == "bar"
+
+ v2 = e.parseString(v.spec())[0]
+ assert v2.key.val == v.key.val
+ assert v2.value.val == v.value.val
+
+ s = v.spec()
+ assert s == e.parseString(s)[0].spec()
+
+ def test_freeze(self):
+ e = TKeyValue.expr()
+ v = e.parseString("h@10=@10'")[0]
+ v2 = v.freeze({})
+ v3 = v2.freeze({})
+ assert v2.key.val == v3.key.val
+ assert v2.value.val == v3.value.val
+
+
+def test_intfield():
+ class TT(base.IntField):
+ preamble = "t"
+ names = {
+ "one": 1,
+ "two": 2,
+ "three": 3
+ }
+ max = 4
+ e = TT.expr()
+
+ v = e.parseString("tone")[0]
+ assert v.value == 1
+ assert v.spec() == "tone"
+ assert v.values(language.Settings())
+
+ v = e.parseString("t1")[0]
+ assert v.value == 1
+ assert v.spec() == "t1"
+
+ v = e.parseString("t4")[0]
+ assert v.value == 4
+ assert v.spec() == "t4"
+
+ tutils.raises("can't exceed", e.parseString, "t5")
+
+
+def test_options_or_value():
+ class TT(base.OptionsOrValue):
+ options = [
+ "one",
+ "two",
+ "three"
+ ]
+ e = TT.expr()
+ assert e.parseString("one")[0].value.val == "one"
+ assert e.parseString("'foo'")[0].value.val == "foo"
+ assert e.parseString("'get'")[0].value.val == "get"
+
+ assert e.parseString("one")[0].spec() == "one"
+ assert e.parseString("'foo'")[0].spec() == "'foo'"
+
+ s = e.parseString("one")[0].spec()
+ assert s == e.parseString(s)[0].spec()
+
+ s = e.parseString("'foo'")[0].spec()
+ assert s == e.parseString(s)[0].spec()
+
+ v = e.parseString("@100")[0]
+ v2 = v.freeze({})
+ v3 = v2.freeze({})
+ assert v2.value.val == v3.value.val
+
+
+def test_integer():
+ e = base.Integer.expr()
+ v = e.parseString("200")[0]
+ assert v.string() == "200"
+ assert v.spec() == "200"
+
+ assert v.freeze({}).value == v.value
+
+ class BInt(base.Integer):
+ bounds = (1, 5)
+
+ tutils.raises("must be between", BInt, 0)
+ tutils.raises("must be between", BInt, 6)
+ assert BInt(5)
+ assert BInt(1)
+ assert BInt(3)
+
+
+class TBoolean(base.Boolean):
+ name = "test"
+
+
+def test_unique_name():
+ b = TBoolean(True)
+ assert b.unique_name
+
+
+class test_boolean():
+ e = TBoolean.expr()
+ assert e.parseString("test")[0].value
+ assert not e.parseString("-test")[0].value
+
+ def roundtrip(s):
+ e = TBoolean.expr()
+ s2 = e.parseString(s)[0].spec()
+ v1 = e.parseString(s)[0].value
+ v2 = e.parseString(s2)[0].value
+ assert s == s2
+ assert v1 == v2
+
+ roundtrip("test")
+ roundtrip("-test")
diff --git a/test/pathod/test_language_generators.py b/test/pathod/test_language_generators.py
new file mode 100644
index 00000000..945560c3
--- /dev/null
+++ b/test/pathod/test_language_generators.py
@@ -0,0 +1,42 @@
+import os
+
+from libpathod.language import generators
+import tutils
+
+
+def test_randomgenerator():
+ g = generators.RandomGenerator("bytes", 100)
+ assert repr(g)
+ assert len(g[:10]) == 10
+ assert len(g[1:10]) == 9
+ assert len(g[:1000]) == 100
+ assert len(g[1000:1001]) == 0
+ assert g[0]
+
+
+def test_filegenerator():
+ with tutils.tmpdir() as t:
+ path = os.path.join(t, "foo")
+ f = open(path, "wb")
+ f.write("x" * 10000)
+ f.close()
+ g = generators.FileGenerator(path)
+ assert len(g) == 10000
+ assert g[0] == "x"
+ assert g[-1] == "x"
+ assert g[0:5] == "xxxxx"
+ assert repr(g)
+ # remove all references to FileGenerator instance to close the file
+ # handle.
+ del g
+
+
+def test_transform_generator():
+ def trans(offset, data):
+ return "a" * len(data)
+ g = "one"
+ t = generators.TransformGenerator(g, trans)
+ assert len(t) == len(g)
+ assert t[0] == "a"
+ assert t[:] == "a" * len(g)
+ assert repr(t)
diff --git a/test/pathod/test_language_http.py b/test/pathod/test_language_http.py
new file mode 100644
index 00000000..26bb6a45
--- /dev/null
+++ b/test/pathod/test_language_http.py
@@ -0,0 +1,358 @@
+import cStringIO
+
+from libpathod import language
+from libpathod.language import http, base
+import tutils
+
+
+def parse_request(s):
+ return language.parse_pathoc(s).next()
+
+
+def test_make_error_response():
+ d = cStringIO.StringIO()
+ s = http.make_error_response("foo")
+ language.serve(s, d, {})
+
+
+class TestRequest:
+
+ def test_nonascii(self):
+ tutils.raises("ascii", parse_request, "get:\xf0")
+
+ def test_err(self):
+ tutils.raises(language.ParseException, parse_request, 'GET')
+
+ def test_simple(self):
+ r = parse_request('GET:"/foo"')
+ assert r.method.string() == "GET"
+ assert r.path.string() == "/foo"
+ r = parse_request('GET:/foo')
+ assert r.path.string() == "/foo"
+ r = parse_request('GET:@1k')
+ assert len(r.path.string()) == 1024
+
+ def test_multiple(self):
+ r = list(language.parse_pathoc("GET:/ PUT:/"))
+ assert r[0].method.string() == "GET"
+ assert r[1].method.string() == "PUT"
+ assert len(r) == 2
+
+ l = """
+ GET
+ "/foo"
+ ir,@1
+
+ PUT
+
+ "/foo
+
+
+
+ bar"
+
+ ir,@1
+ """
+ r = list(language.parse_pathoc(l))
+ assert len(r) == 2
+ assert r[0].method.string() == "GET"
+ assert r[1].method.string() == "PUT"
+
+ l = """
+ get:"http://localhost:9999/p/200":ir,@1
+ get:"http://localhost:9999/p/200":ir,@2
+ """
+ r = list(language.parse_pathoc(l))
+ assert len(r) == 2
+ assert r[0].method.string() == "GET"
+ assert r[1].method.string() == "GET"
+
+ def test_nested_response(self):
+ l = "get:/p:s'200'"
+ r = list(language.parse_pathoc(l))
+ assert len(r) == 1
+ assert len(r[0].tokens) == 3
+ assert isinstance(r[0].tokens[2], http.NestedResponse)
+ assert r[0].values({})
+
+ def test_render(self):
+ s = cStringIO.StringIO()
+ r = parse_request("GET:'/foo'")
+ assert language.serve(
+ r,
+ s,
+ language.Settings(request_host="foo.com")
+ )
+
+ def test_multiline(self):
+ l = """
+ GET
+ "/foo"
+ ir,@1
+ """
+ r = parse_request(l)
+ assert r.method.string() == "GET"
+ assert r.path.string() == "/foo"
+ assert r.actions
+
+ l = """
+ GET
+
+ "/foo
+
+
+
+ bar"
+
+ ir,@1
+ """
+ r = parse_request(l)
+ assert r.method.string() == "GET"
+ assert r.path.string().endswith("bar")
+ assert r.actions
+
+ def test_spec(self):
+ def rt(s):
+ s = parse_request(s).spec()
+ assert parse_request(s).spec() == s
+ rt("get:/foo")
+ rt("get:/foo:da")
+
+ def test_freeze(self):
+ r = parse_request("GET:/:b@100").freeze(language.Settings())
+ assert len(r.spec()) > 100
+
+ def test_path_generator(self):
+ r = parse_request("GET:@100").freeze(language.Settings())
+ assert len(r.spec()) > 100
+
+ def test_websocket(self):
+ r = parse_request('ws:/path/')
+ res = r.resolve(language.Settings())
+ assert res.method.string().lower() == "get"
+ assert res.tok(http.Path).value.val == "/path/"
+ assert res.tok(http.Method).value.val.lower() == "get"
+ assert http.get_header("Upgrade", res.headers).value.val == "websocket"
+
+ r = parse_request('ws:put:/path/')
+ res = r.resolve(language.Settings())
+ assert r.method.string().lower() == "put"
+ assert res.tok(http.Path).value.val == "/path/"
+ assert res.tok(http.Method).value.val.lower() == "put"
+ assert http.get_header("Upgrade", res.headers).value.val == "websocket"
+
+
+class TestResponse:
+
+ def dummy_response(self):
+ return language.parse_pathod("400'msg'").next()
+
+ def test_response(self):
+ r = language.parse_pathod("400:m'msg'").next()
+ assert r.status_code.string() == "400"
+ assert r.reason.string() == "msg"
+
+ r = language.parse_pathod("400:m'msg':b@100b").next()
+ assert r.reason.string() == "msg"
+ assert r.body.values({})
+ assert str(r)
+
+ r = language.parse_pathod("200").next()
+ assert r.status_code.string() == "200"
+ assert not r.reason
+ assert "OK" in [i[:] for i in r.preamble({})]
+
+ def test_render(self):
+ s = cStringIO.StringIO()
+ r = language.parse_pathod("400:m'msg'").next()
+ assert language.serve(r, s, {})
+
+ r = language.parse_pathod("400:p0,100:dr").next()
+ assert "p0" in r.spec()
+ s = r.preview_safe()
+ assert "p0" not in s.spec()
+
+ def test_raw(self):
+ s = cStringIO.StringIO()
+ r = language.parse_pathod("400:b'foo'").next()
+ language.serve(r, s, {})
+ v = s.getvalue()
+ assert "Content-Length" in v
+
+ s = cStringIO.StringIO()
+ r = language.parse_pathod("400:b'foo':r").next()
+ language.serve(r, s, {})
+ v = s.getvalue()
+ assert "Content-Length" not in v
+
+ def test_length(self):
+ def testlen(x):
+ s = cStringIO.StringIO()
+ x = x.next()
+ language.serve(x, s, language.Settings())
+ assert x.length(language.Settings()) == len(s.getvalue())
+ testlen(language.parse_pathod("400:m'msg':r"))
+ testlen(language.parse_pathod("400:m'msg':h'foo'='bar':r"))
+ testlen(language.parse_pathod("400:m'msg':h'foo'='bar':b@100b:r"))
+
+ def test_maximum_length(self):
+ def testlen(x):
+ x = x.next()
+ s = cStringIO.StringIO()
+ m = x.maximum_length({})
+ language.serve(x, s, {})
+ assert m >= len(s.getvalue())
+
+ r = language.parse_pathod("400:m'msg':b@100:d0")
+ testlen(r)
+
+ r = language.parse_pathod("400:m'msg':b@100:d0:i0,'foo'")
+ testlen(r)
+
+ r = language.parse_pathod("400:m'msg':b@100:d0:i0,'foo'")
+ testlen(r)
+
+ def test_parse_err(self):
+ tutils.raises(
+ language.ParseException, language.parse_pathod, "400:msg,b:"
+ )
+ try:
+ language.parse_pathod("400'msg':b:")
+ except language.ParseException as v:
+ assert v.marked()
+ assert str(v)
+
+ def test_nonascii(self):
+ tutils.raises("ascii", language.parse_pathod, "foo:b\xf0")
+
+ def test_parse_header(self):
+ r = language.parse_pathod('400:h"foo"="bar"').next()
+ assert http.get_header("foo", r.headers)
+
+ def test_parse_pause_before(self):
+ r = language.parse_pathod("400:p0,10").next()
+ assert r.actions[0].spec() == "p0,10"
+
+ def test_parse_pause_after(self):
+ r = language.parse_pathod("400:pa,10").next()
+ assert r.actions[0].spec() == "pa,10"
+
+ def test_parse_pause_random(self):
+ r = language.parse_pathod("400:pr,10").next()
+ assert r.actions[0].spec() == "pr,10"
+
+ def test_parse_stress(self):
+ # While larger values are known to work on linux, len() technically
+ # returns an int and a python 2.7 int on windows has 32bit precision.
+ # Therefore, we should keep the body length < 2147483647 bytes in our
+ # tests.
+ r = language.parse_pathod("400:b@1g").next()
+ assert r.length({})
+
+ def test_spec(self):
+ def rt(s):
+ s = language.parse_pathod(s).next().spec()
+ assert language.parse_pathod(s).next().spec() == s
+ rt("400:b@100g")
+ rt("400")
+ rt("400:da")
+
+ def test_websockets(self):
+ r = language.parse_pathod("ws").next()
+ tutils.raises("no websocket key", r.resolve, language.Settings())
+ res = r.resolve(language.Settings(websocket_key="foo"))
+ assert res.status_code.string() == "101"
+
+
+def test_ctype_shortcut():
+ e = http.ShortcutContentType.expr()
+ v = e.parseString("c'foo'")[0]
+ assert v.key.val == "Content-Type"
+ assert v.value.val == "foo"
+
+ s = v.spec()
+ assert s == e.parseString(s)[0].spec()
+
+ e = http.ShortcutContentType.expr()
+ v = e.parseString("c@100")[0]
+ v2 = v.freeze({})
+ v3 = v2.freeze({})
+ assert v2.value.val == v3.value.val
+
+
+def test_location_shortcut():
+ e = http.ShortcutLocation.expr()
+ v = e.parseString("l'foo'")[0]
+ assert v.key.val == "Location"
+ assert v.value.val == "foo"
+
+ s = v.spec()
+ assert s == e.parseString(s)[0].spec()
+
+ e = http.ShortcutLocation.expr()
+ v = e.parseString("l@100")[0]
+ v2 = v.freeze({})
+ v3 = v2.freeze({})
+ assert v2.value.val == v3.value.val
+
+
+def test_shortcuts():
+ assert language.parse_pathod(
+ "400:c'foo'").next().headers[0].key.val == "Content-Type"
+ assert language.parse_pathod(
+ "400:l'foo'").next().headers[0].key.val == "Location"
+
+ assert "Android" in tutils.render(parse_request("get:/:ua"))
+ assert "User-Agent" in tutils.render(parse_request("get:/:ua"))
+
+
+def test_user_agent():
+ e = http.ShortcutUserAgent.expr()
+ v = e.parseString("ua")[0]
+ assert "Android" in v.string()
+
+ e = http.ShortcutUserAgent.expr()
+ v = e.parseString("u'a'")[0]
+ assert "Android" not in v.string()
+
+ v = e.parseString("u@100'")[0]
+ assert len(str(v.freeze({}).value)) > 100
+ v2 = v.freeze({})
+ v3 = v2.freeze({})
+ assert v2.value.val == v3.value.val
+
+
+def test_nested_response():
+ e = http.NestedResponse.expr()
+ v = e.parseString("s'200'")[0]
+ assert v.value.val == "200"
+ tutils.raises(
+ language.ParseException,
+ e.parseString,
+ "s'foo'"
+ )
+
+ v = e.parseString('s"200:b@1"')[0]
+ assert "@1" in v.spec()
+ f = v.freeze({})
+ assert "@1" not in f.spec()
+
+
+def test_nested_response_freeze():
+ e = http.NestedResponse(
+ base.TokValueLiteral(
+ "200:b'foo':i10,'\\x27'".encode(
+ "string_escape"
+ )
+ )
+ )
+ assert e.freeze({})
+ assert e.values({})
+
+
+def test_unique_components():
+ tutils.raises(
+ "multiple body clauses",
+ language.parse_pathod,
+ "400:b@1:b@1"
+ )
diff --git a/test/pathod/test_language_http2.py b/test/pathod/test_language_http2.py
new file mode 100644
index 00000000..9be49452
--- /dev/null
+++ b/test/pathod/test_language_http2.py
@@ -0,0 +1,233 @@
+import cStringIO
+
+import netlib
+from netlib import tcp
+from netlib.http import user_agents
+
+from libpathod import language
+from libpathod.language import http2, base
+import tutils
+
+
+def parse_request(s):
+ return language.parse_pathoc(s, True).next()
+
+
+def parse_response(s):
+ return language.parse_pathod(s, True).next()
+
+
+def default_settings():
+ return language.Settings(
+ request_host="foo.com",
+ protocol=netlib.http.http2.HTTP2Protocol(tcp.TCPClient(('localhost', 1234)))
+ )
+
+
+def test_make_error_response():
+ d = cStringIO.StringIO()
+ s = http2.make_error_response("foo", "bar")
+ language.serve(s, d, default_settings())
+
+
+class TestRequest:
+
+ def test_cached_values(self):
+ req = parse_request("get:/")
+ req_id = id(req)
+ assert req_id == id(req.resolve(default_settings()))
+ assert req.values(default_settings()) == req.values(default_settings())
+
+ def test_nonascii(self):
+ tutils.raises("ascii", parse_request, "get:\xf0")
+
+ def test_err(self):
+ tutils.raises(language.ParseException, parse_request, 'GET')
+
+ def test_simple(self):
+ r = parse_request('GET:"/foo"')
+ assert r.method.string() == "GET"
+ assert r.path.string() == "/foo"
+ r = parse_request('GET:/foo')
+ assert r.path.string() == "/foo"
+
+ def test_multiple(self):
+ r = list(language.parse_pathoc("GET:/ PUT:/"))
+ assert r[0].method.string() == "GET"
+ assert r[1].method.string() == "PUT"
+ assert len(r) == 2
+
+ l = """
+ GET
+ "/foo"
+
+ PUT
+
+ "/foo
+
+
+
+ bar"
+ """
+ r = list(language.parse_pathoc(l, True))
+ assert len(r) == 2
+ assert r[0].method.string() == "GET"
+ assert r[1].method.string() == "PUT"
+
+ l = """
+ get:"http://localhost:9999/p/200"
+ get:"http://localhost:9999/p/200"
+ """
+ r = list(language.parse_pathoc(l, True))
+ assert len(r) == 2
+ assert r[0].method.string() == "GET"
+ assert r[1].method.string() == "GET"
+
+ def test_render_simple(self):
+ s = cStringIO.StringIO()
+ r = parse_request("GET:'/foo'")
+ assert language.serve(
+ r,
+ s,
+ default_settings(),
+ )
+
+ def test_raw_content_length(self):
+ r = parse_request('GET:/:r')
+ assert len(r.headers) == 0
+
+ r = parse_request('GET:/:r:b"foobar"')
+ assert len(r.headers) == 0
+
+ r = parse_request('GET:/')
+ assert len(r.headers) == 1
+ assert r.headers[0].values(default_settings()) == ("content-length", "0")
+
+ r = parse_request('GET:/:b"foobar"')
+ assert len(r.headers) == 1
+ assert r.headers[0].values(default_settings()) == ("content-length", "6")
+
+ r = parse_request('GET:/:b"foobar":h"content-length"="42"')
+ assert len(r.headers) == 1
+ assert r.headers[0].values(default_settings()) == ("content-length", "42")
+
+ r = parse_request('GET:/:r:b"foobar":h"content-length"="42"')
+ assert len(r.headers) == 1
+ assert r.headers[0].values(default_settings()) == ("content-length", "42")
+
+ def test_content_type(self):
+ r = parse_request('GET:/:r:c"foobar"')
+ assert len(r.headers) == 1
+ assert r.headers[0].values(default_settings()) == ("content-type", "foobar")
+
+ def test_user_agent(self):
+ r = parse_request('GET:/:r:ua')
+ assert len(r.headers) == 1
+ assert r.headers[0].values(default_settings()) == ("user-agent", user_agents.get_by_shortcut('a')[2])
+
+ def test_render_with_headers(self):
+ s = cStringIO.StringIO()
+ r = parse_request('GET:/foo:h"foo"="bar"')
+ assert language.serve(
+ r,
+ s,
+ default_settings(),
+ )
+
+ def test_nested_response(self):
+ l = "get:/p/:s'200'"
+ r = parse_request(l)
+ assert len(r.tokens) == 3
+ assert isinstance(r.tokens[2], http2.NestedResponse)
+ assert r.values(default_settings())
+
+
+ def test_render_with_body(self):
+ s = cStringIO.StringIO()
+ r = parse_request("GET:'/foo':bfoobar")
+ assert language.serve(
+ r,
+ s,
+ default_settings(),
+ )
+
+ def test_spec(self):
+ def rt(s):
+ s = parse_request(s).spec()
+ assert parse_request(s).spec() == s
+ rt("get:/foo")
+
+
+class TestResponse:
+
+ def test_cached_values(self):
+ res = parse_response("200")
+ res_id = id(res)
+ assert res_id == id(res.resolve(default_settings()))
+ assert res.values(default_settings()) == res.values(default_settings())
+
+ def test_nonascii(self):
+ tutils.raises("ascii", parse_response, "200:\xf0")
+
+ def test_err(self):
+ tutils.raises(language.ParseException, parse_response, 'GET:/')
+
+ def test_raw_content_length(self):
+ r = parse_response('200:r')
+ assert len(r.headers) == 0
+
+ r = parse_response('200')
+ assert len(r.headers) == 1
+ assert r.headers[0].values(default_settings()) == ("content-length", "0")
+
+ def test_content_type(self):
+ r = parse_response('200:r:c"foobar"')
+ assert len(r.headers) == 1
+ assert r.headers[0].values(default_settings()) == ("content-type", "foobar")
+
+ def test_simple(self):
+ r = parse_response('200:r:h"foo"="bar"')
+ assert r.status_code.string() == "200"
+ assert len(r.headers) == 1
+ assert r.headers[0].values(default_settings()) == ("foo", "bar")
+ assert r.body is None
+
+ r = parse_response('200:r:h"foo"="bar":bfoobar:h"bla"="fasel"')
+ assert r.status_code.string() == "200"
+ assert len(r.headers) == 2
+ assert r.headers[0].values(default_settings()) == ("foo", "bar")
+ assert r.headers[1].values(default_settings()) == ("bla", "fasel")
+ assert r.body.string() == "foobar"
+
+ def test_render_simple(self):
+ s = cStringIO.StringIO()
+ r = parse_response('200')
+ assert language.serve(
+ r,
+ s,
+ default_settings(),
+ )
+
+ def test_render_with_headers(self):
+ s = cStringIO.StringIO()
+ r = parse_response('200:h"foo"="bar"')
+ assert language.serve(
+ r,
+ s,
+ default_settings(),
+ )
+
+ def test_render_with_body(self):
+ s = cStringIO.StringIO()
+ r = parse_response('200:bfoobar')
+ assert language.serve(
+ r,
+ s,
+ default_settings(),
+ )
+
+ def test_spec(self):
+ def rt(s):
+ s = parse_response(s).spec()
+ assert parse_response(s).spec() == s
+ rt("200:bfoobar")
diff --git a/test/pathod/test_language_websocket.py b/test/pathod/test_language_websocket.py
new file mode 100644
index 00000000..d98fd33e
--- /dev/null
+++ b/test/pathod/test_language_websocket.py
@@ -0,0 +1,142 @@
+
+from libpathod import language
+from libpathod.language import websockets
+import netlib.websockets
+import tutils
+
+
+def parse_request(s):
+ return language.parse_pathoc(s).next()
+
+
+class TestWebsocketFrame:
+
+ def _test_messages(self, specs, message_klass):
+ for i in specs:
+ wf = parse_request(i)
+ assert isinstance(wf, message_klass)
+ assert wf
+ assert wf.values(language.Settings())
+ assert wf.resolve(language.Settings())
+
+ spec = wf.spec()
+ wf2 = parse_request(spec)
+ assert wf2.spec() == spec
+
+ def test_server_values(self):
+ specs = [
+ "wf",
+ "wf:dr",
+ "wf:b'foo'",
+ "wf:mask:r'foo'",
+ "wf:l1024:b'foo'",
+ "wf:cbinary",
+ "wf:c1",
+ "wf:mask:knone",
+ "wf:fin",
+ "wf:fin:rsv1:rsv2:rsv3:mask",
+ "wf:-fin:-rsv1:-rsv2:-rsv3:-mask",
+ "wf:k@4",
+ "wf:x10",
+ ]
+ self._test_messages(specs, websockets.WebsocketFrame)
+
+ def test_parse_websocket_frames(self):
+ wf = language.parse_websocket_frame("wf:x10")
+ assert len(list(wf)) == 10
+ tutils.raises(
+ language.ParseException,
+ language.parse_websocket_frame,
+ "wf:x"
+ )
+
+ def test_client_values(self):
+ specs = [
+ "wf:f'wf'",
+ ]
+ self._test_messages(specs, websockets.WebsocketClientFrame)
+
+ def test_nested_frame(self):
+ wf = parse_request("wf:f'wf'")
+ assert wf.nested_frame
+
+ def test_flags(self):
+ wf = parse_request("wf:fin:mask:rsv1:rsv2:rsv3")
+ frm = netlib.websockets.Frame.from_bytes(tutils.render(wf))
+ assert frm.header.fin
+ assert frm.header.mask
+ assert frm.header.rsv1
+ assert frm.header.rsv2
+ assert frm.header.rsv3
+
+ wf = parse_request("wf:-fin:-mask:-rsv1:-rsv2:-rsv3")
+ frm = netlib.websockets.Frame.from_bytes(tutils.render(wf))
+ assert not frm.header.fin
+ assert not frm.header.mask
+ assert not frm.header.rsv1
+ assert not frm.header.rsv2
+ assert not frm.header.rsv3
+
+ def fr(self, spec, **kwargs):
+ settings = language.base.Settings(**kwargs)
+ wf = parse_request(spec)
+ return netlib.websockets.Frame.from_bytes(tutils.render(wf, settings))
+
+ def test_construction(self):
+ assert self.fr("wf:c1").header.opcode == 1
+ assert self.fr("wf:c0").header.opcode == 0
+ assert self.fr("wf:cbinary").header.opcode ==\
+ netlib.websockets.OPCODE.BINARY
+ assert self.fr("wf:ctext").header.opcode ==\
+ netlib.websockets.OPCODE.TEXT
+
+ def test_rawbody(self):
+ frm = self.fr("wf:mask:r'foo'")
+ assert len(frm.payload) == 3
+ assert frm.payload != "foo"
+
+ assert self.fr("wf:r'foo'").payload == "foo"
+
+ def test_construction(self):
+ # Simple server frame
+ frm = self.fr("wf:b'foo'")
+ assert not frm.header.mask
+ assert not frm.header.masking_key
+
+ # Simple client frame
+ frm = self.fr("wf:b'foo'", is_client=True)
+ assert frm.header.mask
+ assert frm.header.masking_key
+ frm = self.fr("wf:b'foo':k'abcd'", is_client=True)
+ assert frm.header.mask
+ assert frm.header.masking_key == 'abcd'
+
+ # Server frame, mask explicitly set
+ frm = self.fr("wf:b'foo':mask")
+ assert frm.header.mask
+ assert frm.header.masking_key
+ frm = self.fr("wf:b'foo':k'abcd'")
+ assert frm.header.mask
+ assert frm.header.masking_key == 'abcd'
+
+ # Client frame, mask explicitly unset
+ frm = self.fr("wf:b'foo':-mask", is_client=True)
+ assert not frm.header.mask
+ assert not frm.header.masking_key
+
+ frm = self.fr("wf:b'foo':-mask:k'abcd'", is_client=True)
+ assert not frm.header.mask
+ # We're reading back a corrupted frame - the first 3 characters of the
+ # mask is mis-interpreted as the payload
+ assert frm.payload == "abc"
+
+ def test_knone(self):
+ with tutils.raises("expected 4 bytes"):
+ self.fr("wf:b'foo':mask:knone")
+
+ def test_length(self):
+ assert self.fr("wf:l3:b'foo'").header.payload_length == 3
+ frm = self.fr("wf:l2:b'foo'")
+ assert frm.header.payload_length == 2
+ assert frm.payload == "fo"
+ tutils.raises("expected 1024 bytes", self.fr, "wf:l1024:b'foo'")
diff --git a/test/pathod/test_language_writer.py b/test/pathod/test_language_writer.py
new file mode 100644
index 00000000..1a532903
--- /dev/null
+++ b/test/pathod/test_language_writer.py
@@ -0,0 +1,91 @@
+import cStringIO
+
+from libpathod import language
+from libpathod.language import writer
+
+
+def test_send_chunk():
+ v = "foobarfoobar"
+ for bs in range(1, len(v) + 2):
+ s = cStringIO.StringIO()
+ writer.send_chunk(s, v, bs, 0, len(v))
+ assert s.getvalue() == v
+ for start in range(len(v)):
+ for end in range(len(v)):
+ s = cStringIO.StringIO()
+ writer.send_chunk(s, v, bs, start, end)
+ assert s.getvalue() == v[start:end]
+
+
+def test_write_values_inject():
+ tst = "foo"
+
+ s = cStringIO.StringIO()
+ writer.write_values(s, [tst], [(0, "inject", "aaa")], blocksize=5)
+ assert s.getvalue() == "aaafoo"
+
+ s = cStringIO.StringIO()
+ writer.write_values(s, [tst], [(1, "inject", "aaa")], blocksize=5)
+ assert s.getvalue() == "faaaoo"
+
+ s = cStringIO.StringIO()
+ writer.write_values(s, [tst], [(1, "inject", "aaa")], blocksize=5)
+ assert s.getvalue() == "faaaoo"
+
+
+def test_write_values_disconnects():
+ s = cStringIO.StringIO()
+ tst = "foo" * 100
+ writer.write_values(s, [tst], [(0, "disconnect")], blocksize=5)
+ assert not s.getvalue()
+
+
+def test_write_values():
+ tst = "foobarvoing"
+ s = cStringIO.StringIO()
+ writer.write_values(s, [tst], [])
+ assert s.getvalue() == tst
+
+ for bs in range(1, len(tst) + 2):
+ for off in range(len(tst)):
+ s = cStringIO.StringIO()
+ writer.write_values(
+ s, [tst], [(off, "disconnect")], blocksize=bs
+ )
+ assert s.getvalue() == tst[:off]
+
+
+def test_write_values_pauses():
+ tst = "".join(str(i) for i in range(10))
+ for i in range(2, 10):
+ s = cStringIO.StringIO()
+ writer.write_values(
+ s, [tst], [(2, "pause", 0), (1, "pause", 0)], blocksize=i
+ )
+ assert s.getvalue() == tst
+
+ for i in range(2, 10):
+ s = cStringIO.StringIO()
+ writer.write_values(s, [tst], [(1, "pause", 0)], blocksize=i)
+ assert s.getvalue() == tst
+
+ tst = ["".join(str(i) for i in range(10))] * 5
+ for i in range(2, 10):
+ s = cStringIO.StringIO()
+ writer.write_values(s, tst[:], [(1, "pause", 0)], blocksize=i)
+ assert s.getvalue() == "".join(tst)
+
+
+def test_write_values_after():
+ s = cStringIO.StringIO()
+ r = language.parse_pathod("400:da").next()
+ language.serve(r, s, {})
+
+ s = cStringIO.StringIO()
+ r = language.parse_pathod("400:pa,0").next()
+ language.serve(r, s, {})
+
+ s = cStringIO.StringIO()
+ r = language.parse_pathod("400:ia,'xx'").next()
+ language.serve(r, s, {})
+ assert s.getvalue().endswith('xx')
diff --git a/test/pathod/test_log.py b/test/pathod/test_log.py
new file mode 100644
index 00000000..8f38c040
--- /dev/null
+++ b/test/pathod/test_log.py
@@ -0,0 +1,25 @@
+import StringIO
+from libpathod import log
+from netlib.exceptions import TcpDisconnect
+import netlib.tcp
+
+
+class DummyIO(StringIO.StringIO):
+
+ def start_log(self, *args, **kwargs):
+ pass
+
+ def get_log(self, *args, **kwargs):
+ return ""
+
+
+def test_disconnect():
+ outf = DummyIO()
+ rw = DummyIO()
+ l = log.ConnectionLogger(outf, False, rw, rw)
+ try:
+ with l.ctx() as lg:
+ lg("Test")
+ except TcpDisconnect:
+ pass
+ assert "Test" in outf.getvalue()
diff --git a/test/pathod/test_pathoc.py b/test/pathod/test_pathoc.py
new file mode 100644
index 00000000..7c912773
--- /dev/null
+++ b/test/pathod/test_pathoc.py
@@ -0,0 +1,310 @@
+import json
+import cStringIO
+import re
+import OpenSSL
+import pytest
+from mock import Mock
+
+from netlib import tcp, http, socks
+from netlib.exceptions import HttpException, TcpException, NetlibException
+from netlib.http import http1, http2
+
+from libpathod import pathoc, test, version, pathod, language
+from netlib.tutils import raises
+import tutils
+
+
+def test_response():
+ r = http.Response("HTTP/1.1", 200, "Message", {}, None, None)
+ assert repr(r)
+
+
+class _TestDaemon:
+ ssloptions = pathod.SSLOptions()
+
+ @classmethod
+ def setup_class(cls):
+ cls.d = test.Daemon(
+ ssl=cls.ssl,
+ ssloptions=cls.ssloptions,
+ staticdir=tutils.test_data.path("data"),
+ anchors=[
+ (re.compile("/anchor/.*"), "202")
+ ]
+ )
+
+ @classmethod
+ def teardown_class(cls):
+ cls.d.shutdown()
+
+ def setUp(self):
+ self.d.clear_log()
+
+ def test_info(self):
+ c = pathoc.Pathoc(
+ ("127.0.0.1", self.d.port),
+ ssl=self.ssl,
+ fp=None
+ )
+ c.connect()
+ resp = c.request("get:/api/info")
+ assert tuple(json.loads(resp.content)["version"]) == version.IVERSION
+
+ def tval(
+ self,
+ requests,
+ showreq=False,
+ showresp=False,
+ explain=False,
+ showssl=False,
+ hexdump=False,
+ timeout=None,
+ ignorecodes=(),
+ ignoretimeout=None,
+ showsummary=True
+ ):
+ s = cStringIO.StringIO()
+ c = pathoc.Pathoc(
+ ("127.0.0.1", self.d.port),
+ ssl=self.ssl,
+ showreq=showreq,
+ showresp=showresp,
+ explain=explain,
+ hexdump=hexdump,
+ ignorecodes=ignorecodes,
+ ignoretimeout=ignoretimeout,
+ showsummary=showsummary,
+ fp=s
+ )
+ c.connect(showssl=showssl, fp=s)
+ if timeout:
+ c.settimeout(timeout)
+ for i in requests:
+ r = language.parse_pathoc(i).next()
+ if explain:
+ r = r.freeze(language.Settings())
+ try:
+ c.request(r)
+ except NetlibException:
+ pass
+ return s.getvalue()
+
+
+class TestDaemonSSL(_TestDaemon):
+ ssl = True
+ ssloptions = pathod.SSLOptions(
+ request_client_cert=True,
+ sans=["test1.com", "test2.com"],
+ alpn_select=b'h2',
+ )
+
+ def test_sni(self):
+ c = pathoc.Pathoc(
+ ("127.0.0.1", self.d.port),
+ ssl=True,
+ sni="foobar.com",
+ fp=None
+ )
+ c.connect()
+ c.request("get:/p/200")
+ r = c.request("get:/api/log")
+ d = json.loads(r.content)
+ assert d["log"][0]["request"]["sni"] == "foobar.com"
+
+ def test_showssl(self):
+ assert "certificate chain" in self.tval(["get:/p/200"], showssl=True)
+
+ def test_clientcert(self):
+ c = pathoc.Pathoc(
+ ("127.0.0.1", self.d.port),
+ ssl=True,
+ clientcert=tutils.test_data.path("data/clientcert/client.pem"),
+ fp=None
+ )
+ c.connect()
+ c.request("get:/p/200")
+ r = c.request("get:/api/log")
+ d = json.loads(r.content)
+ assert d["log"][0]["request"]["clientcert"]["keyinfo"]
+
+ def test_http2_without_ssl(self):
+ fp = cStringIO.StringIO()
+ c = pathoc.Pathoc(
+ ("127.0.0.1", self.d.port),
+ use_http2=True,
+ ssl=False,
+ fp = fp
+ )
+ tutils.raises(NotImplementedError, c.connect)
+
+
+class TestDaemon(_TestDaemon):
+ ssl = False
+
+ def test_ssl_error(self):
+ c = pathoc.Pathoc(("127.0.0.1", self.d.port), ssl=True, fp=None)
+ tutils.raises("ssl handshake", c.connect)
+
+ def test_showssl(self):
+ assert not "certificate chain" in self.tval(
+ ["get:/p/200"],
+ showssl=True)
+
+ def test_ignorecodes(self):
+ assert "200" in self.tval(["get:'/p/200:b@1'"])
+ assert "200" in self.tval(["get:'/p/200:b@1'"])
+ assert "200" in self.tval(["get:'/p/200:b@1'"])
+ assert "200" not in self.tval(["get:'/p/200:b@1'"], ignorecodes=[200])
+ assert "200" not in self.tval(
+ ["get:'/p/200:b@1'"],
+ ignorecodes=[
+ 200,
+ 201])
+ assert "202" in self.tval(["get:'/p/202:b@1'"], ignorecodes=[200, 201])
+
+ def test_timeout(self):
+ assert "Timeout" in self.tval(["get:'/p/200:p0,100'"], timeout=0.01)
+ assert "HTTP" in self.tval(
+ ["get:'/p/200:p5,100'"],
+ showresp=True,
+ timeout=1
+ )
+ assert not "HTTP" in self.tval(
+ ["get:'/p/200:p3,100'"],
+ showresp=True,
+ timeout=1,
+ ignoretimeout=True
+ )
+
+ def test_showresp(self):
+ reqs = ["get:/api/info:p0,0", "get:/api/info:p0,0"]
+ assert self.tval(reqs).count("200") == 2
+ assert self.tval(reqs, showresp=True).count("HTTP/1.1 200 OK") == 2
+ assert self.tval(
+ reqs, showresp=True, hexdump=True
+ ).count("0000000000") == 2
+
+ def test_showresp_httperr(self):
+ v = self.tval(["get:'/p/200:d20'"], showresp=True, showsummary=True)
+ assert "Invalid headers" in v
+ assert "HTTP/" in v
+
+ def test_explain(self):
+ reqs = ["get:/p/200:b@100"]
+ assert "b@100" not in self.tval(reqs, explain=True)
+
+ def test_showreq(self):
+ reqs = ["get:/api/info:p0,0", "get:/api/info:p0,0"]
+ assert self.tval(reqs, showreq=True).count("GET /api") == 2
+ assert self.tval(
+ reqs, showreq=True, hexdump=True
+ ).count("0000000000") == 2
+
+ def test_conn_err(self):
+ assert "Invalid server response" in self.tval(["get:'/p/200:d2'"])
+
+ def test_websocket_shutdown(self):
+ c = pathoc.Pathoc(("127.0.0.1", self.d.port), fp=None)
+ c.connect()
+ c.request("ws:/")
+ c.stop()
+
+ @pytest.mark.xfail
+ def test_wait_finish(self):
+ c = pathoc.Pathoc(
+ ("127.0.0.1", self.d.port),
+ fp=None,
+ ws_read_limit=1
+ )
+ c.connect()
+ c.request("ws:/")
+ c.request("wf:f'wf:x100'")
+ [i for i in c.wait(timeout=0, finish=False)]
+ [i for i in c.wait(timeout=0)]
+
+ def test_connect_fail(self):
+ to = ("foobar", 80)
+ c = pathoc.Pathoc(("127.0.0.1", self.d.port), fp=None)
+ c.rfile, c.wfile = cStringIO.StringIO(), cStringIO.StringIO()
+ with raises("connect failed"):
+ c.http_connect(to)
+ c.rfile = cStringIO.StringIO(
+ "HTTP/1.1 500 OK\r\n"
+ )
+ with raises("connect failed"):
+ c.http_connect(to)
+ c.rfile = cStringIO.StringIO(
+ "HTTP/1.1 200 OK\r\n"
+ )
+ c.http_connect(to)
+
+ def test_socks_connect(self):
+ to = ("foobar", 80)
+ c = pathoc.Pathoc(("127.0.0.1", self.d.port), fp=None)
+ c.rfile, c.wfile = tutils.treader(""), cStringIO.StringIO()
+ tutils.raises(pathoc.PathocError, c.socks_connect, to)
+
+ c.rfile = tutils.treader(
+ "\x05\xEE"
+ )
+ tutils.raises("SOCKS without authentication", c.socks_connect, ("example.com", 0xDEAD))
+
+ c.rfile = tutils.treader(
+ "\x05\x00" +
+ "\x05\xEE\x00\x03\x0bexample.com\xDE\xAD"
+ )
+ tutils.raises("SOCKS server error", c.socks_connect, ("example.com", 0xDEAD))
+
+ c.rfile = tutils.treader(
+ "\x05\x00" +
+ "\x05\x00\x00\x03\x0bexample.com\xDE\xAD"
+ )
+ c.socks_connect(("example.com", 0xDEAD))
+
+
+class TestDaemonHTTP2(_TestDaemon):
+ ssl = True
+
+ if tcp.HAS_ALPN:
+
+ def test_http2(self):
+ c = pathoc.Pathoc(
+ ("127.0.0.1", self.d.port),
+ fp=None,
+ ssl=True,
+ use_http2=True,
+ )
+ assert isinstance(c.protocol, http2.HTTP2Protocol)
+
+ c = pathoc.Pathoc(
+ ("127.0.0.1", self.d.port),
+ )
+ assert c.protocol == http1
+
+ def test_http2_alpn(self):
+ c = pathoc.Pathoc(
+ ("127.0.0.1", self.d.port),
+ fp=None,
+ ssl=True,
+ use_http2=True,
+ http2_skip_connection_preface=True,
+ )
+
+ tmp_convert_to_ssl = c.convert_to_ssl
+ c.convert_to_ssl = Mock()
+ c.convert_to_ssl.side_effect = tmp_convert_to_ssl
+ c.connect()
+
+ _, kwargs = c.convert_to_ssl.call_args
+ assert set(kwargs['alpn_protos']) == set([b'http/1.1', b'h2'])
+
+ def test_request(self):
+ c = pathoc.Pathoc(
+ ("127.0.0.1", self.d.port),
+ fp=None,
+ ssl=True,
+ use_http2=True,
+ )
+ c.connect()
+ resp = c.request("get:/p/200")
+ assert resp.status_code == 200
diff --git a/test/pathod/test_pathoc_cmdline.py b/test/pathod/test_pathoc_cmdline.py
new file mode 100644
index 00000000..74dfef57
--- /dev/null
+++ b/test/pathod/test_pathoc_cmdline.py
@@ -0,0 +1,59 @@
+from libpathod import pathoc_cmdline as cmdline
+import tutils
+import cStringIO
+import mock
+
+
+@mock.patch("argparse.ArgumentParser.error")
+def test_pathoc(perror):
+ assert cmdline.args_pathoc(["pathoc", "foo.com", "get:/"])
+ s = cStringIO.StringIO()
+ with tutils.raises(SystemExit):
+ cmdline.args_pathoc(["pathoc", "--show-uas"], s, s)
+
+ a = cmdline.args_pathoc(["pathoc", "foo.com:8888", "get:/"])
+ assert a.port == 8888
+
+ a = cmdline.args_pathoc(["pathoc", "foo.com:xxx", "get:/"])
+ assert perror.called
+ perror.reset_mock()
+
+ a = cmdline.args_pathoc(["pathoc", "-I", "10, 20", "foo.com:8888", "get:/"])
+ assert a.ignorecodes == [10, 20]
+
+ a = cmdline.args_pathoc(["pathoc", "-I", "xx, 20", "foo.com:8888", "get:/"])
+ assert perror.called
+ perror.reset_mock()
+
+ a = cmdline.args_pathoc(["pathoc", "-c", "foo:10", "foo.com:8888", "get:/"])
+ assert a.connect_to == ["foo", 10]
+
+ a = cmdline.args_pathoc(["pathoc", "foo.com", "get:/", "--http2"])
+ assert a.use_http2 == True
+ assert a.ssl == True
+
+ a = cmdline.args_pathoc(["pathoc", "foo.com", "get:/", "--http2-skip-connection-preface"])
+ assert a.use_http2 == True
+ assert a.ssl == True
+ assert a.http2_skip_connection_preface == True
+
+ a = cmdline.args_pathoc(["pathoc", "-c", "foo", "foo.com:8888", "get:/"])
+ assert perror.called
+ perror.reset_mock()
+
+ a = cmdline.args_pathoc(
+ ["pathoc", "-c", "foo:bar", "foo.com:8888", "get:/"])
+ assert perror.called
+ perror.reset_mock()
+
+ a = cmdline.args_pathoc(
+ [
+ "pathoc",
+ "foo.com:8888",
+ tutils.test_data.path("data/request")
+ ]
+ )
+ assert len(list(a.requests)) == 1
+
+ with tutils.raises(SystemExit):
+ cmdline.args_pathoc(["pathoc", "foo.com", "invalid"], s, s)
diff --git a/test/pathod/test_pathod.py b/test/pathod/test_pathod.py
new file mode 100644
index 00000000..ee5fc7bd
--- /dev/null
+++ b/test/pathod/test_pathod.py
@@ -0,0 +1,289 @@
+import sys
+import cStringIO
+import OpenSSL
+
+from libpathod import pathod, version
+from netlib import tcp, http
+from netlib.exceptions import HttpException, TlsException
+import tutils
+
+
+class TestPathod(object):
+
+ def test_logging(self):
+ s = cStringIO.StringIO()
+ p = pathod.Pathod(("127.0.0.1", 0), logfp=s)
+ assert len(p.get_log()) == 0
+ id = p.add_log(dict(s="foo"))
+ assert p.log_by_id(id)
+ assert len(p.get_log()) == 1
+ p.clear_log()
+ assert len(p.get_log()) == 0
+
+ for _ in range(p.LOGBUF + 1):
+ p.add_log(dict(s="foo"))
+ assert len(p.get_log()) <= p.LOGBUF
+
+
+class TestNoWeb(tutils.DaemonTests):
+ noweb = True
+
+ def test_noweb(self):
+ assert self.get("200:da").status_code == 200
+ assert self.getpath("/").status_code == 800
+
+
+class TestTimeout(tutils.DaemonTests):
+ timeout = 0.01
+
+ def test_noweb(self):
+ # FIXME: Add float values to spec language, reduce test timeout to
+ # increase test performance
+ # This is a bodge - we have some platform difference that causes
+ # different exceptions to be raised here.
+ tutils.raises(Exception, self.pathoc, ["get:/:p1,1"])
+ assert self.d.last_log()["type"] == "timeout"
+
+
+class TestNoApi(tutils.DaemonTests):
+ noapi = True
+
+ def test_noapi(self):
+ assert self.getpath("/log").status_code == 404
+ r = self.getpath("/")
+ assert r.status_code == 200
+ assert not "Log" in r.content
+
+
+class TestNotAfterConnect(tutils.DaemonTests):
+ ssl = False
+ ssloptions = dict(
+ not_after_connect=True
+ )
+
+ def test_connect(self):
+ r, _ = self.pathoc(
+ [r"get:'http://foo.com/p/202':da"],
+ connect_to=("localhost", self.d.port)
+ )
+ assert r[0].status_code == 202
+
+
+class TestCustomCert(tutils.DaemonTests):
+ ssl = True
+ ssloptions = dict(
+ certs=[("*", tutils.test_data.path("data/testkey.pem"))],
+ )
+
+ def test_connect(self):
+ r, _ = self.pathoc([r"get:/p/202"])
+ r = r[0]
+ assert r.status_code == 202
+ assert r.sslinfo
+ assert "test.com" in str(r.sslinfo.certchain[0].get_subject())
+
+
+class TestSSLCN(tutils.DaemonTests):
+ ssl = True
+ ssloptions = dict(
+ cn="foo.com"
+ )
+
+ def test_connect(self):
+ r, _ = self.pathoc([r"get:/p/202"])
+ r = r[0]
+ assert r.status_code == 202
+ assert r.sslinfo
+ assert r.sslinfo.certchain[0].get_subject().CN == "foo.com"
+
+
+class TestNohang(tutils.DaemonTests):
+ nohang = True
+
+ def test_nohang(self):
+ r = self.get("200:p0,0")
+ assert r.status_code == 800
+ l = self.d.last_log()
+ assert "Pauses have been disabled" in l["response"]["msg"]
+
+
+class TestHexdump(tutils.DaemonTests):
+ hexdump = True
+
+ def test_hexdump(self):
+ r = self.get(r"200:b'\xf0'")
+
+
+class TestNocraft(tutils.DaemonTests):
+ nocraft = True
+
+ def test_nocraft(self):
+ r = self.get(r"200:b'\xf0'")
+ assert r.status_code == 800
+ assert "Crafting disabled" in r.content
+
+
+class CommonTests(tutils.DaemonTests):
+
+ def test_binarydata(self):
+ r = self.get(r"200:b'\xf0'")
+ l = self.d.last_log()
+ # FIXME: Other binary data elements
+
+ def test_sizelimit(self):
+ r = self.get("200:b@1g")
+ assert r.status_code == 800
+ l = self.d.last_log()
+ assert "too large" in l["response"]["msg"]
+
+ def test_preline(self):
+ r, _ = self.pathoc([r"get:'/p/200':i0,'\r\n'"])
+ assert r[0].status_code == 200
+
+ def test_info(self):
+ assert tuple(self.d.info()["version"]) == version.IVERSION
+
+ def test_logs(self):
+ assert self.d.clear_log()
+ assert not self.d.last_log()
+ rsp = self.get("202:da")
+ assert len(self.d.log()) == 1
+ assert self.d.clear_log()
+ assert len(self.d.log()) == 0
+
+ def test_disconnect(self):
+ rsp = self.get("202:b@100k:d200")
+ assert len(rsp.content) < 200
+
+ def test_parserr(self):
+ rsp = self.get("400:msg,b:")
+ assert rsp.status_code == 800
+
+ def test_static(self):
+ rsp = self.get("200:b<file")
+ assert rsp.status_code == 200
+ assert rsp.content.strip() == "testfile"
+
+ def test_anchor(self):
+ rsp = self.getpath("anchor/foo")
+ assert rsp.status_code == 202
+
+ def test_invalid_first_line(self):
+ c = tcp.TCPClient(("localhost", self.d.port))
+ c.connect()
+ if self.ssl:
+ c.convert_to_ssl()
+ c.wfile.write("foo\n\n\n")
+ c.wfile.flush()
+ l = self.d.last_log()
+ assert l["type"] == "error"
+ assert "foo" in l["msg"]
+
+ def test_invalid_content_length(self):
+ tutils.raises(
+ HttpException,
+ self.pathoc,
+ ["get:/:h'content-length'='foo'"]
+ )
+ l = self.d.last_log()
+ assert l["type"] == "error"
+ assert "Unparseable Content Length" in l["msg"]
+
+ def test_invalid_headers(self):
+ tutils.raises(HttpException, self.pathoc, ["get:/:h'\t'='foo'"])
+ l = self.d.last_log()
+ assert l["type"] == "error"
+ assert "Invalid headers" in l["msg"]
+
+ def test_access_denied(self):
+ rsp = self.get("=nonexistent")
+ assert rsp.status_code == 800
+
+ def test_source_access_denied(self):
+ rsp = self.get("200:b</foo")
+ assert rsp.status_code == 800
+ assert "File access denied" in rsp.content
+
+ def test_proxy(self):
+ r, _ = self.pathoc([r"get:'http://foo.com/p/202':da"])
+ assert r[0].status_code == 202
+
+ def test_websocket(self):
+ r, _ = self.pathoc(["ws:/p/"], ws_read_limit=0)
+ assert r[0].status_code == 101
+
+ r, _ = self.pathoc(["ws:/p/ws"], ws_read_limit=0)
+ assert r[0].status_code == 101
+
+ def test_websocket_frame(self):
+ r, _ = self.pathoc(
+ ["ws:/p/", "wf:f'wf:b\"test\"':pa,1"],
+ ws_read_limit=1
+ )
+ assert r[1].payload == "test"
+
+ def test_websocket_frame_reflect_error(self):
+ r, _ = self.pathoc(
+ ["ws:/p/", "wf:-mask:knone:f'wf:b@10':i13,'a'"],
+ ws_read_limit=1,
+ timeout=1
+ )
+ # FIXME: Race Condition?
+ assert "Parse error" in self.d.text_log()
+
+ def test_websocket_frame_disconnect_error(self):
+ self.pathoc(["ws:/p/", "wf:b@10:d3"], ws_read_limit=0)
+ assert self.d.last_log()
+
+
+class TestDaemon(CommonTests):
+ ssl = False
+
+ def test_connect(self):
+ r, _ = self.pathoc(
+ [r"get:'http://foo.com/p/202':da"],
+ connect_to=("localhost", self.d.port),
+ ssl=True
+ )
+ assert r[0].status_code == 202
+
+ def test_connect_err(self):
+ tutils.raises(
+ HttpException,
+ self.pathoc,
+ [r"get:'http://foo.com/p/202':da"],
+ connect_to=("localhost", self.d.port)
+ )
+
+
+class TestDaemonSSL(CommonTests):
+ ssl = True
+
+ def test_ssl_conn_failure(self):
+ c = tcp.TCPClient(("localhost", self.d.port))
+ c.rbufsize = 0
+ c.wbufsize = 0
+ c.connect()
+ c.wfile.write("\0\0\0\0")
+ tutils.raises(TlsException, c.convert_to_ssl)
+ l = self.d.last_log()
+ assert l["type"] == "error"
+ assert "SSL" in l["msg"]
+
+ def test_ssl_cipher(self):
+ r, _ = self.pathoc([r"get:/p/202"])
+ assert r[0].status_code == 202
+ assert self.d.last_log()["cipher"][1] > 0
+
+
+class TestHTTP2(tutils.DaemonTests):
+ ssl = True
+ noweb = True
+ noapi = True
+ nohang = True
+
+ if tcp.HAS_ALPN:
+
+ def test_http2(self):
+ r, _ = self.pathoc(["GET:/"], ssl=True, use_http2=True)
+ assert r[0].status_code == 800
diff --git a/test/pathod/test_pathod_cmdline.py b/test/pathod/test_pathod_cmdline.py
new file mode 100644
index 00000000..829c4b32
--- /dev/null
+++ b/test/pathod/test_pathod_cmdline.py
@@ -0,0 +1,85 @@
+from libpathod import pathod_cmdline as cmdline
+import tutils
+import cStringIO
+import mock
+
+
+@mock.patch("argparse.ArgumentParser.error")
+def test_pathod(perror):
+ assert cmdline.args_pathod(["pathod"])
+
+ a = cmdline.args_pathod(
+ [
+ "pathod",
+ "--cert",
+ tutils.test_data.path("data/testkey.pem")
+ ]
+ )
+ assert a.ssl_certs
+
+ a = cmdline.args_pathod(
+ [
+ "pathod",
+ "--cert",
+ "nonexistent"
+ ]
+ )
+ assert perror.called
+ perror.reset_mock()
+
+ a = cmdline.args_pathod(
+ [
+ "pathod",
+ "-a",
+ "foo=200"
+ ]
+ )
+ assert a.anchors
+
+ a = cmdline.args_pathod(
+ [
+ "pathod",
+ "-a",
+ "foo=" + tutils.test_data.path("data/response")
+ ]
+ )
+ assert a.anchors
+
+ a = cmdline.args_pathod(
+ [
+ "pathod",
+ "-a",
+ "?=200"
+ ]
+ )
+ assert perror.called
+ perror.reset_mock()
+
+ a = cmdline.args_pathod(
+ [
+ "pathod",
+ "-a",
+ "foo"
+ ]
+ )
+ assert perror.called
+ perror.reset_mock()
+
+ a = cmdline.args_pathod(
+ [
+ "pathod",
+ "--limit-size",
+ "200k"
+ ]
+ )
+ assert a.sizelimit
+
+ a = cmdline.args_pathod(
+ [
+ "pathod",
+ "--limit-size",
+ "q"
+ ]
+ )
+ assert perror.called
+ perror.reset_mock()
diff --git a/test/pathod/test_test.py b/test/pathod/test_test.py
new file mode 100644
index 00000000..bd92d864
--- /dev/null
+++ b/test/pathod/test_test.py
@@ -0,0 +1,45 @@
+import logging
+import requests
+from libpathod import test
+import tutils
+logging.disable(logging.CRITICAL)
+
+
+class TestDaemonManual:
+
+ def test_simple(self):
+ with test.Daemon() as d:
+ rsp = requests.get("http://localhost:%s/p/202:da" % d.port)
+ assert rsp.ok
+ assert rsp.status_code == 202
+ with tutils.raises(requests.ConnectionError):
+ requests.get("http://localhost:%s/p/202:da" % d.port)
+
+ def test_startstop_ssl(self):
+ d = test.Daemon(ssl=True)
+ rsp = requests.get(
+ "https://localhost:%s/p/202:da" %
+ d.port,
+ verify=False)
+ assert rsp.ok
+ assert rsp.status_code == 202
+ d.shutdown()
+ with tutils.raises(requests.ConnectionError):
+ requests.get("http://localhost:%s/p/202:da" % d.port)
+
+ def test_startstop_ssl_explicit(self):
+ ssloptions = dict(
+ certfile=tutils.test_data.path("data/testkey.pem"),
+ cacert=tutils.test_data.path("data/testkey.pem"),
+ ssl_after_connect=False
+ )
+ d = test.Daemon(ssl=ssloptions)
+ rsp = requests.get(
+ "https://localhost:%s/p/202:da" %
+ d.port,
+ verify=False)
+ assert rsp.ok
+ assert rsp.status_code == 202
+ d.shutdown()
+ with tutils.raises(requests.ConnectionError):
+ requests.get("http://localhost:%s/p/202:da" % d.port)
diff --git a/test/pathod/test_utils.py b/test/pathod/test_utils.py
new file mode 100644
index 00000000..7d24e9e4
--- /dev/null
+++ b/test/pathod/test_utils.py
@@ -0,0 +1,39 @@
+from libpathod import utils
+import tutils
+
+
+def test_membool():
+ m = utils.MemBool()
+ assert not m.v
+ assert m(1)
+ assert m.v == 1
+ assert m(2)
+ assert m.v == 2
+
+
+def test_parse_size():
+ assert utils.parse_size("100") == 100
+ assert utils.parse_size("100k") == 100 * 1024
+ tutils.raises("invalid size spec", utils.parse_size, "foo")
+ tutils.raises("invalid size spec", utils.parse_size, "100kk")
+
+
+def test_parse_anchor_spec():
+ assert utils.parse_anchor_spec("foo=200") == ("foo", "200")
+ assert utils.parse_anchor_spec("foo") is None
+
+
+def test_data_path():
+ tutils.raises(ValueError, utils.data.path, "nonexistent")
+
+
+def test_inner_repr():
+ assert utils.inner_repr("\x66") == "\x66"
+ assert utils.inner_repr(u"foo") == "foo"
+
+
+def test_escape_unprintables():
+ s = "".join([chr(i) for i in range(255)])
+ e = utils.escape_unprintables(s)
+ assert e.encode('ascii')
+ assert not "PATHOD_MARKER" in e
diff --git a/test/pathod/tutils.py b/test/pathod/tutils.py
new file mode 100644
index 00000000..664cdd52
--- /dev/null
+++ b/test/pathod/tutils.py
@@ -0,0 +1,128 @@
+import tempfile
+import os
+import re
+import shutil
+import cStringIO
+from contextlib import contextmanager
+
+import netlib
+from libpathod import utils, test, pathoc, pathod, language
+from netlib import tcp
+import requests
+
+def treader(bytes):
+ """
+ Construct a tcp.Read object from bytes.
+ """
+ fp = cStringIO.StringIO(bytes)
+ return tcp.Reader(fp)
+
+
+class DaemonTests(object):
+ noweb = False
+ noapi = False
+ nohang = False
+ ssl = False
+ timeout = None
+ hexdump = False
+ ssloptions = None
+ nocraft = False
+
+ @classmethod
+ def setup_class(cls):
+ opts = cls.ssloptions or {}
+ cls.confdir = tempfile.mkdtemp()
+ opts["confdir"] = cls.confdir
+ so = pathod.SSLOptions(**opts)
+ cls.d = test.Daemon(
+ staticdir=test_data.path("data"),
+ anchors=[
+ (re.compile("/anchor/.*"), "202:da")
+ ],
+ ssl=cls.ssl,
+ ssloptions=so,
+ sizelimit=1 * 1024 * 1024,
+ noweb=cls.noweb,
+ noapi=cls.noapi,
+ nohang=cls.nohang,
+ timeout=cls.timeout,
+ hexdump=cls.hexdump,
+ nocraft=cls.nocraft,
+ logreq=True,
+ logresp=True,
+ explain=True
+ )
+
+ @classmethod
+ def teardown_class(cls):
+ cls.d.shutdown()
+ shutil.rmtree(cls.confdir)
+
+ def teardown(self):
+ if not (self.noweb or self.noapi):
+ self.d.clear_log()
+
+ def getpath(self, path, params=None):
+ scheme = "https" if self.ssl else "http"
+ resp = requests.get(
+ "%s://localhost:%s/%s" % (
+ scheme,
+ self.d.port,
+ path
+ ),
+ verify=False,
+ params=params
+ )
+ return resp
+
+ def get(self, spec):
+ resp = requests.get(self.d.p(spec), verify=False)
+ return resp
+
+ def pathoc(
+ self,
+ specs,
+ timeout=None,
+ connect_to=None,
+ ssl=None,
+ ws_read_limit=None,
+ use_http2=False,
+ ):
+ """
+ Returns a (messages, text log) tuple.
+ """
+ if ssl is None:
+ ssl = self.ssl
+ logfp = cStringIO.StringIO()
+ c = pathoc.Pathoc(
+ ("localhost", self.d.port),
+ ssl=ssl,
+ ws_read_limit=ws_read_limit,
+ timeout=timeout,
+ fp=logfp,
+ use_http2=use_http2,
+ )
+ c.connect(connect_to)
+ ret = []
+ for i in specs:
+ resp = c.request(i)
+ if resp:
+ ret.append(resp)
+ for frm in c.wait():
+ ret.append(frm)
+ c.stop()
+ return ret, logfp.getvalue()
+
+
+tmpdir = netlib.tutils.tmpdir
+
+raises = netlib.tutils.raises
+
+test_data = utils.Data(__name__)
+
+
+def render(r, settings=language.Settings()):
+ r = r.resolve(settings)
+ s = cStringIO.StringIO()
+ assert language.serve(r, s, settings)
+ return s.getvalue()