diff options
Diffstat (limited to 'test/net')
| -rw-r--r-- | test/net/fixtures/Makefile | 15 | ||||
| -rw-r--r-- | test/net/fixtures/cacert.pem | 24 | ||||
| -rw-r--r-- | test/net/fixtures/dhparams.pem | 29 | ||||
| -rw-r--r-- | test/net/fixtures/server.crt | 21 | ||||
| -rw-r--r-- | test/net/fixtures/server.key | 27 | ||||
| -rw-r--r-- | test/net/http/test_buffered_io.rb | 18 | ||||
| -rw-r--r-- | test/net/http/test_http.rb | 1427 | ||||
| -rw-r--r-- | test/net/http/test_http_request.rb | 125 | ||||
| -rw-r--r-- | test/net/http/test_httpheader.rb | 469 | ||||
| -rw-r--r-- | test/net/http/test_httpresponse.rb | 754 | ||||
| -rw-r--r-- | test/net/http/test_httpresponses.rb | 25 | ||||
| -rw-r--r-- | test/net/http/test_https.rb | 325 | ||||
| -rw-r--r-- | test/net/http/test_https_proxy.rb | 84 | ||||
| -rw-r--r-- | test/net/http/utils.rb | 359 | ||||
| -rw-r--r-- | test/net/protocol/test_protocol.rb | 159 |
15 files changed, 3861 insertions, 0 deletions
diff --git a/test/net/fixtures/Makefile b/test/net/fixtures/Makefile new file mode 100644 index 0000000000..88c232e3b6 --- /dev/null +++ b/test/net/fixtures/Makefile @@ -0,0 +1,15 @@ +all: + +regen_certs: + touch server.key + make server.crt + +cacert.pem: server.key + openssl req -new -x509 -days 3650 -key server.key -out cacert.pem -subj "/C=JP/ST=Shimane/L=Matz-e city/O=Ruby Core Team/CN=Ruby Test CA/emailAddress=security@ruby-lang.org" + +server.csr: + openssl req -new -key server.key -out server.csr -subj "/C=JP/ST=Shimane/O=Ruby Core Team/OU=Ruby Test/CN=localhost" + +server.crt: server.csr cacert.pem + openssl x509 -days 3650 -CA cacert.pem -CAkey server.key -set_serial 00 -in server.csr -req -out server.crt + rm server.csr diff --git a/test/net/fixtures/cacert.pem b/test/net/fixtures/cacert.pem new file mode 100644 index 0000000000..24c83f1c65 --- /dev/null +++ b/test/net/fixtures/cacert.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID+zCCAuOgAwIBAgIUGMvHl3EhtKPKcgc3NQSAYfFuC+8wDQYJKoZIhvcNAQEL +BQAwgYwxCzAJBgNVBAYTAkpQMRAwDgYDVQQIDAdTaGltYW5lMRQwEgYDVQQHDAtN +YXR6LWUgY2l0eTEXMBUGA1UECgwOUnVieSBDb3JlIFRlYW0xFTATBgNVBAMMDFJ1 +YnkgVGVzdCBDQTElMCMGCSqGSIb3DQEJARYWc2VjdXJpdHlAcnVieS1sYW5nLm9y +ZzAeFw0yNDAxMDExMTQ3MjNaFw0zMzEyMjkxMTQ3MjNaMIGMMQswCQYDVQQGEwJK +UDEQMA4GA1UECAwHU2hpbWFuZTEUMBIGA1UEBwwLTWF0ei1lIGNpdHkxFzAVBgNV +BAoMDlJ1YnkgQ29yZSBUZWFtMRUwEwYDVQQDDAxSdWJ5IFRlc3QgQ0ExJTAjBgkq +hkiG9w0BCQEWFnNlY3VyaXR5QHJ1YnktbGFuZy5vcmcwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCw+egZQ6eumJKq3hfKfED4dE/tL4FI5sjqont9ABVI ++1GSqyi1bFBgsRjM0THllIdMbKmJtWwnKW8J+5OgNN8y6Xxv8JmM/Y5vQt2lis0f +qXmG8UTz0VTWdlAXXmhUs6lSADvAaIe4RVrCsZ97L3ZQTryY7JRVcbB4khUN3Gp0 +yg+801SXzoFTTa+UGIRLE66jH51aa5VXu99hnv1OiH8tQrjdi8mH6uG/icq4XuIe +NWMF32wHqIOOPvQcWV3M5D2vxJEj702Ku6k9OQXkAo17qRSEonWW4HtLbtmS8He1 +JNPc/n3dVUm+fM6NoDXPoLP7j55G9zKyqGtGAWXAj1MTAgMBAAGjUzBRMB0GA1Ud +DgQWBBSJGVleDvFp9cu9R+E0/OKYzGkwkTAfBgNVHSMEGDAWgBSJGVleDvFp9cu9 +R+E0/OKYzGkwkTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBl +8GLB8skAWlkSw/FwbUmEV3zyqu+p7PNP5YIYoZs0D74e7yVulGQ6PKMZH5hrZmHo +orFSQU+VUUirG8nDGj7Rzce8WeWBxsaDGC8CE2dq6nC6LuUwtbdMnBrH0LRWAz48 +jGFF3jHtVz8VsGfoZTZCjukWqNXvU6hETT9GsfU+PZqbqcTVRPH52+XgYayKdIbD +r97RM4X3+aXBHcUW0b76eyyi65RR/Xtvn8ioZt2AdX7T2tZzJyXJN3Hupp77s6Ui +AZR35SToHCZeTZD12YBvLBdaTPLZN7O/Q/aAO9ZiJaZ7SbFOjz813B2hxXab4Fob +2uJX6eMWTVxYK5D4M9lm +-----END CERTIFICATE----- diff --git a/test/net/fixtures/dhparams.pem b/test/net/fixtures/dhparams.pem new file mode 100644 index 0000000000..389285bf59 --- /dev/null +++ b/test/net/fixtures/dhparams.pem @@ -0,0 +1,29 @@ + DH Parameters: (2048 bit) + prime: + 00:ec:4e:a4:06:b6:22:ca:f9:8a:00:cc:d0:ee:2f: + 16:bf:05:64:f5:8f:fe:7f:c4:bb:b0:24:cd:ef:5d: + 8a:90:ad:dc:a9:dd:63:84:90:d8:25:ba:d8:78:d5: + 77:91:42:0a:84:fc:56:1e:13:9b:1c:aa:43:d5:1f: + 38:52:92:fe:b3:66:f9:e7:e8:8c:77:a1:a6:2f:b3: + 98:98:d2:13:fc:57:1c:2a:14:dc:bd:e6:9b:54:19: + 99:4f:ce:81:64:a6:32:7f:8e:61:50:5f:45:3a:e5: + 0c:f7:13:f3:b8:ad:d5:77:ca:09:42:f7:d8:30:27: + 7b:2c:f0:b4:b5:a0:04:96:34:0b:47:81:1d:7f:c1: + 3a:62:86:8e:7d:f8:13:7f:9a:b1:8b:09:23:9e:55: + 59:41:cd:f0:86:09:c4:b7:d1:69:54:cb:d0:f5:e9: + 27:c9:e1:81:e4:a1:df:6b:20:1c:df:e8:54:02:f2: + 37:fc:2a:f7:d5:b3:6f:79:7e:70:22:78:79:18:3c: + 75:14:68:4a:05:9f:ac:d4:7f:9a:79:db:9d:0a:6e: + ec:0a:04:70:bf:c9:4a:59:81:a2:1f:33:9b:4a:66: + bc:03:ce:8a:1b:e3:03:ec:ba:39:26:ab:90:dc:39: + 41:a1:d8:f7:20:3c:8f:af:12:2f:f7:a9:6f:44:f1: + 6d:03 + generator: 2 (0x2) +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA7E6kBrYiyvmKAMzQ7i8WvwVk9Y/+f8S7sCTN712KkK3cqd1jhJDY +JbrYeNV3kUIKhPxWHhObHKpD1R84UpL+s2b55+iMd6GmL7OYmNIT/FccKhTcveab +VBmZT86BZKYyf45hUF9FOuUM9xPzuK3Vd8oJQvfYMCd7LPC0taAEljQLR4Edf8E6 +YoaOffgTf5qxiwkjnlVZQc3whgnEt9FpVMvQ9eknyeGB5KHfayAc3+hUAvI3/Cr3 +1bNveX5wInh5GDx1FGhKBZ+s1H+aedudCm7sCgRwv8lKWYGiHzObSma8A86KG+MD +7Lo5JquQ3DlBodj3IDyPrxIv96lvRPFtAwIBAg== +-----END DH PARAMETERS----- diff --git a/test/net/fixtures/server.crt b/test/net/fixtures/server.crt new file mode 100644 index 0000000000..5d2923795d --- /dev/null +++ b/test/net/fixtures/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDYTCCAkkCAQAwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAkpQMRAwDgYD +VQQIDAdTaGltYW5lMRQwEgYDVQQHDAtNYXR6LWUgY2l0eTEXMBUGA1UECgwOUnVi +eSBDb3JlIFRlYW0xFTATBgNVBAMMDFJ1YnkgVGVzdCBDQTElMCMGCSqGSIb3DQEJ +ARYWc2VjdXJpdHlAcnVieS1sYW5nLm9yZzAeFw0yNDAxMDExMTQ3MjNaFw0zMzEy +MjkxMTQ3MjNaMGAxCzAJBgNVBAYTAkpQMRAwDgYDVQQIDAdTaGltYW5lMRcwFQYD +VQQKDA5SdWJ5IENvcmUgVGVhbTESMBAGA1UECwwJUnVieSBUZXN0MRIwEAYDVQQD +DAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCw+egZ +Q6eumJKq3hfKfED4dE/tL4FI5sjqont9ABVI+1GSqyi1bFBgsRjM0THllIdMbKmJ +tWwnKW8J+5OgNN8y6Xxv8JmM/Y5vQt2lis0fqXmG8UTz0VTWdlAXXmhUs6lSADvA +aIe4RVrCsZ97L3ZQTryY7JRVcbB4khUN3Gp0yg+801SXzoFTTa+UGIRLE66jH51a +a5VXu99hnv1OiH8tQrjdi8mH6uG/icq4XuIeNWMF32wHqIOOPvQcWV3M5D2vxJEj +702Ku6k9OQXkAo17qRSEonWW4HtLbtmS8He1JNPc/n3dVUm+fM6NoDXPoLP7j55G +9zKyqGtGAWXAj1MTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACtGNdj5TEtnJBYp +M+LhBeU3oNteldfycEm993gJp6ghWZFg23oX8fVmyEeJr/3Ca9bAgDqg0t9a0npN +oWKEY6wVKqcHgu3gSvThF5c9KhGbeDDmlTSVVNQmXWX0K2d4lS2cwZHH8mCm2mrY +PDqlEkSc7k4qSiqigdS8i80Yk+lDXWsm8CjsiC93qaRM7DnS0WPQR0c16S95oM6G +VklFKUSDAuFjw9aVWA/nahOucjn0w5fVW6lyIlkBslC1ChlaDgJmvhz+Ol3iMsE0 +kAmFNu2KKPVrpMWaBID49QwQTDyhetNLaVVFM88iUdA9JDoVMEuP1mm39JqyzHTu +uBrdP4Q= +-----END CERTIFICATE----- diff --git a/test/net/fixtures/server.key b/test/net/fixtures/server.key new file mode 100644 index 0000000000..6a83d5bcf4 --- /dev/null +++ b/test/net/fixtures/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsPnoGUOnrpiSqt4XynxA+HRP7S+BSObI6qJ7fQAVSPtRkqso +tWxQYLEYzNEx5ZSHTGypibVsJylvCfuToDTfMul8b/CZjP2Ob0LdpYrNH6l5hvFE +89FU1nZQF15oVLOpUgA7wGiHuEVawrGfey92UE68mOyUVXGweJIVDdxqdMoPvNNU +l86BU02vlBiESxOuox+dWmuVV7vfYZ79Toh/LUK43YvJh+rhv4nKuF7iHjVjBd9s +B6iDjj70HFldzOQ9r8SRI+9NirupPTkF5AKNe6kUhKJ1luB7S27ZkvB3tSTT3P59 +3VVJvnzOjaA1z6Cz+4+eRvcysqhrRgFlwI9TEwIDAQABAoIBAEEYiyDP29vCzx/+ +dS3LqnI5BjUuJhXUnc6AWX/PCgVAO+8A+gZRgvct7PtZb0sM6P9ZcLrweomlGezI +FrL0/6xQaa8bBr/ve/a8155OgcjFo6fZEw3Dz7ra5fbSiPmu4/b/kvrg+Br1l77J +aun6uUAs1f5B9wW+vbR7tzbT/mxaUeDiBzKpe15GwcvbJtdIVMa2YErtRjc1/5B2 +BGVXyvlJv0SIlcIEMsHgnAFOp1ZgQ08aDzvilLq8XVMOahAhP1O2A3X8hKdXPyrx +IVWE9bS9ptTo+eF6eNl+d7htpKGEZHUxinoQpWEBTv+iOoHsVunkEJ3vjLP3lyI/ +fY0NQ1ECgYEA3RBXAjgvIys2gfU3keImF8e/TprLge1I2vbWmV2j6rZCg5r/AS0u +pii5CvJ5/T5vfJPNgPBy8B/yRDs+6PJO1GmnlhOkG9JAIPkv0RBZvR0PMBtbp6nT +Y3yo1lwamBVBfY6rc0sLTzosZh2aGoLzrHNMQFMGaauORzBFpY5lU50CgYEAzPHl +u5DI6Xgep1vr8QvCUuEesCOgJg8Yh1UqVoY/SmQh6MYAv1I9bLGwrb3WW/7kqIoD +fj0aQV5buVZI2loMomtU9KY5SFIsPV+JuUpy7/+VE01ZQM5FdY8wiYCQiVZYju9X +Wz5LxMNoz+gT7pwlLCsC4N+R8aoBk404aF1gum8CgYAJ7VTq7Zj4TFV7Soa/T1eE +k9y8a+kdoYk3BASpCHJ29M5R2KEA7YV9wrBklHTz8VzSTFTbKHEQ5W5csAhoL5Fo +qoHzFFi3Qx7MHESQb9qHyolHEMNx6QdsHUn7rlEnaTTyrXh3ifQtD6C0yTmFXUIS +CW9wKApOrnyKJ9nI0HcuZQKBgQCMtoV6e9VGX4AEfpuHvAAnMYQFgeBiYTkBKltQ +XwozhH63uMMomUmtSG87Sz1TmrXadjAhy8gsG6I0pWaN7QgBuFnzQ/HOkwTm+qKw +AsrZt4zeXNwsH7QXHEJCFnCmqw9QzEoZTrNtHJHpNboBuVnYcoueZEJrP8OnUG3r +UjmopwKBgAqB2KYYMUqAOvYcBnEfLDmyZv9BTVNHbR2lKkMYqv5LlvDaBxVfilE0 +2riO4p6BaAdvzXjKeRrGNEKoHNBpOSfYCOM16NjL8hIZB1CaV3WbT5oY+jp7Mzd5 +7d56RZOE+ERK2uz/7JX9VSsM/LbH9pJibd4e8mikDS9ntciqOH/3 +-----END RSA PRIVATE KEY----- diff --git a/test/net/http/test_buffered_io.rb b/test/net/http/test_buffered_io.rb new file mode 100644 index 0000000000..8c299ead03 --- /dev/null +++ b/test/net/http/test_buffered_io.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: false +require 'test/unit' +require 'net/http' +require 'stringio' + +require_relative 'utils' + +module Net + class TestBufferedIO < Test::Unit::TestCase + def test_eof? + s = StringIO.new + assert s.eof? + bio = BufferedIO.new(s) + assert_equal s, bio.io + assert_equal s.eof?, bio.eof? + end + end +end diff --git a/test/net/http/test_http.rb b/test/net/http/test_http.rb new file mode 100644 index 0000000000..4e7fa22756 --- /dev/null +++ b/test/net/http/test_http.rb @@ -0,0 +1,1427 @@ +# coding: US-ASCII +# frozen_string_literal: false +require 'test/unit' +require 'net/http' +require 'stringio' +require_relative 'utils' + +class TestNetHTTP < Test::Unit::TestCase + + def test_class_Proxy + no_proxy_class = Net::HTTP.Proxy nil + + assert_equal Net::HTTP, no_proxy_class + + proxy_class = Net::HTTP.Proxy 'proxy.example', 8000, 'user', 'pass' + + assert_not_equal Net::HTTP, proxy_class + + assert_operator proxy_class, :<, Net::HTTP + + assert_equal 'proxy.example', proxy_class.proxy_address + assert_equal 8000, proxy_class.proxy_port + assert_equal 'user', proxy_class.proxy_user + assert_equal 'pass', proxy_class.proxy_pass + + http = proxy_class.new 'hostname.example' + + assert_not_predicate http, :proxy_from_env? + + + proxy_class = Net::HTTP.Proxy 'proxy.example' + assert_equal 'proxy.example', proxy_class.proxy_address + assert_equal 80, proxy_class.proxy_port + end + + def test_class_Proxy_from_ENV + TestNetHTTPUtils.clean_http_proxy_env do + ENV['http_proxy'] = 'http://proxy.example:8000' + + # These are ignored on purpose. See Bug 4388 and Feature 6546 + ENV['http_proxy_user'] = 'user' + ENV['http_proxy_pass'] = 'pass' + + proxy_class = Net::HTTP.Proxy :ENV + + assert_not_equal Net::HTTP, proxy_class + + assert_operator proxy_class, :<, Net::HTTP + + assert_nil proxy_class.proxy_address + assert_nil proxy_class.proxy_user + assert_nil proxy_class.proxy_pass + + assert_not_equal 8000, proxy_class.proxy_port + + http = proxy_class.new 'hostname.example' + + assert http.proxy_from_env? + end + end + + def test_addr_port + http = Net::HTTP.new 'hostname.example', nil, nil + addr_port = http.__send__ :addr_port + assert_equal 'hostname.example', addr_port + + http.use_ssl = true + addr_port = http.__send__ :addr_port + assert_equal 'hostname.example:80', addr_port + + http = Net::HTTP.new '203.0.113.1', nil, nil + addr_port = http.__send__ :addr_port + assert_equal '203.0.113.1', addr_port + + http.use_ssl = true + addr_port = http.__send__ :addr_port + assert_equal '203.0.113.1:80', addr_port + + http = Net::HTTP.new '2001:db8::1', nil, nil + addr_port = http.__send__ :addr_port + assert_equal '[2001:db8::1]', addr_port + + http.use_ssl = true + addr_port = http.__send__ :addr_port + assert_equal '[2001:db8::1]:80', addr_port + + end + + def test_edit_path + http = Net::HTTP.new 'hostname.example', nil, nil + + edited = http.send :edit_path, '/path' + + assert_equal '/path', edited + + http.use_ssl = true + + edited = http.send :edit_path, '/path' + + assert_equal '/path', edited + end + + def test_edit_path_proxy + http = Net::HTTP.new 'hostname.example', nil, 'proxy.example' + + edited = http.send :edit_path, '/path' + + assert_equal 'http://hostname.example/path', edited + + http.use_ssl = true + + edited = http.send :edit_path, '/path' + + assert_equal '/path', edited + end + + def test_proxy_address + TestNetHTTPUtils.clean_http_proxy_env do + http = Net::HTTP.new 'hostname.example', nil, 'proxy.example' + assert_equal 'proxy.example', http.proxy_address + + http = Net::HTTP.new 'hostname.example', nil + assert_equal nil, http.proxy_address + end + end + + def test_proxy_address_no_proxy + TestNetHTTPUtils.clean_http_proxy_env do + http = Net::HTTP.new 'hostname.example', nil, 'proxy.com', nil, nil, nil, 'example' + assert_nil http.proxy_address + + http = Net::HTTP.new '10.224.1.1', nil, 'proxy.com', nil, nil, nil, 'example,10.224.0.0/22' + assert_nil http.proxy_address + end + end + + def test_proxy_from_env_ENV + TestNetHTTPUtils.clean_http_proxy_env do + ENV['http_proxy'] = 'http://proxy.example:8000' + + assert_equal false, Net::HTTP.proxy_class? + http = Net::HTTP.new 'hostname.example' + + assert_equal true, http.proxy_from_env? + end + end + + def test_proxy_address_ENV + TestNetHTTPUtils.clean_http_proxy_env do + ENV['http_proxy'] = 'http://proxy.example:8000' + + http = Net::HTTP.new 'hostname.example' + + assert_equal 'proxy.example', http.proxy_address + end + end + + def test_proxy_eh_no_proxy + TestNetHTTPUtils.clean_http_proxy_env do + assert_equal false, Net::HTTP.new('hostname.example', nil, nil).proxy? + end + end + + def test_proxy_eh_ENV + TestNetHTTPUtils.clean_http_proxy_env do + ENV['http_proxy'] = 'http://proxy.example:8000' + + http = Net::HTTP.new 'hostname.example' + + assert_equal true, http.proxy? + end + end + + def test_proxy_eh_ENV_with_user + TestNetHTTPUtils.clean_http_proxy_env do + ENV['http_proxy'] = 'http://foo:bar@proxy.example:8000' + + http = Net::HTTP.new 'hostname.example' + + assert_equal true, http.proxy? + assert_equal 'foo', http.proxy_user + assert_equal 'bar', http.proxy_pass + end + end + + def test_proxy_eh_ENV_with_urlencoded_user + TestNetHTTPUtils.clean_http_proxy_env do + ENV['http_proxy'] = 'http://Y%5CX:R%25S%5D%20%3FX@proxy.example:8000' + + http = Net::HTTP.new 'hostname.example' + + assert_equal true, http.proxy? + assert_equal "Y\\X", http.proxy_user + assert_equal "R%S] ?X", http.proxy_pass + end + end + + def test_proxy_eh_ENV_none_set + TestNetHTTPUtils.clean_http_proxy_env do + assert_equal false, Net::HTTP.new('hostname.example').proxy? + end + end + + def test_proxy_eh_ENV_no_proxy + TestNetHTTPUtils.clean_http_proxy_env do + ENV['http_proxy'] = 'http://proxy.example:8000' + ENV['no_proxy'] = 'hostname.example' + + assert_equal false, Net::HTTP.new('hostname.example').proxy? + end + end + + def test_proxy_port + TestNetHTTPUtils.clean_http_proxy_env do + http = Net::HTTP.new 'example', nil, 'proxy.example' + assert_equal 'proxy.example', http.proxy_address + assert_equal 80, http.proxy_port + http = Net::HTTP.new 'example', nil, 'proxy.example', 8000 + assert_equal 8000, http.proxy_port + http = Net::HTTP.new 'example', nil + assert_equal nil, http.proxy_port + end + end + + def test_proxy_port_ENV + TestNetHTTPUtils.clean_http_proxy_env do + ENV['http_proxy'] = 'http://proxy.example:8000' + + http = Net::HTTP.new 'hostname.example' + + assert_equal 8000, http.proxy_port + end + end + + def test_newobj + TestNetHTTPUtils.clean_http_proxy_env do + ENV['http_proxy'] = 'http://proxy.example:8000' + + http = Net::HTTP.newobj 'hostname.example' + + assert_equal false, http.proxy? + end + end + + def test_failure_message_includes_failed_domain_and_port + # hostname to be included in the error message + host = Struct.new(:to_s).new("<example>") + port = 2119 + # hack to let TCPSocket.open fail + def host.to_str; raise SocketError, "open failure"; end + uri = Struct.new(:scheme, :hostname, :port).new("http", host, port) + assert_raise_with_message(SocketError, /#{host}:#{port}/) do + TestNetHTTPUtils.clean_http_proxy_env{ Net::HTTP.get(uri) } + end + end + + def test_default_configuration + Net::HTTP.default_configuration = { open_timeout: 5 } + http = Net::HTTP.new 'hostname.example' + assert_equal 5, http.open_timeout + assert_equal 60, http.read_timeout + + http.open_timeout = 10 + assert_equal 10, http.open_timeout + ensure + Net::HTTP.default_configuration = nil + end + +end + +module TestNetHTTP_version_1_1_methods + + def test_s_start + begin + h = Net::HTTP.start(config('host'), config('port')) + ensure + h&.finish + end + assert_equal config('host'), h.address + assert_equal config('port'), h.port + assert_equal true, h.instance_variable_get(:@proxy_from_env) + + begin + h = Net::HTTP.start(config('host'), config('port'), :ENV) + ensure + h&.finish + end + assert_equal config('host'), h.address + assert_equal config('port'), h.port + assert_equal true, h.instance_variable_get(:@proxy_from_env) + + begin + h = Net::HTTP.start(config('host'), config('port'), nil) + ensure + h&.finish + end + assert_equal config('host'), h.address + assert_equal config('port'), h.port + assert_equal false, h.instance_variable_get(:@proxy_from_env) + end + + def test_s_get + assert_equal $test_net_http_data, + Net::HTTP.get(config('host'), '/', config('port')) + + assert_equal $test_net_http_data, Net::HTTP.get( + URI.parse("http://#{config('host')}:#{config('port')}") + ) + assert_equal $test_net_http_data, Net::HTTP.get( + URI.parse("http://#{config('host')}:#{config('port')}"), "Accept" => "text/plain" + ) + end + + def test_s_get_response + res = Net::HTTP.get_response( + URI.parse("http://#{config('host')}:#{config('port')}") + ) + assert_equal "application/octet-stream", res["Content-Type"] + assert_equal $test_net_http_data, res.body + + res = Net::HTTP.get_response( + URI.parse("http://#{config('host')}:#{config('port')}"), "Accept" => "text/plain" + ) + assert_equal "text/plain", res["Content-Type"] + assert_equal $test_net_http_data, res.body + end + + def test_head + start {|http| + res = http.head('/') + assert_kind_of Net::HTTPResponse, res + assert_equal $test_net_http_data_type, res['Content-Type'] + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_equal $test_net_http_data.size, res['Content-Length'].to_i + end + } + end + + def test_get + start {|http| + _test_get__get http + _test_get__iter http + _test_get__chunked http + } + end + + def _test_get__get(http) + res = http.get('/') + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_not_nil res['content-length'] + assert_equal $test_net_http_data.size, res['content-length'].to_i + end + assert_equal $test_net_http_data_type, res['Content-Type'] + assert_equal $test_net_http_data.size, res.body.size + assert_equal $test_net_http_data, res.body + + assert_nothing_raised { + http.get('/', { 'User-Agent' => 'test' }.freeze) + } + + assert res.decode_content, '[Bug #7924]' if Net::HTTP::HAVE_ZLIB + end + + def _test_get__iter(http) + buf = '' + res = http.get('/') {|s| buf << s } + assert_kind_of Net::HTTPResponse, res + # assert_kind_of String, res.body + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_not_nil res['content-length'] + assert_equal $test_net_http_data.size, res['content-length'].to_i + end + assert_equal $test_net_http_data_type, res['Content-Type'] + assert_equal $test_net_http_data.size, buf.size + assert_equal $test_net_http_data, buf + # assert_equal $test_net_http_data.size, res.body.size + # assert_equal $test_net_http_data, res.body + end + + def _test_get__chunked(http) + buf = '' + res = http.get('/') {|s| buf << s } + assert_kind_of Net::HTTPResponse, res + # assert_kind_of String, res.body + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_not_nil res['content-length'] + assert_equal $test_net_http_data.size, res['content-length'].to_i + end + assert_equal $test_net_http_data_type, res['Content-Type'] + assert_equal $test_net_http_data.size, buf.size + assert_equal $test_net_http_data, buf + # assert_equal $test_net_http_data.size, res.body.size + # assert_equal $test_net_http_data, res.body + end + + def test_get__break + i = 0 + start {|http| + http.get('/') do |str| + i += 1 + break + end + } + assert_equal 1, i + @log_tester = nil # server may encount ECONNRESET + end + + def test_get__implicit_start + res = new().get('/') + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_not_nil res['content-length'] + end + assert_equal $test_net_http_data_type, res['Content-Type'] + assert_equal $test_net_http_data.size, res.body.size + assert_equal $test_net_http_data, res.body + end + + def test_get__crlf + start {|http| + assert_raise(ArgumentError) do + http.get("\r") + end + assert_raise(ArgumentError) do + http.get("\n") + end + } + end + + def test_get2 + start {|http| + http.get2('/') {|res| + EnvUtil.suppress_warning do + assert_kind_of Net::HTTPResponse, res + assert_kind_of Net::HTTPResponse, res.header + end + + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_not_nil res['content-length'] + end + assert_equal $test_net_http_data_type, res['Content-Type'] + assert_kind_of String, res.body + assert_kind_of String, res.entity + assert_equal $test_net_http_data.size, res.body.size + assert_equal $test_net_http_data, res.body + assert_equal $test_net_http_data, res.entity + } + } + end + + def test_post + start {|http| + _test_post__base http + } + start {|http| + _test_post__file http + } + start {|http| + _test_post__no_data http + } + end + + def _test_post__base(http) + uheader = {} + uheader['Accept'] = 'application/octet-stream' + uheader['Content-Type'] = 'application/x-www-form-urlencoded' + data = 'post data' + res = http.post('/', data, uheader) + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + assert_equal data, res.body + assert_equal data, res.entity + end + + def _test_post__file(http) + data = 'post data' + f = StringIO.new + http.post('/', data, {'content-type' => 'application/x-www-form-urlencoded'}, f) + assert_equal data, f.string + end + + def _test_post__no_data(http) + unless self.is_a?(TestNetHTTP_v1_2_chunked) + EnvUtil.suppress_warning do + data = nil + res = http.post('/', data) + assert_not_equal '411', res.code + end + end + end + + def test_s_post + url = "http://#{config('host')}:#{config('port')}/?q=a" + res = Net::HTTP.post( + URI.parse(url), + "a=x") + assert_equal "application/octet-stream", res["Content-Type"] + assert_equal "a=x", res.body + assert_equal url, res["X-request-uri"] + + res = Net::HTTP.post( + URI.parse(url), + "hello world", + "Content-Type" => "text/plain; charset=US-ASCII") + assert_equal "text/plain; charset=US-ASCII", res["Content-Type"] + assert_equal "hello world", res.body + end + + def test_s_post_form + url = "http://#{config('host')}:#{config('port')}/" + res = Net::HTTP.post_form( + URI.parse(url), + "a" => "x") + assert_equal ["a=x"], res.body.split(/[;&]/).sort + + res = Net::HTTP.post_form( + URI.parse(url), + "a" => "x", + "b" => "y") + assert_equal ["a=x", "b=y"], res.body.split(/[;&]/).sort + + res = Net::HTTP.post_form( + URI.parse(url), + "a" => ["x1", "x2"], + "b" => "y") + assert_equal url, res['X-request-uri'] + assert_equal ["a=x1", "a=x2", "b=y"], res.body.split(/[;&]/).sort + + res = Net::HTTP.post_form( + URI.parse(url + '?a=x'), + "b" => "y") + assert_equal url + '?a=x', res['X-request-uri'] + assert_equal ["b=y"], res.body.split(/[;&]/).sort + end + + def test_patch + start {|http| + _test_patch__base http + } + end + + def _test_patch__base(http) + uheader = {} + uheader['Accept'] = 'application/octet-stream' + uheader['Content-Type'] = 'application/x-www-form-urlencoded' + data = 'patch data' + res = http.patch('/', data, uheader) + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + assert_equal data, res.body + assert_equal data, res.entity + end + + def test_timeout_during_HTTP_session_write + th = nil + # listen for connections... but deliberately do not read + TCPServer.open('localhost', 0) {|server| + port = server.addr[1] + + conn = Net::HTTP.new('localhost', port) + conn.write_timeout = EnvUtil.apply_timeout_scale(0.01) + conn.read_timeout = EnvUtil.apply_timeout_scale(0.01) if windows? + conn.open_timeout = EnvUtil.apply_timeout_scale(1) + + th = Thread.new do + err = !windows? ? Net::WriteTimeout : Net::ReadTimeout + assert_raise(err) do + conn.post('/', "a"*50_000_000) + end + end + assert th.join(EnvUtil.apply_timeout_scale(10)) + } + ensure + th&.kill + th&.join + end + + def test_timeout_during_non_chunked_streamed_HTTP_session_write + th = nil + # listen for connections... but deliberately do not read + TCPServer.open('localhost', 0) {|server| + port = server.addr[1] + + conn = Net::HTTP.new('localhost', port) + conn.write_timeout = EnvUtil.apply_timeout_scale(0.01) + conn.read_timeout = EnvUtil.apply_timeout_scale(0.01) if windows? + conn.open_timeout = EnvUtil.apply_timeout_scale(1) + + req = Net::HTTP::Post.new('/') + data = "a"*50_000_000 + req.content_length = data.size + req['Content-Type'] = 'application/x-www-form-urlencoded' + req.body_stream = StringIO.new(data) + + th = Thread.new do + assert_raise(Net::WriteTimeout) { conn.request(req) } + end + assert th.join(10) + } + ensure + th&.kill + th&.join + end + + def test_timeout_during_HTTP_session + bug4246 = "expected the HTTP session to have timed out but have not. c.f. [ruby-core:34203]" + + th = nil + # listen for connections... but deliberately do not read + TCPServer.open('localhost', 0) {|server| + port = server.addr[1] + + conn = Net::HTTP.new('localhost', port) + conn.read_timeout = EnvUtil.apply_timeout_scale(0.01) + conn.open_timeout = EnvUtil.apply_timeout_scale(1) + + th = Thread.new do + assert_raise(Net::ReadTimeout) { + conn.get('/') + } + end + assert th.join(EnvUtil.apply_timeout_scale(10)), bug4246 + } + ensure + th.kill + th.join + end +end + + +module TestNetHTTP_version_1_2_methods + + def test_request + start {|http| + _test_request__GET http + _test_request__accept_encoding http + _test_request__file http + # _test_request__range http # WEBrick does not support Range: header. + _test_request__HEAD http + _test_request__POST http + _test_request__uri http + _test_request__uri_host http + } + start {|http| + _test_request__stream_body http + } + end + + def _test_request__GET(http) + req = Net::HTTP::Get.new('/') + http.request(req) {|res| + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_not_nil res['content-length'] + assert_equal $test_net_http_data.size, res['content-length'].to_i + end + assert_equal $test_net_http_data.size, res.body.size + assert_equal $test_net_http_data, res.body + + assert res.decode_content, 'Bug #7831' if Net::HTTP::HAVE_ZLIB + } + end + + def _test_request__accept_encoding(http) + req = Net::HTTP::Get.new('/', 'accept-encoding' => 'deflate') + http.request(req) {|res| + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_not_nil res['content-length'] + assert_equal $test_net_http_data.size, res['content-length'].to_i + end + assert_equal $test_net_http_data.size, res.body.size + assert_equal $test_net_http_data, res.body + + assert_not_predicate res, :decode_content, 'Bug #7831' if Net::HTTP::HAVE_ZLIB + } + end + + def _test_request__file(http) + req = Net::HTTP::Get.new('/') + http.request(req) {|res| + assert_kind_of Net::HTTPResponse, res + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_not_nil res['content-length'] + assert_equal $test_net_http_data.size, res['content-length'].to_i + end + f = StringIO.new("".force_encoding("ASCII-8BIT")) + res.read_body f + assert_equal $test_net_http_data.bytesize, f.string.bytesize + assert_equal $test_net_http_data.encoding, f.string.encoding + assert_equal $test_net_http_data, f.string + } + end + + def _test_request__range(http) + req = Net::HTTP::Get.new('/') + req['range'] = 'bytes=0-5' + assert_equal $test_net_http_data[0,6], http.request(req).body + end + + def _test_request__HEAD(http) + req = Net::HTTP::Head.new('/') + http.request(req) {|res| + assert_kind_of Net::HTTPResponse, res + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_not_nil res['content-length'] + assert_equal $test_net_http_data.size, res['content-length'].to_i + end + assert_nil res.body + } + end + + def _test_request__POST(http) + data = 'post data' + req = Net::HTTP::Post.new('/') + req['Accept'] = $test_net_http_data_type + req['Content-Type'] = 'application/x-www-form-urlencoded' + http.request(req, data) {|res| + assert_kind_of Net::HTTPResponse, res + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_equal data.size, res['content-length'].to_i + end + assert_kind_of String, res.body + assert_equal data, res.body + } + end + + def _test_request__stream_body(http) + req = Net::HTTP::Post.new('/') + data = $test_net_http_data + req.content_length = data.size + req['Content-Type'] = 'application/x-www-form-urlencoded' + req.body_stream = StringIO.new(data) + res = http.request(req) + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + assert_equal data.size, res.body.size + assert_equal data, res.body + end + + def _test_request__path(http) + uri = URI 'https://hostname.example/' + req = Net::HTTP::Get.new('/') + + res = http.request(req) + + assert_kind_of URI::Generic, req.uri + + assert_not_equal uri, req.uri + + assert_equal uri, res.uri + + assert_not_same uri, req.uri + assert_not_same req.uri, res.uri + end + + def _test_request__uri(http) + uri = URI 'https://hostname.example/' + req = Net::HTTP::Get.new(uri) + + res = http.request(req) + + assert_kind_of URI::Generic, req.uri + + assert_not_equal uri, req.uri + + assert_equal req.uri, res.uri + + assert_not_same uri, req.uri + assert_not_same req.uri, res.uri + end + + def _test_request__uri_host(http) + uri = URI 'http://other.example/' + + req = Net::HTTP::Get.new(uri) + req['host'] = 'hostname.example' + + res = http.request(req) + + assert_kind_of URI::Generic, req.uri + + assert_equal URI("http://hostname.example:#{http.port}"), res.uri + end + + def test_send_request + start {|http| + _test_send_request__GET http + _test_send_request__HEAD http + _test_send_request__POST http + } + end + + def _test_send_request__GET(http) + res = http.send_request('GET', '/') + assert_kind_of Net::HTTPResponse, res + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_equal $test_net_http_data.size, res['content-length'].to_i + end + assert_kind_of String, res.body + assert_equal $test_net_http_data, res.body + end + + def _test_send_request__HEAD(http) + res = http.send_request('HEAD', '/') + assert_kind_of Net::HTTPResponse, res + unless self.is_a?(TestNetHTTP_v1_2_chunked) + assert_not_nil res['content-length'] + assert_equal $test_net_http_data.size, res['content-length'].to_i + end + assert_nil res.body + end + + def _test_send_request__POST(http) + data = 'aaabbb cc ddddddddddd lkjoiu4j3qlkuoa' + res = http.send_request('POST', '/', data, 'content-type' => 'application/x-www-form-urlencoded') + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + assert_equal data.size, res.body.size + assert_equal data, res.body + end + + def test_set_form + require 'tempfile' + Tempfile.create('ruby-test') {|file| + file << "\u{30c7}\u{30fc}\u{30bf}" + data = [ + ['name', 'Gonbei Nanashi'], + ['name', "\u{540d}\u{7121}\u{3057}\u{306e}\u{6a29}\u{5175}\u{885b}"], + ['s"i\o', StringIO.new("\u{3042 3044 4e9c 925b}")], + ["file", file, filename: "ruby-test"] + ] + expected = <<"__EOM__".gsub(/\n/, "\r\n") +--<boundary> +Content-Disposition: form-data; name="name" + +Gonbei Nanashi +--<boundary> +Content-Disposition: form-data; name="name" + +\xE5\x90\x8D\xE7\x84\xA1\xE3\x81\x97\xE3\x81\xAE\xE6\xA8\xA9\xE5\x85\xB5\xE8\xA1\x9B +--<boundary> +Content-Disposition: form-data; name="s\\"i\\\\o" + +\xE3\x81\x82\xE3\x81\x84\xE4\xBA\x9C\xE9\x89\x9B +--<boundary> +Content-Disposition: form-data; name="file"; filename="ruby-test" +Content-Type: application/octet-stream + +\xE3\x83\x87\xE3\x83\xBC\xE3\x82\xBF +--<boundary>-- +__EOM__ + start {|http| + _test_set_form_urlencoded(http, data.reject{|k,v|!v.is_a?(String)}) + } + start {|http| + @server.mount('/', lambda {|req, res| res.body = req.body }) + _test_set_form_multipart(http, false, data, expected) + } + start {|http| + @server.mount('/', lambda {|req, res| res.body = req.body }) + _test_set_form_multipart(http, true, data, expected) + } + } + end + + def _test_set_form_urlencoded(http, data) + req = Net::HTTP::Post.new('/') + req.set_form(data) + res = http.request req + assert_equal "name=Gonbei+Nanashi&name=%E5%90%8D%E7%84%A1%E3%81%97%E3%81%AE%E6%A8%A9%E5%85%B5%E8%A1%9B", res.body + end + + def _test_set_form_multipart(http, chunked_p, data, expected) + data.each{|k,v|v.rewind rescue nil} + req = Net::HTTP::Post.new('/') + req.set_form(data, 'multipart/form-data') + req['Transfer-Encoding'] = 'chunked' if chunked_p + res = http.request req + body = res.body + assert_match(/\A--(?<boundary>\S+)/, body) + /\A--(?<boundary>\S+)/ =~ body + expected = expected.gsub(/<boundary>/, boundary) + assert_equal(expected, body) + end + + def test_set_form_with_file + require 'tempfile' + Tempfile.create('ruby-test') {|file| + file.binmode + file << $test_net_http_data + filename = File.basename(file.to_path) + data = [['file', file]] + expected = <<"__EOM__".gsub(/\n/, "\r\n") +--<boundary> +Content-Disposition: form-data; name="file"; filename="<filename>" +Content-Type: application/octet-stream + +<data> +--<boundary>-- +__EOM__ + expected.sub!(/<filename>/, filename) + expected.sub!(/<data>/, $test_net_http_data) + start {|http| + @server.mount('/', lambda {|req, res| res.body = req.body }) + data.each{|k,v|v.rewind rescue nil} + req = Net::HTTP::Post.new('/') + req.set_form(data, 'multipart/form-data') + res = http.request req + body = res.body + header, _ = body.split(/\r\n\r\n/, 2) + assert_match(/\A--(?<boundary>\S+)/, body) + /\A--(?<boundary>\S+)/ =~ body + expected = expected.gsub(/<boundary>/, boundary) + assert_match(/^--(?<boundary>\S+)\r\n/, header) + assert_match( + /^Content-Disposition: form-data; name="file"; filename="#{filename}"\r\n/, + header) + assert_equal(expected, body) + + # TODO: test with chunked + # data.each{|k,v|v.rewind rescue nil} + # req['Transfer-Encoding'] = 'chunked' + # res = http.request req + # assert_equal(expected, res.body) + } + } + end +end + +class TestNetHTTP_v1_2 < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'proxy_host' => nil, + 'proxy_port' => nil, + } + + include TestNetHTTPUtils + include TestNetHTTP_version_1_1_methods + include TestNetHTTP_version_1_2_methods + + def new + Net::HTTP.version_1_2 + super + end + + def test_send_large_POST_request + start {|http| + data = ' '*6000000 + res = http.send_request('POST', '/', data, 'content-type' => 'application/x-www-form-urlencoded') + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + assert_equal data.size, res.body.size + assert_equal data, res.body + } + end +end + +class TestNetHTTP_v1_2_chunked < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'proxy_host' => nil, + 'proxy_port' => nil, + 'chunked' => true, + } + + include TestNetHTTPUtils + include TestNetHTTP_version_1_1_methods + include TestNetHTTP_version_1_2_methods + + def new + Net::HTTP.version_1_2 + super + end + + def test_chunked_break + assert_nothing_raised("[ruby-core:29229]") { + start {|http| + http.request_get('/') {|res| + res.read_body {|chunk| + break + } + } + } + } + end +end + +class TestNetHTTPContinue < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'proxy_host' => nil, + 'proxy_port' => nil, + 'chunked' => true, + } + + include TestNetHTTPUtils + + def logfile + @debug = StringIO.new('') + end + + def mount_proc(&block) + @server.mount('/continue', block.to_proc) + end + + def test_expect_continue + mount_proc {|req, res| + req.continue + res.body = req.query['body'] + } + start {|http| + uheader = {'content-type' => 'application/x-www-form-urlencoded', 'expect' => '100-continue'} + http.continue_timeout = 0.2 + http.request_post('/continue', 'body=BODY', uheader) {|res| + assert_equal('BODY', res.read_body) + } + } + assert_match(/Expect: 100-continue/, @debug.string) + assert_match(/HTTP\/1.1 100 continue/, @debug.string) + end + + def test_expect_continue_timeout + mount_proc {|req, res| + sleep 0.2 + req.continue # just ignored because it's '100' + res.body = req.query['body'] + } + start {|http| + uheader = {'content-type' => 'application/x-www-form-urlencoded', 'expect' => '100-continue'} + http.continue_timeout = 0 + http.request_post('/continue', 'body=BODY', uheader) {|res| + assert_equal('BODY', res.read_body) + } + } + assert_match(/Expect: 100-continue/, @debug.string) + assert_match(/HTTP\/1.1 100 continue/, @debug.string) + end + + def test_expect_continue_error + mount_proc {|req, res| + res.status = 501 + res.body = req.query['body'] + } + start {|http| + uheader = {'content-type' => 'application/x-www-form-urlencoded', 'expect' => '100-continue'} + http.continue_timeout = 0 + http.request_post('/continue', 'body=ERROR', uheader) {|res| + assert_equal('ERROR', res.read_body) + } + } + assert_match(/Expect: 100-continue/, @debug.string) + assert_not_match(/HTTP\/1.1 100 continue/, @debug.string) + end + + def test_expect_continue_error_before_body + @log_tester = nil + mount_proc {|req, res| + raise TestNetHTTPUtils::Forbidden + } + start {|http| + uheader = {'content-type' => 'application/x-www-form-urlencoded', 'content-length' => '5', 'expect' => '100-continue'} + http.continue_timeout = 1 # allow the server to respond before sending + http.request_post('/continue', 'data', uheader) {|res| + assert_equal(res.code, '403') + } + } + assert_match(/Expect: 100-continue/, @debug.string) + assert_not_match(/HTTP\/1.1 100 continue/, @debug.string) + end + + def test_expect_continue_error_while_waiting + mount_proc {|req, res| + res.status = 501 + res.body = req.query['body'] + } + start {|http| + uheader = {'content-type' => 'application/x-www-form-urlencoded', 'expect' => '100-continue'} + http.continue_timeout = 0.5 + http.request_post('/continue', 'body=ERROR', uheader) {|res| + assert_equal('ERROR', res.read_body) + } + } + assert_match(/Expect: 100-continue/, @debug.string) + assert_not_match(/HTTP\/1.1 100 continue/, @debug.string) + end +end + +class TestNetHTTPSwitchingProtocols < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'proxy_host' => nil, + 'proxy_port' => nil, + 'chunked' => true, + } + + include TestNetHTTPUtils + + def logfile + @debug = StringIO.new('') + end + + def mount_proc(&block) + @server.mount('/continue', block.to_proc) + end + + def test_info + mount_proc {|req, res| + req.instance_variable_get(:@socket) << "HTTP/1.1 101 Switching Protocols\r\n\r\n" + res.body = req.query['body'] + } + start {|http| + http.continue_timeout = 0.2 + http.request_post('/continue', 'body=BODY', + 'content-type' => 'application/x-www-form-urlencoded') {|res| + assert_equal('BODY', res.read_body) + } + } + assert_match(/HTTP\/1.1 101 Switching Protocols/, @debug.string) + end +end + +class TestNetHTTPKeepAlive < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'proxy_host' => nil, + 'proxy_port' => nil, + 'RequestTimeout' => 1, + } + + include TestNetHTTPUtils + + def test_keep_alive_get_auto_reconnect + start {|http| + res = http.get('/') + http.keep_alive_timeout = 1 + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + sleep 1.5 + assert_nothing_raised { + res = http.get('/') + } + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + } + end + + def test_server_closed_connection_auto_reconnect + start {|http| + res = http.get('/') + http.keep_alive_timeout = 5 + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + sleep 1.5 + assert_nothing_raised { + # Net::HTTP should detect the closed connection before attempting the + # request, since post requests cannot be retried. + res = http.post('/', 'query=foo', 'content-type' => 'application/x-www-form-urlencoded') + } + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + } + end + + def test_keep_alive_get_auto_retry + start {|http| + res = http.get('/') + http.keep_alive_timeout = 5 + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + sleep 1.5 + res = http.get('/') + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + } + end + + def test_keep_alive_reset_on_new_connection + # Using debug log output on accepting connection: + # + # "[2021-04-29 20:36:46] DEBUG accept: 127.0.0.1:50674\n" + @log_tester = nil + @logger_level = :debug + + start {|http| + res = http.get('/') + http.keep_alive_timeout = 1 + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + http.finish + assert_equal 1, @log.grep(/accept/i).size + + sleep 1.5 + http.start + res = http.get('/') + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + assert_equal 2, @log.grep(/accept/i).size + } + end + + class MockSocket + attr_reader :count + def initialize(success_after: nil) + @success_after = success_after + @count = 0 + end + def close + end + def closed? + end + def write(_) + end + def readline + @count += 1 + if @success_after && @success_after <= @count + "HTTP/1.1 200 OK" + else + raise Errno::ECONNRESET + end + end + def readuntil(*_) + "" + end + def read_all(_) + end + end + + def test_http_retry_success + start {|http| + socket = MockSocket.new(success_after: 10) + http.instance_variable_get(:@socket).close + http.instance_variable_set(:@socket, socket) + assert_equal 0, socket.count + http.max_retries = 10 + res = http.get('/') + assert_equal 10, socket.count + assert_kind_of Net::HTTPResponse, res + assert_kind_of String, res.body + } + end + + def test_http_retry_failed + start {|http| + socket = MockSocket.new + http.instance_variable_get(:@socket).close + http.instance_variable_set(:@socket, socket) + http.max_retries = 10 + assert_raise(Errno::ECONNRESET){ http.get('/') } + assert_equal 11, socket.count + } + end + + def test_http_retry_failed_with_block + start {|http| + http.max_retries = 10 + called = 0 + assert_raise(Errno::ECONNRESET){ http.get('/'){called += 1; raise Errno::ECONNRESET} } + assert_equal 1, called + } + @log_tester = nil + end + + def test_keep_alive_server_close + def @server.run(sock) + sock.close + end + + start {|http| + assert_raise(EOFError, Errno::ECONNRESET, IOError) { + http.get('/') + } + } + end +end + +class TestNetHTTPLocalBind < Test::Unit::TestCase + CONFIG = { + 'host' => 'localhost', + 'proxy_host' => nil, + 'proxy_port' => nil, + } + + include TestNetHTTPUtils + + def test_bind_to_local_host + @server.mount_proc('/show_ip') { |req, res| res.body = req.remote_ip } + + http = Net::HTTP.new(config('host'), config('port')) + http.local_host = Addrinfo.tcp(config('host'), config('port')).ip_address + assert_not_nil(http.local_host) + assert_nil(http.local_port) + + res = http.get('/show_ip') + assert_equal(http.local_host, res.body) + end + + def test_bind_to_local_port + @server.mount_proc('/show_port') { |req, res| res.body = req.peeraddr[1].to_s } + + http = Net::HTTP.new(config('host'), config('port')) + http.local_host = Addrinfo.tcp(config('host'), config('port')).ip_address + http.local_port = Addrinfo.tcp(config('host'), 0).bind {|s| + s.local_address.ip_port.to_s + } + assert_not_nil(http.local_host) + assert_not_nil(http.local_port) + + res = http.get('/show_port') + assert_equal(http.local_port, res.body) + end +end + +class TestNetHTTPForceEncoding < Test::Unit::TestCase + CONFIG = { + 'host' => 'localhost', + 'proxy_host' => nil, + 'proxy_port' => nil, + } + + include TestNetHTTPUtils + + def fe_request(force_enc, content_type=nil) + @server.mount_proc('/fe') do |req, res| + res['Content-Type'] = content_type if content_type + res.body = "hello\u1234" + end + + http = Net::HTTP.new(config('host'), config('port')) + http.local_host = Addrinfo.tcp(config('host'), config('port')).ip_address + assert_not_nil(http.local_host) + assert_nil(http.local_port) + + http.response_body_encoding = force_enc + http.get('/fe') + end + + def test_response_body_encoding_false + res = fe_request(false) + assert_equal("hello\u1234".b, res.body) + assert_equal(Encoding::ASCII_8BIT, res.body.encoding) + end + + def test_response_body_encoding_true_without_content_type + res = fe_request(true) + assert_equal("hello\u1234".b, res.body) + assert_equal(Encoding::ASCII_8BIT, res.body.encoding) + end + + def test_response_body_encoding_true_with_content_type + res = fe_request(true, 'text/html; charset=utf-8') + assert_equal("hello\u1234", res.body) + assert_equal(Encoding::UTF_8, res.body.encoding) + end + + def test_response_body_encoding_string_without_content_type + res = fe_request('utf-8') + assert_equal("hello\u1234", res.body) + assert_equal(Encoding::UTF_8, res.body.encoding) + end + + def test_response_body_encoding_encoding_without_content_type + res = fe_request(Encoding::UTF_8) + assert_equal("hello\u1234", res.body) + assert_equal(Encoding::UTF_8, res.body.encoding) + end +end + +class TestNetHTTPPartialResponse < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'proxy_host' => nil, + 'proxy_port' => nil, + } + + include TestNetHTTPUtils + + def test_partial_response + str = "0123456789" + @server.mount_proc('/') do |req, res| + res.status = 200 + res['Content-Type'] = 'text/plain' + + res.body = str + res['Content-Length'] = str.length + 1 + end + @server.mount_proc('/show_ip') { |req, res| res.body = req.remote_ip } + + http = Net::HTTP.new(config('host'), config('port')) + res = http.get('/') + assert_equal(str, res.body) + + http = Net::HTTP.new(config('host'), config('port')) + http.ignore_eof = false + assert_raise(EOFError) {http.get('/')} + end +end + +class TestNetHTTPInRactor < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'proxy_host' => nil, + 'proxy_port' => nil, + } + + include TestNetHTTPUtils + + def test_get + assert_ractor(<<~RUBY, require: 'net/http') + expected = #{$test_net_http_data.dump}.b + ret = Ractor.new { + host = #{config('host').dump} + port = #{config('port')} + Net::HTTP.start(host, port) { |http| + res = http.get('/') + res.body + } + }.value + assert_equal expected, ret + RUBY + end +end if defined?(Ractor) && Ractor.method_defined?(:value) diff --git a/test/net/http/test_http_request.rb b/test/net/http/test_http_request.rb new file mode 100644 index 0000000000..9f5cf4f8f5 --- /dev/null +++ b/test/net/http/test_http_request.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: false +require 'net/http' +require 'test/unit' + +class HTTPRequestTest < Test::Unit::TestCase + + def test_initialize_GET + req = Net::HTTP::Get.new '/' + + assert_equal 'GET', req.method + assert_not_predicate req, :request_body_permitted? + assert_predicate req, :response_body_permitted? + + expected = { + 'accept' => %w[*/*], + 'user-agent' => %w[Ruby], + } + + expected['accept-encoding'] = %w[gzip;q=1.0,deflate;q=0.6,identity;q=0.3] if + Net::HTTP::HAVE_ZLIB + + assert_equal expected, req.to_hash + end + + def test_initialize_GET_range + req = Net::HTTP::Get.new '/', 'Range' => 'bytes=0-9' + + assert_equal 'GET', req.method + assert_not_predicate req, :request_body_permitted? + assert_predicate req, :response_body_permitted? + + expected = { + 'accept' => %w[*/*], + 'user-agent' => %w[Ruby], + 'range' => %w[bytes=0-9], + } + + assert_equal expected, req.to_hash + end + + def test_initialize_HEAD + req = Net::HTTP::Head.new '/' + + assert_equal 'HEAD', req.method + assert_not_predicate req, :request_body_permitted? + assert_not_predicate req, :response_body_permitted? + + expected = { + 'accept' => %w[*/*], + "accept-encoding" => %w[gzip;q=1.0,deflate;q=0.6,identity;q=0.3], + 'user-agent' => %w[Ruby], + } + + assert_equal expected, req.to_hash + end + + def test_initialize_accept_encoding + req1 = Net::HTTP::Get.new '/' + + assert req1.decode_content, 'Bug #7831 - automatically decode content' + + req2 = Net::HTTP::Get.new '/', 'accept-encoding' => 'identity' + + assert_not_predicate req2, :decode_content, + 'Bug #7381 - do not decode content if the user overrides' + end if Net::HTTP::HAVE_ZLIB + + def test_initialize_GET_uri + req = Net::HTTP::Get.new(URI("http://example.com/foo")) + assert_equal "/foo", req.path + assert_equal "example.com", req['Host'] + + req = Net::HTTP::Get.new(URI("https://example.com/foo")) + assert_equal "/foo", req.path + assert_equal "example.com", req['Host'] + + req = Net::HTTP::Get.new(URI("https://203.0.113.1/foo")) + assert_equal "/foo", req.path + assert_equal "203.0.113.1", req['Host'] + + req = Net::HTTP::Get.new(URI("https://203.0.113.1:8000/foo")) + assert_equal "/foo", req.path + assert_equal "203.0.113.1:8000", req['Host'] + + req = Net::HTTP::Get.new(URI("https://[2001:db8::1]:8000/foo")) + assert_equal "/foo", req.path + assert_equal "[2001:db8::1]:8000", req['Host'] + + assert_raise(ArgumentError){ Net::HTTP::Get.new(URI("urn:ietf:rfc:7231")) } + assert_raise(ArgumentError){ Net::HTTP::Get.new(URI("http://")) } + end + + def test_header_set + req = Net::HTTP::Get.new '/' + + assert req.decode_content, 'Bug #7831 - automatically decode content' + + req['accept-encoding'] = 'identity' + + assert_not_predicate req, :decode_content, + 'Bug #7831 - do not decode content if the user overrides' + end if Net::HTTP::HAVE_ZLIB + + def test_update_uri + req = Net::HTTP::Get.new(URI.parse("http://203.0.113.1")) + req.update_uri("test", 8080, false) + assert_equal "203.0.113.1", req.uri.host + assert_equal 8080, req.uri.port + + req = Net::HTTP::Get.new(URI.parse("http://203.0.113.1:2020")) + req.update_uri("test", 8080, false) + assert_equal "203.0.113.1", req.uri.host + assert_equal 8080, req.uri.port + + req = Net::HTTP::Get.new(URI.parse("http://[2001:db8::1]")) + req.update_uri("test", 8080, false) + assert_equal "[2001:db8::1]", req.uri.host + assert_equal 8080, req.uri.port + + req = Net::HTTP::Get.new(URI.parse("http://[2001:db8::1]:2020")) + req.update_uri("test", 8080, false) + assert_equal "[2001:db8::1]", req.uri.host + assert_equal 8080, req.uri.port + end +end diff --git a/test/net/http/test_httpheader.rb b/test/net/http/test_httpheader.rb new file mode 100644 index 0000000000..69563168db --- /dev/null +++ b/test/net/http/test_httpheader.rb @@ -0,0 +1,469 @@ +# frozen_string_literal: false +require 'net/http' +require 'test/unit' + +class HTTPHeaderTest < Test::Unit::TestCase + + class C + include Net::HTTPHeader + def initialize + initialize_http_header({}) + end + attr_accessor :body + end + + def setup + @c = C.new + end + + def test_initialize + @c.initialize_http_header("foo"=>"abc") + assert_equal "abc", @c["foo"] + @c.initialize_http_header("foo"=>"abc", "bar"=>"xyz") + assert_equal "xyz", @c["bar"] + @c.initialize_http_header([["foo", "abc"]]) + assert_equal "abc", @c["foo"] + @c.initialize_http_header([["foo", "abc"], ["bar","xyz"]]) + assert_equal "xyz", @c["bar"] + assert_raise(NoMethodError){ @c.initialize_http_header("foo"=>[]) } + assert_raise(ArgumentError){ @c.initialize_http_header("foo"=>"a\nb") } + assert_raise(ArgumentError){ @c.initialize_http_header("foo"=>"a\rb") } + end + + def test_initialize_with_broken_coderange + error = RUBY_VERSION >= "3.2" ? Encoding::CompatibilityError : ArgumentError + assert_raise(error){ @c.initialize_http_header("foo"=>"a\xff") } + end + + def test_initialize_with_symbol + @c.initialize_http_header(foo: "abc") + assert_equal "abc", @c["foo"] + end + + def test_size + assert_equal 0, @c.size + @c['a'] = 'a' + assert_equal 1, @c.size + @c['b'] = 'b' + assert_equal 2, @c.size + @c['b'] = 'b' + assert_equal 2, @c.size + @c['c'] = 'c' + assert_equal 3, @c.size + end + + def test_ASET + @c['My-Header'] = 'test string' + @c['my-Header'] = 'test string' + @c['My-header'] = 'test string' + @c['my-header'] = 'test string' + @c['MY-HEADER'] = 'test string' + assert_equal 1, @c.size + + @c['AaA'] = 'aaa' + @c['aaA'] = 'aaa' + @c['AAa'] = 'aaa' + assert_equal 2, @c.length + + @c['aaa'] = ['aaa', ['bbb', [3]]] + assert_equal 2, @c.length + assert_equal ['aaa', 'bbb', '3'], @c.get_fields('aaa') + + @c['aaa'] = "aaa\xff" + assert_equal 2, @c.length + + assert_raise(ArgumentError){ @c['foo'] = "a\nb" } + assert_raise(ArgumentError){ @c['foo'] = ["a\nb"] } + end + + def test_AREF + @c['My-Header'] = 'test string' + assert_equal 'test string', @c['my-header'] + assert_equal 'test string', @c['MY-header'] + assert_equal 'test string', @c['my-HEADER'] + + @c['Next-Header'] = 'next string' + assert_equal 'next string', @c['next-header'] + end + + def test_add_field + @c.add_field 'My-Header', 'a' + assert_equal 'a', @c['My-Header'] + assert_equal ['a'], @c.get_fields('My-Header') + @c.add_field 'My-Header', 'b' + assert_equal 'a, b', @c['My-Header'] + assert_equal ['a', 'b'], @c.get_fields('My-Header') + @c.add_field 'My-Header', 'c' + assert_equal 'a, b, c', @c['My-Header'] + assert_equal ['a', 'b', 'c'], @c.get_fields('My-Header') + @c.add_field 'My-Header', 'd, d' + assert_equal 'a, b, c, d, d', @c['My-Header'] + assert_equal ['a', 'b', 'c', 'd, d'], @c.get_fields('My-Header') + assert_raise(ArgumentError){ @c.add_field 'My-Header', "d\nd" } + @c.add_field 'My-Header', ['e', ["\xff", 7]] + assert_equal "a, b, c, d, d, e, \xff, 7", @c['My-Header'] + assert_equal ['a', 'b', 'c', 'd, d', 'e', "\xff", '7'], @c.get_fields('My-Header') + end + + def test_get_fields + @c['My-Header'] = 'test string' + assert_equal ['test string'], @c.get_fields('my-header') + assert_equal ['test string'], @c.get_fields('My-header') + assert_equal ['test string'], @c.get_fields('my-Header') + + assert_nil @c.get_fields('not-found') + assert_nil @c.get_fields('Not-Found') + + @c.get_fields('my-header').push 'junk' + assert_equal ['test string'], @c.get_fields('my-header') + @c.get_fields('my-header').clear + assert_equal ['test string'], @c.get_fields('my-header') + end + + class D; include Net::HTTPHeader; end + + def test_nil_variable_header + assert_nothing_raised do + assert_warning("#{__FILE__}:#{__LINE__+1}: warning: net/http: nil HTTP header: Authorization\n") do + D.new.initialize_http_header({Authorization: nil}) + end + end + end + + def test_duplicated_variable_header + assert_nothing_raised do + assert_warning("#{__FILE__}:#{__LINE__+1}: warning: net/http: duplicated HTTP header: Authorization\n") do + D.new.initialize_http_header({"AUTHORIZATION": "yes", "Authorization": "no"}) + end + end + end + + def test_delete + @c['My-Header'] = 'test' + assert_equal 'test', @c['My-Header'] + assert_nil @c['not-found'] + @c.delete 'My-Header' + assert_nil @c['My-Header'] + assert_nil @c['not-found'] + @c.delete 'My-Header' + @c.delete 'My-Header' + assert_nil @c['My-Header'] + assert_nil @c['not-found'] + end + + def test_each + @c['My-Header'] = 'test' + @c.each do |k, v| + assert_equal 'my-header', k + assert_equal 'test', v + end + @c.each do |k, v| + assert_equal 'my-header', k + assert_equal 'test', v + end + e = @c.each + assert_equal 1, e.size + e.each do |k, v| + assert_equal 'my-header', k + assert_equal 'test', v + end + end + + def test_each_key + @c['My-Header'] = 'test' + @c.each_key do |k| + assert_equal 'my-header', k + end + @c.each_key do |k| + assert_equal 'my-header', k + end + e = @c.each_key + assert_equal 1, e.size + e.each do |k| + assert_equal 'my-header', k + end + end + + def test_each_capitalized_name + @c['my-header'] = 'test' + @c.each_capitalized_name do |k| + assert_equal 'My-Header', k + end + @c.each_capitalized_name do |k| + assert_equal 'My-Header', k + end + e = @c.each_capitalized_name + assert_equal 1, e.size + e.each do |k| + assert_equal 'My-Header', k + end + end + + def test_each_value + @c['My-Header'] = 'test' + @c.each_value do |v| + assert_equal 'test', v + end + @c.each_value do |v| + assert_equal 'test', v + end + e = @c.each_value + assert_equal 1, e.size + e.each do |v| + assert_equal 'test', v + end + end + + def test_canonical_each + @c['my-header'] = ['a', 'b'] + @c.canonical_each do |k,v| + assert_equal 'My-Header', k + assert_equal 'a, b', v + end + e = @c.canonical_each + assert_equal 1, e.size + e.each do |k,v| + assert_equal 'My-Header', k + assert_equal 'a, b', v + end + end + + def test_each_capitalized + @c['my-header'] = ['a', 'b'] + @c.each_capitalized do |k,v| + assert_equal 'My-Header', k + assert_equal 'a, b', v + end + e = @c.each_capitalized + assert_equal 1, e.size + e.each do |k,v| + assert_equal 'My-Header', k + assert_equal 'a, b', v + end + end + + def test_each_capitalized_with_symbol + @c[:my_header] = ['a', 'b'] + @c.each_capitalized do |k,v| + assert_equal "My_header", k + assert_equal 'a, b', v + end + e = @c.each_capitalized + assert_equal 1, e.size + e.each do |k,v| + assert_equal 'My_header', k + assert_equal 'a, b', v + end + end + + def test_key? + @c['My-Header'] = 'test' + assert_equal true, @c.key?('My-Header') + assert_equal true, @c.key?('my-header') + assert_equal false, @c.key?('Not-Found') + assert_equal false, @c.key?('not-found') + assert_equal false, @c.key?('') + assert_equal false, @c.key?('x' * 1024) + end + + def test_to_hash + end + + def test_range + try_range([1..5], '1-5') + try_invalid_range('5-1') + try_range([234..567], '234-567') + try_range([-5..-1], '-5') + try_invalid_range('-0') + try_range([1..-1], '1-') + try_range([0..0,-1..-1], '0-0,-1') + try_range([1..2, 3..4], '1-2,3-4') + try_range([1..2, 3..4], '1-2 , 3-4') + try_range([1..2, 1..4], '1-2,1-4') + + try_invalid_range('invalid') + try_invalid_range(' 12-') + try_invalid_range('12- ') + try_invalid_range('123-abc') + try_invalid_range('abc-123') + end + + def try_range(r, s) + @c['range'] = "bytes=#{s}" + assert_equal r, @c.range + end + + def try_invalid_range(s) + @c['range'] = "bytes=#{s}" + assert_raise(Net::HTTPHeaderSyntaxError, s){ @c.range } + end + + def test_range= + @c.range = 0..499 + assert_equal 'bytes=0-499', @c['range'] + @c.range = 0...500 + assert_equal 'bytes=0-499', @c['range'] + @c.range = 300 + assert_equal 'bytes=0-299', @c['range'] + @c.range = -400 + assert_equal 'bytes=-400', @c['range'] + @c.set_range 0, 500 + assert_equal 'bytes=0-499', @c['range'] + end + + def test_content_range + @c['Content-Range'] = "bytes 0-499/1000" + assert_equal 0..499, @c.content_range + @c['Content-Range'] = "bytes 1-500/1000" + assert_equal 1..500, @c.content_range + @c['Content-Range'] = "bytes 1-1/1000" + assert_equal 1..1, @c.content_range + @c['Content-Range'] = "tokens 1-1/1000" + assert_equal nil, @c.content_range + + try_invalid_content_range "invalid" + try_invalid_content_range "bytes 123-abc" + try_invalid_content_range "bytes abc-123" + end + + def test_range_length + @c['Content-Range'] = "bytes 0-499/1000" + assert_equal 500, @c.range_length + @c['Content-Range'] = "bytes 1-500/1000" + assert_equal 500, @c.range_length + @c['Content-Range'] = "bytes 1-1/1000" + assert_equal 1, @c.range_length + @c['Content-Range'] = "tokens 1-1/1000" + assert_equal nil, @c.range_length + + try_invalid_content_range "bytes 1-1/abc" + end + + def try_invalid_content_range(s) + @c['Content-Range'] = "#{s}" + assert_raise(Net::HTTPHeaderSyntaxError, s){ @c.content_range } + end + + def test_chunked? + try_chunked true, 'chunked' + try_chunked true, ' chunked ' + try_chunked true, '(OK)chunked' + + try_chunked false, 'not-chunked' + try_chunked false, 'chunked-but-not-chunked' + end + + def try_chunked(bool, str) + @c['transfer-encoding'] = str + assert_equal bool, @c.chunked? + end + + def test_content_length + @c.delete('content-length') + assert_nil @c['content-length'] + + try_content_length 500, '500' + try_content_length 10000_0000_0000, '1000000000000' + try_content_length 123, ' 123' + try_content_length 1, '1 23' + try_content_length 500, '(OK)500' + assert_raise(Net::HTTPHeaderSyntaxError, 'here is no digit, but') { + @c['content-length'] = 'no digit' + @c.content_length + } + end + + def try_content_length(len, str) + @c['content-length'] = str + assert_equal len, @c.content_length + end + + def test_content_length= + @c.content_length = 0 + assert_equal 0, @c.content_length + @c.content_length = 1 + assert_equal 1, @c.content_length + @c.content_length = 999 + assert_equal 999, @c.content_length + @c.content_length = 10000000000000 + assert_equal 10000000000000, @c.content_length + end + + def test_content_type + assert_nil @c.content_type + @c.content_type = 'text/html' + assert_equal 'text/html', @c.content_type + @c.content_type = 'application/pdf' + assert_equal 'application/pdf', @c.content_type + @c.set_content_type 'text/html', {'charset' => 'iso-2022-jp'} + assert_equal 'text/html', @c.content_type + @c.content_type = 'text' + assert_equal 'text', @c.content_type + end + + def test_main_type + assert_nil @c.main_type + @c.content_type = 'text/html' + assert_equal 'text', @c.main_type + @c.content_type = 'application/pdf' + assert_equal 'application', @c.main_type + @c.set_content_type 'text/html', {'charset' => 'iso-2022-jp'} + assert_equal 'text', @c.main_type + @c.content_type = 'text' + assert_equal 'text', @c.main_type + end + + def test_sub_type + assert_nil @c.sub_type + @c.content_type = 'text/html' + assert_equal 'html', @c.sub_type + @c.content_type = 'application/pdf' + assert_equal 'pdf', @c.sub_type + @c.set_content_type 'text/html', {'charset' => 'iso-2022-jp'} + assert_equal 'html', @c.sub_type + @c.content_type = 'text' + assert_nil @c.sub_type + end + + def test_type_params + assert_equal({}, @c.type_params) + @c.content_type = 'text/html' + assert_equal({}, @c.type_params) + @c.content_type = 'application/pdf' + assert_equal({}, @c.type_params) + @c.set_content_type 'text/html', {'charset' => 'iso-2022-jp'} + assert_equal({'charset' => 'iso-2022-jp'}, @c.type_params) + @c.content_type = 'text' + assert_equal({}, @c.type_params) + end + + def test_set_content_type + end + + def test_form_data= + @c.form_data = {"cmd"=>"search", "q"=>"ruby", "max"=>"50"} + assert_equal 'application/x-www-form-urlencoded', @c.content_type + assert_equal %w( cmd=search max=50 q=ruby ), @c.body.split('&').sort + end + + def test_set_form_data + @c.set_form_data "cmd"=>"search", "q"=>"ruby", "max"=>"50" + assert_equal 'application/x-www-form-urlencoded', @c.content_type + assert_equal %w( cmd=search max=50 q=ruby ), @c.body.split('&').sort + + @c.set_form_data "cmd"=>"search", "q"=>"ruby", "max"=>50 + assert_equal 'application/x-www-form-urlencoded', @c.content_type + assert_equal %w( cmd=search max=50 q=ruby ), @c.body.split('&').sort + + @c.set_form_data({"cmd"=>"search", "q"=>"ruby", "max"=>"50"}, ';') + assert_equal 'application/x-www-form-urlencoded', @c.content_type + assert_equal %w( cmd=search max=50 q=ruby ), @c.body.split(';').sort + end + + def test_basic_auth + end + + def test_proxy_basic_auth + end + +end diff --git a/test/net/http/test_httpresponse.rb b/test/net/http/test_httpresponse.rb new file mode 100644 index 0000000000..01281063cd --- /dev/null +++ b/test/net/http/test_httpresponse.rb @@ -0,0 +1,754 @@ +# coding: US-ASCII +# frozen_string_literal: false +require 'net/http' +require 'test/unit' +require 'stringio' + +class HTTPResponseTest < Test::Unit::TestCase + def test_singleline_header + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Content-Length: 5 +Connection: close + +hello +EOS + res = Net::HTTPResponse.read_new(io) + assert_equal('5', res['content-length']) + assert_equal('close', res['connection']) + end + + def test_multiline_header + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +X-Foo: XXX + YYY +X-Bar: + XXX +\tYYY + +hello +EOS + res = Net::HTTPResponse.read_new(io) + assert_equal('XXX YYY', res['x-foo']) + assert_equal('XXX YYY', res['x-bar']) + end + + def test_read_body + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: 5 + +hello +EOS + + res = Net::HTTPResponse.read_new(io) + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal 'hello', body + end + + def test_read_body_body_encoding_false + body = "hello\u1234" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{body.bytesize} + +#{body} +EOS + + res = Net::HTTPResponse.read_new(io) + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "hello\u1234".b, body + assert_equal Encoding::ASCII_8BIT, body.encoding + end + + def test_read_body_body_encoding_encoding + body = "hello\u1234" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{body.bytesize} + +#{body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = Encoding.find('utf-8') + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "hello\u1234", body + assert_equal Encoding::UTF_8, body.encoding + end + + def test_read_body_body_encoding_string + body = "hello\u1234" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{body.bytesize} + +#{body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = 'utf-8' + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "hello\u1234", body + assert_equal Encoding::UTF_8, body.encoding + end + + def test_read_body_body_encoding_true_without_content_type_header + body = "hello\u1234" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{body.bytesize} + +#{body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "hello\u1234".b, body + assert_equal Encoding::ASCII_8BIT, body.encoding + end + + def test_read_body_body_encoding_true_with_utf8_content_type_header + body = "hello\u1234" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{body.bytesize} +Content-Type: text/plain; charset=utf-8 + +#{body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "hello\u1234", body + assert_equal Encoding::UTF_8, body.encoding + end + + def test_read_body_body_encoding_true_with_iso_8859_1_content_type_header + body = "hello\u1234" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{body.bytesize} +Content-Type: text/plain; charset=iso-8859-1 + +#{body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "hello\u1234".force_encoding("ISO-8859-1"), body + assert_equal Encoding::ISO_8859_1, body.encoding + end + + def test_read_body_body_encoding_true_with_utf8_meta_charset + res_body = "<html><meta charset=\"utf-8\">hello\u1234</html>" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{res_body.bytesize} +Content-Type: text/html + +#{res_body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal res_body, body + assert_equal Encoding::UTF_8, body.encoding + end + + def test_read_body_body_encoding_true_with_iso8859_1_meta_charset + res_body = "<html><meta charset=\"iso-8859-1\">hello\u1234</html>" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{res_body.bytesize} +Content-Type: text/html + +#{res_body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal res_body.force_encoding("ISO-8859-1"), body + assert_equal Encoding::ISO_8859_1, body.encoding + end + + def test_read_body_body_encoding_true_with_utf8_meta_content_charset + res_body = "<meta http-equiv='content-type' content='text/html; charset=UTF-8'>hello\u1234</html>" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{res_body.bytesize} +Content-Type: text/html + +#{res_body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal res_body, body + assert_equal Encoding::UTF_8, body.encoding + end + + def test_read_body_body_encoding_true_with_iso8859_1_meta_content_charset + res_body = "<meta http-equiv='content-type' content='text/html; charset=ISO-8859-1'>hello\u1234</html>" + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: #{res_body.bytesize} +Content-Type: text/html + +#{res_body} +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal res_body.force_encoding("ISO-8859-1"), body + assert_equal Encoding::ISO_8859_1, body.encoding + end + + def test_read_body_block + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: 5 + +hello +EOS + + res = Net::HTTPResponse.read_new(io) + + body = '' + + res.reading_body io, true do + res.read_body do |chunk| + body << chunk + end + end + + assert_equal 'hello', body + end + + def test_read_body_block_mod + # http://ci.rvm.jp/results/trunk-rjit-wait@silicon-docker/3019353 + if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? + omit 'too unstable with --jit-wait, and extending read_timeout did not help it' + end + IO.pipe do |r, w| + buf = 'x' * 1024 + buf.freeze + n = 1024 + len = n * buf.size + th = Thread.new do + w.write("HTTP/1.1 200 OK\r\nContent-Length: #{len}\r\n\r\n") + n.times { w.write(buf) } + :ok + end + io = Net::BufferedIO.new(r) + res = Net::HTTPResponse.read_new(io) + nr = 0 + res.reading_body io, true do + # should be allowed to modify the chunk given to them: + res.read_body do |chunk| + nr += chunk.size + chunk.clear + end + end + assert_equal len, nr + assert_equal :ok, th.value + end + end + + def test_read_body_content_encoding_deflate + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Encoding: deflate +Content-Length: 13 + +x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15 +EOS + + res = Net::HTTPResponse.read_new(io) + res.decode_content = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + if Net::HTTP::HAVE_ZLIB + assert_equal nil, res['content-encoding'] + assert_equal '5', res['content-length'] + assert_equal 'hello', body + else + assert_equal 'deflate', res['content-encoding'] + assert_equal '13', res['content-length'] + assert_equal "x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15", body + end + end + + def test_read_body_content_encoding_deflate_uppercase + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Encoding: DEFLATE +Content-Length: 13 + +x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15 +EOS + + res = Net::HTTPResponse.read_new(io) + res.decode_content = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + if Net::HTTP::HAVE_ZLIB + assert_equal nil, res['content-encoding'] + assert_equal '5', res['content-length'] + assert_equal 'hello', body + else + assert_equal 'DEFLATE', res['content-encoding'] + assert_equal '13', res['content-length'] + assert_equal "x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15", body + end + end + + def test_read_body_content_encoding_deflate_chunked + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Encoding: deflate +Transfer-Encoding: chunked + +6 +x\x9C\xCBH\xCD\xC9 +7 +\xC9\a\x00\x06,\x02\x15 +0 + +EOS + + res = Net::HTTPResponse.read_new(io) + res.decode_content = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + if Net::HTTP::HAVE_ZLIB + assert_equal nil, res['content-encoding'] + assert_equal nil, res['content-length'] + assert_equal 'hello', body + else + assert_equal 'deflate', res['content-encoding'] + assert_equal nil, res['content-length'] + assert_equal "x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15", body + end + end + + def test_read_body_content_encoding_deflate_disabled + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Encoding: deflate +Content-Length: 13 + +x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15 +EOS + + res = Net::HTTPResponse.read_new(io) + res.decode_content = false # user set accept-encoding in request + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal 'deflate', res['content-encoding'], 'Bug #7831' + assert_equal '13', res['content-length'] + assert_equal "x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15", body, 'Bug #7381' + end + + def test_read_body_content_encoding_deflate_no_length + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Encoding: deflate + +x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15 +EOS + + res = Net::HTTPResponse.read_new(io) + res.decode_content = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + if Net::HTTP::HAVE_ZLIB + assert_equal nil, res['content-encoding'] + assert_equal nil, res['content-length'] + assert_equal 'hello', body + else + assert_equal 'deflate', res['content-encoding'] + assert_equal nil, res['content-length'] + assert_equal "x\x9C\xCBH\xCD\xC9\xC9\a\x00\x06,\x02\x15\r\n", body + end + end + + def test_read_body_content_encoding_deflate_content_range + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Accept-Ranges: bytes +Connection: close +Content-Encoding: gzip +Content-Length: 10 +Content-Range: bytes 0-9/55 + +\x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03 +EOS + + res = Net::HTTPResponse.read_new(io) + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + assert_equal "\x1F\x8B\b\x00\x00\x00\x00\x00\x00\x03", body + end + + def test_read_body_content_encoding_deflate_empty_body + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Encoding: deflate +Content-Length: 0 + +EOS + + res = Net::HTTPResponse.read_new(io) + res.decode_content = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + if Net::HTTP::HAVE_ZLIB + assert_equal nil, res['content-encoding'] + assert_equal '0', res['content-length'] + assert_equal '', body + else + assert_equal 'deflate', res['content-encoding'] + assert_equal '0', res['content-length'] + assert_equal '', body + end + end + + def test_read_body_content_encoding_deflate_empty_body_no_length + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Encoding: deflate + +EOS + + res = Net::HTTPResponse.read_new(io) + res.decode_content = true + + body = nil + + res.reading_body io, true do + body = res.read_body + end + + if Net::HTTP::HAVE_ZLIB + assert_equal nil, res['content-encoding'] + assert_equal nil, res['content-length'] + assert_equal '', body + else + assert_equal 'deflate', res['content-encoding'] + assert_equal nil, res['content-length'] + assert_equal '', body + end + end + + def test_read_body_string + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: 5 + +hello +EOS + + res = Net::HTTPResponse.read_new(io) + + body = '' + + res.reading_body io, true do + res.read_body body + end + + assert_equal 'hello', body + end + + def test_read_body_receiving_no_body + io = dummy_io(<<EOS) +HTTP/1.1 204 OK +Connection: close + +EOS + + res = Net::HTTPResponse.read_new(io) + res.body_encoding = 'utf-8' + + body = 'something to override' + + res.reading_body io, true do + body = res.read_body + end + + assert_equal nil, body + assert_equal nil, res.body + end + + def test_read_body_outside_of_reading_body + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Connection: close +Content-Length: 0 + +EOS + + res = Net::HTTPResponse.read_new(io) + + assert_raise IOError do + res.read_body + end + end + + def test_uri_equals + uri = URI 'http://example' + + response = Net::HTTPResponse.new '1.1', 200, 'OK' + + response.uri = nil + + assert_nil response.uri + + response.uri = uri + + assert_equal uri, response.uri + assert_not_same uri, response.uri + end + + def test_ensure_zero_space_does_not_regress + io = dummy_io(<<EOS) +HTTP/1.1 200OK +Content-Length: 5 +Connection: close + +hello +EOS + + assert_raise Net::HTTPBadResponse do + Net::HTTPResponse.read_new(io) + end + end + + def test_allow_trailing_space_after_status + io = dummy_io(<<EOS) +HTTP/1.1 200\s +Content-Length: 5 +Connection: close + +hello +EOS + + res = Net::HTTPResponse.read_new(io) + assert_equal('1.1', res.http_version) + assert_equal('200', res.code) + assert_equal('', res.message) + end + + def test_normal_status_line + io = dummy_io(<<EOS) +HTTP/1.1 200 OK +Content-Length: 5 +Connection: close + +hello +EOS + + res = Net::HTTPResponse.read_new(io) + assert_equal('1.1', res.http_version) + assert_equal('200', res.code) + assert_equal('OK', res.message) + end + + def test_allow_empty_reason_code + io = dummy_io(<<EOS) +HTTP/1.1 200 +Content-Length: 5 +Connection: close + +hello +EOS + + res = Net::HTTPResponse.read_new(io) + assert_equal('1.1', res.http_version) + assert_equal('200', res.code) + assert_equal(nil, res.message) + end + + def test_raises_exception_with_missing_reason + io = dummy_io(<<EOS) +HTTP/1.1 404 +Content-Length: 5 +Connection: close + +hello +EOS + + res = Net::HTTPResponse.read_new(io) + assert_equal(nil, res.message) + assert_raise Net::HTTPClientException do + res.error! + end + end + + def test_read_code_type + res = Net::HTTPUnknownResponse.new('1.0', '???', 'test response') + assert_equal Net::HTTPUnknownResponse, res.code_type + + res = Net::HTTPInformation.new('1.0', '1xx', 'test response') + assert_equal Net::HTTPInformation, res.code_type + + res = Net::HTTPSuccess.new('1.0', '2xx', 'test response') + assert_equal Net::HTTPSuccess, res.code_type + + res = Net::HTTPRedirection.new('1.0', '3xx', 'test response') + assert_equal Net::HTTPRedirection, res.code_type + + res = Net::HTTPClientError.new('1.0', '4xx', 'test response') + assert_equal Net::HTTPClientError, res.code_type + + res = Net::HTTPServerError.new('1.0', '5xx', 'test response') + assert_equal Net::HTTPServerError, res.code_type + end + + def test_inspect_response + res = Net::HTTPUnknownResponse.new('1.0', '???', 'test response') + assert_equal '#<Net::HTTPUnknownResponse ??? test response readbody=false>', res.inspect + + res = Net::HTTPUnknownResponse.new('1.0', '???', 'test response') + socket = Net::BufferedIO.new(StringIO.new('test body')) + res.reading_body(socket, true) {} + assert_equal '#<Net::HTTPUnknownResponse ??? test response readbody=true>', res.inspect + end + +private + + def dummy_io(str) + str = str.gsub(/\n/, "\r\n") + + Net::BufferedIO.new(StringIO.new(str)) + end +end diff --git a/test/net/http/test_httpresponses.rb b/test/net/http/test_httpresponses.rb new file mode 100644 index 0000000000..b389e163cf --- /dev/null +++ b/test/net/http/test_httpresponses.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: false +require 'net/http' +require 'test/unit' + +class HTTPResponsesTest < Test::Unit::TestCase + def test_status_code_classes + Net::HTTPResponse::CODE_TO_OBJ.each_pair { |code, klass| + case code + when /\A1\d\d\z/ + group = Net::HTTPInformation + when /\A2\d\d\z/ + group = Net::HTTPSuccess + when /\A3\d\d\z/ + group = Net::HTTPRedirection + when /\A4\d\d\z/ + group = Net::HTTPClientError + when /\A5\d\d\z/ + group = Net::HTTPServerError + else + flunk "Unknown HTTP status code: #{code} => #{klass.name}" + end + assert(klass < group, "#{klass.name} (#{code}) must inherit from #{group.name}") + } + end +end diff --git a/test/net/http/test_https.rb b/test/net/http/test_https.rb new file mode 100644 index 0000000000..f5b21b901f --- /dev/null +++ b/test/net/http/test_https.rb @@ -0,0 +1,325 @@ +# frozen_string_literal: false +require "test/unit" +require_relative "utils" +begin + require 'net/https' +rescue LoadError + # should skip this test +end + +return unless defined?(OpenSSL::SSL) + +class TestNetHTTPS < Test::Unit::TestCase + include TestNetHTTPUtils + + def self.read_fixture(key) + File.read(File.expand_path("../fixtures/#{key}", __dir__)) + end + + HOST = 'localhost' + HOST_IP = '127.0.0.1' + CA_CERT = OpenSSL::X509::Certificate.new(read_fixture("cacert.pem")) + SERVER_KEY = OpenSSL::PKey.read(read_fixture("server.key")) + SERVER_CERT = OpenSSL::X509::Certificate.new(read_fixture("server.crt")) + TEST_STORE = OpenSSL::X509::Store.new.tap {|s| s.add_cert(CA_CERT) } + + CONFIG = { + 'host' => HOST, + 'proxy_host' => nil, + 'proxy_port' => nil, + 'ssl_enable' => true, + 'ssl_certificate' => SERVER_CERT, + 'ssl_private_key' => SERVER_KEY, + } + + def test_get + http = Net::HTTP.new(HOST, config("port")) + http.use_ssl = true + http.cert_store = TEST_STORE + http.request_get("/") {|res| + assert_equal($test_net_http_data, res.body) + assert_equal(SERVER_CERT.to_der, http.peer_cert.to_der) + } + end + + def test_get_SNI + http = Net::HTTP.new(HOST, config("port")) + http.ipaddr = config('host') + http.use_ssl = true + http.cert_store = TEST_STORE + http.request_get("/") {|res| + assert_equal($test_net_http_data, res.body) + assert_equal(SERVER_CERT.to_der, http.peer_cert.to_der) + } + end + + def test_get_SNI_proxy + TCPServer.open(HOST_IP, 0) {|serv| + _, port, _, _ = serv.addr + client_thread = Thread.new { + proxy = Net::HTTP.Proxy(HOST_IP, port, 'user', 'password') + http = proxy.new("foo.example.org", 8000) + http.ipaddr = "192.0.2.1" + http.use_ssl = true + http.cert_store = TEST_STORE + begin + http.start + rescue EOFError + end + } + server_thread = Thread.new { + sock = serv.accept + begin + proxy_request = sock.gets("\r\n\r\n") + assert_equal( + "CONNECT 192.0.2.1:8000 HTTP/1.1\r\n" + + "Host: foo.example.org:8000\r\n" + + "Proxy-Authorization: Basic dXNlcjpwYXNzd29yZA==\r\n" + + "\r\n", + proxy_request, + "[ruby-dev:25673]") + ensure + sock.close + end + } + assert_join_threads([client_thread, server_thread]) + } + + end + + def test_get_SNI_failure + TestNetHTTPUtils.clean_http_proxy_env do + http = Net::HTTP.new("invalidservername", config("port")) + http.ipaddr = config('host') + http.use_ssl = true + http.cert_store = TEST_STORE + @log_tester = lambda {|_| } + assert_raise(OpenSSL::SSL::SSLError){ http.start } + end + end + + def test_post + http = Net::HTTP.new(HOST, config("port")) + http.use_ssl = true + http.cert_store = TEST_STORE + data = config('ssl_private_key').to_der + http.request_post("/", data, {'content-type' => 'application/x-www-form-urlencoded'}) {|res| + assert_equal(data, res.body) + } + end + + def test_session_reuse + http = Net::HTTP.new(HOST, config("port")) + http.use_ssl = true + http.cert_store = TEST_STORE + + if OpenSSL::OPENSSL_LIBRARY_VERSION =~ /LibreSSL (\d+\.\d+)/ && $1.to_f > 3.19 + # LibreSSL 3.2 defaults to TLSv1.3 in server and client, which doesn't currently + # support session resuse. Limiting the version to the TLSv1.2 stack allows + # this test to continue to work on LibreSSL 3.2+. LibreSSL may eventually + # support session reuse, but there are no current plans to do so. + http.ssl_version = :TLSv1_2 + end + + http.start + session_reused = http.instance_variable_get(:@socket).io.session_reused? + assert_false session_reused unless session_reused.nil? # can not detect re-use under JRuby + http.get("/") + http.finish + + http.start + session_reused = http.instance_variable_get(:@socket).io.session_reused? + assert_true session_reused unless session_reused.nil? # can not detect re-use under JRuby + assert_equal $test_net_http_data, http.get("/").body + http.finish + end + + def test_session_reuse_but_expire + http = Net::HTTP.new(HOST, config("port")) + http.use_ssl = true + http.cert_store = TEST_STORE + + http.ssl_timeout = 1 + http.start + http.get("/") + http.finish + sleep 1.25 + http.start + http.get("/") + + socket = http.instance_variable_get(:@socket).io + assert_equal false, socket.session_reused?, "NOTE: OpenSSL library version is #{OpenSSL::OPENSSL_LIBRARY_VERSION}" + + http.finish + end + + if ENV["RUBY_OPENSSL_TEST_ALL"] + def test_verify + http = Net::HTTP.new("ssl.netlab.jp", 443) + http.use_ssl = true + assert( + (http.request_head("/"){|res| } rescue false), + "The system may not have default CA certificate store." + ) + end + end + + def test_verify_none + http = Net::HTTP.new(HOST, config("port")) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + http.request_get("/") {|res| + assert_equal($test_net_http_data, res.body) + } + end + + def test_skip_hostname_verification + TestNetHTTPUtils.clean_http_proxy_env do + http = Net::HTTP.new('invalidservername', config('port')) + http.ipaddr = config('host') + http.use_ssl = true + http.cert_store = TEST_STORE + http.verify_hostname = false + assert_nothing_raised { http.start } + ensure + http.finish if http&.started? + end + end + + def test_fail_if_verify_hostname_is_true + TestNetHTTPUtils.clean_http_proxy_env do + http = Net::HTTP.new('invalidservername', config('port')) + http.ipaddr = config('host') + http.use_ssl = true + http.cert_store = TEST_STORE + http.verify_hostname = true + @log_tester = lambda { |_| } + assert_raise(OpenSSL::SSL::SSLError) { http.start } + end + end + + def test_certificate_verify_failure + http = Net::HTTP.new(HOST, config("port")) + http.use_ssl = true + ex = assert_raise(OpenSSL::SSL::SSLError){ + http.request_get("/") {|res| } + } + assert_match(/certificate verify failed/, ex.message) + end + + def test_verify_callback + http = Net::HTTP.new(HOST, config("port")) + http.use_ssl = true + http.cert_store = TEST_STORE + certs = [] + http.verify_callback = Proc.new {|preverify_ok, store_ctx| + certs << store_ctx.current_cert + preverify_ok + } + http.request_get("/") {|res| + assert_equal($test_net_http_data, res.body) + } + assert_equal(SERVER_CERT.to_der, certs.last.to_der) + end + + def test_timeout_during_SSL_handshake + bug4246 = "expected the SSL connection to have timed out but have not. [ruby-core:34203]" + + # listen for connections... but deliberately do not complete SSL handshake + TCPServer.open(HOST, 0) {|server| + port = server.addr[1] + + conn = Net::HTTP.new(HOST, port) + conn.use_ssl = true + conn.read_timeout = 0.01 + conn.open_timeout = 0.01 + + th = Thread.new do + assert_raise(Net::OpenTimeout) { + conn.get('/') + } + end + assert th.join(10), bug4246 + } + end + + def test_min_version + http = Net::HTTP.new(HOST, config("port")) + http.use_ssl = true + http.min_version = :TLS1 + http.cert_store = TEST_STORE + http.request_get("/") {|res| + assert_equal($test_net_http_data, res.body) + } + end + + def test_max_version + http = Net::HTTP.new(HOST, config("port")) + http.use_ssl = true + http.max_version = :SSL2 + http.cert_store = TEST_STORE + @log_tester = lambda {|_| } + ex = assert_raise(OpenSSL::SSL::SSLError){ + http.request_get("/") {|res| } + } + re_msg = /\ASSL_connect returned=1 errno=0 |SSL_CTX_set_max_proto_version|No appropriate protocol/ + assert_match(re_msg, ex.message) + end + + def test_ractor + assert_ractor(<<~RUBY, require: 'net/https') + expected = #{$test_net_http_data.dump}.b + ret = Ractor.new { + host = #{HOST.dump} + port = #{config('port')} + ca_cert_pem = #{CA_CERT.to_pem.dump} + cert_store = OpenSSL::X509::Store.new.tap { |s| + s.add_cert(OpenSSL::X509::Certificate.new(ca_cert_pem)) + } + Net::HTTP.start(host, port, use_ssl: true, cert_store: cert_store) { |http| + res = http.get('/') + res.body + } + }.value + assert_equal expected, ret + RUBY + end if defined?(Ractor) && Ractor.method_defined?(:value) +end + +class TestNetHTTPSIdentityVerifyFailure < Test::Unit::TestCase + include TestNetHTTPUtils + + def self.read_fixture(key) + File.read(File.expand_path("../fixtures/#{key}", __dir__)) + end + + HOST = 'localhost' + HOST_IP = '127.0.0.1' + CA_CERT = OpenSSL::X509::Certificate.new(read_fixture("cacert.pem")) + SERVER_KEY = OpenSSL::PKey.read(read_fixture("server.key")) + SERVER_CERT = OpenSSL::X509::Certificate.new(read_fixture("server.crt")) + TEST_STORE = OpenSSL::X509::Store.new.tap {|s| s.add_cert(CA_CERT) } + + CONFIG = { + 'host' => HOST_IP, + 'proxy_host' => nil, + 'proxy_port' => nil, + 'ssl_enable' => true, + 'ssl_certificate' => SERVER_CERT, + 'ssl_private_key' => SERVER_KEY, + } + + def test_identity_verify_failure + # the certificate's subject has CN=localhost + http = Net::HTTP.new(HOST_IP, config("port")) + http.use_ssl = true + http.cert_store = TEST_STORE + @log_tester = lambda {|_| } + ex = assert_raise(OpenSSL::SSL::SSLError){ + http.request_get("/") {|res| } + sleep 0.5 + } + re_msg = /certificate verify failed|hostname \"#{HOST_IP}\" does not match/ + assert_match(re_msg, ex.message) + end +end diff --git a/test/net/http/test_https_proxy.rb b/test/net/http/test_https_proxy.rb new file mode 100644 index 0000000000..237c16e64d --- /dev/null +++ b/test/net/http/test_https_proxy.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: false +begin + require 'net/https' +rescue LoadError +end +require 'test/unit' + +return unless defined?(OpenSSL::SSL) + +class HTTPSProxyTest < Test::Unit::TestCase + def test_https_proxy_authentication + TCPServer.open("127.0.0.1", 0) {|serv| + _, port, _, _ = serv.addr + client_thread = Thread.new { + proxy = Net::HTTP.Proxy("127.0.0.1", port, 'user', 'password') + http = proxy.new("foo.example.org", 8000) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + begin + http.start + rescue EOFError + end + } + server_thread = Thread.new { + sock = serv.accept + begin + proxy_request = sock.gets("\r\n\r\n") + assert_equal( + "CONNECT foo.example.org:8000 HTTP/1.1\r\n" + + "Host: foo.example.org:8000\r\n" + + "Proxy-Authorization: Basic dXNlcjpwYXNzd29yZA==\r\n" + + "\r\n", + proxy_request, + "[ruby-dev:25673]") + ensure + sock.close + end + } + assert_join_threads([client_thread, server_thread]) + } + end + + + def read_fixture(key) + File.read(File.expand_path("../fixtures/#{key}", __dir__)) + end + + def test_https_proxy_ssl_connection + TCPServer.open("127.0.0.1", 0) {|tcpserver| + ctx = OpenSSL::SSL::SSLContext.new + ctx.key = OpenSSL::PKey.read(read_fixture("server.key")) + ctx.cert = OpenSSL::X509::Certificate.new(read_fixture("server.crt")) + serv = OpenSSL::SSL::SSLServer.new(tcpserver, ctx) + + _, port, _, _ = serv.addr + client_thread = Thread.new { + proxy = Net::HTTP.Proxy("127.0.0.1", port, 'user', 'password', true) + http = proxy.new("foo.example.org", 8000) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + begin + http.start + rescue EOFError + end + } + server_thread = Thread.new { + sock = serv.accept + begin + proxy_request = sock.gets("\r\n\r\n") + assert_equal( + "CONNECT foo.example.org:8000 HTTP/1.1\r\n" + + "Host: foo.example.org:8000\r\n" + + "Proxy-Authorization: Basic dXNlcjpwYXNzd29yZA==\r\n" + + "\r\n", + proxy_request, + "[ruby-core:96672]") + ensure + sock.close + end + } + assert_join_threads([client_thread, server_thread]) + } + end +end diff --git a/test/net/http/utils.rb b/test/net/http/utils.rb new file mode 100644 index 0000000000..0b9e440e7c --- /dev/null +++ b/test/net/http/utils.rb @@ -0,0 +1,359 @@ +# frozen_string_literal: false +require 'socket' + +module TestNetHTTPUtils + + class Forbidden < StandardError; end + + class HTTPServer + def initialize(config, &block) + @config = config + @server = TCPServer.new(@config['host'], 0) + @port = @server.addr[1] + @procs = {} + + if @config['ssl_enable'] + require 'openssl' + context = OpenSSL::SSL::SSLContext.new + context.cert = @config['ssl_certificate'] + context.key = @config['ssl_private_key'] + @ssl_server = OpenSSL::SSL::SSLServer.new(@server, context) + end + + @block = block + end + + def start + @thread = Thread.new do + loop do + socket = (@ssl_server || @server).accept + run(socket) + rescue + ensure + socket&.close + end + ensure + (@ssl_server || @server).close + end + end + + def run(socket) + handle_request(socket) + end + + def shutdown + @thread&.kill + @thread&.join + end + + def mount(path, proc) + @procs[path] = proc + end + + def mount_proc(path, &block) + mount(path, block.to_proc) + end + + def handle_request(socket) + request_line = socket.gets + return if request_line.nil? || request_line.strip.empty? + + method, path, _version = request_line.split + headers = {} + while (line = socket.gets) + break if line.strip.empty? + key, value = line.split(': ', 2) + headers[key] = value.strip + end + + if headers['Expect'] == '100-continue' + socket.write "HTTP/1.1 100 Continue\r\n\r\n" + end + + # Set default Content-Type if not provided + if !headers['Content-Type'] && (method == 'POST' || method == 'PUT' || method == 'PATCH') + headers['Content-Type'] = 'application/octet-stream' + end + + req = Request.new(method, path, headers, socket) + if @procs.key?(req.path) || @procs.key?("#{req.path}/") + proc = @procs[req.path] || @procs["#{req.path}/"] + res = Response.new(socket) + begin + proc.call(req, res) + rescue Forbidden + res.status = 403 + end + res.finish + else + @block.call(method, path, headers, socket) + end + end + + def port + @port + end + + class Request + attr_reader :method, :path, :headers, :query, :body + def initialize(method, path, headers, socket) + @method = method + @path, @query = parse_path_and_query(path) + @headers = headers + @socket = socket + if method == 'POST' && (@path == '/continue' || @headers['Content-Type'].include?('multipart/form-data')) + if @headers['Transfer-Encoding'] == 'chunked' + @body = read_chunked_body + else + @body = read_body + end + @query = @body.split('&').each_with_object({}) do |pair, hash| + key, value = pair.split('=') + hash[key] = value + end if @body && @body.include?('=') + end + end + + def [](key) + @headers[key.downcase] + end + + def []=(key, value) + @headers[key.downcase] = value + end + + def continue + @socket.write "HTTP\/1.1 100 continue\r\n\r\n" + end + + def remote_ip + @socket.peeraddr[3] + end + + def peeraddr + @socket.peeraddr + end + + private + + def parse_path_and_query(path) + path, query_string = path.split('?', 2) + query = {} + if query_string + query_string.split('&').each do |pair| + key, value = pair.split('=', 2) + query[key] = value + end + end + [path, query] + end + + def read_body + content_length = @headers['Content-Length']&.to_i + return unless content_length && content_length > 0 + @socket.read(content_length) + end + + def read_chunked_body + body = "" + while (chunk_size = @socket.gets.strip.to_i(16)) > 0 + body << @socket.read(chunk_size) + @socket.read(2) # read \r\n after each chunk + end + body + end + end + + class Response + attr_accessor :body, :headers, :status, :chunked, :cookies + def initialize(client) + @client = client + @body = "" + @headers = {} + @status = 200 + @chunked = false + @cookies = [] + end + + def [](key) + @headers[key.downcase] + end + + def []=(key, value) + @headers[key.downcase] = value + end + + def write_chunk(chunk) + return unless @chunked + @client.write("#{chunk.bytesize.to_s(16)}\r\n") + @client.write("#{chunk}\r\n") + end + + def finish + @client.write build_response_headers + if @chunked + write_chunk(@body) + @client.write "0\r\n\r\n" + else + @client.write @body + end + end + + private + + def build_response_headers + response = "HTTP/1.1 #{@status} #{status_message(@status)}\r\n" + if @chunked + @headers['Transfer-Encoding'] = 'chunked' + else + @headers['Content-Length'] = @body.bytesize.to_s + end + @headers.each do |key, value| + response << "#{key}: #{value}\r\n" + end + @cookies.each do |cookie| + response << "Set-Cookie: #{cookie}\r\n" + end + response << "\r\n" + response + end + + def status_message(code) + case code + when 200 then 'OK' + when 301 then 'Moved Permanently' + when 403 then 'Forbidden' + else 'Unknown' + end + end + end + end + + def start(&block) + new().start(&block) + end + + def new + klass = Net::HTTP::Proxy(config('proxy_host'), config('proxy_port')) + http = klass.new(config('host'), config('port')) + http.set_debug_output logfile + http + end + + def config(key) + @config ||= self.class::CONFIG + @config[key] + end + + def logfile + $stderr if $DEBUG + end + + def setup + spawn_server + end + + def teardown + sleep 0.5 if @config['ssl_enable'] + if @server + @server.shutdown + end + @log_tester.call(@log) if @log_tester + Net::HTTP.version_1_2 + end + + def spawn_server + @log = [] + @log_tester = lambda {|log| assert_equal([], log) } + @config = self.class::CONFIG + @server = HTTPServer.new(@config) do |method, path, headers, socket| + @log << "DEBUG accept: #{@config['host']}:#{socket.addr[1]}" if @logger_level == :debug + case method + when 'HEAD' + handle_head(path, headers, socket) + when 'GET' + handle_get(path, headers, socket) + when 'POST' + handle_post(path, headers, socket) + when 'PATCH' + handle_patch(path, headers, socket) + else + socket.print "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\n\r\n" + end + end + @server.start + @config['port'] = @server.port + end + + def handle_head(path, headers, socket) + if headers['Accept'] != '*/*' + content_type = headers['Accept'] + else + content_type = $test_net_http_data_type + end + response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{$test_net_http_data.bytesize}" + socket.print(response) + end + + def handle_get(path, headers, socket) + if headers['Accept'] != '*/*' + content_type = headers['Accept'] + else + content_type = $test_net_http_data_type + end + response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{$test_net_http_data.bytesize}\r\n\r\n#{$test_net_http_data}" + socket.print(response) + end + + def handle_post(path, headers, socket) + body = socket.read(headers['Content-Length'].to_i) + scheme = headers['X-Request-Scheme'] || 'http' + host = @config['host'] + port = socket.addr[1] + content_type = headers['Content-Type'] || 'application/octet-stream' + charset = parse_content_type(content_type)[1] + path = "#{scheme}://#{host}:#{port}#{path}" + path = path.encode(charset) if charset + response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{body.bytesize}\r\nX-request-uri: #{path}\r\n\r\n#{body}" + socket.print(response) + end + + def handle_patch(path, headers, socket) + body = socket.read(headers['Content-Length'].to_i) + content_type = headers['Content-Type'] || 'application/octet-stream' + response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}" + socket.print(response) + end + + def parse_content_type(content_type) + return [nil, nil] unless content_type + type, *params = content_type.split(';').map(&:strip) + charset = params.find { |param| param.start_with?('charset=') } + charset = charset.split('=', 2).last if charset + [type, charset] + end + + $test_net_http = nil + $test_net_http_data = (0...256).to_a.map { |i| i.chr }.join('') * 64 + $test_net_http_data.force_encoding("ASCII-8BIT") + $test_net_http_data_type = 'application/octet-stream' + + def self.clean_http_proxy_env + orig = { + 'http_proxy' => ENV['http_proxy'], + 'http_proxy_user' => ENV['http_proxy_user'], + 'http_proxy_pass' => ENV['http_proxy_pass'], + 'no_proxy' => ENV['no_proxy'], + } + + orig.each_key do |key| + ENV.delete key + end + + yield + ensure + orig.each do |key, value| + ENV[key] = value + end + end +end diff --git a/test/net/protocol/test_protocol.rb b/test/net/protocol/test_protocol.rb new file mode 100644 index 0000000000..2f42fa3236 --- /dev/null +++ b/test/net/protocol/test_protocol.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true +require "test/unit" +require "net/protocol" +require "stringio" + +class TestProtocol < Test::Unit::TestCase + def test_should_properly_dot_stuff_period_with_no_endline + bug9627 = '[ruby-core:61441] [Bug #9627]' + sio = StringIO.new("".dup) + imio = Net::InternetMessageIO.new(sio) + email = "To: bob@aol.com\nlook, a period with no endline\n." + imio.write_message(email) + assert_equal("To: bob@aol.com\r\nlook, a period with no endline\r\n..\r\n.\r\n", sio.string, bug9627) + end + + def test_each_crlf_line + assert_output('', '') do + sio = StringIO.new("".dup) + imio = Net::InternetMessageIO.new(sio) + assert_equal(23, imio.write_message("\u3042\r\u3044\n\u3046\r\n\u3048")) + assert_equal("\u3042\r\n\u3044\r\n\u3046\r\n\u3048\r\n.\r\n", sio.string) + + sio = StringIO.new("".dup) + imio = Net::InternetMessageIO.new(sio) + assert_equal(8, imio.write_message("\u3042\r")) + assert_equal("\u3042\r\n.\r\n", sio.string) + end + end + + def create_mockio(capacity: 100, max: nil) + mockio = Object.new + mockio.instance_variable_set(:@str, +'') + mockio.instance_variable_set(:@capacity, capacity) + mockio.instance_variable_set(:@max, max) + def mockio.string; @str; end + def mockio.to_io; self; end + def mockio.wait_writable(sec); sleep sec; false; end + def mockio.write_nonblock(*strs, exception: true) + if @capacity <= @str.bytesize + if exception + raise Net::WaitWritable + else + return :wait_writable + end + end + len = 0 + max = @max ? [@capacity, @str.bytesize + @max].min : @capacity + strs.each do |str| + len1 = @str.bytesize + break if max <= len1 + @str << str.byteslice(0, max - @str.bytesize) + len2 = @str.bytesize + len += len2 - len1 + end + len + end + mockio + end + + def test_readuntil + assert_output("", "") do + sio = StringIO.new("12345".dup) + io = Net::BufferedIO.new(sio) + assert_equal "12345", io.readuntil("5") + end + end + + def test_write0_multibyte + mockio = create_mockio(max: 1) + io = Net::BufferedIO.new(mockio) + assert_equal(3, io.write("\u3042")) + end + + def test_write0_timeout + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + assert_raise(Net::WriteTimeout){ io.write("a"*1000) } + end + + def test_write0_success + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + len = io.write("a"*10) + assert_equal "a"*10, mockio.string + assert_equal 10, len + end + + def test_write0_success2 + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + len = io.write("a"*100) + assert_equal "a"*100, mockio.string + assert_equal 100, len + end + + def test_write0_success_multi1 + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + len = io.write("a"*50, "a"*49) + assert_equal "a"*99, mockio.string + assert_equal 99, len + end + + def test_write0_success_multi2 + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + len = io.write("a"*50, "a"*50) + assert_equal "a"*100, mockio.string + assert_equal 100, len + end + + def test_write0_timeout_multi1 + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + assert_raise(Net::WriteTimeout){ io.write("a"*50,"a"*51) } + end + + def test_write0_timeout_multi2 + mockio = create_mockio + io = Net::BufferedIO.new(mockio) + io.write_timeout = 0.1 + assert_raise(Net::WriteTimeout){ io.write("a"*50,"a"*50,"a") } + end + + class FakeReadPartialIO + def initialize(chunks) + @chunks = chunks.map(&:dup) + end + + def read_nonblock(size, buf = nil, exception: false) + if buf + buf.replace(@chunks.shift) + buf + else + @chunks.shift + end + end + end + + def test_shareable_buffer_leak # https://github.com/ruby/net-protocol/pull/19 + expected_chunks = [ + "aaaaa", + "bbbbb", + ] + fake_io = FakeReadPartialIO.new(expected_chunks) + io = Net::BufferedIO.new(fake_io) + actual_chunks = [] + reader = Net::ReadAdapter.new(-> (chunk) { actual_chunks << chunk }) + io.read(5, reader) + io.read(5, reader) + assert_equal expected_chunks, actual_chunks + end +end |
