z-http-request 0.1.0

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 (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