em-http-request-samesite 1.1.7

Sign up to get free protection for your applications and to get access to all the features.
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