├── .document ├── .github ├── release.yml ├── dependabot.yml └── workflows │ ├── test.yml │ ├── sync-ruby.yml │ └── push_gem.yml ├── .gitignore ├── Gemfile ├── bin ├── setup └── console ├── doc └── net-http │ ├── included_getters.rdoc │ └── examples.rdoc ├── Rakefile ├── test ├── lib │ └── helper.rb └── net │ ├── http │ ├── test_buffered_io.rb │ ├── test_httpresponses.rb │ ├── test_https_proxy.rb │ ├── test_http_request.rb │ ├── test_https.rb │ ├── utils.rb │ ├── test_httpheader.rb │ ├── test_httpresponse.rb │ └── test_http.rb │ └── fixtures │ ├── Makefile │ ├── server.crt │ ├── cacert.pem │ └── server.key ├── lib └── net │ ├── http │ ├── proxy_delta.rb │ ├── exceptions.rb │ ├── status.rb │ ├── request.rb │ ├── generic_request.rb │ ├── requests.rb │ ├── response.rb │ └── header.rb │ └── https.rb ├── BSDL ├── net-http.gemspec ├── COPYING └── README.md /.document: -------------------------------------------------------------------------------- 1 | BSDL 2 | COPYING 3 | README.md 4 | doc/ 5 | lib/ 6 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - dependencies # Added by Dependabot 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /pkg/ 6 | /spec/reports/ 7 | /tmp/ 8 | /Gemfile.lock 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "rake" 6 | gem "test-unit" 7 | gem "test-unit-ruby-core" 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /doc/net-http/included_getters.rdoc: -------------------------------------------------------------------------------- 1 | This class also includes (indirectly) module Net::HTTPHeader, 2 | which gives access to its 3 | {methods for getting headers}[rdoc-ref:Net::HTTPHeader@Getters]. 4 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test/lib" 6 | t.ruby_opts << "-rhelper" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | 6 | module Test 7 | module Unit 8 | class TestCase 9 | def windows? platform = RUBY_PLATFORM 10 | /mswin|mingw/ =~ platform 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/net/http/proxy_delta.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Net::HTTP::ProxyDelta #:nodoc: internal use only 3 | private 4 | 5 | def conn_address 6 | proxy_address() 7 | end 8 | 9 | def conn_port 10 | proxy_port() 11 | end 12 | 13 | def edit_path(path) 14 | use_ssl? ? path : "http://#{addr_port()}#{path}" 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "net/http" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /test/net/http/test_buffered_io.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'net/http' 4 | require 'stringio' 5 | 6 | require_relative 'utils' 7 | 8 | module Net 9 | class TestBufferedIO < Test::Unit::TestCase 10 | def test_eof? 11 | s = StringIO.new 12 | assert s.eof? 13 | bio = BufferedIO.new(s) 14 | assert_equal s, bio.io 15 | assert_equal s.eof?, bio.eof? 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/net/fixtures/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | regen_certs: 4 | touch server.key 5 | make server.crt 6 | 7 | cacert.pem: server.key 8 | 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" 9 | 10 | server.csr: 11 | openssl req -new -key server.key -out server.csr -subj "/C=JP/ST=Shimane/O=Ruby Core Team/OU=Ruby Test/CN=localhost" 12 | 13 | server.crt: server.csr cacert.pem 14 | openssl x509 -days 3650 -CA cacert.pem -CAkey server.key -set_serial 00 -in server.csr -req -out server.crt 15 | rm server.csr 16 | -------------------------------------------------------------------------------- /lib/net/https.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | =begin 3 | 4 | = net/https -- SSL/TLS enhancement for Net::HTTP. 5 | 6 | This file has been merged with net/http. There is no longer any need to 7 | require 'net/https' to use HTTPS. 8 | 9 | See Net::HTTP for details on how to make HTTPS connections. 10 | 11 | == Info 12 | 'OpenSSL for Ruby 2' project 13 | Copyright (C) 2001 GOTOU Yuuzou 14 | All rights reserved. 15 | 16 | == Licence 17 | This program is licensed under the same licence as Ruby. 18 | (See the file 'LICENCE'.) 19 | 20 | =end 21 | 22 | require_relative 'http' 23 | require 'openssl' 24 | -------------------------------------------------------------------------------- /test/net/http/test_httpresponses.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'net/http' 3 | require 'test/unit' 4 | 5 | class HTTPResponsesTest < Test::Unit::TestCase 6 | def test_status_code_classes 7 | Net::HTTPResponse::CODE_TO_OBJ.each_pair { |code, klass| 8 | case code 9 | when /\A1\d\d\z/ 10 | group = Net::HTTPInformation 11 | when /\A2\d\d\z/ 12 | group = Net::HTTPSuccess 13 | when /\A3\d\d\z/ 14 | group = Net::HTTPRedirection 15 | when /\A4\d\d\z/ 16 | group = Net::HTTPClientError 17 | when /\A5\d\d\z/ 18 | group = Net::HTTPServerError 19 | else 20 | flunk "Unknown HTTP status code: #{code} => #{klass.name}" 21 | end 22 | assert(klass < group, "#{klass.name} (#{code}) must inherit from #{group.name}") 23 | } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | engine: cruby 10 | min_version: 2.7 11 | 12 | test: 13 | needs: ruby-versions 14 | name: test (${{ matrix.ruby }} / ${{ matrix.os }}) 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 19 | os: [ ubuntu-latest, macos-latest, windows-latest ] 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - uses: actions/checkout@v6 23 | - name: Set up Ruby 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby }} 27 | - name: Install dependencies 28 | run: bundle install 29 | - name: Run test 30 | run: rake test 31 | - name: Build 32 | run: rake build 33 | env: 34 | LANG: C 35 | -------------------------------------------------------------------------------- /doc/net-http/examples.rdoc: -------------------------------------------------------------------------------- 1 | Examples here assume that net/http has been required 2 | (which also requires +uri+): 3 | 4 | require 'net/http' 5 | 6 | Many code examples here use these example websites: 7 | 8 | - https://jsonplaceholder.typicode.com. 9 | - http://example.com. 10 | 11 | Some examples also assume these variables: 12 | 13 | uri = URI('https://jsonplaceholder.typicode.com/') 14 | uri.freeze # Examples may not modify. 15 | hostname = uri.hostname # => "jsonplaceholder.typicode.com" 16 | path = uri.path # => "/" 17 | port = uri.port # => 443 18 | 19 | So that example requests may be written as: 20 | 21 | Net::HTTP.get(uri) 22 | Net::HTTP.get(hostname, '/index.html') 23 | Net::HTTP.start(hostname) do |http| 24 | http.get('/todos/1') 25 | http.get('/todos/2') 26 | end 27 | 28 | An example that needs a modified URI first duplicates +uri+, then modifies the duplicate: 29 | 30 | _uri = uri.dup 31 | _uri.path = '/todos/1' 32 | -------------------------------------------------------------------------------- /lib/net/http/exceptions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Net 3 | # Net::HTTP exception class. 4 | # You cannot use Net::HTTPExceptions directly; instead, you must use 5 | # its subclasses. 6 | module HTTPExceptions # :nodoc: 7 | def initialize(msg, res) #:nodoc: 8 | super msg 9 | @response = res 10 | end 11 | attr_reader :response 12 | alias data response #:nodoc: obsolete 13 | end 14 | 15 | # :stopdoc: 16 | class HTTPError < ProtocolError 17 | include HTTPExceptions 18 | end 19 | 20 | class HTTPRetriableError < ProtoRetriableError 21 | include HTTPExceptions 22 | end 23 | 24 | class HTTPClientException < ProtoServerError 25 | include HTTPExceptions 26 | end 27 | 28 | class HTTPFatalError < ProtoFatalError 29 | include HTTPExceptions 30 | end 31 | 32 | # We cannot use the name "HTTPServerError", it is the name of the response. 33 | HTTPServerException = HTTPClientException # :nodoc: 34 | deprecate_constant(:HTTPServerException) 35 | end 36 | -------------------------------------------------------------------------------- /.github/workflows/sync-ruby.yml: -------------------------------------------------------------------------------- 1 | name: Sync ruby 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | sync: 7 | name: Sync ruby 8 | runs-on: ubuntu-latest 9 | if: ${{ github.repository_owner == 'ruby' }} 10 | steps: 11 | - uses: actions/checkout@v6 12 | 13 | - name: Create GitHub App token 14 | id: app-token 15 | uses: actions/create-github-app-token@v2 16 | with: 17 | app-id: 2060836 18 | private-key: ${{ secrets.RUBY_SYNC_DEFAULT_GEMS_PRIVATE_KEY }} 19 | owner: ruby 20 | repositories: ruby 21 | 22 | - name: Sync to ruby/ruby 23 | uses: convictional/trigger-workflow-and-wait@v1.6.5 24 | with: 25 | owner: ruby 26 | repo: ruby 27 | workflow_file_name: sync_default_gems.yml 28 | github_token: ${{ steps.app-token.outputs.token }} 29 | ref: master 30 | client_payload: | 31 | {"gem":"${{ github.event.repository.name }}","before":"${{ github.event.before }}","after":"${{ github.event.after }}"} 32 | propagate_failure: true 33 | wait_interval: 10 34 | -------------------------------------------------------------------------------- /test/net/fixtures/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDYTCCAkkCAQAwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAkpQMRAwDgYD 3 | VQQIDAdTaGltYW5lMRQwEgYDVQQHDAtNYXR6LWUgY2l0eTEXMBUGA1UECgwOUnVi 4 | eSBDb3JlIFRlYW0xFTATBgNVBAMMDFJ1YnkgVGVzdCBDQTElMCMGCSqGSIb3DQEJ 5 | ARYWc2VjdXJpdHlAcnVieS1sYW5nLm9yZzAeFw0yNDAxMDExMTQ3MjNaFw0zMzEy 6 | MjkxMTQ3MjNaMGAxCzAJBgNVBAYTAkpQMRAwDgYDVQQIDAdTaGltYW5lMRcwFQYD 7 | VQQKDA5SdWJ5IENvcmUgVGVhbTESMBAGA1UECwwJUnVieSBUZXN0MRIwEAYDVQQD 8 | DAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCw+egZ 9 | Q6eumJKq3hfKfED4dE/tL4FI5sjqont9ABVI+1GSqyi1bFBgsRjM0THllIdMbKmJ 10 | tWwnKW8J+5OgNN8y6Xxv8JmM/Y5vQt2lis0fqXmG8UTz0VTWdlAXXmhUs6lSADvA 11 | aIe4RVrCsZ97L3ZQTryY7JRVcbB4khUN3Gp0yg+801SXzoFTTa+UGIRLE66jH51a 12 | a5VXu99hnv1OiH8tQrjdi8mH6uG/icq4XuIeNWMF32wHqIOOPvQcWV3M5D2vxJEj 13 | 702Ku6k9OQXkAo17qRSEonWW4HtLbtmS8He1JNPc/n3dVUm+fM6NoDXPoLP7j55G 14 | 9zKyqGtGAWXAj1MTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACtGNdj5TEtnJBYp 15 | M+LhBeU3oNteldfycEm993gJp6ghWZFg23oX8fVmyEeJr/3Ca9bAgDqg0t9a0npN 16 | oWKEY6wVKqcHgu3gSvThF5c9KhGbeDDmlTSVVNQmXWX0K2d4lS2cwZHH8mCm2mrY 17 | PDqlEkSc7k4qSiqigdS8i80Yk+lDXWsm8CjsiC93qaRM7DnS0WPQR0c16S95oM6G 18 | VklFKUSDAuFjw9aVWA/nahOucjn0w5fVW6lyIlkBslC1ChlaDgJmvhz+Ol3iMsE0 19 | kAmFNu2KKPVrpMWaBID49QwQTDyhetNLaVVFM88iUdA9JDoVMEuP1mm39JqyzHTu 20 | uBrdP4Q= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/net-http' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/net-http 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: ruby 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2 40 | 41 | - name: Create GitHub release 42 | run: | 43 | tag_name="$(git describe --tags --abbrev=0)" 44 | gh release create "${tag_name}" --verify-tag --generate-notes 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /test/net/fixtures/cacert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID+zCCAuOgAwIBAgIUGMvHl3EhtKPKcgc3NQSAYfFuC+8wDQYJKoZIhvcNAQEL 3 | BQAwgYwxCzAJBgNVBAYTAkpQMRAwDgYDVQQIDAdTaGltYW5lMRQwEgYDVQQHDAtN 4 | YXR6LWUgY2l0eTEXMBUGA1UECgwOUnVieSBDb3JlIFRlYW0xFTATBgNVBAMMDFJ1 5 | YnkgVGVzdCBDQTElMCMGCSqGSIb3DQEJARYWc2VjdXJpdHlAcnVieS1sYW5nLm9y 6 | ZzAeFw0yNDAxMDExMTQ3MjNaFw0zMzEyMjkxMTQ3MjNaMIGMMQswCQYDVQQGEwJK 7 | UDEQMA4GA1UECAwHU2hpbWFuZTEUMBIGA1UEBwwLTWF0ei1lIGNpdHkxFzAVBgNV 8 | BAoMDlJ1YnkgQ29yZSBUZWFtMRUwEwYDVQQDDAxSdWJ5IFRlc3QgQ0ExJTAjBgkq 9 | hkiG9w0BCQEWFnNlY3VyaXR5QHJ1YnktbGFuZy5vcmcwggEiMA0GCSqGSIb3DQEB 10 | AQUAA4IBDwAwggEKAoIBAQCw+egZQ6eumJKq3hfKfED4dE/tL4FI5sjqont9ABVI 11 | +1GSqyi1bFBgsRjM0THllIdMbKmJtWwnKW8J+5OgNN8y6Xxv8JmM/Y5vQt2lis0f 12 | qXmG8UTz0VTWdlAXXmhUs6lSADvAaIe4RVrCsZ97L3ZQTryY7JRVcbB4khUN3Gp0 13 | yg+801SXzoFTTa+UGIRLE66jH51aa5VXu99hnv1OiH8tQrjdi8mH6uG/icq4XuIe 14 | NWMF32wHqIOOPvQcWV3M5D2vxJEj702Ku6k9OQXkAo17qRSEonWW4HtLbtmS8He1 15 | JNPc/n3dVUm+fM6NoDXPoLP7j55G9zKyqGtGAWXAj1MTAgMBAAGjUzBRMB0GA1Ud 16 | DgQWBBSJGVleDvFp9cu9R+E0/OKYzGkwkTAfBgNVHSMEGDAWgBSJGVleDvFp9cu9 17 | R+E0/OKYzGkwkTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBl 18 | 8GLB8skAWlkSw/FwbUmEV3zyqu+p7PNP5YIYoZs0D74e7yVulGQ6PKMZH5hrZmHo 19 | orFSQU+VUUirG8nDGj7Rzce8WeWBxsaDGC8CE2dq6nC6LuUwtbdMnBrH0LRWAz48 20 | jGFF3jHtVz8VsGfoZTZCjukWqNXvU6hETT9GsfU+PZqbqcTVRPH52+XgYayKdIbD 21 | r97RM4X3+aXBHcUW0b76eyyi65RR/Xtvn8ioZt2AdX7T2tZzJyXJN3Hupp77s6Ui 22 | AZR35SToHCZeTZD12YBvLBdaTPLZN7O/Q/aAO9ZiJaZ7SbFOjz813B2hxXab4Fob 23 | 2uJX6eMWTVxYK5D4M9lm 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /net-http.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | name = File.basename(__FILE__, ".gemspec") 4 | version = ["lib", Array.new(name.count("-")+1, "..").join("/")].find do |dir| 5 | file = File.join(__dir__, dir, "#{name.tr('-', '/')}.rb") 6 | begin 7 | break File.foreach(file, mode: "rb") do |line| 8 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 9 | end 10 | rescue SystemCallError 11 | next 12 | end 13 | end 14 | 15 | Gem::Specification.new do |spec| 16 | spec.name = name 17 | spec.version = version 18 | spec.authors = ["NARUSE, Yui"] 19 | spec.email = ["naruse@airemix.jp"] 20 | 21 | spec.summary = %q{HTTP client api for Ruby.} 22 | spec.description = %q{HTTP client api for Ruby.} 23 | spec.homepage = "https://github.com/ruby/net-http" 24 | spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0") 25 | spec.licenses = ["Ruby", "BSD-2-Clause"] 26 | 27 | spec.metadata["changelog_uri"] = spec.homepage + "/releases" 28 | spec.metadata["homepage_uri"] = spec.homepage 29 | spec.metadata["source_code_uri"] = spec.homepage 30 | 31 | # Specify which files should be added to the gem when it is released. 32 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 33 | excludes = %W[/.git* /bin /test /*file /#{File.basename(__FILE__)}] 34 | spec.files = IO.popen(%W[git -C #{__dir__} ls-files -z --] + excludes.map {|e| ":^#{e}"}, &:read).split("\x0") 35 | spec.bindir = "exe" 36 | spec.require_paths = ["lib"] 37 | 38 | spec.add_dependency "uri", ">= 0.11.1" 39 | end 40 | -------------------------------------------------------------------------------- /test/net/fixtures/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAsPnoGUOnrpiSqt4XynxA+HRP7S+BSObI6qJ7fQAVSPtRkqso 3 | tWxQYLEYzNEx5ZSHTGypibVsJylvCfuToDTfMul8b/CZjP2Ob0LdpYrNH6l5hvFE 4 | 89FU1nZQF15oVLOpUgA7wGiHuEVawrGfey92UE68mOyUVXGweJIVDdxqdMoPvNNU 5 | l86BU02vlBiESxOuox+dWmuVV7vfYZ79Toh/LUK43YvJh+rhv4nKuF7iHjVjBd9s 6 | B6iDjj70HFldzOQ9r8SRI+9NirupPTkF5AKNe6kUhKJ1luB7S27ZkvB3tSTT3P59 7 | 3VVJvnzOjaA1z6Cz+4+eRvcysqhrRgFlwI9TEwIDAQABAoIBAEEYiyDP29vCzx/+ 8 | dS3LqnI5BjUuJhXUnc6AWX/PCgVAO+8A+gZRgvct7PtZb0sM6P9ZcLrweomlGezI 9 | FrL0/6xQaa8bBr/ve/a8155OgcjFo6fZEw3Dz7ra5fbSiPmu4/b/kvrg+Br1l77J 10 | aun6uUAs1f5B9wW+vbR7tzbT/mxaUeDiBzKpe15GwcvbJtdIVMa2YErtRjc1/5B2 11 | BGVXyvlJv0SIlcIEMsHgnAFOp1ZgQ08aDzvilLq8XVMOahAhP1O2A3X8hKdXPyrx 12 | IVWE9bS9ptTo+eF6eNl+d7htpKGEZHUxinoQpWEBTv+iOoHsVunkEJ3vjLP3lyI/ 13 | fY0NQ1ECgYEA3RBXAjgvIys2gfU3keImF8e/TprLge1I2vbWmV2j6rZCg5r/AS0u 14 | pii5CvJ5/T5vfJPNgPBy8B/yRDs+6PJO1GmnlhOkG9JAIPkv0RBZvR0PMBtbp6nT 15 | Y3yo1lwamBVBfY6rc0sLTzosZh2aGoLzrHNMQFMGaauORzBFpY5lU50CgYEAzPHl 16 | u5DI6Xgep1vr8QvCUuEesCOgJg8Yh1UqVoY/SmQh6MYAv1I9bLGwrb3WW/7kqIoD 17 | fj0aQV5buVZI2loMomtU9KY5SFIsPV+JuUpy7/+VE01ZQM5FdY8wiYCQiVZYju9X 18 | Wz5LxMNoz+gT7pwlLCsC4N+R8aoBk404aF1gum8CgYAJ7VTq7Zj4TFV7Soa/T1eE 19 | k9y8a+kdoYk3BASpCHJ29M5R2KEA7YV9wrBklHTz8VzSTFTbKHEQ5W5csAhoL5Fo 20 | qoHzFFi3Qx7MHESQb9qHyolHEMNx6QdsHUn7rlEnaTTyrXh3ifQtD6C0yTmFXUIS 21 | CW9wKApOrnyKJ9nI0HcuZQKBgQCMtoV6e9VGX4AEfpuHvAAnMYQFgeBiYTkBKltQ 22 | XwozhH63uMMomUmtSG87Sz1TmrXadjAhy8gsG6I0pWaN7QgBuFnzQ/HOkwTm+qKw 23 | AsrZt4zeXNwsH7QXHEJCFnCmqw9QzEoZTrNtHJHpNboBuVnYcoueZEJrP8OnUG3r 24 | UjmopwKBgAqB2KYYMUqAOvYcBnEfLDmyZv9BTVNHbR2lKkMYqv5LlvDaBxVfilE0 25 | 2riO4p6BaAdvzXjKeRrGNEKoHNBpOSfYCOM16NjL8hIZB1CaV3WbT5oY+jp7Mzd5 26 | 7d56RZOE+ERK2uz/7JX9VSsM/LbH9pJibd4e8mikDS9ntciqOH/3 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /lib/net/http/status.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative '../http' 4 | 5 | if $0 == __FILE__ 6 | require 'open-uri' 7 | File.foreach(__FILE__) do |line| 8 | puts line 9 | break if line.start_with?('end') 10 | end 11 | puts 12 | puts "Net::HTTP::STATUS_CODES = {" 13 | url = "https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv" 14 | URI(url).read.each_line do |line| 15 | code, mes, = line.split(',') 16 | next if ['(Unused)', 'Unassigned', 'Description'].include?(mes) 17 | puts " #{code} => '#{mes}'," 18 | end 19 | puts "} # :nodoc:" 20 | end 21 | 22 | Net::HTTP::STATUS_CODES = { 23 | 100 => 'Continue', 24 | 101 => 'Switching Protocols', 25 | 102 => 'Processing', 26 | 103 => 'Early Hints', 27 | 200 => 'OK', 28 | 201 => 'Created', 29 | 202 => 'Accepted', 30 | 203 => 'Non-Authoritative Information', 31 | 204 => 'No Content', 32 | 205 => 'Reset Content', 33 | 206 => 'Partial Content', 34 | 207 => 'Multi-Status', 35 | 208 => 'Already Reported', 36 | 226 => 'IM Used', 37 | 300 => 'Multiple Choices', 38 | 301 => 'Moved Permanently', 39 | 302 => 'Found', 40 | 303 => 'See Other', 41 | 304 => 'Not Modified', 42 | 305 => 'Use Proxy', 43 | 307 => 'Temporary Redirect', 44 | 308 => 'Permanent Redirect', 45 | 400 => 'Bad Request', 46 | 401 => 'Unauthorized', 47 | 402 => 'Payment Required', 48 | 403 => 'Forbidden', 49 | 404 => 'Not Found', 50 | 405 => 'Method Not Allowed', 51 | 406 => 'Not Acceptable', 52 | 407 => 'Proxy Authentication Required', 53 | 408 => 'Request Timeout', 54 | 409 => 'Conflict', 55 | 410 => 'Gone', 56 | 411 => 'Length Required', 57 | 412 => 'Precondition Failed', 58 | 413 => 'Content Too Large', 59 | 414 => 'URI Too Long', 60 | 415 => 'Unsupported Media Type', 61 | 416 => 'Range Not Satisfiable', 62 | 417 => 'Expectation Failed', 63 | 421 => 'Misdirected Request', 64 | 422 => 'Unprocessable Content', 65 | 423 => 'Locked', 66 | 424 => 'Failed Dependency', 67 | 425 => 'Too Early', 68 | 426 => 'Upgrade Required', 69 | 428 => 'Precondition Required', 70 | 429 => 'Too Many Requests', 71 | 431 => 'Request Header Fields Too Large', 72 | 451 => 'Unavailable For Legal Reasons', 73 | 500 => 'Internal Server Error', 74 | 501 => 'Not Implemented', 75 | 502 => 'Bad Gateway', 76 | 503 => 'Service Unavailable', 77 | 504 => 'Gateway Timeout', 78 | 505 => 'HTTP Version Not Supported', 79 | 506 => 'Variant Also Negotiates', 80 | 507 => 'Insufficient Storage', 81 | 508 => 'Loop Detected', 82 | 510 => 'Not Extended (OBSOLETED)', 83 | 511 => 'Network Authentication Required', 84 | } # :nodoc: 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Net::HTTP 2 | 3 | Net::HTTP provides a rich library which can be used to build HTTP 4 | user-agents. For more details about HTTP see 5 | [RFC9110 HTTP Semantics](https://www.ietf.org/rfc/rfc9110.html) and 6 | [RFC9112 HTTP/1.1](https://www.ietf.org/rfc/rfc9112.html). 7 | 8 | Net::HTTP is designed to work closely with URI. URI::HTTP#host, 9 | URI::HTTP#port and URI::HTTP#request_uri are designed to work with 10 | Net::HTTP. 11 | 12 | If you are only performing a few GET requests you should try OpenURI. 13 | 14 | ## Installation 15 | 16 | Add this line to your application's Gemfile: 17 | 18 | ```ruby 19 | gem 'net-http' 20 | ``` 21 | 22 | And then execute: 23 | 24 | $ bundle install 25 | 26 | Or install it yourself as: 27 | 28 | $ gem install net-http 29 | 30 | ## Usage 31 | 32 | All examples assume you have loaded Net::HTTP with: 33 | 34 | ```ruby 35 | require 'net/http' 36 | ``` 37 | 38 | This will also require 'uri' so you don't need to require it separately. 39 | 40 | The Net::HTTP methods in the following section do not persist 41 | connections. They are not recommended if you are performing many HTTP 42 | requests. 43 | 44 | ### GET 45 | 46 | ```ruby 47 | Net::HTTP.get('example.com', '/index.html') # => String 48 | ``` 49 | 50 | ### GET by URI 51 | 52 | ```ruby 53 | uri = URI('http://example.com/index.html?count=10') 54 | Net::HTTP.get(uri) # => String 55 | ``` 56 | 57 | ### GET with Dynamic Parameters 58 | 59 | ```ruby 60 | uri = URI('http://example.com/index.html') 61 | params = { :limit => 10, :page => 3 } 62 | uri.query = URI.encode_www_form(params) 63 | 64 | res = Net::HTTP.get_response(uri) 65 | puts res.body if res.is_a?(Net::HTTPSuccess) 66 | ``` 67 | 68 | ### POST 69 | 70 | ```ruby 71 | uri = URI('http://www.example.com/search.cgi') 72 | res = Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50') 73 | puts res.body 74 | ``` 75 | 76 | ### POST with Multiple Values 77 | 78 | ```ruby 79 | uri = URI('http://www.example.com/search.cgi') 80 | res = Net::HTTP.post_form(uri, 'q' => ['ruby', 'perl'], 'max' => '50') 81 | puts res.body 82 | ``` 83 | 84 | ## Development 85 | 86 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 87 | 88 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 89 | 90 | ## Contributing 91 | 92 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/net-http. 93 | 94 | -------------------------------------------------------------------------------- /test/net/http/test_https_proxy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | begin 3 | require 'net/https' 4 | rescue LoadError 5 | end 6 | require 'test/unit' 7 | 8 | return unless defined?(OpenSSL::SSL) 9 | 10 | class HTTPSProxyTest < Test::Unit::TestCase 11 | def test_https_proxy_authentication 12 | TCPServer.open("127.0.0.1", 0) {|serv| 13 | _, port, _, _ = serv.addr 14 | client_thread = Thread.new { 15 | proxy = Net::HTTP.Proxy("127.0.0.1", port, 'user', 'password') 16 | http = proxy.new("foo.example.org", 8000) 17 | http.use_ssl = true 18 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 19 | begin 20 | http.start 21 | rescue EOFError 22 | end 23 | } 24 | server_thread = Thread.new { 25 | sock = serv.accept 26 | begin 27 | proxy_request = sock.gets("\r\n\r\n") 28 | assert_equal( 29 | "CONNECT foo.example.org:8000 HTTP/1.1\r\n" + 30 | "Host: foo.example.org:8000\r\n" + 31 | "Proxy-Authorization: Basic dXNlcjpwYXNzd29yZA==\r\n" + 32 | "\r\n", 33 | proxy_request, 34 | "[ruby-dev:25673]") 35 | ensure 36 | sock.close 37 | end 38 | } 39 | assert_join_threads([client_thread, server_thread]) 40 | } 41 | end 42 | 43 | 44 | def read_fixture(key) 45 | File.read(File.expand_path("../fixtures/#{key}", __dir__)) 46 | end 47 | 48 | def test_https_proxy_ssl_connection 49 | TCPServer.open("127.0.0.1", 0) {|tcpserver| 50 | ctx = OpenSSL::SSL::SSLContext.new 51 | ctx.key = OpenSSL::PKey.read(read_fixture("server.key")) 52 | ctx.cert = OpenSSL::X509::Certificate.new(read_fixture("server.crt")) 53 | serv = OpenSSL::SSL::SSLServer.new(tcpserver, ctx) 54 | 55 | _, port, _, _ = serv.addr 56 | client_thread = Thread.new { 57 | proxy = Net::HTTP.Proxy("127.0.0.1", port, 'user', 'password', true) 58 | http = proxy.new("foo.example.org", 8000) 59 | http.use_ssl = true 60 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 61 | begin 62 | http.start 63 | rescue EOFError 64 | end 65 | } 66 | server_thread = Thread.new { 67 | sock = serv.accept 68 | begin 69 | proxy_request = sock.gets("\r\n\r\n") 70 | assert_equal( 71 | "CONNECT foo.example.org:8000 HTTP/1.1\r\n" + 72 | "Host: foo.example.org:8000\r\n" + 73 | "Proxy-Authorization: Basic dXNlcjpwYXNzd29yZA==\r\n" + 74 | "\r\n", 75 | proxy_request, 76 | "[ruby-core:96672]") 77 | ensure 78 | sock.close 79 | end 80 | } 81 | assert_join_threads([client_thread, server_thread]) 82 | } 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/net/http/request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This class is the base class for \Net::HTTP request classes. 4 | # The class should not be used directly; 5 | # instead you should use its subclasses, listed below. 6 | # 7 | # == Creating a Request 8 | # 9 | # An request object may be created with either a URI or a string hostname: 10 | # 11 | # require 'net/http' 12 | # uri = URI('https://jsonplaceholder.typicode.com/') 13 | # req = Net::HTTP::Get.new(uri) # => # 14 | # req = Net::HTTP::Get.new(uri.hostname) # => # 15 | # 16 | # And with any of the subclasses: 17 | # 18 | # req = Net::HTTP::Head.new(uri) # => # 19 | # req = Net::HTTP::Post.new(uri) # => # 20 | # req = Net::HTTP::Put.new(uri) # => # 21 | # # ... 22 | # 23 | # The new instance is suitable for use as the argument to Net::HTTP#request. 24 | # 25 | # == Request Headers 26 | # 27 | # A new request object has these header fields by default: 28 | # 29 | # req.to_hash 30 | # # => 31 | # {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"], 32 | # "accept"=>["*/*"], 33 | # "user-agent"=>["Ruby"], 34 | # "host"=>["jsonplaceholder.typicode.com"]} 35 | # 36 | # See: 37 | # 38 | # - {Request header Accept-Encoding}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Accept-Encoding] 39 | # and {Compression and Decompression}[rdoc-ref:Net::HTTP@Compression+and+Decompression]. 40 | # - {Request header Accept}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#accept-request-header]. 41 | # - {Request header User-Agent}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#user-agent-request-header]. 42 | # - {Request header Host}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#host-request-header]. 43 | # 44 | # You can add headers or override default headers: 45 | # 46 | # # res = Net::HTTP::Get.new(uri, {'foo' => '0', 'bar' => '1'}) 47 | # 48 | # This class (and therefore its subclasses) also includes (indirectly) 49 | # module Net::HTTPHeader, which gives access to its 50 | # {methods for setting headers}[rdoc-ref:Net::HTTPHeader@Setters]. 51 | # 52 | # == Request Subclasses 53 | # 54 | # Subclasses for HTTP requests: 55 | # 56 | # - Net::HTTP::Get 57 | # - Net::HTTP::Head 58 | # - Net::HTTP::Post 59 | # - Net::HTTP::Put 60 | # - Net::HTTP::Delete 61 | # - Net::HTTP::Options 62 | # - Net::HTTP::Trace 63 | # - Net::HTTP::Patch 64 | # 65 | # Subclasses for WebDAV requests: 66 | # 67 | # - Net::HTTP::Propfind 68 | # - Net::HTTP::Proppatch 69 | # - Net::HTTP::Mkcol 70 | # - Net::HTTP::Copy 71 | # - Net::HTTP::Move 72 | # - Net::HTTP::Lock 73 | # - Net::HTTP::Unlock 74 | # 75 | class Net::HTTPRequest < Net::HTTPGenericRequest 76 | # Creates an HTTP request object for +path+. 77 | # 78 | # +initheader+ are the default headers to use. Net::HTTP adds 79 | # Accept-Encoding to enable compression of the response body unless 80 | # Accept-Encoding or Range are supplied in +initheader+. 81 | 82 | def initialize(path, initheader = nil) 83 | super self.class::METHOD, 84 | self.class::REQUEST_HAS_BODY, 85 | self.class::RESPONSE_HAS_BODY, 86 | path, initheader 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/net/http/test_http_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'net/http' 3 | require 'test/unit' 4 | 5 | class HTTPRequestTest < Test::Unit::TestCase 6 | 7 | def test_initialize_GET 8 | req = Net::HTTP::Get.new '/' 9 | 10 | assert_equal 'GET', req.method 11 | assert_not_predicate req, :request_body_permitted? 12 | assert_predicate req, :response_body_permitted? 13 | 14 | expected = { 15 | 'accept' => %w[*/*], 16 | 'user-agent' => %w[Ruby], 17 | } 18 | 19 | expected['accept-encoding'] = %w[gzip;q=1.0,deflate;q=0.6,identity;q=0.3] if 20 | Net::HTTP::HAVE_ZLIB 21 | 22 | assert_equal expected, req.to_hash 23 | end 24 | 25 | def test_initialize_GET_range 26 | req = Net::HTTP::Get.new '/', 'Range' => 'bytes=0-9' 27 | 28 | assert_equal 'GET', req.method 29 | assert_not_predicate req, :request_body_permitted? 30 | assert_predicate req, :response_body_permitted? 31 | 32 | expected = { 33 | 'accept' => %w[*/*], 34 | 'user-agent' => %w[Ruby], 35 | 'range' => %w[bytes=0-9], 36 | } 37 | 38 | assert_equal expected, req.to_hash 39 | end 40 | 41 | def test_initialize_HEAD 42 | req = Net::HTTP::Head.new '/' 43 | 44 | assert_equal 'HEAD', req.method 45 | assert_not_predicate req, :request_body_permitted? 46 | assert_not_predicate req, :response_body_permitted? 47 | 48 | expected = { 49 | 'accept' => %w[*/*], 50 | "accept-encoding" => %w[gzip;q=1.0,deflate;q=0.6,identity;q=0.3], 51 | 'user-agent' => %w[Ruby], 52 | } 53 | 54 | assert_equal expected, req.to_hash 55 | end 56 | 57 | def test_initialize_accept_encoding 58 | req1 = Net::HTTP::Get.new '/' 59 | 60 | assert req1.decode_content, 'Bug #7831 - automatically decode content' 61 | 62 | req2 = Net::HTTP::Get.new '/', 'accept-encoding' => 'identity' 63 | 64 | assert_not_predicate req2, :decode_content, 65 | 'Bug #7381 - do not decode content if the user overrides' 66 | end if Net::HTTP::HAVE_ZLIB 67 | 68 | def test_initialize_GET_uri 69 | req = Net::HTTP::Get.new(URI("http://example.com/foo")) 70 | assert_equal "/foo", req.path 71 | assert_equal "example.com", req['Host'] 72 | 73 | req = Net::HTTP::Get.new(URI("https://example.com/foo")) 74 | assert_equal "/foo", req.path 75 | assert_equal "example.com", req['Host'] 76 | 77 | req = Net::HTTP::Get.new(URI("https://203.0.113.1/foo")) 78 | assert_equal "/foo", req.path 79 | assert_equal "203.0.113.1", req['Host'] 80 | 81 | req = Net::HTTP::Get.new(URI("https://203.0.113.1:8000/foo")) 82 | assert_equal "/foo", req.path 83 | assert_equal "203.0.113.1:8000", req['Host'] 84 | 85 | req = Net::HTTP::Get.new(URI("https://[2001:db8::1]:8000/foo")) 86 | assert_equal "/foo", req.path 87 | assert_equal "[2001:db8::1]:8000", req['Host'] 88 | 89 | assert_raise(ArgumentError){ Net::HTTP::Get.new(URI("urn:ietf:rfc:7231")) } 90 | assert_raise(ArgumentError){ Net::HTTP::Get.new(URI("http://")) } 91 | end 92 | 93 | def test_header_set 94 | req = Net::HTTP::Get.new '/' 95 | 96 | assert req.decode_content, 'Bug #7831 - automatically decode content' 97 | 98 | req['accept-encoding'] = 'identity' 99 | 100 | assert_not_predicate req, :decode_content, 101 | 'Bug #7831 - do not decode content if the user overrides' 102 | end if Net::HTTP::HAVE_ZLIB 103 | 104 | def test_update_uri 105 | req = Net::HTTP::Get.new(URI.parse("http://203.0.113.1")) 106 | req.update_uri("test", 8080, false) 107 | assert_equal "203.0.113.1", req.uri.host 108 | assert_equal 8080, req.uri.port 109 | 110 | req = Net::HTTP::Get.new(URI.parse("http://203.0.113.1:2020")) 111 | req.update_uri("test", 8080, false) 112 | assert_equal "203.0.113.1", req.uri.host 113 | assert_equal 8080, req.uri.port 114 | 115 | req = Net::HTTP::Get.new(URI.parse("http://[2001:db8::1]")) 116 | req.update_uri("test", 8080, false) 117 | assert_equal "[2001:db8::1]", req.uri.host 118 | assert_equal 8080, req.uri.port 119 | 120 | req = Net::HTTP::Get.new(URI.parse("http://[2001:db8::1]:2020")) 121 | req.update_uri("test", 8080, false) 122 | assert_equal "[2001:db8::1]", req.uri.host 123 | assert_equal 8080, req.uri.port 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /test/net/http/test_https.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require "test/unit" 3 | require_relative "utils" 4 | begin 5 | require 'net/https' 6 | rescue LoadError 7 | # should skip this test 8 | end 9 | 10 | return unless defined?(OpenSSL::SSL) 11 | 12 | class TestNetHTTPS < Test::Unit::TestCase 13 | include TestNetHTTPUtils 14 | 15 | def self.read_fixture(key) 16 | File.read(File.expand_path("../fixtures/#{key}", __dir__)) 17 | end 18 | 19 | HOST = 'localhost' 20 | HOST_IP = '127.0.0.1' 21 | CA_CERT = OpenSSL::X509::Certificate.new(read_fixture("cacert.pem")) 22 | SERVER_KEY = OpenSSL::PKey.read(read_fixture("server.key")) 23 | SERVER_CERT = OpenSSL::X509::Certificate.new(read_fixture("server.crt")) 24 | TEST_STORE = OpenSSL::X509::Store.new.tap {|s| s.add_cert(CA_CERT) } 25 | 26 | CONFIG = { 27 | 'host' => HOST, 28 | 'proxy_host' => nil, 29 | 'proxy_port' => nil, 30 | 'ssl_enable' => true, 31 | 'ssl_certificate' => SERVER_CERT, 32 | 'ssl_private_key' => SERVER_KEY, 33 | } 34 | 35 | def test_get 36 | http = Net::HTTP.new(HOST, config("port")) 37 | http.use_ssl = true 38 | http.cert_store = TEST_STORE 39 | http.request_get("/") {|res| 40 | assert_equal($test_net_http_data, res.body) 41 | assert_equal(SERVER_CERT.to_der, http.peer_cert.to_der) 42 | } 43 | end 44 | 45 | def test_get_SNI 46 | http = Net::HTTP.new(HOST, config("port")) 47 | http.ipaddr = config('host') 48 | http.use_ssl = true 49 | http.cert_store = TEST_STORE 50 | http.request_get("/") {|res| 51 | assert_equal($test_net_http_data, res.body) 52 | assert_equal(SERVER_CERT.to_der, http.peer_cert.to_der) 53 | } 54 | end 55 | 56 | def test_get_SNI_proxy 57 | TCPServer.open(HOST_IP, 0) {|serv| 58 | _, port, _, _ = serv.addr 59 | client_thread = Thread.new { 60 | proxy = Net::HTTP.Proxy(HOST_IP, port, 'user', 'password') 61 | http = proxy.new("foo.example.org", 8000) 62 | http.ipaddr = "192.0.2.1" 63 | http.use_ssl = true 64 | http.cert_store = TEST_STORE 65 | begin 66 | http.start 67 | rescue EOFError 68 | end 69 | } 70 | server_thread = Thread.new { 71 | sock = serv.accept 72 | begin 73 | proxy_request = sock.gets("\r\n\r\n") 74 | assert_equal( 75 | "CONNECT 192.0.2.1:8000 HTTP/1.1\r\n" + 76 | "Host: foo.example.org:8000\r\n" + 77 | "Proxy-Authorization: Basic dXNlcjpwYXNzd29yZA==\r\n" + 78 | "\r\n", 79 | proxy_request, 80 | "[ruby-dev:25673]") 81 | ensure 82 | sock.close 83 | end 84 | } 85 | assert_join_threads([client_thread, server_thread]) 86 | } 87 | 88 | end 89 | 90 | def test_get_SNI_failure 91 | TestNetHTTPUtils.clean_http_proxy_env do 92 | http = Net::HTTP.new("invalidservername", config("port")) 93 | http.ipaddr = config('host') 94 | http.use_ssl = true 95 | http.cert_store = TEST_STORE 96 | @log_tester = lambda {|_| } 97 | assert_raise(OpenSSL::SSL::SSLError){ http.start } 98 | end 99 | end 100 | 101 | def test_post 102 | http = Net::HTTP.new(HOST, config("port")) 103 | http.use_ssl = true 104 | http.cert_store = TEST_STORE 105 | data = config('ssl_private_key').to_der 106 | http.request_post("/", data, {'content-type' => 'application/x-www-form-urlencoded'}) {|res| 107 | assert_equal(data, res.body) 108 | } 109 | end 110 | 111 | def test_session_reuse 112 | http = Net::HTTP.new(HOST, config("port")) 113 | http.use_ssl = true 114 | http.cert_store = TEST_STORE 115 | 116 | if OpenSSL::OPENSSL_LIBRARY_VERSION =~ /LibreSSL (\d+\.\d+)/ && $1.to_f > 3.19 117 | # LibreSSL 3.2 defaults to TLSv1.3 in server and client, which doesn't currently 118 | # support session resuse. Limiting the version to the TLSv1.2 stack allows 119 | # this test to continue to work on LibreSSL 3.2+. LibreSSL may eventually 120 | # support session reuse, but there are no current plans to do so. 121 | http.ssl_version = :TLSv1_2 122 | end 123 | 124 | http.start 125 | session_reused = http.instance_variable_get(:@socket).io.session_reused? 126 | assert_false session_reused unless session_reused.nil? # can not detect re-use under JRuby 127 | http.get("/") 128 | http.finish 129 | 130 | http.start 131 | session_reused = http.instance_variable_get(:@socket).io.session_reused? 132 | assert_true session_reused unless session_reused.nil? # can not detect re-use under JRuby 133 | assert_equal $test_net_http_data, http.get("/").body 134 | http.finish 135 | end 136 | 137 | def test_session_reuse_but_expire 138 | http = Net::HTTP.new(HOST, config("port")) 139 | http.use_ssl = true 140 | http.cert_store = TEST_STORE 141 | 142 | http.ssl_timeout = 1 143 | http.start 144 | http.get("/") 145 | http.finish 146 | sleep 1.25 147 | http.start 148 | http.get("/") 149 | 150 | socket = http.instance_variable_get(:@socket).io 151 | assert_equal false, socket.session_reused?, "NOTE: OpenSSL library version is #{OpenSSL::OPENSSL_LIBRARY_VERSION}" 152 | 153 | http.finish 154 | end 155 | 156 | if ENV["RUBY_OPENSSL_TEST_ALL"] 157 | def test_verify 158 | http = Net::HTTP.new("ssl.netlab.jp", 443) 159 | http.use_ssl = true 160 | assert( 161 | (http.request_head("/"){|res| } rescue false), 162 | "The system may not have default CA certificate store." 163 | ) 164 | end 165 | end 166 | 167 | def test_verify_none 168 | http = Net::HTTP.new(HOST, config("port")) 169 | http.use_ssl = true 170 | http.verify_mode = OpenSSL::SSL::VERIFY_NONE 171 | http.request_get("/") {|res| 172 | assert_equal($test_net_http_data, res.body) 173 | } 174 | end 175 | 176 | def test_skip_hostname_verification 177 | TestNetHTTPUtils.clean_http_proxy_env do 178 | http = Net::HTTP.new('invalidservername', config('port')) 179 | http.ipaddr = config('host') 180 | http.use_ssl = true 181 | http.cert_store = TEST_STORE 182 | http.verify_hostname = false 183 | assert_nothing_raised { http.start } 184 | ensure 185 | http.finish if http&.started? 186 | end 187 | end 188 | 189 | def test_fail_if_verify_hostname_is_true 190 | TestNetHTTPUtils.clean_http_proxy_env do 191 | http = Net::HTTP.new('invalidservername', config('port')) 192 | http.ipaddr = config('host') 193 | http.use_ssl = true 194 | http.cert_store = TEST_STORE 195 | http.verify_hostname = true 196 | @log_tester = lambda { |_| } 197 | assert_raise(OpenSSL::SSL::SSLError) { http.start } 198 | end 199 | end 200 | 201 | def test_certificate_verify_failure 202 | http = Net::HTTP.new(HOST, config("port")) 203 | http.use_ssl = true 204 | ex = assert_raise(OpenSSL::SSL::SSLError){ 205 | http.request_get("/") {|res| } 206 | } 207 | assert_match(/certificate verify failed/, ex.message) 208 | end 209 | 210 | def test_verify_callback 211 | http = Net::HTTP.new(HOST, config("port")) 212 | http.use_ssl = true 213 | http.cert_store = TEST_STORE 214 | certs = [] 215 | http.verify_callback = Proc.new {|preverify_ok, store_ctx| 216 | certs << store_ctx.current_cert 217 | preverify_ok 218 | } 219 | http.request_get("/") {|res| 220 | assert_equal($test_net_http_data, res.body) 221 | } 222 | assert_equal(SERVER_CERT.to_der, certs.last.to_der) 223 | end 224 | 225 | def test_timeout_during_SSL_handshake 226 | bug4246 = "expected the SSL connection to have timed out but have not. [ruby-core:34203]" 227 | 228 | # listen for connections... but deliberately do not complete SSL handshake 229 | TCPServer.open(HOST, 0) {|server| 230 | port = server.addr[1] 231 | 232 | conn = Net::HTTP.new(HOST, port) 233 | conn.use_ssl = true 234 | conn.read_timeout = 0.01 235 | conn.open_timeout = 0.01 236 | 237 | th = Thread.new do 238 | assert_raise(Net::OpenTimeout) { 239 | conn.get('/') 240 | } 241 | end 242 | assert th.join(10), bug4246 243 | } 244 | end 245 | 246 | def test_min_version 247 | http = Net::HTTP.new(HOST, config("port")) 248 | http.use_ssl = true 249 | http.min_version = :TLS1 250 | http.cert_store = TEST_STORE 251 | http.request_get("/") {|res| 252 | assert_equal($test_net_http_data, res.body) 253 | } 254 | end 255 | 256 | def test_max_version 257 | http = Net::HTTP.new(HOST, config("port")) 258 | http.use_ssl = true 259 | http.max_version = :SSL2 260 | http.cert_store = TEST_STORE 261 | @log_tester = lambda {|_| } 262 | ex = assert_raise(OpenSSL::SSL::SSLError){ 263 | http.request_get("/") {|res| } 264 | } 265 | re_msg = /\ASSL_connect returned=1 errno=0 |SSL_CTX_set_max_proto_version|No appropriate protocol/ 266 | assert_match(re_msg, ex.message) 267 | end 268 | 269 | end 270 | 271 | class TestNetHTTPSIdentityVerifyFailure < Test::Unit::TestCase 272 | include TestNetHTTPUtils 273 | 274 | def self.read_fixture(key) 275 | File.read(File.expand_path("../fixtures/#{key}", __dir__)) 276 | end 277 | 278 | HOST = 'localhost' 279 | HOST_IP = '127.0.0.1' 280 | CA_CERT = OpenSSL::X509::Certificate.new(read_fixture("cacert.pem")) 281 | SERVER_KEY = OpenSSL::PKey.read(read_fixture("server.key")) 282 | SERVER_CERT = OpenSSL::X509::Certificate.new(read_fixture("server.crt")) 283 | TEST_STORE = OpenSSL::X509::Store.new.tap {|s| s.add_cert(CA_CERT) } 284 | 285 | CONFIG = { 286 | 'host' => HOST_IP, 287 | 'proxy_host' => nil, 288 | 'proxy_port' => nil, 289 | 'ssl_enable' => true, 290 | 'ssl_certificate' => SERVER_CERT, 291 | 'ssl_private_key' => SERVER_KEY, 292 | } 293 | 294 | def test_identity_verify_failure 295 | # the certificate's subject has CN=localhost 296 | http = Net::HTTP.new(HOST_IP, config("port")) 297 | http.use_ssl = true 298 | http.cert_store = TEST_STORE 299 | @log_tester = lambda {|_| } 300 | ex = assert_raise(OpenSSL::SSL::SSLError){ 301 | http.request_get("/") {|res| } 302 | sleep 0.5 303 | } 304 | re_msg = /certificate verify failed|hostname \"#{HOST_IP}\" does not match/ 305 | assert_match(re_msg, ex.message) 306 | end 307 | end 308 | -------------------------------------------------------------------------------- /test/net/http/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'socket' 3 | 4 | module TestNetHTTPUtils 5 | 6 | class Forbidden < StandardError; end 7 | 8 | class HTTPServer 9 | def initialize(config, &block) 10 | @config = config 11 | @server = TCPServer.new(@config['host'], 0) 12 | @port = @server.addr[1] 13 | @procs = {} 14 | 15 | if @config['ssl_enable'] 16 | require 'openssl' 17 | context = OpenSSL::SSL::SSLContext.new 18 | context.cert = @config['ssl_certificate'] 19 | context.key = @config['ssl_private_key'] 20 | @ssl_server = OpenSSL::SSL::SSLServer.new(@server, context) 21 | end 22 | 23 | @block = block 24 | end 25 | 26 | def start 27 | @thread = Thread.new do 28 | loop do 29 | socket = (@ssl_server || @server).accept 30 | run(socket) 31 | rescue 32 | ensure 33 | socket&.close 34 | end 35 | ensure 36 | (@ssl_server || @server).close 37 | end 38 | end 39 | 40 | def run(socket) 41 | handle_request(socket) 42 | end 43 | 44 | def shutdown 45 | @thread&.kill 46 | @thread&.join 47 | end 48 | 49 | def mount(path, proc) 50 | @procs[path] = proc 51 | end 52 | 53 | def mount_proc(path, &block) 54 | mount(path, block.to_proc) 55 | end 56 | 57 | def handle_request(socket) 58 | request_line = socket.gets 59 | return if request_line.nil? || request_line.strip.empty? 60 | 61 | method, path, _version = request_line.split 62 | headers = {} 63 | while (line = socket.gets) 64 | break if line.strip.empty? 65 | key, value = line.split(': ', 2) 66 | headers[key] = value.strip 67 | end 68 | 69 | if headers['Expect'] == '100-continue' 70 | socket.write "HTTP/1.1 100 Continue\r\n\r\n" 71 | end 72 | 73 | # Set default Content-Type if not provided 74 | if !headers['Content-Type'] && (method == 'POST' || method == 'PUT' || method == 'PATCH') 75 | headers['Content-Type'] = 'application/octet-stream' 76 | end 77 | 78 | req = Request.new(method, path, headers, socket) 79 | if @procs.key?(req.path) || @procs.key?("#{req.path}/") 80 | proc = @procs[req.path] || @procs["#{req.path}/"] 81 | res = Response.new(socket) 82 | begin 83 | proc.call(req, res) 84 | rescue Forbidden 85 | res.status = 403 86 | end 87 | res.finish 88 | else 89 | @block.call(method, path, headers, socket) 90 | end 91 | end 92 | 93 | def port 94 | @port 95 | end 96 | 97 | class Request 98 | attr_reader :method, :path, :headers, :query, :body 99 | def initialize(method, path, headers, socket) 100 | @method = method 101 | @path, @query = parse_path_and_query(path) 102 | @headers = headers 103 | @socket = socket 104 | if method == 'POST' && (@path == '/continue' || @headers['Content-Type'].include?('multipart/form-data')) 105 | if @headers['Transfer-Encoding'] == 'chunked' 106 | @body = read_chunked_body 107 | else 108 | @body = read_body 109 | end 110 | @query = @body.split('&').each_with_object({}) do |pair, hash| 111 | key, value = pair.split('=') 112 | hash[key] = value 113 | end if @body && @body.include?('=') 114 | end 115 | end 116 | 117 | def [](key) 118 | @headers[key.downcase] 119 | end 120 | 121 | def []=(key, value) 122 | @headers[key.downcase] = value 123 | end 124 | 125 | def continue 126 | @socket.write "HTTP\/1.1 100 continue\r\n\r\n" 127 | end 128 | 129 | def remote_ip 130 | @socket.peeraddr[3] 131 | end 132 | 133 | def peeraddr 134 | @socket.peeraddr 135 | end 136 | 137 | private 138 | 139 | def parse_path_and_query(path) 140 | path, query_string = path.split('?', 2) 141 | query = {} 142 | if query_string 143 | query_string.split('&').each do |pair| 144 | key, value = pair.split('=', 2) 145 | query[key] = value 146 | end 147 | end 148 | [path, query] 149 | end 150 | 151 | def read_body 152 | content_length = @headers['Content-Length']&.to_i 153 | return unless content_length && content_length > 0 154 | @socket.read(content_length) 155 | end 156 | 157 | def read_chunked_body 158 | body = "" 159 | while (chunk_size = @socket.gets.strip.to_i(16)) > 0 160 | body << @socket.read(chunk_size) 161 | @socket.read(2) # read \r\n after each chunk 162 | end 163 | body 164 | end 165 | end 166 | 167 | class Response 168 | attr_accessor :body, :headers, :status, :chunked, :cookies 169 | def initialize(client) 170 | @client = client 171 | @body = "" 172 | @headers = {} 173 | @status = 200 174 | @chunked = false 175 | @cookies = [] 176 | end 177 | 178 | def [](key) 179 | @headers[key.downcase] 180 | end 181 | 182 | def []=(key, value) 183 | @headers[key.downcase] = value 184 | end 185 | 186 | def write_chunk(chunk) 187 | return unless @chunked 188 | @client.write("#{chunk.bytesize.to_s(16)}\r\n") 189 | @client.write("#{chunk}\r\n") 190 | end 191 | 192 | def finish 193 | @client.write build_response_headers 194 | if @chunked 195 | write_chunk(@body) 196 | @client.write "0\r\n\r\n" 197 | else 198 | @client.write @body 199 | end 200 | end 201 | 202 | private 203 | 204 | def build_response_headers 205 | response = "HTTP/1.1 #{@status} #{status_message(@status)}\r\n" 206 | if @chunked 207 | @headers['Transfer-Encoding'] = 'chunked' 208 | else 209 | @headers['Content-Length'] = @body.bytesize.to_s 210 | end 211 | @headers.each do |key, value| 212 | response << "#{key}: #{value}\r\n" 213 | end 214 | @cookies.each do |cookie| 215 | response << "Set-Cookie: #{cookie}\r\n" 216 | end 217 | response << "\r\n" 218 | response 219 | end 220 | 221 | def status_message(code) 222 | case code 223 | when 200 then 'OK' 224 | when 301 then 'Moved Permanently' 225 | when 403 then 'Forbidden' 226 | else 'Unknown' 227 | end 228 | end 229 | end 230 | end 231 | 232 | def start(&block) 233 | new().start(&block) 234 | end 235 | 236 | def new 237 | klass = Net::HTTP::Proxy(config('proxy_host'), config('proxy_port')) 238 | http = klass.new(config('host'), config('port')) 239 | http.set_debug_output logfile 240 | http 241 | end 242 | 243 | def config(key) 244 | @config ||= self.class::CONFIG 245 | @config[key] 246 | end 247 | 248 | def logfile 249 | $stderr if $DEBUG 250 | end 251 | 252 | def setup 253 | spawn_server 254 | end 255 | 256 | def teardown 257 | sleep 0.5 if @config['ssl_enable'] 258 | if @server 259 | @server.shutdown 260 | end 261 | @log_tester.call(@log) if @log_tester 262 | Net::HTTP.version_1_2 263 | end 264 | 265 | def spawn_server 266 | @log = [] 267 | @log_tester = lambda {|log| assert_equal([], log) } 268 | @config = self.class::CONFIG 269 | @server = HTTPServer.new(@config) do |method, path, headers, socket| 270 | @log << "DEBUG accept: #{@config['host']}:#{socket.addr[1]}" if @logger_level == :debug 271 | case method 272 | when 'HEAD' 273 | handle_head(path, headers, socket) 274 | when 'GET' 275 | handle_get(path, headers, socket) 276 | when 'POST' 277 | handle_post(path, headers, socket) 278 | when 'PATCH' 279 | handle_patch(path, headers, socket) 280 | else 281 | socket.print "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\n\r\n" 282 | end 283 | end 284 | @server.start 285 | @config['port'] = @server.port 286 | end 287 | 288 | def handle_head(path, headers, socket) 289 | if headers['Accept'] != '*/*' 290 | content_type = headers['Accept'] 291 | else 292 | content_type = $test_net_http_data_type 293 | end 294 | response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{$test_net_http_data.bytesize}" 295 | socket.print(response) 296 | end 297 | 298 | def handle_get(path, headers, socket) 299 | if headers['Accept'] != '*/*' 300 | content_type = headers['Accept'] 301 | else 302 | content_type = $test_net_http_data_type 303 | end 304 | 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}" 305 | socket.print(response) 306 | end 307 | 308 | def handle_post(path, headers, socket) 309 | body = socket.read(headers['Content-Length'].to_i) 310 | scheme = headers['X-Request-Scheme'] || 'http' 311 | host = @config['host'] 312 | port = socket.addr[1] 313 | content_type = headers['Content-Type'] || 'application/octet-stream' 314 | charset = parse_content_type(content_type)[1] 315 | path = "#{scheme}://#{host}:#{port}#{path}" 316 | path = path.encode(charset) if charset 317 | 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}" 318 | socket.print(response) 319 | end 320 | 321 | def handle_patch(path, headers, socket) 322 | body = socket.read(headers['Content-Length'].to_i) 323 | content_type = headers['Content-Type'] || 'application/octet-stream' 324 | response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}" 325 | socket.print(response) 326 | end 327 | 328 | def parse_content_type(content_type) 329 | return [nil, nil] unless content_type 330 | type, *params = content_type.split(';').map(&:strip) 331 | charset = params.find { |param| param.start_with?('charset=') } 332 | charset = charset.split('=', 2).last if charset 333 | [type, charset] 334 | end 335 | 336 | $test_net_http = nil 337 | $test_net_http_data = (0...256).to_a.map { |i| i.chr }.join('') * 64 338 | $test_net_http_data.force_encoding("ASCII-8BIT") 339 | $test_net_http_data_type = 'application/octet-stream' 340 | 341 | def self.clean_http_proxy_env 342 | orig = { 343 | 'http_proxy' => ENV['http_proxy'], 344 | 'http_proxy_user' => ENV['http_proxy_user'], 345 | 'http_proxy_pass' => ENV['http_proxy_pass'], 346 | 'no_proxy' => ENV['no_proxy'], 347 | } 348 | 349 | orig.each_key do |key| 350 | ENV.delete key 351 | end 352 | 353 | yield 354 | ensure 355 | orig.each do |key, value| 356 | ENV[key] = value 357 | end 358 | end 359 | end 360 | -------------------------------------------------------------------------------- /lib/net/http/generic_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # \HTTPGenericRequest is the parent of the Net::HTTPRequest class. 4 | # 5 | # Do not use this directly; instead, use a subclass of Net::HTTPRequest. 6 | # 7 | # == About the Examples 8 | # 9 | # :include: doc/net-http/examples.rdoc 10 | # 11 | class Net::HTTPGenericRequest 12 | 13 | include Net::HTTPHeader 14 | 15 | def initialize(m, reqbody, resbody, uri_or_path, initheader = nil) # :nodoc: 16 | @method = m 17 | @request_has_body = reqbody 18 | @response_has_body = resbody 19 | 20 | if URI === uri_or_path then 21 | raise ArgumentError, "not an HTTP URI" unless URI::HTTP === uri_or_path 22 | hostname = uri_or_path.host 23 | raise ArgumentError, "no host component for URI" unless (hostname && hostname.length > 0) 24 | @uri = uri_or_path.dup 25 | @path = uri_or_path.request_uri 26 | raise ArgumentError, "no HTTP request path given" unless @path 27 | else 28 | @uri = nil 29 | raise ArgumentError, "no HTTP request path given" unless uri_or_path 30 | raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty? 31 | @path = uri_or_path.dup 32 | end 33 | 34 | @decode_content = false 35 | 36 | if Net::HTTP::HAVE_ZLIB then 37 | if !initheader || 38 | !initheader.keys.any? { |k| 39 | %w[accept-encoding range].include? k.downcase 40 | } then 41 | @decode_content = true if @response_has_body 42 | initheader = initheader ? initheader.dup : {} 43 | initheader["accept-encoding"] = 44 | "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" 45 | end 46 | end 47 | 48 | initialize_http_header initheader 49 | self['Accept'] ||= '*/*' 50 | self['User-Agent'] ||= 'Ruby' 51 | self['Host'] ||= @uri.authority if @uri 52 | @body = nil 53 | @body_stream = nil 54 | @body_data = nil 55 | end 56 | 57 | # Returns the string method name for the request: 58 | # 59 | # Net::HTTP::Get.new(uri).method # => "GET" 60 | # Net::HTTP::Post.new(uri).method # => "POST" 61 | # 62 | attr_reader :method 63 | 64 | # Returns the string path for the request: 65 | # 66 | # Net::HTTP::Get.new(uri).path # => "/" 67 | # Net::HTTP::Post.new('example.com').path # => "example.com" 68 | # 69 | attr_reader :path 70 | 71 | # Returns the URI object for the request, or +nil+ if none: 72 | # 73 | # Net::HTTP::Get.new(uri).uri 74 | # # => # 75 | # Net::HTTP::Get.new('example.com').uri # => nil 76 | # 77 | attr_reader :uri 78 | 79 | # Returns +false+ if the request's header 'Accept-Encoding' 80 | # has been set manually or deleted 81 | # (indicating that the user intends to handle encoding in the response), 82 | # +true+ otherwise: 83 | # 84 | # req = Net::HTTP::Get.new(uri) # => # 85 | # req['Accept-Encoding'] # => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" 86 | # req.decode_content # => true 87 | # req['Accept-Encoding'] = 'foo' 88 | # req.decode_content # => false 89 | # req.delete('Accept-Encoding') 90 | # req.decode_content # => false 91 | # 92 | attr_reader :decode_content 93 | 94 | # Returns a string representation of the request: 95 | # 96 | # Net::HTTP::Post.new(uri).inspect # => "#" 97 | # 98 | def inspect 99 | "\#<#{self.class} #{@method}>" 100 | end 101 | 102 | # Returns a string representation of the request with the details for pp: 103 | # 104 | # require 'pp' 105 | # post = Net::HTTP::Post.new(uri) 106 | # post.inspect # => "#" 107 | # post.pretty_inspect 108 | # # => # ["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"], 112 | # "accept" => ["*/*"], 113 | # "user-agent" => ["Ruby"], 114 | # "host" => ["www.ruby-lang.org"]}> 115 | # 116 | def pretty_print(q) 117 | q.object_group(self) { 118 | q.breakable 119 | q.text @method 120 | q.breakable 121 | q.text "path="; q.pp @path 122 | q.breakable 123 | q.text "headers="; q.pp to_hash 124 | } 125 | end 126 | 127 | ## 128 | # Don't automatically decode response content-encoding if the user indicates 129 | # they want to handle it. 130 | 131 | def []=(key, val) # :nodoc: 132 | @decode_content = false if key.downcase == 'accept-encoding' 133 | 134 | super key, val 135 | end 136 | 137 | # Returns whether the request may have a body: 138 | # 139 | # Net::HTTP::Post.new(uri).request_body_permitted? # => true 140 | # Net::HTTP::Get.new(uri).request_body_permitted? # => false 141 | # 142 | def request_body_permitted? 143 | @request_has_body 144 | end 145 | 146 | # Returns whether the response may have a body: 147 | # 148 | # Net::HTTP::Post.new(uri).response_body_permitted? # => true 149 | # Net::HTTP::Head.new(uri).response_body_permitted? # => false 150 | # 151 | def response_body_permitted? 152 | @response_has_body 153 | end 154 | 155 | def body_exist? # :nodoc: 156 | warn "Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE 157 | response_body_permitted? 158 | end 159 | 160 | # Returns the string body for the request, or +nil+ if there is none: 161 | # 162 | # req = Net::HTTP::Post.new(uri) 163 | # req.body # => nil 164 | # req.body = '{"title": "foo","body": "bar","userId": 1}' 165 | # req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}" 166 | # 167 | attr_reader :body 168 | 169 | # Sets the body for the request: 170 | # 171 | # req = Net::HTTP::Post.new(uri) 172 | # req.body # => nil 173 | # req.body = '{"title": "foo","body": "bar","userId": 1}' 174 | # req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}" 175 | # 176 | def body=(str) 177 | @body = str 178 | @body_stream = nil 179 | @body_data = nil 180 | str 181 | end 182 | 183 | # Returns the body stream object for the request, or +nil+ if there is none: 184 | # 185 | # req = Net::HTTP::Post.new(uri) # => # 186 | # req.body_stream # => nil 187 | # require 'stringio' 188 | # req.body_stream = StringIO.new('xyzzy') # => # 189 | # req.body_stream # => # 190 | # 191 | attr_reader :body_stream 192 | 193 | # Sets the body stream for the request: 194 | # 195 | # req = Net::HTTP::Post.new(uri) # => # 196 | # req.body_stream # => nil 197 | # require 'stringio' 198 | # req.body_stream = StringIO.new('xyzzy') # => # 199 | # req.body_stream # => # 200 | # 201 | def body_stream=(input) 202 | @body = nil 203 | @body_stream = input 204 | @body_data = nil 205 | input 206 | end 207 | 208 | def set_body_internal(str) #:nodoc: internal use only 209 | raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream) 210 | self.body = str if str 211 | if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted? 212 | self.body = '' 213 | end 214 | end 215 | 216 | # 217 | # write 218 | # 219 | 220 | def exec(sock, ver, path) #:nodoc: internal use only 221 | if @body 222 | send_request_with_body sock, ver, path, @body 223 | elsif @body_stream 224 | send_request_with_body_stream sock, ver, path, @body_stream 225 | elsif @body_data 226 | send_request_with_body_data sock, ver, path, @body_data 227 | else 228 | write_header sock, ver, path 229 | end 230 | end 231 | 232 | def update_uri(addr, port, ssl) # :nodoc: internal use only 233 | # reflect the connection and @path to @uri 234 | return unless @uri 235 | 236 | if ssl 237 | scheme = 'https' 238 | klass = URI::HTTPS 239 | else 240 | scheme = 'http' 241 | klass = URI::HTTP 242 | end 243 | 244 | if host = self['host'] 245 | host = URI.parse("//#{host}").host # Remove a port component from the existing Host header 246 | elsif host = @uri.host 247 | else 248 | host = addr 249 | end 250 | # convert the class of the URI 251 | if @uri.is_a?(klass) 252 | @uri.host = host 253 | @uri.port = port 254 | else 255 | @uri = klass.new( 256 | scheme, @uri.userinfo, 257 | host, port, nil, 258 | @uri.path, nil, @uri.query, nil) 259 | end 260 | end 261 | 262 | private 263 | 264 | # :stopdoc: 265 | 266 | class Chunker #:nodoc: 267 | def initialize(sock) 268 | @sock = sock 269 | @prev = nil 270 | end 271 | 272 | def write(buf) 273 | # avoid memcpy() of buf, buf can huge and eat memory bandwidth 274 | rv = buf.bytesize 275 | @sock.write("#{rv.to_s(16)}\r\n", buf, "\r\n") 276 | rv 277 | end 278 | 279 | def finish 280 | @sock.write("0\r\n\r\n") 281 | end 282 | end 283 | 284 | def send_request_with_body(sock, ver, path, body) 285 | self.content_length = body.bytesize 286 | delete 'Transfer-Encoding' 287 | write_header sock, ver, path 288 | wait_for_continue sock, ver if sock.continue_timeout 289 | sock.write body 290 | end 291 | 292 | def send_request_with_body_stream(sock, ver, path, f) 293 | unless content_length() or chunked? 294 | raise ArgumentError, 295 | "Content-Length not given and Transfer-Encoding is not `chunked'" 296 | end 297 | write_header sock, ver, path 298 | wait_for_continue sock, ver if sock.continue_timeout 299 | if chunked? 300 | chunker = Chunker.new(sock) 301 | IO.copy_stream(f, chunker) 302 | chunker.finish 303 | else 304 | IO.copy_stream(f, sock) 305 | end 306 | end 307 | 308 | def send_request_with_body_data(sock, ver, path, params) 309 | if /\Amultipart\/form-data\z/i !~ self.content_type 310 | self.content_type = 'application/x-www-form-urlencoded' 311 | return send_request_with_body(sock, ver, path, URI.encode_www_form(params)) 312 | end 313 | 314 | opt = @form_option.dup 315 | require 'securerandom' unless defined?(SecureRandom) 316 | opt[:boundary] ||= SecureRandom.urlsafe_base64(40) 317 | self.set_content_type(self.content_type, boundary: opt[:boundary]) 318 | if chunked? 319 | write_header sock, ver, path 320 | encode_multipart_form_data(sock, params, opt) 321 | else 322 | require 'tempfile' 323 | file = Tempfile.new('multipart') 324 | file.binmode 325 | encode_multipart_form_data(file, params, opt) 326 | file.rewind 327 | self.content_length = file.size 328 | write_header sock, ver, path 329 | IO.copy_stream(file, sock) 330 | file.close(true) 331 | end 332 | end 333 | 334 | def encode_multipart_form_data(out, params, opt) 335 | charset = opt[:charset] 336 | boundary = opt[:boundary] 337 | require 'securerandom' unless defined?(SecureRandom) 338 | boundary ||= SecureRandom.urlsafe_base64(40) 339 | chunked_p = chunked? 340 | 341 | buf = +'' 342 | params.each do |key, value, h={}| 343 | key = quote_string(key, charset) 344 | filename = 345 | h.key?(:filename) ? h[:filename] : 346 | value.respond_to?(:to_path) ? File.basename(value.to_path) : 347 | nil 348 | 349 | buf << "--#{boundary}\r\n" 350 | if filename 351 | filename = quote_string(filename, charset) 352 | type = h[:content_type] || 'application/octet-stream' 353 | buf << "Content-Disposition: form-data; " \ 354 | "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \ 355 | "Content-Type: #{type}\r\n\r\n" 356 | if !out.respond_to?(:write) || !value.respond_to?(:read) 357 | # if +out+ is not an IO or +value+ is not an IO 358 | buf << (value.respond_to?(:read) ? value.read : value) 359 | elsif value.respond_to?(:size) && chunked_p 360 | # if +out+ is an IO and +value+ is a File, use IO.copy_stream 361 | flush_buffer(out, buf, chunked_p) 362 | out << "%x\r\n" % value.size if chunked_p 363 | IO.copy_stream(value, out) 364 | out << "\r\n" if chunked_p 365 | else 366 | # +out+ is an IO, and +value+ is not a File but an IO 367 | flush_buffer(out, buf, chunked_p) 368 | 1 while flush_buffer(out, value.read(4096), chunked_p) 369 | end 370 | else 371 | # non-file field: 372 | # HTML5 says, "The parts of the generated multipart/form-data 373 | # resource that correspond to non-file fields must not have a 374 | # Content-Type header specified." 375 | buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n" 376 | buf << (value.respond_to?(:read) ? value.read : value) 377 | end 378 | buf << "\r\n" 379 | end 380 | buf << "--#{boundary}--\r\n" 381 | flush_buffer(out, buf, chunked_p) 382 | out << "0\r\n\r\n" if chunked_p 383 | end 384 | 385 | def quote_string(str, charset) 386 | str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset 387 | str.gsub(/[\\"]/, '\\\\\&') 388 | end 389 | 390 | def flush_buffer(out, buf, chunked_p) 391 | return unless buf 392 | out << "%x\r\n"%buf.bytesize if chunked_p 393 | out << buf 394 | out << "\r\n" if chunked_p 395 | buf.clear 396 | end 397 | 398 | ## 399 | # Waits up to the continue timeout for a response from the server provided 400 | # we're speaking HTTP 1.1 and are expecting a 100-continue response. 401 | 402 | def wait_for_continue(sock, ver) 403 | if ver >= '1.1' and @header['expect'] and 404 | @header['expect'].include?('100-continue') 405 | if sock.io.to_io.wait_readable(sock.continue_timeout) 406 | res = Net::HTTPResponse.read_new(sock) 407 | unless res.kind_of?(Net::HTTPContinue) 408 | res.decode_content = @decode_content 409 | throw :response, res 410 | end 411 | end 412 | end 413 | end 414 | 415 | def write_header(sock, ver, path) 416 | reqline = "#{@method} #{path} HTTP/#{ver}" 417 | if /[\r\n]/ =~ reqline 418 | raise ArgumentError, "A Request-Line must not contain CR or LF" 419 | end 420 | buf = +'' 421 | buf << reqline << "\r\n" 422 | each_capitalized do |k,v| 423 | buf << "#{k}: #{v}\r\n" 424 | end 425 | buf << "\r\n" 426 | sock.write buf 427 | end 428 | 429 | end 430 | -------------------------------------------------------------------------------- /test/net/http/test_httpheader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'net/http' 3 | require 'test/unit' 4 | 5 | class HTTPHeaderTest < Test::Unit::TestCase 6 | 7 | class C 8 | include Net::HTTPHeader 9 | def initialize 10 | initialize_http_header({}) 11 | end 12 | attr_accessor :body 13 | end 14 | 15 | def setup 16 | @c = C.new 17 | end 18 | 19 | def test_initialize 20 | @c.initialize_http_header("foo"=>"abc") 21 | assert_equal "abc", @c["foo"] 22 | @c.initialize_http_header("foo"=>"abc", "bar"=>"xyz") 23 | assert_equal "xyz", @c["bar"] 24 | @c.initialize_http_header([["foo", "abc"]]) 25 | assert_equal "abc", @c["foo"] 26 | @c.initialize_http_header([["foo", "abc"], ["bar","xyz"]]) 27 | assert_equal "xyz", @c["bar"] 28 | assert_raise(NoMethodError){ @c.initialize_http_header("foo"=>[]) } 29 | assert_raise(ArgumentError){ @c.initialize_http_header("foo"=>"a\nb") } 30 | assert_raise(ArgumentError){ @c.initialize_http_header("foo"=>"a\rb") } 31 | end 32 | 33 | def test_initialize_with_broken_coderange 34 | error = RUBY_VERSION >= "3.2" ? Encoding::CompatibilityError : ArgumentError 35 | assert_raise(error){ @c.initialize_http_header("foo"=>"a\xff") } 36 | end 37 | 38 | def test_initialize_with_symbol 39 | @c.initialize_http_header(foo: "abc") 40 | assert_equal "abc", @c["foo"] 41 | end 42 | 43 | def test_size 44 | assert_equal 0, @c.size 45 | @c['a'] = 'a' 46 | assert_equal 1, @c.size 47 | @c['b'] = 'b' 48 | assert_equal 2, @c.size 49 | @c['b'] = 'b' 50 | assert_equal 2, @c.size 51 | @c['c'] = 'c' 52 | assert_equal 3, @c.size 53 | end 54 | 55 | def test_ASET 56 | @c['My-Header'] = 'test string' 57 | @c['my-Header'] = 'test string' 58 | @c['My-header'] = 'test string' 59 | @c['my-header'] = 'test string' 60 | @c['MY-HEADER'] = 'test string' 61 | assert_equal 1, @c.size 62 | 63 | @c['AaA'] = 'aaa' 64 | @c['aaA'] = 'aaa' 65 | @c['AAa'] = 'aaa' 66 | assert_equal 2, @c.length 67 | 68 | @c['aaa'] = ['aaa', ['bbb', [3]]] 69 | assert_equal 2, @c.length 70 | assert_equal ['aaa', 'bbb', '3'], @c.get_fields('aaa') 71 | 72 | @c['aaa'] = "aaa\xff" 73 | assert_equal 2, @c.length 74 | 75 | assert_raise(ArgumentError){ @c['foo'] = "a\nb" } 76 | assert_raise(ArgumentError){ @c['foo'] = ["a\nb"] } 77 | end 78 | 79 | def test_AREF 80 | @c['My-Header'] = 'test string' 81 | assert_equal 'test string', @c['my-header'] 82 | assert_equal 'test string', @c['MY-header'] 83 | assert_equal 'test string', @c['my-HEADER'] 84 | 85 | @c['Next-Header'] = 'next string' 86 | assert_equal 'next string', @c['next-header'] 87 | end 88 | 89 | def test_add_field 90 | @c.add_field 'My-Header', 'a' 91 | assert_equal 'a', @c['My-Header'] 92 | assert_equal ['a'], @c.get_fields('My-Header') 93 | @c.add_field 'My-Header', 'b' 94 | assert_equal 'a, b', @c['My-Header'] 95 | assert_equal ['a', 'b'], @c.get_fields('My-Header') 96 | @c.add_field 'My-Header', 'c' 97 | assert_equal 'a, b, c', @c['My-Header'] 98 | assert_equal ['a', 'b', 'c'], @c.get_fields('My-Header') 99 | @c.add_field 'My-Header', 'd, d' 100 | assert_equal 'a, b, c, d, d', @c['My-Header'] 101 | assert_equal ['a', 'b', 'c', 'd, d'], @c.get_fields('My-Header') 102 | assert_raise(ArgumentError){ @c.add_field 'My-Header', "d\nd" } 103 | @c.add_field 'My-Header', ['e', ["\xff", 7]] 104 | assert_equal "a, b, c, d, d, e, \xff, 7", @c['My-Header'] 105 | assert_equal ['a', 'b', 'c', 'd, d', 'e', "\xff", '7'], @c.get_fields('My-Header') 106 | end 107 | 108 | def test_get_fields 109 | @c['My-Header'] = 'test string' 110 | assert_equal ['test string'], @c.get_fields('my-header') 111 | assert_equal ['test string'], @c.get_fields('My-header') 112 | assert_equal ['test string'], @c.get_fields('my-Header') 113 | 114 | assert_nil @c.get_fields('not-found') 115 | assert_nil @c.get_fields('Not-Found') 116 | 117 | @c.get_fields('my-header').push 'junk' 118 | assert_equal ['test string'], @c.get_fields('my-header') 119 | @c.get_fields('my-header').clear 120 | assert_equal ['test string'], @c.get_fields('my-header') 121 | end 122 | 123 | class D; include Net::HTTPHeader; end 124 | 125 | def test_nil_variable_header 126 | assert_nothing_raised do 127 | assert_warning("#{__FILE__}:#{__LINE__+1}: warning: net/http: nil HTTP header: Authorization\n") do 128 | D.new.initialize_http_header({Authorization: nil}) 129 | end 130 | end 131 | end 132 | 133 | def test_duplicated_variable_header 134 | assert_nothing_raised do 135 | assert_warning("#{__FILE__}:#{__LINE__+1}: warning: net/http: duplicated HTTP header: Authorization\n") do 136 | D.new.initialize_http_header({"AUTHORIZATION": "yes", "Authorization": "no"}) 137 | end 138 | end 139 | end 140 | 141 | def test_delete 142 | @c['My-Header'] = 'test' 143 | assert_equal 'test', @c['My-Header'] 144 | assert_nil @c['not-found'] 145 | @c.delete 'My-Header' 146 | assert_nil @c['My-Header'] 147 | assert_nil @c['not-found'] 148 | @c.delete 'My-Header' 149 | @c.delete 'My-Header' 150 | assert_nil @c['My-Header'] 151 | assert_nil @c['not-found'] 152 | end 153 | 154 | def test_each 155 | @c['My-Header'] = 'test' 156 | @c.each do |k, v| 157 | assert_equal 'my-header', k 158 | assert_equal 'test', v 159 | end 160 | @c.each do |k, v| 161 | assert_equal 'my-header', k 162 | assert_equal 'test', v 163 | end 164 | e = @c.each 165 | assert_equal 1, e.size 166 | e.each do |k, v| 167 | assert_equal 'my-header', k 168 | assert_equal 'test', v 169 | end 170 | end 171 | 172 | def test_each_key 173 | @c['My-Header'] = 'test' 174 | @c.each_key do |k| 175 | assert_equal 'my-header', k 176 | end 177 | @c.each_key do |k| 178 | assert_equal 'my-header', k 179 | end 180 | e = @c.each_key 181 | assert_equal 1, e.size 182 | e.each do |k| 183 | assert_equal 'my-header', k 184 | end 185 | end 186 | 187 | def test_each_capitalized_name 188 | @c['my-header'] = 'test' 189 | @c.each_capitalized_name do |k| 190 | assert_equal 'My-Header', k 191 | end 192 | @c.each_capitalized_name do |k| 193 | assert_equal 'My-Header', k 194 | end 195 | e = @c.each_capitalized_name 196 | assert_equal 1, e.size 197 | e.each do |k| 198 | assert_equal 'My-Header', k 199 | end 200 | end 201 | 202 | def test_each_value 203 | @c['My-Header'] = 'test' 204 | @c.each_value do |v| 205 | assert_equal 'test', v 206 | end 207 | @c.each_value do |v| 208 | assert_equal 'test', v 209 | end 210 | e = @c.each_value 211 | assert_equal 1, e.size 212 | e.each do |v| 213 | assert_equal 'test', v 214 | end 215 | end 216 | 217 | def test_canonical_each 218 | @c['my-header'] = ['a', 'b'] 219 | @c.canonical_each do |k,v| 220 | assert_equal 'My-Header', k 221 | assert_equal 'a, b', v 222 | end 223 | e = @c.canonical_each 224 | assert_equal 1, e.size 225 | e.each do |k,v| 226 | assert_equal 'My-Header', k 227 | assert_equal 'a, b', v 228 | end 229 | end 230 | 231 | def test_each_capitalized 232 | @c['my-header'] = ['a', 'b'] 233 | @c.each_capitalized do |k,v| 234 | assert_equal 'My-Header', k 235 | assert_equal 'a, b', v 236 | end 237 | e = @c.each_capitalized 238 | assert_equal 1, e.size 239 | e.each do |k,v| 240 | assert_equal 'My-Header', k 241 | assert_equal 'a, b', v 242 | end 243 | end 244 | 245 | def test_each_capitalized_with_symbol 246 | @c[:my_header] = ['a', 'b'] 247 | @c.each_capitalized do |k,v| 248 | assert_equal "My_header", k 249 | assert_equal 'a, b', v 250 | end 251 | e = @c.each_capitalized 252 | assert_equal 1, e.size 253 | e.each do |k,v| 254 | assert_equal 'My_header', k 255 | assert_equal 'a, b', v 256 | end 257 | end 258 | 259 | def test_key? 260 | @c['My-Header'] = 'test' 261 | assert_equal true, @c.key?('My-Header') 262 | assert_equal true, @c.key?('my-header') 263 | assert_equal false, @c.key?('Not-Found') 264 | assert_equal false, @c.key?('not-found') 265 | assert_equal false, @c.key?('') 266 | assert_equal false, @c.key?('x' * 1024) 267 | end 268 | 269 | def test_to_hash 270 | end 271 | 272 | def test_range 273 | try_range([1..5], '1-5') 274 | try_invalid_range('5-1') 275 | try_range([234..567], '234-567') 276 | try_range([-5..-1], '-5') 277 | try_invalid_range('-0') 278 | try_range([1..-1], '1-') 279 | try_range([0..0,-1..-1], '0-0,-1') 280 | try_range([1..2, 3..4], '1-2,3-4') 281 | try_range([1..2, 3..4], '1-2 , 3-4') 282 | try_range([1..2, 1..4], '1-2,1-4') 283 | 284 | try_invalid_range('invalid') 285 | try_invalid_range(' 12-') 286 | try_invalid_range('12- ') 287 | try_invalid_range('123-abc') 288 | try_invalid_range('abc-123') 289 | end 290 | 291 | def try_range(r, s) 292 | @c['range'] = "bytes=#{s}" 293 | assert_equal r, @c.range 294 | end 295 | 296 | def try_invalid_range(s) 297 | @c['range'] = "bytes=#{s}" 298 | assert_raise(Net::HTTPHeaderSyntaxError, s){ @c.range } 299 | end 300 | 301 | def test_range= 302 | @c.range = 0..499 303 | assert_equal 'bytes=0-499', @c['range'] 304 | @c.range = 0...500 305 | assert_equal 'bytes=0-499', @c['range'] 306 | @c.range = 300 307 | assert_equal 'bytes=0-299', @c['range'] 308 | @c.range = -400 309 | assert_equal 'bytes=-400', @c['range'] 310 | @c.set_range 0, 500 311 | assert_equal 'bytes=0-499', @c['range'] 312 | end 313 | 314 | def test_content_range 315 | @c['Content-Range'] = "bytes 0-499/1000" 316 | assert_equal 0..499, @c.content_range 317 | @c['Content-Range'] = "bytes 1-500/1000" 318 | assert_equal 1..500, @c.content_range 319 | @c['Content-Range'] = "bytes 1-1/1000" 320 | assert_equal 1..1, @c.content_range 321 | @c['Content-Range'] = "tokens 1-1/1000" 322 | assert_equal nil, @c.content_range 323 | 324 | try_invalid_content_range "invalid" 325 | try_invalid_content_range "bytes 123-abc" 326 | try_invalid_content_range "bytes abc-123" 327 | end 328 | 329 | def test_range_length 330 | @c['Content-Range'] = "bytes 0-499/1000" 331 | assert_equal 500, @c.range_length 332 | @c['Content-Range'] = "bytes 1-500/1000" 333 | assert_equal 500, @c.range_length 334 | @c['Content-Range'] = "bytes 1-1/1000" 335 | assert_equal 1, @c.range_length 336 | @c['Content-Range'] = "tokens 1-1/1000" 337 | assert_equal nil, @c.range_length 338 | 339 | try_invalid_content_range "bytes 1-1/abc" 340 | end 341 | 342 | def try_invalid_content_range(s) 343 | @c['Content-Range'] = "#{s}" 344 | assert_raise(Net::HTTPHeaderSyntaxError, s){ @c.content_range } 345 | end 346 | 347 | def test_chunked? 348 | try_chunked true, 'chunked' 349 | try_chunked true, ' chunked ' 350 | try_chunked true, '(OK)chunked' 351 | 352 | try_chunked false, 'not-chunked' 353 | try_chunked false, 'chunked-but-not-chunked' 354 | end 355 | 356 | def try_chunked(bool, str) 357 | @c['transfer-encoding'] = str 358 | assert_equal bool, @c.chunked? 359 | end 360 | 361 | def test_content_length 362 | @c.delete('content-length') 363 | assert_nil @c['content-length'] 364 | 365 | try_content_length 500, '500' 366 | try_content_length 10000_0000_0000, '1000000000000' 367 | try_content_length 123, ' 123' 368 | try_content_length 1, '1 23' 369 | try_content_length 500, '(OK)500' 370 | assert_raise(Net::HTTPHeaderSyntaxError, 'here is no digit, but') { 371 | @c['content-length'] = 'no digit' 372 | @c.content_length 373 | } 374 | end 375 | 376 | def try_content_length(len, str) 377 | @c['content-length'] = str 378 | assert_equal len, @c.content_length 379 | end 380 | 381 | def test_content_length= 382 | @c.content_length = 0 383 | assert_equal 0, @c.content_length 384 | @c.content_length = 1 385 | assert_equal 1, @c.content_length 386 | @c.content_length = 999 387 | assert_equal 999, @c.content_length 388 | @c.content_length = 10000000000000 389 | assert_equal 10000000000000, @c.content_length 390 | end 391 | 392 | def test_content_type 393 | assert_nil @c.content_type 394 | @c.content_type = 'text/html' 395 | assert_equal 'text/html', @c.content_type 396 | @c.content_type = 'application/pdf' 397 | assert_equal 'application/pdf', @c.content_type 398 | @c.set_content_type 'text/html', {'charset' => 'iso-2022-jp'} 399 | assert_equal 'text/html', @c.content_type 400 | @c.content_type = 'text' 401 | assert_equal 'text', @c.content_type 402 | end 403 | 404 | def test_main_type 405 | assert_nil @c.main_type 406 | @c.content_type = 'text/html' 407 | assert_equal 'text', @c.main_type 408 | @c.content_type = 'application/pdf' 409 | assert_equal 'application', @c.main_type 410 | @c.set_content_type 'text/html', {'charset' => 'iso-2022-jp'} 411 | assert_equal 'text', @c.main_type 412 | @c.content_type = 'text' 413 | assert_equal 'text', @c.main_type 414 | end 415 | 416 | def test_sub_type 417 | assert_nil @c.sub_type 418 | @c.content_type = 'text/html' 419 | assert_equal 'html', @c.sub_type 420 | @c.content_type = 'application/pdf' 421 | assert_equal 'pdf', @c.sub_type 422 | @c.set_content_type 'text/html', {'charset' => 'iso-2022-jp'} 423 | assert_equal 'html', @c.sub_type 424 | @c.content_type = 'text' 425 | assert_nil @c.sub_type 426 | end 427 | 428 | def test_type_params 429 | assert_equal({}, @c.type_params) 430 | @c.content_type = 'text/html' 431 | assert_equal({}, @c.type_params) 432 | @c.content_type = 'application/pdf' 433 | assert_equal({}, @c.type_params) 434 | @c.set_content_type 'text/html', {'charset' => 'iso-2022-jp'} 435 | assert_equal({'charset' => 'iso-2022-jp'}, @c.type_params) 436 | @c.content_type = 'text' 437 | assert_equal({}, @c.type_params) 438 | end 439 | 440 | def test_set_content_type 441 | end 442 | 443 | def test_form_data= 444 | @c.form_data = {"cmd"=>"search", "q"=>"ruby", "max"=>"50"} 445 | assert_equal 'application/x-www-form-urlencoded', @c.content_type 446 | assert_equal %w( cmd=search max=50 q=ruby ), @c.body.split('&').sort 447 | end 448 | 449 | def test_set_form_data 450 | @c.set_form_data "cmd"=>"search", "q"=>"ruby", "max"=>"50" 451 | assert_equal 'application/x-www-form-urlencoded', @c.content_type 452 | assert_equal %w( cmd=search max=50 q=ruby ), @c.body.split('&').sort 453 | 454 | @c.set_form_data "cmd"=>"search", "q"=>"ruby", "max"=>50 455 | assert_equal 'application/x-www-form-urlencoded', @c.content_type 456 | assert_equal %w( cmd=search max=50 q=ruby ), @c.body.split('&').sort 457 | 458 | @c.set_form_data({"cmd"=>"search", "q"=>"ruby", "max"=>"50"}, ';') 459 | assert_equal 'application/x-www-form-urlencoded', @c.content_type 460 | assert_equal %w( cmd=search max=50 q=ruby ), @c.body.split(';').sort 461 | end 462 | 463 | def test_basic_auth 464 | end 465 | 466 | def test_proxy_basic_auth 467 | end 468 | 469 | end 470 | -------------------------------------------------------------------------------- /lib/net/http/requests.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # HTTP/1.1 methods --- RFC2616 4 | 5 | # \Class for representing 6 | # {HTTP method GET}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#GET_method]: 7 | # 8 | # require 'net/http' 9 | # uri = URI('http://example.com') 10 | # hostname = uri.hostname # => "example.com" 11 | # req = Net::HTTP::Get.new(uri) # => # 12 | # res = Net::HTTP.start(hostname) do |http| 13 | # http.request(req) 14 | # end 15 | # 16 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 17 | # 18 | # Properties: 19 | # 20 | # - Request body: optional. 21 | # - Response body: yes. 22 | # - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes. 23 | # - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. 24 | # - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes. 25 | # 26 | # Related: 27 | # 28 | # - Net::HTTP.get: sends +GET+ request, returns response body. 29 | # - Net::HTTP#get: sends +GET+ request, returns response object. 30 | # 31 | class Net::HTTP::Get < Net::HTTPRequest 32 | # :stopdoc: 33 | METHOD = 'GET' 34 | REQUEST_HAS_BODY = false 35 | RESPONSE_HAS_BODY = true 36 | end 37 | 38 | # \Class for representing 39 | # {HTTP method HEAD}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#HEAD_method]: 40 | # 41 | # require 'net/http' 42 | # uri = URI('http://example.com') 43 | # hostname = uri.hostname # => "example.com" 44 | # req = Net::HTTP::Head.new(uri) # => # 45 | # res = Net::HTTP.start(hostname) do |http| 46 | # http.request(req) 47 | # end 48 | # 49 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 50 | # 51 | # Properties: 52 | # 53 | # - Request body: optional. 54 | # - Response body: no. 55 | # - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes. 56 | # - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. 57 | # - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes. 58 | # 59 | # Related: 60 | # 61 | # - Net::HTTP#head: sends +HEAD+ request, returns response object. 62 | # 63 | class Net::HTTP::Head < Net::HTTPRequest 64 | # :stopdoc: 65 | METHOD = 'HEAD' 66 | REQUEST_HAS_BODY = false 67 | RESPONSE_HAS_BODY = false 68 | end 69 | 70 | # \Class for representing 71 | # {HTTP method POST}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#POST_method]: 72 | # 73 | # require 'net/http' 74 | # uri = URI('http://example.com') 75 | # hostname = uri.hostname # => "example.com" 76 | # uri.path = '/posts' 77 | # req = Net::HTTP::Post.new(uri) # => # 78 | # req.body = '{"title": "foo","body": "bar","userId": 1}' 79 | # req.content_type = 'application/json' 80 | # res = Net::HTTP.start(hostname) do |http| 81 | # http.request(req) 82 | # end 83 | # 84 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 85 | # 86 | # Properties: 87 | # 88 | # - Request body: yes. 89 | # - Response body: yes. 90 | # - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no. 91 | # - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: no. 92 | # - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes. 93 | # 94 | # Related: 95 | # 96 | # - Net::HTTP.post: sends +POST+ request, returns response object. 97 | # - Net::HTTP#post: sends +POST+ request, returns response object. 98 | # 99 | class Net::HTTP::Post < Net::HTTPRequest 100 | # :stopdoc: 101 | METHOD = 'POST' 102 | REQUEST_HAS_BODY = true 103 | RESPONSE_HAS_BODY = true 104 | end 105 | 106 | # \Class for representing 107 | # {HTTP method PUT}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PUT_method]: 108 | # 109 | # require 'net/http' 110 | # uri = URI('http://example.com') 111 | # hostname = uri.hostname # => "example.com" 112 | # uri.path = '/posts' 113 | # req = Net::HTTP::Put.new(uri) # => # 114 | # req.body = '{"title": "foo","body": "bar","userId": 1}' 115 | # req.content_type = 'application/json' 116 | # res = Net::HTTP.start(hostname) do |http| 117 | # http.request(req) 118 | # end 119 | # 120 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 121 | # 122 | # Properties: 123 | # 124 | # - Request body: yes. 125 | # - Response body: yes. 126 | # - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no. 127 | # - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. 128 | # - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. 129 | # 130 | # Related: 131 | # 132 | # - Net::HTTP.put: sends +PUT+ request, returns response object. 133 | # - Net::HTTP#put: sends +PUT+ request, returns response object. 134 | # 135 | class Net::HTTP::Put < Net::HTTPRequest 136 | # :stopdoc: 137 | METHOD = 'PUT' 138 | REQUEST_HAS_BODY = true 139 | RESPONSE_HAS_BODY = true 140 | end 141 | 142 | # \Class for representing 143 | # {HTTP method DELETE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#DELETE_method]: 144 | # 145 | # require 'net/http' 146 | # uri = URI('http://example.com') 147 | # hostname = uri.hostname # => "example.com" 148 | # uri.path = '/posts/1' 149 | # req = Net::HTTP::Delete.new(uri) # => # 150 | # res = Net::HTTP.start(hostname) do |http| 151 | # http.request(req) 152 | # end 153 | # 154 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 155 | # 156 | # Properties: 157 | # 158 | # - Request body: optional. 159 | # - Response body: yes. 160 | # - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no. 161 | # - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. 162 | # - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. 163 | # 164 | # Related: 165 | # 166 | # - Net::HTTP#delete: sends +DELETE+ request, returns response object. 167 | # 168 | class Net::HTTP::Delete < Net::HTTPRequest 169 | # :stopdoc: 170 | METHOD = 'DELETE' 171 | REQUEST_HAS_BODY = false 172 | RESPONSE_HAS_BODY = true 173 | end 174 | 175 | # \Class for representing 176 | # {HTTP method OPTIONS}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#OPTIONS_method]: 177 | # 178 | # require 'net/http' 179 | # uri = URI('http://example.com') 180 | # hostname = uri.hostname # => "example.com" 181 | # req = Net::HTTP::Options.new(uri) # => # 182 | # res = Net::HTTP.start(hostname) do |http| 183 | # http.request(req) 184 | # end 185 | # 186 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 187 | # 188 | # Properties: 189 | # 190 | # - Request body: optional. 191 | # - Response body: yes. 192 | # - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes. 193 | # - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. 194 | # - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. 195 | # 196 | # Related: 197 | # 198 | # - Net::HTTP#options: sends +OPTIONS+ request, returns response object. 199 | # 200 | class Net::HTTP::Options < Net::HTTPRequest 201 | # :stopdoc: 202 | METHOD = 'OPTIONS' 203 | REQUEST_HAS_BODY = false 204 | RESPONSE_HAS_BODY = true 205 | end 206 | 207 | # \Class for representing 208 | # {HTTP method TRACE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#TRACE_method]: 209 | # 210 | # require 'net/http' 211 | # uri = URI('http://example.com') 212 | # hostname = uri.hostname # => "example.com" 213 | # req = Net::HTTP::Trace.new(uri) # => # 214 | # res = Net::HTTP.start(hostname) do |http| 215 | # http.request(req) 216 | # end 217 | # 218 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 219 | # 220 | # Properties: 221 | # 222 | # - Request body: no. 223 | # - Response body: yes. 224 | # - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes. 225 | # - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. 226 | # - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. 227 | # 228 | # Related: 229 | # 230 | # - Net::HTTP#trace: sends +TRACE+ request, returns response object. 231 | # 232 | class Net::HTTP::Trace < Net::HTTPRequest 233 | # :stopdoc: 234 | METHOD = 'TRACE' 235 | REQUEST_HAS_BODY = false 236 | RESPONSE_HAS_BODY = true 237 | end 238 | 239 | # \Class for representing 240 | # {HTTP method PATCH}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PATCH_method]: 241 | # 242 | # require 'net/http' 243 | # uri = URI('http://example.com') 244 | # hostname = uri.hostname # => "example.com" 245 | # uri.path = '/posts' 246 | # req = Net::HTTP::Patch.new(uri) # => # 247 | # req.body = '{"title": "foo","body": "bar","userId": 1}' 248 | # req.content_type = 'application/json' 249 | # res = Net::HTTP.start(hostname) do |http| 250 | # http.request(req) 251 | # end 252 | # 253 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 254 | # 255 | # Properties: 256 | # 257 | # - Request body: yes. 258 | # - Response body: yes. 259 | # - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no. 260 | # - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: no. 261 | # - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. 262 | # 263 | # Related: 264 | # 265 | # - Net::HTTP#patch: sends +PATCH+ request, returns response object. 266 | # 267 | class Net::HTTP::Patch < Net::HTTPRequest 268 | # :stopdoc: 269 | METHOD = 'PATCH' 270 | REQUEST_HAS_BODY = true 271 | RESPONSE_HAS_BODY = true 272 | end 273 | 274 | # 275 | # WebDAV methods --- RFC2518 276 | # 277 | 278 | # \Class for representing 279 | # {WebDAV method PROPFIND}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND]: 280 | # 281 | # require 'net/http' 282 | # uri = URI('http://example.com') 283 | # hostname = uri.hostname # => "example.com" 284 | # req = Net::HTTP::Propfind.new(uri) # => # 285 | # res = Net::HTTP.start(hostname) do |http| 286 | # http.request(req) 287 | # end 288 | # 289 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 290 | # 291 | # Related: 292 | # 293 | # - Net::HTTP#propfind: sends +PROPFIND+ request, returns response object. 294 | # 295 | class Net::HTTP::Propfind < Net::HTTPRequest 296 | # :stopdoc: 297 | METHOD = 'PROPFIND' 298 | REQUEST_HAS_BODY = true 299 | RESPONSE_HAS_BODY = true 300 | end 301 | 302 | # \Class for representing 303 | # {WebDAV method PROPPATCH}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH]: 304 | # 305 | # require 'net/http' 306 | # uri = URI('http://example.com') 307 | # hostname = uri.hostname # => "example.com" 308 | # req = Net::HTTP::Proppatch.new(uri) # => # 309 | # res = Net::HTTP.start(hostname) do |http| 310 | # http.request(req) 311 | # end 312 | # 313 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 314 | # 315 | # Related: 316 | # 317 | # - Net::HTTP#proppatch: sends +PROPPATCH+ request, returns response object. 318 | # 319 | class Net::HTTP::Proppatch < Net::HTTPRequest 320 | # :stopdoc: 321 | METHOD = 'PROPPATCH' 322 | REQUEST_HAS_BODY = true 323 | RESPONSE_HAS_BODY = true 324 | end 325 | 326 | # \Class for representing 327 | # {WebDAV method MKCOL}[http://www.webdav.org/specs/rfc4918.html#METHOD_MKCOL]: 328 | # 329 | # require 'net/http' 330 | # uri = URI('http://example.com') 331 | # hostname = uri.hostname # => "example.com" 332 | # req = Net::HTTP::Mkcol.new(uri) # => # 333 | # res = Net::HTTP.start(hostname) do |http| 334 | # http.request(req) 335 | # end 336 | # 337 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 338 | # 339 | # Related: 340 | # 341 | # - Net::HTTP#mkcol: sends +MKCOL+ request, returns response object. 342 | # 343 | class Net::HTTP::Mkcol < Net::HTTPRequest 344 | # :stopdoc: 345 | METHOD = 'MKCOL' 346 | REQUEST_HAS_BODY = true 347 | RESPONSE_HAS_BODY = true 348 | end 349 | 350 | # \Class for representing 351 | # {WebDAV method COPY}[http://www.webdav.org/specs/rfc4918.html#METHOD_COPY]: 352 | # 353 | # require 'net/http' 354 | # uri = URI('http://example.com') 355 | # hostname = uri.hostname # => "example.com" 356 | # req = Net::HTTP::Copy.new(uri) # => # 357 | # res = Net::HTTP.start(hostname) do |http| 358 | # http.request(req) 359 | # end 360 | # 361 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 362 | # 363 | # Related: 364 | # 365 | # - Net::HTTP#copy: sends +COPY+ request, returns response object. 366 | # 367 | class Net::HTTP::Copy < Net::HTTPRequest 368 | # :stopdoc: 369 | METHOD = 'COPY' 370 | REQUEST_HAS_BODY = false 371 | RESPONSE_HAS_BODY = true 372 | end 373 | 374 | # \Class for representing 375 | # {WebDAV method MOVE}[http://www.webdav.org/specs/rfc4918.html#METHOD_MOVE]: 376 | # 377 | # require 'net/http' 378 | # uri = URI('http://example.com') 379 | # hostname = uri.hostname # => "example.com" 380 | # req = Net::HTTP::Move.new(uri) # => # 381 | # res = Net::HTTP.start(hostname) do |http| 382 | # http.request(req) 383 | # end 384 | # 385 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 386 | # 387 | # Related: 388 | # 389 | # - Net::HTTP#move: sends +MOVE+ request, returns response object. 390 | # 391 | class Net::HTTP::Move < Net::HTTPRequest 392 | # :stopdoc: 393 | METHOD = 'MOVE' 394 | REQUEST_HAS_BODY = false 395 | RESPONSE_HAS_BODY = true 396 | end 397 | 398 | # \Class for representing 399 | # {WebDAV method LOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_LOCK]: 400 | # 401 | # require 'net/http' 402 | # uri = URI('http://example.com') 403 | # hostname = uri.hostname # => "example.com" 404 | # req = Net::HTTP::Lock.new(uri) # => # 405 | # res = Net::HTTP.start(hostname) do |http| 406 | # http.request(req) 407 | # end 408 | # 409 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 410 | # 411 | # Related: 412 | # 413 | # - Net::HTTP#lock: sends +LOCK+ request, returns response object. 414 | # 415 | class Net::HTTP::Lock < Net::HTTPRequest 416 | # :stopdoc: 417 | METHOD = 'LOCK' 418 | REQUEST_HAS_BODY = true 419 | RESPONSE_HAS_BODY = true 420 | end 421 | 422 | # \Class for representing 423 | # {WebDAV method UNLOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_UNLOCK]: 424 | # 425 | # require 'net/http' 426 | # uri = URI('http://example.com') 427 | # hostname = uri.hostname # => "example.com" 428 | # req = Net::HTTP::Unlock.new(uri) # => # 429 | # res = Net::HTTP.start(hostname) do |http| 430 | # http.request(req) 431 | # end 432 | # 433 | # See {Request Headers}[rdoc-ref:Net::HTTPRequest@Request+Headers]. 434 | # 435 | # Related: 436 | # 437 | # - Net::HTTP#unlock: sends +UNLOCK+ request, returns response object. 438 | # 439 | class Net::HTTP::Unlock < Net::HTTPRequest 440 | # :stopdoc: 441 | METHOD = 'UNLOCK' 442 | REQUEST_HAS_BODY = true 443 | RESPONSE_HAS_BODY = true 444 | end 445 | -------------------------------------------------------------------------------- /test/net/http/test_httpresponse.rb: -------------------------------------------------------------------------------- 1 | # coding: US-ASCII 2 | # frozen_string_literal: false 3 | require 'net/http' 4 | require 'test/unit' 5 | require 'stringio' 6 | 7 | class HTTPResponseTest < Test::Unit::TestCase 8 | def test_singleline_header 9 | io = dummy_io(<hello\u1234" 198 | io = dummy_io(<hello\u1234" 222 | io = dummy_io(<', res.inspect 740 | 741 | res = Net::HTTPUnknownResponse.new('1.0', '???', 'test response') 742 | socket = Net::BufferedIO.new(StringIO.new('test body')) 743 | res.reading_body(socket, true) {} 744 | assert_equal '#', res.inspect 745 | end 746 | 747 | private 748 | 749 | def dummy_io(str) 750 | str = str.gsub(/\n/, "\r\n") 751 | 752 | Net::BufferedIO.new(StringIO.new(str)) 753 | end 754 | end 755 | -------------------------------------------------------------------------------- /lib/net/http/response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This class is the base class for \Net::HTTP response classes. 4 | # 5 | # == About the Examples 6 | # 7 | # :include: doc/net-http/examples.rdoc 8 | # 9 | # == Returned Responses 10 | # 11 | # \Method Net::HTTP.get_response returns 12 | # an instance of one of the subclasses of \Net::HTTPResponse: 13 | # 14 | # Net::HTTP.get_response(uri) 15 | # # => # 16 | # Net::HTTP.get_response(hostname, '/nosuch') 17 | # # => # 18 | # 19 | # As does method Net::HTTP#request: 20 | # 21 | # req = Net::HTTP::Get.new(uri) 22 | # Net::HTTP.start(hostname) do |http| 23 | # http.request(req) 24 | # end # => # 25 | # 26 | # \Class \Net::HTTPResponse includes module Net::HTTPHeader, 27 | # which provides access to response header values via (among others): 28 | # 29 | # - \Hash-like method []. 30 | # - Specific reader methods, such as +content_type+. 31 | # 32 | # Examples: 33 | # 34 | # res = Net::HTTP.get_response(uri) # => # 35 | # res['Content-Type'] # => "text/html; charset=UTF-8" 36 | # res.content_type # => "text/html" 37 | # 38 | # == Response Subclasses 39 | # 40 | # \Class \Net::HTTPResponse has a subclass for each 41 | # {HTTP status code}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes]. 42 | # You can look up the response class for a given code: 43 | # 44 | # Net::HTTPResponse::CODE_TO_OBJ['200'] # => Net::HTTPOK 45 | # Net::HTTPResponse::CODE_TO_OBJ['400'] # => Net::HTTPBadRequest 46 | # Net::HTTPResponse::CODE_TO_OBJ['404'] # => Net::HTTPNotFound 47 | # 48 | # And you can retrieve the status code for a response object: 49 | # 50 | # Net::HTTP.get_response(uri).code # => "200" 51 | # Net::HTTP.get_response(hostname, '/nosuch').code # => "404" 52 | # 53 | # The response subclasses (indentation shows class hierarchy): 54 | # 55 | # - Net::HTTPUnknownResponse (for unhandled \HTTP extensions). 56 | # 57 | # - Net::HTTPInformation: 58 | # 59 | # - Net::HTTPContinue (100) 60 | # - Net::HTTPSwitchProtocol (101) 61 | # - Net::HTTPProcessing (102) 62 | # - Net::HTTPEarlyHints (103) 63 | # 64 | # - Net::HTTPSuccess: 65 | # 66 | # - Net::HTTPOK (200) 67 | # - Net::HTTPCreated (201) 68 | # - Net::HTTPAccepted (202) 69 | # - Net::HTTPNonAuthoritativeInformation (203) 70 | # - Net::HTTPNoContent (204) 71 | # - Net::HTTPResetContent (205) 72 | # - Net::HTTPPartialContent (206) 73 | # - Net::HTTPMultiStatus (207) 74 | # - Net::HTTPAlreadyReported (208) 75 | # - Net::HTTPIMUsed (226) 76 | # 77 | # - Net::HTTPRedirection: 78 | # 79 | # - Net::HTTPMultipleChoices (300) 80 | # - Net::HTTPMovedPermanently (301) 81 | # - Net::HTTPFound (302) 82 | # - Net::HTTPSeeOther (303) 83 | # - Net::HTTPNotModified (304) 84 | # - Net::HTTPUseProxy (305) 85 | # - Net::HTTPTemporaryRedirect (307) 86 | # - Net::HTTPPermanentRedirect (308) 87 | # 88 | # - Net::HTTPClientError: 89 | # 90 | # - Net::HTTPBadRequest (400) 91 | # - Net::HTTPUnauthorized (401) 92 | # - Net::HTTPPaymentRequired (402) 93 | # - Net::HTTPForbidden (403) 94 | # - Net::HTTPNotFound (404) 95 | # - Net::HTTPMethodNotAllowed (405) 96 | # - Net::HTTPNotAcceptable (406) 97 | # - Net::HTTPProxyAuthenticationRequired (407) 98 | # - Net::HTTPRequestTimeOut (408) 99 | # - Net::HTTPConflict (409) 100 | # - Net::HTTPGone (410) 101 | # - Net::HTTPLengthRequired (411) 102 | # - Net::HTTPPreconditionFailed (412) 103 | # - Net::HTTPRequestEntityTooLarge (413) 104 | # - Net::HTTPRequestURITooLong (414) 105 | # - Net::HTTPUnsupportedMediaType (415) 106 | # - Net::HTTPRequestedRangeNotSatisfiable (416) 107 | # - Net::HTTPExpectationFailed (417) 108 | # - Net::HTTPMisdirectedRequest (421) 109 | # - Net::HTTPUnprocessableEntity (422) 110 | # - Net::HTTPLocked (423) 111 | # - Net::HTTPFailedDependency (424) 112 | # - Net::HTTPUpgradeRequired (426) 113 | # - Net::HTTPPreconditionRequired (428) 114 | # - Net::HTTPTooManyRequests (429) 115 | # - Net::HTTPRequestHeaderFieldsTooLarge (431) 116 | # - Net::HTTPUnavailableForLegalReasons (451) 117 | # 118 | # - Net::HTTPServerError: 119 | # 120 | # - Net::HTTPInternalServerError (500) 121 | # - Net::HTTPNotImplemented (501) 122 | # - Net::HTTPBadGateway (502) 123 | # - Net::HTTPServiceUnavailable (503) 124 | # - Net::HTTPGatewayTimeOut (504) 125 | # - Net::HTTPVersionNotSupported (505) 126 | # - Net::HTTPVariantAlsoNegotiates (506) 127 | # - Net::HTTPInsufficientStorage (507) 128 | # - Net::HTTPLoopDetected (508) 129 | # - Net::HTTPNotExtended (510) 130 | # - Net::HTTPNetworkAuthenticationRequired (511) 131 | # 132 | # There is also the Net::HTTPBadResponse exception which is raised when 133 | # there is a protocol error. 134 | # 135 | class Net::HTTPResponse 136 | class << self 137 | # true if the response has a body. 138 | def body_permitted? 139 | self::HAS_BODY 140 | end 141 | 142 | def exception_type # :nodoc: internal use only 143 | self::EXCEPTION_TYPE 144 | end 145 | 146 | def read_new(sock) #:nodoc: internal use only 147 | httpv, code, msg = read_status_line(sock) 148 | res = response_class(code).new(httpv, code, msg) 149 | each_response_header(sock) do |k,v| 150 | res.add_field k, v 151 | end 152 | res 153 | end 154 | 155 | private 156 | # :stopdoc: 157 | 158 | def read_status_line(sock) 159 | str = sock.readline 160 | m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or 161 | raise Net::HTTPBadResponse, "wrong status line: #{str.dump}" 162 | m.captures 163 | end 164 | 165 | def response_class(code) 166 | CODE_TO_OBJ[code] or 167 | CODE_CLASS_TO_OBJ[code[0,1]] or 168 | Net::HTTPUnknownResponse 169 | end 170 | 171 | def each_response_header(sock) 172 | key = value = nil 173 | while true 174 | line = sock.readuntil("\n", true).sub(/\s+\z/, '') 175 | break if line.empty? 176 | if line[0] == ?\s or line[0] == ?\t and value 177 | value << ' ' unless value.empty? 178 | value << line.strip 179 | else 180 | yield key, value if key 181 | key, value = line.strip.split(/\s*:\s*/, 2) 182 | raise Net::HTTPBadResponse, 'wrong header line format' if value.nil? 183 | end 184 | end 185 | yield key, value if key 186 | end 187 | end 188 | 189 | # next is to fix bug in RDoc, where the private inside class << self 190 | # spills out. 191 | public 192 | 193 | include Net::HTTPHeader 194 | 195 | def initialize(httpv, code, msg) #:nodoc: internal use only 196 | @http_version = httpv 197 | @code = code 198 | @message = msg 199 | initialize_http_header nil 200 | @body = nil 201 | @read = false 202 | @uri = nil 203 | @decode_content = false 204 | @body_encoding = false 205 | @ignore_eof = true 206 | end 207 | 208 | # The HTTP version supported by the server. 209 | attr_reader :http_version 210 | 211 | # The HTTP result code string. For example, '302'. You can also 212 | # determine the response type by examining which response subclass 213 | # the response object is an instance of. 214 | attr_reader :code 215 | 216 | # The HTTP result message sent by the server. For example, 'Not Found'. 217 | attr_reader :message 218 | alias msg message # :nodoc: obsolete 219 | 220 | # The URI used to fetch this response. The response URI is only available 221 | # if a URI was used to create the request. 222 | attr_reader :uri 223 | 224 | # Set to true automatically when the request did not contain an 225 | # Accept-Encoding header from the user. 226 | attr_accessor :decode_content 227 | 228 | # Returns the value set by body_encoding=, or +false+ if none; 229 | # see #body_encoding=. 230 | attr_reader :body_encoding 231 | 232 | # Sets the encoding that should be used when reading the body: 233 | # 234 | # - If the given value is an Encoding object, that encoding will be used. 235 | # - Otherwise if the value is a string, the value of 236 | # {Encoding#find(value)}[https://docs.ruby-lang.org/en/master/Encoding.html#method-c-find] 237 | # will be used. 238 | # - Otherwise an encoding will be deduced from the body itself. 239 | # 240 | # Examples: 241 | # 242 | # http = Net::HTTP.new(hostname) 243 | # req = Net::HTTP::Get.new('/') 244 | # 245 | # http.request(req) do |res| 246 | # p res.body.encoding # => # 247 | # end 248 | # 249 | # http.request(req) do |res| 250 | # res.body_encoding = "UTF-8" 251 | # p res.body.encoding # => # 252 | # end 253 | # 254 | def body_encoding=(value) 255 | value = Encoding.find(value) if value.is_a?(String) 256 | @body_encoding = value 257 | end 258 | 259 | # Whether to ignore EOF when reading bodies with a specified Content-Length 260 | # header. 261 | attr_accessor :ignore_eof 262 | 263 | def inspect # :nodoc: 264 | "#<#{self.class} #{@code} #{@message} readbody=#{@read}>" 265 | end 266 | 267 | # 268 | # response <-> exception relationship 269 | # 270 | 271 | def code_type #:nodoc: 272 | self.class 273 | end 274 | 275 | def error! #:nodoc: 276 | message = @code 277 | message = "#{message} #{@message.dump}" if @message 278 | raise error_type().new(message, self) 279 | end 280 | 281 | def error_type #:nodoc: 282 | self.class::EXCEPTION_TYPE 283 | end 284 | 285 | # Raises an HTTP error if the response is not 2xx (success). 286 | def value 287 | error! unless self.kind_of?(Net::HTTPSuccess) 288 | end 289 | 290 | def uri= uri # :nodoc: 291 | @uri = uri.dup if uri 292 | end 293 | 294 | # 295 | # header (for backward compatibility only; DO NOT USE) 296 | # 297 | 298 | def response #:nodoc: 299 | warn "Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE 300 | self 301 | end 302 | 303 | def header #:nodoc: 304 | warn "Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE 305 | self 306 | end 307 | 308 | def read_header #:nodoc: 309 | warn "Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE 310 | self 311 | end 312 | 313 | # 314 | # body 315 | # 316 | 317 | def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only 318 | @socket = sock 319 | @body_exist = reqmethodallowbody && self.class.body_permitted? 320 | begin 321 | yield 322 | self.body # ensure to read body 323 | ensure 324 | @socket = nil 325 | end 326 | end 327 | 328 | # Gets the entity body returned by the remote HTTP server. 329 | # 330 | # If a block is given, the body is passed to the block, and 331 | # the body is provided in fragments, as it is read in from the socket. 332 | # 333 | # If +dest+ argument is given, response is read into that variable, 334 | # with dest#<< method (it could be String or IO, or any 335 | # other object responding to <<). 336 | # 337 | # Calling this method a second or subsequent time for the same 338 | # HTTPResponse object will return the value already read. 339 | # 340 | # http.request_get('/index.html') {|res| 341 | # puts res.read_body 342 | # } 343 | # 344 | # http.request_get('/index.html') {|res| 345 | # p res.read_body.object_id # 538149362 346 | # p res.read_body.object_id # 538149362 347 | # } 348 | # 349 | # # using iterator 350 | # http.request_get('/index.html') {|res| 351 | # res.read_body do |segment| 352 | # print segment 353 | # end 354 | # } 355 | # 356 | def read_body(dest = nil, &block) 357 | if @read 358 | raise IOError, "#{self.class}\#read_body called twice" if dest or block 359 | return @body 360 | end 361 | to = procdest(dest, block) 362 | stream_check 363 | if @body_exist 364 | read_body_0 to 365 | @body = to 366 | else 367 | @body = nil 368 | end 369 | @read = true 370 | return if @body.nil? 371 | 372 | case enc = @body_encoding 373 | when Encoding, false, nil 374 | # Encoding: force given encoding 375 | # false/nil: do not force encoding 376 | else 377 | # other value: detect encoding from body 378 | enc = detect_encoding(@body) 379 | end 380 | 381 | @body.force_encoding(enc) if enc 382 | 383 | @body 384 | end 385 | 386 | # Returns the string response body; 387 | # note that repeated calls for the unmodified body return a cached string: 388 | # 389 | # path = '/todos/1' 390 | # Net::HTTP.start(hostname) do |http| 391 | # res = http.get(path) 392 | # p res.body 393 | # p http.head(path).body # No body. 394 | # end 395 | # 396 | # Output: 397 | # 398 | # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}" 399 | # nil 400 | # 401 | def body 402 | read_body() 403 | end 404 | 405 | # Sets the body of the response to the given value. 406 | def body=(value) 407 | @body = value 408 | end 409 | 410 | alias entity body #:nodoc: obsolete 411 | 412 | private 413 | 414 | # :nodoc: 415 | def detect_encoding(str, encoding=nil) 416 | if encoding 417 | elsif encoding = type_params['charset'] 418 | elsif encoding = check_bom(str) 419 | else 420 | encoding = case content_type&.downcase 421 | when %r{text/x(?:ht)?ml|application/(?:[^+]+\+)?xml} 422 | /\A' 509 | ss.getch 510 | return nil 511 | end 512 | name = ss.scan(/[^=\t\n\f\r \/>]*/) 513 | name.downcase! 514 | raise if name.empty? 515 | ss.skip(/[\t\n\f\r ]*/) 516 | if ss.getch != '=' 517 | value = '' 518 | return [name, value] 519 | end 520 | ss.skip(/[\t\n\f\r ]*/) 521 | case ss.peek(1) 522 | when '"' 523 | ss.getch 524 | value = ss.scan(/[^"]+/) 525 | value.downcase! 526 | ss.getch 527 | when "'" 528 | ss.getch 529 | value = ss.scan(/[^']+/) 530 | value.downcase! 531 | ss.getch 532 | when '>' 533 | value = '' 534 | else 535 | value = ss.scan(/[^\t\n\f\r >]+/) 536 | value.downcase! 537 | end 538 | [name, value] 539 | end 540 | 541 | def extracting_encodings_from_meta_elements(value) 542 | # http://dev.w3.org/html5/spec/fetching-resources.html#algorithm-for-extracting-an-encoding-from-a-meta-element 543 | if /charset[\t\n\f\r ]*=(?:"([^"]*)"|'([^']*)'|["']|\z|([^\t\n\f\r ;]+))/i =~ value 544 | return $1 || $2 || $3 545 | end 546 | return nil 547 | end 548 | 549 | ## 550 | # Checks for a supported Content-Encoding header and yields an Inflate 551 | # wrapper for this response's socket when zlib is present. If the 552 | # Content-Encoding is not supported or zlib is missing, the plain socket is 553 | # yielded. 554 | # 555 | # If a Content-Range header is present, a plain socket is yielded as the 556 | # bytes in the range may not be a complete deflate block. 557 | 558 | def inflater # :nodoc: 559 | return yield @socket unless Net::HTTP::HAVE_ZLIB 560 | return yield @socket unless @decode_content 561 | return yield @socket if self['content-range'] 562 | 563 | v = self['content-encoding'] 564 | case v&.downcase 565 | when 'deflate', 'gzip', 'x-gzip' then 566 | self.delete 'content-encoding' 567 | 568 | inflate_body_io = Inflater.new(@socket) 569 | 570 | begin 571 | yield inflate_body_io 572 | success = true 573 | ensure 574 | begin 575 | inflate_body_io.finish 576 | if self['content-length'] 577 | self['content-length'] = inflate_body_io.bytes_inflated.to_s 578 | end 579 | rescue => err 580 | # Ignore #finish's error if there is an exception from yield 581 | raise err if success 582 | end 583 | end 584 | when 'none', 'identity' then 585 | self.delete 'content-encoding' 586 | 587 | yield @socket 588 | else 589 | yield @socket 590 | end 591 | end 592 | 593 | def read_body_0(dest) 594 | inflater do |inflate_body_io| 595 | if chunked? 596 | read_chunked dest, inflate_body_io 597 | return 598 | end 599 | 600 | @socket = inflate_body_io 601 | 602 | clen = content_length() 603 | if clen 604 | @socket.read clen, dest, @ignore_eof 605 | return 606 | end 607 | clen = range_length() 608 | if clen 609 | @socket.read clen, dest 610 | return 611 | end 612 | @socket.read_all dest 613 | end 614 | end 615 | 616 | ## 617 | # read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF, 618 | # etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip 619 | # encoded. 620 | # 621 | # See RFC 2616 section 3.6.1 for definitions 622 | 623 | def read_chunked(dest, chunk_data_io) # :nodoc: 624 | total = 0 625 | while true 626 | line = @socket.readline 627 | hexlen = line.slice(/[0-9a-fA-F]+/) or 628 | raise Net::HTTPBadResponse, "wrong chunk size line: #{line}" 629 | len = hexlen.hex 630 | break if len == 0 631 | begin 632 | chunk_data_io.read len, dest 633 | ensure 634 | total += len 635 | @socket.read 2 # \r\n 636 | end 637 | end 638 | until @socket.readline.empty? 639 | # none 640 | end 641 | end 642 | 643 | def stream_check 644 | raise IOError, 'attempt to read body out of block' if @socket.nil? || @socket.closed? 645 | end 646 | 647 | def procdest(dest, block) 648 | raise ArgumentError, 'both arg and block given for HTTP method' if 649 | dest and block 650 | if block 651 | Net::ReadAdapter.new(block) 652 | else 653 | dest || +'' 654 | end 655 | end 656 | 657 | ## 658 | # Inflater is a wrapper around Net::BufferedIO that transparently inflates 659 | # zlib and gzip streams. 660 | 661 | class Inflater # :nodoc: 662 | 663 | ## 664 | # Creates a new Inflater wrapping +socket+ 665 | 666 | def initialize socket 667 | @socket = socket 668 | # zlib with automatic gzip detection 669 | @inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS) 670 | end 671 | 672 | ## 673 | # Finishes the inflate stream. 674 | 675 | def finish 676 | return if @inflate.total_in == 0 677 | @inflate.finish 678 | end 679 | 680 | ## 681 | # The number of bytes inflated, used to update the Content-Length of 682 | # the response. 683 | 684 | def bytes_inflated 685 | @inflate.total_out 686 | end 687 | 688 | ## 689 | # Returns a Net::ReadAdapter that inflates each read chunk into +dest+. 690 | # 691 | # This allows a large response body to be inflated without storing the 692 | # entire body in memory. 693 | 694 | def inflate_adapter(dest) 695 | if dest.respond_to?(:set_encoding) 696 | dest.set_encoding(Encoding::ASCII_8BIT) 697 | elsif dest.respond_to?(:force_encoding) 698 | dest.force_encoding(Encoding::ASCII_8BIT) 699 | end 700 | block = proc do |compressed_chunk| 701 | @inflate.inflate(compressed_chunk) do |chunk| 702 | compressed_chunk.clear 703 | dest << chunk 704 | end 705 | end 706 | 707 | Net::ReadAdapter.new(block) 708 | end 709 | 710 | ## 711 | # Reads +clen+ bytes from the socket, inflates them, then writes them to 712 | # +dest+. +ignore_eof+ is passed down to Net::BufferedIO#read 713 | # 714 | # Unlike Net::BufferedIO#read, this method returns more than +clen+ bytes. 715 | # At this time there is no way for a user of Net::HTTPResponse to read a 716 | # specific number of bytes from the HTTP response body, so this internal 717 | # API does not return the same number of bytes as were requested. 718 | # 719 | # See https://bugs.ruby-lang.org/issues/6492 for further discussion. 720 | 721 | def read clen, dest, ignore_eof = false 722 | temp_dest = inflate_adapter(dest) 723 | 724 | @socket.read clen, temp_dest, ignore_eof 725 | end 726 | 727 | ## 728 | # Reads the rest of the socket, inflates it, then writes it to +dest+. 729 | 730 | def read_all dest 731 | temp_dest = inflate_adapter(dest) 732 | 733 | @socket.read_all temp_dest 734 | end 735 | 736 | end 737 | 738 | end 739 | 740 | -------------------------------------------------------------------------------- /lib/net/http/header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # The \HTTPHeader module provides access to \HTTP headers. 4 | # 5 | # The module is included in: 6 | # 7 | # - Net::HTTPGenericRequest (and therefore Net::HTTPRequest). 8 | # - Net::HTTPResponse. 9 | # 10 | # The headers are a hash-like collection of key/value pairs called _fields_. 11 | # 12 | # == Request and Response Fields 13 | # 14 | # Headers may be included in: 15 | # 16 | # - A Net::HTTPRequest object: 17 | # the object's headers will be sent with the request. 18 | # Any fields may be defined in the request; 19 | # see {Setters}[rdoc-ref:Net::HTTPHeader@Setters]. 20 | # - A Net::HTTPResponse object: 21 | # the objects headers are usually those returned from the host. 22 | # Fields may be retrieved from the object; 23 | # see {Getters}[rdoc-ref:Net::HTTPHeader@Getters] 24 | # and {Iterators}[rdoc-ref:Net::HTTPHeader@Iterators]. 25 | # 26 | # Exactly which fields should be sent or expected depends on the host; 27 | # see: 28 | # 29 | # - {Request fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields]. 30 | # - {Response fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields]. 31 | # 32 | # == About the Examples 33 | # 34 | # :include: doc/net-http/examples.rdoc 35 | # 36 | # == Fields 37 | # 38 | # A header field is a key/value pair. 39 | # 40 | # === Field Keys 41 | # 42 | # A field key may be: 43 | # 44 | # - A string: Key 'Accept' is treated as if it were 45 | # 'Accept'.downcase; i.e., 'accept'. 46 | # - A symbol: Key :Accept is treated as if it were 47 | # :Accept.to_s.downcase; i.e., 'accept'. 48 | # 49 | # Examples: 50 | # 51 | # req = Net::HTTP::Get.new(uri) 52 | # req[:accept] # => "*/*" 53 | # req['Accept'] # => "*/*" 54 | # req['ACCEPT'] # => "*/*" 55 | # 56 | # req['accept'] = 'text/html' 57 | # req[:accept] = 'text/html' 58 | # req['ACCEPT'] = 'text/html' 59 | # 60 | # === Field Values 61 | # 62 | # A field value may be returned as an array of strings or as a string: 63 | # 64 | # - These methods return field values as arrays: 65 | # 66 | # - #get_fields: Returns the array value for the given key, 67 | # or +nil+ if it does not exist. 68 | # - #to_hash: Returns a hash of all header fields: 69 | # each key is a field name; its value is the array value for the field. 70 | # 71 | # - These methods return field values as string; 72 | # the string value for a field is equivalent to 73 | # self[key.downcase.to_s].join(', ')): 74 | # 75 | # - #[]: Returns the string value for the given key, 76 | # or +nil+ if it does not exist. 77 | # - #fetch: Like #[], but accepts a default value 78 | # to be returned if the key does not exist. 79 | # 80 | # The field value may be set: 81 | # 82 | # - #[]=: Sets the value for the given key; 83 | # the given value may be a string, a symbol, an array, or a hash. 84 | # - #add_field: Adds a given value to a value for the given key 85 | # (not overwriting the existing value). 86 | # - #delete: Deletes the field for the given key. 87 | # 88 | # Example field values: 89 | # 90 | # - \String: 91 | # 92 | # req['Accept'] = 'text/html' # => "text/html" 93 | # req['Accept'] # => "text/html" 94 | # req.get_fields('Accept') # => ["text/html"] 95 | # 96 | # - \Symbol: 97 | # 98 | # req['Accept'] = :text # => :text 99 | # req['Accept'] # => "text" 100 | # req.get_fields('Accept') # => ["text"] 101 | # 102 | # - Simple array: 103 | # 104 | # req[:foo] = %w[bar baz bat] 105 | # req[:foo] # => "bar, baz, bat" 106 | # req.get_fields(:foo) # => ["bar", "baz", "bat"] 107 | # 108 | # - Simple hash: 109 | # 110 | # req[:foo] = {bar: 0, baz: 1, bat: 2} 111 | # req[:foo] # => "bar, 0, baz, 1, bat, 2" 112 | # req.get_fields(:foo) # => ["bar", "0", "baz", "1", "bat", "2"] 113 | # 114 | # - Nested: 115 | # 116 | # req[:foo] = [%w[bar baz], {bat: 0, bam: 1}] 117 | # req[:foo] # => "bar, baz, bat, 0, bam, 1" 118 | # req.get_fields(:foo) # => ["bar", "baz", "bat", "0", "bam", "1"] 119 | # 120 | # req[:foo] = {bar: %w[baz bat], bam: {bah: 0, bad: 1}} 121 | # req[:foo] # => "bar, baz, bat, bam, bah, 0, bad, 1" 122 | # req.get_fields(:foo) # => ["bar", "baz", "bat", "bam", "bah", "0", "bad", "1"] 123 | # 124 | # == Convenience Methods 125 | # 126 | # Various convenience methods retrieve values, set values, query values, 127 | # set form values, or iterate over fields. 128 | # 129 | # === Setters 130 | # 131 | # \Method #[]= can set any field, but does little to validate the new value; 132 | # some of the other setter methods provide some validation: 133 | # 134 | # - #[]=: Sets the string or array value for the given key. 135 | # - #add_field: Creates or adds to the array value for the given key. 136 | # - #basic_auth: Sets the string authorization header for 'Authorization'. 137 | # - #content_length=: Sets the integer length for field 'Content-Length. 138 | # - #content_type=: Sets the string value for field 'Content-Type'. 139 | # - #proxy_basic_auth: Sets the string authorization header for 'Proxy-Authorization'. 140 | # - #set_range: Sets the value for field 'Range'. 141 | # 142 | # === Form Setters 143 | # 144 | # - #set_form: Sets an HTML form data set. 145 | # - #set_form_data: Sets header fields and a body from HTML form data. 146 | # 147 | # === Getters 148 | # 149 | # \Method #[] can retrieve the value of any field that exists, 150 | # but always as a string; 151 | # some of the other getter methods return something different 152 | # from the simple string value: 153 | # 154 | # - #[]: Returns the string field value for the given key. 155 | # - #content_length: Returns the integer value of field 'Content-Length'. 156 | # - #content_range: Returns the Range value of field 'Content-Range'. 157 | # - #content_type: Returns the string value of field 'Content-Type'. 158 | # - #fetch: Returns the string field value for the given key. 159 | # - #get_fields: Returns the array field value for the given +key+. 160 | # - #main_type: Returns first part of the string value of field 'Content-Type'. 161 | # - #sub_type: Returns second part of the string value of field 'Content-Type'. 162 | # - #range: Returns an array of Range objects of field 'Range', or +nil+. 163 | # - #range_length: Returns the integer length of the range given in field 'Content-Range'. 164 | # - #type_params: Returns the string parameters for 'Content-Type'. 165 | # 166 | # === Queries 167 | # 168 | # - #chunked?: Returns whether field 'Transfer-Encoding' is set to 'chunked'. 169 | # - #connection_close?: Returns whether field 'Connection' is set to 'close'. 170 | # - #connection_keep_alive?: Returns whether field 'Connection' is set to 'keep-alive'. 171 | # - #key?: Returns whether a given key exists. 172 | # 173 | # === Iterators 174 | # 175 | # - #each_capitalized: Passes each field capitalized-name/value pair to the block. 176 | # - #each_capitalized_name: Passes each capitalized field name to the block. 177 | # - #each_header: Passes each field name/value pair to the block. 178 | # - #each_name: Passes each field name to the block. 179 | # - #each_value: Passes each string field value to the block. 180 | # 181 | module Net::HTTPHeader 182 | # The maximum length of HTTP header keys. 183 | MAX_KEY_LENGTH = 1024 184 | # The maximum length of HTTP header values. 185 | MAX_FIELD_LENGTH = 65536 186 | 187 | def initialize_http_header(initheader) #:nodoc: 188 | @header = {} 189 | return unless initheader 190 | initheader.each do |key, value| 191 | warn "net/http: duplicated HTTP header: #{key}", uplevel: 3 if key?(key) and $VERBOSE 192 | if value.nil? 193 | warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE 194 | else 195 | value = value.strip # raise error for invalid byte sequences 196 | if key.to_s.bytesize > MAX_KEY_LENGTH 197 | raise ArgumentError, "too long (#{key.bytesize} bytes) header: #{key[0, 30].inspect}..." 198 | end 199 | if value.to_s.bytesize > MAX_FIELD_LENGTH 200 | raise ArgumentError, "header #{key} has too long field value: #{value.bytesize}" 201 | end 202 | if value.count("\r\n") > 0 203 | raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF" 204 | end 205 | @header[key.downcase.to_s] = [value] 206 | end 207 | end 208 | end 209 | 210 | def size #:nodoc: obsolete 211 | @header.size 212 | end 213 | 214 | alias length size #:nodoc: obsolete 215 | 216 | # Returns the string field value for the case-insensitive field +key+, 217 | # or +nil+ if there is no such key; 218 | # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]: 219 | # 220 | # res = Net::HTTP.get_response(hostname, '/todos/1') 221 | # res['Connection'] # => "keep-alive" 222 | # res['Nosuch'] # => nil 223 | # 224 | # Note that some field values may be retrieved via convenience methods; 225 | # see {Getters}[rdoc-ref:Net::HTTPHeader@Getters]. 226 | def [](key) 227 | a = @header[key.downcase.to_s] or return nil 228 | a.join(', ') 229 | end 230 | 231 | # Sets the value for the case-insensitive +key+ to +val+, 232 | # overwriting the previous value if the field exists; 233 | # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]: 234 | # 235 | # req = Net::HTTP::Get.new(uri) 236 | # req['Accept'] # => "*/*" 237 | # req['Accept'] = 'text/html' 238 | # req['Accept'] # => "text/html" 239 | # 240 | # Note that some field values may be set via convenience methods; 241 | # see {Setters}[rdoc-ref:Net::HTTPHeader@Setters]. 242 | def []=(key, val) 243 | unless val 244 | @header.delete key.downcase.to_s 245 | return val 246 | end 247 | set_field(key, val) 248 | end 249 | 250 | # Adds value +val+ to the value array for field +key+ if the field exists; 251 | # creates the field with the given +key+ and +val+ if it does not exist. 252 | # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]: 253 | # 254 | # req = Net::HTTP::Get.new(uri) 255 | # req.add_field('Foo', 'bar') 256 | # req['Foo'] # => "bar" 257 | # req.add_field('Foo', 'baz') 258 | # req['Foo'] # => "bar, baz" 259 | # req.add_field('Foo', %w[baz bam]) 260 | # req['Foo'] # => "bar, baz, baz, bam" 261 | # req.get_fields('Foo') # => ["bar", "baz", "baz", "bam"] 262 | # 263 | def add_field(key, val) 264 | stringified_downcased_key = key.downcase.to_s 265 | if @header.key?(stringified_downcased_key) 266 | append_field_value(@header[stringified_downcased_key], val) 267 | else 268 | set_field(key, val) 269 | end 270 | end 271 | 272 | # :stopdoc: 273 | private def set_field(key, val) 274 | case val 275 | when Enumerable 276 | ary = [] 277 | append_field_value(ary, val) 278 | @header[key.downcase.to_s] = ary 279 | else 280 | val = val.to_s # for compatibility use to_s instead of to_str 281 | if val.b.count("\r\n") > 0 282 | raise ArgumentError, 'header field value cannot include CR/LF' 283 | end 284 | @header[key.downcase.to_s] = [val] 285 | end 286 | end 287 | 288 | private def append_field_value(ary, val) 289 | case val 290 | when Enumerable 291 | val.each{|x| append_field_value(ary, x)} 292 | else 293 | val = val.to_s 294 | if /[\r\n]/n.match?(val.b) 295 | raise ArgumentError, 'header field value cannot include CR/LF' 296 | end 297 | ary.push val 298 | end 299 | end 300 | # :startdoc: 301 | 302 | # Returns the array field value for the given +key+, 303 | # or +nil+ if there is no such field; 304 | # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]: 305 | # 306 | # res = Net::HTTP.get_response(hostname, '/todos/1') 307 | # res.get_fields('Connection') # => ["keep-alive"] 308 | # res.get_fields('Nosuch') # => nil 309 | # 310 | def get_fields(key) 311 | stringified_downcased_key = key.downcase.to_s 312 | return nil unless @header[stringified_downcased_key] 313 | @header[stringified_downcased_key].dup 314 | end 315 | 316 | # call-seq: 317 | # fetch(key, default_val = nil) {|key| ... } -> object 318 | # fetch(key, default_val = nil) -> value or default_val 319 | # 320 | # With a block, returns the string value for +key+ if it exists; 321 | # otherwise returns the value of the block; 322 | # ignores the +default_val+; 323 | # see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]: 324 | # 325 | # res = Net::HTTP.get_response(hostname, '/todos/1') 326 | # 327 | # # Field exists; block not called. 328 | # res.fetch('Connection') do |value| 329 | # fail 'Cannot happen' 330 | # end # => "keep-alive" 331 | # 332 | # # Field does not exist; block called. 333 | # res.fetch('Nosuch') do |value| 334 | # value.downcase 335 | # end # => "nosuch" 336 | # 337 | # With no block, returns the string value for +key+ if it exists; 338 | # otherwise, returns +default_val+ if it was given; 339 | # otherwise raises an exception: 340 | # 341 | # res.fetch('Connection', 'Foo') # => "keep-alive" 342 | # res.fetch('Nosuch', 'Foo') # => "Foo" 343 | # res.fetch('Nosuch') # Raises KeyError. 344 | # 345 | def fetch(key, *args, &block) #:yield: +key+ 346 | a = @header.fetch(key.downcase.to_s, *args, &block) 347 | a.kind_of?(Array) ? a.join(', ') : a 348 | end 349 | 350 | # Calls the block with each key/value pair: 351 | # 352 | # res = Net::HTTP.get_response(hostname, '/todos/1') 353 | # res.each_header do |key, value| 354 | # p [key, value] if key.start_with?('c') 355 | # end 356 | # 357 | # Output: 358 | # 359 | # ["content-type", "application/json; charset=utf-8"] 360 | # ["connection", "keep-alive"] 361 | # ["cache-control", "max-age=43200"] 362 | # ["cf-cache-status", "HIT"] 363 | # ["cf-ray", "771d17e9bc542cf5-ORD"] 364 | # 365 | # Returns an enumerator if no block is given. 366 | # 367 | # Net::HTTPHeader#each is an alias for Net::HTTPHeader#each_header. 368 | def each_header #:yield: +key+, +value+ 369 | block_given? or return enum_for(__method__) { @header.size } 370 | @header.each do |k,va| 371 | yield k, va.join(', ') 372 | end 373 | end 374 | 375 | alias each each_header 376 | 377 | # Calls the block with each field key: 378 | # 379 | # res = Net::HTTP.get_response(hostname, '/todos/1') 380 | # res.each_key do |key| 381 | # p key if key.start_with?('c') 382 | # end 383 | # 384 | # Output: 385 | # 386 | # "content-type" 387 | # "connection" 388 | # "cache-control" 389 | # "cf-cache-status" 390 | # "cf-ray" 391 | # 392 | # Returns an enumerator if no block is given. 393 | # 394 | # Net::HTTPHeader#each_name is an alias for Net::HTTPHeader#each_key. 395 | def each_name(&block) #:yield: +key+ 396 | block_given? or return enum_for(__method__) { @header.size } 397 | @header.each_key(&block) 398 | end 399 | 400 | alias each_key each_name 401 | 402 | # Calls the block with each capitalized field name: 403 | # 404 | # res = Net::HTTP.get_response(hostname, '/todos/1') 405 | # res.each_capitalized_name do |key| 406 | # p key if key.start_with?('C') 407 | # end 408 | # 409 | # Output: 410 | # 411 | # "Content-Type" 412 | # "Connection" 413 | # "Cache-Control" 414 | # "Cf-Cache-Status" 415 | # "Cf-Ray" 416 | # 417 | # The capitalization is system-dependent; 418 | # see {Case Mapping}[https://docs.ruby-lang.org/en/master/case_mapping_rdoc.html]. 419 | # 420 | # Returns an enumerator if no block is given. 421 | def each_capitalized_name #:yield: +key+ 422 | block_given? or return enum_for(__method__) { @header.size } 423 | @header.each_key do |k| 424 | yield capitalize(k) 425 | end 426 | end 427 | 428 | # Calls the block with each string field value: 429 | # 430 | # res = Net::HTTP.get_response(hostname, '/todos/1') 431 | # res.each_value do |value| 432 | # p value if value.start_with?('c') 433 | # end 434 | # 435 | # Output: 436 | # 437 | # "chunked" 438 | # "cf-q-config;dur=6.0000002122251e-06" 439 | # "cloudflare" 440 | # 441 | # Returns an enumerator if no block is given. 442 | def each_value #:yield: +value+ 443 | block_given? or return enum_for(__method__) { @header.size } 444 | @header.each_value do |va| 445 | yield va.join(', ') 446 | end 447 | end 448 | 449 | # Removes the header for the given case-insensitive +key+ 450 | # (see {Fields}[rdoc-ref:Net::HTTPHeader@Fields]); 451 | # returns the deleted value, or +nil+ if no such field exists: 452 | # 453 | # req = Net::HTTP::Get.new(uri) 454 | # req.delete('Accept') # => ["*/*"] 455 | # req.delete('Nosuch') # => nil 456 | # 457 | def delete(key) 458 | @header.delete(key.downcase.to_s) 459 | end 460 | 461 | # Returns +true+ if the field for the case-insensitive +key+ exists, +false+ otherwise: 462 | # 463 | # req = Net::HTTP::Get.new(uri) 464 | # req.key?('Accept') # => true 465 | # req.key?('Nosuch') # => false 466 | # 467 | def key?(key) 468 | @header.key?(key.downcase.to_s) 469 | end 470 | 471 | # Returns a hash of the key/value pairs: 472 | # 473 | # req = Net::HTTP::Get.new(uri) 474 | # req.to_hash 475 | # # => 476 | # {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"], 477 | # "accept"=>["*/*"], 478 | # "user-agent"=>["Ruby"], 479 | # "host"=>["jsonplaceholder.typicode.com"]} 480 | # 481 | def to_hash 482 | @header.dup 483 | end 484 | 485 | # Like #each_header, but the keys are returned in capitalized form. 486 | # 487 | # Net::HTTPHeader#canonical_each is an alias for Net::HTTPHeader#each_capitalized. 488 | def each_capitalized 489 | block_given? or return enum_for(__method__) { @header.size } 490 | @header.each do |k,v| 491 | yield capitalize(k), v.join(', ') 492 | end 493 | end 494 | 495 | alias canonical_each each_capitalized 496 | 497 | def capitalize(name) # :nodoc: 498 | name.to_s.split('-'.freeze).map {|s| s.capitalize }.join('-'.freeze) 499 | end 500 | private :capitalize 501 | 502 | # Returns an array of Range objects that represent 503 | # the value of field 'Range', 504 | # or +nil+ if there is no such field; 505 | # see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]: 506 | # 507 | # req = Net::HTTP::Get.new(uri) 508 | # req['Range'] = 'bytes=0-99,200-299,400-499' 509 | # req.range # => [0..99, 200..299, 400..499] 510 | # req.delete('Range') 511 | # req.range # # => nil 512 | # 513 | def range 514 | return nil unless @header['range'] 515 | 516 | value = self['Range'] 517 | # byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec ) 518 | # *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] ) 519 | # corrected collected ABNF 520 | # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1 521 | # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C 522 | # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5 523 | unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value 524 | raise Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'" 525 | end 526 | 527 | byte_range_set = $1 528 | result = byte_range_set.split(/,/).map {|spec| 529 | m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or 530 | raise Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'" 531 | d1 = m[1].to_i 532 | d2 = m[2].to_i 533 | if m[1] and m[2] 534 | if d1 > d2 535 | raise Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'" 536 | end 537 | d1..d2 538 | elsif m[1] 539 | d1..-1 540 | elsif m[2] 541 | -d2..-1 542 | else 543 | raise Net::HTTPHeaderSyntaxError, 'range is not specified' 544 | end 545 | } 546 | # if result.empty? 547 | # byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec 548 | # but above regexp already denies it. 549 | if result.size == 1 && result[0].begin == 0 && result[0].end == -1 550 | raise Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length' 551 | end 552 | result 553 | end 554 | 555 | # call-seq: 556 | # set_range(length) -> length 557 | # set_range(offset, length) -> range 558 | # set_range(begin..length) -> range 559 | # 560 | # Sets the value for field 'Range'; 561 | # see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]: 562 | # 563 | # With argument +length+: 564 | # 565 | # req = Net::HTTP::Get.new(uri) 566 | # req.set_range(100) # => 100 567 | # req['Range'] # => "bytes=0-99" 568 | # 569 | # With arguments +offset+ and +length+: 570 | # 571 | # req.set_range(100, 100) # => 100...200 572 | # req['Range'] # => "bytes=100-199" 573 | # 574 | # With argument +range+: 575 | # 576 | # req.set_range(100..199) # => 100..199 577 | # req['Range'] # => "bytes=100-199" 578 | # 579 | # Net::HTTPHeader#range= is an alias for Net::HTTPHeader#set_range. 580 | def set_range(r, e = nil) 581 | unless r 582 | @header.delete 'range' 583 | return r 584 | end 585 | r = (r...r+e) if e 586 | case r 587 | when Numeric 588 | n = r.to_i 589 | rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}") 590 | when Range 591 | first = r.first 592 | last = r.end 593 | last -= 1 if r.exclude_end? 594 | if last == -1 595 | rangestr = (first > 0 ? "#{first}-" : "-#{-first}") 596 | else 597 | raise Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0 598 | raise Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0 599 | raise Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last 600 | rangestr = "#{first}-#{last}" 601 | end 602 | else 603 | raise TypeError, 'Range/Integer is required' 604 | end 605 | @header['range'] = ["bytes=#{rangestr}"] 606 | r 607 | end 608 | 609 | alias range= set_range 610 | 611 | # Returns the value of field 'Content-Length' as an integer, 612 | # or +nil+ if there is no such field; 613 | # see {Content-Length request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-request-header]: 614 | # 615 | # res = Net::HTTP.get_response(hostname, '/nosuch/1') 616 | # res.content_length # => 2 617 | # res = Net::HTTP.get_response(hostname, '/todos/1') 618 | # res.content_length # => nil 619 | # 620 | def content_length 621 | return nil unless key?('Content-Length') 622 | len = self['Content-Length'].slice(/\d+/) or 623 | raise Net::HTTPHeaderSyntaxError, 'wrong Content-Length format' 624 | len.to_i 625 | end 626 | 627 | # Sets the value of field 'Content-Length' to the given numeric; 628 | # see {Content-Length response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-response-header]: 629 | # 630 | # _uri = uri.dup 631 | # hostname = _uri.hostname # => "jsonplaceholder.typicode.com" 632 | # _uri.path = '/posts' # => "/posts" 633 | # req = Net::HTTP::Post.new(_uri) # => # 634 | # req.body = '{"title": "foo","body": "bar","userId": 1}' 635 | # req.content_length = req.body.size # => 42 636 | # req.content_type = 'application/json' 637 | # res = Net::HTTP.start(hostname) do |http| 638 | # http.request(req) 639 | # end # => # 640 | # 641 | def content_length=(len) 642 | unless len 643 | @header.delete 'content-length' 644 | return nil 645 | end 646 | @header['content-length'] = [len.to_i.to_s] 647 | end 648 | 649 | # Returns +true+ if field 'Transfer-Encoding' 650 | # exists and has value 'chunked', 651 | # +false+ otherwise; 652 | # see {Transfer-Encoding response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#transfer-encoding-response-header]: 653 | # 654 | # res = Net::HTTP.get_response(hostname, '/todos/1') 655 | # res['Transfer-Encoding'] # => "chunked" 656 | # res.chunked? # => true 657 | # 658 | def chunked? 659 | return false unless @header['transfer-encoding'] 660 | field = self['Transfer-Encoding'] 661 | (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false 662 | end 663 | 664 | # Returns a Range object representing the value of field 665 | # 'Content-Range', or +nil+ if no such field exists; 666 | # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]: 667 | # 668 | # res = Net::HTTP.get_response(hostname, '/todos/1') 669 | # res['Content-Range'] # => nil 670 | # res['Content-Range'] = 'bytes 0-499/1000' 671 | # res['Content-Range'] # => "bytes 0-499/1000" 672 | # res.content_range # => 0..499 673 | # 674 | def content_range 675 | return nil unless @header['content-range'] 676 | m = %r<\A\s*(\w+)\s+(\d+)-(\d+)/(\d+|\*)>.match(self['Content-Range']) or 677 | raise Net::HTTPHeaderSyntaxError, 'wrong Content-Range format' 678 | return unless m[1] == 'bytes' 679 | m[2].to_i .. m[3].to_i 680 | end 681 | 682 | # Returns the integer representing length of the value of field 683 | # 'Content-Range', or +nil+ if no such field exists; 684 | # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]: 685 | # 686 | # res = Net::HTTP.get_response(hostname, '/todos/1') 687 | # res['Content-Range'] # => nil 688 | # res['Content-Range'] = 'bytes 0-499/1000' 689 | # res.range_length # => 500 690 | # 691 | def range_length 692 | r = content_range() or return nil 693 | r.end - r.begin + 1 694 | end 695 | 696 | # Returns the {media type}[https://en.wikipedia.org/wiki/Media_type] 697 | # from the value of field 'Content-Type', 698 | # or +nil+ if no such field exists; 699 | # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]: 700 | # 701 | # res = Net::HTTP.get_response(hostname, '/todos/1') 702 | # res['content-type'] # => "application/json; charset=utf-8" 703 | # res.content_type # => "application/json" 704 | # 705 | def content_type 706 | main = main_type() 707 | return nil unless main 708 | 709 | sub = sub_type() 710 | if sub 711 | "#{main}/#{sub}" 712 | else 713 | main 714 | end 715 | end 716 | 717 | # Returns the leading ('type') part of the 718 | # {media type}[https://en.wikipedia.org/wiki/Media_type] 719 | # from the value of field 'Content-Type', 720 | # or +nil+ if no such field exists; 721 | # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]: 722 | # 723 | # res = Net::HTTP.get_response(hostname, '/todos/1') 724 | # res['content-type'] # => "application/json; charset=utf-8" 725 | # res.main_type # => "application" 726 | # 727 | def main_type 728 | return nil unless @header['content-type'] 729 | self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip 730 | end 731 | 732 | # Returns the trailing ('subtype') part of the 733 | # {media type}[https://en.wikipedia.org/wiki/Media_type] 734 | # from the value of field 'Content-Type', 735 | # or +nil+ if no such field exists; 736 | # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]: 737 | # 738 | # res = Net::HTTP.get_response(hostname, '/todos/1') 739 | # res['content-type'] # => "application/json; charset=utf-8" 740 | # res.sub_type # => "json" 741 | # 742 | def sub_type 743 | return nil unless @header['content-type'] 744 | _, sub = *self['Content-Type'].split(';').first.to_s.split('/') 745 | return nil unless sub 746 | sub.strip 747 | end 748 | 749 | # Returns the trailing ('parameters') part of the value of field 'Content-Type', 750 | # or +nil+ if no such field exists; 751 | # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]: 752 | # 753 | # res = Net::HTTP.get_response(hostname, '/todos/1') 754 | # res['content-type'] # => "application/json; charset=utf-8" 755 | # res.type_params # => {"charset"=>"utf-8"} 756 | # 757 | def type_params 758 | result = {} 759 | list = self['Content-Type'].to_s.split(';') 760 | list.shift 761 | list.each do |param| 762 | k, v = *param.split('=', 2) 763 | result[k.strip] = v.strip 764 | end 765 | result 766 | end 767 | 768 | # Sets the value of field 'Content-Type'; 769 | # returns the new value; 770 | # see {Content-Type request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-request-header]: 771 | # 772 | # req = Net::HTTP::Get.new(uri) 773 | # req.set_content_type('application/json') # => ["application/json"] 774 | # 775 | # Net::HTTPHeader#content_type= is an alias for Net::HTTPHeader#set_content_type. 776 | def set_content_type(type, params = {}) 777 | @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')] 778 | end 779 | 780 | alias content_type= set_content_type 781 | 782 | # Sets the request body to a URL-encoded string derived from argument +params+, 783 | # and sets request header field 'Content-Type' 784 | # to 'application/x-www-form-urlencoded'. 785 | # 786 | # The resulting request is suitable for HTTP request +POST+ or +PUT+. 787 | # 788 | # Argument +params+ must be suitable for use as argument +enum+ to 789 | # {URI.encode_www_form}[https://docs.ruby-lang.org/en/master/URI.html#method-c-encode_www_form]. 790 | # 791 | # With only argument +params+ given, 792 | # sets the body to a URL-encoded string with the default separator '&': 793 | # 794 | # req = Net::HTTP::Post.new('example.com') 795 | # 796 | # req.set_form_data(q: 'ruby', lang: 'en') 797 | # req.body # => "q=ruby&lang=en" 798 | # req['Content-Type'] # => "application/x-www-form-urlencoded" 799 | # 800 | # req.set_form_data([['q', 'ruby'], ['lang', 'en']]) 801 | # req.body # => "q=ruby&lang=en" 802 | # 803 | # req.set_form_data(q: ['ruby', 'perl'], lang: 'en') 804 | # req.body # => "q=ruby&q=perl&lang=en" 805 | # 806 | # req.set_form_data([['q', 'ruby'], ['q', 'perl'], ['lang', 'en']]) 807 | # req.body # => "q=ruby&q=perl&lang=en" 808 | # 809 | # With string argument +sep+ also given, 810 | # uses that string as the separator: 811 | # 812 | # req.set_form_data({q: 'ruby', lang: 'en'}, '|') 813 | # req.body # => "q=ruby|lang=en" 814 | # 815 | # Net::HTTPHeader#form_data= is an alias for Net::HTTPHeader#set_form_data. 816 | def set_form_data(params, sep = '&') 817 | query = URI.encode_www_form(params) 818 | query.gsub!(/&/, sep) if sep != '&' 819 | self.body = query 820 | self.content_type = 'application/x-www-form-urlencoded' 821 | end 822 | 823 | alias form_data= set_form_data 824 | 825 | # Stores form data to be used in a +POST+ or +PUT+ request. 826 | # 827 | # The form data given in +params+ consists of zero or more fields; 828 | # each field is: 829 | # 830 | # - A scalar value. 831 | # - A name/value pair. 832 | # - An IO stream opened for reading. 833 | # 834 | # Argument +params+ should be an 835 | # {Enumerable}[https://docs.ruby-lang.org/en/master/Enumerable.html#module-Enumerable-label-Enumerable+in+Ruby+Classes] 836 | # (method params.map will be called), 837 | # and is often an array or hash. 838 | # 839 | # First, we set up a request: 840 | # 841 | # _uri = uri.dup 842 | # _uri.path ='/posts' 843 | # req = Net::HTTP::Post.new(_uri) 844 | # 845 | # Argument +params+ As an Array 846 | # 847 | # When +params+ is an array, 848 | # each of its elements is a subarray that defines a field; 849 | # the subarray may contain: 850 | # 851 | # - One string: 852 | # 853 | # req.set_form([['foo'], ['bar'], ['baz']]) 854 | # 855 | # - Two strings: 856 | # 857 | # req.set_form([%w[foo 0], %w[bar 1], %w[baz 2]]) 858 | # 859 | # - When argument +enctype+ (see below) is given as 860 | # 'multipart/form-data': 861 | # 862 | # - A string name and an IO stream opened for reading: 863 | # 864 | # require 'stringio' 865 | # req.set_form([['file', StringIO.new('Ruby is cool.')]]) 866 | # 867 | # - A string name, an IO stream opened for reading, 868 | # and an options hash, which may contain these entries: 869 | # 870 | # - +:filename+: The name of the file to use. 871 | # - +:content_type+: The content type of the uploaded file. 872 | # 873 | # Example: 874 | # 875 | # req.set_form([['file', file, {filename: "other-filename.foo"}]] 876 | # 877 | # The various forms may be mixed: 878 | # 879 | # req.set_form(['foo', %w[bar 1], ['file', file]]) 880 | # 881 | # Argument +params+ As a Hash 882 | # 883 | # When +params+ is a hash, 884 | # each of its entries is a name/value pair that defines a field: 885 | # 886 | # - The name is a string. 887 | # - The value may be: 888 | # 889 | # - +nil+. 890 | # - Another string. 891 | # - An IO stream opened for reading 892 | # (only when argument +enctype+ -- see below -- is given as 893 | # 'multipart/form-data'). 894 | # 895 | # Examples: 896 | # 897 | # # Nil-valued fields. 898 | # req.set_form({'foo' => nil, 'bar' => nil, 'baz' => nil}) 899 | # 900 | # # String-valued fields. 901 | # req.set_form({'foo' => 0, 'bar' => 1, 'baz' => 2}) 902 | # 903 | # # IO-valued field. 904 | # require 'stringio' 905 | # req.set_form({'file' => StringIO.new('Ruby is cool.')}) 906 | # 907 | # # Mixture of fields. 908 | # req.set_form({'foo' => nil, 'bar' => 1, 'file' => file}) 909 | # 910 | # Optional argument +enctype+ specifies the value to be given 911 | # to field 'Content-Type', and must be one of: 912 | # 913 | # - 'application/x-www-form-urlencoded' (the default). 914 | # - 'multipart/form-data'; 915 | # see {RFC 7578}[https://www.rfc-editor.org/rfc/rfc7578]. 916 | # 917 | # Optional argument +formopt+ is a hash of options 918 | # (applicable only when argument +enctype+ 919 | # is 'multipart/form-data') 920 | # that may include the following entries: 921 | # 922 | # - +:boundary+: The value is the boundary string for the multipart message. 923 | # If not given, the boundary is a random string. 924 | # See {Boundary}[https://www.rfc-editor.org/rfc/rfc7578#section-4.1]. 925 | # - +:charset+: Value is the character set for the form submission. 926 | # Field names and values of non-file fields should be encoded with this charset. 927 | # 928 | def set_form(params, enctype='application/x-www-form-urlencoded', formopt={}) 929 | @body_data = params 930 | @body = nil 931 | @body_stream = nil 932 | @form_option = formopt 933 | case enctype 934 | when /\Aapplication\/x-www-form-urlencoded\z/i, 935 | /\Amultipart\/form-data\z/i 936 | self.content_type = enctype 937 | else 938 | raise ArgumentError, "invalid enctype: #{enctype}" 939 | end 940 | end 941 | 942 | # Sets header 'Authorization' using the given 943 | # +account+ and +password+ strings: 944 | # 945 | # req.basic_auth('my_account', 'my_password') 946 | # req['Authorization'] 947 | # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA==" 948 | # 949 | def basic_auth(account, password) 950 | @header['authorization'] = [basic_encode(account, password)] 951 | end 952 | 953 | # Sets header 'Proxy-Authorization' using the given 954 | # +account+ and +password+ strings: 955 | # 956 | # req.proxy_basic_auth('my_account', 'my_password') 957 | # req['Proxy-Authorization'] 958 | # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA==" 959 | # 960 | def proxy_basic_auth(account, password) 961 | @header['proxy-authorization'] = [basic_encode(account, password)] 962 | end 963 | 964 | def basic_encode(account, password) # :nodoc: 965 | 'Basic ' + ["#{account}:#{password}"].pack('m0') 966 | end 967 | private :basic_encode 968 | 969 | # Returns whether the HTTP session is to be closed. 970 | def connection_close? 971 | token = /(?:\A|,)\s*close\s*(?:\z|,)/i 972 | @header['connection']&.grep(token) {return true} 973 | @header['proxy-connection']&.grep(token) {return true} 974 | false 975 | end 976 | 977 | # Returns whether the HTTP session is to be kept alive. 978 | def connection_keep_alive? 979 | token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i 980 | @header['connection']&.grep(token) {return true} 981 | @header['proxy-connection']&.grep(token) {return true} 982 | false 983 | end 984 | 985 | end 986 | -------------------------------------------------------------------------------- /test/net/http/test_http.rb: -------------------------------------------------------------------------------- 1 | # coding: US-ASCII 2 | # frozen_string_literal: false 3 | require 'test/unit' 4 | require 'net/http' 5 | require 'stringio' 6 | require_relative 'utils' 7 | 8 | class TestNetHTTP < Test::Unit::TestCase 9 | 10 | def test_class_Proxy 11 | no_proxy_class = Net::HTTP.Proxy nil 12 | 13 | assert_equal Net::HTTP, no_proxy_class 14 | 15 | proxy_class = Net::HTTP.Proxy 'proxy.example', 8000, 'user', 'pass' 16 | 17 | assert_not_equal Net::HTTP, proxy_class 18 | 19 | assert_operator proxy_class, :<, Net::HTTP 20 | 21 | assert_equal 'proxy.example', proxy_class.proxy_address 22 | assert_equal 8000, proxy_class.proxy_port 23 | assert_equal 'user', proxy_class.proxy_user 24 | assert_equal 'pass', proxy_class.proxy_pass 25 | 26 | http = proxy_class.new 'hostname.example' 27 | 28 | assert_not_predicate http, :proxy_from_env? 29 | 30 | 31 | proxy_class = Net::HTTP.Proxy 'proxy.example' 32 | assert_equal 'proxy.example', proxy_class.proxy_address 33 | assert_equal 80, proxy_class.proxy_port 34 | end 35 | 36 | def test_class_Proxy_from_ENV 37 | TestNetHTTPUtils.clean_http_proxy_env do 38 | ENV['http_proxy'] = 'http://proxy.example:8000' 39 | 40 | # These are ignored on purpose. See Bug 4388 and Feature 6546 41 | ENV['http_proxy_user'] = 'user' 42 | ENV['http_proxy_pass'] = 'pass' 43 | 44 | proxy_class = Net::HTTP.Proxy :ENV 45 | 46 | assert_not_equal Net::HTTP, proxy_class 47 | 48 | assert_operator proxy_class, :<, Net::HTTP 49 | 50 | assert_nil proxy_class.proxy_address 51 | assert_nil proxy_class.proxy_user 52 | assert_nil proxy_class.proxy_pass 53 | 54 | assert_not_equal 8000, proxy_class.proxy_port 55 | 56 | http = proxy_class.new 'hostname.example' 57 | 58 | assert http.proxy_from_env? 59 | end 60 | end 61 | 62 | def test_addr_port 63 | http = Net::HTTP.new 'hostname.example', nil, nil 64 | addr_port = http.__send__ :addr_port 65 | assert_equal 'hostname.example', addr_port 66 | 67 | http.use_ssl = true 68 | addr_port = http.__send__ :addr_port 69 | assert_equal 'hostname.example:80', addr_port 70 | 71 | http = Net::HTTP.new '203.0.113.1', nil, nil 72 | addr_port = http.__send__ :addr_port 73 | assert_equal '203.0.113.1', addr_port 74 | 75 | http.use_ssl = true 76 | addr_port = http.__send__ :addr_port 77 | assert_equal '203.0.113.1:80', addr_port 78 | 79 | http = Net::HTTP.new '2001:db8::1', nil, nil 80 | addr_port = http.__send__ :addr_port 81 | assert_equal '[2001:db8::1]', addr_port 82 | 83 | http.use_ssl = true 84 | addr_port = http.__send__ :addr_port 85 | assert_equal '[2001:db8::1]:80', addr_port 86 | 87 | end 88 | 89 | def test_edit_path 90 | http = Net::HTTP.new 'hostname.example', nil, nil 91 | 92 | edited = http.send :edit_path, '/path' 93 | 94 | assert_equal '/path', edited 95 | 96 | http.use_ssl = true 97 | 98 | edited = http.send :edit_path, '/path' 99 | 100 | assert_equal '/path', edited 101 | end 102 | 103 | def test_edit_path_proxy 104 | http = Net::HTTP.new 'hostname.example', nil, 'proxy.example' 105 | 106 | edited = http.send :edit_path, '/path' 107 | 108 | assert_equal 'http://hostname.example/path', edited 109 | 110 | http.use_ssl = true 111 | 112 | edited = http.send :edit_path, '/path' 113 | 114 | assert_equal '/path', edited 115 | end 116 | 117 | def test_proxy_address 118 | TestNetHTTPUtils.clean_http_proxy_env do 119 | http = Net::HTTP.new 'hostname.example', nil, 'proxy.example' 120 | assert_equal 'proxy.example', http.proxy_address 121 | 122 | http = Net::HTTP.new 'hostname.example', nil 123 | assert_equal nil, http.proxy_address 124 | end 125 | end 126 | 127 | def test_proxy_address_no_proxy 128 | TestNetHTTPUtils.clean_http_proxy_env do 129 | http = Net::HTTP.new 'hostname.example', nil, 'proxy.com', nil, nil, nil, 'example' 130 | assert_nil http.proxy_address 131 | 132 | http = Net::HTTP.new '10.224.1.1', nil, 'proxy.com', nil, nil, nil, 'example,10.224.0.0/22' 133 | assert_nil http.proxy_address 134 | end 135 | end 136 | 137 | def test_proxy_from_env_ENV 138 | TestNetHTTPUtils.clean_http_proxy_env do 139 | ENV['http_proxy'] = 'http://proxy.example:8000' 140 | 141 | assert_equal false, Net::HTTP.proxy_class? 142 | http = Net::HTTP.new 'hostname.example' 143 | 144 | assert_equal true, http.proxy_from_env? 145 | end 146 | end 147 | 148 | def test_proxy_address_ENV 149 | TestNetHTTPUtils.clean_http_proxy_env do 150 | ENV['http_proxy'] = 'http://proxy.example:8000' 151 | 152 | http = Net::HTTP.new 'hostname.example' 153 | 154 | assert_equal 'proxy.example', http.proxy_address 155 | end 156 | end 157 | 158 | def test_proxy_eh_no_proxy 159 | TestNetHTTPUtils.clean_http_proxy_env do 160 | assert_equal false, Net::HTTP.new('hostname.example', nil, nil).proxy? 161 | end 162 | end 163 | 164 | def test_proxy_eh_ENV 165 | TestNetHTTPUtils.clean_http_proxy_env do 166 | ENV['http_proxy'] = 'http://proxy.example:8000' 167 | 168 | http = Net::HTTP.new 'hostname.example' 169 | 170 | assert_equal true, http.proxy? 171 | end 172 | end 173 | 174 | def test_proxy_eh_ENV_with_user 175 | TestNetHTTPUtils.clean_http_proxy_env do 176 | ENV['http_proxy'] = 'http://foo:bar@proxy.example:8000' 177 | 178 | http = Net::HTTP.new 'hostname.example' 179 | 180 | assert_equal true, http.proxy? 181 | assert_equal 'foo', http.proxy_user 182 | assert_equal 'bar', http.proxy_pass 183 | end 184 | end 185 | 186 | def test_proxy_eh_ENV_with_urlencoded_user 187 | TestNetHTTPUtils.clean_http_proxy_env do 188 | ENV['http_proxy'] = 'http://Y%5CX:R%25S%5D%20%3FX@proxy.example:8000' 189 | 190 | http = Net::HTTP.new 'hostname.example' 191 | 192 | assert_equal true, http.proxy? 193 | assert_equal "Y\\X", http.proxy_user 194 | assert_equal "R%S] ?X", http.proxy_pass 195 | end 196 | end 197 | 198 | def test_proxy_eh_ENV_none_set 199 | TestNetHTTPUtils.clean_http_proxy_env do 200 | assert_equal false, Net::HTTP.new('hostname.example').proxy? 201 | end 202 | end 203 | 204 | def test_proxy_eh_ENV_no_proxy 205 | TestNetHTTPUtils.clean_http_proxy_env do 206 | ENV['http_proxy'] = 'http://proxy.example:8000' 207 | ENV['no_proxy'] = 'hostname.example' 208 | 209 | assert_equal false, Net::HTTP.new('hostname.example').proxy? 210 | end 211 | end 212 | 213 | def test_proxy_port 214 | TestNetHTTPUtils.clean_http_proxy_env do 215 | http = Net::HTTP.new 'example', nil, 'proxy.example' 216 | assert_equal 'proxy.example', http.proxy_address 217 | assert_equal 80, http.proxy_port 218 | http = Net::HTTP.new 'example', nil, 'proxy.example', 8000 219 | assert_equal 8000, http.proxy_port 220 | http = Net::HTTP.new 'example', nil 221 | assert_equal nil, http.proxy_port 222 | end 223 | end 224 | 225 | def test_proxy_port_ENV 226 | TestNetHTTPUtils.clean_http_proxy_env do 227 | ENV['http_proxy'] = 'http://proxy.example:8000' 228 | 229 | http = Net::HTTP.new 'hostname.example' 230 | 231 | assert_equal 8000, http.proxy_port 232 | end 233 | end 234 | 235 | def test_newobj 236 | TestNetHTTPUtils.clean_http_proxy_env do 237 | ENV['http_proxy'] = 'http://proxy.example:8000' 238 | 239 | http = Net::HTTP.newobj 'hostname.example' 240 | 241 | assert_equal false, http.proxy? 242 | end 243 | end 244 | 245 | def test_failure_message_includes_failed_domain_and_port 246 | # hostname to be included in the error message 247 | host = Struct.new(:to_s).new("") 248 | port = 2119 249 | # hack to let TCPSocket.open fail 250 | def host.to_str; raise SocketError, "open failure"; end 251 | uri = Struct.new(:scheme, :hostname, :port).new("http", host, port) 252 | assert_raise_with_message(SocketError, /#{host}:#{port}/) do 253 | TestNetHTTPUtils.clean_http_proxy_env{ Net::HTTP.get(uri) } 254 | end 255 | end 256 | 257 | def test_default_configuration 258 | Net::HTTP.default_configuration = { open_timeout: 5 } 259 | http = Net::HTTP.new 'hostname.example' 260 | assert_equal 5, http.open_timeout 261 | assert_equal 60, http.read_timeout 262 | 263 | http.open_timeout = 10 264 | assert_equal 10, http.open_timeout 265 | ensure 266 | Net::HTTP.default_configuration = nil 267 | end 268 | 269 | end 270 | 271 | module TestNetHTTP_version_1_1_methods 272 | 273 | def test_s_start 274 | begin 275 | h = Net::HTTP.start(config('host'), config('port')) 276 | ensure 277 | h&.finish 278 | end 279 | assert_equal config('host'), h.address 280 | assert_equal config('port'), h.port 281 | assert_equal true, h.instance_variable_get(:@proxy_from_env) 282 | 283 | begin 284 | h = Net::HTTP.start(config('host'), config('port'), :ENV) 285 | ensure 286 | h&.finish 287 | end 288 | assert_equal config('host'), h.address 289 | assert_equal config('port'), h.port 290 | assert_equal true, h.instance_variable_get(:@proxy_from_env) 291 | 292 | begin 293 | h = Net::HTTP.start(config('host'), config('port'), nil) 294 | ensure 295 | h&.finish 296 | end 297 | assert_equal config('host'), h.address 298 | assert_equal config('port'), h.port 299 | assert_equal false, h.instance_variable_get(:@proxy_from_env) 300 | end 301 | 302 | def test_s_get 303 | assert_equal $test_net_http_data, 304 | Net::HTTP.get(config('host'), '/', config('port')) 305 | 306 | assert_equal $test_net_http_data, Net::HTTP.get( 307 | URI.parse("http://#{config('host')}:#{config('port')}") 308 | ) 309 | assert_equal $test_net_http_data, Net::HTTP.get( 310 | URI.parse("http://#{config('host')}:#{config('port')}"), "Accept" => "text/plain" 311 | ) 312 | end 313 | 314 | def test_s_get_response 315 | res = Net::HTTP.get_response( 316 | URI.parse("http://#{config('host')}:#{config('port')}") 317 | ) 318 | assert_equal "application/octet-stream", res["Content-Type"] 319 | assert_equal $test_net_http_data, res.body 320 | 321 | res = Net::HTTP.get_response( 322 | URI.parse("http://#{config('host')}:#{config('port')}"), "Accept" => "text/plain" 323 | ) 324 | assert_equal "text/plain", res["Content-Type"] 325 | assert_equal $test_net_http_data, res.body 326 | end 327 | 328 | def test_head 329 | start {|http| 330 | res = http.head('/') 331 | assert_kind_of Net::HTTPResponse, res 332 | assert_equal $test_net_http_data_type, res['Content-Type'] 333 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 334 | assert_equal $test_net_http_data.size, res['Content-Length'].to_i 335 | end 336 | } 337 | end 338 | 339 | def test_get 340 | start {|http| 341 | _test_get__get http 342 | _test_get__iter http 343 | _test_get__chunked http 344 | } 345 | end 346 | 347 | def _test_get__get(http) 348 | res = http.get('/') 349 | assert_kind_of Net::HTTPResponse, res 350 | assert_kind_of String, res.body 351 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 352 | assert_not_nil res['content-length'] 353 | assert_equal $test_net_http_data.size, res['content-length'].to_i 354 | end 355 | assert_equal $test_net_http_data_type, res['Content-Type'] 356 | assert_equal $test_net_http_data.size, res.body.size 357 | assert_equal $test_net_http_data, res.body 358 | 359 | assert_nothing_raised { 360 | http.get('/', { 'User-Agent' => 'test' }.freeze) 361 | } 362 | 363 | assert res.decode_content, '[Bug #7924]' if Net::HTTP::HAVE_ZLIB 364 | end 365 | 366 | def _test_get__iter(http) 367 | buf = '' 368 | res = http.get('/') {|s| buf << s } 369 | assert_kind_of Net::HTTPResponse, res 370 | # assert_kind_of String, res.body 371 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 372 | assert_not_nil res['content-length'] 373 | assert_equal $test_net_http_data.size, res['content-length'].to_i 374 | end 375 | assert_equal $test_net_http_data_type, res['Content-Type'] 376 | assert_equal $test_net_http_data.size, buf.size 377 | assert_equal $test_net_http_data, buf 378 | # assert_equal $test_net_http_data.size, res.body.size 379 | # assert_equal $test_net_http_data, res.body 380 | end 381 | 382 | def _test_get__chunked(http) 383 | buf = '' 384 | res = http.get('/') {|s| buf << s } 385 | assert_kind_of Net::HTTPResponse, res 386 | # assert_kind_of String, res.body 387 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 388 | assert_not_nil res['content-length'] 389 | assert_equal $test_net_http_data.size, res['content-length'].to_i 390 | end 391 | assert_equal $test_net_http_data_type, res['Content-Type'] 392 | assert_equal $test_net_http_data.size, buf.size 393 | assert_equal $test_net_http_data, buf 394 | # assert_equal $test_net_http_data.size, res.body.size 395 | # assert_equal $test_net_http_data, res.body 396 | end 397 | 398 | def test_get__break 399 | i = 0 400 | start {|http| 401 | http.get('/') do |str| 402 | i += 1 403 | break 404 | end 405 | } 406 | assert_equal 1, i 407 | @log_tester = nil # server may encount ECONNRESET 408 | end 409 | 410 | def test_get__implicit_start 411 | res = new().get('/') 412 | assert_kind_of Net::HTTPResponse, res 413 | assert_kind_of String, res.body 414 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 415 | assert_not_nil res['content-length'] 416 | end 417 | assert_equal $test_net_http_data_type, res['Content-Type'] 418 | assert_equal $test_net_http_data.size, res.body.size 419 | assert_equal $test_net_http_data, res.body 420 | end 421 | 422 | def test_get__crlf 423 | start {|http| 424 | assert_raise(ArgumentError) do 425 | http.get("\r") 426 | end 427 | assert_raise(ArgumentError) do 428 | http.get("\n") 429 | end 430 | } 431 | end 432 | 433 | def test_get2 434 | start {|http| 435 | http.get2('/') {|res| 436 | EnvUtil.suppress_warning do 437 | assert_kind_of Net::HTTPResponse, res 438 | assert_kind_of Net::HTTPResponse, res.header 439 | end 440 | 441 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 442 | assert_not_nil res['content-length'] 443 | end 444 | assert_equal $test_net_http_data_type, res['Content-Type'] 445 | assert_kind_of String, res.body 446 | assert_kind_of String, res.entity 447 | assert_equal $test_net_http_data.size, res.body.size 448 | assert_equal $test_net_http_data, res.body 449 | assert_equal $test_net_http_data, res.entity 450 | } 451 | } 452 | end 453 | 454 | def test_post 455 | start {|http| 456 | _test_post__base http 457 | } 458 | start {|http| 459 | _test_post__file http 460 | } 461 | start {|http| 462 | _test_post__no_data http 463 | } 464 | end 465 | 466 | def _test_post__base(http) 467 | uheader = {} 468 | uheader['Accept'] = 'application/octet-stream' 469 | uheader['Content-Type'] = 'application/x-www-form-urlencoded' 470 | data = 'post data' 471 | res = http.post('/', data, uheader) 472 | assert_kind_of Net::HTTPResponse, res 473 | assert_kind_of String, res.body 474 | assert_equal data, res.body 475 | assert_equal data, res.entity 476 | end 477 | 478 | def _test_post__file(http) 479 | data = 'post data' 480 | f = StringIO.new 481 | http.post('/', data, {'content-type' => 'application/x-www-form-urlencoded'}, f) 482 | assert_equal data, f.string 483 | end 484 | 485 | def _test_post__no_data(http) 486 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 487 | EnvUtil.suppress_warning do 488 | data = nil 489 | res = http.post('/', data) 490 | assert_not_equal '411', res.code 491 | end 492 | end 493 | end 494 | 495 | def test_s_post 496 | url = "http://#{config('host')}:#{config('port')}/?q=a" 497 | res = Net::HTTP.post( 498 | URI.parse(url), 499 | "a=x") 500 | assert_equal "application/octet-stream", res["Content-Type"] 501 | assert_equal "a=x", res.body 502 | assert_equal url, res["X-request-uri"] 503 | 504 | res = Net::HTTP.post( 505 | URI.parse(url), 506 | "hello world", 507 | "Content-Type" => "text/plain; charset=US-ASCII") 508 | assert_equal "text/plain; charset=US-ASCII", res["Content-Type"] 509 | assert_equal "hello world", res.body 510 | end 511 | 512 | def test_s_post_form 513 | url = "http://#{config('host')}:#{config('port')}/" 514 | res = Net::HTTP.post_form( 515 | URI.parse(url), 516 | "a" => "x") 517 | assert_equal ["a=x"], res.body.split(/[;&]/).sort 518 | 519 | res = Net::HTTP.post_form( 520 | URI.parse(url), 521 | "a" => "x", 522 | "b" => "y") 523 | assert_equal ["a=x", "b=y"], res.body.split(/[;&]/).sort 524 | 525 | res = Net::HTTP.post_form( 526 | URI.parse(url), 527 | "a" => ["x1", "x2"], 528 | "b" => "y") 529 | assert_equal url, res['X-request-uri'] 530 | assert_equal ["a=x1", "a=x2", "b=y"], res.body.split(/[;&]/).sort 531 | 532 | res = Net::HTTP.post_form( 533 | URI.parse(url + '?a=x'), 534 | "b" => "y") 535 | assert_equal url + '?a=x', res['X-request-uri'] 536 | assert_equal ["b=y"], res.body.split(/[;&]/).sort 537 | end 538 | 539 | def test_patch 540 | start {|http| 541 | _test_patch__base http 542 | } 543 | end 544 | 545 | def _test_patch__base(http) 546 | uheader = {} 547 | uheader['Accept'] = 'application/octet-stream' 548 | uheader['Content-Type'] = 'application/x-www-form-urlencoded' 549 | data = 'patch data' 550 | res = http.patch('/', data, uheader) 551 | assert_kind_of Net::HTTPResponse, res 552 | assert_kind_of String, res.body 553 | assert_equal data, res.body 554 | assert_equal data, res.entity 555 | end 556 | 557 | def test_timeout_during_HTTP_session_write 558 | th = nil 559 | # listen for connections... but deliberately do not read 560 | TCPServer.open('localhost', 0) {|server| 561 | port = server.addr[1] 562 | 563 | conn = Net::HTTP.new('localhost', port) 564 | conn.write_timeout = EnvUtil.apply_timeout_scale(0.01) 565 | conn.read_timeout = EnvUtil.apply_timeout_scale(0.01) if windows? 566 | conn.open_timeout = EnvUtil.apply_timeout_scale(1) 567 | 568 | th = Thread.new do 569 | err = !windows? ? Net::WriteTimeout : Net::ReadTimeout 570 | assert_raise(err) do 571 | conn.post('/', "a"*50_000_000) 572 | end 573 | end 574 | assert th.join(EnvUtil.apply_timeout_scale(10)) 575 | } 576 | ensure 577 | th&.kill 578 | th&.join 579 | end 580 | 581 | def test_timeout_during_non_chunked_streamed_HTTP_session_write 582 | th = nil 583 | # listen for connections... but deliberately do not read 584 | TCPServer.open('localhost', 0) {|server| 585 | port = server.addr[1] 586 | 587 | conn = Net::HTTP.new('localhost', port) 588 | conn.write_timeout = EnvUtil.apply_timeout_scale(0.01) 589 | conn.read_timeout = EnvUtil.apply_timeout_scale(0.01) if windows? 590 | conn.open_timeout = EnvUtil.apply_timeout_scale(1) 591 | 592 | req = Net::HTTP::Post.new('/') 593 | data = "a"*50_000_000 594 | req.content_length = data.size 595 | req['Content-Type'] = 'application/x-www-form-urlencoded' 596 | req.body_stream = StringIO.new(data) 597 | 598 | th = Thread.new do 599 | assert_raise(Net::WriteTimeout) { conn.request(req) } 600 | end 601 | assert th.join(10) 602 | } 603 | ensure 604 | th&.kill 605 | th&.join 606 | end 607 | 608 | def test_timeout_during_HTTP_session 609 | bug4246 = "expected the HTTP session to have timed out but have not. c.f. [ruby-core:34203]" 610 | 611 | th = nil 612 | # listen for connections... but deliberately do not read 613 | TCPServer.open('localhost', 0) {|server| 614 | port = server.addr[1] 615 | 616 | conn = Net::HTTP.new('localhost', port) 617 | conn.read_timeout = EnvUtil.apply_timeout_scale(0.01) 618 | conn.open_timeout = EnvUtil.apply_timeout_scale(1) 619 | 620 | th = Thread.new do 621 | assert_raise(Net::ReadTimeout) { 622 | conn.get('/') 623 | } 624 | end 625 | assert th.join(EnvUtil.apply_timeout_scale(10)), bug4246 626 | } 627 | ensure 628 | th.kill 629 | th.join 630 | end 631 | end 632 | 633 | 634 | module TestNetHTTP_version_1_2_methods 635 | 636 | def test_request 637 | start {|http| 638 | _test_request__GET http 639 | _test_request__accept_encoding http 640 | _test_request__file http 641 | # _test_request__range http # WEBrick does not support Range: header. 642 | _test_request__HEAD http 643 | _test_request__POST http 644 | _test_request__uri http 645 | _test_request__uri_host http 646 | } 647 | start {|http| 648 | _test_request__stream_body http 649 | } 650 | end 651 | 652 | def _test_request__GET(http) 653 | req = Net::HTTP::Get.new('/') 654 | http.request(req) {|res| 655 | assert_kind_of Net::HTTPResponse, res 656 | assert_kind_of String, res.body 657 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 658 | assert_not_nil res['content-length'] 659 | assert_equal $test_net_http_data.size, res['content-length'].to_i 660 | end 661 | assert_equal $test_net_http_data.size, res.body.size 662 | assert_equal $test_net_http_data, res.body 663 | 664 | assert res.decode_content, 'Bug #7831' if Net::HTTP::HAVE_ZLIB 665 | } 666 | end 667 | 668 | def _test_request__accept_encoding(http) 669 | req = Net::HTTP::Get.new('/', 'accept-encoding' => 'deflate') 670 | http.request(req) {|res| 671 | assert_kind_of Net::HTTPResponse, res 672 | assert_kind_of String, res.body 673 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 674 | assert_not_nil res['content-length'] 675 | assert_equal $test_net_http_data.size, res['content-length'].to_i 676 | end 677 | assert_equal $test_net_http_data.size, res.body.size 678 | assert_equal $test_net_http_data, res.body 679 | 680 | assert_not_predicate res, :decode_content, 'Bug #7831' if Net::HTTP::HAVE_ZLIB 681 | } 682 | end 683 | 684 | def _test_request__file(http) 685 | req = Net::HTTP::Get.new('/') 686 | http.request(req) {|res| 687 | assert_kind_of Net::HTTPResponse, res 688 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 689 | assert_not_nil res['content-length'] 690 | assert_equal $test_net_http_data.size, res['content-length'].to_i 691 | end 692 | f = StringIO.new("".force_encoding("ASCII-8BIT")) 693 | res.read_body f 694 | assert_equal $test_net_http_data.bytesize, f.string.bytesize 695 | assert_equal $test_net_http_data.encoding, f.string.encoding 696 | assert_equal $test_net_http_data, f.string 697 | } 698 | end 699 | 700 | def _test_request__range(http) 701 | req = Net::HTTP::Get.new('/') 702 | req['range'] = 'bytes=0-5' 703 | assert_equal $test_net_http_data[0,6], http.request(req).body 704 | end 705 | 706 | def _test_request__HEAD(http) 707 | req = Net::HTTP::Head.new('/') 708 | http.request(req) {|res| 709 | assert_kind_of Net::HTTPResponse, res 710 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 711 | assert_not_nil res['content-length'] 712 | assert_equal $test_net_http_data.size, res['content-length'].to_i 713 | end 714 | assert_nil res.body 715 | } 716 | end 717 | 718 | def _test_request__POST(http) 719 | data = 'post data' 720 | req = Net::HTTP::Post.new('/') 721 | req['Accept'] = $test_net_http_data_type 722 | req['Content-Type'] = 'application/x-www-form-urlencoded' 723 | http.request(req, data) {|res| 724 | assert_kind_of Net::HTTPResponse, res 725 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 726 | assert_equal data.size, res['content-length'].to_i 727 | end 728 | assert_kind_of String, res.body 729 | assert_equal data, res.body 730 | } 731 | end 732 | 733 | def _test_request__stream_body(http) 734 | req = Net::HTTP::Post.new('/') 735 | data = $test_net_http_data 736 | req.content_length = data.size 737 | req['Content-Type'] = 'application/x-www-form-urlencoded' 738 | req.body_stream = StringIO.new(data) 739 | res = http.request(req) 740 | assert_kind_of Net::HTTPResponse, res 741 | assert_kind_of String, res.body 742 | assert_equal data.size, res.body.size 743 | assert_equal data, res.body 744 | end 745 | 746 | def _test_request__path(http) 747 | uri = URI 'https://hostname.example/' 748 | req = Net::HTTP::Get.new('/') 749 | 750 | res = http.request(req) 751 | 752 | assert_kind_of URI::Generic, req.uri 753 | 754 | assert_not_equal uri, req.uri 755 | 756 | assert_equal uri, res.uri 757 | 758 | assert_not_same uri, req.uri 759 | assert_not_same req.uri, res.uri 760 | end 761 | 762 | def _test_request__uri(http) 763 | uri = URI 'https://hostname.example/' 764 | req = Net::HTTP::Get.new(uri) 765 | 766 | res = http.request(req) 767 | 768 | assert_kind_of URI::Generic, req.uri 769 | 770 | assert_not_equal uri, req.uri 771 | 772 | assert_equal req.uri, res.uri 773 | 774 | assert_not_same uri, req.uri 775 | assert_not_same req.uri, res.uri 776 | end 777 | 778 | def _test_request__uri_host(http) 779 | uri = URI 'http://other.example/' 780 | 781 | req = Net::HTTP::Get.new(uri) 782 | req['host'] = 'hostname.example' 783 | 784 | res = http.request(req) 785 | 786 | assert_kind_of URI::Generic, req.uri 787 | 788 | assert_equal URI("http://hostname.example:#{http.port}"), res.uri 789 | end 790 | 791 | def test_send_request 792 | start {|http| 793 | _test_send_request__GET http 794 | _test_send_request__HEAD http 795 | _test_send_request__POST http 796 | } 797 | end 798 | 799 | def _test_send_request__GET(http) 800 | res = http.send_request('GET', '/') 801 | assert_kind_of Net::HTTPResponse, res 802 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 803 | assert_equal $test_net_http_data.size, res['content-length'].to_i 804 | end 805 | assert_kind_of String, res.body 806 | assert_equal $test_net_http_data, res.body 807 | end 808 | 809 | def _test_send_request__HEAD(http) 810 | res = http.send_request('HEAD', '/') 811 | assert_kind_of Net::HTTPResponse, res 812 | unless self.is_a?(TestNetHTTP_v1_2_chunked) 813 | assert_not_nil res['content-length'] 814 | assert_equal $test_net_http_data.size, res['content-length'].to_i 815 | end 816 | assert_nil res.body 817 | end 818 | 819 | def _test_send_request__POST(http) 820 | data = 'aaabbb cc ddddddddddd lkjoiu4j3qlkuoa' 821 | res = http.send_request('POST', '/', data, 'content-type' => 'application/x-www-form-urlencoded') 822 | assert_kind_of Net::HTTPResponse, res 823 | assert_kind_of String, res.body 824 | assert_equal data.size, res.body.size 825 | assert_equal data, res.body 826 | end 827 | 828 | def test_set_form 829 | require 'tempfile' 830 | Tempfile.create('ruby-test') {|file| 831 | file << "\u{30c7}\u{30fc}\u{30bf}" 832 | data = [ 833 | ['name', 'Gonbei Nanashi'], 834 | ['name', "\u{540d}\u{7121}\u{3057}\u{306e}\u{6a29}\u{5175}\u{885b}"], 835 | ['s"i\o', StringIO.new("\u{3042 3044 4e9c 925b}")], 836 | ["file", file, filename: "ruby-test"] 837 | ] 838 | expected = <<"__EOM__".gsub(/\n/, "\r\n") 839 | -- 840 | Content-Disposition: form-data; name="name" 841 | 842 | Gonbei Nanashi 843 | -- 844 | Content-Disposition: form-data; name="name" 845 | 846 | \xE5\x90\x8D\xE7\x84\xA1\xE3\x81\x97\xE3\x81\xAE\xE6\xA8\xA9\xE5\x85\xB5\xE8\xA1\x9B 847 | -- 848 | Content-Disposition: form-data; name="s\\"i\\\\o" 849 | 850 | \xE3\x81\x82\xE3\x81\x84\xE4\xBA\x9C\xE9\x89\x9B 851 | -- 852 | Content-Disposition: form-data; name="file"; filename="ruby-test" 853 | Content-Type: application/octet-stream 854 | 855 | \xE3\x83\x87\xE3\x83\xBC\xE3\x82\xBF 856 | ---- 857 | __EOM__ 858 | start {|http| 859 | _test_set_form_urlencoded(http, data.reject{|k,v|!v.is_a?(String)}) 860 | } 861 | start {|http| 862 | @server.mount('/', lambda {|req, res| res.body = req.body }) 863 | _test_set_form_multipart(http, false, data, expected) 864 | } 865 | start {|http| 866 | @server.mount('/', lambda {|req, res| res.body = req.body }) 867 | _test_set_form_multipart(http, true, data, expected) 868 | } 869 | } 870 | end 871 | 872 | def _test_set_form_urlencoded(http, data) 873 | req = Net::HTTP::Post.new('/') 874 | req.set_form(data) 875 | res = http.request req 876 | 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 877 | end 878 | 879 | def _test_set_form_multipart(http, chunked_p, data, expected) 880 | data.each{|k,v|v.rewind rescue nil} 881 | req = Net::HTTP::Post.new('/') 882 | req.set_form(data, 'multipart/form-data') 883 | req['Transfer-Encoding'] = 'chunked' if chunked_p 884 | res = http.request req 885 | body = res.body 886 | assert_match(/\A--(?\S+)/, body) 887 | /\A--(?\S+)/ =~ body 888 | expected = expected.gsub(//, boundary) 889 | assert_equal(expected, body) 890 | end 891 | 892 | def test_set_form_with_file 893 | require 'tempfile' 894 | Tempfile.create('ruby-test') {|file| 895 | file.binmode 896 | file << $test_net_http_data 897 | filename = File.basename(file.to_path) 898 | data = [['file', file]] 899 | expected = <<"__EOM__".gsub(/\n/, "\r\n") 900 | -- 901 | Content-Disposition: form-data; name="file"; filename="" 902 | Content-Type: application/octet-stream 903 | 904 | 905 | ---- 906 | __EOM__ 907 | expected.sub!(//, filename) 908 | expected.sub!(//, $test_net_http_data) 909 | start {|http| 910 | @server.mount('/', lambda {|req, res| res.body = req.body }) 911 | data.each{|k,v|v.rewind rescue nil} 912 | req = Net::HTTP::Post.new('/') 913 | req.set_form(data, 'multipart/form-data') 914 | res = http.request req 915 | body = res.body 916 | header, _ = body.split(/\r\n\r\n/, 2) 917 | assert_match(/\A--(?\S+)/, body) 918 | /\A--(?\S+)/ =~ body 919 | expected = expected.gsub(//, boundary) 920 | assert_match(/^--(?\S+)\r\n/, header) 921 | assert_match( 922 | /^Content-Disposition: form-data; name="file"; filename="#{filename}"\r\n/, 923 | header) 924 | assert_equal(expected, body) 925 | 926 | # TODO: test with chunked 927 | # data.each{|k,v|v.rewind rescue nil} 928 | # req['Transfer-Encoding'] = 'chunked' 929 | # res = http.request req 930 | # assert_equal(expected, res.body) 931 | } 932 | } 933 | end 934 | end 935 | 936 | class TestNetHTTP_v1_2 < Test::Unit::TestCase 937 | CONFIG = { 938 | 'host' => '127.0.0.1', 939 | 'proxy_host' => nil, 940 | 'proxy_port' => nil, 941 | } 942 | 943 | include TestNetHTTPUtils 944 | include TestNetHTTP_version_1_1_methods 945 | include TestNetHTTP_version_1_2_methods 946 | 947 | def new 948 | Net::HTTP.version_1_2 949 | super 950 | end 951 | 952 | def test_send_large_POST_request 953 | start {|http| 954 | data = ' '*6000000 955 | res = http.send_request('POST', '/', data, 'content-type' => 'application/x-www-form-urlencoded') 956 | assert_kind_of Net::HTTPResponse, res 957 | assert_kind_of String, res.body 958 | assert_equal data.size, res.body.size 959 | assert_equal data, res.body 960 | } 961 | end 962 | end 963 | 964 | class TestNetHTTP_v1_2_chunked < Test::Unit::TestCase 965 | CONFIG = { 966 | 'host' => '127.0.0.1', 967 | 'proxy_host' => nil, 968 | 'proxy_port' => nil, 969 | 'chunked' => true, 970 | } 971 | 972 | include TestNetHTTPUtils 973 | include TestNetHTTP_version_1_1_methods 974 | include TestNetHTTP_version_1_2_methods 975 | 976 | def new 977 | Net::HTTP.version_1_2 978 | super 979 | end 980 | 981 | def test_chunked_break 982 | assert_nothing_raised("[ruby-core:29229]") { 983 | start {|http| 984 | http.request_get('/') {|res| 985 | res.read_body {|chunk| 986 | break 987 | } 988 | } 989 | } 990 | } 991 | end 992 | end 993 | 994 | class TestNetHTTPContinue < Test::Unit::TestCase 995 | CONFIG = { 996 | 'host' => '127.0.0.1', 997 | 'proxy_host' => nil, 998 | 'proxy_port' => nil, 999 | 'chunked' => true, 1000 | } 1001 | 1002 | include TestNetHTTPUtils 1003 | 1004 | def logfile 1005 | @debug = StringIO.new('') 1006 | end 1007 | 1008 | def mount_proc(&block) 1009 | @server.mount('/continue', block.to_proc) 1010 | end 1011 | 1012 | def test_expect_continue 1013 | mount_proc {|req, res| 1014 | req.continue 1015 | res.body = req.query['body'] 1016 | } 1017 | start {|http| 1018 | uheader = {'content-type' => 'application/x-www-form-urlencoded', 'expect' => '100-continue'} 1019 | http.continue_timeout = 0.2 1020 | http.request_post('/continue', 'body=BODY', uheader) {|res| 1021 | assert_equal('BODY', res.read_body) 1022 | } 1023 | } 1024 | assert_match(/Expect: 100-continue/, @debug.string) 1025 | assert_match(/HTTP\/1.1 100 continue/, @debug.string) 1026 | end 1027 | 1028 | def test_expect_continue_timeout 1029 | mount_proc {|req, res| 1030 | sleep 0.2 1031 | req.continue # just ignored because it's '100' 1032 | res.body = req.query['body'] 1033 | } 1034 | start {|http| 1035 | uheader = {'content-type' => 'application/x-www-form-urlencoded', 'expect' => '100-continue'} 1036 | http.continue_timeout = 0 1037 | http.request_post('/continue', 'body=BODY', uheader) {|res| 1038 | assert_equal('BODY', res.read_body) 1039 | } 1040 | } 1041 | assert_match(/Expect: 100-continue/, @debug.string) 1042 | assert_match(/HTTP\/1.1 100 continue/, @debug.string) 1043 | end 1044 | 1045 | def test_expect_continue_error 1046 | mount_proc {|req, res| 1047 | res.status = 501 1048 | res.body = req.query['body'] 1049 | } 1050 | start {|http| 1051 | uheader = {'content-type' => 'application/x-www-form-urlencoded', 'expect' => '100-continue'} 1052 | http.continue_timeout = 0 1053 | http.request_post('/continue', 'body=ERROR', uheader) {|res| 1054 | assert_equal('ERROR', res.read_body) 1055 | } 1056 | } 1057 | assert_match(/Expect: 100-continue/, @debug.string) 1058 | assert_not_match(/HTTP\/1.1 100 continue/, @debug.string) 1059 | end 1060 | 1061 | def test_expect_continue_error_before_body 1062 | @log_tester = nil 1063 | mount_proc {|req, res| 1064 | raise TestNetHTTPUtils::Forbidden 1065 | } 1066 | start {|http| 1067 | uheader = {'content-type' => 'application/x-www-form-urlencoded', 'content-length' => '5', 'expect' => '100-continue'} 1068 | http.continue_timeout = 1 # allow the server to respond before sending 1069 | http.request_post('/continue', 'data', uheader) {|res| 1070 | assert_equal(res.code, '403') 1071 | } 1072 | } 1073 | assert_match(/Expect: 100-continue/, @debug.string) 1074 | assert_not_match(/HTTP\/1.1 100 continue/, @debug.string) 1075 | end 1076 | 1077 | def test_expect_continue_error_while_waiting 1078 | mount_proc {|req, res| 1079 | res.status = 501 1080 | res.body = req.query['body'] 1081 | } 1082 | start {|http| 1083 | uheader = {'content-type' => 'application/x-www-form-urlencoded', 'expect' => '100-continue'} 1084 | http.continue_timeout = 0.5 1085 | http.request_post('/continue', 'body=ERROR', uheader) {|res| 1086 | assert_equal('ERROR', res.read_body) 1087 | } 1088 | } 1089 | assert_match(/Expect: 100-continue/, @debug.string) 1090 | assert_not_match(/HTTP\/1.1 100 continue/, @debug.string) 1091 | end 1092 | end 1093 | 1094 | class TestNetHTTPSwitchingProtocols < Test::Unit::TestCase 1095 | CONFIG = { 1096 | 'host' => '127.0.0.1', 1097 | 'proxy_host' => nil, 1098 | 'proxy_port' => nil, 1099 | 'chunked' => true, 1100 | } 1101 | 1102 | include TestNetHTTPUtils 1103 | 1104 | def logfile 1105 | @debug = StringIO.new('') 1106 | end 1107 | 1108 | def mount_proc(&block) 1109 | @server.mount('/continue', block.to_proc) 1110 | end 1111 | 1112 | def test_info 1113 | mount_proc {|req, res| 1114 | req.instance_variable_get(:@socket) << "HTTP/1.1 101 Switching Protocols\r\n\r\n" 1115 | res.body = req.query['body'] 1116 | } 1117 | start {|http| 1118 | http.continue_timeout = 0.2 1119 | http.request_post('/continue', 'body=BODY', 1120 | 'content-type' => 'application/x-www-form-urlencoded') {|res| 1121 | assert_equal('BODY', res.read_body) 1122 | } 1123 | } 1124 | assert_match(/HTTP\/1.1 101 Switching Protocols/, @debug.string) 1125 | end 1126 | end 1127 | 1128 | class TestNetHTTPKeepAlive < Test::Unit::TestCase 1129 | CONFIG = { 1130 | 'host' => '127.0.0.1', 1131 | 'proxy_host' => nil, 1132 | 'proxy_port' => nil, 1133 | 'RequestTimeout' => 1, 1134 | } 1135 | 1136 | include TestNetHTTPUtils 1137 | 1138 | def test_keep_alive_get_auto_reconnect 1139 | start {|http| 1140 | res = http.get('/') 1141 | http.keep_alive_timeout = 1 1142 | assert_kind_of Net::HTTPResponse, res 1143 | assert_kind_of String, res.body 1144 | sleep 1.5 1145 | assert_nothing_raised { 1146 | res = http.get('/') 1147 | } 1148 | assert_kind_of Net::HTTPResponse, res 1149 | assert_kind_of String, res.body 1150 | } 1151 | end 1152 | 1153 | def test_server_closed_connection_auto_reconnect 1154 | start {|http| 1155 | res = http.get('/') 1156 | http.keep_alive_timeout = 5 1157 | assert_kind_of Net::HTTPResponse, res 1158 | assert_kind_of String, res.body 1159 | sleep 1.5 1160 | assert_nothing_raised { 1161 | # Net::HTTP should detect the closed connection before attempting the 1162 | # request, since post requests cannot be retried. 1163 | res = http.post('/', 'query=foo', 'content-type' => 'application/x-www-form-urlencoded') 1164 | } 1165 | assert_kind_of Net::HTTPResponse, res 1166 | assert_kind_of String, res.body 1167 | } 1168 | end 1169 | 1170 | def test_keep_alive_get_auto_retry 1171 | start {|http| 1172 | res = http.get('/') 1173 | http.keep_alive_timeout = 5 1174 | assert_kind_of Net::HTTPResponse, res 1175 | assert_kind_of String, res.body 1176 | sleep 1.5 1177 | res = http.get('/') 1178 | assert_kind_of Net::HTTPResponse, res 1179 | assert_kind_of String, res.body 1180 | } 1181 | end 1182 | 1183 | def test_keep_alive_reset_on_new_connection 1184 | # Using debug log output on accepting connection: 1185 | # 1186 | # "[2021-04-29 20:36:46] DEBUG accept: 127.0.0.1:50674\n" 1187 | @log_tester = nil 1188 | @logger_level = :debug 1189 | 1190 | start {|http| 1191 | res = http.get('/') 1192 | http.keep_alive_timeout = 1 1193 | assert_kind_of Net::HTTPResponse, res 1194 | assert_kind_of String, res.body 1195 | http.finish 1196 | assert_equal 1, @log.grep(/accept/i).size 1197 | 1198 | sleep 1.5 1199 | http.start 1200 | res = http.get('/') 1201 | assert_kind_of Net::HTTPResponse, res 1202 | assert_kind_of String, res.body 1203 | assert_equal 2, @log.grep(/accept/i).size 1204 | } 1205 | end 1206 | 1207 | class MockSocket 1208 | attr_reader :count 1209 | def initialize(success_after: nil) 1210 | @success_after = success_after 1211 | @count = 0 1212 | end 1213 | def close 1214 | end 1215 | def closed? 1216 | end 1217 | def write(_) 1218 | end 1219 | def readline 1220 | @count += 1 1221 | if @success_after && @success_after <= @count 1222 | "HTTP/1.1 200 OK" 1223 | else 1224 | raise Errno::ECONNRESET 1225 | end 1226 | end 1227 | def readuntil(*_) 1228 | "" 1229 | end 1230 | def read_all(_) 1231 | end 1232 | end 1233 | 1234 | def test_http_retry_success 1235 | start {|http| 1236 | socket = MockSocket.new(success_after: 10) 1237 | http.instance_variable_get(:@socket).close 1238 | http.instance_variable_set(:@socket, socket) 1239 | assert_equal 0, socket.count 1240 | http.max_retries = 10 1241 | res = http.get('/') 1242 | assert_equal 10, socket.count 1243 | assert_kind_of Net::HTTPResponse, res 1244 | assert_kind_of String, res.body 1245 | } 1246 | end 1247 | 1248 | def test_http_retry_failed 1249 | start {|http| 1250 | socket = MockSocket.new 1251 | http.instance_variable_get(:@socket).close 1252 | http.instance_variable_set(:@socket, socket) 1253 | http.max_retries = 10 1254 | assert_raise(Errno::ECONNRESET){ http.get('/') } 1255 | assert_equal 11, socket.count 1256 | } 1257 | end 1258 | 1259 | def test_http_retry_failed_with_block 1260 | start {|http| 1261 | http.max_retries = 10 1262 | called = 0 1263 | assert_raise(Errno::ECONNRESET){ http.get('/'){called += 1; raise Errno::ECONNRESET} } 1264 | assert_equal 1, called 1265 | } 1266 | @log_tester = nil 1267 | end 1268 | 1269 | def test_keep_alive_server_close 1270 | def @server.run(sock) 1271 | sock.close 1272 | end 1273 | 1274 | start {|http| 1275 | assert_raise(EOFError, Errno::ECONNRESET, IOError) { 1276 | http.get('/') 1277 | } 1278 | } 1279 | end 1280 | end 1281 | 1282 | class TestNetHTTPLocalBind < Test::Unit::TestCase 1283 | CONFIG = { 1284 | 'host' => 'localhost', 1285 | 'proxy_host' => nil, 1286 | 'proxy_port' => nil, 1287 | } 1288 | 1289 | include TestNetHTTPUtils 1290 | 1291 | def test_bind_to_local_host 1292 | @server.mount_proc('/show_ip') { |req, res| res.body = req.remote_ip } 1293 | 1294 | http = Net::HTTP.new(config('host'), config('port')) 1295 | http.local_host = Addrinfo.tcp(config('host'), config('port')).ip_address 1296 | assert_not_nil(http.local_host) 1297 | assert_nil(http.local_port) 1298 | 1299 | res = http.get('/show_ip') 1300 | assert_equal(http.local_host, res.body) 1301 | end 1302 | 1303 | def test_bind_to_local_port 1304 | @server.mount_proc('/show_port') { |req, res| res.body = req.peeraddr[1].to_s } 1305 | 1306 | http = Net::HTTP.new(config('host'), config('port')) 1307 | http.local_host = Addrinfo.tcp(config('host'), config('port')).ip_address 1308 | http.local_port = Addrinfo.tcp(config('host'), 0).bind {|s| 1309 | s.local_address.ip_port.to_s 1310 | } 1311 | assert_not_nil(http.local_host) 1312 | assert_not_nil(http.local_port) 1313 | 1314 | res = http.get('/show_port') 1315 | assert_equal(http.local_port, res.body) 1316 | end 1317 | end 1318 | 1319 | class TestNetHTTPForceEncoding < Test::Unit::TestCase 1320 | CONFIG = { 1321 | 'host' => 'localhost', 1322 | 'proxy_host' => nil, 1323 | 'proxy_port' => nil, 1324 | } 1325 | 1326 | include TestNetHTTPUtils 1327 | 1328 | def fe_request(force_enc, content_type=nil) 1329 | @server.mount_proc('/fe') do |req, res| 1330 | res['Content-Type'] = content_type if content_type 1331 | res.body = "hello\u1234" 1332 | end 1333 | 1334 | http = Net::HTTP.new(config('host'), config('port')) 1335 | http.local_host = Addrinfo.tcp(config('host'), config('port')).ip_address 1336 | assert_not_nil(http.local_host) 1337 | assert_nil(http.local_port) 1338 | 1339 | http.response_body_encoding = force_enc 1340 | http.get('/fe') 1341 | end 1342 | 1343 | def test_response_body_encoding_false 1344 | res = fe_request(false) 1345 | assert_equal("hello\u1234".b, res.body) 1346 | assert_equal(Encoding::ASCII_8BIT, res.body.encoding) 1347 | end 1348 | 1349 | def test_response_body_encoding_true_without_content_type 1350 | res = fe_request(true) 1351 | assert_equal("hello\u1234".b, res.body) 1352 | assert_equal(Encoding::ASCII_8BIT, res.body.encoding) 1353 | end 1354 | 1355 | def test_response_body_encoding_true_with_content_type 1356 | res = fe_request(true, 'text/html; charset=utf-8') 1357 | assert_equal("hello\u1234", res.body) 1358 | assert_equal(Encoding::UTF_8, res.body.encoding) 1359 | end 1360 | 1361 | def test_response_body_encoding_string_without_content_type 1362 | res = fe_request('utf-8') 1363 | assert_equal("hello\u1234", res.body) 1364 | assert_equal(Encoding::UTF_8, res.body.encoding) 1365 | end 1366 | 1367 | def test_response_body_encoding_encoding_without_content_type 1368 | res = fe_request(Encoding::UTF_8) 1369 | assert_equal("hello\u1234", res.body) 1370 | assert_equal(Encoding::UTF_8, res.body.encoding) 1371 | end 1372 | end 1373 | 1374 | class TestNetHTTPPartialResponse < Test::Unit::TestCase 1375 | CONFIG = { 1376 | 'host' => '127.0.0.1', 1377 | 'proxy_host' => nil, 1378 | 'proxy_port' => nil, 1379 | } 1380 | 1381 | include TestNetHTTPUtils 1382 | 1383 | def test_partial_response 1384 | str = "0123456789" 1385 | @server.mount_proc('/') do |req, res| 1386 | res.status = 200 1387 | res['Content-Type'] = 'text/plain' 1388 | 1389 | res.body = str 1390 | res['Content-Length'] = str.length + 1 1391 | end 1392 | @server.mount_proc('/show_ip') { |req, res| res.body = req.remote_ip } 1393 | 1394 | http = Net::HTTP.new(config('host'), config('port')) 1395 | res = http.get('/') 1396 | assert_equal(str, res.body) 1397 | 1398 | http = Net::HTTP.new(config('host'), config('port')) 1399 | http.ignore_eof = false 1400 | assert_raise(EOFError) {http.get('/')} 1401 | end 1402 | end 1403 | --------------------------------------------------------------------------------