stormpath-sdk 1.0.0.beta.7 → 1.0.0.beta.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.travis.yml +8 -29
  3. data/CHANGES.md +10 -0
  4. data/README.md +64 -0
  5. data/lib/stormpath-sdk.rb +5 -1
  6. data/lib/stormpath-sdk/api_key.rb +0 -3
  7. data/lib/stormpath-sdk/auth/basic_authenticator.rb +1 -1
  8. data/lib/stormpath-sdk/auth/username_password_request.rb +1 -1
  9. data/lib/stormpath-sdk/client.rb +16 -17
  10. data/lib/stormpath-sdk/data_store.rb +4 -3
  11. data/lib/stormpath-sdk/error.rb +19 -18
  12. data/lib/stormpath-sdk/http/authc/sauthc1_signer.rb +18 -28
  13. data/lib/stormpath-sdk/http/http_client_request_executor.rb +5 -16
  14. data/lib/stormpath-sdk/http/request.rb +3 -2
  15. data/lib/stormpath-sdk/http/response.rb +3 -3
  16. data/lib/stormpath-sdk/id_site/id_site_result.rb +17 -0
  17. data/lib/stormpath-sdk/resource/application.rb +35 -1
  18. data/lib/stormpath-sdk/resource/collection.rb +1 -1
  19. data/lib/stormpath-sdk/resource/directory.rb +2 -0
  20. data/lib/stormpath-sdk/resource/group.rb +1 -1
  21. data/lib/stormpath-sdk/resource/tenant.rb +2 -0
  22. data/lib/stormpath-sdk/util/assert.rb +4 -4
  23. data/lib/stormpath-sdk/version.rb +2 -2
  24. data/spec/auth/basic_authenticator_spec.rb +1 -1
  25. data/spec/auth/sauthc1_signer_spec.rb +4 -4
  26. data/spec/client_spec.rb +1 -1
  27. data/spec/data_store_spec.rb +1 -1
  28. data/spec/provider/account_resolver_spec.rb +1 -1
  29. data/spec/resource/application_spec.rb +176 -0
  30. data/spec/resource/collection_spec.rb +4 -4
  31. data/spec/resource/directory_spec.rb +18 -0
  32. data/spec/resource/group_spec.rb +11 -0
  33. data/spec/resource/tenant_spec.rb +12 -0
  34. data/spec/spec_helper.rb +5 -1
  35. data/stormpath-sdk.gemspec +1 -0
  36. metadata +61 -81
  37. data/lib/stormpath-sdk/ext/hash.rb +0 -31
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 217ba3dfe6d6347582c6a2c428ca2c0ab357b434
4
+ data.tar.gz: 9e8e70ef785bc057c5c25240ad2a40691992d06f
5
+ SHA512:
6
+ metadata.gz: d4d3e8df8df4fc6812d96dc0eb55e776f149d5400c038d5187067d0e87362277b1441cb933505f8996323a4a0e6ac8b3777ea7d5701c278ee9c24531783ba7f5
7
+ data.tar.gz: 2c1431cf95b22b5ab3ee0b8bd8881a0edefcc6c446f3674e97b0a624752a6896a7314fa2e4c43c6b7d477178317f3c1951aac55b483599cdfbb927d28f50db6c
@@ -1,35 +1,14 @@
1
- ---
2
- env:
3
- global:
4
- - secure: |-
5
- USqTzWnRXTxgwF6wpwT3UHx4hoo5QIWP6bmmJMOIG/jLWTV3cCZ4y3LEuGx/
6
- 2x4gTyRuAAbVpJD8n9k/h7Vh6xZ8ts715kRL2OCqD+yOEioKG9RTWWzFCsNY
7
- V3wic2+25BHgyCcnXtNFTjP/TfIKjVQETQjn3Ast2foThmyLVYo=
8
- - secure: |-
9
- UDZPetAAzPMtlosw481STlt60UFEei28DlBrHzDS7j/MKgkum9ZECPm4h7Dr
10
- MXsDBp3GghLqGWc5+yNvlV2RSSZdI6YCeTSG7dXu3Tb1vTLd8Ia7pVZg3Ut2
11
- 5Ru3+xBVHZ/4xrc+8bfL2FGpH8T1mT6GZt9BfkY8v4a+rsn8FLA=
12
- - secure: |-
13
- FRAT9Sjpv2MUmIu9/uaXAa2LRKmsR0vHz3Vc8oCiDIKihqFq4YF9DpHtQ0nF
14
- OYwdcprSH5RzV/bFsf6XcPP4IlIhSRj333FJoHXfmon5cdPcpt70O4in+4YI
15
- gXPNGgbzCpZ4WRsFbmtVazQwpXBUg2fYK8y0jNrhZoVZ9UK61Zc=
16
- - secure: |-
17
- WvFXv/1RKdnh/tJFRlCDMKpiNre0+K3qWLf9PaQzjvWyTAVtq3NK7Av3JSoR
18
- EoPy/0VCzlBJPfcPa+EmLO518trgsAJ1dmBOSUmNhQOyl9bfOFlzvjqZ20Ru
19
- eMksU4t3HbbBdh3LqR9LPxViJGfUk3LvaiuWwp8y0GB5jMjrASg=
20
- - secure: |-
21
- HggUi1F44G7uHy3PyKcBE54JpBMIjrmROvzrKkCPOmRP5DoeZQ+wU+8zJ89z
22
- 4YsxOzQjYLwStB4U95LEZgQYvcbdfjkCY4uthb8XbmSeT7KSkMifhNT619+D
23
- sTekVOlY9Ei+8O/BUr4ZCE+Fs8/8obFOxooWxxdHlaYsjj/atm4=
24
- - secure: |-
25
- cJ2ExNmmGSjBHIgjZ6zs+AUEVBSfyTj2qG9SNV0j/P9mnjg32irU/tG7f1yI
26
- yvaGCsPr2h6rwtiMb8QHA4tMqHsoay0e9s7jyBtn6amgUaLsV2vdhQTWDuIU
27
- cksOE3PMIovYl9ANLb6KrhDWt7ue/fOxEALh0a5rAu50C/tNvgo=
28
-
29
1
  language: ruby
