ably-em-http-request 1.1.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.github/workflows/ci.yml +22 -0
  4. data/.gitignore +9 -0
  5. data/.rspec +0 -0
  6. data/Changelog.md +78 -0
  7. data/Gemfile +14 -0
  8. data/LICENSE +21 -0
  9. data/README.md +66 -0
  10. data/Rakefile +10 -0
  11. data/ably-em-http-request.gemspec +33 -0
  12. data/benchmarks/clients.rb +170 -0
  13. data/benchmarks/em-excon.rb +87 -0
  14. data/benchmarks/em-profile.gif +0 -0
  15. data/benchmarks/em-profile.txt +65 -0
  16. data/benchmarks/server.rb +48 -0
  17. data/examples/.gitignore +1 -0
  18. data/examples/digest_auth/client.rb +25 -0
  19. data/examples/digest_auth/server.rb +28 -0
  20. data/examples/fetch.rb +30 -0
  21. data/examples/fibered-http.rb +51 -0
  22. data/examples/multi.rb +25 -0
  23. data/examples/oauth-tweet.rb +35 -0
  24. data/examples/socks5.rb +23 -0
  25. data/lib/em/io_streamer.rb +51 -0
  26. data/lib/em-http/client.rb +343 -0
  27. data/lib/em-http/core_ext/bytesize.rb +6 -0
  28. data/lib/em-http/decoders.rb +252 -0
  29. data/lib/em-http/http_client_options.rb +51 -0
  30. data/lib/em-http/http_connection.rb +408 -0
  31. data/lib/em-http/http_connection_options.rb +72 -0
  32. data/lib/em-http/http_encoding.rb +151 -0
  33. data/lib/em-http/http_header.rb +85 -0
  34. data/lib/em-http/http_status_codes.rb +59 -0
  35. data/lib/em-http/middleware/digest_auth.rb +114 -0
  36. data/lib/em-http/middleware/json_response.rb +17 -0
  37. data/lib/em-http/middleware/oauth.rb +42 -0
  38. data/lib/em-http/middleware/oauth2.rb +30 -0
  39. data/lib/em-http/multi.rb +59 -0
  40. data/lib/em-http/request.rb +25 -0
  41. data/lib/em-http/version.rb +7 -0
  42. data/lib/em-http-request.rb +1 -0
  43. data/lib/em-http.rb +20 -0
  44. data/spec/client_fiber_spec.rb +23 -0
  45. data/spec/client_spec.rb +1000 -0
  46. data/spec/digest_auth_spec.rb +48 -0
  47. data/spec/dns_spec.rb +41 -0
  48. data/spec/encoding_spec.rb +49 -0
  49. data/spec/external_spec.rb +146 -0
  50. data/spec/fixtures/google.ca +16 -0
  51. data/spec/fixtures/gzip-sample.gz +0 -0
  52. data/spec/gzip_spec.rb +91 -0
  53. data/spec/helper.rb +27 -0
  54. data/spec/http_proxy_spec.rb +268 -0
  55. data/spec/middleware/oauth2_spec.rb +15 -0
  56. data/spec/middleware_spec.rb +143 -0
  57. data/spec/multi_spec.rb +104 -0
  58. data/spec/pipelining_spec.rb +62 -0
  59. data/spec/redirect_spec.rb +430 -0
  60. data/spec/socksify_proxy_spec.rb +56 -0
  61. data/spec/spec_helper.rb +25 -0
  62. data/spec/ssl_spec.rb +67 -0
  63. data/spec/stallion.rb +334 -0
  64. data/spec/stub_server.rb +45 -0
  65. 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,7 @@
1
+ module EventMachine
2
+ module AblyHttpRequest
3
+ class HttpRequest
4
+ VERSION = "1.1.8"
5
+ end
6
+ end
7
+ 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'