C0 code coverage information

Generated on Fri Oct 31 16:37:32 -0700 2008 with rcov 0.7.0


Code reported as executed by Ruby looks like this...
and this: this line is also marked as covered.
Lines considered as run by rcov, but not reported by Ruby, look like this,
and this: these lines were inferred by rcov (using simple heuristics).
Finally, here's a line marked as not executed.
Name Total lines Lines of code Total coverage Code coverage
lib/openid/consumer/associationmanager.rb 340 231
99.4% 
99.1% 
  1 require "openid/dh"
  2 require "openid/util"
  3 require "openid/kvpost"
  4 require "openid/cryptutil"
  5 require "openid/protocolerror"
  6 require "openid/association"
  7 
  8 module OpenID
  9   class Consumer
 10 
 11     # A superclass for implementing Diffie-Hellman association sessions.
 12     class DiffieHellmanSession
 13       class << self
 14         attr_reader :session_type, :secret_size, :allowed_assoc_types,
 15           :hashfunc
 16       end
 17 
 18       def initialize(dh=nil)
 19         if dh.nil?
 20           dh = DiffieHellman.from_defaults
 21         end
 22         @dh = dh
 23       end
 24 
 25       # Return the query parameters for requesting an association
 26       # using this Diffie-Hellman association session
 27       def get_request
 28         args = {'dh_consumer_public' => CryptUtil.num_to_base64(@dh.public)}
 29         if (!@dh.using_default_values?)
 30           args['dh_modulus'] = CryptUtil.num_to_base64(@dh.modulus)
 31           args['dh_gen'] = CryptUtil.num_to_base64(@dh.generator)
 32         end
 33 
 34         return args
 35       end
 36 
 37       # Process the response from a successful association request and
 38       # return the shared secret for this association
 39       def extract_secret(response)
 40         dh_server_public64 = response.get_arg(OPENID_NS, 'dh_server_public',
 41                                               NO_DEFAULT)
 42         enc_mac_key64 = response.get_arg(OPENID_NS, 'enc_mac_key', NO_DEFAULT)
 43         dh_server_public = CryptUtil.base64_to_num(dh_server_public64)
 44         enc_mac_key = Util.from_base64(enc_mac_key64)
 45         return @dh.xor_secret(self.class.hashfunc,
 46                               dh_server_public, enc_mac_key)
 47       end
 48     end
 49 
 50     # A Diffie-Hellman association session that uses SHA1 as its hash
 51     # function
 52     class DiffieHellmanSHA1Session < DiffieHellmanSession
 53       @session_type = 'DH-SHA1'
 54       @secret_size = 20
 55       @allowed_assoc_types = ['HMAC-SHA1']
 56       @hashfunc = CryptUtil.method(:sha1)
 57     end
 58 
 59     # A Diffie-Hellman association session that uses SHA256 as its hash
 60     # function
 61     class DiffieHellmanSHA256Session < DiffieHellmanSession
 62       @session_type = 'DH-SHA256'
 63       @secret_size = 32
 64       @allowed_assoc_types = ['HMAC-SHA256']
 65       @hashfunc = CryptUtil.method(:sha256)
 66     end
 67 
 68     # An association session that does not use encryption
 69     class NoEncryptionSession
 70       class << self
 71         attr_reader :session_type, :allowed_assoc_types
 72       end
 73       @session_type = 'no-encryption'
 74       @allowed_assoc_types = ['HMAC-SHA1', 'HMAC-SHA256']
 75 
 76       def get_request
 77         return {}
 78       end
 79 
 80       def extract_secret(response)
 81         mac_key64 = response.get_arg(OPENID_NS, 'mac_key', NO_DEFAULT)
 82         return Util.from_base64(mac_key64)
 83       end
 84     end
 85 
 86     # An object that manages creating and storing associations for an
 87     # OpenID provider endpoint
 88     class AssociationManager
 89       def self.create_session(session_type)
 90         case session_type
 91         when 'no-encryption'
 92           NoEncryptionSession.new
 93         when 'DH-SHA1'
 94           DiffieHellmanSHA1Session.new
 95         when 'DH-SHA256'
 96           DiffieHellmanSHA256Session.new
 97         else
 98           raise ArgumentError, "Unknown association session type: "\
 99                                "#{session_type.inspect}"