30
2
  rvm:
31
3
  - 1.9.3
32
4
  - 2.0.0
33
5
  - 2.1.2
34
6
  services:
35
- - redis-server
7
+ - redis-server
8
+ env:
9
+ global:
10
+ - secure: cfX/Vagk7SskUp1cnuTTfQHNWz6pjx7bHJOa25ahk/3Jvsqz37qf8MLNs8uRUb/LN63G/YNUtrcRZQiyDS9WHehQy0p7IO68UuOG4zqkJE0iemR0kToCTHMbyLTOB70nhTuwmRfh1KeT5Ja3D9TOtApQErU5IQaKm5jP8CZb4PA=
11
+ - secure: Vhb4qkFc0BcRkJaPU6ZbM9vPHaEHoz/A1LlQ1IMrEdXS/nUabd4l32q6zjJZvfHJ5bI1PXgk1BFztVGT65UT7Uu17KKawzLZWDLmrgzRESkKDj/Ld9Wg79DY6ooJ/juWBrWuJtoNOFKy6CqLdCKWIzNkd9zG+au3qoFP8LfhRh4=
12
+ - secure: dKmkzv90Fn1JQpSc2l+cESLFYw3bnN5TsgDKTc2tFFi285EJuqd2z22wfDGcaYAoqX/ck+YTdPqVH6trrC7b3pSMqJSBEaHtWiYMbEkmf/QWrVAHy4LEKFAcs0oACjwVRe7CGBu8RRBWcl8I1STpYm11S8WDe8Y/GHo0HnXipHo=
13
+ - secure: FejMHUo69w52zjPBMfUThhhgj7md/dj+7gf+1d0sgDuDfZ+gECEyHF8iWvkm64k3j+PX4E/y44HdV1+qIMWTGNA0xSZNFEHBBJrEC1BRKm9BkT8W0KBcupmLRHW0BtLWMrqtKOgOyDEdoufuLx7wDpTNslPeGJ4NUZIGEDTuwLc=
14
+ - secure: DyEw82o0ozA9rbbgYUk8jiC/KNwB0x0OuVv/TX7Xso3uXgfTL1m3IWYLDX6kvV4QIvHNaNdj1g02ddnRwZwuvA3IH81J8+mkZ6aIXTS1YKZPEZMSqewZNOGQPRVaUOeQ9ymQtE7bfxWkViF2JzJlGzc3IxYUKHeo+NE/Hgehuz4=
data/CHANGES.md CHANGED
@@ -1,6 +1,16 @@
1
1
  stormpath-sdk-ruby Changelog
