ruby-saml-federa 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.document +5 -0
  2. data/.gitignore +10 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +12 -0
  5. data/LICENSE +19 -0
  6. data/README.md +124 -0
  7. data/Rakefile +41 -0
  8. data/lib/federa/ruby-saml/authrequest.rb +181 -0
  9. data/lib/federa/ruby-saml/coding.rb +34 -0
  10. data/lib/federa/ruby-saml/logging.rb +26 -0
  11. data/lib/federa/ruby-saml/logout_request.rb +126 -0
  12. data/lib/federa/ruby-saml/logout_response.rb +132 -0
  13. data/lib/federa/ruby-saml/metadata.rb +266 -0
  14. data/lib/federa/ruby-saml/request.rb +81 -0
  15. data/lib/federa/ruby-saml/response.rb +203 -0
  16. data/lib/federa/ruby-saml/settings.rb +28 -0
  17. data/lib/federa/ruby-saml/validation_error.rb +7 -0
  18. data/lib/federa/ruby-saml/version.rb +5 -0
  19. data/lib/ruby-saml-federa.rb +11 -0
  20. data/lib/schemas/saml20assertion_schema.xsd +283 -0
  21. data/lib/schemas/saml20protocol_schema.xsd +302 -0
  22. data/lib/schemas/xenc_schema.xsd +146 -0
  23. data/lib/schemas/xmldsig_schema.xsd +318 -0
  24. data/lib/xml_security.rb +165 -0
  25. data/ruby-saml-federa.gemspec +21 -0
  26. data/test/certificates/certificate1 +12 -0
  27. data/test/logoutrequest_test.rb +98 -0
  28. data/test/request_test.rb +53 -0
  29. data/test/response_test.rb +219 -0
  30. data/test/responses/adfs_response_sha1.xml +46 -0
  31. data/test/responses/adfs_response_sha256.xml +46 -0
  32. data/test/responses/adfs_response_sha384.xml +46 -0
  33. data/test/responses/adfs_response_sha512.xml +46 -0
  34. data/test/responses/no_signature_ns.xml +48 -0
  35. data/test/responses/open_saml_response.xml +56 -0
  36. data/test/responses/response1.xml.base64 +1 -0
  37. data/test/responses/response2.xml.base64 +79 -0
  38. data/test/responses/response3.xml.base64 +66 -0
  39. data/test/responses/response4.xml.base64 +93 -0
  40. data/test/responses/response5.xml.base64 +102 -0
  41. data/test/responses/response_with_ampersands.xml +139 -0
  42. data/test/responses/response_with_ampersands.xml.base64 +93 -0
  43. data/test/responses/simple_saml_php.xml +71 -0
  44. data/test/responses/wrapped_response_2.xml.base64 +150 -0
  45. data/test/settings_test.rb +43 -0
  46. data/test/test_helper.rb +66 -0
  47. data/test/xml_security_test.rb +123 -0
  48. metadata +155 -0
