z-http-request 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gemtest +0 -0
  3. data/.gitignore +10 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +8 -0
  7. data/Gemfile +17 -0
  8. data/README.md +38 -0
  9. data/Rakefile +3 -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/examples/.gitignore +1 -0
  16. data/examples/digest_auth/client.rb +25 -0
  17. data/examples/digest_auth/server.rb +28 -0
  18. data/examples/fetch.rb +30 -0
  19. data/examples/fibered-http.rb +51 -0
  20. data/examples/multi.rb +25 -0
  21. data/examples/oauth-tweet.rb +35 -0
  22. data/examples/socks5.rb +23 -0
  23. data/lib/z-http/client.rb +318 -0
  24. data/lib/z-http/core_ext/bytesize.rb +6 -0
  25. data/lib/z-http/decoders.rb +254 -0
  26. data/lib/z-http/http_client_options.rb +51 -0
  27. data/lib/z-http/http_connection.rb +214 -0
  28. data/lib/z-http/http_connection_options.rb +44 -0
  29. data/lib/z-http/http_encoding.rb +142 -0
  30. data/lib/z-http/http_header.rb +83 -0
  31. data/lib/z-http/http_status_codes.rb +57 -0
  32. data/lib/z-http/middleware/digest_auth.rb +112 -0
  33. data/lib/z-http/middleware/json_response.rb +15 -0
  34. data/lib/z-http/middleware/oauth.rb +40 -0
  35. data/lib/z-http/middleware/oauth2.rb +28 -0
  36. data/lib/z-http/multi.rb +57 -0
  37. data/lib/z-http/request.rb +23 -0
  38. data/lib/z-http/version.rb +5 -0
  39. data/lib/z-http-request.rb +1 -0
  40. data/lib/z-http.rb +18 -0
  41. data/spec/client_spec.rb +892 -0
  42. data/spec/digest_auth_spec.rb +48 -0
  43. data/spec/dns_spec.rb +44 -0
  44. data/spec/encoding_spec.rb +49 -0
  45. data/spec/external_spec.rb +150 -0
  46. data/spec/fixtures/google.ca +16 -0
  47. data/spec/fixtures/gzip-sample.gz +0 -0
  48. data/spec/gzip_spec.rb +68 -0
  49. data/spec/helper.rb +30 -0
  50. data/spec/middleware_spec.rb +143 -0
  51. data/spec/multi_spec.rb +104 -0
  52. data/spec/pipelining_spec.rb +66 -0
  53. data/spec/redirect_spec.rb +321 -0
  54. data/spec/socksify_proxy_spec.rb +60 -0
  55. data/spec/spec_helper.rb +6 -0
  56. data/spec/ssl_spec.rb +20 -0
  57. data/spec/stallion.rb +296 -0
  58. data/spec/stub_server.rb +42 -0
  59. data/z-http-request.gemspec +33 -0
  60. metadata +248 -0