2
2
  ============================
3
3
 
4
+ Version 1.0.0.beta.8
5
+ --------------------
6
+
7
+ Released on July 28, 2015
8
+
9
+ - Added support for Id Site.
10
+ - Added custom data support for tenant, application and directory resources.
11
+ - Added the size property to the collection resources.
12
+
13
+
4
14
  Version 1.0.0.beta.7
5
15
  --------------------
6
16
 
data/README.md CHANGED
@@ -275,6 +275,70 @@ expansion.add_property 'groups', offset: 5, limit: 10
275
275
  client.accounts.get account.href, expansion
276
276
  ```
277
277
 
278
+ ### ID Site
279
+
280
+ ID Site is a set of hosted and pre-built user interface screens that easily add authentication to your application. ID Site can be accessed via your own custom domain like id.mydomain.com and shared across multiple applications to create centralized authentication if needed. To use ID Site an url needs to be generated which contains JWT token as a parameter.
281
+
282
+ #### ID Site Login
283
+
284
+ In order to use ID Site an url needs to be generated. You also need to redirect to the generated url. You can call create_id_site_url which is on application object. For example if you are using sinatra the code would look something like this:
285
+
286
+ ```ruby
287
+ get ‘login’ do
288
+ redirect application.create_id_site_url callback_uri: “#{callback_uri}”
289
+ end
290
+ ```
291
+
292
+ The application will be an instance of your application. callback_uri is a url with which you want to handle the ID Site information, this url also needs to be set in the Stormpath’s dashboard on [ID Site settings page](https://api.stormpath.com/ui2/index.html#/id-site) as Authorized Redirect URLs.
293
+
294
+ #### Handle ID Site Callback
295
+
296
+ For any request you make for ID Site, you need to specify a callback uri. To parse the information from the servers response and to decode the data from the JWT token you need to call the handle_id_site_callback method and pass the Request URI.
297
+
298
+ For example in your sinatra app this would look something like this:
299
+
300
+ ```ruby
301
+ app.get ‘/callback' do
302
+ user_data = application.handle_id_site_callback(request.url)
303
+ end
304
+ ```
305
+
306
+ > NOTE:
307
+ > A JWT Response Token can only be used once. This is to prevent replay attacks. It will also only be valid for a total of 60 seconds. After which time, You will need to restart the workflow you were in.
308
+
309
+ #### Other ID Site Options
310
+
311
+ There are a few other methods that you will need to concern yourself with when using ID Site. Logging out a User, Registering a User, and a User who has forgotten their password. These methods will use the same information from the login method but a few more items will need to be passed into the array. For example if you have a sinatra application.
312
+
313
+ Logging Out a User
314
+ ```ruby
315
+ app.get ‘/logout' do
316
+ user_data = application.handle_id_site_callback(request.url)
317
+ redirect application.create_id_site_url callback_uri: “#{callback_uri}”, logout: true
318
+ end
319
+ ```
320
+
321
+ Registering a User
322
+ ```ruby
323
+ app.get ‘/register' do
324
+ user_data = application.handle_id_site_callback(request.url)
325
+ redirect application.create_id_site_url callback_uri: “#{callback_uri}”, path: ‘/#/register'
326
+ end
327
+ ```
328
+
329
+ Forgot Link
330
+ ```ruby
331
+ app.get ‘/forgot' do
332
+ user_data = application.handle_id_site_callback(request.url)
333
+ redirect application.create_id_site_url callback_uri: “#{callback_uri}”, path: ‘/#/forgot'
334
+ end
335
+ ```
336
+
337
+ Again, with all these methods, You will want your application to link to an internal page where the JWT is created at that time. Without doing this, a user will only have 60 seconds to click on the link before the JWT expires.
338
+
339
+ > NOTE:
340
+ > A JWT will expire after 60 seconds of creation.
341
+
278
342
  ### Registering Accounts
279
343
 
280
344
  Accounts are created on a directory instance. They can be created in two
@@ -5,6 +5,7 @@ require "openssl"
5
5
  require "open-uri"
6
6
  require "uri"
7
7
  require "uuidtools"
8
+ require "jwt"
8
9
  require "yaml"
9
10
  require 'active_support'
10
11
  require "active_support/core_ext"
@@ -15,7 +16,6 @@ require 'active_support/core_ext/array/wrap'
15
16
  require "stormpath-sdk/version" unless defined? Stormpath::VERSION
16
17
 
17
18
  require "stormpath-sdk/util/assert"
18
- require "stormpath-sdk/ext/hash"
19
19
 
20
20
  module Stormpath
21
21
  autoload :Error, 'stormpath-sdk/error'
@@ -91,4 +91,8 @@ module Stormpath
91
91
  autoload :Sauthc1Signer, "stormpath-sdk/http/authc/sauthc1_signer"
92
92
  end
93
93
  end
94
+
95
+ module IdSite
96
+ autoload :IdSiteResult, 'stormpath-sdk/id_site/id_site_result'
97
+ end
94
98
  end
@@ -14,9 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
  module Stormpath
17
-
18
17
  class ApiKey
19
-
20
18
  attr_accessor :id, :secret
21
19
 
22
20
  def initialize(id, secret)
@@ -24,6 +22,5 @@ module Stormpath
24
22
  @secret = secret
25
23
  end
26
24
  end
27
-
28
25
  end
29
26
 
@@ -27,7 +27,7 @@ module Stormpath
27
27
  assert_kind_of UsernamePasswordRequest, request, "Only UsernamePasswordRequest instances are supported."
28
28
 
29
29
  username = request.principals
30
- username = (username != nil) ? username : ''
30
+ username = username || ''
31
31
 
32
32
  password = request.credentials
33
33
  pw_string = password.join
@@ -21,7 +21,7 @@ module Stormpath
21
21
 
22
22
  def initialize username, password, options = {}
23
23
  @username = username
24
- @password = (password != nil and password.length > 0) ? password.chars.to_a : "".chars.to_a
24
+ @password = (password || "").chars.to_a
25
25
  @host = options[:host]
26
26
  @account_store = options[:account_store]
27
27
  end
@@ -23,26 +23,16 @@ module Stormpath
23
23
  attr_reader :data_store, :application
24
24
 
25
25
  def initialize(options)
26
- api_key = options[:api_key]
27
26
  base_url = options[:base_url]
28
27
  cache_opts = options[:cache] || {}
29
28
 
30
- api_key = if api_key
31
- case api_key
32
- when ApiKey then api_key
33
- when Hash then ApiKey.new api_key[:id], api_key[:secret]
34
- end
35
- elsif options[:api_key_file_location]
36
- load_api_key_file options[:api_key_file_location],
37
- options[:api_key_id_property_name],
38
- options[:api_key_secret_property_name]
39
- end
29
+ api_key = ApiKey(options)
40
30
 
41
31
  assert_not_nil api_key, "No API key has been provided. Please pass an 'api_key' or " +
42
32
  "'api_key_file_location' to the Stormpath::Client constructor."
43
33
 
44
- request_executor = Stormpath::Http::HttpClientRequestExecutor.new(api_key, proxy: options[:proxy])
45
- @data_store = Stormpath::DataStore.new(request_executor, cache_opts, self, base_url)
34
+ request_executor = Stormpath::Http::HttpClientRequestExecutor.new(proxy: options[:proxy])
35
+ @data_store = Stormpath::DataStore.new(request_executor, api_key, cache_opts, self, base_url)
46
36
  end
47
37
 
48
38
  def tenant(expansion = nil)
@@ -53,10 +43,6 @@ module Stormpath
53
43
  self
54
44
  end
55
45
 
56
- def cache_stats
57
- @data_source.cache_stats
58
- end
59
-
60
46
  has_many :tenants, href: '/tenants', can: :get
61
47
  has_many :applications, href: '/applications', can: [:get, :create], delegate: true
62
48
  has_many :directories, href: '/directories', can: [:get, :create], delegate: true
@@ -73,6 +59,19 @@ module Stormpath
73
59
 
74
60
  private
75
61
 
62
+ def ApiKey(options={})
63
+ if api_key = options[:api_key]
64
+ case api_key
65
+ when ApiKey then api_key
66
+ when Hash then ApiKey.new api_key[:id], api_key[:secret]
67
+ end
68
+ elsif options[:api_key_file_location]
69
+ load_api_key_file(options[:api_key_file_location],
70
+ options[:api_key_id_property_name],
71
+ options[:api_key_secret_property_name])
72
+ end
73
+ end
74
+
76
75
  def load_api_key_file api_key_file_location, id_property_name, secret_property_name
77
76
  begin
78
77
  api_key_properties = JavaProperties::Properties.new api_key_file_location
@@ -24,14 +24,15 @@ class Stormpath::DataStore
24
24
 
25
25
  CACHE_REGIONS = %w(applications directories accounts groups groupMemberships accountMemberships tenants customData provider providerData)
26
26
 
27
- attr_reader :client, :request_executor, :cache_manager
27
+ attr_reader :client, :request_executor, :cache_manager, :api_key, :base_url
28
28
 
29
- def initialize(request_executor, cache_opts, client, base_url = nil)
29
+ def initialize(request_executor, api_key, cache_opts, client, base_url = nil)
30
30
  assert_not_nil request_executor, "RequestExecutor cannot be null."
31
31
 
32
32
  @client = client
33
33
  @base_url = base_url || DEFAULT_BASE_URL
34
34
  @request_executor = request_executor
35
+ @api_key = api_key
35
36
  initialize_cache cache_opts
36
37
  end
37
38
 
@@ -118,7 +119,7 @@ class Stormpath::DataStore
118
119
  MultiJson.dump(to_hash(resource))
119
120
  end
120
121
 
121
- request = Request.new(http_method, href, query, Hash.new, body)
122
+ request = Request.new(http_method, href, query, Hash.new, body, @api_key)
122
123
  apply_default_request_headers request
123
124
  response = @request_executor.execute_request request
124
125
 
@@ -13,27 +13,28 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
- class Stormpath::Error < RuntimeError
16
+ module Stormpath
17
+ class Error < RuntimeError
17
18
 
18
- def initialize error = nil
19
- super !error.nil? ? error.message : ''
20
- @error = error
21
- end
19
+ attr_reader :status, :code, :developer_message, :more_info
22
20
 
23
- def status
24
- !@error.nil? ? @error.status : -1
25
- end
21
+ def initialize error = NilError.new
22
+ super error.message
23
+ @status = error.status
24
+ @code = error.code
25
+ @developer_message = error.developer_message
26
+ @more_info = error.more_info
27
+ end
26
28
 
27
- def code
28
- !@error.nil? ? @error.code : -1
29
- end
29
+ private
30
30
 
31
- def developer_message
32
- !@error.nil? ? @error.developer_message : nil
33
- end
31
+ class NilError
32
+ def message; '' end
33
+ def status; -1 end
34
+ def code; -1 end
35
+ def developer_message; end
36
+ def more_info; end
37
+ end
34
38
 
35
- def more_info
36
- !@error.nil? ? @error.more_info : nil
37
39
  end
38
-
39
- end
40
+ end
@@ -40,7 +40,7 @@ module Stormpath
40
40
  @uuid_generator = uuid_generator
41
41
  end
42
42
 
43
- def sign_request request, api_key
43
+ def sign_request request
44
44
  request.http_headers.delete(Sauthc1Signer::AUTHORIZATION_HEADER)
45
45
  request.http_headers.delete(Sauthc1Signer::STORMPATH_DATE_HEADER)
46
46
 
@@ -56,7 +56,7 @@ module Stormpath
56
56
  # have to have it in the request by the time we sign.
57
57
  host_header = uri.host
58
58
 
59
- if !default_port?(uri)
59
+ unless default_port?(uri)
60
60
  host_header << ":" << uri.port.to_s
61
61
  end
62
62
 
@@ -71,24 +71,21 @@ module Stormpath
71
71
  signed_headers_string = get_signed_headers request
72
72
  request_payload_hash_hex = to_hex(hash_text(get_request_payload(request)))
73
73
 
74
- canonical_request = method + NL +
75
- canonical_resource_path + NL +
76
- canonical_query_string + NL +
77
- canonical_headers_string + NL +
78
- signed_headers_string + NL +
79
- request_payload_hash_hex
74
+ canonical_request = [method,
75
+ canonical_resource_path,
76
+ canonical_query_string,
77
+ canonical_headers_string,
78
+ signed_headers_string,
79
+ request_payload_hash_hex].join(NL)
80
80
 
81
- id = api_key.id + "/" + date_stamp + "/" + nonce + "/" + ID_TERMINATOR
81
+ id = [request.api_key.id, date_stamp, nonce, ID_TERMINATOR].join("/")
82
82
 
83
83
  canonical_request_hash_hex = to_hex(hash_text(canonical_request))
84
84
 
85
- string_to_sign = ALGORITHM + NL +
86
- time_stamp + NL +
87
- id + NL +
88
- canonical_request_hash_hex
85
+ string_to_sign = [ALGORITHM, time_stamp, id, canonical_request_hash_hex].join(NL)
89
86
 
90
87
  # SAuthc1 uses a series of derived keys, formed by hashing different pieces of data
91
- k_secret = to_utf8 AUTHENTICATION_SCHEME + api_key.secret
88
+ k_secret = to_utf8 AUTHENTICATION_SCHEME + request.api_key.secret
92
89
  k_date = sign date_stamp, k_secret, DEFAULT_ALGORITHM
93
90
  k_nonce = sign nonce, k_date, DEFAULT_ALGORITHM
94
91
  k_signing = sign ID_TERMINATOR, k_nonce, DEFAULT_ALGORITHM
@@ -102,7 +99,6 @@ module Stormpath
102
99
  create_name_value_pair(SAUTHC1_SIGNATURE, signature_hex)
103
100
 
104
101
  request.http_headers.store AUTHORIZATION_HEADER, authorization_header
105
-
106
102
  end
107
103
 
108
104
 
@@ -123,7 +119,7 @@ module Stormpath
123
119
  result
124
120
  end
125
121
 
126
- protected
122
+ private
127
123
 
128
124
  def canonicalize_query_string request
129
125
  request.to_s_query_string true
@@ -149,17 +145,11 @@ module Stormpath
149
145
  end
150
146
 
151
147
  def get_request_payload_without_query_params request
152
- result = ''
153
- if !request.body.nil?
154
- result = request.body
155
- end
156
- result
148
+ request.body || ''
157
149
  end
158
150
 
159
- private
160
-
161
151
  def create_name_value_pair name, value
162
- name + '=' + value
152
+ "#{name}=#{value}"
163
153
  end
164
154
 
165
155
  def canonicalize_resource_path resource_path
@@ -186,10 +176,10 @@ module Stormpath
186
176
  sorted_headers = request.http_headers.keys.sort!
187
177
  result = ''
188
178
  sorted_headers.each do |header|
189
- if !result.empty?
190
- result << ';' << header
191
- else
179
+ if result.empty?
192
180
  result << header
181
+ else
182
+ result << ';' << header
193
183
  end
194
184
  end
195
185
  result.downcase
@@ -199,4 +189,4 @@ module Stormpath
199
189
  end#Sauthc1Signer
200
190
  end#Authc
201
191
  end#Http
202
- end#Stormpath
192
+ end#Stormpath
@@ -19,9 +19,8 @@ module Stormpath
19
19
  include Stormpath::Http::Authc
20
20
  include Stormpath::Util::Assert
21
21
 
22
- def initialize(api_key, options = {})
22
+ def initialize(options = {})
23
23
  @signer = Sauthc1Signer.new
24
- @api_key = api_key
25
24
  @http_client = HTTPClient.new options[:proxy]
26
25
  end
27
26
 
@@ -30,7 +29,7 @@ module Stormpath
30
29
 
31
30
  @redirect_response = nil
32
31
 
33
- @signer.sign_request request, @api_key
32
+ @signer.sign_request request
34
33
 
35
34
  domain = if request.query_string.present?
36
35
  [request.href, request.to_s_query_string(true)].join '?'
@@ -50,20 +49,10 @@ module Stormpath
50
49
  end
51
50
 
52
51
  Response.new response.http_header.status_code,
53
- response.http_header.body_type,
54
- response.content,
55
- response.http_header.body_size
52
+ response.http_header.body_type,
53
+ response.content,
54
+ response.http_header.body_size
56
55
  end
57
-
58
- private
59
-
60
- def add_query_string href, query_string
61
- query_string.each do |key, value|
62
- prefix = if href.include? '?' then '&' else '?' end
63
- href << prefix << key.to_s << '=' << value.to_s
64
- end
65
- end
66
-
67
56
  end
68
57
  end
69
58
  end