ably-em-http-request 1.1.8
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/.github/workflows/ci.yml +22 -0
- data/.gitignore +9 -0
- data/.rspec +0 -0
- data/Changelog.md +78 -0
- data/Gemfile +14 -0
- data/LICENSE +21 -0
- data/README.md +66 -0
- data/Rakefile +10 -0
- data/ably-em-http-request.gemspec +33 -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/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/io_streamer.rb +51 -0
- data/lib/em-http/client.rb +343 -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 +51 -0
- data/lib/em-http/http_connection.rb +408 -0
- data/lib/em-http/http_connection_options.rb +72 -0
- data/lib/em-http/http_encoding.rb +151 -0
- data/lib/em-http/http_header.rb +85 -0
- data/lib/em-http/http_status_codes.rb +59 -0
- data/lib/em-http/middleware/digest_auth.rb +114 -0
- data/lib/em-http/middleware/json_response.rb +17 -0
- data/lib/em-http/middleware/oauth.rb +42 -0
- data/lib/em-http/middleware/oauth2.rb +30 -0
- data/lib/em-http/multi.rb +59 -0
- data/lib/em-http/request.rb +25 -0
- data/lib/em-http/version.rb +7 -0
- data/lib/em-http-request.rb +1 -0
- data/lib/em-http.rb +20 -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 +146 -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 +27 -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 +62 -0
- data/spec/redirect_spec.rb +430 -0
- data/spec/socksify_proxy_spec.rb +56 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/ssl_spec.rb +67 -0
- data/spec/stallion.rb +334 -0
- data/spec/stub_server.rb +45 -0
- metadata +269 -0
@@ -0,0 +1,72 @@
|
|
1
|
+
module AblyHttpRequest
|
2
|
+
class HttpConnectionOptions
|
3
|
+
attr_reader :host, :port, :tls, :proxy, :bind, :bind_port
|
4
|
+
attr_reader :connect_timeout, :inactivity_timeout
|
5
|
+
attr_writer :https
|
6
|
+
|
7
|
+
def initialize(uri, options)
|
8
|
+
@connect_timeout = options[:connect_timeout] || 5 # default connection setup timeout
|
9
|
+
@inactivity_timeout = options[:inactivity_timeout] ||= 10 # default connection inactivity (post-setup) timeout
|
10
|
+
|
11
|
+
@tls = options[:tls] || options[:ssl] || {}
|
12
|
+
|
13
|
+
if bind = options[:bind]
|
14
|
+
@bind = bind[:host] || '0.0.0.0'
|
15
|
+
|
16
|
+
# Eventmachine will open a UNIX socket if bind :port
|
17
|
+
# is explicitly set to nil
|
18
|
+
@bind_port = bind[:port]
|
19
|
+
end
|
20
|
+
|
21
|
+
uri = uri.kind_of?(Addressable::URI) ? uri : Addressable::URI::parse(uri.to_s)
|
22
|
+
@https = uri.scheme == "https"
|
23
|
+
uri.port ||= (@https ? 443 : 80)
|
24
|
+
@tls[:sni_hostname] = uri.hostname
|
25
|
+
|
26
|
+
@proxy = options[:proxy] || proxy_from_env
|
27
|
+
|
28
|
+
if proxy
|
29
|
+
@host = proxy[:host]
|
30
|
+
@port = proxy[:port]
|
31
|
+
else
|
32
|
+
@host = uri.hostname
|
33
|
+
@port = uri.port
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def http_proxy?
|
38
|
+
@proxy && (@proxy[:type] == :http || @proxy[:type].nil?) && !@https
|
39
|
+
end
|
40
|
+
|
41
|
+
def connect_proxy?
|
42
|
+
@proxy && (@proxy[:type] == :http || @proxy[:type].nil?) && @https
|
43
|
+
end
|
44
|
+
|
45
|
+
def socks_proxy?
|
46
|
+
@proxy && (@proxy[:type] == :socks5)
|
47
|
+
end
|
48
|
+
|
49
|
+
def proxy_from_env
|
50
|
+
# TODO: Add support for $http_no_proxy or $no_proxy ?
|
51
|
+
proxy_str = if @https
|
52
|
+
ENV['HTTPS_PROXY'] || ENV['https_proxy']
|
53
|
+
else
|
54
|
+
ENV['HTTP_PROXY'] || ENV['http_proxy']
|
55
|
+
|
56
|
+
# Fall-back to $ALL_PROXY if none of the above env-vars have values
|
57
|
+
end || ENV['ALL_PROXY']
|
58
|
+
|
59
|
+
# Addressable::URI::parse will return `nil` if given `nil` and an empty URL for an empty string
|
60
|
+
# so, let's short-circuit that:
|
61
|
+
return if !proxy_str || proxy_str.empty?
|
62
|
+
|
63
|
+
proxy_env_uri = Addressable::URI::parse(proxy_str)
|
64
|
+
{ :host => proxy_env_uri.host, :port => proxy_env_uri.port, :type => :http }
|
65
|
+
|
66
|
+
rescue Addressable::URI::InvalidURIError
|
67
|
+
# An invalid env-var shouldn't crash the config step, IMHO.
|
68
|
+
# We should somehow log / warn about this, perhaps...
|
69
|
+
return
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module AblyHttpRequest
|
3
|
+
module HttpEncoding
|
4
|
+
HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
|
5
|
+
FIELD_ENCODING = "%s: %s\r\n"
|
6
|
+
|
7
|
+
def escape(s)
|
8
|
+
if defined?(EscapeUtils)
|
9
|
+
EscapeUtils.escape_url(s.to_s)
|
10
|
+
else
|
11
|
+
s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/) {
|
12
|
+
'%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def unescape(s)
|
18
|
+
if defined?(EscapeUtils)
|
19
|
+
EscapeUtils.unescape_url(s.to_s)
|
20
|
+
else
|
21
|
+
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/) {
|
22
|
+
[$1.delete('%')].pack('H*')
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
if ''.respond_to?(:bytesize)
|
28
|
+
def bytesize(string)
|
29
|
+
string.bytesize
|
30
|
+
end
|
31
|
+
else
|
32
|
+
def bytesize(string)
|
33
|
+
string.size
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Map all header keys to a downcased string version
|
38
|
+
def munge_header_keys(head)
|
39
|
+
head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
|
40
|
+
end
|
41
|
+
|
42
|
+
def encode_host
|
43
|
+
if @req.uri.port.nil? || @req.uri.port == 80 || @req.uri.port == 443
|
44
|
+
return @req.uri.host
|
45
|
+
else
|
46
|
+
@req.uri.host + ":#{@req.uri.port}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def encode_request(method, uri, query, connopts)
|
51
|
+
query = encode_query(uri, query)
|
52
|
+
|
53
|
+
# Non CONNECT proxies require that you provide the full request
|
54
|
+
# uri in request header, as opposed to a relative path.
|
55
|
+
# Don't modify the header with CONNECT proxies. It's unneeded and will
|
56
|
+
# cause 400 Bad Request errors with many standard setups.
|
57
|
+
if connopts.proxy && !connopts.connect_proxy?
|
58
|
+
query = uri.join(query)
|
59
|
+
# Drop the userinfo, it's been converted to a header and won't be
|
60
|
+
# accepted by the proxy
|
61
|
+
query.userinfo = nil
|
62
|
+
end
|
63
|
+
|
64
|
+
HTTP_REQUEST_HEADER % [method.to_s.upcase, query]
|
65
|
+
end
|
66
|
+
|
67
|
+
def encode_query(uri, query)
|
68
|
+
encoded_query = if query.kind_of?(Hash)
|
69
|
+
query.map { |k, v| encode_param(k, v) }.join('&')
|
70
|
+
else
|
71
|
+
query.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
if uri && !uri.query.to_s.empty?
|
75
|
+
encoded_query = [encoded_query, uri.query].reject {|part| part.empty?}.join("&")
|
76
|
+
end
|
77
|
+
encoded_query.to_s.empty? ? uri.path : "#{uri.path}?#{encoded_query}"
|
78
|
+
end
|
79
|
+
|
80
|
+
# URL encodes query parameters:
|
81
|
+
# single k=v, or a URL encoded array, if v is an array of values
|
82
|
+
def encode_param(k, v)
|
83
|
+
if v.is_a?(Array)
|
84
|
+
v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
|
85
|
+
else
|
86
|
+
escape(k) + "=" + escape(v)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def form_encode_body(obj)
|
91
|
+
pairs = []
|
92
|
+
recursive = Proc.new do |h, prefix|
|
93
|
+
h.each do |k,v|
|
94
|
+
key = prefix == '' ? escape(k) : "#{prefix}[#{escape(k)}]"
|
95
|
+
|
96
|
+
if v.is_a? Array
|
97
|
+
nh = Hash.new
|
98
|
+
v.size.times { |t| nh[t] = v[t] }
|
99
|
+
recursive.call(nh, key)
|
100
|
+
|
101
|
+
elsif v.is_a? Hash
|
102
|
+
recursive.call(v, key)
|
103
|
+
else
|
104
|
+
pairs << "#{key}=#{escape(v)}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
recursive.call(obj, '')
|
110
|
+
return pairs.join('&')
|
111
|
+
end
|
112
|
+
|
113
|
+
# Encode a field in an HTTP header
|
114
|
+
def encode_field(k, v)
|
115
|
+
FIELD_ENCODING % [k, v]
|
116
|
+
end
|
117
|
+
|
118
|
+
# Encode basic auth in an HTTP header
|
119
|
+
# In: Array ([user, pass]) - for basic auth
|
120
|
+
# String - custom auth string (OAuth, etc)
|
121
|
+
def encode_auth(k,v)
|
122
|
+
if v.is_a? Array
|
123
|
+
FIELD_ENCODING % [k, ["Basic", Base64.strict_encode64(v.join(":")).split.join].join(" ")]
|
124
|
+
else
|
125
|
+
encode_field(k,v)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def encode_headers(head)
|
130
|
+
head.inject('') do |result, (key, value)|
|
131
|
+
# Munge keys from foo-bar-baz to Foo-Bar-Baz
|
132
|
+
key = key.split('-').map { |k| k.to_s.capitalize }.join('-')
|
133
|
+
result << case key
|
134
|
+
when 'Authorization', 'Proxy-Authorization'
|
135
|
+
encode_auth(key, value)
|
136
|
+
else
|
137
|
+
encode_field(key, value)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def encode_cookie(cookie)
|
143
|
+
if cookie.is_a? Hash
|
144
|
+
cookie.inject('') { |result, (k, v)| result << encode_param(k, v) + ";" }
|
145
|
+
else
|
146
|
+
cookie
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module AblyHttpRequest
|
3
|
+
# A simple hash is returned for each request made by HttpClient with the
|
4
|
+
# headers that were given by the server for that request.
|
5
|
+
class HttpResponseHeader < Hash
|
6
|
+
# The reason returned in the http response (string - e.g. "OK")
|
7
|
+
attr_accessor :http_reason
|
8
|
+
|
9
|
+
# The HTTP version returned (string - e.g. "1.1")
|
10
|
+
attr_accessor :http_version
|
11
|
+
|
12
|
+
# The status code (integer - e.g. 200)
|
13
|
+
attr_accessor :http_status
|
14
|
+
|
15
|
+
# Raw headers
|
16
|
+
attr_accessor :raw
|
17
|
+
|
18
|
+
# E-Tag
|
19
|
+
def etag
|
20
|
+
self[HttpClient::ETAG]
|
21
|
+
end
|
22
|
+
|
23
|
+
def last_modified
|
24
|
+
self[HttpClient::LAST_MODIFIED]
|
25
|
+
end
|
26
|
+
|
27
|
+
# HTTP response status
|
28
|
+
def status
|
29
|
+
Integer(http_status) rescue 0
|
30
|
+
end
|
31
|
+
|
32
|
+
# Length of content as an integer, or nil if chunked/unspecified
|
33
|
+
def content_length
|
34
|
+
@content_length ||= ((s = self[HttpClient::CONTENT_LENGTH]) &&
|
35
|
+
(s =~ /^(\d+)$/)) ? $1.to_i : nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# Cookie header from the server
|
39
|
+
def cookie
|
40
|
+
self[HttpClient::SET_COOKIE]
|
41
|
+
end
|
42
|
+
|
43
|
+
# Is the transfer encoding chunked?
|
44
|
+
def chunked_encoding?
|
45
|
+
/chunked/i === self[HttpClient::TRANSFER_ENCODING]
|
46
|
+
end
|
47
|
+
|
48
|
+
def keepalive?
|
49
|
+
/keep-alive/i === self[HttpClient::KEEP_ALIVE]
|
50
|
+
end
|
51
|
+
|
52
|
+
def compressed?
|
53
|
+
/gzip|compressed|deflate/i === self[HttpClient::CONTENT_ENCODING]
|
54
|
+
end
|
55
|
+
|
56
|
+
def location
|
57
|
+
self[HttpClient::LOCATION]
|
58
|
+
end
|
59
|
+
|
60
|
+
def [](key)
|
61
|
+
super(key) || super(key.upcase.gsub('-','_'))
|
62
|
+
end
|
63
|
+
|
64
|
+
def informational?
|
65
|
+
100 <= status && 200 > status
|
66
|
+
end
|
67
|
+
|
68
|
+
def successful?
|
69
|
+
200 <= status && 300 > status
|
70
|
+
end
|
71
|
+
|
72
|
+
def redirection?
|
73
|
+
300 <= status && 400 > status
|
74
|
+
end
|
75
|
+
|
76
|
+
def client_error?
|
77
|
+
400 <= status && 500 > status
|
78
|
+
end
|
79
|
+
|
80
|
+
def server_error?
|
81
|
+
500 <= status && 600 > status
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module AblyHttpRequest
|
3
|
+
module HttpStatus
|
4
|
+
CODE = {
|
5
|
+
100 => 'Continue',
|
6
|
+
101 => 'Switching Protocols',
|
7
|
+
102 => 'Processing',
|
8
|
+
200 => 'OK',
|
9
|
+
201 => 'Created',
|
10
|
+
202 => 'Accepted',
|
11
|
+
203 => 'Non-Authoritative Information',
|
12
|
+
204 => 'No Content',
|
13
|
+
205 => 'Reset Content',
|
14
|
+
206 => 'Partial Content',
|
15
|
+
207 => 'Multi-Status',
|
16
|
+
226 => 'IM Used',
|
17
|
+
300 => 'Multiple Choices',
|
18
|
+
301 => 'Moved Permanently',
|
19
|
+
302 => 'Found',
|
20
|
+
303 => 'See Other',
|
21
|
+
304 => 'Not Modified',
|
22
|
+
305 => 'Use Proxy',
|
23
|
+
306 => 'Reserved',
|
24
|
+
307 => 'Temporary Redirect',
|
25
|
+
400 => 'Bad Request',
|
26
|
+
401 => 'Unauthorized',
|
27
|
+
402 => 'Payment Required',
|
28
|
+
403 => 'Forbidden',
|
29
|
+
404 => 'Not Found',
|
30
|
+
405 => 'Method Not Allowed',
|
31
|
+
406 => 'Not Acceptable',
|
32
|
+
407 => 'Proxy Authentication Required',
|
33
|
+
408 => 'Request Timeout',
|
34
|
+
409 => 'Conflict',
|
35
|
+
410 => 'Gone',
|
36
|
+
411 => 'Length Required',
|
37
|
+
412 => 'Precondition Failed',
|
38
|
+
413 => 'Request Entity Too Large',
|
39
|
+
414 => 'Request-URI Too Long',
|
40
|
+
415 => 'Unsupported Media Type',
|
41
|
+
416 => 'Requested Range Not Satisfiable',
|
42
|
+
417 => 'Expectation Failed',
|
43
|
+
422 => 'Unprocessable Entity',
|
44
|
+
423 => 'Locked',
|
45
|
+
424 => 'Failed Dependency',
|
46
|
+
426 => 'Upgrade Required',
|
47
|
+
500 => 'Internal Server Error',
|
48
|
+
501 => 'Not Implemented',
|
49
|
+
502 => 'Bad Gateway',
|
50
|
+
503 => 'Service Unavailable',
|
51
|
+
504 => 'Gateway Timeout',
|
52
|
+
505 => 'HTTP Version Not Supported',
|
53
|
+
506 => 'Variant Also Negotiates',
|
54
|
+
507 => 'Insufficient Storage',
|
55
|
+
510 => 'Not Extended'
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module AblyHttpRequest
|
3
|
+
module Middleware
|
4
|
+
require 'digest'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
class DigestAuth
|
8
|
+
include EventMachine::AblyHttpRequest::HttpEncoding
|
9
|
+
|
10
|
+
attr_accessor :auth_digest, :is_digest_auth
|
11
|
+
|
12
|
+
def initialize(www_authenticate, opts = {})
|
13
|
+
@nonce_count = -1
|
14
|
+
@opts = opts
|
15
|
+
@digest_params = {
|
16
|
+
algorithm: 'MD5' # MD5 is the default hashing algorithm
|
17
|
+
}
|
18
|
+
if (@is_digest_auth = www_authenticate =~ /^Digest/)
|
19
|
+
get_params(www_authenticate)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def request(client, head, body)
|
24
|
+
# Allow HTTP basic auth fallback
|
25
|
+
if @is_digest_auth
|
26
|
+
head['Authorization'] = build_auth_digest(client.req.method, client.req.uri.path, @opts.merge(@digest_params))
|
27
|
+
else
|
28
|
+
head['Authorization'] = [@opts[:username], @opts[:password]]
|
29
|
+
end
|
30
|
+
[head, body]
|
31
|
+
end
|
32
|
+
|
33
|
+
def response(resp)
|
34
|
+
# If the server responds with the Authentication-Info header, set the nonce to the new value
|
35
|
+
if @is_digest_auth && (authentication_info = resp.response_header['Authentication-Info'])
|
36
|
+
authentication_info =~ /nextnonce="?(.*?)"?(,|\z)/
|
37
|
+
@digest_params[:nonce] = $1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_auth_digest(method, uri, params = nil)
|
42
|
+
params = @opts.merge(@digest_params) if !params
|
43
|
+
nonce_count = next_nonce
|
44
|
+
|
45
|
+
user = unescape params[:username]
|
46
|
+
password = unescape params[:password]
|
47
|
+
|
48
|
+
splitted_algorithm = params[:algorithm].split('-')
|
49
|
+
sess = "-sess" if splitted_algorithm[1]
|
50
|
+
raw_algorithm = splitted_algorithm[0]
|
51
|
+
if %w(MD5 SHA1 SHA2 SHA256 SHA384 SHA512 RMD160).include? raw_algorithm
|
52
|
+
algorithm = eval("Digest::#{raw_algorithm}")
|
53
|
+
else
|
54
|
+
raise "Unknown algorithm: #{raw_algorithm}"
|
55
|
+
end
|
56
|
+
qop = params[:qop]
|
57
|
+
cnonce = make_cnonce if qop or sess
|
58
|
+
a1 = if sess
|
59
|
+
[
|
60
|
+
algorithm.hexdigest("#{params[:username]}:#{params[:realm]}:#{params[:password]}"),
|
61
|
+
params[:nonce],
|
62
|
+
cnonce,
|
63
|
+
].join ':'
|
64
|
+
else
|
65
|
+
"#{params[:username]}:#{params[:realm]}:#{params[:password]}"
|
66
|
+
end
|
67
|
+
ha1 = algorithm.hexdigest a1
|
68
|
+
ha2 = algorithm.hexdigest "#{method}:#{uri}"
|
69
|
+
|
70
|
+
request_digest = [ha1, params[:nonce]]
|
71
|
+
request_digest.push(('%08x' % @nonce_count), cnonce, qop) if qop
|
72
|
+
request_digest << ha2
|
73
|
+
request_digest = request_digest.join ':'
|
74
|
+
header = [
|
75
|
+
"Digest username=\"#{params[:username]}\"",
|
76
|
+
"realm=\"#{params[:realm]}\"",
|
77
|
+
"algorithm=#{raw_algorithm}#{sess}",
|
78
|
+
"uri=\"#{uri}\"",
|
79
|
+
"nonce=\"#{params[:nonce]}\"",
|
80
|
+
"response=\"#{algorithm.hexdigest(request_digest)[0, 32]}\"",
|
81
|
+
]
|
82
|
+
if params[:qop]
|
83
|
+
header << "qop=#{qop}"
|
84
|
+
header << "nc=#{'%08x' % @nonce_count}"
|
85
|
+
header << "cnonce=\"#{cnonce}\""
|
86
|
+
end
|
87
|
+
header << "opaque=\"#{params[:opaque]}\"" if params.key? :opaque
|
88
|
+
header.join(', ')
|
89
|
+
end
|
90
|
+
|
91
|
+
# Process the WWW_AUTHENTICATE header to get the authentication parameters
|
92
|
+
def get_params(www_authenticate)
|
93
|
+
www_authenticate.scan(/(\w+)="?(.*?)"?(,|\z)/).each do |match|
|
94
|
+
@digest_params[match[0].to_sym] = match[1]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Generate a client nonce
|
99
|
+
def make_cnonce
|
100
|
+
Digest::MD5.hexdigest [
|
101
|
+
Time.now.to_i,
|
102
|
+
$$,
|
103
|
+
SecureRandom.random_number(2**32),
|
104
|
+
].join ':'
|
105
|
+
end
|
106
|
+
|
107
|
+
# Keep track of the nounce count
|
108
|
+
def next_nonce
|
109
|
+
@nonce_count += 1
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module AblyHttpRequest
|
5
|
+
module Middleware
|
6
|
+
class JSONResponse
|
7
|
+
def response(resp)
|
8
|
+
begin
|
9
|
+
body = MultiJson.load(resp.response)
|
10
|
+
resp.response = body
|
11
|
+
rescue => e
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'simple_oauth'
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module AblyHttpRequest
|
5
|
+
module Middleware
|
6
|
+
|
7
|
+
class OAuth
|
8
|
+
include HttpEncoding
|
9
|
+
|
10
|
+
def initialize(opts = {})
|
11
|
+
@opts = opts.dup
|
12
|
+
# Allow both `oauth` gem and `simple_oauth` gem opts formats
|
13
|
+
@opts[:token] ||= @opts.delete(:access_token)
|
14
|
+
@opts[:token_secret] ||= @opts.delete(:access_token_secret)
|
15
|
+
end
|
16
|
+
|
17
|
+
def request(client, head, body)
|
18
|
+
request = client.req
|
19
|
+
uri = request.uri.join(encode_query(request.uri, request.query))
|
20
|
+
params = {}
|
21
|
+
|
22
|
+
# from https://github.com/oauth/oauth-ruby/blob/master/lib/oauth/request_proxy/em_http_request.rb
|
23
|
+
if ["POST", "PUT"].include?(request.method)
|
24
|
+
head["content-type"] ||= "application/x-www-form-urlencoded" if body.is_a? Hash
|
25
|
+
form_encoded = head["content-type"].to_s.downcase.start_with?("application/x-www-form-urlencoded")
|
26
|
+
|
27
|
+
if form_encoded
|
28
|
+
CGI.parse(client.normalize_body(body)).each do |k,v|
|
29
|
+
# Since `CGI.parse` always returns values as an array
|
30
|
+
params[k] = v.size == 1 ? v.first : v
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
head["Authorization"] = SimpleOAuth::Header.new(request.method, uri, params, @opts)
|
36
|
+
|
37
|
+
[head,body]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module AblyHttpRequest
|
3
|
+
module Middleware
|
4
|
+
class OAuth2
|
5
|
+
include EM::AblyHttpRequest::HttpEncoding
|
6
|
+
attr_accessor :access_token
|
7
|
+
|
8
|
+
def initialize(opts={})
|
9
|
+
self.access_token = opts[:access_token] or raise "No :access_token provided"
|
10
|
+
end
|
11
|
+
|
12
|
+
def request(client, head, body)
|
13
|
+
uri = client.req.uri.dup
|
14
|
+
update_uri! uri
|
15
|
+
client.req.set_uri uri
|
16
|
+
|
17
|
+
[head, body]
|
18
|
+
end
|
19
|
+
|
20
|
+
def update_uri!(uri)
|
21
|
+
if uri.query.nil?
|
22
|
+
uri.query = encode_param(:access_token, access_token)
|
23
|
+
else
|
24
|
+
uri.query += "&#{encode_param(:access_token, access_token)}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module AblyHttpRequest
|
3
|
+
|
4
|
+
# EventMachine based Multi request client, based on a streaming HTTPRequest class,
|
5
|
+
# which allows you to open multiple parallel connections and return only when all
|
6
|
+
# of them finish. (i.e. ideal for parallelizing workloads)
|
7
|
+
#
|
8
|
+
# == Example
|
9
|
+
#
|
10
|
+
# EventMachine.run {
|
11
|
+
#
|
12
|
+
# multi = EventMachine::AblyHttpRequest::MultiRequest.new
|
13
|
+
#
|
14
|
+
# # add multiple requests to the multi-handler
|
15
|
+
# multi.add(:a, EventMachine::AblyHttpRequest::HttpRequest.new('http://www.google.com/').get)
|
16
|
+
# multi.add(:b, EventMachine::AblyHttpRequest::HttpRequest.new('http://www.yahoo.com/').get)
|
17
|
+
#
|
18
|
+
# multi.callback {
|
19
|
+
# p multi.responses[:callback]
|
20
|
+
# p multi.responses[:errback]
|
21
|
+
#
|
22
|
+
# EventMachine.stop
|
23
|
+
# }
|
24
|
+
# }
|
25
|
+
#
|
26
|
+
|
27
|
+
class MultiRequest
|
28
|
+
include EventMachine::Deferrable
|
29
|
+
|
30
|
+
attr_reader :requests, :responses
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
@requests = {}
|
34
|
+
@responses = {:callback => {}, :errback => {}}
|
35
|
+
end
|
36
|
+
|
37
|
+
def add(name, conn)
|
38
|
+
raise 'Duplicate Multi key' if @requests.key? name
|
39
|
+
|
40
|
+
@requests[name] = conn
|
41
|
+
|
42
|
+
conn.callback { @responses[:callback][name] = conn; check_progress }
|
43
|
+
conn.errback { @responses[:errback][name] = conn; check_progress }
|
44
|
+
end
|
45
|
+
|
46
|
+
def finished?
|
47
|
+
(@responses[:callback].size + @responses[:errback].size) == @requests.size
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
# invoke callback if all requests have completed
|
53
|
+
def check_progress
|
54
|
+
succeed(self) if finished?
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module EventMachine
|
2
|
+
module AblyHttpRequest
|
3
|
+
class HttpRequest
|
4
|
+
@middleware = []
|
5
|
+
|
6
|
+
def self.new(uri, options={})
|
7
|
+
uri = uri.clone
|
8
|
+
connopt = ::AblyHttpRequest::HttpConnectionOptions.new(uri, options)
|
9
|
+
|
10
|
+
c = HttpConnection.new
|
11
|
+
c.connopts = connopt
|
12
|
+
c.uri = uri
|
13
|
+
c
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.use(klass, *args, &block)
|
17
|
+
@middleware << klass.new(*args, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.middleware
|
21
|
+
@middleware
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
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'
|