100         end
101       end
102 
103       def initialize(store, server_url, compatibility_mode=false,
104                      negotiator=nil)
105         @store = store
106         @server_url = server_url
107         @compatibility_mode = compatibility_mode
108         @negotiator = negotiator || DefaultNegotiator
109       end
110 
111       def get_association
112         if @store.nil?
113           return nil
114         end
115 
116         assoc = @store.get_association(@server_url)
117         if assoc.nil? || assoc.expires_in <= 0
118           assoc = negotiate_association
119           if !assoc.nil?
120             @store.store_association(@server_url, assoc)
121           end
122         end
123 
124         return assoc
125       end
126 
127       def negotiate_association
128         assoc_type, session_type = @negotiator.get_allowed_type
129         begin
130           return request_association(assoc_type, session_type)
131         rescue ServerError => why
132           supported_types = extract_supported_association_type(why, assoc_type)
133           if !supported_types.nil?
134             # Attempt to create an association from the assoc_type and
135             # session_type that the server told us it supported.
136             assoc_type, session_type = supported_types
137             begin
138               return request_association(assoc_type, session_type)
139             rescue ServerError => why
140               Util.log("Server #{@server_url} refused its suggested " \
141                        "association type: session_type=#{session_type}, " \
142                        "assoc_type=#{assoc_type}")
143               return nil
144             end
145           end
146         end
147       end
148 
149       protected
150       def extract_supported_association_type(server_error, assoc_type)
151         # Any error message whose code is not 'unsupported-type' should
152         # be considered a total failure.
153         if (server_error.error_code != 'unsupported-type' or
154             server_error.message.is_openid1)
155           Util.log("Server error when requesting an association from "\
156                    "#{@server_url}: #{server_error.error_text}")
157           return nil
158         end
159 
160         # The server didn't like the association/session type that we
161         # sent, and it sent us back a message that might tell us how to
162         # handle it.
163         Util.log("Unsupported association type #{assoc_type}: "\
164                  "#{server_error.error_text}")
165 
166         # Extract the session_type and assoc_type from the error message
167         assoc_type = server_error.message.get_arg(OPENID_NS, 'assoc_type')
168         session_type = server_error.message.get_arg(OPENID_NS, 'session_type')
169 
170         if assoc_type.nil? or session_type.nil?
171           Util.log("Server #{@server_url} responded with unsupported "\
172                    "association session but did not supply a fallback.")
173           return nil
174         elsif !@negotiator.allowed?(assoc_type, session_type)
175           Util.log("Server sent unsupported session/association type: "\
176                    "session_type=#{session_type}, assoc_type=#{assoc_type}")
177           return nil
178         else
179           return [assoc_type, session_type]
180         end
181       end
182 
183       # Make and process one association request to this endpoint's OP
184       # endpoint URL. Returns an association object or nil if the
185       # association processing failed. Raises ServerError when the
186       # remote OpenID server returns an error.
187       def request_association(assoc_type, session_type)
188         assoc_session, args = create_associate_request(assoc_type, session_type)
189 
190         begin
191           response = OpenID.make_kv_post(args, @server_url)
192           return extract_association(response, assoc_session)
193         rescue HTTPStatusError => why
194           Util.log("Got HTTP status error when requesting association: #{why}")
195           return nil
196         rescue Message::KeyNotFound => why
197           Util.log("Missing required parameter in response from "\
198                    "#{@server_url}: #{why}")
199           return nil
200 
201         rescue ProtocolError => why
202           Util.log("Protocol error processing response from #{@server_url}: "\
203                    "#{why}")
204           return nil
205         end
206       end
207 
208       # Create an association request for the given assoc_type and
209       # session_type. Returns a pair of the association session object
210       # and the request message that will be sent to the server.
211       def create_associate_request(assoc_type, session_type)
212         assoc_session = self.class.create_session(session_type)
213         args = {
214           'mode' => 'associate',
215           'assoc_type' => assoc_type,
216         }
217 
218         if !@compatibility_mode
219           args['ns'] = OPENID2_NS
220         end
221 
222         # Leave out the session type if we're in compatibility mode
223         # *and* it's no-encryption.
224         if !@compatibility_mode ||
225             assoc_session.class.session_type != 'no-encryption'
226           args['session_type'] = assoc_session.class.session_type
227         end
228 
229         args.merge!(assoc_session.get_request)
230         message = Message.from_openid_args(args)
231         return assoc_session, message
232       end
233 
234       # Given an association response message, extract the OpenID 1.X
235       # session type. Returns the association type for this message
236       #
237       # This function mostly takes care of the 'no-encryption' default
238       # behavior in OpenID 1.
239       #
240       # If the association type is plain-text, this function will
241       # return 'no-encryption'
242       def get_openid1_session_type(assoc_response)
243         # If it's an OpenID 1 message, allow session_type to default
244         # to nil (which signifies "no-encryption")
245         session_type = assoc_response.get_arg(OPENID1_NS, 'session_type')
246 
247         # Handle the differences between no-encryption association
248         # respones in OpenID 1 and 2:
249 
250         # no-encryption is not really a valid session type for
251         # OpenID 1, but we'll accept it anyway, while issuing a
252         # warning.
253         if session_type == 'no-encryption'
254           Util.log("WARNING: #{@server_url} sent 'no-encryption'"\
255                    "for OpenID 1.X")
256 
257         # Missing or empty session type is the way to flag a
258         # 'no-encryption' response. Change the session type to
259         # 'no-encryption' so that it can be handled in the same
260         # way as OpenID 2 'no-encryption' respones.
261         elsif session_type == '' || session_type.nil?
262           session_type = 'no-encryption'
263         end
264 
265         return session_type
266       end
267 
268       def self.extract_expires_in(message)
269         # expires_in should be a base-10 string.
270         expires_in_str = message.get_arg(OPENID_NS, 'expires_in', NO_DEFAULT)
271         if !(/\A\d+\Z/ =~ expires_in_str)
272           raise ProtocolError, "Invalid expires_in field: #{expires_in_str}"
273         end
274         expires_in_str.to_i
275       end
276 
277       # Attempt to extract an association from the response, given the
278       # association response message and the established association
279       # session.
280       def extract_association(assoc_response, assoc_session)
281         # Extract the common fields from the response, raising an
282         # exception if they are not found
283         assoc_type = assoc_response.get_arg(OPENID_NS, 'assoc_type',
284                                             NO_DEFAULT)
285         assoc_handle = assoc_response.get_arg(OPENID_NS, 'assoc_handle',
286                                               NO_DEFAULT)
287         expires_in = self.class.extract_expires_in(assoc_response)
288 
289         # OpenID 1 has funny association session behaviour.
290         if assoc_response.is_openid1
291             session_type = get_openid1_session_type(assoc_response)
292         else
293           session_type = assoc_response.get_arg(OPENID2_NS, 'session_type',
294                                                 NO_DEFAULT)
295         end
296 
297         # Session type mismatch
298         if assoc_session.class.session_type != session_type
299           if (assoc_response.is_openid1 and session_type == 'no-encryption')
300             # In OpenID 1, any association request can result in a
301             # 'no-encryption' association response. Setting
302             # assoc_session to a new no-encryption session should
303             # make the rest of this function work properly for
304             # that case.
305             assoc_session = NoEncryptionSession.new
306           else
307             # Any other mismatch, regardless of protocol version
308             # results in the failure of the association session
309             # altogether.
310             raise ProtocolError, "Session type mismatch. Expected "\
311                                  "#{assoc_session.class.session_type}, got "\
312                                  "#{session_type}"
313           end
314         end
315 
316         # Make sure assoc_type is valid for session_type
317         if !assoc_session.class.allowed_assoc_types.member?(assoc_type)
318           raise ProtocolError, "Unsupported assoc_type for session "\
319                                "#{assoc_session.class.session_type} "\
320                                "returned: #{assoc_type}"
321         end
322 
323         # Delegate to the association session to extract the secret
324         # from the response, however is appropriate for that session
325         # type.
326         begin
327           secret = assoc_session.extract_secret(assoc_response)
328         rescue Message::KeyNotFound, ArgumentError => why
329           raise ProtocolError, "Malformed response for "\
330                                "#{assoc_session.class.session_type} "\
331                                "session: #{why.message}"
332         end
333 
334 
335         return Association.from_expires_in(expires_in, assoc_handle, secret,
336                                            assoc_type)
337       end
338     end
339   end
340 end

Generated using the rcov code coverage analysis tool for Ruby version 0.7.0.

Valid XHTML 1.0! Valid CSS!