hurley 0.1

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.
@@ -0,0 +1,142 @@
1
+ require "openssl"
2
+ require "securerandom"
3
+
4
+ module Hurley
5
+ class RequestOptions < Struct.new(
6
+ # Integer or Fixnum number of seconds to wait for one block to be read.
7
+ :timeout,
8
+
9
+ # Integer or Fixnum number of seconds to wait for the connection to open.
10
+ :open_timeout,
11
+
12
+ # String boundary to use for multipart request bodies.
13
+ :boundary,
14
+
15
+ # A SocketBinding specifying the host and/or port of the local client
16
+ # socket.
17
+ :bind,
18
+
19
+ # Hurley::Url instance of an HTTP Proxy address.
20
+ :proxy,
21
+
22
+ # Integer limit on the number of redirects that are automatically followed.
23
+ # Default: 5
24
+ :redirection_limit,
25
+
26
+ # Hurley::Query subclass to use for query objects. Defaults to
27
+ # Hurley::Query.default.
28
+ :query_class,
29
+ )
30
+
31
+ def redirection_limit
32
+ self[:redirection_limit] ||= 5
33
+ end
34
+
35
+ def bind=(b)
36
+ self[:bind] = SocketBinding.parse(b)
37
+ end
38
+
39
+ def build_form(body)
40
+ query_class.new(body).to_form(self)
41
+ end
42
+
43
+ def boundary
44
+ self[:boundary] || "Hurley-#{SecureRandom.hex}"
45
+ end
46
+
47
+ def query_class
48
+ self[:query_class] ||= Query.default
49
+ end
50
+ end
51
+
52
+ class SslOptions < Struct.new(
53
+ # Boolean that specifies whether to skip SSL verification.
54
+ :skip_verification,
55
+
56
+ # Sets the maximum depth for the certificate chain verification.
57
+ :verify_depth,
58
+
59
+ # String path of a CA certification file in PEM format.
60
+ :ca_file,
61
+
62
+ # String path of a CA certification directory containing certifications in PEM format.
63
+ :ca_path,
64
+
65
+ # String client cert contents.
66
+ :client_cert,
67
+
68
+ # String path to client certificate.
69
+ :client_cert_path,
70
+
71
+ # String private key contents.
72
+ :private_key,
73
+
74
+ # String path to private key.
75
+ :private_key_path,
76
+
77
+ # String pass for the private key
78
+ :private_key_pass,
79
+
80
+ # An OpenSSL::X509::Certificate object for a client certificate.
81
+ :openssl_client_cert,
82
+
83
+ # An OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
84
+ :openssl_client_key,
85
+
86
+ # The X509::Store to verify peer certificate.
87
+ :openssl_cert_store,
88
+
89
+ # Sets the SSL version. See OpenSSL::SSL::SSLContext::METHODS for available
90
+ # versions.
91
+ :version,
92
+ )
93
+
94
+ def skip_verification?
95
+ self[:skip_verification]
96
+ end
97
+
98
+ def openssl_verify_mode
99
+ skip_verification ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
100
+ end
101
+
102
+ def openssl_client_cert
103
+ self[:openssl_client_cert] ||= begin
104
+ cert_contents = self[:client_cert] || (self[:client_cert_path] && IO.read(self[:client_cert_path]))
105
+ return unless cert_contents
106
+ OpenSSL::X509::Certificate.new(cert_contents)
107
+ end
108
+ end
109
+
110
+ def openssl_cert_store
111
+ self[:openssl_cert_store] ||= OpenSSL::X509::Store.new.tap do |store|
112
+ store.set_default_paths
113
+ end
114
+ end
115
+
116
+ def openssl_private_key
117
+ @openssl_private_key ||= begin
118
+ pkey = if pkey_path = self[:private_key_path]
119
+ File.read(pkey_path)
120
+ else
121
+ self[:private_key]
122
+ end
123
+
124
+ return unless pkey
125
+
126
+ if OpenSSL::PKey.respond_to?(:read)
127
+ OpenSSL::PKey.read(pkey, self[:private_key_pass])
128
+ else
129
+ OpenSSL::PKey::RSA.new(pkey, self[:private_key_pass])
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ class SocketBinding < Struct.new(:host, :port)
136
+ def self.parse(bind)
137
+ h, p = bind.to_s.split(":", 2)
138
+ p = p.to_i
139
+ new(h, p.zero? ? nil : p)
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,252 @@
1
+ require "cgi"
2
+ require "forwardable"
3
+ require "stringio"
4
+
5
+ module Hurley
6
+ class Query
7
+ def self.default
8
+ @default ||= Nested
9
+ end
10
+
11
+ def self.parse(raw_query)
12
+ default.parse(raw_query)
13
+ end
14
+
15
+ def initialize(initial = nil)
16
+ @hash = {}
17
+ update(initial) if initial
18
+ end
19
+
20
+ extend Forwardable
21
+ def_delegators(:@hash,
22
+ :[], :[]=,
23
+ :each,
24
+ :keys,
25
+ :size,
26
+ :delete,
27
+ :key?,
28
+ )
29
+
30
+ def subset_of?(url)
31
+ query = url.respond_to?(:query) ? url.query : url
32
+ @hash.keys.all? do |key|
33
+ query[key] == @hash[key]
34
+ end
35
+ end
36
+
37
+ def update(absolute)
38
+ absolute.each do |key, value|
39
+ @hash[key] = value unless key?(key)
40
+ end
41
+ end
42
+
43
+ def parse_query(raw_query)
44
+ raw_query.to_s.split(AMP).each do |pair|
45
+ escaped_key, escaped_value = pair.split(EQ, 2)
46
+ key = CGI.unescape(escaped_key)
47
+ value = escaped_value ? CGI.unescape(escaped_value) : nil
48
+ send(:decode_pair, key, value)
49
+ end
50
+ end
51
+
52
+ def multipart?
53
+ any_multipart?(@hash.values)
54
+ end
55
+
56
+ def to_query_string
57
+ build_pairs.map!(&:to_s).join(AMP)
58
+ end
59
+
60
+ def to_form(options = nil)
61
+ if multipart?
62
+ boundary = (options || RequestOptions.new).boundary
63
+ return MULTIPART_TYPE % boundary, to_io(boundary)
64
+ else
65
+ return FORM_TYPE, StringIO.new(to_query_string)
66
+ end
67
+ end
68
+
69
+ alias to_s to_query_string
70
+
71
+ def inspect
72
+ "#<%s %s>" % [
73
+ self.class.name,
74
+ @hash.inspect,
75
+ ]
76
+ end
77
+
78
+ def self.inherited(base)
79
+ super
80
+ class << base
81
+ def parse(raw_query)
82
+ q = new
83
+ q.parse_query(raw_query)
84
+ q
85
+ end
86
+ end
87
+ end
88
+
89
+ class Nested < self
90
+ private
91
+
92
+ def decode_pair(key, value)
93
+ if key !~ END_BRACKET
94
+ self[key] = value
95
+ return
96
+ end
97
+
98
+ first_key = key[0, key.index(START_BRACKET)]
99
+ hash_keys = [first_key, *key.scan(/\[([^\]]+)?\]/).map(&:first)]
100
+ last_index = hash_keys.size - 1
101
+ container = self
102
+ hash_keys.each_with_index do |hash_key, index|
103
+ if index < last_index
104
+ if hash_keys[index+1]
105
+ container = if hash_key
106
+ container[hash_key] ||= {}
107
+ else
108
+ c = {}
109
+ container << c
110
+ container = c
111
+ end
112
+ else
113
+ container = container[hash_key] ||= []
114
+ end
115
+ else
116
+ if hash_key
117
+ container[hash_key] = value
118
+ else
119
+ container << value
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ def encode_array(pairs, key, escaped_key, value)
126
+ encode_nested_value(pairs, key, escaped_key, value)
127
+ end
128
+
129
+ def encode_hash(pairs, key, escaped_key, value)
130
+ value.each do |value_key, item|
131
+ nested_key = "#{key}[#{value_key}]"
132
+ nested_escaped_key = "#{escaped_key}%5B#{Url.escape_path(value_key)}%5D"
133
+ encode_nested_value(pairs, nested_key, nested_escaped_key, item)
134
+ end
135
+ end
136
+
137
+ def encode_nested_value(pairs, key, escaped_key, value)
138
+ case value
139
+ when Array
140
+ arr_key = "#{key}#{EMPTY_BRACKET}"
141
+ arr_escaped_key = escaped_key + EMPTY_ESCAPED_BRACKET
142
+ value.each do |item|
143
+ encode_nested_value(pairs, arr_key, arr_escaped_key, item)
144
+ end
145
+ when Hash
146
+ value.each do |hash_key, hash_value|
147
+ nested_key = "#{key}[#{hash_key}]"
148
+ nested_escaped_key = "#{escaped_key}%5B#{Url.escape_path(hash_key)}%5D"
149
+ encode_nested_value(pairs, nested_key, nested_escaped_key, hash_value)
150
+ end
151
+ else
152
+ pairs << Pair.new(key, escaped_key, value)
153
+ end
154
+ end
155
+ end
156
+
157
+ class Flat < self
158
+ private
159
+
160
+ def decode_pair(key, value)
161
+ self[key] = if key?(key)
162
+ Array(self[key]) << value
163
+ else
164
+ value
165
+ end
166
+ end
167
+
168
+ def encode_array(pairs, key, escaped_key, value)
169
+ value.each do |item|
170
+ pairs << Pair.new(key, escaped_key, item)
171
+ end
172
+ end
173
+ end
174
+
175
+ class Pair < Struct.new(:key, :escaped_key, :value)
176
+ def to_s
177
+ if value
178
+ "#{escaped_key}=#{Url.escape_path(value)}"
179
+ else
180
+ escaped_key
181
+ end
182
+ end
183
+ end
184
+
185
+ # Private Hurley::Query methods
186
+
187
+ private
188
+
189
+ def any_multipart?(array)
190
+ array.any? do |v|
191
+ case v
192
+ when Array then any_multipart?(v)
193
+ when Hash then any_multipart?(v.values)
194
+ else
195
+ v.respond_to?(:read)
196
+ end
197
+ end
198
+ end
199
+
200
+ def to_io(boundary, part_headers = nil)
201
+ parts = []
202
+
203
+ part_headers ||= {}
204
+ build_pairs.each do |pair|
205
+ parts << Multipart::Part.new(boundary, pair.key, pair.value, part_headers[pair.key])
206
+ end
207
+ parts << Multipart::EpiloguePart.new(boundary)
208
+ ios = []
209
+ len = 0
210
+ parts.each do |part|
211
+ len += part.length
212
+ ios << part.to_io
213
+ end
214
+
215
+ CompositeReadIO.new(len, *ios)
216
+ end
217
+
218
+ def build_pairs
219
+ pairs = []
220
+ @hash.each do |key, value|
221
+ escaped_key = Url.escape_path(key)
222
+ case value
223
+ when nil then pairs << Pair.new(key, escaped_key, nil)
224
+ when Array
225
+ encode_array(pairs, key, escaped_key, value)
226
+ when Hash
227
+ encode_hash(pairs, key, escaped_key, value)
228
+ else
229
+ pairs << Pair.new(key, escaped_key, value)
230
+ end
231
+ end
232
+ pairs
233
+ end
234
+
235
+ def encode_array(pairs, key, escaped_key, value)
236
+ raise NotImplementedError
237
+ end
238
+
239
+ def encode_hash(pairs, key, escaped_key, value)
240
+ raise NotImplementedError
241
+ end
242
+
243
+ AMP = "&".freeze
244
+ EQ = "=".freeze
245
+ EMPTY_BRACKET = "[]".freeze
246
+ EMPTY_ESCAPED_BRACKET = "%5B%5D".freeze
247
+ START_BRACKET = "[".freeze
248
+ END_BRACKET = /\]\z/
249
+ FORM_TYPE = "application/x-www-form-urlencoded".freeze
250
+ MULTIPART_TYPE = "multipart/form-data; boundary=%s".freeze
251
+ end
252
+ end
@@ -0,0 +1,111 @@
1
+ require "rake"
2
+
3
+ namespace :hurley do
4
+ desc "Start server for live tests. HURLEY_PORT=4000"
5
+ task :start_server do
6
+ without_verbose do
7
+ require File.expand_path("../test/server", __FILE__)
8
+ end
9
+
10
+ Hurley::Live.start_server(
11
+ :port => (ENV["HURLEY_PORT"] || 4000).to_i,
12
+ :ssl_key => ENV["HURLEY_SSL_KEY"],
13
+ :ssl_file => ENV["HURLEY_SSL_FILE"],
14
+ )
15
+ end
16
+
17
+ desc "Start proxy server for live tests. HURLEY_PORT=4001, HURLEY_PROXY_AUTH=user:pass"
18
+ task :start_proxy do
19
+ without_verbose do
20
+ require "webrick"
21
+ require "webrick/httpproxy"
22
+ end
23
+
24
+ if found = ENV["HURLEY_PROXY_AUTH"]
25
+ username, password = ENV["HURLEY_PROXY_AUTH"].split(":", 2)
26
+ end
27
+
28
+ match_credentials = lambda { |credentials|
29
+ got_username, got_password = credentials.to_s.unpack("m*")[0].split(":", 2)
30
+ got_username == username && got_password == password
31
+ }
32
+
33
+ log_io = $stdout
34
+ log_io.sync = true
35
+
36
+ webrick_opts = {
37
+ :Port => (ENV["HURLEY_PORT"] || 4001).to_i,
38
+ :Logger => WEBrick::Log::new(log_io),
39
+ :AccessLog => [[log_io, "[%{X-Hurley-Connection}i] %m %U -> %s %b"]],
40
+ :ProxyAuthProc => lambda { |req, res|
41
+ if username
42
+ type, credentials = req.header["proxy-authorization"].first.to_s.split(/\s+/, 2)
43
+ unless "Basic" == type && match_credentials.call(credentials)
44
+ res["proxy-authenticate"] = %{Basic realm="testing"}
45
+ raise WEBrick::HTTPStatus::ProxyAuthenticationRequired
46
+ end
47
+ end
48
+ }
49
+ }
50
+
51
+ proxy = WEBrick::HTTPProxyServer.new(webrick_opts)
52
+
53
+ trap(:TERM) { proxy.shutdown }
54
+ trap(:INT) { proxy.shutdown }
55
+
56
+ proxy.start
57
+ end
58
+
59
+ desc "Generate test certs for testing Hurley with SSL"
60
+ task :generate_certs do
61
+ without_verbose do
62
+ require "openssl"
63
+ require "fileutils"
64
+ end
65
+
66
+ $shell = !!ENV["IN_SHELL"]
67
+
68
+ # Adapted from WEBrick::Utils. Skips cert extensions so it
69
+ # can be used as a CA bundle
70
+ def create_self_signed_cert(bits, cn, comment)
71
+ rsa = OpenSSL::PKey::RSA.new(bits)
72
+ cert = OpenSSL::X509::Certificate.new
73
+ cert.version = 2
74
+ cert.serial = 1
75
+ name = OpenSSL::X509::Name.new(cn)
76
+ cert.subject = name
77
+ cert.issuer = name
78
+ cert.not_before = Time.now
79
+ cert.not_after = Time.now + (365*24*60*60)
80
+ cert.public_key = rsa.public_key
81
+ cert.sign(rsa, OpenSSL::Digest::SHA1.new)
82
+ return [cert, rsa]
83
+ end
84
+
85
+ def write(file, contents, env_var)
86
+ FileUtils.mkdir_p(File.dirname(file))
87
+ File.open(file, "w") do |f|
88
+ f.puts(contents)
89
+ end
90
+ puts %(export #{env_var}="#{file}") if $shell
91
+ end
92
+
93
+
94
+ # One cert / CA for ease of testing when ignoring verification
95
+ cert, key = create_self_signed_cert(1024, [["CN", "localhost"]], "Hurley Test CA")
96
+ write "tmp/hurley-cert.key", key, "HURLEY_SSL_KEY"
97
+ write "tmp/hurley-cert.crt", cert, "HURLEY_SSL_FILE"
98
+
99
+ # And a second CA to prove that verification can fail
100
+ cert, key = create_self_signed_cert(1024, [["CN", "real-ca.com"]], "A different CA")
101
+ write "tmp/hurley-different-ca-cert.key", key, "HURLEY_SSL_KEY_ALT"
102
+ write "tmp/hurley-different-ca-cert.crt", cert, "HURLEY_SSL_FILE_ALT"
103
+ end
104
+ end
105
+
106
+ def without_verbose
107
+ old_verbose, $VERBOSE = $VERBOSE, nil
108
+ yield
109
+ ensure
110
+ $VERBOSE = old_verbose
111
+ end