@@ -0,0 +1,126 @@
1
+ require 'uuid'
2
+
3
+ module Federa::Saml
4
+ class LogoutRequest
5
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
6
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
7
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
8
+
9
+ include Coding
10
+ include Request
11
+ attr_reader :transaction_id
12
+ attr_accessor :settings
13
+
14
+ def initialize( options = {} )
15
+ opt = { :request => nil, :settings => nil }.merge(options)
16
+ @settings = opt[:settings]
17
+ @issue_instant = Federa::Saml::LogoutRequest.timestamp
18
+ @request_params = Hash.new
19
+ # We need to generate a LogoutRequest to send to the IdP
20
+ if opt[:request].nil?
21
+ @transaction_id = UUID.new.generate
22
+ # The IdP sent us a LogoutRequest (IdP initiated SLO)
23
+ else
24
+ begin
25
+ @request = XMLSecurity::SignedDocument.new( decode( opt[:request] ))
26
+ raise if @request.nil?
27
+ raise if @request.root.nil?
28
+ raise if @request.root.namespace != PROTOCOL
29
+ rescue
30
+ @request = XMLSecurity::SignedDocument.new( inflate( decode( opt[:request] ) ) )
31
+ end
32
+ Logging.debug "LogoutRequest is: \n#{@request}"
33
+ end
34
+ end
35
+
36
+ def create( options = {} )
37
+ opt = { :name_id => nil, :session_index => nil, :extra_parameters => nil }.merge(options)
38
+ return nil unless opt[:name_id]
39
+
40
+ @request = REXML::Document.new
41
+ @request.context[:attribute_quote] = :quote
42
+
43
+
44
+ root = @request.add_element "saml2p:LogoutRequest", { "xmlns:saml2p" => PROTOCOL }
45
+ root.attributes['ID'] = @transaction_id
46
+ root.attributes['IssueInstant'] = @issue_instant
47
+ root.attributes['Version'] = "2.0"
48
+ root.attributes['Destination'] = @settings.single_logout_destination
49
+
50
+ issuer = root.add_element "saml2:Issuer", { "xmlns:saml2" => ASSERTION }
51
+ issuer.attributes['Format'] = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
52
+ #issuer.text = @settings.issuer
53
+ #per la federazione trentina qui ci vanno i metadati...
54
+ issuer.text = @settings.idp_metadata
55
+
56
+ name_id = root.add_element "saml2:NameID", { "xmlns:saml2" => ASSERTION }
57
+ name_id.attributes['Format'] = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
58
+ name_id.attributes['NameQualifier'] = @settings.idp_name_qualifier
59
+ name_id.text = opt[:name_id]
60
+ # I believe the rest of these are optional
61
+ if @settings && @settings.sp_name_qualifier
62
+ name_id.attributes["SPNameQualifier"] = @settings.sp_name_qualifier
63
+ end
64
+ if opt[:session_index]
65
+ session_index = root.add_element "saml2p:SessionIndex" #, { "xmlns:samlp" => PROTOCOL }
66
+ session_index.text = opt[:session_index]
67
+ end
68
+ Logging.debug "Created LogoutRequest: #{@request}"
69
+ meta = Metadata.new(@settings)
70
+ return meta.create_slo_request( to_s, opt[:extra_parameters] )
71
+ #action, content = binding_select("SingleLogoutService")
72
+ #Logging.debug "action: #{action} content: #{content}"
73
+ #return [action, content]
74
+ end
75
+
76
+ # function to return the created request as an XML document
77
+ def to_xml
78
+ text = ""
79
+ @request.write(text, 1)
80
+ return text
81
+ end
82
+ def to_s
83
+ @request.to_s
84
+ end
85
+ # Functions for pulling values out from an IdP initiated LogoutRequest
86
+ def name_id
87
+ element = REXML::XPath.first(@request, "/p:LogoutRequest/a:NameID", {
88
+ "p" => PROTOCOL, "a" => ASSERTION } )
89
+ return nil if element.nil?
90
+ # Can't seem to get this to work right...
91
+ #element.context[:compress_whitespace] = ["NameID"]
92
+ #element.context[:compress_whitespace] = :all
93
+ str = element.text.gsub(/^\s+/, "")
94
+ str.gsub!(/\s+$/, "")
95
+ return str
96
+ end
97
+
98
+ def transaction_id
99
+ return @transaction_id if @transaction_id
100
+ element = REXML::XPath.first(@request, "/p:LogoutRequest", {
101
+ "p" => PROTOCOL} )
102
+ return nil if element.nil?
103
+ return element.attributes["ID"]
104
+ end
105
+ def is_valid?
106
+ validate(soft = true)
107
+ end
108
+
109
+ def validate!
110
+ validate( soft = false )
111
+ end
112
+ def validate( soft = true )
113
+ return false if @request.nil?
114
+ return false if @request.validate(@settings, soft) == false
115
+
116
+ return true
117
+
118
+ end
119
+ private
120
+
121
+ def self.timestamp
122
+ Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
123
+ end
124
+
125
+ end
126
+ end
@@ -0,0 +1,132 @@
1
+ #encoding: utf-8
2
+
3
+ require "rexml/document"
4
+
5
+ module Federa
6
+ module Saml
7
+ class LogoutResponse
8
+ include Coding
9
+ include Request
10
+ ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
11
+ PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
12
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
13
+
14
+ def initialize( options = { } )
15
+ opt = { :response => nil, :settings => nil }.merge(options)
16
+ # We've recieved a LogoutResponse from the IdP
17
+ if opt[:response]
18
+ begin
19
+ @response = XMLSecurity::SignedDocument.new(decode( opt[:response] ))
20
+ # Check to see if we have a root tag using the "protocol" namespace.
21
+ # If not, it means this is deflated text and we need to raise to
22
+ # the rescue below
23
+ raise if @response.nil?
24
+ raise if @response.root.nil?
25
+ raise if @response.root.namespace != PROTOCOL
26
+ document
27
+ rescue
28
+ @response = XMLSecurity::SignedDocument.new( inflate(decode( opt[:response] ) ) )
29
+ end
30
+ end
31
+ # We plan to create() a new LogoutResponse
32
+ if opt[:settings]
33
+ @settings = opt[:settings]
34
+ end
35
+ end
36
+
37
+ # Create a LogoutResponse to to the IdP's LogoutRequest
38
+ # (For IdP initiated SLO)
39
+ def create( options )
40
+ opt = { :transaction_id => nil,
41
+ :in_response_to => nil,
42
+ :status => "urn:oasis:names:tc:SAML:2.0:status:Success",
43
+ :extra_parameters => nil }.merge(options)
44
+ return nil if opt[:transaction_id].nil?
45
+ @response = REXML::Document.new
46
+ @response.context[:attribute_quote] = :quote
47
+ uuid = "_" + UUID.new.generate
48
+ time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
49
+ root = @response.add_element "saml2p:LogoutResponse", { "xmlns:saml2p" => PROTOCOL }
50
+ root.attributes['ID'] = uuid
51
+ root.attributes['IssueInstant'] = time
52
+ root.attributes['Version'] = "2.0"
53
+ # Just convenient naming to accept both names as InResponseTo
54
+ if opt[:transaction_id]
55
+ root.attributes['InResponseTo'] = opt[:transaction_id]
56
+ elsif opt[:in_response_to]
57
+ root.attributes['InResponseTo'] = opt[:in_response_to]
58
+ end
59
+ if opt[:status]
60
+ status = root.add_element "saml2p:Status"
61
+ status_code = status.add_element "saml2p:StatusCode", {
62
+ "Value" => opt[:status]
63
+ }
64
+ end
65
+ if @settings && @settings.issuer
66
+ issuer = root.add_element "saml:Issuer", {
67
+ "xmlns:saml" => "urn:oasis:names:tc:SAML:2.0:assertion"
68
+ }
69
+ issuer.text = @settings.issuer
70
+ end
71
+ meta = Metadata.new( @settings )
72
+ Logging.debug "Created LogoutResponse:\n#{@response}"
73
+ return meta.create_slo_response( to_s, opt[:extra_parameters] )
74
+
75
+ #root.attributes['Destination'] = action
76
+
77
+ end
78
+ # function to return the created request as an XML document
79
+ def to_xml
80
+ text = ""
81
+ @response.write(text, 1)
82
+ return text
83
+ end
84
+ def to_s
85
+ @response.to_s
86
+ end
87
+
88
+ def issuer
89
+ element = REXML::XPath.first(@response, "/p:LogoutResponse/a:Issuer", {
90
+ "p" => PROTOCOL, "a" => ASSERTION} )
91
+ return nil if element.nil?
92
+ element.text
93
+ end
94
+
95
+ def in_response_to
96
+ element = REXML::XPath.first(@response, "/p:LogoutResponse", {
97
+ "p" => PROTOCOL })
98
+ return nil if element.nil?
99
+ element.attributes["InResponseTo"]
100
+ end
101
+
102
+ def success?
103
+ element = REXML::XPath.first(@response, "/p:LogoutResponse/p:Status/p:StatusCode", {
104
+ "p" => PROTOCOL })
105
+ return false if element.nil?
106
+ element.attributes["Value"] == "urn:oasis:names:tc:SAML:2.0:status:Success"
107
+
108
+ end
109
+ def is_valid?
110
+ validate(soft = true)
111
+ end
112
+
113
+ def validate!
114
+ validate( soft = false )
115
+ end
116
+ def validate( soft = true )
117
+ return false if @response.nil?
118
+ # Skip validation with a failed response if we don't have settings
119
+ return false if @settings.nil?
120
+ return false if @response.validate(@settings, soft) == false
121
+
122
+ return true
123
+
124
+ end
125
+
126
+ protected
127
+ def document
128
+ REXML::Document.new(@response)
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,266 @@
1
+ require "rexml/document"
2
+ require "rexml/xpath"
3
+ require "net/https"
4
+ require "uri"
5
+ require "digest/md5"
6
+
7
+ # Class to return SP metadata based on the settings requested.
8
+ # Return this XML in a controller, then give that URL to the the
9
+ # IdP administrator. The IdP will poll the URL and your settings
10
+ # will be updated automatically
11
+ module Federa
12
+ module Saml
13
+ class Metadata
14
+ include REXML
15
+ include Coding
16
+ # a few symbols for SAML class names
17
+ HTTP_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
18
+ HTTP_GET = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
19
+
20
+ def initialize(settings=nil)
21
+ if settings
22
+ @settings = settings
23
+ end
24
+ end
25
+
26
+ def generate(settings)
27
+ meta_doc = REXML::Document.new
28
+ root = meta_doc.add_element "md:EntityDescriptor", {
29
+ "xmlns:md" => "urn:oasis:names:tc:SAML:2.0:metadata",
30
+ "xmlns:xml" => "http://www.w3.org/XML/1998/namespace",
31
+ "cacheDuration" => "P1M"
32
+ }
33
+ if settings.issuer != nil
34
+ root.attributes["entityID"] = settings.issuer
35
+ end
36
+ sp_sso = root.add_element "md:SPSSODescriptor", {
37
+ "protocolSupportEnumeration" => "urn:oasis:names:tc:SAML:2.0:protocol",
38
+ "WantAssertionsSigned" => "true"
39
+
40
+ }
41
+ name_identifier_formats = settings.name_identifier_format
42
+ if name_identifier_formats != nil
43
+ name_id = []
44
+ name_identifier_formats.each_with_index{ |format, index|
45
+ name_id[index] = sp_sso.add_element "md:NameIDFormat"
46
+ name_id[index].text = format
47
+ }
48
+
49
+ end
50
+ if settings.sp_cert != nil
51
+ keyDescriptor = sp_sso.add_element "md:KeyDescriptor", {
52
+ "use" => "signing"
53
+ }
54
+ keyInfo = keyDescriptor.add_element "ds:KeyInfo", {
55
+ "xmlns:ds" => "http://www.w3.org/2000/09/xmldsig#"
56
+ }
57
+ x509Data = keyInfo.add_element "ds:X509Data"
58
+ x509Certificate = x509Data.add_element "ds:X509Certificate"
59
+ file = ""
60
+ File.foreach(settings.sp_cert){ |line|
61
+ file += line unless (line.include?("RSA PUBLIC KEY") || line.include?("CERTIFICATE"))
62
+ }
63
+ x509Certificate.text = file
64
+ end
65
+ if settings.assertion_consumer_service_url != nil
66
+ sp_sso.add_element "md:AssertionConsumerService", {
67
+ # Add this as a setting to create different bindings?
68
+ "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
69
+ "Location" => settings.assertion_consumer_service_url,
70
+ "index" => "1"
71
+ }
72
+ end
73
+ if settings.single_logout_service_url != nil
74
+ sp_sso.add_element "md:SingleLogoutService", {
75
+ "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect",
76
+ "Location" => settings.single_logout_service_url
77
+ }
78
+ sp_sso.add_element "md:SingleLogoutService", {
79
+ "Binding" => "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
80
+ "Location" => settings.single_logout_service_url
81
+ }
82
+ end
83
+ meta_doc << REXML::XMLDecl.new(version='1.0', encoding='UTF-8')
84
+ ret = ""
85
+ # pretty print the XML so IdP administrators can easily see what the SP supports
86
+ meta_doc.write(ret, 1)
87
+
88
+ #Logging.debug "Generated metadata:\n#{ret}"
89
+
90
+ return ret
91
+
92
+ end
93
+
94
+ def create_sso_request(message, extra_parameters = {} )
95
+ build_message( :type => "SAMLRequest",
96
+ :service => "SingleSignOnService",
97
+ :message => message, :extra_parameters => extra_parameters)
98
+ end
99
+ def create_sso_response(message, extra_parameters = {} )
100
+ build_message( :type => "SAMLResponse",
101
+ :service => "SingleSignOnService",
102
+ :message => message, :extra_parameters => extra_parameters)
103
+ end
104
+ def create_slo_request(message, extra_parameters = {} )
105
+ build_message( :type => "SAMLRequest",
106
+ :service => "SingleLogoutService",
107
+ :message => message, :extra_parameters => extra_parameters)
108
+ end
109
+ def create_slo_response(message, extra_parameters = {} )
110
+ build_message( :type => "SAMLResponse",
111
+ :service => "SingleLogoutService",
112
+ :message => message, :extra_parameters => extra_parameters)
113
+ end
114
+
115
+ # Construct a SAML message using information in the IdP metadata.
116
+ # :type can be either "SAMLRequest" or "SAMLResponse"
117
+ # :service refers to the Binding method,
118
+ # either "SingleLogoutService" or "SingleSignOnService"
119
+ # :message is the SAML message itself (XML)
120
+ # I've provided easy to use wrapper functions above
121
+ def build_message( options = {} )
122
+ opt = { :type => nil, :service => nil, :message => nil, :extra_parameters => nil }.merge(options)
123
+ url = binding_select( opt[:service] )
124
+ return message_get( opt[:type], url, opt[:message], opt[:extra_parameters] )
125
+ end
126
+
127
+ # get the IdP metadata, and select the appropriate SSO binding
128
+ # that we can support. Currently this is HTTP-Redirect and HTTP-POST
129
+ # but more could be added in the future
130
+ def binding_select(service)
131
+ # first check if we're still using the old hard coded method for
132
+ # backwards compatability
133
+ if service == "SingleSignOnService" && @settings.idp_metadata == nil && @settings.idp_sso_target_url != nil
134
+ return @settings.idp_sso_target_url
135
+ end
136
+ if service == "SingleLogoutService" && @settings.idp_metadata == nil && @settings.idp_slo_target_url != nil
137
+ return @settings.idp_slo_target_url
138
+ end
139
+
140
+ meta_doc = get_idp_metadata
141
+
142
+ return nil unless meta_doc
143
+ # first try POST
144
+ sso_element = REXML::XPath.first(meta_doc, "/md:EntityDescriptor/md:IDPSSODescriptor/md:#{service}[@Binding='#{HTTP_POST}']")
145
+ if !sso_element.nil?
146
+ @URL = sso_element.attributes["Location"]
147
+ #Logging.debug "binding_select: POST to #{@URL}"
148
+ return @URL
149
+ end
150
+
151
+ # next try GET
152
+ sso_element = REXML::XPath.first(meta_doc, "/md:EntityDescriptor/md:IDPSSODescriptor/md:#{service}[@Binding='#{HTTP_GET}']")
153
+ if !sso_element.nil?
154
+ @URL = sso_element.attributes["Location"]
155
+ Logging.debug "binding_select: GET from #{@URL}"
156
+ return @URL
157
+ end
158
+ # other types we might want to add in the future: SOAP, Artifact
159
+ end
160
+
161
+ # Retrieve the remote IdP metadata from the URL or a cached copy
162
+ # returns a REXML document of the metadata
163
+ def get_idp_metadata
164
+ return false if @settings.idp_metadata.nil?
165
+
166
+ # Look up the metdata in cache first
167
+ id = Digest::MD5.hexdigest(@settings.idp_metadata)
168
+
169
+ uri = URI.parse(@settings.idp_metadata)
170
+ if uri.scheme == "http"
171
+ response = Net::HTTP.get_response(uri)
172
+ meta_text = response.body
173
+ elsif uri.scheme == "https"
174
+ http = Net::HTTP.new(uri.host, uri.port)
175
+ http.use_ssl = true
176
+ # Most IdPs will probably use self signed certs
177
+ #http.verify_mode = OpenSSL::SSL::VERIFY_PEER
178
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
179
+ get = Net::HTTP::Get.new(uri.request_uri)
180
+ response = http.request(get)
181
+ meta_text = response.body
182
+ end
183
+ testo_response = meta_text.sub!(' xmlns:xml="http://www.w3.org/XML/1998/namespace"', '')
184
+ doc = REXML::Document.new(testo_response)
185
+ extract_certificate(doc)
186
+ return doc
187
+
188
+ # USE OF CACHE WITH CERTIFICATE
189
+ # lookup = @cache.read(id)
190
+ # if lookup != nil
191
+ # Logging.debug "IdP metadata cached lookup for #{@settings.idp_metadata}"
192
+ # doc = REXML::Document.new( lookup )
193
+ # extract_certificate( doc )
194
+ # return doc
195
+ # end
196
+
197
+ # Logging.debug "IdP metadata cache miss on #{@settings.idp_metadata}"
198
+ # # cache miss
199
+ # if File.exists?(@settings.idp_metadata)
200
+ # fp = File.open( @settings.idp_metadata, "r")
201
+ # meta_text = fp.read
202
+ # else
203
+ # uri = URI.parse(@settings.idp_metadata)
204
+ # if uri.scheme == "http"
205
+ # response = Net::HTTP.get_response(uri)
206
+ # meta_text = response.body
207
+ # elsif uri.scheme == "https"
208
+ # http = Net::HTTP.new(uri.host, uri.port)
209
+ # http.use_ssl = true
210
+ # # Most IdPs will probably use self signed certs
211
+ # #http.verify_mode = OpenSSL::SSL::VERIFY_PEER
212
+ # http.verify_mode = OpenSSL::SSL::VERIFY_NONE
213
+ # get = Net::HTTP::Get.new(uri.request_uri)
214
+ # response = http.request(get)
215
+ # meta_text = response.body
216
+ # end
217
+ # end
218
+ # # Add it to the cache
219
+ # @cache.write(id, meta_text, @settings.idp_metadata_ttl )
220
+ # doc = REXML::Document.new( meta_text )
221
+ # extract_certificate(doc)
222
+ # return doc
223
+ end
224
+
225
+ def extract_certificate(meta_doc)
226
+
227
+ # pull out the x509 tag
228
+ x509 = REXML::XPath.first(meta_doc, "/md:EntityDescriptor/md:IDPSSODescriptor"+"/md:KeyDescriptor"+"/ds:KeyInfo/ds:X509Data/ds:X509Certificate"
229
+ )
230
+ # If the IdP didn't specify the use attribute
231
+ if x509.nil?
232
+ x509 = REXML::XPath.first(meta_doc,
233
+ "/EntityDescriptor/IDPSSODescriptor" +
234
+ "/KeyDescriptor" +
235
+ "/ds:KeyInfo/ds:X509Data/ds:X509Certificate"
236
+ )
237
+ end
238
+ @settings.idp_cert = x509.text.gsub(/\n/, "").gsub(/\t/, "")
239
+ end
240
+
241
+ # construct the parameter list on the URL and return
242
+ def message_get( type, url, message, extra_parameters = {} )
243
+ params = Hash.new
244
+ if extra_parameters
245
+ params.merge!(extra_parameters)
246
+ end
247
+ # compress GET requests to try and stay under that 8KB request limit
248
+ #deflate of samlrequest
249
+ params[type] = encode( deflate( message ) )
250
+ #Logging.debug "#{type}=#{params[type]}"
251
+
252
+ uri = Addressable::URI.parse(url)
253
+ if uri.query_values == nil
254
+ uri.query_values = params
255
+ else
256
+ # solution to stevenwilkin's parameter merge
257
+ uri.query_values = params.merge(uri.query_values)
258
+ end
259
+ url = uri.to_s
260
+ #Logging.debug "Sending to URL #{url}"
261
+ return url
262
+ end
263
+
264
+ end
265
+ end
266
+ end