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