cookbook-client 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.
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require 'rubygems'
4
+ require 'opscode/rest'
5
+
6
+ client = Opscode::REST.new
7
+
8
+ uri = 'http://localhost:5959/objects/1'
9
+ secret_filename = '/Users/tim/.ssh/secret-timh2-com-stg.txt'
10
+ user_id = 'timh2'
11
+
12
+ user_secret = OpenSSL::PKey::RSA.new(File.read(secret_filename))
13
+
14
+ res = client.request(:get,
15
+ uri,
16
+ :authenticate => true,
17
+ :user_id => user_id,
18
+ :user_secret => user_secret)
19
+ puts "res from GET is: #{res}"
20
+
@@ -0,0 +1,22 @@
1
+
2
+ #!/usr/bin/env ruby -w
3
+
4
+ # Test collectino.find
5
+ hash = {
6
+ 'something_else' => 'value',
7
+ 'another_thing' => 'hmm',
8
+ 'tarball' => File.new('/etc/passwd'),
9
+ 'yet_another' => '..',
10
+ }
11
+
12
+ Hash
13
+ res = hash.find { |k,v| v.is_a?(File) }
14
+ puts "res is #{res[1]}"
15
+
16
+
17
+ # test gsub
18
+ str = "my-testing/0_7_0"
19
+ str = str.gsub(/_/, '.')
20
+ puts "str is #{str}"
21
+
22
+ Hash
@@ -0,0 +1,20 @@
1
+ require 'net/https'
2
+ require 'uri'
3
+
4
+ url = 'https://s3.amazonaws.com/opscode-community/cookbook_versions/tarballs/151/original/teamspeak.tar.gz'
5
+ uri = URI.parse url
6
+
7
+ http_client = Net::HTTP.new(uri.host, uri.port)
8
+ if uri.scheme == 'https'
9
+ http_client.use_ssl = true
10
+ http_client.verify_mode = OpenSSL::SSL::VERIFY_NONE
11
+ end
12
+
13
+ res = http_client.request_get(uri.path)
14
+
15
+ #http = Net::HTTP.new(uri.host, uri.port)
16
+ #http.use_ssl = true if uri.scheme == 'https'
17
+ #res = http.get
18
+
19
+ puts "res #{res}"
20
+ puts "res.body is #{res.body}"
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require 'rubygems'
4
+ require 'rest_client'
5
+ require 'mixlib/authentication/signedheaderauth'
6
+
7
+ uri = 'http://com-stg.opscode.com/api/v1/cookbooks'
8
+ user_secret_filename = '/Users/tim/.ssh/secret-timh2-com-stg.txt'
9
+ user_secret = OpenSSL::PKey::RSA.new(File.read(user_secret_filename))
10
+
11
+ args = {
12
+ :file => File.new('couchdb2.tar.gz'),
13
+ :user_id => 'timh2',
14
+ :http_method => :post,
15
+ :timestamp => Time.now.utc.iso8601
16
+ }
17
+ client = Mixlib::Authentication::SignedHeaderAuth.signing_object(args)
18
+ puts "SignedHeaderAuth client is #{client}"
19
+
20
+ headers = client.sign(user_secret)
21
+ puts "headers is #{headers}"
22
+
23
+ res = RestClient.post(uri,
24
+ {:tarball => File.read('couchdb2.tar.gz'), :cookbook => '{"category":"good"}'},
25
+ #File.new('couchdb2.tar.gz'),
26
+ headers)
27
+ puts "POST res is #{res}"
28
+
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ require 'rubygems'
4
+ require 'mixlib/authentication/signatureverification'
5
+
6
+ sigv = Mixlib::Authentication::SignatureVerification.new
7
+
8
+ # for timh2:
9
+ # -----BEGIN RSA PUBLIC KEY-----
10
+ # MIIBCgKCAQEA0wqGeC4/OSMQwFCp0/6o21dUlSrbWFFFYdVC1wY/ADgpnAbnZGDO
11
+ # URrrmQ8zw2WBMGpZ2RvhaToj7SOlCUOJmM99W5Ou9E3pOFvjRWgWjW0LAe48SIWP
12
+ # t2WtdtI4YBzyS6Dp9Oxdcm5TGGJa7QMbIgaU9VD0Jr8yFDWE9QdqAsy+NBSwM13c
13
+ # CQuBqHtSoJMk9SgzwQyFSm6GCQcliNh5wupKqhNNYCdYOAeJQqJKn25aK9/4+eLu
14
+ # 8F8bVD1MznOBes089+CoY50sy1N2iZnxwftZklmGLJ1tB5C8+vNwB91Qzp9csUtr
15
+ # prTkgeW/9cqybTwTm30pj5yrW6A271pkVwIDAQAB
16
+ # -----END RSA PUBLIC KEY-----
17
+
18
+ # for timh3:
19
+ # -----BEGIN RSA PUBLIC KEY-----
20
+ # MIIBCgKCAQEA0wqGeC4/OSMQwFCp0/6o21dUlSrbWFFFYdVC1wY/ADgpnAbnZGDO
21
+ # URrrmQ8zw2WBMGpZ2RvhaToj7SOlCUOJmM99W5Ou9E3pOFvjRWgWjW0LAe48SIWP
22
+ # t2WtdtI4YBzyS6Dp9Oxdcm5TGGJa7QMbIgaU9VD0Jr8yFDWE9QdqAsy+NBSwM13c
23
+ # CQuBqHtSoJMk9SgzwQyFSm6GCQcliNh5wupKqhNNYCdYOAeJQqJKn25aK9/4+eLu
24
+ # 8F8bVD1MznOBes089+CoY50sy1N2iZnxwftZklmGLJ1tB5C8+vNwB91Qzp9csUtr
25
+ # prTkgeW/9cqybTwTm30pj5yrW6A271pkVwIDAQAB
26
+ # -----END RSA PUBLIC KEY-----
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.join(File.dirname(__FILE__), "..", "opscode-chef", "chef", "lib")
4
+
5
+ require 'rubygems'
6
+ require 'cgi'
7
+ require 'json'
8
+ require 'opscode/rest'
9
+ require 'mixlib/log'
10
+ require 'chef/log'
11
+ require 'streaming_cookbook_uploader'
12
+
13
+ Opscode::REST::Config[:log_level] = :debug
14
+
15
+ client = Opscode::REST.new
16
+
17
+ #uri = 'http://opscode-community.pivotallabs.com/api/v1/cookbooks'
18
+ #user_id = 'timh'
19
+ #secret_key_filename = '/Users/tim/.ssh/pivotallabs-secret.txt'
20
+
21
+ uri = 'http://com-stg.opscode.com/api/v1/cookbooks'
22
+ user_id = 'timh2'
23
+ secret_key_filename = '/Users/tim/.ssh/secret-timh2-com-stg.txt'
24
+
25
+ user_secret = OpenSSL::PKey::RSA.new(File.read(secret_key_filename))
26
+ filename_to_upload = 'couchdb3.tar.gz'
27
+
28
+ if 1
29
+ ########## streaming cookbook uploader
30
+ ########## results in 'invalid authenticity token' exception (pages of rails
31
+ ########## exception) from far end:
32
+ streaming_client = Chef::StreamingCookbookUploader.new
33
+ res = streaming_client.post(uri, user_id, secret_key_filename,
34
+ { 'tarball' => File.new(filename_to_upload),
35
+ 'cookbook' => '{"category":"good"}' })
36
+ puts "res is #{res}"
37
+
38
+ elsif nil
39
+ ########## nuo's alternative
40
+ ########## results in 'unauthorized' from far end
41
+ headers = {:accept=>"application/json", :content_type=>'application/json'}
42
+ options = {:authenticate=> true,
43
+ :user_secret=>user_secret,
44
+ :user_id=>user_id,
45
+ :headers=>headers,
46
+ :payload=>File.read(filename_to_upload)
47
+ }
48
+ res = client.request(:post,uri,options)
49
+ puts "res is #{res}"
50
+
51
+ elsif 1
52
+ ########## my original
53
+ ########## results in 'unauthorized' from far end
54
+ res = client.post(uri,
55
+ :payload => File.read(filename_to_upload),
56
+ :authenticate => true,
57
+ :user_id => user_id,
58
+ :user_secret => user_secret,
59
+ :headers => {
60
+ :content_type => 'application/octet-stream' })
61
+ puts "res is #{res}"
62
+ end
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.join(File.dirname(__FILE__), "..", "opscode-chef", "chef", "lib")
4
+
5
+ require 'rubygems'
6
+ require 'rest_client'
7
+ require 'mixlib/authentication/signedheaderauth'
8
+ require 'mixlib/log'
9
+ require 'chef/log'
10
+ require 'streaming_cookbook_uploader'
11
+
12
+ uri = 'http://com-stg.opscode.com/api/v1/cookbooks'
13
+ user_id = 'timh2'
14
+ user_secret_filename = '/Users/tim/.ssh/secret-timh2-com-stg.txt'
15
+ user_secret = OpenSSL::PKey::RSA.new(File.read(user_secret_filename))
16
+
17
+ # def post(to_url, user_id, secret_key_filename, params = {}, headers = {})
18
+ # make_request(:post, to_url, user_id, secret_key_filename, params, headers)
19
+ # end
20
+
21
+ client = Chef::StreamingCookbookUploader.new
22
+
23
+ res = client.post(uri, user_id, user_secret_filename, {
24
+ :tarball => File.open('couchdb2.tar.gz'),
25
+ :cookbook => '{"category":"databases"}'
26
+ })
27
+ puts "POST res is #{res}"
28
+
@@ -0,0 +1,196 @@
1
+
2
+ require 'chef/streaming_cookbook_uploader'
3
+ require 'chef/remote_cookbook'
4
+ require 'opscode/rest'
5
+ require 'cgi'
6
+
7
+ module Chef
8
+ class CookbookClient
9
+ def initialize(hostname)
10
+ @hostname = hostname
11
+ @client = Opscode::REST.new
12
+ end
13
+
14
+ def do_search(term)
15
+ uri = make_uri('api/v1/search', {'q' => term})
16
+ parse_json_cookbook_list @client.get(uri)
17
+ end
18
+
19
+ def do_list()
20
+ params = {}
21
+ params['items'] = 999999
22
+
23
+ uri = make_uri('api/v1/cookbooks', params)
24
+
25
+ parse_json_cookbook_list @client.get(uri)
26
+ end
27
+
28
+ # parse json output back from pivotallabs. assumes a hash comes back, which
29
+ # contains a key 'items', which itself is an array. that array has cookbook
30
+ # hashes in it.
31
+ # e.g. http://opscode-community.pivotallabs.com/api/v1/cookbooks/
32
+ def parse_json_cookbook_list(json_hash)
33
+ cookbooks_res = []
34
+
35
+ items = json_hash['items']
36
+ if !items.nil? && items.kind_of?(Array)
37
+ items.each do |item|
38
+ cookbook = RemoteCookbook.from_json_list(item)
39
+
40
+ cookbooks_res.push cookbook
41
+ end
42
+ else
43
+ raise Exception, "items should be an Array, but it's #{items}"
44
+ end
45
+ cookbooks_res
46
+ end
47
+
48
+ # Retrieve details and an enumeration of versions for the given cookbook.
49
+ def do_details(cookbook_name)
50
+ cookbook_uri = make_uri "api/v1/cookbooks/#{cookbook_name}"
51
+
52
+ json_hash = @client.get(cookbook_uri)
53
+
54
+ cookbook_res = RemoteCookbook.from_json_details(cookbook_uri, json_hash)
55
+ cookbook_res.versions.each do |ver|
56
+ ver_json_hash = @client.get(ver.uri)
57
+
58
+ # populate additional fields of the version object with the result
59
+ # from the versioned JSON call.
60
+ ver.set_from_json_hash @hostname, ver_json_hash
61
+ end
62
+
63
+ cookbook_res
64
+ end
65
+
66
+ # Download the tarball for a given cookbook. If the version isn't specified,
67
+ # grab the latest. Returns the file
68
+ def do_download(cookbook_name, cookbook_version = nil)
69
+ # Fetch the details of the cookbook we're about to download, so we can
70
+ # look at its versions. If there's no such cookbook, rest client will
71
+ # throw an exception.
72
+ remote_cookbook = do_details cookbook_name
73
+
74
+ # if user specified a version, look it up, otherwise use the latest.
75
+ found_version =
76
+ if cookbook_version
77
+ remote_cookbook.find_version_by_user_version(cookbook_version)
78
+ else
79
+ remote_cookbook.latest_version
80
+ end
81
+
82
+ # Download the cookbook if we were able to find a valid version.
83
+ if found_version
84
+ if found_version.tarball_uri =~ /.*\/([^\/]+)$/
85
+ out_filename = $1
86
+ end
87
+ if out_filename.nil?
88
+ raise ArgumentError, "do_download: can't figure out the filename pattern for #{found_version.tarball_uri}"
89
+ end
90
+
91
+ # TODO: should be using a streaming form, but am using the string all-at-once
92
+ # version for now.
93
+ uri = URI.parse found_version.tarball_uri
94
+ http_client = Net::HTTP.new(uri.host, uri.port)
95
+ if uri.scheme == 'https'
96
+ http_client.use_ssl = true
97
+ http_client.verify_mode = OpenSSL::SSL::VERIFY_NONE
98
+ end
99
+
100
+ http_res = http_client.request_get(uri.path)
101
+ out = File.open(out_filename, 'wb')
102
+ out.write(http_res.body)
103
+ out.close
104
+
105
+ out_filename
106
+ else
107
+ raise Exception, "No such version of cookbook '#{cookbook_name}': #{cookbook_version}"
108
+ end
109
+ end
110
+
111
+ def do_upload(cookbook_filename, cookbook_category, user_id, user_secret_filename)
112
+ cookbook_uploader = StreamingCookbookUploader.new
113
+ uri = make_uri "api/v1/cookbooks"
114
+
115
+ category_string = { 'category'=>cookbook_category }.to_json
116
+
117
+ http_resp = cookbook_uploader.post(uri, user_id, user_secret_filename, {
118
+ :tarball => File.open(cookbook_filename),
119
+ :cookbook => category_string
120
+ })
121
+
122
+ res = JSON.parse(http_resp.body)
123
+ if http_resp.code.to_i != 201
124
+ if !res['error_messages'].nil?
125
+ if res['error_messages'][0] =~ /Version already exists/
126
+ raise "Version already exists"
127
+ else
128
+ raise Exception, res
129
+ end
130
+ else
131
+ raise Exception, "Error uploading: #{res}"
132
+ end
133
+ end
134
+
135
+ #puts "do_upload: POST res is #{res}"
136
+ res
137
+ end
138
+
139
+ # Delete the given cookbook, using the given credentials
140
+ def do_delete(cookbook_name, user_id, user_secret_filename)
141
+ cookbook_to_delete = do_details(cookbook_name)
142
+ if cookbook_to_delete.nil?
143
+ raise ArgumentError, "No such cookbook to delete #{cookbook_name}"
144
+ end
145
+
146
+ user_secret_rsa = OpenSSL::PKey::RSA.new(File.read(user_secret_filename))
147
+
148
+ client_options = {
149
+ :authenticate => true,
150
+ :user_id => user_id,
151
+ :user_secret => user_secret_rsa
152
+ }
153
+
154
+ # delete the whole cookbook
155
+ @client.request(:delete, cookbook_to_delete.uri, client_options)
156
+ end
157
+
158
+ def do_deprecate(cookbook_name, replacement_cookbook_name, user_id, user_secret_filename)
159
+ cookbook_to_deprecate = do_details(cookbook_name)
160
+ if cookbook_to_deprecate.nil?
161
+ raise ArgumentError, "No such cookbook to deprecate #{cookbook_name}"
162
+ end
163
+
164
+ user_secret_rsa = OpenSSL::PKey::RSA.new(File.read(user_secret_filename))
165
+
166
+ deprecate_uri = "#{cookbook_to_deprecate.uri}/deprecation"
167
+ deprecation_json = { 'replacement_cookbook_name' => replacement_cookbook_name }.to_json
168
+
169
+ client_options = {
170
+ :authenticate => true,
171
+ :user_id => user_id,
172
+ :user_secret => user_secret_rsa,
173
+ :payload => deprecation_json,
174
+ }
175
+
176
+ # delete the whole cookbook
177
+ res = @client.post(deprecate_uri, client_options)
178
+ end
179
+
180
+ # make a url given the hostname we were set up with, the base path,
181
+ # and any parameters that were included.
182
+ def make_uri(base, params = {})
183
+ if params && !params.empty?
184
+ path_components = params.each.collect do |key, value|
185
+ key = CGI.escape(key.to_s)
186
+ value = CGI.escape(value.to_s)
187
+ "#{key}=#{value}"
188
+ end
189
+ path_components_str = '?' + path_components.join('&')
190
+ end
191
+ "http://#{@hostname}/#{base}#{path_components_str}"
192
+ end
193
+ end
194
+
195
+ end
196
+
@@ -0,0 +1,114 @@
1
+
2
+ module Chef
3
+ class RemoteCookbookVersion
4
+ attr_accessor :uri
5
+ attr_accessor :tarball_uri
6
+
7
+ def initialize(uri)
8
+ @uri = uri
9
+ end
10
+
11
+ def version
12
+ # http://www.example.com/api/v1/cookbooks/apache/versions/1_0
13
+ version = ""
14
+ if /.+\/([^\/]+)$/ =~ @uri
15
+ version = $1
16
+ end
17
+ version.gsub(/_/, '.')
18
+ end
19
+
20
+ def set_from_json_hash(hostname, json_hash)
21
+ tarball_path = json_hash['file']
22
+
23
+ if tarball_path =~ /http/
24
+ # absolute path. use it verbatim.
25
+ @tarball_uri = tarball_path
26
+ else
27
+ # relative path. use the hostname we already have and the path in 'file'
28
+ # chop off leading slash for relative paths
29
+ if tarball_path =~ /^\/(.+)$/
30
+ tarball_path = $1
31
+ end
32
+
33
+ @tarball_uri = "http://#{hostname}/#{tarball_path}"
34
+ end
35
+
36
+ #created_at = Date.parse json_hash['created_at']
37
+ #updated_at = Date.parse json_hash['updated_at']
38
+ #average_rating = json_hash['average_rating']
39
+ #license = json_hash['license']
40
+ end
41
+
42
+ end
43
+
44
+ class RemoteCookbook
45
+ attr_accessor :uri, :maintainer, :name, :description
46
+ attr_accessor :deprecated
47
+ attr_accessor :versions, :latest_version
48
+
49
+ def initialize(params = {})
50
+ @uri = params[:uri]
51
+ @maintainer = params[:maintainer]
52
+ @name = params[:name]
53
+ @description = params[:description]
54
+
55
+ @versions = Array.new
56
+ end
57
+
58
+ # these kind of responses come back from 'search' and 'list'
59
+ def self.from_json_list(json_hash)
60
+ res = new
61
+
62
+ res.uri = json_hash['cookbook']
63
+ res.maintainer = json_hash['cookbook_maintainer']
64
+ res.name = json_hash['cookbook_name']
65
+ res.description = json_hash['cookbook_description']
66
+
67
+ res
68
+ end
69
+
70
+ # this type of response comes back from directly looking up a cookbook
71
+ def self.from_json_details(uri, json_hash)
72
+ unless json_hash.kind_of?(Hash)
73
+ raise "from_json_show needs a hash"
74
+ end
75
+
76
+ res = new
77
+
78
+ res.uri = uri
79
+ res.name = json_hash['name']
80
+ res.maintainer = json_hash['maintainer']
81
+ res.description = json_hash['description']
82
+ res.deprecated = json_hash['deprecated'] == 'true'
83
+ # res.external_url = json_hash['external_url']
84
+ # res.category = json_hash['category']
85
+ # res.average_rating = json_hash['average_rating']
86
+ # res.created_at = Date.parse json_hash['created_at']
87
+ # res.updated_at = Date.parse json_hash['updated_at']
88
+
89
+ versions = json_hash['versions']
90
+ if (!versions.nil?) && (versions.kind_of? Array)
91
+ versions.each do |ver|
92
+ res.versions.push(RemoteCookbookVersion.new ver)
93
+ end
94
+ res.latest_version = res.find_version_by_uri json_hash['latest_version']
95
+ end
96
+
97
+ res
98
+ end
99
+
100
+ def find_version_by_uri(uri)
101
+ @versions.find { |ver| ver.uri == uri }
102
+ end
103
+
104
+ def find_version_by_user_version(version)
105
+ @versions.find { |ver| ver.version == version }
106
+ end
107
+
108
+ def to_s
109
+ "RemoteCookbook #{@name}: description '#{@description}', maint #{@maintainer}, uri #{@uri}"
110
+ end
111
+
112
+ end
113
+
114
+ end
@@ -0,0 +1,177 @@
1
+ require 'rubygems'
2
+ require 'net/http'
3
+ require 'mixlib/authentication/signedheaderauth'
4
+ require 'openssl'
5
+
6
+ # inspired by/cargo-culted from http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html
7
+ module Chef
8
+ class StreamingCookbookUploader
9
+ DefaultHeaders = { 'accept' => 'application/json' }
10
+
11
+ def post(to_url, user_id, secret_key_filename, params = {}, headers = {})
12
+ make_request(:post, to_url, user_id, secret_key_filename, params, headers)
13
+ end
14
+
15
+ def put(to_url, user_id, secret_key_filename, params = {}, headers = {})
16
+ make_request(:put, to_url, user_id, secret_key_filename, params, headers)
17
+ end
18
+
19
+ def make_request(http_verb, to_url, user_id, secret_key_filename, params = {}, headers = {})
20
+ boundary = '----RubyMultipartClient' + rand(1000000).to_s + 'ZZZZZ'
21
+ parts = []
22
+ content_file = nil
23
+ content_body = nil
24
+
25
+ timestamp = Time.now.utc.iso8601
26
+ secret_key = OpenSSL::PKey::RSA.new(File.read(secret_key_filename))
27
+
28
+ unless params.nil? || params.empty?
29
+ params.each do |key, value|
30
+ if value.kind_of?(File)
31
+ content_file = value
32
+ filepath = value.path
33
+ filename = File.basename(filepath)
34
+ parts << StringPart.new( "--" + boundary + "\r\n" +
35
+ "Content-Disposition: form-data; name=\"" + key.to_s + "\"; filename=\"" + filename + "\"\r\n" +
36
+ "Content-Type: application/octet-stream\r\n\r\n")
37
+ parts << StreamPart.new(value, File.size(filepath))
38
+ parts << StringPart.new("\r\n")
39
+ else
40
+ content_body = value.to_s
41
+ parts << StringPart.new( "--" + boundary + "\r\n" +
42
+ "Content-Disposition: form-data; name=\"" + key.to_s + "\"\r\n\r\n")
43
+ parts << StringPart.new(content_body + "\r\n")
44
+ end
45
+ end
46
+ parts << StringPart.new("--" + boundary + "--\r\n")
47
+ end
48
+
49
+ body_stream = MultipartStream.new(parts)
50
+
51
+ timestamp = Time.now.utc.iso8601
52
+
53
+ signing_options = {
54
+ :http_method=>http_verb,
55
+ :user_id=>user_id,
56
+ :timestamp=>timestamp
57
+ }
58
+ (content_file && signing_options[:file] = content_file) || (signing_options[:body] = (content_body || ""))
59
+
60
+ headers.merge!(Mixlib::Authentication::SignedHeaderAuth.signing_object(signing_options).sign(secret_key))
61
+
62
+ content_file.rewind if content_file
63
+
64
+ # net/http doesn't like symbols for header keys, so we'll to_s each one just in case
65
+ headers = DefaultHeaders.merge(Hash[*headers.map{ |k,v| [k.to_s, v] }.flatten])
66
+
67
+ url = URI.parse(to_url)
68
+ req = case http_verb
69
+ when :put
70
+ Net::HTTP::Put.new(url.path, headers)
71
+ when :post
72
+ Net::HTTP::Post.new(url.path, headers)
73
+ end
74
+ req.content_length = body_stream.size
75
+ req.content_type = 'multipart/form-data; boundary=' + boundary unless parts.empty?
76
+ req.body_stream = body_stream
77
+
78
+ http = Net::HTTP.new(url.host, url.port)
79
+ if url.scheme == "https"
80
+ http.use_ssl = true
81
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
82
+ end
83
+ res = http.request(req)
84
+ #res = http.start {|http_proc| http_proc.request(req) }
85
+
86
+ # alias status to code and to_s to body for test purposes
87
+ # TODO: stop the following madness!
88
+ class << res
89
+ alias :to_s :body
90
+
91
+ # BUGBUG this makes the response compatible with what respsonse_steps expects to test headers (response.headers[] -> response[])
92
+ def headers
93
+ self
94
+ end
95
+
96
+ def status
97
+ code.to_i
98
+ end
99
+ end
100
+ res
101
+ end
102
+ end
103
+
104
+ class StreamPart
105
+ def initialize(stream, size)
106
+ @stream, @size = stream, size
107
+ end
108
+
109
+ def size
110
+ @size
111
+ end
112
+
113
+ # read the specified amount from the stream
114
+ def read(offset, how_much)
115
+ @stream.read(how_much)
116
+ end
117
+ end
118
+
119
+ class StringPart
120
+ def initialize(str)
121
+ @str = str
122
+ end
123
+
124
+ def size
125
+ @str.length
126
+ end
127
+
128
+ # read the specified amount from the string startiung at the offset
129
+ def read(offset, how_much)
130
+ @str[offset, how_much]
131
+ end
132
+ end
133
+
134
+ class MultipartStream
135
+ def initialize(parts)
136
+ @parts = parts
137
+ @part_no = 0
138
+ @part_offset = 0
139
+ end
140
+
141
+ def size
142
+ @parts.inject(0) {|size, part| size + part.size}
143
+ end
144
+
145
+ def read(how_much)
146
+ return nil if @part_no >= @parts.size
147
+
148
+ how_much_current_part = @parts[@part_no].size - @part_offset
149
+
150
+ how_much_current_part = if how_much_current_part > how_much
151
+ how_much
152
+ else
153
+ how_much_current_part
154
+ end
155
+
156
+ how_much_next_part = how_much - how_much_current_part
157
+
158
+ current_part = @parts[@part_no].read(@part_offset, how_much_current_part)
159
+
160
+ # recurse into the next part if the current one was not large enough
161
+ if how_much_next_part > 0
162
+ @part_no += 1
163
+ @part_offset = 0
164
+ next_part = read(how_much_next_part)
165
+ current_part + if next_part
166
+ next_part
167
+ else
168
+ ''
169
+ end
170
+ else
171
+ @part_offset += how_much_current_part
172
+ current_part
173
+ end
174
+ end
175
+ end
176
+
177
+ end