em-http-request-samesite 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +9 -0
- data/.rspec +0 -0
- data/.travis.yml +7 -0
- data/Changelog.md +68 -0
- data/Gemfile +14 -0
- data/README.md +63 -0
- data/Rakefile +10 -0
- data/benchmarks/clients.rb +170 -0
- data/benchmarks/em-excon.rb +87 -0
- data/benchmarks/em-profile.gif +0 -0
- data/benchmarks/em-profile.txt +65 -0
- data/benchmarks/server.rb +48 -0
- data/em-http-request.gemspec +32 -0
- data/examples/.gitignore +1 -0
- data/examples/digest_auth/client.rb +25 -0
- data/examples/digest_auth/server.rb +28 -0
- data/examples/fetch.rb +30 -0
- data/examples/fibered-http.rb +51 -0
- data/examples/multi.rb +25 -0
- data/examples/oauth-tweet.rb +35 -0
- data/examples/socks5.rb +23 -0
- data/lib/em-http-request.rb +1 -0
- data/lib/em-http.rb +20 -0
- data/lib/em-http/client.rb +341 -0
- data/lib/em-http/core_ext/bytesize.rb +6 -0
- data/lib/em-http/decoders.rb +252 -0
- data/lib/em-http/http_client_options.rb +49 -0
- data/lib/em-http/http_connection.rb +321 -0
- data/lib/em-http/http_connection_options.rb +70 -0
- data/lib/em-http/http_encoding.rb +149 -0
- data/lib/em-http/http_header.rb +83 -0
- data/lib/em-http/http_status_codes.rb +57 -0
- data/lib/em-http/middleware/digest_auth.rb +112 -0
- data/lib/em-http/middleware/json_response.rb +15 -0
- data/lib/em-http/middleware/oauth.rb +40 -0
- data/lib/em-http/middleware/oauth2.rb +28 -0
- data/lib/em-http/multi.rb +57 -0
- data/lib/em-http/request.rb +23 -0
- data/lib/em-http/version.rb +5 -0
- data/lib/em/io_streamer.rb +49 -0
- data/spec/client_fiber_spec.rb +23 -0
- data/spec/client_spec.rb +1000 -0
- data/spec/digest_auth_spec.rb +48 -0
- data/spec/dns_spec.rb +41 -0
- data/spec/encoding_spec.rb +49 -0
- data/spec/external_spec.rb +150 -0
- data/spec/fixtures/google.ca +16 -0
- data/spec/fixtures/gzip-sample.gz +0 -0
- data/spec/gzip_spec.rb +91 -0
- data/spec/helper.rb +31 -0
- data/spec/http_proxy_spec.rb +268 -0
- data/spec/middleware/oauth2_spec.rb +15 -0
- data/spec/middleware_spec.rb +143 -0
- data/spec/multi_spec.rb +104 -0
- data/spec/pipelining_spec.rb +66 -0
- data/spec/redirect_spec.rb +430 -0
- data/spec/socksify_proxy_spec.rb +60 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/ssl_spec.rb +71 -0
- data/spec/stallion.rb +334 -0
- data/spec/stub_server.rb +45 -0
- metadata +265 -0
Binary file
|
@@ -0,0 +1,65 @@
|
|
1
|
+
Total: 143 samples
|
2
|
+
67 46.9% 46.9% 109 76.2% Kernel#gem_original_require
|
3
|
+
17 11.9% 58.7% 17 11.9% garbage_collector
|
4
|
+
12 8.4% 67.1% 12 8.4% EventMachine.connect_server
|
5
|
+
11 7.7% 74.8% 11 7.7% Dir.[]
|
6
|
+
11 7.7% 82.5% 11 7.7% File.file?
|
7
|
+
4 2.8% 85.3% 4 2.8% Kernel#eval
|
8
|
+
3 2.1% 87.4% 16 11.2% Enumerable#find
|
9
|
+
3 2.1% 89.5% 3 2.1% Regexp#initialize
|
10
|
+
2 1.4% 90.9% 16 11.2% EventMachine.run_machine
|
11
|
+
2 1.4% 92.3% 2 1.4% Regexp#match
|
12
|
+
1 0.7% 93.0% 1 0.7% FFI::DynamicLibrary.open
|
13
|
+
1 0.7% 93.7% 1 0.7% Hash#values
|
14
|
+
1 0.7% 94.4% 7 4.9% Kernel#load
|
15
|
+
1 0.7% 95.1% 3 2.1% MIME::Type#initialize
|
16
|
+
1 0.7% 95.8% 1 0.7% MatchData#captures
|
17
|
+
1 0.7% 96.5% 13 9.1% Module#bind_connect
|
18
|
+
1 0.7% 97.2% 17 11.9% Module#searcher
|
19
|
+
1 0.7% 97.9% 1 0.7% String#downcase
|
20
|
+
1 0.7% 98.6% 1 0.7% String#scan
|
21
|
+
1 0.7% 99.3% 1 0.7% String#strip
|
22
|
+
1 0.7% 100.0% 1 0.7% TCPSocket#initialize
|
23
|
+
0 0.0% 100.0% 1 0.7% Array#map
|
24
|
+
0 0.0% 100.0% 14 9.8% Array#reverse_each
|
25
|
+
0 0.0% 100.0% 14 9.8% Class#from_gems_in
|
26
|
+
0 0.0% 100.0% 14 9.8% Class#from_installed_gems
|
27
|
+
0 0.0% 100.0% 14 9.8% Class#load
|
28
|
+
0 0.0% 100.0% 1 0.7% Class#parse
|
29
|
+
0 0.0% 100.0% 1 0.7% Class#simplified
|
30
|
+
0 0.0% 100.0% 6 4.2% Enumerable#each_with_index
|
31
|
+
0 0.0% 100.0% 1 0.7% Excon::Connection#connect
|
32
|
+
0 0.0% 100.0% 1 0.7% Excon::Connection#request
|
33
|
+
0 0.0% 100.0% 1 0.7% Excon::Connection#socket
|
34
|
+
0 0.0% 100.0% 1 0.7% FFI::Library#ffi_lib
|
35
|
+
0 0.0% 100.0% 10 7.0% Gem::GemPathSearcher#find
|
36
|
+
0 0.0% 100.0% 1 0.7% Gem::GemPathSearcher#find_active
|
37
|
+
0 0.0% 100.0% 16 11.2% Gem::GemPathSearcher#init_gemspecs
|
38
|
+
0 0.0% 100.0% 16 11.2% Gem::GemPathSearcher#initialize
|
39
|
+
0 0.0% 100.0% 11 7.7% Gem::GemPathSearcher#matching_file?
|
40
|
+
0 0.0% 100.0% 11 7.7% Gem::GemPathSearcher#matching_files
|
41
|
+
0 0.0% 100.0% 14 9.8% Gem::SourceIndex#load_gems_in
|
42
|
+
0 0.0% 100.0% 14 9.8% Gem::SourceIndex#refresh!
|
43
|
+
0 0.0% 100.0% 1 0.7% Gem::SourceIndex#search
|
44
|
+
0 0.0% 100.0% 1 0.7% HttpOptions#initialize
|
45
|
+
0 0.0% 100.0% 1 0.7% HttpOptions#set_uri
|
46
|
+
0 0.0% 100.0% 14 9.8% Integer#times
|
47
|
+
0 0.0% 100.0% 1 0.7% Kernel#loop
|
48
|
+
0 0.0% 100.0% 109 76.2% Kernel#require
|
49
|
+
0 0.0% 100.0% 1 0.7% Module#activate
|
50
|
+
0 0.0% 100.0% 13 9.1% Module#connect
|
51
|
+
0 0.0% 100.0% 1 0.7% Module#get
|
52
|
+
0 0.0% 100.0% 2 1.4% Module#load_full_rubygems_library
|
53
|
+
0 0.0% 100.0% 5 3.5% Module#loaded_path?
|
54
|
+
0 0.0% 100.0% 16 11.2% Module#meter
|
55
|
+
0 0.0% 100.0% 16 11.2% Module#run
|
56
|
+
0 0.0% 100.0% 16 11.2% Module#source_index
|
57
|
+
0 0.0% 100.0% 30 21.0% Module#try_activate
|
58
|
+
0 0.0% 100.0% 17 11.9% Object#with_server
|
59
|
+
0 0.0% 100.0% 2 1.4% Regexp.union
|
60
|
+
0 0.0% 100.0% 1 0.7% TCPSocket.open
|
61
|
+
0 0.0% 100.0% 16 11.2% Tach::Meter#initialize
|
62
|
+
0 0.0% 100.0% 16 11.2% Tach::Meter#run_tach
|
63
|
+
0 0.0% 100.0% 1 0.7% URI::Parser#initialize
|
64
|
+
0 0.0% 100.0% 1 0.7% URI::Parser#initialize_regexp
|
65
|
+
0 0.0% 100.0% 7 4.9% YAML::EngineManager#yamler=
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'excon'
|
2
|
+
require 'httparty'
|
3
|
+
require 'net/http'
|
4
|
+
require 'open-uri'
|
5
|
+
require 'rest_client'
|
6
|
+
require 'tach'
|
7
|
+
require 'typhoeus'
|
8
|
+
require 'sinatra/base'
|
9
|
+
require 'streamly_ffi'
|
10
|
+
require 'curb'
|
11
|
+
|
12
|
+
require File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib', 'em-http')
|
13
|
+
|
14
|
+
module Benchmark
|
15
|
+
class Server < Sinatra::Base
|
16
|
+
|
17
|
+
def self.run
|
18
|
+
Rack::Handler::WEBrick.run(
|
19
|
+
Benchmark::Server.new,
|
20
|
+
:Port => 9292,
|
21
|
+
:AccessLog => [],
|
22
|
+
:Logger => WEBrick::Log.new(nil, WEBrick::Log::ERROR)
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
get '/data/:amount' do |amount|
|
27
|
+
'x' * amount.to_i
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def with_server(&block)
|
34
|
+
pid = Process.fork do
|
35
|
+
Benchmark::Server.run
|
36
|
+
end
|
37
|
+
loop do
|
38
|
+
sleep(1)
|
39
|
+
begin
|
40
|
+
#Excon.get('http://localhost:9292/api/foo')
|
41
|
+
break
|
42
|
+
rescue
|
43
|
+
end
|
44
|
+
end
|
45
|
+
yield
|
46
|
+
ensure
|
47
|
+
Process.kill(9, pid)
|
48
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
3
|
+
require 'em-http/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'em-http-request-samesite'
|
7
|
+
s.version = EventMachine::HttpRequest::VERSION
|
8
|
+
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ["Ilya Grigorik"]
|
11
|
+
s.summary = 'EventMachine based, async HTTP Request client'
|
12
|
+
s.description = s.summary
|
13
|
+
s.license = 'MIT'
|
14
|
+
s.rubyforge_project = 'em-http-request'
|
15
|
+
|
16
|
+
s.add_dependency 'addressable', '>= 2.3.4'
|
17
|
+
s.add_dependency 'cookiejar-future'
|
18
|
+
s.add_dependency 'em-socksify', '>= 0.3'
|
19
|
+
s.add_dependency 'eventmachine', '>= 1.0.3'
|
20
|
+
s.add_dependency 'http_parser.rb', '>= 0.6.0'
|
21
|
+
|
22
|
+
s.add_development_dependency 'mongrel', '~> 1.2.0.pre2'
|
23
|
+
s.add_development_dependency 'multi_json'
|
24
|
+
s.add_development_dependency 'rack', '< 2.0'
|
25
|
+
s.add_development_dependency 'rake'
|
26
|
+
s.add_development_dependency 'rspec'
|
27
|
+
|
28
|
+
s.files = `git ls-files`.split("\n")
|
29
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
30
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
31
|
+
s.require_paths = ['lib']
|
32
|
+
end
|
data/examples/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
twitter_auth.rb
|
@@ -0,0 +1,25 @@
|
|
1
|
+
$: << 'lib' << '../../lib'
|
2
|
+
|
3
|
+
require 'em-http'
|
4
|
+
require 'em-http/middleware/digest_auth'
|
5
|
+
|
6
|
+
digest_config = {
|
7
|
+
:username => 'digest_username',
|
8
|
+
:password => 'digest_password'
|
9
|
+
}
|
10
|
+
|
11
|
+
EM.run do
|
12
|
+
|
13
|
+
conn_handshake = EM::HttpRequest.new('http://localhost:3000')
|
14
|
+
http_handshake = conn_handshake.get
|
15
|
+
|
16
|
+
http_handshake.callback do
|
17
|
+
conn = EM::HttpRequest.new('http://localhost:3000')
|
18
|
+
conn.use EM::Middleware::DigestAuth, http_handshake.response_header['WWW_AUTHENTICATE'], digest_config
|
19
|
+
http = conn.get
|
20
|
+
http.callback do
|
21
|
+
puts http.response
|
22
|
+
EM.stop
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'webrick'
|
2
|
+
|
3
|
+
include WEBrick
|
4
|
+
|
5
|
+
config = { :Realm => 'DigestAuth_REALM' }
|
6
|
+
|
7
|
+
htdigest = WEBrick::HTTPAuth::Htdigest.new 'my_password_file'
|
8
|
+
htdigest.set_passwd config[:Realm], 'digest_username', 'digest_password'
|
9
|
+
htdigest.flush
|
10
|
+
|
11
|
+
config[:UserDB] = htdigest
|
12
|
+
|
13
|
+
digest_auth = WEBrick::HTTPAuth::DigestAuth.new config
|
14
|
+
|
15
|
+
class TestServlet < HTTPServlet::AbstractServlet
|
16
|
+
def do_GET(req, res)
|
17
|
+
@options[0][:authenticator].authenticate req, res
|
18
|
+
res.body = "You are authenticated to see the super secret data\n"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
s = HTTPServer.new(:Port => 3000)
|
23
|
+
s.mount('/', TestServlet, {:authenticator => digest_auth})
|
24
|
+
trap("INT") do
|
25
|
+
File.delete('my_password_file')
|
26
|
+
s.shutdown
|
27
|
+
end
|
28
|
+
s.start
|
data/examples/fetch.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
require '../lib/em-http'
|
4
|
+
|
5
|
+
urls = ARGV
|
6
|
+
if urls.size < 1
|
7
|
+
puts "Usage: #{$0} <url> <url> <...>"
|
8
|
+
exit
|
9
|
+
end
|
10
|
+
|
11
|
+
pending = urls.size
|
12
|
+
|
13
|
+
EM.run do
|
14
|
+
urls.each do |url|
|
15
|
+
http = EM::HttpRequest.new(url).get
|
16
|
+
http.callback {
|
17
|
+
puts "#{url}\n#{http.response_header.status} - #{http.response.length} bytes\n"
|
18
|
+
puts http.response
|
19
|
+
|
20
|
+
pending -= 1
|
21
|
+
EM.stop if pending < 1
|
22
|
+
}
|
23
|
+
http.errback {
|
24
|
+
puts "#{url}\n" + http.error
|
25
|
+
|
26
|
+
pending -= 1
|
27
|
+
EM.stop if pending < 1
|
28
|
+
}
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
$: << 'lib' << '../lib'
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
require 'em-http'
|
5
|
+
require 'fiber'
|
6
|
+
|
7
|
+
# Using Fibers in Ruby 1.9 to simulate blocking IO / IO scheduling
|
8
|
+
# while using the async EventMachine API's
|
9
|
+
|
10
|
+
def async_fetch(url)
|
11
|
+
f = Fiber.current
|
12
|
+
http = EventMachine::HttpRequest.new(url, :connect_timeout => 10, :inactivity_timeout => 20).get
|
13
|
+
|
14
|
+
http.callback { f.resume(http) }
|
15
|
+
http.errback { f.resume(http) }
|
16
|
+
|
17
|
+
Fiber.yield
|
18
|
+
|
19
|
+
if http.error
|
20
|
+
p [:HTTP_ERROR, http.error]
|
21
|
+
end
|
22
|
+
|
23
|
+
http
|
24
|
+
end
|
25
|
+
|
26
|
+
EventMachine.run do
|
27
|
+
Fiber.new{
|
28
|
+
|
29
|
+
puts "Setting up HTTP request #1"
|
30
|
+
data = async_fetch('http://0.0.0.0/')
|
31
|
+
puts "Fetched page #1: #{data.response_header.status}"
|
32
|
+
|
33
|
+
puts "Setting up HTTP request #2"
|
34
|
+
data = async_fetch('http://www.yahoo.com/')
|
35
|
+
puts "Fetched page #2: #{data.response_header.status}"
|
36
|
+
|
37
|
+
puts "Setting up HTTP request #3"
|
38
|
+
data = async_fetch('http://non-existing.domain/')
|
39
|
+
puts "Fetched page #3: #{data.response_header.status}"
|
40
|
+
|
41
|
+
EventMachine.stop
|
42
|
+
}.resume
|
43
|
+
end
|
44
|
+
|
45
|
+
puts "Done"
|
46
|
+
|
47
|
+
# Setting up HTTP request #1
|
48
|
+
# Fetched page #1: 302
|
49
|
+
# Setting up HTTP request #2
|
50
|
+
# Fetched page #2: 200
|
51
|
+
# Done
|
data/examples/multi.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
$: << '../lib' << 'lib'
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
require 'em-http'
|
5
|
+
|
6
|
+
EventMachine.run {
|
7
|
+
multi = EventMachine::MultiRequest.new
|
8
|
+
|
9
|
+
reqs = [
|
10
|
+
'http://google.com/',
|
11
|
+
'http://google.ca:81/'
|
12
|
+
]
|
13
|
+
|
14
|
+
reqs.each_with_index do |url, idx|
|
15
|
+
http = EventMachine::HttpRequest.new(url, :connect_timeout => 1)
|
16
|
+
req = http.get
|
17
|
+
multi.add idx, req
|
18
|
+
end
|
19
|
+
|
20
|
+
multi.callback do
|
21
|
+
p multi.responses[:callback].size
|
22
|
+
p multi.responses[:errback].size
|
23
|
+
EventMachine.stop
|
24
|
+
end
|
25
|
+
}
|
@@ -0,0 +1,35 @@
|
|
1
|
+
$: << 'lib' << '../lib'
|
2
|
+
|
3
|
+
require 'em-http'
|
4
|
+
require 'em-http/middleware/oauth'
|
5
|
+
require 'em-http/middleware/json_response'
|
6
|
+
|
7
|
+
require 'pp'
|
8
|
+
|
9
|
+
OAuthConfig = {
|
10
|
+
:consumer_key => '',
|
11
|
+
:consumer_secret => '',
|
12
|
+
:access_token => '',
|
13
|
+
:access_token_secret => ''
|
14
|
+
}
|
15
|
+
|
16
|
+
EM.run do
|
17
|
+
# automatically parse the JSON response into a Ruby object
|
18
|
+
EventMachine::HttpRequest.use EventMachine::Middleware::JSONResponse
|
19
|
+
|
20
|
+
# sign the request with OAuth credentials
|
21
|
+
conn = EventMachine::HttpRequest.new('http://api.twitter.com/1/statuses/home_timeline.json')
|
22
|
+
conn.use EventMachine::Middleware::OAuth, OAuthConfig
|
23
|
+
|
24
|
+
http = conn.get
|
25
|
+
http.callback do
|
26
|
+
pp http.response
|
27
|
+
EM.stop
|
28
|
+
end
|
29
|
+
|
30
|
+
http.errback do
|
31
|
+
puts "Failed retrieving user stream."
|
32
|
+
pp http.response
|
33
|
+
EM.stop
|
34
|
+
end
|
35
|
+
end
|
data/examples/socks5.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
require '../lib/em-http'
|
4
|
+
|
5
|
+
EM.run do
|
6
|
+
# Establish a SOCKS5 tunnel via SSH
|
7
|
+
# ssh -D 8000 some_remote_machine
|
8
|
+
|
9
|
+
connection_options = {:proxy => {:host => '127.0.0.1', :port => 8000, :type => :socks5}}
|
10
|
+
http = EM::HttpRequest.new('http://igvita.com/', connection_options).get :redirects => 2
|
11
|
+
|
12
|
+
http.callback {
|
13
|
+
puts "#{http.response_header.status} - #{http.response.length} bytes\n"
|
14
|
+
puts http.response
|
15
|
+
EM.stop
|
16
|
+
}
|
17
|
+
|
18
|
+
http.errback {
|
19
|
+
puts "Error: " + http.error
|
20
|
+
puts http.inspect
|
21
|
+
EM.stop
|
22
|
+
}
|
23
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'em-http'
|
data/lib/em-http.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'em-socksify'
|
3
|
+
require 'addressable/uri'
|
4
|
+
require 'http/parser'
|
5
|
+
|
6
|
+
require 'base64'
|
7
|
+
require 'socket'
|
8
|
+
require 'openssl'
|
9
|
+
|
10
|
+
require 'em-http/core_ext/bytesize'
|
11
|
+
require 'em-http/http_connection'
|
12
|
+
require 'em-http/http_header'
|
13
|
+
require 'em-http/http_encoding'
|
14
|
+
require 'em-http/http_status_codes'
|
15
|
+
require 'em-http/http_client_options'
|
16
|
+
require 'em-http/http_connection_options'
|
17
|
+
require 'em-http/client'
|
18
|
+
require 'em-http/multi'
|
19
|
+
require 'em-http/request'
|
20
|
+
require 'em-http/decoders'
|
@@ -0,0 +1,341 @@
|
|
1
|
+
require 'cookiejar'
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
|
5
|
+
|
6
|
+
class HttpClient
|
7
|
+
include Deferrable
|
8
|
+
include HttpEncoding
|
9
|
+
include HttpStatus
|
10
|
+
|
11
|
+
TRANSFER_ENCODING="TRANSFER_ENCODING"
|
12
|
+
CONTENT_ENCODING="CONTENT_ENCODING"
|
13
|
+
CONTENT_LENGTH="CONTENT_LENGTH"
|
14
|
+
CONTENT_TYPE="CONTENT_TYPE"
|
15
|
+
LAST_MODIFIED="LAST_MODIFIED"
|
16
|
+
KEEP_ALIVE="CONNECTION"
|
17
|
+
SET_COOKIE="SET_COOKIE"
|
18
|
+
LOCATION="LOCATION"
|
19
|
+
HOST="HOST"
|
20
|
+
ETAG="ETAG"
|
21
|
+
|
22
|
+
CRLF="\r\n"
|
23
|
+
|
24
|
+
attr_accessor :state, :response, :conn
|
25
|
+
attr_reader :response_header, :error, :content_charset, :req, :cookies
|
26
|
+
|
27
|
+
def initialize(conn, options)
|
28
|
+
@conn = conn
|
29
|
+
@req = options
|
30
|
+
|
31
|
+
@stream = nil
|
32
|
+
@headers = nil
|
33
|
+
@cookies = []
|
34
|
+
@cookiejar = CookieJar.new
|
35
|
+
|
36
|
+
reset!
|
37
|
+
end
|
38
|
+
|
39
|
+
def reset!
|
40
|
+
@response_header = HttpResponseHeader.new
|
41
|
+
@state = :response_header
|
42
|
+
|
43
|
+
@response = ''
|
44
|
+
@error = nil
|
45
|
+
@content_decoder = nil
|
46
|
+
@content_charset = nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def last_effective_url; @req.uri; end
|
50
|
+
def redirects; @req.followed; end
|
51
|
+
def peer; @conn.peer; end
|
52
|
+
|
53
|
+
def connection_completed
|
54
|
+
@state = :response_header
|
55
|
+
|
56
|
+
head, body = build_request, @req.body
|
57
|
+
@conn.middleware.each do |m|
|
58
|
+
head, body = m.request(self, head, body) if m.respond_to?(:request)
|
59
|
+
end
|
60
|
+
|
61
|
+
send_request(head, body)
|
62
|
+
end
|
63
|
+
|
64
|
+
def on_request_complete
|
65
|
+
begin
|
66
|
+
@content_decoder.finalize! if @content_decoder
|
67
|
+
rescue HttpDecoders::DecoderError
|
68
|
+
on_error "Content-decoder error"
|
69
|
+
end
|
70
|
+
|
71
|
+
unbind
|
72
|
+
end
|
73
|
+
|
74
|
+
def continue?
|
75
|
+
@response_header.status == 100 && (@req.method == 'POST' || @req.method == 'PUT')
|
76
|
+
end
|
77
|
+
|
78
|
+
def finished?
|
79
|
+
@state == :finished || (@state == :body && @response_header.content_length.nil?)
|
80
|
+
end
|
81
|
+
|
82
|
+
def redirect?
|
83
|
+
@response_header.redirection? && @req.follow_redirect?
|
84
|
+
end
|
85
|
+
|
86
|
+
def unbind(reason = nil)
|
87
|
+
if finished?
|
88
|
+
if redirect?
|
89
|
+
|
90
|
+
begin
|
91
|
+
@conn.middleware.each do |m|
|
92
|
+
m.response(self) if m.respond_to?(:response)
|
93
|
+
end
|
94
|
+
|
95
|
+
# one of the injected middlewares could have changed
|
96
|
+
# our redirect settings, check if we still want to
|
97
|
+
# follow the location header
|
98
|
+
if redirect?
|
99
|
+
@req.followed += 1
|
100
|
+
|
101
|
+
@cookies.clear
|
102
|
+
@cookies = @cookiejar.get(@response_header.location).map(&:to_s) if @req.pass_cookies
|
103
|
+
|
104
|
+
@conn.redirect(self, @response_header.location)
|
105
|
+
else
|
106
|
+
succeed(self)
|
107
|
+
end
|
108
|
+
|
109
|
+
rescue => e
|
110
|
+
on_error(e.message)
|
111
|
+
end
|
112
|
+
else
|
113
|
+
succeed(self)
|
114
|
+
end
|
115
|
+
|
116
|
+
else
|
117
|
+
on_error(reason || 'connection closed by server')
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def on_error(msg = nil)
|
122
|
+
@error = msg
|
123
|
+
fail(self)
|
124
|
+
end
|
125
|
+
alias :close :on_error
|
126
|
+
|
127
|
+
def stream(&blk); @stream = blk; end
|
128
|
+
def headers(&blk); @headers = blk; end
|
129
|
+
|
130
|
+
def normalize_body(body)
|
131
|
+
body.is_a?(Hash) ? form_encode_body(body) : body
|
132
|
+
end
|
133
|
+
|
134
|
+
def build_request
|
135
|
+
head = @req.headers ? munge_header_keys(@req.headers) : {}
|
136
|
+
|
137
|
+
if @conn.connopts.http_proxy?
|
138
|
+
proxy = @conn.connopts.proxy
|
139
|
+
head['proxy-authorization'] = proxy[:authorization] if proxy[:authorization]
|
140
|
+
end
|
141
|
+
|
142
|
+
# Set the cookie header if provided
|
143
|
+
if cookie = head['cookie']
|
144
|
+
@cookies << encode_cookie(cookie) if cookie
|
145
|
+
end
|
146
|
+
head['cookie'] = @cookies.compact.uniq.join("; ").squeeze(";") unless @cookies.empty?
|
147
|
+
|
148
|
+
# Set connection close unless keepalive
|
149
|
+
if !@req.keepalive
|
150
|
+
head['connection'] = 'close'
|
151
|
+
end
|
152
|
+
|
153
|
+
# Set the Host header if it hasn't been specified already
|
154
|
+
head['host'] ||= encode_host
|
155
|
+
|
156
|
+
# Set the User-Agent if it hasn't been specified
|
157
|
+
if !head.key?('user-agent')
|
158
|
+
head['user-agent'] = 'EventMachine HttpClient'
|
159
|
+
elsif head['user-agent'].nil?
|
160
|
+
head.delete('user-agent')
|
161
|
+
end
|
162
|
+
|
163
|
+
# Set the Accept-Encoding header if none is provided
|
164
|
+
if !head.key?('accept-encoding') && req.compressed
|
165
|
+
head['accept-encoding'] = 'gzip, compressed'
|
166
|
+
end
|
167
|
+
|
168
|
+
# Set the auth from the URI if given
|
169
|
+
head['Authorization'] = @req.uri.userinfo.split(/:/, 2) if @req.uri.userinfo
|
170
|
+
|
171
|
+
head
|
172
|
+
end
|
173
|
+
|
174
|
+
def send_request(head, body)
|
175
|
+
body = normalize_body(body)
|
176
|
+
file = @req.file
|
177
|
+
query = @req.query
|
178
|
+
|
179
|
+
# Set the Content-Length if file is given
|
180
|
+
head['content-length'] = File.size(file) if file
|
181
|
+
|
182
|
+
# Set the Content-Length if body is given,
|
183
|
+
# or we're doing an empty post or put
|
184
|
+
if body
|
185
|
+
head['content-length'] ||= body.respond_to?(:bytesize) ? body.bytesize : body.size
|
186
|
+
elsif @req.method == 'POST' or @req.method == 'PUT'
|
187
|
+
# wont happen if body is set and we already set content-length above
|
188
|
+
head['content-length'] ||= 0
|
189
|
+
end
|
190
|
+
|
191
|
+
# Set content-type header if missing and body is a Ruby hash
|
192
|
+
if !head['content-type'] and @req.body.is_a? Hash
|
193
|
+
head['content-type'] = 'application/x-www-form-urlencoded'
|
194
|
+
end
|
195
|
+
|
196
|
+
request_header ||= encode_request(@req.method, @req.uri, query, @conn.connopts)
|
197
|
+
request_header << encode_headers(head)
|
198
|
+
request_header << CRLF
|
199
|
+
@conn.send_data request_header
|
200
|
+
|
201
|
+
@req_body = body || (@req.file && Pathname.new(@req.file))
|
202
|
+
send_request_body unless @req.headers['expect'] == '100-continue'
|
203
|
+
end
|
204
|
+
|
205
|
+
def on_body_data(data)
|
206
|
+
if @content_decoder
|
207
|
+
begin
|
208
|
+
@content_decoder << data
|
209
|
+
rescue HttpDecoders::DecoderError
|
210
|
+
on_error "Content-decoder error"
|
211
|
+
end
|
212
|
+
else
|
213
|
+
on_decoded_body_data(data)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def on_decoded_body_data(data)
|
218
|
+
data.force_encoding @content_charset if @content_charset
|
219
|
+
if @stream
|
220
|
+
@stream.call(data)
|
221
|
+
else
|
222
|
+
@response << data
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def request_body_pending?
|
227
|
+
!!@req_body
|
228
|
+
end
|
229
|
+
|
230
|
+
def send_request_body
|
231
|
+
return if @req_body.nil?
|
232
|
+
|
233
|
+
if @req_body.is_a?(String)
|
234
|
+
@conn.send_data @req_body
|
235
|
+
|
236
|
+
elsif @req_body.is_a?(Pathname)
|
237
|
+
@conn.stream_file_data @req_body.to_path, http_chunks: false
|
238
|
+
|
239
|
+
elsif @req_body.respond_to?(:read) && @req_body.respond_to?(:eof?) # IO or IO-like object
|
240
|
+
@conn.stream_data @req_body
|
241
|
+
|
242
|
+
else
|
243
|
+
raise "Don't know how to send request body: #{@req_body.inspect}"
|
244
|
+
end
|
245
|
+
@req_body = nil
|
246
|
+
end
|
247
|
+
|
248
|
+
def parse_response_header(header, version, status)
|
249
|
+
@response_header.raw = header
|
250
|
+
header.each do |key, val|
|
251
|
+
@response_header[key.upcase.gsub('-','_')] = val
|
252
|
+
end
|
253
|
+
|
254
|
+
@response_header.http_version = version.join('.')
|
255
|
+
@response_header.http_status = status
|
256
|
+
@response_header.http_reason = CODE[status] || 'unknown'
|
257
|
+
|
258
|
+
# invoke headers callback after full parse
|
259
|
+
# if one is specified by the user
|
260
|
+
@headers.call(@response_header) if @headers
|
261
|
+
|
262
|
+
unless @response_header.http_status and @response_header.http_reason
|
263
|
+
@state = :invalid
|
264
|
+
on_error "no HTTP response"
|
265
|
+
return
|
266
|
+
end
|
267
|
+
|
268
|
+
# add set-cookie's to cookie list
|
269
|
+
if @response_header.cookie && @req.pass_cookies
|
270
|
+
[@response_header.cookie].flatten.each {|cookie| @cookiejar.set(cookie, @req.uri)}
|
271
|
+
end
|
272
|
+
|
273
|
+
# correct location header - some servers will incorrectly give a relative URI
|
274
|
+
if @response_header.location
|
275
|
+
begin
|
276
|
+
location = Addressable::URI.parse(@response_header.location)
|
277
|
+
location.path = "/" if location.path.empty?
|
278
|
+
|
279
|
+
if location.relative?
|
280
|
+
location = @req.uri.join(location)
|
281
|
+
else
|
282
|
+
# if redirect is to an absolute url, check for correct URI structure
|
283
|
+
raise if location.host.nil?
|
284
|
+
end
|
285
|
+
|
286
|
+
@response_header[LOCATION] = location.to_s
|
287
|
+
|
288
|
+
rescue
|
289
|
+
on_error "Location header format error"
|
290
|
+
return
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# Fire callbacks immediately after recieving header requests
|
295
|
+
# if the request method is HEAD. In case of a redirect, terminate
|
296
|
+
# current connection and reinitialize the process.
|
297
|
+
if @req.method == "HEAD"
|
298
|
+
@state = :finished
|
299
|
+
return
|
300
|
+
end
|
301
|
+
|
302
|
+
if @response_header.chunked_encoding?
|
303
|
+
@state = :chunk_header
|
304
|
+
elsif @response_header.content_length
|
305
|
+
@state = :body
|
306
|
+
else
|
307
|
+
@state = :body
|
308
|
+
end
|
309
|
+
|
310
|
+
if @req.decoding && decoder_class = HttpDecoders.decoder_for_encoding(response_header[CONTENT_ENCODING])
|
311
|
+
begin
|
312
|
+
@content_decoder = decoder_class.new do |s| on_decoded_body_data(s) end
|
313
|
+
rescue HttpDecoders::DecoderError
|
314
|
+
on_error "Content-decoder error"
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
# handle malformed header - Content-Type repetitions.
|
319
|
+
content_type = [response_header[CONTENT_TYPE]].flatten.first
|
320
|
+
|
321
|
+
if String.method_defined?(:force_encoding) && /;\s*charset=\s*(.+?)\s*(;|$)/.match(content_type)
|
322
|
+
@content_charset = Encoding.find($1.gsub(/^\"|\"$/, '')) rescue Encoding.default_external
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
class CookieJar
|
327
|
+
def initialize
|
328
|
+
@jar = ::CookieJar::Jar.new
|
329
|
+
end
|
330
|
+
|
331
|
+
def set string, uri
|
332
|
+
@jar.set_cookie(uri, string) rescue nil # drop invalid cookies
|
333
|
+
end
|
334
|
+
|
335
|
+
def get uri
|
336
|
+
uri = URI.parse(uri) rescue nil
|
337
|
+
uri ? @jar.get_cookies(uri) : []
|
338
|
+
end
|
339
|
+
end # CookieJar
|
340
|
+
end
|
341
|
+
end
|