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.
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'