summaryrefslogtreecommitdiff
path: root/test/net
diff options
context:
space:
mode:
Diffstat (limited to 'test/net')
-rw-r--r--test/net/fixtures/Makefile15
-rw-r--r--test/net/fixtures/cacert.pem24
-rw-r--r--test/net/fixtures/dhparams.pem29
-rw-r--r--test/net/fixtures/server.crt21
-rw-r--r--test/net/fixtures/server.key27
-rw-r--r--test/net/http/test_buffered_io.rb18
-rw-r--r--test/net/http/test_http.rb1273
-rw-r--r--test/net/http/test_http_request.rb125
-rw-r--r--test/net/http/test_httpheader.rb164
-rw-r--r--test/net/http/test_httpresponse.rb754
-rw-r--r--test/net/http/test_httpresponses.rb25
-rw-r--r--test/net/http/test_https.rb325
-rw-r--r--test/net/http/test_https_proxy.rb83
-rw-r--r--test/net/http/utils.rb359
-rw-r--r--test/net/imap/test_imap.rb14
-rw-r--r--test/net/protocol/test_protocol.rb159
16 files changed, 3255 insertions, 160 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
index eeb046b54b..4e7fa22756 100644
--- a/test/net/http/test_http.rb
+++ b/test/net/http/test_http.rb
@@ -1,16 +1,328 @@
-# $Id$
-
+# coding: US-ASCII
+# frozen_string_literal: false
require 'test/unit'
require 'net/http'
-require 'webrick'
-require 'webrick/httpservlet/abstract'
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
@@ -18,7 +330,9 @@ module TestNetHTTP_version_1_1_methods
res = http.head('/')
assert_kind_of Net::HTTPResponse, res
assert_equal $test_net_http_data_type, res['Content-Type']
- assert_equal $test_net_http_data.size, res['Content-Length'].to_i
+ unless self.is_a?(TestNetHTTP_v1_2_chunked)
+ assert_equal $test_net_http_data.size, res['Content-Length'].to_i
+ end
}
end
@@ -31,27 +345,33 @@ module TestNetHTTP_version_1_1_methods
end
def _test_get__get(http)
- res, body = http.get('/')
+ res = http.get('/')
assert_kind_of Net::HTTPResponse, res
assert_kind_of String, res.body
- assert_kind_of String, body
- assert_not_nil res['content-length']
- assert_equal $test_net_http_data.size, res['content-length'].to_i
+ 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, body.size
- assert_equal $test_net_http_data, body
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, body = http.get('/') {|s| buf << s }
+ res = http.get('/') {|s| buf << s }
assert_kind_of Net::HTTPResponse, res
# assert_kind_of String, res.body
- # assert_kind_of String, body
- assert_not_nil res['content-length']
- assert_equal $test_net_http_data.size, res['content-length'].to_i
+ 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
@@ -61,12 +381,13 @@ module TestNetHTTP_version_1_1_methods
def _test_get__chunked(http)
buf = ''
- res, body = http.get('/') {|s| buf << s }
+ res = http.get('/') {|s| buf << s }
assert_kind_of Net::HTTPResponse, res
# assert_kind_of String, res.body
- # assert_kind_of String, body
- assert_not_nil res['content-length']
- assert_equal $test_net_http_data.size, res['content-length'].to_i
+ 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
@@ -83,25 +404,43 @@ module TestNetHTTP_version_1_1_methods
end
}
assert_equal 1, i
+ @log_tester = nil # server may encount ECONNRESET
end
def test_get__implicit_start
- res, body = new().get('/')
+ res = new().get('/')
assert_kind_of Net::HTTPResponse, res
- assert_kind_of String, body
assert_kind_of String, res.body
- assert_not_nil res['content-length']
+ 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|
- assert_kind_of Net::HTTPResponse, res
- assert_kind_of Net::HTTPResponse, res.header
- assert_not_nil res['content-length']
+ 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
@@ -115,19 +454,23 @@ module TestNetHTTP_version_1_1_methods
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, body = http.post('/', data)
+ res = http.post('/', data, uheader)
assert_kind_of Net::HTTPResponse, res
- assert_kind_of String, body
assert_kind_of String, res.body
- assert_equal data, body
assert_equal data, res.body
assert_equal data, res.entity
end
@@ -135,29 +478,156 @@ module TestNetHTTP_version_1_1_methods
def _test_post__file(http)
data = 'post data'
f = StringIO.new
- http.post('/', data, nil, f)
+ 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("http://#{config('host')}:#{config('port')}/"),
+ URI.parse(url),
"a" => "x")
assert_equal ["a=x"], res.body.split(/[;&]/).sort
res = Net::HTTP.post_form(
- URI.parse("http://#{config('host')}:#{config('port')}/"),
+ URI.parse(url),
"a" => "x",
"b" => "y")
assert_equal ["a=x", "b=y"], res.body.split(/[;&]/).sort
res = Net::HTTP.post_form(
- URI.parse("http://#{config('host')}:#{config('port')}/"),
+ 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
@@ -166,10 +636,15 @@ 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
@@ -179,10 +654,30 @@ module TestNetHTTP_version_1_2_methods
http.request(req) {|res|
assert_kind_of Net::HTTPResponse, res
assert_kind_of String, res.body
- assert_not_nil res['content-length']
- assert_equal $test_net_http_data.size, res['content-length'].to_i
+ 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
@@ -190,11 +685,14 @@ module TestNetHTTP_version_1_2_methods
req = Net::HTTP::Get.new('/')
http.request(req) {|res|
assert_kind_of Net::HTTPResponse, res
- assert_not_nil res['content-length']
- assert_equal $test_net_http_data.size, res['content-length'].to_i
- f = StringIO.new
+ 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.size, f.string.size
+ 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
@@ -209,8 +707,10 @@ module TestNetHTTP_version_1_2_methods
req = Net::HTTP::Head.new('/')
http.request(req) {|res|
assert_kind_of Net::HTTPResponse, res
- assert_not_nil res['content-length']
- assert_equal $test_net_http_data.size, res['content-length'].to_i
+ 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
@@ -219,9 +719,12 @@ module TestNetHTTP_version_1_2_methods
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
- assert_equal data.size, res['content-length'].to_i
+ 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
}
@@ -231,6 +734,7 @@ module TestNetHTTP_version_1_2_methods
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
@@ -239,9 +743,55 @@ module TestNetHTTP_version_1_2_methods
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
@@ -249,154 +799,629 @@ module TestNetHTTP_version_1_2_methods
def _test_send_request__GET(http)
res = http.send_request('GET', '/')
assert_kind_of Net::HTTPResponse, res
- assert_equal $test_net_http_data.size, res['content-length'].to_i
+ 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)
+ 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
-module TestNetHTTPUtils
- def start(&block)
- new().start(&block)
+ 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
- klass = Net::HTTP::Proxy(config('proxy_host'), config('proxy_port'))
- http = klass.new(config('host'), config('port'))
- http.set_debug_output logfile()
- http
+ Net::HTTP.version_1_2
+ super
end
- def config(key)
- self.class::CONFIG[key]
+ 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 ? $stderr : NullWriter.new
+ @debug = StringIO.new('')
end
- def setup
- spawn_server
+ def mount_proc(&block)
+ @server.mount('/continue', block.to_proc)
end
- def teardown
- # resume global state
- Net::HTTP.version_1_2
+ 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 spawn_server
- return if $test_net_http_server_running
- server = WEBrick::HTTPServer.new(
- :BindAddress => config('host'),
- :Port => config('port'),
- :Logger => WEBrick::Log.new(NullWriter.new),
- :AccessLog => []
- )
- server.mount '/', Servlet
- Signal.trap(:INT) {
- server.shutdown
+ 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']
}
- Thread.fork {
- server.start
+ 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)
+ }
}
- n_try_max = 5
- begin
- TCPSocket.open(config('host'), config('port')).close
- rescue Errno::ECONNREFUSED
- sleep 0.2
- n_try_max -= 1
- raise 'cannot spawn server; give up' if n_try_max < 0
- retry
- end
- $test_net_http_server_running = true
+ 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
- $test_net_http = nil
- $test_net_http_data = (0...256).to_a.map {|i| i.chr }.join('') * 64
- $test_net_http_data_type = 'application/octet-stream'
+ 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 Servlet < WEBrick::HTTPServlet::AbstractServlet
- def do_GET(req, res)
- res['Content-Type'] = $test_net_http_data_type
- res.body = $test_net_http_data
+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
- # echo server
- def do_POST(req, res)
- res['Content-Type'] = req['Content-Type']
- res.body = req.body
+ 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,
+ }
- class NullWriter
- def <<(s) end
- def puts(*args) end
- def print(*args) end
- def printf(*args) end
+ 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 TestNetHTTP_version_1_1 < Test::Unit::TestCase
+class TestNetHTTPForceEncoding < Test::Unit::TestCase
CONFIG = {
- 'host' => '127.0.0.1',
- 'port' => 10081,
+ 'host' => 'localhost',
'proxy_host' => nil,
'proxy_port' => nil,
}
include TestNetHTTPUtils
- include TestNetHTTP_version_1_1_methods
- def new
- Net::HTTP.version_1_1
- super
+ 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 TestNetHTTP_v1_2 < Test::Unit::TestCase
+class TestNetHTTPPartialResponse < Test::Unit::TestCase
CONFIG = {
'host' => '127.0.0.1',
- 'port' => 10081,
'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
+ 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
-=begin
-class TestNetHTTP_proxy < Test::Unit::TestCase
+class TestNetHTTPInRactor < Test::Unit::TestCase
CONFIG = {
'host' => '127.0.0.1',
- 'port' => 10081,
- 'proxy_host' => '127.0.0.1',
- 'proxy_port' => 10082,
+ '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
+ 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
-=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
index 3ba1b217c0..69563168db 100644
--- a/test/net/http/test_httpheader.rb
+++ b/test/net/http/test_httpheader.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
require 'net/http'
require 'test/unit'
@@ -15,6 +16,30 @@ class HTTPHeaderTest < Test::Unit::TestCase
@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'
@@ -39,6 +64,16 @@ class HTTPHeaderTest < Test::Unit::TestCase
@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
@@ -64,6 +99,10 @@ class HTTPHeaderTest < Test::Unit::TestCase
@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
@@ -81,6 +120,24 @@ class HTTPHeaderTest < Test::Unit::TestCase
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']
@@ -104,6 +161,12 @@ class HTTPHeaderTest < Test::Unit::TestCase
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
@@ -114,6 +177,26 @@ class HTTPHeaderTest < Test::Unit::TestCase
@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
@@ -124,6 +207,11 @@ class HTTPHeaderTest < Test::Unit::TestCase
@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
@@ -132,6 +220,12 @@ class HTTPHeaderTest < Test::Unit::TestCase
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
@@ -140,6 +234,26 @@ class HTTPHeaderTest < Test::Unit::TestCase
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?
@@ -156,15 +270,32 @@ class HTTPHeaderTest < Test::Unit::TestCase
end
def test_range
- try_range(1..5, '1-5')
- try_range(234..567, '234-567')
- try_range(-5..-1, '-5')
- try_range(1..-1, '1-')
+ 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, Array(@c.range)[0]
+ 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=
@@ -181,6 +312,18 @@ class HTTPHeaderTest < Test::Unit::TestCase
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
@@ -190,6 +333,15 @@ class HTTPHeaderTest < Test::Unit::TestCase
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?
@@ -215,7 +367,7 @@ class HTTPHeaderTest < Test::Unit::TestCase
try_content_length 123, ' 123'
try_content_length 1, '1 23'
try_content_length 500, '(OK)500'
- assert_raises(Net::HTTPHeaderSyntaxError, 'here is no digit, but') {
+ assert_raise(Net::HTTPHeaderSyntaxError, 'here is no digit, but') {
@c['content-length'] = 'no digit'
@c.content_length
}
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
index 66ca3ddd9b..237c16e64d 100644
--- a/test/net/http/test_https_proxy.rb
+++ b/test/net/http/test_https_proxy.rb
@@ -1,33 +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
- t = nil
TCPServer.open("127.0.0.1", 0) {|serv|
_, port, _, _ = serv.addr
- t = Thread.new {
+ 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
- http.start
+ 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
}
- sock = serv.accept
- 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]")
+ 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])
}
- ensure
- t.join if t
end
-end if defined?(OpenSSL)
-
+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/imap/test_imap.rb b/test/net/imap/test_imap.rb
deleted file mode 100644
index aa4215224f..0000000000
--- a/test/net/imap/test_imap.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-require "net/imap"
-require "test/unit"
-
-class IMAPTest < Test::Unit::TestCase
- def test_encode_utf7
- s = Net::IMAP.encode_utf7("\357\274\241\357\274\242\357\274\243")
- assert_equal("&,yH,Iv8j-", s)
- end
-
- def test_decode_utf7
- s = Net::IMAP.decode_utf7("&,yH,Iv8j-")
- assert_equal("\357\274\241\357\274\242\357\274\243", s)
- 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