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.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +17 -0
- data/README.md +38 -0
- data/Rakefile +3 -0
- data/benchmarks/clients.rb +170 -0
- data/benchmarks/em-excon.rb +87 -0
- data/benchmarks/em-profile.gif +0 -0
- data/benchmarks/em-profile.txt +65 -0
- data/benchmarks/server.rb +48 -0
- data/examples/.gitignore +1 -0
- data/examples/digest_auth/client.rb +25 -0
- data/examples/digest_auth/server.rb +28 -0
- data/examples/fetch.rb +30 -0
- data/examples/fibered-http.rb +51 -0
- data/examples/multi.rb +25 -0
- data/examples/oauth-tweet.rb +35 -0
- data/examples/socks5.rb +23 -0
- data/lib/z-http/client.rb +318 -0
- data/lib/z-http/core_ext/bytesize.rb +6 -0
- data/lib/z-http/decoders.rb +254 -0
- data/lib/z-http/http_client_options.rb +51 -0
- data/lib/z-http/http_connection.rb +214 -0
- data/lib/z-http/http_connection_options.rb +44 -0
- data/lib/z-http/http_encoding.rb +142 -0
- data/lib/z-http/http_header.rb +83 -0
- data/lib/z-http/http_status_codes.rb +57 -0
- data/lib/z-http/middleware/digest_auth.rb +112 -0
- data/lib/z-http/middleware/json_response.rb +15 -0
- data/lib/z-http/middleware/oauth.rb +40 -0
- data/lib/z-http/middleware/oauth2.rb +28 -0
- data/lib/z-http/multi.rb +57 -0
- data/lib/z-http/request.rb +23 -0
- data/lib/z-http/version.rb +5 -0
- data/lib/z-http-request.rb +1 -0
- data/lib/z-http.rb +18 -0
- data/spec/client_spec.rb +892 -0
- data/spec/digest_auth_spec.rb +48 -0
- data/spec/dns_spec.rb +44 -0
- data/spec/encoding_spec.rb +49 -0
- data/spec/external_spec.rb +150 -0
- data/spec/fixtures/google.ca +16 -0
- data/spec/fixtures/gzip-sample.gz +0 -0
- data/spec/gzip_spec.rb +68 -0
- data/spec/helper.rb +30 -0
- data/spec/middleware_spec.rb +143 -0
- data/spec/multi_spec.rb +104 -0
- data/spec/pipelining_spec.rb +66 -0
- data/spec/redirect_spec.rb +321 -0
- data/spec/socksify_proxy_spec.rb +60 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/ssl_spec.rb +20 -0
- data/spec/stallion.rb +296 -0
- data/spec/stub_server.rb +42 -0
- data/z-http-request.gemspec +33 -0
- 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,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
|