@@ -0,0 +1,214 @@
1
+ module ZMachine
2
+
3
+ module HTTPMethods
4
+ def get options = {}, &blk; setup_request(:get, options, &blk); end
5
+ def head options = {}, &blk; setup_request(:head, options, &blk); end
6
+ def delete options = {}, &blk; setup_request(:delete, options, &blk); end
7
+ def put options = {}, &blk; setup_request(:put, options, &blk); end
8
+ def post options = {}, &blk; setup_request(:post, options, &blk); end
9
+ def patch options = {}, &blk; setup_request(:patch, options, &blk); end
10
+ def options options = {}, &blk; setup_request(:options, options, &blk); end
11
+ end
12
+
13
+ class HttpStubConnection < Connection
14
+ include Deferrable
15
+ attr_reader :parent
16
+
17
+ def parent=(p)
18
+ @parent = p
19
+ @parent.conn = self
20
+ end
21
+
22
+ def receive_data(data)
23
+ @parent.receive_data data
24
+ end
25
+
26
+ def connection_completed
27
+ @parent.connection_completed
28
+ end
29
+
30
+ def unbind(reason=nil)
31
+ @parent.unbind(reason)
32
+ end
33
+ end
34
+
35
+ class HttpConnection
36
+ include HTTPMethods
37
+
38
+ attr_reader :deferred
39
+ attr_accessor :error, :connopts, :uri, :conn
40
+
41
+ def initialize
42
+ @deferred = true
43
+ @middleware = []
44
+ end
45
+
46
+ def conn=(c)
47
+ @conn = c
48
+ @deferred = false
49
+ end
50
+
51
+ def activate_connection(client)
52
+ begin
53
+ ZMachine.connect(@connopts.host, @connopts.port, HttpStubConnection) do |conn|
54
+ post_init
55
+
56
+ @deferred = false
57
+ @conn = conn
58
+
59
+ conn.parent = self
60
+ conn.pending_connect_timeout = @connopts.connect_timeout
61
+ conn.comm_inactivity_timeout = @connopts.inactivity_timeout
62
+ end
63
+
64
+ finalize_request(client)
65
+ rescue ZMachine::ConnectionError => e
66
+ #
67
+ # Currently, this can only fire on initial connection setup
68
+ # since #connect is a synchronous method. Hence, rescue the exception,
69
+ # and return a failed deferred which fail any client request at next
70
+ # tick. We fail at next tick to keep a consistent API when the newly
71
+ # created HttpClient is failed. This approach has the advantage to
72
+ # remove a state check of @deferred_status after creating a new
73
+ # HttpRequest. The drawback is that users may setup a callback which we
74
+ # know won't be used.
75
+ #
76
+ # Once there is async-DNS, then we'll iterate over the outstanding
77
+ # client requests and fail them in order.
78
+ #
79
+ # Net outcome: failed connection will invoke the same ConnectionError
80
+ # message on the connection deferred, and on the client deferred.
81
+ #
82
+ ZMachine.next_tick{client.close(e.message)}
83
+ end
84
+ end
85
+
86
+ def setup_request(method, options = {}, c = nil)
87
+ c ||= HttpClient.new(self, HttpClientOptions.new(@uri, options, method))
88
+ @deferred ? activate_connection(c) : finalize_request(c)
89
+ c
90
+ end
91
+
92
+ def finalize_request(c)
93
+ @conn.callback { c.connection_completed }
94
+
95
+ middleware.each do |m|
96
+ c.callback(&m.method(:response)) if m.respond_to?(:response)
97
+ end
98
+
99
+ @clients.push c
100
+ end
101
+
102
+ def middleware
103
+ [HttpRequest.middleware, @middleware].flatten
104
+ end
105
+
106
+ def post_init
107
+ @clients = []
108
+ @pending = []
109
+
110
+ @p = Http::Parser.new
111
+ @p.header_value_type = :mixed
112
+ @p.on_headers_complete = proc do |h|
113
+ client.parse_response_header(h, @p.http_version, @p.status_code)
114
+ :reset if client.req.no_body?
115
+ end
116
+
117
+ @p.on_body = proc do |b|
118
+ client.on_body_data(b)
119
+ end
120
+
121
+ @p.on_message_complete = proc do
122
+ if !client.continue?
123
+ c = @clients.shift
124
+ c.state = :finished
125
+ c.on_request_complete
126
+ end
127
+ end
128
+ end
129
+
130
+ def use(klass, *args, &block)
131
+ @middleware << klass.new(*args, &block)
132
+ end
133
+
134
+ def peer
135
+ Socket.unpack_sockaddr_in(@peer)[1] rescue nil
136
+ end
137
+
138
+ def receive_data(data)
139
+ begin
140
+ @p << data
141
+ rescue HTTP::Parser::Error => e
142
+ c = @clients.shift
143
+ c.nil? ? unbind(e.message) : c.on_error(e.message)
144
+ end
145
+ end
146
+
147
+ def connection_completed
148
+ @peer = @conn.get_peername
149
+
150
+ if @connopts.socks_proxy?
151
+ raise NotImplementedError
152
+ elsif @connopts.connect_proxy?
153
+ raise NotImplementedError
154
+ else
155
+ start
156
+ end
157
+ end
158
+
159
+ def start
160
+ @conn.start_tls(@connopts.tls) if client && client.req.ssl?
161
+ @conn.succeed
162
+ end
163
+
164
+ def redirect(client)
165
+ @pending.push client
166
+ end
167
+
168
+ def unbind(reason = nil)
169
+ #reason ||= Errno::ETIMEDOUT if @conn.channel.timedout?
170
+ @clients.map { |c| c.unbind(reason) }
171
+
172
+ if r = @pending.shift
173
+ @clients.push r
174
+
175
+ r.reset!
176
+ @p.reset!
177
+
178
+ begin
179
+ @conn.set_deferred_status :unknown
180
+
181
+ if @connopts.proxy
182
+ @conn.reconnect(@connopts.host, @connopts.port)
183
+ else
184
+ @conn.reconnect(r.req.host, r.req.port)
185
+ end
186
+
187
+ @conn.pending_connect_timeout = @connopts.connect_timeout
188
+ @conn.comm_inactivity_timeout = @connopts.inactivity_timeout
189
+ @conn.callback { r.connection_completed }
190
+ rescue ZMachine::ConnectionError => e
191
+ @clients.pop.close(e.message)
192
+ end
193
+ else
194
+ @deferred = true
195
+ @conn.close_connection
196
+ end
197
+ end
198
+ alias :close :unbind
199
+
200
+ def send_data(data)
201
+ @conn.send_data data
202
+ end
203
+
204
+ def stream_file_data(filename, args = {})
205
+ @conn.stream_file_data filename, args
206
+ end
207
+
208
+ private
209
+
210
+ def client
211
+ @clients.first
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,44 @@
1
+ class HttpConnectionOptions
2
+ attr_reader :host, :port, :tls, :proxy, :bind, :bind_port
3
+ attr_reader :connect_timeout, :inactivity_timeout
4
+
5
+ def initialize(uri, options)
6
+ @connect_timeout = options[:connect_timeout] || 5 # default connection setup timeout
7
+ @inactivity_timeout = options[:inactivity_timeout] ||= 10 # default connection inactivity (post-setup) timeout
8
+
9
+ @tls = options[:tls] || options[:ssl] || {}
10
+ @proxy = options[:proxy]
11
+
12
+ if bind = options[:bind]
13
+ @bind = bind[:host] || '0.0.0.0'
14
+
15
+ # ZMachine 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
+
24
+ if proxy = options[:proxy]
25
+ @host = proxy[:host]
26
+ @port = proxy[:port]
27
+ else
28
+ @host = uri.host
29
+ @port = uri.port
30
+ end
31
+ end
32
+
33
+ def http_proxy?
34
+ @proxy && (@proxy[:type] == :http || @proxy[:type].nil?) && !@https
35
+ end
36
+
37
+ def connect_proxy?
38
+ @proxy && (@proxy[:type] == :http || @proxy[:type].nil?) && @https
39
+ end
40
+
41
+ def socks_proxy?
42
+ @proxy && (@proxy[:type] == :socks5)
43
+ end
44
+ end
@@ -0,0 +1,142 @@
1
+ module ZMachine
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 == 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, proxy)
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
+ query = uri.join(query) if proxy
55
+
56
+ HTTP_REQUEST_HEADER % [method.to_s.upcase, query]
57
+ end
58
+
59
+ def encode_query(uri, query)
60
+ encoded_query = if query.kind_of?(Hash)
61
+ query.map { |k, v| encode_param(k, v) }.join('&')
62
+ else
63
+ query.to_s
64
+ end
65
+
66
+ if uri && !uri.query.to_s.empty?
67
+ encoded_query = [encoded_query, uri.query].reject {|part| part.empty?}.join("&")
68
+ end
69
+ encoded_query.to_s.empty? ? uri.path : "#{uri.path}?#{encoded_query}"
70
+ end
71
+
72
+ # URL encodes query parameters:
73
+ # single k=v, or a URL encoded array, if v is an array of values
74
+ def encode_param(k, v)
75
+ if v.is_a?(Array)
76
+ v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
77
+ else
78
+ escape(k) + "=" + escape(v)
79
+ end
80
+ end
81
+
82
+ def form_encode_body(obj)
83
+ pairs = []
84
+ recursive = Proc.new do |h, prefix|
85
+ h.each do |k,v|
86
+ key = prefix == '' ? escape(k) : "#{prefix}[#{escape(k)}]"
87
+
88
+ if v.is_a? Array
89
+ nh = Hash.new
90
+ v.size.times { |t| nh[t] = v[t] }
91
+ recursive.call(nh, key)
92
+
93
+ elsif v.is_a? Hash
94
+ recursive.call(v, key)
95
+ else
96
+ pairs << "#{key}=#{escape(v)}"
97
+ end
98
+ end
99
+ end
100
+
101
+ recursive.call(obj, '')
102
+ return pairs.join('&')
103
+ end
104
+
105
+ # Encode a field in an HTTP header
106
+ def encode_field(k, v)
107
+ FIELD_ENCODING % [k, v]
108
+ end
109
+
110
+ # Encode basic auth in an HTTP header
111
+ # In: Array ([user, pass]) - for basic auth
112
+ # String - custom auth string (OAuth, etc)
113
+ def encode_auth(k,v)
114
+ if v.is_a? Array
115
+ FIELD_ENCODING % [k, ["Basic", Base64.encode64(v.join(":")).split.join].join(" ")]
116
+ else
117
+ encode_field(k,v)
118
+ end
119
+ end
120
+
121
+ def encode_headers(head)
122
+ head.inject('') do |result, (key, value)|
123
+ # Munge keys from foo-bar-baz to Foo-Bar-Baz
124
+ key = key.split('-').map { |k| k.to_s.capitalize }.join('-')
125
+ result << case key
126
+ when 'Authorization', 'Proxy-Authorization'
127
+ encode_auth(key, value)
128
+ else
129
+ encode_field(key, value)
130
+ end
131
+ end
132
+ end
133
+
134
+ def encode_cookie(cookie)
135
+ if cookie.is_a? Hash
136
+ cookie.inject('') { |result, (k, v)| result << encode_param(k, v) + ";" }
137
+ else
138
+ cookie
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,83 @@
1
+ module ZMachine
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 ("OK","File not found",etc.)
6
+ attr_accessor :http_reason
7
+
8
+ # The HTTP version returned.
9
+ attr_accessor :http_version
10
+
11
+ # The status code (as a string!)
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 as an integer
27
+ def status
28
+ @status ||= 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 ZMachine
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 ZMachine
2
+ module Middleware
3
+ require 'digest'
4
+ require 'securerandom'
5
+
6
+ class DigestAuth
7
+ include ZMachine::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
@@ -0,0 +1,15 @@
1
+ require 'multi_json'
2
+
3
+ module ZMachine
4
+ module Middleware
5
+ class JSONResponse
6
+ def response(resp)
7
+ begin
8
+ body = MultiJson.load(resp.response)
9
+ resp.response = body
10
+ rescue Exception => e
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,40 @@
1
+ require 'simple_oauth'
2
+
3
+ module ZMachine
4
+ module Middleware
5
+
6
+ class OAuth
7
+ include HttpEncoding
8
+
9
+ def initialize(opts = {})
10
+ @opts = opts.dup
11
+ # Allow both `oauth` gem and `simple_oauth` gem opts formats
12
+ @opts[:token] ||= @opts.delete(:access_token)
13
+ @opts[:token_secret] ||= @opts.delete(:access_token_secret)
14
+ end
15
+
16
+ def request(client, head, body)
17
+ request = client.req
18
+ uri = request.uri.join(encode_query(request.uri, request.query))
19
+ params = {}
20
+
21
+ # from https://github.com/oauth/oauth-ruby/blob/master/lib/oauth/request_proxy/em_http_request.rb
22
+ if ["POST", "PUT"].include?(request.method)
23
+ head["content-type"] ||= "application/x-www-form-urlencoded" if body.is_a? Hash
24
+ form_encoded = head["content-type"].to_s.downcase.start_with?("application/x-www-form-urlencoded")
25
+
26
+ if form_encoded
27
+ CGI.parse(client.normalize_body(body)).each do |k,v|
28
+ # Since `CGI.parse` always returns values as an array
29
+ params[k] = v.size == 1 ? v.first : v
30
+ end
31
+ end
32
+ end
33
+
34
+ head["Authorization"] = SimpleOAuth::Header.new(request.method, uri, params, @opts)
35
+
36
+ [head,body]
37
+ end
38
+ end
39
+ end
40
+ end