From 6b15241358e9c00f1f7c65e371c7a1f5b69e2770 Mon Sep 17 00:00:00 2001 From: Cory Benfield <lukasaoz@gmail.com> Date: Tue, 27 Sep 2016 13:59:37 +0100 Subject: [PATCH 01/60] Prepare for next release cycle. --- HISTORY.rst | 3 +++ hyper/__init__.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 60be8b7d..c45c08ce 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,9 @@ Release History =============== +dev +--- + v0.7.0 (2016-09-27) ------------------- diff --git a/hyper/__init__.py b/hyper/__init__.py index afa88035..99044881 100644 --- a/hyper/__init__.py +++ b/hyper/__init__.py @@ -33,4 +33,4 @@ # Set default logging handler. logging.getLogger(__name__).addHandler(logging.NullHandler()) -__version__ = '0.7.0' +__version__ = '0.8.0dev0' From 506d5fee0dc8851cf7531ad48d65882201997bca Mon Sep 17 00:00:00 2001 From: Cory Benfield <lukasaoz@gmail.com> Date: Tue, 27 Sep 2016 14:03:00 +0100 Subject: [PATCH 02/60] Update cert bundle. --- hyper/certs.pem | 381 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 270 insertions(+), 111 deletions(-) diff --git a/hyper/certs.pem b/hyper/certs.pem index 72a750f9..6b7bccfb 100644 --- a/hyper/certs.pem +++ b/hyper/certs.pem @@ -1703,38 +1703,6 @@ fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= -----END CERTIFICATE----- -# Issuer: CN=IGC/A O=PM/SGDN OU=DCSSI -# Subject: CN=IGC/A O=PM/SGDN OU=DCSSI -# Label: "IGC/A" -# Serial: 245102874772 -# MD5 Fingerprint: 0c:7f:dd:6a:f4:2a:b9:c8:9b:bd:20:7e:a9:db:5c:37 -# SHA1 Fingerprint: 60:d6:89:74:b5:c2:65:9e:8a:0f:c1:88:7c:88:d2:46:69:1b:18:2c -# SHA256 Fingerprint: b9:be:a7:86:0a:96:2e:a3:61:1d:ab:97:ab:6d:a3:e2:1c:10:68:b9:7d:55:57:5e:d0:e1:12:79:c1:1c:89:32 ------BEGIN CERTIFICATE----- -MIIEAjCCAuqgAwIBAgIFORFFEJQwDQYJKoZIhvcNAQEFBQAwgYUxCzAJBgNVBAYT -AkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAMBgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQ -TS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEOMAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG -9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2LmZyMB4XDTAyMTIxMzE0MjkyM1oXDTIw -MTAxNzE0MjkyMlowgYUxCzAJBgNVBAYTAkZSMQ8wDQYDVQQIEwZGcmFuY2UxDjAM -BgNVBAcTBVBhcmlzMRAwDgYDVQQKEwdQTS9TR0ROMQ4wDAYDVQQLEwVEQ1NTSTEO -MAwGA1UEAxMFSUdDL0ExIzAhBgkqhkiG9w0BCQEWFGlnY2FAc2dkbi5wbS5nb3V2 -LmZyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsh/R0GLFMzvABIaI -s9z4iPf930Pfeo2aSVz2TqrMHLmh6yeJ8kbpO0px1R2OLc/mratjUMdUC24SyZA2 -xtgv2pGqaMVy/hcKshd+ebUyiHDKcMCWSo7kVc0dJ5S/znIq7Fz5cyD+vfcuiWe4 -u0dzEvfRNWk68gq5rv9GQkaiv6GFGvm/5P9JhfejcIYyHF2fYPepraX/z9E0+X1b -F8bc1g4oa8Ld8fUzaJ1O/Id8NhLWo4DoQw1VYZTqZDdH6nfK0LJYBcNdfrGoRpAx -Vs5wKpayMLh35nnAvSk7/ZR3TL0gzUEl4C7HG7vupARB0l2tEmqKm0f7yd1GQOGd -PDPQtQIDAQABo3cwdTAPBgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwIBRjAVBgNV -HSAEDjAMMAoGCCqBegF5AQEBMB0GA1UdDgQWBBSjBS8YYFDCiQrdKyFP/45OqDAx -NjAfBgNVHSMEGDAWgBSjBS8YYFDCiQrdKyFP/45OqDAxNjANBgkqhkiG9w0BAQUF -AAOCAQEABdwm2Pp3FURo/C9mOnTgXeQp/wYHE4RKq89toB9RlPhJy3Q2FLwV3duJ -L92PoF189RLrn544pEfMs5bZvpwlqwN+Mw+VgQ39FuCIvjfwbF3QMZsyK10XZZOY -YLxuj7GoPB7ZHPOpJkL5ZB3C55L29B5aqhlSXa/oovdgoPaN8In1buAKBQGVyYsg -Crpa/JosPL3Dt8ldeCUFP1YUmwza+zpI/pdpXsoQhvdOlgQITeywvl3cO45Pwf2a -NjSaTFR+FwNIlQgRHAdvhQh+XU3Endv7rs6y0bO4g2wdsrN58dhwmX7wEwLOXt1R -0982gaEbeC9xs/FZTEYYKKuF0mBWWg== ------END CERTIFICATE----- - # Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication EV RootCA1 # Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication EV RootCA1 # Label: "Security Communication EV RootCA1" @@ -2044,48 +2012,6 @@ h7U/2k3ZIQAw3pDaDtMaSKk+hQsUi4y8QZ5q9w5wwDX3OaJdZtB7WZ+oRxKaJyOk LY4ng5IgodcVf/EuGO70SH8vf/GhGLWhC5SgYiAynB321O+/TIho -----END CERTIFICATE----- -# Issuer: CN=EBG Elektronik Sertifika Hizmet Sağlayıcısı O=EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. -# Subject: CN=EBG Elektronik Sertifika Hizmet Sağlayıcısı O=EBG Bilişim Teknolojileri ve Hizmetleri A.Ş. -# Label: "EBG Elektronik Sertifika Hizmet Sa\xC4\x9Flay\xc4\xb1\x63\xc4\xb1s\xc4\xb1" -# Serial: 5525761995591021570 -# MD5 Fingerprint: 2c:20:26:9d:cb:1a:4a:00:85:b5:b7:5a:ae:c2:01:37 -# SHA1 Fingerprint: 8c:96:ba:eb:dd:2b:07:07:48:ee:30:32:66:a0:f3:98:6e:7c:ae:58 -# SHA256 Fingerprint: 35:ae:5b:dd:d8:f7:ae:63:5c:ff:ba:56:82:a8:f0:0b:95:f4:84:62:c7:10:8e:e9:a0:e5:29:2b:07:4a:af:b2 ------BEGIN CERTIFICATE----- -MIIF5zCCA8+gAwIBAgIITK9zQhyOdAIwDQYJKoZIhvcNAQEFBQAwgYAxODA2BgNV -BAMML0VCRyBFbGVrdHJvbmlrIFNlcnRpZmlrYSBIaXptZXQgU2HEn2xhecSxY8Sx -c8SxMTcwNQYDVQQKDC5FQkcgQmlsacWfaW0gVGVrbm9sb2ppbGVyaSB2ZSBIaXpt -ZXRsZXJpIEEuxZ4uMQswCQYDVQQGEwJUUjAeFw0wNjA4MTcwMDIxMDlaFw0xNjA4 -MTQwMDMxMDlaMIGAMTgwNgYDVQQDDC9FQkcgRWxla3Ryb25payBTZXJ0aWZpa2Eg -SGl6bWV0IFNhxJ9sYXnEsWPEsXPEsTE3MDUGA1UECgwuRUJHIEJpbGnFn2ltIFRl -a25vbG9qaWxlcmkgdmUgSGl6bWV0bGVyaSBBLsWeLjELMAkGA1UEBhMCVFIwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDuoIRh0DpqZhAy2DE4f6en5f2h -4fuXd7hxlugTlkaDT7byX3JWbhNgpQGR4lvFzVcfd2NR/y8927k/qqk153nQ9dAk -tiHq6yOU/im/+4mRDGSaBUorzAzu8T2bgmmkTPiab+ci2hC6X5L8GCcKqKpE+i4s -tPtGmggDg3KriORqcsnlZR9uKg+ds+g75AxuetpX/dfreYteIAbTdgtsApWjluTL -dlHRKJ2hGvxEok3MenaoDT2/F08iiFD9rrbskFBKW5+VQarKD7JK/oCZTqNGFav4 -c0JqwmZ2sQomFd2TkuzbqV9UIlKRcF0T6kjsbgNs2d1s/OsNA/+mgxKb8amTD8Um -TDGyY5lhcucqZJnSuOl14nypqZoaqsNW2xCaPINStnuWt6yHd6i58mcLlEOzrz5z -+kI2sSXFCjEmN1ZnuqMLfdb3ic1nobc6HmZP9qBVFCVMLDMNpkGMvQQxahByCp0O -Lna9XvNRiYuoP1Vzv9s6xiQFlpJIqkuNKgPlV5EQ9GooFW5Hd4RcUXSfGenmHmMW -OeMRFeNYGkS9y8RsZteEBt8w9DeiQyJ50hBs37vmExH8nYQKE3vwO9D8owrXieqW -fo1IhR5kX9tUoqzVegJ5a9KK8GfaZXINFHDk6Y54jzJ0fFfy1tb0Nokb+Clsi7n2 -l9GkLqq+CxnCRelwXQIDAJ3Zo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB -/wQEAwIBBjAdBgNVHQ4EFgQU587GT/wWZ5b6SqMHwQSny2re2kcwHwYDVR0jBBgw -FoAU587GT/wWZ5b6SqMHwQSny2re2kcwDQYJKoZIhvcNAQEFBQADggIBAJuYml2+ -8ygjdsZs93/mQJ7ANtyVDR2tFcU22NU57/IeIl6zgrRdu0waypIN30ckHrMk2pGI -6YNw3ZPX6bqz3xZaPt7gyPvT/Wwp+BVGoGgmzJNSroIBk5DKd8pNSe/iWtkqvTDO -TLKBtjDOWU/aWR1qeqRFsIImgYZ29fUQALjuswnoT4cCB64kXPBfrAowzIpAoHME -wfuJJPaaHFy3PApnNgUIMbOv2AFoKuB4j3TeuFGkjGwgPaL7s9QJ/XvCgKqTbCmY -Iai7FvOpEl90tYeY8pUm3zTvilORiF0alKM/fCL414i6poyWqD1SNGKfAB5UVUJn -xk1Gj7sURT0KlhaOEKGXmdXTMIXM3rRyt7yKPBgpaP3ccQfuJDlq+u2lrDgv+R4Q -DgZxGhBM/nV+/x5XOULK1+EVoVZVWRvRo68R2E7DpSvvkL/A7IITW43WciyTTo9q -Kd+FPNMN4KIYEsxVL0e3p5sC/kH2iExt2qkBR4NkJ2IQgtYSe14DHzSpyZH+r11t -hie3I6p1GMog57AP14kOpmciY/SDQSsGS7tY1dHXt7kQY9iJSrSq3RZj9W6+YKH4 -7ejWkE8axsWgKdOnIaj1Wjz3x0miIZpKlVIglnKaZsv30oZDfCK+lvm9AahH3eU7 -QPl1K5srRmSGjR70j/sHd9DqSaIcjVIUpgqT ------END CERTIFICATE----- - # Issuer: O=certSIGN OU=certSIGN ROOT CA # Subject: O=certSIGN OU=certSIGN ROOT CA # Label: "certSIGN ROOT CA" @@ -2424,43 +2350,6 @@ Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ== -----END CERTIFICATE----- -# Issuer: CN=Juur-SK O=AS Sertifitseerimiskeskus -# Subject: CN=Juur-SK O=AS Sertifitseerimiskeskus -# Label: "Juur-SK" -# Serial: 999181308 -# MD5 Fingerprint: aa:8e:5d:d9:f8:db:0a:58:b7:8d:26:87:6c:82:35:55 -# SHA1 Fingerprint: 40:9d:4b:d9:17:b5:5c:27:b6:9b:64:cb:98:22:44:0d:cd:09:b8:89 -# SHA256 Fingerprint: ec:c3:e9:c3:40:75:03:be:e0:91:aa:95:2f:41:34:8f:f8:8b:aa:86:3b:22:64:be:fa:c8:07:90:15:74:e9:39 ------BEGIN CERTIFICATE----- -MIIE5jCCA86gAwIBAgIEO45L/DANBgkqhkiG9w0BAQUFADBdMRgwFgYJKoZIhvcN -AQkBFglwa2lAc2suZWUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKExlBUyBTZXJ0aWZp -dHNlZXJpbWlza2Vza3VzMRAwDgYDVQQDEwdKdXVyLVNLMB4XDTAxMDgzMDE0MjMw -MVoXDTE2MDgyNjE0MjMwMVowXTEYMBYGCSqGSIb3DQEJARYJcGtpQHNrLmVlMQsw -CQYDVQQGEwJFRTEiMCAGA1UEChMZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1czEQ -MA4GA1UEAxMHSnV1ci1TSzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AIFxNj4zB9bjMI0TfncyRsvPGbJgMUaXhvSYRqTCZUXP00B841oiqBB4M8yIsdOB -SvZiF3tfTQou0M+LI+5PAk676w7KvRhj6IAcjeEcjT3g/1tf6mTll+g/mX8MCgkz -ABpTpyHhOEvWgxutr2TC+Rx6jGZITWYfGAriPrsfB2WThbkasLnE+w0R9vXW+RvH -LCu3GFH+4Hv2qEivbDtPL+/40UceJlfwUR0zlv/vWT3aTdEVNMfqPxZIe5EcgEMP -PbgFPtGzlc3Yyg/CQ2fbt5PgIoIuvvVoKIO5wTtpeyDaTpxt4brNj3pssAki14sL -2xzVWiZbDcDq5WDQn/413z8CAwEAAaOCAawwggGoMA8GA1UdEwEB/wQFMAMBAf8w -ggEWBgNVHSAEggENMIIBCTCCAQUGCisGAQQBzh8BAQEwgfYwgdAGCCsGAQUFBwIC -MIHDHoHAAFMAZQBlACAAcwBlAHIAdABpAGYAaQBrAGEAYQB0ACAAbwBuACAAdgDk -AGwAagBhAHMAdABhAHQAdQBkACAAQQBTAC0AaQBzACAAUwBlAHIAdABpAGYAaQB0 -AHMAZQBlAHIAaQBtAGkAcwBrAGUAcwBrAHUAcwAgAGEAbABhAG0ALQBTAEsAIABz -AGUAcgB0AGkAZgBpAGsAYQBhAHQAaQBkAGUAIABrAGkAbgBuAGkAdABhAG0AaQBz -AGUAawBzMCEGCCsGAQUFBwIBFhVodHRwOi8vd3d3LnNrLmVlL2Nwcy8wKwYDVR0f -BCQwIjAgoB6gHIYaaHR0cDovL3d3dy5zay5lZS9qdXVyL2NybC8wHQYDVR0OBBYE -FASqekej5ImvGs8KQKcYP2/v6X2+MB8GA1UdIwQYMBaAFASqekej5ImvGs8KQKcY -P2/v6X2+MA4GA1UdDwEB/wQEAwIB5jANBgkqhkiG9w0BAQUFAAOCAQEAe8EYlFOi -CfP+JmeaUOTDBS8rNXiRTHyoERF5TElZrMj3hWVcRrs7EKACr81Ptcw2Kuxd/u+g -kcm2k298gFTsxwhwDY77guwqYHhpNjbRxZyLabVAyJRld/JXIWY7zoVAtjNjGr95 -HvxcHdMdkxuLDF2FvZkwMhgJkVLpfKG6/2SSmuz+Ne6ML678IIbsSt4beDI3poHS -na9aEhbKmVv8b20OxaAehsmR0FyYgl9jDIpaq9iVpszLita/ZEuOyoqysOkhMp6q -qIWYNIE5ITuoOlIyPfZrN4YGWhWY3PARZv40ILcD9EEQfTmEeZZyY7aWAuVrua0Z -TbvGRNs2yyqcjg== ------END CERTIFICATE----- - # Issuer: CN=Hongkong Post Root CA 1 O=Hongkong Post # Subject: CN=Hongkong Post Root CA 1 O=Hongkong Post # Label: "Hongkong Post Root CA 1" @@ -5114,3 +5003,273 @@ XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P 5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi DrW5viSP -----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce +# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6 +# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36 +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef +# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66 +# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33 +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +# Issuer: CN=Certplus Root CA G1 O=Certplus +# Subject: CN=Certplus Root CA G1 O=Certplus +# Label: "Certplus Root CA G1" +# Serial: 1491911565779898356709731176965615564637713 +# MD5 Fingerprint: 7f:09:9c:f7:d9:b9:5c:69:69:56:d5:37:3e:14:0d:42 +# SHA1 Fingerprint: 22:fd:d0:b7:fd:a2:4e:0d:ac:49:2c:a0:ac:a6:7b:6a:1f:e3:f7:66 +# SHA256 Fingerprint: 15:2a:40:2b:fc:df:2c:d5:48:05:4d:22:75:b3:9c:7f:ca:3e:c0:97:80:78:b0:f0:ea:76:e5:61:a6:c7:43:3e +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgISESBVg+QtPlRWhS2DN7cs3EYRMA0GCSqGSIb3DQEBDQUA +MD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy +dHBsdXMgUm9vdCBDQSBHMTAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBa +MD4xCzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2Vy +dHBsdXMgUm9vdCBDQSBHMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ANpQh7bauKk+nWT6VjOaVj0W5QOVsjQcmm1iBdTYj+eJZJ+622SLZOZ5KmHNr49a +iZFluVj8tANfkT8tEBXgfs+8/H9DZ6itXjYj2JizTfNDnjl8KvzsiNWI7nC9hRYt +6kuJPKNxQv4c/dMcLRC4hlTqQ7jbxofaqK6AJc96Jh2qkbBIb6613p7Y1/oA/caP +0FG7Yn2ksYyy/yARujVjBYZHYEMzkPZHogNPlk2dT8Hq6pyi/jQu3rfKG3akt62f +6ajUeD94/vI4CTYd0hYCyOwqaK/1jpTvLRN6HkJKHRUxrgwEV/xhc/MxVoYxgKDE +EW4wduOU8F8ExKyHcomYxZ3MVwia9Az8fXoFOvpHgDm2z4QTd28n6v+WZxcIbekN +1iNQMLAVdBM+5S//Ds3EC0pd8NgAM0lm66EYfFkuPSi5YXHLtaW6uOrc4nBvCGrc +h2c0798wct3zyT8j/zXhviEpIDCB5BmlIOklynMxdCm+4kLV87ImZsdo/Rmz5yCT +mehd4F6H50boJZwKKSTUzViGUkAksnsPmBIgJPaQbEfIDbsYIC7Z/fyL8inqh3SV +4EJQeIQEQWGw9CEjjy3LKCHyamz0GqbFFLQ3ZU+V/YDI+HLlJWvEYLF7bY5KinPO +WftwenMGE9nTdDckQQoRb5fc5+R+ob0V8rqHDz1oihYHAgMBAAGjYzBhMA4GA1Ud +DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSowcCbkahDFXxd +Bie0KlHYlwuBsTAfBgNVHSMEGDAWgBSowcCbkahDFXxdBie0KlHYlwuBsTANBgkq +hkiG9w0BAQ0FAAOCAgEAnFZvAX7RvUz1isbwJh/k4DgYzDLDKTudQSk0YcbX8ACh +66Ryj5QXvBMsdbRX7gp8CXrc1cqh0DQT+Hern+X+2B50ioUHj3/MeXrKls3N/U/7 +/SMNkPX0XtPGYX2eEeAC7gkE2Qfdpoq3DIMku4NQkv5gdRE+2J2winq14J2by5BS +S7CTKtQ+FjPlnsZlFT5kOwQ/2wyPX1wdaR+v8+khjPPvl/aatxm2hHSco1S1cE5j +2FddUyGbQJJD+tZ3VTNPZNX70Cxqjm0lpu+F6ALEUz65noe8zDUa3qHpimOHZR4R +Kttjd5cUvpoUmRGywO6wT/gUITJDT5+rosuoD6o7BlXGEilXCNQ314cnrUlZp5Gr +RHpejXDbl85IULFzk/bwg2D5zfHhMf1bfHEhYxQUqq/F3pN+aLHsIqKqkHWetUNy +6mSjhEv9DKgma3GX7lZjZuhCVPnHHd/Qj1vfyDBviP4NxDMcU6ij/UgQ8uQKTuEV +V/xuZDDCVRHc6qnNSlSsKWNEz0pAoNZoWRsz+e86i9sgktxChL8Bq4fA1SCC28a5 +g4VCXA9DO2pJNdWY9BW/+mGBDAkgGNLQFwzLSABQ6XaCjGTXOqAHVcweMcDvOrRl +++O/QmueD6i9a5jc2NvLi6Td11n0bt3+qsOR0C5CB8AMTVPNJLFMWx5R9N/pkvo= +-----END CERTIFICATE----- + +# Issuer: CN=Certplus Root CA G2 O=Certplus +# Subject: CN=Certplus Root CA G2 O=Certplus +# Label: "Certplus Root CA G2" +# Serial: 1492087096131536844209563509228951875861589 +# MD5 Fingerprint: a7:ee:c4:78:2d:1b:ee:2d:b9:29:ce:d6:a7:96:32:31 +# SHA1 Fingerprint: 4f:65:8e:1f:e9:06:d8:28:02:e9:54:47:41:c9:54:25:5d:69:cc:1a +# SHA256 Fingerprint: 6c:c0:50:41:e6:44:5e:74:69:6c:4c:fb:c9:f8:0f:54:3b:7e:ab:bb:44:b4:ce:6f:78:7c:6a:99:71:c4:2f:17 +-----BEGIN CERTIFICATE----- +MIICHDCCAaKgAwIBAgISESDZkc6uo+jF5//pAq/Pc7xVMAoGCCqGSM49BAMDMD4x +CzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs +dXMgUm9vdCBDQSBHMjAeFw0xNDA1MjYwMDAwMDBaFw0zODAxMTUwMDAwMDBaMD4x +CzAJBgNVBAYTAkZSMREwDwYDVQQKDAhDZXJ0cGx1czEcMBoGA1UEAwwTQ2VydHBs +dXMgUm9vdCBDQSBHMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABM0PW1aC3/BFGtat +93nwHcmsltaeTpwftEIRyoa/bfuFo8XlGVzX7qY/aWfYeOKmycTbLXku54uNAm8x +Ik0G42ByRZ0OQneezs/lf4WbGOT8zC5y0xaTTsqZY1yhBSpsBqNjMGEwDgYDVR0P +AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNqDYwJ5jtpMxjwj +FNiPwyCrKGBZMB8GA1UdIwQYMBaAFNqDYwJ5jtpMxjwjFNiPwyCrKGBZMAoGCCqG +SM49BAMDA2gAMGUCMHD+sAvZ94OX7PNVHdTcswYO/jOYnYs5kGuUIe22113WTNch +p+e/IQ8rzfcq3IUHnQIxAIYUFuXcsGXCwI4Un78kFmjlvPl5adytRSv3tjFzzAal +U5ORGpOucGpnutee5WEaXw== +-----END CERTIFICATE----- + +# Issuer: CN=OpenTrust Root CA G1 O=OpenTrust +# Subject: CN=OpenTrust Root CA G1 O=OpenTrust +# Label: "OpenTrust Root CA G1" +# Serial: 1492036577811947013770400127034825178844775 +# MD5 Fingerprint: 76:00:cc:81:29:cd:55:5e:88:6a:7a:2e:f7:4d:39:da +# SHA1 Fingerprint: 79:91:e8:34:f7:e2:ee:dd:08:95:01:52:e9:55:2d:14:e9:58:d5:7e +# SHA256 Fingerprint: 56:c7:71:28:d9:8c:18:d9:1b:4c:fd:ff:bc:25:ee:91:03:d4:75:8e:a2:ab:ad:82:6a:90:f3:45:7d:46:0e:b4 +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgISESCzkFU5fX82bWTCp59rY45nMA0GCSqGSIb3DQEBCwUA +MEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w +ZW5UcnVzdCBSb290IENBIEcxMB4XDTE0MDUyNjA4NDU1MFoXDTM4MDExNTAwMDAw +MFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU +T3BlblRydXN0IFJvb3QgQ0EgRzEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQD4eUbalsUwXopxAy1wpLuwxQjczeY1wICkES3d5oeuXT2R0odsN7faYp6b +wiTXj/HbpqbfRm9RpnHLPhsxZ2L3EVs0J9V5ToybWL0iEA1cJwzdMOWo010hOHQX +/uMftk87ay3bfWAfjH1MBcLrARYVmBSO0ZB3Ij/swjm4eTrwSSTilZHcYTSSjFR0 +77F9jAHiOH3BX2pfJLKOYheteSCtqx234LSWSE9mQxAGFiQD4eCcjsZGT44ameGP +uY4zbGneWK2gDqdkVBFpRGZPTBKnjix9xNRbxQA0MMHZmf4yzgeEtE7NCv82TWLx +p2NX5Ntqp66/K7nJ5rInieV+mhxNaMbBGN4zK1FGSxyO9z0M+Yo0FMT7MzUj8czx +Kselu7Cizv5Ta01BG2Yospb6p64KTrk5M0ScdMGTHPjgniQlQ/GbI4Kq3ywgsNw2 +TgOzfALU5nsaqocTvz6hdLubDuHAk5/XpGbKuxs74zD0M1mKB3IDVedzagMxbm+W +G+Oin6+Sx+31QrclTDsTBM8clq8cIqPQqwWyTBIjUtz9GVsnnB47ev1CI9sjgBPw +vFEVVJSmdz7QdFG9URQIOTfLHzSpMJ1ShC5VkLG631UAC9hWLbFJSXKAqWLXwPYY +EQRVzXR7z2FwefR7LFxckvzluFqrTJOVoSfupb7PcSNCupt2LQIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUl0YhVyE1 +2jZVx/PxN3DlCPaTKbYwHwYDVR0jBBgwFoAUl0YhVyE12jZVx/PxN3DlCPaTKbYw +DQYJKoZIhvcNAQELBQADggIBAB3dAmB84DWn5ph76kTOZ0BP8pNuZtQ5iSas000E +PLuHIT839HEl2ku6q5aCgZG27dmxpGWX4m9kWaSW7mDKHyP7Rbr/jyTwyqkxf3kf +gLMtMrpkZ2CvuVnN35pJ06iCsfmYlIrM4LvgBBuZYLFGZdwIorJGnkSI6pN+VxbS +FXJfLkur1J1juONI5f6ELlgKn0Md/rcYkoZDSw6cMoYsYPXpSOqV7XAp8dUv/TW0 +V8/bhUiZucJvbI/NeJWsZCj9VrDDb8O+WVLhX4SPgPL0DTatdrOjteFkdjpY3H1P +XlZs5VVZV6Xf8YpmMIzUUmI4d7S+KNfKNsSbBfD4Fdvb8e80nR14SohWZ25g/4/I +i+GOvUKpMwpZQhISKvqxnUOOBZuZ2mKtVzazHbYNeS2WuOvyDEsMpZTGMKcmGS3t +TAZQMPH9WD25SxdfGbRqhFS0OE85og2WaMMolP3tLR9Ka0OWLpABEPs4poEL0L91 +09S5zvE/bw4cHjdx5RiHdRk/ULlepEU0rbDK5uUTdg8xFKmOLZTW1YVNcxVPS/Ky +Pu1svf0OnWZzsD2097+o4BGkxK51CUpjAEggpsadCwmKtODmzj7HPiY46SvepghJ +AwSQiumPv+i2tCqjI40cHLI5kqiPAlxAOXXUc0ECd97N4EOH1uS6SsNsEn/+KuYj +1oxx +-----END CERTIFICATE----- + +# Issuer: CN=OpenTrust Root CA G2 O=OpenTrust +# Subject: CN=OpenTrust Root CA G2 O=OpenTrust +# Label: "OpenTrust Root CA G2" +# Serial: 1492012448042702096986875987676935573415441 +# MD5 Fingerprint: 57:24:b6:59:24:6b:ae:c8:fe:1c:0c:20:f2:c0:4e:eb +# SHA1 Fingerprint: 79:5f:88:60:c5:ab:7c:3d:92:e6:cb:f4:8d:e1:45:cd:11:ef:60:0b +# SHA256 Fingerprint: 27:99:58:29:fe:6a:75:15:c1:bf:e8:48:f9:c4:76:1d:b1:6c:22:59:29:25:7b:f4:0d:08:94:f2:9e:a8:ba:f2 +-----BEGIN CERTIFICATE----- +MIIFbzCCA1egAwIBAgISESChaRu/vbm9UpaPI+hIvyYRMA0GCSqGSIb3DQEBDQUA +MEAxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9w +ZW5UcnVzdCBSb290IENBIEcyMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAw +MFowQDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwU +T3BlblRydXN0IFJvb3QgQ0EgRzIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQDMtlelM5QQgTJT32F+D3Y5z1zCU3UdSXqWON2ic2rxb95eolq5cSG+Ntmh +/LzubKh8NBpxGuga2F8ORAbtp+Dz0mEL4DKiltE48MLaARf85KxP6O6JHnSrT78e +CbY2albz4e6WiWYkBuTNQjpK3eCasMSCRbP+yatcfD7J6xcvDH1urqWPyKwlCm/6 +1UWY0jUJ9gNDlP7ZvyCVeYCYitmJNbtRG6Q3ffyZO6v/v6wNj0OxmXsWEH4db0fE +FY8ElggGQgT4hNYdvJGmQr5J1WqIP7wtUdGejeBSzFfdNTVY27SPJIjki9/ca1TS +gSuyzpJLHB9G+h3Ykst2Z7UJmQnlrBcUVXDGPKBWCgOz3GIZ38i1MH/1PCZ1Eb3X +G7OHngevZXHloM8apwkQHZOJZlvoPGIytbU6bumFAYueQ4xncyhZW+vj3CzMpSZy +YhK05pyDRPZRpOLAeiRXyg6lPzq1O4vldu5w5pLeFlwoW5cZJ5L+epJUzpM5ChaH +vGOz9bGTXOBut9Dq+WIyiET7vycotjCVXRIouZW+j1MY5aIYFuJWpLIsEPUdN6b4 +t/bQWVyJ98LVtZR00dX+G7bw5tYee9I8y6jj9RjzIR9u701oBnstXW5DiabA+aC/ +gh7PU3+06yzbXfZqfUAkBXKJOAGTy3HCOV0GEfZvePg3DTmEJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUajn6QiL3 +5okATV59M4PLuG53hq8wHwYDVR0jBBgwFoAUajn6QiL35okATV59M4PLuG53hq8w +DQYJKoZIhvcNAQENBQADggIBAJjLq0A85TMCl38th6aP1F5Kr7ge57tx+4BkJamz +Gj5oXScmp7oq4fBXgwpkTx4idBvpkF/wrM//T2h6OKQQbA2xx6R3gBi2oihEdqc0 +nXGEL8pZ0keImUEiyTCYYW49qKgFbdEfwFFEVn8nNQLdXpgKQuswv42hm1GqO+qT +RmTFAHneIWv2V6CG1wZy7HBGS4tz3aAhdT7cHcCP009zHIXZ/n9iyJVvttN7jLpT +wm+bREx50B1ws9efAvSyB7DH5fitIw6mVskpEndI2S9G/Tvw/HRwkqWOOAgfZDC2 +t0v7NqwQjqBSM2OdAzVWxWm9xiNaJ5T2pBL4LTM8oValX9YZ6e18CL13zSdkzJTa +TkZQh+D5wVOAHrut+0dSixv9ovneDiK3PTNZbNTe9ZUGMg1RGUFcPk8G97krgCf2 +o6p6fAbhQ8MTOWIaNr3gKC6UAuQpLmBVrkA9sHSSXvAgZJY/X0VdiLWK2gKgW0VU +3jg9CcCoSmVGFvyqv1ROTVu+OEO3KMqLM6oaJbolXCkvW0pujOotnCr2BXbgd5eA +iN1nE28daCSLT7d0geX0YJ96Vdc+N9oWaz53rK4YcJUIeSkDiv7BO7M/Gg+kO14f +WKGVyasvc0rQLW6aWQ9VGHgtPFGml4vmu7JwqkwR3v98KzfUetF3NI/n+UL3PIEM +S1IK +-----END CERTIFICATE----- + +# Issuer: CN=OpenTrust Root CA G3 O=OpenTrust +# Subject: CN=OpenTrust Root CA G3 O=OpenTrust +# Label: "OpenTrust Root CA G3" +# Serial: 1492104908271485653071219941864171170455615 +# MD5 Fingerprint: 21:37:b4:17:16:92:7b:67:46:70:a9:96:d7:a8:13:24 +# SHA1 Fingerprint: 6e:26:64:f3:56:bf:34:55:bf:d1:93:3f:7c:01:de:d8:13:da:8a:a6 +# SHA256 Fingerprint: b7:c3:62:31:70:6e:81:07:8c:36:7c:b8:96:19:8f:1e:32:08:dd:92:69:49:dd:8f:57:09:a4:10:f7:5b:62:92 +-----BEGIN CERTIFICATE----- +MIICITCCAaagAwIBAgISESDm+Ez8JLC+BUCs2oMbNGA/MAoGCCqGSM49BAMDMEAx +CzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlPcGVuVHJ1c3QxHTAbBgNVBAMMFE9wZW5U +cnVzdCBSb290IENBIEczMB4XDTE0MDUyNjAwMDAwMFoXDTM4MDExNTAwMDAwMFow +QDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCU9wZW5UcnVzdDEdMBsGA1UEAwwUT3Bl +blRydXN0IFJvb3QgQ0EgRzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARK7liuTcpm +3gY6oxH84Bjwbhy6LTAMidnW7ptzg6kjFYwvWYpa3RTqnVkrQ7cG7DK2uu5Bta1d +oYXM6h0UZqNnfkbilPPntlahFVmhTzeXuSIevRHr9LIfXsMUmuXZl5mjYzBhMA4G +A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRHd8MUi2I5 +DMlv4VBN0BBY3JWIbTAfBgNVHSMEGDAWgBRHd8MUi2I5DMlv4VBN0BBY3JWIbTAK +BggqhkjOPQQDAwNpADBmAjEAj6jcnboMBBf6Fek9LykBl7+BFjNAk2z8+e2AcG+q +j9uEwov1NcoG3GRvaBbhj5G5AjEA2Euly8LQCGzpGPta3U1fJAuwACEl74+nBCZx +4nxp5V2a+EEfOzmTk51V6s2N8fvB +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X1 O=Internet Security Research Group +# Subject: CN=ISRG Root X1 O=Internet Security Research Group +# Label: "ISRG Root X1" +# Serial: 172886928669790476064670243504169061120 +# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e +# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 +# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- From cc033e139db314121b8068d50c1dfc8b748fd521 Mon Sep 17 00:00:00 2001 From: Cory Benfield <lukasaoz@gmail.com> Date: Wed, 28 Sep 2016 09:57:02 +0100 Subject: [PATCH 03/60] Use HTTPS for quickstart example. --- docs/source/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 8f9502ee..6ad44ec1 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -99,7 +99,7 @@ the response from any of them, and switch between them using their stream IDs. For example:: >>> from hyper import HTTPConnection - >>> c = HTTPConnection('http2bin.org') + >>> c = HTTPConnection('http2bin.org', port=443) >>> first = c.request('GET', '/get', headers={'key': 'value'}) >>> second = c.request('POST', '/post', body=b'hello') >>> third = c.request('GET', '/ip') From fdf698e5cccd2ccf63711f1ef4841aac1f1d636a Mon Sep 17 00:00:00 2001 From: Cory Benfield <lukasaoz@gmail.com> Date: Fri, 2 Dec 2016 10:55:35 +0000 Subject: [PATCH 04/60] Pin out hyper-h2 2.5.0. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0d36afaa..cedea1e4 100644 --- a/setup.py +++ b/setup.py @@ -78,7 +78,7 @@ def run_tests(self): 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', ], - install_requires=['h2>=2.4,<3.0', 'hyperframe>=3.2,<4.0'], + install_requires=['h2>=2.4,<3.0,!=2.5.0', 'hyperframe>=3.2,<4.0'], tests_require=['pytest', 'requests', 'mock'], cmdclass={'test': PyTest}, entry_points={ From 30413013b09b90de63b3bd6e375ae47b0d63ec1a Mon Sep 17 00:00:00 2001 From: Cory Benfield <lukasaoz@gmail.com> Date: Fri, 2 Dec 2016 11:12:39 +0000 Subject: [PATCH 05/60] Skip import tests on wrong platforms. --- test/test_import.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_import.py b/test/test_import.py index b8e3a1f2..9da32bd5 100644 --- a/test/test_import.py +++ b/test/test_import.py @@ -5,6 +5,7 @@ class TestImportPython2(object): + @pytest.mark.skipif(sys.version_info[0] == 3, reason="Python 2 only") def test_cannot_import_python_2(self, monkeypatch): monkeypatch.setattr(sys, 'version_info', (2, 6, 5, 'final', 0)) with pytest.raises(ImportError): @@ -12,6 +13,7 @@ def test_cannot_import_python_2(self, monkeypatch): class TestImportPython3(object): + @pytest.mark.skipif(sys.version_info[0] == 2, reason="Python 3 only") def test_cannot_import_python_32(self, monkeypatch): monkeypatch.setattr(sys, 'version_info', (3, 2, 3, 'final', 0)) with pytest.raises(ImportError): From 3475cc2808250fb3b5715c04106ddaa494a51e10 Mon Sep 17 00:00:00 2001 From: Cory Benfield <lukasaoz@gmail.com> Date: Fri, 2 Dec 2016 11:21:54 +0000 Subject: [PATCH 06/60] Extra space for flake8 --- hyper/ssl_compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hyper/ssl_compat.py b/hyper/ssl_compat.py index 976b6235..71ebcd3a 100644 --- a/hyper/ssl_compat.py +++ b/hyper/ssl_compat.py @@ -48,6 +48,7 @@ def inner(self, *args, **kwargs): return getattr(self._conn, method)(*args, **kwargs) return inner + # Referenced in hyper/http20/connection.py. These values come # from the python ssl package, and must be defined in this file # for hyper to work in python versions <2.7.9 From e76f185207b4746052b1519cad13238c35195798 Mon Sep 17 00:00:00 2001 From: Nate Prewitt <Nate.Prewitt@gmail.com> Date: Fri, 2 Dec 2016 08:34:45 -0700 Subject: [PATCH 07/60] adding HTTP version flags to connections and responses --- hyper/common/util.py | 10 ++++++++++ hyper/http11/connection.py | 5 ++++- hyper/http11/response.py | 4 ++++ hyper/http20/connection.py | 7 ++++++- hyper/http20/response.py | 4 ++++ setup.py | 3 ++- test/test_http11.py | 13 +++++++++++++ test/test_hyper.py | 10 +++++++++- 8 files changed, 52 insertions(+), 4 deletions(-) diff --git a/hyper/common/util.py b/hyper/common/util.py index 6d199a0c..a2278b54 100644 --- a/hyper/common/util.py +++ b/hyper/common/util.py @@ -5,6 +5,8 @@ General utility functions for use with hyper. """ +from enum import Enum + from hyper.compat import unicode, bytes, imap from ..packages.rfc3986.uri import URIReference from ..compat import is_py3 @@ -57,3 +59,11 @@ def to_native_string(string, encoding='utf-8'): return string return string.decode(encoding) if is_py3 else string.encode(encoding) + + +class HTTPVersion(Enum): + """ + Collection of all HTTP versions used in hyper. + """ + http11 = "HTTP/1.1" + http20 = "HTTP/2" diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 61361c35..48bde2f0 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -20,7 +20,7 @@ from ..common.bufsocket import BufferedSocket from ..common.exceptions import TLSUpgrade, HTTPUpgrade from ..common.headers import HTTPHeaderMap -from ..common.util import to_bytestring, to_host_port_tuple +from ..common.util import to_bytestring, to_host_port_tuple, HTTPVersion from ..compat import bytes # We prefer pycohttpparser to the pure-Python interpretation @@ -56,6 +56,9 @@ class HTTP11Connection(object): and one also isn't provided in the ``proxy`` parameter, defaults to 8080. """ + + version = HTTPVersion.http11 + def __init__(self, host, port=None, secure=None, ssl_context=None, proxy_host=None, proxy_port=None, **kwargs): if port is None: diff --git a/hyper/http11/response.py b/hyper/http11/response.py index ee23be08..318ab659 100644 --- a/hyper/http11/response.py +++ b/hyper/http11/response.py @@ -13,6 +13,7 @@ from ..common.decoder import DeflateDecoder from ..common.exceptions import ChunkedDecodeError, InvalidResponseError from ..common.exceptions import ConnectionResetError +from ..common.util import HTTPVersion log = logging.getLogger(__name__) @@ -23,6 +24,9 @@ class HTTP11Response(object): provides access to the response headers and the entity body. The response is an iterable object and can be used in a with statement. """ + + version = HTTPVersion.http11 + def __init__(self, code, reason, headers, sock, connection=None): #: The reason phrase returned by the server. self.reason = reason diff --git a/hyper/http20/connection.py b/hyper/http20/connection.py index 31dc7a47..b8a2901e 100644 --- a/hyper/http20/connection.py +++ b/hyper/http20/connection.py @@ -14,7 +14,9 @@ from ..common.exceptions import ConnectionResetError from ..common.bufsocket import BufferedSocket from ..common.headers import HTTPHeaderMap -from ..common.util import to_host_port_tuple, to_native_string, to_bytestring +from ..common.util import ( + to_host_port_tuple, to_native_string, to_bytestring, HTTPVersion +) from ..compat import unicode, bytes from .stream import Stream from .response import HTTP20Response, HTTP20Push @@ -91,6 +93,9 @@ class HTTP20Connection(object): and one also isn't provided in the ``proxy`` parameter, defaults to 8080. """ + + version = HTTPVersion.http20 + def __init__(self, host, port=None, secure=None, window_manager=None, enable_push=False, ssl_context=None, proxy_host=None, proxy_port=None, force_proto=None, **kwargs): diff --git a/hyper/http20/response.py b/hyper/http20/response.py index bb339b2f..94b20f9a 100644 --- a/hyper/http20/response.py +++ b/hyper/http20/response.py @@ -11,6 +11,7 @@ from ..common.decoder import DeflateDecoder from ..common.headers import HTTPHeaderMap +from ..common.util import HTTPVersion log = logging.getLogger(__name__) @@ -36,6 +37,9 @@ class HTTP20Response(object): the persistent connections used in HTTP/2 this has no effect, and is done soley for compatibility). """ + + version = HTTPVersion.http20 + def __init__(self, headers, stream): #: The reason phrase returned by the server. This is not used in #: HTTP/2, and so is always the empty string. diff --git a/setup.py b/setup.py index cedea1e4..861c883a 100644 --- a/setup.py +++ b/setup.py @@ -96,6 +96,7 @@ def run_tests(self): # module at lower than 1.0, because it doesn't support CFFI v1.0 yet. ':platform_python_implementation == "PyPy" and python_full_version < "2.7.9"': [ 'cryptography<1.0' - ] + ], + ':python_version == "2.7" or python_version == "3.3"': ['enum34>=1.0.4, <2'] } ) diff --git a/test/test_http11.py b/test/test_http11.py index d4039588..5f0fcf7a 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -19,6 +19,7 @@ from hyper.http11.response import HTTP11Response from hyper.common.headers import HTTPHeaderMap from hyper.common.exceptions import ChunkedDecodeError, ConnectionResetError +from hyper.common.util import HTTPVersion from hyper.compat import bytes, zlib_compressobj @@ -838,6 +839,18 @@ def test_closing_chunked_reads_dont_call_close_callback(self): assert r._sock is None assert connection.close.call_count == 1 + def test_connection_version(self): + c = HTTP11Connection('httpbin.org') + assert c.version is HTTPVersion.http11 + + def test_response_version(self): + d = DummySocket() + headers = { + b'transfer-encoding': [b'chunked'], b'connection': [b'close'] + } + r = HTTP11Response(200, 'OK', headers, d) + assert r.version is HTTPVersion.http11 + class DummySocket(object): def __init__(self): diff --git a/test/test_hyper.py b/test/test_hyper.py index da0aad57..59d354c7 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -16,7 +16,7 @@ combine_repeated_headers, split_repeated_headers, h2_safe_headers ) from hyper.common.headers import HTTPHeaderMap -from hyper.common.util import to_bytestring +from hyper.common.util import to_bytestring, HTTPVersion from hyper.compat import zlib_compressobj, is_py2 from hyper.contrib import HTTP20Adapter import hyper.http20.errors as errors @@ -82,6 +82,10 @@ def test_connections_can_parse_ipv6_hosts_and_ports(self): assert c.proxy_host == 'ffff:aaaa::1' assert c.proxy_port == 8443 + def test_connection_version(self): + c = HTTP20Connection('www.google.com') + assert c.version is HTTPVersion.http20 + def test_ping(self, frame_buffer): def data_callback(chunk, **kwargs): frame_buffer.add_data(chunk) @@ -1097,6 +1101,10 @@ def test_read_compressed_frames(self): assert received == b'this is test data' + def test_response_version(self): + r = HTTP20Response(HTTPHeaderMap([(':status', '200')]), None) + assert r.version is HTTPVersion.http20 + class TestHTTP20Adapter(object): def test_adapter_reuses_connections(self): From f91f2cad8362ce2f21c86561269ef1ff1abae817 Mon Sep 17 00:00:00 2001 From: Cory Benfield <lukasaoz@gmail.com> Date: Wed, 7 Dec 2016 10:19:36 +0000 Subject: [PATCH 08/60] Stop installing/testing nghttp2. --- .travis.yml | 4 ---- .travis/install.sh | 39 --------------------------------------- 2 files changed, 43 deletions(-) diff --git a/.travis.yml b/.travis.yml index e6eb45d4..92512b96 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,15 +11,11 @@ env: - TEST_RELEASE=false HYPER_FAST_PARSE=true - TEST_RELEASE=true HYPER_FAST_PARSE=false - TEST_RELEASE=true HYPER_FAST_PARSE=true - - NGHTTP2=true matrix: allow_failures: - env: TEST_RELEASE=true HYPER_FAST_PARSE=true - env: TEST_RELEASE=true HYPER_FAST_PARSE=false - exclude: - - env: NGHTTP2=true - python: "pypy-5.3.1" install: - ".travis/install.sh" diff --git a/.travis/install.sh b/.travis/install.sh index d446b38a..d7423ce7 100755 --- a/.travis/install.sh +++ b/.travis/install.sh @@ -3,45 +3,6 @@ set -e set -x -if [[ "$NGHTTP2" = true ]]; then - # GCC 4.6 seems to cause problems, so go straight to 4.8. - sudo add-apt-repository --yes ppa:ubuntu-toolchain-r/test - sudo apt-get update - sudo apt-get install g++-4.8 libstdc++-4.8-dev - export CXX="g++-4.8" CC="gcc-4.8" - $CC --version - - # Install nghttp2. Right now I haven't built a PPA for this so we have to - # do it from source, which kinda sucks. First, install a ton of - # prerequisite packages. - sudo apt-get install autoconf automake autotools-dev libtool pkg-config \ - zlib1g-dev libcunit1-dev libssl-dev libxml2-dev \ - libevent-dev libjansson-dev libjemalloc-dev - pip install cython - - # Now, download and install nghttp2's latest version. - git clone https://github.com/tatsuhiro-t/nghttp2.git - cd nghttp2 - DIR=`pwd` - export PYTHONPATH="$DIR/lib/python${TRAVIS_PYTHON_VERSION}/site-packages" - mkdir -p $PYTHONPATH - autoreconf -i - automake - autoconf - ./configure --disable-threads --prefix=`pwd` - make - make install - - # The makefile doesn't install into the active virtualenv. Install again. - cd python - python setup.py install - cd ../.. - - # Let's try ldconfig. - sudo sh -c 'echo "/usr/local/lib" > /etc/ld.so.conf.d/libnghttp2.conf' - sudo ldconfig -fi - if [[ "$HYPER_FAST_PARSE" = true ]]; then pip install pycohttpparser~=1.0 fi From cced203bf4c7c4ac81007134d2a3b4da5daae8e9 Mon Sep 17 00:00:00 2001 From: Cory Benfield <lukasaoz@gmail.com> Date: Wed, 7 Dec 2016 10:20:20 +0000 Subject: [PATCH 09/60] Don't pass tests if coverage succeeds but tests fail. --- .travis.yml | 12 +----------- .travis/run.sh | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 .travis/run.sh diff --git a/.travis.yml b/.travis.yml index 92512b96..18b52dbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,14 +22,4 @@ install: before_script: "flake8 --max-complexity 15 --exclude 'hyper/packages/*' hyper test" script: - - > - if [[ "$TEST_RELEASE" == true ]]; then - py.test test_release.py - else - if [[ $TRAVIS_PYTHON_VERSION == pypy ]]; then - py.test test/ - else - coverage run -m py.test test/ - coverage report - fi - fi + - ".travis/run.sh" diff --git a/.travis/run.sh b/.travis/run.sh new file mode 100644 index 00000000..43d9dd65 --- /dev/null +++ b/.travis/run.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e +set -x + +if [[ "$TEST_RELEASE" == true ]]; then + py.test test_release.py +else + if [[ $TRAVIS_PYTHON_VERSION == pypy ]]; then + py.test test/ + else + coverage run -m py.test test/ + coverage report + fi +fi From 654d4a4e632829646b70d17fe5d43cd8871be9ba Mon Sep 17 00:00:00 2001 From: Cory Benfield <lukasaoz@gmail.com> Date: Wed, 7 Dec 2016 10:27:20 +0000 Subject: [PATCH 10/60] Installable is good. --- .travis/run.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .travis/run.sh diff --git a/.travis/run.sh b/.travis/run.sh old mode 100644 new mode 100755 From bdcf38e403c1f669d8d66d760ad16ee4c7153451 Mon Sep 17 00:00:00 2001 From: Nate Prewitt <Nate.Prewitt@gmail.com> Date: Mon, 5 Dec 2016 16:11:16 -0600 Subject: [PATCH 11/60] adding itegration tests for HTTPConnection --- test/test_integration.py | 95 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/test/test_integration.py b/test/test_integration.py index 6a6c2600..c51f061f 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -15,6 +15,7 @@ from h2.frame_buffer import FrameBuffer from hyper.compat import ssl from hyper.contrib import HTTP20Adapter +from hyper.common.util import HTTPVersion from hyperframe.frame import ( Frame, SettingsFrame, WindowUpdateFrame, DataFrame, HeadersFrame, GoAwayFrame, RstStreamFrame @@ -935,6 +936,100 @@ def socket_handler(listener): self.tear_down() + def test_version_after_tls_upgrade(self, monkeypatch): + self.set_up() + + # We need to patch the ssl_wrap_socket method to ensure that we + # forcefully upgrade. + old_wrap_socket = hyper.http11.connection.wrap_socket + + def wrap(*args): + sock, _ = old_wrap_socket(*args) + return sock, 'h2' + + monkeypatch.setattr(hyper.http11.connection, 'wrap_socket', wrap) + + send_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + receive_preamble(sock) + + # Send the headers for the response. This response has no body. + f = build_headers_frame( + [(':status', '200'), ('content-length', '0')] + ) + f.flags.add('END_STREAM') + f.stream_id = 1 + sock.sendall(f.serialize()) + + # Wait for the message from the main thread. + send_event.wait() + sock.close() + + self._start_server(socket_handler) + c = hyper.HTTPConnection(self.host, self.port, secure=True) + + assert c.version is HTTPVersion.http11 + assert c.version is not HTTPVersion.http20 + c.request('GET', '/') + send_event.set() + assert c.version is HTTPVersion.http20 + + self.tear_down() + + def test_version_after_http_upgrade(self): + self.set_up() + self.secure = False + + send_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + # We should get the initial request. + data = b'' + while not data.endswith(b'\r\n\r\n'): + data += sock.recv(65535) + assert b'upgrade: h2c\r\n' in data + + send_event.wait() + + # We need to send back a response. + resp = ( + b'HTTP/1.1 101 Upgrade\r\n' + b'Server: socket-level-server\r\n' + b'Content-Length: 0\r\n' + b'Connection: upgrade\r\n' + b'Upgrade: h2c\r\n' + b'\r\n' + ) + sock.sendall(resp) + + # We get a message for connection open, specifically the preamble. + receive_preamble(sock) + + # Send the headers for the response. This response has a body. + f = build_headers_frame( + [(':status', '200'), ('content-length', '0')] + ) + f.stream_id = 1 + f.flags.add('END_STREAM') + sock.sendall(f.serialize()) + + self._start_server(socket_handler) + + c = hyper.HTTPConnection(self.host, self.port) + assert c.version is HTTPVersion.http11 + c.request('GET', '/') + send_event.set() + resp = c.get_response() + assert c.version is HTTPVersion.http20 + assert resp.version is HTTPVersion.http20 + + self.tear_down() + class TestRequestsAdapter(SocketLevelTest): # This uses HTTP/2. From 2cdc92fc04aa0f45115cf5f69d04d83a92ed2ef6 Mon Sep 17 00:00:00 2001 From: laike9m <laike9m@gmail.com> Date: Tue, 17 Jan 2017 23:18:53 +0800 Subject: [PATCH 12/60] Fix test_adapter_received_values and test_adapter_sending_values for Python 3 --- hyper/http20/util.py | 6 +++--- test/test_hyper.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/hyper/http20/util.py b/hyper/http20/util.py index b116d095..ca5547d0 100644 --- a/hyper/http20/util.py +++ b/hyper/http20/util.py @@ -52,9 +52,9 @@ def h2_safe_headers(headers): """ stripped = { i.lower().strip() - for k, v in headers if k == 'connection' - for i in v.split(',') + for k, v in headers if k == b'connection' + for i in v.split(b',') } - stripped.add('connection') + stripped.add(b'connection') return [header for header in headers if header[0] not in stripped] diff --git a/test/test_hyper.py b/test/test_hyper.py index 59d354c7..6a18d592 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -1176,27 +1176,27 @@ def test_nghttp2_installs_correctly(self): assert True def test_stripping_connection_header(self): - headers = [('one', 'two'), ('connection', 'close')] - stripped = [('one', 'two')] + headers = [(b'one', b'two'), (b'connection', b'close')] + stripped = [(b'one', b'two')] assert h2_safe_headers(headers) == stripped def test_stripping_related_headers(self): headers = [ - ('one', 'two'), ('three', 'four'), ('five', 'six'), - ('connection', 'close, three, five') + (b'one', b'two'), (b'three', b'four'), (b'five', b'six'), + (b'connection', b'close, three, five') ] - stripped = [('one', 'two')] + stripped = [(b'one', b'two')] assert h2_safe_headers(headers) == stripped def test_stripping_multiple_connection_headers(self): headers = [ - ('one', 'two'), ('three', 'four'), ('five', 'six'), - ('connection', 'close'), - ('connection', 'three, five') + (b'one', b'two'), (b'three', b'four'), (b'five', b'six'), + (b'connection', b'close'), + (b'connection', b'three, five') ] - stripped = [('one', 'two')] + stripped = [(b'one', b'two')] assert h2_safe_headers(headers) == stripped From e8d4034f9441ee9d4ce5b56fe9ded25f3635c2ec Mon Sep 17 00:00:00 2001 From: laike9m <laike9m@gmail.com> Date: Wed, 18 Jan 2017 22:25:03 +0800 Subject: [PATCH 13/60] Removed unnecessary size check #300 --- hyper/common/bufsocket.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyper/common/bufsocket.py b/hyper/common/bufsocket.py index b35393af..854681ca 100644 --- a/hyper/common/bufsocket.py +++ b/hyper/common/bufsocket.py @@ -138,7 +138,7 @@ def recv(self, amt): else: should_read = True - if (self._remaining_capacity > self._bytes_in_buffer and should_read): + if should_read: count = self._sck.recv_into(self._buffer_view[self._buffer_end:]) # The socket just got closed. We should throw an exception if we From 5e133b6899f86b0ca29fa6df182c30f64fd7e920 Mon Sep 17 00:00:00 2001 From: Jonas Obrist <jonas.obrist@hde.co.jp> Date: Tue, 24 Jan 2017 19:08:44 +0900 Subject: [PATCH 14/60] More informative error message if no protocol found for http2 Added a more informative error message if a HTTP2 connection was unable to select a protocol, added a test for this error message. Changed integration tests to use mock to override acceptable protocols, so as not to leak those changes to other tests. --- hyper/http20/connection.py | 5 ++++- test/test_http20.py | 42 ++++++++++++++++++++++++++++++++++++++ test/test_integration.py | 7 +++++-- 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 test/test_http20.py diff --git a/hyper/http20/connection.py b/hyper/http20/connection.py index b8a2901e..8b2a71e8 100644 --- a/hyper/http20/connection.py +++ b/hyper/http20/connection.py @@ -375,7 +375,10 @@ def connect(self): proto = H2C_PROTOCOL log.debug("Selected NPN protocol: %s", proto) - assert proto in H2_NPN_PROTOCOLS or proto == H2C_PROTOCOL + assert proto in H2_NPN_PROTOCOLS or proto == H2C_PROTOCOL, ( + "No suitable protocol found. Supported protocols: %s. " + "Check your OpenSSL version." + ) % ','.join(H2_NPN_PROTOCOLS + [H2C_PROTOCOL]) self._sock = BufferedSocket(sock, self.network_buffer_size) diff --git a/test/test_http20.py b/test/test_http20.py new file mode 100644 index 00000000..d187c315 --- /dev/null +++ b/test/test_http20.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +""" +test_http20.py +~~~~~~~~~~~~~~ + +Unit tests for hyper's HTTP/2.0 implementation. +""" +import pytest +from mock import patch + +from server import SocketLevelTest + + +class TestHTTP20Connection(SocketLevelTest): + h2 = True + + def test_useful_error_with_no_protocol(self): + self.set_up() + + def socket_handler(listener): + sock = listener.accept()[0] + sock.close() + + self._start_server(socket_handler) + conn = self.get_connection() + + with patch('hyper.http20.connection.wrap_socket') as mock: + mock.return_value = (None, None) + with pytest.raises(AssertionError) as exc_info: + conn.connect() + assert ( + "No suitable protocol found." + in + str(exc_info) + ) + assert ( + "Check your OpenSSL version." + in + str(exc_info) + ) + + self.tear_down() diff --git a/test/test_integration.py b/test/test_integration.py index c51f061f..96a8af76 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -12,6 +12,7 @@ import hyper import hyper.http11.connection import pytest +from mock import patch from h2.frame_buffer import FrameBuffer from hyper.compat import ssl from hyper.contrib import HTTP20Adapter @@ -34,8 +35,8 @@ hyper.tls._context.check_hostname = False hyper.tls._context.verify_mode = ssl.CERT_NONE - # Cover our bases because NPN doesn't yet work on all our test platforms. - hyper.http20.connection.H2_NPN_PROTOCOLS += ['', None] +# Cover our bases because NPN doesn't yet work on all our test platforms. +PROTOCOLS = hyper.http20.connection.H2_NPN_PROTOCOLS + ['', None] def decode_frame(frame_data): @@ -76,6 +77,7 @@ def receive_preamble(sock): return +@patch('hyper.http20.connection.H2_NPN_PROTOCOLS', PROTOCOLS) class TestHyperIntegration(SocketLevelTest): # These are HTTP/2 tests. h2 = True @@ -1031,6 +1033,7 @@ def socket_handler(listener): self.tear_down() +@patch('hyper.http20.connection.H2_NPN_PROTOCOLS', PROTOCOLS) class TestRequestsAdapter(SocketLevelTest): # This uses HTTP/2. h2 = True From 7aa17fda51fe89a67a4d8400d1a462af28f5e7cd Mon Sep 17 00:00:00 2001 From: Hiroaki KAWAI <hiroaki.kawai@gmail.com> Date: Thu, 23 Feb 2017 11:19:59 +0000 Subject: [PATCH 15/60] Add ENABLE_PUSH flag in the Upgrade HTTP2-Settings header Push flag required for the case the initial upgrade request triggered server push. --- hyper/common/connection.py | 3 +- hyper/http11/connection.py | 5 + test/test_abstraction.py | 1 + test/test_hyper.py | 190 ++++++++++++++++++++++++++++++++++++- 4 files changed, 194 insertions(+), 5 deletions(-) diff --git a/hyper/common/connection.py b/hyper/common/connection.py index dee18d68..507a8ad7 100644 --- a/hyper/common/connection.py +++ b/hyper/common/connection.py @@ -62,7 +62,8 @@ def __init__(self, self._port = port self._h1_kwargs = { 'secure': secure, 'ssl_context': ssl_context, - 'proxy_host': proxy_host, 'proxy_port': proxy_port + 'proxy_host': proxy_host, 'proxy_port': proxy_port, + 'enable_push': enable_push } self._h2_kwargs = { 'window_manager': window_manager, 'enable_push': enable_push, diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 48bde2f0..8025c740 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -78,6 +78,7 @@ def __init__(self, host, port=None, secure=None, ssl_context=None, # only send http upgrade headers for non-secure connection self._send_http_upgrade = not self.secure + self._enable_push = kwargs.get('enable_push') self.ssl_context = ssl_context self._sock = None @@ -276,6 +277,10 @@ def _add_upgrade_headers(self, headers): # Settings header. http2_settings = SettingsFrame(0) http2_settings.settings[SettingsFrame.INITIAL_WINDOW_SIZE] = 65535 + if self._enable_push is not None: + http2_settings.settings[SettingsFrame.ENABLE_PUSH] = ( + int(self._enable_push) + ) encoded_settings = base64.urlsafe_b64encode( http2_settings.serialize_body() ) diff --git a/test/test_abstraction.py b/test/test_abstraction.py index cd0e0645..7c2cad1a 100644 --- a/test/test_abstraction.py +++ b/test/test_abstraction.py @@ -19,6 +19,7 @@ def test_h1_kwargs(self): 'proxy_host': False, 'proxy_port': False, 'other_kwarg': True, + 'enable_push': True, } def test_h2_kwargs(self): diff --git a/test/test_hyper.py b/test/test_hyper.py index 6a18d592..09847511 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -9,6 +9,7 @@ PingFrame, FRAME_MAX_ALLOWED_LEN ) from hpack.hpack_compat import Encoder +from hyper.common.connection import HTTPConnection from hyper.http20.connection import HTTP20Connection from hyper.http20.response import HTTP20Response, HTTP20Push from hyper.http20.exceptions import ConnectionError, StreamResetError @@ -731,8 +732,8 @@ def add_data_frame(self, stream_id, data, end_stream=False): frame.flags.add('END_STREAM') self.frames.append(frame) - def request(self): - self.conn = HTTP20Connection('www.google.com', enable_push=True) + def request(self, enable_push=True): + self.conn = HTTP20Connection('www.google.com', enable_push=enable_push) self.conn._sock = DummySocket() self.conn._sock.buffer = BytesIO( b''.join([frame.serialize() for frame in self.frames]) @@ -934,8 +935,7 @@ def test_reset_pushed_streams_when_push_disabled(self): 1, [(':status', '200'), ('content-type', 'text/html')] ) - self.request() - self.conn._enable_push = False + self.request(False) self.conn.get_response() f = RstStreamFrame(2) @@ -1303,6 +1303,188 @@ def test_resetting_streams_after_close(self): c._single_read() +class TestUpgradingPush(object): + http101 = (b"HTTP/1.1 101 Switching Protocols\r\n" + b"Connection: upgrade\r\n" + b"Upgrade: h2c\r\n" + b"\r\n") + + def setup_method(self, method): + self.frames = [SettingsFrame(0)] # Server side preface + self.encoder = Encoder() + self.conn = None + + def add_push_frame(self, stream_id, promised_stream_id, headers, + end_block=True): + frame = PushPromiseFrame(stream_id) + frame.promised_stream_id = promised_stream_id + frame.data = self.encoder.encode(headers) + if end_block: + frame.flags.add('END_HEADERS') + self.frames.append(frame) + + def add_headers_frame(self, stream_id, headers, end_block=True, + end_stream=False): + frame = HeadersFrame(stream_id) + frame.data = self.encoder.encode(headers) + if end_block: + frame.flags.add('END_HEADERS') + if end_stream: + frame.flags.add('END_STREAM') + self.frames.append(frame) + + def add_data_frame(self, stream_id, data, end_stream=False): + frame = DataFrame(stream_id) + frame.data = data + if end_stream: + frame.flags.add('END_STREAM') + self.frames.append(frame) + + def request(self, enable_push=True): + self.conn = HTTPConnection('www.google.com', enable_push=enable_push) + self.conn._conn._sock = DummySocket() + self.conn._conn._sock.buffer = BytesIO( + self.http101 + b''.join([frame.serialize() + for frame in self.frames]) + ) + self.conn.request('GET', '/') + + def assert_response(self): + self.response = self.conn.get_response() + assert self.response.status == 200 + assert dict(self.response.headers) == {b'content-type': [b'text/html']} + + def assert_pushes(self): + self.pushes = list(self.conn.get_pushes()) + assert len(self.pushes) == 1 + assert self.pushes[0].method == b'GET' + assert self.pushes[0].scheme == b'http' + assert self.pushes[0].authority == b'www.google.com' + assert self.pushes[0].path == b'/' + expected_headers = {b'accept-encoding': [b'gzip']} + assert dict(self.pushes[0].request_headers) == expected_headers + + def assert_push_response(self): + push_response = self.pushes[0].get_response() + assert push_response.status == 200 + assert dict(push_response.headers) == { + b'content-type': [b'application/javascript'] + } + assert push_response.read() == b'bar' + + def test_promise_before_headers(self): + # Current implementation only support get_pushes call + # after get_response + pass + + def test_promise_after_headers(self): + self.add_headers_frame( + 1, [(':status', '200'), ('content-type', 'text/html')] + ) + self.add_push_frame( + 1, + 2, + [ + (':method', 'GET'), + (':path', '/'), + (':authority', 'www.google.com'), + (':scheme', 'http'), + ('accept-encoding', 'gzip') + ] + ) + self.add_data_frame(1, b'foo', end_stream=True) + self.add_headers_frame( + 2, [(':status', '200'), ('content-type', 'application/javascript')] + ) + self.add_data_frame(2, b'bar', end_stream=True) + + self.request() + self.assert_response() + assert self.response.read() == b'foo' + self.assert_pushes() + self.assert_push_response() + + def test_promise_after_data(self): + self.add_headers_frame( + 1, [(':status', '200'), ('content-type', 'text/html')] + ) + self.add_data_frame(1, b'fo') + self.add_push_frame( + 1, + 2, + [ + (':method', 'GET'), + (':path', '/'), + (':authority', 'www.google.com'), + (':scheme', 'http'), + ('accept-encoding', 'gzip') + ] + ) + self.add_data_frame(1, b'o', end_stream=True) + self.add_headers_frame( + 2, [(':status', '200'), ('content-type', 'application/javascript')] + ) + self.add_data_frame(2, b'bar', end_stream=True) + + self.request() + self.assert_response() + assert self.response.read() == b'foo' + self.assert_pushes() + self.assert_push_response() + + def test_capture_all_promises(self): + # Current implementation does not support capture_all + # for h2c upgrading connection. + pass + + def test_cancel_push(self): + self.add_push_frame( + 1, + 2, + [ + (':method', 'GET'), + (':path', '/'), + (':authority', 'www.google.com'), + (':scheme', 'http'), + ('accept-encoding', 'gzip') + ] + ) + self.add_headers_frame( + 1, [(':status', '200'), ('content-type', 'text/html')] + ) + + self.request() + self.conn.get_response() + list(self.conn.get_pushes())[0].cancel() + + f = RstStreamFrame(2) + f.error_code = 8 + assert self.conn._sock.queue[-1] == f.serialize() + + def test_reset_pushed_streams_when_push_disabled(self): + self.add_push_frame( + 1, + 2, + [ + (':method', 'GET'), + (':path', '/'), + (':authority', 'www.google.com'), + (':scheme', 'http'), + ('accept-encoding', 'gzip') + ] + ) + self.add_headers_frame( + 1, [(':status', '200'), ('content-type', 'text/html')] + ) + + self.request(False) + self.conn.get_response() + + f = RstStreamFrame(2) + f.error_code = 7 + assert self.conn._sock.queue[-1].endswith(f.serialize()) + + # Some utility classes for the tests. class NullEncoder(object): @staticmethod From 085975fbc0b25cfe9d0333a029822d971b8f961d Mon Sep 17 00:00:00 2001 From: Hiroaki KAWAI <hiroaki.kawai@gmail.com> Date: Thu, 23 Feb 2017 11:22:33 +0000 Subject: [PATCH 16/60] fix test synchronization Some tests requires synchronization to let the connection state machine working. --- test/test_integration.py | 119 +++++++++++++++++++++++++++------------ 1 file changed, 82 insertions(+), 37 deletions(-) diff --git a/test/test_integration.py b/test/test_integration.py index 96a8af76..f78107aa 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -12,6 +12,7 @@ import hyper import hyper.http11.connection import pytest +from contextlib import contextmanager from mock import patch from h2.frame_buffer import FrameBuffer from hyper.compat import ssl @@ -64,17 +65,30 @@ def frame_buffer(): return buffer +@contextmanager +def reusable_frame_buffer(buffer): + # FrameBuffer does not return new iterator for iteration. + data = buffer.data + yield buffer + buffer.data = data + + def receive_preamble(sock): # Receive the HTTP/2 'preamble'. - first = sock.recv(65535) + client_preface = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' + timeout = time.time() + 5 + got = b'' + while len(got) < len(client_preface) and time.time() < timeout: + got += sock.recv(len(client_preface) - len(got)) + + assert got == client_preface, "client preface mismatch" - # Work around some bugs: if the first message received was only the PRI - # string, aim to receive a settings frame as well. - if len(first) <= len(b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'): - sock.recv(65535) + # Send server side HTTP/2 preface sock.send(SettingsFrame(0).serialize()) - sock.recv(65535) - return + # Drain to let the client proceed. + # Note that in the lower socket level, this method is not + # just doing "receive". + return sock.recv(65535) @patch('hyper.http20.connection.H2_NPN_PROTOCOLS', PROTOCOLS) @@ -138,7 +152,7 @@ def socket_handler(listener): self._start_server(socket_handler) conn = self.get_connection() conn.connect() - send_event.wait() + send_event.wait(5) # Get the chunk of data after the preamble and decode it into frames. # We actually expect two, but only the second one contains ENABLE_PUSH. @@ -242,7 +256,7 @@ def socket_handler(listener): f = SettingsFrame(0) sock.send(f.serialize()) - send_event.wait() + send_event.wait(5) sock.recv(65535) sock.close() @@ -260,6 +274,7 @@ def socket_handler(listener): def test_closed_responses_remove_their_streams_from_conn(self): self.set_up() + req_event = threading.Event() recv_event = threading.Event() def socket_handler(listener): @@ -270,6 +285,8 @@ def socket_handler(listener): receive_preamble(sock) sock.recv(65535) + # Wait for request + req_event.wait(5) # Now, send the headers for the response. f = build_headers_frame([(':status', '200')]) f.stream_id = 1 @@ -282,6 +299,7 @@ def socket_handler(listener): self._start_server(socket_handler) conn = self.get_connection() conn.request('GET', '/') + req_event.set() resp = conn.get_response() # Close the response. @@ -296,6 +314,7 @@ def socket_handler(listener): def test_receiving_responses_with_no_body(self): self.set_up() + req_event = threading.Event() recv_event = threading.Event() def socket_handler(listener): @@ -306,6 +325,8 @@ def socket_handler(listener): receive_preamble(sock) sock.recv(65535) + # Wait for request + req_event.wait(5) # Now, send the headers for the response. This response has no body f = build_headers_frame( [(':status', '204'), ('content-length', '0')] @@ -321,6 +342,7 @@ def socket_handler(listener): self._start_server(socket_handler) conn = self.get_connection() conn.request('GET', '/') + req_event.set() resp = conn.get_response() # Confirm the status code. @@ -338,6 +360,7 @@ def socket_handler(listener): def test_receiving_trailers(self): self.set_up() + req_event = threading.Event() recv_event = threading.Event() def socket_handler(listener): @@ -350,6 +373,8 @@ def socket_handler(listener): receive_preamble(sock) sock.recv(65535) + # Wait for request + req_event.wait(5) # Now, send the headers for the response. f = build_headers_frame( [(':status', '200'), ('content-length', '14')], @@ -372,12 +397,13 @@ def socket_handler(listener): sock.send(f.serialize()) # Wait for the message from the main thread. - recv_event.set() + recv_event.wait(5) sock.close() self._start_server(socket_handler) conn = self.get_connection() conn.request('GET', '/') + req_event.set() resp = conn.get_response() # Confirm the status code. @@ -396,13 +422,14 @@ def socket_handler(listener): assert len(resp.trailers) == 2 # Awesome, we're done now. - recv_event.wait(5) + recv_event.set() self.tear_down() def test_receiving_trailers_before_reading(self): self.set_up() + req_event = threading.Event() recv_event = threading.Event() wait_event = threading.Event() @@ -416,6 +443,8 @@ def socket_handler(listener): receive_preamble(sock) sock.recv(65535) + # Wait for request + req_event.wait(5) # Now, send the headers for the response. f = build_headers_frame( [(':status', '200'), ('content-length', '14')], @@ -449,6 +478,7 @@ def socket_handler(listener): self._start_server(socket_handler) conn = self.get_connection() conn.request('GET', '/') + req_event.set() resp = conn.get_response() # Confirm the status code. @@ -647,6 +677,7 @@ def test_resetting_stream_with_frames_in_flight(self): """ self.set_up() + req_event = threading.Event() recv_event = threading.Event() def socket_handler(listener): @@ -657,6 +688,8 @@ def socket_handler(listener): receive_preamble(sock) sock.recv(65535) + # Wait for request + req_event.wait(5) # Now, send the headers for the response. This response has no # body. f = build_headers_frame( @@ -673,6 +706,7 @@ def socket_handler(listener): self._start_server(socket_handler) conn = self.get_connection() stream_id = conn.request('GET', '/') + req_event.set() # Now, trigger the RST_STREAM frame by closing the stream. conn._send_rst_frame(stream_id, 0) @@ -696,6 +730,7 @@ def test_stream_can_be_reset_multiple_times(self): """ self.set_up() + req_event = threading.Event() recv_event = threading.Event() def socket_handler(listener): @@ -706,6 +741,8 @@ def socket_handler(listener): receive_preamble(sock) sock.recv(65535) + # Wait for request + req_event.wait(5) # Now, send two RST_STREAM frames. for _ in range(0, 2): f = RstStreamFrame(1) @@ -718,6 +755,7 @@ def socket_handler(listener): self._start_server(socket_handler) conn = self.get_connection() conn.request('GET', '/') + req_event.set() # Now, eat the Rst frames. These should not cause an exception. conn._single_read() @@ -737,6 +775,7 @@ def socket_handler(listener): def test_read_chunked_http2(self): self.set_up() + req_event = threading.Event() recv_event = threading.Event() wait_event = threading.Event() @@ -748,6 +787,8 @@ def socket_handler(listener): receive_preamble(sock) sock.recv(65535) + # Wait for request + req_event.wait(5) # Now, send the headers for the response. This response has a body. f = build_headers_frame([(':status', '200')]) f.stream_id = 1 @@ -777,6 +818,7 @@ def socket_handler(listener): self._start_server(socket_handler) conn = self.get_connection() conn.request('GET', '/') + req_event.set() resp = conn.get_response() # Confirm the status code. @@ -805,6 +847,7 @@ def socket_handler(listener): def test_read_delayed(self): self.set_up() + req_event = threading.Event() recv_event = threading.Event() wait_event = threading.Event() @@ -816,6 +859,8 @@ def socket_handler(listener): receive_preamble(sock) sock.recv(65535) + # Wait for request + req_event.wait(5) # Now, send the headers for the response. This response has a body. f = build_headers_frame([(':status', '200')]) f.stream_id = 1 @@ -845,6 +890,7 @@ def socket_handler(listener): self._start_server(socket_handler) conn = self.get_connection() conn.request('GET', '/') + req_event.set() resp = conn.get_response() # Confirm the status code. @@ -958,6 +1004,8 @@ def socket_handler(listener): receive_preamble(sock) + # Wait for the message from the main thread. + send_event.wait() # Send the headers for the response. This response has no body. f = build_headers_frame( [(':status', '200'), ('content-length', '0')] @@ -965,9 +1013,6 @@ def socket_handler(listener): f.flags.add('END_STREAM') f.stream_id = 1 sock.sendall(f.serialize()) - - # Wait for the message from the main thread. - send_event.wait() sock.close() self._start_server(socket_handler) @@ -996,7 +1041,7 @@ def socket_handler(listener): data += sock.recv(65535) assert b'upgrade: h2c\r\n' in data - send_event.wait() + send_event.wait(5) # We need to send back a response. resp = ( @@ -1038,7 +1083,7 @@ class TestRequestsAdapter(SocketLevelTest): # This uses HTTP/2. h2 = True - def test_adapter_received_values(self, monkeypatch): + def test_adapter_received_values(self, monkeypatch, frame_buffer): self.set_up() # We need to patch the ssl_wrap_socket method to ensure that we @@ -1051,17 +1096,20 @@ def wrap(*args): monkeypatch.setattr(hyper.http11.connection, 'wrap_socket', wrap) - data = [] - send_event = threading.Event() - def socket_handler(listener): sock = listener.accept()[0] # Do the handshake: conn header, settings, send settings, recv ack. - receive_preamble(sock) + frame_buffer.add_data(receive_preamble(sock)) # Now expect some data. One headers frame. - data.append(sock.recv(65535)) + req_wait = True + while req_wait: + frame_buffer.add_data(sock.recv(65535)) + with reusable_frame_buffer(frame_buffer) as fr: + for f in fr: + if isinstance(f, HeadersFrame): + req_wait = False # Respond! h = HeadersFrame(1) @@ -1078,8 +1126,6 @@ def socket_handler(listener): d.data = b'1234567890' * 2 d.flags.add('END_STREAM') sock.send(d.serialize()) - - send_event.wait(5) sock.close() self._start_server(socket_handler) @@ -1093,11 +1139,9 @@ def socket_handler(listener): assert r.headers[b'Content-Type'] == b'not/real' assert r.content == b'1234567890' * 2 - send_event.set() - self.tear_down() - def test_adapter_sending_values(self, monkeypatch): + def test_adapter_sending_values(self, monkeypatch, frame_buffer): self.set_up() # We need to patch the ssl_wrap_socket method to ensure that we @@ -1110,17 +1154,20 @@ def wrap(*args): monkeypatch.setattr(hyper.http11.connection, 'wrap_socket', wrap) - data = [] - def socket_handler(listener): sock = listener.accept()[0] # Do the handshake: conn header, settings, send settings, recv ack. - receive_preamble(sock) + frame_buffer.add_data(receive_preamble(sock)) # Now expect some data. One headers frame and one data frame. - data.append(sock.recv(65535)) - data.append(sock.recv(65535)) + req_wait = True + while req_wait: + frame_buffer.add_data(sock.recv(65535)) + with reusable_frame_buffer(frame_buffer) as fr: + for f in fr: + if isinstance(f, DataFrame): + req_wait = False # Respond! h = HeadersFrame(1) @@ -1137,7 +1184,6 @@ def socket_handler(listener): d.data = b'1234567890' * 2 d.flags.add('END_STREAM') sock.send(d.serialize()) - sock.close() self._start_server(socket_handler) @@ -1152,11 +1198,10 @@ def socket_handler(listener): # Assert about the sent values. assert r.status_code == 200 - f = decode_frame(data[0]) - assert isinstance(f, HeadersFrame) + frames = list(frame_buffer) + assert isinstance(frames[-2], HeadersFrame) - f = decode_frame(data[1]) - assert isinstance(f, DataFrame) - assert f.data == b'hi there' + assert isinstance(frames[-1], DataFrame) + assert frames[-1].data == b'hi there' self.tear_down() From a974eec5f3f2f110ab78e797a703f1adb30bd109 Mon Sep 17 00:00:00 2001 From: Hiroaki KAWAI <hiroaki.kawai@gmail.com> Date: Thu, 23 Feb 2017 16:04:55 +0000 Subject: [PATCH 17/60] update test introduced FrameEncoderMixin helper class. fix waits for not displaying unwanted EOF errors. --- test/test_hyper.py | 49 ++++++------------ test/test_integration.py | 105 ++++++++++++++++++++++++--------------- 2 files changed, 80 insertions(+), 74 deletions(-) diff --git a/test/test_hyper.py b/test/test_hyper.py index 09847511..27f4e7b2 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -48,6 +48,7 @@ def frame_buffer(): class TestHyperConnection(object): + def test_connections_accept_hosts_and_ports(self): c = HTTP20Connection(host='www.google.com', port=8080) assert c.host == 'www.google.com' @@ -499,6 +500,7 @@ def test_headers_with_continuation(self): def test_send_tolerate_peer_gone(self): class ErrorSocket(DummySocket): + def sendall(self, data): raise socket.error(errno.EPIPE) @@ -700,7 +702,8 @@ def test_incrementing_window_after_close(self): assert len(originally_sent_data) + 1 == len(c._sock.queue) -class TestServerPush(object): +class FrameEncoderMixin(object): + def setup_method(self, method): self.frames = [] self.encoder = Encoder() @@ -732,6 +735,9 @@ def add_data_frame(self, stream_id, data, end_stream=False): frame.flags.add('END_STREAM') self.frames.append(frame) + +class TestServerPush(FrameEncoderMixin): + def request(self, enable_push=True): self.conn = HTTP20Connection('www.google.com', enable_push=enable_push) self.conn._sock = DummySocket() @@ -957,6 +963,7 @@ def test_pushed_requests_ignore_unexpected_headers(self): class TestResponse(object): + def test_status_is_stripped_from_headers(self): headers = HTTPHeaderMap([(':status', '200')]) resp = HTTP20Response(headers, None) @@ -1107,6 +1114,7 @@ def test_response_version(self): class TestHTTP20Adapter(object): + def test_adapter_reuses_connections(self): a = HTTP20Adapter() conn1 = a.get_connection('http2bin.org', 80, 'http') @@ -1130,6 +1138,7 @@ def test_adapter_accept_client_certificate(self): class TestUtilities(object): + def test_combining_repeated_headers(self): test_headers = [ (b'key1', b'val1'), @@ -1303,44 +1312,14 @@ def test_resetting_streams_after_close(self): c._single_read() -class TestUpgradingPush(object): +class TestUpgradingPush(FrameEncoderMixin): http101 = (b"HTTP/1.1 101 Switching Protocols\r\n" b"Connection: upgrade\r\n" b"Upgrade: h2c\r\n" b"\r\n") - def setup_method(self, method): - self.frames = [SettingsFrame(0)] # Server side preface - self.encoder = Encoder() - self.conn = None - - def add_push_frame(self, stream_id, promised_stream_id, headers, - end_block=True): - frame = PushPromiseFrame(stream_id) - frame.promised_stream_id = promised_stream_id - frame.data = self.encoder.encode(headers) - if end_block: - frame.flags.add('END_HEADERS') - self.frames.append(frame) - - def add_headers_frame(self, stream_id, headers, end_block=True, - end_stream=False): - frame = HeadersFrame(stream_id) - frame.data = self.encoder.encode(headers) - if end_block: - frame.flags.add('END_HEADERS') - if end_stream: - frame.flags.add('END_STREAM') - self.frames.append(frame) - - def add_data_frame(self, stream_id, data, end_stream=False): - frame = DataFrame(stream_id) - frame.data = data - if end_stream: - frame.flags.add('END_STREAM') - self.frames.append(frame) - def request(self, enable_push=True): + self.frames = [SettingsFrame(0)] + self.frames # Server side preface self.conn = HTTPConnection('www.google.com', enable_push=enable_push) self.conn._conn._sock = DummySocket() self.conn._conn._sock.buffer = BytesIO( @@ -1487,6 +1466,7 @@ def test_reset_pushed_streams_when_push_disabled(self): # Some utility classes for the tests. class NullEncoder(object): + @staticmethod def encode(headers): @@ -1503,6 +1483,7 @@ def to_str(v): class FixedDecoder(object): + def __init__(self, result): self.result = result @@ -1511,6 +1492,7 @@ def decode(self, headers): class DummySocket(object): + def __init__(self): self.queue = [] self._buffer = BytesIO() @@ -1548,6 +1530,7 @@ def fill(self): class DummyStream(object): + def __init__(self, data, trailers=None): self.data = data self.data_frames = [] diff --git a/test/test_integration.py b/test/test_integration.py index f78107aa..273b4c18 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -255,9 +255,9 @@ def socket_handler(listener): # We need to send back a SettingsFrame. f = SettingsFrame(0) sock.send(f.serialize()) + sock.recv(65535) send_event.wait(5) - sock.recv(65535) sock.close() self._start_server(socket_handler) @@ -293,7 +293,7 @@ def socket_handler(listener): sock.send(f.serialize()) # Wait for the message from the main thread. - recv_event.set() + recv_event.wait(5) sock.close() self._start_server(socket_handler) @@ -305,7 +305,7 @@ def socket_handler(listener): # Close the response. resp.close() - recv_event.wait(5) + recv_event.set() assert not conn.streams @@ -336,7 +336,7 @@ def socket_handler(listener): sock.send(f.serialize()) # Wait for the message from the main thread. - recv_event.set() + recv_event.wait(5) sock.close() self._start_server(socket_handler) @@ -353,8 +353,7 @@ def socket_handler(listener): assert resp._stream._in_window_manager.document_size == 0 # Awesome, we're done now. - recv_event.wait(5) - + recv_event.set() self.tear_down() def test_receiving_trailers(self): @@ -423,15 +422,14 @@ def socket_handler(listener): # Awesome, we're done now. recv_event.set() - self.tear_down() def test_receiving_trailers_before_reading(self): self.set_up() req_event = threading.Event() - recv_event = threading.Event() wait_event = threading.Event() + recv_event = threading.Event() def socket_handler(listener): sock = listener.accept()[0] @@ -472,7 +470,7 @@ def socket_handler(listener): sock.send(f.serialize()) # Wait for the message from the main thread. - recv_event.set() + recv_event.wait(5) sock.close() self._start_server(socket_handler) @@ -500,8 +498,7 @@ def socket_handler(listener): assert resp._stream._in_window_manager.document_size == 14 # Awesome, we're done now. - recv_event.wait(5) - + recv_event.set() self.tear_down() def test_clean_shut_down(self): @@ -522,7 +519,7 @@ def socket_handler(listener): sock.send(f.serialize()) # Wait for the message from the main thread. - recv_event.set() + recv_event.wait(5) sock.close() self._start_server(socket_handler) @@ -533,8 +530,7 @@ def socket_handler(listener): assert conn._sock is None # Awesome, we're done now. - recv_event.wait(5) - + recv_event.set() self.tear_down() def test_unexpected_shut_down(self): @@ -555,8 +551,8 @@ def socket_handler(listener): sock.send(f.serialize()) # Wait for the message from the main thread. + recv_event.wait(5) sock.close() - recv_event.set() self._start_server(socket_handler) conn = self.get_connection() @@ -568,15 +564,15 @@ def socket_handler(listener): assert conn._sock is None # Awesome, we're done now. - recv_event.wait(5) - + recv_event.set() self.tear_down() def test_insecure_connection(self): self.set_up(secure=False) data = [] - send_event = threading.Event() + req_event = threading.Event() + recv_event = threading.Event() def socket_handler(listener): sock = listener.accept()[0] @@ -584,7 +580,7 @@ def socket_handler(listener): receive_preamble(sock) data.append(sock.recv(65535)) - send_event.wait(5) + req_event.wait(5) h = HeadersFrame(1) h.data = self.get_encoder().encode( @@ -603,12 +599,13 @@ def socket_handler(listener): d.flags.add('END_STREAM') sock.send(d.serialize()) + recv_event.wait(5) sock.close() self._start_server(socket_handler) c = self.get_connection() c.request('GET', '/') - send_event.set() + req_event.set() r = c.get_response() assert r.status == 200 @@ -619,13 +616,15 @@ def socket_handler(listener): assert r.read() == b'nsaislistening' + recv_event.set() self.tear_down() def test_proxy_connection(self): self.set_up(proxy=True) data = [] - send_event = threading.Event() + req_event = threading.Event() + recv_event = threading.Event() def socket_handler(listener): sock = listener.accept()[0] @@ -633,7 +632,7 @@ def socket_handler(listener): receive_preamble(sock) data.append(sock.recv(65535)) - send_event.wait(5) + req_event.wait(5) h = HeadersFrame(1) h.data = self.get_encoder().encode( @@ -652,12 +651,13 @@ def socket_handler(listener): d.flags.add('END_STREAM') sock.send(d.serialize()) + recv_event.wait(5) sock.close() self._start_server(socket_handler) c = self.get_connection() c.request('GET', '/') - send_event.set() + req_event.set() r = c.get_response() assert r.status == 200 @@ -668,6 +668,7 @@ def socket_handler(listener): assert r.read() == b'thisisaproxy' + recv_event.set() self.tear_down() def test_resetting_stream_with_frames_in_flight(self): @@ -720,7 +721,6 @@ def socket_handler(listener): # Awesome, we're done now. recv_event.set() - self.tear_down() def test_stream_can_be_reset_multiple_times(self): @@ -812,7 +812,7 @@ def socket_handler(listener): sock.sendall(f.serialize()) # Wait for the message from the main thread. - recv_event.set() + recv_event.wait(5) sock.close() self._start_server(socket_handler) @@ -840,7 +840,7 @@ def socket_handler(listener): assert third_chunk == b'world' # Awesome, we're done now. - recv_event.wait(5) + recv_event.set() self.tear_down() @@ -848,8 +848,8 @@ def test_read_delayed(self): self.set_up() req_event = threading.Event() - recv_event = threading.Event() wait_event = threading.Event() + recv_event = threading.Event() def socket_handler(listener): sock = listener.accept()[0] @@ -884,7 +884,7 @@ def socket_handler(listener): sock.sendall(f.serialize()) # Wait for the message from the main thread. - recv_event.set() + recv_event.wait(5) sock.close() self._start_server(socket_handler) @@ -904,15 +904,14 @@ def socket_handler(listener): assert second_chunk == b'world' # Awesome, we're done now. - recv_event.wait(5) - + recv_event.set() self.tear_down() def test_upgrade(self): self.set_up(secure=False) - recv_event = threading.Event() wait_event = threading.Event() + recv_event = threading.Event() def socket_handler(listener): sock = listener.accept()[0] @@ -961,7 +960,7 @@ def socket_handler(listener): sock.sendall(f.serialize()) # Wait for the message from the main thread. - recv_event.set() + recv_event.wait(5) sock.close() self._start_server(socket_handler) @@ -980,8 +979,7 @@ def socket_handler(listener): assert second_chunk == b'world' # Awesome, we're done now. - recv_event.wait(5) - + recv_event.set() self.tear_down() def test_version_after_tls_upgrade(self, monkeypatch): @@ -997,15 +995,16 @@ def wrap(*args): monkeypatch.setattr(hyper.http11.connection, 'wrap_socket', wrap) - send_event = threading.Event() + req_event = threading.Event() + recv_event = threading.Event() def socket_handler(listener): sock = listener.accept()[0] receive_preamble(sock) - # Wait for the message from the main thread. - send_event.wait() + # Wait for the request + req_event.wait(5) # Send the headers for the response. This response has no body. f = build_headers_frame( [(':status', '200'), ('content-length', '0')] @@ -1013,6 +1012,9 @@ def socket_handler(listener): f.flags.add('END_STREAM') f.stream_id = 1 sock.sendall(f.serialize()) + + # wait for the message from the main thread + recv_event.wait(5) sock.close() self._start_server(socket_handler) @@ -1021,16 +1023,18 @@ def socket_handler(listener): assert c.version is HTTPVersion.http11 assert c.version is not HTTPVersion.http20 c.request('GET', '/') - send_event.set() + req_event.set() assert c.version is HTTPVersion.http20 + recv_event.set() self.tear_down() def test_version_after_http_upgrade(self): self.set_up() self.secure = False - send_event = threading.Event() + req_event = threading.Event() + recv_event = threading.Event() def socket_handler(listener): sock = listener.accept()[0] @@ -1041,7 +1045,7 @@ def socket_handler(listener): data += sock.recv(65535) assert b'upgrade: h2c\r\n' in data - send_event.wait(5) + req_event.wait(5) # We need to send back a response. resp = ( @@ -1065,15 +1069,22 @@ def socket_handler(listener): f.flags.add('END_STREAM') sock.sendall(f.serialize()) + # keep the socket open for clean shutdown + recv_event.wait(5) + sock.close() + self._start_server(socket_handler) c = hyper.HTTPConnection(self.host, self.port) assert c.version is HTTPVersion.http11 + c.request('GET', '/') - send_event.set() + req_event.set() + resp = c.get_response() assert c.version is HTTPVersion.http20 assert resp.version is HTTPVersion.http20 + recv_event.set() self.tear_down() @@ -1096,6 +1107,8 @@ def wrap(*args): monkeypatch.setattr(hyper.http11.connection, 'wrap_socket', wrap) + recv_event = threading.Event() + def socket_handler(listener): sock = listener.accept()[0] @@ -1126,6 +1139,9 @@ def socket_handler(listener): d.data = b'1234567890' * 2 d.flags.add('END_STREAM') sock.send(d.serialize()) + + # keep the socket open for clean shutdown + recv_event.wait(5) sock.close() self._start_server(socket_handler) @@ -1139,6 +1155,7 @@ def socket_handler(listener): assert r.headers[b'Content-Type'] == b'not/real' assert r.content == b'1234567890' * 2 + recv_event.set() self.tear_down() def test_adapter_sending_values(self, monkeypatch, frame_buffer): @@ -1154,6 +1171,8 @@ def wrap(*args): monkeypatch.setattr(hyper.http11.connection, 'wrap_socket', wrap) + recv_event = threading.Event() + def socket_handler(listener): sock = listener.accept()[0] @@ -1184,6 +1203,9 @@ def socket_handler(listener): d.data = b'1234567890' * 2 d.flags.add('END_STREAM') sock.send(d.serialize()) + + # keep the socket open for clean shutdown + recv_event.wait(5) sock.close() self._start_server(socket_handler) @@ -1204,4 +1226,5 @@ def socket_handler(listener): assert isinstance(frames[-1], DataFrame) assert frames[-1].data == b'hi there' + recv_event.set() self.tear_down() From 8089d972a2e0e72d20700de043ea8991e0cc1244 Mon Sep 17 00:00:00 2001 From: Hiroaki KAWAI <hiroaki.kawai@gmail.com> Date: Thu, 23 Feb 2017 16:50:40 +0000 Subject: [PATCH 18/60] revert autopep8 blank lines --- test/test_hyper.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/test/test_hyper.py b/test/test_hyper.py index 27f4e7b2..66ff6f42 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -48,7 +48,6 @@ def frame_buffer(): class TestHyperConnection(object): - def test_connections_accept_hosts_and_ports(self): c = HTTP20Connection(host='www.google.com', port=8080) assert c.host == 'www.google.com' @@ -500,7 +499,6 @@ def test_headers_with_continuation(self): def test_send_tolerate_peer_gone(self): class ErrorSocket(DummySocket): - def sendall(self, data): raise socket.error(errno.EPIPE) @@ -703,7 +701,6 @@ def test_incrementing_window_after_close(self): class FrameEncoderMixin(object): - def setup_method(self, method): self.frames = [] self.encoder = Encoder() @@ -737,7 +734,6 @@ def add_data_frame(self, stream_id, data, end_stream=False): class TestServerPush(FrameEncoderMixin): - def request(self, enable_push=True): self.conn = HTTP20Connection('www.google.com', enable_push=enable_push) self.conn._sock = DummySocket() @@ -963,7 +959,6 @@ def test_pushed_requests_ignore_unexpected_headers(self): class TestResponse(object): - def test_status_is_stripped_from_headers(self): headers = HTTPHeaderMap([(':status', '200')]) resp = HTTP20Response(headers, None) @@ -1114,7 +1109,6 @@ def test_response_version(self): class TestHTTP20Adapter(object): - def test_adapter_reuses_connections(self): a = HTTP20Adapter() conn1 = a.get_connection('http2bin.org', 80, 'http') @@ -1138,7 +1132,6 @@ def test_adapter_accept_client_certificate(self): class TestUtilities(object): - def test_combining_repeated_headers(self): test_headers = [ (b'key1', b'val1'), @@ -1466,7 +1459,6 @@ def test_reset_pushed_streams_when_push_disabled(self): # Some utility classes for the tests. class NullEncoder(object): - @staticmethod def encode(headers): @@ -1483,7 +1475,6 @@ def to_str(v): class FixedDecoder(object): - def __init__(self, result): self.result = result @@ -1492,7 +1483,6 @@ def decode(self, headers): class DummySocket(object): - def __init__(self): self.queue = [] self._buffer = BytesIO() @@ -1530,7 +1520,6 @@ def fill(self): class DummyStream(object): - def __init__(self, data, trailers=None): self.data = data self.data_frames = [] From 3a02fe19df0791b7d195e5a784d7313631c92d6a Mon Sep 17 00:00:00 2001 From: Hiroaki KAWAI <hiroaki.kawai@gmail.com> Date: Thu, 23 Feb 2017 17:09:53 +0000 Subject: [PATCH 19/60] fix socket.recv usage --- test/test_integration.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test_integration.py b/test/test_integration.py index 273b4c18..72da6156 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -76,10 +76,11 @@ def reusable_frame_buffer(buffer): def receive_preamble(sock): # Receive the HTTP/2 'preamble'. client_preface = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' - timeout = time.time() + 5 got = b'' - while len(got) < len(client_preface) and time.time() < timeout: - got += sock.recv(len(client_preface) - len(got)) + while len(got) < len(client_preface): + tmp = sock.recv(len(client_preface) - len(got)) + assert len(tmp) > 0, "unexpected EOF" + got += tmp assert got == client_preface, "client preface mismatch" From cb49fef5f8de6a132c642833bf072bc8669244cf Mon Sep 17 00:00:00 2001 From: Vasiliy Faronov <vfaronov@gmail.com> Date: Sat, 4 Mar 2017 20:56:30 +0300 Subject: [PATCH 20/60] Fix upgrading to h2c Fixes https://github.com/Lukasa/hyper/issues/312 --- hyper/http11/connection.py | 8 +++++++- test/test_integration_http11.py | 2 +- test_release.py | 19 ++++++++++++++++++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 8025c740..4de673bb 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -212,8 +212,14 @@ def get_response(self): self._sock.advance_buffer(response.consumed) + # Check for a successful "switching protocols to h2c" response. + # "Connection: upgrade" is not strictly necessary on the receiving end, + # but we want to fail fast on broken servers or intermediaries: + # https://github.com/Lukasa/hyper/issues/312. + # Connection options are case-insensitive, while upgrade tokens are + # case-sensitive: https://github.com/httpwg/http11bis/issues/8. if (response.status == 101 and - b'upgrade' in headers['connection'] and + b'upgrade' in map(bytes.lower, headers['connection']) and H2C_PROTOCOL.encode('utf-8') in headers['upgrade']): raise HTTPUpgrade(H2C_PROTOCOL, self._sock) diff --git a/test/test_integration_http11.py b/test/test_integration_http11.py index 53cd83a6..8c70f266 100644 --- a/test/test_integration_http11.py +++ b/test/test_integration_http11.py @@ -282,7 +282,7 @@ def socket_handler(listener): b'HTTP/1.1 101 Upgrade\r\n' b'Server: socket-level-server\r\n' b'Content-Length: 0\r\n' - b'Connection: upgrade\r\n' + b'Connection: Upgrade\r\n' b'Upgrade: h2c\r\n' b'\r\n' ) diff --git a/test_release.py b/test_release.py index 498fae71..903994a9 100644 --- a/test_release.py +++ b/test_release.py @@ -15,7 +15,8 @@ import random import requests import threading -from hyper import HTTP20Connection, HTTP11Connection +from hyper import HTTP20Connection, HTTP11Connection, HTTPConnection +from hyper.common.util import HTTPVersion from hyper.contrib import HTTP20Adapter logging.basicConfig(level=logging.INFO) @@ -149,3 +150,19 @@ def test_hitting_httpbin_org_http11(self): assert resp.status == 200 assert resp.read() + + def test_hitting_nghttp2_org_via_h2c_upgrade(self): + """ + This tests our support for cleartext HTTP/1.1 -> HTTP/2 upgrade + against the most common open source HTTP/2 server implementation. + """ + c = HTTPConnection('nghttp2.org:80') + + # Make the request. + c.request('GET', '/') + response = c.get_response() + + # Check that the response is OK and that we did upgrade to HTTP/2. + assert response.status == 200 + assert response.read() + assert response.version == HTTPVersion.http20 From 2453eb3812c0918531c06f462cdf2c58262a6d34 Mon Sep 17 00:00:00 2001 From: Vasiliy Faronov <vfaronov@gmail.com> Date: Sat, 4 Mar 2017 22:08:02 +0300 Subject: [PATCH 21/60] Document how to override certificate verification --- docs/source/advanced.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index 1c42068b..8269ef0e 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -71,7 +71,14 @@ SSL/TLS Certificate Verification By default, all HTTP/2 connections are made over TLS, and ``hyper`` bundles certificate authorities that it uses to verify the offered TLS certificates. -Currently certificate verification cannot be disabled. + +You can change how certificates are verified by passing your own +``ssl_context`` to the :class:`HTTPConnection <hyper.HTTPConnection>`. +For example, this will disable verification altogether:: + + import ssl + context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + conn = HTTPConnection('http2bin.org:443', ssl_context=context) Streaming Uploads ----------------- From c6ae836909c5bcee93019611d8fac514adc6454c Mon Sep 17 00:00:00 2001 From: Cory Benfield <lukasaoz@gmail.com> Date: Sat, 4 Mar 2017 20:35:35 +0000 Subject: [PATCH 22/60] Prefer PROTOCOL_SSLv23 to PROTOCOL_TLSv1_2. --- docs/source/advanced.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index 8269ef0e..75f36fe9 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -77,7 +77,7 @@ You can change how certificates are verified by passing your own For example, this will disable verification altogether:: import ssl - context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) conn = HTTPConnection('http2bin.org:443', ssl_context=context) Streaming Uploads From 12d094102fd6428675358477525663f605df9fab Mon Sep 17 00:00:00 2001 From: Vasiliy Faronov <vfaronov@gmail.com> Date: Sun, 5 Mar 2017 03:15:04 +0300 Subject: [PATCH 23/60] Fix the documentation on overriding the SSL context I completely botched that one. It seemed to work, but it was falling back to HTTP/1.1. And I missed hyper.tls.init_context. --- docs/source/advanced.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst index 75f36fe9..a1d53085 100644 --- a/docs/source/advanced.rst +++ b/docs/source/advanced.rst @@ -72,12 +72,15 @@ SSL/TLS Certificate Verification By default, all HTTP/2 connections are made over TLS, and ``hyper`` bundles certificate authorities that it uses to verify the offered TLS certificates. -You can change how certificates are verified by passing your own -``ssl_context`` to the :class:`HTTPConnection <hyper.HTTPConnection>`. -For example, this will disable verification altogether:: +You can change how certificates are verified by getting a new SSL context +from :func:`hyper.tls.init_context`, tweaking its options, and passing it +to the :class:`HTTPConnection <hyper.HTTPConnection>`. For example, this will +disable verification altogether:: import ssl - context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context = hyper.tls.init_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE conn = HTTPConnection('http2bin.org:443', ssl_context=context) Streaming Uploads From 6ef89c3dcef1c44743fa83dd1f8f38b9e830b89d Mon Sep 17 00:00:00 2001 From: Sobolev Nikita <mail@sobolevn.me> Date: Fri, 14 Apr 2017 13:17:21 +0300 Subject: [PATCH 24/60] Updates README.rst with svg badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0e01b7f7..294a062b 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ Hyper: HTTP/2 Client for Python .. image:: https://raw.github.com/Lukasa/hyper/development/docs/source/images/hyper.png -.. image:: https://travis-ci.org/Lukasa/hyper.png?branch=master +.. image:: https://travis-ci.org/Lukasa/hyper.svg?branch=master :target: https://travis-ci.org/Lukasa/hyper HTTP is changing under our feet. HTTP/1.1, our old friend, is being From b033641a8e0c389049069fd91ef1b171b5b08779 Mon Sep 17 00:00:00 2001 From: Kostya Esmukov <kostya@esmukov.ru> Date: Sat, 6 May 2017 13:28:13 +0300 Subject: [PATCH 25/60] Add tests for requests adapter `verify` option --- test/test_hyper.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/test/test_hyper.py b/test/test_hyper.py index 66ff6f42..fdee058e 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -18,7 +18,7 @@ ) from hyper.common.headers import HTTPHeaderMap from hyper.common.util import to_bytestring, HTTPVersion -from hyper.compat import zlib_compressobj, is_py2 +from hyper.compat import zlib_compressobj, is_py2, ssl from hyper.contrib import HTTP20Adapter import hyper.http20.errors as errors import errno @@ -31,6 +31,7 @@ TEST_DIR = os.path.abspath(os.path.dirname(__file__)) TEST_CERTS_DIR = os.path.join(TEST_DIR, 'certs') CLIENT_PEM_FILE = os.path.join(TEST_CERTS_DIR, 'nopassword.pem') +SERVER_CERT_FILE = os.path.join(TEST_CERTS_DIR, 'server.crt') def decode_frame(frame_data): @@ -1129,6 +1130,29 @@ def test_adapter_accept_client_certificate(self): 'http', cert=CLIENT_PEM_FILE) assert conn1 is conn2 + assert conn1._conn.ssl_context.check_hostname + assert conn1._conn.ssl_context.verify_mode == ssl.CERT_REQUIRED + + def test_adapter_respects_disabled_ca_verification(self): + a = HTTP20Adapter() + conn = a.get_connection( + 'http2bin.org', + 80, + 'http', + verify=False, + cert=CLIENT_PEM_FILE) + assert not conn._conn.ssl_context.check_hostname + assert conn._conn.ssl_context.verify_mode == ssl.CERT_NONE + + def test_adapter_respects_custom_ca_verification(self): + a = HTTP20Adapter() + conn = a.get_connection( + 'http2bin.org', + 80, + 'http', + verify=SERVER_CERT_FILE) + assert conn._conn.ssl_context.check_hostname + assert conn._conn.ssl_context.verify_mode == ssl.CERT_REQUIRED class TestUtilities(object): From c4f8a89a554cc9105193183f8bb46e38e1230779 Mon Sep 17 00:00:00 2001 From: Kostya Esmukov <kostya@esmukov.ru> Date: Sat, 6 May 2017 13:29:13 +0300 Subject: [PATCH 26/60] Respect requests adapter `verify` option --- hyper/contrib.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/hyper/contrib.py b/hyper/contrib.py index dccec518..5c17352d 100644 --- a/hyper/contrib.py +++ b/hyper/contrib.py @@ -15,7 +15,7 @@ HTTPAdapter = object from hyper.common.connection import HTTPConnection -from hyper.compat import urlparse +from hyper.compat import urlparse, ssl from hyper.tls import init_context @@ -29,7 +29,7 @@ def __init__(self, *args, **kwargs): #: A mapping between HTTP netlocs and ``HTTP20Connection`` objects. self.connections = {} - def get_connection(self, host, port, scheme, cert=None): + def get_connection(self, host, port, scheme, cert=None, verify=True): """ Gets an appropriate HTTP/2 connection object based on host/port/scheme/cert tuples. @@ -40,22 +40,29 @@ def get_connection(self, host, port, scheme, cert=None): port = 80 if not secure else 443 ssl_context = None - if cert is not None: + if not verify: + verify = False ssl_context = init_context(cert=cert) + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + elif verify is True and cert is not None: + ssl_context = init_context(cert=cert) + elif verify is not True: + ssl_context = init_context(cert_path=verify, cert=cert) try: - conn = self.connections[(host, port, scheme, cert)] + conn = self.connections[(host, port, scheme, cert, verify)] except KeyError: conn = HTTPConnection( host, port, secure=secure, ssl_context=ssl_context) - self.connections[(host, port, scheme, cert)] = conn + self.connections[(host, port, scheme, cert, verify)] = conn return conn - def send(self, request, stream=False, cert=None, **kwargs): + def send(self, request, stream=False, cert=None, verify=True, **kwargs): """ Sends a HTTP message to the server. """ @@ -64,7 +71,8 @@ def send(self, request, stream=False, cert=None, **kwargs): parsed.hostname, parsed.port, parsed.scheme, - cert=cert) + cert=cert, + verify=verify) # Build the selector. selector = parsed.path From ba6516341101317afcfeaebdeb9ea27c2ceca15a Mon Sep 17 00:00:00 2001 From: ex3me0 <ex3me0@users.noreply.github.com> Date: Tue, 30 May 2017 00:26:08 +0300 Subject: [PATCH 27/60] Fixed case, where "reason" absent --- hyper/http11/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyper/http11/parser.py b/hyper/http11/parser.py index ee391210..7b252feb 100644 --- a/hyper/http11/parser.py +++ b/hyper/http11/parser.py @@ -49,7 +49,7 @@ def parse_response(self, buffer): if index == -1: return None - version, status, reason = temp_buffer[0:index].split(None, 2) + version, status, reason = (temp_buffer[0:index].split(None, 2) + [''])[:3] if not version.startswith(b'HTTP/1.'): raise ParseError("Not HTTP/1.X!") From 8d64797b498bd928f9ef4cfdf98f281f734c9a54 Mon Sep 17 00:00:00 2001 From: ex3me0 <ex3me0@users.noreply.github.com> Date: Tue, 30 May 2017 00:45:18 +0300 Subject: [PATCH 28/60] Added test-case "response with empty reason" --- test/test_http11.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/test_http11.py b/test/test_http11.py index 5f0fcf7a..eddbc181 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -328,6 +328,23 @@ def test_chunked_overrides_body(self): received = b''.join(sock.queue) assert received == expected + + def test_response_with_empty_reason(self): + c = HTTP11Connection('httpbin.org') + c._sock = sock = DummySocket() + + sock._buffer = BytesIO( + b"HTTP/1.1 201\r\n" + b"Connection: close\r\n" + b"Server: Socket\r\n" + b"Content-Length: 0\r\n" + b"\r\n" + ) + + r = c.get_response() + + assert r.status == 201 + assert r.reason == b'' def test_get_response(self): c = HTTP11Connection('httpbin.org') From 0697c07f0678df6785a3bcbac75de4062b5ee294 Mon Sep 17 00:00:00 2001 From: ex3me0 <ex3me0@users.noreply.github.com> Date: Tue, 30 May 2017 00:57:59 +0300 Subject: [PATCH 29/60] Fixed according to PEP (79 chars) --- hyper/http11/parser.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hyper/http11/parser.py b/hyper/http11/parser.py index 7b252feb..b6a80cba 100644 --- a/hyper/http11/parser.py +++ b/hyper/http11/parser.py @@ -49,7 +49,8 @@ def parse_response(self, buffer): if index == -1: return None - version, status, reason = (temp_buffer[0:index].split(None, 2) + [''])[:3] + version, status, reason = \ + (temp_buffer[0:index].split(None, 2) + [''])[:3] if not version.startswith(b'HTTP/1.'): raise ParseError("Not HTTP/1.X!") From f2070e997d38c6ef0ddfcef8e5723bdac331add5 Mon Sep 17 00:00:00 2001 From: ex3me0 <ex3me0@users.noreply.github.com> Date: Tue, 30 May 2017 00:59:03 +0300 Subject: [PATCH 30/60] Removed spaces at blank lines --- test/test_http11.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_http11.py b/test/test_http11.py index eddbc181..dc9df224 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -328,7 +328,7 @@ def test_chunked_overrides_body(self): received = b''.join(sock.queue) assert received == expected - + def test_response_with_empty_reason(self): c = HTTP11Connection('httpbin.org') c._sock = sock = DummySocket() @@ -342,7 +342,7 @@ def test_response_with_empty_reason(self): ) r = c.get_response() - + assert r.status == 201 assert r.reason == b'' From c5367d23cbba9d0c2da71f49c84122418ddbb1ac Mon Sep 17 00:00:00 2001 From: ex3me0 <ex3me0@users.noreply.github.com> Date: Tue, 30 May 2017 01:08:55 +0300 Subject: [PATCH 31/60] Fixed using str instead of bytes --- hyper/http11/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyper/http11/parser.py b/hyper/http11/parser.py index b6a80cba..fc9fd8e1 100644 --- a/hyper/http11/parser.py +++ b/hyper/http11/parser.py @@ -50,7 +50,7 @@ def parse_response(self, buffer): return None version, status, reason = \ - (temp_buffer[0:index].split(None, 2) + [''])[:3] + (temp_buffer[0:index].split(None, 2) + [b''])[:3] if not version.startswith(b'HTTP/1.'): raise ParseError("Not HTTP/1.X!") From a690458a0da7f090f4e2c0bce612b0ec25e583df Mon Sep 17 00:00:00 2001 From: ex3me0 <ex3me0@users.noreply.github.com> Date: Tue, 30 May 2017 12:00:00 +0300 Subject: [PATCH 32/60] Fixed status-line --- test/test_http11.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_http11.py b/test/test_http11.py index dc9df224..7d4be5b6 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -334,7 +334,7 @@ def test_response_with_empty_reason(self): c._sock = sock = DummySocket() sock._buffer = BytesIO( - b"HTTP/1.1 201\r\n" + b"HTTP/1.1 201 \r\n" b"Connection: close\r\n" b"Server: Socket\r\n" b"Content-Length: 0\r\n" From ed270c0020b34c941f9afc544629bdcb72f32b6f Mon Sep 17 00:00:00 2001 From: ex3me0 <ex3me0@users.noreply.github.com> Date: Tue, 30 May 2017 12:02:15 +0300 Subject: [PATCH 33/60] Fixed line continuation according to PEP --- hyper/http11/parser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hyper/http11/parser.py b/hyper/http11/parser.py index fc9fd8e1..acc204f4 100644 --- a/hyper/http11/parser.py +++ b/hyper/http11/parser.py @@ -49,8 +49,8 @@ def parse_response(self, buffer): if index == -1: return None - version, status, reason = \ - (temp_buffer[0:index].split(None, 2) + [b''])[:3] + version, status, reason = ( + temp_buffer[0:index].split(None, 2) + [b''])[:3] if not version.startswith(b'HTTP/1.'): raise ParseError("Not HTTP/1.X!") From fa238d88ce942f56155e3812043184e1db03697a Mon Sep 17 00:00:00 2001 From: Kostya Esmukov <kostya@esmukov.ru> Date: Sat, 6 May 2017 13:38:16 +0300 Subject: [PATCH 34/60] Add HTTPS proxy support Squashed https://github.com/Lukasa/hyper/pull/322 # Conflicts: # hyper/contrib.py --- hyper/common/connection.py | 11 +- hyper/common/exceptions.py | 19 +++ hyper/contrib.py | 41 ++++- hyper/http11/connection.py | 134 +++++++++++---- hyper/http11/response.py | 21 ++- hyper/http20/connection.py | 59 ++++--- hyper/http20/exceptions.py | 3 +- test/server.py | 49 ++++-- test/test_abstraction.py | 6 +- test/test_http11.py | 61 ++++++- test/test_hyper.py | 35 ++++ test/test_integration.py | 278 +++++++++++++++++++++++++++++++- test/test_integration_http11.py | 151 ++++++++++++++++- 13 files changed, 775 insertions(+), 93 deletions(-) diff --git a/hyper/common/connection.py b/hyper/common/connection.py index 507a8ad7..e225852e 100644 --- a/hyper/common/connection.py +++ b/hyper/common/connection.py @@ -44,8 +44,9 @@ class HTTPConnection(object): :param proxy_host: (optional) The proxy to connect to. This can be an IP address or a host name and may include a port. :param proxy_port: (optional) The proxy port to connect to. If not provided - and one also isn't provided in the ``proxy`` parameter, defaults to - 8080. + and one also isn't provided in the ``proxy_host`` parameter, defaults + to 8080. + :param proxy_headers: (optional) The headers to send to a proxy. """ def __init__(self, host, @@ -56,6 +57,7 @@ def __init__(self, ssl_context=None, proxy_host=None, proxy_port=None, + proxy_headers=None, **kwargs): self._host = host @@ -63,12 +65,13 @@ def __init__(self, self._h1_kwargs = { 'secure': secure, 'ssl_context': ssl_context, 'proxy_host': proxy_host, 'proxy_port': proxy_port, - 'enable_push': enable_push + 'proxy_headers': proxy_headers, 'enable_push': enable_push } self._h2_kwargs = { 'window_manager': window_manager, 'enable_push': enable_push, 'secure': secure, 'ssl_context': ssl_context, - 'proxy_host': proxy_host, 'proxy_port': proxy_port + 'proxy_host': proxy_host, 'proxy_port': proxy_port, + 'proxy_headers': proxy_headers } # Add any unexpected kwargs to both dictionaries. diff --git a/hyper/common/exceptions.py b/hyper/common/exceptions.py index 268431ab..78dfefc9 100644 --- a/hyper/common/exceptions.py +++ b/hyper/common/exceptions.py @@ -71,3 +71,22 @@ class MissingCertFile(Exception): The certificate file could not be found. """ pass + + +# Create our own ConnectionError. +try: # pragma: no cover + ConnectionError = ConnectionError +except NameError: # pragma: no cover + class ConnectionError(Exception): + """ + An error occurred during connection to a host. + """ + + +class ProxyError(ConnectionError): + """ + An error occurred during connection to a proxy. + """ + def __init__(self, message, response): + self.response = response + super(ProxyError, self).__init__(message) diff --git a/hyper/contrib.py b/hyper/contrib.py index 5c17352d..9ccf21d3 100644 --- a/hyper/contrib.py +++ b/hyper/contrib.py @@ -9,7 +9,9 @@ from requests.adapters import HTTPAdapter from requests.models import Response from requests.structures import CaseInsensitiveDict - from requests.utils import get_encoding_from_headers + from requests.utils import ( + get_encoding_from_headers, select_proxy, prepend_scheme_if_needed + ) from requests.cookies import extract_cookies_to_jar except ImportError: # pragma: no cover HTTPAdapter = object @@ -29,7 +31,8 @@ def __init__(self, *args, **kwargs): #: A mapping between HTTP netlocs and ``HTTP20Connection`` objects. self.connections = {} - def get_connection(self, host, port, scheme, cert=None, verify=True): + def get_connection(self, host, port, scheme, cert=None, verify=True, + proxy=None): """ Gets an appropriate HTTP/2 connection object based on host/port/scheme/cert tuples. @@ -50,29 +53,51 @@ def get_connection(self, host, port, scheme, cert=None, verify=True): elif verify is not True: ssl_context = init_context(cert_path=verify, cert=cert) + if proxy: + proxy_headers = self.proxy_headers(proxy) + proxy_netloc = urlparse(proxy).netloc + else: + proxy_headers = None + proxy_netloc = None + + # We put proxy headers in the connection_key, because + # ``proxy_headers`` method might be overridden, so we can't + # rely on proxy headers being the same for the same proxies. + proxy_headers_key = (frozenset(proxy_headers.items()) + if proxy_headers else None) + connection_key = (host, port, scheme, cert, verify, + proxy_netloc, proxy_headers_key) try: - conn = self.connections[(host, port, scheme, cert, verify)] + conn = self.connections[connection_key] except KeyError: conn = HTTPConnection( host, port, secure=secure, - ssl_context=ssl_context) - self.connections[(host, port, scheme, cert, verify)] = conn + ssl_context=ssl_context, + proxy_host=proxy_netloc, + proxy_headers=proxy_headers) + self.connections[connection_key] = conn return conn - def send(self, request, stream=False, cert=None, verify=True, **kwargs): + def send(self, request, stream=False, cert=None, verify=True, proxies=None, + **kwargs): """ Sends a HTTP message to the server. """ + proxy = select_proxy(request.url, proxies) + if proxy: + proxy = prepend_scheme_if_needed(proxy, 'http') + parsed = urlparse(request.url) conn = self.get_connection( parsed.hostname, parsed.port, parsed.scheme, cert=cert, - verify=verify) + verify=verify, + proxy=proxy) # Build the selector. selector = parsed.path @@ -97,7 +122,7 @@ def send(self, request, stream=False, cert=None, verify=True, **kwargs): def build_response(self, request, resp): """ Builds a Requests' response object. This emulates most of the logic of - the standard fuction but deals with the lack of the ``.headers`` + the standard function but deals with the lack of the ``.headers`` property on the HTTP20Response object. Additionally, this function builds in a number of features that are diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 4de673bb..0603e68f 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -18,9 +18,11 @@ from .response import HTTP11Response from ..tls import wrap_socket, H2C_PROTOCOL from ..common.bufsocket import BufferedSocket -from ..common.exceptions import TLSUpgrade, HTTPUpgrade +from ..common.exceptions import TLSUpgrade, HTTPUpgrade, ProxyError from ..common.headers import HTTPHeaderMap -from ..common.util import to_bytestring, to_host_port_tuple, HTTPVersion +from ..common.util import ( + to_bytestring, to_host_port_tuple, to_native_string, HTTPVersion +) from ..compat import bytes # We prefer pycohttpparser to the pure-Python interpretation @@ -36,6 +38,43 @@ BODY_FLAT = 2 +def _create_tunnel(proxy_host, proxy_port, target_host, target_port, + proxy_headers=None): + """ + Sends CONNECT method to a proxy and returns a socket with established + connection to the target. + + :returns: socket + """ + conn = HTTP11Connection(proxy_host, proxy_port) + conn.request('CONNECT', '%s:%d' % (target_host, target_port), + headers=proxy_headers) + + resp = conn.get_response() + if resp.status != 200: + raise ProxyError( + "Tunnel connection failed: %d %s" % + (resp.status, to_native_string(resp.reason)), + response=resp + ) + return conn._sock + + +def _headers_to_http_header_map(headers): + # TODO turn this to a classmethod of HTTPHeaderMap + headers = headers or {} + if not isinstance(headers, HTTPHeaderMap): + if isinstance(headers, Mapping): + headers = HTTPHeaderMap(headers.items()) + elif isinstance(headers, Iterable): + headers = HTTPHeaderMap(headers) + else: + raise ValueError( + 'Header argument must be a dictionary or an iterable' + ) + return headers + + class HTTP11Connection(object): """ An object representing a single HTTP/1.1 connection to a server. @@ -53,14 +92,16 @@ class HTTP11Connection(object): :param proxy_host: (optional) The proxy to connect to. This can be an IP address or a host name and may include a port. :param proxy_port: (optional) The proxy port to connect to. If not provided - and one also isn't provided in the ``proxy`` parameter, + and one also isn't provided in the ``proxy_host`` parameter, defaults to 8080. + :param proxy_headers: (optional) The headers to send to a proxy. """ version = HTTPVersion.http11 def __init__(self, host, port=None, secure=None, ssl_context=None, - proxy_host=None, proxy_port=None, **kwargs): + proxy_host=None, proxy_port=None, proxy_headers=None, + **kwargs): if port is None: self.host, self.port = to_host_port_tuple(host, default_port=80) else: @@ -83,17 +124,21 @@ def __init__(self, host, port=None, secure=None, ssl_context=None, self.ssl_context = ssl_context self._sock = None + # Keep the current request method in order to be able to know + # in get_response() what was the request verb. + self._current_request_method = None + # Setup proxy details if applicable. - if proxy_host: - if proxy_port is None: - self.proxy_host, self.proxy_port = to_host_port_tuple( - proxy_host, default_port=8080 - ) - else: - self.proxy_host, self.proxy_port = proxy_host, proxy_port + if proxy_host and proxy_port is None: + self.proxy_host, self.proxy_port = to_host_port_tuple( + proxy_host, default_port=8080 + ) + elif proxy_host: + self.proxy_host, self.proxy_port = proxy_host, proxy_port else: self.proxy_host = None self.proxy_port = None + self.proxy_headers = proxy_headers #: The size of the in-memory buffer used to store data from the #: network. This is used as a performance optimisation. Increase buffer @@ -113,19 +158,28 @@ def connect(self): :returns: Nothing. """ if self._sock is None: - if not self.proxy_host: - host = self.host - port = self.port - else: - host = self.proxy_host - port = self.proxy_port - sock = socket.create_connection((host, port), 5) + if self.proxy_host and self.secure: + # Send http CONNECT method to a proxy and acquire the socket + sock = _create_tunnel( + self.proxy_host, + self.proxy_port, + self.host, + self.port, + proxy_headers=self.proxy_headers + ) + elif self.proxy_host: + # Simple http proxy + sock = socket.create_connection( + (self.proxy_host, self.proxy_port), + 5 + ) + else: + sock = socket.create_connection((self.host, self.port), 5) proto = None if self.secure: - assert not self.proxy_host, "Proxy with HTTPS not supported." - sock, proto = wrap_socket(sock, host, self.ssl_context) + sock, proto = wrap_socket(sock, self.host, self.ssl_context) log.debug("Selected protocol: %s", proto) sock = BufferedSocket(sock, self.network_buffer_size) @@ -154,25 +208,29 @@ def request(self, method, url, body=None, headers=None): :returns: Nothing. """ - headers = headers or {} - method = to_bytestring(method) + is_connect_method = b'CONNECT' == method.upper() + self._current_request_method = method + + if self.proxy_host and not self.secure: + # As per https://tools.ietf.org/html/rfc2068#section-5.1.2: + # The absoluteURI form is required when the request is being made + # to a proxy. + url = self._absolute_http_url(url) url = to_bytestring(url) - if not isinstance(headers, HTTPHeaderMap): - if isinstance(headers, Mapping): - headers = HTTPHeaderMap(headers.items()) - elif isinstance(headers, Iterable): - headers = HTTPHeaderMap(headers) - else: - raise ValueError( - 'Header argument must be a dictionary or an iterable' - ) + headers = _headers_to_http_header_map(headers) + + # Append proxy headers. + if self.proxy_host and not self.secure: + headers.update( + _headers_to_http_header_map(self.proxy_headers).items() + ) if self._sock is None: self.connect() - if self._send_http_upgrade: + if not is_connect_method and self._send_http_upgrade: self._add_upgrade_headers(headers) self._send_http_upgrade = False @@ -180,7 +238,7 @@ def request(self, method, url, body=None, headers=None): if body: body_type = self._add_body_headers(headers, body) - if b'host' not in headers: + if not is_connect_method and b'host' not in headers: headers[b'host'] = self.host # Begin by emitting the header block. @@ -192,6 +250,10 @@ def request(self, method, url, body=None, headers=None): return + def _absolute_http_url(self, url): + port_part = ':%d' % self.port if self.port != 80 else '' + return 'http://%s%s%s' % (self.host, port_part, url) + def get_response(self): """ Returns a response object. @@ -199,6 +261,9 @@ def get_response(self): This is an early beta, so the response object is pretty stupid. That's ok, we'll fix it later. """ + method = self._current_request_method + self._current_request_method = None + headers = HTTPHeaderMap() response = None @@ -228,7 +293,8 @@ def get_response(self): response.msg.tobytes(), headers, self._sock, - self + self, + method ) def _send_headers(self, method, url, headers): diff --git a/hyper/http11/response.py b/hyper/http11/response.py index 318ab659..8f3eb985 100644 --- a/hyper/http11/response.py +++ b/hyper/http11/response.py @@ -27,7 +27,8 @@ class HTTP11Response(object): version = HTTPVersion.http11 - def __init__(self, code, reason, headers, sock, connection=None): + def __init__(self, code, reason, headers, sock, connection=None, + request_method=None): #: The reason phrase returned by the server. self.reason = reason @@ -62,11 +63,19 @@ def __init__(self, code, reason, headers, sock, connection=None): b'chunked' in self.headers.get(b'transfer-encoding', []) ) - # One of the following must be true: we must expect that the connection - # will be closed following the body, or that a content-length was sent, - # or that we're getting a chunked response. - # FIXME: Remove naked assert, replace with something better. - assert self._expect_close or self._length is not None or self._chunked + # When content-length is absent and response is not chunked, + # body length is determined by connection closure. + # https://tools.ietf.org/html/rfc7230#section-3.3.3 + if self._length is None and not self._chunked: + # 200 response to a CONNECT request means that proxy has connected + # to the target host and it will start forwarding everything sent + # from the either side. Thus we must not try to read body of this + # response. Socket of current connection will be taken over by + # the code that has sent a CONNECT request. + if not (request_method is not None and + b'CONNECT' == request_method.upper() and + code == 200): + self._expect_close = True # This object is used for decompressing gzipped request bodies. Right # now we only support gzip because that's all the RFC mandates of us. diff --git a/hyper/http20/connection.py b/hyper/http20/connection.py index 8b2a71e8..54a3c99d 100644 --- a/hyper/http20/connection.py +++ b/hyper/http20/connection.py @@ -18,6 +18,7 @@ to_host_port_tuple, to_native_string, to_bytestring, HTTPVersion ) from ..compat import unicode, bytes +from ..http11.connection import _create_tunnel from .stream import Stream from .response import HTTP20Response, HTTP20Push from .window import FlowControlManager @@ -29,6 +30,7 @@ import socket import time import threading +import itertools log = logging.getLogger(__name__) @@ -90,15 +92,17 @@ class HTTP20Connection(object): :param proxy_host: (optional) The proxy to connect to. This can be an IP address or a host name and may include a port. :param proxy_port: (optional) The proxy port to connect to. If not provided - and one also isn't provided in the ``proxy`` parameter, defaults to - 8080. + and one also isn't provided in the ``proxy_host`` parameter, defaults + to 8080. + :param proxy_headers: (optional) The headers to send to a proxy. """ version = HTTPVersion.http20 def __init__(self, host, port=None, secure=None, window_manager=None, enable_push=False, ssl_context=None, proxy_host=None, - proxy_port=None, force_proto=None, **kwargs): + proxy_port=None, force_proto=None, proxy_headers=None, + **kwargs): """ Creates an HTTP/2 connection to a specific server. """ @@ -118,16 +122,16 @@ def __init__(self, host, port=None, secure=None, window_manager=None, self.ssl_context = ssl_context # Setup proxy details if applicable. - if proxy_host: - if proxy_port is None: - self.proxy_host, self.proxy_port = to_host_port_tuple( - proxy_host, default_port=8080 - ) - else: - self.proxy_host, self.proxy_port = proxy_host, proxy_port + if proxy_host and proxy_port is None: + self.proxy_host, self.proxy_port = to_host_port_tuple( + proxy_host, default_port=8080 + ) + elif proxy_host: + self.proxy_host, self.proxy_port = proxy_host, proxy_port else: self.proxy_host = None self.proxy_port = None + self.proxy_headers = proxy_headers #: The size of the in-memory buffer used to store data from the #: network. This is used as a performance optimisation. Increase buffer @@ -272,10 +276,18 @@ def request(self, method, url, body=None, headers=None): # being sent in the wrong order, which can lead to the out-of-order # messages with lower stream IDs being closed prematurely. with self._write_lock: + # Unlike HTTP/1.1, HTTP/2 (according to RFC 7540) doesn't require + # to use absolute URI when proxying. + stream_id = self.putrequest(method, url) default_headers = (':method', ':scheme', ':authority', ':path') - for name, value in headers.items(): + all_headers = headers.items() + if self.proxy_host and not self.secure: + proxy_headers = self.proxy_headers or {} + all_headers = itertools.chain(all_headers, + proxy_headers.items()) + for name, value in all_headers: is_default = to_native_string(name) in default_headers self.putheader(name, value, stream_id, replace=is_default) @@ -358,18 +370,25 @@ def connect(self): if self._sock is not None: return - if not self.proxy_host: - host = self.host - port = self.port + if self.proxy_host and self.secure: + # Send http CONNECT method to a proxy and acquire the socket + sock = _create_tunnel( + self.proxy_host, + self.proxy_port, + self.host, + self.port, + proxy_headers=self.proxy_headers + ) + elif self.proxy_host: + # Simple http proxy + sock = socket.create_connection( + (self.proxy_host, self.proxy_port) + ) else: - host = self.proxy_host - port = self.proxy_port - - sock = socket.create_connection((host, port)) + sock = socket.create_connection((self.host, self.port)) if self.secure: - assert not self.proxy_host, "Proxy with HTTPS not supported." - sock, proto = wrap_socket(sock, host, self.ssl_context, + sock, proto = wrap_socket(sock, self.host, self.ssl_context, force_proto=self.force_proto) else: proto = H2C_PROTOCOL diff --git a/hyper/http20/exceptions.py b/hyper/http20/exceptions.py index 69e25816..634ef284 100644 --- a/hyper/http20/exceptions.py +++ b/hyper/http20/exceptions.py @@ -5,6 +5,7 @@ This defines exceptions used in the HTTP/2 portion of hyper. """ +from ..common.exceptions import ConnectionError as CommonConnectionError class HTTP20Error(Exception): @@ -28,7 +29,7 @@ class HPACKDecodingError(HTTP20Error): pass -class ConnectionError(HTTP20Error): +class ConnectionError(CommonConnectionError, HTTP20Error): """ The remote party signalled an error affecting the entire HTTP/2 connection, and the connection has been closed. diff --git a/test/server.py b/test/server.py index 3f6ded4a..482bf734 100644 --- a/test/server.py +++ b/test/server.py @@ -15,6 +15,7 @@ import threading import socket import sys +from enum import Enum from hyper import HTTP20Connection from hyper.compat import ssl @@ -27,6 +28,23 @@ from hyper.tls import NPN_PROTOCOL +class SocketSecuritySetting(Enum): + """ + Server socket TLS wrapping strategy: + + SECURE - automatically wrap socket + INSECURE - never wrap + SECURE_NO_AUTO_WRAP - init context, but socket must be wrapped manually + + The values are needed to be able to convert ``secure`` boolean flag of + a client to a member of this enum: + ``socket_security = SocketSecuritySetting(secure)`` + """ + SECURE = True + INSECURE = False + SECURE_NO_AUTO_WRAP = 'NO_AUTO_WRAP' + + class SocketServerThread(threading.Thread): """ This method stolen wholesale from shazow/urllib3 under license. See @@ -42,16 +60,17 @@ def __init__(self, host='localhost', ready_event=None, h2=True, - secure=True): + socket_security=SocketSecuritySetting.SECURE): threading.Thread.__init__(self) self.socket_handler = socket_handler self.host = host - self.secure = secure + self.socket_security = socket_security self.ready_event = ready_event self.daemon = True - if self.secure: + if self.socket_security in (SocketSecuritySetting.SECURE, + SocketSecuritySetting.SECURE_NO_AUTO_WRAP): self.cxt = ssl.SSLContext(ssl.PROTOCOL_SSLv23) if ssl.HAS_NPN and h2: self.cxt.set_npn_protocols([NPN_PROTOCOL]) @@ -63,8 +82,8 @@ def _start_server(self): if sys.platform != 'win32': sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if self.secure: - sock = self.cxt.wrap_socket(sock, server_side=True) + if self.socket_security == SocketSecuritySetting.SECURE: + sock = self.wrap_socket(sock) sock.bind((self.host, 0)) self.port = sock.getsockname()[1] @@ -77,8 +96,8 @@ def _start_server(self): self.socket_handler(sock) sock.close() - def _wrap_socket(self, sock): - raise NotImplementedError() + def wrap_socket(self, sock): + return self.cxt.wrap_socket(sock, server_side=True) def run(self): self.server = self._start_server() @@ -92,7 +111,7 @@ class SocketLevelTest(object): def set_up(self, secure=True, proxy=False): self.host = None self.port = None - self.secure = secure if not proxy else False + self.socket_security = SocketSecuritySetting(secure) self.proxy = proxy self.server_thread = None @@ -105,14 +124,24 @@ def _start_server(self, socket_handler): socket_handler=socket_handler, ready_event=ready_event, h2=self.h2, - secure=self.secure + socket_security=self.socket_security ) self.server_thread.start() ready_event.wait() self.host = self.server_thread.host self.port = self.server_thread.port - self.secure = self.server_thread.secure + self.socket_security = self.server_thread.socket_security + + @property + def secure(self): + return self.socket_security in \ + (SocketSecuritySetting.SECURE, + SocketSecuritySetting.SECURE_NO_AUTO_WRAP) + + @secure.setter + def secure(self, value): + self.socket_security = SocketSecuritySetting(value) def get_connection(self): if self.h2: diff --git a/test/test_abstraction.py b/test/test_abstraction.py index 7c2cad1a..d48b3954 100644 --- a/test/test_abstraction.py +++ b/test/test_abstraction.py @@ -10,7 +10,7 @@ def test_h1_kwargs(self): c = HTTPConnection( 'test', 443, secure=False, window_manager=True, enable_push=True, ssl_context=False, proxy_host=False, proxy_port=False, - other_kwarg=True + proxy_headers=False, other_kwarg=True ) assert c._h1_kwargs == { @@ -18,6 +18,7 @@ def test_h1_kwargs(self): 'ssl_context': False, 'proxy_host': False, 'proxy_port': False, + 'proxy_headers': False, 'other_kwarg': True, 'enable_push': True, } @@ -26,7 +27,7 @@ def test_h2_kwargs(self): c = HTTPConnection( 'test', 443, secure=False, window_manager=True, enable_push=True, ssl_context=True, proxy_host=False, proxy_port=False, - other_kwarg=True + proxy_headers=False, other_kwarg=True ) assert c._h2_kwargs == { @@ -36,6 +37,7 @@ def test_h2_kwargs(self): 'ssl_context': True, 'proxy_host': False, 'proxy_port': False, + 'proxy_headers': False, 'other_kwarg': True, } diff --git a/test/test_http11.py b/test/test_http11.py index 7d4be5b6..27976647 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -170,7 +170,7 @@ def test_proxy_request(self): c.request('GET', '/get', headers={'User-Agent': 'hyper'}) expected = ( - b"GET /get HTTP/1.1\r\n" + b"GET http://httpbin.org/get HTTP/1.1\r\n" b"User-Agent: hyper\r\n" b"connection: Upgrade, HTTP2-Settings\r\n" b"upgrade: h2c\r\n" @@ -182,6 +182,65 @@ def test_proxy_request(self): assert received == expected + def test_proxy_request_with_non_standard_port(self): + c = HTTP11Connection('httpbin.org:8080', proxy_host='localhost') + c._sock = sock = DummySocket() + + c.request('GET', '/get', headers={'User-Agent': 'hyper'}) + + expected = ( + b"GET http://httpbin.org:8080/get HTTP/1.1\r\n" + b"User-Agent: hyper\r\n" + b"connection: Upgrade, HTTP2-Settings\r\n" + b"upgrade: h2c\r\n" + b"HTTP2-Settings: AAQAAP__\r\n" + b"host: httpbin.org\r\n" + b"\r\n" + ) + received = b''.join(sock.queue) + + assert received == expected + + def test_proxy_headers_presence_for_insecure_request(self): + c = HTTP11Connection( + 'httpbin.org', secure=False, proxy_host='localhost', + proxy_headers={'Proxy-Authorization': 'Basic ==='}) + c._sock = sock = DummySocket() + + c.request('GET', '/get', headers={'User-Agent': 'hyper'}) + + expected = ( + b"GET http://httpbin.org/get HTTP/1.1\r\n" + b"User-Agent: hyper\r\n" + b"proxy-authorization: Basic ===\r\n" + b"connection: Upgrade, HTTP2-Settings\r\n" + b"upgrade: h2c\r\n" + b"HTTP2-Settings: AAQAAP__\r\n" + b"host: httpbin.org\r\n" + b"\r\n" + ) + received = b''.join(sock.queue) + + assert received == expected + + def test_proxy_headers_absence_for_secure_request(self): + c = HTTP11Connection( + 'httpbin.org', secure=True, proxy_host='localhost', + proxy_headers={'Proxy-Authorization': 'Basic ==='}) + c._sock = sock = DummySocket() + + c.request('GET', '/get', headers={'User-Agent': 'hyper'}) + + expected = ( + b"GET /get HTTP/1.1\r\n" + b"User-Agent: hyper\r\n" + b"host: httpbin.org\r\n" + b"\r\n" + ) + received = b''.join(sock.queue) + + assert received == expected + def test_request_with_bytestring_body(self): c = HTTP11Connection('httpbin.org') c._sock = sock = DummySocket() diff --git a/test/test_hyper.py b/test/test_hyper.py index fdee058e..0556bb0c 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -585,6 +585,41 @@ def test_that_using_proxy_keeps_http_headers_intact(self): (b':path', b'/'), ] + def test_proxy_headers_presence_for_insecure_request(self): + sock = DummySocket() + c = HTTP20Connection( + 'www.google.com', secure=False, proxy_host='localhost', + proxy_headers={'Proxy-Authorization': 'Basic ==='} + ) + c._sock = sock + c.request('GET', '/') + s = c.recent_stream + + assert list(s.headers.items()) == [ + (b':method', b'GET'), + (b':scheme', b'http'), + (b':authority', b'www.google.com'), + (b':path', b'/'), + (b'proxy-authorization', b'Basic ==='), + ] + + def test_proxy_headers_absence_for_secure_request(self): + sock = DummySocket() + c = HTTP20Connection( + 'www.google.com', secure=True, proxy_host='localhost', + proxy_headers={'Proxy-Authorization': 'Basic ==='} + ) + c._sock = sock + c.request('GET', '/') + s = c.recent_stream + + assert list(s.headers.items()) == [ + (b':method', b'GET'), + (b':scheme', b'https'), + (b':authority', b'www.google.com'), + (b':path', b'/'), + ] + def test_recv_cb_n_times(self): sock = DummySocket() sock.can_read = True diff --git a/test/test_integration.py b/test/test_integration.py index 72da6156..05931274 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -6,6 +6,7 @@ This file defines integration-type tests for hyper. These are still not fully hitting the network, so that's alright. """ +import base64 import requests import threading import time @@ -17,7 +18,8 @@ from h2.frame_buffer import FrameBuffer from hyper.compat import ssl from hyper.contrib import HTTP20Adapter -from hyper.common.util import HTTPVersion +from hyper.common.exceptions import ProxyError +from hyper.common.util import HTTPVersion, to_bytestring from hyperframe.frame import ( Frame, SettingsFrame, WindowUpdateFrame, DataFrame, HeadersFrame, GoAwayFrame, RstStreamFrame @@ -28,7 +30,7 @@ REQUEST_CODES, REQUEST_CODES_LENGTH ) from hyper.http20.exceptions import ConnectionError, StreamResetError -from server import SocketLevelTest +from server import SocketLevelTest, SocketSecuritySetting # Turn off certificate verification for the tests. if ssl is not None: @@ -620,8 +622,8 @@ def socket_handler(listener): recv_event.set() self.tear_down() - def test_proxy_connection(self): - self.set_up(proxy=True) + def test_insecure_proxy_connection(self): + self.set_up(secure=False, proxy=True) data = [] req_event = threading.Event() @@ -672,6 +674,107 @@ def socket_handler(listener): recv_event.set() self.tear_down() + def test_secure_proxy_connection(self): + self.set_up(secure=SocketSecuritySetting.SECURE_NO_AUTO_WRAP, + proxy=True) + + data = [] + connect_request_headers = [] + req_event = threading.Event() + recv_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + # Read the CONNECT request + while not b''.join(connect_request_headers).endswith(b'\r\n\r\n'): + connect_request_headers.append(sock.recv(65535)) + + sock.send(b'HTTP/1.0 200 Connection established\r\n\r\n') + + sock = self.server_thread.wrap_socket(sock) + + receive_preamble(sock) + + data.append(sock.recv(65535)) + req_event.wait(5) + + h = HeadersFrame(1) + h.data = self.get_encoder().encode( + [ + (':status', 200), + ('content-type', 'not/real'), + ('content-length', 12), + ('server', 'socket-level-server') + ] + ) + h.flags.add('END_HEADERS') + sock.send(h.serialize()) + + d = DataFrame(1) + d.data = b'thisisaproxy' + d.flags.add('END_STREAM') + sock.send(d.serialize()) + + recv_event.wait(5) + sock.close() + + self._start_server(socket_handler) + c = self.get_connection() + c.request('GET', '/') + req_event.set() + r = c.get_response() + + assert r.status == 200 + assert len(r.headers) == 3 + assert r.headers[b'server'] == [b'socket-level-server'] + assert r.headers[b'content-length'] == [b'12'] + assert r.headers[b'content-type'] == [b'not/real'] + + assert r.read() == b'thisisaproxy' + + assert (to_bytestring( + 'CONNECT %s:%d HTTP/1.1\r\n\r\n' % (c.host, c.port)) == + b''.join(connect_request_headers)) + + recv_event.set() + self.tear_down() + + def test_failing_proxy_tunnel(self): + self.set_up(secure=SocketSecuritySetting.SECURE_NO_AUTO_WRAP, + proxy=True) + + recv_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + # Read the CONNECT request + connect_data = b'' + while not connect_data.endswith(b'\r\n\r\n'): + connect_data += sock.recv(65535) + + sock.send(b'HTTP/1.0 407 Proxy Authentication Required\r\n\r\n') + + recv_event.wait(5) + sock.close() + + self._start_server(socket_handler) + conn = self.get_connection() + + try: + conn.connect() + assert False, "Exception should have been thrown" + except ProxyError as e: + assert e.response.status == 407 + assert e.response.reason == b'Proxy Authentication Required' + + # Confirm the connection is closed. + assert conn._sock is None + + recv_event.set() + self.tear_down() + def test_resetting_stream_with_frames_in_flight(self): """ Hyper emits only one RST_STREAM frame, despite the other frames in @@ -1229,3 +1332,170 @@ def socket_handler(listener): recv_event.set() self.tear_down() + + def test_adapter_uses_proxies(self): + self.set_up(secure=SocketSecuritySetting.SECURE_NO_AUTO_WRAP, + proxy=True) + + send_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + # Read the CONNECT request + connect_data = b'' + while not connect_data.endswith(b'\r\n\r\n'): + connect_data += sock.recv(65535) + + sock.send(b'HTTP/1.0 200 Connection established\r\n\r\n') + + sock = self.server_thread.wrap_socket(sock) + + # We should get the initial request. + data = b'' + while not data.endswith(b'\r\n\r\n'): + data += sock.recv(65535) + + send_event.wait() + + # We need to send back a response. + resp = ( + b'HTTP/1.1 201 No Content\r\n' + b'Server: socket-level-server\r\n' + b'Content-Length: 0\r\n' + b'Connection: close\r\n' + b'\r\n' + ) + sock.send(resp) + + sock.close() + + self._start_server(socket_handler) + s = requests.Session() + s.proxies = {'all': 'http://%s:%s' % (self.host, self.port)} + s.mount('https://', HTTP20Adapter()) + send_event.set() + r = s.get('https://foobar/') + + assert r.status_code == 201 + assert len(r.headers) == 3 + assert r.headers[b'server'] == b'socket-level-server' + assert r.headers[b'content-length'] == b'0' + assert r.headers[b'connection'] == b'close' + + assert r.content == b'' + + self.tear_down() + + def test_adapter_uses_proxy_auth_for_secure(self): + self.set_up(secure=SocketSecuritySetting.SECURE_NO_AUTO_WRAP, + proxy=True) + + send_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + # Read the CONNECT request + connect_data = b'' + while not connect_data.endswith(b'\r\n\r\n'): + connect_data += sock.recv(65535) + + # Ensure that request contains the proper Proxy-Authorization + # header + assert (b'CONNECT foobar:443 HTTP/1.1\r\n' + b'Proxy-Authorization: Basic ' + + base64.b64encode(b'foo:bar') + b'\r\n' + b'\r\n') == connect_data + + sock.send(b'HTTP/1.0 200 Connection established\r\n\r\n') + + sock = self.server_thread.wrap_socket(sock) + + # We should get the initial request. + data = b'' + while not data.endswith(b'\r\n\r\n'): + data += sock.recv(65535) + # Ensure that proxy headers are not passed via tunnelled connection + assert b'Proxy-Authorization:' not in data + + send_event.wait() + + # We need to send back a response. + resp = ( + b'HTTP/1.1 201 No Content\r\n' + b'Server: socket-level-server\r\n' + b'Content-Length: 0\r\n' + b'Connection: close\r\n' + b'\r\n' + ) + sock.send(resp) + + sock.close() + + self._start_server(socket_handler) + s = requests.Session() + s.proxies = {'all': 'http://foo:bar@%s:%s' % (self.host, self.port)} + s.mount('https://', HTTP20Adapter()) + send_event.set() + r = s.get('https://foobar/') + + assert r.status_code == 201 + assert len(r.headers) == 3 + assert r.headers[b'server'] == b'socket-level-server' + assert r.headers[b'content-length'] == b'0' + assert r.headers[b'connection'] == b'close' + + assert r.content == b'' + + self.tear_down() + + def test_adapter_uses_proxy_auth_for_insecure(self): + self.set_up(secure=False, proxy=True) + + send_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + # We should get the initial request. + connect_data = b'' + while not connect_data.endswith(b'\r\n\r\n'): + connect_data += sock.recv(65535) + + # Ensure that request contains the proper Proxy-Authorization + # header + assert (b'Proxy-Authorization: Basic ' + + base64.b64encode(b'foo:bar') + b'\r\n' + ).lower() in connect_data.lower() + + send_event.wait() + + # We need to send back a response. + resp = ( + b'HTTP/1.1 201 No Content\r\n' + b'Server: socket-level-server\r\n' + b'Content-Length: 0\r\n' + b'Connection: close\r\n' + b'\r\n' + ) + sock.send(resp) + + sock.close() + + self._start_server(socket_handler) + s = requests.Session() + s.proxies = {'all': 'http://foo:bar@%s:%s' % (self.host, self.port)} + s.mount('http://', HTTP20Adapter()) + send_event.set() + r = s.get('http://foobar/') + + assert r.status_code == 201 + assert len(r.headers) == 3 + assert r.headers[b'server'] == b'socket-level-server' + assert r.headers[b'content-length'] == b'0' + assert r.headers[b'connection'] == b'close' + + assert r.content == b'' + + self.tear_down() diff --git a/test/test_integration_http11.py b/test/test_integration_http11.py index 8c70f266..ee318797 100644 --- a/test/test_integration_http11.py +++ b/test/test_integration_http11.py @@ -11,8 +11,9 @@ import pytest from hyper.compat import ssl -from server import SocketLevelTest +from server import SocketLevelTest, SocketSecuritySetting from hyper.common.exceptions import HTTPUpgrade +from hyper.common.util import to_bytestring # Turn off certificate verification for the tests. if ssl is not None: @@ -119,14 +120,110 @@ def socket_handler(listener): assert r.read() == b'hellotheresirfinalfantasy' - def test_proxy_request_response(self): - self.set_up(proxy=True) + def test_closing_response_without_headers(self): + self.set_up() + + send_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + # We should get the initial request. + data = b'' + while not data.endswith(b'\r\n\r\n'): + data += sock.recv(65535) + + send_event.wait() + + # We need to send back a response. + resp = ( + b'HTTP/1.1 200 OK\r\n' + b'Server: socket-level-server\r\n' + b'\r\n' + ) + sock.send(resp) + + sock.send(b'hi') + + sock.close() + + self._start_server(socket_handler) + c = self.get_connection() + c.request('GET', '/') + send_event.set() + r = c.get_response() + + assert r.status == 200 + assert r.reason == b'OK' + assert len(r.headers) == 1 + assert r.headers[b'server'] == [b'socket-level-server'] + + assert r.read() == b'hi' + + assert c._sock is None + + def test_insecure_proxy_request_response(self): + self.set_up(secure=False, proxy=True) + + send_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + # We should get the initial request. + data = b'' + while not data.endswith(b'\r\n\r\n'): + data += sock.recv(65535) + + send_event.wait() + + # We need to send back a response. + resp = ( + b'HTTP/1.1 201 No Content\r\n' + b'Server: socket-level-server\r\n' + b'Content-Length: 0\r\n' + b'Connection: close\r\n' + b'\r\n' + ) + sock.send(resp) + + sock.close() + self._start_server(socket_handler) + c = self.get_connection() + c.request('GET', '/') + send_event.set() + r = c.get_response() + + assert r.status == 201 + assert r.reason == b'No Content' + assert len(r.headers) == 3 + assert r.headers[b'server'] == [b'socket-level-server'] + assert r.headers[b'content-length'] == [b'0'] + assert r.headers[b'connection'] == [b'close'] + + assert r.read() == b'' + + assert c._sock is None + + def test_secure_proxy_request_response(self): + self.set_up(secure=SocketSecuritySetting.SECURE_NO_AUTO_WRAP, + proxy=True) + + connect_request_headers = [] send_event = threading.Event() def socket_handler(listener): sock = listener.accept()[0] + # Read the CONNECT request + while not b''.join(connect_request_headers).endswith(b'\r\n\r\n'): + connect_request_headers.append(sock.recv(65535)) + + sock.send(b'HTTP/1.0 200 Connection established\r\n\r\n') + + sock = self.server_thread.wrap_socket(sock) + # We should get the initial request. data = b'' while not data.endswith(b'\r\n\r\n'): @@ -161,8 +258,56 @@ def socket_handler(listener): assert r.read() == b'' + assert (to_bytestring( + 'CONNECT %s:%d HTTP/1.1\r\n\r\n' % (c.host, c.port)) == + b''.join(connect_request_headers)) + assert c._sock is None + def test_proxy_connection_close_is_respected(self): + self.set_up(secure=False, proxy=True) + + send_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + # We should get the initial request. + data = b'' + while not data.endswith(b'\r\n\r\n'): + data += sock.recv(65535) + + send_event.wait() + + # We need to send back a response. + resp = ( + b'HTTP/1.0 407 Proxy Authentication Required\r\n' + b'Proxy-Authenticate: Basic realm="proxy"\r\n' + b'Proxy-Connection: close\r\n' + b'\r\n' + ) + sock.send(resp) + + sock.close() + + self._start_server(socket_handler) + conn = self.get_connection() + conn.request('GET', '/') + send_event.set() + + r = conn.get_response() + + assert r.status == 407 + assert r.reason == b'Proxy Authentication Required' + assert len(r.headers) == 2 + assert r.headers[b'proxy-authenticate'] == [b'Basic realm="proxy"'] + assert r.headers[b'proxy-connection'] == [b'close'] + + assert r.read() == b'' + + # Confirm the connection is closed. + assert conn._sock is None + def test_response_with_body(self): self.set_up() From 87d10bb4cd94c970eaae18faedb49e8224e379c5 Mon Sep 17 00:00:00 2001 From: Oliver Brown <oliver.brown14@imperial.ac.uk> Date: Tue, 6 Jun 2017 18:39:47 +0100 Subject: [PATCH 35/60] Check HTTP11 socket is initialized before closing --- hyper/http11/connection.py | 3 ++- test/test_http11.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 4de673bb..a4edfd50 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -385,7 +385,8 @@ def close(self): .. warning:: This method should absolutely only be called when you are certain the connection object is no longer needed. """ - self._sock.close() + if self._sock is not None: + self._sock.close() self._sock = None # The following two methods are the implementation of the context manager diff --git a/test/test_http11.py b/test/test_http11.py index 7d4be5b6..3390f213 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -529,6 +529,10 @@ def read(self, size): assert 'File-like bodies must return bytestrings. ' \ 'Got: {}'.format(int) in str(exc_info) + def test_close_with_uninitialized_socket(self): + c = HTTP11Connection('httpbin.org') + c.close() + class TestHTTP11Response(object): def test_short_circuit_read(self): From 0e2fca89013896d86d8b850da40004360044b2a0 Mon Sep 17 00:00:00 2001 From: plucury <plucury@gmail.com> Date: Wed, 16 Nov 2016 11:11:00 +0800 Subject: [PATCH 36/60] Using single lock model in http20 connection --- hyper/http20/connection.py | 59 +++++++++++--------------------------- test/test_integration.py | 40 +++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 44 deletions(-) diff --git a/hyper/http20/connection.py b/hyper/http20/connection.py index 8b2a71e8..b81b60eb 100644 --- a/hyper/http20/connection.py +++ b/hyper/http20/connection.py @@ -139,36 +139,9 @@ def __init__(self, host, port=None, secure=None, window_manager=None, # Concurrency # - # Use one lock (_lock) to synchronize any interaction with global - # connection state, e.g. stream creation/deletion. - # - # It's ok to use the same in lock all these cases as they occur at - # different/linked points in the connection's lifecycle. - # - # Use another 2 locks (_write_lock, _read_lock) to synchronize - # - _send_cb - # - _recv_cb - # respectively. - # - # I.e, send/recieve on the connection and its streams are serialized - # separately across the threads accessing the connection. This is a - # simple way of providing thread-safety. - # - # _write_lock and _read_lock synchronize all interactions between - # streams and the connnection. There is a third I/O callback, - # _close_stream, passed to a stream's constructor. It does not need to - # be synchronized, it uses _send_cb internally (which is serialized); - # its other activity (safe deletion of the stream from self.streams) - # does not require synchronization. - # - # _read_lock may be acquired when already holding the _write_lock, - # when they both held it is always by acquiring _write_lock first. - # - # Either _read_lock or _write_lock may be acquired whilst holding _lock - # which should always be acquired before either of the other two. + # Use one universal lock (_lock) to synchronize all interaction + # with global connection state, _send_cb and _recv_cb. self._lock = threading.RLock() - self._write_lock = threading.RLock() - self._read_lock = threading.RLock() # Create the mutable state. self.__wm_class = window_manager or FlowControlManager @@ -232,7 +205,7 @@ def ping(self, opaque_data): :returns: Nothing """ self.connect() - with self._write_lock: + with self._lock: with self._conn as conn: conn.ping(to_bytestring(opaque_data)) self._send_outstanding_data() @@ -271,7 +244,7 @@ def request(self, method, url, body=None, headers=None): # If threads interleave these operations, it could result in messages # being sent in the wrong order, which can lead to the out-of-order # messages with lower stream IDs being closed prematurely. - with self._write_lock: + with self._lock: stream_id = self.putrequest(method, url) default_headers = (':method', ':scheme', ':authority', ':path') @@ -464,10 +437,10 @@ def _send_outstanding_data(self, tolerate_peer_gone=False, send_empty=True): # Concurrency # - # Hold _write_lock; getting and writing data from _conn is synchronized + # Hold _lock; getting and writing data from _conn is synchronized # # I/O occurs while the lock is held; waiting threads will see a delay. - with self._write_lock: + with self._lock: with self._conn as conn: data = conn.data_to_send() if data or send_empty: @@ -557,9 +530,9 @@ def endheaders(self, message_body=None, final=False, stream_id=None): # Concurrency: # - # Hold _write_lock: synchronize access to the connection's HPACK + # Hold _lock: synchronize access to the connection's HPACK # encoder and decoder and the subsquent write to the connection - with self._write_lock: + with self._lock: stream.send_headers(headers_only) # Send whatever data we have. @@ -622,10 +595,10 @@ def _send_cb(self, data, tolerate_peer_gone=False): """ # Concurrency # - # Hold _write_lock: ensures only writer at a time + # Hold _lock: ensures only writer at a time # # I/O occurs while the lock is held; waiting threads will see a delay. - with self._write_lock: + with self._lock: try: self._sock.sendall(data) except socket.error as e: @@ -640,12 +613,12 @@ def _adjust_receive_window(self, frame_len): """ # Concurrency # - # Hold _write_lock; synchronize the window manager update and the + # Hold _lock; synchronize the window manager update and the # subsequent potential write to the connection # # I/O may occur while the lock is held; waiting threads may see a # delay. - with self._write_lock: + with self._lock: increment = self.window_manager._handle_frame(frame_len) if increment: @@ -667,7 +640,7 @@ def _single_read(self): # Synchronizes reading the data # # I/O occurs while the lock is held; waiting threads will see a delay. - with self._read_lock: + with self._lock: if self._sock is None: raise ConnectionError('tried to read after connection close') self._sock.fill() @@ -761,7 +734,7 @@ def _recv_cb(self, stream_id=0): # re-acquired in the calls to self._single_read. # # I/O occurs while the lock is held; waiting threads will see a delay. - with self._read_lock: + with self._lock: log.debug('recv for stream %d with %s already present', stream_id, self.recent_recv_streams) @@ -812,11 +785,11 @@ def _send_rst_frame(self, stream_id, error_code): """ # Concurrency # - # Hold _write_lock; synchronize generating the reset frame and writing + # Hold _lock; synchronize generating the reset frame and writing # it # # I/O occurs while the lock is held; waiting threads will see a delay. - with self._write_lock: + with self._lock: with self._conn as conn: conn.reset_stream(stream_id, error_code=error_code) self._send_outstanding_data() diff --git a/test/test_integration.py b/test/test_integration.py index 72da6156..e3b1c827 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -14,6 +14,7 @@ import pytest from contextlib import contextmanager from mock import patch +from concurrent.futures import ThreadPoolExecutor, TimeoutError from h2.frame_buffer import FrameBuffer from hyper.compat import ssl from hyper.contrib import HTTP20Adapter @@ -1039,7 +1040,6 @@ def test_version_after_http_upgrade(self): def socket_handler(listener): sock = listener.accept()[0] - # We should get the initial request. data = b'' while not data.endswith(b'\r\n\r\n'): @@ -1089,6 +1089,44 @@ def socket_handler(listener): self.tear_down() + def test_connection_and_send_simultaneously(self): + # Since deadlock occurs probabilistic, + # It still has deadlock probability + # even the testcase is passed. + self.set_up() + + recv_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + receive_preamble(sock) + sock.recv(65535) + + recv_event.set() + sock.close() + + def do_req(conn): + conn.request('GET', '/') + recv_event.wait() + + def do_connect(conn): + conn.connect() + + self._start_server(socket_handler) + conn = self.get_connection() + + pool = ThreadPoolExecutor(max_workers=2) + pool.submit(do_connect, conn) + f = pool.submit(do_req, conn) + + try: + f.result(timeout=10) + except TimeoutError: + assert False + + self.tear_down() + @patch('hyper.http20.connection.H2_NPN_PROTOCOLS', PROTOCOLS) class TestRequestsAdapter(SocketLevelTest): From 35fec8d7090651cfe71d9fae22e8462e7fcf92b3 Mon Sep 17 00:00:00 2001 From: Andrew Conti <aconti@kemptechnologies.com> Date: Wed, 14 Jun 2017 15:32:45 -0400 Subject: [PATCH 37/60] Force pytest to 2.7 --- test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_requirements.txt b/test_requirements.txt index e7deae72..dcaff945 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,4 +1,4 @@ -pytest +pytest>=2.7 pytest-xdist pytest-cov requests From 834585262308ebc479259eadf917a6032d349d3f Mon Sep 17 00:00:00 2001 From: KOLANICH <KOLANICH@users.noreply.github.com> Date: Wed, 5 Jul 2017 12:34:02 +0400 Subject: [PATCH 38/60] Convert headers to strings in HTTP20Adapter to fix #324 (#335) * Convert headers to strings in HTTP20Adapter to fix #324 * Update contrib.py * Update test_integration.py * Update test_integration.py * Update contrib.py * Update contrib.py * Update contrib.py * Update contrib.py --- hyper/contrib.py | 6 +++++- test/test_integration.py | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/hyper/contrib.py b/hyper/contrib.py index 9ccf21d3..c269ab90 100644 --- a/hyper/contrib.py +++ b/hyper/contrib.py @@ -19,6 +19,7 @@ from hyper.common.connection import HTTPConnection from hyper.compat import urlparse, ssl from hyper.tls import init_context +from hyper.common.util import to_native_string class HTTP20Adapter(HTTPAdapter): @@ -132,7 +133,10 @@ def build_response(self, request, resp): response = Response() response.status_code = resp.status - response.headers = CaseInsensitiveDict(resp.headers.iter_raw()) + response.headers = CaseInsensitiveDict(( + map(to_native_string, h) + for h in resp.headers.iter_raw() + )) response.raw = resp response.reason = resp.reason response.encoding = get_encoding_from_headers(response.headers) diff --git a/test/test_integration.py b/test/test_integration.py index 3cacd468..e1c87673 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -1294,7 +1294,7 @@ def socket_handler(listener): # Assert about the received values. assert r.status_code == 200 - assert r.headers[b'Content-Type'] == b'not/real' + assert r.headers['Content-Type'] == 'not/real' assert r.content == b'1234567890' * 2 recv_event.set() @@ -1417,9 +1417,9 @@ def socket_handler(listener): assert r.status_code == 201 assert len(r.headers) == 3 - assert r.headers[b'server'] == b'socket-level-server' - assert r.headers[b'content-length'] == b'0' - assert r.headers[b'connection'] == b'close' + assert r.headers['server'] == 'socket-level-server' + assert r.headers['content-length'] == '0' + assert r.headers['connection'] == 'close' assert r.content == b'' @@ -1480,9 +1480,9 @@ def socket_handler(listener): assert r.status_code == 201 assert len(r.headers) == 3 - assert r.headers[b'server'] == b'socket-level-server' - assert r.headers[b'content-length'] == b'0' - assert r.headers[b'connection'] == b'close' + assert r.headers['server'] == 'socket-level-server' + assert r.headers['content-length'] == '0' + assert r.headers['connection'] == 'close' assert r.content == b'' @@ -1530,9 +1530,9 @@ def socket_handler(listener): assert r.status_code == 201 assert len(r.headers) == 3 - assert r.headers[b'server'] == b'socket-level-server' - assert r.headers[b'content-length'] == b'0' - assert r.headers[b'connection'] == b'close' + assert r.headers['server'] == 'socket-level-server' + assert r.headers['content-length'] == '0' + assert r.headers['connection'] == 'close' assert r.content == b'' From e118dad98a41d44868e6b6132e214a0c135fc3be Mon Sep 17 00:00:00 2001 From: KOLANICH <KOLANICH@users.noreply.github.com> Date: Wed, 5 Jul 2017 14:18:52 +0400 Subject: [PATCH 39/60] Added a dictionary of decompressors (#336) * Added a dictionary of decompressors * Update response.py * Update response.py * Update response.py * Update response.py * Update response.py * Update response.py * Update response.py * Update response.py * Update response.py * Update response.py * Update response.py * Update response.py * Update response.py * Update response.py --- hyper/http20/response.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/hyper/http20/response.py b/hyper/http20/response.py index 94b20f9a..280ffbb2 100644 --- a/hyper/http20/response.py +++ b/hyper/http20/response.py @@ -29,6 +29,12 @@ def strip_headers(headers): del headers[name] +decompressors = { + b'gzip': lambda: zlib.decompressobj(16 + zlib.MAX_WBITS), + b'deflate': DeflateDecoder +} + + class HTTP20Response(object): """ An ``HTTP20Response`` wraps the HTTP/2 response from the server. It @@ -39,6 +45,7 @@ class HTTP20Response(object): """ version = HTTPVersion.http20 + _decompressobj = None def __init__(self, headers, stream): #: The reason phrase returned by the server. This is not used in @@ -71,12 +78,9 @@ def __init__(self, headers, stream): # This 16 + MAX_WBITS nonsense is to force gzip. See this # Stack Overflow answer for more: # http://stackoverflow.com/a/2695466/1401686 - if b'gzip' in self.headers.get(b'content-encoding', []): - self._decompressobj = zlib.decompressobj(16 + zlib.MAX_WBITS) - elif b'deflate' in self.headers.get(b'content-encoding', []): - self._decompressobj = DeflateDecoder() - else: - self._decompressobj = None + for c in self.headers.get(b'content-encoding', []): + self._decompressobj = decompressors.get(c)() + break @property def trailers(self): From 021af18d52a5a5532a10cef517c37089f4e57be7 Mon Sep 17 00:00:00 2001 From: Kostya Esmukov <kostya.shift@gmail.com> Date: Fri, 21 Jul 2017 20:44:38 +0300 Subject: [PATCH 40/60] Unvendor rfc3986 (#341) * Add failing test for userinfo with '!' and '=' chars, which are allowed as per RFC3986 * Unvendor rfc3986. Also fixes issue with ! and = being invalid in userinfo --- .coveragerc | 1 - .travis.yml | 2 +- hyper/common/util.py | 2 +- hyper/packages/__init__.py | 7 - hyper/packages/rfc3986/LICENSE | 13 - hyper/packages/rfc3986/__init__.py | 45 --- hyper/packages/rfc3986/api.py | 92 ------ hyper/packages/rfc3986/compat.py | 31 --- hyper/packages/rfc3986/exceptions.py | 21 -- hyper/packages/rfc3986/misc.py | 214 -------------- hyper/packages/rfc3986/normalizers.py | 115 -------- hyper/packages/rfc3986/parseresult.py | 303 -------------------- hyper/packages/rfc3986/uri.py | 385 -------------------------- setup.py | 6 +- test/test_hyper.py | 10 + tox.ini | 2 +- 16 files changed, 16 insertions(+), 1233 deletions(-) delete mode 100644 hyper/packages/__init__.py delete mode 100644 hyper/packages/rfc3986/LICENSE delete mode 100644 hyper/packages/rfc3986/__init__.py delete mode 100644 hyper/packages/rfc3986/api.py delete mode 100644 hyper/packages/rfc3986/compat.py delete mode 100644 hyper/packages/rfc3986/exceptions.py delete mode 100644 hyper/packages/rfc3986/misc.py delete mode 100644 hyper/packages/rfc3986/normalizers.py delete mode 100644 hyper/packages/rfc3986/parseresult.py delete mode 100644 hyper/packages/rfc3986/uri.py diff --git a/.coveragerc b/.coveragerc index 3d57cf76..f7570d40 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,7 +4,6 @@ omit = hyper/compat.py hyper/httplib_compat.py hyper/ssl_compat.py - hyper/packages/* [report] fail_under = 100 diff --git a/.travis.yml b/.travis.yml index 18b52dbf..4ea96d6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ matrix: install: - ".travis/install.sh" -before_script: "flake8 --max-complexity 15 --exclude 'hyper/packages/*' hyper test" +before_script: "flake8 --max-complexity 15 hyper test" script: - ".travis/run.sh" diff --git a/hyper/common/util.py b/hyper/common/util.py index a2278b54..2f286e10 100644 --- a/hyper/common/util.py +++ b/hyper/common/util.py @@ -8,7 +8,7 @@ from enum import Enum from hyper.compat import unicode, bytes, imap -from ..packages.rfc3986.uri import URIReference +from rfc3986 import URIReference from ..compat import is_py3 diff --git a/hyper/packages/__init__.py b/hyper/packages/__init__.py deleted file mode 100644 index 4cf1e653..00000000 --- a/hyper/packages/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -""" -hyper/packages -~~~~~~~~~~~~~~ - -This module contains external packages that are vendored into hyper. -""" diff --git a/hyper/packages/rfc3986/LICENSE b/hyper/packages/rfc3986/LICENSE deleted file mode 100644 index 72ce24cf..00000000 --- a/hyper/packages/rfc3986/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2014 Ian Cordasco, Rackspace - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/hyper/packages/rfc3986/__init__.py b/hyper/packages/rfc3986/__init__.py deleted file mode 100644 index a3aea4c4..00000000 --- a/hyper/packages/rfc3986/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -rfc3986 -======= - -An implementation of semantics and validations described in RFC 3986. See -http://rfc3986.rtfd.org/ for documentation. - -:copyright: (c) 2014 Rackspace -:license: Apache v2.0, see LICENSE for details -""" - -__title__ = 'rfc3986' -__author__ = 'Ian Cordasco' -__author_email__ = 'ian.cordasco@rackspace.com' -__license__ = 'Apache v2.0' -__copyright__ = 'Copyright 2014 Rackspace' -__version__ = '0.3.0' - -from .api import (URIReference, uri_reference, is_valid_uri, normalize_uri, - urlparse) -from .parseresult import ParseResult - -__all__ = ( - 'ParseResult', - 'URIReference', - 'is_valid_uri', - 'normalize_uri', - 'uri_reference', - 'urlparse', -) diff --git a/hyper/packages/rfc3986/api.py b/hyper/packages/rfc3986/api.py deleted file mode 100644 index 3e9e401a..00000000 --- a/hyper/packages/rfc3986/api.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -rfc3986.api -~~~~~~~~~~~ - -This defines the simple API to rfc3986. This module defines 3 functions and -provides access to the class ``URIReference``. -""" - -from .uri import URIReference -from .parseresult import ParseResult - - -def uri_reference(uri, encoding='utf-8'): - """Parse a URI string into a URIReference. - - This is a convenience function. You could achieve the same end by using - ``URIReference.from_string(uri)``. - - :param str uri: The URI which needs to be parsed into a reference. - :param str encoding: The encoding of the string provided - :returns: A parsed URI - :rtype: :class:`URIReference` - """ - return URIReference.from_string(uri, encoding) - - -def is_valid_uri(uri, encoding='utf-8', **kwargs): - """Determine if the URI given is valid. - - This is a convenience function. You could use either - ``uri_reference(uri).is_valid()`` or - ``URIReference.from_string(uri).is_valid()`` to achieve the same result. - - :param str uri: The URI to be validated. - :param str encoding: The encoding of the string provided - :param bool require_scheme: Set to ``True`` if you wish to require the - presence of the scheme component. - :param bool require_authority: Set to ``True`` if you wish to require the - presence of the authority component. - :param bool require_path: Set to ``True`` if you wish to require the - presence of the path component. - :param bool require_query: Set to ``True`` if you wish to require the - presence of the query component. - :param bool require_fragment: Set to ``True`` if you wish to require the - presence of the fragment component. - :returns: ``True`` if the URI is valid, ``False`` otherwise. - :rtype: bool - """ - return URIReference.from_string(uri, encoding).is_valid(**kwargs) - - -def normalize_uri(uri, encoding='utf-8'): - """Normalize the given URI. - - This is a convenience function. You could use either - ``uri_reference(uri).normalize().unsplit()`` or - ``URIReference.from_string(uri).normalize().unsplit()`` instead. - - :param str uri: The URI to be normalized. - :param str encoding: The encoding of the string provided - :returns: The normalized URI. - :rtype: str - """ - normalized_reference = URIReference.from_string(uri, encoding).normalize() - return normalized_reference.unsplit() - - -def urlparse(uri, encoding='utf-8'): - """Parse a given URI and return a ParseResult. - - This is a partial replacement of the standard library's urlparse function. - - :param str uri: The URI to be parsed. - :param str encoding: The encoding of the string provided. - :returns: A parsed URI - :rtype: :class:`~rfc3986.parseresult.ParseResult` - """ - return ParseResult.from_string(uri, encoding, strict=False) diff --git a/hyper/packages/rfc3986/compat.py b/hyper/packages/rfc3986/compat.py deleted file mode 100644 index 6fc7f6d8..00000000 --- a/hyper/packages/rfc3986/compat.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import sys - - -if sys.version_info >= (3, 0): - unicode = str # Python 3.x - - -def to_str(b, encoding): - if hasattr(b, 'decode') and not isinstance(b, unicode): - b = b.decode('utf-8') - return b - - -def to_bytes(s, encoding): - if hasattr(s, 'encode') and not isinstance(s, bytes): - s = s.encode('utf-8') - return s diff --git a/hyper/packages/rfc3986/exceptions.py b/hyper/packages/rfc3986/exceptions.py deleted file mode 100644 index f9adbde7..00000000 --- a/hyper/packages/rfc3986/exceptions.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -class RFC3986Exception(Exception): - pass - - -class InvalidAuthority(RFC3986Exception): - def __init__(self, authority): - super(InvalidAuthority, self).__init__( - "The authority ({0}) is not valid.".format(authority)) - - -class InvalidPort(RFC3986Exception): - def __init__(self, port): - super(InvalidPort, self).__init__( - 'The port ("{0}") is not valid.'.format(port)) - - -class ResolutionError(RFC3986Exception): - def __init__(self, uri): - super(ResolutionError, self).__init__( - "{0} is not an absolute URI.".format(uri.unsplit())) diff --git a/hyper/packages/rfc3986/misc.py b/hyper/packages/rfc3986/misc.py deleted file mode 100644 index c599434c..00000000 --- a/hyper/packages/rfc3986/misc.py +++ /dev/null @@ -1,214 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -rfc3986.misc -~~~~~~~~~~~~ - -This module contains important constants, patterns, and compiled regular -expressions for parsing and validating URIs and their components. -""" - -import re - -# These are enumerated for the named tuple used as a superclass of -# URIReference -URI_COMPONENTS = ['scheme', 'authority', 'path', 'query', 'fragment'] - -important_characters = { - 'generic_delimiters': ":/?#[]@", - 'sub_delimiters': "!$&'()*+,;=", - # We need to escape the '*' in this case - 're_sub_delimiters': "!$&'()\*+,;=", - 'unreserved_chars': ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' - '0123456789._~-'), - # We need to escape the '-' in this case: - 're_unreserved': 'A-Za-z0-9._~\-', - } -# For details about delimiters and reserved characters, see: -# http://tools.ietf.org/html/rfc3986#section-2.2 -GENERIC_DELIMITERS = set(important_characters['generic_delimiters']) -SUB_DELIMITERS = set(important_characters['sub_delimiters']) -RESERVED_CHARS = GENERIC_DELIMITERS.union(SUB_DELIMITERS) -# For details about unreserved characters, see: -# http://tools.ietf.org/html/rfc3986#section-2.3 -UNRESERVED_CHARS = set(important_characters['unreserved_chars']) -NON_PCT_ENCODED = RESERVED_CHARS.union(UNRESERVED_CHARS).union('%') - -# Extracted from http://tools.ietf.org/html/rfc3986#appendix-B -component_pattern_dict = { - 'scheme': '[^:/?#]+', - 'authority': '[^/?#]*', - 'path': '[^?#]*', - 'query': '[^#]*', - 'fragment': '.*', - } - -# See http://tools.ietf.org/html/rfc3986#appendix-B -# In this case, we name each of the important matches so we can use -# SRE_Match#groupdict to parse the values out if we so choose. This is also -# modified to ignore other matches that are not important to the parsing of -# the reference so we can also simply use SRE_Match#groups. -expression = ('(?:(?P<scheme>{scheme}):)?(?://(?P<authority>{authority}))?' - '(?P<path>{path})(?:\?(?P<query>{query}))?' - '(?:#(?P<fragment>{fragment}))?' - ).format(**component_pattern_dict) - -URI_MATCHER = re.compile(expression) - -# ######################### -# Authority Matcher Section -# ######################### - -# Host patterns, see: http://tools.ietf.org/html/rfc3986#section-3.2.2 -# The pattern for a regular name, e.g., www.google.com, api.github.com -reg_name = '(({0})*|[{1}]*)'.format( - '%[0-9A-Fa-f]{2}', - important_characters['re_sub_delimiters'] + - important_characters['re_unreserved'] - ) -# The pattern for an IPv4 address, e.g., 192.168.255.255, 127.0.0.1, -ipv4 = '(\d{1,3}.){3}\d{1,3}' -# Hexadecimal characters used in each piece of an IPv6 address -hexdig = '[0-9A-Fa-f]{1,4}' -# Least-significant 32 bits of an IPv6 address -ls32 = '({hex}:{hex}|{ipv4})'.format(hex=hexdig, ipv4=ipv4) -# Substitutions into the following patterns for IPv6 patterns defined -# http://tools.ietf.org/html/rfc3986#page-20 -subs = {'hex': hexdig, 'ls32': ls32} - -# Below: h16 = hexdig, see: https://tools.ietf.org/html/rfc5234 for details -# about ABNF (Augmented Backus-Naur Form) use in the comments -variations = [ - # 6( h16 ":" ) ls32 - '(%(hex)s:){6}%(ls32)s' % subs, - # "::" 5( h16 ":" ) ls32 - '::(%(hex)s:){5}%(ls32)s' % subs, - # [ h16 ] "::" 4( h16 ":" ) ls32 - '(%(hex)s)?::(%(hex)s:){4}%(ls32)s' % subs, - # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 - '((%(hex)s:)?%(hex)s)?::(%(hex)s:){3}%(ls32)s' % subs, - # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 - '((%(hex)s:){0,2}%(hex)s)?::(%(hex)s:){2}%(ls32)s' % subs, - # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 - '((%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s' % subs, - # [ *4( h16 ":" ) h16 ] "::" ls32 - '((%(hex)s:){0,4}%(hex)s)?::%(ls32)s' % subs, - # [ *5( h16 ":" ) h16 ] "::" h16 - '((%(hex)s:){0,5}%(hex)s)?::%(hex)s' % subs, - # [ *6( h16 ":" ) h16 ] "::" - '((%(hex)s:){0,6}%(hex)s)?::' % subs, - ] - -ipv6 = '(({0})|({1})|({2})|({3})|({4})|({5})|({6})|({7}))'.format(*variations) - -ipv_future = 'v[0-9A-Fa-f]+.[%s]+' % ( - important_characters['re_unreserved'] + - important_characters['re_sub_delimiters'] + - ':') - -ip_literal = '\[({0}|{1})\]'.format(ipv6, ipv_future) - -# Pattern for matching the host piece of the authority -HOST_PATTERN = '({0}|{1}|{2})'.format(reg_name, ipv4, ip_literal) - -SUBAUTHORITY_MATCHER = re.compile(( - '^(?:(?P<userinfo>[A-Za-z0-9_.~\-%:]+)@)?' # userinfo - '(?P<host>{0}?)' # host - ':?(?P<port>\d+)?$' # port - ).format(HOST_PATTERN)) - -IPv4_MATCHER = re.compile('^' + ipv4 + '$') - - -# #################### -# Path Matcher Section -# #################### - -# See http://tools.ietf.org/html/rfc3986#section-3.3 for more information -# about the path patterns defined below. - -# Percent encoded character values -pct_encoded = '%[A-Fa-f0-9]{2}' -pchar = ('([' + important_characters['re_unreserved'] - + important_characters['re_sub_delimiters'] - + ':@]|%s)' % pct_encoded) -segments = { - 'segment': pchar + '*', - # Non-zero length segment - 'segment-nz': pchar + '+', - # Non-zero length segment without ":" - 'segment-nz-nc': pchar.replace(':', '') + '+' - } - -# Path types taken from Section 3.3 (linked above) -path_empty = '^$' -path_rootless = '%(segment-nz)s(/%(segment)s)*' % segments -path_noscheme = '%(segment-nz-nc)s(/%(segment)s)*' % segments -path_absolute = '/(%s)?' % path_rootless -path_abempty = '(/%(segment)s)*' % segments - -# Matcher used to validate path components -PATH_MATCHER = re.compile('^(%s|%s|%s|%s|%s)$' % ( - path_abempty, path_absolute, path_noscheme, path_rootless, path_empty - )) - - -# ################################## -# Query and Fragment Matcher Section -# ################################## - -QUERY_MATCHER = re.compile( - '^([/?:@' + important_characters['re_unreserved'] - + important_characters['re_sub_delimiters'] - + ']|%s)*$' % pct_encoded) - -FRAGMENT_MATCHER = QUERY_MATCHER - -# Scheme validation, see: http://tools.ietf.org/html/rfc3986#section-3.1 -SCHEME_MATCHER = re.compile('^[A-Za-z][A-Za-z0-9+.\-]*$') - -# Relative reference matcher - -# See http://tools.ietf.org/html/rfc3986#section-4.2 for details -relative_part = '(//%s%s|%s|%s|%s)' % ( - component_pattern_dict['authority'], path_abempty, path_absolute, - path_noscheme, path_empty - ) - -RELATIVE_REF_MATCHER = re.compile('^%s(\?%s)?(#%s)?$' % ( - relative_part, QUERY_MATCHER.pattern, FRAGMENT_MATCHER.pattern - )) - -# See http://tools.ietf.org/html/rfc3986#section-3 for definition -hier_part = '(//%s%s|%s|%s|%s)' % ( - component_pattern_dict['authority'], path_abempty, path_absolute, - path_rootless, path_empty - ) - -# See http://tools.ietf.org/html/rfc3986#section-4.3 -ABSOLUTE_URI_MATCHER = re.compile('^%s:%s(\?%s)?$' % ( - component_pattern_dict['scheme'], hier_part, QUERY_MATCHER.pattern[1:-1] - )) - - -# Path merger as defined in http://tools.ietf.org/html/rfc3986#section-5.2.3 -def merge_paths(base_uri, relative_path): - """Merge a base URI's path with a relative URI's path.""" - if base_uri.path is None and base_uri.authority is not None: - return '/' + relative_path - else: - path = base_uri.path or '' - index = path.rfind('/') - return path[:index] + '/' + relative_path diff --git a/hyper/packages/rfc3986/normalizers.py b/hyper/packages/rfc3986/normalizers.py deleted file mode 100644 index bb0630cb..00000000 --- a/hyper/packages/rfc3986/normalizers.py +++ /dev/null @@ -1,115 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import re - -from .compat import to_bytes -from .misc import NON_PCT_ENCODED - - -def normalize_scheme(scheme): - return scheme.lower() - - -def normalize_authority(authority): - userinfo, host, port = authority - result = '' - if userinfo: - result += normalize_percent_characters(userinfo) + '@' - if host: - result += host.lower() - if port: - result += ':' + port - return result - - -def normalize_path(path): - if not path: - return path - - path = normalize_percent_characters(path) - return remove_dot_segments(path) - - -def normalize_query(query): - return normalize_percent_characters(query) - - -def normalize_fragment(fragment): - return normalize_percent_characters(fragment) - - -PERCENT_MATCHER = re.compile('%[A-Fa-f0-9]{2}') - - -def normalize_percent_characters(s): - """All percent characters should be upper-cased. - - For example, ``"%3afoo%DF%ab"`` should be turned into ``"%3Afoo%DF%AB"``. - """ - matches = set(PERCENT_MATCHER.findall(s)) - for m in matches: - if not m.isupper(): - s = s.replace(m, m.upper()) - return s - - -def remove_dot_segments(s): - # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code - segments = s.split('/') # Turn the path into a list of segments - output = [] # Initialize the variable to use to store output - - for segment in segments: - # '.' is the current directory, so ignore it, it is superfluous - if segment == '.': - continue - # Anything other than '..', should be appended to the output - elif segment != '..': - output.append(segment) - # In this case segment == '..', if we can, we should pop the last - # element - elif output: - output.pop() - - # If the path starts with '/' and the output is empty or the first string - # is non-empty - if s.startswith('/') and (not output or output[0]): - output.insert(0, '') - - # If the path starts with '/.' or '/..' ensure we add one more empty - # string to add a trailing '/' - if s.endswith(('/.', '/..')): - output.append('') - - return '/'.join(output) - - -def encode_component(uri_component, encoding): - if uri_component is None: - return uri_component - - uri_bytes = to_bytes(uri_component, encoding) - - encoded_uri = bytearray() - - for i in range(0, len(uri_bytes)): - # Will return a single character bytestring on both Python 2 & 3 - byte = uri_bytes[i:i+1] - byte_ord = ord(byte) - if byte_ord < 128 and byte.decode() in NON_PCT_ENCODED: - encoded_uri.extend(byte) - continue - encoded_uri.extend('%{0:02x}'.format(byte_ord).encode()) - - return encoded_uri.decode(encoding) diff --git a/hyper/packages/rfc3986/parseresult.py b/hyper/packages/rfc3986/parseresult.py deleted file mode 100644 index 2def55b6..00000000 --- a/hyper/packages/rfc3986/parseresult.py +++ /dev/null @@ -1,303 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015 Ian Cordasco -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from collections import namedtuple - -from . import compat -from . import exceptions -from . import normalizers -from . import uri - -__all__ = ('ParseResult', 'ParseResultBytes') - -PARSED_COMPONENTS = ('scheme', 'userinfo', 'host', 'port', 'path', 'query', - 'fragment') - - -class ParseResultMixin(object): - def _generate_authority(self, attributes): - # I swear I did not align the comparisons below. That's just how they - # happened to align based on pep8 and attribute lengths. - userinfo, host, port = (attributes[p] - for p in ('userinfo', 'host', 'port')) - if (self.userinfo != userinfo or - self.host != host or - self.port != port): - if port: - port = '{0}'.format(port) - return normalizers.normalize_authority( - (compat.to_str(userinfo, self.encoding), - compat.to_str(host, self.encoding), - port) - ) - return self.authority - - def geturl(self): - """Standard library shim to the unsplit method.""" - return self.unsplit() - - @property - def hostname(self): - """Standard library shim for the host portion of the URI.""" - return self.host - - @property - def netloc(self): - """Standard library shim for the authority portion of the URI.""" - return self.authority - - @property - def params(self): - """Standard library shim for the query portion of the URI.""" - return self.query - - -class ParseResult(namedtuple('ParseResult', PARSED_COMPONENTS), - ParseResultMixin): - slots = () - - def __new__(cls, scheme, userinfo, host, port, path, query, fragment, - uri_ref, encoding='utf-8'): - parse_result = super(ParseResult, cls).__new__( - cls, - scheme or None, - userinfo or None, - host, - port or None, - path or None, - query or None, - fragment or None) - parse_result.encoding = encoding - parse_result.reference = uri_ref - return parse_result - - @classmethod - def from_string(cls, uri_string, encoding='utf-8', strict=True): - """Parse a URI from the given unicode URI string. - - :param str uri_string: Unicode URI to be parsed into a reference. - :param str encoding: The encoding of the string provided - :param bool strict: Parse strictly according to :rfc:`3986` if True. - If False, parse similarly to the standard library's urlparse - function. - :returns: :class:`ParseResult` or subclass thereof - """ - reference = uri.URIReference.from_string(uri_string, encoding) - try: - subauthority = reference.authority_info() - except exceptions.InvalidAuthority: - if strict: - raise - userinfo, host, port = split_authority(reference.authority) - else: - # Thanks to Richard Barrell for this idea: - # https://twitter.com/0x2ba22e11/status/617338811975139328 - userinfo, host, port = (subauthority.get(p) - for p in ('userinfo', 'host', 'port')) - - if port: - try: - port = int(port) - except ValueError: - raise exceptions.InvalidPort(port) - - return cls(scheme=reference.scheme, - userinfo=userinfo, - host=host, - port=port, - path=reference.path, - query=reference.query, - fragment=reference.fragment, - uri_ref=reference, - encoding=encoding) - - @property - def authority(self): - """Normalized authority generated from the subauthority parts.""" - return self.reference.authority - - def copy_with(self, scheme=None, userinfo=None, host=None, port=None, - path=None, query=None, fragment=None): - attributes = zip(PARSED_COMPONENTS, - (scheme, userinfo, host, port, path, query, fragment)) - attrs_dict = {} - for name, value in attributes: - if value is None: - value = getattr(self, name) - attrs_dict[name] = value - authority = self._generate_authority(attrs_dict) - ref = self.reference.copy_with(scheme=attrs_dict['scheme'], - authority=authority, - path=attrs_dict['path'], - query=attrs_dict['query'], - fragment=attrs_dict['fragment']) - return ParseResult(uri_ref=ref, encoding=self.encoding, **attrs_dict) - - def encode(self, encoding=None): - encoding = encoding or self.encoding - attrs = dict( - zip(PARSED_COMPONENTS, - (attr.encode(encoding) if hasattr(attr, 'encode') else attr - for attr in self))) - return ParseResultBytes( - uri_ref=self.reference, - encoding=encoding, - **attrs - ) - - def unsplit(self, use_idna=False): - """Create a URI string from the components. - - :returns: The parsed URI reconstituted as a string. - :rtype: str - """ - parse_result = self - if use_idna and self.host: - hostbytes = self.host.encode('idna') - host = hostbytes.decode(self.encoding) - parse_result = self.copy_with(host=host) - return parse_result.reference.unsplit() - - -class ParseResultBytes(namedtuple('ParseResultBytes', PARSED_COMPONENTS), - ParseResultMixin): - def __new__(cls, scheme, userinfo, host, port, path, query, fragment, - uri_ref, encoding='utf-8'): - parse_result = super(ParseResultBytes, cls).__new__( - cls, - scheme or None, - userinfo or None, - host, - port or None, - path or None, - query or None, - fragment or None) - parse_result.encoding = encoding - parse_result.reference = uri_ref - return parse_result - - @classmethod - def from_string(cls, uri_string, encoding='utf-8', strict=True): - """Parse a URI from the given unicode URI string. - - :param str uri_string: Unicode URI to be parsed into a reference. - :param str encoding: The encoding of the string provided - :param bool strict: Parse strictly according to :rfc:`3986` if True. - If False, parse similarly to the standard library's urlparse - function. - :returns: :class:`ParseResultBytes` or subclass thereof - """ - reference = uri.URIReference.from_string(uri_string, encoding) - try: - subauthority = reference.authority_info() - except exceptions.InvalidAuthority: - if strict: - raise - userinfo, host, port = split_authority(reference.authority) - else: - # Thanks to Richard Barrell for this idea: - # https://twitter.com/0x2ba22e11/status/617338811975139328 - userinfo, host, port = (subauthority.get(p) - for p in ('userinfo', 'host', 'port')) - - if port: - try: - port = int(port) - except ValueError: - raise exceptions.InvalidPort(port) - - to_bytes = compat.to_bytes - return cls(scheme=to_bytes(reference.scheme, encoding), - userinfo=to_bytes(userinfo, encoding), - host=to_bytes(host, encoding), - port=port, - path=to_bytes(reference.path, encoding), - query=to_bytes(reference.query, encoding), - fragment=to_bytes(reference.fragment, encoding), - uri_ref=reference, - encoding=encoding) - - @property - def authority(self): - """Normalized authority generated from the subauthority parts.""" - return self.reference.authority.encode(self.encoding) - - def copy_with(self, scheme=None, userinfo=None, host=None, port=None, - path=None, query=None, fragment=None): - attributes = zip(PARSED_COMPONENTS, - (scheme, userinfo, host, port, path, query, fragment)) - attrs_dict = {} - for name, value in attributes: - if value is None: - value = getattr(self, name) - if not isinstance(value, bytes) and hasattr(value, 'encode'): - value = value.encode(self.encoding) - attrs_dict[name] = value - authority = self._generate_authority(attrs_dict) - to_str = compat.to_str - ref = self.reference.copy_with( - scheme=to_str(attrs_dict['scheme'], self.encoding), - authority=authority, - path=to_str(attrs_dict['path'], self.encoding), - query=to_str(attrs_dict['query'], self.encoding), - fragment=to_str(attrs_dict['fragment'], self.encoding) - ) - return ParseResultBytes( - uri_ref=ref, - encoding=self.encoding, - **attrs_dict - ) - - def unsplit(self, use_idna=False): - """Create a URI bytes object from the components. - - :returns: The parsed URI reconstituted as a string. - :rtype: bytes - """ - parse_result = self - if use_idna and self.host: - # self.host is bytes, to encode to idna, we need to decode it - # first - host = self.host.decode(self.encoding) - hostbytes = host.encode('idna') - parse_result = self.copy_with(host=hostbytes) - uri = parse_result.reference.unsplit() - return uri.encode(self.encoding) - - -def split_authority(authority): - # Initialize our expected return values - userinfo = host = port = None - # Initialize an extra var we may need to use - extra_host = None - # Set-up rest in case there is no userinfo portion - rest = authority - - if '@' in authority: - userinfo, rest = authority.rsplit('@', 1) - - # Handle IPv6 host addresses - if rest.startswith('['): - host, rest = rest.split(']', 1) - host += ']' - - if ':' in rest: - extra_host, port = rest.split(':', 1) - elif not host and rest: - host = rest - - if extra_host and not host: - host = extra_host - - return userinfo, host, port diff --git a/hyper/packages/rfc3986/uri.py b/hyper/packages/rfc3986/uri.py deleted file mode 100644 index b7f5ccb7..00000000 --- a/hyper/packages/rfc3986/uri.py +++ /dev/null @@ -1,385 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Copyright (c) 2015 Ian Cordasco -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from collections import namedtuple - -from .compat import to_str -from .exceptions import InvalidAuthority, ResolutionError -from .misc import ( - ABSOLUTE_URI_MATCHER, FRAGMENT_MATCHER, IPv4_MATCHER, PATH_MATCHER, - QUERY_MATCHER, SCHEME_MATCHER, SUBAUTHORITY_MATCHER, URI_MATCHER, - URI_COMPONENTS, merge_paths - ) -from .normalizers import ( - encode_component, normalize_scheme, normalize_authority, normalize_path, - normalize_query, normalize_fragment - ) - - -class URIReference(namedtuple('URIReference', URI_COMPONENTS)): - slots = () - - def __new__(cls, scheme, authority, path, query, fragment, - encoding='utf-8'): - ref = super(URIReference, cls).__new__( - cls, - scheme or None, - authority or None, - path or None, - query or None, - fragment or None) - ref.encoding = encoding - return ref - - def __eq__(self, other): - other_ref = other - if isinstance(other, tuple): - other_ref = URIReference(*other) - elif not isinstance(other, URIReference): - try: - other_ref = URIReference.from_string(other) - except TypeError: - raise TypeError( - 'Unable to compare URIReference() to {0}()'.format( - type(other).__name__)) - - # See http://tools.ietf.org/html/rfc3986#section-6.2 - naive_equality = tuple(self) == tuple(other_ref) - return naive_equality or self.normalized_equality(other_ref) - - @classmethod - def from_string(cls, uri_string, encoding='utf-8'): - """Parse a URI reference from the given unicode URI string. - - :param str uri_string: Unicode URI to be parsed into a reference. - :param str encoding: The encoding of the string provided - :returns: :class:`URIReference` or subclass thereof - """ - uri_string = to_str(uri_string, encoding) - - split_uri = URI_MATCHER.match(uri_string).groupdict() - return cls(split_uri['scheme'], split_uri['authority'], - encode_component(split_uri['path'], encoding), - encode_component(split_uri['query'], encoding), - encode_component(split_uri['fragment'], encoding), encoding) - - def authority_info(self): - """Returns a dictionary with the ``userinfo``, ``host``, and ``port``. - - If the authority is not valid, it will raise a ``InvalidAuthority`` - Exception. - - :returns: - ``{'userinfo': 'username:password', 'host': 'www.example.com', - 'port': '80'}`` - :rtype: dict - :raises InvalidAuthority: If the authority is not ``None`` and can not - be parsed. - """ - if not self.authority: - return {'userinfo': None, 'host': None, 'port': None} - - match = SUBAUTHORITY_MATCHER.match(self.authority) - - if match is None: - # In this case, we have an authority that was parsed from the URI - # Reference, but it cannot be further parsed by our - # SUBAUTHORITY_MATCHER. In this case it must not be a valid - # authority. - raise InvalidAuthority(self.authority.encode(self.encoding)) - - # We had a match, now let's ensure that it is actually a valid host - # address if it is IPv4 - matches = match.groupdict() - host = matches.get('host') - - if (host and IPv4_MATCHER.match(host) and not - valid_ipv4_host_address(host)): - # If we have a host, it appears to be IPv4 and it does not have - # valid bytes, it is an InvalidAuthority. - raise InvalidAuthority(self.authority.encode(self.encoding)) - - return matches - - @property - def host(self): - """If present, a string representing the host.""" - try: - authority = self.authority_info() - except InvalidAuthority: - return None - return authority['host'] - - @property - def port(self): - """If present, the port (as a string) extracted from the authority.""" - try: - authority = self.authority_info() - except InvalidAuthority: - return None - return authority['port'] - - @property - def userinfo(self): - """If present, the userinfo extracted from the authority.""" - try: - authority = self.authority_info() - except InvalidAuthority: - return None - return authority['userinfo'] - - def is_absolute(self): - """Determine if this URI Reference is an absolute URI. - - See http://tools.ietf.org/html/rfc3986#section-4.3 for explanation. - - :returns: ``True`` if it is an absolute URI, ``False`` otherwise. - :rtype: bool - """ - return bool(ABSOLUTE_URI_MATCHER.match(self.unsplit())) - - def is_valid(self, **kwargs): - """Determines if the URI is valid. - - :param bool require_scheme: Set to ``True`` if you wish to require the - presence of the scheme component. - :param bool require_authority: Set to ``True`` if you wish to require - the presence of the authority component. - :param bool require_path: Set to ``True`` if you wish to require the - presence of the path component. - :param bool require_query: Set to ``True`` if you wish to require the - presence of the query component. - :param bool require_fragment: Set to ``True`` if you wish to require - the presence of the fragment component. - :returns: ``True`` if the URI is valid. ``False`` otherwise. - :rtype: bool - """ - validators = [ - (self.scheme_is_valid, kwargs.get('require_scheme', False)), - (self.authority_is_valid, kwargs.get('require_authority', False)), - (self.path_is_valid, kwargs.get('require_path', False)), - (self.query_is_valid, kwargs.get('require_query', False)), - (self.fragment_is_valid, kwargs.get('require_fragment', False)), - ] - return all(v(r) for v, r in validators) - - def _is_valid(self, value, matcher, require): - if require: - return (value is not None - and matcher.match(value)) - - # require is False and value is not None - return value is None or matcher.match(value) - - def authority_is_valid(self, require=False): - """Determines if the authority component is valid. - - :param str require: Set to ``True`` to require the presence of this - component. - :returns: ``True`` if the authority is valid. ``False`` otherwise. - :rtype: bool - """ - try: - self.authority_info() - except InvalidAuthority: - return False - - is_valid = self._is_valid(self.authority, - SUBAUTHORITY_MATCHER, - require) - - # Ensure that IPv4 addresses have valid bytes - if is_valid and self.host and IPv4_MATCHER.match(self.host): - return valid_ipv4_host_address(self.host) - - # Perhaps the host didn't exist or if it did, it wasn't an IPv4-like - # address. In either case, we want to rely on the `_is_valid` check, - # so let's return that. - return is_valid - - def scheme_is_valid(self, require=False): - """Determines if the scheme component is valid. - - :param str require: Set to ``True`` to require the presence of this - component. - :returns: ``True`` if the scheme is valid. ``False`` otherwise. - :rtype: bool - """ - return self._is_valid(self.scheme, SCHEME_MATCHER, require) - - def path_is_valid(self, require=False): - """Determines if the path component is valid. - - :param str require: Set to ``True`` to require the presence of this - component. - :returns: ``True`` if the path is valid. ``False`` otherwise. - :rtype: bool - """ - return self._is_valid(self.path, PATH_MATCHER, require) - - def query_is_valid(self, require=False): - """Determines if the query component is valid. - - :param str require: Set to ``True`` to require the presence of this - component. - :returns: ``True`` if the query is valid. ``False`` otherwise. - :rtype: bool - """ - return self._is_valid(self.query, QUERY_MATCHER, require) - - def fragment_is_valid(self, require=False): - """Determines if the fragment component is valid. - - :param str require: Set to ``True`` to require the presence of this - component. - :returns: ``True`` if the fragment is valid. ``False`` otherwise. - :rtype: bool - """ - return self._is_valid(self.fragment, FRAGMENT_MATCHER, require) - - def normalize(self): - """Normalize this reference as described in Section 6.2.2 - - This is not an in-place normalization. Instead this creates a new - URIReference. - - :returns: A new reference object with normalized components. - :rtype: URIReference - """ - # See http://tools.ietf.org/html/rfc3986#section-6.2.2 for logic in - # this method. - return URIReference(normalize_scheme(self.scheme or ''), - normalize_authority( - (self.userinfo, self.host, self.port)), - normalize_path(self.path or ''), - normalize_query(self.query or ''), - normalize_fragment(self.fragment or '')) - - def normalized_equality(self, other_ref): - """Compare this URIReference to another URIReference. - - :param URIReference other_ref: (required), The reference with which - we're comparing. - :returns: ``True`` if the references are equal, ``False`` otherwise. - :rtype: bool - """ - return tuple(self.normalize()) == tuple(other_ref.normalize()) - - def resolve_with(self, base_uri, strict=False): - """Use an absolute URI Reference to resolve this relative reference. - - Assuming this is a relative reference that you would like to resolve, - use the provided base URI to resolve it. - - See http://tools.ietf.org/html/rfc3986#section-5 for more information. - - :param base_uri: Either a string or URIReference. It must be an - absolute URI or it will raise an exception. - :returns: A new URIReference which is the result of resolving this - reference using ``base_uri``. - :rtype: :class:`URIReference` - :raises ResolutionError: If the ``base_uri`` is not an absolute URI. - """ - if not isinstance(base_uri, URIReference): - base_uri = URIReference.from_string(base_uri) - - if not base_uri.is_absolute(): - raise ResolutionError(base_uri) - - # This is optional per - # http://tools.ietf.org/html/rfc3986#section-5.2.1 - base_uri = base_uri.normalize() - - # The reference we're resolving - resolving = self - - if not strict and resolving.scheme == base_uri.scheme: - resolving = resolving.copy_with(scheme=None) - - # http://tools.ietf.org/html/rfc3986#page-32 - if resolving.scheme is not None: - target = resolving.copy_with(path=normalize_path(resolving.path)) - else: - if resolving.authority is not None: - target = resolving.copy_with( - scheme=base_uri.scheme, - path=normalize_path(resolving.path) - ) - else: - if resolving.path is None: - if resolving.query is not None: - query = resolving.query - else: - query = base_uri.query - target = resolving.copy_with( - scheme=base_uri.scheme, - authority=base_uri.authority, - path=base_uri.path, - query=query - ) - else: - if resolving.path.startswith('/'): - path = normalize_path(resolving.path) - else: - path = normalize_path( - merge_paths(base_uri, resolving.path) - ) - target = resolving.copy_with( - scheme=base_uri.scheme, - authority=base_uri.authority, - path=path, - query=resolving.query - ) - return target - - def unsplit(self): - """Create a URI string from the components. - - :returns: The URI Reference reconstituted as a string. - :rtype: str - """ - # See http://tools.ietf.org/html/rfc3986#section-5.3 - result_list = [] - if self.scheme: - result_list.extend([self.scheme, ':']) - if self.authority: - result_list.extend(['//', self.authority]) - if self.path: - result_list.append(self.path) - if self.query: - result_list.extend(['?', self.query]) - if self.fragment: - result_list.extend(['#', self.fragment]) - return ''.join(result_list) - - def copy_with(self, scheme=None, authority=None, path=None, query=None, - fragment=None): - attributes = { - 'scheme': scheme, - 'authority': authority, - 'path': path, - 'query': query, - 'fragment': fragment, - } - for key, value in list(attributes.items()): - if value is None: - del attributes[key] - return self._replace(**attributes) - - -def valid_ipv4_host_address(host): - # If the host exists, and it might be IPv4, check each byte in the - # address. - return all([0 <= int(byte, base=10) <= 255 for byte in host.split('.')]) diff --git a/setup.py b/setup.py index 861c883a..a2578a6b 100644 --- a/setup.py +++ b/setup.py @@ -49,8 +49,6 @@ def run_tests(self): 'hyper.http20', 'hyper.common', 'hyper.http11', - 'hyper.packages', - 'hyper.packages.rfc3986' ] setup( @@ -78,7 +76,9 @@ def run_tests(self): 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: Implementation :: CPython', ], - install_requires=['h2>=2.4,<3.0,!=2.5.0', 'hyperframe>=3.2,<4.0'], + install_requires=[ + 'h2>=2.4,<3.0,!=2.5.0', 'hyperframe>=3.2,<4.0', 'rfc3986>=1.1.0,<2.0' + ], tests_require=['pytest', 'requests', 'mock'], cmdclass={'test': PyTest}, entry_points={ diff --git a/test/test_hyper.py b/test/test_hyper.py index 0556bb0c..76a68cfe 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -67,6 +67,16 @@ def test_connections_accept_proxy_hosts_and_ports(self): assert c.proxy_host == 'localhost' assert c.proxy_port == 8443 + def test_connections_can_parse_proxy_hosts_with_userinfo(self): + c = HTTP20Connection('www.google.com', + proxy_host='azAz09!==:fakepaswd@localhost:8443') + # Note that the userinfo part is getting stripped out, + # it's not automatically added as Basic Auth header to + # the proxy_headers! It should be done manually. + assert c.host == 'www.google.com' + assert c.proxy_host == 'localhost' + assert c.proxy_port == 8443 + def test_connections_can_parse_proxy_hosts_and_ports(self): c = HTTP20Connection('www.google.com', proxy_host='localhost', diff --git a/tox.ini b/tox.ini index a35f850f..311f9c97 100644 --- a/tox.ini +++ b/tox.ini @@ -14,4 +14,4 @@ commands= py.test {toxinidir}/test/ [testenv:lint] basepython=python3.5 deps = flake8==2.5.4 -commands = flake8 --max-complexity 15 --exclude "hyper/packages/*" hyper test +commands = flake8 --max-complexity 15 hyper test From 75f25f73c17a16cb1f2ca7d46f390b0dfe4925dc Mon Sep 17 00:00:00 2001 From: hyxbiao <hyxbiao@gmail.com> Date: Thu, 27 Jul 2017 13:40:02 +0800 Subject: [PATCH 41/60] Add connection/read timeout for requests adapter (#342) * Add connection/read timeout for requests adapter * Add connection/read timeout for requests adapter * update requests integration test * explicit timeout and more timeout tests * update timeout tests for py3 * update timeout tests for py3 * explicit timeout in send method and update timeout tests * add timeout in common/HTTPConnection and update timeout tests * move timeout in _h1/2_kwargs; add timeout for _create_tunnel funciton; add more tests --- hyper/common/connection.py | 7 +- hyper/contrib.py | 10 +- hyper/http11/connection.py | 27 +++-- hyper/http20/connection.py | 24 ++++- test/server.py | 15 ++- test/test_abstraction.py | 6 +- test/test_http11.py | 10 ++ test/test_hyper.py | 10 ++ test/test_integration.py | 180 ++++++++++++++++++++++++++++++++ test/test_integration_http11.py | 67 ++++++++++++ 10 files changed, 333 insertions(+), 23 deletions(-) diff --git a/hyper/common/connection.py b/hyper/common/connection.py index e225852e..855994f8 100644 --- a/hyper/common/connection.py +++ b/hyper/common/connection.py @@ -58,6 +58,7 @@ def __init__(self, proxy_host=None, proxy_port=None, proxy_headers=None, + timeout=None, **kwargs): self._host = host @@ -65,13 +66,15 @@ def __init__(self, self._h1_kwargs = { 'secure': secure, 'ssl_context': ssl_context, 'proxy_host': proxy_host, 'proxy_port': proxy_port, - 'proxy_headers': proxy_headers, 'enable_push': enable_push + 'proxy_headers': proxy_headers, 'enable_push': enable_push, + 'timeout': timeout } self._h2_kwargs = { 'window_manager': window_manager, 'enable_push': enable_push, 'secure': secure, 'ssl_context': ssl_context, 'proxy_host': proxy_host, 'proxy_port': proxy_port, - 'proxy_headers': proxy_headers + 'proxy_headers': proxy_headers, + 'timeout': timeout } # Add any unexpected kwargs to both dictionaries. diff --git a/hyper/contrib.py b/hyper/contrib.py index c269ab90..5a580f29 100644 --- a/hyper/contrib.py +++ b/hyper/contrib.py @@ -33,7 +33,7 @@ def __init__(self, *args, **kwargs): self.connections = {} def get_connection(self, host, port, scheme, cert=None, verify=True, - proxy=None): + proxy=None, timeout=None): """ Gets an appropriate HTTP/2 connection object based on host/port/scheme/cert tuples. @@ -77,13 +77,14 @@ def get_connection(self, host, port, scheme, cert=None, verify=True, secure=secure, ssl_context=ssl_context, proxy_host=proxy_netloc, - proxy_headers=proxy_headers) + proxy_headers=proxy_headers, + timeout=timeout) self.connections[connection_key] = conn return conn def send(self, request, stream=False, cert=None, verify=True, proxies=None, - **kwargs): + timeout=None, **kwargs): """ Sends a HTTP message to the server. """ @@ -98,7 +99,8 @@ def send(self, request, stream=False, cert=None, verify=True, proxies=None, parsed.scheme, cert=cert, verify=verify, - proxy=proxy) + proxy=proxy, + timeout=timeout) # Build the selector. selector = parsed.path diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 225ef98e..4311d307 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -39,14 +39,14 @@ def _create_tunnel(proxy_host, proxy_port, target_host, target_port, - proxy_headers=None): + proxy_headers=None, timeout=None): """ Sends CONNECT method to a proxy and returns a socket with established connection to the target. :returns: socket """ - conn = HTTP11Connection(proxy_host, proxy_port) + conn = HTTP11Connection(proxy_host, proxy_port, timeout=timeout) conn.request('CONNECT', '%s:%d' % (target_host, target_port), headers=proxy_headers) @@ -101,7 +101,7 @@ class HTTP11Connection(object): def __init__(self, host, port=None, secure=None, ssl_context=None, proxy_host=None, proxy_port=None, proxy_headers=None, - **kwargs): + timeout=None, **kwargs): if port is None: self.host, self.port = to_host_port_tuple(host, default_port=80) else: @@ -150,6 +150,9 @@ def __init__(self, host, port=None, secure=None, ssl_context=None, #: the standard hyper parsing interface. self.parser = Parser() + # timeout + self._timeout = timeout + def connect(self): """ Connect to the server specified when the object was created. This is a @@ -159,6 +162,13 @@ def connect(self): """ if self._sock is None: + if isinstance(self._timeout, tuple): + connect_timeout = self._timeout[0] + read_timeout = self._timeout[1] + else: + connect_timeout = self._timeout + read_timeout = self._timeout + if self.proxy_host and self.secure: # Send http CONNECT method to a proxy and acquire the socket sock = _create_tunnel( @@ -166,16 +176,18 @@ def connect(self): self.proxy_port, self.host, self.port, - proxy_headers=self.proxy_headers + proxy_headers=self.proxy_headers, + timeout=self._timeout ) elif self.proxy_host: # Simple http proxy sock = socket.create_connection( (self.proxy_host, self.proxy_port), - 5 + timeout=connect_timeout ) else: - sock = socket.create_connection((self.host, self.port), 5) + sock = socket.create_connection((self.host, self.port), + timeout=connect_timeout) proto = None if self.secure: @@ -184,6 +196,9 @@ def connect(self): log.debug("Selected protocol: %s", proto) sock = BufferedSocket(sock, self.network_buffer_size) + # Set read timeout + sock.settimeout(read_timeout) + if proto not in ('http/1.1', None): raise TLSUpgrade(proto, sock) diff --git a/hyper/http20/connection.py b/hyper/http20/connection.py index 2451c3fe..b8be292b 100644 --- a/hyper/http20/connection.py +++ b/hyper/http20/connection.py @@ -102,7 +102,7 @@ class HTTP20Connection(object): def __init__(self, host, port=None, secure=None, window_manager=None, enable_push=False, ssl_context=None, proxy_host=None, proxy_port=None, force_proto=None, proxy_headers=None, - **kwargs): + timeout=None, **kwargs): """ Creates an HTTP/2 connection to a specific server. """ @@ -151,6 +151,9 @@ def __init__(self, host, port=None, secure=None, window_manager=None, self.__wm_class = window_manager or FlowControlManager self.__init_state() + # timeout + self._timeout = timeout + return def __init_state(self): @@ -343,6 +346,13 @@ def connect(self): if self._sock is not None: return + if isinstance(self._timeout, tuple): + connect_timeout = self._timeout[0] + read_timeout = self._timeout[1] + else: + connect_timeout = self._timeout + read_timeout = self._timeout + if self.proxy_host and self.secure: # Send http CONNECT method to a proxy and acquire the socket sock = _create_tunnel( @@ -350,15 +360,18 @@ def connect(self): self.proxy_port, self.host, self.port, - proxy_headers=self.proxy_headers + proxy_headers=self.proxy_headers, + timeout=self._timeout ) elif self.proxy_host: # Simple http proxy sock = socket.create_connection( - (self.proxy_host, self.proxy_port) + (self.proxy_host, self.proxy_port), + timeout=connect_timeout ) else: - sock = socket.create_connection((self.host, self.port)) + sock = socket.create_connection((self.host, self.port), + timeout=connect_timeout) if self.secure: sock, proto = wrap_socket(sock, self.host, self.ssl_context, @@ -374,6 +387,9 @@ def connect(self): self._sock = BufferedSocket(sock, self.network_buffer_size) + # Set read timeout + self._sock.settimeout(read_timeout) + self._send_preamble() def _connect_upgrade(self, sock): diff --git a/test/server.py b/test/server.py index 482bf734..edc28755 100644 --- a/test/server.py +++ b/test/server.py @@ -108,12 +108,13 @@ class SocketLevelTest(object): A test-class that defines a few helper methods for running socket-level tests. """ - def set_up(self, secure=True, proxy=False): + def set_up(self, secure=True, proxy=False, timeout=None): self.host = None self.port = None self.socket_security = SocketSecuritySetting(secure) self.proxy = proxy self.server_thread = None + self.timeout = timeout def _start_server(self, socket_handler): """ @@ -146,18 +147,22 @@ def secure(self, value): def get_connection(self): if self.h2: if not self.proxy: - return HTTP20Connection(self.host, self.port, self.secure) + return HTTP20Connection(self.host, self.port, self.secure, + timeout=self.timeout) else: return HTTP20Connection('http2bin.org', secure=self.secure, proxy_host=self.host, - proxy_port=self.port) + proxy_port=self.port, + timeout=self.timeout) else: if not self.proxy: - return HTTP11Connection(self.host, self.port, self.secure) + return HTTP11Connection(self.host, self.port, self.secure, + timeout=self.timeout) else: return HTTP11Connection('httpbin.org', secure=self.secure, proxy_host=self.host, - proxy_port=self.port) + proxy_port=self.port, + timeout=self.timeout) def get_encoder(self): """ diff --git a/test/test_abstraction.py b/test/test_abstraction.py index d48b3954..00ee16ec 100644 --- a/test/test_abstraction.py +++ b/test/test_abstraction.py @@ -10,7 +10,7 @@ def test_h1_kwargs(self): c = HTTPConnection( 'test', 443, secure=False, window_manager=True, enable_push=True, ssl_context=False, proxy_host=False, proxy_port=False, - proxy_headers=False, other_kwarg=True + proxy_headers=False, other_kwarg=True, timeout=5 ) assert c._h1_kwargs == { @@ -21,13 +21,14 @@ def test_h1_kwargs(self): 'proxy_headers': False, 'other_kwarg': True, 'enable_push': True, + 'timeout': 5, } def test_h2_kwargs(self): c = HTTPConnection( 'test', 443, secure=False, window_manager=True, enable_push=True, ssl_context=True, proxy_host=False, proxy_port=False, - proxy_headers=False, other_kwarg=True + proxy_headers=False, other_kwarg=True, timeout=(10, 30) ) assert c._h2_kwargs == { @@ -39,6 +40,7 @@ def test_h2_kwargs(self): 'proxy_port': False, 'proxy_headers': False, 'other_kwarg': True, + 'timeout': (10, 30), } def test_tls_upgrade(self, monkeypatch): diff --git a/test/test_http11.py b/test/test_http11.py index 40fea8a9..21dd7f70 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -110,6 +110,16 @@ def test_initialization_with_ipv6_addresses_proxy_inline_port(self): assert c.proxy_host == 'ffff:aaaa::1' assert c.proxy_port == 8443 + def test_initialization_timeout(self): + c = HTTP11Connection('httpbin.org', timeout=30) + + assert c._timeout == 30 + + def test_initialization_tuple_timeout(self): + c = HTTP11Connection('httpbin.org', timeout=(5, 60)) + + assert c._timeout == (5, 60) + def test_basic_request(self): c = HTTP11Connection('httpbin.org') c._sock = sock = DummySocket() diff --git a/test/test_hyper.py b/test/test_hyper.py index 76a68cfe..f4a5994d 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -98,6 +98,16 @@ def test_connection_version(self): c = HTTP20Connection('www.google.com') assert c.version is HTTPVersion.http20 + def test_connection_timeout(self): + c = HTTP20Connection('httpbin.org', timeout=30) + + assert c._timeout == 30 + + def test_connection_tuple_timeout(self): + c = HTTP20Connection('httpbin.org', timeout=(5, 60)) + + assert c._timeout == (5, 60) + def test_ping(self, frame_buffer): def data_callback(chunk, **kwargs): frame_buffer.add_data(chunk) diff --git a/test/test_integration.py b/test/test_integration.py index e1c87673..bde7d393 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -13,6 +13,7 @@ import hyper import hyper.http11.connection import pytest +from socket import timeout as SocketTimeout from contextlib import contextmanager from mock import patch from concurrent.futures import ThreadPoolExecutor, TimeoutError @@ -1230,6 +1231,110 @@ def do_connect(conn): self.tear_down() + def test_connection_timeout(self): + self.set_up(timeout=0.5) + + def socket_handler(listener): + time.sleep(1) + + self._start_server(socket_handler) + conn = self.get_connection() + + with pytest.raises((SocketTimeout, ssl.SSLError)): + # Py2 raises this as a BaseSSLError, + # Py3 raises it as socket timeout. + conn.connect() + + self.tear_down() + + def test_hyper_connection_timeout(self): + self.set_up(timeout=0.5) + + def socket_handler(listener): + time.sleep(1) + + self._start_server(socket_handler) + conn = hyper.HTTPConnection(self.host, self.port, self.secure, + timeout=self.timeout) + + with pytest.raises((SocketTimeout, ssl.SSLError)): + # Py2 raises this as a BaseSSLError, + # Py3 raises it as socket timeout. + conn.request('GET', '/') + + self.tear_down() + + def test_read_timeout(self): + self.set_up(timeout=(10, 0.5)) + + req_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + # We get two messages for the connection open and then a HEADERS + # frame. + receive_preamble(sock) + sock.recv(65535) + + # Wait for request + req_event.wait(5) + + # Sleep wait for read timeout + time.sleep(1) + + sock.close() + + self._start_server(socket_handler) + conn = self.get_connection() + conn.request('GET', '/') + req_event.set() + + with pytest.raises((SocketTimeout, ssl.SSLError)): + # Py2 raises this as a BaseSSLError, + # Py3 raises it as socket timeout. + conn.get_response() + + self.tear_down() + + def test_default_connection_timeout(self): + self.set_up(timeout=None) + + # Confirm that we send the connection upgrade string and the initial + # SettingsFrame. + data = [] + send_event = threading.Event() + + def socket_handler(listener): + time.sleep(1) + sock = listener.accept()[0] + + # We should get one big chunk. + first = sock.recv(65535) + data.append(first) + + # We need to send back a SettingsFrame. + f = SettingsFrame(0) + sock.send(f.serialize()) + + send_event.set() + sock.close() + + self._start_server(socket_handler) + conn = self.get_connection() + try: + conn.connect() + except (SocketTimeout, ssl.SSLError): + # Py2 raises this as a BaseSSLError, + # Py3 raises it as socket timeout. + pytest.fail() + + send_event.wait(5) + + assert data[0].startswith(b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n') + + self.tear_down() + @patch('hyper.http20.connection.H2_NPN_PROTOCOLS', PROTOCOLS) class TestRequestsAdapter(SocketLevelTest): @@ -1537,3 +1642,78 @@ def socket_handler(listener): assert r.content == b'' self.tear_down() + + def test_adapter_connection_timeout(self, monkeypatch, frame_buffer): + self.set_up() + + # We need to patch the ssl_wrap_socket method to ensure that we + # forcefully upgrade. + old_wrap_socket = hyper.http11.connection.wrap_socket + + def wrap(*args): + sock, _ = old_wrap_socket(*args) + return sock, 'h2' + + monkeypatch.setattr(hyper.http11.connection, 'wrap_socket', wrap) + + def socket_handler(listener): + time.sleep(1) + + self._start_server(socket_handler) + + s = requests.Session() + s.mount('https://%s' % self.host, HTTP20Adapter()) + + with pytest.raises((SocketTimeout, ssl.SSLError)): + # Py2 raises this as a BaseSSLError, + # Py3 raises it as socket timeout. + s.get('https://%s:%s/some/path' % (self.host, self.port), + timeout=0.5) + + self.tear_down() + + def test_adapter_read_timeout(self, monkeypatch, frame_buffer): + self.set_up() + + # We need to patch the ssl_wrap_socket method to ensure that we + # forcefully upgrade. + old_wrap_socket = hyper.http11.connection.wrap_socket + + def wrap(*args): + sock, _ = old_wrap_socket(*args) + return sock, 'h2' + + monkeypatch.setattr(hyper.http11.connection, 'wrap_socket', wrap) + + def socket_handler(listener): + sock = listener.accept()[0] + + # Do the handshake: conn header, settings, send settings, recv ack. + frame_buffer.add_data(receive_preamble(sock)) + + # Now expect some data. One headers frame. + req_wait = True + while req_wait: + frame_buffer.add_data(sock.recv(65535)) + with reusable_frame_buffer(frame_buffer) as fr: + for f in fr: + if isinstance(f, HeadersFrame): + req_wait = False + + # Sleep wait for read timeout + time.sleep(1) + + sock.close() + + self._start_server(socket_handler) + + s = requests.Session() + s.mount('https://%s' % self.host, HTTP20Adapter()) + + with pytest.raises((SocketTimeout, ssl.SSLError)): + # Py2 raises this as a BaseSSLError, + # Py3 raises it as socket timeout. + s.get('https://%s:%s/some/path' % (self.host, self.port), + timeout=(10, 0.5)) + + self.tear_down() diff --git a/test/test_integration_http11.py b/test/test_integration_http11.py index ee318797..7ec3846a 100644 --- a/test/test_integration_http11.py +++ b/test/test_integration_http11.py @@ -9,6 +9,8 @@ import hyper import threading import pytest +import time +from socket import timeout as SocketTimeout from hyper.compat import ssl from server import SocketLevelTest, SocketSecuritySetting @@ -442,3 +444,68 @@ def socket_handler(listener): with pytest.raises(HTTPUpgrade): c.get_response() + + def test_connection_timeout(self): + self.set_up(timeout=0.5) + + def socket_handler(listener): + time.sleep(1) + + self._start_server(socket_handler) + conn = self.get_connection() + + with pytest.raises((SocketTimeout, ssl.SSLError)): + # Py2 raises this as a BaseSSLError, + # Py3 raises it as socket timeout. + conn.connect() + + self.tear_down() + + def test_hyper_connection_timeout(self): + self.set_up(timeout=0.5) + + def socket_handler(listener): + time.sleep(1) + + self._start_server(socket_handler) + conn = hyper.HTTPConnection(self.host, self.port, self.secure, + timeout=self.timeout) + + with pytest.raises((SocketTimeout, ssl.SSLError)): + # Py2 raises this as a BaseSSLError, + # Py3 raises it as socket timeout. + conn.request('GET', '/') + + self.tear_down() + + def test_read_timeout(self): + self.set_up(timeout=(10, 0.5)) + + send_event = threading.Event() + + def socket_handler(listener): + sock = listener.accept()[0] + + # We should get the initial request. + data = b'' + while not data.endswith(b'\r\n\r\n'): + data += sock.recv(65535) + + send_event.wait() + + # Sleep wait for read timeout + time.sleep(1) + + sock.close() + + self._start_server(socket_handler) + conn = self.get_connection() + conn.request('GET', '/') + send_event.set() + + with pytest.raises((SocketTimeout, ssl.SSLError)): + # Py2 raises this as a BaseSSLError, + # Py3 raises it as socket timeout. + conn.get_response() + + self.tear_down() From 669253fe136f28ebe160c9db99257937a1c52a1b Mon Sep 17 00:00:00 2001 From: Kostya Esmukov <kostya.shift@gmail.com> Date: Mon, 31 Jul 2017 20:44:04 +0300 Subject: [PATCH 42/60] HTTP20Adapter.close tests (supersedes #307) (#344) * Implement HTTP20Adapter.close to close connections * Make requests adapter close() tests integrational * Ensure that connections are actually closed on closing requests adapter * Split an assertion in requests adapter close() tests --- hyper/contrib.py | 5 ++ test/test_integration.py | 100 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) diff --git a/hyper/contrib.py b/hyper/contrib.py index 5a580f29..ff4f8ff8 100644 --- a/hyper/contrib.py +++ b/hyper/contrib.py @@ -196,3 +196,8 @@ def getheaders(self, name): orig.msg = FakeOriginalResponse(resp.headers.iter_raw()) return response + + def close(self): + for connection in self.connections.values(): + connection.close() + self.connections.clear() diff --git a/test/test_integration.py b/test/test_integration.py index bde7d393..6a9ece42 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -1717,3 +1717,103 @@ def socket_handler(listener): timeout=(10, 0.5)) self.tear_down() + + def test_adapter_close(self): + self.set_up(secure=False) + + def socket_handler(listener): + sock = listener.accept()[0] + + # We should get the initial request. + data = b'' + while not data.endswith(b'\r\n\r\n'): + data += sock.recv(65535) + + # We need to send back a response. + resp = ( + b'HTTP/1.1 201 No Content\r\n' + b'Server: socket-level-server\r\n' + b'Content-Length: 0\r\n' + b'Connection: close\r\n' + b'\r\n' + ) + sock.send(resp) + sock.close() + + self._start_server(socket_handler) + + a = HTTP20Adapter() + s = requests.Session() + s.mount('http://', a) + r = s.get('http://%s:%s' % (self.host, self.port)) + connections_before_close = list(a.connections.values()) + + # ensure that we have at least 1 connection + assert connections_before_close + + s.close() + + # check that connections cache is empty + assert not a.connections + + # check that all connections are actually closed + assert all(conn._sock is None for conn in connections_before_close) + + assert r.status_code == 201 + assert len(r.headers) == 3 + assert r.headers['server'] == 'socket-level-server' + assert r.headers['content-length'] == '0' + assert r.headers['connection'] == 'close' + + assert r.content == b'' + + self.tear_down() + + def test_adapter_close_context_manager(self): + self.set_up(secure=False) + + def socket_handler(listener): + sock = listener.accept()[0] + + # We should get the initial request. + data = b'' + while not data.endswith(b'\r\n\r\n'): + data += sock.recv(65535) + + # We need to send back a response. + resp = ( + b'HTTP/1.1 201 No Content\r\n' + b'Server: socket-level-server\r\n' + b'Content-Length: 0\r\n' + b'Connection: close\r\n' + b'\r\n' + ) + sock.send(resp) + sock.close() + + self._start_server(socket_handler) + + with requests.Session() as s: + a = HTTP20Adapter() + s.mount('http://', a) + r = s.get('http://%s:%s' % (self.host, self.port)) + connections_before_close = list(a.connections.values()) + + # ensure that we have at least 1 connection + assert connections_before_close + + # check that connections cache is empty + assert not a.connections + + # check that all connections are actually closed + assert all(conn._sock is None for conn in connections_before_close) + + assert r.status_code == 201 + assert len(r.headers) == 3 + assert r.headers['server'] == 'socket-level-server' + assert r.headers['content-length'] == '0' + assert r.headers['connection'] == 'close' + + assert r.content == b'' + + self.tear_down() From 1509cbcd459729ddd90d8f402c05670ea2f34d9c Mon Sep 17 00:00:00 2001 From: Kostya Esmukov <kostya@esmukov.ru> Date: Sat, 2 Sep 2017 10:55:42 +0300 Subject: [PATCH 43/60] Add python 3.6 to the test suite --- .travis.yml | 3 ++- setup.cfg | 3 +++ test/test_SSLContext.py | 1 - tox.ini | 6 +++--- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4ea96d6f..642c7d41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - "2.7" - "3.4" - "3.5" + - "3.6" - "pypy-5.3.1" env: @@ -19,7 +20,7 @@ matrix: install: - ".travis/install.sh" -before_script: "flake8 --max-complexity 15 hyper test" +before_script: "flake8 hyper test" script: - ".travis/run.sh" diff --git a/setup.cfg b/setup.cfg index 5e409001..53d397a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,5 @@ [wheel] universal = 1 + +[flake8] +max-complexity = 15 diff --git a/test/test_SSLContext.py b/test/test_SSLContext.py index 4add16f3..66f5d358 100644 --- a/test/test_SSLContext.py +++ b/test/test_SSLContext.py @@ -40,7 +40,6 @@ def test_custom_context(self): assert not hyper.tls._context.check_hostname assert hyper.tls._context.verify_mode == ssl.CERT_NONE - assert hyper.tls._context.options & ssl.OP_NO_COMPRESSION == 0 def test_HTTPConnection_with_custom_context(self): context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) diff --git a/tox.ini b/tox.ini index 311f9c97..046619e9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27, py34, py35, pypy, lint +envlist = py{27,34,35,36}, pypy, lint [testenv] deps= -r{toxinidir}/test_requirements.txt @@ -12,6 +12,6 @@ commands= commands= py.test {toxinidir}/test/ [testenv:lint] -basepython=python3.5 +basepython=python3 deps = flake8==2.5.4 -commands = flake8 --max-complexity 15 hyper test +commands = flake8 hyper test From 64cbab61577a5cebbd4463a425b4343b8a49e04a Mon Sep 17 00:00:00 2001 From: Kostya Esmukov <kostya@esmukov.ru> Date: Sat, 2 Sep 2017 11:10:02 +0300 Subject: [PATCH 44/60] Fix failing build with pypy on Travis --- .travis/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis/run.sh b/.travis/run.sh index 43d9dd65..321835de 100755 --- a/.travis/run.sh +++ b/.travis/run.sh @@ -6,7 +6,7 @@ set -x if [[ "$TEST_RELEASE" == true ]]; then py.test test_release.py else - if [[ $TRAVIS_PYTHON_VERSION == pypy ]]; then + if [[ $TRAVIS_PYTHON_VERSION == pypy* ]]; then py.test test/ else coverage run -m py.test test/ From 0dc6f13fbb0bfea2ad1b6b7a9006c05dab50eada Mon Sep 17 00:00:00 2001 From: Kostya Esmukov <kostya@esmukov.ru> Date: Sat, 2 Sep 2017 11:18:08 +0300 Subject: [PATCH 45/60] Require pytest>=3.0 (pypy fails with 2.9) --- test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_requirements.txt b/test_requirements.txt index dcaff945..cae2fbc6 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,4 +1,4 @@ -pytest>=2.7 +pytest>=3.0 pytest-xdist pytest-cov requests From 902a2276e6bd46440acd216cfbb846b09167d59a Mon Sep 17 00:00:00 2001 From: Matjaz Pancur <matjaz.pancur@fri.uni-lj.si> Date: Thu, 2 Nov 2017 13:57:26 +0100 Subject: [PATCH 46/60] fix new failing flake checks in CI --- hyper/ssl_compat.py | 2 +- test/test_SSLContext.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hyper/ssl_compat.py b/hyper/ssl_compat.py index 71ebcd3a..380ece30 100644 --- a/hyper/ssl_compat.py +++ b/hyper/ssl_compat.py @@ -191,7 +191,7 @@ def resolve_alias(alias): O='organizationName', OU='organizationalUnitName', CN='commonName', - ).get(alias, alias) + ).get(alias, alias) # noqa: E741 def to_components(name): # TODO Verify that these are actually *supposed* to all be diff --git a/test/test_SSLContext.py b/test/test_SSLContext.py index 66f5d358..e6051af7 100644 --- a/test/test_SSLContext.py +++ b/test/test_SSLContext.py @@ -69,7 +69,7 @@ def test_missing_certs(self): succeeded = True except hyper.common.exceptions.MissingCertFile: threw_expected_exception = True - except: + except Exception: pass assert not succeeded From f1a5fa93e8bfe2af49745376a0ece32c1d45bf46 Mon Sep 17 00:00:00 2001 From: Matjaz Pancur <matjaz.pancur@fri.uni-lj.si> Date: Thu, 2 Nov 2017 14:12:31 +0100 Subject: [PATCH 47/60] move flake ignore error comment to the proper line --- hyper/ssl_compat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hyper/ssl_compat.py b/hyper/ssl_compat.py index 380ece30..97e6fb2e 100644 --- a/hyper/ssl_compat.py +++ b/hyper/ssl_compat.py @@ -188,10 +188,10 @@ def resolve_alias(alias): C='countryName', ST='stateOrProvinceName', L='localityName', - O='organizationName', + O='organizationName', # noqa: E741 OU='organizationalUnitName', CN='commonName', - ).get(alias, alias) # noqa: E741 + ).get(alias, alias) def to_components(name): # TODO Verify that these are actually *supposed* to all be From 0ecc4b1c36c0b2b146b4a8500631e9b147a09ad7 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo <mitsuo_h@outlook.com> Date: Sat, 4 Nov 2017 09:22:58 +0900 Subject: [PATCH 48/60] fix zlib_compressobj wbit --- test/test_http11.py | 6 +++--- test/test_hyper.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_http11.py b/test/test_http11.py index 21dd7f70..954bacd0 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -637,7 +637,7 @@ def test_response_transparently_decrypts_gzip(self): headers = {b'content-encoding': [b'gzip'], b'connection': [b'close']} r = HTTP11Response(200, 'OK', headers, d, None) - c = zlib_compressobj(wbits=24) + c = zlib_compressobj(wbits=25) body = c.compress(b'this is test data') body += c.flush() d._buffer = BytesIO(body) @@ -719,7 +719,7 @@ def test_response_transparently_decrypts_chunked_gzip(self): } r = HTTP11Response(200, 'OK', headers, d, None) - c = zlib_compressobj(wbits=24) + c = zlib_compressobj(wbits=25) body = c.compress(b'this is test data') body += c.flush() @@ -804,7 +804,7 @@ def test_bounded_read_expect_close_with_content_length(self): def test_compressed_bounded_read_expect_close(self): headers = {b'connection': [b'close'], b'content-encoding': [b'gzip']} - c = zlib_compressobj(wbits=24) + c = zlib_compressobj(wbits=25) body = c.compress(b'hello there sir') body += c.flush() diff --git a/test/test_hyper.py b/test/test_hyper.py index f4a5994d..7925ad65 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -1026,7 +1026,7 @@ def test_response_transparently_decrypts_gzip(self): headers = HTTPHeaderMap( [(':status', '200'), ('content-encoding', 'gzip')] ) - c = zlib_compressobj(wbits=24) + c = zlib_compressobj(wbits=25) body = c.compress(b'this is test data') body += c.flush() resp = HTTP20Response(headers, DummyStream(body)) @@ -1144,7 +1144,7 @@ def test_read_compressed_frames(self): headers = HTTPHeaderMap( [(':status', '200'), ('content-encoding', 'gzip')] ) - c = zlib_compressobj(wbits=24) + c = zlib_compressobj(wbits=25) body = c.compress(b'this is test data') body += c.flush() From 3e52e91af22c569f58164a41581ec40682825f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Primo=C5=BE=20Godec?= <p.godec9@gmail.com> Date: Thu, 26 Oct 2017 14:15:56 +0200 Subject: [PATCH 49/60] Fix: no end of the stream when (length of body) % MAX_CHUNK == MAX_CHUNK --- HISTORY.rst | 4 +++ hyper/http20/stream.py | 25 ++++++++++--------- test/test_hyper.py | 55 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index c45c08ce..dab8eed7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,6 +4,10 @@ Release History dev --- +*Bugfixes* + +- Stream end flag when length of last chunk equal to MAX_CHUNK + v0.7.0 (2016-09-27) ------------------- diff --git a/hyper/http20/stream.py b/hyper/http20/stream.py index 598a1490..3c064783 100644 --- a/hyper/http20/stream.py +++ b/hyper/http20/stream.py @@ -122,8 +122,18 @@ def file_iterator(fobj): chunks = (data[i:i+MAX_CHUNK] for i in range(0, len(data), MAX_CHUNK)) - for chunk in chunks: - self._send_chunk(chunk, final) + # since we need to know when we have a last package we need to know + # if there is another package in advance + cur_chunk = None + try: + cur_chunk = next(chunks) + while True: + next_chunk = next(chunks) + self._send_chunk(cur_chunk, False) + cur_chunk = next_chunk + except StopIteration: + if cur_chunk is not None: # cur_chunk none when no chunks to send + self._send_chunk(cur_chunk, final) def _read(self, amt=None): """ @@ -323,19 +333,12 @@ def _send_chunk(self, data, final): while len(data) > self._out_flow_control_window: self._recv_cb() - # If the length of the data is less than MAX_CHUNK, we're probably - # at the end of the file. If this is the end of the data, mark it - # as END_STREAM. - end_stream = False - if len(data) < MAX_CHUNK and final: - end_stream = True - # Send the frame and decrement the flow control window. with self._conn as conn: conn.send_data( - stream_id=self.stream_id, data=data, end_stream=end_stream + stream_id=self.stream_id, data=data, end_stream=final ) self._send_outstanding_data() - if end_stream: + if final: self.local_closed = True diff --git a/test/test_hyper.py b/test/test_hyper.py index f4a5994d..373384a5 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -211,6 +211,61 @@ def data_callback(chunk, **kwargs): assert frames[1].data == b'hello there' assert frames[1].flags == set(['END_STREAM']) + def test_request_correctly_sent_max_chunk(self, frame_buffer): + """ + Test that request correctly sent when data length multiple + max chunk. We check last chunk has a end flag and correct number + of chunks. + """ + def data_callback(chunk, **kwargs): + frame_buffer.add_data(chunk) + + # one chunk + c = HTTP20Connection('www.google.com') + c._sock = DummySocket() + c._send_cb = data_callback + c.putrequest('GET', '/') + c.endheaders(message_body=b'1'*1024, final=True) + + frames = list(frame_buffer) + assert len(frames) == 2 + assert isinstance(frames[1], DataFrame) + assert frames[1].flags == set(['END_STREAM']) + + # two chunks + c = HTTP20Connection('www.google.com') + c._sock = DummySocket() + c._send_cb = data_callback + c.putrequest('GET', '/') + c.endheaders(message_body=b'1' * 2024, final=True) + + frames = list(frame_buffer) + assert len(frames) == 3 + assert isinstance(frames[1], DataFrame) + assert frames[2].flags == set(['END_STREAM']) + + # two chunks with last chunk < 1024 + c = HTTP20Connection('www.google.com') + c._sock = DummySocket() + c._send_cb = data_callback + c.putrequest('GET', '/') + c.endheaders(message_body=b'1' * 2000, final=True) + + frames = list(frame_buffer) + assert len(frames) == 3 + assert isinstance(frames[1], DataFrame) + assert frames[2].flags == set(['END_STREAM']) + + # no chunks + c = HTTP20Connection('www.google.com') + c._sock = DummySocket() + c._send_cb = data_callback + c.putrequest('GET', '/') + c.endheaders(message_body=b'', final=True) + + frames = list(frame_buffer) + assert len(frames) == 1 + def test_that_we_correctly_send_over_the_socket(self): sock = DummySocket() c = HTTP20Connection('www.google.com') From 89adc74d606c67219879645e89767e1b8aeb3195 Mon Sep 17 00:00:00 2001 From: m_heijo <mitsuo_h@outlook.com> Date: Fri, 10 Nov 2017 17:18:02 +0900 Subject: [PATCH 50/60] Change the address of sample code. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 294a062b..20e5c6cd 100644 --- a/README.rst +++ b/README.rst @@ -15,8 +15,8 @@ improved speed, lower bandwidth usage, better connection management, and more. from hyper import HTTPConnection - conn = HTTPConnection('http2bin.org:443') - conn.request('GET', '/get') + conn = HTTPConnection('nghttp2.org:443') + conn.request('GET', '/httpbin/get') resp = conn.get_response() print(resp.read()) From c4d8494d0f309302a650530117d2206902c8c308 Mon Sep 17 00:00:00 2001 From: m_heijo <mitsuo_h@outlook.com> Date: Fri, 10 Nov 2017 18:14:31 +0900 Subject: [PATCH 51/60] fix test_release (http2bin to nghttp2.org/httpbin) --- test_release.py | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/test_release.py b/test_release.py index 903994a9..07c8c9df 100644 --- a/test_release.py +++ b/test_release.py @@ -10,17 +10,19 @@ capable of achieving basic tasks. """ -from concurrent.futures import as_completed, ThreadPoolExecutor import logging import random +from concurrent.futures import as_completed, ThreadPoolExecutor + import requests -import threading + from hyper import HTTP20Connection, HTTP11Connection, HTTPConnection from hyper.common.util import HTTPVersion from hyper.contrib import HTTP20Adapter logging.basicConfig(level=logging.INFO) + class TestHyperActuallyWorks(object): def test_abusing_nghttp2_org(self): """ @@ -93,32 +95,32 @@ def do_one_page(path): assert text_data max_workers = len(paths) - with ThreadPoolExecutor(max_workers=len(paths)) as ex: + with ThreadPoolExecutor(max_workers=max_workers) as ex: futures = [ex.submit(do_one_page, p) for p in paths] for f in as_completed(futures): f.result() - def test_hitting_http2bin_org(self): + def test_hitting_nghttp2_org(self): """ - This test function uses the requests adapter and requests to talk to http2bin. + This test function uses the requests adapter and requests to talk to nghttp2.org/httpbin. """ s = requests.Session() a = HTTP20Adapter() - s.mount('https://http2bin', a) - s.mount('https://www.http2bin', a) + s.mount('https://nghttp2', a) + s.mount('https://www.nghttp2', a) # Here are some nice URLs. urls = [ - 'https://www.http2bin.org/', - 'https://www.http2bin.org/ip', - 'https://www.http2bin.org/user-agent', - 'https://www.http2bin.org/headers', - 'https://www.http2bin.org/get', - 'https://http2bin.org/', - 'https://http2bin.org/ip', - 'https://http2bin.org/user-agent', - 'https://http2bin.org/headers', - 'https://http2bin.org/get', + 'https://www.nghttp2.org/httpbin/', + 'https://www.nghttp2.org/httpbin/ip', + 'https://www.nghttp2.org/httpbin/user-agent', + 'https://www.nghttp2.org/httpbin/headers', + 'https://www.nghttp2.org/httpbin/get', + 'https://nghttp2.org/httpbin/', + 'https://nghttp2.org/httpbin/ip', + 'https://nghttp2.org/httpbin/user-agent', + 'https://nghttp2.org/httpbin/headers', + 'https://nghttp2.org/httpbin/get', ] # Go get everything. @@ -132,7 +134,7 @@ def test_hitting_httpbin_org_http11(self): """ This test function uses hyper's HTTP/1.1 support to talk to httpbin """ - c = HTTP11Connection('httpbin.org') + c = HTTP11Connection('httpbin.org:443') # Here are some nice URLs. urls = [ From 557910320a6696ee6a94ea861100a961ea54eeaf Mon Sep 17 00:00:00 2001 From: m_heijo <mitsuo_h@outlook.com> Date: Sat, 11 Nov 2017 07:40:09 +0900 Subject: [PATCH 52/60] fix HEAD request body length --- hyper/http11/response.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/hyper/http11/response.py b/hyper/http11/response.py index 8f3eb985..5875d1e2 100644 --- a/hyper/http11/response.py +++ b/hyper/http11/response.py @@ -53,10 +53,13 @@ def __init__(self, code, reason, headers, sock, connection=None, self._expect_close = True # The expected length of the body. - try: - self._length = int(self.headers[b'content-length'][0]) - except KeyError: - self._length = None + if request_method.upper() != b'HEAD': + try: + self._length = int(self.headers[b'content-length'][0]) + except KeyError: + self._length = None + else: + self._length = 0 # Whether we expect a chunked response. self._chunked = ( From cff5c3ff99837d00da676d161c9ebc99e53aebd8 Mon Sep 17 00:00:00 2001 From: m_heijo <mitsuo_h@outlook.com> Date: Sat, 11 Nov 2017 09:50:16 +0900 Subject: [PATCH 53/60] Add test function for length of the HTTP/1.1 response-body. --- test_release.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/test_release.py b/test_release.py index 07c8c9df..38138657 100644 --- a/test_release.py +++ b/test_release.py @@ -168,3 +168,27 @@ def test_hitting_nghttp2_org_via_h2c_upgrade(self): assert response.status == 200 assert response.read() assert response.version == HTTPVersion.http20 + + def test_http11_response_body_length(self): + """ + This test function uses check the expected length of the HTTP/1.1-response-body. + """ + c = HTTP11Connection('httpbin.org:443') + + # Make some HTTP/1.1 requests. + methods = ['GET', 'HEAD'] + for method in methods: + c.request(method, '/') + resp = c.get_response() + + # Check the expected length of the body. + if method == 'HEAD': + assert resp._length == 0 + assert resp.read() == b'' + else: + try: + content_length = int(resp.headers[b'Content-Length'][0]) + except KeyError: + continue + assert resp._length == content_length + assert resp.read() From a3dd27b7e377e509cdffe8220ec7c7bd94deaf6a Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo <mitsuo_h@outlook.com> Date: Sat, 11 Nov 2017 10:18:20 +0900 Subject: [PATCH 54/60] fix some error --- hyper/http11/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyper/http11/response.py b/hyper/http11/response.py index 5875d1e2..31d2266d 100644 --- a/hyper/http11/response.py +++ b/hyper/http11/response.py @@ -53,7 +53,7 @@ def __init__(self, code, reason, headers, sock, connection=None, self._expect_close = True # The expected length of the body. - if request_method.upper() != b'HEAD': + if request_method != b'HEAD': try: self._length = int(self.headers[b'content-length'][0]) except KeyError: From 9fa200a11e33cec6802829239295111db086f2b0 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo <mitsuo_h@outlook.com> Date: Sat, 11 Nov 2017 15:21:42 +0900 Subject: [PATCH 55/60] fix HTTP/1.1 response body length --- hyper/http11/response.py | 2 +- test/test_http11.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/hyper/http11/response.py b/hyper/http11/response.py index 31d2266d..3aed4352 100644 --- a/hyper/http11/response.py +++ b/hyper/http11/response.py @@ -63,7 +63,7 @@ def __init__(self, code, reason, headers, sock, connection=None, # Whether we expect a chunked response. self._chunked = ( - b'chunked' in self.headers.get(b'transfer-encoding', []) + b'chunked' in self.headers.get(b'transfer-encoding', []) ) # When content-length is absent and response is not chunked, diff --git a/test/test_http11.py b/test/test_http11.py index 954bacd0..4a9280c9 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -941,6 +941,18 @@ def test_response_version(self): r = HTTP11Response(200, 'OK', headers, d) assert r.version is HTTPVersion.http11 + def test_response_body_length(self): + methods = [b'HEAD', b'GET'] + headers = {b'content-length': [b'15']} + d = DummySocket() + for method in methods: + d.queue = [] + r = HTTP11Response(200, 'OK', headers, d, request_method=method) + if method == b'HEAD': + assert r._length == 0 + else: + assert r._length == int(r.headers[b'content-length'][0]) + class DummySocket(object): def __init__(self): From d293a34cd71780d1b3b3ba8bf0bfb2b80e8d00a1 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo <mitsuo_h@outlook.com> Date: Sat, 11 Nov 2017 15:27:37 +0900 Subject: [PATCH 56/60] fix HTTP/1.1 response body length --- hyper/http11/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyper/http11/response.py b/hyper/http11/response.py index 3aed4352..31d2266d 100644 --- a/hyper/http11/response.py +++ b/hyper/http11/response.py @@ -63,7 +63,7 @@ def __init__(self, code, reason, headers, sock, connection=None, # Whether we expect a chunked response. self._chunked = ( - b'chunked' in self.headers.get(b'transfer-encoding', []) + b'chunked' in self.headers.get(b'transfer-encoding', []) ) # When content-length is absent and response is not chunked, From 4f86b47b24382ef0dd6fa05fcd9e77079ef7fac5 Mon Sep 17 00:00:00 2001 From: Viranch Mehta <email@viranch.me> Date: Wed, 29 Nov 2017 14:49:10 +0530 Subject: [PATCH 57/60] Fix crash on getting unsupported content-encoding --- hyper/http20/response.py | 5 +++-- test/test_hyper.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/hyper/http20/response.py b/hyper/http20/response.py index 280ffbb2..7999d665 100644 --- a/hyper/http20/response.py +++ b/hyper/http20/response.py @@ -79,8 +79,9 @@ def __init__(self, headers, stream): # Stack Overflow answer for more: # http://stackoverflow.com/a/2695466/1401686 for c in self.headers.get(b'content-encoding', []): - self._decompressobj = decompressors.get(c)() - break + if c in decompressors: + self._decompressobj = decompressors.get(c)() + break @property def trailers(self): diff --git a/test/test_hyper.py b/test/test_hyper.py index ae4584a8..74153ecf 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -1110,6 +1110,15 @@ def test_response_transparently_decrypts_wrong_deflate(self): assert resp.read() == b'this is test data' + def test_response_ignored_unsupported_compression(self): + headers = HTTPHeaderMap( + [(':status', '200'), ('content-encoding', 'invalid')] + ) + body = b'this is test data' + resp = HTTP20Response(headers, DummyStream(body)) + + assert resp.read() == b'this is test data' + def test_response_calls_stream_close(self): headers = HTTPHeaderMap([(':status', '200')]) stream = DummyStream('') From 10263130903a176e2bf1042448106a76aaac674f Mon Sep 17 00:00:00 2001 From: Viranch Mehta <email@viranch.me> Date: Wed, 29 Nov 2017 15:18:13 +0530 Subject: [PATCH 58/60] Add support for brotli compression --- hyper/http11/response.py | 3 +++ hyper/http20/response.py | 2 ++ setup.py | 2 +- test/test_http11.py | 11 +++++++++++ test/test_hyper.py | 10 ++++++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/hyper/http11/response.py b/hyper/http11/response.py index 31d2266d..7ff7a523 100644 --- a/hyper/http11/response.py +++ b/hyper/http11/response.py @@ -9,6 +9,7 @@ import logging import weakref import zlib +import brotli from ..common.decoder import DeflateDecoder from ..common.exceptions import ChunkedDecodeError, InvalidResponseError @@ -88,6 +89,8 @@ def __init__(self, code, reason, headers, sock, connection=None, # http://stackoverflow.com/a/2695466/1401686 if b'gzip' in self.headers.get(b'content-encoding', []): self._decompressobj = zlib.decompressobj(16 + zlib.MAX_WBITS) + elif b'br' in self.headers.get(b'content-encoding', []): + self._decompressobj = brotli.Decompressor() elif b'deflate' in self.headers.get(b'content-encoding', []): self._decompressobj = DeflateDecoder() else: diff --git a/hyper/http20/response.py b/hyper/http20/response.py index 280ffbb2..29437cb9 100644 --- a/hyper/http20/response.py +++ b/hyper/http20/response.py @@ -8,6 +8,7 @@ """ import logging import zlib +import brotli from ..common.decoder import DeflateDecoder from ..common.headers import HTTPHeaderMap @@ -31,6 +32,7 @@ def strip_headers(headers): decompressors = { b'gzip': lambda: zlib.decompressobj(16 + zlib.MAX_WBITS), + b'br': brotli.Decompressor, b'deflate': DeflateDecoder } diff --git a/setup.py b/setup.py index a2578a6b..94cd8d21 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ def run_tests(self): 'Programming Language :: Python :: Implementation :: CPython', ], install_requires=[ - 'h2>=2.4,<3.0,!=2.5.0', 'hyperframe>=3.2,<4.0', 'rfc3986>=1.1.0,<2.0' + 'h2>=2.4,<3.0,!=2.5.0', 'hyperframe>=3.2,<4.0', 'rfc3986>=1.1.0,<2.0', 'brotlipy>=0.7.0,<1.0' ], tests_require=['pytest', 'requests', 'mock'], cmdclass={'test': PyTest}, diff --git a/test/test_http11.py b/test/test_http11.py index 4a9280c9..9f3fd3d0 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -7,6 +7,7 @@ """ import os import zlib +import brotli from collections import namedtuple from io import BytesIO, StringIO @@ -644,6 +645,16 @@ def test_response_transparently_decrypts_gzip(self): assert r.read() == b'this is test data' + def test_response_transparently_decrypts_brotli(self): + d = DummySocket() + headers = {b'content-encoding': [b'br'], b'connection': [b'close']} + r = HTTP11Response(200, 'OK', headers, d, None) + + body = brotli.compress(b'this is test data') + d._buffer = BytesIO(body) + + assert r.read() == b'this is test data' + def test_response_transparently_decrypts_real_deflate(self): d = DummySocket() headers = { diff --git a/test/test_hyper.py b/test/test_hyper.py index ae4584a8..ca4696cd 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -26,6 +26,7 @@ import pytest import socket import zlib +import brotli from io import BytesIO TEST_DIR = os.path.abspath(os.path.dirname(__file__)) @@ -1088,6 +1089,15 @@ def test_response_transparently_decrypts_gzip(self): assert resp.read() == b'this is test data' + def test_response_transparently_decrypts_brotli(self): + headers = HTTPHeaderMap( + [(':status', '200'), ('content-encoding', 'br')] + ) + body = brotli.compress(b'this is test data') + resp = HTTP20Response(headers, DummyStream(body)) + + assert resp.read() == b'this is test data' + def test_response_transparently_decrypts_real_deflate(self): headers = HTTPHeaderMap( [(':status', '200'), ('content-encoding', 'deflate')] From d5a9a434f82327dedf9a93458d8bba9b06a395ed Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo <mitsuo_h@outlook.com> Date: Tue, 12 Dec 2017 17:16:12 +0900 Subject: [PATCH 59/60] Add a fix to make it possible to add window_manager to HTTP20Adapter. --- hyper/contrib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hyper/contrib.py b/hyper/contrib.py index ff4f8ff8..79aa7d12 100644 --- a/hyper/contrib.py +++ b/hyper/contrib.py @@ -28,9 +28,10 @@ class HTTP20Adapter(HTTPAdapter): HTTP/2. This implements some degree of connection pooling to maximise the HTTP/2 gain. """ - def __init__(self, *args, **kwargs): + def __init__(self, window_manager=None, *args, **kwargs): #: A mapping between HTTP netlocs and ``HTTP20Connection`` objects. self.connections = {} + self.window_manager = window_manager def get_connection(self, host, port, scheme, cert=None, verify=True, proxy=None, timeout=None): @@ -75,6 +76,7 @@ def get_connection(self, host, port, scheme, cert=None, verify=True, host, port, secure=secure, + window_manager=self.window_manager, ssl_context=ssl_context, proxy_host=proxy_netloc, proxy_headers=proxy_headers, From b77e758f472f00b098481e3aa8651b0808524d84 Mon Sep 17 00:00:00 2001 From: Thomas Kriechbaumer <thomas@kriechbaumer.name> Date: Tue, 12 Jan 2021 19:36:50 +0100 Subject: [PATCH 60/60] goodbye --- README.rst | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 20e5c6cd..99dce29d 100644 --- a/README.rst +++ b/README.rst @@ -2,10 +2,23 @@ Hyper: HTTP/2 Client for Python =============================== -.. image:: https://raw.github.com/Lukasa/hyper/development/docs/source/images/hyper.png +**This project is no longer maintained!** + +Please use an alternative, such as `HTTPX`_ or others. + +.. _HTTPX: https://www.python-httpx.org/ + +We will not publish further updates for ``hyper``. + +Potential security issues will not be addressed. -.. image:: https://travis-ci.org/Lukasa/hyper.svg?branch=master - :target: https://travis-ci.org/Lukasa/hyper +---- + +**So long, and thanks for all the fish!** + +---- + +.. image:: https://raw.github.com/Lukasa/hyper/development/docs/source/images/hyper.png HTTP is changing under our feet. HTTP/1.1, our old friend, is being supplemented by the brand new HTTP/2 standard. HTTP/2 provides many benefits: