summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoseph Tate <jtate@dragonstrider.com>2013-03-18 10:43:07 -0700
committerJoseph Tate <jtate@dragonstrider.com>2013-03-18 10:43:07 -0700
commit9b644fd780854dd4800f64af5c55f9621f93aaca (patch)
treeca8f27bcbefa1a3bbd273a486a7c9db0ab63b0b2
parentfee468673ab90d8542f6613909b4e9ee3449069c (diff)
parentfcbf6cfe34a797250331ebfe1a9cfb43504ef94f (diff)
downloadcherrypy-9b644fd780854dd4800f64af5c55f9621f93aaca.tar.gz
Merged in stpierre/cherrypy/1001-client-cert-verify (pull request #15)
Added support for client certificate verification in SSLAdapter (issue #1001)
-rw-r--r--cherrypy/test/ca.cert32
-rw-r--r--cherrypy/test/ca.key51
-rw-r--r--cherrypy/test/client.cert29
-rw-r--r--cherrypy/test/client.key51
-rw-r--r--cherrypy/test/client_ip.cert29
-rw-r--r--cherrypy/test/client_wildcard.cert29
-rw-r--r--cherrypy/test/client_wrong_ca.cert29
-rw-r--r--cherrypy/test/client_wrong_host.cert29
-rw-r--r--cherrypy/test/https_verifier.py162
-rw-r--r--cherrypy/test/server.cert29
-rw-r--r--cherrypy/test/server.key51
-rw-r--r--cherrypy/test/server_wrong_ca.cert29
-rw-r--r--cherrypy/test/server_wrong_host.cert29
-rw-r--r--cherrypy/test/static/has space.html1
-rw-r--r--cherrypy/test/test.conf11
-rw-r--r--cherrypy/test/test_ssl.py207
-rw-r--r--cherrypy/wsgiserver/ssl_builtin.py113
-rw-r--r--cherrypy/wsgiserver/ssl_pyopenssl.py35
-rw-r--r--cherrypy/wsgiserver/wsgiserver2.py39
-rw-r--r--cherrypy/wsgiserver/wsgiserver3.py31
20 files changed, 1006 insertions, 10 deletions
diff --git a/cherrypy/test/ca.cert b/cherrypy/test/ca.cert
new file mode 100644
index 00000000..3ba97d74
--- /dev/null
+++ b/cherrypy/test/ca.cert
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFlzCCA3+gAwIBAgIJAPYshx+wivLoMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNV
+BAYTAlVTMQswCQYDVQQIEwJYWDEMMAoGA1UEBxMDWFhYMREwDwYDVQQKEwhDaGVy
+cnlQeTAeFw0xMDEwMDQxOTAxNDJaFw0yMDEwMDExOTAxNDJaMDsxCzAJBgNVBAYT
+AlVTMQswCQYDVQQIEwJYWDEMMAoGA1UEBxMDWFhYMREwDwYDVQQKEwhDaGVycnlQ
+eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKzWr8dvyn1Nsq2xRFpM
++Ockg99V0/OAD6SrzrI9Uf//BcSotbooDyvUJ1Z2iW9wOfIj5MQ0juNpu4AlfyTa
+cPZPDrbf/wJkjWj+2UIrImZOrWVmtsRb+OJi2cH1yjibvMfn8BTxhW9P03jUb1O7
+V6Lnnp1XktJM9vZWLhtoKDwIYNzsG/S1WUX4OfBu/QofrPTeBBCFV7Q/6V3FCa9P
+aBijXhuzlt3D9YMxiL4Uu18Mk/KgzReOBCDrTftatbZpdHnB0s9Oq3ZwuF1SbcT0
+lAw2MLguuA1j8RmaaqmoNNogsiyQpXY06ef3Skom86mkiXmOIgyX6IQ/BXhFoH4i
+96vv3SptD43g6TFMjvWEBlJIWDBNCxGNnCbHNj70MCLJ1KeW0KUUc6WTxB2ADYif
+Ifpo1UR2g0KQjHhLhdWDU4aCqj3S/n23Ho7Q4cD36VPdoknMaDJs+7UDJ4B9b+eZ
+L0IYcLhqtSHPVdxxYejS1GLNXWizcwmJV3z1tA+1g0jTRA70Ee6SdMOxlnEf4ACT
+IdwcC8xQWh0j4rgqT4Z1EYmP+iukjh029Ge4rASaBkbyRJ1ALESdxr5nAryq8wxj
+/XVwgOMU5O4zuANggvq8GGAyrixcPJB+f8AYhdHp0yjKuUNJ2YbH1G3Dlg8hqyNA
+uNc88jG8nc1EKd8/OHQnabZVAgMBAAGjgZ0wgZowHQYDVR0OBBYEFDYQnybvD5dR
+D8rLKlXPZifiWPEYMGsGA1UdIwRkMGKAFDYQnybvD5dRD8rLKlXPZifiWPEYoT+k
+PTA7MQswCQYDVQQGEwJVUzELMAkGA1UECBMCWFgxDDAKBgNVBAcTA1hYWDERMA8G
+A1UEChMIQ2hlcnJ5UHmCCQD2LIcfsIry6DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
+DQEBBQUAA4ICAQCSlMKC7WUxee3QvXNPW08Q86IzcwAZqKNZy2+ZyqUPUYzeaFW8
+exTDELv8yMj0C5VjprmdIrhsMKXZSA8D8/JuUZgHUruLLXn/Dc4HRFa79cVxW9yx
+YM6weTgdNnBBtdi+Qz3Sc9UiIOlr5YzoZq2qTZw2u8ogLx9iPPrbg8Qiurx8WJse
+rzb1iqoqSLV/RpaktKNkgQpqao4TrDkxjgtuiKyMj7DKRgTz2sWwKdpPtXLCYIz2
+Xc/jxjj1xFHbKMCHxdjgYCrGUzHBUV8LhyNrii573L1Bxedi/7Jdr2IMkiEWuyus
+QthB+CGwCGUzm/nh/AVsnG2YNCRraiK1FWuq1FawudYM0eoozWIdk0lU9IR1jgNi
+y8LCQD/8d3E8EJ/dkP8o4mvmOeYko7QTb7gCNdT06YierNTi8DCZ31iggQfxOH0P
+eW1siKumG1wztEoPan/nbTsJchhqpfY6qN2YikH/6cQGR0b9tDJZe/BF7mdRXq8d
+vF4HIIOAn6xCK1/TdUbmCp6MaUYdWHM4MoAWbf592j9oER/EIc603DYjdDBTUVO3
+pYPbVg1SWINvKzTTEFFXtOvJNt5dFxFlARMcUR034Au62EMOTvC3/7bQcy0nEPTV
+G50UvRgwvNrr3D2el/jurDaHdJBQ90qUnAnI56uW7puyKvMPFXS1q2bsvQ==
+-----END CERTIFICATE-----
diff --git a/cherrypy/test/ca.key b/cherrypy/test/ca.key
new file mode 100644
index 00000000..11b58545
--- /dev/null
+++ b/cherrypy/test/ca.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/cherrypy/test/client.cert b/cherrypy/test/client.cert
new file mode 100644
index 00000000..1e5c3825
--- /dev/null
+++ b/cherrypy/test/client.cert
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE8DCCAtgCAQMwDQYJKoZIhvcNAQEFBQAwOzELMAkGA1UEBhMCVVMxCzAJBgNV
+BAgTAlhYMQwwCgYDVQQHEwNYWFgxETAPBgNVBAoTCENoZXJyeVB5MB4XDTEwMTAw
+NTE2NDkwNVoXDTIwMTAwMjE2NDkwNVowQTELMAkGA1UEBhMCVVMxCzAJBgNVBAgT
+AlhYMREwDwYDVQQKEwhDaGVycnlQeTESMBAGA1UEAxMJbG9jYWxob3N0MIICIjAN
+BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAymPRfUID7S9YAy36CqGg1JEXGYnf
+/u6CN/6OnTTLbWjQLzUaVy2gjqqVXXamZv9x4dVv7JLYl4RZ5fX72HGmcIP2tyDW
+cRLizmoKlo7Ih8k5Z66u4qFp7ebQrEma0WZvlqKUFrX2ataaJIm51809YJXZXK3G
+2gdwJyjPeCK9B8RkCqRTpiSxR38e3iSvpsqkwqqoxNcZAE2z2VV0c+p9HcwEeNN9
+3ZQEdYkA4ni6U6tnJ5v17CaJwvF7mVfxydsoHU7qH8lE5R3mBN6BgZMjZtwOWDFw
+3grUFBiS9F7NiMxNvnykwr4WTn7fc6hSm/Rvls12ALElIDd5qe9GpnPXJH3OMG87
+BHxa0DRuM7e6yw/dpBlMcfzyo1um7xe9DJJnc63hwQdc1agjr5zzkZWkv4I/4j88
+gmgKjcEa4PbpmyZ8E8uNsPSO9Vy7csel+A7IwHiiArVfoel6dor34km6KYIN2uq7
+P04djTOy+KR0ry+OwCo2vTkjPcd7S5Ta7rVsfGo3wxpxysTCDtBA6lMMd0otIZiE
+Hz2Z89sHS6ZebYM2yvLcxvFYp6w8id4ETcRLoITP5+TCUWedZ+tuixygZrcqfQFF
+WHlpGTlBO765xvH11rzFKDZSVrXwGpo82qHcFCw0imkXnKSoFUIX+EK97TK4RtBs
+kpNSll1e+d7XokECAwEAATANBgkqhkiG9w0BAQUFAAOCAgEAUdqrvx74A1pLYkHp
+bhxHMuMEsg1m0eWVLrM9VN2lUTcDPySYmWmeT8ywOZWUCKyDfc7hBhRwyQbfYkoQ
+FbgDebvrLQvXvTCWIv5paFq4d05oSiBvvQb0KHmw9gDsDne77dYetUUTupyI1YYV
+70bIcZYk27jOSSZa7rFyZvJxO1vc1eBb+w8Wm0i6vWERUDIBI8hcSvGPiWt+/VCB
+feGIYiJN6xyYg3/Yi6OgzZLGGiJxhJiHvlA7BkTR7TSlIrskbipBydWeRrU/d9TS
+OOwviIlw0hGdBSsOgqzfNal1x8KXIJ87rdn6LwOp1m3k1T1QcgXYp5Xv6dYeJYYJ
+/KJ8dOCnh5lRmc/y6dgV3V/U2iT7co8TQjhYANAa+0YhNMflrhbGYfv8TcarWv4v
+eE1eUMgKCTAdqQix5e5yXtimOqM+ubsV1Oo64AUdguy1EyDGpQf8py5Ps0YpfQms
+S2FMskUuEkYRvZQgvp4jtlYpBfc9yojFrHO320tepwfrD4+T6w6pbhEiR8T5oagu
+cARuXtn4fuVFUu49sx4Hs8X1gtLS4HjPAYQveB+ZQuysn8O3JWKz9eBQ/JIVjeh1
+2Ggv9gO8PMO175g4ePAZa6uSl39tCOtNBf0YdZCi4XmJn+FzDNec7eLTv7oognCG
+VCp+fSm/1V5FLqnelUk1NTMbwYg=
+-----END CERTIFICATE-----
diff --git a/cherrypy/test/client.key b/cherrypy/test/client.key
new file mode 100644
index 00000000..8f9a04fc
--- /dev/null
+++ b/cherrypy/test/client.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/cherrypy/test/client_ip.cert b/cherrypy/test/client_ip.cert
new file mode 100644
index 00000000..d99c4b70
--- /dev/null
+++ b/cherrypy/test/client_ip.cert
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE8TCCAtkCAgRjMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlVTMQswCQYD
+VQQIEwJYWDEMMAoGA1UEBxMDWFhYMREwDwYDVQQKEwhDaGVycnlQeTAeFw0xMDEw
+MDgxNjQwMTRaFw0yMDEwMDUxNjQwMTRaMEExCzAJBgNVBAYTAlVTMQswCQYDVQQI
+EwJYWDERMA8GA1UEChMIQ2hlcnJ5UHkxEjAQBgNVBAMTCTEyNy4wLjAuMTCCAiIw
+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMpj0X1CA+0vWAMt+gqhoNSRFxmJ
+3/7ugjf+jp00y21o0C81GlctoI6qlV12pmb/ceHVb+yS2JeEWeX1+9hxpnCD9rcg
+1nES4s5qCpaOyIfJOWeuruKhae3m0KxJmtFmb5ailBa19mrWmiSJudfNPWCV2Vyt
+xtoHcCcoz3givQfEZAqkU6YksUd/Ht4kr6bKpMKqqMTXGQBNs9lVdHPqfR3MBHjT
+fd2UBHWJAOJ4ulOrZyeb9ewmicLxe5lX8cnbKB1O6h/JROUd5gTegYGTI2bcDlgx
+cN4K1BQYkvRezYjMTb58pMK+Fk5+33OoUpv0b5bNdgCxJSA3eanvRqZz1yR9zjBv
+OwR8WtA0bjO3ussP3aQZTHH88qNbpu8XvQySZ3Ot4cEHXNWoI6+c85GVpL+CP+I/
+PIJoCo3BGuD26ZsmfBPLjbD0jvVcu3LHpfgOyMB4ogK1X6HpenaK9+JJuimCDdrq
+uz9OHY0zsvikdK8vjsAqNr05Iz3He0uU2u61bHxqN8MaccrEwg7QQOpTDHdKLSGY
+hB89mfPbB0umXm2DNsry3MbxWKesPIneBE3ES6CEz+fkwlFnnWfrboscoGa3Kn0B
+RVh5aRk5QTu+ucbx9da8xSg2Ula18BqaPNqh3BQsNIppF5ykqBVCF/hCve0yuEbQ
+bJKTUpZdXvne16JBAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBAJpdyMHD7nsrd7dA
+r1wDdUg8nXOq0ZJAcheMIakUNhjzymn4XF/o8KhPykuPdHFilOgqAT6CAM//hFDO
+tzROsq74wB6ICxerPsuDIppxhbSRuVYbFlr2aUXRUQZIWNAjvTnOfLhAy+i8BVlS
+8mEdQrqYLpg1RJhRWT7crSMAex4IIZzSnblFs1yo3Hx97eIgHViTBxGfsSMtvdie
+ZOWbKV9ueYkgxN8t8b9qDlhiaaASoFP8f3kktmKtk+8yxBA1jiPA9sYPL5sqvdP0
+ZF0ul6mo0I6H01ITkXPF0RlNScHVS+ryouIOaWXe80mHtuGwYiAFADTab0WsmIXm
+dRoUFX3yRAc1tJnUJwbHUkZWwI6rU3b6dHmfuaUq06i2MhpdW+awhycCRExKdxCm
+jdTzqlYVoJj0ARXIQqV5wS5EZ+T3pFoykV3mvYaqlBbogfyt+LoPoVmPpPzXG/DX
+ihxaGAmcvENatdwAfqP9ycZCcXKmfLf7amwyekS6qZ6nUgGeRBQNA34mrIxg/Yij
+yzfVhnMJ7Mi7fzLxZrF59N2uIV2y/6oH6eMakJ6kniCexgTh8EoADF4JQqHGvIfX
+c/HBoU3BAlXDYuzZVpL6uAhJwDbqyKMqUVvGTfnM65PSoAtjbyre1Tk6WtgcgD+q
+ycDjzcNUsA9fv/zBCS1QbYPIrVyh
+-----END CERTIFICATE-----
diff --git a/cherrypy/test/client_wildcard.cert b/cherrypy/test/client_wildcard.cert
new file mode 100644
index 00000000..671fd095
--- /dev/null
+++ b/cherrypy/test/client_wildcard.cert
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE8zCCAtsCAivjMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlVTMQswCQYD
+VQQIEwJYWDEMMAoGA1UEBxMDWFhYMREwDwYDVQQKEwhDaGVycnlQeTAeFw0xMDEw
+MDgxNjQwMzBaFw0yMDEwMDUxNjQwMzBaMEMxCzAJBgNVBAYTAlVTMQswCQYDVQQI
+EwJYWDERMA8GA1UEChMIQ2hlcnJ5UHkxFDASBgNVBAMUCyoubG9jYWxob3N0MIIC
+IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAymPRfUID7S9YAy36CqGg1JEX
+GYnf/u6CN/6OnTTLbWjQLzUaVy2gjqqVXXamZv9x4dVv7JLYl4RZ5fX72HGmcIP2
+tyDWcRLizmoKlo7Ih8k5Z66u4qFp7ebQrEma0WZvlqKUFrX2ataaJIm51809YJXZ
+XK3G2gdwJyjPeCK9B8RkCqRTpiSxR38e3iSvpsqkwqqoxNcZAE2z2VV0c+p9HcwE
+eNN93ZQEdYkA4ni6U6tnJ5v17CaJwvF7mVfxydsoHU7qH8lE5R3mBN6BgZMjZtwO
+WDFw3grUFBiS9F7NiMxNvnykwr4WTn7fc6hSm/Rvls12ALElIDd5qe9GpnPXJH3O
+MG87BHxa0DRuM7e6yw/dpBlMcfzyo1um7xe9DJJnc63hwQdc1agjr5zzkZWkv4I/
+4j88gmgKjcEa4PbpmyZ8E8uNsPSO9Vy7csel+A7IwHiiArVfoel6dor34km6KYIN
+2uq7P04djTOy+KR0ry+OwCo2vTkjPcd7S5Ta7rVsfGo3wxpxysTCDtBA6lMMd0ot
+IZiEHz2Z89sHS6ZebYM2yvLcxvFYp6w8id4ETcRLoITP5+TCUWedZ+tuixygZrcq
+fQFFWHlpGTlBO765xvH11rzFKDZSVrXwGpo82qHcFCw0imkXnKSoFUIX+EK97TK4
+RtBskpNSll1e+d7XokECAwEAATANBgkqhkiG9w0BAQUFAAOCAgEAPPJULe6zpVta
+ihGI3j9ALaXbfCAarHCRbzwhFu6YzcmQ76H4Ec1yq/Gt/dhE+UrUXEe1sUMUgQYm
+LyBqppkHbA0m+ehvGg5Rbe/R9ZDp2Q1JzI39YLqDgY3MHIkO6TST6Ps94X0Udn6N
+VxA2gQRu0vI9/e7zWxSg+RkR+X60AYkcJNy+h+PZewTFQoUyAiBWRjrqDzGqiNuC
+WRCX7vE5TqRq6f62zYCC/rnUDbUae80Cf4aTsNHJWdtljijCY/bzsmvOp+mrq7j6
+XEa6uvdS/obvze8/xiFizEbCz9z8UUWTcKq2CiOEQzl8LAlBIbrLJQSKoGH6L+oM
+J+Cwe4itpBe7shgRfaPfC7avwCiJkMY13yBBPfzQ9vTbl9MOg5gTlFUEdl/DHTuw
+1HLmJzAWsfxyBeggt76oeuKytFmXrqOoCQhd/EF113ykrg3I9Crf1x4kjk/AYbjN
+LqLVvsChNYtO7lNjW+dUsqptq4meCVWh/HY1Znk2yVsI2coBgiBkIIrBNfoTxYFX
+0iBxpIp8XL+Y22DiUV6owbErPCy8qUvPmgHa7OF8G1koN4vubEHtfegZEjGLcMZG
+SbIo4cZZ/EmZHSzGilyGVvpV9zYRQfs8sruHhy4BOGS2EFyrJcOVOhLHEODZFMDH
+PzR1WeCLFQAYsgHr3Y7HHk0ljgBHIUY=
+-----END CERTIFICATE-----
diff --git a/cherrypy/test/client_wrong_ca.cert b/cherrypy/test/client_wrong_ca.cert
new file mode 100644
index 00000000..450c98f6
--- /dev/null
+++ b/cherrypy/test/client_wrong_ca.cert
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE4jCCAsoCAQIwDQYJKoZIhvcNAQEFBQAwLTELMAkGA1UEBhMCVVMxCzAJBgNV
+BAgTAlhYMREwDwYDVQQKEwhDaGVycnlQeTAeFw0xMDEwMDQxOTExMzZaFw0yMDEw
+MDExOTExMzZaMEExCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJYWDERMA8GA1UEChMI
+Q2hlcnJ5UHkxEjAQBgNVBAMTCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQAD
+ggIPADCCAgoCggIBAMpj0X1CA+0vWAMt+gqhoNSRFxmJ3/7ugjf+jp00y21o0C81
+GlctoI6qlV12pmb/ceHVb+yS2JeEWeX1+9hxpnCD9rcg1nES4s5qCpaOyIfJOWeu
+ruKhae3m0KxJmtFmb5ailBa19mrWmiSJudfNPWCV2VytxtoHcCcoz3givQfEZAqk
+U6YksUd/Ht4kr6bKpMKqqMTXGQBNs9lVdHPqfR3MBHjTfd2UBHWJAOJ4ulOrZyeb
+9ewmicLxe5lX8cnbKB1O6h/JROUd5gTegYGTI2bcDlgxcN4K1BQYkvRezYjMTb58
+pMK+Fk5+33OoUpv0b5bNdgCxJSA3eanvRqZz1yR9zjBvOwR8WtA0bjO3ussP3aQZ
+THH88qNbpu8XvQySZ3Ot4cEHXNWoI6+c85GVpL+CP+I/PIJoCo3BGuD26ZsmfBPL
+jbD0jvVcu3LHpfgOyMB4ogK1X6HpenaK9+JJuimCDdrquz9OHY0zsvikdK8vjsAq
+Nr05Iz3He0uU2u61bHxqN8MaccrEwg7QQOpTDHdKLSGYhB89mfPbB0umXm2DNsry
+3MbxWKesPIneBE3ES6CEz+fkwlFnnWfrboscoGa3Kn0BRVh5aRk5QTu+ucbx9da8
+xSg2Ula18BqaPNqh3BQsNIppF5ykqBVCF/hCve0yuEbQbJKTUpZdXvne16JBAgMB
+AAEwDQYJKoZIhvcNAQEFBQADggIBABJ6kQ5SLRMVrDiA4MSGNdZ2/5DdaGQ8U5ai
+wjZ+JPu/efhiOkSSdFJCDXXJphAmGHWOTzGWc/NjgEJ2hswixsMS1nYxrP+abGjb
+I+Nw0KOLynd2++hlS7JIVUzBzZsu5aE0QHQtg9bYRt2Sw94LDpiUvpmDSid10coQ
+au444fNxeXVixZdyJxEyS5b5KXB0p3xHL7My4wZ8/bM/TPHyoe4qVJM5egSrw0Oz
+LItyNVJ8akc9gfwOgX8SzRG75T15IuCojnhKKRiaHJMahRQO3u+JB4+gA4hGk0Rx
+JQferTzQM/goYMSq3mQcKUbgsEf3wUmFWJDnkJAXgLHeZh1Oawug9VsGlCxS+IJ3
+Mj9hoPI4RYrMOGI8EUXBrXWhxhhi2A1LR+o4/+O5IrVBp298vEpO+EWHb9/lbt2F
+gpU5jZcRR+M76MdVHF5X4RD+rE97vRZofR7bfhxmvJ+LeForbJk3u0arCLD6LHfG
+TtUGsRWdABQuB/ZvRNYe6KWCxvuVTwTLayYNwbPr5qRCs4fHww6vRJGvDC5fqdIQ
+7aqRvwrKq2w7dMIZK0BiVEtOiBoHcKhvzD3yfi/OkqaoPbIodN5C65g2prHjwyD0
+uCqynSHHjP3qycXLLaChrYSftNwvBSJKKfbCyY/MGU7lOydkpkiFvsGXW8elC7On
+Dvhc259l
+-----END CERTIFICATE-----
diff --git a/cherrypy/test/client_wrong_host.cert b/cherrypy/test/client_wrong_host.cert
new file mode 100644
index 00000000..c40048ac
--- /dev/null
+++ b/cherrypy/test/client_wrong_host.cert
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE9DCCAtwCAQQwDQYJKoZIhvcNAQEFBQAwOzELMAkGA1UEBhMCVVMxCzAJBgNV
+BAgTAlhYMQwwCgYDVQQHEwNYWFgxETAPBgNVBAoTCENoZXJyeVB5MB4XDTEwMTAw
+NDE5MTAzOVoXDTIwMTAwMTE5MTAzOVowRTELMAkGA1UEBhMCVVMxCzAJBgNVBAgT
+AlhYMREwDwYDVQQKEwhDaGVycnlQeTEWMBQGA1UEAxQNbm90X2xvY2FsaG9zdDCC
+AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMpj0X1CA+0vWAMt+gqhoNSR
+FxmJ3/7ugjf+jp00y21o0C81GlctoI6qlV12pmb/ceHVb+yS2JeEWeX1+9hxpnCD
+9rcg1nES4s5qCpaOyIfJOWeuruKhae3m0KxJmtFmb5ailBa19mrWmiSJudfNPWCV
+2VytxtoHcCcoz3givQfEZAqkU6YksUd/Ht4kr6bKpMKqqMTXGQBNs9lVdHPqfR3M
+BHjTfd2UBHWJAOJ4ulOrZyeb9ewmicLxe5lX8cnbKB1O6h/JROUd5gTegYGTI2bc
+DlgxcN4K1BQYkvRezYjMTb58pMK+Fk5+33OoUpv0b5bNdgCxJSA3eanvRqZz1yR9
+zjBvOwR8WtA0bjO3ussP3aQZTHH88qNbpu8XvQySZ3Ot4cEHXNWoI6+c85GVpL+C
+P+I/PIJoCo3BGuD26ZsmfBPLjbD0jvVcu3LHpfgOyMB4ogK1X6HpenaK9+JJuimC
+Ddrquz9OHY0zsvikdK8vjsAqNr05Iz3He0uU2u61bHxqN8MaccrEwg7QQOpTDHdK
+LSGYhB89mfPbB0umXm2DNsry3MbxWKesPIneBE3ES6CEz+fkwlFnnWfrboscoGa3
+Kn0BRVh5aRk5QTu+ucbx9da8xSg2Ula18BqaPNqh3BQsNIppF5ykqBVCF/hCve0y
+uEbQbJKTUpZdXvne16JBAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBABOSv3BiRC7K
+oZXdLuEIWsAyzt5Ir4aRDqXZxIlFhcek2hs9AB+aNOcRLM6o+FR/ynXiUjftBJYL
+lHUsYzXt0xIxrxH8W2SgTgyY2ZxMXq15c1mDWZUeM+lejuBMII1qNTUblb9vy0sl
+xf3JmsrIUk7Ji+0eMY2ZI7eMCYLN3akdzixI4Qb5UgyKoRBqWrTnTomB0SnCqEiv
+md4BG5ifKJjGtr01xJXcTmwa80v8ANmLz1WS4no4lyau2Yf30KGxEJ2nvIiV4Nhs
+2iD/cnJYPcMkqFTKodt9JVLDCiec4Gh1HkN0VWy1XyEZvZasfO2/kFcYS8Pg9xTQ
+2w0/9VxckWx8JA54GYovhVYIYfEDZgIICf4EGIhljGX5K0gQ/jJc9Rx4dYjmKItx
+Js/js+CDLOOErnXJ7ZexArvwyRdbVQZC2PsdK7HcWnKYvjf8x4bGFSzmB/SLVnYL
+mx6ygRhdlftobhB2RnXDPtHzU28SiGGG90zzjvZoV2LdNgHttcwzjPbMU2gRXvcM
+wUFdk+xvEhjuwct6ZmZG5MbBbyXllGHoU8VadvQiK1ufrhcBdla6vaNhyNebuB1t
+P+N+T1Xe7jnjcVSkgQjT4W6du4gif4q3wFQcXuW4WtHua5b6JibQ5kNoOcct+sV7
+ZGrvlNC+PcYw1dFtI6I7ANWrdGmuGD9w
+-----END CERTIFICATE-----
diff --git a/cherrypy/test/https_verifier.py b/cherrypy/test/https_verifier.py
new file mode 100644
index 00000000..87ab236c
--- /dev/null
+++ b/cherrypy/test/https_verifier.py
@@ -0,0 +1,162 @@
+# Copyright 2007 Google Inc.
+#
+# 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.
+#
+
+"""Extensions to allow HTTPS requests with SSL certificate validation."""
+
+
+import httplib
+import re
+import socket
+import urllib2
+import ssl
+
+
+class InvalidCertificateException(httplib.HTTPException, urllib2.URLError):
+ """Raised when a certificate is provided with an invalid hostname."""
+
+ def __init__(self, host, cert, reason):
+ """Constructor.
+
+ Args:
+ host: The hostname the connection was made to.
+ cert: The SSL certificate (as a dictionary) the host returned.
+ """
+ httplib.HTTPException.__init__(self)
+ self.host = host
+ self.cert = cert
+ self.reason = reason
+
+ def __str__(self):
+ return ('Host %s returned an invalid certificate %s %s\n' %
+ (self.host, self.reason, self.cert))
+
+class CertValidatingHTTPSConnection(httplib.HTTPConnection):
+ """An HTTPConnection that connects over SSL and validates certificates."""
+
+ default_port = httplib.HTTPS_PORT
+
+ def __init__(self, host, port=None, key_file=None, cert_file=None,
+ ca_certs=None, strict=None, **kwargs):
+ """Constructor.
+
+ Args:
+ host: The hostname. Can be in 'host:port' form.
+ port: The port. Defaults to 443.
+ key_file: A file containing the client's private key
+ cert_file: A file containing the client's certificates
+ ca_certs: A file contianing a set of concatenated certificate
+ authority certs for validating the server against.
+ strict: When true, causes BadStatusLine to be raised if the status
+ line can't be parsed as a valid HTTP/1.0 or 1.1 status line.
+ """
+ httplib.HTTPConnection.__init__(self, host, port, strict, **kwargs)
+ self.key_file = key_file
+ self.cert_file = cert_file
+ self.ca_certs = ca_certs
+ if self.ca_certs:
+ self.cert_reqs = ssl.CERT_REQUIRED
+ else:
+ self.cert_reqs = ssl.CERT_NONE
+
+ def _GetValidHostsForCert(self, cert):
+ """Returns a list of valid host globs for an SSL certificate.
+
+ Args:
+ cert: A dictionary representing an SSL certificate.
+ Returns:
+ list: A list of valid host globs.
+ """
+ if 'subjectAltName' in cert:
+ return [x[1] for x in cert['subjectAltName']
+ if x[0].lower() == 'dns']
+ else:
+ return [x[0][1] for x in cert['subject']
+ if x[0][0].lower() == 'commonname']
+
+ def _ValidateCertificateHostname(self, cert, hostname):
+ """Validates that a given hostname is valid for an SSL certificate.
+
+ Args:
+ cert: A dictionary representing an SSL certificate.
+ hostname: The hostname to test.
+ Returns:
+ bool: Whether or not the hostname is valid for this certificate.
+ """
+ hosts = self._GetValidHostsForCert(cert)
+ for host in hosts:
+ host_re = host.replace('.', '\.').replace('*', '[^.]*')
+ if re.search('^%s$' % (host_re,), hostname, re.I):
+ return True
+ return False
+
+ def _possible_addresses(self, address):
+ name, port = address[:2]
+ canonical, alt_hosts, host_ips = socket.gethostbyaddr(name)
+ all_addrs = set([name] + [canonical] + alt_hosts + host_ips)
+ all_addrs.update(info[4][0] for addr in all_addrs.copy()
+ for info in socket.getaddrinfo(addr, port))
+ return all_addrs
+
+ def _matches(self, addr, cname):
+ if cname.startswith("*."):
+ return addr == cname[2:] or addr.endswith(cname[1:])
+ else:
+ return addr == cname
+
+ def _address_matches(self, address, cert):
+ for cname in self._GetValidHostsForCert(cert):
+ for possible in self._possible_addresses(address):
+ if self._matches(possible, cname):
+ return True
+ return False
+
+ def connect(self):
+ "Connect to a host on a given (SSL) port."
+ sock = socket.create_connection((self.host, self.port))
+ self.sock = ssl.wrap_socket(sock, keyfile = self.key_file,
+ certfile = self.cert_file,
+ cert_reqs = self.cert_reqs,
+ ca_certs = self.ca_certs)
+ if self.cert_reqs & ssl.CERT_REQUIRED:
+ addr = self.sock.getpeername()
+ cert = self.sock.getpeercert()
+ if not self._address_matches(addr, cert):
+ raise InvalidCertificateException(addr, cert,
+ 'hostname mismatch')
+
+
+class VerifiedHTTPSHandler(urllib2.HTTPSHandler):
+ """An HTTPHandler that validates SSL certificates."""
+
+ def __init__(self, **kwargs):
+ """Constructor. Any keyword args are passed to the httplib handler."""
+ urllib2.AbstractHTTPHandler.__init__(self)
+ self._connection_args = kwargs
+
+ def https_open(self, req):
+ def http_class_wrapper(host, **kwargs):
+ full_kwargs = dict(self._connection_args)
+ full_kwargs.update(kwargs)
+ return CertValidatingHTTPSConnection(host, **full_kwargs)
+
+ try:
+ return self.do_open(http_class_wrapper, req)
+ except urllib2.URLError, e:
+ if type(e.reason) == ssl.SSLError and e.reason.args[0] == 1:
+ raise InvalidCertificateException(req.host, '',
+ e.reason.args[1])
+ raise
+
+ https_request = urllib2.HTTPSHandler.do_request_
diff --git a/cherrypy/test/server.cert b/cherrypy/test/server.cert
new file mode 100644
index 00000000..da8f3cab
--- /dev/null
+++ b/cherrypy/test/server.cert
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE8DCCAtgCAQEwDQYJKoZIhvcNAQEFBQAwOzELMAkGA1UEBhMCVVMxCzAJBgNV
+BAgTAlhYMQwwCgYDVQQHEwNYWFgxETAPBgNVBAoTCENoZXJyeVB5MB4XDTEwMTAw
+NTE2NTU0NFoXDTIwMTAwMjE2NTU0NFowQTELMAkGA1UEBhMCVVMxCzAJBgNVBAgT
+AlhYMREwDwYDVQQKEwhDaGVycnlQeTESMBAGA1UEAxMJbG9jYWxob3N0MIICIjAN
+BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5LKNLY3Il4TFKigGvRqu3RXHvU/T
+9wrNyD/SCkUFFP/kTophyFoFyLu1jhmw6YQQfr7Q8rpvH2wXR/l7wvlr2HzfOXZH
+TA9vUsZLdAo07thYRPzLOAANKjVDmn6/dzyfeGxmpmxYlEioRtYuYDqpUoAM5V7s
+6B7aoW4llWi2rrRn8h6yAGlRrvoAUc/gOugOFzCVerQ6OFvY54YDUEkK/HqhpD70
+grYh9DdNKiNbHaDBDlQ1WEON/N4tTY1KFvYcHo67U5dkxweNQmUBZQYkp2fAO6FA
+FOfQahXjvO7QpoIYU00XjF61YBKNplTQhWH7x9ZAIzdSedhu2e6yZqLc/5wgw9lD
+pYeNqvTGK28UF+9JjvoYoI3T5//CDxrQjTZlhiW7NF1gnapaQR/uwB9AoFierEGI
+dEX5ob8Hv0V8hPhCiJUypQj0tvhGuCwAktf6jtlsWNMGrD4IpE1LEcPzhGbDZBs7
+i0J9chmXaklPed+enTpHCrLzhY7ImRzrnRnfceIkRQCL3TRvWJO+JeQ1Nahiw7m+
+S7jie63cLwiLMgFqcqMu7s0Yt7qrnA4Rg39B2KCdDMw1pAkoU4HMOY5x/8Lvsukz
+3y9IJHN5i+GbPQDN+RdAA4nFTNA9au+j/YJZETw61PUmbx8YdLfnOXyr+/JL/rQ+
+VIAHHMur+dO6qp8CAwEAATANBgkqhkiG9w0BAQUFAAOCAgEAfnA62cflmXJ6fLpi
+wp0egKb4CgQK8Kc8qio4U7X76Fhp3YQFQu8FZu+h2EAqwOuuYFZD4PzjdVA2DgTc
++OcIiKX0K3+KRuAzlhQ1jGxVcXjAQD6oHQdd6bFh8HbnBa3MJqtTx0U3wzEuIpXi
+3Nxjb79mFkVhjWFoj9gKa1l8KPqZGePr4iJZzmmPah/GoaPGJrD6RwTu+8BSjiQi
+FPmmZF7IauMoY2lKGloSNRd6FFiCSvtiTzkIvoL6pjOz2o5QFborEp5myF8z95n9
+UbrFB80JMhLDcxD6F4AInL8Mz6N8g32dgz8DgVXp93pRwWoeUlY+DQSZ7qBsIt2U
+U/gK6sTu7XnYX3JFTAkyBcU9xBMEgZiLLxzTGNYdzK1BDeHm9GVabqpRtc4acsEi
+diWGq6RGhe9o4VoYj0OzBHkI95ZJExK2tcO5DKpbXkpZOK1zTZaMNyW043UkO7FT
+/AXy90aO3aQXGnb9OR+14zbSXk8u0vaVwhPbzHMcQ4JVxaU2acLLSK94rdz+t/oR
+wIIjcST1eyvVb0y1C5TlZR6jIEfcZdRcdzGbGk2+Pi8Pv7HN1F5uoZy9UQmqqOD7
+b6qOGuymiC59o7jHmND+By3L51XqWzpPyyeAZv7WeAln93idRVjEhDYiWDfeFePV
+H7+ZxbdpJ4BDgCwYBaFYyFReMcw=
+-----END CERTIFICATE-----
diff --git a/cherrypy/test/server.key b/cherrypy/test/server.key
new file mode 100644
index 00000000..81453f31
--- /dev/null
+++ b/cherrypy/test/server.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/cherrypy/test/server_wrong_ca.cert b/cherrypy/test/server_wrong_ca.cert
new file mode 100644
index 00000000..aef25868
--- /dev/null
+++ b/cherrypy/test/server_wrong_ca.cert
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE4jCCAsoCAQEwDQYJKoZIhvcNAQEFBQAwLTELMAkGA1UEBhMCVVMxCzAJBgNV
+BAgTAlhYMREwDwYDVQQKEwhDaGVycnlQeTAeFw0xMDEwMDQxOTA2MjJaFw0yMDEw
+MDExOTA2MjJaMEExCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJYWDERMA8GA1UEChMI
+Q2hlcnJ5UHkxEjAQBgNVBAMTCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQAD
+ggIPADCCAgoCggIBAOSyjS2NyJeExSooBr0art0Vx71P0/cKzcg/0gpFBRT/5E6K
+YchaBci7tY4ZsOmEEH6+0PK6bx9sF0f5e8L5a9h83zl2R0wPb1LGS3QKNO7YWET8
+yzgADSo1Q5p+v3c8n3hsZqZsWJRIqEbWLmA6qVKADOVe7Oge2qFuJZVotq60Z/Ie
+sgBpUa76AFHP4DroDhcwlXq0Ojhb2OeGA1BJCvx6oaQ+9IK2IfQ3TSojWx2gwQ5U
+NVhDjfzeLU2NShb2HB6Ou1OXZMcHjUJlAWUGJKdnwDuhQBTn0GoV47zu0KaCGFNN
+F4xetWASjaZU0IVh+8fWQCM3UnnYbtnusmai3P+cIMPZQ6WHjar0xitvFBfvSY76
+GKCN0+f/wg8a0I02ZYYluzRdYJ2qWkEf7sAfQKBYnqxBiHRF+aG/B79FfIT4QoiV
+MqUI9Lb4RrgsAJLX+o7ZbFjTBqw+CKRNSxHD84Rmw2QbO4tCfXIZl2pJT3nfnp06
+Rwqy84WOyJkc650Z33HiJEUAi900b1iTviXkNTWoYsO5vku44nut3C8IizIBanKj
+Lu7NGLe6q5wOEYN/QdignQzMNaQJKFOBzDmOcf/C77LpM98vSCRzeYvhmz0AzfkX
+QAOJxUzQPWrvo/2CWRE8OtT1Jm8fGHS35zl8q/vyS/60PlSABxzLq/nTuqqfAgMB
+AAEwDQYJKoZIhvcNAQEFBQADggIBALGpyj0/UKlJaZGfsbaQTNAPU7sWPAMqVvGT
+7CuLnyCou1wFk7gHyckvAcagsbfK9nfhNuZNvmriPU5BFpFOSA2+492hu1ww+k+7
++UB4IbsIaQ2pCoiPnWkYl4Y6tZ7wvKYY022vyS/54TqzOsB80rvUYYOuaf91U63y
+BX6TTJSvajQRDC+teTHSH6A9lqY0V8HG4BdCpn1WqHGq+P7+SQ0S0hq4Xu86f8Ri
+ycOljavptcEPpGPPl7mLKLkhdDUoUHHfvsYL2oKX1xzzsyALwrYDsUVDO6Ti+PUf
+NLGWxYbGkRWGia7lEP/PkRSWlIeDGPkHEHNXCtBpvPU0LJc8ooTDdTY53ObHSr1/
+p+sFZGQnCarrW19jbft2h29IyEVxqXC3mbzRlx2kw/qbVXqR1tjMkm+brBNYYkxz
+E6wlTIJnvHM4l4XSCXXUonkiBUVqruQMjs/StrbSOATyubdDI385ZaSwNzjPoMGI
+Gfp5i0EkXLIBe5sAoHqNwG0xAcCx0/weAp6UeNZFnq1wVA9K5K+M0wCtAD3YWTzN
+mWTFyZ2WKugS3uDoazd9pUdZ6RLoWzEYVKJde4YcZSnwzGpvOeOwzo9lNVB0XtKN
+ta3QJMWtXCbC1bTWzGnROGB88RmyrFVM46p6QwrsBA68kZwkxYmQaJy5LnZQpUIo
+Wc01Cj3L
+-----END CERTIFICATE-----
diff --git a/cherrypy/test/server_wrong_host.cert b/cherrypy/test/server_wrong_host.cert
new file mode 100644
index 00000000..f2a4b1a4
--- /dev/null
+++ b/cherrypy/test/server_wrong_host.cert
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIE9DCCAtwCAQIwDQYJKoZIhvcNAQEFBQAwOzELMAkGA1UEBhMCVVMxCzAJBgNV
+BAgTAlhYMQwwCgYDVQQHEwNYWFgxETAPBgNVBAoTCENoZXJyeVB5MB4XDTEwMTAw
+NDE5MDczN1oXDTIwMTAwMTE5MDczN1owRTELMAkGA1UEBhMCVVMxCzAJBgNVBAgT
+AlhYMREwDwYDVQQKEwhDaGVycnlQeTEWMBQGA1UEAxQNbm90X2xvY2FsaG9zdDCC
+AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOSyjS2NyJeExSooBr0art0V
+x71P0/cKzcg/0gpFBRT/5E6KYchaBci7tY4ZsOmEEH6+0PK6bx9sF0f5e8L5a9h8
+3zl2R0wPb1LGS3QKNO7YWET8yzgADSo1Q5p+v3c8n3hsZqZsWJRIqEbWLmA6qVKA
+DOVe7Oge2qFuJZVotq60Z/IesgBpUa76AFHP4DroDhcwlXq0Ojhb2OeGA1BJCvx6
+oaQ+9IK2IfQ3TSojWx2gwQ5UNVhDjfzeLU2NShb2HB6Ou1OXZMcHjUJlAWUGJKdn
+wDuhQBTn0GoV47zu0KaCGFNNF4xetWASjaZU0IVh+8fWQCM3UnnYbtnusmai3P+c
+IMPZQ6WHjar0xitvFBfvSY76GKCN0+f/wg8a0I02ZYYluzRdYJ2qWkEf7sAfQKBY
+nqxBiHRF+aG/B79FfIT4QoiVMqUI9Lb4RrgsAJLX+o7ZbFjTBqw+CKRNSxHD84Rm
+w2QbO4tCfXIZl2pJT3nfnp06Rwqy84WOyJkc650Z33HiJEUAi900b1iTviXkNTWo
+YsO5vku44nut3C8IizIBanKjLu7NGLe6q5wOEYN/QdignQzMNaQJKFOBzDmOcf/C
+77LpM98vSCRzeYvhmz0AzfkXQAOJxUzQPWrvo/2CWRE8OtT1Jm8fGHS35zl8q/vy
+S/60PlSABxzLq/nTuqqfAgMBAAEwDQYJKoZIhvcNAQEFBQADggIBABhFU1U6n/KY
+5wNTobrZvsHq6C23wXaVXDU3IjHdH0Dhrjbw/9tlalW5MZtus3v4/SdM5Xv1Br+w
+y31x0uKoGeSUOGRg6Aa5dzb8DlMTW5Vo9CKjJ4EL5c+1fhwb25aUAFuFut92SpXm
+juvu1zSN1+M8nvSGtHp5qxcnJRgqLmyDrXTYqC4Hik8gl0U04BQf3KvmD9fXRq0O
+q0rawmLdLDpbcVatR6yJzwXBtm60mIavr1P6HAMtCceeCn5+9xid71BCC/45IOtX
+d+QBHOAYsmD9yDf/zEpaaRK6E5S/CmAKI8bw3Xy7B47A0BorVk9iTXGTrjNmOrkn
+OCGOtDJh+sOOOc7O/Zm/7YKJmGy+0k6Ws7XCb41ikz7J4ydk3A4VzypHL2zGed/N
+WP4y74zftG/dvHFpRYIbUhCH1I/SbORYqQMSsoMgPMtJc22pl8rCrQ6WTaQSI7gT
+2noblDfAehYQwtZF2I7cndsWWJBmkVfbuUWWuYiMZyTIZPwbkDFDePCU7v7bIKPQ
+pHNRez+4rEINE89ciCV2msANGrZ9KEivu0z7eduNr/O/yUONeMIMQu1Omk9SgTaw
+9mBedXmylRhmmJBk7UpeSTgy4dD1fTbp7LFVbIKJ0kIitWCKK+lokQXqOfGQKyRZ
+Bxqyh6SNyo9S4lxwvlYkWHjVpw9LF8TI
+-----END CERTIFICATE-----
diff --git a/cherrypy/test/static/has space.html b/cherrypy/test/static/has space.html
new file mode 100644
index 00000000..b9f5f097
--- /dev/null
+++ b/cherrypy/test/static/has space.html
@@ -0,0 +1 @@
+Hello, world
diff --git a/cherrypy/test/test.conf b/cherrypy/test/test.conf
new file mode 100644
index 00000000..44660d09
--- /dev/null
+++ b/cherrypy/test/test.conf
@@ -0,0 +1,11 @@
+[global]
+server.socket_host: '127.0.0.1'
+server.socket_port: 54583
+checker.on: False
+log.screen: False
+log.error_file: r'/home/cstpierre/devel/cherrypy/cherrypy/test/test.error.log'
+log.access_file: r'/home/cstpierre/devel/cherrypy/cherrypy/test/test.access.log'
+
+unsubsig: True
+test_case_name: "test_signal_handler_unsubscribe"
+
diff --git a/cherrypy/test/test_ssl.py b/cherrypy/test/test_ssl.py
new file mode 100644
index 00000000..8a15c1ea
--- /dev/null
+++ b/cherrypy/test/test_ssl.py
@@ -0,0 +1,207 @@
+import socket
+import urllib2
+import itertools
+from unittest import TestCase
+from os.path import abspath, dirname, join
+
+import cherrypy
+from cherrypy.wsgiserver import SSLAdapter
+from cherrypy.test.https_verifier import VerifiedHTTPSHandler
+
+
+class HostnameTests(TestCase):
+ def assert_matches(self, addr, common_name):
+ self.assertTrue(SSLAdapter._matches(addr, common_name),
+ "%s doesn't match %s" % (addr, common_name))
+
+ def assert_not_matches(self, addr, common_name):
+ self.assertFalse(SSLAdapter._matches(addr, common_name),
+ "%s matches %s" % (addr, common_name))
+
+ def test_local_valid(self):
+ matcher = SSLAdapter.address_matches
+ self.assertTrue(matcher(("localhost",8080), "localhost"))
+ self.assertTrue(matcher(("127.0.0.1",8080), "localhost"))
+ self.assertTrue(matcher(("localhost",8080), "127.0.0.1"))
+ self.assertTrue(matcher(("127.0.0.1",8080), "127.0.0.1"))
+ self.assertTrue(matcher(("localhost",8080), "*.localhost"))
+ self.assertTrue(matcher(("127.0.0.1",8080), "*.localhost"))
+
+ def test_local_invalid(self):
+ matcher = SSLAdapter.address_matches
+ self.assertFalse(matcher(("localhost",8080), "1.2.3.4"))
+ self.assertFalse(matcher(("localhost",8080), "example.com"))
+ self.assertFalse(matcher(("localhost",8080), "*.example.com"))
+
+ def test_wild_matches(self):
+ self.assertTrue(SSLAdapter._matches("localhost", "*.localhost"))
+ self.assertTrue(SSLAdapter._matches("sub.localhost", "*.localhost"))
+ self.assertTrue(SSLAdapter._matches("a.b.localhost", "*.localhost"))
+ self.assertTrue(SSLAdapter._matches("example.com", "*.example.com"))
+ self.assertTrue(SSLAdapter._matches("sub.example.com", "*.example.com"))
+ self.assertTrue(SSLAdapter._matches("a.b.example.com", "*.example.com"))
+
+ def test_wild_nonmatches(self):
+ self.assertFalse(SSLAdapter._matches("localhost", "localhost.*"))
+ self.assertFalse(SSLAdapter._matches("a.b.localhost", "a.*.localhost"))
+ self.assertFalse(SSLAdapter._matches("not_localhost", "*.localhost"))
+ self.assertFalse(SSLAdapter._matches("not_localhost", "*localhost"))
+ self.assertFalse(SSLAdapter._matches("example.com", "example.com.*"))
+ self.assertFalse(SSLAdapter._matches("example.com", "example.com*"))
+ self.assertFalse(SSLAdapter._matches("example.com", "example.*"))
+ self.assertFalse(SSLAdapter._matches("a.b.example.com", "a.*.example.com"))
+ self.assertFalse(SSLAdapter._matches("not_example.com", "*.example.com"))
+ self.assertFalse(SSLAdapter._matches("not_example.com", "*example.com"))
+
+
+THIS_DIR = abspath(dirname(__file__))
+
+CA_CERT = join(THIS_DIR, "ca.cert")
+
+SERVER_KEY = join(THIS_DIR, "server.key")
+SERVER_CERT = join(THIS_DIR, "server.cert")
+SERVER_WRONG_CA = join(THIS_DIR, "server_wrong_ca.cert")
+SERVER_WRONG_HOST = join(THIS_DIR, "server_wrong_host.cert")
+
+CLIENT_KEY = join(THIS_DIR, "client.key")
+CLIENT_CERT = join(THIS_DIR, "client.cert")
+CLIENT_IP_CERT = join(THIS_DIR, "client_ip.cert")
+CLIENT_WILD_CERT = join(THIS_DIR, "client_wildcard.cert")
+CLIENT_WRONG_CA = join(THIS_DIR, "client_wrong_ca.cert")
+CLIENT_WRONG_HOST = join(THIS_DIR, "client_wrong_host.cert")
+
+class Root:
+ @cherrypy.expose
+ def index(self):
+ return "ok"
+
+class HTTPSTests(object):
+ regular_fail = False
+ checked_fail = False
+ server_host = "localhost"
+ client_host = "localhost"
+ server_ca = CA_CERT
+ server_cert = SERVER_CERT
+ client_cert = CLIENT_CERT
+ server_check = "ignore" # ignore, optional, required
+ server_ssl = "builtin" # builtin, pyopenssl
+ server_check_host = True
+
+ def setUp(self):
+ socket.setdefaulttimeout(1)
+
+ cherrypy.config.update({
+ "checker.on": False,
+ "log.screen": False,
+ "engine.autoreload_on": False,
+
+ "server.socket_host": self.server_host,
+ "server.socket_port": 8080,
+
+ "server.ssl_private_key": SERVER_KEY,
+ "server.ssl_certificate": self.server_cert,
+ "server.ssl_client_CA": self.server_ca,
+ "server.ssl_client_check": self.server_check,
+ "server.ssl_module": self.server_ssl,
+ "server.ssl_client_check_host": self.server_check_host,
+ })
+ cherrypy.tree.mount(Root())
+ cherrypy.engine.start()
+ cherrypy.engine.wait(cherrypy.engine.states.STARTED)
+
+ self.opener = urllib2.build_opener(VerifiedHTTPSHandler(
+ ca_certs = CA_CERT,
+ key_file = CLIENT_KEY,
+ cert_file = self.client_cert
+ ))
+
+ self.url = "https://" + self.client_host + ":8080/"
+
+ def tearDown(self):
+ cherrypy.engine.exit()
+ cherrypy.engine.wait(cherrypy.engine.states.EXITING)
+ cherrypy.server.httpserver = None # force the ssl adaptor to reload
+
+ def test_checked(self):
+ if self.checked_fail:
+ self.assertRaises(Exception, self.opener.open, self.url)
+ else:
+ self.assertEqual("ok", self.opener.open(self.url).read())
+
+ def test_regular(self):
+ if self.regular_fail:
+ self.assertRaises(urllib2.URLError, urllib2.urlopen, self.url)
+ else:
+ self.assertEqual("ok", urllib2.urlopen(self.url).read())
+
+
+SSL_MODULES = ["builtin", "pyopenssl"]
+BAD_SERVER_CERTS = [SERVER_WRONG_CA, SERVER_WRONG_HOST]
+BAD_CLIENT_CERTS = [CLIENT_WRONG_CA, CLIENT_WRONG_HOST]
+GOOD_CLIENT_CERTS = [CLIENT_CERT, CLIENT_IP_CERT, CLIENT_WILD_CERT]
+SERVER_HOSTS = ["localhost", "127.0.0.1", "0.0.0.0"]
+if socket.getaddrinfo("localhost", 8080)[0][0] == socket.AF_INET6:
+ CLIENT_HOSTS = ["localhost"]
+else:
+ CLIENT_HOSTS = ["localhost", "127.0.0.1"]
+
+TESTS = [
+ {"regular_fail": False,
+ "checked_fail": False,
+ "settings": {"server_check": ["ignore","optional"],
+ "client_cert": GOOD_CLIENT_CERTS}},
+ {"regular_fail": False,
+ "checked_fail": False,
+ "settings": {"server_ca": [None],
+ "server_check": ["ignore","optional","required"],
+ "client_cert": GOOD_CLIENT_CERTS + BAD_CLIENT_CERTS}},
+ {"regular_fail": False,
+ "checked_fail": False,
+ "settings": {"server_check": ["ignore"],
+ "client_cert": GOOD_CLIENT_CERTS + BAD_CLIENT_CERTS}},
+ {"regular_fail": True,
+ "checked_fail": False,
+ "settings": {"server_check": ["required"],
+ "client_cert": GOOD_CLIENT_CERTS}},
+ {"regular_fail": True,
+ "checked_fail": True,
+ "settings": {"server_check": ["required"],
+ "client_cert": BAD_CLIENT_CERTS}},
+ {"regular_fail": False,
+ "checked_fail": True,
+ "settings": {"server_check": ["optional"],
+ "client_cert": BAD_CLIENT_CERTS}},
+ {"regular_fail": False,
+ "checked_fail": True,
+ "settings": {"server_check": ["ignore","optional"],
+ "server_cert": BAD_SERVER_CERTS}},
+ {"regular_fail": True,
+ "checked_fail": True,
+ "settings": {"server_check": ["required"],
+ "server_cert": BAD_SERVER_CERTS}},
+ {"regular_fail": True,
+ "checked_fail": False,
+ "settings": {"server_check": ["required"],
+ "client_cert": [CLIENT_WRONG_HOST],
+ "server_check_host": [False]}},
+ {"regular_fail": False,
+ "checked_fail": False,
+ "settings": {"server_check": ["optional"],
+ "client_cert": [CLIENT_WRONG_HOST],
+ "server_check_host": [False]}},
+]
+for ssl_mod in SSL_MODULES:
+ for client_host,server_host in itertools.product(CLIENT_HOSTS,SERVER_HOSTS):
+ for tests in TESTS:
+ combos = []
+ for attr,vals in tests["settings"].items():
+ combos.append([(attr,val) for val in vals])
+
+ for settings in itertools.product(*combos):
+ attrs = dict(settings, server_ssl = ssl_mod,
+ server_host = server_host,
+ client_host = client_host,
+ regular_fail = tests["regular_fail"],
+ checked_fail = tests["checked_fail"])
+ name = "SSLClientCertTest_" + str(attrs)
+ globals()[name] = type(name, (HTTPSTests, TestCase), attrs)
diff --git a/cherrypy/wsgiserver/ssl_builtin.py b/cherrypy/wsgiserver/ssl_builtin.py
index 7148dfda..12fddc80 100644
--- a/cherrypy/wsgiserver/ssl_builtin.py
+++ b/cherrypy/wsgiserver/ssl_builtin.py
@@ -5,6 +5,7 @@ The ssl module must be importable for SSL functionality.
To use this module, set ``CherryPyWSGIServer.ssl_adapter`` to an instance of
``BuiltinSSLAdapter``.
"""
+import socket, errno
try:
import ssl
@@ -21,7 +22,42 @@ except ImportError:
import sys
-from cherrypy import wsgiserver
+from cherrypy import wsgiserver, config
+
+
+def decode_cert(prefix, cert):
+ if not cert:
+ return None
+
+ key_map = {'countryName': 'C',
+ 'stateOrProvinceName': 'ST',
+ 'localityName': 'L',
+ 'organizationName': 'O',
+ 'organizationalUnitName': 'OU',
+ 'commonName': 'CN',
+ # Don't know by what key names python's ssl
+ # implementation uses for these fields.
+ #'???': 'T',
+ #'???': 'I',
+ #'???': 'G',
+ #'???': 'S',
+ #'???': 'D',
+ #'???': 'UID',
+ 'emailAddress': 'Email',
+ }
+
+ DN_string = ["subject="]
+ cert_dict = {}
+
+ for rdn in cert:
+ for key, item in rdn:
+ if key in key_map:
+ cert_dict["%s_%s" % (prefix, key_map[key])] = item
+ DN_string.append("%s=%s" % (key_map[key], item))
+
+ cert_dict[prefix] = "/".join(DN_string)
+
+ return cert_dict
class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
@@ -33,12 +69,26 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
private_key = None
"""The filename of the server's private key file."""
- def __init__(self, certificate, private_key, certificate_chain=None):
+ def __init__(self, certificate, private_key, certificate_chain=None,
+ client_CA=None):
if ssl is None:
raise ImportError("You must install the ssl module to use HTTPS.")
self.certificate = certificate
self.private_key = private_key
self.certificate_chain = certificate_chain
+ self.client_CA = client_CA or config.get("server.ssl_client_CA")
+
+ self.check_host = config.get("server.ssl_client_check_host", False)
+ check = config.get("server.ssl_client_check", "ignore")
+ if check == "ignore":
+ self.check = ssl.CERT_NONE
+ elif check == "optional":
+ self.check = ssl.CERT_OPTIONAL
+ elif check == "required":
+ self.check = ssl.CERT_REQUIRED
+ else:
+ raise ValueError("server.ssl_client_check must be one of 'ignore',"
+ "'optional','required'")
def bind(self, sock):
"""Wrap and return the given socket."""
@@ -47,9 +97,20 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
def wrap(self, sock):
"""Wrap and return the given socket, plus WSGI environ entries."""
try:
- s = ssl.wrap_socket(sock, do_handshake_on_connect=True,
- server_side=True, certfile=self.certificate,
- keyfile=self.private_key, ssl_version=ssl.PROTOCOL_SSLv23)
+ if self.client_CA:
+ s = ssl.wrap_socket(sock, do_handshake_on_connect=True,
+ server_side=True,
+ certfile=self.certificate,
+ keyfile=self.private_key,
+ ssl_version=ssl.PROTOCOL_SSLv23,
+ ca_certs=self.client_CA,
+ cert_reqs=self.check)
+ else:
+ s = ssl.wrap_socket(sock, do_handshake_on_connect=True,
+ server_side=True,
+ certfile=self.certificate,
+ keyfile=self.private_key,
+ ssl_version=ssl.PROTOCOL_SSLv23)
except ssl.SSLError:
e = sys.exc_info()[1]
if e.errno == ssl.SSL_ERROR_EOF:
@@ -80,6 +141,48 @@ class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
## SSL_VERSION_INTERFACE string The mod_ssl program version
## SSL_VERSION_LIBRARY string The OpenSSL program version
}
+
+ client_cert = sock.getpeercert()
+ if self.client_CA and (client_cert or self.check == ssl.CERT_REQUIRED):
+ if self.check == ssl.CERT_REQUIRED and not client_cert:
+ sock.close()
+ raise ssl.SSLError(ssl.SSL_ERROR_SSL, "certificate required but not found")
+
+ client_cert_subject = decode_cert( "SSL_CLIENT_S_DN", client_cert["subject"] )
+ if self.check_host:
+ try:
+ assert self.address_matches(sock.getpeername(),
+ client_cert_subject["SSL_CLIENT_S_DN_CN"])
+ except:
+ sock.close()
+ raise ssl.SSLError(ssl.SSL_ERROR_SSL, "certificate commonName doesn't match client socket address")
+
+ # Update for client environment variables
+ ssl_environ.update( {
+ #"SSL_CLIENT_M_VERSION": "",
+ #"SSL_CLIENT_M_SERIAL": "",
+ #"SSL_CLIENT_V_START": "",
+ "SSL_CLIENT_V_END": client_cert["notAfter"],
+ #"SSL_CLIENT_A_SIG": "",
+ #"SSL_CLIENT_A_KEY": "",
+ #"SSL_CLIENT_CERT": "",
+ #"SSL_CLIENT_CERT_CHAIN": "",
+ #"SSL_CLIENT_VERIFY": "",
+ } )
+
+ ssl_environ.update( client_cert_subject )
+
+ # Update for server environment variables
+ ssl_environ.update( {
+ #"SSL_SERVER_M_VERSION": "",
+ #"SSL_SERVER_M_SERIAL": "",
+ #"SSL_SERVER_V_START": "",
+ #"SSL_SERVER_V_END": "",
+ #"SSL_SERVER_A_SIG": "",
+ #"SSL_SERVER_A_KEY": "",
+ #"SSL_SERVER_CERT": "",
+ } )
+
return ssl_environ
if sys.version_info >= (3, 0):
diff --git a/cherrypy/wsgiserver/ssl_pyopenssl.py b/cherrypy/wsgiserver/ssl_pyopenssl.py
index 42745fbc..9b664f26 100644
--- a/cherrypy/wsgiserver/ssl_pyopenssl.py
+++ b/cherrypy/wsgiserver/ssl_pyopenssl.py
@@ -34,7 +34,7 @@ import socket
import threading
import time
-from cherrypy import wsgiserver
+from cherrypy import wsgiserver, config
try:
from OpenSSL import SSL
@@ -168,7 +168,8 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
This is needed for cheaper "chained root" SSL certificates, and should be
left as None if not required."""
- def __init__(self, certificate, private_key, certificate_chain=None):
+ def __init__(self, certificate, private_key, certificate_chain=None,
+ client_CA=None):
if SSL is None:
raise ImportError("You must install pyOpenSSL to use HTTPS.")
@@ -176,8 +177,21 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
self.certificate = certificate
self.private_key = private_key
self.certificate_chain = certificate_chain
+ self.client_CA = client_CA or config.get("server.ssl_client_CA")
self._environ = None
+ self.check_host = config.get("server.ssl_client_check_host", False)
+ check = config.get("server.ssl_client_check", "ignore")
+ if check == "ignore":
+ self.check = SSL.VERIFY_NONE
+ elif check == "optional":
+ self.check = SSL.VERIFY_PEER
+ elif check == "required":
+ self.check = SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT
+ else:
+ raise ValueError("server.ssl_client_check must be one of 'ignore',"
+ "'optional','required'")
+
def bind(self, sock):
"""Wrap and return the given socket."""
if self.context is None:
@@ -198,6 +212,23 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
if self.certificate_chain:
c.load_verify_locations(self.certificate_chain)
c.use_certificate_file(self.certificate)
+
+ if self.client_CA:
+ c.load_client_ca(self.client_CA)
+
+ c.set_verify_depth(2)
+ c.load_verify_locations(self.client_CA)
+
+ def callback(conn, cert, errno, depth, retcode):
+ if retcode and depth < 1 and self.check_host:
+ try:
+ assert self.address_matches(conn.getpeername(),
+ cert.get_subject().commonName)
+ except:
+ return False
+ return retcode
+
+ c.set_verify(self.check, callback)
return c
def get_environ(self):
diff --git a/cherrypy/wsgiserver/wsgiserver2.py b/cherrypy/wsgiserver/wsgiserver2.py
index bfb8f699..2f3c5ab6 100644
--- a/cherrypy/wsgiserver/wsgiserver2.py
+++ b/cherrypy/wsgiserver/wsgiserver2.py
@@ -161,6 +161,7 @@ def plat_specific_errors(*errnames):
socket_error_eintr = plat_specific_errors("EINTR", "WSAEINTR")
socket_errors_to_ignore = plat_specific_errors(
+ "EPERM",
"EPIPE",
"EBADF", "WSAEBADF",
"ENOTSOCK", "WSAENOTSOCK",
@@ -1049,7 +1050,13 @@ class CP_fileobject(socket._fileobject):
buf.write(data)
del data # explicit free
break
- assert n <= left, "recv(%d) returned %d bytes" % (left, n)
+ elif n > left:
+ # Could happen with SSL transport. Differ
+ # extra data read to the next call
+ buf.write(data[:left])
+ self._rbuf.write(data[left:])
+ del data
+ break
buf.write(data)
buf_len += n
del data # explicit free
@@ -1343,6 +1350,9 @@ class HTTPConnection(object):
"The client sent a plain HTTP request, but "
"this server only speaks HTTPS on this port.")
self.linger = True
+ except TypeError:
+ # Python bug #9729: http://bugs.python.org/issue9729
+ return
except Exception:
e = sys.exc_info()[1]
self.server.error_log(repr(e), level=logging.ERROR, traceback=True)
@@ -1602,10 +1612,12 @@ class SSLAdapter(object):
* ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) -> socket file object``
"""
- def __init__(self, certificate, private_key, certificate_chain=None):
+ def __init__(self, certificate, private_key, certificate_chain=None,
+ client_CA=None):
self.certificate = certificate
self.private_key = private_key
self.certificate_chain = certificate_chain
+ self.client_CA = client_CA
def wrap(self, sock):
raise NotImplemented
@@ -1613,6 +1625,29 @@ class SSLAdapter(object):
def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
raise NotImplemented
+ @staticmethod
+ def possible_addresses(address):
+ name, port = address[:2]
+ canonical, alt_hosts, host_ips = socket.gethostbyaddr(name)
+ all_addrs = set([name] + [canonical] + alt_hosts + host_ips)
+ all_addrs.update(info[4][0] for addr in all_addrs.copy()
+ for info in socket.getaddrinfo(addr, port))
+ return all_addrs
+
+ @staticmethod
+ def _matches(addr, cname):
+ if cname.startswith("*."):
+ return addr == cname[2:] or addr.endswith(cname[1:])
+ else:
+ return addr == cname
+
+ @staticmethod
+ def address_matches(address, common_name):
+ for possible in SSLAdapter.possible_addresses(address):
+ if SSLAdapter._matches(possible, common_name):
+ return True
+ return False
+
class HTTPServer(object):
"""An HTTP server."""
diff --git a/cherrypy/wsgiserver/wsgiserver3.py b/cherrypy/wsgiserver/wsgiserver3.py
index a7da188b..372a1c10 100644
--- a/cherrypy/wsgiserver/wsgiserver3.py
+++ b/cherrypy/wsgiserver/wsgiserver3.py
@@ -151,6 +151,7 @@ def plat_specific_errors(*errnames):
socket_error_eintr = plat_specific_errors("EINTR", "WSAEINTR")
socket_errors_to_ignore = plat_specific_errors(
+ "EPERM",
"EPIPE",
"EBADF", "WSAEBADF",
"ENOTSOCK", "WSAENOTSOCK",
@@ -1077,6 +1078,9 @@ class HTTPConnection(object):
"The client sent a plain HTTP request, but "
"this server only speaks HTTPS on this port.")
self.linger = True
+ except TypeError:
+ # Python bug #9729: http://bugs.python.org/issue9729
+ return
except Exception:
e = sys.exc_info()[1]
self.server.error_log(repr(e), level=logging.ERROR, traceback=True)
@@ -1336,10 +1340,12 @@ class SSLAdapter(object):
* ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) -> socket file object``
"""
- def __init__(self, certificate, private_key, certificate_chain=None):
+ def __init__(self, certificate, private_key, certificate_chain=None,
+ client_CA=None):
self.certificate = certificate
self.private_key = private_key
self.certificate_chain = certificate_chain
+ self.client_CA = client_CA
def wrap(self, sock):
raise NotImplemented
@@ -1347,6 +1353,29 @@ class SSLAdapter(object):
def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
raise NotImplemented
+ @staticmethod
+ def possible_addresses(address):
+ name, port = address[:2]
+ canonical, alt_hosts, host_ips = socket.gethostbyaddr(name)
+ all_addrs = set([name] + [canonical] + alt_hosts + host_ips)
+ all_addrs.update(info[4][0] for addr in all_addrs.copy()
+ for info in socket.getaddrinfo(addr, port))
+ return all_addrs
+
+ @staticmethod
+ def _matches(addr, cname):
+ if cname.startswith("*."):
+ return addr == cname[2:] or addr.endswith(cname[1:])
+ else:
+ return addr == cname
+
+ @staticmethod
+ def address_matches(address, common_name):
+ for possible in SSLAdapter.possible_addresses(address):
+ if SSLAdapter._matches(possible, common_name):
+ return True
+ return False
+
class HTTPServer(object):
"""An HTTP server."""