...
 
Commits (3)
AllCops:
DisplayCopNames: true
## Layout ######################################################################
Layout/DotPosition:
EnforcedStyle: trailing
Layout/SpaceAroundOperators:
AllowForAlignment: true
Layout/SpaceInsideHashLiteralBraces:
EnforcedStyle: no_space
## Metrics #####################################################################
Metrics/AbcSize:
Enabled: false
Metrics/BlockLength:
Exclude:
- spec/**/*
Metrics/BlockNesting:
Max: 2
......@@ -8,19 +28,22 @@ Metrics/ClassLength:
CountComments: false
Max: 125
Metrics/PerceivedComplexity:
# TODO: Lower to 6
Metrics/CyclomaticComplexity:
Max: 8
Metrics/CyclomaticComplexity:
Max: 8 # TODO: Lower to 6
Metrics/PerceivedComplexity:
Max: 8
# TODO: Lower to 80
Metrics/LineLength:
AllowURI: true
Max: 143 # TODO: Lower to 80
Max: 143
# TODO: Lower to 15
Metrics/MethodLength:
CountComments: false
Max: 25 # TODO: Lower to 15
Max: 25
Metrics/ModuleLength:
CountComments: false
......@@ -30,9 +53,14 @@ Metrics/ParameterLists:
Max: 5
CountKeywordArgs: true
Metrics/AbcSize:
## Performance #################################################################
# XXX: requires ruby 2.4+
Performance/RegexpMatch:
Enabled: false
## Style #######################################################################
Style/CollectionMethods:
PreferredMethods:
collect: 'map'
......@@ -43,9 +71,6 @@ Style/CollectionMethods:
Style/Documentation:
Enabled: false
Style/DotPosition:
EnforcedStyle: trailing
Style/DoubleNegation:
Enabled: false
......@@ -58,20 +83,28 @@ Style/Encoding:
Style/EmptyCaseCondition:
Enabled: false
# XXX: Lots of times it suggests making code terrible to read.
Style/GuardClause:
Enabled: false
Style/HashSyntax:
EnforcedStyle: hash_rockets
Style/Lambda:
Enabled: false
Style/SpaceAroundOperators:
AllowForAlignment: true
Style/OptionHash:
Enabled: true
Style/SpaceInsideHashLiteralBraces:
EnforcedStyle: no_space
# XXX: requires ruby 2.3+
Style/SafeNavigation:
Enabled: false
Style/StringLiterals:
EnforcedStyle: double_quotes
Style/TrivialAccessors:
Enabled: false
Style/YodaCondition:
Enabled: false
language: ruby
sudo: false
cache: bundler
before_install:
- gem update --system 2.6.10
- gem update --system
- gem --version
- gem install bundler --version 1.14.3 --no-rdoc --no-ri
- gem install bundler --no-rdoc --no-ri
- bundle --version
install: bundle _1.14.3_ install --without development doc
install: bundle install --without development doc
script: bundle _1.14.3_ exec rake
script: bundle exec rake
env:
global:
- JRUBY_OPTS="$JRUBY_OPTS --debug"
env: JRUBY_OPTS="$JRUBY_OPTS --debug"
rvm:
- jruby-9.1.7.0
- 2.0.0
- 2.1
# Include JRuby first because it takes the longest
- jruby-9.1.13.0
- 2.2
- 2.3.3
- 2.4.0
- 2.3.4
- 2.4.1
matrix:
fast_finish: true
include:
# Only run RuboCop and Yardstick metrics on the latest Ruby
- rvm: 2.4.1
env: SUITE="rubocop"
- rvm: 2.4.1
env: SUITE="yardstick"
branches:
only:
......
## 3.3.0 (2018-04-25)
This version backports some of the fixes and improvements made to development
version of the HTTP gem:
* [#458](https://github.com/httprb/http/pull/458)
Extract HTTP::Client#build_request method.
([@tycoon])
## 3.2.1 (2018-04-24)
* [#468](https://github.com/httprb/http/pull/468)
Rewind `HTTP::Request::Body#source` once `#each` is complete.
([@ixti])
## 3.2.0 (2018-04-22)
This version backports one change we missed to backport in previous release:
* Reduce memory usage when reading response body
([@janko-m])
## 3.1.0 (2018-04-22)
This version backports some of the fixes and improvements made to development
version of the HTTP gem:
* Fix for `#readpartial` to respect max length argument.
([@janko-m], [@marshall-lee])
* Fix for `HTTP::Request#headline` to allow two leading slashes in path.
([@scarfacedeb])
* Fix query string building for string with newlines.
([@mikegee])
* Deallocate temporary strings in `Response::Body#to_s`.
([@janko-m])
* Add `Request::Body#source`.
([@janko-m])
## 3.0.0 (2017-10-01)
* Drop support of Ruby `2.0` and Ruby `2.1`.
([@ixti])
* [#410](https://github.com/httprb/http/pull/410)
Infer `Host` header upon redirects.
([@janko-m])
* [#409](https://github.com/httprb/http/pull/409)
Enables request body streaming on any IO object.
([@janko-m])
* [#413](https://github.com/httprb/http/issues/413),
[#414](https://github.com/httprb/http/pull/414)
Fix encoding of body chunks.
([@janko-m])
* [#368](https://github.com/httprb/http/pull/368),
[#357](https://github.com/httprb/http/issues/357)
Fix timeout issue.
([@HoneyryderChuck])
## 2.2.2 (2017-04-27)
* [#404](https://github.com/httprb/http/issues/404),
......@@ -593,4 +663,8 @@ end
[@britishtea]: https://github.com/britishtea
[@janko-m]: https://github.com/janko-m
[@Bonias]: https://github.com/Bonias
[@nestegg]: https://github.com/nestegg
[@HoneyryderChuck]: https://github.com/HoneyryderChuck
[@marshall-lee]: https://github.com/marshall-lee
[@scarfacedeb]: https://github.com/scarfacedeb
[@mikegee]: https://github.com/mikegee
[@tycoon]: https://github.com/tycooon
# frozen_string_literal: true
source "https://rubygems.org"
ruby RUBY_VERSION
......@@ -15,15 +17,20 @@ group :development do
end
group :test do
gem "activemodel", :require => false # Used by certificate_authority
gem "certificate_authority", :require => false
gem "backports"
gem "coveralls", :require => false
gem "simplecov", ">= 0.9"
gem "rubocop", "= 0.40.0"
gem "rspec", "~> 3.0"
gem "rspec", "~> 3.0"
gem "rspec-its"
gem "rubocop", "= 0.49.1"
gem "yardstick"
gem "certificate_authority", :require => false
gem "activemodel", :require => false # Used by certificate_authority
end
group :doc do
......
# frozen_string_literal: true
# More info at https://github.com/guard/guard#readme
guard :rspec, :cmd => "GUARD_RSPEC=1 bundle exec rspec --no-profile" do
......
......@@ -6,10 +6,11 @@
[![Coverage Status](https://coveralls.io/repos/httprb/http/badge.svg?branch=master)](https://coveralls.io/r/httprb/http)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/httprb/http/blob/master/LICENSE.txt)
_NOTE: This is the 2.x **stable** branch. For the 3.x **development** branch, please see:_
_NOTE: This is the 3.x **stable** branch. For the 4.x **development** branch, please see:_
https://github.com/httprb/http/
## About
HTTP (The Gem! a.k.a. http.rb) is an easy-to-use client library for making requests
......@@ -160,10 +161,9 @@ and call `#readpartial` on it repeatedly until it returns `nil`:
This library aims to support and is [tested against][travis] the following Ruby
versions:
* Ruby 2.0.0
* Ruby 2.1.x
* Ruby 2.2.x
* Ruby 2.3.x
* Ruby 2.4.x
* JRuby 9.1.x.x
If something doesn't work on one of these versions, it's a bug.
......@@ -194,5 +194,5 @@ dropped.
## Copyright
Copyright (c) 2011-2016 Tony Arcieri, Erik Michaels-Ober, Alexey V. Zapparov, Zachary Anker.
Copyright (c) 2011-2018 Tony Arcieri, Alexey V. Zapparov, Erik Michaels-Ober, Zachary Anker.
See LICENSE.txt for further details.
#!/usr/bin/env rake
# frozen_string_literal: true
require "bundler/gem_tasks"
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new
task :test => :spec
begin
require "rubocop/rake_task"
RuboCop::RakeTask.new
rescue LoadError
task :rubocop do
$stderr.puts "RuboCop is disabled"
end
end
require "rubocop/rake_task"
RuboCop::RakeTask.new
require "yardstick/rake/measurement"
Yardstick::Rake::Measurement.new do |measurement|
......@@ -36,7 +29,7 @@ task :generate_status_codes do
code = e.xpath("xmlns:value").text.to_s
desc = e.xpath("xmlns:description").text.to_s
next a if "Unassigned" == desc || "(Unused)" == desc
next a if %w[Unassigned (Unused)].include?(desc)
a << "#{code} => #{desc.inspect}"
end
......@@ -68,4 +61,12 @@ task :generate_status_codes do
end
end
task :default => [:spec, :rubocop, :verify_measurements]
if ENV["CI"].nil?
task :default => %i[spec rubocop verify_measurements]
else
case ENV["SUITE"]
when "rubocop" then task :default => :rubocop
when "yardstick" then task :default => :verify_measurements
else task :default => :spec
end
end
ruby-http (3.3.0-1) experimental; urgency=medium
* Team upload
* New upstream release
* Bump standards version to 4.1.4 (no changes)
-- Manas kashyap <manaskashyaptech@gmail.com> Sat, 05 May 2018 16:59:13 +0000
ruby-http (2.2.2-1) unstable; urgency=medium
* New upstream release
......
......@@ -3,7 +3,7 @@ Section: ruby
Priority: optional
Maintainer: Debian Ruby Extras Maintainers <pkg-ruby-extras-maintainers@lists.alioth.debian.org>
Uploaders: Pirate Praveen <praveen@debian.org>
Build-Depends: debhelper (>= 9~),
Build-Depends: debhelper (>= 11),
gem2deb,
ruby-addressable,
ruby-certificate-authority,
......@@ -14,9 +14,9 @@ Build-Depends: debhelper (>= 9~),
ruby-rspec,
ruby-rspec-its,
ruby-simplecov
Standards-Version: 4.0.0
Vcs-Git: https://anonscm.debian.org/git/pkg-ruby-extras/ruby-http.git
Vcs-Browser: https://anonscm.debian.org/cgit/pkg-ruby-extras/ruby-http.git
Standards-Version: 4.1.4
Vcs-Git: https://salsa.debian.org/ruby-team/ruby-http.git
Vcs-Browser: https://salsa.debian.org/ruby-team/ruby-http
Homepage: https://github.com/httprb/http.rb
Testsuite: autopkgtest-pkg-ruby
XS-Ruby-Versions: all
......
# frozen_string_literal: true
lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "http/version"
......@@ -23,10 +25,10 @@ Gem::Specification.new do |gem|
gem.require_paths = ["lib"]
gem.version = HTTP::VERSION
gem.required_ruby_version = ">= 2.0"
gem.required_ruby_version = ">= 2.2"
gem.add_runtime_dependency "http_parser.rb", "~> 0.6.0"
gem.add_runtime_dependency "http-form_data", "~> 1.0.1"
gem.add_runtime_dependency "http-form_data", "~> 2.0"
gem.add_runtime_dependency "http-cookie", "~> 1.0"
gem.add_runtime_dependency "addressable", "~> 2.3"
......
# frozen_string_literal: true
require "http/parser"
require "http/errors"
......
# frozen_string_literal: true
require "base64"
require "http/headers"
......@@ -8,73 +9,82 @@ module HTTP
# Request a get sans response body
# @param uri
# @option options [Hash]
def head(uri, options = {})
def head(uri, options = {}) # rubocop:disable Style/OptionHash
request :head, uri, options
end
# Get a resource
# @param uri
# @option options [Hash]
def get(uri, options = {})
def get(uri, options = {}) # rubocop:disable Style/OptionHash
request :get, uri, options
end
# Post to a resource
# @param uri
# @option options [Hash]
def post(uri, options = {})
def post(uri, options = {}) # rubocop:disable Style/OptionHash
request :post, uri, options
end
# Put to a resource
# @param uri
# @option options [Hash]
def put(uri, options = {})
def put(uri, options = {}) # rubocop:disable Style/OptionHash
request :put, uri, options
end
# Delete a resource
# @param uri
# @option options [Hash]
def delete(uri, options = {})
def delete(uri, options = {}) # rubocop:disable Style/OptionHash
request :delete, uri, options
end
# Echo the request back to the client
# @param uri
# @option options [Hash]
def trace(uri, options = {})
def trace(uri, options = {}) # rubocop:disable Style/OptionHash
request :trace, uri, options
end
# Return the methods supported on the given URI
# @param uri
# @option options [Hash]
def options(uri, options = {})
def options(uri, options = {}) # rubocop:disable Style/OptionHash
request :options, uri, options
end
# Convert to a transparent TCP/IP tunnel
# @param uri
# @option options [Hash]
def connect(uri, options = {})
def connect(uri, options = {}) # rubocop:disable Style/OptionHash
request :connect, uri, options
end
# Apply partial modifications to a resource
# @param uri
# @option options [Hash]
def patch(uri, options = {})
def patch(uri, options = {}) # rubocop:disable Style/OptionHash
request :patch, uri, options
end
# Make an HTTP request with the given verb
# @param verb
# @param uri
# @option options [Hash]
def request(verb, uri, options = {})
def request(verb, uri, options = {}) # rubocop:disable Style/OptionHash
branch(options).request verb, uri
end
# Prepare an HTTP request with the given verb
# @param verb
# @param uri
# @option options [Hash]
def build_request(verb, uri, options = {}) # rubocop:disable Style/OptionHash
branch(options).build_request verb, uri
end
# @overload timeout(options = {})
# Syntax sugar for `timeout(:per_operation, options)`
# @overload timeout(klass, options = {})
......@@ -85,7 +95,7 @@ module HTTP
# @option options [Float] :read Read timeout
# @option options [Float] :write Write timeout
# @option options [Float] :connect Connect timeout
def timeout(klass, options = {})
def timeout(klass, options = {}) # rubocop:disable Style/OptionHash
if klass.is_a? Hash
options = klass
klass = :per_operation
......@@ -98,7 +108,7 @@ module HTTP
else raise ArgumentError, "Unsupported Timeout class: #{klass}"
end
[:read, :write, :connect].each do |k|
%i[read write connect].each do |k|
next unless options.key? k
options["#{k}_timeout".to_sym] = options.delete k
end
......@@ -170,8 +180,8 @@ module HTTP
# @param opts
# @return [HTTP::Client]
# @see Redirector#initialize
def follow(opts = {})
branch default_options.with_follow opts
def follow(options = {}) # rubocop:disable Style/OptionHash
branch default_options.with_follow options
end
# Make a request with the given headers
......
# frozen_string_literal: true
require "forwardable"
require "http/form_data"
......@@ -23,27 +24,33 @@ module HTTP
end
# Make an HTTP request
def request(verb, uri, opts = {})
def request(verb, uri, opts = {}) # rubocop:disable Style/OptionHash
opts = @default_options.merge(opts)
req = build_request(verb, uri, opts)
res = perform(req, opts)
return res unless opts.follow
Redirector.new(opts.follow).perform(req, res) do |request|
perform(request, opts)
end
end
# Prepare an HTTP request
def build_request(verb, uri, opts = {}) # rubocop:disable Style/OptionHash
opts = @default_options.merge(opts)
uri = make_request_uri(uri, opts)
headers = make_request_headers(opts)
body = make_request_body(opts, headers)
proxy = opts.proxy
req = HTTP::Request.new(
:verb => verb,
:uri => uri,
:headers => headers,
:proxy => proxy,
:body => body
HTTP::Request.new(
:verb => verb,
:uri => uri,
:headers => headers,
:proxy => proxy,
:body => body,
:auto_deflate => opts.feature(:auto_deflate)
)
res = perform(req, opts)
return res unless opts.follow
Redirector.new(opts.follow).perform(req, res) do |request|
perform(request, opts)
end
end
# @!method persistent?
......@@ -121,7 +128,7 @@ module HTTP
uri = HTTP::URI.parse uri
if opts.params && !opts.params.empty?
uri.query = [uri.query, HTTP::URI.form_encode(opts.params)].compact.join("&")
uri.query_values = uri.query_values(Array).to_a.concat(opts.params.to_a)
end
# Some proxies (seen on WEBRick) fail if URL has
......@@ -146,29 +153,30 @@ module HTTP
headers[Headers::COOKIE] = cookies
end
if (auto_deflate = opts.feature(:auto_deflate))
# We need to delete Content-Length header. It will be set automatically
# by HTTP::Request::Writer
headers.delete(Headers::CONTENT_LENGTH)
headers[Headers::CONTENT_ENCODING] = auto_deflate.method
end
headers
end
# Create the request body object to send
def make_request_body(opts, headers)
request_body =
case
when opts.body
opts.body
when opts.form
form = HTTP::FormData.create opts.form
headers[Headers::CONTENT_TYPE] ||= form.content_type
headers[Headers::CONTENT_LENGTH] ||= form.content_length
form.to_s
when opts.json
body = MimeType[:json].encode opts.json
headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name}"
body
end
if (auto_deflate = opts.feature(:auto_deflate))
auto_deflate.deflate(headers, request_body)
else
request_body
case
when opts.body
opts.body
when opts.form
form = HTTP::FormData.create opts.form
headers[Headers::CONTENT_TYPE] ||= form.content_type
form
when opts.json
body = MimeType[:json].encode opts.json
headers[Headers::CONTENT_TYPE] ||= "application/json; charset=#{body.encoding.name}"
body
end
end
end
......
# frozen_string_literal: true
require "forwardable"
require "http/headers"
......@@ -6,21 +7,21 @@ require "http/response/parser"
module HTTP
# A connection to the HTTP server
class Connection
class Connection # rubocop: disable Metrics/ClassLength
extend Forwardable
# Allowed values for CONNECTION header
KEEP_ALIVE = "Keep-Alive".freeze
CLOSE = "close".freeze
KEEP_ALIVE = "Keep-Alive"
CLOSE = "close"
# Attempt to read this much data
BUFFER_SIZE = 16_384
# HTTP/1.0
HTTP_1_0 = "1.0".freeze
HTTP_1_0 = "1.0"
# HTTP/1.1
HTTP_1_1 = "1.1".freeze
HTTP_1_1 = "1.1"
# Returned after HTTP CONNECT (via proxy)
attr_reader :proxy_response_headers
......@@ -34,6 +35,7 @@ module HTTP
@pending_request = false
@pending_response = false
@failed_proxy_connect = false
@buffer = "".b
@parser = Response::Parser.new
......@@ -84,9 +86,11 @@ module HTTP
def readpartial(size = BUFFER_SIZE)
return unless @pending_response
finished = (read_more(size) == :eof) || @parser.finished?
chunk = @parser.chunk
chunk = @parser.read(size)
return chunk if chunk
finished = (read_more(size) == :eof) || @parser.finished?
chunk = @parser.read(size)
finish_response if finished
chunk.to_s
......@@ -209,8 +213,9 @@ module HTTP
def read_more(size)
return if @parser.finished?
value = @socket.readpartial(size)
value = @socket.readpartial(size, @buffer)
if value == :eof
@parser << ""
:eof
elsif value
@parser << value
......
# frozen_string_literal: true
module HTTP
ContentType = Struct.new(:mime_type, :charset) do
MIME_TYPE_RE = %r{^([^/]+/[^;]+)(?:$|;)}
......
# frozen_string_literal: true
module HTTP
# Generic error
class Error < StandardError; end
......@@ -18,6 +19,6 @@ module HTTP
# Generic Timeout error
class TimeoutError < Error; end
# Header name is invalid
class InvalidHeaderNameError < Error; end
# Header value is of unexpected format (similar to Net::HTTPHeaderSyntaxError)
class HeaderError < Error; end
end
# frozen_string_literal: true
module HTTP
class Feature
def initialize(opts = {})
def initialize(opts = {}) # rubocop:disable Style/OptionHash
@opts = opts
end
end
......
# frozen_string_literal: true
require "zlib"
require "tempfile"
module HTTP
module Features
......@@ -12,32 +13,88 @@ module HTTP
@method = @opts.key?(:method) ? @opts[:method].to_s : "gzip"
raise Error, "Only gzip and deflate methods are supported" unless %w(gzip deflate).include?(@method)
raise Error, "Only gzip and deflate methods are supported" unless %w[gzip deflate].include?(@method)
end
def deflate(headers, body)
return body unless body
return body unless body.is_a?(String)
def deflated_body(body)
case method
when "gzip"
GzippedBody.new(body)
when "deflate"
DeflatedBody.new(body)
else
raise ArgumentError, "Unsupported deflate method: #{method}"
end
end
# We need to delete Content-Length header. It will be set automatically
# by HTTP::Request::Writer
headers.delete(Headers::CONTENT_LENGTH)
class CompressedBody
def initialize(body)
@body = body
@compressed = nil
end
headers[Headers::CONTENT_ENCODING] = method
def size
compress_all! unless @compressed
@compressed.size
end
case method
when "gzip" then
StringIO.open do |out|
Zlib::GzipWriter.wrap(out) do |gz|
gz.write body
gz.finish
out.tap(&:rewind).read
end
def each(&block)
return to_enum __method__ unless block
if @compressed
compressed_each(&block)
else
compress(&block)
end
when "deflate" then
Zlib::Deflate.deflate(body)
else
raise ArgumentError, "Unsupported deflate method: #{method}"
self
end
private
def compressed_each
while (data = @compressed.read(Connection::BUFFER_SIZE))
yield data
end
ensure
@compressed.close!
end
def compress_all!
@compressed = Tempfile.new("http-compressed_body", :binmode => true)
compress { |data| @compressed.write(data) }
@compressed.rewind
end
end
class GzippedBody < CompressedBody
def compress(&block)
gzip = Zlib::GzipWriter.new(BlockIO.new(block))
@body.each { |chunk| gzip.write(chunk) }
ensure
gzip.finish
end
class BlockIO
def initialize(block)
@block = block
end
def write(data)
@block.call(data)
end
end
end
class DeflatedBody < CompressedBody
def compress
deflater = Zlib::Deflate.new
@body.each { |chunk| yield deflater.deflate(chunk) }
yield deflater.finish
ensure
deflater.close
end
end
end
......
# frozen_string_literal: true
module HTTP
module Features
class AutoInflate < Feature
def stream_for(connection, response)
if %w(deflate gzip x-gzip).include?(response.headers[:content_encoding])
if %w[deflate gzip x-gzip].include?(response.headers[:content_encoding])
Response::Inflater.new(connection)
else
connection
......
# frozen_string_literal: true
require "forwardable"
require "http/errors"
......@@ -196,7 +197,7 @@ module HTTP
# Transforms `name` to canonical HTTP header capitalization
#
# @param [String] name
# @raise [InvalidHeaderNameError] if normalized name does not
# @raise [HeaderError] if normalized name does not
# match {HEADER_NAME_RE}
# @return [String] canonical HTTP header name
def normalize_header(name)
......@@ -206,7 +207,7 @@ module HTTP
return normalized if normalized =~ COMPLIANT_NAME_RE
raise InvalidHeaderNameError, "Invalid HTTP header field name: #{name.inspect}"
raise HeaderError, "Invalid HTTP header field name: #{name.inspect}"
end
end
end
# frozen_string_literal: true
module HTTP
class Headers
# Content-Types that are acceptable for the response.
ACCEPT = "Accept".freeze
ACCEPT = "Accept"
# The age the object has been in a proxy cache in seconds.
AGE = "Age".freeze
AGE = "Age"
# Authentication credentials for HTTP authentication.
AUTHORIZATION = "Authorization".freeze
AUTHORIZATION = "Authorization"
# Used to specify directives that must be obeyed by all caching mechanisms
# along the request-response chain.
CACHE_CONTROL = "Cache-Control".freeze
CACHE_CONTROL = "Cache-Control"
# An HTTP cookie previously sent by the server with Set-Cookie.
COOKIE = "Cookie".freeze
COOKIE = "Cookie"
# Control options for the current connection and list
# of hop-by-hop request fields.
CONNECTION = "Connection".freeze
CONNECTION = "Connection"
# The length of the request body in octets (8-bit bytes).
CONTENT_LENGTH = "Content-Length".freeze
CONTENT_LENGTH = "Content-Length"
# The MIME type of the body of the request
# (used with POST and PUT requests).
CONTENT_TYPE = "Content-Type".freeze
CONTENT_TYPE = "Content-Type"
# The date and time that the message was sent (in "HTTP-date" format as
# defined by RFC 7231 Date/Time Formats).
DATE = "Date".freeze
DATE = "Date"
# An identifier for a specific version of a resource,
# often a message digest.
ETAG = "ETag".freeze
ETAG = "ETag"
# Gives the date/time after which the response is considered stale (in
# "HTTP-date" format as defined by RFC 7231).
EXPIRES = "Expires".freeze
EXPIRES = "Expires"
# The domain name of the server (for virtual hosting), and the TCP port
# number on which the server is listening. The port number may be omitted
# if the port is the standard port for the service requested.
HOST = "Host".freeze
HOST = "Host"
# Allows a 304 Not Modified to be returned if content is unchanged.
IF_MODIFIED_SINCE = "If-Modified-Since".freeze
IF_MODIFIED_SINCE = "If-Modified-Since"
# Allows a 304 Not Modified to be returned if content is unchanged.
IF_NONE_MATCH = "If-None-Match".freeze
IF_NONE_MATCH = "If-None-Match"
# The last modified date for the requested object (in "HTTP-date" format as
# defined by RFC 7231).
LAST_MODIFIED = "Last-Modified".freeze
LAST_MODIFIED = "Last-Modified"
# Used in redirection, or when a new resource has been created.
LOCATION = "Location".freeze
LOCATION = "Location"
# Authorization credentials for connecting to a proxy.
PROXY_AUTHORIZATION = "Proxy-Authorization".freeze
PROXY_AUTHORIZATION = "Proxy-Authorization"
# An HTTP cookie.
SET_COOKIE = "Set-Cookie".freeze
SET_COOKIE = "Set-Cookie"
# The form of encoding used to safely transfer the entity to the user.
# Currently defined methods are: chunked, compress, deflate, gzip, identity.
TRANSFER_ENCODING = "Transfer-Encoding".freeze
TRANSFER_ENCODING = "Transfer-Encoding"
# Indicates what additional content codings have been applied to the
# entity-body.
CONTENT_ENCODING = "Content-Encoding".freeze
CONTENT_ENCODING = "Content-Encoding"
# The user agent string of the user agent.
USER_AGENT = "User-Agent".freeze
USER_AGENT = "User-Agent"
# Tells downstream proxies how to match future request headers to decide
# whether the cached response can be used rather than requesting a fresh
# one from the origin server.
VARY = "Vary".freeze
VARY = "Vary"
end
end
# frozen_string_literal: true
require "forwardable"
module HTTP
......
# frozen_string_literal: true
module HTTP
# MIME type encode/decode adapters
module MimeType
......
# frozen_string_literal: true
require "forwardable"
require "singleton"
......@@ -13,7 +14,7 @@ module HTTP
def_delegators :instance, :encode, :decode
end
%w(encode decode).each do |operation|
%w[encode decode].each do |operation|
class_eval <<-RUBY, __FILE__, __LINE__
def #{operation}(*)
fail Error, "\#{self.class} does not supports ##{operation}"
......
# frozen_string_literal: true
require "json"
require "http/mime_type/adapter"
......@@ -14,7 +15,7 @@ module HTTP
# Decodes JSON
def decode(str)
::JSON.load str
::JSON.parse str
end
end
......
# frozen_string_literal: true
# rubocop:disable Metrics/ClassLength, Style/RedundantSelf
require "http/headers"
require "openssl"
require "socket"
......@@ -8,7 +11,6 @@ require "http/features/auto_inflate"
require "http/features/auto_deflate"
module HTTP
# rubocop:disable Metrics/ClassLength
class Options
@default_socket_class = TCPSocket
@default_ssl_socket_class = OpenSSL::SSL::SSLSocket
......@@ -22,7 +24,7 @@ module HTTP
attr_accessor :default_socket_class, :default_ssl_socket_class, :default_timeout_class
attr_reader :available_features
def new(options = {})
def new(options = {}) # rubocop:disable Style/OptionHash
return options if options.is_a?(self)
super
end
......@@ -46,7 +48,7 @@ module HTTP
end
end
def initialize(options = {})
def initialize(options = {}) # rubocop:disable Style/OptionHash
defaults = {
:response => :auto,
:proxy => {},
......@@ -115,21 +117,22 @@ module HTTP
end
end
%w(
%w[
proxy params form json body follow response
socket_class nodelay ssl_socket_class ssl_context ssl
persistent keep_alive_timeout timeout_class timeout_options
).each do |method_name|
].each do |method_name|
def_option method_name
end
def follow=(value)
@follow = case
when !value then nil
when true == value then {}
when value.respond_to?(:fetch) then value
else argument_error! "Unsupported follow options: #{value}"
end
@follow =
case
when !value then nil
when true == value then {}
when value.respond_to?(:fetch) then value
else argument_error! "Unsupported follow options: #{value}"
end
end
def persistent=(value)
......@@ -182,7 +185,7 @@ module HTTP
private
def argument_error!(message)
raise(Error, message, caller[1..-1])
raise(Error, message, caller(1..-1))
end
end
end
# frozen_string_literal: true
require "set"
require "http/headers"
......@@ -20,10 +21,10 @@ module HTTP
# Insecure http verbs, which should trigger StateError in strict mode
# upon {STRICT_SENSITIVE_CODES}
UNSAFE_VERBS = [:put, :delete, :post].to_set.freeze
UNSAFE_VERBS = %i[put delete post].to_set.freeze
# Verbs which will remain unchanged upon See Other response.
SEE_OTHER_ALLOWED_VERBS = [:get, :head].to_set.freeze
SEE_OTHER_ALLOWED_VERBS = %i[get head].to_set.freeze
# @!attribute [r] strict
# Returns redirector policy.
......@@ -38,7 +39,7 @@ module HTTP
# @param [Hash] opts
# @option opts [Boolean] :strict (true) redirector hops policy
# @option opts [#to_i] :max_hops (5) maximum allowed amount of hops
def initialize(opts = {})
def initialize(opts = {}) # rubocop:disable Style/OptionHash
@strict = opts.fetch(:strict, true)
@max_hops = opts.fetch(:max_hops, 5).to_i
end
......
# frozen_string_literal: true
require "forwardable"
require "base64"
require "time"
require "http/errors"
require "http/headers"
require "http/request/body"
require "http/request/writer"
require "http/version"
require "http/uri"
......@@ -22,7 +24,7 @@ module HTTP
class UnsupportedSchemeError < RequestError; end
# Default User-Agent header value
USER_AGENT = "http.rb/#{HTTP::VERSION}".freeze
USER_AGENT = "http.rb/#{HTTP::VERSION}"
METHODS = [
# RFC 2616: Hypertext Transfer Protocol -- HTTP/1.1
......@@ -48,7 +50,7 @@ module HTTP
].freeze
# Allowed schemes
SCHEMES = [:http, :https, :ws, :wss].freeze
SCHEMES = %i[http https ws wss].freeze
# Default ports of supported schemes
PORTS = {
......@@ -74,7 +76,7 @@ module HTTP
# @option opts [HTTP::URI, #to_s] :uri
# @option opts [Hash] :headers
# @option opts [Hash] :proxy
# @option opts [String] :body
# @option opts [String, Enumerable, IO, nil] :body
def initialize(opts)
@verb = opts.fetch(:verb).to_s.downcase.to_sym
@uri = normalize_uri(opts.fetch(:uri))
......@@ -84,7 +86,7 @@ module HTTP
raise(UnsupportedSchemeError, "unknown scheme: #{scheme}") unless SCHEMES.include?(@scheme)
@proxy = opts[:proxy] || {}
@body = opts[:body]
@body = request_body(opts[:body], opts)
@version = opts[:version] || "1.1"
@headers = HTTP::Headers.coerce(opts[:headers] || {})
......@@ -94,7 +96,10 @@ module HTTP
# Returns new Request with updated uri
def redirect(uri, verb = @verb)
req = self.class.new(
headers = self.headers.dup
headers.delete(Headers::HOST)
self.class.new(
:verb => verb,
:uri => @uri.join(uri),
:headers => headers,
......@@ -102,9 +107,6 @@ module HTTP
:body => body,
:version => version
)
req[Headers::HOST] = req.uri.host
req
end
# Stream the request to a socket
......@@ -145,8 +147,14 @@ module HTTP
# Compute HTTP request header for direct or proxy request
def headline
request_uri = (using_proxy? && !uri.https?) ? uri : uri.omit(:scheme, :authority)
"#{verb.to_s.upcase} #{request_uri.omit :fragment} HTTP/#{version}"
request_uri =
if using_proxy? && !uri.https?
uri.omit(:fragment)
else
uri.request_uri
end
"#{verb.to_s.upcase} #{request_uri} HTTP/#{version}"
end
# Compute HTTP request header SSL proxy connection
......@@ -178,6 +186,13 @@ module HTTP
private
# Transforms body to an object suitable for streaming.
def request_body(body, opts)
body = Request::Body.new(body) unless body.is_a?(Request::Body)
body = opts[:auto_deflate].deflated_body(body) if opts[:auto_deflate]
body
end
# @!attribute [r] host
# @return [String]
def_delegator :@uri, :host
......
# frozen_string_literal: true
module HTTP
class Request
class Body
attr_reader :source
def initialize(source)
@source = source
validate_source_type!
end
# Returns size which should be used for the "Content-Length" header.
#
# @return [Integer]
def size
if @source.is_a?(String)
@source.bytesize
elsif @source.respond_to?(:read)
raise RequestError, "IO object must respond to #size" unless @source.respond_to?(:size)
@source.size
elsif @source.nil?
0
else
raise RequestError, "cannot determine size of body: #{@source.inspect}"
end
end
# Yields chunks of content to be streamed to the request body.
#
# @yieldparam [String]
def each(&block)
if @source.is_a?(String)
yield @source
elsif @source.respond_to?(:read)
IO.copy_stream(@source, ProcIO.new(block))
@source.rewind if @source.respond_to?(:rewind)
elsif @source.is_a?(Enumerable)
@source.each(&block)
end
end
private
def validate_source_type!
return if @source.is_a?(String)
return if @source.respond_to?(:read)
return if @source.is_a?(Enumerable)
return if @source.nil?
raise RequestError, "body of wrong type: #{@source.class}"
end
# This class provides a "writable IO" wrapper around a proc object, with
# #write simply calling the proc, which we can pass in as the
# "destination IO" in IO.copy_stream.
class ProcIO
def initialize(block)
@block = block
end
def write(data)
@block.call(data)
data.bytesize
end
end
end
end
end
# frozen_string_literal: true
require "http/headers"
module HTTP
class Request
class Writer
# CRLF is the universal HTTP delimiter
CRLF = "\r\n".freeze
CRLF = "\r\n"
# Chunked data termintaor.
ZERO = "0".freeze
ZERO = "0"
# Chunked transfer encoding
CHUNKED = "chunked".freeze
CHUNKED = "chunked"
# End of a chunked transfer
CHUNKED_END = "#{ZERO}#{CRLF}#{CRLF}".freeze
# Types valid to be used as body source
VALID_BODY_TYPES = [String, NilClass, Enumerable].freeze
CHUNKED_END = "#{ZERO}#{CRLF}#{CRLF}"
def initialize(socket, body, headers, headline)
@body = body
@socket = socket
@headers = headers
@request_header = [headline]
validate_body_type!
end
# Adds headers to the request header from the headers array
......@@ -51,13 +47,9 @@ module HTTP
# Adds the headers to the header array for the given request body we are working
# with
def add_body_type_headers
if @body.is_a?(String) && !@headers[Headers::CONTENT_LENGTH]
@request_header << "#{Headers::CONTENT_LENGTH}: #{@body.bytesize}"
elsif @body.nil? && !@headers[Headers::CONTENT_LENGTH]
@request_header << "#{Headers::CONTENT_LENGTH}: 0"
elsif @body.is_a?(Enumerable) && CHUNKED != @headers[Headers::TRANSFER_ENCODING]
raise(RequestError, "invalid transfer encoding")
end
return if @headers[Headers::CONTENT_LENGTH] || chunked?
@request_header << "#{Headers::CONTENT_LENGTH}: #{@body.size}"
end
# Joins the headers specified in the request into a correctly formatted
......@@ -69,29 +61,37 @@ module HTTP
end
def send_request
headers = join_headers
# It's important to send the request in a single write call when
# possible in order to play nicely with Nagle's algorithm. Making
# two writes in a row triggers a pathological case where Nagle is
# expecting a third write that never happens.
case @body
when NilClass
write(headers)
when String
write(headers << @body)
when Enumerable
write(headers)
@body.each do |chunk|
write(chunk.bytesize.to_s(16) << CRLF << chunk << CRLF)
end
write(CHUNKED_END)
else raise TypeError, "invalid body type: #{@body.class}"
data = join_headers
@body.each do |chunk|
data << encode_chunk(chunk)
write(data)
data.clear
end
write(data) unless data.empty?
write(CHUNKED_END) if chunked?
end
# Returns the chunk encoded for to the specified "Transfer-Encoding" header.
def encode_chunk(chunk)
if chunked?
chunk.bytesize.to_s(16) << CRLF << chunk << CRLF
else
chunk
end
end
# Returns true if the request should be sent in chunked encoding.
def chunked?
@headers[Headers::TRANSFER_ENCODING] == CHUNKED
end
private
def write(data)
......@@ -101,11 +101,6 @@ module HTTP
data = data.byteslice(length..-1)
end
end
def validate_body_type!
return if VALID_BODY_TYPES.any? { |type| @body.is_a? type }
raise RequestError, "body of wrong type: #{@body.class}"
end
end
end
end
# frozen_string_literal: true
require "forwardable"
require "http/headers"
......@@ -50,7 +51,7 @@ module HTTP
encoding = opts[:encoding] || charset || Encoding::BINARY
stream = body_stream_for(connection, opts)
@body = Response::Body.new(stream, encoding)
@body = Response::Body.new(stream, :encoding => encoding)
else
@body = opts.fetch(:body)
end
......@@ -98,8 +99,13 @@ module HTTP
# (not an integer, e.g. empty string or string with non-digits).
# @return [Integer] otherwise
def content_length
# http://greenbytes.de/tech/webdav/rfc7230.html#rfc.section.3.3.3
# Clause 3: "If a message is received with both a Transfer-Encoding
# and a Content-Length header field, the Transfer-Encoding overrides the Content-Length.
return nil if @headers.include?(Headers::TRANSFER_ENCODING)
value = @headers[Headers::CONTENT_LENGTH]
return unless value
return nil unless value
begin
Integer(value)
......@@ -131,6 +137,15 @@ module HTTP
end
end
def chunked?
return false unless @headers.include?(Headers::TRANSFER_ENCODING)
encoding = @headers.get(Headers::TRANSFER_ENCODING)
# TODO: "chunked" is frozen in the request writer. How about making it accessible?
encoding.last == "chunked"
end
# Parse response body with corresponding MIME type adapter.
#
# @param [#to_s] as Parse as given MIME type
......
# frozen_string_literal: true
require "forwardable"
require "http/client"
......@@ -15,18 +16,19 @@ module HTTP
# @return [HTTP::Connection]
attr_reader :connection
def initialize(stream, encoding = Encoding::BINARY)
def initialize(stream, encoding: Encoding::BINARY)
@stream = stream
@connection = stream.is_a?(Inflater) ? stream.connection : stream
@streaming = nil
@contents = nil
@encoding = encoding
@encoding = find_encoding(encoding)
end
# (see HTTP::Client#readpartial)
def readpartial(*args)
stream!
@stream.readpartial(*args)
chunk = @stream.readpartial(*args)
chunk.force_encoding(@encoding) if chunk
end
# Iterate over the body, allowing it to be enumerable
......@@ -42,19 +44,13 @@ module HTTP
raise StateError, "body is being streamed" unless @streaming.nil?
# see issue 312
begin
encoding = Encoding.find @encoding
rescue ArgumentError
encoding = Encoding::BINARY
end
begin
@streaming = false
@contents = String.new("").force_encoding(encoding)
@contents = String.new